# 供应商体系差距修复 — 实施计划 > **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 (`- [x]`) syntax for tracking. **Goal:** 修复供应商体系分析中发现的3个可实施差距:维度商品推荐端点缺失、复购提醒端点+定时任务缺失。 **Architecture:** 在现有 `RecommendationController` + `RecommendationService` 上扩展,新增 `RepurchaseReminderScheduledTask` 定时任务类。 **Tech Stack:** Spring Boot 2.7.18 / MyBatis-Plus / Java 8 / uni-app Vue 2 --- ## 差距来源 基于 `docs/superpowers/specs/2026-07-12-dimension-features-energy-map.md` 和 `docs/product-recommendation/PLAN.md` 的设计要求,对照实际代码发现: | 差距 | 设计文档要求 | 实际状态 | |------|-------------|---------| | `/api/recommend/dimension-products` | 维度页按 `dimensionCode` 查询匹配商品,按 `matchScore` 排序 | 前端 `getDimensionProducts()` 调用 → 后端无此端点 → 500 错误 | | `/api/recommend/repurchase-reminders` | 查询用户的复购提醒列表 | 前端 `getRepurchaseReminders()` 调用 → 后端无此端点 | | 复购定时调度 | "系统每日检查" → 生成复购提醒 | `repurchase_reminder_*` 表已建但无 `@Scheduled` 任务 | --- ## Task 1: 维度商品推荐端点 + Service 逻辑 **Files:** - Modify: `cfc-backend/src/main/java/com/etotem/cfc/controller/ai/RecommendationController.java` - Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/RecommendationService.java` - Inject: `cfc-backend/src/main/java/com/etotem/cfc/mapper/ProductDimensionMappingMapper.java` (already exists, unused) ### Step 0: RecommendationResult DTO 新增字段 在 `RecommendationResult.java` 的 `private String source;` 之后新增: ```java private Integer matchScore; // 维度匹配度 0-100 (dimension-product) private String matchReason; // 匹配原因 (dimension-product) private String reason; // 推荐理由 (repurchase/ai_chat/report) private Long productId; // 关联商品ID (repurchase-reminder) ``` ### Step 1: RecommendationService 新增维度商品查询方法 在 `RecommendationService.java` 末尾添加两个方法: ```java @Resource private ProductDimensionMappingMapper productDimensionMappingMapper; /** * 按维度查询关联商品(按 matchScore 降序) */ public List getDimensionProducts(String dimensionCode, Long familyId, Integer limit) { List mappings = productDimensionMappingMapper.selectList( new LambdaQueryWrapper() .eq(ProductDimensionMapping::getDimensionCode, dimensionCode) .eq(ProductDimensionMapping::getEnabled, 1) .orderByDesc(ProductDimensionMapping::getMatchScore) .last(limit != null ? "LIMIT " + Math.min(limit, 20) : "LIMIT 20") ); List results = new ArrayList<>(); for (ProductDimensionMapping m : mappings) { Product product = productMapper.selectById(m.getProductId()); if (product == null || !"上架".equals(product.getStatus()) || product.getStock() <= 0) { continue; } RecommendationResult r = new RecommendationResult(); r.setType("product"); r.setId(product.getId()); r.setName(product.getName()); r.setDescription(product.getIntro() != null ? product.getIntro() : product.getDescription()); r.setCoverImage(product.getCoverImage()); r.setPrice(product.getPrice()); r.setUrl("/pages/shop/detail?id=" + product.getId()); r.setMatchScore(m.getMatchScore()); r.setMatchReason(m.getMatchReason()); r.setSource("dimension"); results.add(r); } return results; } /** * 查询用户复购提醒列表 */ public List getRepurchaseReminders(Long userId) { List reminders = repurchaseReminderRecordMapper.selectList( new LambdaQueryWrapper() .eq(RepurchaseReminderRecord::getUserId, userId) .eq(RepurchaseReminderRecord::getPurchased, 0) .orderByDesc(RepurchaseReminderRecord::getSentAt) ); List results = new ArrayList<>(); for (RepurchaseReminderRecord r : reminders) { Product product = productMapper.selectById(r.getProductId()); if (product == null || !"上架".equals(product.getStatus())) continue; RecommendationResult result = new RecommendationResult(); result.setType("repurchase"); result.setId(r.getId()); result.setProductId(product.getId()); result.setName(product.getName()); result.setCoverImage(product.getCoverImage()); result.setPrice(product.getPrice()); result.setUrl("/pages/shop/detail?id=" + product.getId()); result.setReason("复购提醒"); result.setSource("repurchase"); results.add(result); } return results; } /** * 记录复购提醒点击 */ public void markRepurchaseClicked(Long reminderId) { RepurchaseReminderRecord record = repurchaseReminderRecordMapper.selectById(reminderId); if (record != null) { record.setClicked(1); repurchaseReminderRecordMapper.updateById(record); } } ``` 需要的 import 和 DI 注入在类顶部新增: ```java import com.etotem.cfc.entity.ProductDimensionMapping; import com.etotem.cfc.entity.RepurchaseReminderRecord; import com.etotem.cfc.mapper.ProductDimensionMappingMapper; import com.etotem.cfc.mapper.RepurchaseReminderRecordMapper; // 在 @Resource 区域新增: @Resource private ProductDimensionMappingMapper productDimensionMappingMapper; @Resource private RepurchaseReminderRecordMapper repurchaseReminderRecordMapper; ``` ### Step 2: RecommendationController 新增三个端点 在 `RecommendationController.java` 末尾(`}` 之前)新增: ```java @Operation(summary = "按维度查询推荐商品") @PostMapping("/dimension-products") public Result> dimensionProducts(@RequestBody Map params) { String dimensionCode = (String) params.get("dimensionCode"); Long familyId = params.get("familyId") instanceof Number ? ((Number) params.get("familyId")).longValue() : null; Integer limit = params.get("limit") instanceof Number ? ((Number) params.get("limit")).intValue() : 20; List results = recommendationService.getDimensionProducts( dimensionCode, familyId, limit); return Result.success(results); } @Operation(summary = "查询复购提醒列表") @PostMapping("/repurchase-reminders") public Result> repurchaseReminders(HttpServletRequest request) { Long userId = (Long) request.getAttribute("userId"); List results = recommendationService.getRepurchaseReminders(userId); return Result.success(results); } @Operation(summary = "复购提醒点击记录") @PostMapping("/repurchase-reminder/{id}/click") public Result repurchaseClick(@PathVariable Long id) { recommendationService.markRepurchaseClicked(id); return Result.success(true); } ``` 需要的 import: ```java import javax.servlet.http.HttpServletRequest; import java.util.Map; import org.springframework.web.bind.annotation.PathVariable; ``` **注意**: `/repurchase-reminders` 端点的前端 `api.js` 中定义为 GET 方法,但项目规范统一使用 `@PostMapping`。修改前端 `api.js` 同步为 POST: ```javascript // cfc-frontend/utils/api.js 第 1740-1741 行 export const getRepurchaseReminders = (params) => { return request('/api/recommend/repurchase-reminders', 'POST', params) } ``` ### Step 3: 编译验证 ``` cd cfc-backend && mvn clean compile ``` 预期:BUILD SUCCESS,无新增编译错误 ### Step 4: 提交 ``` git add ... && git commit -m "feat: 维度商品推荐 + 复购提醒端点实现" --- ## Task 2: 复购提醒定时任务 **Files:** - Create: `cfc-backend/src/main/java/com/etotem/cfc/task/RepurchaseReminderScheduledTask.java` - Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/RecommendationService.java` (新增生成提醒方法) ### Step 1: RecommendationService 新增生成复购提醒方法 ```java /** * 每日定时任务:为符合条件的用户生成复购提醒 * * 逻辑:查询最近N天内有购买记录的商品,按 repurchase_reminder_config 的 * reminder_days 配置判断是否需要提醒,生成提醒记录。 */ public void generateDailyRepurchaseReminders() { // 1. 获取所有启用的复购提醒配置 List configs = repurchaseReminderConfigMapper.selectList( new LambdaQueryWrapper() .eq(RepurchaseReminderConfig::getEnabled, 1) ); if (configs.isEmpty()) return; for (RepurchaseReminderConfig config : configs) { int reminderDays = config.getReminderDays() != null ? config.getReminderDays() : 30; Date since = new Date(System.currentTimeMillis() - reminderDays * 24L * 60 * 60 * 1000); // 2. 查询该类目下已送达且超过提醒天数的商品订单 List orders = productOrderMapper.selectList( new LambdaQueryWrapper() .eq(ProductOrder::getStatus, "received") .ge(ProductOrder::getCreatedAt, since) .last("LIMIT 500") ); for (ProductOrder order : orders) { // 3. 检查对应商品是否匹配提醒配置类目 Product product = productMapper.selectById(order.getProductId()); if (product == null) continue; if (config.getProductCategory() != null && !config.getProductCategory().isEmpty() && !config.getProductCategory().equals(product.getCategoryId() != null ? String.valueOf(product.getCategoryId()) : "")) { continue; } // 4. 检查是否已生成过提醒(去重) int existingCount = repurchaseReminderRecordMapper.selectCount( new LambdaQueryWrapper() .eq(RepurchaseReminderRecord::getUserId, order.getBuyerId()) .eq(RepurchaseReminderRecord::getProductId, order.getProductId()) .eq(RepurchaseReminderRecord::getPurchased, 0) ); if (existingCount > 0) continue; // 5. 生成提醒记录 RepurchaseReminderRecord record = new RepurchaseReminderRecord(); record.setUserId(order.getBuyerId()); record.setProductId(order.getProductId()); record.setOrderId(order.getId()); record.setReminderDays(reminderDays); record.setSentAt(new Date()); record.setClicked(0); record.setPurchased(0); repurchaseReminderRecordMapper.insert(record); log.info("生成复购提醒: userId={}, productId={}, orderId={}", order.getBuyerId(), order.getProductId(), order.getId()); } } } ``` 需要的 import: ```java import com.etotem.cfc.entity.ProductOrder; import com.etotem.cfc.entity.RepurchaseReminderConfig; import com.etotem.cfc.mapper.ProductOrderMapper; import com.etotem.cfc.mapper.RepurchaseReminderConfigMapper; // 在 @Resource 区域新增: @Resource private RepurchaseReminderConfigMapper repurchaseReminderConfigMapper; @Resource private ProductOrderMapper productOrderMapper; ``` ### Step 2: 创建 RepurchaseReminderScheduledTask ```java package com.etotem.cfc.task; import com.etotem.cfc.service.RecommendationService; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import javax.annotation.Resource; @Slf4j @Component public class RepurchaseReminderScheduledTask { @Resource private RecommendationService recommendationService; /** * 每日凌晨2点执行复购提醒生成 */ @Scheduled(cron = "0 0 2 * * ?") public void generateReminders() { log.info("===== RepurchaseReminderScheduledTask 开始执行 ====="); try { recommendationService.generateDailyRepurchaseReminders(); } catch (Exception e) { log.error("复购提醒定时任务执行失败", e); } log.info("===== RepurchaseReminderScheduledTask 执行完成 ====="); } } ``` ### Step 3: 编译验证 ``` cd cfc-backend && mvn clean compile ``` 预期:BUILD SUCCESS ### Step 4: 提交 ``` git add ... && git commit -m "feat: 复购提醒定时任务 (每日2点)" ``` --- ## Task 3: 前端 api.js GET→POST 修正 **Files:** - Modify: `cfc-frontend/utils/api.js:1740-1741` ### Step 1: 修改请求方法 ```javascript // 修改前: export const getRepurchaseReminders = (params) => { return request('/api/recommend/repurchase-reminders', 'GET', params) } // 修改后: export const getRepurchaseReminders = (params) => { return request('/api/recommend/repurchase-reminders', 'POST', params) } ``` 同时确认 `clickRepurchaseReminder` 已正确调用。`DimensionProductList.vue` 第31行已 import,但第71-78行的 `goProduct()` 方法中注释掉了点击追踪调用。解除注释: ```javascript // DimensionProductList.vue goProduct() 方法修改: goProduct: function(product) { if (!product || !product.id) return // 调用点击追踪 clickRepurchaseReminder(product.id).catch(function() {}) uni.navigateTo({ url: '/pages/discover/product-detail/product-detail?id=' + product.id + '&from=dimension' }) } ``` ### Step 2: 提交 ``` git add ... && git commit -m "fix: 复购提醒API GET→POST + 点击追踪调用" ``` --- ## Task 4: DAN 测评商城化 Phase 2-5 计划框架 **范围过大,本次不实施**。创建独立计划文档标记待办: **Files:** - Create: `docs/superpowers/plans/2026-07-13-dan-assessment-phase2-5.md` 内容:引用 `docs/assessment-market-plan.md` 的 Phase 2-5 设计,拆分为独立的计划文档。 按 `assessment-market-plan.md` 的 5 阶段依赖: 1. 阶段二 (P0): 测评额度表 + 采购执行解耦 → `AssessmentQuotaService` 2. 阶段三 (P1): 报告双通道 (规划师录入 + 家长上传 + API获取) 3. 阶段四 (P1): 方案生成引擎 (AI + 维度配置) 4. 阶段五 (P1): 审核→确认→执行闭环 在计划文档中标记为"待启动",后续根据业务优先级决定实施顺序。 --- ## 验证标准 | 验证项 | 方法 | |--------|------| | `/api/recommend/dimension-products` 可用 | POST `{"dimensionCode":"body","limit":6}` → 返回按 matchScore 排序的商品列表 | | `/api/recommend/repurchase-reminders` 可用 | POST JWT认证 → 返回用户复购提醒列表 | | 复购定时任务可触发 | 启动应用 → 观察日志 "RepurchaseReminderScheduledTask 开始执行" | | `mvn clean compile` BUILD SUCCESS | 无新增编译错误 |