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: 家长首页对接真实数据,接入系统积分体系(测评建档发放系统积分,用于复测/兑换)
Architecture: 在现有积分体系上增加 category 分类标识区分系统积分与任务积分;测评完成时自动发放系统积分;家长首页对接真实 API 数据替代模拟数据。
Tech Stack: Spring Boot 2.7.18 + MyBatis-Plus + uni-app Vue 2
children 表: + system_points INT DEFAULT 0 -- 系统积分余额
points_log 表: + category VARCHAR(20) -- 'task' / 'system' 分类标识
新增 POST /api/parent/dashboard -- 家长首页统计数据
新增 POST /api/guide/record/create -- 改造: 录入测评结果后自动发放系统积分
新增 POST /api/guide/record/complete -- 字段增: 增加completeRecord方法使之额外发放系统积分
改造 POST /api/user/children/list -- ChildInfoDTO 增加 systemPoints
改造 POST /api/user/family-members -- 修复URL (前端) 或增加后端映射
改造 POST /api/guide/record/create -- 录入结果后发放系统积分
Files:
cfc-backend/src/main/resources/schema.sqlcfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.javacfc-backend/src/main/java/com/etotem/cfc/entity/PointsLog.javaModify: cfc-backend/src/main/java/com/etotem/cfc/entity/Child.java
[ ] Step 1: schema.sql 添加字段
在 children 表添加 system_points 字段,在 points_log 表添加 category 字段:
-- schema.sql children 表: 在 total_points 后添加
system_points INT DEFAULT 0 COMMENT '系统积分(测评建档/平台发放)',
-- schema.sql points_log 表: 在 type 后添加
category VARCHAR(20) DEFAULT 'task' COMMENT '积分分类: task-任务积分, system-系统积分',
[ ] Step 2: DatabaseInitializer 添加迁移
// DatabaseInitializer.java runMigrations() 末尾添加
// 迁移20: children表添加system_points字段
try {
jdbcTemplate.execute("ALTER TABLE children ADD COLUMN system_points INT DEFAULT 0 COMMENT '系统积分'");
log.info("已添加system_points列到children表");
} catch (Exception e) {
log.warn("添加system_points列可能已存在: {}", e.getMessage());
}
// 迁移21: points_log表添加category字段
try {
jdbcTemplate.execute("ALTER TABLE points_log ADD COLUMN category VARCHAR(20) DEFAULT 'task' COMMENT '积分分类: task任务积分, system系统积分'");
log.info("已添加category列到points_log表");
} catch (Exception e) {
log.warn("添加category列可能已存在: {}", e.getMessage());
}
[ ] Step 3: Child 实体添加 systemPoints
// Child.java
/** 系统积分(测评建档/平台发放) */
private Integer systemPoints;
[ ] Step 4: PointsLog 实体添加 category
// PointsLog.java
/** 积分分类: task-任务积分, system-系统积分 */
private String category;
[ ] Step 5: 编译验证
Run: cd cfc-backend && mvn clean compile
Expected: BUILD SUCCESS
Files:
cfc-backend/src/main/java/com/etotem/cfc/service/PointsService.javacfc-backend/src/main/java/com/etotem/cfc/service/api/PointsServiceInterface.javaModify: cfc-backend/src/main/java/com/etotem/cfc/mapper/PointsLogMapper.java (一般不需要改)
[ ] Step 1: PointsService 添加 awardSystemPoints 方法
// PointsService.java
/**
* 发放系统积分
* @param childId 孩子ID
* @param amount 积分数量(正数)
* @param reason 发放原因
* @return 发放后的系统积分余额
*/
@Transactional
public int awardSystemPoints(Long childId, int amount, String reason) {
if (amount <= 0) {
throw new IllegalArgumentException("系统积分数量必须为正数");
}
Child child = childMapper.selectById(childId);
if (child == null) {
throw new RuntimeException("孩子不存在");
}
// 更新系统积分
int newSystemPoints = (child.getSystemPoints() == null ? 0 : child.getSystemPoints()) + amount;
child.setSystemPoints(newSystemPoints);
child.setUpdatedAt(new Date());
childMapper.updateById(child);
// 同时增加总积分
int newTotal = (child.getTotalPoints() == null ? 0 : child.getTotalPoints()) + amount;
child.setTotalPoints(newTotal);
child.setUpdatedAt(new Date());
childMapper.updateById(child);
// 记录流水
PointsLog log = new PointsLog();
log.setChildId(childId);
log.setAmount(amount);
log.setType("earn");
log.setCategory("system");
log.setDescription(reason);
log.setCreatedAt(new Date());
pointsLogMapper.insert(log);
return newSystemPoints;
}
/**
* 扣除系统积分
* @param childId 孩子ID
* @param amount 扣除数量(正数)
* @param reason 扣除原因
* @return 扣除后的系统积分余额,余额不足返回-1
*/
@Transactional
public int deductSystemPoints(Long childId, int amount, String reason) {
if (amount <= 0) {
throw new IllegalArgumentException("扣除数量必须为正数");
}
Child child = childMapper.selectById(childId);
if (child == null) {
throw new RuntimeException("孩子不存在");
}
int currentSystem = child.getSystemPoints() == null ? 0 : child.getSystemPoints();
if (currentSystem < amount) {
return -1; // 余额不足
}
int newSystemPoints = currentSystem - amount;
child.setSystemPoints(newSystemPoints);
child.setUpdatedAt(new Date());
childMapper.updateById(child);
// 同时扣除总积分
int newTotal = (child.getTotalPoints() == null ? 0 : child.getTotalPoints()) - amount;
child.setTotalPoints(newTotal);
child.setUpdatedAt(new Date());
childMapper.updateById(child);
// 记录流水
PointsLog log = new PointsLog();
log.setChildId(childId);
log.setAmount(-amount);
log.setType("spend");
log.setCategory("system");
log.setDescription(reason);
log.setCreatedAt(new Date());
pointsLogMapper.insert(log);
return newSystemPoints;
}
/**
* 获取孩子可用的系统积分余额
*/
public int getSystemPointsBalance(Long childId) {
Child child = childMapper.selectById(childId);
if (child == null) {
return 0;
}
return child.getSystemPoints() == null ? 0 : child.getSystemPoints();
}
/**
* 获取积分流水(支持按分类筛选)
*/
public Page<PointsLog> getPointsLogsByCategory(Long childId, String category, Integer page, Integer size) {
Page<PointsLog> pageParam = new Page<>(page, size);
LambdaQueryWrapper<PointsLog> wrapper = new LambdaQueryWrapper<PointsLog>()
.eq(PointsLog::getChildId, childId);
if (category != null && !category.isEmpty()) {
wrapper.eq(PointsLog::getCategory, category);
}
wrapper.orderByDesc(PointsLog::getCreatedAt);
return pointsLogMapper.selectPage(pageParam, wrapper);
}
[ ] Step 2: PointsServiceInterface 添加接口方法(如果存在)
// PointsServiceInterface.java
int awardSystemPoints(Long childId, int amount, String reason);
int deductSystemPoints(Long childId, int amount, String reason);
int getSystemPointsBalance(Long childId);
Page<PointsLog> getPointsLogsByCategory(Long childId, String category, Integer page, Integer size);
[ ] Step 3: 编译验证
Run: cd cfc-backend && mvn clean compile
Expected: BUILD SUCCESS
Files:
cfc-backend/src/main/java/com/etotem/cfc/controller/guide/GuideRecordController.javaModify: cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentService.java
[ ] Step 1: GuideRecordController.createRecord 中发放系统积分
// GuideRecordController.java
@Resource
private PointsService pointsService;
// 在 createRecord 方法末尾,return 之前增加系统积分发放
@Operation(summary = "录入测评结果(含孩子快照)")
@PostMapping("/create")
public Result<DanAssessmentResult> createRecord(
@RequestAttribute("userId") Long userId,
@RequestAttribute("role") String role,
@RequestBody DanAssessmentResult result) {
if (!"teacher".equals(role)) {
return Result.error("只有成长规划师可以录入测评结果");
}
result.setTeacherId(userId);
result.setStatus("completed");
result.setCreatedAt(new Date());
result.setUpdatedAt(new Date());
if (result.getAssessmentDate() == null) {
result.setAssessmentDate(new Date());
}
DanAssessmentResult saved = assessmentService.recordResult(result);
// 发放系统积分:首次建档奖励(从配置表读取积分值,默认50)
if (saved != null && saved.getChildId() != null) {
try {
AssessmentPointsConfig config = assessmentService.getPointsConfig();
int points = (config != null && config.getPointsValue() != null)
? config.getPointsValue() : 50;
pointsService.awardSystemPoints(saved.getChildId(), points,
"完成测评建档奖励");
} catch (Exception e) {
log.error("发放系统积分失败", e);
// 不阻断主流程
}
}
return Result.success(saved);
}
需要在 GuideRecordController 中添加 Logger:
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(GuideRecordController.class);
getPointsConfig() 已经是 public,可以直接从 Controller 调用,不需要修改。
Run: cd cfc-backend && mvn clean compile
Expected: BUILD SUCCESS
Files:
cfc-backend/src/main/java/com/etotem/cfc/dto/ChildInfoDTO.javaModify: cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java
[ ] Step 1: ChildInfoDTO 添加 systemPoints
// ChildInfoDTO.java
private Integer systemPoints;
[ ] Step 2: toChildInfoDTO 映射 systemPoints
// UserService.java toChildInfoDTO 方法中
dto.setSystemPoints(child.getSystemPoints());
[ ] Step 3: 编译验证
Run: cd cfc-backend && mvn clean compile
Expected: BUILD SUCCESS
Files:
cfc-backend/src/main/java/com/etotem/cfc/controller/parent/ParentDashboardController.javaModify: cfc-backend/src/main/java/com/etotem/cfc/service/TaskStatsService.java (如需要)
[ ] Step 1: 创建 ParentDashboardController
package com.etotem.cfc.controller.parent;
import com.etotem.cfc.common.Result;
import com.etotem.cfc.dto.ChildInfoDTO;
import com.etotem.cfc.service.PointsService;
import com.etotem.cfc.service.TaskService;
import com.etotem.cfc.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.*;
@Tag(name = "家长首页", description = "家长首页数据接口")
@RestController
@RequestMapping("/api/parent")
public class ParentDashboardController {
@Resource
private UserService userService;
@Resource
private TaskService taskService;
@Resource
private PointsService pointsService;
@Operation(summary = "获取家长首页仪表盘数据")
@PostMapping("/dashboard")
public Result<Map<String, Object>> getDashboard(HttpServletRequest request) {
Long userId = (Long) request.getAttribute("userId");
// 获取孩子列表(复用现有接口)
List<ChildInfoDTO> children = userService.getChildren(userId);
List<Map<String, Object>> childStatsList = new ArrayList<>();
int totalCompletionRate = 0;
int totalHabitCount = 0;
int childCount = children.size();
for (ChildInfoDTO child : children) {
Map<String, Object> stats = new HashMap<>();
stats.put("id", child.getId());
stats.put("nickname", child.getNickname());
stats.put("totalPoints", child.getTotalPoints());
stats.put("systemPoints", child.getSystemPoints());
stats.put("streakDays", child.getStreakDays());
stats.put("danLevel", child.getDanLevel());
stats.put("age", child.getAge());
// 计算任务完成率(今日/本周)
// 这里简化处理:从TaskService获取统计数据
Map<String, Object> taskStats = taskService.getChildTaskStats(child.getId());
stats.put("completionRate", taskStats.getOrDefault("completionRate", 0));
stats.put("pendingReviewCount", taskStats.getOrDefault("pendingReviewCount", 0));
// 习惯打卡数量(今日已完成的任务中标记为习惯的数量)
stats.put("habitCount", taskStats.getOrDefault("habitCount", 0));
totalCompletionRate += (Integer) taskStats.getOrDefault("completionRate", 0);
totalHabitCount += (Integer) taskStats.getOrDefault("habitCount", 0);
childStatsList.add(stats);
}
Map<String, Object> result = new HashMap<>();
result.put("children", childStatsList);
result.put("avgCompletionRate", childCount > 0 ? totalCompletionRate / childCount : 0);
result.put("totalHabitCount", totalHabitCount);
return Result.success(result);
}
}
import com.etotem.cfc.mapper.TaskMapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.etotem.cfc.entity.Task;
[ ] Step 2: TaskService 添加 getChildTaskStats 方法
// TaskService.java
@Resource
private TaskMapper taskMapper;
/**
* 获取孩子的任务统计数据
*/
public Map<String, Object> getChildTaskStats(Long childId) {
Map<String, Object> stats = new HashMap<>();
Date now = new Date();
// 今日任务统计
Calendar cal = Calendar.getInstance();
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
Date todayStart = cal.getTime();
cal.set(Calendar.HOUR_OF_DAY, 23);
cal.set(Calendar.MINUTE, 59);
cal.set(Calendar.SECOND, 59);
Date todayEnd = cal.getTime();
// 获取孩子所有任务
List<Task> allTasks = taskMapper.selectList(
new LambdaQueryWrapper<Task>()
.eq(Task::getChildId, childId)
.between(Task::getDeadline, todayStart, todayEnd)
);
long completed = allTasks.stream()
.filter(t -> "completed".equals(t.getStatus()))
.count();
int completionRate = allTasks.isEmpty() ? 0 : (int) (completed * 100 / allTasks.size());
// 待审核任务
List<Task> pendingReview = taskMapper.selectList(
new LambdaQueryWrapper<Task>()
.eq(Task::getChildId, childId)
.eq(Task::getNeedReview, 1)
.eq(Task::getStatus, "pending")
);
// 习惯打卡数量(统计今日完成的habit类任务)
long habitCount = allTasks.stream()
.filter(t -> "completed".equals(t.getStatus()))
.filter(t -> "habit".equals(t.getCategory()))
.count();
stats.put("completionRate", completionRate);
stats.put("pendingReviewCount", pendingReview.size());
stats.put("habitCount", (int) habitCount);
return stats;
}
需要在 TaskService.java 的 import 区域添加:
import java.util.HashMap;
import java.util.Map;
Run: cd cfc-backend && mvn clean compile
Expected: BUILD SUCCESS
Files:
cfc-frontend/utils/api.jsModify: cfc-backend/src/main/java/com/etotem/cfc/controller/UserController.java
[ ] Step 1: 修复前端 API URL
// api.js 第141行
// 改前:
export const getFamilyMembers = () => {
return request('/api/user/family-members', 'POST')
}
// 改后:
export const getFamilyMembers = () => {
return request('/api/family/user/family-members', 'POST')
}
[ ] Step 2: 后端 UserController 增加 /family-members 端点作为额外入口(可选,保持兼容)
如果不想改前端 URL,也可以在 UserController 中增加 /family-members 端点:
// UserController.java
@Resource
private FamilyMapper familyMapper;
@Resource
private ChildMapper childMapper;
@PostMapping("/family-members")
@Operation(summary = "获取家庭成员列表(兼容)")
public Result<Map<String, Object>> getFamilyMembers(HttpServletRequest request) {
Long userId = (Long) request.getAttribute("userId");
User user = userMapper.selectById(userId);
if (user == null || user.getFamilyId() == null) {
return Result.error("用户未加入家庭");
}
List<User> parents = userMapper.selectList(
new LambdaQueryWrapper<User>()
.eq(User::getFamilyId, user.getFamilyId())
.eq(User::getRole, "parent")
);
List<Child> children = childMapper.selectList(
new LambdaQueryWrapper<Child>()
.eq(Child::getFamilyId, user.getFamilyId())
);
Map<String, Object> result = new HashMap<>();
result.put("parents", parents);
result.put("children", children);
return Result.success(result);
}
Run: cd cfc-backend && mvn clean compile
Expected: BUILD SUCCESS
Files:
Modify: cfc-frontend/pages/index/parent-index.vue
[ ] Step 1: 添加 dashboard API 调用
// parent-index.vue script 顶部 import 区域添加
import { getFamilyMembers, getChildren, getPendingReviewTasks, approveTask, rejectTask, getPendingWishes } from '../../utils/api.js'
[ ] Step 2: 新 API 封装
// api.js 添加
export const getParentDashboard = () => {
return request('/api/parent/dashboard', 'POST')
}
[ ] Step 3: 改造 loadData 方法
// parent-index.vue loadData 方法改造
async loadData() {
try {
const userInfo = uni.getStorageSync('userInfo')
this.nickname = (userInfo && userInfo.nickname) ? userInfo.nickname : ''
// 获取仪表盘数据(包含孩子列表 + 统计数据)
try {
const dashRes = await getParentDashboard()
const dashData = dashRes.data || {}
const dashChildren = dashData.children || []
if (dashChildren.length > 0) {
this.children = dashChildren
this.selectedChild = dashChildren[0]
this.taskCompletionRate = dashData.avgCompletionRate || 85
this.habitCount = dashData.totalHabitCount || 0
}
} catch(e) {
console.log('获取仪表盘数据失败,降级到单独接口', e)
// 降级:使用原有逻辑
try {
const membersRes = await getFamilyMembers()
// ... 原有 getFamilyMembers 逻辑
} catch(e2) {
console.log('获取家庭成员失败', e2)
}
const childrenRes = await getChildren()
const rawChildren = childrenRes.data || []
this.children = rawChildren
if (this.children.length > 0 && !this.selectedChild) {
this.selectedChild = this.children[0]
}
}
// 获取待审核任务(保持不变)
const reviewRes = await getPendingReviewTasks()
const allReviewTasks = reviewRes.data || []
this.pendingReviewTasks = allReviewTasks.slice(0, 3)
// 获取待处理心愿(保持不变)
try {
const wishRes = await getPendingWishes()
this.pendingReviewWishes = wishRes.data || []
} catch (e) {
console.log('获取待处理心愿失败', e)
this.pendingReviewWishes = []
}
// 计算待处理数量(保持不变)
const reviewMap = {}
const wishMap = {}
for (const task of allReviewTasks) {
const cid = task.childId || task.assigneeId
if (cid) {
reviewMap[cid] = (reviewMap[cid] || 0) + 1
}
}
for (const wish of (this.pendingReviewWishes || [])) {
const cid = wish.childId
if (cid) {
wishMap[cid] = (wishMap[cid] || 0) + 1
}
}
this.children = this.children.map(c => ({
...c,
pendingReviewCount: reviewMap[c.id] || 0,
pendingWishCount: wishMap[c.id] || 0
}))
// 移除模拟数据,改用真实数据
// this.pendingTasks = [ ... ] // 删除模拟数据
// 如果有待审核任务或心愿,不显示模拟任务
if (allReviewTasks.length > 0) {
this.pendingTasks = allReviewTasks.slice(0, 3).map(t => ({
id: t.id,
title: t.title,
childName: t.childName
}))
} else {
this.pendingTasks = []
}
// 不再使用随机值
// this.taskCompletionRate = Math.floor(Math.random() * 30) + 70 // 删除这行
} catch (e) {
console.error('加载数据失败', e)
}
},
[ ] Step 4: 子卡展示系统积分(可选增强)
<!-- parent-index.vue 子卡中增加系统积分展示 -->
<text class="child-card-system-points" v-if="child.systemPoints > 0">
🏅 系统 {{ child.systemPoints }}
</text>
在 style 区域添加:
.child-card-system-points {
font-size: 20rpx;
color: #667eea;
font-weight: 500;
margin-left: 8rpx;
}
Files:
Modify: cfc-backend/src/main/java/com/etotem/cfc/controller/family/PointsController.java
[ ] Step 1: 添加系统积分余额接口
// PointsController.java
@PostMapping("/system-balance")
public Result<Map<String, Object>> getSystemBalance(@RequestBody Map<String, Object> params) {
Long childId = params.get("childId") != null ? Long.valueOf(params.get("childId").toString()) : null;
if (childId == null) {
return Result.error("childId不能为空");
}
int balance = pointsService.getSystemPointsBalance(childId);
Map<String, Object> result = new HashMap<>();
result.put("systemPoints", balance);
return Result.success(result);
}
@PostMapping("/logs-by-category")
public Result<Page<PointsLog>> getPointsLogsByCategory(@RequestBody Map<String, Object> params) {
Long childId = params.get("childId") != null ? Long.valueOf(params.get("childId").toString()) : null;
String category = params.get("category") != null ? params.get("category").toString() : null;
Integer page = params.get("page") != null ? Integer.valueOf(params.get("page").toString()) : 1;
Integer size = params.get("size") != null ? Integer.valueOf(params.get("size").toString()) : 10;
Page<PointsLog> logs = pointsService.getPointsLogsByCategory(childId, category, page, size);
return Result.success(logs);
}
[ ] Step 2: 编译验证
Run: cd cfc-backend && mvn clean compile
Expected: BUILD SUCCESS
Files:
cfc-frontend/pages/rewards/rewards.vue(积分展示区域)Modify: cfc-frontend/pages/profile/profile.vue(个人中心展示)
[ ] Step 1: 封装前端 API
// api.js 添加
export const getSystemBalance = (childId) => {
return request('/api/points/system-balance', 'POST', { childId })
}
[ ] Step 2: 积分展示页面调整
在 rewards.vue 或 profile.vue 的积分区域展示系统积分:
// profile.vue 或 rewards.vue 的 onShow/loadData 中
// 获取系统积分余额
try {
if (this.children && this.children.length > 0) {
const sysRes = await getSystemBalance(this.children[0].id)
this.systemPoints = sysRes.data.systemPoints || 0
}
} catch (e) {
console.log('获取系统积分失败', e)
}
Run: cd cfc-backend && mvn clean compile
Expected: BUILD SUCCESS
[ ] Step 2: 检查路由冲突
grep -rn '@GetMapping\|@PostMapping\|@PutMapping\|@DeleteMapping' src/main/java/com/etotem/cfc/controller/ | grep -oP '@\w+Mapping\("\K[^"]*' | sort -u | grep -E 'dashboard|system-balance|logs-by-category|family-members'
Expected: 无冲突路由
Run: cd cfc-backend && mvn spring-boot:run
Expected: Started Application in X seconds
1. 需求覆盖:
2. 占位符检查:
3. 类型一致性:
Child.systemPoints 类型一致 (Integer)PointsLog.category 类型一致 (String)PointsService.awardSystemPoints 返回 intgetChildTaskStats 返回 Map<String, Object>