一、统计端点的统一架构

AI-HEALTH系统为五大健康模块设计了统一的统计端点,所有Summary服务遵循相同的设计规范,形成可扩展的统计体系。

RESTful API设计

所有统计控制器采用一致的API规范:

package com.aihealth.controller;

import com.aihealth.service.FoodRecordSummaryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.ComponentScan;

import java.util.Map;

@RestController
@RequestMapping("/api/v1/food-record/summary")
@ComponentScan(basePackages = {"controller", "service"})
public class FoodRecordSummaryController {

    private static final Logger logger = LoggerFactory.getLogger(FoodRecordSummaryController.class);

    @Autowired
    private FoodRecordSummaryService foodRecordSummaryService;

    @GetMapping
    public Map<String, Object> getFoodRecordSummary(
            @RequestParam("username") String username,
            @RequestParam("startDate") String startDate,
            @RequestParam("endDate") String endDate) {
        logger.info("Received request for food record summary: username={}, startDate={}, endDate={}", username, startDate, endDate);
        try {
            Map<String, Object> result = foodRecordSummaryService.getFoodRecordSummary(username, startDate, endDate);
            logger.info("Successfully returned food record summary: {}", result);
            return result;
        } catch (Exception e) {
            logger.error("Error processing food record summary request", e);
            throw new RuntimeException("处理饮食记录总结时出错: " + e.getMessage(), e);
        }
    }
}

MoodRecordSummaryController采用完全相同的结构:

@RestController
@RequestMapping("/api/v1/mood/summary")
@ComponentScan(basePackages = {"controller", "service"})
public class MoodRecordSummaryController {

    private static final Logger logger = LoggerFactory.getLogger(MoodRecordSummaryController.class);

    @Autowired
    private MoodRecordSummaryService moodRecordSummaryService;

    @GetMapping
    public Map<String, Object> getMoodRecordSummary(
            @RequestParam("username") String username,
            @RequestParam("startDate") String startDate,
            @RequestParam("endDate") String endDate) {
        logger.info("Received request for mood record summary: username={}, startDate={}, endDate={}", username, startDate, endDate);
        try {
            Map<String, Object> result = moodRecordSummaryService.getMoodRecordSummary(username, startDate, endDate);
            logger.info("Successfully returned mood record summary: {}", result);
            return result;
        } catch (Exception e) {
            logger.error("Error processing mood record summary request", e);
            throw new RuntimeException("处理情绪记录总结时出错: " + e.getMessage(), e);
        }
    }
}

接口规范

属性
路由模式 /api/v1/{module}/summary
请求方法 GET
参数 username, startDate, endDate
返回格式 Map<String, Object>

二、时间序列数据的构建

趋势分析的核心在于将原始记录转化为按时间排序的数据序列。

日期分组与排序

所有SummaryServiceImpl遵循相同的分组模式:

package com.aihealth.service.impl;

import com.aihealth.entity.FoodRecord;
import com.aihealth.entity.FoodItem;
import com.aihealth.service.FoodRecordService;
import com.aihealth.service.FoodRecordSummaryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
public class FoodRecordSummaryServiceImpl implements FoodRecordSummaryService {

    @Autowired
    private FoodRecordService foodRecordService;

    @Override
    public Map<String, Object> getFoodRecordSummary(String username, String startDate, String endDate) {
        // 获取日期范围内的所有饮食记录
        List<FoodRecord> records = foodRecordService.getFoodRecordsByUsernameAndDateRange(username, startDate, endDate);
        
        // 统计信息
        int totalRecords = records != null ? records.size() : 0;
        int totalCalories = 0;
        List<Map<String, Object>> dailyRecords = new ArrayList<>();
        
        if (records != null && !records.isEmpty()) {
            // 按日期分组统计
            Map<String, List<FoodRecord>> recordsByDate = new HashMap<>();
            for (FoodRecord record : records) {
                if (record == null) {
                    continue;
                }
                // 提取日期部分(YYYY-MM-DD)
                if (record.getRecordTime() != null) {
                    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
                    String date = record.getRecordTime().format(formatter);
                    if (!recordsByDate.containsKey(date)) {
                        recordsByDate.put(date, new ArrayList<>());
                    }
                    recordsByDate.get(date).add(record);
                }
            }
            
            // 处理每天的记录 - 按日期排序
            List<String> sortedDates = new ArrayList<>(recordsByDate.keySet());
            sortedDates.sort(String::compareTo);
            
            for (String date : sortedDates) {
                List<FoodRecord> dayRecords = recordsByDate.get(date);
                
                Map<String, Object> daySummary = new HashMap<>();
                daySummary.put("date", date);
                daySummary.put("recordCount", dayRecords.size());
                
                List<Map<String, Object>> meals = new ArrayList<>();
                int dayCalories = 0;
                
                for (FoodRecord record : dayRecords) {
                    Map<String, Object> meal = new HashMap<>();
                    meal.put("mealType", record.getMealType() != null ? record.getMealType() : "未知");
                    meal.put("recordTime", record.getRecordTime() != null ? record.getRecordTime() : "");
                    
                    // 提取食物项
                    List<Map<String, Object>> foodItems = new ArrayList<>();
                    if (record.getFoodItems() != null) {
                        for (FoodItem item : record.getFoodItems()) {
                            if (item == null) continue;
                            Map<String, Object> foodItem = new HashMap<>();
                            foodItem.put("name", item.getName() != null ? item.getName() : "未知");
                            foodItem.put("amount", item.getAmount() != null ? item.getAmount() : "");
                            foodItems.add(foodItem);
                        }
                    }
                    meal.put("foodItems", foodItems);
                    
                    // 计算卡路里
                    int mealCalories = 0;
                    if (record.getCalories() != null && !record.getCalories().isEmpty()) {
                        try {
                            mealCalories = Integer.parseInt(record.getCalories());
                        } catch (NumberFormatException e) {
                            // 忽略无效的卡路里值
                        }
                    }
                    meal.put("calories", mealCalories);
                    meals.add(meal);
                    
                    dayCalories += mealCalories;
                    totalCalories += mealCalories;
                }
                
                daySummary.put("meals", meals);
                daySummary.put("totalCalories", dayCalories);
                dailyRecords.add(daySummary);
            }
        }
        
        // 构建返回结果
        Map<String, Object> summary = new HashMap<>();
        summary.put("totalRecords", totalRecords);
        summary.put("totalCalories", totalCalories);
        summary.put("dailyRecords", dailyRecords);
        
        return summary;
    }
}

sortedDates数组即为时间序列,按日期升序排列,每个元素对应一天的数据点。


三、趋势指标的计算

1. 饮食记录趋势 - 卡路里摄入变化

返回结果中dailyRecords数组按日期顺序排列,连成每日的卡路里摄入趋势线:

Map<String, Object> summary = new HashMap<>();
summary.put("totalRecords", totalRecords);
summary.put("totalCalories", totalCalories);
summary.put("dailyRecords", dailyRecords);

return summary;

2. 睡眠记录趋势 - 时长与质量变化

package com.aihealth.service.impl;

import com.aihealth.entity.SleepRecord;
import com.aihealth.service.SleepRecordService;
import com.aihealth.service.SleepRecordSummaryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
public class SleepRecordSummaryServiceImpl implements SleepRecordSummaryService {

    @Autowired
    private SleepRecordService sleepRecordService;

    @Override
    public Map<String, Object> getSleepRecordSummary(String username, String startDate, String endDate) {
        List<SleepRecord> records = sleepRecordService.getSleepRecordsByUsernameAndDateRange(username, startDate, endDate);

        int totalRecords = records != null ? records.size() : 0;
        double totalDuration = 0;
        Map<String, Integer> qualityCountMap = new HashMap<>();
        List<Map<String, Object>> dailyRecords = new ArrayList<>();

        Map<String, List<SleepRecord>> recordsByDate = new HashMap<>();
        if (records != null) {
            for (SleepRecord record : records) {
                if (record != null && record.getSleepTime() != null) {
                    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
                    String date = record.getSleepTime().format(formatter);
                    if (!recordsByDate.containsKey(date)) {
                        recordsByDate.put(date, new ArrayList<>());
                    }
                    recordsByDate.get(date).add(record);
                }
                
                if (record != null && record.getSleepQuality() != null && !record.getSleepQuality().isEmpty()) {
                    String quality = record.getSleepQuality();
                    qualityCountMap.put(quality, qualityCountMap.getOrDefault(quality, 0) + 1);
                }
            }
        }

        List<String> sortedDates = new ArrayList<>(recordsByDate.keySet());
        sortedDates.sort(String::compareTo);
        
        for (String date : sortedDates) {
            List<SleepRecord> dayRecords = recordsByDate.get(date);

            Map<String, Object> daySummary = new HashMap<>();
            daySummary.put("date", date);
            daySummary.put("recordCount", dayRecords.size());

            List<Map<String, Object>> sleeps = new ArrayList<>();
            double dayDuration = 0;
            Map<String, Integer> dayQualityCountMap = new HashMap<>();

            for (SleepRecord record : dayRecords) {
                Map<String, Object> sleep = new HashMap<>();
                sleep.put("recordName", record.getRecordName());
                sleep.put("sleepTime", record.getSleepTime());
                sleep.put("wakeTime", record.getWakeTime());
                sleep.put("duration", record.getDuration());
                sleep.put("sleepQuality", record.getSleepQuality());
                sleeps.add(sleep);

                if (record.getDuration() != null && !record.getDuration().isEmpty()) {
                    try {
                        double duration = Double.parseDouble(record.getDuration());
                        dayDuration += duration;
                        totalDuration += duration;
                    } catch (NumberFormatException e) {
                        // 忽略无效的时长值
                    }
                }
                
                if (record.getSleepQuality() != null && !record.getSleepQuality().isEmpty()) {
                    String quality = record.getSleepQuality();
                    dayQualityCountMap.put(quality, dayQualityCountMap.getOrDefault(quality, 0) + 1);
                }
            }

            daySummary.put("sleeps", sleeps);
            daySummary.put("totalDuration", dayDuration);
            daySummary.put("qualityCount", dayQualityCountMap);
            dailyRecords.add(daySummary);
        }

        double avgDuration = totalRecords > 0 ? totalDuration / totalRecords : 0;
        
        Map<String, Object> summary = new HashMap<>();
        summary.put("totalRecords", totalRecords);
        summary.put("totalDuration", totalDuration);
        summary.put("avgDuration", avgDuration);
        summary.put("qualityCount", qualityCountMap);
        summary.put("dailyRecords", dailyRecords);

        return summary;
    }
}

四、趋势分析在各模块的应用

模块 时间序列字段 核心趋势指标 特有统计
饮食 recordTime dayCalories 餐次分组、食物项提取
运动 recordTime dayCalories 运动项目明细
睡眠 sleepTime avgDuration qualityCount分布
体重 recordDate avgWeight min/max体重
情绪 recordTime moodCount 最常见情绪

五、设计总结

原始记录 → 日期分组 → sortedDates排序 → 每日聚合指标 → 时间序列趋势
                                                    ↓
                                           ┌─────────────────┐
                                           │  dailyRecords   │
                                           │  按日期排列     │
                                           │  形成趋势线     │
                                           └─────────────────┘

通过统一的Controller规范、一致的Service模式,AI-HEALTH实现了健康数据的高效统计与趋势分析。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐