Initial commit
This commit is contained in:
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()));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user