yuanzhongqiao пре 1 месец
родитељ
комит
cb19491458
100 измењених фајлова са 10533 додато и 57 уклоњено
  1. 122 0
      .gitignore
  2. 241 57
      README.md
  3. 132 0
      aegis-admin/pom.xml
  4. 32 0
      aegis-admin/src/main/java/com/aegis/RuoYiApplication.java
  5. 18 0
      aegis-admin/src/main/java/com/aegis/RuoYiServletInitializer.java
  6. 200 0
      aegis-admin/src/main/java/com/aegis/web/controller/camera/CameraController.java
  7. 161 0
      aegis-admin/src/main/java/com/aegis/web/controller/cases/CaseController.java
  8. 94 0
      aegis-admin/src/main/java/com/aegis/web/controller/common/CaptchaController.java
  9. 168 0
      aegis-admin/src/main/java/com/aegis/web/controller/common/CommonController.java
  10. 396 0
      aegis-admin/src/main/java/com/aegis/web/controller/emergency/dispatch/DispatchTaskController.java
  11. 220 0
      aegis-admin/src/main/java/com/aegis/web/controller/emergency/duty/DutyAlertController.java
  12. 100 0
      aegis-admin/src/main/java/com/aegis/web/controller/emergency/duty/DutyAlertConvertController.java
  13. 48 0
      aegis-admin/src/main/java/com/aegis/web/controller/emergency/duty/DutyAlertLogController.java
  14. 138 0
      aegis-admin/src/main/java/com/aegis/web/controller/emergency/duty/DutyAlertTransferController.java
  15. 100 0
      aegis-admin/src/main/java/com/aegis/web/controller/emergency/duty/DutyShiftController.java
  16. 96 0
      aegis-admin/src/main/java/com/aegis/web/controller/emergency/duty/DutyShiftMemberController.java
  17. 204 0
      aegis-admin/src/main/java/com/aegis/web/controller/emergency/event/EventController.java
  18. 47 0
      aegis-admin/src/main/java/com/aegis/web/controller/emergency/plan/PlanApprovalController.java
  19. 110 0
      aegis-admin/src/main/java/com/aegis/web/controller/emergency/plan/PlanController.java
  20. 66 0
      aegis-admin/src/main/java/com/aegis/web/controller/emergency/plan/PlanParticipantController.java
  21. 71 0
      aegis-admin/src/main/java/com/aegis/web/controller/emergency/plan/PlanStepController.java
  22. 69 0
      aegis-admin/src/main/java/com/aegis/web/controller/emergency/plan/PlanStepResourceController.java
  23. 44 0
      aegis-admin/src/main/java/com/aegis/web/controller/emergency/plan/PlanUsageController.java
  24. 51 0
      aegis-admin/src/main/java/com/aegis/web/controller/emergency/plan/PlanVersionController.java
  25. 74 0
      aegis-admin/src/main/java/com/aegis/web/controller/emergency/report/ProvinceReportController.java
  26. 83 0
      aegis-admin/src/main/java/com/aegis/web/controller/emergency/report/ReportTemplateController.java
  27. 100 0
      aegis-admin/src/main/java/com/aegis/web/controller/file/FileController.java
  28. 106 0
      aegis-admin/src/main/java/com/aegis/web/controller/knowledge/KnowledgeCategoryController.java
  29. 172 0
      aegis-admin/src/main/java/com/aegis/web/controller/knowledge/KnowledgeDocumentController.java
  30. 121 0
      aegis-admin/src/main/java/com/aegis/web/controller/monitor/CacheController.java
  31. 27 0
      aegis-admin/src/main/java/com/aegis/web/controller/monitor/ServerController.java
  32. 82 0
      aegis-admin/src/main/java/com/aegis/web/controller/monitor/SysLogininforController.java
  33. 69 0
      aegis-admin/src/main/java/com/aegis/web/controller/monitor/SysOperlogController.java
  34. 83 0
      aegis-admin/src/main/java/com/aegis/web/controller/monitor/SysUserOnlineController.java
  35. 242 0
      aegis-admin/src/main/java/com/aegis/web/controller/notification/NotificationController.java
  36. 106 0
      aegis-admin/src/main/java/com/aegis/web/controller/resource/dashboard/ResourceDashboardController.java
  37. 176 0
      aegis-admin/src/main/java/com/aegis/web/controller/resource/equipment/ResourceEquipmentController.java
  38. 164 0
      aegis-admin/src/main/java/com/aegis/web/controller/resource/expert/ResourceExpertController.java
  39. 108 0
      aegis-admin/src/main/java/com/aegis/web/controller/resource/material/ResourceMaterialCategoryController.java
  40. 230 0
      aegis-admin/src/main/java/com/aegis/web/controller/resource/material/ResourceMaterialController.java
  41. 262 0
      aegis-admin/src/main/java/com/aegis/web/controller/resource/procurement/ResourceProcurementController.java
  42. 307 0
      aegis-admin/src/main/java/com/aegis/web/controller/resource/requisition/ResourceRequisitionController.java
  43. 201 0
      aegis-admin/src/main/java/com/aegis/web/controller/resource/supplier/ResourceSupplierController.java
  44. 83 0
      aegis-admin/src/main/java/com/aegis/web/controller/resource/team/ResourceTeamController.java
  45. 51 0
      aegis-admin/src/main/java/com/aegis/web/controller/resource/team/ResourceTeamLogController.java
  46. 108 0
      aegis-admin/src/main/java/com/aegis/web/controller/resource/team/ResourceTeamMemberController.java
  47. 126 0
      aegis-admin/src/main/java/com/aegis/web/controller/resource/vehicle/ResourceVehicleController.java
  48. 97 0
      aegis-admin/src/main/java/com/aegis/web/controller/resource/warehouse/ResourceWarehouseController.java
  49. 133 0
      aegis-admin/src/main/java/com/aegis/web/controller/system/SysConfigController.java
  50. 132 0
      aegis-admin/src/main/java/com/aegis/web/controller/system/SysDeptController.java
  51. 121 0
      aegis-admin/src/main/java/com/aegis/web/controller/system/SysDictDataController.java
  52. 131 0
      aegis-admin/src/main/java/com/aegis/web/controller/system/SysDictTypeController.java
  53. 29 0
      aegis-admin/src/main/java/com/aegis/web/controller/system/SysIndexController.java
  54. 131 0
      aegis-admin/src/main/java/com/aegis/web/controller/system/SysLoginController.java
  55. 142 0
      aegis-admin/src/main/java/com/aegis/web/controller/system/SysMenuController.java
  56. 91 0
      aegis-admin/src/main/java/com/aegis/web/controller/system/SysNoticeController.java
  57. 129 0
      aegis-admin/src/main/java/com/aegis/web/controller/system/SysPostController.java
  58. 148 0
      aegis-admin/src/main/java/com/aegis/web/controller/system/SysProfileController.java
  59. 38 0
      aegis-admin/src/main/java/com/aegis/web/controller/system/SysRegisterController.java
  60. 262 0
      aegis-admin/src/main/java/com/aegis/web/controller/system/SysRoleController.java
  61. 256 0
      aegis-admin/src/main/java/com/aegis/web/controller/system/SysUserController.java
  62. 183 0
      aegis-admin/src/main/java/com/aegis/web/controller/tool/TestController.java
  63. 125 0
      aegis-admin/src/main/java/com/aegis/web/core/config/SwaggerConfig.java
  64. 1 0
      aegis-admin/src/main/resources/META-INF/spring-devtools.properties
  65. 61 0
      aegis-admin/src/main/resources/application-druid.yml
  66. 136 0
      aegis-admin/src/main/resources/application.yml
  67. 24 0
      aegis-admin/src/main/resources/banner.txt
  68. 38 0
      aegis-admin/src/main/resources/i18n/messages.properties
  69. 93 0
      aegis-admin/src/main/resources/logback.xml
  70. 20 0
      aegis-admin/src/main/resources/mybatis/mybatis-config.xml
  71. 54 0
      aegis-camera/pom.xml
  72. 54 0
      aegis-camera/src/main/java/com/aegis/camera/config/ZLMediaKitProperties.java
  73. 58 0
      aegis-camera/src/main/java/com/aegis/camera/domain/Camera.java
  74. 15 0
      aegis-camera/src/main/java/com/aegis/camera/mapper/CameraMapper.java
  75. 21 0
      aegis-camera/src/main/java/com/aegis/camera/service/ICameraService.java
  76. 35 0
      aegis-camera/src/main/java/com/aegis/camera/service/StreamProtocolHandler.java
  77. 152 0
      aegis-camera/src/main/java/com/aegis/camera/service/StreamSessionService.java
  78. 40 0
      aegis-camera/src/main/java/com/aegis/camera/service/ZLMediaKitService.java
  79. 121 0
      aegis-camera/src/main/java/com/aegis/camera/service/impl/CameraServiceImpl.java
  80. 177 0
      aegis-camera/src/main/java/com/aegis/camera/service/impl/RtspStreamProtocolHandler.java
  81. 94 0
      aegis-camera/src/main/java/com/aegis/camera/service/impl/ZLMediaKitServiceImpl.java
  82. 83 0
      aegis-camera/src/main/java/com/aegis/camera/task/StreamCleanupTask.java
  83. 47 0
      aegis-case/pom.xml
  84. 68 0
      aegis-case/src/main/java/com/aegis/cases/domain/Case.java
  85. 14 0
      aegis-case/src/main/java/com/aegis/cases/mapper/CaseMapper.java
  86. 57 0
      aegis-case/src/main/java/com/aegis/cases/service/ICaseService.java
  87. 109 0
      aegis-case/src/main/java/com/aegis/cases/service/impl/CaseServiceImpl.java
  88. 130 0
      aegis-common/pom.xml
  89. 19 0
      aegis-common/src/main/java/com/aegis/common/annotation/Anonymous.java
  90. 33 0
      aegis-common/src/main/java/com/aegis/common/annotation/DataScope.java
  91. 28 0
      aegis-common/src/main/java/com/aegis/common/annotation/DataSource.java
  92. 197 0
      aegis-common/src/main/java/com/aegis/common/annotation/Excel.java
  93. 18 0
      aegis-common/src/main/java/com/aegis/common/annotation/Excels.java
  94. 51 0
      aegis-common/src/main/java/com/aegis/common/annotation/Log.java
  95. 40 0
      aegis-common/src/main/java/com/aegis/common/annotation/RateLimiter.java
  96. 31 0
      aegis-common/src/main/java/com/aegis/common/annotation/RepeatSubmit.java
  97. 24 0
      aegis-common/src/main/java/com/aegis/common/annotation/Sensitive.java
  98. 122 0
      aegis-common/src/main/java/com/aegis/common/config/RuoYiConfig.java
  99. 67 0
      aegis-common/src/main/java/com/aegis/common/config/serializer/SensitiveJsonSerializer.java
  100. 44 0
      aegis-common/src/main/java/com/aegis/common/constant/CacheConstants.java

+ 122 - 0
.gitignore

@@ -0,0 +1,122 @@
+######################################################################
+# Build — Java / Maven
+######################################################################
+
+target/
+!.mvn/wrapper/maven-wrapper.jar
+.flattened-pom.xml
+dependency-reduced-pom.xml
+*.class
+hs_err_pid*
+replay_pid*
+
+# Gradle(若子模块使用)
+.gradle/
+build/
+!gradle/wrapper/gradle-wrapper.jar
+
+######################################################################
+# 前端(aegis-ui / Node)
+######################################################################
+
+aegis-ui/node_modules/
+aegis-ui/dist/
+aegis-ui/.cache/
+aegis-ui/coverage/
+aegis-ui/.eslintcache
+aegis-ui/.sass-cache/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+*.tsbuildinfo
+
+# 仅本地覆盖的环境变量(仓库内可保留无密钥的 .env.development 等模板)
+aegis-ui/.env.local
+aegis-ui/.env.*.local
+
+######################################################################
+# IDE
+######################################################################
+
+# STS / Eclipse
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings/
+.springBeans
+
+# IntelliJ IDEA
+.idea/
+*.iws
+*.iml
+*.ipr
+out/
+
+# JRebel
+rebel.xml
+
+# NetBeans
+nbproject/private/
+nbbuild/
+nbdist/
+.nb-gradle/
+# 注意:前端构建产物在 aegis-ui/dist,已单独忽略;根目录下 NetBeans 的 dist 仍按此处忽略
+
+# Visual Studio Code(需共享时改为 .vscode/* + 下方反选具体文件)
+.vscode/
+
+# Cursor 本地
+.cursor/
+
+######################################################################
+# 操作系统
+######################################################################
+
+.DS_Store
+.DS_Store?
+Thumbs.db
+ehthumbs.db
+Desktop.ini
+
+######################################################################
+# 运行期 / 本机数据(勿推远程)
+######################################################################
+
+# 上传目录(与 application 中 profile 等配置对应时)
+**/app/upload/
+app/upload/
+upload/
+logs/
+*.log
+
+# 本地 Spring 覆写(放数据库密码、Redis 等)
+**/application-local.yml
+**/application-local.yaml
+**/application-local.properties
+**/application-local-*.yml
+**/application-local-*.properties
+
+# 本机或临时证书/密钥(按需保留扩展名;勿把真实密钥放入仓库)
+*.jks
+*.p12
+
+######################################################################
+# 其他
+######################################################################
+
+*.swp
+*.swo
+*~
+*.tmp
+*.temp
+*.xml.versionsBackup
+
+# 根目录下仅用于本机的杂项目录(有正式文档请放 docs/)
+/doc/
+
+!*/build/*.java
+!*/build/*.html
+!*/build/*.xml

+ 241 - 57
README.md

@@ -1,92 +1,276 @@
-# baoan-yingji-system
+# 安澜应急指挥系统
+
+[![Java](https://img.shields.io/badge/Java-1.8-orange.svg)](https://www.oracle.com/java/)
+[![Spring Boot](https://img.shields.io/badge/Spring%20Boot-2.5.15-brightgreen.svg)](https://spring.io/projects/spring-boot)
+[![Vue](https://img.shields.io/badge/Vue-2.6-green.svg)](https://vuejs.org/)
+
+**安澜应急指挥**是一套面向**政府与企事业单位**的应急业务软件:在统一门户下支撑**日常值守、事件接报、调度与处置、资源与预案、数据报送、案例与知识、视频与系统集成**等能力。
+
+![主控制台](images/index.png)
+---
+
+## 一、产品定位与适用场景
+
+| 维度 | 说明 |
+| --- | --- |
+| **谁在用** | 应急指挥中心值班员、调度员、业务科室与系统管理员;可按角色授权访问不同菜单与数据范围。 |
+| **解决什么** | 将事件与任务、资源与位置、预案与流程、上报与留痕放在同一套系统里,减少多系统切换与信息断层。 |
+| **典型场景** | 值守排班与缺班提醒;接警/调度/处置三类工作台与地图协同;资源(队伍/车辆/专家/物资等)全生命周期与看板;上级单位要求的数据报送;案例沉淀与知识检索;摄像头等设备的统一登记与能力扩展。 |
+
+
+---
+
+## 二、能力概览(按业务域)
+
+下表帮助你在“菜单很多”时建立心理模型;具体以你环境中的**菜单与权限**为准。
+
+| 业务域 | 能力要点 |
+| --- | --- |
+| **应急调度** | 排班、接警工作台、调度工作台、处置工作台;与地图、任务状态、参与方等联动。 |
+| **数据报送** | 面向规定的报送链路(如上级报送、模板配置等),支持查询与留痕。 |
+| **预案管理** | 预案维表、状态与发布/审批流(以实际配置为准)。 |
+| **资源管理** | 资源看板、队伍/车辆/专家/装备/仓库/物资,以及供应、采购、征用等扩展能力。 |
+| **案例与知识** | 案例库、知识库分类与内容管理。 |
+| **系统集成** | 摄像头等设备接入与维护(可配合 ZLMediaKit 等流媒体能力,按部署启用)。 |
+| **系统管理 / 监控** | 用户、角色、菜单、部门、字典、参数、通知公告、日志与系统监控等。 |
+| **首页看板** | KPI、趋势与分布类图表、最新事件/调度/待办/公告等;支持演示/联调环境变量(见下节与产品说明)。 |
+
+**地图**:业务页中的地图能力依赖**高德 Web 端**;需在系统**参数配置**中维护 `amap.api.key` 等(前端从后端拉取,未配置时会有明确提示)
+
+---
+
+## 三、技术架构(简图)
+
+```mermaid
+flowchart LR
+  subgraph client [浏览器]
+    UI[Vue 2.6 前端 aegis-ui]
+  end
+  subgraph server [应用层]
+    API[Spring Boot 单体 aegis-admin]
+  end
+  subgraph data [数据与缓存]
+    MySQL[(MySQL)]
+    Redis[(Redis)]
+  end
+  subgraph ext [按部署选装]
+    ZLM[ZLMediaKit]
+    JIT[Jitsi / Nginx 等]
+  end
+  UI -->|/dev-api 或 /prod-api 等同源前缀| API
+  API --> MySQL
+  API --> Redis
+  API -.-> ZLM
+  API -.-> JIT
+```
+
+- **形态**:当前为主应用 `aegis-admin` 聚合一组 `aegis-*` 业务模块的 **Java 服务 + 独立前端工程**,开发时通过 **同域 API 前缀** 访问后端(见下「本地联调」)。  
+- **可选能力**:视频、会议、容器编排等以 `deployment` 与业务开关为准,并非单机开发必需。
+
+---
+
+## 四、界面预览
+
+以下为 `images/` 目录中截图,展示浅色主区 + 深色侧栏、多标签页、地图与列表等典型布局。
+
+| 说明 | 预览 |
+| --- | --- |
+| 主控制台 / 态势总览 | ![主控制台](images/index.png) |
+| 登录(双栏布局与产品要点) | ![登录页](images/4292f298-7e1d-4f32-969d-28f75010076f.png) |
+| 应急调度 · 排班管理 | ![排班管理](images/ae5dd8a4-25e7-43a5-89b6-3eb364e86298.png) |
+| 应急调度 · 接警工作台(含地图) | ![接警工作台](images/09d481a6-fc21-4d8c-aff3-d70af9f4ef9a.png) |
+| 应急调度 · 调度工作台(统一调度地图与资源层) | ![调度工作台](images/5c1dde0c-6efa-4568-a6eb-b29f82075fcf.png) |
+| 应急调度 · 处置工作台(任务详情、事件位置等) | ![处置工作台](images/1fa1fb61-1dcf-4ed8-8e49-393aa8190dcd.png) |
+| 数据报送 · 上级报送 | ![上级报送](images/f19be828-6d9d-43ef-ad31-f25fcd64c476.png) |
+| 预案管理 | ![预案管理](images/90c11be2-f778-44e4-9aa8-15094ab875d2.png) |
+| 资源管理 · 资源看板 | ![资源看板](images/bb0112fe-611f-4998-8cf9-83213ff92794.png) |
+| 资源管理 · 队伍管理 | ![队伍管理](images/3cd08cfb-ebd0-4488-ac7d-163f5425b4f9.png) |
+| 资源管理 · 车辆管理(列表) | ![车辆管理-列表](images/a92d0227-0aff-47ff-b58b-f00592f1bda4.png) |
+| 资源管理 · 车辆管理(位置弹窗、深色顶栏) | ![车辆管理-位置](images/84404068-3376-453e-bca9-0806ac981228.png) |
+| 资源管理 · 专家管理 | ![专家管理](images/3490e00f-6d6e-47aa-ba13-983799b32835.png) |
+| 资源管理 · 物资台账 | ![物资台账](images/b6afab04-bd28-49bc-83d3-9d2830949e05.png) |
+| 系统集成 · 摄像头管理 | ![摄像头管理](images/200f30ec-0b21-4e23-9f82-2f5226471906.png) |
+| 案例库管理 | ![案例库](images/fa8c939c-592a-4368-8eb3-aca88fa2dfeb.png) |
+| 系统管理 · 菜单管理 | ![菜单管理](images/5c45e041-ba0d-4c1a-a9ac-60daeca097b4.png) |
+
+---
+
+## 五、功能特性(摘要)
+
+- **应急调度闭环**:从排班、接报、统一调度到处置工位的分工界面,配合地图与任务详情。  
+- **资源全视图**:看板 + 分资源类型的台账与能力标签、位置能力(依赖地图与设备侧数据质量)。  
+- **预案与报送**:预案全生命周期与报送口径可配置。  
+- **视频与集成**:摄像头等设备的登记与能力扩展。  
+- **系统治理能力**:多租户/多机构常规模型下的用户、角色、菜单、部门、参数与日志。  
+- **可扩展看板与智能助手**:看板可纯真实数据、可演示;顶栏智能助手为独立配置(Key 存浏览器、不经业务后端落库)——见产品说明。  
+
+---
+
+## 六、仓库与模块
+
+```text
+project-aegis-command/
+├── aegis-admin/          # 可运行主应用,聚合各业务模块
+├── aegis-framework/     # 安全、通用 Web 等框架能力
+├── aegis-system/        # 系统管理
+├── aegis-common/        # 公共类库
+├── aegis-emergency/     # 应急管理
+├── aegis-resource/      # 资源管理
+├── aegis-camera/        # 摄像头 / 视频相关
+├── aegis-case/          # 案例
+├── aegis-knowledge/     # 知识库
+├── aegis-notification/  # 通知
+├── aegis-file/          # 文件
+├── aegis-quartz/        # 定时任务
+├── aegis-ui/            # 前端:Vue 2.6、Element UI、ECharts
+├── deployment/          # MySQL 脚本、Docker Compose、Nginx 示例、本地起停脚本
+└── docs/                 # 产品/联调类文档
+```
+
+---
+
+## 七、技术栈
 
+| 层 | 技术 |
+| --- | --- |
+| 后端 | Spring Boot 2.5.15、Spring Security、MyBatis-Plus 3.5.1、Swagger 3.0、Quartz |
+| 数据 | MySQL 8+、Redis 6+ |
+| 前端 | Vue 2.6、Vue Router、Vuex、Element UI 2.15、ECharts、Vue CLI / webpack |
+| 构建 | Maven 3.x、Node.js 14+(用于前端) |
+| 部署(可选) | Docker / Compose、Nginx、ZLMediaKit、Jitsi 等 |
 
+---
 
-## Getting started
+## 八、环境要求
 
-To make it easy for you to get started with GitLab, here's a list of recommended next steps.
+- **JDK** 1.8+  
+- **Maven** 3.6+  
+- **Node.js** 14+(仅开发/构建 `aegis-ui` 时)  
+- **MySQL** 8+、**Redis** 6+(与 `application-druid.yml` 等配置一致)  
+- **Docker** 20.10+(仅在使用 Compose 时)  
 
-Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
+---
 
-## Add your files
+## 九、快速开始
 
-- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
-- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:
+### 9.1 使用 Docker Compose
 
+1. 克隆本仓库。  
+2. 打开并修改 `deployment/docker-compose.yml` 中的**数据库密码、端口、卷**等。  
+3. 在 `deployment` 目录执行:  
+
+   ```bash
+   docker compose up -d
+   ```  
+
+4. 访问地址与 API、Swagger 路径以 **Compose 中端口与路由** 为准,一般为 `http://<主机>:<端口>/swagger-ui/index.html`。  
+
+**Windows 本地一键**(在 `deployment` 目录):
+
+```powershell
+.\up-local.ps1
 ```
-cd existing_repo
-git remote add origin http://www.gitcc.com/dengxxin/baoan-yingji-system.git
-git branch -M main
-git push -uf origin main
+
+或双击 `deployment\up-local.bat`。**停止**:
+
+```powershell
+.\down-local.ps1
 ```
 
-## Integrate with your tools
+若 `.bat` 在极少环境下对中文路径不友好,请直接用 PowerShell 执行上述 `ps1`。
+
+### 9.2 手动部署:数据库脚本顺序
+
+在 `deployment/mysql/` 下按**文件名顺序**(建议与下列一致)在 MySQL 中执行,并保证连接账号、库名与后端配置一致:
+
+1. `01-ruoyi.sql` — 基础库表与预置管理数据  
+2. `02-quartz.sql` — 定时任务相关表  
+3. `03-aegis.sql` — 业务表结构  
+4. `04-aegis-seed-data.sql`(可选)— **演示/联调**用种子数据,**生产环境**请评估后再执行,并在上线后**修改预置密码与敏感参数**。
+
+数据库名在脚本中默认为 **`aegis`**,若你改名,请同步 `application-druid.yml`。
 
-- [ ] [Set up project integrations](http://www.gitcc.com/dengxxin/baoan-yingji-system/-/settings/integrations)
+### 9.3 手动部署:启动后端
 
-## Collaborate with your team
+1. 编辑 `aegis-admin/src/main/resources/application-druid.yml` 与 `application.yml`:数据源、Redis、文件路径 `ruoyi.profile` 等。  
+2. 在项目根目录:  
 
-- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
-- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
-- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
-- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
-- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
+   ```bash
+   mvn clean install -DskipTests
+   cd aegis-admin
+   mvn spring-boot:run
+   ```  
 
-## Test and Deploy
+3. 默认服务端口在 `application.yml` 中为 **8080**(可按需修改)。
 
-Use the built-in continuous integration in GitLab.
+### 9.4 本地联调:前端
 
-- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
-- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
-- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
-- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
-- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
+```bash
+cd aegis-ui
+npm install --registry=https://registry.npmmirror.com
+npm run dev
+```
+
+**前后端如何接上:**
+
+- 开发环境 `aegis-ui/.env.development` 中 `VUE_APP_BASE_API` 为 **`/dev-api`**。  
+- `aegis-ui/vue.config.js` 中 `devServer.proxy` 将 `/dev-api` 代理到 `baseUrl` 指向的后端,默认 **`http://localhost:8080`**,并去掉路径前缀。  
+- 因此:**先启动本机 8080 上的后端,再开前端**;若后端不是 8080,请改 `vue.config.js` 里的 `baseUrl`。  
+- 开发服务默认 **端口 80**(`vue.config.js` 中 `port` 可由环境变量或 npm 覆盖);若 80 被占用,可设置 `port` 环境变量或使用 `npm run dev -- --port 端口号`(以 Vue CLI 行为为准)。  
 
-***
+**生产构建与部署**:
+
+```bash
+npm run build:prod
+```
 
-# Editing this README
+将 `dist` 由 Nginx 等托管;生产环境 API 前缀在 `.env.production` 中一般为 **`/prod-api`**,需在网关或 Nginx 中反向代理到 Java 服务,并与后端 `context-path`、各 `server` 块中的 `location` 一致。示例可参考 `deployment/nginx/`(若存在)。
 
-When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template.
+### 9.5 与部署相关的子数据 / 服务
 
-## Suggestions for a good README
-Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
+- **预置用户**:主初始化脚本中常包含 `admin`、`ry` 等账号,**默认密码需以库内与首次登录要求为准**(常见为 `123456`,**上线后必须改密**)。  
+- **地图**:业务地图使用前请在系统参数中配置 `amap.api.key` 等。  
+- **智能助手**:`VUE_APP_GITCC_API`、同源 `/gitcc-api` 反代、基址与 `/v1` 约定、405 排障等
 
-## Name
-Choose a self-explaining name for your project.
+---
 
-## Description
-Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
+## 十、配置项速查
 
-## Badges
-On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
+| 位置 | 作用 |
+| --- | --- |
+| `aegis-admin/.../application.yml` | 服务端口、上传限制、`spring.profiles`、Token 等全局项 |
+| `aegis-admin/.../application-druid.yml` | 数据源、连接池、MyBatis 等 |
+| `aegis-ui/.env.development` 等 | `VUE_APP_TITLE`、`VUE_APP_BASE_API`、`VUE_APP_GITCC_API` 等 |
+| `aegis-ui/vue.config.js` | 开发代理到后端的 `baseUrl`、端口、GitCC 代理等 |
+| 系统管理 - 参数设置 | 如高德 `amap.api.key` 等运行期业务参数 |
+| `deployment/docker-compose.yml` | 容器化时的服务、环境变量与卷 |
 
-## Visuals
-Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
+---
 
-## Installation
-Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
+## 十一、安全与合规模块(必读)
 
-## Usage
-Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
+- **预置账号与密码**仅用于开发/验收;在公网或生产环境**必须**改密、启用强策略,并收拢管理后台访问来源。  
+- **智能助手 API Key** 仅存用户浏览器,**不**经本系统业务接口持久化,请勿在自研代码中回传、记录或落库用户 Key。  
+- **媒体与地图密钥**:流媒体和地图厂商 key 应放在安全配置与权限可控的位置,并定期轮换。  
 
-## Support
-Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
+---
 
-## Roadmap
-If you have ideas for releases in the future, it is a good idea to list them in the README.
+## 十二、常见问题(FAQ)
 
-## Contributing
-State if you are open to contributions and what your requirements are for accepting them.
+1. **前端能打开但接口全红 / 登不进**  
+   检查后端是否监听 `vue.config.js` 中 `baseUrl` 的地址与端口;若跨域,开发环境应走**代理**而非在浏览器里硬写全路径(除非 CORS 已配好)。  
+2. **地图空白或报「未配置」**  
+   在**参数设置**中配置 `amap.api.key`,并检查浏览器控制台与网络请求。  
+3. **智能助手 401 / 405**  
+   基址、路径 `/v1` 与**同源反代**是否按文档配置;见产品说明 **§5、§6**。  
+4. **表不存在或缺少菜单**  
+   确认 `deployment/mysql` 已按序导入且**库名**与 `application-druid` 中一致。  
 
-For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
+---
 
-You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
+## 十三、参与贡献
 
-## Authors and acknowledgment
-Show your appreciation to those who have contributed to the project.
+欢迎通过 Issue 描述问题、通过合并请求(Pull Request)提交改进。建议单次提交**聚焦单一主题**并写清复现/验证方式,便于审阅。  
 
-## License
-For open source projects, say how it is licensed.
+---
 
-## Project status
-If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.

+ 132 - 0
aegis-admin/pom.xml

@@ -0,0 +1,132 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>aegis-command</artifactId>
+        <groupId>com.aegis</groupId>
+        <version>1.0.0</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <packaging>jar</packaging>
+    <artifactId>aegis-admin</artifactId>
+
+    <description>
+        web服务入口
+    </description>
+
+    <dependencies>
+
+        <!-- spring-boot-devtools -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-devtools</artifactId>
+            <optional>true</optional> <!-- 表示依赖不会传递 -->
+        </dependency>
+
+        <!-- swagger3-->
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-boot-starter</artifactId>
+        </dependency>
+
+        <!-- 防止进入swagger页面报类型转换错误,排除3.0.0中的引用,手动增加1.6.2版本 -->
+        <dependency>
+            <groupId>io.swagger</groupId>
+            <artifactId>swagger-models</artifactId>
+            <version>1.6.2</version>
+        </dependency>
+
+         <!-- Mysql驱动包 -->
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+        </dependency>
+
+        <!-- 核心模块-->
+        <dependency>
+            <groupId>com.aegis</groupId>
+            <artifactId>aegis-framework</artifactId>
+        </dependency>
+
+        <!-- 定时任务-->
+        <dependency>
+            <groupId>com.aegis</groupId>
+            <artifactId>aegis-quartz</artifactId>
+        </dependency>
+
+        <!-- 资源模块-->
+        <dependency>
+            <groupId>com.aegis</groupId>
+            <artifactId>aegis-resource</artifactId>
+        </dependency>
+
+        <!-- 应急调度模块 -->
+        <dependency>
+            <groupId>com.aegis</groupId>
+            <artifactId>aegis-emergency</artifactId>
+        </dependency>
+
+        <!-- 通知模块-->
+        <dependency>
+            <groupId>com.aegis</groupId>
+            <artifactId>aegis-notification</artifactId>
+        </dependency>
+
+        <!-- 文件模块-->
+        <dependency>
+            <groupId>com.aegis</groupId>
+            <artifactId>aegis-file</artifactId>
+        </dependency>
+
+        <!-- 知识库模块-->
+        <dependency>
+            <groupId>com.aegis</groupId>
+            <artifactId>aegis-knowledge</artifactId>
+        </dependency>
+
+        <!-- 案例库模块-->
+        <dependency>
+            <groupId>com.aegis</groupId>
+            <artifactId>aegis-case</artifactId>
+        </dependency>
+
+        <!-- 摄像头模块-->
+        <dependency>
+            <groupId>com.aegis</groupId>
+            <artifactId>aegis-camera</artifactId>
+        </dependency>
+
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>2.5.15</version>
+                <configuration>
+                    <fork>true</fork> <!-- 如果没有该配置,devtools不会生效 -->
+                </configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>   
+                <groupId>org.apache.maven.plugins</groupId>   
+                <artifactId>maven-war-plugin</artifactId>   
+                <version>3.1.0</version>   
+                <configuration>
+                    <failOnMissingWebXml>false</failOnMissingWebXml>
+                    <warName>${project.artifactId}</warName>
+                </configuration>   
+           </plugin>   
+        </plugins>
+        <finalName>${project.artifactId}</finalName>
+    </build>
+
+</project>

+ 32 - 0
aegis-admin/src/main/java/com/aegis/RuoYiApplication.java

@@ -0,0 +1,32 @@
+package com.aegis;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+/**
+ * 启动程序
+ * 
+ * @author ruoyi
+ */
+@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
+@EnableScheduling
+public class RuoYiApplication
+{
+    public static void main(String[] args)
+    {
+        // System.setProperty("spring.devtools.restart.enabled", "false");
+        SpringApplication.run(RuoYiApplication.class, args);
+        System.out.println("(♥◠‿◠)ノ゙  Aegis应急指挥系统启动成功   ლ(´ڡ`ლ)゙  \n" +
+                " .-------.       ____     __        \n" +
+                " |  _ _   \\      \\   \\   /  /    \n" +
+                " | ( ' )  |       \\  _. /  '       \n" +
+                " |(_ o _) /        _( )_ .'         \n" +
+                " | (_,_).' __  ___(_ o _)'          \n" +
+                " |  |\\ \\  |  ||   |(_,_)'         \n" +
+                " |  | \\ `'   /|   `-'  /           \n" +
+                " |  |  \\    /  \\      /           \n" +
+                " ''-'   `'-'    `-..-'              ");
+    }
+}

+ 18 - 0
aegis-admin/src/main/java/com/aegis/RuoYiServletInitializer.java

@@ -0,0 +1,18 @@
+package com.aegis;
+
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
+
+/**
+ * web容器中进行部署
+ * 
+ * @author ruoyi
+ */
+public class RuoYiServletInitializer extends SpringBootServletInitializer
+{
+    @Override
+    protected SpringApplicationBuilder configure(SpringApplicationBuilder application)
+    {
+        return application.sources(RuoYiApplication.class);
+    }
+}

+ 200 - 0
aegis-admin/src/main/java/com/aegis/web/controller/camera/CameraController.java

@@ -0,0 +1,200 @@
+package com.aegis.web.controller.camera;
+
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+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 com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.page.TableDataInfo;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.common.utils.StringUtils;
+import com.aegis.camera.domain.Camera;
+import com.aegis.camera.service.ICameraService;
+import com.aegis.camera.service.StreamSessionService;
+import com.aegis.camera.service.ZLMediaKitService;
+
+/**
+ * 摄像头Controller
+ * 
+ * @author aegis
+ */
+@RestController
+@RequestMapping("/api/cameras")
+public class CameraController extends BaseController {
+
+    @Autowired
+    private ICameraService cameraService;
+
+    @Autowired
+    private ZLMediaKitService zlMediaKitService;
+
+    @Autowired
+    private StreamSessionService streamSessionService;
+
+    /**
+     * 查询摄像头列表
+     */
+    @PreAuthorize("@ss.hasPermi('camera:camera:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(Camera camera) {
+        startPage();
+        List<Camera> list = cameraService.listByCondition(camera);
+        return getDataTable(list);
+    }
+
+    /**
+     * 根据ID获取摄像头详情
+     */
+    @PreAuthorize("@ss.hasPermi('camera:camera:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        return success(cameraService.getById(id));
+    }
+
+    /**
+     * 新增摄像头
+     */
+    @PreAuthorize("@ss.hasPermi('camera:camera:add')")
+    @Log(title = "摄像头管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody Camera camera) {
+        if (StringUtils.isNotEmpty(camera.getCameraCode())) {
+            // 检查编码唯一性在Service中处理
+        }
+        return toAjax(cameraService.save(camera));
+    }
+
+    /**
+     * 修改摄像头
+     */
+    @PreAuthorize("@ss.hasPermi('camera:camera:edit')")
+    @Log(title = "摄像头管理", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody Camera camera) {
+        if (StringUtils.isNotEmpty(camera.getCameraCode())) {
+            // 检查编码唯一性在Service中处理
+        }
+        return toAjax(cameraService.updateById(camera));
+    }
+
+    /**
+     * 删除摄像头
+     */
+    @PreAuthorize("@ss.hasPermi('camera:camera:remove')")
+    @Log(title = "摄像头管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        // 删除前先停止所有相关流
+        for (Long id : ids) {
+            try {
+                Camera camera = cameraService.getById(id);
+                if (camera != null && streamSessionService.isStreamActive(id)) {
+                    zlMediaKitService.stopStream(camera);
+                    streamSessionService.unregisterStream(id);
+                }
+            } catch (Exception e) {
+                // 记录错误但不阻止删除操作
+                logger.warn("删除摄像头时停止流失败,摄像头ID: {}", id, e);
+            }
+        }
+        return toAjax(cameraService.removeByIds(java.util.Arrays.asList(ids)));
+    }
+
+    /**
+     * 启动摄像头流(按需启动)
+     */
+    @PreAuthorize("@ss.hasPermi('camera:camera:query')")
+    @Log(title = "摄像头管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/start")
+    public AjaxResult startStream(@PathVariable Long id) {
+        Camera camera = cameraService.getById(id);
+        if (camera == null) {
+            return error("摄像头不存在");
+        }
+        if (StringUtils.isBlank(camera.getProtocol())) {
+            return error("摄像头协议类型未配置");
+        }
+        
+        try {
+            // 如果流已存在,先停止再启动(确保状态一致)
+            if (streamSessionService.isStreamActive(id)) {
+                zlMediaKitService.stopStream(camera);
+                streamSessionService.unregisterStream(id);
+            }
+            
+            // 启动流
+            boolean started = zlMediaKitService.startStream(camera);
+            if (started) {
+                // 注册流会话
+                streamSessionService.registerStream(id);
+                // 返回播放地址
+                java.util.Map<String, String> playUrls = zlMediaKitService.getPlayUrls(camera.getCameraCode());
+                return success(playUrls);
+            } else {
+                return error("启动流失败,请检查摄像头配置和网络连接");
+            }
+        } catch (Exception e) {
+            logger.error("启动摄像头流失败,摄像头ID: {}", id, e);
+            return error("启动流失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 停止摄像头流
+     */
+    @PreAuthorize("@ss.hasPermi('camera:camera:query')")
+    @Log(title = "摄像头管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/stop")
+    public AjaxResult stopStream(@PathVariable Long id) {
+        Camera camera = cameraService.getById(id);
+        if (camera == null) {
+            return error("摄像头不存在");
+        }
+        
+        try {
+            zlMediaKitService.stopStream(camera);
+            streamSessionService.unregisterStream(id);
+            return success("流已停止");
+        } catch (Exception e) {
+            logger.error("停止摄像头流失败,摄像头ID: {}", id, e);
+            return error("停止流失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 获取摄像头播放地址(仅当流已启动时返回)
+     */
+    @PreAuthorize("@ss.hasPermi('camera:camera:query')")
+    @GetMapping("/{id}/playUrls")
+    public AjaxResult getPlayUrls(@PathVariable Long id) {
+        Camera camera = cameraService.getById(id);
+        if (camera == null) {
+            return error("摄像头不存在");
+        }
+        
+        // 检查流是否活跃
+        if (!streamSessionService.isStreamActive(id)) {
+            return error("流未启动,请先启动流");
+        }
+        
+        try {
+            // 更新最后访问时间,表示仍有用户在观看
+            streamSessionService.updateLastAccess(id);
+            java.util.Map<String, String> playUrls = zlMediaKitService.getPlayUrls(camera.getCameraCode());
+            return success(playUrls);
+        } catch (Exception e) {
+            logger.error("获取播放地址失败,摄像头ID: {}", id, e);
+            return error("获取播放地址失败:" + e.getMessage());
+        }
+    }
+}

+ 161 - 0
aegis-admin/src/main/java/com/aegis/web/controller/cases/CaseController.java

@@ -0,0 +1,161 @@
+package com.aegis.web.controller.cases;
+
+import com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.page.TableDataInfo;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.common.utils.StringUtils;
+import com.aegis.cases.domain.Case;
+import com.aegis.cases.service.ICaseService;
+import java.util.Date;
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+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.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 案例管理 Controller
+ */
+@RestController
+@RequestMapping("/api/cases")
+public class CaseController extends BaseController {
+
+    @Autowired
+    private ICaseService caseService;
+
+    /**
+     * 查询案例列表
+     */
+    @PreAuthorize("@ss.hasPermi('case:case:list')")
+    @GetMapping
+    public TableDataInfo list(@RequestParam(required = false) String title,
+                              @RequestParam(required = false) String eventType,
+                              @RequestParam(required = false) String eventLevel,
+                              @RequestParam(required = false) String tags,
+                              @RequestParam(required = false) Boolean isTypical,
+                              @RequestParam(required = false) Date beginTime,
+                              @RequestParam(required = false) Date endTime) {
+        startPage();
+        List<Case> list = caseService.listByCondition(title, eventType, eventLevel, tags, isTypical, beginTime, endTime);
+        return getDataTable(list);
+    }
+
+    /**
+     * 获取案例详情
+     */
+    @PreAuthorize("@ss.hasPermi('case:case:query')")
+    @GetMapping("/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        Case caseEntity = caseService.getById(id);
+        if (caseEntity == null || "2".equals(caseEntity.getDelFlag())) {
+            return error("案例不存在或已删除");
+        }
+        // 增加查看次数
+        caseService.incrementViewCount(id);
+        return success(caseEntity);
+    }
+
+    /**
+     * 新增案例
+     */
+    @PreAuthorize("@ss.hasPermi('case:case:add')")
+    @Log(title = "案例管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody Case caseEntity) {
+        if (caseEntity == null) {
+            return error("案例信息不能为空");
+        }
+        if (StringUtils.isBlank(caseEntity.getTitle())) {
+            return error("案例标题不能为空");
+        }
+        
+        // 初始化默认值
+        if (caseEntity.getViewCount() == null) {
+            caseEntity.setViewCount(0);
+        }
+        if (caseEntity.getIsTypical() == null) {
+            caseEntity.setIsTypical(false);
+        }
+        
+        boolean saved = caseService.save(caseEntity);
+        if (saved) {
+            return success(caseEntity);
+        }
+        return error("保存失败");
+    }
+
+    /**
+     * 修改案例
+     */
+    @PreAuthorize("@ss.hasPermi('case:case:edit')")
+    @Log(title = "案例管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/{id}")
+    public AjaxResult edit(@PathVariable Long id, @RequestBody Case caseEntity) {
+        caseEntity.setId(id);
+        return toAjax(caseService.updateById(caseEntity));
+    }
+
+    /**
+     * 删除案例
+     */
+    @PreAuthorize("@ss.hasPermi('case:case:remove')")
+    @Log(title = "案例管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{id}")
+    public AjaxResult remove(@PathVariable Long id) {
+        Case caseEntity = caseService.getById(id);
+        if (caseEntity == null || "2".equals(caseEntity.getDelFlag())) {
+            return error("案例不存在或已删除");
+        }
+        // 逻辑删除
+        caseEntity.setDelFlag("2");
+        return toAjax(caseService.updateById(caseEntity));
+    }
+
+    /**
+     * 标记为典型案例
+     */
+    @PreAuthorize("@ss.hasPermi('case:case:markTypical')")
+    @Log(title = "案例管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/mark-typical")
+    public AjaxResult markTypical(@PathVariable Long id) {
+        boolean result = caseService.markTypical(id);
+        if (result) {
+            return success("标记成功");
+        }
+        return error("标记失败");
+    }
+
+    /**
+     * 取消典型案例标记
+     */
+    @PreAuthorize("@ss.hasPermi('case:case:markTypical')")
+    @Log(title = "案例管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/unmark-typical")
+    public AjaxResult unmarkTypical(@PathVariable Long id) {
+        boolean result = caseService.unmarkTypical(id);
+        if (result) {
+            return success("取消标记成功");
+        }
+        return error("取消标记失败");
+    }
+
+    /**
+     * 查询典型案例列表
+     */
+    @PreAuthorize("@ss.hasPermi('case:case:list')")
+    @GetMapping("/typical")
+    public AjaxResult listTypicalCases() {
+        List<Case> list = caseService.listTypicalCases();
+        return success(list);
+    }
+}
+

+ 94 - 0
aegis-admin/src/main/java/com/aegis/web/controller/common/CaptchaController.java

@@ -0,0 +1,94 @@
+package com.aegis.web.controller.common;
+
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import javax.annotation.Resource;
+import javax.imageio.ImageIO;
+import javax.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.util.FastByteArrayOutputStream;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.google.code.kaptcha.Producer;
+import com.aegis.common.config.RuoYiConfig;
+import com.aegis.common.constant.CacheConstants;
+import com.aegis.common.constant.Constants;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.redis.RedisCache;
+import com.aegis.common.utils.sign.Base64;
+import com.aegis.common.utils.uuid.IdUtils;
+import com.aegis.system.service.ISysConfigService;
+
+/**
+ * 验证码操作处理
+ * 
+ * @author ruoyi
+ */
+@RestController
+public class CaptchaController
+{
+    @Resource(name = "captchaProducer")
+    private Producer captchaProducer;
+
+    @Resource(name = "captchaProducerMath")
+    private Producer captchaProducerMath;
+
+    @Autowired
+    private RedisCache redisCache;
+    
+    @Autowired
+    private ISysConfigService configService;
+    /**
+     * 生成验证码
+     */
+    @GetMapping("/captchaImage")
+    public AjaxResult getCode(HttpServletResponse response) throws IOException
+    {
+        AjaxResult ajax = AjaxResult.success();
+        boolean captchaEnabled = configService.selectCaptchaEnabled();
+        ajax.put("captchaEnabled", captchaEnabled);
+        if (!captchaEnabled)
+        {
+            return ajax;
+        }
+
+        // 保存验证码信息
+        String uuid = IdUtils.simpleUUID();
+        String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid;
+
+        String capStr = null, code = null;
+        BufferedImage image = null;
+
+        // 生成验证码
+        String captchaType = RuoYiConfig.getCaptchaType();
+        if ("math".equals(captchaType))
+        {
+            String capText = captchaProducerMath.createText();
+            capStr = capText.substring(0, capText.lastIndexOf("@"));
+            code = capText.substring(capText.lastIndexOf("@") + 1);
+            image = captchaProducerMath.createImage(capStr);
+        }
+        else if ("char".equals(captchaType))
+        {
+            capStr = code = captchaProducer.createText();
+            image = captchaProducer.createImage(capStr);
+        }
+
+        redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
+        // 转换流信息写出
+        FastByteArrayOutputStream os = new FastByteArrayOutputStream();
+        try
+        {
+            ImageIO.write(image, "jpg", os);
+        }
+        catch (IOException e)
+        {
+            return AjaxResult.error(e.getMessage());
+        }
+
+        ajax.put("uuid", uuid);
+        ajax.put("img", Base64.encode(os.toByteArray()));
+        return ajax;
+    }
+}

+ 168 - 0
aegis-admin/src/main/java/com/aegis/web/controller/common/CommonController.java

@@ -0,0 +1,168 @@
+package com.aegis.web.controller.common;
+
+import java.util.ArrayList;
+import java.util.List;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+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.RestController;
+import org.springframework.web.multipart.MultipartFile;
+import com.aegis.common.config.RuoYiConfig;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.utils.StringUtils;
+import com.aegis.common.utils.file.FileUploadUtils;
+import com.aegis.common.utils.file.FileUtils;
+import com.aegis.file.domain.FileInfo;
+import com.aegis.file.service.IFileService;
+import com.aegis.framework.config.ServerConfig;
+
+/**
+ * 通用请求处理
+ * 
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/common")
+public class CommonController
+{
+    private static final Logger log = LoggerFactory.getLogger(CommonController.class);
+
+    @Autowired
+    private ServerConfig serverConfig;
+
+    @Autowired
+    private IFileService fileService;
+
+    private static final String FILE_DELIMETER = ",";
+
+    /**
+     * 通用下载请求
+     * 
+     * @param fileName 文件名称
+     * @param delete 是否删除
+     */
+    @GetMapping("/download")
+    public void fileDownload(String fileName, Boolean delete, HttpServletResponse response, HttpServletRequest request)
+    {
+        try
+        {
+            if (!FileUtils.checkAllowDownload(fileName))
+            {
+                throw new Exception(StringUtils.format("文件名称({})非法,不允许下载。 ", fileName));
+            }
+            String realFileName = System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1);
+            String filePath = RuoYiConfig.getDownloadPath() + fileName;
+
+            response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
+            FileUtils.setAttachmentResponseHeader(response, realFileName);
+            FileUtils.writeBytes(filePath, response.getOutputStream());
+            if (delete)
+            {
+                FileUtils.deleteFile(filePath);
+            }
+        }
+        catch (Exception e)
+        {
+            log.error("下载文件失败", e);
+        }
+    }
+
+    /**
+     * 通用上传请求(单个)
+     * 兼容接口:内部调用新的FileService,会保存到数据库
+     */
+    @PostMapping("/upload")
+    public AjaxResult uploadFile(MultipartFile file) throws Exception
+    {
+        try
+        {
+            // 调用新的FileService上传文件(会自动保存到数据库)
+            FileInfo fileInfo = fileService.uploadFile(file);
+            String fileName = fileInfo.getFilePath();
+            String url = serverConfig.getUrl() + fileName;
+            // 保持原有返回格式,确保向后兼容
+            AjaxResult ajax = AjaxResult.success();
+            ajax.put("url", url);
+            ajax.put("fileName", fileName);
+            ajax.put("newFileName", FileUtils.getName(fileName));
+            ajax.put("originalFilename", fileInfo.getOriginalName());
+            return ajax;
+        }
+        catch (Exception e)
+        {
+            return AjaxResult.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 通用上传请求(多个)
+     */
+    @PostMapping("/uploads")
+    public AjaxResult uploadFiles(List<MultipartFile> files) throws Exception
+    {
+        try
+        {
+            // 上传文件路径
+            String filePath = RuoYiConfig.getUploadPath();
+            List<String> urls = new ArrayList<String>();
+            List<String> fileNames = new ArrayList<String>();
+            List<String> newFileNames = new ArrayList<String>();
+            List<String> originalFilenames = new ArrayList<String>();
+            for (MultipartFile file : files)
+            {
+                // 上传并返回新文件名称
+                String fileName = FileUploadUtils.upload(filePath, file);
+                String url = serverConfig.getUrl() + fileName;
+                urls.add(url);
+                fileNames.add(fileName);
+                newFileNames.add(FileUtils.getName(fileName));
+                originalFilenames.add(file.getOriginalFilename());
+            }
+            AjaxResult ajax = AjaxResult.success();
+            ajax.put("urls", StringUtils.join(urls, FILE_DELIMETER));
+            ajax.put("fileNames", StringUtils.join(fileNames, FILE_DELIMETER));
+            ajax.put("newFileNames", StringUtils.join(newFileNames, FILE_DELIMETER));
+            ajax.put("originalFilenames", StringUtils.join(originalFilenames, FILE_DELIMETER));
+            return ajax;
+        }
+        catch (Exception e)
+        {
+            return AjaxResult.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 本地资源通用下载
+     */
+    @GetMapping("/download/resource")
+    public void resourceDownload(String resource, HttpServletRequest request, HttpServletResponse response)
+            throws Exception
+    {
+        try
+        {
+            if (!FileUtils.checkAllowDownload(resource))
+            {
+                throw new Exception(StringUtils.format("资源文件({})非法,不允许下载。 ", resource));
+            }
+            // 本地资源路径
+            String localPath = RuoYiConfig.getProfile();
+            // 数据库资源地址
+            String downloadPath = localPath + FileUtils.stripPrefix(resource);
+            // 下载名称
+            String downloadName = StringUtils.substringAfterLast(downloadPath, "/");
+            response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
+            FileUtils.setAttachmentResponseHeader(response, downloadName);
+            FileUtils.writeBytes(downloadPath, response.getOutputStream());
+        }
+        catch (Exception e)
+        {
+            log.error("下载文件失败", e);
+        }
+    }
+}

+ 396 - 0
aegis-admin/src/main/java/com/aegis/web/controller/emergency/dispatch/DispatchTaskController.java

@@ -0,0 +1,396 @@
+package com.aegis.web.controller.emergency.dispatch;
+
+import com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.page.TableDataInfo;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.emergency.dispatch.domain.DispatchResourceUsageLink;
+import com.aegis.emergency.dispatch.domain.DispatchTask;
+import com.aegis.emergency.dispatch.domain.DispatchTaskParticipant;
+import com.aegis.emergency.dispatch.domain.DispatchTaskStep;
+import com.aegis.emergency.dispatch.domain.DispatchTaskDisposal;
+import com.aegis.emergency.dispatch.domain.DispatchTaskLog;
+import com.aegis.emergency.dispatch.service.IDispatchTaskService;
+import com.aegis.emergency.dispatch.service.IDispatchTaskDisposalService;
+import com.aegis.emergency.dispatch.service.IDispatchTaskLogService;
+import com.aegis.emergency.dispatch.service.IDispatchResourceUsageLinkService;
+import com.aegis.emergency.dispatch.dto.MaterialAllocationRequest;
+import com.aegis.emergency.dispatch.dto.EquipmentAllocationRequest;
+import com.aegis.emergency.dispatch.dto.VehicleAllocationRequest;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+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.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 调度任务 Controller
+ *
+ * @author aegis
+ */
+@RestController
+@RequestMapping("/emergency/dispatch/tasks")
+public class DispatchTaskController extends BaseController {
+
+    @Autowired
+    private IDispatchTaskService dispatchTaskService;
+
+    @Autowired
+    private IDispatchTaskDisposalService disposalService;
+
+    @Autowired
+    private IDispatchTaskLogService logService;
+
+    @Autowired
+    private IDispatchResourceUsageLinkService resourceUsageLinkService;
+
+    /**
+     * 查询调度任务列表
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:dispatch:task:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(DispatchTask condition) {
+        startPage();
+        List<DispatchTask> list = dispatchTaskService.listByCondition(condition);
+        return getDataTable(list);
+    }
+
+    /**
+     * 查询当前登录用户关联的调度任务
+     */
+    @PreAuthorize("isAuthenticated()")
+    @GetMapping("/my")
+    public TableDataInfo listMyTasks(DispatchTask condition,
+                                     @RequestParam(value = "keyword", required = false) String keyword) {
+        startPage();
+        List<DispatchTask> list = dispatchTaskService.listMyTasks(condition, keyword);
+        return getDataTable(list);
+    }
+
+    /**
+     * 根据ID查询调度任务
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:dispatch:task:query')")
+    @GetMapping("/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        return success(dispatchTaskService.getById(id));
+    }
+
+    /**
+     * 根据任务编号查询
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:dispatch:task:query')")
+    @GetMapping("/no/{taskNo}")
+    public AjaxResult getByTaskNo(@PathVariable String taskNo) {
+        return success(dispatchTaskService.getByTaskNo(taskNo));
+    }
+
+    /**
+     * 新增调度任务
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:dispatch:task:add')")
+    @Log(title = "调度任务", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody DispatchTask task) {
+        return toAjax(dispatchTaskService.save(task));
+    }
+
+    /**
+     * 修改调度任务
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:dispatch:task:edit')")
+    @Log(title = "调度任务", businessType = BusinessType.UPDATE)
+    @PutMapping("/{id}")
+    public AjaxResult edit(@PathVariable Long id, @RequestBody DispatchTask task) {
+        task.setId(id);
+        return toAjax(dispatchTaskService.updateById(task));
+    }
+
+    /**
+     * 删除调度任务
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:dispatch:task:remove')")
+    @Log(title = "调度任务", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        return toAjax(dispatchTaskService.removeByIds(java.util.Arrays.asList(ids)));
+    }
+
+    /**
+     * 发布任务(draft -> published)
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:dispatch:task:publish')")
+    @Log(title = "调度任务", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/publish")
+    public AjaxResult publish(@PathVariable Long id) {
+        return toAjax(dispatchTaskService.publish(id));
+    }
+
+    /**
+     * 标记任务执行中(published -> in_progress)
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:dispatch:task:edit')")
+    @Log(title = "调度任务", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/in-progress")
+    public AjaxResult markInProgress(@PathVariable Long id) {
+        return toAjax(dispatchTaskService.markInProgress(id));
+    }
+
+    /**
+     * 关闭任务(in_progress -> closed)
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:dispatch:task:edit')")
+    @Log(title = "调度任务", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/close")
+    public AjaxResult close(@PathVariable Long id, @RequestBody(required = false) Map<String, String> body) {
+        String remark = body != null ? body.getOrDefault("remark", "") : "";
+        return toAjax(dispatchTaskService.close(id, remark));
+    }
+
+    /**
+     * 取消任务(published/in_progress -> cancelled)
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:dispatch:task:edit')")
+    @Log(title = "调度任务", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/abort")
+    public AjaxResult abort(@PathVariable Long id, @RequestBody(required = false) Map<String, String> body) {
+        String reason = body != null ? body.getOrDefault("reason", "") : "";
+        return toAjax(dispatchTaskService.abort(id, reason));
+    }
+
+    /**
+     * 添加参与方
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:dispatch:task:edit')")
+    @Log(title = "调度任务", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/participants")
+    public AjaxResult addParticipants(@PathVariable Long id, @RequestBody List<DispatchTaskParticipant> participants) {
+        List<DispatchTaskParticipant> safeList = participants != null ? participants : Collections.emptyList();
+        dispatchTaskService.addParticipants(id, safeList);
+        return success();
+    }
+
+    /**
+     * 查询参与方列表
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:dispatch:task:query')")
+    @GetMapping("/{id}/participants")
+    public AjaxResult listParticipants(@PathVariable Long id) {
+        return success(dispatchTaskService.listParticipants(id));
+    }
+
+    /**
+     * 移除参与方
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:dispatch:task:edit')")
+    @Log(title = "调度任务", businessType = BusinessType.UPDATE)
+    @DeleteMapping("/{id}/participants/{participantId}")
+    public AjaxResult removeParticipant(@PathVariable Long id, @PathVariable Long participantId) {
+        return toAjax(dispatchTaskService.removeParticipant(id, participantId));
+    }
+
+    /**
+     * 查询行动项列表
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:dispatch:task:query')")
+    @GetMapping("/{id}/steps")
+    public AjaxResult listSteps(@PathVariable Long id) {
+        return success(dispatchTaskService.listSteps(id));
+    }
+
+    /**
+     * 追加/更新行动项
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:dispatch:task:edit')")
+    @Log(title = "调度任务", businessType = BusinessType.UPDATE)
+    @PutMapping("/{id}/steps")
+    public AjaxResult upsertSteps(@PathVariable Long id, @RequestBody List<DispatchTaskStep> steps) {
+        List<DispatchTaskStep> safeList = steps != null ? steps : Collections.emptyList();
+        dispatchTaskService.upsertSteps(id, safeList);
+        return success();
+    }
+
+    /**
+     * 开始执行行动项
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:dispatch:task:edit')")
+    @Log(title = "调度任务", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/steps/{stepId}/start")
+    public AjaxResult startStep(@PathVariable Long id, @PathVariable Long stepId) {
+        boolean result = dispatchTaskService.startStep(id, stepId);
+        return result ? success() : error("开始执行行动项失败");
+    }
+
+    /**
+     * 完成行动项
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:dispatch:task:edit')")
+    @Log(title = "调度任务", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/steps/{stepId}/complete")
+    public AjaxResult completeStep(@PathVariable Long id, @PathVariable Long stepId, @RequestBody(required = false) Map<String, String> body) {
+        String remark = body != null ? body.get("remark") : null;
+        boolean result = dispatchTaskService.completeStep(id, stepId, remark);
+        return result ? success() : error("完成行动项失败");
+    }
+
+    /**
+     * 标记行动项失败
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:dispatch:task:edit')")
+    @Log(title = "调度任务", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/steps/{stepId}/fail")
+    public AjaxResult failStep(@PathVariable Long id, @PathVariable Long stepId, @RequestBody(required = false) Map<String, String> body) {
+        String reason = body != null ? body.get("reason") : null;
+        boolean result = dispatchTaskService.failStep(id, stepId, reason);
+        return result ? success() : error("标记行动项失败");
+    }
+
+    /**
+     * 查询任务资源链接
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:dispatch:task:query')")
+    @GetMapping("/{id}/resource-links")
+    public AjaxResult listResourceLinks(@PathVariable Long id) {
+        return success(dispatchTaskService.listResourceLinks(id));
+    }
+
+    /**
+     * 查询行动项资源链接
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:dispatch:task:query')")
+    @GetMapping("/{id}/steps/{stepId}/resource-links")
+    public AjaxResult listStepResourceLinks(@PathVariable Long id, @PathVariable Long stepId) {
+        return success(resourceUsageLinkService.listByStepId(stepId));
+    }
+
+    /**
+     * 分配物资到行动项
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:dispatch:task:edit')")
+    @Log(title = "调度任务", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/steps/{stepId}/materials")
+    public AjaxResult allocateMaterialToStep(@PathVariable Long id, @PathVariable Long stepId, 
+                                             @RequestBody MaterialAllocationRequest request) {
+        DispatchResourceUsageLink link = resourceUsageLinkService.allocateMaterialToStep(id, stepId, request);
+        return success(link);
+    }
+
+    /**
+     * 分配装备到行动项
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:dispatch:task:edit')")
+    @Log(title = "调度任务", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/steps/{stepId}/equipment")
+    public AjaxResult allocateEquipmentToStep(@PathVariable Long id, @PathVariable Long stepId,
+                                               @RequestBody EquipmentAllocationRequest request) {
+        DispatchResourceUsageLink link = resourceUsageLinkService.allocateEquipmentToStep(id, stepId, request);
+        return success(link);
+    }
+
+    /**
+     * 分配车辆到行动项
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:dispatch:task:edit')")
+    @Log(title = "调度任务", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/steps/{stepId}/vehicles")
+    public AjaxResult allocateVehicleToStep(@PathVariable Long id, @PathVariable Long stepId,
+                                           @RequestBody VehicleAllocationRequest request) {
+        DispatchResourceUsageLink link = resourceUsageLinkService.allocateVehicleToStep(id, stepId, request);
+        return success(link);
+    }
+
+    /**
+     * 移除行动项资源链接
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:dispatch:task:edit')")
+    @Log(title = "调度任务", businessType = BusinessType.UPDATE)
+    @DeleteMapping("/{id}/steps/{stepId}/resource-links/{linkId}")
+    public AjaxResult removeStepResourceLink(@PathVariable Long id, @PathVariable Long stepId, 
+                                            @PathVariable Long linkId) {
+        boolean result = resourceUsageLinkService.detachResource(id, linkId);
+        return result ? success() : error("移除资源失败");
+    }
+
+    /**
+     * 释放物资锁定
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:dispatch:task:edit')")
+    @Log(title = "调度任务", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/resource-links/{linkId}/release")
+    public AjaxResult releaseMaterialLink(@PathVariable Long id, @PathVariable Long linkId) {
+        return toAjax(resourceUsageLinkService.releaseMaterialLink(id, linkId));
+    }
+
+    /**
+     * 消耗物资
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:dispatch:task:edit')")
+    @Log(title = "调度任务", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/resource-links/{linkId}/consume")
+    public AjaxResult consumeMaterialLink(@PathVariable Long id, @PathVariable Long linkId) {
+        return toAjax(resourceUsageLinkService.consumeMaterialLink(id, linkId));
+    }
+
+    /**
+     * 归还物资
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:dispatch:task:edit')")
+    @Log(title = "调度任务", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/resource-links/{linkId}/return")
+    public AjaxResult returnMaterialLink(@PathVariable Long id, @PathVariable Long linkId) {
+        return toAjax(resourceUsageLinkService.returnMaterialLink(id, linkId));
+    }
+
+    /**
+     * 查询处置记录
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:dispatch:task:query')")
+    @GetMapping("/{id}/disposals")
+    public AjaxResult listDisposals(@PathVariable Long id) {
+        return success(disposalService.listByTaskId(id));
+    }
+
+    /**
+     * 新增处置记录
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:dispatch:task:edit')")
+    @Log(title = "调度任务", businessType = BusinessType.INSERT)
+    @PostMapping("/{id}/disposals")
+    public AjaxResult addDisposal(@PathVariable Long id, @RequestBody DispatchTaskDisposal disposal) {
+        disposal.setTaskId(id);
+        return toAjax(disposalService.save(disposal));
+    }
+
+    /**
+     * 更新处置记录
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:dispatch:task:edit')")
+    @Log(title = "调度任务", businessType = BusinessType.UPDATE)
+    @PutMapping("/{taskId}/disposals/{disposalId}")
+    public AjaxResult updateDisposal(@PathVariable Long taskId, @PathVariable Long disposalId,
+                                     @RequestBody DispatchTaskDisposal disposal) {
+        disposal.setId(disposalId);
+        disposal.setTaskId(taskId);
+        return toAjax(disposalService.updateById(disposal));
+    }
+
+    /**
+     * 查询任务日志
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:dispatch:task:query')")
+    @GetMapping("/{id}/logs")
+    public AjaxResult listLogs(@PathVariable Long id) {
+        List<DispatchTaskLog> logs = logService.listByTaskId(id);
+        return success(logs);
+    }
+}

+ 220 - 0
aegis-admin/src/main/java/com/aegis/web/controller/emergency/duty/DutyAlertController.java

@@ -0,0 +1,220 @@
+package com.aegis.web.controller.emergency.duty;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+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 com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.page.TableDataInfo;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.emergency.alert.domain.DutyAlert;
+import com.aegis.emergency.alert.domain.DutyAlertTransfer;
+import com.aegis.emergency.alert.domain.DutyAlertConvert;
+import com.aegis.emergency.alert.service.DutyAlertService;
+import com.aegis.emergency.alert.service.DutyAlertLogService;
+import com.aegis.emergency.alert.service.DutyAlertTransferService;
+import com.aegis.emergency.alert.service.DutyAlertConvertService;
+import com.aegis.emergency.alert.domain.DutyAlertLog;
+
+/**
+ * 警情Controller
+ * 
+ * @author aegis
+ */
+@RestController
+@RequestMapping("/duty/alerts")
+public class DutyAlertController extends BaseController {
+
+    @Autowired
+    private DutyAlertService dutyAlertService;
+
+    @Autowired
+    private DutyAlertLogService dutyAlertLogService;
+
+    @Autowired
+    private DutyAlertTransferService dutyAlertTransferService;
+
+    @Autowired
+    private DutyAlertConvertService dutyAlertConvertService;
+
+    /**
+     * 查询警情列表
+     */
+    @PreAuthorize("@ss.hasPermi('duty:alert:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(DutyAlert condition) {
+        startPage();
+        List<DutyAlert> list = dutyAlertService.listByCondition(condition);
+        
+        // 如果列表为空,直接返回
+        if (list.isEmpty()) {
+            return getDataTable(list);
+        }
+        
+        // 提取所有警情ID
+        List<Long> alertIds = list.stream()
+            .map(DutyAlert::getId)
+            .collect(Collectors.toList());
+        
+        // 批量查询转警记录(一次查询,无循环)
+        Map<Long, DutyAlertTransfer> transferMap = 
+            dutyAlertTransferService.getLatestByAlertIds(alertIds);
+        
+        // 批量查询转事件记录(一次查询,无循环)
+        Map<Long, DutyAlertConvert> convertMap = 
+            dutyAlertConvertService.getLatestByAlertIds(alertIds);
+        
+        // 在内存中组装数据
+        for (DutyAlert alert : list) {
+            alert.setTransferRecord(transferMap.get(alert.getId()));
+            alert.setConvertRecord(convertMap.get(alert.getId()));
+        }
+        
+        return getDataTable(list);
+    }
+
+    /**
+     * 根据ID查询警情
+     */
+    @PreAuthorize("@ss.hasPermi('duty:alert:query')")
+    @GetMapping("/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        DutyAlert alert = dutyAlertService.getById(id);
+        if (alert == null) {
+            return error("警情不存在或已删除");
+        }
+        return success(alert);
+    }
+
+    /**
+     * 根据警情编号查询警情
+     */
+    @PreAuthorize("@ss.hasPermi('duty:alert:query')")
+    @GetMapping("/no/{alertNo}")
+    public AjaxResult getByAlertNo(@PathVariable String alertNo) {
+        DutyAlert alert = dutyAlertService.getByAlertNo(alertNo);
+        if (alert == null) {
+            return error("警情不存在或已删除");
+        }
+        return success(alert);
+    }
+
+    /**
+     * 新增警情
+     */
+    @PreAuthorize("@ss.hasPermi('duty:alert:add')")
+    @Log(title = "警情管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody DutyAlert alert) {
+        return toAjax(dutyAlertService.save(alert));
+    }
+
+    /**
+     * 修改警情
+     */
+    @PreAuthorize("@ss.hasPermi('duty:alert:edit')")
+    @Log(title = "警情管理", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody DutyAlert alert) {
+        return toAjax(dutyAlertService.updateById(alert));
+    }
+
+    /**
+     * 删除警情(支持批量)
+     */
+    @PreAuthorize("@ss.hasPermi('duty:alert:remove')")
+    @Log(title = "警情管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        boolean allSuccess = true;
+        for (Long id : ids) {
+            allSuccess &= dutyAlertService.removeById(id);
+        }
+        return toAjax(allSuccess);
+    }
+
+    /**
+     * 根据警情ID查询操作日志
+     */
+    @PreAuthorize("@ss.hasPermi('duty:alert:query')")
+    @GetMapping("/{id}/logs")
+    public AjaxResult getLogs(@PathVariable Long id) {
+        List<DutyAlertLog> logs = dutyAlertLogService.listByAlertId(id);
+        return success(logs);
+    }
+
+    /**
+     * 提交警情(从草稿状态变为已接警状态)
+     */
+    @PreAuthorize("@ss.hasPermi('duty:alert:edit')")
+    @Log(title = "警情管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/submit")
+    public AjaxResult submit(@PathVariable Long id, @RequestBody(required = false) DutyAlert alert) {
+        return toAjax(dutyAlertService.submitAlert(id, alert));
+    }
+
+    /**
+     * 关闭警情(将状态变为已关闭)
+     */
+    @PreAuthorize("@ss.hasPermi('duty:alert:edit')")
+    @Log(title = "警情管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/close")
+    public AjaxResult close(@PathVariable Long id) {
+        return toAjax(dutyAlertService.closeAlert(id));
+    }
+
+    /**
+     * 转警(提交转警申请)
+     */
+    @PreAuthorize("@ss.hasPermi('duty:alert:transfer')")
+    @Log(title = "警情管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/transfer")
+    public AjaxResult transfer(@PathVariable Long id, @RequestBody DutyAlertTransfer transfer) {
+        if (transfer == null) {
+            return error("转警信息不能为空");
+        }
+        transfer.setAlertId(id);
+        boolean success = dutyAlertTransferService.applyTransfer(transfer);
+        if (success) {
+            return success("转警申请已提交,等待审核");
+        } else {
+            return error("转警申请提交失败");
+        }
+    }
+
+    /**
+     * 转事件(提交转事件申请)
+     */
+    @PreAuthorize("@ss.hasPermi('duty:alert:convert')")
+    @Log(title = "警情管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/convert")
+    public AjaxResult convert(@PathVariable Long id, @RequestBody DutyAlertConvert convert) {
+        if (convert == null) {
+            return error("转事件信息不能为空");
+        }
+        convert.setAlertId(id);
+        boolean success = dutyAlertConvertService.applyConvert(convert);
+        if (success) {
+            // 判断是否需要审核
+            DutyAlert alert = dutyAlertService.getById(id);
+            if (alert != null && Boolean.TRUE.equals(convert.getReviewRequired())) {
+                return success("转事件申请已提交,等待审核");
+            } else {
+                return success("转事件申请已提交");
+            }
+        } else {
+            return error("转事件申请提交失败");
+        }
+    }
+}

+ 100 - 0
aegis-admin/src/main/java/com/aegis/web/controller/emergency/duty/DutyAlertConvertController.java

@@ -0,0 +1,100 @@
+package com.aegis.web.controller.emergency.duty;
+
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+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 com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.page.TableDataInfo;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.emergency.alert.domain.DutyAlertConvert;
+import com.aegis.emergency.alert.dto.ConvertApproveRequest;
+import com.aegis.emergency.alert.dto.ConvertRejectRequest;
+import com.aegis.emergency.alert.service.DutyAlertConvertService;
+
+/**
+ * 转事件管理Controller
+ * 
+ * @author aegis
+ */
+@RestController
+@RequestMapping("/duty/converts")
+public class DutyAlertConvertController extends BaseController {
+
+    @Autowired
+    private DutyAlertConvertService dutyAlertConvertService;
+
+    /**
+     * 查询转事件列表(支持分页)
+     */
+    @PreAuthorize("@ss.hasPermi('duty:convert:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(DutyAlertConvert condition) {
+        startPage();
+        List<DutyAlertConvert> list = dutyAlertConvertService.listByCondition(condition);
+        return getDataTable(list);
+    }
+
+    /**
+     * 根据ID查询转事件详情
+     */
+    @PreAuthorize("@ss.hasPermi('duty:convert:query')")
+    @GetMapping("/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        DutyAlertConvert convert = dutyAlertConvertService.getById(id);
+        if (convert == null) {
+            return error("转事件记录不存在或已删除");
+        }
+        return success(convert);
+    }
+
+    /**
+     * 根据警情ID查询转事件记录
+     */
+    @PreAuthorize("@ss.hasPermi('duty:convert:query')")
+    @GetMapping("/alert/{alertId}")
+    public AjaxResult getByAlertId(@PathVariable Long alertId) {
+        DutyAlertConvert convert = dutyAlertConvertService.getByAlertId(alertId);
+        // 没有转事件记录是正常情况,返回成功但 data 为 null
+        return success(convert);
+    }
+
+    /**
+     * 审核通过转事件申请
+     */
+    @PreAuthorize("@ss.hasPermi('duty:convert:approve')")
+    @Log(title = "转事件管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/{id}/approve")
+    public AjaxResult approve(@PathVariable Long id, @RequestBody(required = false) ConvertApproveRequest request) {
+        String reviewComment = request != null ? request.getReviewComment() : null;
+        boolean success = dutyAlertConvertService.approveConvert(id, reviewComment);
+        if (success) {
+            return success("转事件申请已审核通过");
+        } else {
+            return error("审核操作失败");
+        }
+    }
+
+    /**
+     * 审核驳回转事件申请
+     */
+    @PreAuthorize("@ss.hasPermi('duty:convert:approve')")
+    @Log(title = "转事件管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/{id}/reject")
+    public AjaxResult reject(@PathVariable Long id, @RequestBody(required = false) ConvertRejectRequest request) {
+        String reviewComment = request != null ? request.getReviewComment() : null;
+        boolean success = dutyAlertConvertService.rejectConvert(id, reviewComment);
+        if (success) {
+            return success("转事件申请已驳回");
+        } else {
+            return error("驳回操作失败");
+        }
+    }
+}

+ 48 - 0
aegis-admin/src/main/java/com/aegis/web/controller/emergency/duty/DutyAlertLogController.java

@@ -0,0 +1,48 @@
+package com.aegis.web.controller.emergency.duty;
+
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+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.RestController;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.page.TableDataInfo;
+import com.aegis.emergency.alert.domain.DutyAlertLog;
+import com.aegis.emergency.alert.service.DutyAlertLogService;
+
+/**
+ * 警情操作日志Controller
+ * 
+ * @author aegis
+ */
+@RestController
+@RequestMapping("/duty/alerts/logs")
+public class DutyAlertLogController extends BaseController {
+
+    @Autowired
+    private DutyAlertLogService dutyAlertLogService;
+
+    /**
+     * 按条件查询警情日志列表(支持分页)
+     */
+    @PreAuthorize("@ss.hasPermi('duty:alert:log:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(DutyAlertLog condition) {
+        startPage();
+        List<DutyAlertLog> list = dutyAlertLogService.listByCondition(condition);
+        return getDataTable(list);
+    }
+
+    /**
+     * 根据警情ID查询操作日志
+     */
+    @PreAuthorize("@ss.hasPermi('duty:alert:log:query')")
+    @GetMapping("/alert/{alertId}")
+    public AjaxResult listByAlertId(@PathVariable Long alertId) {
+        List<DutyAlertLog> logs = dutyAlertLogService.listByAlertId(alertId);
+        return success(logs);
+    }
+}

+ 138 - 0
aegis-admin/src/main/java/com/aegis/web/controller/emergency/duty/DutyAlertTransferController.java

@@ -0,0 +1,138 @@
+package com.aegis.web.controller.emergency.duty;
+
+import java.util.Date;
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+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 com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.page.TableDataInfo;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.emergency.alert.domain.DutyAlertTransfer;
+import com.aegis.emergency.alert.dto.TransferApproveRequest;
+import com.aegis.emergency.alert.dto.TransferRejectRequest;
+import com.aegis.emergency.alert.dto.TransferFeedbackRequest;
+import com.aegis.emergency.alert.dto.TransferProcessRequest;
+import com.aegis.emergency.alert.service.DutyAlertTransferService;
+
+/**
+ * 转警管理Controller
+ * 
+ * @author aegis
+ */
+@RestController
+@RequestMapping("/duty/transfers")
+public class DutyAlertTransferController extends BaseController {
+
+    @Autowired
+    private DutyAlertTransferService dutyAlertTransferService;
+
+    /**
+     * 查询转警列表(支持分页)
+     */
+    @PreAuthorize("@ss.hasPermi('duty:transfer:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(DutyAlertTransfer condition) {
+        startPage();
+        List<DutyAlertTransfer> list = dutyAlertTransferService.listByCondition(condition);
+        return getDataTable(list);
+    }
+
+    /**
+     * 根据ID查询转警详情
+     */
+    @PreAuthorize("@ss.hasPermi('duty:transfer:query')")
+    @GetMapping("/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        DutyAlertTransfer transfer = dutyAlertTransferService.getById(id);
+        if (transfer == null) {
+            return error("转警记录不存在或已删除");
+        }
+        return success(transfer);
+    }
+
+    /**
+     * 根据警情ID查询转警记录
+     */
+    @PreAuthorize("@ss.hasPermi('duty:transfer:query')")
+    @GetMapping("/alert/{alertId}")
+    public AjaxResult getByAlertId(@PathVariable Long alertId) {
+        DutyAlertTransfer transfer = dutyAlertTransferService.getByAlertId(alertId);
+        // 没有转警记录是正常情况,返回成功但 data 为 null
+        return success(transfer);
+    }
+
+    /**
+     * 审核通过转警申请
+     */
+    @PreAuthorize("@ss.hasPermi('duty:transfer:approve')")
+    @Log(title = "转警管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/{id}/approve")
+    public AjaxResult approve(@PathVariable Long id, @RequestBody(required = false) TransferApproveRequest request) {
+        String reviewComment = request != null ? request.getReviewComment() : null;
+        boolean success = dutyAlertTransferService.approveTransfer(id, reviewComment);
+        if (success) {
+            return success("转警申请已审核通过");
+        } else {
+            return error("审核操作失败");
+        }
+    }
+
+    /**
+     * 审核驳回转警申请
+     */
+    @PreAuthorize("@ss.hasPermi('duty:transfer:approve')")
+    @Log(title = "转警管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/{id}/reject")
+    public AjaxResult reject(@PathVariable Long id, @RequestBody(required = false) TransferRejectRequest request) {
+        String reviewComment = request != null ? request.getReviewComment() : null;
+        boolean success = dutyAlertTransferService.rejectTransfer(id, reviewComment);
+        if (success) {
+            return success("转警申请已驳回");
+        } else {
+            return error("驳回操作失败");
+        }
+    }
+
+    /**
+     * 录入反馈
+     */
+    @PreAuthorize("@ss.hasPermi('duty:transfer:edit')")
+    @Log(title = "转警管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/{id}/feedback")
+    public AjaxResult feedback(@PathVariable Long id, @RequestBody TransferFeedbackRequest request) {
+        if (request == null || request.getFeedback() == null || request.getFeedback().trim().isEmpty()) {
+            return error("反馈内容不能为空");
+        }
+        Date feedbackTime = request.getFeedbackTime() != null ? request.getFeedbackTime() : new Date();
+        boolean success = dutyAlertTransferService.recordFeedback(id, request.getFeedback(), feedbackTime);
+        if (success) {
+            return success("反馈已录入");
+        } else {
+            return error("反馈录入失败");
+        }
+    }
+
+    /**
+     * 标记为已处理
+     */
+    @PreAuthorize("@ss.hasPermi('duty:transfer:edit')")
+    @Log(title = "转警管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/{id}/process")
+    public AjaxResult process(@PathVariable Long id, @RequestBody(required = false) TransferProcessRequest request) {
+        String remark = request != null ? request.getRemark() : null;
+        boolean success = dutyAlertTransferService.markProcessed(id, remark);
+        if (success) {
+            return success("转警记录已标记为已处理");
+        } else {
+            return error("标记操作失败");
+        }
+    }
+}

+ 100 - 0
aegis-admin/src/main/java/com/aegis/web/controller/emergency/duty/DutyShiftController.java

@@ -0,0 +1,100 @@
+package com.aegis.web.controller.emergency.duty;
+
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+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 com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.page.TableDataInfo;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.emergency.duty.domain.DutyShift;
+import com.aegis.emergency.duty.service.DutyShiftService;
+
+/**
+ * 值班排班Controller
+ * 
+ * @author aegis
+ */
+@RestController
+@RequestMapping("/duty/shifts")
+public class DutyShiftController extends BaseController {
+
+    @Autowired
+    private DutyShiftService dutyShiftService;
+
+    /**
+     * 查询排班列表
+     */
+    @PreAuthorize("@ss.hasPermi('duty:shift:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(DutyShift condition) {
+        startPage();
+        List<DutyShift> list = dutyShiftService.listByCondition(condition);
+        return getDataTable(list);
+    }
+
+    /**
+     * 根据ID查询排班
+     */
+    @PreAuthorize("@ss.hasPermi('duty:shift:query')")
+    @GetMapping("/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        DutyShift shift = dutyShiftService.getById(id);
+        if (shift == null) {
+            return error("排班不存在或已删除");
+        }
+        return success(shift);
+    }
+
+    /**
+     * 查询当前正在值班的排班
+     */
+    @GetMapping("/current")
+    public AjaxResult getCurrentShift() {
+        DutyShift currentShift = dutyShiftService.getCurrentShift();
+        return success(currentShift);
+    }
+
+    /**
+     * 新增排班
+     */
+    @PreAuthorize("@ss.hasPermi('duty:shift:add')")
+    @Log(title = "值班排班", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody DutyShift shift) {
+        return toAjax(dutyShiftService.save(shift));
+    }
+
+    /**
+     * 修改排班
+     */
+    @PreAuthorize("@ss.hasPermi('duty:shift:edit')")
+    @Log(title = "值班排班", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody DutyShift shift) {
+        return toAjax(dutyShiftService.updateById(shift));
+    }
+
+    /**
+     * 删除排班
+     */
+    @PreAuthorize("@ss.hasPermi('duty:shift:remove')")
+    @Log(title = "值班排班", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        boolean allSuccess = true;
+        for (Long id : ids) {
+            allSuccess &= dutyShiftService.removeById(id);
+        }
+        return toAjax(allSuccess);
+    }
+}

+ 96 - 0
aegis-admin/src/main/java/com/aegis/web/controller/emergency/duty/DutyShiftMemberController.java

@@ -0,0 +1,96 @@
+package com.aegis.web.controller.emergency.duty;
+
+import java.util.List;
+import java.util.Arrays;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+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 com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.page.TableDataInfo;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.emergency.duty.domain.DutyShiftMember;
+import com.aegis.emergency.duty.service.DutyShiftMemberService;
+
+/**
+ * 排班成员Controller
+ * 
+ * @author aegis
+ */
+@RestController
+@RequestMapping("/duty/shift-members")
+public class DutyShiftMemberController extends BaseController {
+
+    @Autowired
+    private DutyShiftMemberService dutyShiftMemberService;
+
+    /**
+     * 查询排班成员列表
+     */
+    @PreAuthorize("@ss.hasPermi('duty:shift:member:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(DutyShiftMember condition) {
+        startPage();
+        List<DutyShiftMember> list = dutyShiftMemberService.listByCondition(condition);
+        return getDataTable(list);
+    }
+
+    /**
+     * 根据排班ID查询成员列表
+     */
+    @PreAuthorize("@ss.hasPermi('duty:shift:member:list')")
+    @GetMapping("/shift/{shiftId}")
+    public AjaxResult listByShiftId(@PathVariable Long shiftId) {
+        DutyShiftMember condition = new DutyShiftMember();
+        condition.setShiftId(shiftId);
+        List<DutyShiftMember> list = dutyShiftMemberService.listByCondition(condition);
+        return success(list);
+    }
+
+    /**
+     * 根据ID查询排班成员
+     */
+    @PreAuthorize("@ss.hasPermi('duty:shift:member:query')")
+    @GetMapping("/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        return success(dutyShiftMemberService.getById(id));
+    }
+
+    /**
+     * 新增排班成员
+     */
+    @PreAuthorize("@ss.hasPermi('duty:shift:member:add')")
+    @Log(title = "排班成员", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody DutyShiftMember member) {
+        return toAjax(dutyShiftMemberService.save(member));
+    }
+
+    /**
+     * 修改排班成员
+     */
+    @PreAuthorize("@ss.hasPermi('duty:shift:member:edit')")
+    @Log(title = "排班成员", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody DutyShiftMember member) {
+        return toAjax(dutyShiftMemberService.updateById(member));
+    }
+
+    /**
+     * 删除排班成员
+     */
+    @PreAuthorize("@ss.hasPermi('duty:shift:member:remove')")
+    @Log(title = "排班成员", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        return toAjax(dutyShiftMemberService.removeByIds(Arrays.asList(ids)));
+    }
+}

+ 204 - 0
aegis-admin/src/main/java/com/aegis/web/controller/emergency/event/EventController.java

@@ -0,0 +1,204 @@
+package com.aegis.web.controller.emergency.event;
+
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+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.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.page.TableDataInfo;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.emergency.event.domain.Event;
+import com.aegis.emergency.event.domain.EventLog;
+import com.aegis.emergency.event.dto.EventReportRequest;
+import com.aegis.emergency.event.service.IEventService;
+import com.aegis.emergency.event.service.IEventLogService;
+
+/**
+ * 事件Controller
+ * 
+ * @author aegis
+ */
+@RestController
+@RequestMapping("/api/events")
+public class EventController extends BaseController {
+
+    @Autowired
+    private IEventService eventService;
+
+    @Autowired
+    private IEventLogService eventLogService;
+
+    /**
+     * 查询事件列表(分页)
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:event:list')")
+    @GetMapping
+    public TableDataInfo list(Event condition) {
+        startPage();
+        List<Event> list = eventService.listByCondition(condition);
+        return getDataTable(list);
+    }
+
+    /**
+     * 根据ID查询事件详情
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:event:query')")
+    @GetMapping("/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        Event event = eventService.getById(id);
+        if (event == null) {
+            return error("事件不存在或已删除");
+        }
+        return success(event);
+    }
+
+    /**
+     * 根据事件编号查询事件
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:event:query')")
+    @GetMapping("/no/{eventNo}")
+    public AjaxResult getByEventNo(@PathVariable String eventNo) {
+        Event event = eventService.getByEventNo(eventNo);
+        if (event == null) {
+            return error("事件不存在或已删除");
+        }
+        return success(event);
+    }
+
+    /**
+     * 新增事件
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:event:add')")
+    @Log(title = "事件管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody Event event) {
+        return toAjax(eventService.save(event));
+    }
+
+    /**
+     * 修改事件
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:event:edit')")
+    @Log(title = "事件管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/{id}")
+    public AjaxResult edit(@PathVariable Long id, @RequestBody Event event) {
+        event.setId(id);
+        return toAjax(eventService.updateById(event));
+    }
+
+    /**
+     * 删除事件(支持批量,仅限草稿状态)
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:event:remove')")
+    @Log(title = "事件管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        boolean allSuccess = true;
+        for (Long id : ids) {
+            allSuccess &= eventService.removeById(id);
+        }
+        return toAjax(allSuccess);
+    }
+
+    /**
+     * 开始处理事件(状态从pending变为processing)
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:event:edit')")
+    @Log(title = "事件管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/start-processing")
+    public AjaxResult startProcessing(@PathVariable Long id) {
+        return toAjax(eventService.startProcessing(id));
+    }
+
+    /**
+     * 完成事件处理(状态从processing变为completed)
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:event:edit')")
+    @Log(title = "事件管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/complete")
+    public AjaxResult complete(@PathVariable Long id) {
+        return toAjax(eventService.completeEvent(id));
+    }
+
+    /**
+     * 归档事件(状态从completed变为archived)
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:event:edit')")
+    @Log(title = "事件管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/archive")
+    public AjaxResult archive(@PathVariable Long id) {
+        return toAjax(eventService.archiveEvent(id));
+    }
+
+    /**
+     * 查询事件操作日志(支持按action类型过滤)
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:event:query')")
+    @GetMapping("/{id}/logs")
+    public AjaxResult getLogs(@PathVariable Long id, 
+                              @RequestParam(required = false) String action) {
+        EventLog condition = new EventLog();
+        condition.setEventId(id);
+        if (action != null && !action.isEmpty()) {
+            condition.setAction(action);
+        }
+        List<EventLog> logs = eventLogService.listByCondition(condition);
+        return success(logs);
+    }
+
+    /**
+     * 上报事件
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:event:report')")
+    @Log(title = "事件管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/report")
+    public AjaxResult report(@PathVariable Long id, @RequestBody EventReportRequest request) {
+        if (request == null || (request.getReportContent() == null || request.getReportContent().trim().isEmpty())) {
+            return error("报告内容不能为空");
+        }
+        return toAjax(eventService.reportEvent(id, request.getReportContent()));
+    }
+
+    /**
+     * 续报事件
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:event:report')")
+    @Log(title = "事件管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/follow-up-report")
+    public AjaxResult followUpReport(@PathVariable Long id, @RequestBody EventReportRequest request) {
+        if (request == null || (request.getReportContent() == null || request.getReportContent().trim().isEmpty())) {
+            return error("报告内容不能为空");
+        }
+        return toAjax(eventService.followUpReport(id, request.getReportContent()));
+    }
+
+    /**
+     * 标记事件为重点
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:event:pin')")
+    @Log(title = "事件管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/pin")
+    public AjaxResult pinEvent(@PathVariable Long id) {
+        return toAjax(eventService.markImportant(id, true));
+    }
+
+    /**
+     * 取消事件重点标记
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:event:pin')")
+    @Log(title = "事件管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/unpin")
+    public AjaxResult unpinEvent(@PathVariable Long id) {
+        return toAjax(eventService.markImportant(id, false));
+    }
+}

+ 47 - 0
aegis-admin/src/main/java/com/aegis/web/controller/emergency/plan/PlanApprovalController.java

@@ -0,0 +1,47 @@
+package com.aegis.web.controller.emergency.plan;
+
+import com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.emergency.plan.domain.PlanApproval;
+import com.aegis.emergency.plan.service.IPlanApprovalService;
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+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.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 预案审批 Controller
+ */
+@RestController
+@RequestMapping("/api/plans/{planId}/approvals")
+public class PlanApprovalController extends BaseController {
+
+    @Autowired
+    private IPlanApprovalService approvalService;
+
+    @PreAuthorize("@ss.hasPermi('emergency:plan:approval:list')")
+    @GetMapping
+    public AjaxResult list(@PathVariable Long planId) {
+        List<PlanApproval> approvals = approvalService.lambdaQuery()
+            .eq(PlanApproval::getPlanId, planId)
+            .orderByAsc(PlanApproval::getApprovalOrder)
+            .list();
+        return success(approvals);
+    }
+
+    @PreAuthorize("@ss.hasPermi('emergency:plan:approval:add')")
+    @Log(title = "预案审批", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult record(@PathVariable Long planId, @RequestBody PlanApproval approval) {
+        approval.setPlanId(planId);
+        PlanApproval saved = approvalService.recordApproval(approval);
+        return success(saved);
+    }
+}

+ 110 - 0
aegis-admin/src/main/java/com/aegis/web/controller/emergency/plan/PlanController.java

@@ -0,0 +1,110 @@
+package com.aegis.web.controller.emergency.plan;
+
+import com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.page.TableDataInfo;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.emergency.plan.domain.Plan;
+import com.aegis.emergency.plan.service.IPlanService;
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+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.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 预案管理 Controller
+ */
+@RestController
+@RequestMapping("/api/plans")
+public class PlanController extends BaseController {
+
+    @Autowired
+    private IPlanService planService;
+
+    /**
+     * 查询预案列表
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:plan:list')")
+    @GetMapping
+    public TableDataInfo list(Plan condition,
+                              @RequestParam(required = false) Date beginTime,
+                              @RequestParam(required = false) Date endTime) {
+        startPage();
+        List<Plan> list = planService.listByCondition(condition, beginTime, endTime);
+        return getDataTable(list);
+    }
+
+    /**
+     * 获取预案详情
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:plan:query')")
+    @GetMapping("/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        Plan plan = planService.getById(id);
+        if (plan == null || "2".equals(plan.getDelFlag())) {
+            return error("预案不存在或已删除");
+        }
+        return success(plan);
+    }
+
+    /**
+     * 新增预案
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:plan:add')")
+    @Log(title = "预案管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody Plan plan) {
+        boolean saved = planService.save(plan);
+        if (saved) {
+            // 返回保存后的预案对象(包含ID)
+            return success(plan);
+        }
+        return error("保存失败");
+    }
+
+    /**
+     * 修改预案
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:plan:edit')")
+    @Log(title = "预案管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/{id}")
+    public AjaxResult edit(@PathVariable Long id, @RequestBody Plan plan) {
+        plan.setId(id);
+        return toAjax(planService.updateById(plan));
+    }
+
+    /**
+     * 删除预案(支持批量)
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:plan:remove')")
+    @Log(title = "预案管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        boolean result = true;
+        for (Serializable id : ids) {
+            result &= planService.removeById(id);
+        }
+        return toAjax(result);
+    }
+
+    /**
+     * 发布预案
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:plan:publish')")
+    @Log(title = "预案管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/publish")
+    public AjaxResult publish(@PathVariable Long id) {
+        return toAjax(planService.publishPlan(id));
+    }
+}

+ 66 - 0
aegis-admin/src/main/java/com/aegis/web/controller/emergency/plan/PlanParticipantController.java

@@ -0,0 +1,66 @@
+package com.aegis.web.controller.emergency.plan;
+
+import com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.emergency.plan.domain.PlanParticipant;
+import com.aegis.emergency.plan.service.IPlanParticipantService;
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+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;
+
+/**
+ * 预案执行对象 Controller
+ */
+@RestController
+@RequestMapping("/api/plans/{planId}/participants")
+public class PlanParticipantController extends BaseController {
+
+    @Autowired
+    private IPlanParticipantService participantService;
+
+    @PreAuthorize("@ss.hasPermi('emergency:plan:participant:list')")
+    @GetMapping
+    public AjaxResult list(@PathVariable Long planId) {
+        List<PlanParticipant> list = participantService.listByPlanId(planId);
+        return success(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('emergency:plan:participant:add')")
+    @Log(title = "预案执行对象", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@PathVariable Long planId, @RequestBody PlanParticipant participant) {
+        participant.setPlanId(planId);
+        participant.setDelFlag("0");
+        return toAjax(participantService.save(participant));
+    }
+
+    @PreAuthorize("@ss.hasPermi('emergency:plan:participant:edit')")
+    @Log(title = "预案执行对象", businessType = BusinessType.UPDATE)
+    @PutMapping("/{id}")
+    public AjaxResult edit(@PathVariable Long planId, @PathVariable Long id, @RequestBody PlanParticipant participant) {
+        participant.setId(id);
+        participant.setPlanId(planId);
+        return toAjax(participantService.updateById(participant));
+    }
+
+    @PreAuthorize("@ss.hasPermi('emergency:plan:participant:remove')")
+    @Log(title = "预案执行对象", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        boolean ok = true;
+        for (Long id : ids) {
+            ok &= participantService.removeById(id);
+        }
+        return toAjax(ok);
+    }
+}

+ 71 - 0
aegis-admin/src/main/java/com/aegis/web/controller/emergency/plan/PlanStepController.java

@@ -0,0 +1,71 @@
+package com.aegis.web.controller.emergency.plan;
+
+import com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.emergency.plan.domain.PlanStep;
+import com.aegis.emergency.plan.service.IPlanStepResourceService;
+import com.aegis.emergency.plan.service.IPlanStepService;
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+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;
+
+/**
+ * 预案行动项 Controller
+ */
+@RestController
+@RequestMapping("/api/plans/{planId}/steps")
+public class PlanStepController extends BaseController {
+
+    @Autowired
+    private IPlanStepService stepService;
+
+    @Autowired
+    private IPlanStepResourceService stepResourceService;
+
+    @PreAuthorize("@ss.hasPermi('emergency:plan:step:list')")
+    @GetMapping
+    public AjaxResult list(@PathVariable Long planId) {
+        List<PlanStep> list = stepService.listByPlanId(planId);
+        return success(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('emergency:plan:step:add')")
+    @Log(title = "预案行动项", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@PathVariable Long planId, @RequestBody PlanStep step) {
+        step.setPlanId(planId);
+        step.setDelFlag("0");
+        return toAjax(stepService.save(step));
+    }
+
+    @PreAuthorize("@ss.hasPermi('emergency:plan:step:edit')")
+    @Log(title = "预案行动项", businessType = BusinessType.UPDATE)
+    @PutMapping("/{id}")
+    public AjaxResult edit(@PathVariable Long planId, @PathVariable Long id, @RequestBody PlanStep step) {
+        step.setId(id);
+        step.setPlanId(planId);
+        return toAjax(stepService.updateById(step));
+    }
+
+    @PreAuthorize("@ss.hasPermi('emergency:plan:step:remove')")
+    @Log(title = "预案行动项", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long planId, @PathVariable Long[] ids) {
+        boolean ok = true;
+        for (Long id : ids) {
+            stepResourceService.removeByStepId(id);
+            ok &= stepService.removeById(id);
+        }
+        return toAjax(ok);
+    }
+}

+ 69 - 0
aegis-admin/src/main/java/com/aegis/web/controller/emergency/plan/PlanStepResourceController.java

@@ -0,0 +1,69 @@
+package com.aegis.web.controller.emergency.plan;
+
+import com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.emergency.plan.domain.PlanStepResource;
+import com.aegis.emergency.plan.service.IPlanStepResourceService;
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+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;
+
+/**
+ * 行动项资源 Controller
+ */
+@RestController
+@RequestMapping("/api/plans/{planId}/steps/{stepId}/resources")
+public class PlanStepResourceController extends BaseController {
+
+    @Autowired
+    private IPlanStepResourceService stepResourceService;
+
+    @PreAuthorize("@ss.hasPermi('emergency:plan:resource:list')")
+    @GetMapping
+    public AjaxResult list(@PathVariable Long stepId) {
+        List<PlanStepResource> list = stepResourceService.listByStepId(stepId);
+        return success(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('emergency:plan:resource:add')")
+    @Log(title = "预案资源", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@PathVariable Long planId, @PathVariable Long stepId, @RequestBody PlanStepResource resource) {
+        resource.setPlanId(planId);
+        resource.setStepId(stepId);
+        resource.setDelFlag("0");
+        return toAjax(stepResourceService.save(resource));
+    }
+
+    @PreAuthorize("@ss.hasPermi('emergency:plan:resource:edit')")
+    @Log(title = "预案资源", businessType = BusinessType.UPDATE)
+    @PutMapping("/{id}")
+    public AjaxResult edit(@PathVariable Long planId, @PathVariable Long stepId, @PathVariable Long id,
+                           @RequestBody PlanStepResource resource) {
+        resource.setId(id);
+        resource.setPlanId(planId);
+        resource.setStepId(stepId);
+        return toAjax(stepResourceService.updateById(resource));
+    }
+
+    @PreAuthorize("@ss.hasPermi('emergency:plan:resource:remove')")
+    @Log(title = "预案资源", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        boolean ok = true;
+        for (Long id : ids) {
+            ok &= stepResourceService.removeById(id);
+        }
+        return toAjax(ok);
+    }
+}

+ 44 - 0
aegis-admin/src/main/java/com/aegis/web/controller/emergency/plan/PlanUsageController.java

@@ -0,0 +1,44 @@
+package com.aegis.web.controller.emergency.plan;
+
+import com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.emergency.plan.domain.PlanUsage;
+import com.aegis.emergency.plan.service.IPlanUsageService;
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+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.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 预案使用记录 Controller
+ */
+@RestController
+@RequestMapping("/api/plans/{planId}/usage")
+public class PlanUsageController extends BaseController {
+
+    @Autowired
+    private IPlanUsageService usageService;
+
+    @PreAuthorize("@ss.hasPermi('emergency:plan:usage:list')")
+    @GetMapping
+    public AjaxResult list(@PathVariable Long planId) {
+        List<PlanUsage> usages = usageService.listByPlanId(planId);
+        return success(usages);
+    }
+
+    @PreAuthorize("@ss.hasPermi('emergency:plan:usage:add')")
+    @Log(title = "预案使用记录", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@PathVariable Long planId, @RequestBody PlanUsage usage) {
+        usage.setPlanId(planId);
+        usage.setDelFlag("0");
+        return toAjax(usageService.save(usage));
+    }
+}

+ 51 - 0
aegis-admin/src/main/java/com/aegis/web/controller/emergency/plan/PlanVersionController.java

@@ -0,0 +1,51 @@
+package com.aegis.web.controller.emergency.plan;
+
+import com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.emergency.plan.domain.PlanVersion;
+import com.aegis.emergency.plan.service.IPlanVersionService;
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+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.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 预案版本 Controller
+ */
+@RestController
+@RequestMapping("/api/plans/{planId}/versions")
+public class PlanVersionController extends BaseController {
+
+    @Autowired
+    private IPlanVersionService planVersionService;
+
+    @PreAuthorize("@ss.hasPermi('emergency:plan:version:list')")
+    @GetMapping
+    public AjaxResult list(@PathVariable Long planId) {
+        List<PlanVersion> versions = planVersionService.listByPlanId(planId);
+        return success(versions);
+    }
+
+    @PreAuthorize("@ss.hasPermi('emergency:plan:version:add')")
+    @Log(title = "预案版本", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult create(@PathVariable Long planId, @RequestBody PlanVersion version) {
+        version.setPlanId(planId);
+        PlanVersion saved = planVersionService.createVersion(version);
+        return success(saved);
+    }
+
+    @PreAuthorize("@ss.hasPermi('emergency:plan:version:restore')")
+    @Log(title = "预案版本", businessType = BusinessType.UPDATE)
+    @PostMapping("/{versionId}/restore")
+    public AjaxResult restore(@PathVariable Long planId, @PathVariable Long versionId) {
+        return toAjax(planVersionService.restoreVersion(planId, versionId));
+    }
+}

+ 74 - 0
aegis-admin/src/main/java/com/aegis/web/controller/emergency/report/ProvinceReportController.java

@@ -0,0 +1,74 @@
+package com.aegis.web.controller.emergency.report;
+
+import com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.page.TableDataInfo;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.emergency.report.domain.ProvinceReport;
+import com.aegis.emergency.report.dto.ProvinceReportRequest;
+import com.aegis.emergency.report.service.IProvinceReportService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * 省厅上报管理 Controller
+ * 
+ * @author aegis
+ */
+@Api(tags = "应急调度-省厅上报管理")
+@RestController
+@RequestMapping("/api/emergency/province-reports")
+public class ProvinceReportController extends BaseController {
+
+    @Autowired
+    private IProvinceReportService provinceReportService;
+
+    @ApiOperation("查询上报记录列表")
+    @GetMapping("/list")
+    public TableDataInfo list(ProvinceReport condition) {
+        startPage();
+        List<ProvinceReport> list = provinceReportService.listReports(condition);
+        return getDataTable(list);
+    }
+
+    @ApiOperation("根据事件ID查询上报记录列表")
+    @GetMapping("/event/{eventId}")
+    public AjaxResult listByEvent(@PathVariable Long eventId) {
+        ProvinceReport condition = new ProvinceReport();
+        condition.setEventId(eventId);
+        List<ProvinceReport> list = provinceReportService.listReports(condition);
+        return success(list);
+    }
+
+    @ApiOperation("获取上报记录详情")
+    @GetMapping("/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        ProvinceReport report = provinceReportService.getById(id);
+        if (report == null) {
+            return error("上报记录不存在或已删除");
+        }
+        return success(report);
+    }
+
+    @ApiOperation("提交上报")
+    @Log(title = "省厅上报管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult submit(@RequestBody ProvinceReportRequest request) {
+        ProvinceReport report = provinceReportService.submit(request);
+        return success(report);
+    }
+
+    @ApiOperation("删除上报记录")
+    @Log(title = "省厅上报管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        return toAjax(provinceReportService.removeByIds(Arrays.asList(ids)));
+    }
+
+}

+ 83 - 0
aegis-admin/src/main/java/com/aegis/web/controller/emergency/report/ReportTemplateController.java

@@ -0,0 +1,83 @@
+package com.aegis.web.controller.emergency.report;
+
+import com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.page.TableDataInfo;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.emergency.report.domain.ReportTemplate;
+import com.aegis.emergency.report.service.IReportTemplateService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * 上报模板管理 Controller
+ * 
+ * @author aegis
+ */
+@RestController
+@RequestMapping("/api/emergency/report-templates")
+public class ReportTemplateController extends BaseController {
+
+    @Autowired
+    private IReportTemplateService reportTemplateService;
+
+    /**
+     * 查询模板列表
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:report:template:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(ReportTemplate condition) {
+        startPage();
+        List<ReportTemplate> list = reportTemplateService.listTemplates(condition);
+        return getDataTable(list);
+    }
+
+    /**
+     * 获取模板详情
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:report:template:query')")
+    @GetMapping("/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        ReportTemplate template = reportTemplateService.getById(id);
+        if (template == null) {
+            return error("模板不存在或已删除");
+        }
+        return success(template);
+    }
+
+    /**
+     * 新增模板
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:report:template:add')")
+    @Log(title = "上报模板管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody ReportTemplate template) {
+        return toAjax(reportTemplateService.save(template));
+    }
+
+    /**
+     * 修改模板
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:report:template:edit')")
+    @Log(title = "上报模板管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/{id}")
+    public AjaxResult edit(@PathVariable Long id, @RequestBody ReportTemplate template) {
+        template.setId(id);
+        return toAjax(reportTemplateService.updateById(template));
+    }
+
+    /**
+     * 删除模板
+     */
+    @PreAuthorize("@ss.hasPermi('emergency:report:template:remove')")
+    @Log(title = "上报模板管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        return toAjax(reportTemplateService.removeByIds(Arrays.asList(ids)));
+    }
+}

+ 100 - 0
aegis-admin/src/main/java/com/aegis/web/controller/file/FileController.java

@@ -0,0 +1,100 @@
+package com.aegis.web.controller.file;
+
+import java.util.List;
+import javax.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+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.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+import com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.page.TableDataInfo;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.file.domain.FileInfo;
+import com.aegis.file.service.IFileService;
+
+/**
+ * 文件管理Controller
+ * 
+ * @author aegis
+ */
+@RestController
+@RequestMapping("/file")
+public class FileController extends BaseController {
+    
+    @Autowired
+    private IFileService fileService;
+    
+    /**
+     * 上传文件(单个)
+     */
+    @PreAuthorize("@ss.hasPermi('file:upload')")
+    @Log(title = "文件管理", businessType = BusinessType.INSERT)
+    @PostMapping("/upload")
+    public AjaxResult uploadFile(@RequestParam("file") MultipartFile file) {
+        // Service层已处理异常并抛出ServiceException,这里直接调用即可
+        // 全局异常处理器会统一处理ServiceException
+        FileInfo fileInfo = fileService.uploadFile(file);
+        return success(fileInfo);
+    }
+    
+    /**
+     * 根据ID获取文件信息
+     */
+    @PreAuthorize("@ss.hasPermi('file:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        FileInfo fileInfo = fileService.selectFileById(id);
+        if (fileInfo == null) {
+            return error("文件不存在或已删除");
+        }
+        return success(fileInfo);
+    }
+    
+    /**
+     * 批量获取文件信息
+     */
+    @PreAuthorize("@ss.hasPermi('file:query')")
+    @GetMapping("/batch")
+    public AjaxResult getBatch(@RequestParam("fileIds") Long[] fileIds) {
+        List<FileInfo> files = fileService.selectFilesByIds(fileIds);
+        return success(files);
+    }
+    
+    /**
+     * 下载文件
+     */
+    @PreAuthorize("@ss.hasPermi('file:download')")
+    @GetMapping("/{id}/download")
+    public void downloadFile(@PathVariable Long id, HttpServletResponse response) {
+        fileService.downloadFile(id, response);
+    }
+    
+    /**
+     * 查询文件列表
+     */
+    @PreAuthorize("@ss.hasPermi('file:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(FileInfo fileInfo) {
+        startPage();
+        List<FileInfo> list = fileService.selectFileList(fileInfo);
+        return getDataTable(list);
+    }
+    
+    /**
+     * 删除文件
+     */
+    @PreAuthorize("@ss.hasPermi('file:remove')")
+    @Log(title = "文件管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{id}")
+    public AjaxResult remove(@PathVariable Long id) {
+        return toAjax(fileService.deleteFile(id));
+    }
+}

+ 106 - 0
aegis-admin/src/main/java/com/aegis/web/controller/knowledge/KnowledgeCategoryController.java

@@ -0,0 +1,106 @@
+package com.aegis.web.controller.knowledge;
+
+import com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.knowledge.domain.KnowledgeCategory;
+import com.aegis.knowledge.service.IKnowledgeCategoryService;
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+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;
+
+/**
+ * 知识库分类管理 Controller
+ */
+@RestController
+@RequestMapping("/api/knowledge/categories")
+public class KnowledgeCategoryController extends BaseController {
+
+    @Autowired
+    private IKnowledgeCategoryService categoryService;
+
+    /**
+     * 查询分类列表(树形结构)
+     */
+    @PreAuthorize("@ss.hasPermi('knowledge:category:list')")
+    @GetMapping
+    public AjaxResult list() {
+        List<KnowledgeCategory> list = categoryService.listTree();
+        return success(list);
+    }
+
+    /**
+     * 根据父分类ID查询子分类
+     */
+    @PreAuthorize("@ss.hasPermi('knowledge:category:list')")
+    @GetMapping("/parent/{parentId}")
+    public AjaxResult listByParentId(@PathVariable Long parentId) {
+        List<KnowledgeCategory> list = categoryService.listByParentId(parentId);
+        return success(list);
+    }
+
+    /**
+     * 获取分类详情
+     */
+    @PreAuthorize("@ss.hasPermi('knowledge:category:query')")
+    @GetMapping("/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        KnowledgeCategory category = categoryService.getById(id);
+        if (category == null || "2".equals(category.getDelFlag())) {
+            return error("分类不存在或已删除");
+        }
+        return success(category);
+    }
+
+    /**
+     * 新增分类
+     */
+    @PreAuthorize("@ss.hasPermi('knowledge:category:add')")
+    @Log(title = "知识库分类管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody KnowledgeCategory category) {
+        return toAjax(categoryService.save(category));
+    }
+
+    /**
+     * 修改分类
+     */
+    @PreAuthorize("@ss.hasPermi('knowledge:category:edit')")
+    @Log(title = "知识库分类管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/{id}")
+    public AjaxResult edit(@PathVariable Long id, @RequestBody KnowledgeCategory category) {
+        category.setId(id);
+        return toAjax(categoryService.updateById(category));
+    }
+
+    /**
+     * 删除分类
+     */
+    @PreAuthorize("@ss.hasPermi('knowledge:category:remove')")
+    @Log(title = "知识库分类管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{id}")
+    public AjaxResult remove(@PathVariable Long id) {
+        KnowledgeCategory category = categoryService.getById(id);
+        if (category == null || "2".equals(category.getDelFlag())) {
+            return error("分类不存在或已删除");
+        }
+        // 检查是否有子分类
+        List<KnowledgeCategory> children = categoryService.listByParentId(id);
+        if (children != null && !children.isEmpty()) {
+            return error("该分类下存在子分类,无法删除");
+        }
+        // 逻辑删除
+        category.setDelFlag("2");
+        return toAjax(categoryService.updateById(category));
+    }
+}
+

+ 172 - 0
aegis-admin/src/main/java/com/aegis/web/controller/knowledge/KnowledgeDocumentController.java

@@ -0,0 +1,172 @@
+package com.aegis.web.controller.knowledge;
+
+import com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.page.TableDataInfo;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.common.exception.ServiceException;
+import com.aegis.common.utils.StringUtils;
+import com.aegis.file.domain.FileInfo;
+import com.aegis.file.service.IFileService;
+import com.aegis.knowledge.domain.KnowledgeDocument;
+import com.aegis.knowledge.service.IKnowledgeDocumentService;
+import java.util.Date;
+import java.util.List;
+import javax.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+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.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 知识库文档管理 Controller
+ */
+@RestController
+@RequestMapping("/api/knowledge/documents")
+public class KnowledgeDocumentController extends BaseController {
+
+    @Autowired
+    private IKnowledgeDocumentService documentService;
+
+    @Autowired
+    private IFileService fileService;
+
+    /**
+     * 查询文档列表
+     */
+    @PreAuthorize("@ss.hasPermi('knowledge:document:list')")
+    @GetMapping
+    public TableDataInfo list(@RequestParam(required = false) String keyword,
+                              @RequestParam(required = false) Long categoryId,
+                              @RequestParam(required = false) String tags,
+                              @RequestParam(required = false) Date beginTime,
+                              @RequestParam(required = false) Date endTime) {
+        startPage();
+        List<KnowledgeDocument> list = documentService.listByCondition(keyword, categoryId, tags, beginTime, endTime);
+        return getDataTable(list);
+    }
+
+    /**
+     * 获取文档详情
+     */
+    @PreAuthorize("@ss.hasPermi('knowledge:document:query')")
+    @GetMapping("/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        KnowledgeDocument document = documentService.getById(id);
+        if (document == null || "2".equals(document.getDelFlag())) {
+            return error("文档不存在或已删除");
+        }
+        // 增加查看次数
+        documentService.incrementViewCount(id);
+        return success(document);
+    }
+
+    /**
+     * 新增文档
+     */
+    @PreAuthorize("@ss.hasPermi('knowledge:document:add')")
+    @Log(title = "知识库文档管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody KnowledgeDocument document) {
+        if (document == null) {
+            return error("文档信息不能为空");
+        }
+        if (StringUtils.isBlank(document.getTitle())) {
+            return error("文档标题不能为空");
+        }
+        if (document.getFileId() == null) {
+            return error("文件ID不能为空");
+        }
+        
+        // 验证文件是否存在
+        FileInfo fileInfo = fileService.selectFileById(document.getFileId());
+        if (fileInfo == null) {
+            return error("文件不存在");
+        }
+        
+        // 设置文件信息
+        if (StringUtils.isBlank(document.getFileName())) {
+            document.setFileName(fileInfo.getOriginalName());
+        }
+        if (document.getFileSize() == null) {
+            document.setFileSize(fileInfo.getFileSize());
+        }
+        if (StringUtils.isBlank(document.getFileType())) {
+            document.setFileType(fileInfo.getFileType());
+        }
+        
+        boolean saved = documentService.save(document);
+        if (saved) {
+            return success(document);
+        }
+        return error("保存失败");
+    }
+
+    /**
+     * 修改文档
+     */
+    @PreAuthorize("@ss.hasPermi('knowledge:document:edit')")
+    @Log(title = "知识库文档管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/{id}")
+    public AjaxResult edit(@PathVariable Long id, @RequestBody KnowledgeDocument document) {
+        document.setId(id);
+        return toAjax(documentService.updateById(document));
+    }
+
+    /**
+     * 删除文档
+     */
+    @PreAuthorize("@ss.hasPermi('knowledge:document:remove')")
+    @Log(title = "知识库文档管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{id}")
+    public AjaxResult remove(@PathVariable Long id) {
+        KnowledgeDocument document = documentService.getById(id);
+        if (document == null || "2".equals(document.getDelFlag())) {
+            return error("文档不存在或已删除");
+        }
+        // 逻辑删除
+        document.setDelFlag("2");
+        return toAjax(documentService.updateById(document));
+    }
+
+    /**
+     * 下载文档
+     */
+    @PreAuthorize("@ss.hasPermi('knowledge:document:download')")
+    @GetMapping("/{id}/download")
+    public void download(@PathVariable Long id, HttpServletResponse response) {
+        KnowledgeDocument document = documentService.getById(id);
+        if (document == null || "2".equals(document.getDelFlag())) {
+            throw new ServiceException("文档不存在或已删除");
+        }
+        if (document.getFileId() == null) {
+            throw new ServiceException("文件ID不能为空");
+        }
+        
+        // 增加下载次数
+        documentService.incrementDownloadCount(id);
+        
+        // 通过文件模块下载
+        fileService.downloadFile(document.getFileId(), response);
+    }
+
+    /**
+     * 根据事件类型和关键词推荐文档
+     */
+    @PreAuthorize("@ss.hasPermi('knowledge:document:recommend')")
+    @GetMapping("/recommend")
+    public AjaxResult recommend(@RequestParam(required = false) String eventType,
+                                 @RequestParam(required = false) String keywords) {
+        List<KnowledgeDocument> list = documentService.recommendDocuments(eventType, keywords);
+        return success(list);
+    }
+}
+

+ 121 - 0
aegis-admin/src/main/java/com/aegis/web/controller/monitor/CacheController.java

@@ -0,0 +1,121 @@
+package com.aegis.web.controller.monitor;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.TreeSet;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisCallback;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.security.access.prepost.PreAuthorize;
+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.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.aegis.common.constant.CacheConstants;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.utils.StringUtils;
+import com.aegis.system.domain.SysCache;
+
+/**
+ * 缓存监控
+ * 
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/monitor/cache")
+public class CacheController
+{
+    @Autowired
+    private RedisTemplate<String, String> redisTemplate;
+
+    private final static List<SysCache> caches = new ArrayList<SysCache>();
+    {
+        caches.add(new SysCache(CacheConstants.LOGIN_TOKEN_KEY, "用户信息"));
+        caches.add(new SysCache(CacheConstants.SYS_CONFIG_KEY, "配置信息"));
+        caches.add(new SysCache(CacheConstants.SYS_DICT_KEY, "数据字典"));
+        caches.add(new SysCache(CacheConstants.CAPTCHA_CODE_KEY, "验证码"));
+        caches.add(new SysCache(CacheConstants.REPEAT_SUBMIT_KEY, "防重提交"));
+        caches.add(new SysCache(CacheConstants.RATE_LIMIT_KEY, "限流处理"));
+        caches.add(new SysCache(CacheConstants.PWD_ERR_CNT_KEY, "密码错误次数"));
+    }
+
+    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
+    @GetMapping()
+    public AjaxResult getInfo() throws Exception
+    {
+        Properties info = (Properties) redisTemplate.execute((RedisCallback<Object>) connection -> connection.info());
+        Properties commandStats = (Properties) redisTemplate.execute((RedisCallback<Object>) connection -> connection.info("commandstats"));
+        Object dbSize = redisTemplate.execute((RedisCallback<Object>) connection -> connection.dbSize());
+
+        Map<String, Object> result = new HashMap<>(3);
+        result.put("info", info);
+        result.put("dbSize", dbSize);
+
+        List<Map<String, String>> pieList = new ArrayList<>();
+        commandStats.stringPropertyNames().forEach(key -> {
+            Map<String, String> data = new HashMap<>(2);
+            String property = commandStats.getProperty(key);
+            data.put("name", StringUtils.removeStart(key, "cmdstat_"));
+            data.put("value", StringUtils.substringBetween(property, "calls=", ",usec"));
+            pieList.add(data);
+        });
+        result.put("commandStats", pieList);
+        return AjaxResult.success(result);
+    }
+
+    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
+    @GetMapping("/getNames")
+    public AjaxResult cache()
+    {
+        return AjaxResult.success(caches);
+    }
+
+    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
+    @GetMapping("/getKeys/{cacheName}")
+    public AjaxResult getCacheKeys(@PathVariable String cacheName)
+    {
+        Set<String> cacheKeys = redisTemplate.keys(cacheName + "*");
+        return AjaxResult.success(new TreeSet<>(cacheKeys));
+    }
+
+    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
+    @GetMapping("/getValue/{cacheName}/{cacheKey}")
+    public AjaxResult getCacheValue(@PathVariable String cacheName, @PathVariable String cacheKey)
+    {
+        String cacheValue = redisTemplate.opsForValue().get(cacheKey);
+        SysCache sysCache = new SysCache(cacheName, cacheKey, cacheValue);
+        return AjaxResult.success(sysCache);
+    }
+
+    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
+    @DeleteMapping("/clearCacheName/{cacheName}")
+    public AjaxResult clearCacheName(@PathVariable String cacheName)
+    {
+        Collection<String> cacheKeys = redisTemplate.keys(cacheName + "*");
+        redisTemplate.delete(cacheKeys);
+        return AjaxResult.success();
+    }
+
+    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
+    @DeleteMapping("/clearCacheKey/{cacheKey}")
+    public AjaxResult clearCacheKey(@PathVariable String cacheKey)
+    {
+        redisTemplate.delete(cacheKey);
+        return AjaxResult.success();
+    }
+
+    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
+    @DeleteMapping("/clearCacheAll")
+    public AjaxResult clearCacheAll()
+    {
+        Collection<String> cacheKeys = redisTemplate.keys("*");
+        redisTemplate.delete(cacheKeys);
+        return AjaxResult.success();
+    }
+}

+ 27 - 0
aegis-admin/src/main/java/com/aegis/web/controller/monitor/ServerController.java

@@ -0,0 +1,27 @@
+package com.aegis.web.controller.monitor;
+
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.framework.web.domain.Server;
+
+/**
+ * 服务器监控
+ * 
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/monitor/server")
+public class ServerController
+{
+    @PreAuthorize("@ss.hasPermi('monitor:server:list')")
+    @GetMapping()
+    public AjaxResult getInfo() throws Exception
+    {
+        Server server = new Server();
+        server.copyTo();
+        return AjaxResult.success(server);
+    }
+}

+ 82 - 0
aegis-admin/src/main/java/com/aegis/web/controller/monitor/SysLogininforController.java

@@ -0,0 +1,82 @@
+package com.aegis.web.controller.monitor;
+
+import java.util.List;
+import javax.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+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.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.page.TableDataInfo;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.common.utils.poi.ExcelUtil;
+import com.aegis.framework.web.service.SysPasswordService;
+import com.aegis.system.domain.SysLogininfor;
+import com.aegis.system.service.ISysLogininforService;
+
+/**
+ * 系统访问记录
+ * 
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/monitor/logininfor")
+public class SysLogininforController extends BaseController
+{
+    @Autowired
+    private ISysLogininforService logininforService;
+
+    @Autowired
+    private SysPasswordService passwordService;
+
+    @PreAuthorize("@ss.hasPermi('monitor:logininfor:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(SysLogininfor logininfor)
+    {
+        startPage();
+        List<SysLogininfor> list = logininforService.selectLogininforList(logininfor);
+        return getDataTable(list);
+    }
+
+    @Log(title = "登录日志", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('monitor:logininfor:export')")
+    @PostMapping("/export")
+    public void export(HttpServletResponse response, SysLogininfor logininfor)
+    {
+        List<SysLogininfor> list = logininforService.selectLogininforList(logininfor);
+        ExcelUtil<SysLogininfor> util = new ExcelUtil<SysLogininfor>(SysLogininfor.class);
+        util.exportExcel(response, list, "登录日志");
+    }
+
+    @PreAuthorize("@ss.hasPermi('monitor:logininfor:remove')")
+    @Log(title = "登录日志", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{infoIds}")
+    public AjaxResult remove(@PathVariable Long[] infoIds)
+    {
+        return toAjax(logininforService.deleteLogininforByIds(infoIds));
+    }
+
+    @PreAuthorize("@ss.hasPermi('monitor:logininfor:remove')")
+    @Log(title = "登录日志", businessType = BusinessType.CLEAN)
+    @DeleteMapping("/clean")
+    public AjaxResult clean()
+    {
+        logininforService.cleanLogininfor();
+        return success();
+    }
+
+    @PreAuthorize("@ss.hasPermi('monitor:logininfor:unlock')")
+    @Log(title = "账户解锁", businessType = BusinessType.OTHER)
+    @GetMapping("/unlock/{userName}")
+    public AjaxResult unlock(@PathVariable("userName") String userName)
+    {
+        passwordService.clearLoginRecordCache(userName);
+        return success();
+    }
+}

+ 69 - 0
aegis-admin/src/main/java/com/aegis/web/controller/monitor/SysOperlogController.java

@@ -0,0 +1,69 @@
+package com.aegis.web.controller.monitor;
+
+import java.util.List;
+import javax.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+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.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.page.TableDataInfo;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.common.utils.poi.ExcelUtil;
+import com.aegis.system.domain.SysOperLog;
+import com.aegis.system.service.ISysOperLogService;
+
+/**
+ * 操作日志记录
+ * 
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/monitor/operlog")
+public class SysOperlogController extends BaseController
+{
+    @Autowired
+    private ISysOperLogService operLogService;
+
+    @PreAuthorize("@ss.hasPermi('monitor:operlog:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(SysOperLog operLog)
+    {
+        startPage();
+        List<SysOperLog> list = operLogService.selectOperLogList(operLog);
+        return getDataTable(list);
+    }
+
+    @Log(title = "操作日志", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('monitor:operlog:export')")
+    @PostMapping("/export")
+    public void export(HttpServletResponse response, SysOperLog operLog)
+    {
+        List<SysOperLog> list = operLogService.selectOperLogList(operLog);
+        ExcelUtil<SysOperLog> util = new ExcelUtil<SysOperLog>(SysOperLog.class);
+        util.exportExcel(response, list, "操作日志");
+    }
+
+    @Log(title = "操作日志", businessType = BusinessType.DELETE)
+    @PreAuthorize("@ss.hasPermi('monitor:operlog:remove')")
+    @DeleteMapping("/{operIds}")
+    public AjaxResult remove(@PathVariable Long[] operIds)
+    {
+        return toAjax(operLogService.deleteOperLogByIds(operIds));
+    }
+
+    @Log(title = "操作日志", businessType = BusinessType.CLEAN)
+    @PreAuthorize("@ss.hasPermi('monitor:operlog:remove')")
+    @DeleteMapping("/clean")
+    public AjaxResult clean()
+    {
+        operLogService.cleanOperLog();
+        return success();
+    }
+}

+ 83 - 0
aegis-admin/src/main/java/com/aegis/web/controller/monitor/SysUserOnlineController.java

@@ -0,0 +1,83 @@
+package com.aegis.web.controller.monitor;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+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.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.aegis.common.annotation.Log;
+import com.aegis.common.constant.CacheConstants;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.domain.model.LoginUser;
+import com.aegis.common.core.page.TableDataInfo;
+import com.aegis.common.core.redis.RedisCache;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.common.utils.StringUtils;
+import com.aegis.system.domain.SysUserOnline;
+import com.aegis.system.service.ISysUserOnlineService;
+
+/**
+ * 在线用户监控
+ * 
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/monitor/online")
+public class SysUserOnlineController extends BaseController
+{
+    @Autowired
+    private ISysUserOnlineService userOnlineService;
+
+    @Autowired
+    private RedisCache redisCache;
+
+    @PreAuthorize("@ss.hasPermi('monitor:online:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(String ipaddr, String userName)
+    {
+        Collection<String> keys = redisCache.keys(CacheConstants.LOGIN_TOKEN_KEY + "*");
+        List<SysUserOnline> userOnlineList = new ArrayList<SysUserOnline>();
+        for (String key : keys)
+        {
+            LoginUser user = redisCache.getCacheObject(key);
+            if (StringUtils.isNotEmpty(ipaddr) && StringUtils.isNotEmpty(userName))
+            {
+                userOnlineList.add(userOnlineService.selectOnlineByInfo(ipaddr, userName, user));
+            }
+            else if (StringUtils.isNotEmpty(ipaddr))
+            {
+                userOnlineList.add(userOnlineService.selectOnlineByIpaddr(ipaddr, user));
+            }
+            else if (StringUtils.isNotEmpty(userName) && StringUtils.isNotNull(user.getUser()))
+            {
+                userOnlineList.add(userOnlineService.selectOnlineByUserName(userName, user));
+            }
+            else
+            {
+                userOnlineList.add(userOnlineService.loginUserToUserOnline(user));
+            }
+        }
+        Collections.reverse(userOnlineList);
+        userOnlineList.removeAll(Collections.singleton(null));
+        return getDataTable(userOnlineList);
+    }
+
+    /**
+     * 强退用户
+     */
+    @PreAuthorize("@ss.hasPermi('monitor:online:forceLogout')")
+    @Log(title = "在线用户", businessType = BusinessType.FORCE)
+    @DeleteMapping("/{tokenId}")
+    public AjaxResult forceLogout(@PathVariable String tokenId)
+    {
+        redisCache.deleteObject(CacheConstants.LOGIN_TOKEN_KEY + tokenId);
+        return success();
+    }
+}

+ 242 - 0
aegis-admin/src/main/java/com/aegis/web/controller/notification/NotificationController.java

@@ -0,0 +1,242 @@
+package com.aegis.web.controller.notification;
+
+import java.util.List;
+import java.util.Map;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+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.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.page.TableDataInfo;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.common.utils.SecurityUtils;
+import com.aegis.notification.domain.Notification;
+import com.aegis.notification.domain.NotificationTemplate;
+import com.aegis.notification.service.INotificationService;
+import org.springframework.validation.annotation.Validated;
+
+/**
+ * 通知管理Controller
+ * 
+ * @author aegis
+ */
+@RestController
+@RequestMapping("/notification")
+public class NotificationController extends BaseController {
+    
+    @Autowired
+    private INotificationService notificationService;
+    
+    /**
+     * 查询我的通知列表
+     */
+    @PreAuthorize("@ss.hasPermi('notification:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(@RequestParam(required = false) String status, 
+                              @RequestParam(required = false) String channel) {
+        startPage();
+        Long receiverId = SecurityUtils.getUserId();
+        List<Notification> list = notificationService.selectNotificationList(receiverId, status, channel);
+        return getDataTable(list);
+    }
+    
+    /**
+     * 查询未读通知数量
+     */
+    @PreAuthorize("@ss.hasPermi('notification:query')")
+    @GetMapping("/unread-count")
+    public AjaxResult getUnreadCount() {
+        Long receiverId = SecurityUtils.getUserId();
+        int count = notificationService.countUnreadNotifications(receiverId);
+        return success(count);
+    }
+    
+    /**
+     * 根据ID获取通知详情
+     */
+    @PreAuthorize("@ss.hasPermi('notification:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        Long receiverId = SecurityUtils.getUserId();
+        Notification notification = notificationService.selectNotificationById(id, receiverId);
+        return success(notification);
+    }
+    
+    /**
+     * 发送通知(单个接收者)
+     */
+    @PreAuthorize("@ss.hasPermi('notification:send')")
+    @Log(title = "通知管理", businessType = BusinessType.INSERT)
+    @PostMapping("/send")
+    public AjaxResult send(@RequestBody Map<String, Object> params) {
+        Long receiverId = Long.valueOf(params.get("receiverId").toString());
+        String title = params.get("title").toString();
+        String content = params.get("content").toString();
+        String channel = params.containsKey("channel") ? params.get("channel").toString() : "station";
+        String businessType = params.containsKey("businessType") ? params.get("businessType").toString() : null;
+        Long businessId = params.containsKey("businessId") ? Long.valueOf(params.get("businessId").toString()) : null;
+        
+        Long notificationId = notificationService.sendNotification(receiverId, title, content, 
+                                                                    channel, businessType, businessId);
+        return success(notificationId);
+    }
+    
+    /**
+     * 发送通知(批量接收者)
+     */
+    @PreAuthorize("@ss.hasPermi('notification:send')")
+    @Log(title = "通知管理", businessType = BusinessType.INSERT)
+    @PostMapping("/send-batch")
+    public AjaxResult sendBatch(@RequestBody Map<String, Object> params) {
+        @SuppressWarnings("unchecked")
+        List<Long> receiverIds = (List<Long>) params.get("receiverIds");
+        String title = params.get("title").toString();
+        String content = params.get("content").toString();
+        String channel = params.containsKey("channel") ? params.get("channel").toString() : "station";
+        String businessType = params.containsKey("businessType") ? params.get("businessType").toString() : null;
+        Long businessId = params.containsKey("businessId") ? Long.valueOf(params.get("businessId").toString()) : null;
+        
+        List<Long> notificationIds = notificationService.sendBatchNotification(receiverIds, title, content, 
+                                                                                channel, businessType, businessId);
+        return success(notificationIds);
+    }
+    
+    /**
+     * 使用模板发送通知
+     */
+    @PreAuthorize("@ss.hasPermi('notification:send')")
+    @Log(title = "通知管理", businessType = BusinessType.INSERT)
+    @PostMapping("/send-template")
+    public AjaxResult sendTemplate(@RequestBody Map<String, Object> params) {
+        Long receiverId = Long.valueOf(params.get("receiverId").toString());
+        String templateCode = params.get("templateCode").toString();
+        @SuppressWarnings("unchecked")
+        Map<String, Object> variables = params.containsKey("variables") ? 
+            (Map<String, Object>) params.get("variables") : null;
+        String channel = params.containsKey("channel") ? params.get("channel").toString() : null;
+        String businessType = params.containsKey("businessType") ? params.get("businessType").toString() : null;
+        Long businessId = params.containsKey("businessId") ? Long.valueOf(params.get("businessId").toString()) : null;
+        
+        Long notificationId = notificationService.sendNotificationByTemplate(receiverId, templateCode, variables, 
+                                                                              channel, businessType, businessId);
+        return success(notificationId);
+    }
+    
+    /**
+     * 标记通知为已读
+     */
+    @PreAuthorize("@ss.hasPermi('notification:edit')")
+    @Log(title = "通知管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/{id}/read")
+    public AjaxResult markAsRead(@PathVariable Long id) {
+        Long receiverId = SecurityUtils.getUserId();
+        return toAjax(notificationService.markAsRead(id, receiverId));
+    }
+    
+    /**
+     * 批量标记为已读
+     */
+    @PreAuthorize("@ss.hasPermi('notification:edit')")
+    @Log(title = "通知管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/batch-read")
+    public AjaxResult markBatchAsRead(@RequestBody Long[] ids) {
+        Long receiverId = SecurityUtils.getUserId();
+        return toAjax(notificationService.markBatchAsRead(ids, receiverId));
+    }
+    
+    /**
+     * 标记全部为已读
+     */
+    @PreAuthorize("@ss.hasPermi('notification:edit')")
+    @Log(title = "通知管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/all-read")
+    public AjaxResult markAllAsRead() {
+        Long receiverId = SecurityUtils.getUserId();
+        return toAjax(notificationService.markAllAsRead(receiverId));
+    }
+    
+    /**
+     * 删除通知
+     */
+    @PreAuthorize("@ss.hasPermi('notification:remove')")
+    @Log(title = "通知管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{id}")
+    public AjaxResult remove(@PathVariable Long id) {
+        Long receiverId = SecurityUtils.getUserId();
+        return toAjax(notificationService.deleteNotification(id, receiverId));
+    }
+    
+    /**
+     * 批量删除通知
+     */
+    @PreAuthorize("@ss.hasPermi('notification:remove')")
+    @Log(title = "通知管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/batch-delete")
+    public AjaxResult batchRemove(@RequestBody Long[] ids) {
+        Long receiverId = SecurityUtils.getUserId();
+        return toAjax(notificationService.deleteBatchNotification(ids, receiverId));
+    }
+    
+    // ========== 模板管理接口 ==========
+    
+    /**
+     * 查询通知模板列表
+     */
+    @PreAuthorize("@ss.hasPermi('notification:template:list')")
+    @GetMapping("/template/list")
+    public TableDataInfo templateList(NotificationTemplate template) {
+        startPage();
+        List<NotificationTemplate> list = notificationService.selectTemplateList(template);
+        return getDataTable(list);
+    }
+    
+    /**
+     * 根据ID获取模板详情
+     */
+    @PreAuthorize("@ss.hasPermi('notification:template:query')")
+    @GetMapping(value = "/template/{id}")
+    public AjaxResult getTemplateInfo(@PathVariable Long id) {
+        return success(notificationService.selectTemplateById(id));
+    }
+    
+    /**
+     * 新增通知模板
+     */
+    @PreAuthorize("@ss.hasPermi('notification:template:add')")
+    @Log(title = "通知模板", businessType = BusinessType.INSERT)
+    @PostMapping("/template")
+    public AjaxResult addTemplate(@Validated @RequestBody NotificationTemplate template) {
+        template.setCreateBy(getUsername());
+        return toAjax(notificationService.insertTemplate(template));
+    }
+    
+    /**
+     * 修改通知模板
+     */
+    @PreAuthorize("@ss.hasPermi('notification:template:edit')")
+    @Log(title = "通知模板", businessType = BusinessType.UPDATE)
+    @PutMapping("/template")
+    public AjaxResult editTemplate(@Validated @RequestBody NotificationTemplate template) {
+        template.setUpdateBy(getUsername());
+        return toAjax(notificationService.updateTemplate(template));
+    }
+    
+    /**
+     * 删除通知模板
+     */
+    @PreAuthorize("@ss.hasPermi('notification:template:remove')")
+    @Log(title = "通知模板", businessType = BusinessType.DELETE)
+    @DeleteMapping("/template/{ids}")
+    public AjaxResult removeTemplate(@PathVariable Long[] ids) {
+        return toAjax(notificationService.deleteTemplateByIds(ids));
+    }
+}

+ 106 - 0
aegis-admin/src/main/java/com/aegis/web/controller/resource/dashboard/ResourceDashboardController.java

@@ -0,0 +1,106 @@
+package com.aegis.web.controller.resource.dashboard;
+
+import java.util.List;
+import java.util.Map;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+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.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.resource.dashboard.service.IResourceDashboardService;
+import com.aegis.resource.dashboard.vo.InventoryAlertVO;
+import com.aegis.resource.dashboard.vo.ResourceStatsVO;
+import com.aegis.resource.dashboard.vo.UsageTrendVO;
+
+/**
+ * 资源看板Controller
+ * 
+ * @author aegis
+ */
+@RestController
+@RequestMapping("/resources/dashboard")
+public class ResourceDashboardController extends BaseController {
+    
+    @Autowired
+    private IResourceDashboardService dashboardService;
+    
+    /**
+     * 获取资源总览统计
+     */
+    @PreAuthorize("@ss.hasPermi('resource:dashboard:view')")
+    @GetMapping("/stats")
+    public AjaxResult getResourceStats() {
+        ResourceStatsVO stats = dashboardService.getResourceStats();
+        return success(stats);
+    }
+    
+    /**
+     * 获取库存预警列表
+     */
+    @PreAuthorize("@ss.hasPermi('resource:dashboard:view')")
+    @GetMapping("/inventory-alerts")
+    public AjaxResult getInventoryAlerts() {
+        List<InventoryAlertVO> alerts = dashboardService.getInventoryAlerts();
+        Map<String, Object> result = new java.util.HashMap<>();
+        result.put("total", alerts != null ? alerts.size() : 0);
+        result.put("list", alerts != null ? alerts : new java.util.ArrayList<>());
+        return success(result);
+    }
+    
+    /**
+     * 获取采购状态统计
+     */
+    @PreAuthorize("@ss.hasPermi('resource:dashboard:view')")
+    @GetMapping("/procurement-status")
+    public AjaxResult getProcurementStatus() {
+        Map<String, Integer> status = dashboardService.getProcurementStatus();
+        return success(status);
+    }
+    
+    /**
+     * 获取征用状态统计
+     */
+    @PreAuthorize("@ss.hasPermi('resource:dashboard:view')")
+    @GetMapping("/requisition-status")
+    public AjaxResult getRequisitionStatus() {
+        Map<String, Integer> status = dashboardService.getRequisitionStatus();
+        return success(status);
+    }
+    
+    /**
+     * 获取资源状态分布
+     */
+    @PreAuthorize("@ss.hasPermi('resource:dashboard:view')")
+    @GetMapping("/resource-distribution")
+    public AjaxResult getResourceDistribution() {
+        Map<String, Map<String, Double>> distribution = dashboardService.getResourceDistribution();
+        return success(distribution);
+    }
+    
+    /**
+     * 获取资源使用趋势
+     */
+    @PreAuthorize("@ss.hasPermi('resource:dashboard:view')")
+    @GetMapping("/usage-trends")
+    public AjaxResult getUsageTrends(@RequestParam(value = "days", required = false, defaultValue = "7") Integer days) {
+        UsageTrendVO trends = dashboardService.getUsageTrends(days);
+        return success(trends);
+    }
+    
+    /**
+     * 手动刷新看板缓存
+     */
+    @PreAuthorize("@ss.hasPermi('resource:dashboard:refresh')")
+    @Log(title = "资源看板", businessType = BusinessType.CLEAN)
+    @PostMapping("/refresh")
+    public AjaxResult refreshDashboard() {
+        dashboardService.refreshDashboardCache();
+        return success("看板缓存刷新成功");
+    }
+}

+ 176 - 0
aegis-admin/src/main/java/com/aegis/web/controller/resource/equipment/ResourceEquipmentController.java

@@ -0,0 +1,176 @@
+package com.aegis.web.controller.resource.equipment;
+
+import java.util.List;
+import java.util.Map;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+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 com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.page.TableDataInfo;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.common.utils.StringUtils;
+import com.aegis.resource.equipment.assembler.EquipmentAssembler;
+import com.aegis.resource.equipment.domain.Equipment;
+import com.aegis.resource.equipment.domain.EquipmentLog;
+import com.aegis.resource.equipment.dto.EquipmentDTO;
+import com.aegis.resource.equipment.service.IResourceEquipmentService;
+
+/**
+ * 装备Controller
+ * 
+ * @author aegis
+ */
+@RestController
+@RequestMapping("/resources/equipment")
+public class ResourceEquipmentController extends BaseController {
+
+    @Autowired
+    private IResourceEquipmentService equipmentService;
+
+    /**
+     * 查询装备列表
+     */
+    @PreAuthorize("@ss.hasPermi('resource:equipment:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(Equipment equipment) {
+        startPage();
+        List<Equipment> list = equipmentService.selectEquipmentList(equipment);
+        return getDataTable(list);
+    }
+
+    /**
+     * 根据ID获取装备详情
+     */
+    @PreAuthorize("@ss.hasPermi('resource:equipment:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        return success(equipmentService.selectEquipmentById(id));
+    }
+
+    /**
+     * 根据队伍ID查询装备列表
+     */
+    @PreAuthorize("@ss.hasPermi('resource:equipment:list')")
+    @GetMapping("/team/{teamId}")
+    public AjaxResult listByTeam(@PathVariable Long teamId) {
+        List<Equipment> list = equipmentService.selectEquipmentListByTeamId(teamId);
+        return success(list);
+    }
+
+    /**
+     * 新增装备
+     */
+    @PreAuthorize("@ss.hasPermi('resource:equipment:add')")
+    @Log(title = "装备管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody EquipmentDTO equipmentDTO) {
+        Equipment equipment = EquipmentAssembler.toDomain(equipmentDTO);
+        if (StringUtils.isNotEmpty(equipment.getEquipmentCode()) && !equipmentService.checkEquipmentCodeUnique(equipment)) {
+            return error("新增装备'" + equipment.getEquipmentName() + "'失败,装备编码已存在");
+        }
+        return toAjax(equipmentService.insertEquipment(equipment));
+    }
+
+    /**
+     * 修改装备
+     */
+    @PreAuthorize("@ss.hasPermi('resource:equipment:edit')")
+    @Log(title = "装备管理", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody EquipmentDTO equipmentDTO) {
+        Equipment equipment = EquipmentAssembler.toDomain(equipmentDTO);
+        if (StringUtils.isNotEmpty(equipment.getEquipmentCode()) && !equipmentService.checkEquipmentCodeUnique(equipment)) {
+            return error("修改装备'" + equipment.getEquipmentName() + "'失败,装备编码已存在");
+        }
+        return toAjax(equipmentService.updateEquipment(equipment));
+    }
+
+    /**
+     * 删除装备
+     */
+    @PreAuthorize("@ss.hasPermi('resource:equipment:remove')")
+    @Log(title = "装备管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        return toAjax(equipmentService.deleteEquipmentByIds(ids));
+    }
+
+    /**
+     * 分配占用(available → busy)
+     */
+    @PreAuthorize("@ss.hasPermi('resource:equipment:assign')")
+    @Log(title = "装备管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/assign")
+    public AjaxResult assign(@PathVariable Long id, @RequestBody Map<String, Object> params) {
+        Long dispatchId = params.get("dispatchId") != null ? Long.valueOf(params.get("dispatchId").toString()) : null;
+        Long eventId = params.get("eventId") != null ? Long.valueOf(params.get("eventId").toString()) : null;
+        String remark = StringUtils.isNotNull(params.get("remark")) ? params.get("remark").toString() : null;
+        return toAjax(equipmentService.assignEquipment(id, dispatchId, eventId, remark));
+    }
+
+    /**
+     * 解除占用(busy → available)
+     */
+    @PreAuthorize("@ss.hasPermi('resource:equipment:release')")
+    @Log(title = "装备管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/release")
+    public AjaxResult release(@PathVariable Long id, @RequestBody Map<String, Object> params) {
+        String remark = StringUtils.isNotNull(params.get("remark")) ? params.get("remark").toString() : null;
+        return toAjax(equipmentService.releaseEquipment(id, remark));
+    }
+
+    /**
+     * 送修(available/busy → repair)
+     */
+    @PreAuthorize("@ss.hasPermi('resource:equipment:edit')")
+    @Log(title = "装备管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/send-repair")
+    public AjaxResult sendRepair(@PathVariable Long id, @RequestBody Map<String, Object> params) {
+        String remark = StringUtils.isNotNull(params.get("remark")) ? params.get("remark").toString() : null;
+        return toAjax(equipmentService.sendRepair(id, remark));
+    }
+
+    /**
+     * 修复完成(repair → available)
+     */
+    @PreAuthorize("@ss.hasPermi('resource:equipment:edit')")
+    @Log(title = "装备管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/finish-repair")
+    public AjaxResult finishRepair(@PathVariable Long id, @RequestBody Map<String, Object> params) {
+        String remark = StringUtils.isNotNull(params.get("remark")) ? params.get("remark").toString() : null;
+        return toAjax(equipmentService.finishRepair(id, remark));
+    }
+
+    /**
+     * 报废(→ scrap,终态)
+     */
+    @PreAuthorize("@ss.hasPermi('resource:equipment:edit')")
+    @Log(title = "装备管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/scrap")
+    public AjaxResult scrap(@PathVariable Long id, @RequestBody Map<String, Object> params) {
+        String remark = StringUtils.isNotNull(params.get("remark")) ? params.get("remark").toString() : null;
+        return toAjax(equipmentService.scrapEquipment(id, remark));
+    }
+
+    /**
+     * 查询操作日志
+     */
+    @PreAuthorize("@ss.hasPermi('resource:equipment:query')")
+    @GetMapping("/{id}/logs")
+    public TableDataInfo listLogs(@PathVariable Long id, EquipmentLog log) {
+        log.setEquipmentId(id);
+        startPage();
+        List<EquipmentLog> list = equipmentService.selectEquipmentLogList(log);
+        return getDataTable(list);
+    }
+}

+ 164 - 0
aegis-admin/src/main/java/com/aegis/web/controller/resource/expert/ResourceExpertController.java

@@ -0,0 +1,164 @@
+package com.aegis.web.controller.resource.expert;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+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 com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.page.TableDataInfo;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.resource.expert.domain.Expert;
+import com.aegis.resource.expert.service.IResourceExpertService;
+import com.aegis.system.service.ISysDictTypeService;
+import com.aegis.common.core.domain.entity.SysDictData;
+
+/**
+ * 专家Controller
+ * 
+ * @author aegis
+ */
+@RestController
+@RequestMapping("/resources/experts")
+public class ResourceExpertController extends BaseController {
+
+    @Autowired
+    private IResourceExpertService expertService;
+
+    @Autowired
+    private ISysDictTypeService dictTypeService;
+
+    /**
+     * 查询专家列表
+     */
+    @PreAuthorize("@ss.hasPermi('resource:expert:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(Expert expert) {
+        startPage();
+        List<Expert> list = expertService.listByCondition(expert);
+        return getDataTable(list);
+    }
+
+    /**
+     * 根据ID获取专家详情
+     */
+    @PreAuthorize("@ss.hasPermi('resource:expert:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        return success(expertService.getById(id));
+    }
+
+    /**
+     * 根据专长领域查询专家列表
+     */
+    @PreAuthorize("@ss.hasPermi('resource:expert:list')")
+    @GetMapping("/specialty/{specialty}")
+    public AjaxResult listBySpecialty(@PathVariable String specialty) {
+        List<Expert> list = expertService.listExpertsBySpecialty(specialty);
+        return success(list);
+    }
+
+    /**
+     * 获取专家专长字典列表
+     */
+    @GetMapping("/specialty/dict")
+    public AjaxResult getSpecialtyDict() {
+        List<SysDictData> dictList = dictTypeService.selectDictDataByType("expert_specialty");
+        return success(dictList);
+    }
+
+    /**
+     * 新增专家
+     */
+    @PreAuthorize("@ss.hasPermi('resource:expert:add')")
+    @Log(title = "专家管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody Expert expert) {
+        return toAjax(expertService.save(expert));
+    }
+
+    /**
+     * 修改专家
+     */
+    @PreAuthorize("@ss.hasPermi('resource:expert:edit')")
+    @Log(title = "专家管理", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody Expert expert) {
+        return toAjax(expertService.updateById(expert));
+    }
+
+    /**
+     * 删除专家
+     */
+    @PreAuthorize("@ss.hasPermi('resource:expert:remove')")
+    @Log(title = "专家管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        return toAjax(expertService.removeByIds(Arrays.asList(ids)));
+    }
+
+    /**
+     * 启用专家
+     */
+    @PreAuthorize("@ss.hasPermi('resource:expert:activate')")
+    @Log(title = "专家管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/activate")
+    public AjaxResult activate(@PathVariable Long id) {
+        return toAjax(expertService.activateExpert(id));
+    }
+
+    /**
+     * 停用专家
+     */
+    @PreAuthorize("@ss.hasPermi('resource:expert:deactivate')")
+    @Log(title = "专家管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/deactivate")
+    public AjaxResult deactivate(@PathVariable Long id) {
+        return toAjax(expertService.deactivateExpert(id));
+    }
+
+    /**
+     * 加入黑名单
+     */
+    @PreAuthorize("@ss.hasPermi('resource:expert:blacklist')")
+    @Log(title = "专家管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/blacklist")
+    public AjaxResult blacklist(@PathVariable Long id) {
+        return toAjax(expertService.blacklistExpert(id));
+    }
+
+    /**
+     * 解除黑名单
+     */
+    @PreAuthorize("@ss.hasPermi('resource:expert:blacklist')")
+    @Log(title = "专家管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/unblacklist")
+    public AjaxResult unblacklist(@PathVariable Long id) {
+        return toAjax(expertService.unblacklistExpert(id));
+    }
+
+    /**
+     * 评分(1-5)
+     */
+    @PreAuthorize("@ss.hasPermi('resource:expert:rate')")
+    @Log(title = "专家管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/rate")
+    public AjaxResult rate(@PathVariable Long id, @RequestBody Map<String, Object> params) {
+        Integer rating = params.get("rating") != null ? Integer.valueOf(params.get("rating").toString()) : null;
+        if (rating == null || rating < 1 || rating > 5) {
+            return error("评分必须在1-5之间");
+        }
+        return toAjax(expertService.rateExpert(id, rating));
+    }
+}

+ 108 - 0
aegis-admin/src/main/java/com/aegis/web/controller/resource/material/ResourceMaterialCategoryController.java

@@ -0,0 +1,108 @@
+package com.aegis.web.controller.resource.material;
+
+import java.util.List;
+import org.apache.commons.lang3.ArrayUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+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 com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.common.utils.StringUtils;
+import com.aegis.resource.material.domain.MaterialCategory;
+import com.aegis.resource.material.service.IResourceMaterialCategoryService;
+
+/**
+ * 物资分类Controller
+ * 
+ * @author aegis
+ */
+@RestController
+@RequestMapping("/resources/material-categories")
+public class ResourceMaterialCategoryController extends BaseController {
+
+    @Autowired
+    private IResourceMaterialCategoryService categoryService;
+
+    /**
+     * 查询物资分类列表
+     */
+    @PreAuthorize("@ss.hasPermi('resource:material:category:list')")
+    @GetMapping("/list")
+    public AjaxResult list(MaterialCategory category) {
+        List<MaterialCategory> list = categoryService.listByCondition(category);
+        return success(categoryService.buildCategoryTree(list));
+    }
+
+    /**
+     * 获取物资分类下拉树列表
+     */
+    @GetMapping("/treeselect")
+    public AjaxResult treeselect(MaterialCategory category) {
+        List<MaterialCategory> categories = categoryService.listByCondition(category);
+        return success(categoryService.buildCategoryTreeSelect(categories));
+    }
+
+    /**
+     * 查询物资分类列表(排除节点)
+     */
+    @PreAuthorize("@ss.hasPermi('resource:material:category:list')")
+    @GetMapping("/list/exclude/{id}")
+    public AjaxResult excludeChild(@PathVariable(value = "id", required = false) Long id) {
+        List<MaterialCategory> categories = categoryService.listByCondition(new MaterialCategory());
+        categories.removeIf(c -> c.getId().intValue() == id || ArrayUtils.contains(StringUtils.split(c.getAncestors(), ","), id + ""));
+        return success(categoryService.buildCategoryTree(categories));
+    }
+
+    /**
+     * 根据ID获取物资分类详情
+     */
+    @PreAuthorize("@ss.hasPermi('resource:material:category:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        return success(categoryService.getById(id));
+    }
+
+    /**
+     * 新增物资分类
+     */
+    @PreAuthorize("@ss.hasPermi('resource:material:category:add')")
+    @Log(title = "物资分类管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody MaterialCategory category) {
+        return toAjax(categoryService.save(category));
+    }
+
+    /**
+     * 修改物资分类
+     */
+    @PreAuthorize("@ss.hasPermi('resource:material:category:edit')")
+    @Log(title = "物资分类管理", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody MaterialCategory category) {
+        Long categoryId = category.getId();
+        if (category.getParentId() != null && category.getParentId().equals(categoryId)) {
+            return error("修改分类'" + category.getCategoryName() + "'失败,上级分类不能是自己");
+        }
+        return toAjax(categoryService.updateById(category));
+    }
+
+    /**
+     * 删除物资分类
+     */
+    @PreAuthorize("@ss.hasPermi('resource:material:category:remove')")
+    @Log(title = "物资分类管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{id}")
+    public AjaxResult remove(@PathVariable Long id) {
+        return toAjax(categoryService.removeById(id));
+    }
+}

+ 230 - 0
aegis-admin/src/main/java/com/aegis/web/controller/resource/material/ResourceMaterialController.java

@@ -0,0 +1,230 @@
+package com.aegis.web.controller.resource.material;
+
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+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 com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.page.TableDataInfo;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.common.utils.StringUtils;
+import com.aegis.resource.material.domain.Material;
+import com.aegis.resource.material.domain.MaterialInventory;
+import com.aegis.resource.material.dto.InventoryAction;
+import com.aegis.resource.material.dto.InventoryAdjustment;
+import com.aegis.resource.material.domain.MaterialUsage;
+import com.aegis.resource.material.service.IResourceMaterialService;
+import com.aegis.resource.material.service.IResourceMaterialInventoryService;
+import com.aegis.resource.material.service.IResourceMaterialUsageService;
+
+/**
+ * 物资管理Controller
+ * 
+ * @author aegis
+ */
+@RestController
+@RequestMapping("/resources/materials")
+public class ResourceMaterialController extends BaseController {
+
+    @Autowired
+    private IResourceMaterialService materialService;
+
+    @Autowired
+    private IResourceMaterialInventoryService inventoryService;
+
+    @Autowired
+    private IResourceMaterialUsageService usageService;
+
+    /**
+     * 查询物资列表
+     */
+    @PreAuthorize("@ss.hasPermi('resource:material:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(Material material) {
+        startPage();
+        List<Material> list = materialService.listByCondition(material);
+        return getDataTable(list);
+    }
+
+    /**
+     * 根据ID获取物资详情
+     */
+    @PreAuthorize("@ss.hasPermi('resource:material:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        return success(materialService.getById(id));
+    }
+
+    /**
+     * 新增物资
+     */
+    @PreAuthorize("@ss.hasPermi('resource:material:add')")
+    @Log(title = "物资管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody Material material) {
+        return toAjax(materialService.save(material));
+    }
+
+    /**
+     * 修改物资
+     */
+    @PreAuthorize("@ss.hasPermi('resource:material:edit')")
+    @Log(title = "物资管理", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody Material material) {
+        return toAjax(materialService.updateById(material));
+    }
+
+    /**
+     * 删除物资
+     */
+    @PreAuthorize("@ss.hasPermi('resource:material:remove')")
+    @Log(title = "物资管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        boolean removed = materialService.removeByIds(Arrays.asList(ids));
+        return removed ? success() : error("删除失败");
+    }
+
+    /**
+     * 设置报警阈值
+     */
+    @PreAuthorize("@ss.hasPermi('resource:material:edit')")
+    @Log(title = "物资管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/alert")
+    public AjaxResult setAlert(@PathVariable Long id, @RequestBody Material material) {
+        return toAjax(materialService.updateMaterialThreshold(id, material.getThreshold()));
+    }
+
+    /**
+     * 查询库存(支持按仓库查询)
+     */
+    @PreAuthorize("@ss.hasPermi('resource:material:inventory:list')")
+    @GetMapping("/{id}/inventory")
+    public TableDataInfo listInventory(@PathVariable Long id, MaterialInventory inventory) {
+        inventory.setMaterialId(id);
+        startPage();
+        List<MaterialInventory> list = inventoryService.listByCondition(inventory);
+        return getDataTable(list);
+    }
+
+    /**
+     * 根据库存ID获取库存详情
+     */
+    @PreAuthorize("@ss.hasPermi('resource:material:inventory:query')")
+    @GetMapping("/inventory/{inventoryId}")
+    public AjaxResult getInventoryInfo(@PathVariable Long inventoryId) {
+        return success(inventoryService.getById(inventoryId));
+    }
+
+    /**
+     * 添加库存(指定仓库和数量)
+     */
+    @PreAuthorize("@ss.hasPermi('resource:material:inventory:add')")
+    @Log(title = "物资库存", businessType = BusinessType.INSERT)
+    @PostMapping("/{id}/inventory")
+    public AjaxResult addInventory(@PathVariable Long id, @Validated @RequestBody MaterialInventory inventory) {
+        inventory.setMaterialId(id);
+        return toAjax(inventoryService.addInventory(inventory));
+    }
+
+    /**
+     * 占用库存(需指定仓库ID)
+     */
+    @PreAuthorize("@ss.hasPermi('resource:material:inventory:lock')")
+    @Log(title = "物资库存", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/lock")
+    public AjaxResult lockInventory(@PathVariable Long id, @RequestBody Map<String, Object> params) {
+        InventoryAction action = buildInventoryAction(id, params);
+        return toAjax(inventoryService.lockInventory(action));
+    }
+
+    /**
+     * 释放库存(需指定仓库ID)
+     */
+    @PreAuthorize("@ss.hasPermi('resource:material:inventory:unlock')")
+    @Log(title = "物资库存", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/unlock")
+    public AjaxResult unlockInventory(@PathVariable Long id, @RequestBody Map<String, Object> params) {
+        InventoryAction action = buildInventoryAction(id, params);
+        return toAjax(inventoryService.unlockInventory(action));
+    }
+
+    /**
+     * 消耗结算(一次性物资,需指定仓库ID)
+     */
+    @PreAuthorize("@ss.hasPermi('resource:material:inventory:consume')")
+    @Log(title = "物资库存", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/consume")
+    public AjaxResult consumeInventory(@PathVariable Long id, @RequestBody Map<String, Object> params) {
+        InventoryAction action = buildInventoryAction(id, params);
+        return toAjax(inventoryService.consumeInventory(action));
+    }
+
+    /**
+     * 归还入库(可重复使用物资,需指定仓库ID)
+     */
+    @PreAuthorize("@ss.hasPermi('resource:material:inventory:return')")
+    @Log(title = "物资库存", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/return")
+    public AjaxResult returnInventory(@PathVariable Long id, @RequestBody Map<String, Object> params) {
+        InventoryAction action = buildInventoryAction(id, params);
+        return toAjax(inventoryService.returnInventory(action));
+    }
+
+    /**
+     * 库存调整(用于盘点、损失、发现等非业务流操作)
+     */
+    @PreAuthorize("@ss.hasPermi('resource:material:inventory:adjust')")
+    @Log(title = "物资库存", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/adjust")
+    public AjaxResult adjustInventory(@PathVariable Long id, @Validated @RequestBody InventoryAdjustment adjustment) {
+        adjustment.setMaterialId(id);
+        return toAjax(inventoryService.adjustInventory(adjustment));
+    }
+
+    /**
+     * 查询使用记录(支持按仓库筛选)
+     */
+    @PreAuthorize("@ss.hasPermi('resource:material:usage:list')")
+    @GetMapping("/{id}/usages")
+    public TableDataInfo listUsages(@PathVariable Long id, MaterialUsage usage) {
+        usage.setMaterialId(id);
+        startPage();
+        List<MaterialUsage> list = usageService.listByCondition(usage);
+        return getDataTable(list);
+    }
+
+    /**
+     * 根据ID获取使用记录详情
+     */
+    @PreAuthorize("@ss.hasPermi('resource:material:usage:query')")
+    @GetMapping("/usages/{usageId}")
+    public AjaxResult getUsageInfo(@PathVariable Long usageId) {
+        return success(usageService.getById(usageId));
+    }
+
+    private InventoryAction buildInventoryAction(Long materialId, Map<String, Object> params) {
+        InventoryAction action = new InventoryAction();
+        action.setMaterialId(materialId);
+        action.setWarehouseId(params.get("warehouseId") != null ? Long.valueOf(params.get("warehouseId").toString()) : null);
+        action.setQty(params.get("qty") != null ? new BigDecimal(params.get("qty").toString()) : null);
+        action.setEventId(params.get("eventId") != null ? Long.valueOf(params.get("eventId").toString()) : null);
+        action.setTaskId(params.get("taskId") != null ? Long.valueOf(params.get("taskId").toString()) : null);
+        action.setRemark(StringUtils.isNotNull(params.get("remark")) ? params.get("remark").toString() : null);
+        return action;
+    }
+}

+ 262 - 0
aegis-admin/src/main/java/com/aegis/web/controller/resource/procurement/ResourceProcurementController.java

@@ -0,0 +1,262 @@
+package com.aegis.web.controller.resource.procurement;
+
+import java.util.List;
+import java.util.Map;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+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 com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.page.TableDataInfo;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.common.utils.StringUtils;
+import com.aegis.resource.procurement.assembler.ProcurementAssembler;
+import com.aegis.resource.procurement.assembler.ProcurementItemAssembler;
+import com.aegis.resource.procurement.assembler.ProcurementQuoteAssembler;
+import com.aegis.resource.procurement.domain.Procurement;
+import com.aegis.resource.procurement.domain.ProcurementItem;
+import com.aegis.resource.procurement.domain.ProcurementQuote;
+import com.aegis.resource.procurement.dto.ProcurementDTO;
+import com.aegis.resource.procurement.dto.ProcurementItemDTO;
+import com.aegis.resource.procurement.dto.ProcurementQuoteDTO;
+import com.aegis.resource.procurement.service.IResourceProcurementService;
+
+/**
+ * 采购申请Controller
+ * 
+ * @author aegis
+ */
+@RestController
+@RequestMapping("/resources/procurement")
+public class ResourceProcurementController extends BaseController {
+
+    @Autowired
+    private IResourceProcurementService procurementService;
+
+    /**
+     * 查询采购申请列表
+     */
+    @PreAuthorize("@ss.hasPermi('resource:procurement:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(Procurement procurement) {
+        startPage();
+        List<Procurement> list = procurementService.listProcurements(procurement);
+        return getDataTable(list);
+    }
+
+    /**
+     * 根据ID获取采购申请详情
+     */
+    @PreAuthorize("@ss.hasPermi('resource:procurement:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        Procurement procurement = procurementService.getProcurementById(id);
+        return success(ProcurementAssembler.toDTO(procurement));
+    }
+
+    /**
+     * 新增采购申请
+     */
+    @PreAuthorize("@ss.hasPermi('resource:procurement:add')")
+    @Log(title = "采购管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody ProcurementDTO dto) {
+        if (!procurementService.isProcurementNoUnique(dto.getProcurementNo(), null)) {
+            return error("新增采购申请'" + dto.getTitle() + "'失败,采购编号已存在");
+        }
+        Procurement procurement = ProcurementAssembler.toDomain(dto);
+        return toAjax(procurementService.createProcurement(procurement));
+    }
+
+    /**
+     * 修改采购申请
+     */
+    @PreAuthorize("@ss.hasPermi('resource:procurement:edit')")
+    @Log(title = "采购管理", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody ProcurementDTO dto) {
+        if (!procurementService.isProcurementNoUnique(dto.getProcurementNo(), dto.getId())) {
+            return error("修改采购申请'" + dto.getTitle() + "'失败,采购编号已存在");
+        }
+        Procurement procurement = ProcurementAssembler.toDomain(dto);
+        return toAjax(procurementService.modifyProcurement(procurement));
+    }
+
+    /**
+     * 删除采购申请
+     */
+    @PreAuthorize("@ss.hasPermi('resource:procurement:remove')")
+    @Log(title = "采购管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        return toAjax(procurementService.removeProcurements(ids));
+    }
+
+    /**
+     * 提交采购申请(draft → submitted)
+     */
+    @PreAuthorize("@ss.hasPermi('resource:procurement:edit')")
+    @Log(title = "采购管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/submit")
+    public AjaxResult submit(@PathVariable Long id) {
+        return toAjax(procurementService.submitProcurement(id));
+    }
+
+    /**
+     * 审核通过(submitted → approved)
+     */
+    @PreAuthorize("@ss.hasPermi('resource:procurement:approve')")
+    @Log(title = "采购管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/approve")
+    public AjaxResult approve(@PathVariable Long id, @RequestBody Map<String, Object> params) {
+        String comment = StringUtils.isNotNull(params.get("comment")) ? params.get("comment").toString() : null;
+        return toAjax(procurementService.approveProcurement(id, comment));
+    }
+
+    /**
+     * 审核驳回(submitted → rejected)
+     */
+    @PreAuthorize("@ss.hasPermi('resource:procurement:approve')")
+    @Log(title = "采购管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/reject")
+    public AjaxResult reject(@PathVariable Long id, @RequestBody Map<String, Object> params) {
+        String comment = StringUtils.isNotNull(params.get("comment")) ? params.get("comment").toString() : null;
+        return toAjax(procurementService.rejectProcurement(id, comment));
+    }
+
+    /**
+     * 重新提交(rejected → submitted)
+     * 驳回后修改完成,直接提交审核
+     */
+    @PreAuthorize("@ss.hasPermi('resource:procurement:edit')")
+    @Log(title = "采购管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/resubmit")
+    public AjaxResult resubmit(@PathVariable Long id) {
+        return toAjax(procurementService.resubmitProcurement(id));
+    }
+
+    /**
+     * 确认中标供应商(approved → in_progress)
+     */
+    @PreAuthorize("@ss.hasPermi('resource:procurement:edit')")
+    @Log(title = "采购管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/award")
+    public AjaxResult award(@PathVariable Long id, @RequestBody Map<String, Object> params) {
+        Long quoteId = params.get("quoteId") != null ? Long.valueOf(params.get("quoteId").toString()) : null;
+        if (quoteId == null) {
+            return error("报价ID不能为空");
+        }
+        return toAjax(procurementService.awardProcurement(id, quoteId));
+    }
+
+    /**
+     * 采购入库(in_progress → received)
+     */
+    @PreAuthorize("@ss.hasPermi('resource:procurement:edit')")
+    @Log(title = "采购管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/receive")
+    public AjaxResult receive(@PathVariable Long id, @RequestBody Map<String, Object> params) {
+        Long warehouseId = params.get("warehouseId") != null ? Long.valueOf(params.get("warehouseId").toString()) : null;
+        return toAjax(procurementService.receiveProcurement(id, warehouseId));
+    }
+
+    /**
+     * 查询采购明细列表
+     */
+    @PreAuthorize("@ss.hasPermi('resource:procurement:query')")
+    @GetMapping("/{id}/items")
+    public TableDataInfo listItems(@PathVariable Long id, ProcurementItem item) {
+        item.setProcurementId(id);
+        startPage();
+        List<ProcurementItem> list = procurementService.listProcurementItems(item);
+        return getDataTable(list);
+    }
+
+    /**
+     * 根据ID获取采购明细详情
+     */
+    @PreAuthorize("@ss.hasPermi('resource:procurement:query')")
+    @GetMapping("/items/{itemId}")
+    public AjaxResult getItemInfo(@PathVariable Long itemId) {
+        ProcurementItem item = procurementService.getProcurementItemById(itemId);
+        return success(ProcurementItemAssembler.toDTO(item));
+    }
+
+    /**
+     * 新增采购明细
+     */
+    @PreAuthorize("@ss.hasPermi('resource:procurement:edit')")
+    @Log(title = "采购明细管理", businessType = BusinessType.INSERT)
+    @PostMapping("/{id}/items")
+    public AjaxResult addItem(@PathVariable Long id, @Validated @RequestBody ProcurementItemDTO dto) {
+        dto.setProcurementId(id);
+        ProcurementItem item = ProcurementItemAssembler.toDomain(dto);
+        return toAjax(procurementService.createProcurementItem(item));
+    }
+
+    /**
+     * 修改采购明细
+     */
+    @PreAuthorize("@ss.hasPermi('resource:procurement:edit')")
+    @Log(title = "采购明细管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/{id}/items/{itemId}")
+    public AjaxResult editItem(@PathVariable Long id, @PathVariable Long itemId, @Validated @RequestBody ProcurementItemDTO dto) {
+        dto.setId(itemId);
+        dto.setProcurementId(id);
+        ProcurementItem item = ProcurementItemAssembler.toDomain(dto);
+        return toAjax(procurementService.modifyProcurementItem(item));
+    }
+
+    /**
+     * 删除采购明细
+     */
+    @PreAuthorize("@ss.hasPermi('resource:procurement:edit')")
+    @Log(title = "采购明细管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{id}/items/{itemId}")
+    public AjaxResult removeItem(@PathVariable Long id, @PathVariable Long itemId) {
+        return toAjax(procurementService.removeProcurementItem(itemId));
+    }
+
+    /**
+     * 供应商报价
+     */
+    @PreAuthorize("@ss.hasPermi('resource:procurement:quote')")
+    @Log(title = "采购报价管理", businessType = BusinessType.INSERT)
+    @PostMapping("/{id}/quote")
+    public AjaxResult submitQuote(@PathVariable Long id, @Validated @RequestBody ProcurementQuoteDTO dto) {
+        dto.setProcurementId(id);
+        ProcurementQuote quote = ProcurementQuoteAssembler.toDomain(dto);
+        return toAjax(procurementService.submitQuote(quote));
+    }
+
+    /**
+     * 查询报价记录(多供应商比价)
+     */
+    @PreAuthorize("@ss.hasPermi('resource:procurement:query')")
+    @GetMapping("/{id}/quotes")
+    public TableDataInfo listQuotes(@PathVariable Long id, ProcurementQuote quote) {
+        quote.setProcurementId(id);
+        startPage();
+        List<ProcurementQuote> list = procurementService.listProcurementQuotes(quote);
+        return getDataTable(list);
+    }
+
+    /**
+     * 根据ID获取报价详情
+     */
+    @PreAuthorize("@ss.hasPermi('resource:procurement:query')")
+    @GetMapping("/quotes/{quoteId}")
+    public AjaxResult getQuoteInfo(@PathVariable Long quoteId) {
+        ProcurementQuote quote = procurementService.getProcurementQuoteById(quoteId);
+        return success(ProcurementQuoteAssembler.toDTO(quote));
+    }
+}

+ 307 - 0
aegis-admin/src/main/java/com/aegis/web/controller/resource/requisition/ResourceRequisitionController.java

@@ -0,0 +1,307 @@
+package com.aegis.web.controller.resource.requisition;
+
+import java.util.List;
+import java.util.Map;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+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 com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.page.TableDataInfo;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.common.utils.StringUtils;
+import com.aegis.resource.requisition.assembler.RequisitionAssembler;
+import com.aegis.resource.requisition.assembler.RequisitionAgreementAssembler;
+import com.aegis.resource.requisition.assembler.RequisitionCompensationAssembler;
+import com.aegis.resource.requisition.domain.Requisition;
+import com.aegis.resource.requisition.domain.RequisitionAgreement;
+import com.aegis.resource.requisition.domain.RequisitionCompensation;
+import com.aegis.resource.requisition.dto.RequisitionDTO;
+import com.aegis.resource.requisition.dto.RequisitionAgreementDTO;
+import com.aegis.resource.requisition.dto.RequisitionCompensationDTO;
+import com.aegis.resource.requisition.service.IResourceRequisitionService;
+
+/**
+ * 征用申请Controller
+ * 
+ * @author aegis
+ */
+@RestController
+@RequestMapping("/resources/requisitions")
+public class ResourceRequisitionController extends BaseController {
+
+    @Autowired
+    private IResourceRequisitionService requisitionService;
+
+    /**
+     * 查询征用申请列表
+     */
+    @PreAuthorize("@ss.hasPermi('resource:requisition:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(Requisition requisition) {
+        startPage();
+        List<Requisition> list = requisitionService.listRequisitions(requisition);
+        return getDataTable(list);
+    }
+
+    /**
+     * 根据队伍ID查询征用申请列表
+     */
+    @PreAuthorize("@ss.hasPermi('resource:requisition:list')")
+    @GetMapping("/team/{teamId}")
+    public AjaxResult listByTeam(@PathVariable Long teamId) {
+        List<Requisition> list = requisitionService.listRequisitionsByTeamId(teamId);
+        return success(list);
+    }
+
+    /**
+     * 根据ID获取征用申请详情
+     */
+    @PreAuthorize("@ss.hasPermi('resource:requisition:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        Requisition requisition = requisitionService.getRequisitionById(id);
+        return success(RequisitionAssembler.toDTO(requisition));
+    }
+
+    /**
+     * 新增征用申请
+     */
+    @PreAuthorize("@ss.hasPermi('resource:requisition:add')")
+    @Log(title = "征用管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody RequisitionDTO dto) {
+        if (!requisitionService.isRequisitionNoUnique(dto.getRequisitionNo(), null)) {
+            return error("新增征用申请失败,征用编号已存在");
+        }
+        Requisition requisition = RequisitionAssembler.toDomain(dto);
+        return toAjax(requisitionService.createRequisition(requisition));
+    }
+
+    /**
+     * 修改征用申请
+     */
+    @PreAuthorize("@ss.hasPermi('resource:requisition:edit')")
+    @Log(title = "征用管理", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody RequisitionDTO dto) {
+        if (!requisitionService.isRequisitionNoUnique(dto.getRequisitionNo(), dto.getId())) {
+            return error("修改征用申请失败,征用编号已存在");
+        }
+        Requisition requisition = RequisitionAssembler.toDomain(dto);
+        return toAjax(requisitionService.modifyRequisition(requisition));
+    }
+
+    /**
+     * 删除征用申请
+     */
+    @PreAuthorize("@ss.hasPermi('resource:requisition:remove')")
+    @Log(title = "征用管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        return toAjax(requisitionService.removeRequisitions(ids));
+    }
+
+    /**
+     * 审批通过(draft → approved)
+     */
+    @PreAuthorize("@ss.hasPermi('resource:requisition:approve')")
+    @Log(title = "征用管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/approve")
+    public AjaxResult approve(@PathVariable Long id, @RequestBody Map<String, Object> params) {
+        String comment = StringUtils.isNotNull(params.get("comment")) ? params.get("comment").toString() : null;
+        return toAjax(requisitionService.approveRequisition(id, comment));
+    }
+
+    /**
+     * 审批驳回(draft → rejected)
+     */
+    @PreAuthorize("@ss.hasPermi('resource:requisition:approve')")
+    @Log(title = "征用管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/reject")
+    public AjaxResult reject(@PathVariable Long id, @RequestBody Map<String, Object> params) {
+        String comment = StringUtils.isNotNull(params.get("comment")) ? params.get("comment").toString() : null;
+        return toAjax(requisitionService.rejectRequisition(id, comment));
+    }
+
+    /**
+     * 开始征用(approved → active,置队伍 busy)
+     */
+    @PreAuthorize("@ss.hasPermi('resource:requisition:edit')")
+    @Log(title = "征用管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/start")
+    public AjaxResult start(@PathVariable Long id) {
+        return toAjax(requisitionService.startRequisition(id));
+    }
+
+    /**
+     * 结束征用(active → ended,置队伍 available)
+     */
+    @PreAuthorize("@ss.hasPermi('resource:requisition:edit')")
+    @Log(title = "征用管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/end")
+    public AjaxResult end(@PathVariable Long id) {
+        return toAjax(requisitionService.endRequisition(id));
+    }
+
+    /**
+     * 重新提交(rejected → approved)
+     */
+    @PreAuthorize("@ss.hasPermi('resource:requisition:edit')")
+    @Log(title = "征用管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/resubmit")
+    public AjaxResult resubmit(@PathVariable Long id) {
+        return toAjax(requisitionService.resubmitRequisition(id));
+    }
+
+    /**
+     * 查询征用协议列表
+     */
+    @PreAuthorize("@ss.hasPermi('resource:requisition:query')")
+    @GetMapping("/{id}/agreements")
+    public TableDataInfo listAgreements(@PathVariable Long id, RequisitionAgreement agreement) {
+        agreement.setRequisitionId(id);
+        startPage();
+        List<RequisitionAgreement> list = requisitionService.listRequisitionAgreements(agreement);
+        return getDataTable(list);
+    }
+
+    /**
+     * 根据ID获取协议详情
+     */
+    @PreAuthorize("@ss.hasPermi('resource:requisition:query')")
+    @GetMapping("/agreements/{agreementId}")
+    public AjaxResult getAgreementInfo(@PathVariable Long agreementId) {
+        RequisitionAgreement agreement = requisitionService.getRequisitionAgreementById(agreementId);
+        return success(RequisitionAgreementAssembler.toDTO(agreement));
+    }
+
+    /**
+     * 新增征用协议
+     */
+    @PreAuthorize("@ss.hasPermi('resource:requisition:edit')")
+    @Log(title = "征用协议管理", businessType = BusinessType.INSERT)
+    @PostMapping("/{id}/agreements")
+    public AjaxResult addAgreement(@PathVariable Long id, @Validated @RequestBody RequisitionAgreementDTO dto) {
+        dto.setRequisitionId(id);
+        RequisitionAgreement agreement = RequisitionAgreementAssembler.toDomain(dto);
+        return toAjax(requisitionService.createRequisitionAgreement(agreement));
+    }
+
+    /**
+     * 修改征用协议
+     */
+    @PreAuthorize("@ss.hasPermi('resource:requisition:edit')")
+    @Log(title = "征用协议管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/{id}/agreements/{agreementId}")
+    public AjaxResult editAgreement(@PathVariable Long id, @PathVariable Long agreementId, @Validated @RequestBody RequisitionAgreementDTO dto) {
+        dto.setId(agreementId);
+        dto.setRequisitionId(id);
+        RequisitionAgreement agreement = RequisitionAgreementAssembler.toDomain(dto);
+        return toAjax(requisitionService.modifyRequisitionAgreement(agreement));
+    }
+
+    /**
+     * 删除征用协议
+     */
+    @PreAuthorize("@ss.hasPermi('resource:requisition:edit')")
+    @Log(title = "征用协议管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{id}/agreements/{agreementId}")
+    public AjaxResult removeAgreement(@PathVariable Long id, @PathVariable Long agreementId) {
+        return toAjax(requisitionService.removeRequisitionAgreement(agreementId));
+    }
+
+    /**
+     * 查询补偿记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('resource:requisition:query')")
+    @GetMapping("/{id}/compensations")
+    public TableDataInfo listCompensations(@PathVariable Long id, RequisitionCompensation compensation) {
+        compensation.setRequisitionId(id);
+        startPage();
+        List<RequisitionCompensation> list = requisitionService.listRequisitionCompensations(compensation);
+        return getDataTable(list);
+    }
+
+    /**
+     * 根据ID获取补偿记录详情
+     */
+    @PreAuthorize("@ss.hasPermi('resource:requisition:query')")
+    @GetMapping("/compensations/{compensationId}")
+    public AjaxResult getCompensationInfo(@PathVariable Long compensationId) {
+        RequisitionCompensation compensation = requisitionService.getRequisitionCompensationById(compensationId);
+        return success(RequisitionCompensationAssembler.toDTO(compensation));
+    }
+
+    /**
+     * 新增补偿记录
+     */
+    @PreAuthorize("@ss.hasPermi('resource:requisition:edit')")
+    @Log(title = "补偿管理", businessType = BusinessType.INSERT)
+    @PostMapping("/{id}/compensations")
+    public AjaxResult addCompensation(@PathVariable Long id, @Validated @RequestBody RequisitionCompensationDTO dto) {
+        dto.setRequisitionId(id);
+        RequisitionCompensation compensation = RequisitionCompensationAssembler.toDomain(dto);
+        return toAjax(requisitionService.createRequisitionCompensation(compensation));
+    }
+
+    /**
+     * 修改补偿记录
+     */
+    @PreAuthorize("@ss.hasPermi('resource:requisition:edit')")
+    @Log(title = "补偿管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/{id}/compensations/{compensationId}")
+    public AjaxResult editCompensation(@PathVariable Long id, @PathVariable Long compensationId, @Validated @RequestBody RequisitionCompensationDTO dto) {
+        dto.setId(compensationId);
+        dto.setRequisitionId(id);
+        RequisitionCompensation compensation = RequisitionCompensationAssembler.toDomain(dto);
+        return toAjax(requisitionService.modifyRequisitionCompensation(compensation));
+    }
+
+    /**
+     * 删除补偿记录
+     */
+    @PreAuthorize("@ss.hasPermi('resource:requisition:edit')")
+    @Log(title = "补偿管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{id}/compensations/{compensationId}")
+    public AjaxResult removeCompensation(@PathVariable Long id, @PathVariable Long compensationId) {
+        return toAjax(requisitionService.removeRequisitionCompensation(compensationId));
+    }
+
+    /**
+     * 结算补偿(pending → settled)
+     */
+    @PreAuthorize("@ss.hasPermi('resource:requisition:edit')")
+    @Log(title = "补偿管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/compensations/{compensationId}/settle")
+    public AjaxResult settleCompensation(@PathVariable Long compensationId, @RequestBody Map<String, Object> params) {
+        String invoiceNo = StringUtils.isNotNull(params.get("invoiceNo")) ? params.get("invoiceNo").toString() : null;
+        java.util.Date paidAt = null;
+        if (params.get("paidAt") != null) {
+            if (params.get("paidAt") instanceof String) {
+                try {
+                    paidAt = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse((String) params.get("paidAt"));
+                } catch (Exception e) {
+                    return error("支付时间格式错误");
+                }
+            } else if (params.get("paidAt") instanceof Long) {
+                paidAt = new java.util.Date((Long) params.get("paidAt"));
+            } else if (params.get("paidAt") instanceof java.util.Date) {
+                paidAt = (java.util.Date) params.get("paidAt");
+            }
+        }
+        if (paidAt == null) {
+            paidAt = new java.util.Date();
+        }
+        return toAjax(requisitionService.settleCompensation(compensationId, invoiceNo, paidAt));
+    }
+}

+ 201 - 0
aegis-admin/src/main/java/com/aegis/web/controller/resource/supplier/ResourceSupplierController.java

@@ -0,0 +1,201 @@
+package com.aegis.web.controller.resource.supplier;
+
+import java.util.List;
+import java.util.Map;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+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 com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.page.TableDataInfo;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.resource.supplier.assembler.SupplierAssembler;
+import com.aegis.resource.supplier.domain.Supplier;
+import com.aegis.resource.supplier.domain.SupplierLicense;
+import com.aegis.resource.supplier.dto.SupplierDTO;
+import com.aegis.resource.supplier.service.IResourceSupplierService;
+
+/**
+ * 供应商Controller
+ * 
+ * @author aegis
+ */
+@RestController
+@RequestMapping("/resources/suppliers")
+public class ResourceSupplierController extends BaseController {
+
+    @Autowired
+    private IResourceSupplierService supplierService;
+
+    /**
+     * 查询供应商列表
+     */
+    @PreAuthorize("@ss.hasPermi('resource:supplier:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(Supplier supplier) {
+        startPage();
+        List<Supplier> list = supplierService.listSuppliers(supplier);
+        return getDataTable(list);
+    }
+
+    /**
+     * 根据ID获取供应商详情
+     */
+    @PreAuthorize("@ss.hasPermi('resource:supplier:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        return success(supplierService.getSupplierById(id));
+    }
+
+    /**
+     * 新增供应商
+     */
+    @PreAuthorize("@ss.hasPermi('resource:supplier:add')")
+    @Log(title = "供应商管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody SupplierDTO supplierDTO) {
+        Supplier supplier = SupplierAssembler.toDomain(supplierDTO);
+        // 唯一性检查和恢复逻辑由 Service 层统一处理
+        return toAjax(supplierService.createSupplier(supplier));
+    }
+
+    /**
+     * 修改供应商
+     */
+    @PreAuthorize("@ss.hasPermi('resource:supplier:edit')")
+    @Log(title = "供应商管理", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody SupplierDTO supplierDTO) {
+        Supplier supplier = SupplierAssembler.toDomain(supplierDTO);
+        // 唯一性检查和恢复逻辑由 Service 层统一处理
+        return toAjax(supplierService.modifySupplier(supplier));
+    }
+
+    /**
+     * 删除供应商
+     */
+    @PreAuthorize("@ss.hasPermi('resource:supplier:remove')")
+    @Log(title = "供应商管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable String ids) {
+        // 支持单个ID和多个ID(逗号分隔)
+        Long[] idArray = null;
+        if (ids.contains(",")) {
+            String[] idStrings = ids.split(",");
+            idArray = new Long[idStrings.length];
+            for (int i = 0; i < idStrings.length; i++) {
+                idArray[i] = Long.parseLong(idStrings[i].trim());
+            }
+        } else {
+            idArray = new Long[]{Long.parseLong(ids.trim())};
+        }
+        return toAjax(supplierService.removeSuppliers(idArray));
+    }
+
+    /**
+     * 启用供应商
+     */
+    @PreAuthorize("@ss.hasPermi('resource:supplier:edit')")
+    @Log(title = "供应商管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/activate")
+    public AjaxResult activate(@PathVariable Long id) {
+        return toAjax(supplierService.activateSupplier(id));
+    }
+
+    /**
+     * 停用供应商
+     */
+    @PreAuthorize("@ss.hasPermi('resource:supplier:edit')")
+    @Log(title = "供应商管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/deactivate")
+    public AjaxResult deactivate(@PathVariable Long id) {
+        return toAjax(supplierService.deactivateSupplier(id));
+    }
+
+    /**
+     * 加入黑名单
+     */
+    @PreAuthorize("@ss.hasPermi('resource:supplier:edit')")
+    @Log(title = "供应商管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/blacklist")
+    public AjaxResult blacklist(@PathVariable Long id) {
+        return toAjax(supplierService.blacklistSupplier(id));
+    }
+
+    /**
+     * 评分(1-5星)
+     */
+    @PreAuthorize("@ss.hasPermi('resource:supplier:edit')")
+    @Log(title = "供应商管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/rate")
+    public AjaxResult rate(@PathVariable Long id, @RequestBody Map<String, Object> params) {
+        Integer rating = params.get("rating") != null ? Integer.valueOf(params.get("rating").toString()) : null;
+        if (rating == null || rating < 1 || rating > 5) {
+            return error("评分必须在1-5之间");
+        }
+        return toAjax(supplierService.updateSupplierRating(id, rating));
+    }
+
+    /**
+     * 查询资质列表
+     */
+    @PreAuthorize("@ss.hasPermi('resource:supplier:query')")
+    @GetMapping("/{id}/licenses")
+    public TableDataInfo listLicenses(@PathVariable Long id, SupplierLicense license) {
+        license.setSupplierId(id);
+        startPage();
+        List<SupplierLicense> list = supplierService.listLicenses(license);
+        return getDataTable(list);
+    }
+
+    /**
+     * 根据ID获取资质详情
+     */
+    @PreAuthorize("@ss.hasPermi('resource:supplier:query')")
+    @GetMapping("/licenses/{licenseId}")
+    public AjaxResult getLicenseInfo(@PathVariable Long licenseId) {
+        return success(supplierService.getLicenseById(licenseId));
+    }
+
+    /**
+     * 新增资质
+     */
+    @PreAuthorize("@ss.hasPermi('resource:supplier:edit')")
+    @Log(title = "供应商资质管理", businessType = BusinessType.INSERT)
+    @PostMapping("/{id}/licenses")
+    public AjaxResult addLicense(@PathVariable Long id, @Validated @RequestBody SupplierLicense license) {
+        license.setSupplierId(id);
+        return toAjax(supplierService.createLicense(license));
+    }
+
+    /**
+     * 修改资质
+     */
+    @PreAuthorize("@ss.hasPermi('resource:supplier:edit')")
+    @Log(title = "供应商资质管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/{id}/licenses/{licenseId}")
+    public AjaxResult editLicense(@PathVariable Long id, @PathVariable Long licenseId, @Validated @RequestBody SupplierLicense license) {
+        license.setId(licenseId);
+        license.setSupplierId(id);
+        return toAjax(supplierService.modifyLicense(license));
+    }
+
+    /**
+     * 删除资质
+     */
+    @PreAuthorize("@ss.hasPermi('resource:supplier:edit')")
+    @Log(title = "供应商资质管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{id}/licenses/{licenseId}")
+    public AjaxResult removeLicense(@PathVariable Long id, @PathVariable Long licenseId) {
+        return toAjax(supplierService.removeLicense(licenseId));
+    }
+}

+ 83 - 0
aegis-admin/src/main/java/com/aegis/web/controller/resource/team/ResourceTeamController.java

@@ -0,0 +1,83 @@
+package com.aegis.web.controller.resource.team;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+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 com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.page.TableDataInfo;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.resource.team.domain.Team;
+import com.aegis.resource.team.service.IResourceTeamService;
+
+/**
+ * 应急队伍Controller
+ * 
+ * @author aegis
+ */
+@RestController
+@RequestMapping("/resources/teams")
+public class ResourceTeamController extends BaseController {
+
+    @Autowired
+    private IResourceTeamService teamService;
+
+    /**
+     * 查询队伍列表
+     */
+    @PreAuthorize("@ss.hasPermi('resource:team:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(Team team) {
+        startPage();
+        return getDataTable(teamService.listByCondition(team));
+    }
+
+    /**
+     * 根据ID获取队伍详情
+     */
+    @PreAuthorize("@ss.hasPermi('resource:team:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        return success(teamService.getById(id));
+    }
+
+    /**
+     * 新增队伍
+     */
+    @PreAuthorize("@ss.hasPermi('resource:team:add')")
+    @Log(title = "应急队伍", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody Team team) {
+        return toAjax(teamService.save(team));
+    }
+
+    /**
+     * 修改队伍
+     */
+    @PreAuthorize("@ss.hasPermi('resource:team:edit')")
+    @Log(title = "应急队伍", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody Team team) {
+        return toAjax(teamService.updateById(team));
+    }
+
+    /**
+     * 删除队伍
+     */
+    @PreAuthorize("@ss.hasPermi('resource:team:remove')")
+    @Log(title = "应急队伍", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        boolean removed = teamService.removeByIds(java.util.Arrays.asList(ids));
+        return removed ? success() : error("删除失败");
+    }
+}

+ 51 - 0
aegis-admin/src/main/java/com/aegis/web/controller/resource/team/ResourceTeamLogController.java

@@ -0,0 +1,51 @@
+package com.aegis.web.controller.resource.team;
+
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+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.RestController;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.page.TableDataInfo;
+import com.aegis.resource.team.domain.TeamLog;
+import com.aegis.resource.team.service.IResourceTeamLogService;
+
+/**
+ * 队伍操作日志Controller
+ * 
+ * @author aegis
+ */
+@RestController
+@RequestMapping("/resources/teams/{teamId}/logs")
+public class ResourceTeamLogController extends BaseController {
+    @Autowired
+    private IResourceTeamLogService teamLogService;
+
+    /**
+     * 查询队伍操作日志列表
+     */
+    @PreAuthorize("@ss.hasPermi('resource:team:log:list')")
+    @GetMapping
+    public TableDataInfo list(@PathVariable Long teamId, TeamLog teamLog) {
+        teamLog.setTeamId(teamId);
+        startPage();
+        List<TeamLog> list = teamLogService.listByCondition(teamLog);
+        return getDataTable(list);
+    }
+
+    /**
+     * 根据ID获取日志详情
+     */
+    @PreAuthorize("@ss.hasPermi('resource:team:log:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable Long teamId, @PathVariable Long id) {
+        TeamLog log = teamLogService.getById(id);
+        if (log != null && !log.getTeamId().equals(teamId)) {
+            return error("日志不属于该队伍");
+        }
+        return success(log);
+    }
+}

+ 108 - 0
aegis-admin/src/main/java/com/aegis/web/controller/resource/team/ResourceTeamMemberController.java

@@ -0,0 +1,108 @@
+package com.aegis.web.controller.resource.team;
+
+import java.util.Arrays;
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+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 com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.page.TableDataInfo;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.resource.team.domain.TeamMember;
+import com.aegis.resource.team.service.IResourceTeamMemberService;
+
+/**
+ * 队伍成员Controller
+ * 
+ * @author aegis
+ */
+@RestController
+@RequestMapping("/resources/teams/{teamId}/members")
+public class ResourceTeamMemberController extends BaseController {
+
+    @Autowired
+    private IResourceTeamMemberService teamMemberService;
+
+    /**
+     * 查询队伍成员列表
+     */
+    @PreAuthorize("@ss.hasPermi('resource:team:member:list')")
+    @GetMapping
+    public TableDataInfo list(@PathVariable Long teamId, TeamMember teamMember) {
+        teamMember.setTeamId(teamId);
+        startPage();
+        List<TeamMember> list = teamMemberService.listByCondition(teamMember);
+        return getDataTable(list);
+    }
+
+    /**
+     * 根据ID获取成员详情
+     */
+    @PreAuthorize("@ss.hasPermi('resource:team:member:query')")
+    @GetMapping(value = "/{memberId}")
+    public AjaxResult getInfo(@PathVariable Long teamId, @PathVariable Long memberId) {
+        TeamMember member = teamMemberService.getById(memberId);
+        if (member != null && !member.getTeamId().equals(teamId)) {
+            return error("成员不属于该队伍");
+        }
+        return success(member);
+    }
+
+    /**
+     * 新增队伍成员
+     */
+    @PreAuthorize("@ss.hasPermi('resource:team:member:add')")
+    @Log(title = "队伍成员", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@PathVariable Long teamId, @Validated @RequestBody TeamMember teamMember) {
+        teamMember.setTeamId(teamId);
+        return toAjax(teamMemberService.save(teamMember) ? 1 : 0);
+    }
+
+    /**
+     * 修改队伍成员
+     */
+    @PreAuthorize("@ss.hasPermi('resource:team:member:edit')")
+    @Log(title = "队伍成员", businessType = BusinessType.UPDATE)
+    @PutMapping("/{memberId}")
+    public AjaxResult edit(@PathVariable Long teamId, @PathVariable Long memberId, @Validated @RequestBody TeamMember teamMember) {
+        TeamMember existingMember = teamMemberService.getById(memberId);
+        if (existingMember == null) {
+            return error("成员不存在");
+        }
+        if (!existingMember.getTeamId().equals(teamId)) {
+            return error("成员不属于该队伍");
+        }
+        teamMember.setId(memberId);
+        teamMember.setTeamId(teamId);
+        return toAjax(teamMemberService.updateById(teamMember) ? 1 : 0);
+    }
+
+    /**
+     * 删除队伍成员
+     */
+    @PreAuthorize("@ss.hasPermi('resource:team:member:remove')")
+    @Log(title = "队伍成员", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{memberIds}")
+    public AjaxResult remove(@PathVariable Long teamId, @PathVariable Long[] memberIds) {
+        // 验证所有成员都属于该队伍
+        for (Long memberId : memberIds) {
+            TeamMember member = teamMemberService.getById(memberId);
+            if (member == null || !member.getTeamId().equals(teamId)) {
+                return error("存在不属于该队伍的成员");
+            }
+        }
+        boolean removed = teamMemberService.removeByIds(Arrays.asList(memberIds));
+        return removed ? success() : error("删除失败");
+    }
+}

+ 126 - 0
aegis-admin/src/main/java/com/aegis/web/controller/resource/vehicle/ResourceVehicleController.java

@@ -0,0 +1,126 @@
+package com.aegis.web.controller.resource.vehicle;
+
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+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 com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.page.TableDataInfo;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.resource.vehicle.domain.Vehicle;
+import com.aegis.resource.vehicle.service.IResourceVehicleService;
+
+/**
+ * 车辆Controller
+ * 
+ * @author aegis
+ */
+@RestController
+@RequestMapping("/resources/vehicles")
+public class ResourceVehicleController extends BaseController {
+
+    @Autowired
+    private IResourceVehicleService vehicleService;
+
+    /**
+     * 查询车辆列表
+     */
+    @PreAuthorize("@ss.hasPermi('resource:vehicle:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(Vehicle vehicle) {
+        startPage();
+        List<Vehicle> list = vehicleService.listByCondition(vehicle);
+        return getDataTable(list);
+    }
+
+    /**
+     * 根据ID获取车辆详情
+     */
+    @PreAuthorize("@ss.hasPermi('resource:vehicle:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        return success(vehicleService.getById(id));
+    }
+
+    /**
+     * 根据队伍ID查询车辆列表
+     */
+    @PreAuthorize("@ss.hasPermi('resource:vehicle:list')")
+    @GetMapping("/team/{teamId}")
+    public AjaxResult listByTeam(@PathVariable Long teamId) {
+        List<Vehicle> list = vehicleService.listByTeamId(teamId);
+        return success(list);
+    }
+
+    /**
+     * 新增车辆
+     */
+    @PreAuthorize("@ss.hasPermi('resource:vehicle:add')")
+    @Log(title = "车辆管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody Vehicle vehicle) {
+        return toAjax(vehicleService.save(vehicle));
+    }
+
+    /**
+     * 修改车辆
+     */
+    @PreAuthorize("@ss.hasPermi('resource:vehicle:edit')")
+    @Log(title = "车辆管理", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody Vehicle vehicle) {
+        return toAjax(vehicleService.updateById(vehicle));
+    }
+
+    /**
+     * 删除车辆
+     */
+    @PreAuthorize("@ss.hasPermi('resource:vehicle:remove')")
+    @Log(title = "车辆管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        boolean removed = vehicleService.removeByIds(Arrays.asList(ids));
+        return removed ? success() : error("删除失败");
+    }
+
+    /**
+     * 设置车辆状态
+     */
+    @PreAuthorize("@ss.hasPermi('resource:vehicle:edit')")
+    @Log(title = "车辆管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/{id}/status")
+    public AjaxResult updateStatus(@PathVariable Long id, @RequestBody Vehicle vehicle) {
+        Vehicle existingVehicle = vehicleService.getById(id);
+        if (existingVehicle == null) {
+            return error("车辆不存在");
+        }
+        existingVehicle.setStatus(vehicle.getStatus());
+        return toAjax(vehicleService.updateById(existingVehicle));
+    }
+
+    /**
+     * 更新GPS定位(用于GPS设备上报)
+     */
+    @PostMapping("/{id}/location")
+    public AjaxResult updateLocation(@PathVariable Long id, @RequestBody Map<String, Object> params) {
+        BigDecimal gpsLat = params.get("gpsLat") != null ? new BigDecimal(params.get("gpsLat").toString()) : null;
+        BigDecimal gpsLng = params.get("gpsLng") != null ? new BigDecimal(params.get("gpsLng").toString()) : null;
+        if (gpsLat == null || gpsLng == null) {
+            return error("GPS坐标不能为空");
+        }
+        return toAjax(vehicleService.updateVehicleLocation(id, gpsLat, gpsLng));
+    }
+}

+ 97 - 0
aegis-admin/src/main/java/com/aegis/web/controller/resource/warehouse/ResourceWarehouseController.java

@@ -0,0 +1,97 @@
+package com.aegis.web.controller.resource.warehouse;
+
+import com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.page.TableDataInfo;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.common.exception.ServiceException;
+import com.aegis.resource.warehouse.domain.Warehouse;
+import com.aegis.resource.warehouse.service.IResourceWarehouseService;
+import java.util.Arrays;
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+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;
+
+@RestController
+@RequestMapping("/resources/warehouses")
+public class ResourceWarehouseController extends BaseController {
+
+    @Autowired
+    private IResourceWarehouseService warehouseService;
+
+    @PreAuthorize("@ss.hasPermi('resource:warehouse:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(Warehouse warehouse) {
+        startPage();
+        List<Warehouse> list = warehouseService.listByCondition(warehouse);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('resource:warehouse:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        return success(warehouseService.getById(id));
+    }
+
+    @PreAuthorize("@ss.hasPermi('resource:warehouse:add')")
+    @Log(title = "仓库管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody Warehouse warehouse) {
+        try {
+            return toAjax(warehouseService.save(warehouse));
+        } catch (ServiceException e) {
+            return error(e.getMessage());
+        }
+    }
+
+    @PreAuthorize("@ss.hasPermi('resource:warehouse:edit')")
+    @Log(title = "仓库管理", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody Warehouse warehouse) {
+        try {
+            return toAjax(warehouseService.updateById(warehouse));
+        } catch (ServiceException e) {
+            return error(e.getMessage());
+        }
+    }
+
+    @PreAuthorize("@ss.hasPermi('resource:warehouse:remove')")
+    @Log(title = "仓库管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        boolean removed = warehouseService.removeByIds(Arrays.asList(ids));
+        return removed ? success() : error("删除失败");
+    }
+
+    @PreAuthorize("@ss.hasPermi('resource:warehouse:edit')")
+    @Log(title = "仓库管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/enable")
+    public AjaxResult enable(@PathVariable Long id) {
+        try {
+            return toAjax(warehouseService.updateStatus(id, "0"));
+        } catch (ServiceException e) {
+            return error(e.getMessage());
+        }
+    }
+
+    @PreAuthorize("@ss.hasPermi('resource:warehouse:edit')")
+    @Log(title = "仓库管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/{id}/disable")
+    public AjaxResult disable(@PathVariable Long id) {
+        try {
+            return toAjax(warehouseService.updateStatus(id, "1"));
+        } catch (ServiceException e) {
+            return error(e.getMessage());
+        }
+    }
+}

+ 133 - 0
aegis-admin/src/main/java/com/aegis/web/controller/system/SysConfigController.java

@@ -0,0 +1,133 @@
+package com.aegis.web.controller.system;
+
+import java.util.List;
+import javax.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+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 com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.page.TableDataInfo;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.common.utils.poi.ExcelUtil;
+import com.aegis.system.domain.SysConfig;
+import com.aegis.system.service.ISysConfigService;
+
+/**
+ * 参数配置 信息操作处理
+ * 
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/system/config")
+public class SysConfigController extends BaseController
+{
+    @Autowired
+    private ISysConfigService configService;
+
+    /**
+     * 获取参数配置列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:config:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(SysConfig config)
+    {
+        startPage();
+        List<SysConfig> list = configService.selectConfigList(config);
+        return getDataTable(list);
+    }
+
+    @Log(title = "参数管理", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('system:config:export')")
+    @PostMapping("/export")
+    public void export(HttpServletResponse response, SysConfig config)
+    {
+        List<SysConfig> list = configService.selectConfigList(config);
+        ExcelUtil<SysConfig> util = new ExcelUtil<SysConfig>(SysConfig.class);
+        util.exportExcel(response, list, "参数数据");
+    }
+
+    /**
+     * 根据参数编号获取详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('system:config:query')")
+    @GetMapping(value = "/{configId}")
+    public AjaxResult getInfo(@PathVariable Long configId)
+    {
+        return success(configService.selectConfigById(configId));
+    }
+
+    /**
+     * 根据参数键名查询参数值
+     */
+    @GetMapping(value = "/configKey/{configKey}")
+    public AjaxResult getConfigKey(@PathVariable String configKey)
+    {
+        return success(configService.selectConfigByKey(configKey));
+    }
+
+    /**
+     * 新增参数配置
+     */
+    @PreAuthorize("@ss.hasPermi('system:config:add')")
+    @Log(title = "参数管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody SysConfig config)
+    {
+        if (!configService.checkConfigKeyUnique(config))
+        {
+            return error("新增参数'" + config.getConfigName() + "'失败,参数键名已存在");
+        }
+        config.setCreateBy(getUsername());
+        return toAjax(configService.insertConfig(config));
+    }
+
+    /**
+     * 修改参数配置
+     */
+    @PreAuthorize("@ss.hasPermi('system:config:edit')")
+    @Log(title = "参数管理", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody SysConfig config)
+    {
+        if (!configService.checkConfigKeyUnique(config))
+        {
+            return error("修改参数'" + config.getConfigName() + "'失败,参数键名已存在");
+        }
+        config.setUpdateBy(getUsername());
+        return toAjax(configService.updateConfig(config));
+    }
+
+    /**
+     * 删除参数配置
+     */
+    @PreAuthorize("@ss.hasPermi('system:config:remove')")
+    @Log(title = "参数管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{configIds}")
+    public AjaxResult remove(@PathVariable Long[] configIds)
+    {
+        configService.deleteConfigByIds(configIds);
+        return success();
+    }
+
+    /**
+     * 刷新参数缓存
+     */
+    @PreAuthorize("@ss.hasPermi('system:config:remove')")
+    @Log(title = "参数管理", businessType = BusinessType.CLEAN)
+    @DeleteMapping("/refreshCache")
+    public AjaxResult refreshCache()
+    {
+        configService.resetConfigCache();
+        return success();
+    }
+}

+ 132 - 0
aegis-admin/src/main/java/com/aegis/web/controller/system/SysDeptController.java

@@ -0,0 +1,132 @@
+package com.aegis.web.controller.system;
+
+import java.util.List;
+import org.apache.commons.lang3.ArrayUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+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 com.aegis.common.annotation.Log;
+import com.aegis.common.constant.UserConstants;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.domain.entity.SysDept;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.common.utils.StringUtils;
+import com.aegis.system.service.ISysDeptService;
+
+/**
+ * 部门信息
+ * 
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/system/dept")
+public class SysDeptController extends BaseController
+{
+    @Autowired
+    private ISysDeptService deptService;
+
+    /**
+     * 获取部门列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:dept:list')")
+    @GetMapping("/list")
+    public AjaxResult list(SysDept dept)
+    {
+        List<SysDept> depts = deptService.selectDeptList(dept);
+        return success(depts);
+    }
+
+    /**
+     * 查询部门列表(排除节点)
+     */
+    @PreAuthorize("@ss.hasPermi('system:dept:list')")
+    @GetMapping("/list/exclude/{deptId}")
+    public AjaxResult excludeChild(@PathVariable(value = "deptId", required = false) Long deptId)
+    {
+        List<SysDept> depts = deptService.selectDeptList(new SysDept());
+        depts.removeIf(d -> d.getDeptId().intValue() == deptId || ArrayUtils.contains(StringUtils.split(d.getAncestors(), ","), deptId + ""));
+        return success(depts);
+    }
+
+    /**
+     * 根据部门编号获取详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('system:dept:query')")
+    @GetMapping(value = "/{deptId}")
+    public AjaxResult getInfo(@PathVariable Long deptId)
+    {
+        deptService.checkDeptDataScope(deptId);
+        return success(deptService.selectDeptById(deptId));
+    }
+
+    /**
+     * 新增部门
+     */
+    @PreAuthorize("@ss.hasPermi('system:dept:add')")
+    @Log(title = "部门管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody SysDept dept)
+    {
+        if (!deptService.checkDeptNameUnique(dept))
+        {
+            return error("新增部门'" + dept.getDeptName() + "'失败,部门名称已存在");
+        }
+        dept.setCreateBy(getUsername());
+        return toAjax(deptService.insertDept(dept));
+    }
+
+    /**
+     * 修改部门
+     */
+    @PreAuthorize("@ss.hasPermi('system:dept:edit')")
+    @Log(title = "部门管理", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody SysDept dept)
+    {
+        Long deptId = dept.getDeptId();
+        deptService.checkDeptDataScope(deptId);
+        if (!deptService.checkDeptNameUnique(dept))
+        {
+            return error("修改部门'" + dept.getDeptName() + "'失败,部门名称已存在");
+        }
+        else if (dept.getParentId().equals(deptId))
+        {
+            return error("修改部门'" + dept.getDeptName() + "'失败,上级部门不能是自己");
+        }
+        else if (StringUtils.equals(UserConstants.DEPT_DISABLE, dept.getStatus()) && deptService.selectNormalChildrenDeptById(deptId) > 0)
+        {
+            return error("该部门包含未停用的子部门!");
+        }
+        dept.setUpdateBy(getUsername());
+        return toAjax(deptService.updateDept(dept));
+    }
+
+    /**
+     * 删除部门
+     */
+    @PreAuthorize("@ss.hasPermi('system:dept:remove')")
+    @Log(title = "部门管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{deptId}")
+    public AjaxResult remove(@PathVariable Long deptId)
+    {
+        if (deptService.hasChildByDeptId(deptId))
+        {
+            return warn("存在下级部门,不允许删除");
+        }
+        if (deptService.checkDeptExistUser(deptId))
+        {
+            return warn("部门存在用户,不允许删除");
+        }
+        deptService.checkDeptDataScope(deptId);
+        return toAjax(deptService.deleteDeptById(deptId));
+    }
+}

+ 121 - 0
aegis-admin/src/main/java/com/aegis/web/controller/system/SysDictDataController.java

@@ -0,0 +1,121 @@
+package com.aegis.web.controller.system;
+
+import java.util.ArrayList;
+import java.util.List;
+import javax.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+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 com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.domain.entity.SysDictData;
+import com.aegis.common.core.page.TableDataInfo;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.common.utils.StringUtils;
+import com.aegis.common.utils.poi.ExcelUtil;
+import com.aegis.system.service.ISysDictDataService;
+import com.aegis.system.service.ISysDictTypeService;
+
+/**
+ * 数据字典信息
+ * 
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/system/dict/data")
+public class SysDictDataController extends BaseController
+{
+    @Autowired
+    private ISysDictDataService dictDataService;
+
+    @Autowired
+    private ISysDictTypeService dictTypeService;
+
+    @PreAuthorize("@ss.hasPermi('system:dict:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(SysDictData dictData)
+    {
+        startPage();
+        List<SysDictData> list = dictDataService.selectDictDataList(dictData);
+        return getDataTable(list);
+    }
+
+    @Log(title = "字典数据", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('system:dict:export')")
+    @PostMapping("/export")
+    public void export(HttpServletResponse response, SysDictData dictData)
+    {
+        List<SysDictData> list = dictDataService.selectDictDataList(dictData);
+        ExcelUtil<SysDictData> util = new ExcelUtil<SysDictData>(SysDictData.class);
+        util.exportExcel(response, list, "字典数据");
+    }
+
+    /**
+     * 查询字典数据详细
+     */
+    @PreAuthorize("@ss.hasPermi('system:dict:query')")
+    @GetMapping(value = "/{dictCode}")
+    public AjaxResult getInfo(@PathVariable Long dictCode)
+    {
+        return success(dictDataService.selectDictDataById(dictCode));
+    }
+
+    /**
+     * 根据字典类型查询字典数据信息
+     */
+    @GetMapping(value = "/type/{dictType}")
+    public AjaxResult dictType(@PathVariable String dictType)
+    {
+        List<SysDictData> data = dictTypeService.selectDictDataByType(dictType);
+        if (StringUtils.isNull(data))
+        {
+            data = new ArrayList<SysDictData>();
+        }
+        return success(data);
+    }
+
+    /**
+     * 新增字典类型
+     */
+    @PreAuthorize("@ss.hasPermi('system:dict:add')")
+    @Log(title = "字典数据", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody SysDictData dict)
+    {
+        dict.setCreateBy(getUsername());
+        return toAjax(dictDataService.insertDictData(dict));
+    }
+
+    /**
+     * 修改保存字典类型
+     */
+    @PreAuthorize("@ss.hasPermi('system:dict:edit')")
+    @Log(title = "字典数据", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody SysDictData dict)
+    {
+        dict.setUpdateBy(getUsername());
+        return toAjax(dictDataService.updateDictData(dict));
+    }
+
+    /**
+     * 删除字典类型
+     */
+    @PreAuthorize("@ss.hasPermi('system:dict:remove')")
+    @Log(title = "字典类型", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{dictCodes}")
+    public AjaxResult remove(@PathVariable Long[] dictCodes)
+    {
+        dictDataService.deleteDictDataByIds(dictCodes);
+        return success();
+    }
+}

+ 131 - 0
aegis-admin/src/main/java/com/aegis/web/controller/system/SysDictTypeController.java

@@ -0,0 +1,131 @@
+package com.aegis.web.controller.system;
+
+import java.util.List;
+import javax.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+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 com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.domain.entity.SysDictType;
+import com.aegis.common.core.page.TableDataInfo;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.common.utils.poi.ExcelUtil;
+import com.aegis.system.service.ISysDictTypeService;
+
+/**
+ * 数据字典信息
+ * 
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/system/dict/type")
+public class SysDictTypeController extends BaseController
+{
+    @Autowired
+    private ISysDictTypeService dictTypeService;
+
+    @PreAuthorize("@ss.hasPermi('system:dict:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(SysDictType dictType)
+    {
+        startPage();
+        List<SysDictType> list = dictTypeService.selectDictTypeList(dictType);
+        return getDataTable(list);
+    }
+
+    @Log(title = "字典类型", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('system:dict:export')")
+    @PostMapping("/export")
+    public void export(HttpServletResponse response, SysDictType dictType)
+    {
+        List<SysDictType> list = dictTypeService.selectDictTypeList(dictType);
+        ExcelUtil<SysDictType> util = new ExcelUtil<SysDictType>(SysDictType.class);
+        util.exportExcel(response, list, "字典类型");
+    }
+
+    /**
+     * 查询字典类型详细
+     */
+    @PreAuthorize("@ss.hasPermi('system:dict:query')")
+    @GetMapping(value = "/{dictId}")
+    public AjaxResult getInfo(@PathVariable Long dictId)
+    {
+        return success(dictTypeService.selectDictTypeById(dictId));
+    }
+
+    /**
+     * 新增字典类型
+     */
+    @PreAuthorize("@ss.hasPermi('system:dict:add')")
+    @Log(title = "字典类型", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody SysDictType dict)
+    {
+        if (!dictTypeService.checkDictTypeUnique(dict))
+        {
+            return error("新增字典'" + dict.getDictName() + "'失败,字典类型已存在");
+        }
+        dict.setCreateBy(getUsername());
+        return toAjax(dictTypeService.insertDictType(dict));
+    }
+
+    /**
+     * 修改字典类型
+     */
+    @PreAuthorize("@ss.hasPermi('system:dict:edit')")
+    @Log(title = "字典类型", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody SysDictType dict)
+    {
+        if (!dictTypeService.checkDictTypeUnique(dict))
+        {
+            return error("修改字典'" + dict.getDictName() + "'失败,字典类型已存在");
+        }
+        dict.setUpdateBy(getUsername());
+        return toAjax(dictTypeService.updateDictType(dict));
+    }
+
+    /**
+     * 删除字典类型
+     */
+    @PreAuthorize("@ss.hasPermi('system:dict:remove')")
+    @Log(title = "字典类型", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{dictIds}")
+    public AjaxResult remove(@PathVariable Long[] dictIds)
+    {
+        dictTypeService.deleteDictTypeByIds(dictIds);
+        return success();
+    }
+
+    /**
+     * 刷新字典缓存
+     */
+    @PreAuthorize("@ss.hasPermi('system:dict:remove')")
+    @Log(title = "字典类型", businessType = BusinessType.CLEAN)
+    @DeleteMapping("/refreshCache")
+    public AjaxResult refreshCache()
+    {
+        dictTypeService.resetDictCache();
+        return success();
+    }
+
+    /**
+     * 获取字典选择框列表
+     */
+    @GetMapping("/optionselect")
+    public AjaxResult optionselect()
+    {
+        List<SysDictType> dictTypes = dictTypeService.selectDictTypeAll();
+        return success(dictTypes);
+    }
+}

+ 29 - 0
aegis-admin/src/main/java/com/aegis/web/controller/system/SysIndexController.java

@@ -0,0 +1,29 @@
+package com.aegis.web.controller.system;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.aegis.common.config.RuoYiConfig;
+import com.aegis.common.utils.StringUtils;
+
+/**
+ * 首页
+ *
+ * @author ruoyi
+ */
+@RestController
+public class SysIndexController
+{
+    /** 系统基础配置 */
+    @Autowired
+    private RuoYiConfig ruoyiConfig;
+
+    /**
+     * 访问首页,提示语
+     */
+    @RequestMapping("/")
+    public String index()
+    {
+        return StringUtils.format("欢迎使用{}后台管理框架,当前版本:v{},请通过前端地址访问。", ruoyiConfig.getName(), ruoyiConfig.getVersion());
+    }
+}

+ 131 - 0
aegis-admin/src/main/java/com/aegis/web/controller/system/SysLoginController.java

@@ -0,0 +1,131 @@
+package com.aegis.web.controller.system;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Set;
+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.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+import com.aegis.common.constant.Constants;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.domain.entity.SysMenu;
+import com.aegis.common.core.domain.entity.SysUser;
+import com.aegis.common.core.domain.model.LoginBody;
+import com.aegis.common.core.domain.model.LoginUser;
+import com.aegis.common.core.text.Convert;
+import com.aegis.common.utils.DateUtils;
+import com.aegis.common.utils.SecurityUtils;
+import com.aegis.common.utils.StringUtils;
+import com.aegis.framework.web.service.SysLoginService;
+import com.aegis.framework.web.service.SysPermissionService;
+import com.aegis.framework.web.service.TokenService;
+import com.aegis.system.service.ISysConfigService;
+import com.aegis.system.service.ISysMenuService;
+
+/**
+ * 登录验证
+ * 
+ * @author ruoyi
+ */
+@RestController
+public class SysLoginController
+{
+    @Autowired
+    private SysLoginService loginService;
+
+    @Autowired
+    private ISysMenuService menuService;
+
+    @Autowired
+    private SysPermissionService permissionService;
+
+    @Autowired
+    private TokenService tokenService;
+
+    @Autowired
+    private ISysConfigService configService;
+
+    /**
+     * 登录方法
+     * 
+     * @param loginBody 登录信息
+     * @return 结果
+     */
+    @PostMapping("/login")
+    public AjaxResult login(@RequestBody LoginBody loginBody)
+    {
+        AjaxResult ajax = AjaxResult.success();
+        // 生成令牌
+        String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
+                loginBody.getUuid());
+        ajax.put(Constants.TOKEN, token);
+        return ajax;
+    }
+
+    /**
+     * 获取用户信息
+     * 
+     * @return 用户信息
+     */
+    @GetMapping("getInfo")
+    public AjaxResult getInfo()
+    {
+        LoginUser loginUser = SecurityUtils.getLoginUser();
+        SysUser user = loginUser.getUser();
+        // 角色集合
+        Set<String> roles = permissionService.getRolePermission(user);
+        // 权限集合
+        Set<String> permissions = permissionService.getMenuPermission(user);
+        if (!loginUser.getPermissions().equals(permissions))
+        {
+            loginUser.setPermissions(permissions);
+            tokenService.refreshToken(loginUser);
+        }
+        AjaxResult ajax = AjaxResult.success();
+        ajax.put("user", user);
+        ajax.put("roles", roles);
+        ajax.put("permissions", permissions);
+        ajax.put("isDefaultModifyPwd", initPasswordIsModify(user.getPwdUpdateDate()));
+        ajax.put("isPasswordExpired", passwordIsExpiration(user.getPwdUpdateDate()));
+        return ajax;
+    }
+
+    /**
+     * 获取路由信息
+     * 
+     * @return 路由信息
+     */
+    @GetMapping("getRouters")
+    public AjaxResult getRouters()
+    {
+        Long userId = SecurityUtils.getUserId();
+        List<SysMenu> menus = menuService.selectMenuTreeByUserId(userId);
+        return AjaxResult.success(menuService.buildMenus(menus));
+    }
+    
+    // 检查初始密码是否提醒修改
+    public boolean initPasswordIsModify(Date pwdUpdateDate)
+    {
+        Integer initPasswordModify = Convert.toInt(configService.selectConfigByKey("sys.account.initPasswordModify"));
+        return initPasswordModify != null && initPasswordModify == 1 && pwdUpdateDate == null;
+    }
+
+    // 检查密码是否过期
+    public boolean passwordIsExpiration(Date pwdUpdateDate)
+    {
+        Integer passwordValidateDays = Convert.toInt(configService.selectConfigByKey("sys.account.passwordValidateDays"));
+        if (passwordValidateDays != null && passwordValidateDays > 0)
+        {
+            if (StringUtils.isNull(pwdUpdateDate))
+            {
+                // 如果从未修改过初始密码,直接提醒过期
+                return true;
+            }
+            Date nowDate = DateUtils.getNowDate();
+            return DateUtils.differentDaysByMillisecond(nowDate, pwdUpdateDate) > passwordValidateDays;
+        }
+        return false;
+    }
+}

+ 142 - 0
aegis-admin/src/main/java/com/aegis/web/controller/system/SysMenuController.java

@@ -0,0 +1,142 @@
+package com.aegis.web.controller.system;
+
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+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 com.aegis.common.annotation.Log;
+import com.aegis.common.constant.UserConstants;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.domain.entity.SysMenu;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.common.utils.StringUtils;
+import com.aegis.system.service.ISysMenuService;
+
+/**
+ * 菜单信息
+ * 
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/system/menu")
+public class SysMenuController extends BaseController
+{
+    @Autowired
+    private ISysMenuService menuService;
+
+    /**
+     * 获取菜单列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:menu:list')")
+    @GetMapping("/list")
+    public AjaxResult list(SysMenu menu)
+    {
+        List<SysMenu> menus = menuService.selectMenuList(menu, getUserId());
+        return success(menus);
+    }
+
+    /**
+     * 根据菜单编号获取详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('system:menu:query')")
+    @GetMapping(value = "/{menuId}")
+    public AjaxResult getInfo(@PathVariable Long menuId)
+    {
+        return success(menuService.selectMenuById(menuId));
+    }
+
+    /**
+     * 获取菜单下拉树列表
+     */
+    @GetMapping("/treeselect")
+    public AjaxResult treeselect(SysMenu menu)
+    {
+        List<SysMenu> menus = menuService.selectMenuList(menu, getUserId());
+        return success(menuService.buildMenuTreeSelect(menus));
+    }
+
+    /**
+     * 加载对应角色菜单列表树
+     */
+    @GetMapping(value = "/roleMenuTreeselect/{roleId}")
+    public AjaxResult roleMenuTreeselect(@PathVariable("roleId") Long roleId)
+    {
+        List<SysMenu> menus = menuService.selectMenuList(getUserId());
+        AjaxResult ajax = AjaxResult.success();
+        ajax.put("checkedKeys", menuService.selectMenuListByRoleId(roleId));
+        ajax.put("menus", menuService.buildMenuTreeSelect(menus));
+        return ajax;
+    }
+
+    /**
+     * 新增菜单
+     */
+    @PreAuthorize("@ss.hasPermi('system:menu:add')")
+    @Log(title = "菜单管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody SysMenu menu)
+    {
+        if (!menuService.checkMenuNameUnique(menu))
+        {
+            return error("新增菜单'" + menu.getMenuName() + "'失败,菜单名称已存在");
+        }
+        else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath()))
+        {
+            return error("新增菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头");
+        }
+        menu.setCreateBy(getUsername());
+        return toAjax(menuService.insertMenu(menu));
+    }
+
+    /**
+     * 修改菜单
+     */
+    @PreAuthorize("@ss.hasPermi('system:menu:edit')")
+    @Log(title = "菜单管理", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody SysMenu menu)
+    {
+        if (!menuService.checkMenuNameUnique(menu))
+        {
+            return error("修改菜单'" + menu.getMenuName() + "'失败,菜单名称已存在");
+        }
+        else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath()))
+        {
+            return error("修改菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头");
+        }
+        else if (menu.getMenuId().equals(menu.getParentId()))
+        {
+            return error("修改菜单'" + menu.getMenuName() + "'失败,上级菜单不能选择自己");
+        }
+        menu.setUpdateBy(getUsername());
+        return toAjax(menuService.updateMenu(menu));
+    }
+
+    /**
+     * 删除菜单
+     */
+    @PreAuthorize("@ss.hasPermi('system:menu:remove')")
+    @Log(title = "菜单管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{menuId}")
+    public AjaxResult remove(@PathVariable("menuId") Long menuId)
+    {
+        if (menuService.hasChildByMenuId(menuId))
+        {
+            return warn("存在子菜单,不允许删除");
+        }
+        if (menuService.checkMenuExistRole(menuId))
+        {
+            return warn("菜单已分配,不允许删除");
+        }
+        return toAjax(menuService.deleteMenuById(menuId));
+    }
+}

+ 91 - 0
aegis-admin/src/main/java/com/aegis/web/controller/system/SysNoticeController.java

@@ -0,0 +1,91 @@
+package com.aegis.web.controller.system;
+
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+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 com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.page.TableDataInfo;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.system.domain.SysNotice;
+import com.aegis.system.service.ISysNoticeService;
+
+/**
+ * 公告 信息操作处理
+ * 
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/system/notice")
+public class SysNoticeController extends BaseController
+{
+    @Autowired
+    private ISysNoticeService noticeService;
+
+    /**
+     * 获取通知公告列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:notice:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(SysNotice notice)
+    {
+        startPage();
+        List<SysNotice> list = noticeService.selectNoticeList(notice);
+        return getDataTable(list);
+    }
+
+    /**
+     * 根据通知公告编号获取详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('system:notice:query')")
+    @GetMapping(value = "/{noticeId}")
+    public AjaxResult getInfo(@PathVariable Long noticeId)
+    {
+        return success(noticeService.selectNoticeById(noticeId));
+    }
+
+    /**
+     * 新增通知公告
+     */
+    @PreAuthorize("@ss.hasPermi('system:notice:add')")
+    @Log(title = "通知公告", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody SysNotice notice)
+    {
+        notice.setCreateBy(getUsername());
+        return toAjax(noticeService.insertNotice(notice));
+    }
+
+    /**
+     * 修改通知公告
+     */
+    @PreAuthorize("@ss.hasPermi('system:notice:edit')")
+    @Log(title = "通知公告", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody SysNotice notice)
+    {
+        notice.setUpdateBy(getUsername());
+        return toAjax(noticeService.updateNotice(notice));
+    }
+
+    /**
+     * 删除通知公告
+     */
+    @PreAuthorize("@ss.hasPermi('system:notice:remove')")
+    @Log(title = "通知公告", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{noticeIds}")
+    public AjaxResult remove(@PathVariable Long[] noticeIds)
+    {
+        return toAjax(noticeService.deleteNoticeByIds(noticeIds));
+    }
+}

+ 129 - 0
aegis-admin/src/main/java/com/aegis/web/controller/system/SysPostController.java

@@ -0,0 +1,129 @@
+package com.aegis.web.controller.system;
+
+import java.util.List;
+import javax.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+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 com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.page.TableDataInfo;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.common.utils.poi.ExcelUtil;
+import com.aegis.system.domain.SysPost;
+import com.aegis.system.service.ISysPostService;
+
+/**
+ * 岗位信息操作处理
+ * 
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/system/post")
+public class SysPostController extends BaseController
+{
+    @Autowired
+    private ISysPostService postService;
+
+    /**
+     * 获取岗位列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:post:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(SysPost post)
+    {
+        startPage();
+        List<SysPost> list = postService.selectPostList(post);
+        return getDataTable(list);
+    }
+    
+    @Log(title = "岗位管理", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('system:post:export')")
+    @PostMapping("/export")
+    public void export(HttpServletResponse response, SysPost post)
+    {
+        List<SysPost> list = postService.selectPostList(post);
+        ExcelUtil<SysPost> util = new ExcelUtil<SysPost>(SysPost.class);
+        util.exportExcel(response, list, "岗位数据");
+    }
+
+    /**
+     * 根据岗位编号获取详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('system:post:query')")
+    @GetMapping(value = "/{postId}")
+    public AjaxResult getInfo(@PathVariable Long postId)
+    {
+        return success(postService.selectPostById(postId));
+    }
+
+    /**
+     * 新增岗位
+     */
+    @PreAuthorize("@ss.hasPermi('system:post:add')")
+    @Log(title = "岗位管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody SysPost post)
+    {
+        if (!postService.checkPostNameUnique(post))
+        {
+            return error("新增岗位'" + post.getPostName() + "'失败,岗位名称已存在");
+        }
+        else if (!postService.checkPostCodeUnique(post))
+        {
+            return error("新增岗位'" + post.getPostName() + "'失败,岗位编码已存在");
+        }
+        post.setCreateBy(getUsername());
+        return toAjax(postService.insertPost(post));
+    }
+
+    /**
+     * 修改岗位
+     */
+    @PreAuthorize("@ss.hasPermi('system:post:edit')")
+    @Log(title = "岗位管理", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody SysPost post)
+    {
+        if (!postService.checkPostNameUnique(post))
+        {
+            return error("修改岗位'" + post.getPostName() + "'失败,岗位名称已存在");
+        }
+        else if (!postService.checkPostCodeUnique(post))
+        {
+            return error("修改岗位'" + post.getPostName() + "'失败,岗位编码已存在");
+        }
+        post.setUpdateBy(getUsername());
+        return toAjax(postService.updatePost(post));
+    }
+
+    /**
+     * 删除岗位
+     */
+    @PreAuthorize("@ss.hasPermi('system:post:remove')")
+    @Log(title = "岗位管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{postIds}")
+    public AjaxResult remove(@PathVariable Long[] postIds)
+    {
+        return toAjax(postService.deletePostByIds(postIds));
+    }
+
+    /**
+     * 获取岗位选择框列表
+     */
+    @GetMapping("/optionselect")
+    public AjaxResult optionselect()
+    {
+        List<SysPost> posts = postService.selectPostAll();
+        return success(posts);
+    }
+}

+ 148 - 0
aegis-admin/src/main/java/com/aegis/web/controller/system/SysProfileController.java

@@ -0,0 +1,148 @@
+package com.aegis.web.controller.system;
+
+import java.util.Map;
+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.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+import com.aegis.common.annotation.Log;
+import com.aegis.common.config.RuoYiConfig;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.domain.entity.SysUser;
+import com.aegis.common.core.domain.model.LoginUser;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.common.utils.DateUtils;
+import com.aegis.common.utils.SecurityUtils;
+import com.aegis.common.utils.StringUtils;
+import com.aegis.common.utils.file.FileUploadUtils;
+import com.aegis.common.utils.file.FileUtils;
+import com.aegis.common.utils.file.MimeTypeUtils;
+import com.aegis.framework.web.service.TokenService;
+import com.aegis.system.service.ISysUserService;
+
+/**
+ * 个人信息 业务处理
+ * 
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/system/user/profile")
+public class SysProfileController extends BaseController
+{
+    @Autowired
+    private ISysUserService userService;
+
+    @Autowired
+    private TokenService tokenService;
+
+    /**
+     * 个人信息
+     */
+    @GetMapping
+    public AjaxResult profile()
+    {
+        LoginUser loginUser = getLoginUser();
+        SysUser user = loginUser.getUser();
+        AjaxResult ajax = AjaxResult.success(user);
+        ajax.put("roleGroup", userService.selectUserRoleGroup(loginUser.getUsername()));
+        ajax.put("postGroup", userService.selectUserPostGroup(loginUser.getUsername()));
+        return ajax;
+    }
+
+    /**
+     * 修改用户
+     */
+    @Log(title = "个人信息", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult updateProfile(@RequestBody SysUser user)
+    {
+        LoginUser loginUser = getLoginUser();
+        SysUser currentUser = loginUser.getUser();
+        currentUser.setNickName(user.getNickName());
+        currentUser.setEmail(user.getEmail());
+        currentUser.setPhonenumber(user.getPhonenumber());
+        currentUser.setSex(user.getSex());
+        if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(currentUser))
+        {
+            return error("修改用户'" + loginUser.getUsername() + "'失败,手机号码已存在");
+        }
+        if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(currentUser))
+        {
+            return error("修改用户'" + loginUser.getUsername() + "'失败,邮箱账号已存在");
+        }
+        if (userService.updateUserProfile(currentUser) > 0)
+        {
+            // 更新缓存用户信息
+            tokenService.setLoginUser(loginUser);
+            return success();
+        }
+        return error("修改个人信息异常,请联系管理员");
+    }
+
+    /**
+     * 重置密码
+     */
+    @Log(title = "个人信息", businessType = BusinessType.UPDATE)
+    @PutMapping("/updatePwd")
+    public AjaxResult updatePwd(@RequestBody Map<String, String> params)
+    {
+        String oldPassword = params.get("oldPassword");
+        String newPassword = params.get("newPassword");
+        LoginUser loginUser = getLoginUser();
+        Long userId = loginUser.getUserId();
+        String password = loginUser.getPassword();
+        if (!SecurityUtils.matchesPassword(oldPassword, password))
+        {
+            return error("修改密码失败,旧密码错误");
+        }
+        if (SecurityUtils.matchesPassword(newPassword, password))
+        {
+            return error("新密码不能与旧密码相同");
+        }
+        newPassword = SecurityUtils.encryptPassword(newPassword);
+        if (userService.resetUserPwd(userId, newPassword) > 0)
+        {
+            // 更新缓存用户密码&密码最后更新时间
+            loginUser.getUser().setPwdUpdateDate(DateUtils.getNowDate());
+            loginUser.getUser().setPassword(newPassword);
+            tokenService.setLoginUser(loginUser);
+            return success();
+        }
+        return error("修改密码异常,请联系管理员");
+    }
+
+    /**
+     * 头像上传
+     */
+    @Log(title = "用户头像", businessType = BusinessType.UPDATE)
+    @PostMapping("/avatar")
+    public AjaxResult avatar(@RequestParam("avatarfile") MultipartFile file) throws Exception
+    {
+        if (!file.isEmpty())
+        {
+            LoginUser loginUser = getLoginUser();
+            String avatar = FileUploadUtils.upload(RuoYiConfig.getAvatarPath(), file, MimeTypeUtils.IMAGE_EXTENSION, true);
+            if (userService.updateUserAvatar(loginUser.getUserId(), avatar))
+            {
+                String oldAvatar = loginUser.getUser().getAvatar();
+                if (StringUtils.isNotEmpty(oldAvatar))
+                {
+                    FileUtils.deleteFile(RuoYiConfig.getProfile() + FileUtils.stripPrefix(oldAvatar));
+                }
+                AjaxResult ajax = AjaxResult.success();
+                ajax.put("imgUrl", avatar);
+                // 更新缓存用户头像
+                loginUser.getUser().setAvatar(avatar);
+                tokenService.setLoginUser(loginUser);
+                return ajax;
+            }
+        }
+        return error("上传图片异常,请联系管理员");
+    }
+}

+ 38 - 0
aegis-admin/src/main/java/com/aegis/web/controller/system/SysRegisterController.java

@@ -0,0 +1,38 @@
+package com.aegis.web.controller.system;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.domain.model.RegisterBody;
+import com.aegis.common.utils.StringUtils;
+import com.aegis.framework.web.service.SysRegisterService;
+import com.aegis.system.service.ISysConfigService;
+
+/**
+ * 注册验证
+ * 
+ * @author ruoyi
+ */
+@RestController
+public class SysRegisterController extends BaseController
+{
+    @Autowired
+    private SysRegisterService registerService;
+
+    @Autowired
+    private ISysConfigService configService;
+
+    @PostMapping("/register")
+    public AjaxResult register(@RequestBody RegisterBody user)
+    {
+        if (!("true".equals(configService.selectConfigByKey("sys.account.registerUser"))))
+        {
+            return error("当前系统没有开启注册功能!");
+        }
+        String msg = registerService.register(user);
+        return StringUtils.isEmpty(msg) ? success() : error(msg);
+    }
+}

+ 262 - 0
aegis-admin/src/main/java/com/aegis/web/controller/system/SysRoleController.java

@@ -0,0 +1,262 @@
+package com.aegis.web.controller.system;
+
+import java.util.List;
+import javax.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+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 com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.domain.entity.SysDept;
+import com.aegis.common.core.domain.entity.SysRole;
+import com.aegis.common.core.domain.entity.SysUser;
+import com.aegis.common.core.domain.model.LoginUser;
+import com.aegis.common.core.page.TableDataInfo;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.common.utils.StringUtils;
+import com.aegis.common.utils.poi.ExcelUtil;
+import com.aegis.framework.web.service.SysPermissionService;
+import com.aegis.framework.web.service.TokenService;
+import com.aegis.system.domain.SysUserRole;
+import com.aegis.system.service.ISysDeptService;
+import com.aegis.system.service.ISysRoleService;
+import com.aegis.system.service.ISysUserService;
+
+/**
+ * 角色信息
+ * 
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/system/role")
+public class SysRoleController extends BaseController
+{
+    @Autowired
+    private ISysRoleService roleService;
+
+    @Autowired
+    private TokenService tokenService;
+
+    @Autowired
+    private SysPermissionService permissionService;
+
+    @Autowired
+    private ISysUserService userService;
+
+    @Autowired
+    private ISysDeptService deptService;
+
+    @PreAuthorize("@ss.hasPermi('system:role:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(SysRole role)
+    {
+        startPage();
+        List<SysRole> list = roleService.selectRoleList(role);
+        return getDataTable(list);
+    }
+
+    @Log(title = "角色管理", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('system:role:export')")
+    @PostMapping("/export")
+    public void export(HttpServletResponse response, SysRole role)
+    {
+        List<SysRole> list = roleService.selectRoleList(role);
+        ExcelUtil<SysRole> util = new ExcelUtil<SysRole>(SysRole.class);
+        util.exportExcel(response, list, "角色数据");
+    }
+
+    /**
+     * 根据角色编号获取详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:query')")
+    @GetMapping(value = "/{roleId}")
+    public AjaxResult getInfo(@PathVariable Long roleId)
+    {
+        roleService.checkRoleDataScope(roleId);
+        return success(roleService.selectRoleById(roleId));
+    }
+
+    /**
+     * 新增角色
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:add')")
+    @Log(title = "角色管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody SysRole role)
+    {
+        if (!roleService.checkRoleNameUnique(role))
+        {
+            return error("新增角色'" + role.getRoleName() + "'失败,角色名称已存在");
+        }
+        else if (!roleService.checkRoleKeyUnique(role))
+        {
+            return error("新增角色'" + role.getRoleName() + "'失败,角色权限已存在");
+        }
+        role.setCreateBy(getUsername());
+        return toAjax(roleService.insertRole(role));
+
+    }
+
+    /**
+     * 修改保存角色
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:edit')")
+    @Log(title = "角色管理", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody SysRole role)
+    {
+        roleService.checkRoleAllowed(role);
+        roleService.checkRoleDataScope(role.getRoleId());
+        if (!roleService.checkRoleNameUnique(role))
+        {
+            return error("修改角色'" + role.getRoleName() + "'失败,角色名称已存在");
+        }
+        else if (!roleService.checkRoleKeyUnique(role))
+        {
+            return error("修改角色'" + role.getRoleName() + "'失败,角色权限已存在");
+        }
+        role.setUpdateBy(getUsername());
+        
+        if (roleService.updateRole(role) > 0)
+        {
+            // 更新缓存用户权限
+            LoginUser loginUser = getLoginUser();
+            if (StringUtils.isNotNull(loginUser.getUser()) && !loginUser.getUser().isAdmin())
+            {
+                loginUser.setUser(userService.selectUserByUserName(loginUser.getUser().getUserName()));
+                loginUser.setPermissions(permissionService.getMenuPermission(loginUser.getUser()));
+                tokenService.setLoginUser(loginUser);
+            }
+            return success();
+        }
+        return error("修改角色'" + role.getRoleName() + "'失败,请联系管理员");
+    }
+
+    /**
+     * 修改保存数据权限
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:edit')")
+    @Log(title = "角色管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/dataScope")
+    public AjaxResult dataScope(@RequestBody SysRole role)
+    {
+        roleService.checkRoleAllowed(role);
+        roleService.checkRoleDataScope(role.getRoleId());
+        return toAjax(roleService.authDataScope(role));
+    }
+
+    /**
+     * 状态修改
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:edit')")
+    @Log(title = "角色管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/changeStatus")
+    public AjaxResult changeStatus(@RequestBody SysRole role)
+    {
+        roleService.checkRoleAllowed(role);
+        roleService.checkRoleDataScope(role.getRoleId());
+        role.setUpdateBy(getUsername());
+        return toAjax(roleService.updateRoleStatus(role));
+    }
+
+    /**
+     * 删除角色
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:remove')")
+    @Log(title = "角色管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{roleIds}")
+    public AjaxResult remove(@PathVariable Long[] roleIds)
+    {
+        return toAjax(roleService.deleteRoleByIds(roleIds));
+    }
+
+    /**
+     * 获取角色选择框列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:query')")
+    @GetMapping("/optionselect")
+    public AjaxResult optionselect()
+    {
+        return success(roleService.selectRoleAll());
+    }
+
+    /**
+     * 查询已分配用户角色列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:list')")
+    @GetMapping("/authUser/allocatedList")
+    public TableDataInfo allocatedList(SysUser user)
+    {
+        startPage();
+        List<SysUser> list = userService.selectAllocatedList(user);
+        return getDataTable(list);
+    }
+
+    /**
+     * 查询未分配用户角色列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:list')")
+    @GetMapping("/authUser/unallocatedList")
+    public TableDataInfo unallocatedList(SysUser user)
+    {
+        startPage();
+        List<SysUser> list = userService.selectUnallocatedList(user);
+        return getDataTable(list);
+    }
+
+    /**
+     * 取消授权用户
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:edit')")
+    @Log(title = "角色管理", businessType = BusinessType.GRANT)
+    @PutMapping("/authUser/cancel")
+    public AjaxResult cancelAuthUser(@RequestBody SysUserRole userRole)
+    {
+        return toAjax(roleService.deleteAuthUser(userRole));
+    }
+
+    /**
+     * 批量取消授权用户
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:edit')")
+    @Log(title = "角色管理", businessType = BusinessType.GRANT)
+    @PutMapping("/authUser/cancelAll")
+    public AjaxResult cancelAuthUserAll(Long roleId, Long[] userIds)
+    {
+        return toAjax(roleService.deleteAuthUsers(roleId, userIds));
+    }
+
+    /**
+     * 批量选择用户授权
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:edit')")
+    @Log(title = "角色管理", businessType = BusinessType.GRANT)
+    @PutMapping("/authUser/selectAll")
+    public AjaxResult selectAuthUserAll(Long roleId, Long[] userIds)
+    {
+        roleService.checkRoleDataScope(roleId);
+        return toAjax(roleService.insertAuthUsers(roleId, userIds));
+    }
+
+    /**
+     * 获取对应角色部门树列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:query')")
+    @GetMapping(value = "/deptTree/{roleId}")
+    public AjaxResult deptTree(@PathVariable("roleId") Long roleId)
+    {
+        AjaxResult ajax = AjaxResult.success();
+        ajax.put("checkedKeys", deptService.selectDeptListByRoleId(roleId));
+        ajax.put("depts", deptService.selectDeptTreeList(new SysDept()));
+        return ajax;
+    }
+}

+ 256 - 0
aegis-admin/src/main/java/com/aegis/web/controller/system/SysUserController.java

@@ -0,0 +1,256 @@
+package com.aegis.web.controller.system;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.commons.lang3.ArrayUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+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 org.springframework.web.multipart.MultipartFile;
+import com.aegis.common.annotation.Log;
+import com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.AjaxResult;
+import com.aegis.common.core.domain.entity.SysDept;
+import com.aegis.common.core.domain.entity.SysRole;
+import com.aegis.common.core.domain.entity.SysUser;
+import com.aegis.common.core.page.TableDataInfo;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.common.utils.SecurityUtils;
+import com.aegis.common.utils.StringUtils;
+import com.aegis.common.utils.poi.ExcelUtil;
+import com.aegis.system.service.ISysDeptService;
+import com.aegis.system.service.ISysPostService;
+import com.aegis.system.service.ISysRoleService;
+import com.aegis.system.service.ISysUserService;
+
+/**
+ * 用户信息
+ * 
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/system/user")
+public class SysUserController extends BaseController
+{
+    @Autowired
+    private ISysUserService userService;
+
+    @Autowired
+    private ISysRoleService roleService;
+
+    @Autowired
+    private ISysDeptService deptService;
+
+    @Autowired
+    private ISysPostService postService;
+
+    /**
+     * 获取用户列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:user:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(SysUser user)
+    {
+        startPage();
+        List<SysUser> list = userService.selectUserList(user);
+        return getDataTable(list);
+    }
+
+    @Log(title = "用户管理", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('system:user:export')")
+    @PostMapping("/export")
+    public void export(HttpServletResponse response, SysUser user)
+    {
+        List<SysUser> list = userService.selectUserList(user);
+        ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
+        util.exportExcel(response, list, "用户数据");
+    }
+
+    @Log(title = "用户管理", businessType = BusinessType.IMPORT)
+    @PreAuthorize("@ss.hasPermi('system:user:import')")
+    @PostMapping("/importData")
+    public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception
+    {
+        ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
+        List<SysUser> userList = util.importExcel(file.getInputStream());
+        String operName = getUsername();
+        String message = userService.importUser(userList, updateSupport, operName);
+        return success(message);
+    }
+
+    @PostMapping("/importTemplate")
+    public void importTemplate(HttpServletResponse response)
+    {
+        ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
+        util.importTemplateExcel(response, "用户数据");
+    }
+
+    /**
+     * 根据用户编号获取详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('system:user:query')")
+    @GetMapping(value = { "/", "/{userId}" })
+    public AjaxResult getInfo(@PathVariable(value = "userId", required = false) Long userId)
+    {
+        AjaxResult ajax = AjaxResult.success();
+        if (StringUtils.isNotNull(userId))
+        {
+            userService.checkUserDataScope(userId);
+            SysUser sysUser = userService.selectUserById(userId);
+            ajax.put(AjaxResult.DATA_TAG, sysUser);
+            ajax.put("postIds", postService.selectPostListByUserId(userId));
+            ajax.put("roleIds", sysUser.getRoles().stream().map(SysRole::getRoleId).collect(Collectors.toList()));
+        }
+        List<SysRole> roles = roleService.selectRoleAll();
+        ajax.put("roles", SysUser.isAdmin(userId) ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList()));
+        ajax.put("posts", postService.selectPostAll());
+        return ajax;
+    }
+
+    /**
+     * 新增用户
+     */
+    @PreAuthorize("@ss.hasPermi('system:user:add')")
+    @Log(title = "用户管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody SysUser user)
+    {
+        deptService.checkDeptDataScope(user.getDeptId());
+        roleService.checkRoleDataScope(user.getRoleIds());
+        if (!userService.checkUserNameUnique(user))
+        {
+            return error("新增用户'" + user.getUserName() + "'失败,登录账号已存在");
+        }
+        else if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(user))
+        {
+            return error("新增用户'" + user.getUserName() + "'失败,手机号码已存在");
+        }
+        else if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(user))
+        {
+            return error("新增用户'" + user.getUserName() + "'失败,邮箱账号已存在");
+        }
+        user.setCreateBy(getUsername());
+        user.setPassword(SecurityUtils.encryptPassword(user.getPassword()));
+        return toAjax(userService.insertUser(user));
+    }
+
+    /**
+     * 修改用户
+     */
+    @PreAuthorize("@ss.hasPermi('system:user:edit')")
+    @Log(title = "用户管理", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody SysUser user)
+    {
+        userService.checkUserAllowed(user);
+        userService.checkUserDataScope(user.getUserId());
+        deptService.checkDeptDataScope(user.getDeptId());
+        roleService.checkRoleDataScope(user.getRoleIds());
+        if (!userService.checkUserNameUnique(user))
+        {
+            return error("修改用户'" + user.getUserName() + "'失败,登录账号已存在");
+        }
+        else if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(user))
+        {
+            return error("修改用户'" + user.getUserName() + "'失败,手机号码已存在");
+        }
+        else if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(user))
+        {
+            return error("修改用户'" + user.getUserName() + "'失败,邮箱账号已存在");
+        }
+        user.setUpdateBy(getUsername());
+        return toAjax(userService.updateUser(user));
+    }
+
+    /**
+     * 删除用户
+     */
+    @PreAuthorize("@ss.hasPermi('system:user:remove')")
+    @Log(title = "用户管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{userIds}")
+    public AjaxResult remove(@PathVariable Long[] userIds)
+    {
+        if (ArrayUtils.contains(userIds, getUserId()))
+        {
+            return error("当前用户不能删除");
+        }
+        return toAjax(userService.deleteUserByIds(userIds));
+    }
+
+    /**
+     * 重置密码
+     */
+    @PreAuthorize("@ss.hasPermi('system:user:resetPwd')")
+    @Log(title = "用户管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/resetPwd")
+    public AjaxResult resetPwd(@RequestBody SysUser user)
+    {
+        userService.checkUserAllowed(user);
+        userService.checkUserDataScope(user.getUserId());
+        user.setPassword(SecurityUtils.encryptPassword(user.getPassword()));
+        user.setUpdateBy(getUsername());
+        return toAjax(userService.resetPwd(user));
+    }
+
+    /**
+     * 状态修改
+     */
+    @PreAuthorize("@ss.hasPermi('system:user:edit')")
+    @Log(title = "用户管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/changeStatus")
+    public AjaxResult changeStatus(@RequestBody SysUser user)
+    {
+        userService.checkUserAllowed(user);
+        userService.checkUserDataScope(user.getUserId());
+        user.setUpdateBy(getUsername());
+        return toAjax(userService.updateUserStatus(user));
+    }
+
+    /**
+     * 根据用户编号获取授权角色
+     */
+    @PreAuthorize("@ss.hasPermi('system:user:query')")
+    @GetMapping("/authRole/{userId}")
+    public AjaxResult authRole(@PathVariable("userId") Long userId)
+    {
+        AjaxResult ajax = AjaxResult.success();
+        SysUser user = userService.selectUserById(userId);
+        List<SysRole> roles = roleService.selectRolesByUserId(userId);
+        ajax.put("user", user);
+        ajax.put("roles", SysUser.isAdmin(userId) ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList()));
+        return ajax;
+    }
+
+    /**
+     * 用户授权角色
+     */
+    @PreAuthorize("@ss.hasPermi('system:user:edit')")
+    @Log(title = "用户管理", businessType = BusinessType.GRANT)
+    @PutMapping("/authRole")
+    public AjaxResult insertAuthRole(Long userId, Long[] roleIds)
+    {
+        userService.checkUserDataScope(userId);
+        roleService.checkRoleDataScope(roleIds);
+        userService.insertUserAuth(userId, roleIds);
+        return success();
+    }
+
+    /**
+     * 获取部门树列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:user:list')")
+    @GetMapping("/deptTree")
+    public AjaxResult deptTree(SysDept dept)
+    {
+        return success(deptService.selectDeptTreeList(dept));
+    }
+}

+ 183 - 0
aegis-admin/src/main/java/com/aegis/web/controller/tool/TestController.java

@@ -0,0 +1,183 @@
+package com.aegis.web.controller.tool;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+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 com.aegis.common.core.controller.BaseController;
+import com.aegis.common.core.domain.R;
+import com.aegis.common.utils.StringUtils;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiImplicitParams;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import io.swagger.annotations.ApiOperation;
+
+/**
+ * swagger 用户测试方法
+ * 
+ * @author ruoyi
+ */
+@Api("用户信息管理")
+@RestController
+@RequestMapping("/test/user")
+public class TestController extends BaseController
+{
+    private final static Map<Integer, UserEntity> users = new LinkedHashMap<Integer, UserEntity>();
+    {
+        users.put(1, new UserEntity(1, "admin", "admin123", "15888888888"));
+        users.put(2, new UserEntity(2, "ry", "admin123", "15666666666"));
+    }
+
+    @ApiOperation("获取用户列表")
+    @GetMapping("/list")
+    public R<List<UserEntity>> userList()
+    {
+        List<UserEntity> userList = new ArrayList<UserEntity>(users.values());
+        return R.ok(userList);
+    }
+
+    @ApiOperation("获取用户详细")
+    @ApiImplicitParam(name = "userId", value = "用户ID", required = true, dataType = "int", paramType = "path", dataTypeClass = Integer.class)
+    @GetMapping("/{userId}")
+    public R<UserEntity> getUser(@PathVariable Integer userId)
+    {
+        if (!users.isEmpty() && users.containsKey(userId))
+        {
+            return R.ok(users.get(userId));
+        }
+        else
+        {
+            return R.fail("用户不存在");
+        }
+    }
+
+    @ApiOperation("新增用户")
+    @ApiImplicitParams({
+        @ApiImplicitParam(name = "userId", value = "用户id", dataType = "Integer", dataTypeClass = Integer.class),
+        @ApiImplicitParam(name = "username", value = "用户名称", dataType = "String", dataTypeClass = String.class),
+        @ApiImplicitParam(name = "password", value = "用户密码", dataType = "String", dataTypeClass = String.class),
+        @ApiImplicitParam(name = "mobile", value = "用户手机", dataType = "String", dataTypeClass = String.class)
+    })
+    @PostMapping("/save")
+    public R<String> save(UserEntity user)
+    {
+        if (StringUtils.isNull(user) || StringUtils.isNull(user.getUserId()))
+        {
+            return R.fail("用户ID不能为空");
+        }
+        users.put(user.getUserId(), user);
+        return R.ok();
+    }
+
+    @ApiOperation("更新用户")
+    @PutMapping("/update")
+    public R<String> update(@RequestBody UserEntity user)
+    {
+        if (StringUtils.isNull(user) || StringUtils.isNull(user.getUserId()))
+        {
+            return R.fail("用户ID不能为空");
+        }
+        if (users.isEmpty() || !users.containsKey(user.getUserId()))
+        {
+            return R.fail("用户不存在");
+        }
+        users.remove(user.getUserId());
+        users.put(user.getUserId(), user);
+        return R.ok();
+    }
+
+    @ApiOperation("删除用户信息")
+    @ApiImplicitParam(name = "userId", value = "用户ID", required = true, dataType = "int", paramType = "path", dataTypeClass = Integer.class)
+    @DeleteMapping("/{userId}")
+    public R<String> delete(@PathVariable Integer userId)
+    {
+        if (!users.isEmpty() && users.containsKey(userId))
+        {
+            users.remove(userId);
+            return R.ok();
+        }
+        else
+        {
+            return R.fail("用户不存在");
+        }
+    }
+}
+
+@ApiModel(value = "UserEntity", description = "用户实体")
+class UserEntity
+{
+    @ApiModelProperty("用户ID")
+    private Integer userId;
+
+    @ApiModelProperty("用户名称")
+    private String username;
+
+    @ApiModelProperty("用户密码")
+    private String password;
+
+    @ApiModelProperty("用户手机")
+    private String mobile;
+
+    public UserEntity()
+    {
+
+    }
+
+    public UserEntity(Integer userId, String username, String password, String mobile)
+    {
+        this.userId = userId;
+        this.username = username;
+        this.password = password;
+        this.mobile = mobile;
+    }
+
+    public Integer getUserId()
+    {
+        return userId;
+    }
+
+    public void setUserId(Integer userId)
+    {
+        this.userId = userId;
+    }
+
+    public String getUsername()
+    {
+        return username;
+    }
+
+    public void setUsername(String username)
+    {
+        this.username = username;
+    }
+
+    public String getPassword()
+    {
+        return password;
+    }
+
+    public void setPassword(String password)
+    {
+        this.password = password;
+    }
+
+    public String getMobile()
+    {
+        return mobile;
+    }
+
+    public void setMobile(String mobile)
+    {
+        this.mobile = mobile;
+    }
+}

+ 125 - 0
aegis-admin/src/main/java/com/aegis/web/core/config/SwaggerConfig.java

@@ -0,0 +1,125 @@
+package com.aegis.web.core.config;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import com.aegis.common.config.RuoYiConfig;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.models.auth.In;
+import springfox.documentation.builders.ApiInfoBuilder;
+import springfox.documentation.builders.PathSelectors;
+import springfox.documentation.builders.RequestHandlerSelectors;
+import springfox.documentation.service.ApiInfo;
+import springfox.documentation.service.ApiKey;
+import springfox.documentation.service.AuthorizationScope;
+import springfox.documentation.service.Contact;
+import springfox.documentation.service.SecurityReference;
+import springfox.documentation.service.SecurityScheme;
+import springfox.documentation.spi.DocumentationType;
+import springfox.documentation.spi.service.contexts.SecurityContext;
+import springfox.documentation.spring.web.plugins.Docket;
+
+/**
+ * Swagger2的接口配置
+ * 
+ * @author ruoyi
+ */
+@Configuration
+public class SwaggerConfig
+{
+    /** 系统基础配置 */
+    @Autowired
+    private RuoYiConfig ruoyiConfig;
+
+    /** 是否开启swagger */
+    @Value("${swagger.enabled}")
+    private boolean enabled;
+
+    /** 设置请求的统一前缀 */
+    @Value("${swagger.pathMapping}")
+    private String pathMapping;
+
+    /**
+     * 创建API
+     */
+    @Bean
+    public Docket createRestApi()
+    {
+        return new Docket(DocumentationType.OAS_30)
+                // 是否启用Swagger
+                .enable(enabled)
+                // 用来创建该API的基本信息,展示在文档的页面中(自定义展示的信息)
+                .apiInfo(apiInfo())
+                // 设置哪些接口暴露给Swagger展示
+                .select()
+                // 扫描所有有注解的api,用这种方式更灵活
+                .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
+                // 扫描指定包中的swagger注解
+                // .apis(RequestHandlerSelectors.basePackage("com.ruoyi.project.tool.swagger"))
+                // 扫描所有 .apis(RequestHandlerSelectors.any())
+                .paths(PathSelectors.any())
+                .build()
+                /* 设置安全模式,swagger可以设置访问token */
+                .securitySchemes(securitySchemes())
+                .securityContexts(securityContexts())
+                .pathMapping(pathMapping);
+    }
+
+    /**
+     * 安全模式,这里指定token通过Authorization头请求头传递
+     */
+    private List<SecurityScheme> securitySchemes()
+    {
+        List<SecurityScheme> apiKeyList = new ArrayList<SecurityScheme>();
+        apiKeyList.add(new ApiKey("Authorization", "Authorization", In.HEADER.toValue()));
+        return apiKeyList;
+    }
+
+    /**
+     * 安全上下文
+     */
+    private List<SecurityContext> securityContexts()
+    {
+        List<SecurityContext> securityContexts = new ArrayList<>();
+        securityContexts.add(
+                SecurityContext.builder()
+                        .securityReferences(defaultAuth())
+                        .operationSelector(o -> o.requestMappingPattern().matches("/.*"))
+                        .build());
+        return securityContexts;
+    }
+
+    /**
+     * 默认的安全上引用
+     */
+    private List<SecurityReference> defaultAuth()
+    {
+        AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
+        AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
+        authorizationScopes[0] = authorizationScope;
+        List<SecurityReference> securityReferences = new ArrayList<>();
+        securityReferences.add(new SecurityReference("Authorization", authorizationScopes));
+        return securityReferences;
+    }
+
+    /**
+     * 添加摘要信息
+     */
+    private ApiInfo apiInfo()
+    {
+        // 用ApiInfoBuilder进行定制
+        return new ApiInfoBuilder()
+                // 设置标题
+                .title("标题:Aegis应急指挥系统_接口文档")
+                // 描述
+                .description("描述:Aegis应急指挥系统,用于管理应急事件、调度、预案、资源、值守、知识库、案例库、预测等核心业务模块,支持事件全生命周期管理、应急资源统一管理、预案编制与执行、联动协调调度、7×24小时值守、知识库智能调取、案例自动归档、路网与客流预测等功能")
+                // 作者信息
+                .contact(new Contact(ruoyiConfig.getName(), null, null))
+                // 版本
+                .version("版本号:" + ruoyiConfig.getVersion())
+                .build();
+    }
+}

+ 1 - 0
aegis-admin/src/main/resources/META-INF/spring-devtools.properties

@@ -0,0 +1 @@
+restart.include.json=/com.alibaba.fastjson2.*.jar

+ 61 - 0
aegis-admin/src/main/resources/application-druid.yml

@@ -0,0 +1,61 @@
+# 数据源配置
+spring:
+    datasource:
+        type: com.alibaba.druid.pool.DruidDataSource
+        driverClassName: com.mysql.cj.jdbc.Driver
+        druid:
+            # 主库数据源
+            master:
+                url: jdbc:mysql://mysql:3306/aegis?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                username: root
+                password: aegis
+            # 从库数据源
+            slave:
+                # 从数据源开关/默认关闭
+                enabled: false
+                url: 
+                username: 
+                password: 
+            # 初始连接数
+            initialSize: 5
+            # 最小连接池数量
+            minIdle: 10
+            # 最大连接池数量
+            maxActive: 20
+            # 配置获取连接等待超时的时间
+            maxWait: 60000
+            # 配置连接超时时间
+            connectTimeout: 30000
+            # 配置网络超时时间
+            socketTimeout: 60000
+            # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
+            timeBetweenEvictionRunsMillis: 60000
+            # 配置一个连接在池中最小生存的时间,单位是毫秒
+            minEvictableIdleTimeMillis: 300000
+            # 配置一个连接在池中最大生存的时间,单位是毫秒
+            maxEvictableIdleTimeMillis: 900000
+            # 配置检测连接是否有效
+            validationQuery: SELECT 1 FROM DUAL
+            testWhileIdle: true
+            testOnBorrow: false
+            testOnReturn: false
+            webStatFilter: 
+                enabled: true
+            statViewServlet:
+                enabled: true
+                # 设置白名单,不填则允许所有访问
+                allow:
+                url-pattern: /druid/*
+                # 控制台管理用户名和密码
+                login-username: ruoyi
+                login-password: 123456
+            filter:
+                stat:
+                    enabled: true
+                    # 慢SQL记录
+                    log-slow-sql: true
+                    slow-sql-millis: 1000
+                    merge-sql: true
+                wall:
+                    config:
+                        multi-statement-allow: true

+ 136 - 0
aegis-admin/src/main/resources/application.yml

@@ -0,0 +1,136 @@
+# 项目相关配置
+ruoyi:
+  # 名称
+  name: Aegis
+  # 版本
+  version: 1.0.0
+  # 版权年份
+  copyrightYear: 2025
+  # 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
+  profile: app/upload
+  # 获取ip地址开关
+  addressEnabled: false
+  # 验证码类型 math 数字计算 char 字符验证
+  captchaType: math
+
+# 开发环境配置
+server:
+  # 服务器的HTTP端口,默认为8080
+  port: 8080
+  servlet:
+    # 应用的访问路径
+    context-path: /
+  tomcat:
+    # tomcat的URI编码
+    uri-encoding: UTF-8
+    # 连接数满后的排队数,默认为100
+    accept-count: 1000
+    threads:
+      # tomcat最大线程数,默认为200
+      max: 800
+      # Tomcat启动初始化的线程数,默认值10
+      min-spare: 100
+
+# 日志配置
+logging:
+  level:
+    com.ruoyi: debug
+    org.springframework: warn
+
+# 用户配置
+user:
+  password:
+    # 密码最大错误次数
+    maxRetryCount: 5
+    # 密码锁定时间(默认10分钟)
+    lockTime: 10
+
+# Spring配置
+spring:
+  # 资源信息
+  messages:
+    # 国际化资源文件路径
+    basename: i18n/messages
+  profiles:
+    active: druid
+  # 文件上传
+  servlet:
+    multipart:
+      # 单个文件大小
+      max-file-size: 10MB
+      # 设置总上传的文件大小
+      max-request-size: 20MB
+  # 服务模块
+  devtools:
+    restart:
+      # 热部署开关
+      enabled: true
+  # redis 配置
+  redis:
+    # 地址
+    host: redis
+    # 端口,默认为6379
+    port: 6379
+    # 数据库索引
+    database: 0
+    # 密码
+    password:
+    # 连接超时时间
+    timeout: 10s
+    lettuce:
+      pool:
+        # 连接池中的最小空闲连接
+        min-idle: 0
+        # 连接池中的最大空闲连接
+        max-idle: 8
+        # 连接池的最大数据库连接数
+        max-active: 8
+        # #连接池最大阻塞等待时间(使用负值表示没有限制)
+        max-wait: -1ms
+
+# token配置
+token:
+  # 令牌自定义标识
+  header: Authorization
+  # 令牌密钥
+  secret: abcdefghijklmnopqrstuvwxyz
+  # 令牌有效期(默认30分钟)
+  expireTime: 30
+
+# MyBatis配置
+mybatis:
+  # 搜索指定包别名
+  typeAliasesPackage: com.aegis.**.domain
+  # 配置mapper的扫描,找到所有的mapper.xml映射文件
+  mapperLocations: classpath*:mapper/**/*Mapper.xml
+  # 加载全局的配置文件
+  configLocation: classpath:mybatis/mybatis-config.xml
+
+# PageHelper分页插件
+pagehelper:
+  helperDialect: mysql
+  supportMethodsArguments: true
+  params: count=countSql
+
+# Swagger配置
+swagger:
+  # 是否开启swagger
+  enabled: true
+  # 请求前缀
+  pathMapping: /dev-api
+
+# 防盗链配置
+referer:
+  # 防盗链开关
+  enabled: false
+  # 允许的域名列表
+  allowed-domains: localhost,127.0.0.1,ruoyi.vip,www.ruoyi.vip
+
+# 防止XSS攻击
+xss:
+  # 过滤开关
+  enabled: true
+  # 排除链接(多个用逗号分隔)
+  excludes: /system/notice
+  # 匹配链接
+  urlPatterns: /system/*,/monitor/*,/tool/*

+ 24 - 0
aegis-admin/src/main/resources/banner.txt

@@ -0,0 +1,24 @@
+Application Version: ${ruoyi.version}
+Spring Boot Version: ${spring-boot.version}
+////////////////////////////////////////////////////////////////////
+//                          _ooOoo_                               //
+//                         o8888888o                              //
+//                         88" . "88                              //
+//                         (| ^_^ |)                              //
+//                         O\  =  /O                              //
+//                      ____/`---'\____                           //
+//                    .'  \\|     |//  `.                         //
+//                   /  \\|||  :  |||//  \                        //
+//                  /  _||||| -:- |||||-  \                       //
+//                  |   | \\\  -  /// |   |                       //
+//                  | \_|  ''\---/''  |   |                       //
+//                  \  .-\__  `-`  ___/-. /                       //
+//                ___`. .'  /--.--\  `. . ___                     //
+//              ."" '<  `.___\_<|>_/___.'  >'"".                  //
+//            | | :  `- \`.;`\ _ /`;.`/ - ` : | |                 //
+//            \  \ `-.   \_ __\ /__ _/   .-` /  /                 //
+//      ========`-.____`-.___\_____/___.-`____.-'========         //
+//                           `=---='                              //
+//      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^        //
+//             佛祖保佑       永不宕机      永无BUG               //
+////////////////////////////////////////////////////////////////////

+ 38 - 0
aegis-admin/src/main/resources/i18n/messages.properties

@@ -0,0 +1,38 @@
+#错误消息
+not.null=* 必须填写
+user.jcaptcha.error=验证码错误
+user.jcaptcha.expire=验证码已失效
+user.not.exists=用户不存在/密码错误
+user.password.not.match=用户不存在/密码错误
+user.password.retry.limit.count=密码输入错误{0}次
+user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分钟
+user.password.delete=对不起,您的账号已被删除
+user.blocked=用户已封禁,请联系管理员
+role.blocked=角色已封禁,请联系管理员
+login.blocked=很遗憾,访问IP已被列入系统黑名单
+user.logout.success=退出成功
+
+length.not.valid=长度必须在{min}到{max}个字符之间
+
+user.username.not.valid=* 2到20个汉字、字母、数字或下划线组成,且必须以非数字开头
+user.password.not.valid=* 5-50个字符
+ 
+user.email.not.valid=邮箱格式错误
+user.mobile.phone.number.not.valid=手机号格式错误
+user.login.success=登录成功
+user.register.success=注册成功
+user.notfound=请重新登录
+user.forcelogout=管理员强制退出,请重新登录
+user.unknown.error=未知错误,请重新登录
+
+##文件上传消息
+upload.exceed.maxSize=上传的文件大小超出限制的文件大小!<br/>允许的文件最大大小是:{0}MB!
+upload.filename.exceed.length=上传的文件名最长{0}个字符
+
+##权限
+no.permission=您没有数据的权限,请联系管理员添加权限 [{0}]
+no.create.permission=您没有创建数据的权限,请联系管理员添加权限 [{0}]
+no.update.permission=您没有修改数据的权限,请联系管理员添加权限 [{0}]
+no.delete.permission=您没有删除数据的权限,请联系管理员添加权限 [{0}]
+no.export.permission=您没有导出数据的权限,请联系管理员添加权限 [{0}]
+no.view.permission=您没有查看数据的权限,请联系管理员添加权限 [{0}]

+ 93 - 0
aegis-admin/src/main/resources/logback.xml

@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+    <!-- 日志存放路径 -->
+	<property name="log.path" value="/home/ruoyi/logs" />
+    <!-- 日志输出格式 -->
+	<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
+
+	<!-- 控制台输出 -->
+	<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+		<encoder>
+			<pattern>${log.pattern}</pattern>
+		</encoder>
+	</appender>
+	
+	<!-- 系统日志输出 -->
+	<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
+	    <file>${log.path}/sys-info.log</file>
+        <!-- 循环政策:基于时间创建日志文件 -->
+		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志文件名格式 -->
+			<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
+			<!-- 日志最大的历史 60天 -->
+			<maxHistory>60</maxHistory>
+		</rollingPolicy>
+		<encoder>
+			<pattern>${log.pattern}</pattern>
+		</encoder>
+		<filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <!-- 过滤的级别 -->
+            <level>INFO</level>
+            <!-- 匹配时的操作:接收(记录) -->
+            <onMatch>ACCEPT</onMatch>
+            <!-- 不匹配时的操作:拒绝(不记录) -->
+            <onMismatch>DENY</onMismatch>
+        </filter>
+	</appender>
+	
+	<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
+	    <file>${log.path}/sys-error.log</file>
+        <!-- 循环政策:基于时间创建日志文件 -->
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志文件名格式 -->
+            <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
+			<!-- 日志最大的历史 60天 -->
+			<maxHistory>60</maxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <!-- 过滤的级别 -->
+            <level>ERROR</level>
+			<!-- 匹配时的操作:接收(记录) -->
+            <onMatch>ACCEPT</onMatch>
+			<!-- 不匹配时的操作:拒绝(不记录) -->
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+	
+	<!-- 用户访问日志输出  -->
+    <appender name="sys-user" class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<file>${log.path}/sys-user.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 按天回滚 daily -->
+            <fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <!-- 日志最大的历史 60天 -->
+            <maxHistory>60</maxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+    </appender>
+	
+	<!-- 系统模块日志级别控制  -->
+	<logger name="com.aegis" level="info" />
+	<!-- Spring日志级别控制  -->
+	<logger name="org.springframework" level="warn" />
+
+	<root level="info">
+		<appender-ref ref="console" />
+	</root>
+	
+	<!--系统操作日志-->
+    <root level="info">
+        <appender-ref ref="file_info" />
+        <appender-ref ref="file_error" />
+    </root>
+	
+	<!--系统用户操作日志-->
+    <logger name="sys-user" level="info">
+        <appender-ref ref="sys-user"/>
+    </logger>
+</configuration> 

+ 20 - 0
aegis-admin/src/main/resources/mybatis/mybatis-config.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE configuration
+PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-config.dtd">
+<configuration>
+    <!-- 全局参数 -->
+    <settings>
+        <!-- 使全局的映射器启用或禁用缓存 -->
+        <setting name="cacheEnabled"             value="true"   />
+        <!-- 允许JDBC 支持自动生成主键 -->
+        <setting name="useGeneratedKeys"         value="true"   />
+        <!-- 配置默认的执行器.SIMPLE就是普通执行器;REUSE执行器会重用预处理语句(prepared statements);BATCH执行器将重用语句并执行批量更新 -->
+        <setting name="defaultExecutorType"      value="SIMPLE" />
+		<!-- 指定 MyBatis 所用日志的具体实现 -->
+        <setting name="logImpl"                  value="SLF4J"  />
+        <!-- 使用驼峰命名法转换字段 -->
+		<!-- <setting name="mapUnderscoreToCamelCase" value="true"/> -->
+	</settings>
+	
+</configuration>

+ 54 - 0
aegis-camera/pom.xml

@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>aegis-command</artifactId>
+        <groupId>com.aegis</groupId>
+        <version>1.0.0</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>aegis-camera</artifactId>
+
+    <description>
+        camera摄像头模块
+    </description>
+
+    <dependencies>
+
+        <!-- 通用工具-->
+        <dependency>
+            <groupId>com.aegis</groupId>
+            <artifactId>aegis-common</artifactId>
+        </dependency>
+
+        <!-- 系统模块,复用组织/用户等能力 -->
+        <dependency>
+            <groupId>com.aegis</groupId>
+            <artifactId>aegis-system</artifactId>
+        </dependency>
+
+        <!-- MyBatis-Plus -->
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-boot-starter</artifactId>
+        </dependency>
+
+        <!-- 测试依赖 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <scope>provided</scope>
+            <optional>true</optional>
+        </dependency>
+
+    </dependencies>
+
+</project>

+ 54 - 0
aegis-camera/src/main/java/com/aegis/camera/config/ZLMediaKitProperties.java

@@ -0,0 +1,54 @@
+package com.aegis.camera.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * ZLMediaKit 相关配置
+ *
+ * 建议在应用配置中增加:
+ *
+ * zlm:
+ *   base-url: http://zlmediakit:8080   # ZLM HTTP API 根地址
+ *   secret: your_secret                # ZLM 配置中的 api.secret
+ *   app: live                          # 默认 app 名称
+ *   vhost: __defaultVhost__           # 默认 vhost
+ *   enable-hls: true
+ *   enable-mp4: false
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "zlm")
+public class ZLMediaKitProperties {
+
+    /**
+     * ZLMediaKit HTTP API 根地址,例如:http://127.0.0.1:8080
+     */
+    private String baseUrl = "http://127.0.0.1:8080";
+
+    /**
+     * 与 ZLMediaKit 配置中的 api.secret 保持一致
+     */
+    private String secret = "";
+
+    /**
+     * 默认应用名(app),通常用 live
+     */
+    private String app = "live";
+
+    /**
+     * 默认虚拟主机
+     */
+    private String vhost = "__defaultVhost__";
+
+    /**
+     * 是否启用 HLS
+     */
+    private boolean enableHls = true;
+
+    /**
+     * 是否启用 MP4 录制
+     */
+    private boolean enableMp4 = false;
+}

+ 58 - 0
aegis-camera/src/main/java/com/aegis/camera/domain/Camera.java

@@ -0,0 +1,58 @@
+package com.aegis.camera.domain;
+
+import java.math.BigDecimal;
+import com.aegis.common.core.domain.BaseEntity;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 摄像头实体类 camera
+ * 
+ * @author aegis
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("camera")
+public class Camera extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 摄像头ID */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /** 摄像头编码/设备ID */
+    private String cameraCode;
+
+    /** 摄像头名称 */
+    private String cameraName;
+
+    /** 协议:rtsp/gb28181/rtmp/onvif */
+    private String protocol;
+
+    /** 所属机构 */
+    private String orgName;
+
+    /** 经度 */
+    private BigDecimal longitude;
+
+    /** 纬度 */
+    private BigDecimal latitude;
+
+    /** 地址 */
+    private String address;
+
+    /** 状态:online/offline */
+    private String status;
+
+    /** 扩展配置(JSON字符串) */
+    private String extraConfig;
+
+    /** 删除标志(0代表存在 2代表删除) */
+    @TableLogic
+    private String delFlag;
+}

+ 15 - 0
aegis-camera/src/main/java/com/aegis/camera/mapper/CameraMapper.java

@@ -0,0 +1,15 @@
+package com.aegis.camera.mapper;
+
+import com.aegis.camera.domain.Camera;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 摄像头Mapper接口
+ * 
+ * @author aegis
+ */
+@Mapper
+public interface CameraMapper extends BaseMapper<Camera> {
+
+}

+ 21 - 0
aegis-camera/src/main/java/com/aegis/camera/service/ICameraService.java

@@ -0,0 +1,21 @@
+package com.aegis.camera.service;
+
+import com.aegis.camera.domain.Camera;
+import com.baomidou.mybatisplus.extension.service.IService;
+import java.util.List;
+
+/**
+ * 摄像头Service接口
+ * 
+ * @author aegis
+ */
+public interface ICameraService extends IService<Camera> {
+
+    /**
+     * 按条件查询摄像头列表
+     * 
+     * @param condition 查询条件
+     * @return 摄像头集合
+     */
+    List<Camera> listByCondition(Camera condition);
+}

+ 35 - 0
aegis-camera/src/main/java/com/aegis/camera/service/StreamProtocolHandler.java

@@ -0,0 +1,35 @@
+package com.aegis.camera.service;
+
+import com.aegis.camera.config.ZLMediaKitProperties;
+import com.aegis.camera.domain.Camera;
+
+/**
+ * 摄像头流协议处理器
+ *
+ * 针对不同协议(RTSP/GB28181/RTMP/ONVIF 等)抽象统一接口,便于扩展。
+ */
+public interface StreamProtocolHandler {
+
+    /**
+     * 协议类型标识(例如:rtsp、gb28181、rtmp、onvif)
+     */
+    String getProtocol();
+
+    /**
+     * 启动指定协议的流。
+     *
+     * @param camera     摄像头实体
+     * @param properties ZLMediaKit 配置
+     * @return true 表示指令下发成功
+     */
+    boolean startStream(Camera camera, ZLMediaKitProperties properties);
+
+    /**
+     * 停止指定协议的流。
+     *
+     * @param camera     摄像头实体
+     * @param properties ZLMediaKit 配置
+     * @return true 表示指令下发成功
+     */
+    boolean stopStream(Camera camera, ZLMediaKitProperties properties);
+}

+ 152 - 0
aegis-camera/src/main/java/com/aegis/camera/service/StreamSessionService.java

@@ -0,0 +1,152 @@
+package com.aegis.camera.service;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+import org.springframework.stereotype.Service;
+
+/**
+ * 流会话管理服务
+ * 用于跟踪活跃的视频流,支持空闲流清理
+ *
+ * @author aegis
+ */
+@Service
+public class StreamSessionService {
+
+    /**
+     * 流会话信息:包含启动时间、最后访问时间和观看者数量
+     */
+    private static class StreamSessionInfo {
+        final long startTime;
+        volatile long lastAccessTime;
+        volatile int watcherCount;
+
+        StreamSessionInfo(long startTime) {
+            this.startTime = startTime;
+            this.lastAccessTime = startTime;
+            this.watcherCount = 0;
+        }
+    }
+
+    /**
+     * 存储活跃流的映射:摄像头ID -> 流会话信息
+     */
+    private final Map<Long, StreamSessionInfo> activeStreams = new ConcurrentHashMap<>();
+
+    /**
+     * 注册一个活跃流
+     *
+     * @param cameraId 摄像头ID
+     */
+    public void registerStream(Long cameraId) {
+        if (cameraId != null) {
+            activeStreams.put(cameraId, new StreamSessionInfo(System.currentTimeMillis()));
+        }
+    }
+
+    /**
+     * 注销一个流(停止跟踪)
+     *
+     * @param cameraId 摄像头ID
+     */
+    public void unregisterStream(Long cameraId) {
+        if (cameraId != null) {
+            activeStreams.remove(cameraId);
+        }
+    }
+
+    /**
+     * 更新指定流的最后访问时间(心跳/活动信号)
+     *
+     * @param cameraId 摄像头ID
+     */
+    public void updateLastAccess(Long cameraId) {
+        if (cameraId != null) {
+            StreamSessionInfo info = activeStreams.get(cameraId);
+            if (info != null) {
+                info.lastAccessTime = System.currentTimeMillis();
+            }
+        }
+    }
+
+    /**
+     * 增加观看者计数(可选,用于更精确的跟踪)
+     *
+     * @param cameraId 摄像头ID
+     */
+    public void incrementWatcher(Long cameraId) {
+        if (cameraId != null) {
+            StreamSessionInfo info = activeStreams.get(cameraId);
+            if (info != null) {
+                info.watcherCount++;
+                info.lastAccessTime = System.currentTimeMillis();
+            }
+        }
+    }
+
+    /**
+     * 减少观看者计数
+     *
+     * @param cameraId 摄像头ID
+     */
+    public void decrementWatcher(Long cameraId) {
+        if (cameraId != null) {
+            StreamSessionInfo info = activeStreams.get(cameraId);
+            if (info != null && info.watcherCount > 0) {
+                info.watcherCount--;
+                info.lastAccessTime = System.currentTimeMillis();
+            }
+        }
+    }
+
+    /**
+     * 获取空闲流列表(仅基于最后访问时间,自愈式策略)
+     *
+     * 策略说明:
+     * 1. 仅使用 lastAccessTime 判断是否空闲,不依赖 watcherCount,避免计数不一致导致永不清理。
+     * 2. 如果前端正常调用 updateLastAccess(如获取播放地址或心跳),流会保持活跃;
+     * 3. 如果前端崩溃或未调用,lastAccessTime 不会更新,超时后会被自动清理(自愈)。
+     *
+     * @param maxIdleMs 最大空闲时间(毫秒),从最后访问时间算起
+     * @return 空闲流的摄像头ID列表
+     */
+    public List<Long> getIdleStreams(long maxIdleMs) {
+        long now = System.currentTimeMillis();
+        return activeStreams.entrySet().stream()
+            .filter(e -> {
+                StreamSessionInfo info = e.getValue();
+                long idle = now - info.lastAccessTime;
+                return idle > maxIdleMs;
+            })
+            .map(Map.Entry::getKey)
+            .collect(Collectors.toList());
+    }
+
+    /**
+     * 检查指定摄像头是否有活跃流
+     *
+     * @param cameraId 摄像头ID
+     * @return true 如果有活跃流,false 否则
+     */
+    public boolean isStreamActive(Long cameraId) {
+        return cameraId != null && activeStreams.containsKey(cameraId);
+    }
+
+    /**
+     * 获取所有活跃流的数量
+     *
+     * @return 活跃流数量
+     */
+    public int getActiveStreamCount() {
+        return activeStreams.size();
+    }
+
+    /**
+     * 清除所有流记录(通常用于系统重启或维护)
+     */
+    public void clearAll() {
+        activeStreams.clear();
+    }
+}

+ 40 - 0
aegis-camera/src/main/java/com/aegis/camera/service/ZLMediaKitService.java

@@ -0,0 +1,40 @@
+package com.aegis.camera.service;
+
+import java.util.Map;
+import com.aegis.camera.domain.Camera;
+
+/**
+ * ZLMediaKit 集成服务
+ *
+ * 负责与 ZLMediaKit 的 HTTP API 交互,启动/停止流并生成播放地址。
+ *
+ * 说明:
+ * - 此接口只定义能力,不直接耦合到具体协议细节(rtsp/gb28181 等);
+ * - 由实现类从 Camera.extraConfig 中解析出协议相关参数。
+ */
+public interface ZLMediaKitService {
+
+    /**
+     * 启动指定摄像头的流(通常是从 RTSP/GB28181 拉流)。
+     *
+     * @param camera 摄像头实体,需包含 protocol 和 extraConfig 等信息
+     * @return true 表示已成功向 ZLMediaKit 下发启动指令(不保证设备一定在线)
+     */
+    boolean startStream(Camera camera);
+
+    /**
+     * 停止指定摄像头的流。
+     *
+     * @param camera 摄像头实体
+     * @return true 表示已成功向 ZLMediaKit 下发停止指令
+     */
+    boolean stopStream(Camera camera);
+
+    /**
+     * 根据摄像头编码获取播放地址(HLS/FLV/WebSocket-FLV 等)。
+     *
+     * @param cameraCode 摄像头编码(通常映射为 ZLM 的 streamId)
+     * @return key 为协议类型(如 hls、flv、wsFlv),value 为对应播放 URL
+     */
+    Map<String, String> getPlayUrls(String cameraCode);
+}

+ 121 - 0
aegis-camera/src/main/java/com/aegis/camera/service/impl/CameraServiceImpl.java

@@ -0,0 +1,121 @@
+package com.aegis.camera.service.impl;
+
+import com.aegis.camera.domain.Camera;
+import com.aegis.camera.mapper.CameraMapper;
+import com.aegis.camera.service.ICameraService;
+import com.aegis.common.exception.ServiceException;
+import com.aegis.common.utils.SecurityUtils;
+import com.aegis.common.utils.StringUtils;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * 摄像头Service业务层处理
+ * 
+ * @author aegis
+ */
+@Service
+public class CameraServiceImpl extends ServiceImpl<CameraMapper, Camera> implements ICameraService {
+
+    @Override
+    public List<Camera> listByCondition(Camera condition) {
+        LambdaQueryWrapper<Camera> wrapper = new LambdaQueryWrapper<>();
+        if (condition == null) {
+            wrapper.orderByDesc(Camera::getCreateTime);
+        } else {
+            wrapper.like(StringUtils.isNotBlank(condition.getCameraName()), Camera::getCameraName, condition.getCameraName());
+            wrapper.like(StringUtils.isNotBlank(condition.getCameraCode()), Camera::getCameraCode, condition.getCameraCode());
+            wrapper.eq(StringUtils.isNotBlank(condition.getProtocol()), Camera::getProtocol, condition.getProtocol());
+            wrapper.eq(StringUtils.isNotBlank(condition.getOrgName()), Camera::getOrgName, condition.getOrgName());
+            wrapper.eq(StringUtils.isNotBlank(condition.getStatus()), Camera::getStatus, condition.getStatus());
+            Object beginTime = condition.getParams().get("beginTime");
+            if (beginTime != null) {
+                wrapper.ge(Camera::getCreateTime, beginTime);
+            }
+            Object endTime = condition.getParams().get("endTime");
+            if (endTime != null) {
+                wrapper.le(Camera::getCreateTime, endTime);
+            }
+            wrapper.orderByDesc(Camera::getCreateTime);
+        }
+        return list(wrapper);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean save(Camera camera) {
+        if (camera == null) {
+            throw new ServiceException("摄像头信息不能为空");
+        }
+        if (StringUtils.isNotBlank(camera.getCameraCode())) {
+            checkCameraCodeUnique(camera.getCameraCode(), null);
+        }
+        applyDefaultValues(camera);
+        String operator = SecurityUtils.getUsername();
+        Date now = new Date();
+        camera.setCreateBy(operator);
+        camera.setCreateTime(now);
+        return super.save(camera);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean updateById(Camera camera) {
+        if (camera == null) {
+            throw new ServiceException("摄像头信息不能为空");
+        }
+        if (StringUtils.isNotBlank(camera.getCameraCode())) {
+            checkCameraCodeUnique(camera.getCameraCode(), camera.getId());
+        }
+        String operator = SecurityUtils.getUsername();
+        Date now = new Date();
+        camera.setUpdateBy(operator);
+        camera.setUpdateTime(now);
+        return super.updateById(camera);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean removeById(Serializable id) {
+        return super.removeById(id);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean removeByIds(Collection<?> idList) {
+        return super.removeByIds(idList);
+    }
+
+    /**
+     * 校验摄像头编码是否唯一
+     */
+    private void checkCameraCodeUnique(String cameraCode, Long excludeId) {
+        LambdaQueryWrapper<Camera> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(Camera::getCameraCode, cameraCode);
+        // 只校验未删除的记录,允许与已逻辑删除的数据重复
+        wrapper.eq(Camera::getDelFlag, "0");
+        if (excludeId != null) {
+            wrapper.ne(Camera::getId, excludeId);
+        }
+        long count = count(wrapper);
+        if (count > 0) {
+            throw new ServiceException("摄像头编码已存在");
+        }
+    }
+
+    /**
+     * 应用默认值
+     */
+    private void applyDefaultValues(Camera camera) {
+        if (StringUtils.isBlank(camera.getStatus())) {
+            camera.setStatus("offline");
+        }
+    }
+}

+ 177 - 0
aegis-camera/src/main/java/com/aegis/camera/service/impl/RtspStreamProtocolHandler.java

@@ -0,0 +1,177 @@
+package com.aegis.camera.service.impl;
+
+import com.aegis.camera.config.ZLMediaKitProperties;
+import com.aegis.camera.domain.Camera;
+import com.aegis.camera.service.StreamProtocolHandler;
+import com.aegis.common.exception.ServiceException;
+import com.aegis.common.utils.StringUtils;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.RequestEntity;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriComponentsBuilder;
+
+/**
+ * RTSP 协议处理器
+ *
+ * 通过 ZLMediaKit 的 addStreamProxy / close_stream 接口实现 RTSP 拉流。
+ */
+@Slf4j
+@Component
+public class RtspStreamProtocolHandler implements StreamProtocolHandler {
+
+    private final RestTemplate restTemplate = new RestTemplate();
+    private final ObjectMapper objectMapper = new ObjectMapper();
+
+    @Override
+    public String getProtocol() {
+        return "rtsp";
+    }
+
+    @Override
+    public boolean startStream(Camera camera, ZLMediaKitProperties properties) {
+        if (camera == null) {
+            throw new ServiceException("摄像头信息不能为空");
+        }
+        String url = extractRtspUrl(camera);
+        if (StringUtils.isBlank(url)) {
+            throw new ServiceException("摄像头扩展配置中 RTSP 地址不能为空");
+        }
+
+        String streamId = camera.getCameraCode();
+        if (StringUtils.isBlank(streamId)) {
+            throw new ServiceException("摄像头编码不能为空,无法作为流标识");
+        }
+
+        URI uri = buildApiUri(properties, "/index/api/addStreamProxy", new HashMap<String, Object>() {{
+            put("vhost", properties.getVhost());
+            put("app", properties.getApp());
+            put("stream", streamId);
+            put("url", url);
+            put("enable_hls", properties.isEnableHls() ? 1 : 0);
+            put("enable_mp4", properties.isEnableMp4() ? 1 : 0);
+            put("rtp_type", 0);
+            put("secret", properties.getSecret());
+        }});
+
+        try {
+            log.info("RTSP startStream 请求:{}", uri);
+            ResponseEntity<Map> response = restTemplate.exchange(
+                new RequestEntity<>(HttpMethod.GET, uri), Map.class);
+            Map<String, Object> body = response.getBody();
+            log.info("RTSP startStream 响应:{}", body);
+            return isOk(body);
+        } catch (Exception e) {
+            log.error("调用 ZLMediaKit 启动 RTSP 流失败, cameraCode={}, url={}", streamId, url, e);
+            throw new ServiceException("调用 ZLMediaKit 启动 RTSP 流失败:" + e.getMessage());
+        }
+    }
+
+    @Override
+    public boolean stopStream(Camera camera, ZLMediaKitProperties properties) {
+        if (camera == null) {
+            throw new ServiceException("摄像头信息不能为空");
+        }
+        String streamId = camera.getCameraCode();
+        if (StringUtils.isBlank(streamId)) {
+            throw new ServiceException("摄像头编码不能为空,无法作为流标识");
+        }
+
+        URI uri = buildApiUri(properties, "/index/api/close_stream", new HashMap<String, Object>() {{
+            put("vhost", properties.getVhost());
+            put("app", properties.getApp());
+            put("stream", streamId);
+            put("force", 1);
+            put("secret", properties.getSecret());
+        }});
+
+        try {
+            log.info("RTSP stopStream 请求:{}", uri);
+            ResponseEntity<Map> response = restTemplate.exchange(
+                new RequestEntity<>(HttpMethod.GET, uri), Map.class);
+            Map<String, Object> body = response.getBody();
+            log.info("RTSP stopStream 响应:{}", body);
+            return isOk(body);
+        } catch (Exception e) {
+            log.error("调用 ZLMediaKit 停止 RTSP 流失败, cameraCode={}", streamId, e);
+            throw new ServiceException("调用 ZLMediaKit 停止 RTSP 流失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 从 Camera.extraConfig 中解析 RTSP 地址。
+     * 约定 extraConfig 为 JSON,对象中包含 "url" 字段:
+     * { "url": "rtsp://user:pass@ip:554/stream", ... }
+     */
+    private String extractRtspUrl(Camera camera) {
+        String extra = camera.getExtraConfig();
+        if (StringUtils.isBlank(extra)) {
+            return null;
+        }
+        try {
+            Map<String, Object> map = objectMapper.readValue(
+                extra, new TypeReference<Map<String, Object>>() {});
+            Object url = map.get("url");
+            return url != null ? String.valueOf(url) : null;
+        } catch (Exception e) {
+            log.warn("解析摄像头扩展配置失败,无法获取 RTSP 地址, cameraId={}, extraConfig={}",
+                camera.getId(), extra, e);
+            return null;
+        }
+    }
+
+    private URI buildApiUri(ZLMediaKitProperties properties, String path, Map<String, Object> params) {
+        String base = properties.getBaseUrl();
+        if (StringUtils.isBlank(base)) {
+            throw new ServiceException("ZLMediaKit base-url 未配置");
+        }
+        String normalizedBase = trimTrailingSlash(base);
+        UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(
+            normalizedBase + path);
+        if (params != null) {
+            params.forEach(builder::queryParam);
+        }
+        try {
+            return builder.build(true).toUri();
+        } catch (IllegalArgumentException e) {
+            throw new ServiceException("构建 ZLMediaKit 接口地址失败:" + e.getMessage());
+        }
+    }
+
+    private String trimTrailingSlash(String url) {
+        if (url == null) {
+            return null;
+        }
+        while (url.endsWith("/")) {
+            url = url.substring(0, url.length() - 1);
+        }
+        return url;
+    }
+
+    /**
+     * 判断 ZLMediaKit 返回是否成功。
+     * 通常 ZLM HTTP API 返回形如:{"code":0, "msg":"success", ...}
+     */
+    @SuppressWarnings("unchecked")
+    private boolean isOk(Map body) {
+        if (body == null) {
+            return false;
+        }
+        Object code = body.get("code");
+        if (code == null) {
+            return false;
+        }
+        try {
+            return Integer.parseInt(code.toString()) == 0;
+        } catch (NumberFormatException e) {
+            return false;
+        }
+    }
+}

+ 94 - 0
aegis-camera/src/main/java/com/aegis/camera/service/impl/ZLMediaKitServiceImpl.java

@@ -0,0 +1,94 @@
+package com.aegis.camera.service.impl;
+
+import com.aegis.camera.config.ZLMediaKitProperties;
+import com.aegis.camera.domain.Camera;
+import com.aegis.camera.service.ZLMediaKitService;
+import com.aegis.camera.service.StreamProtocolHandler;
+import com.aegis.common.exception.ServiceException;
+import com.aegis.common.utils.StringUtils;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * ZLMediaKit 集成实现
+ *
+ * 说明:
+ * - 当前实现主要面向 RTSP 拉流场景,通过 /index/api/addStreamProxy / close_stream 等接口控制流;
+ * - GB28181/RTMP/ONVIF 等协议可在后续根据需要扩展;
+ * - 解析 Camera.extraConfig 时约定为 JSON 字符串,至少包含 "url" 字段(RTSP 地址)。
+ */
+@Slf4j
+@Service
+public class ZLMediaKitServiceImpl implements ZLMediaKitService {
+
+    private static final String DEFAULT_SCHEMA_HTTP = "http";
+
+    @Autowired
+    private ZLMediaKitProperties properties;
+
+    @Autowired
+    private List<StreamProtocolHandler> protocolHandlers;
+
+    @Override
+    public boolean startStream(Camera camera) {
+        if (camera == null) {
+            throw new ServiceException("摄像头信息不能为空");
+        }
+        String protocol = camera.getProtocol();
+        if (StringUtils.isBlank(protocol)) {
+            throw new ServiceException("摄像头协议类型不能为空");
+        }
+        StreamProtocolHandler handler = getHandler(protocol);
+        return handler.startStream(camera, properties);
+    }
+
+    @Override
+    public boolean stopStream(Camera camera) {
+        if (camera == null) {
+            throw new ServiceException("摄像头信息不能为空");
+        }
+        String protocol = camera.getProtocol();
+        if (StringUtils.isBlank(protocol)) {
+            throw new ServiceException("摄像头协议类型不能为空");
+        }
+        StreamProtocolHandler handler = getHandler(protocol);
+        return handler.stopStream(camera, properties);
+    }
+
+    @Override
+    public Map<String, String> getPlayUrls(String cameraCode) {
+        if (StringUtils.isBlank(cameraCode)) {
+            return Collections.emptyMap();
+        }
+        // 返回相对路径,由前端根据自身协议/域名/端口构建完整 URL,避免在后端硬编码外网地址
+        Map<String, String> urls = new HashMap<>();
+        // HLS 相对路径,例如:/live/{cameraCode}/hls.m3u8
+        urls.put("hls", String.format("/%s/%s/%s.m3u8",
+            properties.getApp(), cameraCode, "hls"));
+        // HTTP-FLV 相对路径,例如:/live/{cameraCode}.live.flv
+        urls.put("flv", String.format("/%s/%s.live.flv",
+            properties.getApp(), cameraCode));
+        // WebSocket-FLV 相对路径(前端需根据协议选择 ws/wss)
+        urls.put("wsFlv", String.format("/%s/%s.live.flv",
+            properties.getApp(), cameraCode));
+        return urls;
+    }
+
+    private StreamProtocolHandler getHandler(String protocol) {
+        if (protocolHandlers == null || protocolHandlers.isEmpty()) {
+            throw new ServiceException("未找到任何摄像头协议处理器,请检查 Spring 配置");
+        }
+        return protocolHandlers.stream()
+            .filter(h -> protocol.equalsIgnoreCase(h.getProtocol()))
+            .findFirst()
+            .orElseThrow(() -> new ServiceException("不支持的协议类型: " + protocol));
+    }
+}

+ 83 - 0
aegis-camera/src/main/java/com/aegis/camera/task/StreamCleanupTask.java

@@ -0,0 +1,83 @@
+package com.aegis.camera.task;
+
+import com.aegis.camera.domain.Camera;
+import com.aegis.camera.service.ICameraService;
+import com.aegis.camera.service.StreamSessionService;
+import com.aegis.camera.service.ZLMediaKitService;
+import java.util.List;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+/**
+ * 流清理定时任务
+ * 定期检查并关闭空闲的视频流,释放 ZLMediaKit 资源
+ *
+ * @author aegis
+ */
+@Slf4j
+@Component
+public class StreamCleanupTask {
+
+    @Autowired
+    private StreamSessionService streamSessionService;
+
+    @Autowired
+    private ICameraService cameraService;
+
+    @Autowired
+    private ZLMediaKitService zlMediaKitService;
+
+    /**
+     * 最大空闲时间(毫秒),默认 30 分钟
+     * 超过此时间的流将被自动关闭
+     */
+    private static final long MAX_IDLE_TIME_MS = 30 * 60 * 1000L;
+
+    /**
+     * 每 5 分钟执行一次清理任务
+     */
+    @Scheduled(fixedRate = 5 * 60 * 1000)
+    public void cleanupIdleStreams() {
+        try {
+            List<Long> idleCameraIds = streamSessionService.getIdleStreams(MAX_IDLE_TIME_MS);
+
+            if (!idleCameraIds.isEmpty()) {
+                log.info("发现 {} 个空闲流,开始清理", idleCameraIds.size());
+
+                int successCount = 0;
+                int failCount = 0;
+
+                for (Long cameraId : idleCameraIds) {
+                    try {
+                        Camera camera = cameraService.getById(cameraId);
+                        if (camera != null) {
+                            // 停止 ZLMediaKit 中的流
+                            zlMediaKitService.stopStream(camera);
+                            // 从会话管理中移除
+                            streamSessionService.unregisterStream(cameraId);
+                            successCount++;
+                            log.debug("已清理空闲流,摄像头ID: {}, 编码: {}", cameraId, camera.getCameraCode());
+                        } else {
+                            // 摄像头已不存在,直接移除记录
+                            streamSessionService.unregisterStream(cameraId);
+                            log.warn("摄像头不存在,已移除流记录,摄像头ID: {}", cameraId);
+                        }
+                    } catch (Exception e) {
+                        failCount++;
+                        log.error("清理空闲流失败,摄像头ID: {}", cameraId, e);
+                        // 即使失败也尝试移除记录,避免重复处理
+                        streamSessionService.unregisterStream(cameraId);
+                    }
+                }
+
+                log.info("空闲流清理完成,成功: {}, 失败: {}", successCount, failCount);
+            } else {
+                log.debug("未发现空闲流,当前活跃流数量: {}", streamSessionService.getActiveStreamCount());
+            }
+        } catch (Exception e) {
+            log.error("执行空闲流清理任务时发生异常", e);
+        }
+    }
+}

+ 47 - 0
aegis-case/pom.xml

@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>aegis-command</artifactId>
+        <groupId>com.aegis</groupId>
+        <version>1.0.0</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>aegis-case</artifactId>
+
+    <description>
+        case案例库模块
+    </description>
+
+    <dependencies>
+        <!-- 通用工具-->
+        <dependency>
+            <groupId>com.aegis</groupId>
+            <artifactId>aegis-common</artifactId>
+        </dependency>
+        
+        <!-- 系统模块(用于获取用户信息)-->
+        <dependency>
+            <groupId>com.aegis</groupId>
+            <artifactId>aegis-system</artifactId>
+        </dependency>
+
+        <!-- MyBatis-Plus -->
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-boot-starter</artifactId>
+        </dependency>
+
+        <!-- Lombok -->
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <scope>provided</scope>
+            <optional>true</optional>
+        </dependency>
+    </dependencies>
+
+</project>
+

+ 68 - 0
aegis-case/src/main/java/com/aegis/cases/domain/Case.java

@@ -0,0 +1,68 @@
+package com.aegis.cases.domain;
+
+import com.aegis.common.core.domain.BaseEntity;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+import java.util.Date;
+
+/**
+ * 案例实体类 case
+ *
+ * @author
+ */
+@Getter
+@Setter
+@ToString(callSuper = true)
+@TableName("`case`")
+public class Case extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 案例ID */
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long id;
+
+    /** 案例编号 */
+    private String caseNo;
+
+    /** 案例标题 */
+    private String title;
+
+    /** 事件类型(字典值) */
+    private String eventType;
+
+    /** 事件级别(I/II/III/IV) */
+    private String eventLevel;
+
+    /** 发生地点 */
+    private String location;
+
+    /** 发生时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date occurredTime;
+
+    /** 案例描述 */
+    private String description;
+
+    /** 处置摘要 */
+    private String disposalSummary;
+
+    /** 标签(逗号分隔) */
+    private String tags;
+
+    /** 是否典型案例 */
+    private Boolean isTypical;
+
+    /** 查看次数 */
+    private Integer viewCount;
+
+    /** 删除标志 */
+    private String delFlag;
+}
+

+ 14 - 0
aegis-case/src/main/java/com/aegis/cases/mapper/CaseMapper.java

@@ -0,0 +1,14 @@
+package com.aegis.cases.mapper;
+
+import com.aegis.cases.domain.Case;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 案例 Mapper
+ */
+@Mapper
+public interface CaseMapper extends BaseMapper<Case> {
+
+}
+

+ 57 - 0
aegis-case/src/main/java/com/aegis/cases/service/ICaseService.java

@@ -0,0 +1,57 @@
+package com.aegis.cases.service;
+
+import com.aegis.cases.domain.Case;
+import com.baomidou.mybatisplus.extension.service.IService;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 案例 Service
+ */
+public interface ICaseService extends IService<Case> {
+
+    /**
+     * 条件查询案例列表
+     *
+     * @param title 标题(模糊搜索)
+     * @param eventType 事件类型
+     * @param eventLevel 事件级别
+     * @param tags 标签(逗号分隔)
+     * @param isTypical 是否典型案例
+     * @param beginTime 起始时间
+     * @param endTime 截止时间
+     * @return 案例列表
+     */
+    List<Case> listByCondition(String title, String eventType, String eventLevel, String tags, Boolean isTypical, Date beginTime, Date endTime);
+
+    /**
+     * 标记为典型案例
+     *
+     * @param id 案例ID
+     * @return 是否成功
+     */
+    boolean markTypical(Long id);
+
+    /**
+     * 取消典型案例标记
+     *
+     * @param id 案例ID
+     * @return 是否成功
+     */
+    boolean unmarkTypical(Long id);
+
+    /**
+     * 查询典型案例列表
+     *
+     * @return 典型案例列表
+     */
+    List<Case> listTypicalCases();
+
+    /**
+     * 增加查看次数
+     *
+     * @param id 案例ID
+     */
+    void incrementViewCount(Long id);
+}
+

+ 109 - 0
aegis-case/src/main/java/com/aegis/cases/service/impl/CaseServiceImpl.java

@@ -0,0 +1,109 @@
+package com.aegis.cases.service.impl;
+
+import com.aegis.common.utils.StringUtils;
+import com.aegis.cases.domain.Case;
+import com.aegis.cases.mapper.CaseMapper;
+import com.aegis.cases.service.ICaseService;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.stereotype.Service;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 案例 Service 实现
+ */
+@Service
+public class CaseServiceImpl extends ServiceImpl<CaseMapper, Case> implements ICaseService {
+
+    @Override
+    public List<Case> listByCondition(String title, String eventType, String eventLevel, String tags, Boolean isTypical, Date beginTime, Date endTime) {
+        LambdaQueryWrapper<Case> wrapper = new LambdaQueryWrapper<>();
+        
+        // 标题模糊搜索
+        if (StringUtils.isNotBlank(title)) {
+            wrapper.like(Case::getTitle, title);
+        }
+        
+        // 事件类型筛选
+        if (StringUtils.isNotBlank(eventType)) {
+            wrapper.eq(Case::getEventType, eventType);
+        }
+        
+        // 事件级别筛选
+        if (StringUtils.isNotBlank(eventLevel)) {
+            wrapper.eq(Case::getEventLevel, eventLevel);
+        }
+        
+        // 标签筛选
+        if (StringUtils.isNotBlank(tags)) {
+            String[] tagArray = tags.split(",");
+            for (String tag : tagArray) {
+                if (StringUtils.isNotBlank(tag)) {
+                    wrapper.like(Case::getTags, tag.trim());
+                }
+            }
+        }
+        
+        // 典型案例筛选
+        if (isTypical != null) {
+            wrapper.eq(Case::getIsTypical, isTypical);
+        }
+        
+        // 时间范围(按发生时间)
+        if (beginTime != null) {
+            wrapper.ge(Case::getOccurredTime, beginTime);
+        }
+        if (endTime != null) {
+            wrapper.le(Case::getOccurredTime, endTime);
+        }
+        
+        wrapper.eq(Case::getDelFlag, "0");
+        wrapper.orderByDesc(Case::getOccurredTime);
+        
+        return this.list(wrapper);
+    }
+
+    @Override
+    public boolean markTypical(Long id) {
+        Case caseEntity = this.getById(id);
+        if (caseEntity == null || "2".equals(caseEntity.getDelFlag())) {
+            return false;
+        }
+        caseEntity.setIsTypical(true);
+        caseEntity.setUpdateTime(new Date());
+        return this.updateById(caseEntity);
+    }
+
+    @Override
+    public boolean unmarkTypical(Long id) {
+        Case caseEntity = this.getById(id);
+        if (caseEntity == null || "2".equals(caseEntity.getDelFlag())) {
+            return false;
+        }
+        caseEntity.setIsTypical(false);
+        caseEntity.setUpdateTime(new Date());
+        return this.updateById(caseEntity);
+    }
+
+    @Override
+    public List<Case> listTypicalCases() {
+        LambdaQueryWrapper<Case> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(Case::getIsTypical, true);
+        wrapper.eq(Case::getDelFlag, "0");
+        wrapper.orderByDesc(Case::getOccurredTime);
+        return this.list(wrapper);
+    }
+
+    @Override
+    public void incrementViewCount(Long id) {
+        Case caseEntity = this.getById(id);
+        if (caseEntity != null && !"2".equals(caseEntity.getDelFlag())) {
+            caseEntity.setViewCount((caseEntity.getViewCount() == null ? 0 : caseEntity.getViewCount()) + 1);
+            caseEntity.setUpdateTime(new Date());
+            this.updateById(caseEntity);
+        }
+    }
+}
+

+ 130 - 0
aegis-common/pom.xml

@@ -0,0 +1,130 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>aegis-command</artifactId>
+        <groupId>com.aegis</groupId>
+        <version>1.0.0</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>aegis-common</artifactId>
+
+    <description>
+        common通用工具
+    </description>
+
+    <dependencies>
+
+        <!-- Spring框架基本的核心工具 -->
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-context-support</artifactId>
+        </dependency>
+
+        <!-- SpringWeb模块 -->
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-web</artifactId>
+        </dependency>
+
+        <!-- spring security 安全认证 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-security</artifactId>
+        </dependency>
+
+        <!-- pagehelper 分页插件 -->
+        <dependency>
+            <groupId>com.github.pagehelper</groupId>
+            <artifactId>pagehelper-spring-boot-starter</artifactId>
+        </dependency>
+
+        <!-- 自定义验证注解 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+
+        <!--常用工具类 -->
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+        </dependency>
+  
+        <!-- JSON工具类 -->
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+        </dependency>
+        
+        <!-- 阿里JSON解析器 -->
+        <dependency>
+            <groupId>com.alibaba.fastjson2</groupId>
+            <artifactId>fastjson2</artifactId>
+        </dependency>
+
+        <!-- io常用工具类 -->
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+        </dependency>
+
+        <!-- excel工具 -->
+        <dependency>
+            <groupId>org.apache.poi</groupId>
+            <artifactId>poi-ooxml</artifactId>
+        </dependency>
+
+        <!-- yml解析器 -->
+        <dependency>
+            <groupId>org.yaml</groupId>
+            <artifactId>snakeyaml</artifactId>
+        </dependency>
+
+        <!-- Token生成与解析-->
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt</artifactId>
+        </dependency>
+
+        <!-- Jaxb -->
+        <dependency>
+            <groupId>javax.xml.bind</groupId>
+            <artifactId>jaxb-api</artifactId>
+        </dependency>
+
+        <!-- redis 缓存操作 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
+
+        <!-- pool 对象池 -->
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-pool2</artifactId>
+        </dependency>
+
+        <!-- 解析客户端操作系统、浏览器等 -->
+        <dependency>
+            <groupId>eu.bitwalker</groupId>
+            <artifactId>UserAgentUtils</artifactId>
+        </dependency>
+
+        <!-- servlet包 -->
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>javax.servlet-api</artifactId>
+        </dependency>
+
+        <!-- MyBatis-Plus (for @TableField annotation) -->
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-boot-starter</artifactId>
+        </dependency>
+
+    </dependencies>
+
+</project>

+ 19 - 0
aegis-common/src/main/java/com/aegis/common/annotation/Anonymous.java

@@ -0,0 +1,19 @@
+package com.aegis.common.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 匿名访问不鉴权注解
+ * 
+ * @author ruoyi
+ */
+@Target({ ElementType.METHOD, ElementType.TYPE })
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Anonymous
+{
+}

+ 33 - 0
aegis-common/src/main/java/com/aegis/common/annotation/DataScope.java

@@ -0,0 +1,33 @@
+package com.aegis.common.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 数据权限过滤注解
+ * 
+ * @author ruoyi
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface DataScope
+{
+    /**
+     * 部门表的别名
+     */
+    public String deptAlias() default "";
+
+    /**
+     * 用户表的别名
+     */
+    public String userAlias() default "";
+
+    /**
+     * 权限字符(用于多个角色匹配符合要求的权限)默认根据权限注解@ss获取,多个权限用逗号分隔开来
+     */
+    public String permission() default "";
+}

+ 28 - 0
aegis-common/src/main/java/com/aegis/common/annotation/DataSource.java

@@ -0,0 +1,28 @@
+package com.aegis.common.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import com.aegis.common.enums.DataSourceType;
+
+/**
+ * 自定义多数据源切换注解
+ *
+ * 优先级:先方法,后类,如果方法覆盖了类上的数据源类型,以方法的为准,否则以类上的为准
+ *
+ * @author ruoyi
+ */
+@Target({ ElementType.METHOD, ElementType.TYPE })
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+public @interface DataSource
+{
+    /**
+     * 切换数据源名称
+     */
+    public DataSourceType value() default DataSourceType.MASTER;
+}

+ 197 - 0
aegis-common/src/main/java/com/aegis/common/annotation/Excel.java

@@ -0,0 +1,197 @@
+package com.aegis.common.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.math.BigDecimal;
+import org.apache.poi.ss.usermodel.HorizontalAlignment;
+import org.apache.poi.ss.usermodel.IndexedColors;
+import com.aegis.common.utils.poi.ExcelHandlerAdapter;
+
+/**
+ * 自定义导出Excel数据注解
+ * 
+ * @author ruoyi
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface Excel
+{
+    /**
+     * 导出时在excel中排序
+     */
+    public int sort() default Integer.MAX_VALUE;
+
+    /**
+     * 导出到Excel中的名字.
+     */
+    public String name() default "";
+
+    /**
+     * 日期格式, 如: yyyy-MM-dd
+     */
+    public String dateFormat() default "";
+
+    /**
+     * 如果是字典类型,请设置字典的type值 (如: sys_user_sex)
+     */
+    public String dictType() default "";
+
+    /**
+     * 读取内容转表达式 (如: 0=男,1=女,2=未知)
+     */
+    public String readConverterExp() default "";
+
+    /**
+     * 分隔符,读取字符串组内容
+     */
+    public String separator() default ",";
+
+    /**
+     * BigDecimal 精度 默认:-1(默认不开启BigDecimal格式化)
+     */
+    public int scale() default -1;
+
+    /**
+     * BigDecimal 舍入规则 默认:BigDecimal.ROUND_HALF_EVEN
+     */
+    public int roundingMode() default BigDecimal.ROUND_HALF_EVEN;
+
+    /**
+     * 导出时在excel中每个列的高度
+     */
+    public double height() default 14;
+
+    /**
+     * 导出时在excel中每个列的宽度
+     */
+    public double width() default 16;
+
+    /**
+     * 文字后缀,如% 90 变成90%
+     */
+    public String suffix() default "";
+
+    /**
+     * 当值为空时,字段的默认值
+     */
+    public String defaultValue() default "";
+
+    /**
+     * 提示信息
+     */
+    public String prompt() default "";
+
+    /**
+     * 是否允许内容换行 
+     */
+    public boolean wrapText() default false;
+
+    /**
+     * 设置只能选择不能输入的列内容.
+     */
+    public String[] combo() default {};
+
+    /**
+     * 是否从字典读数据到combo,默认不读取,如读取需要设置dictType注解.
+     */
+    public boolean comboReadDict() default false;
+
+    /**
+     * 是否需要纵向合并单元格,应对需求:含有list集合单元格)
+     */
+    public boolean needMerge() default false;
+
+    /**
+     * 是否导出数据,应对需求:有时我们需要导出一份模板,这是标题需要但内容需要用户手工填写.
+     */
+    public boolean isExport() default true;
+
+    /**
+     * 另一个类中的属性名称,支持多级获取,以小数点隔开
+     */
+    public String targetAttr() default "";
+
+    /**
+     * 是否自动统计数据,在最后追加一行统计数据总和
+     */
+    public boolean isStatistics() default false;
+
+    /**
+     * 导出类型(0数字 1字符串 2图片)
+     */
+    public ColumnType cellType() default ColumnType.STRING;
+
+    /**
+     * 导出列头背景颜色
+     */
+    public IndexedColors headerBackgroundColor() default IndexedColors.GREY_50_PERCENT;
+
+    /**
+     * 导出列头字体颜色
+     */
+    public IndexedColors headerColor() default IndexedColors.WHITE;
+
+    /**
+     * 导出单元格背景颜色
+     */
+    public IndexedColors backgroundColor() default IndexedColors.WHITE;
+
+    /**
+     * 导出单元格字体颜色
+     */
+    public IndexedColors color() default IndexedColors.BLACK;
+
+    /**
+     * 导出字段对齐方式
+     */
+    public HorizontalAlignment align() default HorizontalAlignment.CENTER;
+
+    /**
+     * 自定义数据处理器
+     */
+    public Class<?> handler() default ExcelHandlerAdapter.class;
+
+    /**
+     * 自定义数据处理器参数
+     */
+    public String[] args() default {};
+
+    /**
+     * 字段类型(0:导出导入;1:仅导出;2:仅导入)
+     */
+    Type type() default Type.ALL;
+
+    public enum Type
+    {
+        ALL(0), EXPORT(1), IMPORT(2);
+        private final int value;
+
+        Type(int value)
+        {
+            this.value = value;
+        }
+
+        public int value()
+        {
+            return this.value;
+        }
+    }
+
+    public enum ColumnType
+    {
+        NUMERIC(0), STRING(1), IMAGE(2), TEXT(3);
+        private final int value;
+
+        ColumnType(int value)
+        {
+            this.value = value;
+        }
+
+        public int value()
+        {
+            return this.value;
+        }
+    }
+}

+ 18 - 0
aegis-common/src/main/java/com/aegis/common/annotation/Excels.java

@@ -0,0 +1,18 @@
+package com.aegis.common.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Excel注解集
+ * 
+ * @author ruoyi
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Excels
+{
+    public Excel[] value();
+}

+ 51 - 0
aegis-common/src/main/java/com/aegis/common/annotation/Log.java

@@ -0,0 +1,51 @@
+package com.aegis.common.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import com.aegis.common.enums.BusinessType;
+import com.aegis.common.enums.OperatorType;
+
+/**
+ * 自定义操作日志记录注解
+ * 
+ * @author ruoyi
+ *
+ */
+@Target({ ElementType.PARAMETER, ElementType.METHOD })
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Log
+{
+    /**
+     * 模块
+     */
+    public String title() default "";
+
+    /**
+     * 功能
+     */
+    public BusinessType businessType() default BusinessType.OTHER;
+
+    /**
+     * 操作人类别
+     */
+    public OperatorType operatorType() default OperatorType.MANAGE;
+
+    /**
+     * 是否保存请求的参数
+     */
+    public boolean isSaveRequestData() default true;
+
+    /**
+     * 是否保存响应的参数
+     */
+    public boolean isSaveResponseData() default true;
+
+    /**
+     * 排除指定的请求参数
+     */
+    public String[] excludeParamNames() default {};
+}

+ 40 - 0
aegis-common/src/main/java/com/aegis/common/annotation/RateLimiter.java

@@ -0,0 +1,40 @@
+package com.aegis.common.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import com.aegis.common.constant.CacheConstants;
+import com.aegis.common.enums.LimitType;
+
+/**
+ * 限流注解
+ * 
+ * @author ruoyi
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface RateLimiter
+{
+    /**
+     * 限流key
+     */
+    public String key() default CacheConstants.RATE_LIMIT_KEY;
+
+    /**
+     * 限流时间,单位秒
+     */
+    public int time() default 60;
+
+    /**
+     * 限流次数
+     */
+    public int count() default 100;
+
+    /**
+     * 限流类型
+     */
+    public LimitType limitType() default LimitType.DEFAULT;
+}

+ 31 - 0
aegis-common/src/main/java/com/aegis/common/annotation/RepeatSubmit.java

@@ -0,0 +1,31 @@
+package com.aegis.common.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 自定义注解防止表单重复提交
+ * 
+ * @author ruoyi
+ *
+ */
+@Inherited
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface RepeatSubmit
+{
+    /**
+     * 间隔时间(ms),小于此时间视为重复提交
+     */
+    public int interval() default 5000;
+
+    /**
+     * 提示消息
+     */
+    public String message() default "不允许重复提交,请稍候再试";
+}

+ 24 - 0
aegis-common/src/main/java/com/aegis/common/annotation/Sensitive.java

@@ -0,0 +1,24 @@
+package com.aegis.common.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.aegis.common.config.serializer.SensitiveJsonSerializer;
+import com.aegis.common.enums.DesensitizedType;
+
+/**
+ * 数据脱敏注解
+ *
+ * @author ruoyi
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+@JacksonAnnotationsInside
+@JsonSerialize(using = SensitiveJsonSerializer.class)
+public @interface Sensitive
+{
+    DesensitizedType desensitizedType();
+}

+ 122 - 0
aegis-common/src/main/java/com/aegis/common/config/RuoYiConfig.java

@@ -0,0 +1,122 @@
+package com.aegis.common.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * 读取项目相关配置
+ * 
+ * @author ruoyi
+ */
+@Component
+@ConfigurationProperties(prefix = "ruoyi")
+public class RuoYiConfig
+{
+    /** 项目名称 */
+    private String name;
+
+    /** 版本 */
+    private String version;
+
+    /** 版权年份 */
+    private String copyrightYear;
+
+    /** 上传路径 */
+    private static String profile;
+
+    /** 获取地址开关 */
+    private static boolean addressEnabled;
+
+    /** 验证码类型 */
+    private static String captchaType;
+
+    public String getName()
+    {
+        return name;
+    }
+
+    public void setName(String name)
+    {
+        this.name = name;
+    }
+
+    public String getVersion()
+    {
+        return version;
+    }
+
+    public void setVersion(String version)
+    {
+        this.version = version;
+    }
+
+    public String getCopyrightYear()
+    {
+        return copyrightYear;
+    }
+
+    public void setCopyrightYear(String copyrightYear)
+    {
+        this.copyrightYear = copyrightYear;
+    }
+
+    public static String getProfile()
+    {
+        return profile;
+    }
+
+    public void setProfile(String profile)
+    {
+        RuoYiConfig.profile = profile;
+    }
+
+    public static boolean isAddressEnabled()
+    {
+        return addressEnabled;
+    }
+
+    public void setAddressEnabled(boolean addressEnabled)
+    {
+        RuoYiConfig.addressEnabled = addressEnabled;
+    }
+
+    public static String getCaptchaType() {
+        return captchaType;
+    }
+
+    public void setCaptchaType(String captchaType) {
+        RuoYiConfig.captchaType = captchaType;
+    }
+
+    /**
+     * 获取导入上传路径
+     */
+    public static String getImportPath()
+    {
+        return getProfile() + "/import";
+    }
+
+    /**
+     * 获取头像上传路径
+     */
+    public static String getAvatarPath()
+    {
+        return getProfile() + "/avatar";
+    }
+
+    /**
+     * 获取下载路径
+     */
+    public static String getDownloadPath()
+    {
+        return getProfile() + "/download/";
+    }
+
+    /**
+     * 获取上传路径
+     */
+    public static String getUploadPath()
+    {
+        return getProfile() + "/upload";
+    }
+}

+ 67 - 0
aegis-common/src/main/java/com/aegis/common/config/serializer/SensitiveJsonSerializer.java

@@ -0,0 +1,67 @@
+package com.aegis.common.config.serializer;
+
+import java.io.IOException;
+import java.util.Objects;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.BeanProperty;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.ser.ContextualSerializer;
+import com.aegis.common.annotation.Sensitive;
+import com.aegis.common.core.domain.model.LoginUser;
+import com.aegis.common.enums.DesensitizedType;
+import com.aegis.common.utils.SecurityUtils;
+
+/**
+ * 数据脱敏序列化过滤
+ *
+ * @author ruoyi
+ */
+public class SensitiveJsonSerializer extends JsonSerializer<String> implements ContextualSerializer
+{
+    private DesensitizedType desensitizedType;
+
+    @Override
+    public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException
+    {
+        if (desensitization())
+        {
+            gen.writeString(desensitizedType.desensitizer().apply(value));
+        }
+        else
+        {
+            gen.writeString(value);
+        }
+    }
+
+    @Override
+    public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property)
+            throws JsonMappingException
+    {
+        Sensitive annotation = property.getAnnotation(Sensitive.class);
+        if (Objects.nonNull(annotation) && Objects.equals(String.class, property.getType().getRawClass()))
+        {
+            this.desensitizedType = annotation.desensitizedType();
+            return this;
+        }
+        return prov.findValueSerializer(property.getType(), property);
+    }
+
+    /**
+     * 是否需要脱敏处理
+     */
+    private boolean desensitization()
+    {
+        try
+        {
+            LoginUser securityUser = SecurityUtils.getLoginUser();
+            // 管理员不脱敏
+            return !securityUser.getUser().isAdmin();
+        }
+        catch (Exception e)
+        {
+            return true;
+        }
+    }
+}

+ 44 - 0
aegis-common/src/main/java/com/aegis/common/constant/CacheConstants.java

@@ -0,0 +1,44 @@
+package com.aegis.common.constant;
+
+/**
+ * 缓存的key 常量
+ * 
+ * @author ruoyi
+ */
+public class CacheConstants
+{
+    /**
+     * 登录用户 redis key
+     */
+    public static final String LOGIN_TOKEN_KEY = "login_tokens:";
+
+    /**
+     * 验证码 redis key
+     */
+    public static final String CAPTCHA_CODE_KEY = "captcha_codes:";
+
+    /**
+     * 参数管理 cache key
+     */
+    public static final String SYS_CONFIG_KEY = "sys_config:";
+
+    /**
+     * 字典管理 cache key
+     */
+    public static final String SYS_DICT_KEY = "sys_dict:";
+
+    /**
+     * 防重提交 redis key
+     */
+    public static final String REPEAT_SUBMIT_KEY = "repeat_submit:";
+
+    /**
+     * 限流 redis key
+     */
+    public static final String RATE_LIMIT_KEY = "rate_limit:";
+
+    /**
+     * 登录账户密码错误次数 redis key
+     */
+    public static final String PWD_ERR_CNT_KEY = "pwd_err_cnt:";
+}

Неке датотеке нису приказане због велике количине промена