Initial commit

This commit is contained in:
2026-04-23 14:23:42 +08:00
commit 9ceba6f9e3
141 changed files with 2886 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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