Pārlūkot izejas kodu

feat(health): 方案维护功能 — 规划师审核发布流程

- HealthPlan实体新增 reviewed_by/reviewed_at/review_comment/updatedAt 字段
- DatabaseInitializer: 迁移250 新增审核字段,status枚举扩展 pending_review/published/rejected
- HealthPlanService: 新增 listPendingReviewPlans/updatePlanContent/approveAndPublish/rejectPlan 方法
- HealthPlanServiceImpl: savePlan 默认 pending_review 状态,审核通过后生成任务
- HealthPlanController: 新增 /pending-review/{list,update,approve,reject} 四个端点

流程: 用户生成方案 → draft/pending_review → 规划师编辑 → 审核通过(published) → 自动生成任务
iwt 1 mēnesi atpakaļ
vecāks
revīzija
c4744618

+ 9 - 1
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -2961,10 +2961,18 @@ log.info("已添加template_id列到tasks表");
 			log.warn("health_plans.plan_json 添加失败(可能已存在): " + e.getMessage());
 		}
 		try {
-			ensureColumn("health_plans", "status", "VARCHAR(20) DEFAULT 'draft' COMMENT '方案状态: draft(草稿)/confirmed(已确认)'");
+			ensureColumn("health_plans", "status", "VARCHAR(20) DEFAULT 'draft' COMMENT '方案状态: draft(草稿)/pending_review(待审核)/published(已发布)/rejected(已驳回)'");
 		} catch (Exception e) {
 			log.warn("health_plans.status 添加失败(可能已存在): " + e.getMessage());
 		}
+		// 迁移250: health_plans 新增审核字段(规划师/营养师方案维护流程)
+		try {
+			ensureColumn("health_plans", "reviewed_by", "BIGINT DEFAULT NULL COMMENT '审核人ID(规划师/营养师)'");
+			ensureColumn("health_plans", "reviewed_at", "DATETIME DEFAULT NULL COMMENT '审核时间'");
+			ensureColumn("health_plans", "review_comment", "TEXT DEFAULT NULL COMMENT '审核意见'");
+		} catch (Exception e) {
+			log.warn("health_plans 审核字段添加失败(可能已存在): " + e.getMessage());
+		}
         ensureColumn("file_record", "file_type", "VARCHAR(50) DEFAULT NULL COMMENT '文件类型: image/pdf/video/audio/other'");
         ensureColumn("file_record", "description", "TEXT COMMENT '文件描述'");
 

+ 67 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/HealthPlanController.java

@@ -112,4 +112,70 @@ public class HealthPlanController {
         if (plan == null) return Result.error("方案不存在");
         return Result.success(plan);
     }
-}
+
+    // ===== 规划师方案维护接口 =====
+
+    @Operation(summary = "规划师查看待审核方案列表")
+    @PostMapping("/pending-review/list")
+    public Result<List<HealthPlan>> listPendingReview(
+            @RequestBody Map<String, Object> params,
+            @RequestAttribute("userId") Long userId,
+            @RequestAttribute("role") String role,
+            @RequestAttribute(value = "familyId", required = false) Long familyId) {
+        if (!"teacher".equals(role) && !"admin".equals(role)) {
+            return Result.error("无权限");
+        }
+        if (familyId == null) return Result.error("familyId不能为空");
+        List<HealthPlan> list = healthPlanService.listPendingReviewPlans(familyId);
+        return Result.success(list);
+    }
+
+    @Operation(summary = "规划师编辑方案内容")
+    @PostMapping("/pending-review/update")
+    public Result<HealthPlan> updatePlan(
+            @RequestBody Map<String, Object> params,
+            @RequestAttribute("userId") Long userId,
+            @RequestAttribute("role") String role) {
+        if (!"teacher".equals(role) && !"admin".equals(role)) {
+            return Result.error("无权限");
+        }
+        Long planId = ParamUtils.getLong(params.get("planId"));
+        if (planId == null) return Result.error("planId不能为空");
+        String planContent = (String) params.get("planContent");
+        String planJson = (String) params.get("planJson");
+        HealthPlan plan = healthPlanService.updatePlanContent(planId, planContent, planJson);
+        return Result.success(plan);
+    }
+
+    @Operation(summary = "规划师审核通过并发布(自动生成任务)")
+    @PostMapping("/pending-review/approve")
+    public Result<HealthPlan> approvePlan(
+            @RequestBody Map<String, Object> params,
+            @RequestAttribute("userId") Long userId,
+            @RequestAttribute("role") String role) {
+        if (!"teacher".equals(role) && !"admin".equals(role)) {
+            return Result.error("无权限");
+        }
+        Long planId = ParamUtils.getLong(params.get("planId"));
+        if (planId == null) return Result.error("planId不能为空");
+        String comment = (String) params.get("comment");
+        HealthPlan plan = healthPlanService.approveAndPublish(planId, userId, comment);
+        return Result.success(plan);
+    }
+
+    @Operation(summary = "规划师驳回方案")
+    @PostMapping("/pending-review/reject")
+    public Result<HealthPlan> rejectPlan(
+            @RequestBody Map<String, Object> params,
+            @RequestAttribute("userId") Long userId,
+            @RequestAttribute("role") String role) {
+        if (!"teacher".equals(role) && !"admin".equals(role)) {
+            return Result.error("无权限");
+        }
+        Long planId = ParamUtils.getLong(params.get("planId"));
+        if (planId == null) return Result.error("planId不能为空");
+        String comment = (String) params.get("comment");
+        HealthPlan plan = healthPlanService.rejectPlan(planId, userId, comment);
+        return Result.success(plan);
+    }
+}

+ 4 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/HealthPlan.java

@@ -21,5 +21,9 @@ public class HealthPlan implements Serializable {
     @TableField("member_name") private String memberName;
     @TableField("plan_json") private String planJson;
     @TableField("status") private String status;
+    @TableField("reviewed_by") private Long reviewedBy;
+    @TableField("reviewed_at") private Date reviewedAt;
+    @TableField("review_comment") private String reviewComment;
     @TableField("created_at") private Date createdAt;
+    @TableField("updated_at") private Date updatedAt;
 }

+ 8 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/HealthPlanService.java

@@ -10,4 +10,12 @@ public interface HealthPlanService {
     HealthPlan getPlanDetail(Long planId);
     String generatePlan(Long familyId, String memberIds, String dimensions, String goal);
     String regenerateSection(Long familyId, String section, String feedback, String planJsonStr);
+    /** 规划师查看待审核方案列表 */
+    List<HealthPlan> listPendingReviewPlans(Long familyId);
+    /** 规划师编辑方案内容 */
+    HealthPlan updatePlanContent(Long planId, String planContent, String planJson);
+    /** 规划师审核通过并发布(同时生成任务) */
+    HealthPlan approveAndPublish(Long planId, Long reviewedBy, String comment);
+    /** 规划师驳回方案 */
+    HealthPlan rejectPlan(Long planId, Long reviewedBy, String comment);
 }

+ 72 - 8
cfc-backend/src/main/java/com/etotem/cfc/service/impl/HealthPlanServiceImpl.java

@@ -48,13 +48,20 @@ public class HealthPlanServiceImpl implements HealthPlanService {
     @Override
     public Long savePlan(HealthPlan plan) {
         plan.setCreatedAt(new Date());
+        // 新方案默认为 pending_review 状态,待规划师审核后发布
+        if (plan.getStatus() == null || plan.getStatus().isEmpty()) {
+            plan.setStatus("pending_review");
+        }
         healthPlanMapper.insert(plan);
-        try {
-            generateDailyTasksFromPlan(plan);
-            log.info("方案{}自动生成任务完成,familyId={}, memberIds={}",
-                plan.getId(), plan.getFamilyId(), plan.getMemberIds());
-        } catch (Exception e) {
-            log.warn("方案{}自动生成任务失败,不影响方案保存: {}", plan.getId(), e.getMessage());
+        // 仅在已发布状态才自动生成任务
+        if ("published".equals(plan.getStatus())) {
+            try {
+                generateDailyTasksFromPlan(plan);
+                log.info("方案{}发布时生成任务完成,familyId={}, memberIds={}",
+                    plan.getId(), plan.getFamilyId(), plan.getMemberIds());
+            } catch (Exception e) {
+                log.warn("方案{}发布时生成任务失败,不影响方案保存: {}", plan.getId(), e.getMessage());
+            }
         }
         return plan.getId();
     }
@@ -77,7 +84,8 @@ public class HealthPlanServiceImpl implements HealthPlanService {
     public Long confirmPlan(Long planId) {
         HealthPlan plan = healthPlanMapper.selectById(planId);
         if (plan == null) throw new RuntimeException("方案不存在");
-        plan.setStatus("confirmed");
+        plan.setStatus("published");
+        plan.setUpdatedAt(new Date());
         healthPlanMapper.updateById(plan);
         try {
             generateDailyTasksFromPlan(plan);
@@ -264,4 +272,60 @@ public class HealthPlanServiceImpl implements HealthPlanService {
         }
         return tasks;
     }
-}
+@Override
+    public List<HealthPlan> listPendingReviewPlans(Long familyId) {
+        com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<HealthPlan> w = new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<>();
+        w.eq("family_id", familyId);
+        w.in("status", "draft", "pending_review");
+        w.orderByDesc("created_at");
+        return healthPlanMapper.selectList(w);
+    }
+
+    @Override
+    public HealthPlan updatePlanContent(Long planId, String planContent, String planJson) {
+        HealthPlan plan = healthPlanMapper.selectById(planId);
+        if (plan == null) throw new RuntimeException("方案不存在");
+        if (!"pending_review".equals(plan.getStatus()) && !"draft".equals(plan.getStatus())) {
+            throw new RuntimeException("方案状态不允许编辑");
+        }
+        if (planContent != null) plan.setPlanContent(planContent);
+        if (planJson != null) plan.setPlanJson(planJson);
+        plan.setUpdatedAt(new Date());
+        healthPlanMapper.updateById(plan);
+        return plan;
+    }
+
+    @Override
+    public HealthPlan approveAndPublish(Long planId, Long reviewedBy, String comment) {
+        HealthPlan plan = healthPlanMapper.selectById(planId);
+        if (plan == null) throw new RuntimeException("方案不存在");
+        if (!"pending_review".equals(plan.getStatus()) && !"draft".equals(plan.getStatus())) {
+            throw new RuntimeException("方案状态不允许审核");
+        }
+        plan.setStatus("published");
+        plan.setReviewedBy(reviewedBy);
+        plan.setReviewedAt(new Date());
+        plan.setReviewComment(comment);
+        plan.setUpdatedAt(new Date());
+        healthPlanMapper.updateById(plan);
+        try {
+            generateDailyTasksFromPlan(plan);
+        } catch (Exception e) {
+            log.warn("方案{}发布时生成任务失败: {}", planId, e.getMessage());
+        }
+        return plan;
+    }
+
+    @Override
+    public HealthPlan rejectPlan(Long planId, Long reviewedBy, String comment) {
+        HealthPlan plan = healthPlanMapper.selectById(planId);
+        if (plan == null) throw new RuntimeException("方案不存在");
+        plan.setStatus("rejected");
+        plan.setReviewedBy(reviewedBy);
+        plan.setReviewedAt(new Date());
+        plan.setReviewComment(comment);
+        plan.setUpdatedAt(new Date());
+        healthPlanMapper.updateById(plan);
+        return plan;
+    }
+}

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-dd98d4317614957472c3f90ebcb1f9e6dda128e2
+c8dcc0a8125a803abaf226046ac16068eb880f32