Initial commit
This commit is contained in:
@@ -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