在小程序中实现三类商品推荐场景:
步骤 1:product_dimension_mapping 表
路径:cfc-backend/src/main/resources/schema.sql
在 products 表定义之后添加:
CREATE TABLE product_dimension_mapping (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
product_id BIGINT NOT NULL COMMENT '商品ID',
dimension_code VARCHAR(32) NOT NULL COMMENT '维度: body/wisdom/mind/action/wealth',
match_score INT DEFAULT 100 COMMENT '匹配度 0-100',
match_reason VARCHAR(200) COMMENT '匹配原因,如"专注力提升"',
tags VARCHAR(500) COMMENT '推荐标签 JSON',
enabled TINYINT DEFAULT 1,
created_at DATETIME,
updated_at DATETIME,
INDEX idx_product (product_id),
INDEX idx_dimension (dimension_code)
) ENGINE=InnoDB COMMENT='商品维度关联表';
步骤 2:product_recommendation_log 表
CREATE TABLE product_recommendation_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT,
family_id BIGINT,
product_id BIGINT NOT NULL,
scene VARCHAR(32) NOT NULL COMMENT 'dimension_page/ai_chat/report_upload/repurchase',
reason VARCHAR(200),
match_score INT,
was_clicked TINYINT DEFAULT 0,
was_purchased TINYINT DEFAULT 0,
clicked_at DATETIME,
purchased_at DATETIME,
created_at DATETIME,
INDEX idx_user_scene (user_id, scene),
INDEX idx_product_purchased (product_id, was_purchased)
) ENGINE=InnoDB COMMENT='推荐曝光日志';
步骤 3:repurchase_reminder_record 表
CREATE TABLE repurchase_reminder_record (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
order_id BIGINT COMMENT '关联订单ID',
reminder_days INT DEFAULT 30,
sent_at DATETIME,
clicked TINYINT DEFAULT 0,
purchased TINYINT DEFAULT 0,
INDEX idx_user_pending (user_id, purchased)
) ENGINE=InnoDB COMMENT='复购提醒发送记录';
步骤 4:repurchase_reminder_config 表
CREATE TABLE repurchase_reminder_config (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
product_category VARCHAR(100),
product_id BIGINT COMMENT '特定商品ID(优先于category)',
reminder_days INT DEFAULT 30,
reminder_template VARCHAR(500) COMMENT '提醒话术模板',
max_reminders INT DEFAULT 3,
enabled TINYINT DEFAULT 1,
created_at DATETIME
) ENGINE=InnoDB COMMENT='复购提醒配置表';
步骤 5:products 表新增字段
ALTER TABLE products ADD COLUMN recommendation_tags VARCHAR(500) COMMENT '推荐标签 JSON';
ALTER TABLE products ADD COLUMN purchase_count_threshold INT DEFAULT 0;
ALTER TABLE products ADD COLUMN repurchase_interval_days INT DEFAULT 30;
路径:cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
在 runMigrations() 方法末尾添加迁移N(编号递增),使用 ensureColumn 和 jdbcTemplate.execute 创建新表。
| 类 | 路径 | 说明 |
|---|---|---|
ProductDimensionMapping |
entity/ProductDimensionMapping.java |
商品-维度关联 |
ProductRecommendationLog |
entity/ProductRecommendationLog.java |
推荐曝光日志 |
RepurchaseReminderRecord |
entity/RepurchaseReminderRecord.java |
复购提醒记录 |
RepurchaseReminderConfig |
entity/RepurchaseReminderConfig.java |
复购提醒配置 |
| 类 | 路径 | 说明 |
|---|---|---|
ProductDimensionMappingMapper |
mapper/ProductDimensionMappingMapper.java |
继承 BaseMapper |
ProductRecommendationLogMapper |
mapper/ProductRecommendationLogMapper.java |
继承 BaseMapper |
RepurchaseReminderRecordMapper |
mapper/RepurchaseReminderRecordMapper.java |
继承 BaseMapper |
RepurchaseReminderConfigMapper |
mapper/RepurchaseReminderConfigMapper.java |
继承 BaseMapper |
接口:POST /api/recommend/dimension-products
路径:cfc-backend/src/main/java/com/etotem/cfc/controller/recommendation/ProductRecommendationController.java
请求体:
{
"dimensionCode": "wisdom",
"familyId": 1,
"memberId": 5,
"excludeProductIds": [3, 7],
"limit": 6
}
响应:
{
"code": 200,
"data": [
{
"id": 10,
"name": "认知能力测评套餐",
"coverImage": "https://...",
"price": 29900,
"memberPrice": 19900,
"reason": "专注力得分偏低,推荐优先提升",
"matchScore": 85,
"productType": "assessment",
"url": "/pages/shop/detail?id=10"
}
]
}
ProductRecommendationService)路径:cfc-backend/src/main/java/com/etotem/cfc/service/ProductRecommendationService.java
算法逻辑:
1. 查询所有上架且有库存的商品(status='上架', stock > 0)
2. 匹配维度:
a. Product.domain == dimensionCode(权重1.0)
b. 或 product_dimension_mapping.dimension_code == dimensionCode(权重1.5,从mapping表读取)
3. 查询 members 的 five_dimension_scores,按 dimension_code 排序
→ 得分低的维度 → 对应商品推荐权重 × 1.2
4. 查询 ProductOrder,确认排除已购商品(buyerId 或 familyId 在 90 天内购买过)
5. 按 match_score = 基础分 × 维度缺口加权 × 已购惩罚 排序
6. 取前 limit 条返回
新增方法:
getDimensionRecommendations(String dimensionCode, Long familyId, Long memberId, List<Long> excludeProductIds, int limit)getPurchasedProductIds(Long userId, Long familyId, int daysAgo)getMemberDimensionScores(Long familyId, Long memberId)路径:cfc-frontend/components/DimensionProductList.vue
功能:
dimensionCode, familyId, memberScores, excludeProductIds, limitPOST /api/recommend/dimension-products嵌入位置:
cfc-frontend/pages/wisdom/index.vue:在认知雷达图下方添加 <DimensionProductList dimensionCode="wisdom" ... />每次返回推荐结果前,写入 product_recommendation_log(scene=dimension_page),记录 product_id / user_id / match_score / created_at。
路径:cfc-backend/src/main/java/com/etotem/cfc/service/FamilyContextService.java
修改 buildContext(Long userId) 方法:
在返回的 inputs Map 中新增3个字段:
// 新增:成员维度得分(供 Dify 理解家庭短板)
inputs.put("dimensionScores", buildDimensionScoresContext(userId));
// 新增:最近认知测评摘要
inputs.put("recentCognitiveResult", buildCognitiveContext(userId));
// 新增:已购买商品标签(避免重复推荐)
inputs.put("purchasedProductTags", buildPurchasedTagsContext(userId));
新增私有方法:
buildDimensionScoresContext(Long userId) → 查询 five_dimension_scores 返回 [{dimension, score, memberName}]buildCognitiveContext(Long userId) → 查询 dan_assessment_results 最新一条,返回 {weakDimensions: [...], overallScore}buildPurchasedTagsContext(Long userId) → 查询 product_orders 中用户已购商品的 recommendation_tags路径:cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIChatController.java
扩展 [RECOMMEND] 解析:
在 sendNutritionMessage() 的 [RECOMMEND:] 解析块中新增:
// 新增:从 RecommendationQuery 中取 dimensionCode 和 userId,过滤已购
if (tags != null && !tags.isEmpty()) {
RecommendationQuery rq = new RecommendationQuery();
rq.setNutritionTags(tags);
rq.setTypes(types != null && !types.isEmpty() ? types : null);
rq.setLimit(limit);
rq.setUserId(userId); // 新增:传入userId用于过滤已购
rq.setFamilyId(familyId); // 新增:传入familyId用于过滤已购
recommendations = recommendationService.search(rq);
}
路径:cfc-backend/src/main/java/com/etotem/cfc/service/RecommendationService.java
修改 search(RecommendationQuery query) 方法:
// 在 searchProducts() 中新增过滤逻辑:
// 1. 如果 query.userId 或 query.familyId 存在,排除 90 天内已购商品
// 2. 如果 query.dimensionCode 存在,按 match_score 排序时加权
新增字段到 RecommendationQuery DTO:
private Long userId;
private Long familyId;
private String dimensionCode; // 用于维度加权
路径:cfc-frontend/pages/ai/chat.vue
在现有的 recommendation 卡片展示逻辑中,新增:
reason 字段读取)触发点: HealthReportService.analyze(reportId) 执行完成后
在 cfc-backend/src/main/java/com/etotem/cfc/service/HealthAnalysisService.java 的 analyze() 方法末尾添加:
// 触发维度推荐
try {
Map<String, Object> analysisResult = parseAnalysisResult(reportId);
List<String> dimensionNeeds = extractDimensionNeeds(analysisResult);
List<RecommendationResult> products =
productRecommendationService.getReportRelatedProducts(
"health_report", analysisResult, userId, 3);
// 记录推荐日志,scene = 'report_upload'
for (RecommendationResult r : products) {
productRecommendationLogService.log(userId, r, "report_upload",
"健康报告分析触发:" + String.join(",", dimensionNeeds));
}
} catch (Exception e) {
log.warn("报告关联推荐生成失败: {}", e.getMessage());
}
触发点: DanAssessmentResult 写入完成(source=parent_upload)
在 cfc-backend/src/main/java/com/etotem/cfc/service/CognitiveService.java 的 saveAssessmentResult() 或相关写入方法末尾添加类似逻辑:
// 提取6维得分中最低的2个维度
List<String> weakDims = findWeakDimensions(result); // e.g. ["focusScore", "processingSpeedScore"]
// 映射到 dimensionCode:focusScore/processingSpeedScore → "wisdom"
// 查询对应维度商品
List<RecommendationResult> products =
productRecommendationService.getReportRelatedProducts(
"cognitive_assessment", weakDims, userId, 3);
// 记录推荐日志
public List<RecommendationResult> getReportRelatedProducts(
String reportType, Object analysis, Long userId, int limit) {
// reportType = "health_report" 或 "cognitive_assessment"
// 根据 reportType 提取关联维度码和标签
// 调用 search() 时设置 dimensionCode 和已购过滤
}
路径:cfc-backend/src/main/java/com/etotem/cfc/service/RepurchaseReminderService.java
定时扫描(每天 09:00):
@Scheduled(cron = "0 0 9 * * ?")
public void scanAndCreateReminders() {
// 1. 查询过去 30~60 天内有已支付订单的用户
// 2. 对每个订单商品,匹配 repurchase_reminder_config
// 3. 检查是否已发送过 reminder 且未过期
// 4. 创建 repurchase_reminder_record(sent_at = now)
// 5. 发送小程序订阅消息(调用现有消息通知机制)
}
public void onReminderClicked(Long reminderId) { ... }
public void onReminderPurchased(Long reminderId, Long orderId) { ... }
路径:cfc-frontend/components/RepurchaseReminder.vue
from=repurchase 参数)POST /api/recommend/repurchase-reminders → GET /api/recommend/repurchase-reminders| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | mvn clean compile |
编译通过,无错误 |
| 2 | curl -X POST /api/recommend/dimension-products |
返回维度关联商品列表 |
| 3 | 小程序打开智页 | 底部显示推荐商品(DimensionProductList) |
| 4 | 上传健康报告 | 推荐日志写入,scene=report_upload |
| 5 | AI营养对话触发 [RECOMMEND] | 返回过滤已购后的商品 |
| 6 | 查看 product_recommendation_log 表 |
有 dimension_page 和 report_upload 记录 |
| 操作 | 文件路径 |
|---|---|
| 新增表 | resources/schema.sql 中 4 个 CREATE TABLE + ALTER TABLE |
| 新增迁移 | config/DatabaseInitializer.java runMigrations() |
| 新增实体 ×4 | entity/ProductDimensionMapping.java 等 |
| 新增 Mapper ×4 | mapper/ProductDimensionMappingMapper.java 等 |
| 新增 Service | service/ProductRecommendationService.java |
| 新增 Service | service/RepurchaseReminderService.java |
| 新增 Controller | controller/recommendation/ProductRecommendationController.java |
| 修改 Service | service/FamilyContextService.java — buildContext() |
| 修改 DTO | dto/RecommendationQuery.java — 新增 userId/familyId/dimensionCode |
| 修改 Service | service/RecommendationService.java — 过滤已购 |
| 修改 Controller | controller/ai/AIChatController.java — 传入 userId/familyId |
| 修改 Service | service/HealthAnalysisService.java — 报告上传触发推荐 |
| 修改 Service | service/CognitiveService.java — 测评上传触发推荐 |
| 修改 Service | service/ProductRecommendationLogService.java(新建) |
| 新增前端组件 | components/DimensionProductList.vue |
| 新增前端组件 | components/RepurchaseReminder.vue |
| 修改前端页面 | pages/wisdom/index.vue 等 — 嵌入 DimensionProductList |
| 修改前端页面 | pages/ai/chat.vue — 推荐理由展示 |
| 修改前端 API | utils/api.js — 新增推荐相关接口 |
| 风险 | 缓解 |
|---|---|
| Dify 幻觉推荐 | 后端兜底过滤(status=上架,stock>0),仅在 nutrition/send 接口触发 |
| 冷启动(mapping 表空) | 初期用 Product.domain 隐式匹配;mapping 表由运营后台手动标注或导入 |
| 推荐效果未验证 | product_recommendation_log 记录曝光,后续可做转化率统计 |
| 复购周期判断不准 | repurchase_interval_days 可按商品类别配置,默认 30 天 |