2026-09-17-product-recommendation-rule-engine-design.md 15 KB

商品推荐规则引擎设计文档

1. 概述

统一商品推荐的 5 种业务规则,引入统一规则引擎,由配置表 recommendation_rules 驱动。现有实现(已购90天排除 + 定时复购提醒)并入新引擎,所有场景(维度页/商城首页/商品详情/下单成功/报告关联)统一走一套计算逻辑,配置在后台管理端。

2. 核心目标

  1. 统一入口:所有推荐场景通过同一个引擎计算,输入输出一致
  2. 配置驱动:规则均由 recommendation_rules 表配置,运营可在 Web 管理端增删改,无需改代码
  3. 降级兼容:规则表为空时退化为现有硬编码行为,保证不出现空结果
  4. 最小侵入:现有接口/服务改造仅改调用引擎,核心业务逻辑不改
  5. 覆盖全场景:5 种规则全覆盖,且易扩展新规则

3. 数据模型

3.1 新表 recommendation_rules

字段 类型 主键 说明
id BIGINT PK 主键,AUTO_INCREMENT
rule_type TINYINT 规则类型 1-5
rule_name VARCHAR(100) 运营可读名称
trigger_product_id BIGINT 单商品触发(规则1/2/3/4)
trigger_product_ids VARCHAR(1000) 多商品触发 JSON 数组(规则5,如 "[1,2,3]"
target_product_id BIGINT 目标商品ID(被推荐/排除/重点推)
priority INT 优先级分,越大越先处理
enabled TINYINT 启用 1/0
time_window_days INT 购买回溯窗口(天),默认 null=不限
delay_days INT 复购延迟天数(规则2)
scope VARCHAR(20) family/individual
scene_codes VARCHAR(500) 生效场景 JSON 数组,如 "[\"dimension_page\",\"product_detail\"]"
created_at DATETIME 创建时间
updated_at DATETIME 更新时间

3.2 Index 建议

  • idx_rules_enabled(enabled, rule_type)
  • idx_rules_scene(scene_codes, enabled)
  • idx_rules_priority(priority, enabled)

3.3 MyBatis-Plus Mapper 注解示例(仅示意)

@Mapper
public interface RecommendationRuleMapper {
    @Select("SELECT * FROM recommendation_rules WHERE enabled = 1 AND FIND_IN_SET(?, scene_codes)")
    List<RecommendationRule> selectByScene(String sceneCode);

    @Select("SELECT * FROM recommendation_rules WHERE enabled = 1 AND rule_type = ? ORDER BY priority DESC")
    List<RecommendationRule> selectByRuleType(Integer ruleType);

    @Select("SELECT * FROM recommendation_rules WHERE enabled = 1 AND rule_type = ? AND FIND_IN_SET(?, trigger_product_ids)")
    List<RecommendationRule> selectByProductIds(Integer ruleType, String productIdsJson);
}

4. 引擎设计

4.1 入口接口

/**
 * 推荐上下文
 */
public class RecommendationContext {
    private Long userId;          // 登录用户ID
    private Long familyId;        // 家庭ID(scene 为 family 时必填)
    private Long memberId;        // 成员ID(scope=individual 时)
    private String scene;         // 生效场景代码:dimension_page, mall_home, product_detail, after_order, report_related
    private Long currentProductId; // 商品详情页/下单页的当前商品ID
    private List<Long> excludeProductIds; // 用户自行排除的商品ID(如已选/不喜欢)
    private Integer limit;        // 返回上限,默认6
}
/**
 * 推荐结果项
 */
public class RecommendedProduct {
    private Long productId;
    private String name;
    private String coverImage;
    private Integer price; // 分
    private String reason; // 推荐理由
    private Double matchScore; // 匹配分
    private Integer ruleId; // 触发该结果的规则ID
    private Integer ruleType; // 规则类型 1-5
    private Integer priority; // 该规则优先级
    private String source; // 来源标识:engine/campaign/...
}
/**
 * 引擎核心方法
 */
public interface RecommendationRuleEngine {
    /**
     * 计算推荐
     * @param ctx 推荐上下文
     * @param limit 若覆盖默认limit,可传;否则用 ctx.limit
     * @return 推荐列表
     */
    List<RecommendedProduct> recommend(RecommendationContext ctx, Integer limit);
}

4.2 处理流程(按 priority 降序遍历规则)

1. 加载规则
   - 根据 scene 匹配 `scene_codes`(精确匹配 JSON 中的字符串)
   - enabled=1,按 priority DESC 排序

2. 查购买记录
   - scope=family:查 family_members 关联的订单(ProductOrder 中 familyId 匹配)
   - scope=individual:查 buyerId(userId) 的订单
   - 状态过滤:status in ("已支付", "completed")
   - 时间窗口:time_window_days 为 null 或 返回的时间差 <= time_window_days
     若为 null,默认使用全局配置 default_purchased_exclude_days(默认 90)

3. 构建候选池(场景基础候选)
   - dimension_page → 维度匹配商品(已有 ProductRecommendationService 逻辑)
   - mall_home      → 全量上架在售商品(product status=on_shelf, stock>0)
   - product_detail → 从规则3/5 中命中的 targetProductId
   - after_order    → 从规则3/5 中命中的 targetProductId
   - report_related → 报告提取的弱维度商品(已有 extractDimensionNeedsFromHealth/FromCognitive)

4. 应用规则(按 priority 从高到低遍历已加载的规则)

   4.1 规则4(购买A不推B)
       - 条件:已购买 trigger_product_id
       - 动作:从候选池移除 target_product_id
       - 若未命中则跳过

   4.2 规则1(已购不推)
       - 条件:已购买 trigger_product_id(或全局默认已购窗口内任意商品)
       - 动作:从候选池移除 target_product_id
       - 若未命中则跳过(已购默认由 step2/3 在全局层面完成,规则1只作特定覆盖)

   4.3 规则3(购买A推B)
       - 条件:已购买 trigger_product_id
       - 动作:将 target_product_id 加入候选池 + reason="根据您的购买记录推荐"
       - 若候选池已有该productId,则不重复添加,仅更新 reason

   4.4 规则5(系列重点推)
       - 条件:已购买 trigger_product_ids 中的 ALL 个商品
       - 动作:将 target_product_id 加入候选池 + priority 加 10(提升排序位置)

   4.5 规则2(定时复购)
       - 条件:已购买 trigger_product_id 且(当前日期 - 购买日期) >= delay_days
       - 动作:将 target_product_id 加入候选池 + reason="该商品您已购买,可考虑复购"
       - 建议 delay_days 默认值:30

5. 结果排序 & 截断
   - 按 (priority DESC, matchScore DESC) 排序
   - 截断 limit(默认6)

6. 返回结果
   - List<RecommendedProduct>,每项含 productId, name, coverImage, price, reason, matchScore, ruleId, ruleType, priority, source

4.3 规则语义表

规则 rule_type 触发 动作 关键字段
1 已购不推 1 购买 trigger_product(或全局已购窗口内任意) 从候选排除 target_product trigger_product_id, target_product_id, time_window_days
2 定时复购 2 购买 trigger_product 且距今>=delay_days target_product 加入候选(复购) delay_days, target_product_id
3 购买A推B 3 购买 trigger_product(A) target_product(B) 加入候选 trigger_product_id, target_product_id
4 购买A不推B 4 购买 trigger_product(A) 从候选排除 target_product(B) trigger_product_id, target_product_id
5 系列重点推 5 购买 trigger_product_ids 全部 target_product 加入候选 + boost trigger_product_ids, target_product_id

4.4 全局默认已购排除

  • 系统级配置 default_purchased_exclude_days(默认 90 天),存入 application.yml 或系统参数表
  • 引擎在 step2 查购买记录阶段,scope=family 时查 family 成员订单,scope=individual 时查 buyerId 订单
  • 无论规则1是否配置,所有候选商品在进入 step4 前都已过滤掉回溯窗口内的已购商品(作为兜底)
  • 规则1 仅作“特定商品覆盖”:如 trigger_product_id=123,time_window_days=180,则仅将商品123 的购买记录从候选排除(放宽或收窄默认 90 天)

5. 现有实现迁移

6. 管理端(cfc-web)

6.1 页面:商品推荐规则

  • 列表:分页 + rule_type / enabled / priority 筛选 + 排序
  • 新建/编辑
    • rule_type:1-5 下拉
    • rule_name:文本
    • trigger_product_id / trigger_product_ids:商品选择器(支持搜索、多选)
    • target_product_id:目标商品选择器
    • priority:数字输入(整数,默认 10)
    • enabled:开关
    • time_window_days:整数(天,默认 90)
    • delay_days:整数(天,默认 30,仅规则2)
    • scope:single/family 单选
    • scene_codes:多行文本/标签选择器(可选:dimension_page, mall_home, product_detail, after_order, report_related)

6.2 管理端 API

  • GET /api/admin/recommendation-rules - 分页列表
  • POST /api/admin/recommendation-rules - 新建
  • PUT /api/admin/recommendation-rules/{id} - 编辑
  • DELETE /api/admin/recommendation-rules/{id} - 物理删除(慎用,建议设 enabled=0 停用)
  • POST /api/admin/recommendation-rules/batch-enable - 批量启用/停用

7. 容错与降级

  1. 规则表为空时:引擎不抛出异常,直接进入“场景基础候选”阶段(即现有行为),全局已购排除默认 90 天,返回已有候选列表。

  2. 规则配置冲突:同一规则 type 存在多条启用记录时,按 priority 降序处理,较早的生效。运营若需调整顺序,在管理端修改 priority 即可。

  3. trigger_product_id 不存在:引擎检查 trigger_product_id 是否存在于 products 表中,不存在则跳过该规则,记录 warn 日志。

  4. scene_codes 匹配失败

    • 引擎首先尝试精确匹配 FIND_IN_SET(sceneCode, scene_codes)
    • 若未匹配,再尝试前缀匹配(如 sceneCode 为 "dimension_page",scene_codes 包含 "dimension" 也视为匹配)
    • 作为最后兜底,若 scene_codes 为空字符串 "",视为“所有场景生效”
  5. 时间窗口边界

    • time_window_days=0 表示“只计算今日购买”
    • time_window_days<0 视为异常,引擎取绝对值但标记日志警告

8. 测试要点

  1. 规则单独生效

    • 规则1:已购特定商品(180天)从候选排除
    • 规则2:30天后出现复购推荐
    • 规则3:购买A后推B
    • 规则4:购买A后不推B
    • 规则5:购买A+B+C系列后重点推X
  2. 优先级排序

    • 验证 priority 从高到低的处理顺序
    • 验证同 priority 时,matchScore DESC
  3. scope family/individual

    • family:查 familyId 关联订单
    • individual:查 buyerId(userId) 订单
  4. time_window_days 边界:0、90、365 各自行为

  5. 降级测试

    • 删除所有规则记录,验证系统仍能返回推荐(退化为全局已购排除 + 基础候选)
    • 验证旧定时任务仍正常工作
  6. 编译验证mvn clean compile

9. 变更清单

9.1 数据库

-- 新建表(迁移编号自行在 DatabaseInitializer 末尾递增)
CREATE TABLE recommendation_rules (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    rule_type TINYINT NOT NULL COMMENT '1-5',
    rule_name VARCHAR(100) COMMENT '运营名称',
    trigger_product_id BIGINT COMMENT '触发商品ID',
    trigger_product_ids VARCHAR(1000) COMMENT '触发商品ID JSON 数组',
    target_product_id BIGINT COMMENT '目标商品ID',
    priority INT DEFAULT 10 COMMENT '优先级,越大越优',
    enabled TINYINT DEFAULT 1 COMMENT '启用 1/0',
    time_window_days INT COMMENT '回溯窗口(天)',
    delay_days INT COMMENT '复购延迟天数',
    scope VARCHAR(20) COMMENT 'family/individual',
    scene_codes VARCHAR(500) COMMENT '生效场景 JSON 数组',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- 建议索引
CREATE INDEX idx_rules_enabled ON recommendation_rules(enabled);
CREATE INDEX idx_rules_scene ON recommendation_rules(scene_codes);
CREATE INDEX idx_rules_priority ON recommendation_rules(priority);

9.2 Java 代码变更

  • 新增 entity/RecommendationRule.java
  • 新增 mapper/RecommendationRuleMapper.java
  • 新增 service/RecommendationRuleService.java(CRUD + 规则校验)
  • 新增 service/RecommendationRuleEngine.java(核心计算逻辑)
  • 新增 controller/recommendation/RecommendationRuleAdminController.java(管理端 API)
  • 修改 service/ProductRecommendationService.java → 改为调用 engine.recommend
  • 修改 service/RepurchaseReminderService.java → rule_type=2 配置读取改为从 recommendation_rules
  • 删除/废弃 repurchase_reminder_config 业务逻辑(保留表结构)

9.3 API 变更(后端)

  • 维度页推荐:POST /api/recommend/dimension-products 不变,内部实现改调用 engine
  • 新增(如需要):POST /api/recommend/mall-homePOST /api/recommend/product-detailPOST /api/recommend/after-orderPOST /api/recommend/report-related

9.4 Web管理端

  • 新增页面:/admin/recommendation-rules(Vue 2 + Element UI,遵循项目现有设计规范)
  • 权限:role=admin 或 role=teacher

10. 完成标准

  • 新表 recommendation_rules 建表 + schema.sql 同步
  • RecommendationRuleMapper + RecommendationRuleService 实现
  • RecommendationRuleEngine.recommend() 完整实现(5种规则流程)
  • RepurchaseReminderService 规则2 配置读取改造
  • ProductRecommendationService 改为调用 engine
  • 管理端 CRUD 页面 + API
  • 单元测试:5种规则各自命中/未命中 + priority 排序 + scope family/individual
  • 集成测试:mvn clean compile 通过
  • 文档同步:docs/superpowers/specs/2026-09-17-product-recommendation-rule-engine-design.md
现有代码/组件 迁移动作
ProductRecommendationService.getDimensionRecommendations 改为调用 engine.recommend(ctx.scene="dimension_page"),ctx.limit=6
getPurchasedProductIds(familyId, 90) 硬编码 90 天 移除该方法,改为在引擎内读 default_purchased_exclude_days
RepurchaseReminderService.scanAndCreateReminders @Scheduled 保留(不做改动,但内部配置读取改为从规则表读;rule_type=2 的 config 从 recommendation_rules 读,旧 repurchase_reminder_config 表数据迁移),外部定时任务仍可继续生成 reminder 记录
repurchase_reminder_config 保留(点击/购买追踪表),不做结构变更,仅在新引擎规则2 中通过 rule_type 匹配
ContentRecommendService.getPurchasedProductIds 复用引擎的全局已购排除能力
API Controller(维度页推荐) 不改接口签名,内部改调用 engine.recommend,返回结果自动映射为 Map 格式
新增 API 接口(可选) 若前端需要更多场景,可在 POST /api/recommend/* 下新增,内部同样调用 engine.recommend