# 评级时效推荐 + 自动分单 + 报价 设计文档(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 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` 已抽象,便于扩展)。