diff --git a/.vite/deps/_metadata.json b/.vite/deps/_metadata.json new file mode 100644 index 0000000..0c37021 --- /dev/null +++ b/.vite/deps/_metadata.json @@ -0,0 +1,8 @@ +{ + "hash": "764c59fb", + "configHash": "64491d45", + "lockfileHash": "e3b0c442", + "browserHash": "4636522a", + "optimized": {}, + "chunks": {} +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 7b016a8..e012065 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ { - "java.compile.nullAnalysis.mode": "automatic" + "java.compile.nullAnalysis.mode": "automatic", + "java.configuration.updateBuildConfiguration": "interactive" } \ No newline at end of file diff --git a/ota/1.1.1.bin b/ota/1.1.1.bin new file mode 100644 index 0000000..7442d21 --- /dev/null +++ b/ota/1.1.1.bin @@ -0,0 +1,138 @@ +``` +String finalNickName = currentUserNickName; +CompletableFuture.runAsync(() -> { + try { + if (uploadFile != null && uploadFile.exists()) { + String planUserIds = dingTalkProperties.getPlanUserId(); + if (StringUtils.isNotBlank(planUserIds)) { + String mediaId = mediaService.uploadMedia("file", uploadFile.getAbsolutePath()); + // 支持多人推送,假设多个 userId 以逗号分隔 + String[] userIds = planUserIds.split(","); + for (String userId : userIds) { + String cleanUserId = userId.trim(); + if (StringUtils.isNotBlank(cleanUserId)) { + try { + robotGroupService.sendFileToUser(cleanUserId, mediaId, originalFilename); + log.info("上传的工艺计划表已成功发送给用户: {}", cleanUserId); + } catch (Exception e) { + log.error("发送文件给用户 {} 异常", cleanUserId, e); + } + } + } + } else { + log.warn("未配置钉钉工艺计划接收人(planUserId),跳过发送文件"); + } + } + } catch (Exception e) { + log.error("发送文件给工艺计划接收人异常", e); + } finally { + if (uploadFile != null && uploadFile.exists()) { + uploadFile.delete(); + } + } +``` + +``` +/** + * 发送文件给个人 + * + * @param userId 接收人userId + * @param mediaId 媒体文件ID + * @param fileName 文件名 + * @return success + */ +public String sendFileToUser(String userId, String mediaId, String fileName) { + try { + Config config = new Config(); + config.protocol = "https"; + config.regionId = "central"; + Client client = new Client(config); + + BatchSendOTOHeaders headers = new BatchSendOTOHeaders(); + headers.xAcsDingtalkAccessToken = tokenManager.getAccessToken(); + + String msgParam = String.format("{\"mediaId\":\"%s\",\"fileName\":\"%s\"}", + mediaId, fileName); + + BatchSendOTORequest request = new BatchSendOTORequest() + .setRobotCode(properties.getRobotCode()) + .setUserIds(Arrays.asList(userId)) + .setMsgKey("sampleFile") + .setMsgParam(msgParam); + + client.batchSendOTOWithOptions(request, headers, new com.aliyun.teautil.models.RuntimeOptions()); + log.info("机器人个人文件发送成功, userId: {}, fileName: {}", userId, fileName); + return "success"; + } catch (Exception e) { + log.error("机器人个人文件发送失败", e); + throw new DingException("机器人个人文件发送失败", e); + } +} +``` + +``` +/** + * 发送Markdown消息给个人 (批量单聊) + * + * @param userId 接收人userId + * @param title 标题 + * @param text Markdown内容 + */ +public void sendMarkdownToUser(String userId, String title, String text) { + try { + Config config = new Config(); + config.protocol = "https"; + config.regionId = "central"; + Client client = new Client(config); + + BatchSendOTOHeaders headers = new BatchSendOTOHeaders(); + headers.xAcsDingtalkAccessToken = tokenManager.getAccessToken(); + + String msgParam = String.format("{\"title\":\"%s\",\"text\":\"%s\"}", + title.replace("\"", "\\\""), + text.replace("\"", "\\\"").replace("\n", "\\n")); + + BatchSendOTORequest request = new BatchSendOTORequest() + .setRobotCode(properties.getRobotCode()) + .setUserIds(Collections.singletonList(userId)) + .setMsgKey("sampleMarkdown") + .setMsgParam(msgParam); + + client.batchSendOTOWithOptions(request, headers, new RuntimeOptions()); + log.info("机器人个人Markdown消息发送成功, userId: {}, title: {}", userId, title); + } catch (Exception e) { + log.error("机器人个人Markdown消息发送失败", e); + throw new DingException("机器人个人Markdown消息发送失败", e); + } +} +``` + +``` +/** + * 刷新AccessToken + */ +private void refresh() { + try { + Config config = new Config(); + config.protocol = "https"; + config.regionId = "central"; + Client client = new Client(config); + GetAccessTokenRequest request = new GetAccessTokenRequest() + .setAppKey(properties.getAppKey()) + .setAppSecret(properties.getAppSecret()); + + GetAccessTokenResponse response = client.getAccessToken(request); + if (response.getBody() != null) { + this.accessToken = response.getBody().getAccessToken(); + // 设置过期时间,提前100秒刷新 + this.expireTime = System.currentTimeMillis() + (response.getBody().getExpireIn() * 1000) - 100000; + log.info("钉钉AccessToken刷新成功,过期时间: {}", this.expireTime); + } else { + throw new DingException("刷新AccessToken失败,返回为空"); + } + } catch (Exception e) { + log.error("刷新钉钉AccessToken失败", e); + throw new DingException("刷新钉钉AccessToken失败: " + e.getMessage(), e); + } +} +``` \ No newline at end of file diff --git a/ota/2.2.0.bin b/ota/2.2.0.bin new file mode 100644 index 0000000..7442d21 --- /dev/null +++ b/ota/2.2.0.bin @@ -0,0 +1,138 @@ +``` +String finalNickName = currentUserNickName; +CompletableFuture.runAsync(() -> { + try { + if (uploadFile != null && uploadFile.exists()) { + String planUserIds = dingTalkProperties.getPlanUserId(); + if (StringUtils.isNotBlank(planUserIds)) { + String mediaId = mediaService.uploadMedia("file", uploadFile.getAbsolutePath()); + // 支持多人推送,假设多个 userId 以逗号分隔 + String[] userIds = planUserIds.split(","); + for (String userId : userIds) { + String cleanUserId = userId.trim(); + if (StringUtils.isNotBlank(cleanUserId)) { + try { + robotGroupService.sendFileToUser(cleanUserId, mediaId, originalFilename); + log.info("上传的工艺计划表已成功发送给用户: {}", cleanUserId); + } catch (Exception e) { + log.error("发送文件给用户 {} 异常", cleanUserId, e); + } + } + } + } else { + log.warn("未配置钉钉工艺计划接收人(planUserId),跳过发送文件"); + } + } + } catch (Exception e) { + log.error("发送文件给工艺计划接收人异常", e); + } finally { + if (uploadFile != null && uploadFile.exists()) { + uploadFile.delete(); + } + } +``` + +``` +/** + * 发送文件给个人 + * + * @param userId 接收人userId + * @param mediaId 媒体文件ID + * @param fileName 文件名 + * @return success + */ +public String sendFileToUser(String userId, String mediaId, String fileName) { + try { + Config config = new Config(); + config.protocol = "https"; + config.regionId = "central"; + Client client = new Client(config); + + BatchSendOTOHeaders headers = new BatchSendOTOHeaders(); + headers.xAcsDingtalkAccessToken = tokenManager.getAccessToken(); + + String msgParam = String.format("{\"mediaId\":\"%s\",\"fileName\":\"%s\"}", + mediaId, fileName); + + BatchSendOTORequest request = new BatchSendOTORequest() + .setRobotCode(properties.getRobotCode()) + .setUserIds(Arrays.asList(userId)) + .setMsgKey("sampleFile") + .setMsgParam(msgParam); + + client.batchSendOTOWithOptions(request, headers, new com.aliyun.teautil.models.RuntimeOptions()); + log.info("机器人个人文件发送成功, userId: {}, fileName: {}", userId, fileName); + return "success"; + } catch (Exception e) { + log.error("机器人个人文件发送失败", e); + throw new DingException("机器人个人文件发送失败", e); + } +} +``` + +``` +/** + * 发送Markdown消息给个人 (批量单聊) + * + * @param userId 接收人userId + * @param title 标题 + * @param text Markdown内容 + */ +public void sendMarkdownToUser(String userId, String title, String text) { + try { + Config config = new Config(); + config.protocol = "https"; + config.regionId = "central"; + Client client = new Client(config); + + BatchSendOTOHeaders headers = new BatchSendOTOHeaders(); + headers.xAcsDingtalkAccessToken = tokenManager.getAccessToken(); + + String msgParam = String.format("{\"title\":\"%s\",\"text\":\"%s\"}", + title.replace("\"", "\\\""), + text.replace("\"", "\\\"").replace("\n", "\\n")); + + BatchSendOTORequest request = new BatchSendOTORequest() + .setRobotCode(properties.getRobotCode()) + .setUserIds(Collections.singletonList(userId)) + .setMsgKey("sampleMarkdown") + .setMsgParam(msgParam); + + client.batchSendOTOWithOptions(request, headers, new RuntimeOptions()); + log.info("机器人个人Markdown消息发送成功, userId: {}, title: {}", userId, title); + } catch (Exception e) { + log.error("机器人个人Markdown消息发送失败", e); + throw new DingException("机器人个人Markdown消息发送失败", e); + } +} +``` + +``` +/** + * 刷新AccessToken + */ +private void refresh() { + try { + Config config = new Config(); + config.protocol = "https"; + config.regionId = "central"; + Client client = new Client(config); + GetAccessTokenRequest request = new GetAccessTokenRequest() + .setAppKey(properties.getAppKey()) + .setAppSecret(properties.getAppSecret()); + + GetAccessTokenResponse response = client.getAccessToken(request); + if (response.getBody() != null) { + this.accessToken = response.getBody().getAccessToken(); + // 设置过期时间,提前100秒刷新 + this.expireTime = System.currentTimeMillis() + (response.getBody().getExpireIn() * 1000) - 100000; + log.info("钉钉AccessToken刷新成功,过期时间: {}", this.expireTime); + } else { + throw new DingException("刷新AccessToken失败,返回为空"); + } + } catch (Exception e) { + log.error("刷新钉钉AccessToken失败", e); + throw new DingException("刷新钉钉AccessToken失败: " + e.getMessage(), e); + } +} +``` \ No newline at end of file diff --git a/pom.xml b/pom.xml index 01e3267..c48a9ea 100644 --- a/pom.xml +++ b/pom.xml @@ -22,7 +22,7 @@ UTF-8 UTF-8 - 1.8 + 16 3.1.1 1.4.7 2.0.58 @@ -30,6 +30,7 @@ 2.19.0 1.21 0.9.1 + 8.0.33 2.3.3 3.0.0 4.1.2 @@ -44,6 +45,12 @@ + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot @@ -102,8 +109,9 @@ - mysql - mysql-connector-java + com.mysql + mysql-connector-j + ${mysql.version} @@ -276,12 +284,6 @@ lombok provided - - io.springfox - springfox-boot-starter - 3.0.0 - - @@ -298,8 +300,8 @@ org.apache.maven.plugins maven-compiler-plugin - 16 - 16 + ${java.version} + ${java.version} diff --git a/src/main/java/com/evobms/common/constant/BboxApiConstants.java b/src/main/java/com/evobms/common/constant/BboxApiConstants.java index 1509fee..332f3ca 100644 --- a/src/main/java/com/evobms/common/constant/BboxApiConstants.java +++ b/src/main/java/com/evobms/common/constant/BboxApiConstants.java @@ -15,6 +15,7 @@ public final class BboxApiConstants { public static final int CODE_DEVICE_NOT_REGISTERED = 901; // 设备未在平台注册,如平台未录入 public static final int CODE_DEVICE_DISABLED = 902; // 设备已被禁用 public static final int CODE_FILE_NOT_FOUND = 903; // 文件不存在 + public static final int CODE_PARAM_INVALID = 904; // 参数无效/缺失 // 对应消息文本(可用于统一提示) public static final String MSG_SUCCESS = "成功"; @@ -23,4 +24,5 @@ public final class BboxApiConstants { public static final String MSG_DEVICE_NOT_REGISTERED = "设备未在平台注册,如平台未录入"; public static final String MSG_DEVICE_DISABLED = "设备已被禁用"; public static final String MSG_FILE_NOT_FOUND = "文件不存在"; -} \ No newline at end of file + public static final String MSG_PARAM_INVALID = "参数无效"; +} diff --git a/src/main/java/com/evobms/common/utils/PageUtils.java b/src/main/java/com/evobms/common/utils/PageUtils.java index df45a6f..622e5c8 100644 --- a/src/main/java/com/evobms/common/utils/PageUtils.java +++ b/src/main/java/com/evobms/common/utils/PageUtils.java @@ -12,17 +12,61 @@ import com.evobms.framework.web.page.TableSupport; */ public class PageUtils extends PageHelper { + /** 设备历史平铺列表允许的每页条数 */ + public static final int[] HISTORY_PAGE_SIZES = {10, 20, 100, 500}; + /** * 设置请求分页数据 */ public static void startPage() + { + startPage(true); + } + + /** + * @param doCount 是否执行 COUNT;大数据量历史列表可传 false 避免扫行计数超时 + */ + public static void startPage(boolean doCount) { PageDomain pageDomain = TableSupport.buildPageRequest(); Integer pageNum = pageDomain.getPageNum(); Integer pageSize = pageDomain.getPageSize(); String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy()); Boolean reasonable = pageDomain.getReasonable(); - PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(reasonable); + PageHelper.startPage(pageNum, pageSize, doCount, null, null) + .setOrderBy(orderBy) + .setReasonable(reasonable); + } + + /** + * 历史平铺:对时间范围内全部 vehicle_data 做库内分页;pageSize 规范为 10/20/100/500。 + * + * @return 实际使用的每页条数 + */ + public static int startHistoryPage(boolean doCount) { + PageDomain pageDomain = TableSupport.buildPageRequest(); + int pageNum = pageDomain.getPageNum() == null || pageDomain.getPageNum() < 1 ? 1 : pageDomain.getPageNum(); + int pageSize = normalizeHistoryPageSize(pageDomain.getPageSize()); + String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy()); + Boolean reasonable = pageDomain.getReasonable(); + PageHelper.startPage(pageNum, pageSize, doCount, null, null) + .setOrderBy(orderBy) + .setReasonable(reasonable); + return pageSize; + } + + /** 将请求的 pageSize 收敛到 10、20、100、500(不足 10 按 10) */ + public static int normalizeHistoryPageSize(Integer requested) { + if (requested == null || requested <= 0) { + return HISTORY_PAGE_SIZES[0]; + } + int chosen = HISTORY_PAGE_SIZES[0]; + for (int allowed : HISTORY_PAGE_SIZES) { + if (requested >= allowed) { + chosen = allowed; + } + } + return chosen; } /** diff --git a/src/main/java/com/evobms/framework/aspectj/lang/annotation/Excel.java b/src/main/java/com/evobms/framework/aspectj/lang/annotation/Excel.java index 215dc3f..05265c2 100644 --- a/src/main/java/com/evobms/framework/aspectj/lang/annotation/Excel.java +++ b/src/main/java/com/evobms/framework/aspectj/lang/annotation/Excel.java @@ -1,13 +1,14 @@ package com.evobms.framework.aspectj.lang.annotation; +import com.evobms.common.utils.poi.ExcelHandlerAdapter; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.IndexedColors; + import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.math.BigDecimal; -import org.apache.poi.ss.usermodel.HorizontalAlignment; -import org.apache.poi.ss.usermodel.IndexedColors; -import com.evobms.common.utils.poi.ExcelHandlerAdapter; /** * 自定义导出Excel数据注解 diff --git a/src/main/java/com/evobms/framework/config/MqttConfig.java b/src/main/java/com/evobms/framework/config/MqttConfig.java index b671597..01140ac 100644 --- a/src/main/java/com/evobms/framework/config/MqttConfig.java +++ b/src/main/java/com/evobms/framework/config/MqttConfig.java @@ -10,7 +10,9 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.integration.annotation.ServiceActivator; import org.springframework.integration.channel.DirectChannel; +import org.springframework.integration.channel.ExecutorChannel; import org.springframework.integration.core.MessageProducer; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.integration.handler.AbstractMessageHandler; import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory; import org.springframework.integration.mqtt.core.MqttPahoClientFactory; @@ -60,6 +62,9 @@ public class MqttConfig { @Value("${mqtt.cleanSession}") private boolean cleanSession; + @Value("${mqtt.completionTimeout:20000}") + private int completionTimeout; + // 支持逗号分隔的多个主题,例如:/bbox/EVO0002/#,/bbox/000010000200003/# @Value("#{'${mqtt.subscribeTopic}'.split(',')}") private String[] subscribeTopics; @@ -87,11 +92,17 @@ public class MqttConfig { } /** - * MQTT消息接收通道 + * MQTT消息接收通道 (异步处理,防止入库慢阻塞MQTT底层网络线程导致掉线) */ @Bean public MessageChannel mqttInputChannel() { - return new DirectChannel(); + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(10); + executor.setMaxPoolSize(50); + executor.setQueueCapacity(1000); + executor.setThreadNamePrefix("mqtt-handler-"); + executor.initialize(); + return new ExecutorChannel(executor); } /** @@ -108,7 +119,7 @@ public class MqttConfig { MqttPahoMessageDrivenChannelAdapter adapter = new MqttPahoMessageDrivenChannelAdapter(clientId + "_inbound", mqttClientFactory(), topics); - adapter.setCompletionTimeout(5000); + adapter.setCompletionTimeout(completionTimeout); // 使用字节载荷转换器,保留原始二进制帧 DefaultPahoMessageConverter converter = new DefaultPahoMessageConverter(); converter.setPayloadAsBytes(true); diff --git a/src/main/java/com/evobms/project/battery/service/ISubsystemTemperatureService.java b/src/main/java/com/evobms/project/battery/service/ISubsystemTemperatureService.java index f9dfe0b..3c2630d 100644 --- a/src/main/java/com/evobms/project/battery/service/ISubsystemTemperatureService.java +++ b/src/main/java/com/evobms/project/battery/service/ISubsystemTemperatureService.java @@ -1,6 +1,7 @@ package com.evobms.project.battery.service; import java.util.List; +import com.baomidou.mybatisplus.extension.service.IService; import com.evobms.project.battery.domain.SubsystemTemperature; /** @@ -9,7 +10,7 @@ import com.evobms.project.battery.domain.SubsystemTemperature; * @author 田志阳 * @date 2025-11-15 */ -public interface ISubsystemTemperatureService +public interface ISubsystemTemperatureService extends IService { /** * 查询可充电储能子系统温度信息 diff --git a/src/main/java/com/evobms/project/battery/service/ISubsystemVoltageService.java b/src/main/java/com/evobms/project/battery/service/ISubsystemVoltageService.java index 1e89c1c..7f00bda 100644 --- a/src/main/java/com/evobms/project/battery/service/ISubsystemVoltageService.java +++ b/src/main/java/com/evobms/project/battery/service/ISubsystemVoltageService.java @@ -1,6 +1,7 @@ package com.evobms.project.battery.service; import java.util.List; +import com.baomidou.mybatisplus.extension.service.IService; import com.evobms.project.battery.domain.SubsystemVoltage; /** @@ -9,7 +10,7 @@ import com.evobms.project.battery.domain.SubsystemVoltage; * @author 田志阳 * @date 2025-11-15 */ -public interface ISubsystemVoltageService +public interface ISubsystemVoltageService extends IService { /** * 查询可充电储能子系统电压信息 diff --git a/src/main/java/com/evobms/project/bms/controller/GBT32960MqttReceiver.java b/src/main/java/com/evobms/project/bms/controller/GBT32960MqttReceiver.java index 78ba944..9d7bd30 100644 --- a/src/main/java/com/evobms/project/bms/controller/GBT32960MqttReceiver.java +++ b/src/main/java/com/evobms/project/bms/controller/GBT32960MqttReceiver.java @@ -8,7 +8,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration -@ConditionalOnProperty(name = "mqtt.enabled", havingValue = "true") +@ConditionalOnProperty(name = "mqtt.legacyReceiver.enabled", havingValue = "true") public class GBT32960MqttReceiver { @Value("${mqtt.host}") private String broker; diff --git a/src/main/java/com/evobms/project/bms/domain/BmsChargeDetail.java b/src/main/java/com/evobms/project/bms/domain/BmsChargeDetail.java index a740017..8c0edae 100644 --- a/src/main/java/com/evobms/project/bms/domain/BmsChargeDetail.java +++ b/src/main/java/com/evobms/project/bms/domain/BmsChargeDetail.java @@ -4,6 +4,7 @@ import java.math.BigDecimal; import java.util.Date; import com.fasterxml.jackson.annotation.JsonFormat; import com.github.xiaoymin.knife4j.annotations.Ignore; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.EqualsAndHashCode; import org.apache.commons.lang3.builder.ToStringBuilder; @@ -32,55 +33,68 @@ public class BmsChargeDetail extends BaseEntity /** 数据时间戳 */ @JsonFormat(pattern = "yyyy-MM-dd") + @ApiModelProperty(value = "数据时间戳") @Excel(name = "数据时间戳", width = 30, dateFormat = "yyyy-MM-dd") private Date timestamp; /** 慢充电流(A) */ @Excel(name = "慢充电流(A)") + @ApiModelProperty(value = "慢充电流(A)") private BigDecimal acChargeCurrent; /** 慢充电压(V) */ @Excel(name = "慢充电压(V)") + @ApiModelProperty(value = "慢充电压(V)") private BigDecimal acChargeVoltage; /** 快充电流(A) */ @Excel(name = "快充电流(A)") + @ApiModelProperty(value = "快充电流(A)") private BigDecimal dcChargeCurrent; /** 快充电压(V) */ @Excel(name = "快充电压(V)") + @ApiModelProperty(value = "快充电压(V)") private BigDecimal dcChargeVoltage; /** 充电时长(min) */ @Excel(name = "充电时长(min)") + @ApiModelProperty(value = "充电时长min") private Long chargeTime; /** 慢充停止码 */ @Excel(name = "慢充停止码") + @ApiModelProperty(value = "慢充停止码") private Integer acStopCode; /** 快充停止码 */ @Excel(name = "快充停止码") + @ApiModelProperty(value = "快充停止码") private Integer dcStopCode; /** 慢充CP值 */ @Excel(name = "慢充CP值") + @ApiModelProperty(value = "慢充CP值") private Integer acCpValue; /** 慢充枪阻值 */ @Excel(name = "慢充枪阻值") + @ApiModelProperty(value = "慢充枪阻值") private Integer acGunResistance; /** 充电阶段 */ @Excel(name = "充电阶段") + @ApiModelProperty(value = "充电阶段") private Integer chargingStage; /** 慢充阶段 */ @Excel(name = "慢充阶段") + @ApiModelProperty(value = "慢充阶段") private Integer acStage; /** 快充阶段 */ @Excel(name = "快充阶段") + @ApiModelProperty(value = "快充阶段") private Integer dcStage; diff --git a/src/main/java/com/evobms/project/bms/domain/BmsChargeTemperature.java b/src/main/java/com/evobms/project/bms/domain/BmsChargeTemperature.java index e169c0c..b13503d 100644 --- a/src/main/java/com/evobms/project/bms/domain/BmsChargeTemperature.java +++ b/src/main/java/com/evobms/project/bms/domain/BmsChargeTemperature.java @@ -4,6 +4,7 @@ import java.io.Serial; import java.math.BigDecimal; import java.util.Date; import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.EqualsAndHashCode; import org.apache.commons.lang3.builder.ToStringBuilder; @@ -34,14 +35,17 @@ public class BmsChargeTemperature extends BaseEntity /** 数据时间戳 */ @JsonFormat(pattern = "yyyy-MM-dd") @Excel(name = "数据时间戳", width = 30, dateFormat = "yyyy-MM-dd") - private Date timestap; + @ApiModelProperty(value = "数据时间戳") + private Date timestamp; /** 温度编号(1-8) */ @Excel(name = "温度编号(1-8)") + @ApiModelProperty(value = "温度编号(1-8)") private Integer tempIndex; /** 温度(℃) */ @Excel(name = "温度(℃)") + @ApiModelProperty(value = "温度(℃)") private BigDecimal temperature; } diff --git a/src/main/java/com/evobms/project/bms/domain/BmsRuntimeData.java b/src/main/java/com/evobms/project/bms/domain/BmsRuntimeData.java index bc9ed29..0e26eee 100644 --- a/src/main/java/com/evobms/project/bms/domain/BmsRuntimeData.java +++ b/src/main/java/com/evobms/project/bms/domain/BmsRuntimeData.java @@ -1,8 +1,10 @@ package com.evobms.project.bms.domain; +import java.io.Serial; import java.math.BigDecimal; import java.util.Date; import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.EqualsAndHashCode; import org.apache.commons.lang3.builder.ToStringBuilder; @@ -20,6 +22,7 @@ import com.evobms.framework.web.domain.BaseEntity; @EqualsAndHashCode(callSuper = false) public class BmsRuntimeData extends BaseEntity { + @Serial private static final long serialVersionUID = 1L; /** 主键ID */ @@ -31,47 +34,58 @@ public class BmsRuntimeData extends BaseEntity /** 数据时间戳 */ @JsonFormat(pattern = "yyyy-MM-dd") + @ApiModelProperty(value = "数据时间戳") @Excel(name = "数据时间戳", width = 30, dateFormat = "yyyy-MM-dd") - private Date timestap; + private Date timestamp; /** 车速(km/h) */ @Excel(name = "车速(km/h)") + @ApiModelProperty(value = "车速(km/h)") private BigDecimal vehicleSpeed; /** 总里程(km) */ @Excel(name = "总里程(km)") + @ApiModelProperty(value = "总里程(km)") private Long mileage; /** 系统电压(V) */ @Excel(name = "系统电压(V)") + @ApiModelProperty(value = "系统电压(V)") private BigDecimal systemVoltage; /** 系统电流(A) */ @Excel(name = "系统电流(A)") + @ApiModelProperty(value = "系统电流(A)") private BigDecimal systemCurrent; /** 绝缘电阻(KΩ) */ @Excel(name = "绝缘电阻(KΩ)") + @ApiModelProperty(value = "绝缘电阻(KΩ)") private Long insulationResistance; /** 最大SOC(%) */ @Excel(name = "最大SOC(%)") + @ApiModelProperty(value = "最大SOC(%)") private BigDecimal socMax; /** 最小SOC(%) */ @Excel(name = "最小SOC(%)") + @ApiModelProperty(value = "最小SOC(%)") private BigDecimal socMin; /** GPV电压(V) */ @Excel(name = "GPV电压(V)") + @ApiModelProperty(value = "GPV电压(V)") private BigDecimal gpvVoltage; /** 行车阶段 */ @Excel(name = "行车阶段") + @ApiModelProperty(value = "行车阶段") private Integer runStatus; /** 行车上电阶段 */ @Excel(name = "行车上电阶段") + @ApiModelProperty(value = "行车上电阶段") private Integer powerStatus; } diff --git a/src/main/java/com/evobms/project/bms/domain/BmsSopData.java b/src/main/java/com/evobms/project/bms/domain/BmsSopData.java index 801de71..dd09933 100644 --- a/src/main/java/com/evobms/project/bms/domain/BmsSopData.java +++ b/src/main/java/com/evobms/project/bms/domain/BmsSopData.java @@ -2,6 +2,8 @@ package com.evobms.project.bms.domain; import java.util.Date; import com.fasterxml.jackson.annotation.JsonFormat; +import com.baomidou.mybatisplus.annotation.TableField; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.EqualsAndHashCode; import org.apache.commons.lang3.builder.ToStringBuilder; @@ -30,55 +32,79 @@ public class BmsSopData extends BaseEntity /** 数据时间戳 */ @JsonFormat(pattern = "yyyy-MM-dd") + @ApiModelProperty(value = "数据时间戳") @Excel(name = "数据时间戳", width = 30, dateFormat = "yyyy-MM-dd") - private Date timestap; + @TableField("`timestamp`") + private Date timestamp; /** 10秒放电SOP(A) */ @Excel(name = "10秒放电SOP(A)") + @ApiModelProperty(value = "10秒放电SOP(A)") + @TableField("discharge_sop_10s") private Long dischargeSop10s; /** 30秒放电SOP(A) */ @Excel(name = "30秒放电SOP(A)") + @ApiModelProperty(value = "30秒放电SOP(A)") + @TableField("discharge_sop_30s") private Long dischargeSop30s; /** 60秒放电SOP(A) */ @Excel(name = "60秒放电SOP(A)") + @ApiModelProperty(value = "60秒放电SOP(A)") + @TableField("discharge_sop_60s") private Long dischargeSop60s; /** 持续放电SOP(A) */ @Excel(name = "持续放电SOP(A)") + @ApiModelProperty(value = "持续放电SOP(A)") + @TableField("discharge_sop_continuous") private Long dischargeSopContinuous; /** 放电融合SOP(A) */ @Excel(name = "放电融合SOP(A)") + @ApiModelProperty(value = "放电融合SOP(A)") private Long dischargeSopTotal; /** 10秒回馈SOP(A) */ @Excel(name = "10秒回馈SOP(A)") + @ApiModelProperty(value = "10秒回馈SOP(A)") + @TableField("regen_sop_10s") private Long regenSop10s; /** 30秒回馈SOP(A) */ @Excel(name = "30秒回馈SOP(A)") + @ApiModelProperty(value = "30秒回馈SOP(A)") + @TableField("regen_sop_30s") private Long regenSop30s; /** 60秒回馈SOP(A) */ @Excel(name = "60秒回馈SOP(A)") + @ApiModelProperty(value = "60秒回馈SOP(A)") + @TableField("regen_sop_60s") private Long regenSop60s; /** 持续回馈SOP(A) */ @Excel(name = "持续回馈SOP(A)") + @ApiModelProperty(value = "持续回馈SOP(A)") + @TableField("regen_sop_continuous") private Long regenSopContinuous; /** 回馈融合SOP(A) */ @Excel(name = "回馈融合SOP(A)") + @ApiModelProperty(value = "回馈融合SOP(A)") private Long regenSopTotal; /** 充电SOP(A) */ @Excel(name = "充电SOP(A)") + @ApiModelProperty(value = "充电SOP(A)") + @TableField("charge_sop") private Long chargeSop; /** 慢充SOP(A) */ @Excel(name = "慢充SOP(A)") + @ApiModelProperty(value = "慢充SOP(A)") + @TableField("ac_charge_sop") private Long acChargeSop; diff --git a/src/main/java/com/evobms/project/bms/domain/BmsSystemStatus.java b/src/main/java/com/evobms/project/bms/domain/BmsSystemStatus.java index 913d019..70e3ae9 100644 --- a/src/main/java/com/evobms/project/bms/domain/BmsSystemStatus.java +++ b/src/main/java/com/evobms/project/bms/domain/BmsSystemStatus.java @@ -3,6 +3,8 @@ package com.evobms.project.bms.domain; import java.math.BigDecimal; import java.util.Date; import com.fasterxml.jackson.annotation.JsonFormat; +import com.baomidou.mybatisplus.annotation.TableField; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.EqualsAndHashCode; import org.apache.commons.lang3.builder.ToStringBuilder; @@ -32,30 +34,49 @@ public class BmsSystemStatus extends BaseEntity /** 数据时间戳 */ @JsonFormat(pattern = "yyyy-MM-dd") @Excel(name = "数据时间戳", width = 30, dateFormat = "yyyy-MM-dd") - private Date timestap; + @ApiModelProperty(value = "数据时间戳") + private Date timestamp; /** 继电器状态 */ @Excel(name = "继电器状态") + @ApiModelProperty(value = "继电器状态") private Long relayStatus; /** ON/A+/CC状态 */ @Excel(name = "ON/A+/CC状态") + @ApiModelProperty(value = "ON/A+/CC状态") private Long onaaccStatus; /** 系统复位次数 */ @Excel(name = "系统复位次数") + @ApiModelProperty(value = "系统复位次数") private Long resetCount; /** 电池循环次数 */ @Excel(name = "电池循环次数") + @ApiModelProperty(value = "电池循环次数") private Long cycleCount; + /** 电池循环次数 */ + @Excel(name = "电池芯单体数") + @ApiModelProperty(value = "电池芯单体数") + private Long cellCount; /** 2.5V采样电压(V) */ @Excel(name = "2.5V采样电压(V)") + @ApiModelProperty(value = "2.5V采样电压(V)") + @TableField("sample_25v") private BigDecimal sample25v; /** 5V采样电压(V) */ @Excel(name = "5V采样电压(V)") + @ApiModelProperty(value = "5V采样电压(V)") + @TableField("sample_5v") private BigDecimal sample5v; + /** 测温点数量(V) */ + @Excel(name = "测温点数量(V)") + @ApiModelProperty(value = "测温点数量(V)") + @TableField("temp_sensor_count") + private Integer tempSensorCount; + } diff --git a/src/main/java/com/evobms/project/bms/domain/BmsVoltageChannelData.java b/src/main/java/com/evobms/project/bms/domain/BmsVoltageChannelData.java index df9ba05..02c6bcf 100644 --- a/src/main/java/com/evobms/project/bms/domain/BmsVoltageChannelData.java +++ b/src/main/java/com/evobms/project/bms/domain/BmsVoltageChannelData.java @@ -3,6 +3,7 @@ package com.evobms.project.bms.domain; import java.math.BigDecimal; import java.util.Date; import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.EqualsAndHashCode; import org.apache.commons.lang3.builder.ToStringBuilder; @@ -32,18 +33,22 @@ public class BmsVoltageChannelData extends BaseEntity /** 数据时间戳 */ @JsonFormat(pattern = "yyyy-MM-dd") @Excel(name = "数据时间戳", width = 30, dateFormat = "yyyy-MM-dd") - private Date timestap; + @ApiModelProperty(value = "数据时间戳") + private Date timestamp; /** 通道类型(RTP/RTN) */ @Excel(name = "通道类型(RTP/RTN)") + @ApiModelProperty(value = "通道类型(RTP/RTN)") private String channelType; /** 通道编号 */ @Excel(name = "通道编号") + @ApiModelProperty(value = "通道编号") private Integer channelIndex; /** 电压(V) */ @Excel(name = "电压(V)") + @ApiModelProperty(value = "电压(V)") private BigDecimal voltage; } diff --git a/src/main/java/com/evobms/project/bms/service/MqttService.java b/src/main/java/com/evobms/project/bms/service/MqttService.java index 0b4b073..c601a85 100644 --- a/src/main/java/com/evobms/project/bms/service/MqttService.java +++ b/src/main/java/com/evobms/project/bms/service/MqttService.java @@ -289,7 +289,7 @@ public class MqttService { // 7. 累计充放电量 - 绝对索引543-553(11字节) | 相对偏移: 519-529 parseChargeDischargeAtIndex(message, 519, deviceSn, frameTime); - parseExtendedDataAtIndex(message, 506, deviceSn, frameTime); + parseExtendedDataAtIndex(message, 530, deviceSn, frameTime); log.info("设备 {} 所有数据段解析完成", deviceSn); @@ -341,40 +341,40 @@ public class MqttService { private void parseVehicleDataAtIndex(byte[] message, int startIndex, String deviceSn, Date frameTime) { try { if (message.length < startIndex + 20) { - log.warn("设备 {}: 整车数据索引越界", deviceSn); + /// log.warn("设备 {}: 整车数据索引越界", deviceSn); return; } // 索引30: 数据标识 (应该是0x01) byte dataFlag = message[startIndex]; if (dataFlag != 0x01) { - log.warn("设备 {}: 整车数据标识错误: 0x{}", deviceSn, String.format("%02X", dataFlag)); + // log.warn("设备 {}: 整车数据标识错误: 0x{}", deviceSn, String.format("%02X", dataFlag)); return; } - log.info("设备 {} - 发现整车数据", deviceSn); + // log.info("设备 {} - 发现整车数据", deviceSn); // 索引32: 充电状态 byte chargeStatus = message[startIndex + 2]; - log.info("设备 {} - 充电状态: 0x{} ({})", deviceSn, String.format("%02X", chargeStatus), getChargeStatusDescription(chargeStatus)); + // log.info("设备 {} - 充电状态: 0x{} ({})", deviceSn, String.format("%02X", chargeStatus), getChargeStatusDescription(chargeStatus)); // 索引40-41: 总电压 (0.1V) int totalVoltage = ((message[startIndex + 10] & 0xFF) << 8) | (message[startIndex + 11] & 0xFF); double voltageValue = totalVoltage * 0.1; - log.info("设备 {} - 总电压: {} V", deviceSn, voltageValue); + //log.info("设备 {} - 总电压: {} V", deviceSn, voltageValue); // 索引42-43: 总电流 (0.1A, 偏移10000) int totalCurrent = ((message[startIndex + 12] & 0xFF) << 8) | (message[startIndex + 13] & 0xFF); double currentValue = (totalCurrent - 10000) * 0.1; - log.info("设备 {} - 总电流: {} A", deviceSn, currentValue); + // log.info("设备 {} - 总电流: {} A", deviceSn, currentValue); // 索引44: SOC (%) int soc = message[startIndex + 14] & 0xFF; - log.info("设备 {} - SOC: {} %", deviceSn, soc); + //log.info("设备 {} - SOC: {} %", deviceSn, soc); // 索引47-48: 正极绝缘电阻 (Ω) int insulationResistance = ((message[startIndex + 17] & 0xFF) << 8) | (message[startIndex + 18] & 0xFF); - log.info("设备 {} - 正极绝缘电阻: {} Ω", deviceSn, insulationResistance); + // log.info("设备 {} - 正极绝缘电阻: {} Ω", deviceSn, insulationResistance); // 保存整车数据 saveVehicleData(deviceSn, voltageValue, currentValue, soc, chargeStatus, insulationResistance, frameTime); @@ -649,7 +649,7 @@ public class MqttService { return; } - log.info("设备 {} - 发现累计充放电数据", deviceSn); + // log.info("设备 {} - 发现累计充放电数据", deviceSn); // 跳过索引544-545的保留字节 // 索引546-549: 累计放电电量 (0.1kWh) @@ -661,7 +661,7 @@ public class MqttService { double dischargeKwh = dischargeEnergy * 0.1; double chargeKwh = chargeEnergy * 0.1; - log.info("设备 {} - 累计充放电量: 放电{}kWh, 充电{}kWh", deviceSn, dischargeKwh, chargeKwh); + //log.info("设备 {} - 累计充放电量: 放电{}kWh, 充电{}kWh", deviceSn, dischargeKwh, chargeKwh); saveChargeDischargeSummary(deviceSn, dischargeKwh, chargeKwh, frameTime); @@ -1041,7 +1041,7 @@ public class MqttService { BmsRuntimeData rd = new BmsRuntimeData(); rd.setDeviceId(deviceSn); - rd.setTimestap(frameTime); + rd.setTimestamp(frameTime); rd.setVehicleSpeed(BigDecimal.valueOf(carSpd)); rd.setMileage(mileage); rd.setSystemVoltage(BigDecimal.valueOf(sysVol)); @@ -1058,7 +1058,7 @@ public class MqttService { BmsSopData sop = new BmsSopData(); sop.setDeviceId(deviceSn); - sop.setTimestap(frameTime); + sop.setTimestamp(frameTime); sop.setDischargeSop10s(disSop10); sop.setDischargeSop30s(disSop30); sop.setDischargeSop60s(disSop60); @@ -1075,7 +1075,7 @@ public class MqttService { BmsSystemStatus ss = new BmsSystemStatus(); ss.setDeviceId(deviceSn); - ss.setTimestap(frameTime); + ss.setTimestamp(frameTime); ss.setRelayStatus((long) relayStatus); ss.setOnaaccStatus((long) onaaccStatus); ss.setResetCount(resetCount); @@ -1093,7 +1093,7 @@ public class MqttService { for (int i = 0; i < chargeTemps.length; i++) { BmsChargeTemperature ct = new BmsChargeTemperature(); ct.setDeviceId(deviceSn); - ct.setTimestap(frameTime); + ct.setTimestamp(frameTime); ct.setTempIndex(i + 1); ct.setTemperature(BigDecimal.valueOf(chargeTemps[i])); ct.setCreateTime(new Date()); @@ -1107,7 +1107,7 @@ public class MqttService { for (int i = 0; i < rtpVals.length; i++) { BmsVoltageChannelData ch = new BmsVoltageChannelData(); ch.setDeviceId(deviceSn); - ch.setTimestap(frameTime); + ch.setTimestamp(frameTime); ch.setChannelType("RTP"); ch.setChannelIndex(i + 1); ch.setVoltage(BigDecimal.valueOf(rtpVals[i])); diff --git a/src/main/java/com/evobms/project/iot/controller/IotAuthController.java b/src/main/java/com/evobms/project/iot/controller/IotAuthController.java index a9581d1..d2c6413 100644 --- a/src/main/java/com/evobms/project/iot/controller/IotAuthController.java +++ b/src/main/java/com/evobms/project/iot/controller/IotAuthController.java @@ -9,6 +9,13 @@ import com.evobms.project.iot.service.DeviceTokenService; import com.evobms.project.system.domain.BmsDevices; import com.evobms.project.system.service.IBmsDevicesService; import com.evobms.project.ota.service.IOtaTasksService; +import com.evobms.project.ota.domain.OtaTasks; +import com.evobms.project.iot.domain.dto.OtaUploadDto; +import com.evobms.project.iot.domain.dto.OtaLoginDto; +import com.evobms.project.iot.domain.dto.OtaHistoryResultDTO; +import com.evobms.project.iot.domain.dto.OtaProgressResultDTO; +import com.evobms.project.iot.domain.dto.OtaUploadResultDTO; +import org.springframework.web.multipart.MultipartFile; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.v3.oas.annotations.Operation; @@ -33,6 +40,7 @@ import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.InputStream; import java.net.URI; @@ -42,10 +50,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.security.SecureRandom; import java.security.MessageDigest; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; /** * 设备鉴权令牌接口 @@ -57,7 +62,6 @@ import java.util.Map; @RequestMapping("/iot") @Slf4j public class IotAuthController { - private static final Logger log = LoggerFactory.getLogger(IotAuthController.class); @Autowired(required = false) private IBmsDevicesService bmsDevicesService; @@ -68,9 +72,13 @@ public class IotAuthController { @Value("${token.header:Authorization}") private String tokenHeader; - @Value("${mqtt.host:tcp://192.168.5.23:1883}") + @Value("${mqtt.host:tcp://61.182.73.218:11883}") private String mqttHostUri; - @Value("${mqtt.username:00000001}") + @Value("${mqtt.publicHost:61.182.73.218}") + private String mqttPublicHost; + @Value("${mqtt.publicPort:11883}") + private int mqttPublicPort; + @Value("${mqtt.username:`00000001`}") private String mqttUsername; @Value("${mqtt.password:741a03a10f3de6b2}") private String mqttPassword; @@ -82,7 +90,7 @@ public class IotAuthController { private String otaCount; // OTA文件存储位置:支持 classpath 或 文件系统目录,例如:classpath:/ota 或 /data/ota - @Value("${ota.repo:D:/OTA}") + @Value("${ota.repo:./ota}") private String otaRepo; // OTA分片大小(字节),规范为1KB=1024字节 @@ -187,19 +195,21 @@ public class IotAuthController { return resp; } - // 解析 MQTT 主机与端口 - String host = "192.168.5.23"; - int port = 1883; - try { - URI uri = new URI(mqttHostUri); - if (uri.getHost() != null) { - host = uri.getHost(); + String host = safeTrim(mqttPublicHost); + Integer port = mqttPublicPort > 0 ? mqttPublicPort : null; + if (StringUtils.isEmpty(host) || port == null) { + try { + URI uri = new URI(mqttHostUri); + if (StringUtils.isEmpty(host) && uri.getHost() != null && !uri.getHost().isEmpty()) { + host = uri.getHost(); + } + if (port == null && uri.getPort() != -1) { + port = uri.getPort(); + } + } catch (Exception ignore) { } - if (uri.getPort() != -1) { - port = uri.getPort(); - } - } catch (Exception ignore) { } + if (port == null || port <= 0) port = 11883; String use = fixLengthRight(mqttUsername, 8); String pass = fixLengthRight(mqttPassword, 16); @@ -333,7 +343,7 @@ public class IotAuthController { // 若存在目标版本,对应固件可用时,动态计算分片总数(总长度含 64 字节校验头);否则回退配置值 Integer dynamicCount = computeOtaChunkCount(targetVersion); if (dynamicCount != null && dynamicCount > 0) { - // 转为 3 位字符串(左侧补零),例如 7 -> "007" + // 转为 3 位字符串(左侧补零), 7 -> "007" count = fixLengthLeft(String.valueOf(dynamicCount), 3); } else { // 回退使用配置 `ota.count`,默认 "001" @@ -450,7 +460,7 @@ public class IotAuthController { try { int previewLen = Math.min(otaChunkSize, firmwareBytes); byte[] preview = Arrays.copyOfRange(firmware, 0, previewLen); - // 变更说明:headerSHA256日志打印为“首片1024字节数据”的摘要,而非整包 + // headerSHA256日志打印为“首片1024字节数据”的摘要,而非整包 String headerHex = sha256Hex(preview); log.info("OTA下载开始: sn={} version={} headerSHA256={} headerMode={} chunkSize={} firmwareBytes={} chunks={}", sn, version, headerHex, "binary32", otaChunkSize, firmwareBytes, chunks); } catch (Exception ignore) { @@ -481,7 +491,7 @@ public class IotAuthController { } catch (Exception ignore) { } } else { - // 后续分片:从首片实际数据(1024)之后开始切片;并在分片前置校验头(摘要基于该分片数据) + // 从首片实际数据(1024)之后开始切片;并在分片前置校验头(摘要基于该分片数据) int firstDataLen = Math.min(otaChunkSize, firmwareBytes); int offset = firstDataLen + (idx - 1) * otaChunkSize; if (offset >= firmwareBytes) { @@ -518,10 +528,60 @@ public class IotAuthController { log.debug("OTA分片发送: sn={} version={} num={} chunkLen={}", sn, version, num, chunk.length); + double progress = (idx + 1) * 100.0 / Math.max(chunks, 1); + try { + if (otaTasksService != null) { + String deviceId = sn; + if (bmsDevicesService != null) { + try { + BmsDevices q = new BmsDevices(); + q.setDeviceSn(sn); + BmsDevices dev = bmsDevicesService.selectBmsDevicesList1(q); + if (dev != null && dev.getDeviceId() != null) { + deviceId = dev.getDeviceId(); + } + } catch (Exception ignore) {} + } + OtaTasks filter = new OtaTasks(); + filter.setDeviceId(deviceId); + filter.setFirmwareVersion(version); + List list = otaTasksService.selectOtaTasksList(filter); + OtaTasks task = (list != null && !list.isEmpty()) ? list.get(0) : new OtaTasks(); + boolean isNew = (task.getId() == null || task.getId().isEmpty()); + if (isNew) { + task.setDeviceId(deviceId); + task.setTaskName("OTA-" + version); + task.setFirmwareVersion(version); + task.setStatus("IN_PROGRESS"); + task.setProgress((long) Math.round((float) progress)); + task.setStartTime(new Date()); + task.setCreateBy(sn); + otaTasksService.insertOtaTasks(task); + } else { + task.setProgress((long) Math.round((float) progress)); + if (idx == chunks - 1) { + task.setStatus("DONE"); + task.setEndTime(new java.util.Date()); + } else { + task.setStatus("IN_PROGRESS"); + } + task.setUpdateTime(new java.util.Date()); + otaTasksService.updateOtaTasks(task); + } + } + } catch (Exception ex) { + log.warn("OTA任务记录更新失败: sn={} version={} err={}", sn, version, ex.getMessage()); + } return ResponseEntity .ok() .contentType(MediaType.APPLICATION_OCTET_STREAM) .contentLength(chunk.length) + .header("X-OTA-Chunk-Index", String.valueOf(idx)) + .header("X-OTA-Chunk-Total", String.valueOf(chunks)) + .header("X-OTA-Progress", String.format(java.util.Locale.ROOT, "%.2f", progress)) + .header("X-OTA-Chunk-Size", String.valueOf(otaChunkSize)) + .header("X-OTA-Firmware-Bytes", String.valueOf(firmwareBytes)) + .header("X-OTA-Mode", mode) .body(chunk); } catch (Exception e) { log.error("OTA分片下载异常: {}", e.getMessage(), e); @@ -552,6 +612,240 @@ public class IotAuthController { } } + @PostMapping(value = "/ota/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @ApiOperation(value = "上传OTA固件", notes = "上传固件到 ota.repo 指定目录,保存为 .bin", response = OtaUploadResultDTO.class) + public OtaUploadResultDTO uploadOta(@RequestParam("version") String version, @RequestParam("file") MultipartFile file) { + OtaUploadResultDTO resp = new OtaUploadResultDTO(); + try { + if (StringUtils.isEmpty(version) || file == null || file.isEmpty()) { + resp.setCode(BboxApiConstants.CODE_PARAM_INVALID); + resp.setSuccess("0"); + resp.setMsg("version或file缺失"); + return resp; + } + if (otaRepo.startsWith("classpath:")) { + resp.setCode(BboxApiConstants.CODE_PARAM_INVALID); + resp.setSuccess("0"); + resp.setMsg("classpath仓库不支持上传,请配置文件系统路径"); + return resp; + } + Path dir = Paths.get(otaRepo); + if (!Files.exists(dir)) { + Files.createDirectories(dir); + } + Path target = dir.resolve(version + ".bin"); + Files.copy(file.getInputStream(), target, java.nio.file.StandardCopyOption.REPLACE_EXISTING); + resp.setCode(BboxApiConstants.CODE_SUCCESS); + resp.setSuccess("1"); + resp.setPath(target.toString()); + resp.setSize(Files.size(target)); + log.info("OTA上传成功: version={} repo={} target={} size={}", version, otaRepo, target.toString(), resp.getSize()); + // 返回分片元信息 + Integer chunks = computeOtaChunkCount(version); + resp.setChunks(chunks); + resp.setChunkSize(otaChunkSize); + return resp; + } catch (IOException e) { + resp.setCode(BboxApiConstants.CODE_FILE_NOT_FOUND); + resp.setSuccess("0"); + resp.setMsg(e.getMessage()); + return resp; + } + } + + @GetMapping("/ota/meta") + @ApiOperation(value = "查询OTA元信息", notes = "返回分片总数、分片大小、固件字节数") + public Map otaMeta(@RequestParam("version") String version) { + Map resp = new HashMap<>(); + if (StringUtils.isEmpty(version)) { + resp.put("code", BboxApiConstants.CODE_PARAM_INVALID); + resp.put("success", "0"); + return resp; + } + byte[] fw = loadFirmwareBytes(version); + if (fw == null) { + resp.put("code", BboxApiConstants.CODE_FILE_NOT_FOUND); + resp.put("success", "0"); + return resp; + } + resp.put("code", BboxApiConstants.CODE_SUCCESS); + resp.put("success", "1"); + resp.put("firmwareBytes", fw.length); + resp.put("chunkSize", otaChunkSize); + resp.put("chunks", (fw.length + otaChunkSize - 1) / otaChunkSize); + return resp; + } + + @GetMapping("/ota/history") + @ApiOperation(value = "OTA操作历史", notes = "前端用于展示操作历史列表;sn为空时可从Authorization解析") + public OtaHistoryResultDTO otaHistory(@RequestParam(value = "sn", required = false) String sn, + @RequestParam(value = "version", required = false) String version, + @RequestParam(value = "limit", required = false) Integer limit, + HttpServletRequest request) { + OtaHistoryResultDTO out = new OtaHistoryResultDTO(); + String snx = safeTrim(sn); + if (StringUtils.isEmpty(snx)) { + try { + snx = deviceTokenService.resolveSn(request); + } catch (Exception ignore) { + } + } + if (StringUtils.isEmpty(snx)) { + out.setCode(BboxApiConstants.CODE_PARAM_INVALID); + out.setSuccess("0"); + out.setMsg("sn不能为空"); + return out; + } + if (otaTasksService == null) { + out.setCode(BboxApiConstants.CODE_NOT_FOUND); + out.setSuccess("0"); + out.setMsg("otaTasksService未启用"); + out.setSn(snx); + return out; + } + + String deviceId = resolveDeviceIdBySn(snx); + OtaTasks filter = new OtaTasks(); + filter.setDeviceId(deviceId); + if (StringUtils.isNotEmpty(version)) { + filter.setFirmwareVersion(version); + } + List list = otaTasksService.selectOtaTasksList(filter); + List sorted = list == null ? new ArrayList<>() : new ArrayList<>(list); + sorted.sort(Comparator.comparing(this::resolveOtaSortTime, Comparator.nullsLast(Date::compareTo)).reversed()); + + int lim = (limit == null || limit <= 0) ? 50 : limit; + if (sorted.size() > lim) { + sorted = new ArrayList<>(sorted.subList(0, lim)); + } + + out.setCode(BboxApiConstants.CODE_SUCCESS); + out.setSuccess("1"); + out.setMsg(BboxApiConstants.MSG_SUCCESS); + out.setSn(snx); + out.setDeviceId(deviceId); + out.setTaskList(sorted); + return out; + } + + /** + * OTA 任务进度(管理端轮询) + * - 路径:GET /iot/ota/progress + * - 参数:sn(可选,缺省时从 Authorization 解析设备)、version(可选,缺省时取该设备最新任务) + * - 说明:`progress` 为任务整体 0–100;`totalChunks`/`chunkSize` 与设备登录 `count`、分片下载逻辑对齐; + * `downloadedChunks` 由 `progress` 与 `totalChunks` 推算,用于可选的「分片进度」文案(与浏览器上传到 /iot/ota/upload 无关)。 + * - 响应示例: + *
+     * {
+     *   "code": 200,
+     *   "success": "1",
+     *   "msg": "success",
+     *   "sn": "137PBP1M11001AFCF0000007",
+     *   "deviceId": "BAT_001",
+     *   "firmwareVersion": "1.0.1",
+     *   "status": "IN_PROGRESS",
+     *   "progress": 45,
+     *   "totalChunks": 120,
+     *   "downloadedChunks": 54,
+     *   "chunkSize": 1024,
+     *   "startTime": "2026-05-11 10:00:00",
+     *   "endTime": null,
+     *   "updateTime": "2026-05-11 10:05:00"
+     * }
+     * 
+ */ + @GetMapping("/ota/progress") + @ApiOperation(value = "OTA进度条", notes = "前端轮询展示设备 OTA 任务进度;含 totalChunks/downloadedChunks 供可选分片文案;上传进度请用前端 multipart 的 on-progress") + public OtaProgressResultDTO otaProgress(@RequestParam(value = "sn", required = false) String sn, + @RequestParam(value = "version", required = false) String version, + HttpServletRequest request) { + OtaProgressResultDTO out = new OtaProgressResultDTO(); + String snx = safeTrim(sn); + if (StringUtils.isEmpty(snx)) { + try { + snx = deviceTokenService.resolveSn(request); + } catch (Exception ignore) { + } + } + if (StringUtils.isEmpty(snx)) { + out.setCode(BboxApiConstants.CODE_PARAM_INVALID); + out.setSuccess("0"); + out.setMsg("sn不能为空"); + return out; + } + if (otaTasksService == null) { + out.setCode(BboxApiConstants.CODE_NOT_FOUND); + out.setSuccess("0"); + out.setMsg("otaTasksService未启用"); + out.setSn(snx); + return out; + } + + String deviceId = resolveDeviceIdBySn(snx); + OtaTasks filter = new OtaTasks(); + filter.setDeviceId(deviceId); + if (StringUtils.isNotEmpty(version)) { + filter.setFirmwareVersion(version); + } + List list = otaTasksService.selectOtaTasksList(filter); + List sorted = list == null ? new ArrayList<>() : new ArrayList<>(list); + sorted.sort(Comparator.comparing(this::resolveOtaSortTime, Comparator.nullsLast(Date::compareTo)).reversed()); + OtaTasks latest = sorted.isEmpty() ? null : sorted.get(0); + + out.setCode(BboxApiConstants.CODE_SUCCESS); + out.setSuccess("1"); + out.setMsg(BboxApiConstants.MSG_SUCCESS); + out.setSn(snx); + out.setDeviceId(deviceId); + if (latest != null) { + out.setFirmwareVersion(latest.getFirmwareVersion()); + out.setStatus(latest.getStatus()); + out.setProgress(latest.getProgress() == null ? 0L : latest.getProgress()); + out.setStartTime(latest.getStartTime()); + out.setEndTime(latest.getEndTime()); + out.setUpdateTime(latest.getUpdateTime()); + } else { + out.setFirmwareVersion(version); + out.setStatus("NONE"); + out.setProgress(0L); + } + attachOtaChunkProgressFields(out); + return out; + } + + /** + * 为进度接口附加分片元数据:totalChunks 与登录接口 count 同源;downloadedChunks 由任务 progress 反推(与分片下载时写回 DB 的规则一致)。 + */ + private void attachOtaChunkProgressFields(OtaProgressResultDTO out) { + out.setChunkSize(otaChunkSize); + String fv = out.getFirmwareVersion(); + if (StringUtils.isEmpty(fv)) { + return; + } + Integer total = computeOtaChunkCount(fv); + out.setTotalChunks(total); + if (total == null || total <= 0) { + return; + } + String st = out.getStatus(); + if (isOtaFinishedSuccess(st)) { + out.setDownloadedChunks(total); + return; + } + long prog = out.getProgress() == null ? 0L : out.getProgress(); + int est = (int) Math.round(prog * (double) total / 100.0); + est = Math.max(0, Math.min(est, total)); + out.setDownloadedChunks(est); + } + + private static boolean isOtaFinishedSuccess(String status) { + if (StringUtils.isEmpty(status)) { + return false; + } + String u = status.trim().toUpperCase(Locale.ROOT); + return "DONE".equals(u) || "SUCCESS".equals(u) || "COMPLETED".equals(u); + } + /** * 构造 OTA 负载(仅 binary32) * - 格式:`[32字节SHA256原始摘要] + [固件]` @@ -600,6 +894,27 @@ public class IotAuthController { } } + private String resolveDeviceIdBySn(String sn) { + String deviceId = sn; + if (bmsDevicesService != null) { + try { + BmsDevices dev = bmsDevicesService.selectBmsDevicesBySn(sn); + if (dev != null && StringUtils.isNotEmpty(dev.getDeviceId())) { + deviceId = dev.getDeviceId(); + } + } catch (Exception ignore) { + } + } + return deviceId; + } + + private Date resolveOtaSortTime(OtaTasks task) { + if (task == null) return null; + if (task.getUpdateTime() != null) return task.getUpdateTime(); + if (task.getCreateTime() != null) return task.getCreateTime(); + return task.getStartTime(); + } + /** * 计算 SHA256 的十六进制字符串 * - 输出:小写,长度 64 的 ASCII 字符串 diff --git a/src/main/java/com/evobms/project/iot/controller/IotOtaController.java b/src/main/java/com/evobms/project/iot/controller/IotOtaController.java index a73c49f..ced2243 100644 --- a/src/main/java/com/evobms/project/iot/controller/IotOtaController.java +++ b/src/main/java/com/evobms/project/iot/controller/IotOtaController.java @@ -52,7 +52,7 @@ public class IotOtaController { MDC.put("sn", sn); MDC.put("version", prevVersion); - // 规范化状态:仅识别 "1" 为成功,其余按失败处理;仍返回成功表示上报接口调用成功 + String normalizedStatus = "0"; if ("1".equals(statusStr)) { normalizedStatus = "1"; diff --git a/src/main/java/com/evobms/project/iot/domain/dto/OtaHistoryResultDTO.java b/src/main/java/com/evobms/project/iot/domain/dto/OtaHistoryResultDTO.java new file mode 100644 index 0000000..97638cf --- /dev/null +++ b/src/main/java/com/evobms/project/iot/domain/dto/OtaHistoryResultDTO.java @@ -0,0 +1,31 @@ +package com.evobms.project.iot.domain.dto; + +import com.evobms.project.ota.domain.OtaTasks; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.util.List; + +@ApiModel(value = "OtaHistoryResultDTO", description = "OTA操作历史查询结果") +@Data +public class OtaHistoryResultDTO { + + @ApiModelProperty(value = "状态码", example = "200") + private Integer code; + + @ApiModelProperty(value = "是否成功,1成功/0失败", example = "1") + private String success; + + @ApiModelProperty(value = "提示信息", example = "success") + private String msg; + + @ApiModelProperty(value = "设备SN", example = "137PBP1M11001AFCF0000007") + private String sn; + + @ApiModelProperty(value = "设备ID(平台侧deviceId)", example = "BAT_001") + private String deviceId; + + @ApiModelProperty(value = "任务列表(按最近时间倒序)") + private List taskList; +} diff --git a/src/main/java/com/evobms/project/iot/domain/dto/OtaLoginDto.java b/src/main/java/com/evobms/project/iot/domain/dto/OtaLoginDto.java new file mode 100644 index 0000000..c290c7c --- /dev/null +++ b/src/main/java/com/evobms/project/iot/domain/dto/OtaLoginDto.java @@ -0,0 +1,15 @@ +package com.evobms.project.iot.domain.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + * 设备登录DTO + */ +@ApiModel(value = "OtaLoginDto", description = "设备登录DTO") +@Data +public class OtaLoginDto { + @ApiModelProperty(value = "固件版本") + private String version; +} diff --git a/src/main/java/com/evobms/project/iot/domain/dto/OtaProgressResultDTO.java b/src/main/java/com/evobms/project/iot/domain/dto/OtaProgressResultDTO.java new file mode 100644 index 0000000..b64ffa5 --- /dev/null +++ b/src/main/java/com/evobms/project/iot/domain/dto/OtaProgressResultDTO.java @@ -0,0 +1,59 @@ +package com.evobms.project.iot.domain.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.util.Date; + +@ApiModel(value = "OtaProgressResultDTO", description = "OTA进度查询结果(用于进度条)") +@Data +public class OtaProgressResultDTO { + + @ApiModelProperty(value = "状态码", example = "200") + private Integer code; + + @ApiModelProperty(value = "是否成功,1成功/0失败", example = "1") + private String success; + + @ApiModelProperty(value = "提示信息", example = "success") + private String msg; + + @ApiModelProperty(value = "设备SN", example = "137PBP1M11001AFCF0000007") + private String sn; + + @ApiModelProperty(value = "设备ID(平台侧deviceId)", example = "BAT_001") + private String deviceId; + + @ApiModelProperty(value = "目标固件版本", example = "v1.0.3") + private String firmwareVersion; + + @ApiModelProperty(value = "任务状态", example = "IN_PROGRESS") + private String status; + + @ApiModelProperty(value = "进度(0-100)", example = "30") + private Long progress; + + /** 与设备登录接口返回的 count、GET /iot/ota/meta 的 chunks 同一口径(按固件字节与 ota.chunk-size 计算) */ + @ApiModelProperty(value = "OTA 分片总数(与设备侧 count 对齐;服务端根据仓库内固件计算,缺文件时为 null)", example = "120") + private Integer totalChunks; + + /** + * 基于当前任务的 progress 与 totalChunks 推算的已拉取分片数(与 downloadOtaChunk 中按片更新 progress 的规则一致;取整误差 ±1 片属正常)。 + * 任务状态为 DONE 等终态成功时置为 totalChunks。 + */ + @ApiModelProperty(value = "推算的已下载分片数(来自 progress×totalChunks,仅供展示;无 totalChunks 时为 null)", example = "54") + private Integer downloadedChunks; + + @ApiModelProperty(value = "单分片字节数(与 ota.chunk-size 一致)", example = "1024") + private Integer chunkSize; + + @ApiModelProperty(value = "开始时间") + private Date startTime; + + @ApiModelProperty(value = "结束时间") + private Date endTime; + + @ApiModelProperty(value = "更新时间") + private Date updateTime; +} diff --git a/src/main/java/com/evobms/project/iot/domain/dto/OtaUploadDto.java b/src/main/java/com/evobms/project/iot/domain/dto/OtaUploadDto.java new file mode 100644 index 0000000..77ff056 --- /dev/null +++ b/src/main/java/com/evobms/project/iot/domain/dto/OtaUploadDto.java @@ -0,0 +1,18 @@ +package com.evobms.project.iot.domain.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + * OTA固件上传DTO + */ +@ApiModel(value = "OtaUploadDto", description = "OTA固件上传DTO") +@Data +public class OtaUploadDto { + @ApiModelProperty(value = "设备SN码") + private String deviceSn; + + @ApiModelProperty(value = "固件版本") + private String version; +} diff --git a/src/main/java/com/evobms/project/iot/domain/dto/OtaUploadResponseDto.java b/src/main/java/com/evobms/project/iot/domain/dto/OtaUploadResponseDto.java new file mode 100644 index 0000000..30f414f --- /dev/null +++ b/src/main/java/com/evobms/project/iot/domain/dto/OtaUploadResponseDto.java @@ -0,0 +1,24 @@ +package com.evobms.project.iot.domain.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + * OTA固件上传响应DTO + */ +@ApiModel(value = "OtaUploadResponseDto", description = "OTA固件上传响应DTO") +@Data +public class OtaUploadResponseDto { + @ApiModelProperty(value = "状态码") + private String code; + + @ApiModelProperty(value = "是否成功") + private String success; + + @ApiModelProperty(value = "上传进度") + private Integer progress; + + @ApiModelProperty(value = "消息") + private String message; +} diff --git a/src/main/java/com/evobms/project/iot/domain/dto/OtaUploadResultDTO.java b/src/main/java/com/evobms/project/iot/domain/dto/OtaUploadResultDTO.java new file mode 100644 index 0000000..4aff15b --- /dev/null +++ b/src/main/java/com/evobms/project/iot/domain/dto/OtaUploadResultDTO.java @@ -0,0 +1,31 @@ +package com.evobms.project.iot.domain.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@ApiModel(value = "OtaUploadResultDTO", description = "OTA固件上传结果") +@Data +public class OtaUploadResultDTO { + + @ApiModelProperty(value = "状态码", example = "200") + private Integer code; + + @ApiModelProperty(value = "是否成功,1成功/0失败", example = "1") + private String success; + + @ApiModelProperty(value = "提示信息", example = "success") + private String msg; + + @ApiModelProperty(value = "固件落盘路径", example = "E:/OTA/v1.0.3.bin") + private String path; + + @ApiModelProperty(value = "文件大小(字节)", example = "102400") + private Long size; + + @ApiModelProperty(value = "分片总数", example = "100") + private Integer chunks; + + @ApiModelProperty(value = "分片大小(字节)", example = "1024") + private Integer chunkSize; +} diff --git a/src/main/java/com/evobms/project/ota/controller/OtaTasksController.java b/src/main/java/com/evobms/project/ota/controller/OtaTasksController.java index fbe4e13..2fd89f2 100644 --- a/src/main/java/com/evobms/project/ota/controller/OtaTasksController.java +++ b/src/main/java/com/evobms/project/ota/controller/OtaTasksController.java @@ -1,6 +1,9 @@ package com.evobms.project.ota.controller; import java.util.List; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Date; import javax.servlet.http.HttpServletResponse; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.beans.factory.annotation.Autowired; @@ -12,10 +15,14 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import com.evobms.common.utils.StringUtils; import com.evobms.framework.aspectj.lang.annotation.Log; import com.evobms.framework.aspectj.lang.enums.BusinessType; import com.evobms.project.ota.domain.OtaTasks; +import com.evobms.project.ota.dto.OtaTaskQueryDTO; import com.evobms.project.ota.service.IOtaTasksService; +import com.evobms.project.system.domain.BmsDevices; +import com.evobms.project.system.service.IBmsDevicesService; import com.evobms.framework.web.controller.BaseController; import com.evobms.framework.web.domain.AjaxResult; import com.evobms.common.utils.poi.ExcelUtil; @@ -37,6 +44,9 @@ public class OtaTasksController extends BaseController @Autowired private IOtaTasksService otaTasksService; + @Autowired(required = false) + private IBmsDevicesService bmsDevicesService; + /** * 查询OTA任务列表 */ @@ -50,6 +60,44 @@ public class OtaTasksController extends BaseController return getDataTable(list); } + /** + * 根据SN查询设备OTA更新信息 + */ + @GetMapping("/sn/{sn}") + @ApiOperation(value = "根据SN查询OTA任务信息", notes = "返回设备对象、最新任务和任务列表,供前端展示设备升级信息") + public AjaxResult getBySn(@PathVariable("sn") String sn) + { + if (StringUtils.isEmpty(sn)) + { + return AjaxResult.error("sn不能为空"); + } + + BmsDevices device = null; + String deviceId = sn; + if (bmsDevicesService != null) + { + device = bmsDevicesService.selectBmsDevicesBySn(sn); + if (device != null && StringUtils.isNotEmpty(device.getDeviceId())) + { + deviceId = device.getDeviceId(); + } + } + + OtaTasks query = new OtaTasks(); + query.setDeviceId(deviceId); + List taskList = otaTasksService.selectOtaTasksList(query); + List sortedTaskList = taskList == null ? new ArrayList<>() : new ArrayList<>(taskList); + sortedTaskList.sort(Comparator.comparing(this::resolveSortTime, + Comparator.nullsLast(Date::compareTo)).reversed()); + + OtaTaskQueryDTO dto = new OtaTaskQueryDTO(); + dto.setSn(sn); + dto.setDevice(device); + dto.setTaskList(sortedTaskList); + dto.setLatestTask(sortedTaskList.isEmpty() ? null : sortedTaskList.get(0)); + return success(dto); + } + /** * 导出OTA任务列表 */ @@ -109,4 +157,21 @@ public class OtaTasksController extends BaseController { return toAjax(otaTasksService.deleteOtaTasksByIds(ids)); } + + private Date resolveSortTime(OtaTasks task) + { + if (task == null) + { + return null; + } + if (task.getUpdateTime() != null) + { + return task.getUpdateTime(); + } + if (task.getCreateTime() != null) + { + return task.getCreateTime(); + } + return task.getStartTime(); + } } diff --git a/src/main/java/com/evobms/project/ota/dto/OtaTaskQueryDTO.java b/src/main/java/com/evobms/project/ota/dto/OtaTaskQueryDTO.java new file mode 100644 index 0000000..287d10a --- /dev/null +++ b/src/main/java/com/evobms/project/ota/dto/OtaTaskQueryDTO.java @@ -0,0 +1,26 @@ +package com.evobms.project.ota.dto; + +import com.evobms.project.ota.domain.OtaTasks; +import com.evobms.project.system.domain.BmsDevices; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.util.List; + +@Data +@ApiModel(value = "OtaTaskQueryDTO", description = "根据SN查询设备OTA任务响应") +public class OtaTaskQueryDTO { + + @ApiModelProperty(value = "设备SN", example = "137PBP1M11001AFCF0000007") + private String sn; + + @ApiModelProperty(value = "设备对象") + private BmsDevices device; + + @ApiModelProperty(value = "最新一条OTA任务") + private OtaTasks latestTask; + + @ApiModelProperty(value = "该设备的OTA任务列表,按最近更新时间倒序") + private List taskList; +} diff --git a/src/main/java/com/evobms/project/system/controller/BmsDevicesController.java b/src/main/java/com/evobms/project/system/controller/BmsDevicesController.java index e2d8238..dc8c143 100644 --- a/src/main/java/com/evobms/project/system/controller/BmsDevicesController.java +++ b/src/main/java/com/evobms/project/system/controller/BmsDevicesController.java @@ -1,29 +1,35 @@ package com.evobms.project.system.controller; -import java.util.List; +import java.math.BigDecimal; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; import javax.servlet.http.HttpServletResponse; + +import com.evobms.project.vehicledata.domain.VehicleLocation; +import com.evobms.project.vehicledata.service.IVehicleLocationService; +import io.swagger.annotations.ApiParam; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import com.evobms.framework.aspectj.lang.annotation.Log; import com.evobms.framework.aspectj.lang.enums.BusinessType; import com.evobms.project.system.domain.BmsDevices; import com.evobms.project.system.service.IBmsDevicesService; import com.evobms.framework.web.controller.BaseController; import com.evobms.framework.web.domain.AjaxResult; +import com.evobms.framework.web.domain.R; +import com.evobms.common.constant.HttpStatus; import com.evobms.common.utils.poi.ExcelUtil; import com.evobms.framework.web.page.TableDataInfo; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.evobms.project.vehicledata.dto.DeviceHistoryFlatRowDTO; import com.evobms.project.vehicledata.dto.DeviceRealtimeDTO; +import com.evobms.project.vehicledata.dto.DeviceRealtimeAggregateDTO; import com.evobms.project.vehicledata.dto.NameValueDTO; import com.evobms.project.vehicledata.dto.DeviceRealtimeDTO.SystemStatusSection; import com.evobms.project.vehicledata.dto.DeviceRealtimeDTO.RunStatusSection; @@ -31,15 +37,20 @@ import com.evobms.project.vehicledata.dto.DeviceRealtimeDTO.ChargeSection; import com.evobms.project.vehicledata.dto.DeviceRealtimeDTO.StatisticsSection; import com.evobms.project.vehicledata.dto.LineSeriesDTO; import com.evobms.project.vehicledata.dto.TimePointDTO; +import com.evobms.project.vehicledata.dto.ChargeDischargeMonthDTO; +import com.evobms.project.vehicledata.dto.ChargeDischargeRecordDTO; +import com.evobms.project.vehicledata.dto.ChargeDischargeHistoryDTO; +import com.evobms.project.vehicledata.service.DeviceRealtimeHistoryAssembler; import com.evobms.project.vehicledata.service.IVehicleDataService; import com.evobms.project.vehicledata.domain.VehicleData; import com.evobms.project.battery.service.IExtremeValuesService; +import com.evobms.project.battery.service.ISubsystemVoltageService; +import com.evobms.project.battery.service.ISubsystemTemperatureService; import com.evobms.project.battery.domain.ExtremeValues; +import com.evobms.project.battery.domain.SubsystemVoltage; +import com.evobms.project.battery.domain.SubsystemTemperature; import com.evobms.project.bms.service.*; import com.evobms.project.bms.domain.*; -import java.util.ArrayList; -import java.util.Date; -import java.util.stream.Collectors; /** * BBOX管理Controller @@ -55,6 +66,18 @@ public class BmsDevicesController extends BaseController @Autowired private IBmsDevicesService bmsDevicesService; + @Autowired private IVehicleDataService vehicleDataService; + @Autowired private IExtremeValuesService extremeValuesService; + @Autowired private IBmsSystemStatusService bmsSystemStatusService; + @Autowired private IBmsChargeDetailService bmsChargeDetailService; + @Autowired private IBmsChargeTemperatureService bmsChargeTemperatureService; + @Autowired private IBmsVoltageChannelDataService bmsVoltageChannelDataService; + @Autowired private IBmsSopDataService bmsSopDataService; + @Autowired private IVehicleLocationService vehicleLocationService; + @Autowired private IBmsRuntimeDataService bmsRuntimeDataService; + @Autowired private ISubsystemVoltageService subsystemVoltageService; + @Autowired private ISubsystemTemperatureService subsystemTemperatureService; + @Autowired private DeviceRealtimeHistoryAssembler deviceRealtimeHistoryAssembler; /** * 查询BBOX管理列表 */ @@ -68,6 +91,124 @@ public class BmsDevicesController extends BaseController return getDataTable(list); } + @GetMapping("/history/{deviceId}") + @ApiOperation(value = "运行历史计算器(充/放电统计)", response = ChargeDischargeHistoryDTO.class) + public R history( + @ApiParam(value = "设备ID", required = true) @PathVariable("deviceId") String deviceId, + @ApiParam(value = "开始时间戳(毫秒)", required = false) @RequestParam(value = "start", required = false) Long startMillis, + @ApiParam(value = "结束时间戳(毫秒)", required = false) @RequestParam(value = "end", required = false) Long endMillis + ) { + Date end = endMillis == null ? new Date() : new Date(endMillis); + Date start = startMillis == null ? new Date(end.getTime() - 30L * 86400_000L) : new Date(startMillis); + SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + SimpleDateFormat mf = new SimpleDateFormat("yyyy-MM"); + VehicleData q = new VehicleData(); + Map params = new HashMap<>(); + params.put("beginTime", df.format(start)); + params.put("endTime", df.format(end)); + q.setDeviceId(deviceId); + q.setParams(params); + List all = vehicleDataService.selectVehicleDataList(q); + all.removeIf(v -> v.getTimestamp() == null); + all.sort(Comparator.comparing(VehicleData::getTimestamp)); + + List records = new ArrayList<>(); + String currentStatus = null; + int segmentStartIdx = -1; + Integer prevSoc = null; + for (int i = 0; i < all.size(); i++) { + VehicleData v = all.get(i); + Integer soc = v.getSoc(); + BigDecimal cur = v.getTotalCurrent(); + String status = null; + if (cur != null) { + int cmp = cur.compareTo(BigDecimal.ZERO); + if (cmp > 0) status = "充电"; + else if (cmp < 0) status = "放电"; + } + if (status == null && prevSoc != null && soc != null) { + if (soc > prevSoc) status = "充电"; + else if (soc < prevSoc) status = "放电"; + } + if (status == null && v.getChargingStatus() != null) { + Integer st = v.getChargingStatus(); + if (st == 1 || st == 2) status = "充电"; + else if (st == 3) status = "放电"; + } + if (status == null) { + prevSoc = soc; + continue; + } + if (currentStatus == null) { + currentStatus = status; + segmentStartIdx = i; + } else if (!currentStatus.equals(status)) { + // finalize previous segment + ChargeDischargeRecordDTO rec = new ChargeDischargeRecordDTO(); + rec.setStatus(currentStatus); + com.evobms.project.vehicledata.domain.VehicleData sv = all.get(segmentStartIdx); + com.evobms.project.vehicledata.domain.VehicleData evd = all.get(i - 1); + rec.setStartTime(df.format(sv.getTimestamp())); + rec.setEndTime(df.format(evd.getTimestamp())); + rec.setStartSoc(sv.getSoc()); + rec.setEndSoc(evd.getSoc()); + long seconds = (evd.getTimestamp().getTime() - sv.getTimestamp().getTime()) / 1000L; + java.math.BigDecimal hours = new java.math.BigDecimal(seconds).divide(new java.math.BigDecimal(3600), 2, java.math.RoundingMode.HALF_UP); + rec.setDurationHours(hours); + // 忽略过短区段(小于5分钟) + if (seconds >= 300) { + records.add(rec); + } + currentStatus = status; + segmentStartIdx = i; + } + prevSoc = soc; + } + // finalize last + if (currentStatus != null && segmentStartIdx >= 0 && all.size() - 1 >= segmentStartIdx) { + ChargeDischargeRecordDTO rec = new ChargeDischargeRecordDTO(); + rec.setStatus(currentStatus); + com.evobms.project.vehicledata.domain.VehicleData sv = all.get(segmentStartIdx); + com.evobms.project.vehicledata.domain.VehicleData evd = all.get(all.size() - 1); + rec.setStartTime(df.format(sv.getTimestamp())); + rec.setEndTime(df.format(evd.getTimestamp())); + rec.setStartSoc(sv.getSoc()); + rec.setEndSoc(evd.getSoc()); + long seconds = (evd.getTimestamp().getTime() - sv.getTimestamp().getTime()) / 1000L; + java.math.BigDecimal hours = new java.math.BigDecimal(seconds).divide(new java.math.BigDecimal(3600), 2, java.math.RoundingMode.HALF_UP); + rec.setDurationHours(hours); + if (seconds >= 300) { + records.add(rec); + } + } + + java.util.Map monthAgg = new java.util.HashMap<>(); + for (ChargeDischargeRecordDTO rec : records) { + String m = rec.getStartTime().substring(0, 7); + java.math.BigDecimal[] pair = monthAgg.computeIfAbsent(m, k -> new java.math.BigDecimal[]{java.math.BigDecimal.ZERO, java.math.BigDecimal.ZERO}); + if ("充电".equals(rec.getStatus())) { + pair[0] = pair[0].add(rec.getDurationHours()); + } else { + pair[1] = pair[1].add(rec.getDurationHours()); + } + } + java.util.List months = new java.util.ArrayList<>(); + java.util.List keys = new java.util.ArrayList<>(monthAgg.keySet()); + keys.sort(String::compareTo); + for (String k : keys) { + ChargeDischargeMonthDTO m = new ChargeDischargeMonthDTO(); + m.setMonth(k); + java.math.BigDecimal[] pair = monthAgg.get(k); + m.setChargeHours(pair[0]); + m.setDischargeHours(pair[1]); + months.add(m); + } + + ChargeDischargeHistoryDTO out = new ChargeDischargeHistoryDTO(); + out.setMonths(months); + out.setRecords(records); + return R.ok(out); + } /** * 导出BBOX管理列表 */ @@ -93,165 +234,118 @@ public class BmsDevicesController extends BaseController return success(bmsDevicesService.selectBmsDevicesById(id)); } - @Autowired private IVehicleDataService vehicleDataService; - @Autowired private IExtremeValuesService extremeValuesService; - @Autowired private IBmsSystemStatusService bmsSystemStatusService; - @Autowired private IBmsChargeDetailService bmsChargeDetailService; - @Autowired private IBmsChargeTemperatureService bmsChargeTemperatureService; - @Autowired private IBmsVoltageChannelDataService bmsVoltageChannelDataService; - @Autowired private IBmsSopDataService bmsSopDataService; + + /** 子系统去重预取行数(按 timestamp 倒序,一般远小于子系统数×上报次数) */ + private static final int REALTIME_SUBSYSTEM_PREFETCH = 32; + /** 24h 趋势曲线最多点数 */ + private static final int REALTIME_TREND_MAX_POINTS = 360; @GetMapping("/realtime/{deviceId}") - @ApiOperation("设备实时数据") - public AjaxResult realtime(@PathVariable("deviceId") String deviceId) { - DeviceRealtimeDTO dto = new DeviceRealtimeDTO(); - dto.setDeviceId(deviceId); - VehicleData vd = vehicleDataService.selectLatestByDeviceId(deviceId); - ExtremeValues ev = extremeValuesService.selectLatestByDeviceId(deviceId); - SystemStatusSection sys = new SystemStatusSection(); - if (vd != null) { - sys.setTotalVoltage(vd.getTotalVoltage()); - sys.setTotalCurrent(vd.getTotalCurrent()); - sys.setSoc(vd.getSoc()); + @ApiOperation(value = "设备实时数据", notes = "默认不含 dashboard 趋势(24h),避免超时;趋势请调 includeTrends=true 或 GET .../trends", response = DeviceRealtimeAggregateDTO.class) + public R realtime(@PathVariable("deviceId") String deviceId, @ApiParam("是否包含 dashboard 24h 趋势(较慢,易触发前端 10s 超时)") @RequestParam(value = "includeTrends", required = false, defaultValue = "true") boolean includeTrends) { + RealtimeSnapshot snap = loadRealtimeSnapshot(deviceId); + DeviceRealtimeAggregateDTO agg = new DeviceRealtimeAggregateDTO(); + agg.setDeviceId(deviceId); + fillAggregateFromSnapshot(agg, snap); + if (includeTrends) { + SimpleDateFormat tf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date now = new Date(); + Date trendBegin = new Date(now.getTime() - 24L * 3600_000L); + agg.setDashboard(buildRealtimeDashboard(deviceId, snap.vehicleData, snap.extremeValues, snap.systemStatus, snap.chargeDetail, snap.chargeTemperatures, snap.voltageChannels, trendBegin, now, tf)); } - if (ev != null) { - sys.setMaxCellVoltage(ev.getMaxVoltageValue()); - sys.setMaxCellPos(ev.getMaxVoltageSubsystemNo() == null || ev.getMaxVoltageBatteryNo() == null ? null : - ev.getMaxVoltageSubsystemNo() + "-" + ev.getMaxVoltageBatteryNo()); - sys.setMinCellVoltage(ev.getMinVoltageValue()); - sys.setMinCellPos(ev.getMinVoltageSubsystemNo() == null || ev.getMinVoltageBatteryNo() == null ? null : - ev.getMinVoltageSubsystemNo() + "-" + ev.getMinVoltageBatteryNo()); - sys.setMaxTemp(ev.getMaxTempValue()); - sys.setMaxTempPos(ev.getMaxTempSubsystemNo() == null || ev.getMaxTempProbeNo() == null ? null : - ev.getMaxTempSubsystemNo() + "-" + ev.getMaxTempProbeNo()); - sys.setMinTemp(ev.getMinTempValue()); - sys.setMinTempPos(ev.getMinTempSubsystemNo() == null || ev.getMinTempProbeNo() == null ? null : - ev.getMinTempSubsystemNo() + "-" + ev.getMinTempProbeNo()); - } - dto.setSystemStatus(sys); + return R.ok(agg); + } - RunStatusSection run = new RunStatusSection(); - if (vd != null) { - run.setChargingStatus(vd.getChargingStatus()); - } - BmsSystemStatus ss = bmsSystemStatusService.lambdaQuery() - .eq(BmsSystemStatus::getDeviceId, deviceId) - .orderByDesc(BmsSystemStatus::getCreateTime).last("limit 1").one(); - if (ss != null) { - run.setRelayStatus(ss.getRelayStatus()); - run.setOnaaccStatus(ss.getOnaaccStatus()); - run.setResetCount(ss.getResetCount()); - run.setCycleCount(ss.getCycleCount()); - run.setSample25v(ss.getSample25v()); - run.setSample5v(ss.getSample5v()); - } - dto.setRunStatus(run); - - ChargeSection chg = new ChargeSection(); - BmsChargeDetail cd = bmsChargeDetailService.lambdaQuery() - .eq(BmsChargeDetail::getDeviceId, deviceId) - .orderByDesc(BmsChargeDetail::getCreateTime).last("limit 1").one(); - if (cd != null) { - chg.setAcChargeCurrent(cd.getAcChargeCurrent()); - chg.setAcChargeVoltage(cd.getAcChargeVoltage()); - chg.setDcChargeCurrent(cd.getDcChargeCurrent()); - chg.setDcChargeVoltage(cd.getDcChargeVoltage()); - chg.setChargeTime(cd.getChargeTime()); - chg.setAcStopCode(cd.getAcStopCode()); - chg.setDcStopCode(cd.getDcStopCode()); - chg.setAcCpValue(cd.getAcCpValue()); - chg.setAcGunResistance(cd.getAcGunResistance()); - chg.setChargingStage(cd.getChargingStage()); - chg.setAcStage(cd.getAcStage()); - chg.setDcStage(cd.getDcStage()); - } - dto.setCharge(chg); - - StatisticsSection stat = new StatisticsSection(); - if (ss != null) { - stat.setCycleCount(ss.getCycleCount()); - } - stat.setTotalRunHours(0L); - stat.setTotalChargeHours(0L); - stat.setTotalDischargeHours(0L); - dto.setStatistics(stat); - - List temps = bmsChargeTemperatureService.lambdaQuery() - .eq(BmsChargeTemperature::getDeviceId, deviceId) - .orderByDesc(BmsChargeTemperature::getCreateTime).last("limit 8").list(); - List tempItems = new ArrayList<>(); - if (temps != null) { - String ts = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); - for (BmsChargeTemperature t : temps) { - NameValueDTO nv = new NameValueDTO(); - nv.setName("T" + t.getTempIndex()); - nv.setValue(t.getTemperature() == null ? 0 : t.getTemperature().longValue()); - nv.setCreateTime(ts); - tempItems.add(nv); - } - } - dto.setTemperatures(tempItems); - - List chs = bmsVoltageChannelDataService.lambdaQuery() - .eq(BmsVoltageChannelData::getDeviceId, deviceId) - .orderByDesc(BmsVoltageChannelData::getCreateTime).last("limit 16").list(); - List voltItems = new ArrayList<>(); - if (chs != null) { - String ts = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); - for (BmsVoltageChannelData c : chs) { - NameValueDTO nv = new NameValueDTO(); - nv.setName((c.getChannelType() == null ? "CH" : c.getChannelType()) + "-" + c.getChannelIndex()); - nv.setValue(c.getVoltage() == null ? 0 : c.getVoltage().longValue()); - nv.setCreateTime(ts); - voltItems.add(nv); - } - } - dto.setVoltages(voltItems); - - dto.setFaults(new ArrayList<>()); - // 折线图:最近24小时(按createTime过滤),默认RTP-1与T1 + @GetMapping("/realtime/{deviceId}/trends") + @ApiOperation(value = "设备实时 24h 趋势(dashboard)", notes = "与 realtime 拆分,避免与快照同请求导致 Axios 10s 超时", response = DeviceRealtimeDTO.class) + public R realtimeTrends( + @PathVariable("deviceId") String deviceId, + @ApiParam("向前小时数,默认 24") @RequestParam(value = "hours", required = false, defaultValue = "24") int hours) { + RealtimeSnapshot snap = loadRealtimeSnapshot(deviceId); + SimpleDateFormat tf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Date now = new Date(); - Date begin = new Date(now.getTime() - 24L * 3600_000L); - List vtrend = bmsVoltageChannelDataService.lambdaQuery() - .eq(BmsVoltageChannelData::getDeviceId, deviceId) - .eq(BmsVoltageChannelData::getChannelType, "RTP") - .eq(BmsVoltageChannelData::getChannelIndex, 1) - .ge(BmsVoltageChannelData::getCreateTime, begin) - .le(BmsVoltageChannelData::getCreateTime, now) - .orderByAsc(BmsVoltageChannelData::getCreateTime) - .list(); - java.text.SimpleDateFormat f = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - List vpoints = new ArrayList<>(); - for (BmsVoltageChannelData it : vtrend) { - TimePointDTO p = new TimePointDTO(); - p.setTime(it.getCreateTime() == null ? null : f.format(it.getCreateTime())); - p.setValue(it.getVoltage()); - vpoints.add(p); - } - LineSeriesDTO vseries = new LineSeriesDTO(); - vseries.setName("RTP-1"); - vseries.setPoints(vpoints); - dto.setVoltageTrend(java.util.Collections.singletonList(vseries)); + int h = hours <= 0 ? 24 : Math.min(hours, 168); + Date trendBegin = new Date(now.getTime() - (long) h * 3600_000L); + return R.ok(buildRealtimeDashboard(deviceId, snap.vehicleData, snap.extremeValues, snap.systemStatus, + snap.chargeDetail, snap.chargeTemperatures, snap.voltageChannels, trendBegin, now, tf)); + } - List ttrend = bmsChargeTemperatureService.lambdaQuery() - .eq(BmsChargeTemperature::getDeviceId, deviceId) - .eq(BmsChargeTemperature::getTempIndex, 1) - .ge(BmsChargeTemperature::getCreateTime, begin) - .le(BmsChargeTemperature::getCreateTime, now) - .orderByAsc(BmsChargeTemperature::getCreateTime) - .list(); - List tpoints = new ArrayList<>(); - for (BmsChargeTemperature it : ttrend) { - TimePointDTO p = new TimePointDTO(); - p.setTime(it.getCreateTime() == null ? null : f.format(it.getCreateTime())); - p.setValue(it.getTemperature()); - tpoints.add(p); + /** + * 与 {@link #realtime(String)} 同源字段的历史平铺列表:时间范围内全部 vehicle_data 在库内分页(pageNum/pageSize), + * 每页再与其它表按时间点合并;pageSize 仅支持 10、20、100、500。 + */ + @PreAuthorize("@ss.hasPermi('system:devices:query')") + @GetMapping("/realtime/history/{deviceId}") + @ApiOperation(value = "设备实时同源历史(平铺分页)", notes = "对 beginTime~endTime 内全部整车记录分页;参数 pageNum、pageSize(10|20|100|500);total 为符合条件的总条数", response = DeviceHistoryFlatRowDTO.class) + public TableDataInfo realtimeHistoryFlat( + @ApiParam(value = "设备ID", required = true) @PathVariable("deviceId") String deviceId, + @ApiParam(value = "开始时间戳(毫秒),与 beginTime 二选一") @RequestParam(value = "start", required = false) Long startMillis, + @ApiParam(value = "结束时间戳(毫秒),与 endTime 二选一") @RequestParam(value = "end", required = false) Long endMillis, + @ApiParam(value = "开始时间 yyyy-MM-dd HH:mm:ss") @RequestParam(value = "beginTime", required = false) String beginTimeStr, + @ApiParam(value = "结束时间 yyyy-MM-dd HH:mm:ss") @RequestParam(value = "endTime", required = false) String endTimeStr, + @ApiParam(value = "排序:desc 默认最新在前;asc 时间正序") @RequestParam(value = "order", required = false, defaultValue = "desc") String order, + @ApiParam(value = "是否查询总条数 COUNT;大数据量可传 false 加快首屏") @RequestParam(value = "count", required = false, defaultValue = "true") boolean countTotal + ) { + SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date end; + Date start; + boolean userRange = startMillis != null || (beginTimeStr != null && !beginTimeStr.isEmpty()); + try { + end = parseHistoryEnd(endMillis, endTimeStr, df); + start = parseHistoryStart(startMillis, beginTimeStr, df, end); + } catch (ParseException e) { + TableDataInfo err = new TableDataInfo(); + err.setCode(HttpStatus.ERROR); + err.setMsg("时间解析失败,请使用 yyyy-MM-dd HH:mm:ss 或毫秒时间戳"); + err.setRows(Collections.emptyList()); + err.setTotal(0); + return err; } - LineSeriesDTO tseries = new LineSeriesDTO(); - tseries.setName("T1"); - tseries.setPoints(tpoints); - dto.setTemperatureTrend(java.util.Collections.singletonList(tseries)); - return success(dto); + boolean desc = !"asc".equalsIgnoreCase(order); + if (!userRange) { + start = clampHistoryStart(start, end); + } + int pageSize = com.evobms.common.utils.PageUtils.startHistoryPage(countTotal); + List vehicles = vehicleDataService.selectVehicleDataListForHistory(deviceId, start, end, desc); + // total 必须来自 PageHelper 分页查询结果;不能用组装后的 rows(否则 total=当前页条数) + long total = new com.github.pagehelper.PageInfo<>(vehicles).getTotal(); + SimpleDateFormat tf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + List rows = deviceRealtimeHistoryAssembler.build(deviceId, vehicles, tf, pageSize); + TableDataInfo table = getDataTable(rows); + table.setTotal(total); + return table; + } + + private static Date parseHistoryEnd(Long endMillis, String endTimeStr, SimpleDateFormat df) throws ParseException { + if (endMillis != null) { + return new Date(endMillis); + } + if (endTimeStr != null && !endTimeStr.isEmpty()) { + return df.parse(endTimeStr); + } + return new Date(); + } + + private static Date parseHistoryStart(Long startMillis, String beginTimeStr, SimpleDateFormat df, Date end) throws ParseException { + if (startMillis != null) { + return new Date(startMillis); + } + if (beginTimeStr != null && !beginTimeStr.isEmpty()) { + return df.parse(beginTimeStr); + } + return new Date(end.getTime() - 7L * 86400_000L); + } + + /** 单次查询时间窗上限 7 天,避免一次扫过多行 */ + private static Date clampHistoryStart(Date start, Date end) { + if (start == null || end == null) { + return start; + } + long maxWindow = 7L * 86400_000L; + if (end.getTime() - start.getTime() > maxWindow) { + return new Date(end.getTime() - maxWindow); + } + return start; } /** @@ -289,4 +383,392 @@ public class BmsDevicesController extends BaseController { return toAjax(bmsDevicesService.deleteBmsDevicesByIds(ids)); } + + /** 扁平视图:与原始列表共用同一批查询结果,避免重复组装且趋势只在 dashboard 中暴露一份 */ + private DeviceRealtimeDTO buildRealtimeDashboard( + String deviceId, + VehicleData vd, + ExtremeValues ev, + BmsSystemStatus ss, + BmsChargeDetail cd, + List chargeTemps, + List voltageChs, + Date trendBegin, + Date trendEnd, + SimpleDateFormat tf) { + DeviceRealtimeDTO dto = new DeviceRealtimeDTO(); + dto.setDeviceId(deviceId); + + SystemStatusSection sys = new SystemStatusSection(); + if (vd != null) { + sys.setTotalVoltage(vd.getTotalVoltage()); + sys.setTotalCurrent(vd.getTotalCurrent()); + sys.setSoc(vd.getSoc()); + } + if (ev != null) { + sys.setMaxCellVoltage(ev.getMaxVoltageValue()); + sys.setMaxCellPos(joinSubsysIndex(ev.getMaxVoltageSubsystemNo(), ev.getMaxVoltageBatteryNo())); + sys.setMinCellVoltage(ev.getMinVoltageValue()); + sys.setMinCellPos(joinSubsysIndex(ev.getMinVoltageSubsystemNo(), ev.getMinVoltageBatteryNo())); + sys.setMaxTemp(ev.getMaxTempValue()); + sys.setMaxTempPos(joinSubsysIndex(ev.getMaxTempSubsystemNo(), ev.getMaxTempProbeNo())); + sys.setMinTemp(ev.getMinTempValue()); + sys.setMinTempPos(joinSubsysIndex(ev.getMinTempSubsystemNo(), ev.getMinTempProbeNo())); + } + dto.setSystemStatus(sys); + + RunStatusSection run = new RunStatusSection(); + if (vd != null) { + run.setChargingStatus(vd.getChargingStatus()); + } + if (ss != null) { + run.setRelayStatus(ss.getRelayStatus()); + run.setOnaaccStatus(ss.getOnaaccStatus()); + run.setResetCount(ss.getResetCount()); + run.setCycleCount(ss.getCycleCount()); + run.setSample25v(ss.getSample25v()); + run.setSample5v(ss.getSample5v()); + } + dto.setRunStatus(run); + + ChargeSection chg = new ChargeSection(); + if (cd != null) { + chg.setAcChargeCurrent(cd.getAcChargeCurrent()); + chg.setAcChargeVoltage(cd.getAcChargeVoltage()); + chg.setDcChargeCurrent(cd.getDcChargeCurrent()); + chg.setDcChargeVoltage(cd.getDcChargeVoltage()); + chg.setChargeTime(cd.getChargeTime()); + chg.setAcStopCode(cd.getAcStopCode()); + chg.setDcStopCode(cd.getDcStopCode()); + chg.setAcCpValue(cd.getAcCpValue()); + chg.setAcGunResistance(cd.getAcGunResistance()); + chg.setChargingStage(cd.getChargingStage()); + chg.setAcStage(cd.getAcStage()); + chg.setDcStage(cd.getDcStage()); + } + dto.setCharge(chg); + + StatisticsSection stat = new StatisticsSection(); + if (ss != null) { + stat.setCycleCount(ss.getCycleCount()); + } + stat.setTotalRunHours(0L); + stat.setTotalChargeHours(0L); + stat.setTotalDischargeHours(0L); + dto.setStatistics(stat); + + dto.setTemperatures(toChargeTemperatureNameValues(chargeTemps, tf)); + dto.setVoltages(toVoltageChannelNameValues(voltageChs, tf)); + dto.setFaults(new ArrayList<>()); + dto.setVoltageTrend(buildRtp1VoltageTrendSeries(deviceId, trendBegin, trendEnd, tf)); + dto.setTemperatureTrend(buildT1TemperatureTrendSeries(deviceId, trendBegin, trendEnd, tf)); + + List vehicleTrendRows = listVehicleDataForTrend(deviceId, trendBegin, trendEnd); + dto.setTotalVoltageTrend(Collections.singletonList( + buildVehicleNumericTrendSeries(vehicleTrendRows, "总电压(V)", trendBegin, trendEnd, tf, VehicleData::getTotalVoltage))); + dto.setTotalCurrentTrend(Collections.singletonList( + buildVehicleNumericTrendSeries(vehicleTrendRows, "总电流(A)", trendBegin, trendEnd, tf, VehicleData::getTotalCurrent))); + dto.setSocTrend(Collections.singletonList( + buildVehicleNumericTrendSeries(vehicleTrendRows, "SOC(%)", trendBegin, trendEnd, tf, + v -> v.getSoc() == null ? null : BigDecimal.valueOf(v.getSoc())))); + return dto; + } + + /** 趋势图:DB 侧限量采样,避免 24h 全量拉取 */ + private List listVehicleDataForTrend(String deviceId, Date begin, Date end) { + return vehicleDataService.selectVehicleDataTrendInRange(deviceId, begin, end, REALTIME_TREND_MAX_POINTS); + } + + private static Date vehicleTrendPointTime(VehicleData v) { + if (v == null) { + return null; + } + return v.getTimestamp() != null ? v.getTimestamp() : v.getCreateTime(); + } + + private static LineSeriesDTO buildVehicleNumericTrendSeries( + List rows, + String seriesName, + Date rangeBegin, + Date rangeEnd, + SimpleDateFormat tf, + Function valueFn) { + List points = new ArrayList<>(); + if (rows != null) { + for (VehicleData v : rows) { + Date t = vehicleTrendPointTime(v); + if (t == null || t.before(rangeBegin) || t.after(rangeEnd)) { + continue; + } + BigDecimal val = valueFn.apply(v); + if (val == null) { + continue; + } + TimePointDTO p = new TimePointDTO(); + p.setTime(tf.format(t)); + p.setValue(val); + points.add(p); + } + } + LineSeriesDTO series = new LineSeriesDTO(); + series.setName(seriesName); + series.setPoints(points); + return series; + } + + private static String joinSubsysIndex(Integer a, Integer b) { + if (a == null || b == null) { + return null; + } + return a + "-" + b; + } + + private static List toChargeTemperatureNameValues(List temps, SimpleDateFormat tf) { + if (temps == null || temps.isEmpty()) { + return new ArrayList<>(); + } + List out = new ArrayList<>(temps.size()); + for (BmsChargeTemperature t : temps) { + NameValueDTO nv = new NameValueDTO(); + nv.setName("T" + t.getTempIndex()); + nv.setValue(t.getTemperature() == null ? 0 : t.getTemperature().longValue()); + nv.setCreateTime(formatTime(t.getCreateTime(), tf)); + out.add(nv); + } + return out; + } + + private static List toVoltageChannelNameValues(List chs, SimpleDateFormat tf) { + if (chs == null || chs.isEmpty()) { + return new ArrayList<>(); + } + List out = new ArrayList<>(chs.size()); + for (BmsVoltageChannelData c : chs) { + NameValueDTO nv = new NameValueDTO(); + nv.setName((c.getChannelType() == null ? "CH" : c.getChannelType()) + "-" + c.getChannelIndex()); + nv.setValue(c.getVoltage() == null ? 0 : c.getVoltage().longValue()); + nv.setCreateTime(formatTime(c.getCreateTime(), tf)); + out.add(nv); + } + return out; + } + + private static String formatTime(Date d, SimpleDateFormat tf) { + return d == null ? null : tf.format(d); + } + + private List buildRtp1VoltageTrendSeries(String deviceId, Date begin, Date end, SimpleDateFormat tf) { + List rows = bmsVoltageChannelDataService.lambdaQuery() + .eq(BmsVoltageChannelData::getDeviceId, deviceId) + .eq(BmsVoltageChannelData::getChannelType, "RTP") + .eq(BmsVoltageChannelData::getChannelIndex, 1) + .isNotNull(BmsVoltageChannelData::getTimestamp) + .ge(BmsVoltageChannelData::getTimestamp, begin) + .le(BmsVoltageChannelData::getTimestamp, end) + .orderByDesc(BmsVoltageChannelData::getTimestamp) + .last("limit " + REALTIME_TREND_MAX_POINTS) + .list(); + Collections.reverse(rows); + List points = new ArrayList<>(rows.size()); + for (BmsVoltageChannelData it : rows) { + TimePointDTO p = new TimePointDTO(); + Date t = it.getTimestamp() != null ? it.getTimestamp() : it.getCreateTime(); + p.setTime(formatTime(t, tf)); + p.setValue(it.getVoltage()); + points.add(p); + } + LineSeriesDTO series = new LineSeriesDTO(); + series.setName("RTP-1"); + series.setPoints(points); + return Collections.singletonList(series); + } + + private List buildT1TemperatureTrendSeries(String deviceId, Date begin, Date end, SimpleDateFormat tf) { + List rows = bmsChargeTemperatureService.lambdaQuery() + .eq(BmsChargeTemperature::getDeviceId, deviceId) + .isNotNull(BmsChargeTemperature::getTimestamp) + .ge(BmsChargeTemperature::getTimestamp, begin) + .le(BmsChargeTemperature::getTimestamp, end) + .eq(BmsChargeTemperature::getTempIndex, 1) + .orderByDesc(BmsChargeTemperature::getTimestamp) + .last("limit " + REALTIME_TREND_MAX_POINTS) + .list(); + Collections.reverse(rows); + List points = new ArrayList<>(rows.size()); + for (BmsChargeTemperature it : rows) { + TimePointDTO p = new TimePointDTO(); + Date t = it.getTimestamp() != null ? it.getTimestamp() : it.getCreateTime(); + p.setTime(formatTime(t, tf)); + p.setValue(it.getTemperature()); + points.add(p); + } + LineSeriesDTO series = new LineSeriesDTO(); + series.setName("T1"); + series.setPoints(points); + return Collections.singletonList(series); + } + + /** 并行加载实时快照各表最新数据(默认不含 24h 趋势 SQL) */ + private RealtimeSnapshot loadRealtimeSnapshot(String deviceId) { + CompletableFuture vehicleF = CompletableFuture.supplyAsync( + () -> vehicleDataService.selectLatestByDeviceId(deviceId)); + CompletableFuture extremeF = CompletableFuture.supplyAsync( + () -> extremeValuesService.selectLatestByDeviceId(deviceId)); + CompletableFuture locationF = CompletableFuture.supplyAsync( + () -> vehicleLocationService.selectLatestByDeviceId(deviceId)); + CompletableFuture systemF = CompletableFuture.supplyAsync( + () -> latestSystemStatus(deviceId)); + CompletableFuture chargeDetailF = CompletableFuture.supplyAsync( + () -> latestChargeDetail(deviceId)); + CompletableFuture runtimeF = CompletableFuture.supplyAsync( + () -> latestRuntimeData(deviceId)); + CompletableFuture sopF = CompletableFuture.supplyAsync( + () -> latestSopData(deviceId)); + CompletableFuture> svF = CompletableFuture.supplyAsync( + () -> listLatestSubsystemVoltages(deviceId)); + CompletableFuture> stF = CompletableFuture.supplyAsync( + () -> listLatestSubsystemTemperatures(deviceId)); + CompletableFuture> chargeTempF = CompletableFuture.supplyAsync( + () -> listLatestChargeTemperatures(deviceId)); + CompletableFuture> voltageChF = CompletableFuture.supplyAsync( + () -> listLatestVoltageChannels(deviceId)); + + CompletableFuture.allOf(vehicleF, extremeF, locationF, systemF, chargeDetailF, runtimeF, sopF, + svF, stF, chargeTempF, voltageChF).join(); + + RealtimeSnapshot snap = new RealtimeSnapshot(); + snap.vehicleData = vehicleF.join(); + snap.extremeValues = extremeF.join(); + snap.latestLocation = locationF.join(); + snap.systemStatus = systemF.join(); + snap.chargeDetail = chargeDetailF.join(); + snap.runtimeData = runtimeF.join(); + snap.sopData = sopF.join(); + snap.subsystemVoltageRows = svF.join(); + snap.subsystemTemperatureRows = stF.join(); + snap.chargeTemperatures = chargeTempF.join(); + snap.voltageChannels = voltageChF.join(); + return snap; + } + + private static void fillAggregateFromSnapshot(DeviceRealtimeAggregateDTO agg, RealtimeSnapshot snap) { + agg.setVehicleData(snap.vehicleData); + agg.setLatestLocation(snap.latestLocation); + agg.setExtremeValues(snap.extremeValues); + agg.setSystemStatus(snap.systemStatus); + agg.setChargeDetail(snap.chargeDetail); + agg.setRuntimeData(snap.runtimeData); + agg.setSopData(snap.sopData); + agg.setChargeTemperatures(snap.chargeTemperatures); + agg.setVoltageChannels(snap.voltageChannels); + List svRows = snap.subsystemVoltageRows; + agg.setSubsystemVoltage(svRows == null || svRows.isEmpty() ? null : svRows.get(0)); + agg.setSubsystemVoltages(latestOnePerSubsystem(svRows, SubsystemVoltage::getSubsystemNo)); + List stRows = snap.subsystemTemperatureRows; + agg.setSubsystemTemperature(stRows == null || stRows.isEmpty() ? null : stRows.get(0)); + agg.setSubsystemTemperatures(latestOnePerSubsystem(stRows, SubsystemTemperature::getSubsystemNo)); + } + + private BmsSystemStatus latestSystemStatus(String deviceId) { + return bmsSystemStatusService.lambdaQuery() + .eq(BmsSystemStatus::getDeviceId, deviceId) + .isNotNull(BmsSystemStatus::getTimestamp) + .orderByDesc(BmsSystemStatus::getTimestamp) + .last("limit 1") + .one(); + } + + private BmsChargeDetail latestChargeDetail(String deviceId) { + return bmsChargeDetailService.lambdaQuery() + .eq(BmsChargeDetail::getDeviceId, deviceId) + .isNotNull(BmsChargeDetail::getTimestamp) + .orderByDesc(BmsChargeDetail::getTimestamp) + .last("limit 1") + .one(); + } + + private BmsRuntimeData latestRuntimeData(String deviceId) { + return bmsRuntimeDataService.lambdaQuery() + .eq(BmsRuntimeData::getDeviceId, deviceId) + .isNotNull(BmsRuntimeData::getTimestamp) + .orderByDesc(BmsRuntimeData::getTimestamp) + .last("limit 1") + .one(); + } + + private BmsSopData latestSopData(String deviceId) { + return bmsSopDataService.lambdaQuery() + .eq(BmsSopData::getDeviceId, deviceId) + .isNotNull(BmsSopData::getTimestamp) + .orderByDesc(BmsSopData::getTimestamp) + .last("limit 1") + .one(); + } + + private List listLatestSubsystemVoltages(String deviceId) { + return subsystemVoltageService.lambdaQuery() + .eq(SubsystemVoltage::getDeviceId, deviceId) + .isNotNull(SubsystemVoltage::getTimestamp) + .orderByDesc(SubsystemVoltage::getTimestamp) + .last("limit " + REALTIME_SUBSYSTEM_PREFETCH) + .list(); + } + + private List listLatestSubsystemTemperatures(String deviceId) { + return subsystemTemperatureService.lambdaQuery() + .eq(SubsystemTemperature::getDeviceId, deviceId) + .isNotNull(SubsystemTemperature::getTimestamp) + .orderByDesc(SubsystemTemperature::getTimestamp) + .last("limit " + REALTIME_SUBSYSTEM_PREFETCH) + .list(); + } + + private List listLatestChargeTemperatures(String deviceId) { + return bmsChargeTemperatureService.lambdaQuery() + .eq(BmsChargeTemperature::getDeviceId, deviceId) + .isNotNull(BmsChargeTemperature::getTimestamp) + .orderByDesc(BmsChargeTemperature::getTimestamp) + .last("limit 8") + .list(); + } + + private List listLatestVoltageChannels(String deviceId) { + return bmsVoltageChannelDataService.lambdaQuery() + .eq(BmsVoltageChannelData::getDeviceId, deviceId) + .isNotNull(BmsVoltageChannelData::getTimestamp) + .orderByDesc(BmsVoltageChannelData::getTimestamp) + .last("limit 16") + .list(); + } + + private static final class RealtimeSnapshot { + VehicleData vehicleData; + ExtremeValues extremeValues; + VehicleLocation latestLocation; + BmsSystemStatus systemStatus; + BmsChargeDetail chargeDetail; + BmsRuntimeData runtimeData; + BmsSopData sopData; + List subsystemVoltageRows; + List subsystemTemperatureRows; + List chargeTemperatures; + List voltageChannels; + } + + /** 从按时间倒序的结果中取每个子系统号的第一条(即该子系统最新数据) */ + private static List latestOnePerSubsystem(List orderedNewestFirst, Function subsystemNoFn) { + if (orderedNewestFirst == null || orderedNewestFirst.isEmpty()) { + return Collections.emptyList(); + } + Map seen = new LinkedHashMap<>(); + for (T row : orderedNewestFirst) { + Integer no = subsystemNoFn.apply(row); + if (no != null && !seen.containsKey(no)) { + seen.put(no, row); + } + } + List out = new ArrayList<>(seen.values()); + out.sort(Comparator.comparing(subsystemNoFn, Comparator.nullsLast(Integer::compareTo))); + return out; + } } diff --git a/src/main/java/com/evobms/project/vehicledata/domain/VehicleData.java b/src/main/java/com/evobms/project/vehicledata/domain/VehicleData.java index eda5c3f..9800a4e 100644 --- a/src/main/java/com/evobms/project/vehicledata/domain/VehicleData.java +++ b/src/main/java/com/evobms/project/vehicledata/domain/VehicleData.java @@ -42,17 +42,17 @@ public class VehicleData extends BaseEntity /** 车辆状态 (1:启动 2:熄火 3:其他 254:异常 255:无效) */ @ApiModelProperty(value = "车辆状态 (1:启动 2:熄火 3:其他 254:异常 255:无效)") - @Excel(name = "车辆状态 (1:启动 2:熄火 3:其他 254:异常 255:无效)") + @Excel(name = "车辆状态 ") private Integer vehicleStatus; /** 充电状态 (1:停车充电 2:行驶充电 3:未充电 4:充电完成 254:异常 255:无效) */ @ApiModelProperty(value = "充电状态 (1:停车充电 2:行驶充电 3:未充电 4:充电完成 254:异常 255:无效)") - @Excel(name = "充电状态 (1:停车充电 2:行驶充电 3:未充电 4:充电完成 254:异常 255:无效)") + @Excel(name = "充电状态 ") private Integer chargingStatus; /** 运行模式 (1:纯电 2:混动 3:燃油 254:异常 255:无效) */ @ApiModelProperty(value = "运行模式 (1:纯电 2:混动 3:燃油 254:异常 255:无效)") - @Excel(name = "运行模式 (1:纯电 2:混动 3:燃油 254:异常 255:无效)") + @Excel(name = "运行模式 ") private Integer operationMode; /** 车速(km/h) */ @@ -87,7 +87,7 @@ public class VehicleData extends BaseEntity /** 档位 (0:空档 1-6:1-6档 13:倒档 14:自动D档 15:停车P档) */ @ApiModelProperty(value = "档位 (0:空档 1-6:1-6档 13:倒档 14:自动D档 15:停车P档)") - @Excel(name = "档位 (0:空档 1-6:1-6档 13:倒档 14:自动D档 15:停车P档)") + @Excel(name = "档位 ") private Integer gearPosition; /** 绝缘电阻(kΩ) */ diff --git a/src/main/java/com/evobms/project/vehicledata/dto/ChargeDischargeHistoryDTO.java b/src/main/java/com/evobms/project/vehicledata/dto/ChargeDischargeHistoryDTO.java new file mode 100644 index 0000000..1cf8266 --- /dev/null +++ b/src/main/java/com/evobms/project/vehicledata/dto/ChargeDischargeHistoryDTO.java @@ -0,0 +1,15 @@ +package com.evobms.project.vehicledata.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import java.util.List; + +@Data +@ApiModel(value = "ChargeDischargeHistoryDTO", description = "充放电历史统计与明细") +public class ChargeDischargeHistoryDTO { + @ApiModelProperty(value = "月度充放电时长统计") + private List months; + @ApiModelProperty(value = "区段明细记录") + private List records; +} diff --git a/src/main/java/com/evobms/project/vehicledata/dto/ChargeDischargeMonthDTO.java b/src/main/java/com/evobms/project/vehicledata/dto/ChargeDischargeMonthDTO.java new file mode 100644 index 0000000..0749d0a --- /dev/null +++ b/src/main/java/com/evobms/project/vehicledata/dto/ChargeDischargeMonthDTO.java @@ -0,0 +1,17 @@ +package com.evobms.project.vehicledata.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import java.math.BigDecimal; + +@Data +@ApiModel(value = "ChargeDischargeMonthDTO", description = "月充放电时长统计") +public class ChargeDischargeMonthDTO { + @ApiModelProperty(value = "月份(yyyy-MM)", example = "2026-01") + private String month; + @ApiModelProperty(value = "充电总时长(小时)", example = "85.5") + private BigDecimal chargeHours; + @ApiModelProperty(value = "放电总时长(小时)", example = "92.0") + private BigDecimal dischargeHours; +} diff --git a/src/main/java/com/evobms/project/vehicledata/dto/ChargeDischargeRecordDTO.java b/src/main/java/com/evobms/project/vehicledata/dto/ChargeDischargeRecordDTO.java new file mode 100644 index 0000000..66743d2 --- /dev/null +++ b/src/main/java/com/evobms/project/vehicledata/dto/ChargeDischargeRecordDTO.java @@ -0,0 +1,23 @@ +package com.evobms.project.vehicledata.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import java.math.BigDecimal; + +@Data +@ApiModel(value = "ChargeDischargeRecordDTO", description = "充放电区段记录") +public class ChargeDischargeRecordDTO { + @ApiModelProperty(value = "状态:充电/放电", example = "充电") + private String status; + @ApiModelProperty(value = "开始时间", example = "2026-01-10 15:30:00") + private String startTime; + @ApiModelProperty(value = "结束时间", example = "2026-01-10 23:30:00") + private String endTime; + @ApiModelProperty(value = "开始SOC(%)", example = "30") + private Integer startSoc; + @ApiModelProperty(value = "结束SOC(%)", example = "100") + private Integer endSoc; + @ApiModelProperty(value = "本次时长(小时)", example = "5") + private BigDecimal durationHours; +} diff --git a/src/main/java/com/evobms/project/vehicledata/dto/DashboardOverviewDTO.java b/src/main/java/com/evobms/project/vehicledata/dto/DashboardOverviewDTO.java new file mode 100644 index 0000000..5809dc1 --- /dev/null +++ b/src/main/java/com/evobms/project/vehicledata/dto/DashboardOverviewDTO.java @@ -0,0 +1,21 @@ +package com.evobms.project.vehicledata.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import java.util.List; +import lombok.Data; + +@Data +@ApiModel(value = "DashboardOverviewDTO", description = "首页概览聚合数据") +public class DashboardOverviewDTO { + @ApiModelProperty("基础统计(项目/设备/在线/故障)") + private DashboardSummaryDTO summary; + @ApiModelProperty("SOC分布(饼图)") + private List socDistribution; + @ApiModelProperty("日充放电趋势(柱状图)") + private List trend; + @ApiModelProperty("运行时长累计与分布(左侧列表+右侧柱状图)") + private RuntimeSummaryDTO runtimeSummary; + @ApiModelProperty("运行时长分布(柱状图,单位小时)") + private List runtimeDuration; +} diff --git a/src/main/java/com/evobms/project/vehicledata/dto/DashboardSocDistributionDTO.java b/src/main/java/com/evobms/project/vehicledata/dto/DashboardSocDistributionDTO.java new file mode 100644 index 0000000..3f31014 --- /dev/null +++ b/src/main/java/com/evobms/project/vehicledata/dto/DashboardSocDistributionDTO.java @@ -0,0 +1,22 @@ +package com.evobms.project.vehicledata.dto; + +import java.util.List; +import java.util.Map; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +@ApiModel(value = "DashboardSocDistributionDTO", description = "设备SOC分布统计结果") +public class DashboardSocDistributionDTO { + @ApiModelProperty(value = "参与分桶的设备总数(不含unknown)", example = "865") + private long totalDevices; + @ApiModelProperty(value = "未取到有效SOC的设备数", example = "7") + private long unknownCount; + @ApiModelProperty(value = "分桶明细,按从低到高顺序返回") + private List bins; + @ApiModelProperty(value = "生成时间(ISO8601)", example = "2026-03-18T10:22:00Z") + private String generatedAt; + @ApiModelProperty(value = "参数回显(bins/includeOffline/includeUnknown 等)") + private Map paramsEcho; +} diff --git a/src/main/java/com/evobms/project/vehicledata/dto/DashboardSummaryDTO.java b/src/main/java/com/evobms/project/vehicledata/dto/DashboardSummaryDTO.java new file mode 100644 index 0000000..3bc8b84 --- /dev/null +++ b/src/main/java/com/evobms/project/vehicledata/dto/DashboardSummaryDTO.java @@ -0,0 +1,18 @@ +package com.evobms.project.vehicledata.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +@ApiModel(value = "DashboardSummaryDTO", description = "首页概览卡片统计数据") +public class DashboardSummaryDTO { + @ApiModelProperty(value = "项目总数", example = "5") + private long projectCount; + @ApiModelProperty(value = "BBOX设备总数", example = "865") + private long deviceCount; + @ApiModelProperty(value = "在线设备总数", example = "789") + private long onlineCount; + @ApiModelProperty(value = "告警/故障数量", example = "12") + private long faultCount; +} diff --git a/src/main/java/com/evobms/project/vehicledata/dto/DashboardTrendItemDTO.java b/src/main/java/com/evobms/project/vehicledata/dto/DashboardTrendItemDTO.java new file mode 100644 index 0000000..65113a2 --- /dev/null +++ b/src/main/java/com/evobms/project/vehicledata/dto/DashboardTrendItemDTO.java @@ -0,0 +1,16 @@ +package com.evobms.project.vehicledata.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +@ApiModel(value = "DashboardTrendItemDTO", description = "日充放电时长项") +public class DashboardTrendItemDTO { + @ApiModelProperty(value = "日期", example = "2025-01-12") + private String date; + @ApiModelProperty(value = "充电时长(秒)", example = "252000") + private long chargeDuration; + @ApiModelProperty(value = "放电时长(秒)", example = "7200") + private long dischargeDuration; +} diff --git a/src/main/java/com/evobms/project/vehicledata/dto/DeviceDetailDTO.java b/src/main/java/com/evobms/project/vehicledata/dto/DeviceDetailDTO.java new file mode 100644 index 0000000..f9d6273 --- /dev/null +++ b/src/main/java/com/evobms/project/vehicledata/dto/DeviceDetailDTO.java @@ -0,0 +1,72 @@ +package com.evobms.project.vehicledata.dto; + +import com.evobms.framework.aspectj.lang.annotation.Excel; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.math.BigDecimal; + +@Data +@ApiModel(value = "DeviceDetailDTO", description = "设备详细信息聚合") +public class DeviceDetailDTO { + @ApiModelProperty("设备ID") + private String deviceId; + @ApiModelProperty("设备名称") + private String deviceName; + @ApiModelProperty("设备SN") + private String deviceSn; + @ApiModelProperty("设备类型") + private String deviceType; + @ApiModelProperty("设备状态") + private String status; + @ApiModelProperty("固件版本") + private String firmwareVersion; + @ApiModelProperty("最后在线时间") + private String lastOnline; + + @ApiModelProperty("总电压(V)") + private BigDecimal totalVoltage; + @ApiModelProperty("总电流(A)") + private BigDecimal totalCurrent; + @ApiModelProperty("SOC(%)") + private Integer soc; + @ApiModelProperty("SOP(%)") + private Integer sop; + @ApiModelProperty(value = "电池单体电压最高值(V)") + private BigDecimal maxVoltageValue; + @ApiModelProperty(value = "电池单体电压最低值(V)") + private BigDecimal minVoltageValue; + @ApiModelProperty(value = "最高温度值(℃)") + private BigDecimal maxTempValue; + @ApiModelProperty(value = "最低温度值(℃)") + private BigDecimal minTempValue; + + + @ApiModelProperty("车辆状态") + private Integer vehicleStatus; + @ApiModelProperty("充电状态") + private Integer chargingStatus; + @ApiModelProperty("运行模式") + private Integer operationMode; + @ApiModelProperty("DC-DC状态") + private Integer dcdcStatus; + @ApiModelProperty("档位") + private Integer gearPosition; + @ApiModelProperty("绝缘电阻(kΩ)") + private Integer insulationResistance; + + @ApiModelProperty("经度") + private Double longitude; + @ApiModelProperty("纬度") + private Double latitude; + @ApiModelProperty("位置时间") + private String locationTime; + @ApiModelProperty("定位状态") + private Integer positioningStatus; + + @ApiModelProperty("云端维护-最近维护时间") + private String lastMaintenanceTime; + @ApiModelProperty("云端维护-最近升级时间") + private String lastUpgradeTime; +} diff --git a/src/main/java/com/evobms/project/vehicledata/dto/DeviceHistoryFlatRowDTO.java b/src/main/java/com/evobms/project/vehicledata/dto/DeviceHistoryFlatRowDTO.java new file mode 100644 index 0000000..3533848 --- /dev/null +++ b/src/main/java/com/evobms/project/vehicledata/dto/DeviceHistoryFlatRowDTO.java @@ -0,0 +1,142 @@ +package com.evobms.project.vehicledata.dto; + +import java.math.BigDecimal; +import java.util.List; + +import com.evobms.project.battery.domain.SubsystemTemperature; +import com.evobms.project.battery.domain.SubsystemVoltage; +import com.evobms.project.bms.domain.BmsChargeTemperature; +import com.evobms.project.bms.domain.BmsVoltageChannelData; +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + * One flattened history row aligned to vehicle_data time; other tables use latest snapshot at or before that time. + */ +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +@ApiModel(value = "DeviceHistoryFlatRowDTO", description = "Device history flat row (same sources as realtime aggregate)") +public class DeviceHistoryFlatRowDTO { + + @ApiModelProperty("Device id") + private String deviceId; + + @ApiModelProperty("vehicle_data primary key") + private Long vehicleDataId; + + @ApiModelProperty("Point-in-time: business timestamp if present else create_time") + private String pointTime; + + @ApiModelProperty("vehicle_data create_time") + private String vehicleCreateTime; + + @ApiModelProperty("SOC percent") + private Integer soc; + @ApiModelProperty("Total voltage V") + private BigDecimal totalVoltage; + @ApiModelProperty("Total current A") + private BigDecimal totalCurrent; + @ApiModelProperty("Vehicle speed km/h") + private BigDecimal vehicleSpeed; + @ApiModelProperty("Total mileage km") + private BigDecimal totalMileage; + @ApiModelProperty("Charging status") + private Integer chargingStatus; + @ApiModelProperty("Vehicle status") + private Integer vehicleStatus; + @ApiModelProperty("Operation mode") + private Integer operationMode; + @ApiModelProperty("DC-DC status") + private Integer dcdcStatus; + @ApiModelProperty("Gear") + private Integer gearPosition; + @ApiModelProperty("Insulation resistance kOhm") + private Integer insulationResistance; + + @ApiModelProperty("Max cell voltage V") + private BigDecimal maxCellVoltage; + @ApiModelProperty("Min cell voltage V") + private BigDecimal minCellVoltage; + @ApiModelProperty("Max temperature") + private BigDecimal maxTempValue; + @ApiModelProperty("Min temperature") + private BigDecimal minTempValue; + @ApiModelProperty("Max cell position subsystem-cell") + private String maxCellPos; + @ApiModelProperty("Min cell position") + private String minCellPos; + @ApiModelProperty("Max temp position subsystem-probe") + private String maxTempPos; + @ApiModelProperty("Min temp position") + private String minTempPos; + + private Long relayStatus; + private Long onaaccStatus; + private Long resetCount; + private Long cycleCount; + private Long cellCount; + private BigDecimal sample25v; + private BigDecimal sample5v; + private Integer tempSensorCount; + + private BigDecimal acChargeCurrent; + private BigDecimal acChargeVoltage; + private BigDecimal dcChargeCurrent; + private BigDecimal dcChargeVoltage; + private Long chargeTime; + private Integer acStopCode; + private Integer dcStopCode; + private Integer acCpValue; + private Integer acGunResistance; + private Integer chargingStage; + private Integer acStage; + private Integer dcStage; + + private BigDecimal runtimeVehicleSpeed; + private Long runtimeMileage; + private BigDecimal systemVoltage; + private BigDecimal systemCurrent; + private Long runtimeInsulationResistance; + private BigDecimal socMax; + private BigDecimal socMin; + private BigDecimal gpvVoltage; + private Integer runStatus; + private Integer powerStatus; + + private Long dischargeSop10s; + private Long dischargeSop30s; + private Long dischargeSop60s; + private Long dischargeSopContinuous; + private Long dischargeSopTotal; + private Long regenSop10s; + private Long regenSop30s; + private Long regenSop60s; + private Long regenSopContinuous; + private Long regenSopTotal; + private Long chargeSop; + private Long acChargeSop; + + private BigDecimal longitude; + private BigDecimal latitude; + private Integer positioningStatus; + + @ApiModelProperty("Charge seat temperatures, latest per probe index 1-8 at point time") + private List chargeTemperaturesAtPoint; + + @ApiModelProperty("Voltage channels, latest per type+index at point time") + private List voltageChannelsAtPoint; + + @ApiModelProperty("Per-subsystem voltage frame, latest per subsystem no at point time") + private List subsystemVoltagesAtPoint; + + @ApiModelProperty("Per-subsystem temperature frame, latest per subsystem no at point time") + private List subsystemTemperaturesAtPoint; + + @ApiModelProperty("Convenience: probe T1 deg C") + private BigDecimal chargeSeatTemp1; + + @ApiModelProperty("Convenience: RTP channel 1 voltage") + private BigDecimal rtp1Voltage; +} diff --git a/src/main/java/com/evobms/project/vehicledata/dto/DeviceLatestLocationDTO.java b/src/main/java/com/evobms/project/vehicledata/dto/DeviceLatestLocationDTO.java new file mode 100644 index 0000000..96b776a --- /dev/null +++ b/src/main/java/com/evobms/project/vehicledata/dto/DeviceLatestLocationDTO.java @@ -0,0 +1,15 @@ +package com.evobms.project.vehicledata.dto; + +import com.evobms.project.system.domain.BmsDevices; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +@ApiModel(value = "DeviceLatestLocationDTO", description = "设备与其最新位置") +public class DeviceLatestLocationDTO { + @ApiModelProperty(value = "设备对象") + private BmsDevices device; + @ApiModelProperty(value = "最新位置点,可能为空") + private TrackPointDTO location; +} diff --git a/src/main/java/com/evobms/project/vehicledata/dto/DeviceRealtimeAggregateDTO.java b/src/main/java/com/evobms/project/vehicledata/dto/DeviceRealtimeAggregateDTO.java new file mode 100644 index 0000000..c99aeac --- /dev/null +++ b/src/main/java/com/evobms/project/vehicledata/dto/DeviceRealtimeAggregateDTO.java @@ -0,0 +1,53 @@ +package com.evobms.project.vehicledata.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import java.util.List; +import com.evobms.project.vehicledata.domain.VehicleData; +import com.evobms.project.vehicledata.domain.VehicleLocation; +import com.evobms.project.battery.domain.ExtremeValues; +import com.evobms.project.battery.domain.SubsystemTemperature; +import com.evobms.project.battery.domain.SubsystemVoltage; +import com.evobms.project.bms.domain.*; + +@Data +@ApiModel(value = "DeviceRealtimeAggregateDTO", description = "设备实时聚合:原始实体 + dashboard 扁平视图(含24h趋势)") +public class DeviceRealtimeAggregateDTO { + @ApiModelProperty(value = "设备ID", example = "EVO0001") + private String deviceId; + + @ApiModelProperty(value = "基础:最新整车数据") + private VehicleData vehicleData; + @ApiModelProperty(value = "基础:最新位置数据") + private VehicleLocation latestLocation; + + @ApiModelProperty(value = "极值:单体电压/温度极值") + private ExtremeValues extremeValues; + + @ApiModelProperty(value = "BMS:系统状态") + private BmsSystemStatus systemStatus; + @ApiModelProperty(value = "BMS:实时运行") + private BmsRuntimeData runtimeData; + @ApiModelProperty(value = "BMS:充电详情") + private BmsChargeDetail chargeDetail; + @ApiModelProperty(value = "BMS:SOP功率能力") + private BmsSopData sopData; + @ApiModelProperty(value = "BMS:充电座温度最新") + private List chargeTemperatures; + @ApiModelProperty(value = "BMS:电压通道最新") + private List voltageChannels; + + @ApiModelProperty(value = "可充电储能子系统电压:入库时间最新的一条(任意子系统号,便于单独展示)") + private SubsystemVoltage subsystemVoltage; + @ApiModelProperty(value = "可充电储能子系统温度:入库时间最新的一条(任意子系统号,便于单独展示)") + private SubsystemTemperature subsystemTemperature; + + @ApiModelProperty(value = "可充电储能子系统电压:各子系统号各保留入库最新一条") + private List subsystemVoltages; + @ApiModelProperty(value = "可充电储能子系统温度:各子系统号各保留入库最新一条") + private List subsystemTemperatures; + + @ApiModelProperty(value = "前端扁平视图:系统状态分块、通道/探头列表、24h 趋势等(与上方原始实体互补,避免重复组装)") + private DeviceRealtimeDTO dashboard; +} diff --git a/src/main/java/com/evobms/project/vehicledata/dto/DeviceRealtimeDTO.java b/src/main/java/com/evobms/project/vehicledata/dto/DeviceRealtimeDTO.java new file mode 100644 index 0000000..24e9839 --- /dev/null +++ b/src/main/java/com/evobms/project/vehicledata/dto/DeviceRealtimeDTO.java @@ -0,0 +1,142 @@ +package com.evobms.project.vehicledata.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import java.math.BigDecimal; +import java.util.List; + +@Data +@ApiModel(value = "DeviceRealtimeDTO", description = "设备实时详情聚合") +public class DeviceRealtimeDTO { + @ApiModelProperty(value = "设备ID", example = "EVO0001") + private String deviceId; + + @ApiModelProperty(value = "系统状态(总电压/电流、SOC、单体/温度极值等)") + private SystemStatusSection systemStatus; + + @ApiModelProperty(value = "故障信息(名称+时间),暂无则为空数组") + private List faults; + + @ApiModelProperty(value = "运行状态(充放电状态、继电器、复位次数、采样电压等)") + private RunStatusSection runStatus; + + @ApiModelProperty(value = "充电信息(AC/DC电压电流、停止码、阶段等)") + private ChargeSection charge; + + @ApiModelProperty(value = "统计信息(循环次数、累计时长等)") + private StatisticsSection statistics; + + @ApiModelProperty(value = "单体电压列表,name=通道名,value=电压值") + private List voltages; + + @ApiModelProperty(value = "单体温度列表,name=T序号,value=温度值") + private List temperatures; + + @ApiModelProperty(value = "单体电压折线图(默认RTP-1,最近24小时)") + private List voltageTrend; + + @ApiModelProperty(value = "单体温度折线图(默认T1,最近24小时)") + private List temperatureTrend; + + @ApiModelProperty(value = "整车总电压折线图(vehicle_data,最近24小时)") + private List totalVoltageTrend; + @ApiModelProperty(value = "整车总电流折线图(vehicle_data,最近24小时)") + private List totalCurrentTrend; + @ApiModelProperty(value = "整车 SOC 折线图(vehicle_data,最近24小时)") + private List socTrend; + + @Data + public static class SystemStatusSection { + @ApiModelProperty(value = "总电压(V)", example = "372.5") + private BigDecimal totalVoltage; + @ApiModelProperty(value = "总电流(A)", example = "-52.3") + private BigDecimal totalCurrent; + @ApiModelProperty(value = "SOC(%)", example = "95") + private Integer soc; + @ApiModelProperty(value = "SOH(%),暂无数据时为空", example = "86") + private Integer soh; + @ApiModelProperty(value = "最高单体电压(V)", example = "5.0") + private BigDecimal maxCellVoltage; + @ApiModelProperty(value = "最高单体位置(子系统-单体序号)", example = "10-5") + private String maxCellPos; + @ApiModelProperty(value = "最低单体电压(V)", example = "2.0") + private BigDecimal minCellVoltage; + @ApiModelProperty(value = "最低单体位置(子系统-单体序号)", example = "15-3") + private String minCellPos; + @ApiModelProperty(value = "平均单体电压(V)", example = "3.7") + private BigDecimal avgCellVoltage; + @ApiModelProperty(value = "压差单体(V) = 最高-最低", example = "0.005") + private BigDecimal voltageDiff; + @ApiModelProperty(value = "最高温度(℃)", example = "25") + private BigDecimal maxTemp; + @ApiModelProperty(value = "最高温度位置(子系统-探针序号)", example = "4-2") + private String maxTempPos; + @ApiModelProperty(value = "最低温度(℃)", example = "20") + private BigDecimal minTemp; + @ApiModelProperty(value = "最低温度位置(子系统-探针序号)", example = "5-1") + private String minTempPos; + @ApiModelProperty(value = "平均温度(℃)", example = "22") + private BigDecimal avgTemp; + @ApiModelProperty(value = "温差单体(℃) = 最高-最低", example = "2") + private BigDecimal tempDiff; + } + + @Data + public static class RunStatusSection { + @ApiModelProperty(value = "充放电状态:1停车充电/2行驶充电/3未充电/4充电完成/254异常/255无效", example = "1") + private Integer chargingStatus; + @ApiModelProperty(value = "继电器状态(按位标识)", example = "5") + private Long relayStatus; + @ApiModelProperty(value = "ON/A+/CC 状态(按位标识)", example = "4") + private Long onaaccStatus; + @ApiModelProperty(value = "系统复位次数", example = "12") + private Long resetCount; + @ApiModelProperty(value = "电池循环次数", example = "345") + private Long cycleCount; + @ApiModelProperty(value = "2.5V采样电压(V)", example = "2.5") + private BigDecimal sample25v; + @ApiModelProperty(value = "5V采样电压(V)", example = "5.0") + private BigDecimal sample5v; + } + + @Data + public static class ChargeSection { + @ApiModelProperty(value = "慢充电流(A)", example = "7") + private BigDecimal acChargeCurrent; + @ApiModelProperty(value = "慢充电压(V)", example = "12") + private BigDecimal acChargeVoltage; + @ApiModelProperty(value = "快充电流(A)", example = "0") + private BigDecimal dcChargeCurrent; + @ApiModelProperty(value = "快充电压(V)", example = "52") + private BigDecimal dcChargeVoltage; + @ApiModelProperty(value = "本次充电时长(min)", example = "6") + private Long chargeTime; + @ApiModelProperty(value = "慢充停止码", example = "5") + private Integer acStopCode; + @ApiModelProperty(value = "快充停止码", example = "12") + private Integer dcStopCode; + @ApiModelProperty(value = "慢充CP值", example = "20") + private Integer acCpValue; + @ApiModelProperty(value = "慢充枪阻值", example = "5") + private Integer acGunResistance; + @ApiModelProperty(value = "充电阶段", example = "3") + private Integer chargingStage; + @ApiModelProperty(value = "慢充阶段", example = "2") + private Integer acStage; + @ApiModelProperty(value = "快充阶段", example = "1") + private Integer dcStage; + } + + @Data + public static class StatisticsSection { + @ApiModelProperty(value = "循环次数", example = "345") + private Long cycleCount; + @ApiModelProperty(value = "累计运行时间(小时)", example = "1233") + private Long totalRunHours; + @ApiModelProperty(value = "累计充电时间(小时)", example = "456") + private Long totalChargeHours; + @ApiModelProperty(value = "累计放电时间(小时)", example = "321") + private Long totalDischargeHours; + } +} diff --git a/src/main/java/com/evobms/project/vehicledata/dto/DeviceTrackDTO.java b/src/main/java/com/evobms/project/vehicledata/dto/DeviceTrackDTO.java new file mode 100644 index 0000000..e2d3a3e --- /dev/null +++ b/src/main/java/com/evobms/project/vehicledata/dto/DeviceTrackDTO.java @@ -0,0 +1,38 @@ +package com.evobms.project.vehicledata.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import java.util.List; +import java.util.Date; +import lombok.Data; + +@Data +@ApiModel(value = "DeviceTrackDTO", description = "设备轨迹数据") +public class DeviceTrackDTO { + @ApiModelProperty(value = "设备ID", example = "BAT_001") + private String deviceId; + @ApiModelProperty(value = "设备名称", example = "设备A") + private String deviceName; + @ApiModelProperty(value = "设备SN", example = "SN001") + private String deviceSn; + @ApiModelProperty(value = "设备类型", example = "BBOX") + private String deviceType; + @ApiModelProperty(value = "设备状态", example = "online") + private String status; + @ApiModelProperty(value = "固件版本", example = "v1.0.0") + private String firmwareVersion; + @ApiModelProperty(value = "最后在线时间", example = "2026-03-18 10:55:00") + private String lastOnline; + @ApiModelProperty(value = "车速(km/h)", example = "35.2") + private java.math.BigDecimal vehicleSpeed; + @ApiModelProperty(value = "累计里程(km)", example = "12345.6") + private java.math.BigDecimal totalMileage; + @ApiModelProperty(value = "总电压(V)", example = "372.5") + private java.math.BigDecimal totalVoltage; + @ApiModelProperty(value = "总电流(A)", example = "-52.3") + private java.math.BigDecimal totalCurrent; + @ApiModelProperty(value = "SOC(%)", example = "78") + private Integer soc; + @ApiModelProperty(value = "轨迹点列表") + private List trackList; +} diff --git a/src/main/java/com/evobms/project/vehicledata/dto/LineSeriesDTO.java b/src/main/java/com/evobms/project/vehicledata/dto/LineSeriesDTO.java new file mode 100644 index 0000000..21284ea --- /dev/null +++ b/src/main/java/com/evobms/project/vehicledata/dto/LineSeriesDTO.java @@ -0,0 +1,15 @@ +package com.evobms.project.vehicledata.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import java.util.List; + +@Data +@ApiModel(value = "LineSeriesDTO", description = "折线图序列") +public class LineSeriesDTO { + @ApiModelProperty(value = "系列名称", example = "RTP-1") + private String name; + @ApiModelProperty(value = "时间点列表") + private List points; +} diff --git a/src/main/java/com/evobms/project/vehicledata/dto/NameValueDTO.java b/src/main/java/com/evobms/project/vehicledata/dto/NameValueDTO.java new file mode 100644 index 0000000..822d080 --- /dev/null +++ b/src/main/java/com/evobms/project/vehicledata/dto/NameValueDTO.java @@ -0,0 +1,16 @@ +package com.evobms.project.vehicledata.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +@ApiModel(value = "NameValueDTO", description = "名称-数值数据项") +public class NameValueDTO { + @ApiModelProperty(value = "名称", example = "0-20%") + private String name; + @ApiModelProperty(value = "数值", example = "15") + private long value; + @ApiModelProperty(value = "生成时间", example = "2026-03-18T10:22:00Z") + private String createTime; +} diff --git a/src/main/java/com/evobms/project/vehicledata/dto/RuntimeSummaryDTO.java b/src/main/java/com/evobms/project/vehicledata/dto/RuntimeSummaryDTO.java new file mode 100644 index 0000000..1042c69 --- /dev/null +++ b/src/main/java/com/evobms/project/vehicledata/dto/RuntimeSummaryDTO.java @@ -0,0 +1,24 @@ +package com.evobms.project.vehicledata.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +@ApiModel(value = "RuntimeSummaryDTO", description = "运行时长与能量容量累计统计") +public class RuntimeSummaryDTO { + @ApiModelProperty(value = "累计运行时间(小时)", example = "1233") + private long totalRunHours; + @ApiModelProperty(value = "累计充电时间(小时)", example = "545456") + private long totalChargeHours; + @ApiModelProperty(value = "累计放电时间(小时)", example = "565432") + private long totalDischargeHours; + @ApiModelProperty(value = "累计充电能量(kWh)", example = "71233") + private long totalChargeEnergyKWh; + @ApiModelProperty(value = "累计放电能量(kWh)", example = "556452") + private long totalDischargeEnergyKWh; + @ApiModelProperty(value = "累计充电容量(Ah)", example = "8233") + private long totalChargeCapacityAh; + @ApiModelProperty(value = "累计放电容量(Ah)", example = "8233") + private long totalDischargeCapacityAh; +} diff --git a/src/main/java/com/evobms/project/vehicledata/dto/SocBinDTO.java b/src/main/java/com/evobms/project/vehicledata/dto/SocBinDTO.java new file mode 100644 index 0000000..b2691bb --- /dev/null +++ b/src/main/java/com/evobms/project/vehicledata/dto/SocBinDTO.java @@ -0,0 +1,20 @@ +package com.evobms.project.vehicledata.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +@ApiModel(value = "SocBinDTO", description = "SOC分布分桶信息") +public class SocBinDTO { + @ApiModelProperty(value = "标签,例如 20-40", example = "20-40") + private String label; + @ApiModelProperty(value = "桶下界(含)", example = "20") + private int min; + @ApiModelProperty(value = "桶上界(默认不含,最后一桶含)", example = "40") + private int max; + @ApiModelProperty(value = "该桶内设备数量", example = "96") + private long count; + @ApiModelProperty(value = "占比(四位小数)", example = "0.1159") + private double ratio; +} diff --git a/src/main/java/com/evobms/project/vehicledata/dto/TimePointDTO.java b/src/main/java/com/evobms/project/vehicledata/dto/TimePointDTO.java new file mode 100644 index 0000000..841d477 --- /dev/null +++ b/src/main/java/com/evobms/project/vehicledata/dto/TimePointDTO.java @@ -0,0 +1,15 @@ +package com.evobms.project.vehicledata.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import java.math.BigDecimal; + +@Data +@ApiModel(value = "TimePointDTO", description = "折线图时间点") +public class TimePointDTO { + @ApiModelProperty(value = "时间", example = "2026-03-18 12:00:00") + private String time; + @ApiModelProperty(value = "数值", example = "4.95") + private BigDecimal value; +} diff --git a/src/main/java/com/evobms/project/vehicledata/dto/TrackPointDTO.java b/src/main/java/com/evobms/project/vehicledata/dto/TrackPointDTO.java new file mode 100644 index 0000000..f7d7e62 --- /dev/null +++ b/src/main/java/com/evobms/project/vehicledata/dto/TrackPointDTO.java @@ -0,0 +1,18 @@ +package com.evobms.project.vehicledata.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +@ApiModel(value = "TrackPointDTO", description = "轨迹点") +public class TrackPointDTO { + @ApiModelProperty(value = "时间", example = "2026-03-18 10:00:00") + private String timestamp; + @ApiModelProperty(value = "经度", example = "116.397128") + private Double longitude; + @ApiModelProperty(value = "纬度", example = "39.916527") + private Double latitude; + @ApiModelProperty(value = "定位状态(0无效1有效)", example = "1") + private Integer positioningStatus; +} diff --git a/src/main/java/com/evobms/project/vehicledata/mapper/VehicleDataMapper.java b/src/main/java/com/evobms/project/vehicledata/mapper/VehicleDataMapper.java index 8c760b3..10366a6 100644 --- a/src/main/java/com/evobms/project/vehicledata/mapper/VehicleDataMapper.java +++ b/src/main/java/com/evobms/project/vehicledata/mapper/VehicleDataMapper.java @@ -65,4 +65,14 @@ public interface VehicleDataMapper extends BaseMapper List selectByDeviceAndStartTime(@Param("deviceId") String deviceId, @Param("startTime") Date startTime); VehicleData selectLatestByDeviceId(String deviceId); + + /** + * 历史分页:仅按 timestamp 过滤/排序,走 (device_id, timestamp) 索引,避免 COALESCE filesort。 + * MQTT 入库均带 timestamp;timestamp 为空的旧数据不在此接口返回。 + */ + List selectVehicleDataListForHistory( + @Param("deviceId") String deviceId, + @Param("begin") Date begin, + @Param("end") Date end, + @Param("descending") boolean descending); } diff --git a/src/main/java/com/evobms/project/vehicledata/service/DeviceRealtimeHistoryAssembler.java b/src/main/java/com/evobms/project/vehicledata/service/DeviceRealtimeHistoryAssembler.java new file mode 100644 index 0000000..07a641b --- /dev/null +++ b/src/main/java/com/evobms/project/vehicledata/service/DeviceRealtimeHistoryAssembler.java @@ -0,0 +1,753 @@ +package com.evobms.project.vehicledata.service; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.support.SFunction; +import com.evobms.project.battery.domain.ExtremeValues; +import com.evobms.project.battery.domain.SubsystemTemperature; +import com.evobms.project.battery.domain.SubsystemVoltage; +import com.evobms.project.battery.mapper.ExtremeValuesMapper; +import com.evobms.project.battery.service.ISubsystemTemperatureService; +import com.evobms.project.battery.service.ISubsystemVoltageService; +import com.evobms.project.bms.domain.BmsChargeDetail; +import com.evobms.project.bms.domain.BmsChargeTemperature; +import com.evobms.project.bms.domain.BmsRuntimeData; +import com.evobms.project.bms.domain.BmsSopData; +import com.evobms.project.bms.domain.BmsSystemStatus; +import com.evobms.project.bms.domain.BmsVoltageChannelData; +import com.evobms.project.bms.service.IBmsChargeDetailService; +import com.evobms.project.bms.service.IBmsChargeTemperatureService; +import com.evobms.project.bms.service.IBmsRuntimeDataService; +import com.evobms.project.bms.service.IBmsSopDataService; +import com.evobms.project.bms.service.IBmsSystemStatusService; +import com.evobms.project.bms.service.IBmsVoltageChannelDataService; +import com.evobms.project.vehicledata.domain.VehicleData; +import com.evobms.project.vehicledata.domain.VehicleLocation; +import com.evobms.project.vehicledata.dto.DeviceHistoryFlatRowDTO; +import com.evobms.project.vehicledata.mapper.VehicleLocationMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** Merges multi-table BMS/vehicle data into flat history rows by vehicle_data timeline. */ +@Component +public class DeviceRealtimeHistoryAssembler { + + private static final int PREFETCH_CAP_MAX = 20_000; + private static final int PREFETCH_PER_VEHICLE = 40; + private static final int PREFETCH_FLOOR = 128; + /** 多探针/多通道表单页预取上限(pageSize=500 时约 8000) */ + private static final int MAP_PREFETCH_CAP_MAX = 20_000; + /** 每页整车行对应多探针表预取系数(约 8 路温度 × 余量) */ + private static final int MAP_ROWS_PER_VEHICLE = 16; + + @Autowired + private ExtremeValuesMapper extremeValuesMapper; + @Autowired + private IBmsSystemStatusService bmsSystemStatusService; + @Autowired + private IBmsChargeDetailService bmsChargeDetailService; + @Autowired + private IBmsRuntimeDataService bmsRuntimeDataService; + @Autowired + private IBmsSopDataService bmsSopDataService; + @Autowired + private IBmsChargeTemperatureService bmsChargeTemperatureService; + @Autowired + private IBmsVoltageChannelDataService bmsVoltageChannelDataService; + @Autowired + private ISubsystemVoltageService subsystemVoltageService; + @Autowired + private ISubsystemTemperatureService subsystemTemperatureService; + @Autowired + private VehicleLocationMapper vehicleLocationMapper; + + /** + * @param configuredPageSize 本页请求的 pageSize(10/20/100/500),用于放大预取上限;实际行数可能更少(末页) + */ + public List build( + String deviceId, + List vehicleRows, + SimpleDateFormat tf, + int configuredPageSize) { + List out = new ArrayList<>(); + if (vehicleRows == null || vehicleRows.isEmpty()) { + return out; + } + Date tMin = null; + Date tMax = null; + for (VehicleData v : vehicleRows) { + Date t = vehiclePointTime(v); + if (t == null) { + continue; + } + if (tMin == null || t.before(tMin)) { + tMin = t; + } + if (tMax == null || t.after(tMax)) { + tMax = t; + } + } + if (tMax == null) { + return out; + } + // 仅预取「当前页」时间窗内的辅助表数据,避免按整段 beginTime~endTime 拉数千行 + Date lo = tMin; + Date hi = tMax; + Date bootstrapBefore = tMin; + int capRows = Math.max(vehicleRows.size(), Math.max(1, configuredPageSize)); + int cap = prefetchCap(capRows); + int mapCap = mapPrefetchCap(capRows); + PrefetchContext ctx = loadPrefetchParallel(deviceId, bootstrapBefore, lo, hi, cap, mapCap); + + ExtremeValues curEx = ctx.curEx; + BmsSystemStatus curSs = ctx.curSs; + BmsChargeDetail curCd = ctx.curCd; + BmsRuntimeData curRt = ctx.curRt; + BmsSopData curSop = ctx.curSop; + VehicleLocation curLoc = ctx.curLoc; + Map chargeByIdx = ctx.chargeByIdx; + Map voltByKey = ctx.voltByKey; + Map svByNo = ctx.svByNo; + Map stByNo = ctx.stByNo; + + List extremeAsc = ctx.extremeAsc; + List ssAsc = ctx.ssAsc; + List cdAsc = ctx.cdAsc; + List rtAsc = ctx.rtAsc; + List sopAsc = ctx.sopAsc; + List locAsc = ctx.locAsc; + List chargeAsc = ctx.chargeAsc; + List voltAsc = ctx.voltAsc; + List svAsc = ctx.svAsc; + List stAsc = ctx.stAsc; + + List orderAsc = sortedIndicesByPointTimeAsc(vehicleRows); + + int pEx = 0, pSs = 0, pCd = 0, pRt = 0, pSop = 0, pLoc = 0; + int pCh = 0, pVc = 0, pSv = 0, pSt = 0; + + Map slot = new HashMap<>(); + + for (int idx : orderAsc) { + VehicleData vd = vehicleRows.get(idx); + Date ti = vehiclePointTime(vd); + if (ti == null) { + continue; + } + + while (pEx < extremeAsc.size() && extremeEffectiveTime(extremeAsc.get(pEx)).compareTo(ti) <= 0) { + curEx = extremeAsc.get(pEx++); + } + while (pSs < ssAsc.size() && nullSafe(ssAsc.get(pSs).getCreateTime()).compareTo(ti) <= 0) { + curSs = ssAsc.get(pSs++); + } + while (pCd < cdAsc.size() && nullSafe(cdAsc.get(pCd).getCreateTime()).compareTo(ti) <= 0) { + curCd = cdAsc.get(pCd++); + } + while (pRt < rtAsc.size() && nullSafe(rtAsc.get(pRt).getCreateTime()).compareTo(ti) <= 0) { + curRt = rtAsc.get(pRt++); + } + while (pSop < sopAsc.size() && nullSafe(sopAsc.get(pSop).getCreateTime()).compareTo(ti) <= 0) { + curSop = sopAsc.get(pSop++); + } + while (pLoc < locAsc.size() && locationEffectiveTime(locAsc.get(pLoc)).compareTo(ti) <= 0) { + curLoc = locAsc.get(pLoc++); + } + while (pCh < chargeAsc.size() && chargeTempEffectiveTime(chargeAsc.get(pCh)).compareTo(ti) <= 0) { + BmsChargeTemperature row = chargeAsc.get(pCh++); + if (row.getTempIndex() != null) { + chargeByIdx.put(row.getTempIndex(), row); + } + } + while (pVc < voltAsc.size() && voltageChannelEffectiveTime(voltAsc.get(pVc)).compareTo(ti) <= 0) { + BmsVoltageChannelData row = voltAsc.get(pVc++); + voltByKey.put(voltChannelKey(row), row); + } + while (pSv < svAsc.size() && subsystemVoltageEffectiveTime(svAsc.get(pSv)).compareTo(ti) <= 0) { + SubsystemVoltage row = svAsc.get(pSv++); + if (row.getSubsystemNo() != null) { + svByNo.put(row.getSubsystemNo(), row); + } + } + while (pSt < stAsc.size() && subsystemTemperatureEffectiveTime(stAsc.get(pSt)).compareTo(ti) <= 0) { + SubsystemTemperature row = stAsc.get(pSt++); + if (row.getSubsystemNo() != null) { + stByNo.put(row.getSubsystemNo(), row); + } + } + + DeviceHistoryFlatRowDTO dto = toFlat(deviceId, vd, tf, curEx, curSs, curCd, curRt, curSop, curLoc, + chargeByIdx, voltByKey, svByNo, stByNo); + slot.put(idx, dto); + } + + for (int i = 0; i < vehicleRows.size(); i++) { + DeviceHistoryFlatRowDTO row = slot.get(i); + if (row != null) { + out.add(row); + } + } + return out; + } + + private PrefetchContext loadPrefetchParallel( + String deviceId, Date bootstrapBefore, Date lo, Date hi, int cap, int mapCap) { + CompletableFuture curExF = CompletableFuture.supplyAsync(() -> bootstrapExtremeValues(deviceId, bootstrapBefore)); + CompletableFuture curSsF = CompletableFuture.supplyAsync(() -> bootstrapSystemStatus(deviceId, bootstrapBefore)); + CompletableFuture curCdF = CompletableFuture.supplyAsync(() -> bootstrapChargeDetail(deviceId, bootstrapBefore)); + CompletableFuture curRtF = CompletableFuture.supplyAsync(() -> bootstrapRuntimeData(deviceId, bootstrapBefore)); + CompletableFuture curSopF = CompletableFuture.supplyAsync(() -> bootstrapSopData(deviceId, bootstrapBefore)); + CompletableFuture curLocF = CompletableFuture.supplyAsync(() -> bootstrapVehicleLocation(deviceId, bootstrapBefore)); + CompletableFuture> chargeMapF = CompletableFuture.supplyAsync(() -> bootstrapChargeTemps(deviceId, bootstrapBefore, mapCap)); + CompletableFuture> voltMapF = CompletableFuture.supplyAsync(() -> bootstrapVoltageChannels(deviceId, bootstrapBefore, mapCap)); + CompletableFuture> svMapF = CompletableFuture.supplyAsync(() -> bootstrapSubsystemVoltages(deviceId, bootstrapBefore, mapCap)); + CompletableFuture> stMapF = CompletableFuture.supplyAsync(() -> bootstrapSubsystemTemperatures(deviceId, bootstrapBefore, mapCap)); + + CompletableFuture> extremeAscF = CompletableFuture.supplyAsync(() -> loadExtremeValues(deviceId, lo, hi, cap)); + CompletableFuture> ssAscF = CompletableFuture.supplyAsync(() -> loadSystemStatus(deviceId, lo, hi, cap)); + CompletableFuture> cdAscF = CompletableFuture.supplyAsync(() -> loadChargeDetail(deviceId, lo, hi, cap)); + CompletableFuture> rtAscF = CompletableFuture.supplyAsync(() -> loadRuntimeData(deviceId, lo, hi, cap)); + CompletableFuture> sopAscF = CompletableFuture.supplyAsync(() -> loadSopData(deviceId, lo, hi, cap)); + CompletableFuture> locAscF = CompletableFuture.supplyAsync(() -> loadVehicleLocations(deviceId, lo, hi, cap)); + CompletableFuture> chargeAscF = CompletableFuture.supplyAsync(() -> loadChargeTemps(deviceId, lo, hi, mapCap)); + CompletableFuture> voltAscF = CompletableFuture.supplyAsync(() -> loadVoltageChannels(deviceId, lo, hi, mapCap)); + CompletableFuture> svAscF = CompletableFuture.supplyAsync(() -> loadSubsystemVoltages(deviceId, lo, hi, mapCap)); + CompletableFuture> stAscF = CompletableFuture.supplyAsync(() -> loadSubsystemTemperatures(deviceId, lo, hi, mapCap)); + + CompletableFuture.allOf( + curExF, curSsF, curCdF, curRtF, curSopF, curLocF, chargeMapF, voltMapF, svMapF, stMapF, + extremeAscF, ssAscF, cdAscF, rtAscF, sopAscF, locAscF, chargeAscF, voltAscF, svAscF, stAscF + ).join(); + + PrefetchContext ctx = new PrefetchContext(); + ctx.curEx = curExF.join(); + ctx.curSs = curSsF.join(); + ctx.curCd = curCdF.join(); + ctx.curRt = curRtF.join(); + ctx.curSop = curSopF.join(); + ctx.curLoc = curLocF.join(); + ctx.chargeByIdx = chargeMapF.join(); + ctx.voltByKey = voltMapF.join(); + ctx.svByNo = svMapF.join(); + ctx.stByNo = stMapF.join(); + ctx.extremeAsc = extremeAscF.join(); + ctx.ssAsc = ssAscF.join(); + ctx.cdAsc = cdAscF.join(); + ctx.rtAsc = rtAscF.join(); + ctx.sopAsc = sopAscF.join(); + ctx.locAsc = locAscF.join(); + ctx.chargeAsc = chargeAscF.join(); + ctx.voltAsc = voltAscF.join(); + ctx.svAsc = svAscF.join(); + ctx.stAsc = stAscF.join(); + return ctx; + } + + private static final class PrefetchContext { + ExtremeValues curEx; + BmsSystemStatus curSs; + BmsChargeDetail curCd; + BmsRuntimeData curRt; + BmsSopData curSop; + VehicleLocation curLoc; + Map chargeByIdx; + Map voltByKey; + Map svByNo; + Map stByNo; + List extremeAsc; + List ssAsc; + List cdAsc; + List rtAsc; + List sopAsc; + List locAsc; + List chargeAsc; + List voltAsc; + List svAsc; + List stAsc; + } + + private static int prefetchCap(int vehicleCount) { + long cap = (long) vehicleCount * PREFETCH_PER_VEHICLE; + cap = Math.max(PREFETCH_FLOOR, cap); + cap = Math.min(PREFETCH_CAP_MAX, cap); + return (int) cap; + } + + private static int mapPrefetchCap(int vehicleCount) { + long cap = (long) vehicleCount * MAP_ROWS_PER_VEHICLE; + cap = Math.max(64, cap); + cap = Math.min(MAP_PREFETCH_CAP_MAX, cap); + return (int) cap; + } + + private static List sortedIndicesByPointTimeAsc(List vehicleRows) { + List ix = new ArrayList<>(vehicleRows.size()); + for (int i = 0; i < vehicleRows.size(); i++) { + ix.add(i); + } + ix.sort(Comparator.comparing(i -> vehiclePointTime(vehicleRows.get(i)), Comparator.nullsLast(Date::compareTo))); + return ix; + } + + private DeviceHistoryFlatRowDTO toFlat( + String deviceId, + VehicleData vd, + SimpleDateFormat tf, + ExtremeValues ev, + BmsSystemStatus ss, + BmsChargeDetail cd, + BmsRuntimeData rt, + BmsSopData sop, + VehicleLocation loc, + Map chargeByIdx, + Map voltByKey, + Map svByNo, + Map stByNo) { + + DeviceHistoryFlatRowDTO dto = new DeviceHistoryFlatRowDTO(); + dto.setDeviceId(deviceId); + dto.setVehicleDataId(vd.getId()); + Date pt = vehiclePointTime(vd); + dto.setPointTime(pt == null ? null : tf.format(pt)); + dto.setVehicleCreateTime(vd.getCreateTime() == null ? null : tf.format(vd.getCreateTime())); + + dto.setSoc(vd.getSoc()); + dto.setTotalVoltage(vd.getTotalVoltage()); + dto.setTotalCurrent(vd.getTotalCurrent()); + dto.setVehicleSpeed(vd.getVehicleSpeed()); + dto.setTotalMileage(vd.getTotalMileage()); + dto.setChargingStatus(vd.getChargingStatus()); + dto.setVehicleStatus(vd.getVehicleStatus()); + dto.setOperationMode(vd.getOperationMode()); + dto.setDcdcStatus(vd.getDcdcStatus()); + dto.setGearPosition(vd.getGearPosition()); + dto.setInsulationResistance(vd.getInsulationResistance()); + + if (ev != null) { + dto.setMaxCellVoltage(ev.getMaxVoltageValue()); + dto.setMinCellVoltage(ev.getMinVoltageValue()); + dto.setMaxTempValue(ev.getMaxTempValue()); + dto.setMinTempValue(ev.getMinTempValue()); + dto.setMaxCellPos(joinPos(ev.getMaxVoltageSubsystemNo(), ev.getMaxVoltageBatteryNo())); + dto.setMinCellPos(joinPos(ev.getMinVoltageSubsystemNo(), ev.getMinVoltageBatteryNo())); + dto.setMaxTempPos(joinPos(ev.getMaxTempSubsystemNo(), ev.getMaxTempProbeNo())); + dto.setMinTempPos(joinPos(ev.getMinTempSubsystemNo(), ev.getMinTempProbeNo())); + } + + if (ss != null) { + dto.setRelayStatus(ss.getRelayStatus()); + dto.setOnaaccStatus(ss.getOnaaccStatus()); + dto.setResetCount(ss.getResetCount()); + dto.setCycleCount(ss.getCycleCount()); + dto.setCellCount(ss.getCellCount()); + dto.setSample25v(ss.getSample25v()); + dto.setSample5v(ss.getSample5v()); + dto.setTempSensorCount(ss.getTempSensorCount()); + } + + if (cd != null) { + dto.setAcChargeCurrent(cd.getAcChargeCurrent()); + dto.setAcChargeVoltage(cd.getAcChargeVoltage()); + dto.setDcChargeCurrent(cd.getDcChargeCurrent()); + dto.setDcChargeVoltage(cd.getDcChargeVoltage()); + dto.setChargeTime(cd.getChargeTime()); + dto.setAcStopCode(cd.getAcStopCode()); + dto.setDcStopCode(cd.getDcStopCode()); + dto.setAcCpValue(cd.getAcCpValue()); + dto.setAcGunResistance(cd.getAcGunResistance()); + dto.setChargingStage(cd.getChargingStage()); + dto.setAcStage(cd.getAcStage()); + dto.setDcStage(cd.getDcStage()); + } + + if (rt != null) { + dto.setRuntimeVehicleSpeed(rt.getVehicleSpeed()); + dto.setRuntimeMileage(rt.getMileage()); + dto.setSystemVoltage(rt.getSystemVoltage()); + dto.setSystemCurrent(rt.getSystemCurrent()); + dto.setRuntimeInsulationResistance(rt.getInsulationResistance()); + dto.setSocMax(rt.getSocMax()); + dto.setSocMin(rt.getSocMin()); + dto.setGpvVoltage(rt.getGpvVoltage()); + dto.setRunStatus(rt.getRunStatus()); + dto.setPowerStatus(rt.getPowerStatus()); + } + + if (sop != null) { + dto.setDischargeSop10s(sop.getDischargeSop10s()); + dto.setDischargeSop30s(sop.getDischargeSop30s()); + dto.setDischargeSop60s(sop.getDischargeSop60s()); + dto.setDischargeSopContinuous(sop.getDischargeSopContinuous()); + dto.setDischargeSopTotal(sop.getDischargeSopTotal()); + dto.setRegenSop10s(sop.getRegenSop10s()); + dto.setRegenSop30s(sop.getRegenSop30s()); + dto.setRegenSop60s(sop.getRegenSop60s()); + dto.setRegenSopContinuous(sop.getRegenSopContinuous()); + dto.setRegenSopTotal(sop.getRegenSopTotal()); + dto.setChargeSop(sop.getChargeSop()); + dto.setAcChargeSop(sop.getAcChargeSop()); + } + + if (loc != null) { + dto.setLongitude(loc.getLongitude()); + dto.setLatitude(loc.getLatitude()); + dto.setPositioningStatus(loc.getPositioningStatus()); + } + + List temps = new ArrayList<>(chargeByIdx.values()); + temps.sort(Comparator.comparing(BmsChargeTemperature::getTempIndex, Comparator.nullsLast(Integer::compareTo))); + dto.setChargeTemperaturesAtPoint(temps.isEmpty() ? null : temps); + + List volts = new ArrayList<>(voltByKey.values()); + volts.sort(Comparator + .comparing(BmsVoltageChannelData::getChannelType, Comparator.nullsFirst(String::compareTo)) + .thenComparing(BmsVoltageChannelData::getChannelIndex, Comparator.nullsLast(Integer::compareTo))); + dto.setVoltageChannelsAtPoint(volts.isEmpty() ? null : volts); + + List svs = new ArrayList<>(svByNo.values()); + svs.sort(Comparator.comparing(SubsystemVoltage::getSubsystemNo, Comparator.nullsLast(Integer::compareTo))); + dto.setSubsystemVoltagesAtPoint(svs.isEmpty() ? null : svs); + + List sts = new ArrayList<>(stByNo.values()); + sts.sort(Comparator.comparing(SubsystemTemperature::getSubsystemNo, Comparator.nullsLast(Integer::compareTo))); + dto.setSubsystemTemperaturesAtPoint(sts.isEmpty() ? null : sts); + + BmsChargeTemperature t1 = chargeByIdx.get(1); + dto.setChargeSeatTemp1(t1 == null ? null : t1.getTemperature()); + + BmsVoltageChannelData rtp1 = voltByKey.get("RTP|1"); + dto.setRtp1Voltage(rtp1 == null ? null : rtp1.getVoltage()); + + return dto; + } + + private static String voltChannelKey(BmsVoltageChannelData row) { + String t = row.getChannelType() == null ? "" : row.getChannelType(); + Integer ix = row.getChannelIndex(); + return t + "|" + (ix == null ? "" : ix); + } + + private static String joinPos(Integer a, Integer b) { + if (a == null || b == null) { + return null; + } + return a + "-" + b; + } + + private static Date nullSafe(Date d) { + return d == null ? new Date(0L) : d; + } + + private static Date vehiclePointTime(VehicleData v) { + if (v == null) { + return null; + } + return v.getTimestamp() != null ? v.getTimestamp() : v.getCreateTime(); + } + + private static Date extremeEffectiveTime(ExtremeValues e) { + return e.getTimestamp() != null ? e.getTimestamp() : e.getCreateTime(); + } + + private static Date locationEffectiveTime(VehicleLocation l) { + return l.getTimestamp() != null ? l.getTimestamp() : l.getCreateTime(); + } + + private static Date chargeTempEffectiveTime(BmsChargeTemperature r) { + return r.getTimestamp() != null ? r.getTimestamp() : r.getCreateTime(); + } + + private static Date voltageChannelEffectiveTime(BmsVoltageChannelData r) { + return r.getTimestamp() != null ? r.getTimestamp() : r.getCreateTime(); + } + + private static Date subsystemVoltageEffectiveTime(SubsystemVoltage r) { + return r.getTimestamp() != null ? r.getTimestamp() : r.getCreateTime(); + } + + private static Date subsystemTemperatureEffectiveTime(SubsystemTemperature r) { + return r.getTimestamp() != null ? r.getTimestamp() : r.getCreateTime(); + } + + private ExtremeValues bootstrapExtremeValues(String deviceId, Date before) { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + qw.eq(ExtremeValues::getDeviceId, deviceId); + applyTimestampLt(qw, ExtremeValues::getTimestamp, before); + qw.orderByDesc(ExtremeValues::getTimestamp); + qw.last("LIMIT 1"); + List rows = extremeValuesMapper.selectList(qw); + return rows == null || rows.isEmpty() ? null : rows.get(0); + } + + private VehicleLocation bootstrapVehicleLocation(String deviceId, Date before) { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + qw.eq(VehicleLocation::getDeviceId, deviceId); + applyTimestampLt(qw, VehicleLocation::getTimestamp, before); + qw.orderByDesc(VehicleLocation::getTimestamp); + qw.last("LIMIT 1"); + List rows = vehicleLocationMapper.selectList(qw); + return rows == null || rows.isEmpty() ? null : rows.get(0); + } + + private BmsSystemStatus bootstrapSystemStatus(String deviceId, Date before) { + return bmsSystemStatusService.lambdaQuery() + .eq(BmsSystemStatus::getDeviceId, deviceId) + .isNotNull(BmsSystemStatus::getTimestamp) + .lt(BmsSystemStatus::getTimestamp, before) + .orderByDesc(BmsSystemStatus::getTimestamp) + .last("limit 1") + .one(); + } + + private BmsChargeDetail bootstrapChargeDetail(String deviceId, Date before) { + return bmsChargeDetailService.lambdaQuery() + .eq(BmsChargeDetail::getDeviceId, deviceId) + .isNotNull(BmsChargeDetail::getTimestamp) + .lt(BmsChargeDetail::getTimestamp, before) + .orderByDesc(BmsChargeDetail::getTimestamp) + .last("limit 1") + .one(); + } + + private BmsRuntimeData bootstrapRuntimeData(String deviceId, Date before) { + return bmsRuntimeDataService.lambdaQuery() + .eq(BmsRuntimeData::getDeviceId, deviceId) + .isNotNull(BmsRuntimeData::getTimestamp) + .lt(BmsRuntimeData::getTimestamp, before) + .orderByDesc(BmsRuntimeData::getTimestamp) + .last("limit 1") + .one(); + } + + private BmsSopData bootstrapSopData(String deviceId, Date before) { + return bmsSopDataService.lambdaQuery() + .eq(BmsSopData::getDeviceId, deviceId) + .isNotNull(BmsSopData::getTimestamp) + .lt(BmsSopData::getTimestamp, before) + .orderByDesc(BmsSopData::getTimestamp) + .last("limit 1") + .one(); + } + + private Map bootstrapChargeTemps(String deviceId, Date before, int limit) { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + qw.eq(BmsChargeTemperature::getDeviceId, deviceId); + applyTimestampLt(qw, BmsChargeTemperature::getTimestamp, before); + qw.orderByAsc(BmsChargeTemperature::getTimestamp); + qw.last("LIMIT " + limit); + return replayChargeTemps(bmsChargeTemperatureService.list(qw)); + } + + private Map bootstrapVoltageChannels(String deviceId, Date before, int limit) { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + qw.eq(BmsVoltageChannelData::getDeviceId, deviceId); + applyTimestampLt(qw, BmsVoltageChannelData::getTimestamp, before); + qw.orderByAsc(BmsVoltageChannelData::getTimestamp); + qw.last("LIMIT " + limit); + return replayVoltageChannels(bmsVoltageChannelDataService.list(qw)); + } + + private Map bootstrapSubsystemVoltages(String deviceId, Date before, int limit) { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + qw.eq(SubsystemVoltage::getDeviceId, deviceId); + applyTimestampLt(qw, SubsystemVoltage::getTimestamp, before); + qw.orderByAsc(SubsystemVoltage::getTimestamp); + qw.last("LIMIT " + limit); + return replaySubsystemVoltages(subsystemVoltageService.list(qw)); + } + + private Map bootstrapSubsystemTemperatures(String deviceId, Date before, int limit) { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + qw.eq(SubsystemTemperature::getDeviceId, deviceId); + applyTimestampLt(qw, SubsystemTemperature::getTimestamp, before); + qw.orderByAsc(SubsystemTemperature::getTimestamp); + qw.last("LIMIT " + limit); + return replaySubsystemTemperatures(subsystemTemperatureService.list(qw)); + } + + private static Map replayChargeTemps(List rows) { + Map map = new HashMap<>(); + if (rows == null) { + return map; + } + for (BmsChargeTemperature row : rows) { + if (row.getTempIndex() != null) { + map.put(row.getTempIndex(), row); + } + } + return map; + } + + private static Map replayVoltageChannels(List rows) { + Map map = new HashMap<>(); + if (rows == null) { + return map; + } + for (BmsVoltageChannelData row : rows) { + map.put(voltChannelKey(row), row); + } + return map; + } + + private static Map replaySubsystemVoltages(List rows) { + Map map = new HashMap<>(); + if (rows == null) { + return map; + } + for (SubsystemVoltage row : rows) { + if (row.getSubsystemNo() != null) { + map.put(row.getSubsystemNo(), row); + } + } + return map; + } + + private static Map replaySubsystemTemperatures(List rows) { + Map map = new HashMap<>(); + if (rows == null) { + return map; + } + for (SubsystemTemperature row : rows) { + if (row.getSubsystemNo() != null) { + map.put(row.getSubsystemNo(), row); + } + } + return map; + } + + private List loadExtremeValues(String deviceId, Date lo, Date hi, int cap) { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + qw.eq(ExtremeValues::getDeviceId, deviceId); + applyTimestampRange(qw, ExtremeValues::getTimestamp, lo, hi); + qw.orderByAsc(ExtremeValues::getTimestamp); + qw.last("LIMIT " + cap); + List rows = extremeValuesMapper.selectList(qw); + return rows == null ? new ArrayList<>() : rows; + } + + private List loadVehicleLocations(String deviceId, Date lo, Date hi, int cap) { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + qw.eq(VehicleLocation::getDeviceId, deviceId); + applyTimestampRange(qw, VehicleLocation::getTimestamp, lo, hi); + qw.orderByAsc(VehicleLocation::getTimestamp); + qw.last("LIMIT " + cap); + List rows = vehicleLocationMapper.selectList(qw); + return rows == null ? new ArrayList<>() : rows; + } + + private List loadChargeTemps(String deviceId, Date lo, Date hi, int cap) { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + qw.eq(BmsChargeTemperature::getDeviceId, deviceId); + applyTimestampRange(qw, BmsChargeTemperature::getTimestamp, lo, hi); + qw.orderByAsc(BmsChargeTemperature::getTimestamp); + qw.last("LIMIT " + cap); + List rows = bmsChargeTemperatureService.list(qw); + return rows == null ? new ArrayList<>() : rows; + } + + private List loadVoltageChannels(String deviceId, Date lo, Date hi, int cap) { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + qw.eq(BmsVoltageChannelData::getDeviceId, deviceId); + applyTimestampRange(qw, BmsVoltageChannelData::getTimestamp, lo, hi); + qw.orderByAsc(BmsVoltageChannelData::getTimestamp); + qw.last("LIMIT " + cap); + List rows = bmsVoltageChannelDataService.list(qw); + return rows == null ? new ArrayList<>() : rows; + } + + private List loadSubsystemVoltages(String deviceId, Date lo, Date hi, int cap) { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + qw.eq(SubsystemVoltage::getDeviceId, deviceId); + applyTimestampRange(qw, SubsystemVoltage::getTimestamp, lo, hi); + qw.orderByAsc(SubsystemVoltage::getTimestamp); + qw.last("LIMIT " + cap); + List rows = subsystemVoltageService.list(qw); + return rows == null ? new ArrayList<>() : rows; + } + + private List loadSubsystemTemperatures(String deviceId, Date lo, Date hi, int cap) { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + qw.eq(SubsystemTemperature::getDeviceId, deviceId); + applyTimestampRange(qw, SubsystemTemperature::getTimestamp, lo, hi); + qw.orderByAsc(SubsystemTemperature::getTimestamp); + qw.last("LIMIT " + cap); + List rows = subsystemTemperatureService.list(qw); + return rows == null ? new ArrayList<>() : rows; + } + + private List loadSystemStatus(String deviceId, Date lo, Date hi, int cap) { + return bmsSystemStatusService.lambdaQuery() + .eq(BmsSystemStatus::getDeviceId, deviceId) + .isNotNull(BmsSystemStatus::getTimestamp) + .ge(BmsSystemStatus::getTimestamp, lo) + .le(BmsSystemStatus::getTimestamp, hi) + .orderByAsc(BmsSystemStatus::getTimestamp) + .last("limit " + cap) + .list(); + } + + private List loadChargeDetail(String deviceId, Date lo, Date hi, int cap) { + return bmsChargeDetailService.lambdaQuery() + .eq(BmsChargeDetail::getDeviceId, deviceId) + .isNotNull(BmsChargeDetail::getTimestamp) + .ge(BmsChargeDetail::getTimestamp, lo) + .le(BmsChargeDetail::getTimestamp, hi) + .orderByAsc(BmsChargeDetail::getTimestamp) + .last("limit " + cap) + .list(); + } + + private List loadRuntimeData(String deviceId, Date lo, Date hi, int cap) { + return bmsRuntimeDataService.lambdaQuery() + .eq(BmsRuntimeData::getDeviceId, deviceId) + .isNotNull(BmsRuntimeData::getTimestamp) + .ge(BmsRuntimeData::getTimestamp, lo) + .le(BmsRuntimeData::getTimestamp, hi) + .orderByAsc(BmsRuntimeData::getTimestamp) + .last("limit " + cap) + .list(); + } + + private List loadSopData(String deviceId, Date lo, Date hi, int cap) { + return bmsSopDataService.lambdaQuery() + .eq(BmsSopData::getDeviceId, deviceId) + .isNotNull(BmsSopData::getTimestamp) + .ge(BmsSopData::getTimestamp, lo) + .le(BmsSopData::getTimestamp, hi) + .orderByAsc(BmsSopData::getTimestamp) + .last("limit " + cap) + .list(); + } + + private static void applyTimestampLt( + LambdaQueryWrapper qw, + SFunction timestampCol, + Date before) { + qw.isNotNull(timestampCol); + if (before != null) { + qw.lt(timestampCol, before); + } + } + + private static void applyTimestampRange( + LambdaQueryWrapper qw, + SFunction timestampCol, + Date lo, + Date hi) { + qw.isNotNull(timestampCol); + if (lo != null) { + qw.ge(timestampCol, lo); + } + if (hi != null) { + qw.le(timestampCol, hi); + } + } +} diff --git a/src/main/java/com/evobms/project/vehicledata/service/IVehicleDataService.java b/src/main/java/com/evobms/project/vehicledata/service/IVehicleDataService.java index f1dfd43..6de1152 100644 --- a/src/main/java/com/evobms/project/vehicledata/service/IVehicleDataService.java +++ b/src/main/java/com/evobms/project/vehicledata/service/IVehicleDataService.java @@ -67,4 +67,14 @@ public interface IVehicleDataService VehicleData selectLatestByDeviceId(String deviceId); HomeDashboardDTO getHomeDashboard(); + + /** + * 历史平铺分页:按业务时间优先(timestamp),否则 create_time;支持时间范围与排序。 + */ + List selectVehicleDataListForHistory(String deviceId, Date begin, Date end, boolean descending); + + /** + * 趋势图采样:时间范围内按业务时间倒序取最多 maxPoints 条,返回结果按时间正序。 + */ + List selectVehicleDataTrendInRange(String deviceId, Date begin, Date end, int maxPoints); } diff --git a/src/main/java/com/evobms/project/vehicledata/service/impl/VehicleDataServiceImpl.java b/src/main/java/com/evobms/project/vehicledata/service/impl/VehicleDataServiceImpl.java index 5a76e2a..b78ba99 100644 --- a/src/main/java/com/evobms/project/vehicledata/service/impl/VehicleDataServiceImpl.java +++ b/src/main/java/com/evobms/project/vehicledata/service/impl/VehicleDataServiceImpl.java @@ -1,6 +1,8 @@ package com.evobms.project.vehicledata.service.impl; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; @@ -54,6 +56,11 @@ public class VehicleDataServiceImpl extends ServiceImpl selectVehicleDataListForHistory(String deviceId, Date begin, Date end, boolean descending) { + return vehicleDataMapper.selectVehicleDataListForHistory(deviceId, begin, end, descending); + } + @Override public List selectVehicleDataList(VehicleData vehicleData) { @@ -81,6 +88,37 @@ public class VehicleDataServiceImpl extends ServiceImpl selectVehicleDataTrendInRange(String deviceId, Date begin, Date end, int maxPoints) { + int limit = maxPoints <= 0 ? 360 : Math.min(maxPoints, 2000); + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + qw.select( + VehicleData::getId, + VehicleData::getDeviceId, + VehicleData::getTimestamp, + VehicleData::getTotalVoltage, + VehicleData::getTotalCurrent, + VehicleData::getSoc, + VehicleData::getCreateTime); + qw.eq(VehicleData::getDeviceId, deviceId); + qw.isNotNull(VehicleData::getTimestamp); + if (begin != null) { + qw.ge(VehicleData::getTimestamp, begin); + } + if (end != null) { + qw.le(VehicleData::getTimestamp, end); + } + qw.orderByDesc(VehicleData::getTimestamp); + qw.last("LIMIT " + limit); + List rows = vehicleDataMapper.selectList(qw); + if (rows == null || rows.isEmpty()) { + return Collections.emptyList(); + } + List asc = new ArrayList<>(rows); + Collections.reverse(asc); + return asc; + } + /** * 新增整车数据 * diff --git a/src/main/resources/application-druid.yml b/src/main/resources/application-druid.yml index 02a9004..57ed93e 100644 --- a/src/main/resources/application-druid.yml +++ b/src/main/resources/application-druid.yml @@ -6,7 +6,8 @@ spring: druid: # 主库数据源 master: - url: jdbc:mysql://192.168.5.121:3306/evo_bms1?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=Asia/Shanghai + # tcpKeepAlive 减轻 NAT/防火墙长时间空闲断 TCP;配合 testOnBorrow 避免用到已被服务端关闭的池内连接 + url: jdbc:mysql://192.168.5.121:3306/evo_bms1?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=Asia/Shanghai&tcpKeepAlive=true username: root password: root # 从库数据源 @@ -34,10 +35,11 @@ spring: minEvictableIdleTimeMillis: 300000 # 配置一个连接在池中最大生存的时间,单位是毫秒 maxEvictableIdleTimeMillis: 900000 - # 配置检测连接是否有效 - validationQuery: SELECT 1 FROM DUAL + # 配置检测连接是否有效(MySQL 可用 SELECT 1) + validationQuery: SELECT 1 testWhileIdle: true - testOnBorrow: false + # 借连接前校验,避免 MySQL wait_timeout / 网络断开后仍使用池中死连接(略增开销) + testOnBorrow: true testOnReturn: false webStatFilter: enabled: true diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 4d85b7c..8e0dbd2 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -156,6 +156,13 @@ swagger: # 请求前缀 pathMapping: /prod-api +# OTA配置(设备固件上传/分片下载) +ota: + count: ${OTA_COUNT:001} + repo: ${OTA_REPO:./ota} + chunk-size: ${OTA_CHUNK_SIZE:1024} + header-format: ${OTA_HEADER_FORMAT:binary32} + # 防盗链配置 referer: # 防盗链开关 @@ -189,8 +196,12 @@ gen: mqtt: # 是否启用MQTT功能(设置为false可在没有MQTT服务器时正常启动) enabled: true + legacyReceiver: + enabled: false + publicHost: ${MQTT_PUBLIC_HOST:61.182.73.218} + publicPort: ${MQTT_PUBLIC_PORT:11883} # MQTT服务器地址 - host: ${MQTT_HOST_URI:tcp://61.182.73.218:1883} + host: ${MQTT_HOST_URI:tcp://61.182.73.218:11883} # 客户端ID(避免冲突,随机后缀) clientId: evobms-client-${random.int} # 用户名(如需认证请填写) @@ -199,6 +210,8 @@ mqtt: password: ${MQTT_PASSWORD:741a03a10f3de6b2} # 连接超时时间(秒) connectionTimeout: 30 + # 订阅/连接完成等待时间(毫秒) + completionTimeout: 20000 # 心跳间隔(秒) keepAliveInterval: 60 # 是否自动重连 diff --git a/src/main/resources/mybatis/bms/BmsChargeTemperatureMapper.xml b/src/main/resources/mybatis/bms/BmsChargeTemperatureMapper.xml index f94be85..3688af3 100644 --- a/src/main/resources/mybatis/bms/BmsChargeTemperatureMapper.xml +++ b/src/main/resources/mybatis/bms/BmsChargeTemperatureMapper.xml @@ -7,7 +7,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - + @@ -17,14 +17,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - select id, device_id, timestap, temp_index, temperature, create_by, update_by, create_time, update_time from bms_charge_temperature + select id, device_id, `timestamp`, temp_index, temperature, create_by, update_by, create_time, update_time from bms_charge_temperature and device_id = #{deviceId} - and timestap = #{timestap} + and `timestamp` = #{timestamp} and vehicle_speed = #{vehicleSpeed} and mileage = #{mileage} and system_voltage = #{systemVoltage} @@ -55,7 +55,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" insert into bms_runtime_data device_id, - timestap, + `timestamp`, vehicle_speed, mileage, system_voltage, @@ -73,7 +73,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" #{deviceId}, - #{timestap}, + #{timestamp}, #{vehicleSpeed}, #{mileage}, #{systemVoltage}, @@ -95,7 +95,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" update bms_runtime_data device_id = #{deviceId}, - timestap = #{timestap}, + `timestamp` = #{timestamp}, vehicle_speed = #{vehicleSpeed}, mileage = #{mileage}, system_voltage = #{systemVoltage}, diff --git a/src/main/resources/mybatis/bms/BmsSystemStatusMapper.xml b/src/main/resources/mybatis/bms/BmsSystemStatusMapper.xml index 7f92049..173d7b6 100644 --- a/src/main/resources/mybatis/bms/BmsSystemStatusMapper.xml +++ b/src/main/resources/mybatis/bms/BmsSystemStatusMapper.xml @@ -25,7 +25,7 @@ insert into bms_system_status device_id, - timestamp, + `timestamp`, relay_status, onaacc_status, reset_count, diff --git a/src/main/resources/mybatis/bms/BmsVoltageChannelDataMapper.xml b/src/main/resources/mybatis/bms/BmsVoltageChannelDataMapper.xml index 97bc7a5..9f6520e 100644 --- a/src/main/resources/mybatis/bms/BmsVoltageChannelDataMapper.xml +++ b/src/main/resources/mybatis/bms/BmsVoltageChannelDataMapper.xml @@ -7,7 +7,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - + @@ -18,14 +18,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - select id, device_id, timestap, channel_type, channel_index, voltage, create_by, update_by, create_time, update_time from bms_voltage_channel_data + select id, device_id, `timestamp`, channel_type, channel_index, voltage, create_by, update_by, create_time, update_time from bms_voltage_channel_data + + id, device_id, timestamp, vehicle_status, charging_status, operation_mode, vehicle_speed, + total_mileage, total_voltage, total_current, soc, dcdc_status, gear_position, + insulation_resistance, create_time + + + +