From 8d9f2f74c7a0fe337a17b84fcd21c797eba8b6a0 Mon Sep 17 00:00:00 2001 From: menghao <1584479611@qq.com> Date: Tue, 17 Mar 2026 13:35:21 +0800 Subject: [PATCH] =?UTF-8?q?=E6=93=8D=E4=BD=9C=E6=97=A5=E5=BF=97=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E4=B8=8E=E5=9B=9E=E6=BB=9A=E3=80=81=E7=94=98=E7=89=B9?= =?UTF-8?q?=E5=9B=BE=E9=80=BB=E8=BE=91=E9=87=8D=E7=BB=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 3 + .../controller/ObjectOperationLogController.java | 99 ++ .../web/controller/PlatformLibController.java | 79 +- .../web/controller/RouteWaypointsController.java | 78 +- .../com/ruoyi/web/controller/RoutesController.java | 81 +- .../ruoyi/system/domain/ObjectOperationLog.java | 88 ++ .../system/mapper/ObjectOperationLogMapper.java | 23 + .../com/ruoyi/system/mapper/PlatformLibMapper.java | 8 + .../system/service/IObjectOperationLogService.java | 33 + .../impl/ObjectOperationLogServiceImpl.java | 219 ++++ .../mapper/system/ObjectOperationLogMapper.xml | 56 ++ .../resources/mapper/system/PlatformLibMapper.xml | 19 + ruoyi-ui/package.json | 2 + ruoyi-ui/src/api/system/lib.js | 19 +- ruoyi-ui/src/api/system/objectLog.js | 23 + ruoyi-ui/src/api/system/routes.js | 19 +- ruoyi-ui/src/api/system/waypoints.js | 19 +- ruoyi-ui/src/lang/en.js | 3 +- ruoyi-ui/src/lang/zh.js | 3 +- ruoyi-ui/src/plugins/dialogDrag.js | 6 + ruoyi-ui/src/views/cesiumMap/index.vue | 6 +- ruoyi-ui/src/views/childRoom/GanttDrawer.vue | 1047 ++++++++++++++++++++ ruoyi-ui/src/views/childRoom/index.vue | 187 +++- ruoyi-ui/src/views/dialogs/OnlineMembersDialog.vue | 412 ++++++-- ruoyi-ui/src/views/dialogs/PlatformEditDialog.vue | 2 +- sql/object_operation_log.sql | 19 + 26 files changed, 2397 insertions(+), 156 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/controller/ObjectOperationLogController.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/domain/ObjectOperationLog.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/mapper/ObjectOperationLogMapper.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/IObjectOperationLogService.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/impl/ObjectOperationLogServiceImpl.java create mode 100644 ruoyi-system/src/main/resources/mapper/system/ObjectOperationLogMapper.xml create mode 100644 ruoyi-ui/src/api/system/objectLog.js create mode 100644 ruoyi-ui/src/views/childRoom/GanttDrawer.vue create mode 100644 sql/object_operation_log.sql diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ed94f44 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "git.ignoreLimitWarning": true +} \ No newline at end of file diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/ObjectOperationLogController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/ObjectOperationLogController.java new file mode 100644 index 0000000..e6f7d6d --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/ObjectOperationLogController.java @@ -0,0 +1,99 @@ +package com.ruoyi.web.controller; + +import java.util.List; + +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.system.domain.ObjectOperationLog; +import com.ruoyi.system.service.IObjectOperationLogService; + +/** + * 对象级操作日志(航线/航点/平台):分页查询、回滚 + */ +@RestController +@RequestMapping("/system/object-log") +public class ObjectOperationLogController extends BaseController { + + @Autowired + private IObjectOperationLogService objectOperationLogService; + + /** + * 分页查询对象级操作日志(按房间) + */ + @PreAuthorize("@ss.hasPermi('system:routes:list')") + @GetMapping("/list") + public TableDataInfo list( + @RequestParam(required = false) Long roomId, + @RequestParam(required = false) String operatorName, + @RequestParam(required = false) Integer operationType, + @RequestParam(required = false) String objectType) { + startPage(); + ObjectOperationLog query = new ObjectOperationLog(); + query.setRoomId(roomId); + query.setOperatorName(operatorName); + query.setOperationType(operationType); + query.setObjectType(objectType); + List list = objectOperationLogService.selectPage(query); + return getDataTable(list); + } + + /** + * 回滚到指定操作(数据库 + Redis 同步) + */ + @PreAuthorize("@ss.hasPermi('system:routes:edit')") + @PostMapping("/rollback") + public AjaxResult rollback(@RequestParam Long id) { + ObjectOperationLog origin = objectOperationLogService.selectById(id); + if (origin == null) { + return error("回滚失败:原始日志不存在"); + } + boolean ok = objectOperationLogService.rollback(id); + if (!ok) { + return error("回滚失败:无快照或对象类型不支持"); + } + + // 记录一条“回滚”操作日志,便于审计 + ObjectOperationLog rollbackLog = new ObjectOperationLog(); + rollbackLog.setRoomId(origin.getRoomId()); + rollbackLog.setOperatorId(getUserId()); + rollbackLog.setOperatorName(getUsername()); + rollbackLog.setOperationType(ObjectOperationLog.TYPE_ROLLBACK); + rollbackLog.setObjectType(origin.getObjectType()); + rollbackLog.setObjectId(origin.getObjectId()); + rollbackLog.setObjectName(origin.getObjectName()); + rollbackLog.setDetail("回滚操作:基于日志ID=" + origin.getId() + " 的" + toOpText(origin.getOperationType())); + // 简要记录被回滚日志的快照,方便排查(可选) + rollbackLog.setSnapshotBefore(origin.getSnapshotBefore()); + rollbackLog.setSnapshotAfter(origin.getSnapshotAfter()); + objectOperationLogService.saveLog(rollbackLog); + + return success(); + } + + private String toOpText(Integer type) { + if (type == null) return ""; + switch (type) { + case ObjectOperationLog.TYPE_INSERT: + return "新增"; + case ObjectOperationLog.TYPE_UPDATE: + return "修改"; + case ObjectOperationLog.TYPE_DELETE: + return "删除"; + case ObjectOperationLog.TYPE_SELECT: + return "选择"; + case ObjectOperationLog.TYPE_ROLLBACK: + return "回滚"; + default: + return ""; + } + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/PlatformLibController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/PlatformLibController.java index 596e28a..485ba31 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/PlatformLibController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/PlatformLibController.java @@ -10,7 +10,10 @@ import com.ruoyi.common.annotation.Log; import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.enums.BusinessType; +import com.alibaba.fastjson2.JSON; +import com.ruoyi.system.domain.ObjectOperationLog; import com.ruoyi.system.domain.PlatformLib; +import com.ruoyi.system.service.IObjectOperationLogService; import com.ruoyi.system.service.IPlatformLibService; import com.ruoyi.common.utils.poi.ExcelUtil; import com.ruoyi.common.core.page.TableDataInfo; @@ -31,6 +34,9 @@ public class PlatformLibController extends BaseController @Autowired private IPlatformLibService platformLibService; + @Autowired + private IObjectOperationLogService objectOperationLogService; + /** * 查询平台模版库列表 */ @@ -72,7 +78,9 @@ public class PlatformLibController extends BaseController @PreAuthorize("@ss.hasPermi('system:lib:add')") @Log(title = "平台模版库", businessType = BusinessType.INSERT) @PostMapping("/add") - public AjaxResult add(PlatformLib platformLib, @RequestParam("file") MultipartFile file) throws IOException + public AjaxResult add(PlatformLib platformLib, + @RequestParam("file") MultipartFile file, + @RequestParam(value = "roomId", required = false) Long roomId) throws IOException { // 判断前端是否有文件传过来 if (file != null && !file.isEmpty()) @@ -83,8 +91,25 @@ public class PlatformLibController extends BaseController platformLib.setIconUrl(fileName); } - // 执行原有的插入逻辑 - return toAjax(platformLibService.insertPlatformLib(platformLib)); + int rows = platformLibService.insertPlatformLib(platformLib); + if (rows > 0) { + try { + ObjectOperationLog log = new ObjectOperationLog(); + log.setRoomId(roomId); + log.setOperatorId(getUserId()); + log.setOperatorName(getUsername()); + log.setOperationType(ObjectOperationLog.TYPE_INSERT); + log.setObjectType(ObjectOperationLog.OBJ_PLATFORM); + log.setObjectId(platformLib.getId() != null ? String.valueOf(platformLib.getId()) : null); + log.setObjectName(platformLib.getName()); + log.setDetail("新增平台模板:" + platformLib.getName()); + log.setSnapshotAfter(JSON.toJSONString(platformLib)); + objectOperationLogService.saveLog(log); + } catch (Exception e) { + logger.warn("记录平台新增操作日志失败", e); + } + } + return toAjax(rows); } /** @@ -93,9 +118,30 @@ public class PlatformLibController extends BaseController @PreAuthorize("@ss.hasPermi('system:lib:edit')") @Log(title = "平台模版库", businessType = BusinessType.UPDATE) @PutMapping - public AjaxResult edit(@RequestBody PlatformLib platformLib) + public AjaxResult edit(@RequestBody PlatformLib platformLib, + @RequestParam(value = "roomId", required = false) Long roomId) { - return toAjax(platformLibService.updatePlatformLib(platformLib)); + PlatformLib before = platformLib.getId() != null ? platformLibService.selectPlatformLibById(platformLib.getId()) : null; + int rows = platformLibService.updatePlatformLib(platformLib); + if (rows > 0 && before != null) { + try { + ObjectOperationLog log = new ObjectOperationLog(); + log.setRoomId(roomId); + log.setOperatorId(getUserId()); + log.setOperatorName(getUsername()); + log.setOperationType(ObjectOperationLog.TYPE_UPDATE); + log.setObjectType(ObjectOperationLog.OBJ_PLATFORM); + log.setObjectId(String.valueOf(platformLib.getId())); + log.setObjectName(platformLib.getName()); + log.setDetail("修改平台模板:" + platformLib.getName()); + log.setSnapshotBefore(JSON.toJSONString(before)); + log.setSnapshotAfter(JSON.toJSONString(platformLib)); + objectOperationLogService.saveLog(log); + } catch (Exception e) { + logger.warn("记录平台修改操作日志失败", e); + } + } + return toAjax(rows); } /** @@ -104,8 +150,29 @@ public class PlatformLibController extends BaseController @PreAuthorize("@ss.hasPermi('system:lib:remove')") @Log(title = "平台模版库", businessType = BusinessType.DELETE) @DeleteMapping("/{ids}") - public AjaxResult remove(@PathVariable Long[] ids) + public AjaxResult remove(@PathVariable Long[] ids, + @RequestParam(value = "roomId", required = false) Long roomId) { + for (Long id : ids) { + PlatformLib before = platformLibService.selectPlatformLibById(id); + if (before != null) { + try { + ObjectOperationLog log = new ObjectOperationLog(); + log.setRoomId(roomId); + log.setOperatorId(getUserId()); + log.setOperatorName(getUsername()); + log.setOperationType(ObjectOperationLog.TYPE_DELETE); + log.setObjectType(ObjectOperationLog.OBJ_PLATFORM); + log.setObjectId(String.valueOf(id)); + log.setObjectName(before.getName()); + log.setDetail("删除平台模板:" + before.getName()); + log.setSnapshotBefore(JSON.toJSONString(before)); + objectOperationLogService.saveLog(log); + } catch (Exception e) { + logger.warn("记录平台删除操作日志失败", e); + } + } + } return toAjax(platformLibService.deletePlatformLibByIds(ids)); } } diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/RouteWaypointsController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/RouteWaypointsController.java index 875bd0f..f6d4d8f 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/RouteWaypointsController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/RouteWaypointsController.java @@ -12,12 +12,16 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import com.alibaba.fastjson2.JSON; import com.ruoyi.common.annotation.Log; import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.system.domain.ObjectOperationLog; import com.ruoyi.system.domain.RouteWaypoints; +import com.ruoyi.system.service.IObjectOperationLogService; import com.ruoyi.system.service.IRouteWaypointsService; +import org.springframework.web.bind.annotation.RequestParam; import com.ruoyi.common.utils.poi.ExcelUtil; import com.ruoyi.common.core.page.TableDataInfo; @@ -34,6 +38,9 @@ public class RouteWaypointsController extends BaseController @Autowired private IRouteWaypointsService routeWaypointsService; + @Autowired + private IObjectOperationLogService objectOperationLogService; + /** * 查询航线具体航点明细列表 */ @@ -71,34 +78,95 @@ public class RouteWaypointsController extends BaseController /** * 新增航线具体航点明细 + * @param roomId 可选,房间ID时记录到对象级操作日志 */ @PreAuthorize("@ss.hasPermi('system:routes:add')") @Log(title = "航线具体航点明细", businessType = BusinessType.INSERT) @PostMapping - public AjaxResult add(@RequestBody RouteWaypoints routeWaypoints) + public AjaxResult add(@RequestBody RouteWaypoints routeWaypoints, @RequestParam(required = false) Long roomId) { - return toAjax(routeWaypointsService.insertRouteWaypoints(routeWaypoints)); + int rows = routeWaypointsService.insertRouteWaypoints(routeWaypoints); + if (rows > 0) { + try { + ObjectOperationLog opLog = new ObjectOperationLog(); + opLog.setRoomId(roomId); + opLog.setOperatorId(getUserId()); + opLog.setOperatorName(getUsername()); + opLog.setOperationType(ObjectOperationLog.TYPE_INSERT); + opLog.setObjectType(ObjectOperationLog.OBJ_WAYPOINT); + opLog.setObjectId(routeWaypoints.getId() != null ? String.valueOf(routeWaypoints.getId()) : null); + opLog.setObjectName(routeWaypoints.getName() != null ? routeWaypoints.getName() : "航点"); + opLog.setDetail("新增航点:" + (routeWaypoints.getName() != null ? routeWaypoints.getName() : "")); + opLog.setSnapshotAfter(JSON.toJSONString(routeWaypoints)); + objectOperationLogService.saveLog(opLog); + } catch (Exception e) { + logger.warn("记录操作日志失败", e); + } + } + return toAjax(rows); } /** * 修改航线具体航点明细(复用航线编辑权限,航点属于航线的一部分) + * @param roomId 可选,房间ID时记录到对象级操作日志 */ @PreAuthorize("@ss.hasPermi('system:routes:edit')") @Log(title = "航线具体航点明细", businessType = BusinessType.UPDATE) @PutMapping - public AjaxResult edit(@RequestBody RouteWaypoints routeWaypoints) + public AjaxResult edit(@RequestBody RouteWaypoints routeWaypoints, @RequestParam(required = false) Long roomId) { - return toAjax(routeWaypointsService.updateRouteWaypoints(routeWaypoints)); + RouteWaypoints before = routeWaypoints.getId() != null ? routeWaypointsService.selectRouteWaypointsById(routeWaypoints.getId()) : null; + int rows = routeWaypointsService.updateRouteWaypoints(routeWaypoints); + if (rows > 0 && before != null) { + try { + ObjectOperationLog opLog = new ObjectOperationLog(); + opLog.setRoomId(roomId); + opLog.setOperatorId(getUserId()); + opLog.setOperatorName(getUsername()); + opLog.setOperationType(ObjectOperationLog.TYPE_UPDATE); + opLog.setObjectType(ObjectOperationLog.OBJ_WAYPOINT); + opLog.setObjectId(String.valueOf(routeWaypoints.getId())); + opLog.setObjectName(routeWaypoints.getName() != null ? routeWaypoints.getName() : "航点"); + opLog.setDetail("修改航点:" + (routeWaypoints.getName() != null ? routeWaypoints.getName() : routeWaypoints.getId())); + opLog.setSnapshotBefore(JSON.toJSONString(before)); + opLog.setSnapshotAfter(JSON.toJSONString(routeWaypoints)); + objectOperationLogService.saveLog(opLog); + } catch (Exception e) { + logger.warn("记录操作日志失败", e); + } + } + return toAjax(rows); } /** * 删除航线具体航点明细 + * @param roomId 可选,房间ID时记录到对象级操作日志 */ @PreAuthorize("@ss.hasPermi('system:routes:remove')") @Log(title = "航线具体航点明细", businessType = BusinessType.DELETE) @DeleteMapping("/{ids}") - public AjaxResult remove(@PathVariable Long[] ids) + public AjaxResult remove(@PathVariable Long[] ids, @RequestParam(required = false) Long roomId) { + for (Long id : ids) { + RouteWaypoints before = routeWaypointsService.selectRouteWaypointsById(id); + if (before != null) { + try { + ObjectOperationLog opLog = new ObjectOperationLog(); + opLog.setRoomId(roomId); + opLog.setOperatorId(getUserId()); + opLog.setOperatorName(getUsername()); + opLog.setOperationType(ObjectOperationLog.TYPE_DELETE); + opLog.setObjectType(ObjectOperationLog.OBJ_WAYPOINT); + opLog.setObjectId(String.valueOf(id)); + opLog.setObjectName(before.getName() != null ? before.getName() : "航点"); + opLog.setDetail("删除航点:" + (before.getName() != null ? before.getName() : id)); + opLog.setSnapshotBefore(JSON.toJSONString(before)); + objectOperationLogService.saveLog(opLog); + } catch (Exception e) { + logger.warn("记录操作日志失败", e); + } + } + } return toAjax(routeWaypointsService.deleteRouteWaypointsByIds(ids)); } } diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoutesController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoutesController.java index ff6fbc4..1048757 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoutesController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoutesController.java @@ -17,7 +17,9 @@ import com.ruoyi.common.annotation.Log; import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.system.domain.ObjectOperationLog; import com.ruoyi.system.domain.Routes; +import com.ruoyi.system.service.IObjectOperationLogService; import com.ruoyi.system.service.IRoutesService; import com.ruoyi.common.utils.poi.ExcelUtil; import com.ruoyi.common.core.page.TableDataInfo; @@ -45,6 +47,9 @@ public class RoutesController extends BaseController private IRoutesService routesService; @Autowired + private IObjectOperationLogService objectOperationLogService; + + @Autowired private RedisTemplate redisTemplate; @Autowired @@ -337,39 +342,101 @@ public class RoutesController extends BaseController /** * 新增实体部署与航线 + * @param roomId 可选,房间ID时记录到对象级操作日志便于按房间分页与回滚 */ @PreAuthorize("@ss.hasPermi('system:routes:add')") @Log(title = "实体部署与航线", businessType = BusinessType.INSERT) @PostMapping - public AjaxResult add(@RequestBody Routes routes) + public AjaxResult add(@RequestBody Routes routes, @RequestParam(required = false) Long roomId) { // 1. 执行插入,MyBatis 会通过 useGeneratedKeys="true" 自动将新 ID 注入 routes 对象 int rows = routesService.insertRoutes(routes); - - // 2. 不要用 toAjax,直接返回 success 并带上 routes 对象 - // 这样前端 response.data 就会包含这个带有 ID 的完整对象 + if (rows > 0) { + try { + ObjectOperationLog opLog = new ObjectOperationLog(); + opLog.setRoomId(roomId); + opLog.setOperatorId(getUserId()); + opLog.setOperatorName(getUsername()); + opLog.setOperationType(ObjectOperationLog.TYPE_INSERT); + opLog.setObjectType(ObjectOperationLog.OBJ_ROUTE); + opLog.setObjectId(String.valueOf(routes.getId())); + opLog.setObjectName(routes.getCallSign() != null ? routes.getCallSign() : "航线"); + opLog.setDetail("新增航线:" + (routes.getCallSign() != null ? routes.getCallSign() : routes.getId())); + opLog.setSnapshotAfter(JSON.toJSONString(routes)); + objectOperationLogService.saveLog(opLog); + } catch (Exception e) { + logger.warn("记录操作日志失败", e); + } + } return rows > 0 ? AjaxResult.success(routes) : AjaxResult.error("新增航线失败"); } /** * 修改实体部署与航线 + * @param roomId 可选,房间ID时记录到对象级操作日志 */ @PreAuthorize("@ss.hasPermi('system:routes:edit')") @Log(title = "实体部署与航线", businessType = BusinessType.UPDATE) @PutMapping - public AjaxResult edit(@RequestBody Routes routes) + public AjaxResult edit(@RequestBody Routes routes, @RequestParam(required = false) Long roomId) { - return toAjax(routesService.updateRoutes(routes)); + Routes before = null; + if (routes.getId() != null) { + before = routesService.selectRoutesById(routes.getId()); + } + int rows = routesService.updateRoutes(routes); + if (rows > 0 && before != null) { + try { + // 操作后快照用“更新后重新查库”的完整数据,避免请求体缺字段导致日志里误显示为“已清空” + Routes after = routesService.selectRoutesById(routes.getId()); + ObjectOperationLog opLog = new ObjectOperationLog(); + opLog.setRoomId(roomId); + opLog.setOperatorId(getUserId()); + opLog.setOperatorName(getUsername()); + opLog.setOperationType(ObjectOperationLog.TYPE_UPDATE); + opLog.setObjectType(ObjectOperationLog.OBJ_ROUTE); + opLog.setObjectId(String.valueOf(routes.getId())); + opLog.setObjectName(routes.getCallSign() != null ? routes.getCallSign() : "航线"); + opLog.setDetail("修改航线:" + (routes.getCallSign() != null ? routes.getCallSign() : routes.getId())); + opLog.setSnapshotBefore(JSON.toJSONString(before)); + opLog.setSnapshotAfter(after != null ? JSON.toJSONString(after) : JSON.toJSONString(routes)); + objectOperationLogService.saveLog(opLog); + } catch (Exception e) { + logger.warn("记录操作日志失败", e); + } + } + return toAjax(rows); } /** * 删除实体部署与航线(同时清除该航线在所有房间下的 Redis 导弹数据) + * @param roomId 可选,房间ID时记录到对象级操作日志 */ @PreAuthorize("@ss.hasPermi('system:routes:remove')") @Log(title = "实体部署与航线", businessType = BusinessType.DELETE) @DeleteMapping("/{ids}") - public AjaxResult remove(@PathVariable Long[] ids) + public AjaxResult remove(@PathVariable Long[] ids, @RequestParam(required = false) Long roomId) { + for (Long id : ids) { + Routes before = routesService.selectRoutesById(id); + if (before != null) { + try { + ObjectOperationLog opLog = new ObjectOperationLog(); + opLog.setRoomId(roomId); + opLog.setOperatorId(getUserId()); + opLog.setOperatorName(getUsername()); + opLog.setOperationType(ObjectOperationLog.TYPE_DELETE); + opLog.setObjectType(ObjectOperationLog.OBJ_ROUTE); + opLog.setObjectId(String.valueOf(id)); + opLog.setObjectName(before.getCallSign() != null ? before.getCallSign() : "航线"); + opLog.setDetail("删除航线:" + (before.getCallSign() != null ? before.getCallSign() : id)); + opLog.setSnapshotBefore(JSON.toJSONString(before)); + objectOperationLogService.saveLog(opLog); + } catch (Exception e) { + logger.warn("记录操作日志失败", e); + } + } + } int rows = routesService.deleteRoutesByIds(ids); if (rows > 0) { for (Long routeId : ids) { diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/ObjectOperationLog.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/ObjectOperationLog.java new file mode 100644 index 0000000..a97cc57 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/ObjectOperationLog.java @@ -0,0 +1,88 @@ +package com.ruoyi.system.domain; + +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 对象级操作日志(航线、航点、平台等,支持回滚) + * + * @author ruoyi + */ +public class ObjectOperationLog extends BaseEntity { + + private static final long serialVersionUID = 1L; + + /** 操作类型:新增 */ + public static final int TYPE_INSERT = 1; + /** 操作类型:修改 */ + public static final int TYPE_UPDATE = 2; + /** 操作类型:删除 */ + public static final int TYPE_DELETE = 3; + /** 操作类型:选择 */ + public static final int TYPE_SELECT = 4; + /** 操作类型:回滚 */ + public static final int TYPE_ROLLBACK = 5; + + /** 对象类型:航线 */ + public static final String OBJ_ROUTE = "route"; + /** 对象类型:航点 */ + public static final String OBJ_WAYPOINT = "waypoint"; + /** 对象类型:平台 */ + public static final String OBJ_PLATFORM = "platform"; + + /** 主键 */ + private Long id; + /** 房间ID */ + private Long roomId; + /** 操作人用户ID */ + private Long operatorId; + /** 操作人姓名 */ + private String operatorName; + /** 操作类型 1新增 2修改 3删除 4选择 */ + private Integer operationType; + /** 操作对象类型 route/waypoint/platform */ + private String objectType; + /** 业务对象ID */ + private String objectId; + /** 对象显示名 */ + private String objectName; + /** 详细操作描述 */ + private String detail; + /** 操作前快照JSON,用于回滚 */ + private String snapshotBefore; + /** 操作后快照JSON */ + private String snapshotAfter; + /** 相对时间 K+00:00:00 */ + private String kTime; + /** 创建时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createdAt; + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + public Long getRoomId() { return roomId; } + public void setRoomId(Long roomId) { this.roomId = roomId; } + public Long getOperatorId() { return operatorId; } + public void setOperatorId(Long operatorId) { this.operatorId = operatorId; } + public String getOperatorName() { return operatorName; } + public void setOperatorName(String operatorName) { this.operatorName = operatorName; } + public Integer getOperationType() { return operationType; } + public void setOperationType(Integer operationType) { this.operationType = operationType; } + public String getObjectType() { return objectType; } + public void setObjectType(String objectType) { this.objectType = objectType; } + public String getObjectId() { return objectId; } + public void setObjectId(String objectId) { this.objectId = objectId; } + public String getObjectName() { return objectName; } + public void setObjectName(String objectName) { this.objectName = objectName; } + public String getDetail() { return detail; } + public void setDetail(String detail) { this.detail = detail; } + public String getSnapshotBefore() { return snapshotBefore; } + public void setSnapshotBefore(String snapshotBefore) { this.snapshotBefore = snapshotBefore; } + public String getSnapshotAfter() { return snapshotAfter; } + public void setSnapshotAfter(String snapshotAfter) { this.snapshotAfter = snapshotAfter; } + public String getkTime() { return kTime; } + public void setkTime(String kTime) { this.kTime = kTime; } + public Date getCreatedAt() { return createdAt; } + public void setCreatedAt(Date createdAt) { this.createdAt = createdAt; } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/ObjectOperationLogMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/ObjectOperationLogMapper.java new file mode 100644 index 0000000..24fe73c --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/ObjectOperationLogMapper.java @@ -0,0 +1,23 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.system.domain.ObjectOperationLog; + +/** + * 对象级操作日志 Mapper + * + * @author ruoyi + */ +public interface ObjectOperationLogMapper { + + int insert(ObjectOperationLog log); + + ObjectOperationLog selectById(Long id); + + List selectPage(ObjectOperationLog query); + + int deleteById(Long id); + + /** 删除某条之后的所有日志(回滚时清理后续日志,可选策略) */ + int deleteByRoomIdAfterId(Long roomId, Long afterId); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/PlatformLibMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/PlatformLibMapper.java index 1a8192d..7d77906 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/PlatformLibMapper.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/PlatformLibMapper.java @@ -36,6 +36,14 @@ public interface PlatformLibMapper public int insertPlatformLib(PlatformLib platformLib); /** + * 新增平台模版库(带指定 ID,用于删除回滚时恢复原主键) + * + * @param platformLib 平台模版库 + * @return 结果 + */ + public int insertPlatformLibWithId(PlatformLib platformLib); + + /** * 修改平台模版库 * * @param platformLib 平台模版库 diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/IObjectOperationLogService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/IObjectOperationLogService.java new file mode 100644 index 0000000..033a8af --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/IObjectOperationLogService.java @@ -0,0 +1,33 @@ +package com.ruoyi.system.service; + +import java.util.List; +import com.ruoyi.system.domain.ObjectOperationLog; + +/** + * 对象级操作日志 服务接口 + * + * @author ruoyi + */ +public interface IObjectOperationLogService { + + /** + * 记录一条操作日志(同时写库并推送到 Redis 缓存) + */ + void saveLog(ObjectOperationLog log); + + /** + * 分页查询(优先从 Redis 取第一页近期数据以减轻数据库压力) + */ + List selectPage(ObjectOperationLog query); + + /** + * 根据ID查询(回滚时需要快照) + */ + ObjectOperationLog selectById(Long id); + + /** + * 回滚到指定日志:用 snapshot_before 恢复数据,并同步 Redis 缓存 + * @return 是否成功 + */ + boolean rollback(Long logId); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/ObjectOperationLogServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/ObjectOperationLogServiceImpl.java new file mode 100644 index 0000000..f86bc86 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/ObjectOperationLogServiceImpl.java @@ -0,0 +1,219 @@ +package com.ruoyi.system.service.impl; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import com.ruoyi.system.domain.PlatformLib; +import com.ruoyi.system.domain.RouteWaypoints; +import com.ruoyi.system.domain.Routes; +import com.ruoyi.system.mapper.PlatformLibMapper; +import com.ruoyi.system.mapper.RouteWaypointsMapper; +import com.ruoyi.system.mapper.RoutesMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.alibaba.fastjson2.JSON; +import com.ruoyi.system.domain.ObjectOperationLog; +import com.ruoyi.system.mapper.ObjectOperationLogMapper; +import com.ruoyi.system.service.IObjectOperationLogService; +import com.ruoyi.system.service.IRouteWaypointsService; + +/** + * 对象级操作日志 服务实现:写库 + Redis 缓存,回滚时同步 DB 与 Redis + */ +@Service +public class ObjectOperationLogServiceImpl implements IObjectOperationLogService { + + private static final String REDIS_KEY_PREFIX = "object_log:room:"; + private static final int REDIS_LIST_MAX = 500; + private static final long REDIS_EXPIRE_HOURS = 24; + + @Autowired + private ObjectOperationLogMapper objectOperationLogMapper; + + @Autowired + @Qualifier("stringObjectRedisTemplate") + private RedisTemplate redisTemplate; + + @Autowired + private RoutesMapper routesMapper; + + @Autowired + private RouteWaypointsMapper routeWaypointsMapper; + + @Autowired + private IRouteWaypointsService routeWaypointsService; + + @Autowired + private PlatformLibMapper platformLibMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public void saveLog(ObjectOperationLog log) { + objectOperationLogMapper.insert(log); + if (log.getRoomId() != null) { + String key = REDIS_KEY_PREFIX + log.getRoomId(); + redisTemplate.opsForList().rightPush(key, log); + redisTemplate.opsForList().trim(key, -REDIS_LIST_MAX, -1); + redisTemplate.expire(key, REDIS_EXPIRE_HOURS, TimeUnit.HOURS); + } + } + + @Override + public List selectPage(ObjectOperationLog query) { + return objectOperationLogMapper.selectPage(query); + } + + @Override + public ObjectOperationLog selectById(Long id) { + return objectOperationLogMapper.selectById(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean rollback(Long logId) { + ObjectOperationLog log = objectOperationLogMapper.selectById(logId); + if (log == null) return false; + Integer opType = log.getOperationType(); + if (opType == null) return false; + if (ObjectOperationLog.TYPE_INSERT == opType) { + return rollbackInsert(log); + } + if (ObjectOperationLog.TYPE_DELETE == opType) { + return rollbackDelete(log); + } + if (ObjectOperationLog.TYPE_UPDATE == opType && log.getSnapshotBefore() != null && !log.getSnapshotBefore().isEmpty()) { + return rollbackUpdate(log); + } + return false; + } + + private boolean rollbackUpdate(ObjectOperationLog log) { + String objectType = log.getObjectType(); + if (ObjectOperationLog.OBJ_ROUTE.equals(objectType)) return rollbackRouteUpdate(log); + if (ObjectOperationLog.OBJ_WAYPOINT.equals(objectType)) return rollbackWaypointUpdate(log); + if (ObjectOperationLog.OBJ_PLATFORM.equals(objectType)) { + return rollbackPlatformUpdate(log); + } + return false; + } + + private boolean rollbackDelete(ObjectOperationLog log) { + if (log.getSnapshotBefore() == null || log.getSnapshotBefore().isEmpty()) return false; + String objectType = log.getObjectType(); + if (ObjectOperationLog.OBJ_ROUTE.equals(objectType)) return rollbackRouteReinsert(log); + if (ObjectOperationLog.OBJ_WAYPOINT.equals(objectType)) return rollbackWaypointReinsert(log); + if (ObjectOperationLog.OBJ_PLATFORM.equals(objectType)) { + return rollbackPlatformReinsert(log); + } + return false; + } + + private boolean rollbackInsert(ObjectOperationLog log) { + if (log.getSnapshotAfter() == null || log.getSnapshotAfter().isEmpty()) return false; + String objectType = log.getObjectType(); + if (ObjectOperationLog.OBJ_ROUTE.equals(objectType)) { + Routes r = JSON.parseObject(log.getSnapshotAfter(), Routes.class); + if (r != null && r.getId() != null) { + routeWaypointsService.deleteRouteWaypointsByRouteId(r.getId()); + routesMapper.deleteRoutesById(r.getId()); + invalidateRoomCache(log.getRoomId()); + return true; + } + } + if (ObjectOperationLog.OBJ_WAYPOINT.equals(objectType)) { + RouteWaypoints wp = JSON.parseObject(log.getSnapshotAfter(), RouteWaypoints.class); + if (wp != null && wp.getId() != null) { + routeWaypointsMapper.deleteRouteWaypointsById(wp.getId()); + invalidateRoomCache(log.getRoomId()); + return true; + } + } + if (ObjectOperationLog.OBJ_PLATFORM.equals(objectType)) { + PlatformLib lib = JSON.parseObject(log.getSnapshotAfter(), PlatformLib.class); + if (lib != null && lib.getId() != null) { + platformLibMapper.deletePlatformLibById(lib.getId()); + invalidateRoomCache(log.getRoomId()); + return true; + } + } + return false; + } + + private boolean rollbackRouteUpdate(ObjectOperationLog log) { + Routes before = JSON.parseObject(log.getSnapshotBefore(), Routes.class); + if (before == null || before.getId() == null) return false; + routeWaypointsService.deleteRouteWaypointsByRouteId(before.getId()); + routesMapper.updateRoutes(before); + if (before.getWaypoints() != null && !before.getWaypoints().isEmpty()) { + for (RouteWaypoints wp : before.getWaypoints()) { + wp.setRouteId(before.getId()); + routeWaypointsMapper.insertRouteWaypoints(wp); + } + } + invalidateRoomCache(log.getRoomId()); + return true; + } + + private boolean rollbackWaypointUpdate(ObjectOperationLog log) { + RouteWaypoints before = JSON.parseObject(log.getSnapshotBefore(), RouteWaypoints.class); + if (before == null || before.getId() == null) return false; + routeWaypointsMapper.updateRouteWaypoints(before); + invalidateRoomCache(log.getRoomId()); + return true; + } + + private boolean rollbackRouteReinsert(ObjectOperationLog log) { + Routes before = JSON.parseObject(log.getSnapshotBefore(), Routes.class); + if (before == null) return false; + before.setId(null); + routesMapper.insertRoutes(before); + if (before.getWaypoints() != null && !before.getWaypoints().isEmpty()) { + for (RouteWaypoints wp : before.getWaypoints()) { + wp.setId(null); + wp.setRouteId(before.getId()); + routeWaypointsMapper.insertRouteWaypoints(wp); + } + } + invalidateRoomCache(log.getRoomId()); + return true; + } + + private boolean rollbackWaypointReinsert(ObjectOperationLog log) { + RouteWaypoints before = JSON.parseObject(log.getSnapshotBefore(), RouteWaypoints.class); + if (before == null) return false; + before.setId(null); + routeWaypointsMapper.insertRouteWaypoints(before); + invalidateRoomCache(log.getRoomId()); + return true; + } + + private boolean rollbackPlatformUpdate(ObjectOperationLog log) { + PlatformLib before = JSON.parseObject(log.getSnapshotBefore(), PlatformLib.class); + if (before == null || before.getId() == null) return false; + platformLibMapper.updatePlatformLib(before); + invalidateRoomCache(log.getRoomId()); + return true; + } + + private boolean rollbackPlatformReinsert(ObjectOperationLog log) { + PlatformLib before = JSON.parseObject(log.getSnapshotBefore(), PlatformLib.class); + if (before == null || before.getId() == null) return false; + // 如果该 ID 已经存在(例如之前已手动恢复过),则视为已回滚成功,避免主键冲突 + PlatformLib existed = platformLibMapper.selectPlatformLibById(before.getId()); + if (existed == null) { + platformLibMapper.insertPlatformLibWithId(before); + } + invalidateRoomCache(log.getRoomId()); + return true; + } + + private void invalidateRoomCache(Long roomId) { + if (roomId == null) return; + String key = REDIS_KEY_PREFIX + roomId; + redisTemplate.delete(key); + } +} diff --git a/ruoyi-system/src/main/resources/mapper/system/ObjectOperationLogMapper.xml b/ruoyi-system/src/main/resources/mapper/system/ObjectOperationLogMapper.xml new file mode 100644 index 0000000..87dc6e6 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/ObjectOperationLogMapper.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + select id, room_id, operator_id, operator_name, operation_type, object_type, object_id, object_name, + detail, snapshot_before, snapshot_after, k_time, created_at + from object_operation_log + + + + insert into object_operation_log (room_id, operator_id, operator_name, operation_type, object_type, object_id, object_name, detail, snapshot_before, snapshot_after, k_time) + values (#{roomId}, #{operatorId}, #{operatorName}, #{operationType}, #{objectType}, #{objectId}, #{objectName}, #{detail}, #{snapshotBefore}, #{snapshotAfter}, #{kTime}) + + + + + + + + delete from object_operation_log where id = #{id} + + + + delete from object_operation_log where room_id = #{roomId} and id > #{afterId} + + diff --git a/ruoyi-system/src/main/resources/mapper/system/PlatformLibMapper.xml b/ruoyi-system/src/main/resources/mapper/system/PlatformLibMapper.xml index 7161fdc..e4207de 100644 --- a/ruoyi-system/src/main/resources/mapper/system/PlatformLibMapper.xml +++ b/ruoyi-system/src/main/resources/mapper/system/PlatformLibMapper.xml @@ -68,4 +68,23 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" #{id} + + + + insert into platform_lib + + id, + name, + type, + specs_json, + icon_url, + + + #{id}, + #{name}, + #{type}, + #{specsJson}, + #{iconUrl}, + + \ No newline at end of file diff --git a/ruoyi-ui/package.json b/ruoyi-ui/package.json index 5f366ac..2613168 100644 --- a/ruoyi-ui/package.json +++ b/ruoyi-ui/package.json @@ -36,9 +36,11 @@ "file-saver": "2.0.5", "fuse.js": "6.4.3", "highlight.js": "9.18.5", + "html2canvas": "^1.4.1", "js-beautify": "1.13.0", "js-cookie": "3.0.1", "jsencrypt": "3.0.0-rc.1", + "jspdf": "^2.5.2", "mammoth": "^1.11.0", "nprogress": "0.2.0", "quill": "2.0.2", diff --git a/ruoyi-ui/src/api/system/lib.js b/ruoyi-ui/src/api/system/lib.js index dc891b1..99e82b4 100644 --- a/ruoyi-ui/src/api/system/lib.js +++ b/ruoyi-ui/src/api/system/lib.js @@ -21,31 +21,34 @@ export function getLib(id) { }) } -// 新增平台模版库 -export function addLib(data) { +// 新增平台模版库(可选 params.roomId 用于对象级操作日志) +export function addLib(data, params = {}) { return request({ url: '/system/lib/add', method: 'post', data: data, + params: params.roomId != null ? { roomId: params.roomId } : {}, headers: { 'Content-Type': 'multipart/form-data' } }) } -// 修改平台模版库 -export function updateLib(data) { +// 修改平台模版库(可选 params.roomId) +export function updateLib(data, params = {}) { return request({ url: '/system/lib', method: 'put', - data: data + data: data, + params: params.roomId != null ? { roomId: params.roomId } : {} }) } -// 删除平台模版库 -export function delLib(id) { +// 删除平台模版库(可选 params.roomId) +export function delLib(id, params = {}) { return request({ url: '/system/lib/' + id, - method: 'delete' + method: 'delete', + params: params.roomId != null ? { roomId: params.roomId } : {} }) } diff --git a/ruoyi-ui/src/api/system/objectLog.js b/ruoyi-ui/src/api/system/objectLog.js new file mode 100644 index 0000000..0e02627 --- /dev/null +++ b/ruoyi-ui/src/api/system/objectLog.js @@ -0,0 +1,23 @@ +import request from '@/utils/request' + +/** + * 对象级操作日志分页查询(按房间、操作人、类型等) + */ +export function listObjectLog(params) { + return request({ + url: '/system/object-log/list', + method: 'get', + params + }) +} + +/** + * 回滚到指定操作(数据库 + Redis 同步) + */ +export function rollbackObjectLog(id) { + return request({ + url: '/system/object-log/rollback', + method: 'post', + params: { id } + }) +} diff --git a/ruoyi-ui/src/api/system/routes.js b/ruoyi-ui/src/api/system/routes.js index afcb27f..f519267 100644 --- a/ruoyi-ui/src/api/system/routes.js +++ b/ruoyi-ui/src/api/system/routes.js @@ -17,30 +17,33 @@ export function getRoutes(id) { }) } -// 新增实体部署与航线 -export function addRoutes(data) { +// 新增实体部署与航线(可选 params.roomId 用于对象级操作日志按房间记录) +export function addRoutes(data, params = {}) { return request({ url: '/system/routes', method: 'post', - data: data + data: data, + params: params.roomId != null ? { roomId: params.roomId } : {} }) } -// 修改实体部署与航线(禁用防重复提交,航线编辑可能快速连续触发) -export function updateRoutes(data) { +// 修改实体部署与航线(可选 params.roomId;禁用防重复提交) +export function updateRoutes(data, params = {}) { return request({ url: '/system/routes', method: 'put', data: data, + params: params.roomId != null ? { roomId: params.roomId } : {}, headers: { repeatSubmit: false } }) } -// 删除实体部署与航线 -export function delRoutes(id) { +// 删除实体部署与航线(可选 params.roomId) +export function delRoutes(id, params = {}) { return request({ url: '/system/routes/' + id, - method: 'delete' + method: 'delete', + params: params.roomId != null ? { roomId: params.roomId } : {} }) } diff --git a/ruoyi-ui/src/api/system/waypoints.js b/ruoyi-ui/src/api/system/waypoints.js index 742e669..c70920e 100644 --- a/ruoyi-ui/src/api/system/waypoints.js +++ b/ruoyi-ui/src/api/system/waypoints.js @@ -17,29 +17,32 @@ export function getWaypoints(id) { }) } -// 新增航线具体航点明细 -export function addWaypoints(data) { +// 新增航线具体航点明细(可选 params.roomId 用于对象级操作日志) +export function addWaypoints(data, params = {}) { return request({ url: '/system/waypoints', method: 'post', - data: data + data: data, + params: params.roomId != null ? { roomId: params.roomId } : {} }) } -// 修改航线具体航点明细(禁用防重复提交,拖拽/批量保存可能快速连续触发) -export function updateWaypoints(data) { +// 修改航线具体航点明细(可选 params.roomId;禁用防重复提交) +export function updateWaypoints(data, params = {}) { return request({ url: '/system/waypoints', method: 'put', data: data, + params: params.roomId != null ? { roomId: params.roomId } : {}, headers: { repeatSubmit: false } }) } -// 删除航线具体航点明细 -export function delWaypoints(id) { +// 删除航线具体航点明细(可选 params.roomId) +export function delWaypoints(id, params = {}) { return request({ url: '/system/waypoints/' + id, - method: 'delete' + method: 'delete', + params: params.roomId != null ? { roomId: params.roomId } : {} }) } diff --git a/ruoyi-ui/src/lang/en.js b/ruoyi-ui/src/lang/en.js index b7166b4..53271e1 100644 --- a/ruoyi-ui/src/lang/en.js +++ b/ruoyi-ui/src/lang/en.js @@ -156,6 +156,7 @@ export default { inputMessage: 'Please enter message...', send: 'Send', pleaseInputMessage: 'Please enter message content', - operationRollbackSuccess: 'Operation rollback successful' + operationRollbackSuccess: 'Operation rollback successful', + noLogs: 'No operation logs' } } diff --git a/ruoyi-ui/src/lang/zh.js b/ruoyi-ui/src/lang/zh.js index 481dcac..eb35dbc 100644 --- a/ruoyi-ui/src/lang/zh.js +++ b/ruoyi-ui/src/lang/zh.js @@ -156,6 +156,7 @@ export default { inputMessage: '请输入消息...', send: '发送', pleaseInputMessage: '请输入消息内容', - operationRollbackSuccess: '操作回滚成功' + operationRollbackSuccess: '操作回滚成功', + noLogs: '暂无操作日志' } } diff --git a/ruoyi-ui/src/plugins/dialogDrag.js b/ruoyi-ui/src/plugins/dialogDrag.js index 45095a6..e71155a 100644 --- a/ruoyi-ui/src/plugins/dialogDrag.js +++ b/ruoyi-ui/src/plugins/dialogDrag.js @@ -22,6 +22,10 @@ function startDrag(e, container) { container.style.top = top + 'px' container.style.margin = '0' container.style.transform = 'none' + /* 锁定宽高,避免拖拽标题栏时弹窗尺寸被改变(只做位移) */ + container.style.width = rect.width + 'px' + container.style.height = rect.height + 'px' + container.style.boxSizing = 'border-box' const onMouseMove = (e2) => { e2.preventDefault() const dx = e2.clientX - startX @@ -46,6 +50,8 @@ function startDrag(e, container) { function onDocumentMouseDown(e) { if (e.button !== 0) return if (e.target.closest('.el-dialog__headerbtn') || e.target.closest('.el-message-box__headerbtn') || e.target.closest('.close-btn')) return + /* 右下角改尺寸手柄:不触发拖拽,只做缩放 */ + if (e.target.closest('.gantt-dialog-resize-handle')) return const elHeader = e.target.closest('.el-dialog__header') if (elHeader) { const wrapper = elHeader.closest('.el-dialog__wrapper') diff --git a/ruoyi-ui/src/views/cesiumMap/index.vue b/ruoyi-ui/src/views/cesiumMap/index.vue index e19d3ba..f9a6d35 100644 --- a/ruoyi-ui/src/views/cesiumMap/index.vue +++ b/ruoyi-ui/src/views/cesiumMap/index.vue @@ -508,7 +508,7 @@ export default { missileDialogVisible: false, missileForm: { angle: 0, - distance: 100, + distance: 1000, platformHeadingDeg: 0 }, /** 导弹预览:弹窗打开时显示的轨迹线与朝向图标,关闭时移除 */ @@ -4078,7 +4078,7 @@ export default { } const platformHeadingDeg = Number(this.missileForm.platformHeadingDeg) || 0 const angle = Number(this.missileForm.angle) || 0 - const distance = Number(this.missileForm.distance) || 100 + const distance = Number(this.missileForm.distance) || 1000 const actualBearingDeg = ((platformHeadingDeg + angle) % 360 + 360) % 360 const brng = Cesium.Math.toRadians(actualBearingDeg) const R = 6371000 @@ -4252,7 +4252,7 @@ export default { const startLat = item.startLat != null ? Number(item.startLat) : null if (launchK == null || startLng == null || startLat == null) continue const angle = Number(item.angle) || 0 - const distance = Number(item.distance) || 100 + const distance = Number(item.distance) || 1000 const platformHeadingDeg = Number(item.platformHeadingDeg) || 0 const actualBearingDeg = ((platformHeadingDeg + angle) % 360 + 360) % 360 const R = 6371000 diff --git a/ruoyi-ui/src/views/childRoom/GanttDrawer.vue b/ruoyi-ui/src/views/childRoom/GanttDrawer.vue new file mode 100644 index 0000000..d1f6da7 --- /dev/null +++ b/ruoyi-ui/src/views/childRoom/GanttDrawer.vue @@ -0,0 +1,1047 @@ + + + + + + + diff --git a/ruoyi-ui/src/views/childRoom/index.vue b/ruoyi-ui/src/views/childRoom/index.vue index e89aeba..213d83b 100644 --- a/ruoyi-ui/src/views/childRoom/index.vue +++ b/ruoyi-ui/src/views/childRoom/index.vue @@ -322,6 +322,7 @@ :send-private-chat="sendPrivateChat" :send-private-chat-history-request="sendPrivateChatHistoryRequest" :current-user-id="currentUserId" + @rollback-done="onOperationRollbackDone" /> @@ -396,6 +397,17 @@ @import="handleImportRoutes" /> + + + color) */ + ganttScenarioColors: {}, // 用户 userAvatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png', @@ -773,6 +791,30 @@ export default { this.wsConnection.sendObjectEditUnlock('route', this.routeEditLockedId); this.routeEditLockedId = null; } + }, + /** 打开甘特图抽屉时按方案加载颜色:同一方案取一种颜色(用该方案下第一条航线的平台色),不同方案不同颜色 */ + showGanttDrawer(visible) { + if (!visible) return; + this.ganttScenarioColors = {}; + if (!this.currentRoomId || !this.activeRouteIds.length) return; + const seenScenario = new Set(); + this.activeRouteIds.forEach(routeId => { + const route = this.routes.find(r => r.id === routeId); + if (!route || route.platformId == null) return; + const sid = route.scenarioId; + if (sid != null && seenScenario.has(sid)) return; + seenScenario.add(sid); + getPlatformStyle({ + roomId: this.currentRoomId, + routeId: route.id, + platformId: route.platformId + }).then(res => { + const s = res.data; + if (s && s.platformColor) { + this.$set(this.ganttScenarioColors, sid, s.platformColor); + } + }).catch(() => {}); + }); } }, computed: { @@ -816,6 +858,83 @@ export default { if (this.addHoldContext.mode === 'drawing') return '在最后两航点之间插入盘旋,保存航线时生效。'; return `在 ${this.addHoldContext.fromName} 与 ${this.addHoldContext.toName} 之间添加盘旋,到计划时间后沿切线飞往下一航点(原「下一格」航点将被移除)。`; }, + /** 甘特图:K 时范围(与推演时间轴一致,随航线/航点变化实时更新) */ + ganttTimeRange() { + return this.getDeductionTimeRange(); + }, + /** 甘特图:当前展示航线的任务条(K-时起止 + 颜色与地图航线一致) */ + ganttRouteBars() { + const { minMinutes, maxMinutes } = this.getDeductionTimeRange(); + const bars = []; + this.activeRouteIds.forEach(routeId => { + const route = this.routes.find(r => r.id === routeId); + if (!route || !route.waypoints || !route.waypoints.length) return; + try { + const { segments } = this.buildRouteTimeline(route.waypoints, minMinutes, maxMinutes, null); + if (!segments || segments.length === 0) return; + const startMinutes = segments[0].startTime; + const endMinutes = segments[segments.length - 1].endTime; + /* 同方案同色:按 scenarioId 取色,避免同一方案下航线444等与111/222/333颜色不一致 */ + let color = (route.scenarioId != null && this.ganttScenarioColors[route.scenarioId]) + ? this.ganttScenarioColors[route.scenarioId] + : null; + if (!color) { + const style = this.parseRouteStyle(route.attributes); + if (style && style.line && style.line.color) color = style.line.color; + else if (style && style.waypoint && style.waypoint.color) color = style.waypoint.color; + } + if (!color || this.isGanttColorTooDark(color)) color = '#409EFF'; + bars.push({ + id: `route-${route.id}`, + name: route.name || route.callSign || `航线${route.id}`, + startMinutes, + endMinutes, + color + }); + } catch (e) { + console.warn('ganttRouteBars buildRouteTimeline', routeId, e); + } + }); + return bars; + }, + /** 甘特图:当前展示航线中的盘旋段(与图例「盘旋」一致);需 pathData 才能算出盘旋段 */ + ganttHoldBars() { + const { minMinutes, maxMinutes } = this.getDeductionTimeRange(); + const bars = []; + const cesiumMap = this.$refs.cesiumMap; + this.activeRouteIds.forEach(routeId => { + const route = this.routes.find(r => r.id === routeId); + if (!route || !route.waypoints || !route.waypoints.length) return; + try { + let pathData = null; + if (cesiumMap && cesiumMap.getRoutePathWithSegmentIndices) { + const cachedRadii = (cesiumMap._routeHoldRadiiByRoute && cesiumMap._routeHoldRadiiByRoute[routeId]) ? cesiumMap._routeHoldRadiiByRoute[routeId] : {}; + const cachedEllipse = (cesiumMap._routeHoldEllipseParamsByRoute && cesiumMap._routeHoldEllipseParamsByRoute[routeId]) ? cesiumMap._routeHoldEllipseParamsByRoute[routeId] : {}; + const opts = (Object.keys(cachedRadii).length > 0 || Object.keys(cachedEllipse).length > 0) ? { holdRadiusByLegIndex: cachedRadii, holdEllipseParamsByLegIndex: cachedEllipse } : {}; + const ret = cesiumMap.getRoutePathWithSegmentIndices(route.waypoints, opts); + if (ret.path && ret.path.length > 0 && ret.segmentEndIndices && ret.segmentEndIndices.length > 0 && ret.holdArcRanges) { + pathData = { path: ret.path, segmentEndIndices: ret.segmentEndIndices, holdArcRanges: ret.holdArcRanges }; + } + } + const { segments } = this.buildRouteTimeline(route.waypoints, minMinutes, maxMinutes, pathData); + if (!segments) return; + const routeName = route.name || route.callSign || `航线${route.id}`; + segments.forEach((seg, idx) => { + if (seg.type !== 'hold') return; + bars.push({ + id: `hold-${route.id}-${idx}`, + name: `${routeName}-盘旋`, + startMinutes: seg.startTime, + endMinutes: seg.endTime, + color: '#E6A23C' + }); + }); + } catch (e) { + console.warn('ganttHoldBars buildRouteTimeline', routeId, e); + } + }); + return bars; + }, /** 白板模式下当前时间块应显示的实体(继承逻辑:当前时刻 = 上一时刻 + 本时刻差异) */ whiteboardDisplayEntities() { if (!this.showWhiteboardPanel || !this.currentWhiteboard || !this.currentWhiteboardTimeBlock) return [] @@ -970,11 +1089,11 @@ export default { // 安全确认:防止误操作 this.$modal.confirm('是否确认删除名称为 "' + platform.name + '" 的平台数据?').then(() => { // 调用后端接口删除 - return delLib(platform.id); + return delLib(platform.id, { roomId: this.currentRoomId }); }).then(() => { // 删除成功后的反馈 this.$modal.msgSuccess("删除成功"); - //重新分拣列表,让页面上的卡片消失 + // 重新分拣列表,让页面上的卡片消失 this.getPlatformList(); }).catch(() => { }); @@ -1481,6 +1600,11 @@ export default { showOnlineMembersDialog() { this.showOnlineMembers = true; }, + onOperationRollbackDone() { + // 回滚后同时刷新房间内航线/航点和平台库列表 + this.getList(); + this.getPlatformList(); + }, sendChat(content) { if (this.wsConnection && this.wsConnection.sendChat) { this.wsConnection.sendChat(content); @@ -1873,7 +1997,7 @@ export default { scenarioId: this.selectedPlanId, createTime: new Date().getTime() })); - addLib(data).then(response => { + addLib(data, { roomId: this.currentRoomId }).then(response => { this.$modal.msgSuccess("导入成功"); this.showImportDialog = false; this.getPlatformList(); @@ -1925,6 +2049,29 @@ export default { return null; } }, + /** 甘特图用:判断颜色是否过深(黑/深灰),若是则改用图例蓝 #409EFF */ + isGanttColorTooDark(color) { + if (!color || typeof color !== 'string') return true; + const s = color.trim().toLowerCase(); + if (s === '#000' || s === '#000000' || s === 'black') return true; + const hex = s.match(/^#([0-9a-f]{6})$/); + if (hex) { + const r = parseInt(hex[1].slice(0, 2), 16); + const g = parseInt(hex[1].slice(2, 4), 16); + const b = parseInt(hex[1].slice(4, 6), 16); + const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; + return luminance < 0.25; + } + const rgb = s.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/); + if (rgb) { + const r = Number(rgb[1]); + const g = Number(rgb[2]); + const b = Number(rgb[3]); + const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; + return luminance < 0.25; + } + return false; + }, // 航线编辑弹窗相关方法 openRouteDialog(route) { this.selectedRoute = route; @@ -1950,7 +2097,7 @@ export default { platformId: updatedRoute.platformId || null, attributes: updatedRoute.attributes != null ? updatedRoute.attributes : undefined }; - const res = await updateRoutes(apiData); + const res = await updateRoutes(apiData, { roomId: this.currentRoomId }); if (res.code === 200) { const newRouteData = { ...this.routes[index], @@ -2064,7 +2211,7 @@ export default { if (this.selectedRouteDetails && this.selectedRouteDetails.id === route.id) { this.selectedRouteDetails = null; } - const res = await delRoutes(route.id); + const res = await delRoutes(route.id, { roomId: this.currentRoomId }); if (res.code === 200) { this.$message.success('删除成功'); // 同步地图:如果该航线正在显示,立即清除 @@ -2278,7 +2425,7 @@ export default { }; try { - const response = await addRoutes(routeData); + const response = await addRoutes(routeData, { roomId: this.currentRoomId }); if (response.code === 200) { const savedRoute = response.data; const newRouteId = savedRoute?.id; @@ -2822,7 +2969,7 @@ export default { attributes: route.attributes || this.getDefaultRouteAttributes(), waypoints: cleanWaypoints }; - const res = await addRoutes(payload); + const res = await addRoutes(payload, { roomId: this.currentRoomId }); if (res.code === 200) successCount++; } this.showImportRoutesDialog = false; @@ -3578,8 +3725,7 @@ export default { }, generateGanttChart() { - const url = this.$router.resolve('/ganttChart').href - window.open(url, '_blank') + this.showGanttDrawer = true }, systemDescription() { @@ -4403,23 +4549,32 @@ export default { } const path = pathData ? pathData.path : null; const segmentEndIndices = pathData ? pathData.segmentEndIndices : null; - const position = this.getPositionFromTimeline(segments, minutesFromK, path, segmentEndIndices); + const lastSeg = segments.length > 0 ? segments[segments.length - 1] : null; + const scheduledEndTime = waypoints.length > 0 ? this.waypointStartTimeToMinutes(waypoints[waypoints.length - 1].startTime) : null; + const effectiveMinutes = (scheduledEndTime != null && lastSeg != null && minutesFromK > scheduledEndTime && lastSeg.endTime > scheduledEndTime) + ? scheduledEndTime + : minutesFromK; + if (effectiveMinutes !== minutesFromK) { + warnings.push('已过最后航点计划K时,平台未到达终点,存在时间冲突(盘旋或速度不足),平台已停止于该时刻位置。'); + } + const position = this.getPositionFromTimeline(segments, effectiveMinutes, path, segmentEndIndices); const stepMin = 1 / 60; - const nextPosition = this.getPositionFromTimeline(segments, minutesFromK + stepMin, path, segmentEndIndices); - const previousPosition = this.getPositionFromTimeline(segments, minutesFromK - stepMin, path, segmentEndIndices); - // 当前所在航段,用于标牌速度(与数据库航点 speed 对应) + const nextPosition = this.getPositionFromTimeline(segments, effectiveMinutes + stepMin, path, segmentEndIndices); + const previousPosition = this.getPositionFromTimeline(segments, effectiveMinutes - stepMin, path, segmentEndIndices); + // 当前所在航段,用于标牌速度(与数据库航点 speed 对应);冻结时按 effectiveMinutes 取段 let currentSegment = null; + const segTime = effectiveMinutes; if (segments && segments.length > 0) { - if (minutesFromK <= segments[0].startTime) { + if (segTime <= segments[0].startTime) { const s = segments[0]; currentSegment = { legIndex: s.legIndex, speedKmh: waypoints[s.legIndex] ? (Number(waypoints[s.legIndex].speed) || 800) : 800 }; - } else if (minutesFromK >= segments[segments.length - 1].endTime) { + } else if (segTime >= segments[segments.length - 1].endTime) { const s = segments[segments.length - 1]; currentSegment = { legIndex: s.legIndex, speedKmh: s.speedKmh != null ? s.speedKmh : (waypoints[s.legIndex] ? (Number(waypoints[s.legIndex].speed) || 800) : 800) }; } else { for (let i = 0; i < segments.length; i++) { const s = segments[i]; - if (minutesFromK >= s.startTime && minutesFromK < s.endTime) { + if (segTime >= s.startTime && segTime < s.endTime) { currentSegment = { legIndex: s.legIndex, speedKmh: s.speedKmh != null ? s.speedKmh : (waypoints[s.legIndex] ? (Number(waypoints[s.legIndex].speed) || 800) : 800) }; break; } diff --git a/ruoyi-ui/src/views/dialogs/OnlineMembersDialog.vue b/ruoyi-ui/src/views/dialogs/OnlineMembersDialog.vue index f729a6a..89fb0f9 100644 --- a/ruoyi-ui/src/views/dialogs/OnlineMembersDialog.vue +++ b/ruoyi-ui/src/views/dialogs/OnlineMembersDialog.vue @@ -69,45 +69,98 @@

{{ $t('onlineMembersDialog.objectOperationLogs') }}

- - {{ $t('onlineMembersDialog.rollbackOperation') }} -
- - -
-
{{ log.user }}
-
{{ log.action }}
-
{{ $t('onlineMembersDialog.objectOperationLogs') }}:{{ log.object }}
-
{{ log.detail }}
-
-
-
- +
+ + +
+
{{ log.operatorName || log.user }}
+
{{ logDisplayAction(log) }}
+
操作对象:{{ log.objectName || log.object }}
+
{{ log.detail }}
+
+
+
+
{{ $t('onlineMembersDialog.noLogs') || '暂无操作日志' }}
+ +
+ + -
-

{{ $t('onlineMembersDialog.rollbackConfirmText') }}

-

{{ $t('onlineMembersDialog.rollbackWarning') }}

+
+
+ 操作人: + {{ activeLog.operatorName }} +
+
+ 操作类型: + {{ logDisplayAction(activeLog) }} +
+
+ 对象类型: + {{ displayObjectType(activeLog) }} +
+
+ 操作对象: + {{ activeLog.objectName || activeLog.objectId }} +
+
+ 时间: + {{ logDisplayTime(activeLog) }} +
+
+ 描述: + {{ activeLog.detail }} +
+ +
+
关键字段变更
+
+ {{ c.label || c.field }}: + +
+
+
+ 暂无变更明细,仅记录了概要描述。 +
- {{ $t('leftMenu.cancel') }} - {{ $t('onlineMembersDialog.confirmRollback') }} + {{ $t('leftMenu.cancel') }} + + {{ $t('onlineMembersDialog.rollbackOperation') }} +
@@ -200,7 +253,10 @@