Parcourir la source

S-cfc-237 teacher.mind.record: 规划师舌诊/情绪测评统一录入链路落地(SC-T-25)

- 新增 MindAssessmentController(POST /api/mind/assessment/create & /list,
  代录受评客户按 memberId 归属,录入规划师按 guideId 归属)
- 新增 MindAssessmentService(createAssessment 入库 / getAssessments 按
  assessmentDate 倒序)
- 新增 MindPsychologicalAssessment 实体 + Mapper(memberId/guideId/
  assessmentType tongue|emotion /status 默认 draft→confirmed /assessmentDate)
- 迁移468 建表 mind_psychological_assessments + 同步 schema.sql
- 红单测 TeacherMindRecordContractTest 13/13 转绿,mvn clean compile 通过
Sisyphus il y a 2 heures
Parent
commit
f4107b69d8

+ 36 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -3504,6 +3504,10 @@ insertSysConfigSeed("gift_membership_pay_price", "198000", "送会员商品达
         // 本列仅记录「某条服务角色申请挂在哪个邀请人名下」,与 users.referrerId(用户级推荐人)并存互不替代,
         // 本列仅记录「某条服务角色申请挂在哪个邀请人名下」,与 users.referrerId(用户级推荐人)并存互不替代,
         // 且服务端写入时不触发任何分销/佣金副作用(物化推荐树/directCount/F24 CF 等)。
         // 且服务端写入时不触发任何分销/佣金副作用(物化推荐树/directCount/F24 CF 等)。
         addServiceRoleApplicationInviterColumn();
         addServiceRoleApplicationInviterColumn();
+
+        // 迁移468: 创建心理测评记录表 mind_psychological_assessments
+        //(S-cfc-237 teacher.mind.record · SC-T-25 舌诊/情绪测评录入(规划师)统一录入链路承载表)
+        createMindPsychologicalAssessmentTable();
     }
     }
 
 
     /**
     /**
@@ -15740,6 +15744,38 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
         }
         }
     }
     }
 
 
+    /**
+     * 迁移468: 创建心理测评记录表(S-cfc-237 teacher.mind.record · SC-T-25 舌诊/情绪测评录入(规划师))。
+     *
+     * <p>统一承载规划师代客户录入的舌诊(tongue)/情绪(emotion)测评记录:受评客户按 memberId 归属
+     * (用户模型统一口径)、录入规划师按 guideId 归属、status 默认 draft → confirmed 状态机(对齐舌诊链)、
+     * assessmentDate 为倒序查询排序键。幂等(CREATE TABLE IF NOT EXISTS)。</p>
+     */
+    private void createMindPsychologicalAssessmentTable() {
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS mind_psychological_assessments ("
+                    + "id BIGINT AUTO_INCREMENT PRIMARY KEY, "
+                    + "member_id BIGINT NULL COMMENT '受评客户家庭成员ID(memberId 归属,用户模型统一口径)', "
+                    + "guide_id BIGINT NULL COMMENT '录入规划师ID(guideId 归属,代录链路审计)', "
+                    + "assessment_type VARCHAR(32) NULL COMMENT '测评类型: tongue(舌诊)/emotion(情绪) 双类', "
+                    + "status VARCHAR(16) NOT NULL DEFAULT 'draft' COMMENT '状态机: draft(新建未确认)->confirmed(确认入库)', "
+                    + "assessment_date DATETIME NULL COMMENT '测评时间(倒序查询排序键)', "
+                    + "total_score INT NULL COMMENT '测评总分(可空)', "
+                    + "result_level VARCHAR(32) NULL COMMENT '结果等级(可空)', "
+                    + "result_summary VARCHAR(500) NULL COMMENT '结果摘要(可空)', "
+                    + "result_detail TEXT NULL COMMENT '结果明细(可空)', "
+                    + "report_url VARCHAR(500) NULL COMMENT '报告URL(可空)', "
+                    + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', "
+                    + "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', "
+                    + "INDEX idx_member_date (member_id, assessment_date), "
+                    + "INDEX idx_guide (guide_id) "
+                    + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='心理测评记录(舌诊/情绪统一录入,SC-T-25)'");
+            log.info("迁移468: 已创建 mind_psychological_assessments 表");
+        } catch (Exception e) {
+            log.warn("迁移468: 创建 mind_psychological_assessments 表失败(可能已存在): {}", e.getMessage());
+        }
+    }
+
     /** 幂等 upsert 维度种子:按 dimension_code 存在则补 source_system/legacy_alias,不存在则插入。 */
     /** 幂等 upsert 维度种子:按 dimension_code 存在则补 source_system/legacy_alias,不存在则插入。 */
     private void upsertDimensionSeed(String code, String name, String dimensionType, Long parentId,
     private void upsertDimensionSeed(String code, String name, String dimensionType, Long parentId,
                                      int sort, String sourceSystem, String nodeKind,
                                      int sort, String sourceSystem, String nodeKind,

+ 76 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/mind/MindAssessmentController.java

@@ -0,0 +1,76 @@
+package com.etotem.cfc.controller.mind;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.MindPsychologicalAssessment;
+import com.etotem.cfc.service.MindAssessmentService;
+import com.etotem.cfc.util.ParamUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestAttribute;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 规划师舌诊/情绪测评录入统一入口(能力键 {@code teacher.mind.record},SC-T-25)。
+ *
+ * <p>承载规划师代客户录入舌诊(tongue)/情绪(emotion)测评的统一链路:
+ * <ul>
+ *   <li>{@code POST /api/mind/assessment/create}:代录,受评客户按 {@code memberId} 归属,
+ *       录入规划师按当前登录 {@code userId} 归属(guideId),不落 userId/childId 表达受评主体;</li>
+ *   <li>{@code POST /api/mind/assessment/list}:按受评客户成员查看测评记录(assessmentDate 倒序)。</li>
+ * </ul>
+ *
+ * <p>契约溯源:docs/superpowers/specs/2026-11-07-planner-zone-contract.md §2(SC-T-25)/ §5.5;
+ * API_REFERENCE §4.57;设计 docs/backup/superpowers/plans/2026-06-02-care-family-club-design.md
+ * (controller/mind/MindAssessmentController)。权限流(仅绑定家庭客户可查)由 JwtInterceptor 角色隔离
+ * + SC-T-13 契约承担,本控制器不重复断言。
+ */
+@Slf4j
+@RestController
+@RequestMapping("/api/mind/assessment")
+public class MindAssessmentController {
+
+    @Resource
+    private MindAssessmentService mindAssessmentService;
+
+    /**
+     * 规划师代录:受评客户按 memberId 归属,录入规划师按当前登录 userId 归属(guideId)。
+     */
+    @PostMapping("/create")
+    public Result<?> create(@RequestBody MindPsychologicalAssessment assessment,
+                            @RequestAttribute("userId") Long userId) {
+        try {
+            if (assessment == null || assessment.getMemberId() == null) {
+                return Result.badRequest("受评客户 memberId 不能为空");
+            }
+            // 代录链路:录入规划师 = 当前登录账号(role=teacher)
+            if (assessment.getGuideId() == null) {
+                assessment.setGuideId(userId);
+            }
+            Object result = mindAssessmentService.createAssessment(assessment);
+            return Result.success(result);
+        } catch (Exception e) {
+            log.error("创建心理测评记录失败", e);
+            return Result.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 按受评客户成员查看测评记录(assessmentDate 倒序)。
+     */
+    @PostMapping("/list")
+    public Result<?> list(@RequestBody Map<String, Object> params,
+                          @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
+        if (memberId == null) {
+            return Result.badRequest("受评客户 memberId 不能为空");
+        }
+        List<MindPsychologicalAssessment> result = mindAssessmentService.getAssessments(memberId);
+        return Result.success(result);
+    }
+}

+ 65 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/MindPsychologicalAssessment.java

@@ -0,0 +1,65 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 心理测评记录载体(能力键 {@code teacher.mind.record},SC-T-25 舌诊/情绪测评录入·规划师)。
+ *
+ * <p>统一承载规划师代客户录入的舌诊(tongue)/情绪(emotion)测评记录:
+ * <ul>
+ *   <li>{@code memberId}:受评客户家庭成员归属(遵循用户模型统一口径,禁止回退 userId/childId 表达受评主体);</li>
+ *   <li>{@code guideId}:录入规划师(role=teacher)归属,代录链路审计必备;</li>
+ *   <li>{@code assessmentType}:tongue(舌诊)/ emotion(情绪)双类;非 tongue 类型不得落入舌诊链;</li>
+ *   <li>{@code status}:状态机 draft(新建未确认)→ confirmed(规划师确认入库),默认 draft,对齐舌诊链语义;</li>
+ *   <li>{@code assessmentDate}:测评时间,{@code getAssessments} 按此倒序返回。</li>
+ * </ul>
+ *
+ * <p>契约溯源:docs/superpowers/specs/2026-11-07-planner-zone-contract.md §2(SC-T-25)/ §5.5;
+ * API_REFERENCE §4.57;设计 docs/backup/superpowers/plans/2026-06-02-care-family-club-design.md(mind 域实体)。
+ */
+@Data
+@TableName("mind_psychological_assessments")
+public class MindPsychologicalAssessment {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 受评客户家庭成员归属(用户模型统一口径,不用 userId/childId)。 */
+    private Long memberId;
+
+    /** 录入规划师(role=teacher)归属。 */
+    private Long guideId;
+
+    /** 测评类型:tongue(舌诊)/ emotion(情绪)双类(SC-T-25)。 */
+    private String assessmentType;
+
+    /** 测评记录状态机:draft(新建未确认)→ confirmed(规划师确认入库),默认 draft。 */
+    private String status = "draft";
+
+    /** 测评时间(getAssessments 倒序排序键)。 */
+    private LocalDateTime assessmentDate;
+
+    /** 测评总分(情绪自评等,可空)。 */
+    private Integer totalScore;
+
+    /** 结果等级(可空)。 */
+    private String resultLevel;
+
+    /** 结果摘要(可空)。 */
+    private String resultSummary;
+
+    /** 结果明细(可空)。 */
+    private String resultDetail;
+
+    /** 报告 URL(可空)。 */
+    private String reportUrl;
+
+    private LocalDateTime createdAt;
+
+    private LocalDateTime updatedAt;
+}

+ 13 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/MindPsychologicalAssessmentMapper.java

@@ -0,0 +1,13 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.MindPsychologicalAssessment;
+
+/**
+ * 心理测评记录 Mapper(能力键 {@code teacher.mind.record},SC-T-25)。
+ *
+ * <p>由 {@code @MapperScan("com.etotem.cfc.mapper")} 扫描注册,无需 {@code @Mapper} 注解。
+ * 对应表 {@code mind_psychological_assessments}(迁移468)。
+ */
+public interface MindPsychologicalAssessmentMapper extends BaseMapper<MindPsychologicalAssessment> {
+}

+ 63 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/MindAssessmentService.java

@@ -0,0 +1,63 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.MindPsychologicalAssessment;
+import com.etotem.cfc.mapper.MindPsychologicalAssessmentMapper;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 统一录入服务(能力键 {@code teacher.mind.record},SC-T-25 舌诊/情绪测评录入·规划师)。
+ *
+ * <p>规划师端舌诊/情绪测评录入统一入口的服务载体:桥接既有双类载体语义
+ * (舌诊链 {@code TongueDiagnosisService} + 情绪链 {@code EmotionCheckinService}),
+ * 统一落库 {@link MindPsychologicalAssessment}(memberId 受评客户归属 + guideId 录入规划师归属 +
+ * assessmentType tongue/emotion 双类 + status draft→confirmed 状态机 + assessmentDate 测评时间)。
+ *
+ * <p>契约溯源:docs/superpowers/specs/2026-11-07-planner-zone-contract.md §2(SC-T-25)/ §5.5;
+ * API_REFERENCE §4.57;设计 docs/backup/superpowers/plans/2026-06-02-care-family-club-design.md
+ * (MindAssessmentService.createAssessment → mapper.insert / getAssessments orderByDesc(assessmentDate))。
+ */
+@Service
+public class MindAssessmentService {
+
+    @Resource
+    private MindPsychologicalAssessmentMapper mindPsychologicalAssessmentMapper;
+
+    /**
+     * 测评记录入库:默认 status=draft、assessmentDate=now,落库后回填自增 id。
+     */
+    public MindPsychologicalAssessment createAssessment(MindPsychologicalAssessment assessment) {
+        if (assessment == null) {
+            throw new IllegalArgumentException("assessment 不能为空");
+        }
+        if (assessment.getStatus() == null || assessment.getStatus().isEmpty()) {
+            assessment.setStatus("draft");
+        }
+        if (assessment.getAssessmentDate() == null) {
+            assessment.setAssessmentDate(LocalDateTime.now());
+        }
+        if (assessment.getCreatedAt() == null) {
+            assessment.setCreatedAt(LocalDateTime.now());
+        }
+        if (assessment.getUpdatedAt() == null) {
+            assessment.setUpdatedAt(LocalDateTime.now());
+        }
+        mindPsychologicalAssessmentMapper.insert(assessment);
+        return assessment;
+    }
+
+    /**
+     * 按受评客户成员查询测评记录,{@code assessmentDate} 倒序。
+     */
+    public List<MindPsychologicalAssessment> getAssessments(Long memberId) {
+        return mindPsychologicalAssessmentMapper.selectList(
+                new LambdaQueryWrapper<MindPsychologicalAssessment>()
+                        .eq(MindPsychologicalAssessment::getMemberId, memberId)
+                        .orderByDesc(MindPsychologicalAssessment::getAssessmentDate)
+        );
+    }
+}

+ 21 - 0
cfc-backend/src/main/resources/schema.sql

@@ -7218,3 +7218,24 @@ CREATE TABLE IF NOT EXISTS service_cost_config (
     updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
     updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
     UNIQUE KEY uk_service_cost (service_type, product_id)
     UNIQUE KEY uk_service_cost (service_type, product_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='服务成本价配置';
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='服务成本价配置';
+
+-- ============================================================
+-- 心理测评记录表(迁移468,S-cfc-237 teacher.mind.record · SC-T-25 舌诊/情绪测评录入(规划师)统一录入链路)
+-- ============================================================
+CREATE TABLE IF NOT EXISTS mind_psychological_assessments (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    member_id BIGINT NULL COMMENT '受评客户家庭成员ID(memberId 归属,用户模型统一口径)',
+    guide_id BIGINT NULL COMMENT '录入规划师ID(guideId 归属,代录链路审计)',
+    assessment_type VARCHAR(32) NULL COMMENT '测评类型: tongue(舌诊)/emotion(情绪) 双类',
+    status VARCHAR(16) NOT NULL DEFAULT 'draft' COMMENT '状态机: draft(新建未确认)->confirmed(确认入库)',
+    assessment_date DATETIME NULL COMMENT '测评时间(倒序查询排序键)',
+    total_score INT NULL COMMENT '测评总分(可空)',
+    result_level VARCHAR(32) NULL COMMENT '结果等级(可空)',
+    result_summary VARCHAR(500) NULL COMMENT '结果摘要(可空)',
+    result_detail TEXT NULL COMMENT '结果明细(可空)',
+    report_url VARCHAR(500) NULL COMMENT '报告URL(可空)',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    INDEX idx_member_date (member_id, assessment_date),
+    INDEX idx_guide (guide_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='心理测评记录(舌诊/情绪统一录入,SC-T-25)';