For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 为 RatingRecommend 项目实现一个无状态计算型 REST 接口——接收一批卡识别特征,逐卡按规则推荐评级时效(普通/快速/闪评),按时效精确分组为 N 个订单建议,附带每卡单价、订单数量与总计(价格走订单服务,预留口子+降级)。
Architecture: 沿用 mango 多模块五层(Controller→Service→Manager→Mapper→PO),新增 1 张只读规则表。匹配降级(全等→L2 series+cardSet+year→L3 series+cardSet→兜底)由 Service 编排 + 3 个精确 SQL;分组抽成无 Spring 依赖的纯类 OrderGrouper,TDD 驱动;价格抽象 EfficiencyPriceProvider 接口,本期实现预留调订单服务的 Feign 口子并支持降级。
Tech Stack: Spring Boot 2.2.5、PostgreSQL、MyBatis(手写 XML)、Lombok、Swagger 注解、JUnit5 + Mockito。
| # | 决策 | 备注 |
|---|---|---|
| D1 | 时效统一用 EvaluateEfficiencyEnum.code(Integer) |
普通=1 / 快速=2 / 闪评=3 |
| D2 | 匹配策略:全等优先 + 系列主导模糊降级 + 兜底 | 命中即止 |
| D3 | 降级查询:多次精确 SQL(Mapper 提供 3 个查询) | 走索引、DB 友好 |
| D4 | 分组:同时效精确分组(纯类 OrderGrouper,TDD) |
容差聚类后续可扩展 |
| D5 | 价格来源:调订单服务(order-service),预留 Feign 口子 | 本期降级,未接通则 unitPrice/totalAmount = null("待计算") |
| D6 | 规则表只读 | 不产生任何 INSERT/UPDATE/DELETE 语句 |
t_rating_recommend_rule),由大侠在评审后手动执行;本计划只提供脚本文本,执行器不主动对数据库运行任何写操作。mango-common
enums/MatchLevelEnum.javapo/RatingRecommendRulePO.javarequest/recommend/RecommendCardDTO.java、request/recommend/RecommendRequest.javaresponse/recommend/RecommendCardVO.java、response/recommend/OrderSuggestionVO.java、response/recommend/RecommendResultVO.javamango-infrastructure
mapper/RatingRecommendRuleMapper.java + resources/mapper/RatingRecommendRuleMapper.xml(仅 SELECT)mango-manager
manager/RatingRecommendRuleManager.java + manager/impl/RatingRecommendRuleManagerImpl.javamango-domain
service/recommend/OrderGrouper.java(纯类)service/recommend/EfficiencyPriceProvider.java + service/recommend/impl/EfficiencyPriceProviderImpl.javaservice/RecommendService.java + service/impl/RecommendServiceImpl.javamango-application
RatingRecommendApplication.java(启用数据源)app/controller/RecommendController.javaconfig/RecommendProperties.javaresources/application.yml(新增 recommend.* 配置)src/test/java/.../recommend/OrderGrouperTest.java、RecommendServiceImplTest.javaDDL
docs/superpowers/specs/ddl/2026-06-24-rating-recommend-rule.sqlOrderGrouper:纯 JUnit5 TDD,覆盖:空 / 单卡 / 全同档 / 多档乱序 / 3 档同时存在。毫秒级运行。RecommendServiceImpl:Mockito 单测,覆盖:全等命中 / L2 命中 / L3 命中 / 兜底 / 非法规则值兜底 / 缺字段跳层 / 价格命中 / 价格缺失降级 / 空输入。mvn -pl mango-application -am test -Dtest=OrderGrouperTestmvn -q -DskipTests clean install产出:项目启动时正常装配 Druid + PostgreSQL,规则表 Mapper 后续可被调用。
Files:
Modify: mango-application/src/main/java/com/mangoo/rating/recommend/RatingRecommendApplication.java
[ ] Step 1: 查看当前启动类
Run: mvn -q -pl mango-application -am -DskipTests compile
Expected: BUILD SUCCESS(先确认基线可编译)
DataSourceAutoConfiguration将启动类上原有的:
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, SecurityAutoConfiguration.class})
改为:
// 原排除 DataSourceAutoConfiguration 已去除:评级时效推荐功能需要查询规则表,必须启用数据源(PostgreSQL)
@SpringBootApplication(exclude = {SecurityAutoConfiguration.class})
并删除不再需要的 import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;(如果存在)。
Run: mvn -q -pl mango-application -am -DskipTests compile
Expected: BUILD SUCCESS
Run: mvn -q -pl mango-application -am spring-boot:run
Expected: 启动日志含 Druid / HikariPool 之类数据源装配信息,应用端口 8094 监听成功,无 SQLException。
⚠️ 若启动失败(数据源连接异常),需先确认
application.yml中spring.datasource.url/账号密码可用(默认192.168.77.80:5432/mongo_grade/postgres / 123456)。
[ ] Step 5: 提交
git add mango-application/src/main/java/com/mangoo/rating/recommend/RatingRecommendApplication.java
git commit -m "chore(boot): 启用数据源自动装配(评级推荐功能前置)"
产出:可只读访问规则数据的 PO、Mapper、枚举、建表脚本。
MatchLevelEnumFiles:
Create: mango-common/src/main/java/com/mangoo/rating/recommend/enums/MatchLevelEnum.java
[ ] Step 1: 编写枚举(照 EvaluateEfficiencyEnum 范式)
package com.mangoo.rating.recommend.enums;
import lombok.Getter;
/**
* 时效推荐命中层级枚举
* EXACT:四字段全等命中
* L2:series + cardSet + year 命中
* L3:series + cardSet 命中
* DEFAULT:未命中,走兜底默认档
*
* @author gengjintao
* @date 2026/06/24
*/
@Getter
public enum MatchLevelEnum {
EXACT("EXACT", "四字段全等命中"),
L2("L2", "系列+卡种+年份命中"),
L3("L3", "系列+卡种命中"),
DEFAULT("DEFAULT", "兜底默认档");
private final String code;
private final String desc;
MatchLevelEnum(String code, String desc) {
this.code = code;
this.desc = desc;
}
}
[ ] Step 2: 编译验证
Run: mvn -q -pl mango-common -am -DskipTests compile
Expected: BUILD SUCCESS
[ ] Step 3: 提交
git add mango-common/src/main/java/com/mangoo/rating/recommend/enums/MatchLevelEnum.java
git commit -m "feat(recommend): 新增命中层级枚举 MatchLevelEnum"
RatingRecommendRulePOFiles:
Create: mango-common/src/main/java/com/mangoo/rating/recommend/po/RatingRecommendRulePO.java
[ ] Step 1: 编写 PO(照 RecognizedInfoPO 范式)
package com.mangoo.rating.recommend.po;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 评级时效推荐规则 PO(数仓沉淀,本期只读查询;命中键:player+year+series+cardSet)
*
* @author gengjintao
* @date 2026/06/24
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RatingRecommendRulePO {
@ApiModelProperty(value = "主键ID")
private Long id;
@ApiModelProperty(value = "球员")
private String player;
@ApiModelProperty(value = "年份")
private String year;
@ApiModelProperty(value = "系列")
private String series;
@ApiModelProperty(value = "卡种")
private String cardSet;
@ApiModelProperty(value = "推荐时效(EvaluateEfficiencyEnum.code)")
private Integer recommendEfficiency;
@ApiModelProperty(value = "规则生效时间")
private LocalDateTime effectiveTime;
@ApiModelProperty(value = "创建时间")
private LocalDateTime createTime;
@ApiModelProperty(value = "修改时间")
private LocalDateTime updateTime;
@ApiModelProperty(value = "删除标识(0-未删,1-已删)")
private Integer delFlag;
}
[ ] Step 2: 编译验证
Run: mvn -q -pl mango-common -am -DskipTests compile
Expected: BUILD SUCCESS
[ ] Step 3: 提交
git add mango-common/src/main/java/com/mangoo/rating/recommend/po/RatingRecommendRulePO.java
git commit -m "feat(recommend): 新增推荐规则 PO RatingRecommendRulePO"
Files:
mango-infrastructure/src/main/java/com/mangoo/rating/recommend/mapper/RatingRecommendRuleMapper.javaCreate: mango-infrastructure/src/main/resources/mapper/RatingRecommendRuleMapper.xml
[ ] Step 1: 编写 Mapper 接口(提供 L1/L2/L3 三个精确查询)
package com.mangoo.rating.recommend.mapper;
import com.mangoo.rating.recommend.po.RatingRecommendRulePO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* 评级时效推荐规则 Mapper(本期仅只读查询)
*
* @author gengjintao
* @date 2026/06/24
*/
@Mapper
public interface RatingRecommendRuleMapper {
/**
* L1:四字段全等命中(取最新生效)
*/
RatingRecommendRulePO selectByExact(@Param("player") String player,
@Param("year") String year,
@Param("series") String series,
@Param("cardSet") String cardSet);
/**
* L2:series + cardSet + year 命中(player 放宽)
*/
RatingRecommendRulePO selectBySeriesSetYear(@Param("series") String series,
@Param("cardSet") String cardSet,
@Param("year") String year);
/**
* L3:series + cardSet 命中(player + year 放宽)
*/
RatingRecommendRulePO selectBySeriesSet(@Param("series") String series,
@Param("cardSet") String cardSet);
}
[ ] Step 2: 编写 Mapper XML
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mangoo.rating.recommend.mapper.RatingRecommendRuleMapper">
<resultMap type="com.mangoo.rating.recommend.po.RatingRecommendRulePO" id="BaseResultMap">
<result property="id" column="id"/>
<result property="player" column="player"/>
<result property="year" column="year"/>
<result property="series" column="series"/>
<result property="cardSet" column="card_set"/>
<result property="recommendEfficiency" column="recommend_efficiency"/>
<result property="effectiveTime" column="effective_time"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
<result property="delFlag" column="del_flag"/>
</resultMap>
<sql id="baseColumnList">
id, player, year, series, card_set, recommend_efficiency, effective_time,
create_time, update_time, del_flag
</sql>
<!-- L1:四字段全等命中(取最新生效) -->
<select id="selectByExact" resultMap="BaseResultMap">
SELECT <include refid="baseColumnList"/>
FROM t_rating_recommend_rule
WHERE del_flag = 0
AND player = #{player}
AND year = #{year}
AND series = #{series}
AND card_set = #{cardSet}
AND (effective_time IS NULL OR effective_time <= now())
ORDER BY effective_time DESC NULLS LAST, id DESC
LIMIT 1
</select>
<!-- L2:系列+卡种+年份命中 -->
<select id="selectBySeriesSetYear" resultMap="BaseResultMap">
SELECT <include refid="baseColumnList"/>
FROM t_rating_recommend_rule
WHERE del_flag = 0
AND series = #{series}
AND card_set = #{cardSet}
AND year = #{year}
AND (effective_time IS NULL OR effective_time <= now())
ORDER BY effective_time DESC NULLS LAST, id DESC
LIMIT 1
</select>
<!-- L3:系列+卡种命中 -->
<select id="selectBySeriesSet" resultMap="BaseResultMap">
SELECT <include refid="baseColumnList"/>
FROM t_rating_recommend_rule
WHERE del_flag = 0
AND series = #{series}
AND card_set = #{cardSet}
AND (effective_time IS NULL OR effective_time <= now())
ORDER BY effective_time DESC NULLS LAST, id DESC
LIMIT 1
</select>
</mapper>
[ ] Step 3: 编译验证
Run: mvn -q -pl mango-infrastructure -am -DskipTests compile
Expected: BUILD SUCCESS
[ ] Step 4: 提交
git add mango-infrastructure/src/main/java/com/mangoo/rating/recommend/mapper/RatingRecommendRuleMapper.java mango-infrastructure/src/main/resources/mapper/RatingRecommendRuleMapper.xml
git commit -m "feat(recommend): 新增推荐规则 Mapper 与 XML(L1/L2/L3 三层精确查询)"
Files:
docs/superpowers/specs/ddl/2026-06-24-rating-recommend-rule.sql此脚本不由执行器运行。 仅生成文本,交大侠评审后在目标库手动执行。字段口径与 PO/XML 完全对应。
[ ] Step 1: 编写 DDL 脚本
-- ============================================================
-- 评级时效推荐规则表 建表脚本(PostgreSQL)
-- ⚠️ 需大侠评审确认后手动执行;本期只读查询、不接受任何写入
-- 2026-06-24
-- ============================================================
CREATE TABLE IF NOT EXISTS t_rating_recommend_rule (
id BIGSERIAL PRIMARY KEY,
player VARCHAR(255) NOT NULL,
year VARCHAR(64) NOT NULL,
series VARCHAR(255) NOT NULL,
card_set VARCHAR(255) NOT NULL,
recommend_efficiency SMALLINT NOT NULL,
effective_time TIMESTAMP,
create_time TIMESTAMP NOT NULL DEFAULT now(),
update_time TIMESTAMP NOT NULL DEFAULT now(),
del_flag SMALLINT NOT NULL DEFAULT 0
);
COMMENT ON TABLE t_rating_recommend_rule IS '评级时效推荐规则表(数仓沉淀,只读查询)';
-- 支撑 L2/L3 系列主导降级查询
CREATE INDEX IF NOT EXISTS idx_rrr_series_set_year ON t_rating_recommend_rule (series, card_set, year);
-- 支撑 L1 全等命中查询
CREATE INDEX IF NOT EXISTS idx_rrr_feature ON t_rating_recommend_rule (player, year, series, card_set);
[ ] Step 2: 提交脚本(仅文本,不执行)
git add docs/superpowers/specs/ddl/2026-06-24-rating-recommend-rule.sql
git commit -m "docs(recommend): 新增推荐规则表 DDL 脚本(待评审执行)"
[ ] Step 3: 🛑 检查点——请大侠确认 DDL 后,在目标库手动执行,再继续 S2。
RatingRecommendRuleManager 接口 + ImplFiles:
mango-manager/src/main/java/com/mangoo/rating/recommend/manager/RatingRecommendRuleManager.javaCreate: mango-manager/src/main/java/com/mangoo/rating/recommend/manager/impl/RatingRecommendRuleManagerImpl.java
[ ] Step 1: 编写接口
package com.mangoo.rating.recommend.manager;
import com.mangoo.rating.recommend.po.RatingRecommendRulePO;
/**
* 评级时效推荐规则 Manager(封装 Mapper,做空值校验与策略上抛)
*
* @author gengjintao
* @date 2026/06/24
*/
public interface RatingRecommendRuleManager {
/**
* L1:四字段全等命中(任一为空则不查询,返回 null)
*/
RatingRecommendRulePO selectByExact(String player, String year, String series, String cardSet);
/**
* L2:系列+卡种+年份命中(三个参与字段任一为空则不查询)
*/
RatingRecommendRulePO selectBySeriesSetYear(String series, String cardSet, String year);
/**
* L3:系列+卡种命中(两个参与字段任一为空则不查询)
*/
RatingRecommendRulePO selectBySeriesSet(String series, String cardSet);
}
[ ] Step 2: 编写实现
package com.mangoo.rating.recommend.manager.impl;
import com.mangoo.rating.recommend.manager.RatingRecommendRuleManager;
import com.mangoo.rating.recommend.mapper.RatingRecommendRuleMapper;
import com.mangoo.rating.recommend.po.RatingRecommendRulePO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 评级时效推荐规则 Manager 实现
*
* @author gengjintao
* @date 2026/06/24
*/
@Slf4j
@Component
public class RatingRecommendRuleManagerImpl implements RatingRecommendRuleManager {
@Resource
private RatingRecommendRuleMapper ratingRecommendRuleMapper;
@Override
public RatingRecommendRulePO selectByExact(String player, String year, String series, String cardSet) {
// L1 参与字段全部不为空才查询(避免无意义 SQL)
if (isBlank(player) || isBlank(year) || isBlank(series) || isBlank(cardSet)) {
return null;
}
return ratingRecommendRuleMapper.selectByExact(player, year, series, cardSet);
}
@Override
public RatingRecommendRulePO selectBySeriesSetYear(String series, String cardSet, String year) {
// L2 三个参与字段不能为空
if (isBlank(series) || isBlank(cardSet) || isBlank(year)) {
return null;
}
return ratingRecommendRuleMapper.selectBySeriesSetYear(series, cardSet, year);
}
@Override
public RatingRecommendRulePO selectBySeriesSet(String series, String cardSet) {
// L3 两个参与字段不能为空
if (isBlank(series) || isBlank(cardSet)) {
return null;
}
return ratingRecommendRuleMapper.selectBySeriesSet(series, cardSet);
}
/**
* 简单空白判断(避免引入 commons-lang 仅用一次)
*/
private boolean isBlank(String s) {
return s == null || s.trim().isEmpty();
}
}
[ ] Step 3: 编译验证
Run: mvn -q -pl mango-manager -am -DskipTests compile
Expected: BUILD SUCCESS
[ ] Step 4: 提交
git add mango-manager/src/main/java/com/mangoo/rating/recommend/manager/RatingRecommendRuleManager.java mango-manager/src/main/java/com/mangoo/rating/recommend/manager/impl/RatingRecommendRuleManagerImpl.java
git commit -m "feat(recommend): 新增推荐规则 Manager 接口与实现"
RecommendProperties + ymlFiles:
mango-application/src/main/java/com/mangoo/rating/recommend/config/RecommendProperties.javaModify: mango-application/src/main/resources/application.yml
[ ] Step 1: 编写配置类
package com.mangoo.rating.recommend.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 评级时效推荐相关配置
*
* @author gengjintao
* @date 2026/06/24
*/
@Data
@Component
@ConfigurationProperties(prefix = "recommend")
public class RecommendProperties {
/** 兜底默认时效档(EvaluateEfficiencyEnum.code,默认 1=普通) */
private int defaultEfficiency = 1;
/** 单次请求卡数量上限 */
private int batchMax = 50;
/**
* 服务等级时效字符串 → 时效 code 映射(报价用,订单服务返回的 timeLimit 是字符串)。
* 例:「普通: 1」「快速: 2」「闪评: 3」「7个工作日: 1」「1个工作日: 3」
*/
private Map<String, Integer> timeLimitMapping = new LinkedHashMap<>();
}
[ ] Step 2: 在 application.yml 文件末尾追加配置(注意与现有文件根级缩进一致)
# 评级时效推荐功能配置
recommend:
default-efficiency: 1
batch-max: 50
time-limit-mapping:
"普通": 1
"快速": 2
"闪评": 3
[ ] Step 3: 编译验证
Run: mvn -q -pl mango-application -am -DskipTests compile
Expected: BUILD SUCCESS
[ ] Step 4: 提交
git add mango-application/src/main/java/com/mangoo/rating/recommend/config/RecommendProperties.java mango-application/src/main/resources/application.yml
git commit -m "feat(recommend): 新增推荐功能配置 RecommendProperties"
RecommendCardDTO、RecommendRequest)Files:
mango-common/src/main/java/com/mangoo/rating/recommend/request/recommend/RecommendCardDTO.javaCreate: mango-common/src/main/java/com/mangoo/rating/recommend/request/recommend/RecommendRequest.java
[ ] Step 1: 编写 RecommendCardDTO
package com.mangoo.rating.recommend.request.recommend;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
* 单卡推荐入参(识别特征 + 透传字段)
*
* @author gengjintao
* @date 2026/06/24
*/
@Data
@ApiModel("单卡推荐入参")
public class RecommendCardDTO {
@ApiModelProperty(value = "前端临时卡标识(用于回指订单归属,仅透传不入库)", required = true)
@NotBlank(message = "clientCardId 不能为空")
private String clientCardId;
@ApiModelProperty(value = "球员")
private String player;
@ApiModelProperty(value = "年份")
private String year;
@ApiModelProperty(value = "系列")
private String series;
@ApiModelProperty(value = "卡种")
private String cardSet;
@ApiModelProperty(value = "正面图URL(仅透传回显)")
private String frontImageUrl;
@ApiModelProperty(value = "反面图URL(仅透传回显)")
private String backImageUrl;
}
[ ] Step 2: 编写 RecommendRequest
package com.mangoo.rating.recommend.request.recommend;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import java.util.List;
/**
* 评级时效推荐请求体(批量卡)
*
* @author gengjintao
* @date 2026/06/24
*/
@Data
@ApiModel("评级时效推荐请求体")
public class RecommendRequest {
@ApiModelProperty(value = "卡列表", required = true)
@NotEmpty(message = "卡列表不能为空")
@Valid
private List<RecommendCardDTO> cards;
}
[ ] Step 3: 编译验证
Run: mvn -q -pl mango-common -am -DskipTests compile
Expected: BUILD SUCCESS
[ ] Step 4: 提交
git add mango-common/src/main/java/com/mangoo/rating/recommend/request/recommend/RecommendCardDTO.java mango-common/src/main/java/com/mangoo/rating/recommend/request/recommend/RecommendRequest.java
git commit -m "feat(recommend): 新增推荐请求 DTO RecommendCardDTO/RecommendRequest"
RecommendCardVO、OrderSuggestionVO、RecommendResultVO)Files:
mango-common/src/main/java/com/mangoo/rating/recommend/response/recommend/RecommendCardVO.javamango-common/src/main/java/com/mangoo/rating/recommend/response/recommend/OrderSuggestionVO.javaCreate: mango-common/src/main/java/com/mangoo/rating/recommend/response/recommend/RecommendResultVO.java
[ ] Step 1: 编写 RecommendCardVO
package com.mangoo.rating.recommend.response.recommend;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
/**
* 单卡推荐结果 VO
*
* @author gengjintao
* @date 2026/06/24
*/
@Data
@ApiModel("单卡推荐结果")
public class RecommendCardVO {
@ApiModelProperty(value = "前端临时卡标识(原样回传)")
private String clientCardId;
@ApiModelProperty(value = "球员")
private String player;
@ApiModelProperty(value = "年份")
private String year;
@ApiModelProperty(value = "系列")
private String series;
@ApiModelProperty(value = "卡种")
private String cardSet;
@ApiModelProperty(value = "推荐时效(EvaluateEfficiencyEnum.code)")
private Integer recommendEfficiency;
@ApiModelProperty(value = "命中层级(EXACT/L2/L3/DEFAULT)")
private String matchLevel;
@ApiModelProperty(value = "该卡单价(价格取不到时为 null=待计算)")
private BigDecimal unitPrice;
@ApiModelProperty(value = "正面图URL(透传)")
private String frontImageUrl;
@ApiModelProperty(value = "反面图URL(透传)")
private String backImageUrl;
}
[ ] Step 2: 编写 OrderSuggestionVO
package com.mangoo.rating.recommend.response.recommend;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
* 订单建议 VO(一个订单 = 一组同时效卡 + 一个统一推荐时效)
*
* @author gengjintao
* @date 2026/06/24
*/
@Data
@ApiModel("订单建议")
public class OrderSuggestionVO {
@ApiModelProperty(value = "分组号(1 起,档位升序)")
private Integer groupNo;
@ApiModelProperty(value = "订单推荐时效(EvaluateEfficiencyEnum.code)")
private Integer efficiency;
@ApiModelProperty(value = "订单推荐时效描述")
private String efficiencyDesc;
@ApiModelProperty(value = "该档单价(价格取不到时为 null=待计算)")
private BigDecimal unitPrice;
@ApiModelProperty(value = "订单中卡片数量")
private Integer cardCount;
@ApiModelProperty(value = "订单总计(=单价×数量;任一缺价则为 null)")
private BigDecimal totalAmount;
@ApiModelProperty(value = "订单内卡列表")
private List<RecommendCardVO> cards;
}
[ ] Step 3: 编写 RecommendResultVO
package com.mangoo.rating.recommend.response.recommend;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
* 评级时效推荐响应结果
*
* @author gengjintao
* @date 2026/06/24
*/
@Data
@ApiModel("评级时效推荐响应")
public class RecommendResultVO {
@ApiModelProperty(value = "总卡数")
private Integer totalCards;
@ApiModelProperty(value = "订单数")
private Integer orderCount;
@ApiModelProperty(value = "所有订单合计(任一订单缺价则为 null)")
private BigDecimal grandTotal;
@ApiModelProperty(value = "订单建议列表")
private List<OrderSuggestionVO> orders;
}
[ ] Step 4: 编译验证
Run: mvn -q -pl mango-common -am -DskipTests compile
Expected: BUILD SUCCESS
[ ] Step 5: 提交
git add mango-common/src/main/java/com/mangoo/rating/recommend/response/recommend/RecommendCardVO.java mango-common/src/main/java/com/mangoo/rating/recommend/response/recommend/OrderSuggestionVO.java mango-common/src/main/java/com/mangoo/rating/recommend/response/recommend/RecommendResultVO.java
git commit -m "feat(recommend): 新增推荐响应 VO RecommendCardVO/OrderSuggestionVO/RecommendResultVO"
产出:无 Spring 依赖、纯函数式的
OrderGrouper,TDD 覆盖全部边界。
OrderGrouperTestFiles:
mango-application/src/test/java/com/mangoo/rating/recommend/cart/OrderGrouperTest.java测试放在 mango-application/src/test(已确认含
spring-boot-starter-test,JUnit5 可用)。
[ ] Step 1: 编写失败测试
package com.mangoo.rating.recommend.cart;
import com.mangoo.rating.recommend.response.recommend.RecommendCardVO;
import com.mangoo.rating.recommend.service.recommend.OrderGrouper;
import com.mangoo.rating.recommend.response.recommend.OrderSuggestionVO;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* OrderGrouper TDD 单测
*
* @author gengjintao
* @date 2026/06/24
*/
class OrderGrouperTest {
/**
* 构造测试用卡 VO,仅设置 clientCardId 与 recommendEfficiency(其他字段不参与分组)
*/
private RecommendCardVO c(String no, int eff) {
RecommendCardVO v = new RecommendCardVO();
v.setClientCardId(no);
v.setRecommendEfficiency(eff);
return v;
}
@Test
void 空输入_返回空订单列表() {
List<OrderSuggestionVO> orders = OrderGrouper.group(new ArrayList<>());
assertTrue(orders.isEmpty());
}
@Test
void null输入_返回空订单列表() {
List<OrderSuggestionVO> orders = OrderGrouper.group(null);
assertTrue(orders.isEmpty());
}
@Test
void 单卡_返回单订单_组号1() {
List<OrderSuggestionVO> orders = OrderGrouper.group(Arrays.asList(c("a", 2)));
assertEquals(1, orders.size());
assertEquals(1, orders.get(0).getGroupNo());
assertEquals(2, orders.get(0).getEfficiency());
assertEquals(1, orders.get(0).getCardCount());
assertEquals("a", orders.get(0).getCards().get(0).getClientCardId());
}
@Test
void 全同档_合并为一单() {
List<OrderSuggestionVO> orders = OrderGrouper.group(Arrays.asList(c("a", 1), c("b", 1), c("c", 1)));
assertEquals(1, orders.size());
assertEquals(1, orders.get(0).getEfficiency());
assertEquals(3, orders.get(0).getCardCount());
}
@Test
void 两档_按档位升序输出_组号从1递增() {
// 输入 闪评/普通 乱序,期望输出 普通(1) 在前
List<OrderSuggestionVO> orders = OrderGrouper.group(Arrays.asList(c("x", 3), c("y", 1), c("z", 1)));
assertEquals(2, orders.size());
assertEquals(1, orders.get(0).getGroupNo());
assertEquals(1, orders.get(0).getEfficiency());
assertEquals(2, orders.get(0).getCardCount());
assertEquals(2, orders.get(1).getGroupNo());
assertEquals(3, orders.get(1).getEfficiency());
assertEquals(1, orders.get(1).getCardCount());
}
@Test
void 三档同时存在_最多3个订单_按档位升序() {
List<OrderSuggestionVO> orders = OrderGrouper.group(Arrays.asList(c("a", 2), c("b", 3), c("c", 1)));
assertEquals(3, orders.size());
assertEquals(1, orders.get(0).getEfficiency()); // 普通
assertEquals(2, orders.get(1).getEfficiency()); // 快速
assertEquals(3, orders.get(2).getEfficiency()); // 闪评
}
@Test
void 组内卡列表保持原顺序() {
// 同档卡 a/b/c 输入顺序 a c b,期望分组内顺序 a c b
List<OrderSuggestionVO> orders = OrderGrouper.group(Arrays.asList(c("a", 1), c("c", 1), c("b", 1)));
assertEquals(1, orders.size());
assertEquals("a", orders.get(0).getCards().get(0).getClientCardId());
assertEquals("c", orders.get(0).getCards().get(1).getClientCardId());
assertEquals("b", orders.get(0).getCards().get(2).getClientCardId());
}
}
[ ] Step 2: 运行测试,确认 FAIL(编译失败:OrderGrouper 不存在)
Run: mvn -pl mango-application -am test -Dtest=OrderGrouperTest
Expected: 编译失败 / 找不到符号 — 符合 TDD 红灯
[ ] Step 3: 提交失败测试(红灯阶段)
git add mango-application/src/test/java/com/mangoo/rating/recommend/cart/OrderGrouperTest.java
git commit -m "test(recommend): OrderGrouper TDD 红灯测试"
OrderGrouper 让测试通过Files:
Create: mango-domain/src/main/java/com/mangoo/rating/recommend/service/recommend/OrderGrouper.java
[ ] Step 1: 编写实现
package com.mangoo.rating.recommend.service.recommend;
import com.mangoo.rating.recommend.enums.EvaluateEfficiencyEnum;
import com.mangoo.rating.recommend.response.recommend.OrderSuggestionVO;
import com.mangoo.rating.recommend.response.recommend.RecommendCardVO;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
/**
* 订单分组纯算法(无 Spring 依赖)。
* 规则:按每卡的 recommendEfficiency 精确分组,相同档位归为一个订单;
* 订单按档位升序输出,groupNo 从 1 递增;
* 组内卡列表保留输入原顺序(稳定)。
*
* @author gengjintao
* @date 2026/06/24
*/
public final class OrderGrouper {
private OrderGrouper() {
}
/**
* 将卡列表按推荐时效分组为订单建议
*/
public static List<OrderSuggestionVO> group(List<RecommendCardVO> cards) {
List<OrderSuggestionVO> result = new ArrayList<>();
if (cards == null || cards.isEmpty()) {
return result;
}
// 用 TreeMap 按档位升序归并;同档列表用 ArrayList 保留输入顺序(稳定)
Map<Integer, List<RecommendCardVO>> bucket = new TreeMap<>();
for (RecommendCardVO card : cards) {
Integer eff = card.getRecommendEfficiency();
bucket.computeIfAbsent(eff, k -> new ArrayList<>()).add(card);
}
int groupNo = 1;
for (Map.Entry<Integer, List<RecommendCardVO>> entry : bucket.entrySet()) {
OrderSuggestionVO order = new OrderSuggestionVO();
order.setGroupNo(groupNo++);
order.setEfficiency(entry.getKey());
order.setEfficiencyDesc(EvaluateEfficiencyEnum.getDescByCode(entry.getKey()));
order.setCardCount(entry.getValue().size());
order.setCards(entry.getValue());
// unitPrice / totalAmount 由上层报价后回填,此处不设置
result.add(order);
}
return result;
}
}
[ ] Step 2: 运行测试,确认 PASS
Run: mvn -pl mango-application -am test -Dtest=OrderGrouperTest
Expected: Tests run: 7, Failures: 0, Errors: 0 — PASS
[ ] Step 3: 提交
git add mango-domain/src/main/java/com/mangoo/rating/recommend/service/recommend/OrderGrouper.java
git commit -m "feat(recommend): 实现订单分组纯类 OrderGrouper(7 项 TDD 单测通过)"
EfficiencyPriceProviderFiles:
Create: mango-domain/src/main/java/com/mangoo/rating/recommend/service/recommend/EfficiencyPriceProvider.java
[ ] Step 1: 编写接口
package com.mangoo.rating.recommend.service.recommend;
import java.math.BigDecimal;
import java.util.Map;
/**
* 时效价格 Provider(抽象接口,便于后续替换实现:本地缓存/订单服务 Feign/其他)。
*
* @author gengjintao
* @date 2026/06/24
*/
public interface EfficiencyPriceProvider {
/**
* 加载"时效 code → 单价"映射。
* 取不到 / 异常时应返回空 Map,由上层降级为"待计算"。
*
* @return 时效 code -> 单价(如 1 -> 50.00, 2 -> 100.00, 3 -> 200.00)
*/
Map<Integer, BigDecimal> loadEfficiencyPrices();
}
[ ] Step 2: 编译验证
Run: mvn -q -pl mango-domain -am -DskipTests compile
Expected: BUILD SUCCESS
[ ] Step 3: 提交
git add mango-domain/src/main/java/com/mangoo/rating/recommend/service/recommend/EfficiencyPriceProvider.java
git commit -m "feat(recommend): 新增时效价格 Provider 接口"
Files:
mango-domain/src/main/java/com/mangoo/rating/recommend/service/recommend/impl/EfficiencyPriceProviderImpl.java本期为预留口子:未接通订单服务 Feign,loadEfficiencyPrices() 始终返回空 Map → 上层降级为"待计算"。后续接通时只需在此处填充查询逻辑、改 timeLimit 字符串→time效 code 的映射,不影响调用方。
[ ] Step 1: 编写实现
package com.mangoo.rating.recommend.service.recommend.impl;
import com.mangoo.rating.recommend.config.RecommendProperties;
import com.mangoo.rating.recommend.dto.product.ProductServiceLevelCacheDTO;
import com.mangoo.rating.recommend.enums.EvaluateEfficiencyEnum;
import com.mangoo.rating.recommend.service.recommend.EfficiencyPriceProvider;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 时效价格 Provider 实现(预留调订单服务的口子 + 降级)。
*
* 当前实现:未接通订单服务 Feign,直接返回空 Map,触发上层"待计算"降级。
* 后续接通时:在 fetchServiceLevels() 中通过 Feign 调订单服务、得到 ProductServiceLevelCacheDTO 列表,
* 再用 RecommendProperties.timeLimitMapping 把 timeLimit 字符串映射到 EvaluateEfficiencyEnum.code。
*
* @author gengjintao
* @date 2026/06/24
*/
@Slf4j
@Component
public class EfficiencyPriceProviderImpl implements EfficiencyPriceProvider {
@Resource
private RecommendProperties recommendProperties;
@Override
public Map<Integer, BigDecimal> loadEfficiencyPrices() {
try {
// 1. 从订单服务获取全部服务等级(预留口子,当前返回空列表 → 触发降级)
List<ProductServiceLevelCacheDTO> levels = fetchServiceLevels();
if (levels == null || levels.isEmpty()) {
log.info("时效价格未配置或订单服务未接通,全部走'待计算'降级");
return Collections.emptyMap();
}
// 2. 将服务等级的 timeLimit(字符串) → EvaluateEfficiencyEnum.code 后聚合
Map<Integer, BigDecimal> priceMap = new LinkedHashMap<>();
Map<String, Integer> mapping = recommendProperties.getTimeLimitMapping();
for (ProductServiceLevelCacheDTO lvl : levels) {
if (lvl == null || lvl.getTimeLimit() == null || lvl.getPrice() == null) {
continue;
}
Integer effCode = mapping == null ? null : mapping.get(lvl.getTimeLimit().trim());
if (effCode == null || EvaluateEfficiencyEnum.getByCode(effCode) == null) {
log.debug("无法映射服务等级 timeLimit={} 到时效 code,跳过", lvl.getTimeLimit());
continue;
}
// 同档存在多条时,保留首个(订单服务侧应已去重;此处简单防御)
priceMap.putIfAbsent(effCode, lvl.getPrice());
}
return priceMap;
} catch (Exception e) {
// 价格能力任何异常都不应阻断推荐主体结果
log.warn("加载时效价格异常,本次降级为'待计算'", e);
return Collections.emptyMap();
}
}
/**
* 调订单服务取全部服务等级 —— 预留口子。
* 本期返回空列表(不接 Feign),后续接通时实现真实调用。
*/
private List<ProductServiceLevelCacheDTO> fetchServiceLevels() {
// TODO: 后续接入 order-service Feign 客户端:调用其"全部服务等级"接口
return Collections.emptyList();
}
}
[ ] Step 2: 编译验证
Run: mvn -q -pl mango-domain -am -DskipTests compile
Expected: BUILD SUCCESS
[ ] Step 3: 提交
git add mango-domain/src/main/java/com/mangoo/rating/recommend/service/recommend/impl/EfficiencyPriceProviderImpl.java
git commit -m "feat(recommend): 价格 Provider 实现(订单服务预留口子+降级)"
RecommendService 编排 + 单测RecommendService 接口 + ImplFiles:
mango-domain/src/main/java/com/mangoo/rating/recommend/service/RecommendService.javaCreate: mango-domain/src/main/java/com/mangoo/rating/recommend/service/impl/RecommendServiceImpl.java
[ ] Step 1: 编写接口
package com.mangoo.rating.recommend.service;
import com.mangoo.rating.recommend.AjaxResult;
import com.mangoo.rating.recommend.request.recommend.RecommendRequest;
/**
* 评级时效推荐服务(编排:逐卡降级匹配 → 精确分组 → 报价 → 组装响应)
*
* @author gengjintao
* @date 2026/06/24
*/
public interface RecommendService {
/**
* 推荐评级时效并按时效分单
*
* @param request 批量卡请求
* @return AjaxResult.data = RecommendResultVO
*/
AjaxResult recommendEfficiency(RecommendRequest request);
}
[ ] Step 2: 编写实现(编排:四步走 + 价格降级)
package com.mangoo.rating.recommend.service.impl;
import com.mangoo.rating.recommend.AjaxResult;
import com.mangoo.rating.recommend.config.RecommendProperties;
import com.mangoo.rating.recommend.enums.EvaluateEfficiencyEnum;
import com.mangoo.rating.recommend.enums.MatchLevelEnum;
import com.mangoo.rating.recommend.manager.RatingRecommendRuleManager;
import com.mangoo.rating.recommend.po.RatingRecommendRulePO;
import com.mangoo.rating.recommend.request.recommend.RecommendCardDTO;
import com.mangoo.rating.recommend.request.recommend.RecommendRequest;
import com.mangoo.rating.recommend.response.recommend.OrderSuggestionVO;
import com.mangoo.rating.recommend.response.recommend.RecommendCardVO;
import com.mangoo.rating.recommend.response.recommend.RecommendResultVO;
import com.mangoo.rating.recommend.service.RecommendService;
import com.mangoo.rating.recommend.service.recommend.EfficiencyPriceProvider;
import com.mangoo.rating.recommend.service.recommend.OrderGrouper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 评级时效推荐服务实现
*
* 流程:
* 1) 逐卡按"全等→L2→L3→兜底"降级匹配,得到 (recommendEfficiency, matchLevel)
* 2) 用纯类 OrderGrouper 按时效精确分组
* 3) 加载时效价格(取不到则全部降级"待计算")
* 4) 回填单价/总计/合计,组装响应
*
* @author gengjintao
* @date 2026/06/24
*/
@Slf4j
@Service
public class RecommendServiceImpl implements RecommendService {
@Resource
private RatingRecommendRuleManager ratingRecommendRuleManager;
@Resource
private EfficiencyPriceProvider efficiencyPriceProvider;
@Resource
private RecommendProperties recommendProperties;
@Override
public AjaxResult recommendEfficiency(RecommendRequest request) {
// 入参校验
if (request == null || request.getCards() == null || request.getCards().isEmpty()) {
return AjaxResult.error("卡列表不能为空");
}
if (request.getCards().size() > recommendProperties.getBatchMax()) {
return AjaxResult.error("单次卡数量超过上限:" + recommendProperties.getBatchMax());
}
// 1) 逐卡推荐 → 转 VO
List<RecommendCardVO> cardVOs = new ArrayList<>(request.getCards().size());
for (RecommendCardDTO dto : request.getCards()) {
cardVOs.add(recommendOneCard(dto));
}
// 2) 按时效精确分组
List<OrderSuggestionVO> orders = OrderGrouper.group(cardVOs);
// 3) 报价(取不到则全部 null=待计算,不阻断)
Map<Integer, BigDecimal> priceMap = efficiencyPriceProvider.loadEfficiencyPrices();
BigDecimal grandTotal = applyPricing(orders, priceMap);
// 4) 组装响应
RecommendResultVO result = new RecommendResultVO();
result.setTotalCards(request.getCards().size());
result.setOrderCount(orders.size());
result.setGrandTotal(grandTotal);
result.setOrders(orders);
return AjaxResult.success("推荐成功", result);
}
/**
* 单卡降级匹配:EXACT → L2 → L3 → DEFAULT
*/
private RecommendCardVO recommendOneCard(RecommendCardDTO dto) {
RecommendCardVO vo = new RecommendCardVO();
vo.setClientCardId(dto.getClientCardId());
vo.setPlayer(dto.getPlayer());
vo.setYear(dto.getYear());
vo.setSeries(dto.getSeries());
vo.setCardSet(dto.getCardSet());
vo.setFrontImageUrl(dto.getFrontImageUrl());
vo.setBackImageUrl(dto.getBackImageUrl());
// L1:四字段全等命中
RatingRecommendRulePO rule = ratingRecommendRuleManager.selectByExact(
dto.getPlayer(), dto.getYear(), dto.getSeries(), dto.getCardSet());
if (isLegalRule(rule)) {
vo.setRecommendEfficiency(rule.getRecommendEfficiency());
vo.setMatchLevel(MatchLevelEnum.EXACT.getCode());
return vo;
}
// L2:series + cardSet + year
rule = ratingRecommendRuleManager.selectBySeriesSetYear(dto.getSeries(), dto.getCardSet(), dto.getYear());
if (isLegalRule(rule)) {
vo.setRecommendEfficiency(rule.getRecommendEfficiency());
vo.setMatchLevel(MatchLevelEnum.L2.getCode());
return vo;
}
// L3:series + cardSet
rule = ratingRecommendRuleManager.selectBySeriesSet(dto.getSeries(), dto.getCardSet());
if (isLegalRule(rule)) {
vo.setRecommendEfficiency(rule.getRecommendEfficiency());
vo.setMatchLevel(MatchLevelEnum.L3.getCode());
return vo;
}
// 兜底
vo.setRecommendEfficiency(recommendProperties.getDefaultEfficiency());
vo.setMatchLevel(MatchLevelEnum.DEFAULT.getCode());
return vo;
}
/**
* 校验规则及其时效值是否合法(非空 + 时效在 EvaluateEfficiencyEnum 范围内)
*/
private boolean isLegalRule(RatingRecommendRulePO rule) {
return rule != null
&& rule.getRecommendEfficiency() != null
&& EvaluateEfficiencyEnum.getByCode(rule.getRecommendEfficiency()) != null;
}
/**
* 给订单列表回填单价/总计,并返回所有订单合计。
* 价格 Map 为空或某档无价 → 该订单 unitPrice/totalAmount = null("待计算"),并将 grandTotal 置 null。
*/
private BigDecimal applyPricing(List<OrderSuggestionVO> orders, Map<Integer, BigDecimal> priceMap) {
if (orders == null || orders.isEmpty()) {
return null;
}
BigDecimal grand = BigDecimal.ZERO;
boolean grandNull = false;
for (OrderSuggestionVO order : orders) {
BigDecimal unit = (priceMap == null) ? null : priceMap.get(order.getEfficiency());
if (unit == null) {
// 该档价格缺失 → 订单 unitPrice/totalAmount = null,整体合计同步置 null
order.setUnitPrice(null);
order.setTotalAmount(null);
grandNull = true;
// 卡级单价同步 null
if (order.getCards() != null) {
for (RecommendCardVO c : order.getCards()) {
c.setUnitPrice(null);
}
}
continue;
}
BigDecimal total = unit.multiply(BigDecimal.valueOf(order.getCardCount()));
order.setUnitPrice(unit);
order.setTotalAmount(total);
if (order.getCards() != null) {
for (RecommendCardVO c : order.getCards()) {
c.setUnitPrice(unit);
}
}
grand = grand.add(total);
}
return grandNull ? null : grand;
}
}
[ ] Step 3: 编译验证
Run: mvn -q -pl mango-domain -am -DskipTests compile
Expected: BUILD SUCCESS
[ ] Step 4: 提交
git add mango-domain/src/main/java/com/mangoo/rating/recommend/service/RecommendService.java mango-domain/src/main/java/com/mangoo/rating/recommend/service/impl/RecommendServiceImpl.java
git commit -m "feat(recommend): 实现 RecommendService(降级匹配+分组+报价编排)"
RecommendServiceImpl 单元测试(Mockito)Files:
Create: mango-application/src/test/java/com/mangoo/rating/recommend/cart/RecommendServiceImplTest.java
[ ] Step 1: 编写测试(覆盖:L1/L2/L3/兜底/非法规则值/价格命中/价格降级/空入参/超限)
package com.mangoo.rating.recommend.cart;
import com.mangoo.rating.recommend.AjaxResult;
import com.mangoo.rating.recommend.config.RecommendProperties;
import com.mangoo.rating.recommend.enums.MatchLevelEnum;
import com.mangoo.rating.recommend.manager.RatingRecommendRuleManager;
import com.mangoo.rating.recommend.po.RatingRecommendRulePO;
import com.mangoo.rating.recommend.request.recommend.RecommendCardDTO;
import com.mangoo.rating.recommend.request.recommend.RecommendRequest;
import com.mangoo.rating.recommend.response.recommend.OrderSuggestionVO;
import com.mangoo.rating.recommend.response.recommend.RecommendResultVO;
import com.mangoo.rating.recommend.service.impl.RecommendServiceImpl;
import com.mangoo.rating.recommend.service.recommend.EfficiencyPriceProvider;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.when;
/**
* RecommendServiceImpl Mockito 单测
*
* @author gengjintao
* @date 2026/06/24
*/
@ExtendWith(MockitoExtension.class)
class RecommendServiceImplTest {
@Mock
private RatingRecommendRuleManager ruleManager;
@Mock
private EfficiencyPriceProvider priceProvider;
@Mock
private RecommendProperties recommendProperties;
@InjectMocks
private RecommendServiceImpl recommendService;
@BeforeEach
void setUp() {
lenient().when(recommendProperties.getDefaultEfficiency()).thenReturn(1);
lenient().when(recommendProperties.getBatchMax()).thenReturn(50);
// 默认价格 Map:1=>50, 2=>100, 3=>200
Map<Integer, BigDecimal> defaultPrices = new HashMap<>();
defaultPrices.put(1, new BigDecimal("50.00"));
defaultPrices.put(2, new BigDecimal("100.00"));
defaultPrices.put(3, new BigDecimal("200.00"));
lenient().when(priceProvider.loadEfficiencyPrices()).thenReturn(defaultPrices);
}
private RecommendCardDTO card(String id, String player, String year, String series, String cardSet) {
RecommendCardDTO d = new RecommendCardDTO();
d.setClientCardId(id);
d.setPlayer(player);
d.setYear(year);
d.setSeries(series);
d.setCardSet(cardSet);
return d;
}
private RecommendRequest req(RecommendCardDTO... cards) {
RecommendRequest r = new RecommendRequest();
r.setCards(new ArrayList<>(Arrays.asList(cards)));
return r;
}
private RatingRecommendRulePO rule(Integer eff) {
RatingRecommendRulePO po = new RatingRecommendRulePO();
po.setRecommendEfficiency(eff);
return po;
}
@Test
void 空入参_返回error() {
AjaxResult result = recommendService.recommendEfficiency(null);
assertEquals(500, ((Integer) result.get("code")));
result = recommendService.recommendEfficiency(new RecommendRequest());
assertEquals(500, ((Integer) result.get("code")));
}
@Test
void 超限_返回error() {
when(recommendProperties.getBatchMax()).thenReturn(1);
AjaxResult result = recommendService.recommendEfficiency(
req(card("c1", "p", "y", "s", "cs"), card("c2", "p", "y", "s", "cs")));
assertEquals(500, ((Integer) result.get("code")));
}
@Test
void L1全等命中_单卡_单订单_含价格() {
when(ruleManager.selectByExact("Jordan", "1986", "Fleer", "Base")).thenReturn(rule(3));
AjaxResult result = recommendService.recommendEfficiency(req(card("c1", "Jordan", "1986", "Fleer", "Base")));
RecommendResultVO data = (RecommendResultVO) result.get("data");
assertEquals(1, data.getTotalCards());
assertEquals(1, data.getOrderCount());
OrderSuggestionVO order = data.getOrders().get(0);
assertEquals(3, order.getEfficiency());
assertEquals(MatchLevelEnum.EXACT.getCode(), order.getCards().get(0).getMatchLevel());
assertEquals(new BigDecimal("200.00"), order.getUnitPrice());
assertEquals(new BigDecimal("200.00"), order.getTotalAmount());
assertEquals(new BigDecimal("200.00"), data.getGrandTotal());
}
@Test
void L1未命中_L2命中_matchLevel为L2() {
when(ruleManager.selectByExact(anyString(), anyString(), anyString(), anyString())).thenReturn(null);
when(ruleManager.selectBySeriesSetYear("Fleer", "Base", "1986")).thenReturn(rule(2));
AjaxResult result = recommendService.recommendEfficiency(req(card("c1", "Jordan", "1986", "Fleer", "Base")));
RecommendResultVO data = (RecommendResultVO) result.get("data");
assertEquals(MatchLevelEnum.L2.getCode(), data.getOrders().get(0).getCards().get(0).getMatchLevel());
assertEquals(2, data.getOrders().get(0).getEfficiency());
}
@Test
void L1L2未命中_L3命中_matchLevel为L3() {
when(ruleManager.selectByExact(anyString(), anyString(), anyString(), anyString())).thenReturn(null);
when(ruleManager.selectBySeriesSetYear(anyString(), anyString(), anyString())).thenReturn(null);
when(ruleManager.selectBySeriesSet("Fleer", "Base")).thenReturn(rule(2));
AjaxResult result = recommendService.recommendEfficiency(req(card("c1", "Jordan", "1986", "Fleer", "Base")));
RecommendResultVO data = (RecommendResultVO) result.get("data");
assertEquals(MatchLevelEnum.L3.getCode(), data.getOrders().get(0).getCards().get(0).getMatchLevel());
}
@Test
void 三层均未命中_走兜底_matchLevel为DEFAULT() {
when(ruleManager.selectByExact(any(), any(), any(), any())).thenReturn(null);
when(ruleManager.selectBySeriesSetYear(any(), any(), any())).thenReturn(null);
when(ruleManager.selectBySeriesSet(any(), any())).thenReturn(null);
AjaxResult result = recommendService.recommendEfficiency(req(card("c1", "X", "Y", "Z", "W")));
RecommendResultVO data = (RecommendResultVO) result.get("data");
assertEquals(MatchLevelEnum.DEFAULT.getCode(), data.getOrders().get(0).getCards().get(0).getMatchLevel());
assertEquals(1, data.getOrders().get(0).getEfficiency()); // 默认普通
}
@Test
void 规则时效非法_跳过该层继续降级() {
when(ruleManager.selectByExact(any(), any(), any(), any())).thenReturn(rule(99)); // 非法
when(ruleManager.selectBySeriesSetYear(any(), any(), any())).thenReturn(rule(2)); // 合法
AjaxResult result = recommendService.recommendEfficiency(req(card("c1", "p", "y", "s", "cs")));
RecommendResultVO data = (RecommendResultVO) result.get("data");
// 99 非法 → 当作 L1 未命中,进入 L2 命中
assertEquals(MatchLevelEnum.L2.getCode(), data.getOrders().get(0).getCards().get(0).getMatchLevel());
}
@Test
void 价格Map为空_订单与卡的单价均为null_grandTotal为null() {
when(ruleManager.selectByExact(any(), any(), any(), any())).thenReturn(rule(3));
when(priceProvider.loadEfficiencyPrices()).thenReturn(Collections.emptyMap());
AjaxResult result = recommendService.recommendEfficiency(req(card("c1", "p", "y", "s", "cs")));
RecommendResultVO data = (RecommendResultVO) result.get("data");
assertNull(data.getOrders().get(0).getUnitPrice());
assertNull(data.getOrders().get(0).getTotalAmount());
assertNull(data.getOrders().get(0).getCards().get(0).getUnitPrice());
assertNull(data.getGrandTotal());
}
@Test
void 多卡多档_按档位升序分3单_总计正确() {
// 三张卡,分别命中 1/2/3 档
when(ruleManager.selectByExact("p1", "y", "s", "cs")).thenReturn(rule(1));
when(ruleManager.selectByExact("p2", "y", "s", "cs")).thenReturn(rule(2));
when(ruleManager.selectByExact("p3", "y", "s", "cs")).thenReturn(rule(3));
AjaxResult result = recommendService.recommendEfficiency(req(
card("c1", "p1", "y", "s", "cs"),
card("c2", "p2", "y", "s", "cs"),
card("c3", "p3", "y", "s", "cs")));
RecommendResultVO data = (RecommendResultVO) result.get("data");
assertEquals(3, data.getOrderCount());
assertEquals(1, data.getOrders().get(0).getEfficiency());
assertEquals(2, data.getOrders().get(1).getEfficiency());
assertEquals(3, data.getOrders().get(2).getEfficiency());
// grandTotal = 50 + 100 + 200 = 350
assertEquals(new BigDecimal("350.00"), data.getGrandTotal());
}
@Test
void 同档多卡_合并为一单_单价不变_总计为单价乘以数量() {
when(ruleManager.selectByExact(any(), any(), any(), any())).thenReturn(rule(2));
AjaxResult result = recommendService.recommendEfficiency(req(
card("c1", "p", "y", "s", "cs"),
card("c2", "p", "y", "s", "cs"),
card("c3", "p", "y", "s", "cs")));
RecommendResultVO data = (RecommendResultVO) result.get("data");
assertEquals(1, data.getOrderCount());
OrderSuggestionVO order = data.getOrders().get(0);
assertEquals(3, order.getCardCount());
assertEquals(new BigDecimal("100.00"), order.getUnitPrice());
// 100 * 3 = 300.00
assertEquals(new BigDecimal("300.00"), order.getTotalAmount());
}
@Test
void 图片透传_前端clientCardId与图片原样返回() {
when(ruleManager.selectByExact(any(), any(), any(), any())).thenReturn(rule(1));
RecommendCardDTO c = card("MY-CARD-1", "p", "y", "s", "cs");
c.setFrontImageUrl("http://x/f.jpg");
c.setBackImageUrl("http://x/b.jpg");
AjaxResult result = recommendService.recommendEfficiency(req(c));
RecommendResultVO data = (RecommendResultVO) result.get("data");
assertEquals("MY-CARD-1", data.getOrders().get(0).getCards().get(0).getClientCardId());
assertEquals("http://x/f.jpg", data.getOrders().get(0).getCards().get(0).getFrontImageUrl());
assertEquals("http://x/b.jpg", data.getOrders().get(0).getCards().get(0).getBackImageUrl());
}
}
[ ] Step 2: 运行测试,确认 PASS
Run: mvn -pl mango-application -am test -Dtest=RecommendServiceImplTest
Expected: Tests run: 11, Failures: 0, Errors: 0 — PASS
若
AjaxResult.get("code")/get("data")在当前实现中签名不同(如getCode()/getData()),按实际签名替换断言取值方式。AjaxResult在本项目中已确认存在success(String,Object)/error(String)等方法,断言层细节按运行时实际接口调整即可,不影响测试覆盖意图。
[ ] Step 3: 提交
git add mango-application/src/test/java/com/mangoo/rating/recommend/cart/RecommendServiceImplTest.java
git commit -m "test(recommend): RecommendServiceImpl Mockito 单测(11 项)"
RecommendControllerFiles:
Create: mango-application/src/main/java/com/mangoo/rating/recommend/app/controller/RecommendController.java
[ ] Step 1: 编写 Controller
package com.mangoo.rating.recommend.app.controller;
import com.mangoo.rating.recommend.AjaxResult;
import com.mangoo.rating.recommend.annotation.ApiLog;
import com.mangoo.rating.recommend.enums.BusinessType;
import com.mangoo.rating.recommend.request.recommend.RecommendRequest;
import com.mangoo.rating.recommend.service.RecommendService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* 小程序-评级时效推荐接口
*
* @author gengjintao
* @date 2026/06/24
*/
@Api(tags = "小程序-评级时效推荐接口")
@RestController
@RequestMapping("/api/recommend")
public class RecommendController {
@Resource
private RecommendService recommendService;
@ApiOperation("批量推荐评级时效并按时效分单")
@PostMapping("/efficiency")
@ApiLog(title = "评级时效推荐", businessType = BusinessType.SEARCH)
public AjaxResult recommendEfficiency(@Validated @RequestBody RecommendRequest request) {
return recommendService.recommendEfficiency(request);
}
}
[ ] Step 2: 编译验证
Run: mvn -q -pl mango-application -am -DskipTests compile
Expected: BUILD SUCCESS
Run: mvn -q -pl mango-application -am spring-boot:run
调用接口(PowerShell 示例):
$body = @'
{
"cards": [
{"clientCardId":"c1","player":"Jordan","year":"1986","series":"Fleer","cardSet":"Base","frontImageUrl":"http://x/c1f.jpg"},
{"clientCardId":"c2","player":"Kobe","year":"2000","series":"Topps","cardSet":"Base"}
]
}
'@
Invoke-RestMethod -Method Post -Uri "http://localhost:8094/api/recommend/efficiency" -ContentType "application/json; charset=utf-8" -Body $body
Expected:
code = 0(success)data.totalCards = 2,data.orderCount 与 data.orders 反映分组结果data.grandTotal = null(订单服务未接通 → 价格降级"待计算")clientCardId / frontImageUrl 原样回传每张卡 matchLevel = DEFAULT(规则表为空时全部走兜底)
[ ] Step 4: 提交
git add mango-application/src/main/java/com/mangoo/rating/recommend/app/controller/RecommendController.java
git commit -m "feat(recommend): 新增推荐时效接口 Controller"
Run: mvn -q -DskipTests clean install
Expected: BUILD SUCCESS
Run: mvn -pl mango-application -am test -Dtest=OrderGrouperTest,RecommendServiceImplTest
Expected: 全部 PASS(OrderGrouperTest 7 + RecommendServiceImplTest 11 = 18 项)
往 t_rating_recommend_rule 手动插入若干条规则数据(⚠️ 仅作为测试数据,需大侠确认后由您手动 INSERT;本计划不提供 INSERT 语句),例如:
-- ⚠️ 仅作为测试数据,需大侠手动执行
-- INSERT INTO t_rating_recommend_rule(player, year, series, card_set, recommend_efficiency, effective_time)
-- VALUES ('Jordan', '1986', 'Fleer', 'Base', 3, now());
调用接口,期望:
matchLevel = EXACT,对应时效正确matchLevel = L2matchLevel = L3无任何匹配 → matchLevel = DEFAULT,时效=1
[ ] Step 4: 提交(如有微调)/ 关闭计划
如全程无修改,跳过;如有修复,统一以 fix(recommend): xxx 提交。
1. 规格覆盖(对照 spec 12 节逐条)
/api/recommend/efficiency ✓recommend.*:S3-1 配置类 + yml ✓2. 占位符扫描:无 "TBD/TODO/待补充/类似 Task N" 占位;EfficiencyPriceProviderImpl.fetchServiceLevels() 内有一处 // TODO: 后续接入 order-service Feign 属于预留口子的明示标注(spec D5 明确为本期不做),不是计划占位符。
3. 类型/签名一致性:
Integer(EvaluateEfficiencyEnum.code),PO/请求/响应/分组/报价口径一致 ✓selectByExact / selectBySeriesSetYear / selectBySeriesSet,Manager/Service 调用名一致 ✓OrderGrouper.group(List<RecommendCardVO>) → List<OrderSuggestionVO> 定义与调用一致 ✓EfficiencyPriceProvider.loadEfficiencyPrices() → Map<Integer, BigDecimal> 定义与调用一致 ✓MatchLevelEnum.getCode() 在 Service 写入 VO 时使用,VO 字段类型为 String,匹配 ✓recommend.default-efficiency / recommend.batch-max / recommend.time-limit-mapping 与 RecommendProperties 字段一致 ✓4. 歧义澄清:
grandTotal=null;订单内 unitPrice/totalAmount=null 与卡级 unitPrice=null 同步(S6-1 测试已固化)✓Plan complete and saved to docs/superpowers/plans/2026-06-24-rating-efficiency-recommend.md. 两种执行方式:
1. Subagent-Driven(推荐) — 每个 Task 派新子代理执行,任务间双阶段审查,迭代快、上下文干净。
2. Inline Execution — 在当前会话用 executing-plans 批量执行 + 检查点审查。
⚠️ 检查点:
请大侠选择执行方式?