Explorar o código

docs: supplier-system gap-fix plan + DAN Phase 2-5 framework

- 2026-07-13-supplier-system-gap-fix.md: gap analysis + 4-task
  implementation plan for dimension-products, repurchase-reminders,
  scheduled task, and frontend fixes
- 2026-07-13-dan-assessment-phase2-5.md: pending Phase 2-5 plan
  referencing assessment-market-plan.md — quota system, dual-channel
  reports, plan generation engine, and review→confirm→execute loop

Co-authored-by: Sisyphus <sisyphus@omo.codes>
Sisyphus hai 3 meses
pai
achega
140faf5afb

+ 100 - 0
docs/superpowers/plans/2026-07-13-dan-assessment-phase2-5.md

@@ -0,0 +1,100 @@
+# DAN 测评商城化 Phase 2-5 — 待启动计划
+
+> 创建日期: 2026-07-13
+> 状态: **待启动**
+> 前置: Phase 1(商品化)已完成
+> 设计文档: `docs/assessment-market-plan.md`
+
+---
+
+## 依赖关系
+
+```
+Phase 1 (商品化) ✅ 已完成
+    ↓
+Phase 2 (采购与执行解耦)  ←─ 依赖 Phase 1
+    ↓
+Phase 3 (报告双通道)  ←─ 可与 Phase 2 并行
+    ↓
+Phase 4 (方案生成引擎)  ←─ 依赖 Phase 3
+    ↓
+Phase 5 (审核→确认→执行)  ←─ 依赖 Phase 4
+```
+
+---
+
+## Phase 2: 采购与测评执行解耦 (P0)
+
+**目标:** 购买测评商品后获得"测评额度",具体测什么在测评时确定
+
+**核心新增:**
+- `assessment_quotas` 表 (额度管理)
+- `assessment_executions` 表 (执行记录)
+- `AssessmentQuotaService` — 额度发放/消耗/查询
+- 预约流程改造: 订单→额度→预约
+
+**验证要点:**
+- 购买后自动发放额度
+- 预约时消耗额度(剩余次数递减)
+- 额度过期自动标记
+
+---
+
+## Phase 3: 报告双通道 + 结构化指导意见 (P1)
+
+**目标:** 家长可上传报告,规划师录入结构化指导意见
+
+**核心新增:**
+- `DanAssessmentResult` 增加 `source` 字段 (planner_entry/parent_upload/auto_fetch)
+- `growth_guidances` 表 (结构化指导意见)
+- `FileStorageService` + `ReportParseService` (文件上传与解析)
+- SPI 扩展点: `ReportFetchProvider`
+
+**验证要点:**
+- 家长可上传 PDF/JPG 报告
+- 规划师可确认上传报告并补充评分
+- 指导意见从大文本升级为结构化多维度存储
+
+---
+
+## Phase 4: 结果→任务方案自动生成引擎 (P1)
+
+**目标:** 测评结果 + 指导意见 → 自动生成训练方案
+
+**核心新增:**
+- `assessment_plan_rules` 表 (规则配置)
+- `AssessmentPlanGenerator` — 规则匹配 + 模板合并
+- `TaskPlanInstance` 增加 source/source_result_id 字段
+
+**验证要点:**
+- 规则配置: 维度+分数区间→训练模板
+- 规划师提交结果后自动触发生成器
+- 方案状态为 generated(非直接激活)
+
+---
+
+## Phase 5: 方案审核→确认→执行跟踪 (P1)
+
+**目标:** 完整的方案生命周期管理
+
+**核心新增:**
+- 方案状态机: generated→pending_review→active→completed
+- `TaskGenerationService` — 每日任务生成
+- `PlanTaskGenerator` 定时任务 (每日5:30)
+- 复测提醒机制
+
+**验证要点:**
+- 规划师审核→家长确认→激活执行
+- 每日自动生成当日任务
+- 方案完成后触发复测提醒
+
+---
+
+## 启动条件
+
+选择任一 Phase 启动时,需确认:
+
+1. [ ] 对应 Phase 的设计文档已完成 (见 `assessment-market-plan.md`)
+2. [ ] 前置 Phase 已完成或可并行
+3. [ ] 数据库迁移已规划 (DatabaseInitializer)
+4. [ ] 前端页面已规划 (pages.json 注册)

+ 415 - 0
docs/superpowers/plans/2026-07-13-supplier-system-gap-fix.md

@@ -0,0 +1,415 @@
+# 供应商体系差距修复 — 实施计划
+
+> **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<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 注入在类顶部新增:
+
+```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<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:
+
+```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<RepurchaseReminderConfig> 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 | 无新增编译错误 |