ソースを参照

feat: 新增成长规划Controller、任务统计服务和申请记录实体

- GrowthPlanController: 成长规划接口

- TaskStatsService: 任务统计服务

- GuideApplicationRecord: 规划师申请记录实体和Mapper

- stats包: 统计相关控制器

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
User 4 ヶ月 前
コミット
0055759650

+ 95 - 0
zxyj-backend/src/main/java/com/zxyj/controller/growth/GrowthPlanController.java

@@ -0,0 +1,95 @@
+package com.zxyj.controller.growth;
+
+import com.zxyj.common.Result;
+import com.zxyj.entity.GrowthPlan;
+import com.zxyj.service.GrowthPlanService;
+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 java.util.List;
+import java.util.Map;
+
+@Tag(name = "成长方案", description = "成长方案创建、更新、复盘等接口")
+@RestController
+@RequestMapping("/api/growth/plan")
+public class GrowthPlanController {
+
+    @Resource
+    private GrowthPlanService growthPlanService;
+
+    @Operation(summary = "创建成长方案")
+    @PostMapping("/create")
+    public Result<GrowthPlan> createPlan(@RequestBody GrowthPlan plan,
+                                          @RequestAttribute("userId") Long userId) {
+        if (plan.getChildId() == null) {
+            return Result.error("请选择孩子");
+        }
+        plan.setTeacherId(userId);
+        GrowthPlan result = growthPlanService.createPlan(plan);
+        return Result.success(result);
+    }
+
+    @Operation(summary = "获取孩子的成长方案列表")
+    @PostMapping("/child/{childId}")
+    public Result<List<GrowthPlan>> getChildPlans(@PathVariable Long childId) {
+        List<GrowthPlan> plans = growthPlanService.getByChildId(childId);
+        return Result.success(plans);
+    }
+
+    @Operation(summary = "获取孩子当前活跃方案")
+    @PostMapping("/child/{childId}/active")
+    public Result<GrowthPlan> getActivePlan(@PathVariable Long childId) {
+        GrowthPlan plan = growthPlanService.getActivePlanByChildId(childId);
+        if (plan == null) {
+            return Result.error("暂无活跃方案");
+        }
+        return Result.success(plan);
+    }
+
+    @Operation(summary = "获取方案详情")
+    @PostMapping("/{id}")
+    public Result<GrowthPlan> getPlanDetail(@PathVariable Long id) {
+        GrowthPlan plan = growthPlanService.getById(id);
+        if (plan == null) {
+            return Result.error("方案不存在");
+        }
+        return Result.success(plan);
+    }
+
+    @Operation(summary = "更新成长方案")
+    @PostMapping("/update")
+    public Result<Boolean> updatePlan(@RequestBody Map<String, Object> body) {
+        Long id = body.get("id") != null ? Long.valueOf(body.get("id").toString()) : null;
+        if (id == null) return Result.error("方案ID不能为空");
+
+        GrowthPlan plan = growthPlanService.getById(id);
+        if (plan == null) return Result.error("方案不存在");
+
+        if (body.containsKey("planTitle")) plan.setPlanTitle(body.get("planTitle").toString());
+        if (body.containsKey("planContent")) plan.setPlanContent(body.get("planContent").toString());
+        if (body.containsKey("targetDanLevel")) plan.setTargetDanLevel(body.get("targetDanLevel").toString());
+        if (body.containsKey("durationMonths")) plan.setDurationMonths(Integer.valueOf(body.get("durationMonths").toString()));
+        if (body.containsKey("status")) plan.setStatus(body.get("status").toString());
+        if (body.containsKey("startDate") && body.get("startDate") != null) {
+            plan.setStartDate(java.sql.Date.valueOf(body.get("startDate").toString()));
+        }
+        if (body.containsKey("endDate") && body.get("endDate") != null) {
+            plan.setEndDate(java.sql.Date.valueOf(body.get("endDate").toString()));
+        }
+
+        boolean success = growthPlanService.updatePlan(plan);
+        return Result.success(success);
+    }
+
+    @Operation(summary = "复盘成长方案")
+    @PostMapping("/{id}/review")
+    public Result<Boolean> reviewPlan(@PathVariable Long id) {
+        boolean success = growthPlanService.reviewPlan(id);
+        if (!success) {
+            return Result.error("方案不存在");
+        }
+        return Result.success(true);
+    }
+}

+ 58 - 0
zxyj-backend/src/main/java/com/zxyj/controller/stats/StatsController.java

@@ -0,0 +1,58 @@
+package com.zxyj.controller.stats;
+
+import com.zxyj.common.Result;
+import com.zxyj.entity.User;
+import com.zxyj.mapper.ChildMapper;
+import com.zxyj.mapper.UserMapper;
+import com.zxyj.service.TaskStatsService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.format.annotation.DateTimeFormat;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.Map;
+
+@Tag(name = "任务统计", description = "任务完成率分析")
+@RestController
+@RequestMapping("/api/stats")
+public class StatsController {
+
+    @Resource
+    private TaskStatsService taskStatsService;
+
+    @Resource
+    private UserMapper userMapper;
+
+    @Resource
+    private ChildMapper childMapper;
+
+    @Operation(summary = "孩子任务完成率", description = "统计指定时间段内单个孩子的任务完成情况")
+    @PostMapping("/child-completion")
+    public Result<Map<String, Object>> childCompletion(
+            @RequestAttribute("userId") Long userId,
+            @RequestParam(required = false) Long childId,
+            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate,
+            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate) {
+        User user = userMapper.selectById(userId);
+        if (user == null) return Result.error("用户不存在");
+
+        Map<String, Object> stats = taskStatsService.getChildStats(childId, startDate, endDate);
+        return Result.success(stats);
+    }
+
+    @Operation(summary = "家庭任务完成率", description = "统计家庭维度所有孩子的任务完成情况")
+    @PostMapping("/family-completion")
+    public Result<Map<String, Object>> familyCompletion(
+            @RequestAttribute("userId") Long userId,
+            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate,
+            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate) {
+        User user = userMapper.selectById(userId);
+        if (user == null) return Result.error("用户不存在");
+        if (user.getFamilyId() == null) return Result.error("未关联家庭");
+
+        Map<String, Object> stats = taskStatsService.getFamilyStats(user.getFamilyId(), startDate, endDate);
+        return Result.success(stats);
+    }
+}

+ 31 - 0
zxyj-backend/src/main/java/com/zxyj/entity/GuideApplicationRecord.java

@@ -0,0 +1,31 @@
+package com.zxyj.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("guide_application_records")
+public class GuideApplicationRecord implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long applicationId;
+
+    private Long userId;
+
+    private String action;
+
+    private String fromStatus;
+
+    private String toStatus;
+
+    private String remark;
+
+    private Date createdAt;
+}

+ 9 - 0
zxyj-backend/src/main/java/com/zxyj/mapper/GuideApplicationRecordMapper.java

@@ -0,0 +1,9 @@
+package com.zxyj.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zxyj.entity.GuideApplicationRecord;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface GuideApplicationRecordMapper extends BaseMapper<GuideApplicationRecord> {
+}

+ 110 - 0
zxyj-backend/src/main/java/com/zxyj/service/TaskStatsService.java

@@ -0,0 +1,110 @@
+package com.zxyj.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.zxyj.entity.Child;
+import com.zxyj.entity.Task;
+import com.zxyj.mapper.ChildMapper;
+import com.zxyj.mapper.TaskMapper;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Service
+public class TaskStatsService {
+
+    @Resource
+    private TaskMapper taskMapper;
+
+    @Resource
+    private ChildMapper childMapper;
+
+    /**
+     * 获取单个孩子的任务完成统计
+     */
+    public Map<String, Object> getChildStats(Long childId, Date startDate, Date endDate) {
+        List<Task> tasks = queryTasks(null, childId, startDate, endDate);
+        return buildStats(tasks, null, childId);
+    }
+
+    /**
+     * 获取家庭维度的任务完成统计
+     */
+    public Map<String, Object> getFamilyStats(Long familyId, Date startDate, Date endDate) {
+        List<Task> tasks = queryTasks(familyId, null, startDate, endDate);
+        Map<String, Object> stats = buildStats(tasks, familyId, null);
+
+        // 按孩子分组
+        List<Child> children = childMapper.selectList(
+            new LambdaQueryWrapper<Child>().eq(Child::getFamilyId, familyId));
+        List<Map<String, Object>> byChild = new ArrayList<>();
+        for (Child child : children) {
+            List<Task> childTasks = tasks.stream()
+                .filter(t -> child.getId().equals(t.getChildId()))
+                .collect(Collectors.toList());
+            if (!childTasks.isEmpty()) {
+                Map<String, Object> childStat = new HashMap<>();
+                childStat.put("childId", child.getId());
+                childStat.put("childName", child.getNickname());
+                childStat.put("totalTasks", childTasks.size());
+                long completed = childTasks.stream().filter(t -> "completed".equals(t.getStatus())).count();
+                childStat.put("completedTasks", completed);
+                childStat.put("completionRate", childTasks.size() > 0 ? Math.round(completed * 1000.0 / childTasks.size()) / 10.0 : 0);
+                byChild.add(childStat);
+            }
+        }
+        stats.put("byChild", byChild);
+        return stats;
+    }
+
+    /**
+     * 查询指定范围的任务(排除模板任务和已取消的)
+     */
+    private List<Task> queryTasks(Long familyId, Long childId, Date startDate, Date endDate) {
+        LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<Task>()
+            .ne(Task::getIsTemplate, 1)
+            .ne(Task::getStatus, "cancelled");
+
+        if (familyId != null) {
+            wrapper.eq(Task::getFamilyId, familyId);
+        }
+        if (childId != null) {
+            wrapper.eq(Task::getChildId, childId);
+        }
+        if (startDate != null) {
+            wrapper.ge(Task::getCreatedAt, startDate);
+        }
+        if (endDate != null) {
+            wrapper.le(Task::getCreatedAt, endDate);
+        }
+        wrapper.orderByDesc(Task::getCreatedAt);
+
+        return taskMapper.selectList(wrapper);
+    }
+
+    /**
+     * 构建统计结果
+     */
+    private Map<String, Object> buildStats(List<Task> tasks, Long familyId, Long childId) {
+        Date now = new Date();
+        int total = tasks.size();
+        long completed = tasks.stream().filter(t -> "completed".equals(t.getStatus())).count();
+        long pending = tasks.stream().filter(t -> "pending".equals(t.getStatus())).count();
+        long overdue = tasks.stream()
+            .filter(t -> "pending".equals(t.getStatus()) && t.getDeadline() != null && t.getDeadline().before(now))
+            .count();
+
+        double rate = total > 0 ? Math.round(completed * 1000.0 / total) / 10.0 : 0;
+
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("totalTasks", total);
+        result.put("completedTasks", completed);
+        result.put("pendingTasks", pending);
+        result.put("overdueTasks", overdue);
+        result.put("completionRate", rate);
+
+        return result;
+    }
+}