Parcourir la source

电子地图、系统管理相关接口

hanzhengyi il y a 2 jours
Parent
commit
27596c54f7
62 fichiers modifiés avec 2233 ajouts et 71 suppressions
  1. 170 4
      API设计文档.md
  2. 8 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/controller/web/SasAlarmGroupController.java
  3. 121 28
      service-sas/service-sas-biz/src/main/java/com/usky/sas/controller/web/SasDeviceController.java
  4. 39 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/controller/web/SasEventCodeController.java
  5. 2 2
      service-sas/service-sas-biz/src/main/java/com/usky/sas/controller/web/SasEventGroupController.java
  6. 2 2
      service-sas/service-sas-biz/src/main/java/com/usky/sas/controller/web/SasMapController.java
  7. 72 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/controller/web/SasVideoMonitorGroupController.java
  8. 19 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/domain/SasAcquisitionEventCode.java
  9. 19 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/domain/SasAlarsasEventCode.java
  10. 19 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/domain/SasCollectionEventCode.java
  11. 19 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/domain/SasEntranceEventCode.java
  12. 2 1
      service-sas/service-sas-biz/src/main/java/com/usky/sas/domain/SasEventTypeGroup.java
  13. 20 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/domain/SasGauthCertification.java
  14. 19 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/domain/SasParkingEventCode.java
  15. 19 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/domain/SasPatrolEventCode.java
  16. 19 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/domain/SasPerceptionEventCode.java
  17. 19 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/domain/SasRoadblockEventCode.java
  18. 21 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/domain/SasSnapTypeCode.java
  19. 19 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/domain/SasUsbEventCode.java
  20. 19 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/domain/SasVideoEventCode.java
  21. 34 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/domain/SasVideoMonitorGroupType.java
  22. 54 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/enums/SystemTypeCodeEnum.java
  23. 8 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/mapper/SasAcquisitionEventCodeMapper.java
  24. 8 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/mapper/SasAlarsasEventCodeMapper.java
  25. 8 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/mapper/SasCollectionEventCodeMapper.java
  26. 8 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/mapper/SasEntranceEventCodeMapper.java
  27. 8 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/mapper/SasGauthCertificationMapper.java
  28. 8 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/mapper/SasParkingEventCodeMapper.java
  29. 8 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/mapper/SasPatrolEventCodeMapper.java
  30. 8 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/mapper/SasPerceptionEventCodeMapper.java
  31. 8 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/mapper/SasRoadblockEventCodeMapper.java
  32. 8 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/mapper/SasSnapTypeCodeMapper.java
  33. 8 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/mapper/SasUsbEventCodeMapper.java
  34. 8 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/mapper/SasVideoEventCodeMapper.java
  35. 10 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/mapper/SasVideoMonitorGroupTypeMapper.java
  36. 40 19
      service-sas/service-sas-biz/src/main/java/com/usky/sas/service/SasDeviceService.java
  37. 28 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/service/SasEventCodeService.java
  38. 2 1
      service-sas/service-sas-biz/src/main/java/com/usky/sas/service/SasEventTypeGroupService.java
  39. 2 1
      service-sas/service-sas-biz/src/main/java/com/usky/sas/service/SasMapService.java
  40. 25 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/service/SasVideoMonitorGroupTypeService.java
  41. 33 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/service/dto/agbox/AgboxChannelListVo.java
  42. 45 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/service/dto/agbox/AgboxDeviceListVo.java
  43. 473 8
      service-sas/service-sas-biz/src/main/java/com/usky/sas/service/impl/SasDeviceServiceImpl.java
  44. 174 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/service/impl/SasEventCodeServiceImpl.java
  45. 54 2
      service-sas/service-sas-biz/src/main/java/com/usky/sas/service/impl/SasEventTypeGroupServiceImpl.java
  46. 76 3
      service-sas/service-sas-biz/src/main/java/com/usky/sas/service/impl/SasMapServiceImpl.java
  47. 83 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/service/impl/SasVideoMonitorGroupTypeServiceImpl.java
  48. 13 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/service/vo/DeviceAlarmGroupBatchSetRequest.java
  49. 13 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/service/vo/DeviceAlarmGroupSetRequest.java
  50. 9 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/service/vo/DeviceConfigVO.java
  51. 11 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/service/vo/DeviceDeleteBatchRequest.java
  52. 54 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/service/vo/DeviceDetailVO.java
  53. 20 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/service/vo/DeviceIPCItemVO.java
  54. 78 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/service/vo/DeviceImportTemplateVO.java
  55. 13 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/service/vo/DeviceVideoGroupBatchSetRequest.java
  56. 11 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/service/vo/DeviceVideoGroupSetRequest.java
  57. 20 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/service/vo/EventCodeInfo.java
  58. 27 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/service/vo/EventGroupInfo.java
  59. 51 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/service/vo/MapListItem.java
  60. 11 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/service/vo/VideoMonitorGroupPageRequest.java
  61. 15 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/service/vo/VideoMonitorGroupSaveRequest.java
  62. 11 0
      service-sas/service-sas-biz/src/main/resources/mapper/sas/SasVideoMonitorGroupTypeMapper.xml

+ 170 - 4
API设计文档.md

@@ -517,6 +517,43 @@ Authorization: Bearer {token}
 | msg | string | 操作结果提示信息 |
 | data | null | 无业务数据 |
 
+### 事件编码/子类型查询
+
+#### 3.5 根据系统类型查询事件编码列表
+
+**接口地址**: `GET /prod-api/service-sas/event/codes`
+
+**功能描述**: 根据系统类型编码返回对应的事件编码(子类型)列表,用于配置事件组、筛选事件类型等。
+
+**请求参数**:
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| code | int | 是 | 系统类型编码,见 SystemTypeCodeEnum:1001 实时智能分析、1002 视频安防监控、1003 视频导出防护、1004 组合认证控制、1005 出入门禁控制、1006 车牌抓拍分析、1007 入侵紧急报警、1008 实时电子巡检、1009 状态感知探测、1010 数据采集探测、1011 状态采集探测、1012 阻车路障探测 |
+
+**响应示例**:
+
+```json
+{
+  "status": "SUCCESS",
+  "code": 200,
+  "data": [
+    { "code": 1, "name": "人员识别" },
+    { "code": 2, "name": "人脸识别" }
+  ]
+}
+```
+
+**字段说明**:
+
+| 字段名 | 类型 | 说明 |
+|--------|------|------|
+| status | string | 接口调用状态 |
+| code | int | 业务状态码 |
+| data | array | 事件编码列表;当请求的 code 未匹配任何系统类型时为 null |
+| data[].code | int | 事件编码(gauth 时为 functions) |
+| data[].name | string | 名称 |
+
 ### 设备心跳
 
 #### 4.1 查询设备状态
@@ -604,7 +641,7 @@ Authorization: Bearer {token}
 
 **接口地址**: `GET /prod-api/service-sas/event/group`
 
-**功能描述**: 展示已配置事件组信息
+**功能描述**: 展示已配置事件组信息,返回事件组名称、设备事件类型名称、事件等级名称以及按系统类型解析后的事件编码列表。
 
 **请求参数**:
 
@@ -626,9 +663,14 @@ Authorization: Bearer {token}
       {
         "id": "group_001",
         "name": "报警事件组",
-        "deviceType": 1,
-        "eventCodes": "1001,1002,1003",
+        "deviceType": 1007,
+        "deviceTypeName": "入侵紧急报警",
+        "eventCodes": [
+          { "code": 2001, "name": "入侵报警" },
+          { "code": 2002, "name": "紧急报警" }
+        ],
         "eventLevel": 1,
+        "eventLevelName": "紧急",
         "canDel": true,
         "createTime": "2026-01-30T10:00:00",
         "updateTime": "2026-01-30T10:00:00"
@@ -641,6 +683,30 @@ Authorization: Bearer {token}
 }
 ```
 
+**字段说明**(结合表 `sas_event_type_group` 及各事件编码表,如 `sas_alarsas_event_code`、`sas_snap_type_code` 等):
+
+| 字段名 | 类型 | 说明 |
+|--------|------|------|
+| status | string | 接口调用状态 |
+| code | int | 业务状态码 |
+| data | object | 分页数据对象 |
+| data.list | array | 事件组配置列表 |
+| data.list[].id | string | 主键id(sas_event_type_group.id) |
+| data.list[].name | string | 事件分组名称(sas_event_type_group.name) |
+| data.list[].deviceType | int | 设备事件类型编码(sas_event_type_group.device_type,对应 `SystemTypeCodeEnum.code`) |
+| data.list[].deviceTypeName | string | 设备事件类型名称(`SystemTypeCodeEnum.message`,如:入侵紧急报警、实时智能分析等) |
+| data.list[].eventCodes | array | 事件编码列表,已按系统类型解析为对象数组 |
+| data.list[].eventCodes[].code | int | 事件编码(各事件编码表 code;组合认证 gauth 时为 functions) |
+| data.list[].eventCodes[].name | string | 事件名称(各事件编码表 name,gauth 时为 name/actionName) |
+| data.list[].eventLevel | int | 事件等级编码(sas_event_type_group.event_level,对应事件优先级编码) |
+| data.list[].eventLevelName | string | 事件等级名称,如:紧急/高/普通/无 |
+| data.list[].canDel | boolean | 是否可删除:true 可删,false 不可删(sas_event_type_group.can_del) |
+| data.list[].createTime | string | 创建时间(sas_event_type_group.create_time) |
+| data.list[].updateTime | string | 更新时间(sas_event_type_group.update_time) |
+| data.total | int | 总记录数 |
+| data.current | int | 当前页码 |
+| data.size | int | 每页数量 |
+
 #### 5.2 新增事件组配置
 
 **接口地址**: `POST /prod-api/service-sas/event/group`
@@ -974,6 +1040,106 @@ Authorization: Bearer {token}
 | code | int | 业务状态码 |
 | msg | string | 删除结果提示信息 |
 
+#### 7.5 同步 AG 设备
+
+**接口地址**: `POST /prod-api/service-sas/device/syncAgboxDevice`
+
+**功能描述**: 从 AG(Agbox)同步设备到 `sas_device`。按系统类型(实时智能分析、视频安防监控、出入门禁控制等)依次调用 AG 的 `getDeviceList`,对每个设备可选调用 `getDeviceChannelList`;若有通道则按通道落库多条设备,否则按设备落库一条(通道号为 0)。已存在的设备(按 deviceId + deviceType + channel 判定)不会重复插入。若 AG 返回设备经纬度信息则自动写入 `sas_gis` 并关联设备的 `gis_id`。依赖 Agbox 配置(`sas_config` 的 host、port、keyds)。
+
+**请求参数**: 无(POST 无请求体)
+
+**响应示例**:
+
+```json
+{
+  "status": "SUCCESS",
+  "code": 200,
+  "data": null,
+  "msg": "操作成功"
+}
+```
+
+**字段说明**:
+
+| 字段名 | 类型 | 说明 |
+|--------|------|------|
+| status | string | 接口调用状态 |
+| code | int | 业务状态码 |
+| msg | string | 操作结果提示信息 |
+
+#### 7.6 获取设备导入模板
+
+**接口地址**: `GET /prod-api/service-sas/device/import/template`
+
+**功能描述**: 获取设备批量导入的 Excel 模板文件。前端调用后浏览器会直接下载一个名为「设备导入模板」的 `.xlsx` 文件,示例中会包含一行演示数据,方便用户参考填写格式。后续可配合“批量设备导入”接口使用(本次仅实现模板导出)。
+
+**请求参数**: 无
+
+**响应说明**:
+
+- **Content-Type**: `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`
+- **响应体**: 二进制 Excel 文件流,不再包装 `ApiResult`。
+
+**模板示例字段**(列头与内容示例):
+
+| 列名 | 示例值 | 说明 |
+|------|--------|------|
+| 设备类型(必填) | 1001 | 对应 `SystemTypeCodeEnum.code`,如 1001-实时智能分析 |
+| 设备编号(必填) | test-01 | 设备唯一编号 |
+| 设备IP(必填) | 192.168.3.141 | 设备 IP 地址 |
+| 端口(必填) | 8000 | 设备端口 |
+| 通道号 | 1 | NVR 通道号,普通 IPC 可填 1 或留空 |
+| 视频设备类型 | 1 | 预留字段,例如 1-IPC、2-NVR 等 |
+| 视频协议 | 2 | 预留字段,例如 1-ONVIF、2-海康、3-大华 等 |
+| 用户名 | admin | 登录用户名 |
+| 密码 | admin | 登录密码 |
+| 备注 | 备注 | 说明文字 |
+| 安装位置 | 位置示例 | 安装地点描述 |
+| 部位编码(houseCode) | 示例-houseCode | 关联 `sas_device.house_code` |
+| 经度 |  | 可选,经度 |
+| 纬度 |  | 可选,纬度 |
+| 高度 |  | 可选,高度 |
+| 楼层 | 11 | 可选,楼层号 |
+
+#### 7.7 批量设备导入(Excel)
+
+**接口地址**: `POST /prod-api/service-sas/device/importBatchDeviceByExcel`
+
+**功能描述**: 通过上传设备导入模板 Excel 文件,批量新增设备到 `sas_device` 表。系统会按行解析 Excel 内容,对每一行生成一条设备记录;若已存在相同的设备(按 `deviceId + deviceType + channel` 判重)则跳过,不会重复插入。
+
+**请求参数**(`multipart/form-data`):
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| file | file | 是 | 设备导入 Excel 文件(可使用 7.6 接口下载的模板) |
+
+**响应示例**:
+
+```json
+{
+  "status": "SUCCESS",
+  "code": 200,
+  "data": "设备批量导入成功",
+  "msg": "操作成功"
+}
+```
+
+**字段说明**:
+
+| 字段名 | 类型 | 说明 |
+|--------|------|------|
+| status | string | 接口调用状态 |
+| code | int | 业务状态码 |
+| data | string | 导入结果提示信息,如“设备批量导入成功” |
+| msg | string | 通用提示信息 |
+
+**导入规则说明**:
+
+- 必填列:**设备类型(必填)**、**设备编号(必填)**、**设备IP(必填)**、**端口(必填)**,任意一列为空则整行导入失败并返回错误。
+- 判重规则:同一条记录以 `deviceId + deviceType + channel` 作为唯一键;若数据库中已存在相同组合,则该行数据会被自动跳过,不报错。
+- 经纬度/高度:若模板中填写了经纬度或高度,则会自动在 `sas_gis` 中生成一条坐标记录,并把 `gis_id` 关联到设备。
+- 其他字段:用户名、密码、安装位置、备注、houseCode、视频设备类型、视频协议等字段会直接写入 `sas_device` 对应列;`is_binding` 默认 `false`,`shield` 默认 `false`。
+
 ### 报警联动配置
 
 #### 8.1 查询联动报警组
@@ -1277,7 +1443,7 @@ Authorization: Bearer {token}
 
 **接口地址**: `DELETE /prod-api/service-sas/map/{id}`
 
-**功能描述**: 删除电子地图
+**功能描述**: 删除电子地图。会**递归删除**当前地图及其所有子地图,并同时删除这些地图下绑定的所有设备点位(`sas_map_device` 记录)。
 
 **响应示例**:
 

+ 8 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/controller/web/SasAlarmGroupController.java

@@ -17,6 +17,8 @@ import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
+import java.util.List;
+
 /**
  * 报警联动配置接口
  *
@@ -38,6 +40,12 @@ public class SasAlarmGroupController {
         return ApiResult.success(alarsasGroupTypeService.page(request));
     }
 
+    /** 报警联动组下拉列表(不分页) */
+    @GetMapping("/list")
+    public ApiResult<List<SasAlarsasGroupType>> list() {
+        return ApiResult.success(alarsasGroupTypeService.list());
+    }
+
     @PostMapping
     public ApiResult<CreateResponse> create(@RequestBody AlarmGroupSaveRequest request) {
         String id = alarsasGroupTypeService.create(request);

+ 121 - 28
service-sas/service-sas-biz/src/main/java/com/usky/sas/controller/web/SasDeviceController.java

@@ -2,28 +2,40 @@ package com.usky.sas.controller.web;
 
 import com.usky.common.core.bean.ApiResult;
 import com.usky.common.core.bean.CommonPage;
+import com.usky.common.core.utils.poi.ExcelUtil;
+import com.usky.sas.enums.SystemTypeCodeEnum;
 import com.usky.sas.service.SasDeviceService;
-import com.usky.sas.service.vo.DeviceConfigPageRequest;
-import com.usky.sas.service.vo.DeviceConfigSaveRequest;
-import com.usky.sas.service.vo.DeviceConfigVO;
-import com.usky.sas.service.vo.DeviceHeartbeatRequest;
-import com.usky.sas.service.vo.DeviceHeartbeatResponse;
+import com.usky.sas.service.vo.*;
 import lombok.Data;
 import org.springframework.beans.factory.annotation.Autowired;
-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.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
 
 /**
- * 设备心跳接口
+ * 设备配置与心跳接口
  *
- * 对应 API 文档:
- * GET /prod-api/service-sas/device/heartbeat
+ * GET  /prod-api/service-sas/device/heartbeat          设备心跳分页
+ * GET  /prod-api/service-sas/device/config             设备配置分页
+ * GET  /prod-api/service-sas/device/config/{id}        设备详情
+ * POST /prod-api/service-sas/device/config             新增设备
+ * PUT  /prod-api/service-sas/device/config/{id}        编辑设备
+ * DELETE /prod-api/service-sas/device/config/{id}      删除设备
+ * POST /prod-api/service-sas/device/config/batch-delete 批量删除
+ * POST /prod-api/service-sas/device/setAlarmGroup      设置报警联动组
+ * POST /prod-api/service-sas/device/setAlarmGroupBatch 批量设置报警联动组
+ * POST /prod-api/service-sas/device/setVideoGroup      设置视频监控组
+ * POST /prod-api/service-sas/device/setVideoGroupBatch 批量设置视频监控组
+ * GET  /prod-api/service-sas/device/notBindIPCList     未绑定IPC列表
+ * POST /prod-api/service-sas/device/bindingIPC         绑定IPC
+ * POST /prod-api/service-sas/device/unbindIPC          解绑IPC
+ * POST /prod-api/service-sas/device/syncAgboxDevice    同步AG设备
+ * GET  /prod-api/service-sas/device/import/template    获取设备导入模板(Excel)
+ * POST /prod-api/service-sas/device/importBatchDeviceByExcel 批量设备导入
  */
 @RestController
 @RequestMapping("/device")
@@ -37,17 +49,16 @@ public class SasDeviceController {
         return ApiResult.success(sasDeviceService.heartbeatPage(request));
     }
 
-    /**
-     * 设备配置分页查询
-     */
     @GetMapping("/config")
     public ApiResult<CommonPage<DeviceConfigVO>> configPage(DeviceConfigPageRequest request) {
         return ApiResult.success(sasDeviceService.configPage(request));
     }
 
-    /**
-     * 新增设备配置
-     */
+    @GetMapping("/config/{id}")
+    public ApiResult<DeviceDetailVO> getDetail(@PathVariable("id") String id) {
+        return ApiResult.success(sasDeviceService.getDetail(id));
+    }
+
     @PostMapping("/config")
     public ApiResult<CreateDeviceResponse> createConfig(@RequestBody DeviceConfigSaveRequest request) {
         String id = sasDeviceService.createConfig(request);
@@ -56,24 +67,106 @@ public class SasDeviceController {
         return ApiResult.success(resp);
     }
 
-    /**
-     * 编辑设备配置
-     */
     @PutMapping("/config/{id}")
     public ApiResult<Void> updateConfig(@PathVariable("id") String id, @RequestBody DeviceConfigSaveRequest request) {
         sasDeviceService.updateConfig(id, request);
         return ApiResult.success();
     }
 
-    /**
-     * 删除设备配置
-     */
     @DeleteMapping("/config/{id}")
     public ApiResult<Void> deleteConfig(@PathVariable("id") String id) {
         sasDeviceService.deleteConfig(id);
         return ApiResult.success();
     }
 
+    @PostMapping("/config/batch-delete")
+    public ApiResult<Void> deleteConfigBatch(@RequestBody DeviceDeleteBatchRequest request) {
+        sasDeviceService.deleteConfigBatch(request.getIds());
+        return ApiResult.success();
+    }
+
+    @PostMapping("/setAlarmGroup")
+    public ApiResult<Void> setAlarmGroup(@RequestBody DeviceAlarmGroupSetRequest request) {
+        sasDeviceService.setAlarmGroup(request.getId(), request.getAlarmGroupType());
+        return ApiResult.success();
+    }
+
+    @PostMapping("/setAlarmGroupBatch")
+    public ApiResult<Void> setAlarmGroupBatch(@RequestBody DeviceAlarmGroupBatchSetRequest request) {
+        sasDeviceService.setAlarmGroupBatch(request.getIds(), request.getAlarmGroupType());
+        return ApiResult.success();
+    }
+
+    @PostMapping("/setVideoGroup")
+    public ApiResult<Void> setVideoGroup(@RequestBody DeviceVideoGroupSetRequest request) {
+        sasDeviceService.setVideoGroup(request.getId(), request.getVideoGroupType());
+        return ApiResult.success();
+    }
+
+    @PostMapping("/setVideoGroupBatch")
+    public ApiResult<Void> setVideoGroupBatch(@RequestBody DeviceVideoGroupBatchSetRequest request) {
+        sasDeviceService.setVideoGroupBatch(request.getIds(), request.getVideoGroupType());
+        return ApiResult.success();
+    }
+
+    @GetMapping("/notBindIPCList")
+    public ApiResult<List<DeviceIPCItemVO>> getNotBindIPCList() {
+        return ApiResult.success(sasDeviceService.getNotBindIPCList());
+    }
+
+    @PostMapping("/bindingIPC")
+    public ApiResult<Void> bindingIPC(@RequestParam("id") String id, @RequestParam("ipcId") String ipcId) {
+        sasDeviceService.bindingIPC(id, ipcId);
+        return ApiResult.success();
+    }
+
+    @PostMapping("/unbindIPC")
+    public ApiResult<Void> unbindIPC(@RequestParam("id") String id) {
+        sasDeviceService.unbindIPC(id);
+        return ApiResult.success();
+    }
+
+    /** 从 AG(Agbox)同步设备到 sas_device */
+    @PostMapping("/syncAgboxDevice")
+    public ApiResult<Void> syncAgboxDevice() {
+        sasDeviceService.syncAgboxDevice();
+        return ApiResult.success();
+    }
+
+    /**
+     * 获取设备导入模板(Excel)
+     */
+    @GetMapping("/import/template")
+    public void getDeviceImportTemplate(HttpServletResponse response) throws IOException {
+        List<DeviceImportTemplateVO> list = new ArrayList<>();
+        DeviceImportTemplateVO demo = new DeviceImportTemplateVO();
+        demo.setDeviceType(SystemTypeCodeEnum.snap.getCode());
+        demo.setDeviceId("test-01");
+        demo.setIpAddr("192.168.3.141");
+        demo.setPort(8000);
+        demo.setChannel(1);
+        demo.setVideoDeviceType(1);
+        demo.setVideoProtocol(2);
+        demo.setUsername("admin");
+        demo.setPassword("admin");
+        demo.setNote("备注");
+        demo.setAddress("位置示例");
+        demo.setHouseCode("示例-houseCode");
+        list.add(demo);
+
+        ExcelUtil<DeviceImportTemplateVO> util = new ExcelUtil<>(DeviceImportTemplateVO.class);
+        util.exportExcel(response, list, "设备导入模板", "设备导入模板");
+    }
+
+    /**
+     * 批量设备导入(Excel)
+     */
+    @PostMapping("/importBatchDeviceByExcel")
+    public ApiResult<String> importBatchDeviceByExcel(@RequestParam("file") MultipartFile file) {
+        String msg = sasDeviceService.importDeviceByExcel(file);
+        return ApiResult.success(msg);
+    }
+
     @Data
     public static class CreateDeviceResponse {
         private String id;

+ 39 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/controller/web/SasEventCodeController.java

@@ -0,0 +1,39 @@
+package com.usky.sas.controller.web;
+
+import com.usky.common.core.bean.ApiResult;
+import com.usky.sas.service.SasEventCodeService;
+import com.usky.sas.service.vo.EventCodeInfo;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * 事件编码/子类型查询接口
+ *
+ * GET /prod-api/service-sas/event/codes?code=1001
+ */
+@RestController
+@RequestMapping("/event")
+public class SasEventCodeController {
+
+    @Autowired
+    private SasEventCodeService sasEventCodeService;
+
+    /**
+     * 根据系统类型编码查询对应事件编码(子类型)列表
+     *
+     * @param code 系统类型编码,见 SystemTypeCodeEnum:1001 实时智能分析、1002 视频安防监控、1003 视频导出防护、
+     *             1004 组合认证控制、1005 出入门禁控制、1006 车牌抓拍分析、1007 入侵紧急报警、1008 实时电子巡检、
+     *             1009 状态感知探测、1010 数据采集探测、1011 状态采集探测、1012 阻车路障探测
+     * @return 对应的事件编码列表;code 未匹配时 data 为 null
+     */
+    @GetMapping("/codes")
+    public ApiResult<List<EventCodeInfo>> getEventCodes(@RequestParam Integer code) {
+        List<EventCodeInfo> list = sasEventCodeService.getEventCodes(code);
+        return ApiResult.success(list);
+    }
+}

+ 2 - 2
service-sas/service-sas-biz/src/main/java/com/usky/sas/controller/web/SasEventGroupController.java

@@ -2,8 +2,8 @@ package com.usky.sas.controller.web;
 
 import com.usky.common.core.bean.ApiResult;
 import com.usky.common.core.bean.CommonPage;
-import com.usky.sas.domain.SasEventTypeGroup;
 import com.usky.sas.service.SasEventTypeGroupService;
+import com.usky.sas.service.vo.EventGroupInfo;
 import com.usky.sas.service.vo.EventGroupPageRequest;
 import com.usky.sas.service.vo.EventGroupSaveRequest;
 import lombok.Data;
@@ -37,7 +37,7 @@ public class SasEventGroupController {
      * 事件组分页查询
      */
     @GetMapping
-    public ApiResult<CommonPage<SasEventTypeGroup>> page(EventGroupPageRequest request) {
+    public ApiResult<CommonPage<EventGroupInfo>> page(EventGroupPageRequest request) {
         return ApiResult.success(eventTypeGroupService.page(request));
     }
 

+ 2 - 2
service-sas/service-sas-biz/src/main/java/com/usky/sas/controller/web/SasMapController.java

@@ -2,9 +2,9 @@ package com.usky.sas.controller.web;
 
 import com.usky.common.core.bean.ApiResult;
 import com.usky.common.core.bean.CommonPage;
-import com.usky.sas.domain.SasMaps;
 import com.usky.sas.service.SasMapService;
 import com.usky.sas.service.vo.MapDeviceBindRequest;
+import com.usky.sas.service.vo.MapListItem;
 import com.usky.sas.service.vo.MapPageRequest;
 import com.usky.sas.service.vo.MapSaveRequest;
 import lombok.Data;
@@ -37,7 +37,7 @@ public class SasMapController {
     private SasMapService sasMapService;
 
     @GetMapping
-    public ApiResult<CommonPage<SasMaps>> page(MapPageRequest request) {
+    public ApiResult<CommonPage<MapListItem>> page(MapPageRequest request) {
         return ApiResult.success(sasMapService.page(request));
     }
 

+ 72 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/controller/web/SasVideoMonitorGroupController.java

@@ -0,0 +1,72 @@
+package com.usky.sas.controller.web;
+
+import com.usky.common.core.bean.ApiResult;
+import com.usky.common.core.bean.CommonPage;
+import com.usky.sas.domain.SasVideoMonitorGroupType;
+import com.usky.sas.service.SasVideoMonitorGroupTypeService;
+import com.usky.sas.service.vo.VideoMonitorGroupPageRequest;
+import com.usky.sas.service.vo.VideoMonitorGroupSaveRequest;
+import lombok.Data;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * 视频监控组配置接口
+ *
+ * GET /prod-api/service-sas/video/group/list  下拉列表
+ * GET /prod-api/service-sas/video/group       分页
+ * POST /prod-api/service-sas/video/group     新增
+ * PUT /prod-api/service-sas/video/group/{id} 编辑
+ * DELETE /prod-api/service-sas/video/group/{id} 删除
+ */
+@RestController
+@RequestMapping("/video/group")
+public class SasVideoMonitorGroupController {
+
+    @Autowired
+    private SasVideoMonitorGroupTypeService videoMonitorGroupTypeService;
+
+    @GetMapping("/list")
+    public ApiResult<List<SasVideoMonitorGroupType>> list() {
+        return ApiResult.success(videoMonitorGroupTypeService.listAll());
+    }
+
+    @GetMapping
+    public ApiResult<CommonPage<SasVideoMonitorGroupType>> page(VideoMonitorGroupPageRequest request) {
+        return ApiResult.success(videoMonitorGroupTypeService.page(request));
+    }
+
+    @PostMapping
+    public ApiResult<CreateResponse> create(@RequestBody VideoMonitorGroupSaveRequest request) {
+        String id = videoMonitorGroupTypeService.create(request);
+        CreateResponse resp = new CreateResponse();
+        resp.setId(id);
+        return ApiResult.success(resp);
+    }
+
+    @PutMapping("/{id}")
+    public ApiResult<Void> update(@PathVariable("id") String id, @RequestBody VideoMonitorGroupSaveRequest request) {
+        videoMonitorGroupTypeService.update(id, request);
+        return ApiResult.success();
+    }
+
+    @DeleteMapping("/{id}")
+    public ApiResult<Void> delete(@PathVariable("id") String id) {
+        videoMonitorGroupTypeService.delete(id);
+        return ApiResult.success();
+    }
+
+    @Data
+    public static class CreateResponse {
+        private String id;
+    }
+}

+ 19 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/domain/SasAcquisitionEventCode.java

@@ -0,0 +1,19 @@
+package com.usky.sas.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+
+/** 数据采集设备事件编码表 */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("sas_acquisition_event_code")
+public class SasAcquisitionEventCode implements Serializable {
+    private static final long serialVersionUID = 1L;
+    @TableId(value = "code")
+    private Integer code;
+    private String name;
+}

+ 19 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/domain/SasAlarsasEventCode.java

@@ -0,0 +1,19 @@
+package com.usky.sas.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+
+/** 入侵和紧急报警事件编码表 */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("sas_alarsas_event_code")
+public class SasAlarsasEventCode implements Serializable {
+    private static final long serialVersionUID = 1L;
+    @TableId(value = "code")
+    private Integer code;
+    private String name;
+}

+ 19 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/domain/SasCollectionEventCode.java

@@ -0,0 +1,19 @@
+package com.usky.sas.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+
+/** 状态采集探测事件编码表 */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("sas_collection_event_code")
+public class SasCollectionEventCode implements Serializable {
+    private static final long serialVersionUID = 1L;
+    @TableId(value = "code")
+    private Integer code;
+    private String name;
+}

+ 19 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/domain/SasEntranceEventCode.java

@@ -0,0 +1,19 @@
+package com.usky.sas.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+
+/** 出入口控制事件类型表 */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("sas_entrance_event_code")
+public class SasEntranceEventCode implements Serializable {
+    private static final long serialVersionUID = 1L;
+    @TableId(value = "code")
+    private Integer code;
+    private String name;
+}

+ 2 - 1
service-sas/service-sas-biz/src/main/java/com/usky/sas/domain/SasEventTypeGroup.java

@@ -1,5 +1,6 @@
 package com.usky.sas.domain;
 
+import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.Data;
@@ -18,7 +19,7 @@ public class SasEventTypeGroup implements Serializable {
 
     private static final long serialVersionUID = 1L;
 
-    @TableId(value = "id")
+    @TableId(value = "id", type = IdType.INPUT)
     private String id;
 
     /**

+ 20 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/domain/SasGauthCertification.java

@@ -0,0 +1,20 @@
+package com.usky.sas.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+
+/** 组合认证功能/方式表 */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("sas_gauth_certification")
+public class SasGauthCertification implements Serializable {
+    private static final long serialVersionUID = 1L;
+    @TableId(value = "functions")
+    private Integer functions;
+    private String actionName;
+    private String name;
+}

+ 19 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/domain/SasParkingEventCode.java

@@ -0,0 +1,19 @@
+package com.usky.sas.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+
+/** 停车场事件编码表 */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("sas_parking_event_code")
+public class SasParkingEventCode implements Serializable {
+    private static final long serialVersionUID = 1L;
+    @TableId(value = "code")
+    private Integer code;
+    private String name;
+}

+ 19 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/domain/SasPatrolEventCode.java

@@ -0,0 +1,19 @@
+package com.usky.sas.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+
+/** 实时电子巡检事件编码表 */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("sas_patrol_event_code")
+public class SasPatrolEventCode implements Serializable {
+    private static final long serialVersionUID = 1L;
+    @TableId(value = "code")
+    private Integer code;
+    private String name;
+}

+ 19 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/domain/SasPerceptionEventCode.java

@@ -0,0 +1,19 @@
+package com.usky.sas.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+
+/** 状态感知探测事件编码表 */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("sas_perception_event_code")
+public class SasPerceptionEventCode implements Serializable {
+    private static final long serialVersionUID = 1L;
+    @TableId(value = "code")
+    private Integer code;
+    private String name;
+}

+ 19 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/domain/SasRoadblockEventCode.java

@@ -0,0 +1,19 @@
+package com.usky.sas.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+
+/** 阻车路障事件编码表 */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("sas_roadblock_event_code")
+public class SasRoadblockEventCode implements Serializable {
+    private static final long serialVersionUID = 1L;
+    @TableId(value = "code")
+    private Integer code;
+    private String name;
+}

+ 21 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/domain/SasSnapTypeCode.java

@@ -0,0 +1,21 @@
+package com.usky.sas.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+
+/** 实时智能分析事件编码表 */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("sas_snap_type_code")
+public class SasSnapTypeCode implements Serializable {
+    private static final long serialVersionUID = 1L;
+    @TableId(value = "code")
+    private Integer code;
+    private String name;
+    /** 1:启用 0:未启用 */
+    private Integer isUsed;
+}

+ 19 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/domain/SasUsbEventCode.java

@@ -0,0 +1,19 @@
+package com.usky.sas.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+
+/** USB/视频导出防护事件类型表 */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("sas_usb_event_code")
+public class SasUsbEventCode implements Serializable {
+    private static final long serialVersionUID = 1L;
+    @TableId(value = "code")
+    private Integer code;
+    private String name;
+}

+ 19 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/domain/SasVideoEventCode.java

@@ -0,0 +1,19 @@
+package com.usky.sas.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+
+/** 视频安防监控事件编码表 */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("sas_video_event_code")
+public class SasVideoEventCode implements Serializable {
+    private static final long serialVersionUID = 1L;
+    @TableId(value = "code")
+    private Integer code;
+    private String name;
+}

+ 34 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/domain/SasVideoMonitorGroupType.java

@@ -0,0 +1,34 @@
+package com.usky.sas.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+
+/**
+ * 视频监控组类型表
+ *
+ * 表结构见 usky-agapp.sql:sas_video_monitor_group_type
+ * - id   varchar(255) 主键
+ * - name varchar(255) 名称
+ * - note varchar(255) 描述
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("sas_video_monitor_group_type")
+public class SasVideoMonitorGroupType implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 主键id */
+    @TableId(value = "id")
+    private String id;
+
+    /** 名称 */
+    private String name;
+
+    /** 描述 */
+    private String note;
+}

+ 54 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/enums/SystemTypeCodeEnum.java

@@ -0,0 +1,54 @@
+package com.usky.sas.enums;
+
+/**
+ * 系统类型/设备类型编码枚举
+ */
+public enum SystemTypeCodeEnum {
+
+    snap(1001, "实时智能分析"),
+    video(1002, "视频安防监控"),
+    usbalarm(1003, "视频导出防护"),
+    gauth(1004, "组合认证控制"),
+    entrance(1005, "出入门禁控制"),
+    parking(1006, "车牌抓拍分析"),
+    alarm(1007, "入侵紧急报警"),
+    patrol(1008, "实时电子巡检"),
+    perception(1009, "状态感知探测"),
+    acquisition(1010, "数据采集探测"),
+    collection(1011, "状态采集探测"),
+    roadblock(1012, "阻车路障探测");
+
+    private final int code;
+    private final String message;
+
+    SystemTypeCodeEnum(int code, String message) {
+        this.code = code;
+        this.message = message;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+
+    /** 根据编码获取枚举,未匹配返回 null */
+    public static SystemTypeCodeEnum getByCode(Integer code) {
+        if (code == null) {
+            return null;
+        }
+        for (SystemTypeCodeEnum e : values()) {
+            if (e.code == code) {
+                return e;
+            }
+        }
+        return null;
+    }
+
+    /** AG 设备接口路径使用的类型字符串(小写),如 snap、alarm、entrance */
+    public String getAgboxType() {
+        return this.name().toLowerCase();
+    }
+}

+ 8 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/mapper/SasAcquisitionEventCodeMapper.java

@@ -0,0 +1,8 @@
+package com.usky.sas.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.sas.domain.SasAcquisitionEventCode;
+
+/** 数据采集设备事件编码 Mapper */
+public interface SasAcquisitionEventCodeMapper extends CrudMapper<SasAcquisitionEventCode> {
+}

+ 8 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/mapper/SasAlarsasEventCodeMapper.java

@@ -0,0 +1,8 @@
+package com.usky.sas.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.sas.domain.SasAlarsasEventCode;
+
+/** 入侵和紧急报警事件编码 Mapper */
+public interface SasAlarsasEventCodeMapper extends CrudMapper<SasAlarsasEventCode> {
+}

+ 8 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/mapper/SasCollectionEventCodeMapper.java

@@ -0,0 +1,8 @@
+package com.usky.sas.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.sas.domain.SasCollectionEventCode;
+
+/** 状态采集探测事件编码 Mapper */
+public interface SasCollectionEventCodeMapper extends CrudMapper<SasCollectionEventCode> {
+}

+ 8 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/mapper/SasEntranceEventCodeMapper.java

@@ -0,0 +1,8 @@
+package com.usky.sas.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.sas.domain.SasEntranceEventCode;
+
+/** 出入口控制事件类型 Mapper */
+public interface SasEntranceEventCodeMapper extends CrudMapper<SasEntranceEventCode> {
+}

+ 8 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/mapper/SasGauthCertificationMapper.java

@@ -0,0 +1,8 @@
+package com.usky.sas.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.sas.domain.SasGauthCertification;
+
+/** 组合认证功能/方式 Mapper */
+public interface SasGauthCertificationMapper extends CrudMapper<SasGauthCertification> {
+}

+ 8 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/mapper/SasParkingEventCodeMapper.java

@@ -0,0 +1,8 @@
+package com.usky.sas.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.sas.domain.SasParkingEventCode;
+
+/** 停车场事件编码 Mapper */
+public interface SasParkingEventCodeMapper extends CrudMapper<SasParkingEventCode> {
+}

+ 8 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/mapper/SasPatrolEventCodeMapper.java

@@ -0,0 +1,8 @@
+package com.usky.sas.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.sas.domain.SasPatrolEventCode;
+
+/** 实时电子巡检事件编码 Mapper */
+public interface SasPatrolEventCodeMapper extends CrudMapper<SasPatrolEventCode> {
+}

+ 8 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/mapper/SasPerceptionEventCodeMapper.java

@@ -0,0 +1,8 @@
+package com.usky.sas.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.sas.domain.SasPerceptionEventCode;
+
+/** 状态感知探测事件编码 Mapper */
+public interface SasPerceptionEventCodeMapper extends CrudMapper<SasPerceptionEventCode> {
+}

+ 8 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/mapper/SasRoadblockEventCodeMapper.java

@@ -0,0 +1,8 @@
+package com.usky.sas.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.sas.domain.SasRoadblockEventCode;
+
+/** 阻车路障事件编码 Mapper */
+public interface SasRoadblockEventCodeMapper extends CrudMapper<SasRoadblockEventCode> {
+}

+ 8 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/mapper/SasSnapTypeCodeMapper.java

@@ -0,0 +1,8 @@
+package com.usky.sas.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.sas.domain.SasSnapTypeCode;
+
+/** 实时智能分析事件编码 Mapper */
+public interface SasSnapTypeCodeMapper extends CrudMapper<SasSnapTypeCode> {
+}

+ 8 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/mapper/SasUsbEventCodeMapper.java

@@ -0,0 +1,8 @@
+package com.usky.sas.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.sas.domain.SasUsbEventCode;
+
+/** USB/视频导出防护事件类型 Mapper */
+public interface SasUsbEventCodeMapper extends CrudMapper<SasUsbEventCode> {
+}

+ 8 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/mapper/SasVideoEventCodeMapper.java

@@ -0,0 +1,8 @@
+package com.usky.sas.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.sas.domain.SasVideoEventCode;
+
+/** 视频安防监控事件编码 Mapper */
+public interface SasVideoEventCodeMapper extends CrudMapper<SasVideoEventCode> {
+}

+ 10 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/mapper/SasVideoMonitorGroupTypeMapper.java

@@ -0,0 +1,10 @@
+package com.usky.sas.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.sas.domain.SasVideoMonitorGroupType;
+
+/**
+ * 视频监控组类型 Mapper 接口
+ */
+public interface SasVideoMonitorGroupTypeMapper extends CrudMapper<SasVideoMonitorGroupType> {
+}

+ 40 - 19
service-sas/service-sas-biz/src/main/java/com/usky/sas/service/SasDeviceService.java

@@ -3,40 +3,61 @@ package com.usky.sas.service;
 import com.usky.common.core.bean.CommonPage;
 import com.usky.common.mybatis.core.CrudService;
 import com.usky.sas.domain.SasDevice;
-import com.usky.sas.service.vo.DeviceConfigPageRequest;
-import com.usky.sas.service.vo.DeviceConfigSaveRequest;
-import com.usky.sas.service.vo.DeviceConfigVO;
-import com.usky.sas.service.vo.DeviceHeartbeatRequest;
-import com.usky.sas.service.vo.DeviceHeartbeatResponse;
+import com.usky.sas.service.vo.*;
+
+import java.util.List;
+
+import org.springframework.web.multipart.MultipartFile;
 
 /**
  * 设备信息 服务接口
  */
 public interface SasDeviceService extends CrudService<SasDevice> {
 
-    /**
-     * 设备心跳分页查询
-     */
     CommonPage<DeviceHeartbeatResponse> heartbeatPage(DeviceHeartbeatRequest request);
 
-    /**
-     * 设备配置分页查询
-     */
     CommonPage<DeviceConfigVO> configPage(DeviceConfigPageRequest request);
 
-    /**
-     * 新增设备配置
-     */
     String createConfig(DeviceConfigSaveRequest request);
 
-    /**
-     * 编辑设备配置
-     */
     void updateConfig(String id, DeviceConfigSaveRequest request);
 
+    void deleteConfig(String id);
+
+    /** 批量删除设备配置 */
+    void deleteConfigBatch(List<String> ids);
+
+    /** 获取设备详情(含类型名称、报警组名称、视频组名称) */
+    DeviceDetailVO getDetail(String id);
+
+    /** 单个设备设置报警联动组 */
+    void setAlarmGroup(String id, String alarmGroupType);
+
+    /** 批量设置报警联动组 */
+    void setAlarmGroupBatch(List<String> ids, String alarmGroupType);
+
+    /** 单个设备设置视频监控组 */
+    void setVideoGroup(String id, String videoGroupType);
+
+    /** 批量设置视频监控组 */
+    void setVideoGroupBatch(List<String> ids, String videoGroupType);
+
+    /** 获取未绑定 IPC 的设备列表(设备类型=视频) */
+    List<DeviceIPCItemVO> getNotBindIPCList();
+
+    /** 绑定 IPC 设备 */
+    void bindingIPC(String id, String ipcId);
+
+    /** 解绑 IPC 设备 */
+    void unbindIPC(String id);
+
     /**
-     * 删除设备配置
+     * 从 AG(Agbox)同步设备到 sas_device。
+     * 按系统类型调用 AG getDeviceList,若有通道则按通道落库,否则按设备落库一条。
      */
-    void deleteConfig(String id);
+    void syncAgboxDevice();
+
+    /** 通过 Excel 批量导入设备 */
+    String importDeviceByExcel(MultipartFile file);
 }
 

+ 28 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/service/SasEventCodeService.java

@@ -0,0 +1,28 @@
+package com.usky.sas.service;
+
+import com.usky.sas.service.vo.EventCodeInfo;
+
+import java.util.List;
+
+/**
+ * 根据系统类型编码查询对应事件编码/子类型列表
+ */
+public interface SasEventCodeService {
+
+    /**
+     * 根据系统类型 code(见 SystemTypeCodeEnum)返回对应的事件编码列表
+     *
+     * @param code 系统类型编码,如 1001(snap)、1007(alarm) 等
+     * @return 事件编码列表,未匹配时返回 null
+     */
+    List<EventCodeInfo> getEventCodes(Integer code);
+
+    /**
+     * 根据系统类型与编码 id 列表查询事件编码信息(用于事件组详情/分页填充 eventCodes)
+     *
+     * @param systemTypeCode 系统类型编码
+     * @param ids            事件编码 id 列表(如 code/functions)
+     * @return 事件编码信息列表,未匹配类型或空 id 返回空列表
+     */
+    List<EventCodeInfo> getEventCodesByIds(Integer systemTypeCode, List<Integer> ids);
+}

+ 2 - 1
service-sas/service-sas-biz/src/main/java/com/usky/sas/service/SasEventTypeGroupService.java

@@ -3,6 +3,7 @@ package com.usky.sas.service;
 import com.usky.common.core.bean.CommonPage;
 import com.usky.common.mybatis.core.CrudService;
 import com.usky.sas.domain.SasEventTypeGroup;
+import com.usky.sas.service.vo.EventGroupInfo;
 import com.usky.sas.service.vo.EventGroupPageRequest;
 import com.usky.sas.service.vo.EventGroupSaveRequest;
 
@@ -11,7 +12,7 @@ import com.usky.sas.service.vo.EventGroupSaveRequest;
  */
 public interface SasEventTypeGroupService extends CrudService<SasEventTypeGroup> {
 
-    CommonPage<SasEventTypeGroup> page(EventGroupPageRequest request);
+    CommonPage<EventGroupInfo> page(EventGroupPageRequest request);
 
     String create(EventGroupSaveRequest request);
 

+ 2 - 1
service-sas/service-sas-biz/src/main/java/com/usky/sas/service/SasMapService.java

@@ -3,13 +3,14 @@ package com.usky.sas.service;
 import com.usky.common.core.bean.CommonPage;
 import com.usky.common.mybatis.core.CrudService;
 import com.usky.sas.domain.SasMaps;
+import com.usky.sas.service.vo.MapListItem;
 import com.usky.sas.service.vo.MapDeviceBindRequest;
 import com.usky.sas.service.vo.MapPageRequest;
 import com.usky.sas.service.vo.MapSaveRequest;
 
 public interface SasMapService extends CrudService<SasMaps> {
 
-    CommonPage<SasMaps> page(MapPageRequest request);
+    CommonPage<MapListItem> page(MapPageRequest request);
 
     String create(MapSaveRequest request);
 

+ 25 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/service/SasVideoMonitorGroupTypeService.java

@@ -0,0 +1,25 @@
+package com.usky.sas.service;
+
+import com.usky.common.core.bean.CommonPage;
+import com.usky.common.mybatis.core.CrudService;
+import com.usky.sas.domain.SasVideoMonitorGroupType;
+import com.usky.sas.service.vo.VideoMonitorGroupPageRequest;
+import com.usky.sas.service.vo.VideoMonitorGroupSaveRequest;
+
+import java.util.List;
+
+/**
+ * 视频监控组类型 服务接口
+ */
+public interface SasVideoMonitorGroupTypeService extends CrudService<SasVideoMonitorGroupType> {
+
+    List<SasVideoMonitorGroupType> listAll();
+
+    CommonPage<SasVideoMonitorGroupType> page(VideoMonitorGroupPageRequest request);
+
+    String create(VideoMonitorGroupSaveRequest request);
+
+    void update(String id, VideoMonitorGroupSaveRequest request);
+
+    void delete(String id);
+}

+ 33 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/service/dto/agbox/AgboxChannelListVo.java

@@ -0,0 +1,33 @@
+package com.usky.sas.service.dto.agbox;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * AG 接口 getDeviceChannelList 响应
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class AgboxChannelListVo {
+
+    private AgboxChannelResult result;
+
+    @Data
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class AgboxChannelResult {
+        /** 通道列表 */
+        private List<AgboxChannelInfo> channelList;
+    }
+
+    @Data
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class AgboxChannelInfo {
+        private String deviceId;
+        private Integer channel;
+        /** 通道地址(如 IP) */
+        private String addr;
+        private String placeName;
+    }
+}

+ 45 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/service/dto/agbox/AgboxDeviceListVo.java

@@ -0,0 +1,45 @@
+package com.usky.sas.service.dto.agbox;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * AG 接口 getDeviceList 响应
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class AgboxDeviceListVo {
+
+    private AgboxDeviceResult result;
+
+    @Data
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class AgboxDeviceResult {
+        /** 设备列表 */
+        private List<AgboxDeviceInfo> deviceList;
+    }
+
+    @Data
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class AgboxDeviceInfo {
+        private String deviceId;
+        /** 触发时间,可能为日期时间字符串 */
+        private String triggerTime;
+        private String note;
+        private String ipAddr;
+        private String villageCode;
+        private String address;
+        /** 经纬度信息,可选 */
+        private AgboxGisInfo gis;
+    }
+
+    @Data
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class AgboxGisInfo {
+        private Double lon;
+        private Double lat;
+        private Double alt;
+    }
+}

+ 473 - 8
service-sas/service-sas-biz/src/main/java/com/usky/sas/service/impl/SasDeviceServiceImpl.java

@@ -1,33 +1,72 @@
 package com.usky.sas.service.impl;
 
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.usky.common.core.bean.CommonPage;
+import com.usky.common.core.exception.BusinessException;
 import com.usky.common.mybatis.core.AbstractCrudService;
+import com.usky.sas.domain.SasAlarsasGroupType;
 import com.usky.sas.domain.SasDevice;
+import com.usky.sas.domain.SasGis;
+import com.usky.sas.domain.SasConfig;
+import com.usky.sas.domain.SasVideoMonitorGroupType;
+import com.usky.sas.enums.SystemTypeCodeEnum;
+import com.usky.sas.mapper.SasAlarsasGroupTypeMapper;
 import com.usky.sas.mapper.SasDeviceMapper;
+import com.usky.sas.mapper.SasGisMapper;
+import com.usky.sas.mapper.SasVideoMonitorGroupTypeMapper;
+import com.usky.sas.service.SasConfigService;
 import com.usky.sas.service.SasDeviceService;
-import com.usky.sas.service.vo.DeviceConfigPageRequest;
-import com.usky.sas.service.vo.DeviceConfigSaveRequest;
-import com.usky.sas.service.vo.DeviceConfigVO;
-import com.usky.sas.service.vo.DeviceHeartbeatRequest;
-import com.usky.sas.service.vo.DeviceHeartbeatResponse;
+import com.usky.sas.service.dto.agbox.AgboxChannelListVo;
+import com.usky.sas.service.dto.agbox.AgboxDeviceListVo;
+import com.usky.sas.service.dto.agbox.JsonRpcRequest;
+import com.usky.sas.service.vo.*;
+import cn.afterturn.easypoi.excel.ExcelImportUtil;
+import cn.afterturn.easypoi.excel.entity.ImportParams;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.json.JSONUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.multipart.MultipartFile;
 
+import java.math.BigDecimal;
 import java.time.LocalDateTime;
 import java.time.temporal.ChronoUnit;
-import java.util.List;
-import java.util.UUID;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
 import java.util.stream.Collectors;
 
 /**
  * 设备信息 服务实现
  */
+@Slf4j
 @Service
 public class SasDeviceServiceImpl extends AbstractCrudService<SasDeviceMapper, SasDevice>
         implements SasDeviceService {
 
+    private static final String AGBOX_DEVICE_PATH = "/agbox/device";
+    private static final int MAX_RETRIES = 3;
+    private static final int HTTP_TIMEOUT_MS = 30000;
+
+    @Autowired
+    private SasAlarsasGroupTypeMapper alarsasGroupTypeMapper;
+
+    @Autowired
+    private SasVideoMonitorGroupTypeMapper videoMonitorGroupTypeMapper;
+
+    @Autowired
+    private SasConfigService sasConfigService;
+
+    @Autowired
+    private SasGisMapper sasGisMapper;
+
+    private final ObjectMapper objectMapper = new ObjectMapper();
+
     @Override
     public CommonPage<DeviceHeartbeatResponse> heartbeatPage(DeviceHeartbeatRequest request) {
         IPage<SasDevice> page = new Page<>(request.getCurrent(), request.getSize());
@@ -83,7 +122,9 @@ public class SasDeviceServiceImpl extends AbstractCrudService<SasDeviceMapper, S
         wrapper.orderByDesc("create_time");
         page = this.page(page, wrapper);
 
-        List<DeviceConfigVO> list = page.getRecords().stream().map(this::toConfigVO).collect(Collectors.toList());
+        List<DeviceConfigVO> list = page.getRecords().stream()
+                .map(this::toConfigVOEnriched)
+                .collect(Collectors.toList());
         return new CommonPage<>(list, page.getTotal(), page.getCurrent(), page.getSize());
     }
 
@@ -115,6 +156,430 @@ public class SasDeviceServiceImpl extends AbstractCrudService<SasDeviceMapper, S
         this.removeById(id);
     }
 
+    @Override
+    public void deleteConfigBatch(List<String> ids) {
+        if (ids == null || ids.isEmpty()) {
+            return;
+        }
+        this.removeByIds(ids);
+    }
+
+    @Override
+    public DeviceDetailVO getDetail(String id) {
+        SasDevice device = this.getById(id);
+        if (device == null) {
+            return null;
+        }
+        DeviceDetailVO vo = new DeviceDetailVO();
+        vo.setId(device.getId());
+        vo.setDeviceId(device.getDeviceId());
+        vo.setChannel(device.getChannel());
+        vo.setDeviceType(device.getDeviceType());
+        vo.setDeviceTypeName(deviceTypeName(device.getDeviceType()));
+        vo.setIpAddr(device.getIpAddr());
+        vo.setPort(device.getPort());
+        vo.setUsername(device.getUsername());
+        vo.setShield(device.getShield());
+        vo.setVillageCode(device.getVillageCode());
+        vo.setHouseCode(device.getHouseCode());
+        vo.setAddress(device.getAddress());
+        vo.setVideoGroupType(device.getVideoGroupType());
+        vo.setVideoGroupTypeName(groupName(device.getVideoGroupType(), true));
+        vo.setAlarmGroupType(device.getAlarsasGroupType());
+        vo.setAlarmGroupTypeName(groupName(device.getAlarsasGroupType(), false));
+        vo.setNote(device.getNote());
+        vo.setBindingIpc(device.getBindingIpc());
+        vo.setIsBinding(device.getIsBinding());
+        vo.setCreateTime(device.getCreateTime());
+        vo.setUpdateTime(device.getUpdateTime());
+        return vo;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void setAlarmGroup(String id, String alarmGroupType) {
+        SasDevice device = this.getById(id);
+        if (device == null) {
+            return;
+        }
+        device.setAlarsasGroupType(alarmGroupType);
+        device.setUpdateTime(LocalDateTime.now());
+        this.updateById(device);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void setAlarmGroupBatch(List<String> ids, String alarmGroupType) {
+        if (ids == null || ids.isEmpty()) {
+            return;
+        }
+        for (String id : ids) {
+            SasDevice device = this.getById(id);
+            if (device != null) {
+                device.setAlarsasGroupType(alarmGroupType);
+                device.setUpdateTime(LocalDateTime.now());
+                this.updateById(device);
+            }
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void setVideoGroup(String id, String videoGroupType) {
+        SasDevice device = this.getById(id);
+        if (device == null) {
+            return;
+        }
+        device.setVideoGroupType(videoGroupType);
+        device.setUpdateTime(LocalDateTime.now());
+        this.updateById(device);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void setVideoGroupBatch(List<String> ids, String videoGroupType) {
+        if (ids == null || ids.isEmpty()) {
+            return;
+        }
+        for (String id : ids) {
+            SasDevice device = this.getById(id);
+            if (device != null) {
+                device.setVideoGroupType(videoGroupType);
+                device.setUpdateTime(LocalDateTime.now());
+                this.updateById(device);
+            }
+        }
+    }
+
+    @Override
+    public List<DeviceIPCItemVO> getNotBindIPCList() {
+        LambdaQueryWrapper<SasDevice> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(SasDevice::getDeviceType, SystemTypeCodeEnum.video.getCode())
+                .and(w -> w.isNull(SasDevice::getIsBinding).or().eq(SasDevice::getIsBinding, false));
+        List<SasDevice> list = this.list(wrapper);
+        if (list == null) {
+            return Collections.emptyList();
+        }
+        return list.stream().map(d -> {
+            DeviceIPCItemVO vo = new DeviceIPCItemVO();
+            vo.setId(d.getId());
+            vo.setDeviceId(d.getDeviceId());
+            vo.setChannel(d.getChannel());
+            vo.setIpAddr(d.getIpAddr());
+            vo.setNote(d.getNote());
+            return vo;
+        }).collect(Collectors.toList());
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void bindingIPC(String id, String ipcId) {
+        SasDevice device = this.getById(id);
+        if (device == null) {
+            return;
+        }
+        if (ipcId != null && ipcId.equals(device.getBindingIpc())) {
+            return;
+        }
+        device.setBindingIpc(ipcId);
+        device.setUpdateTime(LocalDateTime.now());
+        this.updateById(device);
+        SasDevice ipc = this.getById(ipcId);
+        if (ipc != null) {
+            ipc.setIsBinding(true);
+            ipc.setUpdateTime(LocalDateTime.now());
+            this.updateById(ipc);
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void unbindIPC(String id) {
+        SasDevice device = this.getById(id);
+        if (device == null || device.getBindingIpc() == null) {
+            return;
+        }
+        String ipcId = device.getBindingIpc();
+        device.setBindingIpc(null);
+        device.setUpdateTime(LocalDateTime.now());
+        this.updateById(device);
+        SasDevice ipc = this.getById(ipcId);
+        if (ipc != null) {
+            ipc.setIsBinding(false);
+            ipc.setUpdateTime(LocalDateTime.now());
+            this.updateById(ipc);
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void syncAgboxDevice() {
+        SasConfig config = sasConfigService.getConfig();
+        if (config == null || config.getHost() == null || config.getHost().isEmpty()) {
+            log.warn("同步 AG 设备:未配置 Agbox 或 host 为空,跳过");
+            return;
+        }
+        String baseUrl = buildAgboxBaseUrl(config.getHost(), config.getPort());
+        String key = config.getKeyds() != null ? config.getKeyds() : "";
+        for (SystemTypeCodeEnum type : SystemTypeCodeEnum.values()) {
+            String deviceUrl = baseUrl + AGBOX_DEVICE_PATH + "/" + type.getAgboxType();
+            try {
+                AgboxDeviceListVo listVo = postDeviceList(deviceUrl, key);
+                if (listVo == null || listVo.getResult() == null || listVo.getResult().getDeviceList() == null
+                        || listVo.getResult().getDeviceList().isEmpty()) {
+                    continue;
+                }
+                for (AgboxDeviceListVo.AgboxDeviceInfo dev : listVo.getResult().getDeviceList()) {
+                    if (dev.getDeviceId() == null || dev.getDeviceId().isEmpty()) {
+                        continue;
+                    }
+                    List<AgboxChannelListVo.AgboxChannelInfo> channels = postChannelList(deviceUrl, key, dev.getDeviceId());
+                    if (channels != null && !channels.isEmpty()) {
+                        for (AgboxChannelListVo.AgboxChannelInfo ch : channels) {
+                            saveDeviceFromAgbox(dev, ch, type.getCode());
+                        }
+                    } else {
+                        saveDeviceFromAgbox(dev, null, type.getCode());
+                    }
+                }
+            } catch (Exception e) {
+                log.warn("同步 AG 设备 type={} 失败: {}", type.getAgboxType(), e.getMessage());
+            }
+        }
+    }
+
+    private AgboxDeviceListVo postDeviceList(String deviceUrl, String key) throws Exception {
+        JsonRpcRequest rpc = new JsonRpcRequest("getDeviceList", null, null);
+        Map<String, Object> body = new HashMap<>();
+        body.put("key", key);
+        body.put("json", JSONUtil.toJsonStr(rpc));
+        String res = postWithRetry(deviceUrl, body);
+        return objectMapper.readValue(res, AgboxDeviceListVo.class);
+    }
+
+    private List<AgboxChannelListVo.AgboxChannelInfo> postChannelList(String deviceUrl, String key, String deviceId) {
+        try {
+            Map<String, Object> params = new HashMap<>();
+            params.put("deviceId", deviceId);
+            JsonRpcRequest rpc = new JsonRpcRequest("getDeviceChannelList", params, null);
+            Map<String, Object> body = new HashMap<>();
+            body.put("key", key);
+            body.put("json", JSONUtil.toJsonStr(rpc));
+            String res = postWithRetry(deviceUrl, body);
+            AgboxChannelListVo vo = objectMapper.readValue(res, AgboxChannelListVo.class);
+            if (vo != null && vo.getResult() != null && vo.getResult().getChannelList() != null) {
+                return vo.getResult().getChannelList();
+            }
+        } catch (Exception e) {
+            log.debug("getDeviceChannelList deviceId={} 失败: {}", deviceId, e.getMessage());
+        }
+        return Collections.emptyList();
+    }
+
+    private void saveDeviceFromAgbox(AgboxDeviceListVo.AgboxDeviceInfo dev,
+                                     AgboxChannelListVo.AgboxChannelInfo channel,
+                                     int deviceTypeCode) {
+        String deviceId = dev.getDeviceId();
+        int channelNum = channel != null ? (channel.getChannel() != null ? channel.getChannel() : 0) : 0;
+        LambdaQueryWrapper<SasDevice> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(SasDevice::getDeviceId, deviceId)
+                .eq(SasDevice::getDeviceType, deviceTypeCode)
+                .eq(SasDevice::getChannel, channelNum);
+        if (this.count(wrapper) > 0) {
+            return;
+        }
+        String gisId = ensureGis(dev);
+        SasDevice device = new SasDevice();
+        device.setId(UUID.randomUUID().toString());
+        device.setDeviceId(deviceId);
+        device.setChannel(channelNum);
+        device.setDeviceType(deviceTypeCode);
+        String ipAddr = (channel != null && channel.getAddr() != null && !channel.getAddr().isEmpty())
+                ? channel.getAddr() : dev.getIpAddr();
+        device.setIpAddr(ipAddr);
+        device.setVillageCode(dev.getVillageCode());
+        device.setAddress(dev.getAddress());
+        device.setNote(channel != null && channel.getPlaceName() != null ? channel.getPlaceName() : dev.getNote());
+        device.setGisId(gisId);
+        device.setTriggerTime(parseTriggerTime(dev.getTriggerTime()));
+        LocalDateTime now = LocalDateTime.now();
+        device.setCreateTime(now);
+        device.setUpdateTime(now);
+        this.save(device);
+    }
+
+    private String ensureGis(AgboxDeviceListVo.AgboxDeviceInfo dev) {
+        if (dev.getGis() == null) {
+            return null;
+        }
+        AgboxDeviceListVo.AgboxGisInfo g = dev.getGis();
+        if (g.getLon() == null && g.getLat() == null) {
+            return null;
+        }
+        String gisId = UUID.randomUUID().toString();
+        SasGis gis = new SasGis();
+        gis.setId(gisId);
+        gis.setLon(g.getLon() != null ? BigDecimal.valueOf(g.getLon()) : BigDecimal.ZERO);
+        gis.setLat(g.getLat() != null ? BigDecimal.valueOf(g.getLat()) : BigDecimal.ZERO);
+        gis.setAlt(g.getAlt() != null ? BigDecimal.valueOf(g.getAlt()) : BigDecimal.ZERO);
+        sasGisMapper.insert(gis);
+        return gisId;
+    }
+
+    private LocalDateTime parseTriggerTime(String triggerTime) {
+        if (triggerTime == null || triggerTime.isEmpty()) {
+            return null;
+        }
+        String s = triggerTime.trim().replace(' ', 'T');
+        if (s.length() > 19) {
+            s = s.substring(0, 19);
+        }
+        try {
+            return LocalDateTime.parse(s, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
+        } catch (Exception e1) {
+            try {
+                return LocalDateTime.parse(triggerTime.trim(), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
+            } catch (Exception e2) {
+                return null;
+            }
+        }
+    }
+
+    private String buildAgboxBaseUrl(String host, String port) {
+        if (host == null || host.isEmpty()) {
+            return "";
+        }
+        if (host.startsWith("http://") || host.startsWith("https://")) {
+            return host;
+        }
+        return "http://" + host;
+    }
+
+    private String postWithRetry(String url, Map<String, Object> requestBody) {
+        Exception lastEx = null;
+        for (int i = 0; i < MAX_RETRIES; i++) {
+            try {
+                return HttpRequest.post(url)
+                        .form(requestBody)
+                        .timeout(HTTP_TIMEOUT_MS)
+                        .execute()
+                        .body();
+            } catch (Exception e) {
+                lastEx = e;
+                if (i < MAX_RETRIES - 1) {
+                    log.warn("AG 设备接口请求失败,第 {} 次重试:{}", i + 1, e.getMessage());
+                    try {
+                        Thread.sleep(1000L * (i + 1));
+                    } catch (InterruptedException ie) {
+                        Thread.currentThread().interrupt();
+                        throw new RuntimeException("重试被中断", ie);
+                    }
+                }
+            }
+        }
+        throw new RuntimeException("AG 接口请求失败,重试 " + MAX_RETRIES + " 次后仍失败", lastEx);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public String importDeviceByExcel(MultipartFile file) {
+        if (file == null || file.isEmpty()) {
+            throw new BusinessException("请选择一个Excel文件上传");
+        }
+        ImportParams params = new ImportParams();
+        params.setHeadRows(1);
+        params.setTitleRows(1);
+        String err = "文件导入失败";
+        try {
+            List<DeviceImportTemplateVO> rows = ExcelImportUtil.importExcel(
+                    file.getInputStream(), DeviceImportTemplateVO.class, params);
+            if (rows == null || rows.isEmpty()) {
+                throw new BusinessException("文件不能为空");
+            }
+            int index = 0;
+            for (DeviceImportTemplateVO row : rows) {
+                index++;
+                if (row.getDeviceType() == null
+                        || row.getDeviceId() == null || row.getDeviceId().isEmpty()
+                        || row.getIpAddr() == null || row.getIpAddr().isEmpty()
+                        || row.getPort() == null) {
+                    throw new BusinessException("第" + (index + 1) + "行数据导入失败,设备类型、设备编号、设备IP、端口为必填");
+                }
+                int channel = row.getChannel() != null ? row.getChannel() : 0;
+                LambdaQueryWrapper<SasDevice> wrapper = new LambdaQueryWrapper<>();
+                wrapper.eq(SasDevice::getDeviceId, row.getDeviceId())
+                        .eq(SasDevice::getDeviceType, row.getDeviceType())
+                        .eq(SasDevice::getChannel, channel);
+                if (this.count(wrapper) > 0) {
+                    continue;
+                }
+                String gisId = null;
+                if (row.getLon() != null || row.getLat() != null || row.getAlt() != null) {
+                    SasGis gis = new SasGis();
+                    gisId = UUID.randomUUID().toString();
+                    gis.setId(gisId);
+                    gis.setLon(row.getLon() != null ? row.getLon() : BigDecimal.ZERO);
+                    gis.setLat(row.getLat() != null ? row.getLat() : BigDecimal.ZERO);
+                    gis.setAlt(row.getAlt() != null ? row.getAlt() : BigDecimal.ZERO);
+                    sasGisMapper.insert(gis);
+                }
+                SasDevice device = new SasDevice();
+                device.setId(UUID.randomUUID().toString());
+                device.setDeviceId(row.getDeviceId());
+                device.setDeviceType(row.getDeviceType());
+                device.setChannel(channel);
+                device.setIpAddr(row.getIpAddr());
+                device.setPort(row.getPort());
+                device.setUsername(row.getUsername());
+                device.setPassword(row.getPassword());
+                device.setVideoType(row.getVideoDeviceType());
+                device.setVideoProtocol(row.getVideoProtocol());
+                device.setNote(row.getNote());
+                device.setAddress(row.getAddress());
+                device.setHouseCode(row.getHouseCode());
+                device.setGisId(gisId);
+                device.setIsBinding(false);
+                device.setShield(false);
+                LocalDateTime now = LocalDateTime.now();
+                device.setCreateTime(now);
+                device.setUpdateTime(now);
+                this.save(device);
+            }
+        } catch (BusinessException e) {
+            throw e;
+        } catch (Exception e) {
+            log.error("批量导入设备失败", e);
+            throw new BusinessException(err);
+        }
+        return "设备批量导入成功";
+    }
+
+    private String deviceTypeName(Integer code) {
+        SystemTypeCodeEnum e = SystemTypeCodeEnum.getByCode(code);
+        return e != null ? e.getMessage() : null;
+    }
+
+    private String groupName(String groupId, boolean video) {
+        if (groupId == null || groupId.isEmpty()) {
+            return null;
+        }
+        if (video) {
+            SasVideoMonitorGroupType g = videoMonitorGroupTypeMapper.selectById(groupId);
+            return g != null ? g.getName() : null;
+        } else {
+            SasAlarsasGroupType g = alarsasGroupTypeMapper.selectById(groupId);
+            return g != null ? g.getName() : null;
+        }
+    }
+
+    private DeviceConfigVO toConfigVOEnriched(SasDevice d) {
+        DeviceConfigVO vo = toConfigVO(d);
+        vo.setDeviceTypeName(deviceTypeName(d.getDeviceType()));
+        vo.setAlarmGroupTypeName(groupName(d.getAlarsasGroupType(), false));
+        vo.setVideoGroupTypeName(groupName(d.getVideoGroupType(), true));
+        return vo;
+    }
+
     private void applyConfig(SasDevice device, DeviceConfigSaveRequest request) {
         device.setDeviceId(request.getDeviceId());
         device.setChannel(request.getChannel());

+ 174 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/service/impl/SasEventCodeServiceImpl.java

@@ -0,0 +1,174 @@
+package com.usky.sas.service.impl;
+
+import cn.hutool.core.bean.BeanUtil;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.usky.sas.domain.*;
+import com.usky.sas.enums.SystemTypeCodeEnum;
+import com.usky.sas.mapper.*;
+import com.usky.sas.service.SasEventCodeService;
+import com.usky.sas.service.vo.EventCodeInfo;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 根据系统类型编码返回对应事件编码/子类型列表
+ */
+@Slf4j
+@Service
+public class SasEventCodeServiceImpl implements SasEventCodeService {
+
+    @Autowired
+    private SasAcquisitionEventCodeMapper acquisitionEventCodeMapper;
+    @Autowired
+    private SasAlarsasEventCodeMapper alarsasEventCodeMapper;
+    @Autowired
+    private SasCollectionEventCodeMapper collectionEventCodeMapper;
+    @Autowired
+    private SasEntranceEventCodeMapper entranceEventCodeMapper;
+    @Autowired
+    private SasParkingEventCodeMapper parkingEventCodeMapper;
+    @Autowired
+    private SasPatrolEventCodeMapper patrolEventCodeMapper;
+    @Autowired
+    private SasPerceptionEventCodeMapper perceptionEventCodeMapper;
+    @Autowired
+    private SasRoadblockEventCodeMapper roadblockEventCodeMapper;
+    @Autowired
+    private SasSnapTypeCodeMapper snapTypeCodeMapper;
+    @Autowired
+    private SasUsbEventCodeMapper usbEventCodeMapper;
+    @Autowired
+    private SasVideoEventCodeMapper videoEventCodeMapper;
+    @Autowired
+    private SasGauthCertificationMapper gauthCertificationMapper;
+
+    @Override
+    public List<EventCodeInfo> getEventCodes(Integer code) {
+        log.debug("请求参数:code={}", code);
+        if (code == null) {
+            return null;
+        }
+        SystemTypeCodeEnum type = SystemTypeCodeEnum.getByCode(code);
+        if (type == null) {
+            return null;
+        }
+        List<?> list = listByType(type, null);
+        if (list == null) {
+            return null;
+        }
+        if (type == SystemTypeCodeEnum.gauth) {
+            return gauthToEventCodeInfo(list);
+        }
+        return toEventCodeInfoList(list);
+    }
+
+    @Override
+    public List<EventCodeInfo> getEventCodesByIds(Integer systemTypeCode, List<Integer> ids) {
+        if (systemTypeCode == null || ids == null || ids.isEmpty()) {
+            return Collections.emptyList();
+        }
+        SystemTypeCodeEnum type = SystemTypeCodeEnum.getByCode(systemTypeCode);
+        if (type == null) {
+            return Collections.emptyList();
+        }
+        List<?> list = listByType(type, ids);
+        if (list == null || list.isEmpty()) {
+            return Collections.emptyList();
+        }
+        if (type == SystemTypeCodeEnum.gauth) {
+            return gauthToEventCodeInfo(list);
+        }
+        return toEventCodeInfoList(list);
+    }
+
+    /**
+     * 按系统类型查询事件编码:ids 为 null 时查全部,否则按 id 列表查
+     */
+    private List<?> listByType(SystemTypeCodeEnum type, List<Integer> ids) {
+        if (ids == null || ids.isEmpty()) {
+            switch (type) {
+                case acquisition:
+                    return acquisitionEventCodeMapper.selectList(Wrappers.emptyWrapper());
+                case alarm:
+                    return alarsasEventCodeMapper.selectList(Wrappers.emptyWrapper());
+                case roadblock:
+                    return roadblockEventCodeMapper.selectList(Wrappers.emptyWrapper());
+                case perception:
+                    return perceptionEventCodeMapper.selectList(Wrappers.emptyWrapper());
+                case patrol:
+                    return patrolEventCodeMapper.selectList(Wrappers.emptyWrapper());
+                case usbalarm:
+                    return usbEventCodeMapper.selectList(Wrappers.emptyWrapper());
+                case collection:
+                    return collectionEventCodeMapper.selectList(Wrappers.emptyWrapper());
+                case entrance:
+                    return entranceEventCodeMapper.selectList(Wrappers.emptyWrapper());
+                case video:
+                    return videoEventCodeMapper.selectList(Wrappers.emptyWrapper());
+                case snap:
+                    return snapTypeCodeMapper.selectList(Wrappers.emptyWrapper());
+                case parking:
+                    return parkingEventCodeMapper.selectList(Wrappers.emptyWrapper());
+                case gauth:
+                    return gauthCertificationMapper.selectList(Wrappers.emptyWrapper());
+                default:
+                    return null;
+            }
+        }
+        switch (type) {
+            case acquisition:
+                return acquisitionEventCodeMapper.selectBatchIds(ids);
+            case alarm:
+                return alarsasEventCodeMapper.selectBatchIds(ids);
+            case roadblock:
+                return roadblockEventCodeMapper.selectBatchIds(ids);
+            case perception:
+                return perceptionEventCodeMapper.selectBatchIds(ids);
+            case patrol:
+                return patrolEventCodeMapper.selectBatchIds(ids);
+            case usbalarm:
+                return usbEventCodeMapper.selectBatchIds(ids);
+            case collection:
+                return collectionEventCodeMapper.selectBatchIds(ids);
+            case entrance:
+                return entranceEventCodeMapper.selectBatchIds(ids);
+            case video:
+                return videoEventCodeMapper.selectBatchIds(ids);
+            case snap:
+                return snapTypeCodeMapper.selectBatchIds(ids);
+            case parking:
+                return parkingEventCodeMapper.selectBatchIds(ids);
+            case gauth:
+                return gauthCertificationMapper.selectBatchIds(ids);
+            default:
+                return Collections.emptyList();
+        }
+    }
+
+    private List<EventCodeInfo> gauthToEventCodeInfo(List<?> list) {
+        if (list == null) {
+            return Collections.emptyList();
+        }
+        return list.stream().map(g -> {
+            SasGauthCertification cert = (SasGauthCertification) g;
+            EventCodeInfo info = new EventCodeInfo();
+            info.setCode(cert.getFunctions());
+            info.setName(cert.getName() != null ? cert.getName() : cert.getActionName());
+            return info;
+        }).collect(Collectors.toList());
+    }
+
+    private List<EventCodeInfo> toEventCodeInfoList(List<?> list) {
+        if (list == null) {
+            return Collections.emptyList();
+        }
+        return list.stream()
+                .map(entity -> BeanUtil.toBean(entity, EventCodeInfo.class))
+                .collect(Collectors.toList());
+    }
+}

+ 54 - 2
service-sas/service-sas-biz/src/main/java/com/usky/sas/service/impl/SasEventTypeGroupServiceImpl.java

@@ -1,18 +1,25 @@
 package com.usky.sas.service.impl;
 
+import cn.hutool.core.util.StrUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.usky.common.core.bean.CommonPage;
 import com.usky.common.mybatis.core.AbstractCrudService;
 import com.usky.sas.domain.SasEventTypeGroup;
+import com.usky.sas.enums.SystemTypeCodeEnum;
 import com.usky.sas.mapper.SasEventTypeGroupMapper;
+import com.usky.sas.service.SasEventCodeService;
 import com.usky.sas.service.SasEventTypeGroupService;
+import com.usky.sas.service.vo.EventGroupInfo;
 import com.usky.sas.service.vo.EventGroupPageRequest;
 import com.usky.sas.service.vo.EventGroupSaveRequest;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.List;
 import java.util.UUID;
 import java.util.stream.Collectors;
 
@@ -20,8 +27,11 @@ import java.util.stream.Collectors;
 public class SasEventTypeGroupServiceImpl extends AbstractCrudService<SasEventTypeGroupMapper, SasEventTypeGroup>
         implements SasEventTypeGroupService {
 
+    @Autowired
+    private SasEventCodeService sasEventCodeService;
+
     @Override
-    public CommonPage<SasEventTypeGroup> page(EventGroupPageRequest request) {
+    public CommonPage<EventGroupInfo> page(EventGroupPageRequest request) {
         IPage<SasEventTypeGroup> page = new Page<>(request.getCurrent(), request.getSize());
         LambdaQueryWrapper<SasEventTypeGroup> wrapper = new LambdaQueryWrapper<>();
         if (request.getDeviceType() != null) {
@@ -32,7 +42,49 @@ public class SasEventTypeGroupServiceImpl extends AbstractCrudService<SasEventTy
         }
         wrapper.orderByDesc(SasEventTypeGroup::getCreateTime);
         page = this.page(page, wrapper);
-        return new CommonPage<>(page.getRecords(), page.getTotal(), page.getCurrent(), page.getSize());
+
+        List<EventGroupInfo> records = page.getRecords().stream()
+                .map(this::toEventGroupInfo)
+                .collect(Collectors.toList());
+
+        return new CommonPage<>(records, page.getTotal(), page.getCurrent(), page.getSize());
+    }
+
+    /**
+     * 将事件组实体转为带设备类型名称、事件编码列表的 VO
+     */
+    private EventGroupInfo toEventGroupInfo(SasEventTypeGroup group) {
+        EventGroupInfo info = new EventGroupInfo();
+        info.setId(group.getId());
+        info.setName(group.getName());
+        info.setDeviceType(group.getDeviceType());
+        info.setEventLevel(group.getEventLevel());
+        info.setCreateTime(group.getCreateTime());
+        info.setUpdateTime(group.getUpdateTime());
+        info.setCanDel(group.getCanDel());
+
+        Integer deviceType = group.getDeviceType();
+        SystemTypeCodeEnum typeEnum = SystemTypeCodeEnum.getByCode(deviceType);
+        if (typeEnum != null) {
+            info.setDeviceTypeName(typeEnum.getMessage());
+        }
+
+        if (group.getEventLevel() != null) {
+            info.setEventLevelName(String.valueOf(group.getEventLevel()));
+        }
+
+        if (StrUtil.isNotBlank(group.getEventCodes())) {
+            List<Integer> codes = Arrays.stream(group.getEventCodes().split(","))
+                    .map(String::trim)
+                    .filter(StrUtil::isNotBlank)
+                    .map(Integer::valueOf)
+                    .collect(Collectors.toList());
+            if (!codes.isEmpty()) {
+                info.setEventCodes(sasEventCodeService.getEventCodesByIds(deviceType, codes));
+            }
+        }
+
+        return info;
     }
 
     @Override

+ 76 - 3
service-sas/service-sas-biz/src/main/java/com/usky/sas/service/impl/SasMapServiceImpl.java

@@ -11,6 +11,7 @@ import com.usky.sas.mapper.SasMapDeviceMapper;
 import com.usky.sas.mapper.SasMapsMapper;
 import com.usky.sas.service.SasMapService;
 import com.usky.sas.service.vo.MapDeviceBindRequest;
+import com.usky.sas.service.vo.MapListItem;
 import com.usky.sas.service.vo.MapPageRequest;
 import com.usky.sas.service.vo.MapSaveRequest;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -18,7 +19,11 @@ import org.springframework.stereotype.Service;
 
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
 import java.util.UUID;
+import java.util.stream.Collectors;
 
 @Service
 public class SasMapServiceImpl extends AbstractCrudService<SasMapsMapper, SasMaps>
@@ -28,7 +33,7 @@ public class SasMapServiceImpl extends AbstractCrudService<SasMapsMapper, SasMap
     private SasMapDeviceMapper mapDeviceMapper;
 
     @Override
-    public CommonPage<SasMaps> page(MapPageRequest request) {
+    public CommonPage<MapListItem> page(MapPageRequest request) {
         IPage<SasMaps> page = new Page<>(request.getCurrent(), request.getSize());
         LambdaQueryWrapper<SasMaps> wrapper = new LambdaQueryWrapper<>();
         if (request.getParentId() != null) {
@@ -36,7 +41,52 @@ public class SasMapServiceImpl extends AbstractCrudService<SasMapsMapper, SasMap
         }
         wrapper.orderByDesc(SasMaps::getCreateTime);
         page = this.page(page, wrapper);
-        return new CommonPage<>(page.getRecords(), page.getTotal(), page.getCurrent(), page.getSize());
+
+        List<SasMaps> records = page.getRecords();
+        if (records == null || records.isEmpty()) {
+            return new CommonPage<>(Collections.emptyList(), page.getTotal(), page.getCurrent(), page.getSize());
+        }
+
+        List<String> mapIds = records.stream()
+                .map(SasMaps::getId)
+                .collect(Collectors.toList());
+
+        LambdaQueryWrapper<SasMapDevice> deviceWrapper = new LambdaQueryWrapper<>();
+        deviceWrapper.in(SasMapDevice::getMapId, mapIds);
+        List<SasMapDevice> mapDevices = mapDeviceMapper.selectList(deviceWrapper);
+        Map<String, List<SasMapDevice>> deviceGroup = mapDevices.stream()
+                .collect(Collectors.groupingBy(SasMapDevice::getMapId));
+
+        List<MapListItem> items = records.stream().map(map -> {
+            MapListItem item = new MapListItem();
+            item.setId(map.getId());
+            item.setName(map.getName());
+            item.setType(map.getType());
+            item.setRemark(map.getRemark());
+            item.setParentId(map.getParentId());
+            item.setWidth(map.getWidth());
+            item.setHeight(map.getHeight());
+            item.setIsMask(map.getIsMask());
+            item.setBackImgId(map.getBackImgId());
+            item.setCreateTime(map.getCreateTime());
+            item.setUpdateTime(map.getUpdateTime());
+
+            List<SasMapDevice> devices = deviceGroup.getOrDefault(map.getId(), Collections.emptyList());
+            List<MapListItem.MapDeviceItem> deviceItems = devices.stream().map(d -> {
+                MapListItem.MapDeviceItem di = new MapListItem.MapDeviceItem();
+                di.setId(d.getId());
+                di.setDeviceId(d.getDeviceId());
+                di.setDeviceName(d.getText());
+                di.setType(d.getType());
+                di.setX(d.getX() != null ? d.getX().doubleValue() : null);
+                di.setY(d.getY() != null ? d.getY().doubleValue() : null);
+                return di;
+            }).collect(Collectors.toList());
+            item.setDevices(deviceItems);
+            return item;
+        }).collect(Collectors.toList());
+
+        return new CommonPage<>(items, page.getTotal(), page.getCurrent(), page.getSize());
     }
 
     @Override
@@ -65,7 +115,7 @@ public class SasMapServiceImpl extends AbstractCrudService<SasMapsMapper, SasMap
 
     @Override
     public void delete(String id) {
-        this.removeById(id);
+        deleteRecursively(id);
     }
 
     @Override
@@ -102,6 +152,29 @@ public class SasMapServiceImpl extends AbstractCrudService<SasMapsMapper, SasMap
         mapDeviceMapper.delete(wrapper);
     }
 
+    /**
+     * 递归删除地图及其子地图,同时删除绑定设备
+     */
+    private void deleteRecursively(String id) {
+        // 删除子地图
+        LambdaQueryWrapper<SasMaps> childWrapper = new LambdaQueryWrapper<>();
+        childWrapper.eq(SasMaps::getParentId, id);
+        List<SasMaps> children = this.list(childWrapper);
+        if (children != null && !children.isEmpty()) {
+            for (SasMaps child : children) {
+                deleteRecursively(child.getId());
+            }
+        }
+
+        // 删除当前地图下绑定设备
+        LambdaQueryWrapper<SasMapDevice> deviceWrapper = new LambdaQueryWrapper<>();
+        deviceWrapper.eq(SasMapDevice::getMapId, id);
+        mapDeviceMapper.delete(deviceWrapper);
+
+        // 删除当前地图
+        this.removeById(id);
+    }
+
     private void applyMap(SasMaps map, MapSaveRequest request) {
         map.setName(request.getName());
         map.setType(request.getType());

+ 83 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/service/impl/SasVideoMonitorGroupTypeServiceImpl.java

@@ -0,0 +1,83 @@
+package com.usky.sas.service.impl;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.usky.common.core.bean.CommonPage;
+import com.usky.common.mybatis.core.AbstractCrudService;
+import com.usky.sas.domain.SasDevice;
+import com.usky.sas.domain.SasVideoMonitorGroupType;
+import com.usky.sas.mapper.SasVideoMonitorGroupTypeMapper;
+import com.usky.sas.service.SasDeviceService;
+import com.usky.sas.service.SasVideoMonitorGroupTypeService;
+import com.usky.sas.service.vo.VideoMonitorGroupPageRequest;
+import com.usky.sas.service.vo.VideoMonitorGroupSaveRequest;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.UUID;
+
+@Service
+public class SasVideoMonitorGroupTypeServiceImpl extends AbstractCrudService<SasVideoMonitorGroupTypeMapper, SasVideoMonitorGroupType>
+        implements SasVideoMonitorGroupTypeService {
+
+    @Autowired
+    private SasDeviceService sasDeviceService;
+
+    @Override
+    public List<SasVideoMonitorGroupType> listAll() {
+        return this.list();
+    }
+
+    @Override
+    public CommonPage<SasVideoMonitorGroupType> page(VideoMonitorGroupPageRequest request) {
+        IPage<SasVideoMonitorGroupType> page = new Page<>(request.getCurrent(), request.getSize());
+        page = this.page(page);
+        return new CommonPage<>(page.getRecords(), page.getTotal(), page.getCurrent(), page.getSize());
+    }
+
+    @Override
+    public String create(VideoMonitorGroupSaveRequest request) {
+        String id = UUID.randomUUID().toString();
+        SasVideoMonitorGroupType group = new SasVideoMonitorGroupType();
+        group.setId(id);
+        group.setName(request.getName());
+        group.setNote(request.getNote());
+        this.save(group);
+        bindDevices(id, request.getDeviceIds());
+        return id;
+    }
+
+    @Override
+    public void update(String id, VideoMonitorGroupSaveRequest request) {
+        SasVideoMonitorGroupType group = this.getById(id);
+        if (group == null) {
+            return;
+        }
+        group.setName(request.getName());
+        group.setNote(request.getNote());
+        this.updateById(group);
+        bindDevices(id, request.getDeviceIds());
+    }
+
+    @Override
+    public void delete(String id) {
+        this.removeById(id);
+    }
+
+    private void bindDevices(String groupId, List<String> deviceIds) {
+        if (deviceIds == null) {
+            return;
+        }
+        for (String deviceId : deviceIds) {
+            SasDevice device = sasDeviceService.getById(deviceId);
+            if (device == null) {
+                continue;
+            }
+            device.setVideoGroupType(groupId);
+            device.setUpdateTime(LocalDateTime.now());
+            sasDeviceService.updateById(device);
+        }
+    }
+}

+ 13 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/service/vo/DeviceAlarmGroupBatchSetRequest.java

@@ -0,0 +1,13 @@
+package com.usky.sas.service.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class DeviceAlarmGroupBatchSetRequest {
+
+    private List<String> ids;
+
+    private String alarmGroupType;
+}

+ 13 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/service/vo/DeviceAlarmGroupSetRequest.java

@@ -0,0 +1,13 @@
+package com.usky.sas.service.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class DeviceAlarmGroupSetRequest {
+
+    private String id;
+
+    private String alarmGroupType;
+}

+ 9 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/service/vo/DeviceConfigVO.java

@@ -38,5 +38,14 @@ public class DeviceConfigVO {
     private LocalDateTime createTime;
 
     private LocalDateTime updateTime;
+
+    /** 设备类型名称(如:视频安防监控) */
+    private String deviceTypeName;
+
+    /** 报警联动组名称 */
+    private String alarmGroupTypeName;
+
+    /** 视频监控组名称 */
+    private String videoGroupTypeName;
 }
 

+ 11 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/service/vo/DeviceDeleteBatchRequest.java

@@ -0,0 +1,11 @@
+package com.usky.sas.service.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class DeviceDeleteBatchRequest {
+
+    private List<String> ids;
+}

+ 54 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/service/vo/DeviceDetailVO.java

@@ -0,0 +1,54 @@
+package com.usky.sas.service.vo;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 设备详情(含设备类型名称、报警组名称、视频组名称等)
+ */
+@Data
+public class DeviceDetailVO {
+
+    private String id;
+
+    private String deviceId;
+
+    private Integer channel;
+
+    private Integer deviceType;
+
+    private String deviceTypeName;
+
+    private String ipAddr;
+
+    private Integer port;
+
+    private String username;
+
+    private Boolean shield;
+
+    private String villageCode;
+
+    private String houseCode;
+
+    private String address;
+
+    private String videoGroupType;
+
+    private String videoGroupTypeName;
+
+    private String alarmGroupType;
+
+    private String alarmGroupTypeName;
+
+    private String note;
+
+    private String bindingIpc;
+
+    private Boolean isBinding;
+
+    private LocalDateTime createTime;
+
+    private LocalDateTime updateTime;
+}

+ 20 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/service/vo/DeviceIPCItemVO.java

@@ -0,0 +1,20 @@
+package com.usky.sas.service.vo;
+
+import lombok.Data;
+
+/**
+ * 未绑定 IPC 设备项(用于地图/报警等选择设备)
+ */
+@Data
+public class DeviceIPCItemVO {
+
+    private String id;
+
+    private String deviceId;
+
+    private Integer channel;
+
+    private String ipAddr;
+
+    private String note;
+}

+ 78 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/service/vo/DeviceImportTemplateVO.java

@@ -0,0 +1,78 @@
+package com.usky.sas.service.vo;
+
+import cn.afterturn.easypoi.excel.annotation.Excel;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 设备导入模板行(用于导出 Excel 模板)
+ */
+@Data
+public class DeviceImportTemplateVO {
+
+    /** 设备类型编码(必填,对应 SystemTypeCodeEnum.code) */
+    @Excel(name = "设备类型(必填)")
+    private Integer deviceType;
+
+    /** 设备编号(必填) */
+    @Excel(name = "设备编号(必填)")
+    private String deviceId;
+
+    /** 设备 IP(必填) */
+    @Excel(name = "设备IP(必填)")
+    private String ipAddr;
+
+    /** 设备端口(必填) */
+    @Excel(name = "端口(必填)")
+    private Integer port;
+
+    /** 通道号(NVR 场景使用) */
+    @Excel(name = "通道号")
+    private Integer channel;
+
+    /** 视频设备类型:如 1-IPC、2-NVR 等(预留字段) */
+    @Excel(name = "视频设备类型")
+    private Integer videoDeviceType;
+
+    /** 视频协议:如 1-ONVIF、2-海康、3-大华 等(预留字段) */
+    @Excel(name = "视频协议")
+    private Integer videoProtocol;
+
+    /** 登录用户名 */
+    @Excel(name = "用户名")
+    private String username;
+
+    /** 登录密码 */
+    @Excel(name = "密码")
+    private String password;
+
+    /** 备注 */
+    @Excel(name = "备注")
+    private String note;
+
+    /** 安装位置描述 */
+    @Excel(name = "安装位置")
+    private String address;
+
+    /** 部位编码(houseCode) */
+    @Excel(name = "部位编码(houseCode)")
+    private String houseCode;
+
+    /** 经度 */
+    @Excel(name = "经度")
+    private BigDecimal lon;
+
+    /** 纬度 */
+    @Excel(name = "纬度")
+    private BigDecimal lat;
+
+    /** 高度 */
+    @Excel(name = "高度")
+    private BigDecimal alt;
+
+    /** 楼层 */
+    @Excel(name = "楼层")
+    private Integer floor;
+}
+

+ 13 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/service/vo/DeviceVideoGroupBatchSetRequest.java

@@ -0,0 +1,13 @@
+package com.usky.sas.service.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class DeviceVideoGroupBatchSetRequest {
+
+    private List<String> ids;
+
+    private String videoGroupType;
+}

+ 11 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/service/vo/DeviceVideoGroupSetRequest.java

@@ -0,0 +1,11 @@
+package com.usky.sas.service.vo;
+
+import lombok.Data;
+
+@Data
+public class DeviceVideoGroupSetRequest {
+
+    private String id;
+
+    private String videoGroupType;
+}

+ 20 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/service/vo/EventCodeInfo.java

@@ -0,0 +1,20 @@
+package com.usky.sas.service.vo;
+
+import lombok.Data;
+
+/**
+ * 事件编码/子类型信息(用于按系统类型返回编码列表)
+ */
+@Data
+public class EventCodeInfo {
+
+    /**
+     * 编码(或 gauth 的 functions)
+     */
+    private Integer code;
+
+    /**
+     * 名称
+     */
+    private String name;
+}

+ 27 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/service/vo/EventGroupInfo.java

@@ -0,0 +1,27 @@
+package com.usky.sas.service.vo;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 事件组分页/详情返回 VO(含设备类型名称、事件等级名称、事件编码列表)
+ */
+@Data
+public class EventGroupInfo {
+
+    private String id;
+    private String name;
+    private Integer deviceType;
+    /** 设备类型名称(如:实时智能分析) */
+    private String deviceTypeName;
+    /** 事件编码列表(按设备类型解析后的 EventCodeInfo) */
+    private List<EventCodeInfo> eventCodes;
+    private Integer eventLevel;
+    /** 事件等级名称 */
+    private String eventLevelName;
+    private LocalDateTime createTime;
+    private LocalDateTime updateTime;
+    private Boolean canDel;
+}

+ 51 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/service/vo/MapListItem.java

@@ -0,0 +1,51 @@
+package com.usky.sas.service.vo;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 电子地图配置列表项,包含地图基础信息及绑定设备简要信息
+ */
+@Data
+public class MapListItem {
+
+    private String id;
+
+    private String name;
+
+    private Integer type;
+
+    private String remark;
+
+    private String parentId;
+
+    private Double width;
+
+    private Double height;
+
+    private Boolean isMask;
+
+    private String backImgId;
+
+    private LocalDateTime createTime;
+
+    private LocalDateTime updateTime;
+
+    /**
+     * 绑定在该地图上的设备列表
+     */
+    private List<MapDeviceItem> devices;
+
+    @Data
+    public static class MapDeviceItem {
+        private String id;
+        private String deviceId;
+        private String deviceName;
+        private Double x;
+        private Double y;
+        private String type;
+    }
+}
+

+ 11 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/service/vo/VideoMonitorGroupPageRequest.java

@@ -0,0 +1,11 @@
+package com.usky.sas.service.vo;
+
+import lombok.Data;
+
+@Data
+public class VideoMonitorGroupPageRequest {
+
+    private Integer current = 1;
+
+    private Integer size = 10;
+}

+ 15 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/service/vo/VideoMonitorGroupSaveRequest.java

@@ -0,0 +1,15 @@
+package com.usky.sas.service.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class VideoMonitorGroupSaveRequest {
+
+    private String name;
+
+    private String note;
+
+    private List<String> deviceIds;
+}

+ 11 - 0
service-sas/service-sas-biz/src/main/resources/mapper/sas/SasVideoMonitorGroupTypeMapper.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.usky.sas.mapper.SasVideoMonitorGroupTypeMapper">
+
+    <resultMap id="BaseResultMap" type="com.usky.sas.domain.SasVideoMonitorGroupType">
+        <id column="id" property="id"/>
+        <result column="name" property="name"/>
+        <result column="note" property="note"/>
+    </resultMap>
+
+</mapper>