Initial commit
This commit is contained in:
66
.gitignore
vendored
Normal file
66
.gitignore
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
target/
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea/modules.xml
|
||||
.idea/jarRepositories.xml
|
||||
.idea/compiler.xml
|
||||
.idea/libraries/
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### Eclipse ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
### Mac OS ###
|
||||
.DS_Store
|
||||
/.idea/
|
||||
|
||||
# ---> Java
|
||||
# Compiled class file
|
||||
*.class
|
||||
|
||||
# Log file
|
||||
*.log
|
||||
|
||||
# BlueJ files
|
||||
*.ctxt
|
||||
|
||||
# Mobile Tools for Java (J2ME)
|
||||
.mtj.tmp/
|
||||
|
||||
# Package Files #
|
||||
*.jar
|
||||
*.war
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*
|
||||
replay_pid*
|
||||
|
||||
18
.idea/compiler.xml
generated
Normal file
18
.idea/compiler.xml
generated
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<annotationProcessing>
|
||||
<profile name="Maven default annotation processors profile" enabled="true">
|
||||
<sourceOutputDir name="target/generated-sources/annotations" />
|
||||
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
|
||||
<outputRelativeToContentRoot value="true" />
|
||||
<module name="ota-server" />
|
||||
</profile>
|
||||
</annotationProcessing>
|
||||
</component>
|
||||
<component name="JavacSettings">
|
||||
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
|
||||
<module name="ota-server" options="-parameters" />
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/encodings.xml
generated
Normal file
6
.idea/encodings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding">
|
||||
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
|
||||
</component>
|
||||
</project>
|
||||
5
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
5
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,5 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
</profile>
|
||||
</component>
|
||||
20
.idea/jarRepositories.xml
generated
Normal file
20
.idea/jarRepositories.xml
generated
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RemoteRepositoriesConfiguration">
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Central Repository" />
|
||||
<option name="url" value="https://repo.maven.apache.org/maven2" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Maven Central repository" />
|
||||
<option name="url" value="https://repo1.maven.org/maven2" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="jboss.community" />
|
||||
<option name="name" value="JBoss Community repository" />
|
||||
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
|
||||
</remote-repository>
|
||||
</component>
|
||||
</project>
|
||||
12
.idea/misc.xml
generated
Normal file
12
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="MavenProjectsManager">
|
||||
<option name="originalFiles">
|
||||
<list>
|
||||
<option value="$PROJECT_DIR$/pom.xml" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK" />
|
||||
</project>
|
||||
7
.idea/vcs.xml
generated
Normal file
7
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
42
README.md
Normal file
42
README.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# 云端 OTA Server
|
||||
|
||||
这是一个基于 `Spring Boot 3.2.1 + Maven + JDK 17 + MySQL + MyBatis-Plus` 的最小可用云端 OTA 管理服务,面向“云端发布、车端检测、安卓提示、人工确认、Docker 执行”的半自动升级模式。
|
||||
|
||||
## 当前能力
|
||||
- 发布 OTA 版本清单
|
||||
- 注册车辆
|
||||
- 为指定车辆分配可见版本
|
||||
- 提供 Agent 心跳、检查更新、结果上报接口
|
||||
- 维护任务状态:`AVAILABLE`、`WAITING_CONFIRM`、`UPGRADING`、`SUCCESS`、`FAILED`、`ROLLED_BACK`、`SKIPPED`
|
||||
- 数据持久化到 MySQL
|
||||
|
||||
## 启动前准备
|
||||
1. 安装 JDK 17、Maven、MySQL 8.x
|
||||
2. 创建数据库并执行初始化脚本:`src/main/resources/schema.sql`
|
||||
3. 按实际环境修改 `src/main/resources/application.yml` 中的数据库连接
|
||||
|
||||
## 启动方式
|
||||
1. 在项目根目录运行:
|
||||
`mvn spring-boot:run`
|
||||
2. 默认端口:`8080`
|
||||
|
||||
## 鉴权
|
||||
Agent 侧接口要求请求头:
|
||||
- `X-OTA-TOKEN: dev-token`
|
||||
|
||||
可在 `src/main/resources/application.yml` 中修改。
|
||||
|
||||
## 管理端接口
|
||||
- `POST /api/admin/releases`
|
||||
- `GET /api/admin/releases`
|
||||
- `POST /api/admin/vehicles`
|
||||
- `GET /api/admin/vehicles`
|
||||
- `POST /api/admin/assignments`
|
||||
- `GET /api/admin/assignments`
|
||||
|
||||
## Agent 接口
|
||||
- `POST /api/agent/heartbeat`
|
||||
- `POST /api/agent/update-check`
|
||||
- `POST /api/agent/report`
|
||||
- `POST /api/agent/confirm`
|
||||
- `POST /api/agent/postpone`
|
||||
80
pom.xml
Normal file
80
pom.xml
Normal file
@@ -0,0 +1,80 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.2.1</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>com.noblelift</groupId>
|
||||
<artifactId>ota-server</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>ota-server</name>
|
||||
<description>Cloud OTA management platform for vehicle docker upgrades</description>
|
||||
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
<mybatis-plus.version>3.5.5</mybatis-plus.version>
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-spring-boot3-starter</artifactId>
|
||||
<version>${sa-token.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||
<version>${mybatis-plus.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
12
src/main/java/com/noblelift/ota/OtaServerApplication.java
Normal file
12
src/main/java/com/noblelift/ota/OtaServerApplication.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package com.noblelift.ota;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class OtaServerApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(OtaServerApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.noblelift.ota.api;
|
||||
|
||||
import com.noblelift.ota.common.dto.ApiMessage;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public ApiMessage handleIllegalArgument(IllegalArgumentException ex) {
|
||||
return new ApiMessage("BAD_REQUEST", ex.getMessage(), Instant.now());
|
||||
}
|
||||
|
||||
@ExceptionHandler(DataIntegrityViolationException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public ApiMessage handleDataIntegrityViolation(DataIntegrityViolationException ex) {
|
||||
String message = ex.getMostSpecificCause() == null ? ex.getMessage() : ex.getMostSpecificCause().getMessage();
|
||||
if (message != null && message.contains("fk_assignment_release_version")) {
|
||||
message = "该版本已被任务引用,无法删除或修改版本号";
|
||||
} else if (message != null && message.contains("fk_assignment_vehicle_id")) {
|
||||
message = "该车辆存在任务记录,无法删除或修改车辆 ID";
|
||||
} else {
|
||||
message = "数据被关联引用,当前操作无法完成";
|
||||
}
|
||||
return new ApiMessage("BAD_REQUEST", message, Instant.now());
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public ApiMessage handleValidation(MethodArgumentNotValidException ex) {
|
||||
String message = ex.getBindingResult().getFieldErrors().stream()
|
||||
.findFirst()
|
||||
.map(error -> error.getField() + " " + error.getDefaultMessage())
|
||||
.orElse("Validation failed");
|
||||
return new ApiMessage("VALIDATION_ERROR", message, Instant.now());
|
||||
}
|
||||
}
|
||||
13
src/main/java/com/noblelift/ota/common/annotation/Log.java
Normal file
13
src/main/java/com/noblelift/ota/common/annotation/Log.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package com.noblelift.ota.common.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface Log {
|
||||
|
||||
String value() default "";
|
||||
}
|
||||
61
src/main/java/com/noblelift/ota/common/aspect/LogAspect.java
Normal file
61
src/main/java/com/noblelift/ota/common/aspect/LogAspect.java
Normal file
@@ -0,0 +1,61 @@
|
||||
package com.noblelift.ota.common.aspect;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.noblelift.ota.common.annotation.Log;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
@Aspect
|
||||
@Component
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class LogAspect {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Around("@annotation(logAnnotation)")
|
||||
public Object around(ProceedingJoinPoint joinPoint, Log logAnnotation) throws Throwable {
|
||||
long start = System.currentTimeMillis();
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
|
||||
HttpServletRequest request = requestAttributes instanceof ServletRequestAttributes servletRequestAttributes
|
||||
? servletRequestAttributes.getRequest()
|
||||
: null;
|
||||
String requestUri = request == null ? "N/A" : request.getRequestURI();
|
||||
String requestMethod = request == null ? "N/A" : request.getMethod();
|
||||
String params = serializeArgs(joinPoint.getArgs());
|
||||
String operation = logAnnotation.value().isBlank() ? signature.getMethod().getName() : logAnnotation.value();
|
||||
log.info("[API-REQUEST] operation={}, method={}, uri={}, params={}", operation, requestMethod, requestUri, params);
|
||||
try {
|
||||
Object result = joinPoint.proceed();
|
||||
log.info("[API-RESPONSE] operation={}, duration={}ms", operation, System.currentTimeMillis() - start);
|
||||
return result;
|
||||
} catch (Throwable ex) {
|
||||
log.error("[API-ERROR] operation={}, duration={}ms, message={}", operation, System.currentTimeMillis() - start, ex.getMessage(), ex);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
private String serializeArgs(Object[] args) {
|
||||
Object[] filtered = Arrays.stream(args)
|
||||
.filter(arg -> !(arg instanceof HttpServletRequest))
|
||||
.toArray();
|
||||
try {
|
||||
return objectMapper.writeValueAsString(filtered);
|
||||
} catch (JsonProcessingException e) {
|
||||
return Arrays.toString(filtered);
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/main/java/com/noblelift/ota/common/dto/ApiMessage.java
Normal file
19
src/main/java/com/noblelift/ota/common/dto/ApiMessage.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package com.noblelift.ota.common.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ApiMessage {
|
||||
|
||||
private String code;
|
||||
private String message;
|
||||
private Instant timestamp;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.noblelift.ota.common.model;
|
||||
|
||||
import com.noblelift.ota.domain.UpgradeMode;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ReleaseManifest {
|
||||
|
||||
private String releaseVersion;
|
||||
private String releaseNotes;
|
||||
private UpgradeMode upgradeMode;
|
||||
private Instant publishedAt;
|
||||
private Boolean parkingRequired;
|
||||
private Map<String, Object> components;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.noblelift.ota.common.model;
|
||||
|
||||
import com.noblelift.ota.domain.TaskStatus;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class VehicleAssignment {
|
||||
|
||||
private String vehicleId;
|
||||
private String releaseVersion;
|
||||
private TaskStatus taskStatus;
|
||||
private Instant promptedAt;
|
||||
private Instant confirmedAt;
|
||||
private Instant startedAt;
|
||||
private Instant finishedAt;
|
||||
private Integer postponeCount;
|
||||
private String lastMessage;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.noblelift.ota.common.model;
|
||||
|
||||
import com.noblelift.ota.domain.AgentStatus;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class VehicleInfo {
|
||||
|
||||
private String vehicleId;
|
||||
private String vin;
|
||||
private String currentRelease;
|
||||
private String currentBackendVersion;
|
||||
private String currentFrontendVersion;
|
||||
private String currentRosVersion;
|
||||
private Instant lastSeenAt;
|
||||
private AgentStatus agentStatus;
|
||||
private String targetRelease;
|
||||
private String lastResult;
|
||||
private Map<String, String> images;
|
||||
private String backupFile;
|
||||
private Boolean online;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.noblelift.ota.common.util;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class ReleaseVersionComparator {
|
||||
|
||||
private static final Pattern TOKEN_PATTERN = Pattern.compile("\\d+");
|
||||
|
||||
private ReleaseVersionComparator() {
|
||||
}
|
||||
|
||||
public static int compare(String left, String right) {
|
||||
List<String> leftTokens = tokenize(left);
|
||||
List<String> rightTokens = tokenize(right);
|
||||
int size = Math.max(leftTokens.size(), rightTokens.size());
|
||||
for (int i = 0; i < size; i++) {
|
||||
String leftToken = i < leftTokens.size() ? leftTokens.get(i) : "0";
|
||||
String rightToken = i < rightTokens.size() ? rightTokens.get(i) : "0";
|
||||
int result = new BigInteger(leftToken).compareTo(new BigInteger(rightToken));
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static List<String> tokenize(String value) {
|
||||
List<String> tokens = new ArrayList<>();
|
||||
if (value == null || value.isBlank()) {
|
||||
return tokens;
|
||||
}
|
||||
Matcher matcher = TOKEN_PATTERN.matcher(value);
|
||||
while (matcher.find()) {
|
||||
tokens.add(matcher.group());
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.noblelift.ota.config;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
@Component
|
||||
public class AgentTokenAuthenticator {
|
||||
|
||||
private static final String TOKEN_HEADER = "X-OTA-TOKEN";
|
||||
private static final String AUTHORIZATION_HEADER = "Authorization";
|
||||
private static final String BEARER_PREFIX = "Bearer ";
|
||||
|
||||
private final OtaProperties otaProperties;
|
||||
|
||||
public AgentTokenAuthenticator(OtaProperties otaProperties) {
|
||||
this.otaProperties = otaProperties;
|
||||
}
|
||||
|
||||
public void verify(HttpServletRequest request) {
|
||||
String expectedToken = trimToNull(otaProperties.getAuthToken());
|
||||
if (expectedToken == null) {
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "OTA agent token is not configured");
|
||||
}
|
||||
|
||||
String providedToken = extractToken(request);
|
||||
if (!expectedToken.equals(providedToken)) {
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid OTA agent token");
|
||||
}
|
||||
}
|
||||
|
||||
private String extractToken(HttpServletRequest request) {
|
||||
String token = trimToNull(request.getHeader(TOKEN_HEADER));
|
||||
if (token != null) {
|
||||
return token;
|
||||
}
|
||||
String authorization = trimToNull(request.getHeader(AUTHORIZATION_HEADER));
|
||||
if (authorization != null && authorization.regionMatches(true, 0, BEARER_PREFIX, 0, BEARER_PREFIX.length())) {
|
||||
return trimToNull(authorization.substring(BEARER_PREFIX.length()));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String trimToNull(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = value.trim();
|
||||
return trimmed.isEmpty() ? null : trimmed;
|
||||
}
|
||||
}
|
||||
26
src/main/java/com/noblelift/ota/config/OtaConfiguration.java
Normal file
26
src/main/java/com/noblelift/ota/config/OtaConfiguration.java
Normal file
@@ -0,0 +1,26 @@
|
||||
package com.noblelift.ota.config;
|
||||
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
import org.springframework.web.filter.CorsFilter;
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(OtaProperties.class)
|
||||
public class OtaConfiguration {
|
||||
|
||||
@Bean
|
||||
public CorsFilter corsFilter(OtaProperties otaProperties) {
|
||||
CorsConfiguration configuration = new CorsConfiguration();
|
||||
configuration.setAllowedOrigins(otaProperties.getCors().getAllowedOrigins());
|
||||
configuration.setAllowedMethods(otaProperties.getCors().getAllowedMethods());
|
||||
configuration.setAllowedHeaders(otaProperties.getCors().getAllowedHeaders());
|
||||
configuration.setAllowCredentials(otaProperties.getCors().isAllowCredentials());
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", configuration);
|
||||
return new CorsFilter(source);
|
||||
}
|
||||
}
|
||||
77
src/main/java/com/noblelift/ota/config/OtaProperties.java
Normal file
77
src/main/java/com/noblelift/ota/config/OtaProperties.java
Normal file
@@ -0,0 +1,77 @@
|
||||
package com.noblelift.ota.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@ConfigurationProperties(prefix = "ota")
|
||||
public class OtaProperties {
|
||||
|
||||
private String authToken = "dev-token";
|
||||
private long heartbeatTimeoutSeconds = 120;
|
||||
private Cors cors = new Cors();
|
||||
|
||||
public String getAuthToken() {
|
||||
return authToken;
|
||||
}
|
||||
|
||||
public void setAuthToken(String authToken) {
|
||||
this.authToken = authToken;
|
||||
}
|
||||
|
||||
public long getHeartbeatTimeoutSeconds() {
|
||||
return heartbeatTimeoutSeconds;
|
||||
}
|
||||
|
||||
public void setHeartbeatTimeoutSeconds(long heartbeatTimeoutSeconds) {
|
||||
this.heartbeatTimeoutSeconds = heartbeatTimeoutSeconds;
|
||||
}
|
||||
|
||||
public Cors getCors() {
|
||||
return cors;
|
||||
}
|
||||
|
||||
public void setCors(Cors cors) {
|
||||
this.cors = cors;
|
||||
}
|
||||
|
||||
public static class Cors {
|
||||
private List<String> allowedOrigins = new ArrayList<>(List.of("http://localhost:5173", "http://127.0.0.1:5173"));
|
||||
private List<String> allowedMethods = new ArrayList<>(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
||||
private List<String> allowedHeaders = new ArrayList<>(List.of("*"));
|
||||
private boolean allowCredentials = true;
|
||||
|
||||
public List<String> getAllowedOrigins() {
|
||||
return allowedOrigins;
|
||||
}
|
||||
|
||||
public void setAllowedOrigins(List<String> allowedOrigins) {
|
||||
this.allowedOrigins = allowedOrigins;
|
||||
}
|
||||
|
||||
public List<String> getAllowedMethods() {
|
||||
return allowedMethods;
|
||||
}
|
||||
|
||||
public void setAllowedMethods(List<String> allowedMethods) {
|
||||
this.allowedMethods = allowedMethods;
|
||||
}
|
||||
|
||||
public List<String> getAllowedHeaders() {
|
||||
return allowedHeaders;
|
||||
}
|
||||
|
||||
public void setAllowedHeaders(List<String> allowedHeaders) {
|
||||
this.allowedHeaders = allowedHeaders;
|
||||
}
|
||||
|
||||
public boolean isAllowCredentials() {
|
||||
return allowCredentials;
|
||||
}
|
||||
|
||||
public void setAllowCredentials(boolean allowCredentials) {
|
||||
this.allowCredentials = allowCredentials;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/main/java/com/noblelift/ota/config/SaTokenConfigure.java
Normal file
18
src/main/java/com/noblelift/ota/config/SaTokenConfigure.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package com.noblelift.ota.config;
|
||||
|
||||
import cn.dev33.satoken.interceptor.SaInterceptor;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
public class SaTokenConfigure implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(new SaInterceptor(handler -> StpUtil.checkLogin()))
|
||||
.addPathPatterns("/api/releases/**", "/api/vehicles/**", "/api/assignments/**")
|
||||
.excludePathPatterns("/api/auth/login", "/error");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.noblelift.ota.config;
|
||||
|
||||
import cn.dev33.satoken.exception.NotLoginException;
|
||||
import cn.dev33.satoken.exception.NotRoleException;
|
||||
import com.noblelift.ota.common.dto.ApiMessage;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@RestControllerAdvice
|
||||
public class SaTokenExceptionHandler {
|
||||
|
||||
@ExceptionHandler(NotLoginException.class)
|
||||
@ResponseStatus(HttpStatus.UNAUTHORIZED)
|
||||
public ApiMessage handleNotLogin(NotLoginException ex) {
|
||||
return new ApiMessage("UNAUTHORIZED", ex.getMessage(), Instant.now());
|
||||
}
|
||||
|
||||
@ExceptionHandler(NotRoleException.class)
|
||||
@ResponseStatus(HttpStatus.FORBIDDEN)
|
||||
public ApiMessage handleNotRole(NotRoleException ex) {
|
||||
return new ApiMessage("FORBIDDEN", ex.getMessage(), Instant.now());
|
||||
}
|
||||
}
|
||||
15
src/main/java/com/noblelift/ota/domain/AgentStatus.java
Normal file
15
src/main/java/com/noblelift/ota/domain/AgentStatus.java
Normal file
@@ -0,0 +1,15 @@
|
||||
package com.noblelift.ota.domain;
|
||||
|
||||
public enum AgentStatus {
|
||||
IDLE,
|
||||
HAS_UPDATE,
|
||||
WAIT_USER_CONFIRM,
|
||||
BACKING_UP_DATABASE,
|
||||
PULLING_IMAGE,
|
||||
RESTARTING_SERVICE,
|
||||
HEALTH_CHECKING,
|
||||
SUCCESS,
|
||||
FAILED,
|
||||
ROLLBACKING,
|
||||
ROLLED_BACK
|
||||
}
|
||||
12
src/main/java/com/noblelift/ota/domain/TaskStatus.java
Normal file
12
src/main/java/com/noblelift/ota/domain/TaskStatus.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package com.noblelift.ota.domain;
|
||||
|
||||
public enum TaskStatus {
|
||||
PUBLISHED,
|
||||
AVAILABLE,
|
||||
WAITING_CONFIRM,
|
||||
UPGRADING,
|
||||
SUCCESS,
|
||||
FAILED,
|
||||
ROLLED_BACK,
|
||||
SKIPPED
|
||||
}
|
||||
7
src/main/java/com/noblelift/ota/domain/UpgradeMode.java
Normal file
7
src/main/java/com/noblelift/ota/domain/UpgradeMode.java
Normal file
@@ -0,0 +1,7 @@
|
||||
package com.noblelift.ota.domain;
|
||||
|
||||
public enum UpgradeMode {
|
||||
MANUAL_CONFIRM,
|
||||
FORCE_CONFIRM,
|
||||
AUTO
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.noblelift.ota.module.agent.controller;
|
||||
|
||||
import com.noblelift.ota.common.annotation.Log;
|
||||
import com.noblelift.ota.common.dto.ApiMessage;
|
||||
import com.noblelift.ota.config.AgentTokenAuthenticator;
|
||||
import com.noblelift.ota.module.agent.dto.HeartbeatResponse;
|
||||
import com.noblelift.ota.module.agent.dto.UpdateCheckResponse;
|
||||
import com.noblelift.ota.module.agent.param.HeartbeatParam;
|
||||
import com.noblelift.ota.module.agent.param.ReportParam;
|
||||
import com.noblelift.ota.module.agent.param.UpdateCheckParam;
|
||||
import com.noblelift.ota.module.agent.service.AgentService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/agent")
|
||||
@RequiredArgsConstructor
|
||||
public class AgentController {
|
||||
|
||||
private final AgentService agentService;
|
||||
private final AgentTokenAuthenticator agentTokenAuthenticator;
|
||||
|
||||
@Log("Agent心跳")
|
||||
@PostMapping("/heartbeat")
|
||||
public HeartbeatResponse heartbeat(HttpServletRequest request, @Valid @RequestBody HeartbeatParam param) {
|
||||
agentTokenAuthenticator.verify(request);
|
||||
return agentService.heartbeat(param);
|
||||
}
|
||||
|
||||
@Log("Agent检查更新")
|
||||
@PostMapping("/update-check")
|
||||
public UpdateCheckResponse updateCheck(HttpServletRequest request, @Valid @RequestBody UpdateCheckParam param) {
|
||||
agentTokenAuthenticator.verify(request);
|
||||
return agentService.updateCheck(param);
|
||||
}
|
||||
|
||||
@Log("Agent上报结果")
|
||||
@PostMapping("/report")
|
||||
public ApiMessage report(HttpServletRequest request, @Valid @RequestBody ReportParam param) {
|
||||
agentTokenAuthenticator.verify(request);
|
||||
return agentService.report(param);
|
||||
}
|
||||
|
||||
@Log("Agent确认升级")
|
||||
@PostMapping("/confirm")
|
||||
public ApiMessage confirmVehicleUpgrade(HttpServletRequest request, @Valid @RequestBody UpdateCheckParam param) {
|
||||
agentTokenAuthenticator.verify(request);
|
||||
return agentService.confirmUpgrade(param);
|
||||
}
|
||||
|
||||
@Log("Agent稍后升级")
|
||||
@PostMapping("/postpone")
|
||||
@ResponseStatus(HttpStatus.ACCEPTED)
|
||||
public ApiMessage postponeVehicleUpgrade(HttpServletRequest request, @Valid @RequestBody UpdateCheckParam param) {
|
||||
agentTokenAuthenticator.verify(request);
|
||||
return agentService.postponeUpgrade(param);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.noblelift.ota.module.agent.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class HeartbeatResponse {
|
||||
|
||||
private boolean accepted;
|
||||
private Instant serverTime;
|
||||
private String message;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.noblelift.ota.module.agent.dto;
|
||||
|
||||
import com.noblelift.ota.common.model.ReleaseManifest;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class UpdateCheckResponse {
|
||||
|
||||
private boolean hasUpdate;
|
||||
private ReleaseManifest manifest;
|
||||
private String message;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.noblelift.ota.module.agent.param;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||
import com.noblelift.ota.domain.AgentStatus;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
public class HeartbeatParam {
|
||||
|
||||
@NotBlank
|
||||
@JsonAlias("vehicle_id")
|
||||
private String vehicleId;
|
||||
|
||||
@JsonAlias("vin")
|
||||
private String vin;
|
||||
|
||||
@NotBlank
|
||||
@JsonAlias("current_release")
|
||||
private String currentRelease;
|
||||
|
||||
@JsonAlias({"agent_status", "status"})
|
||||
private AgentStatus agentStatus;
|
||||
|
||||
@JsonAlias({"target_release", "release_version"})
|
||||
private String targetRelease;
|
||||
|
||||
@JsonAlias("last_result")
|
||||
private String lastResult;
|
||||
|
||||
private Map<String, String> images;
|
||||
|
||||
@JsonAlias("backup_file")
|
||||
private String backupFile;
|
||||
|
||||
@JsonAlias("ip_address")
|
||||
private String ipAddress;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.noblelift.ota.module.agent.param;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||
import com.noblelift.ota.domain.AgentStatus;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
public class ReportParam {
|
||||
|
||||
@NotBlank
|
||||
@JsonAlias("vehicle_id")
|
||||
private String vehicleId;
|
||||
|
||||
@JsonAlias("vin")
|
||||
private String vin;
|
||||
|
||||
@JsonAlias("current_release")
|
||||
private String currentRelease;
|
||||
|
||||
@JsonAlias({"release_version", "target_release"})
|
||||
private String releaseVersion;
|
||||
|
||||
@JsonAlias({"agent_status", "status"})
|
||||
private AgentStatus agentStatus;
|
||||
|
||||
private boolean success;
|
||||
|
||||
@JsonAlias({"message", "detail"})
|
||||
private String message;
|
||||
|
||||
private Map<String, String> images;
|
||||
|
||||
@JsonAlias("backup_file")
|
||||
private String backupFile;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.noblelift.ota.module.agent.param;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class UpdateCheckParam {
|
||||
|
||||
@NotBlank
|
||||
@JsonAlias("vehicle_id")
|
||||
private String vehicleId;
|
||||
|
||||
@JsonAlias("vin")
|
||||
private String vin;
|
||||
|
||||
@NotBlank
|
||||
@JsonAlias("current_release")
|
||||
private String currentRelease;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.noblelift.ota.module.agent.service;
|
||||
|
||||
import com.noblelift.ota.common.dto.ApiMessage;
|
||||
import com.noblelift.ota.module.agent.dto.HeartbeatResponse;
|
||||
import com.noblelift.ota.module.agent.dto.UpdateCheckResponse;
|
||||
import com.noblelift.ota.module.agent.param.HeartbeatParam;
|
||||
import com.noblelift.ota.module.agent.param.ReportParam;
|
||||
import com.noblelift.ota.module.agent.param.UpdateCheckParam;
|
||||
|
||||
public interface AgentService {
|
||||
|
||||
HeartbeatResponse heartbeat(HeartbeatParam param);
|
||||
|
||||
UpdateCheckResponse updateCheck(UpdateCheckParam param);
|
||||
|
||||
ApiMessage report(ReportParam param);
|
||||
|
||||
ApiMessage confirmUpgrade(UpdateCheckParam param);
|
||||
|
||||
ApiMessage postponeUpgrade(UpdateCheckParam param);
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package com.noblelift.ota.module.agent.service.impl;
|
||||
|
||||
import com.noblelift.ota.common.dto.ApiMessage;
|
||||
import com.noblelift.ota.common.model.ReleaseManifest;
|
||||
import com.noblelift.ota.common.model.VehicleAssignment;
|
||||
import com.noblelift.ota.common.model.VehicleInfo;
|
||||
import com.noblelift.ota.common.util.ReleaseVersionComparator;
|
||||
import com.noblelift.ota.domain.AgentStatus;
|
||||
import com.noblelift.ota.domain.TaskStatus;
|
||||
import com.noblelift.ota.module.agent.dto.HeartbeatResponse;
|
||||
import com.noblelift.ota.module.agent.dto.UpdateCheckResponse;
|
||||
import com.noblelift.ota.module.agent.param.HeartbeatParam;
|
||||
import com.noblelift.ota.module.agent.param.ReportParam;
|
||||
import com.noblelift.ota.module.agent.param.UpdateCheckParam;
|
||||
import com.noblelift.ota.module.agent.service.AgentService;
|
||||
import com.noblelift.ota.module.assignment.service.AssignmentService;
|
||||
import com.noblelift.ota.module.release.service.ReleaseService;
|
||||
import com.noblelift.ota.module.vehicle.service.VehicleService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AgentServiceImpl implements AgentService {
|
||||
|
||||
private static final int MAX_LAST_MESSAGE_LENGTH = 500;
|
||||
|
||||
private final VehicleService vehicleService;
|
||||
private final AssignmentService assignmentService;
|
||||
private final ReleaseService releaseService;
|
||||
|
||||
@Override
|
||||
public HeartbeatResponse heartbeat(HeartbeatParam param) {
|
||||
AgentStatus agentStatus = resolveAgentStatus(param.getAgentStatus());
|
||||
vehicleService.saveHeartbeat(
|
||||
param.getVehicleId(),
|
||||
param.getVin(),
|
||||
param.getCurrentRelease(),
|
||||
agentStatus,
|
||||
param.getTargetRelease(),
|
||||
param.getLastResult(),
|
||||
param.getImages(),
|
||||
param.getBackupFile()
|
||||
);
|
||||
return new HeartbeatResponse(true, Instant.now(), "heartbeat accepted");
|
||||
}
|
||||
|
||||
@Override
|
||||
public UpdateCheckResponse updateCheck(UpdateCheckParam param) {
|
||||
VehicleAssignment assignment;
|
||||
try {
|
||||
assignment = assignmentService.getAssignment(param.getVehicleId());
|
||||
} catch (IllegalArgumentException ex) {
|
||||
return new UpdateCheckResponse(false, null, "No release assigned");
|
||||
}
|
||||
|
||||
ReleaseManifest manifest = releaseService.getRelease(assignment.getReleaseVersion());
|
||||
int versionCompare = ReleaseVersionComparator.compare(manifest.getReleaseVersion(), param.getCurrentRelease());
|
||||
if (versionCompare == 0) {
|
||||
return new UpdateCheckResponse(false, null, "Already on latest assigned release");
|
||||
}
|
||||
if (versionCompare < 0) {
|
||||
return new UpdateCheckResponse(false, null, "Assigned release is lower than current release");
|
||||
}
|
||||
|
||||
if (assignment.getTaskStatus() == TaskStatus.AVAILABLE || assignment.getTaskStatus() == TaskStatus.SKIPPED) {
|
||||
assignment.setTaskStatus(TaskStatus.WAITING_CONFIRM);
|
||||
assignment.setPromptedAt(assignment.getPromptedAt() == null ? Instant.now() : assignment.getPromptedAt());
|
||||
assignment.setLastMessage("Update is visible to vehicle");
|
||||
assignmentService.saveAssignment(assignment);
|
||||
}
|
||||
return new UpdateCheckResponse(true, manifest, "Update available");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiMessage report(ReportParam param) {
|
||||
VehicleAssignment assignment = assignmentService.getAssignment(param.getVehicleId());
|
||||
AgentStatus agentStatus = resolveAgentStatus(param.getAgentStatus());
|
||||
boolean success = resolveSuccess(param, agentStatus);
|
||||
TaskStatus status = mapTaskStatus(success, agentStatus);
|
||||
Instant now = Instant.now();
|
||||
String releaseVersion = resolveReleaseVersion(param, assignment);
|
||||
assignment.setReleaseVersion(releaseVersion);
|
||||
assignment.setTaskStatus(status);
|
||||
if (assignment.getConfirmedAt() == null && status == TaskStatus.UPGRADING) {
|
||||
assignment.setConfirmedAt(now);
|
||||
}
|
||||
if (assignment.getStartedAt() == null && status == TaskStatus.UPGRADING) {
|
||||
assignment.setStartedAt(now);
|
||||
}
|
||||
if (status == TaskStatus.SUCCESS || status == TaskStatus.FAILED || status == TaskStatus.ROLLED_BACK) {
|
||||
assignment.setFinishedAt(now);
|
||||
}
|
||||
assignment.setLastMessage(truncateLastMessage(param.getMessage()));
|
||||
assignmentService.saveAssignment(assignment);
|
||||
|
||||
vehicleService.saveHeartbeat(
|
||||
param.getVehicleId(),
|
||||
param.getVin(),
|
||||
resolveCurrentRelease(param, success, releaseVersion),
|
||||
agentStatus,
|
||||
releaseVersion,
|
||||
truncateLastMessage(param.getMessage()),
|
||||
param.getImages(),
|
||||
param.getBackupFile()
|
||||
);
|
||||
return new ApiMessage("OK", "report accepted", Instant.now());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiMessage confirmUpgrade(UpdateCheckParam param) {
|
||||
assignmentService.markConfirmed(param.getVehicleId());
|
||||
return new ApiMessage("OK", "vehicle upgrade confirmed", Instant.now());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiMessage postponeUpgrade(UpdateCheckParam param) {
|
||||
assignmentService.markPostponed(param.getVehicleId(), "vehicle postponed upgrade");
|
||||
return new ApiMessage("OK", "vehicle upgrade postponed", Instant.now());
|
||||
}
|
||||
|
||||
private TaskStatus mapTaskStatus(boolean success, AgentStatus agentStatus) {
|
||||
if (success && agentStatus == AgentStatus.SUCCESS) {
|
||||
return TaskStatus.SUCCESS;
|
||||
}
|
||||
if (agentStatus == AgentStatus.ROLLBACKING || agentStatus == AgentStatus.ROLLED_BACK) {
|
||||
return TaskStatus.ROLLED_BACK;
|
||||
}
|
||||
if (agentStatus == AgentStatus.BACKING_UP_DATABASE
|
||||
|| agentStatus == AgentStatus.PULLING_IMAGE
|
||||
|| agentStatus == AgentStatus.RESTARTING_SERVICE
|
||||
|| agentStatus == AgentStatus.HEALTH_CHECKING) {
|
||||
return TaskStatus.UPGRADING;
|
||||
}
|
||||
return success ? TaskStatus.SUCCESS : TaskStatus.FAILED;
|
||||
}
|
||||
|
||||
private AgentStatus resolveAgentStatus(AgentStatus agentStatus) {
|
||||
return agentStatus == null ? AgentStatus.IDLE : agentStatus;
|
||||
}
|
||||
|
||||
private boolean resolveSuccess(ReportParam param, AgentStatus agentStatus) {
|
||||
if (agentStatus == AgentStatus.SUCCESS) {
|
||||
return true;
|
||||
}
|
||||
if (agentStatus == AgentStatus.FAILED || agentStatus == AgentStatus.ROLLBACKING || agentStatus == AgentStatus.ROLLED_BACK) {
|
||||
return false;
|
||||
}
|
||||
return param.isSuccess();
|
||||
}
|
||||
|
||||
private String resolveReleaseVersion(ReportParam param, VehicleAssignment assignment) {
|
||||
if (param.getReleaseVersion() != null && !param.getReleaseVersion().isBlank()) {
|
||||
return param.getReleaseVersion();
|
||||
}
|
||||
if (param.getCurrentRelease() != null && !param.getCurrentRelease().isBlank()) {
|
||||
return param.getCurrentRelease();
|
||||
}
|
||||
return assignment.getReleaseVersion();
|
||||
}
|
||||
|
||||
private String resolveCurrentRelease(ReportParam param, boolean success, String releaseVersion) {
|
||||
if (param.getCurrentRelease() != null && !param.getCurrentRelease().isBlank()) {
|
||||
return param.getCurrentRelease();
|
||||
}
|
||||
return success ? releaseVersion : null;
|
||||
}
|
||||
|
||||
private String truncateLastMessage(String message) {
|
||||
if (message == null || message.length() <= MAX_LAST_MESSAGE_LENGTH) {
|
||||
return message;
|
||||
}
|
||||
return message.substring(0, MAX_LAST_MESSAGE_LENGTH - 3) + "...";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.noblelift.ota.module.assignment.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckRole;
|
||||
import com.noblelift.ota.common.annotation.Log;
|
||||
import com.noblelift.ota.module.assignment.dto.AssignmentView;
|
||||
import com.noblelift.ota.module.assignment.param.AssignReleaseParam;
|
||||
import com.noblelift.ota.module.assignment.service.AssignmentService;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/assignments")
|
||||
@RequiredArgsConstructor
|
||||
public class AssignmentController {
|
||||
|
||||
private final AssignmentService assignmentService;
|
||||
|
||||
@SaCheckRole(value = {"SUPER_ADMIN", "ADMIN"}, mode = cn.dev33.satoken.annotation.SaMode.OR)
|
||||
@Log("分配版本")
|
||||
@PostMapping
|
||||
public List<AssignmentView> assignRelease(@Valid @RequestBody AssignReleaseParam param) {
|
||||
return assignmentService.assignRelease(param.getReleaseVersion(), param.getVehicleIds()).stream()
|
||||
.map(this::toView)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@SaCheckRole(value = {"SUPER_ADMIN", "ADMIN"}, mode = cn.dev33.satoken.annotation.SaMode.OR)
|
||||
@Log("修改任务")
|
||||
@PutMapping("/{vehicleId}")
|
||||
public AssignmentView updateAssignment(@PathVariable String vehicleId, @Valid @RequestBody AssignReleaseParam param) {
|
||||
return toView(assignmentService.updateAssignment(vehicleId, param));
|
||||
}
|
||||
|
||||
@SaCheckRole(value = {"SUPER_ADMIN", "ADMIN"}, mode = cn.dev33.satoken.annotation.SaMode.OR)
|
||||
@Log("删除任务")
|
||||
@DeleteMapping("/{vehicleId}")
|
||||
public void deleteAssignment(@PathVariable String vehicleId) {
|
||||
assignmentService.deleteAssignment(vehicleId);
|
||||
}
|
||||
|
||||
@SaCheckRole(value = {"SUPER_ADMIN", "ADMIN"}, mode = cn.dev33.satoken.annotation.SaMode.OR)
|
||||
@Log("查询任务列表")
|
||||
@GetMapping
|
||||
public List<AssignmentView> listAssignments() {
|
||||
return assignmentService.listAssignments().stream()
|
||||
.map(this::toView)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private AssignmentView toView(com.noblelift.ota.common.model.VehicleAssignment item) {
|
||||
return new AssignmentView(
|
||||
item.getVehicleId(),
|
||||
item.getReleaseVersion(),
|
||||
item.getTaskStatus(),
|
||||
item.getPromptedAt(),
|
||||
item.getConfirmedAt(),
|
||||
item.getStartedAt(),
|
||||
item.getFinishedAt(),
|
||||
item.getPostponeCount(),
|
||||
item.getLastMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.noblelift.ota.module.assignment.dto;
|
||||
|
||||
import com.noblelift.ota.domain.TaskStatus;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AssignmentView {
|
||||
|
||||
private String vehicleId;
|
||||
private String releaseVersion;
|
||||
private TaskStatus taskStatus;
|
||||
private Instant promptedAt;
|
||||
private Instant confirmedAt;
|
||||
private Instant startedAt;
|
||||
private Instant finishedAt;
|
||||
private Integer postponeCount;
|
||||
private String lastMessage;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.noblelift.ota.module.assignment.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Data
|
||||
@TableName("ota_vehicle_assignment")
|
||||
public class VehicleAssignmentEntity {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private String vehicleId;
|
||||
private String releaseVersion;
|
||||
private String taskStatus;
|
||||
private Instant promptedAt;
|
||||
private Instant confirmedAt;
|
||||
private Instant startedAt;
|
||||
private Instant finishedAt;
|
||||
private Integer postponeCount;
|
||||
private String lastMessage;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.noblelift.ota.module.assignment.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.noblelift.ota.module.assignment.entity.VehicleAssignmentEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface VehicleAssignmentMapper extends BaseMapper<VehicleAssignmentEntity> {
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.noblelift.ota.module.assignment.param;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class AssignReleaseParam {
|
||||
|
||||
@NotBlank
|
||||
private String releaseVersion;
|
||||
|
||||
private List<String> vehicleIds;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.noblelift.ota.module.assignment.service;
|
||||
|
||||
import com.noblelift.ota.common.model.VehicleAssignment;
|
||||
import com.noblelift.ota.module.assignment.param.AssignReleaseParam;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface AssignmentService {
|
||||
|
||||
List<VehicleAssignment> listAssignments();
|
||||
|
||||
List<VehicleAssignment> assignRelease(String releaseVersion, List<String> vehicleIds);
|
||||
|
||||
VehicleAssignment updateAssignment(String vehicleId, AssignReleaseParam param);
|
||||
|
||||
void deleteAssignment(String vehicleId);
|
||||
|
||||
VehicleAssignment getAssignment(String vehicleId);
|
||||
|
||||
VehicleAssignment markPostponed(String vehicleId, String reason);
|
||||
|
||||
VehicleAssignment markConfirmed(String vehicleId);
|
||||
|
||||
VehicleAssignment saveAssignment(VehicleAssignment assignment);
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
package com.noblelift.ota.module.assignment.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.noblelift.ota.common.model.VehicleAssignment;
|
||||
import com.noblelift.ota.common.model.VehicleInfo;
|
||||
import com.noblelift.ota.common.util.ReleaseVersionComparator;
|
||||
import com.noblelift.ota.domain.TaskStatus;
|
||||
import com.noblelift.ota.module.assignment.entity.VehicleAssignmentEntity;
|
||||
import com.noblelift.ota.module.assignment.mapper.VehicleAssignmentMapper;
|
||||
import com.noblelift.ota.module.assignment.param.AssignReleaseParam;
|
||||
import com.noblelift.ota.module.assignment.service.AssignmentService;
|
||||
import com.noblelift.ota.module.release.service.ReleaseService;
|
||||
import com.noblelift.ota.module.vehicle.service.VehicleService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AssignmentServiceImpl implements AssignmentService {
|
||||
|
||||
private final VehicleAssignmentMapper vehicleAssignmentMapper;
|
||||
private final VehicleService vehicleService;
|
||||
private final ReleaseService releaseService;
|
||||
|
||||
@Override
|
||||
public List<VehicleAssignment> listAssignments() {
|
||||
return vehicleAssignmentMapper.selectList(new LambdaQueryWrapper<VehicleAssignmentEntity>()
|
||||
.orderByAsc(VehicleAssignmentEntity::getVehicleId))
|
||||
.stream()
|
||||
.map(this::toModel)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VehicleAssignment> assignRelease(String releaseVersion, List<String> vehicleIds) {
|
||||
releaseService.getRelease(releaseVersion);
|
||||
return vehicleIds.stream()
|
||||
.map(vehicleId -> saveAssignment(buildAssignment(releaseVersion, vehicleId)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public VehicleAssignment updateAssignment(String vehicleId, AssignReleaseParam param) {
|
||||
VehicleAssignmentEntity existing = getEntity(vehicleId);
|
||||
VehicleAssignment assignment = buildAssignment(param.getReleaseVersion(), vehicleId);
|
||||
assignment.setTaskStatus(existing.getTaskStatus() == null ? TaskStatus.AVAILABLE : TaskStatus.valueOf(existing.getTaskStatus()));
|
||||
assignment.setPromptedAt(existing.getPromptedAt());
|
||||
assignment.setConfirmedAt(existing.getConfirmedAt());
|
||||
assignment.setStartedAt(existing.getStartedAt());
|
||||
assignment.setFinishedAt(existing.getFinishedAt());
|
||||
assignment.setPostponeCount(existing.getPostponeCount() == null ? 0 : existing.getPostponeCount());
|
||||
if (existing.getLastMessage() != null && !existing.getLastMessage().isBlank()) {
|
||||
assignment.setLastMessage(existing.getLastMessage());
|
||||
}
|
||||
return saveAssignment(assignment, existing.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteAssignment(String vehicleId) {
|
||||
VehicleAssignmentEntity existing = getEntity(vehicleId);
|
||||
vehicleAssignmentMapper.deleteById(existing.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public VehicleAssignment getAssignment(String vehicleId) {
|
||||
return toModel(getEntity(vehicleId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public VehicleAssignment markPostponed(String vehicleId, String reason) {
|
||||
VehicleAssignment assignment = getAssignment(vehicleId);
|
||||
assignment.setTaskStatus(TaskStatus.SKIPPED);
|
||||
assignment.setPromptedAt(assignment.getPromptedAt() == null ? Instant.now() : assignment.getPromptedAt());
|
||||
assignment.setPostponeCount((assignment.getPostponeCount() == null ? 0 : assignment.getPostponeCount()) + 1);
|
||||
assignment.setLastMessage(reason);
|
||||
return saveAssignment(assignment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VehicleAssignment markConfirmed(String vehicleId) {
|
||||
VehicleAssignment assignment = getAssignment(vehicleId);
|
||||
Instant now = Instant.now();
|
||||
assignment.setTaskStatus(TaskStatus.UPGRADING);
|
||||
assignment.setPromptedAt(assignment.getPromptedAt() == null ? now : assignment.getPromptedAt());
|
||||
assignment.setConfirmedAt(now);
|
||||
assignment.setStartedAt(now);
|
||||
assignment.setLastMessage("User confirmed upgrade");
|
||||
return saveAssignment(assignment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VehicleAssignment saveAssignment(VehicleAssignment assignment) {
|
||||
VehicleAssignmentEntity existing = vehicleAssignmentMapper.selectOne(new LambdaQueryWrapper<VehicleAssignmentEntity>()
|
||||
.eq(VehicleAssignmentEntity::getVehicleId, assignment.getVehicleId())
|
||||
.last("limit 1"));
|
||||
return saveAssignment(assignment, existing == null ? null : existing.getId());
|
||||
}
|
||||
|
||||
private VehicleAssignment saveAssignment(VehicleAssignment assignment, Long existingId) {
|
||||
VehicleAssignmentEntity entity = toEntity(assignment);
|
||||
if (existingId == null) {
|
||||
vehicleAssignmentMapper.insert(entity);
|
||||
} else {
|
||||
entity.setId(existingId);
|
||||
vehicleAssignmentMapper.updateById(entity);
|
||||
}
|
||||
return assignment;
|
||||
}
|
||||
|
||||
private VehicleAssignment buildAssignment(String releaseVersion, String vehicleId) {
|
||||
VehicleInfo vehicle = vehicleService.listVehicles().stream()
|
||||
.filter(item -> item.getVehicleId().equals(vehicleId))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("Vehicle not found: " + vehicleId));
|
||||
|
||||
int versionCompare = ReleaseVersionComparator.compare(releaseVersion, vehicle.getCurrentRelease());
|
||||
if (versionCompare <= 0) {
|
||||
throw new IllegalArgumentException("Assigned release must be higher than current release for vehicle: " + vehicleId);
|
||||
}
|
||||
|
||||
return VehicleAssignment.builder()
|
||||
.vehicleId(vehicleId)
|
||||
.releaseVersion(releaseVersion)
|
||||
.taskStatus(TaskStatus.AVAILABLE)
|
||||
.postponeCount(0)
|
||||
.lastMessage("Release available for upgrade")
|
||||
.build();
|
||||
}
|
||||
|
||||
private VehicleAssignmentEntity getEntity(String vehicleId) {
|
||||
VehicleAssignmentEntity entity = vehicleAssignmentMapper.selectOne(new LambdaQueryWrapper<VehicleAssignmentEntity>()
|
||||
.eq(VehicleAssignmentEntity::getVehicleId, vehicleId)
|
||||
.last("limit 1"));
|
||||
if (entity == null) {
|
||||
throw new IllegalArgumentException("Assignment not found for vehicle: " + vehicleId);
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
private VehicleAssignment toModel(VehicleAssignmentEntity entity) {
|
||||
return VehicleAssignment.builder()
|
||||
.vehicleId(entity.getVehicleId())
|
||||
.releaseVersion(entity.getReleaseVersion())
|
||||
.taskStatus(TaskStatus.valueOf(entity.getTaskStatus()))
|
||||
.promptedAt(entity.getPromptedAt())
|
||||
.confirmedAt(entity.getConfirmedAt())
|
||||
.startedAt(entity.getStartedAt())
|
||||
.finishedAt(entity.getFinishedAt())
|
||||
.postponeCount(entity.getPostponeCount() == null ? 0 : entity.getPostponeCount())
|
||||
.lastMessage(entity.getLastMessage())
|
||||
.build();
|
||||
}
|
||||
|
||||
private VehicleAssignmentEntity toEntity(VehicleAssignment assignment) {
|
||||
VehicleAssignmentEntity entity = new VehicleAssignmentEntity();
|
||||
entity.setVehicleId(assignment.getVehicleId());
|
||||
entity.setReleaseVersion(assignment.getReleaseVersion());
|
||||
entity.setTaskStatus(assignment.getTaskStatus().name());
|
||||
entity.setPromptedAt(assignment.getPromptedAt());
|
||||
entity.setConfirmedAt(assignment.getConfirmedAt());
|
||||
entity.setStartedAt(assignment.getStartedAt());
|
||||
entity.setFinishedAt(assignment.getFinishedAt());
|
||||
entity.setPostponeCount(assignment.getPostponeCount());
|
||||
entity.setLastMessage(assignment.getLastMessage());
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.noblelift.ota.module.auth.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckLogin;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import com.noblelift.ota.common.annotation.Log;
|
||||
import com.noblelift.ota.common.dto.ApiMessage;
|
||||
import com.noblelift.ota.module.auth.dto.LoginResponse;
|
||||
import com.noblelift.ota.module.auth.param.LoginParam;
|
||||
import com.noblelift.ota.module.auth.service.AuthService;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/auth")
|
||||
@RequiredArgsConstructor
|
||||
public class AuthController {
|
||||
|
||||
private final AuthService authService;
|
||||
|
||||
@Log("用户登录")
|
||||
@PostMapping("/login")
|
||||
public LoginResponse login(@Valid @RequestBody LoginParam param) {
|
||||
return authService.login(param);
|
||||
}
|
||||
|
||||
@Log("用户退出")
|
||||
@SaCheckLogin
|
||||
@PostMapping("/logout")
|
||||
public ApiMessage logout() {
|
||||
authService.logout();
|
||||
return new ApiMessage("OK", "logout success", Instant.now());
|
||||
}
|
||||
|
||||
@Log("查询当前用户")
|
||||
@SaCheckLogin
|
||||
@GetMapping("/me")
|
||||
public Map<String, Object> currentUser() {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("loginId", StpUtil.getLoginIdAsLong());
|
||||
result.put("roles", authService.getCurrentRoles());
|
||||
result.put("token", StpUtil.getTokenValue());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.noblelift.ota.module.auth.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class LoginResponse {
|
||||
|
||||
private String token;
|
||||
private Long userId;
|
||||
private String username;
|
||||
private String nickname;
|
||||
private List<String> roles;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.noblelift.ota.module.auth.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Data
|
||||
@TableName("sys_role")
|
||||
public class SysRoleEntity {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private String roleCode;
|
||||
private String roleName;
|
||||
private Integer status;
|
||||
private Instant createdAt;
|
||||
private Instant updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.noblelift.ota.module.auth.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Data
|
||||
@TableName("sys_user")
|
||||
public class SysUserEntity {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private String username;
|
||||
private String password;
|
||||
private String nickname;
|
||||
private Integer status;
|
||||
private Instant createdAt;
|
||||
private Instant updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.noblelift.ota.module.auth.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Data
|
||||
@TableName("sys_user_role")
|
||||
public class SysUserRoleEntity {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Long userId;
|
||||
private Long roleId;
|
||||
private Instant createdAt;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.noblelift.ota.module.auth.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.noblelift.ota.module.auth.entity.SysRoleEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface SysRoleMapper extends BaseMapper<SysRoleEntity> {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.noblelift.ota.module.auth.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.noblelift.ota.module.auth.entity.SysUserEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface SysUserMapper extends BaseMapper<SysUserEntity> {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.noblelift.ota.module.auth.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.noblelift.ota.module.auth.entity.SysUserRoleEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface SysUserRoleMapper extends BaseMapper<SysUserRoleEntity> {
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.noblelift.ota.module.auth.param;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class LoginParam {
|
||||
|
||||
@NotBlank
|
||||
private String username;
|
||||
|
||||
@NotBlank
|
||||
private String password;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.noblelift.ota.module.auth.service;
|
||||
|
||||
import com.noblelift.ota.module.auth.dto.LoginResponse;
|
||||
import com.noblelift.ota.module.auth.param.LoginParam;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface AuthService {
|
||||
|
||||
LoginResponse login(LoginParam param);
|
||||
|
||||
void logout();
|
||||
|
||||
List<String> getCurrentRoles();
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.noblelift.ota.module.auth.service.impl;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.noblelift.ota.module.auth.dto.LoginResponse;
|
||||
import com.noblelift.ota.module.auth.entity.SysRoleEntity;
|
||||
import com.noblelift.ota.module.auth.entity.SysUserEntity;
|
||||
import com.noblelift.ota.module.auth.entity.SysUserRoleEntity;
|
||||
import com.noblelift.ota.module.auth.mapper.SysRoleMapper;
|
||||
import com.noblelift.ota.module.auth.mapper.SysUserMapper;
|
||||
import com.noblelift.ota.module.auth.mapper.SysUserRoleMapper;
|
||||
import com.noblelift.ota.module.auth.param.LoginParam;
|
||||
import com.noblelift.ota.module.auth.service.AuthService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.DigestUtils;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AuthServiceImpl implements AuthService {
|
||||
|
||||
private final SysUserMapper sysUserMapper;
|
||||
private final SysRoleMapper sysRoleMapper;
|
||||
private final SysUserRoleMapper sysUserRoleMapper;
|
||||
|
||||
@Override
|
||||
public LoginResponse login(LoginParam param) {
|
||||
SysUserEntity user = sysUserMapper.selectOne(new LambdaQueryWrapper<SysUserEntity>()
|
||||
.eq(SysUserEntity::getUsername, param.getUsername())
|
||||
.last("limit 1"));
|
||||
if (user == null || user.getStatus() == null || user.getStatus() != 1) {
|
||||
throw new IllegalArgumentException("用户不存在或已禁用");
|
||||
}
|
||||
|
||||
String encodedPassword = DigestUtils.md5DigestAsHex(param.getPassword().getBytes(StandardCharsets.UTF_8));
|
||||
if (!encodedPassword.equals(user.getPassword())) {
|
||||
throw new IllegalArgumentException("用户名或密码错误");
|
||||
}
|
||||
|
||||
StpUtil.login(user.getId());
|
||||
List<String> roles = getRolesByUserId(user.getId());
|
||||
return new LoginResponse(StpUtil.getTokenValue(), user.getId(), user.getUsername(), user.getNickname(), roles);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logout() {
|
||||
StpUtil.logout();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getCurrentRoles() {
|
||||
Object loginId = StpUtil.getLoginIdDefaultNull();
|
||||
if (loginId == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return getRolesByUserId(Long.valueOf(String.valueOf(loginId)));
|
||||
}
|
||||
|
||||
private List<String> getRolesByUserId(Long userId) {
|
||||
List<Long> roleIds = sysUserRoleMapper.selectList(new LambdaQueryWrapper<SysUserRoleEntity>()
|
||||
.eq(SysUserRoleEntity::getUserId, userId))
|
||||
.stream()
|
||||
.map(SysUserRoleEntity::getRoleId)
|
||||
.toList();
|
||||
if (roleIds.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return sysRoleMapper.selectBatchIds(roleIds).stream()
|
||||
.map(SysRoleEntity::getRoleCode)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.noblelift.ota.module.auth.service.impl;
|
||||
|
||||
import cn.dev33.satoken.stp.StpInterface;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.noblelift.ota.module.auth.entity.SysRoleEntity;
|
||||
import com.noblelift.ota.module.auth.entity.SysUserRoleEntity;
|
||||
import com.noblelift.ota.module.auth.mapper.SysRoleMapper;
|
||||
import com.noblelift.ota.module.auth.mapper.SysUserRoleMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class StpInterfaceImpl implements StpInterface {
|
||||
|
||||
private final SysUserRoleMapper sysUserRoleMapper;
|
||||
private final SysRoleMapper sysRoleMapper;
|
||||
|
||||
@Override
|
||||
public List<String> getPermissionList(Object loginId, String loginType) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getRoleList(Object loginId, String loginType) {
|
||||
Long userId = Long.valueOf(String.valueOf(loginId));
|
||||
List<Long> roleIds = sysUserRoleMapper.selectList(new LambdaQueryWrapper<SysUserRoleEntity>()
|
||||
.eq(SysUserRoleEntity::getUserId, userId))
|
||||
.stream()
|
||||
.map(SysUserRoleEntity::getRoleId)
|
||||
.toList();
|
||||
if (roleIds.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return sysRoleMapper.selectBatchIds(roleIds).stream()
|
||||
.map(SysRoleEntity::getRoleCode)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.noblelift.ota.module.release.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckRole;
|
||||
import com.noblelift.ota.common.annotation.Log;
|
||||
import com.noblelift.ota.module.release.dto.ReleaseView;
|
||||
import com.noblelift.ota.module.release.param.CreateReleaseParam;
|
||||
import com.noblelift.ota.module.release.service.ReleaseService;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/releases")
|
||||
@RequiredArgsConstructor
|
||||
public class ReleaseController {
|
||||
|
||||
private final ReleaseService releaseService;
|
||||
|
||||
@SaCheckRole(value = {"SUPER_ADMIN", "ADMIN"}, mode = cn.dev33.satoken.annotation.SaMode.OR)
|
||||
@Log("创建版本")
|
||||
@PostMapping
|
||||
public ReleaseView createRelease(@Valid @RequestBody CreateReleaseParam param) {
|
||||
return new ReleaseView(releaseService.createRelease(param));
|
||||
}
|
||||
|
||||
@SaCheckRole(value = {"SUPER_ADMIN", "ADMIN"}, mode = cn.dev33.satoken.annotation.SaMode.OR)
|
||||
@Log("修改版本")
|
||||
@PutMapping("/{releaseVersion}")
|
||||
public ReleaseView updateRelease(@PathVariable String releaseVersion, @Valid @RequestBody CreateReleaseParam param) {
|
||||
return new ReleaseView(releaseService.updateRelease(releaseVersion, param));
|
||||
}
|
||||
|
||||
@SaCheckRole(value = {"SUPER_ADMIN", "ADMIN"}, mode = cn.dev33.satoken.annotation.SaMode.OR)
|
||||
@Log("删除版本")
|
||||
@DeleteMapping("/{releaseVersion}")
|
||||
public void deleteRelease(@PathVariable String releaseVersion) {
|
||||
releaseService.deleteRelease(releaseVersion);
|
||||
}
|
||||
|
||||
@SaCheckRole(value = {"SUPER_ADMIN", "ADMIN"}, mode = cn.dev33.satoken.annotation.SaMode.OR)
|
||||
@Log("查询版本列表")
|
||||
@GetMapping
|
||||
public List<ReleaseView> listReleases() {
|
||||
return releaseService.listReleases().stream()
|
||||
.map(ReleaseView::new)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.noblelift.ota.module.release.dto;
|
||||
|
||||
import com.noblelift.ota.common.model.ReleaseManifest;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ReleaseView {
|
||||
|
||||
private ReleaseManifest manifest;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.noblelift.ota.module.release.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Data
|
||||
@TableName("ota_release")
|
||||
public class ReleaseEntity {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private String releaseVersion;
|
||||
private String releaseNotes;
|
||||
private String upgradeMode;
|
||||
private Instant publishedAt;
|
||||
private Boolean parkingRequired;
|
||||
private String componentsJson;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.noblelift.ota.module.release.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.noblelift.ota.module.release.entity.ReleaseEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface ReleaseMapper extends BaseMapper<ReleaseEntity> {
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.noblelift.ota.module.release.param;
|
||||
|
||||
import com.noblelift.ota.domain.UpgradeMode;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
public class CreateReleaseParam {
|
||||
|
||||
@NotBlank
|
||||
private String releaseVersion;
|
||||
|
||||
@NotBlank
|
||||
private String releaseNotes;
|
||||
|
||||
private UpgradeMode upgradeMode;
|
||||
|
||||
private Boolean parkingRequired;
|
||||
|
||||
@NotEmpty
|
||||
private Map<String, Object> components;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.noblelift.ota.module.release.service;
|
||||
|
||||
import com.noblelift.ota.common.model.ReleaseManifest;
|
||||
import com.noblelift.ota.module.release.param.CreateReleaseParam;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ReleaseService {
|
||||
|
||||
ReleaseManifest createRelease(CreateReleaseParam param);
|
||||
|
||||
ReleaseManifest updateRelease(String releaseVersion, CreateReleaseParam param);
|
||||
|
||||
void deleteRelease(String releaseVersion);
|
||||
|
||||
List<ReleaseManifest> listReleases();
|
||||
|
||||
ReleaseManifest getRelease(String releaseVersion);
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package com.noblelift.ota.module.release.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.noblelift.ota.common.model.ReleaseManifest;
|
||||
import com.noblelift.ota.domain.UpgradeMode;
|
||||
import com.noblelift.ota.module.assignment.entity.VehicleAssignmentEntity;
|
||||
import com.noblelift.ota.module.assignment.mapper.VehicleAssignmentMapper;
|
||||
import com.noblelift.ota.module.release.entity.ReleaseEntity;
|
||||
import com.noblelift.ota.module.release.mapper.ReleaseMapper;
|
||||
import com.noblelift.ota.module.release.param.CreateReleaseParam;
|
||||
import com.noblelift.ota.module.release.service.ReleaseService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ReleaseServiceImpl implements ReleaseService {
|
||||
|
||||
private final ReleaseMapper releaseMapper;
|
||||
private final VehicleAssignmentMapper vehicleAssignmentMapper;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Override
|
||||
public ReleaseManifest createRelease(CreateReleaseParam param) {
|
||||
ReleaseManifest manifest = ReleaseManifest.builder()
|
||||
.releaseVersion(param.getReleaseVersion())
|
||||
.releaseNotes(param.getReleaseNotes())
|
||||
.upgradeMode(param.getUpgradeMode() == null ? UpgradeMode.MANUAL_CONFIRM : param.getUpgradeMode())
|
||||
.publishedAt(Instant.now())
|
||||
.parkingRequired(Boolean.TRUE.equals(param.getParkingRequired()))
|
||||
.components(param.getComponents())
|
||||
.build();
|
||||
|
||||
ReleaseEntity existing = releaseMapper.selectOne(new LambdaQueryWrapper<ReleaseEntity>()
|
||||
.eq(ReleaseEntity::getReleaseVersion, manifest.getReleaseVersion())
|
||||
.last("limit 1"));
|
||||
|
||||
ReleaseEntity entity = toEntity(manifest);
|
||||
if (existing == null) {
|
||||
releaseMapper.insert(entity);
|
||||
} else {
|
||||
entity.setId(existing.getId());
|
||||
releaseMapper.updateById(entity);
|
||||
}
|
||||
return manifest;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReleaseManifest updateRelease(String releaseVersion, CreateReleaseParam param) {
|
||||
ReleaseEntity existing = getEntity(releaseVersion);
|
||||
ReleaseManifest manifest = ReleaseManifest.builder()
|
||||
.releaseVersion(param.getReleaseVersion())
|
||||
.releaseNotes(param.getReleaseNotes())
|
||||
.upgradeMode(param.getUpgradeMode() == null ? UpgradeMode.MANUAL_CONFIRM : param.getUpgradeMode())
|
||||
.publishedAt(existing.getPublishedAt())
|
||||
.parkingRequired(Boolean.TRUE.equals(param.getParkingRequired()))
|
||||
.components(param.getComponents())
|
||||
.build();
|
||||
|
||||
ReleaseEntity duplicate = releaseMapper.selectOne(new LambdaQueryWrapper<ReleaseEntity>()
|
||||
.eq(ReleaseEntity::getReleaseVersion, manifest.getReleaseVersion())
|
||||
.last("limit 1"));
|
||||
if (duplicate != null && !duplicate.getId().equals(existing.getId())) {
|
||||
throw new IllegalArgumentException("版本号已存在:" + manifest.getReleaseVersion());
|
||||
}
|
||||
|
||||
ReleaseEntity entity = toEntity(manifest);
|
||||
entity.setId(existing.getId());
|
||||
releaseMapper.updateById(entity);
|
||||
return manifest;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteRelease(String releaseVersion) {
|
||||
ReleaseEntity entity = getEntity(releaseVersion);
|
||||
VehicleAssignmentEntity assignment = vehicleAssignmentMapper.selectOne(new LambdaQueryWrapper<VehicleAssignmentEntity>()
|
||||
.eq(VehicleAssignmentEntity::getReleaseVersion, releaseVersion)
|
||||
.last("limit 1"));
|
||||
if (assignment != null) {
|
||||
throw new IllegalArgumentException("该版本已被任务引用,无法删除:" + releaseVersion);
|
||||
}
|
||||
releaseMapper.deleteById(entity.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ReleaseManifest> listReleases() {
|
||||
return releaseMapper.selectList(new LambdaQueryWrapper<ReleaseEntity>()
|
||||
.orderByDesc(ReleaseEntity::getPublishedAt))
|
||||
.stream()
|
||||
.map(this::toModel)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReleaseManifest getRelease(String releaseVersion) {
|
||||
return toModel(getEntity(releaseVersion));
|
||||
}
|
||||
|
||||
private ReleaseEntity getEntity(String releaseVersion) {
|
||||
ReleaseEntity entity = releaseMapper.selectOne(new LambdaQueryWrapper<ReleaseEntity>()
|
||||
.eq(ReleaseEntity::getReleaseVersion, releaseVersion)
|
||||
.last("limit 1"));
|
||||
if (entity == null) {
|
||||
throw new IllegalArgumentException("Release not found: " + releaseVersion);
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
private ReleaseManifest toModel(ReleaseEntity entity) {
|
||||
return ReleaseManifest.builder()
|
||||
.releaseVersion(entity.getReleaseVersion())
|
||||
.releaseNotes(entity.getReleaseNotes())
|
||||
.upgradeMode(UpgradeMode.valueOf(entity.getUpgradeMode()))
|
||||
.publishedAt(entity.getPublishedAt())
|
||||
.parkingRequired(Boolean.TRUE.equals(entity.getParkingRequired()))
|
||||
.components(readComponents(entity.getComponentsJson()))
|
||||
.build();
|
||||
}
|
||||
|
||||
private ReleaseEntity toEntity(ReleaseManifest manifest) {
|
||||
ReleaseEntity entity = new ReleaseEntity();
|
||||
entity.setReleaseVersion(manifest.getReleaseVersion());
|
||||
entity.setReleaseNotes(manifest.getReleaseNotes());
|
||||
entity.setUpgradeMode(manifest.getUpgradeMode().name());
|
||||
entity.setPublishedAt(manifest.getPublishedAt());
|
||||
entity.setParkingRequired(manifest.getParkingRequired());
|
||||
entity.setComponentsJson(writeComponents(manifest.getComponents()));
|
||||
return entity;
|
||||
}
|
||||
|
||||
private Map<String, Object> readComponents(String json) {
|
||||
if (json == null || json.isBlank()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(json, new TypeReference<>() {
|
||||
});
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalStateException("Failed to parse componentsJson", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String writeComponents(Map<String, Object> components) {
|
||||
try {
|
||||
return objectMapper.writeValueAsString(components == null ? Collections.emptyMap() : components);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalStateException("Failed to serialize componentsJson", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.noblelift.ota.module.vehicle.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckRole;
|
||||
import com.noblelift.ota.common.annotation.Log;
|
||||
import com.noblelift.ota.module.vehicle.dto.VehicleView;
|
||||
import com.noblelift.ota.module.vehicle.param.RegisterVehicleParam;
|
||||
import com.noblelift.ota.module.vehicle.service.VehicleService;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/vehicles")
|
||||
@RequiredArgsConstructor
|
||||
public class VehicleController {
|
||||
|
||||
private final VehicleService vehicleService;
|
||||
|
||||
@SaCheckRole(value = {"SUPER_ADMIN", "ADMIN"}, mode = cn.dev33.satoken.annotation.SaMode.OR)
|
||||
@Log("注册车辆")
|
||||
@PostMapping
|
||||
public VehicleView registerVehicle(@Valid @RequestBody RegisterVehicleParam param) {
|
||||
return toView(vehicleService.registerVehicle(param));
|
||||
}
|
||||
|
||||
@SaCheckRole(value = {"SUPER_ADMIN", "ADMIN"}, mode = cn.dev33.satoken.annotation.SaMode.OR)
|
||||
@Log("修改车辆")
|
||||
@PutMapping("/{vehicleId}")
|
||||
public VehicleView updateVehicle(@PathVariable String vehicleId, @Valid @RequestBody RegisterVehicleParam param) {
|
||||
return toView(vehicleService.updateVehicle(vehicleId, param));
|
||||
}
|
||||
|
||||
@SaCheckRole(value = {"SUPER_ADMIN", "ADMIN"}, mode = cn.dev33.satoken.annotation.SaMode.OR)
|
||||
@Log("删除车辆")
|
||||
@DeleteMapping("/{vehicleId}")
|
||||
public void deleteVehicle(@PathVariable String vehicleId) {
|
||||
vehicleService.deleteVehicle(vehicleId);
|
||||
}
|
||||
|
||||
@SaCheckRole(value = {"SUPER_ADMIN", "ADMIN"}, mode = cn.dev33.satoken.annotation.SaMode.OR)
|
||||
@Log("查询车辆列表")
|
||||
@GetMapping
|
||||
public List<VehicleView> listVehicles() {
|
||||
return vehicleService.listVehicles().stream()
|
||||
.map(this::toView)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private VehicleView toView(com.noblelift.ota.common.model.VehicleInfo vehicle) {
|
||||
return new VehicleView(
|
||||
vehicle.getVehicleId(),
|
||||
vehicle.getVin(),
|
||||
vehicle.getCurrentRelease(),
|
||||
vehicle.getCurrentBackendVersion(),
|
||||
vehicle.getCurrentFrontendVersion(),
|
||||
vehicle.getCurrentRosVersion(),
|
||||
vehicle.getLastSeenAt(),
|
||||
vehicle.getAgentStatus(),
|
||||
vehicle.getTargetRelease(),
|
||||
vehicle.getLastResult(),
|
||||
vehicle.getImages(),
|
||||
vehicle.getBackupFile(),
|
||||
vehicle.getOnline()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.noblelift.ota.module.vehicle.dto;
|
||||
|
||||
import com.noblelift.ota.domain.AgentStatus;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class VehicleView {
|
||||
|
||||
private String vehicleId;
|
||||
private String vin;
|
||||
private String currentRelease;
|
||||
private String currentBackendVersion;
|
||||
private String currentFrontendVersion;
|
||||
private String currentRosVersion;
|
||||
private Instant lastSeenAt;
|
||||
private AgentStatus agentStatus;
|
||||
private String targetRelease;
|
||||
private String lastResult;
|
||||
private Map<String, String> images;
|
||||
private String backupFile;
|
||||
private Boolean online;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.noblelift.ota.module.vehicle.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Data
|
||||
@TableName("ota_vehicle")
|
||||
public class VehicleEntity {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private String vehicleId;
|
||||
private String vin;
|
||||
private String currentRelease;
|
||||
private String currentBackendVersion;
|
||||
private String currentFrontendVersion;
|
||||
private String currentRosVersion;
|
||||
private Instant lastSeenAt;
|
||||
private String agentStatus;
|
||||
private String targetRelease;
|
||||
private String lastResult;
|
||||
private String imagesJson;
|
||||
private String backupFile;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.noblelift.ota.module.vehicle.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.noblelift.ota.module.vehicle.entity.VehicleEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface VehicleMapper extends BaseMapper<VehicleEntity> {
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.noblelift.ota.module.vehicle.param;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class RegisterVehicleParam {
|
||||
|
||||
@NotBlank
|
||||
private String vehicleId;
|
||||
|
||||
@NotBlank
|
||||
private String vin;
|
||||
|
||||
@NotBlank
|
||||
private String currentRelease;
|
||||
|
||||
private String currentBackendVersion;
|
||||
private String currentFrontendVersion;
|
||||
private String currentRosVersion;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.noblelift.ota.module.vehicle.service;
|
||||
|
||||
import com.noblelift.ota.common.model.VehicleInfo;
|
||||
import com.noblelift.ota.domain.AgentStatus;
|
||||
import com.noblelift.ota.module.vehicle.param.RegisterVehicleParam;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface VehicleService {
|
||||
|
||||
VehicleInfo registerVehicle(RegisterVehicleParam param);
|
||||
|
||||
VehicleInfo updateVehicle(String vehicleId, RegisterVehicleParam param);
|
||||
|
||||
void deleteVehicle(String vehicleId);
|
||||
|
||||
List<VehicleInfo> listVehicles();
|
||||
|
||||
VehicleInfo saveHeartbeat(String vehicleId,
|
||||
String vin,
|
||||
String currentRelease,
|
||||
AgentStatus agentStatus,
|
||||
String targetRelease,
|
||||
String lastResult,
|
||||
Map<String, String> images,
|
||||
String backupFile);
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
package com.noblelift.ota.module.vehicle.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.noblelift.ota.common.model.VehicleInfo;
|
||||
import com.noblelift.ota.config.OtaProperties;
|
||||
import com.noblelift.ota.domain.AgentStatus;
|
||||
import com.noblelift.ota.module.assignment.entity.VehicleAssignmentEntity;
|
||||
import com.noblelift.ota.module.assignment.mapper.VehicleAssignmentMapper;
|
||||
import com.noblelift.ota.module.vehicle.entity.VehicleEntity;
|
||||
import com.noblelift.ota.module.vehicle.mapper.VehicleMapper;
|
||||
import com.noblelift.ota.module.vehicle.param.RegisterVehicleParam;
|
||||
import com.noblelift.ota.module.vehicle.service.VehicleService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VehicleServiceImpl implements VehicleService {
|
||||
|
||||
private static final TypeReference<Map<String, String>> STRING_MAP_TYPE = new TypeReference<>() {
|
||||
};
|
||||
|
||||
private final VehicleMapper vehicleMapper;
|
||||
private final VehicleAssignmentMapper vehicleAssignmentMapper;
|
||||
private final OtaProperties otaProperties;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Override
|
||||
public VehicleInfo registerVehicle(RegisterVehicleParam param) {
|
||||
VehicleEntity current = vehicleMapper.selectOne(new LambdaQueryWrapper<VehicleEntity>()
|
||||
.eq(VehicleEntity::getVehicleId, param.getVehicleId())
|
||||
.last("limit 1"));
|
||||
|
||||
VehicleInfo vehicleInfo = VehicleInfo.builder()
|
||||
.vehicleId(param.getVehicleId())
|
||||
.vin(param.getVin())
|
||||
.currentRelease(param.getCurrentRelease())
|
||||
.currentBackendVersion(param.getCurrentBackendVersion())
|
||||
.currentFrontendVersion(param.getCurrentFrontendVersion())
|
||||
.currentRosVersion(param.getCurrentRosVersion())
|
||||
.lastSeenAt(current == null ? null : current.getLastSeenAt())
|
||||
.agentStatus(current == null || current.getAgentStatus() == null ? AgentStatus.IDLE : AgentStatus.valueOf(current.getAgentStatus()))
|
||||
.targetRelease(current == null ? null : current.getTargetRelease())
|
||||
.lastResult(current == null ? null : current.getLastResult())
|
||||
.images(current == null ? Map.of() : parseImages(current.getImagesJson()))
|
||||
.backupFile(current == null ? null : current.getBackupFile())
|
||||
.online(current != null && isOnline(current.getLastSeenAt()))
|
||||
.build();
|
||||
|
||||
saveVehicle(vehicleInfo);
|
||||
return vehicleInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VehicleInfo updateVehicle(String vehicleId, RegisterVehicleParam param) {
|
||||
VehicleEntity existing = getEntity(vehicleId);
|
||||
VehicleEntity duplicate = vehicleMapper.selectOne(new LambdaQueryWrapper<VehicleEntity>()
|
||||
.eq(VehicleEntity::getVehicleId, param.getVehicleId())
|
||||
.last("limit 1"));
|
||||
if (duplicate != null && !duplicate.getId().equals(existing.getId())) {
|
||||
throw new IllegalArgumentException("车辆 ID 已存在:" + param.getVehicleId());
|
||||
}
|
||||
|
||||
VehicleInfo vehicleInfo = VehicleInfo.builder()
|
||||
.vehicleId(param.getVehicleId())
|
||||
.vin(param.getVin())
|
||||
.currentRelease(param.getCurrentRelease())
|
||||
.currentBackendVersion(param.getCurrentBackendVersion())
|
||||
.currentFrontendVersion(param.getCurrentFrontendVersion())
|
||||
.currentRosVersion(param.getCurrentRosVersion())
|
||||
.lastSeenAt(existing.getLastSeenAt())
|
||||
.agentStatus(existing.getAgentStatus() == null ? AgentStatus.IDLE : AgentStatus.valueOf(existing.getAgentStatus()))
|
||||
.targetRelease(existing.getTargetRelease())
|
||||
.lastResult(existing.getLastResult())
|
||||
.images(parseImages(existing.getImagesJson()))
|
||||
.backupFile(existing.getBackupFile())
|
||||
.online(isOnline(existing.getLastSeenAt()))
|
||||
.build();
|
||||
|
||||
saveVehicle(vehicleInfo, existing.getId());
|
||||
return vehicleInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteVehicle(String vehicleId) {
|
||||
VehicleEntity existing = getEntity(vehicleId);
|
||||
VehicleAssignmentEntity assignment = vehicleAssignmentMapper.selectOne(new LambdaQueryWrapper<VehicleAssignmentEntity>()
|
||||
.eq(VehicleAssignmentEntity::getVehicleId, vehicleId)
|
||||
.last("limit 1"));
|
||||
if (assignment != null) {
|
||||
throw new IllegalArgumentException("该车辆存在任务记录,无法删除:" + vehicleId);
|
||||
}
|
||||
vehicleMapper.deleteById(existing.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VehicleInfo> listVehicles() {
|
||||
return vehicleMapper.selectList(new LambdaQueryWrapper<VehicleEntity>()
|
||||
.orderByAsc(VehicleEntity::getVehicleId))
|
||||
.stream()
|
||||
.map(this::toModel)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public VehicleInfo saveHeartbeat(String vehicleId,
|
||||
String vin,
|
||||
String currentRelease,
|
||||
AgentStatus agentStatus,
|
||||
String targetRelease,
|
||||
String lastResult,
|
||||
Map<String, String> images,
|
||||
String backupFile) {
|
||||
VehicleEntity current = vehicleMapper.selectOne(new LambdaQueryWrapper<VehicleEntity>()
|
||||
.eq(VehicleEntity::getVehicleId, vehicleId)
|
||||
.last("limit 1"));
|
||||
|
||||
VehicleInfo info = VehicleInfo.builder()
|
||||
.vehicleId(vehicleId)
|
||||
.vin(vin)
|
||||
.currentRelease(currentRelease)
|
||||
.currentBackendVersion(current == null ? null : current.getCurrentBackendVersion())
|
||||
.currentFrontendVersion(current == null ? null : current.getCurrentFrontendVersion())
|
||||
.currentRosVersion(current == null ? null : current.getCurrentRosVersion())
|
||||
.lastSeenAt(Instant.now())
|
||||
.agentStatus(agentStatus == null
|
||||
? (current == null || current.getAgentStatus() == null ? AgentStatus.IDLE : AgentStatus.valueOf(current.getAgentStatus()))
|
||||
: agentStatus)
|
||||
.targetRelease(targetRelease == null && current != null ? current.getTargetRelease() : targetRelease)
|
||||
.lastResult(lastResult == null && current != null ? current.getLastResult() : lastResult)
|
||||
.images(images == null ? (current == null ? Map.of() : parseImages(current.getImagesJson())) : new LinkedHashMap<>(images))
|
||||
.backupFile(backupFile == null && current != null ? current.getBackupFile() : backupFile)
|
||||
.online(true)
|
||||
.build();
|
||||
saveVehicle(info, current == null ? null : current.getId());
|
||||
return info;
|
||||
}
|
||||
|
||||
private void saveVehicle(VehicleInfo vehicleInfo, Long existingId) {
|
||||
VehicleEntity entity = toEntity(vehicleInfo);
|
||||
if (existingId == null) {
|
||||
vehicleMapper.insert(entity);
|
||||
} else {
|
||||
entity.setId(existingId);
|
||||
vehicleMapper.updateById(entity);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveVehicle(VehicleInfo vehicleInfo) {
|
||||
VehicleEntity existing = vehicleMapper.selectOne(new LambdaQueryWrapper<VehicleEntity>()
|
||||
.eq(VehicleEntity::getVehicleId, vehicleInfo.getVehicleId())
|
||||
.last("limit 1"));
|
||||
saveVehicle(vehicleInfo, existing == null ? null : existing.getId());
|
||||
}
|
||||
|
||||
private VehicleEntity getEntity(String vehicleId) {
|
||||
VehicleEntity entity = vehicleMapper.selectOne(new LambdaQueryWrapper<VehicleEntity>()
|
||||
.eq(VehicleEntity::getVehicleId, vehicleId)
|
||||
.last("limit 1"));
|
||||
if (entity == null) {
|
||||
throw new IllegalArgumentException("Vehicle not found: " + vehicleId);
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
private VehicleInfo toModel(VehicleEntity entity) {
|
||||
return VehicleInfo.builder()
|
||||
.vehicleId(entity.getVehicleId())
|
||||
.vin(entity.getVin())
|
||||
.currentRelease(entity.getCurrentRelease())
|
||||
.currentBackendVersion(entity.getCurrentBackendVersion())
|
||||
.currentFrontendVersion(entity.getCurrentFrontendVersion())
|
||||
.currentRosVersion(entity.getCurrentRosVersion())
|
||||
.lastSeenAt(entity.getLastSeenAt())
|
||||
.agentStatus(entity.getAgentStatus() == null ? AgentStatus.IDLE : AgentStatus.valueOf(entity.getAgentStatus()))
|
||||
.targetRelease(entity.getTargetRelease())
|
||||
.lastResult(entity.getLastResult())
|
||||
.images(parseImages(entity.getImagesJson()))
|
||||
.backupFile(entity.getBackupFile())
|
||||
.online(isOnline(entity.getLastSeenAt()))
|
||||
.build();
|
||||
}
|
||||
|
||||
private VehicleEntity toEntity(VehicleInfo vehicleInfo) {
|
||||
VehicleEntity entity = new VehicleEntity();
|
||||
entity.setVehicleId(vehicleInfo.getVehicleId());
|
||||
entity.setVin(vehicleInfo.getVin());
|
||||
entity.setCurrentRelease(vehicleInfo.getCurrentRelease());
|
||||
entity.setCurrentBackendVersion(vehicleInfo.getCurrentBackendVersion());
|
||||
entity.setCurrentFrontendVersion(vehicleInfo.getCurrentFrontendVersion());
|
||||
entity.setCurrentRosVersion(vehicleInfo.getCurrentRosVersion());
|
||||
entity.setLastSeenAt(vehicleInfo.getLastSeenAt());
|
||||
entity.setAgentStatus(vehicleInfo.getAgentStatus() == null ? null : vehicleInfo.getAgentStatus().name());
|
||||
entity.setTargetRelease(vehicleInfo.getTargetRelease());
|
||||
entity.setLastResult(vehicleInfo.getLastResult());
|
||||
entity.setImagesJson(writeImages(vehicleInfo.getImages()));
|
||||
entity.setBackupFile(vehicleInfo.getBackupFile());
|
||||
return entity;
|
||||
}
|
||||
|
||||
private Map<String, String> parseImages(String imagesJson) {
|
||||
if (imagesJson == null || imagesJson.isBlank()) {
|
||||
return Map.of();
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(imagesJson, STRING_MAP_TYPE);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalStateException("Failed to parse imagesJson", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String writeImages(Map<String, String> images) {
|
||||
if (images == null || images.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return objectMapper.writeValueAsString(images);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalStateException("Failed to serialize imagesJson", e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isOnline(Instant lastSeenAt) {
|
||||
return lastSeenAt != null
|
||||
&& lastSeenAt.isAfter(Instant.now().minusSeconds(otaProperties.getHeartbeatTimeoutSeconds()));
|
||||
}
|
||||
}
|
||||
55
src/main/resources/application.yml
Normal file
55
src/main/resources/application.yml
Normal file
@@ -0,0 +1,55 @@
|
||||
spring:
|
||||
application:
|
||||
name: ota-server
|
||||
datasource:
|
||||
url: jdbc:mysql://127.0.0.1:3306/ota_server?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
|
||||
username: root
|
||||
password: 123456
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
ota:
|
||||
auth-token: f47ac10b-58cc-4372-a567-0e02b2c3d479
|
||||
heartbeat-timeout-seconds: 120
|
||||
cors:
|
||||
allowed-origins:
|
||||
- http://localhost:5173
|
||||
- http://127.0.0.1:5173
|
||||
allowed-methods:
|
||||
- GET
|
||||
- POST
|
||||
- PUT
|
||||
- DELETE
|
||||
- OPTIONS
|
||||
allowed-headers:
|
||||
- "*"
|
||||
allow-credentials: true
|
||||
|
||||
sa-token:
|
||||
token-name: X-OTA-TOKEN
|
||||
token-prefix: ""
|
||||
timeout: 1800
|
||||
active-timeout: -1
|
||||
is-concurrent: true
|
||||
is-share: true
|
||||
is-log: true
|
||||
token-style: uuid
|
||||
is-read-header: true
|
||||
is-read-cookie: false
|
||||
is-read-body: false
|
||||
is-read-param: false
|
||||
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
global-config:
|
||||
db-config:
|
||||
id-type: auto
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info
|
||||
90
src/main/resources/schema.sql
Normal file
90
src/main/resources/schema.sql
Normal file
@@ -0,0 +1,90 @@
|
||||
CREATE DATABASE IF NOT EXISTS ota_server DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
USE ota_server;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ota_release (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
release_version VARCHAR(128) NOT NULL UNIQUE,
|
||||
release_notes TEXT NOT NULL,
|
||||
upgrade_mode VARCHAR(32) NOT NULL,
|
||||
published_at DATETIME(6) NOT NULL,
|
||||
parking_required TINYINT(1) NOT NULL DEFAULT 0,
|
||||
components_json JSON NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ota_vehicle (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
vehicle_id VARCHAR(64) NOT NULL UNIQUE,
|
||||
vin VARCHAR(64) NOT NULL,
|
||||
current_release VARCHAR(128) NOT NULL,
|
||||
current_backend_version VARCHAR(128) NULL,
|
||||
current_frontend_version VARCHAR(128) NULL,
|
||||
current_ros_version VARCHAR(128) NULL,
|
||||
last_seen_at DATETIME(6) NULL,
|
||||
agent_status VARCHAR(32) NULL,
|
||||
target_release VARCHAR(128) NULL,
|
||||
last_result VARCHAR(512) NULL,
|
||||
images_json JSON NULL,
|
||||
backup_file VARCHAR(512) NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ota_vehicle_assignment (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
vehicle_id VARCHAR(64) NOT NULL UNIQUE,
|
||||
release_version VARCHAR(128) NOT NULL,
|
||||
task_status VARCHAR(32) NOT NULL,
|
||||
prompted_at DATETIME(6) NULL,
|
||||
confirmed_at DATETIME(6) NULL,
|
||||
started_at DATETIME(6) NULL,
|
||||
finished_at DATETIME(6) NULL,
|
||||
postpone_count INT NOT NULL DEFAULT 0,
|
||||
last_message VARCHAR(512) NULL,
|
||||
CONSTRAINT fk_assignment_release_version FOREIGN KEY (release_version) REFERENCES ota_release(release_version)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sys_user (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
username VARCHAR(64) NOT NULL UNIQUE,
|
||||
password VARCHAR(128) NOT NULL,
|
||||
nickname VARCHAR(64) NOT NULL,
|
||||
status TINYINT NOT NULL DEFAULT 1,
|
||||
created_at DATETIME(6) NOT NULL,
|
||||
updated_at DATETIME(6) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sys_role (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
role_code VARCHAR(64) NOT NULL UNIQUE,
|
||||
role_name VARCHAR(64) NOT NULL,
|
||||
status TINYINT NOT NULL DEFAULT 1,
|
||||
created_at DATETIME(6) NOT NULL,
|
||||
updated_at DATETIME(6) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sys_user_role (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
user_id BIGINT NOT NULL,
|
||||
role_id BIGINT NOT NULL,
|
||||
created_at DATETIME(6) NOT NULL,
|
||||
UNIQUE KEY uk_user_role (user_id, role_id),
|
||||
CONSTRAINT fk_user_role_user FOREIGN KEY (user_id) REFERENCES sys_user(id),
|
||||
CONSTRAINT fk_user_role_role FOREIGN KEY (role_id) REFERENCES sys_role(id)
|
||||
);
|
||||
|
||||
INSERT INTO sys_role (id, role_code, role_name, status, created_at, updated_at)
|
||||
VALUES
|
||||
(1, 'SUPER_ADMIN', '系统管理员', 1, NOW(6), NOW(6)),
|
||||
(2, 'ADMIN', '运维管理员', 1, NOW(6), NOW(6)),
|
||||
(3, 'AGENT', '车端代理', 1, NOW(6), NOW(6))
|
||||
ON DUPLICATE KEY UPDATE role_name = VALUES(role_name), status = VALUES(status), updated_at = NOW(6);
|
||||
|
||||
INSERT INTO sys_user (id, username, password, nickname, status, created_at, updated_at)
|
||||
VALUES
|
||||
(1, 'admin', 'e10adc3949ba59abbe56e057f20f883e', '超级管理员', 1, NOW(6), NOW(6)),
|
||||
(2, 'agent', 'e10adc3949ba59abbe56e057f20f883e', '车端代理', 1, NOW(6), NOW(6))
|
||||
ON DUPLICATE KEY UPDATE password = VALUES(password), nickname = VALUES(nickname), status = VALUES(status), updated_at = NOW(6);
|
||||
|
||||
INSERT INTO sys_user_role (user_id, role_id, created_at)
|
||||
VALUES
|
||||
(1, 1, NOW(6)),
|
||||
(2, 3, NOW(6))
|
||||
ON DUPLICATE KEY UPDATE created_at = VALUES(created_at);
|
||||
55
target/classes/application.yml
Normal file
55
target/classes/application.yml
Normal file
@@ -0,0 +1,55 @@
|
||||
spring:
|
||||
application:
|
||||
name: ota-server
|
||||
datasource:
|
||||
url: jdbc:mysql://127.0.0.1:3306/ota_server?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
|
||||
username: root
|
||||
password: 123456
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
ota:
|
||||
auth-token: f47ac10b-58cc-4372-a567-0e02b2c3d479
|
||||
heartbeat-timeout-seconds: 120
|
||||
cors:
|
||||
allowed-origins:
|
||||
- http://localhost:5173
|
||||
- http://127.0.0.1:5173
|
||||
allowed-methods:
|
||||
- GET
|
||||
- POST
|
||||
- PUT
|
||||
- DELETE
|
||||
- OPTIONS
|
||||
allowed-headers:
|
||||
- "*"
|
||||
allow-credentials: true
|
||||
|
||||
sa-token:
|
||||
token-name: X-OTA-TOKEN
|
||||
token-prefix: ""
|
||||
timeout: 1800
|
||||
active-timeout: -1
|
||||
is-concurrent: true
|
||||
is-share: true
|
||||
is-log: true
|
||||
token-style: uuid
|
||||
is-read-header: true
|
||||
is-read-cookie: false
|
||||
is-read-body: false
|
||||
is-read-param: false
|
||||
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
global-config:
|
||||
db-config:
|
||||
id-type: auto
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info
|
||||
BIN
target/classes/com/noblelift/ota/OtaServerApplication.class
Normal file
BIN
target/classes/com/noblelift/ota/OtaServerApplication.class
Normal file
Binary file not shown.
Binary file not shown.
BIN
target/classes/com/noblelift/ota/common/annotation/Log.class
Normal file
BIN
target/classes/com/noblelift/ota/common/annotation/Log.class
Normal file
Binary file not shown.
BIN
target/classes/com/noblelift/ota/common/aspect/LogAspect.class
Normal file
BIN
target/classes/com/noblelift/ota/common/aspect/LogAspect.class
Normal file
Binary file not shown.
Binary file not shown.
BIN
target/classes/com/noblelift/ota/common/dto/ApiMessage.class
Normal file
BIN
target/classes/com/noblelift/ota/common/dto/ApiMessage.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
target/classes/com/noblelift/ota/common/model/VehicleInfo.class
Normal file
BIN
target/classes/com/noblelift/ota/common/model/VehicleInfo.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
target/classes/com/noblelift/ota/config/OtaConfiguration.class
Normal file
BIN
target/classes/com/noblelift/ota/config/OtaConfiguration.class
Normal file
Binary file not shown.
BIN
target/classes/com/noblelift/ota/config/OtaProperties$Cors.class
Normal file
BIN
target/classes/com/noblelift/ota/config/OtaProperties$Cors.class
Normal file
Binary file not shown.
BIN
target/classes/com/noblelift/ota/config/OtaProperties.class
Normal file
BIN
target/classes/com/noblelift/ota/config/OtaProperties.class
Normal file
Binary file not shown.
BIN
target/classes/com/noblelift/ota/config/SaTokenConfigure.class
Normal file
BIN
target/classes/com/noblelift/ota/config/SaTokenConfigure.class
Normal file
Binary file not shown.
Binary file not shown.
BIN
target/classes/com/noblelift/ota/domain/AgentStatus.class
Normal file
BIN
target/classes/com/noblelift/ota/domain/AgentStatus.class
Normal file
Binary file not shown.
BIN
target/classes/com/noblelift/ota/domain/TaskStatus.class
Normal file
BIN
target/classes/com/noblelift/ota/domain/TaskStatus.class
Normal file
Binary file not shown.
BIN
target/classes/com/noblelift/ota/domain/UpgradeMode.class
Normal file
BIN
target/classes/com/noblelift/ota/domain/UpgradeMode.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user