|
|
@@ -0,0 +1,779 @@
|
|
|
+# 五维手动填分 + MBTI 完整体系 实施计划
|
|
|
+
|
|
|
+> **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 (`- [ ]`) syntax for tracking.
|
|
|
+
|
|
|
+**Goal:** 实现"只填结果"的五维手动填分功能(0-100 分,家长可给家庭成员填),以及 MBTI 完整体系(标准版 ~30题测评 + 16型人格百科 + 五维联动解读)。
|
|
|
+
|
|
|
+**Architecture:** 后端新增两组 Controller/Service/Mapper/Entity;前端新增 5 个页面 + 1 个管理端页面;数据库 3 张新表(mbti_questions, mbti_results, mbti_profiles)通过 DatabaseInitializer 迁移;种子数据脚本初始化题库与 16 型画像。
|
|
|
+
|
|
|
+**Tech Stack:** Java 8 + Spring Boot 2.7.18 + MyBatis-Plus / uni-app Vue 2 (微信小程序) / Vue 2 + Element UI (Web 管理端) / MySQL 8.0
|
|
|
+
|
|
|
+**Spec:** 本计划基于前期 brainstorming 确认的设计,包含 Part 1(手动填分)与 Part 2(MBTI 体系)两大模块。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Global Constraints
|
|
|
+
|
|
|
+- 统一 `@PostMapping`,禁止 `@GetMapping/@PutMapping/@DeleteMapping`
|
|
|
+- 响应包装 `Result<T>` (code/message/data)
|
|
|
+- JWT Bearer Token 认证,`JwtInterceptor` 拦截 `/api/**`
|
|
|
+- DI 使用 `@Resource`,字段名必须与 Bean Name 一致
|
|
|
+- MyBatis-Plus `@TableName` + `@TableId(type = IdType.AUTO)`
|
|
|
+- 小程序限制:禁止 `?.`、CSS Grid、`:key` 表达式、`new Date(string)`(用 `parseDate()`)
|
|
|
+- 数据库迁移唯一入口:`DatabaseInitializer.runMigrations()`,使用 `ensureColumn` / try-catch 幂等
|
|
|
+- 每次迁移必须同步更新 `schema.sql` 对应 `CREATE TABLE`
|
|
|
+- 新增接口前必须查阅 `docs/superpowers/api/API_REFERENCE.md` 确认无重复
|
|
|
+- Bean 命名冲突检查:新增 Controller/Service 确保类名不重复
|
|
|
+- 编译验证:`mvn clean compile` 唯一验证方式
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 文件结构映射
|
|
|
+
|
|
|
+### 后端新增/修改文件
|
|
|
+
|
|
|
+| 文件 | 类型 | 说明 |
|
|
|
+|------|------|------|
|
|
|
+| `entity/MbtiQuestion.java` | 新增 | MBTI 题库实体 |
|
|
|
+| `entity/MbtiResult.java` | 新增 | 用户 MBTI 测评结果实体 |
|
|
|
+| `entity/MbtiProfile.java` | 新增 | 16型人格画像库实体 |
|
|
|
+| `mapper/MbtiQuestionMapper.java` | 新增 | 题库 Mapper |
|
|
|
+| `mapper/MbtiResultMapper.java` | 新增 | 结果 Mapper |
|
|
|
+| `mapper/MbtiProfileMapper.java` | 新增 | 画像 Mapper |
|
|
|
+| `service/MbtiService.java` | 新增 | MBTI 业务接口 |
|
|
|
+| `service/impl/MbtiServiceImpl.java` | 新增 | MBTI 业务实现 |
|
|
|
+| `controller/family/MbtiController.java` | 新增 | `/api/family/mbti` 接口 |
|
|
|
+| `controller/family/ManualDimensionScoreController.java` | 新增 | `/api/family/manual-score` 接口 |
|
|
|
+| `service/ManualDimensionScoreService.java` | 新增 | 手动填分业务接口 |
|
|
|
+| `service/impl/ManualDimensionScoreServiceImpl.java` | 新增 | 手动填分业务实现 |
|
|
|
+| `dto/MbtiQuestionVO.java` | 新增 | 题目 VO |
|
|
|
+| `dto/MbtiResultVO.java` | 新增 | 结果 VO |
|
|
|
+| `dto/MbtiProfileVO.java` | 新增 | 画像 VO |
|
|
|
+| `dto/ManualScoreSubmitDTO.java` | 新增 | 手动提交 DTO |
|
|
|
+| `dto/ManualScoreVO.java` | 新增 | 手动填分 VO |
|
|
|
+| `config/DatabaseInitializer.java` | 修改 | 添加 3 表迁移 |
|
|
|
+
|
|
|
+### 前端新增/修改文件
|
|
|
+
|
|
|
+| 文件 | 类型 | 说明 |
|
|
|
+|------|------|------|
|
|
|
+| `pages/family/manual-score-entry.vue` | 新增 | 手动填分录入页 |
|
|
|
+| `pages/family/mbti-entry.vue` | 新增 | MBTI 入口页(选成员) |
|
|
|
+| `pages/family/mbti-test.vue` | 新增 | MBTI 答题页 |
|
|
|
+| `pages/family/mbti-result.vue` | 新增 | MBTI 结果页 |
|
|
|
+| `pages/family/mbti-profile.vue` | 新增 | MBTI 16型百科页 |
|
|
|
+| `pages/family/self-check-entry.vue` | 修改 | 增加"直接填分"按钮 + MBTI 卡片入口 |
|
|
|
+| `utils/api.js` | 修改 | 新增 10+ API 封装 |
|
|
|
+| `pages.json` | 修改 | 注册 5 个新页面 |
|
|
|
+
|
|
|
+### 管理端新增文件
|
|
|
+
|
|
|
+| 文件 | 类型 | 说明 |
|
|
|
+|------|------|------|
|
|
|
+| `src/views/admin/MbtiProfiles.vue` | 新增 | 16 型配置管理页 |
|
|
|
+| `src/api/mbti.js` | 新增 | 管理端 MBTI API |
|
|
|
+| `src/router/index.js` | 修改 | 新增路由 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 任务分解
|
|
|
+
|
|
|
+### Phase 0: 数据库迁移与种子数据
|
|
|
+
|
|
|
+#### Task 0.1: DatabaseInitializer 添加 3 表迁移
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java` (搜索 `// 迁移` 找到最新编号)
|
|
|
+- Modify: `cfc-backend/src/main/resources/schema.sql` (追加 3 表 CREATE TABLE)
|
|
|
+
|
|
|
+**Interfaces:**
|
|
|
+- Produces: 3 张表结构在数据库中可用
|
|
|
+
|
|
|
+```markdown
|
|
|
+- [ ] **Step 1: 在 DatabaseInitializer.runMigrations() 末尾添加 3 个迁移块**
|
|
|
+```java
|
|
|
+// 迁移XXX: 创建 mbti_questions 表
|
|
|
+try {
|
|
|
+ jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS mbti_questions (" +
|
|
|
+ "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
|
|
|
+ "dimension CHAR(1) NOT NULL COMMENT 'E/I/S/N/T/F/J/P', " +
|
|
|
+ "question_text TEXT NOT NULL COMMENT '题目文本', " +
|
|
|
+ "option_a TEXT NOT NULL COMMENT '选项 A(正向)', " +
|
|
|
+ "option_b TEXT NOT NULL COMMENT '选项 B(反向)', " +
|
|
|
+ "weight DECIMAL(3,2) DEFAULT 1.0 COMMENT '权重', " +
|
|
|
+ "sort_order INT DEFAULT 0, " +
|
|
|
+ "is_active TINYINT DEFAULT 1, " +
|
|
|
+ "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
|
|
|
+ "INDEX idx_dim_active (dimension, is_active)" +
|
|
|
+ ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MBTI 测评题库'");
|
|
|
+ log.info("已创建 mbti_questions 表");
|
|
|
+} catch (Exception e) { /* 忽略 */ }
|
|
|
+
|
|
|
+// 迁移XXX+1: 创建 mbti_results 表
|
|
|
+try {
|
|
|
+ jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS mbti_results (" +
|
|
|
+ "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
|
|
|
+ "user_id BIGINT NOT NULL COMMENT '答题人用户ID', " +
|
|
|
+ "family_member_id BIGINT COMMENT '关联家庭成员ID', " +
|
|
|
+ "answers_json TEXT COMMENT '用户答案JSON', " +
|
|
|
+ "type_code CHAR(4) NOT NULL COMMENT 'MBTI 类型', " +
|
|
|
+ "ei_score INT COMMENT 'E/I 原始分', " +
|
|
|
+ "sn_score INT COMMENT 'S/N 原始分', " +
|
|
|
+ "tf_score INT COMMENT 'T/F 原始分', " +
|
|
|
+ "jp_score INT COMMENT 'J/P 原始分', " +
|
|
|
+ "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
|
|
|
+ "INDEX idx_user (user_id), " +
|
|
|
+ "INDEX idx_member (family_member_id)" +
|
|
|
+ ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MBTI 测评结果'");
|
|
|
+ log.info("已创建 mbti_results 表");
|
|
|
+} catch (Exception e) { /* 忽略 */ }
|
|
|
+
|
|
|
+// 迁移XXX+2: 创建 mbti_profiles 表
|
|
|
+try {
|
|
|
+ jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS mbti_profiles (" +
|
|
|
+ "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
|
|
|
+ "type_code CHAR(4) NOT NULL UNIQUE COMMENT '类型代码', " +
|
|
|
+ "name VARCHAR(50) COMMENT '中文名称', " +
|
|
|
+ "description TEXT COMMENT '核心特质描述', " +
|
|
|
+ "strengths TEXT COMMENT '优势', " +
|
|
|
+ "weaknesses TEXT COMMENT '挑战', " +
|
|
|
+ "career_tendency TEXT COMMENT '职业倾向', " +
|
|
|
+ "family_style TEXT COMMENT '家庭/育儿风格', " +
|
|
|
+ "pairing_notes TEXT COMMENT '伴侣/孩子类型搭配建议', " +
|
|
|
+ "energy_suggestion JSON COMMENT '五维能量联动建议', " +
|
|
|
+ "is_active TINYINT DEFAULT 1, " +
|
|
|
+ "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
|
|
|
+ "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" +
|
|
|
+ ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MBTI 人格画像库'");
|
|
|
+ log.info("已创建 mbti_profiles 表");
|
|
|
+} catch (Exception e) { /* 忽略 */ }
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 同步追加到 schema.sql 末尾**
|
|
|
+```sql
|
|
|
+-- 与上面 3 个 CREATE TABLE IF NOT EXISTS 完全一致
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 编译验证**
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn clean compile
|
|
|
+```
|
|
|
+
|
|
|
+#### Task 0.2: MBTI 种子数据 SQL 脚本
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `cfc-backend/src/main/resources/migration_mbti_seed_data.sql`
|
|
|
+
|
|
|
+**Interfaces:**
|
|
|
+- Produces: 30 题题库 + 16 型基础画像数据
|
|
|
+
|
|
|
+```markdown
|
|
|
+- [ ] **Step 1: 编写种子数据脚本**
|
|
|
+包含:
|
|
|
+- mbti_questions: 30 题(E/I 8题, S/N 8题, T/F 7题, J/P 7题),每题 dimension + question_text + option_a/option_b + weight + sort_order
|
|
|
+- mbti_profiles: 16 条记录(INTJ/INTP/ENTJ/ENTP/INFJ/INFP/ENFJ/ENFP/ISTJ/ISFJ/ESTJ/ESFJ/ISTP/ISFP/ESTP/ESFP),每条含 name/description/strengths/weaknesses/career_tendency/family_style/pairing_notes/energy_suggestion(JSON)
|
|
|
+
|
|
|
+- [ ] **Step 2: 在 DatabaseInitializer 执行种子数据**
|
|
|
+```java
|
|
|
+// 迁移XXX+3: MBTI 种子数据初始化
|
|
|
+try {
|
|
|
+ String seedSql = FileUtils.readFileToString(new File("src/main/resources/migration_mbti_seed_data.sql"), "UTF-8");
|
|
|
+ for (String stmt : seedSql.split(";")) {
|
|
|
+ String trimmed = stmt.trim();
|
|
|
+ if (!trimmed.isEmpty()) {
|
|
|
+ jdbcTemplate.execute(trimmed);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ log.info("MBTI 种子数据初始化完成");
|
|
|
+} catch (Exception e) {
|
|
|
+ log.warn("MBTI 种子数据初始化失败(可能已存在): {}", e.getMessage());
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 编译验证**
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn clean compile
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Phase 1: 后端手动填分模块
|
|
|
+
|
|
|
+#### Task 1.1: ManualDimensionScore 实体、Mapper、DTO
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `entity/ManualDimensionScore.java` (可复用 FiveDimensionScore,仅 source_type='manual' 区分,无需新表)
|
|
|
+- Create: `dto/ManualScoreSubmitDTO.java`
|
|
|
+- Create: `dto/ManualScoreVO.java`
|
|
|
+
|
|
|
+```markdown
|
|
|
+- [ ] **Step 1: ManualScoreSubmitDTO**
|
|
|
+```java
|
|
|
+package com.etotem.cfc.dto;
|
|
|
+import lombok.Data;
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+@Data
|
|
|
+public class ManualScoreSubmitDTO {
|
|
|
+ private Long memberId; // 必填
|
|
|
+ private Map<String, Integer> scores; // 必填: {"body":80,"mind":60,"wisdom":70,"action":50,"wealth":90}
|
|
|
+ private String note; // 可选备注
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: ManualScoreVO (返回前端)**
|
|
|
+```java
|
|
|
+package com.etotem.cfc.dto;
|
|
|
+import lombok.Data;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.List;
|
|
|
+
|
|
|
+@Data
|
|
|
+public class ManualScoreVO {
|
|
|
+ private Long id; // five_dimension_scores.id
|
|
|
+ private Long memberId;
|
|
|
+ private String memberName;
|
|
|
+ private Map<String, Integer> scores; // 5维度分数
|
|
|
+ private Integer totalScore; // 5维总和
|
|
|
+ private Date createdAt;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 编译验证**
|
|
|
+
|
|
|
+#### Task 1.2: ManualDimensionScoreService 接口与实现
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `service/ManualDimensionScoreService.java`
|
|
|
+- Create: `service/impl/ManualDimensionScoreServiceImpl.java`
|
|
|
+
|
|
|
+```markdown
|
|
|
+- [ ] **Step 1: 接口定义**
|
|
|
+```java
|
|
|
+package com.etotem.cfc.service;
|
|
|
+import com.etotem.cfc.dto.ManualScoreSubmitDTO;
|
|
|
+import com.etotem.cfc.dto.ManualScoreVO;
|
|
|
+import java.util.List;
|
|
|
+
|
|
|
+public interface ManualDimensionScoreService {
|
|
|
+ ManualScoreVO submit(ManualScoreSubmitDTO dto, Long userId);
|
|
|
+ ManualScoreVO getLatest(Long userId, Long memberId);
|
|
|
+ List<ManualScoreVO> getHistory(Long userId, Long memberId, int limit);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 实现类**
|
|
|
+```java
|
|
|
+@Service
|
|
|
+public class ManualDimensionScoreServiceImpl implements ManualDimensionScoreService {
|
|
|
+ @Resource private FiveDimensionScoreMapper fiveDimensionScoreMapper;
|
|
|
+ @Resource private FiveDimensionSelfCheckMapper selfCheckMapper;
|
|
|
+ @Resource private FamilyMemberMapper familyMemberMapper;
|
|
|
+ @Resource private UserMapper userMapper;
|
|
|
+ @Resource private FiveDimensionScoreService fiveDimensionScoreService;
|
|
|
+ private final ObjectMapper objectMapper = new ObjectMapper();
|
|
|
+
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public ManualScoreVO submit(ManualScoreSubmitDTO dto, Long userId) {
|
|
|
+ // 1. 校验 memberId 存在且属于当前用户家庭
|
|
|
+ // 2. 校验 5 维度键全为 body/mind/wisdom/action/wealth,值 0-100,至少一项>0
|
|
|
+ // 3. 写入 five_dimension_scores (source_type='manual', source_id=自增ID)
|
|
|
+ // 4. 写入 five_dimension_self_checks (scores_json 存 0-100 映射,totalScore=5维总和,questionIdsJson=[])
|
|
|
+ // 5. 返回 VO
|
|
|
+ }
|
|
|
+
|
|
|
+ public ManualScoreVO getLatest(Long userId, Long memberId) {
|
|
|
+ // 查 five_dimension_scores 最近一条 source_type='manual'
|
|
|
+ }
|
|
|
+
|
|
|
+ public List<ManualScoreVO> getHistory(Long userId, Long memberId, int limit) {
|
|
|
+ // 查历史
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 编译验证**
|
|
|
+
|
|
|
+#### Task 1.3: ManualDimensionScoreController
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `controller/family/ManualDimensionScoreController.java`
|
|
|
+
|
|
|
+```markdown
|
|
|
+- [ ] **Step 1: Controller 实现**
|
|
|
+```java
|
|
|
+@Tag(name = "五维手动填分", description = "直接填写 0-100 分,跳过问卷")
|
|
|
+@RestController
|
|
|
+@RequestMapping("/api/family/manual-score")
|
|
|
+public class ManualDimensionScoreController {
|
|
|
+ @Resource private ManualDimensionScoreService manualScoreService;
|
|
|
+
|
|
|
+ @PostMapping("/submit")
|
|
|
+ public Result<ManualScoreVO> submit(@RequestBody ManualScoreSubmitDTO dto, @RequestAttribute("userId") Long userId) {
|
|
|
+ // 校验 userId, 调用 service, 返回 Result
|
|
|
+ }
|
|
|
+
|
|
|
+ @PostMapping("/latest")
|
|
|
+ public Result<ManualScoreVO> latest(@RequestBody(required=false) Map<String,Object> body, @RequestAttribute("userId") Long userId) {
|
|
|
+ Long memberId = ParamUtils.getLong(body?.get("memberId"));
|
|
|
+ // 调用 service
|
|
|
+ }
|
|
|
+
|
|
|
+ @PostMapping("/history")
|
|
|
+ public Result<List<ManualScoreVO>> history(@RequestBody(required=false) Map<String,Object> body, @RequestAttribute("userId") Long userId) {
|
|
|
+ Long memberId = ParamUtils.getLong(body?.get("memberId"));
|
|
|
+ Integer limit = body?.get("limit") != null ? Integer.valueOf(body.get("limit").toString()) : 20;
|
|
|
+ // 调用 service
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 编译验证**
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Phase 2: 后端 MBTI 模块
|
|
|
+
|
|
|
+#### Task 2.1: MBTI 实体、Mapper
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `entity/MbtiQuestion.java`
|
|
|
+- Create: `entity/MbtiResult.java`
|
|
|
+- Create: `entity/MbtiProfile.java`
|
|
|
+- Create: `mapper/MbtiQuestionMapper.java`
|
|
|
+- Create: `mapper/MbtiResultMapper.java`
|
|
|
+- Create: `mapper/MbtiProfileMapper.java`
|
|
|
+
|
|
|
+```markdown
|
|
|
+- [ ] **Step 1: MbtiQuestion 实体**
|
|
|
+```java
|
|
|
+@Data @TableName("mbti_questions")
|
|
|
+public class MbtiQuestion implements Serializable {
|
|
|
+ @TableId(type=IdType.AUTO) private Long id;
|
|
|
+ private String dimension; // E/I/S/N/T/F/J/P
|
|
|
+ private String questionText;
|
|
|
+ private String optionA;
|
|
|
+ private String optionB;
|
|
|
+ private BigDecimal weight;
|
|
|
+ private Integer sortOrder;
|
|
|
+ private Integer isActive;
|
|
|
+ private Date createdAt;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: MbtiResult 实体**
|
|
|
+```java
|
|
|
+@Data @TableName("mbti_results")
|
|
|
+public class MbtiResult implements Serializable {
|
|
|
+ @TableId(type=IdType.AUTO) private Long id;
|
|
|
+ private Long userId;
|
|
|
+ private Long familyMemberId;
|
|
|
+ private String answersJson;
|
|
|
+ private String typeCode; // INTJ 等
|
|
|
+ private Integer eiScore;
|
|
|
+ private Integer snScore;
|
|
|
+ private Integer tfScore;
|
|
|
+ private Integer jpScore;
|
|
|
+ private Date createdAt;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: MbtiProfile 实体**
|
|
|
+```java
|
|
|
+@Data @TableName("mbti_profiles")
|
|
|
+public class MbtiProfile implements Serializable {
|
|
|
+ @TableId(type=IdType.AUTO) private Long id;
|
|
|
+ private String typeCode; // INTJ 等,唯一
|
|
|
+ private String name; // 架构师
|
|
|
+ private String description;
|
|
|
+ private String strengths;
|
|
|
+ private String weaknesses;
|
|
|
+ private String careerTendency;
|
|
|
+ private String familyStyle;
|
|
|
+ private String pairingNotes;
|
|
|
+ private String energySuggestion; // JSON 字符串
|
|
|
+ private Integer isActive;
|
|
|
+ private Date createdAt;
|
|
|
+ private Date updatedAt;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: 3 个 Mapper 接口** (extends BaseMapper)
|
|
|
+
|
|
|
+- [ ] **Step 5: 编译验证**
|
|
|
+
|
|
|
+#### Task 2.2: MBTI DTO/VO
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `dto/MbtiQuestionVO.java`
|
|
|
+- Create: `dto/MbtiResultVO.java`
|
|
|
+- Create: `dto/MbtiProfileVO.java`
|
|
|
+- Create: `dto/SubmitMbtiDTO.java`
|
|
|
+
|
|
|
+```markdown
|
|
|
+- [ ] **Step 1: MbtiQuestionVO** (题目含选项 A/B)
|
|
|
+```java
|
|
|
+@Data
|
|
|
+public class MbtiQuestionVO {
|
|
|
+ private Long id;
|
|
|
+ private String dimension; // E/I/S/N/T/F/J/P
|
|
|
+ private String questionText;
|
|
|
+ private OptionVO optionA;
|
|
|
+ private OptionVO optionB;
|
|
|
+ @Data static class OptionVO { String value; String label; }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: SubmitMbtiDTO**
|
|
|
+```java
|
|
|
+@Data
|
|
|
+public class SubmitMbtiDTO {
|
|
|
+ private Long familyMemberId;
|
|
|
+ private List<AnswerItem> answers;
|
|
|
+ @Data static class AnswerItem { Long questionId; String answer; } // answer: "A" 或 "B"
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: MbtiResultVO** (结果含 4 维度分 + 类型代码)
|
|
|
+```java
|
|
|
+@Data
|
|
|
+public class MbtiResultVO {
|
|
|
+ private Long id;
|
|
|
+ private String typeCode;
|
|
|
+ private String typeName; // 中文名
|
|
|
+ private Integer eiScore;
|
|
|
+ private Integer snScore;
|
|
|
+ private Integer tfScore;
|
|
|
+ private Integer jpScore;
|
|
|
+ private String eiType; // "E" 或 "I"
|
|
|
+ private String snType; // "S" 或 "N"
|
|
|
+ private String tfType; // "T" 或 "F"
|
|
|
+ private String jpType; // "J" 或 "P"
|
|
|
+ private MbtiProfileVO profile; // 画像详情
|
|
|
+ private Date createdAt;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: MbtiProfileVO** (画像详情)
|
|
|
+```java
|
|
|
+@Data
|
|
|
+public class MbtiProfileVO {
|
|
|
+ private String typeCode;
|
|
|
+ private String name;
|
|
|
+ private String description;
|
|
|
+ private String strengths;
|
|
|
+ private String weaknesses;
|
|
|
+ private String careerTendency;
|
|
|
+ private String familyStyle;
|
|
|
+ private String pairingNotes;
|
|
|
+ private Map<String, String> energySuggestion; // 5维度建议
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 5: 编译验证**
|
|
|
+
|
|
|
+#### Task 2.3: MbtiService 接口与实现
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `service/MbtiService.java`
|
|
|
+- Create: `service/impl/MbtiServiceImpl.java`
|
|
|
+
|
|
|
+```markdown
|
|
|
+- [ ] **Step 1: 接口**
|
|
|
+```java
|
|
|
+public interface MbtiService {
|
|
|
+ List<MbtiQuestionVO> getQuestions();
|
|
|
+ MbtiResultVO submit(SubmitMbtiDTO dto, Long userId);
|
|
|
+ MbtiResultVO getLatest(Long userId, Long memberId);
|
|
|
+ MbtiProfileVO getProfile(String typeCode);
|
|
|
+ List<MbtiResultVO> getHistory(Long userId, Long memberId, int limit);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 实现核心计算逻辑**
|
|
|
+```java
|
|
|
+@Service
|
|
|
+public class MbtiServiceImpl implements MbtiService {
|
|
|
+ @Resource private MbtiQuestionMapper questionMapper;
|
|
|
+ @Resource private MbtiResultMapper resultMapper;
|
|
|
+ @Resource private MbtiProfileMapper profileMapper;
|
|
|
+ @Resource private FamilyMemberMapper familyMemberMapper;
|
|
|
+ private final ObjectMapper objectMapper = new ObjectMapper();
|
|
|
+
|
|
|
+ public List<MbtiQuestionVO> getQuestions() {
|
|
|
+ // 查 is_active=1,按 dimension+sort_order 排序,转 VO
|
|
|
+ }
|
|
|
+
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public MbtiResultVO submit(SubmitMbtiDTO dto, Long userId) {
|
|
|
+ // 1. 校验 memberId
|
|
|
+ // 2. 加载题库 Map<id, MbtiQuestion>
|
|
|
+ // 3. 遍历 answers,累加 4 维度分:
|
|
|
+ // dimension='E' -> eiScore += (answer=="A"? weight : -weight)
|
|
|
+ // dimension='I' -> eiScore -= (answer=="A"? weight : -weight) // 反向
|
|
|
+ // S/N、T/F、J/P 同理
|
|
|
+ // 4. 判定 4 字母:eiScore>0?"E":"I" 等
|
|
|
+ // 5. typeCode = ei+sn+tf+jp
|
|
|
+ // 6. 查 profileMapper.selectByTypeCode(typeCode)
|
|
|
+ // 7. 写入 mbti_results
|
|
|
+ // 8. 组装 VO 返回
|
|
|
+ }
|
|
|
+
|
|
|
+ // getLatest, getProfile, getHistory 均为简单查询
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 编译验证**
|
|
|
+
|
|
|
+#### Task 2.4: MbtiController
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `controller/family/MbtiController.java`
|
|
|
+
|
|
|
+```markdown
|
|
|
+- [ ] **Step 1: Controller 实现**
|
|
|
+```java
|
|
|
+@Tag(name = "MBTI 人格测评", description = "标准版 ~30题 + 16型画像 + 五维联动")
|
|
|
+@RestController
|
|
|
+@RequestMapping("/api/family/mbti")
|
|
|
+public class MbtiController {
|
|
|
+ @Resource private MbtiService mbtiService;
|
|
|
+
|
|
|
+ @PostMapping("/questions")
|
|
|
+ public Result<List<MbtiQuestionVO>> questions(@RequestAttribute("userId") Long userId) {
|
|
|
+ return Result.success(mbtiService.getQuestions());
|
|
|
+ }
|
|
|
+
|
|
|
+ @PostMapping("/submit")
|
|
|
+ public Result<MbtiResultVO> submit(@RequestBody SubmitMbtiDTO dto, @RequestAttribute("userId") Long userId) {
|
|
|
+ return Result.success(mbtiService.submit(dto, userId));
|
|
|
+ }
|
|
|
+
|
|
|
+ @PostMapping("/latest")
|
|
|
+ public Result<MbtiResultVO> latest(@RequestBody(required=false) Map<String,Object> body, @RequestAttribute("userId") Long userId) {
|
|
|
+ Long memberId = ParamUtils.getLong(body?.get("memberId"));
|
|
|
+ return Result.success(mbtiService.getLatest(userId, memberId));
|
|
|
+ }
|
|
|
+
|
|
|
+ @PostMapping("/profile/{typeCode}")
|
|
|
+ public Result<MbtiProfileVO> profile(@PathVariable String typeCode, @RequestAttribute("userId") Long userId) {
|
|
|
+ return Result.success(mbtiService.getProfile(typeCode.toUpperCase()));
|
|
|
+ }
|
|
|
+
|
|
|
+ @PostMapping("/history")
|
|
|
+ public Result<List<MbtiResultVO>> history(@RequestBody(required=false) Map<String,Object> body, @RequestAttribute("userId") Long userId) {
|
|
|
+ Long memberId = ParamUtils.getLong(body?.get("memberId"));
|
|
|
+ Integer limit = body?.get("limit") != null ? Integer.valueOf(body.get("limit").toString()) : 20;
|
|
|
+ return Result.success(mbtiService.getHistory(userId, memberId, limit));
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 编译验证**
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Phase 3: 前端手动填分页面
|
|
|
+
|
|
|
+#### Task 3.1: manual-score-entry.vue
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `cfc-frontend/pages/family/manual-score-entry.vue`
|
|
|
+
|
|
|
+```markdown
|
|
|
+- [ ] **Step 1: 页面结构**
|
|
|
+- 顶部:成员选择器(横向滑动卡片,显示头像+昵称+角色标签,复用 getFamilyMemberList)
|
|
|
+- 中部:5 个维度滑块(Slider 组件或自定义拖拽条),每维度 0-100,实时显示数值
|
|
|
+- 底部:提交按钮(主色调渐变),提交后跳转 self-check-result.vue (复用结果页,传参 source=manual)
|
|
|
+
|
|
|
+- [ ] **Step 2: 逻辑**
|
|
|
+- onLoad 调用 getFamilyMemberList({visibleOnly:true}) 获取成员列表
|
|
|
+- 默认选中当前用户自己
|
|
|
+- scores 对象响应式绑定 5 维度
|
|
|
+- 提交调用 submitManualScore({memberId, scores}) → 跳转结果页
|
|
|
+
|
|
|
+- [ ] **Step 3: 样式遵循现有设计 Token (--color-primary, --bg, --shadow-clay 等)**
|
|
|
+
|
|
|
+#### Task 3.2: 修改 self-check-entry.vue 入口
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `cfc-frontend/pages/family/self-check-entry.vue`
|
|
|
+
|
|
|
+```markdown
|
|
|
+- [ ] **Step 1: 底部按钮区域增加两个按钮**
|
|
|
+```html
|
|
|
+<!-- 原有按钮保留 -->
|
|
|
+<button v-if="canCheck && lastResult" class="sce-btn sce-btn-primary" @click="retake">再次自检</button>
|
|
|
+<button v-if="canCheck && lastResult" class="sce-btn sce-btn-outline" @click="ignore">忽略本次</button>
|
|
|
+<button v-if="!lastResult" class="sce-btn sce-btn-primary" @click="retake">开始自检</button>
|
|
|
+
|
|
|
+<!-- 新增:直接填分 (橙色主按钮) -->
|
|
|
+<button class="sce-btn sce-btn-primary" @click="goManualScore">直接填分</button>
|
|
|
+
|
|
|
+<!-- 新增:MBTI 测试 (轮廓按钮) -->
|
|
|
+<button class="sce-btn sce-btn-outline" @click="goMbti">MBTI 人格测试</button>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: methods 新增跳转**
|
|
|
+```javascript
|
|
|
+goManualScore() { uni.navigateTo({ url: '/pages/family/manual-score-entry' }) },
|
|
|
+goMbti() { uni.navigateTo({ url: '/pages/family/mbti-entry' }) }
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Phase 4: 前端 MBTI 页面 (4 个页面)
|
|
|
+
|
|
|
+#### Task 4.1: mbti-entry.vue (入口选成员)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `cfc-frontend/pages/family/mbti-entry.vue`
|
|
|
+
|
|
|
+```markdown
|
|
|
+- [ ] 成员横向选择卡片 (复用 manual-score-entry 逻辑)
|
|
|
+- 大字标题:"MBTI 人格测评" + 副标题"发现你的天赋型,获得专属育儿建议"
|
|
|
+- "开始测评" 按钮 → navigateTo mbti-test.vue?memberId=xxx
|
|
|
+```
|
|
|
+
|
|
|
+#### Task 4.2: mbti-test.vue (答题页)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `cfc-frontend/pages/family/mbti-test.vue`
|
|
|
+
|
|
|
+```markdown
|
|
|
+- [ ] 单题卡片模式 (类似 self-check.vue)
|
|
|
+- 进度条:当前题号 / 总题数
|
|
|
+- 题目文本 + 两个选项按钮 (A/B),左右分布
|
|
|
+- 选中高亮,可点击切换
|
|
|
+- 底部:上一题 / 下一题 (最后一题显示"提交")
|
|
|
+- 答案暂存本地数组 answers[]
|
|
|
+- 提交调用 submitMbti({familyMemberId, answers}) → navigateTo mbti-result.vue?id=xxx
|
|
|
+```
|
|
|
+
|
|
|
+#### Task 4.3: mbti-result.vue (结果页)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `cfc-frontend/pages/family/mbti-result.vue`
|
|
|
+
|
|
|
+```markdown
|
|
|
+- [ ] 大字展示类型代码 (如 INTJ) + 中文名 (架构师) + 动画入场
|
|
|
+- 4 维度雷达图 (Canvas 2D,复用 self-check-result 雷达逻辑,4 轴 E/I S/N T/F J/P)
|
|
|
+- 画像卡片:核心特质 / 优势 / 挑战 / 职业倾向 / 家庭风格 / 搭配建议
|
|
|
+- 五维能量联动建议卡片 (从 profile.energySuggestion 读取,展示 5 个维度建议)
|
|
|
+- 底部按钮:查看百科、分享、重新测评
|
|
|
+```
|
|
|
+
|
|
|
+#### Task 4.4: mbti-profile.vue (16 型百科)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `cfc-frontend/pages/family/mbti-profile.vue`
|
|
|
+
|
|
|
+```markdown
|
|
|
+- [ ] 4x4 网格卡片 (16 种类型),每卡片显示类型代码 + 中文名 + 1 行标签
|
|
|
+- 点击卡片 → 调用 getMbtiProfile(typeCode) → 弹窗或新页展示完整画像
|
|
|
+- 顶部搜索/筛选:按 E/I、S/N、T/F、J/P 筛选
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Phase 5: 管理端 MBTI 配置
|
|
|
+
|
|
|
+#### Task 5.1: 管理端 API 与页面
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `cfc-web/src/api/mbti.js`
|
|
|
+- Create: `cfc-web/src/views/admin/MbtiProfiles.vue`
|
|
|
+- Modify: `cfc-web/src/router/index.js`
|
|
|
+
|
|
|
+```markdown
|
|
|
+- [ ] **Step 1: API 封装** (mbti.js)
|
|
|
+```js
|
|
|
+export function getMbtiProfileList() { return request('/api/admin/mbti/profile/list', 'POST') }
|
|
|
+export function saveMbtiProfile(data) { return request('/api/admin/mbti/profile/save', 'POST', data) }
|
|
|
+export function deleteMbtiProfile(id) { return request('/api/admin/mbti/profile/delete', 'POST', {id}) }
|
|
|
+export function initMbtiSeedData() { return request('/api/admin/mbti/seed/init', 'POST') }
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: MbtiProfiles.vue** (Element UI Table + Dialog 编辑)
|
|
|
+- 列表展示 16 行:type_code, name, is_active, 操作(编辑/删除)
|
|
|
+- 编辑弹窗:所有字段可编辑,energy_suggestion 用 JSON 编辑器 (textarea + 格式化)
|
|
|
+- "初始化种子数据"按钮调用 initMbtiSeedData()
|
|
|
+
|
|
|
+- [ ] **Step 3: 路由注册** (admin 子路由)
|
|
|
+```
|
|
|
+
|
|
|
+#### Task 5.2: 管理端 Controller (可选,若需后端增删改)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `controller/admin/MbtiProfileController.java` (若管理端走后端 CRUD)
|
|
|
+- 或直接在 MbtiService 暴露 admin 方法,MbtiController 复用
|
|
|
+
|
|
|
+```markdown
|
|
|
+- [ ] 视情况新增或复用,编译验证
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Phase 6: API 文档同步与验收
|
|
|
+
|
|
|
+#### Task 6.1: 更新 API_REFERENCE.md
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `docs/superpowers/api/API_REFERENCE.md`
|
|
|
+
|
|
|
+```markdown
|
|
|
+- [ ] 在 Controller 总览表新增:
|
|
|
+| ManualDimensionScoreController | /api/family/manual-score | 五维手动填分 | — |
|
|
|
+| MbtiController | /api/family/mbti | MBTI 人格测评 | — |
|
|
|
+| MbtiProfileController | /api/admin/mbti/profile | MBTI 画像管理(管理端) | — |
|
|
|
+
|
|
|
+- [ ] 详细记录每个接口的请求/响应示例
|
|
|
+```
|
|
|
+
|
|
|
+#### Task 6.2: 全量编译 + 功能自测
|
|
|
+
|
|
|
+```markdown
|
|
|
+- [ ] cd cfc-backend && mvn clean compile
|
|
|
+- [ ] 启动后端,用 Postman/curl 测试 8 个新接口
|
|
|
+- [ ] 微信开发者工具预览 5 个新页面流程
|
|
|
+- [ ] 管理端 npm run serve 验证 MBTI 配置页
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 验收标准 (Definition of Done)
|
|
|
+
|
|
|
+1. **手动填分**:家长可在 self-check-entry 点"直接填分" → 选成员 → 5 个滑块 0-100 → 提交 → 结果页显示雷达图/总分,能量沙盘实时更新
|
|
|
+2. **MBTI 测评**:入口 → 选成员 → 30 题答完 → 显示类型代码+画像+4维雷达+五维建议 → 可查百科
|
|
|
+3. **管理端**:16 型画像可增删改,种子数据一键初始化
|
|
|
+4. **数据库**:3 表结构正确,迁移幂等,schema.sql 同步
|
|
|
+5. **编译**:`mvn clean compile` 通过,无 Lint 报错
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 执行建议
|
|
|
+
|
|
|
+**推荐 Subagent-Driven 并行执行** (7 个独立子任务可并行):
|
|
|
+
|
|
|
+| 并行组 | 任务 | 预计耗时 |
|
|
|
+|---------|------|----------|
|
|
|
+| A | Task 0.1+0.2 (DB迁移+种子) | 30 min |
|
|
|
+| B | Task 1.1-1.3 (手动填分后端) | 45 min |
|
|
|
+| C | Task 2.1-2.4 (MBTI 后端) | 60 min |
|
|
|
+| D | Task 3.1-3.2 (前端手动填分) | 45 min |
|
|
|
+| E | Task 4.1-4.4 (前端 MBTI 4页) | 90 min |
|
|
|
+| F | Task 5.1-5.2 (管理端) | 45 min |
|
|
|
+| G | Task 6.1-6.2 (文档+验收) | 30 min |
|
|
|
+
|
|
|
+**两阶段 Review:** 每组完成后暂停,人工确认无冲突再合并。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+**Plan complete and saved to `docs/superpowers/plans/2026-09-10-manual-dimension-mbti-plan.md`. Two execution options:**
|
|
|
+
|
|
|
+**1. Subagent-Driven (recommended)** - I dispatch a fresh subagent per task group, review between groups, fast iteration
|
|
|
+
|
|
|
+**2. Inline Execution** - Execute tasks in this session using executing-plans, batch execution with checkpoints
|
|
|
+
|
|
|
+**Which approach?**
|