diff --git a/ljwx-admin/src/views/health/info/index.vue b/ljwx-admin/src/views/health/info/index.vue index 028850f..a558ba3 100644 --- a/ljwx-admin/src/views/health/info/index.vue +++ b/ljwx-admin/src/views/health/info/index.vue @@ -1,421 +1,404 @@ + + \ No newline at end of file diff --git a/ljwx-boot/ljwx-boot-admin/src/main/java/com/ljwx/admin/controller/health/TUserHealthDataController.java b/ljwx-boot/ljwx-boot-admin/src/main/java/com/ljwx/admin/controller/health/TUserHealthDataController.java index bf6350c..109e2fc 100644 --- a/ljwx-boot/ljwx-boot-admin/src/main/java/com/ljwx/admin/controller/health/TUserHealthDataController.java +++ b/ljwx-boot/ljwx-boot-admin/src/main/java/com/ljwx/admin/controller/health/TUserHealthDataController.java @@ -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> page(PageQuery pageQuery, TUserHealthDataSearchDTO dto) { - HealthDataPageVO> vo = tUserHealthDataFacade.listTUserHealthDataPage(pageQuery, dto); - Map 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 result = unifiedHealthDataQueryService.queryHealthData(queryDTO); + + // 构建返回格式 + Map 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 supportedFields = (Map) 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> buildDynamicColumns(Map supportedFields) { + List> 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 entry : supportedFields.entrySet()) { + String fieldKey = entry.getKey(); + String fieldType = entry.getValue(); + + // 只添加快字段(非慢字段) + if (!"slow".equals(fieldType)) { + Map column = createHealthColumn(fieldKey); + if (column != null) { + columns.add(column); + } + } + } + + return columns; + } + + /** + * 创建健康指标列配置 + */ + private Map 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") diff --git a/ljwx-boot/ljwx-boot-modules/src/main/java/com/ljwx/config/AsyncConfig.java b/ljwx-boot/ljwx-boot-modules/src/main/java/com/ljwx/config/AsyncConfig.java index 6db1c68..d560084 100644 --- a/ljwx-boot/ljwx-boot-modules/src/main/java/com/ljwx/config/AsyncConfig.java +++ b/ljwx-boot/ljwx-boot-modules/src/main/java/com/ljwx/config/AsyncConfig.java @@ -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() // 拒绝策略 + ); + } } \ No newline at end of file diff --git a/ljwx-boot/ljwx-boot-modules/src/main/java/com/ljwx/modules/health/service/BasicHealthDataQueryService.java b/ljwx-boot/ljwx-boot-modules/src/main/java/com/ljwx/modules/health/service/BasicHealthDataQueryService.java new file mode 100644 index 0000000..1dd6e96 --- /dev/null +++ b/ljwx-boot/ljwx-boot-modules/src/main/java/com/ljwx/modules/health/service/BasicHealthDataQueryService.java @@ -0,0 +1,468 @@ +/* + * All Rights Reserved: Copyright [2024] [ljwx (brunoGao@gmail.com)] + * 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 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> 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) cachedResult); + } + + // 2. 查询主表数据 + List mainData = queryMainTableData(queryDTO); + log.info("📊 主表查询结果: {} 条", mainData.size()); + + // 3. 查询历史分表数据(解决跨月查询问题) + List shardedData = queryShardedTableData(queryDTO); + log.info("📊 分表查询结果: {} 条", shardedData.size()); + + // 4. 合并数据 + List 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 pageData = combinedData.subList(start, end); + + // 7. 转换为基础VO(仅包含快字段) + List basicDataList = convertToBasicVO(pageData); + + // 8. 构建返回结果 + Map 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 queryMainTableData(UnifiedHealthQueryDTO queryDTO) { + LambdaQueryWrapper wrapper = buildQueryWrapper(queryDTO); + return healthDataMapper.selectList(wrapper); + } + + /** + * 构建查询条件 + */ + private LambdaQueryWrapper buildQueryWrapper(UnifiedHealthQueryDTO queryDTO) { + LambdaQueryWrapper 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 convertToBasicVO(List 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 buildPageResult(long total, + List basicDataList, + UnifiedHealthQueryDTO queryDTO) { + Map 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> buildBasicColumns() { + List> 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 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 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 queryShardedTableData(UnifiedHealthQueryDTO queryDTO) { + List allShardedData = new ArrayList<>(); + + // 获取需要查询的月份表 + List tableNames = getShardedTableNames(queryDTO.getStartDate(), queryDTO.getEndDate()); + + for (String tableName : tableNames) { + try { + // 检查表是否存在 + if (tableExists(tableName)) { + List data = querySpecificShardedTable(tableName, queryDTO); + allShardedData.addAll(data); + log.debug("📊 分表 {} 查询结果: {} 条", tableName, data.size()); + } + } catch (Exception e) { + log.warn("⚠️ 查询分表 {} 失败: {}", tableName, e.getMessage()); + } + } + + return allShardedData; + } + + /** + * 获取需要查询的分表名称 + */ + private List getShardedTableNames(LocalDateTime startDate, LocalDateTime endDate) { + List 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 querySpecificShardedTable(String tableName, UnifiedHealthQueryDTO queryDTO) { + try { + // 使用JDBC直接查询分表 + StringBuilder sql = new StringBuilder("SELECT * FROM ").append(tableName).append(" WHERE 1=1"); + List 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> 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 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; + } +} \ No newline at end of file diff --git a/ljwx-boot/ljwx-boot-modules/src/main/java/com/ljwx/modules/health/service/UnifiedHealthDataQueryService.java b/ljwx-boot/ljwx-boot-modules/src/main/java/com/ljwx/modules/health/service/UnifiedHealthDataQueryService.java index 34402b9..bff9d50 100644 --- a/ljwx-boot/ljwx-boot-modules/src/main/java/com/ljwx/modules/health/service/UnifiedHealthDataQueryService.java +++ b/ljwx-boot/ljwx-boot-modules/src/main/java/com/ljwx/modules/health/service/UnifiedHealthDataQueryService.java @@ -139,8 +139,8 @@ public class UnifiedHealthDataQueryService { } /** - * 查询健康数据 - 供数据分析模块调用 - * 返回Map格式的原始数据,支持baseline/score/prediction/recommendation/profile + * 查询健康数据 - 表格图表分离架构 + * 返回基础数据(表格)和独立的daily/weekly数据(图表) */ public Map queryHealthData(UnifiedHealthQueryDTO queryDTO) { log.info("🔍 统一健康数据查询: customerId={}, orgId={}, userId={}, 时间范围={} ~ {}", @@ -148,12 +148,17 @@ public class UnifiedHealthDataQueryService { queryDTO.getStartDate(), queryDTO.getEndDate()); try { - // 查询主表和分表数据 - List> allData = queryAllHealthData(queryDTO); + // 表格图表分离查询 + Map allDataResult = queryAllHealthData(queryDTO); Map 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 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> queryAllHealthData(UnifiedHealthQueryDTO queryDTO) { - List> allData = new ArrayList<>(); + private Map queryAllHealthData(UnifiedHealthQueryDTO queryDTO) { + Map result = new HashMap<>(); // 1. 获取基础分析字段配置(使用 Basic Enabled Metrics) Map supportedFields = getBasicAnalysisFields(queryDTO.getCustomerId()); @@ -193,35 +200,50 @@ public class UnifiedHealthDataQueryService { List shardedData = queryShardedTableData(queryDTO); log.info("📊 分表查询结果: {} 条", shardedData.size()); - // 4. 合并数据 + // 4. 合并并排序基础数据 List 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> 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 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 dailyDataCollection = new HashMap<>(); + if (needsDailyData(supportedFields)) { + dailyDataCollection = queryDailyDataCollection(queryDTO, supportedFields); + } + + // 7. 独立查询weekly数据(不合并到每条记录) + Map 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 queryDailyDataCollection(UnifiedHealthQueryDTO queryDTO, Map supportedFields) { + Map dailyCollection = new HashMap<>(); + + try { + // 构建查询条件 + LambdaQueryWrapper 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 dailyList = dailyMapper.selectList(wrapper); + log.info("📊 查询到daily数据: {} 条", dailyList.size()); + + // 按字段类型分组收集数据 + List> sleepDataList = new ArrayList<>(); + List> exerciseDataList = new ArrayList<>(); + List> workoutDataList = new ArrayList<>(); + List> scientificSleepDataList = new ArrayList<>(); + + for (TUserHealthDataDaily daily : dailyList) { + Map dailyRecord = new HashMap<>(); + dailyRecord.put("userId", daily.getUserId()); + dailyRecord.put("date", daily.getTimestamp()); + + // 收集睡眠数据 + if ((supportedFields.containsKey("sleepData") || supportedFields.containsKey("sleep")) + && daily.getSleepData() != null) { + Map 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 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 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 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 queryWeeklyDataCollection(UnifiedHealthQueryDTO queryDTO, Map supportedFields) { + Map weeklyCollection = new HashMap<>(); + + try { + // 构建查询条件 + LambdaQueryWrapper 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 weeklyList = weeklyMapper.selectList(wrapper); + log.info("📊 查询到weekly数据: {} 条", weeklyList.size()); + + // 收集周运动数据 + List> exerciseWeekDataList = new ArrayList<>(); + + for (TUserHealthDataWeekly weekly : weeklyList) { + if ((supportedFields.containsKey("exerciseWeekData") || supportedFields.containsKey("exercise_week")) + && weekly.getExerciseWeekData() != null) { + + Map 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; + } } \ No newline at end of file