134 Commits d38d229e17 ... 08ffdfd482

Auteur SHA1 Message Date
  zhaojinyu 08ffdfd482 优化安防系统信息中客户端ID重新生成逻辑 il y a 1 semaine
  hanzhengyi 0caf41707b Merge branch 'feature/service-vpp-20260701' of uskycloud/usky-modules into master il y a 2 semaines
  fuyuchuan d05125be86 Merge branch 'fyc-vpp' of uskycloud/usky-modules into feature/service-vpp-20260701 il y a 2 semaines
  fuyuchuan 18b9f85b01 将已停用的 mock 假数据改为注释保留 il y a 2 semaines
  fuyuchuan 9f72d9c776 响应监测曲线改为单站点查询,并移除 vpp 模块 mock 假数据 il y a 2 semaines
  fuyuchuan d5ce170ce4 效果评-估响应监控调整 il y a 2 semaines
  fuyuchuan ea6bac9788 监测曲线复用基线方法,调整补贴完成率与偏差率 il y a 2 semaines
  fuyuchuan 05133988a7 Merge branch 'fyc-vpp' of uskycloud/usky-modules into feature/service-vpp-20260701 il y a 2 semaines
  fuyuchuan a463e328fc 设备新增运管资源ID,站点省份改为模糊查询 il y a 2 semaines
  fuyuchuan 977ebe5750 Merge branch 'fyc-vpp' of uskycloud/usky-modules into feature/service-vpp-20260701 il y a 2 semaines
  fuyuchuan 57c2f03dd8 补贴预测新增客户ID与名称 il y a 2 semaines
  fuyuchuan d0b5e3238f Merge branch 'fyc-vpp' of uskycloud/usky-modules into feature/service-vpp-20260701 il y a 2 semaines
  fuyuchuan 6980b64f8f 补贴金额&响应量偏差价格调整 il y a 2 semaines
  fuyuchuan 921e9602c3 Merge branch 'fyc-vpp' of uskycloud/usky-modules into feature/service-vpp-20260701 il y a 2 semaines
  fuyuchuan 991ca3b335 接口调整 il y a 2 semaines
  fuyuchuan dad5ad7b13 Merge branch 'fyc-vpp' of uskycloud/usky-modules into feature/service-vpp-20260701 il y a 2 semaines
  fuyuchuan 71e5b46f97 新增响应量偏差价格调整表&优化补贴金额预测 il y a 2 semaines
  fuyuchuan 57344113e3 补充下浮系数 il y a 2 semaines
  fuyuchuan be834dc538 合同关联代码调整 il y a 2 semaines
  fuyuchuan b50f7e81e1 Merge branch 'fyc-vpp' of uskycloud/usky-modules into feature/service-vpp-20260701 il y a 2 semaines
  fuyuchuan 7c2de9cbf8 客户需求调整代码 il y a 2 semaines
  fanghuisheng f30824eb9b Merge branch 'fanghuisheng' of uskycloud/usky-modules into master il y a 3 semaines
  fanghuisheng fedf4fadc5 调整门禁记录接口字段处理逻辑 il y a 3 semaines
  fanghuisheng 706c18197c Merge branch 'fanghuisheng' of uskycloud/usky-modules into master il y a 3 semaines
  fanghuisheng 07ab221fe5 门禁pom配置修改 il y a 3 semaines
  fanghuisheng dda0fd204a Merge branch 'fanghuisheng' of uskycloud/usky-modules into master il y a 3 semaines
  fuyuchuan 4fb50cefb3 Merge branch 'fyc-vpp' of uskycloud/usky-modules into feature/service-vpp-20260701 il y a 3 semaines
  fuyuchuan 9e7d14288b 配置文件迁移nacos&更新短信模板 il y a 3 semaines
  hanzhengyi 5da21b5c54 Merge branch 'usky-zyj' of uskycloud/usky-modules into master il y a 3 semaines
  james c4d13cbd77 优化告警列表接口中获取满足条件的总数记录值 il y a 3 semaines
  hanzhengyi b1f7ec5c34 Merge branch 'feature/service-vpp-20260701' of uskycloud/usky-modules into master il y a 3 semaines
  fuyuchuan 279e62405a Merge branch 'fyc-vpp' of uskycloud/usky-modules into feature/service-vpp-20260701 il y a 3 semaines
  fuyuchuan 4a4f715c8e 适配需求响应审核短信模板变量组装 il y a 3 semaines
  fuyuchuan f17a76bd47 Merge branch 'fyc-vpp' of uskycloud/usky-modules into feature/service-vpp-20260701 il y a 3 semaines
  fuyuchuan ca0f16672d 完善合同模板与客户删除校验:有关联合同时禁止删除 il y a 3 semaines
  fanghuisheng 60f56f5df3 完成会议接口调整/新增人员绑定接口 il y a 3 semaines
  fanghuisheng 45178db9d7 门禁人员绑定列表接口新增返回字段 il y a 3 semaines
  hanzhengyi b67736b6d9 Merge branch 'usky-zyj' of uskycloud/usky-modules into master il y a 3 semaines
  james b8c602cf12 优化所有调用消息中心API方法的逻辑,增加传入tenantId的处理逻辑 il y a 3 semaines
  fuyuchuan 3a8ba9dcd8 Merge branch 'fyc-vpp' of uskycloud/usky-modules into feature/service-vpp-20260701 il y a 3 semaines
  fuyuchuan a74b2ee335 优化执行监控-负荷分析&对比分析 il y a 3 semaines
  fuyuchuan 69fd5896aa 档案管理代码优化 il y a 3 semaines
  fanghuisheng 214e713de7 Merge branch 'fanghuisheng' of uskycloud/usky-modules into master il y a 3 semaines
  fanghuisheng 27f821ba9c Merge branch 'master' of http://47.111.81.118:3000/uskycloud/usky-modules into fanghuisheng il y a 3 semaines
  fuyuchuan c4d0ffbcb8 过期合同添加草稿排查 il y a 3 semaines
  fuyuchuan d2da97e0fd Merge branch 'feature/service-vpp-20260701' of uskycloud/usky-modules into master il y a 3 semaines
  fuyuchuan f11c4d7450 Merge branch 'fyc-vpp' of uskycloud/usky-modules into feature/service-vpp-20260701 il y a 3 semaines
  fuyuchuan 85d8779110 竞价配置字段调整 il y a 3 semaines
  hanzhengyi b129f1e10f Merge branch 'usky-zyj' of uskycloud/usky-modules into master il y a 3 semaines
  james 830f125cd6 优化告警中心推送短信逻辑,修改短信推送key等信息1 il y a 3 semaines
  james 4eb3e8ed9c 优化告警中心推送短信逻辑,修改短信推送key等信息 il y a 3 semaines
  fuyuchuan 714d6342ec Merge branch 'fyc-vpp' of uskycloud/usky-modules into feature/service-vpp-20260701 il y a 3 semaines
  fuyuchuan efd5905d22 档案管理代码优化&客户软删新增合同判断 il y a 3 semaines
  fuyuchuan 42f76aa39a Merge branch 'fyc-vpp' of uskycloud/usky-modules into feature/service-vpp-20260701 il y a 3 semaines
  fuyuchuan 8a608578c8 合同管理接口优化 il y a 4 semaines
  fuyuchuan e825a9149d Merge branch 'fyc-vpp' of uskycloud/usky-modules into feature/service-vpp-20260701 il y a 4 semaines
  james 7e0cd834a0 Merge branch 'vpp-zyj' into feature/service-vpp-20260701 il y a 4 semaines
  james 44b8093fd9 优化响应申报容量接口,增加获取预估响应容量的逻辑 il y a 4 semaines
  fuyuchuan cb792a609a 新增客户合同到期查询&去掉竞价配置关联站点 il y a 4 semaines
  fuyuchuan 80b343a387 Merge branch 'fyc-vpp' of uskycloud/usky-modules into feature/service-vpp-20260701 il y a 4 semaines
  james f1e2755e80 优化设备数据导入时序数据库接口,增加过滤空值的逻辑,记录报错信息到error日志中,然后继续下一个记录的操作 il y a 4 semaines
  fuyuchuan 9f95cf5a01 优化json字段校验逻辑,放开空值&空字符串限制 il y a 4 semaines
  james edf7557ff8 优化基线管理 - 负荷曲线和响应申报容量两个接口,删除站点有资源点但是设备没有对应数据的mock数据 il y a 4 semaines
  james 533e8b2067 1、优化基线管理 - 负荷曲线和响应申报容量两个接口,调整资源点刷选条件从是否可控改为是否可参与调峰,同时删除站点没有资源点的mock数据; il y a 4 semaines
  hanzhengyi f39e380bfd Merge branch 'feature/service-vpp-20260701' of uskycloud/usky-modules into master il y a 4 semaines
  fuyuchuan cfd368a731 Merge branch 'fyc-vpp' of uskycloud/usky-modules into feature/service-vpp-20260701 il y a 4 semaines
  fuyuchuan 43e380b620 回退cid日志 il y a 4 semaines
  fuyuchuan 55c32ea02c Merge branch 'fyc-vpp' of uskycloud/usky-modules into feature/service-vpp-20260701 il y a 4 semaines
  fuyuchuan ae502f05ea 提交效果评估代码 il y a 4 semaines
  james b7e00022f6 Merge branch 'vpp-zyj' into feature/service-vpp-20260701 il y a 4 semaines
  james 123bcdfad8 开发service-vpp服务的导入设备时序数据到 TDengine接口 il y a 4 semaines
  fuyuchuan 90ed059bd3 修改日志存放路径 il y a 1 mois
  hanzhengyi 828c8993fa Merge branch 'feature/service-vpp-20260701' of uskycloud/usky-modules into master il y a 1 mois
  james 42d11ef66e 解决冲突 il y a 1 mois
  hanzhengyi b33a6fd891 Merge branch 'vpp-zyj' of uskycloud/usky-modules into master il y a 1 mois
  james f127fc4b48 解决冲突 il y a 1 mois
  James 13d213c4d3 Merge branch 'usky-zyj' of uskycloud/usky-modules into master il y a 1 mois
  james 52d1c3c546 优化告警中心订阅mqtt告警信息逻辑,根据设备Id和公司编码查询对应设备的产品编码 il y a 1 mois
  fuyuchuan 30ae0ef103 Merge branch 'fyc-vpp' of uskycloud/usky-modules into feature/service-vpp-20260701 il y a 1 mois
  fuyuchuan 7c054a12b0 大屏接口代码优化调整 il y a 1 mois
  james eed1520320 解决冲突 il y a 1 mois
  james dc3d7c97d5 优化站点管理 - 分页列表\设备信息-列表\资源概览 - 今日电能概况\首页 - 实时曲线四个接口 il y a 1 mois
  fuyuchuan 7dfdefdad5 Merge branch 'fyc-vpp' of uskycloud/usky-modules into feature/service-vpp-20260701 il y a 1 mois
  fuyuchuan cff4ca2947 大屏接口&响应事件、邀约代码调整 il y a 1 mois
  fuyuchuan 59258ba2b8 需求响应接口调整:添加字段 il y a 1 mois
  fuyuchuan c43894800f Merge branch 'fyc-vpp' of uskycloud/usky-modules into feature/service-vpp-20260701 il y a 1 mois
  fuyuchuan 65d45ccb02 新增补贴费用预测&根据新需求调整代码 il y a 1 mois
  James 14795c7ca7 Merge branch 'feature/service-vpp-20260701' of uskycloud/usky-modules into master il y a 1 mois
  hanzhengyi 583990e806 Merge branch 'usky-zyj' of uskycloud/usky-modules into master il y a 1 mois
  james fc8e3f46c2 优化产品信息表-新增、产品信息表-修改两个接口,增加原子性操作1 il y a 1 mois
  hanzhengyi bf34145423 Merge branch 'han' of uskycloud/usky-modules into master il y a 1 mois
  hanzhengyi b039e6b2cb 巡检子计划定时任务更新 il y a 1 mois
  james 312094dcb8 Merge branch 'vpp-zyj' into feature/service-vpp-20260701 il y a 1 mois
  james a9bae1cfd7 优化资源看板和资源概览两个界面的相关接口 il y a 1 mois
  hanzhengyi 0ca6052dbb Merge branch 'usky-zyj' of uskycloud/usky-modules into master il y a 1 mois
  james 09a9402353 解决冲突 il y a 1 mois
  hanzhengyi 88717b4f6d Merge branch 'han' of uskycloud/usky-modules into master il y a 1 mois
  hanzhengyi 80ee6825a3 巡检子计划定时任务更新 il y a 1 mois
  hanzhengyi f8932de95b service-vpp模块代码更新 il y a 1 mois
  james 7781a58805 Merge branch 'vpp-zyj' into feature/service-vpp-20260701 il y a 1 mois
  james 37c2b1d5ea 优化站点管理-分页列表接口,增加站点户号和所属街镇值的回显 il y a 1 mois
  fuyuchuan 6b7adedbca Merge branch 'fyc-vpp' of uskycloud/usky-modules into feature/service-vpp-20260701 il y a 1 mois
  fuyuchuan 43ff60a5be 准入管理接口代码优化,优化必填项等 il y a 1 mois
  zhaojinyu d38d229e17 优化巡检记录定时任务调度器可多线程;巡检子计划为向前预生成today+7+补最近7天缺口 il y a 1 mois
  james d28b2a8967 Merge branch 'vpp-zyj' into feature/service-vpp-20260701 il y a 1 mois
  james 650bd2be68 开发基线管理 - 负荷曲线接口,开发响应申报容量接口,优化站点管理 - 分页列表、站点管理 - 新增、站点管理 - 编辑三个接口,增加所属户号和所属街镇两个字段的逻辑 il y a 1 mois
  james c28f72a29f Merge branch 'vpp-zyj' into feature/service-vpp-20260701 il y a 1 mois
  james 1c19035f0f 开发基线管理 - 负荷曲线接口,优化站点管理 - 分页列表、站点管理 - 新增、站点管理 - 编辑三个接口,增加所属户号和所属街镇两个字段的逻辑 il y a 1 mois
  fuyuchuan 762efe4972 Merge branch 'fyc-vpp' of uskycloud/usky-modules into feature/service-vpp-20260701 il y a 1 mois
  fuyuchuan 783d235fb0 准入管理调整&电费核算 il y a 1 mois
  james b642f23f70 Merge branch 'vpp-zyj' into feature/service-vpp-20260701 il y a 1 mois
  james 85cad5ab69 优化站点下新增设备、资源管理-设备列表两个接口,增加所属户号和所属街镇两个字段的逻辑 il y a 1 mois
  fuyuchuan 86b46b3ffb 代码调整 il y a 1 mois
  fuyuchuan b9c5bc2408 调整电力户号、客户名称校验 il y a 1 mois
  fuyuchuan 6cdd60b2e8 Merge branch 'fyc-vpp' of uskycloud/usky-modules into feature/service-vpp-20260701 il y a 1 mois
  fuyuchuan 72a1e02ac2 Merge branch 'feature/service-vpp-20260701' of http://47.111.81.118:3000/uskycloud/usky-modules into fyc-vpp il y a 1 mois
  fuyuchuan 024965afcc 电量核算代码提交 il y a 1 mois
  fuyuchuan 528790a66b Merge branch 'fyc-vpp' of uskycloud/usky-modules into feature/service-vpp-20260701 il y a 1 mois
  james c728779f77 优化设备信息表-新增接口,增加业务状态传值的判断取值 il y a 1 mois
  fuyuchuan 1a292e981e Merge branch 'feature/service-vpp-20260701' of http://47.111.81.118:3000/uskycloud/usky-modules into fyc-vpp il y a 1 mois
  james fc9d927c99 优化告警信息表-新增接口,增加报错日志的记录 il y a 1 mois
  james 966f8cd336 优化告警信息表-新增接口,增加报错日志的记录,优化设备信息表-新增接口,增加业务状态传值的判断取值 il y a 1 mois
  fuyuchuan 94f1078e86 客户管理接口优化;电量核算代码部分提交 il y a 1 mois
  hanzhengyi fb49c1cf2b service-vpp模块代码更新 il y a 1 mois
  hanzhengyi df6791ced3 service-vpp模块代码更新 il y a 1 mois
  james 94af9207a3 优化资源管理-设备信息-修改接口,去除设备类型必录的校验 il y a 1 mois
  fuyuchuan 90f4f26080 新增‘修改驳回准入’接口;优化合同状态动态调整 il y a 1 mois
  fuyuchuan 6df3eadc35 优化代码 il y a 1 mois
  james 117543b8a6 优化产品信息表-新增、产品信息表-修改两个接口,增加原子性操作 il y a 1 mois
  fuyuchuan 02c37f02ec 合同状态调整 il y a 1 mois
  fanghuisheng 710a225fad 应用远程更新功能模块完成 il y a 2 mois
  fanghuisheng fe23e1b11e Merge branch 'master' of http://47.111.81.118:3000/uskycloud/usky-modules into fanghuisheng il y a 2 mois
  fanghuisheng d466f0d50f Merge branch 'master' of http://47.111.81.118:3000/uskycloud/usky-modules into fanghuisheng il y a 3 mois
  fanghuisheng 5cf096ee74 移除门禁服务内冗余代码/调整门禁绑定相关功能代码 il y a 3 mois
100 fichiers modifiés avec 2746 ajouts et 889 suppressions
  1. 3 2
      service-alarm/service-alarm-biz/src/main/java/com/usky/alarm/service/impl/BaseAlarmNoticeResultServiceImpl.java
  2. 6 2
      service-alarm/service-alarm-biz/src/main/java/com/usky/alarm/service/impl/BaseAlarmServiceImpl.java
  3. 3 1
      service-alarm/service-alarm-biz/src/main/java/com/usky/alarm/service/listener/MqttListener.java
  4. 1 0
      service-alarm/service-alarm-biz/src/main/java/com/usky/alarm/service/mqtt/alarm/Alarm.java
  5. 4 2
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/SipPushController.java
  6. 10 1
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/SipRtspPushService.java
  7. 8 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/sip/config/SipClientProperties.java
  8. 1 1
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/sip/config/SipServerProperties.java
  9. 101 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/sip/device/Gb28181CatalogBuilder.java
  10. 162 14
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/sip/device/Gb28181DeviceSimulator.java
  11. 8 1
      service-cdi/service-cdi-biz/src/main/resources/application-gb28181-video.yml
  12. 82 0
      service-eg/service-eg-biz/pom.xml
  13. 10 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/adb/EgDeviceAdbConfiguration.java
  14. 75 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/adb/EgDeviceAdbProperties.java
  15. 17 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/controller/api/ServiceEgTaskApi.java
  16. 97 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/controller/web/EgDeviceAdbController.java
  17. 0 86
      service-eg/service-eg-biz/src/main/java/com/usky/eg/domain/MeetingFace.java
  18. 0 32
      service-eg/service-eg-biz/src/main/java/com/usky/eg/domain/MeetingFaceDevice.java
  19. 0 7
      service-eg/service-eg-biz/src/main/java/com/usky/eg/mapper/EgDeviceMapper.java
  20. 0 18
      service-eg/service-eg-biz/src/main/java/com/usky/eg/mapper/MeetingFaceDeviceMapper.java
  21. 0 18
      service-eg/service-eg-biz/src/main/java/com/usky/eg/mapper/MeetingFaceMapper.java
  22. 6 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/mapper/SysPersonMapper.java
  23. 67 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/mqtt/EgMqttOutboundConfig.java
  24. 81 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/mqtt/EgMqttProperties.java
  25. 20 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/EgDeviceAdbService.java
  26. 19 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/EgDeviceRemoteUpgradeMqttService.java
  27. 325 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/impl/EgDeviceAdbServiceImpl.java
  28. 173 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/impl/EgDeviceRemoteUpgradeMqttServiceImpl.java
  29. 34 7
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/impl/EgDeviceServiceImpl.java
  30. 3 1
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/impl/EgRecordServiceImpl.java
  31. 13 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceAdbEndpointVO.java
  32. 19 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceAdbRemoteUpgradeBatchItemVO.java
  33. 21 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceAdbRemoteUpgradeBatchResultVO.java
  34. 34 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceAdbRemoteUpgradeItemResultVO.java
  35. 36 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceAdbRemoteUpgradeVO.java
  36. 29 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceAdbResultVO.java
  37. 15 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceAdbTcpipVO.java
  38. 4 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceBindFacePersonVO.java
  39. 66 0
      service-eg/service-eg-biz/src/main/resources/bootstrap.yml
  40. 0 24
      service-eg/service-eg-biz/src/main/resources/mapper/eg/EgDeviceMapper.xml
  41. 0 11
      service-eg/service-eg-biz/src/main/resources/mapper/eg/MeetingFaceDeviceMapper.xml
  42. 0 21
      service-eg/service-eg-biz/src/main/resources/mapper/eg/MeetingFaceMapper.xml
  43. 13 0
      service-eg/service-eg-biz/src/main/resources/mapper/eg/SysPersonMapper.xml
  44. 13 0
      service-fire/service-fire-api/src/main/java/com/usky/fire/RemotePlanSonService.java
  45. 33 0
      service-fire/service-fire-api/src/main/java/com/usky/fire/factory/RemotePlanSonFactory.java
  46. 35 0
      service-fire/service-fire-biz/src/main/java/com/usky/fire/controller/api/planSonApi.java
  47. 11 0
      service-fire/service-fire-biz/src/main/java/com/usky/fire/controller/web/PatrolInspectionPlanController.java
  48. 5 5
      service-fire/service-fire-biz/src/main/java/com/usky/fire/controller/web/TaskController.java
  49. 5 0
      service-fire/service-fire-biz/src/main/java/com/usky/fire/service/PatrolInspectionPlanService.java
  50. 0 32
      service-fire/service-fire-biz/src/main/java/com/usky/fire/service/config/SchedulerConfig.java
  51. 2 1
      service-fire/service-fire-biz/src/main/java/com/usky/fire/service/config/SyncPatrolInspectionEvent.java
  52. 1 1
      service-fire/service-fire-biz/src/main/java/com/usky/fire/service/impl/PatrolInspectionEventServiceImpl.java
  53. 166 61
      service-fire/service-fire-biz/src/main/java/com/usky/fire/service/impl/PatrolInspectionPlanServiceImpl.java
  54. 1 0
      service-iot/service-iot-biz/src/main/java/com/usky/iot/controller/web/DmpProductInfoController.java
  55. 1 0
      service-iot/service-iot-biz/src/main/java/com/usky/iot/service/config/DingTalkAndMessage.java
  56. 54 49
      service-iot/service-iot-biz/src/main/java/com/usky/iot/service/impl/BaseAlarmServiceImpl.java
  57. 4 1
      service-iot/service-iot-biz/src/main/java/com/usky/iot/service/impl/DmpDeviceInfoServiceImpl.java
  58. 3 0
      service-iot/service-iot-biz/src/main/java/com/usky/iot/service/impl/DmpProductInfoServiceImpl.java
  59. 2 0
      service-iot/service-iot-biz/src/main/java/com/usky/iot/service/impl/PmWorkReportServiceImpl.java
  60. 6 0
      service-job/pom.xml
  61. 21 0
      service-job/src/main/java/com/ruoyi/job/task/RyTask.java
  62. 0 10
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/annotation/FaceLog.java
  63. 0 50
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/annotation/aspect/FaceAspect.java
  64. 23 2
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/controller/web/MeetingDeviceController.java
  65. 0 182
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/controller/web/MeetingFaceController.java
  66. 0 60
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/controller/web/MeetingFaceVefLogController.java
  67. 6 0
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/domain/MeetingDevice.java
  68. 25 0
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/domain/MeetingDevicePersonBind.java
  69. 0 75
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/domain/MeetingFaceVefLog.java
  70. 48 0
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/domain/SysPerson.java
  71. 29 0
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/mapper/MeetingDevicePersonBindMapper.java
  72. 0 6
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/mapper/MeetingFaceMapper.java
  73. 0 16
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/mapper/MeetingFaceVefLogMapper.java
  74. 21 0
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/mapper/SysPersonMapper.java
  75. 0 19
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/repository/MeetingInfoRepository.java
  76. 12 1
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/MeetingDeviceService.java
  77. 0 16
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/MeetingFaceVefLogService.java
  78. 192 0
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/impl/MeetingDeviceServiceImpl.java
  79. 0 20
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/impl/MeetingFaceVefLogServiceImpl.java
  80. 0 1
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/impl/MeetingFileServiceImpl.java
  81. 23 0
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/vo/MeetingDeviceBindFacePersonQueryVO.java
  82. 27 0
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/vo/MeetingDeviceBindFacePersonVO.java
  83. 18 0
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/vo/MeetingDeviceBindPersonRequest.java
  84. 34 0
      service-meeting/service-meeting-biz/src/main/resources/mapper/meeting/MeetingDevicePersonBindMapper.xml
  85. 0 15
      service-meeting/service-meeting-biz/src/main/resources/mapper/meeting/MeetingFaceMapper.xml
  86. 0 16
      service-meeting/service-meeting-biz/src/main/resources/mapper/meeting/MeetingFaceVefLogMapper.xml
  87. 18 0
      service-meeting/service-meeting-biz/src/main/resources/mapper/meeting/SysPersonMapper.xml
  88. 3 0
      service-oa/service-oa-biz/src/main/java/com/usky/oa/service/impl/OaApprovalServiceImpl.java
  89. 1 0
      service-oa/service-oa-biz/src/main/java/com/usky/oa/service/impl/OaFbdDocumentServiceImpl.java
  90. 2 1
      service-oa/service-oa-biz/src/main/java/com/usky/oa/service/utils/OaSendMessageCenter.java
  91. 1 0
      service-pm/service-pm-biz/src/main/java/com/usky/pm/service/config/DingTalkAndMessage.java
  92. 2 0
      service-pm/service-pm-biz/src/main/java/com/usky/pm/service/impl/PmWorkReportServiceImpl.java
  93. 9 0
      service-tsdb/service-tsdb-api/pom.xml
  94. 9 0
      service-tsdb/service-tsdb-api/src/main/java/com/usky/demo/RemoteTsdbProxyService.java
  95. 23 0
      service-tsdb/service-tsdb-api/src/main/java/com/usky/demo/domain/DeviceCsvImportResultVO.java
  96. 7 0
      service-tsdb/service-tsdb-api/src/main/java/com/usky/demo/factory/RemoteTsdbProxyFallbackFactory.java
  97. 8 0
      service-tsdb/service-tsdb-api/src/main/java/com/usky/demo/feign/RemoteTsdbProxyFeignConfiguration.java
  98. 14 0
      service-tsdb/service-tsdb-biz/src/main/java/com/usky/demo/controller/api/DataTsdbProxyControllerApi.java
  99. 7 0
      service-tsdb/service-tsdb-biz/src/main/java/com/usky/demo/service/QueryTdengineDataService.java
  100. 255 0
      service-tsdb/service-tsdb-biz/src/main/java/com/usky/demo/service/impl/QueryTdengineDataServiceImpl.java

+ 3 - 2
service-alarm/service-alarm-biz/src/main/java/com/usky/alarm/service/impl/BaseAlarmNoticeResultServiceImpl.java

@@ -52,8 +52,8 @@ public class BaseAlarmNoticeResultServiceImpl extends AbstractCrudService<BaseAl
     private static final String SMS_SIGN_NAME = "上海永天科技股份有限公司";
     private static final String SMS_TEMPLATE_CODE = "SMS_463791105";
     private static final String REGION_ID = "cn-beijing";
-    private static final String ACCESS_KEY_ID = "LTAI5tH3VvRL5BUkovCokHJX";
-    private static final String ACCESS_KEY_SECRET = "SaaWUouNqvcA0C746gcNOH9m6SRYN4";
+    private static final String ACCESS_KEY_ID = "LTAI5t7GgkVKSdAbPWAzUq8U";
+    private static final String ACCESS_KEY_SECRET = "9zQ3e8GPTSlcfF1ev0JaBjlpvvSG6N";
 
     private static final String INFO_TYPE = "4";
     private static final String INFO_TITLE = "设备告警";
@@ -209,6 +209,7 @@ public class BaseAlarmNoticeResultServiceImpl extends AbstractCrudService<BaseAl
         mceRequestVO.setUserIds(Collections.singletonList(sysUser.getUserId()));
         mceRequestVO.setUserName(sysUser.getUserName());
         mceRequestVO.setId(baseAlarm.getId());
+        mceRequestVO.setTenantId(baseAlarm.getTenantId());
         return mceRequestVO;
     }
 

+ 6 - 2
service-alarm/service-alarm-biz/src/main/java/com/usky/alarm/service/impl/BaseAlarmServiceImpl.java

@@ -120,10 +120,14 @@ public class BaseAlarmServiceImpl extends AbstractCrudService<BaseAlarmMapper, B
                     return saveResult;
                 }
             }else {
-                throw new BusinessException("告警数据投递失败,请检查产品编码、设备编号、告警类型,是否有误;设备是否激活");
+                //throw new BusinessException("告警数据投递失败,请检查产品编码、设备编号、告警类型,是否有误;设备是否激活");
+                log.error("告警数据投递失败,请检查产品编码、设备编号、告警类型,是否有误;设备是否激活");
+                return false;
             }
         }else {
-            throw new BusinessException("告警数据投递失败,请检查产品编码、设备编号、告警类型,不能为空");
+            //throw new BusinessException("告警数据投递失败,请检查产品编码、设备编号、告警类型,不能为空");
+            log.error("告警数据投递失败,请检查产品编码、设备编号、告警类型,不能为空");
+            return false;
         }
     }
 

+ 3 - 1
service-alarm/service-alarm-biz/src/main/java/com/usky/alarm/service/listener/MqttListener.java

@@ -44,6 +44,7 @@ public class MqttListener {
             String[] code5 = TopListener.SMOKE_ALERT.getCode().split("/");
             String[] code6 = TopListener.ELECTRICAL_ALERT.getCode().split("/");
             String[] code7 = TopListener.LIQUID_ALERT.getCode().split("/");
+            String[] code8 = TopListener.LR_SMOKE_ALERT.getCode().split("/");
             Object mqttReceivedTopic = message.getHeaders().get("mqtt_receivedTopic");
             if (null != mqttReceivedTopic) {
                 String topic = mqttReceivedTopic.toString();
@@ -55,7 +56,8 @@ public class MqttListener {
                         (topic.indexOf(code4[2]) != -1 && topic.indexOf(code4[5]) != -1) ||
                         (topic.indexOf(code5[2]) != -1 && topic.indexOf(code5[5]) != -1) ||
                         (topic.indexOf(code6[2]) != -1 && topic.indexOf(code6[5]) != -1) ||
-                        (topic.indexOf(code7[2]) != -1 && topic.indexOf(code7[5]) != -1)) {
+                        (topic.indexOf(code7[2]) != -1 && topic.indexOf(code7[5]) != -1) ||
+                        (topic.indexOf(code8[2]) != -1 && topic.indexOf(code8[5]) != -1)) {
                     mqttBaseVO.setDescribe("alarm");
                 }else {
                     mqttBaseVO.setDescribe("fireInfoAndAlarm");

+ 1 - 0
service-alarm/service-alarm-biz/src/main/java/com/usky/alarm/service/mqtt/alarm/Alarm.java

@@ -51,6 +51,7 @@ public class Alarm implements MqttStrategy {
         String[] code4 = TopListener.VIDEO_ALERT.getCode().split("/");
         LambdaQueryWrapper<DmpDevice> queryWrapper1 = Wrappers.lambdaQuery();
         queryWrapper1.select(DmpDevice::getProductCode,DmpDevice::getDeviceUuid)
+                .eq(DmpDevice::getCompanyCode,topic.split("/")[3])
                 .eq(DmpDevice::getDeviceId, map.get("devId").toString())
                 .eq(DmpDevice::getDeleteFlag, 0);
         List<DmpDevice> list = dmpDeviceService.list(queryWrapper1);

+ 4 - 2
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/SipPushController.java

@@ -24,8 +24,10 @@ public class SipPushController {
 
     /** 手动注册设备到 GB28181 平台(deviceId 动态传入,20 位数字) */
     @PostMapping("/device/register")
-    public Map<String, Object> registerDevice(@RequestParam String deviceId) {
-        return sipRtspPushService.registerDevice(deviceId);
+    public Map<String, Object> registerDevice(
+            @RequestParam String deviceId,
+            @RequestParam(required = false) String channelId) {
+        return sipRtspPushService.registerDevice(deviceId, channelId);
     }
 
     /** 手动注销设备(REGISTER Expires=0) */

+ 10 - 1
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/SipRtspPushService.java

@@ -36,8 +36,17 @@ public class SipRtspPushService {
     }
 
     public Map<String, Object> registerDevice(String deviceId) {
+        return registerDevice(deviceId, null);
+    }
+
+    public Map<String, Object> registerDevice(String deviceId, String channelId) {
         try {
-            return deviceSimulator.register(deviceId);
+            Map<String, Object> status = deviceSimulator.register(deviceId, channelId);
+            if (!Boolean.TRUE.equals(status.get("registered"))) {
+                Object err = status.get("lastRegisterError");
+                throw new BusinessException(err != null ? err.toString() : "GB28181 注册失败,请检查密码与平台设备配置");
+            }
+            return status;
         } catch (IllegalArgumentException | IllegalStateException e) {
             throw new BusinessException(e.getMessage());
         }

+ 8 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/sip/config/SipClientProperties.java

@@ -25,8 +25,16 @@ public class SipClientProperties {
     private String password = "jkjj_wlgz";
     private String sipTransport = "tcp";
     private int registerExpires = 3600;
+    /** 等待 REGISTER 最终响应超时(秒) */
+    private int registerTimeout = 15;
+    /**
+     * REGISTER Request-URI 用户部分:platform=国标默认(平台编码),device=部分平台兼容模式(设备编码)
+     */
+    private String registerUriUser = "platform";
 
     private int inviteWaitSeconds = 300;
     private int defaultDurationSeconds = 60;
     private String rtspTransport = "tcp";
+    /** 通道名称(Catalog 上报),默认 Channel-1 */
+    private String channelName = "Channel-1";
 }

+ 1 - 1
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/sip/config/SipServerProperties.java

@@ -10,7 +10,7 @@ import org.springframework.stereotype.Component;
 public class SipServerProperties {
 
     private boolean enabled = true;
-    private String host = "0.0.0.0";
+    private String host = "192.168.0.100";
     private int port = 5060;
     private String localIp;
     private String platformId = "34020000002000000001";

+ 101 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/sip/device/Gb28181CatalogBuilder.java

@@ -0,0 +1,101 @@
+package com.usky.cdi.service.sip.device;
+
+import org.springframework.util.StringUtils;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * GB28181 MANSCDP+xml 目录应答(Catalog / DeviceInfo)。
+ */
+final class Gb28181CatalogBuilder {
+
+    private static final DateTimeFormatter TIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss");
+
+    private Gb28181CatalogBuilder() {
+    }
+
+    static String buildCatalogResponse(String sn, String deviceId, String channelId, String channelName) {
+        String now = LocalDateTime.now().format(TIME_FMT);
+        String name = StringUtils.hasText(channelName) ? channelName : "Channel-1";
+        return "<?xml version=\"1.0\" encoding=\"GB2312\"?>\r\n"
+                + "<Response>\r\n"
+                + "<CmdType>Catalog</CmdType>\r\n"
+                + "<SN>" + escapeXml(sn) + "</SN>\r\n"
+                + "<DeviceID>" + escapeXml(deviceId) + "</DeviceID>\r\n"
+                + "<SumNum>1</SumNum>\r\n"
+                + "<DeviceList Num=\"1\">\r\n"
+                + "<Item>\r\n"
+                + "<DeviceID>" + escapeXml(channelId) + "</DeviceID>\r\n"
+                + "<Name>" + escapeXml(name) + "</Name>\r\n"
+                + "<Manufacturer>USky</Manufacturer>\r\n"
+                + "<Model>Gb28181Simulator</Model>\r\n"
+                + "<Owner>Owner</Owner>\r\n"
+                + "<CivilCode>" + civilCode(deviceId) + "</CivilCode>\r\n"
+                + "<Address>Simulator</Address>\r\n"
+                + "<Parental>0</Parental>\r\n"
+                + "<ParentID>" + escapeXml(deviceId) + "</ParentID>\r\n"
+                + "<SafetyWay>0</SafetyWay>\r\n"
+                + "<RegisterWay>1</RegisterWay>\r\n"
+                + "<Secrecy>0</Secrecy>\r\n"
+                + "<Status>ON</Status>\r\n"
+                + "<Event>ADD</Event>\r\n"
+                + "<RegisterTime>" + now + "</RegisterTime>\r\n"
+                + "</Item>\r\n"
+                + "</DeviceList>\r\n"
+                + "</Response>\r\n";
+    }
+
+    static String buildDeviceInfoResponse(String sn, String deviceId) {
+        String now = LocalDateTime.now().format(TIME_FMT);
+        return "<?xml version=\"1.0\" encoding=\"GB2312\"?>\r\n"
+                + "<Response>\r\n"
+                + "<CmdType>DeviceInfo</CmdType>\r\n"
+                + "<SN>" + escapeXml(sn) + "</SN>\r\n"
+                + "<DeviceID>" + escapeXml(deviceId) + "</DeviceID>\r\n"
+                + "<Result>OK</Result>\r\n"
+                + "<DeviceName>Gb28181Simulator</DeviceName>\r\n"
+                + "<Manufacturer>USky</Manufacturer>\r\n"
+                + "<Model>Gb28181Simulator</Model>\r\n"
+                + "<Firmware>1.0</Firmware>\r\n"
+                + "<Channel>1</Channel>\r\n"
+                + "<RegisterTime>" + now + "</RegisterTime>\r\n"
+                + "</Response>\r\n";
+    }
+
+    static String extractXmlValue(String xml, String tag) {
+        if (!StringUtils.hasText(xml) || !StringUtils.hasText(tag)) {
+            return null;
+        }
+        String open = "<" + tag + ">";
+        String close = "</" + tag + ">";
+        int start = xml.indexOf(open);
+        if (start < 0) {
+            return null;
+        }
+        start += open.length();
+        int end = xml.indexOf(close, start);
+        if (end < 0) {
+            return null;
+        }
+        return xml.substring(start, end).trim();
+    }
+
+    private static String civilCode(String deviceId) {
+        if (deviceId != null && deviceId.length() >= 6) {
+            return deviceId.substring(0, 6);
+        }
+        return "340200";
+    }
+
+    private static String escapeXml(String value) {
+        if (value == null) {
+            return "";
+        }
+        return value.replace("&", "&amp;")
+                .replace("<", "&lt;")
+                .replace(">", "&gt;")
+                .replace("\"", "&quot;")
+                .replace("'", "&apos;");
+    }
+}

+ 162 - 14
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/sip/device/Gb28181DeviceSimulator.java

@@ -67,6 +67,10 @@ public class Gb28181DeviceSimulator implements SipListener {
     private volatile Future<?> streamingTask;
     private volatile ScheduledFuture<?> reRegisterTask;
     private volatile String activeDeviceId;
+    /** Catalog 上报的通道 ID,IPC 默认与 deviceId 相同 */
+    private volatile String activeChannelId;
+    private volatile CountDownLatch registerLatch;
+    private volatile String lastRegisterError;
 
     public Gb28181DeviceSimulator(SipClientProperties properties) {
         this.properties = properties;
@@ -98,9 +102,21 @@ public class Gb28181DeviceSimulator implements SipListener {
      * @param deviceId 20 位设备编码(动态传入)
      */
     public synchronized Map<String, Object> register(String deviceId) {
+        return register(deviceId, null);
+    }
+
+    /**
+     * 注册到 GB28181 平台。
+     *
+     * @param deviceId  20 位设备编码
+     * @param channelId 可选通道编码;为空时与 deviceId 相同(单路 IPC)
+     */
+    public synchronized Map<String, Object> register(String deviceId, String channelId) {
         validateDeviceId(deviceId);
-        if (registered.get() && deviceId.equals(activeDeviceId)) {
-            log.info("设备已注册: deviceId={}", activeDeviceId);
+        String resolvedChannelId = resolveChannelId(deviceId, channelId);
+        if (registered.get() && deviceId.equals(activeDeviceId)
+                && resolvedChannelId.equals(activeChannelId)) {
+            log.info("设备已注册: deviceId={}, channelId={}", activeDeviceId, activeChannelId);
             return status();
         }
         if (sipStack != null) {
@@ -108,16 +124,22 @@ public class Gb28181DeviceSimulator implements SipListener {
         }
         try {
             activeDeviceId = deviceId.trim();
+            activeChannelId = resolvedChannelId;
             initStack();
             registerAuthSent.set(false);
             authChallenge.set(null);
+            lastRegisterError = null;
+            registerLatch = new CountDownLatch(1);
             sendRegister(false, properties.getRegisterExpires());
             scheduleReRegister();
-            log.info("GB28181 注册请求已发送: deviceId={}, platform={}@{}:{}, transport={}",
-                    activeDeviceId, properties.getPlatformId(),
-                    properties.getServerHost(), properties.getServerPort(), sipTransport);
+            log.info("GB28181 注册请求已发送: deviceId={}, channelId={}, platform={}@{}:{}, transport={}, passwordLen={}",
+                    activeDeviceId, activeChannelId, properties.getPlatformId(),
+                    properties.getServerHost(), properties.getServerPort(), sipTransport,
+                    passwordLength());
+            awaitRegisterResult();
         } catch (Exception e) {
             activeDeviceId = null;
+            activeChannelId = null;
             shutdownStack();
             sipStack = null;
             sipProvider = null;
@@ -149,6 +171,7 @@ public class Gb28181DeviceSimulator implements SipListener {
         }
         registered.set(false);
         activeDeviceId = null;
+        activeChannelId = null;
         boundRtspUrl.set(null);
         pendingStream.set(null);
         shutdownStack();
@@ -197,6 +220,7 @@ public class Gb28181DeviceSimulator implements SipListener {
         Map<String, Object> map = new HashMap<>();
         map.put("registered", registered.get());
         map.put("deviceId", activeDeviceId);
+        map.put("channelId", activeChannelId);
         map.put("platformId", properties.getPlatformId());
         map.put("serverHost", properties.getServerHost());
         map.put("serverPort", properties.getServerPort());
@@ -205,9 +229,44 @@ public class Gb28181DeviceSimulator implements SipListener {
         map.put("localSipAddress", localIp != null ? localIp + ":" + localSipPort : null);
         map.put("boundRtspUrl", boundRtspUrl.get());
         map.put("streaming", streamingTask != null && !streamingTask.isDone());
+        map.put("lastRegisterError", lastRegisterError);
+        map.put("passwordConfigured", StringUtils.hasText(properties.getPassword()));
         return map;
     }
 
+    private void awaitRegisterResult() {
+        CountDownLatch latch = registerLatch;
+        if (latch == null) {
+            return;
+        }
+        try {
+            int timeout = Math.max(5, properties.getRegisterTimeout());
+            if (!latch.await(timeout, TimeUnit.SECONDS)) {
+                lastRegisterError = "注册超时(" + timeout + "s 内未收到平台最终响应)";
+                log.warn(lastRegisterError);
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            lastRegisterError = "注册等待被中断";
+        }
+    }
+
+    private void completeRegisterAttempt(int status, String error) {
+        if (status == Response.OK) {
+            lastRegisterError = null;
+        } else if (StringUtils.hasText(error)) {
+            lastRegisterError = error;
+        }
+        CountDownLatch latch = registerLatch;
+        if (latch != null) {
+            latch.countDown();
+        }
+    }
+
+    private int passwordLength() {
+        return properties.getPassword() != null ? properties.getPassword().length() : 0;
+    }
+
     private void initStack() throws Exception {
         SipFactory sipFactory = SipFactory.getInstance();
         sipFactory.setPathName("gov.nist");
@@ -255,15 +314,15 @@ public class Gb28181DeviceSimulator implements SipListener {
 
     private void sendRegister(boolean withAuth, int expires) throws Exception {
         ensureActiveDeviceId();
-        SipURI requestUri = createUri(activeDeviceId, properties.getDomain());
-        requestUri.setPort(properties.getServerPort());
-        requestUri.setTransportParam(sipTransport);
+        String uriUser = resolveRegisterUriUser();
+        SipURI requestUri = createUri(uriUser, properties.getDomain());
+        String digestUri = "sip:" + uriUser + "@" + properties.getDomain();
 
         Request register = buildRequest(Request.REGISTER, requestUri, activeDeviceId, "reg-tag");
         register.addHeader(headerFactory.createExpiresHeader(expires));
 
         if (withAuth) {
-            AuthorizationHeader auth = buildAuthorization(Request.REGISTER, requestUri.toString());
+            AuthorizationHeader auth = buildAuthorization(Request.REGISTER, digestUri);
             if (auth != null) {
                 register.addHeader(auth);
             }
@@ -271,7 +330,16 @@ public class Gb28181DeviceSimulator implements SipListener {
 
         ClientTransaction ct = sipProvider.getNewClientTransaction(register);
         ct.sendRequest();
-        log.debug("发送 REGISTER{} -> {}:{}", withAuth ? "(鉴权)" : "", properties.getServerHost(), properties.getServerPort());
+        log.info("发送 REGISTER{} uri={} mode={} deviceId={} -> {}:{}",
+                withAuth ? "(鉴权)" : "", digestUri, properties.getRegisterUriUser(), activeDeviceId,
+                properties.getServerHost(), properties.getServerPort());
+    }
+
+    private String resolveRegisterUriUser() {
+        if ("device".equalsIgnoreCase(properties.getRegisterUriUser())) {
+            return activeDeviceId;
+        }
+        return properties.getPlatformId();
     }
 
     private void handleIncomingInvite(RequestEvent event) throws Exception {
@@ -414,6 +482,51 @@ public class Gb28181DeviceSimulator implements SipListener {
         });
     }
 
+    private void handleIncomingMessage(RequestEvent event) throws Exception {
+        Request request = event.getRequest();
+        String body = extractSdp(request);
+        if (!StringUtils.hasText(body)) {
+            sendResponse(event, Response.BAD_REQUEST);
+            return;
+        }
+
+        String cmdType = Gb28181CatalogBuilder.extractXmlValue(body, "CmdType");
+        String sn = Gb28181CatalogBuilder.extractXmlValue(body, "SN");
+        if (!StringUtils.hasText(sn)) {
+            sn = "1";
+        }
+
+        sendResponse(event, Response.OK);
+
+        if ("Catalog".equalsIgnoreCase(cmdType)) {
+            String catalogXml = Gb28181CatalogBuilder.buildCatalogResponse(
+                    sn, activeDeviceId, activeChannelId, properties.getChannelName());
+            sendManscdpMessage(catalogXml);
+            log.info("已应答 Catalog 查询: deviceId={}, channelId={}", activeDeviceId, activeChannelId);
+        } else if ("DeviceInfo".equalsIgnoreCase(cmdType)) {
+            String infoXml = Gb28181CatalogBuilder.buildDeviceInfoResponse(sn, activeDeviceId);
+            sendManscdpMessage(infoXml);
+            log.info("已应答 DeviceInfo 查询: deviceId={}", activeDeviceId);
+        } else {
+            log.debug("忽略未处理的 MESSAGE CmdType={}", cmdType);
+        }
+    }
+
+    private void sendManscdpMessage(String xmlBody) throws Exception {
+        ensureActiveDeviceId();
+        SipURI requestUri = createUri(properties.getPlatformId(), properties.getDomain());
+        requestUri.setPort(properties.getServerPort());
+        requestUri.setTransportParam(sipTransport);
+
+        Request message = buildRequest(Request.MESSAGE, requestUri, activeDeviceId, "msg-tag");
+        message.setContent(xmlBody.getBytes(StandardCharsets.UTF_8),
+                headerFactory.createContentTypeHeader("Application", "MANSCDP+xml"));
+
+        ClientTransaction ct = sipProvider.getNewClientTransaction(message);
+        ct.sendRequest();
+        log.debug("发送 MESSAGE (MANSCDP+xml) -> {}:{}", properties.getServerHost(), properties.getServerPort());
+    }
+
     private void handleIncomingBye(RequestEvent event) throws Exception {
         log.info("收到平台 BYE");
         if (streamingTask != null) {
@@ -467,9 +580,10 @@ public class Gb28181DeviceSimulator implements SipListener {
         }
         String cnonce = UUID.randomUUID().toString().replace("-", "").substring(0, 8);
         String nc = "00000001";
+        String qop = resolveQop(challenge.getQop());
         String response = SipDigestAuth.computeResponse(
                 activeDeviceId, properties.getPassword(), challenge.getRealm(),
-                method, uri, challenge.getNonce(), nc, cnonce, challenge.getQop());
+                method, uri, challenge.getNonce(), nc, cnonce, qop);
 
         AuthorizationHeader auth = headerFactory.createAuthorizationHeader("Digest");
         auth.setUsername(activeDeviceId);
@@ -478,14 +592,22 @@ public class Gb28181DeviceSimulator implements SipListener {
         auth.setURI(addressFactory.createURI(uri));
         auth.setResponse(response);
         auth.setAlgorithm("MD5");
-        if (challenge.getQop() != null && !challenge.getQop().isEmpty()) {
-            auth.setQop(challenge.getQop());
+        if (qop != null && !qop.isEmpty()) {
+            auth.setQop(qop);
             auth.setCNonce(cnonce);
             auth.setNonceCount(Integer.parseInt(nc, 16));
         }
         return auth;
     }
 
+    private static String resolveQop(String qop) {
+        if (qop == null || qop.isEmpty()) {
+            return null;
+        }
+        int comma = qop.indexOf(',');
+        return comma > 0 ? qop.substring(0, comma).trim() : qop.trim();
+    }
+
     private void sendResponse(RequestEvent event, int code) throws Exception {
         Response response = messageFactory.createResponse(code, event.getRequest());
         if (code >= 200) {
@@ -561,6 +683,14 @@ public class Gb28181DeviceSimulator implements SipListener {
         }
     }
 
+    private static String resolveChannelId(String deviceId, String channelId) {
+        if (StringUtils.hasText(channelId)) {
+            validateDeviceId(channelId);
+            return channelId.trim();
+        }
+        return deviceId.trim();
+    }
+
     private void shutdownStack() {
         try {
             if (sipProvider != null) {
@@ -589,6 +719,7 @@ public class Gb28181DeviceSimulator implements SipListener {
                 challenge = (WWWAuthenticateHeader) event.getResponse().getHeader(ProxyAuthenticateHeader.NAME);
             }
             authChallenge.set(challenge);
+            log.info("收到 REGISTER 401 挑战, realm={}", challenge != null ? challenge.getRealm() : "unknown");
             if (!registerAuthSent.getAndSet(true)) {
                 try {
                     ExpiresHeader expires = (ExpiresHeader) ct.getRequest().getHeader(ExpiresHeader.NAME);
@@ -607,9 +738,24 @@ public class Gb28181DeviceSimulator implements SipListener {
                 registered.set(true);
                 log.info("GB28181 注册成功: deviceId={}(平台应可见设备在线)", activeDeviceId);
             }
+            completeRegisterAttempt(status, null);
         } else if (status >= 300) {
             registered.set(false);
-            log.warn("GB28181 注册失败: deviceId={}, status={}", activeDeviceId, status);
+            String error;
+            if (status == Response.FORBIDDEN) {
+                error = "平台拒绝注册(403):密码错误或设备未在平台添加,当前 passwordLen="
+                        + passwordLength() + " registerUriUser=" + properties.getRegisterUriUser();
+                log.warn("GB28181 注册失败 403: deviceId={}, platformId={}, domain={}, passwordLen={}。"
+                        + " 请核对:① sip.client.password 与平台该设备密码一致"
+                        + " ② 平台已添加设备 {} ③ domain/platformId 与海康配置一致"
+                        + " ④ 仍失败可试 sip.client.register-uri-user=device",
+                        activeDeviceId, properties.getPlatformId(), properties.getDomain(),
+                        passwordLength(), activeDeviceId);
+            } else {
+                error = "平台拒绝注册(" + status + ")";
+                log.warn("GB28181 注册失败: deviceId={}, status={}", activeDeviceId, status);
+            }
+            completeRegisterAttempt(status, error);
         }
     }
 
@@ -624,6 +770,8 @@ public class Gb28181DeviceSimulator implements SipListener {
                 handleIncomingAck(event);
             } else if (Request.BYE.equals(method)) {
                 handleIncomingBye(event);
+            } else if (Request.MESSAGE.equals(method)) {
+                handleIncomingMessage(event);
             }
         } catch (Exception e) {
             log.error("处理 {} 失败: {}", method, e.getMessage(), e);

+ 8 - 1
service-cdi/service-cdi-biz/src/main/resources/application-gb28181-video.yml

@@ -8,13 +8,20 @@ sip:
   client:
     enabled: true
     auto-start: false
-    server-host: 192.168.20.166
+    server-host: 114.80.201.142
     server-port: 15060
     domain: 3402000000
     platform-id: 34020000002000000001
+    # 必须与平台侧该设备的 SIP 密码完全一致(Nacos 会覆盖此值)
     password: jkjj_wlgz
     sip-transport: tcp
     register-expires: 3600
+    register-uri-user: platform
     invite-wait-seconds: 300
     default-duration-seconds: 60
     rtsp-transport: tcp
+    local-ip: 192.168.0.100
+    # 注册超时时间,单位秒,建议设置为10~15秒,适配高延迟链路
+    register-timeout: 15
+    # TCP长连接开启保活,避免网络波动导致连接中断
+    tcp-keep-alive: true

+ 82 - 0
service-eg/service-eg-biz/pom.xml

@@ -16,6 +16,16 @@
             <artifactId>common-cloud-starter</artifactId>
         </dependency>
 
+        <!-- MQTT:远程升级指令下发(与终端 connectWithDevice 订阅主题对齐) -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-integration</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.integration</groupId>
+            <artifactId>spring-integration-mqtt</artifactId>
+        </dependency>
+
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-configuration-processor</artifactId>
@@ -58,6 +68,78 @@
             <version>0.0.1</version>
         </dependency>
 
+        <!-- SmartJavaAI Face Recognition Dependencies -->
+        <dependency>
+            <groupId>cn.smartjavaai</groupId>
+            <artifactId>face</artifactId>
+            <version>1.1.1</version>
+        </dependency>
+        <dependency>
+            <groupId>cn.smartjavaai</groupId>
+            <artifactId>common</artifactId>
+            <version>1.1.1</version>
+        </dependency>
+
+        <!-- DJL Core Dependencies -->
+        <dependency>
+            <groupId>ai.djl</groupId>
+            <artifactId>api</artifactId>
+            <version>0.34.0</version>
+        </dependency>
+        <dependency>
+            <groupId>ai.djl</groupId>
+            <artifactId>model-zoo</artifactId>
+            <version>0.34.0</version>
+        </dependency>
+
+        <!-- DJL PyTorch Engine -->
+        <dependency>
+            <groupId>ai.djl.pytorch</groupId>
+            <artifactId>pytorch-engine</artifactId>
+            <version>0.34.0</version>
+        </dependency>
+        <dependency>
+            <groupId>ai.djl.pytorch</groupId>
+            <artifactId>pytorch-model-zoo</artifactId>
+            <version>0.34.0</version>
+        </dependency>
+
+        <!-- DJL ONNX Runtime -->
+        <dependency>
+            <groupId>ai.djl.onnxruntime</groupId>
+            <artifactId>onnxruntime-engine</artifactId>
+            <version>0.34.0</version>
+        </dependency>
+
+        <!-- DJL OpenCV -->
+        <dependency>
+            <groupId>ai.djl.opencv</groupId>
+            <artifactId>opencv</artifactId>
+            <version>0.34.0</version>
+        </dependency>
+
+        <!-- SQLite for face database -->
+        <dependency>
+            <groupId>org.xerial</groupId>
+            <artifactId>sqlite-jdbc</artifactId>
+            <version>3.36.0.3</version>
+        </dependency>
+
+        <!-- Milvus Vector Database (Optional) -->
+        <dependency>
+            <groupId>io.milvus</groupId>
+            <artifactId>milvus-sdk-java</artifactId>
+            <version>2.5.7</version>
+            <optional>true</optional>
+        </dependency>
+
+        <!-- Seetaface SDK -->
+        <dependency>
+            <groupId>io.gitee.dengwenjie</groupId>
+            <artifactId>seeta-sdk-platform</artifactId>
+            <version>1.2.2</version>
+        </dependency>
+
     </dependencies>
 
     <build>

+ 10 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/adb/EgDeviceAdbConfiguration.java

@@ -0,0 +1,10 @@
+package com.usky.eg.adb;
+
+import com.usky.eg.mqtt.EgMqttProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@EnableConfigurationProperties({EgDeviceAdbProperties.class, EgMqttProperties.class})
+public class EgDeviceAdbConfiguration {
+}

+ 75 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/adb/EgDeviceAdbProperties.java

@@ -0,0 +1,75 @@
+package com.usky.eg.adb;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * 服务端调用官方 platform-tools adb(需在 PATH 或可执行路径配置)。
+ */
+@ConfigurationProperties(prefix = "eg.adb")
+public class EgDeviceAdbProperties {
+
+    /**
+     * adb 可执行文件名或绝对路径,默认依赖系统 PATH 中的 adb
+     */
+    private String executable = "adb";
+
+    /**
+     * connect / disconnect / tcpip 等命令超时(秒)
+     */
+    private long commandTimeoutSeconds = 45L;
+
+    /**
+     * screencap 超时(秒)
+     */
+    private long screenshotTimeoutSeconds = 60L;
+
+    /**
+     * tcpip 失败时是否先执行一次 adb connect 再重试 tcpip
+     */
+    private boolean tcpipRetryAfterConnect = true;
+
+    /**
+     * 截屏前是否先执行 adb connect(避免仅调 screenshot 时 adb 侧尚无该 serial,报 device not found)
+     */
+    private boolean screenshotConnectFirst = true;
+
+    public String getExecutable() {
+        return executable;
+    }
+
+    public void setExecutable(String executable) {
+        this.executable = executable;
+    }
+
+    public long getCommandTimeoutSeconds() {
+        return commandTimeoutSeconds;
+    }
+
+    public void setCommandTimeoutSeconds(long commandTimeoutSeconds) {
+        this.commandTimeoutSeconds = commandTimeoutSeconds;
+    }
+
+    public long getScreenshotTimeoutSeconds() {
+        return screenshotTimeoutSeconds;
+    }
+
+    public void setScreenshotTimeoutSeconds(long screenshotTimeoutSeconds) {
+        this.screenshotTimeoutSeconds = screenshotTimeoutSeconds;
+    }
+
+    public boolean isTcpipRetryAfterConnect() {
+        return tcpipRetryAfterConnect;
+    }
+
+    public void setTcpipRetryAfterConnect(boolean tcpipRetryAfterConnect) {
+        this.tcpipRetryAfterConnect = tcpipRetryAfterConnect;
+    }
+
+    public boolean isScreenshotConnectFirst() {
+        return screenshotConnectFirst;
+    }
+
+    public void setScreenshotConnectFirst(boolean screenshotConnectFirst) {
+        this.screenshotConnectFirst = screenshotConnectFirst;
+    }
+}

+ 17 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/controller/api/ServiceEgTaskApi.java

@@ -0,0 +1,17 @@
+package com.usky.eg.controller.api;
+
+import com.usky.eg.RemoteEgService;
+import com.usky.eg.service.EgDeviceHeartbeatService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+public class ServiceEgTaskApi implements RemoteEgService {
+    @Autowired
+    private EgDeviceHeartbeatService egDeviceHeartbeatService;
+
+    @Override
+    public void egDeviceStatus(){
+        egDeviceHeartbeatService.updateEgDeviceStatus();
+    }
+}

+ 97 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/controller/web/EgDeviceAdbController.java

@@ -0,0 +1,97 @@
+package com.usky.eg.controller.web;
+
+import com.usky.common.core.bean.ApiResult;
+import com.usky.common.core.exception.BusinessException;
+import com.usky.eg.service.EgDeviceAdbService;
+import com.usky.eg.service.EgDeviceRemoteUpgradeMqttService;
+import com.usky.eg.service.vo.EgDeviceAdbEndpointVO;
+import com.usky.eg.service.vo.EgDeviceAdbRemoteUpgradeBatchResultVO;
+import com.usky.eg.service.vo.EgDeviceAdbRemoteUpgradeVO;
+import com.usky.eg.service.vo.EgDeviceAdbResultVO;
+import com.usky.eg.service.vo.EgDeviceAdbTcpipVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 服务端官方 adb:部署机需安装 Android Platform Tools(adb 在 PATH),且网络可达设备。
+ * <p>
+ * 网关前缀一般为 /service-eg,完整路径示例:POST /service-eg/egDevice/adb/connect
+ */
+@RestController
+@RequestMapping("/egDevice/adb")
+public class EgDeviceAdbController {
+
+    @Autowired
+    private EgDeviceAdbService egDeviceAdbService;
+
+    @Autowired
+    private EgDeviceRemoteUpgradeMqttService egDeviceRemoteUpgradeMqttService;
+
+    @PostMapping("/connect")
+    public ApiResult<EgDeviceAdbResultVO> connect(@RequestBody EgDeviceAdbEndpointVO vo) {
+        EgDeviceAdbResultVO r = egDeviceAdbService.connect(vo.getDeviceIp(), vo.getDevicePort());
+        return ApiResult.success(r);
+    }
+
+    @PostMapping("/disconnect")
+    public ApiResult<EgDeviceAdbResultVO> disconnect(@RequestBody EgDeviceAdbEndpointVO vo) {
+        EgDeviceAdbResultVO r = egDeviceAdbService.disconnect(vo.getDeviceIp(), vo.getDevicePort());
+        return ApiResult.success(r);
+    }
+
+    @PostMapping("/tcpip")
+    public ApiResult<EgDeviceAdbResultVO> tcpip(@RequestBody EgDeviceAdbTcpipVO vo) {
+        EgDeviceAdbResultVO r = egDeviceAdbService.tcpip(vo.getDeviceIp(), vo.getDevicePort(), vo.getPort());
+        return ApiResult.success(r);
+    }
+
+    /**
+     * 远程升级:仅通过 MQTT 向 Broker 发布下行指令,由终端 {@code connectWithDevice} 订阅后在设备侧自行下载安装
+     * 单台:{@code fileUrl} 必填;{@code deviceId} 或 {@code deviceCode} 至少其一。
+     * 批量:非空 {@code items},每项含 {@code deviceId} 或 {@code deviceCode},并与同级 {@code fileUrl}、可选 {@code launchPackage} 共用。
+     * 完整路径示例:POST /service-eg/egDevice/adb/remoteUpgrade
+     */
+    @PostMapping("/remoteUpgrade")
+    public ApiResult<?> remoteUpgrade(@RequestBody EgDeviceAdbRemoteUpgradeVO vo) {
+        if (vo.getItems() != null && !vo.getItems().isEmpty()) {
+            EgDeviceAdbRemoteUpgradeBatchResultVO batch = egDeviceRemoteUpgradeMqttService.publishRemoteUpgradeBatch(
+                    vo.getItems(), vo.getFileUrl(), vo.getLaunchPackage());
+            return ApiResult.success(batch);
+        }
+        EgDeviceAdbResultVO r = egDeviceRemoteUpgradeMqttService.publishRemoteUpgrade(vo);
+        return ApiResult.success(r);
+    }
+
+    /**
+     * 原始 PNG 二进制,无 JSON 封装(前端 responseType: blob)。
+     * 使用 {@link HttpServletResponse} 写出流,避免全局 ApiResult 包装器把返回值当成 JSON 再强转为 byte[] 导致 ClassCastException。
+     */
+    @GetMapping(value = "/screenshot", produces = MediaType.IMAGE_PNG_VALUE)
+    public void screenshot(@RequestParam("deviceIp") String deviceIp,
+                           @RequestParam("devicePort") Integer devicePort,
+                           HttpServletResponse response) throws IOException {
+        try {
+            byte[] png = egDeviceAdbService.screenshot(deviceIp, devicePort);
+            response.setStatus(HttpServletResponse.SC_OK);
+            response.setContentType(MediaType.IMAGE_PNG_VALUE);
+            response.setHeader(HttpHeaders.CACHE_CONTROL, "no-store, no-cache");
+            response.setContentLength(png.length);
+            response.getOutputStream().write(png);
+            response.getOutputStream().flush();
+        } catch (BusinessException e) {
+            String msg = e.getMessage() != null ? e.getMessage() : "Bad Request";
+            byte[] err = msg.getBytes(StandardCharsets.UTF_8);
+            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+            response.setContentType(MediaType.TEXT_PLAIN_VALUE);
+            response.setContentLength(err.length);
+            response.getOutputStream().write(err);
+            response.getOutputStream().flush();
+        }
+    }
+}

+ 0 - 86
service-eg/service-eg-biz/src/main/java/com/usky/eg/domain/MeetingFace.java

@@ -1,86 +0,0 @@
-package com.usky.eg.domain;
-
-import com.baomidou.mybatisplus.annotation.IdType;
-import com.baomidou.mybatisplus.annotation.TableId;
-import java.time.LocalDateTime;
-import java.io.Serializable;
-import lombok.Data;
-import lombok.EqualsAndHashCode;
-
-/**
- * <p>
- * 
- * </p>
- *
- * @author zyj
- * @since 2024-11-27
- */
-@Data
-@EqualsAndHashCode(callSuper = false)
-public class MeetingFace implements Serializable {
-
-    private static final long serialVersionUID = 1L;
-
-    /**
-     * 主键
-     */
-    @TableId(value = "fid", type = IdType.AUTO)
-    private Integer fid;
-
-    /**
-     * 图片数据 base_64编码
-     */
-    private String faceBase;
-
-    /**
-     * 插入时间
-     */
-    private LocalDateTime createTime;
-
-    /**
-     * 验证次数
-     */
-    private Integer vefNum;
-
-    /**
-     * 人脸名称
-     */
-    private String faceName;
-
-    /**
-     * 人脸备注
-     */
-    private String remark;
-
-    /**
-     * 人脸是否可用,(0==可用,1,不可用)
-     */
-    private Integer faceStatus;
-
-    /**
-     * 卡号
-     */
-    private String cardNum;
-
-    /**
-     * 绑定设备
-     */
-    private String bindDevice;
-
-    /**
-     * 组织机构ID
-     */
-    private Integer deptId;
-
-    /**
-     * 租户ID
-     */
-    private Integer tenantId;
-
-    /**
-     * 用户ID
-     */
-    private Long userId;
-
-
-}

+ 0 - 32
service-eg/service-eg-biz/src/main/java/com/usky/eg/domain/MeetingFaceDevice.java

@@ -1,32 +0,0 @@
-package com.usky.eg.domain;
-
-import java.io.Serializable;
-import lombok.Data;
-import lombok.EqualsAndHashCode;
-
-/**
- * <p>
- * 人脸设备关联表
- * </p>
- *
- * @author zyj
- * @since 2024-11-27
- */
-@Data
-@EqualsAndHashCode(callSuper = false)
-public class MeetingFaceDevice implements Serializable {
-
-    private static final long serialVersionUID = 1L;
-
-    /**
-     * 人脸主键ID
-     */
-    private Integer faceId;
-
-    /**
-     * 设备主键ID
-     */
-    private Integer deviceId;
-
-
-}

+ 0 - 7
service-eg/service-eg-biz/src/main/java/com/usky/eg/mapper/EgDeviceMapper.java

@@ -15,13 +15,6 @@ import org.springframework.stereotype.Repository;
  */
 @Repository
 public interface EgDeviceMapper extends CrudMapper<EgDevice> {
-    void insertMeetingFaceDevice(@Param("fid") Integer fid, @Param("egDeviceId") Integer egDeviceId);
-
-    void deleteMeetingFaceDevice(@Param("egDeviceId") Integer egDeviceId);
 
     Integer sysTenantId(@Param("domain") String domain);
-
-    Integer getMeetingFaceData(@Param("userId") Long userId);
-
-    Integer[] getMeetingFaceDeviceList(@Param("fid") Integer fid);
 }

+ 0 - 18
service-eg/service-eg-biz/src/main/java/com/usky/eg/mapper/MeetingFaceDeviceMapper.java

@@ -1,18 +0,0 @@
-package com.usky.eg.mapper;
-
-import com.usky.eg.domain.MeetingFaceDevice;
-import com.usky.common.mybatis.core.CrudMapper;
-import org.springframework.stereotype.Repository;
-
-/**
- * <p>
- * 人脸设备关联表 Mapper 接口
- * </p>
- *
- * @author zyj
- * @since 2024-11-27
- */
-@Repository
-public interface MeetingFaceDeviceMapper extends CrudMapper<MeetingFaceDevice> {
-
-}

+ 0 - 18
service-eg/service-eg-biz/src/main/java/com/usky/eg/mapper/MeetingFaceMapper.java

@@ -1,18 +0,0 @@
-package com.usky.eg.mapper;
-
-import com.usky.eg.domain.MeetingFace;
-import com.usky.common.mybatis.core.CrudMapper;
-import org.springframework.stereotype.Repository;
-
-/**
- * <p>
- *  Mapper 接口
- * </p>
- *
- * @author zyj
- * @since 2024-11-27
- */
-@Repository
-public interface MeetingFaceMapper extends CrudMapper<MeetingFace> {
-
-}

+ 6 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/mapper/SysPersonMapper.java

@@ -7,6 +7,7 @@ import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
 
 import java.util.List;
+import java.util.Map;
 
 /**
  * 人员信息表 Mapper
@@ -19,4 +20,9 @@ public interface SysPersonMapper extends BaseMapper<SysPerson> {
      */
     @Select("SELECT id, full_name, face_base, face_name, card_num FROM sys_person WHERE face_base IS NOT NULL AND face_base != '' AND face_status = 0")
     List<SysPerson> selectAllValidFace();
+
+    /**
+     * 按人员ID批量查询关联用户ID(sys_user_person)
+     */
+    List<Map<String, Object>> selectUserIdsByPersonIds(@Param("personIds") List<Integer> personIds);
 }

+ 67 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/mqtt/EgMqttOutboundConfig.java

@@ -0,0 +1,67 @@
+package com.usky.eg.mqtt;
+
+import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.integration.annotation.MessagingGateway;
+import org.springframework.integration.annotation.ServiceActivator;
+import org.springframework.integration.channel.DirectChannel;
+import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory;
+import org.springframework.integration.mqtt.core.MqttPahoClientFactory;
+import org.springframework.integration.mqtt.outbound.MqttPahoMessageHandler;
+import org.springframework.integration.mqtt.support.MqttHeaders;
+import org.springframework.messaging.MessageChannel;
+import org.springframework.messaging.MessageHandler;
+import org.springframework.messaging.handler.annotation.Header;
+
+/**
+ * 出站 MQTT(发布远程升级下行指令)。
+ */
+@Configuration
+public class EgMqttOutboundConfig {
+
+    public static final String CHANNEL_NAME_OUT = "egMqttOutboundChannel";
+    public static final String DEFAULT_TOPIC = "eg/default";
+
+    @Autowired
+    private EgMqttProperties egMqttProperties;
+
+    @Bean
+    public MqttPahoClientFactory egMqttPahoClientFactory() {
+        DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
+        MqttConnectOptions options = new MqttConnectOptions();
+        options.setServerURIs(new String[]{egMqttProperties.getUrl()});
+        String user = egMqttProperties.getUsername();
+        if (user != null && !user.isEmpty()) {
+            options.setUserName(user);
+        }
+        String pwd = egMqttProperties.getPassword();
+        if (pwd != null && !pwd.isEmpty()) {
+            options.setPassword(pwd.toCharArray());
+        }
+        factory.setConnectionOptions(options);
+        return factory;
+    }
+
+    @Bean(name = CHANNEL_NAME_OUT)
+    public MessageChannel egMqttOutboundChannel() {
+        return new DirectChannel();
+    }
+
+    @Bean
+    @ServiceActivator(inputChannel = CHANNEL_NAME_OUT)
+    public MessageHandler egMqttOutbound(MqttPahoClientFactory egMqttPahoClientFactory) {
+        String clientId = "service-eg-mqtt-out-" + System.currentTimeMillis();
+        MqttPahoMessageHandler handler = new MqttPahoMessageHandler(clientId, egMqttPahoClientFactory);
+        handler.setAsync(true);
+        handler.setDefaultTopic(DEFAULT_TOPIC);
+        return handler;
+    }
+
+    @MessagingGateway(defaultRequestChannel = CHANNEL_NAME_OUT)
+    public interface EgMqttPublishGateway {
+
+        void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic, @Header(MqttHeaders.QOS) int qos, String payload);
+    }
+}

+ 81 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/mqtt/EgMqttProperties.java

@@ -0,0 +1,81 @@
+package com.usky.eg.mqtt;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * 远程升级 MQTT 下发(服务端发布,终端 {@code connectWithDevice} 订阅)。
+ * <p>
+ * 配置前缀 {@code eg.mqtt};{@link com.usky.eg.controller.web.EgDeviceAdbController#remoteUpgrade} 仅通过本配置连接 Broker 并发布。
+ */
+@ConfigurationProperties(prefix = "eg.mqtt")
+public class EgMqttProperties {
+
+    /** 例如 tcp://47.98.201.73:1883 */
+    private String url = "tcp://47.98.201.73:1883";
+
+    private String username = "usky";
+
+    private String password = "usky";
+
+    /**
+     * 发布主题模板,须包含占位符 {@code {deviceId}},与终端 {@code deviceId: getSerial()} 一致。
+     * 示例:usky/device/{deviceId}/command
+     */
+    private String commandTopicTemplate = "usky/device/{deviceId}/command";
+
+    /** 发布 QoS,0/1/2 */
+    private int qos = 1;
+
+    /**
+     * 下发 JSON 中 {@code type} 字段,终端可在 {@code onCommand} 里按类型分支。
+     */
+    private String commandType = "egRemoteUpgrade";
+
+    public String getUrl() {
+        return url;
+    }
+
+    public void setUrl(String url) {
+        this.url = url;
+    }
+
+    public String getUsername() {
+        return username;
+    }
+
+    public void setUsername(String username) {
+        this.username = username;
+    }
+
+    public String getPassword() {
+        return password;
+    }
+
+    public void setPassword(String password) {
+        this.password = password;
+    }
+
+    public String getCommandTopicTemplate() {
+        return commandTopicTemplate;
+    }
+
+    public void setCommandTopicTemplate(String commandTopicTemplate) {
+        this.commandTopicTemplate = commandTopicTemplate;
+    }
+
+    public int getQos() {
+        return qos;
+    }
+
+    public void setQos(int qos) {
+        this.qos = qos;
+    }
+
+    public String getCommandType() {
+        return commandType;
+    }
+
+    public void setCommandType(String commandType) {
+        this.commandType = commandType;
+    }
+}

+ 20 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/EgDeviceAdbService.java

@@ -0,0 +1,20 @@
+package com.usky.eg.service;
+
+import com.usky.eg.service.vo.EgDeviceAdbResultVO;
+
+/**
+ * 服务端调用官方 adb(部署机需安装 Android Platform Tools 且能访问设备网段)
+ */
+public interface EgDeviceAdbService {
+
+    EgDeviceAdbResultVO connect(String deviceIp, Integer devicePort);
+
+    EgDeviceAdbResultVO disconnect(String deviceIp, Integer devicePort);
+
+    EgDeviceAdbResultVO tcpip(String deviceIp, Integer devicePort, Integer listenPort);
+
+    /**
+     * exec-out screencap -p 原始 PNG 字节;调用方需保证已与设备建立 adb 会话(通常先 connect)
+     */
+    byte[] screenshot(String deviceIp, Integer devicePort);
+}

+ 19 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/EgDeviceRemoteUpgradeMqttService.java

@@ -0,0 +1,19 @@
+package com.usky.eg.service;
+
+import com.usky.eg.service.vo.EgDeviceAdbRemoteUpgradeBatchItemVO;
+import com.usky.eg.service.vo.EgDeviceAdbRemoteUpgradeBatchResultVO;
+import com.usky.eg.service.vo.EgDeviceAdbRemoteUpgradeVO;
+import com.usky.eg.service.vo.EgDeviceAdbResultVO;
+
+import java.util.List;
+
+/**
+ * 通过 MQTT 向终端下发远程升级参数(终端 {@code onCommand} 接收后自行下载安装)。
+ */
+public interface EgDeviceRemoteUpgradeMqttService {
+
+    EgDeviceAdbResultVO publishRemoteUpgrade(EgDeviceAdbRemoteUpgradeVO vo);
+
+    EgDeviceAdbRemoteUpgradeBatchResultVO publishRemoteUpgradeBatch(List<EgDeviceAdbRemoteUpgradeBatchItemVO> items,
+                                                                    String fileUrl, String launchPackage);
+}

+ 325 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/impl/EgDeviceAdbServiceImpl.java

@@ -0,0 +1,325 @@
+package com.usky.eg.service.impl;
+
+import com.baomidou.mybatisplus.core.toolkit.StringUtils;
+import com.usky.common.core.exception.BusinessException;
+import com.usky.eg.adb.EgDeviceAdbProperties;
+import com.usky.eg.service.EgDeviceAdbService;
+import com.usky.eg.service.vo.EgDeviceAdbResultVO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 通过 {@link ProcessBuilder} 调用本机 PATH 中的官方 adb。
+ */
+@Slf4j
+@Service
+public class EgDeviceAdbServiceImpl implements EgDeviceAdbService {
+
+    private static final int BUFFER = 16384;
+
+    /**
+     * Windows 下 adb 常输出系统本地编码(如 GBK),勿固定按 UTF-8 解码以免日志/接口里出现乱码。
+     */
+    private static final Charset ADB_CONSOLE_CHARSET = Charset.defaultCharset();
+
+    private static final String NETWORK_TIMEOUT_HINT =
+            " 【排查】运行本服务的机器须与设备网络互通并能访问目标 TCP 端口(常见 5555);"
+                    + "检查防火墙/安全组/跨网段路由;确认手机「无线调试」已开启且端口与请求一致;"
+                    + "Android 11+ 有时需先用系统界面配对,端口可能不是 5555;"
+                    + "错误 10060 表示连接超时,多为网络不可达或端口未监听。";
+
+    @Autowired
+    private EgDeviceAdbProperties properties;
+
+    @Override
+    public EgDeviceAdbResultVO connect(String deviceIp, Integer devicePort) {
+        validateEndpoint(deviceIp, devicePort);
+        String endpoint = toEndpoint(deviceIp, devicePort);
+        try {
+            String out = runAdbMergedOutput(properties.getCommandTimeoutSeconds(),
+                    Arrays.asList(properties.getExecutable(), "connect", endpoint));
+            return EgDeviceAdbResultVO.ok(out);
+        } catch (Exception e) {
+            log.warn("adb connect failed: {}", endpoint, e);
+            return EgDeviceAdbResultVO.fail(e.getMessage() != null ? e.getMessage() : e.toString());
+        }
+    }
+
+    @Override
+    public EgDeviceAdbResultVO disconnect(String deviceIp, Integer devicePort) {
+        validateEndpoint(deviceIp, devicePort);
+        String endpoint = toEndpoint(deviceIp, devicePort);
+        try {
+            String out = runAdbMergedOutput(properties.getCommandTimeoutSeconds(),
+                    Arrays.asList(properties.getExecutable(), "disconnect", endpoint));
+            return EgDeviceAdbResultVO.ok(out);
+        } catch (Exception e) {
+            log.warn("adb disconnect failed: {}", endpoint, e);
+            return EgDeviceAdbResultVO.fail(e.getMessage() != null ? e.getMessage() : e.toString());
+        }
+    }
+
+    @Override
+    public EgDeviceAdbResultVO tcpip(String deviceIp, Integer devicePort, Integer listenPort) {
+        validateEndpoint(deviceIp, devicePort);
+        if (listenPort == null || listenPort < 1 || listenPort > 65535) {
+            throw new BusinessException("port 无效(tcpip 监听端口)");
+        }
+        String serial = toSerial(deviceIp, devicePort);
+        EgDeviceAdbResultVO first = runTcpipOnce(serial, listenPort);
+        if (EgDeviceAdbResultVO.STATUS_SUCCESS.equals(first.getStatus())) {
+            return first;
+        }
+        if (!properties.isTcpipRetryAfterConnect()) {
+            return first;
+        }
+        log.info("adb tcpip 首次失败,尝试 connect 后重试: {}", serial);
+        EgDeviceAdbResultVO c = connect(deviceIp, devicePort);
+        if (!EgDeviceAdbResultVO.STATUS_SUCCESS.equals(c.getStatus())) {
+            return EgDeviceAdbResultVO.fail("tcpip 失败且自动 connect 未成功: " + first.getDetail() + " | connect: " + c.getDetail());
+        }
+        return runTcpipOnce(serial, listenPort);
+    }
+
+    private EgDeviceAdbResultVO runTcpipOnce(String serial, int listenPort) {
+        try {
+            String out = runAdbMergedOutput(properties.getCommandTimeoutSeconds(),
+                    Arrays.asList(properties.getExecutable(), "-s", serial, "tcpip", String.valueOf(listenPort)));
+            return EgDeviceAdbResultVO.ok(out);
+        } catch (Exception e) {
+            log.warn("adb tcpip failed: {} {}", serial, listenPort, e);
+            return EgDeviceAdbResultVO.fail(e.getMessage() != null ? e.getMessage() : e.toString());
+        }
+    }
+
+    @Override
+    public byte[] screenshot(String deviceIp, Integer devicePort) {
+        validateEndpoint(deviceIp, devicePort);
+        String serial = toSerial(deviceIp, devicePort);
+        if (properties.isScreenshotConnectFirst()) {
+            try {
+                ensureAdbDeviceOnline(deviceIp, devicePort);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                throw new BusinessException("设备未就绪,无法截屏: " + e.getMessage());
+            } catch (IOException e) {
+                throw new BusinessException("设备未就绪,无法截屏: " + e.getMessage());
+            }
+        }
+        List<String> cmd = Arrays.asList(
+                properties.getExecutable(),
+                "-s", serial,
+                "exec-out", "screencap", "-p");
+        try {
+            byte[] png = runAdbBinaryStdout(cmd, properties.getScreenshotTimeoutSeconds());
+            checkPng(png);
+            return png;
+        } catch (Exception e) {
+            log.warn("adb screencap failed: {}", serial, e);
+            throw new BusinessException("截屏失败: " + e.getMessage()
+                    + "(请确认设备已开无线调试、端口正确,或先调用 /egDevice/adb/connect)");
+        }
+    }
+
+    /**
+     * 让本机 adb 真正识别该 serial:执行 connect、校验 {@code adb devices} 为 {@code device} 状态。
+     * 若仅“假装 connect”或忽略失败,会继续出现 {@code device 'ip:port' not found}。
+     */
+    private void ensureAdbDeviceOnline(String deviceIp, int devicePort) throws IOException, InterruptedException {
+        String serial = toSerial(deviceIp, devicePort);
+        String endpoint = toEndpoint(deviceIp, devicePort);
+
+        String connectOut = runAdbMergedOutput(properties.getCommandTimeoutSeconds(),
+                Arrays.asList(properties.getExecutable(), "connect", endpoint));
+        if (connectOutputIndicatesFailure(connectOut)) {
+            throw new IOException(withTimeoutHint("adb connect 输出表明失败: " + connectOut));
+        }
+
+        if (waitUntilDeviceOnline(serial, 3, 400)) {
+            Thread.sleep(200);
+            return;
+        }
+
+        log.warn("首次 connect 后 devices 仍未就绪,重试 connect: {}", endpoint);
+        connectOut = runAdbMergedOutput(properties.getCommandTimeoutSeconds(),
+                Arrays.asList(properties.getExecutable(), "connect", endpoint));
+        if (connectOutputIndicatesFailure(connectOut)) {
+            throw new IOException(withTimeoutHint("adb connect(重试)失败: " + connectOut));
+        }
+        if (!waitUntilDeviceOnline(serial, 5, 500)) {
+            String devicesList = runAdbMergedOutput(properties.getCommandTimeoutSeconds(),
+                    Arrays.asList(properties.getExecutable(), "devices", "-l"));
+            throw new IOException(withTimeoutHint("adb devices 中无可用设备 " + serial
+                    + "。connect 输出: " + connectOut + ";当前列表:\n" + devicesList));
+        }
+        Thread.sleep(200);
+    }
+
+    private boolean waitUntilDeviceOnline(String serial, int attempts, long gapMs) throws IOException, InterruptedException {
+        for (int i = 0; i < attempts; i++) {
+            if (i > 0) {
+                Thread.sleep(gapMs);
+            }
+            String list = runAdbMergedOutput(properties.getCommandTimeoutSeconds(),
+                    Arrays.asList(properties.getExecutable(), "devices"));
+            String state = findSerialState(list, serial);
+            if ("device".equals(state)) {
+                return true;
+            }
+            if ("unauthorized".equals(state)) {
+                throw new IOException("设备已发现但未授权调试,请在设备上点「允许 USB 调试」: " + serial);
+            }
+            if ("offline".equals(state)) {
+                log.warn("设备当前为 offline: {}", serial);
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 解析 {@code adb devices},返回 serial 对应第二列状态;未找到返回 null
+     */
+    private static String findSerialState(String devicesOutput, String serial) {
+        if (devicesOutput == null) {
+            return null;
+        }
+        for (String raw : devicesOutput.split("\\r?\\n")) {
+            String line = raw.trim();
+            if (line.isEmpty() || line.startsWith("List of devices")) {
+                continue;
+            }
+            String[] parts = line.split("\\s+");
+            if (parts.length >= 2 && serial.equals(parts[0])) {
+                return parts[1];
+            }
+        }
+        return null;
+    }
+
+    private static boolean connectOutputIndicatesFailure(String connectOut) {
+        if (connectOut == null || connectOut.isEmpty()) {
+            return false;
+        }
+        String s = connectOut.toLowerCase();
+        return s.contains("cannot connect")
+                || s.contains("failed to connect")
+                || s.contains("unable to connect")
+                || s.contains("connection refused")
+                || s.contains("10061")
+                || s.contains("10060")
+                || s.contains("timed out")
+                || connectOut.contains("连接尝试失败")
+                || connectOut.contains("没有正确答复");
+    }
+
+    private static String withTimeoutHint(String message) {
+        if (message == null) {
+            return NETWORK_TIMEOUT_HINT.trim();
+        }
+        String m = message;
+        String lower = m.toLowerCase();
+        if (lower.contains("10060") || lower.contains("10061") || lower.contains("cannot connect")
+                || lower.contains("timed out") || m.contains("连接尝试失败") || m.contains("没有正确答复")) {
+            return m + NETWORK_TIMEOUT_HINT;
+        }
+        return m;
+    }
+
+    /**
+     * adb 文本类输出(stdout/stderr)按 JVM 默认字符集解码
+     */
+    private static String decodeAdbConsole(byte[] raw) {
+        if (raw == null || raw.length == 0) {
+            return "";
+        }
+        return new String(raw, ADB_CONSOLE_CHARSET).trim();
+    }
+
+    private static void validateEndpoint(String deviceIp, Integer devicePort) {
+        if (StringUtils.isBlank(deviceIp)) {
+            throw new BusinessException("deviceIp 不能为空");
+        }
+        if (devicePort == null || devicePort < 1 || devicePort > 65535) {
+            throw new BusinessException("devicePort 无效");
+        }
+    }
+
+    private static String toEndpoint(String deviceIp, int devicePort) {
+        return deviceIp.trim() + ":" + devicePort;
+    }
+
+    private static String toSerial(String deviceIp, int devicePort) {
+        return toEndpoint(deviceIp, devicePort);
+    }
+
+    /**
+     * 合并 stdout/stderr,非 0 退出码抛出异常(文本类命令)
+     */
+    private String runAdbMergedOutput(long timeoutSeconds, List<String> command) throws IOException, InterruptedException {
+        ProcessBuilder pb = new ProcessBuilder(command);
+        pb.redirectErrorStream(true);
+        Process p = pb.start();
+        byte[] raw = readAll(p.getInputStream());
+        String text = decodeAdbConsole(raw);
+        boolean finished = p.waitFor(timeoutSeconds, TimeUnit.SECONDS);
+        if (!finished) {
+            p.destroyForcibly();
+            throw new IOException("adb 执行超时(" + timeoutSeconds + "s)");
+        }
+        int code = p.exitValue();
+        if (code != 0) {
+            throw new IOException(withTimeoutHint("adb 退出码 " + code + (text.isEmpty() ? "" : (": " + text))));
+        }
+        return text.isEmpty() ? "OK" : text;
+    }
+
+    private byte[] runAdbBinaryStdout(List<String> command, long timeoutSeconds) throws IOException, InterruptedException {
+        ProcessBuilder pb = new ProcessBuilder(command);
+        pb.redirectErrorStream(false);
+        Process p = pb.start();
+        byte[] out = readAll(p.getInputStream());
+        byte[] err = readAll(p.getErrorStream());
+        boolean finished = p.waitFor(timeoutSeconds, TimeUnit.SECONDS);
+        if (!finished) {
+            p.destroyForcibly();
+            throw new IOException("adb 执行超时(" + timeoutSeconds + "s)");
+        }
+        int code = p.exitValue();
+        String errStr = err.length == 0 ? "" : decodeAdbConsole(err);
+        if (code != 0) {
+            throw new IOException(withTimeoutHint("adb 退出码 " + code + (errStr.isEmpty() ? "" : (": " + errStr))));
+        }
+        if (out.length == 0 && !errStr.isEmpty()) {
+            throw new IOException(withTimeoutHint(errStr));
+        }
+        return out;
+    }
+
+    private static byte[] readAll(InputStream in) throws IOException {
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        byte[] buf = new byte[BUFFER];
+        int n;
+        while ((n = in.read(buf)) != -1) {
+            bos.write(buf, 0, n);
+        }
+        return bos.toByteArray();
+    }
+
+    private static void checkPng(byte[] data) {
+        if (data == null || data.length < 24) {
+            throw new BusinessException("截屏数据无效(过小)");
+        }
+        if (data[0] != (byte) 0x89 || data[1] != 0x50 || data[2] != 0x4E || data[3] != 0x47) {
+            throw new BusinessException("截屏数据不是有效的 PNG");
+        }
+    }
+}

+ 173 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/impl/EgDeviceRemoteUpgradeMqttServiceImpl.java

@@ -0,0 +1,173 @@
+package com.usky.eg.service.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.toolkit.StringUtils;
+import com.usky.common.core.exception.BusinessException;
+import com.usky.eg.mqtt.EgMqttOutboundConfig.EgMqttPublishGateway;
+import com.usky.eg.mqtt.EgMqttProperties;
+import com.usky.eg.service.EgDeviceRemoteUpgradeMqttService;
+import com.usky.eg.service.vo.EgDeviceAdbRemoteUpgradeBatchItemVO;
+import com.usky.eg.service.vo.EgDeviceAdbRemoteUpgradeBatchResultVO;
+import com.usky.eg.service.vo.EgDeviceAdbRemoteUpgradeItemResultVO;
+import com.usky.eg.service.vo.EgDeviceAdbRemoteUpgradeVO;
+import com.usky.eg.service.vo.EgDeviceAdbResultVO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+@Slf4j
+@Service
+public class EgDeviceRemoteUpgradeMqttServiceImpl implements EgDeviceRemoteUpgradeMqttService {
+
+    @Autowired
+    private EgMqttPublishGateway mqttPublishGateway;
+
+    @Autowired
+    private EgMqttProperties mqttProperties;
+
+    @Override
+    public EgDeviceAdbResultVO publishRemoteUpgrade(EgDeviceAdbRemoteUpgradeVO vo) {
+        if (vo == null) {
+            return EgDeviceAdbResultVO.fail("请求体为空");
+        }
+        if (StringUtils.isBlank(vo.getFileUrl())) {
+            throw new BusinessException("fileUrl 不能为空");
+        }
+        String urlTrim = vo.getFileUrl().trim();
+        if (!urlTrim.startsWith("http://") && !urlTrim.startsWith("https://")) {
+            throw new BusinessException("fileUrl 须为 http 或 https 地址");
+        }
+        String deviceId = resolveDeviceId(vo.getDeviceId(), vo.getDeviceCode());
+        if (StringUtils.isBlank(deviceId)) {
+            throw new BusinessException("MQTT 下发须指定 deviceId 或 deviceCode(与终端 connectWithDevice.deviceId 一致)");
+        }
+        String topic = buildTopic(deviceId);
+        String cmdId = UUID.randomUUID().toString();
+        String payload = buildPayload(cmdId, urlTrim, vo.getLaunchPackage(),
+                vo.getDeviceCode(), vo.getDeviceIp(), vo.getDevicePort());
+        return publishOne(topic, payload, cmdId);
+    }
+
+    @Override
+    public EgDeviceAdbRemoteUpgradeBatchResultVO publishRemoteUpgradeBatch(List<EgDeviceAdbRemoteUpgradeBatchItemVO> items,
+                                                                           String fileUrl, String launchPackage) {
+        if (items == null || items.isEmpty()) {
+            throw new BusinessException("items 不能为空");
+        }
+        if (StringUtils.isBlank(fileUrl)) {
+            throw new BusinessException("fileUrl 不能为空");
+        }
+        String urlTrim = fileUrl.trim();
+        if (!urlTrim.startsWith("http://") && !urlTrim.startsWith("https://")) {
+            throw new BusinessException("fileUrl 须为 http 或 https 地址");
+        }
+        List<EgDeviceAdbRemoteUpgradeItemResultVO> rows = new ArrayList<>(items.size());
+        int success = 0;
+        int fail = 0;
+        int index = 0;
+        for (EgDeviceAdbRemoteUpgradeBatchItemVO item : items) {
+            EgDeviceAdbResultVO r;
+            String mqttTopic = null;
+            String mqttCmdId = null;
+            if (item == null) {
+                r = EgDeviceAdbResultVO.fail("请求项为空");
+            } else {
+                String deviceId = resolveDeviceId(item.getDeviceId(), item.getDeviceCode());
+                if (StringUtils.isBlank(deviceId)) {
+                    r = EgDeviceAdbResultVO.fail("须指定 deviceId 或 deviceCode");
+                } else {
+                    try {
+                        String topic = buildTopic(deviceId);
+                        String cmdId = UUID.randomUUID().toString();
+                        String payload = buildPayload(cmdId, urlTrim, launchPackage,
+                                item.getDeviceCode(), item.getDeviceIp(), item.getDevicePort());
+                        r = publishOne(topic, payload, cmdId);
+                        mqttTopic = topic;
+                        mqttCmdId = cmdId;
+                    } catch (BusinessException e) {
+                        r = EgDeviceAdbResultVO.fail(e.getMessage() != null ? e.getMessage() : "参数错误");
+                    } catch (Exception e) {
+                        log.warn("mqtt remote upgrade publish failed index={} deviceId={}", index, deviceId, e);
+                        r = EgDeviceAdbResultVO.fail(e.getMessage() != null ? e.getMessage() : e.toString());
+                    }
+                }
+            }
+            if (EgDeviceAdbResultVO.STATUS_SUCCESS.equals(r.getStatus())) {
+                success++;
+            } else {
+                fail++;
+            }
+            String code = item != null ? item.getDeviceCode() : null;
+            String ip = item != null ? item.getDeviceIp() : null;
+            String port = item != null ? item.getDevicePort() : null;
+            String routeId = item != null ? resolveDeviceId(item.getDeviceId(), item.getDeviceCode()) : null;
+            rows.add(new EgDeviceAdbRemoteUpgradeItemResultVO(index++, code, ip, port, routeId, mqttTopic, mqttCmdId, r));
+        }
+        return new EgDeviceAdbRemoteUpgradeBatchResultVO(items.size(), success, fail, rows);
+    }
+
+    private EgDeviceAdbResultVO publishOne(String topic, String payload, String cmdId) {
+        try {
+            int qos = clampQos(mqttProperties.getQos());
+            mqttPublishGateway.sendToMqtt(topic, qos, payload);
+            log.info("mqtt remote upgrade published topic={} cmdId={}", topic, cmdId);
+            return EgDeviceAdbResultVO.ok("topic=" + topic + "; cmdId=" + cmdId);
+        } catch (Exception e) {
+            log.warn("mqtt publish failed topic={} cmdId={}", topic, cmdId, e);
+            return EgDeviceAdbResultVO.fail(e.getMessage() != null ? e.getMessage() : e.toString());
+        }
+    }
+
+    private String buildPayload(String cmdId, String fileUrl, String launchPackage,
+                                String deviceCode, String deviceIp, String devicePort) {
+        JSONObject o = new JSONObject();
+        o.put("id", cmdId);
+        o.put("type", mqttProperties.getCommandType() != null ? mqttProperties.getCommandType() : "egRemoteUpgrade");
+        o.put("fileUrl", fileUrl.trim());
+        if (StringUtils.isNotBlank(launchPackage)) {
+            o.put("launchPackage", launchPackage.trim());
+        }
+        if (StringUtils.isNotBlank(deviceCode)) {
+            o.put("deviceCode", deviceCode.trim());
+        }
+        if (StringUtils.isNotBlank(deviceIp)) {
+            o.put("deviceIp", deviceIp.trim());
+        }
+        if (StringUtils.isNotBlank(devicePort)) {
+            o.put("devicePort", devicePort.trim());
+        }
+        return o.toJSONString();
+    }
+
+    private static String resolveDeviceId(String deviceId, String deviceCode) {
+        if (StringUtils.isNotBlank(deviceId)) {
+            return deviceId.trim();
+        }
+        if (StringUtils.isNotBlank(deviceCode)) {
+            return deviceCode.trim();
+        }
+        return null;
+    }
+
+    private String buildTopic(String deviceId) {
+        String template = mqttProperties.getCommandTopicTemplate();
+        if (StringUtils.isBlank(template) || !template.contains("{deviceId}")) {
+            throw new BusinessException("eg.mqtt.command-topic-template 须为非空且包含占位符 {deviceId}");
+        }
+        return template.replace("{deviceId}", deviceId);
+    }
+
+    private static int clampQos(int qos) {
+        if (qos < 0) {
+            return 0;
+        }
+        if (qos > 2) {
+            return 2;
+        }
+        return qos;
+    }
+}

+ 34 - 7
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/impl/EgDeviceServiceImpl.java

@@ -279,8 +279,8 @@ public class EgDeviceServiceImpl extends AbstractCrudService<EgDeviceMapper, EgD
             }
         }
 
-        // 若未传deviceCode但传了deviceIp,尝试从心跳表中获取deviceCode
-        if (StringUtils.isBlank(egDevice.getDeviceCode()) && StringUtils.isNotBlank(egDevice.getDeviceIp())) {
+        // 若传了deviceIp,尝试从心跳表中获取deviceCode
+        if (StringUtils.isNotBlank(egDevice.getDeviceIp())) {
             LambdaQueryWrapper<EgDeviceHeartbeat> heartbeatCheckWrapper = Wrappers.lambdaQuery();
             heartbeatCheckWrapper.eq(EgDeviceHeartbeat::getIpAddr, egDevice.getDeviceIp())
                     .eq(EgDeviceHeartbeat::getDeviceStatus, 1);
@@ -311,11 +311,6 @@ public class EgDeviceServiceImpl extends AbstractCrudService<EgDeviceMapper, EgD
 
     @Override
     public void attachUpdate(EgDevice egDevice){
-
-        EgDevice one = this.getById(egDevice.getId());
-        egDevice.setBindFace(one.getBindFace());
-
-
         if(checkDeviceNameUnique(egDevice)){
             throw new BusinessException("新增门禁设备'"+egDevice.getDeviceName()+"'失败,设备已存在");
         }
@@ -548,9 +543,18 @@ public class EgDeviceServiceImpl extends AbstractCrudService<EgDeviceMapper, EgD
         page = sysPersonMapper.selectPage(page, personWrapper);
 
         List<SysPerson> records = page.getRecords();
+        List<Integer> pagePersonIds = new ArrayList<>();
+        for (SysPerson p : records) {
+            if (p != null && p.getId() != null) {
+                pagePersonIds.add(p.getId());
+            }
+        }
+        Map<Integer, Long> personUserIdMap = buildPersonUserIdMap(pagePersonIds);
         List<EgDeviceBindFacePersonVO> rows = new ArrayList<>(records.size());
         for (SysPerson p : records) {
             EgDeviceBindFacePersonVO vo = new EgDeviceBindFacePersonVO();
+            vo.setId(p.getId());
+            vo.setUserId(personUserIdMap.get(p.getId()));
             vo.setFullName(p.getFullName());
             vo.setAge(p.getAge());
             vo.setGender(p.getGender());
@@ -564,6 +568,29 @@ public class EgDeviceServiceImpl extends AbstractCrudService<EgDeviceMapper, EgD
         return new CommonPage<>(rows, page.getTotal(), sz, cur);
     }
 
+    private Map<Integer, Long> buildPersonUserIdMap(List<Integer> personIds) {
+        Map<Integer, Long> map = new HashMap<>();
+        if (CollectionUtils.isEmpty(personIds)) {
+            return map;
+        }
+        List<Map<String, Object>> rows = sysPersonMapper.selectUserIdsByPersonIds(personIds);
+        if (CollectionUtils.isEmpty(rows)) {
+            return map;
+        }
+        for (Map<String, Object> row : rows) {
+            if (row == null) {
+                continue;
+            }
+            Object personIdObj = row.get("personId");
+            Object userIdObj = row.get("userId");
+            if (personIdObj == null || userIdObj == null) {
+                continue;
+            }
+            map.put(Integer.valueOf(personIdObj.toString()), Long.valueOf(userIdObj.toString()));
+        }
+        return map;
+    }
+
     @Override
     public void bindPerson(Integer deviceId, String personIds, Integer isLoginNotify) {
         if (deviceId == null) {

+ 3 - 1
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/impl/EgRecordServiceImpl.java

@@ -61,7 +61,9 @@ public class EgRecordServiceImpl extends AbstractCrudService<EgRecordMapper, EgR
         }
         tenantId = one.getTenantId();
         egRecord.setEgDeviceId(one.getId());
-        egRecord.setCreateTime(LocalDateTime.now());
+        LocalDateTime now = LocalDateTime.now();
+        egRecord.setPassTime(now);
+        egRecord.setCreateTime(now);
         egRecord.setTenantId(tenantId);
         
         // 安全获取部门ID

+ 13 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceAdbEndpointVO.java

@@ -0,0 +1,13 @@
+package com.usky.eg.service.vo;
+
+import lombok.Data;
+
+/**
+ * adb connect / disconnect 请求体
+ */
+@Data
+public class EgDeviceAdbEndpointVO {
+
+    private String deviceIp;
+    private Integer devicePort;
+}

+ 19 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceAdbRemoteUpgradeBatchItemVO.java

@@ -0,0 +1,19 @@
+package com.usky.eg.service.vo;
+
+import lombok.Data;
+
+/**
+ * 批量远程升级中的单台设备:设备标识与连接信息;MQTT 模式须含 {@link #deviceId} 或 {@link #deviceCode}。
+ * 安装包与启动参数与 {@link EgDeviceAdbRemoteUpgradeVO} 中 {@code fileUrl}、{@code launchPackage} 同级。
+ */
+@Data
+public class EgDeviceAdbRemoteUpgradeBatchItemVO {
+
+    /** MQTT 路由用,未传时使用 {@link #deviceCode} */
+    private String deviceId;
+
+    private String deviceCode;
+    private String deviceIp;
+    /** 无线调试端口,缺省由服务端按 5555 处理 */
+    private String devicePort;
+}

+ 21 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceAdbRemoteUpgradeBatchResultVO.java

@@ -0,0 +1,21 @@
+package com.usky.eg.service.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+/**
+ * 批量远程升级汇总结果。
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class EgDeviceAdbRemoteUpgradeBatchResultVO {
+
+    private int total;
+    private int successCount;
+    private int failCount;
+    private List<EgDeviceAdbRemoteUpgradeItemResultVO> items;
+}

+ 34 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceAdbRemoteUpgradeItemResultVO.java

@@ -0,0 +1,34 @@
+package com.usky.eg.service.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 批量远程升级中单条设备的结果(与请求 {@code items} 下标对应)。
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class EgDeviceAdbRemoteUpgradeItemResultVO {
+
+    /** 与请求体 {@code items} 数组下标一致,从 0 开始 */
+    private int index;
+    private String deviceCode;
+    private String deviceIp;
+    private String devicePort;
+    /**
+     * MQTT 下发时:用于路由的主题中的设备标识(与请求 deviceId 或 deviceCode 解析结果一致)。
+     */
+    private String mqttRouteDeviceId;
+    /** MQTT 下发成功时填充 */
+    private String mqttTopic;
+    /** MQTT 下发成功时填充,与下行 JSON 中 {@code id} 一致 */
+    private String mqttCmdId;
+    private EgDeviceAdbResultVO result;
+
+    public EgDeviceAdbRemoteUpgradeItemResultVO(int index, String deviceCode, String deviceIp, String devicePort,
+                                                EgDeviceAdbResultVO result) {
+        this(index, deviceCode, deviceIp, devicePort, null, null, null, result);
+    }
+}

+ 36 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceAdbRemoteUpgradeVO.java

@@ -0,0 +1,36 @@
+package com.usky.eg.service.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * MQTT 远程升级请求体:向 Broker 发布下行 JSON(终端 {@code onCommand} 接收后自行下载安装)。
+ * <p>
+ * {@code fileUrl} 必填(http/https)。须指定 {@code deviceId} 或与终端序列号一致的 {@code deviceCode},用于主题
+ * {@code eg.mqtt.command-topic-template} 中的占位符 {@code {deviceId}}。
+ * {@code deviceIp}、{@code devicePort} 可选,会一并写入下行 JSON 供终端展示或日志。
+ * <p>
+ * 批量:当 {@code items} 非空时,与本对象同级的 {@code fileUrl}、{@code launchPackage} 对所有设备共用;
+ * 忽略顶层 {@code deviceCode}、{@code deviceIp}、{@code devicePort}。{@code items} 每项见 {@link EgDeviceAdbRemoteUpgradeBatchItemVO}。
+ */
+@Data
+public class EgDeviceAdbRemoteUpgradeVO {
+
+    /** 非空且非空列表时启用批量模式 */
+    private List<EgDeviceAdbRemoteUpgradeBatchItemVO> items;
+
+    /**
+     * 与终端 {@code connectWithDevice({ deviceId: getSerial() })} 一致;
+     * 未传时与 {@link #deviceCode} 等价(二选一至少填一个)。
+     */
+    private String deviceId;
+
+    private String deviceCode;
+    private String deviceIp;
+    /** 可选,写入下行 JSON */
+    private String devicePort;
+    private String fileUrl;
+    /** 可选,写入下行 JSON,终端安装成功后自行拉起 */
+    private String launchPackage;
+}

+ 29 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceAdbResultVO.java

@@ -0,0 +1,29 @@
+package com.usky.eg.service.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 官方 adb 文本类命令的统一返回(与前端约定 status)
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class EgDeviceAdbResultVO {
+
+    public static final String STATUS_SUCCESS = "SUCCESS";
+    public static final String STATUS_FAIL = "FAIL";
+
+    private String status;
+    /** adb 标准输出/合并日志摘要,便于排查 */
+    private String detail;
+
+    public static EgDeviceAdbResultVO ok(String detail) {
+        return new EgDeviceAdbResultVO(STATUS_SUCCESS, detail == null ? "" : detail.trim());
+    }
+
+    public static EgDeviceAdbResultVO fail(String detail) {
+        return new EgDeviceAdbResultVO(STATUS_FAIL, detail == null ? "" : detail.trim());
+    }
+}

+ 15 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceAdbTcpipVO.java

@@ -0,0 +1,15 @@
+package com.usky.eg.service.vo;
+
+import lombok.Data;
+
+/**
+ * adb tcpip 请求体(对应 adb -s ip:devicePort tcpip &lt;port&gt;)
+ */
+@Data
+public class EgDeviceAdbTcpipVO {
+
+    private String deviceIp;
+    private Integer devicePort;
+    /** tcpip 命令中的监听端口参数,例如 5555 */
+    private Integer port;
+}

+ 4 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceBindFacePersonVO.java

@@ -10,6 +10,10 @@ public class EgDeviceBindFacePersonVO implements Serializable {
 
     private static final long serialVersionUID = 1L;
 
+    /** 人员主键ID(sys_person.id) */
+    private Integer id;
+    /** 用户ID(sys_user_person.user_id) */
+    private Long userId;
     private String fullName;
     private Integer age;
     private Integer gender;

+ 66 - 0
service-eg/service-eg-biz/src/main/resources/bootstrap.yml

@@ -23,3 +23,69 @@ spring:
         # 共享配置
         shared-configs:
           - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
+
+# SmartFace 人脸识别配置
+smartface:
+  # 启用人脸识别功能
+  enabled: true
+  # 是否延迟加载模型(true=首次使用时加载,false=启动时加载)
+  # 如果模型文件还未准备好,建议设置为true,避免启动失败
+  lazy-load: true
+  # 人脸检测配置
+  detection:
+    # 模型类型: MTCNN, RETINA_FACE, YOLOV5_FACE_320, YOLOV5_FACE_640
+    model-type: MTCNN
+    # 人脸检测模型路径(留空,将自动从classpath复制到临时目录)
+    model-path:
+    # 置信度阈值
+    confidence-threshold: 0.5
+    # NMS阈值
+    nms-threshold: 0.4
+    # 设备类型: CPU, GPU
+    device: CPU
+  # 人脸识别配置
+  recognition:
+    # 模型类型: INSIGHT_FACE_IRSE50_MODEL, INSIGHT_FACE_MOBILE_FACENET_MODEL, FACE_NET_MODEL
+    model-type: INSIGHT_FACE_IRSE50_MODEL
+    # 人脸识别模型路径(留空,将自动从classpath复制到临时目录)
+    model-path:
+    # 是否裁剪人脸
+    crop-face: true
+    # 是否启用人脸对齐
+    align: true
+    # 相似度阈值(0-1之间,推荐0.62)
+    similarity-threshold: 0.62
+    # 设备类型: CPU, GPU
+    device: CPU
+  # 向量数据库配置
+  vector-db:
+    # 数据库类型: SQLITE, MILVUS
+    type: SQLITE
+    # SQLite数据库文件路径
+    sqlite-path: ./data/face_db.sqlite
+    # Milvus配置(如果使用Milvus)
+    milvus:
+      host: 127.0.0.1
+      port: 19530
+      collection-name: face_collection
+
+# 服务端调用官方 adb(platform-tools 需在 PATH);详见 EgDeviceAdbController:/egDevice/adb/connect 等
+eg:
+  adb:
+    executable: adb
+    command-timeout-seconds: 45
+    screenshot-timeout-seconds: 60
+    tcpip-retry-after-connect: true
+    # 截屏前先 adb connect,避免服务端 adb 未登记设备 serial
+    screenshot-connect-first: true
+
+  # 远程升级仅 MQTT 下发(与终端 connectWithDevice 订阅主题一致)
+  mqtt:
+    url: tcp://47.98.201.73:1883
+    username: usky
+    password: usky
+    # 须含 {deviceId},与终端 deviceId(如 getSerial())一致
+    command-topic-template: usky/device/{deviceId}/command
+    qos: 1
+    # 下行 JSON 的 type 字段,终端 onCommand 可按此分支
+    command-type: egRemoteUpgrade

+ 0 - 24
service-eg/service-eg-biz/src/main/resources/mapper/eg/EgDeviceMapper.xml

@@ -25,12 +25,6 @@
         <result column="password" property="password" />
         <result column="work_status" property="workStatus" />
     </resultMap>
-    <insert id="insertMeetingFaceDevice" parameterType="integer">
-        insert into meeting_face_device (face_id,device_id) value (#{fid}, #{egDeviceId});
-    </insert>
-    <delete id="deleteMeetingFaceDevice" parameterType="integer">
-        delete from meeting_face_device where device_id = #{egDeviceId};
-    </delete>
     <select id="sysTenantId" resultType="integer">
         select id
         from sys_tenant
@@ -40,23 +34,5 @@
             </if>
         </where>
     </select>
-    <select id="getMeetingFaceData" resultType="integer">
-        select mf.fid as fid
-        from meeting_face mf
-        <where>
-            <if test="userId != null">
-                and mf.user_id = #{userId}
-            </if>
-        </where>
-    </select>
-    <select id="getMeetingFaceDeviceList" resultType="integer">
-        select mfd.device_id as deviceFid
-        from meeting_face_device mfd
-        <where>
-            <if test="fid != null">
-                and mfd.face_id = #{fid}
-            </if>
-        </where>
-    </select>
 
 </mapper>

+ 0 - 11
service-eg/service-eg-biz/src/main/resources/mapper/eg/MeetingFaceDeviceMapper.xml

@@ -1,11 +0,0 @@
-<?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.eg.mapper.MeetingFaceDeviceMapper">
-
-    <!-- 通用查询映射结果 -->
-    <resultMap id="BaseResultMap" type="com.usky.eg.domain.MeetingFaceDevice">
-        <id column="face_id" property="faceId" />
-        <result column="device_id" property="deviceId" />
-    </resultMap>
-
-</mapper>

+ 0 - 21
service-eg/service-eg-biz/src/main/resources/mapper/eg/MeetingFaceMapper.xml

@@ -1,21 +0,0 @@
-<?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.eg.mapper.MeetingFaceMapper">
-
-    <!-- 通用查询映射结果 -->
-    <resultMap id="BaseResultMap" type="com.usky.eg.domain.MeetingFace">
-        <id column="fid" property="fid" />
-        <result column="face_base" property="faceBase" />
-        <result column="create_time" property="createTime" />
-        <result column="vef_num" property="vefNum" />
-        <result column="face_name" property="faceName" />
-        <result column="remark" property="remark" />
-        <result column="face_status" property="faceStatus" />
-        <result column="card_num" property="cardNum" />
-        <result column="bind_device" property="bindDevice" />
-        <result column="dept_id" property="deptId" />
-        <result column="tenant_id" property="tenantId" />
-        <result column="user_id" property="userId" />
-    </resultMap>
-
-</mapper>

+ 13 - 0
service-eg/service-eg-biz/src/main/resources/mapper/eg/SysPersonMapper.xml

@@ -29,4 +29,17 @@
         <result column="card_num" property="cardNum"/>
     </resultMap>
 
+    <select id="selectUserIdsByPersonIds" resultType="java.util.HashMap">
+        select person_id as personId, user_id as userId
+        from sys_user_person
+        <where>
+            <if test="personIds != null and personIds.size() > 0">
+                and person_id in
+                <foreach collection="personIds" item="id" open="(" separator="," close=")">
+                    #{id}
+                </foreach>
+            </if>
+        </where>
+    </select>
+
 </mapper>

+ 13 - 0
service-fire/service-fire-api/src/main/java/com/usky/fire/RemotePlanSonService.java

@@ -0,0 +1,13 @@
+package com.usky.fire;
+
+
+import com.usky.fire.factory.RemoteFireFallbackFactory;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.*;
+
+@FeignClient(contextId = "remotePlanSonService", value = "service-fire" , fallbackFactory = RemoteFireFallbackFactory.class)
+public interface RemotePlanSonService {
+
+    @GetMapping("/addPlanSon")
+    void addPlanSon();
+}

+ 33 - 0
service-fire/service-fire-api/src/main/java/com/usky/fire/factory/RemotePlanSonFactory.java

@@ -0,0 +1,33 @@
+package com.usky.fire.factory;
+
+import com.usky.common.core.exception.FeignBadRequestException;
+import com.usky.fire.RemotePlanSonService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.cloud.openfeign.FallbackFactory;
+import org.springframework.stereotype.Component;
+
+
+/**
+ * 用户服务降级处理
+ *
+ * @author ruoyi
+ */
+@Component
+public class RemotePlanSonFactory implements FallbackFactory<RemotePlanSonService>
+{
+    private static final Logger log = LoggerFactory.getLogger(RemoteFireFallbackFactory.class);
+
+    @Override
+    public RemotePlanSonService create(Throwable throwable)
+    {
+        log.error("用户服务调用失败:{}", throwable.getMessage());
+        return new RemotePlanSonService()
+        {
+            @Override
+            public void addPlanSon() {
+                throw new FeignBadRequestException(500,"生成子计划数据异常"+throwable.getMessage());
+            }
+        };
+    }
+}

+ 35 - 0
service-fire/service-fire-biz/src/main/java/com/usky/fire/controller/api/planSonApi.java

@@ -0,0 +1,35 @@
+package com.usky.fire.controller.api;
+
+import com.usky.common.core.bean.ApiResult;
+import com.usky.fire.RemoteFireService;
+import com.usky.fire.RemotePlanSonService;
+import com.usky.fire.service.PatrolInspectionPlanService;
+import com.usky.fire.service.PatrolInspectionPlanSonService;
+import com.usky.system.RemoteDeptService;
+import io.swagger.annotations.Api;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cloud.openfeign.FallbackFactory;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * <p>
+ *  数据统一查询 前端控制器
+ * </p>
+ *
+ */
+@RestController
+public class planSonApi implements RemotePlanSonService {
+
+    @Autowired
+    private PatrolInspectionPlanService patrolInspectionPlanService;
+
+    @Override
+    public void addPlanSon(){
+        patrolInspectionPlanService.addPatrolInspectionPlanSon();
+    }
+
+
+}

+ 11 - 0
service-fire/service-fire-biz/src/main/java/com/usky/fire/controller/web/PatrolInspectionPlanController.java

@@ -123,5 +123,16 @@ public class PatrolInspectionPlanController {
         return ApiResult.success();
     }
 
+    /**
+     * 巡查自检-近七天子计划缺失补插(已存在则跳过)
+     *
+     * @return
+     */
+    @GetMapping("supplementNearSevenDaysPlanSon")
+    public ApiResult<Void> supplementNearSevenDaysPlanSon() {
+        patrolInspectionPlanService.supplementNearSevenDaysPlanSon();
+        return ApiResult.success();
+    }
+
 }
 

+ 5 - 5
service-fire/service-fire-biz/src/main/java/com/usky/fire/controller/web/TaskController.java

@@ -78,11 +78,11 @@ public class TaskController {
         demReportDataService.regularReportData();
     }
 
-    @Scheduled(cron = "0 0 23 * * ? ")//每天23点
-    public void task5(){
-        System.out.println(Thread.currentThread().getName() + "巡检计划分批次创建执行中");
-        patrolInspectionPlanService.addPatrolInspectionPlanSon();
-    }
+//    @Scheduled(cron = "0 0 23 * * ? ")//每天23点
+//    public void task5(){
+//        System.out.println(Thread.currentThread().getName() + "巡检计划分批次创建执行中");
+//        patrolInspectionPlanService.addPatrolInspectionPlanSon();
+//    }
 
 //    @Scheduled(cron = "0 5 * * * ?") //每个小时第五分钟执行
 //    public void task6(){

+ 5 - 0
service-fire/service-fire-biz/src/main/java/com/usky/fire/service/PatrolInspectionPlanService.java

@@ -28,6 +28,11 @@ public interface PatrolInspectionPlanService extends CrudService<PatrolInspectio
 
     void addPatrolInspectionPlanSon();
 
+    /**
+     * 查询近七天符合时间段的主计划,检查其近七天子计划是否存在;不存在则补插,已存在则跳过
+     */
+    void supplementNearSevenDaysPlanSon();
+
     void delPatrolInspectionPlan(Integer id);
 
     CommonPage<PatrolInspectionPlanDataVo> patrolInspectionPlanList(String planName,Integer planType, Integer areaId,Integer pageNum, Integer pageSize, Integer id);

+ 0 - 32
service-fire/service-fire-biz/src/main/java/com/usky/fire/service/config/SchedulerConfig.java

@@ -1,32 +0,0 @@
-package com.usky.fire.service.config;
-
-import org.springframework.context.annotation.Configuration;
-import org.springframework.scheduling.annotation.EnableScheduling;
-import org.springframework.scheduling.annotation.SchedulingConfigurer;
-import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
-import org.springframework.scheduling.config.ScheduledTaskRegistrar;
-
-/**
- * 定时任务调度器配置
- * 默认 Spring 调度器线程池为 1,多个 @Scheduled 任务会互相阻塞,
- * 当某个任务(如每 5 分钟的 task())发生网络阻塞时会拖垮其它任务(如巡检计划补生成 task5)。
- * 这里改为多线程调度器,避免任务间相互饿死。
- *
- * @author JCB
- * @since 2026-07-13
- */
-@Configuration
-@EnableScheduling
-public class SchedulerConfig implements SchedulingConfigurer {
-
-    @Override
-    public void configureTasks(ScheduledTaskRegistrar registrar) {
-        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
-        scheduler.setPoolSize(5);
-        scheduler.setThreadNamePrefix("sched-pool-");
-        scheduler.setWaitForTasksToCompleteOnShutdown(true);
-        scheduler.setAwaitTerminationSeconds(60);
-        scheduler.initialize();
-        registrar.setTaskScheduler(scheduler);
-    }
-}

+ 2 - 1
service-fire/service-fire-biz/src/main/java/com/usky/fire/service/config/SyncPatrolInspectionEvent.java

@@ -17,7 +17,7 @@ public class SyncPatrolInspectionEvent {
     private RemoteDictService remoteDictService;
 
     @Async("asyncServiceExecutor")
-    public void sendAsyncMessage(Integer eventId,String eventName,Integer eventType){
+    public void sendAsyncMessage(Integer tenantId,Integer eventId,String eventName,Integer eventType){
         String eventTypeName = remoteDictService.selectNameById("patrol_event_type",
                 eventType.toString());
         JsonObject jsonObject = new JsonObject();
@@ -26,6 +26,7 @@ public class SyncPatrolInspectionEvent {
         jsonObject.addProperty("infoType",2);
         jsonObject.addProperty("userName", SecurityUtils.getUsername());
         jsonObject.addProperty("id",eventId);
+        jsonObject.addProperty("tenantId",tenantId);
         remoteMceService.addMce(jsonObject.toString());
     }
 }

+ 1 - 1
service-fire/service-fire-biz/src/main/java/com/usky/fire/service/impl/PatrolInspectionEventServiceImpl.java

@@ -134,7 +134,7 @@ public class PatrolInspectionEventServiceImpl extends AbstractCrudService<Patrol
 
         this.save(patrolInspectionEvent);
 
-//        syncPatrolInspectionEvent.sendAsyncMessage(patrolInspectionEvent.getId(),patrolInspectionEvent.getEventName(),patrolInspectionEvent.getEventType());
+//        syncPatrolInspectionEvent.sendAsyncMessage(SecurityUtils.getTenantId(),patrolInspectionEvent.getId(),patrolInspectionEvent.getEventName(),patrolInspectionEvent.getEventType());
 
     }
 

+ 166 - 61
service-fire/service-fire-biz/src/main/java/com/usky/fire/service/impl/PatrolInspectionPlanServiceImpl.java

@@ -25,7 +25,6 @@ import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import java.util.stream.Collectors;
 
 /**
@@ -482,24 +481,112 @@ public class PatrolInspectionPlanServiceImpl extends AbstractCrudService<PatrolI
 
     @Override
     @Transactional
-    public void addPatrolInspectionPlanSon() {
-        LocalDate today = LocalDate.now();
-        // 1) 补最近 7 天缺口:today-6 ~ today(含当天,共 7 天)
-        LocalDate backfillStart = today.minusDays(6);
-        // 2) 维持向前预生成:today + 7(保持未来 7 天视野)
-        LocalDate forwardDate = today.plusDays(7);
+    public void addPatrolInspectionPlanSon(){
+        LambdaQueryWrapper<PatrolInspectionPlan> queryWrapper = Wrappers.lambdaQuery();
+        queryWrapper.le(PatrolInspectionPlan::getStartDate,LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")))
+                .ge(PatrolInspectionPlan::getEndDate,OnlineMethod.getDate(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")), "yyyy-MM-dd", true, 7))
+                .eq(PatrolInspectionPlan::getEnable,1);
+        List<PatrolInspectionPlan> list = this.list(queryWrapper);
+        if(CollectionUtils.isNotEmpty(list)){
+            List<Integer> planIdList = new ArrayList<>();
+            for (int i = 0; i < list.size(); i++) {
+                planIdList.add(list.get(i).getId());
+            }
+
+            //巡检日程关联信息
+            LambdaQueryWrapper<PatrolInspectionPlanSchedule> queryWrapper1 = Wrappers.lambdaQuery();
+            queryWrapper1.in(PatrolInspectionPlanSchedule::getPlanId,planIdList);
+            List<PatrolInspectionPlanSchedule> scheduleList = planScheduleService.list(queryWrapper1);
+            if(CollectionUtils.isEmpty(scheduleList)){
+                throw new BusinessException("无巡检日程关联信息");
+            }
+
+            //主计划地点关联
+            LambdaQueryWrapper<PatrolInspectionPlanSite> queryWrapper2 = Wrappers.lambdaQuery();
+            queryWrapper2.in(PatrolInspectionPlanSite::getPlanId,planIdList);
+            List<PatrolInspectionPlanSite> siteList = planSiteService.list(queryWrapper2);
+            if(CollectionUtils.isEmpty(siteList)){
+                throw new BusinessException("无主计划地点关联信息");
+            }
+
+            //分配创建巡检计划之前先判断数据库中是否已经存在这条计划记录
+            LocalDate inspectionDate = OnlineMethod.getDate(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")), "yyyy-MM-dd", true, 7);
+            LambdaQueryWrapper<PatrolInspectionPlanSon> queryWrapper3 = Wrappers.lambdaQuery();
+            queryWrapper3.select(PatrolInspectionPlanSon::getPlanId)
+                    .eq(PatrolInspectionPlanSon::getInspectionDate,inspectionDate);
+            List<PatrolInspectionPlanSon> planSonList = patrolInspectionPlanSonService.list(queryWrapper3);
+            List<Integer> planSonPlanIdList = new ArrayList<>();
+            if(CollectionUtils.isNotEmpty(planSonList)){
+                for (int i = 0; i < planSonList.size(); i++) {
+                    planSonPlanIdList.add(planSonList.get(i).getPlanId());
+                }
+            }
+
+            for (int m = 0; m < list.size(); m++) {
+                //子表数据添加
+                if (list.get(m).getPlanType() == 1) {//普通计划 当前日期+7天在巡检计划时间范围内,就创建一条当前日期+7天日期对应的巡检记录
+                    for (int j = 0; j < scheduleList.size(); j++) {
+                        if((list.get(m).getId().equals(scheduleList.get(j).getPlanId())) && (!planSonPlanIdList.contains(list.get(m).getId()))){
+                            LocalDate s = OnlineMethod.getDate(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")), "yyyy-MM-dd", true, 7);
+                            Date date = Date.from(s.atStartOfDay(ZoneOffset.ofHours(8)).toInstant());
+                            String week = OnlineMethod.getWeekOfDate(date);
+                            if (!list.get(m).getRestDay().contains(week)) {
+                                PatrolInspectionPlanSon patrolInspectionPlanSon = new PatrolInspectionPlanSon();
+                                patrolInspectionPlanSon.setPlanId(list.get(m).getId());
+                                patrolInspectionPlanSon.setInspectionDate(s);
+                                patrolInspectionPlanSon.setStartTime(scheduleList.get(j).getStartTime());
+                                patrolInspectionPlanSon.setEndTime(scheduleList.get(j).getEndTime());
+                                patrolInspectionPlanSon.setPersonnelId(scheduleList.get(j).getPersonnelId());
+                                patrolInspectionPlanSon.setAreaId(list.get(m).getAreaId());
+                                patrolInspectionPlanSon.setPlanType(list.get(m).getPlanType());
+                                patrolInspectionPlanSon.setPlanCycle(list.get(m).getPlanCycle());
+                                patrolInspectionPlanSon.setPlanFrequency(list.get(m).getPlanFrequency());
+                                patrolInspectionPlanSon.setLapTime(list.get(m).getLapTime());
+                                patrolInspectionPlanSon.setIntervalTime(list.get(m).getIntervalTime());
+                                patrolInspectionPlanSon.setPlanDescribe(list.get(m).getPlanDescribe());
+                                patrolInspectionPlanSon.setCompletion(0);
+                                patrolInspectionPlanSon.setCreateTime(LocalDateTime.now());
+                                patrolInspectionPlanSon.setCreator(list.get(m).getCreator());
+                                patrolInspectionPlanSon.setAlternateField("0");
+                                patrolInspectionPlanSon.setTenantId(list.get(m).getTenantId());
+                                patrolInspectionPlanSon.setCompanyId(list.get(m).getCompanyId());
+                                patrolInspectionPlanSonService.save(patrolInspectionPlanSon);
+                                Integer zfid = patrolInspectionPlanSon.getId();
+                                for (int k = 0; k < siteList.size(); k++) {
+                                    if(list.get(m).getId().equals(siteList.get(k).getPlanId())){
+                                        PatrolInspectionPlanSiteSon planSiteSon = new PatrolInspectionPlanSiteSon();
+                                        planSiteSon.setPlanId(zfid);
+                                        planSiteSon.setSiteId(siteList.get(k).getSiteId());
+                                        planSiteSon.setInspectionStatus(1);
+                                        planSiteSonService.save(planSiteSon);
+                                    }
+
+                                }
+                            }
+                        }
+
+                    }
+                }
+            }
 
-        // 候选日期集合 = 最近 7 天 + 向前第 7 天(不做全量历史回填)
-        List<LocalDate> candidateDates = new ArrayList<>();
-        for (int i = 0; i <= 6; i++) {
-            candidateDates.add(backfillStart.plusDays(i));
         }
-        candidateDates.add(forwardDate);
+    }
+
+    /**
+     * 查询近七天(今天 ~ 今天+6)符合时间段的主计划,按日检查子计划是否存在;
+     * 不存在则补插子计划及地点关联,已存在则跳过
+     */
+    @Override
+    @Transactional
+    public void supplementNearSevenDaysPlanSon() {
+        String todayStr = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
+        LocalDate startOfNearSeven = LocalDate.now();
+        LocalDate endOfNearSeven = OnlineMethod.getDate(todayStr, "yyyy-MM-dd", true, 6);
 
-        // 查询在候选日期窗口内仍生效、且覆盖该窗口的普通计划
+        // 查询近七天时间范围内生效的主计划:开始日期 <= 近七天末天,结束日期 >= 今天
         LambdaQueryWrapper<PatrolInspectionPlan> queryWrapper = Wrappers.lambdaQuery();
-        queryWrapper.le(PatrolInspectionPlan::getStartDate, forwardDate)
-                .ge(PatrolInspectionPlan::getEndDate, backfillStart)
+        queryWrapper.le(PatrolInspectionPlan::getStartDate, endOfNearSeven)
+                .ge(PatrolInspectionPlan::getEndDate, startOfNearSeven)
                 .eq(PatrolInspectionPlan::getEnable, 1)
                 .eq(PatrolInspectionPlan::getPlanType, 1);
         List<PatrolInspectionPlan> list = this.list(queryWrapper);
@@ -507,9 +594,12 @@ public class PatrolInspectionPlanServiceImpl extends AbstractCrudService<PatrolI
             return;
         }
 
-        List<Integer> planIdList = list.stream().map(PatrolInspectionPlan::getId).collect(Collectors.toList());
+        List<Integer> planIdList = new ArrayList<>();
+        for (int i = 0; i < list.size(); i++) {
+            planIdList.add(list.get(i).getId());
+        }
 
-        //巡检日程关联信息
+        // 巡检日程关联信息
         LambdaQueryWrapper<PatrolInspectionPlanSchedule> queryWrapper1 = Wrappers.lambdaQuery();
         queryWrapper1.in(PatrolInspectionPlanSchedule::getPlanId, planIdList);
         List<PatrolInspectionPlanSchedule> scheduleList = planScheduleService.list(queryWrapper1);
@@ -517,7 +607,7 @@ public class PatrolInspectionPlanServiceImpl extends AbstractCrudService<PatrolI
             throw new BusinessException("无巡检日程关联信息");
         }
 
-        //主计划地点关联
+        // 主计划地点关联
         LambdaQueryWrapper<PatrolInspectionPlanSite> queryWrapper2 = Wrappers.lambdaQuery();
         queryWrapper2.in(PatrolInspectionPlanSite::getPlanId, planIdList);
         List<PatrolInspectionPlanSite> siteList = planSiteService.list(queryWrapper2);
@@ -525,65 +615,80 @@ public class PatrolInspectionPlanServiceImpl extends AbstractCrudService<PatrolI
             throw new BusinessException("无主计划地点关联信息");
         }
 
-        // 已生成过子计划的 (planId, inspectionDate) 组合,避免重复插入
+        // 查询近七天已存在的子计划(按主计划id + 巡检日期判断是否存在)
         LambdaQueryWrapper<PatrolInspectionPlanSon> queryWrapper3 = Wrappers.lambdaQuery();
-        queryWrapper3.in(PatrolInspectionPlanSon::getPlanId, planIdList)
-                .in(PatrolInspectionPlanSon::getInspectionDate, candidateDates)
-                .select(PatrolInspectionPlanSon::getPlanId, PatrolInspectionPlanSon::getInspectionDate);
-        Set<String> existsKeys = patrolInspectionPlanSonService.list(queryWrapper3).stream()
-                .map(e -> e.getPlanId() + "_" + e.getInspectionDate().toString())
-                .collect(Collectors.toSet());
-
-        for (PatrolInspectionPlan plan : list) {
-            for (LocalDate targetDate : candidateDates) {
-                // 仅处理落在计划有效期内的日期
-                if (targetDate.isBefore(plan.getStartDate()) || targetDate.isAfter(plan.getEndDate())) {
+        queryWrapper3.select(PatrolInspectionPlanSon::getPlanId, PatrolInspectionPlanSon::getInspectionDate)
+                .in(PatrolInspectionPlanSon::getPlanId, planIdList)
+                .ge(PatrolInspectionPlanSon::getInspectionDate, startOfNearSeven)
+                .le(PatrolInspectionPlanSon::getInspectionDate, endOfNearSeven);
+        List<PatrolInspectionPlanSon> planSonList = patrolInspectionPlanSonService.list(queryWrapper3);
+        List<String> existPlanSonKeyList = new ArrayList<>();
+        if (CollectionUtils.isNotEmpty(planSonList)) {
+            for (int i = 0; i < planSonList.size(); i++) {
+                existPlanSonKeyList.add(planSonList.get(i).getPlanId() + "_" + planSonList.get(i).getInspectionDate());
+            }
+        }
+
+        // 逐日检查并补插缺失的子计划
+        for (int dayOffset = 0; dayOffset < 7; dayOffset++) {
+            LocalDate targetDate = dayOffset == 0 ? startOfNearSeven
+                    : OnlineMethod.getDate(todayStr, "yyyy-MM-dd", true, dayOffset);
+            Date date = Date.from(targetDate.atStartOfDay(ZoneOffset.ofHours(8)).toInstant());
+            String week = OnlineMethod.getWeekOfDate(date);
+
+            for (int m = 0; m < list.size(); m++) {
+                PatrolInspectionPlan plan = list.get(m);
+                // 目标日期需落在该主计划有效期内
+                if (plan.getStartDate().isAfter(targetDate) || plan.getEndDate().isBefore(targetDate)) {
                     continue;
                 }
-                // 已生成过则跳过(防重复)
-                if (existsKeys.contains(plan.getId() + "_" + targetDate.toString())) {
+                String existKey = plan.getId() + "_" + targetDate;
+                // 已存在则不做任何操作
+                if (existPlanSonKeyList.contains(existKey)) {
                     continue;
                 }
-                Date date = Date.from(targetDate.atStartOfDay(ZoneOffset.ofHours(8)).toInstant());
-                String week = OnlineMethod.getWeekOfDate(date);
+                // 休息日不生成子计划
                 if (plan.getRestDay() != null && plan.getRestDay().contains(week)) {
                     continue;
                 }
-                for (PatrolInspectionPlanSchedule schedule : scheduleList) {
-                    if (!plan.getId().equals(schedule.getPlanId())) {
+
+                for (int j = 0; j < scheduleList.size(); j++) {
+                    if (!plan.getId().equals(scheduleList.get(j).getPlanId())) {
                         continue;
                     }
-                    PatrolInspectionPlanSon son = new PatrolInspectionPlanSon();
-                    son.setPlanId(plan.getId());
-                    son.setInspectionDate(targetDate);
-                    son.setStartTime(schedule.getStartTime());
-                    son.setEndTime(schedule.getEndTime());
-                    son.setPersonnelId(schedule.getPersonnelId());
-                    son.setAreaId(plan.getAreaId());
-                    son.setPlanType(plan.getPlanType());
-                    son.setPlanCycle(plan.getPlanCycle());
-                    son.setPlanFrequency(plan.getPlanFrequency());
-                    son.setLapTime(plan.getLapTime());
-                    son.setIntervalTime(plan.getIntervalTime());
-                    son.setPlanDescribe(plan.getPlanDescribe());
-                    son.setCompletion(0);
-                    son.setCreateTime(LocalDateTime.now());
-                    son.setCreator(plan.getCreator());
-                    son.setAlternateField("0");
-                    son.setTenantId(plan.getTenantId());
-                    son.setCompanyId(plan.getCompanyId());
-                    patrolInspectionPlanSonService.save(son);
-                    Integer zfid = son.getId();
-                    for (PatrolInspectionPlanSite site : siteList) {
-                        if (plan.getId().equals(site.getPlanId())) {
+                    PatrolInspectionPlanSon patrolInspectionPlanSon = new PatrolInspectionPlanSon();
+                    patrolInspectionPlanSon.setPlanId(plan.getId());
+                    patrolInspectionPlanSon.setInspectionDate(targetDate);
+                    patrolInspectionPlanSon.setStartTime(scheduleList.get(j).getStartTime());
+                    patrolInspectionPlanSon.setEndTime(scheduleList.get(j).getEndTime());
+                    patrolInspectionPlanSon.setPersonnelId(scheduleList.get(j).getPersonnelId());
+                    patrolInspectionPlanSon.setAreaId(plan.getAreaId());
+                    patrolInspectionPlanSon.setPlanType(plan.getPlanType());
+                    patrolInspectionPlanSon.setPlanCycle(plan.getPlanCycle());
+                    patrolInspectionPlanSon.setPlanFrequency(plan.getPlanFrequency());
+                    patrolInspectionPlanSon.setLapTime(plan.getLapTime());
+                    patrolInspectionPlanSon.setIntervalTime(plan.getIntervalTime());
+                    patrolInspectionPlanSon.setPlanDescribe(plan.getPlanDescribe());
+                    patrolInspectionPlanSon.setCompletion(0);
+                    patrolInspectionPlanSon.setCreateTime(LocalDateTime.now());
+                    patrolInspectionPlanSon.setCreator(plan.getCreator());
+                    patrolInspectionPlanSon.setAlternateField("0");
+                    patrolInspectionPlanSon.setTenantId(plan.getTenantId());
+                    patrolInspectionPlanSon.setCompanyId(plan.getCompanyId());
+                    patrolInspectionPlanSonService.save(patrolInspectionPlanSon);
+                    Integer zfid = patrolInspectionPlanSon.getId();
+                    for (int k = 0; k < siteList.size(); k++) {
+                        if (plan.getId().equals(siteList.get(k).getPlanId())) {
                             PatrolInspectionPlanSiteSon planSiteSon = new PatrolInspectionPlanSiteSon();
                             planSiteSon.setPlanId(zfid);
-                            planSiteSon.setSiteId(site.getSiteId());
+                            planSiteSon.setSiteId(siteList.get(k).getSiteId());
                             planSiteSon.setInspectionStatus(1);
                             planSiteSonService.save(planSiteSon);
                         }
                     }
                 }
+                // 补插后加入已存在标记,避免同日重复创建
+                existPlanSonKeyList.add(existKey);
             }
         }
     }
@@ -789,4 +894,4 @@ public class PatrolInspectionPlanServiceImpl extends AbstractCrudService<PatrolI
         }
         return list;
     }
-}
+}

+ 1 - 0
service-iot/service-iot-biz/src/main/java/com/usky/iot/controller/web/DmpProductInfoController.java

@@ -9,6 +9,7 @@ import com.usky.iot.domain.DmpProductInfo;
 import com.usky.iot.service.DmpProductInfoService;
 import com.usky.iot.service.vo.DmpProductInfoRequest;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.bind.annotation.*;
 
 import java.util.List;

+ 1 - 0
service-iot/service-iot-biz/src/main/java/com/usky/iot/service/config/DingTalkAndMessage.java

@@ -330,6 +330,7 @@ public class DingTalkAndMessage {
         jsonObject.put("id", newReport.getId());
         jsonObject.put("infoTypeName", INFO_TITLE);
         jsonObject.put("userName", newReport.getCreateBy());
+        jsonObject.put("tenantId", newReport.getTenantId());
         if (!userId.isEmpty()) {
             jsonObject.put("userIds", userId);
         }

+ 54 - 49
service-iot/service-iot-biz/src/main/java/com/usky/iot/service/impl/BaseAlarmServiceImpl.java

@@ -205,6 +205,7 @@ public class BaseAlarmServiceImpl extends AbstractCrudService<BaseAlarmMapper, B
         }
 
         IPage<BaseAlarm> page = new Page<>(baseAlarmListVO.getCurrent(), baseAlarmListVO.getSize());
+        List<BaseAlarm> records = new ArrayList<>();
         if(CollectionUtils.isNotEmpty(devIdList)){
             LambdaQueryWrapper<BaseAlarm> queryWrapper = Wrappers.lambdaQuery();
             queryWrapper.in(BaseAlarm::getDeviceId,devIdList)
@@ -227,65 +228,27 @@ public class BaseAlarmServiceImpl extends AbstractCrudService<BaseAlarmMapper, B
             }else {
                 queryWrapper.orderByDesc(BaseAlarm::getId);
             }
-            page = this.page(page,queryWrapper);
-//        List<BaseAlarm> records = this.list(queryWrapper);
-            if(page.getRecords().size()>0){
-                List<String> devList = new ArrayList<>();
-                for(int i=0;i<page.getRecords().size();i++){
-                    devList.add(page.getRecords().get(i).getDeviceId());
-                }
-
-                if(devList.size() > 0){
-                    LambdaQueryWrapper<BaseFacilityDevice> lambdaQuery1 = Wrappers.lambdaQuery();
-                    lambdaQuery1.in(BaseFacilityDevice::getDeviceId,devList);
-                    List<BaseFacilityDevice> records1 = baseFacilityDeviceService.list(lambdaQuery1);
-                    if(CollectionUtils.isNotEmpty(deviceInfo)){
-                        for(int i=0;i<page.getRecords().size();i++){
-                            for(int j=0;j<deviceInfo.size();j++){
-                                if(page.getRecords().get(i).getDeviceId().equals(deviceInfo.get(j).getDeviceId())){
-                                    page.getRecords().get(i).setDeviceName(deviceInfo.get(j).getDeviceName());
-                                    page.getRecords().get(i).setDeviceType(deviceInfo.get(j).getDeviceType());
-                                    break;
-                                }
-                            }
-                            for(int k=0;k<records1.size();k++){
-                                if(page.getRecords().get(i).getDeviceId().equals(records1.get(k).getDeviceId())){
-                                    page.getRecords().get(i).setFacilityId(records1.get(k).getFacilityId());
-                                    break;
-                                }
-                            }
-                        }
-                    }
-                }
-
-                List<BaseAlarmType> alarmTypeList = baseMapper.getAlarmTypeList();
-                if(CollectionUtils.isNotEmpty(alarmTypeList)){
-                    for(int i=0;i<page.getRecords().size();i++){
-                        for(int j=0;j<alarmTypeList.size();j++){
-                            if(page.getRecords().get(i).getAlarmType().equals(alarmTypeList.get(j).getTypeCode())){
-                                page.getRecords().get(i).setTypeImg(alarmTypeList.get(j).getTypeImg());
-                                break;
-                            }
-                        }
-                    }
-                }
+//            page = this.page(page,queryWrapper);
+            records = this.list(queryWrapper);
+            if(records.size() == 0){
+                return new CommonPage<>(new ArrayList<>(),0,0,0);
 
             }
         }
 
         if(StringUtils.isNotBlank(baseAlarmListVO.getDeviceName())){
-            list = page.getRecords().stream().filter(e -> baseAlarmListVO.getDeviceName().equals(e.getDeviceName())).collect(Collectors.toList());
+            list = records.stream().filter(e -> baseAlarmListVO.getDeviceName().equals(e.getDeviceName())).collect(Collectors.toList());
         }else {
-            if(page.getRecords().size()>0){
-                for(int k=0;k<page.getRecords().size();k++){
-                    list.add(page.getRecords().get(k));
+            if(records.size()>0){
+                for(int k=0;k<records.size();k++){
+                    list.add(records.get(k));
                 }
             }
 
         }
-        int total = 0;
+
         if(list.size()>0){
-            total = list.size();
+            page.setTotal(list.size());
         }
         int current = 0;
         Integer pageNum = baseAlarmListVO.getCurrent();
@@ -297,6 +260,46 @@ public class BaseAlarmServiceImpl extends AbstractCrudService<BaseAlarmMapper, B
         queryWrapper5.eq("tenant_id",SecurityUtils.getTenantId());
         List<DmpDeviceType> deviceTypeList = dmpDeviceTypeMapper.selectList(queryWrapper5);
         List<BaseAlarm> info = list.stream().sorted(Comparator.comparing(BaseAlarm::getId,Comparator.reverseOrder())).skip(current).limit(pageSize).collect(Collectors.toList());
+        List<String> devList = new ArrayList<>();
+        for(int i=0;i<info.size();i++){
+            devList.add(info.get(i).getDeviceId());
+        }
+
+        if(devList.size() > 0){
+            LambdaQueryWrapper<BaseFacilityDevice> lambdaQuery1 = Wrappers.lambdaQuery();
+            lambdaQuery1.in(BaseFacilityDevice::getDeviceId,devList);
+            List<BaseFacilityDevice> records1 = baseFacilityDeviceService.list(lambdaQuery1);
+            if(CollectionUtils.isNotEmpty(deviceInfo)){
+                for(int i=0;i<info.size();i++){
+                    for(int j=0;j<deviceInfo.size();j++){
+                        if(info.get(i).getDeviceId().equals(deviceInfo.get(j).getDeviceId())){
+                            info.get(i).setDeviceName(deviceInfo.get(j).getDeviceName());
+                            info.get(i).setDeviceType(deviceInfo.get(j).getDeviceType());
+                            break;
+                        }
+                    }
+                    for(int k=0;k<records1.size();k++){
+                        if(info.get(i).getDeviceId().equals(records1.get(k).getDeviceId())){
+                            info.get(i).setFacilityId(records1.get(k).getFacilityId());
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+
+        List<BaseAlarmType> alarmTypeList = baseMapper.getAlarmTypeList();
+        if(CollectionUtils.isNotEmpty(alarmTypeList)){
+            for(int i=0;i<info.size();i++){
+                for(int j=0;j<alarmTypeList.size();j++){
+                    if(info.get(i).getAlarmType().equals(alarmTypeList.get(j).getTypeCode())){
+                        info.get(i).setTypeImg(alarmTypeList.get(j).getTypeImg());
+                        break;
+                    }
+                }
+            }
+        }
+
         LocalDateTime toDateTime = LocalDateTime.now();
         for(int l=0;l<info.size();l++){
             LocalDateTime fromDateTime = info.get(l).getAlarmTime();
@@ -318,8 +321,10 @@ public class BaseAlarmServiceImpl extends AbstractCrudService<BaseAlarmMapper, B
                     info.get(l).setDeviceTypeName(deviceTypeList.get(m).getTypeName());
                 }
             }
+
         }
-        return new CommonPage<BaseAlarm>(info,total,pageSize,pageNum);
+        page.setRecords(info);
+        return new CommonPage<BaseAlarm>(page.getRecords(),page.getTotal(),pageSize,pageNum);
 
     }
 

+ 4 - 1
service-iot/service-iot-biz/src/main/java/com/usky/iot/service/impl/DmpDeviceInfoServiceImpl.java

@@ -548,7 +548,10 @@ public class DmpDeviceInfoServiceImpl extends AbstractCrudService<DmpDeviceInfoM
             dmpDeviceInfo.setCreatedTime(LocalDateTime.now());
             dmpDeviceInfo.setTenantId(SecurityUtils.getTenantId());
 
-            dmpDeviceInfo.setServiceStatus(1);
+            if(dmpDeviceInfo.getServiceStatus() == null) {
+                dmpDeviceInfo.setServiceStatus(1);
+            }
+
             if (StringUtils.isBlank(dmpDeviceInfo.getDeviceUuid())) {
                 dmpDeviceInfo.setDeviceUuid(UUIDUtils.uuid().substring(0, 16));
             }

+ 3 - 0
service-iot/service-iot-biz/src/main/java/com/usky/iot/service/impl/DmpProductInfoServiceImpl.java

@@ -29,6 +29,7 @@ import com.usky.iot.service.vo.DmpProductInfoRequest;
 import com.usky.transfer.RemoteTransferService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
 import java.math.BigDecimal;
 import java.math.RoundingMode;
@@ -70,6 +71,7 @@ public class DmpProductInfoServiceImpl extends AbstractCrudService<DmpProductInf
     private RemoteTsdbProxyService remoteTsdbProxyService;
 
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public void add(List<DmpProductInfo> dmpProductInfo) {
         if (CollectionUtils.isEmpty(dmpProductInfo)) {
             throw new BusinessException("数据不能为空");
@@ -218,6 +220,7 @@ public class DmpProductInfoServiceImpl extends AbstractCrudService<DmpProductInf
     }
 
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public void update(DmpProductInfo dmpProductInfo) {
         dmpProductInfo.setUpdatedTime(new Date());
         if (checkNameUnique(dmpProductInfo)){

+ 2 - 0
service-iot/service-iot-biz/src/main/java/com/usky/iot/service/impl/PmWorkReportServiceImpl.java

@@ -421,6 +421,7 @@ public class PmWorkReportServiceImpl extends AbstractCrudService<PmWorkReportMap
                     rp.setReportStatus(1);
                     pmWorkReportMapper.updateById(rp);
                     if (StringUtils.isNotBlank(ccTo)) {
+                        rp.setTenantId(tenantId);
                         dingTalkAndMessage.sendAsyncMessage(rp);
                         receiveMessages(ccTo, rid);
                     }
@@ -776,6 +777,7 @@ public class PmWorkReportServiceImpl extends AbstractCrudService<PmWorkReportMap
         jsonObject.put("infoTypeName", INFO_TITLE);
         jsonObject.put("userName", "报告提醒通知");
         jsonObject.put("userIds", userIds);
+        jsonObject.put("tenantId", tenantId);
 
         // 推送消息中心
         try {

+ 6 - 0
service-job/pom.xml

@@ -94,6 +94,12 @@
             <version>0.0.1</version>
             <scope>compile</scope>
         </dependency>
+        <dependency>
+            <groupId>com.usky</groupId>
+            <artifactId>service-vpp-api</artifactId>
+            <version>0.0.1</version>
+            <scope>compile</scope>
+        </dependency>
     </dependencies>
 
     <build>

+ 21 - 0
service-job/src/main/java/com/ruoyi/job/task/RyTask.java

@@ -4,8 +4,10 @@ import com.usky.cdi.AlarmDataSyncTaskService;
 import com.usky.cdi.RemotecdiTaskService;
 import com.usky.common.core.utils.StringUtils;
 import com.usky.eg.RemoteEgService;
+import com.usky.fire.RemotePlanSonService;
 import com.usky.meeting.RemoteMeetingService;
 import com.usky.ems.RemoteEmsTaskService;
+import com.usky.vpp.RemoteVppTaskService;
 import com.usky.fire.RemoteFireService;
 import com.usky.iot.RemoteIotTaskService;
 import com.usky.pm.RemotePmService;
@@ -43,6 +45,13 @@ public class RyTask {
     @Autowired
     private RemoteEgService remoteEgService;
 
+    @Autowired
+    private RemotePlanSonService remotePlanSonService;
+
+    @Autowired
+    private RemoteVppTaskService remoteVppTaskService;
+
+
     public void ryMultipleParams(String s, Boolean b, Long l, Double d, Integer i) {
         System.out.println(StringUtils.format("执行多参方法: 字符串类型{},布尔类型{},长整型{},浮点型{},整形{}", s, b, l, d, i));
     }
@@ -130,4 +139,16 @@ public class RyTask {
         remoteEgService.egDeviceStatus();
     }
 
+    // 创建巡检子计划
+    public void addPlanSon(){
+        System.out.println(Thread.currentThread().getName() + "巡检计划分批次创建执行中");
+        remotePlanSonService.addPlanSon();
+    }
+
+    // 刷新站点平均完成率
+    public void refreshSiteAvgCompletionRate() {
+        System.out.println("refreshSiteAvgCompletionRate start......");
+        remoteVppTaskService.refreshSiteAvgCompletionRate();
+	}
+
 }

+ 0 - 10
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/annotation/FaceLog.java

@@ -1,10 +0,0 @@
-package com.usky.meeting.annotation;
-
-import java.lang.annotation.*;
-
-@Documented
-@Retention(RetentionPolicy.RUNTIME)
-@Target(value = ElementType.METHOD)
-public @interface FaceLog {
-
-}

+ 0 - 50
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/annotation/aspect/FaceAspect.java

@@ -1,50 +0,0 @@
-package com.usky.meeting.annotation.aspect;
-
-
-
-import com.usky.meeting.domain.MeetingFaceVefLog;
-import com.usky.meeting.service.MeetingFaceVefLogService;
-import com.usky.meeting.service.vo.FaceResultVO;
-import com.usky.meeting.service.vo.MeetingFaceVO;
-import org.aspectj.lang.JoinPoint;
-import org.aspectj.lang.annotation.AfterReturning;
-import org.aspectj.lang.annotation.Aspect;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
-
-import java.time.LocalDateTime;
-import java.util.Objects;
-
-/**
- * @author tanyongpeng
- * <p>方法执行后,添加日志</p>
- **/
-@Component
-@Aspect
-public class FaceAspect {
-
-    @Autowired
-    private MeetingFaceVefLogService faceVefLogService;
-
-    @AfterReturning(value = "@annotation(com.usky.meeting.annotation.FaceLog)",returning = "res")
-    public void addLog(JoinPoint joinPoint ,Object res){
-        MeetingFaceVO result = (MeetingFaceVO)res;
-        MeetingFaceVefLog faceVefLog = new MeetingFaceVefLog();
-        faceVefLog.setVefTime(LocalDateTime.now());
-        faceVefLog.setVefCode(result.getCode());
-        faceVefLog.setVefMsg(result.getMsg());
-        faceVefLog.setLoginName(result.getFaceName());
-        faceVefLog.setTenantId(result.getTenantId());
-        if(Objects.nonNull(result.getFaceBase())){
-            faceVefLog.setFaceBase(result.getFaceBase());
-            faceVefLog.setVefType(1);
-        }
-        if(Objects.nonNull(result.getCardNum())){
-            faceVefLog.setCardNum(result.getCardNum());
-            faceVefLog.setVefType(2);
-        }
-        faceVefLogService.save(faceVefLog);
-    }
-
-
-}

+ 23 - 2
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/controller/web/MeetingDeviceController.java

@@ -7,13 +7,14 @@ import com.usky.common.log.annotation.Log;
 import com.usky.common.log.enums.BusinessType;
 import com.usky.meeting.domain.MeetingDevice;
 import com.usky.meeting.service.MeetingDeviceService;
+import com.usky.meeting.service.vo.MeetingDeviceBindFacePersonQueryVO;
+import com.usky.meeting.service.vo.MeetingDeviceBindFacePersonVO;
+import com.usky.meeting.service.vo.MeetingDeviceBindPersonRequest;
 import com.usky.meeting.service.vo.MeetingDeviceRequestVO;
 import lombok.RequiredArgsConstructor;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
-import org.springframework.stereotype.Controller;
-
 /**
  * <p>
  * 设备表 前端控制器
@@ -40,6 +41,15 @@ public class MeetingDeviceController {
         return ApiResult.success(meetingDeviceService.meetingDeviceList(requestVO));
     }
 
+    /**
+     * 分页查询设备已绑定人员(可通过 deviceCode 过滤)
+     */
+    @PostMapping("/bindFacePerson/page")
+    public ApiResult<CommonPage<MeetingDeviceBindFacePersonVO>> pageBindFacePersons(
+            @RequestBody MeetingDeviceBindFacePersonQueryVO queryVO) {
+        return ApiResult.success(meetingDeviceService.pageBindFacePersons(queryVO));
+    }
+
     /**
      * 新增
      * @param meetingDevice
@@ -76,5 +86,16 @@ public class MeetingDeviceController {
         return ApiResult.success();
     }
 
+    /**
+     * 设备人员授权(覆盖式)
+     * personIds 为空字符串时解除该设备全部授权
+     */
+    @Log(title = "会议设备人员授权", businessType = BusinessType.UPDATE)
+    @PostMapping("/bindPerson")
+    public ApiResult<Void> bindPerson(@RequestBody MeetingDeviceBindPersonRequest request) {
+        meetingDeviceService.bindPerson(request);
+        return ApiResult.success();
+    }
+
 }
 

+ 0 - 182
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/controller/web/MeetingFaceController.java

@@ -1,182 +0,0 @@
-package com.usky.meeting.controller.web;
-
-
-import cn.hutool.core.util.StrUtil;
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
-import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.baomidou.mybatisplus.core.metadata.IPage;
-import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
-import com.baomidou.mybatisplus.core.toolkit.StringUtils;
-import com.baomidou.mybatisplus.core.toolkit.Wrappers;
-import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import com.usky.common.core.bean.ApiResult;
-import com.usky.common.core.bean.CommonPage;
-import com.usky.common.core.exception.BusinessException;
-import com.usky.common.security.utils.SecurityUtils;
-import com.usky.meeting.annotation.FaceLog;
-import com.usky.meeting.domain.MeetingFace;
-import com.usky.meeting.domain.SysDept;
-import com.usky.meeting.mapper.MeetingFaceMapper;
-import com.usky.meeting.mapper.SysDeptMapper;
-import com.usky.meeting.repository.MeetingInfoRepository;
-import com.usky.meeting.service.MeetingFaceService;
-import com.usky.meeting.service.vo.DmpDeviceInfoVO;
-import com.usky.meeting.service.vo.FaceResultVO;
-import com.usky.meeting.service.vo.MeetingFaceRequestVO;
-import com.usky.meeting.service.vo.MeetingFaceVO;
-import io.swagger.annotations.Api;
-import io.swagger.annotations.ApiOperation;
-import io.swagger.models.auth.In;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Service;
-import org.springframework.web.bind.annotation.*;
-
-import org.springframework.stereotype.Controller;
-
-import java.time.LocalDateTime;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Objects;
-import java.util.stream.Collectors;
-
-/**
- * <p>
- *  前端控制器
- * </p>
- *
- * @author zyj
- * @since 2024-03-08
- */
-@RestController
-@RequestMapping("/meetingFace")
-@Api("人脸验证接口")
-@Slf4j
-public class MeetingFaceController {
-    @Autowired
-    MeetingFaceService faceService;
-    @Autowired
-    MeetingInfoRepository meetingInfoRepository;
-    @Autowired
-    private MeetingFaceMapper meetingFaceMapper;
-    @Autowired
-    private SysDeptMapper sysDeptMapper;
-
-    @PostMapping("/vef")
-    @ApiOperation(value="人脸验证", notes="根据传入的base64编码和数据的base64编码进行对比")
-    @FaceLog
-    public MeetingFaceVO faceVef(@RequestBody MeetingFaceRequestVO requestVO){
-        return faceService.vef(requestVO);
-    }
-
-    @GetMapping("/faceList")
-    @ApiOperation(value = "人脸列表",notes = "查询所有的人脸信息")
-    public ApiResult<CommonPage<MeetingFace>> faceList(
-            @RequestParam Integer current,
-            @RequestParam Integer size,
-            @RequestParam(required = false) String faceName,
-            @RequestParam(required = false) String faceStatus,
-            @RequestParam(required = false) Integer fid,
-            @RequestParam(required = false) Long userId,
-            @RequestParam(required = false) Integer deptId
-    ){
-        IPage<MeetingFace> page = faceService.page(new Page(current, size), new QueryWrapper<MeetingFace>()
-                .like(StrUtil.isNotBlank(faceName),"face_name",faceName)
-                .like(StrUtil.isNotBlank(faceStatus),"face_status",faceStatus)
-                .eq(fid != null,"fid",fid)
-                .eq(userId != null,"user_id",userId)
-                .eq(deptId != null,"dept_id",deptId)
-                .eq("tenant_id", SecurityUtils.getTenantId()));
-        if(page.getRecords().size() > 0){
-//            for (int i = 0; i < page.getRecords().size(); i++) {
-//                if(Objects.nonNull(page.getRecords().get(i).getBindDevice()) || StringUtils.isNotBlank(page.getRecords().get(i).getBindDevice())){
-//                    String[] deviceIdStr = page.getRecords().get(i).getBindDevice().split(",");
-//                    Integer[] deviceIds = Arrays.stream(deviceIdStr).map(Integer::parseInt).toArray(Integer[]::new);
-//                    List<DmpDeviceInfoVO> list = meetingFaceMapper.getDeviceInfo(Arrays.asList(deviceIds));
-//                    page.getRecords().get(i).setDeviceInfos(list);
-//
-//                }
-//            }
-
-            QueryWrapper<SysDept> queryWrapper = Wrappers.query();
-            queryWrapper.eq("del_flag",0).eq("tenant_id",SecurityUtils.getTenantId());
-            List<SysDept> sysDeptList = sysDeptMapper.selectList(queryWrapper);
-            if(sysDeptList.size() > 0){
-                for (int i = 0; i < page.getRecords().size(); i++) {
-                    Integer deptId1 = page.getRecords().get(i).getDeptId();
-                    for (int j = 0; j < sysDeptList.size(); j++) {
-                        if(deptId1.equals(Integer.valueOf(sysDeptList.get(j).getDeptId().toString()))){
-                            page.getRecords().get(i).setSysDept(sysDeptList.get(j));
-                            break;
-                        }
-                    }
-
-                }
-            }
-
-        }
-        return ApiResult.success(new CommonPage<>(page.getRecords(),page.getTotal(),size,current));
-    }
-
-
-    @GetMapping("/faceDelete/{fid}")
-    @ApiOperation(value = "删除人脸",notes = "根据id进行删除")
-    public FaceResultVO faceDelete(@PathVariable Integer fid){
-        faceService.removeById(fid);
-        meetingInfoRepository.delMeetingFaceDevice(fid);
-        return FaceResultVO.success("删除成功");
-    }
-
-    @GetMapping("/info/{fid}")
-    @ApiOperation(value = "查询方法",notes = "根据id查询人脸信息")
-    public FaceResultVO info(@PathVariable Integer fid){
-        return FaceResultVO.success(faceService.getById(fid));
-    }
-
-    @PostMapping("/save")
-    @ApiOperation(value = "添加")
-    public FaceResultVO save(@RequestBody MeetingFace face){
-        Long userId = face.getUserId();
-        LambdaQueryWrapper<MeetingFace> queryWrapper = Wrappers.lambdaQuery();
-        queryWrapper.eq(MeetingFace::getUserId,userId);
-        List<MeetingFace> list = faceService.list(queryWrapper);
-        if(CollectionUtils.isNotEmpty(list)){
-            throw new BusinessException("人员已绑定,请检查");
-        }
-
-        face.setVefNum(0);
-        face.setCreateTime(LocalDateTime.now());
-        face.setTenantId(SecurityUtils.getTenantId());
-        String[] devicefids = new String[0];
-        if(Objects.nonNull(face.getBindDevice()) || StringUtils.isNotBlank(face.getBindDevice())){
-            devicefids = face.getBindDevice().split(",");
-        }
-        faceService.save(face);
-        if(devicefids.length > 0){
-            for (int i = 0; i < devicefids.length; i++) {
-                meetingInfoRepository.insertMeetingFaceDevice(face.getFid(),Integer.parseInt(devicefids[i]));
-            }
-        }
-        return FaceResultVO.success("添加成功");
-    }
-
-    @PostMapping("/update")
-    @ApiOperation(value = "修改")
-    public FaceResultVO update(@RequestBody MeetingFace face){
-        faceService.updateById(face);
-        meetingInfoRepository.delMeetingFaceDevice(face.getFid());
-        String[] devicefids = new String[0];
-        if(Objects.nonNull(face.getBindDevice()) || StringUtils.isNotBlank(face.getBindDevice())){
-            devicefids = face.getBindDevice().split(",");
-        }
-        if(devicefids.length > 0){
-            for (int i = 0; i < devicefids.length; i++) {
-                meetingInfoRepository.insertMeetingFaceDevice(face.getFid(),Integer.parseInt(devicefids[i]));
-            }
-        }
-        return FaceResultVO.success("修改成功");
-    }
-}
-

+ 0 - 60
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/controller/web/MeetingFaceVefLogController.java

@@ -1,60 +0,0 @@
-package com.usky.meeting.controller.web;
-
-
-import cn.hutool.core.util.StrUtil;
-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.usky.common.core.bean.ApiResult;
-import com.usky.common.core.bean.CommonPage;
-import com.usky.meeting.domain.MeetingFaceVefLog;
-import com.usky.meeting.service.MeetingFaceVefLogService;
-import com.usky.meeting.service.vo.FaceResultVO;
-import io.swagger.annotations.Api;
-import io.swagger.annotations.ApiOperation;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-
-import org.springframework.stereotype.Controller;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.RestController;
-
-/**
- * <p>
- *  前端控制器
- * </p>
- *
- * @author zyj
- * @since 2024-03-08
- */
-@RestController
-@RequestMapping("/meetingFaceVefLog")
-@Api("人脸日志接口")
-@Slf4j
-public class MeetingFaceVefLogController {
-
-    @Autowired
-    MeetingFaceVefLogService faceVefLogService;
-
-    @GetMapping("/list")
-    @ApiOperation(value = "日志接口",notes = "查询所有验证人脸日志信息")
-    public ApiResult<CommonPage<MeetingFaceVefLog>> faceList(
-            @RequestParam Integer current,
-            @RequestParam Integer size,
-            @RequestParam(required = false) String startTime,
-            @RequestParam(required = false) String endTime,
-            @RequestParam(required = false) String vefCode,
-            @RequestParam(required = false) String loginName
-    ){
-        IPage<MeetingFaceVefLog> page = faceVefLogService.page(new Page(current, size), new QueryWrapper<MeetingFaceVefLog>()
-                .like(StrUtil.isNotBlank(vefCode),"vef_code",vefCode)
-                .like(StrUtil.isNotBlank(loginName),"login_name",loginName)
-                .between(StrUtil.isNotBlank(startTime) && StrUtil.isNotBlank(endTime),"vef_time",startTime,endTime)
-                .orderByDesc("vef_time"));
-        return ApiResult.success(new CommonPage<>(page.getRecords(),page.getTotal(),size,current));
-    }
-
-}
-

+ 6 - 0
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/domain/MeetingDevice.java

@@ -166,4 +166,10 @@ public class MeetingDevice implements Serializable {
      * 设备方向 1横屏 2竖屏
      */
     private Integer deviceDirection;
+
+    /**
+     * 绑定人员ID(逗号分隔,来自 meeting_device_person_bind)
+     */
+    @TableField(exist = false)
+    private String bindPerson;
 }

+ 25 - 0
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/domain/MeetingDevicePersonBind.java

@@ -0,0 +1,25 @@
+package com.usky.meeting.domain;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+
+/**
+ * 会议设备人员绑定表 meeting_device_person_bind
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+public class MeetingDevicePersonBind implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 设备主键ID(meeting_device.device_uuid) */
+    private Long deviceUuid;
+
+    /** 人员ID */
+    private Integer personId;
+
+    /** 是否打开登录通知(1 是 0 否 默认0) */
+    private Integer isLoginNotify;
+}

+ 0 - 75
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/domain/MeetingFaceVefLog.java

@@ -1,75 +0,0 @@
-package com.usky.meeting.domain;
-
-import com.baomidou.mybatisplus.annotation.IdType;
-import com.baomidou.mybatisplus.annotation.TableId;
-import java.time.LocalDateTime;
-import java.io.Serializable;
-import lombok.Data;
-import lombok.EqualsAndHashCode;
-
-/**
- * <p>
- * 
- * </p>
- *
- * @author zyj
- * @since 2024-03-08
- */
-@Data
-@EqualsAndHashCode(callSuper = false)
-public class MeetingFaceVefLog implements Serializable {
-
-    private static final long serialVersionUID = 1L;
-
-    /**
-     * 主键
-     */
-    @TableId(value = "lid", type = IdType.AUTO)
-    private Integer lid;
-
-    /**
-     * 验证时间
-     */
-    private LocalDateTime vefTime;
-
-    /**
-     * 返回code
-     */
-    private Integer vefCode;
-
-    /**
-     * 返回的消息
-     */
-    private String vefMsg;
-
-    /**
-     * 验证人
-     */
-    private String loginName;
-
-    /**
-     * 组织机构ID
-     */
-    private Integer deptId;
-
-    /**
-     * 租户ID
-     */
-    private Integer tenantId;
-
-    /**
-     * 图片数据 base_64编码
-     */
-    private String faceBase;
-
-    /**
-     * 卡号
-     */
-    private String cardNum;
-
-    /**
-     * 验证类型(1、人脸 2、卡号)
-     */
-    private Integer vefType;
-
-}

+ 48 - 0
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/domain/SysPerson.java

@@ -0,0 +1,48 @@
+package com.usky.meeting.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 人员信息表 sys_person
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("sys_person")
+public class SysPerson implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    private String fullName;
+    private Integer age;
+    private Integer gender;
+    private String address;
+    private Integer educationDegree;
+    private String idNumber;
+    private String linkPhone;
+    private Long postId;
+    private Long deptId;
+    private LocalDateTime entryTime;
+    private String certificateUrl1;
+    private String certificateUrl2;
+    private String certificateUrl3;
+    private String creator;
+    private LocalDateTime createTime;
+    private String updatePerson;
+    private LocalDateTime updateTime;
+    private String faceBase;
+    private Integer vefNum;
+    private String faceName;
+    private String remark;
+    private Byte faceStatus;
+    private String cardNum;
+}

+ 29 - 0
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/mapper/MeetingDevicePersonBindMapper.java

@@ -0,0 +1,29 @@
+package com.usky.meeting.mapper;
+
+import com.usky.meeting.domain.MeetingDevicePersonBind;
+import org.apache.ibatis.annotations.Param;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+/**
+ * 会议设备人员绑定 Mapper
+ */
+@Repository
+public interface MeetingDevicePersonBindMapper {
+
+    /**
+     * 根据设备UUID删除绑定关系
+     */
+    void deleteByDeviceUuid(@Param("deviceUuid") Long deviceUuid);
+
+    /**
+     * 根据多个设备UUID查询绑定关系
+     */
+    List<MeetingDevicePersonBind> selectByDeviceUuids(@Param("deviceUuids") List<Long> deviceUuids);
+
+    /**
+     * 新增绑定关系
+     */
+    void insert(MeetingDevicePersonBind bind);
+}

+ 0 - 6
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/mapper/MeetingFaceMapper.java

@@ -2,13 +2,8 @@ package com.usky.meeting.mapper;
 
 import com.usky.meeting.domain.MeetingFace;
 import com.usky.common.mybatis.core.CrudMapper;
-import com.usky.meeting.service.vo.DmpDeviceInfoVO;
-import io.swagger.models.auth.In;
-import org.apache.ibatis.annotations.Param;
 import org.springframework.stereotype.Repository;
 
-import java.util.List;
-
 /**
  * <p>
  *  Mapper 接口
@@ -19,5 +14,4 @@ import java.util.List;
  */
 @Repository
 public interface MeetingFaceMapper extends CrudMapper<MeetingFace> {
-    List<DmpDeviceInfoVO> getDeviceInfo(@Param("deviceIdList") List<Integer> deviceIdList);
 }

+ 0 - 16
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/mapper/MeetingFaceVefLogMapper.java

@@ -1,16 +0,0 @@
-package com.usky.meeting.mapper;
-
-import com.usky.meeting.domain.MeetingFaceVefLog;
-import com.usky.common.mybatis.core.CrudMapper;
-
-/**
- * <p>
- *  Mapper 接口
- * </p>
- *
- * @author zyj
- * @since 2024-03-08
- */
-public interface MeetingFaceVefLogMapper extends CrudMapper<MeetingFaceVefLog> {
-
-}

+ 21 - 0
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/mapper/SysPersonMapper.java

@@ -0,0 +1,21 @@
+package com.usky.meeting.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.meeting.domain.SysPerson;
+import org.apache.ibatis.annotations.Param;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 人员信息表 Mapper
+ */
+@Repository
+public interface SysPersonMapper extends CrudMapper<SysPerson> {
+
+    /**
+     * 按人员ID批量查询关联用户ID(sys_user_person)
+     */
+    List<Map<String, Object>> selectUserIdsByPersonIds(@Param("personIds") List<Integer> personIds);
+}

+ 0 - 19
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/repository/MeetingInfoRepository.java

@@ -306,25 +306,6 @@ public interface MeetingInfoRepository extends JpaRepository<MeetingInfo, Long>,
     @Query(value = "select * from meeting_info where room_id = :roomId",nativeQuery = true)
     List<MeetingInfo> queryAllMeetingByRoomId(@Param("roomId") Long roomId);
 
-    /**
-     * 新增人脸设备关联表记录
-     * @param faceId  人脸主键ID
-     * @param deviceId  设备主键ID
-     */
-    @Transactional
-    @Modifying
-    @Query(value = "insert into meeting_face_device (face_id,device_id) values (:faceId,:deviceId)",nativeQuery = true)
-    void insertMeetingFaceDevice(@Param("faceId") Integer faceId, @Param("deviceId") Integer deviceId);
-
-    /**
-     * 删除人脸设备关联表记录
-     * @param faceId
-     */
-    @Transactional
-    @Modifying
-    @Query(value = "delete from meeting_face_device where face_id = :faceId",nativeQuery = true)
-    void delMeetingFaceDevice(@Param("faceId") Integer faceId);
-
 //
 //
 //    /**

+ 12 - 1
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/MeetingDeviceService.java

@@ -3,8 +3,10 @@ package com.usky.meeting.service;
 import com.usky.common.core.bean.CommonPage;
 import com.usky.meeting.domain.MeetingDevice;
 import com.usky.common.mybatis.core.CrudService;
+import com.usky.meeting.service.vo.MeetingDeviceBindFacePersonQueryVO;
+import com.usky.meeting.service.vo.MeetingDeviceBindFacePersonVO;
+import com.usky.meeting.service.vo.MeetingDeviceBindPersonRequest;
 import com.usky.meeting.service.vo.MeetingDeviceRequestVO;
-import org.springframework.web.bind.annotation.RequestBody;
 
 /**
  * <p>
@@ -24,4 +26,13 @@ public interface MeetingDeviceService extends CrudService<MeetingDevice> {
 
     void remove(Long deviceId);
 
+    /**
+     * 设备人员授权(覆盖式):先清空该设备绑定,再按 personIds 重新绑定;空字符串表示解除全部授权
+     */
+    void bindPerson(MeetingDeviceBindPersonRequest request);
+
+    /**
+     * 分页查询设备已绑定人员(支持 deviceCode / 姓名 / 人脸状态)
+     */
+    CommonPage<MeetingDeviceBindFacePersonVO> pageBindFacePersons(MeetingDeviceBindFacePersonQueryVO queryVO);
 }

+ 0 - 16
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/MeetingFaceVefLogService.java

@@ -1,16 +0,0 @@
-package com.usky.meeting.service;
-
-import com.usky.meeting.domain.MeetingFaceVefLog;
-import com.usky.common.mybatis.core.CrudService;
-
-/**
- * <p>
- *  服务类
- * </p>
- *
- * @author zyj
- * @since 2024-03-08
- */
-public interface MeetingFaceVefLogService extends CrudService<MeetingFaceVefLog> {
-
-}

+ 192 - 0
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/impl/MeetingDeviceServiceImpl.java

@@ -19,6 +19,9 @@ import com.usky.meeting.repository.MeetingRoomRepository;
 import com.usky.meeting.service.MeetingDeviceService;
 import com.usky.common.mybatis.core.AbstractCrudService;
 import com.usky.meeting.service.MeetingRoomService;
+import com.usky.meeting.service.vo.MeetingDeviceBindFacePersonQueryVO;
+import com.usky.meeting.service.vo.MeetingDeviceBindFacePersonVO;
+import com.usky.meeting.service.vo.MeetingDeviceBindPersonRequest;
 import com.usky.meeting.service.vo.MeetingDeviceRequestVO;
 import net.bytebuddy.implementation.bytecode.Throw;
 import org.aspectj.weaver.patterns.ThisOrTargetAnnotationPointcut;
@@ -56,6 +59,10 @@ public class MeetingDeviceServiceImpl extends AbstractCrudService<MeetingDeviceM
     private MeetingDeviceHeartbeatMapper heartbeatMapper;
     @Autowired
     private MeetingTemplateMapper meetingTemplateMapper;
+    @Autowired
+    private MeetingDevicePersonBindMapper meetingDevicePersonBindMapper;
+    @Autowired
+    private SysPersonMapper sysPersonMapper;
 
     @Override
     public CommonPage<MeetingDevice> meetingDeviceList(MeetingDeviceRequestVO requestVO) {
@@ -174,11 +181,54 @@ public class MeetingDeviceServiceImpl extends AbstractCrudService<MeetingDeviceM
                     MeetingDevice::getTemplateId,
                     buildIdMap(meetingTemplateList, MeetingTemplate::getId),
                     MeetingDevice::setMeetingTemplate);
+
+            // 5. 填充绑定人员
+            fillBindPerson(page.getRecords());
         }
 
         return new CommonPage<>(page.getRecords(), page.getTotal(), size, current);
     }
 
+    /**
+     * 根据设备UUID集合查询 meeting_device_person_bind,将 person_id 按逗号拼接后写入 bindPerson
+     */
+    private void fillBindPerson(List<MeetingDevice> devices) {
+        if (CollectionUtils.isEmpty(devices)) {
+            return;
+        }
+        List<Long> deviceUuids = devices.stream()
+                .map(MeetingDevice::getDeviceUuid)
+                .filter(Objects::nonNull)
+                .distinct()
+                .collect(Collectors.toList());
+        if (deviceUuids.isEmpty()) {
+            return;
+        }
+        List<MeetingDevicePersonBind> bindList = meetingDevicePersonBindMapper.selectByDeviceUuids(deviceUuids);
+        Map<Long, List<String>> devicePersonMap = new HashMap<>();
+        if (!CollectionUtils.isEmpty(bindList)) {
+            for (MeetingDevicePersonBind bind : bindList) {
+                if (bind.getDeviceUuid() == null || bind.getPersonId() == null) {
+                    continue;
+                }
+                devicePersonMap
+                        .computeIfAbsent(bind.getDeviceUuid(), k -> new ArrayList<>())
+                        .add(String.valueOf(bind.getPersonId()));
+            }
+        }
+        for (MeetingDevice device : devices) {
+            if (device == null || device.getDeviceUuid() == null) {
+                continue;
+            }
+            List<String> personIds = devicePersonMap.get(device.getDeviceUuid());
+            if (personIds != null && !personIds.isEmpty()) {
+                device.setBindPerson(String.join(",", personIds));
+            } else {
+                device.setBindPerson("");
+            }
+        }
+    }
+
     /**
      * 构建ID到对象的Map,用于快速查找
      * @param dataList 待处理的列表
@@ -286,6 +336,148 @@ public class MeetingDeviceServiceImpl extends AbstractCrudService<MeetingDeviceM
         // remoteIotTaskService.deleteDeviceInfo(deviceId.toString());
     }
 
+    @Override
+    public void bindPerson(MeetingDeviceBindPersonRequest request) {
+        if (request == null || request.getDeviceUuid() == null) {
+            throw new BusinessException("设备UUID不能为空");
+        }
+        Long deviceUuid = request.getDeviceUuid();
+        MeetingDevice device = this.getById(deviceUuid);
+        Optional.ofNullable(device).orElseThrow(() -> new BusinessException("会议设备信息不存在"));
+
+        // 覆盖式:先清空原有绑定
+        meetingDevicePersonBindMapper.deleteByDeviceUuid(deviceUuid);
+
+        String personIds = request.getPersonIds();
+        // 空字符串视为解除全部授权
+        if (!StringUtils.isNotBlank(personIds)) {
+            return;
+        }
+
+        String[] personIdArr = personIds.split(",");
+        for (String personIdStr : personIdArr) {
+            if (!StringUtils.isNotBlank(personIdStr)) {
+                continue;
+            }
+            MeetingDevicePersonBind bind = new MeetingDevicePersonBind();
+            bind.setDeviceUuid(deviceUuid);
+            bind.setPersonId(Integer.parseInt(personIdStr.trim()));
+            bind.setIsLoginNotify(0);
+            meetingDevicePersonBindMapper.insert(bind);
+        }
+    }
+
+    @Override
+    public CommonPage<MeetingDeviceBindFacePersonVO> pageBindFacePersons(MeetingDeviceBindFacePersonQueryVO queryVO) {
+        Integer cur = queryVO.getCurrent();
+        Integer sz = queryVO.getSize();
+        IPage<SysPerson> page = new Page<>(cur, sz);
+
+        List<Long> deviceUuids = new ArrayList<>();
+        if (StringUtils.isNotBlank(queryVO.getDeviceCode())) {
+            MeetingDevice tempDevice = deviceMapper.selectOne(Wrappers.<MeetingDevice>lambdaQuery()
+                    .eq(MeetingDevice::getDeviceCode, queryVO.getDeviceCode()));
+            if (tempDevice == null) {
+                return new CommonPage<>(new ArrayList<>(), 0L, sz, cur);
+            }
+            MeetingDevice device = deviceMapper.selectOne(Wrappers.<MeetingDevice>lambdaQuery()
+                    .eq(MeetingDevice::getDeviceCode, queryVO.getDeviceCode())
+                    .eq(MeetingDevice::getTenantId, tempDevice.getTenantId()));
+            if (device == null || device.getDeviceUuid() == null) {
+                return new CommonPage<>(new ArrayList<>(), 0L, sz, cur);
+            }
+            deviceUuids.add(device.getDeviceUuid());
+        } else {
+            List<MeetingDevice> devices = deviceMapper.selectList(Wrappers.<MeetingDevice>lambdaQuery()
+                    .eq(MeetingDevice::getTenantId, SecurityUtils.getTenantId()));
+            if (!CollectionUtils.isEmpty(devices)) {
+                for (MeetingDevice d : devices) {
+                    if (d != null && d.getDeviceUuid() != null) {
+                        deviceUuids.add(d.getDeviceUuid());
+                    }
+                }
+            }
+        }
+
+        if (deviceUuids.isEmpty()) {
+            return new CommonPage<>(new ArrayList<>(), 0L, sz, cur);
+        }
+
+        List<MeetingDevicePersonBind> bindList = meetingDevicePersonBindMapper.selectByDeviceUuids(deviceUuids);
+        LinkedHashSet<Integer> personIdSet = new LinkedHashSet<>();
+        if (!CollectionUtils.isEmpty(bindList)) {
+            for (MeetingDevicePersonBind b : bindList) {
+                if (b.getPersonId() != null) {
+                    personIdSet.add(b.getPersonId());
+                }
+            }
+        }
+        if (personIdSet.isEmpty()) {
+            return new CommonPage<>(new ArrayList<>(), 0L, sz, cur);
+        }
+        List<Integer> personIds = new ArrayList<>(personIdSet);
+
+        LambdaQueryWrapper<SysPerson> personWrapper = Wrappers.lambdaQuery();
+        personWrapper.in(SysPerson::getId, personIds);
+        if (StringUtils.isNotBlank(queryVO.getFullName())) {
+            personWrapper.like(SysPerson::getFullName, queryVO.getFullName());
+        }
+        Integer faceStatusFilter = queryVO.getFaceStatus();
+        if (faceStatusFilter != null) {
+            personWrapper.eq(SysPerson::getFaceStatus, faceStatusFilter.byteValue());
+        }
+        personWrapper.orderByAsc(SysPerson::getId);
+        page = sysPersonMapper.selectPage(page, personWrapper);
+
+        List<SysPerson> records = page.getRecords();
+        List<Integer> pagePersonIds = new ArrayList<>();
+        for (SysPerson p : records) {
+            if (p != null && p.getId() != null) {
+                pagePersonIds.add(p.getId());
+            }
+        }
+        Map<Integer, Long> personUserIdMap = buildPersonUserIdMap(pagePersonIds);
+        List<MeetingDeviceBindFacePersonVO> rows = new ArrayList<>(records.size());
+        for (SysPerson p : records) {
+            MeetingDeviceBindFacePersonVO vo = new MeetingDeviceBindFacePersonVO();
+            vo.setId(p.getId());
+            vo.setUserId(personUserIdMap.get(p.getId()));
+            vo.setFullName(p.getFullName());
+            vo.setAge(p.getAge());
+            vo.setGender(p.getGender());
+            vo.setLinkPhone(p.getLinkPhone());
+            vo.setFaceBase(p.getFaceBase());
+            vo.setVefNum(p.getVefNum());
+            vo.setFaceStatus(p.getFaceStatus());
+            vo.setCardNum(p.getCardNum());
+            rows.add(vo);
+        }
+        return new CommonPage<>(rows, page.getTotal(), sz, cur);
+    }
+
+    private Map<Integer, Long> buildPersonUserIdMap(List<Integer> personIds) {
+        Map<Integer, Long> map = new HashMap<>();
+        if (CollectionUtils.isEmpty(personIds)) {
+            return map;
+        }
+        List<Map<String, Object>> rows = sysPersonMapper.selectUserIdsByPersonIds(personIds);
+        if (CollectionUtils.isEmpty(rows)) {
+            return map;
+        }
+        for (Map<String, Object> row : rows) {
+            if (row == null) {
+                continue;
+            }
+            Object personIdObj = row.get("personId");
+            Object userIdObj = row.get("userId");
+            if (personIdObj == null || userIdObj == null) {
+                continue;
+            }
+            map.put(Integer.valueOf(personIdObj.toString()), Long.valueOf(userIdObj.toString()));
+        }
+        return map;
+    }
+
     public boolean checkNameUnique(MeetingDevice meetingDevice) {
         Long deviceId = null == meetingDevice.getDeviceUuid() ? -1 : meetingDevice.getDeviceUuid();
         LambdaQueryWrapper<MeetingDevice> queryWrapper = Wrappers.lambdaQuery();

+ 0 - 20
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/impl/MeetingFaceVefLogServiceImpl.java

@@ -1,20 +0,0 @@
-package com.usky.meeting.service.impl;
-
-import com.usky.meeting.domain.MeetingFaceVefLog;
-import com.usky.meeting.mapper.MeetingFaceVefLogMapper;
-import com.usky.meeting.service.MeetingFaceVefLogService;
-import com.usky.common.mybatis.core.AbstractCrudService;
-import org.springframework.stereotype.Service;
-
-/**
- * <p>
- *  服务实现类
- * </p>
- *
- * @author zyj
- * @since 2024-03-08
- */
-@Service
-public class MeetingFaceVefLogServiceImpl extends AbstractCrudService<MeetingFaceVefLogMapper, MeetingFaceVefLog> implements MeetingFaceVefLogService {
-
-}

+ 0 - 1
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/impl/MeetingFileServiceImpl.java

@@ -6,7 +6,6 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.usky.common.core.bean.CommonPage;
 import com.usky.common.security.utils.SecurityUtils;
-import com.usky.meeting.domain.MeetingFace;
 import com.usky.meeting.domain.MeetingFile;
 import com.usky.meeting.mapper.MeetingFileMapper;
 import com.usky.meeting.service.MeetingFileService;

+ 23 - 0
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/vo/MeetingDeviceBindFacePersonQueryVO.java

@@ -0,0 +1,23 @@
+package com.usky.meeting.service.vo;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 会议设备绑定人员分页查询参数
+ */
+@Data
+public class MeetingDeviceBindFacePersonQueryVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private Integer current;
+    private Integer size;
+    /** 设备编码,空则当前租户全部设备 */
+    private String deviceCode;
+    /** 姓名模糊,可空 */
+    private String fullName;
+    /** 人脸状态 0/1,可空 */
+    private Integer faceStatus;
+}

+ 27 - 0
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/vo/MeetingDeviceBindFacePersonVO.java

@@ -0,0 +1,27 @@
+package com.usky.meeting.service.vo;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 会议设备绑定人员列表项
+ */
+@Data
+public class MeetingDeviceBindFacePersonVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 人员主键ID(sys_person.id) */
+    private Integer id;
+    /** 用户ID(sys_user_person.user_id) */
+    private Long userId;
+    private String fullName;
+    private Integer age;
+    private Integer gender;
+    private String linkPhone;
+    private String faceBase;
+    private Integer vefNum;
+    private Byte faceStatus;
+    private String cardNum;
+}

+ 18 - 0
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/service/vo/MeetingDeviceBindPersonRequest.java

@@ -0,0 +1,18 @@
+package com.usky.meeting.service.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+/**
+ * 会议设备人员授权请求
+ */
+@Data
+public class MeetingDeviceBindPersonRequest {
+
+    /** 会议设备 UUID(meeting_device.device_uuid) */
+    @JsonFormat(shape = JsonFormat.Shape.STRING)
+    private Long deviceUuid;
+
+    /** 人员 ID,逗号分隔;空字符串表示清空授权 */
+    private String personIds;
+}

+ 34 - 0
service-meeting/service-meeting-biz/src/main/resources/mapper/meeting/MeetingDevicePersonBindMapper.xml

@@ -0,0 +1,34 @@
+<?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.meeting.mapper.MeetingDevicePersonBindMapper">
+
+    <resultMap id="BaseResultMap" type="com.usky.meeting.domain.MeetingDevicePersonBind">
+        <result column="device_uuid" property="deviceUuid"/>
+        <result column="person_id" property="personId"/>
+        <result column="is_login_notify" property="isLoginNotify"/>
+    </resultMap>
+
+    <delete id="deleteByDeviceUuid">
+        delete from meeting_device_person_bind
+        where device_uuid = #{deviceUuid}
+    </delete>
+
+    <select id="selectByDeviceUuids" resultMap="BaseResultMap">
+        select device_uuid, person_id, is_login_notify
+        from meeting_device_person_bind
+        <where>
+            <if test="deviceUuids != null and deviceUuids.size() > 0">
+                and device_uuid in
+                <foreach collection="deviceUuids" item="id" open="(" separator="," close=")">
+                    #{id}
+                </foreach>
+            </if>
+        </where>
+    </select>
+
+    <insert id="insert" parameterType="com.usky.meeting.domain.MeetingDevicePersonBind">
+        insert into meeting_device_person_bind (device_uuid, person_id, is_login_notify)
+        values (#{deviceUuid}, #{personId}, #{isLoginNotify})
+    </insert>
+
+</mapper>

+ 0 - 15
service-meeting/service-meeting-biz/src/main/resources/mapper/meeting/MeetingFaceMapper.xml

@@ -17,20 +17,5 @@
         <result column="tenant_id" property="tenantId" />
         <result column="user_id" property="userId" />
     </resultMap>
-    <select id="getDeviceInfo" resultType="com.usky.meeting.service.vo.DmpDeviceInfoVO">
-        SELECT dd.id,dd.device_id,dd.device_name,dd.product_id,dd.tenant_id,dd.company_code,dd.install_address,dd.service_status,dd.product_code,dd.device_uuid
-            FROM dmp_device dd
-        <where>
-            <if test="deviceIdList != null and deviceIdList.size() > 0">
-                AND dd.id in
-                <foreach item="item" collection="deviceIdList" open="(" separator="," close=")">
-                    #{item}
-                </foreach>
-            </if>
-            <if test="1 == 1">
-                and dd.delete_flag = 0
-            </if>
-        </where>
-    </select>
 
 </mapper>

+ 0 - 16
service-meeting/service-meeting-biz/src/main/resources/mapper/meeting/MeetingFaceVefLogMapper.xml

@@ -1,16 +0,0 @@
-<?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.meeting.mapper.MeetingFaceVefLogMapper">
-
-    <!-- 通用查询映射结果 -->
-    <resultMap id="BaseResultMap" type="com.usky.meeting.domain.MeetingFaceVefLog">
-        <id column="lid" property="lid" />
-        <result column="vef_time" property="vefTime" />
-        <result column="vef_code" property="vefCode" />
-        <result column="vef_msg" property="vefMsg" />
-        <result column="login_name" property="loginName" />
-        <result column="dept_id" property="deptId" />
-        <result column="tenant_id" property="tenantId" />
-    </resultMap>
-
-</mapper>

+ 18 - 0
service-meeting/service-meeting-biz/src/main/resources/mapper/meeting/SysPersonMapper.xml

@@ -0,0 +1,18 @@
+<?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.meeting.mapper.SysPersonMapper">
+
+    <select id="selectUserIdsByPersonIds" resultType="java.util.HashMap">
+        select person_id as personId, user_id as userId
+        from sys_user_person
+        <where>
+            <if test="personIds != null and personIds.size() > 0">
+                and person_id in
+                <foreach collection="personIds" item="id" open="(" separator="," close=")">
+                    #{id}
+                </foreach>
+            </if>
+        </where>
+    </select>
+
+</mapper>

+ 3 - 0
service-oa/service-oa-biz/src/main/java/com/usky/oa/service/impl/OaApprovalServiceImpl.java

@@ -238,6 +238,7 @@ public class OaApprovalServiceImpl extends AbstractCrudService<OaApprovalMapper,
 
                             // 发送消息通知下一步审批人
                             oaSendMessageCenter.sendAsyncMessage(
+                                    tenantId,
                                     oaApproval.getCreateBy(),
                                     oaApproval.getApprovalUid(),
                                     approval.getId(),
@@ -253,6 +254,7 @@ public class OaApprovalServiceImpl extends AbstractCrudService<OaApprovalMapper,
             else {
                 Long proposerId = Long.valueOf(oaApproval.getProposer());
                 oaSendMessageCenter.sendAsyncMessage(
+                        tenantId,
                         oaApproval.getCreateBy(),
                         oaApproval.getApprovalUid(),
                         oaApproval.getId(),
@@ -265,6 +267,7 @@ public class OaApprovalServiceImpl extends AbstractCrudService<OaApprovalMapper,
             Long proposerId = Long.valueOf(oaApproval.getProposer());
             // 发送消息通知申请人审核不通过
             oaSendMessageCenter.sendAsyncMessage(
+                    tenantId,
                     oaApproval.getCreateBy(),
                     oaApproval.getApprovalUid(),
                     oaApproval.getId(),

+ 1 - 0
service-oa/service-oa-biz/src/main/java/com/usky/oa/service/impl/OaFbdDocumentServiceImpl.java

@@ -179,6 +179,7 @@ public class OaFbdDocumentServiceImpl extends AbstractCrudService<OaFbdDocumentM
             oaApprovalMapper.insert(approval);
 
             oaSendMessageCenter.sendAsyncMessage(
+                    oaFbdDocument.getTenantId(),
                     oaFbdDocument.getCreateBy(),
                     approval.getApprovalUid(),
                     oaFbdDocument.getId(),

+ 2 - 1
service-oa/service-oa-biz/src/main/java/com/usky/oa/service/utils/OaSendMessageCenter.java

@@ -57,7 +57,7 @@ public class OaSendMessageCenter {
      * @param sign 申请单类型
      */
     @Async
-    public void sendAsyncMessage(String username, Long approvalUid, Integer id, List<Long> receivers, Integer isPass, String sign) {
+    public void sendAsyncMessage(Integer tenantId, String username, Long approvalUid, Integer id, List<Long> receivers, Integer isPass, String sign) {
 
         log.info(username + "的申请开始发送消息中心-----------------------------------");
 
@@ -99,6 +99,7 @@ public class OaSendMessageCenter {
         mceRequestVO.setInfoTypeName(INFO_TITLE);
         mceRequestVO.setUserName(username);
         mceRequestVO.setUserIds(receivers);
+        mceRequestVO.setTenantId(tenantId);
         try {
             // 推送消息中心
             //ApiResult<Void> voidApiResult = remoteMceService.addMce(jsonObject.toString());

+ 1 - 0
service-pm/service-pm-biz/src/main/java/com/usky/pm/service/config/DingTalkAndMessage.java

@@ -330,6 +330,7 @@ public class DingTalkAndMessage {
         mceRequestVO.setInfoTypeName(INFO_TITLE);
         mceRequestVO.setUserName(username);
         mceRequestVO.setRealName(nickName.getNickName());
+        mceRequestVO.setTenantId(newReport.getTenantId());
 
         if (!userId.isEmpty()) {
             mceRequestVO.setUserIds(userId);

+ 2 - 0
service-pm/service-pm-biz/src/main/java/com/usky/pm/service/impl/PmWorkReportServiceImpl.java

@@ -443,6 +443,7 @@ public class PmWorkReportServiceImpl extends AbstractCrudService<PmWorkReportMap
                         if (pmWorkReport.getSendDingTalk().equals(1)) {
                             dingTalkAndMessage.sendDingTalkDailyReport(rp, contents);
                         }
+                        rp.setTenantId(tenantId);
                         dingTalkAndMessage.sendAsyncMessage(rp);
                         receiveMessages(ccTo, rid);
                     }
@@ -808,6 +809,7 @@ public class PmWorkReportServiceImpl extends AbstractCrudService<PmWorkReportMap
         mceRequestVO.setId(0);
         mceRequestVO.setInfoTypeName(INFO_TITLE);
         mceRequestVO.setUserName("报告管理系统");
+        mceRequestVO.setTenantId(tenantId);
         if (!userIds.isEmpty()) {
             mceRequestVO.setUserIds(userIds);
         }

+ 9 - 0
service-tsdb/service-tsdb-api/pom.xml

@@ -20,6 +20,15 @@
             <groupId>com.usky</groupId>
             <artifactId>usky-common-core</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.github.openfeign.form</groupId>
+            <artifactId>feign-form-spring</artifactId>
+            <version>3.8.0</version>
+        </dependency>
     </dependencies>
 
     <build>

+ 9 - 0
service-tsdb/service-tsdb-api/src/main/java/com/usky/demo/RemoteTsdbProxyService.java

@@ -6,7 +6,9 @@ import com.usky.demo.domain.*;
 import com.usky.demo.factory.RemoteTsdbProxyFallbackFactory;
 import com.usky.demo.feign.RemoteTsdbProxyFeignConfiguration;
 import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.http.MediaType;
 import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
 
 import java.util.List;
 import java.util.Map;
@@ -105,4 +107,11 @@ public interface RemoteTsdbProxyService {
      */
     @PostMapping("/energyItemTrend")
     EnergyItemTrendResultVO sumEnergyItemTrend(@RequestBody EnergyItemTrendQueryVO requestVO);
+
+    /**
+     * 解析 CSV 并导入设备时序数据:INSERT INTO _{deviceUuid} (ts, p) VALUES ...
+     */
+    @PostMapping(value = "/importDeviceCsv", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    ApiResult<DeviceCsvImportResultVO> importDeviceCsv(@RequestPart("deviceUuid") String deviceUuid,
+                                                       @RequestPart("file") MultipartFile file);
 }

+ 23 - 0
service-tsdb/service-tsdb-api/src/main/java/com/usky/demo/domain/DeviceCsvImportResultVO.java

@@ -0,0 +1,23 @@
+package com.usky.demo.domain;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * TDengine CSV 导入结果
+ */
+@Data
+public class DeviceCsvImportResultVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 设备 UUID(入参) */
+    private String deviceUuid;
+
+    /** 实际写入的子表名(通常为 _{deviceUuid}) */
+    private String tableName;
+
+    /** 成功导入的数据行数 */
+    private Integer importedRows;
+}

+ 7 - 0
service-tsdb/service-tsdb-api/src/main/java/com/usky/demo/factory/RemoteTsdbProxyFallbackFactory.java

@@ -104,6 +104,13 @@ public class RemoteTsdbProxyFallbackFactory implements FallbackFactory<RemoteTsd
             {
                 throw new BusinessException("能耗分项趋势查询:" + throwable.getMessage());
             }
+
+            @Override
+            public ApiResult<DeviceCsvImportResultVO> importDeviceCsv(String deviceUuid, org.springframework.web.multipart.MultipartFile file)
+            {
+                log.error("设备时序 CSV 导入失败: {}", throwable.getMessage());
+                return ApiResult.error("设备时序 CSV 导入:" + throwable.getMessage());
+            }
         };
     }
 }

+ 8 - 0
service-tsdb/service-tsdb-api/src/main/java/com/usky/demo/feign/RemoteTsdbProxyFeignConfiguration.java

@@ -2,10 +2,13 @@ package com.usky.demo.feign;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import feign.codec.Decoder;
+import feign.codec.Encoder;
+import feign.form.spring.SpringFormEncoder;
 import org.springframework.beans.factory.ObjectFactory;
 import org.springframework.beans.factory.ObjectProvider;
 import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
 import org.springframework.cloud.openfeign.support.SpringDecoder;
+import org.springframework.cloud.openfeign.support.SpringEncoder;
 import org.springframework.context.annotation.Bean;
 
 /**
@@ -13,6 +16,11 @@ import org.springframework.context.annotation.Bean;
  */
 public class RemoteTsdbProxyFeignConfiguration {
 
+    @Bean
+    public Encoder feignEncoder(ObjectFactory<HttpMessageConverters> messageConverters) {
+        return new SpringFormEncoder(new SpringEncoder(messageConverters));
+    }
+
     @Bean
     public Decoder feignDecoder(
             ObjectFactory<HttpMessageConverters> messageConverters,

+ 14 - 0
service-tsdb/service-tsdb-biz/src/main/java/com/usky/demo/controller/api/DataTsdbProxyControllerApi.java

@@ -14,8 +14,10 @@ import com.usky.demo.service.QueryTdengineDataService;
 import com.usky.demo.service.SysUserService;
 import com.usky.system.RemoteUserService;
 import com.usky.system.domain.SysUserVO;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.MediaType;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 
@@ -27,6 +29,7 @@ import java.util.Map;
 import java.util.Optional;
 
 @RestController
+@Slf4j
 public class DataTsdbProxyControllerApi implements RemoteTsdbProxyService {
     @Value("${spring.sourcetype}")
     private String sourcetype;
@@ -175,4 +178,15 @@ public class DataTsdbProxyControllerApi implements RemoteTsdbProxyService {
         return queryTdengineDataService.sumEnergyItemTrend(requestVO);
     }
 
+    @Override
+    @PostMapping(value = "/importDeviceCsv", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    public ApiResult<DeviceCsvImportResultVO> importDeviceCsv(@RequestPart("deviceUuid") String deviceUuid,
+                                                              @RequestPart("file") MultipartFile file) {
+        if (!"taos".equals(sourcetype)) {
+            log.error("当前数据源不支持 TDengine CSV 导入, sourcetype={}", sourcetype);
+            return ApiResult.error("当前数据源不支持 TDengine CSV 导入");
+        }
+        return queryTdengineDataService.importDeviceCsv(deviceUuid, file);
+    }
+
 }

+ 7 - 0
service-tsdb/service-tsdb-biz/src/main/java/com/usky/demo/service/QueryTdengineDataService.java

@@ -1,11 +1,13 @@
 package com.usky.demo.service;
 
+import com.usky.common.core.bean.ApiResult;
 import com.usky.common.mybatis.core.CrudService;
 import com.usky.demo.domain.*;
 import com.usky.demo.service.vo.SuperTableVO;
 import org.apache.ibatis.annotations.Param;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.multipart.MultipartFile;
 
 import java.util.List;
 import java.util.Map;
@@ -48,4 +50,9 @@ public interface QueryTdengineDataService extends CrudService<QueryTdengineData>
      * 能耗分项趋势:按时间粒度 INTERVAL 聚合,返回 time/value 列表
      */
     EnergyItemTrendResultVO sumEnergyItemTrend(EnergyItemTrendQueryVO requestVO);
+
+    /**
+     * 从 CSV 解析并导入设备子表时序:INSERT INTO _{deviceUuid} (ts, p) VALUES ...
+     */
+    ApiResult<DeviceCsvImportResultVO> importDeviceCsv(String deviceUuid, MultipartFile file);
 }

+ 255 - 0
service-tsdb/service-tsdb-biz/src/main/java/com/usky/demo/service/impl/QueryTdengineDataServiceImpl.java

@@ -4,6 +4,7 @@ import com.baomidou.dynamic.datasource.annotation.DS;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.usky.common.core.bean.ApiResult;
 import com.usky.common.core.exception.BusinessException;
 import com.usky.common.mybatis.core.AbstractCrudService;
 import com.usky.demo.domain.*;
@@ -23,12 +24,21 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.cache.annotation.Cacheable;
 import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
 
 import javax.sql.DataSource;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
 import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
 import java.sql.*;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
 import java.util.*;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
@@ -47,6 +57,16 @@ public class QueryTdengineDataServiceImpl extends AbstractCrudService<QueryTdeng
 
     private static final Pattern SQL_IDENTIFIER_PATTERN = Pattern.compile("^[a-zA-Z_][a-zA-Z0-9_]*$");
 
+    private static final Pattern DEVICE_UUID_PATTERN = Pattern.compile("^[a-zA-Z0-9_-]+$");
+
+    private static final int IMPORT_BATCH_SIZE = 500;
+
+    private static final DateTimeFormatter TS_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+    private static final List<String> TS_HEADER_ALIASES = Arrays.asList("ts", "timestamp", "time");
+
+    private static final List<String> P_HEADER_ALIASES = Arrays.asList("p", "power", "active_power", "value");
+
     @Autowired
     private TsdbUtils tsdbUtils;
     @Autowired
@@ -460,4 +480,239 @@ public class QueryTdengineDataServiceImpl extends AbstractCrudService<QueryTdeng
             throw new BusinessException(paramName + " 格式非法,仅允许字母、数字与下划线,且不能以数字开头");
         }
     }
+
+    @Override
+    public ApiResult<DeviceCsvImportResultVO> importDeviceCsv(String deviceUuid, MultipartFile file) {
+        if (StringUtils.isBlank(deviceUuid)) {
+            log.error("设备UUID不能为空");
+            return ApiResult.error("设备UUID不能为空");
+        }
+        if (!DEVICE_UUID_PATTERN.matcher(deviceUuid.trim()).matches()) {
+            log.error("设备UUID格式无效, deviceUuid=" + deviceUuid);
+            return ApiResult.error("设备UUID格式无效");
+        }
+        if (file == null || file.isEmpty()) {
+            log.error("导入文件不能为空, deviceUuid=" + deviceUuid);
+            return ApiResult.error("导入文件不能为空");
+        }
+        String originalFilename = file.getOriginalFilename();
+        if (StringUtils.isBlank(originalFilename) || !originalFilename.toLowerCase(Locale.ROOT).endsWith(".csv")) {
+            log.error("仅支持 CSV 文件, deviceUuid=" + deviceUuid + ", filename=" + originalFilename);
+            return ApiResult.error("仅支持 CSV 文件");
+        }
+
+        String normalizedUuid = deviceUuid.trim();
+        String tableName = buildDeviceSubTableName(normalizedUuid);
+        try {
+            List<TsdbCsvRow> rows = parseCsvRows(file);
+            if (rows == null) {
+                return ApiResult.error("CSV 解析失败");
+            }
+            if (rows.isEmpty()) {
+                log.error("CSV 无有效数据行, deviceUuid=" + normalizedUuid + ", tableName=" + tableName);
+                return ApiResult.error("CSV 无有效可导入数据行");
+            }
+            int importedRows = batchInsertRows(tableName, rows);
+            if (importedRows == 0) {
+                log.error("CSV 无有效可导入数据行, deviceUuid=" + normalizedUuid + ", tableName=" + tableName);
+                return ApiResult.error("CSV 无有效可导入数据行");
+            }
+
+            DeviceCsvImportResultVO result = new DeviceCsvImportResultVO();
+            result.setDeviceUuid(normalizedUuid);
+            result.setTableName(tableName);
+            result.setImportedRows(importedRows);
+            return ApiResult.success(result);
+        } catch (IOException ex) {
+            log.error("解析 CSV 文件失败, deviceUuid=" + normalizedUuid + ", tableName=" + tableName + ", error=" + ex.getMessage());
+            return ApiResult.error("解析 CSV 文件失败: " + ex.getMessage());
+        } catch (SQLException ex) {
+            log.error("时序数据导入失败, deviceUuid=" + normalizedUuid + ", tableName=" + tableName + ", error=" + ex.getMessage());
+            return ApiResult.error("时序数据导入失败: " + ex.getMessage());
+        }
+    }
+
+    private List<TsdbCsvRow> parseCsvRows(MultipartFile file) throws IOException {
+        List<TsdbCsvRow> rows = new ArrayList<>();
+        try (BufferedReader reader = new BufferedReader(
+                new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8))) {
+            String line;
+            int tsIndex = 0;
+            int pIndex = 1;
+            boolean headerResolved = false;
+            int lineNo = 0;
+            while ((line = reader.readLine()) != null) {
+                lineNo++;
+                line = line.trim();
+                if (line.isEmpty()) {
+                    continue;
+                }
+                String[] parts = splitCsvLine(line);
+                if (!headerResolved) {
+                    if (isHeaderRow(parts)) {
+                        tsIndex = findColumnIndex(parts, TS_HEADER_ALIASES, "ts");
+                        if (tsIndex < 0) {
+                            return null;
+                        }
+                        pIndex = findColumnIndex(parts, P_HEADER_ALIASES, "p");
+                        if (pIndex < 0) {
+                            return null;
+                        }
+                        headerResolved = true;
+                        continue;
+                    }
+                    headerResolved = true;
+                }
+                if (parts.length <= Math.max(tsIndex, pIndex)) {
+                    log.error("CSV 第 " + lineNo + " 行列数不足,已跳过");
+                    continue;
+                }
+                String tsRaw = parts[tsIndex];
+                String pRaw = parts[pIndex];
+                if (StringUtils.isBlank(tsRaw) || StringUtils.isBlank(pRaw)) {
+                    log.error("CSV 第 " + lineNo + " 行 ts 或 p 存在空值,已跳过");
+                    continue;
+                }
+                Long tsMillis = parseTimestamp(tsRaw, lineNo);
+                if (tsMillis == null) {
+                    continue;
+                }
+                Double pValue = parseMetricValue(pRaw, lineNo);
+                if (pValue == null) {
+                    continue;
+                }
+                TsdbCsvRow row = new TsdbCsvRow(lineNo, tsMillis, pValue);
+                if (!isImportableRow(row)) {
+                    continue;
+                }
+                rows.add(row);
+            }
+        }
+        return rows;
+    }
+
+    private int batchInsertRows(String tableName, List<TsdbCsvRow> rows) throws SQLException {
+        int importedRows = 0;
+        try (Connection connection = dataSource.getConnection();
+             Statement statement = connection.createStatement()) {
+            for (int start = 0; start < rows.size(); start += IMPORT_BATCH_SIZE) {
+                int end = Math.min(start + IMPORT_BATCH_SIZE, rows.size());
+                StringBuilder sql = new StringBuilder("INSERT INTO ")
+                        .append(tableName)
+                        .append(" (ts, p) VALUES ");
+                int batchCount = 0;
+                for (int i = start; i < end; i++) {
+                    TsdbCsvRow row = rows.get(i);
+                    if (!isImportableRow(row)) {
+                        continue;
+                    }
+                    if (batchCount > 0) {
+                        sql.append(' ');
+                    }
+                    sql.append('(').append(row.tsMillis).append(',').append(row.pValue).append(')');
+                    batchCount++;
+                }
+                if (batchCount == 0) {
+                    continue;
+                }
+                statement.execute(sql.toString());
+                importedRows += batchCount;
+            }
+        }
+        return importedRows;
+    }
+
+    private boolean isImportableRow(TsdbCsvRow row) {
+        if (row.tsMillis <= 0) {
+            log.error("CSV 第 " + row.lineNo + " 行 ts 无效,已跳过");
+            return false;
+        }
+        if (!Double.isFinite(row.pValue)) {
+            log.error("CSV 第 " + row.lineNo + " 行 p 不是有效数值,已跳过");
+            return false;
+        }
+        return true;
+    }
+
+    private static String[] splitCsvLine(String line) {
+        return Arrays.stream(line.split(",", -1))
+                .map(String::trim)
+                .toArray(String[]::new);
+    }
+
+    private static boolean isHeaderRow(String[] parts) {
+        for (String part : parts) {
+            String normalized = part.trim().toLowerCase(Locale.ROOT);
+            if (TS_HEADER_ALIASES.contains(normalized) || P_HEADER_ALIASES.contains(normalized)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private int findColumnIndex(String[] headers, List<String> aliases, String columnLabel) {
+        for (int i = 0; i < headers.length; i++) {
+            if (aliases.contains(headers[i].trim().toLowerCase(Locale.ROOT))) {
+                return i;
+            }
+        }
+        log.error("CSV 缺少 " + columnLabel + " 列");
+        return -1;
+    }
+
+    private Long parseTimestamp(String rawValue, int lineNo) {
+        String value = rawValue == null ? "" : rawValue.trim();
+        if (value.isEmpty()) {
+            log.error("CSV 第 " + lineNo + " 行 ts 为空,已跳过");
+            return null;
+        }
+        if (value.matches("\\d+")) {
+            long epoch = Long.parseLong(value);
+            return value.length() <= 10 ? epoch * 1000L : epoch;
+        }
+        try {
+            return LocalDateTime.parse(value, TS_FORMAT)
+                    .atZone(ZoneId.systemDefault())
+                    .toInstant()
+                    .toEpochMilli();
+        } catch (DateTimeParseException ignored) {
+            // fall through
+        }
+        try {
+            return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").parse(value).getTime();
+        } catch (ParseException ex) {
+            log.error("CSV 第 " + lineNo + " 行 ts 格式无效: " + value + ",已跳过");
+            return null;
+        }
+    }
+
+    private Double parseMetricValue(String rawValue, int lineNo) {
+        String value = rawValue == null ? "" : rawValue.trim();
+        if (value.isEmpty()) {
+            log.error("CSV 第 " + lineNo + " 行 p 为空,已跳过");
+            return null;
+        }
+        try {
+            return new BigDecimal(value).doubleValue();
+        } catch (NumberFormatException ex) {
+            log.error("CSV 第 " + lineNo + " 行 p 不是有效数值: " + value + ",已跳过");
+            return null;
+        }
+    }
+
+    private static String buildDeviceSubTableName(String deviceUuid) {
+        return "_" + deviceUuid;
+    }
+
+    private static final class TsdbCsvRow {
+        private final int lineNo;
+        private final long tsMillis;
+        private final double pValue;
+
+        private TsdbCsvRow(int lineNo, long tsMillis, double pValue) {
+            this.lineNo = lineNo;
+            this.tsMillis = tsMillis;
+            this.pValue = pValue;
+        }
+    }
 }

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff