فهرست منبع

feat: 燃气真实数据化、维修统计与窨井设备类型修复

- 燃气大屏管网与压力监测点由硬编码坐标改读真实台账,指标条同步
- 管网导出新增 networkIds 参数支持勾选导出,查询字典翻译设施类型/等级/状态,
  修复导出显示裸编码;分页与导出共用同一套查询条件
- 维修统计新增 networkSummary 管段健康汇总,趋势分组改按字典编码 0/1/2/3 归类
- 压力监测管线名称按 high/medium 编码匹配压力等级
- 新增 ManholeDeviceTypes,沿类型树 parent_id 上溯窨井大类根(4)动态取全部子类型,
  修复台账有数据而首页与统计恒为 0;统计空结果显式返回全 0
- 补充排水基础数据、燃气管点与派单按钮迁移脚本
null 1 هفته پیش
والد
کامیت
428db6480a
15فایلهای تغییر یافته به همراه1456 افزوده شده و 84 حذف شده
  1. 201 0
      pipe-network-service/sql/dispatch_button_perms.sql
  2. 16 0
      pipe-network-service/sql/dispatch_plan_library_content_text.sql
  3. 325 0
      pipe-network-service/sql/migration_drainage_engineering_facility.sql
  4. 258 0
      pipe-network-service/sql/migration_drainage_jcsj_menu_perms.sql
  5. 151 0
      pipe-network-service/sql/migration_drainage_waterlogging_level_dict.sql
  6. 85 0
      pipe-network-service/sql/migration_gas_pipe_point_menu.sql
  7. 4 4
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/gasbasic/GasPipeNetworkController.java
  8. 83 4
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/gasbasic/GasRepairRecordController.java
  9. 8 2
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/PipeNetworkBaseService.java
  10. 8 6
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/EquipmentBaseServiceImpl.java
  11. 114 41
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/GasDashboardServiceImpl.java
  12. 15 9
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/ManholeLayerServiceImpl.java
  13. 74 17
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/PipeNetworkBaseServiceImpl.java
  14. 1 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/PressureMonitorServiceImpl.java
  15. 113 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/util/ManholeDeviceTypes.java

+ 201 - 0
pipe-network-service/sql/dispatch_button_perms.sql

@@ -0,0 +1,201 @@
+-- =====================================================================
+-- 调度模块按钮权限初始化(sys_menu,menu_type='F')
+-- 页面菜单按 component 定位,按钮按 parent_id + perms 幂等判重,可重复执行。
+-- 执行后需在「系统管理-角色管理」给相关角色勾选这些按钮权限并重新登录。
+-- 事务按段独立提交,失败只影响当前段。
+-- 作者/日期:2026-10-16
+-- =====================================================================
+
+-- ---------- 接报管理 ----------
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type,
+                      visible, status, perms, icon, create_by, create_time, remark)
+SELECT '新增接报', (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disOnDuty/jbgl' LIMIT 1), 1, '', NULL, 1, 0, 'F',
+       '0', '0', 'dispatch:event:add', '#', 'admin', CURRENT_TIMESTAMP, '接报管理-新增/编辑按钮'
+ WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'dispatch:event:add'
+                    AND parent_id = (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disOnDuty/jbgl' LIMIT 1));
+
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type,
+                      visible, status, perms, icon, create_by, create_time, remark)
+SELECT '编辑接报', (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disOnDuty/jbgl' LIMIT 1), 2, '', NULL, 1, 0, 'F',
+       '0', '0', 'dispatch:event:edit', '#', 'admin', CURRENT_TIMESTAMP, '接报管理-编辑按钮'
+ WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'dispatch:event:edit'
+                    AND parent_id = (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disOnDuty/jbgl' LIMIT 1));
+
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type,
+                      visible, status, perms, icon, create_by, create_time, remark)
+SELECT '删除接报', (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disOnDuty/jbgl' LIMIT 1), 3, '', NULL, 1, 0, 'F',
+       '0', '0', 'dispatch:event:remove', '#', 'admin', CURRENT_TIMESTAMP, '接报管理-删除按钮'
+ WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'dispatch:event:remove'
+                    AND parent_id = (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disOnDuty/jbgl' LIMIT 1));
+
+-- ---------- 初判核实 ----------
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type,
+                      visible, status, perms, icon, create_by, create_time, remark)
+SELECT '事件核实', (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disOnDuty/cphs' LIMIT 1), 1, '', NULL, 1, 0, 'F',
+       '0', '0', 'dispatch:event:verify', '#', 'admin', CURRENT_TIMESTAMP, '初判核实-核实按钮'
+ WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'dispatch:event:verify'
+                    AND parent_id = (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disOnDuty/cphs' LIMIT 1));
+
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type,
+                      visible, status, perms, icon, create_by, create_time, remark)
+SELECT '删除事件', (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disOnDuty/cphs' LIMIT 1), 2, '', NULL, 1, 0, 'F',
+       '0', '0', 'dispatch:event:remove', '#', 'admin', CURRENT_TIMESTAMP, '初判核实-删除按钮'
+ WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'dispatch:event:remove'
+                    AND parent_id = (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disOnDuty/cphs' LIMIT 1));
+
+-- ---------- 转接报送 ----------
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type,
+                      visible, status, perms, icon, create_by, create_time, remark)
+SELECT '事件转接', (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disOnDuty/zjbs' LIMIT 1), 1, '', NULL, 1, 0, 'F',
+       '0', '0', 'dispatch:event:transfer', '#', 'admin', CURRENT_TIMESTAMP, '转接报送-转接按钮'
+ WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'dispatch:event:transfer'
+                    AND parent_id = (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disOnDuty/zjbs' LIMIT 1));
+
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type,
+                      visible, status, perms, icon, create_by, create_time, remark)
+SELECT '事件报送', (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disOnDuty/zjbs' LIMIT 1), 2, '', NULL, 1, 0, 'F',
+       '0', '0', 'dispatch:event:report', '#', 'admin', CURRENT_TIMESTAMP, '转接报送-报送按钮'
+ WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'dispatch:event:report'
+                    AND parent_id = (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disOnDuty/zjbs' LIMIT 1));
+
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type,
+                      visible, status, perms, icon, create_by, create_time, remark)
+SELECT '删除事件', (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disOnDuty/zjbs' LIMIT 1), 3, '', NULL, 1, 0, 'F',
+       '0', '0', 'dispatch:event:remove', '#', 'admin', CURRENT_TIMESTAMP, '转接报送-删除按钮'
+ WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'dispatch:event:remove'
+                    AND parent_id = (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disOnDuty/zjbs' LIMIT 1));
+
+-- ---------- 工作催办 ----------
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type,
+                      visible, status, perms, icon, create_by, create_time, remark)
+SELECT '工作催办', (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disOnDuty/gzcb' LIMIT 1), 1, '', NULL, 1, 0, 'F',
+       '0', '0', 'dispatch:event:urge', '#', 'admin', CURRENT_TIMESTAMP, '工作催办-催办按钮'
+ WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'dispatch:event:urge'
+                    AND parent_id = (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disOnDuty/gzcb' LIMIT 1));
+
+-- ---------- 跟踪解除 ----------
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type,
+                      visible, status, perms, icon, create_by, create_time, remark)
+SELECT '开始处置', (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disOnDuty/gzjc' LIMIT 1), 1, '', NULL, 1, 0, 'F',
+       '0', '0', 'dispatch:event:dispose', '#', 'admin', CURRENT_TIMESTAMP, '跟踪解除-开始处置按钮'
+ WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'dispatch:event:dispose'
+                    AND parent_id = (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disOnDuty/gzjc' LIMIT 1));
+
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type,
+                      visible, status, perms, icon, create_by, create_time, remark)
+SELECT '处置跟踪', (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disOnDuty/gzjc' LIMIT 1), 2, '', NULL, 1, 0, 'F',
+       '0', '0', 'dispatch:event:track', '#', 'admin', CURRENT_TIMESTAMP, '跟踪解除-跟踪按钮'
+ WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'dispatch:event:track'
+                    AND parent_id = (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disOnDuty/gzjc' LIMIT 1));
+
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type,
+                      visible, status, perms, icon, create_by, create_time, remark)
+SELECT '事件解除', (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disOnDuty/gzjc' LIMIT 1), 3, '', NULL, 1, 0, 'F',
+       '0', '0', 'dispatch:event:release', '#', 'admin', CURRENT_TIMESTAMP, '跟踪解除-解除按钮'
+ WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'dispatch:event:release'
+                    AND parent_id = (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disOnDuty/gzjc' LIMIT 1));
+
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type,
+                      visible, status, perms, icon, create_by, create_time, remark)
+SELECT '删除事件', (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disOnDuty/gzjc' LIMIT 1), 4, '', NULL, 1, 0, 'F',
+       '0', '0', 'dispatch:event:remove', '#', 'admin', CURRENT_TIMESTAMP, '跟踪解除-删除按钮'
+ WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'dispatch:event:remove'
+                    AND parent_id = (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disOnDuty/gzjc' LIMIT 1));
+
+-- ---------- 事件分级 ----------
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type,
+                      visible, status, perms, icon, create_by, create_time, remark)
+SELECT '新增分级', (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disPlanManage/DisEventGrade' LIMIT 1), 1, '', NULL, 1, 0, 'F',
+       '0', '0', 'dispatch:grade:add', '#', 'admin', CURRENT_TIMESTAMP, '事件分级-新增按钮'
+ WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'dispatch:grade:add'
+                    AND parent_id = (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disPlanManage/DisEventGrade' LIMIT 1));
+
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type,
+                      visible, status, perms, icon, create_by, create_time, remark)
+SELECT '编辑分级', (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disPlanManage/DisEventGrade' LIMIT 1), 2, '', NULL, 1, 0, 'F',
+       '0', '0', 'dispatch:grade:edit', '#', 'admin', CURRENT_TIMESTAMP, '事件分级-编辑按钮'
+ WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'dispatch:grade:edit'
+                    AND parent_id = (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disPlanManage/DisEventGrade' LIMIT 1));
+
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type,
+                      visible, status, perms, icon, create_by, create_time, remark)
+SELECT '删除分级', (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disPlanManage/DisEventGrade' LIMIT 1), 3, '', NULL, 1, 0, 'F',
+       '0', '0', 'dispatch:grade:remove', '#', 'admin', CURRENT_TIMESTAMP, '事件分级-删除按钮'
+ WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'dispatch:grade:remove'
+                    AND parent_id = (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disPlanManage/DisEventGrade' LIMIT 1));
+
+-- ---------- 预案编辑 ----------
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type,
+                      visible, status, perms, icon, create_by, create_time, remark)
+SELECT '新增预案', (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disPlanManage/DisPlanEditing' LIMIT 1), 1, '', NULL, 1, 0, 'F',
+       '0', '0', 'dispatch:plan:add', '#', 'admin', CURRENT_TIMESTAMP, '预案编辑-新增按钮'
+ WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'dispatch:plan:add'
+                    AND parent_id = (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disPlanManage/DisPlanEditing' LIMIT 1));
+
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type,
+                      visible, status, perms, icon, create_by, create_time, remark)
+SELECT '编辑预案', (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disPlanManage/DisPlanEditing' LIMIT 1), 2, '', NULL, 1, 0, 'F',
+       '0', '0', 'dispatch:plan:edit', '#', 'admin', CURRENT_TIMESTAMP, '预案编辑-编辑/修订按钮'
+ WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'dispatch:plan:edit'
+                    AND parent_id = (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disPlanManage/DisPlanEditing' LIMIT 1));
+
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type,
+                      visible, status, perms, icon, create_by, create_time, remark)
+SELECT '删除预案', (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disPlanManage/DisPlanEditing' LIMIT 1), 3, '', NULL, 1, 0, 'F',
+       '0', '0', 'dispatch:plan:remove', '#', 'admin', CURRENT_TIMESTAMP, '预案编辑-删除按钮'
+ WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'dispatch:plan:remove'
+                    AND parent_id = (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disPlanManage/DisPlanEditing' LIMIT 1));
+
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type,
+                      visible, status, perms, icon, create_by, create_time, remark)
+SELECT '提交审核', (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disPlanManage/DisPlanEditing' LIMIT 1), 4, '', NULL, 1, 0, 'F',
+       '0', '0', 'dispatch:plan:submit', '#', 'admin', CURRENT_TIMESTAMP, '预案编辑-提交审核按钮'
+ WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'dispatch:plan:submit'
+                    AND parent_id = (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disPlanManage/DisPlanEditing' LIMIT 1));
+
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type,
+                      visible, status, perms, icon, create_by, create_time, remark)
+SELECT '模板库维护', (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disPlanManage/DisPlanEditing' LIMIT 1), 5, '', NULL, 1, 0, 'F',
+       '0', '0', 'dispatch:planLibrary:add', '#', 'admin', CURRENT_TIMESTAMP, '预案编辑-模板库新增/编辑'
+ WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'dispatch:planLibrary:add'
+                    AND parent_id = (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disPlanManage/DisPlanEditing' LIMIT 1));
+
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type,
+                      visible, status, perms, icon, create_by, create_time, remark)
+SELECT '模板库删除', (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disPlanManage/DisPlanEditing' LIMIT 1), 6, '', NULL, 1, 0, 'F',
+       '0', '0', 'dispatch:planLibrary:remove', '#', 'admin', CURRENT_TIMESTAMP, '预案编辑-模板库删除'
+ WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'dispatch:planLibrary:remove'
+                    AND parent_id = (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disPlanManage/DisPlanEditing' LIMIT 1));
+
+-- ---------- 预案审批 ----------
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type,
+                      visible, status, perms, icon, create_by, create_time, remark)
+SELECT '预案审批', (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disPlanManage/DisPlanApproval' LIMIT 1), 1, '', NULL, 1, 0, 'F',
+       '0', '0', 'dispatch:plan:approve', '#', 'admin', CURRENT_TIMESTAMP, '预案审批-通过/驳回按钮'
+ WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'dispatch:plan:approve'
+                    AND parent_id = (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disPlanManage/DisPlanApproval' LIMIT 1));
+
+-- ---------- 查询权限(页面列表/详情接口) ----------
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type,
+                      visible, status, perms, icon, create_by, create_time, remark)
+SELECT '事件查询', (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disOnDuty/jbgl' LIMIT 1), 9, '', NULL, 1, 0, 'F',
+       '0', '0', 'dispatch:event:list', '#', 'admin', CURRENT_TIMESTAMP, '值守管理-事件列表/详情/日志查询(各页共用)'
+ WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'dispatch:event:list');
+
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type,
+                      visible, status, perms, icon, create_by, create_time, remark)
+SELECT '分级查询', (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disPlanManage/DisEventGrade' LIMIT 1), 9, '', NULL, 1, 0, 'F',
+       '0', '0', 'dispatch:grade:list', '#', 'admin', CURRENT_TIMESTAMP, '事件分级-列表查询'
+ WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'dispatch:grade:list');
+
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type,
+                      visible, status, perms, icon, create_by, create_time, remark)
+SELECT '预案查询', (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disPlanManage/DisPlanEditing' LIMIT 1), 9, '', NULL, 1, 0, 'F',
+       '0', '0', 'dispatch:plan:list', '#', 'admin', CURRENT_TIMESTAMP, '预案编辑/审批-列表详情版本查询'
+ WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'dispatch:plan:list');
+
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type,
+                      visible, status, perms, icon, create_by, create_time, remark)
+SELECT '模板库查询', (SELECT menu_id FROM sys_menu WHERE component = 'subSystem/dispatch/disPlanManage/DisPlanEditing' LIMIT 1), 10, '', NULL, 1, 0, 'F',
+       '0', '0', 'dispatch:planLibrary:list', '#', 'admin', CURRENT_TIMESTAMP, '预案模板库-列表查询'
+ WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'dispatch:planLibrary:list');

+ 16 - 0
pipe-network-service/sql/dispatch_plan_library_content_text.sql

@@ -0,0 +1,16 @@
+-- =====================================================================
+-- 迁移脚本:模板库正文列改为 text(适配富文本 HTML 存储)
+-- 背景:dis_plan_library.plan_content 建表脚本未入库,原始类型未知;
+--       若为 varchar(n),富文本 HTML 超长会被静默截断或报「值超长」。
+--       dis_plan.content 与 dis_plan_version.content 已是 text,无需处理。
+-- 说明:PG/KES 中 varchar → text 属放宽改动,仅改元数据不重写数据,秒级完成;
+--       列已是 text 时本语句同样执行成功(等价 no-op),可重复执行(幂等)。
+-- 执行方式:KingbaseES 客户端逐条执行,或整段独立事务一次执行。
+-- 执行前建议:pg_dump -n app_user 备份(项目惯例)。
+-- 作者/日期:2026-10-16
+-- =====================================================================
+
+ALTER TABLE app_user.dis_plan_library
+    ALTER COLUMN plan_content TYPE text;
+
+COMMENT ON COLUMN app_user.dis_plan_library.plan_content IS '章节正文(富文本 HTML,无图片/视频)';

+ 325 - 0
pipe-network-service/sql/migration_drainage_engineering_facility.sql

@@ -0,0 +1,325 @@
+-- 工程设施基础数据(闸门 / 调蓄设施 / 泵站)表重建与迁移(KingbaseES, schema app_user)
+--
+-- 需求依据:《排水管网安全运行监测系统》5.3.3.1.3 工程设施基础数据管理
+--   「支持对闸门、调蓄设施、泵站等工程设施的查看、查询、筛选等管理操作,
+--     并可将相关要素在 GIS 地图中叠加展示。」
+--
+-- 背景:旧表 app_user.psgw_engineering_facility 是「监测点」口径的临时表结构,
+--   字段名与语义都不对:name 注释写成「监测点名称/设施名称」、area 实为行政区域、
+--   time 是「数据时间」、point_count 由前端现算却落库、points 存多节点 JSON,
+--   而一个工程设施在地图上就是一个点要素,应直接落经纬度。
+--
+-- 本脚本做三件事:
+--   1. 建新表 app_user.drainage_engineering_facility(按基础档案口径设计);
+--   2. 登记两个字典:drainage_facility_type(设施类型)、drainage_facility_status(运行状态);
+--   3. 把旧表数据搬到新表(坐标取 points 的第一个点;旧表保留不删,确认无误后再自行下线)。
+--
+-- 幂等:建表/索引/字典均可重复执行;数据迁移仅在「新表为空」时执行一次。
+--
+-- =====================================================================
+-- 为什么字典 INSERT 要「显式指定主键」
+-- ---------------------------------------------------------------------
+-- 本库由 MySQL 迁移至 Kingbase,sys_dict_data / sys_dict_type 沿用了 MySQL 的
+-- auto_increment(KES 的 MySQL 兼容自增,不是 PG 的 serial/identity)。迁数据时是带
+-- 显式主键写入的,自增计数器没有跟着推进,于是「INSERT 不写主键」会发出一个已被占用的号:
+--     ERROR: 重复键违反唯一约束 "sys_dict_data_pkey"
+--     DETAIL: 键值 "dict_code"=(415) 已经存在
+-- 且该列不是 identity 列,pg_get_serial_sequence / pg_depend 都拿不到序列。
+-- 因此这里统一写成 max(主键) + n,插入后再尽力校准序列。
+-- =====================================================================
+--
+-- 前置(必须先执行):结束上一次失败执行遗留的 aborted 事务
+--   上一次语句报错后,PG/KES 会把该事务标记为已失败,此时同一连接上后续任何语句都只返回
+--   「当前事务被终止,事务块结束之前的查询被忽略」,必须先回滚才能继续。
+--   注意:本句会回滚该连接上所有未提交事务;若你有别的未提交手工操作,请先单独提交再跑本脚本。
+ROLLBACK;
+
+BEGIN;
+
+-- =====================================================================
+-- 1. 建新表
+-- =====================================================================
+CREATE TABLE IF NOT EXISTS app_user.drainage_engineering_facility (
+    facility_id      varchar(64)    NOT NULL,
+    facility_code    varchar(64),
+    facility_name    varchar(200)   NOT NULL,
+    facility_type    varchar(8)     NOT NULL,
+    facility_level   varchar(8),
+    management_unit  varchar(200),
+    location         varchar(500),
+    longitude        numeric(12, 8),
+    latitude         numeric(12, 8),
+    status           varchar(8)     DEFAULT '0',
+    build_date       date,
+    design_flow      numeric(12, 2),
+    design_head      numeric(10, 2),
+    storage_capacity numeric(14, 2),
+    equipment_id     varchar(500),
+    remark           varchar(500),
+    create_by        varchar(64),
+    create_time      timestamp,
+    update_by        varchar(64),
+    update_time      timestamp,
+    del_flag         char(1)        DEFAULT '0',
+    CONSTRAINT pk_drainage_engineering_facility PRIMARY KEY (facility_id)
+);
+
+COMMENT ON TABLE  app_user.drainage_engineering_facility IS '工程设施基础数据(闸门/调蓄设施/泵站)';
+COMMENT ON COLUMN app_user.drainage_engineering_facility.facility_id      IS '设施ID(主键)';
+COMMENT ON COLUMN app_user.drainage_engineering_facility.facility_code    IS '设施编码';
+COMMENT ON COLUMN app_user.drainage_engineering_facility.facility_name    IS '设施名称';
+COMMENT ON COLUMN app_user.drainage_engineering_facility.facility_type    IS '设施类型(字典 drainage_facility_type:0闸门 1调蓄设施 2泵站)';
+COMMENT ON COLUMN app_user.drainage_engineering_facility.facility_level   IS '设施等级';
+COMMENT ON COLUMN app_user.drainage_engineering_facility.management_unit  IS '管理单位';
+COMMENT ON COLUMN app_user.drainage_engineering_facility.location        IS '位置描述';
+COMMENT ON COLUMN app_user.drainage_engineering_facility.longitude       IS 'WGS84 经度';
+COMMENT ON COLUMN app_user.drainage_engineering_facility.latitude        IS 'WGS84 纬度';
+COMMENT ON COLUMN app_user.drainage_engineering_facility.status          IS '运行状态(字典 drainage_facility_status:0正常 1检修 2停用 3故障)';
+COMMENT ON COLUMN app_user.drainage_engineering_facility.build_date      IS '建设日期';
+COMMENT ON COLUMN app_user.drainage_engineering_facility.design_flow     IS '设计流量(m³/s)';
+COMMENT ON COLUMN app_user.drainage_engineering_facility.design_head     IS '设计扬程(m)';
+COMMENT ON COLUMN app_user.drainage_engineering_facility.storage_capacity IS '调蓄容积(m³)';
+COMMENT ON COLUMN app_user.drainage_engineering_facility.equipment_id    IS '关联设备ID(多个用逗号分隔,沿用旧口径)';
+COMMENT ON COLUMN app_user.drainage_engineering_facility.remark          IS '备注';
+COMMENT ON COLUMN app_user.drainage_engineering_facility.del_flag        IS '删除标志(0正常 2删除)';
+
+CREATE INDEX IF NOT EXISTS idx_def_facility_type   ON app_user.drainage_engineering_facility (facility_type);
+CREATE INDEX IF NOT EXISTS idx_def_facility_status ON app_user.drainage_engineering_facility (status);
+CREATE INDEX IF NOT EXISTS idx_def_management_unit ON app_user.drainage_engineering_facility (management_unit);
+CREATE INDEX IF NOT EXISTS idx_def_facility_name   ON app_user.drainage_engineering_facility (facility_name);
+CREATE UNIQUE INDEX IF NOT EXISTS uk_def_facility_code ON app_user.drainage_engineering_facility (facility_code);
+
+-- =====================================================================
+-- 2. 字典:设施类型、运行状态
+-- =====================================================================
+-- 2.1 设施类型
+INSERT INTO app_user.sys_dict_type
+    (dict_id, dict_name, dict_type, status, create_by, create_time, remark)
+SELECT (SELECT COALESCE(MAX(dict_id), 0) FROM app_user.sys_dict_type) + 1,
+       '工程设施类型', 'drainage_facility_type', '0', 'admin', CURRENT_TIMESTAMP,
+       '工程设施类型:0闸门 1调蓄设施 2泵站'
+ WHERE NOT EXISTS (SELECT 1 FROM app_user.sys_dict_type WHERE dict_type = 'drainage_facility_type');
+
+UPDATE app_user.sys_dict_type
+   SET dict_name = '工程设施类型',
+       status = '0',
+       update_by = 'admin',
+       update_time = CURRENT_TIMESTAMP,
+       remark = '工程设施类型:0闸门 1调蓄设施 2泵站'
+ WHERE dict_type = 'drainage_facility_type';
+
+DELETE FROM app_user.sys_dict_data WHERE dict_type = 'drainage_facility_type';
+
+INSERT INTO app_user.sys_dict_data
+    (dict_code, dict_sort, dict_label, dict_value, dict_type, css_class, list_class,
+     is_default, status, create_by, create_time, remark)
+SELECT (SELECT COALESCE(MAX(dict_code), 0) FROM app_user.sys_dict_data) + v.rn,
+       v.dict_sort, v.dict_label, v.dict_value, 'drainage_facility_type',
+       '', v.list_class, v.is_default, '0', 'admin', CURRENT_TIMESTAMP, v.remark
+  FROM (VALUES
+        (1, 1, '闸门',     '0', 'primary', 'Y', '工程设施类型:闸门'),
+        (2, 2, '调蓄设施', '1', 'warning', 'N', '工程设施类型:调蓄设施'),
+        (3, 3, '泵站',     '2', 'success', 'N', '工程设施类型:泵站')
+       ) AS v(rn, dict_sort, dict_label, dict_value, list_class, is_default, remark)
+ ORDER BY v.rn;
+
+-- 2.2 运行状态
+INSERT INTO app_user.sys_dict_type
+    (dict_id, dict_name, dict_type, status, create_by, create_time, remark)
+SELECT (SELECT COALESCE(MAX(dict_id), 0) FROM app_user.sys_dict_type) + 1,
+       '设施运行状态', 'drainage_facility_status', '0', 'admin', CURRENT_TIMESTAMP,
+       '设施运行状态:0正常 1检修 2停用 3故障'
+ WHERE NOT EXISTS (SELECT 1 FROM app_user.sys_dict_type WHERE dict_type = 'drainage_facility_status');
+
+UPDATE app_user.sys_dict_type
+   SET dict_name = '设施运行状态',
+       status = '0',
+       update_by = 'admin',
+       update_time = CURRENT_TIMESTAMP,
+       remark = '设施运行状态:0正常 1检修 2停用 3故障'
+ WHERE dict_type = 'drainage_facility_status';
+
+DELETE FROM app_user.sys_dict_data WHERE dict_type = 'drainage_facility_status';
+
+INSERT INTO app_user.sys_dict_data
+    (dict_code, dict_sort, dict_label, dict_value, dict_type, css_class, list_class,
+     is_default, status, create_by, create_time, remark)
+SELECT (SELECT COALESCE(MAX(dict_code), 0) FROM app_user.sys_dict_data) + v.rn,
+       v.dict_sort, v.dict_label, v.dict_value, 'drainage_facility_status',
+       '', v.list_class, v.is_default, '0', 'admin', CURRENT_TIMESTAMP, v.remark
+  FROM (VALUES
+        (1, 1, '正常', '0', 'success', 'Y', '设施运行状态:正常'),
+        (2, 2, '检修', '1', 'warning', 'N', '设施运行状态:检修'),
+        (3, 3, '停用', '2', 'info',    'N', '设施运行状态:停用'),
+        (4, 4, '故障', '3', 'danger',  'N', '设施运行状态:故障')
+       ) AS v(rn, dict_sort, dict_label, dict_value, list_class, is_default, remark)
+ ORDER BY v.rn;
+
+-- =====================================================================
+-- 3. 自增计数器校准(放在插入之后:此时 max 已含新插入行)
+--    三种方式依次尝试,任一成功即可;都不成功只提示,不影响已插入的数据。
+-- =====================================================================
+DO $$
+DECLARE
+  t         record;
+  v_seq     text;
+  v_max     bigint;
+  v_default text;
+BEGIN
+  FOR t IN
+    SELECT * FROM (VALUES ('sys_dict_data', 'dict_code'),
+                          ('sys_dict_type', 'dict_id')) AS x(tbl, col)
+  LOOP
+    v_seq := NULL;
+    v_default := NULL;
+
+    -- 方式一
+    BEGIN
+      v_seq := pg_get_serial_sequence(format('app_user.%I', t.tbl), t.col);
+    EXCEPTION WHEN OTHERS THEN
+      v_seq := NULL;
+    END;
+
+    -- 方式二:解析列默认值里的 nextval('xxx')
+    IF v_seq IS NULL THEN
+      BEGIN
+        SELECT pg_get_expr(ad.adbin, ad.adrelid) INTO v_default
+          FROM pg_attribute a
+          JOIN pg_attrdef ad ON ad.adrelid = a.attrelid AND ad.adnum = a.attnum
+         WHERE a.attrelid = format('app_user.%I', t.tbl)::regclass
+           AND a.attname::text = t.col
+           AND a.attnum > 0
+           AND NOT a.attisdropped;
+      EXCEPTION WHEN OTHERS THEN
+        v_default := NULL;
+      END;
+
+      IF v_default LIKE '%nextval%' THEN
+        v_seq := substring(v_default from 'nextval\(''([^'']+)''');
+      END IF;
+    END IF;
+
+    -- 方式三
+    IF v_seq IS NULL THEN
+      BEGIN
+        SELECT d.objid::regclass::text INTO v_seq
+          FROM pg_depend d
+          JOIN pg_attribute a ON a.attrelid = d.refobjid AND a.attnum = d.refobjsubid
+          JOIN pg_class sc ON sc.oid = d.objid AND sc.relkind = 'S'
+         WHERE d.refobjid = format('app_user.%I', t.tbl)::regclass
+           AND a.attname::text = t.col
+           AND d.deptype IN ('a', 'i')
+         LIMIT 1;
+      EXCEPTION WHEN OTHERS THEN
+        v_seq := NULL;
+      END;
+    END IF;
+
+    IF v_seq IS NULL THEN
+      RAISE NOTICE '%.% 没有关联序列(应为 MySQL 兼容 auto_increment),跳过校准。本次数据已显式指定主键插入,不影响本脚本;但后续在该表手工新增行仍可能撞主键。',
+                   t.tbl, t.col;
+      CONTINUE;
+    END IF;
+
+    EXECUTE format('SELECT COALESCE(max(%I), 0) FROM app_user.%I', t.col, t.tbl) INTO v_max;
+    BEGIN
+      PERFORM setval(v_seq::regclass, GREATEST(v_max, 1), true);
+      RAISE NOTICE '%.% 自增序列已校准: % (max=%, next=%)', t.tbl, t.col, v_seq, v_max, v_max + 1;
+    EXCEPTION WHEN OTHERS THEN
+      RAISE NOTICE '%.% 序列校准失败: %,跳过', t.tbl, t.col, SQLERRM;
+    END;
+  END LOOP;
+END $$;
+
+-- =====================================================================
+-- 4. 旧表数据迁移(仅当新表为空时执行一次)
+--    坐标:旧表 points 是 JSON 数组,取第一个元素的 lng/lat;
+--          解析失败或为空时置 NULL,该设施不会出现在地图上,但列表仍可见。
+--    旧表字段若有缺失,整段迁移跳过并提示,不影响建表与字典。
+-- =====================================================================
+DO $$
+DECLARE
+  v_idx integer := 0;
+  r     record;
+  v_lng numeric(12, 8);
+  v_lat numeric(12, 8);
+BEGIN
+  IF to_regclass('app_user.psgw_engineering_facility') IS NULL THEN
+    RAISE NOTICE '旧表 app_user.psgw_engineering_facility 不存在,跳过数据迁移';
+    RETURN;
+  END IF;
+
+  IF EXISTS (SELECT 1 FROM app_user.drainage_engineering_facility) THEN
+    RAISE NOTICE '新表 app_user.drainage_engineering_facility 已有数据,跳过迁移(避免重复导入)';
+    RETURN;
+  END IF;
+
+  FOR r IN SELECT * FROM app_user.psgw_engineering_facility LOOP
+    v_idx := v_idx + 1;
+    v_lng := NULL;
+    v_lat := NULL;
+
+    BEGIN
+      IF r.points IS NOT NULL AND r.points <> '' THEN
+        v_lng := (regexp_match(r.points, '"lng"\s*:\s*(-?[0-9]+(\.[0-9]+)?)'))[1]::numeric(12, 8);
+        v_lat := (regexp_match(r.points, '"lat"\s*:\s*(-?[0-9]+(\.[0-9]+)?)'))[1]::numeric(12, 8);
+      END IF;
+    EXCEPTION WHEN OTHERS THEN
+      v_lng := NULL;
+      v_lat := NULL;
+    END;
+
+    INSERT INTO app_user.drainage_engineering_facility (
+        facility_id, facility_code, facility_name, facility_type, facility_level,
+        management_unit, location, longitude, latitude, status, build_date,
+        design_flow, design_head, storage_capacity, equipment_id, remark,
+        create_by, create_time, update_by, update_time, del_flag
+    ) VALUES (
+        r.id,
+        'GCSS-' || lpad(v_idx::text, 4, '0'),
+        COALESCE(NULLIF(trim(r.name), ''), '未命名设施'),
+        CASE r.facility_type
+            WHEN '闸门'     THEN '0'
+            WHEN '调蓄设施' THEN '1'
+            WHEN '泵站'     THEN '2'
+            WHEN '0'        THEN '0'
+            WHEN '1'        THEN '1'
+            WHEN '2'        THEN '2'
+            ELSE '0'
+        END,
+        NULL,
+        NULL,
+        r.area,
+        v_lng,
+        v_lat,
+        '0',
+        NULL,
+        NULL, NULL, NULL,
+        r.equipment_id,
+        NULL,
+        'admin', COALESCE(r.create_time, CURRENT_TIMESTAMP),
+        'admin', COALESCE(r.update_time, r.create_time, CURRENT_TIMESTAMP),
+        '0'
+    );
+  END LOOP;
+
+  RAISE NOTICE '旧表数据迁移完成,共迁移 % 条', v_idx;
+EXCEPTION WHEN OTHERS THEN
+  RAISE NOTICE '旧表数据迁移跳过:%', SQLERRM;
+END $$;
+
+-- =====================================================================
+-- 5. 结果核验(可选,取消注释执行)
+-- =====================================================================
+-- 设施类型与数量
+--    SELECT facility_type, count(*) FROM app_user.drainage_engineering_facility
+--     WHERE del_flag = '0' GROUP BY facility_type ORDER BY facility_type;
+-- 能在地图上展示的条数(有坐标)
+--    SELECT count(*) FROM app_user.drainage_engineering_facility
+--     WHERE del_flag = '0' AND longitude IS NOT NULL AND latitude IS NOT NULL;
+-- 字典是否齐全(应分别返回 3 行、4 行)
+--    SELECT dict_type, dict_value, dict_label, list_class FROM app_user.sys_dict_data
+--     WHERE dict_type IN ('drainage_facility_type', 'drainage_facility_status')
+--     ORDER BY dict_type, dict_sort;
+
+COMMIT;

+ 258 - 0
pipe-network-service/sql/migration_drainage_jcsj_menu_perms.sql

@@ -0,0 +1,258 @@
+-- 排水基础数据(工程设施 / 易积水点):菜单、按钮、角色授权
+--
+-- 覆盖范围:
+--   页面菜单(menu_type='C')
+--     · 工程设施基础数据管理  subSystem/drainage/jcsj/gcssgl
+--     · 易积水点管理          subSystem/drainage/jcsj/yjsdgl
+--   按钮权限(menu_type='F')
+--     · drainage:facility:add / edit / remove
+--     · drainage:waterlogging:add / edit / remove
+--   角色授权(sys_role_menu)
+--     · 新建页面菜单 → 授予「已经拥有其父级目录(基础数据)权限」的角色
+--     · 新建按钮     → 授予「已经拥有该页面菜单权限」的角色
+--     这样只跟随既有授权扩散,不会把按钮放开给原本没权限的角色。
+--
+-- 父级定位(多路兜底):
+--   1) 用兄弟页面 component='subSystem/drainage/jcsj/psfqgl' 反查 parent_id(最可靠)
+--   2) 任意 drainage/jcsj 页面的 parent_id
+--   3) menu_type='M' 且 menu_name='基础数据'
+--
+-- 幂等:菜单按 component 判重,按钮按 parent_id + perms 判重,授权按 (role_id, menu_id) 判重,
+--       可重复执行;已存在的菜单只更新名称/路径/组件/权限标识,不改排序与父级。
+-- 主键:menu_id 显式取 max+1 —— 本库由 MySQL 迁移而来,自增计数器可能未推进。
+--
+-- 执行后建议重新登录一次,让前端重新拉取菜单与权限。
+--
+-- 前置:结束上一次失败执行遗留的 aborted 事务(事务外执行只会告警,不报错)
+ROLLBACK;
+
+BEGIN;
+
+SET search_path TO app_user, public;
+
+DO $$
+DECLARE
+  v_parent bigint;
+  v_page   bigint;
+  v_next   bigint;
+  v_role   record;
+  r        record;
+
+  -- 【按需修改】需要直接授予这套菜单/按钮的角色 role_key。
+  --   上面的步骤只会「跟随既有授权」扩散(谁原本能进基础数据,谁就拿到新页面;
+  --   谁原本能进该页面,谁就拿到新按钮)。如果目标角色原本连基础数据目录都没有,
+  --   就在这里显式列出它的 role_key,脚本会额外给它授权。
+  --   先执行 `SELECT role_id, role_key, role_name FROM sys_role;` 查清 role_key 再填。
+  --   留空数组表示不做额外授权。
+  v_extra_role_keys text[] := ARRAY[]::text[];
+BEGIN
+  -- ================================================================
+  -- 1. 定位「基础数据」目录
+  -- ================================================================
+  SELECT parent_id INTO v_parent
+    FROM sys_menu
+   WHERE component = 'subSystem/drainage/jcsj/psfqgl'
+   LIMIT 1;
+
+  IF v_parent IS NULL THEN
+    SELECT parent_id INTO v_parent
+      FROM sys_menu
+     WHERE component LIKE '%drainage/jcsj/%'
+       AND parent_id IS NOT NULL
+       AND parent_id <> 0
+     LIMIT 1;
+  END IF;
+
+  IF v_parent IS NULL THEN
+    SELECT menu_id INTO v_parent
+      FROM sys_menu
+     WHERE menu_type = 'M'
+       AND menu_name = '基础数据'
+     LIMIT 1;
+  END IF;
+
+  IF v_parent IS NULL THEN
+    RAISE NOTICE '未找到「基础数据」目录菜单,请先确认排水子系统菜单结构后手工添加。';
+    RETURN;
+  END IF;
+
+  RAISE NOTICE '「基础数据」目录 menu_id = %', v_parent;
+
+  -- ================================================================
+  -- 2. 页面菜单(menu_type = 'C')
+  -- ================================================================
+  FOR r IN
+    SELECT * FROM (VALUES
+        ('工程设施基础数据管理', 'gcssgl', 'subSystem/drainage/jcsj/gcssgl',
+         'drainage:facility:list', 'guide', 3),
+        ('易积水点管理', 'yjsdgl', 'subSystem/drainage/jcsj/yjsdgl',
+         'drainage:waterlogging:list', 'location', 4)
+      ) AS x(menu_name, path, component, perms, icon, order_num)
+  LOOP
+    -- 先按「组件 + 基础数据目录」精确定位,避免多子系统复用同一组件时取错记录
+    SELECT menu_id INTO v_page
+      FROM sys_menu
+     WHERE component = r.component AND parent_id = v_parent
+     LIMIT 1;
+
+    IF v_page IS NULL THEN
+      SELECT menu_id INTO v_page
+        FROM sys_menu
+       WHERE component = r.component
+       LIMIT 1;
+    END IF;
+
+    IF v_page IS NULL THEN
+      SELECT COALESCE(MAX(menu_id), 0) + 1 INTO v_next FROM sys_menu;
+
+      INSERT INTO sys_menu
+          (menu_id, menu_name, parent_id, order_num, path, component,
+           is_frame, is_cache, menu_type, visible, status, perms, icon,
+           create_by, create_time, remark)
+      VALUES
+          (v_next, r.menu_name, v_parent, r.order_num, r.path, r.component,
+           1, 0, 'C', '0', '0', r.perms, r.icon,
+           'admin', CURRENT_TIMESTAMP, r.menu_name);
+
+      v_page := v_next;
+      RAISE NOTICE '已新增页面菜单:% (menu_id=%)', r.menu_name, v_page;
+
+      -- 跟随父级授权:拥有「基础数据」目录的角色,一并获得新页面
+      INSERT INTO sys_role_menu (role_id, menu_id)
+      SELECT DISTINCT rm.role_id, v_page
+        FROM sys_role_menu rm
+       WHERE rm.menu_id = v_parent
+         AND NOT EXISTS (
+             SELECT 1 FROM sys_role_menu e
+              WHERE e.role_id = rm.role_id AND e.menu_id = v_page
+         );
+      RAISE NOTICE '  → 已按「基础数据」目录的授权同步给角色';
+    ELSE
+      UPDATE sys_menu
+         SET menu_name  = r.menu_name,
+             path       = r.path,
+             component  = r.component,
+             menu_type  = 'C',
+             visible    = '0',
+             status     = '0',
+             perms      = r.perms,
+             icon       = r.icon,
+             update_by  = 'admin',
+             update_time = CURRENT_TIMESTAMP
+       WHERE menu_id = v_page;
+
+      RAISE NOTICE '页面菜单已存在,已同步名称/路径/组件:% (menu_id=%)', r.menu_name, v_page;
+    END IF;
+  END LOOP;
+
+  -- ================================================================
+  -- 3. 按钮权限(menu_type = 'F')
+  -- ================================================================
+  FOR r IN
+    SELECT * FROM (VALUES
+        ('subSystem/drainage/jcsj/gcssgl', '新增工程设施', 'drainage:facility:add',       1),
+        ('subSystem/drainage/jcsj/gcssgl', '修改工程设施', 'drainage:facility:edit',      2),
+        ('subSystem/drainage/jcsj/gcssgl', '删除工程设施', 'drainage:facility:remove',    3),
+        ('subSystem/drainage/jcsj/yjsdgl', '新增易积水点', 'drainage:waterlogging:add',    1),
+        ('subSystem/drainage/jcsj/yjsdgl', '修改易积水点', 'drainage:waterlogging:edit',   2),
+        ('subSystem/drainage/jcsj/yjsdgl', '删除易积水点', 'drainage:waterlogging:remove', 3)
+      ) AS x(parent_component, menu_name, perms, order_num)
+  LOOP
+    SELECT menu_id INTO v_page
+      FROM sys_menu
+     WHERE component = r.parent_component
+     ORDER BY menu_id
+     LIMIT 1;
+
+    IF v_page IS NULL THEN
+      RAISE NOTICE '跳过按钮 %:未找到页面菜单 %', r.perms, r.parent_component;
+      CONTINUE;
+    END IF;
+
+    IF EXISTS (
+        SELECT 1 FROM sys_menu
+         WHERE perms = r.perms AND parent_id = v_page
+    ) THEN
+      RAISE NOTICE '按钮已存在,跳过:%', r.perms;
+      CONTINUE;
+    END IF;
+
+    SELECT COALESCE(MAX(menu_id), 0) + 1 INTO v_next FROM sys_menu;
+
+    INSERT INTO sys_menu
+        (menu_id, menu_name, parent_id, order_num, path, component,
+         is_frame, is_cache, menu_type, visible, status, perms, icon,
+         create_by, create_time, remark)
+    VALUES
+        (v_next, r.menu_name, v_page, r.order_num, '', NULL,
+         1, 0, 'F', '0', '0', r.perms, '#',
+         'admin', CURRENT_TIMESTAMP, '页面按钮权限');
+
+    RAISE NOTICE '已新增按钮:% (%)', r.menu_name, r.perms;
+
+    -- 跟随页面授权:拥有该页面菜单的角色,一并获得新按钮
+    INSERT INTO sys_role_menu (role_id, menu_id)
+    SELECT DISTINCT rm.role_id, v_next
+      FROM sys_role_menu rm
+     WHERE rm.menu_id = v_page
+       AND NOT EXISTS (
+           SELECT 1 FROM sys_role_menu e
+            WHERE e.role_id = rm.role_id AND e.menu_id = v_next
+       );
+  END LOOP;
+
+  -- ================================================================
+  -- 4. 显式角色授权(按需启用)
+  --    给 v_extra_role_keys 里列出的角色,一次性授齐:
+  --    两个页面菜单 + 六个按钮。
+  -- ================================================================
+  IF v_extra_role_keys IS NOT NULL AND array_length(v_extra_role_keys, 1) > 0 THEN
+    FOR v_role IN
+      SELECT role_id, role_key, role_name FROM sys_role WHERE role_key = ANY(v_extra_role_keys)
+    LOOP
+      INSERT INTO sys_role_menu (role_id, menu_id)
+      SELECT v_role.role_id, m.menu_id
+        FROM sys_menu m
+       WHERE (
+               m.component IN ('subSystem/drainage/jcsj/gcssgl', 'subSystem/drainage/jcsj/yjsdgl')
+               OR m.perms IN ('drainage:facility:add', 'drainage:facility:edit', 'drainage:facility:remove',
+                              'drainage:waterlogging:add', 'drainage:waterlogging:edit',
+                              'drainage:waterlogging:remove')
+             )
+         AND NOT EXISTS (
+             SELECT 1 FROM sys_role_menu e
+              WHERE e.role_id = v_role.role_id AND e.menu_id = m.menu_id
+         );
+
+      RAISE NOTICE '已显式授权角色:% (%)', v_role.role_name, v_role.role_key;
+    END LOOP;
+
+    IF NOT FOUND THEN
+      RAISE NOTICE '警告:v_extra_role_keys 中的 role_key 在 sys_role 里一个都没匹配到,请核对。';
+    END IF;
+  END IF;
+
+  RAISE NOTICE '菜单与按钮处理完成。若某个角色仍看不到按钮,请在「角色管理」中手工勾选。';
+END $$;
+
+-- =====================================================================
+-- 结果核验(可选,取消注释执行)
+-- =====================================================================
+-- 页面菜单与按钮(应返回 2 个 'C' + 6 个 'F')
+--    SELECT m.menu_id, m.menu_name, m.parent_id, m.menu_type, m.perms, m.order_num
+--      FROM sys_menu m
+--     WHERE m.component IN ('subSystem/drainage/jcsj/gcssgl', 'subSystem/drainage/jcsj/yjsdgl')
+--        OR m.perms IN ('drainage:facility:add','drainage:facility:edit','drainage:facility:remove',
+--                       'drainage:waterlogging:add','drainage:waterlogging:edit','drainage:waterlogging:remove')
+--     ORDER BY m.menu_type DESC, m.order_num;
+-- 角色授权情况
+--    SELECT rm.role_id, r.role_name, m.menu_name, m.menu_type, m.perms
+--      FROM sys_role_menu rm
+--      JOIN sys_role r ON r.role_id = rm.role_id
+--      JOIN sys_menu m ON m.menu_id = rm.menu_id
+--     WHERE m.perms LIKE 'drainage:facility:%'
+--        OR m.perms LIKE 'drainage:waterlogging:%'
+--        OR m.component IN ('subSystem/drainage/jcsj/gcssgl', 'subSystem/drainage/jcsj/yjsdgl')
+--     ORDER BY rm.role_id, m.menu_type DESC, m.order_num;
+
+COMMIT;

+ 151 - 0
pipe-network-service/sql/migration_drainage_waterlogging_level_dict.sql

@@ -0,0 +1,151 @@
+-- 排水分区「积水等级」字典(KingbaseES)
+-- 用途:排水分区基础数据管理(psgw_drainage_partition.level)的积水等级改为字典驱动,
+--      下拉框、列表标签配色、详情展示统一读取 sys_dict_data。
+-- 口径:与易积水点(WaterloggingPoint.waterlogging_level)保持一致
+--      0-未积水 / 1-轻度积水 / 2-中度积水 / 3-严重积水。
+-- 说明:只维护字典定义,不改表结构、不改业务数据;脚本幂等,可重复执行。
+--
+-- =====================================================================
+-- 为什么本脚本要「显式指定主键」
+-- ---------------------------------------------------------------------
+-- 本库由 MySQL 迁移至 Kingbase,sys_dict_data / sys_dict_type 建表沿用了 MySQL 的
+-- auto_increment(KES 的 MySQL 兼容自增,不是 PG 的 serial/identity)。迁数据时是带显式
+-- 主键写入的,自增计数器没有跟着推进,于是「INSERT 不写主键」会发出一个已被占用的号:
+--     ERROR: 重复键违反唯一约束 "sys_dict_data_pkey"
+--     DETAIL: 键值 "dict_code"=(415) 已经存在
+-- 且该列不是 identity 列,pg_get_serial_sequence / pg_depend 都拿不到序列,
+-- ALTER COLUMN ... RESTART WITH 也会报「不是标识列」。
+--
+-- 因此处理方式是:
+--   ① INSERT 一律写成 max(主键) + n,显式指定,彻底绕开自增计数器;
+--   ② 插入完再把能找到的序列 setval 到最新 max(步骤 3),让后续新增不受影响;
+--      若确实没有关联序列(纯 auto_increment),步骤 3 只提示、不报错。
+-- =====================================================================
+--
+-- 前置(必须先执行):结束上一次失败执行遗留的 aborted 事务
+--   上一次语句报错后,PG/KES 会把该事务标记为已失败,此时同一连接上后续任何语句都只返回
+--   「当前事务被终止,事务块结束之前的查询被忽略」,必须先回滚才能继续。
+--   注意:本句会回滚该连接上所有未提交事务;若你有别的未提交手工操作,请先单独提交再跑本脚本。
+ROLLBACK;
+
+BEGIN;
+
+-- 1. 字典类型(幂等登记;dict_id 显式取 max+1)
+INSERT INTO app_user.sys_dict_type
+    (dict_id, dict_name, dict_type, status, create_by, create_time, remark)
+SELECT (SELECT COALESCE(MAX(dict_id), 0) FROM app_user.sys_dict_type) + 1,
+       '积水等级', 'drainage_waterlogging_level', '0', 'admin', CURRENT_TIMESTAMP,
+       '积水等级:0未积水 1轻度积水 2中度积水 3严重积水'
+ WHERE NOT EXISTS (SELECT 1 FROM app_user.sys_dict_type WHERE dict_type = 'drainage_waterlogging_level');
+
+UPDATE app_user.sys_dict_type
+   SET dict_name = '积水等级',
+       status = '0',
+       update_by = 'admin',
+       update_time = CURRENT_TIMESTAMP,
+       remark = '积水等级:0未积水 1轻度积水 2中度积水 3严重积水'
+ WHERE dict_type = 'drainage_waterlogging_level';
+
+-- 2. 字典数据(幂等重写;dict_code 显式取 max+序号;list_class 决定列表标签配色)
+DELETE FROM app_user.sys_dict_data WHERE dict_type = 'drainage_waterlogging_level';
+
+INSERT INTO app_user.sys_dict_data
+    (dict_code, dict_sort, dict_label, dict_value, dict_type, css_class, list_class,
+     is_default, status, create_by, create_time, remark)
+SELECT (SELECT COALESCE(MAX(dict_code), 0) FROM app_user.sys_dict_data) + v.rn,
+       v.dict_sort, v.dict_label, v.dict_value, 'drainage_waterlogging_level',
+       '', v.list_class, v.is_default, '0', 'admin', CURRENT_TIMESTAMP, v.remark
+  FROM (VALUES
+        (1, 1, '未积水',   '0', 'success', 'Y', '积水等级:未积水'),
+        (2, 2, '轻度积水', '1', 'primary', 'N', '积水等级:轻度积水'),
+        (3, 3, '中度积水', '2', 'warning', 'N', '积水等级:中度积水'),
+        (4, 4, '严重积水', '3', 'danger',  'N', '积水等级:严重积水')
+       ) AS v(rn, dict_sort, dict_label, dict_value, list_class, is_default, remark)
+ ORDER BY v.rn;
+
+-- 3. 自增计数器校准(放在插入之后:此时 max 已含新插入的 4 行)
+--    三种方式依次尝试,任一成功即可;都不成功只提示,不影响本脚本已插入的数据。
+--      方式一 pg_get_serial_sequence(serial / identity)
+--      方式二 从列默认值里解析 nextval('...')(KES 常见形态)
+--      方式三 pg_depend 关联序列(兜底)
+DO $$
+DECLARE
+  t         record;
+  v_seq     text;
+  v_max     bigint;
+  v_default text;
+BEGIN
+  FOR t IN
+    SELECT * FROM (VALUES ('sys_dict_data', 'dict_code'),
+                          ('sys_dict_type', 'dict_id')) AS x(tbl, col)
+  LOOP
+    v_seq := NULL;
+    v_default := NULL;
+
+    -- 方式一
+    BEGIN
+      v_seq := pg_get_serial_sequence(format('app_user.%I', t.tbl), t.col);
+    EXCEPTION WHEN OTHERS THEN
+      v_seq := NULL;
+    END;
+
+    -- 方式二:解析列默认值里的 nextval('xxx')
+    IF v_seq IS NULL THEN
+      BEGIN
+        SELECT pg_get_expr(ad.adbin, ad.adrelid) INTO v_default
+          FROM pg_attribute a
+          JOIN pg_attrdef ad ON ad.adrelid = a.attrelid AND ad.adnum = a.attnum
+         WHERE a.attrelid = format('app_user.%I', t.tbl)::regclass
+           AND a.attname::text = t.col
+           AND a.attnum > 0
+           AND NOT a.attisdropped;
+      EXCEPTION WHEN OTHERS THEN
+        v_default := NULL;
+      END;
+
+      IF v_default LIKE '%nextval%' THEN
+        v_seq := substring(v_default from 'nextval\(''([^'']+)''');
+      END IF;
+    END IF;
+
+    -- 方式三
+    IF v_seq IS NULL THEN
+      BEGIN
+        SELECT d.objid::regclass::text INTO v_seq
+          FROM pg_depend d
+          JOIN pg_attribute a ON a.attrelid = d.refobjid AND a.attnum = d.refobjsubid
+          JOIN pg_class sc ON sc.oid = d.objid AND sc.relkind = 'S'
+         WHERE d.refobjid = format('app_user.%I', t.tbl)::regclass
+           AND a.attname::text = t.col
+           AND d.deptype IN ('a', 'i')
+         LIMIT 1;
+      EXCEPTION WHEN OTHERS THEN
+        v_seq := NULL;
+      END;
+    END IF;
+
+    IF v_seq IS NULL THEN
+      RAISE NOTICE '%.% 没有关联序列(应为 MySQL 兼容 auto_increment),跳过校准。本次数据已显式指定主键插入,不影响本脚本;但后续在该表手工新增行仍可能撞主键。',
+                   t.tbl, t.col;
+      CONTINUE;
+    END IF;
+
+    EXECUTE format('SELECT COALESCE(max(%I), 0) FROM app_user.%I', t.col, t.tbl) INTO v_max;
+    BEGIN
+      PERFORM setval(v_seq::regclass, GREATEST(v_max, 1), true);
+      RAISE NOTICE '%.% 自增序列已校准: % (max=%, next=%)', t.tbl, t.col, v_seq, v_max, v_max + 1;
+    EXCEPTION WHEN OTHERS THEN
+      RAISE NOTICE '%.% 序列校准失败: %,跳过', t.tbl, t.col, SQLERRM;
+    END;
+  END LOOP;
+END $$;
+
+-- 4. 存量数据体检(只查不改):level 为空或不在 0-3 的记录
+--    SELECT id, name, "level" FROM app_user.psgw_drainage_partition
+--     WHERE "level" IS NULL OR "level" NOT IN (0, 1, 2, 3);
+
+-- 5. 结果核验(可选,取消注释执行):应返回 4 行,dict_value 依次 0~3
+--    SELECT dict_value, dict_label, list_class FROM app_user.sys_dict_data
+--     WHERE dict_type = 'drainage_waterlogging_level' ORDER BY dict_sort;
+
+COMMIT;

+ 85 - 0
pipe-network-service/sql/migration_gas_pipe_point_menu.sql

@@ -0,0 +1,85 @@
+-- 燃气基础数据:新增「管点台账」菜单
+--
+-- 页面组件:subSystem/gas/gasBasicadata/gasPipePoint
+-- 数据来源:app_user.gas_pipe_point(燃气管点专属表)
+--   后端接口 /api/gas-pipe-point/{findByPage|getById|save|updateById|deleteById}
+--
+-- 父级定位(两路兜底):
+--   1) 用兄弟页面 component='subSystem/gas/gasBasicadata/gasPipelintNet' 反查 parent_id(最可靠)
+--   2) menu_type='M' 且 menu_name='基础数据管理',且父级是燃气目录
+--
+-- 幂等:菜单按 component 判重,已存在则只更新名称/路径/父级,不改排序;
+--       授权按 (role_id, menu_id) 判重。
+-- 主键:menu_id 显式取 max+1 —— 本库由 MySQL 迁移而来,自增计数器可能未推进。
+--
+-- 执行后请重新登录一次,让前端重新拉取菜单。
+
+-- 前置:结束上一次失败执行遗留的 aborted 事务
+ROLLBACK;
+
+BEGIN;
+
+SET search_path TO app_user, public;
+
+DO $$
+DECLARE
+  v_parent bigint;
+  v_page   bigint;
+  v_role   record;
+BEGIN
+  -- 1. 定位「燃气 - 基础数据管理」目录
+  SELECT parent_id INTO v_parent
+    FROM sys_menu
+   WHERE component = 'subSystem/gas/gasBasicadata/gasPipelintNet'
+   LIMIT 1;
+
+  IF v_parent IS NULL THEN
+    SELECT menu_id INTO v_parent
+      FROM sys_menu
+     WHERE menu_type = 'M'
+       AND menu_name = '基础数据管理'
+       AND parent_id IN (SELECT menu_id FROM sys_menu WHERE menu_name LIKE '%燃气%')
+     LIMIT 1;
+  END IF;
+
+  IF v_parent IS NULL THEN
+    RAISE NOTICE '未找到燃气-基础数据管理目录,请手工确认 parent_id 后重跑';
+    RETURN;
+  END IF;
+
+  -- 2. 页面菜单
+  SELECT menu_id INTO v_page
+    FROM sys_menu
+   WHERE component = 'subSystem/gas/gasBasicadata/gasPipePoint'
+   LIMIT 1;
+
+  IF v_page IS NULL THEN
+    SELECT COALESCE(MAX(menu_id), 0) + 1 INTO v_page FROM sys_menu;
+    INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query,
+                          is_frame, is_cache, menu_type, visible, status, perms, icon,
+                          create_by, create_time, remark)
+    VALUES (v_page, '管点台账', v_parent, 3, 'gasPipePoint',
+            'subSystem/gas/gasBasicadata/gasPipePoint', NULL,
+            1, 0, 'C', '0', '0', '', 'list',
+            'admin', CURRENT_TIMESTAMP, '燃气管点台账');
+    RAISE NOTICE '已新增菜单,menu_id=%', v_page;
+  ELSE
+    UPDATE sys_menu
+       SET menu_name = '管点台账',
+           parent_id = v_parent,
+           path      = 'gasPipePoint',
+           visible   = '0',
+           status    = '0'
+     WHERE menu_id = v_page;
+    RAISE NOTICE '菜单已存在,已更新,menu_id=%', v_page;
+  END IF;
+
+  -- 3. 跟随既有授权:原本能进父级目录的角色,一并授予该页面
+  FOR v_role IN SELECT role_id FROM sys_role_menu WHERE menu_id = v_parent LOOP
+    IF NOT EXISTS (SELECT 1 FROM sys_role_menu WHERE role_id = v_role.role_id AND menu_id = v_page) THEN
+      INSERT INTO sys_role_menu (role_id, menu_id) VALUES (v_role.role_id, v_page);
+    END IF;
+  END LOOP;
+END $$;
+
+COMMIT;

+ 4 - 4
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/gasbasic/GasPipeNetworkController.java

@@ -82,10 +82,10 @@ public class GasPipeNetworkController {
     @ApiOperation("导出Excel")
     @Anonymous
     public void exportData(HttpServletResponse response,
-                           @RequestParam(required = false) String networkName,
-                           @RequestParam(required = false) String status) {
-        // 燃气模块:仅查询 network_type=0 的燃气设施
-        List<PipeNetworkExportDTO> list = service.exportGasData(networkName, status);
+                           PipeNetworkPageInDTO dto,
+                           @RequestParam(required = false) List<String> networkIds) {
+        // 燃气模块:仅查询 network_type=0 的燃气设施;networkIds 非空时只导出勾选记录
+        List<PipeNetworkExportDTO> list = service.exportGasData(dto, networkIds);
         ExcelUtil<PipeNetworkExportDTO> util = new ExcelUtil<>(PipeNetworkExportDTO.class);
         util.exportExcel(response, list, "燃气管网数据");
     }

+ 83 - 4
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/gasbasic/GasRepairRecordController.java

@@ -16,7 +16,13 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
+import java.time.LocalDate;
 import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
 
 @Slf4j
 @RestController
@@ -24,6 +30,11 @@ import java.time.LocalDateTime;
 @Api(tags = "燃气管网维修记录")
 public class GasRepairRecordController {
 
+    /**
+     * 健康等级分界:累计维修达到该次数即计入重点管段
+     */
+    private static final int HEALTH_HIGH_RISK_TIMES = 4;
+
     @Autowired
     private PipeRepairRecordService service;
 
@@ -68,19 +79,87 @@ public class GasRepairRecordController {
             int mDiff = (today.getYear() - r.getRepairDate().getYear()) * 12 + today.getMonthValue() - r.getRepairDate().getMonthValue();
             int idx = 5 - mDiff;
             if (idx < 0 || idx >= 6) continue;
-            String lv = levelMap.getOrDefault(r.getNetworkId(), "medium");
-            if ("high".equals(lv)) highData[idx]++;
-            else if ("low".equals(lv)) lowData[idx]++;
-            else mediumData[idx]++;
+            // network_level 存的是字典 pipe_network_level 的编码:0-高压、1-次高压、2-中压、3-低压
+            // 图表只画三档,次高压并入中压;兼容历史数据里直接存 high/low 的写法
+            String lv = levelMap.get(r.getNetworkId());
+            if ("0".equals(lv) || "high".equals(lv)) {
+                highData[idx]++;
+            } else if ("3".equals(lv) || "low".equals(lv)) {
+                lowData[idx]++;
+            } else {
+                mediumData[idx]++;
+            }
         }
         java.util.Map<String, Object> trend = new java.util.LinkedHashMap<>();
         trend.put("months", months); trend.put("high", highData);
         trend.put("medium", mediumData); trend.put("low", lowData);
         result.put("trend", trend);
 
+        // 按管网聚合,供维修记录拓扑图做健康状态分级
+        result.put("networkSummary", buildNetworkSummary(all));
+
         return AjaxResult.success(result);
     }
 
+    /**
+     * 按管网聚合维修情况
+     *
+     * 返回累计维修次数、最近维修日期与最近维修类型,以及重点管段与近 30 天维修的汇总值。
+     */
+    private Map<String, Object> buildNetworkSummary(List<PipeRepairRecord> all) {
+        LocalDate today = LocalDate.now();
+        LocalDate thirtyDaysAgo = today.minusDays(30);
+
+        Map<String, Integer> countMap = new HashMap<>();
+        Map<String, LocalDate> lastDateMap = new HashMap<>();
+        Map<String, String> lastTypeMap = new HashMap<>();
+        int last30Days = 0;
+
+        for (PipeRepairRecord record : all) {
+            String networkId = record.getNetworkId();
+            if (networkId == null || networkId.isEmpty()) {
+                continue;
+            }
+            countMap.merge(networkId, 1, Integer::sum);
+            LocalDate repairDate = record.getRepairDate();
+            if (repairDate == null) {
+                continue;
+            }
+            LocalDate lastDate = lastDateMap.get(networkId);
+            if (lastDate == null || repairDate.isAfter(lastDate)) {
+                lastDateMap.put(networkId, repairDate);
+                lastTypeMap.put(networkId, record.getRepairType());
+            }
+            if (!repairDate.isBefore(thirtyDaysAgo)) {
+                last30Days++;
+            }
+        }
+
+        int highRiskNetworks = 0;
+        List<Map<String, Object>> items = new ArrayList<>(countMap.size());
+        for (Map.Entry<String, Integer> entry : countMap.entrySet()) {
+            int repairCount = entry.getValue();
+            if (repairCount >= HEALTH_HIGH_RISK_TIMES) {
+                highRiskNetworks++;
+            }
+            Map<String, Object> item = new LinkedHashMap<>();
+            item.put("networkId", entry.getKey());
+            item.put("repairCount", repairCount);
+            LocalDate lastDate = lastDateMap.get(entry.getKey());
+            item.put("lastRepairDate", lastDate == null ? null : lastDate.toString());
+            item.put("lastRepairType", lastTypeMap.get(entry.getKey()));
+            items.add(item);
+        }
+
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("totalRepairs", all.size());
+        result.put("repairedNetworks", countMap.size());
+        result.put("highRiskNetworks", highRiskNetworks);
+        result.put("last30DaysRepairs", last30Days);
+        result.put("items", items);
+        return result;
+    }
+
     @GetMapping("/findByPage")
     @ApiOperation("维修记录分页查询")
     @Anonymous

+ 8 - 2
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/PipeNetworkBaseService.java

@@ -38,9 +38,15 @@ public interface PipeNetworkBaseService extends IService<PipeNetworkBase> {
 
     /**
      * 燃气设施导出数据
-     * <p>仅查询 network_type=0 的燃气设施</p>
+     *
+     * 仅查询 network_type=0 的燃气设施。networkIds 非空时按主键导出勾选记录,
+     * 否则按 dto 中的筛选条件导出全部匹配记录。
+     *
+     * @param dto        筛选条件,可为空
+     * @param networkIds 勾选的管网主键,可为空
+     * @return 导出数据
      */
-    List<PipeNetworkExportDTO> exportGasData(String networkName, String status);
+    List<PipeNetworkExportDTO> exportGasData(PipeNetworkPageInDTO dto, List<String> networkIds);
 
     /* ==================== 排水模块专用查询(强制 network_type=1) ==================== */
 

+ 8 - 6
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/EquipmentBaseServiceImpl.java

@@ -16,6 +16,7 @@ import com.zksy.base.enums.CurrentStatusEnum;
 import com.zksy.base.enums.DistrictEnum;
 import com.zksy.base.enums.EquipmentTypeEnum;
 import com.zksy.base.enums.OnlineStatusEnum;
+import com.zksy.base.util.ManholeDeviceTypes;
 import com.zksy.base.domain.vo.EquipmentBaseVO;
 import com.zksy.base.domain.vo.EquipmentFullVO;
 import com.zksy.base.event.DeviceStatusChangePublisher;
@@ -589,9 +590,12 @@ public class EquipmentBaseServiceImpl extends ServiceImpl<EquipmentBaseMapper, E
      */
     @Override
     public Map<String, Object> getManholeDeviceStatistics(ManholeDeviceStatisticsInDTO inDTO) {
-        // 窨井设备:排水窨井(equipment_type_id=9)/供水窨井(equipment_type_id=10),筛选时仅统计所选类别
+        // 窨井设备类型按类型树动态解析(窨井大类下全部子类型 + 历史遗留编码 5/9/10),
+        // 不再硬编码,避免台账有设备而统计恒为 0
+        List<EquipmentType> allTypes = equipmentTypeMapper.selectList(null);
         List<Integer> manholeTypeIds = inDTO.getEquipmentTypeId() != null
-                ? Collections.singletonList(inDTO.getEquipmentTypeId()) : Arrays.asList(5, 9, 10);
+                ? Collections.singletonList(inDTO.getEquipmentTypeId())
+                : ManholeDeviceTypes.resolveTypeIds(allTypes);
         List<EquipmentBase> devices = this.list(new LambdaQueryWrapper<EquipmentBase>()
                 .in(EquipmentBase::getEquipmentTypeId, manholeTypeIds)
                 .eq(inDTO.getDistrict() != null, EquipmentBase::getDistrict, inDTO.getDistrict()));
@@ -612,10 +616,8 @@ public class EquipmentBaseServiceImpl extends ServiceImpl<EquipmentBaseMapper, E
                 })
                 .collect(Collectors.toList());
 
-        // 类型分布:按设备类型分组统计(9=排水窨井设备,10=供水窨井设备)
-        Map<Integer, String> typeNameMap = new HashMap<>();
-        typeNameMap.put(9, "排水窨井设备");
-        typeNameMap.put(10, "供水窨井设备");
+        // 类型分布:按设备类型分组统计,名称取设备类型表
+        Map<Integer, String> typeNameMap = ManholeDeviceTypes.resolveTypeNames(allTypes);
         Map<Integer, Long> typeCountMap = devices.stream()
                 .filter(d -> d.getEquipmentTypeId() != null)
                 .collect(Collectors.groupingBy(EquipmentBase::getEquipmentTypeId, Collectors.counting()));

+ 114 - 41
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/GasDashboardServiceImpl.java

@@ -48,15 +48,13 @@ public class GasDashboardServiceImpl implements GasDashboardService {
     private PipeNetworkBaseMapper pipeNetworkBaseMapper;
 
     @Autowired
-    private com.zksy.base.pressure.mapper.FirefightingPressureMapper firefightingPressureMapper;
+    private com.zksy.base.service.PressureMonitorService pressureMonitorService;
 
     @Override
     public Map<String, Object> summary() {
         Map<String, Object> result = new LinkedHashMap<>();
 
-        // 1. 基础概况
-        //todo: 不管是管点和监测点都不好统计在线和离线(一个管点和监测点都能关联多个燃气设备)
-        // 因为状态是设备表里的
+        // 1. 管点口径(地图右侧「当前管点信息」用,保留原统计方式)
         List<GasPipePoint> allPoints = gasPipePointMapper.selectList(null);
         long totalPoints = allPoints.size();
         long onlinePoints = allPoints.stream()
@@ -64,14 +62,12 @@ public class GasDashboardServiceImpl implements GasDashboardService {
                 .count();
         long offlinePoints = totalPoints - onlinePoints;
 
-        // 2. 燃气设备 + 异常设备统计
-        List<String> gasTypeIds = equipmentTypeMapper.selectList(null).stream()
-                .filter(t -> GasEquipmentTypeEnum.parent_id.equals(t.getParentId()))
-                .map(type -> String.valueOf(type.getId()))
-                .collect(Collectors.toList());
+        // 2. 燃气设备:按设备类型树的根节点圈定,不只看一级子类型
+        List<EquipmentType> allTypes = equipmentTypeMapper.selectList(null);
+        Set<String> gasTypeIds = resolveTypeIdsUnderRoot(allTypes, GasEquipmentTypeEnum.parent_id);
         List<EquipmentBase> gasEquipments;
         if (gasTypeIds.isEmpty()) {
-            gasEquipments = equipmentBaseMapper.selectList(null);
+            gasEquipments = Collections.emptyList();
         } else {
             gasEquipments = equipmentBaseMapper.selectList(new LambdaQueryWrapper<EquipmentBase>()
                     .in(EquipmentBase::getEquipmentTypeId, gasTypeIds));
@@ -80,16 +76,21 @@ public class GasDashboardServiceImpl implements GasDashboardService {
                 .map(EquipmentBase::getEquipmentId)
                 .collect(Collectors.toList());
 
-        List<EquipmentStatus> abnormalList;
-        if (gasEquipmentIds.isEmpty()) {
-            abnormalList = Collections.emptyList();
-        } else {
-            abnormalList = equipmentStatusMapper.selectList(
-                    new LambdaQueryWrapper<EquipmentStatus>()
-                            .in(EquipmentStatus::getEquipmentId, gasEquipmentIds)
-                            .and(w -> w.eq(EquipmentStatus::getAlarmStatus, 1)
-                                    .or().eq(EquipmentStatus::getOnlineStatus, 0)));
-        }
+        // 每台设备只取最新一条状态记录,避免历史状态重复计入
+        Map<String, EquipmentStatus> latestStatus = latestStatusByEquipment(gasEquipmentIds);
+        long onlineEquipments = latestStatus.values().stream()
+                .filter(s -> s.getOnlineStatus() != null && s.getOnlineStatus() == 1)
+                .count();
+        long offlineEquipments = gasEquipments.size() - onlineEquipments;
+        // 在线率按燃气设备统计
+        double onlineRate = gasEquipments.isEmpty()
+                ? 0d
+                : Math.round(onlineEquipments * 1000.0 / gasEquipments.size()) / 10.0;
+
+        List<EquipmentStatus> abnormalList = latestStatus.values().stream()
+                .filter(s -> (s.getAlarmStatus() != null && s.getAlarmStatus() == 1)
+                        || (s.getOnlineStatus() != null && s.getOnlineStatus() == 0))
+                .collect(Collectors.toList());
         long alarmCount = abnormalList.size();
 
         Map<String, Object> baseStats = new LinkedHashMap<>();
@@ -97,6 +98,10 @@ public class GasDashboardServiceImpl implements GasDashboardService {
         baseStats.put("onlinePoints", onlinePoints);
         baseStats.put("offlinePoints", offlinePoints);
         baseStats.put("alarmCount", alarmCount);
+        baseStats.put("totalEquipments", gasEquipments.size());
+        baseStats.put("onlineEquipments", onlineEquipments);
+        baseStats.put("offlineEquipments", offlineEquipments);
+        baseStats.put("onlineRate", onlineRate);
         result.put("baseStats", baseStats);
 
         // ---- 管网统计 ----
@@ -121,7 +126,7 @@ public class GasDashboardServiceImpl implements GasDashboardService {
 
         // ---- 设备统计 ----
         // 门阀数 = 燃气设备中类型名称含"阀"的数量
-        Map<String, String> typeIdToName = equipmentTypeMapper.selectList(null).stream()
+        Map<String, String> typeIdToName = allTypes.stream()
                 .collect(Collectors.toMap(type -> String.valueOf(type.getId()), EquipmentType::getTypeName, (a, b) -> a));
         long valveCount = gasEquipments.stream()
                 .filter(e -> {
@@ -131,37 +136,60 @@ public class GasDashboardServiceImpl implements GasDashboardService {
                 .count();
         // 今日告警
         String todayStr = LocalDate.now().toString();
-        long todayAlarm = 0;
-        if (!gasEquipmentIds.isEmpty()) {
-            todayAlarm = equipmentStatusMapper.selectList(
-                    new LambdaQueryWrapper<EquipmentStatus>()
-                            .in(EquipmentStatus::getEquipmentId, gasEquipmentIds)
-                            .eq(EquipmentStatus::getAlarmStatus, 1)).stream()
-                    .filter(s -> s.getStatusUpdateTime() != null && s.getStatusUpdateTime().toString().startsWith(todayStr))
-                    .count();
-        }
+        long todayAlarm = abnormalList.stream()
+                .filter(s -> s.getAlarmStatus() != null && s.getAlarmStatus() == 1)
+                .filter(s -> s.getStatusUpdateTime() != null && s.getStatusUpdateTime().toString().startsWith(todayStr))
+                .count();
 
-        // 压力均值(从firefighting_pressure取最新一条)
-        double avgPressure = 0;
+        // 压力:每个遥测站点取最新一条采集值,再对最新值求均值(kPa → MPa)
+        List<Map<String, Object>> pressurePoints = new ArrayList<>();
+        Double avgPressureMpa = null;
+        String pressureUpdatedAt = null;
         try {
-            List<com.zksy.base.pressure.domain.FirefightingPressure> fpList =
-                    firefightingPressureMapper.selectList(null);
-            if (!fpList.isEmpty()) {
-                avgPressure = fpList.stream()
-                        .filter(p -> p.getPressureValue() != null)
-                        .mapToDouble(com.zksy.base.pressure.domain.FirefightingPressure::getPressureValue)
-                        .average().orElse(0);
+            List<Map<String, Object>> points = pressureMonitorService.getPoints();
+            if (points != null) {
+                pressurePoints = points;
+                double sum = 0;
+                int count = 0;
+                for (Map<String, Object> point : points) {
+                    Object latest = point.get("latestPressure");
+                    if (latest instanceof Number) {
+                        double value = ((Number) latest).doubleValue();
+                        if (value > 0) {
+                            sum += value;
+                            count++;
+                        }
+                    }
+                    Object time = point.get("latestTime");
+                    if (time != null && !String.valueOf(time).isEmpty()) {
+                        String text = String.valueOf(time);
+                        if (pressureUpdatedAt == null || text.compareTo(pressureUpdatedAt) > 0) {
+                            pressureUpdatedAt = text;
+                        }
+                    }
+                }
+                if (count > 0) {
+                    avgPressureMpa = Math.round(sum / count / 1000.0 * 100) / 100.0;
+                }
             }
-        } catch (Exception ignored) {}
-        double avgPressureMpa = Math.round(avgPressure / 1000.0 * 100) / 100.0; // kPa → MPa
+        } catch (Exception e) {
+            log.warn("统计燃气压力最新值失败", e);
+        }
 
         Map<String, Object> equipmentStats = new LinkedHashMap<>();
         equipmentStats.put("valveCount", valveCount);
         equipmentStats.put("totalEquipment", gasEquipments.size());
         equipmentStats.put("todayAlarm", todayAlarm);
         equipmentStats.put("avgPressure", avgPressureMpa);
+        equipmentStats.put("onlineEquipments", onlineEquipments);
+        equipmentStats.put("offlineEquipments", offlineEquipments);
+        equipmentStats.put("onlineRate", onlineRate);
         result.put("equipmentStats", equipmentStats);
 
+        // 地图压力点位(含最新压力值与阈值状态),供首页实时打击
+        result.put("pressurePoints", pressurePoints);
+        result.put("pressureUpdatedAt", pressureUpdatedAt);
+
         List<EquipmentBase> equipmentList = gasEquipments.stream()
                 .limit(10)
                 .collect(Collectors.toList());
@@ -265,6 +293,51 @@ public class GasDashboardServiceImpl implements GasDashboardService {
         return result;
     }
 
+    /**
+     * 取某个根类型下的全部类型主键(含根自身与多级子类型)
+     *
+     * 设备类型是多级树,只看一级子类型会漏掉挂在三级类型上的设备。
+     */
+    private Set<String> resolveTypeIdsUnderRoot(List<EquipmentType> allTypes, Integer rootId) {
+        Map<Integer, Integer> parentOf = new HashMap<>();
+        for (EquipmentType type : allTypes) {
+            parentOf.put(type.getId(), type.getParentId());
+        }
+        Set<String> result = new HashSet<>();
+        for (EquipmentType type : allTypes) {
+            Integer current = type.getId();
+            int guard = 0;
+            while (current != null && guard++ < 20) {
+                if (Objects.equals(current, rootId)) {
+                    result.add(String.valueOf(type.getId()));
+                    break;
+                }
+                current = parentOf.get(current);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * 按设备取最新一条状态记录
+     *
+     * 设备状态表可能保留多条历史记录,直接 selectList 会把历史状态重复计入统计。
+     */
+    private Map<String, EquipmentStatus> latestStatusByEquipment(List<String> equipmentIds) {
+        if (equipmentIds == null || equipmentIds.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        List<EquipmentStatus> list = equipmentStatusMapper.selectList(
+                new LambdaQueryWrapper<EquipmentStatus>()
+                        .in(EquipmentStatus::getEquipmentId, equipmentIds)
+                        .orderByDesc(EquipmentStatus::getStatusUpdateTime));
+        Map<String, EquipmentStatus> latest = new LinkedHashMap<>();
+        for (EquipmentStatus status : list) {
+            latest.putIfAbsent(status.getEquipmentId(), status);
+        }
+        return latest;
+    }
+
     private Map<String, Object> buildPointRow(int id, GasPipePoint point, GasPipePointDeviceRel relation) {
         Map<String, Object> row = new LinkedHashMap<>();
         row.put("id", id);

+ 15 - 9
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/ManholeLayerServiceImpl.java

@@ -15,9 +15,9 @@ import com.zksy.base.service.EquipmentBaseService;
 import com.zksy.base.service.EquipmentStatusService;
 import com.zksy.base.service.EquipmentTypeService;
 import com.zksy.base.service.ManholeLayerService;
+import com.zksy.base.util.ManholeDeviceTypes;
 import com.zksy.manhole.dto.in.ManholeDeviceListInDTO;
 import com.zksy.manhole.dto.out.EquipmentBaseOutDTO;
-import com.zksy.manhole.dto.out.ManholeLayerStatisticsOutDTO;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
@@ -52,8 +52,9 @@ public class ManholeLayerServiceImpl implements ManholeLayerService {
      */
     @Override
     public Map<String, Object> getManholeLayerData(ManholeDeviceListInDTO listInDTO) {
-        // 1. 仅查窨井类型设备:排水窨井(equipment_type_id=9)/供水窨井(equipment_type_id=10)
-        List<String> manholeTypeIds = Arrays.asList("5", "9", "10");
+        // 1. 仅查窨井类型设备:按设备类型表动态解析窨井大类(parent_id 上溯到根 4)下的全部子类型,
+        //    不再硬编码类型编码,避免新增窨井子类型后台账有数据而本页查不到
+        List<Integer> manholeTypeIds = ManholeDeviceTypes.resolveTypeIds(equipmentTypeService.list());
 
         // 2. 构建设备基础信息查询条件(不按状态预过滤,前端做前端过滤)
         LambdaQueryWrapper<EquipmentBase> queryWrapper = new LambdaQueryWrapper<>();
@@ -68,11 +69,19 @@ public class ManholeLayerServiceImpl implements ManholeLayerService {
 
         List<EquipmentBase> deviceList = equipmentBaseService.list(queryWrapper);
         if (CollUtil.isEmpty(deviceList)) {
+            // 无设备时统计口径全部置 0,避免前端拿到 null 展示空白
+            Map<String, Object> emptyStatistics = new HashMap<>();
+            emptyStatistics.put("total", 0);
+            emptyStatistics.put("inUse", 0);
+            emptyStatistics.put("maintenance", 0);
+            emptyStatistics.put("alarm", 0);
+            emptyStatistics.put("normal", 0);
+            emptyStatistics.put("online", 0);
+            emptyStatistics.put("offline", 0);
+
             Map<String, Object> result = new HashMap<>();
             result.put("devices", Collections.emptyList());
-            ManholeLayerStatisticsOutDTO stats = new ManholeLayerStatisticsOutDTO();
-            stats.setTotal(0);
-            result.put("statistics", stats);
+            result.put("statistics", emptyStatistics);
             return result;
         }
 
@@ -230,7 +239,4 @@ public class ManholeLayerServiceImpl implements ManholeLayerService {
         result.put("statistics", statisticsMap);
         return result;
     }
-
-
-
 }

+ 74 - 17
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/PipeNetworkBaseServiceImpl.java

@@ -10,12 +10,16 @@ import com.zksy.base.dto.PipeNetworkExportDTO;
 import com.zksy.base.dto.PipeNetworkPageInDTO;
 import com.zksy.base.mapper.PipeNetworkBaseMapper;
 import com.zksy.base.service.PipeNetworkBaseService;
+import com.zksy.common.core.domain.entity.SysDictData;
 import com.zksy.common.exception.ServiceException;
+import com.zksy.system.mapper.SysDictDataMapper;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -30,6 +34,9 @@ import java.util.stream.Collectors;
 public class PipeNetworkBaseServiceImpl extends ServiceImpl<PipeNetworkBaseMapper, PipeNetworkBase>
         implements PipeNetworkBaseService {
 
+    @Autowired
+    private SysDictDataMapper dictDataMapper;
+
     @Override
     public Page<PipeNetworkBase> queryByPage(PipeNetworkPageInDTO dto) {
         Page<PipeNetworkBase> page = new Page<>(dto.getPageNum(), dto.getPageSize());
@@ -175,16 +182,7 @@ public class PipeNetworkBaseServiceImpl extends ServiceImpl<PipeNetworkBaseMappe
     @Override
     public Page<PipeNetworkBase> queryGasByPage(PipeNetworkPageInDTO dto) {
         Page<PipeNetworkBase> page = new Page<>(dto.getPageNum(), dto.getPageSize());
-        LambdaQueryWrapper<PipeNetworkBase> wrapper = new LambdaQueryWrapper<>();
-        // 燃气模块只允许查询燃气设施
-        wrapper.eq(PipeNetworkBase::getNetworkType, PipeNetworkBase.NETWORK_TYPE_GAS);
-        wrapper.like(dto.getNetworkName() != null && !dto.getNetworkName().isEmpty(), PipeNetworkBase::getNetworkName, dto.getNetworkName());
-        wrapper.like(dto.getManagementUnit() != null && !dto.getManagementUnit().isEmpty(), PipeNetworkBase::getManagementUnit, dto.getManagementUnit());
-        wrapper.eq(dto.getNetworkLevel() != null && !dto.getNetworkLevel().isEmpty(), PipeNetworkBase::getNetworkLevel, dto.getNetworkLevel());
-        wrapper.like(dto.getMaterial() != null && !dto.getMaterial().isEmpty(), PipeNetworkBase::getMaterial, dto.getMaterial());
-        wrapper.eq(dto.getStatus() != null && !dto.getStatus().isEmpty(), PipeNetworkBase::getStatus, dto.getStatus());
-        wrapper.orderByDesc(PipeNetworkBase::getCreateTime);
-        return this.page(page, wrapper);
+        return this.page(page, buildGasWrapper(dto));
     }
 
     @Override
@@ -209,14 +207,37 @@ public class PipeNetworkBaseServiceImpl extends ServiceImpl<PipeNetworkBaseMappe
         return this.list(wrapper);
     }
 
-    @Override
-    public List<PipeNetworkExportDTO> exportGasData(String networkName, String status) {
+    /**
+     * 构建燃气管网查询条件
+     *
+     * 燃气模块只查询燃气设施,其余条件为空时忽略。
+     */
+    private LambdaQueryWrapper<PipeNetworkBase> buildGasWrapper(PipeNetworkPageInDTO dto) {
+        PipeNetworkPageInDTO query = dto == null ? new PipeNetworkPageInDTO() : dto;
         LambdaQueryWrapper<PipeNetworkBase> wrapper = new LambdaQueryWrapper<>();
         // 燃气模块只允许查询燃气设施
         wrapper.eq(PipeNetworkBase::getNetworkType, PipeNetworkBase.NETWORK_TYPE_GAS);
-        wrapper.like(networkName != null && !networkName.isEmpty(), PipeNetworkBase::getNetworkName, networkName);
-        wrapper.eq(status != null && !status.isEmpty(), PipeNetworkBase::getStatus, status);
+        wrapper.like(query.getNetworkName() != null && !query.getNetworkName().isEmpty(), PipeNetworkBase::getNetworkName, query.getNetworkName());
+        wrapper.like(query.getManagementUnit() != null && !query.getManagementUnit().isEmpty(), PipeNetworkBase::getManagementUnit, query.getManagementUnit());
+        wrapper.eq(query.getNetworkLevel() != null && !query.getNetworkLevel().isEmpty(), PipeNetworkBase::getNetworkLevel, query.getNetworkLevel());
+        wrapper.like(query.getMaterial() != null && !query.getMaterial().isEmpty(), PipeNetworkBase::getMaterial, query.getMaterial());
+        wrapper.eq(query.getStatus() != null && !query.getStatus().isEmpty(), PipeNetworkBase::getStatus, query.getStatus());
         wrapper.orderByDesc(PipeNetworkBase::getCreateTime);
+        return wrapper;
+    }
+
+    @Override
+    public List<PipeNetworkExportDTO> exportGasData(PipeNetworkPageInDTO dto, List<String> networkIds) {
+        LambdaQueryWrapper<PipeNetworkBase> wrapper;
+        if (networkIds != null && !networkIds.isEmpty()) {
+            // 勾选导出:以勾选的主键为准,仍限制在燃气范围内
+            wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(PipeNetworkBase::getNetworkType, PipeNetworkBase.NETWORK_TYPE_GAS);
+            wrapper.in(PipeNetworkBase::getNetworkId, networkIds);
+            wrapper.orderByDesc(PipeNetworkBase::getCreateTime);
+        } else {
+            wrapper = buildGasWrapper(dto);
+        }
         return buildExportList(this.list(wrapper));
     }
 
@@ -329,15 +350,20 @@ public class PipeNetworkBaseServiceImpl extends ServiceImpl<PipeNetworkBaseMappe
 
     /**
      * 管网设施列表转导出DTO
+     *
+     * 设施类型、等级、状态在库里存的是字典编码,导出统一转成字典标签;字典未配置该值时保留原编码。
      */
     private List<PipeNetworkExportDTO> buildExportList(List<PipeNetworkBase> list) {
+        Map<String, String> typeLabels = dictLabels("pipe_network_type");
+        Map<String, String> levelLabels = dictLabels("pipe_network_level");
+        Map<String, String> statusLabels = dictLabels("pipe_network_status");
         List<PipeNetworkExportDTO> result = new ArrayList<>(list.size());
         for (PipeNetworkBase entity : list) {
             PipeNetworkExportDTO dto = new PipeNetworkExportDTO();
             dto.setNetworkCode(entity.getNetworkCode());
             dto.setNetworkName(entity.getNetworkName());
-            dto.setNetworkTypeName(entity.getNetworkType());
-            dto.setNetworkLevelName(entity.getNetworkLevel());
+            dto.setNetworkTypeName(labelOf(typeLabels, entity.getNetworkType()));
+            dto.setNetworkLevelName(labelOf(levelLabels, entity.getNetworkLevel()));
             dto.setManagementUnit(entity.getManagementUnit());
             dto.setMaterial(entity.getMaterial());
             dto.setLocation(entity.getLocation());
@@ -347,7 +373,7 @@ public class PipeNetworkBaseServiceImpl extends ServiceImpl<PipeNetworkBaseMappe
             dto.setEndLatitude(entity.getEndLatitude());
             dto.setLength(entity.getLength());
             dto.setDiameter(entity.getDiameter());
-            dto.setStatusName(entity.getStatus());
+            dto.setStatusName(labelOf(statusLabels, entity.getStatus()));
             dto.setBuildDate(entity.getBuildDate());
             dto.setRemark(entity.getRemark());
             dto.setCreateTime(entity.getCreateTime());
@@ -355,4 +381,35 @@ public class PipeNetworkBaseServiceImpl extends ServiceImpl<PipeNetworkBaseMappe
         }
         return result;
     }
+
+    /**
+     * 查询字典的 值 -> 标签 映射
+     *
+     * @param dictType 字典类型
+     * @return 映射,字典无数据时返回空 Map
+     */
+    private Map<String, String> dictLabels(String dictType) {
+        List<SysDictData> dataList = dictDataMapper.selectDictDataByType(dictType);
+        if (CollectionUtils.isEmpty(dataList)) {
+            return Collections.emptyMap();
+        }
+        Map<String, String> labels = new HashMap<>(dataList.size());
+        for (SysDictData data : dataList) {
+            if (data.getDictValue() != null) {
+                labels.put(data.getDictValue(), data.getDictLabel());
+            }
+        }
+        return labels;
+    }
+
+    /**
+     * 取字典标签,未命中或标签为空时返回原值
+     */
+    private static String labelOf(Map<String, String> labels, String value) {
+        if (value == null || value.isEmpty()) {
+            return value;
+        }
+        String label = labels.get(value);
+        return label == null || label.isEmpty() ? value : label;
+    }
 }

+ 1 - 1
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/PressureMonitorServiceImpl.java

@@ -241,7 +241,7 @@ public class PressureMonitorServiceImpl implements PressureMonitorService {
     }
 
     private String derivePipelineName(String pointName, String level) {
-        String levelStr = "高压".equals(level) ? "高压段" : "中压".equals(level) ? "中压段" : "低压段";
+        String levelStr = "high".equals(level) ? "高压段" : "medium".equals(level) ? "中压段" : "低压段";
         return pointName.replace("监测点", "").replace("LNG储配站", "LNG段").replace("计量站", "计量段") + levelStr;
     }
 

+ 113 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/util/ManholeDeviceTypes.java

@@ -0,0 +1,113 @@
+package com.zksy.base.util;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import com.zksy.base.domain.EquipmentType;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 窨井设备类型解析工具
+ *
+ * 设备类型是多级树结构,窨井大类(根类型ID=4)下的子类型会随业务增加。
+ * 查询窨井设备统一按 parent_id 上溯到根动态取全部子类型,避免各处硬编码类型编码
+ * 导致台账有数据而页面查不到。
+ */
+public final class ManholeDeviceTypes {
+
+    /**
+     * 窨井设备大类根类型,对应 EquipmentTypeEnum.ROOT_MANHOLE_DEVICE
+     */
+    private static final int MANHOLE_ROOT_TYPE_ID = 4;
+
+    /**
+     * 历史遗留窨井类型编码,旧库曾用 5/9/10 表示窨井,保留以兼容存量数据
+     */
+    private static final List<Integer> LEGACY_MANHOLE_TYPE_IDS = Arrays.asList(5, 9, 10);
+
+    /**
+     * 历史遗留类型编码对应的名称,类型表有同名记录时以类型表为准
+     */
+    private static final Map<Integer, String> LEGACY_TYPE_NAMES = new LinkedHashMap<>();
+
+    static {
+        LEGACY_TYPE_NAMES.put(5, "供水窨井");
+        LEGACY_TYPE_NAMES.put(9, "排水窨井");
+        LEGACY_TYPE_NAMES.put(10, "燃气窨井");
+    }
+
+    private ManholeDeviceTypes() {
+    }
+
+    /**
+     * 解析窨井大类下的全部设备类型ID
+     *
+     * @param allTypes 设备类型全表数据
+     * @return 类型ID集合,含历史遗留编码
+     */
+    public static List<Integer> resolveTypeIds(List<EquipmentType> allTypes) {
+        Set<Integer> typeIds = new LinkedHashSet<>(LEGACY_MANHOLE_TYPE_IDS);
+        if (CollUtil.isEmpty(allTypes)) {
+            return new ArrayList<>(typeIds);
+        }
+        Map<Integer, Integer> parentMap = new HashMap<>();
+        for (EquipmentType type : allTypes) {
+            if (type.getId() != null) {
+                parentMap.put(type.getId(), type.getParentId() == null ? 0 : type.getParentId());
+            }
+        }
+        for (EquipmentType type : allTypes) {
+            if (type.getId() != null && MANHOLE_ROOT_TYPE_ID == resolveRootTypeId(parentMap, type.getId())) {
+                typeIds.add(type.getId());
+            }
+        }
+        return new ArrayList<>(typeIds);
+    }
+
+    /**
+     * 解析设备类型ID与名称的映射
+     *
+     * @param allTypes 设备类型全表数据
+     * @return 类型ID到名称的映射
+     */
+    public static Map<Integer, String> resolveTypeNames(List<EquipmentType> allTypes) {
+        Map<Integer, String> names = new LinkedHashMap<>(LEGACY_TYPE_NAMES);
+        if (CollUtil.isEmpty(allTypes)) {
+            return names;
+        }
+        for (EquipmentType type : allTypes) {
+            if (type.getId() != null && StrUtil.isNotBlank(type.getTypeName())) {
+                names.put(type.getId(), type.getTypeName());
+            }
+        }
+        return names;
+    }
+
+    /**
+     * 沿 parent_id 上溯到一级根节点,parent_id 缺失或成环时返回当前节点
+     *
+     * @param parentMap 类型ID到父类型ID的映射
+     * @param typeId    起始类型ID
+     * @return 一级根节点类型ID
+     */
+    private static int resolveRootTypeId(Map<Integer, Integer> parentMap, Integer typeId) {
+        int current = typeId;
+        Set<Integer> visited = new HashSet<>();
+        while (visited.add(current)) {
+            Integer parentId = parentMap.get(current);
+            if (parentId == null || parentId == 0) {
+                break;
+            }
+            current = parentId;
+        }
+        return current;
+    }
+}