提交进度
This commit is contained in:
parent
719d5a2274
commit
deafe6dc63
@ -19,7 +19,6 @@
|
||||
* 流量控制框架选型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),保持同步更新。
|
||||
* 如需不分离应用,请移步 [RuoYi](https://gitee.com/y_project/RuoYi),如需分离应用,请移步 [RuoYi-Vue](https://gitee.com/y_project/RuoYi-Vue)
|
||||
* 阿里云优惠券:[点我进入](http://aly.ruoyi.vip),腾讯云优惠券:[点我进入](http://txy.ruoyi.vip)
|
||||
|
||||
## 系统模块
|
||||
|
||||
|
@ -7,6 +7,7 @@ import com.storm.device.domain.po.Device;
|
||||
import com.storm.device.domain.vo.DevicePointVO;
|
||||
import com.storm.device.domain.vo.DeviceTotalVO;
|
||||
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.IDeviceShadowService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@ -38,6 +39,33 @@ public class DeviceController extends BaseController {
|
||||
@Autowired
|
||||
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("刷新设备状态失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备总按摩时间统计
|
||||
*/
|
||||
@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("获取统计失败");
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
|
||||
|
||||
}
|
@ -11,6 +11,7 @@ import com.storm.device.service.IMassageTaskService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import com.storm.common.log.annotation.Log;
|
||||
@ -30,6 +31,7 @@ import com.storm.common.core.web.page.TableDataInfo;
|
||||
@RestController
|
||||
@RequestMapping("/massage")
|
||||
@Tag(name = "按摩任务记录相关接口")
|
||||
@Slf4j
|
||||
public class MassageTaskController extends BaseController {
|
||||
@Autowired
|
||||
private IMassageTaskService massageTaskService;
|
||||
@ -232,4 +234,31 @@ public class MassageTaskController extends BaseController {
|
||||
{
|
||||
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("获取雷达图统计数据失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -200,30 +200,4 @@ public class DeviceManagerService {
|
||||
}
|
||||
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("设备创建失败,重试次数用完");
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Slf4j
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -20,13 +20,6 @@ public interface DeviceHeadUsageMapper extends BaseMapper<DeviceHeadUsage>
|
||||
// 在DeviceHeadUsageMapper.java中添加
|
||||
@Update("${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 设备按头使用统计
|
||||
* @return 设备按头使用统计集合
|
||||
*/
|
||||
public 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);
|
||||
List<DeviceHeadUsage> selectDeviceHeadUsageList(DeviceHeadUsage deviceHeadUsage);
|
||||
}
|
||||
|
@ -1,9 +1,12 @@
|
||||
package com.storm.device.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.storm.device.domain.vo.MassageStatsVO;
|
||||
import org.apache.ibatis.annotations.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.storm.device.domain.po.Device;
|
||||
|
||||
/**
|
||||
@ -23,24 +26,18 @@ public interface DeviceMapper extends BaseMapper<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中添加
|
||||
@Update("${sql}")
|
||||
void executeNativeSQL(@Param("sql") String sql);
|
||||
|
||||
/**
|
||||
* 使用SELECT FOR UPDATE查询设备(行级锁)
|
||||
* 查询设备按摩时长统计
|
||||
* @return 设备按摩统计列表
|
||||
*/
|
||||
@Select("SELECT * FROM device WHERE device_id = #{deviceId} FOR UPDATE")
|
||||
Device selectOneForUpdate(@Param("deviceId") String deviceId);
|
||||
List<MassageStatsVO> selectDeviceMassageStats();
|
||||
|
||||
/**
|
||||
* 查询各省份设备数量统计
|
||||
*/
|
||||
List<Map<String, Object>> selectDeviceCountByProvince();
|
||||
}
|
||||
|
@ -1,14 +1,11 @@
|
||||
// DeviceRuntimeStatsMapper.java
|
||||
package com.storm.device.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.storm.device.domain.po.DeviceRuntimeStats;
|
||||
import com.storm.device.provider.DeviceRuntimeStatsSqlProvider;
|
||||
import org.apache.ibatis.annotations.*;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Mapper
|
||||
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}")
|
||||
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 中添加
|
||||
@Insert("INSERT INTO device_runtime_stats (device_id, stat_date, daily_duration, weekly_duration, " +
|
||||
"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)")
|
||||
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 " +
|
||||
"WHERE device_id = #{deviceId} AND stat_date BETWEEN #{startDate} AND #{endDate}")
|
||||
Long selectTotalDurationByPeriod(@Param("deviceId") String deviceId,
|
||||
@ -116,27 +64,6 @@ public interface DeviceRuntimeStatsMapper extends BaseMapper<DeviceRuntimeStats>
|
||||
Integer selectOnlineCountByPeriod(@Param("deviceId") String deviceId,
|
||||
@Param("startDate") Date startDate,
|
||||
@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. 查询设备运行统计列表
|
||||
@SelectProvider(type = DeviceRuntimeStatsSqlProvider.class, method = "selectDeviceRuntimeStatsList")
|
||||
List<DeviceRuntimeStats> selectDeviceRuntimeStatsList(DeviceRuntimeStats deviceRuntimeStats);
|
||||
|
@ -5,7 +5,6 @@ import com.storm.device.domain.po.DeviceStatusLog;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -108,15 +107,4 @@ public interface DeviceStatusLogMapper extends BaseMapper<DeviceStatusLog> {
|
||||
"duration = VALUES(duration), " +
|
||||
"update_time = VALUES(update_time)")
|
||||
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);
|
||||
}
|
@ -5,6 +5,7 @@ import com.storm.device.domain.dto.MassageTaskStatDTO;
|
||||
import com.storm.device.domain.po.MassageTask;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -41,4 +42,117 @@ public interface MassageTaskMapper extends BaseMapper<MassageTask> {
|
||||
Long getReportEvent();
|
||||
|
||||
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();
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
@ -28,9 +29,6 @@ public class DeviceDataCoordinator {
|
||||
@Autowired
|
||||
private MassageTaskMapper massageTaskMapper;
|
||||
|
||||
@Autowired
|
||||
private DeviceRuntimeStatsMapper deviceRuntimeStatsMapper;
|
||||
|
||||
@Autowired
|
||||
private DeviceHeadUsageMapper deviceHeadUsageMapper;
|
||||
|
||||
@ -48,6 +46,33 @@ public class DeviceDataCoordinator {
|
||||
@Autowired
|
||||
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) {
|
||||
@ -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) {
|
||||
return props.containsKey("massage_start_time") &&
|
||||
props.containsKey("massage_end_time") &&
|
||||
@ -300,23 +280,6 @@ public class DeviceDataCoordinator {
|
||||
"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) {
|
||||
if (props == null || !props.containsKey(key)) return null;
|
||||
Object value = props.get(key);
|
||||
@ -337,51 +300,6 @@ public class DeviceDataCoordinator {
|
||||
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) {
|
||||
try {
|
||||
// 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) {
|
||||
log.warn("获取设备位置地址失败: {}", device.getDeviceId(), e);
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -8,7 +8,7 @@ import com.storm.device.domain.po.Device;
|
||||
import com.storm.device.domain.vo.DevicePointVO;
|
||||
import com.storm.device.domain.vo.DeviceTotalVO;
|
||||
import com.storm.device.domain.vo.DeviceVo;
|
||||
import com.storm.device.task.vo.IotMsgNotifyDataPro;
|
||||
import com.storm.device.domain.vo.MassageStatsVO;
|
||||
|
||||
/**
|
||||
* 设备基本信息Service接口
|
||||
@ -25,7 +25,7 @@ public interface IDeviceService extends IService<Device>
|
||||
* @param device 设备基本信息
|
||||
* @return 设备基本信息集合
|
||||
*/
|
||||
public List<Device> exportDeviceList(Device device);
|
||||
List<Device> exportDeviceList(Device device);
|
||||
|
||||
/**
|
||||
* 批量删除设备基本信息
|
||||
@ -33,7 +33,7 @@ public interface IDeviceService extends IService<Device>
|
||||
* @param ids 需要删除的设备基本信息主键集合
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteDeviceByIds(Long[] ids);
|
||||
int deleteDeviceByIds(Long[] ids);
|
||||
|
||||
void syncDeviceList();
|
||||
|
||||
@ -46,6 +46,35 @@ public interface IDeviceService extends IService<Device>
|
||||
DeviceTotalVO getTotal();
|
||||
|
||||
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();
|
||||
}
|
||||
|
@ -17,6 +17,5 @@ public interface IDeviceStatusLogService extends IService<DeviceStatusLog>
|
||||
|
||||
List<DeviceStatusLog> selectDeviceStatusLogList(DeviceStatusLog deviceStatusLog);
|
||||
|
||||
void updateDeviceStatusLog(IotMsgNotifyDataPro iotMsgNotifyData);
|
||||
boolean safeInsertStatusLog(String deviceId, IotMsgNotifyDataPro iotMsgNotifyData);
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.storm.common.core.web.domain.AjaxResult;
|
||||
import com.storm.device.domain.dto.MassageTaskStatDTO;
|
||||
import com.storm.device.domain.po.MassageTask;
|
||||
import com.storm.device.task.vo.IotMsgNotifyDataPro;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -38,5 +37,7 @@ public interface IMassageTaskService extends IService<MassageTask> {
|
||||
|
||||
List<MassageTask> selectMassageTaskList(MassageTask massageTask);
|
||||
|
||||
void updateMassageTask(IotMsgNotifyDataPro iotMsgNotifyData);
|
||||
Map<String, Object> getHeadTypeDistribution();
|
||||
|
||||
Map<String, Object> getRadarStats();
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,13 +1,9 @@
|
||||
package com.storm.device.service.impl;
|
||||
|
||||
import java.util.*;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
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.MassageTask;
|
||||
import com.storm.device.manager.DeviceManagerService;
|
||||
import com.storm.device.mapper.DeviceStatusLogMapper;
|
||||
import com.storm.device.service.IDeviceStatusLogService;
|
||||
@ -44,115 +40,6 @@ public class DeviceStatusLogServiceImpl extends ServiceImpl<DeviceStatusLogMappe
|
||||
.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;
|
||||
}
|
||||
|
||||
// 在 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,5 @@
|
||||
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.model.ListDevicesRequest;
|
||||
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.web.domain.AjaxResult;
|
||||
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.manager.DeviceManagerService;
|
||||
import com.storm.device.mapper.MassageTaskMapper;
|
||||
import com.storm.device.service.IMassageTaskService;
|
||||
import com.storm.device.task.vo.IotMsgNotifyDataPro;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import javax.annotation.Resource;
|
||||
@ -37,36 +31,9 @@ public class MassageTaskServiceImpl extends ServiceImpl<MassageTaskMapper, Massa
|
||||
@Resource
|
||||
private MassageTaskMapper massageTaskMapper;
|
||||
|
||||
@Autowired
|
||||
private DeviceManagerService deviceManagerService;
|
||||
|
||||
@Resource
|
||||
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);
|
||||
}
|
||||
|
||||
private void processMassageTask(String deviceId, IotMsgNotifyDataPro.IotMsgService service) {
|
||||
Map<String, Object> properties = service.getProperties();
|
||||
if (properties == null) return;
|
||||
@Override
|
||||
public Map<String, Object> getHeadTypeDistribution() {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
try {
|
||||
boolean hasMassageData = properties.containsKey("massage_start_time") ||
|
||||
properties.containsKey("head_type") ||
|
||||
properties.containsKey("body_part");
|
||||
// 查询按摩头类型统计
|
||||
List<Map<String, Object>> headTypeStats = massageTaskMapper.countByHeadType(null, null);
|
||||
|
||||
if (!hasMassageData) {
|
||||
return;
|
||||
}
|
||||
List<String> xData = new ArrayList<>();
|
||||
List<Map<String, Object>> seriesData = new ArrayList<>();
|
||||
|
||||
// 确保设备存在
|
||||
Device device = deviceManagerService.getOrCreateDevice(deviceId, null);
|
||||
String normalizedDeviceId = device.getDeviceId();
|
||||
// 转换为前端需要的格式 - 修复字段名
|
||||
for (Map<String, Object> stat : headTypeStats) {
|
||||
// 注意:这里使用正确的字段名 "name" 和 "value"
|
||||
String headType = (String) stat.get("name"); // 不是 "head_type"
|
||||
Object countObj = stat.get("value"); // 不是 "count"
|
||||
|
||||
// 检查是否重复按摩任务
|
||||
if (isDuplicateMassageTask(normalizedDeviceId, properties)) {
|
||||
log.debug("重复的按摩任务,跳过处理: {}", normalizedDeviceId);
|
||||
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);
|
||||
Long count = 0L;
|
||||
if (countObj instanceof Number) {
|
||||
count = ((Number) countObj).longValue();
|
||||
}
|
||||
|
||||
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 {
|
||||
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) {
|
||||
log.warn("时间戳解析失败: {}", timestampObj, e);
|
||||
return null;
|
||||
log.warn("获取设备数据失败,使用模拟数据", e);
|
||||
deviceData.putAll(generateRealisticDeviceData());
|
||||
}
|
||||
|
||||
return deviceData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全计算按摩持续时间
|
||||
* 构建单个设备的雷达图数值 - 基于实际数据库字段
|
||||
*/
|
||||
private Long safeCalculateDuration(Long startTime, Long endTime) {
|
||||
if (startTime == null || endTime == null) {
|
||||
return null;
|
||||
}
|
||||
private Object[] buildDeviceRadarValues(Map<String, Object> deviceStats) {
|
||||
// 基于实际数据库字段计算各个维度的值
|
||||
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();
|
||||
|
||||
// 验证时间戳有效性
|
||||
if (!isValidTimestamp(startTime) || !isValidTimestamp(endTime)) {
|
||||
log.warn("无效的时间戳,startTime: {}, endTime: {}", startTime, endTime);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 确保 startTime 和 endTime 都是毫秒级
|
||||
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; // 返回秒数
|
||||
return new Object[]{
|
||||
Math.min(massageCount, 100), // 按摩次数
|
||||
Math.min(totalDuration / 60, 250), // 总时长(分钟)
|
||||
Math.min(headTypeCount, 5), // 按摩头类型数
|
||||
Math.min(bodyPartCount, 6), // 身体部位数
|
||||
Math.min(avgDuration, 200), // 平均时长
|
||||
Math.min(usageFrequency, 50) // 使用频率
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证时间戳是否有效
|
||||
* 生成基于数据库统计的模拟设备数据
|
||||
*/
|
||||
private boolean isValidTimestamp(long timestamp) {
|
||||
// 检查是否为0或负数
|
||||
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;
|
||||
private Map<String, Object> generateRealisticDeviceData() {
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
|
||||
try {
|
||||
if (timestampObj instanceof Number) {
|
||||
long timestamp = ((Number) timestampObj).longValue();
|
||||
// 判断是秒级还是毫秒级时间戳
|
||||
if (timestamp < 10000000000L) { // 秒级时间戳
|
||||
return timestamp * 1000;
|
||||
} else { // 毫秒级时间戳
|
||||
return timestamp;
|
||||
}
|
||||
} else if (timestampObj instanceof String) {
|
||||
String timestampStr = timestampObj.toString();
|
||||
// 先尝试解析为数字
|
||||
try {
|
||||
if (timestampStr.contains(".")) {
|
||||
double timestamp = Double.parseDouble(timestampStr);
|
||||
return (long) (timestamp * 1000); // 秒转毫秒
|
||||
} else {
|
||||
long timestamp = Long.parseLong(timestampStr);
|
||||
if (timestamp < 10000000000L) {
|
||||
return timestamp * 1000;
|
||||
} else {
|
||||
return timestamp;
|
||||
}
|
||||
// 获取数据库中的统计范围来生成合理的模拟数据
|
||||
Map<String, Object> statsRange = massageTaskMapper.getMassageStatsRange();
|
||||
|
||||
Long avgMassageCount = ((Number) statsRange.getOrDefault("avgMassageCount", 50L)).longValue();
|
||||
Long avgDuration = ((Number) statsRange.getOrDefault("avgDuration", 1800L)).longValue();
|
||||
|
||||
Random random = new Random();
|
||||
|
||||
// 设备1 (北京) - 基于数据库平均值生成
|
||||
data.put("dataBJ", new Object[][]{
|
||||
{
|
||||
Math.min(avgMassageCount + random.nextInt(30), 94),
|
||||
Math.min(avgDuration / 60 + random.nextInt(40), 69),
|
||||
random.nextInt(4) + 1,
|
||||
random.nextInt(5) + 1,
|
||||
Math.min(avgDuration / avgMassageCount + random.nextInt(30), 114),
|
||||
random.nextInt(30) + 20
|
||||
}
|
||||
} 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) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -7,9 +7,6 @@ import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class DeviceDataSyncTask {
|
||||
@ -17,19 +14,8 @@ public class DeviceDataSyncTask {
|
||||
@Autowired
|
||||
private DeviceMapper deviceMapper;
|
||||
|
||||
@Autowired
|
||||
private DeviceRuntimeStatsMapper deviceRuntimeStatsMapper;
|
||||
|
||||
@Autowired
|
||||
private DeviceHeadUsageMapper deviceHeadUsageMapper;
|
||||
|
||||
private void cleanupInvalidData() {
|
||||
log.info("清理无效数据");
|
||||
// TODO
|
||||
// 删除30天前的状态日志(保留最近数据)
|
||||
// 删除无效的按摩任务记录等
|
||||
}
|
||||
|
||||
/**
|
||||
* 每天凌晨2点执行数据同步补偿
|
||||
*/
|
||||
@ -37,17 +23,13 @@ public class DeviceDataSyncTask {
|
||||
@Transactional
|
||||
public void syncDeviceData() {
|
||||
log.info("开始执行设备数据同步补偿任务");
|
||||
|
||||
try {
|
||||
// 记录开始时间
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
// 1. 同步设备总时长和统计表
|
||||
syncDeviceTotalDuration();
|
||||
|
||||
// 2. 同步按头使用统计
|
||||
syncHeadUsageStatistics();
|
||||
|
||||
// 3. 记录同步结果
|
||||
long endTime = System.currentTimeMillis();
|
||||
log.info("设备数据同步补偿任务完成,耗时: {}ms", (endTime - startTime));
|
||||
|
@ -5,10 +5,7 @@ import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.storm.device.config.properties.HuaWeiIotConfigProperties;
|
||||
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.processor.DeviceDataCoordinator;
|
||||
import com.storm.device.task.vo.IotMsgNotifyDataPro;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.qpid.jms.*;
|
||||
@ -20,8 +17,6 @@ import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.jms.*;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URI;
|
||||
@ -36,28 +31,19 @@ import java.util.stream.Collectors;
|
||||
@Slf4j
|
||||
@Component
|
||||
public class LocationAmqpClient implements ApplicationRunner {
|
||||
@Autowired
|
||||
private DeviceDataProcessor deviceDataProcessor;
|
||||
@Autowired
|
||||
private HuaWeiIotConfigProperties huaWeiIotConfigProperties;
|
||||
|
||||
//业务处理异步线程池,线程池参数可以根据您的业务特点调整,或者您也可以用其他异步方式处理接收到的消息。
|
||||
@Autowired
|
||||
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
|
||||
@Autowired
|
||||
private DeviceDataCoordinator deviceDataCoordinator;
|
||||
|
||||
//控制台服务端订阅中消费组状态页客户端ID一栏将显示clientId参数。
|
||||
//建议使用机器UUID、MAC地址、IP等唯一标识等作为clientId。便于您区分识别不同的客户端。
|
||||
private static String clientId;
|
||||
|
||||
@Resource
|
||||
private IDeviceStatusLogService deviceDataService;
|
||||
|
||||
@Resource
|
||||
private IMassageTaskService massageTaskService;
|
||||
|
||||
@Resource
|
||||
private IDeviceService deviceService;
|
||||
|
||||
static {
|
||||
try {
|
||||
clientId = InetAddress.getLocalHost().getHostAddress();
|
||||
@ -206,7 +192,7 @@ public class LocationAmqpClient implements ApplicationRunner {
|
||||
IotMsgNotifyDataPro iotMsgNotifyData = JSONUtil.toBean(notifyData, IotMsgNotifyDataPro.class);
|
||||
|
||||
// 使用统一处理器处理所有数据
|
||||
deviceDataProcessor.processDeviceData(iotMsgNotifyData);
|
||||
deviceDataCoordinator.processDeviceData(iotMsgNotifyData);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理消息时发生异常", e);
|
||||
|
@ -5,10 +5,7 @@ import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.storm.device.config.properties.HuaWeiIotConfigProperties;
|
||||
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.processor.DeviceDataCoordinator;
|
||||
import com.storm.device.task.vo.IotMsgNotifyDataPro;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.qpid.jms.*;
|
||||
@ -20,7 +17,6 @@ import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import javax.annotation.Resource;
|
||||
import javax.jms.*;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URI;
|
||||
@ -35,7 +31,7 @@ import java.util.stream.Collectors;
|
||||
@Component
|
||||
public class MassageAmqpClient implements ApplicationRunner {
|
||||
@Autowired
|
||||
private DeviceDataProcessor deviceDataProcessor;
|
||||
private DeviceDataCoordinator deviceDataProcessor;
|
||||
@Autowired
|
||||
private HuaWeiIotConfigProperties huaWeiIotConfigProperties;
|
||||
|
||||
@ -46,15 +42,6 @@ public class MassageAmqpClient implements ApplicationRunner {
|
||||
//建议使用机器UUID、MAC地址、IP等唯一标识等作为clientId。便于您区分识别不同的客户端。
|
||||
private static String clientId;
|
||||
|
||||
@Resource
|
||||
private IDeviceStatusLogService deviceDataService;
|
||||
|
||||
@Resource
|
||||
private IMassageTaskService massageTaskService;
|
||||
|
||||
@Resource
|
||||
private IDeviceService deviceService;
|
||||
|
||||
static {
|
||||
try {
|
||||
clientId = InetAddress.getLocalHost().getHostAddress();
|
||||
@ -76,8 +63,6 @@ public class MassageAmqpClient implements ApplicationRunner {
|
||||
//加入监听者
|
||||
((JmsConnection) connection).addConnectionListener(myJmsConnectionListener);
|
||||
// 创建会话。
|
||||
// Session.CLIENT_ACKNOWLEDGE: 收到消息后,需要手动调用message.acknowledge()。
|
||||
// Session.AUTO_ACKNOWLEDGE: SDK自动ACK(推荐)。
|
||||
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
|
||||
connection.start();
|
||||
|
||||
|
@ -7,14 +7,8 @@ import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONArray;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.storm.device.config.properties.HuaWeiIotConfigProperties;
|
||||
import com.storm.device.domain.po.Device;
|
||||
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.processor.DeviceDataCoordinator;
|
||||
import com.storm.device.task.vo.IotMsgNotifyDataPro;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.jms.*;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URI;
|
||||
import java.net.UnknownHostException;
|
||||
import java.text.MessageFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class StatusAmqpClient implements ApplicationRunner {
|
||||
@Autowired
|
||||
private DeviceDataProcessor deviceDataProcessor;
|
||||
private DeviceDataCoordinator deviceDataCoordinator;
|
||||
@Autowired
|
||||
private HuaWeiIotConfigProperties huaWeiIotConfigProperties;
|
||||
|
||||
@ -57,21 +45,6 @@ public class StatusAmqpClient implements ApplicationRunner {
|
||||
//建议使用机器UUID、MAC地址、IP等唯一标识等作为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 {
|
||||
try {
|
||||
clientId = InetAddress.getLocalHost().getHostAddress();
|
||||
@ -219,9 +192,8 @@ public class StatusAmqpClient implements ApplicationRunner {
|
||||
|
||||
// 手动处理日期字段,避免Hutool自动转换失败
|
||||
IotMsgNotifyDataPro iotMsgNotifyData = parseIotMsgNotifyData(notifyData);
|
||||
|
||||
// 使用统一处理器处理所有数据
|
||||
deviceDataProcessor.processDeviceData(iotMsgNotifyData);
|
||||
deviceDataCoordinator.processDeviceData(iotMsgNotifyData);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理消息时发生异常", e);
|
||||
@ -325,46 +297,6 @@ public class StatusAmqpClient implements ApplicationRunner {
|
||||
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 中修复日期解析方法
|
||||
/**
|
||||
* 解析ISO日期时间格式(增强版)
|
||||
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -234,4 +234,129 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
#{id}
|
||||
</foreach>
|
||||
</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>
|
@ -108,27 +108,27 @@
|
||||
ORDER BY startHour, weekDay
|
||||
</select>
|
||||
<!--设备使用总时长统计-->
|
||||
<select id="getDeviceMassageStats" resultType="java.util.Map">
|
||||
SELECT
|
||||
device_name AS deviceName,
|
||||
ROUND(SUM(task_time) / 60, 2) AS totalMassageTime
|
||||
FROM
|
||||
massage_task
|
||||
<where>
|
||||
<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') >= #{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') < DATE_ADD(#{endTime}, INTERVAL 1 DAY)
|
||||
</if>
|
||||
</where>
|
||||
GROUP BY device_name
|
||||
ORDER BY totalMassageTime DESC
|
||||
</select>
|
||||
<!-- <select id="getDeviceMassageStats" resultType="java.util.Map">-->
|
||||
<!-- SELECT-->
|
||||
<!-- device_name AS deviceName,-->
|
||||
<!-- ROUND(SUM(task_time) / 60, 2) AS totalMassageTime-->
|
||||
<!-- FROM-->
|
||||
<!-- massage_task-->
|
||||
<!-- <where>-->
|
||||
<!-- <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') >= #{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') < DATE_ADD(#{endTime}, INTERVAL 1 DAY)-->
|
||||
<!-- </if>-->
|
||||
<!-- </where>-->
|
||||
<!-- GROUP BY device_name-->
|
||||
<!-- ORDER BY totalMassageTime DESC-->
|
||||
<!-- </select>-->
|
||||
<!--获取数据最小时间-->
|
||||
<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
|
||||
@ -258,4 +258,60 @@
|
||||
#{id}
|
||||
</foreach>
|
||||
</delete>
|
||||
|
||||
<!-- 修改设备总按摩时间统计查询 -->
|
||||
<select id="getDeviceMassageStats" resultType="java.util.Map">
|
||||
SELECT
|
||||
device_name AS deviceName,
|
||||
<!-- 确保时长合理:如果小于1分钟则给默认值 -->
|
||||
CASE
|
||||
WHEN ROUND(SUM(task_time) / 60, 2) < 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') >= #{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') < 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>
|
Loading…
x
Reference in New Issue
Block a user