Просмотр исходного кода

chore(repo): 更新忽略规则和服务运行日志
- 在 .gitignore 中排除测试方案、接口文档及任务规划文件
- 更新 pipe-network-service 启动日志,记录最新本地运行状态
- 添加 供水设施GIS一张图 接口
- 添加 管网数据统计分析 接口

Kazerin 3 недель назад
Родитель
Сommit
1b1da7b2fe
46 измененных файлов с 4139 добавлено и 1180 удалено
  1. 9 0
      .gitignore
  2. 7 0
      .mvn/settings.xml
  3. 2502 1162
      logs/pipe-network-service/spring.log
  4. 56 0
      pipe-network-service/sql/water_supply_alarm.sql
  5. 190 0
      pipe-network-service/sql/water_supply_alarm_test_data.sql
  6. 52 0
      pipe-network-service/sql/water_supply_facility_gis.sql
  7. 168 0
      pipe-network-service/sql/water_supply_facility_test_data.sql
  8. 1 1
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/WaterSupply/FlowMonitoring/FlowMonitoringController.java
  9. 1 1
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/WaterSupply/LeakageMonitoring/LeakageMonitoringController.java
  10. 78 0
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/WaterSupply/PipeNetworkStatistics/PipeNetworkStatisticsController.java
  11. 1 1
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/WaterSupply/PressureMonitoring/PressureMonitoringController.java
  12. 78 0
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/WaterSupply/WaterFacilityExportController.java
  13. 70 0
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/WaterSupply/WaterSupplyGisMap/WaterSupplyGisMapController.java
  14. 6 1
      pipe-network-service/zksy-system/pom.xml
  15. 12 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/PipeNetworkStatistics/dto/FacilityStatisticItemDTO.java
  16. 12 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/PipeNetworkStatistics/dto/MetricItemDTO.java
  17. 18 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/PipeNetworkStatistics/dto/PipeNetworkStatisticsDashboardDTO.java
  18. 30 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/PipeNetworkStatistics/mapper/PipeNetworkStatisticsMapper.java
  19. 10 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/PipeNetworkStatistics/service/PipeNetworkStatisticsExportService.java
  20. 9 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/PipeNetworkStatistics/service/PipeNetworkStatisticsService.java
  21. 86 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/PipeNetworkStatistics/service/impl/PipeNetworkStatisticsExportServiceImpl.java
  22. 97 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/PipeNetworkStatistics/service/impl/PipeNetworkStatisticsServiceImpl.java
  23. 13 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/WaterPipeInfo/domain/WaterPipeInfo.java
  24. 7 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/WaterPlantInfo/domain/WaterPlantInfo.java
  25. 7 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/WaterPumpStation/domain/WaterPumpStation.java
  26. 8 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/WaterSourceInfo/domain/WaterSourceInfo.java
  27. 12 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/WaterSupplyGisMap/dto/GisFeatureCollectionDTO.java
  28. 14 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/WaterSupplyGisMap/dto/GisFeatureDTO.java
  29. 12 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/WaterSupplyGisMap/dto/GisOverviewItemDTO.java
  30. 17 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/WaterSupplyGisMap/mapper/WaterSupplyGisMapMapper.java
  31. 15 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/WaterSupplyGisMap/service/WaterSupplyGisMapService.java
  32. 135 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/WaterSupplyGisMap/service/impl/WaterSupplyGisMapServiceImpl.java
  33. 7 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/WaterUserInfo/domain/WaterUserInfo.java
  34. 22 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/export/WaterPipeExportDTO.java
  35. 20 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/export/WaterPlantExportDTO.java
  36. 21 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/export/WaterPumpStationExportDTO.java
  37. 20 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/export/WaterSourceExportDTO.java
  38. 20 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/export/WaterUserExportDTO.java
  39. 112 0
      pipe-network-service/zksy-system/src/main/resources/mapper/WaterSupply/PipeNetworkStatistics/PipeNetworkStatisticsMapper.xml
  40. 6 1
      pipe-network-service/zksy-system/src/main/resources/mapper/WaterSupply/WaterPipeInfo/WaterPipeInfoMapper.xml
  41. 4 2
      pipe-network-service/zksy-system/src/main/resources/mapper/WaterSupply/WaterPlantInfo/WaterPlantInfoMapper.xml
  42. 4 2
      pipe-network-service/zksy-system/src/main/resources/mapper/WaterSupply/WaterPumpStation/WaterPumpStationMapper.xml
  43. 4 2
      pipe-network-service/zksy-system/src/main/resources/mapper/WaterSupply/WaterSourceInfo/WaterSourceInfoMapper.xml
  44. 45 0
      pipe-network-service/zksy-system/src/main/resources/mapper/WaterSupply/WaterSupplyGisMap/WaterSupplyGisMapMapper.xml
  45. 4 2
      pipe-network-service/zksy-system/src/main/resources/mapper/WaterSupply/WaterUserInfo/WaterUserInfoMapper.xml
  46. 117 0
      pipe-network-service/zksy-system/src/test/java/com/zksy/WaterSupply/FacilityCoordinateModelTest.java

+ 9 - 0
.gitignore

@@ -40,3 +40,12 @@ build/
 
 logs/
 
+/pipe-network-service/供水监测报警接口冒烟测试.ps1
+/pipe-network-service/供水监测报警模块测试方案.md
+/pipe-network-service/管网数据统计分析与供水设施GIS一张图接口说明.md
+/记忆库.md
+/task_plan.md
+/progress.md
+/findings.md
+/docs/superpowers/plans/2026-08-13-water-supply-statistics-gis.md
+/docs/superpowers/specs/2026-08-13-water-supply-statistics-gis-design.md

+ 7 - 0
.mvn/settings.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
+          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+          xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd">
+    <!-- Project-local Maven settings: bypasses malformed machine-global configuration. -->
+    <localRepository>C:\Users\ASUS\.m2\repository</localRepository>
+</settings>

Разница между файлами не показана из-за своего большого размера
+ 2502 - 1162
logs/pipe-network-service/spring.log


+ 56 - 0
pipe-network-service/sql/water_supply_alarm.sql

@@ -0,0 +1,56 @@
+-- ============================================================
+-- 供水管网安全运行监测子系统 - 监测报警模块 数据库脚本
+-- 数据库:PostgreSQL
+-- 说明:本项目运行库为 PostgreSQL(schema: app_user),
+--       若 warning_threshold 不在 app_user schema 下,请去掉前缀或调整 search_path。
+-- ============================================================
+
+-- ------------------------------------------------------------
+-- 1. 报警审核表(报警审核/解除 + 意见 + 附件 + 全流程跟踪)
+-- ------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS app_user.alarm_audit (
+	audit_id bigserial NOT NULL,
+	alarm_id varchar(64) NOT NULL,
+	-- 审核动作:1-审核 2-解除
+	audit_action int2 NOT NULL,
+	-- 审核结果:1-属实/审核通过 2-误报/解除报警
+	audit_result int2 NOT NULL,
+	-- 审核/解除意见
+	audit_opinion varchar(500) NULL,
+	-- 附件地址(多个用逗号分隔)
+	attach_url text NULL,
+	-- 操作人
+	audit_user_id int8 NULL,
+	audit_user_name varchar(50) NULL,
+	-- 操作时间
+	audit_time sys."timestamp" NULL,
+	create_time sys."timestamp" NULL,
+	update_time sys."timestamp" NULL,
+	CONSTRAINT alarm_audit_pkey PRIMARY KEY (audit_id)
+);
+
+CREATE INDEX IF NOT EXISTS idx_alarm_audit_alarm_id ON app_user.alarm_audit USING btree (alarm_id);
+CREATE INDEX IF NOT EXISTS idx_alarm_audit_time ON app_user.alarm_audit USING btree (audit_time);
+
+COMMENT ON TABLE app_user.alarm_audit IS '供水监测报警审核记录表';
+COMMENT ON COLUMN app_user.alarm_audit.audit_action IS '审核动作:1-审核 2-解除';
+COMMENT ON COLUMN app_user.alarm_audit.audit_result IS '审核结果:1-属实/审核通过 2-误报/解除报警';
+
+-- ------------------------------------------------------------
+-- 2. 预警阈值表补充分时阈值字段(分时阈值设置)
+--    period_start / period_end 格式 HH:mm,为空表示全天生效;
+--    同一设备+预警编码可配置多条不同时段阈值,时段不允许重叠。
+-- ------------------------------------------------------------
+ALTER TABLE warning_threshold ADD COLUMN IF NOT EXISTS period_start varchar(5) NULL;
+ALTER TABLE warning_threshold ADD COLUMN IF NOT EXISTS period_end varchar(5) NULL;
+
+COMMENT ON COLUMN warning_threshold.period_start IS '分时阈值生效开始时间(HH:mm),为空表示全天生效';
+COMMENT ON COLUMN warning_threshold.period_end IS '分时阈值生效结束时间(HH:mm),为空表示全天生效';
+
+-- ------------------------------------------------------------
+-- 3. 可选:菜单/权限初始化(按需执行)
+--    父菜单:供水管网安全运行监测子系统(若不存在则创建)
+-- ------------------------------------------------------------
+-- 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 '监测报警', menu_id, 10, 'monitorAlarm', 'waterSupply/monitorAlarm/index', '1', '0', 'C', '0', '0', 'waterSupply:alarm:list', 'monitor', 'admin', now(), '供水管网-监测报警'
+-- FROM sys_menu WHERE menu_name = '供水管网安全运行监测子系统' AND parent_id = 0 AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'waterSupply:alarm:list');

+ 190 - 0
pipe-network-service/sql/water_supply_alarm_test_data.sql

@@ -0,0 +1,190 @@
+-- ============================================================
+-- 供水管网-监测报警模块 测试数据脚本(人大金仓 KingbaseES / PostgreSQL 兼容)
+-- 说明:
+--   1. 测试数据使用 TEST- / test- 前缀,可安全重复执行(先删除再插入)。
+--   2. 若设备类别中已存在顶级类型"供水",则复用;否则自动创建测试顶级类型。
+--   3. 清理请执行本文件末尾的清理脚本,或单独执行 water_supply_alarm_test_cleanup.sql。
+-- ============================================================
+
+-- ------------------------------------------------------------
+-- 0. 清理历史测试数据(保证可重复执行)
+-- ------------------------------------------------------------
+DELETE FROM app_user.alarm_audit WHERE alarm_id LIKE 'test-alarm-%';
+DELETE FROM app_user.work_order_log WHERE order_id IN
+    (SELECT order_id FROM app_user.work_order WHERE alarm_id LIKE 'test-alarm-%' OR order_no = 'TEST-WO-001');
+DELETE FROM app_user.work_order WHERE alarm_id LIKE 'test-alarm-%' OR order_no = 'TEST-WO-001';
+DELETE FROM app_user.alarm_data WHERE id LIKE 'test-alarm-%';
+DELETE FROM app_user.warning_threshold WHERE id LIKE 'test-thr-%' OR device_code LIKE 'TEST-%';
+DELETE FROM app_user.equipment_base WHERE equipment_id LIKE 'TEST-EQ-%';
+DELETE FROM app_user.equipment_type WHERE id LIKE 'TEST-WATER-%';
+
+-- ------------------------------------------------------------
+-- 1. 设备类别与设备基础信息
+-- ------------------------------------------------------------
+DO $$
+DECLARE
+    v_top_id       varchar(64);
+    v_level_id     varchar(64);
+    v_pressure_id  varchar(64);
+    v_flow_id      varchar(64);
+BEGIN
+    -- 顶级类型"供水"(parent_type_id='0'),不存在则创建
+    SELECT id INTO v_top_id FROM app_user.equipment_type
+     WHERE type_name = '供水' AND parent_type_id = '0' LIMIT 1;
+    IF v_top_id IS NULL THEN
+        INSERT INTO app_user.equipment_type (id, type_id, type_name, parent_type_id, type_remark)
+        VALUES ('TEST-WATER-TOP', 'test_water_top', '供水', '0', '供水监测报警测试-顶级类型(可清理)')
+        RETURNING id INTO v_top_id;
+    END IF;
+
+    -- 子类型:水位 / 压力 / 流量
+    SELECT id INTO v_level_id FROM app_user.equipment_type WHERE type_id = 'test_water_level' LIMIT 1;
+    IF v_level_id IS NULL THEN
+        INSERT INTO app_user.equipment_type (id, type_id, type_name, parent_type_id, type_remark)
+        VALUES ('TEST-WATER-LEVEL', 'test_water_level', '水位监测(测试)', v_top_id, '供水监测报警测试子类型')
+        RETURNING id INTO v_level_id;
+    END IF;
+
+    SELECT id INTO v_pressure_id FROM app_user.equipment_type WHERE type_id = 'test_water_pressure' LIMIT 1;
+    IF v_pressure_id IS NULL THEN
+        INSERT INTO app_user.equipment_type (id, type_id, type_name, parent_type_id, type_remark)
+        VALUES ('TEST-WATER-PRESSURE', 'test_water_pressure', '压力监测(测试)', v_top_id, '供水监测报警测试子类型')
+        RETURNING id INTO v_pressure_id;
+    END IF;
+
+    SELECT id INTO v_flow_id FROM app_user.equipment_type WHERE type_id = 'test_water_flow' LIMIT 1;
+    IF v_flow_id IS NULL THEN
+        INSERT INTO app_user.equipment_type (id, type_id, type_name, parent_type_id, type_remark)
+        VALUES ('TEST-WATER-FLOW', 'test_water_flow', '流量监测(测试)', v_top_id, '供水监测报警测试子类型')
+        RETURNING id INTO v_flow_id;
+    END IF;
+
+    -- 4 台测试设备(均带经纬度,供一张图点位使用)
+    INSERT INTO app_user.equipment_base
+        (equipment_id, equipment_code, equipment_name, equipment_model, equipment_type_id,
+         longitude, latitude, equipment_location, maintainer, maintainer_phone, ownership_unit,
+         remark, create_time, update_time, del_flag, district)
+    VALUES
+        ('TEST-EQ-WL-001', 'TEST-WL-001', '测试水位监测点1', 'WL-TEST-1', v_level_id,
+         113.28000000, 23.12000000, '测试大道1号', '测试维护员', '13800000001', '测试供水公司',
+         '供水监测报警测试数据', now(), now(), '0', '测试区'),
+        ('TEST-EQ-WL-002', 'TEST-WL-002', '测试水位监测点2', 'WL-TEST-2', v_level_id,
+         113.29000000, 23.13000000, '测试大道2号', '测试维护员', '13800000002', '测试供水公司',
+         '供水监测报警测试数据', now(), now(), '0', '测试区'),
+        ('TEST-EQ-PR-001', 'TEST-PR-001', '测试压力监测点1', 'PR-TEST-1', v_pressure_id,
+         113.30000000, 23.14000000, '测试大道3号', '测试维护员', '13800000003', '测试供水公司',
+         '供水监测报警测试数据', now(), now(), '0', '测试区'),
+        ('TEST-EQ-FL-001', 'TEST-FL-001', '测试流量监测点1', 'FL-TEST-1', v_flow_id,
+         113.31000000, 23.15000000, '测试大道4号', '测试维护员', '13800000004', '测试供水公司',
+         '供水监测报警测试数据', now(), now(), '0', '测试区');
+END $$;
+
+-- ------------------------------------------------------------
+-- 2. 报警数据(alarm_data)
+--    预期统计:实时报警3个设备 / 今日报警6条 / 平均响应25.00分钟 / 设备异常率75%
+-- ------------------------------------------------------------
+INSERT INTO app_user.alarm_data
+    (id, device_code, device_name, device_type, warning_type, warning_code,
+     actual_value, min_value, max_value, alarm_status, alarm_level, deviation_ratio,
+     alarm_time, handle_time, handle_user, handle_remark, remark, create_time, update_time)
+VALUES
+    -- 未处理:水位1级(今日)
+    ('test-alarm-001', 'TEST-WL-001', '测试水位监测点1', '水位监测', '水位预警', 'WARN-WATER-LEVEL-TEST',
+     0.20, 0.50, 3.00, 0, 1, 40.00,
+     now() - interval '180 minutes', NULL, NULL, NULL, '供水监测报警测试数据',
+     now() - interval '180 minutes', now() - interval '180 minutes'),
+    -- 未处理:水位2级(今日,同设备第二条)
+    ('test-alarm-002', 'TEST-WL-001', '测试水位监测点1', '水位监测', '水位预警', 'WARN-WATER-LEVEL-TEST',
+     0.30, 0.50, 3.00, 0, 2, 35.00,
+     now() - interval '150 minutes', NULL, NULL, NULL, '供水监测报警测试数据',
+     now() - interval '150 minutes', now() - interval '150 minutes'),
+    -- 已处理:水位3级(今日,响应35分钟)→ 关联测试工单
+    ('test-alarm-003', 'TEST-WL-002', '测试水位监测点2', '水位监测', '水位预警', 'WARN-WATER-LEVEL-TEST',
+     3.80, 0.50, 3.00, 1, 3, 25.00,
+     now() - interval '120 minutes', now() - interval '85 minutes', '测试用户', '现场处理完成',
+     '供水监测报警测试数据', now() - interval '120 minutes', now() - interval '85 minutes'),
+    -- 未处理:压力1级(今日,一张图最高级点位)
+    ('test-alarm-004', 'TEST-PR-001', '测试压力监测点1', '压力监测', '压力预警', 'WARN-PRESSURE-TEST',
+     0.95, 0.40, 0.80, 0, 1, 30.00,
+     now() - interval '100 minutes', NULL, NULL, NULL, '供水监测报警测试数据',
+     now() - interval '100 minutes', now() - interval '100 minutes'),
+    -- 已处理:压力2级(今日,响应30分钟)
+    ('test-alarm-005', 'TEST-PR-001', '测试压力监测点1', '压力监测', '压力预警', 'WARN-PRESSURE-TEST',
+     0.90, 0.40, 0.80, 1, 2, 28.00,
+     now() - interval '90 minutes', now() - interval '60 minutes', '测试用户', '压力调整',
+     '供水监测报警测试数据', now() - interval '90 minutes', now() - interval '60 minutes'),
+    -- 已处理:流量4级(昨日,不计入今日统计)
+    ('test-alarm-006', 'TEST-FL-001', '测试流量监测点1', '流量监测', '流量预警', 'WARN-FLOW-TEST',
+     120.00, 10.00, 100.00, 1, 4, 20.00,
+     now() - interval '1 day' - interval '60 minutes', now() - interval '1 day',
+     '测试用户', '流量恢复正常', '供水监测报警测试数据',
+     now() - interval '1 day' - interval '60 minutes', now() - interval '1 day'),
+    -- 未处理:流量3级(昨日,用于实时列表/历史过滤/解除测试)
+    ('test-alarm-007', 'TEST-FL-001', '测试流量监测点1', '流量监测', '流量预警', 'WARN-FLOW-TEST',
+     115.00, 10.00, 100.00, 0, 3, 15.00,
+     now() - interval '1 day' - interval '30 minutes', NULL, NULL, NULL, '供水监测报警测试数据',
+     now() - interval '1 day' - interval '30 minutes', now() - interval '1 day' - interval '30 minutes'),
+    -- 已处理:水位2级(今日,响应10分钟)
+    ('test-alarm-008', 'TEST-WL-002', '测试水位监测点2', '水位监测', '水位预警', 'WARN-WATER-LEVEL-TEST',
+     3.50, 0.50, 3.00, 1, 2, 18.00,
+     now() - interval '300 minutes', now() - interval '290 minutes', '测试用户', '水位回落',
+     '供水监测报警测试数据', now() - interval '300 minutes', now() - interval '290 minutes');
+
+-- ------------------------------------------------------------
+-- 3. 阈值数据(warning_threshold,含分时阈值)
+-- ------------------------------------------------------------
+INSERT INTO app_user.warning_threshold
+    (id, device_code, warning_type, warning_code, min_value, max_value,
+     period_start, period_end, remark, create_time, update_time)
+VALUES
+    ('test-thr-001', 'TEST-WL-001', '水位预警', 'WARN-WATER-LEVEL-TEST', 0.50, 3.00,
+     NULL, NULL, '供水监测报警测试-全天阈值', now(), now()),
+    ('test-thr-002', 'TEST-WL-002', '水位预警', 'WARN-WATER-LEVEL-TEST', 0.60, 3.20,
+     NULL, NULL, '供水监测报警测试-全天阈值', now(), now()),
+    ('test-thr-003', 'TEST-PR-001', '压力预警', 'WARN-PRESSURE-TEST', 0.40, 0.80,
+     '08:00', '20:00', '供水监测报警测试-分时阈值(08:00-20:00)', now(), now()),
+    ('test-thr-004', 'TEST-FL-001', '流量预警', 'WARN-FLOW-TEST', 10.00, 100.00,
+     NULL, NULL, '供水监测报警测试-全天阈值', now(), now());
+
+-- ------------------------------------------------------------
+-- 4. 工单与工单日志(关联 test-alarm-003,用于全流程跟踪测试)
+-- ------------------------------------------------------------
+INSERT INTO app_user.work_order
+    (order_no, alarm_id, case_id, case_no, device_id, device_code,
+     order_type, order_level, order_desc, dept_id, dispatch_user, dispatch_time,
+     receive_user, order_status, delayed_reason, delayed_deadline, plan_finish_time,
+     real_finish_time, repair_result, verify_user, verify_time, create_time, update_time,
+     work_image, hazard_id, hazard_no)
+VALUES
+    ('TEST-WO-001', 'test-alarm-003', NULL, NULL, 'TEST-EQ-WL-002', 'TEST-WL-002',
+     1, 2, '供水监测报警测试工单-水位超限', NULL, NULL, NULL,
+     NULL, 5, NULL, NULL, NULL,
+     NULL, '现场已处理', NULL, NULL, now(), now(),
+     NULL, NULL, NULL);
+
+INSERT INTO app_user.work_order_log
+    (order_id, oper_type, oper_desc, oper_user_id, oper_user_name, oper_time)
+SELECT order_id, 5, '供水监测报警测试-提交验收', 1, '测试用户', now()
+  FROM app_user.work_order WHERE order_no = 'TEST-WO-001';
+
+-- ------------------------------------------------------------
+-- 5. 报警审核记录(关联 test-alarm-002,用于审核记录/全流程测试)
+-- ------------------------------------------------------------
+INSERT INTO app_user.alarm_audit
+    (alarm_id, audit_action, audit_result, audit_opinion, attach_url,
+     audit_user_id, audit_user_name, audit_time, create_time, update_time)
+VALUES
+    ('test-alarm-002', 1, 1, '测试审核:现场核实报警属实', NULL,
+     1, '测试用户', now(), now(), now());
+
+-- ============================================================
+-- 清理脚本(如需清理测试数据,执行以下语句)
+-- ============================================================
+-- DELETE FROM app_user.alarm_audit WHERE alarm_id LIKE 'test-alarm-%';
+-- DELETE FROM app_user.work_order_log WHERE order_id IN
+--     (SELECT order_id FROM app_user.work_order WHERE alarm_id LIKE 'test-alarm-%' OR order_no = 'TEST-WO-001');
+-- DELETE FROM app_user.work_order WHERE alarm_id LIKE 'test-alarm-%' OR order_no = 'TEST-WO-001';
+-- DELETE FROM app_user.alarm_data WHERE id LIKE 'test-alarm-%';
+-- DELETE FROM app_user.warning_threshold WHERE id LIKE 'test-thr-%' OR device_code LIKE 'TEST-%';
+-- DELETE FROM app_user.equipment_base WHERE equipment_id LIKE 'TEST-EQ-%';
+-- DELETE FROM app_user.equipment_type WHERE id LIKE 'TEST-WATER-%';

+ 52 - 0
pipe-network-service/sql/water_supply_facility_gis.sql

@@ -0,0 +1,52 @@
+-- 供水设施 GIS 一张图:WGS84 坐标字段增量脚本(KingbaseES / PostgreSQL 兼容)
+-- 可重复执行;仅增加可空字段、注释和索引,不修改或删除现有业务数据。
+
+ALTER TABLE app_user.water_source_info
+    ADD COLUMN IF NOT EXISTS longitude numeric(11,8),
+    ADD COLUMN IF NOT EXISTS latitude numeric(10,8);
+COMMENT ON COLUMN app_user.water_source_info.longitude IS 'WGS84经度';
+COMMENT ON COLUMN app_user.water_source_info.latitude IS 'WGS84纬度';
+CREATE INDEX IF NOT EXISTS idx_water_source_info_lon_lat ON app_user.water_source_info (longitude, latitude);
+
+ALTER TABLE app_user.water_plant_info
+    ADD COLUMN IF NOT EXISTS longitude numeric(11,8),
+    ADD COLUMN IF NOT EXISTS latitude numeric(10,8);
+COMMENT ON COLUMN app_user.water_plant_info.longitude IS 'WGS84经度';
+COMMENT ON COLUMN app_user.water_plant_info.latitude IS 'WGS84纬度';
+CREATE INDEX IF NOT EXISTS idx_water_plant_info_lon_lat ON app_user.water_plant_info (longitude, latitude);
+
+ALTER TABLE app_user.water_pump_station
+    ADD COLUMN IF NOT EXISTS longitude numeric(11,8),
+    ADD COLUMN IF NOT EXISTS latitude numeric(10,8);
+COMMENT ON COLUMN app_user.water_pump_station.longitude IS 'WGS84经度';
+COMMENT ON COLUMN app_user.water_pump_station.latitude IS 'WGS84纬度';
+CREATE INDEX IF NOT EXISTS idx_water_pump_station_lon_lat ON app_user.water_pump_station (longitude, latitude);
+
+ALTER TABLE app_user.water_user_info
+    ADD COLUMN IF NOT EXISTS longitude numeric(11,8),
+    ADD COLUMN IF NOT EXISTS latitude numeric(10,8);
+COMMENT ON COLUMN app_user.water_user_info.longitude IS 'WGS84经度';
+COMMENT ON COLUMN app_user.water_user_info.latitude IS 'WGS84纬度';
+CREATE INDEX IF NOT EXISTS idx_water_user_info_lon_lat ON app_user.water_user_info (longitude, latitude);
+
+ALTER TABLE app_user.water_pipe_info
+    ADD COLUMN IF NOT EXISTS start_longitude numeric(11,8),
+    ADD COLUMN IF NOT EXISTS start_latitude numeric(10,8),
+    ADD COLUMN IF NOT EXISTS end_longitude numeric(11,8),
+    ADD COLUMN IF NOT EXISTS end_latitude numeric(10,8);
+COMMENT ON COLUMN app_user.water_pipe_info.start_longitude IS '管网起点WGS84经度';
+COMMENT ON COLUMN app_user.water_pipe_info.start_latitude IS '管网起点WGS84纬度';
+COMMENT ON COLUMN app_user.water_pipe_info.end_longitude IS '管网终点WGS84经度';
+COMMENT ON COLUMN app_user.water_pipe_info.end_latitude IS '管网终点WGS84纬度';
+CREATE INDEX IF NOT EXISTS idx_water_pipe_info_start_end_lon_lat
+    ON app_user.water_pipe_info (start_longitude, start_latitude, end_longitude, end_latitude);
+
+-- 以下菜单/按钮权限 SQL 需在目标库核对供水模块父菜单后手动启用:
+-- waterSupply:pipeNetwork:statistics
+-- waterSupply:pipeNetwork:statistics:export
+-- waterSupply:gis:map
+-- waterSupply:waterSource:export
+-- waterSupply:waterPlant:export
+-- waterSupply:waterPumpStation:export
+-- waterSupply:waterPipe:export
+-- waterSupply:waterUser:export

+ 168 - 0
pipe-network-service/sql/water_supply_facility_test_data.sql

@@ -0,0 +1,168 @@
+-- ============================================================
+-- 供水管网统计分析 / GIS 一张图接口测试数据
+-- KingbaseES / PostgreSQL 兼容;目标 schema:app_user
+--
+-- 用法:
+--   1. 先执行 water_supply_facility_gis.sql,确保坐标列已存在。
+--   2. 执行本脚本。本脚本按 GIS-TEST- 前缀清理后重建,可重复执行。
+--   3. 通过末尾 SELECT 获取详情接口所需的实际主键 ID。
+--   4. 测试完成后执行文件末尾注释中的清理 SQL。
+--
+-- 设计覆盖:
+--   - 五类设施的 Point / LineString GeoJSON;
+--   - 完整坐标与缺失坐标过滤;
+--   - 正常、异常、空状态;
+--   - 管径、铺设年代(含未填报)、材质、压力等级和用水量分组。
+-- ============================================================
+
+BEGIN;
+
+-- ------------------------------------------------------------
+-- 0. 清理本脚本历史数据
+-- ------------------------------------------------------------
+DELETE FROM app_user.water_source_info
+ WHERE source_id LIKE 'GIS-TEST-%';
+DELETE FROM app_user.water_plant_info
+ WHERE plant_code LIKE 'GIS-TEST-%';
+DELETE FROM app_user.water_pump_station
+ WHERE station_code LIKE 'GIS-TEST-%';
+DELETE FROM app_user.water_pipe_info
+ WHERE pipe_code LIKE 'GIS-TEST-%';
+DELETE FROM app_user.water_user_info
+ WHERE user_code LIKE 'GIS-TEST-%';
+
+-- ------------------------------------------------------------
+-- 1. 水源地:2 个坐标完整 + 1 个缺失坐标
+-- ------------------------------------------------------------
+INSERT INTO app_user.water_source_info
+    (source_id, source_name, source_type, unit, address, water_type,
+     protection_level, water_quality_level, protection_scope, report_time,
+     status, remark, del_flag, create_by, create_time, longitude, latitude)
+VALUES
+    ('GIS-TEST-S-001', 'GIS测试水源地-东湖', '地表水', '东湖水务', '测试东湖路1号', '湖泊',
+     '一级', 'II类', '半径500米', DATE '2025-01-15',
+     '0', 'GIS接口测试-坐标完整-正常', '0', 'test', CURRENT_TIMESTAMP, 113.28000000, 23.12000000),
+    ('GIS-TEST-S-002', 'GIS测试水源地-北河', '河流水源', '北河水务', '测试北河路2号', '河流',
+     '二级', 'III类', '半径300米', DATE '2025-02-15',
+     '1', 'GIS接口测试-坐标完整-异常', '0', 'test', CURRENT_TIMESTAMP, 113.29000000, 23.13000000),
+    ('GIS-TEST-S-003', 'GIS测试水源地-待测', '地下水', '测试水务', '测试待测路3号', '地下水',
+     '三级', 'IV类', '待补录', DATE '2025-03-15',
+     NULL, 'GIS接口测试-缺失坐标-不应进入图层', '0', 'test', CURRENT_TIMESTAMP, NULL, NULL);
+
+-- ------------------------------------------------------------
+-- 2. 水厂:1 个坐标完整 + 1 个缺失坐标
+-- ------------------------------------------------------------
+INSERT INTO app_user.water_plant_info
+    (plant_code, plant_name, "location", water_source_type,
+     design_capacity, current_capacity, treatment_process,
+     commission_date, water_quality_standard, status, principal,
+     contact_phone, remark, del_flag, create_by, create_time, longitude, latitude)
+VALUES
+    ('GIS-TEST-P-001', 'GIS测试水厂-一厂', '测试工业园88号', '地表水',
+     80000.00, 60000.00, '常规处理', DATE '2018-06-01', 'GB5749-2022', '0',
+     '测试厂长', '13800000001', 'GIS接口测试-坐标完整-正常', '0', 'test', CURRENT_TIMESTAMP,
+     113.30000000, 23.14000000),
+    ('GIS-TEST-P-002', 'GIS测试水厂-二厂', '测试开发区99号', '地下水',
+     50000.00, 45000.00, '深度处理', DATE '2020-08-01', 'GB5749-2022', '1',
+     '测试厂长2', '13800000002', 'GIS接口测试-缺失坐标-概览计数', '0', 'test', CURRENT_TIMESTAMP,
+     NULL, NULL);
+
+-- ------------------------------------------------------------
+-- 3. 泵站:1 个坐标完整 + 1 个缺失坐标
+-- ------------------------------------------------------------
+INSERT INTO app_user.water_pump_station
+    (station_code, station_name, "location", pump_count, design_flow,
+     design_head, power_capacity, commission_date, status, principal,
+     contact_phone, remark, del_flag, create_by, create_time, longitude, latitude)
+VALUES
+    ('GIS-TEST-PS-001', 'GIS测试泵站-东区', '测试东区泵站路10号', 4, 1200.00,
+     35.00, 450.00, DATE '2019-04-01', '0', '测试站长', '13800000003',
+     'GIS接口测试-坐标完整-正常', '0', 'test', CURRENT_TIMESTAMP, 113.31000000, 23.15000000),
+    ('GIS-TEST-PS-002', 'GIS测试泵站-西区', '测试西区泵站路20号', 2, 600.00,
+     28.00, 180.00, DATE '2021-09-01', NULL, '测试站长2', '13800000004',
+     'GIS接口测试-缺失坐标-概览计数', '0', 'test', CURRENT_TIMESTAMP, NULL, NULL);
+
+-- ------------------------------------------------------------
+-- 4. 管网:3 条坐标完整 + 1 条缺失坐标
+--    其中包含 laying_year NULL,验证“未填报”分组。
+-- ------------------------------------------------------------
+INSERT INTO app_user.water_pipe_info
+    (pipe_code, pipe_name, pipe_material, pipe_diameter, pipe_length,
+     start_point, end_point, laying_year, pressure_rating, status,
+     remark, del_flag, create_by, create_time,
+     start_longitude, start_latitude, end_longitude, end_latitude)
+VALUES
+    ('GIS-TEST-PIPE-001', 'GIS测试管网-东湖主干线', '球墨铸铁', 800, 12500.50,
+     '东湖水源地', '一厂', 2012, 1.60, '0',
+     'GIS接口测试-完整LineString-大管径', '0', 'test', CURRENT_TIMESTAMP,
+     113.28000000, 23.12000000, 113.30000000, 23.14000000),
+    ('GIS-TEST-PIPE-002', 'GIS测试管网-北河支线', '钢管', 250, 6800.00,
+     '北河水源地', '东区泵站', 2021, 1.00, '1',
+     'GIS接口测试-完整LineString-异常', '0', 'test', CURRENT_TIMESTAMP,
+     113.29000000, 23.13000000, 113.31000000, 23.15000000),
+    ('GIS-TEST-PIPE-003', 'GIS测试管网-待补年代线', NULL, 80, 900.00,
+     '东区泵站', '测试用户1', NULL, NULL, NULL,
+     'GIS接口测试-完整LineString-空分组字段', '0', 'test', CURRENT_TIMESTAMP,
+     113.31000000, 23.15000000, 113.32000000, 23.16000000),
+    ('GIS-TEST-PIPE-004', 'GIS测试管网-缺失坐标线', 'PVC', 300, 1500.00,
+     '测试用户1', '测试用户2', 2018, 0.80, '0',
+     'GIS接口测试-缺失终点坐标-不应进入图层', '0', 'test', CURRENT_TIMESTAMP,
+     113.32000000, 23.16000000, NULL, NULL);
+
+-- ------------------------------------------------------------
+-- 5. 大用水户:2 个坐标完整 + 1 个缺失坐标
+-- ------------------------------------------------------------
+INSERT INTO app_user.water_user_info
+    (user_code, user_name, user_type, address, contact_person,
+     contact_phone, meter_number, initial_reading, current_reading,
+     annual_water_consumption, status, remark, del_flag, create_by,
+     create_time, longitude, latitude)
+VALUES
+    ('GIS-TEST-U-001', 'GIS测试用水户-工业园', '工业', '测试工业园1号', '测试联系人1',
+     '13800000005', 'GIS-METER-001', 1000.00, 52000.00, 51000.00, '0',
+     'GIS接口测试-坐标完整-大用水量', '0', 'test', CURRENT_TIMESTAMP, 113.32000000, 23.16000000),
+    ('GIS-TEST-U-002', 'GIS测试用水户-医院', '公共机构', '测试医院2号', '测试联系人2',
+     '13800000006', 'GIS-METER-002', 500.00, 12500.00, 12000.00, '1',
+     'GIS接口测试-坐标完整-异常', '0', 'test', CURRENT_TIMESTAMP, 113.33000000, 23.17000000),
+    ('GIS-TEST-U-003', 'GIS测试用水户-待测', '商业', '测试商业街3号', '测试联系人3',
+     '13800000007', 'GIS-METER-003', 100.00, 800.00, NULL, NULL,
+     'GIS接口测试-缺失坐标-概览计数', '0', 'test', CURRENT_TIMESTAMP, NULL, NULL);
+
+COMMIT;
+
+-- ------------------------------------------------------------
+-- 6. 验证输出
+-- ------------------------------------------------------------
+-- 预期:source=3/完整2,plant=2/完整1,pumpStation=2/完整1,pipe=4/完整3,user=3/完整2。
+SELECT 'source' facility_type, COUNT(*) total_count,
+       COALESCE(SUM(CASE WHEN longitude IS NOT NULL AND latitude IS NOT NULL THEN 1 ELSE 0 END), 0) geo_complete_count
+  FROM app_user.water_source_info WHERE source_id LIKE 'GIS-TEST-%'
+UNION ALL
+SELECT 'plant', COUNT(*), COALESCE(SUM(CASE WHEN longitude IS NOT NULL AND latitude IS NOT NULL THEN 1 ELSE 0 END), 0)
+  FROM app_user.water_plant_info WHERE plant_code LIKE 'GIS-TEST-%'
+UNION ALL
+SELECT 'pumpStation', COUNT(*), COALESCE(SUM(CASE WHEN longitude IS NOT NULL AND latitude IS NOT NULL THEN 1 ELSE 0 END), 0)
+  FROM app_user.water_pump_station WHERE station_code LIKE 'GIS-TEST-%'
+UNION ALL
+SELECT 'pipe', COUNT(*), COALESCE(SUM(CASE WHEN start_longitude IS NOT NULL AND start_latitude IS NOT NULL
+                                             AND end_longitude IS NOT NULL AND end_latitude IS NOT NULL THEN 1 ELSE 0 END), 0)
+  FROM app_user.water_pipe_info WHERE pipe_code LIKE 'GIS-TEST-%'
+UNION ALL
+SELECT 'user', COUNT(*), COALESCE(SUM(CASE WHEN longitude IS NOT NULL AND latitude IS NOT NULL THEN 1 ELSE 0 END), 0)
+  FROM app_user.water_user_info WHERE user_code LIKE 'GIS-TEST-%';
+
+-- 详情接口 ID 查询:将结果中的 id 填入 /waterSupply/gis/map/detail/{facilityType}/{id}。
+SELECT 'source' facility_type, id, source_id code FROM app_user.water_source_info WHERE source_id LIKE 'GIS-TEST-%' ORDER BY source_id;
+SELECT 'plant' facility_type, id, plant_code code FROM app_user.water_plant_info WHERE plant_code LIKE 'GIS-TEST-%' ORDER BY plant_code;
+SELECT 'pumpStation' facility_type, id, station_code code FROM app_user.water_pump_station WHERE station_code LIKE 'GIS-TEST-%' ORDER BY station_code;
+SELECT 'pipe' facility_type, id, pipe_code code FROM app_user.water_pipe_info WHERE pipe_code LIKE 'GIS-TEST-%' ORDER BY pipe_code;
+SELECT 'user' facility_type, id, user_code code FROM app_user.water_user_info WHERE user_code LIKE 'GIS-TEST-%' ORDER BY user_code;
+
+-- ============================================================
+-- 清理脚本(测试完成后手动执行)
+-- ============================================================
+-- DELETE FROM app_user.water_source_info WHERE source_id LIKE 'GIS-TEST-%';
+-- DELETE FROM app_user.water_plant_info WHERE plant_code LIKE 'GIS-TEST-%';
+-- DELETE FROM app_user.water_pump_station WHERE station_code LIKE 'GIS-TEST-%';
+-- DELETE FROM app_user.water_pipe_info WHERE pipe_code LIKE 'GIS-TEST-%';
+-- DELETE FROM app_user.water_user_info WHERE user_code LIKE 'GIS-TEST-%';

+ 1 - 1
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/WaterSupply/FlowMonitoring/FlowMonitoringController.java

@@ -18,7 +18,7 @@ import java.util.List;
 import java.util.Map;
 
 @Slf4j
-@Api(tags = "运行监测-流量监测")
+@Api(tags = "供水管网运行监测-流量监测")
 @RestController
 @RequestMapping("/waterSupply/monitoring/flow")
 public class FlowMonitoringController {

+ 1 - 1
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/WaterSupply/LeakageMonitoring/LeakageMonitoringController.java

@@ -18,7 +18,7 @@ import java.util.List;
 import java.util.Map;
 
 @Slf4j
-@Api(tags = "运行监测-漏失监测")
+@Api(tags = "供水管网运行监测-漏失监测")
 @RestController
 @RequestMapping("/waterSupply/monitoring/leakage")
 public class LeakageMonitoringController {

+ 78 - 0
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/WaterSupply/PipeNetworkStatistics/PipeNetworkStatisticsController.java

@@ -0,0 +1,78 @@
+package com.zksy.web.controller.WaterSupply.PipeNetworkStatistics;
+
+import com.zksy.WaterSupply.PipeNetworkStatistics.dto.PipeNetworkStatisticsDashboardDTO;
+import com.zksy.WaterSupply.PipeNetworkStatistics.service.PipeNetworkStatisticsExportService;
+import com.zksy.WaterSupply.PipeNetworkStatistics.service.PipeNetworkStatisticsService;
+import com.zksy.common.annotation.Log;
+import com.zksy.common.core.domain.AjaxResult;
+import com.zksy.common.enums.BusinessType;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@RestController
+@RequestMapping("/waterSupply/pipeNetwork/statistics")
+@Api(tags = "供水管网-管网数据统计分析")
+public class PipeNetworkStatisticsController {
+    @Resource private PipeNetworkStatisticsService statisticsService;
+    @Resource private PipeNetworkStatisticsExportService exportService;
+
+    @GetMapping("/dashboard")
+    @ApiOperation(value = "供水管网-管网数据统计分析-综合统计仪表盘",
+            notes = "请求参数为可选查询条件。响应 data 包含 facilityOverview、pipeNetwork、waterPlant、pumpStation、waterSource、waterUser 六个区块;分组项结构为 {name, value, unit}。")
+    @PreAuthorize("@ss.hasPermi('waterSupply:pipeNetwork:statistics')")
+    public AjaxResult dashboard(
+            @ApiParam(value = "设施状态:0正常、1/2异常;不传查询全部", example = "0")
+            @RequestParam(required = false) String status,
+            @ApiParam(value = "设施类型,逗号分隔:source、plant、pumpStation、pipe、user;不传查询全部", example = "plant,pipe")
+            @RequestParam(required = false) String facilityTypes) {
+        try {
+            validateStatus(status);
+            return AjaxResult.success(statisticsService.getDashboard(status, parseTypes(facilityTypes)));
+        } catch (IllegalArgumentException e) {
+            return AjaxResult.error(e.getMessage());
+        }
+    }
+
+    @GetMapping("/export")
+    @ApiOperation(value = "供水管网-管网数据统计分析-导出统计 Excel",
+            notes = "请求参数与综合统计仪表盘一致。响应为文件流,文件名为管网数据统计分析.xlsx,包含综合概览、管网统计、水厂统计、泵站统计、水源地统计、大用水户统计六个工作表。")
+    @Log(title = "管网数据统计分析", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('waterSupply:pipeNetwork:statistics:export')")
+    public void export(HttpServletResponse response,
+                       @ApiParam(value = "设施状态:0正常、1/2异常;不传查询全部", example = "0")
+                       @RequestParam(required = false) String status,
+                       @ApiParam(value = "设施类型,逗号分隔:source、plant、pumpStation、pipe、user;不传查询全部", example = "source,user")
+                       @RequestParam(required = false) String facilityTypes) throws IOException {
+        validateStatus(status);
+        PipeNetworkStatisticsDashboardDTO dashboard = statisticsService.getDashboard(status, parseTypes(facilityTypes));
+        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+        response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode("管网数据统计分析.xlsx", StandardCharsets.UTF_8.name()));
+        exportService.write(response.getOutputStream(), dashboard);
+    }
+
+    private Set<String> parseTypes(String text) {
+        if (text == null || text.trim().isEmpty()) return Collections.emptySet();
+        return Arrays.stream(text.split(",")).map(String::trim).filter(s -> !s.isEmpty()).collect(Collectors.toSet());
+    }
+
+    private void validateStatus(String status) {
+        if (status != null && !status.isEmpty() && !"0".equals(status) && !"1".equals(status) && !"2".equals(status)) throw new IllegalArgumentException("无效的设施状态:" + status);
+    }
+}

+ 1 - 1
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/WaterSupply/PressureMonitoring/PressureMonitoringController.java

@@ -18,7 +18,7 @@ import java.util.List;
 import java.util.Map;
 
 @Slf4j
-@Api(tags = "运行监测-压力监测")
+@Api(tags = "供水管网运行监测-压力监测")
 @RestController
 @RequestMapping("/waterSupply/monitoring/pressure")
 public class PressureMonitoringController {

+ 78 - 0
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/WaterSupply/WaterFacilityExportController.java

@@ -0,0 +1,78 @@
+package com.zksy.web.controller.WaterSupply;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zksy.WaterSupply.WaterPipeInfo.domain.WaterPipeInfo;
+import com.zksy.WaterSupply.WaterPipeInfo.service.IWaterPipeInfoService;
+import com.zksy.WaterSupply.WaterPlantInfo.domain.WaterPlantInfo;
+import com.zksy.WaterSupply.WaterPlantInfo.service.IWaterPlantInfoService;
+import com.zksy.WaterSupply.WaterPumpStation.domain.WaterPumpStation;
+import com.zksy.WaterSupply.WaterPumpStation.service.IWaterPumpStationService;
+import com.zksy.WaterSupply.WaterSourceInfo.domain.WaterSourceInfo;
+import com.zksy.WaterSupply.WaterSourceInfo.service.IWaterSourceInfoService;
+import com.zksy.WaterSupply.WaterUserInfo.domain.WaterUserInfo;
+import com.zksy.WaterSupply.WaterUserInfo.service.IWaterUserInfoService;
+import com.zksy.WaterSupply.export.*;
+import com.zksy.common.annotation.Log;
+import com.zksy.common.annotation.Excel;
+import com.zksy.common.enums.BusinessType;
+import com.zksy.common.utils.poi.ExcelUtil;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import java.time.LocalDateTime;
+import java.util.stream.Collectors;
+
+@RestController
+@RequestMapping("/api/water")
+public class WaterFacilityExportController {
+    @Resource private IWaterSourceInfoService sourceService;
+    @Resource private IWaterPlantInfoService plantService;
+    @Resource private IWaterPumpStationService pumpService;
+    @Resource private IWaterPipeInfoService pipeService;
+    @Resource private IWaterUserInfoService userService;
+
+    @GetMapping("/sourceInfo/export")
+    @Log(title = "导出水源地信息", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('waterSupply:waterSource:export')")
+    public void sourceExport(HttpServletResponse response, @RequestParam(required = false) String sourceId, @RequestParam(required = false) String sourceName, @RequestParam(required = false) String status, @RequestParam(required = false) String unit, @RequestParam(required = false) LocalDateTime startTime, @RequestParam(required = false) LocalDateTime endTime) {
+        var page = sourceService.queryPage(new Page<WaterSourceInfo>(1, Integer.MAX_VALUE), sourceId, sourceName, status, unit, startTime, endTime);
+        new ExcelUtil<>(WaterSourceExportDTO.class).exportExcel(response, page.getRecords().stream().map(WaterSourceExportDTO::from).collect(Collectors.toList()), "水源地信息");
+    }
+
+    @GetMapping("/plantInfo/export")
+    @Log(title = "导出水厂信息", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('waterSupply:waterPlant:export')")
+    public void plantExport(HttpServletResponse response, @RequestParam(required = false) String plantCode, @RequestParam(required = false) String plantName, @RequestParam(required = false) String status, @RequestParam(required = false) String principal, @RequestParam(required = false) LocalDateTime startTime, @RequestParam(required = false) LocalDateTime endTime) {
+        var page = plantService.queryPage(new Page<WaterPlantInfo>(1, Integer.MAX_VALUE), plantCode, plantName, status, principal, startTime, endTime);
+        new ExcelUtil<>(WaterPlantExportDTO.class).exportExcel(response, page.getRecords().stream().map(WaterPlantExportDTO::from).collect(Collectors.toList()), "水厂信息");
+    }
+
+    @GetMapping("/pumpStation/export")
+    @Log(title = "导出泵站信息", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('waterSupply:waterPumpStation:export')")
+    public void pumpExport(HttpServletResponse response, @RequestParam(required = false) String stationCode, @RequestParam(required = false) String stationName, @RequestParam(required = false) String status, @RequestParam(required = false) String principal, @RequestParam(required = false) LocalDateTime startTime, @RequestParam(required = false) LocalDateTime endTime) {
+        var page = pumpService.queryPage(new Page<WaterPumpStation>(1, Integer.MAX_VALUE), stationCode, stationName, status, principal, startTime, endTime);
+        new ExcelUtil<>(WaterPumpStationExportDTO.class).exportExcel(response, page.getRecords().stream().map(WaterPumpStationExportDTO::from).collect(Collectors.toList()), "泵站信息");
+    }
+
+    @GetMapping("/pipe/export")
+    @Log(title = "导出管网信息", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('waterSupply:waterPipe:export')")
+    public void pipeExport(HttpServletResponse response, @RequestParam(required = false) String pipeCode, @RequestParam(required = false) String pipeName, @RequestParam(required = false) String pipeMaterial, @RequestParam(required = false) String status, @RequestParam(required = false) LocalDateTime startTime, @RequestParam(required = false) LocalDateTime endTime) {
+        var page = pipeService.queryPage(new Page<WaterPipeInfo>(1, Integer.MAX_VALUE), pipeCode, pipeName, pipeMaterial, status, startTime, endTime);
+        new ExcelUtil<>(WaterPipeExportDTO.class).exportExcel(response, page.getRecords().stream().map(WaterPipeExportDTO::from).collect(Collectors.toList()), "管网信息");
+    }
+
+    @GetMapping("/userInfo/export")
+    @Log(title = "导出用水户信息", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('waterSupply:waterUser:export')")
+    public void userExport(HttpServletResponse response, @RequestParam(required = false) String userCode, @RequestParam(required = false) String userName, @RequestParam(required = false) String status, @RequestParam(required = false) String userType, @RequestParam(required = false) LocalDateTime startTime, @RequestParam(required = false) LocalDateTime endTime) {
+        var page = userService.queryPage(new Page<WaterUserInfo>(1, Integer.MAX_VALUE), userCode, userName, status, userType, startTime, endTime);
+        new ExcelUtil<>(WaterUserExportDTO.class).exportExcel(response, page.getRecords().stream().map(WaterUserExportDTO::from).collect(Collectors.toList()), "用水户信息");
+    }
+}

+ 70 - 0
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/WaterSupply/WaterSupplyGisMap/WaterSupplyGisMapController.java

@@ -0,0 +1,70 @@
+package com.zksy.web.controller.WaterSupply.WaterSupplyGisMap;
+
+import com.zksy.WaterSupply.WaterSupplyGisMap.service.WaterSupplyGisMapService;
+import com.zksy.common.core.domain.AjaxResult;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@RestController
+@RequestMapping("/waterSupply/gis/map")
+@Api(tags = "供水管网-供水设施GIS一张图")
+public class WaterSupplyGisMapController {
+    @Resource private WaterSupplyGisMapService gisMapService;
+
+    @GetMapping("/features")
+    @ApiOperation(value = "供水管网-供水设施GIS一张图-查询 GeoJSON 图层",
+            notes = "响应 data 为 GeoJSON FeatureCollection。点设施 geometry 为 Point,管网 geometry 为 LineString;坐标顺序固定为 [longitude, latitude],缺少完整坐标的记录不返回。")
+    @PreAuthorize("@ss.hasPermi('waterSupply:gis:map')")
+    public AjaxResult features(
+            @ApiParam(value = "设施类型,逗号分隔:source、plant、pumpStation、pipe、user;不传返回全部图层", example = "plant,pipe")
+            @RequestParam(required = false) String types) {
+        try {
+            return AjaxResult.success(gisMapService.getFeatures(parseTypes(types)));
+        } catch (IllegalArgumentException e) {
+            return AjaxResult.error(e.getMessage());
+        }
+    }
+
+    @GetMapping("/detail/{facilityType}/{id}")
+    @ApiOperation(value = "供水管网-供水设施GIS一张图-查询设施详情",
+            notes = "facilityType 仅支持 source、plant、pumpStation、pipe、user。响应 data 为对应设施实体,包含新增 WGS84 坐标字段;详情查询不要求坐标完整。")
+    @PreAuthorize("@ss.hasPermi('waterSupply:gis:map')")
+    public AjaxResult detail(
+            @ApiParam(value = "设施类型:source、plant、pumpStation、pipe、user", required = true, example = "plant")
+            @PathVariable String facilityType,
+            @ApiParam(value = "设施主键 ID", required = true, example = "12")
+            @PathVariable Long id) {
+        try {
+            Object detail = gisMapService.getDetail(facilityType, id);
+            return detail == null ? AjaxResult.error("设施不存在") : AjaxResult.success(detail);
+        } catch (IllegalArgumentException e) {
+            return AjaxResult.error(e.getMessage());
+        }
+    }
+
+    @GetMapping("/overview")
+    @ApiOperation(value = "供水管网-供水设施GIS一张图-查询坐标完整性概览",
+            notes = "响应 data 为数组,按设施类型返回 totalCount、geoCompleteCount、geoMissingCount,并追加 facilityType=total 的合计项。")
+    @PreAuthorize("@ss.hasPermi('waterSupply:gis:map')")
+    public AjaxResult overview() {
+        return AjaxResult.success(gisMapService.getOverview());
+    }
+
+    private Set<String> parseTypes(String text) {
+        if (text == null || text.trim().isEmpty()) return Collections.emptySet();
+        return Arrays.stream(text.split(",")).map(String::trim).filter(s -> !s.isEmpty()).collect(Collectors.toSet());
+    }
+}

+ 6 - 1
pipe-network-service/zksy-system/pom.xml

@@ -16,6 +16,11 @@
     </description>
 
     <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
         <!-- 通用工具-->
         <dependency>
             <groupId>com.zksy</groupId>
@@ -56,4 +61,4 @@
             </plugin>
         </plugins>
     </build>
-</project>
+</project>

+ 12 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/PipeNetworkStatistics/dto/FacilityStatisticItemDTO.java

@@ -0,0 +1,12 @@
+package com.zksy.WaterSupply.PipeNetworkStatistics.dto;
+
+import lombok.Data;
+
+@Data
+public class FacilityStatisticItemDTO {
+    private String facilityType;
+    private String facilityTypeName;
+    private Long totalCount;
+    private Long normalCount;
+    private Long abnormalCount;
+}

+ 12 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/PipeNetworkStatistics/dto/MetricItemDTO.java

@@ -0,0 +1,12 @@
+package com.zksy.WaterSupply.PipeNetworkStatistics.dto;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class MetricItemDTO {
+    private String name;
+    private BigDecimal value;
+    private String unit;
+}

+ 18 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/PipeNetworkStatistics/dto/PipeNetworkStatisticsDashboardDTO.java

@@ -0,0 +1,18 @@
+package com.zksy.WaterSupply.PipeNetworkStatistics.dto;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+@Data
+public class PipeNetworkStatisticsDashboardDTO {
+    private List<FacilityStatisticItemDTO> facilityOverview = new ArrayList<>();
+    private Map<String, Object> pipeNetwork = new LinkedHashMap<>();
+    private Map<String, Object> waterPlant = new LinkedHashMap<>();
+    private Map<String, Object> pumpStation = new LinkedHashMap<>();
+    private Map<String, Object> waterSource = new LinkedHashMap<>();
+    private Map<String, Object> waterUser = new LinkedHashMap<>();
+}

+ 30 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/PipeNetworkStatistics/mapper/PipeNetworkStatisticsMapper.java

@@ -0,0 +1,30 @@
+package com.zksy.WaterSupply.PipeNetworkStatistics.mapper;
+
+import com.zksy.WaterSupply.PipeNetworkStatistics.dto.FacilityStatisticItemDTO;
+import com.zksy.WaterSupply.PipeNetworkStatistics.dto.MetricItemDTO;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+import java.util.Map;
+
+@Mapper
+public interface PipeNetworkStatisticsMapper {
+    List<FacilityStatisticItemDTO> selectFacilityOverview(@Param("status") String status);
+    Map<String, Object> selectPipeSummary(@Param("status") String status);
+    Map<String, Object> selectWaterPlantSummary(@Param("status") String status);
+    Map<String, Object> selectPumpStationSummary(@Param("status") String status);
+    Map<String, Object> selectWaterSourceSummary(@Param("status") String status);
+    Map<String, Object> selectWaterUserSummary(@Param("status") String status);
+    List<MetricItemDTO> selectPipeMaterialDistribution(@Param("status") String status);
+    List<MetricItemDTO> selectPipeDiameterDistribution(@Param("status") String status);
+    List<MetricItemDTO> selectPipeLayingYearDistribution(@Param("status") String status);
+    List<MetricItemDTO> selectPipePressureDistribution(@Param("status") String status);
+    List<MetricItemDTO> selectStatusDistribution(@Param("tableType") String tableType, @Param("status") String status);
+    List<MetricItemDTO> selectWaterPlantSourceTypeDistribution(@Param("status") String status);
+    List<MetricItemDTO> selectWaterSourceTypeDistribution(@Param("status") String status);
+    List<MetricItemDTO> selectWaterSourceQualityDistribution(@Param("status") String status);
+    List<MetricItemDTO> selectWaterSourceProtectionDistribution(@Param("status") String status);
+    List<MetricItemDTO> selectWaterUserTypeDistribution(@Param("status") String status);
+    List<MetricItemDTO> selectWaterUserConsumptionDistribution(@Param("status") String status);
+}

+ 10 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/PipeNetworkStatistics/service/PipeNetworkStatisticsExportService.java

@@ -0,0 +1,10 @@
+package com.zksy.WaterSupply.PipeNetworkStatistics.service;
+
+import com.zksy.WaterSupply.PipeNetworkStatistics.dto.PipeNetworkStatisticsDashboardDTO;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+public interface PipeNetworkStatisticsExportService {
+    void write(OutputStream outputStream, PipeNetworkStatisticsDashboardDTO dashboard) throws IOException;
+}

+ 9 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/PipeNetworkStatistics/service/PipeNetworkStatisticsService.java

@@ -0,0 +1,9 @@
+package com.zksy.WaterSupply.PipeNetworkStatistics.service;
+
+import com.zksy.WaterSupply.PipeNetworkStatistics.dto.PipeNetworkStatisticsDashboardDTO;
+
+import java.util.Set;
+
+public interface PipeNetworkStatisticsService {
+    PipeNetworkStatisticsDashboardDTO getDashboard(String status, Set<String> facilityTypes);
+}

+ 86 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/PipeNetworkStatistics/service/impl/PipeNetworkStatisticsExportServiceImpl.java

@@ -0,0 +1,86 @@
+package com.zksy.WaterSupply.PipeNetworkStatistics.service.impl;
+
+import com.zksy.WaterSupply.PipeNetworkStatistics.dto.MetricItemDTO;
+import com.zksy.WaterSupply.PipeNetworkStatistics.dto.PipeNetworkStatisticsDashboardDTO;
+import com.zksy.WaterSupply.PipeNetworkStatistics.service.PipeNetworkStatisticsExportService;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.usermodel.Workbook;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.springframework.stereotype.Service;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class PipeNetworkStatisticsExportServiceImpl implements PipeNetworkStatisticsExportService {
+    private static final String[] HEADERS = {"指标", "分组", "数值", "单位"};
+
+    @Override
+    public void write(OutputStream outputStream, PipeNetworkStatisticsDashboardDTO dashboard) throws IOException {
+        try (Workbook workbook = new XSSFWorkbook()) {
+            writeOverview(workbook.createSheet("综合概览"), dashboard);
+            writeMapSheet(workbook.createSheet("管网统计"), dashboard.getPipeNetwork());
+            writeMapSheet(workbook.createSheet("水厂统计"), dashboard.getWaterPlant());
+            writeMapSheet(workbook.createSheet("泵站统计"), dashboard.getPumpStation());
+            writeMapSheet(workbook.createSheet("水源地统计"), dashboard.getWaterSource());
+            writeMapSheet(workbook.createSheet("大用水户统计"), dashboard.getWaterUser());
+            workbook.write(outputStream);
+        }
+    }
+
+    private void writeOverview(Sheet sheet, PipeNetworkStatisticsDashboardDTO dashboard) {
+        header(sheet);
+        int rowIndex = 1;
+        if (dashboard.getFacilityOverview() != null) {
+            for (var item : dashboard.getFacilityOverview()) {
+                Row row = sheet.createRow(rowIndex++);
+                row.createCell(0).setCellValue("设施总数");
+                row.createCell(1).setCellValue(item.getFacilityTypeName());
+                row.createCell(2).setCellValue(item.getTotalCount() == null ? 0 : item.getTotalCount());
+                row.createCell(3).setCellValue("项");
+            }
+        }
+    }
+
+    private void writeMapSheet(Sheet sheet, Map<String, Object> values) {
+        header(sheet);
+        if (values == null) return;
+        int rowIndex = 1;
+        for (Map.Entry<String, Object> entry : values.entrySet()) {
+            Object value = entry.getValue();
+            if (value instanceof List) {
+                for (Object item : (List<?>) value) {
+                    Row row = sheet.createRow(rowIndex++);
+                    if (item instanceof MetricItemDTO) {
+                        MetricItemDTO metric = (MetricItemDTO) item;
+                        row.createCell(0).setCellValue(entry.getKey());
+                        row.createCell(1).setCellValue(metric.getName());
+                        setValue(row, 2, metric.getValue());
+                        row.createCell(3).setCellValue(metric.getUnit());
+                    }
+                }
+            } else {
+                Row row = sheet.createRow(rowIndex++);
+                row.createCell(0).setCellValue(entry.getKey());
+                row.createCell(1).setCellValue("");
+                setValue(row, 2, value);
+                row.createCell(3).setCellValue("");
+            }
+        }
+    }
+
+    private void header(Sheet sheet) {
+        Row row = sheet.createRow(0);
+        for (int i = 0; i < HEADERS.length; i++) row.createCell(i).setCellValue(HEADERS[i]);
+    }
+
+    private void setValue(Row row, int index, Object value) {
+        if (value instanceof Number) row.createCell(index).setCellValue(((Number) value).doubleValue());
+        else if (value instanceof BigDecimal) row.createCell(index).setCellValue(((BigDecimal) value).doubleValue());
+        else row.createCell(index).setCellValue(value == null ? "" : String.valueOf(value));
+    }
+}

+ 97 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/PipeNetworkStatistics/service/impl/PipeNetworkStatisticsServiceImpl.java

@@ -0,0 +1,97 @@
+package com.zksy.WaterSupply.PipeNetworkStatistics.service.impl;
+
+import com.zksy.WaterSupply.PipeNetworkStatistics.dto.MetricItemDTO;
+import com.zksy.WaterSupply.PipeNetworkStatistics.dto.PipeNetworkStatisticsDashboardDTO;
+import com.zksy.WaterSupply.PipeNetworkStatistics.mapper.PipeNetworkStatisticsMapper;
+import com.zksy.WaterSupply.PipeNetworkStatistics.service.PipeNetworkStatisticsService;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@Service
+public class PipeNetworkStatisticsServiceImpl implements PipeNetworkStatisticsService {
+    private static final Set<String> SUPPORTED_TYPES = Set.of("source", "plant", "pumpStation", "pipe", "user");
+
+    @Resource
+    private PipeNetworkStatisticsMapper statisticsMapper;
+
+    @Override
+    public PipeNetworkStatisticsDashboardDTO getDashboard(String status, Set<String> facilityTypes) {
+        validateFacilityTypes(facilityTypes);
+        Set<String> types = facilityTypes == null || facilityTypes.isEmpty() ? SUPPORTED_TYPES : facilityTypes;
+        PipeNetworkStatisticsDashboardDTO dashboard = new PipeNetworkStatisticsDashboardDTO();
+        List<com.zksy.WaterSupply.PipeNetworkStatistics.dto.FacilityStatisticItemDTO> overview = statisticsMapper.selectFacilityOverview(status);
+        dashboard.setFacilityOverview((overview == null ? Collections.<com.zksy.WaterSupply.PipeNetworkStatistics.dto.FacilityStatisticItemDTO>emptyList() : overview)
+                .stream().filter(item -> types.contains(item.getFacilityType())).collect(Collectors.toList()));
+        if (types.contains("pipe")) {
+            Map<String, Object> result = zeroSafe(statisticsMapper.selectPipeSummary(status));
+            result.put("materialDistribution", list(statisticsMapper.selectPipeMaterialDistribution(status)));
+            result.put("diameterDistribution", list(statisticsMapper.selectPipeDiameterDistribution(status)));
+            result.put("layingYearDistribution", list(statisticsMapper.selectPipeLayingYearDistribution(status)));
+            result.put("pressureDistribution", list(statisticsMapper.selectPipePressureDistribution(status)));
+            result.put("statusDistribution", list(statisticsMapper.selectStatusDistribution("pipe", status)));
+            dashboard.setPipeNetwork(result);
+        }
+        if (types.contains("plant")) {
+            Map<String, Object> result = zeroSafe(statisticsMapper.selectWaterPlantSummary(status));
+            result.put("capacityUtilization", utilization(result));
+            result.put("statusDistribution", list(statisticsMapper.selectStatusDistribution("plant", status)));
+            result.put("waterSourceTypeDistribution", list(statisticsMapper.selectWaterPlantSourceTypeDistribution(status)));
+            dashboard.setWaterPlant(result);
+        }
+        if (types.contains("pumpStation")) {
+            Map<String, Object> result = zeroSafe(statisticsMapper.selectPumpStationSummary(status));
+            result.put("statusDistribution", list(statisticsMapper.selectStatusDistribution("pumpStation", status)));
+            dashboard.setPumpStation(result);
+        }
+        if (types.contains("source")) {
+            Map<String, Object> result = zeroSafe(statisticsMapper.selectWaterSourceSummary(status));
+            result.put("statusDistribution", list(statisticsMapper.selectStatusDistribution("source", status)));
+            result.put("sourceTypeDistribution", list(statisticsMapper.selectWaterSourceTypeDistribution(status)));
+            result.put("qualityDistribution", list(statisticsMapper.selectWaterSourceQualityDistribution(status)));
+            result.put("protectionDistribution", list(statisticsMapper.selectWaterSourceProtectionDistribution(status)));
+            dashboard.setWaterSource(result);
+        }
+        if (types.contains("user")) {
+            Map<String, Object> result = zeroSafe(statisticsMapper.selectWaterUserSummary(status));
+            result.put("statusDistribution", list(statisticsMapper.selectStatusDistribution("user", status)));
+            result.put("userTypeDistribution", list(statisticsMapper.selectWaterUserTypeDistribution(status)));
+            result.put("consumptionDistribution", list(statisticsMapper.selectWaterUserConsumptionDistribution(status)));
+            dashboard.setWaterUser(result);
+        }
+        return dashboard;
+    }
+
+    private void validateFacilityTypes(Set<String> facilityTypes) {
+        if (facilityTypes != null && !SUPPORTED_TYPES.containsAll(facilityTypes)) {
+            throw new IllegalArgumentException("不支持的设施类型");
+        }
+    }
+
+    private Map<String, Object> zeroSafe(Map<String, Object> value) {
+        return value == null ? new LinkedHashMap<>() : new LinkedHashMap<>(value);
+    }
+
+    private List<MetricItemDTO> list(List<MetricItemDTO> value) {
+        return value == null ? Collections.emptyList() : value;
+    }
+
+    private BigDecimal utilization(Map<String, Object> values) {
+        BigDecimal design = decimal(values.get("designCapacity") == null ? values.get("design_capacity") : values.get("designCapacity"));
+        BigDecimal current = decimal(values.get("currentCapacity") == null ? values.get("current_capacity") : values.get("currentCapacity"));
+        return design.signum() == 0 ? BigDecimal.ZERO : current.multiply(BigDecimal.valueOf(100)).divide(design, 2, RoundingMode.HALF_UP);
+    }
+
+    private BigDecimal decimal(Object value) {
+        if (value instanceof BigDecimal) return (BigDecimal) value;
+        return value == null ? BigDecimal.ZERO : new BigDecimal(value.toString());
+    }
+}

+ 13 - 1
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/WaterPipeInfo/domain/WaterPipeInfo.java

@@ -46,6 +46,18 @@ public class WaterPipeInfo {
     @ApiModelProperty(value = "终点位置", example = "XX路100号", position = 8)
     private String endPoint;
 
+    @ApiModelProperty(value = "起点WGS84经度", example = "113.12345678", position = 9)
+    private BigDecimal startLongitude;
+
+    @ApiModelProperty(value = "起点WGS84纬度", example = "23.12345678", position = 10)
+    private BigDecimal startLatitude;
+
+    @ApiModelProperty(value = "终点WGS84经度", example = "113.22345678", position = 11)
+    private BigDecimal endLongitude;
+
+    @ApiModelProperty(value = "终点WGS84纬度", example = "23.22345678", position = 12)
+    private BigDecimal endLatitude;
+
     @ApiModelProperty(value = "铺设年份", example = "2026", position = 9)
     private Integer layingYear;
 
@@ -79,4 +91,4 @@ public class WaterPipeInfo {
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     @TableField(fill = com.baomidou.mybatisplus.annotation.FieldFill.UPDATE)
     private LocalDateTime updateTime;
-}
+}

+ 7 - 1
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/WaterPlantInfo/domain/WaterPlantInfo.java

@@ -60,6 +60,12 @@ public class WaterPlantInfo {
     )
     private String location;
 
+    @ApiModelProperty(value = "WGS84经度", example = "113.12345678", position = 5)
+    private BigDecimal longitude;
+
+    @ApiModelProperty(value = "WGS84纬度", example = "23.12345678", position = 6)
+    private BigDecimal latitude;
+
     @ApiModelProperty(
             value = "水源类型",
             example = "地表水",
@@ -194,4 +200,4 @@ public class WaterPlantInfo {
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     @TableField(fill = com.baomidou.mybatisplus.annotation.FieldFill.UPDATE)
     private LocalDateTime updateTime;
-}
+}

+ 7 - 1
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/WaterPumpStation/domain/WaterPumpStation.java

@@ -60,6 +60,12 @@ public class WaterPumpStation {
     )
     private String location;
 
+    @ApiModelProperty(value = "WGS84经度", example = "113.12345678")
+    private BigDecimal longitude;
+
+    @ApiModelProperty(value = "WGS84纬度", example = "23.12345678")
+    private BigDecimal latitude;
+
     @ApiModelProperty(
             value = "水泵数量",
             example = "4",
@@ -186,4 +192,4 @@ public class WaterPumpStation {
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     @TableField(fill = com.baomidou.mybatisplus.annotation.FieldFill.UPDATE)
     private LocalDateTime updateTime;
-}
+}

+ 8 - 1
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/WaterSourceInfo/domain/WaterSourceInfo.java

@@ -10,6 +10,7 @@ import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 
+import java.math.BigDecimal;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 
@@ -71,6 +72,12 @@ public class WaterSourceInfo {
     )
     private String address;
 
+    @ApiModelProperty(value = "WGS84经度", example = "113.12345678", position = 7)
+    private BigDecimal longitude;
+
+    @ApiModelProperty(value = "WGS84纬度", example = "23.12345678", position = 8)
+    private BigDecimal latitude;
+
     @ApiModelProperty(
             value = "水源类型",
             example = "地表水",
@@ -173,4 +180,4 @@ public class WaterSourceInfo {
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     @TableField(fill = com.baomidou.mybatisplus.annotation.FieldFill.UPDATE)
     private LocalDateTime updateTime;
-}
+}

+ 12 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/WaterSupplyGisMap/dto/GisFeatureCollectionDTO.java

@@ -0,0 +1,12 @@
+package com.zksy.WaterSupply.WaterSupplyGisMap.dto;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+public class GisFeatureCollectionDTO {
+    private String type = "FeatureCollection";
+    private List<GisFeatureDTO> features = new ArrayList<>();
+}

+ 14 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/WaterSupplyGisMap/dto/GisFeatureDTO.java

@@ -0,0 +1,14 @@
+package com.zksy.WaterSupply.WaterSupplyGisMap.dto;
+
+import lombok.Data;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+@Data
+public class GisFeatureDTO {
+    private String type = "Feature";
+    private String id;
+    private Map<String, Object> geometry = new LinkedHashMap<>();
+    private Map<String, Object> properties = new LinkedHashMap<>();
+}

+ 12 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/WaterSupplyGisMap/dto/GisOverviewItemDTO.java

@@ -0,0 +1,12 @@
+package com.zksy.WaterSupply.WaterSupplyGisMap.dto;
+
+import lombok.Data;
+
+@Data
+public class GisOverviewItemDTO {
+    private String facilityType;
+    private String facilityTypeName;
+    private Long totalCount;
+    private Long geoCompleteCount;
+    private Long geoMissingCount;
+}

+ 17 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/WaterSupplyGisMap/mapper/WaterSupplyGisMapMapper.java

@@ -0,0 +1,17 @@
+package com.zksy.WaterSupply.WaterSupplyGisMap.mapper;
+
+import com.zksy.WaterSupply.WaterSupplyGisMap.dto.GisOverviewItemDTO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+import java.util.Map;
+
+@Mapper
+public interface WaterSupplyGisMapMapper {
+    List<Map<String, Object>> selectSourceFeatures();
+    List<Map<String, Object>> selectPlantFeatures();
+    List<Map<String, Object>> selectPumpStationFeatures();
+    List<Map<String, Object>> selectPipeFeatures();
+    List<Map<String, Object>> selectUserFeatures();
+    List<GisOverviewItemDTO> selectOverview();
+}

+ 15 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/WaterSupplyGisMap/service/WaterSupplyGisMapService.java

@@ -0,0 +1,15 @@
+package com.zksy.WaterSupply.WaterSupplyGisMap.service;
+
+import com.zksy.WaterSupply.WaterSupplyGisMap.dto.GisFeatureCollectionDTO;
+import com.zksy.WaterSupply.WaterSupplyGisMap.dto.GisFeatureDTO;
+import com.zksy.WaterSupply.WaterSupplyGisMap.dto.GisOverviewItemDTO;
+
+import java.util.List;
+import java.util.Set;
+
+public interface WaterSupplyGisMapService {
+    GisFeatureCollectionDTO getFeatures(Set<String> types);
+    Object getDetail(String facilityType, Long id);
+    List<GisOverviewItemDTO> getOverview();
+    GisFeatureDTO toPipeFeature(java.util.Map<String, Object> row);
+}

+ 135 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/WaterSupplyGisMap/service/impl/WaterSupplyGisMapServiceImpl.java

@@ -0,0 +1,135 @@
+package com.zksy.WaterSupply.WaterSupplyGisMap.service.impl;
+
+import com.zksy.WaterSupply.WaterPipeInfo.service.IWaterPipeInfoService;
+import com.zksy.WaterSupply.WaterPlantInfo.service.IWaterPlantInfoService;
+import com.zksy.WaterSupply.WaterPumpStation.service.IWaterPumpStationService;
+import com.zksy.WaterSupply.WaterSourceInfo.service.IWaterSourceInfoService;
+import com.zksy.WaterSupply.WaterSupplyGisMap.dto.GisFeatureCollectionDTO;
+import com.zksy.WaterSupply.WaterSupplyGisMap.dto.GisFeatureDTO;
+import com.zksy.WaterSupply.WaterSupplyGisMap.dto.GisOverviewItemDTO;
+import com.zksy.WaterSupply.WaterSupplyGisMap.mapper.WaterSupplyGisMapMapper;
+import com.zksy.WaterSupply.WaterSupplyGisMap.service.WaterSupplyGisMapService;
+import com.zksy.WaterSupply.WaterUserInfo.service.IWaterUserInfoService;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+@Service
+public class WaterSupplyGisMapServiceImpl implements WaterSupplyGisMapService {
+    private static final Set<String> SUPPORTED_TYPES = Set.of("source", "plant", "pumpStation", "pipe", "user");
+    private static final Map<String, String> TYPE_NAMES = Map.of("source", "水源地", "plant", "水厂", "pumpStation", "泵站", "pipe", "供水管网", "user", "大用水户");
+
+    @Resource private WaterSupplyGisMapMapper mapper;
+    @Resource private IWaterSourceInfoService sourceService;
+    @Resource private IWaterPlantInfoService plantService;
+    @Resource private IWaterPumpStationService pumpStationService;
+    @Resource private IWaterPipeInfoService pipeService;
+    @Resource private IWaterUserInfoService userService;
+
+    @Override
+    public GisFeatureCollectionDTO getFeatures(Set<String> types) {
+        validateTypes(types);
+        Set<String> selected = types == null || types.isEmpty() ? SUPPORTED_TYPES : types;
+        GisFeatureCollectionDTO collection = new GisFeatureCollectionDTO();
+        if (selected.contains("source")) addPointFeatures(collection, "source", mapper.selectSourceFeatures());
+        if (selected.contains("plant")) addPointFeatures(collection, "plant", mapper.selectPlantFeatures());
+        if (selected.contains("pumpStation")) addPointFeatures(collection, "pumpStation", mapper.selectPumpStationFeatures());
+        if (selected.contains("pipe")) addPipeFeatures(collection, mapper.selectPipeFeatures());
+        if (selected.contains("user")) addPointFeatures(collection, "user", mapper.selectUserFeatures());
+        return collection;
+    }
+
+    @Override
+    public Object getDetail(String facilityType, Long id) {
+        if (!SUPPORTED_TYPES.contains(facilityType)) throw new IllegalArgumentException("不支持的设施类型:" + facilityType);
+        switch (facilityType) {
+            case "source": return sourceService.getById(id);
+            case "plant": return plantService.getById(id);
+            case "pumpStation": return pumpStationService.getById(id);
+            case "pipe": return pipeService.getById(id);
+            default: return userService.getById(id);
+        }
+    }
+
+    @Override
+    public List<GisOverviewItemDTO> getOverview() {
+        List<GisOverviewItemDTO> rows = mapper.selectOverview();
+        if (rows == null || rows.isEmpty()) return Collections.emptyList();
+        GisOverviewItemDTO total = new GisOverviewItemDTO();
+        total.setFacilityType("total");
+        total.setFacilityTypeName("合计");
+        total.setTotalCount(rows.stream().mapToLong(item -> item.getTotalCount() == null ? 0 : item.getTotalCount()).sum());
+        total.setGeoCompleteCount(rows.stream().mapToLong(item -> item.getGeoCompleteCount() == null ? 0 : item.getGeoCompleteCount()).sum());
+        total.setGeoMissingCount(rows.stream().mapToLong(item -> item.getGeoMissingCount() == null ? 0 : item.getGeoMissingCount()).sum());
+        List<GisOverviewItemDTO> result = new ArrayList<>(rows);
+        result.add(total);
+        return result;
+    }
+
+    @Override
+    public GisFeatureDTO toPipeFeature(Map<String, Object> row) {
+        GisFeatureDTO feature = baseFeature("pipe", row);
+        Map<String, Object> geometry = new LinkedHashMap<>();
+        geometry.put("type", "LineString");
+        List<List<BigDecimal>> coordinates = new ArrayList<>();
+        coordinates.add(List.of(decimal(row, "startLongitude", "start_longitude"), decimal(row, "startLatitude", "start_latitude")));
+        coordinates.add(List.of(decimal(row, "endLongitude", "end_longitude"), decimal(row, "endLatitude", "end_latitude")));
+        geometry.put("coordinates", coordinates);
+        feature.setGeometry(geometry);
+        return feature;
+    }
+
+    private void addPointFeatures(GisFeatureCollectionDTO collection, String type, List<Map<String, Object>> rows) {
+        if (rows == null) return;
+        for (Map<String, Object> row : rows) {
+            GisFeatureDTO feature = baseFeature(type, row);
+            Map<String, Object> geometry = new LinkedHashMap<>();
+            geometry.put("type", "Point");
+            geometry.put("coordinates", List.of(decimal(row, "longitude"), decimal(row, "latitude")));
+            feature.setGeometry(geometry);
+            collection.getFeatures().add(feature);
+        }
+    }
+
+    private void addPipeFeatures(GisFeatureCollectionDTO collection, List<Map<String, Object>> rows) {
+        if (rows != null) for (Map<String, Object> row : rows) collection.getFeatures().add(toPipeFeature(row));
+    }
+
+    private GisFeatureDTO baseFeature(String type, Map<String, Object> row) {
+        GisFeatureDTO feature = new GisFeatureDTO();
+        Object id = value(row, "id");
+        feature.setId(type + ":" + id);
+        Map<String, Object> properties = new LinkedHashMap<>();
+        properties.put("facilityType", type);
+        properties.put("facilityTypeName", TYPE_NAMES.get(type));
+        properties.put("id", id);
+        properties.put("code", value(row, "code"));
+        properties.put("name", value(row, "name"));
+        properties.put("status", value(row, "status"));
+        properties.put("location", value(row, "location"));
+        feature.setProperties(properties);
+        return feature;
+    }
+
+    private void validateTypes(Set<String> types) {
+        if (types != null && !SUPPORTED_TYPES.containsAll(types)) throw new IllegalArgumentException("不支持的设施类型");
+    }
+
+    private Object value(Map<String, Object> row, String... keys) {
+        for (String key : keys) if (row.containsKey(key)) return row.get(key);
+        return null;
+    }
+
+    private BigDecimal decimal(Map<String, Object> row, String... keys) {
+        Object value = value(row, keys);
+        return value instanceof BigDecimal ? (BigDecimal) value : new BigDecimal(value.toString());
+    }
+}

+ 7 - 1
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/WaterUserInfo/domain/WaterUserInfo.java

@@ -64,6 +64,12 @@ public class WaterUserInfo {
     )
     private String address;
 
+    @ApiModelProperty(value = "WGS84经度", example = "113.12345678")
+    private BigDecimal longitude;
+
+    @ApiModelProperty(value = "WGS84纬度", example = "23.12345678")
+    private BigDecimal latitude;
+
     @ApiModelProperty(
             value = "联系人",
             example = "XX",
@@ -172,4 +178,4 @@ public class WaterUserInfo {
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     @TableField(fill = com.baomidou.mybatisplus.annotation.FieldFill.UPDATE)
     private LocalDateTime updateTime;
-}
+}

+ 22 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/export/WaterPipeExportDTO.java

@@ -0,0 +1,22 @@
+package com.zksy.WaterSupply.export;
+
+import com.zksy.WaterSupply.WaterPipeInfo.domain.WaterPipeInfo;
+import com.zksy.common.annotation.Excel;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class WaterPipeExportDTO {
+    @Excel(name = "管网编号") private String pipeCode;
+    @Excel(name = "管网名称") private String pipeName;
+    @Excel(name = "材质") private String pipeMaterial;
+    @Excel(name = "管径(mm)") private Integer pipeDiameter;
+    @Excel(name = "长度(m)") private BigDecimal pipeLength;
+    @Excel(name = "状态") private String status;
+    @Excel(name = "起点经度") private BigDecimal startLongitude;
+    @Excel(name = "起点纬度") private BigDecimal startLatitude;
+    @Excel(name = "终点经度") private BigDecimal endLongitude;
+    @Excel(name = "终点纬度") private BigDecimal endLatitude;
+    public static WaterPipeExportDTO from(WaterPipeInfo value) { WaterPipeExportDTO dto = new WaterPipeExportDTO(); dto.pipeCode=value.getPipeCode(); dto.pipeName=value.getPipeName(); dto.pipeMaterial=value.getPipeMaterial(); dto.pipeDiameter=value.getPipeDiameter(); dto.pipeLength=value.getPipeLength(); dto.status=value.getStatus(); dto.startLongitude=value.getStartLongitude(); dto.startLatitude=value.getStartLatitude(); dto.endLongitude=value.getEndLongitude(); dto.endLatitude=value.getEndLatitude(); return dto; }
+}

+ 20 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/export/WaterPlantExportDTO.java

@@ -0,0 +1,20 @@
+package com.zksy.WaterSupply.export;
+
+import com.zksy.WaterSupply.WaterPlantInfo.domain.WaterPlantInfo;
+import com.zksy.common.annotation.Excel;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class WaterPlantExportDTO {
+    @Excel(name = "水厂编号") private String plantCode;
+    @Excel(name = "水厂名称") private String plantName;
+    @Excel(name = "位置") private String location;
+    @Excel(name = "设计能力") private BigDecimal designCapacity;
+    @Excel(name = "当前能力") private BigDecimal currentCapacity;
+    @Excel(name = "状态") private String status;
+    @Excel(name = "经度") private BigDecimal longitude;
+    @Excel(name = "纬度") private BigDecimal latitude;
+    public static WaterPlantExportDTO from(WaterPlantInfo value) { WaterPlantExportDTO dto = new WaterPlantExportDTO(); dto.plantCode=value.getPlantCode(); dto.plantName=value.getPlantName(); dto.location=value.getLocation(); dto.designCapacity=value.getDesignCapacity(); dto.currentCapacity=value.getCurrentCapacity(); dto.status=value.getStatus(); dto.longitude=value.getLongitude(); dto.latitude=value.getLatitude(); return dto; }
+}

+ 21 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/export/WaterPumpStationExportDTO.java

@@ -0,0 +1,21 @@
+package com.zksy.WaterSupply.export;
+
+import com.zksy.WaterSupply.WaterPumpStation.domain.WaterPumpStation;
+import com.zksy.common.annotation.Excel;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class WaterPumpStationExportDTO {
+    @Excel(name = "泵站编号") private String stationCode;
+    @Excel(name = "泵站名称") private String stationName;
+    @Excel(name = "位置") private String location;
+    @Excel(name = "泵数量") private Integer pumpCount;
+    @Excel(name = "设计流量") private BigDecimal designFlow;
+    @Excel(name = "设计扬程") private BigDecimal designHead;
+    @Excel(name = "状态") private String status;
+    @Excel(name = "经度") private BigDecimal longitude;
+    @Excel(name = "纬度") private BigDecimal latitude;
+    public static WaterPumpStationExportDTO from(WaterPumpStation value) { WaterPumpStationExportDTO dto = new WaterPumpStationExportDTO(); dto.stationCode=value.getStationCode(); dto.stationName=value.getStationName(); dto.location=value.getLocation(); dto.pumpCount=value.getPumpCount(); dto.designFlow=value.getDesignFlow(); dto.designHead=value.getDesignHead(); dto.status=value.getStatus(); dto.longitude=value.getLongitude(); dto.latitude=value.getLatitude(); return dto; }
+}

+ 20 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/export/WaterSourceExportDTO.java

@@ -0,0 +1,20 @@
+package com.zksy.WaterSupply.export;
+
+import com.zksy.WaterSupply.WaterSourceInfo.domain.WaterSourceInfo;
+import com.zksy.common.annotation.Excel;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class WaterSourceExportDTO {
+    @Excel(name = "水源地编号") private String sourceId;
+    @Excel(name = "水源地名称") private String sourceName;
+    @Excel(name = "水源类型") private String waterType;
+    @Excel(name = "水质等级") private String waterQualityLevel;
+    @Excel(name = "保护等级") private String protectionLevel;
+    @Excel(name = "状态") private String status;
+    @Excel(name = "经度") private BigDecimal longitude;
+    @Excel(name = "纬度") private BigDecimal latitude;
+    public static WaterSourceExportDTO from(WaterSourceInfo value) { WaterSourceExportDTO dto = new WaterSourceExportDTO(); dto.sourceId=value.getSourceId(); dto.sourceName=value.getSourceName(); dto.waterType=value.getWaterType(); dto.waterQualityLevel=value.getWaterQualityLevel(); dto.protectionLevel=value.getProtectionLevel(); dto.status=value.getStatus(); dto.longitude=value.getLongitude(); dto.latitude=value.getLatitude(); return dto; }
+}

+ 20 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/export/WaterUserExportDTO.java

@@ -0,0 +1,20 @@
+package com.zksy.WaterSupply.export;
+
+import com.zksy.WaterSupply.WaterUserInfo.domain.WaterUserInfo;
+import com.zksy.common.annotation.Excel;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class WaterUserExportDTO {
+    @Excel(name = "用户编号") private String userCode;
+    @Excel(name = "用户名称") private String userName;
+    @Excel(name = "用户类型") private String userType;
+    @Excel(name = "地址") private String address;
+    @Excel(name = "年用水量") private BigDecimal annualWaterConsumption;
+    @Excel(name = "状态") private String status;
+    @Excel(name = "经度") private BigDecimal longitude;
+    @Excel(name = "纬度") private BigDecimal latitude;
+    public static WaterUserExportDTO from(WaterUserInfo value) { WaterUserExportDTO dto = new WaterUserExportDTO(); dto.userCode=value.getUserCode(); dto.userName=value.getUserName(); dto.userType=value.getUserType(); dto.address=value.getAddress(); dto.annualWaterConsumption=value.getAnnualWaterConsumption(); dto.status=value.getStatus(); dto.longitude=value.getLongitude(); dto.latitude=value.getLatitude(); return dto; }
+}

+ 112 - 0
pipe-network-service/zksy-system/src/main/resources/mapper/WaterSupply/PipeNetworkStatistics/PipeNetworkStatisticsMapper.xml

@@ -0,0 +1,112 @@
+<?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.zksy.WaterSupply.PipeNetworkStatistics.mapper.PipeNetworkStatisticsMapper">
+    <select id="selectFacilityOverview" resultType="com.zksy.WaterSupply.PipeNetworkStatistics.dto.FacilityStatisticItemDTO">
+        SELECT 'source' facility_type, '水源地' facility_type_name, COUNT(*) total_count,
+               COALESCE(SUM(CASE WHEN status = '0' THEN 1 ELSE 0 END), 0) normal_count,
+               COALESCE(SUM(CASE WHEN status IS NOT NULL AND status &lt;&gt; '0' THEN 1 ELSE 0 END), 0) abnormal_count
+        FROM app_user.water_source_info WHERE del_flag = '0'
+        <if test="status != null and status != ''">AND status = #{status}</if>
+        UNION ALL
+        SELECT 'plant', '水厂', COUNT(*), COALESCE(SUM(CASE WHEN status = '0' THEN 1 ELSE 0 END), 0), COALESCE(SUM(CASE WHEN status IS NOT NULL AND status &lt;&gt; '0' THEN 1 ELSE 0 END), 0)
+        FROM app_user.water_plant_info WHERE del_flag = '0'
+        <if test="status != null and status != ''">AND status = #{status}</if>
+        UNION ALL
+        SELECT 'pumpStation', '泵站', COUNT(*), COALESCE(SUM(CASE WHEN status = '0' THEN 1 ELSE 0 END), 0), COALESCE(SUM(CASE WHEN status IS NOT NULL AND status &lt;&gt; '0' THEN 1 ELSE 0 END), 0)
+        FROM app_user.water_pump_station WHERE del_flag = '0'
+        <if test="status != null and status != ''">AND status = #{status}</if>
+        UNION ALL
+        SELECT 'pipe', '供水管网', COUNT(*), COALESCE(SUM(CASE WHEN status = '0' THEN 1 ELSE 0 END), 0), COALESCE(SUM(CASE WHEN status IS NOT NULL AND status &lt;&gt; '0' THEN 1 ELSE 0 END), 0)
+        FROM app_user.water_pipe_info WHERE del_flag = '0'
+        <if test="status != null and status != ''">AND status = #{status}</if>
+        UNION ALL
+        SELECT 'user', '大用水户', COUNT(*), COALESCE(SUM(CASE WHEN status = '0' THEN 1 ELSE 0 END), 0), COALESCE(SUM(CASE WHEN status IS NOT NULL AND status &lt;&gt; '0' THEN 1 ELSE 0 END), 0)
+        FROM app_user.water_user_info WHERE del_flag = '0'
+        <if test="status != null and status != ''">AND status = #{status}</if>
+    </select>
+
+    <select id="selectPipeSummary" resultType="java.util.Map">
+        SELECT COALESCE(SUM(pipe_length), 0) total_length, COALESCE(AVG(pipe_diameter), 0) average_diameter,
+               COUNT(*) total_count FROM app_user.water_pipe_info WHERE del_flag = '0'
+        <if test="status != null and status != ''">AND status = #{status}</if>
+    </select>
+    <select id="selectWaterPlantSummary" resultType="java.util.Map">
+        SELECT COUNT(*) total_count, COALESCE(SUM(design_capacity),0) design_capacity,
+               COALESCE(SUM(current_capacity),0) current_capacity FROM app_user.water_plant_info WHERE del_flag = '0'
+        <if test="status != null and status != ''">AND status = #{status}</if>
+    </select>
+    <select id="selectPumpStationSummary" resultType="java.util.Map">
+        SELECT COUNT(*) total_count, COALESCE(SUM(pump_count),0) pump_count,
+               COALESCE(SUM(design_flow),0) design_flow, COALESCE(SUM(design_head),0) design_head,
+               COALESCE(SUM(power_capacity),0) power_capacity FROM app_user.water_pump_station WHERE del_flag = '0'
+        <if test="status != null and status != ''">AND status = #{status}</if>
+    </select>
+    <select id="selectWaterSourceSummary" resultType="java.util.Map">
+        SELECT COUNT(*) total_count FROM app_user.water_source_info WHERE del_flag = '0'
+        <if test="status != null and status != ''">AND status = #{status}</if>
+    </select>
+    <select id="selectWaterUserSummary" resultType="java.util.Map">
+        SELECT COUNT(*) total_count, COALESCE(SUM(annual_water_consumption),0) annual_water_consumption
+        FROM app_user.water_user_info WHERE del_flag = '0'
+        <if test="status != null and status != ''">AND status = #{status}</if>
+    </select>
+
+    <select id="selectPipeMaterialDistribution" resultType="com.zksy.WaterSupply.PipeNetworkStatistics.dto.MetricItemDTO">
+        SELECT COALESCE(pipe_material, '未填报') name, COUNT(*) "value", '条' unit FROM app_user.water_pipe_info WHERE del_flag = '0'
+        <if test="status != null and status != ''">AND status = #{status}</if> GROUP BY pipe_material ORDER BY "value" DESC
+    </select>
+    <select id="selectPipeDiameterDistribution" resultType="com.zksy.WaterSupply.PipeNetworkStatistics.dto.MetricItemDTO">
+        SELECT CASE WHEN pipe_diameter IS NULL THEN '未填报' WHEN pipe_diameter &lt; 100 THEN '&lt;100mm'
+          WHEN pipe_diameter &lt; 300 THEN '100-299mm' WHEN pipe_diameter &lt; 600 THEN '300-599mm' ELSE '&gt;=600mm' END name,
+          COUNT(*) "value", '条' unit FROM app_user.water_pipe_info WHERE del_flag = '0'
+        <if test="status != null and status != ''">AND status = #{status}</if>
+        GROUP BY CASE WHEN pipe_diameter IS NULL THEN '未填报' WHEN pipe_diameter &lt; 100 THEN '&lt;100mm' WHEN pipe_diameter &lt; 300 THEN '100-299mm' WHEN pipe_diameter &lt; 600 THEN '300-599mm' ELSE '&gt;=600mm' END
+    </select>
+    <select id="selectPipeLayingYearDistribution" resultType="com.zksy.WaterSupply.PipeNetworkStatistics.dto.MetricItemDTO">
+        SELECT CASE WHEN laying_year IS NULL THEN CAST('未填报' AS VARCHAR)
+          ELSE CONCAT(CAST((laying_year / 10) * 10 AS VARCHAR), '-',
+                      CAST((laying_year / 10) * 10 + 9 AS VARCHAR)) END name,
+          COUNT(*) "value", '条' unit FROM app_user.water_pipe_info WHERE del_flag = '0'
+        <if test="status != null and status != ''">AND status = #{status}</if>
+        GROUP BY CASE WHEN laying_year IS NULL THEN CAST('未填报' AS VARCHAR)
+          ELSE CONCAT(CAST((laying_year / 10) * 10 AS VARCHAR), '-',
+                      CAST((laying_year / 10) * 10 + 9 AS VARCHAR)) END
+    </select>
+    <select id="selectPipePressureDistribution" resultType="com.zksy.WaterSupply.PipeNetworkStatistics.dto.MetricItemDTO">
+        SELECT COALESCE(CAST(pressure_rating AS VARCHAR), '未填报') name, COUNT(*) "value", '条' unit FROM app_user.water_pipe_info WHERE del_flag = '0'
+        <if test="status != null and status != ''">AND status = #{status}</if> GROUP BY pressure_rating
+    </select>
+
+    <select id="selectStatusDistribution" resultType="com.zksy.WaterSupply.PipeNetworkStatistics.dto.MetricItemDTO">
+        SELECT COALESCE(status, '未填报') name, COUNT(*) "value", '条' unit FROM
+        <choose><when test="tableType == 'pipe'">app_user.water_pipe_info</when><when test="tableType == 'plant'">app_user.water_plant_info</when><when test="tableType == 'pumpStation'">app_user.water_pump_station</when><when test="tableType == 'source'">app_user.water_source_info</when><otherwise>app_user.water_user_info</otherwise></choose>
+        WHERE del_flag = '0' <if test="status != null and status != ''">AND status = #{status}</if> GROUP BY status
+    </select>
+    <select id="selectWaterPlantSourceTypeDistribution" resultType="com.zksy.WaterSupply.PipeNetworkStatistics.dto.MetricItemDTO">
+        SELECT COALESCE(water_source_type,'未填报') name, COUNT(*) "value", '座' unit FROM app_user.water_plant_info WHERE del_flag = '0'
+        <if test="status != null and status != ''">AND status = #{status}</if> GROUP BY water_source_type
+    </select>
+    <select id="selectWaterSourceTypeDistribution" resultType="com.zksy.WaterSupply.PipeNetworkStatistics.dto.MetricItemDTO">
+        SELECT COALESCE(source_type,'未填报') name, COUNT(*) "value", '处' unit FROM app_user.water_source_info WHERE del_flag = '0'
+        <if test="status != null and status != ''">AND status = #{status}</if> GROUP BY source_type
+    </select>
+    <select id="selectWaterSourceQualityDistribution" resultType="com.zksy.WaterSupply.PipeNetworkStatistics.dto.MetricItemDTO">
+        SELECT COALESCE(water_quality_level,'未填报') name, COUNT(*) "value", '处' unit FROM app_user.water_source_info WHERE del_flag = '0'
+        <if test="status != null and status != ''">AND status = #{status}</if> GROUP BY water_quality_level
+    </select>
+    <select id="selectWaterSourceProtectionDistribution" resultType="com.zksy.WaterSupply.PipeNetworkStatistics.dto.MetricItemDTO">
+        SELECT COALESCE(protection_level,'未填报') name, COUNT(*) "value", '处' unit FROM app_user.water_source_info WHERE del_flag = '0'
+        <if test="status != null and status != ''">AND status = #{status}</if> GROUP BY protection_level
+    </select>
+    <select id="selectWaterUserTypeDistribution" resultType="com.zksy.WaterSupply.PipeNetworkStatistics.dto.MetricItemDTO">
+        SELECT COALESCE(user_type,'未填报') name, COUNT(*) "value", '户' unit FROM app_user.water_user_info WHERE del_flag = '0'
+        <if test="status != null and status != ''">AND status = #{status}</if> GROUP BY user_type
+    </select>
+    <select id="selectWaterUserConsumptionDistribution" resultType="com.zksy.WaterSupply.PipeNetworkStatistics.dto.MetricItemDTO">
+        SELECT CASE WHEN annual_water_consumption IS NULL THEN '未填报' WHEN annual_water_consumption &lt; 10000 THEN '&lt;1万m³'
+          WHEN annual_water_consumption &lt; 100000 THEN '1-10万m³' WHEN annual_water_consumption &lt; 500000 THEN '10-50万m³' ELSE '&gt;=50万m³' END name,
+          COUNT(*) "value", '户' unit FROM app_user.water_user_info WHERE del_flag = '0'
+        <if test="status != null and status != ''">AND status = #{status}</if>
+        GROUP BY CASE WHEN annual_water_consumption IS NULL THEN '未填报' WHEN annual_water_consumption &lt; 10000 THEN '&lt;1万m³' WHEN annual_water_consumption &lt; 100000 THEN '1-10万m³' WHEN annual_water_consumption &lt; 500000 THEN '10-50万m³' ELSE '&gt;=50万m³' END
+    </select>
+</mapper>

+ 6 - 1
pipe-network-service/zksy-system/src/main/resources/mapper/WaterSupply/WaterPipeInfo/WaterPipeInfoMapper.xml

@@ -11,6 +11,10 @@
         <result column="pipe_length" property="pipeLength"/>
         <result column="start_point" property="startPoint"/>
         <result column="end_point" property="endPoint"/>
+        <result column="start_longitude" property="startLongitude"/>
+        <result column="start_latitude" property="startLatitude"/>
+        <result column="end_longitude" property="endLongitude"/>
+        <result column="end_latitude" property="endLatitude"/>
         <result column="laying_year" property="layingYear"/>
         <result column="pressure_rating" property="pressureRating"/>
         <result column="status" property="status"/>
@@ -24,7 +28,8 @@
 
     <sql id="Base_Column_List">
         id, pipe_code, pipe_name, pipe_material, pipe_diameter, pipe_length,
-        start_point, end_point, laying_year, pressure_rating, status, remark,
+        start_point, end_point, start_longitude, start_latitude, end_longitude, end_latitude,
+        laying_year, pressure_rating, status, remark,
         del_flag, create_by, create_time, update_by, update_time
     </sql>
 

+ 4 - 2
pipe-network-service/zksy-system/src/main/resources/mapper/WaterSupply/WaterPlantInfo/WaterPlantInfoMapper.xml

@@ -7,6 +7,8 @@
         <result column="plant_code" property="plantCode" />
         <result column="plant_name" property="plantName" />
         <result column="location" property="location" />
+        <result column="longitude" property="longitude" />
+        <result column="latitude" property="latitude" />
         <result column="water_source_type" property="waterSourceType" />
         <result column="design_capacity" property="designCapacity" />
         <result column="current_capacity" property="currentCapacity" />
@@ -25,7 +27,7 @@
     </resultMap>
 
     <sql id="Base_Column_List">
-        id, plant_code, plant_name, location, water_source_type, design_capacity,
+        id, plant_code, plant_name, location, longitude, latitude, water_source_type, design_capacity,
         current_capacity, treatment_process, commission_date, water_quality_standard,
         status, principal, contact_phone, remark, del_flag, create_by, create_time,
         update_by, update_time
@@ -97,4 +99,4 @@
         </foreach>
     </delete>
 
-</mapper>
+</mapper>

+ 4 - 2
pipe-network-service/zksy-system/src/main/resources/mapper/WaterSupply/WaterPumpStation/WaterPumpStationMapper.xml

@@ -7,6 +7,8 @@
         <result column="station_code" property="stationCode" />
         <result column="station_name" property="stationName" />
         <result column="location" property="location" />
+        <result column="longitude" property="longitude" />
+        <result column="latitude" property="latitude" />
         <result column="pump_count" property="pumpCount" />
         <result column="design_flow" property="designFlow" />
         <result column="design_head" property="designHead" />
@@ -24,7 +26,7 @@
     </resultMap>
 
     <sql id="Base_Column_List">
-        id, station_code, station_name, location, pump_count, design_flow, design_head,
+        id, station_code, station_name, location, longitude, latitude, pump_count, design_flow, design_head,
         power_capacity, commission_date, status, principal, contact_phone, remark,
         del_flag, create_by, create_time, update_by, update_time
     </sql>
@@ -96,4 +98,4 @@
         </foreach>
     </delete>
 
-</mapper>
+</mapper>

+ 4 - 2
pipe-network-service/zksy-system/src/main/resources/mapper/WaterSupply/WaterSourceInfo/WaterSourceInfoMapper.xml

@@ -10,6 +10,8 @@
         <result column="source_type" property="sourceType"/>
         <result column="unit" property="unit"/>
         <result column="address" property="address"/>
+        <result column="longitude" property="longitude"/>
+        <result column="latitude" property="latitude"/>
         <result column="water_type" property="waterType"/>
         <result column="protection_level" property="protectionLevel"/>
         <result column="water_quality_level" property="waterQualityLevel"/>
@@ -25,7 +27,7 @@
     </resultMap>
 
     <sql id="Base_Column_List">
-        id, source_id, source_name, source_type, unit, address, water_type,
+        id, source_id, source_name, source_type, unit, address, longitude, latitude, water_type,
         protection_level, water_quality_level, protection_scope, report_time,
         status, remark, del_flag, create_by, create_time, update_by, update_time
     </sql>
@@ -95,4 +97,4 @@
         </foreach>
     </delete>
 
-</mapper>
+</mapper>

+ 45 - 0
pipe-network-service/zksy-system/src/main/resources/mapper/WaterSupply/WaterSupplyGisMap/WaterSupplyGisMapMapper.xml

@@ -0,0 +1,45 @@
+<?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.zksy.WaterSupply.WaterSupplyGisMap.mapper.WaterSupplyGisMapMapper">
+    <select id="selectSourceFeatures" resultType="java.util.Map">
+        SELECT id, source_id code, source_name name, status, address AS "location", longitude, latitude
+        FROM app_user.water_source_info WHERE del_flag = '0' AND longitude IS NOT NULL AND latitude IS NOT NULL
+    </select>
+    <select id="selectPlantFeatures" resultType="java.util.Map">
+        SELECT id, plant_code code, plant_name name, status, "location", longitude, latitude
+        FROM app_user.water_plant_info WHERE del_flag = '0' AND longitude IS NOT NULL AND latitude IS NOT NULL
+    </select>
+    <select id="selectPumpStationFeatures" resultType="java.util.Map">
+        SELECT id, station_code code, station_name name, status, "location", longitude, latitude
+        FROM app_user.water_pump_station WHERE del_flag = '0' AND longitude IS NOT NULL AND latitude IS NOT NULL
+    </select>
+    <select id="selectPipeFeatures" resultType="java.util.Map">
+        SELECT id, pipe_code code, pipe_name name, status, start_point AS "location",
+               start_longitude, start_latitude, end_longitude, end_latitude
+        FROM app_user.water_pipe_info WHERE del_flag = '0'
+          AND start_longitude IS NOT NULL AND start_latitude IS NOT NULL
+          AND end_longitude IS NOT NULL AND end_latitude IS NOT NULL
+    </select>
+    <select id="selectUserFeatures" resultType="java.util.Map">
+        SELECT id, user_code code, user_name name, status, address AS "location", longitude, latitude
+        FROM app_user.water_user_info WHERE del_flag = '0' AND longitude IS NOT NULL AND latitude IS NOT NULL
+    </select>
+    <select id="selectOverview" resultType="com.zksy.WaterSupply.WaterSupplyGisMap.dto.GisOverviewItemDTO">
+        SELECT 'source' facility_type, '水源地' facility_type_name, COUNT(*) total_count,
+          COALESCE(SUM(CASE WHEN longitude IS NOT NULL AND latitude IS NOT NULL THEN 1 ELSE 0 END), 0) geo_complete_count,
+          COALESCE(SUM(CASE WHEN longitude IS NULL OR latitude IS NULL THEN 1 ELSE 0 END), 0) geo_missing_count
+        FROM app_user.water_source_info WHERE del_flag = '0'
+        UNION ALL
+        SELECT 'plant', '水厂', COUNT(*), COALESCE(SUM(CASE WHEN longitude IS NOT NULL AND latitude IS NOT NULL THEN 1 ELSE 0 END), 0), COALESCE(SUM(CASE WHEN longitude IS NULL OR latitude IS NULL THEN 1 ELSE 0 END), 0)
+        FROM app_user.water_plant_info WHERE del_flag = '0'
+        UNION ALL
+        SELECT 'pumpStation', '泵站', COUNT(*), COALESCE(SUM(CASE WHEN longitude IS NOT NULL AND latitude IS NOT NULL THEN 1 ELSE 0 END), 0), COALESCE(SUM(CASE WHEN longitude IS NULL OR latitude IS NULL THEN 1 ELSE 0 END), 0)
+        FROM app_user.water_pump_station WHERE del_flag = '0'
+        UNION ALL
+        SELECT 'pipe', '供水管网', COUNT(*), COALESCE(SUM(CASE WHEN start_longitude IS NOT NULL AND start_latitude IS NOT NULL AND end_longitude IS NOT NULL AND end_latitude IS NOT NULL THEN 1 ELSE 0 END), 0), COALESCE(SUM(CASE WHEN start_longitude IS NULL OR start_latitude IS NULL OR end_longitude IS NULL OR end_latitude IS NULL THEN 1 ELSE 0 END), 0)
+        FROM app_user.water_pipe_info WHERE del_flag = '0'
+        UNION ALL
+        SELECT 'user', '大用水户', COUNT(*), COALESCE(SUM(CASE WHEN longitude IS NOT NULL AND latitude IS NOT NULL THEN 1 ELSE 0 END), 0), COALESCE(SUM(CASE WHEN longitude IS NULL OR latitude IS NULL THEN 1 ELSE 0 END), 0)
+        FROM app_user.water_user_info WHERE del_flag = '0'
+    </select>
+</mapper>

+ 4 - 2
pipe-network-service/zksy-system/src/main/resources/mapper/WaterSupply/WaterUserInfo/WaterUserInfoMapper.xml

@@ -9,6 +9,8 @@
         <result column="user_name" property="userName"/>
         <result column="user_type" property="userType"/>
         <result column="address" property="address"/>
+        <result column="longitude" property="longitude"/>
+        <result column="latitude" property="latitude"/>
         <result column="contact_person" property="contactPerson"/>
         <result column="contact_phone" property="contactPhone"/>
         <result column="meter_number" property="meterNumber"/>
@@ -25,7 +27,7 @@
     </resultMap>
 
     <sql id="Base_Column_List">
-        id, user_code, user_name, user_type, address, contact_person, contact_phone,
+        id, user_code, user_name, user_type, address, longitude, latitude, contact_person, contact_phone,
         meter_number, initial_reading, current_reading, annual_water_consumption,
         status, remark, del_flag, create_by, create_time, update_by, update_time
     </sql>
@@ -95,4 +97,4 @@
         </foreach>
     </delete>
 
-</mapper>
+</mapper>

+ 117 - 0
pipe-network-service/zksy-system/src/test/java/com/zksy/WaterSupply/FacilityCoordinateModelTest.java

@@ -0,0 +1,117 @@
+package com.zksy.WaterSupply;
+
+import com.zksy.WaterSupply.WaterPipeInfo.domain.WaterPipeInfo;
+import com.zksy.WaterSupply.WaterPlantInfo.domain.WaterPlantInfo;
+import org.junit.jupiter.api.Test;
+
+import java.io.InputStream;
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class FacilityCoordinateModelTest {
+
+    @Test
+    void pointFacilityRetainsWgs84LongitudeAndLatitude() {
+        WaterPlantInfo plant = new WaterPlantInfo();
+        plant.setLongitude(new BigDecimal("113.12345678"));
+        plant.setLatitude(new BigDecimal("23.12345678"));
+
+        assertEquals(new BigDecimal("113.12345678"), plant.getLongitude());
+        assertEquals(new BigDecimal("23.12345678"), plant.getLatitude());
+    }
+
+    @Test
+    void pipeRetainsBothWgs84LineEndpoints() {
+        WaterPipeInfo pipe = new WaterPipeInfo();
+        pipe.setStartLongitude(new BigDecimal("113.10000000"));
+        pipe.setStartLatitude(new BigDecimal("23.10000000"));
+        pipe.setEndLongitude(new BigDecimal("113.20000000"));
+        pipe.setEndLatitude(new BigDecimal("23.20000000"));
+
+        assertEquals(new BigDecimal("113.10000000"), pipe.getStartLongitude());
+        assertEquals(new BigDecimal("23.10000000"), pipe.getStartLatitude());
+        assertEquals(new BigDecimal("113.20000000"), pipe.getEndLongitude());
+        assertEquals(new BigDecimal("23.20000000"), pipe.getEndLatitude());
+    }
+
+    @Test
+    void facilityMappersSelectCoordinateColumns() throws Exception {
+        Map<String, String[]> expectedColumns = Map.of(
+                "mapper/WaterSupply/WaterSourceInfo/WaterSourceInfoMapper.xml", new String[]{"longitude", "latitude"},
+                "mapper/WaterSupply/WaterPlantInfo/WaterPlantInfoMapper.xml", new String[]{"longitude", "latitude"},
+                "mapper/WaterSupply/WaterPumpStation/WaterPumpStationMapper.xml", new String[]{"longitude", "latitude"},
+                "mapper/WaterSupply/WaterUserInfo/WaterUserInfoMapper.xml", new String[]{"longitude", "latitude"},
+                "mapper/WaterSupply/WaterPipeInfo/WaterPipeInfoMapper.xml", new String[]{"start_longitude", "start_latitude", "end_longitude", "end_latitude"}
+        );
+
+        for (Map.Entry<String, String[]> mapper : expectedColumns.entrySet()) {
+            try (InputStream input = getClass().getClassLoader().getResourceAsStream(mapper.getKey())) {
+                assertNotNull(input, mapper.getKey());
+                String xml = new String(input.readAllBytes(), StandardCharsets.UTF_8);
+                for (String column : mapper.getValue()) {
+                    assertTrue(xml.contains("column=\"" + column + "\""), mapper.getKey() + " resultMap should map " + column);
+                    assertTrue(xml.contains(column), mapper.getKey() + " column list should include " + column);
+                }
+            }
+        }
+    }
+
+    @Test
+    void statisticsMapperQuotesKingbaseReservedValueAlias() throws Exception {
+        try (InputStream input = getClass().getClassLoader().getResourceAsStream(
+                "mapper/WaterSupply/PipeNetworkStatistics/PipeNetworkStatisticsMapper.xml")) {
+            assertNotNull(input);
+            String xml = new String(input.readAllBytes(), StandardCharsets.UTF_8);
+            assertFalse(xml.contains("COUNT(*) value"), "KingbaseES reserves value as a keyword");
+            assertTrue(xml.contains("COUNT(*) \"value\""));
+            assertFalse(xml.contains("ORDER BY value"), "Reserved aliases must also be quoted in ORDER BY");
+        }
+    }
+
+    @Test
+    void statisticsMapperKeepsLayingYearDistributionTextTyped() throws Exception {
+        try (InputStream input = getClass().getClassLoader().getResourceAsStream(
+                "mapper/WaterSupply/PipeNetworkStatistics/PipeNetworkStatisticsMapper.xml")) {
+            assertNotNull(input);
+            String xml = new String(input.readAllBytes(), StandardCharsets.UTF_8);
+            assertTrue(xml.contains("CAST('未填报' AS VARCHAR)"));
+            assertFalse(xml.contains("CAST(laying_year AS INTEGER)"), "DDL declares laying_year as int4");
+            assertTrue(xml.contains("CONCAT("), "laying year labels must use a text-returning concatenation function");
+        }
+    }
+
+    @Test
+    void statisticsMapperUsesTheAppUserWaterSupplyTables() throws Exception {
+        try (InputStream input = getClass().getClassLoader().getResourceAsStream(
+                "mapper/WaterSupply/PipeNetworkStatistics/PipeNetworkStatisticsMapper.xml")) {
+            assertNotNull(input);
+            String xml = new String(input.readAllBytes(), StandardCharsets.UTF_8);
+            assertFalse(xml.matches("(?s).*\\bFROM\\s+(?!app_user\\.)water_(?:source_info|plant_info|pump_station|pipe_info|user_info)\\b.*"),
+                    "statistics SQL must not resolve water supply tables through search_path");
+            assertFalse(xml.matches("(?s).*<choose>.*>water_(?:source_info|plant_info|pump_station|pipe_info|user_info)</when>.*"),
+                    "status distribution SQL must use app_user-qualified table names");
+        }
+    }
+
+    @Test
+    void gisMapperQuotesLocationAliasAndUsesAppUserSchema() throws Exception {
+        try (InputStream input = getClass().getClassLoader().getResourceAsStream(
+                "mapper/WaterSupply/WaterSupplyGisMap/WaterSupplyGisMapMapper.xml")) {
+            assertNotNull(input);
+            String xml = new String(input.readAllBytes(), StandardCharsets.UTF_8);
+            assertTrue(xml.contains("address AS \"location\""));
+            assertTrue(xml.contains("start_point AS \"location\""));
+            assertTrue(xml.contains("status, \"location\", longitude, latitude"));
+            assertFalse(xml.contains("address location"));
+            assertFalse(xml.contains("start_point location"));
+            assertFalse(xml.matches("(?s).*\\bFROM\\s+(?!app_user\\.)water_(?:source_info|plant_info|pump_station|pipe_info|user_info)\\b.*"),
+                    "GIS SQL must not resolve water supply tables through search_path");
+        }
+    }
+}

Некоторые файлы не были показаны из-за большого количества измененных файлов