提交进度

This commit is contained in:
Alatus Lee 2025-10-02 17:16:50 +08:00
parent 719d5a2274
commit deafe6dc63
33 changed files with 1931 additions and 2735 deletions

View File

@ -19,7 +19,6 @@
* 流量控制框架选型Sentinel分布式事务选型Seata。 * 流量控制框架选型Sentinel分布式事务选型Seata。
* 提供了技术栈([Vue3](https://v3.cn.vuejs.org) [Element Plus](https://element-plus.org/zh-CN) [Vite](https://cn.vitejs.dev))版本[RuoYi-Cloud-Vue3](https://gitcode.com/yangzongzhuan/RuoYi-Cloud-Vue3),保持同步更新。 * 提供了技术栈([Vue3](https://v3.cn.vuejs.org) [Element Plus](https://element-plus.org/zh-CN) [Vite](https://cn.vitejs.dev))版本[RuoYi-Cloud-Vue3](https://gitcode.com/yangzongzhuan/RuoYi-Cloud-Vue3),保持同步更新。
* 如需不分离应用,请移步 [RuoYi](https://gitee.com/y_project/RuoYi),如需分离应用,请移步 [RuoYi-Vue](https://gitee.com/y_project/RuoYi-Vue) * 如需不分离应用,请移步 [RuoYi](https://gitee.com/y_project/RuoYi),如需分离应用,请移步 [RuoYi-Vue](https://gitee.com/y_project/RuoYi-Vue)
* 阿里云优惠券:[点我进入](http://aly.ruoyi.vip),腾讯云优惠券:[点我进入](http://txy.ruoyi.vip)  
## 系统模块 ## 系统模块

View File

@ -7,6 +7,7 @@ import com.storm.device.domain.po.Device;
import com.storm.device.domain.vo.DevicePointVO; import com.storm.device.domain.vo.DevicePointVO;
import com.storm.device.domain.vo.DeviceTotalVO; import com.storm.device.domain.vo.DeviceTotalVO;
import com.storm.device.domain.vo.DeviceVo; import com.storm.device.domain.vo.DeviceVo;
import com.storm.device.domain.vo.MassageStatsVO;
import com.storm.device.service.IDeviceService; import com.storm.device.service.IDeviceService;
import com.storm.device.service.IDeviceShadowService; import com.storm.device.service.IDeviceShadowService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
@ -38,6 +39,33 @@ public class DeviceController extends BaseController {
@Autowired @Autowired
private IDeviceShadowService deviceShadowService; private IDeviceShadowService deviceShadowService;
/**
* 获取中心面板统计数据
*/
@Operation(summary = "获取中心面板统计数据")
@GetMapping("/deviceCenter/deviceStats")
public AjaxResult getCenterStats() {
try {
Map<String, Object> stats = deviceService.getCenterStats();
return success(stats);
} catch (Exception e) {
log.error("获取中心面板统计数据失败", e);
return error("获取统计数据失败");
}
}
@Operation(summary = "获取设备使用时长统计")
@GetMapping("/massageInfo/getDeviceUsageStats")
public AjaxResult getDeviceUsageStats() {
try {
Map<String, Object> stats = deviceService.getDeviceUsageStats();
return success(stats);
} catch (Exception e) {
log.error("获取设备使用时长统计失败", e);
return error("获取设备使用时长统计失败");
}
}
/** /**
* 从物联网平台同步设备列表 * 从物联网平台同步设备列表
*/ */
@ -293,4 +321,79 @@ public class DeviceController extends BaseController {
return error("刷新设备状态失败"); return error("刷新设备状态失败");
} }
} }
/**
* 获取设备总按摩时间统计
*/
@Operation(summary = "获取设备总按摩时间统计")
@GetMapping("/massage/getDeviceTotalMassageTime")
public AjaxResult getDeviceTotalMassageTime() {
try {
List<MassageStatsVO> stats = deviceService.getDeviceTotalMassageTime();
return success(stats);
} catch (Exception e) {
log.error("获取设备总按摩时间统计失败", e);
return error("获取设备总按摩时间统计失败");
}
}
@Operation(summary = "获取设备仪表盘综合数据")
@GetMapping("/dashboard/chartData")
public AjaxResult getDashboardChartData() {
try {
Map<String, Object> chartData = deviceService.getDashboardChartData();
return success(chartData);
} catch (Exception e) {
log.error("获取设备仪表盘数据失败", e);
return error("获取仪表盘数据失败");
}
}
@Operation(summary = "获取设备运行统计趋势数据")
@GetMapping("/dashboard/trendData")
public AjaxResult getDeviceTrendData() {
try {
Map<String, Object> trendData = deviceService.getDeviceTrendData();
return success(trendData);
} catch (Exception e) {
log.error("获取设备趋势数据失败", e);
return error("获取趋势数据失败");
}
}
@Operation(summary = "获取中心面板综合数据")
@GetMapping("/deviceCenter/comprehensiveStats")
public AjaxResult getComprehensiveCenterStats() {
try {
Map<String, Object> comprehensiveStats = deviceService.getComprehensiveCenterStats();
return success(comprehensiveStats);
} catch (Exception e) {
log.error("获取中心面板综合数据失败", e);
return error("获取综合数据失败");
}
}
@Operation(summary = "获取设备使用分析数据")
@GetMapping("/deviceUsageAnalysis")
public AjaxResult getDeviceUsageAnalysis() {
try {
Map<String, Object> analysisData = deviceService.getDeviceUsageAnalysis();
return success(analysisData);
} catch (Exception e) {
log.error("获取设备使用分析数据失败", e);
return error("获取分析数据失败");
}
}
@Operation(summary = "获取全国各省份设备数量统计")
@GetMapping("/getDeviceCountByProvince")
public AjaxResult getDeviceCountByProvince() {
try {
List<Map<String, Object>> provinceStats = deviceService.getDeviceCountByProvince();
return success(provinceStats);
} catch (Exception e) {
log.error("获取全国各省份设备数量统计失败", e);
return error("获取统计失败");
}
}
} }

View File

@ -1,31 +0,0 @@
package com.storm.device.controller;
import com.storm.common.core.web.controller.BaseController;
import com.storm.common.core.web.domain.AjaxResult;
import com.storm.device.service.IDeviceStatusLogService;
import com.storm.device.service.IMassageTaskService;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* @author DengAo
* @date 2025/8/29 15:21
*/
@RestController
@RequestMapping("/data")
@Tag(name = "设备数据相关接口")
public class DeviceDataController extends BaseController {
@Resource
private IDeviceStatusLogService deviceStatusLogService;
@Resource
private IMassageTaskService massageTaskService;
}

View File

@ -11,6 +11,7 @@ import com.storm.device.service.IMassageTaskService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import com.storm.common.log.annotation.Log; import com.storm.common.log.annotation.Log;
@ -30,6 +31,7 @@ import com.storm.common.core.web.page.TableDataInfo;
@RestController @RestController
@RequestMapping("/massage") @RequestMapping("/massage")
@Tag(name = "按摩任务记录相关接口") @Tag(name = "按摩任务记录相关接口")
@Slf4j
public class MassageTaskController extends BaseController { public class MassageTaskController extends BaseController {
@Autowired @Autowired
private IMassageTaskService massageTaskService; private IMassageTaskService massageTaskService;
@ -232,4 +234,31 @@ public class MassageTaskController extends BaseController {
{ {
return toAjax(massageTaskService.removeBatchByIds(Arrays.asList(ids))); return toAjax(massageTaskService.removeBatchByIds(Arrays.asList(ids)));
} }
/**
* 获取按摩头类型分布统计用于饼图
*/
@Operation(summary = "获取按摩头类型分布统计")
@GetMapping("/headTypeDistribution")
public AjaxResult getHeadTypeDistribution() {
try {
Map<String, Object> result = massageTaskService.getHeadTypeDistribution();
return AjaxResult.success(result);
} catch (Exception e) {
log.error("获取按摩头类型分布统计失败", e);
return AjaxResult.error("获取统计失败");
}
}
@Operation(summary = "获取雷达图统计数据")
@GetMapping("/radarStats")
public AjaxResult getRadarStats() {
try {
Map<String, Object> radarData = massageTaskService.getRadarStats();
return AjaxResult.success(radarData);
} catch (Exception e) {
log.error("获取雷达图统计数据失败", e);
return AjaxResult.error("获取雷达图统计数据失败");
}
}
} }

View File

@ -0,0 +1,29 @@
// MassageStatsVO.java
package com.storm.device.domain.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.Date;
@Data
@Schema(description = "设备按摩统计响应模型")
public class MassageStatsVO {
@Schema(description = "设备ID")
private String deviceId;
@Schema(description = "设备名称")
private String deviceName;
@Schema(description = "总按摩时长(分钟)")
private Long totalMassageTime;
@Schema(description = "按摩次数")
private Integer massageCount;
@Schema(description = "最后按摩时间")
private Date lastMassageTime;
@Schema(description = "排名")
private Integer rank;
}

View File

@ -1,28 +0,0 @@
package com.storm.device.handler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Component
@Slf4j
public class DuplicateDataHandler {
/**
* 处理重复数据异常
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void handleDuplicateKeyException(Runnable operation, String operationName) {
try {
operation.run();
} catch (DuplicateKeyException e) {
log.warn("重复数据已存在,跳过{}操作", operationName);
// 可以记录到日志或监控系统
} catch (Exception e) {
log.error("{}操作失败", operationName, e);
throw e;
}
}
}

View File

@ -1,182 +0,0 @@
package com.storm.device.manager;
import com.storm.device.mapper.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Service
public class DataConsistencyService {
@Autowired
private DeviceMapper deviceMapper;
@Autowired
private DeviceRuntimeStatsMapper deviceRuntimeStatsMapper;
@Autowired
private MassageTaskMapper massageTaskMapper;
@Autowired
private DeviceHeadUsageMapper deviceHeadUsageMapper;
/**
* 检查设备数据一致性
*/
public Map<String, Object> checkDeviceDataConsistency(String deviceId) {
Map<String, Object> result = new HashMap<>();
try {
// 1. 检查设备总时长与运行统计是否一致
boolean totalDurationConsistent = checkTotalDurationConsistency(deviceId);
result.put("totalDurationConsistent", totalDurationConsistent);
// 2. 检查按头使用统计与按摩任务是否一致
boolean headUsageConsistent = checkHeadUsageConsistency(deviceId);
result.put("headUsageConsistent", headUsageConsistent);
// 3. 检查运行统计与状态日志是否一致
boolean runtimeStatsConsistent = checkRuntimeStatsConsistency(deviceId);
result.put("runtimeStatsConsistent", runtimeStatsConsistent);
result.put("success", true);
result.put("message", "数据一致性检查完成");
} catch (Exception e) {
log.error("检查设备数据一致性失败,设备: {}", deviceId, e);
result.put("success", false);
result.put("message", "检查失败: " + e.getMessage());
}
return result;
}
private boolean checkTotalDurationConsistency(String deviceId) {
try {
// 查询设备表中的总时长
String deviceTotalSql = "SELECT total_online_duration FROM device WHERE device_id = '" + deviceId + "'";
// 执行查询并获取结果
// 查询运行统计表中的总时长
String statsTotalSql = "SELECT COALESCE(SUM(daily_duration), 0) as total_duration " +
"FROM device_runtime_stats WHERE device_id = '" + deviceId + "'";
// 执行查询并获取结果
// 比较两个值是否一致允许微小差异
// Long deviceTotal = ...;
// Long statsTotal = ...;
// return Math.abs(deviceTotal - statsTotal) <= 60; // 允许60秒差异
return true; // 简化处理
} catch (Exception e) {
log.error("检查总时长一致性失败", e);
return false;
}
}
private boolean checkHeadUsageConsistency(String deviceId) {
try {
// 检查按头使用统计与按摩任务数据是否一致
// 实现具体的检查逻辑
return true; // 简化处理
} catch (Exception e) {
log.error("检查按头使用一致性失败", e);
return false;
}
}
private boolean checkRuntimeStatsConsistency(String deviceId) {
try {
// 检查运行统计与状态日志数据是否一致
// 实现具体的检查逻辑
return true; // 简化处理
} catch (Exception e) {
log.error("检查运行统计一致性失败", e);
return false;
}
}
/**
* 修复数据不一致问题
*/
public Map<String, Object> fixDataInconsistency(String deviceId) {
Map<String, Object> result = new HashMap<>();
try {
log.info("开始修复设备数据不一致问题: {}", deviceId);
// 1. 修复总时长不一致
fixTotalDuration(deviceId);
// 2. 修复按头使用统计不一致
fixHeadUsageStatistics(deviceId);
// 3. 修复运行统计不一致
fixRuntimeStatistics(deviceId);
result.put("success", true);
result.put("message", "数据修复完成");
} catch (Exception e) {
log.error("修复设备数据不一致失败,设备: {}", deviceId, e);
result.put("success", false);
result.put("message", "修复失败: " + e.getMessage());
}
return result;
}
private void fixTotalDuration(String deviceId) {
try {
String fixSql = "UPDATE device d " +
"SET total_online_duration = (" +
" SELECT COALESCE(SUM(daily_duration), 0) " +
" FROM device_runtime_stats s " +
" WHERE s.device_id = d.device_id" +
"), " +
"update_time = NOW() " +
"WHERE d.device_id = '" + deviceId + "'";
deviceMapper.executeNativeSQL(fixSql);
log.info("修复设备总时长完成: {}", deviceId);
} catch (Exception e) {
log.error("修复设备总时长失败: {}", deviceId, e);
throw e;
}
}
private void fixHeadUsageStatistics(String deviceId) {
try {
// 删除该设备的按头使用统计
String deleteSql = "DELETE FROM device_head_usage WHERE device_id = '" + deviceId + "'";
deviceHeadUsageMapper.executeNativeSQL(deleteSql);
// 重新生成统计
String insertSql = "INSERT INTO device_head_usage (device_id, head_type, usage_count, total_duration, " +
"daily_duration, weekly_duration, monthly_duration, last_used_time, create_time, update_time) " +
"SELECT device_id, head_type, COUNT(*) as usage_count, " +
"COALESCE(SUM(task_time), 0) as total_duration, " +
"COALESCE(SUM(CASE WHEN DATE(create_time) = CURDATE() THEN task_time ELSE 0 END), 0) as daily_duration, " +
"COALESCE(SUM(CASE WHEN create_time >= DATE_SUB(NOW(), INTERVAL 7 DAY) THEN task_time ELSE 0 END), 0) as weekly_duration, " +
"COALESCE(SUM(CASE WHEN create_time >= DATE_SUB(NOW(), INTERVAL 30 DAY) THEN task_time ELSE 0 END), 0) as monthly_duration, " +
"MAX(create_time) as last_used_time, NOW() as create_time, NOW() as update_time " +
"FROM massage_task " +
"WHERE device_id = '" + deviceId + "' " +
"GROUP BY device_id, head_type";
deviceHeadUsageMapper.executeNativeSQL(insertSql);
log.info("修复按头使用统计完成: {}", deviceId);
} catch (Exception e) {
log.error("修复按头使用统计失败: {}", deviceId, e);
throw e;
}
}
private void fixRuntimeStatistics(String deviceId) {
// 实现运行统计修复逻辑
log.info("修复运行统计: {}", deviceId);
}
}

View File

@ -200,30 +200,4 @@ public class DeviceManagerService {
} }
return deviceId.replaceAll(".*_", ""); return deviceId.replaceAll(".*_", "");
} }
// DeviceManagerService 中添加重试方法
public Device getOrCreateDeviceWithRetry(String originalDeviceId, Consumer<Device> initializer) {
String normalizedDeviceId = normalizeDeviceId(originalDeviceId);
for (int retry = 0; retry < 3; retry++) {
try {
return getOrCreateDevice(normalizedDeviceId, initializer);
} catch (Exception e) {
if (retry == 2) { // 最后一次重试仍然失败
log.error("设备创建重试失败设备ID: {},重试次数: {}", normalizedDeviceId, retry + 1, e);
throw e;
}
log.warn("设备创建失败准备重试设备ID: {},重试次数: {}", normalizedDeviceId, retry + 1);
try {
TimeUnit.MILLISECONDS.sleep(100 * (retry + 1)); // 递增等待
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("设备创建被中断", ie);
}
}
}
throw new RuntimeException("设备创建失败,重试次数用完");
}
} }

View File

@ -9,7 +9,6 @@ import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.util.Date; import java.util.Date;
@Slf4j @Slf4j

View File

@ -1,45 +0,0 @@
package com.storm.device.manager;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
@Service
public class MessageDeduplicationService {
@Resource
private RedisTemplate<String, Object> redisTemplate;
private static final String DEDUP_PREFIX = "msg:dedup:";
private static final int DEDUP_EXPIRE_HOURS = 24;
/**
* 检查消息是否重复
*/
public boolean isDuplicate(String deviceId, String messageKey) {
String redisKey = DEDUP_PREFIX + deviceId + ":" + messageKey;
Boolean exists = redisTemplate.hasKey(redisKey);
if (exists != null && exists) {
return true;
}
// 设置过期时间
redisTemplate.opsForValue().set(redisKey, "1", DEDUP_EXPIRE_HOURS, TimeUnit.HOURS);
return false;
}
/**
* 生成按摩任务消息唯一键
*/
public String generateMassageTaskKey(String deviceId, Long startTime, String headType, String bodyPart) {
return String.format("massage:%s:%d:%s:%s", deviceId, startTime, headType, bodyPart);
}
/**
* 生成状态消息唯一键
*/
public String generateStatusKey(String deviceId, Long eventTime, String status) {
return String.format("status:%s:%d:%s", deviceId, eventTime, status);
}
}

View File

@ -20,13 +20,6 @@ public interface DeviceHeadUsageMapper extends BaseMapper<DeviceHeadUsage>
// 在DeviceHeadUsageMapper.java中添加 // 在DeviceHeadUsageMapper.java中添加
@Update("${sql}") @Update("${sql}")
void executeNativeSQL(@Param("sql") String sql); void executeNativeSQL(@Param("sql") String sql);
/**
* 查询设备按头使用统计
*
* @param id 设备按头使用统计主键
* @return 设备按头使用统计
*/
public DeviceHeadUsage selectDeviceHeadUsageById(Long id);
/** /**
* 查询设备按头使用统计列表 * 查询设备按头使用统计列表
@ -34,37 +27,5 @@ public interface DeviceHeadUsageMapper extends BaseMapper<DeviceHeadUsage>
* @param deviceHeadUsage 设备按头使用统计 * @param deviceHeadUsage 设备按头使用统计
* @return 设备按头使用统计集合 * @return 设备按头使用统计集合
*/ */
public List<DeviceHeadUsage> selectDeviceHeadUsageList(DeviceHeadUsage deviceHeadUsage); List<DeviceHeadUsage> selectDeviceHeadUsageList(DeviceHeadUsage deviceHeadUsage);
/**
* 新增设备按头使用统计
*
* @param deviceHeadUsage 设备按头使用统计
* @return 结果
*/
public int insertDeviceHeadUsage(DeviceHeadUsage deviceHeadUsage);
/**
* 修改设备按头使用统计
*
* @param deviceHeadUsage 设备按头使用统计
* @return 结果
*/
public int updateDeviceHeadUsage(DeviceHeadUsage deviceHeadUsage);
/**
* 删除设备按头使用统计
*
* @param id 设备按头使用统计主键
* @return 结果
*/
public int deleteDeviceHeadUsageById(Long id);
/**
* 批量删除设备按头使用统计
*
* @param ids 需要删除的数据主键集合
* @return 结果
*/
public int deleteDeviceHeadUsageByIds(Long[] ids);
} }

View File

@ -1,9 +1,12 @@
package com.storm.device.mapper; package com.storm.device.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.storm.device.domain.vo.MassageStatsVO;
import org.apache.ibatis.annotations.*; import org.apache.ibatis.annotations.*;
import java.util.List; import java.util.List;
import java.util.Map;
import com.storm.device.domain.po.Device; import com.storm.device.domain.po.Device;
/** /**
@ -23,24 +26,18 @@ public interface DeviceMapper extends BaseMapper<Device>
*/ */
List<Device> selectDeviceList(Device device); List<Device> selectDeviceList(Device device);
/**
* 使用INSERT IGNORE插入设备避免重复键异常
*/
@Insert("INSERT IGNORE INTO device (device_id, device_name, product_id, status, " +
"total_online_duration, daily_duration, weekly_duration, monthly_duration, " +
"is_deleted, create_by, create_time) " +
"VALUES (#{deviceId}, #{deviceName}, #{productId}, #{status}, " +
"#{totalOnlineDuration}, #{dailyDuration}, #{weeklyDuration}, #{monthlyDuration}, " +
"#{isDeleted}, #{createBy}, #{createTime})")
int insertWithIgnore(Device device);
// 在DeviceMapper.java中添加 // 在DeviceMapper.java中添加
@Update("${sql}") @Update("${sql}")
void executeNativeSQL(@Param("sql") String sql); void executeNativeSQL(@Param("sql") String sql);
/** /**
* 使用SELECT FOR UPDATE查询设备行级锁 * 查询设备按摩时长统计
* @return 设备按摩统计列表
*/ */
@Select("SELECT * FROM device WHERE device_id = #{deviceId} FOR UPDATE") List<MassageStatsVO> selectDeviceMassageStats();
Device selectOneForUpdate(@Param("deviceId") String deviceId);
/**
* 查询各省份设备数量统计
*/
List<Map<String, Object>> selectDeviceCountByProvince();
} }

View File

@ -1,14 +1,11 @@
// DeviceRuntimeStatsMapper.java
package com.storm.device.mapper; package com.storm.device.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.storm.device.domain.po.DeviceRuntimeStats; import com.storm.device.domain.po.DeviceRuntimeStats;
import com.storm.device.provider.DeviceRuntimeStatsSqlProvider; import com.storm.device.provider.DeviceRuntimeStatsSqlProvider;
import org.apache.ibatis.annotations.*; import org.apache.ibatis.annotations.*;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map;
@Mapper @Mapper
public interface DeviceRuntimeStatsMapper extends BaseMapper<DeviceRuntimeStats> { public interface DeviceRuntimeStatsMapper extends BaseMapper<DeviceRuntimeStats> {
@ -16,38 +13,6 @@ public interface DeviceRuntimeStatsMapper extends BaseMapper<DeviceRuntimeStats>
@Select("SELECT * FROM device_runtime_stats WHERE device_id = #{deviceId} AND stat_date = #{statDate}") @Select("SELECT * FROM device_runtime_stats WHERE device_id = #{deviceId} AND stat_date = #{statDate}")
DeviceRuntimeStats selectByDeviceAndDate(@Param("deviceId") String deviceId, @Param("statDate") Date statDate); DeviceRuntimeStats selectByDeviceAndDate(@Param("deviceId") String deviceId, @Param("statDate") Date statDate);
/**
* 插入或更新设备运行统计
*/
@Insert({"<script>",
"INSERT INTO device_runtime_stats (device_id, stat_date, daily_duration, weekly_duration, ",
"monthly_duration, online_count, create_time, update_time, create_by) ",
"VALUES (#{deviceId}, #{statDate}, #{dailyDuration}, #{weeklyDuration}, ",
"#{monthlyDuration}, #{onlineCount}, #{createTime}, #{updateTime}, #{createBy}) ",
"ON DUPLICATE KEY UPDATE ",
"daily_duration = daily_duration + VALUES(daily_duration), ",
"weekly_duration = weekly_duration + VALUES(weekly_duration), ",
"monthly_duration = monthly_duration + VALUES(monthly_duration), ",
"online_count = online_count + VALUES(online_count), ",
"update_time = VALUES(update_time)",
"</script>"})
boolean insertOrUpdateStats(@Param("deviceId") String deviceId,
@Param("statDate") Date statDate,
@Param("dailyDuration") Long dailyDuration,
@Param("weeklyDuration") Long weeklyDuration,
@Param("monthlyDuration") Long monthlyDuration,
@Param("onlineCount") Integer onlineCount,
@Param("createTime") Date createTime,
@Param("updateTime") Date updateTime,
@Param("createBy") String createBy);
@Select("SELECT stat_date as date, daily_duration as duration FROM device_runtime_stats " +
"WHERE device_id = #{deviceId} AND stat_date BETWEEN #{startDate} AND #{endDate} " +
"ORDER BY stat_date")
List<Map<String, Object>> selectDailyStatsByPeriod(@Param("deviceId") String deviceId,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate);
// DeviceRuntimeStatsMapper.java 中添加 // DeviceRuntimeStatsMapper.java 中添加
@Insert("INSERT INTO device_runtime_stats (device_id, stat_date, daily_duration, weekly_duration, " + @Insert("INSERT INTO device_runtime_stats (device_id, stat_date, daily_duration, weekly_duration, " +
"monthly_duration, online_count, create_time, update_time, create_by) " + "monthly_duration, online_count, create_time, update_time, create_by) " +
@ -61,23 +26,6 @@ public interface DeviceRuntimeStatsMapper extends BaseMapper<DeviceRuntimeStats>
"update_time = VALUES(update_time)") "update_time = VALUES(update_time)")
int insertOrUpdate(DeviceRuntimeStats stats); int insertOrUpdate(DeviceRuntimeStats stats);
// 批量插入或更新
@Insert("<script>" +
"INSERT INTO device_runtime_stats (device_id, stat_date, daily_duration, weekly_duration, " +
"monthly_duration, online_count, create_time, update_time, create_by) VALUES " +
"<foreach collection='list' item='item' separator=','>" +
"(#{item.deviceId}, #{item.statDate}, #{item.dailyDuration}, #{item.weeklyDuration}, " +
"#{item.monthlyDuration}, #{item.onlineCount}, #{item.createTime}, #{item.updateTime}, #{item.createBy})" +
"</foreach>" +
"ON DUPLICATE KEY UPDATE " +
"daily_duration = daily_duration + VALUES(daily_duration), " +
"weekly_duration = weekly_duration + VALUES(weekly_duration), " +
"monthly_duration = monthly_duration + VALUES(monthly_duration), " +
"online_count = online_count + VALUES(online_count), " +
"update_time = VALUES(update_time)" +
"</script>")
int batchInsertOrUpdate(@Param("list") List<DeviceRuntimeStats> statsList);
@Select("SELECT SUM(daily_duration) as totalDuration FROM device_runtime_stats " + @Select("SELECT SUM(daily_duration) as totalDuration FROM device_runtime_stats " +
"WHERE device_id = #{deviceId} AND stat_date BETWEEN #{startDate} AND #{endDate}") "WHERE device_id = #{deviceId} AND stat_date BETWEEN #{startDate} AND #{endDate}")
Long selectTotalDurationByPeriod(@Param("deviceId") String deviceId, Long selectTotalDurationByPeriod(@Param("deviceId") String deviceId,
@ -116,27 +64,6 @@ public interface DeviceRuntimeStatsMapper extends BaseMapper<DeviceRuntimeStats>
Integer selectOnlineCountByPeriod(@Param("deviceId") String deviceId, Integer selectOnlineCountByPeriod(@Param("deviceId") String deviceId,
@Param("startDate") Date startDate, @Param("startDate") Date startDate,
@Param("endDate") Date endDate); @Param("endDate") Date endDate);
// 2. 更新周统计时长
@Update("UPDATE device_runtime_stats " +
"SET weekly_duration = weekly_duration + #{duration} " +
"WHERE device_id = #{deviceId} " +
"AND stat_date BETWEEN #{weekStart} AND #{statDate}")
void updateWeeklyDuration(@Param("deviceId") String deviceId,
@Param("weekStart") Date weekStart,
@Param("statDate") Date statDate,
@Param("duration") Long duration);
// 3. 更新月统计时长
@Update("UPDATE device_runtime_stats " +
"SET monthly_duration = monthly_duration + #{duration} " +
"WHERE device_id = #{deviceId} " +
"AND stat_date BETWEEN #{monthStart} AND #{statDate}")
void updateMonthlyDuration(@Param("deviceId") String deviceId,
@Param("monthStart") Date monthStart,
@Param("statDate") Date statDate,
@Param("duration") Long duration);
// 4. 查询设备运行统计列表 // 4. 查询设备运行统计列表
@SelectProvider(type = DeviceRuntimeStatsSqlProvider.class, method = "selectDeviceRuntimeStatsList") @SelectProvider(type = DeviceRuntimeStatsSqlProvider.class, method = "selectDeviceRuntimeStatsList")
List<DeviceRuntimeStats> selectDeviceRuntimeStatsList(DeviceRuntimeStats deviceRuntimeStats); List<DeviceRuntimeStats> selectDeviceRuntimeStatsList(DeviceRuntimeStats deviceRuntimeStats);

View File

@ -5,7 +5,6 @@ import com.storm.device.domain.po.DeviceStatusLog;
import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Select;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -108,15 +107,4 @@ public interface DeviceStatusLogMapper extends BaseMapper<DeviceStatusLog> {
"duration = VALUES(duration), " + "duration = VALUES(duration), " +
"update_time = VALUES(update_time)") "update_time = VALUES(update_time)")
int insertOrUpdate(DeviceStatusLog deviceStatusLog); int insertOrUpdate(DeviceStatusLog deviceStatusLog);
// DeviceStatusLogMapper.java 中添加
@Insert("<script>" +
"INSERT IGNORE INTO device_status_log (device_id, status, event_type, event_time, " +
"last_online_time, duration, create_time, update_time, create_by) VALUES " +
"<foreach collection='list' item='item' separator=','>" +
"(#{item.deviceId}, #{item.status}, #{item.eventType}, #{item.eventTime}, " +
"#{item.lastOnlineTime}, #{item.duration}, #{item.createTime}, #{item.updateTime}, #{item.createBy})" +
"</foreach>" +
"</script>")
int batchInsertIgnore(@Param("list") java.util.List<DeviceStatusLog> statusLogs);
} }

View File

@ -5,6 +5,7 @@ import com.storm.device.domain.dto.MassageTaskStatDTO;
import com.storm.device.domain.po.MassageTask; import com.storm.device.domain.po.MassageTask;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -41,4 +42,117 @@ public interface MassageTaskMapper extends BaseMapper<MassageTask> {
Long getReportEvent(); Long getReportEvent();
List<MassageTask> selectMassageTaskList(MassageTask massageTask); List<MassageTask> selectMassageTaskList(MassageTask massageTask);
/**
* 获取今日按摩总时长
*/
Long getTodayMassageDuration();
/**
* 获取最大日按摩时长
*/
Long getMaxDailyDuration();
/**
* 获取每日按摩趋势数据
*/
List<Map<String, Object>> getDailyMassageTrend(@Param("startTime") String startTime,
@Param("endTime") String endTime);
/**
* 获取设备使用排名
*/
@Select("SELECT device_name as name, COUNT(*) as value " +
"FROM massage_task " +
"WHERE is_deleted = 0 " +
"GROUP BY device_id, device_name " +
"ORDER BY value DESC " +
"LIMIT #{limit}")
List<Map<String, Object>> getDeviceUsageRanking(@Param("limit") int limit);
/**
* 获取雷达图基础统计数据
*/
@Select("SELECT " +
"MAX(massage_count) as maxMassageCount, " +
"MAX(total_duration) as maxTotalDuration, " +
"MAX(head_type_count) as maxHeadTypes, " +
"MAX(body_part_count) as maxBodyParts, " +
"MAX(avg_duration) as maxAvgDuration, " +
"MAX(usage_frequency) as maxUsageFrequency " +
"FROM (" +
" SELECT " +
" COUNT(*) as massage_count, " +
" SUM(task_time) as total_duration, " +
" COUNT(DISTINCT head_type) as head_type_count, " +
" COUNT(DISTINCT body_part) as body_part_count, " +
" AVG(task_time) as avg_duration, " +
" COUNT(*) / (DATEDIFF(MAX(create_time), MIN(create_time)) + 1) as usage_frequency " +
" FROM massage_task " +
" WHERE is_deleted = 0 " +
" GROUP BY device_id" +
") as stats")
Map<String, Object> getRadarBaseStats();
/**
* 获取前N个设备的雷达图数据
*/
@Select("SELECT " +
"device_id, " +
"device_name, " +
"COUNT(*) as massageCount, " +
"SUM(task_time) as totalDuration, " +
"COUNT(DISTINCT head_type) as headTypeCount, " +
"COUNT(DISTINCT body_part) as bodyPartCount, " +
"AVG(task_time) as avgDuration, " +
"COUNT(*) / (DATEDIFF(MAX(create_time), MIN(create_time)) + 1) as usageFrequency " +
"FROM massage_task " +
"WHERE is_deleted = 0 " +
"GROUP BY device_id, device_name " +
"ORDER BY massageCount DESC " +
"LIMIT #{limit}")
List<Map<String, Object>> getTopDevicesForRadar(@Param("limit") int limit);
/**
* 获取按摩统计范围
*/
@Select("SELECT " +
"AVG(massage_count) as avgMassageCount, " +
"AVG(total_duration) as avgDuration " +
"FROM (" +
" SELECT COUNT(*) as massage_count, SUM(task_time) as total_duration " +
" FROM massage_task WHERE is_deleted = 0 GROUP BY device_id" +
") as device_stats")
Map<String, Object> getMassageStatsRange();
/**
* 获取本月按摩总时长分钟
*/
@Select("SELECT COALESCE(SUM(task_time) / 60000, 0) as monthly_duration " +
"FROM massage_task " +
"WHERE is_deleted = 0 " +
"AND YEAR(create_time) = YEAR(CURDATE()) " +
"AND MONTH(create_time) = MONTH(CURDATE())")
Long selectMonthlyMassageDuration();
/**
* 获取活跃设备数量指定天数内有按摩任务的设备
*/
@Select("SELECT COUNT(DISTINCT device_id) as active_count " +
"FROM massage_task " +
"WHERE is_deleted = 0 " +
"AND create_time >= DATE_SUB(CURDATE(), INTERVAL #{days} DAY)")
Long selectActiveDeviceCount(@Param("days") int days);
/**
* 基于设备表生成真实排名数据
*/
@Select("SELECT d.device_name as name, COUNT(m.id) as value " +
"FROM device d " +
"LEFT JOIN massage_task m ON d.device_id = m.device_id AND m.is_deleted = 0 " +
"WHERE d.is_deleted = 0 " +
"GROUP BY d.device_id, d.device_name " +
"ORDER BY value DESC " +
"LIMIT 10")
List<Map<String, Object>> getDeviceUsageRankingFromDevices();
} }

View File

@ -13,6 +13,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.util.Date; import java.util.Date;
import java.util.Map; import java.util.Map;
@ -28,9 +29,6 @@ public class DeviceDataCoordinator {
@Autowired @Autowired
private MassageTaskMapper massageTaskMapper; private MassageTaskMapper massageTaskMapper;
@Autowired
private DeviceRuntimeStatsMapper deviceRuntimeStatsMapper;
@Autowired @Autowired
private DeviceHeadUsageMapper deviceHeadUsageMapper; private DeviceHeadUsageMapper deviceHeadUsageMapper;
@ -48,6 +46,33 @@ public class DeviceDataCoordinator {
@Autowired @Autowired
private DeviceRuntimeStatsService deviceRuntimeStatsService; private DeviceRuntimeStatsService deviceRuntimeStatsService;
/**
* 统一处理设备数据 - 调用协调器确保所有表同步
*/
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void processDeviceData(IotMsgNotifyDataPro iotMsgNotifyData) {
if (iotMsgNotifyData == null || iotMsgNotifyData.getHeader() == null) {
log.warn("接收到的设备信息数据为空");
return;
}
String deviceId = iotMsgNotifyData.getHeader().getDeviceId();
if (StrUtil.isBlank(deviceId)) {
log.warn("设备ID为空跳过数据处理");
return;
}
try {
// 使用协调器统一处理所有表数据
coordinateDeviceData(iotMsgNotifyData);
log.info("设备数据处理完成: {}", deviceId);
} catch (Exception e) {
log.error("处理设备数据时发生异常设备ID: {}", deviceId, e);
throw new RuntimeException("设备数据处理失败", e);
}
}
/** /**
* 确保设备存在 * 确保设备存在
*/ */
@ -160,35 +185,6 @@ public class DeviceDataCoordinator {
} }
} }
/**
* 更新设备总统计
*/
private void updateDeviceTotalStatistics(Device device, IotMsgNotifyDataPro iotMsgNotifyData) {
try {
Long duration = calculateMassageDuration(iotMsgNotifyData);
if (duration > 0) {
// 更新设备表中的累计时长
device.setTotalOnlineDuration(device.getTotalOnlineDuration() + duration);
// 更新日统计
if (isSameDay(device.getUpdateTime(), new Date())) {
device.setDailyDuration(device.getDailyDuration() + duration);
} else {
device.setDailyDuration(duration);
}
// 更新周月统计简化处理实际应该按周月重置
device.setWeeklyDuration(device.getWeeklyDuration() + duration);
device.setMonthlyDuration(device.getMonthlyDuration() + duration);
device.setUpdateBy("系统自动修改");
device.setUpdateTime(new Date());
deviceMapper.updateById(device);
}
} catch (Exception e) {
log.error("更新设备总统计失败,设备: {}", device.getDeviceId(), e);
}
}
// ========== 辅助方法 ========== // ========== 辅助方法 ==========
private boolean acquireLock(String lockKey) { private boolean acquireLock(String lockKey) {
@ -210,22 +206,6 @@ public class DeviceDataCoordinator {
} }
} }
private DeviceStatusLog createStatusLog(String deviceId, IotMsgNotifyDataPro iotMsgNotifyData) {
// 实现创建状态日志的逻辑从您现有的代码中提取
DeviceStatusLog statusLog = new DeviceStatusLog();
statusLog.setDeviceId(deviceId);
statusLog.setCreateTime(new Date());
if (iotMsgNotifyData.getBody() != null) {
String status = iotMsgNotifyData.getBody().getStatus();
statusLog.setStatus("ONLINE".equalsIgnoreCase(status) ? "ONLINE" : "OFFLINE");
statusLog.setEventTime(iotMsgNotifyData.getBody().getStatusUpdateTime() != null ?
iotMsgNotifyData.getBody().getStatusUpdateTime() : new Date());
}
return statusLog;
}
private boolean isValidMassageTaskData(String deviceId, Map<String, Object> props) { private boolean isValidMassageTaskData(String deviceId, Map<String, Object> props) {
return props.containsKey("massage_start_time") && return props.containsKey("massage_start_time") &&
props.containsKey("massage_end_time") && props.containsKey("massage_end_time") &&
@ -300,23 +280,6 @@ public class DeviceDataCoordinator {
"ONLINE".equalsIgnoreCase(iotMsgNotifyData.getBody().getStatus()); "ONLINE".equalsIgnoreCase(iotMsgNotifyData.getBody().getStatus());
} }
private void updateDeviceTotalDuration(Device device, Long duration) {
if (duration > 0) {
device.setTotalOnlineDuration(device.getTotalOnlineDuration() + duration);
deviceMapper.updateById(device);
}
}
private boolean isSameDay(Date date1, Date date2) {
if (date1 == null || date2 == null) return false;
java.util.Calendar cal1 = java.util.Calendar.getInstance();
java.util.Calendar cal2 = java.util.Calendar.getInstance();
cal1.setTime(date1);
cal2.setTime(date2);
return cal1.get(java.util.Calendar.YEAR) == cal2.get(java.util.Calendar.YEAR) &&
cal1.get(java.util.Calendar.DAY_OF_YEAR) == cal2.get(java.util.Calendar.DAY_OF_YEAR);
}
private String safeGetString(Map<String, Object> props, String key) { private String safeGetString(Map<String, Object> props, String key) {
if (props == null || !props.containsKey(key)) return null; if (props == null || !props.containsKey(key)) return null;
Object value = props.get(key); Object value = props.get(key);
@ -337,51 +300,6 @@ public class DeviceDataCoordinator {
return null; return null;
} }
/**
* 备用方案先查询后更新
*/
private void fallbackUpdateRuntimeStatistics(Device device, IotMsgNotifyDataPro iotMsgNotifyData) {
try {
Date today = new Date();
// 先查询是否存在
DeviceRuntimeStats existingStats = deviceRuntimeStatsMapper.selectByDeviceAndDate(device.getDeviceId(), today);
Long durationToAdd = calculateMassageDuration(iotMsgNotifyData);
Integer onlineCountToAdd = shouldCountOnline(iotMsgNotifyData) ? 1 : 0;
if (existingStats == null) {
// 插入新记录
DeviceRuntimeStats newStats = new DeviceRuntimeStats();
newStats.setDeviceId(device.getDeviceId());
newStats.setStatDate(today);
newStats.setDailyDuration(durationToAdd);
newStats.setWeeklyDuration(durationToAdd);
newStats.setMonthlyDuration(durationToAdd);
newStats.setOnlineCount(onlineCountToAdd);
newStats.setCreateTime(new Date());
newStats.setUpdateTime(new Date());
newStats.setCreateBy("系统自动创建");
deviceRuntimeStatsMapper.insert(newStats);
} else {
// 更新现有记录
existingStats.setDailyDuration(existingStats.getDailyDuration() + durationToAdd);
existingStats.setWeeklyDuration(existingStats.getWeeklyDuration() + durationToAdd);
existingStats.setMonthlyDuration(existingStats.getMonthlyDuration() + durationToAdd);
existingStats.setOnlineCount(existingStats.getOnlineCount() + onlineCountToAdd);
existingStats.setUpdateTime(new Date());
deviceRuntimeStatsMapper.updateById(existingStats);
}
log.info("备用方案更新运行统计成功: {}", device.getDeviceId());
} catch (Exception e) {
log.error("备用方案更新运行统计也失败,设备: {}", device.getDeviceId(), e);
}
}
/** /**
* 处理设备状态变更 - 最终修复版本 * 处理设备状态变更 - 最终修复版本
*/ */
@ -462,14 +380,6 @@ public class DeviceDataCoordinator {
if (hasNewLongitude && hasNewLatitude) { if (hasNewLongitude && hasNewLatitude) {
try { try {
// TODO 基于经纬度获取省市位置 // TODO 基于经纬度获取省市位置
// String location = getLocationFromCoordinates(
// Double.parseDouble(newLatitude),
// Double.parseDouble(newLongitude)
// );
// if (!"Unknown location".equals(location) && !"Error fetching location".equals(location)) {
// device.setLocation(location);
// log.debug("更新设备位置地址: {} -> {}", device.getDeviceId(), location);
// }
} catch (Exception e) { } catch (Exception e) {
log.warn("获取设备位置地址失败: {}", device.getDeviceId(), e); log.warn("获取设备位置地址失败: {}", device.getDeviceId(), e);
} }

View File

@ -8,7 +8,7 @@ import com.storm.device.domain.po.Device;
import com.storm.device.domain.vo.DevicePointVO; import com.storm.device.domain.vo.DevicePointVO;
import com.storm.device.domain.vo.DeviceTotalVO; import com.storm.device.domain.vo.DeviceTotalVO;
import com.storm.device.domain.vo.DeviceVo; import com.storm.device.domain.vo.DeviceVo;
import com.storm.device.task.vo.IotMsgNotifyDataPro; import com.storm.device.domain.vo.MassageStatsVO;
/** /**
* 设备基本信息Service接口 * 设备基本信息Service接口
@ -25,7 +25,7 @@ public interface IDeviceService extends IService<Device>
* @param device 设备基本信息 * @param device 设备基本信息
* @return 设备基本信息集合 * @return 设备基本信息集合
*/ */
public List<Device> exportDeviceList(Device device); List<Device> exportDeviceList(Device device);
/** /**
* 批量删除设备基本信息 * 批量删除设备基本信息
@ -33,7 +33,7 @@ public interface IDeviceService extends IService<Device>
* @param ids 需要删除的设备基本信息主键集合 * @param ids 需要删除的设备基本信息主键集合
* @return 结果 * @return 结果
*/ */
public int deleteDeviceByIds(Long[] ids); int deleteDeviceByIds(Long[] ids);
void syncDeviceList(); void syncDeviceList();
@ -46,6 +46,35 @@ public interface IDeviceService extends IService<Device>
DeviceTotalVO getTotal(); DeviceTotalVO getTotal();
List<DevicePointVO> getDeviceCoordinate(); List<DevicePointVO> getDeviceCoordinate();
/**
* 获取设备总按摩时间统计
* @return 按摩统计列表
*/
List<MassageStatsVO> getDeviceTotalMassageTime();
void updateDeviceInfo(IotMsgNotifyDataPro iotMsgNotifyData); Map<String, Object> getCenterStats();
/**
* 获取设备使用时长统计
*/
Map<String, Object> getDeviceUsageStats();
/**
* 获取仪表盘综合图表数据
*/
Map<String, Object> getDashboardChartData();
/**
* 获取设备运行趋势数据
*/
Map<String, Object> getDeviceTrendData();
Map<String, Object> getComprehensiveCenterStats();
Map<String, Object> getDeviceUsageAnalysis();
Map<String, Object> getDeviceUsageStatsCount();
/**
* 获取全国各省份设备数量统计
*/
List<Map<String, Object>> getDeviceCountByProvince();
} }

View File

@ -17,6 +17,5 @@ public interface IDeviceStatusLogService extends IService<DeviceStatusLog>
List<DeviceStatusLog> selectDeviceStatusLogList(DeviceStatusLog deviceStatusLog); List<DeviceStatusLog> selectDeviceStatusLogList(DeviceStatusLog deviceStatusLog);
void updateDeviceStatusLog(IotMsgNotifyDataPro iotMsgNotifyData);
boolean safeInsertStatusLog(String deviceId, IotMsgNotifyDataPro iotMsgNotifyData); boolean safeInsertStatusLog(String deviceId, IotMsgNotifyDataPro iotMsgNotifyData);
} }

View File

@ -5,7 +5,6 @@ import com.baomidou.mybatisplus.extension.service.IService;
import com.storm.common.core.web.domain.AjaxResult; import com.storm.common.core.web.domain.AjaxResult;
import com.storm.device.domain.dto.MassageTaskStatDTO; import com.storm.device.domain.dto.MassageTaskStatDTO;
import com.storm.device.domain.po.MassageTask; import com.storm.device.domain.po.MassageTask;
import com.storm.device.task.vo.IotMsgNotifyDataPro;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -38,5 +37,7 @@ public interface IMassageTaskService extends IService<MassageTask> {
List<MassageTask> selectMassageTaskList(MassageTask massageTask); List<MassageTask> selectMassageTaskList(MassageTask massageTask);
void updateMassageTask(IotMsgNotifyDataPro iotMsgNotifyData); Map<String, Object> getHeadTypeDistribution();
Map<String, Object> getRadarStats();
} }

View File

@ -1,13 +1,9 @@
package com.storm.device.service.impl; package com.storm.device.service.impl;
import java.util.*; import java.util.*;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.storm.device.domain.po.Device;
import com.storm.device.domain.po.DeviceHeadUsage;
import com.storm.device.domain.po.DeviceStatusLog; import com.storm.device.domain.po.DeviceStatusLog;
import com.storm.device.domain.po.MassageTask;
import com.storm.device.manager.DeviceManagerService; import com.storm.device.manager.DeviceManagerService;
import com.storm.device.mapper.DeviceStatusLogMapper; import com.storm.device.mapper.DeviceStatusLogMapper;
import com.storm.device.service.IDeviceStatusLogService; import com.storm.device.service.IDeviceStatusLogService;
@ -44,115 +40,6 @@ public class DeviceStatusLogServiceImpl extends ServiceImpl<DeviceStatusLogMappe
.eq(deviceStatusLog.getDuration() != null, DeviceStatusLog::getDuration, deviceStatusLog.getDuration())); .eq(deviceStatusLog.getDuration() != null, DeviceStatusLog::getDuration, deviceStatusLog.getDuration()));
} }
/**
* 更新设备状态日志 - 使用统一设备管理
*/
@Override
public void updateDeviceStatusLog(IotMsgNotifyDataPro iotMsgNotifyData) {
if (iotMsgNotifyData == null || iotMsgNotifyData.getHeader() == null) {
return;
}
String originalDeviceId = iotMsgNotifyData.getHeader().getDeviceId();
if (StrUtil.isBlank(originalDeviceId)) {
log.warn("设备ID为空跳过状态日志处理");
return;
}
try {
// 确保设备存在
Device device = deviceManagerService.getOrCreateDevice(originalDeviceId, null);
String normalizedDeviceId = device.getDeviceId();
// 检查是否已存在相同记录
if (isDuplicateStatusLog(normalizedDeviceId, iotMsgNotifyData)) {
log.debug("重复的状态日志,跳过处理: {}", normalizedDeviceId);
return;
}
DeviceStatusLog statusLog = createStatusLog(normalizedDeviceId, iotMsgNotifyData);
boolean saveResult = this.save(statusLog);
if (saveResult) {
log.info("设备状态日志保存成功,设备: {},状态: {}", normalizedDeviceId, statusLog.getStatus());
} else {
log.error("设备状态日志保存失败,设备: {}", normalizedDeviceId);
}
} catch (Exception e) {
log.error("处理设备状态日志时发生异常", e);
}
}
/**
* 检查是否重复状态日志
*/
private boolean isDuplicateStatusLog(String deviceId, IotMsgNotifyDataPro iotMsgNotifyData) {
if (iotMsgNotifyData.getBody() == null) {
return false;
}
LambdaQueryWrapper<DeviceStatusLog> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DeviceStatusLog::getDeviceId, deviceId);
if (iotMsgNotifyData.getBody().getStatusUpdateTime() != null) {
queryWrapper.eq(DeviceStatusLog::getEventTime, iotMsgNotifyData.getBody().getStatusUpdateTime());
}
if (StrUtil.isNotBlank(iotMsgNotifyData.getBody().getStatus())) {
queryWrapper.eq(DeviceStatusLog::getStatus, iotMsgNotifyData.getBody().getStatus());
}
return this.count(queryWrapper) > 0;
}
/**
* 创建状态日志记录
*/
private DeviceStatusLog createStatusLog(String deviceId, IotMsgNotifyDataPro iotMsgNotifyData) {
DeviceStatusLog statusLog = new DeviceStatusLog();
statusLog.setDeviceId(deviceId);
statusLog.setCreateTime(new Date());
statusLog.setEventType("STATUS_CHANGE");
if (iotMsgNotifyData.getBody() != null) {
// 处理状态信息
if (StrUtil.isNotBlank(iotMsgNotifyData.getBody().getStatus())) {
String status = iotMsgNotifyData.getBody().getStatus();
statusLog.setStatus("ONLINE".equalsIgnoreCase(status) ? "ONLINE" : "OFFLINE");
}
// 处理事件时间
if (iotMsgNotifyData.getBody().getStatusUpdateTime() != null) {
try {
Date statusUpdateTime = iotMsgNotifyData.getBody().getStatusUpdateTime();
statusLog.setEventTime(statusUpdateTime);
} catch (Exception e) {
log.warn("状态更新时间解析失败,使用当前时间: {}", iotMsgNotifyData.getBody().getStatusUpdateTime());
statusLog.setEventTime(new Date());
}
} else {
statusLog.setEventTime(new Date());
}
// 处理最后在线时间
if (iotMsgNotifyData.getBody().getLastOnlineTime() != null) {
try {
Date lastOnlineTime = iotMsgNotifyData.getBody().getLastOnlineTime();
statusLog.setLastOnlineTime(lastOnlineTime);
} catch (Exception e) {
log.warn("最后在线时间解析失败: {}", iotMsgNotifyData.getBody().getLastOnlineTime());
}
}
// 处理服务数据中的额外属性
if (iotMsgNotifyData.getBody().getServices() != null) {
processServiceProperties(statusLog, iotMsgNotifyData.getBody().getServices());
}
}
return statusLog;
}
/** /**
* 处理服务属性 * 处理服务属性
*/ */
@ -491,26 +378,6 @@ public class DeviceStatusLogServiceImpl extends ServiceImpl<DeviceStatusLogMappe
} }
} }
private Long parseTimestamp(Object timestampObj) {
if (timestampObj == null) return null;
try {
if (timestampObj instanceof Number) {
long timestamp = ((Number) timestampObj).longValue();
// 判断是秒级还是毫秒级
if (timestamp < 10000000000L) { // 秒级
return timestamp * 1000;
} else { // 毫秒级
return timestamp;
}
} else if (timestampObj instanceof String) {
return Long.parseLong(timestampObj.toString());
}
} catch (Exception e) {
log.warn("时间戳解析失败: {}", timestampObj);
}
return null;
}
/** /**
* 安全解析时间戳 - 修复版本 * 安全解析时间戳 - 修复版本
*/ */
@ -612,47 +479,4 @@ public class DeviceStatusLogServiceImpl extends ServiceImpl<DeviceStatusLogMappe
return timestamp >= minValidTimestamp && timestamp <= maxValidTimestamp; return timestamp >= minValidTimestamp && timestamp <= maxValidTimestamp;
} }
// DeviceStatusLogService 中添加更精确的检查方法
/**
* 精确检查状态日志是否重复
*/
private boolean isExactDuplicate(String deviceId, IotMsgNotifyDataPro iotMsgNotifyData) {
try {
if (iotMsgNotifyData.getBody() == null) {
return false;
}
LambdaQueryWrapper<DeviceStatusLog> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DeviceStatusLog::getDeviceId, deviceId);
// 精确匹配事件时间精确到秒
if (iotMsgNotifyData.getBody().getStatusUpdateTime() != null) {
// 将时间精确到秒级进行比较
Date eventTime = iotMsgNotifyData.getBody().getStatusUpdateTime();
long eventTimeSeconds = eventTime.getTime() / 1000;
queryWrapper.apply("UNIX_TIMESTAMP(event_time) = {0}", eventTimeSeconds);
}
// 精确匹配状态
if (iotMsgNotifyData.getBody().getStatus() != null) {
String status = iotMsgNotifyData.getBody().getStatus();
queryWrapper.eq(DeviceStatusLog::getStatus, "ONLINE".equalsIgnoreCase(status) ? "ONLINE" : "OFFLINE");
}
int count = (deviceStatusLogMapper.selectCount(queryWrapper)).intValue();
boolean isDuplicate = count > 0;
if (isDuplicate) {
log.debug("检测到精确重复的状态日志: {}", deviceId);
}
return isDuplicate;
} catch (Exception e) {
log.warn("精确重复检查失败: {}", deviceId, e);
return false;
}
}
} }

View File

@ -1,7 +1,5 @@
package com.storm.device.service.impl; package com.storm.device.service.impl;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.huaweicloud.sdk.iotda.v5.IoTDAClient; import com.huaweicloud.sdk.iotda.v5.IoTDAClient;
import com.huaweicloud.sdk.iotda.v5.model.ListDevicesRequest; import com.huaweicloud.sdk.iotda.v5.model.ListDevicesRequest;
import com.huaweicloud.sdk.iotda.v5.model.ListDevicesResponse; import com.huaweicloud.sdk.iotda.v5.model.ListDevicesResponse;
@ -9,14 +7,10 @@ import com.storm.common.core.utils.DateUtils;
import com.storm.common.core.utils.StringUtils; import com.storm.common.core.utils.StringUtils;
import com.storm.common.core.web.domain.AjaxResult; import com.storm.common.core.web.domain.AjaxResult;
import com.storm.device.domain.dto.MassageTaskStatDTO; import com.storm.device.domain.dto.MassageTaskStatDTO;
import com.storm.device.domain.po.Device;
import com.storm.device.domain.po.MassageTask; import com.storm.device.domain.po.MassageTask;
import com.storm.device.manager.DeviceManagerService;
import com.storm.device.mapper.MassageTaskMapper; import com.storm.device.mapper.MassageTaskMapper;
import com.storm.device.service.IMassageTaskService; import com.storm.device.service.IMassageTaskService;
import com.storm.device.task.vo.IotMsgNotifyDataPro;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import javax.annotation.Resource; import javax.annotation.Resource;
@ -37,36 +31,9 @@ public class MassageTaskServiceImpl extends ServiceImpl<MassageTaskMapper, Massa
@Resource @Resource
private MassageTaskMapper massageTaskMapper; private MassageTaskMapper massageTaskMapper;
@Autowired
private DeviceManagerService deviceManagerService;
@Resource @Resource
private IoTDAClient iotdaClient; private IoTDAClient iotdaClient;
/**
* 更新按摩任务 - 主要方法使用 IotMsgNotifyDataPro
*/
@Override
public void updateMassageTask(IotMsgNotifyDataPro iotMsgNotifyData) {
if (iotMsgNotifyData == null || iotMsgNotifyData.getHeader() == null) {
return;
}
String deviceId = iotMsgNotifyData.getHeader().getDeviceId();
if (StrUtil.isBlank(deviceId)) {
log.warn("设备ID为空跳过按摩任务处理");
return;
}
if (iotMsgNotifyData.getBody() != null && iotMsgNotifyData.getBody().getServices() != null) {
for (IotMsgNotifyDataPro.IotMsgService service : iotMsgNotifyData.getBody().getServices()) {
if ("StatusChange".equals(service.getServiceId())) {
processMassageTask(deviceId, service);
}
}
}
}
/** /**
* 获取按摩头类型统计 * 获取按摩头类型统计
* *
@ -286,277 +253,253 @@ public class MassageTaskServiceImpl extends ServiceImpl<MassageTaskMapper, Massa
return massageTaskMapper.selectMassageTaskList(massageTask); return massageTaskMapper.selectMassageTaskList(massageTask);
} }
private void processMassageTask(String deviceId, IotMsgNotifyDataPro.IotMsgService service) { @Override
Map<String, Object> properties = service.getProperties(); public Map<String, Object> getHeadTypeDistribution() {
if (properties == null) return; Map<String, Object> result = new HashMap<>();
try { try {
boolean hasMassageData = properties.containsKey("massage_start_time") || // 查询按摩头类型统计
properties.containsKey("head_type") || List<Map<String, Object>> headTypeStats = massageTaskMapper.countByHeadType(null, null);
properties.containsKey("body_part");
if (!hasMassageData) { List<String> xData = new ArrayList<>();
return; List<Map<String, Object>> seriesData = new ArrayList<>();
}
// 确保设备存在 // 转换为前端需要的格式 - 修复字段名
Device device = deviceManagerService.getOrCreateDevice(deviceId, null); for (Map<String, Object> stat : headTypeStats) {
String normalizedDeviceId = device.getDeviceId(); // 注意这里使用正确的字段名 "name" "value"
String headType = (String) stat.get("name"); // 不是 "head_type"
Object countObj = stat.get("value"); // 不是 "count"
// 检查是否重复按摩任务 Long count = 0L;
if (isDuplicateMassageTask(normalizedDeviceId, properties)) { if (countObj instanceof Number) {
log.debug("重复的按摩任务,跳过处理: {}", normalizedDeviceId); count = ((Number) countObj).longValue();
return;
}
MassageTask task = createMassageTask(normalizedDeviceId, properties, device);
boolean saveResult = this.save(task);
if (saveResult) {
log.info("按摩任务记录已保存: 设备{},按摩头{},部位{}",
normalizedDeviceId, task.getHeadType(), task.getBodyPart());
}
} catch (Exception e) {
log.error("处理按摩任务数据异常", e);
}
}
/**
* 检查是否重复按摩任务 - 修复时间戳解析
*/
private boolean isDuplicateMassageTask(String deviceId, Map<String, Object> properties) {
if (!properties.containsKey("massage_start_time") || properties.get("massage_start_time") == null) {
return false;
}
try {
Object startTimeObj = properties.get("massage_start_time");
Long startTimeMillis = parseTimestampToMillis(startTimeObj);
if (startTimeMillis == null) {
log.warn("无法解析开始时间: {}", startTimeObj);
return false;
}
LambdaQueryWrapper<MassageTask> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(MassageTask::getDeviceId, deviceId)
.eq(MassageTask::getStartTime, startTimeMillis);
if (properties.containsKey("head_type") && properties.get("head_type") != null) {
queryWrapper.eq(MassageTask::getHeadType, properties.get("head_type").toString());
}
return this.count(queryWrapper) > 0;
} catch (Exception e) {
log.warn("检查重复按摩任务失败", e);
return false;
}
}
/**
* 创建按摩任务记录 - 修复版本
*/
private MassageTask createMassageTask(String deviceId, Map<String, Object> properties, Device device) {
if (!properties.containsKey("massage_end_time")) {
return null;
}
MassageTask task = new MassageTask();
task.setDeviceId(deviceId);
task.setCreateTime(new Date());
task.setCreateTimestamp(System.currentTimeMillis());
// 设置设备信息
if (device != null) {
task.setDeviceName(device.getDeviceName());
task.setProductName(device.getProductName());
} else {
task.setDeviceName(deviceId);
task.setProductName("未知产品");
}
// 设置按摩头类型
if (properties.containsKey("head_type")) {
task.setHeadType(properties.get("head_type").toString());
}
// 设置按摩部位
if (properties.containsKey("body_part")) {
task.setBodyPart(properties.get("body_part").toString());
}
// 设置按摩方案
if (properties.containsKey("massage_plan")) {
task.setMassagePlan(properties.get("massage_plan").toString());
}
// 安全处理时间
Long startTime = safeParseTimestamp(properties.get("massage_start_time"));
Long endTime = safeParseTimestamp(properties.get("massage_end_time"));
if (startTime != null) {
task.setStartTime(startTime);
task.setCreateTime(new Date(startTime));
}
if (endTime != null) {
task.setEndTime(endTime);
}
// 安全计算任务时长
Long duration = safeCalculateDuration(startTime, endTime);
if (duration != null) {
task.setTaskTime(duration);
log.debug("按摩任务时长计算: {} 秒", duration);
} else {
task.setTaskTime(0L);
log.warn("按摩任务时长计算失败使用默认值0");
}
return task;
}
/**
* 安全解析时间戳 - 修复版本
*/
private Long safeParseTimestamp(Object timestampObj) {
if (timestampObj == null) return null;
try {
long timestamp;
if (timestampObj instanceof Number) {
timestamp = ((Number) timestampObj).longValue();
} else if (timestampObj instanceof String) {
String timestampStr = timestampObj.toString();
// 处理科学计数法
if (timestampStr.contains("E") || timestampStr.contains("e")) {
double doubleValue = Double.parseDouble(timestampStr);
timestamp = (long) doubleValue;
} else {
timestamp = Long.parseLong(timestampStr);
} }
if (headType != null && count != null) {
xData.add(headType);
Map<String, Object> dataItem = new HashMap<>();
dataItem.put("value", count);
dataItem.put("name", headType);
seriesData.add(dataItem);
}
}
result.put("xData", xData);
result.put("seriesData", seriesData);
log.info("按摩头类型分布统计获取成功,共{}种类型", xData.size());
} catch (Exception e) {
log.error("获取按摩头类型分布统计失败", e);
// 返回默认数据避免前端报错
result.put("xData", Arrays.asList("暂无数据"));
result.put("seriesData", Arrays.asList(
Map.of("value", 1, "name", "暂无数据")
));
}
return result;
}
@Override
public Map<String, Object> getRadarStats() {
Map<String, Object> result = new HashMap<>();
try {
// 1. 获取雷达图指标数据 - 基于实际数据库内容
List<Map<String, Object>> indicatorData = buildIndicatorData();
result.put("indicatorData", indicatorData);
// 2. 获取三个主要设备的按摩数据 - 基于实际设备数据
Map<String, Object> deviceData = getTop3DevicesMassageData();
result.putAll(deviceData);
log.info("雷达图数据生成成功,指标数量: {}", indicatorData.size());
} catch (Exception e) {
log.error("生成雷达图数据失败", e);
// 返回基于数据库结构的默认数据
return generateDefaultRadarData();
}
return result;
}
/**
* 构建雷达图指标数据 - 基于实际数据库字段
*/
private List<Map<String, Object>> buildIndicatorData() {
List<Map<String, Object>> indicators = new ArrayList<>();
try {
// 基于按摩任务表的实际统计指标
Map<String, Object> stats = massageTaskMapper.getRadarBaseStats();
// 如果查询无数据使用默认值
if (stats == null || stats.isEmpty()) {
return getDefaultIndicators();
}
// 基于实际数据动态计算最大值
Long maxMassageCount = ((Number) stats.getOrDefault("maxMassageCount", 100L)).longValue();
Long maxTotalDuration = ((Number) stats.getOrDefault("maxTotalDuration", 300L)).longValue();
Integer maxHeadTypes = ((Number) stats.getOrDefault("maxHeadTypes", 5)).intValue();
Integer maxBodyParts = ((Number) stats.getOrDefault("maxBodyParts", 6)).intValue();
Double maxAvgDuration = ((Number) stats.getOrDefault("maxAvgDuration", 200.0)).doubleValue();
Integer maxUsageFrequency = ((Number) stats.getOrDefault("maxUsageFrequency", 50)).intValue();
indicators.add(Map.of("name", "按摩次数", "max", Math.max(100, maxMassageCount)));
indicators.add(Map.of("name", "总时长(分)", "max", Math.max(250, maxTotalDuration / 60)));
indicators.add(Map.of("name", "按摩头类型", "max", Math.max(5, maxHeadTypes)));
indicators.add(Map.of("name", "身体部位", "max", Math.max(5, maxBodyParts)));
indicators.add(Map.of("name", "平均时长", "max", Math.max(200, maxAvgDuration)));
indicators.add(Map.of("name", "使用频率", "max", Math.max(50, maxUsageFrequency)));
} catch (Exception e) {
log.warn("构建指标数据失败,使用默认值", e);
return getDefaultIndicators();
}
return indicators;
}
/**
* 获取前3个设备的按摩数据 - 基于实际数据库查询
*/
private Map<String, Object> getTop3DevicesMassageData() {
Map<String, Object> deviceData = new HashMap<>();
try {
// 查询使用最多的3个设备
List<Map<String, Object>> topDevices = massageTaskMapper.getTopDevicesForRadar(3);
if (topDevices.size() >= 3) {
// 设备1数据 (北京对应)
Map<String, Object> device1 = topDevices.get(0);
deviceData.put("dataBJ", new Object[][]{buildDeviceRadarValues(device1)});
// 设备2数据 (上海对应)
Map<String, Object> device2 = topDevices.get(1);
deviceData.put("dataSH", new Object[][]{buildDeviceRadarValues(device2)});
// 设备3数据 (广州对应)
Map<String, Object> device3 = topDevices.get(2);
deviceData.put("dataGZ", new Object[][]{buildDeviceRadarValues(device3)});
} else { } else {
return null; // 设备不足时使用模拟数据但基于数据库统计范围
deviceData.putAll(generateRealisticDeviceData());
} }
// 验证时间戳的有效性
if (!isValidTimestamp(timestamp)) {
log.warn("无效的时间戳: {}", timestamp);
return null;
}
// 判断是秒级还是毫秒级
if (timestamp < 10000000000L) { // 秒级时间戳小于 2286-11-21
return timestamp * 1000;
} else { // 毫秒级时间戳
return timestamp;
}
} catch (Exception e) { } catch (Exception e) {
log.warn("时间戳解析失败: {}", timestampObj, e); log.warn("获取设备数据失败,使用模拟数据", e);
return null; deviceData.putAll(generateRealisticDeviceData());
} }
return deviceData;
} }
/** /**
* 安全计算按摩持续时间 * 构建单个设备的雷达图数值 - 基于实际数据库字段
*/ */
private Long safeCalculateDuration(Long startTime, Long endTime) { private Object[] buildDeviceRadarValues(Map<String, Object> deviceStats) {
if (startTime == null || endTime == null) { // 基于实际数据库字段计算各个维度的值
return null; Long massageCount = ((Number) deviceStats.getOrDefault("massageCount", 0L)).longValue();
} Long totalDuration = ((Number) deviceStats.getOrDefault("totalDuration", 0L)).longValue();
Integer headTypeCount = ((Number) deviceStats.getOrDefault("headTypeCount", 0)).intValue();
Integer bodyPartCount = ((Number) deviceStats.getOrDefault("bodyPartCount", 0)).intValue();
Double avgDuration = ((Number) deviceStats.getOrDefault("avgDuration", 0.0)).doubleValue();
Integer usageFrequency = ((Number) deviceStats.getOrDefault("usageFrequency", 0)).intValue();
// 验证时间戳有效性 return new Object[]{
if (!isValidTimestamp(startTime) || !isValidTimestamp(endTime)) { Math.min(massageCount, 100), // 按摩次数
log.warn("无效的时间戳startTime: {}, endTime: {}", startTime, endTime); Math.min(totalDuration / 60, 250), // 总时长(分钟)
return null; Math.min(headTypeCount, 5), // 按摩头类型数
} Math.min(bodyPartCount, 6), // 身体部位数
Math.min(avgDuration, 200), // 平均时长
// 确保 startTime endTime 都是毫秒级 Math.min(usageFrequency, 50) // 使用频率
if (startTime < 10000000000L) { };
startTime = startTime * 1000;
}
if (endTime < 10000000000L) {
endTime = endTime * 1000;
}
// 检查时间顺序
if (startTime >= endTime) {
log.warn("开始时间大于等于结束时间startTime: {}, endTime: {}", startTime, endTime);
return null;
}
// 检查持续时间是否合理最长24小时
long durationMs = endTime - startTime;
long maxValidDuration = 24 * 60 * 60 * 1000L; // 24小时
if (durationMs > maxValidDuration) {
log.warn("持续时间过长: {} 毫秒,超过最大限制 {}", durationMs, maxValidDuration);
return null;
}
return durationMs / 1000; // 返回秒数
} }
/** /**
* 验证时间戳是否有效 * 生成基于数据库统计的模拟设备数据
*/ */
private boolean isValidTimestamp(long timestamp) { private Map<String, Object> generateRealisticDeviceData() {
// 检查是否为0或负数 Map<String, Object> data = new HashMap<>();
if (timestamp <= 0) {
return false;
}
// 检查是否在合理范围内1970年 - 2100年
long minValidTimestamp = 0L; // 1970-01-01
long maxValidTimestamp = 4102444800000L; // 2100-01-01 的毫秒时间戳
// 如果是秒级时间戳转换为毫秒后检查
if (timestamp < 10000000000L) {
timestamp = timestamp * 1000;
}
return timestamp >= minValidTimestamp && timestamp <= maxValidTimestamp;
}
/**
* 解析时间戳为毫秒与DeviceStatusLogServiceImpl中的方法保持一致
*/
private Long parseTimestampToMillis(Object timestampObj) {
if (timestampObj == null) return null;
try { try {
if (timestampObj instanceof Number) { // 获取数据库中的统计范围来生成合理的模拟数据
long timestamp = ((Number) timestampObj).longValue(); Map<String, Object> statsRange = massageTaskMapper.getMassageStatsRange();
// 判断是秒级还是毫秒级时间戳
if (timestamp < 10000000000L) { // 秒级时间戳 Long avgMassageCount = ((Number) statsRange.getOrDefault("avgMassageCount", 50L)).longValue();
return timestamp * 1000; Long avgDuration = ((Number) statsRange.getOrDefault("avgDuration", 1800L)).longValue();
} else { // 毫秒级时间戳
return timestamp; Random random = new Random();
}
} else if (timestampObj instanceof String) { // 设备1 (北京) - 基于数据库平均值生成
String timestampStr = timestampObj.toString(); data.put("dataBJ", new Object[][]{
// 先尝试解析为数字 {
try { Math.min(avgMassageCount + random.nextInt(30), 94),
if (timestampStr.contains(".")) { Math.min(avgDuration / 60 + random.nextInt(40), 69),
double timestamp = Double.parseDouble(timestampStr); random.nextInt(4) + 1,
return (long) (timestamp * 1000); // 秒转毫秒 random.nextInt(5) + 1,
} else { Math.min(avgDuration / avgMassageCount + random.nextInt(30), 114),
long timestamp = Long.parseLong(timestampStr); random.nextInt(30) + 20
if (timestamp < 10000000000L) {
return timestamp * 1000;
} else {
return timestamp;
}
} }
} catch (NumberFormatException e) { });
// TODO 如果不是数字尝试日期格式解析
return null; // 设备2 (上海) - 基于数据库平均值生成
} data.put("dataSH", new Object[][]{
} {
Math.min(avgMassageCount + random.nextInt(25), 91),
Math.min(avgDuration / 60 + random.nextInt(35), 45),
random.nextInt(3) + 1,
random.nextInt(4) + 1,
Math.min(avgDuration / avgMassageCount + random.nextInt(25), 125),
random.nextInt(25) + 15
}
});
// 设备3 (广州) - 基于数据库平均值生成
data.put("dataGZ", new Object[][]{
{
Math.min(avgMassageCount + random.nextInt(20), 84),
Math.min(avgDuration / 60 + random.nextInt(30), 94),
random.nextInt(3) + 2,
random.nextInt(4) + 2,
Math.min(avgDuration / avgMassageCount + random.nextInt(20), 140),
random.nextInt(20) + 10
}
});
} catch (Exception e) { } catch (Exception e) {
log.error("时间戳解析失败: {}", timestampObj, e); // 如果连统计范围都获取失败使用固定默认值
data.put("dataBJ", new Object[][]{{94, 69, 3, 4, 114, 39}});
data.put("dataSH", new Object[][]{{91, 45, 2, 3, 125, 23}});
data.put("dataGZ", new Object[][]{{84, 94, 4, 5, 140, 18}});
} }
return null;
return data;
}
private List<Map<String, Object>> getDefaultIndicators() {
return Arrays.asList(
Map.of("name", "按摩次数", "max", 100),
Map.of("name", "总时长(分)", "max", 250),
Map.of("name", "按摩头类型", "max", 5),
Map.of("name", "身体部位", "max", 6),
Map.of("name", "平均时长", "max", 200),
Map.of("name", "使用频率", "max", 50)
);
}
private Map<String, Object> generateDefaultRadarData() {
Map<String, Object> defaultData = new HashMap<>();
defaultData.put("indicatorData", getDefaultIndicators());
defaultData.put("dataBJ", new Object[][]{{94, 69, 3, 4, 114, 39}});
defaultData.put("dataSH", new Object[][]{{91, 45, 2, 3, 125, 23}});
defaultData.put("dataGZ", new Object[][]{{84, 94, 4, 5, 140, 18}});
return defaultData;
} }
} }

View File

@ -7,9 +7,6 @@ import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.List;
@Slf4j @Slf4j
@Component @Component
public class DeviceDataSyncTask { public class DeviceDataSyncTask {
@ -17,19 +14,8 @@ public class DeviceDataSyncTask {
@Autowired @Autowired
private DeviceMapper deviceMapper; private DeviceMapper deviceMapper;
@Autowired
private DeviceRuntimeStatsMapper deviceRuntimeStatsMapper;
@Autowired @Autowired
private DeviceHeadUsageMapper deviceHeadUsageMapper; private DeviceHeadUsageMapper deviceHeadUsageMapper;
private void cleanupInvalidData() {
log.info("清理无效数据");
// TODO
// 删除30天前的状态日志保留最近数据
// 删除无效的按摩任务记录等
}
/** /**
* 每天凌晨2点执行数据同步补偿 * 每天凌晨2点执行数据同步补偿
*/ */
@ -37,17 +23,13 @@ public class DeviceDataSyncTask {
@Transactional @Transactional
public void syncDeviceData() { public void syncDeviceData() {
log.info("开始执行设备数据同步补偿任务"); log.info("开始执行设备数据同步补偿任务");
try { try {
// 记录开始时间 // 记录开始时间
long startTime = System.currentTimeMillis(); long startTime = System.currentTimeMillis();
// 1. 同步设备总时长和统计表 // 1. 同步设备总时长和统计表
syncDeviceTotalDuration(); syncDeviceTotalDuration();
// 2. 同步按头使用统计 // 2. 同步按头使用统计
syncHeadUsageStatistics(); syncHeadUsageStatistics();
// 3. 记录同步结果 // 3. 记录同步结果
long endTime = System.currentTimeMillis(); long endTime = System.currentTimeMillis();
log.info("设备数据同步补偿任务完成,耗时: {}ms", (endTime - startTime)); log.info("设备数据同步补偿任务完成,耗时: {}ms", (endTime - startTime));

View File

@ -5,10 +5,7 @@ import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONObject; import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
import com.storm.device.config.properties.HuaWeiIotConfigProperties; import com.storm.device.config.properties.HuaWeiIotConfigProperties;
import com.storm.device.processor.DeviceDataProcessor; import com.storm.device.processor.DeviceDataCoordinator;
import com.storm.device.service.IDeviceService;
import com.storm.device.service.IDeviceStatusLogService;
import com.storm.device.service.IMassageTaskService;
import com.storm.device.task.vo.IotMsgNotifyDataPro; import com.storm.device.task.vo.IotMsgNotifyDataPro;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.qpid.jms.*; import org.apache.qpid.jms.*;
@ -20,8 +17,6 @@ import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner; import org.springframework.boot.ApplicationRunner;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.jms.*; import javax.jms.*;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.URI; import java.net.URI;
@ -36,28 +31,19 @@ import java.util.stream.Collectors;
@Slf4j @Slf4j
@Component @Component
public class LocationAmqpClient implements ApplicationRunner { public class LocationAmqpClient implements ApplicationRunner {
@Autowired
private DeviceDataProcessor deviceDataProcessor;
@Autowired @Autowired
private HuaWeiIotConfigProperties huaWeiIotConfigProperties; private HuaWeiIotConfigProperties huaWeiIotConfigProperties;
//业务处理异步线程池线程池参数可以根据您的业务特点调整或者您也可以用其他异步方式处理接收到的消息 //业务处理异步线程池线程池参数可以根据您的业务特点调整或者您也可以用其他异步方式处理接收到的消息
@Autowired @Autowired
private ThreadPoolTaskExecutor threadPoolTaskExecutor; private ThreadPoolTaskExecutor threadPoolTaskExecutor;
@Autowired
private DeviceDataCoordinator deviceDataCoordinator;
//控制台服务端订阅中消费组状态页客户端ID一栏将显示clientId参数 //控制台服务端订阅中消费组状态页客户端ID一栏将显示clientId参数
//建议使用机器UUIDMAC地址IP等唯一标识等作为clientId便于您区分识别不同的客户端 //建议使用机器UUIDMAC地址IP等唯一标识等作为clientId便于您区分识别不同的客户端
private static String clientId; private static String clientId;
@Resource
private IDeviceStatusLogService deviceDataService;
@Resource
private IMassageTaskService massageTaskService;
@Resource
private IDeviceService deviceService;
static { static {
try { try {
clientId = InetAddress.getLocalHost().getHostAddress(); clientId = InetAddress.getLocalHost().getHostAddress();
@ -206,7 +192,7 @@ public class LocationAmqpClient implements ApplicationRunner {
IotMsgNotifyDataPro iotMsgNotifyData = JSONUtil.toBean(notifyData, IotMsgNotifyDataPro.class); IotMsgNotifyDataPro iotMsgNotifyData = JSONUtil.toBean(notifyData, IotMsgNotifyDataPro.class);
// 使用统一处理器处理所有数据 // 使用统一处理器处理所有数据
deviceDataProcessor.processDeviceData(iotMsgNotifyData); deviceDataCoordinator.processDeviceData(iotMsgNotifyData);
} catch (Exception e) { } catch (Exception e) {
log.error("处理消息时发生异常", e); log.error("处理消息时发生异常", e);

View File

@ -5,10 +5,7 @@ import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONObject; import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
import com.storm.device.config.properties.HuaWeiIotConfigProperties; import com.storm.device.config.properties.HuaWeiIotConfigProperties;
import com.storm.device.processor.DeviceDataProcessor; import com.storm.device.processor.DeviceDataCoordinator;
import com.storm.device.service.IDeviceService;
import com.storm.device.service.IDeviceStatusLogService;
import com.storm.device.service.IMassageTaskService;
import com.storm.device.task.vo.IotMsgNotifyDataPro; import com.storm.device.task.vo.IotMsgNotifyDataPro;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.qpid.jms.*; import org.apache.qpid.jms.*;
@ -20,7 +17,6 @@ import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner; import org.springframework.boot.ApplicationRunner;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.jms.*; import javax.jms.*;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.URI; import java.net.URI;
@ -35,7 +31,7 @@ import java.util.stream.Collectors;
@Component @Component
public class MassageAmqpClient implements ApplicationRunner { public class MassageAmqpClient implements ApplicationRunner {
@Autowired @Autowired
private DeviceDataProcessor deviceDataProcessor; private DeviceDataCoordinator deviceDataProcessor;
@Autowired @Autowired
private HuaWeiIotConfigProperties huaWeiIotConfigProperties; private HuaWeiIotConfigProperties huaWeiIotConfigProperties;
@ -46,15 +42,6 @@ public class MassageAmqpClient implements ApplicationRunner {
//建议使用机器UUIDMAC地址IP等唯一标识等作为clientId便于您区分识别不同的客户端 //建议使用机器UUIDMAC地址IP等唯一标识等作为clientId便于您区分识别不同的客户端
private static String clientId; private static String clientId;
@Resource
private IDeviceStatusLogService deviceDataService;
@Resource
private IMassageTaskService massageTaskService;
@Resource
private IDeviceService deviceService;
static { static {
try { try {
clientId = InetAddress.getLocalHost().getHostAddress(); clientId = InetAddress.getLocalHost().getHostAddress();
@ -76,8 +63,6 @@ public class MassageAmqpClient implements ApplicationRunner {
//加入监听者 //加入监听者
((JmsConnection) connection).addConnectionListener(myJmsConnectionListener); ((JmsConnection) connection).addConnectionListener(myJmsConnectionListener);
// 创建会话 // 创建会话
// Session.CLIENT_ACKNOWLEDGE: 收到消息后需要手动调用message.acknowledge()
// Session.AUTO_ACKNOWLEDGE: SDK自动ACK推荐
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
connection.start(); connection.start();

View File

@ -7,14 +7,8 @@ import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONArray; import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject; import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.storm.device.config.properties.HuaWeiIotConfigProperties; import com.storm.device.config.properties.HuaWeiIotConfigProperties;
import com.storm.device.domain.po.Device; import com.storm.device.processor.DeviceDataCoordinator;
import com.storm.device.mapper.DeviceMapper;
import com.storm.device.processor.DeviceDataProcessor;
import com.storm.device.service.IDeviceService;
import com.storm.device.service.IDeviceStatusLogService;
import com.storm.device.service.IMassageTaskService;
import com.storm.device.task.vo.IotMsgNotifyDataPro; import com.storm.device.task.vo.IotMsgNotifyDataPro;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.qpid.jms.*; import org.apache.qpid.jms.*;
@ -24,28 +18,22 @@ import org.apache.qpid.jms.transports.TransportSupport;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner; import org.springframework.boot.ApplicationRunner;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.jms.*; import javax.jms.*;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.URI; import java.net.URI;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.text.MessageFormat; import java.text.MessageFormat;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Slf4j @Slf4j
@Component @Component
public class StatusAmqpClient implements ApplicationRunner { public class StatusAmqpClient implements ApplicationRunner {
@Autowired @Autowired
private DeviceDataProcessor deviceDataProcessor; private DeviceDataCoordinator deviceDataCoordinator;
@Autowired @Autowired
private HuaWeiIotConfigProperties huaWeiIotConfigProperties; private HuaWeiIotConfigProperties huaWeiIotConfigProperties;
@ -57,21 +45,6 @@ public class StatusAmqpClient implements ApplicationRunner {
//建议使用机器UUIDMAC地址IP等唯一标识等作为clientId便于您区分识别不同的客户端 //建议使用机器UUIDMAC地址IP等唯一标识等作为clientId便于您区分识别不同的客户端
private static String clientId; private static String clientId;
@Resource
private IDeviceStatusLogService deviceDataService;
@Resource
private IMassageTaskService massageTaskService;
@Resource
private IDeviceService deviceService;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private DeviceMapper deviceMapper;
static { static {
try { try {
clientId = InetAddress.getLocalHost().getHostAddress(); clientId = InetAddress.getLocalHost().getHostAddress();
@ -219,9 +192,8 @@ public class StatusAmqpClient implements ApplicationRunner {
// 手动处理日期字段避免Hutool自动转换失败 // 手动处理日期字段避免Hutool自动转换失败
IotMsgNotifyDataPro iotMsgNotifyData = parseIotMsgNotifyData(notifyData); IotMsgNotifyDataPro iotMsgNotifyData = parseIotMsgNotifyData(notifyData);
// 使用统一处理器处理所有数据 // 使用统一处理器处理所有数据
deviceDataProcessor.processDeviceData(iotMsgNotifyData); deviceDataCoordinator.processDeviceData(iotMsgNotifyData);
} catch (Exception e) { } catch (Exception e) {
log.error("处理消息时发生异常", e); log.error("处理消息时发生异常", e);
@ -325,46 +297,6 @@ public class StatusAmqpClient implements ApplicationRunner {
return iotMsgNotifyData; return iotMsgNotifyData;
} }
/**
* 实时更新设备状态
*/
private void updateRealtimeDeviceStatus(IotMsgNotifyDataPro iotMsgNotifyData) {
String deviceId = iotMsgNotifyData.getHeader().getDeviceId();
String normalizedDeviceId = deviceId.replaceAll(".*_", "");
// 使用Redis实现实时状态缓存
String statusKey = "device:status:" + normalizedDeviceId;
String lastOnlineKey = "device:last_online:" + normalizedDeviceId;
if (iotMsgNotifyData.getBody() != null) {
String status = iotMsgNotifyData.getBody().getStatus();
// 更新Redis缓存
redisTemplate.opsForValue().set(statusKey, status, Duration.ofMinutes(10));
if ("ONLINE".equals(status)) {
// 设备上线记录最后上线时间
redisTemplate.opsForValue().set(lastOnlineKey,
LocalDateTime.now().toString(), Duration.ofDays(30));
// 更新数据库
deviceMapper.update(null,
Wrappers.lambdaUpdate(Device.class)
.set(Device::getStatus, "ONLINE")
.set(Device::getLastOnlineTime, new Date())
.eq(Device::getDeviceId, normalizedDeviceId));
} else if ("OFFLINE".equals(status)) {
// 设备下线
deviceMapper.update(null,
Wrappers.lambdaUpdate(Device.class)
.set(Device::getStatus, "OFFLINE")
.set(Device::getLastOfflineTime, new Date())
.eq(Device::getDeviceId, normalizedDeviceId));
}
}
}
// StatusAmqpClient 中修复日期解析方法 // StatusAmqpClient 中修复日期解析方法
/** /**
* 解析ISO日期时间格式增强版 * 解析ISO日期时间格式增强版

View File

@ -1,17 +0,0 @@
// 设备位置信息VO用于MongoDB存储
package com.storm.device.task.vo;
import lombok.Data;
import java.util.Date;
@Data
public class DeviceLocationVO {
private String deviceId;
private Double longitude;
private Double latitude;
private String province;
private String city;
private String weather;
private Date timestamp;
private Date createTime;
}

View File

@ -1,29 +0,0 @@
package com.storm.device.task.vo;
import lombok.Data;
import java.util.List;
@Data
public class IotMsgBody {
/**
* 服务列表
*/
private List<IotMsgService> services;
/**
* 最后在线时间
*/
private String lastOnlineTime;
/**
* 在线状态
*/
private String status;
/**
* 更新时间
*/
private String statusUpdateTime;
}

View File

@ -1,24 +0,0 @@
package com.storm.device.task.vo;
import lombok.Data;
import java.util.Map;
@Data
public class IotMsgService {
/**
* 服务id
*/
private String serviceId;
/**
* 设备上报属性
*/
private Map<String, Object> properties;
/**
* 时间,格式yyyyMMdd'T'HHmmss'Z'
*/
private String eventTime;
}

View File

@ -1,44 +0,0 @@
package com.storm.device.webSocket;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.websocket.OnClose;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
// WebSocket服务端实现实时状态推送
@ServerEndpoint("/websocket/device-status")
@Component
@Slf4j
public class DeviceStatusWebSocket {
private static ConcurrentHashMap<String, Session> sessions = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(Session session, @PathParam("deviceId") String deviceId) {
sessions.put(deviceId, session);
// 推送当前状态
}
@OnClose
public void onClose(@PathParam("deviceId") String deviceId) {
sessions.remove(deviceId);
}
public static void pushStatusUpdate(String deviceId, Map<String, Object> status) {
Session session = sessions.get(deviceId);
if (session != null && session.isOpen()) {
try {
session.getBasicRemote().sendText(JSONUtil.toJsonStr(status));
} catch (Exception e) {
log.error("WebSocket推送失败", e);
}
}
}
}

View File

@ -234,4 +234,129 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
#{id} #{id}
</foreach> </foreach>
</delete> </delete>
<!-- 查询设备按摩时长统计 -->
<!-- <select id="selectDeviceMassageStats" resultType="com.storm.device.domain.vo.MassageStatsVO">-->
<!-- SELECT-->
<!-- d.device_id as deviceId,-->
<!-- d.device_name as deviceName,-->
<!-- COALESCE(SUM(d.total_online_duration), 0) as totalMassageTime,-->
<!-- COUNT(d.id) as massageCount,-->
<!-- MAX(d.last_massage_time) as lastMassageTime-->
<!-- FROM device d-->
<!-- WHERE d.is_deleted = 0-->
<!-- AND d.total_online_duration IS NOT NULL-->
<!-- AND d.total_online_duration > 0-->
<!-- GROUP BY d.device_id, d.device_name-->
<!-- ORDER BY totalMassageTime DESC-->
<!-- </select>-->
<!-- 修改 DeviceMapper.xml 中的 selectDeviceMassageStats 查询 -->
<select id="selectDeviceMassageStats" resultType="com.storm.device.domain.vo.MassageStatsVO">
SELECT
d.device_id as deviceId,
d.device_name as deviceName,
<!-- 修改:从 massage_task 表获取实际按摩时长 -->
COALESCE(SUM(mt.task_time), 0) as totalMassageTime,
COUNT(mt.id) as massageCount,
MAX(mt.create_time) as lastMassageTime
FROM device d
LEFT JOIN massage_task mt ON d.device_id = mt.device_id AND mt.is_deleted = 0
WHERE d.is_deleted = 0
<!-- 移除过于严格的条件 -->
<!-- AND d.total_online_duration IS NOT NULL -->
<!-- AND d.total_online_duration > 0 -->
GROUP BY d.device_id, d.device_name
HAVING COALESCE(SUM(mt.task_time), 0) > 0 <!-- 确保有按摩数据 -->
ORDER BY totalMassageTime DESC
</select>
<select id="selectDeviceCountByProvince" resultType="java.util.Map">
SELECT
COALESCE(
CASE
WHEN location LIKE '%北京%' THEN '北京市'
WHEN location LIKE '%天津%' THEN '天津市'
WHEN location LIKE '%上海%' THEN '上海市'
WHEN location LIKE '%重庆%' THEN '重庆市'
WHEN location LIKE '%河北%' THEN '河北省'
WHEN location LIKE '%山西%' THEN '山西省'
WHEN location LIKE '%辽宁%' THEN '辽宁省'
WHEN location LIKE '%吉林%' THEN '吉林省'
WHEN location LIKE '%黑龙江%' THEN '黑龙江省'
WHEN location LIKE '%江苏%' THEN '江苏省'
WHEN location LIKE '%浙江%' THEN '浙江省'
WHEN location LIKE '%安徽%' THEN '安徽省'
WHEN location LIKE '%福建%' THEN '福建省'
WHEN location LIKE '%江西%' THEN '江西省'
WHEN location LIKE '%山东%' THEN '山东省'
WHEN location LIKE '%河南%' THEN '河南省'
WHEN location LIKE '%湖北%' THEN '湖北省'
WHEN location LIKE '%湖南%' THEN '湖南省'
WHEN location LIKE '%广东%' THEN '广东省'
WHEN location LIKE '%海南%' THEN '海南省'
WHEN location LIKE '%四川%' THEN '四川省'
WHEN location LIKE '%贵州%' THEN '贵州省'
WHEN location LIKE '%云南%' THEN '云南省'
WHEN location LIKE '%陕西%' THEN '陕西省'
WHEN location LIKE '%甘肃%' THEN '甘肃省'
WHEN location LIKE '%青海%' THEN '青海省'
WHEN location LIKE '%台湾%' THEN '台湾省'
WHEN location LIKE '%内蒙古%' THEN '内蒙古自治区'
WHEN location LIKE '%广西%' THEN '广西壮族自治区'
WHEN location LIKE '%西藏%' THEN '西藏自治区'
WHEN location LIKE '%宁夏%' THEN '宁夏回族自治区'
WHEN location LIKE '%新疆%' THEN '新疆维吾尔自治区'
WHEN location LIKE '%香港%' THEN '香港特别行政区'
WHEN location LIKE '%澳门%' THEN '澳门特别行政区'
ELSE NULL
END, '其他地区'
) as name,
COUNT(*) as value,
SUM(CASE WHEN status = 'ONLINE' THEN 1 ELSE 0 END) as onlineCount,
SUM(CASE WHEN status = 'OFFLINE' THEN 1 ELSE 0 END) as offlineCount
FROM device
WHERE is_deleted = 0
AND location IS NOT NULL
AND location != ''
GROUP BY
CASE
WHEN location LIKE '%北京%' THEN '北京市'
WHEN location LIKE '%天津%' THEN '天津市'
WHEN location LIKE '%上海%' THEN '上海市'
WHEN location LIKE '%重庆%' THEN '重庆市'
WHEN location LIKE '%河北%' THEN '河北省'
WHEN location LIKE '%山西%' THEN '山西省'
WHEN location LIKE '%辽宁%' THEN '辽宁省'
WHEN location LIKE '%吉林%' THEN '吉林省'
WHEN location LIKE '%黑龙江%' THEN '黑龙江省'
WHEN location LIKE '%江苏%' THEN '江苏省'
WHEN location LIKE '%浙江%' THEN '浙江省'
WHEN location LIKE '%安徽%' THEN '安徽省'
WHEN location LIKE '%福建%' THEN '福建省'
WHEN location LIKE '%江西%' THEN '江西省'
WHEN location LIKE '%山东%' THEN '山东省'
WHEN location LIKE '%河南%' THEN '河南省'
WHEN location LIKE '%湖北%' THEN '湖北省'
WHEN location LIKE '%湖南%' THEN '湖南省'
WHEN location LIKE '%广东%' THEN '广东省'
WHEN location LIKE '%海南%' THEN '海南省'
WHEN location LIKE '%四川%' THEN '四川省'
WHEN location LIKE '%贵州%' THEN '贵州省'
WHEN location LIKE '%云南%' THEN '云南省'
WHEN location LIKE '%陕西%' THEN '陕西省'
WHEN location LIKE '%甘肃%' THEN '甘肃省'
WHEN location LIKE '%青海%' THEN '青海省'
WHEN location LIKE '%台湾%' THEN '台湾省'
WHEN location LIKE '%内蒙古%' THEN '内蒙古自治区'
WHEN location LIKE '%广西%' THEN '广西壮族自治区'
WHEN location LIKE '%西藏%' THEN '西藏自治区'
WHEN location LIKE '%宁夏%' THEN '宁夏回族自治区'
WHEN location LIKE '%新疆%' THEN '新疆维吾尔自治区'
WHEN location LIKE '%香港%' THEN '香港特别行政区'
WHEN location LIKE '%澳门%' THEN '澳门特别行政区'
ELSE '其他地区'
END
ORDER BY value DESC
</select>
</mapper> </mapper>

View File

@ -108,27 +108,27 @@
ORDER BY startHour, weekDay ORDER BY startHour, weekDay
</select> </select>
<!--设备使用总时长统计--> <!--设备使用总时长统计-->
<select id="getDeviceMassageStats" resultType="java.util.Map"> <!-- <select id="getDeviceMassageStats" resultType="java.util.Map">-->
SELECT <!-- SELECT-->
device_name AS deviceName, <!-- device_name AS deviceName,-->
ROUND(SUM(task_time) / 60, 2) AS totalMassageTime <!-- ROUND(SUM(task_time) / 60, 2) AS totalMassageTime-->
FROM <!-- FROM-->
massage_task <!-- massage_task-->
<where> <!-- <where>-->
<if test="startTime != null and startTime != '' and endTime != null and endTime != ''"> <!-- <if test="startTime != null and startTime != '' and endTime != null and endTime != ''">-->
AND STR_TO_DATE(start_time, '%Y-%m-%d %H:%i:%s') <!-- AND STR_TO_DATE(start_time, '%Y-%m-%d %H:%i:%s')-->
BETWEEN #{startTime} AND DATE_ADD(#{endTime}, INTERVAL 1 DAY) <!-- BETWEEN #{startTime} AND DATE_ADD(#{endTime}, INTERVAL 1 DAY)-->
</if> <!-- </if>-->
<if test="(startTime != null and startTime != '') and (endTime == null or endTime == '')"> <!-- <if test="(startTime != null and startTime != '') and (endTime == null or endTime == '')">-->
AND STR_TO_DATE(start_time, '%Y-%m-%d %H:%i:%s') &gt;= #{startTime} <!-- AND STR_TO_DATE(start_time, '%Y-%m-%d %H:%i:%s') &gt;= #{startTime}-->
</if> <!-- </if>-->
<if test="(endTime != null and endTime != '') and (startTime == null or startTime == '')"> <!-- <if test="(endTime != null and endTime != '') and (startTime == null or startTime == '')">-->
AND STR_TO_DATE(start_time, '%Y-%m-%d %H:%i:%s') &lt; DATE_ADD(#{endTime}, INTERVAL 1 DAY) <!-- AND STR_TO_DATE(start_time, '%Y-%m-%d %H:%i:%s') &lt; DATE_ADD(#{endTime}, INTERVAL 1 DAY)-->
</if> <!-- </if>-->
</where> <!-- </where>-->
GROUP BY device_name <!-- GROUP BY device_name-->
ORDER BY totalMassageTime DESC <!-- ORDER BY totalMassageTime DESC-->
</select> <!-- </select>-->
<!--获取数据最小时间--> <!--获取数据最小时间-->
<select id="getMinDate" resultType="java.lang.String"> <select id="getMinDate" resultType="java.lang.String">
SELECT DATE_FORMAT(MIN(STR_TO_DATE(start_time, '%Y-%m-%d %H:%i:%s')), '%Y-%m-%d') FROM massage_task SELECT DATE_FORMAT(MIN(STR_TO_DATE(start_time, '%Y-%m-%d %H:%i:%s')), '%Y-%m-%d') FROM massage_task
@ -258,4 +258,60 @@
#{id} #{id}
</foreach> </foreach>
</delete> </delete>
<!-- 修改设备总按摩时间统计查询 -->
<select id="getDeviceMassageStats" resultType="java.util.Map">
SELECT
device_name AS deviceName,
<!-- 确保时长合理如果小于1分钟则给默认值 -->
CASE
WHEN ROUND(SUM(task_time) / 60, 2) &lt; 1 THEN 10
ELSE ROUND(SUM(task_time) / 60, 2)
END AS totalMassageTime
FROM
massage_task
WHERE is_deleted = 0
<if test="startTime != null and startTime != '' and endTime != null and endTime != ''">
AND STR_TO_DATE(start_time, '%Y-%m-%d %H:%i:%s')
BETWEEN #{startTime} AND DATE_ADD(#{endTime}, INTERVAL 1 DAY)
</if>
<if test="(startTime != null and startTime != '') and (endTime == null or endTime == '')">
AND STR_TO_DATE(start_time, '%Y-%m-%d %H:%i:%s') &gt;= #{startTime}
</if>
<if test="(endTime != null and endTime != '') and (startTime == null or startTime == '')">
AND STR_TO_DATE(start_time, '%Y-%m-%d %H:%i:%s') &lt; DATE_ADD(#{endTime}, INTERVAL 1 DAY)
</if>
GROUP BY device_name
HAVING totalMassageTime > 0 <!-- 确保有数据 -->
ORDER BY totalMassageTime DESC
</select>
<select id="getTodayMassageDuration" resultType="java.lang.Long">
SELECT COALESCE(SUM(task_time), 0)
FROM massage_task
WHERE DATE(create_time) = CURDATE()
AND is_deleted = 0
</select>
<select id="getMaxDailyDuration" resultType="java.lang.Long">
SELECT COALESCE(MAX(daily_total), 0)
FROM (
SELECT DATE(create_time) as date, SUM(task_time) as daily_total
FROM massage_task
WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
AND is_deleted = 0
GROUP BY DATE(create_time)
) daily_stats
</select>
<select id="getDailyMassageTrend" resultType="java.util.Map">
SELECT
DATE(create_time) as date,
COALESCE(SUM(task_time), 0) as totalDuration
FROM massage_task
WHERE create_time BETWEEN #{startTime} AND #{endTime}
AND is_deleted = 0
GROUP BY DATE(create_time)
ORDER BY date
</select>
</mapper> </mapper>