2026-07-13-supplier-system-gap-fix.md 15 KB

供应商体系差距修复 — 实施计划

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.mddocs/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.javaprivate 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)

Step 1: RecommendationService 新增维度商品查询方法

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;

Step 2: RecommendationController 新增三个端点

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)
}

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<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 + 点击追踪调用" ```


    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 无新增编译错误