145 Commits 8da7539662 ... cc362b8cba

Author SHA1 Message Date
  hanzhengyi cc362b8cba Merge branch 'usky-ems' of uskycloud/usky-modules into master 2 weeks ago
  james dc0bd09c37 解决冲突 2 weeks ago
  hanzhengyi aeae33b905 Merge branch 'usky-zyj' of uskycloud/usky-modules into master 2 weeks ago
  james 6b316d4110 修改能源分项编码表字段 2 weeks ago
  hanzhengyi cddea72e6d Merge branch 'fyc-ems' of uskycloud/usky-modules into master 2 weeks ago
  james d28d00d959 Merge branch 'usky-ems' of http://47.111.81.118:3000/uskycloud/usky-modules into usky-ems 2 weeks ago
  james 764031b7b2 查询ems_device_item_code表中符合要求的list;开发区域设备信息和开发区域设备信息两个接口; 2 weeks ago
  fuyuchuan 66e6c755be 修复报错 2 weeks ago
  fuyuchuan f29842eb33 Merge branch 'fyc-ems' of uskycloud/usky-modules into usky-ems 2 weeks ago
  fuyuchuan 953ff911d5 解决报错 2 weeks ago
  fuyuchuan f66e9fa042 能耗接口报错修复、优化 2 weeks ago
  james fa1a662192 优化网关配置信息-导出接口,调整按照map结构返回数据 2 weeks ago
  james 02de973bf7 1、解决时序数据库插入不存在表报错的问题; 2 weeks ago
  james 388eb1e218 调整所属网关uuid刷选条件逻辑 2 weeks ago
  james 14c41c71dc 解决发布启动失败的问题 2 weeks ago
  james db5a081802 优化设备信息表-新增和设备信息表-修改两个接口,增加地址码字段值的录入;优化设备信息表-分页和设备信息表-分页-白名单两个接口,增加所属网关的刷选条件;优化网关配置信息-导出接口,调整导出文件格式和导出方式; 2 weeks ago
  fuyuchuan 8633fb644b 能耗分析接口代码提交 2 weeks ago
  hanzhengyi 3fe30ca9f6 Merge branch 'fyc-cdi' of uskycloud/usky-modules into master 2 weeks ago
  fuyuchuan 7d906a5b0a 人防代码更新,优化部分代码,添加新设备 3 weeks ago
  james 4bdf0a3903 优化能耗类型列表接口,增加能耗类型绑定产品id集合字段取值;优化能耗分项列表接口,增加能耗分项绑定设备uuid集合字段取值; 3 weeks ago
  james 2b9f5deaa2 开发能耗类型列表、能耗分项列表、产品关联的能源类型和设备关联的能源分项四个接口 3 weeks ago
  hanzhengyi 60e4456b63 Merge branch 'modules-zjy' of uskycloud/usky-modules into master 3 weeks ago
  zhaojinyu 424c64ddad Merge branch 'han' of uskycloud/usky-modules into modules-zjy 3 weeks ago
  zhaojinyu 0c661fce48 优化电子地图配置添加文字配置失效问题 3 weeks ago
  zhaojinyu a0ca9dccad Merge branch 'han' of uskycloud/usky-modules into modules-zjy 3 weeks ago
  zhaojinyu c6e4c80380 电子地图配置去除设备类型过滤 3 weeks ago
  zhaojinyu 76d590bfbc 解决地图配置中添加设备选择背景色未存库问题 3 weeks ago
  fuyuchuan b040096f34 Merge branch 'master' of http://47.111.81.118:3000/uskycloud/usky-modules into fyc-ems 3 weeks ago
  fuyuchuan 65b94f36d8 Merge branch 'usky-ems' of http://47.111.81.118:3000/uskycloud/usky-modules into fyc-ems 3 weeks ago
  fuyuchuan ce253da0c7 能耗报表接口 3 weeks ago
  fuyuchuan 441e718cda 能耗接口代码提交 3 weeks ago
  fuyuchuan 260358b651 能耗报表代码提交口 3 weeks ago
  zhaojinyu baae54f311 新增巡检异常记录导出接口 3 weeks ago
  zhaojinyu b235a606e7 Merge branch 'master' of http://47.111.81.118:3000/uskycloud/usky-modules into modules-zjy 3 weeks ago
  hanzhengyi 88266319e9 Merge branch 'usky-ems' of uskycloud/usky-modules into master 3 weeks ago
  james 903b603613 Merge branch 'master' into usky-ems 3 weeks ago
  hanzhengyi c9ac62b6ec Merge branch 'fyc-cdi' of uskycloud/usky-modules into master 1 month ago
  fuyuchuan f6ff0de8df 能耗报表分析三个接口代码提交 1 month ago
  fuyuchuan 1a9094dc0f Merge branch 'usky-ems' of http://47.111.81.118:3000/uskycloud/usky-modules into fyc-ems 1 month ago
  fuyuchuan 89a1cb5e97 二氧化碳数据精确到小数点后三位 1 month ago
  fuyuchuan 4867fee05e 二氧化碳数据除以10000并保留两位小数 1 month ago
  fuyuchuan 6310ee0a7d 二氧化碳数据除以10000并保留两位小数 1 month ago
  zhaojinyu 7bc8964b90 优化出入门禁事件图片处理异常问题 1 month ago
  fuyuchuan 551bfdadd4 Merge branch 'fyc-cdi' of uskycloud/usky-modules into master 1 month ago
  fuyuchuan c7efb89d86 代码调整 1 month ago
  fuyuchuan 711c67a640 Merge branch 'fyc-cdi' of uskycloud/usky-modules into master 1 month ago
  fuyuchuan 680b3001e2 人防监测数据推送增加日志和检测时间配置 1 month ago
  james bd32c9dc08 优化分类能耗统计(按能源类型关联产品,调用 TSDB 分项汇总)接口,增加等量植树、上期等量植树和同期等量植树三个字段值的采集 1 month ago
  james 9e38cda2e3 Merge branch 'usky-ems' into server-165 1 month ago
  james 041eea3240 优化设备信息表-分页和设备信息表-分页-白名单两个接口,增加监控位置字段的刷选处理逻辑 1 month ago
  fuyuchuan fd9c1dd954 Merge branch 'fyc-cdi' of uskycloud/usky-modules into master 1 month ago
  fuyuchuan fbaaf39d1c 人防代码优化 1 month ago
  fuyuchuan 98364e2236 人防代码优化 1 month ago
  james 680f147a28 5、开发建筑能耗分析(按建筑排名)接口; 1 month ago
  james 7ba68a4623 2、调整能耗类型查询逻辑,从类文件中获取改为从数据库配置表中获取; 1 month ago
  james 940c4a08e8 1、开发能耗用能趋势接口; 1 month ago
  fuyuchuan 62d0517d2d 添加掩蔽人数2S延迟 1 month ago
  james 999bbc7488 开发单位综合能耗、综合累计能耗、分类能耗占比(总览顶部)接口 1 month ago
  james 92f486b442 1、优化分类能耗统计接口,调整能耗分项汇总api方法 1 month ago
  james f2d37c4138 3、开发能耗分项汇总查询接口; 1 month ago
  james 60ad6d5ef4 1、优化获取概览页能源类型条目(如电/水/气)接口; 1 month ago
  zhaojinyu 54bbd4efe4 修复状态感知无图片问题 1 month ago
  zhaojinyu ec1bf8b736 修复车辆抓拍无图片问题 1 month ago
  hanzhengyi 80e89014ca Merge branch 'fyc-cdi' of uskycloud/usky-modules into master 1 month ago
  fuyuchuan 7271b4a8cd 掩蔽人数代码提交 1 month ago
  zhaojinyu 50cbbe9873 解决实时智能分析、出入门禁、车辆抓拍、入侵告警、状态感知事件类型未正常获取问题 1 month ago
  james e2d6c3bfc3 优化空间绑定网关(一个空间绑定一个或多个网关)接口,空间绑定设备列表中的网关设备;优化根据空间ID查询网关列表接口,获取设备状态表中网关设备在线状态、最后上线时间、最后离线时间字段值; 1 month ago
  fuyuchuan 3ed958b625 人防代码优化,以及平面图上传代码恢复 1 month ago
  fuyuchuan 6137d09b40 解决冲突 2 months ago
  fuyuchuan b3ded96b86 人防代码优化 2 months ago
  hanzhengyi 91b70b7bcf Merge branch 'usky-ems' of uskycloud/usky-modules into master 2 months ago
  james e68149e898 开发空间解绑网关(一个空间解绑一个或多个网关)接口 2 months ago
  james fdab38d9b7 优化根据空间ID查询网关列表接口,增加子空间查询逻辑 2 months ago
  james 2140657872 开发按网关 UUID 分页查询 dmp_device 设备信息列表接口 2 months ago
  james 9d1087c02f 优化项目新增、修改、删除接口逻辑,通过空间Id操作项目表内容 2 months ago
  james c0ab6f2a99 开发项目(ems_project)新增、修改、删除三个接口 2 months ago
  fuyuchuan 6e420588d7 Merge branch 'fu-dev' of uskycloud/usky-modules into master 2 months ago
  fuyuchuan e67e218ae0 人防代码优化 2 months ago
  hanzhengyi 399d6009b8 Merge branch 'fu-dev' of uskycloud/usky-modules into master 2 months ago
  fuyuchuan 0a86ef7deb 注释测试代码 2 months ago
  fuyuchuan 9df27d1605 优化调整人防代码 2 months ago
  hanzhengyi 6d23577968 Merge branch 'fu-dev' of uskycloud/usky-modules into master 2 months ago
  fuyuchuan 4276dc4036 人防代码优化 2 months ago
  fuyuchuan ecca2d4791 人防代码优化 2 months ago
  fuyuchuan b08f5cb37f 人防代码优化 2 months ago
  fuyuchuan da02d5c6b5 人防代码优化 2 months ago
  fuyuchuan 749e2bebc9 人防代码优化 2 months ago
  james 3dde41b625 开发设备管理下区域管理、网关设备界面相关接口 2 months ago
  fuyuchuan 964f653258 人防检测数据代码优化 2 months ago
  hanzhengyi 09f181493a Merge branch 'fu-dev' of uskycloud/usky-modules into master 2 months ago
  fuyuchuan 6e59a7bdc6 人防检测数据代码优化 2 months ago
  fuyuchuan 7f65dfc77c 人防检测数据代码优化 2 months ago
  fuyuchuan 817ad28040 数据中心代码恢复 2 months ago
  fuyuchuan 8e2a0d9a58 人防代码优化 2 months ago
  fuyuchuan 799b3b3c18 Merge branch 'master' of http://47.111.81.118:3000/uskycloud/usky-modules into fu-dev 2 months ago
  hanzhengyi 797c2895ef Merge branch 'usky-zyj' of uskycloud/usky-modules into master 2 months ago
  james 77f58a6f10 Merge branch 'master' into usky-zyj 2 months ago
  james dd572ee53f 开发空间、建筑、区域相关接口 2 months ago
  zhaojinyu a489825023 解决主键id报错问题 2 months ago
  james ff38d17d5d 1、开发获取空间树接口; 2 months ago
  james ff61c6e525 1、开发获取空间树接口; 2 months ago
  hanzhengyi eba74003b8 Merge branch 'fanghuisheng' of uskycloud/usky-modules into master 2 months ago
  fanghuisheng 319e08594f 调整门禁设备domain逻辑、设备绑定方法、新增设备心跳接口、修复会议接口调用报错问题 2 months ago
  james b5c390ad55 优化解析巡检日期上报逻辑 2 months ago
  fuyuchuan 753b4cad76 人防告警推送代码优化 2 months ago
  james 7bb385626c 调整设备能耗`ConsumptionJob`触发频率为每小时整点 3 months ago
  fuyuchuan 06f6b60fef 优化人防单元等基础数据推送代码 3 months ago
  james 8799f46670 排查触发条件为设备触发触发方式为用量值的规则引擎没有生效的问题,调用查询时序数据库历史接口属性字段改为小写,同时调整查询时间格式 3 months ago
  james 73e431e5a3 排查触发条件为设备触发触发方式为用量值的规则引擎没有生效的问题 3 months ago
  james 1abe56018e 优化查询日志详情接口,将同一个规则下触发的所有动作合并到一个日志记录; 3 months ago
  james a0df8c4b14 1、调整service-tsdb服务中对外查询实时和历史api接口封装体; 3 months ago
  james 7a47bf0dfb 查询日志详情中增加操作成功码的判断 3 months ago
  james 9b2f8086f4 排查解决查询规则引擎日志详情报错的问题 3 months ago
  james 68f77368c2 触发动作日志中增加获取规则详情中设备对应的租户Id作为日志记录的租户Id 3 months ago
  james 79ae95495f 开发规则引擎中执行冷却机制,上报触发规则后进入冷却期,在冷却期范围内不再触发规则 3 months ago
  james bfe41a0f09 订阅消息时增加定时约束条件的判断 3 months ago
  james ff17274fc8 将规则详情信息中的属性编码字段在与上报数据进行对比或与时序数据库查询的数据进行对比时统一改为小写 3 months ago
  james 8f43878f93 将规则详情信息中的属性编码字段在与上报数据进行对比或与时序数据库查询的数据进行对比时统一改为小写 3 months ago
  james 9ca585decc 调整推送告警数据获取设备名称逻辑 3 months ago
  james 6b93af26e7 调整推送告警数据获取设备名称逻辑 3 months ago
  james 959ace362f Git 强制添加被 .gitignore 过滤的文件 3 months ago
  james 3026794849 Git 强制添加被 .gitignore 过滤的文件 3 months ago
  james 1c545a9562 优化规则执行日志详情接口中控制日志设备控制描述内容和告警推送内容 3 months ago
  james 337c0bc08c 优化规则执行日志详情接口中控制日志描述内容 3 months ago
  zhaojinyu 0b36774a51 优化调整能源分析中按年传参时间维度异常问题 3 months ago
  james b7b0c8a76f 优化订阅设备RocketMQ、定时触发、采集值三个条件下获取租户Id的逻辑;优化规则引擎-配置规则接口,在插入数据到规则设备表前增加设备Id、属性Id和规则Id唯一校验的判断,解决重复插入数据的问题; 3 months ago
  james d74f09bde3 优化规则引擎-条件下拉接口,去除租户刷选的逻辑,做到通用;优化当触发条件中触发方式为采集量时,条件对应的开始时间和结束时间的处理,以及次日、次月、次周的处理逻辑; 3 months ago
  james 360430740a 在订阅RocketMQ消息触发执行动作、定时任务触发执行动作、采集量定时任务触发执行动作增加日志采集 3 months ago
  james f3e360e7bc 修改告警推送体Json结构,优化推送代码逻辑,可以选择一个产品的多个设备 3 months ago
  james a9c430e0a7 1、消费RocketMQ消息到判断触发执行动作的处理逻辑;2、定时规则触发逻辑; 3 months ago
  james 03d28fe251 优化规则执行日志分页接口,解决根据规则名称刷选无效的问题 3 months ago
  fuyuchuan d64e8e9161 解决冲突 3 months ago
  fuyuchuan 413fa03a24 区域管理部分接口提交 3 months ago
  james 8697be4444 添加被git屏蔽掉的文件夹 3 months ago
  james d1e57ae8a2 优化规则引擎操作日志存储详情字段生成逻辑 3 months ago
  james 80f2d88851 开发数据规则引擎基础功能 3 months ago
  hanzhengyi d9a0ee41bb 统计报表相关接口 4 months ago
  zhaojinyu ef8bd8f4fd 删除sql文件 4 months ago
  zhaojinyu 92c15d3fd5 修改能耗分析接口完善趋势、指标、分类占比、区域分析、对比分析 4 months ago
  zhaojinyu 11b2ada2be Merge remote-tracking branch 'origin/usky-ems' into usky-ems 4 months ago
  zhaojinyu 009550bae4 Merge branch 'han' of http://47.111.81.118:3000/uskycloud/usky-modules into usky-ems 4 months ago
  hanzhengyi 1cac234e8b 项目总览相关接口 4 months ago
  hanzhengyi a7475b5cee 调整接口返回报错 4 months ago
  hanzhengyi 8465571f48 调整拆分部分接口 4 months ago
  zhaojinyu 010472a47b 对接算法盒子完成并添加算法设备入库增删改查操作 9 months ago
100 changed files with 5685 additions and 541 deletions
  1. 2 1
      pom.xml
  2. 19 10
      service-ai/service-ai-biz/pom.xml
  3. 17 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/AITestApplication.java
  4. 3 6
      service-ai/service-ai-biz/src/main/java/com/usky/ai/controller/web/AiChatController.java
  5. 1 1
      service-ai/service-ai-biz/src/main/java/com/usky/ai/controller/web/AiQuestionController.java
  6. 3 3
      service-ai/service-ai-biz/src/main/java/com/usky/ai/controller/web/AiSessionController.java
  7. 32 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/controller/web/BoardPingController.java
  8. 69 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/controller/web/DeviceController.java
  9. 128 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/controller/web/EdgeAlgController.java
  10. 62 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/controller/web/EdgeMediaController.java
  11. 59 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/controller/web/RepositoryController.java
  12. 27 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/controller/web/TaskPreviewController.java
  13. 158 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/controller/web/UniversalUploadController.java
  14. 104 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/domain/AiAlarmLog.java
  15. 83 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/domain/AiDevice.java
  16. 1 1
      service-ai/service-ai-biz/src/main/java/com/usky/ai/domain/AiQuestion.java
  17. 1 1
      service-ai/service-ai-biz/src/main/java/com/usky/ai/domain/AiQuestionItem.java
  18. 1 1
      service-ai/service-ai-biz/src/main/java/com/usky/ai/domain/AiSession.java
  19. 13 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgAbilityFetchRequestDTO.java
  20. 131 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgAbilityFetchResponseDTO.java
  21. 80 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgScheduleCreateRequestDTO.java
  22. 25 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgScheduleCreateResponseDTO.java
  23. 16 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgScheduleDeleteRequestDTO.java
  24. 40 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgScheduleDeleteResponseDTO.java
  25. 13 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgScheduleFetchRequestDTO.java
  26. 113 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgScheduleFetchResponseDTO.java
  27. 20 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgSuitAppendRequestDTO.java
  28. 36 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgSuitAppendResponseDTO.java
  29. 16 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgSuitCreateRequestDTO.java
  30. 36 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgSuitCreateResponseDTO.java
  31. 13 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgSuitFetchRequestDTO.java
  32. 65 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgSuitFetchResponseDTO.java
  33. 18 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgSuitGroupRemoveRequestDTO.java
  34. 33 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgSuitGroupRemoveResponseDTO.java
  35. 20 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgSuitRemoveRequestDTO.java
  36. 33 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgSuitRemoveResponseDTO.java
  37. 16 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgTaskSnapRequestDTO.java
  38. 43 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgTaskSnapResponseDTO.java
  39. 109 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/BoardPingDTO.java
  40. 13 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/DeleteFaceRequestDTO.java
  41. 34 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/DeleteFaceResponseDTO.java
  42. 14 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/EdgeAlgTaskCommandRequestDTO.java
  43. 82 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/EdgeAlgTaskConfigRequestDTO.java
  44. 15 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/EdgeAlgTaskFetchRequestDTO.java
  45. 177 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/EdgeAlgTaskListResponseDTO.java
  46. 34 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/EdgeAlgTaskResponseDTO.java
  47. 32 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/EdgeAppControllerReplyDTO.java
  48. 42 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/EdgeAppControllerRequestDTO.java
  49. 16 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/EdgeAppDeleteRequestDTO.java
  50. 37 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/EdgeAppDeleteResponseDTO.java
  51. 116 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/EdgeAppFetchResponseDTO.java
  52. 15 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/FaceRecognitionRequestDTO.java
  53. 50 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/FaceRecognitionResponseDTO.java
  54. 16 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/RegisterFaceRequestDTO.java
  55. 43 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/RegisterFaceResponseDTO.java
  56. 52 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/RepositoriesInfoResponseDTO.java
  57. 11 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/RepositoryCreateRequestDTO.java
  58. 36 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/RepositoryCreateResponseDTO.java
  59. 10 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/RepositoryDeleteRequestDTO.java
  60. 33 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/RepositoryDeleteResponseDTO.java
  61. 16 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/RepositoryFacesRequestDTO.java
  62. 48 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/RepositoryFacesResponseDTO.java
  63. 13 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/RepositoryUpdateRequestDTO.java
  64. 36 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/RepositoryUpdateResponseDTO.java
  65. 19 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/UpdateFaceRequestDTO.java
  66. 43 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/UpdateFaceResponseDTO.java
  67. 27 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/mapper/AiDeviceMapper.java
  68. 1 1
      service-ai/service-ai-biz/src/main/java/com/usky/ai/mapper/AiQuestionMapper.java
  69. 2 2
      service-ai/service-ai-biz/src/main/java/com/usky/ai/mapper/AiSessionMapper.java
  70. 22 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/service/AiDeviceService.java
  71. 332 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/service/EdgeMediaService.java
  72. 13 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/service/config/AppConfig.java
  73. 48 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/service/enums/TopListener.java
  74. 60 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/service/impl/AiDeviceServiceImpl.java
  75. 299 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/service/listener/BoardPingListener.java
  76. 50 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/service/mqtt/MqttBaseConfig.java
  77. 17 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/service/mqtt/MqttGateway.java
  78. 56 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/service/mqtt/MqttInConfig.java
  79. 41 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/service/mqtt/MqttOutConfig.java
  80. 34 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/service/util/GlobalExceptionHandler.java
  81. 15 0
      service-ai/service-ai-biz/src/main/java/com/usky/ai/service/util/JsonUtils.java
  82. 62 0
      service-ai/service-ai-biz/src/main/resources/mapper/AiDeviceMapper.xml
  83. 3 2
      service-ai/service-ai-biz/src/main/resources/static/tyqw.html
  84. 11 33
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/AlarmDataController.java
  85. 5 2
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/BaseDataController.java
  86. 1 1
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/CdiDeliveryLogService.java
  87. 6 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/config/DeviceFieldConfig.java
  88. 4 1
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/enums/AlarmType.java
  89. 2 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/enums/MqttTopics.java
  90. 15 8
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/AlarmDataSyncService.java
  91. 31 146
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/AlarmDataTransferService.java
  92. 55 16
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/BaseDataTransferService.java
  93. 468 222
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/CdiDeliveryLogServiceImpl.java
  94. 436 29
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/IotDataTransferService.java
  95. 381 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/util/AirDefenseSimulator.java
  96. 177 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/util/BasementClimateUtil.java
  97. 213 45
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/util/DeviceDataQuery.java
  98. 5 5
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/util/DeviceDataSyncService.java
  99. 49 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/util/FixedWaterLevelGenerator.java
  100. 32 4
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/util/HttpClientUtils.java

+ 2 - 1
pom.xml

@@ -64,7 +64,6 @@
 
         <module>service-agbox</module>
 
-
         <module>service-job</module>
 
 
@@ -88,6 +87,8 @@
 
         <module>service-tsdb</module>
 
+        <module>service-rule</module>
+
         <!--    <module>service-data</module>-->
 
         <module>service-sas</module>

+ 19 - 10
service-ai/service-ai-biz/pom.xml

@@ -35,6 +35,15 @@
             <artifactId>mysql-connector-java</artifactId>
             <scope>runtime</scope>
         </dependency>
+        <!--MQTT依赖-->
+        <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>
 
         <!-- 阿里dashscope依赖 -->
         <dependency>
@@ -48,8 +57,15 @@
                 </exclusion>
             </exclusions>
         </dependency>
-
-
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-jpa</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-boot-starter</artifactId>
+            <version>3.4.0</version>
+        </dependency>
     </dependencies>
 
     <build>
@@ -72,17 +88,10 @@
                 <artifactId>smart-doc-maven-plugin</artifactId>
                 <version>2.1.1</version>
                 <configuration>
-                    <!--指定生成文档的使用的配置文件,配置文件放在自己的项目中-->
                     <configFile>./src/main/resources/smart-doc.json</configFile>
-                    <!--指定项目名称-->
                     <projectName>test</projectName>
-                    <!--                    <excludes>-->
-                    <!--                        <exclude>com.bizmatics:product-service-provider</exclude>-->
-                    <!--                        <exclude>cn.afterturn:easypoi-web</exclude>-->
-                    <!--                    </excludes>-->
                 </configuration>
             </plugin>
         </plugins>
     </build>
-
-</project>
+</project>

+ 17 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/AITestApplication.java

@@ -0,0 +1,17 @@
+package com.usky.ai;
+
+import com.usky.ai.dto.AlgScheduleFetchResponseDTO;
+import java.util.List;
+
+public class AITestApplication {
+    public static void main(String[] args) {
+        String value = "100000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000";
+        AlgScheduleFetchResponseDTO.Schedule schedule = new AlgScheduleFetchResponseDTO.Schedule();
+        schedule.setValue(value);
+
+        List<String> timePeriods = schedule.getReadableTimePeriods();
+        for (String period : timePeriods) {
+            System.out.println(period);
+        }
+    }
+}

+ 3 - 6
service-ai/service-ai-biz/src/main/java/com/usky/ai/controller/web/AiChatController.java

@@ -11,11 +11,10 @@ import com.alibaba.fastjson.JSONObject;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.usky.ai.mapper.AiQuestionMapper;
 import com.usky.ai.mapper.AiSessionMapper;
-import com.usky.ai.service.AiQuestion;
-import com.usky.ai.service.AiQuestionItem;
-import com.usky.ai.service.AiSession;
+import com.usky.ai.domain.AiQuestion;
+import com.usky.ai.domain.AiQuestionItem;
+import com.usky.ai.domain.AiSession;
 import com.usky.ai.service.vo.AiStreamOutputVO;
-import com.usky.common.core.bean.ApiResult;
 import com.usky.common.security.utils.SecurityUtils;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -27,12 +26,10 @@ import org.springframework.web.bind.annotation.*;
 import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
 
 import javax.annotation.Resource;
-import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.time.LocalDateTime;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.stream.Collectors;
 
 @Slf4j
 @RestController

+ 1 - 1
service-ai/service-ai-biz/src/main/java/com/usky/ai/controller/web/AiQuestionController.java

@@ -1,7 +1,7 @@
 package com.usky.ai.controller.web;
 
 import com.usky.ai.mapper.AiQuestionMapper;
-import com.usky.ai.service.AiQuestion;
+import com.usky.ai.domain.AiQuestion;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;

+ 3 - 3
service-ai/service-ai-biz/src/main/java/com/usky/ai/controller/web/AiSessionController.java

@@ -2,9 +2,9 @@ package com.usky.ai.controller.web;
 
 import com.usky.ai.mapper.AiQuestionMapper;
 import com.usky.ai.mapper.AiSessionMapper;
-import com.usky.ai.service.AiQuestion;
-import com.usky.ai.service.AiQuestionItem;
-import com.usky.ai.service.AiSession;
+import com.usky.ai.domain.AiQuestion;
+import com.usky.ai.domain.AiQuestionItem;
+import com.usky.ai.domain.AiSession;
 import com.usky.common.core.bean.ApiResult;
 import com.usky.common.security.utils.SecurityUtils;
 import lombok.extern.slf4j.Slf4j;

+ 32 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/controller/web/BoardPingController.java

@@ -0,0 +1,32 @@
+package com.usky.ai.controller.web;
+
+
+import com.usky.ai.dto.BoardPingDTO;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+@RestController
+@RequestMapping("/api/heartbeat")
+public class BoardPingController {
+
+    // 每个 BoardId 最新一条心跳
+    public static final Map<String, BoardPingDTO> CACHE = new ConcurrentHashMap<>();
+
+    /**
+     * 返回全部设备最新心跳
+     */
+    @GetMapping
+    public Map<String, BoardPingDTO> all() {
+        return CACHE;
+    }
+
+    /**
+     * 返回指定设备最新心跳
+     */
+    @GetMapping("/{boardId}")
+    public BoardPingDTO one(@PathVariable String boardId) {
+        return CACHE.get(boardId);
+    }
+}

+ 69 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/controller/web/DeviceController.java

@@ -0,0 +1,69 @@
+package com.usky.ai.controller.web;
+
+import com.usky.ai.domain.AiDevice;
+import com.usky.ai.service.AiDeviceService;
+import com.usky.common.core.bean.ApiResult;
+import com.usky.common.core.bean.CommonPage;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/device")
+public class DeviceController {
+
+    private final AiDeviceService aiDeviceService;
+
+    @Autowired
+    public DeviceController(AiDeviceService aiDeviceService) {
+        this.aiDeviceService = aiDeviceService;
+    }
+
+    // 返回设备信息,支持分页
+    @GetMapping
+    public ApiResult<CommonPage<AiDevice>> devices(
+            @RequestParam(required = false) String boardId,
+            @RequestParam(defaultValue = "1") int pageNum,
+            @RequestParam(defaultValue = "10") int pageSize) {
+        List<AiDevice> devices = aiDeviceService.getDevices(boardId, pageNum, pageSize);
+        long total = aiDeviceService.getTotalDevices(boardId); // 获取总记录数
+        CommonPage<AiDevice> page = new CommonPage<>(devices, total, pageSize, pageNum);
+        return ApiResult.success(page);
+    }
+
+    // 新增设备
+    @PostMapping
+    public ApiResult<Void> addDevice(@RequestBody AiDevice device) {
+        try {
+            aiDeviceService.addDevice(device);
+            return ApiResult.success();
+        } catch (IllegalArgumentException e) {
+            return ApiResult.error(e.getMessage());
+        } catch (Exception e) {
+            return ApiResult.error("新增失败", e.getMessage());
+        }
+    }
+
+    // 编辑设备
+    @PutMapping
+    public ApiResult<Void> editDevice(@RequestBody AiDevice device) {
+        try {
+            aiDeviceService.editDevice(device);
+            return ApiResult.success();
+        } catch (Exception e) {
+            return ApiResult.error("编辑失败", e.getMessage());
+        }
+    }
+
+    // 删除指定设备信息
+    @DeleteMapping("/{boardId}")
+    public ApiResult<Void> delete(@PathVariable String boardId) {
+        try {
+            aiDeviceService.deleteDevice(boardId);
+            return ApiResult.success();
+        } catch (Exception e) {
+            return ApiResult.error("删除失败", e.getMessage());
+        }
+    }
+}

+ 128 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/controller/web/EdgeAlgController.java

@@ -0,0 +1,128 @@
+package com.usky.ai.controller.web;
+
+import com.usky.ai.dto.*;
+import com.usky.ai.service.EdgeMediaService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/edge-alg")
+@RequiredArgsConstructor
+public class EdgeAlgController {
+
+    private final EdgeMediaService service;
+
+    //算法任务配置
+    @PostMapping("/task/config")
+    public Object configTask(@RequestBody EdgeAlgTaskConfigRequestDTO req) {
+        return service.configTask(req);
+    }
+
+    //算法任务控制
+    @PostMapping("/task/control")
+    public Object controlTask(@RequestParam String boardId,
+                              @RequestParam String session,
+                              @RequestParam Integer command) {
+        return service.controlTask(boardId, session, command);
+    }
+
+    //算法任务删除
+    @DeleteMapping("/task/{boardId}/{session}")
+    public Object deleteTask(@PathVariable String boardId,
+                             @PathVariable String session) {
+        return service.deleteTask(boardId, session);
+    }
+
+    //算法任务查询
+    @GetMapping("/tasks/{boardId}")
+    public Object fetchTasks(@PathVariable String boardId) {
+        return service.fetchTasks(boardId);
+    }
+
+    //新建计划模板
+    @PostMapping("/schedule/create")
+    public Object createSchedule(@RequestBody AlgScheduleCreateRequestDTO req) {
+        return service.createSchedule(req);
+    }
+
+    //查询计划模板
+    @GetMapping("/schedule/fetch/{boardId}")
+    public Object fetchSchedules(@PathVariable String boardId) {
+        AlgScheduleFetchResponseDTO response = service.fetchSchedules(boardId);
+        System.out.println("获取到的计划模板响应: " + response);
+
+        // 获取计划模板数据,优先从result.content获取,其次从content获取
+        List<AlgScheduleFetchResponseDTO.Schedule> schedules = new ArrayList<>();
+        if (response.isSuccess()) {
+            if (response.getResult() != null && response.getResult().getContent() != null && !response.getResult().getContent().isEmpty()) {
+                schedules = response.getResult().getContent();
+            } else if (response.getContent() != null && !response.getContent().isEmpty()) {
+                schedules = response.getContent();
+            }
+        }
+
+        // 直接返回原始响应数据,不进行解析
+        if (!schedules.isEmpty()) {
+            // 打印每个计划模板的信息用于调试
+            for (AlgScheduleFetchResponseDTO.Schedule schedule : schedules) {
+                System.out.println("计划模板: ID=" + schedule.getId() +
+                        ", Name=" + schedule.getName() +
+                        ", Value长度=" + (schedule.getValue() != null ? schedule.getValue().length() : "null") +
+                        ", Value=" + schedule.getValue());
+            }
+            return schedules;
+        } else {
+            List<String> result = new ArrayList<>();
+            result.add("No schedules found");
+            return result;
+        }
+    }
+
+    //删除计划模板
+    @DeleteMapping("/schedule/delete/{boardId}/{id}")
+    public Object deleteSchedule(@PathVariable String boardId,
+                                 @PathVariable Long id) {
+        return service.deleteSchedule(boardId, id);
+    }
+
+    //新建工装组
+    @PostMapping("/suit/create")
+    public Object createSuitGroup(@RequestBody AlgSuitCreateRequestDTO req) {
+        return service.createSuitGroup(req);
+    }
+
+    //删除工装组
+    @DeleteMapping("/suit/group/{boardId}/{sid}")
+    public Object removeSuitGroup(@PathVariable String boardId, @PathVariable int sid) {
+        AlgSuitGroupRemoveRequestDTO req = new AlgSuitGroupRemoveRequestDTO();
+        req.setBoardId(boardId);
+        req.setSid(sid);
+        return service.removeSuitGroup(req);
+    }
+
+    //上传工装模版
+    @PostMapping("/suit/append")
+    public Object appendSuitTemplate(@RequestBody AlgSuitAppendRequestDTO req) {
+        return service.appendSuitTemplate(req);
+    }
+
+    //删除工装模版
+    @DeleteMapping("/suit/remove/{boardId}/{sid}/{fid}")
+    public Object removeSuitTemplate(@PathVariable String boardId, @PathVariable int sid, @PathVariable int fid) {
+        AlgSuitRemoveRequestDTO req = new AlgSuitRemoveRequestDTO();
+        req.setBoardId(boardId);
+        req.setSid(sid);
+        req.setFid(fid);
+        return service.removeSuitTemplate(req);
+    }
+
+    //获取当前配置的模板信息
+    @GetMapping("/suit/fetch/{boardId}")
+    public Object fetchSuitGroup(@PathVariable String boardId) {
+        return service.fetchSuitGroup(boardId);
+    }
+
+}

+ 62 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/controller/web/EdgeMediaController.java

@@ -0,0 +1,62 @@
+package com.usky.ai.controller.web;
+
+import com.usky.ai.dto.*;
+import com.usky.ai.service.EdgeMediaService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/api/edge-media")
+@RequiredArgsConstructor
+public class EdgeMediaController {
+
+    private final EdgeMediaService service;
+
+    /**
+     * 新增/修改通道
+     */
+    @PostMapping("/channel")
+    public Object addChannel(@RequestBody EdgeAppControllerRequestDTO req) {
+        // 简单参数校验
+        if (req.getBoardId() == null || req.getMediaName() == null || req.getMediaUrl() == null) {
+            return "BoardId/MediaName/MediaUrl 不能为空";
+        }
+        EdgeAppControllerReplyDTO reply = service.addChannel(req);
+        if (reply.isSuccess()) {
+            // mediaRepository.save(req);
+            return "success";
+        }
+        return reply.getResult();
+    }
+
+
+    /**
+     * 查询通道
+     */
+    @GetMapping("/channels/{boardId}")
+    public Object fetchChannels(@PathVariable String boardId) {
+        EdgeAppFetchResponseDTO resp = service.fetchChannels(boardId);
+        if (resp.isSuccess()) {
+            return resp.getContent();
+        }
+        return resp.getResult();
+    }
+
+    /**
+     * 删除通道
+     */
+    @DeleteMapping("/delchannel/{boardId}/{mediaName}")
+    public Object deleteChannel(@PathVariable String boardId, @PathVariable String mediaName) {
+        EdgeAppDeleteResponseDTO resp = service.deleteChannel(boardId, mediaName);
+        return resp.isSuccess() ? "deleted" : resp.getResult();
+    }
+
+    @GetMapping("/algorithm/ability/{boardId}")
+    public Object fetchAlgorithmAbility(@PathVariable String boardId) {
+        AlgAbilityFetchResponseDTO resp = service.fetchAlgorithmAbility(boardId);
+        if (resp.isSuccess()) {
+            return resp.getAbility();
+        }
+        return resp.getResult();
+    }
+}

+ 59 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/controller/web/RepositoryController.java

@@ -0,0 +1,59 @@
+package com.usky.ai.controller.web;
+
+import com.usky.ai.dto.*;
+import com.usky.ai.service.EdgeMediaService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/api/repositories")
+@RequiredArgsConstructor
+public class RepositoryController {
+
+    private final EdgeMediaService service;
+
+    @GetMapping
+    public RepositoriesInfoResponseDTO listRepositories() {
+        return service.listRepositories();
+    }
+
+    @PostMapping("/create")
+    public RepositoryCreateResponseDTO createRepository(@RequestBody RepositoryCreateRequestDTO request) {
+        return service.createRepository(request);
+    }
+
+    @PostMapping("/update")
+    public RepositoryUpdateResponseDTO updateRepository(@RequestBody RepositoryUpdateRequestDTO request) {
+        return service.updateRepository(request);
+    }
+
+    @DeleteMapping("/delete")
+    public RepositoryDeleteResponseDTO deleteRepository(@RequestBody RepositoryDeleteRequestDTO request) {
+        return service.deleteRepository(request);
+    }
+
+    @PostMapping("/faces")
+    public RepositoryFacesResponseDTO getRepositoryFaces(@RequestBody RepositoryFacesRequestDTO request) {
+        return service.getRepositoryFaces(request);
+    }
+
+    @PostMapping("/register")
+    public RegisterFaceResponseDTO registerFace(@RequestBody RegisterFaceRequestDTO request) {
+        return service.registerFace(request);
+    }
+
+    @PostMapping("/update-face")
+    public UpdateFaceResponseDTO updateFace(@RequestBody UpdateFaceRequestDTO request) {
+        return service.updateFace(request);
+    }
+
+    @DeleteMapping("/delete-face")
+    public DeleteFaceResponseDTO deleteFace(@RequestBody DeleteFaceRequestDTO request) {
+        return service.deleteFace(request);
+    }
+
+    @PostMapping("/recognize")
+    public FaceRecognitionResponseDTO recognizeFace(@RequestBody FaceRecognitionRequestDTO request) {
+        return service.recognizeFace(request);
+    }
+}

+ 27 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/controller/web/TaskPreviewController.java

@@ -0,0 +1,27 @@
+package com.usky.ai.controller.web;
+
+import com.usky.ai.service.EdgeMediaService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+public class TaskPreviewController {
+
+    @Autowired
+    private EdgeMediaService edgeMediaService;
+
+    @GetMapping("/api/task/preview/{boardId}/{algTaskSession}")
+    public ResponseEntity<String> getTaskPreview(
+            @PathVariable String boardId,
+            @PathVariable String algTaskSession) {
+        try {
+            String imageUrl = edgeMediaService.getTaskPreviewUrl(boardId, algTaskSession);
+            return ResponseEntity.ok(imageUrl);
+        } catch (Exception e) {
+            return ResponseEntity.badRequest().body("Failed to get task preview: " + e.getMessage());
+        }
+    }
+}

+ 158 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/controller/web/UniversalUploadController.java

@@ -0,0 +1,158 @@
+package com.usky.ai.controller.web;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.usky.ai.domain.AiAlarmLog;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.client.RestTemplate;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.*;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.LocalDateTime;
+import org.springframework.http.*;
+import java.time.format.DateTimeFormatter;
+import java.util.Base64;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@RestController
+@RequestMapping("/upload")
+public class UniversalUploadController {
+
+    @Value("${file.domain}")
+    private String fileDomain;
+
+    @Value("${file.path}")
+    private String filePath;
+
+    @Value("${file.prefix}")
+    private String filePrefix;
+
+    @Value("${aiAlarmLog.api}")
+    private String alarmLogApi;
+
+    // 定义一个线程安全的计数器
+    private static final AtomicInteger counter = new AtomicInteger(0);
+
+    @PostMapping(value = "/anything", consumes = "*/*")
+    public String receiveAnything(HttpServletRequest request) {
+        try (BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()))) {
+            StringBuilder sb = new StringBuilder();
+            String line;
+            while ((line = reader.readLine()) != null) {
+                sb.append(line).append("\n");
+            }
+            String body = sb.toString();
+            System.out.println("Received raw data:");
+            System.out.println(body);
+
+            // 解析 JSON 数据
+            ObjectMapper objectMapper = new ObjectMapper();
+            JsonNode jsonNode = objectMapper.readTree(body);
+
+            // 创建告警日志对象
+            AiAlarmLog alarmLog = new AiAlarmLog();
+
+            // 设置字段
+            alarmLog.setDeviceId(getTextValue(jsonNode, "BoardId"));
+            alarmLog.setAlarmTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
+            alarmLog.setAlarmType("666");
+            alarmLog.setAlarmObject(getTextValue(jsonNode, "TaskSession"));
+            alarmLog.setAlarmData("0");
+            alarmLog.setAlarmAttribute(getTextValue(jsonNode, "Summary"));
+            alarmLog.setAlarmContent(getTextValue(jsonNode.get("Result"), "Description"));
+            alarmLog.setAlarmGrade(1);
+            alarmLog.setAlarmAddress("二楼研发中心");
+            alarmLog.setProductCode("513_202");
+
+
+            // 处理图片存储
+            String imageDataLabeled = getTextValue(jsonNode, "ImageDataLabeled");
+            if (!imageDataLabeled.isEmpty()) {
+                try {
+                    String imageUrl = saveImage(imageDataLabeled, "sitePhoto");
+                    alarmLog.setSitePhoto(imageUrl);
+                } catch (Exception e) {
+                    System.err.println("Failed to save image: " + e.getMessage());
+                    e.printStackTrace();
+                }
+            }
+
+            // 调用新增告警日志接口
+            if (!sendAlarmLog(alarmLog)) {
+                return "Failed to send alarm log";
+            }
+
+            return "Received data:\n" + body;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return "Error reading request body: " + e.getMessage();
+        }
+    }
+
+    // 添加辅助方法处理可能的空值
+    private String getTextValue(JsonNode node, String fieldName) {
+        JsonNode fieldNode = node.get(fieldName);
+        if (fieldNode != null && !fieldNode.isNull()) {
+            return fieldNode.asText();
+        }
+        return "";
+    }
+
+    // 保存图片并返回 URL
+    private String saveImage(String base64Image, String fileName) throws IOException {
+        // 检查 Base64 数据是否符合预期格式
+        byte[] imageBytes;
+
+        // 处理带有前缀的Base64数据 (如: data:image/jpeg;base64,/9j/...)
+        if (base64Image.contains(",")) {
+            String[] parts = base64Image.split(",", 2);
+            if (parts.length < 2) {
+                throw new IllegalArgumentException("Invalid Base64 image data format");
+            }
+            imageBytes = Base64.getDecoder().decode(parts[1]);
+        } else {
+            // 处理不带前缀的Base64数据 (如: /9j/...)
+            imageBytes = Base64.getDecoder().decode(base64Image);
+        }
+
+
+        // 生成文件路径 (使用yyyyMMddHHmmss格式)
+        LocalDateTime now = LocalDateTime.now();
+        String monthPath = now.format(DateTimeFormatter.ofPattern("yyyyMM"));
+        String formattedTime = now.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
+        int counterSuffix = counter.getAndIncrement(); // 获取并递增计数器
+        String filePathWithPrefix = filePath + "/" + monthPath + "/" + fileName + "_" + formattedTime + "_" + counterSuffix + ".jpg";
+        Path path = Paths.get(filePathWithPrefix);
+
+        // 保存图片到文件系统
+        Files.createDirectories(path.getParent());
+        Files.write(path, imageBytes);
+
+        // 返回可访问的 URL
+        return fileDomain + filePrefix + "/" + monthPath + "/" + fileName + "_" + formattedTime + "_" + counterSuffix + ".jpg";
+    }
+
+    // 调用新增告警日志接口
+    private boolean sendAlarmLog(AiAlarmLog alarmLog) {
+        RestTemplate restTemplate = new RestTemplate();
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_JSON);
+
+        HttpEntity<AiAlarmLog> request = new HttpEntity<>(alarmLog, headers);
+
+        try {
+            restTemplate.postForObject(alarmLogApi, request, String.class);
+            return true;
+        } catch (Exception e) {
+            System.err.println("Failed to send alarm log: " + e.getMessage());
+            e.printStackTrace();
+            return false;
+        }
+    }
+}

+ 104 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/domain/AiAlarmLog.java

@@ -0,0 +1,104 @@
+package com.usky.ai.domain;
+
+public class AiAlarmLog {
+    private String deviceId;
+    private String alarmTime;
+    private String alarmType;
+    private String alarmObject;
+    private String alarmData;
+    private String alarmAttribute;
+    private String alarmContent;
+    private Integer alarmGrade;
+    private String alarmAddress;
+    private String productCode;
+    private String sitePhoto;
+
+    // Getters and Setters
+    public String getDeviceId() {
+        return deviceId;
+    }
+
+    public void setDeviceId(String deviceId) {
+        this.deviceId = deviceId;
+    }
+
+    public String getAlarmTime() {
+        return alarmTime;
+    }
+
+    public void setAlarmTime(String alarmTime) {
+        this.alarmTime = alarmTime;
+    }
+
+    public String getAlarmType() {
+        return alarmType;
+    }
+
+    public void setAlarmType(String alarmType) {
+        this.alarmType = alarmType;
+    }
+
+    public String getAlarmObject() {
+        return alarmObject;
+    }
+
+    public void setAlarmObject(String alarmObject) {
+        this.alarmObject = alarmObject;
+    }
+
+    public String getAlarmData() {
+        return alarmData;
+    }
+
+    public void setAlarmData(String alarmData) {
+        this.alarmData = alarmData;
+    }
+
+    public String getAlarmAttribute() {
+        return alarmAttribute;
+    }
+
+    public void setAlarmAttribute(String alarmAttribute) {
+        this.alarmAttribute = alarmAttribute;
+    }
+
+    public String getAlarmContent() {
+        return alarmContent;
+    }
+
+    public void setAlarmContent(String alarmContent) {
+        this.alarmContent = alarmContent;
+    }
+
+    public Integer getAlarmGrade() {
+        return alarmGrade;
+    }
+
+    public void setAlarmGrade(Integer alarmGrade) {
+        this.alarmGrade = alarmGrade;
+    }
+
+    public String getAlarmAddress() {
+        return alarmAddress;
+    }
+
+    public void setAlarmAddress(String alarmAddress) {
+        this.alarmAddress = alarmAddress;
+    }
+
+    public String getProductCode() {
+        return productCode;
+    }
+
+    public void setProductCode(String productCode) {
+        this.productCode = productCode;
+    }
+
+    public String getSitePhoto() {
+        return sitePhoto;
+    }
+
+    public void setSitePhoto(String sitePhoto) {
+        this.sitePhoto = sitePhoto;
+    }
+}

+ 83 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/domain/AiDevice.java

@@ -0,0 +1,83 @@
+package com.usky.ai.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+
+import java.sql.Timestamp;
+
+public class AiDevice {
+    private String boardId;
+    private String boardIp;
+    private String description;
+    private Integer status;
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Timestamp createTime;
+    private String createBy;
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Timestamp updateTime;
+    private String updateBy;
+
+    // Getters and Setters
+    public String getBoardId() {
+        return boardId;
+    }
+
+    public void setBoardId(String boardId) {
+        this.boardId = boardId;
+    }
+
+    public String getBoardIp() {
+        return boardIp;
+    }
+
+    public void setBoardIp(String boardIp) {
+        this.boardIp = boardIp;
+    }
+
+    public Integer getStatus() {
+        return status;
+    }
+
+    public void setStatus(Integer status) {
+        this.status = status;
+    }
+
+    public String getDescription() { // 新增 getter
+        return description;
+    }
+
+    public void setDescription(String description) { // 新增 setter
+        this.description = description;
+    }
+
+    public Timestamp getCreateTime() {
+        return createTime;
+    }
+
+    public void setCreateTime(Timestamp createTime) {
+        this.createTime = createTime;
+    }
+
+    public String getCreateBy() {
+        return createBy;
+    }
+
+    public void setCreateBy(String createBy) {
+        this.createBy = createBy;
+    }
+
+    public Timestamp getUpdateTime() {
+        return updateTime;
+    }
+
+    public void setUpdateTime(Timestamp updateTime) {
+        this.updateTime = updateTime;
+    }
+
+    public String getUpdateBy() {
+        return updateBy;
+    }
+
+    public void setUpdateBy(String updateBy) {
+        this.updateBy = updateBy;
+    }
+}

+ 1 - 1
service-ai/service-ai-biz/src/main/java/com/usky/ai/service/AiQuestion.java → service-ai/service-ai-biz/src/main/java/com/usky/ai/domain/AiQuestion.java

@@ -1,4 +1,4 @@
-package com.usky.ai.service;
+package com.usky.ai.domain;
 
 import java.time.LocalDateTime;
 

+ 1 - 1
service-ai/service-ai-biz/src/main/java/com/usky/ai/service/AiQuestionItem.java → service-ai/service-ai-biz/src/main/java/com/usky/ai/domain/AiQuestionItem.java

@@ -1,4 +1,4 @@
-package com.usky.ai.service;
+package com.usky.ai.domain;
 
 import java.time.LocalDateTime;
 

+ 1 - 1
service-ai/service-ai-biz/src/main/java/com/usky/ai/service/AiSession.java → service-ai/service-ai-biz/src/main/java/com/usky/ai/domain/AiSession.java

@@ -1,4 +1,4 @@
-package com.usky.ai.service;
+package com.usky.ai.domain;
 
 import java.time.LocalDateTime;
 import java.util.List;

+ 13 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgAbilityFetchRequestDTO.java

@@ -0,0 +1,13 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class AlgAbilityFetchRequestDTO {
+    @JsonProperty("BoardId")
+    private String boardId;
+
+    @JsonProperty("Event")
+    private String event = "/alg_ability_fetch"; // 固定值
+}

+ 131 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgAbilityFetchResponseDTO.java

@@ -0,0 +1,131 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class AlgAbilityFetchResponseDTO {
+    @JsonProperty("BoardId")
+    private String boardId;
+
+    @JsonProperty("BoardIp")
+    private String boardIp;
+
+    @JsonProperty("Ability")
+    private List<Algorithm> ability;
+
+    @JsonProperty("Result")
+    private Result result;
+
+    private long time = System.currentTimeMillis();
+
+    public boolean isSuccess() {
+        return result != null && Integer.valueOf(0).equals(result.getCode());
+    }
+
+    @Data
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class Algorithm {
+        @JsonProperty("attribute")
+        private JsonNode attribute;
+
+        @JsonProperty("parameters")
+        private List<Parameter> parameters;
+
+        @JsonProperty("permitted")
+        private boolean permitted;
+
+        @JsonProperty("code")
+        private int code;
+
+        @JsonProperty("sub")
+        private boolean sub;
+
+        @JsonProperty("name")
+        private String name;
+
+        @JsonProperty("desc")
+        private String desc;
+
+        @JsonProperty("item")
+        private int item;
+
+        @JsonProperty("policy")
+        private List<Policy> policy;
+
+        @Data
+        @JsonIgnoreProperties(ignoreUnknown = true)
+        public static class Parameter {
+            @JsonProperty("class")
+            private String classType;
+
+            @JsonProperty("type")
+            private int type;
+
+            @JsonProperty("max")
+            private Object max;
+
+            @JsonProperty("min")
+            private Object min;
+
+            @JsonProperty("default")
+            private Object defaultValue;
+
+            @JsonProperty("key")
+            private String key;
+
+            @JsonProperty("name")
+            private String name;
+
+            @JsonProperty("required")
+            private boolean required;
+
+            @JsonProperty("value")
+            private Object value;
+
+            @JsonProperty("options")
+            private List<Option> options;
+
+            @Data
+            @JsonIgnoreProperties(ignoreUnknown = true)
+            public static class Option {
+                @JsonProperty("enable")
+                private boolean enable;
+
+                @JsonProperty("key")
+                private String key;
+
+                @JsonProperty("value")
+                private int value;
+
+                @JsonProperty("name")
+                private String name;
+            }
+        }
+
+        @Data
+        @JsonIgnoreProperties(ignoreUnknown = true)
+        public static class Policy {
+            @JsonProperty("property")
+            private String property;
+
+            @JsonProperty("name")
+            private String name;
+        }
+    }
+
+    @Data
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class Result {
+        @JsonProperty("Code")
+        private int code;
+
+        @JsonProperty("Desc")
+        private String desc;
+    }
+}

+ 80 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgScheduleCreateRequestDTO.java

@@ -0,0 +1,80 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@NoArgsConstructor
+public class AlgScheduleCreateRequestDTO {
+    @JsonProperty("BoardId")
+    private String boardId;
+
+    @JsonProperty("Event")
+    private String event = "/alg_schedule_create";
+
+    @JsonProperty("name")
+    private String name;
+
+    @JsonProperty("summary")
+    private String summary;
+
+    @JsonProperty("timePeriods")
+    private List<TimePeriod> timePeriods;
+
+    @JsonProperty("value")
+    private String value;
+
+    public void convertTimePeriodsToBinary() {
+        StringBuilder binaryValue = new StringBuilder();
+        for (int i = 0; i < 336; i++) {
+            binaryValue.append("0");
+        }
+
+        for (TimePeriod period : timePeriods) {
+            processTimePeriod(binaryValue, period);
+        }
+
+        this.value = binaryValue.toString();
+    }
+
+    private void processTimePeriod(StringBuilder binaryValue, TimePeriod period) {
+        // 将 dayOfWeek 转换为从 0 开始的索引
+        int startDay = period.getDayOfWeek() - 1;
+        int endDay = startDay; // 默认结束天数与开始天数相同
+        if (period.getToDayOfWeek() != null) {
+            endDay = period.getToDayOfWeek() - 1; // 如果有结束天数,也转换为从 0 开始的索引
+        }
+
+        // 计算开始和结束时间的分钟数
+        int startMinutes = Integer.parseInt(period.getStartTime().substring(0, 2)) * 60 + Integer.parseInt(period.getStartTime().substring(3));
+        int endMinutes = Integer.parseInt(period.getEndTime().substring(0, 2)) * 60 + Integer.parseInt(period.getEndTime().substring(3));
+
+        // 计算开始和结束时间的索引
+        int startIdx = (startDay * 48) + (startMinutes / 30);
+        int endIdx = (endDay * 48) + (endMinutes / 30);
+
+        // 填充时间段
+        for (int i = startIdx; i < endIdx; i++) {
+            binaryValue.setCharAt(i, '1');
+        }
+    }
+
+    @Data
+    @NoArgsConstructor
+    public static class TimePeriod {
+        @JsonProperty("dayOfWeek")
+        private int dayOfWeek; // 0-6 表示周日到周六
+
+        @JsonProperty("toDayOfWeek")
+        private Integer toDayOfWeek; // 可选,表示连续的结束星期几
+
+        @JsonProperty("startTime")
+        private String startTime; // 格式为 "HH:mm"
+
+        @JsonProperty("endTime")
+        private String endTime; // 格式为 "HH:mm"
+    }
+}

+ 25 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgScheduleCreateResponseDTO.java

@@ -0,0 +1,25 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class AlgScheduleCreateResponseDTO {
+
+    @JsonProperty("BoardIp")
+    private String boardIp;
+
+    @JsonProperty("BoardId")
+    private String boardId;
+
+    @JsonProperty("ScheduleId")
+    private String scheduleId;
+
+    @JsonProperty("Event")
+    private String event = "/alg_schedule_create";
+
+    private long time = System.currentTimeMillis();
+
+}

+ 16 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgScheduleDeleteRequestDTO.java

@@ -0,0 +1,16 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class AlgScheduleDeleteRequestDTO {
+    @JsonProperty("BoardId")
+    private String boardId;
+
+    @JsonProperty("Event")
+    private String event = "/alg_schedule_delete";
+
+    @JsonProperty("id")
+    private Long id;
+}

+ 40 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgScheduleDeleteResponseDTO.java

@@ -0,0 +1,40 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class AlgScheduleDeleteResponseDTO {
+    @JsonProperty("BoardIp")
+    private String boardIp;
+
+    @JsonProperty("BoardId")
+    private String boardId;
+
+    @JsonProperty("Event")
+    private String event = "/alg_schedule_delete";
+
+    @JsonProperty("Result")
+    private Result result;
+
+    @JsonProperty("ScheduleId")
+    private Long scheduleId;
+
+    public boolean isSuccess() {
+        return result != null && result.getCode() == 0;
+    }
+
+    private long time = System.currentTimeMillis();
+
+    @Data
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class Result {
+        @JsonProperty("Code")
+        private int code;
+
+        @JsonProperty("Desc")
+        private String desc;
+    }
+}

+ 13 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgScheduleFetchRequestDTO.java

@@ -0,0 +1,13 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class AlgScheduleFetchRequestDTO {
+    @JsonProperty("BoardId")
+    private String boardId;
+
+    @JsonProperty("Event")
+    private String event = "/alg_schedule_fetch";
+}

+ 113 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgScheduleFetchResponseDTO.java

@@ -0,0 +1,113 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class AlgScheduleFetchResponseDTO {
+    @JsonProperty("BoardIp")
+    private String boardIp;
+
+    @JsonProperty("BoardId")
+    private String boardId;
+
+    @JsonProperty("Event")
+    private String event = "/alg_schedule_fetch";
+
+    @JsonProperty("Result")
+    private Result result;
+
+    @JsonProperty("Content")
+    private List<Schedule> content;
+
+    public boolean isSuccess() {
+        return result != null && result.getCode() == 0;
+    }
+
+    private long time = System.currentTimeMillis();
+
+    @Data
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class Result {
+        @JsonProperty("Code")  //0成功/其他失败
+        private int code;
+
+        @JsonProperty("Content")
+        private List<Schedule> content; // Content 属性在 Result 中
+
+        @JsonProperty("Desc")
+        private String desc;
+    }
+
+    @Data
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class Schedule {
+        @JsonProperty("Id")
+        private int id;
+
+        @JsonProperty("Name")
+        private String name;
+
+        @JsonProperty("Summary")
+        private String summary;
+
+        @JsonProperty("Value")
+        private String value;
+
+        public List<String> getReadableTimePeriods() {
+            List<String> timePeriods = new ArrayList<>();
+            char[] binaryValue = value.toCharArray();
+            String[] days = {"星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日",};
+
+            // 检查 Value 字段的长度是否正确
+            if (binaryValue.length != 336) {
+                //System.err.println("Value 字段长度不正确,预期长度为 336,实际长度为 " + binaryValue.length);
+                return timePeriods; // 如果长度不足,返回空列表
+            }
+
+            for (int day = 0; day < 7; day++) {
+                StringBuilder dayPeriods = new StringBuilder();
+                boolean inPeriod = false;
+                int startHour = -1;
+                int startMinute = -1;
+
+                for (int halfHour = 0; halfHour < 48; halfHour++) {
+                    int hour = halfHour / 2;
+                    int minute = (halfHour % 2) * 30;
+                    int index = day * 48 + halfHour;
+
+                    if (index < binaryValue.length && binaryValue[index] == '1') {
+                        if (!inPeriod) {
+                            inPeriod = true;
+                            startHour = hour;
+                            startMinute = minute;
+                        }
+                    } else {
+                        if (inPeriod) {
+                            inPeriod = false;
+                            dayPeriods.append(String.format("%02d:%02d~%02d:%02d", startHour, startMinute, hour, minute)).append(", ");
+                        }
+                    }
+                }
+
+                if (inPeriod) {
+                    int hour = 23;
+                    int minute = 30;
+                    dayPeriods.append(String.format("%02d:%02d~%02d:%02d", startHour, startMinute, hour, minute)).append(", ");
+                }
+
+                if (dayPeriods.length() > 0) {
+                    timePeriods.add(days[day] + " " + dayPeriods.toString().trim().replaceAll(", $", ""));
+                }
+            }
+
+            System.out.println("Readable time periods: " + timePeriods);
+            return timePeriods;
+        }
+    }
+}

+ 20 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgSuitAppendRequestDTO.java

@@ -0,0 +1,20 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class AlgSuitAppendRequestDTO {
+
+    @JsonProperty("BoardId")
+    private String boardId;
+
+    @JsonProperty("Event")
+    private String event = "/alg_suit_append";
+
+    @JsonProperty("sid")
+    private int sid;
+
+    @JsonProperty("jpeg")
+    private String jpeg;
+}

+ 36 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgSuitAppendResponseDTO.java

@@ -0,0 +1,36 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class AlgSuitAppendResponseDTO {
+    @JsonProperty("BoardIp")
+    private String boardIp;
+
+    @JsonProperty("BoardId")
+    private String boardId;
+
+    @JsonProperty("Event")
+    private String event;
+
+    @JsonProperty("Result")
+    private Result result;
+
+    private long time = System.currentTimeMillis();
+
+    @Data
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class Result {
+        @JsonProperty("Code")
+        private int code;
+
+        @JsonProperty("Desc")
+        private String desc;
+
+        @JsonProperty("FeatureId")
+        private Integer featureId;
+    }
+}

+ 16 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgSuitCreateRequestDTO.java

@@ -0,0 +1,16 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class AlgSuitCreateRequestDTO {
+    @JsonProperty("BoardId")
+    private String boardId;
+
+    @JsonProperty("Event")
+    private String event = "/alg_suit_create";
+
+    @JsonProperty("name")
+    private String name;
+}

+ 36 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgSuitCreateResponseDTO.java

@@ -0,0 +1,36 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class AlgSuitCreateResponseDTO {
+    @JsonProperty("BoardIp")
+    private String boardIp;
+
+    @JsonProperty("BoardId")
+    private String boardId;
+
+    @JsonProperty("Event")
+    private String event;
+
+    @JsonProperty("Result")
+    private Result result;
+
+    private long time = System.currentTimeMillis();
+
+    @Data
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class Result {
+        @JsonProperty("Code")
+        private int code;
+
+        @JsonProperty("Desc")
+        private String desc;
+
+        @JsonProperty("SuitId")
+        private Integer suitId;
+    }
+}

+ 13 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgSuitFetchRequestDTO.java

@@ -0,0 +1,13 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class AlgSuitFetchRequestDTO {
+    @JsonProperty("BoardId")
+    private String boardId;
+
+    @JsonProperty("Event")
+    private String event = "/alg_suit_fetch";
+}

+ 65 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgSuitFetchResponseDTO.java

@@ -0,0 +1,65 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class AlgSuitFetchResponseDTO {
+    @JsonProperty("BoardIp")
+    private String boardIp;
+
+    @JsonProperty("BoardId")
+    private String boardId;
+
+    @JsonProperty("Event")
+    private String event;
+
+    @JsonProperty("Result")
+    private Result result;
+
+    @Data
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class Result {
+        @JsonProperty("Code")
+        private int code;
+
+        @JsonProperty("Desc")
+        private String desc;
+
+        @JsonProperty("Content")
+        private List<SuitGroup> content;
+    }
+
+    @Data
+    public static class SuitGroup {
+        @JsonProperty("id")
+        private int id;
+
+        @JsonProperty("name")
+        private String name;
+
+        @JsonProperty("features")
+        private List<Feature> features;
+    }
+
+    @Data
+    public static class Feature {
+        @JsonProperty("feature")
+        private String feature;
+
+        @JsonProperty("fid")
+        private int fid;
+
+        @JsonProperty("jpeg")
+        private String jpeg;
+
+        @JsonProperty("size")
+        private int size;
+    }
+
+    private long time = System.currentTimeMillis();
+}

+ 18 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgSuitGroupRemoveRequestDTO.java

@@ -0,0 +1,18 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class AlgSuitGroupRemoveRequestDTO {
+
+    @JsonProperty("BoardId")
+    private String boardId;
+
+    @JsonProperty("Event")
+    private String event = "/alg_suit_group_remove";
+
+    @JsonProperty("sid")
+    private int sid;
+}

+ 33 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgSuitGroupRemoveResponseDTO.java

@@ -0,0 +1,33 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class AlgSuitGroupRemoveResponseDTO {
+    @JsonProperty("BoardIp")
+    private String boardIp;
+
+    @JsonProperty("BoardId")
+    private String boardId;
+
+    @JsonProperty("Event")
+    private String event;
+
+    @JsonProperty("Result")
+    private Result result;
+
+    private long time = System.currentTimeMillis();
+
+    @Data
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class Result {
+        @JsonProperty("Code")
+        private int code;
+
+        @JsonProperty("Desc")
+        private String desc;
+    }
+}

+ 20 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgSuitRemoveRequestDTO.java

@@ -0,0 +1,20 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class AlgSuitRemoveRequestDTO {
+
+    @JsonProperty("BoardId")
+    private String boardId;
+
+    @JsonProperty("Event")
+    private String event = "/alg_suit_remove";
+
+    @JsonProperty("sid")
+    private int sid;
+
+    @JsonProperty("fid")
+    private int fid;
+}

+ 33 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgSuitRemoveResponseDTO.java

@@ -0,0 +1,33 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class AlgSuitRemoveResponseDTO {
+    @JsonProperty("BoardIp")
+    private String boardIp;
+
+    @JsonProperty("BoardId")
+    private String boardId;
+
+    @JsonProperty("Event")
+    private String event;
+
+    @JsonProperty("Result")
+    private Result result;
+
+    private long time = System.currentTimeMillis();
+
+    @Data
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class Result {
+        @JsonProperty("Code")
+        private int code;
+
+        @JsonProperty("Desc")
+        private String desc;
+    }
+}

+ 16 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgTaskSnapRequestDTO.java

@@ -0,0 +1,16 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class AlgTaskSnapRequestDTO {
+    @JsonProperty("BoardId")
+    private String boardId;
+
+    @JsonProperty("Event")
+    private String event = "/alg_task_snap";
+
+    @JsonProperty("AlgTaskSession")
+    private String algTaskSession;
+}

+ 43 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/AlgTaskSnapResponseDTO.java

@@ -0,0 +1,43 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class AlgTaskSnapResponseDTO {
+    @JsonProperty("BoardIp")
+    private String boardIp;
+
+    @JsonProperty("BoardId")
+    private String boardId;
+
+    @JsonProperty("Event")
+    private String event;
+
+    @JsonProperty("Result")
+    private Result result;
+
+    @JsonProperty("AlgTaskSession")
+    private String algTaskSession;
+
+    @JsonProperty("Base64")
+    private String base64;
+
+    public boolean isSuccess() {
+        return result != null && Integer.valueOf(0).equals(result.getCode());
+    }
+
+    @Data
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class Result {
+        @JsonProperty("Code")
+        private int code;
+
+        @JsonProperty("Desc")
+        private String desc;
+    }
+
+    private long time = System.currentTimeMillis();
+}

+ 109 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/BoardPingDTO.java

@@ -0,0 +1,109 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.Data;
+
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class BoardPingDTO {
+    //当前盒子唯一表示
+    @JsonProperty("BoardId")
+    private String boardId;
+
+    //当前ip
+    @JsonProperty("BoardIp")
+    private String boardIp;
+
+    //当前设备硬件类型
+    @JsonProperty("BoardPlatform")
+    private String boardPlatform;
+
+    //当前设备芯片温度
+    @JsonProperty("BoardTemp")
+    private String boardTemp;
+
+    //当前设备接入方式 WAN/4G
+    @JsonProperty("BoardType")
+    private String boardType;
+
+    //GB28181 设备号配置后透传
+    @JsonProperty("GBClientId")
+    private String gbClientId;
+
+    @JsonProperty("GrantCode")
+    private String grantCode;
+
+    @JsonProperty("GrantDesc")
+    private String grantDesc;
+
+    //当前设备状态
+    @JsonProperty("Key")
+    private String key;
+
+    //GPS 信息 部分设备支持
+    @JsonProperty("Latitude")
+    private String latitude;
+
+    //GPS 信息 部分设备支持
+    @JsonProperty("Longitude")
+    private String longitude;
+
+    // 当前应用VmRSS信息
+    @JsonProperty("Malloc")
+    private String malloc;
+
+    //本次应用PID
+    @JsonProperty("Pid")
+    private String pid;
+
+    //特定设备时透传主控端IP
+    @JsonProperty("Se6ip")
+    private String se6ip;
+
+    @JsonProperty("Status")
+    private String status;
+
+    //当前设备系统版本
+    @JsonProperty("System")
+    private String system;
+
+    //当前时间戳毫秒
+    @JsonProperty("Time")
+    private Long time;
+
+    //软件版本
+    @JsonProperty("Version")
+    private String version;
+
+    //GPS 信息 部分设备支持
+    @JsonProperty("angleCourse")
+    private String angleCourse;
+    @JsonProperty("kSpeed")
+    private String kSpeed;
+    @JsonProperty("nSpeed")
+    private String nSpeed;
+
+
+
+    //当前设备硬盘情况kB
+    @JsonProperty("HostDisk")
+    private JsonNode hostDisk;
+
+    //当前设备内存使用情况
+    @JsonProperty("HostMemory")
+    private JsonNode hostMemory;
+
+    //当前设备添加的通道信息
+    @JsonProperty("Medias")
+    private JsonNode medias;
+
+    //当前设备配置的任务信息
+    @JsonProperty("Tasks")
+    private JsonNode tasks;
+
+    //当前设备的算力资源使用情况
+    @JsonProperty("Tpu")
+    private JsonNode tpu;
+}

+ 13 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/DeleteFaceRequestDTO.java

@@ -0,0 +1,13 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class DeleteFaceRequestDTO {
+    @JsonProperty("albumName")
+    private String albumName;
+
+    @JsonProperty("photoId")
+    private int photoId;
+}

+ 34 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/DeleteFaceResponseDTO.java

@@ -0,0 +1,34 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class DeleteFaceResponseDTO {
+    @JsonProperty("BoardId")
+    private String boardId;
+
+    @JsonProperty("BoardIp")
+    private String boardIp;
+
+    @JsonProperty("Event")
+    private String event;
+
+    @JsonProperty("Result")
+    private Result result;
+
+    @JsonProperty("albumId")
+    private int albumId;
+
+    @JsonProperty("photoId")
+    private int photoId;
+
+    @Data
+    public static class Result {
+        @JsonProperty("Code")
+        private Integer code;
+
+        @JsonProperty("Desc")
+        private String desc;
+    }
+}

+ 14 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/EdgeAlgTaskCommandRequestDTO.java

@@ -0,0 +1,14 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class EdgeAlgTaskCommandRequestDTO {
+    @JsonProperty("BoardId") private String boardId;
+    @JsonProperty("Event") private String event; // /alg_task_control 或 /alg_task_delete
+    @JsonProperty("ControlCommand") private Integer controlCommand; // 0 停止 1 启动
+    @JsonProperty("AlgTaskSession") private String algTaskSession;
+}

+ 82 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/EdgeAlgTaskConfigRequestDTO.java

@@ -0,0 +1,82 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class EdgeAlgTaskConfigRequestDTO {
+    @JsonProperty("BoardId")
+    private String boardId;
+    @JsonProperty("Event")
+    private String event = "/alg_task_config";
+    @JsonProperty("AlgTaskSession")
+    private String algTaskSession;
+    @JsonProperty("TaskDesc")
+    private String taskDesc;
+    @JsonProperty("AlgInfo")
+    private List<Integer> algInfo;
+    @JsonProperty("MediaName")
+    private String mediaName;
+    @JsonProperty("MetadataUrl")
+    private List<String> metadataUrl;
+    @JsonProperty("ScheduleId")
+    private Integer scheduleId = -1;
+    @JsonProperty("UserData")
+    private UserData userData;
+    @JsonProperty("RuleProperty")
+    private List<RuleProperty> ruleProperty;
+    @JsonProperty("Template")
+    private String template = "";
+    @JsonProperty("GB28181Channel")
+    private String gb28181Channel = "";
+    @JsonProperty("AlarmProtocol")
+    private Integer alarmProtocol = 0;
+    @JsonProperty("AlarmBody")
+    private Integer alarmBody = 0;
+    @JsonProperty("Restart")
+    private Boolean restart = true;
+
+    @Data
+    public static class UserData {
+        @JsonProperty("MethodConfig")
+        private List<Integer> methodConfig;
+
+        /* 离岗算法业务字段 */
+        @JsonProperty("staff_sec")
+        private Integer staffSec;
+        @JsonProperty("staff_number")
+        private Integer staffNumber;
+    }
+
+    @Data
+    public static class RuleProperty {
+        @JsonProperty("Algo")
+        private Algo algo;
+        @JsonProperty("Points")
+        private List<Point> points;
+        @JsonProperty("RuleId")
+        private String ruleId;
+        @JsonProperty("RuleType")
+        private Integer ruleType;
+
+        @Data
+        public static class Algo {
+            @JsonProperty("majorId")
+            private Integer majorId;
+            @JsonProperty("minorId")
+            private Integer minorId;
+        }
+
+        @Data
+        public static class Point {
+            @JsonProperty("X")
+            private Double x;
+            @JsonProperty("Y")
+            private Double y;
+        }
+    }
+}

+ 15 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/EdgeAlgTaskFetchRequestDTO.java

@@ -0,0 +1,15 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class EdgeAlgTaskFetchRequestDTO {
+    @JsonProperty("BoardId")
+    private String boardId;
+
+    @JsonProperty("Event")
+    private String event = "/alg_task_fetch";
+}

+ 177 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/EdgeAlgTaskListResponseDTO.java

@@ -0,0 +1,177 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class EdgeAlgTaskListResponseDTO {
+
+    @JsonProperty("BoardId")
+    private String boardId;
+
+    @JsonProperty("BoardIp")
+    private String boardIp;
+
+    @JsonProperty("Content")
+    private List<TaskItem> content;
+
+    @JsonProperty("Event")
+    private String event;
+
+    @JsonProperty("Result")
+    private Result result;
+
+    public boolean isSuccess() {
+        return result != null && Integer.valueOf(0).equals(result.getCode());
+    }
+
+
+    @Data
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class TaskItem {
+        @JsonProperty("AlgTaskSession")
+        private String algTaskSession;
+
+        @JsonProperty("MediaName")
+        private String mediaName;
+
+        @JsonProperty("AlgTaskStatus")
+        private AlgTaskStatus algTaskStatus;
+
+        @JsonProperty("AlarmBody")
+        private Integer alarmBody;
+
+        @JsonProperty("AlarmProtocol")
+        private Integer alarmProtocol;
+
+        @JsonProperty("MetadataUrl")
+        private String metadataUrl;
+
+        @JsonProperty("TaskDesc")
+        private String taskDesc;
+
+        @JsonProperty("UserData")
+        private UserData userData;
+
+        @JsonProperty("AlgInfo")
+        private List<Integer> algInfo;
+
+        @JsonProperty("GB28181Channel")
+        private String gb28181Channel;
+
+        @JsonProperty("Template")
+        private String template;
+
+        @JsonProperty("Restart")
+        private Boolean restart;
+
+        @JsonProperty("RuleProperty")
+        private List<RuleProperty> ruleProperty;
+
+        @JsonProperty("BaseAlgItem")
+        private List<BaseAlgItem> baseAlgItem;
+    }
+
+    @Data
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class AlgTaskStatus {
+        @JsonProperty("label")
+        private String label;
+
+        @JsonProperty("style")
+        private String style;
+
+        @JsonProperty("type")
+        private Integer type;
+    }
+
+    @Data
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class UserData {
+        @JsonProperty("MethodConfig")
+        private List<Integer> methodConfig;
+    }
+
+    @Data
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class RuleProperty {
+        @JsonProperty("RuleId")
+        private String ruleId;
+
+        @JsonProperty("RuleType")
+        private Integer ruleType;
+
+        @JsonProperty("RuleTypeName")
+        private String ruleTypeName;
+
+        @JsonProperty("Algo")
+        private Algo algo;
+
+        @JsonProperty("Points")
+        private List<Point> points;
+
+        @Data
+        @JsonIgnoreProperties(ignoreUnknown = true)
+        public static class Algo {
+            @JsonProperty("majorId")
+            private Integer majorId;
+
+            @JsonProperty("minorId")
+            private Integer minorId;
+        }
+
+        @Data
+        @JsonIgnoreProperties(ignoreUnknown = true)
+        public static class Point {
+            @JsonProperty("X")
+            private Double x;
+
+            @JsonProperty("Y")
+            private Double y;
+        }
+    }
+
+    @Data
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class BaseAlgItem {
+        @JsonProperty("lineDesc")
+        private String lineDesc;
+
+        @JsonProperty("lineRequired")
+        private Boolean lineRequired;
+
+        @JsonProperty("majorId")
+        private Integer majorId;
+
+        @JsonProperty("majorOnly")
+        private Boolean majorOnly;
+
+        @JsonProperty("minorId")
+        private Integer minorId;
+
+        @JsonProperty("name")
+        private String name;
+
+        @JsonProperty("zoneDesc")
+        private String zoneDesc;
+
+        @JsonProperty("zoneRequired")
+        private Boolean zoneRequired;
+    }
+
+    @Data
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class Result {
+        @JsonProperty("Code")
+        private Integer code;
+
+        @JsonProperty("Desc")
+        private String desc;
+    }
+
+    private long time = System.currentTimeMillis();
+}

+ 34 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/EdgeAlgTaskResponseDTO.java

@@ -0,0 +1,34 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class EdgeAlgTaskResponseDTO {
+    @JsonProperty("BoardId")
+    private String boardId;
+    @JsonProperty("BoardIp")
+    private String boardIp;
+    @JsonProperty("Event")
+    private String event;
+    @JsonProperty("AlgTaskSession")
+    private String algTaskSession;
+    @JsonProperty("Result")
+    private Result result;
+
+    @Data
+    public static class Result {
+        @JsonProperty("Code")
+        private Integer code;
+        @JsonProperty("Desc")
+        private String desc;
+    }
+
+    public boolean isSuccess() {
+        return result != null && Integer.valueOf(0).equals(result.code);
+    }
+
+    private long time = System.currentTimeMillis();
+}

+ 32 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/EdgeAppControllerReplyDTO.java

@@ -0,0 +1,32 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class EdgeAppControllerReplyDTO {
+    @JsonProperty("BoardId")
+    private String boardId;
+    @JsonProperty("BoardIp")
+    private String boardIp;
+    @JsonProperty("Event")
+    private String event = "/alg_media_config"; // 固定值
+    @JsonProperty("MediaName")
+    private String mediaName;
+    @JsonProperty("Result")
+    private Result result;
+
+    private long time = System.currentTimeMillis();
+
+    @Data
+    public static class Result {
+        @JsonProperty("Code")
+        private Integer code;
+        @JsonProperty("Desc")
+        private String desc;
+    }
+
+    public boolean isSuccess() {
+        return result != null && Integer.valueOf(0).equals(result.code);
+    }
+}

+ 42 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/EdgeAppControllerRequestDTO.java

@@ -0,0 +1,42 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class EdgeAppControllerRequestDTO {
+
+    /** 与文档一致,首字母大写以兼容盒子 */
+    @JsonProperty("BoardId")
+    private String boardId;
+    @JsonProperty("Event")
+    private String event;
+    @JsonProperty("MediaName")
+    private String mediaName;
+    @JsonProperty("MediaUrl")
+    private String mediaUrl;
+    @JsonProperty("MediaDesc")
+    private String mediaDesc;
+    @JsonProperty("RtspTransport")
+    private Boolean rtspTransport;
+    @JsonProperty("GBTransport")
+    private Boolean gbTransport;
+    @JsonProperty("SubId")
+    private String subId;
+    @JsonProperty("Params")
+    private List<Param> params;
+
+    @Data
+    public static class Param {
+        @JsonProperty("Key")
+        private String key;
+        @JsonProperty("Name")
+        private String name;
+        @JsonProperty("Type")
+        private String type;
+        @JsonProperty("Value")
+        private String value;
+    }
+}

+ 16 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/EdgeAppDeleteRequestDTO.java

@@ -0,0 +1,16 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class EdgeAppDeleteRequestDTO {
+    @JsonProperty("BoardId")
+    private String boardId;
+
+    @JsonProperty("Event")
+    private String event = "/alg_media_delete"; // 固定值
+
+    @JsonProperty("MediaName")
+    private String mediaName;
+}

+ 37 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/EdgeAppDeleteResponseDTO.java

@@ -0,0 +1,37 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class EdgeAppDeleteResponseDTO {
+    @JsonProperty("BoardId")
+    private String boardId;
+
+    @JsonProperty("BoardIp")
+    private String boardIp;
+
+    @JsonProperty("Event")
+    private String event;
+
+    @JsonProperty("MediaName")
+    private String mediaName;
+
+    @JsonProperty("Result")
+    private Result result;
+
+    private long time = System.currentTimeMillis();
+
+    @Data
+    public static class Result {
+        @JsonProperty("Code")
+        private Integer code;
+
+        @JsonProperty("Desc")
+        private String desc;
+    }
+
+    public boolean isSuccess() {
+        return result != null && Integer.valueOf(0).equals(result.code);
+    }
+}

+ 116 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/EdgeAppFetchResponseDTO.java

@@ -0,0 +1,116 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class EdgeAppFetchResponseDTO {
+    @JsonProperty("BoardId")
+    private String boardId;
+
+    @JsonProperty("BoardIp")
+    private String boardIp;
+
+    @JsonProperty("Content")
+    private List<MediaContent> content;
+
+    @JsonProperty("Event")
+    private String event = "/alg_media_fetch"; // 固定值
+
+    @JsonProperty("Result")
+    private Result result;
+
+    private long time = System.currentTimeMillis();
+
+    public boolean isSuccess() {
+        return result != null && Integer.valueOf(0).equals(result.getCode());
+    }
+
+    @Data
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class Result {
+        @JsonProperty("Code")
+        private Integer code;
+
+        @JsonProperty("Desc")
+        private String desc;
+    }
+
+    @Data
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class MediaContent {
+        @JsonProperty("MediaName")
+        private String mediaName;
+
+        @JsonProperty("MediaDesc")
+        private String mediaDesc;
+
+        @JsonProperty("MediaUrl")
+        private String mediaUrl;
+
+        @JsonProperty("RtspTransport")
+        private Boolean rtspTransport;
+
+        @JsonProperty("GBTransport")
+        private Boolean gbTransport;
+
+        @JsonProperty("SubId")
+        private String subId;
+
+        @JsonProperty("MediaStatus")
+        private MediaStatus mediaStatus;
+
+        @JsonProperty("Params")
+        private List<Param> params;
+
+        @JsonProperty("ProtocolType")
+        private Integer protocolType;
+
+        @JsonProperty("SipBChannelId")
+        private String sipBChannelId;
+
+        @Data
+        @JsonIgnoreProperties(ignoreUnknown = true)
+        public static class Param {
+            @JsonProperty("Key")
+            private String key;
+            @JsonProperty("Name")
+            private String name;
+            @JsonProperty("Type")
+            private String type;
+
+            // 既能是 String 也能是 List<String>
+            @JsonProperty("Value")
+            private Object value;
+        }
+
+        @Data
+        @JsonIgnoreProperties(ignoreUnknown = true)
+        public static class MediaStatus {
+            @JsonProperty("type")
+            private Integer type;
+
+            @JsonProperty("style")
+            private String style;
+
+            @JsonProperty("label")
+            private String label;
+
+            @JsonProperty("size")
+            private Size size;
+
+            @Data
+            public static class Size {
+                @JsonProperty("width")
+                private Integer width;
+
+                @JsonProperty("height")
+                private Integer height;
+            }
+        }
+    }
+}

+ 15 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/FaceRecognitionRequestDTO.java

@@ -0,0 +1,15 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class FaceRecognitionRequestDTO {
+    @JsonProperty("albumIdList")
+    private List<Integer> albumIdList;
+
+    @JsonProperty("imageBase64")
+    private String imageBase64;
+}

+ 50 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/FaceRecognitionResponseDTO.java

@@ -0,0 +1,50 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import java.util.List;
+
+@Data
+public class FaceRecognitionResponseDTO {
+    @JsonProperty("Result")
+    private Result result;
+
+    @JsonProperty("album1vnList")
+    private List<Album1vn> album1vnList;
+
+    @JsonProperty("totalCount")
+    private int totalCount;
+
+    @Data
+    public static class Result {
+        @JsonProperty("Code")
+        private Integer code;
+
+        @JsonProperty("Desc")
+        private String desc;
+    }
+
+    @Data
+    public static class Album1vn {
+        @JsonProperty("Height")
+        private int height;
+
+        @JsonProperty("Width")
+        private int width;
+
+        @JsonProperty("X")
+        private int x;
+
+        @JsonProperty("Y")
+        private int y;
+
+        @JsonProperty("albumId")
+        private int albumId;
+
+        @JsonProperty("photoId")
+        private int photoId;
+
+        @JsonProperty("score")
+        private double score;
+    }
+}

+ 16 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/RegisterFaceRequestDTO.java

@@ -0,0 +1,16 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class RegisterFaceRequestDTO {
+    @JsonProperty("albumName")
+    private String albumName;
+
+    @JsonProperty("name")
+    private String name;
+
+    @JsonProperty("image")
+    private String image;
+}

+ 43 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/RegisterFaceResponseDTO.java

@@ -0,0 +1,43 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class RegisterFaceResponseDTO {
+    @JsonProperty("BoardId")
+    private String boardId;
+
+    @JsonProperty("BoardIp")
+    private String boardIp;
+
+    @JsonProperty("Event")
+    private String event;
+
+    @JsonProperty("Result")
+    private Result result;
+
+    @JsonProperty("albumId")
+    private int albumId;
+
+    @JsonProperty("alignedImage")
+    private String alignedImage;
+
+    @JsonProperty("croppedImage")
+    private String croppedImage;
+
+    @JsonProperty("photoId")
+    private int photoId;
+
+    @JsonProperty("photoName")
+    private String photoName;
+
+    @Data
+    public static class Result {
+        @JsonProperty("Code")
+        private Integer code;
+
+        @JsonProperty("Desc")
+        private String desc;
+    }
+}

+ 52 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/RepositoriesInfoResponseDTO.java

@@ -0,0 +1,52 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class RepositoriesInfoResponseDTO {
+    @JsonProperty("BoardId")
+    private String boardId;
+
+    @JsonProperty("BoardIp")
+    private String boardIp;
+
+    @JsonProperty("Event")
+    private String event;
+
+    @JsonProperty("Result")
+    private Result result;
+
+    @JsonProperty("data")
+    private List<Album> data;
+
+    @JsonProperty("totalCount")
+    private int totalCount;
+
+    @Data
+    public static class Album {
+        @JsonProperty("albumId")
+        private int albumId;
+
+        @JsonProperty("albumName")
+        private String albumName;
+
+        @JsonProperty("pictureNo")
+        private int pictureNo;
+    }
+
+    @Data
+    public static class Result {
+        @JsonProperty("Code")
+        private Integer code;
+
+        @JsonProperty("Desc")
+        private String desc;
+    }
+
+    private long time = System.currentTimeMillis();
+}

+ 11 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/RepositoryCreateRequestDTO.java

@@ -0,0 +1,11 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class RepositoryCreateRequestDTO {
+    @JsonProperty("albumName")
+    private String albumName;
+}
+

+ 36 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/RepositoryCreateResponseDTO.java

@@ -0,0 +1,36 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class RepositoryCreateResponseDTO {
+    @JsonProperty("BoardId")
+    private String boardId;
+
+    @JsonProperty("BoardIp")
+    private String boardIp;
+
+    @JsonProperty("Event")
+    private String event;
+
+    @JsonProperty("Result")
+    private Result result;
+
+    @JsonProperty("albumId")
+    private int albumId;
+
+    @JsonProperty("albumName")
+    private String albumName;
+
+    @Data
+    public static class Result {
+        @JsonProperty("Code")
+        private Integer code;
+
+        @JsonProperty("Desc")
+        private String desc;
+    }
+
+    private long time = System.currentTimeMillis();
+}

+ 10 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/RepositoryDeleteRequestDTO.java

@@ -0,0 +1,10 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class RepositoryDeleteRequestDTO {
+    @JsonProperty("albumName")
+    private String albumName;
+}

+ 33 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/RepositoryDeleteResponseDTO.java

@@ -0,0 +1,33 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class RepositoryDeleteResponseDTO {
+    @JsonProperty("BoardId")
+    private String boardId;
+
+    @JsonProperty("BoardIp")
+    private String boardIp;
+
+    @JsonProperty("Event")
+    private String event;
+
+    @JsonProperty("Result")
+    private Result result;
+
+    @JsonProperty("albumName")
+    private String albumName;
+
+    @Data
+    public static class Result {
+        @JsonProperty("Code")
+        private Integer code;
+
+        @JsonProperty("Desc")
+        private String desc;
+    }
+
+    private long time = System.currentTimeMillis();
+}

+ 16 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/RepositoryFacesRequestDTO.java

@@ -0,0 +1,16 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class RepositoryFacesRequestDTO {
+    @JsonProperty("albumName")
+    private String albumName;
+
+    @JsonProperty("page")
+    private int page;
+
+    @JsonProperty("size")
+    private int size;
+}

+ 48 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/RepositoryFacesResponseDTO.java

@@ -0,0 +1,48 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class RepositoryFacesResponseDTO {
+    @JsonProperty("BoardId")
+    private String boardId;
+
+    @JsonProperty("BoardIp")
+    private String boardIp;
+
+    @JsonProperty("Event")
+    private String event;
+
+    @JsonProperty("Result")
+    private Result result;
+
+    @JsonProperty("data")
+    private List<Face> data;
+
+    @JsonProperty("total")
+    private int total;
+
+    @Data
+    public static class Face {
+        @JsonProperty("albumId")
+        private String albumId;
+
+        @JsonProperty("photoId")
+        private int photoId;
+
+        @JsonProperty("photoName")
+        private String photoName;
+    }
+
+    @Data
+    public static class Result {
+        @JsonProperty("Code")
+        private Integer code;
+
+        @JsonProperty("Desc")
+        private String desc;
+    }
+}

+ 13 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/RepositoryUpdateRequestDTO.java

@@ -0,0 +1,13 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class RepositoryUpdateRequestDTO {
+    @JsonProperty("albumName")
+    private String albumName;
+
+    @JsonProperty("name")
+    private String name;
+}

+ 36 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/RepositoryUpdateResponseDTO.java

@@ -0,0 +1,36 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class RepositoryUpdateResponseDTO {
+    @JsonProperty("BoardId")
+    private String boardId;
+
+    @JsonProperty("BoardIp")
+    private String boardIp;
+
+    @JsonProperty("Event")
+    private String event;
+
+    @JsonProperty("Result")
+    private Result result;
+
+    @JsonProperty("albumId")
+    private int albumId;
+
+    @JsonProperty("albumName")
+    private String albumName;
+
+    @Data
+    public static class Result {
+        @JsonProperty("Code")
+        private Integer code;
+
+        @JsonProperty("Desc")
+        private String desc;
+    }
+
+    private long time = System.currentTimeMillis();
+}

+ 19 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/UpdateFaceRequestDTO.java

@@ -0,0 +1,19 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class UpdateFaceRequestDTO {
+    @JsonProperty("albumName")
+    private String albumName;
+
+    @JsonProperty("name")
+    private String name;
+
+    @JsonProperty("photoId")
+    private int photoId;
+
+    @JsonProperty("image")
+    private String image;
+}

+ 43 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/dto/UpdateFaceResponseDTO.java

@@ -0,0 +1,43 @@
+package com.usky.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class UpdateFaceResponseDTO {
+    @JsonProperty("BoardId")
+    private String boardId;
+
+    @JsonProperty("BoardIp")
+    private String boardIp;
+
+    @JsonProperty("Event")
+    private String event;
+
+    @JsonProperty("Result")
+    private Result result;
+
+    @JsonProperty("albumId")
+    private int albumId;
+
+    @JsonProperty("alignedImage")
+    private String alignedImage;
+
+    @JsonProperty("croppedImage")
+    private String croppedImage;
+
+    @JsonProperty("photoId")
+    private int photoId;
+
+    @JsonProperty("photoName")
+    private String photoName;
+
+    @Data
+    public static class Result {
+        @JsonProperty("Code")
+        private Integer code;
+
+        @JsonProperty("Desc")
+        private String desc;
+    }
+}

+ 27 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/mapper/AiDeviceMapper.java

@@ -0,0 +1,27 @@
+package com.usky.ai.mapper;
+
+import com.usky.ai.domain.AiDevice;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+@Mapper
+public interface AiDeviceMapper {
+
+    void insertDevice(AiDevice device);
+
+    void updateDevice(AiDevice device);
+
+    void saveOrUpdateDevice(AiDevice device);
+
+    void updateDeviceStatus();
+
+    List<AiDevice> getDevices(@Param("boardId") String boardId, @Param("offset") int offset, @Param("limit") int limit);
+
+    long getTotalDevices(@Param("boardId") String boardId);
+
+    void deleteDevice(@Param("boardId") String boardId);
+
+    boolean checkExists(@Param("boardId") String boardId, @Param("boardIp") String boardIp);
+}

+ 1 - 1
service-ai/service-ai-biz/src/main/java/com/usky/ai/mapper/AiQuestionMapper.java

@@ -1,6 +1,6 @@
 package com.usky.ai.mapper;
 
-import com.usky.ai.service.AiQuestion;
+import com.usky.ai.domain.AiQuestion;
 import org.apache.ibatis.annotations.*;
 
 import java.util.List;

+ 2 - 2
service-ai/service-ai-biz/src/main/java/com/usky/ai/mapper/AiSessionMapper.java

@@ -1,7 +1,7 @@
 package com.usky.ai.mapper;
 
-import com.usky.ai.service.AiQuestion;
-import com.usky.ai.service.AiSession;
+import com.usky.ai.domain.AiQuestion;
+import com.usky.ai.domain.AiSession;
 import org.apache.ibatis.annotations.*;
 
 import java.util.List;

+ 22 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/service/AiDeviceService.java

@@ -0,0 +1,22 @@
+package com.usky.ai.service;
+
+import com.usky.ai.domain.AiDevice;
+
+import java.util.List;
+
+public interface AiDeviceService {
+
+    void addDevice(AiDevice device);
+
+    void editDevice(AiDevice device);
+
+    void saveOrUpdateDevice(AiDevice device);
+
+    void updateDeviceStatus();
+
+    List<AiDevice> getDevices(String boardId, int pageNum, int pageSize);
+
+    long getTotalDevices(String boardId);
+
+    void deleteDevice(String boardId);
+}

+ 332 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/service/EdgeMediaService.java

@@ -0,0 +1,332 @@
+package com.usky.ai.service;
+
+
+import com.usky.ai.dto.*;
+import com.usky.ai.service.listener.BoardPingListener;
+import com.usky.ai.service.mqtt.MqttGateway;
+import com.usky.ai.service.util.JsonUtils;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.util.Base64Utils;
+import org.springframework.web.client.RestTemplate;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static com.usky.ai.service.listener.BoardPingListener.ReplyCache.*;
+
+/**
+ * 统一封装所有与盒子交互的 MQTT 请求
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class EdgeMediaService {
+
+    private final MqttGateway gateway;
+
+    private final RestTemplate restTemplate;
+
+    @Value("${api.base-url}")
+    private String baseUrl;
+
+    @Value("${file.path}")
+    private String filePath;
+
+    @Value("${file.domain}")
+    private String fileDomain;
+
+    @Value("${file.prefix}")
+    private String filePrefix;
+
+    public EdgeAppControllerReplyDTO addChannel(EdgeAppControllerRequestDTO req) {
+        req.setEvent("/alg_media_config");
+        gateway.sendToMqtt("/edge_app_controller", 2, JsonUtils.toJson(req));
+        return waitCache(REPLY, req.getBoardId() + "-" + req.getMediaName());
+    }
+
+    public EdgeAppFetchResponseDTO fetchChannels(String boardId) {
+        EdgeAppControllerRequestDTO req = new EdgeAppControllerRequestDTO();
+        req.setBoardId(boardId);
+        req.setEvent("/alg_media_fetch");
+        gateway.sendToMqtt("/edge_app_controller", 2, JsonUtils.toJson(req));
+        log.info("【发送】查询通道请求:{}", JsonUtils.toJson(req));
+        return waitCache(FETCH, boardId);
+    }
+
+    public EdgeAppDeleteResponseDTO deleteChannel(String boardId, String mediaName) {
+        EdgeAppControllerRequestDTO req = new EdgeAppControllerRequestDTO();
+        req.setBoardId(boardId);
+        req.setEvent("/alg_media_delete");
+        req.setMediaName(mediaName);
+        gateway.sendToMqtt("/edge_app_controller", 2, JsonUtils.toJson(req));
+        return waitCache(DELETE, boardId + "-" + mediaName);
+    }
+
+    public EdgeAlgTaskResponseDTO configTask(EdgeAlgTaskConfigRequestDTO req) {
+        req.setEvent("/alg_task_config");
+        gateway.sendToMqtt("/edge_app_controller", 2, JsonUtils.toJson(req));
+        return waitCache(TASK, req.getBoardId() + "-" + req.getAlgTaskSession());
+    }
+
+    public EdgeAlgTaskResponseDTO controlTask(String boardId, String algTaskSession, int command) {
+        EdgeAlgTaskCommandRequestDTO req = new EdgeAlgTaskCommandRequestDTO();
+        req.setBoardId(boardId);
+        req.setEvent("/alg_task_control");
+        req.setControlCommand(command);
+        req.setAlgTaskSession(algTaskSession);
+        gateway.sendToMqtt("/edge_app_controller", 2, JsonUtils.toJson(req));
+        return waitCache(TASK, boardId + "-" + algTaskSession);
+    }
+
+    public EdgeAlgTaskResponseDTO deleteTask(String boardId, String algTaskSession) {
+        EdgeAlgTaskCommandRequestDTO req = new EdgeAlgTaskCommandRequestDTO();
+        req.setBoardId(boardId);
+        req.setEvent("/alg_task_delete");
+        req.setAlgTaskSession(algTaskSession);
+        gateway.sendToMqtt("/edge_app_controller", 2, JsonUtils.toJson(req));
+        return waitCache(TASK, boardId + "-" + algTaskSession);
+    }
+
+    public EdgeAlgTaskListResponseDTO fetchTasks(String boardId) {
+        EdgeAlgTaskFetchRequestDTO req = new EdgeAlgTaskFetchRequestDTO();
+        req.setBoardId(boardId);
+        req.setEvent("/alg_task_fetch");
+        gateway.sendToMqtt("/edge_app_controller", 2, JsonUtils.toJson(req));
+        return waitCache(ALG_TASK_LIST, boardId);
+    }
+
+    public AlgAbilityFetchResponseDTO fetchAlgorithmAbility(String boardId) {
+        AlgAbilityFetchRequestDTO req = new AlgAbilityFetchRequestDTO();
+        req.setBoardId(boardId);
+        gateway.sendToMqtt("/edge_app_controller", 2, JsonUtils.toJson(req));
+        return waitCache(BoardPingListener.ReplyCache.ALG_ABILITY, boardId + "-alg_ability_fetch");
+    }
+
+
+    public RepositoriesInfoResponseDTO listRepositories() {
+        String url = baseUrl + "/api_repositories_info";
+        ResponseEntity<RepositoriesInfoResponseDTO> response = restTemplate.getForEntity(url, RepositoriesInfoResponseDTO.class);
+        if (response.getStatusCode().is2xxSuccessful()) {
+            return response.getBody();
+        } else {
+            throw new RuntimeException("Failed to list repositories: " + response.getStatusCode());
+        }
+    }
+
+    public RepositoryCreateResponseDTO createRepository(RepositoryCreateRequestDTO request) {
+        String url = baseUrl + "/api_repository_create";
+        ResponseEntity<RepositoryCreateResponseDTO> response = restTemplate.postForEntity(url, request, RepositoryCreateResponseDTO.class);
+        if (response.getStatusCode().is2xxSuccessful()) {
+            return response.getBody();
+        } else {
+            throw new RuntimeException("Failed to create repository: " + response.getStatusCode());
+        }
+    }
+
+    public RepositoryUpdateResponseDTO updateRepository(RepositoryUpdateRequestDTO request) {
+        String url = baseUrl + "/api_repository_update";
+        ResponseEntity<RepositoryUpdateResponseDTO> response = restTemplate.postForEntity(url, request, RepositoryUpdateResponseDTO.class);
+        if (response.getStatusCode().is2xxSuccessful()) {
+            return response.getBody();
+        } else {
+            throw new RuntimeException("Failed to update repository: " + response.getStatusCode());
+        }
+    }
+
+    public RepositoryDeleteResponseDTO deleteRepository(RepositoryDeleteRequestDTO request) {
+        String url = baseUrl + "/api_repository_delete";
+        ResponseEntity<RepositoryDeleteResponseDTO> response = restTemplate.postForEntity(url, request, RepositoryDeleteResponseDTO.class);
+        if (response.getStatusCode().is2xxSuccessful()) {
+            return response.getBody();
+        } else {
+            throw new RuntimeException("Failed to delete repository: " + response.getStatusCode());
+        }
+    }
+
+    public RepositoryFacesResponseDTO getRepositoryFaces(RepositoryFacesRequestDTO request) {
+        String url = baseUrl + "/api_repository_faces";
+        ResponseEntity<RepositoryFacesResponseDTO> response = restTemplate.postForEntity(url, request, RepositoryFacesResponseDTO.class);
+        if (response.getStatusCode().is2xxSuccessful()) {
+            return response.getBody();
+        } else {
+            throw new RuntimeException("Failed to get repository faces: " + response.getStatusCode());
+        }
+    }
+
+    public RegisterFaceResponseDTO registerFace(RegisterFaceRequestDTO request) {
+        String url = baseUrl + "/api_register_face";
+        ResponseEntity<RegisterFaceResponseDTO> response = restTemplate.postForEntity(url, request, RegisterFaceResponseDTO.class);
+        if (response.getStatusCode().is2xxSuccessful()) {
+            return response.getBody();
+        } else {
+            throw new RuntimeException("Failed to register face: " + response.getStatusCode());
+        }
+    }
+
+    public UpdateFaceResponseDTO updateFace(UpdateFaceRequestDTO request) {
+        String url = baseUrl + "/api_face_update";
+        ResponseEntity<UpdateFaceResponseDTO> response = restTemplate.postForEntity(url, request, UpdateFaceResponseDTO.class);
+        if (response.getStatusCode().is2xxSuccessful()) {
+            return response.getBody();
+        } else {
+            throw new RuntimeException("Failed to update face: " + response.getStatusCode());
+        }
+    }
+
+    public DeleteFaceResponseDTO deleteFace(DeleteFaceRequestDTO request) {
+        String url = baseUrl + "/api_face_delete";
+        ResponseEntity<DeleteFaceResponseDTO> response = restTemplate.postForEntity(url, request, DeleteFaceResponseDTO.class);
+        if (response.getStatusCode().is2xxSuccessful()) {
+            return response.getBody();
+        } else {
+            throw new RuntimeException("Failed to delete face: " + response.getStatusCode());
+        }
+    }
+
+    public FaceRecognitionResponseDTO recognizeFace(FaceRecognitionRequestDTO request) {
+        String url = baseUrl + "/api/faceengine/face/1vn";
+        ResponseEntity<FaceRecognitionResponseDTO> response = restTemplate.postForEntity(url, request, FaceRecognitionResponseDTO.class);
+        if (response.getStatusCode().is2xxSuccessful()) {
+            return response.getBody();
+        } else {
+            throw new RuntimeException("Failed to recognize face: " + response.getStatusCode());
+        }
+    }
+
+    public AlgTaskSnapResponseDTO getTaskPreview(String boardId, String algTaskSession) {
+        AlgTaskSnapRequestDTO req = new AlgTaskSnapRequestDTO();
+        req.setBoardId(boardId);
+        req.setAlgTaskSession(algTaskSession);
+
+        gateway.sendToMqtt("/edge_app_controller", 2, JsonUtils.toJson(req));
+        return waitCache(BoardPingListener.ReplyCache.TASK_PREVIEW, boardId + "-" + algTaskSession);
+    }
+
+    public AlgScheduleCreateResponseDTO createSchedule(AlgScheduleCreateRequestDTO req) {
+        req.setEvent("/alg_schedule_create");
+        req.convertTimePeriodsToBinary(); // 转换时间段为二进制数据
+        gateway.sendToMqtt("/edge_app_controller", 2, JsonUtils.toJson(req));
+        return waitCache(BoardPingListener.ReplyCache.SCHEDULE_CREATE, req.getBoardId() + "-schedule-create");
+    }
+
+    public AlgScheduleFetchResponseDTO fetchSchedules(String boardId) {
+        AlgScheduleFetchRequestDTO req = new AlgScheduleFetchRequestDTO();
+        req.setBoardId(boardId);
+        gateway.sendToMqtt("/edge_app_controller", 2, JsonUtils.toJson(req));
+        return waitCache(BoardPingListener.ReplyCache.SCHEDULE_FETCH, boardId + "-schedule-fetch");
+    }
+
+    public AlgScheduleDeleteResponseDTO deleteSchedule(String boardId, Long id) {
+        AlgScheduleDeleteRequestDTO req = new AlgScheduleDeleteRequestDTO();
+        req.setBoardId(boardId);
+        req.setId(id);
+        gateway.sendToMqtt("/edge_app_controller", 2, JsonUtils.toJson(req));
+        return waitCache(BoardPingListener.ReplyCache.SCHEDULE_DELETE, boardId + "-schedule-delete-" + id);
+    }
+
+    public AlgSuitCreateResponseDTO createSuitGroup(AlgSuitCreateRequestDTO req) {
+        log.info("收到创建工装组请求:{}", req);
+        gateway.sendToMqtt("/edge_app_controller", 2, JsonUtils.toJson(req));
+        return waitCache(BoardPingListener.ReplyCache.SUIT_CREATE, req.getBoardId() + "-suit-create");
+    }
+
+    public AlgSuitGroupRemoveResponseDTO removeSuitGroup(AlgSuitGroupRemoveRequestDTO req) {
+        gateway.sendToMqtt("/edge_app_controller", 2, JsonUtils.toJson(req));
+        return waitCache(BoardPingListener.ReplyCache.SUIT_REMOVE, req.getBoardId() + "-suit-remove");
+    }
+
+    public AlgSuitAppendResponseDTO appendSuitTemplate(AlgSuitAppendRequestDTO req) {
+        gateway.sendToMqtt("/edge_app_controller", 2, JsonUtils.toJson(req));
+        return waitCache(BoardPingListener.ReplyCache.SUIT_APPEND, req.getBoardId() + "-suit-append");
+    }
+
+    public AlgSuitRemoveResponseDTO removeSuitTemplate(AlgSuitRemoveRequestDTO req) {
+        gateway.sendToMqtt("/edge_app_controller", 2, JsonUtils.toJson(req));
+        return waitCache(BoardPingListener.ReplyCache.SUIT_REMOVE_TEMPLATE, req.getBoardId() + "-suit-remove-template");
+    }
+
+    public AlgSuitFetchResponseDTO fetchSuitGroup(String boardId) {
+        AlgSuitFetchRequestDTO req = new AlgSuitFetchRequestDTO();
+        req.setBoardId(boardId);
+        gateway.sendToMqtt("/edge_app_controller", 2, JsonUtils.toJson(req));
+        return waitCache(BoardPingListener.ReplyCache.SUIT_FETCH, boardId + "-suit-fetch");
+    }
+
+    public String getTaskPreviewUrl(String boardId, String algTaskSession) {
+        AlgTaskSnapResponseDTO response = getTaskPreview(boardId, algTaskSession);
+        if (response.isSuccess() && response.getBase64() != null) {
+            try {
+                return saveImage(response.getBase64(), "taskPreview");
+            } catch (IOException e) {
+                log.error("Failed to save task preview image", e);
+                throw new RuntimeException("Failed to save task preview image", e);
+            }
+        } else {
+            throw new RuntimeException("Failed to get task preview: " + response.getResult().getDesc());
+        }
+    }
+
+    private String saveImage(String base64Image, String fileName) throws IOException {
+        // 检查 Base64 数据是否符合预期格式
+        byte[] imageBytes = Base64Utils.decodeFromString(base64Image);
+
+        // 生成文件路径 (使用yyyyMMddHHmmss格式)
+        LocalDateTime now = LocalDateTime.now();
+        String monthPath = now.format(DateTimeFormatter.ofPattern("yyyyMM"));
+        String formattedTime = now.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
+        int counterSuffix = new AtomicInteger(0).getAndIncrement(); // 获取并递增计数器
+        String filePathWithPrefix = filePath + "/" + monthPath + "/" + fileName + "_" + formattedTime + "_" + counterSuffix + ".jpg";
+        Path path = Paths.get(filePathWithPrefix);
+
+        // 保存图片到文件系统
+        Files.createDirectories(path.getParent());
+        Files.write(path, imageBytes);
+
+        // 返回可访问的 URL
+        return fileDomain + filePrefix + "/" + monthPath + "/" + fileName + "_" + formattedTime + "_" + counterSuffix + ".jpg";
+    }
+
+    private <T> T waitCache(Map<String, T> cache, String key) {
+        for (int i = 0; i < 50; i++) {
+            T resp = cache.get(key);
+            if (resp == null) {
+                try {
+                    TimeUnit.MILLISECONDS.sleep(100);
+                } catch (InterruptedException ignored) {
+                }
+                continue;
+            }
+
+            if (resp instanceof EdgeAppControllerReplyDTO) {
+                EdgeAppControllerReplyDTO reply = (EdgeAppControllerReplyDTO) resp;
+                if (!reply.isSuccess()) {
+                    cache.remove(key);
+                    throw new RuntimeException(reply.getResult().getDesc());
+                }
+            }
+
+            if (resp instanceof AlgAbilityFetchResponseDTO) {
+                AlgAbilityFetchResponseDTO reply = (AlgAbilityFetchResponseDTO) resp;
+                if (!reply.isSuccess()) {
+                    cache.remove(key);
+                    throw new RuntimeException(reply.getResult().getDesc());
+                }
+            }
+
+            cache.remove(key);
+            return resp;
+        }
+        throw new RuntimeException("等待盒子响应超时");
+    }
+}

+ 13 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/service/config/AppConfig.java

@@ -0,0 +1,13 @@
+package com.usky.ai.service.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.client.RestTemplate;
+
+@Configuration
+public class AppConfig {
+    @Bean
+    public RestTemplate restTemplate() {
+        return new RestTemplate();
+    }
+}

+ 48 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/service/enums/TopListener.java

@@ -0,0 +1,48 @@
+package com.usky.ai.service.enums;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public enum TopListener {
+
+    /**
+     * AI算法识别
+     */
+    // 主动心跳上报
+    BOARD_PING("boardPing", "/board_ping", 1),
+    //新增媒体通道
+    EDGE_APP_CONTROLLER("edgeAppController", "/edge_app_controller", 0),
+    //媒体通道响应结果
+    EDGE_APP_CONTROLLER_REPLY("edgeAppControllerReply", "/edge_app_controller_reply", 1);
+
+
+
+    private final String name;
+    private final String code;
+    private final Integer type;
+
+    TopListener(String name, String code, Integer type) {
+        this.name = name;
+        this.code = code;
+        this.type = type;
+    }
+
+    public static TopListener parse(String code) {
+        for (TopListener t : values()) {
+            if (t.code.equals(code)) return t;
+        }
+        return null;
+    }
+
+    public static List<TopListener> parse(Integer type) {
+        List<TopListener> list = new ArrayList<>();
+        for (TopListener t : values()) {
+            if (t.type.equals(type)) list.add(t);
+        }
+        return list;
+    }
+
+    public String getName() { return name; }
+    public String getCode() { return code; }
+    public Integer getType() { return type; }
+}

+ 60 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/service/impl/AiDeviceServiceImpl.java

@@ -0,0 +1,60 @@
+package com.usky.ai.service.impl;
+
+import com.usky.ai.domain.AiDevice;
+import com.usky.ai.mapper.AiDeviceMapper;
+import com.usky.ai.service.AiDeviceService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class AiDeviceServiceImpl implements AiDeviceService {
+
+    private final AiDeviceMapper aiDeviceMapper;
+
+    @Autowired
+    public AiDeviceServiceImpl(AiDeviceMapper aiDeviceMapper) {
+        this.aiDeviceMapper = aiDeviceMapper;
+    }
+
+    @Override
+    public void addDevice(AiDevice device) {
+        if (aiDeviceMapper.checkExists(device.getBoardId(), device.getBoardIp())) {
+            throw new IllegalArgumentException("设备已存在, 设备Id 或 设备Ip 已存在");
+        }
+        aiDeviceMapper.insertDevice(device);
+    }
+
+    @Override
+    public void editDevice(AiDevice device) {
+        aiDeviceMapper.updateDevice(device);
+    }
+
+    @Override
+    public void saveOrUpdateDevice(AiDevice device) {
+        aiDeviceMapper.saveOrUpdateDevice(device);
+    }
+
+    @Override
+    public void updateDeviceStatus() {
+        aiDeviceMapper.updateDeviceStatus();
+    }
+
+    @Override
+    public List<AiDevice> getDevices(String boardId, int pageNum, int pageSize) {
+        updateDeviceStatus(); // 在查询之前更新设备的 status 状态
+        int offset = (pageNum - 1) * pageSize;
+        return aiDeviceMapper.getDevices(boardId, offset, pageSize);
+    }
+
+    @Override
+    public long getTotalDevices(String boardId) {
+        return aiDeviceMapper.getTotalDevices(boardId);
+    }
+
+    @Override
+    public void deleteDevice(String boardId) {
+        aiDeviceMapper.deleteDevice(boardId);
+    }
+}

+ 299 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/service/listener/BoardPingListener.java

@@ -0,0 +1,299 @@
+package com.usky.ai.service.listener;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.usky.ai.controller.web.BoardPingController;
+import com.usky.ai.domain.AiDevice;
+import com.usky.ai.dto.*;
+import com.usky.ai.mapper.AiDeviceMapper;
+import com.usky.ai.service.mqtt.MqttInConfig;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.integration.annotation.ServiceActivator;
+import org.springframework.messaging.Message;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Slf4j
+@Component
+@ConditionalOnProperty(prefix = "mqtt", value = "enabled", havingValue = "true")
+public class BoardPingListener {
+
+    private static final Map<String, Long> LAST_PING = new ConcurrentHashMap<>();
+    private static final long TIMEOUT = 6_000;
+
+    private final AiDeviceMapper aiDeviceMapper;
+
+    private final ObjectMapper objectMapper = new ObjectMapper();
+
+    @Autowired
+    public BoardPingListener(AiDeviceMapper aiDeviceMapper) {
+        this.aiDeviceMapper = aiDeviceMapper;
+    }
+
+    public static class ReplyCache {
+        public static final Map<String, EdgeAppControllerReplyDTO> REPLY = new ConcurrentHashMap<>();
+        public static final Map<String, EdgeAppFetchResponseDTO> FETCH = new ConcurrentHashMap<>();
+        public static final Map<String, EdgeAppDeleteResponseDTO> DELETE = new ConcurrentHashMap<>();
+        public static final Map<String, EdgeAlgTaskResponseDTO> TASK = new ConcurrentHashMap<>();
+        public static final Map<String, EdgeAlgTaskListResponseDTO> ALG_TASK_LIST = new ConcurrentHashMap<>();
+        public static final Map<String, AlgAbilityFetchResponseDTO> ALG_ABILITY = new ConcurrentHashMap<>();
+        public static final Map<String, RepositoriesInfoResponseDTO> REPOSITORIES_INFO = new ConcurrentHashMap<>();
+        public static final Map<String, RepositoryCreateResponseDTO> REPOSITORY_CREATE = new ConcurrentHashMap<>();
+        public static final Map<String, RepositoryUpdateResponseDTO> REPOSITORY_UPDATE = new ConcurrentHashMap<>();
+        public static final Map<String, RepositoryDeleteResponseDTO> REPOSITORY_DELETE = new ConcurrentHashMap<>();
+        public static final Map<String, AlgTaskSnapResponseDTO> TASK_PREVIEW = new ConcurrentHashMap<>();
+        public static final Map<String, AlgScheduleCreateResponseDTO> SCHEDULE_CREATE = new ConcurrentHashMap<>();
+        public static final Map<String, AlgScheduleFetchResponseDTO> SCHEDULE_FETCH = new ConcurrentHashMap<>();
+        public static final Map<String, AlgScheduleDeleteResponseDTO> SCHEDULE_DELETE = new ConcurrentHashMap<>();
+        public static final Map<String, AlgSuitCreateResponseDTO> SUIT_CREATE = new ConcurrentHashMap<>();
+        public static final Map<String, AlgSuitGroupRemoveResponseDTO> SUIT_REMOVE = new ConcurrentHashMap<>();
+        public static final Map<String, AlgSuitAppendResponseDTO> SUIT_APPEND = new ConcurrentHashMap<>();
+        public static final Map<String, AlgSuitRemoveResponseDTO> SUIT_REMOVE_TEMPLATE = new ConcurrentHashMap<>();
+        public static final Map<String, AlgSuitFetchResponseDTO> SUIT_FETCH = new ConcurrentHashMap<>();
+    }
+
+    @ServiceActivator(inputChannel = MqttInConfig.CHANNEL_NAME_INPUT)
+    public void handleHeartbeat(Message<?> message) {
+        String topic = (String) message.getHeaders().get("mqtt_receivedTopic");
+        if (!"/board_ping".equals(topic)) return;
+
+        String payload = (String) message.getPayload();
+        try {
+            if (payload == null) {
+                log.error("接收到的 payload 为空");
+                return;
+            }
+
+            BoardPingDTO dto = objectMapper.readValue(payload, BoardPingDTO.class);
+            if (dto == null) {
+                log.error("解析心跳消息失败,结果为 null");
+                return;
+            }
+
+            BoardPingController.CACHE.put(dto.getBoardId(), dto);
+            log.info("收到心跳:{}", dto.getBoardId());
+
+            // 将 BoardPingDTO 转换为 AiDevice
+            AiDevice aiDevice = new AiDevice();
+            aiDevice.setBoardId(dto.getBoardId());
+            aiDevice.setBoardIp(dto.getBoardIp());
+            aiDevice.setDescription(null);
+            aiDevice.setStatus(dto.getKey().equals("运行中") ? 1 : 0);
+            aiDevice.setCreateTime(new java.sql.Timestamp(System.currentTimeMillis()));
+            aiDevice.setCreateBy("system");
+            aiDevice.setUpdateTime(new java.sql.Timestamp(System.currentTimeMillis()));
+            aiDevice.setUpdateBy("system");
+
+            aiDeviceMapper.saveOrUpdateDevice(aiDevice);
+        } catch (Exception e) {
+            log.error("解析心跳失败", e);
+        }
+    }
+
+    @ServiceActivator(inputChannel = MqttInConfig.CHANNEL_NAME_INPUT)
+    public void handleAllReplies(Message<?> message) {
+        String topic = (String) message.getHeaders().get("mqtt_receivedTopic");
+        if (!"/edge_app_controller_reply".equals(topic)) return;
+
+        String payload = (String) message.getPayload();
+        log.info("收到 /edge_app_controller_reply 原始消息: {}", payload);
+        try {
+            if (payload == null) {
+                log.error("接收到的 payload 为空");
+                return;
+            }
+
+            JsonNode root = objectMapper.readTree(payload);
+            if (root == null) {
+                log.error("解析 payload 为空");
+                return;
+            }
+
+            JsonNode eventNode = root.get("Event");
+            if (eventNode == null) {
+                log.error("JSON 中缺少 'Event' 字段");
+                return;
+            }
+
+            String event = eventNode.asText();
+            if (event == null || event.isEmpty()) {
+                log.error("Event 字段为空");
+                return;
+            }
+
+            switch (event) {
+                case "/alg_media_config":
+                    EdgeAppControllerReplyDTO configResp = objectMapper.readValue(payload, EdgeAppControllerReplyDTO.class);
+                    String configKey = configResp.getBoardId() + "-" + configResp.getMediaName();
+                    ReplyCache.REPLY.put(configKey, configResp);
+                    log.info("收到通道配置响应:{}", configKey);
+                    break;
+
+                case "/alg_media_fetch":
+                    EdgeAppFetchResponseDTO fetchResp = objectMapper.readValue(payload, EdgeAppFetchResponseDTO.class);
+                    ReplyCache.FETCH.put(fetchResp.getBoardId(), fetchResp);
+                    log.info("收到通道查询响应:{}", fetchResp.getBoardId());
+                    break;
+
+                case "/alg_media_delete":
+                    EdgeAppDeleteResponseDTO deleteResp = objectMapper.readValue(payload, EdgeAppDeleteResponseDTO.class);
+                    String deleteKey = deleteResp.getBoardId() + "-" + deleteResp.getMediaName();
+                    ReplyCache.DELETE.put(deleteKey, deleteResp);
+                    log.info("收到通道删除响应:{}", deleteKey);
+                    break;
+
+                case "/alg_task_config":
+                case "/alg_task_control":
+                case "/alg_task_delete":
+                    EdgeAlgTaskResponseDTO taskResp = objectMapper.readValue(payload, EdgeAlgTaskResponseDTO.class);
+                    String taskKey = taskResp.getBoardId() + "-" + taskResp.getAlgTaskSession();
+                    ReplyCache.TASK.put(taskKey, taskResp);
+                    log.info("收到任务响应:{}-{}", taskResp.getBoardId(), taskResp.getAlgTaskSession());
+                    break;
+
+                case "/alg_task_fetch":
+                    EdgeAlgTaskListResponseDTO taskListResp = objectMapper.readValue(payload, EdgeAlgTaskListResponseDTO.class);
+                    ReplyCache.ALG_TASK_LIST.put(taskListResp.getBoardId(), taskListResp);
+                    log.info("✅ 收到任务列表响应:{},内容数量:{}", taskListResp.getBoardId(), taskListResp.getContent().size());
+                    break;
+
+                case "/alg_ability_fetch":
+                    AlgAbilityFetchResponseDTO abilityResp = objectMapper.readValue(payload, AlgAbilityFetchResponseDTO.class);
+                    String abilityKey = abilityResp.getBoardId() + "-alg_ability_fetch";
+                    ReplyCache.ALG_ABILITY.put(abilityKey, abilityResp);
+                    log.info("收到算法能力获取响应:{}", abilityKey);
+                    break;
+
+                case "/api_repositories_info":
+                    RepositoriesInfoResponseDTO repositoriesInfoResp = objectMapper.readValue(payload, RepositoriesInfoResponseDTO.class);
+                    ReplyCache.REPOSITORIES_INFO.put("repositories_info", repositoriesInfoResp);
+                    log.info("收到人脸库列表响应");
+                    break;
+
+                case "/api_repository_create":
+                    RepositoryCreateResponseDTO createResp = objectMapper.readValue(payload, RepositoryCreateResponseDTO.class);
+                    String createKey = "repository_create-" + createResp.getAlbumName();
+                    ReplyCache.REPOSITORY_CREATE.put(createKey, createResp);
+                    log.info("收到人脸库创建响应:{}", createKey);
+                    break;
+
+                case "/api_repository_update":
+                    RepositoryUpdateResponseDTO updateResp = objectMapper.readValue(payload, RepositoryUpdateResponseDTO.class);
+                    String updateKey = "repository_update-" + updateResp.getAlbumName();
+                    ReplyCache.REPOSITORY_UPDATE.put(updateKey, updateResp);
+                    log.info("收到人脸库更新响应:{}", updateKey);
+                    break;
+
+                case "/api_repository_delete":
+                    RepositoryDeleteResponseDTO repoDeleteResp = objectMapper.readValue(payload, RepositoryDeleteResponseDTO.class);
+                    String repoDeleteKey = "repository_delete-" + repoDeleteResp.getAlbumName();
+                    ReplyCache.REPOSITORY_DELETE.put(repoDeleteKey, repoDeleteResp);
+                    log.info("收到人脸库删除响应:{}", repoDeleteKey);
+                    break;
+
+                case "/alg_task_snap":
+                    AlgTaskSnapResponseDTO snapResp = objectMapper.readValue(payload, AlgTaskSnapResponseDTO.class);
+                    String snapKey = snapResp.getBoardId() + "-" + snapResp.getAlgTaskSession();
+                    ReplyCache.TASK_PREVIEW.put(snapKey, snapResp);
+                    log.info("收到任务预览图响应:{}", snapKey);
+                    break;
+
+                case "/alg_schedule_create":
+                    AlgScheduleCreateResponseDTO scheduleResp = objectMapper.readValue(payload, AlgScheduleCreateResponseDTO.class);
+                    String scheduleKey = scheduleResp.getBoardId() + "-schedule-create";
+                    ReplyCache.SCHEDULE_CREATE.put(scheduleKey, scheduleResp);
+                    log.info("收到计划模板创建响应:{}", scheduleKey);
+                    break;
+
+                case "/alg_schedule_fetch":
+                    AlgScheduleFetchResponseDTO scfetchResp = objectMapper.readValue(payload, AlgScheduleFetchResponseDTO.class);
+                    String scfetchKey = scfetchResp.getBoardId() + "-schedule-fetch";
+                    ReplyCache.SCHEDULE_FETCH.put(scfetchKey, scfetchResp);
+                    log.info("收到计划模板查询响应:{}", scfetchKey);
+                    break;
+
+                case "/alg_schedule_delete":
+                    AlgScheduleDeleteResponseDTO scheduleDeleteResp = objectMapper.readValue(payload, AlgScheduleDeleteResponseDTO.class);
+                    String scheduleDeleteKey = scheduleDeleteResp.getBoardId() + "-schedule-delete-" + scheduleDeleteResp.getScheduleId();
+                    ReplyCache.SCHEDULE_DELETE.put(scheduleDeleteKey, scheduleDeleteResp);
+                    log.info("收到计划模板删除响应:{}", scheduleDeleteKey);
+                    break;
+
+                case "/alg_suit_create":
+                    AlgSuitCreateResponseDTO suitCreateResp = objectMapper.readValue(payload, AlgSuitCreateResponseDTO.class);
+                    String suitCreateKey = suitCreateResp.getBoardId() + "-suit-create";
+                    ReplyCache.SUIT_CREATE.put(suitCreateKey, suitCreateResp);
+                    log.info("收到创建工装组响应:{}", suitCreateKey);
+                    break;
+
+                case "/alg_suit_group_remove":
+                    AlgSuitGroupRemoveResponseDTO suitRemoveResp = objectMapper.readValue(payload, AlgSuitGroupRemoveResponseDTO.class);
+                    String suitRemoveKey = suitRemoveResp.getBoardId() + "-suit-remove";
+                    ReplyCache.SUIT_REMOVE.put(suitRemoveKey, suitRemoveResp);
+                    log.info("收到删除工装组响应:{}", suitRemoveKey);
+                    break;
+
+                case "/alg_suit_append":
+                    AlgSuitAppendResponseDTO suitAppendResp = objectMapper.readValue(payload, AlgSuitAppendResponseDTO.class);
+                    String suitAppendKey = suitAppendResp.getBoardId() + "-suit-append";
+                    ReplyCache.SUIT_APPEND.put(suitAppendKey, suitAppendResp);
+                    log.info("收到上传工装模板响应:{}", suitAppendKey);
+                    break;
+
+                case "/alg_suit_remove":
+                    AlgSuitRemoveResponseDTO suitRemoveTemplateResp = objectMapper.readValue(payload, AlgSuitRemoveResponseDTO.class);
+                    String suitRemoveTemplateKey = suitRemoveTemplateResp.getBoardId() + "-suit-remove-template";
+                    ReplyCache.SUIT_REMOVE_TEMPLATE.put(suitRemoveTemplateKey, suitRemoveTemplateResp);
+                    log.info("收到删除工装模板响应:{}", suitRemoveTemplateKey);
+                    break;
+
+                case "/alg_suit_fetch":
+                    AlgSuitFetchResponseDTO suitFetchResp = objectMapper.readValue(payload, AlgSuitFetchResponseDTO.class);
+                    String suitFetchKey = suitFetchResp.getBoardId() + "-suit-fetch";
+                    ReplyCache.SUIT_FETCH.put(suitFetchKey, suitFetchResp);
+                    log.info("收到工装组信息响应:{}", suitFetchKey);
+                    break;
+
+                default:
+                    log.warn("未知事件类型:{}", event);
+            }
+        } catch (Exception e) {
+            log.error("解析 /edge_app_controller_reply 失败,原始消息: {}", payload, e);
+        }
+    }
+
+    @Scheduled(fixedDelay = 300_000) // 每 5 分钟
+    public void cleanExpiredCache() {
+        long now = System.currentTimeMillis();
+        long timeout = 300_000;
+
+        ReplyCache.REPLY.entrySet().removeIf(e -> now - e.getValue().getTime() > timeout);
+        ReplyCache.FETCH.entrySet().removeIf(e -> now - e.getValue().getTime() > timeout);
+        ReplyCache.DELETE.entrySet().removeIf(e -> now - e.getValue().getTime() > timeout);
+        ReplyCache.TASK.entrySet().removeIf(e -> now - e.getValue().getTime() > timeout);
+        ReplyCache.ALG_TASK_LIST.entrySet().removeIf(e -> now - e.getValue().getTime() > timeout);
+        ReplyCache.ALG_ABILITY.entrySet().removeIf(e -> now - e.getValue().getTime() > timeout);
+        ReplyCache.REPOSITORIES_INFO.entrySet().removeIf(e -> now - e.getValue().getTime() > timeout);
+        ReplyCache.REPOSITORY_CREATE.entrySet().removeIf(e -> now - e.getValue().getTime() > timeout);
+        ReplyCache.REPOSITORY_UPDATE.entrySet().removeIf(e -> now - e.getValue().getTime() > timeout);
+        ReplyCache.REPOSITORY_DELETE.entrySet().removeIf(e -> now - e.getValue().getTime() > timeout);
+        ReplyCache.TASK_PREVIEW.entrySet().removeIf(e -> now - e.getValue().getTime() > timeout);
+        ReplyCache.SCHEDULE_CREATE.entrySet().removeIf(e -> now - e.getValue().getTime() > timeout);
+        ReplyCache.SCHEDULE_FETCH.entrySet().removeIf(e -> now - e.getValue().getTime() > timeout);
+        ReplyCache.SCHEDULE_DELETE.entrySet().removeIf(e -> now - e.getValue().getTime() > timeout);
+        ReplyCache.SUIT_CREATE.entrySet().removeIf(e -> now - e.getValue().getTime() > timeout);
+        ReplyCache.SUIT_REMOVE.entrySet().removeIf(e -> now - e.getValue().getTime() > timeout);
+        ReplyCache.SUIT_APPEND.entrySet().removeIf(e -> now - e.getValue().getTime() > timeout);
+        ReplyCache.SUIT_REMOVE_TEMPLATE.entrySet().removeIf(e -> now - e.getValue().getTime() > timeout);
+        ReplyCache.SUIT_FETCH.entrySet().removeIf(e -> now - e.getValue().getTime() > timeout);
+
+        log.info("清理过期缓存:REPLY={} FETCH={} DELETE={} TASK={} TASK_FETCH={}",
+                ReplyCache.REPLY.size(), ReplyCache.FETCH.size(), ReplyCache.DELETE.size(),
+                ReplyCache.TASK.size(), ReplyCache.ALG_TASK_LIST.size());
+    }
+}

+ 50 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/service/mqtt/MqttBaseConfig.java

@@ -0,0 +1,50 @@
+package com.usky.ai.service.mqtt;
+
+import lombok.Data;
+import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory;
+import org.springframework.integration.mqtt.core.MqttPahoClientFactory;
+import org.springframework.stereotype.Component;
+
+@ConditionalOnProperty(prefix = "mqtt", value = {"enabled"}, havingValue = "true")
+@Data
+@Component
+@ConfigurationProperties(prefix = "mqtt")
+public class MqttBaseConfig {
+
+    @Value("${mqtt.username}")
+    private String username;
+
+    @Value("${mqtt.password}")
+    private String password;
+
+    @Value("${mqtt.url}")
+    private String hostUrl;
+
+    @Value("${mqtt.sub-topics}")
+    private String msgTopic;
+
+    @Value("${mqtt.keep-alive-interval}")
+    //心跳间隔
+    private int keepAliveInterval;
+    @Value("${mqtt.completionTimeout}")
+    //心跳间隔
+    private int completionTimeout;
+
+
+    @Bean
+    public MqttPahoClientFactory mqttClientFactory() {
+        DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
+        MqttConnectOptions options = new MqttConnectOptions();
+        options.setServerURIs(new String[]{this.getHostUrl()});
+        options.setUserName(this.getUsername());
+        options.setPassword(this.getPassword().toCharArray());
+        factory.setConnectionOptions(options);
+        return factory;
+    }
+
+}

+ 17 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/service/mqtt/MqttGateway.java

@@ -0,0 +1,17 @@
+package com.usky.ai.service.mqtt;
+
+import org.springframework.integration.annotation.MessagingGateway;
+import org.springframework.integration.mqtt.support.MqttHeaders;
+import org.springframework.messaging.handler.annotation.Header;
+
+@MessagingGateway(defaultRequestChannel = "mqttOutboundChannel")
+public interface MqttGateway {
+
+    void sendToMqtt(String payload);
+
+    void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic, String payload);
+
+    void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic,
+                    @Header(MqttHeaders.QOS) int qos,
+                    String payload);
+}

+ 56 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/service/mqtt/MqttInConfig.java

@@ -0,0 +1,56 @@
+package com.usky.ai.service.mqtt;
+
+import com.usky.ai.service.enums.TopListener;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.integration.channel.DirectChannel;
+import org.springframework.integration.endpoint.MessageProducerSupport;
+import org.springframework.integration.mqtt.core.MqttPahoClientFactory;
+import org.springframework.integration.mqtt.inbound.MqttPahoMessageDrivenChannelAdapter;
+import org.springframework.integration.mqtt.support.DefaultPahoMessageConverter;
+import org.springframework.messaging.MessageChannel;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Configuration
+public class MqttInConfig {
+
+    public static final String CHANNEL_NAME_INPUT = "mqttInboundChannel";
+    public static final String CHANNEL_BOARD_PING = "boardPingChannel";
+
+    @Bean
+    public MessageProducerSupport mqttInbound(MqttPahoClientFactory factory) {
+        // 获取所有需要订阅的主题
+        List<String> topics = TopListener.parse(1).stream()
+                .map(TopListener::getCode)
+                .collect(Collectors.toList()); // 使用 collect(Collectors.toList())
+
+        // 创建适配器实例,订阅所有主题
+        MqttPahoMessageDrivenChannelAdapter adapter =
+                new MqttPahoMessageDrivenChannelAdapter(
+                        "server-in", factory, topics.toArray(new String[0]));
+
+        // 设置消息转换器
+        adapter.setConverter(new DefaultPahoMessageConverter());
+        // 设置 QoS
+        adapter.setQos(1);
+        // 设置完成超时时间
+        adapter.setCompletionTimeout(5000);
+
+        // 设置输出通道
+        adapter.setOutputChannel(mqttInboundChannel());
+
+        return adapter;
+    }
+
+    @Bean(name = CHANNEL_NAME_INPUT)
+    public MessageChannel mqttInboundChannel() {
+        return new DirectChannel();
+    }
+
+    @Bean(name = CHANNEL_BOARD_PING)
+    public MessageChannel boardPingChannel() {
+        return new DirectChannel();
+    }
+}

+ 41 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/service/mqtt/MqttOutConfig.java

@@ -0,0 +1,41 @@
+package com.usky.ai.service.mqtt;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.integration.annotation.IntegrationComponentScan;
+import org.springframework.integration.annotation.ServiceActivator;
+import org.springframework.integration.channel.DirectChannel;
+import org.springframework.integration.mqtt.outbound.MqttPahoMessageHandler;
+import org.springframework.messaging.MessageChannel;
+import org.springframework.messaging.MessageHandler;
+
+@Configuration
+@IntegrationComponentScan
+@ConditionalOnProperty(prefix = "mqtt", value = {"enabled"}, havingValue = "true")
+public class MqttOutConfig {
+
+    @Autowired
+    public MqttBaseConfig mqttBaseConfig;
+
+    public static final String CHANNEL_NAME_OUT = "mqttOutboundChannel";
+    public static final String MESSAGE_NAME = "messageOut";
+    public static final String DEFAULT_TOPIC = "testTopic";
+
+    @Bean(name = CHANNEL_NAME_OUT)
+    public MessageChannel mqttOutboundChannel() {
+        return new DirectChannel();
+    }
+
+    @Bean(name = MESSAGE_NAME)
+    @ServiceActivator(inputChannel = CHANNEL_NAME_OUT)
+    public MessageHandler outbound() {
+        String clientId = "h-backend-mqtt-out-" + System.currentTimeMillis();
+        MqttPahoMessageHandler messageHandler =
+                new MqttPahoMessageHandler(clientId, mqttBaseConfig.mqttClientFactory());
+        messageHandler.setAsync(true);
+        messageHandler.setDefaultTopic(DEFAULT_TOPIC);
+        return messageHandler;
+    }
+}

+ 34 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/service/util/GlobalExceptionHandler.java

@@ -0,0 +1,34 @@
+package com.usky.ai.service.util;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.Map;
+
+@ControllerAdvice
+public class GlobalExceptionHandler {
+
+    @ExceptionHandler(RuntimeException.class)
+    public ResponseEntity<Map<String, Object>> handleRuntimeException(RuntimeException ex) {
+        Map<String, Object> response = new HashMap<>();
+        response.put("timestamp", LocalDateTime.now());
+        response.put("message", ex.getMessage());
+        response.put("status", HttpStatus.NOT_FOUND.value());
+
+        return new ResponseEntity<>(response, HttpStatus.NOT_FOUND);
+    }
+
+    @ExceptionHandler(Exception.class)
+    public ResponseEntity<Map<String, Object>> handleGenericException(Exception ex) {
+        Map<String, Object> response = new HashMap<>();
+        response.put("timestamp", LocalDateTime.now());
+        response.put("message", "An error occurred: " + ex.getMessage());
+        response.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
+
+        return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
+    }
+}

+ 15 - 0
service-ai/service-ai-biz/src/main/java/com/usky/ai/service/util/JsonUtils.java

@@ -0,0 +1,15 @@
+package com.usky.ai.service.util;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public class JsonUtils {
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+    public static String toJson(Object obj) {
+        try {
+            return MAPPER.writeValueAsString(obj);
+        } catch (JsonProcessingException e) {
+            throw new RuntimeException(e);
+        }
+    }
+}

+ 62 - 0
service-ai/service-ai-biz/src/main/resources/mapper/AiDeviceMapper.xml

@@ -0,0 +1,62 @@
+<?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.ai.mapper.AiDeviceMapper">
+
+    <insert id="insertDevice" useGeneratedKeys="false">
+        INSERT INTO ai_device (board_id, board_ip, description, status, create_time, create_by, update_time, update_by)
+        VALUES (#{boardId}, #{boardIp}, #{description}, #{status}, #{createTime}, #{createBy}, #{updateTime}, #{updateBy})
+    </insert>
+
+    <insert id="saveOrUpdateDevice" useGeneratedKeys="false">
+        INSERT INTO ai_device (board_id, board_ip, description, status, create_time, create_by, update_time, update_by)
+        VALUES (#{boardId}, #{boardIp}, #{description}, #{status}, #{createTime}, #{createBy}, #{updateTime}, #{updateBy})
+            ON DUPLICATE KEY UPDATE
+                                 board_ip = VALUES(board_ip),
+                                 description = VALUES(description),
+                                 status = VALUES(status),
+                                 update_time = VALUES(update_time),
+                                 update_by = VALUES(update_by)
+    </insert>
+
+    <update id="updateDevice">
+        UPDATE ai_device
+        SET board_ip = #{boardIp},
+            description = #{description},
+            status = #{status},
+            update_time = #{updateTime},
+            update_by = #{updateBy}
+        WHERE board_id = #{boardId}
+    </update>
+
+    <update id="updateDeviceStatus">
+        UPDATE ai_device SET status = 0 WHERE update_time &lt;= NOW() - INTERVAL 10 MINUTE
+    </update>
+
+    <select id="getDevices" resultType="com.usky.ai.domain.AiDevice">
+        SELECT * FROM ai_device
+        <where>
+            <if test="boardId != null and boardId != ''">
+                AND board_id = #{boardId}
+            </if>
+        </where>
+        LIMIT #{limit} OFFSET #{offset}
+    </select>
+
+    <select id="getTotalDevices" resultType="long">
+        SELECT COUNT(*) FROM ai_device
+        <where>
+            <if test="boardId != null and boardId != ''">
+                AND board_id = #{boardId}
+            </if>
+        </where>
+    </select>
+
+    <delete id="deleteDevice">
+        DELETE FROM ai_device WHERE board_id = #{boardId}
+    </delete>
+
+    <select id="checkExists" resultType="boolean">
+        SELECT COUNT(*) FROM ai_device
+        WHERE board_id = #{boardId} OR board_ip = #{boardIp}
+    </select>
+</mapper>

+ 3 - 2
service-ai/service-ai-biz/src/main/resources/static/tyqw.html

@@ -92,7 +92,7 @@
 
         const requestBody = JSON.stringify({content: content});
 
-        const token = "eyJhbGciOiJIUzUxMiJ9.eyIiOjEwMDMsInVzZXJfaWQiOjIxMywidXNlcl9rZXkiOiJlYzUxODMzNjdmYTk0ODgwOGQwZjEwODEyOWVmNjgwOSIsInVzZXJuYW1lIjoi6LW16YeR6ZuoIn0.zWulXcesI1TRcDmiAHuQ9P2WHDE2l7mDmuunx13TmVl6E5Yvs8nZvu1ddtINdw0lrnnR3Q5lZaRH3mJJTaDhig";
+        const token = "eyJhbGciOiJIUzUxMiJ9.eyIiOjEwMDMsIm9zSW5mbyI6IldpbmRvd3MgMTAiLCJ1c2VyX2lkIjoxLCJ1c2VyX2tleSI6IjRlYTE2MTQ0NWRlMTQ5MzBiYTQ3N2ExN2M3YzVhNmEzIiwiYnJvd3NlckluZm8iOiJDaHJvbWUgMTQiLCJ1c2VybmFtZSI6ImFkbWluIn0.FGLbxHpe0nbIanCy5sCdnMCiAc2SNad5TrneVZsnmPyjz4CMF9Rix39U3IJm7C30gaDFYlCwER2Pqa02NqIXbw";
 
         fetch('/ai/aliTyqw', {
             method: 'POST',
@@ -147,4 +147,5 @@
     })
 </script>
 </body>
-</html>-->
+</html>
+-->

+ 11 - 33
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/AlarmDataController.java

@@ -2,9 +2,8 @@ package com.usky.cdi.controller;
 
 import com.usky.cdi.service.impl.AlarmDataTransferService;
 import com.usky.cdi.service.vo.alarm.AlarmMessageVO;
-import com.usky.cdi.service.vo.alarm.AlarmMessage1VO;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
+import com.usky.common.core.bean.ApiResult;
+import lombok.RequiredArgsConstructor;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
@@ -12,50 +11,29 @@ import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
 /**
- * 基础类数据传输控制器
- * 提供基础类数据上报的接口
+ * 告警数据 HTTP 入口:将告警上报至市适配平台(MQTT)。
  *
  * @author han
  * @date 2025/12/08
  */
-@Slf4j
 @RestController
 @RequestMapping("/api/alarm")
 @ConditionalOnProperty(prefix = "mqtt", value = {"enabled"}, havingValue = "true")
+@RequiredArgsConstructor
 public class AlarmDataController {
-    @Autowired
-    private AlarmDataTransferService alarmDataTransferService;
 
-    /**
-     * 上报人防工程基础信息
-     */
-    @PostMapping("/alarmMessage")
-    public String sendAlarmMessage(@RequestBody AlarmMessageVO vo) {
-        boolean success = alarmDataTransferService.sendAlarmMessage(vo);
-        return success ? "上报成功" : "上报失败";
-    }
+    private final AlarmDataTransferService alarmDataTransferService;
 
-    /**
-     * 上报人防工程基础信息
-     */
-    @PostMapping("/alarmMessage1")
-    public String sendAlarmMessage1(@RequestBody AlarmMessageVO vo) {
-        boolean success = alarmDataTransferService.sendAlarmMessage1(vo);
-        return success ? "上报成功" : "上报失败";
+    private static ApiResult<String> toSubmitResult(boolean success) {
+        return success ? ApiResult.success("上报成功") : ApiResult.error("上报失败");
     }
 
     /**
-     * 上报倾斜、位移、裂缝监测事件
+     * 告警推送
      */
-    @PostMapping("/alarmMessage2")
-    public String sendAlarmMessage2(@RequestBody AlarmMessageVO vo) {
-        boolean success = alarmDataTransferService.sendAlarmMessage2(vo);
-        return success ? "上报成功" : "上报失败";
+    @PostMapping("/alarmMessage")
+    public ApiResult<String> alarmMessage(@RequestBody AlarmMessageVO<?> vo) {
+        return toSubmitResult(alarmDataTransferService.publishAlarm(vo));
     }
 
-    @PostMapping("/alarmMessage3")
-    public String sendAlarmMessage3(@RequestBody AlarmMessageVO vo) {
-        boolean success = alarmDataTransferService.sendEngineeringBase(vo);
-        return success ? "上报成功" : "上报失败";
-    }
 }

+ 5 - 2
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/BaseDataController.java

@@ -78,8 +78,11 @@ public class BaseDataController {
      * 批量上报智能监管物联设施信息
      */
     @GetMapping("/sensorInfos")
-    public String batchSendSensorInfos(@RequestParam(value = "tenantId",required = false) Integer tenantId) {
-        Map<String, Integer> map = baseDataTransferService.batchSendSensorInfos(tenantId);
+    public String batchSendSensorInfos(@RequestParam(value = "tenantId",required = false) Integer tenantId,
+                                       @RequestParam(value = "engineeringId") Long engineeringId,
+                                       @RequestParam(value = "username") String username,
+                                       @RequestParam(value = "password") String password) {
+        Map<String, Integer> map = baseDataTransferService.batchSendSensorInfos(tenantId, engineeringId, username, password);
         return String.format("上报成功 %d", map.get("success"));
     }
 }

+ 1 - 1
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/CdiDeliveryLogService.java

@@ -27,5 +27,5 @@ public interface CdiDeliveryLogService extends CrudService<CdiDeliveryLog> {
 
     // 存储日志
     void saveLog(String topic, String dataTypeName, Integer dataType, Integer tenantId, Long engineeringId, LocalDateTime now, long startTime, long endTime,
-                 int total, int success, int failure, int notSynced, int pushFlag);
+                 int total, int success, int failure, int notSynced, int pushFlag, String userName);
 }

+ 6 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/config/DeviceFieldConfig.java

@@ -32,6 +32,9 @@ public class DeviceFieldConfig {
         // 电气火灾(704)
         fieldMapping.put("704", "voltage_a,voltage_b,voltage_c,current_a,current_b,current_c,temperature_a,temperature_b,temperature_c,current_residual,active_power");
 
+        // 电能采集(705)
+        fieldMapping.put("705", "electrical_energy");
+
         // 温度传感器(707)
         fieldMapping.put("707", "wd");
 
@@ -56,6 +59,9 @@ public class DeviceFieldConfig {
         // 位移传感器
         fieldMapping.put("714", "wy");
 
+        // 液位
+        fieldMapping.put("716", "sensorValue");
+
         return fieldMapping;
     }
 }

+ 4 - 1
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/enums/AlarmType.java

@@ -46,7 +46,10 @@ public enum AlarmType {
     TILT("712", "34.1", Category.STRUCTURE_MONITORING, "有倾斜"),
 
     // 裂缝检测告警
-    CRACK("713", "36.1", Category.STRUCTURE_MONITORING, "有裂缝");
+    CRACK("713", "36.1", Category.STRUCTURE_MONITORING, "有裂缝"),
+
+    // 集水井水位偏高
+    SEWAGE_LEVEL("716", "11.1", Category.STRUCTURE_MONITORING, "集水井水位偏高");
 
     private final String originalCode;
     private final String mappedCode;

+ 2 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/enums/MqttTopics.java

@@ -148,6 +148,8 @@ public final class MqttTopics {
                 return IotInfo.WATER_LEAK;
             case 714:
                 return IotInfo.DEVIATION;
+            case 719:
+                return IotInfo.PERSON;
             default:
                 return IotInfo.MONITORING_DATA;
         }

+ 15 - 8
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/AlarmDataSyncService.java

@@ -12,6 +12,7 @@ import com.usky.cdi.service.mqtt.MqttConnectionTool;
 import com.usky.cdi.service.util.SnowflakeIdGenerator;
 import com.usky.cdi.service.vo.alarm.AlarmMessageVO;
 import com.usky.cdi.service.enums.AlarmType;
+import com.usky.common.security.utils.SecurityUtils;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -21,11 +22,9 @@ import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.PostConstruct;
+import java.text.SimpleDateFormat;
 import java.time.LocalDateTime;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
+import java.util.*;
 import java.util.stream.Collectors;
 
 /**
@@ -49,6 +48,7 @@ public class AlarmDataSyncService {
     private static final String WARTIME = "wartime";
     private static final String ALARM_DELIVERY_KEY_PREFIX = "alarm:delivery:";
     private final MqttConnectionTool mqttConnectionTool;
+    private final SimpleDateFormat timeFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
 
     @Autowired
     private StringRedisTemplate stringRedisTemplate;
@@ -69,8 +69,8 @@ public class AlarmDataSyncService {
     /**
      * 获取当前时间
      */
-    private LocalDateTime getCurrentTime() {
-        return LocalDateTime.now();
+    private String getCurrentTime() {
+        return timeFormat.format(new Date());
     }
 
     /**
@@ -117,6 +117,13 @@ public class AlarmDataSyncService {
         String topic = MqttTopics.Alarm.MESSAGE.getTopic();
         String desc = MqttTopics.Alarm.MESSAGE.getDesc();
 
+        String userName = "自动同步";
+        try {
+            userName = SecurityUtils.getUsername();
+        } catch (Exception e) {
+            log.error("定时任务无法获取用户名,使用默认‘自动同步’", e);
+        }
+
         try {
             // 2.创建MQTT连接
             mqttConnectionTool.connectOrRefresh(username, password);
@@ -198,7 +205,7 @@ public class AlarmDataSyncService {
             endTime = System.currentTimeMillis();
 
             cdiDeliveryLogService.saveLog(topic, desc, 5, tenantId, engineeringId, now, startTime, endTime, size,
-                    successCount, failureCount, size - successCount - failureCount, 1);
+                    successCount, failureCount, size - successCount - failureCount, 1, userName);
         } catch (Exception e) {
             log.error("租户{}的告警数据推送定时任务执行失败:{}", tenantId, e.getMessage(), e);
         } finally {
@@ -206,7 +213,7 @@ public class AlarmDataSyncService {
             log.info("结束时间:{}, 耗时:{}ms", getCurrentTime(), endTime - startTime);
 
             cdiDeliveryLogService.saveLog(topic, desc, 5, tenantId, engineeringId, now, startTime, endTime, size,
-                    successCount, failureCount, size - successCount - failureCount, 0);
+                    successCount, failureCount, size - successCount - failureCount, 0, userName);
         }
     }
 

+ 31 - 146
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/AlarmDataTransferService.java

@@ -1,26 +1,17 @@
 package com.usky.cdi.service.impl;
 
 import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONObject;
-// import com.alibaba.nacos.shaded.com.google.gson.Gson;
-import com.usky.cdi.service.config.mqtt.MqttOutConfig;
 import com.usky.cdi.service.mqtt.MqttConnectionTool;
 import com.usky.cdi.service.util.SnowflakeIdGenerator;
 import com.usky.cdi.service.vo.alarm.AlarmMessageVO;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.stereotype.Service;
 
-import javax.annotation.Resource;
-import java.text.SimpleDateFormat;
 import java.time.LocalDateTime;
-import java.util.Date;
-import java.util.HashMap;
+import java.time.format.DateTimeFormatter;
 
 /**
- * 告警类数据传输服务
- * 负责向市适配平台发送告警类数据
+ * 告警类数据传输服务:向市适配平台通过 MQTT 上报告警数据。
  *
  * @author han
  * @date 2025/12/08
@@ -29,162 +20,56 @@ import java.util.HashMap;
 @Service
 public class AlarmDataTransferService {
 
-    @Autowired
-    private MqttConnectionTool mqttConnectionTool;
-    @Resource
-    private MqttOutConfig.MqttGateway mqttGateway;
+    private static final String MQTT_USERNAME = "3101100021";
+    private static final String MQTT_PASSWORD = "SIixzph1";
+    private static final String ALARM_TOPIC = "alarm/message";
+    private static final DateTimeFormatter PUBLISH_TIME_FORMAT =
+            DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
 
-    private final SnowflakeIdGenerator idGenerator;
-    private final SimpleDateFormat timeFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
+    private final MqttConnectionTool mqttConnectionTool;
+    private final SnowflakeIdGenerator idGenerator = new SnowflakeIdGenerator(3L, 3L);
 
-    public AlarmDataTransferService() {
-        // 使用默认的workerId和datacenterId,实际项目中可以从配置读取
-        this.idGenerator = new SnowflakeIdGenerator(3L, 3L);
+    public AlarmDataTransferService(MqttConnectionTool mqttConnectionTool) {
+        this.mqttConnectionTool = mqttConnectionTool;
     }
 
-    /**
-     * 获取当前时间字符串
-     */
     private String getCurrentTime() {
-        return timeFormat.format(new Date());
+        return LocalDateTime.now().format(PUBLISH_TIME_FORMAT);
     }
 
-    /**
-     * 生成数据包ID
-     */
-    private Long generateDataPacketID() {
+    private long generateDataPacketId() {
         return idGenerator.nextPacketId();
     }
 
     /**
-     * 发送告警信息
-     * Topic: base/floorPlane
+     * 上报告警消息至 MQTT(topic: alarm/message)。
      *
-     * @param vo 楼层平面图信息
-     * @return 是否发送成功
+     * @return 是否已成功投递到底层网关(不代表敌方平台一定消费成功)
      */
-    public boolean sendAlarmMessage(AlarmMessageVO vo) {
-        try {
-            if (vo.getDataPacketID() == null) {
-                vo.setDataPacketID(generateDataPacketID());
-            }
-            if (vo.getPublishTime() == null) {
-                vo.setPublishTime(LocalDateTime.now());
-            }
-
-//            HashMap<String, Object> map = new HashMap<>();
-//            map.put("dataPacketID", vo.getDataPacketID());
-//            map.put("engineeringID", vo.getEngineeringID());
-//            map.put("floor", vo.getFloor());
-//            map.put("floorFileID", vo.getFloorFileID());
-//            map.put("floorFileName", vo.getFloorFileName());
-//            map.put("floorFileSuffix", vo.getFloorFileSuffix());
-//            map.put("filePixWidth", vo.getFilePixWidth());
-//            map.put("filePixHeight", vo.getFilePixHeight());
-//            map.put("floorFile", imageBytes);
-//            map.put("publishTime", vo.getPublishTime());
-//            Gson gson = new Gson();
-            JSONObject jsonObject = (JSONObject) JSON.toJSON(vo);
-            String json = jsonObject.toJSONString();
-            System.out.println(json);
-            MqttConnectionTool.MqttGateway IQeIRyXG = mqttConnectionTool.connectOrRefresh("3101070011", "5RqhJ7VG");
-            String topic = "alarm/message";
-            IQeIRyXG.sendToMqtt(topic, json);
-
-            return true;
-        } catch (Exception e) {
-            log.error("发送告警信息失败,AlarmID: {}", vo.getAlarmID(), e);
-            return false;
-        }
-    }
-
-    /**
-     * 发送告警信息
-     * Topic: base/floorPlane
-     *
-     * @param vo 楼层平面图信息
-     * @return 是否发送成功
-     */
-    public boolean sendAlarmMessage1(AlarmMessageVO vo) {
-        MqttConnectionTool.MqttGateway IQeIRyXG = mqttConnectionTool.connectOrRefresh("3101070011", "5RqhJ7VG");
-
+    public boolean publishAlarm(AlarmMessageVO<?> vo) {
         try {
-            if (vo.getDataPacketID() == null) {
-                vo.setDataPacketID(generateDataPacketID());
-            }
-            if (vo.getPublishTime() == null) {
-                vo.setPublishTime(LocalDateTime.now());
-            }
-
-            HashMap<String, Object> map = new HashMap<>();
-//            map.put("dataPacketID", vo.getDataPacketID());
-//            map.put("engineeringID", vo.getEngineeringID());
-//            map.put("floor", vo.getFloor());
-//            map.put("floorFileID", vo.getFloorFileID());
-//            map.put("floorFileName", vo.getFloorFileName());
-//            map.put("floorFileSuffix", vo.getFloorFileSuffix());
-//            map.put("filePixWidth", vo.getFilePixWidth());
-//            map.put("filePixHeight", vo.getFilePixHeight());
-//            map.put("floorFile", imageBytes);
-//            map.put("publishTime", vo.getPublishTime());
-//             Gson gson = new Gson();
-            JSONObject jsonObject = (JSONObject) JSON.toJSON(vo);
-            String json = jsonObject.toJSONString();
-            System.out.println(json);
-            String topic = "alarm/message";
-            IQeIRyXG.sendToMqtt(topic, json);
-
+            fillDefaults(vo);
+            String json = JSON.toJSONString(vo);
+            log.info("告警 MQTT 载荷: {}", json);
+            MqttConnectionTool.MqttGateway gateway =
+                    mqttConnectionTool.connectOrRefresh(MQTT_USERNAME, MQTT_PASSWORD);
+            gateway.sendToMqtt(ALARM_TOPIC, json);
             return true;
         } catch (Exception e) {
-            log.error("发送告警信息失败,AlarmID: {}", vo.getAlarmID(), e);
+            log.error("发送告警信息失败, alarmID: {}, engineeringID: {}",
+                    vo != null ? vo.getAlarmID() : null,
+                    vo != null ? vo.getEngineeringID() : null,
+                    e);
             return false;
         }
     }
 
-    public boolean sendAlarmMessage2(AlarmMessageVO vo) {
-        try {
-            if (vo.getDataPacketID() == null) {
-                vo.setDataPacketID(generateDataPacketID());
-            }
-            if (vo.getPublishTime() == null) {
-                vo.setPublishTime(LocalDateTime.now());
-            }
-
-            JSONObject jsonObject = (JSONObject) JSON.toJSON(vo);
-            String json = jsonObject.toJSONString();
-            System.out.println(json);
-            MqttConnectionTool.MqttGateway IQeIRyXG = mqttConnectionTool.connectOrRefresh("3101070011", "5RqhJ7VG");
-            String topic = "alarm/message";
-            IQeIRyXG.sendToMqtt(topic, json);
-
-            return true;
-        } catch (Exception e) {
-            log.error("发送告警信息失败,AlarmID: {}", vo.getAlarmID(), e);
-            return false;
+    private void fillDefaults(AlarmMessageVO<?> vo) {
+        if (vo.getDataPacketID() == null) {
+            vo.setDataPacketID(generateDataPacketId());
         }
-    }
-
-    public boolean sendEngineeringBase(AlarmMessageVO vo) {
-        try {
-            if (vo.getDataPacketID() == null) {
-                vo.setDataPacketID(generateDataPacketID());
-            }
-            if (vo.getPublishTime() == null) {
-                vo.setPublishTime(LocalDateTime.now());
-            }
-
-            MqttConnectionTool.MqttGateway IQeIRyXG = mqttConnectionTool.connectOrRefresh("3101070011", "5RqhJ7VG");
-
-            JSONObject jsonObject = (JSONObject) JSON.toJSON(vo);
-            String json = jsonObject.toJSONString();
-            String topic = "alarm/message";
-            System.out.println("推送的数据: " + json);
-            IQeIRyXG.sendToMqtt(topic, json);
-
-            return true;
-        } catch (Exception e) {
-            log.error("发送电流告警信息失败,EngineeringID: {}", vo.getEngineeringID(), e);
-            return false;
+        if (vo.getPublishTime() == null) {
+            vo.setPublishTime(getCurrentTime());
         }
     }
 }

+ 55 - 16
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/BaseDataTransferService.java

@@ -3,13 +3,15 @@ package com.usky.cdi.service.impl;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
 import com.alibaba.nacos.shaded.com.google.gson.Gson;
+import com.alibaba.nacos.shaded.com.google.gson.GsonBuilder;
+import com.alibaba.nacos.shaded.com.google.gson.LongSerializationPolicy;
 import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
 import com.usky.cdi.domain.BaseBuildFacility;
 import com.usky.cdi.domain.DmpDevice;
 import com.usky.cdi.service.BaseBuildFacilityService;
-import com.usky.cdi.service.CdiDeliveryLogService;
 import com.usky.cdi.service.DmpDeviceInfoService;
 import com.usky.cdi.service.config.mqtt.MqttOutConfig;
+import com.usky.cdi.service.mqtt.MqttConnectionTool;
 import com.usky.cdi.service.util.SnowflakeIdGenerator;
 import com.usky.cdi.service.vo.info.EngineeringBaseVO;
 import com.usky.cdi.service.vo.info.FacilityDeviceVO;
@@ -22,9 +24,14 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
 import java.nio.file.Files;
 import java.nio.file.Paths;
 import java.text.SimpleDateFormat;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.*;
 
 /**
@@ -48,8 +55,11 @@ public class BaseDataTransferService {
     @Resource
     private MqttOutConfig.MqttGateway mqttGateway;
 
-    @Value("${config.engineeringID}")
-    private String engineeringID;
+    // @Value("${config.engineeringID}")
+    // private String engineeringID;
+
+    @Autowired
+    private MqttConnectionTool mqttConnectionTool;
 
     private final SnowflakeIdGenerator idGenerator;
     private final SimpleDateFormat timeFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
@@ -59,9 +69,21 @@ public class BaseDataTransferService {
         this.idGenerator = new SnowflakeIdGenerator(1L, 1L);
     }
 
-    /**
-     * 获取当前时间字符串
-     */
+    private String convertFloor(String floor) {
+        if (floor == null || floor.trim().isEmpty()) {
+            return floor;
+        }
+        try {
+            int floorNum = Integer.parseInt(floor.trim());
+            if (floorNum < 0) {
+                return "B" + Math.abs(floorNum);
+            }
+        } catch (NumberFormatException e) {
+            log.warn("楼层格式转换失败,原始值: {}", floor);
+        }
+        return floor;
+    }
+
     private String getCurrentTime() {
         return timeFormat.format(new Date());
     }
@@ -122,7 +144,8 @@ public class BaseDataTransferService {
             String topic = "base/protectiveUnit";
 
             log.info("发送防护单元基础信息,Topic: {}, Data: {}", topic, json);
-            mqttGateway.sendToMqtt(topic, json);
+            MqttConnectionTool.MqttGateway gateway = mqttConnectionTool.connectOrRefresh(vo.getUserName(), vo.getPassword());
+            gateway.sendToMqtt(topic, json);
 
             return true;
         } catch (Exception e) {
@@ -140,6 +163,7 @@ public class BaseDataTransferService {
      */
     public boolean sendFloorPlane(FloorPlaneVO vo) {
         try {
+            // ========== 1. 基础参数填充 ==========
             if (vo.getDataPacketID() == null) {
                 vo.setDataPacketID(generateDataPacketID());
             }
@@ -147,7 +171,7 @@ public class BaseDataTransferService {
                 vo.setPublishTime(getCurrentTime());
             }
 
-            String imagePath = "D://games/3492.jpg";
+            String imagePath = "D://142.jpg";
             // 将图片文件读取为字节数组
             byte[] imageBytes = Files.readAllBytes(Paths.get(imagePath));
 
@@ -186,7 +210,9 @@ public class BaseDataTransferService {
             log.info("发送楼层平面图信息,Topic: {}, FileID: {}, FileSize: {} bytes",
                     topic, vo.getFloorFileID(),
                     vo.getFloorFile() != null ? vo.getFloorFile().length : 0);
-            mqttGateway.sendToMqtt(topic, gson.toJson(map));
+
+            MqttConnectionTool.MqttGateway gateway = mqttConnectionTool.connectOrRefresh(vo.getUserName(), vo.getPassword());
+            gateway.sendToMqtt(topic, gson.toJson(map));
 
             return true;
         } catch (Exception e) {
@@ -221,21 +247,30 @@ public class BaseDataTransferService {
         try {
             Map<Integer, Integer> userIdToName = new HashMap<>();
             userIdToName.put(702, 31);
+            // 30:网络监控摄像机(可兼用平时摄像机)
+            // userIdToName.put(717, 30);
+            // 20:人员统计-掩蔽人数双目统计摄像机
+            // userIdToName.put(719, 20);
+            // 33:人员闯入-人员闯入监测传感器
             userIdToName.put(703, 33);
             userIdToName.put(704, 11);
+            userIdToName.put(705, 11);
             userIdToName.put(707, 19);
             userIdToName.put(708, 19);
             userIdToName.put(709, 15);
             userIdToName.put(710, 16);
             userIdToName.put(711, 2);
-            //userIdToName.put(712, 34);
-            //userIdToName.put(713, 36);
+            // userIdToName.put(712, 34);
+            // userIdToName.put(713, 36);
             userIdToName.put(714, 37);
+            userIdToName.put(716, 26);
+            // 柴油发电机蓄电池监测传感器
+            userIdToName.put(720, 12);
 
             HashMap<String, Object> map = new HashMap<>();
             map.put("dataPacketID", generateDataPacketID());
-            map.put("engineeringID", Long.parseLong(engineeringID));
-            map.put("floor", "B2");
+            map.put("engineeringID", vo.getEngineeringID());
+            map.put("floor", convertFloor(vo.getFloor()));
             map.put("floorFileID", 1);
             map.put("sensorID", Integer.parseInt(vo.getDeviceId()));
             map.put("sensorNo", vo.getDeviceUuid());
@@ -252,8 +287,9 @@ public class BaseDataTransferService {
             Gson gson = new Gson();
             String topic = "base/sensorInfo";
             System.out.println(gson.toJson(map));
-//            log.info("发送智能监管物联设施信息,Topic: {}, SensorID: {}", topic, vo.getSensorID());
-            mqttGateway.sendToMqtt(topic, gson.toJson(map));
+            log.info("发送智能监管物联设施信息,Topic: {}, SensorID: {}", topic, vo.getDeviceId());
+            MqttConnectionTool.MqttGateway gateway = mqttConnectionTool.connectOrRefresh(vo.getUserName(), vo.getPassword());
+            gateway.sendToMqtt(topic, gson.toJson(map));
 
             return true;
         } catch (Exception e) {
@@ -290,7 +326,7 @@ public class BaseDataTransferService {
      * @param tenantId 租户ID
      * @return 成功发送的数量
      */
-    public Map<String, Integer> batchSendSensorInfos(Integer tenantId) {
+    public Map<String, Integer> batchSendSensorInfos(Integer tenantId, Long engineeringId, String username, String password) {
         List<BaseBuildFacility> list = baseBuildFacilityService.facilityInfo(tenantId);
         List<DmpDevice> list1 = dmpDeviceInfoService.deviceInfo(tenantId);
         List<FacilityDeviceVO> list2 = new ArrayList<>();
@@ -309,6 +345,9 @@ public class BaseDataTransferService {
                         facilityDeviceVO.setDeviceUuid(list1.get(k).getDeviceUuid());
                         facilityDeviceVO.setFacilityDesc(list.get(j).getFacilityDesc());
                         facilityDeviceVO.setDeviceType(list1.get(k).getDeviceType());
+                        facilityDeviceVO.setEngineeringID(engineeringId);
+                        facilityDeviceVO.setUserName(username);
+                        facilityDeviceVO.setPassword(password);
                         list2.add(facilityDeviceVO);
                     }
                 }

+ 468 - 222
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/CdiDeliveryLogServiceImpl.java

@@ -3,12 +3,13 @@ package com.usky.cdi.service.impl;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.date.DateUtil;
 import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.io.IoUtil;
 import cn.hutool.core.lang.Assert;
 import cn.hutool.core.util.StrUtil;
-import cn.hutool.json.JSONArray;
 import cn.hutool.json.JSONObject;
 import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
+import com.alibaba.nacos.shaded.com.google.gson.Gson;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -17,6 +18,7 @@ import com.usky.cdi.service.enums.MqttTopics;
 import com.usky.cdi.mapper.*;
 import com.usky.cdi.service.CdiDeliveryLogService;
 import com.usky.cdi.service.mqtt.MqttConnectionTool;
+import com.usky.cdi.service.util.SnowflakeIdGenerator;
 import com.usky.cdi.service.vo.SyncTaskStatisticsVO;
 import com.usky.cdi.service.vo.info.FloorPlaneVO;
 import com.usky.cdi.service.vo.info.ProtectiveUnitVO;
@@ -29,15 +31,21 @@ import org.apache.commons.collections4.CollectionUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
+import org.springframework.beans.factory.annotation.Value;
 
+import javax.annotation.PostConstruct;
 import javax.imageio.ImageIO;
 import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
 import java.math.BigDecimal;
+import java.net.HttpURLConnection;
 import java.net.URL;
 import java.net.URLConnection;
+import java.net.URLEncoder;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
 import java.util.*;
@@ -84,108 +92,60 @@ public class CdiDeliveryLogServiceImpl extends AbstractCrudService<CdiDeliveryLo
     @Autowired
     private MqttConnectionTool mqttConnectionTool;
 
+    // 从配置文件读取Snowflake参数,默认值为1
+    @Value("${snowflake.worker-id:1}")
+    private long workerId;
+
+    @Value("${snowflake.data-center-id:1}")
+    private long dataCenterId;
+
+    private SnowflakeIdGenerator idGenerator;
+
+    @PostConstruct
+    public void init() {
+        this.idGenerator = new SnowflakeIdGenerator(workerId, dataCenterId);
+    }
+
+    /**
+     * 生成数据包ID
+     */
+    private Long generateDataPacketID() {
+        return idGenerator.nextPacketId();
+    }
+
     @Override
     public List<SyncTaskStatisticsVO> selectById(Long id) {
-        // 1. 租户ID校验(必须非空,无租户直接返回空列表)
         Integer tenantId = SecurityUtils.getTenantId();
-        if (tenantId == null) {
+        if (tenantId == null || tenantId <= 0) {
             log.warn("未获取到当前租户ID,无法查询人防投递日志");
             return Collections.emptyList();
         }
 
-        // 2. 动态构建查询条件:id为null时只查租户,id不为null时租户+id精准查
-        // 【小优化】按ID倒序,后续取最新数据更直观(ID自增则大ID是最新)
         List<CdiDeliveryLog> logList = lambdaQuery()
                 .eq(CdiDeliveryLog::getTenantId, tenantId)
                 .eq(id != null, CdiDeliveryLog::getId, id)
-                .orderByDesc(CdiDeliveryLog::getId) // 改为倒序,优先最新数据
+                .orderByDesc(CdiDeliveryLog::getId)
                 .list();
 
         LambdaQueryWrapper<CdiDefenseProject> queryWrapper = new LambdaQueryWrapper<>();
         queryWrapper.eq(CdiDefenseProject::getTenantId, tenantId);
-        // 【空指针防护】新增非空判断,避免selectOne返回null时报错
         CdiDefenseProject defenseProject = cdiDefenseProjectMapper.selectOne(queryWrapper);
         boolean isEnable = defenseProject != null && defenseProject.getIsEnable() == 0;
 
         List<SyncTaskStatisticsVO> finalResult = new ArrayList<>();
 
-        // 3. 日志集合判空:无数据查询设备表(原逻辑不变)
-        if (CollectionUtils.isEmpty(logList)) {
-
-            // 单元数据
-            List<BaseBuildUnit> buildUnitList = getBuildUnitList(tenantId);
-            SyncTaskStatisticsVO vo1 = new SyncTaskStatisticsVO();
-            vo1.setDataTypeName(MqttTopics.Base.PROTECTIVE_UNIT.getDesc());
-            vo1.setTopic(MqttTopics.Base.PROTECTIVE_UNIT.getTopic());
-            vo1.setDataType(1);
-            vo1.setTotal(buildUnitList.size());
-            vo1.setSuccessNumber(0);
-            vo1.setFailNumber(0);
-            vo1.setNotSynced(0);
-            vo1.setState(isEnable ? 1 : 0);
-            finalResult.add(vo1);
-
-            // 平面图
-            List<Integer> buildIdList = getBuildList(tenantId).stream().map(BaseBuild::getId).collect(Collectors.toList());
-            List<BaseBuildPlane> buildPlaneList = getBuildPlaneList(buildIdList);
-            SyncTaskStatisticsVO vo4 = new SyncTaskStatisticsVO();
-            vo4.setDataTypeName(MqttTopics.Base.FLOOR_PLANE.getDesc());
-            vo4.setTopic(MqttTopics.Base.FLOOR_PLANE.getTopic());
-            vo4.setDataType(2);
-            vo4.setTotal(buildPlaneList.size());
-            vo4.setSuccessNumber(0);
-            vo4.setFailNumber(0);
-            vo4.setNotSynced(0);
-            vo4.setState(isEnable ? 1 : 0);
-            finalResult.add(vo4);
-
-            // 设施数据
-            List<BaseBuildFacility> buildFacilityList = getBuildFacilityList(tenantId);
-            SyncTaskStatisticsVO vo2 = new SyncTaskStatisticsVO();
-            vo2.setDataTypeName(MqttTopics.Base.SENSOR_INFO.getDesc());
-            vo2.setTopic(MqttTopics.Base.SENSOR_INFO.getTopic());
-            vo2.setDataType(3);
-            vo2.setTotal(buildFacilityList.size());
-            vo2.setSuccessNumber(0);
-            vo2.setFailNumber(0);
-            vo2.setNotSynced(0);
-            vo2.setState(isEnable ? 1 : 0);
-            finalResult.add(vo2);
-
-            // 监测数据
-            SyncTaskStatisticsVO vo3 = new SyncTaskStatisticsVO();
-            vo3.setDataTypeName(MqttTopics.IotInfo.MONITORING_DATA.getDesc());
-            vo3.setTopic(MqttTopics.IotInfo.MONITORING_DATA.getTopic());
-            vo3.setDataType(4);
-            vo3.setTotal(buildFacilityList.size());
-            vo3.setSuccessNumber(0);
-            vo3.setFailNumber(0);
-            vo3.setNotSynced(0);
-            vo3.setState(isEnable ? 1 : 0);
-            finalResult.add(vo3);
-
-            SyncTaskStatisticsVO vo5 = new SyncTaskStatisticsVO();
-            vo5.setDataTypeName(MqttTopics.Alarm.MESSAGE.getDesc());
-            vo5.setTopic(MqttTopics.Alarm.MESSAGE.getTopic());
-            vo5.setDataType(5);
-            vo5.setTotal(0);
-            vo5.setSuccessNumber(0);
-            vo5.setFailNumber(0);
-            vo5.setNotSynced(0);
-            vo5.setState(isEnable ? 1 : 0);
-            finalResult.add(vo5);
+        // 预先查询各类型的基础数据量(无日志时使用)
+        List<BaseBuildUnit> buildUnitList = getBuildUnitList(tenantId);
+        List<BaseBuild> buildList = getBuildList(tenantId);
+        List<Integer> buildIdList = buildList.stream().map(BaseBuild::getId).collect(Collectors.toList());
+        List<BaseBuildPlane> buildPlaneList = buildIdList.isEmpty() ? Collections.emptyList() : getBuildPlaneList(buildIdList);
+        List<BaseBuildFacility> buildFacilityList = getBuildFacilityList(tenantId);
 
+        if (CollectionUtils.isEmpty(logList)) {
+            fillEmptyStatistics(finalResult, tenantId, isEnable);
             return finalResult;
         }
 
-        // 优先自动解析:JSON数组直接转VO列表,简洁高效
-        //     JSONArray jsonArray = JSONUtil.parseArray(jsonContent);
-        //     List<SyncTaskStatisticsVO> autoParseList = JSONUtil.toList(jsonArray, SyncTaskStatisticsVO.class);
-        //     finalResult.addAll(autoParseList);
-        //     log.info("租户ID:{} 日志ID:{} 自动解析成功,解析出{}条同步统计数据", tenantId, logId, autoParseList.size());
-
-        // ########## 核心改造:按dataType分组,取每种类型最新的一条日志 ##########
-        // 步骤1:先过滤出infoContent非空的有效日志(提前过滤,减少分组计算量)
         List<CdiDeliveryLog> validLogList = logList.stream()
                 .filter(logEntity -> logEntity != null
                         && StrUtil.isNotBlank(logEntity.getInfoContent())
@@ -193,130 +153,185 @@ public class CdiDeliveryLogServiceImpl extends AbstractCrudService<CdiDeliveryLo
                 .collect(Collectors.toList());
 
         if (CollectionUtils.isEmpty(validLogList)) {
-            log.warn("租户ID:{} 查询到{}条日志,但所有日志的JSON内容为空或dataType为空,查询ID:{}", tenantId, logList.size(), id);
-            return Collections.emptyList();
+            log.warn("租户ID:{} 查询到{}条日志,但所有日志的JSON内容为空或dataType为空", tenantId, logList.size());
+            fillEmptyStatistics(finalResult, tenantId, isEnable);
+            return finalResult;
         }
 
-        // 步骤2:按dataType分组,每组按ID倒序(最新)取第一条,保证1-4类型各一条
-        Map<Integer, CdiDeliveryLog> latestLogByType = validLogList.stream()
-                .collect(Collectors.groupingBy(
-                        CdiDeliveryLog::getDataType, // 分组键:dataType(1-4)
-                        Collectors.collectingAndThen(
-                                Collectors.maxBy(Comparator.comparingLong(CdiDeliveryLog::getId)), // 取组内ID最大的(最新)
-                                opt -> opt.orElse(null) // 空值处理
-                        )
-                ));
+        // ★ 改动1:按 dataType 分组,保留每种类型的全部日志
+        Map<Integer, List<CdiDeliveryLog>> allLogsByType = validLogList.stream()
+                .collect(Collectors.groupingBy(CdiDeliveryLog::getDataType));
 
-        // 步骤3:转换为列表,仅保留1-4类型的最新日志(过滤非目标类型)
-        List<CdiDeliveryLog> finalValidLogList = new ArrayList<>();
         for (int type = 1; type <= 5; type++) {
-            CdiDeliveryLog latestLog = latestLogByType.get(type);
-            if (latestLog != null) {
-                finalValidLogList.add(latestLog);
+            List<CdiDeliveryLog> logsForType = allLogsByType.get(type);
+
+            // 创建基础 VO,设置 dataType 和 topic
+            SyncTaskStatisticsVO vo = createEmptyVO(type, isEnable);
+
+            // ★ total 始终从数据库查询获取(单元表、平面图表等),与日志记录无关
+            switch (type) {
+                case 1:
+                    vo.setTotal(buildUnitList.size());
+                    break;
+                case 2:
+                    vo.setTotal(buildPlaneList.size());
+                    break;
+                case 3:
+                case 4:
+                    vo.setTotal(buildFacilityList.isEmpty() ? 0 : buildFacilityList.size());
+                    break;
+                case 5:
+                default:
+                    break;
             }
-            // 若某类型无日志,无需处理:后续解析后若该类型无数据,是否补空VO可按需求调整,原逻辑是返回解析到的内容
-        }
 
-        if (CollectionUtils.isEmpty(finalValidLogList)) {
-            log.warn("租户ID:{} 无1-4类型的有效日志,查询ID:{}", tenantId, id);
-            return Collections.emptyList();
-        }
-        // ########## 核心改造结束 ##########
+            if (logsForType != null && !logsForType.isEmpty()) {
+                // ★ 有日志时:按类型累加 success / fail / notSynced
+                int sumSuccess = 0, sumFail = 0, sumNotSynced = 0;
 
-        // 5. 遍历【每种类型最新的日志】,解析JSON并合并结果(单日志解析失败不影响其他)
-        for (CdiDeliveryLog deliveryLog : finalValidLogList) {
-            Long logId = deliveryLog.getId();
-            String jsonContent = deliveryLog.getInfoContent();
-            try {
-                JSONArray array = JSONUtil.parseArray(jsonContent);
-                List<SyncTaskStatisticsVO> manualParseList = new ArrayList<>(array.size());
-                for (int i = 0; i < array.size(); i++) {
-                    JSONObject obj = array.getJSONObject(i);
-                    SyncTaskStatisticsVO vo = new SyncTaskStatisticsVO();
-                    // 手动映射字段:按实际入库的JSON字段名匹配
-                    vo.setId(obj.getLong("id"));
-                    vo.setTopic(obj.getStr("topic"));
-                    switch (obj.getInt("dataType")) {
-                        case 1:
-                            vo.setDataTypeName(MqttTopics.Base.PROTECTIVE_UNIT.getDesc());
-                            break;
-                        case 2:
-                            vo.setDataTypeName(MqttTopics.Base.FLOOR_PLANE.getDesc());
-                            break;
-                        case 3:
-                            vo.setDataTypeName(MqttTopics.Base.SENSOR_INFO.getDesc());
-                            break;
-                        case 4:
-                            vo.setDataTypeName(MqttTopics.IotInfo.MONITORING_DATA.getDesc());
-                            break;
-                        case 5:
-                            vo.setDataTypeName(MqttTopics.Alarm.MESSAGE.getDesc());
-                            break;
+                for (CdiDeliveryLog logEntry : logsForType) {
+                    try {
+                        // ★ infoContent 是 JSON 对象,用 parseObj 解析
+                        JSONObject o = JSONUtil.parseObj(logEntry.getInfoContent());
+                        sumSuccess += o.getInt("successNumber");
+                        sumFail += o.getInt("failNumber");
+                        sumNotSynced += o.getInt("notSynced");
+                    } catch (Exception ignored) {
+                        // 单条解析失败不影响其他日志的累加
+                    }
+                }
+
+                vo.setSuccessNumber(sumSuccess);
+                vo.setFailNumber(sumFail);
+                vo.setNotSynced(sumNotSynced);
+
+                // 取最新一条日志作为元数据来源(createTime / costTime 等)
+                CdiDeliveryLog latestLog = logsForType.stream()
+                        .max(Comparator.comparingLong(CdiDeliveryLog::getId))
+                        .orElse(null);
+
+                if (latestLog != null) {
+                    try {
+                        // ★ infoContent 是 JSON 对象,用 parseObj 解析
+                        JSONObject obj = JSONUtil.parseObj(latestLog.getInfoContent());
+                        vo.setId(obj.getLong("id"));
+                        vo.setCreateTime(obj.getStr("createTime"));
+                        vo.setCostTime(obj.getFloat("costTime"));
+                        vo.setState(isEnable ? 1 : obj.getInt("state"));
+                    } catch (Exception ex) {
+                        log.error("租户ID:{} dataType:{} 解析最新日志元数据失败", tenantId, type, ex);
                     }
-                    vo.setDataType(obj.getInt("dataType"));
-                    vo.setTotal(obj.getInt("total"));
-                    vo.setSuccessNumber(obj.getInt("successNumber"));
-                    vo.setFailNumber(obj.getInt("failNumber"));
-                    vo.setNotSynced(obj.getInt("notSynced"));
-                    vo.setCreateTime(obj.getStr("createTime"));
-                    vo.setCostTime(obj.getFloat("costTime"));
-                    vo.setState(isEnable ? 1 : obj.getInt("state"));
-                    manualParseList.add(vo);
                 }
-                finalResult.addAll(manualParseList);
-                log.info("租户ID:{} 日志ID:{} 手动解析成功,解析出{}条同步统计数据", tenantId, logId, manualParseList.size());
-            } catch (Exception ex) {
-                // 单日志解析失败,仅打印日志,继续解析其他日志
-                log.error("租户ID:{} 日志ID:{} 解析失败,跳过该日志", tenantId, logId, ex);
+
+                log.info("租户ID:{} dataType:{} 共{}条日志, 聚合结果: total={}, success={}, fail={}, notSynced={}",
+                        tenantId, type, logsForType.size(), vo.getTotal(), sumSuccess, sumFail, sumNotSynced);
+
+            } else {
+                // ★ 无日志时:success/fail/notSynced 保持为0(createEmptyVO已设置)
             }
+
+            finalResult.add(vo);
         }
 
-        // // 6. 【可选优化】若解析后部分类型缺失,补全空VO(和无日志时格式完全一致)
-        // // 提取已解析的dataType
-        // Set<Integer> parsedTypes = finalResult.stream()
-        //         .map(SyncTaskStatisticsVO::getDataType)
-        //         .filter(Objects::nonNull)
-        //         .collect(Collectors.toSet());
-        // // 补全1-4中缺失的类型,设置默认值(和无日志时一致)
-        // for (int type = 1; type <= 3; type++) {
-        //     if (!parsedTypes.contains(type)) {
-        //         SyncTaskStatisticsVO emptyVo = new SyncTaskStatisticsVO();
-        //         // 按类型设置名称、主题,和情况一保持一致
-        //         switch (type) {
-        //             case 1:
-        //                 emptyVo.setDataTypeName(MqttTopics.Base.PROTECTIVE_UNIT.getDesc());
-        //                 emptyVo.setTopic(MqttTopics.Base.PROTECTIVE_UNIT.getTopic());
-        //                 break;
-        //             case 2:
-        //                 emptyVo.setDataTypeName(MqttTopics.Base.FLOOR_PLANE.getDesc());
-        //                 emptyVo.setTopic(MqttTopics.Base.FLOOR_PLANE.getTopic());
-        //                 break;
-        //             case 3:
-        //                 emptyVo.setDataTypeName(MqttTopics.Base.SENSOR_INFO.getDesc());
-        //                 emptyVo.setTopic(MqttTopics.Base.SENSOR_INFO.getTopic());
-        //                 break;
-        //             // case 4:
-        //             //     emptyVo.setDataTypeName(MqttTopics.IotInfo.MONITORING_DATA.getDesc());
-        //             //     emptyVo.setTopic(MqttTopics.IotInfo.MONITORING_DATA.getTopic());
-        //             //     break;
-        //         }
-        //         emptyVo.setDataType(type);
-        //         emptyVo.setTotal(0);
-        //         emptyVo.setSuccessNumber(0);
-        //         emptyVo.setFailNumber(0);
-        //         emptyVo.setNotSynced(0);
-        //         emptyVo.setState(isEnable ? 1 : 0);
-        //         finalResult.add(emptyVo);
-        //     }
-        // }
-
-        // 对结果按dataType排序(1-4),和情况一返回顺序一致
         finalResult.sort(Comparator.comparingInt(SyncTaskStatisticsVO::getDataType));
-
-        // 6. 返回结果
         return finalResult;
     }
 
+    private void fillEmptyStatistics(List<SyncTaskStatisticsVO> result, Integer tenantId, boolean isEnable) {
+        List<BaseBuildUnit> buildUnitList = getBuildUnitList(tenantId);
+        SyncTaskStatisticsVO vo1 = createEmptyVO(1, isEnable);
+        vo1.setTotal(buildUnitList.size());
+        result.add(vo1);
+
+        List<Integer> buildIdList = getBuildList(tenantId).stream().map(BaseBuild::getId).collect(Collectors.toList());
+        List<BaseBuildPlane> buildPlaneList = buildIdList.isEmpty() ? Collections.emptyList() : getBuildPlaneList(buildIdList);
+        SyncTaskStatisticsVO vo2 = createEmptyVO(2, isEnable);
+        vo2.setTotal(buildPlaneList.size());
+        result.add(vo2);
+
+        List<BaseBuildFacility> buildFacilityList = getBuildFacilityList(tenantId);
+        SyncTaskStatisticsVO vo3 = createEmptyVO(3, isEnable);
+        vo3.setTotal(buildFacilityList.isEmpty() ? 0 : buildFacilityList.size());
+        result.add(vo3);
+
+        SyncTaskStatisticsVO vo4 = createEmptyVO(4, isEnable);
+        vo4.setTotal(buildFacilityList.isEmpty() ? 0 : buildFacilityList.size());
+        result.add(vo4);
+
+        SyncTaskStatisticsVO vo5 = createEmptyVO(5, isEnable);
+        result.add(vo5);
+    }
+
+    private SyncTaskStatisticsVO createEmptyVO(int dataType, boolean isEnable) {
+        SyncTaskStatisticsVO vo = new SyncTaskStatisticsVO();
+        vo.setDataType(dataType);
+        vo.setSuccessNumber(0);
+        vo.setFailNumber(0);
+        vo.setNotSynced(0);
+        vo.setState(isEnable ? 1 : 0);
+
+        switch (dataType) {
+            case 1:
+                vo.setDataTypeName(MqttTopics.Base.PROTECTIVE_UNIT.getDesc());
+                vo.setTopic(MqttTopics.Base.PROTECTIVE_UNIT.getTopic());
+                break;
+            case 2:
+                vo.setDataTypeName(MqttTopics.Base.FLOOR_PLANE.getDesc());
+                vo.setTopic(MqttTopics.Base.FLOOR_PLANE.getTopic());
+                break;
+            case 3:
+                vo.setDataTypeName(MqttTopics.Base.SENSOR_INFO.getDesc());
+                vo.setTopic(MqttTopics.Base.SENSOR_INFO.getTopic());
+                break;
+            case 4:
+                vo.setDataTypeName(MqttTopics.IotInfo.MONITORING_DATA.getDesc());
+                vo.setTopic(MqttTopics.IotInfo.MONITORING_DATA.getTopic());
+                break;
+            case 5:
+                vo.setDataTypeName(MqttTopics.Alarm.MESSAGE.getDesc());
+                vo.setTopic(MqttTopics.Alarm.MESSAGE.getTopic());
+                break;
+        }
+        return vo;
+    }
+
+    private SyncTaskStatisticsVO parseSyncStatisticsVO(JSONObject obj, boolean isEnable) {
+        SyncTaskStatisticsVO vo = new SyncTaskStatisticsVO();
+        vo.setId(obj.getLong("id"));
+        vo.setTopic(obj.getStr("topic"));
+
+        int dataType = obj.getInt("dataType");
+        vo.setDataType(dataType);
+
+        switch (dataType) {
+            case 1:
+                vo.setDataTypeName(MqttTopics.Base.PROTECTIVE_UNIT.getDesc());
+                break;
+            case 2:
+                vo.setDataTypeName(MqttTopics.Base.FLOOR_PLANE.getDesc());
+                break;
+            case 3:
+                vo.setDataTypeName(MqttTopics.Base.SENSOR_INFO.getDesc());
+                break;
+            case 4:
+                vo.setDataTypeName(MqttTopics.IotInfo.MONITORING_DATA.getDesc());
+                break;
+            case 5:
+                vo.setDataTypeName(MqttTopics.Alarm.MESSAGE.getDesc());
+                break;
+        }
+
+        vo.setTotal(obj.getInt("total"));
+        vo.setSuccessNumber(obj.getInt("successNumber"));
+        vo.setFailNumber(obj.getInt("failNumber"));
+        vo.setNotSynced(obj.getInt("notSynced"));
+        vo.setCreateTime(obj.getStr("createTime"));
+        vo.setCostTime(obj.getFloat("costTime"));
+        vo.setState(isEnable ? 1 : obj.getInt("state"));
+
+        return vo;
+    }
+
     @Override
     public CommonPage<CdiDeliveryLog> logList(Long id, Integer pageNum, Integer pageSize, Integer dataType, Integer
             logType, String startTime, String endTime) {
@@ -432,6 +447,13 @@ public class CdiDeliveryLogServiceImpl extends AbstractCrudService<CdiDeliveryLo
         Integer tenantId = one.getTenantId();
         LocalDateTime now = LocalDateTime.now();
 
+        String userName = "自动同步";
+        try {
+            userName = SecurityUtils.getUsername();
+        } catch (Exception e) {
+            log.error("无法获取用户名或姓名,使用默认‘自动同步’", e);
+        }
+
         switch (vo.getDataType()) {
             // 单元信息
             case 1:
@@ -460,7 +482,7 @@ public class CdiDeliveryLogServiceImpl extends AbstractCrudService<CdiDeliveryLo
                 endTime = System.currentTimeMillis();
 
                 notSynced = total - success - failure;
-                saveLog(topic, desc, 1, tenantId, engineeringId, now, startTime, endTime, total, success, failure, notSynced, failure > 0 ? 0 : 1);
+                saveLog(topic, desc, 1, tenantId, engineeringId, now, startTime, endTime, total, success, failure, notSynced, failure > 0 ? 0 : 1, userName);
 
                 break;
             // 平面图信息
@@ -472,10 +494,16 @@ public class CdiDeliveryLogServiceImpl extends AbstractCrudService<CdiDeliveryLo
                 int total2 = 0, success2 = 0, failure2 = 0, notSynced2 = 0;
 
                 List<FloorPlaneVO> floorPlaneVOS = buildPlanes(tenantId, engineeringId);
+                if (CollUtil.isEmpty(floorPlaneVOS)) {
+                    log.error("未找到楼层平面图信息!");
+                    break;
+                }
                 total2 = floorPlaneVOS.size();
                 iotDataTransferService.createMqttConnection(username, password);
                 String topic1 = MqttTopics.Base.FLOOR_PLANE.getTopic();
                 String desc1 = MqttTopics.Base.FLOOR_PLANE.getDesc();
+
+
                 for (FloorPlaneVO floorPlaneVO : floorPlaneVOS) {
                     try {
                         iotDataTransferService.sendMqttMessage(topic1, floorPlaneVO, desc1, username);
@@ -488,7 +516,7 @@ public class CdiDeliveryLogServiceImpl extends AbstractCrudService<CdiDeliveryLo
                 endTime2 = System.currentTimeMillis();
                 notSynced2 = total2 - success2 - failure2;
                 saveLog(topic1, desc1, 2, tenantId, engineeringId, now, startTime2, endTime2, total2, success2,
-                        failure2, notSynced2, failure2 > 0 ? 0 : 1);
+                        failure2, notSynced2, failure2 > 0 ? 0 : 1, userName);
 
                 break;
             // 推送设施信息
@@ -498,11 +526,11 @@ public class CdiDeliveryLogServiceImpl extends AbstractCrudService<CdiDeliveryLo
                 long startTime3, endTime3;
                 startTime3 = System.currentTimeMillis();
 
-                Map<String, Integer> map = baseDataTransferService.batchSendSensorInfos(tenantId);
+                Map<String, Integer> map = baseDataTransferService.batchSendSensorInfos(tenantId, engineeringId, username, password);
 
                 endTime3 = System.currentTimeMillis();
                 saveLog(MqttTopics.Base.SENSOR_INFO.getTopic(), MqttTopics.Base.SENSOR_INFO.getDesc(), 3, tenantId, engineeringId,
-                        now, startTime3, endTime3, map.get("total"), map.get("success"), map.get("failure"), map.get("notSynced"), map.get("failure") > 0 ? 0 : 1);
+                        now, startTime3, endTime3, map.get("total"), map.get("success"), map.get("failure"), map.get("notSynced"), map.get("failure") > 0 ? 0 : 1, userName);
 
                 break;
             // 推送监测数据
@@ -518,7 +546,7 @@ public class CdiDeliveryLogServiceImpl extends AbstractCrudService<CdiDeliveryLo
     @Override
     @Async("asyncServiceExecutor")
     public void saveLog(String topic, String dataTypeName, Integer dataType, Integer tenantId, Long engineeringId, LocalDateTime now, long startTime, long endTime,
-                        int total, int success, int failure, int notSynced, int pushFlag) {
+                        int total, int success, int failure, int notSynced, int pushFlag, String userName) {
 
         SyncTaskStatisticsVO vo = new SyncTaskStatisticsVO();
         vo.setDataType(dataType);
@@ -537,8 +565,7 @@ public class CdiDeliveryLogServiceImpl extends AbstractCrudService<CdiDeliveryLo
         log.setDataType(dataType);
         log.setTopic(topic);
         log.setDataTypeName(vo.getDataTypeName());
-        log.setUserName(SecurityUtils.getUsername() == null ? "自动同步" : SecurityUtils.getUsername());
-        log.setNickName(SecurityUtils.getUsername() == null ? "自动同步" : SecurityUtils.getLoginUser().getSysUser().getNickName());
+        log.setUserName(userName);
         log.setCreateTime(now);
         log.setTenantId(tenantId);
         log.setPushFlag(pushFlag);
@@ -559,6 +586,7 @@ public class CdiDeliveryLogServiceImpl extends AbstractCrudService<CdiDeliveryLo
         List<ProtectiveUnitVO> result = new ArrayList<>(buildUnitList.size());
         for (BaseBuildUnit buildUnit : buildUnitList) {
             ProtectiveUnitVO vo = new ProtectiveUnitVO();
+            vo.setDataPacketID(generateDataPacketID());
             vo.setEngineeringID(engineeringId);
             vo.setUnitName(buildUnit.getUnitName());
             vo.setFloor(buildUnit.getFloor());
@@ -586,21 +614,93 @@ public class CdiDeliveryLogServiceImpl extends AbstractCrudService<CdiDeliveryLo
         List<Integer> buildIds = buildList.stream().map(BaseBuild::getId).collect(Collectors.toList());
         List<BaseBuildPlane> buildPlaneList = getBuildPlaneList(buildIds);
 
+        if (CollUtil.isEmpty(buildPlaneList)) {
+            return Collections.emptyList();
+        }
+
+        Map<String, BaseBuildPlane> latestPlaneByFloor = buildPlaneList.stream()
+                .collect(Collectors.groupingBy(
+                        BaseBuildPlane::getFloor,
+                        Collectors.collectingAndThen(
+                                Collectors.maxBy(Comparator.comparingInt(BaseBuildPlane::getId)),
+                                opt -> opt.orElse(null)
+                        )
+                ));
+
+        List<BaseBuildPlane> filteredPlaneList = latestPlaneByFloor.values().stream()
+                .filter(Objects::nonNull)
+                .sorted(Comparator.comparing(BaseBuildPlane::getFloor))
+                .collect(Collectors.toList());
+
+        log.info("楼层平面图数据过滤:原始{}条,按楼层去重后{}条", buildPlaneList.size(), filteredPlaneList.size());
+
         String time = DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss.SSS");
 
-        List<FloorPlaneVO> result = new ArrayList<>(buildPlaneList.size());
-        for (BaseBuildPlane buildPlane : buildPlaneList) {
+        List<FloorPlaneVO> result = new ArrayList<>(filteredPlaneList.size());
+        int skipCount = 0;
+
+        for (BaseBuildPlane buildPlane : filteredPlaneList) {
+            try {
+                String planeViewUrl = buildPlane.getPlaneViewUrl();
 
-            String planeViewUrl = buildPlane.getPlaneViewUrl();
+                // ✅ 修复1: 增加详细的处理日志
+                log.info("正在处理楼层平面图:ID={}, Floor={}, URL={}",
+                        buildPlane.getId(), buildPlane.getFloor(), planeViewUrl);
 
+                FloorPlaneVO vo = new FloorPlaneVO();
 
-            FloorPlaneVO vo = new FloorPlaneVO();
-            checkFileSize(vo, planeViewUrl);
-            vo.setEngineeringID(engineeringId);
-            vo.setFloor(buildPlane.getFloor());
-            vo.setFloorFileID(Long.valueOf(buildPlane.getId()));
-            fillImageInfo(vo, planeViewUrl);
-            vo.setPublishTime(time);
+                // ✅ 修复2: 单条记录异常不中断整个批次
+                if (StrUtil.isBlank(planeViewUrl)) {
+                    log.warn("楼层平面图URL为空,跳过该记录:ID={}, Floor={}", buildPlane.getId(), buildPlane.getFloor());
+                    skipCount++;
+                    continue;
+                }
+
+                // 读取文件并校验大小
+                checkFileSize(vo, planeViewUrl);
+
+                // ✅ 修复3: 复用已读取的文件数据来获取图片尺寸(避免重复I/O)
+                fillImageInfoWithBytes(vo, planeViewUrl, vo.getFloorFile());
+
+                // 设置基础字段
+                vo.setDataPacketID(generateDataPacketID());
+                vo.setEngineeringID(engineeringId);
+                vo.setFloor(convertFloor(buildPlane.getFloor()));
+                vo.setFloorFileID(Long.valueOf(buildPlane.getId()));
+                vo.setPublishTime(time);
+
+                // ✅ 修复4: 校验关键字段完整性
+                if (vo.getFloorFile() == null || vo.getFloorFile().length == 0) {
+                    log.warn("楼层平面图文件为空,跳过该记录:ID={}, Floor={}", buildPlane.getId(), buildPlane.getFloor());
+                    skipCount++;
+                    continue;
+                }
+
+                // ✅ 修复5: 记录成功处理的详细信息
+                log.info("楼层平面图处理成功:FileID={}, Floor={}, FileName={}, FileSize={}bytes, Pix={}x{}",
+                        vo.getFloorFileID(),
+                        vo.getFloor(),
+                        vo.getFloorFileName(),
+                        vo.getFloorFile().length,
+                        vo.getFilePixWidth(),
+                        vo.getFilePixHeight());
+
+                result.add(vo);
+
+            } catch (BusinessException e) {
+                log.error("楼层平面图业务校验失败,跳过该记录:ID={}, Floor={}, 错误={}",
+                        buildPlane.getId(), buildPlane.getFloor(), e.getMessage());
+                skipCount++;
+            } catch (Exception e) {
+                log.error("楼层平面图处理异常,跳过该记录:ID={}, Floor={}",
+                        buildPlane.getId(), buildPlane.getFloor(), e);
+                skipCount++;
+            }
+        }
+
+        if (skipCount > 0) {
+            log.warn("楼层平面图处理完成:成功{}条,跳过{}条(文件不存在/超限/读取失败等)",
+                    result.size(), skipCount);
         }
 
         return result;
@@ -613,42 +713,188 @@ public class CdiDeliveryLogServiceImpl extends AbstractCrudService<CdiDeliveryLo
     private void checkFileSize(FloorPlaneVO vo, String filePath) {
         Assert.notBlank(filePath, "文件路径不能为空");
 
-        long size = FileUtil.size(new File(filePath));
-        if (size > MAX_FILE_SIZE_BYTES) {
-            double sizeMB = size / 1024.0 / 1024.0;
-            throw new BusinessException(
-                    StrUtil.format("楼层平面图大小超过{}MB!当前:{:.2f}MB", MAX_FILE_SIZE_MB, sizeMB)
-            );
+        byte[] fileBytes;
+
+        if (filePath.startsWith("http://") || filePath.startsWith("https://")) {
+            fileBytes = downloadFileFromUrl(filePath);
+        } else {
+            File localFile = new File(filePath);
+            if (!localFile.exists()) {
+                log.warn("本地文件不存在: {}", filePath);
+                throw new BusinessException("楼层平面图文件不存在:" + filePath);
+            }
+
+            long size = FileUtil.size(localFile);
+            if (size > MAX_FILE_SIZE_BYTES) {
+                double sizeMB = size / 1024.0 / 1024.0;
+                throw new BusinessException(
+                        StrUtil.format("楼层平面图大小超过{}MB!当前:{:.2f}MB", MAX_FILE_SIZE_MB, sizeMB)
+                );
+            }
+            fileBytes = FileUtil.readBytes(localFile);
         }
-        vo.setFloorFile(FileUtil.readBytes(filePath));
+
+        vo.setFloorFile(fileBytes);
     }
 
-    // 获取图片信息
-    private void fillImageInfo(FloorPlaneVO vo, String imageUrl) {
+    private byte[] downloadFileFromUrl(String fileUrl) {
+        try {
+            String encodedUrl = encodeUrl(fileUrl);
+            URL url = new URL(encodedUrl);
+            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+            conn.setRequestMethod("GET");
+            conn.setConnectTimeout(5000);
+            conn.setReadTimeout(10000);
+            conn.setRequestProperty("User-Agent", "Mozilla/5.0");
+
+            int responseCode = conn.getResponseCode();
+            if (responseCode != HttpURLConnection.HTTP_OK) {
+                log.error("下载文件失败,HTTP状态码: {}, URL: {}", responseCode, fileUrl);
+                throw new BusinessException("下载楼层平面图失败,服务器返回状态码:" + responseCode);
+            }
+
+            int contentLength = conn.getContentLength();
+            if (contentLength > MAX_FILE_SIZE_BYTES) {
+                double sizeMB = contentLength / 1024.0 / 1024.0;
+                throw new BusinessException(
+                        StrUtil.format("楼层平面图大小超过{}MB!当前:{:.2f}MB", MAX_FILE_SIZE_MB, sizeMB)
+                );
+            }
+
+            try (InputStream inputStream = conn.getInputStream()) {
+                return IoUtil.readBytes(inputStream);
+            }
+        } catch (BusinessException e) {
+            throw e;
+        } catch (IOException e) {
+            log.error("从URL下载文件失败: {}", fileUrl, e);
+            throw new BusinessException("下载楼层平面图失败:" + e.getMessage());
+        }
+    }
+
+    private String convertFloor(String floor) {
+        if (floor == null || floor.trim().isEmpty()) {
+            return floor;
+        }
+        try {
+            int floorNum = Integer.parseInt(floor.trim());
+            if (floorNum < 0) {
+                return "B" + Math.abs(floorNum);
+            }
+        } catch (NumberFormatException e) {
+            log.warn("楼层格式转换失败,原始值: {}", floor);
+        }
+        return floor;
+    }
+
+    private String encodeUrl(String url) throws UnsupportedEncodingException {
+        if (StrUtil.isBlank(url)) {
+            return url;
+        }
+
+        if (!(url.startsWith("http://") || url.startsWith("https://"))) {
+            return url;
+        }
+
+        int protocolEnd = url.indexOf("://");
+        String protocol = url.substring(0, protocolEnd + 3);
+        String rest = url.substring(protocolEnd + 3);
+
+        int firstSlash = rest.indexOf("/");
+        if (firstSlash == -1) {
+            return url;
+        }
+
+        String hostAndPort = rest.substring(0, firstSlash);
+        String path = rest.substring(firstSlash);
+
+        String[] pathSegments = path.split("/");
+        StringBuilder encodedPath = new StringBuilder();
+        for (int i = 0; i < pathSegments.length; i++) {
+            if (i > 0) {
+                encodedPath.append("/");
+            }
+            encodedPath.append(URLEncoder.encode(pathSegments[i], "UTF-8"));
+        }
+
+        return protocol + hostAndPort + encodedPath.toString();
+    }
+
+    /**
+     * 填充图片信息(使用已读取的字节数组,避免重复I/O)
+     * @param vo 楼层平面图VO
+     * @param imageUrl 图片URL或本地路径(用于提取文件名和后缀)
+     * @param imageBytes 已读取的图片字节数组
+     */
+    private void fillImageInfoWithBytes(FloorPlaneVO vo, String imageUrl, byte[] imageBytes) {
         if (StrUtil.isBlank(imageUrl)) {
             return;
         }
 
-        // 提取文件名信息
+        // 从URL/路径中提取文件名和后缀
         String fileName = FileUtil.getName(imageUrl);
         vo.setFloorFileName(FileUtil.mainName(fileName));
         vo.setFloorFileSuffix(FileUtil.extName(fileName));
 
-        // 读取像素尺寸(带超时控制)
+        // ✅ 优化:直接从已读取的byte[]获取尺寸,避免重复读取文件
         try {
-            URLConnection conn = new URL(imageUrl).openConnection();
-            conn.setConnectTimeout(3000);
-            conn.setReadTimeout(5000);
+            if (imageBytes != null && imageBytes.length > 0) {
+                ByteArrayInputStream bais = new ByteArrayInputStream(imageBytes);
+                BufferedImage image = ImageIO.read(bais);
 
-            try (InputStream in = conn.getInputStream()) {
-                BufferedImage image = ImageIO.read(in);
                 if (image != null) {
                     vo.setFilePixWidth(image.getWidth());
                     vo.setFilePixHeight(image.getHeight());
                 }
             }
         } catch (IOException e) {
-            log.error("获取图片尺寸失败: {}", imageUrl);
+            log.error("从字节数组获取图片尺寸失败: {}", imageUrl, e);
+        }
+    }
+
+    private void fillImageInfo(FloorPlaneVO vo, String imageUrl) {
+        if (StrUtil.isBlank(imageUrl)) {
+            return;
+        }
+
+        String fileName = FileUtil.getName(imageUrl);
+        vo.setFloorFileName(FileUtil.mainName(fileName));
+        vo.setFloorFileSuffix(FileUtil.extName(fileName));
+
+        try {
+            BufferedImage image;
+            if (imageUrl.startsWith("http://") || imageUrl.startsWith("https://")) {
+                String encodedUrl = encodeUrl(imageUrl);
+                URL url = new URL(encodedUrl);
+                URLConnection conn = url.openConnection();
+                conn.setConnectTimeout(3000);
+                conn.setReadTimeout(5000);
+
+                try (InputStream in = conn.getInputStream()) {
+                    image = ImageIO.read(in);
+                }
+            } else {
+                File imageFile = new File(imageUrl);
+                if (imageFile.exists()) {
+                    image = ImageIO.read(imageFile);
+                } else {
+                    log.warn("图片文件不存在: {}", imageUrl);
+                    vo.setFilePixWidth(7016);
+                    vo.setFilePixHeight(9933);
+                    return;
+                }
+            }
+
+            if (image != null) {
+                vo.setFilePixWidth(image.getWidth());
+                vo.setFilePixHeight(image.getHeight());
+            } else {
+                log.warn("无法读取图片尺寸: {}", imageUrl);
+                vo.setFilePixWidth(7016);
+                vo.setFilePixHeight(9933);
+            }
+        } catch (IOException e) {
+            log.error("获取图片尺寸失败: {}", imageUrl, e);
             vo.setFilePixWidth(7016);
             vo.setFilePixHeight(9933);
         }

+ 436 - 29
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/IotDataTransferService.java

@@ -2,20 +2,14 @@ package com.usky.cdi.service.impl;
 
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
+import com.alibaba.nacos.shaded.com.google.gson.Gson;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.toolkit.StringUtils;
-import com.usky.cdi.domain.CdiDefenseProject;
-import com.usky.cdi.domain.CdiDeliveryLog;
-import com.usky.cdi.domain.DmpDevice;
-import com.usky.cdi.domain.DmpProduct;
-import com.usky.cdi.mapper.CdiDefenseProjectMapper;
-import com.usky.cdi.mapper.CdiDeliveryLogMapper;
-import com.usky.cdi.mapper.DmpDeviceMapper;
-import com.usky.cdi.mapper.DmpProductMapper;
-import com.usky.cdi.service.CdiDeliveryLogService;
-import com.usky.cdi.service.config.mqtt.MqttOutConfig;
+import com.usky.cdi.domain.*;
+import com.usky.cdi.mapper.*;
 import com.usky.cdi.service.enums.MqttTopics;
 import com.usky.cdi.service.mqtt.MqttConnectionTool;
+import com.usky.cdi.service.util.AirDefenseSimulator;
 import com.usky.cdi.service.util.DeviceDataQuery;
 import com.usky.cdi.service.util.SnowflakeIdGenerator;
 import com.usky.cdi.service.vo.IotDataTransferVO;
@@ -39,6 +33,7 @@ import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
 import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ThreadLocalRandom;
 import java.util.stream.Collectors;
 
 /**
@@ -50,8 +45,6 @@ import java.util.stream.Collectors;
 @Service
 public class IotDataTransferService {
 
-    private MqttOutConfig.MqttGateway mqttGateway;
-
     @Autowired
     private MqttConnectionTool mqttConnectionTool;
 
@@ -96,6 +89,9 @@ public class IotDataTransferService {
     @Autowired
     private CdiDeliveryLogMapper cdiDeliveryLogMapper;
 
+    @Autowired
+    private BaseBuildUnitMapper baseBuildUnitMapper;
+
     @PostConstruct
     public void init() {
         this.idGenerator = new SnowflakeIdGenerator(workerId, dataCenterId);
@@ -164,6 +160,9 @@ public class IotDataTransferService {
                     continue;
                 }
 
+                // 为每条数据的监测时间添加毫秒级微差
+                dataEndTime = addTimeOffset(dataEndTime);
+
                 Integer deviceId = deviceDataItem.getIntValue("device_id");
                 Integer value = deviceDataItem.getInteger("leach_status");
                 if (value == null) {
@@ -181,6 +180,7 @@ public class IotDataTransferService {
                 vo.setDataEndTime(dataEndTime);
 
                 try {
+                    log.info("【水浸数据】开始推送,设备:{},数据:{}", deviceId, JSON.toJSONString(vo));
                     sendMqttMessage(MqttTopics.IotInfo.WATER_LEAK.getTopic(), vo, MqttTopics.IotInfo.WATER_LEAK.getDesc(), transferVO.getUsername());
                     result.put("successCount", result.get("successCount") + 1);
                 } catch (Exception e) {
@@ -242,6 +242,7 @@ public class IotDataTransferService {
 
             Long engineeringId = transferVO.getEngineeringId();
 
+            int skippedNullField = 0;
             for (JSONObject deviceDataItem : deviceData) {
                 Integer deviceId = deviceDataItem.getIntValue("device_id");
                 LocalDateTime dataEndTime = parseDataTime(deviceDataItem);
@@ -251,6 +252,35 @@ public class IotDataTransferService {
                     continue;
                 }
 
+                // 为每条数据的监测时间添加毫秒级微差
+                dataEndTime = addTimeOffset(dataEndTime);
+
+                // 检查业务字段是否为空,为空则计入失败(修复:原逻辑静默跳过并误计为成功)
+                boolean fieldMissing = false;
+                switch (deviceType) {
+                    case 707:
+                        if (deviceDataItem.getFloat("wd") == null) fieldMissing = true;
+                        break;
+                    case 708:
+                        if (deviceDataItem.getFloat("sd") == null) fieldMissing = true;
+                        break;
+                    case 709:
+                        if (deviceDataItem.getFloat("o2") == null) fieldMissing = true;
+                        break;
+                    case 710:
+                        if (deviceDataItem.getFloat("co2") == null) fieldMissing = true;
+                        break;
+                    case 711:
+                        if (deviceDataItem.getFloat("co") == null) fieldMissing = true;
+                        break;
+                }
+                if (fieldMissing) {
+                    log.warn("设备{}[类型{}]的业务字段数据为空,跳过推送,计入失败", deviceId, deviceType);
+                    result.put("failureCount", result.get("failureCount") + 1);
+                    skippedNullField++;
+                    continue;
+                }
+
                 boolean deviceSuccess = true;
                 try {
                     switch (deviceType) {
@@ -271,7 +301,7 @@ public class IotDataTransferService {
                             break;
                     }
                 } catch (Exception e) {
-                    log.warn("设备{}推送失败:{}", deviceId, e.getMessage());
+                    log.warn("设备{}[类型{}]推送异常:{}", deviceId, deviceType, e.getMessage(), e);
                     deviceSuccess = false;
                 }
 
@@ -282,6 +312,10 @@ public class IotDataTransferService {
                 }
             }
 
+            if (skippedNullField > 0) {
+                log.warn("[类型{}] 因业务字段为空而跳过的设备数量:{}", deviceType, skippedNullField);
+            }
+
             int success = result.get("successCount");
             int failure = result.get("failureCount");
             int notSynced = totalDevices - success - failure;
@@ -360,6 +394,88 @@ public class IotDataTransferService {
         return cdiDefenseProject.getTenantId();
     }
 
+    /**
+     * 发送人员统计情况(703)-掩蔽人数
+     *
+     * @return 推送结果,包含成功数和失败数
+     **/
+    public Map<String, Integer> sendPersonCount(IotDataTransferVO transferVO) {
+        Map<String, Integer> result = new HashMap<>();
+        result.put("successCount", 0);
+        result.put("failureCount", 0);
+
+        if (!validateMqttGateway(transferVO.getUsername())) {
+            return result;
+        }
+
+        LocalDateTime now = LocalDateTime.now();
+        long startTime = System.currentTimeMillis();
+
+        // 模拟人员统计
+        log.info("开始执行【人员统计】数据推送,工程ID:{}", transferVO.getEngineeringId());
+        try {
+            LambdaQueryWrapper<BaseBuildUnit> buildUnitQuery = new LambdaQueryWrapper<>();
+            buildUnitQuery.eq(BaseBuildUnit::getTenantId, transferVO.getTenantId());
+            List<BaseBuildUnit> buildUnitList = baseBuildUnitMapper.selectList(buildUnitQuery);
+
+            // 修复3:单元为空,打印日志+标记失败,不静默返回
+            if (buildUnitList.isEmpty()) {
+                log.warn("【人员统计】未查询到建筑单元,租户ID:{},推送终止", transferVO.getTenantId());
+                result.put("failureCount", 1);
+                return result;
+            }
+
+            log.info("【人员统计】获取到建筑单元数量:{}", buildUnitList.size());
+            for (BaseBuildUnit buildUnit : buildUnitList) {
+                int peopleCount = AirDefenseSimulator.calculatePeopleCount(now);
+                HeadcountVO headcountVO = new HeadcountVO();
+                headcountVO.setDataPacketID(generateDataPacketID());
+                headcountVO.setEngineeringID(transferVO.getEngineeringId());
+                headcountVO.setSensorValue(peopleCount);
+                // headcountVO.setSensorID(buildUnit.getId());
+
+                // 为每条数据的监测时间添加毫秒级微差
+                LocalDateTime dataEndTime = addTimeOffset(now);
+
+                headcountVO.setDataEndTime(dataEndTime);
+                headcountVO.setPublishTime(getCurrentTime());
+                headcountVO.setUnitName(buildUnit.getUnitName());
+                headcountVO.setFloor(buildUnit.getFloor());
+
+                try {
+                    log.info("【人员统计】开始推送,单元:{},数据:{}", buildUnit.getUnitName(), JSON.toJSONString(headcountVO));
+                    sendMqttMessage(
+                            MqttTopics.IotInfo.PERSON.getTopic(),
+                            headcountVO,
+                            MqttTopics.IotInfo.PERSON.getDesc(),
+                            transferVO.getUsername()
+                    );
+                    // 添加2S延时
+                    Thread.sleep(2000);
+                    result.put("successCount", result.get("successCount") + 1);
+                    log.info("【人员统计】推送成功,单元:{},人数:{}", buildUnit.getUnitName(), peopleCount);
+                } catch (Exception e) {
+                    log.warn("【人员统计】数据推送失败,单元:{},异常:{}", buildUnit.getUnitName(), e.getMessage());
+                    result.put("failureCount", result.get("failureCount") + 1);
+                }
+            }
+
+            // 修复4:统计推送完成,打印汇总日志
+            log.info("【人员统计】推送完成,成功:{},失败:{}", result.get("successCount"), result.get("failureCount"));
+            // 可选:和闯入一样保存汇总日志(推荐加上)
+            long endTime = System.currentTimeMillis();
+            saveLog(transferVO, now, startTime, endTime, buildUnitList.size(),
+                    result.get("successCount"), result.get("failureCount"), 0,
+                    result.get("successCount") > 0 ? 1 : 0, SecurityUtils.getUsername());
+
+        } catch (Exception e) {
+            // 修复5:全局异常兜底,绝不丢失
+            log.error("【人员统计】推送发生全局异常", e);
+            result.put("failureCount", result.getOrDefault("failureCount", 0) + 1);
+        }
+        return result;
+    }
+
     /**
      * 发送人员闯入情况(703)
      *
@@ -376,7 +492,6 @@ public class IotDataTransferService {
 
         LocalDateTime now = LocalDateTime.now();
         long startTime = System.currentTimeMillis();
-
         try {
             List<JSONObject> deviceData = deviceDataQuery.getDeviceData(transferVO);
             Integer deviceType = transferVO.getDeviceType();
@@ -407,6 +522,9 @@ public class IotDataTransferService {
                     continue;
                 }
 
+                // 处理数据时间
+                dataEndTime = addTimeOffset(dataEndTime);
+
                 Integer deviceId = deviceDataItem.getIntValue("device_id");
 
                 PersonPresenceVO vo = new PersonPresenceVO();
@@ -418,6 +536,7 @@ public class IotDataTransferService {
                 vo.setSensorValue(0); // 固定值(根据业务需求)
 
                 try {
+                    log.info("【人员闯入】开始推送,设备ID:{},数据:{}", deviceId, JSON.toJSONString(vo));
                     sendMqttMessage(
                             MqttTopics.IotInfo.PERSON_PRESENCE.getTopic(),
                             vo,
@@ -506,6 +625,7 @@ public class IotDataTransferService {
                     result.put("failureCount", result.get("failureCount") + 1);
                     continue;
                 }
+                dataEndTime = addTimeOffset(dataEndTime);
 
                 Integer deviceId = deviceDataItem.getIntValue("device_id");
 
@@ -533,6 +653,7 @@ public class IotDataTransferService {
                 vo.setTotalPower(totalPower);
 
                 try {
+                    log.info("【人防用电负荷情况】开始推送,设备ID:{},数据:{}", deviceId, JSON.toJSONString(vo));
                     sendMqttMessage(
                             MqttTopics.IotInfo.ELECTRICITY_LOAD.getTopic(),
                             vo,
@@ -576,7 +697,7 @@ public class IotDataTransferService {
     }
 
     /**
-     * 推送温度信息(701
+     * 推送温度信息(707
      *
      * @param deviceDataItem 设备数据
      * @param deviceId 设备ID
@@ -597,11 +718,14 @@ public class IotDataTransferService {
         tempVO.setPublishTime(getCurrentTime());
         tempVO.setSensorValue(value);
         tempVO.setDataEndTime(dataEndTime);
+        System.out.println("监测时间:" + dataEndTime);
+
+        log.info("【温度】开始推送,设备ID:{},数据:{}", deviceId, JSON.toJSONString(tempVO));
         sendMqttMessage(MqttTopics.IotInfo.TEMP.getTopic(), tempVO, MqttTopics.IotInfo.TEMP.getDesc(), username);
     }
 
     /**
-     * 推送湿度信息(702
+     * 推送湿度信息(708
      *
      * @param deviceDataItem 设备数据
      * @param deviceId 设备ID
@@ -622,6 +746,8 @@ public class IotDataTransferService {
         humidityVO.setPublishTime(getCurrentTime());
         humidityVO.setSensorValue(value);
         humidityVO.setDataEndTime(dataEndTime);
+
+        log.info("【湿度】开始推送,设备ID:{},数据:{}", deviceId, JSON.toJSONString(humidityVO));
         sendMqttMessage(MqttTopics.IotInfo.HUMIDITY.getTopic(), humidityVO, MqttTopics.IotInfo.HUMIDITY.getDesc(), username);
     }
 
@@ -647,11 +773,13 @@ public class IotDataTransferService {
         oxygenVO.setPublishTime(getCurrentTime());
         oxygenVO.setSensorValue(value);
         oxygenVO.setDataEndTime(dataEndTime);
+
+        log.info("【氧气浓度】开始推送,设备ID:{},数据:{}", deviceId, JSON.toJSONString(oxygenVO));
         sendMqttMessage(MqttTopics.IotInfo.OXYGEN.getTopic(), oxygenVO, MqttTopics.IotInfo.OXYGEN.getDesc(), username);
     }
 
     /**
-     * 推送一氧化碳浓度信息(706
+     * 推送一氧化碳浓度信息(711
      *
      * @param deviceDataItem 设备数据
      * @param deviceId 设备ID
@@ -672,11 +800,13 @@ public class IotDataTransferService {
         coVO.setPublishTime(getCurrentTime());
         coVO.setSensorValue(value);
         coVO.setDataEndTime(dataEndTime);
+
+        log.info("【一氧化碳浓度】开始推送,设备ID:{},数据:{}", deviceId, JSON.toJSONString(coVO));
         sendMqttMessage(MqttTopics.IotInfo.CO.getTopic(), coVO, MqttTopics.IotInfo.CO.getDesc(), username);
     }
 
     /**
-     * 推送二氧化碳浓度信息(707
+     * 推送二氧化碳浓度信息(710)
      *
      * @param deviceDataItem 设备数据
      * @param deviceId 设备ID
@@ -695,8 +825,14 @@ public class IotDataTransferService {
         co2VO.setSensorID(deviceId);
         co2VO.setEngineeringID(engineeringID);
         co2VO.setPublishTime(getCurrentTime());
-        co2VO.setSensorValue(value);
+        // 将value除以10000并保留三位小数
+        Float processedValue = new java.math.BigDecimal(value / 10000f)
+                .setScale(3, java.math.RoundingMode.HALF_UP)
+                .floatValue();
+        co2VO.setSensorValue(processedValue);
         co2VO.setDataEndTime(dataEndTime);
+
+        log.info("【二氧化碳浓度】开始推送,设备ID:{},数据:{}", deviceId, JSON.toJSONString(co2VO));
         sendMqttMessage(MqttTopics.IotInfo.CO2.getTopic(), co2VO, MqttTopics.IotInfo.CO2.getDesc(), username);
     }
 
@@ -884,6 +1020,7 @@ public class IotDataTransferService {
                     result.put("failureCount", result.get("failureCount") + 1);
                     continue;
                 }
+                dataEndTime = addTimeOffset(dataEndTime);
 
                 Integer deviceId = deviceDataItem.getIntValue("device_id");
                 Integer value = deviceDataItem.getInteger("wy");
@@ -902,6 +1039,7 @@ public class IotDataTransferService {
                 vo.setSensorValue(value == 0 ? 0 : 1);
 
                 try {
+                    log.info("【位移数据】开始推送,设备ID:{},数据:{}", deviceId, JSON.toJSONString(vo));
                     sendMqttMessage(MqttTopics.IotInfo.DEVIATION.getTopic(), vo, MqttTopics.IotInfo.DEVIATION.getDesc(), transferVO.getUsername());
                     result.put("successCount", result.get("successCount") + 1);
                 } catch (Exception e) {
@@ -929,6 +1067,179 @@ public class IotDataTransferService {
         }
     }
 
+    /**
+     * 发送液位数据(716)
+     *
+     * @return 推送结果,包含成功数和失败数
+     **/
+    public Map<String, Integer> sendWaterLevel(IotDataTransferVO transferVO) {
+        Map<String, Integer> result = new HashMap<>();
+        result.put("successCount", 0);
+        result.put("failureCount", 0);
+
+        if (!validateMqttGateway(transferVO.getUsername())) {
+            return result;
+        }
+
+        LocalDateTime now = LocalDateTime.now();
+        long startTime = System.currentTimeMillis();
+        long endTime;
+
+        List<JSONObject> deviceData = deviceDataQuery.getDeviceData(transferVO);
+        Integer deviceType = transferVO.getDeviceType();
+        Integer totalDevices = transferVO.getDevices().size();
+
+        try {
+
+            log.info("开始推送【集水井水位】数据,设备类型:{},设备数量:{},获取到的数据条数:{}",
+                    deviceType, totalDevices, deviceData.size());
+
+            if (deviceData.isEmpty()) {
+                log.warn("没有获取到位移数据!设备类型:{}", deviceType);
+                result.put("failureCount", totalDevices);
+                return result;
+            }
+
+            Long engineeringId = transferVO.getEngineeringId();
+            for (JSONObject deviceDataItem : deviceData) {
+                LocalDateTime dataEndTime = parseDataTime(deviceDataItem);
+                if (dataEndTime == null) {
+                    result.put("failureCount", result.get("failureCount") + 1);
+                    continue;
+                }
+                dataEndTime = addTimeOffset(dataEndTime);
+
+                Integer deviceId = deviceDataItem.getIntValue("device_id");
+                Double value = deviceDataItem.getDouble("sensorValue");
+                if (value == null) {
+                    log.warn("设备{}的水位数据为空", deviceId);
+                    result.put("failureCount", result.get("failureCount") + 1);
+                    continue;
+                }
+
+                WaterLevelVO vo = new WaterLevelVO();
+                vo.setDataPacketID(generateDataPacketID());
+                vo.setSensorID(deviceId);
+                vo.setEngineeringID(engineeringId);
+                vo.setPublishTime(getCurrentTime());
+                vo.setDataEndTime(dataEndTime);
+                vo.setSensorValue(value);
+
+                try {
+                    log.info("【水位数据】开始推送,设备ID:{},数据:{}", deviceId, JSON.toJSONString(vo));
+                    sendMqttMessage(MqttTopics.IotInfo.SEWAGE_LEVEL.getTopic(), vo, MqttTopics.IotInfo.SEWAGE_LEVEL.getDesc(), transferVO.getUsername());
+                    result.put("successCount", result.get("successCount") + 1);
+                } catch (Exception e) {
+                    log.warn("设备{}的水位数据推送失败:{}", deviceId, e.getMessage());
+                    result.put("failureCount", result.get("failureCount") + 1);
+                }
+            }
+
+            log.info("水位数据推送完成,设备类型:{},成功:{},失败:{}",
+                    deviceType, result.get("successCount"), result.get("failureCount"));
+
+            endTime = System.currentTimeMillis();
+            saveLog(transferVO, now, startTime, endTime, totalDevices,
+                    result.get("successCount"), result.get("failureCount"),
+                    totalDevices - result.get("successCount") - result.get("failureCount"), 1, SecurityUtils.getUsername());
+            return result;
+        } catch (Exception e) {
+            log.error("水位数据推送发生异常", e);
+            result.put("failureCount", transferVO.getDevices().size());
+            endTime = System.currentTimeMillis();
+            saveLog(transferVO, now, startTime, endTime, totalDevices,
+                    result.get("successCount"), result.get("failureCount"),
+                    totalDevices - result.get("successCount") - result.get("failureCount"), 0, SecurityUtils.getUsername());
+            return result;
+        }
+    }
+
+    /**
+     * 发送发电机蓄电池监测数据(720)
+     *
+     * @return 推送结果,包含成功数和失败数
+     **/
+    public Map<String, Integer> sendGeneratorMonitoring(IotDataTransferVO transferVO) {
+        Map<String, Integer> result = new HashMap<>();
+        result.put("successCount", 0);
+        result.put("failureCount", 0);
+
+        if (!validateMqttGateway(transferVO.getUsername())) {
+            return result;
+        }
+
+        LocalDateTime now = LocalDateTime.now();
+        long startTime = System.currentTimeMillis();
+        long endTime;
+
+        List<JSONObject> deviceData = deviceDataQuery.getDeviceData(transferVO);
+        Integer deviceType = transferVO.getDeviceType();
+        Integer totalDevices = transferVO.getDevices().size();
+
+        try {
+
+            log.info("开始推送【战时柴油发电机每日启动时长】数据,设备类型:{},设备数量:{},获取到的数据条数:{}",
+                    deviceType, totalDevices, deviceData.size());
+
+            if (deviceData.isEmpty()) {
+                log.warn("没有获取到战时柴油发电机每日启动时长数据!设备类型:{}", deviceType);
+                result.put("failureCount", totalDevices);
+                return result;
+            }
+
+            Long engineeringId = transferVO.getEngineeringId();
+            for (JSONObject deviceDataItem : deviceData) {
+                LocalDateTime dataEndTime = parseDataTime(deviceDataItem);
+                if (dataEndTime == null) {
+                    result.put("failureCount", result.get("failureCount") + 1);
+                    continue;
+                }
+                dataEndTime = addTimeOffset(dataEndTime);
+
+                Integer deviceId = deviceDataItem.getIntValue("device_id");
+                Integer startDuration = deviceDataItem.get("startDuration") == null ? 0 : deviceDataItem.getIntValue("startDuration");
+
+                GeneratorMonitoringVO vo = new GeneratorMonitoringVO();
+                vo.setDataPacketID(generateDataPacketID());
+                vo.setSensorID(deviceId);
+                vo.setEngineeringID(engineeringId);
+                vo.setPublishTime(getCurrentTime());
+                vo.setDataEndTime(dataEndTime);
+                vo.setSensorDate(dataEndTime.toLocalDate());
+                vo.setStartDuration(startDuration);
+                if (deviceDataItem.containsKey("batteryVoltage")) {
+                    vo.setBatteryVoltage(deviceDataItem.getDouble("batteryVoltage"));
+                }
+
+                try {
+                    log.info("【战时柴油发电机每日启动时长】开始推送,设备ID:{},数据:{}", deviceId, JSON.toJSONString(vo));
+                    sendMqttMessage(MqttTopics.IotInfo.ALTERNATOR_STARTUP_TIME.getTopic(), vo, MqttTopics.IotInfo.ALTERNATOR_STARTUP_TIME.getDesc(), transferVO.getUsername());
+                    result.put("successCount", result.get("successCount") + 1);
+                } catch (Exception e) {
+                    log.warn("设备{}的战时柴油发电机每日启动时长数据推送失败:{}", deviceId, e.getMessage());
+                    result.put("failureCount", result.get("failureCount") + 1);
+                }
+            }
+
+            log.info("战时柴油发电机每日启动时长数据推送完成,设备类型:{},成功:{},失败:{}",
+                    deviceType, result.get("successCount"), result.get("failureCount"));
+
+            endTime = System.currentTimeMillis();
+            saveLog(transferVO, now, startTime, endTime, totalDevices,
+                    result.get("successCount"), result.get("failureCount"),
+                    totalDevices - result.get("successCount") - result.get("failureCount"), 1, SecurityUtils.getUsername());
+            return result;
+        } catch (Exception e) {
+            log.error("战时柴油发电机每日启动时长数据推送发生异常", e);
+            result.put("failureCount", transferVO.getDevices().size());
+            endTime = System.currentTimeMillis();
+            saveLog(transferVO, now, startTime, endTime, totalDevices,
+                    result.get("successCount"), result.get("failureCount"),
+                    totalDevices - result.get("successCount") - result.get("failureCount"), 0, SecurityUtils.getUsername());
+            return result;
+        }
+    }
+
     /**
      * 同步设备数据
      * @param tenantId 租户ID
@@ -970,6 +1281,7 @@ public class IotDataTransferService {
             transferVO.setDevices(devices);
             transferVO.setEngineeringId(engineeringId);
             transferVO.setUsername(username); // 保存当前任务的用户名
+            transferVO.setTenantId(tenantId);
             transferList.add(transferVO);
         });
 
@@ -1015,8 +1327,13 @@ public class IotDataTransferService {
                     result = sendWaterLeak(transferVO);
                     break;
                 case 703:
+                    if (transferVO.getEngineeringId() == 3101100024L) {
+                        result = sendPersonCount(transferVO);
+                        break;
+                    }
                     result = sendPersonPresence(transferVO);
                     break;
+                case 705:
                 case 704:
                     if (tenantId == 1205) {
                         // 设置默认值,避免空指针
@@ -1035,15 +1352,31 @@ public class IotDataTransferService {
                 case 714:
                     result = sendDeviationData(transferVO);
                     break;
-
+                case 716:
+                    result = sendWaterLevel(transferVO);
+                    break;
+                case 719:
+                    result = sendPersonCount(transferVO);
+                    break;
+                case 720:
+                    result = sendGeneratorMonitoring(transferVO);
+                    break;
                 default:
                     log.debug("不支持的设备类型:{}", deviceType);
                     continue;
             }
 
             // 累加成功数和失败数
-            totalSuccessCount += result.getOrDefault("successCount", 0);
-            totalFailureCount += result.getOrDefault("failureCount", 0);
+            int typeSuccess = result.getOrDefault("successCount", 0);
+            int typeFailure = result.getOrDefault("failureCount", 0);
+            totalSuccessCount += typeSuccess;
+            totalFailureCount += typeFailure;
+
+            // 分类型诊断日志:精确定位每种类型的成功/失败/未同步
+            int typeTotal = transferVO.getDevices() != null ? transferVO.getDevices().size() : 0;
+            int typeNotSynced = typeTotal - typeSuccess - typeFailure;
+            log.info("[类型分诊] deviceType={} | 总设备={} | 成功={} | 失败={} | 未同步={}",
+                    deviceType, typeTotal, typeSuccess, typeFailure, typeNotSynced);
         }
 
         // 任务完成总结
@@ -1136,12 +1469,21 @@ public class IotDataTransferService {
      * @return 解析后的时间,如果解析失败返回null
      */
     private LocalDateTime parseDataTime(JSONObject deviceDataItem) {
-        // log.warn("解析的json{}", deviceDataItem.toString());
-        Long dataTime = deviceDataItem.getLong("realtime");
-        if (dataTime == null) {
-            log.warn("设备{}的time为空", deviceDataItem.getString("device_id"));
-            return null;
+        Object raw = deviceDataItem.get("realtime");
+        Long dataTime = null;
+        if (raw instanceof Long) {
+            dataTime = (Long) raw;
+        } else if (raw instanceof Integer) {
+            dataTime = ((Integer) raw).longValue();
+        } else if (raw instanceof Number) {
+            dataTime = ((Number) raw).longValue();
+        } else if (raw instanceof String) {
+            try {
+                dataTime = Long.parseLong((String) raw);
+            } catch (Exception ignored) {
+            }
         }
+        if (dataTime == null) return null;
         return LocalDateTime.ofInstant(Instant.ofEpochMilli(dataTime), ZoneId.systemDefault());
     }
 
@@ -1153,8 +1495,17 @@ public class IotDataTransferService {
      * @param username 用户名
      */
     void sendMqttMessage(String topic, Object vo, String messageType, String username) {
-        String json = JSON.toJSONString(vo);
-        // 不再记录每条数据的详情,只记录发送操作
+        String json;
+
+        // 针对楼层平面图特殊处理:将 byte[] 转为 Base64 字符串
+        if (vo instanceof com.usky.cdi.service.vo.info.FloorPlaneVO) {
+            json = serializeFloorPlaneVO((com.usky.cdi.service.vo.info.FloorPlaneVO) vo);
+        } else {
+            json = JSON.toJSONString(vo);
+        }
+
+        log.info("发送MQTT消息,Topic: {}, 消息类型: {}, JSON长度: {}", topic, messageType, json.length());
+
         MqttConnectionTool.MqttGateway gateway = mqttGatewayMap.get(username);
         if (gateway != null) {
             gateway.sendToMqtt(topic, json);
@@ -1163,8 +1514,64 @@ public class IotDataTransferService {
         }
     }
 
+    /**
+     * 序列化楼层平面图VO(将 floorFile byte[] 转为 Base64 字符串)
+     */
+    private String serializeFloorPlaneVO(com.usky.cdi.service.vo.info.FloorPlaneVO vo) {
+        Map<String, Object> jsonObject = new HashMap<>();
+        jsonObject.put("dataPacketID", vo.getDataPacketID());
+        jsonObject.put("engineeringID", vo.getEngineeringID());
+        jsonObject.put("floor", vo.getFloor());
+        jsonObject.put("floorFileID", vo.getFloorFileID());
+        jsonObject.put("floorFileName", vo.getFloorFileName());
+        jsonObject.put("floorFileSuffix", vo.getFloorFileSuffix());
+        jsonObject.put("filePixWidth", vo.getFilePixWidth());
+        jsonObject.put("filePixHeight", vo.getFilePixHeight());
+        jsonObject.put("publishTime", vo.getPublishTime());
+        jsonObject.put("floorFile", vo.getFloorFile());
+        Gson gson = new Gson();
+        return gson.toJson(jsonObject);
+    }
+
     public void allData(Long engineeringId, String username, String password) {
         Integer tenantId = 0;
         synchronizeDeviceData(tenantId, engineeringId, username, password);
     }
+
+    /**
+     * 为监测时间添加随机偏移(用于模拟真实采集场景)
+     * @param originalTime 原始监测时间
+     * @return 偏移后的时间
+     */
+    private LocalDateTime addTimeOffset(LocalDateTime originalTime) {
+        if (originalTime == null) {
+            return null;
+        }
+
+        // 获取当前时间作为上限
+        LocalDateTime now = LocalDateTime.now();
+
+        // 如果原始时间已经是未来时间,先修正为当前时间
+        if (originalTime.isAfter(now)) {
+            log.warn("检测到未来时间的监测数据,已修正为当前时间:{}", originalTime);
+            originalTime = now;
+        }
+
+        // 随机秒偏移:0、1、2 秒(向前偏移,使用减法)
+        int secondsOffset = ThreadLocalRandom.current().nextInt(0, 3);
+        // 随机毫秒偏移:0 ~ 999 毫秒(向前偏移,使用减法)
+        int millisOffset = ThreadLocalRandom.current().nextInt(0, 1000);
+
+        LocalDateTime offsetTime = originalTime.minusSeconds(secondsOffset)
+                .minusNanos(millisOffset * 1_000_000L);
+
+        // 最终校验:确保不会晚于当前时间
+        if (offsetTime.isAfter(now)) {
+            log.warn("偏移后时间仍然晚于当前时间,已修正:原始={}, 偏移后={}, 当前={}",
+                    originalTime, offsetTime, now);
+            return now;
+        }
+
+        return offsetTime;
+    }
 }

+ 381 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/util/AirDefenseSimulator.java

@@ -0,0 +1,381 @@
+package com.usky.cdi.service.util;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * 人防掩蔽单元人数模拟工具类(动态自适应版)
+ * <p>
+ * 核心特性:
+ * 1. 即时计算:根据当前时间动态返回合理总人数,无预生成/硬编码条数限制
+ * 2. 自适应调用管理器:跟踪调用频率,动态评估并自动扩容阈值
+ * 3. 分时段基准曲线:每时段独立配置基准人数与波动范围,支持平滑插值
+ * 4. 边界安全:防负数、防无限循环、防溢出
+ *
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2026/5/30
+ */
+public class AirDefenseSimulator {
+
+    // ===================== 可配置常量 =====================
+    private static final int MIN_PEOPLE = 0;                    // 最小人数(禁止负数)
+    private static final int DEFAULT_MAX_CALLS = 48;            // 默认最大调用次数(向下兼容)
+    private static final int MAX_EXPANSION_LIMIT = 288;         // 扩容上限(=24h*12次/5min,即5分钟粒度全天)
+    private static final double EXPANSION_FACTOR = 2.0;         // 每次扩容倍数
+    private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm");
+
+    // ===================== 自适应调用管理器(单例状态) =====================
+    /** 已调用次数计数器 */
+    private static final AtomicInteger callCount = new AtomicInteger(0);
+    /** 当前动态阈值(初始为默认值,可自动扩容) */
+    private static final AtomicInteger dynamicThreshold = new AtomicInteger(DEFAULT_MAX_CALLS);
+    /** 上次调用时间戳(用于检测异常高频调用) */
+    private static final AtomicLong lastCallTimestamp = new AtomicLong(0);
+    /** 是否已触发过扩容标志 */
+    private static volatile boolean expanded = false;
+
+    // ===================== 时段规则定义(基准人数 + 波动范围)=====================
+    /**
+     * 时段规则:[起始小时, 结束小时) → {基准人数, 最小波动, 最大波动}
+     * 基准人数代表该时段中心时刻的典型值,波动范围为±随机偏移量
+     */
+    private static final int[][] TIME_SLOT_RULES = {
+        //  起始H   结束H   基准   波动Min  波动Max   规则说明
+            {0,      5,      0,     0,       0},      // 规则1: 00:00-05:00  人数为0
+            {5,      6,      3,     2,       3},      // 规则2: 05:00-06:00  缓慢上升
+            {6,      8,      12,    4,       6},      // 规则3: 06:00-08:00  攀升至第一高点
+            {8,      9,      25,    2,       5},      // 规则3延:08:00-09:00 第一高点后短暂调整
+            {9,      11,     31,    2,       5},      // 规则4: 09:00-11:00  趋于稳定
+            {11,     12,     19,    1,       3},      // 规则5: 11:00-12:00  大量流出(就餐)
+            {12,     13,     12,    1,       3},      // 规则5延:12:00-13:00 午间低谷
+            {13,     14,     21,    3,       6},      // 规则6: 13:00-14:00  开始回流
+            {14,     15,     30,    4,       7},      // 规则7: 14:00-15:00  达到第二高点
+            {15,     19,     31,    2,       5},      // 规则8: 15:00-19:00  保持稳定
+            {19,     21,     18,    3,       6},      // 规则9: 19:00-21:00  持续流出
+            {21,     22,     12,    2,       5},      // 规则9延:21:00-22:00  继续清场
+            {22,     24,     6,     1,       3}       // 规则10:22:00-24:00  逐渐降至0
+    };
+
+    // ===================== 公开API:获取当前时刻掩蔽单元内的总人数 =====================
+
+    /**
+     * 【核心方法】根据当前系统时间,动态计算并返回掩蔽单元内应有的模拟总人数。
+     * <p>
+     * 无需预生成数据,即时根据24小时时间规则曲线计算出合理人数,
+     * 支持任意时间粒度的连续调用,通过自适应管理器自动扩容保护。
+     *
+     * @return 当前时刻的模拟人数(int, >= 0)
+     */
+    public static int getCurrentPeopleCount() {
+        return calculatePeopleCount(LocalDateTime.now());
+    }
+
+    /**
+     * 【核心方法-指定时间版本】根据指定时间,动态计算掩蔽单元内应有的模拟总人数。
+     *
+     * @param targetTime 目标时间
+     * @return 该时刻的模拟人数(int, >= 0)
+     */
+    public static int calculatePeopleCount(LocalDateTime targetTime) {
+        // 1. 自适应检查:是否需要扩容
+        adaptiveCheck();
+
+        int hour = targetTime.getHour();
+        int minute = targetTime.getMinute();
+
+        // 2. 匹配对应时段规则
+        int[] rule = matchTimeRule(hour);
+        int baseCount = rule[2];
+        int fluctuationMin = rule[3];
+        int fluctuationMax = rule[4];
+
+        // 3. 时段内线性插值:使同一小时内不同分钟的人数更平滑自然
+        // 特殊处理:基准为0的时段(如00:00-05:00无人期)不参与插值,强制保持0
+        double minuteRatio = minute / 60.0;
+        int interpolatedBase = (baseCount == 0) ? 0 : interpolateWithinHour(hour, baseCount, minuteRatio);
+
+        // 4. 叠加随机波动
+        int fluctuation = ThreadLocalRandom.current().nextInt(fluctuationMin, fluctuationMax + 1);
+        boolean positiveTrend = isInRisingPhase(hour);
+        int peopleCount = interpolatedBase + (positiveTrend ? fluctuation : -fluctuation);
+
+        // 5. 边界约束
+        peopleCount = Math.max(peopleCount, MIN_PEOPLE);
+
+        // 6. 记录调用
+        recordCall();
+
+        return peopleCount / 4;
+    }
+
+    // ===================== 兼容接口:生成全天模拟数据列表(动态条数)=====================
+
+    /**
+     * 生成当天全天的模拟数据列表。
+     * <p>
+     * 条数由自适应管理器的当前阈值决定(默认48条/半小时粒度),
+     * 当业务需求更高频时可自动扩容至更细粒度(如288条/5分钟粒度)。
+     *
+     * @return 模拟数据列表
+     */
+    public static List<PeopleFlowData> generateAllDayData() {
+        return generateAllDayData(dynamicThreshold.get());
+    }
+
+    /**
+     * 生成全天模拟数据列表,自定义数据条数。
+     * <p>
+     * 动态扩容策略:
+     * - requestedCount <= DEFAULT_MAX_CALLS(48): 正常生成,半小时粒度
+     * - 48 < requestedCount <= MAX_EXPANSION_LIMIT(288): 自动扩容,最小粒度5分钟
+     * - requestedCount > 288: 截断到上限,防止内存溢出
+     *
+     * @param requestedCount 请求数据条数
+     * @return 模拟数据列表(实际条数可能因边界截断而小于请求值)
+     */
+    public static List<PeopleFlowData> generateAllDayData(int requestedCount) {
+        // 边界约束:防止非法参数导致问题
+        int actualCount = normalizeRequestedCount(requestedCount);
+
+        List<PeopleFlowData> dataList = new ArrayList<>(actualCount);
+        long totalMinutes = 24 * 60;
+        long intervalMinutes = totalMinutes / actualCount;
+
+        LocalDateTime currentTime = LocalDateTime.now().withHour(0).withMinute(0).withSecond(0).withNano(0);
+        int lastPeople = 0;
+
+        for (int i = 0; i < actualCount; i++) {
+            int currentPeople = calculatePeopleCount(currentTime);
+
+            // 反推流入流出(保持数据完整性)
+            int delta = currentPeople - lastPeople;
+            int inFlow, outFlow;
+            if (delta >= 0) {
+                inFlow = delta + ThreadLocalRandom.current().nextInt(0, 2);
+                outFlow = ThreadLocalRandom.current().nextInt(0, 2);
+            } else {
+                inFlow = ThreadLocalRandom.current().nextInt(0, 2);
+                outFlow = -delta + ThreadLocalRandom.current().nextInt(0, 2);
+            }
+
+            String timeStr = currentTime.format(TIME_FORMATTER);
+            dataList.add(new PeopleFlowData(timeStr, lastPeople, inFlow, outFlow, currentPeople));
+
+            lastPeople = currentPeople;
+            currentTime = currentTime.plusMinutes(intervalMinutes);
+        }
+        return dataList;
+    }
+
+    // ===================== 自适应调用管理器 =====================
+
+    /**
+     * 自适应检查:在每次调用前评估是否需要触发扩容
+     */
+    private static void adaptiveCheck() {
+        int calls = callCount.incrementAndGet();
+        int threshold = dynamicThreshold.get();
+
+        if (calls > threshold && !expanded) {
+            synchronized (AirDefenseSimulator.class) {
+                // 双检锁,防止并发重复扩容
+                if (calls > dynamicThreshold.get() && !expanded) {
+                    int newThreshold = expandThreshold(threshold);
+                    dynamicThreshold.set(newThreshold);
+                    expanded = true;
+                }
+            }
+        }
+    }
+
+    /**
+     * 动态扩容策略:按指数因子扩展阈值,但不超过硬性上限
+     *
+     * @param currentThreshold 当前阈值
+     * @return 扩容后的新阈值
+     */
+    private static int expandThreshold(int currentThreshold) {
+        int newThreshold = (int) Math.min(currentThreshold * EXPANSION_FACTOR, MAX_EXPANSION_LIMIT);
+        return Math.max(newThreshold, currentThreshold + 1); // 至少+1保证递增
+    }
+
+    /**
+     * 记录一次有效调用
+     */
+    private static void recordCall() {
+        lastCallTimestamp.set(System.currentTimeMillis());
+    }
+
+    /**
+     * 归置自适应管理器状态(测试/重启场景使用)
+     */
+    public static void resetAdaptiveManager() {
+        callCount.set(0);
+        dynamicThreshold.set(DEFAULT_MAX_CALLS);
+        lastCallTimestamp.set(0);
+        expanded = false;
+    }
+
+    // ===================== 管理器状态查询API =====================
+
+    /** 获取已调用次数 */
+    public static int getCallCount() { return callCount.get(); }
+
+    /** 获取当前动态阈值 */
+    public static int getDynamicThreshold() { return dynamicThreshold.get(); }
+
+    /** 获取扩容上限 */
+    public static int getMaxExpansionLimit() { return MAX_EXPANSION_LIMIT; }
+
+    /** 是否已触发过扩容 */
+    public static boolean isExpanded() { return expanded; }
+
+    // ===================== 私有工具方法 =====================
+
+    /**
+     * 根据小时数匹配对应的时段规则
+     */
+    private static int[] matchTimeRule(int hour) {
+        for (int[] rule : TIME_SLOT_RULES) {
+            if (hour >= rule[0] && hour < rule[1]) {
+                return rule;
+            }
+        }
+        // 兜底:返回午夜规则
+        return TIME_SLOT_RULES[0];
+    }
+
+    /**
+     * 判断当前时段是否处于人数上升期(用于决定波动的正负方向)
+     */
+    private static boolean isInRisingPhase(int hour) {
+        return (hour >= 5 && hour < 9) || (hour >= 13 && hour < 15);
+    }
+
+    /**
+     * 时段内线性插值:根据分钟位置微调基准值,实现同小时内平滑过渡
+     * <p>
+     * 例如:6:00基准15人→8:00基准30人,6:30应约为22人而非突变为30人
+     */
+    private static int interpolateWithinHour(int hour, int baseCount, double minuteRatio) {
+        int[] currentRule = matchTimeRule(hour);
+        int nextHour = (hour + 1) % 24;
+        int[] nextRule = matchTimeRule(nextHour);
+        int nextBase = nextRule[2];
+
+        // 线性插值:当前基准 + (下一基准-当前基准) * 分钟占比
+        int interpolated = (int) Math.round(baseCount + (nextBase - baseCount) * minuteRatio * 0.3);
+        return Math.max(interpolated, MIN_PEOPLE);
+    }
+
+    /**
+     * 规范化请求数据条数:边界条件处理,防止越界
+     */
+    private static int normalizeRequestedCount(int requestedCount) {
+        if (requestedCount <= 0) {
+            return DEFAULT_MAX_CALLS; // 非法值回退默认
+        }
+        if (requestedCount > MAX_EXPANSION_LIMIT) {
+            return MAX_EXPANSION_LIMIT; // 截断到上限
+        }
+        return requestedCount;
+    }
+
+    // ===================== 测试主方法 =====================
+    public static void main(String[] args) {
+        System.out.println("========== 自适应人防掩蔽单元人数模拟器 ==========");
+        System.out.println("初始阈值: " + DEFAULT_MAX_CALLS + ", 上限: " + MAX_EXPANSION_LIMIT);
+
+        // ========== 核心:60次模拟调用测试 ==========
+        test60Calls();
+    }
+
+    /**
+     * 模拟60次调用 calculatePeopleCount,每次传入不同的时间点,
+     * 验证自适应扩容机制和时段规则的正确性
+     */
+    private static void test60Calls() {
+        final int TOTAL_CALLS = 60;
+
+        // 重置管理器,确保从干净状态开始
+        resetAdaptiveManager();
+
+        System.out.println("\n========== 【60次模拟调用测试】开始 ==========");
+        System.out.printf("初始状态 → 阈值:%d, 已调用:%d, 已扩容:%s%n",
+                getDynamicThreshold(), getCallCount(), isExpanded());
+        printSeparator(70);
+        System.out.printf("%-6s %-8s %-10s %-15s %-10s%n", "序号", "时间点", "返回人数", "时段规则描述", "当前阈值");
+        printSeparator(70);
+
+        LocalDateTime baseDate = LocalDateTime.now().toLocalDate().atStartOfDay();
+
+        for (int i = 0; i < TOTAL_CALLS; i++) {
+            // 每24分钟推进一次(60次 × 24min = 1440min = 24小时,刚好覆盖全天)
+            int totalMinutesElapsed = i * 24;
+            int hour = (totalMinutesElapsed / 60) % 24;
+            int minute = totalMinutesElapsed % 60;
+
+            LocalDateTime targetTime = baseDate.plusMinutes(totalMinutesElapsed);
+            int peopleCount = calculatePeopleCount(targetTime);
+
+            // 获取时段描述
+            String timeSlotDesc = getTimeSlotDescription(hour);
+            String timeStr = String.format("%02d:%02d", hour, minute);
+
+            System.out.printf("%-6d %-8s %-10d %-15s %-10d%n",
+                    i + 1, timeStr, peopleCount, timeSlotDesc, getDynamicThreshold());
+        }
+
+        printSeparator(70);
+        System.out.printf("最终状态 → 总调用:%d, 阈值:%d, 已扩容:%b%n",
+                getCallCount(), getDynamicThreshold(), isExpanded());
+
+        // 验证结论
+        if (getCallCount() >= TOTAL_CALLS) {
+            System.out.println("[PASS] 60次调用全部完成,自适应机制正常工作");
+        } else {
+            System.out.println("[WARN] 调用次数异常: 期望" + TOTAL_CALLS + ", 实际" + getCallCount());
+        }
+        if (getDynamicThreshold() > DEFAULT_MAX_CALLS) {
+            System.out.println("[PASS] 阈值已自动扩容: " + DEFAULT_MAX_CALLS + " → " + getDynamicThreshold()
+                    + "(因调用次数超过原阈值触发)");
+        }
+        System.out.println("========== 【60次模拟调用测试】结束 ==========");
+    }
+
+    /**
+     * Java8兼容:打印指定长度的分隔线(替代 String.repeat())
+     */
+    private static void printSeparator(int length) {
+        StringBuilder sb = new StringBuilder(length);
+        for (int i = 0; i < length; i++) {
+            sb.append('─');
+        }
+        System.out.println(sb.toString());
+    }
+
+    /**
+     * 根据小时数返回时段规则描述(用于测试输出)
+     */
+    private static String getTimeSlotDescription(int hour) {
+        if (hour < 5) return "00-05无人";
+        if (hour < 6) return "05-06缓升";
+        if (hour < 8) return "06-08第一高点";
+        if (hour < 9) return "08-09调整";
+        if (hour < 11) return "09-11稳定";
+        if (hour < 12) return "11-12就餐流出";
+        if (hour < 13) return "12-13午间低谷";
+        if (hour < 14) return "13-14回流";
+        if (hour < 15) return "14-15第二高点";
+        if (hour < 19) return "15-19稳定";
+        if (hour < 21) return "19-21持续流出";
+        if (hour < 22) return "21-22清场";
+        return "22-00降至0";
+    }
+}

+ 177 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/util/BasementClimateUtil.java

@@ -0,0 +1,177 @@
+package com.usky.cdi.service.util;
+
+import java.time.LocalDate;
+import java.time.Month;
+import java.util.Random;
+/**
+ *
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2026/4/23
+ */
+
+/**
+ * 上海地区 地下室温湿度常量 + 四季判断工具
+ */
+public class BasementClimateUtil {
+
+    private static final Random RANDOM = new Random();
+
+    // ====================== 春季(3-5月)======================
+    public static final double SPRING_TEMP_MIN = 16.0;
+    public static final double SPRING_TEMP_MAX = 20.0;
+    public static final double SPRING_HUMI_MIN = 70.0;
+    public static final double SPRING_HUMI_MAX = 74.0;
+
+    // ====================== 夏季(6-8月)======================
+    public static final double SUMMER_TEMP_MIN = 22.0;
+    public static final double SUMMER_TEMP_MAX = 26.0;
+    public static final double SUMMER_HUMI_MIN = 80.0;
+    public static final double SUMMER_HUMI_MAX = 84.0;
+
+    // ====================== 秋季(9-11月)=====================
+    public static final double AUTUMN_TEMP_MIN = 17.0;
+    public static final double AUTUMN_TEMP_MAX = 21.0;
+    public static final double AUTUMN_HUMI_MIN = 68.0;
+    public static final double AUTUMN_HUMI_MAX = 72.0;
+
+    // ====================== 冬季(12-1-2月)===================
+    public static final double WINTER_TEMP_MIN = 12.0;
+    public static final double WINTER_TEMP_MAX = 16.0;
+    public static final double WINTER_HUMI_MIN = 60.0;
+    public static final double WINTER_HUMI_MAX = 64.0;
+
+    /**
+     * 获取当前系统时间的季节
+     * 0=春 1=夏 2=秋 3=冬
+     */
+    public static int getSeason() {
+        return getSeason(LocalDate.now());
+    }
+
+    /**
+     * 根据日期判断季节(JDK8 标准写法)
+     */
+    public static int getSeason(LocalDate date) {
+        Month month = date.getMonth();
+        int monthValue = month.getValue();
+
+        if (monthValue >= 3 && monthValue <= 5) {
+            return 0; // 春
+        } else if (monthValue >= 6 && monthValue <= 8) {
+            return 1; // 夏
+        } else if (monthValue >= 9 && monthValue <= 11) {
+            return 2; // 秋
+        } else {
+            return 3; // 冬(12、1、2月)
+        }
+    }
+
+    /**
+     * 获取当前季节的温度范围 [最小值, 最大值]
+     */
+    public static double getCurrentTempRange() {
+        return randomBetween(getTempRange(getSeason())[0], getTempRange(getSeason())[1]);
+    }
+
+    /**
+     * 获取当前季节的湿度范围 [最小值, 最大值]
+     */
+    public static double getCurrentHumiRange() {
+        return randomBetween(getHumiRange(getSeason())[0], getHumiRange(getSeason())[1]);
+    }
+
+    /**
+     * 根据季节获取温度范围
+     */
+    public static double[] getTempRange(int season) {
+        if (season == 0) {
+            return new double[]{SPRING_TEMP_MIN, SPRING_TEMP_MAX};
+        } else if (season == 1) {
+            return new double[]{SUMMER_TEMP_MIN, SUMMER_TEMP_MAX};
+        } else if (season == 2) {
+            return new double[]{AUTUMN_TEMP_MIN, AUTUMN_TEMP_MAX};
+        } else if (season == 3) {
+            return new double[]{WINTER_TEMP_MIN, WINTER_TEMP_MAX};
+        } else {
+            throw new IllegalArgumentException("无效季节:" + season);
+        }
+    }
+
+    /**
+     * 根据季节获取湿度范围
+     */
+    public static double[] getHumiRange(int season) {
+        if (season == 0) {
+            return new double[]{SPRING_HUMI_MIN, SPRING_HUMI_MAX};
+        } else if (season == 1) {
+            return new double[]{SUMMER_HUMI_MIN, SUMMER_HUMI_MAX};
+        } else if (season == 2) {
+            return new double[]{AUTUMN_HUMI_MIN, AUTUMN_HUMI_MAX};
+        } else if (season == 3) {
+            return new double[]{WINTER_HUMI_MIN, WINTER_HUMI_MAX};
+        } else {
+            throw new IllegalArgumentException("无效季节:" + season);
+        }
+    }
+
+    /**
+     * 获取地下室相对室外的温度差值
+     * 负=地下室更冷,正=地下室更暖
+     */
+    public static double getTempDiffWithOutdoor() {
+        return getTempDiffWithOutdoor(getSeason());
+    }
+
+    /**
+     * 根据季节获取温度差值
+     */
+    public static double getTempDiffWithOutdoor(int season) {
+        // 所有区间范围都 < 2.0
+        switch (season) {
+            case 0: // 春:地下室略低
+                return randomBetween(-1.0, -0.5);
+            case 1: // 夏:地下室明显低
+                return randomBetween(-6.0, -5.5);
+            case 2: // 秋:低于室外
+                return randomBetween(-3.5, -3.0);
+            case 3: // 冬:地下室更高
+                return randomBetween(4.5, 5.0);
+            default:
+                return 0.0;
+        }
+    }
+
+    /**
+     * 获取地下室相对室外的湿度差值
+     */
+    public static double getHumiDiffWithOutdoor() {
+        return getHumiDiffWithOutdoor(getSeason());
+    }
+
+    /**
+     * 根据季节获取湿度差值
+     */
+    public static double getHumiDiffWithOutdoor(int season) {
+        switch (season) {
+            case 0:
+                return randomBetween(12.0, 13.0);
+            case 1:
+                return randomBetween(19.0, 20.0);
+            case 2:
+                return randomBetween(9.0, 10.0);
+            case 3:
+                return randomBetween(7.0, 8.0);
+            default:
+                return 0.0;
+        }
+    }
+
+    /**
+     * 生成 [min, max] 之间的随机小数,保留 2 位
+     */
+    private static double randomBetween(double min, double max) {
+        double val = min + (max - min) * RANDOM.nextDouble();
+        return Math.round(val * 100) / 100.0;
+    }
+}

+ 213 - 45
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/util/DeviceDataQuery.java

@@ -3,6 +3,7 @@ package com.usky.cdi.service.util;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.usky.cdi.domain.DmpDevice;
 import com.usky.cdi.mapper.DmpDeviceMapper;
 import com.usky.cdi.service.config.DeviceFieldConfig;
@@ -11,7 +12,6 @@ import lombok.Data;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
-import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.stereotype.Component;
 
 import java.text.DecimalFormat;
@@ -40,9 +40,14 @@ public class DeviceDataQuery {
     @Autowired
     private DeviceFieldConfig deviceFieldConfig;
 
+    private static final Set<Integer> ALLOWED_DEVICE_TYPES = new HashSet<>(Arrays.asList(
+            711, 710, 709, 708, 707
+    ));
+
     // 定义各参数的格式化器(整数位,小数位)
     private static final DecimalFormat FORMAT_2_2 = new DecimalFormat("00.00"); // 2位整数+2位小数
     private static final DecimalFormat FORMAT_0_3 = new DecimalFormat("0.000"); // 0位整数+3位小数
+    private static final DecimalFormat FORMAT_1_2 = new DecimalFormat("0.00");  // 1位整数+2位小数
     private static final DecimalFormat FORMAT_3_2 = new DecimalFormat("000.00"); // 3位整数+2位小数
     private static final DecimalFormat FORMAT_4_2 = new DecimalFormat("0000.00"); // 4位整数+2位小数
 
@@ -79,6 +84,8 @@ public class DeviceDataQuery {
     private static final double FLOATING_RANGE_MAX = 0.5;
     private static final double FLOATING_RANGE_MIN1 = 0.0;
     private static final double FLOATING_RANGE_MAX1 = 1.0;
+    private static final double SEWAGE_LEVEL_MIN = 0.0;
+    private static final double SEWAGE_LEVEL_MAX = 0.1;
 
     private static int callCount = 0;
 
@@ -104,14 +111,56 @@ public class DeviceDataQuery {
 
             log.debug("请求设备数据接口,设备数量:{}", deviceUuids.size());
             String response = HttpClientUtils.doPostJson(baseUrl, requestBody.toJSONString());
-            log.warn("接口返回数据:{}", response);
+            log.info("接口返回数据:{}", response);
 
             List<JSONObject> resultList = parseResponseData(response, transferVO.getDeviceType(), transferVO.getDevices());
+            boolean useXiangyuMock = false;
+            List<JSONObject> xiangyuDataList = new ArrayList<>();
+
+            // 当租户数据为空时获取桃浦象屿相同设备数据进行模拟(修复版:不修改原始设备列表)
+            if (resultList.isEmpty() && ALLOWED_DEVICE_TYPES.contains(transferVO.getDeviceType())) {
+                LambdaQueryWrapper<DmpDevice> queryWrapper = new LambdaQueryWrapper<>();
+                queryWrapper.eq(DmpDevice::getTenantId, 1205)
+                        .eq(DmpDevice::getDeviceType, transferVO.getDeviceType())
+                        .orderByAsc(DmpDevice::getId)
+                        .last("LIMIT 1");
+                DmpDevice xiangyuDevice = dmpDeviceMapper.selectOne(queryWrapper);
+
+                if (xiangyuDevice != null) {
+                    JSONObject xiangyuReq = new JSONObject();
+                    xiangyuReq.put("deviceuuid", Collections.singletonList(xiangyuDevice.getDeviceUuid()));
+                    String xiangyuResp = HttpClientUtils.doPostJson(baseUrl, xiangyuReq.toJSONString());
+
+                    JSONObject responseObj = JSON.parseObject(xiangyuResp);
+                    // 修复:象屿接口可能返回null/空串/非JSON,导致responseObj为null引发NPE
+                    if (responseObj != null && responseObj.getJSONArray("data") != null && !responseObj.getJSONArray("data").isEmpty()) {
+                        JSONArray dataArray = responseObj.getJSONArray("data");
+                        JSONObject deviceData = dataArray.getJSONObject(0);
+                        JSONObject metrics = deviceData.getJSONObject("metrics");
+                        Object realtime = metrics.get("realtime");
+                        System.out.println("接口返回数据时间戳对象:" + realtime);
+                        Long timestamp = realtime instanceof Long ? (Long) realtime : null;
+                        System.out.println("接口返回数据时间戳:" + timestamp);
+
+                        if (timestamp != null && timestamp >= (System.currentTimeMillis() - 7 * 24 * 60 * 60 * 1000L)) {
+                            // 修复:不修改原始设备列表,直接生成模拟模板
+                            useXiangyuMock = true;
+                            xiangyuDataList = parseResponseData(xiangyuResp, transferVO.getDeviceType(),
+                                    Collections.singletonList(xiangyuDevice));
+                        }
+                    }
+                }
+            }
 
             // 若返回数据为空且开启模拟模式,则生成模拟数据
             if (resultList.isEmpty() && simulation) {
                 log.info("接口返回数据为空,生成模拟数据,设备类型:{}", transferVO.getDeviceType());
-                resultList = generateSimulationData(transferVO.getDeviceType(), transferVO.getDevices(), null);
+                if (useXiangyuMock && !xiangyuDataList.isEmpty()) {
+                    // 使用象屿数据作为模板生成全量模拟
+                    resultList = generateSimulationData(transferVO.getDeviceType(), transferVO.getDevices(), xiangyuDataList.get(0));
+                } else {
+                    resultList = generateSimulationData(transferVO.getDeviceType(), transferVO.getDevices(), null);
+                }
             } else if (resultList.size() < transferVO.getDevices().size()) {
                 log.warn("接口返回数据数量与请求数量不一致,设备类型:{}", transferVO.getDeviceType());
 
@@ -136,22 +185,22 @@ public class DeviceDataQuery {
                 List<DmpDevice> missingDevices = missingDeviceIds.stream()
                         .map(requestDeviceMap::get)
                         .collect(Collectors.toList());
-                List<JSONObject> missingSimulationData = generateSimulationData(transferVO.getDeviceType(), missingDevices, resultList.get(0));
-                // 将模拟数据与返回数据结合
-                resultList.addAll(missingSimulationData);
 
-                // 校验结合后的数据是否与请求的device_id一一对应
-                Set<String> combinedDeviceIds = resultList.stream()
-                        .map(data -> data.getString("device_id"))
-                        .filter(Objects::nonNull)
-                        .collect(Collectors.toSet());
+                JSONObject templateData = resultList.isEmpty() ?
+                        (xiangyuDataList.isEmpty() ? null : xiangyuDataList.get(0))
+                        : resultList.get(0);
 
-                if (combinedDeviceIds.size() != transferVO.getDevices().size()) {
-                    log.warn("数据整合后仍存在缺失,请求设备数量:{},返回设备数量:{}",
-                            transferVO.getDevices().size(), combinedDeviceIds.size());
-                } else {
-                    log.debug("数据整合完成,设备数量与请求一致:{}", combinedDeviceIds.size());
-                }
+                List<JSONObject> missingSimulationData = generateSimulationData(transferVO.getDeviceType(), missingDevices, templateData);
+                // 修复:不再清空原有数据
+                resultList.addAll(missingSimulationData);
+            }
+
+            // 最终校验
+            if (resultList.size() != transferVO.getDevices().size()) {
+                log.warn("数据整合后仍存在缺失,请求设备数量:{},返回设备数量:{}",
+                        transferVO.getDevices().size(), resultList.size());
+            } else {
+                log.debug("数据整合完成,设备数量与请求一致:{}", resultList.size());
             }
 
             return resultList;
@@ -231,8 +280,15 @@ public class DeviceDataQuery {
                 }
 
                 // 添加设备标识信息
-                targetData.put("deviceuuid", deviceUuid);
                 String deviceId = deviceUuidToIdMap.get(deviceUuid);
+
+                // 如果找不到,直接丢弃这条数据,不放入结果
+                if (deviceId == null) {
+                    continue;
+                }
+
+                // 能走到这里,说明一定有 deviceId
+                targetData.put("deviceuuid", deviceUuid);
                 targetData.put("device_id", deviceId);
 
                 if (hasValidData) {
@@ -287,10 +343,15 @@ public class DeviceDataQuery {
     private List<JSONObject> generateSimulationData(Integer deviceType, List<DmpDevice> devices, JSONObject standard) {
         List<JSONObject> simulationList = new ArrayList<>();
         long currentTime = System.currentTimeMillis();
+        // System.out.println("深拷贝当前时间戳:" + currentTime);
 
         // 获取天气数据(仅在需要时)
+        // 期望:只有“象屿模板为空/缺字段”时,才用天气数据对温湿度做修正;有象屿模板就不要再相加
+        boolean needWeatherForTemp = (deviceType != null && deviceType == 707) && (standard == null || standard.get("wd") == null);
+        boolean needWeatherForHumi = (deviceType != null && deviceType == 708) && (standard == null || standard.get("sd") == null);
+        boolean needWeatherForElectric = (deviceType != null && deviceType == 704) && (standard == null);
         Map<String, Double> weatherData = null;
-        if (standard == null && (deviceType == 707 || deviceType == 704)) {
+        if (needWeatherForTemp || needWeatherForHumi || needWeatherForElectric) {
             weatherData = WeatherFetcher.fetchWeather();
         }
 
@@ -298,18 +359,46 @@ public class DeviceDataQuery {
             JSONObject simData;
 
             if (standard != null) {
-                // ✅ 使用标准模板进行模拟(深拷贝)
-                simData = JSONObject.parseObject(standard.toJSONString());
+                simData = new JSONObject();
+                simData.putAll(standard);
+
+                // 【关键】先移除,确保 FastJSON 内部缓存失效
+                simData.remove("realtime");
+                simData.remove("device_id");
+                simData.remove("deviceuuid");
+
+                // 再重新 put,强制类型正确
                 simData.put("realtime", currentTime);
                 simData.put("device_id", device.getDeviceId());
-                // 可选:替换 device_uuid(如果模板中有)
-                if (device.getDeviceUuid() != null && simData.containsKey("device_uuid")) {
-                    simData.put("device_uuid", device.getDeviceUuid());
+                // System.out.println("深拷贝时间戳1:" + simData.getLong("realtime"));
+
+                if (device.getDeviceUuid() != null && standard.containsKey("deviceuuid")) {
+                    simData.put("deviceuuid", device.getDeviceUuid());
                 }
 
-                // 对数值字段添加微小扰动(±2% 或固定范围)
-                addNoiseToNumericFields(simData, deviceType);
+                // 若象屿模板里缺少温湿度字段,则再回退到天气+差值逻辑补齐(避免“所有情况都相加”)
+                if (deviceType != null && deviceType == 707 && simData.get("wd") == null) {
+                    double temp707;
+                    if (weatherData != null && !weatherData.isEmpty()) {
+                        double floating = BasementClimateUtil.getTempDiffWithOutdoor();
+                        temp707 = weatherData.get("temperature") + floating;
+                    } else {
+                        temp707 = BasementClimateUtil.getCurrentTempRange();
+                    }
+                    simData.put("wd", temp707);
+                } else if (deviceType != null && deviceType == 708 && simData.get("sd") == null) {
+                    double humi708;
+                    if (weatherData != null && !weatherData.isEmpty()) {
+                        double floating = BasementClimateUtil.getHumiDiffWithOutdoor();
+                        humi708 = weatherData.get("humidity") + floating;
+                    } else {
+                        humi708 = BasementClimateUtil.getCurrentHumiRange();
+                    }
+                    simData.put("sd", humi708);
+                }
 
+                addNoiseToNumericFields(simData, deviceType);
+                // System.out.println("深拷贝时间戳2:" + simData.getLong("realtime"));
             } else {
                 // ❌ 无标准模板,走原始随机逻辑
                 simData = new JSONObject();
@@ -320,16 +409,23 @@ public class DeviceDataQuery {
                     case 707:
                         double temp707 = 0.0;
                         if (weatherData != null && !weatherData.isEmpty()) {
-                            double floating = ThreadLocalRandom.current().nextDouble(FLOATING_RANGE_MIN, FLOATING_RANGE_MAX);
-                            temp707 = weatherData.get("temperature") + 0.5 + floating;
+                            double floating = BasementClimateUtil.getTempDiffWithOutdoor();
+                            temp707 = weatherData.get("temperature") + floating;
                         } else {
-                            temp707 = ThreadLocalRandom.current().nextDouble(TEMP_RANGE_MIN, TEMP_RANGE_MAX);
+                            temp707 = BasementClimateUtil.getCurrentTempRange();
                         }
-                        simData.put("wd", formatNumber(temp707, FORMAT_2_2));
+                        simData.put("wd", temp707);
                         break;
 
                     case 708:
-                        simData.put("sd", formatNumber(ThreadLocalRandom.current().nextDouble(HUMIDITY_RANGE_MIN, HUMIDITY_RANGE_MAX), FORMAT_2_2));
+                        double humi708 = 0.0;
+                        if (weatherData != null && !weatherData.isEmpty()) {
+                            double floating = BasementClimateUtil.getHumiDiffWithOutdoor();
+                            humi708 = weatherData.get("humidity") + floating;
+                        } else {
+                            humi708 = BasementClimateUtil.getCurrentHumiRange();
+                        }
+                        simData.put("sd", humi708);
                         break;
 
                     case 709:
@@ -355,6 +451,7 @@ public class DeviceDataQuery {
                         break;
 
                     case 704:
+                    case 705:
                         double aVoltage = ThreadLocalRandom.current().nextDouble(VOLTAGE_RANGE_MIN, VOLTAGE_RANGE_MAX);
                         simData.put("aVoltage", formatNumber(aVoltage, FORMAT_3_2));
                         double bVoltage = ThreadLocalRandom.current().nextDouble(VOLTAGE_RANGE_MIN, VOLTAGE_RANGE_MAX);
@@ -399,6 +496,17 @@ public class DeviceDataQuery {
                         simData.put("wy", 0);
                         break;
 
+                    case 716:
+                        String deviceId1 = device.getDeviceId();
+                        double value = FixedWaterLevelGenerator.getSensorValue(deviceId1);
+                        simData.put("sensorValue", formatNumber(value, FORMAT_1_2));
+                        break;
+                    // case 719:
+                    //     break;
+                    case 720:
+                        simData.put("startDuration", 0);
+                        simData.put("batteryVoltage", randomDouble261_266());
+                        break;
                     default:
                         log.warn("未知设备类型:{},无法生成模拟数据", deviceType);
                         continue;
@@ -412,34 +520,94 @@ public class DeviceDataQuery {
         return simulationList;
     }
 
+    private double randomDouble261_266() {
+        Random random = new Random();
+        int intVal = 2610 + random.nextInt(51);
+        return intVal / 100.0;
+    }
+
     private void addNoiseToNumericFields(JSONObject data, Integer deviceType) {
-        // 定义各字段的噪声比例或范围(可根据设备类型定制)
         Map<String, Double> noiseConfig = getNoiseConfigByDeviceType(deviceType);
 
+        // 这些字段永远不加噪
+        Set<String> skipFields = new HashSet<>(Arrays.asList("realtime", "device_id", "deviceuuid"));
+
         for (String key : data.keySet()) {
+            if (skipFields.contains(key)) continue;
+            if (isDiscreteField(key)) continue;
+
             Object value = data.get(key);
-            if (value instanceof Number) {
-                double original = ((Number) value).doubleValue();
-                // 默认 ±2%
-                double noiseFactor = noiseConfig.getOrDefault(key, 0.02);
+            Double original = null;
 
-                // 避免对状态类字段(如 0/1)加噪
-                if (isDiscreteField(key)) {
-                    continue;
+            // 【关键】支持 Number 和 String 两种类型
+            if (value instanceof Number) {
+                original = ((Number) value).doubleValue();
+            } else if (value instanceof String) {
+                try {
+                    original = Double.parseDouble((String) value);
+                } catch (NumberFormatException e) {
+                    System.out.println("跳过字段(非数字): " + key + " = " + value);
+                    continue; // 不是数字字符串,跳过
                 }
+            }
 
-                double noise = original * noiseFactor * (ThreadLocalRandom.current().nextDouble(-1, 1));
-                double newValue = original + noise;
+            if (original == null) continue;
 
-                // 根据原格式保留小数位(这里简化处理,也可记录原格式)
+            // 每个设备、每个字段、每次调用都是独立的随机噪声 ✅
+            double noiseFactor = noiseConfig.getOrDefault(key, 0.02);
+            double noise = original * noiseFactor * (ThreadLocalRandom.current().nextDouble(-1, 1));
+            double newValue = original + noise;
+
+            System.out.println("加噪: " + key + " | 原值=" + original + " | 噪声=" + noise + " | 新值=" + newValue);
+            log.info("加噪: {} | 原值={} | 噪声={} | 新值={}", key, original, noise, newValue);
+
+            // 保持原类型:原来是 String 就格式化回 String,原来是 Number 就放 Double
+            if (value instanceof String) {
+                data.put(key, formatNumber(newValue, getFormatByKey(key)));
+            } else {
                 data.put(key, newValue);
             }
         }
     }
 
+    /**
+     * 根据字段名获取对应的格式化器
+     */
+    private DecimalFormat getFormatByKey(String key) {
+        switch (key) {
+            case "aVoltage":
+            case "bVoltage":
+            case "cVoltage":
+            case "aElectricity":
+            case "bElectricity":
+            case "cElectricity":
+                return FORMAT_3_2;
+            case "totalPower":
+                return FORMAT_4_2;
+            case "line1TEMP":
+            case "Line2TEMP":
+            case "Line3TEMP":
+            case "Line4TEMP":
+            case "wd":
+            case "sd":
+            case "o2":
+                return FORMAT_2_2;
+            case "co2":
+                return FORMAT_0_3;
+            case "leakageCurrent":
+                return FORMAT_4_2;
+            case "sensorValue":
+                return FORMAT_1_2;
+            default:
+                return null; // 不格式化,保持原样
+        }
+    }
+
     private boolean isDiscreteField(String field) {
-        // 这些字段是状态码,不应加噪
-        return field.equalsIgnoreCase("leach_status")
+        return field.equalsIgnoreCase("realtime")
+                || field.equalsIgnoreCase("device_id")
+                || field.equalsIgnoreCase("deviceuuid")
+                || field.equalsIgnoreCase("leach_status")
                 || field.equalsIgnoreCase("sensorValue")
                 || field.equalsIgnoreCase("qx")
                 || field.equalsIgnoreCase("cd")
@@ -452,7 +620,7 @@ public class DeviceDataQuery {
         Map<String, Double> config = new HashMap<>();
         switch (deviceType) {
             case 707: // 温度
-                config.put("wd", 0.03); // ±3%
+                config.put("wd", 0.03); //
                 break;
             case 708: // 湿度
                 config.put("sd", 0.05);

+ 5 - 5
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/util/DeviceDataSyncService.java

@@ -26,12 +26,12 @@ public class DeviceDataSyncService {
      * fixedDelay:任务执行完成后固定延迟29分钟执行下一次
      * initialDelay:初始化后立即执行第一次任务
      */
-    // @Scheduled(fixedDelay = 26 * 60 * 1000, initialDelay = 0)
+    // @Scheduled(fixedDelay = 10 * 60 * 1000, initialDelay = 0)
     // public void scheduledDeviceDataSync() {
-    //     Integer tenantId = 1205;
-    //     Long engineeringId = 3101070011L;
-    //     String username = "3101070011";
-    //     String password = "5RqhJ7VG";
+    //     Integer tenantId = 1222;
+    //     Long engineeringId = 3101100021L;
+    //     String username = "3101100021";
+    //     String password = "SIixzph1";
     //     log.info("开始执行桃浦象屿人防设备数据同步定时任务,租户ID:{},工程ID:{}", tenantId, engineeringId);
     //
     //     try {

+ 49 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/util/FixedWaterLevelGenerator.java

@@ -0,0 +1,49 @@
+package com.usky.cdi.service.util;
+
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.Random;
+
+/**
+ * 固定水位生成器 - 为液位传感器(716)生成模拟数据
+ * <p>
+ * 修复说明:
+ * 1. HashMap 改为 ConcurrentHashMap,解决并发安全问题
+ * 2. 移除 containsValue 死循环风险,改用基于设备ID哈希的确定性算法,
+ *    保证每个设备获得唯一且稳定的水位值,不受设备数量限制
+ *
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2026/4/28
+ */
+
+/**
+ * 固定水位值生成器:
+ * 1. 每台设备首次调用时基于设备ID生成一个随机水位值
+ * 2. 同一设备后续调用直接取缓存,保持一致
+ * 3. 不同设备之间数值随机分散
+ */
+@Slf4j
+public class FixedWaterLevelGenerator {
+
+    private static final Map<String, Double> DEVICE_LEVEL_MAP = new ConcurrentHashMap<>();
+
+    private static final double MIN_VALUE = 0.02;
+    private static final double MAX_VALUE = 0.05;
+
+    public static double getSensorValue(String deviceId) {
+        return DEVICE_LEVEL_MAP.computeIfAbsent(deviceId, FixedWaterLevelGenerator::computeFixedValue);
+    }
+
+    private static double computeFixedValue(String deviceId) {
+        // 乘以一个大的质数,打乱原本连续ID的哈希值分布
+        long seed = (deviceId.hashCode() * 2654435761L) & 0xFFFFFFFFL;
+        Random random = new Random(seed);
+
+        double value = MIN_VALUE + random.nextDouble() * (MAX_VALUE - MIN_VALUE);
+        log.info("生成设备[{}]的固定水位值:{}", deviceId, value);
+        return Math.round(value * 1000) / 1000.0;
+    }
+}

+ 32 - 4
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/util/HttpClientUtils.java

@@ -20,6 +20,7 @@ import java.net.URI;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.UUID;
 
 @Slf4j
 public class HttpClientUtils {
@@ -111,7 +112,9 @@ public class HttpClientUtils {
     }
 
     public static String doPostJson(String url, String json) {
-        log.info(">>> 请求URL={}, 请求体={}", url, json);
+        final String requestId = UUID.randomUUID().toString();
+        final long start = System.currentTimeMillis();
+        log.info("[{}] >>> POST JSON 请求开始 URL={}, body={}", requestId, url, json);
 
         CloseableHttpResponse response = null;
         String resultString = "";
@@ -128,19 +131,44 @@ public class HttpClientUtils {
             httpPost.setEntity(entity);
             // 执行http请求
             response = HTTP_CLIENT.execute(httpPost);
-            resultString = EntityUtils.toString(response.getEntity(), "utf-8");
+            int statusCode = response.getStatusLine() != null ? response.getStatusLine().getStatusCode() : -1;
+            resultString = response.getEntity() == null ? "" : EntityUtils.toString(response.getEntity(), "utf-8");
+
+            long costMs = System.currentTimeMillis() - start;
+            String preview = resultString == null ? "null" : (resultString.length() > 500 ? resultString.substring(0, 500) + "...(truncated)" : resultString);
+
+            if (statusCode != 200) {
+                log.warn("[{}] <<< POST JSON 响应异常 status={} costMs={} preview={}", requestId, statusCode, costMs, preview);
+            } else if (resultString == null || resultString.trim().isEmpty()) {
+                log.warn("[{}] <<< POST JSON 响应为空 status=200 costMs={}", requestId, costMs);
+            } else {
+                log.info("[{}] <<< POST JSON 响应成功 status=200 costMs={} preview={}", requestId, costMs, preview);
+            }
         } catch (Exception e) {
-            e.printStackTrace();
+            long costMs = System.currentTimeMillis() - start;
+            String root = buildRootCause(e);
+            log.error("[{}] !!! POST JSON 请求异常 costMs={} URL={} body={} exType={} exMsg={} rootCause={}",
+                    requestId, costMs, url, json, e.getClass().getName(), e.getMessage(), root, e);
         } finally {
             try {
                 if (response != null) {
                     response.close();
                 }
             } catch (IOException e) {
-                e.printStackTrace();
+                log.warn("[{}] 关闭HTTP响应失败: {}", requestId, e.getMessage(), e);
             }
         }
 
         return resultString;
     }
+
+    private static String buildRootCause(Throwable t) {
+        if (t == null) return "null";
+        Throwable cur = t;
+        while (cur.getCause() != null && cur.getCause() != cur) {
+            cur = cur.getCause();
+        }
+        String msg = cur.getMessage();
+        return cur.getClass().getName() + (msg == null ? "" : (": " + msg));
+    }
 }

Some files were not shown because too many files changed in this diff