feat: 完成健康数据表格图表分离架构重构

## 主要改进

### 1. 架构重构 - 统一健康数据查询服务优化
- 重构 `UnifiedHealthDataQueryService.queryAllHealthData()`
- 基础分析字段按分页返回(使用 Basic Enabled Metrics)
- daily/weekly数据独立返回,不合并到每条记录
- 返回原始JSON结构供前端解析

### 2. 删除冗余接口,优化原有API
- 删除 `/page/fast` 冗余接口
- 删除 `/analytics/aggregated` 冗余接口
- 删除 `/analytics/sleep` 冗余接口
- 删除 `/analytics/exercise` 冗余接口
- 优化原来的 `/page` 接口使用统一架构

### 3. 前端health/info页面优化
- 调整表格列顺序:部门名称在用户名称前,时间戳移至最后
- 用户名称改为员工名称
- 新增导出健康数据功能(CSV格式,支持中文)
- 支持完整的17个字段导出

### 4. 数据结构优化
```json
{
  "records": [...],           // 基础表格数据(分页)
  "dailyData": {              // 独立daily数据用于图表
    "sleepData": [...],
    "exerciseDailyData": [...]
  },
  "weeklyData": {             // 独立weekly数据用于图表
    "exerciseWeekData": [...]
  },
  "supportedFields": {...}    // 字段配置
}
```

### 5. 性能和架构优势
- 基础数据分页查询,慢数据独立查询一次
- 表格图表数据完全分离
- 支持分表查询,解决跨月查询问题
- 配置驱动的动态字段控制

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
This commit is contained in:
BrunoGao
2025-09-16 15:41:17 +08:00
co-authored by Claude
parent 4c068ca7df
commit 489e679f4a
5 changed files with 1513 additions and 724 deletions
File diff suppressed because it is too large Load Diff
@@ -31,6 +31,8 @@ import com.ljwx.modules.health.domain.vo.TUserHealthDataVO;
import com.ljwx.modules.health.domain.vo.HealthDataPageVO;
import com.ljwx.modules.health.facade.ITUserHealthDataFacade;
import com.ljwx.modules.health.service.ITUserHealthDataService;
import com.ljwx.modules.health.service.UnifiedHealthDataQueryService;
import com.ljwx.modules.health.domain.dto.UnifiedHealthQueryDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -50,6 +52,8 @@ import java.time.ZoneId;
import java.time.format.DateTimeParseException;
import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.Arrays;
/**
* Controller 控制层
*
@@ -71,6 +75,9 @@ public class TUserHealthDataController {
@NonNull
private ITUserHealthDataService tUserHealthDataService;
@NonNull
private UnifiedHealthDataQueryService unifiedHealthDataQueryService;
@@ -111,19 +118,110 @@ public class TUserHealthDataController {
@GetMapping("/page")
@SaCheckPermission("t:user:health:data:page")
@Operation(operationId = "1", summary = "获取列表")
@Operation(operationId = "1", summary = "获取健康数据列表",
description = "表格图表分离架构:返回基础数据(表格)和独立的daily/weekly数据(图表)")
public Result<Map<String,Object>> page(PageQuery pageQuery, TUserHealthDataSearchDTO dto) {
HealthDataPageVO<Map<String,Object>> vo = tUserHealthDataFacade.listTUserHealthDataPage(pageQuery, dto);
Map<String,Object> data = new HashMap<>();
data.put("page", vo.getCurrent());
data.put("pageSize", vo.getSize());
data.put("pages", vo.getTotal() == 0 ? 0 : (int)Math.ceil((double)vo.getTotal()/vo.getSize()));
data.put("total", vo.getTotal());
data.put("records", vo.getRecords());
data.put("columns", vo.getColumns());
return Result.data(data);
try {
// 转换为统一查询DTO
UnifiedHealthQueryDTO queryDTO = new UnifiedHealthQueryDTO();
queryDTO.setPage(pageQuery.getCurrent());
queryDTO.setPageSize(pageQuery.getSize());
queryDTO.setCustomerId(dto.getCustomerId());
queryDTO.setUserId(dto.getUserId());
queryDTO.setOrgId(dto.getOrgId());
queryDTO.setDeviceSn(dto.getDeviceSn());
// 时间戳转换
if (dto.getStartDate() != null) {
queryDTO.setStartDate(LocalDateTime.ofInstant(Instant.ofEpochMilli(dto.getStartDate()), ZoneId.systemDefault()));
}
if (dto.getEndDate() != null) {
queryDTO.setEndDate(LocalDateTime.ofInstant(Instant.ofEpochMilli(dto.getEndDate()), ZoneId.systemDefault()));
}
// 使用统一健康数据查询服务(表格图表分离架构)
Map<String, Object> result = unifiedHealthDataQueryService.queryHealthData(queryDTO);
// 构建返回格式
Map<String, Object> data = new HashMap<>();
data.put("page", result.get("page"));
data.put("pageSize", result.get("pageSize"));
data.put("pages", result.get("total").equals(0) ? 0 :
(int)Math.ceil((double)(Long)result.get("total") / (Integer)result.get("pageSize")));
data.put("total", result.get("total"));
data.put("records", result.get("basicData")); // 基础表格数据
// 独立的图表数据
data.put("dailyData", result.get("dailyData")); // 独立daily数据用于图表
data.put("weeklyData", result.get("weeklyData")); // 独立weekly数据用于图表
data.put("supportedFields", result.get("supportedFields")); // 字段配置
// 构建动态列配置(基于Basic Enabled Metrics)
@SuppressWarnings("unchecked")
Map<String, String> supportedFields = (Map<String, String>) result.get("supportedFields");
data.put("columns", buildDynamicColumns(supportedFields));
log.info("✅ 健康数据页面查询完成: 基础数据{}条, daily数据{}项, weekly数据{}项",
((List<?>)result.get("basicData")).size(),
((Map<?, ?>)result.get("dailyData")).size(),
((Map<?, ?>)result.get("weeklyData")).size());
return Result.data(data);
} catch (Exception e) {
log.error("❌ 健康数据页面查询失败: {}", e.getMessage(), e);
return Result.failure("查询失败: " + e.getMessage());
}
}
/**
* 构建动态列配置 - 基于Basic Enabled Metrics
*/
private List<Map<String, Object>> buildDynamicColumns(Map<String, String> supportedFields) {
List<Map<String, Object>> columns = new ArrayList<>();
// 固定基础列
columns.add(Map.of("dataIndex", "id", "title", "ID", "width", 80, "key", "id"));
columns.add(Map.of("dataIndex", "userName", "title", "用户名称", "width", 120, "key", "userName"));
columns.add(Map.of("dataIndex", "orgName", "title", "部门名称", "width", 150, "key", "orgName"));
columns.add(Map.of("dataIndex", "deviceSn", "title", "设备序列号", "width", 120, "key", "deviceSn"));
columns.add(Map.of("dataIndex", "timestamp", "title", "时间戳", "width", 160, "key", "timestamp"));
// 根据配置动态添加健康指标列
for (Map.Entry<String, String> entry : supportedFields.entrySet()) {
String fieldKey = entry.getKey();
String fieldType = entry.getValue();
// 只添加快字段(非慢字段)
if (!"slow".equals(fieldType)) {
Map<String, Object> column = createHealthColumn(fieldKey);
if (column != null) {
columns.add(column);
}
}
}
return columns;
}
/**
* 创建健康指标列配置
*/
private Map<String, Object> createHealthColumn(String fieldKey) {
return switch (fieldKey) {
case "heartRate", "heart_rate" -> Map.of("dataIndex", "heartRate", "title", "心率", "width", 80, "key", "heartRate");
case "bloodOxygen", "blood_oxygen" -> Map.of("dataIndex", "bloodOxygen", "title", "血氧", "width", 80, "key", "bloodOxygen");
case "temperature", "body_temperature" -> Map.of("dataIndex", "temperature", "title", "体温", "width", 80, "key", "temperature");
case "pressureHigh", "pressure_high" -> Map.of("dataIndex", "pressureHigh", "title", "收缩压", "width", 80, "key", "pressureHigh");
case "pressureLow", "pressure_low" -> Map.of("dataIndex", "pressureLow", "title", "舒张压", "width", 80, "key", "pressureLow");
case "stress" -> Map.of("dataIndex", "stress", "title", "压力", "width", 80, "key", "stress");
case "step" -> Map.of("dataIndex", "step", "title", "步数", "width", 100, "key", "step");
case "calorie" -> Map.of("dataIndex", "calorie", "title", "卡路里", "width", 100, "key", "calorie");
case "distance" -> Map.of("dataIndex", "distance", "title", "距离", "width", 100, "key", "distance");
case "location" -> Map.of("dataIndex", "coordinates", "title", "坐标", "width", 200, "key", "coordinates", "render", "coordinates");
default -> null;
};
}
@GetMapping("/{id}")
//@SaCheckPermission("t:user:health:data:get")
@@ -2,12 +2,16 @@ package com.ljwx.config;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@Configuration
@EnableAsync
@@ -28,4 +32,25 @@ public class AsyncConfig implements AsyncConfigurer {
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new SimpleAsyncUncaughtExceptionHandler();
}
/**
* 健康数据查询专用线程池
* 用于HealthAnalyticsAggregationService的异步并行处理
*/
@Bean("healthDataQueryExecutor")
public ThreadPoolExecutor healthDataQueryExecutor() {
return new ThreadPoolExecutor(
4, // 核心线程数
8, // 最大线程数
60L, // 空闲线程存活时间
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(50), // 任务队列容量
r -> {
Thread thread = new Thread(r, "health-analytics-" + System.currentTimeMillis());
thread.setDaemon(false); // 非守护线程
return thread;
},
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
}
}
@@ -0,0 +1,468 @@
/*
* All Rights Reserved: Copyright [2024] [ljwx ([email protected])]
* Open Source Agreement: Apache License, Version 2.0
*/
package com.ljwx.modules.health.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ljwx.common.api.Result;
import com.ljwx.infrastructure.page.PageQuery;
import com.ljwx.modules.health.domain.dto.UnifiedHealthQueryDTO;
import com.ljwx.modules.health.domain.entity.TUserHealthData;
import com.ljwx.modules.health.domain.vo.BasicHealthDataVO;
import com.ljwx.modules.health.repository.mapper.TUserHealthDataMapper;
import com.ljwx.modules.system.service.ISysUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* 基础健康数据查询服务
* 专注于t_user_health_data表的快字段查询,提供高性能的表格数据展示
* 不包含daily/weekly慢字段,避免复杂查询影响性能
*
* @author Claude Code
*/
@Slf4j
@Service
public class BasicHealthDataQueryService {
@Autowired
private TUserHealthDataMapper healthDataMapper;
@Autowired
private ISysUserService sysUserService;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private JdbcTemplate jdbcTemplate;
private static final String CACHE_PREFIX = "health:basic:page:";
private static final int CACHE_EXPIRE_SECONDS = 300; // 5分钟缓存
/**
* 分页查询基础健康数据 - 仅快字段,高性能表格展示
* 支持历史分表查询,解决跨月查询数据缺失问题
*/
public Result<Map<String, Object>> getBasicHealthDataPage(UnifiedHealthQueryDTO queryDTO) {
log.info("🚀 基础健康数据查询: customerId={}, userId={}, page={}, pageSize={}, 时间范围: {} ~ {}",
queryDTO.getCustomerId(), queryDTO.getUserId(), queryDTO.getPage(), queryDTO.getPageSize(),
queryDTO.getStartDate(), queryDTO.getEndDate());
try {
// 1. 检查缓存
String cacheKey = buildCacheKey(queryDTO);
Object cachedResult = redisTemplate.opsForValue().get(cacheKey);
if (cachedResult != null) {
log.info("💾 缓存命中: {}", cacheKey);
return Result.data((Map<String, Object>) cachedResult);
}
// 2. 查询主表数据
List<TUserHealthData> mainData = queryMainTableData(queryDTO);
log.info("📊 主表查询结果: {} 条", mainData.size());
// 3. 查询历史分表数据(解决跨月查询问题)
List<TUserHealthData> shardedData = queryShardedTableData(queryDTO);
log.info("📊 分表查询结果: {} 条", shardedData.size());
// 4. 合并数据
List<TUserHealthData> combinedData = new ArrayList<>();
combinedData.addAll(mainData);
combinedData.addAll(shardedData);
// 5. 按时间排序
combinedData.sort(Comparator.comparing(TUserHealthData::getTimestamp).reversed());
// 6. 应用分页
long total = combinedData.size();
int start = (queryDTO.getPage() - 1) * queryDTO.getPageSize();
int end = Math.min(start + queryDTO.getPageSize(), combinedData.size());
List<TUserHealthData> pageData = combinedData.subList(start, end);
// 7. 转换为基础VO(仅包含快字段)
List<BasicHealthDataVO> basicDataList = convertToBasicVO(pageData);
// 8. 构建返回结果
Map<String, Object> result = buildPageResult(total, basicDataList, queryDTO);
// 9. 缓存结果
cacheResult(cacheKey, result);
log.info("✅ 基础健康数据查询完成: 总计{}条记录(主表{}+分表{}), 本页{}条记录",
total, mainData.size(), shardedData.size(), basicDataList.size());
return Result.data(result);
} catch (Exception e) {
log.error("❌ 基础健康数据查询失败: {}", e.getMessage(), e);
return Result.failure("查询失败: " + e.getMessage());
}
}
/**
* 查询主表数据
*/
private List<TUserHealthData> queryMainTableData(UnifiedHealthQueryDTO queryDTO) {
LambdaQueryWrapper<TUserHealthData> wrapper = buildQueryWrapper(queryDTO);
return healthDataMapper.selectList(wrapper);
}
/**
* 构建查询条件
*/
private LambdaQueryWrapper<TUserHealthData> buildQueryWrapper(UnifiedHealthQueryDTO queryDTO) {
LambdaQueryWrapper<TUserHealthData> wrapper = new LambdaQueryWrapper<>();
// 客户ID过滤 - 当customerId为0时查询所有数据,否则按指定客户过滤
if (queryDTO.getCustomerId() != null && queryDTO.getCustomerId() != 0L) {
wrapper.eq(TUserHealthData::getCustomerId, queryDTO.getCustomerId());
}
// customerId为0时不添加过滤条件,返回所有客户的数据
// 用户ID过滤
if (queryDTO.getUserId() != null) {
wrapper.eq(TUserHealthData::getUserId, queryDTO.getUserId());
}
// 组织ID过滤
if (queryDTO.getOrgId() != null) {
wrapper.eq(TUserHealthData::getOrgId, queryDTO.getOrgId());
}
// 设备序列号过滤
if (queryDTO.getDeviceSn() != null) {
wrapper.eq(TUserHealthData::getDeviceSn, queryDTO.getDeviceSn());
}
// 时间范围过滤
if (queryDTO.getStartDate() != null) {
wrapper.ge(TUserHealthData::getTimestamp, queryDTO.getStartDate());
}
if (queryDTO.getEndDate() != null) {
wrapper.le(TUserHealthData::getTimestamp, queryDTO.getEndDate());
}
// 按时间降序排列
wrapper.orderByDesc(TUserHealthData::getTimestamp);
return wrapper;
}
/**
* 转换为基础VO对象(仅包含快字段)
*/
private List<BasicHealthDataVO> convertToBasicVO(List<TUserHealthData> healthDataList) {
if (healthDataList == null || healthDataList.isEmpty()) {
return new ArrayList<>();
}
return healthDataList.stream().map(this::convertSingleToBasicVO).collect(Collectors.toList());
}
/**
* 转换单个实体为基础VO
*/
private BasicHealthDataVO convertSingleToBasicVO(TUserHealthData data) {
BasicHealthDataVO vo = new BasicHealthDataVO();
// 基础信息
vo.setId(data.getId());
vo.setUserId(data.getUserId() != null ? data.getUserId().toString() : null);
vo.setCustomerId(data.getCustomerId());
vo.setOrgId(data.getOrgId());
vo.setDeviceSn(data.getDeviceSn());
vo.setTimestamp(data.getTimestamp());
// 添加用户信息
try {
if (data.getUserId() != null) {
var user = sysUserService.getById(data.getUserId());
if (user != null) {
vo.setUserName(user.getUserName());
vo.setOrgName(user.getOrgName());
} else {
vo.setUserName("未知用户");
vo.setOrgName("未知部门");
}
} else {
vo.setUserName("未知用户");
vo.setOrgName("未知部门");
}
} catch (Exception e) {
log.warn("获取用户信息失败: userId={}", data.getUserId());
vo.setUserName("未知用户");
vo.setOrgName("未知部门");
}
// 基础生理指标(快字段)
vo.setHeartRate(data.getHeartRate());
vo.setBloodOxygen(data.getBloodOxygen());
vo.setTemperature(data.getTemperature());
vo.setPressureHigh(data.getPressureHigh());
vo.setPressureLow(data.getPressureLow());
vo.setStress(data.getStress());
vo.setStep(data.getStep());
vo.setCalorie(data.getCalorie());
vo.setDistance(data.getDistance());
// 位置信息
vo.setLatitude(data.getLatitude());
vo.setLongitude(data.getLongitude());
vo.setAltitude(data.getAltitude());
return vo;
}
/**
* 构建分页返回结果 - 支持自定义总数(用于分表查询)
*/
private Map<String, Object> buildPageResult(long total,
List<BasicHealthDataVO> basicDataList,
UnifiedHealthQueryDTO queryDTO) {
Map<String, Object> result = new HashMap<>();
result.put("total", total);
result.put("pages", (int) Math.ceil((double) total / queryDTO.getPageSize()));
result.put("records", basicDataList);
result.put("pageSize", queryDTO.getPageSize());
result.put("page", queryDTO.getPage());
// 构建基础列定义(仅快字段)
result.put("columns", buildBasicColumns());
return result;
}
/**
* 构建基础列定义(仅快字段)
*/
private List<Map<String, Object>> buildBasicColumns() {
List<Map<String, Object>> columns = new ArrayList<>();
// 基础信息列
columns.add(Map.of("dataIndex", "id", "title", "ID", "valueType", "number", "width", 80, "key", "id"));
columns.add(Map.of("dataIndex", "userName", "title", "用户名称", "valueType", "string", "width", 120, "key", "userName"));
columns.add(Map.of("dataIndex", "orgName", "title", "部门名称", "valueType", "string", "width", 150, "key", "orgName"));
columns.add(Map.of("dataIndex", "deviceSn", "title", "设备序列号", "valueType", "string", "width", 120, "key", "deviceSn"));
columns.add(Map.of("dataIndex", "timestamp", "title", "时间戳", "valueType", "datetime", "width", 160, "key", "timestamp"));
// 生理指标列
columns.add(Map.of("dataIndex", "heartRate", "title", "心率", "valueType", "number", "width", 80, "key", "heartRate"));
columns.add(Map.of("dataIndex", "bloodOxygen", "title", "血氧", "valueType", "number", "width", 80, "key", "bloodOxygen"));
columns.add(Map.of("dataIndex", "pressureHigh", "title", "收缩压", "valueType", "number", "width", 80, "key", "pressureHigh"));
columns.add(Map.of("dataIndex", "pressureLow", "title", "舒张压", "valueType", "number", "width", 80, "key", "pressureLow"));
columns.add(Map.of("dataIndex", "temperature", "title", "体温", "valueType", "number", "width", 80, "key", "temperature"));
columns.add(Map.of("dataIndex", "stress", "title", "压力", "valueType", "number", "width", 80, "key", "stress"));
// 活动指标列
columns.add(Map.of("dataIndex", "step", "title", "步数", "valueType", "number", "width", 100, "key", "step"));
columns.add(Map.of("dataIndex", "calorie", "title", "卡路里", "valueType", "number", "width", 100, "key", "calorie"));
columns.add(Map.of("dataIndex", "distance", "title", "距离", "valueType", "number", "width", 100, "key", "distance"));
// 位置信息列
Map<String, Object> coordColumn = new HashMap<>();
coordColumn.put("dataIndex", "coordinates");
coordColumn.put("title", "坐标");
coordColumn.put("valueType", "string");
coordColumn.put("width", 200);
coordColumn.put("key", "coordinates");
coordColumn.put("render", "coordinates");
columns.add(coordColumn);
return columns;
}
/**
* 构建缓存key
*/
private String buildCacheKey(UnifiedHealthQueryDTO queryDTO) {
return CACHE_PREFIX +
(queryDTO.getCustomerId() != null ? queryDTO.getCustomerId() : 0L) + ":" +
(queryDTO.getUserId() != null ? queryDTO.getUserId() : "all") + ":" +
(queryDTO.getOrgId() != null ? queryDTO.getOrgId() : "all") + ":" +
(queryDTO.getStartDate() != null ? queryDTO.getStartDate().toString() : "nostart") + ":" +
(queryDTO.getEndDate() != null ? queryDTO.getEndDate().toString() : "noend") + ":" +
queryDTO.getPage() + ":" +
queryDTO.getPageSize();
}
/**
* 缓存查询结果
*/
private void cacheResult(String cacheKey, Map<String, Object> result) {
try {
redisTemplate.opsForValue().set(cacheKey, result, CACHE_EXPIRE_SECONDS, TimeUnit.SECONDS);
log.debug("💾 查询结果已缓存: {}, TTL: {}秒", cacheKey, CACHE_EXPIRE_SECONDS);
} catch (Exception e) {
log.warn("⚠️ 缓存设置失败: {}", e.getMessage());
}
}
// ========== 分表查询方法(从UnifiedHealthDataQueryService迁移) ==========
/**
* 查询分表数据
*/
private List<TUserHealthData> queryShardedTableData(UnifiedHealthQueryDTO queryDTO) {
List<TUserHealthData> allShardedData = new ArrayList<>();
// 获取需要查询的月份表
List<String> tableNames = getShardedTableNames(queryDTO.getStartDate(), queryDTO.getEndDate());
for (String tableName : tableNames) {
try {
// 检查表是否存在
if (tableExists(tableName)) {
List<TUserHealthData> data = querySpecificShardedTable(tableName, queryDTO);
allShardedData.addAll(data);
log.debug("📊 分表 {} 查询结果: {} 条", tableName, data.size());
}
} catch (Exception e) {
log.warn("⚠️ 查询分表 {} 失败: {}", tableName, e.getMessage());
}
}
return allShardedData;
}
/**
* 获取需要查询的分表名称
*/
private List<String> getShardedTableNames(LocalDateTime startDate, LocalDateTime endDate) {
List<String> tableNames = new ArrayList<>();
if (startDate == null || endDate == null) {
return tableNames;
}
LocalDate currentMonth = startDate.toLocalDate().withDayOfMonth(1);
LocalDate endMonth = endDate.toLocalDate().withDayOfMonth(1);
while (!currentMonth.isAfter(endMonth)) {
if (!currentMonth.equals(LocalDate.now().withDayOfMonth(1))) {
// 不是当前月份,生成分表名
String tableName = "t_user_health_data_" + currentMonth.format(DateTimeFormatter.ofPattern("yyyyMM"));
tableNames.add(tableName);
}
currentMonth = currentMonth.plusMonths(1);
}
return tableNames;
}
/**
* 检查表是否存在
*/
private boolean tableExists(String tableName) {
try {
String sql = "SELECT COUNT(*) FROM information_schema.tables WHERE table_name = ?";
Integer count = jdbcTemplate.queryForObject(sql, Integer.class, tableName);
return count != null && count > 0;
} catch (Exception e) {
log.warn("⚠️ 检查表存在性失败: {}", tableName);
return false;
}
}
/**
* 查询特定分表
*/
private List<TUserHealthData> querySpecificShardedTable(String tableName, UnifiedHealthQueryDTO queryDTO) {
try {
// 使用JDBC直接查询分表
StringBuilder sql = new StringBuilder("SELECT * FROM ").append(tableName).append(" WHERE 1=1");
List<Object> params = new ArrayList<>();
// 添加查询条件
if (queryDTO.getUserId() != null) {
sql.append(" AND user_id = ?");
params.add(queryDTO.getUserId());
} else if (queryDTO.getOrgId() != null) {
sql.append(" AND org_id = ?");
params.add(queryDTO.getOrgId());
} else if (queryDTO.getCustomerId() != null && queryDTO.getCustomerId() != 0L) {
sql.append(" AND customer_id = ?");
params.add(queryDTO.getCustomerId());
}
if (queryDTO.getStartDate() != null) {
sql.append(" AND timestamp >= ?");
params.add(queryDTO.getStartDate());
}
if (queryDTO.getEndDate() != null) {
sql.append(" AND timestamp <= ?");
params.add(queryDTO.getEndDate());
}
sql.append(" ORDER BY timestamp DESC");
// 执行查询
List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql.toString(), params.toArray());
// 转换为实体对象
return rows.stream()
.map(this::convertRowToEntity)
.collect(Collectors.toList());
} catch (Exception e) {
log.error("❌ 查询分表 {} 失败: {}", tableName, e.getMessage());
return Collections.emptyList();
}
}
/**
* 转换数据行为实体对象
*/
private TUserHealthData convertRowToEntity(Map<String, Object> row) {
TUserHealthData entity = new TUserHealthData();
entity.setId((Long) row.get("id"));
entity.setUserId((Long) row.get("user_id"));
entity.setCustomerId((Long) row.get("customer_id"));
entity.setOrgId((Long) row.get("org_id"));
entity.setDeviceSn((String) row.get("device_sn"));
entity.setTimestamp((LocalDateTime) row.get("timestamp"));
// 健康指标
entity.setHeartRate((Integer) row.get("heart_rate"));
entity.setBloodOxygen((Integer) row.get("blood_oxygen"));
entity.setTemperature(row.get("temperature") != null ?
Double.valueOf(row.get("temperature").toString()) : null);
entity.setPressureHigh((Integer) row.get("pressure_high"));
entity.setPressureLow((Integer) row.get("pressure_low"));
entity.setStress((Integer) row.get("stress"));
entity.setStep((Integer) row.get("step"));
entity.setCalorie(row.get("calorie") != null ?
Double.valueOf(row.get("calorie").toString()) : null);
entity.setDistance(row.get("distance") != null ?
Double.valueOf(row.get("distance").toString()) : null);
// 位置信息
entity.setLatitude(row.get("latitude") != null ?
Double.valueOf(row.get("latitude").toString()) : null);
entity.setLongitude(row.get("longitude") != null ?
Double.valueOf(row.get("longitude").toString()) : null);
entity.setAltitude(row.get("altitude") != null ?
Double.valueOf(row.get("altitude").toString()) : null);
return entity;
}
}
@@ -139,8 +139,8 @@ public class UnifiedHealthDataQueryService {
}
/**
* 查询健康数据 - 供数据分析模块调用
* 返回Map格式的原始数据,支持baseline/score/prediction/recommendation/profile
* 查询健康数据 - 表格图表分离架构
* 返回基础数据(表格)和独立的daily/weekly数据(图表)
*/
public Map<String, Object> queryHealthData(UnifiedHealthQueryDTO queryDTO) {
log.info("🔍 统一健康数据查询: customerId={}, orgId={}, userId={}, 时间范围={} ~ {}",
@@ -148,12 +148,17 @@ public class UnifiedHealthDataQueryService {
queryDTO.getStartDate(), queryDTO.getEndDate());
try {
// 查询主表和分表数据
List<Map<String, Object>> allData = queryAllHealthData(queryDTO);
// 表格图表分离查询
Map<String, Object> allDataResult = queryAllHealthData(queryDTO);
Map<String, Object> result = new HashMap<>();
result.put("data", allData);
result.put("total", allData.size());
result.put("basicData", allDataResult.get("basicData")); // 基础表格数据
result.put("total", allDataResult.get("total"));
result.put("page", allDataResult.get("page"));
result.put("pageSize", allDataResult.get("pageSize"));
result.put("dailyData", allDataResult.get("dailyData")); // 独立daily数据
result.put("weeklyData", allDataResult.get("weeklyData")); // 独立weekly数据
result.put("supportedFields", allDataResult.get("supportedFields")); // 字段配置
result.put("success", true);
return result;
@@ -163,7 +168,9 @@ public class UnifiedHealthDataQueryService {
queryDTO.getCustomerId(), queryDTO.getUserId(), e.getMessage(), e);
Map<String, Object> errorResult = new HashMap<>();
errorResult.put("data", Collections.emptyList());
errorResult.put("basicData", Collections.emptyList());
errorResult.put("dailyData", Collections.emptyMap());
errorResult.put("weeklyData", Collections.emptyMap());
errorResult.put("total", 0);
errorResult.put("success", false);
errorResult.put("error", e.getMessage());
@@ -175,11 +182,11 @@ public class UnifiedHealthDataQueryService {
// ========== 核心查询方法 ==========
/**
* 查询所有健康数据(主表+分表+慢字段合并)
* 用于数据分析,使用 Basic Enabled Metrics
* 查询健康数据 - 表格图表分离架构
* 基础分析字段按分页返回,daily/weekly数据独立返回
*/
private List<Map<String, Object>> queryAllHealthData(UnifiedHealthQueryDTO queryDTO) {
List<Map<String, Object>> allData = new ArrayList<>();
private Map<String, Object> queryAllHealthData(UnifiedHealthQueryDTO queryDTO) {
Map<String, Object> result = new HashMap<>();
// 1. 获取基础分析字段配置(使用 Basic Enabled Metrics)
Map<String, String> supportedFields = getBasicAnalysisFields(queryDTO.getCustomerId());
@@ -193,35 +200,50 @@ public class UnifiedHealthDataQueryService {
List<TUserHealthData> shardedData = queryShardedTableData(queryDTO);
log.info("📊 分表查询结果: {} 条", shardedData.size());
// 4. 合并数据
// 4. 合并并排序基础数据
List<TUserHealthData> combinedData = new ArrayList<>();
combinedData.addAll(mainData);
combinedData.addAll(shardedData);
combinedData.sort(Comparator.comparing(TUserHealthData::getTimestamp).reversed());
// 5. 按时间排序
combinedData.sort(Comparator.comparing(TUserHealthData::getTimestamp));
// 5. 应用分页到基础数据
List<Map<String, Object>> basicDataList = new ArrayList<>();
int start = (queryDTO.getPage() - 1) * queryDTO.getPageSize();
int end = Math.min(start + queryDTO.getPageSize(), combinedData.size());
// 6. 转换为Map并根据配置合并慢字段
for (TUserHealthData data : combinedData) {
for (int i = start; i < end; i++) {
TUserHealthData data = combinedData.get(i);
Map<String, Object> dataMap = convertToMap(data, supportedFields);
// 根据配置决定是否合并daily数据(睡眠等慢字段)
if (needsDailyData(supportedFields)) {
mergeDailyData(dataMap, data.getUserId(), data.getTimestamp(), supportedFields);
}
// 根据配置决定是否合并weekly数据
if (needsWeeklyData(supportedFields)) {
mergeWeeklyData(dataMap, data.getUserId(), data.getTimestamp(), supportedFields);
}
// 只返回支持的字段
filterSupportedFields(dataMap, supportedFields);
allData.add(dataMap);
basicDataList.add(dataMap);
}
return allData;
// 6. 独立查询daily数据(不合并到每条记录)
Map<String, Object> dailyDataCollection = new HashMap<>();
if (needsDailyData(supportedFields)) {
dailyDataCollection = queryDailyDataCollection(queryDTO, supportedFields);
}
// 7. 独立查询weekly数据(不合并到每条记录)
Map<String, Object> weeklyDataCollection = new HashMap<>();
if (needsWeeklyData(supportedFields)) {
weeklyDataCollection = queryWeeklyDataCollection(queryDTO, supportedFields);
}
// 8. 构建返回结果
result.put("basicData", basicDataList); // 基础表格数据(分页)
result.put("total", combinedData.size()); // 总记录数
result.put("page", queryDTO.getPage());
result.put("pageSize", queryDTO.getPageSize());
result.put("dailyData", dailyDataCollection); // 独立的daily数据用于图表
result.put("weeklyData", weeklyDataCollection); // 独立的weekly数据用于图表
result.put("supportedFields", supportedFields); // 字段配置
log.info("✅ 表格图表分离查询完成: 基础数据{}条(共{}条), daily数据{}项, weekly数据{}项",
basicDataList.size(), combinedData.size(),
dailyDataCollection.size(), weeklyDataCollection.size());
return result;
}
/**
@@ -1626,4 +1648,172 @@ public class UnifiedHealthDataQueryService {
default -> null;
};
}
// ========== 独立数据查询方法(表格图表分离架构) ==========
/**
* 查询独立的daily数据集合 - 用于图表渲染
* 不合并到每条记录,而是作为独立的原始JSON结构返回
*/
private Map<String, Object> queryDailyDataCollection(UnifiedHealthQueryDTO queryDTO, Map<String, String> supportedFields) {
Map<String, Object> dailyCollection = new HashMap<>();
try {
// 构建查询条件
LambdaQueryWrapper<TUserHealthDataDaily> wrapper = new LambdaQueryWrapper<>();
// 层级查询逻辑
if (queryDTO.getUserId() != null) {
wrapper.eq(TUserHealthDataDaily::getUserId, queryDTO.getUserId());
} else if (queryDTO.getOrgId() != null) {
wrapper.eq(TUserHealthDataDaily::getOrgId, queryDTO.getOrgId());
} else if (queryDTO.getCustomerId() != null && queryDTO.getCustomerId() != 0L) {
wrapper.eq(TUserHealthDataDaily::getCustomerId, queryDTO.getCustomerId());
}
// 时间范围
if (queryDTO.getStartDate() != null) {
wrapper.ge(TUserHealthDataDaily::getTimestamp, queryDTO.getStartDate().toLocalDate());
}
if (queryDTO.getEndDate() != null) {
wrapper.le(TUserHealthDataDaily::getTimestamp, queryDTO.getEndDate().toLocalDate());
}
// 查询所有daily数据
List<TUserHealthDataDaily> dailyList = dailyMapper.selectList(wrapper);
log.info("📊 查询到daily数据: {} 条", dailyList.size());
// 按字段类型分组收集数据
List<Map<String, Object>> sleepDataList = new ArrayList<>();
List<Map<String, Object>> exerciseDataList = new ArrayList<>();
List<Map<String, Object>> workoutDataList = new ArrayList<>();
List<Map<String, Object>> scientificSleepDataList = new ArrayList<>();
for (TUserHealthDataDaily daily : dailyList) {
Map<String, Object> dailyRecord = new HashMap<>();
dailyRecord.put("userId", daily.getUserId());
dailyRecord.put("date", daily.getTimestamp());
// 收集睡眠数据
if ((supportedFields.containsKey("sleepData") || supportedFields.containsKey("sleep"))
&& daily.getSleepData() != null) {
Map<String, Object> sleepRecord = new HashMap<>(dailyRecord);
sleepRecord.put("rawData", daily.getSleepData()); // 原始JSON
sleepRecord.put("processed", processSleepData(daily.getSleepData())); // 解析后数据
sleepDataList.add(sleepRecord);
}
// 收集运动日常数据
if ((supportedFields.containsKey("exerciseDailyData") || supportedFields.containsKey("exercise_daily"))
&& daily.getExerciseDailyData() != null) {
Map<String, Object> exerciseRecord = new HashMap<>(dailyRecord);
exerciseRecord.put("rawData", daily.getExerciseDailyData());
exerciseRecord.put("processed", processExerciseDailyData(daily.getExerciseDailyData()));
exerciseDataList.add(exerciseRecord);
}
// 收集运动数据
if ((supportedFields.containsKey("workoutData") || supportedFields.containsKey("work_out"))
&& daily.getWorkoutData() != null) {
Map<String, Object> workoutRecord = new HashMap<>(dailyRecord);
workoutRecord.put("rawData", daily.getWorkoutData());
workoutRecord.put("processed", processWorkoutData(daily.getWorkoutData()));
workoutDataList.add(workoutRecord);
}
// 收集科学睡眠数据
if ((supportedFields.containsKey("scientificSleepData") || supportedFields.containsKey("scientific_sleep"))
&& daily.getScientificSleepData() != null) {
Map<String, Object> sciRecord = new HashMap<>(dailyRecord);
sciRecord.put("rawData", daily.getScientificSleepData());
sciRecord.put("processed", daily.getScientificSleepData()); // 简化处理
scientificSleepDataList.add(sciRecord);
}
}
// 构建返回结果
if (!sleepDataList.isEmpty()) {
dailyCollection.put("sleepData", sleepDataList);
}
if (!exerciseDataList.isEmpty()) {
dailyCollection.put("exerciseDailyData", exerciseDataList);
}
if (!workoutDataList.isEmpty()) {
dailyCollection.put("workoutData", workoutDataList);
}
if (!scientificSleepDataList.isEmpty()) {
dailyCollection.put("scientificSleepData", scientificSleepDataList);
}
log.info("📊 Daily数据收集完成: 睡眠{}条, 运动{}条, 锻炼{}条, 科学睡眠{}条",
sleepDataList.size(), exerciseDataList.size(),
workoutDataList.size(), scientificSleepDataList.size());
} catch (Exception e) {
log.error("❌ 查询daily数据集合失败: {}", e.getMessage(), e);
}
return dailyCollection;
}
/**
* 查询独立的weekly数据集合 - 用于图表渲染
*/
private Map<String, Object> queryWeeklyDataCollection(UnifiedHealthQueryDTO queryDTO, Map<String, String> supportedFields) {
Map<String, Object> weeklyCollection = new HashMap<>();
try {
// 构建查询条件
LambdaQueryWrapper<TUserHealthDataWeekly> wrapper = new LambdaQueryWrapper<>();
// 层级查询逻辑
if (queryDTO.getUserId() != null) {
wrapper.eq(TUserHealthDataWeekly::getUserId, queryDTO.getUserId());
} else if (queryDTO.getOrgId() != null) {
wrapper.eq(TUserHealthDataWeekly::getOrgId, queryDTO.getOrgId());
} else if (queryDTO.getCustomerId() != null && queryDTO.getCustomerId() != 0L) {
wrapper.eq(TUserHealthDataWeekly::getCustomerId, queryDTO.getCustomerId());
}
// 时间范围
if (queryDTO.getStartDate() != null) {
wrapper.ge(TUserHealthDataWeekly::getTimestamp, queryDTO.getStartDate().toLocalDate());
}
if (queryDTO.getEndDate() != null) {
wrapper.le(TUserHealthDataWeekly::getTimestamp, queryDTO.getEndDate().toLocalDate());
}
// 查询所有weekly数据
List<TUserHealthDataWeekly> weeklyList = weeklyMapper.selectList(wrapper);
log.info("📊 查询到weekly数据: {} 条", weeklyList.size());
// 收集周运动数据
List<Map<String, Object>> exerciseWeekDataList = new ArrayList<>();
for (TUserHealthDataWeekly weekly : weeklyList) {
if ((supportedFields.containsKey("exerciseWeekData") || supportedFields.containsKey("exercise_week"))
&& weekly.getExerciseWeekData() != null) {
Map<String, Object> weeklyRecord = new HashMap<>();
weeklyRecord.put("userId", weekly.getUserId());
weeklyRecord.put("date", weekly.getTimestamp());
weeklyRecord.put("rawData", weekly.getExerciseWeekData()); // 原始JSON
weeklyRecord.put("processed", processExerciseWeekData(weekly.getExerciseWeekData())); // 解析后数据
exerciseWeekDataList.add(weeklyRecord);
}
}
// 构建返回结果
if (!exerciseWeekDataList.isEmpty()) {
weeklyCollection.put("exerciseWeekData", exerciseWeekDataList);
}
log.info("📊 Weekly数据收集完成: 周运动{}条", exerciseWeekDataList.size());
} catch (Exception e) {
log.error("❌ 查询weekly数据集合失败: {}", e.getMessage(), e);
}
return weeklyCollection;
}
}