Просмотр исходного кода

chore: auto bump version and changelog [skip ci]

iwt 3 месяцев назад
Родитель
Сommit
3443be507e

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-9e555cb30d49ceb84614a768234f785ff7cf2d49
+c98820ad7f6326f95c590019f429609a34beeadd

+ 528 - 0
docs/superpowers/plans/2026-07-23-P2-2-family-health-interaction.md

@@ -0,0 +1,528 @@
+# P2-2 家庭健康互动 Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task.
+
+**Goal:** 实现家庭健康排行榜、点赞系统、家庭挑战三大功能
+
+**Architecture:** 后端3个Controller + 3个Service + 3个Entity/Mapper / 前端3个组件 + 集成到parent首页
+
+**Tech Stack:** Spring Boot 2.7.18 + MyBatis-Plus / uni-app Vue 2 小程序
+
+---
+
+### Task 1: 后端Entity + Mapper (3张新表)
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyChallenge.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyChallengeProgress.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/entity/HealthInteraction.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/mapper/FamilyChallengeMapper.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/mapper/FamilyChallengeProgressMapper.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/mapper/HealthInteractionMapper.java`
+- Modify: `cfc-backend/src/main/resources/schema.sql` (追加3张表)
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java` (迁移107/108/109)
+
+- [ ] **Step 1: Create FamilyChallenge.java**
+
+```java
+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.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("family_challenge")
+public class FamilyChallenge implements Serializable {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private Long familyId;
+    private String challengeType;         // full_checkin, sports, health_week
+    private String title;
+    private String description;
+    private Integer durationDays;
+    private Integer rewardPoints;
+    private String rewardBadge;
+    private Date startDate;
+    private Date endDate;
+    private String status;                // active, completed, cancelled
+    private Date createdAt;
+}
+```
+
+- [ ] **Step 2: Create FamilyChallengeProgress.java**
+
+```java
+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.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("family_challenge_progress")
+public class FamilyChallengeProgress implements Serializable {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private Long challengeId;
+    private Long childId;
+    private Integer progressValue;        // e.g. checkin days count, sports minutes
+    private Boolean completed;
+    private Date createdAt;
+    private Date updatedAt;
+}
+```
+
+- [ ] **Step 3: Create HealthInteraction.java**
+
+```java
+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.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("health_interaction")
+public class HealthInteraction implements Serializable {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private Long familyId;
+    private Long fromUserId;
+    private Long toUserId;
+    private String actionType;            // health_checkin, micro_action, etc.
+    private Long actionId;                // reference to the completed action record
+    private Date createdAt;
+}
+```
+
+- [ ] **Step 4: Create 3 Mapper interfaces**
+
+```java
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.FamilyChallenge;
+
+public interface FamilyChallengeMapper extends BaseMapper<FamilyChallenge> {}
+```
+
+(同样模式创建 FamilyChallengeProgressMapper 和 HealthInteractionMapper)
+
+- [ ] **Step 5: Add migrations 107/108/109 to DatabaseInitializer**
+
+```java
+// 迁移107: 创建 family_challenge 表(P2-2家庭挑战)
+try {
+    jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS family_challenge (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "family_id BIGINT NOT NULL, " +
+            "challenge_type VARCHAR(50) NOT NULL COMMENT 'full_checkin/sports/health_week', " +
+            "title VARCHAR(200) NOT NULL, " +
+            "description TEXT, " +
+            "duration_days INT DEFAULT 7, " +
+            "reward_points INT DEFAULT 0, " +
+            "reward_badge VARCHAR(100), " +
+            "start_date DATE, " +
+            "end_date DATE, " +
+            "status VARCHAR(20) DEFAULT 'active' COMMENT 'active/completed/cancelled', " +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+            "INDEX idx_family_id (family_id)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭挑战'");
+    log.info("已创建family_challenge表");
+} catch (Exception e) { /* 忽略 */ }
+
+// 迁移108: 创建 family_challenge_progress 表(P2-2挑战进度)
+try {
+    jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS family_challenge_progress (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "challenge_id BIGINT NOT NULL, " +
+            "child_id BIGINT NOT NULL, " +
+            "progress_value INT DEFAULT 0, " +
+            "completed TINYINT(1) DEFAULT 0, " +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+            "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+            "INDEX idx_challenge_id (challenge_id)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭挑战进度'");
+    log.info("已创建family_challenge_progress表");
+} catch (Exception e) { /* 忽略 */ }
+
+// 迁移109: 创建 health_interaction 表(P2-2点赞记录)
+try {
+    jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS health_interaction (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "family_id BIGINT NOT NULL, " +
+            "from_user_id BIGINT NOT NULL, " +
+            "to_user_id BIGINT NOT NULL, " +
+            "action_type VARCHAR(50) NOT NULL, " +
+            "action_id BIGINT, " +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+            "INDEX idx_family_action (family_id, action_type, action_id)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康互动记录(点赞)'");
+    log.info("已创建health_interaction表");
+} catch (Exception e) { /* 忽略 */ }
+```
+
+- [ ] **Step 6: Sync schema.sql — append 3 CREATE TABLE statements**
+
+在 schema.sql 末尾追加上述3张表的 CREATE TABLE IF NOT EXISTS。
+
+- [ ] **Step 7: mvn clean compile** — 验证编译通过
+
+---
+
+### Task 2: HealthInteractionService + Controller (点赞系统)
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/service/HealthInteractionService.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/controller/HealthInteractionController.java`
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/HealthScoreService.java` (加 public `addScore(childId, points, reason)` 方法)
+
+- [ ] **Step 1: Add `addScore` method to HealthScoreService**
+
+```java
+/** 给指定孩子增加健康积分(供点赞系统调用) */
+public void addScore(Long childId, int points, String reason) {
+    String dateKey = LocalDate.now().toString();
+    // 在 existing score map 中增加
+    if (reason != null) {
+        // 记录积分来源
+    }
+}
+```
+
+- [ ] **Step 2: Create HealthInteractionService**
+
+```java
+@Service
+public class HealthInteractionService {
+    @Resource
+    private HealthInteractionMapper healthInteractionMapper;
+    @Resource
+    private HealthScoreService healthScoreService;
+    @Resource
+    private FamilyMembersMapper familyMembersMapper;
+
+    /** 点赞: 双方各+1分, 24h内同一行为不可重复点赞 */
+    @Transactional
+    public Result<Void> like(Long fromUserId, Long toUserId, String actionType, Long actionId, Long familyId) {
+        // 1. 查24h内是否已点赞
+        // 2. 如果已存在 → return Result.error("已点赞")
+        // 3. 插入 HealthInteraction 记录
+        // 4. 调用 healthScoreService.addScore(toUserId, 1, "收到点赞")
+        // 5. 调用 healthScoreService.addScore(fromUserId, 1, "点赞他人")
+        // 6. return Result.success()
+    }
+
+    /** 检查24h内是否已点赞 */
+    public boolean hasLiked24h(Long fromUserId, String actionType, Long actionId) {
+        // LocalDateTime.now().minusHours(24) 过滤
+    }
+}
+```
+
+- [ ] **Step 3: Create HealthInteractionController**
+
+```java
+@RestController
+@RequestMapping("/api/health/interact")
+public class HealthInteractionController {
+    @Resource
+    private HealthInteractionService healthInteractionService;
+
+    @PostMapping("/like")
+    public Result<Void> like(@RequestBody Map<String, Object> params,
+                             @RequestAttribute("userId") Long userId,
+                             @RequestAttribute("role") String role) {
+        Long toUserId = Long.valueOf(params.get("toUserId").toString());
+        String actionType = (String) params.get("actionType");
+        Long actionId = Long.valueOf(params.get("actionId").toString());
+        Long familyId = Long.valueOf(params.get("familyId").toString());
+        return healthInteractionService.like(userId, toUserId, actionType, actionId, familyId);
+    }
+}
+```
+
+- [ ] **Step 4: mvn clean compile**
+
+---
+
+### Task 3: FamilyHealthScoreService + FamilyHealthController (排行榜)
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/service/FamilyHealthScoreService.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/controller/FamilyHealthController.java`
+
+- [ ] **Step 1: FamilyHealthScoreService**
+
+```java
+@Service
+public class FamilyHealthScoreService {
+    @Resource
+    private FamilyMembersMapper familyMembersMapper;
+    @Resource
+    private HealthScoreService healthScoreService;
+
+    /**
+     * 获取家庭健康排行榜
+     * @return List<{childId, name, avatar, score, levelTitle, streakDays}>
+     */
+    public List<Map<String, Object>> getFamilyRanking(Long familyId, Long currentUserId, String role) {
+        // 1. 查家庭所有成员 (child)
+        // 2. 对每个child查 HealthScoreService 获取积分
+        // 3. 按积分降序排序
+        // 4. 如果是child角色, 只返回自己的排名
+        // 5. 计算全家总积分
+        // 6. 返回 (含 rank 序号)
+    }
+}
+```
+
+- [ ] **Step 2: FamilyHealthController**
+
+```java
+@RestController
+@RequestMapping("/api/health/family")
+public class FamilyHealthController {
+    @Resource
+    private FamilyHealthScoreService familyHealthScoreService;
+
+    @PostMapping("/ranking")
+    public Result<List<Map<String, Object>>> getRanking(
+            @RequestBody Map<String, Object> params,
+            @RequestAttribute("userId") Long userId,
+            @RequestAttribute("role") String role) {
+        Long familyId = Long.valueOf(params.get("familyId").toString());
+        List<Map<String, Object>> ranking = familyHealthScoreService.getFamilyRanking(familyId, userId, role);
+        return Result.success(ranking);
+    }
+}
+```
+
+- [ ] **Step 3: mvn clean compile**
+
+---
+
+### Task 4: FamilyChallengeService + FamilyChallengeController (挑战引擎)
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/service/FamilyChallengeService.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/controller/FamilyChallengeController.java`
+
+- [ ] **Step 1: FamilyChallengeService**
+
+```java
+@Service
+public class FamilyChallengeService {
+    @Resource
+    private FamilyChallengeMapper familyChallengeMapper;
+    @Resource
+    private FamilyChallengeProgressMapper familyChallengeProgressMapper;
+    @Resource
+    private HealthScoreService healthScoreService;
+
+    /** 获取活跃挑战, 若无则自动创建 */
+    public List<FamilyChallenge> getActiveChallenges(Long familyId) {
+        // 1. 查 active 状态的挑战
+        // 2. 如果为空 → autoCreateChallenges(familyId)
+        // 3. 再查一次返回
+    }
+
+    /** 按当前月份自动创建挑战 (全员打卡/运动PK/健康周) */
+    public void autoCreateChallenges(Long familyId) {
+        // 本月1日 → 下月1日
+        // 创建3种挑战模板
+    }
+
+    /** 更新挑战进度 */
+    public Result<Void> updateProgress(Long challengeId, Long childId, int delta) {
+        // 查找或创建进度记录
+        // 更新 progressValue
+        // 检查是否完成后自动结算
+    }
+
+    /** 结算挑战: 达标发奖 */
+    private void settleChallenge(Long challengeId) {
+        // 读取所有进度
+        // 判断达标条件
+        // 达标: healthScoreService.addScore + 更新status=completed
+        // 超期未达标: status=completed (不发奖)
+    }
+
+    /** 挑战历史 */
+    public List<FamilyChallenge> getChallengeHistory(Long familyId) {
+        // status=completed, 按时间倒序
+    }
+}
+```
+
+- [ ] **Step 2: FamilyChallengeController**
+
+```java
+@RestController
+@RequestMapping("/api/health/challenge")
+public class FamilyChallengeController {
+    @Resource
+    private FamilyChallengeService familyChallengeService;
+
+    @PostMapping("/list")
+    public Result<List<FamilyChallenge>> getList(@RequestBody Map<String, Object> params) {
+        Long familyId = Long.valueOf(params.get("familyId").toString());
+        return Result.success(familyChallengeService.getActiveChallenges(familyId));
+    }
+
+    @PostMapping("/history")
+    public Result<List<FamilyChallenge>> getHistory(@RequestBody Map<String, Object> params) {
+        Long familyId = Long.valueOf(params.get("familyId").toString());
+        return Result.success(familyChallengeService.getChallengeHistory(familyId));
+    }
+
+    @PostMapping("/progress")
+    public Result<Void> updateProgress(@RequestBody Map<String, Object> params) {
+        Long challengeId = Long.valueOf(params.get("challengeId").toString());
+        Long childId = Long.valueOf(params.get("childId").toString());
+        int delta = Integer.parseInt(params.get("delta").toString());
+        return familyChallengeService.updateProgress(challengeId, childId, delta);
+    }
+}
+```
+
+- [ ] **Step 3: mvn clean compile**
+
+---
+
+### Task 5: 前端组件 - FamilyHealthRanking.vue + HealthLikeButton.vue
+
+**Files:**
+- Create: `cfc-frontend/components/FamilyHealthRanking.vue`
+- Create: `cfc-frontend/components/HealthLikeButton.vue`
+
+- [ ] **Step 1: FamilyHealthRanking.vue**
+
+```vue
+<template>
+  <view class="family-ranking" v-if="members.length">
+    <view class="ranking-header">
+      <text class="ranking-title">👨‍👩‍👧‍👦 家庭健康榜</text>
+      <text class="ranking-total">全家积分: {{ totalScore }}</text>
+    </view>
+    <view class="ranking-list">
+      <view
+        class="ranking-item"
+        v-for="(m, idx) in members"
+        :key="m.childId"
+      >
+        <text class="rank-num">{{ '🥇🥈🥉'[idx] || '#' + (idx+1) }}</text>
+        <image class="rank-avatar" :src="m.avatar || '/static/default-avatar.png'" />
+        <view class="rank-info">
+          <text class="rank-name">{{ m.name }}</text>
+          <text class="rank-level">{{ m.levelTitle }}</text>
+        </view>
+        <view class="rank-score">
+          <text class="score-val">{{ m.score }}</text>
+          <text class="score-unit">分</text>
+        </view>
+        <text class="rank-streak" v-if="m.streakDays > 0">🔥 {{ m.streakDays }}天</text>
+      </view>
+    </view>
+  </view>
+</template>
+```
+
+- [ ] **Step 2: HealthLikeButton.vue**
+
+```vue
+<template>
+  <view class="like-btn" :class="{ liked: isLiked }" @click="handleLike">
+    <text class="like-icon">{{ isLiked ? '❤️' : '🤍' }}</text>
+    <text class="like-count">{{ count }}</text>
+  </view>
+</template>
+<script>
+export default {
+  props: { actionType: String, actionId: [Number, String], toUserId: [Number, String] },
+  data() { return { isLiked: false, count: 0 } },
+  methods: {
+    handleLike() {
+      if (this.isLiked) return
+      // POST /api/health/interact/like
+    }
+  }
+}
+</script>
+```
+
+---
+
+### Task 6: 前端组件 FamilyChallengeCard.vue + 集成到 parent/index.vue
+
+**Files:**
+- Create: `cfc-frontend/components/FamilyChallengeCard.vue`
+- Modify: `cfc-frontend/pages/index/index.vue` (parent tab 嵌入排行榜+挑战)
+
+- [ ] **Step 1: FamilyChallengeCard.vue**
+
+```vue
+<template>
+  <view class="challenge-card" v-if="challenge">
+    <view class="card-header">
+      <text class="card-title">{{ challenge.title }}</text>
+      <text class="card-status">{{ challenge.status === 'active' ? '进行中' : '已完成' }}</text>
+    </view>
+    <text class="card-desc">{{ challenge.description }}</text>
+    <view class="card-footer">
+      <text class="card-deadline">截止: {{ formatDate(challenge.endDate) }}</text>
+      <text class="card-reward">🏆 +{{ challenge.rewardPoints }}分</text>
+    </view>
+    <!-- 进度条 -->
+    <view class="progress-bar" v-if="challenge.progress !== undefined">
+      <view class="progress-fill" :style="{ width: progressPct + '%' }"></view>
+    </view>
+  </view>
+</template>
+```
+
+- [ ] **Step 2: 在 parent/index.vue 中嵌入排行榜+挑战卡片**
+
+在家长首页适当位置添加 `<FamilyHealthRanking />` 和 `<FamilyChallengeCard />`。
+
+- [ ] **Step 3: 添加 api.js 端点**
+
+```js
+export const getFamilyRanking = (data) => request('/api/health/family/ranking', 'POST', data)
+export const getChallengeList = (data) => request('/api/health/challenge/list', 'POST', data)
+export const getChallengeHistory = (data) => request('/api/health/challenge/history', 'POST', data)
+export const updateChallengeProgress = (data) => request('/api/health/challenge/progress', 'POST', data)
+export const sendLike = (data) => request('/api/health/interact/like', 'POST', data)
+```
+
+---
+
+### Task 7: 路由检查 + 编译验证 + 提交
+
+- [ ] **Step 1: 检查重复路由**
+
+```bash
+grep -rn '@Mapping' cfc-backend/src/.../controller/ | grep -oP '@\w+Mapping\("\K[^"]*' | sort -u
+```
+
+- [ ] **Step 2: mvn clean compile** — 全量编译验证
+
+- [ ] **Step 3: git add + commit**
+
+```bash
+git add ... # all P2-2 files
+git commit -m "feat: P2-2 家庭健康互动 - 排行榜+点赞+挑战系统"
+git push
+```
+
+- [ ] **Step 4: 更新 PROJECT-OVERVIEW.md** — P2-2 标记 ⚡ 开发中