Browse Source

refactor(stats): 合并 ReportStatsController 到 StatsController,统一 /api/stats 路径

- 将 /api/stats/child/overview、/family/summary、/points/ranking 三个端点迁移至 StatsController
- 删除独立的 ReportStatsController,消除同路径下双 Controller 冲突
- 所有接口路径不变,前端调用无需修改
Sisyphus 1 tháng trước cách đây
mục cha
commit
ae3d448078

+ 0 - 220
cfc-backend/src/main/java/com/etotem/cfc/controller/ReportStatsController.java

@@ -1,220 +0,0 @@
-package com.etotem.cfc.controller;
-
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.etotem.cfc.common.Result;
-import com.etotem.cfc.entity.FamilyMember;
-import com.etotem.cfc.entity.GameRecord;
-import com.etotem.cfc.entity.PointsLog;
-import com.etotem.cfc.entity.Task;
-import com.etotem.cfc.mapper.FamilyMemberMapper;
-import com.etotem.cfc.mapper.PointsLogMapper;
-import com.etotem.cfc.service.GameRecordService;
-import com.etotem.cfc.service.TaskService;
-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.text.SimpleDateFormat;
-import java.util.*;
-import java.util.stream.Collectors;
-import com.etotem.cfc.util.ParamUtils;
-
-@Tag(name = "统计报表", description = "统一数据统计与报表接口")
-@RestController
-@RequestMapping("/api/stats")
-public class ReportStatsController {
-
-    @Resource
-    private FamilyMemberMapper familyMemberMapper;
-
-    @Resource
-    private PointsLogMapper pointsLogMapper;
-
-    @Resource
-    private TaskService taskService;
-
-    @Resource
-    private GameRecordService gameRecordService;
-
-    @Operation(summary = "孩子数据概览")
-    @PostMapping("/child/overview")
-    public Result<Map<String, Object>> childOverview(@RequestBody Map<String, Object> body) {
-        Long memberId = ParamUtils.getLong(body.get("memberId"));
-        Map<String, Object> result = new HashMap<>();
-
-        // 基础信息
-        FamilyMember child = familyMemberMapper.selectById(memberId);
-        result.put("totalPoints", child != null ? child.getTotalPoints() : 0);
-
-        // 月活跃天数(当月有积分变动的天数)
-        Calendar monthCal = Calendar.getInstance();
-        monthCal.set(Calendar.DAY_OF_MONTH, 1);
-        monthCal.set(Calendar.HOUR_OF_DAY, 0);
-        monthCal.set(Calendar.MINUTE, 0);
-        monthCal.set(Calendar.SECOND, 0);
-        monthCal.set(Calendar.MILLISECOND, 0);
-        Date monthStart = monthCal.getTime();
-
-        List<PointsLog> monthLogs = pointsLogMapper.selectList(
-                new LambdaQueryWrapper<PointsLog>()
-                        .eq(PointsLog::getChildId, memberId)
-                        .ge(PointsLog::getCreatedAt, monthStart)
-                        .orderByAsc(PointsLog::getCreatedAt));
-
-        Set<String> activeDays = new HashSet<>();
-        SimpleDateFormat daySdf = new SimpleDateFormat("yyyy-MM-dd");
-        for (PointsLog log : monthLogs) {
-            if (log.getCreatedAt() != null) {
-                activeDays.add(daySdf.format(log.getCreatedAt()));
-            }
-        }
-        result.put("monthActiveDays", activeDays.size());
-
-        // 任务统计
-        List<Task> tasks = taskService.getAllTasksByChild(memberId);
-        long taskTotal = tasks.size();
-        long taskCompleted = tasks.stream().filter(t -> "completed".equals(t.getStatus())).count();
-        long taskPending = tasks.stream().filter(t -> "pending".equals(t.getStatus())).count();
-        long taskInReview = tasks.stream().filter(t -> "review".equals(t.getStatus())).count();
-
-        Map<String, Object> taskStats = new HashMap<>();
-        taskStats.put("total", taskTotal);
-        taskStats.put("completed", taskCompleted);
-        taskStats.put("pending", taskPending);
-        taskStats.put("inReview", taskInReview);
-        taskStats.put("completionRate", taskTotal > 0 ? (int) (taskCompleted * 10000 / taskTotal) / 100 : 0);
-        result.put("taskStats", taskStats);
-
-        // 游戏统计
-        List<GameRecord> records = gameRecordService.listByChildId(memberId);
-        long gameTotal = records.size();
-        int gameTotalPoints = records.stream().mapToInt(GameRecord::getPointsEarned).sum();
-        int bestScore = records.stream().mapToInt(GameRecord::getScore).max().orElse(0);
-        int totalTime = records.stream().mapToInt(GameRecord::getCompletionTime).sum();
-
-        Map<String, Object> gameStats = new HashMap<>();
-        gameStats.put("total", gameTotal);
-        gameStats.put("totalPoints", gameTotalPoints);
-        gameStats.put("bestScore", bestScore);
-        gameStats.put("totalTime", totalTime);
-        result.put("gameStats", gameStats);
-
-        // 积分趋势(近7天)
-        Calendar cal = Calendar.getInstance();
-        Date today = cal.getTime();
-        cal.add(Calendar.DAY_OF_YEAR, -7);
-        Date sevenDaysAgo = cal.getTime();
-
-        List<PointsLog> recentLogs = pointsLogMapper.selectList(
-                new LambdaQueryWrapper<PointsLog>()
-                        .eq(PointsLog::getChildId, memberId)
-                        .ge(PointsLog::getCreatedAt, sevenDaysAgo)
-                        .orderByAsc(PointsLog::getCreatedAt));
-
-        List<Map<String, Object>> pointsTrend = new ArrayList<>();
-        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
-        for (int i = 6; i >= 0; i--) {
-            Calendar dayCal = Calendar.getInstance();
-            dayCal.add(Calendar.DAY_OF_YEAR, -i);
-            dayCal.set(Calendar.HOUR_OF_DAY, 0);
-            dayCal.set(Calendar.MINUTE, 0);
-            dayCal.set(Calendar.SECOND, 0);
-            dayCal.set(Calendar.MILLISECOND, 0);
-            Date dayStart = dayCal.getTime();
-
-            dayCal.set(Calendar.HOUR_OF_DAY, 23);
-            dayCal.set(Calendar.MINUTE, 59);
-            dayCal.set(Calendar.SECOND, 59);
-            Date dayEnd = dayCal.getTime();
-
-            Date finalDayStart = dayStart;
-            Date finalDayEnd = dayEnd;
-            int dayPoints = recentLogs.stream()
-                    .filter(l -> l.getCreatedAt() != null
-                            && l.getCreatedAt().after(finalDayStart)
-                            && l.getCreatedAt().before(finalDayEnd))
-                    .mapToInt(PointsLog::getAmount)
-                    .sum();
-            Map<String, Object> point = new HashMap<>();
-            point.put("date", sdf.format(dayStart));
-            point.put("points", dayPoints);
-            pointsTrend.add(point);
-        }
-        result.put("pointsTrend", pointsTrend);
-
-        return Result.success(result);
-    }
-
-    @Operation(summary = "家庭汇总统计")
-    @PostMapping("/family/summary")
-    public Result<Map<String, Object>> familySummary(@RequestBody Map<String, Object> body,
-                                                     @RequestAttribute(value = "familyId", required = false) Long familyId) {
-        if (familyId == null) {
-            return Result.noFamily("请先创建或加入家庭");
-        }
-        List<FamilyMember> children = familyMemberMapper.selectList(
-                new LambdaQueryWrapper<FamilyMember>().eq(FamilyMember::getFamilyId, familyId));
-
-        List<Map<String, Object>> childrenStats = new ArrayList<>();
-        int familyTotalTasks = 0;
-        int familyCompletedTasks = 0;
-        int familyTotalGames = 0;
-
-        for (FamilyMember child : children) {
-            Map<String, Object> childStat = new HashMap<>();
-            childStat.put("memberId", child.getId());
-            childStat.put("childName", child.getNickname());
-
-            List<Task> tasks = taskService.getAllTasksByChild(child.getId());
-            long c = tasks.stream().filter(t -> "completed".equals(t.getStatus())).count();
-            childStat.put("taskTotal", tasks.size());
-            childStat.put("taskCompleted", c);
-            childStat.put("points", child.getTotalPoints());
-
-            List<GameRecord> records = gameRecordService.listByChildId(child.getId());
-            childStat.put("gameTotal", records.size());
-
-            familyTotalTasks += tasks.size();
-            familyCompletedTasks += c;
-            familyTotalGames += records.size();
-            childrenStats.add(childStat);
-        }
-
-        Map<String, Object> result = new HashMap<>();
-        result.put("children", childrenStats);
-        result.put("familyTotalTasks", familyTotalTasks);
-        result.put("familyCompletedTasks", familyCompletedTasks);
-        result.put("familyCompletionRate", familyTotalTasks > 0
-                ? (int) (familyCompletedTasks * 10000 / familyTotalTasks) / 100 : 0);
-        result.put("familyTotalGames", familyTotalGames);
-        result.put("childCount", children.size());
-
-        return Result.success(result);
-    }
-
-    @Operation(summary = "积分排行榜(家庭内)")
-    @PostMapping("/points/ranking")
-    public Result<List<Map<String, Object>>> pointsRanking(@RequestBody Map<String, Object> body,
-                                                           @RequestAttribute(value = "familyId", required = false) Long familyId) {
-        if (familyId == null) {
-            return Result.noFamily("请先创建或加入家庭");
-        }
-        List<FamilyMember> children = familyMemberMapper.selectList(
-                new LambdaQueryWrapper<FamilyMember>()
-                        .eq(FamilyMember::getFamilyId, familyId)
-                        .orderByDesc(FamilyMember::getTotalPoints));
-
-        List<Map<String, Object>> ranking = new ArrayList<>();
-        int rank = 1;
-        for (FamilyMember child : children) {
-            Map<String, Object> item = new HashMap<>();
-            item.put("rank", rank++);
-            item.put("memberId", child.getId());
-            item.put("childName", child.getNickname());
-            item.put("points", child.getTotalPoints());
-            ranking.add(item);
-        }
-        return Result.success(ranking);
-    }
-}

+ 232 - 202
cfc-backend/src/main/java/com/etotem/cfc/controller/stats/StatsController.java

@@ -1,24 +1,30 @@
 package com.etotem.cfc.controller.stats;
 
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.etotem.cfc.common.Result;
 import com.etotem.cfc.entity.AfterSalesRequest;
-import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.CommissionRecord;
 import com.etotem.cfc.entity.Family;
+import com.etotem.cfc.entity.FamilyMember;
+import com.etotem.cfc.entity.GameRecord;
 import com.etotem.cfc.entity.MemberUpgradeRecord;
+import com.etotem.cfc.entity.PointsLog;
 import com.etotem.cfc.entity.Task;
 import com.etotem.cfc.entity.TrialMembership;
 import com.etotem.cfc.entity.User;
 import com.etotem.cfc.mapper.AfterSalesRequestMapper;
-import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.CommissionRecordMapper;
 import com.etotem.cfc.mapper.FamilyMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.GuidePackageMapper;
 import com.etotem.cfc.mapper.MemberUpgradeRecordMapper;
+import com.etotem.cfc.mapper.PointsLogMapper;
 import com.etotem.cfc.mapper.TaskMapper;
 import com.etotem.cfc.mapper.TrialMembershipMapper;
 import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.GameRecordService;
+import com.etotem.cfc.service.TaskService;
 import com.etotem.cfc.service.TaskStatsService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
@@ -27,83 +33,51 @@ import org.springframework.web.bind.annotation.*;
 import javax.annotation.Resource;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.concurrent.CompletableFuture;
 import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
 import com.etotem.cfc.util.SortUtil;
 import com.etotem.cfc.util.ParamUtils;
 
-@Tag(name = "任务统计", description = "任务完成率分析")
+@Tag(name = "统计", description = "管理后台数据看板、任务完成率、家庭数据概览")
 @RestController
 @RequestMapping("/api/stats")
 public class StatsController {
 
-    @Resource
-    private TaskStatsService taskStatsService;
-
-    @Resource
-    private UserMapper userMapper;
-
-    @Resource
-    private FamilyMemberMapper familyMemberMapper;
-
-    @Resource
-    private FamilyMapper familyMapper;
-
-    @Resource
-    private TaskMapper taskMapper;
-
-    @Resource
-    private GuidePackageMapper guidePackageMapper;
-
-    @Resource
-    private ThreadPoolTaskExecutor taskExecutor;
-
-    @Resource
-    private CommissionRecordMapper commissionRecordMapper;
-
-    @Resource
-    private TrialMembershipMapper trialMembershipMapper;
-
-    @Resource
-    private MemberUpgradeRecordMapper memberUpgradeRecordMapper;
-
-    @Resource
-    private AfterSalesRequestMapper afterSalesRequestMapper;
+    @Resource private TaskStatsService taskStatsService;
+    @Resource private UserMapper userMapper;
+    @Resource private FamilyMemberMapper familyMemberMapper;
+    @Resource private FamilyMapper familyMapper;
+    @Resource private TaskService taskService;
+    @Resource private TaskMapper taskMapper;
+    @Resource private GuidePackageMapper guidePackageMapper;
+    @Resource private ThreadPoolTaskExecutor taskExecutor;
+    @Resource private CommissionRecordMapper commissionRecordMapper;
+    @Resource private TrialMembershipMapper trialMembershipMapper;
+    @Resource private MemberUpgradeRecordMapper memberUpgradeRecordMapper;
+    @Resource private AfterSalesRequestMapper afterSalesRequestMapper;
+    @Resource private PointsLogMapper pointsLogMapper;
+    @Resource private GameRecordService gameRecordService;
+
+    // ── 管理后台仪表盘 ──
 
     @Operation(summary = "管理后台仪表盘汇总")
     @PostMapping("/dashboard")
     public Result<Map<String, Object>> getDashboardSummary() {
         Map<String, Object> data = new HashMap<>();
-
-        CompletableFuture<Long> familyFuture = CompletableFuture.supplyAsync(() ->
-            familyMapper.selectCount(null), taskExecutor);
-        CompletableFuture<Long> parentFuture = CompletableFuture.supplyAsync(() ->
-            userMapper.selectCount(new QueryWrapper<User>().eq("role", "parent")), taskExecutor);
-        CompletableFuture<Long> childFuture = CompletableFuture.supplyAsync(() ->
-            familyMemberMapper.selectCount(null), taskExecutor);
-        CompletableFuture<Long> teacherFuture = CompletableFuture.supplyAsync(() ->
-            userMapper.selectCount(new QueryWrapper<User>().eq("role", "teacher")), taskExecutor);
-        CompletableFuture<Long> pendingGuideFuture = CompletableFuture.supplyAsync(() ->
-            userMapper.selectCount(new QueryWrapper<User>().eq("role", "teacher").eq("teacher_status", "pending")), taskExecutor);
-        CompletableFuture<Long> pendingPackageFuture = CompletableFuture.supplyAsync(() ->
-            guidePackageMapper.selectCount(new QueryWrapper<com.etotem.cfc.entity.GuidePackage>()
-                .eq("status", "pending")), taskExecutor);
+        CompletableFuture<Long> familyFuture = CompletableFuture.supplyAsync(() -> familyMapper.selectCount(null), taskExecutor);
+        CompletableFuture<Long> parentFuture = CompletableFuture.supplyAsync(() -> userMapper.selectCount(new QueryWrapper<User>().eq("role", "parent")), taskExecutor);
+        CompletableFuture<Long> childFuture = CompletableFuture.supplyAsync(() -> familyMemberMapper.selectCount(null), taskExecutor);
+        CompletableFuture<Long> teacherFuture = CompletableFuture.supplyAsync(() -> userMapper.selectCount(new QueryWrapper<User>().eq("role", "teacher")), taskExecutor);
+        CompletableFuture<Long> pendingGuideFuture = CompletableFuture.supplyAsync(() -> userMapper.selectCount(new QueryWrapper<User>().eq("role", "teacher").eq("teacher_status", "pending")), taskExecutor);
+        CompletableFuture<Long> pendingPackageFuture = CompletableFuture.supplyAsync(() -> guidePackageMapper.selectCount(new QueryWrapper<com.etotem.cfc.entity.GuidePackage>().eq("status", "pending")), taskExecutor);
         CompletableFuture<List<Task>> recentTasksFuture = CompletableFuture.supplyAsync(() -> {
             QueryWrapper<Task> q = new QueryWrapper<Task>().orderByDesc("created_at").last("LIMIT 5");
             SortUtil.applySort(q);
             return taskMapper.selectList(q);
         }, taskExecutor);
-
         CompletableFuture<Long> pendingRefundFuture = CompletableFuture.supplyAsync(() ->
-            afterSalesRequestMapper.selectCount(new QueryWrapper<AfterSalesRequest>()
-                .eq("status", "pending")), taskExecutor);
-
+            afterSalesRequestMapper.selectCount(new QueryWrapper<AfterSalesRequest>().eq("status", "pending")), taskExecutor);
         try {
             data.put("totalFamilies", familyFuture.get());
             data.put("totalParents", parentFuture.get());
@@ -116,91 +90,33 @@ public class StatsController {
         } catch (Exception e) {
             return Result.error("获取仪表盘数据失败");
         }
-
         return Result.success(data);
     }
 
-    @Operation(summary = "孩子任务完成率")
-    @PostMapping("/child-completion")
-    public Result<Map<String, Object>> childCompletion(
-            @RequestAttribute("userId") Long userId,
-            @RequestBody Map<String, Object> params) {
-        User user = userMapper.selectById(userId);
-        if (user == null) return Result.error("用户不存在");
-
-        Long memberId = ParamUtils.getLong(params.get("memberId"));
-        Date startDate = parseDate(params.get("startDate"));
-        Date endDate = parseDate(params.get("endDate"));
-
-        Map<String, Object> stats = taskStatsService.getChildStats(memberId, startDate, endDate);
-        return Result.success(stats);
-    }
-
-    @Operation(summary = "家庭任务完成率")
-    @PostMapping("/family-completion")
-    public Result<Map<String, Object>> familyCompletion(
-            @RequestAttribute("userId") Long userId,
-            @RequestBody Map<String, Object> params) {
-        User user = userMapper.selectById(userId);
-        if (user == null) return Result.error("用户不存在");
-        if (user.getFamilyId() == null) return Result.noFamily("请先创建或加入家庭");
-
-        Date startDate = parseDate(params.get("startDate"));
-        Date endDate = parseDate(params.get("endDate"));
-
-        Map<String, Object> stats = taskStatsService.getFamilyStats(user.getFamilyId(), startDate, endDate);
-        return Result.success(stats);
-    }
-
-    private Date parseDate(Object value) {
-        if (value == null) return null;
-        try {
-            return new SimpleDateFormat("yyyy-MM-dd").parse(value.toString());
-        } catch (ParseException e) {
-            return null;
-        }
-    }
-
     @Operation(summary = "管理后台数据总览(新)")
     @PostMapping("/overview")
     public Result<Map<String, Object>> getOverview() {
         Map<String, Object> data = new HashMap<>();
         try {
-            CompletableFuture<Long> familyFuture = CompletableFuture.supplyAsync(() ->
-                familyMapper.selectCount(null), taskExecutor);
-            CompletableFuture<Long> parentFuture = CompletableFuture.supplyAsync(() ->
-                userMapper.selectCount(new QueryWrapper<User>().eq("role", "parent")), taskExecutor);
-            CompletableFuture<Long> childFuture = CompletableFuture.supplyAsync(() ->
-                familyMemberMapper.selectCount(null), taskExecutor);
-            CompletableFuture<Long> teacherFuture = CompletableFuture.supplyAsync(() ->
-                userMapper.selectCount(new QueryWrapper<User>().eq("role", "teacher")), taskExecutor);
-
+            CompletableFuture<Long> familyFuture = CompletableFuture.supplyAsync(() -> familyMapper.selectCount(null), taskExecutor);
+            CompletableFuture<Long> parentFuture = CompletableFuture.supplyAsync(() -> userMapper.selectCount(new QueryWrapper<User>().eq("role", "parent")), taskExecutor);
+            CompletableFuture<Long> childFuture = CompletableFuture.supplyAsync(() -> familyMemberMapper.selectCount(null), taskExecutor);
+            CompletableFuture<Long> teacherFuture = CompletableFuture.supplyAsync(() -> userMapper.selectCount(new QueryWrapper<User>().eq("role", "teacher")), taskExecutor);
             SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
             String monthStart = sdf.format(new Date()).substring(0, 7) + "-01";
             Date monthStartDate = sdf.parse(monthStart);
             CompletableFuture<Long> monthFamiliesFuture = CompletableFuture.supplyAsync(() ->
-                familyMapper.selectCount(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Family>()
-                    .ge(Family::getCreatedAt, monthStartDate)), taskExecutor);
-
+                familyMapper.selectCount(new LambdaQueryWrapper<Family>().ge(Family::getCreatedAt, monthStartDate)), taskExecutor);
             CompletableFuture<Long> monthParentsFuture = CompletableFuture.supplyAsync(() ->
-                userMapper.selectCount(new QueryWrapper<User>()
-                    .eq("role", "parent")
-                    .ge("created_at", monthStartDate)), taskExecutor);
-
+                userMapper.selectCount(new QueryWrapper<User>().eq("role", "parent").ge("created_at", monthStartDate)), taskExecutor);
             CompletableFuture<Long> pendingGuideFuture = CompletableFuture.supplyAsync(() ->
-                userMapper.selectCount(new QueryWrapper<User>()
-                    .eq("role", "teacher").eq("teacher_status", "pending")), taskExecutor);
-
+                userMapper.selectCount(new QueryWrapper<User>().eq("role", "teacher").eq("teacher_status", "pending")), taskExecutor);
             CompletableFuture<Long> pendingPackageFuture = CompletableFuture.supplyAsync(() ->
-                guidePackageMapper.selectCount(new QueryWrapper<com.etotem.cfc.entity.GuidePackage>()
-                    .eq("status", "pending")), taskExecutor);
-
+                guidePackageMapper.selectCount(new QueryWrapper<com.etotem.cfc.entity.GuidePackage>().eq("status", "pending")), taskExecutor);
             Calendar cal = Calendar.getInstance();
             cal.add(Calendar.DAY_OF_MONTH, -7);
             CompletableFuture<Long> activeUsersFuture = CompletableFuture.supplyAsync(() ->
-                userMapper.selectCount(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<User>()
-                    .ge(User::getUpdatedAt, cal.getTime())), taskExecutor);
-
+                userMapper.selectCount(new LambdaQueryWrapper<User>().ge(User::getUpdatedAt, cal.getTime())), taskExecutor);
             data.put("totalFamilies", familyFuture.get());
             data.put("totalParents", parentFuture.get());
             data.put("totalChildren", childFuture.get());
@@ -216,99 +132,88 @@ public class StatsController {
         return Result.success(data);
     }
 
+    // ── 任务完成率 ──
+
+    @Operation(summary = "孩子任务完成率")
+    @PostMapping("/child-completion")
+    public Result<Map<String, Object>> childCompletion(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, Object> params) {
+        User user = userMapper.selectById(userId);
+        if (user == null) return Result.error("用户不存在");
+        Long memberId = ParamUtils.getLong(params.get("memberId"));
+        Date startDate = parseDate(params.get("startDate"));
+        Date endDate = parseDate(params.get("endDate"));
+        return Result.success(taskStatsService.getChildStats(memberId, startDate, endDate));
+    }
+
+    @Operation(summary = "家庭任务完成率")
+    @PostMapping("/family-completion")
+    public Result<Map<String, Object>> familyCompletion(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, Object> params) {
+        User user = userMapper.selectById(userId);
+        if (user == null) return Result.error("用户不存在");
+        if (user.getFamilyId() == null) return Result.noFamily("请先创建或加入家庭");
+        Date startDate = parseDate(params.get("startDate"));
+        Date endDate = parseDate(params.get("endDate"));
+        return Result.success(taskStatsService.getFamilyStats(user.getFamilyId(), startDate, endDate));
+    }
+
+    // ── 收入统计 ──
+
     @Operation(summary = "收入统计")
     @PostMapping("/revenue")
     public Result<Map<String, Object>> getRevenueStats(@RequestBody Map<String, Object> params) {
         Map<String, Object> data = new HashMap<>();
         try {
             Calendar cal = Calendar.getInstance();
-            cal.set(Calendar.DAY_OF_MONTH, 1);
-            cal.set(Calendar.HOUR_OF_DAY, 0);
-            cal.set(Calendar.MINUTE, 0);
-            cal.set(Calendar.SECOND, 0);
+            cal.set(Calendar.DAY_OF_MONTH, 1); cal.set(Calendar.HOUR_OF_DAY, 0);
+            cal.set(Calendar.MINUTE, 0); cal.set(Calendar.SECOND, 0);
             Date monthStart = cal.getTime();
-            CompletableFuture<Double> monthCommissionFuture = CompletableFuture.supplyAsync(() -> {
-                try {
-                    List<CommissionRecord> records = commissionRecordMapper.selectList(
-                        new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<CommissionRecord>()
-                            .ge(CommissionRecord::getCreatedAt, monthStart)
-                            .eq(CommissionRecord::getStatus, "settled"));
-                    return records.stream()
-                        .filter(r -> r.getCommissionAmount() != null)
-                        .mapToDouble(CommissionRecord::getCommissionAmount)
-                        .sum();
-                } catch (Exception e) {
-                    return 0.0;
-                }
-            }, taskExecutor);
-
-            CompletableFuture<Double> totalCommissionFuture = CompletableFuture.supplyAsync(() -> {
-                try {
-                    List<CommissionRecord> records = commissionRecordMapper.selectList(
-                        new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<CommissionRecord>()
-                            .eq(CommissionRecord::getStatus, "settled"));
-                    return records.stream()
-                        .filter(r -> r.getCommissionAmount() != null)
-                        .mapToDouble(CommissionRecord::getCommissionAmount)
-                        .sum();
-                } catch (Exception e) {
-                    return 0.0;
-                }
-            }, taskExecutor);
-
-            CompletableFuture<Double> pendingCommissionFuture = CompletableFuture.supplyAsync(() -> {
-                try {
-                    List<CommissionRecord> records = commissionRecordMapper.selectList(
-                        new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<CommissionRecord>()
-                            .eq(CommissionRecord::getStatus, "pending"));
-                    return records.stream()
-                        .filter(r -> r.getCommissionAmount() != null)
-                        .mapToDouble(CommissionRecord::getCommissionAmount)
-                        .sum();
-                } catch (Exception e) {
-                    return 0.0;
-                }
-            }, taskExecutor);
-
-            data.put("monthCommission", monthCommissionFuture.get() / 100.0);
-            data.put("totalCommission", totalCommissionFuture.get() / 100.0);
-            data.put("pendingCommission", pendingCommissionFuture.get() / 100.0);
+            CompletableFuture<Double> monthFuture = CompletableFuture.supplyAsync(() -> sumCommission(commissionRecordMapper, monthStart, true), taskExecutor);
+            CompletableFuture<Double> totalFuture = CompletableFuture.supplyAsync(() -> sumCommission(commissionRecordMapper, null, true), taskExecutor);
+            CompletableFuture<Double> pendingFuture = CompletableFuture.supplyAsync(() -> sumCommission(commissionRecordMapper, null, false), taskExecutor);
+            data.put("monthCommission", monthFuture.get() / 100.0);
+            data.put("totalCommission", totalFuture.get() / 100.0);
+            data.put("pendingCommission", pendingFuture.get() / 100.0);
         } catch (Exception e) {
-            data.put("monthCommission", 0.0);
-            data.put("totalCommission", 0.0);
-            data.put("pendingCommission", 0.0);
+            data.put("monthCommission", 0.0); data.put("totalCommission", 0.0); data.put("pendingCommission", 0.0);
         }
         return Result.success(data);
     }
 
+    private double sumCommission(CommissionRecordMapper mapper, Date since, boolean settled) {
+        try {
+            LambdaQueryWrapper<CommissionRecord> q = new LambdaQueryWrapper<CommissionRecord>();
+            if (since != null) q.ge(CommissionRecord::getCreatedAt, since);
+            if (settled) q.eq(CommissionRecord::getStatus, "settled"); else q.eq(CommissionRecord::getStatus, "pending");
+            return mapper.selectList(q).stream()
+                .filter(r -> r.getCommissionAmount() != null)
+                .mapToDouble(CommissionRecord::getCommissionAmount).sum();
+        } catch (Exception e) { return 0.0; }
+    }
+
+    // ── 会员分布 ──
+
     @Operation(summary = "会员分布统计")
     @PostMapping("/membership")
     public Result<Map<String, Object>> getMembershipStats() {
         Map<String, Object> data = new HashMap<>();
         try {
             CompletableFuture<Long> freeFuture = CompletableFuture.supplyAsync(() ->
-                userMapper.selectCount(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<User>()
-                    .or().eq(User::getMemberLevel, "FREE").isNull(User::getMemberLevel)), taskExecutor);
+                userMapper.selectCount(new LambdaQueryWrapper<User>().or().eq(User::getMemberLevel, "FREE").isNull(User::getMemberLevel)), taskExecutor);
             CompletableFuture<Long> familyFuture = CompletableFuture.supplyAsync(() ->
-                userMapper.selectCount(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<User>()
-                    .eq(User::getMemberLevel, "FAMILY")), taskExecutor);
+                userMapper.selectCount(new LambdaQueryWrapper<User>().eq(User::getMemberLevel, "FAMILY")), taskExecutor);
             CompletableFuture<Long> providerFuture = CompletableFuture.supplyAsync(() ->
-                userMapper.selectCount(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<User>()
-                    .eq(User::getMemberLevel, "PROVIDER")), taskExecutor);
-
+                userMapper.selectCount(new LambdaQueryWrapper<User>().eq(User::getMemberLevel, "PROVIDER")), taskExecutor);
             CompletableFuture<Long> trialFuture = CompletableFuture.supplyAsync(() ->
-                trialMembershipMapper.selectCount(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TrialMembership>()
-                    .eq(TrialMembership::getStatus, "ACTIVE")), taskExecutor);
+                trialMembershipMapper.selectCount(new LambdaQueryWrapper<TrialMembership>().eq(TrialMembership::getStatus, "ACTIVE")), taskExecutor);
             Calendar cal = Calendar.getInstance();
-            cal.set(Calendar.DAY_OF_MONTH, 1);
-            cal.set(Calendar.HOUR_OF_DAY, 0);
-            cal.set(Calendar.MINUTE, 0);
-            cal.set(Calendar.SECOND, 0);
+            cal.set(Calendar.DAY_OF_MONTH, 1); cal.set(Calendar.HOUR_OF_DAY, 0); cal.set(Calendar.MINUTE, 0); cal.set(Calendar.SECOND, 0);
             CompletableFuture<Long> monthUpgradesFuture = CompletableFuture.supplyAsync(() ->
-                memberUpgradeRecordMapper.selectCount(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<MemberUpgradeRecord>()
-                    .eq(MemberUpgradeRecord::getToLevel, "FAMILY")
-                    .ge(MemberUpgradeRecord::getCreatedAt, cal.getTime())), taskExecutor);
-
+                memberUpgradeRecordMapper.selectCount(new LambdaQueryWrapper<MemberUpgradeRecord>()
+                    .eq(MemberUpgradeRecord::getToLevel, "FAMILY").ge(MemberUpgradeRecord::getCreatedAt, cal.getTime())), taskExecutor);
             data.put("freeCount", freeFuture.get());
             data.put("familyCount", familyFuture.get());
             data.put("providerCount", providerFuture.get());
@@ -320,6 +225,8 @@ public class StatsController {
         return Result.success(data);
     }
 
+    // ── 每日趋势 ──
+
     @Operation(summary = "每日数据趋势(近30天)")
     @PostMapping("/trend")
     public Result<Map<String, Object>> getTrend(@RequestBody Map<String, Object> params) {
@@ -328,9 +235,7 @@ public class StatsController {
             Calendar cal = Calendar.getInstance();
             cal.add(Calendar.DAY_OF_MONTH, -30);
             Date thirtyDaysAgo = cal.getTime();
-
-            List<Family> families = familyMapper.selectList(
-                new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Family>().ge(Family::getCreatedAt, thirtyDaysAgo));
+            List<Family> families = familyMapper.selectList(new LambdaQueryWrapper<Family>().ge(Family::getCreatedAt, thirtyDaysAgo));
             Map<String, Long> familyTrend = new LinkedHashMap<>();
             SimpleDateFormat dayFmt = new SimpleDateFormat("MM-dd");
             for (Family f : families) {
@@ -340,9 +245,7 @@ public class StatsController {
                 }
             }
             data.put("familyTrend", familyTrend);
-
-            List<User> users = userMapper.selectList(
-                new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<User>().ge(User::getCreatedAt, thirtyDaysAgo));
+            List<User> users = userMapper.selectList(new LambdaQueryWrapper<User>().ge(User::getCreatedAt, thirtyDaysAgo));
             Map<String, Long> userTrend = new LinkedHashMap<>();
             for (User u : users) {
                 if (u.getCreatedAt() != null) {
@@ -357,4 +260,131 @@ public class StatsController {
         }
         return Result.success(data);
     }
+
+    // ── 孩子数据概览 ──
+
+    @Operation(summary = "孩子数据概览")
+    @PostMapping("/child/overview")
+    public Result<Map<String, Object>> childOverview(@RequestBody Map<String, Object> body) {
+        Long memberId = ParamUtils.getLong(body.get("memberId"));
+        Map<String, Object> result = new HashMap<>();
+        FamilyMember child = familyMemberMapper.selectById(memberId);
+        result.put("totalPoints", child != null ? child.getTotalPoints() : 0);
+
+        Calendar monthCal = Calendar.getInstance();
+        monthCal.set(Calendar.DAY_OF_MONTH, 1); monthCal.set(Calendar.HOUR_OF_DAY, 0);
+        monthCal.set(Calendar.MINUTE, 0); monthCal.set(Calendar.SECOND, 0); monthCal.set(Calendar.MILLISECOND, 0);
+        Date monthStart = monthCal.getTime();
+        List<PointsLog> monthLogs = pointsLogMapper.selectList(
+                new LambdaQueryWrapper<PointsLog>().eq(PointsLog::getChildId, memberId)
+                        .ge(PointsLog::getCreatedAt, monthStart).orderByAsc(PointsLog::getCreatedAt));
+        Set<String> activeDays = new HashSet<>();
+        for (PointsLog log : monthLogs) {
+            if (log.getCreatedAt() != null) activeDays.add(new SimpleDateFormat("yyyy-MM-dd").format(log.getCreatedAt()));
+        }
+        result.put("monthActiveDays", activeDays.size());
+
+        List<Task> tasks = taskService.getAllTasksByChild(memberId);
+        long taskTotal = tasks.size();
+        long taskCompleted = tasks.stream().filter(t -> "completed".equals(t.getStatus())).count();
+        long taskPending = tasks.stream().filter(t -> "pending".equals(t.getStatus())).count();
+        long taskInReview = tasks.stream().filter(t -> "review".equals(t.getStatus())).count();
+        Map<String, Object> taskStats = new HashMap<>();
+        taskStats.put("total", taskTotal); taskStats.put("completed", taskCompleted);
+        taskStats.put("pending", taskPending); taskStats.put("inReview", taskInReview);
+        taskStats.put("completionRate", taskTotal > 0 ? (int)(taskCompleted * 10000 / taskTotal) / 100 : 0);
+        result.put("taskStats", taskStats);
+
+        List<GameRecord> records = gameRecordService.listByChildId(memberId);
+        int gameTotalPoints = records.stream().mapToInt(GameRecord::getPointsEarned).sum();
+        int bestScore = records.stream().mapToInt(GameRecord::getScore).max().orElse(0);
+        int totalTime = records.stream().mapToInt(GameRecord::getCompletionTime).sum();
+        Map<String, Object> gameStats = new HashMap<>();
+        gameStats.put("total", records.size()); gameStats.put("totalPoints", gameTotalPoints);
+        gameStats.put("bestScore", bestScore); gameStats.put("totalTime", totalTime);
+        result.put("gameStats", gameStats);
+
+        Calendar cal = Calendar.getInstance();
+        cal.add(Calendar.DAY_OF_YEAR, -7);
+        List<PointsLog> recentLogs = pointsLogMapper.selectList(
+                new LambdaQueryWrapper<PointsLog>().eq(PointsLog::getChildId, memberId)
+                        .ge(PointsLog::getCreatedAt, cal.getTime()).orderByAsc(PointsLog::getCreatedAt));
+        List<Map<String, Object>> pointsTrend = new ArrayList<>();
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+        for (int i = 6; i >= 0; i--) {
+            Calendar dayCal = Calendar.getInstance();
+            dayCal.add(Calendar.DAY_OF_YEAR, -i);
+            dayCal.set(Calendar.HOUR_OF_DAY, 0); dayCal.set(Calendar.MINUTE, 0);
+            dayCal.set(Calendar.SECOND, 0); dayCal.set(Calendar.MILLISECOND, 0);
+            Date dayStart = dayCal.getTime();
+            dayCal.set(Calendar.HOUR_OF_DAY, 23); dayCal.set(Calendar.MINUTE, 59); dayCal.set(Calendar.SECOND, 59);
+            Date dayEnd = dayCal.getTime();
+            int dayPoints = recentLogs.stream()
+                    .filter(l -> l.getCreatedAt() != null && l.getCreatedAt().after(dayStart) && l.getCreatedAt().before(dayEnd))
+                    .mapToInt(PointsLog::getAmount).sum();
+            Map<String, Object> point = new HashMap<>();
+            point.put("date", sdf.format(dayStart)); point.put("points", dayPoints);
+            pointsTrend.add(point);
+        }
+        result.put("pointsTrend", pointsTrend);
+        return Result.success(result);
+    }
+
+    // ── 家庭汇总 ──
+
+    @Operation(summary = "家庭汇总统计")
+    @PostMapping("/family/summary")
+    public Result<Map<String, Object>> familySummary(@RequestBody Map<String, Object> body,
+                                                     @RequestAttribute(value = "familyId", required = false) Long familyId) {
+        if (familyId == null) return Result.noFamily("请先创建或加入家庭");
+        List<FamilyMember> children = familyMemberMapper.selectList(
+                new LambdaQueryWrapper<FamilyMember>().eq(FamilyMember::getFamilyId, familyId));
+        List<Map<String, Object>> childrenStats = new ArrayList<>();
+        int familyTotalTasks = 0, familyCompletedTasks = 0, familyTotalGames = 0;
+        for (FamilyMember child : children) {
+            Map<String, Object> s = new HashMap<>();
+            s.put("memberId", child.getId()); s.put("childName", child.getNickname());
+            List<Task> tasks = taskService.getAllTasksByChild(child.getId());
+            long c = tasks.stream().filter(t -> "completed".equals(t.getStatus())).count();
+            s.put("taskTotal", tasks.size()); s.put("taskCompleted", c); s.put("points", child.getTotalPoints());
+            s.put("gameTotal", gameRecordService.listByChildId(child.getId()).size());
+            familyTotalTasks += tasks.size(); familyCompletedTasks += c;
+            familyTotalGames += ((Number)s.get("gameTotal")).intValue();
+            childrenStats.add(s);
+        }
+        Map<String, Object> result = new HashMap<>();
+        result.put("children", childrenStats);
+        result.put("familyTotalTasks", familyTotalTasks);
+        result.put("familyCompletedTasks", familyCompletedTasks);
+        result.put("familyCompletionRate", familyTotalTasks > 0 ? (int)(familyCompletedTasks * 10000 / familyTotalTasks) / 100 : 0);
+        result.put("familyTotalGames", familyTotalGames);
+        result.put("childCount", children.size());
+        return Result.success(result);
+    }
+
+    // ── 积分排行 ──
+
+    @Operation(summary = "积分排行榜(家庭内)")
+    @PostMapping("/points/ranking")
+    public Result<List<Map<String, Object>>> pointsRanking(@RequestBody Map<String, Object> body,
+                                                           @RequestAttribute(value = "familyId", required = false) Long familyId) {
+        if (familyId == null) return Result.noFamily("请先创建或加入家庭");
+        List<FamilyMember> children = familyMemberMapper.selectList(
+                new LambdaQueryWrapper<FamilyMember>().eq(FamilyMember::getFamilyId, familyId).orderByDesc(FamilyMember::getTotalPoints));
+        List<Map<String, Object>> ranking = new ArrayList<>();
+        int rank = 1;
+        for (FamilyMember child : children) {
+            Map<String, Object> item = new HashMap<>();
+            item.put("rank", rank++); item.put("memberId", child.getId());
+            item.put("childName", child.getNickname()); item.put("points", child.getTotalPoints());
+            ranking.add(item);
+        }
+        return Result.success(ranking);
+    }
+
+    private Date parseDate(Object value) {
+        if (value == null) return null;
+        try { return new SimpleDateFormat("yyyy-MM-dd").parse(value.toString()); }
+        catch (ParseException e) { return null; }
+    }
 }