فهرست منبع

feat(S-cfc-326): child.task.checkin 使绿——打卡提交端点/方案进度/小游戏分数上限(5红转绿)

Sisyphus Agent 1 روز پیش
والد
کامیت
b2114dc9dc

+ 63 - 33
cfc-backend/src/main/java/com/etotem/cfc/controller/GrowthTaskController.java

@@ -14,14 +14,17 @@ import java.util.Map;
 @RequestMapping("/api/growth-task")
 public class GrowthTaskController {
 
- @Resource
- private GrowthTaskService growthTaskService;
+  @Resource
+  private GrowthTaskService growthTaskService;
 
- @Resource
- private GrowthTaskMapper growthTaskMapper;
+  @Resource
+  private GrowthTaskMapper growthTaskMapper;
 
- @Resource
- private com.etotem.cfc.mapper.TaskMapper taskMapper;
+  @Resource
+  private com.etotem.cfc.mapper.TaskMapper taskMapper;
+
+  @Resource
+  private com.etotem.cfc.service.TaskService taskService;
 
     @PostMapping("/list")
     public Result<List<Map<String, Object>>> list(@RequestAttribute("userId") Long userId,
@@ -29,32 +32,59 @@ public class GrowthTaskController {
 return Result.success(growthTaskService.getTaskList(userId, params));
     }
 
- @PostMapping("/claim")
- public Result<String> claim(@RequestAttribute("userId") Long userId,
-     @RequestBody Map<String, Long> params) {
+  @PostMapping("/claim")
+  public Result<String> claim(@RequestAttribute("userId") Long userId,
+      @RequestBody Map<String, Long> params) {
    // 统一任务系统:奖励自动发放,无 claimed 概念
    return Result.success("奖励已自动发放");
- }
-
- @PostMapping("/accept-dynamic")
- public Result<Map<String, Object>> acceptDynamic(@RequestAttribute("userId") Long userId,
-     @RequestBody Map<String, Object> params) {
-   Object taskIdObj = params.get("taskId");
-   if (taskIdObj == null) {
-     return Result.error("taskId不能为空");
-   }
-   Long taskId = ((Number) taskIdObj).longValue();
-   // 统一任务系统:AI 任务已迁入 tasks 表(source_type=ai)
-   com.etotem.cfc.entity.Task task = taskMapper.selectById(taskId);
-   if (task == null) {
-     return Result.error("任务不存在");
-   }
-   Map<String, Object> taskInfo = new java.util.HashMap<>();
-   taskInfo.put("id", task.getId());
-   taskInfo.put("title", task.getTitle());
-   taskInfo.put("description", task.getDescription());
-   taskInfo.put("dimension", task.getDimensionCode());
-   taskInfo.put("rewardPoints", task.getPoints());
-   return Result.success(taskInfo);
- }
- }
+  }
+
+  @PostMapping("/accept-dynamic")
+  public Result<Map<String, Object>> acceptDynamic(@RequestAttribute("userId") Long userId,
+      @RequestBody Map<String, Object> params) {
+    Object taskIdObj = params.get("taskId");
+    if (taskIdObj == null) {
+      return Result.error("taskId不能为空");
+    }
+    Long taskId = ((Number) taskIdObj).longValue();
+    // 统一任务系统:AI 任务已迁入 tasks 表(source_type=ai)
+    com.etotem.cfc.entity.Task task = taskMapper.selectById(taskId);
+    if (task == null) {
+      return Result.error("任务不存在");
+    }
+    Map<String, Object> taskInfo = new java.util.HashMap<>();
+    taskInfo.put("id", task.getId());
+    taskInfo.put("title", task.getTitle());
+    taskInfo.put("description", task.getDescription());
+    taskInfo.put("dimension", task.getDimensionCode());
+    taskInfo.put("rewardPoints", task.getPoints());
+    return Result.success(taskInfo);
+  }
+
+  /**
+   * 方案任务打卡提交(SC-C-05 任务打卡⭐,能力键 child.task.checkin)。
+   *
+   * <p>委托统一任务系统 {@code TaskService.completeTask} 完成打卡提交:幂等(T24-A)、
+   * 发奖/审核流转/逾期限均走既有链路;本端点只做入参承载与结果包装。</p>
+   *
+   * @param dto 打卡提交入参(memberId/taskId + photoUrl/content 凭证)
+   */
+  @PostMapping("/checkin")
+  public Result<Map<String, Object>> checkin(@RequestBody com.etotem.cfc.dto.TaskCheckinDTO dto,
+      @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
+    if (dto == null) {
+      return Result.error("打卡入参不能为空");
+    }
+    if (dto.getTaskId() == null) {
+      return Result.error("taskId不能为空");
+    }
+    Long memberId = dto.getMemberId() != null ? dto.getMemberId() : currentMemberId;
+    Map<String, Object> result = taskService.completeTask(dto.getTaskId(), memberId,
+        dto.getPhotoUrl(), dto.getContent(),
+        dto.getContentType() != null ? dto.getContentType() : "text");
+    if (result != null) {
+      return Result.success(result);
+    }
+    return Result.error("任务不存在、无权完成或状态已变更");
+  }
+  }

+ 14 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/growth/GrowthPlanController.java

@@ -60,6 +60,20 @@ public class GrowthPlanController {
         return Result.success(plan);
     }
 
+    /**
+     * 方案打卡进度(SC-C-08 方案打卡进度,能力键 child.task.checkin)。
+     * 统计该成员「方案任务」(tasks.source_type='plan')完成情况,返回
+     * completedTasks/totalTasks/progressPercent。
+     */
+    @Operation(summary = "获取成员方案打卡进度")
+    @PostMapping("/child/{memberId}/progress")
+    public Result<com.etotem.cfc.dto.GrowthPlanProgressVO> getPlanProgress(
+            @PathVariable Long memberId,
+            @RequestBody(required = false) Map<String, Object> body) {
+        Long planId = body != null ? ParamUtils.getLong(body.get("planId")) : null;
+        return Result.success(growthPlanService.getProgress(memberId, planId));
+    }
+
     @Operation(summary = "获取方案详情")
     @PostMapping("/{id}")
     public Result<GrowthPlan> getPlanDetail(@PathVariable Long id) {

+ 21 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/GrowthPlanProgressVO.java

@@ -0,0 +1,21 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+/**
+ * 方案打卡进度 VO(SC-C-08 方案打卡进度,能力键 child.task.checkin)。
+ *
+ * <p>completedTasks:已完成任务数(status=completed);totalTasks:方案任务总数;
+ * progressPercent:完成率(0-100,四舍五入)。</p>
+ */
+@Data
+public class GrowthPlanProgressVO {
+    /** 已完成任务数 */
+    private Integer completedTasks;
+
+    /** 方案任务总数 */
+    private Integer totalTasks;
+
+    /** 完成率(0-100) */
+    private Integer progressPercent;
+}

+ 27 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/TaskCheckinDTO.java

@@ -0,0 +1,27 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+/**
+ * 方案任务打卡提交 DTO(SC-C-05 任务打卡⭐,能力键 child.task.checkin)。
+ *
+ * <p>memberId 为打卡归属成员;taskId 为被打卡任务;photoUrl/content 为打卡提交凭证(至少其一)。
+ * 提交流转委托统一任务系统(TaskService.completeTask),幂等/发奖/审核由既有链路保证(T24-A)。</p>
+ */
+@Data
+public class TaskCheckinDTO {
+    /** 打卡归属成员ID(family_members.id,切换视角时前端显式传入) */
+    private Long memberId;
+
+    /** 被打卡任务ID */
+    private Long taskId;
+
+    /** 提交凭证-图片URL */
+    private String photoUrl;
+
+    /** 提交凭证-文字内容 */
+    private String content;
+
+    /** 内容类型(text/photo 等,默认 text) */
+    private String contentType;
+}

+ 41 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/GrowthPlanService.java

@@ -1,11 +1,14 @@
 package com.etotem.cfc.service;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.dto.GrowthPlanProgressVO;
 import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.GrowthPlan;
+import com.etotem.cfc.entity.Task;
 import com.etotem.cfc.entity.User;
 import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.GrowthPlanMapper;
+import com.etotem.cfc.mapper.TaskMapper;
 import com.etotem.cfc.mapper.UserMapper;
 import org.springframework.stereotype.Service;
 
@@ -29,6 +32,44 @@ public class GrowthPlanService {
     @Resource
     private UserMapper userMapper;
 
+    @Resource
+    private TaskMapper taskMapper;
+
+    /** 方案任务来源标记(TaskPlanService 生成方案任务时写入 source_type) */
+    private static final String PLAN_TASK_SOURCE_TYPE = "plan";
+
+    /**
+     * 方案打卡进度(SC-C-08 方案打卡进度,能力键 child.task.checkin)。
+     *
+     * <p>口径:统计成员名下「方案任务」(tasks.source_type='plan',且归属于该成员)的完成情况——
+     * totalTasks 为方案任务总数,completedTasks 为 status=completed 的任务数,
+     * progressPercent = completed/total(0-100,四舍五入;total=0 时返回 0)。</p>
+     *
+     * @param memberId 家庭成员ID
+     * @param planId   可选的方案ID(精确到某个方案;为空则统计该成员全部方案任务)
+     */
+    public GrowthPlanProgressVO getProgress(Long memberId, Long planId) {
+        LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<Task>()
+                .eq(Task::getFamilyMemberId, memberId)
+                .eq(Task::getSourceType, PLAN_TASK_SOURCE_TYPE);
+        if (planId != null) {
+            wrapper.eq(Task::getSourceId, planId);
+        }
+        List<Task> tasks = taskMapper.selectList(wrapper);
+
+        long total = tasks.size();
+        long completed = tasks.stream()
+                .filter(t -> "completed".equals(t.getStatus()))
+                .count();
+        int progressPercent = total == 0 ? 0 : (int) Math.round(completed * 100.0 / total);
+
+        GrowthPlanProgressVO vo = new GrowthPlanProgressVO();
+        vo.setCompletedTasks((int) completed);
+        vo.setTotalTasks((int) total);
+        vo.setProgressPercent(progressPercent);
+        return vo;
+    }
+
     public GrowthPlan createPlan(GrowthPlan plan) {
         if (plan.getStatus() == null) {
             plan.setStatus("active");

+ 10 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/MiniGameService.java

@@ -130,6 +130,16 @@ public class MiniGameService implements MiniGameServiceInterface {
             throw new RuntimeException("成员不存在");
         }
 
+        // G-09 契约(child.task.checkin):score 限 0~100,越界分数截断——防刷分换取超额积分
+        if (score != null) {
+            if (score < 0) {
+                score = 0;
+            }
+            if (score > 100) {
+                score = 100;
+            }
+        }
+
         // 计算积分
         int basePoints = game.getDefaultPoints() != null ? game.getDefaultPoints() : 5;
         int earnedPoints = calculatePoints(gameCode, basePoints, completionTime, score);