From 5309b98bcfb81ca5b471fc5b5cf2a1efc9f56de0 Mon Sep 17 00:00:00 2001 From: sladro Date: Thu, 22 Jan 2026 14:11:52 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BC=80=E5=8F=91=E5=AE=8C=E6=88=90=E7=AD=89?= =?UTF-8?q?=E5=BE=85=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../VehicleManagerApiController.java | 153 ++++++++++++ .../controller/VehicleTaskController.java | 125 ++++++++++ .../service/VehicleManagerQueryService.java | 190 +++++++++++++++ .../collision/task/InMemoryTaskStore.java | 89 +++++++ .../qaup/framework/config/SecurityConfig.java | 1 + 前端开发_无人车VehicleManager接口.md | 222 ++++++++++++++++++ 6 files changed, 780 insertions(+) create mode 100644 qaup-collision/src/main/java/com/qaup/collision/controller/VehicleManagerApiController.java create mode 100644 qaup-collision/src/main/java/com/qaup/collision/controller/VehicleTaskController.java create mode 100644 qaup-collision/src/main/java/com/qaup/collision/datacollector/service/VehicleManagerQueryService.java create mode 100644 qaup-collision/src/main/java/com/qaup/collision/task/InMemoryTaskStore.java create mode 100644 前端开发_无人车VehicleManager接口.md diff --git a/qaup-collision/src/main/java/com/qaup/collision/controller/VehicleManagerApiController.java b/qaup-collision/src/main/java/com/qaup/collision/controller/VehicleManagerApiController.java new file mode 100644 index 0000000..a2eb7ed --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/controller/VehicleManagerApiController.java @@ -0,0 +1,153 @@ +package com.qaup.collision.controller; + +import com.fasterxml.jackson.databind.JsonNode; +import com.qaup.common.constant.HttpStatus; +import com.qaup.common.core.domain.AjaxResult; +import com.qaup.collision.datacollector.dao.DataCollectorDao; +import com.qaup.collision.datacollector.model.dto.BatteryStatusDTO; +import com.qaup.collision.datacollector.model.dto.MissionContextDTO; +import com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO; +import com.qaup.collision.datacollector.service.VehicleManagerCacheService; +import com.qaup.collision.datacollector.service.VehicleManagerQueryService; +import com.qaup.collision.datacollector.service.VehicleStatusAggregationService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/vehicle-manager") +@RequiredArgsConstructor +public class VehicleManagerApiController { + + private final VehicleManagerQueryService queryService; + private final VehicleManagerCacheService cacheService; + private final VehicleStatusAggregationService vehicleStatusAggregationService; + private final DataCollectorDao dataCollectorDao; + + @GetMapping("/vehicles/summary") + public AjaxResult getVehicleSummary(@RequestParam(defaultValue = "30000") long staleMillis) { + VehicleManagerQueryService.VehicleSummary summary = queryService.getVehicleSummary(staleMillis); + return AjaxResult.success(summary).put("timestamp", System.currentTimeMillis()); + } + + @GetMapping("/vehicles/{vehicleId}") + public AjaxResult getVehicleSnapshot(@PathVariable String vehicleId) { + Map snapshot = queryService.getVehicleSnapshot(vehicleId); + return AjaxResult.success(snapshot).put("timestamp", System.currentTimeMillis()); + } + + @GetMapping("/vehicles/{vehicleId}/battery") + public AjaxResult getVehicleBattery(@PathVariable String vehicleId) { + UniversalVehicleStatusDTO status = dataCollectorDao.getVehicleManagerStatus(vehicleId); + + BatteryStatusDTO batteryStatus = status != null ? status.getBatteryStatus() : null; + if (batteryStatus == null) { + batteryStatus = BatteryStatusDTO.builder() + .mainBattery(BatteryStatusDTO.MainBatteryDTO.builder() + .chargeLevel(null) + .voltage(0.0) + .current(0.0) + .temperature(0.0) + .chargingStatus("DISCHARGING") + .build()) + .build(); + } else if (batteryStatus.getMainBattery() == null) { + batteryStatus.setMainBattery(BatteryStatusDTO.MainBatteryDTO.builder() + .chargeLevel(null) + .voltage(0.0) + .current(0.0) + .temperature(0.0) + .chargingStatus("DISCHARGING") + .build()); + } + + Map data = new HashMap<>(); + data.put("vehicleId", vehicleId); + data.put("batteryStatus", batteryStatus); + data.put("source", "vehicle_manager_http"); + data.put("upstreamTimestamp", status != null ? status.getOperationalStatus() != null ? status.getOperationalStatus().getLastHeartbeat() : null : null); + + return AjaxResult.success(data).put("timestamp", System.currentTimeMillis()); + } + + @GetMapping("/vehicles/{vehicleId}/path") + public AjaxResult getVehiclePath(@PathVariable String vehicleId) { + VehicleManagerCacheService.CacheEntry entry = cacheService.getVehiclePath(vehicleId); + + Map data = new HashMap<>(); + data.put("vehicleId", vehicleId); + + if (entry != null && entry.getData() != null && entry.getData().has("path")) { + JsonNode rawPath = entry.getData().get("path"); + data.put("rawPath", rawPath); + data.put("waypoints", convertRawPathToWaypoints(rawPath)); + data.put("source", "ws_cache"); + return AjaxResult.success(data).put("timestamp", System.currentTimeMillis()); + } + + UniversalVehicleStatusDTO status = vehicleStatusAggregationService.getAggregatedStatus(vehicleId); + if (status == null || status.getMissionContext() == null) { + return AjaxResult.error(HttpStatus.NOT_FOUND, "车辆路径不可用").put("timestamp", System.currentTimeMillis()); + } + + MissionContextDTO missionContext = status.getMissionContext(); + data.put("rawPath", null); + data.put("waypoints", missionContext.getWaypoints() != null ? missionContext.getWaypoints() : List.of()); + data.put("source", "http_fallback"); + + return AjaxResult.success(data).put("timestamp", System.currentTimeMillis()); + } + + @GetMapping("/charging-stations") + public AjaxResult getChargingStations() { + return AjaxResult.success(List.of()).put("timestamp", System.currentTimeMillis()); + } + + private List convertRawPathToWaypoints(JsonNode rawPath) { + if (rawPath == null || !rawPath.isArray()) { + return List.of(); + } + List waypoints = new ArrayList<>(); + int index = 1; + for (JsonNode node : rawPath) { + Double x = getDouble(node, "x"); + Double y = getDouble(node, "y"); + waypoints.add(MissionContextDTO.WaypointDTO.builder() + .waypointId(String.valueOf(index++)) + .longitude(x) + .latitude(y) + .status("PENDING") + .build()); + } + return waypoints; + } + + private Double getDouble(JsonNode node, String field) { + if (node == null || node.isMissingNode()) { + return null; + } + JsonNode value = node.get(field); + if (value == null || value.isNull()) { + return null; + } + if (value.isNumber()) { + return value.asDouble(); + } + if (value.isTextual()) { + try { + return Double.parseDouble(value.asText()); + } catch (NumberFormatException ignored) { + return null; + } + } + return null; + } +} diff --git a/qaup-collision/src/main/java/com/qaup/collision/controller/VehicleTaskController.java b/qaup-collision/src/main/java/com/qaup/collision/controller/VehicleTaskController.java new file mode 100644 index 0000000..e0744ce --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/controller/VehicleTaskController.java @@ -0,0 +1,125 @@ +package com.qaup.collision.controller; + +import com.fasterxml.jackson.databind.JsonNode; +import com.qaup.common.constant.HttpStatus; +import com.qaup.common.core.domain.AjaxResult; +import com.qaup.collision.task.InMemoryTaskStore; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/api/vehicle-manager/tasks") +@Validated +@RequiredArgsConstructor +public class VehicleTaskController { + + private final InMemoryTaskStore taskStore; + + @Data + public static class TaskUpsertRequest { + @NotBlank + private String name; + + @NotBlank + private String type; + + private Integer priority; + + private JsonNode payload; + } + + @GetMapping + public AjaxResult list(@RequestParam(required = false, defaultValue = "1") int pageNum, + @RequestParam(required = false, defaultValue = "10") int pageSize) { + List all = taskStore.listAll(); + int total = all.size(); + + int fromIndex = Math.max(0, (pageNum - 1) * pageSize); + int toIndex = Math.min(total, fromIndex + pageSize); + List page = fromIndex >= total ? List.of() : all.subList(fromIndex, toIndex); + + Map data = new HashMap<>(); + data.put("total", total); + data.put("pageNum", pageNum); + data.put("pageSize", pageSize); + data.put("rows", page); + + return AjaxResult.success(data).put("timestamp", System.currentTimeMillis()); + } + + @GetMapping("/{id}") + public AjaxResult get(@PathVariable long id) { + InMemoryTaskStore.Task task = taskStore.get(id); + if (task == null) { + return AjaxResult.error(HttpStatus.NOT_FOUND, "任务不存在").put("timestamp", System.currentTimeMillis()); + } + return AjaxResult.success(task).put("timestamp", System.currentTimeMillis()); + } + + @PostMapping + public AjaxResult create(@Valid @RequestBody TaskUpsertRequest request) { + InMemoryTaskStore.Task task = taskStore.create( + request.getName(), + request.getType(), + request.getPriority(), + request.getPayload() + ); + return AjaxResult.success(task).put("timestamp", System.currentTimeMillis()); + } + + @PutMapping("/{id}") + public AjaxResult update(@PathVariable long id, @Valid @RequestBody TaskUpsertRequest request) { + InMemoryTaskStore.Task task = taskStore.update( + id, + request.getName(), + request.getType(), + request.getPriority(), + request.getPayload() + ); + if (task == null) { + return AjaxResult.error(HttpStatus.NOT_FOUND, "任务不存在").put("timestamp", System.currentTimeMillis()); + } + return AjaxResult.success(task).put("timestamp", System.currentTimeMillis()); + } + + @DeleteMapping("/{ids}") + public AjaxResult delete(@PathVariable String ids) { + List idList = parseIds(ids); + int removed = taskStore.deleteMany(idList); + + Map data = new HashMap<>(); + data.put("removed", removed); + data.put("requested", idList.size()); + + return AjaxResult.success(data).put("timestamp", System.currentTimeMillis()); + } + + private List parseIds(String ids) { + if (ids == null || ids.isBlank()) { + return List.of(); + } + return List.of(ids.split(",")) + .stream() + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(s -> { + try { + return Long.parseLong(s); + } catch (NumberFormatException e) { + return null; + } + }) + .filter(v -> v != null) + .collect(Collectors.toList()); + } +} diff --git a/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/VehicleManagerQueryService.java b/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/VehicleManagerQueryService.java new file mode 100644 index 0000000..b88314b --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/VehicleManagerQueryService.java @@ -0,0 +1,190 @@ +package com.qaup.collision.datacollector.service; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +@Service +@RequiredArgsConstructor +public class VehicleManagerQueryService { + + private final VehicleManagerCacheService cacheService; + + @Data + @Builder + public static class VehicleSummary { + private int totalCount; + private int onlineCount; + private int offlineCount; + private int emergencyCount; + private int faultCount; + private int unknownCount; + private long staleMillis; + private long timestamp; + } + + public VehicleSummary getVehicleSummary(long staleMillis) { + long now = System.currentTimeMillis(); + Set vehicleIds = cacheService.getKnownVehicleIds(); + + int online = 0; + int offline = 0; + int emergency = 0; + int fault = 0; + int unknown = 0; + + for (String vehicleId : vehicleIds) { + VehicleManagerCacheService.CacheEntry loginEntry = cacheService.getVehicleLoginStatus(vehicleId); + boolean loginStale = isStale(loginEntry, now, staleMillis); + + if (loginEntry == null) { + long lastSeenAt = getLastSeenAt(vehicleId); + if (now - lastSeenAt > staleMillis) { + offline++; + } else { + unknown++; + } + } else { + if (loginStale) { + offline++; + } else { + String loginStatus = getText(loginEntry.getData(), "loginStatus"); + if ("login".equalsIgnoreCase(loginStatus)) { + online++; + } else { + offline++; + } + } + } + + VehicleManagerCacheService.CacheEntry suspendEntry = cacheService.getVehicleSuspend(vehicleId); + if (!isStale(suspendEntry, now, staleMillis)) { + Integer suspendStatus = getInt(suspendEntry != null ? suspendEntry.getData() : null, "suspendStatus"); + if (suspendStatus != null && suspendStatus != 0) { + emergency++; + } + } + + VehicleManagerCacheService.CacheEntry fmsEntry = cacheService.getVehicleFmsMessage(vehicleId); + if (!isStale(fmsEntry, now, staleMillis)) { + if (isFault(fmsEntry != null ? fmsEntry.getData() : null)) { + fault++; + } + } + } + + return VehicleSummary.builder() + .totalCount(vehicleIds.size()) + .onlineCount(online) + .offlineCount(offline) + .emergencyCount(emergency) + .faultCount(fault) + .unknownCount(unknown) + .staleMillis(staleMillis) + .timestamp(now) + .build(); + } + + public Map getVehicleSnapshot(String vehicleId) { + Map data = new HashMap<>(); + data.put("vehicleId", vehicleId); + + VehicleManagerCacheService.CacheEntry details = cacheService.getVehicleDetails(vehicleId); + VehicleManagerCacheService.CacheEntry login = cacheService.getVehicleLoginStatus(vehicleId); + VehicleManagerCacheService.CacheEntry chassis = cacheService.getVehicleChassis(vehicleId); + VehicleManagerCacheService.CacheEntry order = cacheService.getVehicleOrder(vehicleId); + VehicleManagerCacheService.CacheEntry suspend = cacheService.getVehicleSuspend(vehicleId); + VehicleManagerCacheService.CacheEntry tailer = cacheService.getVehicleTailer(vehicleId); + VehicleManagerCacheService.CacheEntry position = cacheService.getVehiclePosition(vehicleId); + VehicleManagerCacheService.CacheEntry path = cacheService.getVehiclePath(vehicleId); + VehicleManagerCacheService.CacheEntry fms = cacheService.getVehicleFmsMessage(vehicleId); + + data.put("details", details != null ? details.getData() : null); + data.put("loginStatus", login != null ? login.getData() : null); + data.put("chassis", chassis != null ? chassis.getData() : null); + data.put("order", order != null ? order.getData() : null); + data.put("suspend", suspend != null ? suspend.getData() : null); + data.put("tailer", tailer != null ? tailer.getData() : null); + data.put("position", position != null ? position.getData() : null); + data.put("path", path != null ? path.getData() : null); + data.put("fmsMessage", fms != null ? fms.getData() : null); + data.put("lastSeenAt", getLastSeenAt(vehicleId)); + + return data; + } + + private long getLastSeenAt(String vehicleId) { + long last = 0L; + last = Math.max(last, getTimestamp(cacheService.getVehicleDetails(vehicleId))); + last = Math.max(last, getTimestamp(cacheService.getVehicleLoginStatus(vehicleId))); + last = Math.max(last, getTimestamp(cacheService.getVehicleChassis(vehicleId))); + last = Math.max(last, getTimestamp(cacheService.getVehicleOrder(vehicleId))); + last = Math.max(last, getTimestamp(cacheService.getVehicleSuspend(vehicleId))); + last = Math.max(last, getTimestamp(cacheService.getVehicleTailer(vehicleId))); + last = Math.max(last, getTimestamp(cacheService.getVehiclePosition(vehicleId))); + last = Math.max(last, getTimestamp(cacheService.getVehiclePath(vehicleId))); + last = Math.max(last, getTimestamp(cacheService.getVehicleFmsMessage(vehicleId))); + return last; + } + + private long getTimestamp(VehicleManagerCacheService.CacheEntry entry) { + return entry == null ? 0L : entry.getTimestamp(); + } + + private boolean isStale(VehicleManagerCacheService.CacheEntry entry, long now, long staleMillis) { + if (entry == null) { + return true; + } + return now - entry.getTimestamp() > staleMillis; + } + + private boolean isFault(JsonNode node) { + if (node == null || node.isNull()) { + return false; + } + Integer isActive = getInt(node, "isActive"); + if (isActive != null && isActive == 1) { + return true; + } + Integer level = getInt(node, "level"); + return level != null && level >= 4; + } + + private Integer getInt(JsonNode node, String field) { + if (node == null || node.isMissingNode()) { + return null; + } + JsonNode value = node.get(field); + if (value == null || value.isNull()) { + return null; + } + if (value.isInt() || value.isLong()) { + return value.asInt(); + } + if (value.isTextual()) { + try { + return Integer.parseInt(value.asText()); + } catch (NumberFormatException ignored) { + return null; + } + } + return null; + } + + private String getText(JsonNode node, String field) { + if (node == null || node.isMissingNode()) { + return null; + } + JsonNode value = node.get(field); + if (value == null || value.isNull()) { + return null; + } + return value.asText(); + } +} diff --git a/qaup-collision/src/main/java/com/qaup/collision/task/InMemoryTaskStore.java b/qaup-collision/src/main/java/com/qaup/collision/task/InMemoryTaskStore.java new file mode 100644 index 0000000..ac19d6e --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/task/InMemoryTaskStore.java @@ -0,0 +1,89 @@ +package com.qaup.collision.task; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Builder; +import lombok.Data; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +@Component +public class InMemoryTaskStore { + + private final AtomicLong idGenerator = new AtomicLong(1); + private final Map tasks = new ConcurrentHashMap<>(); + + @Data + @Builder + public static class Task { + private Long id; + private String name; + private String type; + private Integer priority; + private JsonNode payload; + private long createdAt; + private long updatedAt; + } + + public Task create(String name, String type, Integer priority, JsonNode payload) { + long now = System.currentTimeMillis(); + long id = idGenerator.getAndIncrement(); + Task task = Task.builder() + .id(id) + .name(name) + .type(type) + .priority(priority) + .payload(payload) + .createdAt(now) + .updatedAt(now) + .build(); + tasks.put(id, task); + return task; + } + + public Task update(long id, String name, String type, Integer priority, JsonNode payload) { + Task existing = tasks.get(id); + if (existing == null) { + return null; + } + long now = System.currentTimeMillis(); + if (name != null) { + existing.setName(name); + } + if (type != null) { + existing.setType(type); + } + existing.setPriority(priority); + existing.setPayload(payload); + existing.setUpdatedAt(now); + return existing; + } + + public Task get(long id) { + return tasks.get(id); + } + + public List listAll() { + List list = new ArrayList<>(tasks.values()); + list.sort(Comparator.comparing(Task::getId).reversed()); + return list; + } + + public int deleteMany(List ids) { + int removed = 0; + for (Long id : ids) { + if (id == null) { + continue; + } + if (tasks.remove(id) != null) { + removed++; + } + } + return removed; + } +} diff --git a/qaup-framework/src/main/java/com/qaup/framework/config/SecurityConfig.java b/qaup-framework/src/main/java/com/qaup/framework/config/SecurityConfig.java index ecfc90e..ac6f029 100644 --- a/qaup-framework/src/main/java/com/qaup/framework/config/SecurityConfig.java +++ b/qaup-framework/src/main/java/com/qaup/framework/config/SecurityConfig.java @@ -119,6 +119,7 @@ public class SecurityConfig // 前端 API 接口,允许匿名访问(测试阶段用) .requestMatchers("/system/**").permitAll() .requestMatchers("/api/v1/vehicles/**").permitAll() + .requestMatchers("/api/vehicle-manager/**").permitAll() // 除上面外的所有请求全部需要鉴权认证 .anyRequest().authenticated(); }) diff --git a/前端开发_无人车VehicleManager接口.md b/前端开发_无人车VehicleManager接口.md new file mode 100644 index 0000000..60e4e5d --- /dev/null +++ b/前端开发_无人车VehicleManager接口.md @@ -0,0 +1,222 @@ +# 前端开发文档:无人车 VehicleManager HTTP 接口 + +本文档描述本次新增的前端 HTTP 接口(基于“接口文档1/2”无人车数据整合)。 + +## 1. 基本信息 + +- **服务地址**:`http://localhost:8080` +- **接口前缀**:`/api/vehicle-manager` +- **鉴权**:当前已在后端放开 `permitAll`(无需 token 即可访问该前缀)。后续如恢复鉴权,前端需要按系统登录流程携带 `Authorization`。 +- **返回格式**:RuoYi 风格 `AjaxResult` + - `code`: 200 成功 + - `msg`: 提示信息 + - `data`: 业务数据 + - `timestamp`: 后端附加的时间戳(毫秒) + +## 2. 接口列表 + +### 2.1 车辆统计汇总 + +**GET** `/api/vehicle-manager/vehicles/summary` + +#### Query +- `staleMillis`(可选,默认 `30000`):缓存过期阈值(毫秒)。超过该阈值会被视为离线/不可用。 + +#### Response.data +```json +{ + "totalCount": 10, + "onlineCount": 8, + "offlineCount": 2, + "emergencyCount": 1, + "faultCount": 1, + "unknownCount": 0, + "staleMillis": 30000, + "timestamp": 1730000000000 +} +``` + +#### 说明 +- online 判定:`VehicleLoginStatus.loginStatus == "login"` 且未过期 +- emergency 判定:`VehicleSuspendReport.suspendStatus != 0` 且未过期 +- fault(故障)临时规则: + - `GetFmsMessage.isActive == 1`(字段存在时)或 + - `GetFmsMessage.level >= 4` + +--- + +### 2.2 车辆详情快照(缓存聚合) + +**GET** `/api/vehicle-manager/vehicles/{vehicleId}` + +#### Path +- `vehicleId`:车号(例如 `AET01`) + +#### Response.data +```json +{ + "vehicleId": "AET01", + "details": { "messageName": "VehicleDetails", "vehicleId": "AET01", "vehicleType": "QTRUCK" }, + "loginStatus": { "messageName": "VehicleLoginStatus", "vehicleId": "AET01", "loginStatus": "login" }, + "position": { "messageName": "VehiclePositionInfo", "vehicleId": "AET01", "x": 6.12, "y": 101.70 }, + "chassis": { "messageName": "VehicleChassisInfo", "vehicleId": "AET01", "sys_info": { "state_info": { "d_speed_kmph": 0, "d_battery_available": "0.0" } } }, + "suspend": { "messageName": "VehicleSuspendReport", "vehicleId": "AET01", "suspendStatus": 0 }, + "tailer": { "messageName": "VehicleTailerNum", "vehicleId": "AET01", "tailerNum": "0" }, + "order": { "messageName": "VehicleOrderInfo", "vehicleId": "AET01", "businessKey": "1767..." }, + "path": { "messageName": "NaviShortPathReport", "vehicleId": "AET01", "path": [ {"x": 1, "y": 2, "theta": 0} ] }, + "fmsMessage": { "messageName": "GetFmsMessage", "vehicleID": "AET01", "level": 4, "description": "..." }, + "lastSeenAt": 1730000000000 +} +``` + +#### 说明 +- 该接口只读后端内存缓存,不会额外打上游 HTTP。 +- 字段可能为 `null`(例如 WS 尚未推送/缓存未命中)。 + +--- + +### 2.3 电池信息(通过接口文档2上游 HTTP 获取后转发) + +**GET** `/api/vehicle-manager/vehicles/{vehicleId}/battery` + +#### 行为 +- 后端会调用上游(接口文档2):`POST /api/vehicle_manager/v1/vehicles/{vehicleId}/status` +- 并把其中的 `batteryStatus` 转发给前端 + +#### Response.data +```json +{ + "vehicleId": "AET01", + "batteryStatus": { + "mainBattery": { + "chargeLevel": 0.0, + "voltage": 0.0, + "current": 0.0, + "temperature": 0.0, + "chargingStatus": "DISCHARGING" + } + }, + "source": "vehicle_manager_http", + "upstreamTimestamp": 1767838334301.7153 +} +``` + +#### 说明 +- 上游不可达或无数据时,后端会返回默认结构(voltage/current/temperature=0,chargingStatus=DISCHARGING)。 + +--- + +### 2.4 车辆路径 + +**GET** `/api/vehicle-manager/vehicles/{vehicleId}/path` + +#### Response.data +```json +{ + "vehicleId": "AET01", + "rawPath": [ + { "x": 6.12613, "y": 101.70456, "theta": -1.52245 } + ], + "waypoints": [ + { "waypointId": "1", "longitude": 6.12613, "latitude": 101.70456, "status": "PENDING" } + ], + "source": "ws_cache" +} +``` + +#### 说明 +- `source=ws_cache`:来自文档1 `NaviShortPathReport.path` +- `source=http_fallback`:WS 缓存没有路径时,后端会走聚合服务兜底(从 `missionContext.waypoints` 返回) + +--- + +### 2.5 充电桩(占位接口) + +**GET** `/api/vehicle-manager/charging-stations` + +#### Response.data +```json +[] +``` + +--- + +## 3. 任务 CRUD(内存,不落库) + +> 注意:这是“前端任务管理/任务库”的 CRUD,数据仅存在于后端内存中,服务重启会清空。 + +### 3.1 列表 + +**GET** `/api/vehicle-manager/tasks?pageNum=1&pageSize=10` + +#### Response.data +```json +{ + "total": 1, + "pageNum": 1, + "pageSize": 10, + "rows": [ + { + "id": 1, + "name": "测试任务1", + "type": "CARGO_TRANSPORT", + "priority": 3, + "payload": { "note": "from frontend" }, + "createdAt": 1730000000000, + "updatedAt": 1730000000000 + } + ] +} +``` + +### 3.2 创建 + +**POST** `/api/vehicle-manager/tasks` + +Body: +```json +{ + "name": "测试任务1", + "type": "CARGO_TRANSPORT", + "priority": 3, + "payload": { + "note": "from frontend" + } +} +``` + +### 3.3 详情 + +**GET** `/api/vehicle-manager/tasks/{id}` + +### 3.4 更新 + +**PUT** `/api/vehicle-manager/tasks/{id}` + +Body(同创建结构): +```json +{ + "name": "测试任务1-已更新", + "type": "CARGO_TRANSPORT", + "priority": 5, + "payload": { "note": "updated" } +} +``` + +### 3.5 删除(支持批量) + +**DELETE** `/api/vehicle-manager/tasks/{ids}` + +示例: +- 删除单个:`/api/vehicle-manager/tasks/1` +- 删除多个:`/api/vehicle-manager/tasks/1,2,3` + +--- + +## 4. 前端接入建议 + +1) 页面初始化先调:`/vehicles/summary` 拿到统计。 +2) 点击某辆车再调:`/vehicles/{vehicleId}` 拿快照。 +3) 电池面板单独调:`/vehicles/{vehicleId}/battery`(该接口会请求上游,避免高频轮询)。 +4) 路径面板调:`/vehicles/{vehicleId}/path`。 +5) 任务管理页面直接使用 `/tasks` CRUD。