车辆数据、OTA、IoT 及 BMS 功能更新

以本地开发版本为准,包含仪表盘、设备实时数据、OTA 升级等改动。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
tzy 2026-06-26 09:37:36 +08:00
parent 257bc96045
commit 88021b2912
63 changed files with 3313 additions and 261 deletions

View File

@ -0,0 +1,8 @@
{
"hash": "764c59fb",
"configHash": "64491d45",
"lockfileHash": "e3b0c442",
"browserHash": "4636522a",
"optimized": {},
"chunks": {}
}

View File

@ -1,3 +1,4 @@
{ {
"java.compile.nullAnalysis.mode": "automatic" "java.compile.nullAnalysis.mode": "automatic",
"java.configuration.updateBuildConfiguration": "interactive"
} }

138
ota/1.1.1.bin Normal file
View File

@ -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);
}
}
```

138
ota/2.2.0.bin Normal file
View File

@ -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);
}
}
```

24
pom.xml
View File

@ -22,7 +22,7 @@
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version> <java.version>16</java.version>
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version> <maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
<pagehelper.spring.boot.starter.version>1.4.7</pagehelper.spring.boot.starter.version> <pagehelper.spring.boot.starter.version>1.4.7</pagehelper.spring.boot.starter.version>
<fastjson.version>2.0.58</fastjson.version> <fastjson.version>2.0.58</fastjson.version>
@ -30,6 +30,7 @@
<commons.io.version>2.19.0</commons.io.version> <commons.io.version>2.19.0</commons.io.version>
<bitwalker.version>1.21</bitwalker.version> <bitwalker.version>1.21</bitwalker.version>
<jwt.version>0.9.1</jwt.version> <jwt.version>0.9.1</jwt.version>
<mysql.version>8.0.33</mysql.version>
<kaptcha.version>2.3.3</kaptcha.version> <kaptcha.version>2.3.3</kaptcha.version>
<swagger.version>3.0.0</swagger.version> <swagger.version>3.0.0</swagger.version>
<poi.version>4.1.2</poi.version> <poi.version>4.1.2</poi.version>
@ -44,6 +45,12 @@
<dependencies> <dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- SpringBoot 核心包 --> <!-- SpringBoot 核心包 -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
@ -102,8 +109,9 @@
<!-- Mysql驱动包 --> <!-- Mysql驱动包 -->
<dependency> <dependency>
<groupId>mysql</groupId> <groupId>com.mysql</groupId>
<artifactId>mysql-connector-java</artifactId> <artifactId>mysql-connector-j</artifactId>
<version>${mysql.version}</version>
<!-- <scope>runtime</scope>--> <!-- <scope>runtime</scope>-->
</dependency> </dependency>
@ -276,12 +284,6 @@
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
</dependencies> </dependencies>
<build> <build>
@ -298,8 +300,8 @@
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
<configuration> <configuration>
<source>16</source> <source>${java.version}</source>
<target>16</target> <target>${java.version}</target>
</configuration> </configuration>
</plugin> </plugin>
</plugins> </plugins>

View File

@ -15,6 +15,7 @@ public final class BboxApiConstants {
public static final int CODE_DEVICE_NOT_REGISTERED = 901; // 设备未在平台注册如平台未录入 public static final int CODE_DEVICE_NOT_REGISTERED = 901; // 设备未在平台注册如平台未录入
public static final int CODE_DEVICE_DISABLED = 902; // 设备已被禁用 public static final int CODE_DEVICE_DISABLED = 902; // 设备已被禁用
public static final int CODE_FILE_NOT_FOUND = 903; // 文件不存在 public static final int CODE_FILE_NOT_FOUND = 903; // 文件不存在
public static final int CODE_PARAM_INVALID = 904; // 参数无效/缺失
// 对应消息文本可用于统一提示 // 对应消息文本可用于统一提示
public static final String MSG_SUCCESS = "成功"; 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_NOT_REGISTERED = "设备未在平台注册,如平台未录入";
public static final String MSG_DEVICE_DISABLED = "设备已被禁用"; public static final String MSG_DEVICE_DISABLED = "设备已被禁用";
public static final String MSG_FILE_NOT_FOUND = "文件不存在"; public static final String MSG_FILE_NOT_FOUND = "文件不存在";
} public static final String MSG_PARAM_INVALID = "参数无效";
}

View File

@ -12,17 +12,61 @@ import com.evobms.framework.web.page.TableSupport;
*/ */
public class PageUtils extends PageHelper public class PageUtils extends PageHelper
{ {
/** 设备历史平铺列表允许的每页条数 */
public static final int[] HISTORY_PAGE_SIZES = {10, 20, 100, 500};
/** /**
* 设置请求分页数据 * 设置请求分页数据
*/ */
public static void startPage() public static void startPage()
{
startPage(true);
}
/**
* @param doCount 是否执行 COUNT大数据量历史列表可传 false 避免扫行计数超时
*/
public static void startPage(boolean doCount)
{ {
PageDomain pageDomain = TableSupport.buildPageRequest(); PageDomain pageDomain = TableSupport.buildPageRequest();
Integer pageNum = pageDomain.getPageNum(); Integer pageNum = pageDomain.getPageNum();
Integer pageSize = pageDomain.getPageSize(); Integer pageSize = pageDomain.getPageSize();
String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy()); String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy());
Boolean reasonable = pageDomain.getReasonable(); 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;
} }
/** /**

View File

@ -1,13 +1,14 @@
package com.evobms.framework.aspectj.lang.annotation; 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.ElementType;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import java.math.BigDecimal; 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数据注解 * 自定义导出Excel数据注解

View File

@ -10,7 +10,9 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.integration.annotation.ServiceActivator; import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.channel.DirectChannel; import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.channel.ExecutorChannel;
import org.springframework.integration.core.MessageProducer; import org.springframework.integration.core.MessageProducer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.integration.handler.AbstractMessageHandler; import org.springframework.integration.handler.AbstractMessageHandler;
import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory; import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory;
import org.springframework.integration.mqtt.core.MqttPahoClientFactory; import org.springframework.integration.mqtt.core.MqttPahoClientFactory;
@ -60,6 +62,9 @@ public class MqttConfig {
@Value("${mqtt.cleanSession}") @Value("${mqtt.cleanSession}")
private boolean cleanSession; private boolean cleanSession;
@Value("${mqtt.completionTimeout:20000}")
private int completionTimeout;
// 支持逗号分隔的多个主题例如/bbox/EVO0002/#,/bbox/000010000200003/# // 支持逗号分隔的多个主题例如/bbox/EVO0002/#,/bbox/000010000200003/#
@Value("#{'${mqtt.subscribeTopic}'.split(',')}") @Value("#{'${mqtt.subscribeTopic}'.split(',')}")
private String[] subscribeTopics; private String[] subscribeTopics;
@ -87,11 +92,17 @@ public class MqttConfig {
} }
/** /**
* MQTT消息接收通道 * MQTT消息接收通道 (异步处理防止入库慢阻塞MQTT底层网络线程导致掉线)
*/ */
@Bean @Bean
public MessageChannel mqttInputChannel() { 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 = MqttPahoMessageDrivenChannelAdapter adapter =
new MqttPahoMessageDrivenChannelAdapter(clientId + "_inbound", mqttClientFactory(), topics); new MqttPahoMessageDrivenChannelAdapter(clientId + "_inbound", mqttClientFactory(), topics);
adapter.setCompletionTimeout(5000); adapter.setCompletionTimeout(completionTimeout);
// 使用字节载荷转换器保留原始二进制帧 // 使用字节载荷转换器保留原始二进制帧
DefaultPahoMessageConverter converter = new DefaultPahoMessageConverter(); DefaultPahoMessageConverter converter = new DefaultPahoMessageConverter();
converter.setPayloadAsBytes(true); converter.setPayloadAsBytes(true);

View File

@ -1,6 +1,7 @@
package com.evobms.project.battery.service; package com.evobms.project.battery.service;
import java.util.List; import java.util.List;
import com.baomidou.mybatisplus.extension.service.IService;
import com.evobms.project.battery.domain.SubsystemTemperature; import com.evobms.project.battery.domain.SubsystemTemperature;
/** /**
@ -9,7 +10,7 @@ import com.evobms.project.battery.domain.SubsystemTemperature;
* @author 田志阳 * @author 田志阳
* @date 2025-11-15 * @date 2025-11-15
*/ */
public interface ISubsystemTemperatureService public interface ISubsystemTemperatureService extends IService<SubsystemTemperature>
{ {
/** /**
* 查询可充电储能子系统温度信息 * 查询可充电储能子系统温度信息

View File

@ -1,6 +1,7 @@
package com.evobms.project.battery.service; package com.evobms.project.battery.service;
import java.util.List; import java.util.List;
import com.baomidou.mybatisplus.extension.service.IService;
import com.evobms.project.battery.domain.SubsystemVoltage; import com.evobms.project.battery.domain.SubsystemVoltage;
/** /**
@ -9,7 +10,7 @@ import com.evobms.project.battery.domain.SubsystemVoltage;
* @author 田志阳 * @author 田志阳
* @date 2025-11-15 * @date 2025-11-15
*/ */
public interface ISubsystemVoltageService public interface ISubsystemVoltageService extends IService<SubsystemVoltage>
{ {
/** /**
* 查询可充电储能子系统电压信息 * 查询可充电储能子系统电压信息

View File

@ -8,7 +8,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@Configuration @Configuration
@ConditionalOnProperty(name = "mqtt.enabled", havingValue = "true") @ConditionalOnProperty(name = "mqtt.legacyReceiver.enabled", havingValue = "true")
public class GBT32960MqttReceiver { public class GBT32960MqttReceiver {
@Value("${mqtt.host}") @Value("${mqtt.host}")
private String broker; private String broker;

View File

@ -4,6 +4,7 @@ import java.math.BigDecimal;
import java.util.Date; import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import com.github.xiaoymin.knife4j.annotations.Ignore; import com.github.xiaoymin.knife4j.annotations.Ignore;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringBuilder;
@ -32,55 +33,68 @@ public class BmsChargeDetail extends BaseEntity
/** 数据时间戳 */ /** 数据时间戳 */
@JsonFormat(pattern = "yyyy-MM-dd") @JsonFormat(pattern = "yyyy-MM-dd")
@ApiModelProperty(value = "数据时间戳")
@Excel(name = "数据时间戳", width = 30, dateFormat = "yyyy-MM-dd") @Excel(name = "数据时间戳", width = 30, dateFormat = "yyyy-MM-dd")
private Date timestamp; private Date timestamp;
/** 慢充电流(A) */ /** 慢充电流(A) */
@Excel(name = "慢充电流(A)") @Excel(name = "慢充电流(A)")
@ApiModelProperty(value = "慢充电流(A)")
private BigDecimal acChargeCurrent; private BigDecimal acChargeCurrent;
/** 慢充电压(V) */ /** 慢充电压(V) */
@Excel(name = "慢充电压(V)") @Excel(name = "慢充电压(V)")
@ApiModelProperty(value = "慢充电压(V)")
private BigDecimal acChargeVoltage; private BigDecimal acChargeVoltage;
/** 快充电流(A) */ /** 快充电流(A) */
@Excel(name = "快充电流(A)") @Excel(name = "快充电流(A)")
@ApiModelProperty(value = "快充电流(A)")
private BigDecimal dcChargeCurrent; private BigDecimal dcChargeCurrent;
/** 快充电压(V) */ /** 快充电压(V) */
@Excel(name = "快充电压(V)") @Excel(name = "快充电压(V)")
@ApiModelProperty(value = "快充电压(V)")
private BigDecimal dcChargeVoltage; private BigDecimal dcChargeVoltage;
/** 充电时长(min) */ /** 充电时长(min) */
@Excel(name = "充电时长(min)") @Excel(name = "充电时长(min)")
@ApiModelProperty(value = "充电时长min")
private Long chargeTime; private Long chargeTime;
/** 慢充停止码 */ /** 慢充停止码 */
@Excel(name = "慢充停止码") @Excel(name = "慢充停止码")
@ApiModelProperty(value = "慢充停止码")
private Integer acStopCode; private Integer acStopCode;
/** 快充停止码 */ /** 快充停止码 */
@Excel(name = "快充停止码") @Excel(name = "快充停止码")
@ApiModelProperty(value = "快充停止码")
private Integer dcStopCode; private Integer dcStopCode;
/** 慢充CP值 */ /** 慢充CP值 */
@Excel(name = "慢充CP值") @Excel(name = "慢充CP值")
@ApiModelProperty(value = "慢充CP值")
private Integer acCpValue; private Integer acCpValue;
/** 慢充枪阻值 */ /** 慢充枪阻值 */
@Excel(name = "慢充枪阻值") @Excel(name = "慢充枪阻值")
@ApiModelProperty(value = "慢充枪阻值")
private Integer acGunResistance; private Integer acGunResistance;
/** 充电阶段 */ /** 充电阶段 */
@Excel(name = "充电阶段") @Excel(name = "充电阶段")
@ApiModelProperty(value = "充电阶段")
private Integer chargingStage; private Integer chargingStage;
/** 慢充阶段 */ /** 慢充阶段 */
@Excel(name = "慢充阶段") @Excel(name = "慢充阶段")
@ApiModelProperty(value = "慢充阶段")
private Integer acStage; private Integer acStage;
/** 快充阶段 */ /** 快充阶段 */
@Excel(name = "快充阶段") @Excel(name = "快充阶段")
@ApiModelProperty(value = "快充阶段")
private Integer dcStage; private Integer dcStage;

View File

@ -4,6 +4,7 @@ import java.io.Serial;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Date; import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringBuilder;
@ -34,14 +35,17 @@ public class BmsChargeTemperature extends BaseEntity
/** 数据时间戳 */ /** 数据时间戳 */
@JsonFormat(pattern = "yyyy-MM-dd") @JsonFormat(pattern = "yyyy-MM-dd")
@Excel(name = "数据时间戳", width = 30, dateFormat = "yyyy-MM-dd") @Excel(name = "数据时间戳", width = 30, dateFormat = "yyyy-MM-dd")
private Date timestap; @ApiModelProperty(value = "数据时间戳")
private Date timestamp;
/** 温度编号(1-8) */ /** 温度编号(1-8) */
@Excel(name = "温度编号(1-8)") @Excel(name = "温度编号(1-8)")
@ApiModelProperty(value = "温度编号(1-8)")
private Integer tempIndex; private Integer tempIndex;
/** 温度(℃) */ /** 温度(℃) */
@Excel(name = "温度(℃)") @Excel(name = "温度(℃)")
@ApiModelProperty(value = "温度(℃)")
private BigDecimal temperature; private BigDecimal temperature;
} }

View File

@ -1,8 +1,10 @@
package com.evobms.project.bms.domain; package com.evobms.project.bms.domain;
import java.io.Serial;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Date; import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringBuilder;
@ -20,6 +22,7 @@ import com.evobms.framework.web.domain.BaseEntity;
@EqualsAndHashCode(callSuper = false) @EqualsAndHashCode(callSuper = false)
public class BmsRuntimeData extends BaseEntity public class BmsRuntimeData extends BaseEntity
{ {
@Serial
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/** 主键ID */ /** 主键ID */
@ -31,47 +34,58 @@ public class BmsRuntimeData extends BaseEntity
/** 数据时间戳 */ /** 数据时间戳 */
@JsonFormat(pattern = "yyyy-MM-dd") @JsonFormat(pattern = "yyyy-MM-dd")
@ApiModelProperty(value = "数据时间戳")
@Excel(name = "数据时间戳", width = 30, dateFormat = "yyyy-MM-dd") @Excel(name = "数据时间戳", width = 30, dateFormat = "yyyy-MM-dd")
private Date timestap; private Date timestamp;
/** 车速(km/h) */ /** 车速(km/h) */
@Excel(name = "车速(km/h)") @Excel(name = "车速(km/h)")
@ApiModelProperty(value = "车速(km/h)")
private BigDecimal vehicleSpeed; private BigDecimal vehicleSpeed;
/** 总里程(km) */ /** 总里程(km) */
@Excel(name = "总里程(km)") @Excel(name = "总里程(km)")
@ApiModelProperty(value = "总里程(km)")
private Long mileage; private Long mileage;
/** 系统电压(V) */ /** 系统电压(V) */
@Excel(name = "系统电压(V)") @Excel(name = "系统电压(V)")
@ApiModelProperty(value = "系统电压(V)")
private BigDecimal systemVoltage; private BigDecimal systemVoltage;
/** 系统电流(A) */ /** 系统电流(A) */
@Excel(name = "系统电流(A)") @Excel(name = "系统电流(A)")
@ApiModelProperty(value = "系统电流(A)")
private BigDecimal systemCurrent; private BigDecimal systemCurrent;
/** 绝缘电阻(KΩ) */ /** 绝缘电阻(KΩ) */
@Excel(name = "绝缘电阻(KΩ)") @Excel(name = "绝缘电阻(KΩ)")
@ApiModelProperty(value = "绝缘电阻(KΩ)")
private Long insulationResistance; private Long insulationResistance;
/** 最大SOC(%) */ /** 最大SOC(%) */
@Excel(name = "最大SOC(%)") @Excel(name = "最大SOC(%)")
@ApiModelProperty(value = "最大SOC(%)")
private BigDecimal socMax; private BigDecimal socMax;
/** 最小SOC(%) */ /** 最小SOC(%) */
@Excel(name = "最小SOC(%)") @Excel(name = "最小SOC(%)")
@ApiModelProperty(value = "最小SOC(%)")
private BigDecimal socMin; private BigDecimal socMin;
/** GPV电压(V) */ /** GPV电压(V) */
@Excel(name = "GPV电压(V)") @Excel(name = "GPV电压(V)")
@ApiModelProperty(value = "GPV电压(V)")
private BigDecimal gpvVoltage; private BigDecimal gpvVoltage;
/** 行车阶段 */ /** 行车阶段 */
@Excel(name = "行车阶段") @Excel(name = "行车阶段")
@ApiModelProperty(value = "行车阶段")
private Integer runStatus; private Integer runStatus;
/** 行车上电阶段 */ /** 行车上电阶段 */
@Excel(name = "行车上电阶段") @Excel(name = "行车上电阶段")
@ApiModelProperty(value = "行车上电阶段")
private Integer powerStatus; private Integer powerStatus;
} }

View File

@ -2,6 +2,8 @@ package com.evobms.project.bms.domain;
import java.util.Date; import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import com.baomidou.mybatisplus.annotation.TableField;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringBuilder;
@ -30,55 +32,79 @@ public class BmsSopData extends BaseEntity
/** 数据时间戳 */ /** 数据时间戳 */
@JsonFormat(pattern = "yyyy-MM-dd") @JsonFormat(pattern = "yyyy-MM-dd")
@ApiModelProperty(value = "数据时间戳")
@Excel(name = "数据时间戳", width = 30, dateFormat = "yyyy-MM-dd") @Excel(name = "数据时间戳", width = 30, dateFormat = "yyyy-MM-dd")
private Date timestap; @TableField("`timestamp`")
private Date timestamp;
/** 10秒放电SOP(A) */ /** 10秒放电SOP(A) */
@Excel(name = "10秒放电SOP(A)") @Excel(name = "10秒放电SOP(A)")
@ApiModelProperty(value = "10秒放电SOP(A)")
@TableField("discharge_sop_10s")
private Long dischargeSop10s; private Long dischargeSop10s;
/** 30秒放电SOP(A) */ /** 30秒放电SOP(A) */
@Excel(name = "30秒放电SOP(A)") @Excel(name = "30秒放电SOP(A)")
@ApiModelProperty(value = "30秒放电SOP(A)")
@TableField("discharge_sop_30s")
private Long dischargeSop30s; private Long dischargeSop30s;
/** 60秒放电SOP(A) */ /** 60秒放电SOP(A) */
@Excel(name = "60秒放电SOP(A)") @Excel(name = "60秒放电SOP(A)")
@ApiModelProperty(value = "60秒放电SOP(A)")
@TableField("discharge_sop_60s")
private Long dischargeSop60s; private Long dischargeSop60s;
/** 持续放电SOP(A) */ /** 持续放电SOP(A) */
@Excel(name = "持续放电SOP(A)") @Excel(name = "持续放电SOP(A)")
@ApiModelProperty(value = "持续放电SOP(A)")
@TableField("discharge_sop_continuous")
private Long dischargeSopContinuous; private Long dischargeSopContinuous;
/** 放电融合SOP(A) */ /** 放电融合SOP(A) */
@Excel(name = "放电融合SOP(A)") @Excel(name = "放电融合SOP(A)")
@ApiModelProperty(value = "放电融合SOP(A)")
private Long dischargeSopTotal; private Long dischargeSopTotal;
/** 10秒回馈SOP(A) */ /** 10秒回馈SOP(A) */
@Excel(name = "10秒回馈SOP(A)") @Excel(name = "10秒回馈SOP(A)")
@ApiModelProperty(value = "10秒回馈SOP(A)")
@TableField("regen_sop_10s")
private Long regenSop10s; private Long regenSop10s;
/** 30秒回馈SOP(A) */ /** 30秒回馈SOP(A) */
@Excel(name = "30秒回馈SOP(A)") @Excel(name = "30秒回馈SOP(A)")
@ApiModelProperty(value = "30秒回馈SOP(A)")
@TableField("regen_sop_30s")
private Long regenSop30s; private Long regenSop30s;
/** 60秒回馈SOP(A) */ /** 60秒回馈SOP(A) */
@Excel(name = "60秒回馈SOP(A)") @Excel(name = "60秒回馈SOP(A)")
@ApiModelProperty(value = "60秒回馈SOP(A)")
@TableField("regen_sop_60s")
private Long regenSop60s; private Long regenSop60s;
/** 持续回馈SOP(A) */ /** 持续回馈SOP(A) */
@Excel(name = "持续回馈SOP(A)") @Excel(name = "持续回馈SOP(A)")
@ApiModelProperty(value = "持续回馈SOP(A)")
@TableField("regen_sop_continuous")
private Long regenSopContinuous; private Long regenSopContinuous;
/** 回馈融合SOP(A) */ /** 回馈融合SOP(A) */
@Excel(name = "回馈融合SOP(A)") @Excel(name = "回馈融合SOP(A)")
@ApiModelProperty(value = "回馈融合SOP(A)")
private Long regenSopTotal; private Long regenSopTotal;
/** 充电SOP(A) */ /** 充电SOP(A) */
@Excel(name = "充电SOP(A)") @Excel(name = "充电SOP(A)")
@ApiModelProperty(value = "充电SOP(A)")
@TableField("charge_sop")
private Long chargeSop; private Long chargeSop;
/** 慢充SOP(A) */ /** 慢充SOP(A) */
@Excel(name = "慢充SOP(A)") @Excel(name = "慢充SOP(A)")
@ApiModelProperty(value = "慢充SOP(A)")
@TableField("ac_charge_sop")
private Long acChargeSop; private Long acChargeSop;

View File

@ -3,6 +3,8 @@ package com.evobms.project.bms.domain;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Date; import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import com.baomidou.mybatisplus.annotation.TableField;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringBuilder;
@ -32,30 +34,49 @@ public class BmsSystemStatus extends BaseEntity
/** 数据时间戳 */ /** 数据时间戳 */
@JsonFormat(pattern = "yyyy-MM-dd") @JsonFormat(pattern = "yyyy-MM-dd")
@Excel(name = "数据时间戳", width = 30, dateFormat = "yyyy-MM-dd") @Excel(name = "数据时间戳", width = 30, dateFormat = "yyyy-MM-dd")
private Date timestap; @ApiModelProperty(value = "数据时间戳")
private Date timestamp;
/** 继电器状态 */ /** 继电器状态 */
@Excel(name = "继电器状态") @Excel(name = "继电器状态")
@ApiModelProperty(value = "继电器状态")
private Long relayStatus; private Long relayStatus;
/** ON/A+/CC状态 */ /** ON/A+/CC状态 */
@Excel(name = "ON/A+/CC状态") @Excel(name = "ON/A+/CC状态")
@ApiModelProperty(value = "ON/A+/CC状态")
private Long onaaccStatus; private Long onaaccStatus;
/** 系统复位次数 */ /** 系统复位次数 */
@Excel(name = "系统复位次数") @Excel(name = "系统复位次数")
@ApiModelProperty(value = "系统复位次数")
private Long resetCount; private Long resetCount;
/** 电池循环次数 */ /** 电池循环次数 */
@Excel(name = "电池循环次数") @Excel(name = "电池循环次数")
@ApiModelProperty(value = "电池循环次数")
private Long cycleCount; private Long cycleCount;
/** 电池循环次数 */
@Excel(name = "电池芯单体数")
@ApiModelProperty(value = "电池芯单体数")
private Long cellCount;
/** 2.5V采样电压(V) */ /** 2.5V采样电压(V) */
@Excel(name = "2.5V采样电压(V)") @Excel(name = "2.5V采样电压(V)")
@ApiModelProperty(value = "2.5V采样电压(V)")
@TableField("sample_25v")
private BigDecimal sample25v; private BigDecimal sample25v;
/** 5V采样电压(V) */ /** 5V采样电压(V) */
@Excel(name = "5V采样电压(V)") @Excel(name = "5V采样电压(V)")
@ApiModelProperty(value = "5V采样电压(V)")
@TableField("sample_5v")
private BigDecimal sample5v; private BigDecimal sample5v;
/** 测温点数量(V) */
@Excel(name = "测温点数量(V)")
@ApiModelProperty(value = "测温点数量(V)")
@TableField("temp_sensor_count")
private Integer tempSensorCount;
} }

View File

@ -3,6 +3,7 @@ package com.evobms.project.bms.domain;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Date; import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringBuilder;
@ -32,18 +33,22 @@ public class BmsVoltageChannelData extends BaseEntity
/** 数据时间戳 */ /** 数据时间戳 */
@JsonFormat(pattern = "yyyy-MM-dd") @JsonFormat(pattern = "yyyy-MM-dd")
@Excel(name = "数据时间戳", width = 30, dateFormat = "yyyy-MM-dd") @Excel(name = "数据时间戳", width = 30, dateFormat = "yyyy-MM-dd")
private Date timestap; @ApiModelProperty(value = "数据时间戳")
private Date timestamp;
/** 通道类型(RTP/RTN) */ /** 通道类型(RTP/RTN) */
@Excel(name = "通道类型(RTP/RTN)") @Excel(name = "通道类型(RTP/RTN)")
@ApiModelProperty(value = "通道类型(RTP/RTN)")
private String channelType; private String channelType;
/** 通道编号 */ /** 通道编号 */
@Excel(name = "通道编号") @Excel(name = "通道编号")
@ApiModelProperty(value = "通道编号")
private Integer channelIndex; private Integer channelIndex;
/** 电压(V) */ /** 电压(V) */
@Excel(name = "电压(V)") @Excel(name = "电压(V)")
@ApiModelProperty(value = "电压(V)")
private BigDecimal voltage; private BigDecimal voltage;
} }

View File

@ -289,7 +289,7 @@ public class MqttService {
// 7. 累计充放电量 - 绝对索引543-553(11字节) | 相对偏移: 519-529 // 7. 累计充放电量 - 绝对索引543-553(11字节) | 相对偏移: 519-529
parseChargeDischargeAtIndex(message, 519, deviceSn, frameTime); parseChargeDischargeAtIndex(message, 519, deviceSn, frameTime);
parseExtendedDataAtIndex(message, 506, deviceSn, frameTime); parseExtendedDataAtIndex(message, 530, deviceSn, frameTime);
log.info("设备 {} 所有数据段解析完成", deviceSn); log.info("设备 {} 所有数据段解析完成", deviceSn);
@ -341,40 +341,40 @@ public class MqttService {
private void parseVehicleDataAtIndex(byte[] message, int startIndex, String deviceSn, Date frameTime) { private void parseVehicleDataAtIndex(byte[] message, int startIndex, String deviceSn, Date frameTime) {
try { try {
if (message.length < startIndex + 20) { if (message.length < startIndex + 20) {
log.warn("设备 {}: 整车数据索引越界", deviceSn); /// log.warn("设备 {}: 整车数据索引越界", deviceSn);
return; return;
} }
// 索引30: 数据标识 (应该是0x01) // 索引30: 数据标识 (应该是0x01)
byte dataFlag = message[startIndex]; byte dataFlag = message[startIndex];
if (dataFlag != 0x01) { if (dataFlag != 0x01) {
log.warn("设备 {}: 整车数据标识错误: 0x{}", deviceSn, String.format("%02X", dataFlag)); // log.warn("设备 {}: 整车数据标识错误: 0x{}", deviceSn, String.format("%02X", dataFlag));
return; return;
} }
log.info("设备 {} - 发现整车数据", deviceSn); // log.info("设备 {} - 发现整车数据", deviceSn);
// 索引32: 充电状态 // 索引32: 充电状态
byte chargeStatus = message[startIndex + 2]; 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) // 索引40-41: 总电压 (0.1V)
int totalVoltage = ((message[startIndex + 10] & 0xFF) << 8) | (message[startIndex + 11] & 0xFF); int totalVoltage = ((message[startIndex + 10] & 0xFF) << 8) | (message[startIndex + 11] & 0xFF);
double voltageValue = totalVoltage * 0.1; double voltageValue = totalVoltage * 0.1;
log.info("设备 {} - 总电压: {} V", deviceSn, voltageValue); //log.info("设备 {} - 总电压: {} V", deviceSn, voltageValue);
// 索引42-43: 总电流 (0.1A, 偏移10000) // 索引42-43: 总电流 (0.1A, 偏移10000)
int totalCurrent = ((message[startIndex + 12] & 0xFF) << 8) | (message[startIndex + 13] & 0xFF); int totalCurrent = ((message[startIndex + 12] & 0xFF) << 8) | (message[startIndex + 13] & 0xFF);
double currentValue = (totalCurrent - 10000) * 0.1; double currentValue = (totalCurrent - 10000) * 0.1;
log.info("设备 {} - 总电流: {} A", deviceSn, currentValue); // log.info("设备 {} - 总电流: {} A", deviceSn, currentValue);
// 索引44: SOC (%) // 索引44: SOC (%)
int soc = message[startIndex + 14] & 0xFF; int soc = message[startIndex + 14] & 0xFF;
log.info("设备 {} - SOC: {} %", deviceSn, soc); //log.info("设备 {} - SOC: {} %", deviceSn, soc);
// 索引47-48: 正极绝缘电阻 (Ω) // 索引47-48: 正极绝缘电阻 (Ω)
int insulationResistance = ((message[startIndex + 17] & 0xFF) << 8) | (message[startIndex + 18] & 0xFF); 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); saveVehicleData(deviceSn, voltageValue, currentValue, soc, chargeStatus, insulationResistance, frameTime);
@ -649,7 +649,7 @@ public class MqttService {
return; return;
} }
log.info("设备 {} - 发现累计充放电数据", deviceSn); // log.info("设备 {} - 发现累计充放电数据", deviceSn);
// 跳过索引544-545的保留字节 // 跳过索引544-545的保留字节
// 索引546-549: 累计放电电量 (0.1kWh) // 索引546-549: 累计放电电量 (0.1kWh)
@ -661,7 +661,7 @@ public class MqttService {
double dischargeKwh = dischargeEnergy * 0.1; double dischargeKwh = dischargeEnergy * 0.1;
double chargeKwh = chargeEnergy * 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); saveChargeDischargeSummary(deviceSn, dischargeKwh, chargeKwh, frameTime);
@ -1041,7 +1041,7 @@ public class MqttService {
BmsRuntimeData rd = new BmsRuntimeData(); BmsRuntimeData rd = new BmsRuntimeData();
rd.setDeviceId(deviceSn); rd.setDeviceId(deviceSn);
rd.setTimestap(frameTime); rd.setTimestamp(frameTime);
rd.setVehicleSpeed(BigDecimal.valueOf(carSpd)); rd.setVehicleSpeed(BigDecimal.valueOf(carSpd));
rd.setMileage(mileage); rd.setMileage(mileage);
rd.setSystemVoltage(BigDecimal.valueOf(sysVol)); rd.setSystemVoltage(BigDecimal.valueOf(sysVol));
@ -1058,7 +1058,7 @@ public class MqttService {
BmsSopData sop = new BmsSopData(); BmsSopData sop = new BmsSopData();
sop.setDeviceId(deviceSn); sop.setDeviceId(deviceSn);
sop.setTimestap(frameTime); sop.setTimestamp(frameTime);
sop.setDischargeSop10s(disSop10); sop.setDischargeSop10s(disSop10);
sop.setDischargeSop30s(disSop30); sop.setDischargeSop30s(disSop30);
sop.setDischargeSop60s(disSop60); sop.setDischargeSop60s(disSop60);
@ -1075,7 +1075,7 @@ public class MqttService {
BmsSystemStatus ss = new BmsSystemStatus(); BmsSystemStatus ss = new BmsSystemStatus();
ss.setDeviceId(deviceSn); ss.setDeviceId(deviceSn);
ss.setTimestap(frameTime); ss.setTimestamp(frameTime);
ss.setRelayStatus((long) relayStatus); ss.setRelayStatus((long) relayStatus);
ss.setOnaaccStatus((long) onaaccStatus); ss.setOnaaccStatus((long) onaaccStatus);
ss.setResetCount(resetCount); ss.setResetCount(resetCount);
@ -1093,7 +1093,7 @@ public class MqttService {
for (int i = 0; i < chargeTemps.length; i++) { for (int i = 0; i < chargeTemps.length; i++) {
BmsChargeTemperature ct = new BmsChargeTemperature(); BmsChargeTemperature ct = new BmsChargeTemperature();
ct.setDeviceId(deviceSn); ct.setDeviceId(deviceSn);
ct.setTimestap(frameTime); ct.setTimestamp(frameTime);
ct.setTempIndex(i + 1); ct.setTempIndex(i + 1);
ct.setTemperature(BigDecimal.valueOf(chargeTemps[i])); ct.setTemperature(BigDecimal.valueOf(chargeTemps[i]));
ct.setCreateTime(new Date()); ct.setCreateTime(new Date());
@ -1107,7 +1107,7 @@ public class MqttService {
for (int i = 0; i < rtpVals.length; i++) { for (int i = 0; i < rtpVals.length; i++) {
BmsVoltageChannelData ch = new BmsVoltageChannelData(); BmsVoltageChannelData ch = new BmsVoltageChannelData();
ch.setDeviceId(deviceSn); ch.setDeviceId(deviceSn);
ch.setTimestap(frameTime); ch.setTimestamp(frameTime);
ch.setChannelType("RTP"); ch.setChannelType("RTP");
ch.setChannelIndex(i + 1); ch.setChannelIndex(i + 1);
ch.setVoltage(BigDecimal.valueOf(rtpVals[i])); ch.setVoltage(BigDecimal.valueOf(rtpVals[i]));

View File

@ -9,6 +9,13 @@ import com.evobms.project.iot.service.DeviceTokenService;
import com.evobms.project.system.domain.BmsDevices; import com.evobms.project.system.domain.BmsDevices;
import com.evobms.project.system.service.IBmsDevicesService; import com.evobms.project.system.service.IBmsDevicesService;
import com.evobms.project.ota.service.IOtaTasksService; 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.Api;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
@ -33,6 +40,7 @@ import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URI; import java.net.URI;
@ -42,10 +50,7 @@ import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.util.Arrays; import java.util.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** /**
* 设备鉴权令牌接口 * 设备鉴权令牌接口
@ -57,7 +62,6 @@ import java.util.Map;
@RequestMapping("/iot") @RequestMapping("/iot")
@Slf4j @Slf4j
public class IotAuthController { public class IotAuthController {
private static final Logger log = LoggerFactory.getLogger(IotAuthController.class);
@Autowired(required = false) @Autowired(required = false)
private IBmsDevicesService bmsDevicesService; private IBmsDevicesService bmsDevicesService;
@ -68,9 +72,13 @@ public class IotAuthController {
@Value("${token.header:Authorization}") @Value("${token.header:Authorization}")
private String tokenHeader; private String tokenHeader;
@Value("${mqtt.host:tcp://192.168.5.23:1883}") @Value("${mqtt.host:tcp://61.182.73.218:11883}")
private String mqttHostUri; 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; private String mqttUsername;
@Value("${mqtt.password:741a03a10f3de6b2}") @Value("${mqtt.password:741a03a10f3de6b2}")
private String mqttPassword; private String mqttPassword;
@ -82,7 +90,7 @@ public class IotAuthController {
private String otaCount; private String otaCount;
// OTA文件存储位置支持 classpath 文件系统目录例如classpath:/ota /data/ota // OTA文件存储位置支持 classpath 文件系统目录例如classpath:/ota /data/ota
@Value("${ota.repo:D:/OTA}") @Value("${ota.repo:./ota}")
private String otaRepo; private String otaRepo;
// OTA分片大小字节规范为1KB=1024字节 // OTA分片大小字节规范为1KB=1024字节
@ -187,19 +195,21 @@ public class IotAuthController {
return resp; return resp;
} }
// 解析 MQTT 主机与端口 String host = safeTrim(mqttPublicHost);
String host = "192.168.5.23"; Integer port = mqttPublicPort > 0 ? mqttPublicPort : null;
int port = 1883; if (StringUtils.isEmpty(host) || port == null) {
try { try {
URI uri = new URI(mqttHostUri); URI uri = new URI(mqttHostUri);
if (uri.getHost() != null) { if (StringUtils.isEmpty(host) && uri.getHost() != null && !uri.getHost().isEmpty()) {
host = uri.getHost(); 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 use = fixLengthRight(mqttUsername, 8);
String pass = fixLengthRight(mqttPassword, 16); String pass = fixLengthRight(mqttPassword, 16);
@ -333,7 +343,7 @@ public class IotAuthController {
// 若存在目标版本对应固件可用时动态计算分片总数总长度含 64 字节校验头否则回退配置值 // 若存在目标版本对应固件可用时动态计算分片总数总长度含 64 字节校验头否则回退配置值
Integer dynamicCount = computeOtaChunkCount(targetVersion); Integer dynamicCount = computeOtaChunkCount(targetVersion);
if (dynamicCount != null && dynamicCount > 0) { if (dynamicCount != null && dynamicCount > 0) {
// 转为 3 位字符串左侧补零例如 7 -> "007" // 转为 3 位字符串左侧补零 7 -> "007"
count = fixLengthLeft(String.valueOf(dynamicCount), 3); count = fixLengthLeft(String.valueOf(dynamicCount), 3);
} else { } else {
// 回退使用配置 `ota.count`默认 "001" // 回退使用配置 `ota.count`默认 "001"
@ -450,7 +460,7 @@ public class IotAuthController {
try { try {
int previewLen = Math.min(otaChunkSize, firmwareBytes); int previewLen = Math.min(otaChunkSize, firmwareBytes);
byte[] preview = Arrays.copyOfRange(firmware, 0, previewLen); byte[] preview = Arrays.copyOfRange(firmware, 0, previewLen);
// 变更说明headerSHA256日志打印为首片1024字节数据的摘要而非整包 // headerSHA256日志打印为首片1024字节数据的摘要而非整包
String headerHex = sha256Hex(preview); String headerHex = sha256Hex(preview);
log.info("OTA下载开始: sn={} version={} headerSHA256={} headerMode={} chunkSize={} firmwareBytes={} chunks={}", sn, version, headerHex, "binary32", otaChunkSize, firmwareBytes, chunks); log.info("OTA下载开始: sn={} version={} headerSHA256={} headerMode={} chunkSize={} firmwareBytes={} chunks={}", sn, version, headerHex, "binary32", otaChunkSize, firmwareBytes, chunks);
} catch (Exception ignore) { } catch (Exception ignore) {
@ -481,7 +491,7 @@ public class IotAuthController {
} catch (Exception ignore) { } catch (Exception ignore) {
} }
} else { } else {
// 后续分片从首片实际数据(1024)之后开始切片并在分片前置校验头摘要基于该分片数据 // 从首片实际数据(1024)之后开始切片并在分片前置校验头摘要基于该分片数据
int firstDataLen = Math.min(otaChunkSize, firmwareBytes); int firstDataLen = Math.min(otaChunkSize, firmwareBytes);
int offset = firstDataLen + (idx - 1) * otaChunkSize; int offset = firstDataLen + (idx - 1) * otaChunkSize;
if (offset >= firmwareBytes) { if (offset >= firmwareBytes) {
@ -518,10 +528,60 @@ public class IotAuthController {
log.debug("OTA分片发送: sn={} version={} num={} chunkLen={}", log.debug("OTA分片发送: sn={} version={} num={} chunkLen={}",
sn, version, num, chunk.length); 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<OtaTasks> 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 return ResponseEntity
.ok() .ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM) .contentType(MediaType.APPLICATION_OCTET_STREAM)
.contentLength(chunk.length) .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); .body(chunk);
} catch (Exception e) { } catch (Exception e) {
log.error("OTA分片下载异常: {}", e.getMessage(), 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 指定目录,保存为 <version>.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<String, Object> otaMeta(@RequestParam("version") String version) {
Map<String, Object> 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<OtaTasks> list = otaTasksService.selectOtaTasksList(filter);
List<OtaTasks> 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` 为任务整体 0100`totalChunks`/`chunkSize` 与设备登录 `count`分片下载逻辑对齐
* `downloadedChunks` `progress` `totalChunks` 推算用于可选的分片进度文案与浏览器上传到 /iot/ota/upload 无关
* - 响应示例
* <pre>
* {
* "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"
* }
* </pre>
*/
@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<OtaTasks> list = otaTasksService.selectOtaTasksList(filter);
List<OtaTasks> 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 * 构造 OTA 负载 binary32
* - 格式`[32字节SHA256原始摘要] + [固件]` * - 格式`[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 的十六进制字符串 * 计算 SHA256 的十六进制字符串
* - 输出小写长度 64 ASCII 字符串 * - 输出小写长度 64 ASCII 字符串

View File

@ -52,7 +52,7 @@ public class IotOtaController {
MDC.put("sn", sn); MDC.put("sn", sn);
MDC.put("version", prevVersion); MDC.put("version", prevVersion);
// 规范化状态仅识别 "1" 为成功其余按失败处理仍返回成功表示上报接口调用成功
String normalizedStatus = "0"; String normalizedStatus = "0";
if ("1".equals(statusStr)) { if ("1".equals(statusStr)) {
normalizedStatus = "1"; normalizedStatus = "1";

View File

@ -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<OtaTasks> taskList;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -1,6 +1,9 @@
package com.evobms.project.ota.controller; package com.evobms.project.ota.controller;
import java.util.List; import java.util.List;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired; 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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; 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.annotation.Log;
import com.evobms.framework.aspectj.lang.enums.BusinessType; import com.evobms.framework.aspectj.lang.enums.BusinessType;
import com.evobms.project.ota.domain.OtaTasks; 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.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.controller.BaseController;
import com.evobms.framework.web.domain.AjaxResult; import com.evobms.framework.web.domain.AjaxResult;
import com.evobms.common.utils.poi.ExcelUtil; import com.evobms.common.utils.poi.ExcelUtil;
@ -37,6 +44,9 @@ public class OtaTasksController extends BaseController
@Autowired @Autowired
private IOtaTasksService otaTasksService; private IOtaTasksService otaTasksService;
@Autowired(required = false)
private IBmsDevicesService bmsDevicesService;
/** /**
* 查询OTA任务列表 * 查询OTA任务列表
*/ */
@ -50,6 +60,44 @@ public class OtaTasksController extends BaseController
return getDataTable(list); 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<OtaTasks> taskList = otaTasksService.selectOtaTasksList(query);
List<OtaTasks> 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任务列表 * 导出OTA任务列表
*/ */
@ -109,4 +157,21 @@ public class OtaTasksController extends BaseController
{ {
return toAjax(otaTasksService.deleteOtaTasksByIds(ids)); 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();
}
} }

View File

@ -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<OtaTasks> taskList;
}

View File

@ -1,29 +1,35 @@
package com.evobms.project.system.controller; 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 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.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.*;
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 com.evobms.framework.aspectj.lang.annotation.Log; import com.evobms.framework.aspectj.lang.annotation.Log;
import com.evobms.framework.aspectj.lang.enums.BusinessType; import com.evobms.framework.aspectj.lang.enums.BusinessType;
import com.evobms.project.system.domain.BmsDevices; import com.evobms.project.system.domain.BmsDevices;
import com.evobms.project.system.service.IBmsDevicesService; import com.evobms.project.system.service.IBmsDevicesService;
import com.evobms.framework.web.controller.BaseController; import com.evobms.framework.web.controller.BaseController;
import com.evobms.framework.web.domain.AjaxResult; 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.common.utils.poi.ExcelUtil;
import com.evobms.framework.web.page.TableDataInfo; import com.evobms.framework.web.page.TableDataInfo;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 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.DeviceRealtimeDTO;
import com.evobms.project.vehicledata.dto.DeviceRealtimeAggregateDTO;
import com.evobms.project.vehicledata.dto.NameValueDTO; import com.evobms.project.vehicledata.dto.NameValueDTO;
import com.evobms.project.vehicledata.dto.DeviceRealtimeDTO.SystemStatusSection; import com.evobms.project.vehicledata.dto.DeviceRealtimeDTO.SystemStatusSection;
import com.evobms.project.vehicledata.dto.DeviceRealtimeDTO.RunStatusSection; 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.DeviceRealtimeDTO.StatisticsSection;
import com.evobms.project.vehicledata.dto.LineSeriesDTO; import com.evobms.project.vehicledata.dto.LineSeriesDTO;
import com.evobms.project.vehicledata.dto.TimePointDTO; 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.service.IVehicleDataService;
import com.evobms.project.vehicledata.domain.VehicleData; import com.evobms.project.vehicledata.domain.VehicleData;
import com.evobms.project.battery.service.IExtremeValuesService; 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.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.service.*;
import com.evobms.project.bms.domain.*; import com.evobms.project.bms.domain.*;
import java.util.ArrayList;
import java.util.Date;
import java.util.stream.Collectors;
/** /**
* BBOX管理Controller * BBOX管理Controller
@ -55,6 +66,18 @@ public class BmsDevicesController extends BaseController
@Autowired @Autowired
private IBmsDevicesService bmsDevicesService; 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管理列表 * 查询BBOX管理列表
*/ */
@ -68,6 +91,124 @@ public class BmsDevicesController extends BaseController
return getDataTable(list); return getDataTable(list);
} }
@GetMapping("/history/{deviceId}")
@ApiOperation(value = "运行历史计算器(充/放电统计)", response = ChargeDischargeHistoryDTO.class)
public R<ChargeDischargeHistoryDTO> 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<String, Object> params = new HashMap<>();
params.put("beginTime", df.format(start));
params.put("endTime", df.format(end));
q.setDeviceId(deviceId);
q.setParams(params);
List<VehicleData> all = vehicleDataService.selectVehicleDataList(q);
all.removeIf(v -> v.getTimestamp() == null);
all.sort(Comparator.comparing(VehicleData::getTimestamp));
List<ChargeDischargeRecordDTO> 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<String, java.math.BigDecimal[]> 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<ChargeDischargeMonthDTO> months = new java.util.ArrayList<>();
java.util.List<String> 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管理列表 * 导出BBOX管理列表
*/ */
@ -93,165 +234,118 @@ public class BmsDevicesController extends BaseController
return success(bmsDevicesService.selectBmsDevicesById(id)); return success(bmsDevicesService.selectBmsDevicesById(id));
} }
@Autowired private IVehicleDataService vehicleDataService;
@Autowired private IExtremeValuesService extremeValuesService; /** 子系统去重预取行数(按 timestamp 倒序,一般远小于子系统数×上报次数) */
@Autowired private IBmsSystemStatusService bmsSystemStatusService; private static final int REALTIME_SUBSYSTEM_PREFETCH = 32;
@Autowired private IBmsChargeDetailService bmsChargeDetailService; /** 24h 趋势曲线最多点数 */
@Autowired private IBmsChargeTemperatureService bmsChargeTemperatureService; private static final int REALTIME_TREND_MAX_POINTS = 360;
@Autowired private IBmsVoltageChannelDataService bmsVoltageChannelDataService;
@Autowired private IBmsSopDataService bmsSopDataService;
@GetMapping("/realtime/{deviceId}") @GetMapping("/realtime/{deviceId}")
@ApiOperation("设备实时数据") @ApiOperation(value = "设备实时数据", notes = "默认不含 dashboard 趋势(24h),避免超时;趋势请调 includeTrends=true 或 GET .../trends", response = DeviceRealtimeAggregateDTO.class)
public AjaxResult realtime(@PathVariable("deviceId") String deviceId) { public R<DeviceRealtimeAggregateDTO> realtime(@PathVariable("deviceId") String deviceId, @ApiParam("是否包含 dashboard 24h 趋势(较慢,易触发前端 10s 超时)") @RequestParam(value = "includeTrends", required = false, defaultValue = "true") boolean includeTrends) {
DeviceRealtimeDTO dto = new DeviceRealtimeDTO(); RealtimeSnapshot snap = loadRealtimeSnapshot(deviceId);
dto.setDeviceId(deviceId); DeviceRealtimeAggregateDTO agg = new DeviceRealtimeAggregateDTO();
VehicleData vd = vehicleDataService.selectLatestByDeviceId(deviceId); agg.setDeviceId(deviceId);
ExtremeValues ev = extremeValuesService.selectLatestByDeviceId(deviceId); fillAggregateFromSnapshot(agg, snap);
SystemStatusSection sys = new SystemStatusSection(); if (includeTrends) {
if (vd != null) { SimpleDateFormat tf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sys.setTotalVoltage(vd.getTotalVoltage()); Date now = new Date();
sys.setTotalCurrent(vd.getTotalCurrent()); Date trendBegin = new Date(now.getTime() - 24L * 3600_000L);
sys.setSoc(vd.getSoc()); agg.setDashboard(buildRealtimeDashboard(deviceId, snap.vehicleData, snap.extremeValues, snap.systemStatus, snap.chargeDetail, snap.chargeTemperatures, snap.voltageChannels, trendBegin, now, tf));
} }
if (ev != null) { return R.ok(agg);
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);
RunStatusSection run = new RunStatusSection(); @GetMapping("/realtime/{deviceId}/trends")
if (vd != null) { @ApiOperation(value = "设备实时 24h 趋势(dashboard)", notes = "与 realtime 拆分,避免与快照同请求导致 Axios 10s 超时", response = DeviceRealtimeDTO.class)
run.setChargingStatus(vd.getChargingStatus()); public R<DeviceRealtimeDTO> realtimeTrends(
} @PathVariable("deviceId") String deviceId,
BmsSystemStatus ss = bmsSystemStatusService.lambdaQuery() @ApiParam("向前小时数,默认 24") @RequestParam(value = "hours", required = false, defaultValue = "24") int hours) {
.eq(BmsSystemStatus::getDeviceId, deviceId) RealtimeSnapshot snap = loadRealtimeSnapshot(deviceId);
.orderByDesc(BmsSystemStatus::getCreateTime).last("limit 1").one(); SimpleDateFormat tf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
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<BmsChargeTemperature> temps = bmsChargeTemperatureService.lambdaQuery()
.eq(BmsChargeTemperature::getDeviceId, deviceId)
.orderByDesc(BmsChargeTemperature::getCreateTime).last("limit 8").list();
List<NameValueDTO> 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<BmsVoltageChannelData> chs = bmsVoltageChannelDataService.lambdaQuery()
.eq(BmsVoltageChannelData::getDeviceId, deviceId)
.orderByDesc(BmsVoltageChannelData::getCreateTime).last("limit 16").list();
List<NameValueDTO> 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
Date now = new Date(); Date now = new Date();
Date begin = new Date(now.getTime() - 24L * 3600_000L); int h = hours <= 0 ? 24 : Math.min(hours, 168);
List<BmsVoltageChannelData> vtrend = bmsVoltageChannelDataService.lambdaQuery() Date trendBegin = new Date(now.getTime() - (long) h * 3600_000L);
.eq(BmsVoltageChannelData::getDeviceId, deviceId) return R.ok(buildRealtimeDashboard(deviceId, snap.vehicleData, snap.extremeValues, snap.systemStatus,
.eq(BmsVoltageChannelData::getChannelType, "RTP") snap.chargeDetail, snap.chargeTemperatures, snap.voltageChannels, trendBegin, now, tf));
.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<TimePointDTO> 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));
List<BmsChargeTemperature> ttrend = bmsChargeTemperatureService.lambdaQuery() /**
.eq(BmsChargeTemperature::getDeviceId, deviceId) * {@link #realtime(String)} 同源字段的历史平铺列表时间范围内全部 vehicle_data 在库内分页pageNum/pageSize
.eq(BmsChargeTemperature::getTempIndex, 1) * 每页再与其它表按时间点合并pageSize 仅支持 1020100500
.ge(BmsChargeTemperature::getCreateTime, begin) */
.le(BmsChargeTemperature::getCreateTime, now) @PreAuthorize("@ss.hasPermi('system:devices:query')")
.orderByAsc(BmsChargeTemperature::getCreateTime) @GetMapping("/realtime/history/{deviceId}")
.list(); @ApiOperation(value = "设备实时同源历史(平铺分页)", notes = "对 beginTime~endTime 内全部整车记录分页;参数 pageNum、pageSize(10|20|100|500)total 为符合条件的总条数", response = DeviceHistoryFlatRowDTO.class)
List<TimePointDTO> tpoints = new ArrayList<>(); public TableDataInfo realtimeHistoryFlat(
for (BmsChargeTemperature it : ttrend) { @ApiParam(value = "设备ID", required = true) @PathVariable("deviceId") String deviceId,
TimePointDTO p = new TimePointDTO(); @ApiParam(value = "开始时间戳(毫秒),与 beginTime 二选一") @RequestParam(value = "start", required = false) Long startMillis,
p.setTime(it.getCreateTime() == null ? null : f.format(it.getCreateTime())); @ApiParam(value = "结束时间戳(毫秒),与 endTime 二选一") @RequestParam(value = "end", required = false) Long endMillis,
p.setValue(it.getTemperature()); @ApiParam(value = "开始时间 yyyy-MM-dd HH:mm:ss") @RequestParam(value = "beginTime", required = false) String beginTimeStr,
tpoints.add(p); @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(); boolean desc = !"asc".equalsIgnoreCase(order);
tseries.setName("T1"); if (!userRange) {
tseries.setPoints(tpoints); start = clampHistoryStart(start, end);
dto.setTemperatureTrend(java.util.Collections.singletonList(tseries)); }
return success(dto); int pageSize = com.evobms.common.utils.PageUtils.startHistoryPage(countTotal);
List<VehicleData> 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<DeviceHistoryFlatRowDTO> 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)); return toAjax(bmsDevicesService.deleteBmsDevicesByIds(ids));
} }
/** 扁平视图:与原始列表共用同一批查询结果,避免重复组装且趋势只在 dashboard 中暴露一份 */
private DeviceRealtimeDTO buildRealtimeDashboard(
String deviceId,
VehicleData vd,
ExtremeValues ev,
BmsSystemStatus ss,
BmsChargeDetail cd,
List<BmsChargeTemperature> chargeTemps,
List<BmsVoltageChannelData> 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<VehicleData> 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<VehicleData> 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<VehicleData> rows,
String seriesName,
Date rangeBegin,
Date rangeEnd,
SimpleDateFormat tf,
Function<VehicleData, BigDecimal> valueFn) {
List<TimePointDTO> 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<NameValueDTO> toChargeTemperatureNameValues(List<BmsChargeTemperature> temps, SimpleDateFormat tf) {
if (temps == null || temps.isEmpty()) {
return new ArrayList<>();
}
List<NameValueDTO> 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<NameValueDTO> toVoltageChannelNameValues(List<BmsVoltageChannelData> chs, SimpleDateFormat tf) {
if (chs == null || chs.isEmpty()) {
return new ArrayList<>();
}
List<NameValueDTO> 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<LineSeriesDTO> buildRtp1VoltageTrendSeries(String deviceId, Date begin, Date end, SimpleDateFormat tf) {
List<BmsVoltageChannelData> 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<TimePointDTO> 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<LineSeriesDTO> buildT1TemperatureTrendSeries(String deviceId, Date begin, Date end, SimpleDateFormat tf) {
List<BmsChargeTemperature> 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<TimePointDTO> 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<VehicleData> vehicleF = CompletableFuture.supplyAsync(
() -> vehicleDataService.selectLatestByDeviceId(deviceId));
CompletableFuture<ExtremeValues> extremeF = CompletableFuture.supplyAsync(
() -> extremeValuesService.selectLatestByDeviceId(deviceId));
CompletableFuture<VehicleLocation> locationF = CompletableFuture.supplyAsync(
() -> vehicleLocationService.selectLatestByDeviceId(deviceId));
CompletableFuture<BmsSystemStatus> systemF = CompletableFuture.supplyAsync(
() -> latestSystemStatus(deviceId));
CompletableFuture<BmsChargeDetail> chargeDetailF = CompletableFuture.supplyAsync(
() -> latestChargeDetail(deviceId));
CompletableFuture<BmsRuntimeData> runtimeF = CompletableFuture.supplyAsync(
() -> latestRuntimeData(deviceId));
CompletableFuture<BmsSopData> sopF = CompletableFuture.supplyAsync(
() -> latestSopData(deviceId));
CompletableFuture<List<SubsystemVoltage>> svF = CompletableFuture.supplyAsync(
() -> listLatestSubsystemVoltages(deviceId));
CompletableFuture<List<SubsystemTemperature>> stF = CompletableFuture.supplyAsync(
() -> listLatestSubsystemTemperatures(deviceId));
CompletableFuture<List<BmsChargeTemperature>> chargeTempF = CompletableFuture.supplyAsync(
() -> listLatestChargeTemperatures(deviceId));
CompletableFuture<List<BmsVoltageChannelData>> 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<SubsystemVoltage> svRows = snap.subsystemVoltageRows;
agg.setSubsystemVoltage(svRows == null || svRows.isEmpty() ? null : svRows.get(0));
agg.setSubsystemVoltages(latestOnePerSubsystem(svRows, SubsystemVoltage::getSubsystemNo));
List<SubsystemTemperature> 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<SubsystemVoltage> listLatestSubsystemVoltages(String deviceId) {
return subsystemVoltageService.lambdaQuery()
.eq(SubsystemVoltage::getDeviceId, deviceId)
.isNotNull(SubsystemVoltage::getTimestamp)
.orderByDesc(SubsystemVoltage::getTimestamp)
.last("limit " + REALTIME_SUBSYSTEM_PREFETCH)
.list();
}
private List<SubsystemTemperature> listLatestSubsystemTemperatures(String deviceId) {
return subsystemTemperatureService.lambdaQuery()
.eq(SubsystemTemperature::getDeviceId, deviceId)
.isNotNull(SubsystemTemperature::getTimestamp)
.orderByDesc(SubsystemTemperature::getTimestamp)
.last("limit " + REALTIME_SUBSYSTEM_PREFETCH)
.list();
}
private List<BmsChargeTemperature> listLatestChargeTemperatures(String deviceId) {
return bmsChargeTemperatureService.lambdaQuery()
.eq(BmsChargeTemperature::getDeviceId, deviceId)
.isNotNull(BmsChargeTemperature::getTimestamp)
.orderByDesc(BmsChargeTemperature::getTimestamp)
.last("limit 8")
.list();
}
private List<BmsVoltageChannelData> 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<SubsystemVoltage> subsystemVoltageRows;
List<SubsystemTemperature> subsystemTemperatureRows;
List<BmsChargeTemperature> chargeTemperatures;
List<BmsVoltageChannelData> voltageChannels;
}
/** 从按时间倒序的结果中取每个子系统号的第一条(即该子系统最新数据) */
private static <T> List<T> latestOnePerSubsystem(List<T> orderedNewestFirst, Function<T, Integer> subsystemNoFn) {
if (orderedNewestFirst == null || orderedNewestFirst.isEmpty()) {
return Collections.emptyList();
}
Map<Integer, T> seen = new LinkedHashMap<>();
for (T row : orderedNewestFirst) {
Integer no = subsystemNoFn.apply(row);
if (no != null && !seen.containsKey(no)) {
seen.put(no, row);
}
}
List<T> out = new ArrayList<>(seen.values());
out.sort(Comparator.comparing(subsystemNoFn, Comparator.nullsLast(Integer::compareTo)));
return out;
}
} }

View File

@ -42,17 +42,17 @@ public class VehicleData extends BaseEntity
/** 车辆状态 (1:启动 2:熄火 3:其他 254:异常 255:无效) */ /** 车辆状态 (1:启动 2:熄火 3:其他 254:异常 255:无效) */
@ApiModelProperty(value = "车辆状态 (1:启动 2:熄火 3:其他 254:异常 255:无效)") @ApiModelProperty(value = "车辆状态 (1:启动 2:熄火 3:其他 254:异常 255:无效)")
@Excel(name = "车辆状态 (1:启动 2:熄火 3:其他 254:异常 255:无效)") @Excel(name = "车辆状态 ")
private Integer vehicleStatus; private Integer vehicleStatus;
/** 充电状态 (1:停车充电 2:行驶充电 3:未充电 4:充电完成 254:异常 255:无效) */ /** 充电状态 (1:停车充电 2:行驶充电 3:未充电 4:充电完成 254:异常 255:无效) */
@ApiModelProperty(value = "充电状态 (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; private Integer chargingStatus;
/** 运行模式 (1:纯电 2:混动 3:燃油 254:异常 255:无效) */ /** 运行模式 (1:纯电 2:混动 3:燃油 254:异常 255:无效) */
@ApiModelProperty(value = "运行模式 (1:纯电 2:混动 3:燃油 254:异常 255:无效)") @ApiModelProperty(value = "运行模式 (1:纯电 2:混动 3:燃油 254:异常 255:无效)")
@Excel(name = "运行模式 (1:纯电 2:混动 3:燃油 254:异常 255:无效)") @Excel(name = "运行模式 ")
private Integer operationMode; private Integer operationMode;
/** 车速(km/h) */ /** 车速(km/h) */
@ -87,7 +87,7 @@ public class VehicleData extends BaseEntity
/** 档位 (0:空档 1-6:1-6档 13:倒档 14:自动D档 15:停车P档) */ /** 档位 (0:空档 1-6:1-6档 13:倒档 14:自动D档 15:停车P档) */
@ApiModelProperty(value = "档位 (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; private Integer gearPosition;
/** 绝缘电阻(kΩ) */ /** 绝缘电阻(kΩ) */

View File

@ -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<ChargeDischargeMonthDTO> months;
@ApiModelProperty(value = "区段明细记录")
private List<ChargeDischargeRecordDTO> records;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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<NameValueDTO> socDistribution;
@ApiModelProperty("日充放电趋势(柱状图)")
private List<DashboardTrendItemDTO> trend;
@ApiModelProperty("运行时长累计与分布(左侧列表+右侧柱状图)")
private RuntimeSummaryDTO runtimeSummary;
@ApiModelProperty("运行时长分布(柱状图,单位小时)")
private List<NameValueDTO> runtimeDuration;
}

View File

@ -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<SocBinDTO> bins;
@ApiModelProperty(value = "生成时间ISO8601", example = "2026-03-18T10:22:00Z")
private String generatedAt;
@ApiModelProperty(value = "参数回显bins/includeOffline/includeUnknown 等)")
private Map<String, Object> paramsEcho;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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<BmsChargeTemperature> chargeTemperaturesAtPoint;
@ApiModelProperty("Voltage channels, latest per type+index at point time")
private List<BmsVoltageChannelData> voltageChannelsAtPoint;
@ApiModelProperty("Per-subsystem voltage frame, latest per subsystem no at point time")
private List<SubsystemVoltage> subsystemVoltagesAtPoint;
@ApiModelProperty("Per-subsystem temperature frame, latest per subsystem no at point time")
private List<SubsystemTemperature> subsystemTemperaturesAtPoint;
@ApiModelProperty("Convenience: probe T1 deg C")
private BigDecimal chargeSeatTemp1;
@ApiModelProperty("Convenience: RTP channel 1 voltage")
private BigDecimal rtp1Voltage;
}

View File

@ -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;
}

View File

@ -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 = "BMSSOP功率能力")
private BmsSopData sopData;
@ApiModelProperty(value = "BMS充电座温度最新")
private List<BmsChargeTemperature> chargeTemperatures;
@ApiModelProperty(value = "BMS电压通道最新")
private List<BmsVoltageChannelData> voltageChannels;
@ApiModelProperty(value = "可充电储能子系统电压:入库时间最新的一条(任意子系统号,便于单独展示)")
private SubsystemVoltage subsystemVoltage;
@ApiModelProperty(value = "可充电储能子系统温度:入库时间最新的一条(任意子系统号,便于单独展示)")
private SubsystemTemperature subsystemTemperature;
@ApiModelProperty(value = "可充电储能子系统电压:各子系统号各保留入库最新一条")
private List<SubsystemVoltage> subsystemVoltages;
@ApiModelProperty(value = "可充电储能子系统温度:各子系统号各保留入库最新一条")
private List<SubsystemTemperature> subsystemTemperatures;
@ApiModelProperty(value = "前端扁平视图:系统状态分块、通道/探头列表、24h 趋势等(与上方原始实体互补,避免重复组装)")
private DeviceRealtimeDTO dashboard;
}

View File

@ -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<NameValueDTO> faults;
@ApiModelProperty(value = "运行状态(充放电状态、继电器、复位次数、采样电压等)")
private RunStatusSection runStatus;
@ApiModelProperty(value = "充电信息AC/DC电压电流、停止码、阶段等")
private ChargeSection charge;
@ApiModelProperty(value = "统计信息(循环次数、累计时长等)")
private StatisticsSection statistics;
@ApiModelProperty(value = "单体电压列表name=通道名value=电压值")
private List<NameValueDTO> voltages;
@ApiModelProperty(value = "单体温度列表name=T序号value=温度值")
private List<NameValueDTO> temperatures;
@ApiModelProperty(value = "单体电压折线图默认RTP-1最近24小时")
private List<LineSeriesDTO> voltageTrend;
@ApiModelProperty(value = "单体温度折线图默认T1最近24小时")
private List<LineSeriesDTO> temperatureTrend;
@ApiModelProperty(value = "整车总电压折线图vehicle_data最近24小时")
private List<LineSeriesDTO> totalVoltageTrend;
@ApiModelProperty(value = "整车总电流折线图vehicle_data最近24小时")
private List<LineSeriesDTO> totalCurrentTrend;
@ApiModelProperty(value = "整车 SOC 折线图vehicle_data最近24小时")
private List<LineSeriesDTO> 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;
}
}

View File

@ -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<TrackPointDTO> trackList;
}

View File

@ -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<TimePointDTO> points;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -65,4 +65,14 @@ public interface VehicleDataMapper extends BaseMapper<VehicleData>
List<VehicleData> selectByDeviceAndStartTime(@Param("deviceId") String deviceId, @Param("startTime") Date startTime); List<VehicleData> selectByDeviceAndStartTime(@Param("deviceId") String deviceId, @Param("startTime") Date startTime);
VehicleData selectLatestByDeviceId(String deviceId); VehicleData selectLatestByDeviceId(String deviceId);
/**
* 历史分页仅按 timestamp 过滤/排序 (device_id, timestamp) 索引避免 COALESCE filesort
* MQTT 入库均带 timestamptimestamp 为空的旧数据不在此接口返回
*/
List<VehicleData> selectVehicleDataListForHistory(
@Param("deviceId") String deviceId,
@Param("begin") Date begin,
@Param("end") Date end,
@Param("descending") boolean descending);
} }

View File

@ -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 本页请求的 pageSize10/20/100/500用于放大预取上限实际行数可能更少末页
*/
public List<DeviceHistoryFlatRowDTO> build(
String deviceId,
List<VehicleData> vehicleRows,
SimpleDateFormat tf,
int configuredPageSize) {
List<DeviceHistoryFlatRowDTO> 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<Integer, BmsChargeTemperature> chargeByIdx = ctx.chargeByIdx;
Map<String, BmsVoltageChannelData> voltByKey = ctx.voltByKey;
Map<Integer, SubsystemVoltage> svByNo = ctx.svByNo;
Map<Integer, SubsystemTemperature> stByNo = ctx.stByNo;
List<ExtremeValues> extremeAsc = ctx.extremeAsc;
List<BmsSystemStatus> ssAsc = ctx.ssAsc;
List<BmsChargeDetail> cdAsc = ctx.cdAsc;
List<BmsRuntimeData> rtAsc = ctx.rtAsc;
List<BmsSopData> sopAsc = ctx.sopAsc;
List<VehicleLocation> locAsc = ctx.locAsc;
List<BmsChargeTemperature> chargeAsc = ctx.chargeAsc;
List<BmsVoltageChannelData> voltAsc = ctx.voltAsc;
List<SubsystemVoltage> svAsc = ctx.svAsc;
List<SubsystemTemperature> stAsc = ctx.stAsc;
List<Integer> 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<Integer, DeviceHistoryFlatRowDTO> 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<ExtremeValues> curExF = CompletableFuture.supplyAsync(() -> bootstrapExtremeValues(deviceId, bootstrapBefore));
CompletableFuture<BmsSystemStatus> curSsF = CompletableFuture.supplyAsync(() -> bootstrapSystemStatus(deviceId, bootstrapBefore));
CompletableFuture<BmsChargeDetail> curCdF = CompletableFuture.supplyAsync(() -> bootstrapChargeDetail(deviceId, bootstrapBefore));
CompletableFuture<BmsRuntimeData> curRtF = CompletableFuture.supplyAsync(() -> bootstrapRuntimeData(deviceId, bootstrapBefore));
CompletableFuture<BmsSopData> curSopF = CompletableFuture.supplyAsync(() -> bootstrapSopData(deviceId, bootstrapBefore));
CompletableFuture<VehicleLocation> curLocF = CompletableFuture.supplyAsync(() -> bootstrapVehicleLocation(deviceId, bootstrapBefore));
CompletableFuture<Map<Integer, BmsChargeTemperature>> chargeMapF = CompletableFuture.supplyAsync(() -> bootstrapChargeTemps(deviceId, bootstrapBefore, mapCap));
CompletableFuture<Map<String, BmsVoltageChannelData>> voltMapF = CompletableFuture.supplyAsync(() -> bootstrapVoltageChannels(deviceId, bootstrapBefore, mapCap));
CompletableFuture<Map<Integer, SubsystemVoltage>> svMapF = CompletableFuture.supplyAsync(() -> bootstrapSubsystemVoltages(deviceId, bootstrapBefore, mapCap));
CompletableFuture<Map<Integer, SubsystemTemperature>> stMapF = CompletableFuture.supplyAsync(() -> bootstrapSubsystemTemperatures(deviceId, bootstrapBefore, mapCap));
CompletableFuture<List<ExtremeValues>> extremeAscF = CompletableFuture.supplyAsync(() -> loadExtremeValues(deviceId, lo, hi, cap));
CompletableFuture<List<BmsSystemStatus>> ssAscF = CompletableFuture.supplyAsync(() -> loadSystemStatus(deviceId, lo, hi, cap));
CompletableFuture<List<BmsChargeDetail>> cdAscF = CompletableFuture.supplyAsync(() -> loadChargeDetail(deviceId, lo, hi, cap));
CompletableFuture<List<BmsRuntimeData>> rtAscF = CompletableFuture.supplyAsync(() -> loadRuntimeData(deviceId, lo, hi, cap));
CompletableFuture<List<BmsSopData>> sopAscF = CompletableFuture.supplyAsync(() -> loadSopData(deviceId, lo, hi, cap));
CompletableFuture<List<VehicleLocation>> locAscF = CompletableFuture.supplyAsync(() -> loadVehicleLocations(deviceId, lo, hi, cap));
CompletableFuture<List<BmsChargeTemperature>> chargeAscF = CompletableFuture.supplyAsync(() -> loadChargeTemps(deviceId, lo, hi, mapCap));
CompletableFuture<List<BmsVoltageChannelData>> voltAscF = CompletableFuture.supplyAsync(() -> loadVoltageChannels(deviceId, lo, hi, mapCap));
CompletableFuture<List<SubsystemVoltage>> svAscF = CompletableFuture.supplyAsync(() -> loadSubsystemVoltages(deviceId, lo, hi, mapCap));
CompletableFuture<List<SubsystemTemperature>> 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<Integer, BmsChargeTemperature> chargeByIdx;
Map<String, BmsVoltageChannelData> voltByKey;
Map<Integer, SubsystemVoltage> svByNo;
Map<Integer, SubsystemTemperature> stByNo;
List<ExtremeValues> extremeAsc;
List<BmsSystemStatus> ssAsc;
List<BmsChargeDetail> cdAsc;
List<BmsRuntimeData> rtAsc;
List<BmsSopData> sopAsc;
List<VehicleLocation> locAsc;
List<BmsChargeTemperature> chargeAsc;
List<BmsVoltageChannelData> voltAsc;
List<SubsystemVoltage> svAsc;
List<SubsystemTemperature> 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<Integer> sortedIndicesByPointTimeAsc(List<VehicleData> vehicleRows) {
List<Integer> 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<Integer, BmsChargeTemperature> chargeByIdx,
Map<String, BmsVoltageChannelData> voltByKey,
Map<Integer, SubsystemVoltage> svByNo,
Map<Integer, SubsystemTemperature> 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<BmsChargeTemperature> temps = new ArrayList<>(chargeByIdx.values());
temps.sort(Comparator.comparing(BmsChargeTemperature::getTempIndex, Comparator.nullsLast(Integer::compareTo)));
dto.setChargeTemperaturesAtPoint(temps.isEmpty() ? null : temps);
List<BmsVoltageChannelData> 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<SubsystemVoltage> svs = new ArrayList<>(svByNo.values());
svs.sort(Comparator.comparing(SubsystemVoltage::getSubsystemNo, Comparator.nullsLast(Integer::compareTo)));
dto.setSubsystemVoltagesAtPoint(svs.isEmpty() ? null : svs);
List<SubsystemTemperature> 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<ExtremeValues> qw = new LambdaQueryWrapper<>();
qw.eq(ExtremeValues::getDeviceId, deviceId);
applyTimestampLt(qw, ExtremeValues::getTimestamp, before);
qw.orderByDesc(ExtremeValues::getTimestamp);
qw.last("LIMIT 1");
List<ExtremeValues> rows = extremeValuesMapper.selectList(qw);
return rows == null || rows.isEmpty() ? null : rows.get(0);
}
private VehicleLocation bootstrapVehicleLocation(String deviceId, Date before) {
LambdaQueryWrapper<VehicleLocation> qw = new LambdaQueryWrapper<>();
qw.eq(VehicleLocation::getDeviceId, deviceId);
applyTimestampLt(qw, VehicleLocation::getTimestamp, before);
qw.orderByDesc(VehicleLocation::getTimestamp);
qw.last("LIMIT 1");
List<VehicleLocation> 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<Integer, BmsChargeTemperature> bootstrapChargeTemps(String deviceId, Date before, int limit) {
LambdaQueryWrapper<BmsChargeTemperature> 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<String, BmsVoltageChannelData> bootstrapVoltageChannels(String deviceId, Date before, int limit) {
LambdaQueryWrapper<BmsVoltageChannelData> 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<Integer, SubsystemVoltage> bootstrapSubsystemVoltages(String deviceId, Date before, int limit) {
LambdaQueryWrapper<SubsystemVoltage> 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<Integer, SubsystemTemperature> bootstrapSubsystemTemperatures(String deviceId, Date before, int limit) {
LambdaQueryWrapper<SubsystemTemperature> 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<Integer, BmsChargeTemperature> replayChargeTemps(List<BmsChargeTemperature> rows) {
Map<Integer, BmsChargeTemperature> 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<String, BmsVoltageChannelData> replayVoltageChannels(List<BmsVoltageChannelData> rows) {
Map<String, BmsVoltageChannelData> map = new HashMap<>();
if (rows == null) {
return map;
}
for (BmsVoltageChannelData row : rows) {
map.put(voltChannelKey(row), row);
}
return map;
}
private static Map<Integer, SubsystemVoltage> replaySubsystemVoltages(List<SubsystemVoltage> rows) {
Map<Integer, SubsystemVoltage> 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<Integer, SubsystemTemperature> replaySubsystemTemperatures(List<SubsystemTemperature> rows) {
Map<Integer, SubsystemTemperature> 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<ExtremeValues> loadExtremeValues(String deviceId, Date lo, Date hi, int cap) {
LambdaQueryWrapper<ExtremeValues> qw = new LambdaQueryWrapper<>();
qw.eq(ExtremeValues::getDeviceId, deviceId);
applyTimestampRange(qw, ExtremeValues::getTimestamp, lo, hi);
qw.orderByAsc(ExtremeValues::getTimestamp);
qw.last("LIMIT " + cap);
List<ExtremeValues> rows = extremeValuesMapper.selectList(qw);
return rows == null ? new ArrayList<>() : rows;
}
private List<VehicleLocation> loadVehicleLocations(String deviceId, Date lo, Date hi, int cap) {
LambdaQueryWrapper<VehicleLocation> qw = new LambdaQueryWrapper<>();
qw.eq(VehicleLocation::getDeviceId, deviceId);
applyTimestampRange(qw, VehicleLocation::getTimestamp, lo, hi);
qw.orderByAsc(VehicleLocation::getTimestamp);
qw.last("LIMIT " + cap);
List<VehicleLocation> rows = vehicleLocationMapper.selectList(qw);
return rows == null ? new ArrayList<>() : rows;
}
private List<BmsChargeTemperature> loadChargeTemps(String deviceId, Date lo, Date hi, int cap) {
LambdaQueryWrapper<BmsChargeTemperature> qw = new LambdaQueryWrapper<>();
qw.eq(BmsChargeTemperature::getDeviceId, deviceId);
applyTimestampRange(qw, BmsChargeTemperature::getTimestamp, lo, hi);
qw.orderByAsc(BmsChargeTemperature::getTimestamp);
qw.last("LIMIT " + cap);
List<BmsChargeTemperature> rows = bmsChargeTemperatureService.list(qw);
return rows == null ? new ArrayList<>() : rows;
}
private List<BmsVoltageChannelData> loadVoltageChannels(String deviceId, Date lo, Date hi, int cap) {
LambdaQueryWrapper<BmsVoltageChannelData> qw = new LambdaQueryWrapper<>();
qw.eq(BmsVoltageChannelData::getDeviceId, deviceId);
applyTimestampRange(qw, BmsVoltageChannelData::getTimestamp, lo, hi);
qw.orderByAsc(BmsVoltageChannelData::getTimestamp);
qw.last("LIMIT " + cap);
List<BmsVoltageChannelData> rows = bmsVoltageChannelDataService.list(qw);
return rows == null ? new ArrayList<>() : rows;
}
private List<SubsystemVoltage> loadSubsystemVoltages(String deviceId, Date lo, Date hi, int cap) {
LambdaQueryWrapper<SubsystemVoltage> qw = new LambdaQueryWrapper<>();
qw.eq(SubsystemVoltage::getDeviceId, deviceId);
applyTimestampRange(qw, SubsystemVoltage::getTimestamp, lo, hi);
qw.orderByAsc(SubsystemVoltage::getTimestamp);
qw.last("LIMIT " + cap);
List<SubsystemVoltage> rows = subsystemVoltageService.list(qw);
return rows == null ? new ArrayList<>() : rows;
}
private List<SubsystemTemperature> loadSubsystemTemperatures(String deviceId, Date lo, Date hi, int cap) {
LambdaQueryWrapper<SubsystemTemperature> qw = new LambdaQueryWrapper<>();
qw.eq(SubsystemTemperature::getDeviceId, deviceId);
applyTimestampRange(qw, SubsystemTemperature::getTimestamp, lo, hi);
qw.orderByAsc(SubsystemTemperature::getTimestamp);
qw.last("LIMIT " + cap);
List<SubsystemTemperature> rows = subsystemTemperatureService.list(qw);
return rows == null ? new ArrayList<>() : rows;
}
private List<BmsSystemStatus> 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<BmsChargeDetail> 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<BmsRuntimeData> 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<BmsSopData> 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 <T> void applyTimestampLt(
LambdaQueryWrapper<T> qw,
SFunction<T, Date> timestampCol,
Date before) {
qw.isNotNull(timestampCol);
if (before != null) {
qw.lt(timestampCol, before);
}
}
private static <T> void applyTimestampRange(
LambdaQueryWrapper<T> qw,
SFunction<T, Date> timestampCol,
Date lo,
Date hi) {
qw.isNotNull(timestampCol);
if (lo != null) {
qw.ge(timestampCol, lo);
}
if (hi != null) {
qw.le(timestampCol, hi);
}
}
}

View File

@ -67,4 +67,14 @@ public interface IVehicleDataService
VehicleData selectLatestByDeviceId(String deviceId); VehicleData selectLatestByDeviceId(String deviceId);
HomeDashboardDTO getHomeDashboard(); HomeDashboardDTO getHomeDashboard();
/**
* 历史平铺分页按业务时间优先timestamp否则 create_time支持时间范围与排序
*/
List<VehicleData> selectVehicleDataListForHistory(String deviceId, Date begin, Date end, boolean descending);
/**
* 趋势图采样时间范围内按业务时间倒序取最多 maxPoints 返回结果按时间正序
*/
List<VehicleData> selectVehicleDataTrendInRange(String deviceId, Date begin, Date end, int maxPoints);
} }

View File

@ -1,6 +1,8 @@
package com.evobms.project.vehicledata.service.impl; package com.evobms.project.vehicledata.service.impl;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -54,6 +56,11 @@ public class VehicleDataServiceImpl extends ServiceImpl<VehicleDataMapper, Vehic
* @param vehicleData 整车数据 * @param vehicleData 整车数据
* @return 整车数据 * @return 整车数据
*/ */
@Override
public List<VehicleData> selectVehicleDataListForHistory(String deviceId, Date begin, Date end, boolean descending) {
return vehicleDataMapper.selectVehicleDataListForHistory(deviceId, begin, end, descending);
}
@Override @Override
public List<VehicleData> selectVehicleDataList(VehicleData vehicleData) public List<VehicleData> selectVehicleDataList(VehicleData vehicleData)
{ {
@ -81,6 +88,37 @@ public class VehicleDataServiceImpl extends ServiceImpl<VehicleDataMapper, Vehic
return vehicleDataMapper.selectList(qw); return vehicleDataMapper.selectList(qw);
} }
@Override
public List<VehicleData> selectVehicleDataTrendInRange(String deviceId, Date begin, Date end, int maxPoints) {
int limit = maxPoints <= 0 ? 360 : Math.min(maxPoints, 2000);
LambdaQueryWrapper<VehicleData> 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<VehicleData> rows = vehicleDataMapper.selectList(qw);
if (rows == null || rows.isEmpty()) {
return Collections.emptyList();
}
List<VehicleData> asc = new ArrayList<>(rows);
Collections.reverse(asc);
return asc;
}
/** /**
* 新增整车数据 * 新增整车数据
* *

View File

@ -6,7 +6,8 @@ spring:
druid: druid:
# 主库数据源 # 主库数据源
master: 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 username: root
password: root password: root
# 从库数据源 # 从库数据源
@ -34,10 +35,11 @@ spring:
minEvictableIdleTimeMillis: 300000 minEvictableIdleTimeMillis: 300000
# 配置一个连接在池中最大生存的时间,单位是毫秒 # 配置一个连接在池中最大生存的时间,单位是毫秒
maxEvictableIdleTimeMillis: 900000 maxEvictableIdleTimeMillis: 900000
# 配置检测连接是否有效 # 配置检测连接是否有效MySQL 可用 SELECT 1
validationQuery: SELECT 1 FROM DUAL validationQuery: SELECT 1
testWhileIdle: true testWhileIdle: true
testOnBorrow: false # 借连接前校验,避免 MySQL wait_timeout / 网络断开后仍使用池中死连接(略增开销)
testOnBorrow: true
testOnReturn: false testOnReturn: false
webStatFilter: webStatFilter:
enabled: true enabled: true

View File

@ -156,6 +156,13 @@ swagger:
# 请求前缀 # 请求前缀
pathMapping: /prod-api 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: referer:
# 防盗链开关 # 防盗链开关
@ -189,8 +196,12 @@ gen:
mqtt: mqtt:
# 是否启用MQTT功能设置为false可在没有MQTT服务器时正常启动 # 是否启用MQTT功能设置为false可在没有MQTT服务器时正常启动
enabled: true enabled: true
legacyReceiver:
enabled: false
publicHost: ${MQTT_PUBLIC_HOST:61.182.73.218}
publicPort: ${MQTT_PUBLIC_PORT:11883}
# MQTT服务器地址 # MQTT服务器地址
host: ${MQTT_HOST_URI:tcp://61.182.73.218:1883} host: ${MQTT_HOST_URI:tcp://61.182.73.218:11883}
# 客户端ID避免冲突随机后缀 # 客户端ID避免冲突随机后缀
clientId: evobms-client-${random.int} clientId: evobms-client-${random.int}
# 用户名(如需认证请填写) # 用户名(如需认证请填写)
@ -199,6 +210,8 @@ mqtt:
password: ${MQTT_PASSWORD:741a03a10f3de6b2} password: ${MQTT_PASSWORD:741a03a10f3de6b2}
# 连接超时时间(秒) # 连接超时时间(秒)
connectionTimeout: 30 connectionTimeout: 30
# 订阅/连接完成等待时间(毫秒)
completionTimeout: 20000
# 心跳间隔(秒) # 心跳间隔(秒)
keepAliveInterval: 60 keepAliveInterval: 60
# 是否自动重连 # 是否自动重连

View File

@ -7,7 +7,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<resultMap type="BmsChargeTemperature" id="BmsChargeTemperatureResult"> <resultMap type="BmsChargeTemperature" id="BmsChargeTemperatureResult">
<result property="id" column="id" /> <result property="id" column="id" />
<result property="deviceId" column="device_id" /> <result property="deviceId" column="device_id" />
<result property="timestap" column="timestap" /> <result property="timestamp" column="timestamp" />
<result property="tempIndex" column="temp_index" /> <result property="tempIndex" column="temp_index" />
<result property="temperature" column="temperature" /> <result property="temperature" column="temperature" />
<result property="createBy" column="create_by" /> <result property="createBy" column="create_by" />
@ -17,14 +17,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</resultMap> </resultMap>
<sql id="selectBmsChargeTemperatureVo"> <sql id="selectBmsChargeTemperatureVo">
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
</sql> </sql>
<select id="selectBmsChargeTemperatureList" parameterType="BmsChargeTemperature" resultMap="BmsChargeTemperatureResult"> <select id="selectBmsChargeTemperatureList" parameterType="BmsChargeTemperature" resultMap="BmsChargeTemperatureResult">
<include refid="selectBmsChargeTemperatureVo"/> <include refid="selectBmsChargeTemperatureVo"/>
<where> <where>
<if test="deviceId != null and deviceId != ''"> and device_id = #{deviceId}</if> <if test="deviceId != null and deviceId != ''"> and device_id = #{deviceId}</if>
<if test="timestap != null "> and timestap = #{timestap}</if> <if test="timestamp != null "> and `timestamp` = #{timestamp}</if>
<if test="tempIndex != null "> and temp_index = #{tempIndex}</if> <if test="tempIndex != null "> and temp_index = #{tempIndex}</if>
<if test="temperature != null "> and temperature = #{temperature}</if> <if test="temperature != null "> and temperature = #{temperature}</if>
</where> </where>
@ -39,7 +39,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
insert into bms_charge_temperature insert into bms_charge_temperature
<trim prefix="(" suffix=")" suffixOverrides=","> <trim prefix="(" suffix=")" suffixOverrides=",">
<if test="deviceId != null and deviceId != ''">device_id,</if> <if test="deviceId != null and deviceId != ''">device_id,</if>
<if test="timestap != null">timestap,</if> <if test="timestamp != null">`timestamp`,</if>
<if test="tempIndex != null">temp_index,</if> <if test="tempIndex != null">temp_index,</if>
<if test="temperature != null">temperature,</if> <if test="temperature != null">temperature,</if>
<if test="createBy != null and createBy != ''">create_by,</if> <if test="createBy != null and createBy != ''">create_by,</if>
@ -49,7 +49,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</trim> </trim>
<trim prefix="values (" suffix=")" suffixOverrides=","> <trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="deviceId != null and deviceId != ''">#{deviceId},</if> <if test="deviceId != null and deviceId != ''">#{deviceId},</if>
<if test="timestap != null">#{timestap},</if> <if test="timestamp != null">#{timestamp},</if>
<if test="tempIndex != null">#{tempIndex},</if> <if test="tempIndex != null">#{tempIndex},</if>
<if test="temperature != null">#{temperature},</if> <if test="temperature != null">#{temperature},</if>
<if test="createBy != null and createBy != ''">#{createBy},</if> <if test="createBy != null and createBy != ''">#{createBy},</if>
@ -63,7 +63,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
update bms_charge_temperature update bms_charge_temperature
<trim prefix="SET" suffixOverrides=","> <trim prefix="SET" suffixOverrides=",">
<if test="deviceId != null and deviceId != ''">device_id = #{deviceId},</if> <if test="deviceId != null and deviceId != ''">device_id = #{deviceId},</if>
<if test="timestap != null">timestap = #{timestap},</if> <if test="timestamp != null">`timestamp` = #{timestamp},</if>
<if test="tempIndex != null">temp_index = #{tempIndex},</if> <if test="tempIndex != null">temp_index = #{tempIndex},</if>
<if test="temperature != null">temperature = #{temperature},</if> <if test="temperature != null">temperature = #{temperature},</if>
<if test="createBy != null and createBy != ''">create_by = #{createBy},</if> <if test="createBy != null and createBy != ''">create_by = #{createBy},</if>
@ -84,4 +84,4 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
#{id} #{id}
</foreach> </foreach>
</delete> </delete>
</mapper> </mapper>

View File

@ -7,7 +7,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<resultMap type="BmsRuntimeData" id="BmsRuntimeDataResult"> <resultMap type="BmsRuntimeData" id="BmsRuntimeDataResult">
<result property="id" column="id" /> <result property="id" column="id" />
<result property="deviceId" column="device_id" /> <result property="deviceId" column="device_id" />
<result property="timestamp" column="timestap" /> <result property="timestamp" column="timestamp" />
<result property="vehicleSpeed" column="vehicle_speed" /> <result property="vehicleSpeed" column="vehicle_speed" />
<result property="mileage" column="mileage" /> <result property="mileage" column="mileage" />
<result property="systemVoltage" column="system_voltage" /> <result property="systemVoltage" column="system_voltage" />
@ -25,14 +25,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</resultMap> </resultMap>
<sql id="selectBmsRuntimeDataVo"> <sql id="selectBmsRuntimeDataVo">
select id, device_id, timestap, vehicle_speed, mileage, system_voltage, system_current, insulation_resistance, soc_max, soc_min, gpv_voltage, run_status, power_status, create_by, update_by, create_time, update_time from bms_runtime_data select id, device_id, `timestamp`, vehicle_speed, mileage, system_voltage, system_current, insulation_resistance, soc_max, soc_min, gpv_voltage, run_status, power_status, create_by, update_by, create_time, update_time from bms_runtime_data
</sql> </sql>
<select id="selectBmsRuntimeDataList" parameterType="BmsRuntimeData" resultMap="BmsRuntimeDataResult"> <select id="selectBmsRuntimeDataList" parameterType="BmsRuntimeData" resultMap="BmsRuntimeDataResult">
<include refid="selectBmsRuntimeDataVo"/> <include refid="selectBmsRuntimeDataVo"/>
<where> <where>
<if test="deviceId != null and deviceId != ''"> and device_id = #{deviceId}</if> <if test="deviceId != null and deviceId != ''"> and device_id = #{deviceId}</if>
<if test="timestap != null "> and timestap = #{timestap}</if> <if test="timestamp != null "> and `timestamp` = #{timestamp}</if>
<if test="vehicleSpeed != null "> and vehicle_speed = #{vehicleSpeed}</if> <if test="vehicleSpeed != null "> and vehicle_speed = #{vehicleSpeed}</if>
<if test="mileage != null "> and mileage = #{mileage}</if> <if test="mileage != null "> and mileage = #{mileage}</if>
<if test="systemVoltage != null "> and system_voltage = #{systemVoltage}</if> <if test="systemVoltage != null "> and system_voltage = #{systemVoltage}</if>
@ -55,7 +55,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
insert into bms_runtime_data insert into bms_runtime_data
<trim prefix="(" suffix=")" suffixOverrides=","> <trim prefix="(" suffix=")" suffixOverrides=",">
<if test="deviceId != null and deviceId != ''">device_id,</if> <if test="deviceId != null and deviceId != ''">device_id,</if>
<if test="timestap != null">timestap,</if> <if test="timestamp != null">`timestamp`,</if>
<if test="vehicleSpeed != null">vehicle_speed,</if> <if test="vehicleSpeed != null">vehicle_speed,</if>
<if test="mileage != null">mileage,</if> <if test="mileage != null">mileage,</if>
<if test="systemVoltage != null">system_voltage,</if> <if test="systemVoltage != null">system_voltage,</if>
@ -73,7 +73,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</trim> </trim>
<trim prefix="values (" suffix=")" suffixOverrides=","> <trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="deviceId != null and deviceId != ''">#{deviceId},</if> <if test="deviceId != null and deviceId != ''">#{deviceId},</if>
<if test="timestap != null">#{timestap},</if> <if test="timestamp != null">#{timestamp},</if>
<if test="vehicleSpeed != null">#{vehicleSpeed},</if> <if test="vehicleSpeed != null">#{vehicleSpeed},</if>
<if test="mileage != null">#{mileage},</if> <if test="mileage != null">#{mileage},</if>
<if test="systemVoltage != null">#{systemVoltage},</if> <if test="systemVoltage != null">#{systemVoltage},</if>
@ -95,7 +95,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
update bms_runtime_data update bms_runtime_data
<trim prefix="SET" suffixOverrides=","> <trim prefix="SET" suffixOverrides=",">
<if test="deviceId != null and deviceId != ''">device_id = #{deviceId},</if> <if test="deviceId != null and deviceId != ''">device_id = #{deviceId},</if>
<if test="timestap != null">timestap = #{timestap},</if> <if test="timestamp != null">`timestamp` = #{timestamp},</if>
<if test="vehicleSpeed != null">vehicle_speed = #{vehicleSpeed},</if> <if test="vehicleSpeed != null">vehicle_speed = #{vehicleSpeed},</if>
<if test="mileage != null">mileage = #{mileage},</if> <if test="mileage != null">mileage = #{mileage},</if>
<if test="systemVoltage != null">system_voltage = #{systemVoltage},</if> <if test="systemVoltage != null">system_voltage = #{systemVoltage},</if>

View File

@ -25,7 +25,7 @@
insert into bms_system_status insert into bms_system_status
<trim prefix="(" suffix=")" suffixOverrides=","> <trim prefix="(" suffix=")" suffixOverrides=",">
<if test="deviceId != null and deviceId != ''">device_id,</if> <if test="deviceId != null and deviceId != ''">device_id,</if>
<if test="timestamp != null">timestamp,</if> <if test="timestamp != null">`timestamp`,</if>
<if test="relayStatus != null">relay_status,</if> <if test="relayStatus != null">relay_status,</if>
<if test="onaaccStatus != null">onaacc_status,</if> <if test="onaaccStatus != null">onaacc_status,</if>
<if test="resetCount != null">reset_count,</if> <if test="resetCount != null">reset_count,</if>

View File

@ -7,7 +7,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<resultMap type="BmsVoltageChannelData" id="BmsVoltageChannelDataResult"> <resultMap type="BmsVoltageChannelData" id="BmsVoltageChannelDataResult">
<result property="id" column="id" /> <result property="id" column="id" />
<result property="deviceId" column="device_id" /> <result property="deviceId" column="device_id" />
<result property="timestap" column="timestap" /> <result property="timestamp" column="timestamp" />
<result property="channelType" column="channel_type" /> <result property="channelType" column="channel_type" />
<result property="channelIndex" column="channel_index" /> <result property="channelIndex" column="channel_index" />
<result property="voltage" column="voltage" /> <result property="voltage" column="voltage" />
@ -18,14 +18,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</resultMap> </resultMap>
<sql id="selectBmsVoltageChannelDataVo"> <sql id="selectBmsVoltageChannelDataVo">
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
</sql> </sql>
<select id="selectBmsVoltageChannelDataList" parameterType="BmsVoltageChannelData" resultMap="BmsVoltageChannelDataResult"> <select id="selectBmsVoltageChannelDataList" parameterType="BmsVoltageChannelData" resultMap="BmsVoltageChannelDataResult">
<include refid="selectBmsVoltageChannelDataVo"/> <include refid="selectBmsVoltageChannelDataVo"/>
<where> <where>
<if test="deviceId != null and deviceId != ''"> and device_id = #{deviceId}</if> <if test="deviceId != null and deviceId != ''"> and device_id = #{deviceId}</if>
<if test="timestap != null "> and timestap = #{timestap}</if> <if test="timestamp != null "> and `timestamp` = #{timestamp}</if>
<if test="channelType != null and channelType != ''"> and channel_type = #{channelType}</if> <if test="channelType != null and channelType != ''"> and channel_type = #{channelType}</if>
<if test="channelIndex != null "> and channel_index = #{channelIndex}</if> <if test="channelIndex != null "> and channel_index = #{channelIndex}</if>
<if test="voltage != null "> and voltage = #{voltage}</if> <if test="voltage != null "> and voltage = #{voltage}</if>
@ -41,7 +41,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
insert into bms_voltage_channel_data insert into bms_voltage_channel_data
<trim prefix="(" suffix=")" suffixOverrides=","> <trim prefix="(" suffix=")" suffixOverrides=",">
<if test="deviceId != null and deviceId != ''">device_id,</if> <if test="deviceId != null and deviceId != ''">device_id,</if>
<if test="timestap != null">timestap,</if> <if test="timestamp != null">`timestamp`,</if>
<if test="channelType != null and channelType != ''">channel_type,</if> <if test="channelType != null and channelType != ''">channel_type,</if>
<if test="channelIndex != null">channel_index,</if> <if test="channelIndex != null">channel_index,</if>
<if test="voltage != null">voltage,</if> <if test="voltage != null">voltage,</if>
@ -52,7 +52,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</trim> </trim>
<trim prefix="values (" suffix=")" suffixOverrides=","> <trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="deviceId != null and deviceId != ''">#{deviceId},</if> <if test="deviceId != null and deviceId != ''">#{deviceId},</if>
<if test="timestap != null">#{timestap},</if> <if test="timestamp != null">#{timestamp},</if>
<if test="channelType != null and channelType != ''">#{channelType},</if> <if test="channelType != null and channelType != ''">#{channelType},</if>
<if test="channelIndex != null">#{channelIndex},</if> <if test="channelIndex != null">#{channelIndex},</if>
<if test="voltage != null">#{voltage},</if> <if test="voltage != null">#{voltage},</if>
@ -67,7 +67,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
update bms_voltage_channel_data update bms_voltage_channel_data
<trim prefix="SET" suffixOverrides=","> <trim prefix="SET" suffixOverrides=",">
<if test="deviceId != null and deviceId != ''">device_id = #{deviceId},</if> <if test="deviceId != null and deviceId != ''">device_id = #{deviceId},</if>
<if test="timestap != null">timestap = #{timestap},</if> <if test="timestamp != null">`timestamp` = #{timestamp},</if>
<if test="channelType != null and channelType != ''">channel_type = #{channelType},</if> <if test="channelType != null and channelType != ''">channel_type = #{channelType},</if>
<if test="channelIndex != null">channel_index = #{channelIndex},</if> <if test="channelIndex != null">channel_index = #{channelIndex},</if>
<if test="voltage != null">voltage = #{voltage},</if> <if test="voltage != null">voltage = #{voltage},</if>
@ -89,4 +89,4 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
#{id} #{id}
</foreach> </foreach>
</delete> </delete>
</mapper> </mapper>

View File

@ -50,6 +50,33 @@
</where> </where>
</select> </select>
<sql id="historyListColumns">
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
</sql>
<select id="selectVehicleDataListForHistory" resultMap="VehicleDataResult">
select <include refid="historyListColumns"/>
from vehicle_data
where device_id = #{deviceId}
and timestamp is not null
<if test="begin != null">
and timestamp &gt;= #{begin}
</if>
<if test="end != null">
and timestamp &lt;= #{end}
</if>
<choose>
<when test="descending">
order by timestamp desc
</when>
<otherwise>
order by timestamp asc
</otherwise>
</choose>
</select>
<select id="selectLatestByDeviceId" resultMap="VehicleDataResult"> <select id="selectLatestByDeviceId" resultMap="VehicleDataResult">
<include refid="selectVehicleDataVo"/> <include refid="selectVehicleDataVo"/>
where device_id = #{deviceId} where device_id = #{deviceId}

Binary file not shown.

Binary file not shown.