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 小程序
Files:
cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyChallenge.javacfc-backend/src/main/java/com/etotem/cfc/entity/FamilyChallengeProgress.javacfc-backend/src/main/java/com/etotem/cfc/entity/HealthInteraction.javacfc-backend/src/main/java/com/etotem/cfc/mapper/FamilyChallengeMapper.javacfc-backend/src/main/java/com/etotem/cfc/mapper/FamilyChallengeProgressMapper.javacfc-backend/src/main/java/com/etotem/cfc/mapper/HealthInteractionMapper.javacfc-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
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
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
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
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
// 迁移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。
Files:
cfc-backend/src/main/java/com/etotem/cfc/service/HealthInteractionService.javacfc-backend/src/main/java/com/etotem/cfc/controller/HealthInteractionController.javaModify: cfc-backend/src/main/java/com/etotem/cfc/service/HealthScoreService.java (加 public addScore(childId, points, reason) 方法)
[ ] Step 1: Add addScore method to HealthScoreService
/** 给指定孩子增加健康积分(供点赞系统调用) */
public void addScore(Long childId, int points, String reason) {
String dateKey = LocalDate.now().toString();
// 在 existing score map 中增加
if (reason != null) {
// 记录积分来源
}
}
[ ] Step 2: Create HealthInteractionService
@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
@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
Files:
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyHealthScoreService.javaCreate: cfc-backend/src/main/java/com/etotem/cfc/controller/FamilyHealthController.java
[ ] Step 1: FamilyHealthScoreService
@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
@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
Files:
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyChallengeService.javaCreate: cfc-backend/src/main/java/com/etotem/cfc/controller/FamilyChallengeController.java
[ ] Step 1: FamilyChallengeService
@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
@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
Files:
cfc-frontend/components/FamilyHealthRanking.vueCreate: cfc-frontend/components/HealthLikeButton.vue
[ ] Step 1: FamilyHealthRanking.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
<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>
Files:
cfc-frontend/components/FamilyChallengeCard.vueModify: cfc-frontend/pages/index/index.vue (parent tab 嵌入排行榜+挑战)
[ ] Step 1: FamilyChallengeCard.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 端点
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)
[ ] Step 1: 检查重复路由
grep -rn '@Mapping' cfc-backend/src/.../controller/ | grep -oP '@\w+Mapping\("\K[^"]*' | sort -u
[ ] Step 2: mvn clean compile — 全量编译验证
[ ] Step 3: git add + commit
git add ... # all P2-2 files
git commit -m "feat: P2-2 家庭健康互动 - 排行榜+点赞+挑战系统"
git push
[ ] Step 4: 更新 PROJECT-OVERVIEW.md — P2-2 标记 ⚡ 开发中