Kaynağa Gözat

docs(recommend): 评级时效推荐 设计文档与实现计划

- 设计稿: docs/superpowers/specs/2026-06-24-rating-efficiency-recommend-design.md
- 实现计划: docs/superpowers/plans/2026-06-24-rating-efficiency-recommend.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jintao.geng 1 ay önce
ebeveyn
işleme
5d3f8ab6e6

+ 1877 - 0
docs/superpowers/plans/2026-06-24-rating-efficiency-recommend.md

@@ -0,0 +1,1877 @@
+# 评级时效推荐 + 自动分单 + 报价 实现计划(Implementation Plan)
+
+> **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。
+
+---
+
+## 关键设计决策(与 spec 一致)
+
+| # | 决策 | 备注 |
+|---|---|---|
+| 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 语句 |
+
+---
+
+## 安全与合规(CLAUDE.md 数据库规则)
+
+- 涉及 **1 张新表 DDL**(`t_rating_recommend_rule`),由大侠在评审后**手动执行**;本计划只提供脚本文本,执行器不主动对数据库运行任何写操作。
+- Mapper XML **仅 SELECT**。
+- 不写 Redis、不发 MQ、不调用第三方变更接口。
+- 执行计划前请大侠对"建表 DDL(Task S1-4)"明确放行。
+
+---
+
+## 文件结构总览(新建/修改)
+
+**mango-common**
+- 新建 `enums/MatchLevelEnum.java`
+- 新建 `po/RatingRecommendRulePO.java`
+- 新建 `request/recommend/RecommendCardDTO.java`、`request/recommend/RecommendRequest.java`
+- 新建 `response/recommend/RecommendCardVO.java`、`response/recommend/OrderSuggestionVO.java`、`response/recommend/RecommendResultVO.java`
+
+**mango-infrastructure**
+- 新建 `mapper/RatingRecommendRuleMapper.java` + `resources/mapper/RatingRecommendRuleMapper.xml`(仅 SELECT)
+
+**mango-manager**
+- 新建 `manager/RatingRecommendRuleManager.java` + `manager/impl/RatingRecommendRuleManagerImpl.java`
+
+**mango-domain**
+- 新建 `service/recommend/OrderGrouper.java`(纯类)
+- 新建 `service/recommend/EfficiencyPriceProvider.java` + `service/recommend/impl/EfficiencyPriceProviderImpl.java`
+- 新建 `service/RecommendService.java` + `service/impl/RecommendServiceImpl.java`
+
+**mango-application**
+- 修改 `RatingRecommendApplication.java`(启用数据源)
+- 新建 `app/controller/RecommendController.java`
+- 新建 `config/RecommendProperties.java`
+- 修改 `resources/application.yml`(新增 `recommend.*` 配置)
+- 测试:`src/test/java/.../recommend/OrderGrouperTest.java`、`RecommendServiceImplTest.java`
+
+**DDL**
+- 新建 `docs/superpowers/specs/ddl/2026-06-24-rating-recommend-rule.sql`
+
+---
+
+## 测试策略
+
+- **纯算法 `OrderGrouper`**:纯 JUnit5 TDD,覆盖:空 / 单卡 / 全同档 / 多档乱序 / 3 档同时存在。毫秒级运行。
+- **`RecommendServiceImpl`**:Mockito 单测,覆盖:全等命中 / L2 命中 / L3 命中 / 兜底 / 非法规则值兜底 / 缺字段跳层 / 价格命中 / 价格缺失降级 / 空输入。
+- **Controller / 编排**:编译通过 + 手动接口验证;不写无断言的假测试。
+- 运行命令(Windows / 项目根):
+  - 单模块单测:`mvn -pl mango-application -am test -Dtest=OrderGrouperTest`
+  - 全量编译:`mvn -q -DskipTests clean install`
+
+---
+
+## Phase S0 — 前置改造:启用数据源
+
+> 产出:项目启动时正常装配 Druid + PostgreSQL,规则表 Mapper 后续可被调用。
+
+### Task S0-1: 启用数据源自动装配
+
+**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(先确认基线可编译)
+
+- [ ] **Step 2: 修改启动类排除项,去掉 `DataSourceAutoConfiguration`**
+
+将启动类上原有的:
+```java
+@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, SecurityAutoConfiguration.class})
+```
+改为:
+```java
+// 原排除 DataSourceAutoConfiguration 已去除:评级时效推荐功能需要查询规则表,必须启用数据源(PostgreSQL)
+@SpringBootApplication(exclude = {SecurityAutoConfiguration.class})
+```
+
+并删除不再需要的 `import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;`(如果存在)。
+
+- [ ] **Step 3: 编译验证**
+
+Run: `mvn -q -pl mango-application -am -DskipTests compile`
+Expected: BUILD SUCCESS
+
+- [ ] **Step 4: 启动应用验证数据源装配**
+
+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: 提交**
+
+```bash
+git add mango-application/src/main/java/com/mangoo/rating/recommend/RatingRecommendApplication.java
+git commit -m "chore(boot): 启用数据源自动装配(评级推荐功能前置)"
+```
+
+---
+
+## Phase S1 — 数据底座:枚举、PO、Mapper、DDL
+
+> 产出:可只读访问规则数据的 PO、Mapper、枚举、建表脚本。
+
+### Task S1-1: 新增枚举 `MatchLevelEnum`
+
+**Files:**
+- Create: `mango-common/src/main/java/com/mangoo/rating/recommend/enums/MatchLevelEnum.java`
+
+- [ ] **Step 1: 编写枚举**(照 `EvaluateEfficiencyEnum` 范式)
+
+```java
+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: 提交**
+
+```bash
+git add mango-common/src/main/java/com/mangoo/rating/recommend/enums/MatchLevelEnum.java
+git commit -m "feat(recommend): 新增命中层级枚举 MatchLevelEnum"
+```
+
+---
+
+### Task S1-2: 新增 PO `RatingRecommendRulePO`
+
+**Files:**
+- Create: `mango-common/src/main/java/com/mangoo/rating/recommend/po/RatingRecommendRulePO.java`
+
+- [ ] **Step 1: 编写 PO**(照 `RecognizedInfoPO` 范式)
+
+```java
+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: 提交**
+
+```bash
+git add mango-common/src/main/java/com/mangoo/rating/recommend/po/RatingRecommendRulePO.java
+git commit -m "feat(recommend): 新增推荐规则 PO RatingRecommendRulePO"
+```
+
+---
+
+### Task S1-3: Mapper 接口 + XML(三层查询,仅 SELECT)
+
+**Files:**
+- Create: `mango-infrastructure/src/main/java/com/mangoo/rating/recommend/mapper/RatingRecommendRuleMapper.java`
+- Create: `mango-infrastructure/src/main/resources/mapper/RatingRecommendRuleMapper.xml`
+
+- [ ] **Step 1: 编写 Mapper 接口**(提供 L1/L2/L3 三个精确查询)
+
+```java
+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
+<?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 &lt;= 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 &lt;= 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 &lt;= 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: 提交**
+
+```bash
+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 三层精确查询)"
+```
+
+---
+
+### Task S1-4: ⚠️ 建表 DDL(需大侠评审确认后执行)
+
+**Files:**
+- Create: `docs/superpowers/specs/ddl/2026-06-24-rating-recommend-rule.sql`
+
+> **此脚本不由执行器运行。** 仅生成文本,交大侠评审后在目标库手动执行。字段口径与 PO/XML 完全对应。
+
+- [ ] **Step 1: 编写 DDL 脚本**
+
+```sql
+-- ============================================================
+-- 评级时效推荐规则表 建表脚本(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: 提交脚本(仅文本,不执行)**
+
+```bash
+git add docs/superpowers/specs/ddl/2026-06-24-rating-recommend-rule.sql
+git commit -m "docs(recommend): 新增推荐规则表 DDL 脚本(待评审执行)"
+```
+
+- [ ] **Step 3: 🛑 检查点——请大侠确认 DDL 后,在目标库手动执行,再继续 S2。**
+
+---
+
+## Phase S2 — Manager(接口 + 实现)
+
+### Task S2-1: `RatingRecommendRuleManager` 接口 + Impl
+
+**Files:**
+- Create: `mango-manager/src/main/java/com/mangoo/rating/recommend/manager/RatingRecommendRuleManager.java`
+- Create: `mango-manager/src/main/java/com/mangoo/rating/recommend/manager/impl/RatingRecommendRuleManagerImpl.java`
+
+- [ ] **Step 1: 编写接口**
+
+```java
+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: 编写实现**
+
+```java
+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: 提交**
+
+```bash
+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 接口与实现"
+```
+
+---
+
+## Phase S3 — 配置 + 请求/响应 DTO
+
+### Task S3-1: 配置类 `RecommendProperties` + yml
+
+**Files:**
+- Create: `mango-application/src/main/java/com/mangoo/rating/recommend/config/RecommendProperties.java`
+- Modify: `mango-application/src/main/resources/application.yml`
+
+- [ ] **Step 1: 编写配置类**
+
+```java
+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` 文件末尾追加配置**(注意与现有文件根级缩进一致)
+
+```yaml
+# 评级时效推荐功能配置
+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: 提交**
+
+```bash
+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"
+```
+
+---
+
+### Task S3-2: 请求 DTO(`RecommendCardDTO`、`RecommendRequest`)
+
+**Files:**
+- Create: `mango-common/src/main/java/com/mangoo/rating/recommend/request/recommend/RecommendCardDTO.java`
+- Create: `mango-common/src/main/java/com/mangoo/rating/recommend/request/recommend/RecommendRequest.java`
+
+- [ ] **Step 1: 编写 `RecommendCardDTO`**
+
+```java
+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`**
+
+```java
+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: 提交**
+
+```bash
+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"
+```
+
+---
+
+### Task S3-3: 响应 VO(`RecommendCardVO`、`OrderSuggestionVO`、`RecommendResultVO`)
+
+**Files:**
+- Create: `mango-common/src/main/java/com/mangoo/rating/recommend/response/recommend/RecommendCardVO.java`
+- Create: `mango-common/src/main/java/com/mangoo/rating/recommend/response/recommend/OrderSuggestionVO.java`
+- Create: `mango-common/src/main/java/com/mangoo/rating/recommend/response/recommend/RecommendResultVO.java`
+
+- [ ] **Step 1: 编写 `RecommendCardVO`**
+
+```java
+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`**
+
+```java
+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`**
+
+```java
+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: 提交**
+
+```bash
+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"
+```
+
+---
+
+## Phase S4 — 分组纯类(TDD)
+
+> 产出:无 Spring 依赖、纯函数式的 `OrderGrouper`,TDD 覆盖全部边界。
+
+### Task S4-1: 先写失败测试 `OrderGrouperTest`
+
+**Files:**
+- Create: `mango-application/src/test/java/com/mangoo/rating/recommend/cart/OrderGrouperTest.java`
+
+> 测试放在 mango-application/src/test(已确认含 `spring-boot-starter-test`,JUnit5 可用)。
+
+- [ ] **Step 1: 编写失败测试**
+
+```java
+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: 提交失败测试(红灯阶段)**
+
+```bash
+git add mango-application/src/test/java/com/mangoo/rating/recommend/cart/OrderGrouperTest.java
+git commit -m "test(recommend): OrderGrouper TDD 红灯测试"
+```
+
+---
+
+### Task S4-2: 实现 `OrderGrouper` 让测试通过
+
+**Files:**
+- Create: `mango-domain/src/main/java/com/mangoo/rating/recommend/service/recommend/OrderGrouper.java`
+
+- [ ] **Step 1: 编写实现**
+
+```java
+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: 提交**
+
+```bash
+git add mango-domain/src/main/java/com/mangoo/rating/recommend/service/recommend/OrderGrouper.java
+git commit -m "feat(recommend): 实现订单分组纯类 OrderGrouper(7 项 TDD 单测通过)"
+```
+
+---
+
+## Phase S5 — 价格 Provider(接口 + 实现 + 降级)
+
+### Task S5-1: 价格 Provider 接口 `EfficiencyPriceProvider`
+
+**Files:**
+- Create: `mango-domain/src/main/java/com/mangoo/rating/recommend/service/recommend/EfficiencyPriceProvider.java`
+
+- [ ] **Step 1: 编写接口**
+
+```java
+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: 提交**
+
+```bash
+git add mango-domain/src/main/java/com/mangoo/rating/recommend/service/recommend/EfficiencyPriceProvider.java
+git commit -m "feat(recommend): 新增时效价格 Provider 接口"
+```
+
+---
+
+### Task S5-2: 价格 Provider 实现(订单服务口子 + 降级)
+
+**Files:**
+- Create: `mango-domain/src/main/java/com/mangoo/rating/recommend/service/recommend/impl/EfficiencyPriceProviderImpl.java`
+
+> 本期为预留口子:未接通订单服务 Feign,loadEfficiencyPrices() 始终返回空 Map → 上层降级为"待计算"。后续接通时只需在此处填充查询逻辑、改 timeLimit 字符串→time效 code 的映射,不影响调用方。
+
+- [ ] **Step 1: 编写实现**
+
+```java
+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: 提交**
+
+```bash
+git add mango-domain/src/main/java/com/mangoo/rating/recommend/service/recommend/impl/EfficiencyPriceProviderImpl.java
+git commit -m "feat(recommend): 价格 Provider 实现(订单服务预留口子+降级)"
+```
+
+---
+
+## Phase S6 — `RecommendService` 编排 + 单测
+
+### Task S6-1: `RecommendService` 接口 + Impl
+
+**Files:**
+- Create: `mango-domain/src/main/java/com/mangoo/rating/recommend/service/RecommendService.java`
+- Create: `mango-domain/src/main/java/com/mangoo/rating/recommend/service/impl/RecommendServiceImpl.java`
+
+- [ ] **Step 1: 编写接口**
+
+```java
+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: 编写实现**(编排:四步走 + 价格降级)
+
+```java
+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: 提交**
+
+```bash
+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(降级匹配+分组+报价编排)"
+```
+
+---
+
+### Task S6-2: `RecommendServiceImpl` 单元测试(Mockito)
+
+**Files:**
+- Create: `mango-application/src/test/java/com/mangoo/rating/recommend/cart/RecommendServiceImplTest.java`
+
+- [ ] **Step 1: 编写测试**(覆盖:L1/L2/L3/兜底/非法规则值/价格命中/价格降级/空入参/超限)
+
+```java
+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: 提交**
+
+```bash
+git add mango-application/src/test/java/com/mangoo/rating/recommend/cart/RecommendServiceImplTest.java
+git commit -m "test(recommend): RecommendServiceImpl Mockito 单测(11 项)"
+```
+
+---
+
+## Phase S7 — Controller + 冒烟验证
+
+### Task S7-1: 新增 `RecommendController`
+
+**Files:**
+- Create: `mango-application/src/main/java/com/mangoo/rating/recommend/app/controller/RecommendController.java`
+
+- [ ] **Step 1: 编写 Controller**
+
+```java
+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
+
+- [ ] **Step 3: 启动应用 + 冒烟验证(需先完成 S0-1 数据源启用 + S1-4 DDL 评审执行)**
+
+Run: `mvn -q -pl mango-application -am spring-boot:run`
+
+调用接口(PowerShell 示例):
+```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: 提交**
+
+```bash
+git add mango-application/src/main/java/com/mangoo/rating/recommend/app/controller/RecommendController.java
+git commit -m "feat(recommend): 新增推荐时效接口 Controller"
+```
+
+---
+
+## Phase S8 — 全链路回归 + 收尾
+
+### Task S8-1: 全量编译 + 全部单测
+
+- [ ] **Step 1: 全量编译**
+
+Run: `mvn -q -DskipTests clean install`
+Expected: BUILD SUCCESS
+
+- [ ] **Step 2: 跑全部新增单测**
+
+Run: `mvn -pl mango-application -am test -Dtest=OrderGrouperTest,RecommendServiceImplTest`
+Expected: 全部 PASS(`OrderGrouperTest` 7 + `RecommendServiceImplTest` 11 = 18 项)
+
+- [ ] **Step 3: 手动接口回归(条件:DDL 已执行 + 数据源接通)**
+
+往 `t_rating_recommend_rule` 手动插入若干条规则数据(⚠️ 仅作为测试数据,需大侠确认后由您手动 INSERT;本计划不提供 INSERT 语句),例如:
+
+```sql
+-- ⚠️ 仅作为测试数据,需大侠手动执行
+-- INSERT INTO t_rating_recommend_rule(player, year, series, card_set, recommend_efficiency, effective_time)
+-- VALUES ('Jordan', '1986', 'Fleer', 'Base', 3, now());
+```
+
+调用接口,期望:
+- 同 player/year/series/cardSet 全等 → `matchLevel = EXACT`,对应时效正确
+- 同 series/cardSet/year 部分匹配 → `matchLevel = L2`
+- 同 series/cardSet 部分匹配 → `matchLevel = L3`
+- 无任何匹配 → `matchLevel = DEFAULT`,时效=1
+
+- [ ] **Step 4: 提交(如有微调)/ 关闭计划**
+
+如全程无修改,跳过;如有修复,统一以 `fix(recommend): xxx` 提交。
+
+---
+
+## 计划自审(Plan Self-Review)
+
+**1. 规格覆盖(对照 spec 12 节逐条)**
+- §1 目标无状态计算接口 → 全 Phase 实现(不写库)✓
+- §2 范围:本期做 5 项均覆盖(S1-S7);本期不做 3 项均未触及 ✓
+- §3 关键决策 D1-D8 → 在计划 Phase/Task 中逐条落地 ✓
+- §4 安全合规:DDL 标注评审、Mapper 仅 SELECT、无 Redis/MQ 写入 ✓
+- §5 接口契约:S3-2/3-3 完整 DTO+VO;S7-1 Controller 路径 `/api/recommend/efficiency` ✓
+- §6 核心逻辑:S6-1 实现编排(降级+分组+报价),S4 实现纯类分组 ✓
+- §7 分层文件结构:与计划"文件结构总览"完全对齐 ✓
+- §8 DDL:S1-4 提供完整 DDL(含两个索引)✓
+- §9 前置改造:S0-1 启用数据源 ✓
+- §10 测试策略:OrderGrouper 7 项 TDD + RecommendService 11 项 Mockito + 手动冒烟 ✓
+- §11 配置项 `recommend.*`:S3-1 配置类 + yml ✓
+- §12 开放/简化项:在 S5-2 实现中明确注释、留 TODO 给后续接入订单服务 ✓
+
+**2. 占位符扫描**:无 "TBD/TODO/待补充/类似 Task N" 占位;`EfficiencyPriceProviderImpl.fetchServiceLevels()` 内有一处 `// TODO: 后续接入 order-service Feign` 属于**预留口子的明示标注**(spec D5 明确为本期不做),不是计划占位符。
+
+**3. 类型/签名一致性**:
+- 时效统一 `Integer`(`EvaluateEfficiencyEnum.code`),PO/请求/响应/分组/报价口径一致 ✓
+- Mapper 三个方法 `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 测试已固化)✓
+- 规则非法值(不在 1/2/3)→ 视为该层未命中,继续降级(S6-2 测试已固化)✓
+- 三档同时存在 → 最多 3 个订单,按档位升序(S4-1 测试已固化)✓
+- 组内卡列表顺序 = 输入顺序(S4-1 测试已固化)✓
+
+---
+
+## 执行交接(Execution Handoff)
+
+**Plan complete and saved to `docs/superpowers/plans/2026-06-24-rating-efficiency-recommend.md`. 两种执行方式:**
+
+**1. Subagent-Driven(推荐)** — 每个 Task 派新子代理执行,任务间双阶段审查,迭代快、上下文干净。
+
+**2. Inline Execution** — 在当前会话用 executing-plans 批量执行 + 检查点审查。
+
+**⚠️ 检查点:**
+- **S0-1 启用数据源**:可能影响应用启动稳定性,建议大侠在执行后亲自启动一次确认。
+- **S1-4 建表 DDL**:必须由大侠在评审后**手动执行 DDL**,方可进入 S2 之后的运行时验证。
+
+**请大侠选择执行方式?**

+ 215 - 0
docs/superpowers/specs/2026-06-24-rating-efficiency-recommend-design.md

@@ -0,0 +1,215 @@
+# 评级时效推荐 + 自动分单 + 报价 设计文档(Design Spec)
+
+> 项目:RatingRecommend(卡牌评级小程序-推荐服务)
+> 日期:2026-06-24
+> 作者:gengjintao(大侠)/ Claude 协助
+> 状态:已评审,待转实现计划
+
+---
+
+## 1. 目标(Goal)
+
+提供一个**无状态的计算型 REST 接口**:接收一批卡的识别特征(含图片),为每张卡推荐评级时效档位,再按时效将卡聚合为"多个订单建议",并给出每卡单价、订单数量与总计金额。
+
+- **无状态**:全程不写库、不建购物车、不创建真实订单、不结算。
+- 价格能力以"调订单服务 + 预留扩展口子 + 取不到则降级"的方式接入,本期不强依赖订单服务。
+
+## 2. 范围(Scope)
+
+**本期做:**
+- 批量卡特征输入 → 逐卡时效推荐(规则表)。
+- 时效精确分组 → 多订单建议。
+- 逐卡报价(时效→价格,来自订单服务,预留口子 + 降级)。
+- 独立 REST 接口(Controller→Service→Manager→Mapper 全链路)。
+- 规则表建表 DDL(标注需评审后手动执行)。
+
+**本期不做(YAGNI):**
+- 购物车落库、人工校对/重算、结算 checkout。
+- 容差聚类取齐分单(仅做"同时效精确分组",分组逻辑抽象、后续可扩展)。
+- 套餐入参、价格表新建、用户登录强校验。
+
+## 3. 关键设计决策
+
+| # | 决策 | 说明 |
+|---|---|---|
+| D1 | 时效档位直接用 `EvaluateEfficiencyEnum.code` | 普通=1 / 快速=2 / 闪评=3,天然有序,与现有订单口径一致。 |
+| D2 | 交付形态 = 独立 REST 接口(全链路) | `POST /api/recommend/efficiency`。 |
+| D3 | 匹配策略 = 全等优先 + 系列主导模糊降级 + 兜底 | 降级阶梯见 §6.1,命中即止。 |
+| D4 | 分组 = 同时效精确分组 | 相同最终时效归一个订单建议,按档位升序出 `groupNo`,最多 3 单;分组逻辑抽象为纯类,容差聚类后续扩展。 |
+| D5 | 价格来源 = 订单服务(order-service),实现预留口子 | 抽象 `EfficiencyPriceProvider`,本期可占位/降级;订单服务未接通时单价/总计为 `null`("待计算"),不阻断推荐。 |
+| D6 | 降级查询实现 = 多次精确 SQL | Mapper 按 L1/L2/L3 提供 3 个查询,Service 逐级调用,走索引、DB 友好、逻辑直白。 |
+| D7 | 接口不强制登录 | 纯计算无需用户上下文,沿用现有放行策略。 |
+| D8 | 规则数据只读 | 规则表数据由数仓同步 / 人工维护;本功能不产生任何写库语句。 |
+
+## 4. 安全与合规(CLAUDE.md 数据库规则)
+
+- 涉及 **1 张新表 DDL**(`t_rating_recommend_rule`),标注"⚠️ 需大侠评审确认后手动执行",执行器不主动对数据库运行任何写操作。
+- Mapper 仅含 **SELECT**,无任何 INSERT/UPDATE/DELETE。
+- 不写 Redis、不发 MQ、不调用第三方变更接口。
+- 价格读取为只读(订单服务查询 / 缓存读取)。
+
+## 5. 接口契约
+
+### 5.1 请求 `POST /api/recommend/efficiency`
+
+`RecommendRequest`:
+```json
+{
+  "cards": [
+    {
+      "clientCardId": "c1",
+      "player": "Jordan",
+      "year": "1986",
+      "series": "Fleer",
+      "cardSet": "Base",
+      "frontImageUrl": "http://x/c1f.jpg",
+      "backImageUrl": "http://x/c1b.jpg"
+    }
+  ]
+}
+```
+- `clientCardId`:前端为每张卡生成的临时标识,仅透传、不入库,用于回指卡归属的订单。
+- 四特征:`player / year / series / cardSet`。
+- 图片:`frontImageUrl / backImageUrl`,仅透传回显。
+
+### 5.2 响应 `AjaxResult.data = RecommendResultVO`
+
+```json
+{
+  "totalCards": 3,
+  "orderCount": 2,
+  "grandTotal": 360.00,
+  "orders": [
+    {
+      "groupNo": 1,
+      "efficiency": 3,
+      "efficiencyDesc": "闪评",
+      "unitPrice": 120.00,
+      "cardCount": 2,
+      "totalAmount": 240.00,
+      "cards": [
+        {
+          "clientCardId": "c1",
+          "player": "Jordan", "year": "1986", "series": "Fleer", "cardSet": "Base",
+          "recommendEfficiency": 3,
+          "matchLevel": "EXACT",
+          "unitPrice": 120.00,
+          "frontImageUrl": "http://x/c1f.jpg",
+          "backImageUrl": "http://x/c1b.jpg"
+        }
+      ]
+    }
+  ]
+}
+```
+
+字段说明:
+- 订单级:`groupNo`、`efficiency`/`efficiencyDesc`(订单推荐时效)、`unitPrice`(该档单价)、`cardCount`(卡数量)、`totalAmount`(订单总计 = 单价 × 数量)。
+- 卡级:`recommendEfficiency`、`matchLevel`(命中层级 EXACT/L2/L3/DEFAULT)、`unitPrice`、图片透传。
+- 汇总:`grandTotal`(所有订单合计)。
+- **降级**:价格取不到时,相关 `unitPrice/totalAmount/grandTotal` = `null`(前端显示"待计算"),不阻断推荐主体结果。
+
+## 6. 核心逻辑
+
+### 6.1 单卡匹配降级(系列主导,命中即止)
+
+```
+EXACT : player + year + series + cardSet 全等
+L2    : series + cardSet + year
+L3    : series + cardSet
+DEFAULT: 兜底默认档(可配置,默认 普通=1)
+```
+- 某一层参与字段存在空值,则跳过该层(如 `series` 为空,则 L2/L3 均无法命中 → 直接兜底)。
+- 命中但规则值非法(不在 1/2/3)→ 兜底。
+- 输出 `(recommendEfficiency, matchLevel)`。
+
+### 6.2 分组(纯类 `OrderGrouper`)
+
+- 按每卡最终推荐时效精确分组,相同档归一组。
+- 按档位升序输出 `groupNo`(普通→快速→闪评),最多 3 组。
+- 纯函数、无 Spring 依赖;TDD 覆盖:空输入 / 单卡 / 全同档 / 多档乱序。
+
+### 6.3 报价口子(`EfficiencyPriceProvider`)
+
+- 抽象方法:`Map<Integer, BigDecimal> loadEfficiencyPrices()`(时效 code → 单价)。
+- 本期实现:预留调订单服务(order-service)的 Feign 口子,取全部服务等级(`ProductServiceLevelCacheDTO`:含 `timeLimit` 字符串 + `price`),按"`timeLimit` 字符串 → `EvaluateEfficiencyEnum.code`"映射(映射规则配置化)。
+- 订单服务未接通 / 取不到 → 返回空 Map → 上层降级为"待计算"。
+- 设计意图:**口子预留,后续替换实现对上层零改动**。
+
+## 7. 分层与文件结构(包名 `com.mangoo.rating.recommend.*`)
+
+**mango-common**
+- `request/recommend/RecommendRequest.java`、`request/recommend/RecommendCardDTO.java`
+- `response/recommend/RecommendResultVO.java`、`response/recommend/OrderSuggestionVO.java`、`response/recommend/RecommendCardVO.java`
+- `po/RatingRecommendRulePO.java`
+- `enums/MatchLevelEnum.java`(EXACT/L2/L3/DEFAULT)
+- 复用 `enums/EvaluateEfficiencyEnum.java`
+
+**mango-infrastructure**
+- `mapper/RatingRecommendRuleMapper.java` + `resources/mapper/RatingRecommendRuleMapper.xml`(仅 SELECT)
+
+**mango-manager**
+- `manager/RatingRecommendRuleManager.java` + `manager/impl/RatingRecommendRuleManagerImpl.java`
+
+**mango-domain**
+- `service/RecommendService.java` + `service/impl/RecommendServiceImpl.java`(编排)
+- `service/recommend/OrderGrouper.java`(纯类,分组)
+- `service/recommend/EfficiencyPriceProvider.java` + 实现(价格口子 + 降级)
+
+**mango-application**
+- `app/controller/RecommendController.java`
+- `config/RecommendProperties.java`(默认兜底档 + 时效字符串映射配置)
+- 测试:`OrderGrouperTest`、`RecommendServiceImplTest`(Mockito)
+
+**DDL**
+- `docs/superpowers/specs/ddl/2026-06-24-rating-recommend-rule.sql`(待评审执行)
+
+## 8. 规则表 DDL(PostgreSQL,待评审)
+
+```sql
+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 '评级时效推荐规则表(数仓沉淀,只读查询)';
+-- 支撑系列主导降级查询
+CREATE INDEX IF NOT EXISTS idx_rrr_series_set_year ON t_rating_recommend_rule (series, card_set, year);
+-- 支撑全等命中查询
+CREATE INDEX IF NOT EXISTS idx_rrr_feature ON t_rating_recommend_rule (player, year, series, card_set);
+```
+
+## 9. 前置改造(必须)
+
+当前启动类 `RatingRecommendApplication` 上 `exclude = {DataSourceAutoConfiguration.class}`,**数据源未启用**。本功能需查询规则表,**实现计划第一步须先启用数据源、接通 PostgreSQL**(移除排除项并校验既有配置)。
+
+## 10. 测试策略
+
+- **纯算法 `OrderGrouper`**:纯 JUnit5,TDD 先行,覆盖空/单卡/全同/多档乱序。
+- **降级匹配 / 报价映射**:Mockito 单测(mock Manager / PriceProvider / 配置)。
+- **编排 / 落库查询 / Controller**:编译通过 + `@SpringBootTest` 冒烟 + 手动接口验证;不写无断言的假测试。
+
+## 11. 配置项(`recommend.*`)
+
+```yaml
+recommend:
+  default-efficiency: 1          # 兜底默认档(EvaluateEfficiencyEnum.code)
+  # 服务等级时效字符串 → 时效 code 映射(报价用),示例:
+  time-limit-mapping:
+    "普通": 1
+    "快速": 2
+    "闪评": 3
+```
+
+## 12. 开放/已知简化项
+
+- 报价依赖订单服务,本期为预留口子;未接通时整单"待计算"。
+- 模糊匹配为"系列主导"三级阶梯,更复杂的相似度/热度权重留后续。
+- 分组为同时效精确分组,容差聚类取齐留后续(`OrderGrouper` 已抽象,便于扩展)。