From fe82afd934baefb978d483606c8d119584b514e5 Mon Sep 17 00:00:00 2001 From: wanghao Date: Wed, 4 Mar 2026 15:30:07 +0800 Subject: [PATCH] =?UTF-8?q?=E6=89=8B=E8=A1=A8=E6=8E=A5=E5=8F=A3=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../watch/vo/UserData/res/UserStat.java | 38 +++- .../service/impl/WatchDataApiServiceImpl.java | 199 +++++++++++++++--- 2 files changed, 203 insertions(+), 34 deletions(-) diff --git a/health-watch/health-watch-api/src/main/java/com/renkang/watch/vo/UserData/res/UserStat.java b/health-watch/health-watch-api/src/main/java/com/renkang/watch/vo/UserData/res/UserStat.java index 4901b8e..62ababf 100644 --- a/health-watch/health-watch-api/src/main/java/com/renkang/watch/vo/UserData/res/UserStat.java +++ b/health-watch/health-watch-api/src/main/java/com/renkang/watch/vo/UserData/res/UserStat.java @@ -1,18 +1,23 @@ package com.renkang.watch.vo.UserData.res; import com.fasterxml.jackson.annotation.JsonFormat; +import com.renkang.watch.WatchH5IndexItemNewVo; import lombok.Data; import java.math.BigDecimal; import java.util.Date; -/** 用户统计数据 +/** + * 用户统计数据 + * * @author stan * @since 2024-11-04 16:33 */ @Data public class UserStat { - /** 数据类型:0=心率 1=血氧 2=压力 3=体温 4=睡眠 */ + /** + * 数据类型:0=心率 1=血氧 2=压力 3=体温 4=睡眠 + */ private String type; private BigDecimal latest; private BigDecimal max; @@ -21,4 +26,33 @@ public class UserStat { @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd") private Date latestTime; + public UserStat() { + } + + public UserStat(WatchH5IndexItemNewVo watchH5IndexItemNewVo) { + switch (watchH5IndexItemNewVo.getWdType()) { + case "sleep": + this.type = "4"; + break; + case "heart_rate": + this.type = "0"; + break; + case "stress": + this.type = "2"; + break; + case "spo2": + this.type = "1"; + break; + case "temperature": + this.type = "3"; + break; + default: + break; + } + this.latest = BigDecimal.valueOf(Double.parseDouble(watchH5IndexItemNewVo.getDataValue())); + this.max = BigDecimal.valueOf(Double.parseDouble(watchH5IndexItemNewVo.getDataMax())); + this.min = BigDecimal.valueOf(Double.parseDouble(watchH5IndexItemNewVo.getDataMin())); + this.avg = BigDecimal.valueOf(Double.parseDouble(watchH5IndexItemNewVo.getDataAvg())); + this.latestTime = watchH5IndexItemNewVo.getDataDate(); + } } diff --git a/health-watch/health-watch-biz/src/main/java/com/renkang/watch/api/service/impl/WatchDataApiServiceImpl.java b/health-watch/health-watch-biz/src/main/java/com/renkang/watch/api/service/impl/WatchDataApiServiceImpl.java index 0765d73..5f71327 100644 --- a/health-watch/health-watch-biz/src/main/java/com/renkang/watch/api/service/impl/WatchDataApiServiceImpl.java +++ b/health-watch/health-watch-biz/src/main/java/com/renkang/watch/api/service/impl/WatchDataApiServiceImpl.java @@ -7,6 +7,8 @@ import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.github.yulichang.wrapper.MPJLambdaWrapper; +import com.renkang.watch.WatchH5IndexItemNewVo; import com.renkang.watch.api.bean.response.UserWatchInfo; import com.renkang.watch.api.service.WatchDataApiService; import com.renkang.watch.dto.WatchDataWorkoutDTO; @@ -24,6 +26,7 @@ import com.renkang.watch.vo.UserData.res.bodyTemperature.BodyTempStat; import com.renkang.watch.vo.UserData.res.bodyTemperature.BodyTempStatRes; import com.renkang.watch.vo.UserData.res.heartRate.WatchDataHeartRateRes; import com.renkang.watch.vo.UserData.res.sleep.WatchDataSleepRes; +import com.renkang.watch.vo.WatchH5IndexItemVo; import org.apache.commons.collections4.CollectionUtils; import org.jeecg.common.api.vo.Result; import org.jeecg.common.system.vo.LoginUser; @@ -32,25 +35,25 @@ import org.jeecg.common.util.RedisUtil; import org.jeecg.global.GlobalUtils; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.task.AsyncTaskExecutor; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.stereotype.Service; +import javax.annotation.PostConstruct; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.*; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; +import java.util.stream.Stream; @Service public class WatchDataApiServiceImpl implements WatchDataApiService { - @Autowired private WatchDataWorkoutMapper watchDataWorkoutMapper; - - @Autowired - private WatchStatUserInfoDayHeartRateMapper watchStatUserInfoDayHeartRateMapper; - - @Autowired private WatchDataHeartRateMapper watchDataHeartRateMapper; @@ -62,9 +65,6 @@ public class WatchDataApiServiceImpl implements WatchDataApiService { @Autowired private WatchStatUserInfoDaySpo2ServiceImpl watchStatUserInfoDaySpo2ServiceImpl; - @Autowired - private WatchStatUserInfoDaySleepMapper watchStatUserInfoDaySleepMapper; - @Autowired private WatchDataSleepNewMinuteMapper watchDataSleepNewMinuteMapper; @@ -98,7 +98,6 @@ public class WatchDataApiServiceImpl implements WatchDataApiService { @Autowired private IWatchDataTemperatureService watchDataTemperatureService; - @Autowired private IWatchDataStressService watchDataStressService; @Autowired @@ -108,6 +107,10 @@ public class WatchDataApiServiceImpl implements WatchDataApiService { @Autowired private WatchUserDataMapper watchUserDataMapper; @Autowired + private WatchStatUserInfoDayHeartRateMapper watchStatUserInfoDayHeartRateMapper; + @Autowired + private WatchStatUserInfoDaySleepMapper watchStatUserInfoDaySleepMapper; + @Autowired private WatchStatUserInfoDaySpo2Mapper watchStatUserInfoDaySpo2Mapper; @Autowired private WatchStatUserInfoDayStressMapper watchStatUserInfoDayStressMapper; @@ -120,6 +123,31 @@ public class WatchDataApiServiceImpl implements WatchDataApiService { /** 用户统计全量数据 Redis 缓存 key 前缀 */ private static final String USER_STAT_ALL_CACHE_KEY = "watch:userStatAll:"; + private AsyncTaskExecutor asyncTaskExecutor; + + public AsyncTaskExecutor getAsyncTaskExecutor() { + if (asyncTaskExecutor == null) { + initAsyncExecutor(); + } + return asyncTaskExecutor; + } + + @PostConstruct + public void init() { + initAsyncExecutor(); + } + + private void initAsyncExecutor() { + ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); + taskExecutor.setCorePoolSize(150); + taskExecutor.setMaxPoolSize(500); + taskExecutor.setQueueCapacity(500); + taskExecutor.setWaitForTasksToCompleteOnShutdown(true); + taskExecutor.setThreadNamePrefix("watch-latest-pool1-"); + taskExecutor.initialize(); + asyncTaskExecutor = taskExecutor; + } + @Override public Result> selectSportCostListApi() { LoginUser loginUser = GlobalUtils.getLoginUser(); @@ -866,39 +894,146 @@ public class WatchDataApiServiceImpl implements WatchDataApiService { // 命中 Redis 缓存直接返回 Object cached = redisUtil.get(cacheKey); if (cached != null) { - //noinspection unchecked return (List) cached; } - // 并行异步查询5种数据,thenApply 在任务完成时立即绑定 type,与顺序无关 - CompletableFuture heartRateFuture = CompletableFuture.supplyAsync(() -> handleHeartRate(userId, date)) - .thenApply(stat -> setType(stat, "0")); - CompletableFuture bloodOxygenFuture = CompletableFuture.supplyAsync(() -> handleBloodOxygen(userId, date)) - .thenApply(stat -> setType(stat, "1")); - CompletableFuture stressFuture = CompletableFuture.supplyAsync(() -> handleStress(userId, date)) - .thenApply(stat -> setType(stat, "2")); - CompletableFuture bodyTempFuture = CompletableFuture.supplyAsync(() -> handleBodyTemperature(userId, date)) - .thenApply(stat -> setType(stat, "3")); - CompletableFuture sleepFuture = CompletableFuture.supplyAsync(() -> handleSleep(userId, date)) - .thenApply(stat -> setType(stat, "4")); + // 构建「数据类型 → 异步查询 Future」映射,消除 switch-case 冗余 + Map> futureMap = new HashMap<>(); + futureMap.put("sleep", CompletableFuture.supplyAsync(() -> latestSleepData(userId), getAsyncTaskExecutor())); + futureMap.put("heart_rate", CompletableFuture.supplyAsync(() -> latestHeartRateData(userId), getAsyncTaskExecutor())); + futureMap.put("spo2", CompletableFuture.supplyAsync(() -> latestSpo2Data(userId), getAsyncTaskExecutor())); + futureMap.put("stress", CompletableFuture.supplyAsync(() -> latestStressData(userId), getAsyncTaskExecutor())); + futureMap.put("temperature", CompletableFuture.supplyAsync(() -> latestTempData(userId), getAsyncTaskExecutor())); - // 等待所有子任务完成 - CompletableFuture.allOf(heartRateFuture, bloodOxygenFuture, stressFuture, bodyTempFuture, sleepFuture).join(); + // 等待全部异步查询完成 + CompletableFuture.allOf(futureMap.values().toArray(new CompletableFuture[0])).join(); - // 组装结果列表,每条数据已在异步阶段绑定好 type - List result = Arrays.asList( - heartRateFuture.join(), - bloodOxygenFuture.join(), - stressFuture.join(), - bodyTempFuture.join(), - sleepFuture.join() - ); + // 转为顺序流遍历,避免并行流写入 ArrayList 的线程安全问题 + List list = new ArrayList<>(); + emptyItems().sequential().forEach(itemVo -> { + CompletableFuture future = futureMap.get(itemVo.getWdType()); + // steps 等暂无对应查询的类型直接跳过 + if (future == null) { + return; + } + WatchH5IndexItemNewVo data = future.join(); + if (data == null) { + data = new WatchH5IndexItemNewVo(); + } + // 将模板字段(wdType、wdTypeName、阈值)合并到结果对象 + data.fieldCopy(itemVo); + // 睡眠数据特殊处理:dataValue 为空时补 "0" + if ("sleep".equals(itemVo.getWdType()) && StrUtil.isEmpty(data.getDataValue())) { + data.setDataValue("0"); + } + list.add(data); + }); + + List result = list.stream().map(UserStat::new).collect(Collectors.toList()); // 写入 Redis 缓存,TTL=3分钟(180秒) redisUtil.set(cacheKey, result, 180); return result; } + private Stream emptyItems() { + //睡眠 + WatchH5IndexItemVo sleepVo = new WatchH5IndexItemVo(); + sleepVo.setWdType("sleep"); + //步数 + WatchH5IndexItemVo stepsVo = new WatchH5IndexItemVo(); + stepsVo.setWdType("steps"); + //心率 + WatchH5IndexItemVo hearRateVo = new WatchH5IndexItemVo(); + hearRateVo.setWdType("heart_rate"); + //压力 + WatchH5IndexItemVo stressVo = new WatchH5IndexItemVo(); + stressVo.setWdType("stress"); + //血氧饱和度 + WatchH5IndexItemVo spo2Vo = new WatchH5IndexItemVo(); + spo2Vo.setWdType("spo2"); + //体温 + WatchH5IndexItemVo tempVo = new WatchH5IndexItemVo(); + tempVo.setWdType("temperature"); + List list = Arrays.asList(sleepVo, stepsVo, hearRateVo, stressVo, spo2Vo, tempVo); + return list.parallelStream() + .peek(itemVo -> { + itemVo.setWarnMin("0.0"); + itemVo.setWarnMax("0.0"); + }); + } + + private WatchH5IndexItemNewVo latestSleepData(String userId) { + MPJLambdaWrapper wrapper = new MPJLambdaWrapper<>(); + //因为睡眠详情页取的是长睡时长作为当天的睡眠时间 , 所以这里保持一致并没有取当天的睡眠总时长 + wrapper.selectAs(WatchStatUserInfoDaySleep::getLongDurationTotal,WatchH5IndexItemNewVo::getDataValue); + wrapper.selectAs(WatchStatUserInfoDaySleep::getLongDurationTotal,WatchH5IndexItemNewVo::getDataMax); + wrapper.selectAs(WatchStatUserInfoDaySleep::getShortDurationTotal,WatchH5IndexItemNewVo::getDataMin); + wrapper.selectAs(WatchStatUserInfoDaySleep::getDataDate,WatchH5IndexItemNewVo::getDataDate); + wrapper.eq(WatchStatUserInfoDaySleep::getUserId, userId); + wrapper.orderByDesc(WatchStatUserInfoDaySleep::getDataDate); + wrapper.last("limit 1"); + return watchStatUserInfoDaySleepMapper.selectJoinOne(WatchH5IndexItemNewVo.class,wrapper); + } + + private WatchH5IndexItemNewVo latestHeartRateData(String userId) { + MPJLambdaWrapper wrapper = new MPJLambdaWrapper<>(); + // 必须是watch_stat_user_info_day_heart_rate表中new_data字段启用后才能这样查 + wrapper.selectAs(WatchStatUserInfoDayHeartRate::getNewValue,WatchH5IndexItemNewVo::getDataValue); + wrapper.selectAs(WatchStatUserInfoDayHeartRate::getMaxValue,WatchH5IndexItemNewVo::getDataMax); + wrapper.selectAs(WatchStatUserInfoDayHeartRate::getMinValue,WatchH5IndexItemNewVo::getDataMin); + wrapper.selectAs(WatchStatUserInfoDayHeartRate::getAvgValue,WatchH5IndexItemNewVo::getDataAvg); + wrapper.selectAs(WatchStatUserInfoDayHeartRate::getDataDate,WatchH5IndexItemNewVo::getDataDate); + wrapper.eq(WatchStatUserInfoDayHeartRate::getUserId, userId); + wrapper.orderByDesc(WatchStatUserInfoDayHeartRate::getDataDate); + wrapper.last("limit 1"); + return watchStatUserInfoDayHeartRateMapper.selectJoinOne(WatchH5IndexItemNewVo.class,wrapper); + } + + private WatchH5IndexItemNewVo latestSpo2Data(String userId) { + MPJLambdaWrapper wrapper = new MPJLambdaWrapper<>(); + // 必须是watch_stat_user_info_day_spo2表中new_data字段启用后才能这样查 + wrapper.selectAs(WatchStatUserInfoDaySpo2::getNewValue,WatchH5IndexItemNewVo::getDataValue); + wrapper.selectAs(WatchStatUserInfoDaySpo2::getMaxValue,WatchH5IndexItemNewVo::getDataMax); + wrapper.selectAs(WatchStatUserInfoDaySpo2::getMinValue,WatchH5IndexItemNewVo::getDataMin); + wrapper.selectAs(WatchStatUserInfoDaySpo2::getAvgValue,WatchH5IndexItemNewVo::getDataAvg); + wrapper.selectAs(WatchStatUserInfoDaySpo2::getDataDate,WatchH5IndexItemNewVo::getDataDate); + wrapper.eq(WatchStatUserInfoDaySpo2::getUserId, userId); + wrapper.orderByDesc(WatchStatUserInfoDaySpo2::getDataDate); + wrapper.last("limit 1"); + return watchStatUserInfoDaySpo2Mapper.selectJoinOne(WatchH5IndexItemNewVo.class,wrapper); + } + + private WatchH5IndexItemNewVo latestStressData(String userId) { + MPJLambdaWrapper wrapper = new MPJLambdaWrapper<>(); + // 必须是watch_stat_user_info_day_stress表中new_data字段启用后才能这样查 + wrapper.selectAs(WatchStatUserInfoDayStress::getNewValue,WatchH5IndexItemNewVo::getDataValue); + wrapper.selectAs(WatchStatUserInfoDayStress::getMaxValue,WatchH5IndexItemNewVo::getDataMax); + wrapper.selectAs(WatchStatUserInfoDayStress::getMinValue,WatchH5IndexItemNewVo::getDataMin); + wrapper.selectAs(WatchStatUserInfoDayStress::getAvgValue,WatchH5IndexItemNewVo::getDataAvg); + wrapper.selectAs(WatchStatUserInfoDayStress::getDataDate,WatchH5IndexItemNewVo::getDataDate); + wrapper.eq(WatchStatUserInfoDayStress::getUserId, userId); + wrapper.orderByDesc(WatchStatUserInfoDayStress::getDataDate); + wrapper.last("limit 1"); + return watchStatUserInfoDayStressMapper.selectJoinOne(WatchH5IndexItemNewVo.class,wrapper); + } + + private WatchH5IndexItemNewVo latestTempData(String userId) { + MPJLambdaWrapper wrapper = new MPJLambdaWrapper<>(); + // 必须是watch_stat_user_info_day_temp表中new_data字段启用后才能这样查 + wrapper.selectAs(WatchStatUserInfoDayTemp::getNewValue,WatchH5IndexItemNewVo::getDataValue); + wrapper.selectAs(WatchStatUserInfoDayTemp::getMaxValue,WatchH5IndexItemNewVo::getDataMax); + wrapper.selectAs(WatchStatUserInfoDayTemp::getMinValue,WatchH5IndexItemNewVo::getDataMin); + wrapper.selectAs(WatchStatUserInfoDayTemp::getAvgValue,WatchH5IndexItemNewVo::getDataAvg); + wrapper.selectAs(WatchStatUserInfoDayTemp::getDataDate,WatchH5IndexItemNewVo::getDataDate); + wrapper.eq(WatchStatUserInfoDayTemp::getUserId, userId); + wrapper.orderByDesc(WatchStatUserInfoDayTemp::getDataDate); + wrapper.last("limit 1"); + return watchStatUserInfoDayTempMapper.selectJoinOne(WatchH5IndexItemNewVo.class,wrapper); + } + + + @Override public Boolean userHaveWatch(String userId) { if (StrUtil.isBlank(userId)) {