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 任务 |
Files:
cfc-backend/src/main/java/com/etotem/cfc/controller/ai/RecommendationController.javacfc-backend/src/main/java/com/etotem/cfc/service/RecommendationService.javacfc-backend/src/main/java/com/etotem/cfc/mapper/ProductDimensionMappingMapper.java (already exists, unused)在 RecommendationResult.java 的 private String source; 之后新增:
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)
在 RecommendationService.java 末尾添加两个方法:
@Resource
private ProductDimensionMappingMapper productDimensionMappingMapper;
/**
* 按维度查询关联商品(按 matchScore 降序)
*/
public List<RecommendationResult> getDimensionProducts(String dimensionCode, Long familyId, Integer limit) {
List<ProductDimensionMapping> mappings = productDimensionMappingMapper.selectList(
new LambdaQueryWrapper<ProductDimensionMapping>()
.eq(ProductDimensionMapping::getDimensionCode, dimensionCode)
.eq(ProductDimensionMapping::getEnabled, 1)
.orderByDesc(ProductDimensionMapping::getMatchScore)
.last(limit != null ? "LIMIT " + Math.min(limit, 20) : "LIMIT 20")
);
List<RecommendationResult> 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<RecommendationResult> getRepurchaseReminders(Long userId) {
List<RepurchaseReminderRecord> reminders = repurchaseReminderRecordMapper.selectList(
new LambdaQueryWrapper<RepurchaseReminderRecord>()
.eq(RepurchaseReminderRecord::getUserId, userId)
.eq(RepurchaseReminderRecord::getPurchased, 0)
.orderByDesc(RepurchaseReminderRecord::getSentAt)
);
List<RecommendationResult> 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 注入在类顶部新增:
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;
在 RecommendationController.java 末尾(} 之前)新增:
@Operation(summary = "按维度查询推荐商品")
@PostMapping("/dimension-products")
public Result<List<RecommendationResult>> dimensionProducts(@RequestBody Map<String, Object> 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<RecommendationResult> results = recommendationService.getDimensionProducts(
dimensionCode, familyId, limit);
return Result.success(results);
}
@Operation(summary = "查询复购提醒列表")
@PostMapping("/repurchase-reminders")
public Result<List<RecommendationResult>> repurchaseReminders(HttpServletRequest request) {
Long userId = (Long) request.getAttribute("userId");
List<RecommendationResult> results = recommendationService.getRepurchaseReminders(userId);
return Result.success(results);
}
@Operation(summary = "复购提醒点击记录")
@PostMapping("/repurchase-reminder/{id}/click")
public Result<Boolean> repurchaseClick(@PathVariable Long id) {
recommendationService.markRepurchaseClicked(id);
return Result.success(true);
}
需要的 import:
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:
// cfc-frontend/utils/api.js 第 1740-1741 行
export const getRepurchaseReminders = (params) => {
return request('/api/recommend/repurchase-reminders', 'POST', params)
}
cd cfc-backend && mvn clean compile
预期:BUILD SUCCESS,无新增编译错误
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 /**
reminder_days 配置判断是否需要提醒,生成提醒记录。 */ public void generateDailyRepurchaseReminders() { // 1. 获取所有启用的复购提醒配置 List configs = repurchaseReminderConfigMapper.selectList(
new LambdaQueryWrapper<RepurchaseReminderConfig>()
.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<ProductOrder> orders = productOrderMapper.selectList(
new LambdaQueryWrapper<ProductOrder>()
.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<RepurchaseReminderRecord>()
.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 + 点击追踪调用" ```
范围过大,本次不实施。创建独立计划文档标记待办:
Files:
docs/superpowers/plans/2026-07-13-dan-assessment-phase2-5.md内容:引用 docs/assessment-market-plan.md 的 Phase 2-5 设计,拆分为独立的计划文档。
按 assessment-market-plan.md 的 5 阶段依赖:
AssessmentQuotaService在计划文档中标记为"待启动",后续根据业务优先级决定实施顺序。
| 验证项 | 方法 |
|---|---|
/api/recommend/dimension-products 可用 |
POST {"dimensionCode":"body","limit":6} → 返回按 matchScore 排序的商品列表 |
/api/recommend/repurchase-reminders 可用 |
POST JWT认证 → 返回用户复购提醒列表 |
| 复购定时任务可触发 | 启动应用 → 观察日志 "RepurchaseReminderScheduledTask 开始执行" |
mvn clean compile BUILD SUCCESS |
无新增编译错误 |