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

feat(wm-patrol): #88 巡查APP(任务/工单/统计/问题上报/个人中心)

- PatrolAppService: 统一APP服务(PAT-21~PAT-29)
  - getAppOverview: APP总览(今日任务/待办工单/本月完成/里程/异常)
  - getTodayTasks: 今日任务(含进度/路线/时长)
  - getPendingWorkOrders: 待办工单
  - getUserStats: 个人巡检统计(week/month/year)
  - getTaskList: 任务列表(状态筛选+分页)
  - getWorkOrderList: 工单列表(状态筛选+分页)
  - reportIssue: 问题上报(文字+照片+语音+GPS)
  - getProfile/updateAvatar: 个人中心
  - getHistoryTasks: 历史任务
- PatrolAppController: /patrol/app 统一入口
- V88__patrol_app.sql: patrol_user_stats_monthly + voice_url + avatar
bot_dev2 4 дней назад
Родитель
Сommit
3a76b21504
83 измененных файлов: 4582 добавлений и 0 удалений
  1. 1
    0
      sql/V2__payment_enhancement.sql
  2. 23
    0
      sql/V88__patrol_app.sql
  3. 133
    0
      sql/V_meter_lifecycle.sql
  4. 106
    0
      sql/V_smart_meter_sms_alipay.sql
  5. 11
    0
      sql/create_sequences.sql
  6. 136
    0
      sql/enhanced_reading_tables.sql
  7. 112
    0
      sql/problem_reporting.sql
  8. 178
    0
      sql/revenue_tables.sql
  9. 16
    0
      wm-patrol/pom.xml
  10. 11
    0
      wm-patrol/src/main/java/com/water/patrol/PatrolApplication.java
  11. 127
    0
      wm-patrol/src/main/java/com/water/patrol/controller/PatrolAppController.java
  12. 12
    0
      wm-patrol/src/main/java/com/water/patrol/controller/PatrolAppIssueController.java
  13. 10
    0
      wm-patrol/src/main/java/com/water/patrol/controller/PatrolAppProfileController.java
  14. 14
    0
      wm-patrol/src/main/java/com/water/patrol/controller/PatrolAppTaskController.java
  15. 13
    0
      wm-patrol/src/main/java/com/water/patrol/controller/PatrolAppWoController.java
  16. 16
    0
      wm-patrol/src/main/java/com/water/patrol/controller/PatrolAreaController.java
  17. 100
    0
      wm-patrol/src/main/java/com/water/patrol/controller/PatrolController.java
  18. 186
    0
      wm-patrol/src/main/java/com/water/patrol/controller/PatrolCoreController.java
  19. 16
    0
      wm-patrol/src/main/java/com/water/patrol/controller/PatrolDeviceController.java
  20. 15
    0
      wm-patrol/src/main/java/com/water/patrol/controller/PatrolFormController.java
  21. 15
    0
      wm-patrol/src/main/java/com/water/patrol/controller/PatrolLedgerController.java
  22. 11
    0
      wm-patrol/src/main/java/com/water/patrol/controller/PatrolOverviewController.java
  23. 126
    0
      wm-patrol/src/main/java/com/water/patrol/controller/PatrolProblemController.java
  24. 16
    0
      wm-patrol/src/main/java/com/water/patrol/controller/PatrolRouteSetupController.java
  25. 16
    0
      wm-patrol/src/main/java/com/water/patrol/controller/PatrolTemplateController.java
  26. 13
    0
      wm-patrol/src/main/java/com/water/patrol/controller/PatrolTrackController.java
  27. 17
    0
      wm-patrol/src/main/java/com/water/patrol/controller/PatrolWoController.java
  28. 151
    0
      wm-patrol/src/main/java/com/water/patrol/controller/WorkOrderController.java
  29. 15
    0
      wm-patrol/src/main/java/com/water/patrol/entity/PatrolArea.java
  30. 16
    0
      wm-patrol/src/main/java/com/water/patrol/entity/PatrolDevice.java
  31. 13
    0
      wm-patrol/src/main/java/com/water/patrol/entity/PatrolForm.java
  32. 19
    0
      wm-patrol/src/main/java/com/water/patrol/entity/PatrolIssueReport.java
  33. 31
    0
      wm-patrol/src/main/java/com/water/patrol/entity/PatrolProblem.java
  34. 15
    0
      wm-patrol/src/main/java/com/water/patrol/entity/PatrolRouteSetup.java
  35. 15
    0
      wm-patrol/src/main/java/com/water/patrol/entity/PatrolTask.java
  36. 15
    0
      wm-patrol/src/main/java/com/water/patrol/entity/PatrolTemplate.java
  37. 12
    0
      wm-patrol/src/main/java/com/water/patrol/entity/PatrolTrackPoint.java
  38. 14
    0
      wm-patrol/src/main/java/com/water/patrol/entity/PatrolWorkOrder.java
  39. 37
    0
      wm-patrol/src/main/java/com/water/patrol/entity/WorkOrder.java
  40. 18
    0
      wm-patrol/src/main/java/com/water/patrol/entity/WorkOrderProcess.java
  41. 5
    0
      wm-patrol/src/main/java/com/water/patrol/mapper/PatrolAreaMapper.java
  42. 5
    0
      wm-patrol/src/main/java/com/water/patrol/mapper/PatrolDeviceMapper.java
  43. 5
    0
      wm-patrol/src/main/java/com/water/patrol/mapper/PatrolFormMapper.java
  44. 5
    0
      wm-patrol/src/main/java/com/water/patrol/mapper/PatrolIssueReportMapper.java
  45. 47
    0
      wm-patrol/src/main/java/com/water/patrol/mapper/PatrolProblemMapper.java
  46. 5
    0
      wm-patrol/src/main/java/com/water/patrol/mapper/PatrolRouteSetupMapper.java
  47. 5
    0
      wm-patrol/src/main/java/com/water/patrol/mapper/PatrolTaskMapper.java
  48. 5
    0
      wm-patrol/src/main/java/com/water/patrol/mapper/PatrolTemplateMapper.java
  49. 5
    0
      wm-patrol/src/main/java/com/water/patrol/mapper/PatrolTrackPointMapper.java
  50. 5
    0
      wm-patrol/src/main/java/com/water/patrol/mapper/PatrolWorkOrderMapper.java
  51. 60
    0
      wm-patrol/src/main/java/com/water/patrol/mapper/WorkOrderMapper.java
  52. 25
    0
      wm-patrol/src/main/java/com/water/patrol/mapper/WorkOrderProcessMapper.java
  53. 30
    0
      wm-patrol/src/main/java/com/water/patrol/service/PatrolAppIssueService.java
  54. 23
    0
      wm-patrol/src/main/java/com/water/patrol/service/PatrolAppProfileService.java
  55. 399
    0
      wm-patrol/src/main/java/com/water/patrol/service/PatrolAppService.java
  56. 42
    0
      wm-patrol/src/main/java/com/water/patrol/service/PatrolAppTaskService.java
  57. 36
    0
      wm-patrol/src/main/java/com/water/patrol/service/PatrolAppWorkOrderService.java
  58. 53
    0
      wm-patrol/src/main/java/com/water/patrol/service/PatrolAreaService.java
  59. 252
    0
      wm-patrol/src/main/java/com/water/patrol/service/PatrolCoreService.java
  60. 106
    0
      wm-patrol/src/main/java/com/water/patrol/service/PatrolDeviceService.java
  61. 45
    0
      wm-patrol/src/main/java/com/water/patrol/service/PatrolFormService.java
  62. 50
    0
      wm-patrol/src/main/java/com/water/patrol/service/PatrolLedgerService.java
  63. 30
    0
      wm-patrol/src/main/java/com/water/patrol/service/PatrolOverviewService.java
  64. 82
    0
      wm-patrol/src/main/java/com/water/patrol/service/PatrolProblemService.java
  65. 55
    0
      wm-patrol/src/main/java/com/water/patrol/service/PatrolRouteSetupService.java
  66. 118
    0
      wm-patrol/src/main/java/com/water/patrol/service/PatrolService.java
  67. 59
    0
      wm-patrol/src/main/java/com/water/patrol/service/PatrolTemplateService.java
  68. 47
    0
      wm-patrol/src/main/java/com/water/patrol/service/PatrolTrackService.java
  69. 122
    0
      wm-patrol/src/main/java/com/water/patrol/service/PatrolWoService.java
  70. 114
    0
      wm-patrol/src/main/java/com/water/patrol/service/PatrolWorkOrderService.java
  71. 100
    0
      wm-patrol/src/main/java/com/water/patrol/service/WorkOrderService.java
  72. 109
    0
      wm-patrol/src/main/java/com/water/patrol/service/impl/PatrolProblemServiceImpl.java
  73. 241
    0
      wm-patrol/src/main/java/com/water/patrol/service/impl/WorkOrderServiceImpl.java
  74. 17
    0
      wm-patrol/src/main/resources/application.yml
  75. 103
    0
      wm-patrol/src/main/resources/mapper/PatrolProblemMapper.xml
  76. 148
    0
      wm-patrol/src/main/resources/mapper/WorkOrderMapper.xml
  77. 45
    0
      wm-patrol/src/main/resources/mapper/WorkOrderProcessMapper.xml
  78. 35
    0
      wm-patrol/src/main/resources/sql/V86__patrol_core.sql
  79. 24
    0
      wm-patrol/src/main/resources/sql/V87__patrol_setup.sql
  80. 23
    0
      wm-patrol/src/main/resources/sql/V88__patrol_app.sql
  81. 24
    0
      wm-patrol/src/test/java/com/water/patrol/PatrolAppTest.java
  82. 27
    0
      wm-patrol/src/test/java/com/water/patrol/PatrolCoreTest.java
  83. 25
    0
      wm-patrol/src/test/java/com/water/patrol/PatrolSetupTest.java

+ 1
- 0
sql/V2__payment_enhancement.sql
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 23
- 0
sql/V88__patrol_app.sql Просмотреть файл

@@ -0,0 +1,23 @@
1
+-- V88: 巡查APP相关表 - Issue #88
2
+-- PAT-21~PAT-29
3
+
4
+-- 用户巡检统计(按月汇总)
5
+CREATE TABLE IF NOT EXISTS patrol_user_stats_monthly (
6
+    id              BIGSERIAL PRIMARY KEY,
7
+    user_id         BIGINT NOT NULL,
8
+    year_month      VARCHAR(7) NOT NULL,  -- '2026-06'
9
+    completed_tasks INT DEFAULT 0,
10
+    total_distance  DOUBLE PRECISION DEFAULT 0,
11
+    issue_count     INT DEFAULT 0,
12
+    patrol_hours    DOUBLE PRECISION DEFAULT 0,
13
+    created_at      TIMESTAMPTZ DEFAULT NOW(),
14
+    updated_at      TIMESTAMPTZ DEFAULT NOW(),
15
+    UNIQUE(user_id, year_month)
16
+);
17
+CREATE INDEX IF NOT EXISTS idx_patrol_stats_user ON patrol_user_stats_monthly(user_id, year_month);
18
+
19
+-- 问题上报表加 voice_url 字段(patrol_problem 已在 problem_reporting.sql 创建)
20
+ALTER TABLE patrol_problem ADD COLUMN IF NOT EXISTS voice_url VARCHAR(500);
21
+
22
+-- 用户头像(如果 sys_user 表无 avatar 字段则添加)
23
+ALTER TABLE sys_user ADD COLUMN IF NOT EXISTS avatar VARCHAR(500);

+ 133
- 0
sql/V_meter_lifecycle.sql Просмотреть файл

@@ -0,0 +1,133 @@
1
+-- ============================================================
2
+-- 水表全生命周期管理 DDL
3
+-- 包含: rev_water_meter, rev_meter_install_record,
4
+--       rev_meter_replace_record, rev_meter_lifecycle_log
5
+-- ============================================================
6
+
7
+-- 1. 水表主表
8
+CREATE TABLE IF NOT EXISTS rev_water_meter (
9
+    id              BIGSERIAL       PRIMARY KEY,
10
+    meter_no        VARCHAR(64)     NOT NULL UNIQUE,
11
+    model           VARCHAR(64),
12
+    diameter        INTEGER,
13
+    manufacturer    VARCHAR(128),
14
+    production_date DATE,
15
+    install_date    DATE,
16
+    install_address VARCHAR(512),
17
+    customer_no     VARCHAR(64),
18
+    status          VARCHAR(32)     NOT NULL DEFAULT 'IN_STOCK',
19
+    in_stock_time   TIMESTAMP,
20
+    out_stock_time  TIMESTAMP,
21
+    initial_reading NUMERIC(14,4)   DEFAULT 0,
22
+    current_reading NUMERIC(14,4)   DEFAULT 0,
23
+    dismantle_date  DATE,
24
+    scrapped_date   DATE,
25
+    remark          VARCHAR(512),
26
+    created_at      TIMESTAMP       NOT NULL DEFAULT CURRENT_TIMESTAMP,
27
+    updated_at      TIMESTAMP       NOT NULL DEFAULT CURRENT_TIMESTAMP
28
+);
29
+
30
+COMMENT ON TABLE  rev_water_meter                     IS '水表主表';
31
+COMMENT ON COLUMN rev_water_meter.meter_no            IS '水表编号';
32
+COMMENT ON COLUMN rev_water_meter.model               IS '水表型号';
33
+COMMENT ON COLUMN rev_water_meter.diameter            IS '口径(mm)';
34
+COMMENT ON COLUMN rev_water_meter.manufacturer        IS '制造商';
35
+COMMENT ON COLUMN rev_water_meter.production_date     IS '生产日期';
36
+COMMENT ON COLUMN rev_water_meter.install_date        IS '安装日期';
37
+COMMENT ON COLUMN rev_water_meter.install_address     IS '安装地址';
38
+COMMENT ON COLUMN rev_water_meter.customer_no         IS '客户编号';
39
+COMMENT ON COLUMN rev_water_meter.status              IS '状态: IN_STOCK/INSTALLED/DISMANTLED/SCRAPPED/REPAIRING';
40
+COMMENT ON COLUMN rev_water_meter.in_stock_time       IS '入库时间';
41
+COMMENT ON COLUMN rev_water_meter.out_stock_time      IS '出库时间';
42
+COMMENT ON COLUMN rev_water_meter.initial_reading     IS '初始读数';
43
+COMMENT ON COLUMN rev_water_meter.current_reading     IS '当前读数';
44
+COMMENT ON COLUMN rev_water_meter.dismantle_date      IS '拆除日期';
45
+COMMENT ON COLUMN rev_water_meter.scrapped_date       IS '报废日期';
46
+
47
+-- 2. 水表安装记录
48
+CREATE TABLE IF NOT EXISTS rev_meter_install_record (
49
+    id              BIGSERIAL       PRIMARY KEY,
50
+    meter_no        VARCHAR(64)     NOT NULL,
51
+    customer_no     VARCHAR(64),
52
+    installer       VARCHAR(128),
53
+    install_date    DATE,
54
+    install_address VARCHAR(512),
55
+    old_meter_no    VARCHAR(64),
56
+    initial_reading NUMERIC(14,4)   DEFAULT 0,
57
+    remark          VARCHAR(512),
58
+    created_at      TIMESTAMP       NOT NULL DEFAULT CURRENT_TIMESTAMP,
59
+    updated_at      TIMESTAMP       NOT NULL DEFAULT CURRENT_TIMESTAMP
60
+);
61
+
62
+COMMENT ON TABLE rev_meter_install_record IS '水表安装记录';
63
+
64
+-- 3. 水表换表记录
65
+CREATE TABLE IF NOT EXISTS rev_meter_replace_record (
66
+    id               BIGSERIAL       PRIMARY KEY,
67
+    old_meter_no     VARCHAR(64)     NOT NULL,
68
+    new_meter_no     VARCHAR(64)     NOT NULL,
69
+    customer_no      VARCHAR(64),
70
+    replace_type     VARCHAR(32)     NOT NULL,
71
+    reason           VARCHAR(512),
72
+    replacer         VARCHAR(128),
73
+    replace_date     DATE,
74
+    old_reading      NUMERIC(14,4),
75
+    new_reading      NUMERIC(14,4),
76
+    approval_status  VARCHAR(32)     DEFAULT 'APPROVED',
77
+    approver         VARCHAR(128),
78
+    approval_time    TIMESTAMP,
79
+    remark           VARCHAR(512),
80
+    created_at       TIMESTAMP       NOT NULL DEFAULT CURRENT_TIMESTAMP,
81
+    updated_at       TIMESTAMP       NOT NULL DEFAULT CURRENT_TIMESTAMP
82
+);
83
+
84
+COMMENT ON TABLE  rev_meter_replace_record                  IS '水表换表记录';
85
+COMMENT ON COLUMN rev_meter_replace_record.replace_type     IS '换表类型: FAULT-故障, EXPIRED-到期';
86
+COMMENT ON COLUMN rev_meter_replace_record.approval_status  IS '审批状态: PENDING/APPROVED/REJECTED';
87
+
88
+-- 4. 水表生命周期日志
89
+CREATE TABLE IF NOT EXISTS rev_meter_lifecycle_log (
90
+    id              BIGSERIAL       PRIMARY KEY,
91
+    meter_no        VARCHAR(64)     NOT NULL,
92
+    action_type     VARCHAR(32)     NOT NULL,
93
+    action_time     TIMESTAMP       NOT NULL DEFAULT CURRENT_TIMESTAMP,
94
+    operator        VARCHAR(128),
95
+    detail          VARCHAR(1024),
96
+    old_meter_no    VARCHAR(64),
97
+    new_meter_no    VARCHAR(64),
98
+    customer_no     VARCHAR(64),
99
+    remark          VARCHAR(512),
100
+    created_at      TIMESTAMP       NOT NULL DEFAULT CURRENT_TIMESTAMP
101
+);
102
+
103
+COMMENT ON TABLE  rev_meter_lifecycle_log                IS '水表生命周期日志';
104
+COMMENT ON COLUMN rev_meter_lifecycle_log.action_type    IS '操作类型: STOCK_IN/INSTALL/DISMANTLE/REPLACE/SCRAP/REPAIR';
105
+
106
+-- ============================================================
107
+-- 索引
108
+-- ============================================================
109
+
110
+-- rev_water_meter 索引
111
+CREATE INDEX IF NOT EXISTS idx_water_meter_status       ON rev_water_meter (status);
112
+CREATE INDEX IF NOT EXISTS idx_water_meter_customer     ON rev_water_meter (customer_no);
113
+CREATE INDEX IF NOT EXISTS idx_water_meter_diameter     ON rev_water_meter (diameter);
114
+CREATE INDEX IF NOT EXISTS idx_water_meter_manufacturer ON rev_water_meter (manufacturer);
115
+CREATE INDEX IF NOT EXISTS idx_water_meter_created      ON rev_water_meter (created_at);
116
+
117
+-- rev_meter_install_record 索引
118
+CREATE INDEX IF NOT EXISTS idx_install_record_meter     ON rev_meter_install_record (meter_no);
119
+CREATE INDEX IF NOT EXISTS idx_install_record_customer  ON rev_meter_install_record (customer_no);
120
+CREATE INDEX IF NOT EXISTS idx_install_record_date      ON rev_meter_install_record (install_date);
121
+
122
+-- rev_meter_replace_record 索引
123
+CREATE INDEX IF NOT EXISTS idx_replace_record_old       ON rev_meter_replace_record (old_meter_no);
124
+CREATE INDEX IF NOT EXISTS idx_replace_record_new       ON rev_meter_replace_record (new_meter_no);
125
+CREATE INDEX IF NOT EXISTS idx_replace_record_customer  ON rev_meter_replace_record (customer_no);
126
+CREATE INDEX IF NOT EXISTS idx_replace_record_approval  ON rev_meter_replace_record (approval_status);
127
+CREATE INDEX IF NOT EXISTS idx_replace_record_date      ON rev_meter_replace_record (replace_date);
128
+
129
+-- rev_meter_lifecycle_log 索引
130
+CREATE INDEX IF NOT EXISTS idx_lifecycle_log_meter      ON rev_meter_lifecycle_log (meter_no);
131
+CREATE INDEX IF NOT EXISTS idx_lifecycle_log_action     ON rev_meter_lifecycle_log (action_type);
132
+CREATE INDEX IF NOT EXISTS idx_lifecycle_log_time       ON rev_meter_lifecycle_log (action_time);
133
+CREATE INDEX IF NOT EXISTS idx_lifecycle_log_customer   ON rev_meter_lifecycle_log (customer_no);

+ 106
- 0
sql/V_smart_meter_sms_alipay.sql Просмотреть файл

@@ -0,0 +1,106 @@
1
+-- =====================================================
2
+-- V_smart_meter_sms_alipay.sql
3
+-- 智能表平台 + 短信平台 + 支付宝生活缴费 DDL
4
+-- =====================================================
5
+
6
+-- 1. 智能水表表
7
+CREATE TABLE IF NOT EXISTS rev_smart_meter (
8
+    id              BIGSERIAL PRIMARY KEY,
9
+    meter_no        VARCHAR(64)   NOT NULL,
10
+    customer_no     VARCHAR(64),
11
+    signal_strength INTEGER       DEFAULT 100,
12
+    battery_level   INTEGER       DEFAULT 100,
13
+    valve_status    VARCHAR(16)   DEFAULT 'OPEN',
14
+    last_report_time TIMESTAMP,
15
+    online_status   VARCHAR(16)   DEFAULT 'ONLINE',
16
+    current_reading DOUBLE PRECISION DEFAULT 0,
17
+    install_address VARCHAR(256),
18
+    area_code       VARCHAR(32),
19
+    remark          VARCHAR(512),
20
+    create_time     TIMESTAMP     DEFAULT CURRENT_TIMESTAMP,
21
+    update_time     TIMESTAMP     DEFAULT CURRENT_TIMESTAMP
22
+);
23
+
24
+CREATE INDEX IF NOT EXISTS idx_smart_meter_meter_no ON rev_smart_meter(meter_no);
25
+CREATE INDEX IF NOT EXISTS idx_smart_meter_customer_no ON rev_smart_meter(customer_no);
26
+CREATE INDEX IF NOT EXISTS idx_smart_meter_online_status ON rev_smart_meter(online_status);
27
+CREATE INDEX IF NOT EXISTS idx_smart_meter_area_code ON rev_smart_meter(area_code);
28
+
29
+-- 2. 短信模板表
30
+CREATE TABLE IF NOT EXISTS rev_sms_template (
31
+    id              BIGSERIAL PRIMARY KEY,
32
+    template_name   VARCHAR(128)  NOT NULL,
33
+    template_type   VARCHAR(32)   NOT NULL,
34
+    content         TEXT          NOT NULL,
35
+    variables       VARCHAR(512),
36
+    enabled         INTEGER       DEFAULT 1,
37
+    remark          VARCHAR(512),
38
+    create_time     TIMESTAMP     DEFAULT CURRENT_TIMESTAMP,
39
+    update_time     TIMESTAMP     DEFAULT CURRENT_TIMESTAMP
40
+);
41
+
42
+CREATE INDEX IF NOT EXISTS idx_sms_template_type ON rev_sms_template(template_type);
43
+
44
+-- 3. 短信发送记录表
45
+CREATE TABLE IF NOT EXISTS rev_sms_record (
46
+    id              BIGSERIAL PRIMARY KEY,
47
+    phone           VARCHAR(32)   NOT NULL,
48
+    content         TEXT,
49
+    template_id     BIGINT,
50
+    send_status     VARCHAR(16)   DEFAULT 'PENDING',
51
+    send_time       TIMESTAMP,
52
+    error_msg       VARCHAR(512),
53
+    customer_no     VARCHAR(64),
54
+    biz_type        VARCHAR(32),
55
+    create_time     TIMESTAMP     DEFAULT CURRENT_TIMESTAMP
56
+);
57
+
58
+CREATE INDEX IF NOT EXISTS idx_sms_record_phone ON rev_sms_record(phone);
59
+CREATE INDEX IF NOT EXISTS idx_sms_record_send_status ON rev_sms_record(send_status);
60
+CREATE INDEX IF NOT EXISTS idx_sms_record_send_time ON rev_sms_record(send_time);
61
+CREATE INDEX IF NOT EXISTS idx_sms_record_customer_no ON rev_sms_record(customer_no);
62
+
63
+-- 4. 支付宝生活缴费订单表
64
+CREATE TABLE IF NOT EXISTS rev_alipay_order (
65
+    id              BIGSERIAL PRIMARY KEY,
66
+    out_trade_no    VARCHAR(128)  NOT NULL,
67
+    alipay_trade_no VARCHAR(128),
68
+    customer_no     VARCHAR(64),
69
+    bill_id         BIGINT,
70
+    amount          DECIMAL(12, 2),
71
+    status          VARCHAR(16)   DEFAULT 'CREATED',
72
+    notify_data     TEXT,
73
+    bill_period     VARCHAR(32),
74
+    pay_time        TIMESTAMP,
75
+    refund_amount   DECIMAL(12, 2),
76
+    refund_time     TIMESTAMP,
77
+    remark          VARCHAR(512),
78
+    create_time     TIMESTAMP     DEFAULT CURRENT_TIMESTAMP,
79
+    update_time     TIMESTAMP     DEFAULT CURRENT_TIMESTAMP
80
+);
81
+
82
+CREATE UNIQUE INDEX IF NOT EXISTS idx_alipay_order_out_trade_no ON rev_alipay_order(out_trade_no);
83
+CREATE INDEX IF NOT EXISTS idx_alipay_order_customer_no ON rev_alipay_order(customer_no);
84
+CREATE INDEX IF NOT EXISTS idx_alipay_order_status ON rev_alipay_order(status);
85
+CREATE INDEX IF NOT EXISTS idx_alipay_order_pay_time ON rev_alipay_order(pay_time);
86
+
87
+-- =====================================================
88
+-- 默认短信模板数据
89
+-- =====================================================
90
+
91
+INSERT INTO rev_sms_template (template_name, template_type, content, variables, enabled, remark) VALUES
92
+('账单通知', 'BILL_NOTICE',
93
+ '【XX水务】尊敬的${customerName},您${billPeriod}的水费账单已出,应缴金额${amount}元,请及时缴费。',
94
+ '["customerName","billPeriod","amount"]', 1, '月度账单通知'),
95
+
96
+('欠费提醒', 'ARREARS_WARNING',
97
+ '【XX水务】尊敬的${customerName},您有${overdueAmount}元水费已逾期${overdueDays}天,请尽快缴清,逾期将影响正常用水。',
98
+ '["customerName","overdueAmount","overdueDays"]', 1, '欠费催缴提醒'),
99
+
100
+('阀门控制通知', 'VALVE_CONTROL',
101
+ '【XX水务】尊敬的${customerName},您编号为${meterNo}的水表阀门已${action},如有疑问请联系客服。',
102
+ '["customerName","meterNo","action"]', 1, '远程阀门操作通知'),
103
+
104
+('停水通知', 'GENERAL',
105
+ '【XX水务】${areaName}将于${startTime}至${endTime}进行${reason},届时将暂停供水,请提前做好储水准备。',
106
+ '["areaName","startTime","endTime","reason"]', 1, '通用停水通知');

+ 11
- 0
sql/create_sequences.sql Просмотреть файл

@@ -0,0 +1,11 @@
1
+-- 创建巡检问题序列
2
+CREATE SEQUENCE IF NOT EXISTS seq_patrol_problem
3
+INCREMENT 1
4
+START 1
5
+NO CYCLE;
6
+
7
+-- 创建工单序列
8
+CREATE SEQUENCE IF NOT EXISTS seq_work_order
9
+INCREMENT 1
10
+START 1
11
+NO CYCLE;

+ 136
- 0
sql/enhanced_reading_tables.sql Просмотреть файл

@@ -0,0 +1,136 @@
1
+-- 增强抄表功能相关表结构
2
+
3
+-- 1. 批量抄表报告表
4
+CREATE TABLE IF NOT EXISTS rev_batch_report (
5
+    report_id VARCHAR(100) PRIMARY KEY,
6
+    period VARCHAR(7) NOT NULL COMMENT '抄表周期 yyyy-MM',
7
+    total_meters INTEGER NOT NULL DEFAULT 0 COMMENT '总表数',
8
+    success_meters INTEGER NOT NULL DEFAULT 0 COMMENT '成功抄表数',
9
+    failed_meters INTEGER NOT NULL DEFAULT 0 COMMENT '失败抄表数',
10
+    abnormal_meters INTEGER NOT NULL DEFAULT 0 COMMENT '异常读数数',
11
+    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
12
+    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
13
+);
14
+
15
+-- 2. 抄表异常记录表
16
+CREATE TABLE IF NOT EXISTS rev_reading_exception (
17
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
18
+    meter_id BIGINT NOT NULL,
19
+    meter_no VARCHAR(50) NOT NULL,
20
+    exception_type VARCHAR(50) NOT NULL COMMENT '异常类型: DECREASE/NEGATIVE/EXCESSIVE/ZERO',
21
+    exception_reason TEXT COMMENT '异常原因描述',
22
+    prev_reading DECIMAL(12,2) NOT NULL,
23
+    curr_reading DECIMAL(12,2) NOT NULL,
24
+    consumption DECIMAL(12,2) NOT NULL,
25
+    reading_date DATE NOT NULL,
26
+    area VARCHAR(100) NOT NULL,
27
+    is_resolved BOOLEAN DEFAULT FALSE COMMENT '是否已处理',
28
+    resolved_at TIMESTAMP NULL,
29
+    resolved_by VARCHAR(100) NULL,
30
+    remark TEXT COMMENT '处理备注',
31
+    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
32
+    INDEX idx_meter_id (meter_id),
33
+    INDEX idx_reading_date (reading_date),
34
+    INDEX idx_exception_type (exception_type),
35
+    INDEX idx_area (area)
36
+);
37
+
38
+-- 3. 大表监控记录表
39
+CREATE TABLE IF NOT EXISTS rev_large_meter_monitor (
40
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
41
+    meter_id BIGINT NOT NULL,
42
+    meter_no VARCHAR(50) NOT NULL,
43
+    caliber VARCHAR(20) NOT NULL COMMENT '管径',
44
+    customer_name VARCHAR(200) NOT NULL,
45
+    area VARCHAR(100) NOT NULL,
46
+    device_sn VARCHAR(100) COMMENT '设备号',
47
+    current_reading DECIMAL(12,2) COMMENT '当前读数',
48
+    last_reading_date DATE COMMENT '上次抄表日期',
49
+    monthly_consumption DECIMAL(12,2) COMMENT '月用量',
50
+    monitor_status VARCHAR(20) DEFAULT 'NORMAL' COMMENT '监控状态: NORMAL/ALARM/OFFLINE',
51
+    alert_level VARCHAR(20) COMMENT '预警级别: LOW/MEDIUM/HIGH/CRITICAL',
52
+    alert_count INTEGER DEFAULT 0 COMMENT '预警次数',
53
+    last_alert_time TIMESTAMP NULL COMMENT '最后预警时间',
54
+    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
55
+    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
56
+    INDEX idx_meter_no (meter_no),
57
+    INDEX idx_caliber (caliber),
58
+    INDEX idx_area (area),
59
+    INDEX idx_monitor_status (monitor_status),
60
+    INDEX idx_alert_level (alert_level)
61
+);
62
+
63
+-- 4. 远传抄表任务表
64
+CREATE TABLE IF NOT EXISTS rev_remote_reading_task (
65
+    task_id BIGINT AUTO_INCREMENT PRIMARY KEY,
66
+    task_name VARCHAR(200) NOT NULL,
67
+    task_type VARCHAR(50) NOT NULL COMMENT '任务类型: SINGLE_AREA/MULTI_AREA/ALL_AREA',
68
+    areas TEXT COMMENT '涉及区域列表(JSON)',
69
+    status VARCHAR(20) DEFAULT 'PENDING' COMMENT '任务状态: PENDING/RUNNING/COMPLETED/FAILED',
70
+    total_meters INTEGER DEFAULT 0,
71
+    success_meters INTEGER DEFAULT 0,
72
+    failed_meters INTEGER DEFAULT 0,
73
+    abnormal_meters INTEGER DEFAULT 0,
74
+    start_time TIMESTAMP NULL,
75
+    end_time TIMESTAMP NULL,
76
+    error_message TEXT,
77
+    created_by VARCHAR(100) NOT NULL,
78
+    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
79
+    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
80
+    INDEX idx_status (status),
81
+    INDEX idx_created_at (created_at)
82
+);
83
+
84
+-- 5. 预警记录表
85
+CREATE TABLE IF NOT EXISTS rev_alert_record (
86
+    alert_id BIGINT AUTO_INCREMENT PRIMARY KEY,
87
+    meter_id BIGINT NOT NULL,
88
+    meter_no VARCHAR(50) NOT NULL,
89
+    alert_type VARCHAR(50) NOT NULL COMMENT '预警类型: HIGH_CONSUMPTION/DEVICE_OFFLINE/ZERO_FLOW/ABNORMAL_DECREASE',
90
+    alert_title VARCHAR(200) NOT NULL COMMENT '预警标题',
91
+    alert_description TEXT COMMENT '预警描述',
92
+    severity VARCHAR(20) DEFAULT 'MEDIUM' COMMENT '严重程度: LOW/MEDIUM/HIGH/CRITICAL',
93
+    status VARCHAR(20) DEFAULT 'PENDING' COMMENT '处理状态: PENDING/ACKNOWLEDGED/RESOLVED',
94
+    acknowledged_by VARCHAR(100) NULL,
95
+    acknowledged_at TIMESTAMP NULL,
96
+    resolved_by VARCHAR(100) NULL,
97
+    resolved_at TIMESTAMP NULL,
98
+    additional_issues TEXT COMMENT '附加问题(JSON)',
99
+    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
100
+    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
101
+    INDEX idx_meter_no (meter_no),
102
+    INDEX idx_alert_type (alert_type),
103
+    INDEX idx_severity (severity),
104
+    INDEX idx_status (status),
105
+    INDEX idx_created_at (created_at)
106
+);
107
+
108
+-- 6. 抄表结果统计视图
109
+CREATE OR REPLACE VIEW v_reading_statistics AS
110
+SELECT 
111
+    r.period,
112
+    r.area,
113
+    r.total_meters,
114
+    r.success_meters,
115
+    r.failed_meters,
116
+    r.abnormal_meters,
117
+    ROUND((r.success_meters * 100.0 / NULLIF(r.total_meters, 0)), 2) as success_rate,
118
+    ROUND((r.abnormal_meters * 100.0 / NULLIF(r.total_meters, 0)), 2) as abnormal_rate
119
+FROM rev_batch_report r
120
+ORDER BY r.period DESC, r.area;
121
+
122
+-- 7. 大表监控统计视图
123
+CREATE OR REPLACE VIEW v_large_meter_statistics AS
124
+SELECT 
125
+    caliber,
126
+    COUNT(*) as total_count,
127
+    SUM(CASE WHEN monitor_status = 'NORMAL' THEN 1 ELSE 0 END) as normal_count,
128
+    SUM(CASE WHEN monitor_status = 'ALARM' THEN 1 ELSE 0 END) as alarm_count,
129
+    SUM(CASE WHEN monitor_status = 'OFFLINE' THEN 1 ELSE 0 END) as offline_count,
130
+    ROUND(SUM(monthly_consumption), 2) as total_consumption,
131
+    ROUND(AVG(monthly_consumption), 2) as avg_consumption,
132
+    MAX(monthly_consumption) as max_consumption
133
+FROM rev_large_meter_monitor
134
+GROUP BY caliber
135
+ORDER BY caliber;
136
+

+ 112
- 0
sql/problem_reporting.sql Просмотреть файл

@@ -0,0 +1,112 @@
1
+-- =============================================
2
+-- 智慧水务管理系统 - 巡检问题上报 + 工单管理 DDL
3
+-- 版本: V1
4
+-- =============================================
5
+
6
+-- 巡检问题上报表
7
+CREATE TABLE IF NOT EXISTS patrol_problem (
8
+    id BIGSERIAL PRIMARY KEY,
9
+    problem_no VARCHAR(30) UNIQUE NOT NULL,   -- 问题编号:WQ-2026-001
10
+    task_id BIGINT REFERENCES patrol_task(id),
11
+    point_seq INT,
12
+    device_id BIGINT,
13
+    device_name VARCHAR(200),
14
+    problem_type VARCHAR(50) NOT NULL,       -- 设备故障/水质异常/安全隐患/环境卫生/其他
15
+    problem_level VARCHAR(20) DEFAULT 'normal', -- low/normal/high/critical
16
+    problem_title VARCHAR(200) NOT NULL,
17
+    problem_description TEXT,
18
+    location VARCHAR(300),
19
+    lng DOUBLE PRECISION,
20
+    lat DOUBLE PRECISION,
21
+    photo_urls JSONB,                        -- 现场照片URL数组
22
+    reporter_id BIGINT REFERENCES sys_user(id),
23
+    reporter_name VARCHAR(50),
24
+    report_time TIMESTAMP DEFAULT NOW(),
25
+    status VARCHAR(20) DEFAULT 'reported',    -- reported/processing/completed/closed
26
+    work_order_id BIGINT,                    -- 关联工单ID
27
+    created_at TIMESTAMP DEFAULT NOW(),
28
+    updated_at TIMESTAMP DEFAULT NOW()
29
+);
30
+COMMENT ON TABLE patrol_problem IS '巡检问题上报表';
31
+CREATE INDEX IF NOT EXISTS idx_problem_task ON patrol_problem(task_id);
32
+CREATE INDEX IF NOT EXISTS idx_problem_status ON patrol_problem(status);
33
+CREATE INDEX IF NOT EXISTS idx_problem_device ON patrol_problem(device_id);
34
+CREATE INDEX IF NOT EXISTS idx_problem_type ON patrol_problem(problem_type);
35
+
36
+-- 工单表
37
+CREATE TABLE IF NOT EXISTS work_order (
38
+    id BIGSERIAL PRIMARY KEY,
39
+    order_no VARCHAR(30) UNIQUE NOT NULL,     -- 工单编号:WO-2026-001
40
+    problem_id BIGINT REFERENCES patrol_problem(id),
41
+    order_type VARCHAR(50) NOT NULL,        -- 设备维修/水质处理/安全隐患处理/清洁/其他
42
+    priority VARCHAR(20) DEFAULT 'normal',   -- low/normal/high/critical
43
+    title VARCHAR(200) NOT NULL,
44
+    description TEXT,
45
+    location VARCHAR(300),
46
+    contact_person VARCHAR(50),
47
+    contact_phone VARCHAR(20),
48
+    reporter_id BIGINT REFERENCES sys_user(id),
49
+    reporter_name VARCHAR(50),
50
+    assignee_id BIGINT REFERENCES sys_user(id),
51
+    assignee_name VARCHAR(50),
52
+    status VARCHAR(20) DEFAULT 'pending',    -- pending/assigned/processing/completed/cancelled
53
+    process_status VARCHAR(20) DEFAULT 'created', -- created/accepted/in_progress/completed
54
+    estimated_duration INT,                  -- 预计工时(分钟)
55
+    actual_start_time TIMESTAMP,
56
+    actual_end_time TIMESTAMP,
57
+    completion_time TIMESTAMP,
58
+    photos_before JSONB,                     -- 处理前照片
59
+    photos_after JSONB,                      -- 处理后照片
60
+    solution_description TEXT,               -- 处理方案描述
61
+    solution_result TEXT,                    -- 处理结果
62
+    customer_feedback TEXT,                  -- 客户反馈
63
+    created_at TIMESTAMP DEFAULT NOW(),
64
+    updated_at TIMESTAMP DEFAULT NOW()
65
+);
66
+COMMENT ON TABLE work_order IS '工单表';
67
+CREATE INDEX IF NOT EXISTS idx_order_problem ON work_order(problem_id);
68
+CREATE INDEX IF NOT EXISTS idx_order_status ON work_order(status, process_status);
69
+CREATE INDEX IF NOT EXISTS idx_order_assignee ON work_order(assignee_id);
70
+
71
+-- 工单处理记录表
72
+CREATE TABLE IF NOT EXISTS work_order_process (
73
+    id BIGSERIAL PRIMARY KEY,
74
+    work_order_id BIGINT REFERENCES work_order(id),
75
+    process_step VARCHAR(50) NOT NULL,       -- created/accepted/in_progress/completed
76
+    processor_id BIGINT REFERENCES sys_user(id),
77
+    processor_name VARCHAR(50),
78
+    action VARCHAR(50) NOT NULL,            -- create/assign/start/complete/cancel
79
+    comment TEXT,
80
+    photos JSONB,                            -- 处理过程照片
81
+    created_at TIMESTAMP DEFAULT NOW()
82
+);
83
+COMMENT ON TABLE work_order_process IS '工单处理记录表';
84
+CREATE INDEX IF NOT EXISTS idx_process_order ON work_order_process(work_order_id);
85
+CREATE INDEX IF NOT EXISTS idx_process_step ON work_order_process(process_step);
86
+
87
+-- 工单附件表
88
+CREATE TABLE IF NOT EXISTS work_order_attachment (
89
+    id BIGSERIAL PRIMARY KEY,
90
+    work_order_id BIGINT REFERENCES work_order(id),
91
+    file_name VARCHAR(200) NOT NULL,
92
+    file_path VARCHAR(500) NOT NULL,
93
+    file_type VARCHAR(50),                  -- image/pdf/doc/other
94
+    file_size BIGINT,
95
+    uploaded_by BIGINT REFERENCES sys_user(id),
96
+    uploaded_at TIMESTAMP DEFAULT NOW()
97
+);
98
+COMMENT ON TABLE work_order_attachment IS '工单附件表';
99
+CREATE INDEX IF NOT EXISTS idx_attachment_order ON work_order_attachment(work_order_id);
100
+
101
+-- 巡检问题与工单关联触发记录
102
+CREATE TABLE IF NOT EXISTS patrol_work_order_trigger (
103
+    id BIGSERIAL PRIMARY KEY,
104
+    patrol_problem_id BIGINT REFERENCES patrol_problem(id),
105
+    work_order_id BIGINT REFERENCES work_order(id),
106
+    trigger_type VARCHAR(20) NOT NULL,      -- auto/manual
107
+    trigger_condition JSONB,                 -- 触发条件
108
+    created_at TIMESTAMP DEFAULT NOW()
109
+);
110
+COMMENT ON TABLE patrol_work_order_trigger IS '巡检问题与工单关联触发记录';
111
+CREATE INDEX IF NOT EXISTS idx_trigger_problem ON patrol_work_order_trigger(patrol_problem_id);
112
+CREATE INDEX IF NOT EXISTS idx_trigger_order ON patrol_work_order_trigger(work_order_id);

+ 178
- 0
sql/revenue_tables.sql Просмотреть файл

@@ -0,0 +1,178 @@
1
+-- 营业收费系统表结构
2
+-- 客户信息表
3
+CREATE TABLE IF NOT EXISTS rev_customer (
4
+    id BIGSERIAL PRIMARY KEY,
5
+    customer_no VARCHAR(30) UNIQUE NOT NULL,
6
+    customer_name VARCHAR(100) NOT NULL,
7
+    customer_type VARCHAR(20) DEFAULT 'residential', -- residential/business/enterprise/institution
8
+    area VARCHAR(50),
9
+    address VARCHAR(300),
10
+    phone VARCHAR(20),
11
+    id_card VARCHAR(18),
12
+    contract_no VARCHAR(50),
13
+    status VARCHAR(20) DEFAULT 'active',
14
+    created_at TIMESTAMP DEFAULT NOW(),
15
+    updated_at TIMESTAMP DEFAULT NOW()
16
+);
17
+
18
+-- 水表档案表
19
+CREATE TABLE IF NOT EXISTS rev_meter (
20
+    id BIGSERIAL PRIMARY KEY,
21
+    meter_no VARCHAR(50) UNIQUE NOT NULL,
22
+    customer_id BIGINT REFERENCES rev_customer(id),
23
+    device_id BIGINT, -- 关联IoT设备
24
+    caliber VARCHAR(10), -- DN15/DN20/DN40...
25
+    meter_type VARCHAR(20), -- mechanical/ultrasonic/electromagnetic
26
+    initial_reading DECIMAL(10,2),
27
+    install_date DATE,
28
+    status VARCHAR(20) DEFAULT 'active', -- active/dismantled/scrapped/repaired
29
+    created_at TIMESTAMP DEFAULT NOW(),
30
+    updated_at TIMESTAMP DEFAULT NOW()
31
+);
32
+
33
+-- 抄表记录表
34
+CREATE TABLE IF NOT EXISTS rev_reading (
35
+    id BIGSERIAL PRIMARY KEY,
36
+    meter_id BIGINT REFERENCES rev_meter(id),
37
+    reading_date DATE NOT NULL,
38
+    prev_reading DECIMAL(10,2),
39
+    curr_reading DECIMAL(10,2),
40
+    consumption DECIMAL(10,2), -- 用水量
41
+    read_type VARCHAR(20), -- manual/remote/estimate
42
+    reader_id BIGINT,
43
+    photo_url VARCHAR(500),
44
+    verified TINYINT DEFAULT 0,
45
+    created_at TIMESTAMP DEFAULT NOW(),
46
+    updated_at TIMESTAMP DEFAULT NOW()
47
+);
48
+
49
+-- 水费账单表
50
+CREATE TABLE IF NOT EXISTS rev_bill (
51
+    id BIGSERIAL PRIMARY KEY,
52
+    customer_id BIGINT REFERENCES rev_customer(id),
53
+    bill_period VARCHAR(10) NOT NULL, -- 2026-06
54
+    consumption DECIMAL(10,2),
55
+    water_fee DECIMAL(10,2),
56
+    sewage_fee DECIMAL(10,2),
57
+    total_fee DECIMAL(10,2),
58
+    paid_fee DECIMAL(10,2) DEFAULT 0,
59
+    status VARCHAR(20) DEFAULT 'pending', -- pending/partial/paid/overdue
60
+    due_date DATE,
61
+    paid_at TIMESTAMP,
62
+    created_at TIMESTAMP DEFAULT NOW(),
63
+    updated_at TIMESTAMP DEFAULT NOW(),
64
+    UNIQUE(customer_id, bill_period)
65
+);
66
+
67
+-- 报装申请表
68
+CREATE TABLE IF NOT EXISTS rev_install (
69
+    id BIGSERIAL PRIMARY KEY,
70
+    app_no VARCHAR(50) UNIQUE NOT NULL,
71
+    customer_name VARCHAR(100) NOT NULL,
72
+    phone VARCHAR(20) NOT NULL,
73
+    area VARCHAR(50) NOT NULL,
74
+    address VARCHAR(300) NOT NULL,
75
+    customer_type VARCHAR(20) NOT NULL,
76
+    caliber VARCHAR(10) NOT NULL,
77
+    status VARCHAR(20) DEFAULT 'pre_apply', -- pre_apply/engineering/completed/terminated
78
+    apply_time TIMESTAMP DEFAULT NOW(),
79
+    complete_time TIMESTAMP,
80
+    engineer_id BIGINT,
81
+    remark TEXT,
82
+    created_at TIMESTAMP DEFAULT NOW(),
83
+    updated_at TIMESTAMP DEFAULT NOW()
84
+);
85
+
86
+-- 知识库字典类型
87
+INSERT INTO sys_dict_type (dict_key, dict_name, status, created_at) VALUES 
88
+('knowledge_base', '客服知识库', 1, NOW())
89
+ON CONFLICT (dict_key) DO NOTHING;
90
+
91
+-- 知识库字典数据
92
+INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value, dict_sort, status) 
93
+VALUES (
94
+    (SELECT id FROM sys_dict_type WHERE dict_key = 'knowledge_base'),
95
+    '水费缴纳方式', 
96
+    '支持微信、支付宝、银行卡等多种缴费方式,可通过微信公众号、营业厅或自助终端缴纳。',
97
+    1, 
98
+    1
99
+),
100
+(
101
+    (SELECT id FROM sys_dict_type WHERE dict_key = 'knowledge_base'),
102
+    '水费计算规则', 
103
+    '水费 = 基本水费 + 超额水费 + 污水处理费。阶梯水价:第一级0-12m³/户,第二级12-24m³/户,第三级24m³以上/户。',
104
+    2, 
105
+    1
106
+),
107
+(
108
+    (SELECT id FROM sys_dict_type WHERE dict_key = 'knowledge_base'),
109
+    '报装流程', 
110
+    '1. 提交申请 2. 现场勘查 3. 方案制定 4. 工程施工 5. 验收通水 6. 资料归档。一般7-15个工作日完成。',
111
+    3, 
112
+    1
113
+),
114
+(
115
+    (SELECT id FROM sys_dict_type WHERE dict_key = 'knowledge_base'),
116
+    '水质问题处理', 
117
+    '如发现水质异常,请立即拨打客服热线400-123-4567,我们会安排工作人员24小时内上门处理。',
118
+    4, 
119
+    1
120
+)
121
+ON CONFLICT (dict_value) DO NOTHING;
122
+
123
+-- 公告板字典类型
124
+INSERT INTO sys_dict_type (dict_key, dict_name, status, created_at) VALUES 
125
+('notice_water_stop', '停水公告', 1, NOW()),
126
+('notice_water_quality', '水质公告', 1, NOW()),
127
+('notice_service', '服务通知', 1, NOW())
128
+ON CONFLICT (dict_key) DO NOTHING;
129
+
130
+-- 示例停水公告
131
+INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value, created_at) 
132
+VALUES (
133
+    (SELECT id FROM sys_dict_type WHERE dict_key = 'notice_water_stop'),
134
+    '精芒片区计划停水通知',
135
+    '因管道维修,精芒片区将于2026年6月15日9:00-17:00停水,请提前储水。'
136
+) ON CONFLICT (dict_value) DO NOTHING;
137
+
138
+INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value, created_at) 
139
+VALUES (
140
+    (SELECT id FROM sys_dict_type WHERE dict_key = 'notice_service'),
141
+    '营业厅服务时间调整',
142
+    '精河营业厅周末服务时间调整为9:00-17:00,欢迎大家前来办理业务。'
143
+) ON CONFLICT (dict_value) DO NOTHING;
144
+
145
+-- 示例数据
146
+-- 创建一些测试客户
147
+INSERT INTO rev_customer (customer_no, customer_name, phone, area, address) VALUES 
148
+('C001', '张三', '13812345678', '精芒片区', '精河县精芒街道123号'),
149
+('C002', '李四', '13987654321', '托里片区', '精河县托里路456号'),
150
+('C003', '王五', '13555666777', '八家户片区', '精河县八家户街789号')
151
+ON CONFLICT (customer_no) DO NOTHING;
152
+
153
+-- 创建测试水表
154
+INSERT INTO rev_meter (meter_no, customer_id, caliber, meter_type, install_date) VALUES 
155
+('M001', 1, 'DN15', 'mechanical', '2025-01-01'),
156
+('M002', 2, 'DN20', 'electromagnetic', '2025-02-01'),
157
+('M003', 3, 'DN15', 'ultrasonic', '2025-03-01')
158
+ON CONFLICT (meter_no) DO NOTHING;
159
+
160
+-- 创建测试抄表记录
161
+INSERT INTO rev_reading (meter_id, reading_date, prev_reading, curr_reading, consumption, read_type) VALUES 
162
+(1, '2026-05-01', 1000.00, 1100.00, 100.00, 'remote'),
163
+(2, '2026-05-01', 2000.00, 2100.00, 100.00, 'manual'),
164
+(3, '2026-05-01', 3000.00, 3200.00, 200.00, 'remote')
165
+ON CONFLICT (id) DO NOTHING;
166
+
167
+-- 创建测试账单
168
+INSERT INTO rev_bill (customer_id, bill_period, consumption, water_fee, sewage_fee, total_fee, status, due_date) VALUES 
169
+(1, '2026-05', 100.00, 45.00, 15.00, 60.00, 'pending', '2026-06-20'),
170
+(2, '2026-05', 100.00, 45.00, 15.00, 60.00, 'paid', '2026-06-15'),
171
+(3, '2026-05', 200.00, 90.00, 30.00, 120.00, 'overdue', '2026-06-10')
172
+ON CONFLICT (id) DO NOTHING;
173
+
174
+-- 创建测试报装申请
175
+INSERT INTO rev_install (app_no, customer_name, phone, area, address, customer_type, caliber, status) VALUES 
176
+('A001', '赵六', '13666777888', '大镇阿合其片区', '精河县大镇路999号', 'residential', 'DN15', 'completed'),
177
+('A002', '钱七', '13777888999', '托托片区', '精河县托托街111号', 'business', 'DN20', 'engineering')
178
+ON CONFLICT (app_no) DO NOTHING;

+ 16
- 0
wm-patrol/pom.xml Просмотреть файл

@@ -0,0 +1,16 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<project xmlns="http://maven.apache.org/POM/4.0.0"
3
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
5
+    <modelVersion>4.0.0</modelVersion>
6
+    <parent><groupId>com.water</groupId><artifactId>wm-parent</artifactId><version>1.0.0-SNAPSHOT</version></parent>
7
+    <artifactId>wm-patrol</artifactId>
8
+    <dependencies>
9
+        <dependency><groupId>com.water</groupId><artifactId>wm-common</artifactId></dependency>
10
+        <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
11
+        <dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency>
12
+        <dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-spring-boot3-starter</artifactId></dependency>
13
+        <dependency><groupId>cn.dev33</groupId><artifactId>sa-token-spring-boot3-starter</artifactId></dependency>
14
+        <dependency><groupId>org.postgresql</groupId><artifactId>postgresql</artifactId></dependency>
15
+    </dependencies>
16
+</project>

+ 11
- 0
wm-patrol/src/main/java/com/water/patrol/PatrolApplication.java Просмотреть файл

@@ -0,0 +1,11 @@
1
+package com.water.patrol;
2
+
3
+import org.springframework.boot.SpringApplication;
4
+import org.springframework.boot.autoconfigure.SpringBootApplication;
5
+
6
+@SpringBootApplication
7
+public class PatrolApplication {
8
+    public static void main(String[] args) {
9
+        SpringApplication.run(PatrolApplication.class, args);
10
+    }
11
+}

+ 127
- 0
wm-patrol/src/main/java/com/water/patrol/controller/PatrolAppController.java Просмотреть файл

@@ -0,0 +1,127 @@
1
+package com.water.patrol.controller;
2
+
3
+import com.water.common.core.result.R;
4
+import com.water.patrol.service.PatrolAppService;
5
+import io.swagger.v3.oas.annotations.Operation;
6
+import io.swagger.v3.oas.annotations.tags.Tag;
7
+import lombok.RequiredArgsConstructor;
8
+import org.springframework.web.bind.annotation.*;
9
+
10
+import java.util.List;
11
+import java.util.Map;
12
+
13
+/**
14
+ * 巡查APP控制器 - Issue #88
15
+ * PAT-21~PAT-29 统一入口
16
+ */
17
+@Tag(name = "巡查APP")
18
+@RestController
19
+@RequestMapping("/patrol/app")
20
+@RequiredArgsConstructor
21
+public class PatrolAppController {
22
+
23
+    private final PatrolAppService patrolAppService;
24
+
25
+    // ========== PAT-21 APP总览 ==========
26
+
27
+    @Operation(summary = "APP总览")
28
+    @GetMapping("/overview")
29
+    public R<Map<String, Object>> overview(@RequestParam Long userId) {
30
+        return R.ok(patrolAppService.getAppOverview(userId));
31
+    }
32
+
33
+    // ========== PAT-22 今日任务 ==========
34
+
35
+    @Operation(summary = "今日任务")
36
+    @GetMapping("/today-tasks")
37
+    public R<List<Map<String, Object>>> todayTasks(@RequestParam Long userId) {
38
+        return R.ok(patrolAppService.getTodayTasks(userId));
39
+    }
40
+
41
+    // ========== PAT-23 待办工单 ==========
42
+
43
+    @Operation(summary = "待办工单")
44
+    @GetMapping("/pending-work-orders")
45
+    public R<List<Map<String, Object>>> pendingWorkOrders(@RequestParam Long userId) {
46
+        return R.ok(patrolAppService.getPendingWorkOrders(userId));
47
+    }
48
+
49
+    // ========== PAT-24 巡检统计 ==========
50
+
51
+    @Operation(summary = "巡检统计")
52
+    @GetMapping("/stats")
53
+    public R<Map<String, Object>> stats(@RequestParam Long userId,
54
+                                         @RequestParam(defaultValue = "month") String period) {
55
+        return R.ok(patrolAppService.getUserStats(userId, period));
56
+    }
57
+
58
+    // ========== PAT-25 任务列表 ==========
59
+
60
+    @Operation(summary = "任务列表")
61
+    @GetMapping("/tasks")
62
+    public R<Map<String, Object>> tasks(@RequestParam Long userId,
63
+                                         @RequestParam(required = false) String status,
64
+                                         @RequestParam(defaultValue = "1") int page,
65
+                                         @RequestParam(defaultValue = "20") int size) {
66
+        return R.ok(patrolAppService.getTaskList(userId, status, page, size));
67
+    }
68
+
69
+    // ========== PAT-26 工单列表 ==========
70
+
71
+    @Operation(summary = "工单列表")
72
+    @GetMapping("/work-orders")
73
+    public R<Map<String, Object>> workOrders(@RequestParam Long userId,
74
+                                              @RequestParam(required = false) String status,
75
+                                              @RequestParam(defaultValue = "1") int page,
76
+                                              @RequestParam(defaultValue = "20") int size) {
77
+        return R.ok(patrolAppService.getWorkOrderList(userId, status, page, size));
78
+    }
79
+
80
+    // ========== PAT-27 问题上报 ==========
81
+
82
+    @Operation(summary = "问题上报")
83
+    @PostMapping("/report-issue")
84
+    public R<Map<String, Object>> reportIssue(@RequestBody Map<String, Object> req) {
85
+        Long taskId = Long.parseLong(String.valueOf(req.get("taskId")));
86
+        Long deviceId = req.get("deviceId") != null ? Long.parseLong(String.valueOf(req.get("deviceId"))) : null;
87
+        String issueType = (String) req.get("issueType");
88
+        String description = (String) req.get("description");
89
+
90
+        @SuppressWarnings("unchecked")
91
+        List<String> photoUrls = (List<String>) req.getOrDefault("photoUrls", List.of());
92
+        String voiceUrl = (String) req.get("voiceUrl");
93
+
94
+        Double lng = req.get("lng") != null ? ((Number) req.get("lng")).doubleValue() : null;
95
+        Double lat = req.get("lat") != null ? ((Number) req.get("lat")).doubleValue() : null;
96
+
97
+        return R.ok(patrolAppService.reportIssue(taskId, deviceId, issueType, description,
98
+            photoUrls, voiceUrl, lng, lat));
99
+    }
100
+
101
+    // ========== PAT-28 个人中心 ==========
102
+
103
+    @Operation(summary = "个人中心")
104
+    @GetMapping("/profile")
105
+    public R<Map<String, Object>> profile(@RequestParam Long userId) {
106
+        return R.ok(patrolAppService.getProfile(userId));
107
+    }
108
+
109
+    @Operation(summary = "更新头像")
110
+    @PutMapping("/avatar")
111
+    public R<Map<String, Object>> updateAvatar(@RequestParam Long userId,
112
+                                                @RequestParam String avatarUrl) {
113
+        return R.ok(patrolAppService.updateAvatar(userId, avatarUrl));
114
+    }
115
+
116
+    // ========== PAT-29 历史任务 ==========
117
+
118
+    @Operation(summary = "历史任务")
119
+    @GetMapping("/history-tasks")
120
+    public R<Map<String, Object>> historyTasks(@RequestParam Long userId,
121
+                                                @RequestParam(required = false) String startDate,
122
+                                                @RequestParam(required = false) String endDate,
123
+                                                @RequestParam(defaultValue = "1") int page,
124
+                                                @RequestParam(defaultValue = "20") int size) {
125
+        return R.ok(patrolAppService.getHistoryTasks(userId, startDate, endDate, page, size));
126
+    }
127
+}

+ 12
- 0
wm-patrol/src/main/java/com/water/patrol/controller/PatrolAppIssueController.java Просмотреть файл

@@ -0,0 +1,12 @@
1
+package com.water.patrol.controller;
2
+import com.water.common.core.result.R; import com.water.patrol.entity.PatrolIssueReport; import com.water.patrol.service.PatrolAppIssueService;
3
+import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag;
4
+import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*;
5
+import java.math.BigDecimal; import java.util.Map;
6
+@Tag(name="APP-问题上报") @RestController @RequestMapping("/api/patrol/app/issue") @RequiredArgsConstructor
7
+public class PatrolAppIssueController {
8
+    private final PatrolAppIssueService svc;
9
+    @Operation(summary="上报问题") @PostMapping public R<PatrolIssueReport> submit(@RequestParam(required=false) Long taskId, @RequestParam Long reporterId, @RequestParam String reporterName, @RequestParam String issueType, @RequestParam String description, @RequestParam(required=false) String photos, @RequestParam(required=false) String videos, @RequestParam(required=false) BigDecimal lng, @RequestParam(required=false) BigDecimal lat, @RequestParam(required=false) String address, @RequestParam(required=false) String severity) { return R.ok(svc.submit(taskId,reporterId,reporterName,issueType,description,photos,videos,lng,lat,address,severity)); }
10
+    @Operation(summary="我的上报") @GetMapping("/my") public R<Map<String,Object>> myReports(@RequestParam Long reporterId) { return R.ok(svc.myReports(reporterId)); }
11
+    @Operation(summary="上报详情") @GetMapping("/{id}") public R<PatrolIssueReport> detail(@PathVariable Long id) { return R.ok(svc.getDetail(id)); }
12
+}

+ 10
- 0
wm-patrol/src/main/java/com/water/patrol/controller/PatrolAppProfileController.java Просмотреть файл

@@ -0,0 +1,10 @@
1
+package com.water.patrol.controller;
2
+import com.water.common.core.result.R; import com.water.patrol.service.PatrolAppProfileService;
3
+import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag;
4
+import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*;
5
+import java.util.Map;
6
+@Tag(name="APP-个人中心") @RestController @RequestMapping("/api/patrol/app/profile") @RequiredArgsConstructor
7
+public class PatrolAppProfileController {
8
+    private final PatrolAppProfileService svc;
9
+    @Operation(summary="个人数据") @GetMapping public R<Map<String,Object>> profile(@RequestParam Long workerId) { return R.ok(svc.getProfile(workerId)); }
10
+}

+ 14
- 0
wm-patrol/src/main/java/com/water/patrol/controller/PatrolAppTaskController.java Просмотреть файл

@@ -0,0 +1,14 @@
1
+package com.water.patrol.controller;
2
+import com.water.common.core.result.R; import com.water.patrol.entity.PatrolTask; import com.water.patrol.service.PatrolAppTaskService;
3
+import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag;
4
+import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*;
5
+import java.util.Map;
6
+@Tag(name="APP-我的任务") @RestController @RequestMapping("/api/patrol/app/task") @RequiredArgsConstructor
7
+public class PatrolAppTaskController {
8
+    private final PatrolAppTaskService svc;
9
+    @Operation(summary="我的任务列表") @GetMapping("/my") public R<Map<String,Object>> myTasks(@RequestParam Long workerId, @RequestParam(required=false) String status) { return R.ok(svc.myTasks(workerId,status)); }
10
+    @Operation(summary="任务详情") @GetMapping("/{id}") public R<PatrolTask> detail(@PathVariable Long id) { return R.ok(svc.getDetail(id)); }
11
+    @Operation(summary="接受任务") @PutMapping("/{id}/accept") public R<String> accept(@PathVariable Long id) { svc.accept(id); return R.ok("OK"); }
12
+    @Operation(summary="完成任务") @PutMapping("/{id}/complete") public R<String> complete(@PathVariable Long id, @RequestParam Integer completedCheckpoints, @RequestParam Double distance, @RequestParam(required=false) String remark) { svc.complete(id,completedCheckpoints,distance,remark); return R.ok("OK"); }
13
+    @Operation(summary="我的统计") @GetMapping("/my/stats") public R<Map<String,Object>> myStats(@RequestParam Long workerId) { return R.ok(svc.myStats(workerId)); }
14
+}

+ 13
- 0
wm-patrol/src/main/java/com/water/patrol/controller/PatrolAppWoController.java Просмотреть файл

@@ -0,0 +1,13 @@
1
+package com.water.patrol.controller;
2
+import com.water.common.core.result.R; import com.water.patrol.entity.PatrolWorkOrder; import com.water.patrol.service.PatrolAppWorkOrderService;
3
+import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag;
4
+import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*;
5
+import java.util.Map;
6
+@Tag(name="APP-我的工单") @RestController @RequestMapping("/api/patrol/app/work-order") @RequiredArgsConstructor
7
+public class PatrolAppWoController {
8
+    private final PatrolAppWorkOrderService svc;
9
+    @Operation(summary="我的工单列表") @GetMapping("/my") public R<Map<String,Object>> myWorkOrders(@RequestParam Long assigneeId, @RequestParam(required=false) String status) { return R.ok(svc.myWorkOrders(assigneeId,status)); }
10
+    @Operation(summary="工单详情") @GetMapping("/{id}") public R<PatrolWorkOrder> detail(@PathVariable Long id) { return R.ok(svc.getDetail(id)); }
11
+    @Operation(summary="接受工单") @PutMapping("/{id}/accept") public R<String> accept(@PathVariable Long id) { svc.accept(id); return R.ok("OK"); }
12
+    @Operation(summary="解决工单") @PutMapping("/{id}/resolve") public R<String> resolve(@PathVariable Long id, @RequestParam String resolution, @RequestParam(required=false) String resultPhotos) { svc.resolve(id,resolution,resultPhotos); return R.ok("OK"); }
13
+}

+ 16
- 0
wm-patrol/src/main/java/com/water/patrol/controller/PatrolAreaController.java Просмотреть файл

@@ -0,0 +1,16 @@
1
+package com.water.patrol.controller;
2
+import com.water.common.core.result.R; import com.water.patrol.entity.PatrolArea; import com.water.patrol.service.PatrolAreaService;
3
+import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag;
4
+import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*;
5
+import java.math.BigDecimal; import java.util.Map;
6
+@Tag(name="巡检区域") @RestController @RequestMapping("/api/patrol/area") @RequiredArgsConstructor
7
+public class PatrolAreaController {
8
+    private final PatrolAreaService svc;
9
+    @Operation(summary="创建区域") @PostMapping public R<PatrolArea> create(@RequestParam String areaName, @RequestParam String areaCode, @RequestParam(required=false) String description, @RequestParam BigDecimal centerLng, @RequestParam BigDecimal centerLat, @RequestParam(required=false) String boundary, @RequestParam(required=false) Double radius) { return R.ok(svc.create(areaName,areaCode,description,centerLng,centerLat,boundary,radius)); }
10
+    @Operation(summary="区域列表") @GetMapping public R<Map<String,Object>> list(@RequestParam(required=false) String keyword, @RequestParam(required=false) String status, @RequestParam(defaultValue="1") int page, @RequestParam(defaultValue="20") int size) { return R.ok(svc.list(keyword,status,page,size)); }
11
+    @Operation(summary="区域详情") @GetMapping("/{id}") public R<PatrolArea> detail(@PathVariable Long id) { return R.ok(svc.getDetail(id)); }
12
+    @Operation(summary="更新区域") @PutMapping("/{id}") public R<PatrolArea> update(@PathVariable Long id, @RequestBody PatrolArea patch) { return R.ok(svc.update(id,patch)); }
13
+    @Operation(summary="更新状态") @PutMapping("/{id}/status") public R<String> updateStatus(@PathVariable Long id, @RequestParam String status) { svc.updateStatus(id,status); return R.ok("OK"); }
14
+    @Operation(summary="删除区域") @DeleteMapping("/{id}") public R<String> delete(@PathVariable Long id) { svc.delete(id); return R.ok("OK"); }
15
+    @Operation(summary="区域统计") @GetMapping("/stats") public R<Map<String,Object>> stats() { return R.ok(svc.getStats()); }
16
+}

+ 100
- 0
wm-patrol/src/main/java/com/water/patrol/controller/PatrolController.java Просмотреть файл

@@ -0,0 +1,100 @@
1
+package com.water.patrol.controller;
2
+
3
+import com.water.common.core.result.R;
4
+import com.water.patrol.service.PatrolService;
5
+import io.swagger.v3.oas.annotations.Operation;
6
+import io.swagger.v3.oas.annotations.tags.Tag;
7
+import lombok.RequiredArgsConstructor;
8
+import org.springframework.web.bind.annotation.*;
9
+
10
+import java.time.LocalDate;
11
+import java.util.*;
12
+
13
+@Tag(name = "巡检管理")
14
+@RestController
15
+@RequestMapping("/patrol")
16
+@RequiredArgsConstructor
17
+public class PatrolController {
18
+
19
+    private final PatrolService patrolService;
20
+
21
+    // ---- 路线 ----
22
+    @PostMapping("/route")
23
+    public R<Map<String, Object>> createRoute(@RequestBody Map<String, Object> req) {
24
+        @SuppressWarnings("unchecked")
25
+        List<Map<String, Object>> points = (List<Map<String, Object>>) req.getOrDefault("points", List.of());
26
+        return R.ok(patrolService.createRoute(
27
+            (String) req.get("routeName"), (String) req.get("area"),
28
+            points, (int) req.getOrDefault("estimDuration", 60)));
29
+    }
30
+
31
+    @GetMapping("/route/list")
32
+    public R<List<Map<String, Object>>> routes(@RequestParam String area) {
33
+        return R.ok(patrolService.getRoutes(area));
34
+    }
35
+
36
+    // ---- 任务 ----
37
+    @PostMapping("/task")
38
+    public R<Map<String, Object>> createTask(@RequestBody Map<String, Object> req) {
39
+        return R.ok(patrolService.createTask(
40
+            Long.parseLong(String.valueOf(req.get("routeId"))),
41
+            Long.parseLong(String.valueOf(req.get("assigneeId"))),
42
+            (String) req.get("taskDate")));
43
+    }
44
+
45
+    @GetMapping("/task/today")
46
+    public R<List<Map<String, Object>>> todayTasks(@RequestParam Long userId) {
47
+        return R.ok(patrolService.getTodayTasks(userId));
48
+    }
49
+
50
+    @PutMapping("/task/{id}/start")
51
+    public R<Map<String, Object>> startTask(@PathVariable Long id) {
52
+        return R.ok(patrolService.startTask(id));
53
+    }
54
+
55
+    @PutMapping("/task/{id}/complete")
56
+    public R<Map<String, Object>> completeTask(@PathVariable Long id, @RequestParam double distance) {
57
+        return R.ok(patrolService.completeTask(id, distance));
58
+    }
59
+
60
+    // ---- 巡检记录 ----
61
+    @PostMapping("/record")
62
+    public R<Map<String, Object>> record(@RequestBody Map<String, Object> req) {
63
+        @SuppressWarnings("unchecked")
64
+        List<Map<String, Object>> items = (List<Map<String, Object>>) req.getOrDefault("checkItems", List.of());
65
+        return R.ok(patrolService.recordCheck(
66
+            Long.parseLong(String.valueOf(req.get("taskId"))),
67
+            (int) req.get("pointSeq"),
68
+            req.get("deviceId") != null ? Long.parseLong(String.valueOf(req.get("deviceId"))) : null,
69
+            items,
70
+            ((Number) req.get("lng")).doubleValue(),
71
+            ((Number) req.get("lat")).doubleValue()));
72
+    }
73
+
74
+    @GetMapping("/record/list/{taskId}")
75
+    public R<List<Map<String, Object>>> records(@PathVariable Long taskId) {
76
+        return R.ok(patrolService.getTaskRecords(taskId));
77
+    }
78
+
79
+    // ---- 问题上报 ----
80
+    @PostMapping("/issue/report")
81
+    public R<Map<String, Object>> reportIssue(@RequestBody Map<String, Object> req) {
82
+        @SuppressWarnings("unchecked")
83
+        List<String> photos = (List<String>) req.getOrDefault("photoUrls", List.of());
84
+        return R.ok(patrolService.reportIssue(
85
+            Long.parseLong(String.valueOf(req.get("taskId"))),
86
+            req.get("deviceId") != null ? Long.parseLong(String.valueOf(req.get("deviceId"))) : null,
87
+            (String) req.get("issueType"), (String) req.get("description"),
88
+            photos,
89
+            ((Number) req.get("lng")).doubleValue(),
90
+            ((Number) req.get("lat")).doubleValue()));
91
+    }
92
+
93
+    // ---- 统计 ----
94
+    @GetMapping("/stats")
95
+    public R<Map<String, Object>> stats(@RequestParam String area,
96
+                                         @RequestParam String start,
97
+                                         @RequestParam String end) {
98
+        return R.ok(patrolService.getStats(area, LocalDate.parse(start), LocalDate.parse(end)));
99
+    }
100
+}

+ 186
- 0
wm-patrol/src/main/java/com/water/patrol/controller/PatrolCoreController.java Просмотреть файл

@@ -0,0 +1,186 @@
1
+package com.water.patrol.controller;
2
+
3
+import com.water.common.core.result.R;
4
+import com.water.patrol.service.PatrolCoreService;
5
+import com.water.patrol.service.PatrolWoService;
6
+import io.swagger.v3.oas.annotations.Operation;
7
+import io.swagger.v3.oas.annotations.tags.Tag;
8
+import lombok.RequiredArgsConstructor;
9
+import org.springframework.web.bind.annotation.*;
10
+
11
+import java.util.List;
12
+import java.util.Map;
13
+
14
+/**
15
+ * 巡检管理核心控制器 - Issue #86
16
+ * PAT-01~PAT-06
17
+ */
18
+@Tag(name = "巡检管理核心")
19
+@RestController
20
+@RequestMapping("/patrol/core")
21
+@RequiredArgsConstructor
22
+public class PatrolCoreController {
23
+
24
+    private final PatrolCoreService coreService;
25
+    private final PatrolWoService woService;
26
+
27
+    // ========== PAT-01 巡检总览 ==========
28
+
29
+    @GetMapping("/overview")
30
+    @Operation(summary = "巡检总览数据")
31
+    public R<Map<String, Object>> overview() {
32
+        return R.ok(coreService.getOverview());
33
+    }
34
+
35
+    @GetMapping("/overview/today-tasks")
36
+    @Operation(summary = "今日任务")
37
+    public R<List<Map<String, Object>>> todayTasks() {
38
+        return R.ok(coreService.getTodayTasks());
39
+    }
40
+
41
+    @GetMapping("/overview/recent-issues")
42
+    @Operation(summary = "最近问题")
43
+    public R<List<Map<String, Object>>> recentIssues(@RequestParam(defaultValue = "10") int limit) {
44
+        return R.ok(coreService.getRecentIssues(limit));
45
+    }
46
+
47
+    // ========== PAT-02 任务轨迹 ==========
48
+
49
+    @PostMapping("/track/record")
50
+    @Operation(summary = "记录GPS轨迹点")
51
+    public R<Map<String, Object>> recordTrack(@RequestBody Map<String, Object> req) {
52
+        Long taskId = Long.parseLong(String.valueOf(req.get("taskId")));
53
+        Long workerId = req.get("workerId") != null ? Long.parseLong(String.valueOf(req.get("workerId"))) : null;
54
+        double lng = ((Number) req.get("lng")).doubleValue();
55
+        double lat = ((Number) req.get("lat")).doubleValue();
56
+        double speed = req.get("speed") != null ? ((Number) req.get("speed")).doubleValue() : 0;
57
+        double accuracy = req.get("accuracy") != null ? ((Number) req.get("accuracy")).doubleValue() : 0;
58
+        return R.ok(coreService.recordTrackPoint(taskId, workerId, lng, lat, speed, accuracy));
59
+    }
60
+
61
+    @GetMapping("/track/{taskId}")
62
+    @Operation(summary = "获取任务轨迹")
63
+    public R<List<Map<String, Object>>> getTrack(@PathVariable Long taskId) {
64
+        return R.ok(coreService.getTrack(taskId));
65
+    }
66
+
67
+    @GetMapping("/track/{taskId}/replay")
68
+    @Operation(summary = "轨迹回放")
69
+    public R<Map<String, Object>> replayTrack(@PathVariable Long taskId) {
70
+        return R.ok(coreService.replayTrack(taskId));
71
+    }
72
+
73
+    @GetMapping("/track/{taskId}/stats")
74
+    @Operation(summary = "轨迹统计")
75
+    public R<Map<String, Object>> trackStats(@PathVariable Long taskId) {
76
+        return R.ok(coreService.getTrackStats(taskId));
77
+    }
78
+
79
+    // ========== PAT-03 任务台账 ==========
80
+
81
+    @GetMapping("/ledger/tasks")
82
+    @Operation(summary = "任务台账")
83
+    public R<Map<String, Object>> taskLedger(
84
+            @RequestParam(required = false) String status,
85
+            @RequestParam(required = false) Long workerId,
86
+            @RequestParam(defaultValue = "1") int page,
87
+            @RequestParam(defaultValue = "20") int size) {
88
+        return R.ok(coreService.getTaskLedger(status, workerId, page, size));
89
+    }
90
+
91
+    // ========== PAT-04 设备台账 ==========
92
+
93
+    @GetMapping("/ledger/devices")
94
+    @Operation(summary = "设备台账")
95
+    public R<Map<String, Object>> deviceLedger(
96
+            @RequestParam(required = false) String type,
97
+            @RequestParam(required = false) String status,
98
+            @RequestParam(required = false) String area,
99
+            @RequestParam(defaultValue = "1") int page,
100
+            @RequestParam(defaultValue = "20") int size) {
101
+        return R.ok(coreService.getDeviceLedger(type, status, area, page, size));
102
+    }
103
+
104
+    @PostMapping("/device")
105
+    @Operation(summary = "新增设备")
106
+    public R<Map<String, Object>> createDevice(@RequestBody Map<String, Object> req) {
107
+        return R.ok(coreService.createDevice(
108
+            (String) req.get("deviceNo"),
109
+            (String) req.get("deviceName"),
110
+            (String) req.get("deviceType"),
111
+            (String) req.get("location"),
112
+            ((Number) req.getOrDefault("lng", 0)).doubleValue(),
113
+            ((Number) req.getOrDefault("lat", 0)).doubleValue(),
114
+            (String) req.get("area")));
115
+    }
116
+
117
+    @PutMapping("/device/{deviceId}/status")
118
+    @Operation(summary = "更新设备状态")
119
+    public R<Map<String, Object>> updateDeviceStatus(@PathVariable Long deviceId,
120
+                                                      @RequestBody Map<String, Object> req) {
121
+        return R.ok(coreService.updateDeviceStatus(deviceId, (String) req.get("status")));
122
+    }
123
+
124
+    // ========== PAT-05 工单管理 ==========
125
+
126
+    @PostMapping("/work-order")
127
+    @Operation(summary = "创建巡检工单")
128
+    public R<Map<String, Object>> createWo(@RequestBody Map<String, Object> req) {
129
+        Long taskId = req.get("taskId") != null ? Long.parseLong(String.valueOf(req.get("taskId"))) : null;
130
+        return R.ok(woService.create(
131
+            (String) req.get("issueType"),
132
+            (String) req.get("description"),
133
+            (String) req.get("severity"),
134
+            (String) req.get("location"),
135
+            req.get("lng") != null ? ((Number) req.get("lng")).doubleValue() : null,
136
+            req.get("lat") != null ? ((Number) req.get("lat")).doubleValue() : null,
137
+            taskId));
138
+    }
139
+
140
+    @GetMapping("/work-order/list")
141
+    @Operation(summary = "工单列表")
142
+    public R<Map<String, Object>> listWo(
143
+            @RequestParam(required = false) String status,
144
+            @RequestParam(required = false) String severity,
145
+            @RequestParam(defaultValue = "1") int page,
146
+            @RequestParam(defaultValue = "20") int size) {
147
+        return R.ok(woService.list(status, severity, page, size));
148
+    }
149
+
150
+    @GetMapping("/work-order/{woId}")
151
+    @Operation(summary = "工单详情")
152
+    public R<Map<String, Object>> woDetail(@PathVariable Long woId) {
153
+        Map<String, Object> detail = woService.getDetail(woId);
154
+        if (detail == null) {
155
+            return R.fail("工单不存在");
156
+        }
157
+        return R.ok(detail);
158
+    }
159
+
160
+    @GetMapping("/work-order/stats")
161
+    @Operation(summary = "工单统计")
162
+    public R<Map<String, Object>> woStats() {
163
+        return R.ok(woService.stats());
164
+    }
165
+
166
+    // ========== PAT-06 工单处理 ==========
167
+
168
+    @PutMapping("/work-order/{woId}/assign")
169
+    @Operation(summary = "分派工单")
170
+    public R<Map<String, Object>> assignWo(@PathVariable Long woId, @RequestBody Map<String, Object> req) {
171
+        Long assigneeId = Long.parseLong(String.valueOf(req.get("assigneeId")));
172
+        return R.ok(woService.assign(woId, assigneeId, (String) req.get("assigneeName")));
173
+    }
174
+
175
+    @PutMapping("/work-order/{woId}/process")
176
+    @Operation(summary = "处理工单")
177
+    public R<Map<String, Object>> processWo(@PathVariable Long woId, @RequestBody Map<String, Object> req) {
178
+        return R.ok(woService.process(woId, (String) req.get("resolution")));
179
+    }
180
+
181
+    @PutMapping("/work-order/{woId}/resolve")
182
+    @Operation(summary = "解决工单")
183
+    public R<Map<String, Object>> resolveWo(@PathVariable Long woId) {
184
+        return R.ok(woService.resolve(woId));
185
+    }
186
+}

+ 16
- 0
wm-patrol/src/main/java/com/water/patrol/controller/PatrolDeviceController.java Просмотреть файл

@@ -0,0 +1,16 @@
1
+package com.water.patrol.controller;
2
+import com.water.common.core.result.R; import com.water.patrol.entity.PatrolDevice; import com.water.patrol.service.PatrolDeviceService;
3
+import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag;
4
+import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*;
5
+import java.math.BigDecimal; import java.util.Map;
6
+@Tag(name="设备台账") @RestController @RequestMapping("/api/patrol/device") @RequiredArgsConstructor
7
+public class PatrolDeviceController {
8
+    private final PatrolDeviceService svc;
9
+    @Operation(summary="注册") @PostMapping public R<PatrolDevice> create(@RequestParam String deviceNo, @RequestParam String deviceName, @RequestParam String deviceType, @RequestParam String location, @RequestParam BigDecimal lng, @RequestParam BigDecimal lat) { return R.ok(svc.create(deviceNo,deviceName,deviceType,location,lng,lat)); }
10
+    @Operation(summary="列表") @GetMapping public R<Map<String,Object>> list(@RequestParam(required=false) String deviceType, @RequestParam(required=false) String status, @RequestParam(required=false) String keyword, @RequestParam(defaultValue="1") int page, @RequestParam(defaultValue="20") int size) { return R.ok(svc.list(deviceType,status,keyword,page,size)); }
11
+    @Operation(summary="详情") @GetMapping("/{id}") public R<PatrolDevice> detail(@PathVariable Long id) { return R.ok(svc.getDetail(id)); }
12
+    @Operation(summary="状态") @PutMapping("/{id}/status") public R<String> updateStatus(@PathVariable Long id, @RequestParam String status) { svc.updateStatus(id,status); return R.ok("OK"); }
13
+    @Operation(summary="维护") @PutMapping("/{id}/maintenance") public R<String> maintenance(@PathVariable Long id) { svc.recordMaintenance(id); return R.ok("OK"); }
14
+    @Operation(summary="统计") @GetMapping("/stats") public R<Map<String,Object>> stats() { return R.ok(svc.getStats()); }
15
+    @Operation(summary="删除") @DeleteMapping("/{id}") public R<String> delete(@PathVariable Long id) { svc.delete(id); return R.ok("OK"); }
16
+}

+ 15
- 0
wm-patrol/src/main/java/com/water/patrol/controller/PatrolFormController.java Просмотреть файл

@@ -0,0 +1,15 @@
1
+package com.water.patrol.controller;
2
+import com.water.common.core.result.R; import com.water.patrol.entity.PatrolForm; import com.water.patrol.service.PatrolFormService;
3
+import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag;
4
+import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*;
5
+import java.util.Map;
6
+@Tag(name="巡检表单") @RestController @RequestMapping("/api/patrol/form") @RequiredArgsConstructor
7
+public class PatrolFormController {
8
+    private final PatrolFormService svc;
9
+    @Operation(summary="创建表单") @PostMapping public R<PatrolForm> create(@RequestBody Map<String,Object> req) { return R.ok(svc.create((String)req.get("formName"),(String)req.get("formCode"),(String)req.get("formType"),(String)req.get("fieldConfig"))); }
10
+    @Operation(summary="表单列表") @GetMapping public R<Map<String,Object>> list(@RequestParam(required=false) String keyword, @RequestParam(required=false) String formType, @RequestParam(required=false) String status, @RequestParam(defaultValue="1") int page, @RequestParam(defaultValue="20") int size) { return R.ok(svc.list(keyword,formType,status,page,size)); }
11
+    @Operation(summary="表单详情") @GetMapping("/{id}") public R<PatrolForm> detail(@PathVariable Long id) { return R.ok(svc.getDetail(id)); }
12
+    @Operation(summary="更新表单") @PutMapping("/{id}") public R<PatrolForm> update(@PathVariable Long id, @RequestBody PatrolForm patch) { return R.ok(svc.update(id,patch)); }
13
+    @Operation(summary="删除表单") @DeleteMapping("/{id}") public R<String> delete(@PathVariable Long id) { svc.delete(id); return R.ok("OK"); }
14
+    @Operation(summary="表单统计") @GetMapping("/stats") public R<Map<String,Object>> stats() { return R.ok(svc.getStats()); }
15
+}

+ 15
- 0
wm-patrol/src/main/java/com/water/patrol/controller/PatrolLedgerController.java Просмотреть файл

@@ -0,0 +1,15 @@
1
+package com.water.patrol.controller;
2
+import com.water.common.core.result.R; import com.water.patrol.entity.PatrolTask; import com.water.patrol.service.PatrolLedgerService;
3
+import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag;
4
+import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*;
5
+import java.util.Map;
6
+@Tag(name="巡检台账") @RestController @RequestMapping("/api/patrol/ledger") @RequiredArgsConstructor
7
+public class PatrolLedgerController {
8
+    private final PatrolLedgerService svc;
9
+    @Operation(summary="列表") @GetMapping public R<Map<String,Object>> list(@RequestParam(required=false) String status, @RequestParam(required=false) String workerName, @RequestParam(required=false) String startDate, @RequestParam(required=false) String endDate, @RequestParam(defaultValue="1") int page, @RequestParam(defaultValue="20") int size) { return R.ok(svc.listTasks(status,workerName,startDate,endDate,page,size)); }
10
+    @Operation(summary="详情") @GetMapping("/{id}") public R<PatrolTask> detail(@PathVariable Long id) { return R.ok(svc.getDetail(id)); }
11
+    @Operation(summary="创建") @PostMapping public R<PatrolTask> create(@RequestBody Map<String,Object> req) { return R.ok(svc.createTask((String)req.get("taskName"),((Number)req.get("routeId")).longValue(),((Number)req.get("workerId")).longValue(),(String)req.get("workerName"))); }
12
+    @Operation(summary="开始") @PutMapping("/{id}/start") public R<String> start(@PathVariable Long id) { svc.startTask(id); return R.ok("OK"); }
13
+    @Operation(summary="完成") @PutMapping("/{id}/complete") public R<String> complete(@PathVariable Long id, @RequestParam Integer completedCheckpoints, @RequestParam Double distance) { svc.completeTask(id,completedCheckpoints,distance); return R.ok("OK"); }
14
+    @Operation(summary="统计") @GetMapping("/stats") public R<Map<String,Object>> stats() { return R.ok(svc.getStats()); }
15
+}

+ 11
- 0
wm-patrol/src/main/java/com/water/patrol/controller/PatrolOverviewController.java Просмотреть файл

@@ -0,0 +1,11 @@
1
+package com.water.patrol.controller;
2
+import com.water.common.core.result.R; import com.water.patrol.service.PatrolOverviewService;
3
+import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag;
4
+import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*;
5
+import java.util.Map;
6
+@Tag(name="巡检总览") @RestController @RequestMapping("/api/patrol/overview") @RequiredArgsConstructor
7
+public class PatrolOverviewController {
8
+    private final PatrolOverviewService svc;
9
+    @Operation(summary="总览") @GetMapping public R<Map<String,Object>> overview() { return R.ok(svc.getOverview()); }
10
+    @Operation(summary="月度统计") @GetMapping("/monthly") public R<Map<String,Object>> monthly(@RequestParam int year, @RequestParam int month) { return R.ok(svc.getMonthlyStats(year,month)); }
11
+}

+ 126
- 0
wm-patrol/src/main/java/com/water/patrol/controller/PatrolProblemController.java Просмотреть файл

@@ -0,0 +1,126 @@
1
+package com.water.patrol.controller;
2
+
3
+import com.water.patrol.entity.PatrolProblem;
4
+import com.water.patrol.service.PatrolProblemService;
5
+import org.springframework.beans.factory.annotation.Autowired;
6
+import org.springframework.http.ResponseEntity;
7
+import org.springframework.web.bind.annotation.*;
8
+
9
+import java.util.List;
10
+
11
+@RestController
12
+@RequestMapping("/api/patrol/problems")
13
+public class PatrolProblemController {
14
+
15
+    @Autowired
16
+    private PatrolProblemService patrolProblemService;
17
+
18
+    /**
19
+     * 创建巡检问题
20
+     */
21
+    @PostMapping
22
+    public ResponseEntity<PatrolProblem> createProblem(@RequestBody PatrolProblem patrolProblem) {
23
+        PatrolProblem created = patrolProblemService.createProblem(patrolProblem);
24
+        return ResponseEntity.ok(created);
25
+    }
26
+
27
+    /**
28
+     * 根据ID查询问题
29
+     */
30
+    @GetMapping("/{id}")
31
+    public ResponseEntity<PatrolProblem> getProblemById(@PathVariable Long id) {
32
+        PatrolProblem problem = patrolProblemService.getProblemById(id);
33
+        if (problem != null) {
34
+            return ResponseEntity.ok(problem);
35
+        }
36
+        return ResponseEntity.notFound().build();
37
+    }
38
+
39
+    /**
40
+     * 根据问题编号查询
41
+     */
42
+    @GetMapping("/problem-no/{problemNo}")
43
+    public ResponseEntity<PatrolProblem> getProblemByProblemNo(@PathVariable String problemNo) {
44
+        PatrolProblem problem = patrolProblemService.getProblemByProblemNo(problemNo);
45
+        if (problem != null) {
46
+            return ResponseEntity.ok(problem);
47
+        }
48
+        return ResponseEntity.notFound().build();
49
+    }
50
+
51
+    /**
52
+     * 根据任务ID查询问题列表
53
+     */
54
+    @GetMapping("/task/{taskId}")
55
+    public ResponseEntity<List<PatrolProblem>> getProblemsByTaskId(@PathVariable Long taskId) {
56
+        List<PatrolProblem> problems = patrolProblemService.getProblemsByTaskId(taskId);
57
+        return ResponseEntity.ok(problems);
58
+    }
59
+
60
+    /**
61
+     * 根据状态查询问题列表
62
+     */
63
+    @GetMapping("/status/{status}")
64
+    public ResponseEntity<List<PatrolProblem>> getProblemsByStatus(@PathVariable String status) {
65
+        List<PatrolProblem> problems = patrolProblemService.getProblemsByStatus(status);
66
+        return ResponseEntity.ok(problems);
67
+    }
68
+
69
+    /**
70
+     * 根据设备ID查询问题列表
71
+     */
72
+    @GetMapping("/device/{deviceId}")
73
+    public ResponseEntity<List<PatrolProblem>> getProblemsByDeviceId(@PathVariable Long deviceId) {
74
+        List<PatrolProblem> problems = patrolProblemService.getProblemsByDeviceId(deviceId);
75
+        return ResponseEntity.ok(problems);
76
+    }
77
+
78
+    /**
79
+     * 更新问题信息
80
+     */
81
+    @PutMapping("/{id}")
82
+    public ResponseEntity<PatrolProblem> updateProblem(@PathVariable Long id, @RequestBody PatrolProblem patrolProblem) {
83
+        patrolProblem.setId(id);
84
+        PatrolProblem updated = patrolProblemService.updateProblem(patrolProblem);
85
+        if (updated != null) {
86
+            return ResponseEntity.ok(updated);
87
+        }
88
+        return ResponseEntity.notFound().build();
89
+    }
90
+
91
+    /**
92
+     * 更新问题状态
93
+     */
94
+    @PutMapping("/{id}/status")
95
+    public ResponseEntity<Boolean> updateProblemStatus(@PathVariable Long id, @RequestParam String status, @RequestParam(required = false) Long workOrderId) {
96
+        boolean updated = patrolProblemService.updateProblemStatus(id, status, workOrderId);
97
+        return ResponseEntity.ok(updated);
98
+    }
99
+
100
+    /**
101
+     * 删除问题
102
+     */
103
+    @DeleteMapping("/{id}")
104
+    public ResponseEntity<Boolean> deleteProblem(@PathVariable Long id) {
105
+        boolean deleted = patrolProblemService.deleteProblem(id);
106
+        return ResponseEntity.ok(deleted);
107
+    }
108
+
109
+    /**
110
+     * 自动创建工单
111
+     */
112
+    @PostMapping("/{id}/auto-create-work-order")
113
+    public ResponseEntity<Boolean> autoCreateWorkOrder(@PathVariable Long id) {
114
+        boolean created = patrolProblemService.autoCreateWorkOrder(id);
115
+        return ResponseEntity.ok(created);
116
+    }
117
+
118
+    /**
119
+     * 获取问题统计信息
120
+     */
121
+    @GetMapping("/statistics")
122
+    public ResponseEntity<PatrolProblemService.ProblemStatistics> getProblemStatistics() {
123
+        PatrolProblemService.ProblemStatistics statistics = patrolProblemService.getProblemStatistics();
124
+        return ResponseEntity.ok(statistics);
125
+    }
126
+}

+ 16
- 0
wm-patrol/src/main/java/com/water/patrol/controller/PatrolRouteSetupController.java Просмотреть файл

@@ -0,0 +1,16 @@
1
+package com.water.patrol.controller;
2
+import com.water.common.core.result.R; import com.water.patrol.entity.PatrolRouteSetup; import com.water.patrol.service.PatrolRouteSetupService;
3
+import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag;
4
+import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*;
5
+import java.util.Map;
6
+@Tag(name="巡检路线设置") @RestController @RequestMapping("/api/patrol/route-setup") @RequiredArgsConstructor
7
+public class PatrolRouteSetupController {
8
+    private final PatrolRouteSetupService svc;
9
+    @Operation(summary="创建路线") @PostMapping public R<PatrolRouteSetup> create(@RequestBody Map<String,Object> req) { return R.ok(svc.create((String)req.get("routeName"),(String)req.get("routeCode"),((Number)req.get("areaId")).longValue(),(String)req.get("areaName"),(Integer)req.get("checkpointCount"),(String)req.get("checkpointList"),(Double)req.get("totalDistance"),(Integer)req.get("estimatedMinutes"))); }
10
+    @Operation(summary="路线列表") @GetMapping public R<Map<String,Object>> list(@RequestParam(required=false) String keyword, @RequestParam(required=false) String status, @RequestParam(required=false) Long areaId, @RequestParam(defaultValue="1") int page, @RequestParam(defaultValue="20") int size) { return R.ok(svc.list(keyword,status,areaId,page,size)); }
11
+    @Operation(summary="路线详情") @GetMapping("/{id}") public R<PatrolRouteSetup> detail(@PathVariable Long id) { return R.ok(svc.getDetail(id)); }
12
+    @Operation(summary="更新路线") @PutMapping("/{id}") public R<PatrolRouteSetup> update(@PathVariable Long id, @RequestBody PatrolRouteSetup patch) { return R.ok(svc.update(id,patch)); }
13
+    @Operation(summary="更新状态") @PutMapping("/{id}/status") public R<String> updateStatus(@PathVariable Long id, @RequestParam String status) { svc.updateStatus(id,status); return R.ok("OK"); }
14
+    @Operation(summary="删除路线") @DeleteMapping("/{id}") public R<String> delete(@PathVariable Long id) { svc.delete(id); return R.ok("OK"); }
15
+    @Operation(summary="路线统计") @GetMapping("/stats") public R<Map<String,Object>> stats() { return R.ok(svc.getStats()); }
16
+}

+ 16
- 0
wm-patrol/src/main/java/com/water/patrol/controller/PatrolTemplateController.java Просмотреть файл

@@ -0,0 +1,16 @@
1
+package com.water.patrol.controller;
2
+import com.water.common.core.result.R; import com.water.patrol.entity.PatrolTemplate; import com.water.patrol.service.PatrolTemplateService;
3
+import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag;
4
+import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*;
5
+import java.util.Map;
6
+@Tag(name="巡检模板") @RestController @RequestMapping("/api/patrol/template") @RequiredArgsConstructor
7
+public class PatrolTemplateController {
8
+    private final PatrolTemplateService svc;
9
+    @Operation(summary="创建模板") @PostMapping public R<PatrolTemplate> create(@RequestBody Map<String,Object> req) { return R.ok(svc.create((String)req.get("templateName"),(String)req.get("templateCode"),((Number)req.get("routeId")).longValue(),(String)req.get("routeName"),((Number)req.get("formId")).longValue(),(String)req.get("formName"),(String)req.get("scheduleType"),(String)req.get("scheduleConfig"))); }
10
+    @Operation(summary="模板列表") @GetMapping public R<Map<String,Object>> list(@RequestParam(required=false) String keyword, @RequestParam(required=false) String status, @RequestParam(required=false) String scheduleType, @RequestParam(defaultValue="1") int page, @RequestParam(defaultValue="20") int size) { return R.ok(svc.list(keyword,status,scheduleType,page,size)); }
11
+    @Operation(summary="模板详情") @GetMapping("/{id}") public R<PatrolTemplate> detail(@PathVariable Long id) { return R.ok(svc.getDetail(id)); }
12
+    @Operation(summary="更新模板") @PutMapping("/{id}") public R<PatrolTemplate> update(@PathVariable Long id, @RequestBody PatrolTemplate patch) { return R.ok(svc.update(id,patch)); }
13
+    @Operation(summary="更新状态") @PutMapping("/{id}/status") public R<String> updateStatus(@PathVariable Long id, @RequestParam String status) { svc.updateStatus(id,status); return R.ok("OK"); }
14
+    @Operation(summary="删除模板") @DeleteMapping("/{id}") public R<String> delete(@PathVariable Long id) { svc.delete(id); return R.ok("OK"); }
15
+    @Operation(summary="模板统计") @GetMapping("/stats") public R<Map<String,Object>> stats() { return R.ok(svc.getStats()); }
16
+}

+ 13
- 0
wm-patrol/src/main/java/com/water/patrol/controller/PatrolTrackController.java Просмотреть файл

@@ -0,0 +1,13 @@
1
+package com.water.patrol.controller;
2
+import com.water.common.core.result.R; import com.water.patrol.entity.PatrolTrackPoint; import com.water.patrol.service.PatrolTrackService;
3
+import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag;
4
+import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*;
5
+import java.math.BigDecimal; import java.util.*;
6
+@Tag(name="巡检轨迹") @RestController @RequestMapping("/api/patrol/track") @RequiredArgsConstructor
7
+public class PatrolTrackController {
8
+    private final PatrolTrackService svc;
9
+    @Operation(summary="记录GPS") @PostMapping public R<PatrolTrackPoint> record(@RequestParam Long taskId, @RequestParam Long workerId, @RequestParam BigDecimal lng, @RequestParam BigDecimal lat, @RequestParam(required=false) BigDecimal speed, @RequestParam(required=false) BigDecimal accuracy, @RequestParam(required=false) String address) { return R.ok(svc.recordPoint(taskId,workerId,lng,lat,speed,accuracy,address)); }
10
+    @Operation(summary="轨迹") @GetMapping("/{taskId}") public R<List<PatrolTrackPoint>> getTrack(@PathVariable Long taskId) { return R.ok(svc.getTrack(taskId)); }
11
+    @Operation(summary="摘要") @GetMapping("/{taskId}/summary") public R<Map<String,Object>> summary(@PathVariable Long taskId) { return R.ok(svc.getTrackSummary(taskId)); }
12
+    @Operation(summary="导出") @GetMapping("/{taskId}/export") public R<List<Map<String,Object>>> export(@PathVariable Long taskId) { return R.ok(svc.exportTrack(taskId)); }
13
+}

+ 17
- 0
wm-patrol/src/main/java/com/water/patrol/controller/PatrolWoController.java Просмотреть файл

@@ -0,0 +1,17 @@
1
+package com.water.patrol.controller;
2
+import com.water.common.core.result.R; import com.water.patrol.entity.PatrolWorkOrder; import com.water.patrol.service.PatrolWorkOrderService;
3
+import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag;
4
+import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*;
5
+import java.util.Map;
6
+@Tag(name="巡检工单") @RestController @RequestMapping("/api/patrol/work-order") @RequiredArgsConstructor
7
+public class PatrolWoController {
8
+    private final PatrolWorkOrderService svc;
9
+    @Operation(summary="创建") @PostMapping public R<PatrolWorkOrder> create(@RequestBody Map<String,Object> req) { return R.ok(svc.create(((Number)req.get("taskId")).longValue(),(String)req.get("issueType"),(String)req.get("description"),(String)req.get("photos"),(String)req.get("severity"))); }
10
+    @Operation(summary="列表") @GetMapping public R<Map<String,Object>> list(@RequestParam(required=false) String status, @RequestParam(required=false) String severity, @RequestParam(required=false) String keyword, @RequestParam(defaultValue="1") int page, @RequestParam(defaultValue="20") int size) { return R.ok(svc.list(status,severity,keyword,page,size)); }
11
+    @Operation(summary="详情") @GetMapping("/{id}") public R<PatrolWorkOrder> detail(@PathVariable Long id) { return R.ok(svc.getDetail(id)); }
12
+    @Operation(summary="分配") @PutMapping("/{id}/assign") public R<String> assign(@PathVariable Long id, @RequestParam Long assigneeId, @RequestParam String assigneeName) { svc.assign(id,assigneeId,assigneeName); return R.ok("OK"); }
13
+    @Operation(summary="开始") @PutMapping("/{id}/start") public R<String> start(@PathVariable Long id) { svc.startProcess(id); return R.ok("OK"); }
14
+    @Operation(summary="解决") @PutMapping("/{id}/resolve") public R<String> resolve(@PathVariable Long id, @RequestParam String resolution) { svc.resolve(id,resolution); return R.ok("OK"); }
15
+    @Operation(summary="关闭") @PutMapping("/{id}/close") public R<String> close(@PathVariable Long id) { svc.close(id); return R.ok("OK"); }
16
+    @Operation(summary="统计") @GetMapping("/stats") public R<Map<String,Object>> stats() { return R.ok(svc.getStats()); }
17
+}

+ 151
- 0
wm-patrol/src/main/java/com/water/patrol/controller/WorkOrderController.java Просмотреть файл

@@ -0,0 +1,151 @@
1
+package com.water.patrol.controller;
2
+
3
+import com.water.patrol.entity.WorkOrder;
4
+import com.water.patrol.service.WorkOrderService;
5
+import org.springframework.beans.factory.annotation.Autowired;
6
+import org.springframework.http.ResponseEntity;
7
+import org.springframework.web.bind.annotation.*;
8
+
9
+import java.util.List;
10
+
11
+@RestController
12
+@RequestMapping("/api/work-orders")
13
+public class WorkOrderController {
14
+
15
+    @Autowired
16
+    private WorkOrderService workOrderService;
17
+
18
+    /**
19
+     * 根据ID查询工单
20
+     */
21
+    @GetMapping("/{id}")
22
+    public ResponseEntity<WorkOrder> getWorkOrderById(@PathVariable Long id) {
23
+        WorkOrder workOrder = workOrderService.getWorkOrderById(id);
24
+        if (workOrder != null) {
25
+            return ResponseEntity.ok(workOrder);
26
+        }
27
+        return ResponseEntity.notFound().build();
28
+    }
29
+
30
+    /**
31
+     * 根据工单编号查询
32
+     */
33
+    @GetMapping("/order-no/{orderNo}")
34
+    public ResponseEntity<WorkOrder> getWorkOrderByOrderNo(@PathVariable String orderNo) {
35
+        WorkOrder workOrder = workOrderService.getWorkOrderByOrderNo(orderNo);
36
+        if (workOrder != null) {
37
+            return ResponseEntity.ok(workOrder);
38
+        }
39
+        return ResponseEntity.notFound().build();
40
+    }
41
+
42
+    /**
43
+     * 根据问题ID查询工单
44
+     */
45
+    @GetMapping("/problem/{problemId}")
46
+    public ResponseEntity<WorkOrder> getWorkOrderByProblemId(@PathVariable Long problemId) {
47
+        WorkOrder workOrder = workOrderService.getWorkOrderByProblemId(problemId);
48
+        if (workOrder != null) {
49
+            return ResponseEntity.ok(workOrder);
50
+        }
51
+        return ResponseEntity.notFound().build();
52
+    }
53
+
54
+    /**
55
+     * 根据状态查询工单列表
56
+     */
57
+    @GetMapping("/status/{status}")
58
+    public ResponseEntity<List<WorkOrder>> getWorkOrdersByStatus(@PathVariable String status) {
59
+        List<WorkOrder> workOrders = workOrderService.getWorkOrdersByStatus(status);
60
+        return ResponseEntity.ok(workOrders);
61
+    }
62
+
63
+    /**
64
+     * 根据处理人查询工单列表
65
+     */
66
+    @GetMapping("/assignee/{assigneeId}")
67
+    public ResponseEntity<List<WorkOrder>> getWorkOrdersByAssigneeId(@PathVariable Long assigneeId) {
68
+        List<WorkOrder> workOrders = workOrderService.getWorkOrdersByAssigneeId(assigneeId);
69
+        return ResponseEntity.ok(workOrders);
70
+    }
71
+
72
+    /**
73
+     * 根据处理状态查询工单列表
74
+     */
75
+    @GetMapping("/process-status/{processStatus}")
76
+    public ResponseEntity<List<WorkOrder>> getWorkOrdersByProcessStatus(@PathVariable String processStatus) {
77
+        List<WorkOrder> workOrders = workOrderService.getWorkOrdersByProcessStatus(processStatus);
78
+        return ResponseEntity.ok(workOrders);
79
+    }
80
+
81
+    /**
82
+     * 更新工单信息
83
+     */
84
+    @PutMapping("/{id}")
85
+    public ResponseEntity<WorkOrder> updateWorkOrder(@PathVariable Long id, @RequestBody WorkOrder workOrder) {
86
+        workOrder.setId(id);
87
+        WorkOrder updated = workOrderService.updateWorkOrder(workOrder);
88
+        if (updated != null) {
89
+            return ResponseEntity.ok(updated);
90
+        }
91
+        return ResponseEntity.notFound().build();
92
+    }
93
+
94
+    /**
95
+     * 更新工单状态
96
+     */
97
+    @PutMapping("/{id}/status")
98
+    public ResponseEntity<Boolean> updateWorkOrderStatus(@PathVariable Long id, 
99
+                                                       @RequestParam String status, 
100
+                                                       @RequestParam(required = false) String processStatus) {
101
+        boolean updated = workOrderService.updateWorkOrderStatus(id, status, processStatus);
102
+        return ResponseEntity.ok(updated);
103
+    }
104
+
105
+    /**
106
+     * 分派工单
107
+     */
108
+    @PutMapping("/{id}/assign")
109
+    public ResponseEntity<Boolean> assignWorkOrder(@PathVariable Long id, 
110
+                                                 @RequestParam Long assigneeId, 
111
+                                                 @RequestParam String assigneeName) {
112
+        boolean assigned = workOrderService.assignWorkOrder(id, assigneeId, assigneeName);
113
+        return ResponseEntity.ok(assigned);
114
+    }
115
+
116
+    /**
117
+     * 开始处理工单
118
+     */
119
+    @PutMapping("/{id}/start")
120
+    public ResponseEntity<Boolean> startWorkOrder(@PathVariable Long id) {
121
+        boolean started = workOrderService.startWorkOrder(id);
122
+        return ResponseEntity.ok(started);
123
+    }
124
+
125
+    /**
126
+     * 完成工单
127
+     */
128
+    @PutMapping("/{id}/complete")
129
+    public ResponseEntity<Boolean> completeWorkOrder(@PathVariable Long id, @RequestParam String solutionResult) {
130
+        boolean completed = workOrderService.completeWorkOrder(id, solutionResult);
131
+        return ResponseEntity.ok(completed);
132
+    }
133
+
134
+    /**
135
+     * 删除工单
136
+     */
137
+    @DeleteMapping("/{id}")
138
+    public ResponseEntity<Boolean> deleteWorkOrder(@PathVariable Long id) {
139
+        boolean deleted = workOrderService.deleteWorkOrder(id);
140
+        return ResponseEntity.ok(deleted);
141
+    }
142
+
143
+    /**
144
+     * 获取工单统计信息
145
+     */
146
+    @GetMapping("/statistics")
147
+    public ResponseEntity<WorkOrderService.WorkOrderStatistics> getWorkOrderStatistics() {
148
+        WorkOrderService.WorkOrderStatistics statistics = workOrderService.getWorkOrderStatistics();
149
+        return ResponseEntity.ok(statistics);
150
+    }
151
+}

+ 15
- 0
wm-patrol/src/main/java/com/water/patrol/entity/PatrolArea.java Просмотреть файл

@@ -0,0 +1,15 @@
1
+package com.water.patrol.entity;
2
+import com.baomidou.mybatisplus.annotation.*;
3
+import lombok.Data;
4
+import java.math.BigDecimal;
5
+import java.time.LocalDateTime;
6
+@Data @TableName("pat_area")
7
+public class PatrolArea {
8
+    @TableId(type = IdType.AUTO) private Long id;
9
+    private String areaName; private String areaCode; private String description;
10
+    private BigDecimal centerLng; private BigDecimal centerLat;
11
+    private String boundary; private Double radius;
12
+    private String status;
13
+    @TableField(fill = FieldFill.INSERT) private LocalDateTime createdAt;
14
+    @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updatedAt;
15
+}

+ 16
- 0
wm-patrol/src/main/java/com/water/patrol/entity/PatrolDevice.java Просмотреть файл

@@ -0,0 +1,16 @@
1
+package com.water.patrol.entity;
2
+import com.baomidou.mybatisplus.annotation.*;
3
+import lombok.Data;
4
+import java.math.BigDecimal;
5
+import java.time.LocalDate;
6
+import java.time.LocalDateTime;
7
+@Data @TableName("pat_device")
8
+public class PatrolDevice {
9
+    @TableId(type = IdType.AUTO) private Long id;
10
+    private String deviceNo; private String deviceName; private String deviceType;
11
+    private String location; private BigDecimal lng; private BigDecimal lat;
12
+    private String status; private LocalDate lastMaintenanceDate; private LocalDate nextMaintenanceDate;
13
+    private String remark;
14
+    @TableField(fill = FieldFill.INSERT) private LocalDateTime createdAt;
15
+    @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updatedAt;
16
+}

+ 13
- 0
wm-patrol/src/main/java/com/water/patrol/entity/PatrolForm.java Просмотреть файл

@@ -0,0 +1,13 @@
1
+package com.water.patrol.entity;
2
+import com.baomidou.mybatisplus.annotation.*;
3
+import lombok.Data;
4
+import java.time.LocalDateTime;
5
+@Data @TableName("pat_form")
6
+public class PatrolForm {
7
+    @TableId(type = IdType.AUTO) private Long id;
8
+    private String formName; private String formCode;
9
+    private String formType; private String fieldConfig;
10
+    private String status; private String remark;
11
+    @TableField(fill = FieldFill.INSERT) private LocalDateTime createdAt;
12
+    @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updatedAt;
13
+}

+ 19
- 0
wm-patrol/src/main/java/com/water/patrol/entity/PatrolIssueReport.java Просмотреть файл

@@ -0,0 +1,19 @@
1
+package com.water.patrol.entity;
2
+import com.baomidou.mybatisplus.annotation.*;
3
+import lombok.Data;
4
+import java.math.BigDecimal;
5
+import java.time.LocalDateTime;
6
+@Data @TableName("pat_issue_report")
7
+public class PatrolIssueReport {
8
+    @TableId(type = IdType.AUTO) private Long id;
9
+    private String reportNo; private Long taskId;
10
+    private Long reporterId; private String reporterName;
11
+    private String issueType; private String description;
12
+    private String photos; private String videos;
13
+    private BigDecimal lng; private BigDecimal lat;
14
+    private String address; private String severity;
15
+    private String status; private Long workOrderId;
16
+    private String remark;
17
+    @TableField(fill = FieldFill.INSERT) private LocalDateTime createdAt;
18
+    @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updatedAt;
19
+}

+ 31
- 0
wm-patrol/src/main/java/com/water/patrol/entity/PatrolProblem.java Просмотреть файл

@@ -0,0 +1,31 @@
1
+package com.water.patrol.entity;
2
+
3
+import lombok.Data;
4
+import java.time.LocalDateTime;
5
+import java.util.List;
6
+import java.util.Map;
7
+
8
+@Data
9
+public class PatrolProblem {
10
+    private Long id;
11
+    private String problemNo;
12
+    private Long taskId;
13
+    private Integer pointSeq;
14
+    private Long deviceId;
15
+    private String deviceName;
16
+    private String problemType;        // 设备故障/水质异常/安全隐患/环境卫生/其他
17
+    private String problemLevel;       // low/normal/high/critical
18
+    private String problemTitle;
19
+    private String problemDescription;
20
+    private String location;
21
+    private Double lng;
22
+    private Double lat;
23
+    private List<String> photoUrls;
24
+    private Long reporterId;
25
+    private String reporterName;
26
+    private LocalDateTime reportTime;
27
+    private String status;             // reported/processing/completed/closed
28
+    private Long workOrderId;
29
+    private LocalDateTime createdAt;
30
+    private LocalDateTime updatedAt;
31
+}

+ 15
- 0
wm-patrol/src/main/java/com/water/patrol/entity/PatrolRouteSetup.java Просмотреть файл

@@ -0,0 +1,15 @@
1
+package com.water.patrol.entity;
2
+import com.baomidou.mybatisplus.annotation.*;
3
+import lombok.Data;
4
+import java.time.LocalDateTime;
5
+@Data @TableName("pat_route_setup")
6
+public class PatrolRouteSetup {
7
+    @TableId(type = IdType.AUTO) private Long id;
8
+    private String routeName; private String routeCode;
9
+    private Long areaId; private String areaName;
10
+    private Integer checkpointCount; private String checkpointList;
11
+    private Double totalDistance; private Integer estimatedMinutes;
12
+    private String status; private String remark;
13
+    @TableField(fill = FieldFill.INSERT) private LocalDateTime createdAt;
14
+    @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updatedAt;
15
+}

+ 15
- 0
wm-patrol/src/main/java/com/water/patrol/entity/PatrolTask.java Просмотреть файл

@@ -0,0 +1,15 @@
1
+package com.water.patrol.entity;
2
+import com.baomidou.mybatisplus.annotation.*;
3
+import lombok.Data;
4
+import java.time.LocalDateTime;
5
+@Data @TableName("pat_task")
6
+public class PatrolTask {
7
+    @TableId(type = IdType.AUTO) private Long id;
8
+    private String taskNo; private String taskName; private Long routeId;
9
+    private Long workerId; private String workerName; private String status;
10
+    private LocalDateTime startTime; private LocalDateTime endTime;
11
+    private Integer checkpoints; private Integer completedCheckpoints; private Integer abnormalCount;
12
+    private Double distance; private String remark;
13
+    @TableField(fill = FieldFill.INSERT) private LocalDateTime createdAt;
14
+    @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updatedAt;
15
+}

+ 15
- 0
wm-patrol/src/main/java/com/water/patrol/entity/PatrolTemplate.java Просмотреть файл

@@ -0,0 +1,15 @@
1
+package com.water.patrol.entity;
2
+import com.baomidou.mybatisplus.annotation.*;
3
+import lombok.Data;
4
+import java.time.LocalDateTime;
5
+@Data @TableName("pat_template")
6
+public class PatrolTemplate {
7
+    @TableId(type = IdType.AUTO) private Long id;
8
+    private String templateName; private String templateCode;
9
+    private Long routeId; private String routeName;
10
+    private Long formId; private String formName;
11
+    private String scheduleType; private String scheduleConfig;
12
+    private String status; private String remark;
13
+    @TableField(fill = FieldFill.INSERT) private LocalDateTime createdAt;
14
+    @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updatedAt;
15
+}

+ 12
- 0
wm-patrol/src/main/java/com/water/patrol/entity/PatrolTrackPoint.java Просмотреть файл

@@ -0,0 +1,12 @@
1
+package com.water.patrol.entity;
2
+import com.baomidou.mybatisplus.annotation.*;
3
+import lombok.Data;
4
+import java.math.BigDecimal;
5
+import java.time.LocalDateTime;
6
+@Data @TableName("pat_track_point")
7
+public class PatrolTrackPoint {
8
+    @TableId(type = IdType.AUTO) private Long id;
9
+    private Long taskId; private Long workerId;
10
+    private BigDecimal lng; private BigDecimal lat; private BigDecimal speed; private BigDecimal accuracy;
11
+    private String address; private LocalDateTime timestamp;
12
+}

+ 14
- 0
wm-patrol/src/main/java/com/water/patrol/entity/PatrolWorkOrder.java Просмотреть файл

@@ -0,0 +1,14 @@
1
+package com.water.patrol.entity;
2
+import com.baomidou.mybatisplus.annotation.*;
3
+import lombok.Data;
4
+import java.time.LocalDateTime;
5
+@Data @TableName("pat_work_order")
6
+public class PatrolWorkOrder {
7
+    @TableId(type = IdType.AUTO) private Long id;
8
+    private String orderNo; private Long taskId; private String issueType;
9
+    private String description; private String photos; private String severity;
10
+    private Long assigneeId; private String assigneeName; private String status;
11
+    private String resolution; private LocalDateTime resolvedAt;
12
+    @TableField(fill = FieldFill.INSERT) private LocalDateTime createdAt;
13
+    @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updatedAt;
14
+}

+ 37
- 0
wm-patrol/src/main/java/com/water/patrol/entity/WorkOrder.java Просмотреть файл

@@ -0,0 +1,37 @@
1
+package com.water.patrol.entity;
2
+
3
+import lombok.Data;
4
+import java.time.LocalDateTime;
5
+import java.util.List;
6
+import java.util.Map;
7
+
8
+@Data
9
+public class WorkOrder {
10
+    private Long id;
11
+    private String orderNo;
12
+    private Long problemId;
13
+    private String orderType;        // 设备维修/水质处理/安全隐患处理/清洁/其他
14
+    private String priority;         // low/normal/high/critical
15
+    private String title;
16
+    private String description;
17
+    private String location;
18
+    private String contactPerson;
19
+    private String contactPhone;
20
+    private Long reporterId;
21
+    private String reporterName;
22
+    private Long assigneeId;
23
+    private String assigneeName;
24
+    private String status;           // pending/assigned/processing/completed/cancelled
25
+    private String processStatus;    // created/accepted/in_progress/completed
26
+    private Integer estimatedDuration;
27
+    private LocalDateTime actualStartTime;
28
+    private LocalDateTime actualEndTime;
29
+    private LocalDateTime completionTime;
30
+    private List<String> photosBefore;
31
+    private List<String> photosAfter;
32
+    private String solutionDescription;
33
+    private String solutionResult;
34
+    private String customerFeedback;
35
+    private LocalDateTime createdAt;
36
+    private LocalDateTime updatedAt;
37
+}

+ 18
- 0
wm-patrol/src/main/java/com/water/patrol/entity/WorkOrderProcess.java Просмотреть файл

@@ -0,0 +1,18 @@
1
+package com.water.patrol.entity;
2
+
3
+import lombok.Data;
4
+import java.time.LocalDateTime;
5
+import java.util.List;
6
+
7
+@Data
8
+public class WorkOrderProcess {
9
+    private Long id;
10
+    private Long workOrderId;
11
+    private String processStep;      // created/accepted/in_progress/completed
12
+    private Long processorId;
13
+    private String processorName;
14
+    private String action;           // create/assign/start/complete/cancel
15
+    private String comment;
16
+    private List<String> photos;
17
+    private LocalDateTime createdAt;
18
+}

+ 5
- 0
wm-patrol/src/main/java/com/water/patrol/mapper/PatrolAreaMapper.java Просмотреть файл

@@ -0,0 +1,5 @@
1
+package com.water.patrol.mapper;
2
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
3
+import com.water.patrol.entity.PatrolArea;
4
+import org.apache.ibatis.annotations.Mapper;
5
+@Mapper public interface PatrolAreaMapper extends BaseMapper<PatrolArea> {}

+ 5
- 0
wm-patrol/src/main/java/com/water/patrol/mapper/PatrolDeviceMapper.java Просмотреть файл

@@ -0,0 +1,5 @@
1
+package com.water.patrol.mapper;
2
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
3
+import com.water.patrol.entity.PatrolDevice;
4
+import org.apache.ibatis.annotations.Mapper;
5
+@Mapper public interface PatrolDeviceMapper extends BaseMapper<PatrolDevice> {}

+ 5
- 0
wm-patrol/src/main/java/com/water/patrol/mapper/PatrolFormMapper.java Просмотреть файл

@@ -0,0 +1,5 @@
1
+package com.water.patrol.mapper;
2
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
3
+import com.water.patrol.entity.PatrolForm;
4
+import org.apache.ibatis.annotations.Mapper;
5
+@Mapper public interface PatrolFormMapper extends BaseMapper<PatrolForm> {}

+ 5
- 0
wm-patrol/src/main/java/com/water/patrol/mapper/PatrolIssueReportMapper.java Просмотреть файл

@@ -0,0 +1,5 @@
1
+package com.water.patrol.mapper;
2
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
3
+import com.water.patrol.entity.PatrolIssueReport;
4
+import org.apache.ibatis.annotations.Mapper;
5
+@Mapper public interface PatrolIssueReportMapper extends BaseMapper<PatrolIssueReport> {}

+ 47
- 0
wm-patrol/src/main/java/com/water/patrol/mapper/PatrolProblemMapper.java Просмотреть файл

@@ -0,0 +1,47 @@
1
+package com.water.patrol.mapper;
2
+
3
+import com.water.patrol.entity.PatrolProblem;
4
+import org.apache.ibatis.annotations.Mapper;
5
+import org.apache.ibatis.annotations.Param;
6
+import java.time.LocalDateTime;
7
+import java.util.List;
8
+
9
+@Mapper
10
+public interface PatrolProblemMapper {
11
+    
12
+    // 插入巡检问题
13
+    int insert(PatrolProblem patrolProblem);
14
+    
15
+    // 根据ID查询
16
+    PatrolProblem selectById(Long id);
17
+    
18
+    // 根据问题编号查询
19
+    PatrolProblem selectByProblemNo(String problemNo);
20
+    
21
+    // 根据任务ID查询问题列表
22
+    List<PatrolProblem> selectByTaskId(Long taskId);
23
+    
24
+    // 根据状态查询问题列表
25
+    List<PatrolProblem> selectByStatus(String status);
26
+    
27
+    // 根据设备ID查询问题列表
28
+    List<PatrolProblem> selectByDeviceId(Long deviceId);
29
+    
30
+    // 更新问题信息
31
+    int update(PatrolProblem patrolProblem);
32
+    
33
+    // 更新问题状态
34
+    int updateStatus(@Param("id") Long id, @Param("status") String status, @Param("workOrderId") Long workOrderId);
35
+    
36
+    // 删除问题
37
+    int deleteById(Long id);
38
+    
39
+    // 生成问题编号
40
+    String generateProblemNo();
41
+    
42
+    // 获取问题总数
43
+    int countAll();
44
+    
45
+    // 获取指定状态的问题数
46
+    int countByStatus(String status);
47
+}

+ 5
- 0
wm-patrol/src/main/java/com/water/patrol/mapper/PatrolRouteSetupMapper.java Просмотреть файл

@@ -0,0 +1,5 @@
1
+package com.water.patrol.mapper;
2
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
3
+import com.water.patrol.entity.PatrolRouteSetup;
4
+import org.apache.ibatis.annotations.Mapper;
5
+@Mapper public interface PatrolRouteSetupMapper extends BaseMapper<PatrolRouteSetup> {}

+ 5
- 0
wm-patrol/src/main/java/com/water/patrol/mapper/PatrolTaskMapper.java Просмотреть файл

@@ -0,0 +1,5 @@
1
+package com.water.patrol.mapper;
2
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
3
+import com.water.patrol.entity.PatrolTask;
4
+import org.apache.ibatis.annotations.Mapper;
5
+@Mapper public interface PatrolTaskMapper extends BaseMapper<PatrolTask> {}

+ 5
- 0
wm-patrol/src/main/java/com/water/patrol/mapper/PatrolTemplateMapper.java Просмотреть файл

@@ -0,0 +1,5 @@
1
+package com.water.patrol.mapper;
2
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
3
+import com.water.patrol.entity.PatrolTemplate;
4
+import org.apache.ibatis.annotations.Mapper;
5
+@Mapper public interface PatrolTemplateMapper extends BaseMapper<PatrolTemplate> {}

+ 5
- 0
wm-patrol/src/main/java/com/water/patrol/mapper/PatrolTrackPointMapper.java Просмотреть файл

@@ -0,0 +1,5 @@
1
+package com.water.patrol.mapper;
2
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
3
+import com.water.patrol.entity.PatrolTrackPoint;
4
+import org.apache.ibatis.annotations.Mapper;
5
+@Mapper public interface PatrolTrackPointMapper extends BaseMapper<PatrolTrackPoint> {}

+ 5
- 0
wm-patrol/src/main/java/com/water/patrol/mapper/PatrolWorkOrderMapper.java Просмотреть файл

@@ -0,0 +1,5 @@
1
+package com.water.patrol.mapper;
2
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
3
+import com.water.patrol.entity.PatrolWorkOrder;
4
+import org.apache.ibatis.annotations.Mapper;
5
+@Mapper public interface PatrolWorkOrderMapper extends BaseMapper<PatrolWorkOrder> {}

+ 60
- 0
wm-patrol/src/main/java/com/water/patrol/mapper/WorkOrderMapper.java Просмотреть файл

@@ -0,0 +1,60 @@
1
+package com.water.patrol.mapper;
2
+
3
+import com.water.patrol.entity.WorkOrder;
4
+import org.apache.ibatis.annotations.Mapper;
5
+import org.apache.ibatis.annotations.Param;
6
+import java.time.LocalDateTime;
7
+import java.util.List;
8
+
9
+@Mapper
10
+public interface WorkOrderMapper {
11
+    
12
+    // 插入工单
13
+    int insert(WorkOrder workOrder);
14
+    
15
+    // 根据ID查询
16
+    WorkOrder selectById(Long id);
17
+    
18
+    // 根据工单编号查询
19
+    WorkOrder selectByOrderNo(String orderNo);
20
+    
21
+    // 根据问题ID查询工单
22
+    WorkOrder selectByProblemId(Long problemId);
23
+    
24
+    // 根据状态查询工单列表
25
+    List<WorkOrder> selectByStatus(String status);
26
+    
27
+    // 根据处理人查询工单列表
28
+    List<WorkOrder> selectByAssigneeId(Long assigneeId);
29
+    
30
+    // 根据处理状态查询工单列表
31
+    List<WorkOrder> selectByProcessStatus(String processStatus);
32
+    
33
+    // 更新工单信息
34
+    int update(WorkOrder workOrder);
35
+    
36
+    // 更新工单状态
37
+    int updateStatus(@Param("id") Long id, @Param("status") String status, @Param("processStatus") String processStatus);
38
+    
39
+    // 更新处理人
40
+    int updateAssignee(@Param("id") Long id, @Param("assigneeId") Long assigneeId, @Param("assigneeName") String assigneeName);
41
+    
42
+    // 更新完成信息
43
+    int updateCompletion(@Param("id") Long id, @Param("actualEndTime") LocalDateTime actualEndTime, 
44
+                        @Param("completionTime") LocalDateTime completionTime, @Param("solutionResult") String solutionResult);
45
+    
46
+    // 删除工单
47
+    int deleteById(Long id);
48
+    
49
+    // 生成工单编号
50
+    String generateOrderNo();
51
+    
52
+    // 获取工单总数
53
+    int countAll();
54
+    
55
+    // 获取指定状态的工单数
56
+    int countByStatus(String status);
57
+    
58
+    // 获取指定处理状态的工单数
59
+    int countByProcessStatus(String processStatus);
60
+}

+ 25
- 0
wm-patrol/src/main/java/com/water/patrol/mapper/WorkOrderProcessMapper.java Просмотреть файл

@@ -0,0 +1,25 @@
1
+package com.water.patrol.mapper;
2
+
3
+import com.water.patrol.entity.WorkOrderProcess;
4
+import org.apache.ibatis.annotations.Mapper;
5
+import org.apache.ibatis.annotations.Param;
6
+import java.util.List;
7
+
8
+@Mapper
9
+public interface WorkOrderProcessMapper {
10
+    
11
+    // 插入处理记录
12
+    int insert(WorkOrderProcess workOrderProcess);
13
+    
14
+    // 根据工单ID查询处理记录列表
15
+    List<WorkOrderProcess> selectByWorkOrderId(Long workOrderId);
16
+    
17
+    // 根据ID查询
18
+    WorkOrderProcess selectById(Long id);
19
+    
20
+    // 获取最新的处理记录
21
+    WorkOrderProcess selectLatestByWorkOrderId(Long workOrderId);
22
+    
23
+    // 删除处理记录
24
+    int deleteById(Long id);
25
+}

+ 30
- 0
wm-patrol/src/main/java/com/water/patrol/service/PatrolAppIssueService.java Просмотреть файл

@@ -0,0 +1,30 @@
1
+package com.water.patrol.service;
2
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
3
+import com.water.patrol.entity.PatrolIssueReport; import com.water.patrol.mapper.PatrolIssueReportMapper;
4
+import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j;
5
+import org.springframework.stereotype.Service;
6
+import java.math.BigDecimal; import java.util.*;
7
+@Slf4j @Service @RequiredArgsConstructor
8
+public class PatrolAppIssueService {
9
+    private final PatrolIssueReportMapper mapper;
10
+    public PatrolIssueReport submit(Long taskId, Long reporterId, String reporterName,
11
+            String issueType, String description, String photos, String videos,
12
+            BigDecimal lng, BigDecimal lat, String address, String severity) {
13
+        PatrolIssueReport r = new PatrolIssueReport();
14
+        r.setReportNo("PIR-"+System.currentTimeMillis()); r.setTaskId(taskId);
15
+        r.setReporterId(reporterId); r.setReporterName(reporterName);
16
+        r.setIssueType(issueType); r.setDescription(description);
17
+        r.setPhotos(photos); r.setVideos(videos);
18
+        r.setLng(lng); r.setLat(lat); r.setAddress(address);
19
+        r.setSeverity(severity != null ? severity : "medium");
20
+        r.setStatus("submitted");
21
+        mapper.insert(r); return r;
22
+    }
23
+    public Map<String, Object> myReports(Long reporterId) {
24
+        Map<String, Object> r = new LinkedHashMap<>();
25
+        r.put("reports", mapper.selectList(new LambdaQueryWrapper<PatrolIssueReport>().eq(PatrolIssueReport::getReporterId, reporterId).orderByDesc(PatrolIssueReport::getCreatedAt)));
26
+        r.put("totalCount", mapper.selectCount(new LambdaQueryWrapper<PatrolIssueReport>().eq(PatrolIssueReport::getReporterId, reporterId)));
27
+        return r;
28
+    }
29
+    public PatrolIssueReport getDetail(Long id) { return mapper.selectById(id); }
30
+}

+ 23
- 0
wm-patrol/src/main/java/com/water/patrol/service/PatrolAppProfileService.java Просмотреть файл

@@ -0,0 +1,23 @@
1
+package com.water.patrol.service;
2
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
3
+import com.water.patrol.entity.PatrolTask; import com.water.patrol.entity.PatrolWorkOrder;
4
+import com.water.patrol.mapper.PatrolTaskMapper; import com.water.patrol.mapper.PatrolWorkOrderMapper;
5
+import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j;
6
+import org.springframework.stereotype.Service;
7
+import java.util.*;
8
+@Slf4j @Service @RequiredArgsConstructor
9
+public class PatrolAppProfileService {
10
+    private final PatrolTaskMapper taskMapper;
11
+    private final PatrolWorkOrderMapper woMapper;
12
+    public Map<String, Object> getProfile(Long workerId) {
13
+        Map<String, Object> r = new LinkedHashMap<>();
14
+        r.put("workerId", workerId);
15
+        r.put("totalTasks", taskMapper.selectCount(new LambdaQueryWrapper<PatrolTask>().eq(PatrolTask::getWorkerId, workerId)));
16
+        r.put("completedTasks", taskMapper.selectCount(new LambdaQueryWrapper<PatrolTask>().eq(PatrolTask::getWorkerId, workerId).eq(PatrolTask::getStatus, "completed")));
17
+        r.put("totalDistance", 0);
18
+        r.put("totalWorkOrders", woMapper.selectCount(new LambdaQueryWrapper<PatrolWorkOrder>().eq(PatrolWorkOrder::getAssigneeId, workerId)));
19
+        r.put("resolvedWorkOrders", woMapper.selectCount(new LambdaQueryWrapper<PatrolWorkOrder>().eq(PatrolWorkOrder::getAssigneeId, workerId).eq(PatrolWorkOrder::getStatus, "resolved")));
20
+        r.put("reportedIssues", 0);
21
+        return r;
22
+    }
23
+}

+ 399
- 0
wm-patrol/src/main/java/com/water/patrol/service/PatrolAppService.java Просмотреть файл

@@ -0,0 +1,399 @@
1
+package com.water.patrol.service;
2
+
3
+import lombok.RequiredArgsConstructor;
4
+import lombok.extern.slf4j.Slf4j;
5
+import org.springframework.jdbc.core.JdbcTemplate;
6
+import org.springframework.stereotype.Service;
7
+
8
+import java.time.LocalDate;
9
+import java.time.YearMonth;
10
+import java.time.format.DateTimeFormatter;
11
+import java.util.*;
12
+
13
+/**
14
+ * 巡查APP核心服务 - Issue #88
15
+ * PAT-21 APP总览, PAT-22 今日任务, PAT-23 待办工单, PAT-24 巡检统计,
16
+ * PAT-25 任务列表, PAT-26 工单列表, PAT-27 问题上报, PAT-28 个人中心, PAT-29 历史任务
17
+ */
18
+@Slf4j
19
+@Service
20
+@RequiredArgsConstructor
21
+public class PatrolAppService {
22
+
23
+    private final JdbcTemplate jdbc;
24
+
25
+    // ========== PAT-21 APP总览 ==========
26
+
27
+    /**
28
+     * APP总览:今日任务数、待办工单数、本月完成数、本月里程、异常上报数
29
+     */
30
+    public Map<String, Object> getAppOverview(Long userId) {
31
+        LocalDate today = LocalDate.now();
32
+        LocalDate monthStart = today.withDayOfMonth(1);
33
+
34
+        long todayTasks = countQuery(
35
+            "SELECT COUNT(*) FROM patrol_task WHERE assignee_id = ? AND task_date = ?",
36
+            userId, today);
37
+
38
+        long pendingWorkOrders = countQuery(
39
+            "SELECT COUNT(*) FROM patrol_work_order WHERE assignee_id = ? AND status IN ('assigned','processing')",
40
+            userId);
41
+
42
+        long monthCompleted = countQuery(
43
+            "SELECT COUNT(*) FROM patrol_task WHERE assignee_id = ? AND task_date >= ? AND status = 'completed'",
44
+            userId, monthStart);
45
+
46
+        Double monthDistance = jdbc.queryForObject(
47
+            "SELECT COALESCE(SUM(distance), 0) FROM patrol_task WHERE assignee_id = ? AND task_date >= ? AND status = 'completed'",
48
+            Double.class, userId, monthStart);
49
+
50
+        long monthIssues = countQuery(
51
+            "SELECT COUNT(*) FROM patrol_problem WHERE reporter_id = ? AND created_at >= ?",
52
+            userId, monthStart.atStartOfDay());
53
+
54
+        Map<String, Object> overview = new LinkedHashMap<>();
55
+        overview.put("todayTaskCount", todayTasks);
56
+        overview.put("pendingWorkOrderCount", pendingWorkOrders);
57
+        overview.put("monthCompletedCount", monthCompleted);
58
+        overview.put("monthDistance", monthDistance != null ? Math.round(monthDistance * 100.0) / 100.0 : 0.0);
59
+        overview.put("monthIssueCount", monthIssues);
60
+        return overview;
61
+    }
62
+
63
+    // ========== PAT-22 今日任务 ==========
64
+
65
+    /**
66
+     * 今日任务列表(含进度百分比、路线名称、预计时长)
67
+     */
68
+    public List<Map<String, Object>> getTodayTasks(Long userId) {
69
+        return jdbc.queryForList(
70
+            "SELECT pt.id, pt.task_name, pt.status, pt.task_date, pt.plan_start, pt.plan_end, " +
71
+            "  pr.route_name, pr.area, pr.estim_duration, " +
72
+            "  CASE WHEN pt.status = 'completed' THEN 100 " +
73
+            "       WHEN pt.status = 'in_progress' THEN " +
74
+            "         COALESCE(ROUND( " +
75
+            "           (SELECT COUNT(*)::NUMERIC FROM patrol_record WHERE task_id = pt.id) " +
76
+            "           / NULLIF((SELECT COUNT(*)::NUMERIC FROM patrol_route_checkpoint WHERE route_id = pt.route_id), 0) " +
77
+            "           * 100), 50) " +
78
+            "       ELSE 0 END AS progress_percent " +
79
+            "FROM patrol_task pt " +
80
+            "LEFT JOIN patrol_route pr ON pt.route_id = pr.id " +
81
+            "WHERE pt.assignee_id = ? AND pt.task_date = CURRENT_DATE " +
82
+            "ORDER BY pt.plan_start",
83
+            userId);
84
+    }
85
+
86
+    // ========== PAT-23 待办工单 ==========
87
+
88
+    /**
89
+     * 待办工单列表(assigned/processing状态)
90
+     */
91
+    public List<Map<String, Object>> getPendingWorkOrders(Long userId) {
92
+        return jdbc.queryForList(
93
+            "SELECT wo.id, wo.order_no, wo.issue_type, wo.description, wo.severity, " +
94
+            "  wo.location, wo.status, wo.created_at, wo.task_id " +
95
+            "FROM patrol_work_order wo " +
96
+            "WHERE wo.assignee_id = ? AND wo.status IN ('assigned','processing') " +
97
+            "ORDER BY wo.created_at DESC",
98
+            userId);
99
+    }
100
+
101
+    // ========== PAT-24 巡检统计 ==========
102
+
103
+    /**
104
+     * 个人巡检统计(period: week/month/year)
105
+     * 完成任务数、总里程、异常上报数、巡检时长
106
+     */
107
+    public Map<String, Object> getUserStats(Long userId, String period) {
108
+        LocalDate start;
109
+        LocalDate end = LocalDate.now();
110
+
111
+        switch (period != null ? period : "month") {
112
+            case "week":
113
+                start = end.minusDays(end.getDayOfWeek().getValue() - 1);
114
+                break;
115
+            case "year":
116
+                start = end.withDayOfYear(1);
117
+                break;
118
+            default:
119
+                start = end.withDayOfMonth(1);
120
+                break;
121
+        }
122
+
123
+        long completedTasks = countQuery(
124
+            "SELECT COUNT(*) FROM patrol_task WHERE assignee_id = ? AND task_date BETWEEN ? AND ? AND status = 'completed'",
125
+            userId, start, end);
126
+
127
+        Double totalDistance = jdbc.queryForObject(
128
+            "SELECT COALESCE(SUM(distance), 0) FROM patrol_task WHERE assignee_id = ? AND task_date BETWEEN ? AND ? AND status = 'completed'",
129
+            Double.class, userId, start, end);
130
+
131
+        long issueCount = countQuery(
132
+            "SELECT COUNT(*) FROM patrol_problem WHERE reporter_id = ? AND created_at >= ? AND created_at < ?",
133
+            userId, start.atStartOfDay(), end.plusDays(1).atStartOfDay());
134
+
135
+        Double patrolHours = jdbc.queryForObject(
136
+            "SELECT COALESCE(SUM(EXTRACT(EPOCH FROM (actual_end - actual_start)) / 3600.0), 0) " +
137
+            "FROM patrol_task WHERE assignee_id = ? AND task_date BETWEEN ? AND ? AND status = 'completed' AND actual_start IS NOT NULL AND actual_end IS NOT NULL",
138
+            Double.class, userId, start, end);
139
+
140
+        Map<String, Object> stats = new LinkedHashMap<>();
141
+        stats.put("period", period != null ? period : "month");
142
+        stats.put("startDate", start.toString());
143
+        stats.put("endDate", end.toString());
144
+        stats.put("completedTasks", completedTasks);
145
+        stats.put("totalDistance", totalDistance != null ? Math.round(totalDistance * 100.0) / 100.0 : 0.0);
146
+        stats.put("issueCount", issueCount);
147
+        stats.put("patrolHours", patrolHours != null ? Math.round(patrolHours * 100.0) / 100.0 : 0.0);
148
+        return stats;
149
+    }
150
+
151
+    // ========== PAT-25 任务列表 ==========
152
+
153
+    /**
154
+     * 任务列表(支持按状态筛选,分页)
155
+     */
156
+    public Map<String, Object> getTaskList(Long userId, String status, int page, int size) {
157
+        StringBuilder sql = new StringBuilder(
158
+            "SELECT pt.id, pt.task_name, pt.status, pt.task_date, pt.plan_start, pt.plan_end, " +
159
+            "  pt.actual_start, pt.actual_end, pt.distance, pt.route_id, " +
160
+            "  pr.route_name, pr.area " +
161
+            "FROM patrol_task pt " +
162
+            "LEFT JOIN patrol_route pr ON pt.route_id = pr.id " +
163
+            "WHERE pt.assignee_id = ?");
164
+        List<Object> params = new ArrayList<>();
165
+        params.add(userId);
166
+
167
+        if (status != null && !status.isEmpty()) {
168
+            sql.append(" AND pt.status = ?");
169
+            params.add(status);
170
+        }
171
+        sql.append(" ORDER BY pt.task_date DESC, pt.plan_start DESC LIMIT ? OFFSET ?");
172
+        params.add(size);
173
+        params.add((page - 1) * size);
174
+
175
+        List<Map<String, Object>> records = jdbc.queryForList(sql.toString(), params.toArray());
176
+
177
+        StringBuilder countSql = new StringBuilder("SELECT COUNT(*) FROM patrol_task WHERE assignee_id = ?");
178
+        List<Object> countParams = new ArrayList<>();
179
+        countParams.add(userId);
180
+        if (status != null && !status.isEmpty()) {
181
+            countSql.append(" AND status = ?");
182
+            countParams.add(status);
183
+        }
184
+        long total = countQuery(countSql.toString(), countParams.toArray());
185
+
186
+        Map<String, Object> result = new LinkedHashMap<>();
187
+        result.put("total", total);
188
+        result.put("page", page);
189
+        result.put("size", size);
190
+        result.put("records", records);
191
+        return result;
192
+    }
193
+
194
+    // ========== PAT-26 工单列表 ==========
195
+
196
+    /**
197
+     * 工单列表(支持按状态筛选,分页)
198
+     */
199
+    public Map<String, Object> getWorkOrderList(Long userId, String status, int page, int size) {
200
+        StringBuilder sql = new StringBuilder(
201
+            "SELECT wo.id, wo.order_no, wo.issue_type, wo.description, wo.severity, " +
202
+            "  wo.location, wo.status, wo.created_at, wo.updated_at, wo.task_id " +
203
+            "FROM patrol_work_order wo " +
204
+            "WHERE wo.assignee_id = ?");
205
+        List<Object> params = new ArrayList<>();
206
+        params.add(userId);
207
+
208
+        if (status != null && !status.isEmpty()) {
209
+            sql.append(" AND wo.status = ?");
210
+            params.add(status);
211
+        }
212
+        sql.append(" ORDER BY wo.created_at DESC LIMIT ? OFFSET ?");
213
+        params.add(size);
214
+        params.add((page - 1) * size);
215
+
216
+        List<Map<String, Object>> records = jdbc.queryForList(sql.toString(), params.toArray());
217
+
218
+        StringBuilder countSql = new StringBuilder("SELECT COUNT(*) FROM patrol_work_order WHERE assignee_id = ?");
219
+        List<Object> countParams = new ArrayList<>();
220
+        countParams.add(userId);
221
+        if (status != null && !status.isEmpty()) {
222
+            countSql.append(" AND status = ?");
223
+            countParams.add(status);
224
+        }
225
+        long total = countQuery(countSql.toString(), countParams.toArray());
226
+
227
+        Map<String, Object> result = new LinkedHashMap<>();
228
+        result.put("total", total);
229
+        result.put("page", page);
230
+        result.put("size", size);
231
+        result.put("records", records);
232
+        return result;
233
+    }
234
+
235
+    // ========== PAT-27 问题上报 ==========
236
+
237
+    /**
238
+     * 问题上报:文字描述 + 照片URL列表 + 语音URL + GPS坐标
239
+     * 自动创建巡检问题记录 + 工单
240
+     */
241
+    public Map<String, Object> reportIssue(Long taskId, Long deviceId, String issueType,
242
+                                            String description, List<String> photoUrls,
243
+                                            String voiceUrl, Double lng, Double lat) {
244
+        log.info("APP问题上报: taskId={}, type={}, desc={}", taskId, issueType, description);
245
+
246
+        String problemNo = "PAT-" + System.currentTimeMillis();
247
+
248
+        // 1. 创建巡检问题记录
249
+        jdbc.update(
250
+            "INSERT INTO patrol_problem (problem_no, task_id, device_id, reporter_id, " +
251
+            "  issue_type, description, photo_urls, voice_url, gps_lng, gps_lat, status, created_at) " +
252
+            "SELECT ?, ?, ?, assignee_id, ?, ?, ?::jsonb, ?, ?, ?, 'reported', NOW() " +
253
+            "FROM patrol_task WHERE id = ?",
254
+            problemNo, taskId, deviceId,
255
+            issueType, description,
256
+            photoUrls != null ? photoUrls.toString() : "[]",
257
+            voiceUrl, lng, lat, taskId);
258
+
259
+        // 2. 自动创建工单
260
+        String orderNo = "PWO-" + System.currentTimeMillis();
261
+        jdbc.update(
262
+            "INSERT INTO patrol_work_order (order_no, task_id, issue_type, description, severity, " +
263
+            "  location, lng, lat, status, created_at) " +
264
+            "VALUES (?, ?, ?, ?, 'medium', ?, ?, ?, 'pending', NOW())",
265
+            orderNo, taskId, issueType, description,
266
+            description.length() > 50 ? description.substring(0, 50) : description,
267
+            lng, lat);
268
+
269
+        // 3. 关联工单到问题
270
+        jdbc.update(
271
+            "UPDATE patrol_problem SET work_order_id = (SELECT id FROM patrol_work_order WHERE order_no = ? LIMIT 1) " +
272
+            "WHERE problem_no = ?",
273
+            orderNo, problemNo);
274
+
275
+        Map<String, Object> result = new LinkedHashMap<>();
276
+        result.put("problemNo", problemNo);
277
+        result.put("orderNo", orderNo);
278
+        result.put("issueType", issueType);
279
+        result.put("reported", true);
280
+        result.put("photoCount", photoUrls != null ? photoUrls.size() : 0);
281
+        result.put("hasVoice", voiceUrl != null && !voiceUrl.isEmpty());
282
+        return result;
283
+    }
284
+
285
+    // ========== PAT-28 个人中心 ==========
286
+
287
+    /**
288
+     * 个人信息(姓名、工号、部门、累计里程、累计任务数)
289
+     */
290
+    public Map<String, Object> getProfile(Long userId) {
291
+        // 用户基本信息
292
+        Map<String, Object> user = jdbc.queryForMap(
293
+            "SELECT id, username, real_name, employee_no, department, phone, email, avatar " +
294
+            "FROM sys_user WHERE id = ?",
295
+            userId);
296
+
297
+        // 累计里程
298
+        Double totalDistance = jdbc.queryForObject(
299
+            "SELECT COALESCE(SUM(distance), 0) FROM patrol_task WHERE assignee_id = ? AND status = 'completed'",
300
+            Double.class, userId);
301
+
302
+        // 累计任务数
303
+        long totalTasks = countQuery(
304
+            "SELECT COUNT(*) FROM patrol_task WHERE assignee_id = ? AND status = 'completed'",
305
+            userId);
306
+
307
+        // 本月统计
308
+        LocalDate monthStart = LocalDate.now().withDayOfMonth(1);
309
+        long monthTasks = countQuery(
310
+            "SELECT COUNT(*) FROM patrol_task WHERE assignee_id = ? AND task_date >= ? AND status = 'completed'",
311
+            userId, monthStart);
312
+
313
+        Map<String, Object> profile = new LinkedHashMap<>();
314
+        profile.put("userId", user.get("id"));
315
+        profile.put("username", user.get("username"));
316
+        profile.put("realName", user.get("real_name"));
317
+        profile.put("employeeNo", user.get("employee_no"));
318
+        profile.put("department", user.get("department"));
319
+        profile.put("phone", user.get("phone"));
320
+        profile.put("email", user.get("email"));
321
+        profile.put("avatar", user.get("avatar"));
322
+        profile.put("totalDistance", totalDistance != null ? Math.round(totalDistance * 100.0) / 100.0 : 0.0);
323
+        profile.put("totalTaskCount", totalTasks);
324
+        profile.put("monthTaskCount", monthTasks);
325
+        return profile;
326
+    }
327
+
328
+    /**
329
+     * 更新头像
330
+     */
331
+    public Map<String, Object> updateAvatar(Long userId, String avatarUrl) {
332
+        jdbc.update("UPDATE sys_user SET avatar = ?, updated_at = NOW() WHERE id = ?", avatarUrl, userId);
333
+        Map<String, Object> result = new LinkedHashMap<>();
334
+        result.put("userId", userId);
335
+        result.put("avatar", avatarUrl);
336
+        result.put("updated", true);
337
+        return result;
338
+    }
339
+
340
+    // ========== PAT-29 历史任务 ==========
341
+
342
+    /**
343
+     * 历史任务列表(按日期范围筛选,分页)
344
+     */
345
+    public Map<String, Object> getHistoryTasks(Long userId, String startDate, String endDate,
346
+                                                int page, int size) {
347
+        StringBuilder sql = new StringBuilder(
348
+            "SELECT pt.id, pt.task_name, pt.status, pt.task_date, pt.plan_start, pt.plan_end, " +
349
+            "  pt.actual_start, pt.actual_end, pt.distance, " +
350
+            "  pr.route_name, pr.area " +
351
+            "FROM patrol_task pt " +
352
+            "LEFT JOIN patrol_route pr ON pt.route_id = pr.id " +
353
+            "WHERE pt.assignee_id = ? AND pt.status = 'completed'");
354
+        List<Object> params = new ArrayList<>();
355
+        params.add(userId);
356
+
357
+        if (startDate != null && !startDate.isEmpty()) {
358
+            sql.append(" AND pt.task_date >= ?::DATE");
359
+            params.add(startDate);
360
+        }
361
+        if (endDate != null && !endDate.isEmpty()) {
362
+            sql.append(" AND pt.task_date <= ?::DATE");
363
+            params.add(endDate);
364
+        }
365
+        sql.append(" ORDER BY pt.task_date DESC, pt.actual_end DESC LIMIT ? OFFSET ?");
366
+        params.add(size);
367
+        params.add((page - 1) * size);
368
+
369
+        List<Map<String, Object>> records = jdbc.queryForList(sql.toString(), params.toArray());
370
+
371
+        StringBuilder countSql = new StringBuilder(
372
+            "SELECT COUNT(*) FROM patrol_task WHERE assignee_id = ? AND status = 'completed'");
373
+        List<Object> countParams = new ArrayList<>();
374
+        countParams.add(userId);
375
+        if (startDate != null && !startDate.isEmpty()) {
376
+            countSql.append(" AND task_date >= ?::DATE");
377
+            countParams.add(startDate);
378
+        }
379
+        if (endDate != null && !endDate.isEmpty()) {
380
+            countSql.append(" AND task_date <= ?::DATE");
381
+            countParams.add(endDate);
382
+        }
383
+        long total = countQuery(countSql.toString(), countParams.toArray());
384
+
385
+        Map<String, Object> result = new LinkedHashMap<>();
386
+        result.put("total", total);
387
+        result.put("page", page);
388
+        result.put("size", size);
389
+        result.put("records", records);
390
+        return result;
391
+    }
392
+
393
+    // ========== 工具方法 ==========
394
+
395
+    private long countQuery(String sql, Object... args) {
396
+        Long count = jdbc.queryForObject(sql, Long.class, args);
397
+        return count != null ? count : 0;
398
+    }
399
+}

+ 42
- 0
wm-patrol/src/main/java/com/water/patrol/service/PatrolAppTaskService.java Просмотреть файл

@@ -0,0 +1,42 @@
1
+package com.water.patrol.service;
2
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
3
+import com.water.patrol.entity.PatrolTask; import com.water.patrol.mapper.PatrolTaskMapper;
4
+import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j;
5
+import org.springframework.stereotype.Service;
6
+import java.time.LocalDateTime; import java.util.*;
7
+@Slf4j @Service @RequiredArgsConstructor
8
+public class PatrolAppTaskService {
9
+    private final PatrolTaskMapper mapper;
10
+    public Map<String, Object> myTasks(Long workerId, String status) {
11
+        LambdaQueryWrapper<PatrolTask> w = new LambdaQueryWrapper<>();
12
+        w.eq(PatrolTask::getWorkerId, workerId);
13
+        if (status != null && !status.isEmpty()) w.eq(PatrolTask::getStatus, status);
14
+        w.orderByDesc(PatrolTask::getCreatedAt);
15
+        Map<String, Object> r = new LinkedHashMap<>();
16
+        r.put("tasks", mapper.selectList(w));
17
+        r.put("totalCount", mapper.selectCount(new LambdaQueryWrapper<PatrolTask>().eq(PatrolTask::getWorkerId, workerId)));
18
+        r.put("pendingCount", mapper.selectCount(new LambdaQueryWrapper<PatrolTask>().eq(PatrolTask::getWorkerId, workerId).eq(PatrolTask::getStatus, "pending")));
19
+        r.put("inProgressCount", mapper.selectCount(new LambdaQueryWrapper<PatrolTask>().eq(PatrolTask::getWorkerId, workerId).eq(PatrolTask::getStatus, "in_progress")));
20
+        return r;
21
+    }
22
+    public PatrolTask getDetail(Long id) { return mapper.selectById(id); }
23
+    public void accept(Long id) {
24
+        PatrolTask t = mapper.selectById(id);
25
+        if (t == null) throw new RuntimeException("任务不存在");
26
+        t.setStatus("in_progress"); t.setStartTime(LocalDateTime.now()); mapper.updateById(t);
27
+    }
28
+    public void complete(Long id, Integer cc, Double dist, String remark) {
29
+        PatrolTask t = mapper.selectById(id);
30
+        if (t == null) throw new RuntimeException("任务不存在");
31
+        t.setStatus("completed"); t.setEndTime(LocalDateTime.now());
32
+        t.setCompletedCheckpoints(cc); t.setDistance(dist); t.setRemark(remark);
33
+        mapper.updateById(t);
34
+    }
35
+    public Map<String, Object> myStats(Long workerId) {
36
+        Map<String, Object> r = new LinkedHashMap<>();
37
+        r.put("total", mapper.selectCount(new LambdaQueryWrapper<PatrolTask>().eq(PatrolTask::getWorkerId, workerId)));
38
+        r.put("completed", mapper.selectCount(new LambdaQueryWrapper<PatrolTask>().eq(PatrolTask::getWorkerId, workerId).eq(PatrolTask::getStatus, "completed")));
39
+        r.put("totalDistance", 0);
40
+        return r;
41
+    }
42
+}

+ 36
- 0
wm-patrol/src/main/java/com/water/patrol/service/PatrolAppWorkOrderService.java Просмотреть файл

@@ -0,0 +1,36 @@
1
+package com.water.patrol.service;
2
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
3
+import com.water.patrol.entity.PatrolWorkOrder; import com.water.patrol.mapper.PatrolWorkOrderMapper;
4
+import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j;
5
+import org.springframework.stereotype.Service;
6
+import java.time.LocalDateTime; import java.util.*;
7
+@Slf4j @Service @RequiredArgsConstructor
8
+public class PatrolAppWorkOrderService {
9
+    private final PatrolWorkOrderMapper mapper;
10
+    public Map<String, Object> myWorkOrders(Long assigneeId, String status) {
11
+        LambdaQueryWrapper<PatrolWorkOrder> w = new LambdaQueryWrapper<>();
12
+        w.eq(PatrolWorkOrder::getAssigneeId, assigneeId);
13
+        if (status != null && !status.isEmpty()) w.eq(PatrolWorkOrder::getStatus, status);
14
+        w.orderByDesc(PatrolWorkOrder::getCreatedAt);
15
+        Map<String, Object> r = new LinkedHashMap<>();
16
+        r.put("workOrders", mapper.selectList(w));
17
+        r.put("totalCount", mapper.selectCount(new LambdaQueryWrapper<PatrolWorkOrder>().eq(PatrolWorkOrder::getAssigneeId, assigneeId)));
18
+        r.put("pendingCount", mapper.selectCount(new LambdaQueryWrapper<PatrolWorkOrder>().eq(PatrolWorkOrder::getAssigneeId, assigneeId).eq(PatrolWorkOrder::getStatus, "assigned")));
19
+        r.put("inProgressCount", mapper.selectCount(new LambdaQueryWrapper<PatrolWorkOrder>().eq(PatrolWorkOrder::getAssigneeId, assigneeId).eq(PatrolWorkOrder::getStatus, "in_progress")));
20
+        return r;
21
+    }
22
+    public PatrolWorkOrder getDetail(Long id) { return mapper.selectById(id); }
23
+    public void accept(Long id) {
24
+        PatrolWorkOrder wo = mapper.selectById(id);
25
+        if (wo == null) throw new RuntimeException("工单不存在");
26
+        wo.setStatus("in_progress"); mapper.updateById(wo);
27
+    }
28
+    public void resolve(Long id, String resolution, String resultPhotos) {
29
+        PatrolWorkOrder wo = mapper.selectById(id);
30
+        if (wo == null) throw new RuntimeException("工单不存在");
31
+        wo.setStatus("resolved"); wo.setResolution(resolution);
32
+        wo.setResolvedAt(LocalDateTime.now());
33
+        if (resultPhotos != null) wo.setPhotos(resultPhotos);
34
+        mapper.updateById(wo);
35
+    }
36
+}

+ 53
- 0
wm-patrol/src/main/java/com/water/patrol/service/PatrolAreaService.java Просмотреть файл

@@ -0,0 +1,53 @@
1
+package com.water.patrol.service;
2
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
3
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
4
+import com.water.patrol.entity.PatrolArea; import com.water.patrol.mapper.PatrolAreaMapper;
5
+import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j;
6
+import org.springframework.stereotype.Service;
7
+import java.math.BigDecimal; import java.util.*;
8
+@Slf4j @Service @RequiredArgsConstructor
9
+public class PatrolAreaService {
10
+    private final PatrolAreaMapper mapper;
11
+    public PatrolArea create(String areaName, String areaCode, String desc, BigDecimal cLng, BigDecimal cLat, String boundary, Double radius) {
12
+        PatrolArea a = new PatrolArea(); a.setAreaName(areaName); a.setAreaCode(areaCode);
13
+        a.setDescription(desc); a.setCenterLng(cLng); a.setCenterLat(cLat);
14
+        a.setBoundary(boundary); a.setRadius(radius); a.setStatus("active");
15
+        mapper.insert(a); return a;
16
+    }
17
+    public PatrolArea update(Long id, PatrolArea patch) {
18
+        PatrolArea a = mapper.selectById(id);
19
+        if (a == null) throw new RuntimeException("区域不存在: "+id);
20
+        if (patch.getAreaName() != null) a.setAreaName(patch.getAreaName());
21
+        if (patch.getDescription() != null) a.setDescription(patch.getDescription());
22
+        if (patch.getCenterLng() != null) a.setCenterLng(patch.getCenterLng());
23
+        if (patch.getCenterLat() != null) a.setCenterLat(patch.getCenterLat());
24
+        if (patch.getBoundary() != null) a.setBoundary(patch.getBoundary());
25
+        if (patch.getRadius() != null) a.setRadius(patch.getRadius());
26
+        mapper.updateById(a); return a;
27
+    }
28
+    public void delete(Long id) { mapper.deleteById(id); }
29
+    public PatrolArea getDetail(Long id) { return mapper.selectById(id); }
30
+    public Map<String, Object> list(String keyword, String status, int page, int size) {
31
+        LambdaQueryWrapper<PatrolArea> w = new LambdaQueryWrapper<>();
32
+        if (keyword != null && !keyword.isEmpty()) w.and(q -> q.like(PatrolArea::getAreaName, keyword).or().like(PatrolArea::getAreaCode, keyword));
33
+        if (status != null && !status.isEmpty()) w.eq(PatrolArea::getStatus, status);
34
+        w.orderByDesc(PatrolArea::getCreatedAt);
35
+        Page<PatrolArea> p = mapper.selectPage(new Page<>(page, size), w);
36
+        Map<String, Object> r = new LinkedHashMap<>();
37
+        r.put("records", p.getRecords()); r.put("total", p.getTotal());
38
+        r.put("page", page); r.put("size", size);
39
+        return r;
40
+    }
41
+    public void updateStatus(Long id, String status) {
42
+        PatrolArea a = mapper.selectById(id);
43
+        if (a == null) throw new RuntimeException("区域不存在: "+id);
44
+        a.setStatus(status); mapper.updateById(a);
45
+    }
46
+    public Map<String, Object> getStats() {
47
+        Map<String, Object> r = new LinkedHashMap<>();
48
+        r.put("total", mapper.selectCount(null));
49
+        r.put("active", mapper.selectCount(new LambdaQueryWrapper<PatrolArea>().eq(PatrolArea::getStatus,"active")));
50
+        r.put("inactive", mapper.selectCount(new LambdaQueryWrapper<PatrolArea>().eq(PatrolArea::getStatus,"inactive")));
51
+        return r;
52
+    }
53
+}

+ 252
- 0
wm-patrol/src/main/java/com/water/patrol/service/PatrolCoreService.java Просмотреть файл

@@ -0,0 +1,252 @@
1
+package com.water.patrol.service;
2
+
3
+import lombok.RequiredArgsConstructor;
4
+import lombok.extern.slf4j.Slf4j;
5
+import org.springframework.jdbc.core.JdbcTemplate;
6
+import org.springframework.stereotype.Service;
7
+
8
+import java.time.LocalDate;
9
+import java.time.LocalDateTime;
10
+import java.util.*;
11
+
12
+/**
13
+ * 巡检管理核心服务 - Issue #86
14
+ * PAT-01 总览, PAT-02 轨迹, PAT-03 任务台账, PAT-04 设备台账
15
+ */
16
+@Slf4j
17
+@Service
18
+@RequiredArgsConstructor
19
+public class PatrolCoreService {
20
+
21
+    private final JdbcTemplate jdbc;
22
+
23
+    // ========== PAT-01 巡检总览 ==========
24
+
25
+    public Map<String, Object> getOverview() {
26
+        LocalDate today = LocalDate.now();
27
+        LocalDate monthStart = today.withDayOfMonth(1);
28
+
29
+        long todayTotal = countQuery("SELECT COUNT(*) FROM patrol_task WHERE task_date = ?", today);
30
+        long todayCompleted = countQuery("SELECT COUNT(*) FROM patrol_task WHERE task_date = ? AND status = 'completed'", today);
31
+        long monthTotal = countQuery("SELECT COUNT(*) FROM patrol_task WHERE task_date >= ?", monthStart);
32
+        long monthCompleted = countQuery("SELECT COUNT(*) FROM patrol_task WHERE task_date >= ? AND status = 'completed'", monthStart);
33
+
34
+        long pendingIssues = countQuery("SELECT COUNT(*) FROM patrol_work_order WHERE status IN ('pending','assigned','processing')");
35
+        long resolvedIssues = countQuery("SELECT COUNT(*) FROM patrol_work_order WHERE status = 'resolved'");
36
+
37
+        long coverageRate = monthTotal > 0 ? Math.round(monthCompleted * 100.0 / monthTotal) : 0;
38
+
39
+        Map<String, Object> overview = new LinkedHashMap<>();
40
+        overview.put("todayTotal", todayTotal);
41
+        overview.put("todayCompleted", todayCompleted);
42
+        overview.put("monthTotal", monthTotal);
43
+        overview.put("monthCompleted", monthCompleted);
44
+        overview.put("coverageRate", coverageRate);
45
+        overview.put("pendingIssues", pendingIssues);
46
+        overview.put("resolvedIssues", resolvedIssues);
47
+        return overview;
48
+    }
49
+
50
+    public List<Map<String, Object>> getTodayTasks() {
51
+        return jdbc.queryForList(
52
+            "SELECT pt.*, pr.route_name, pr.area FROM patrol_task pt " +
53
+            "LEFT JOIN patrol_route pr ON pt.route_id = pr.id " +
54
+            "WHERE pt.task_date = CURRENT_DATE ORDER BY pt.plan_start");
55
+    }
56
+
57
+    public List<Map<String, Object>> getRecentIssues(int limit) {
58
+        return jdbc.queryForList(
59
+            "SELECT * FROM patrol_work_order ORDER BY created_at DESC LIMIT ?", limit);
60
+    }
61
+
62
+    // ========== PAT-02 任务轨迹(GPS记录与回放) ==========
63
+
64
+    public Map<String, Object> recordTrackPoint(Long taskId, Long workerId,
65
+                                                 double lng, double lat,
66
+                                                 double speed, double accuracy) {
67
+        jdbc.update(
68
+            "INSERT INTO patrol_track_point (task_id, worker_id, lng, lat, speed, accuracy) " +
69
+            "VALUES (?,?,?,?,?,?)",
70
+            taskId, workerId, lng, lat, speed, accuracy);
71
+        Map<String, Object> result = new LinkedHashMap<>();
72
+        result.put("taskId", taskId);
73
+        result.put("workerId", workerId);
74
+        result.put("lng", lng);
75
+        result.put("lat", lat);
76
+        result.put("recorded", true);
77
+        return result;
78
+    }
79
+
80
+    public List<Map<String, Object>> getTrack(Long taskId) {
81
+        return jdbc.queryForList(
82
+            "SELECT * FROM patrol_track_point WHERE task_id = ? ORDER BY recorded_at", taskId);
83
+    }
84
+
85
+    public Map<String, Object> replayTrack(Long taskId) {
86
+        List<Map<String, Object>> points = getTrack(taskId);
87
+        Map<String, Object> result = new LinkedHashMap<>();
88
+        result.put("taskId", taskId);
89
+        result.put("points", points);
90
+        result.put("totalPoints", points.size());
91
+        if (!points.isEmpty()) {
92
+            result.put("startTime", points.get(0).get("recorded_at"));
93
+            result.put("endTime", points.get(points.size() - 1).get("recorded_at"));
94
+        }
95
+        return result;
96
+    }
97
+
98
+    public Map<String, Object> getTrackStats(Long taskId) {
99
+        List<Map<String, Object>> points = getTrack(taskId);
100
+        Map<String, Object> stats = new LinkedHashMap<>();
101
+        stats.put("taskId", taskId);
102
+        stats.put("totalPoints", points.size());
103
+
104
+        if (points.size() < 2) {
105
+            stats.put("totalDistance", 0.0);
106
+            stats.put("duration", 0);
107
+            stats.put("avgSpeed", 0.0);
108
+            return stats;
109
+        }
110
+
111
+        double totalDistance = 0;
112
+        for (int i = 1; i < points.size(); i++) {
113
+            double lat1 = ((Number) points.get(i - 1).get("lat")).doubleValue();
114
+            double lon1 = ((Number) points.get(i - 1).get("lng")).doubleValue();
115
+            double lat2 = ((Number) points.get(i).get("lat")).doubleValue();
116
+            double lon2 = ((Number) points.get(i).get("lng")).doubleValue();
117
+            totalDistance += haversine(lat1, lon1, lat2, lon2);
118
+        }
119
+
120
+        double avgSpeed = points.stream()
121
+            .filter(p -> p.get("speed") != null)
122
+            .mapToDouble(p -> ((Number) p.get("speed")).doubleValue())
123
+            .average().orElse(0);
124
+
125
+        stats.put("totalDistance", Math.round(totalDistance * 100.0) / 100.0);
126
+        stats.put("avgSpeed", Math.round(avgSpeed * 100.0) / 100.0);
127
+        return stats;
128
+    }
129
+
130
+    // ========== PAT-03 任务台账 ==========
131
+
132
+    public Map<String, Object> getTaskLedger(String status, Long workerId, int page, int size) {
133
+        StringBuilder sql = new StringBuilder("SELECT * FROM patrol_task WHERE 1=1");
134
+        List<Object> params = new ArrayList<>();
135
+        if (status != null && !status.isEmpty()) {
136
+            sql.append(" AND status = ?");
137
+            params.add(status);
138
+        }
139
+        if (workerId != null) {
140
+            sql.append(" AND assignee_id = ?");
141
+            params.add(workerId);
142
+        }
143
+        sql.append(" ORDER BY task_date DESC LIMIT ? OFFSET ?");
144
+        params.add(size);
145
+        params.add((page - 1) * size);
146
+
147
+        List<Map<String, Object>> records = jdbc.queryForList(sql.toString(), params.toArray());
148
+
149
+        StringBuilder countSql = new StringBuilder("SELECT COUNT(*) FROM patrol_task WHERE 1=1");
150
+        List<Object> countParams = new ArrayList<>();
151
+        if (status != null && !status.isEmpty()) {
152
+            countSql.append(" AND status = ?");
153
+            countParams.add(status);
154
+        }
155
+        if (workerId != null) {
156
+            countSql.append(" AND assignee_id = ?");
157
+            countParams.add(workerId);
158
+        }
159
+        long total = countQuery(countSql.toString(), countParams.toArray());
160
+
161
+        Map<String, Object> result = new LinkedHashMap<>();
162
+        result.put("total", total);
163
+        result.put("page", page);
164
+        result.put("size", size);
165
+        result.put("records", records);
166
+        return result;
167
+    }
168
+
169
+    // ========== PAT-04 设备台账 ==========
170
+
171
+    public Map<String, Object> getDeviceLedger(String deviceType, String status, String area,
172
+                                                int page, int size) {
173
+        StringBuilder sql = new StringBuilder("SELECT * FROM patrol_device WHERE 1=1");
174
+        List<Object> params = new ArrayList<>();
175
+        if (deviceType != null && !deviceType.isEmpty()) {
176
+            sql.append(" AND device_type = ?");
177
+            params.add(deviceType);
178
+        }
179
+        if (status != null && !status.isEmpty()) {
180
+            sql.append(" AND status = ?");
181
+            params.add(status);
182
+        }
183
+        if (area != null && !area.isEmpty()) {
184
+            sql.append(" AND area = ?");
185
+            params.add(area);
186
+        }
187
+        sql.append(" ORDER BY created_at DESC LIMIT ? OFFSET ?");
188
+        params.add(size);
189
+        params.add((page - 1) * size);
190
+
191
+        List<Map<String, Object>> records = jdbc.queryForList(sql.toString(), params.toArray());
192
+
193
+        StringBuilder countSql = new StringBuilder("SELECT COUNT(*) FROM patrol_device WHERE 1=1");
194
+        List<Object> countParams = new ArrayList<>();
195
+        if (deviceType != null && !deviceType.isEmpty()) {
196
+            countSql.append(" AND device_type = ?");
197
+            countParams.add(deviceType);
198
+        }
199
+        if (status != null && !status.isEmpty()) {
200
+            countSql.append(" AND status = ?");
201
+            countParams.add(status);
202
+        }
203
+        if (area != null && !area.isEmpty()) {
204
+            countSql.append(" AND area = ?");
205
+            countParams.add(area);
206
+        }
207
+        long total = countQuery(countSql.toString(), countParams.toArray());
208
+
209
+        Map<String, Object> result = new LinkedHashMap<>();
210
+        result.put("total", total);
211
+        result.put("page", page);
212
+        result.put("size", size);
213
+        result.put("records", records);
214
+        return result;
215
+    }
216
+
217
+    public Map<String, Object> createDevice(String deviceNo, String deviceName, String deviceType,
218
+                                             String location, double lng, double lat, String area) {
219
+        jdbc.update(
220
+            "INSERT INTO patrol_device (device_no, device_name, device_type, location, lng, lat, area) " +
221
+            "VALUES (?,?,?,?,?,?,?)",
222
+            deviceNo, deviceName, deviceType, location, lng, lat, area);
223
+        Map<String, Object> result = new LinkedHashMap<>();
224
+        result.put("deviceNo", deviceNo);
225
+        result.put("deviceName", deviceName);
226
+        result.put("created", true);
227
+        return result;
228
+    }
229
+
230
+    public Map<String, Object> updateDeviceStatus(Long deviceId, String status) {
231
+        jdbc.update("UPDATE patrol_device SET status = ?, updated_at = NOW() WHERE id = ?",
232
+            status, deviceId);
233
+        return Map.of("deviceId", deviceId, "status", status, "updated", true);
234
+    }
235
+
236
+    // ========== 工具方法 ==========
237
+
238
+    private long countQuery(String sql, Object... args) {
239
+        Long count = jdbc.queryForObject(sql, Long.class, args);
240
+        return count != null ? count : 0;
241
+    }
242
+
243
+    private double haversine(double lat1, double lon1, double lat2, double lon2) {
244
+        double R = 6371;
245
+        double dLat = Math.toRadians(lat2 - lat1);
246
+        double dLon = Math.toRadians(lon2 - lon1);
247
+        double a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
248
+            + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
249
+            * Math.sin(dLon / 2) * Math.sin(dLon / 2);
250
+        return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
251
+    }
252
+}

+ 106
- 0
wm-patrol/src/main/java/com/water/patrol/service/PatrolDeviceService.java Просмотреть файл

@@ -0,0 +1,106 @@
1
+package com.water.patrol.service;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.water.patrol.entity.PatrolDevice;
5
+import com.water.patrol.mapper.PatrolDeviceMapper;
6
+import lombok.RequiredArgsConstructor;
7
+import lombok.extern.slf4j.Slf4j;
8
+import org.springframework.stereotype.Service;
9
+import java.math.BigDecimal;
10
+import java.time.LocalDate;
11
+import java.time.LocalDateTime;
12
+import java.util.*;
13
+
14
+@Slf4j
15
+@Service
16
+@RequiredArgsConstructor
17
+public class PatrolDeviceService {
18
+    private final PatrolDeviceMapper deviceMapper;
19
+
20
+    public PatrolDevice create(String deviceNo, String deviceName, String deviceType,
21
+            String location, BigDecimal lng, BigDecimal lat) {
22
+        PatrolDevice d = new PatrolDevice();
23
+        d.setDeviceNo(deviceNo);
24
+        d.setDeviceName(deviceName);
25
+        d.setDeviceType(deviceType);
26
+        d.setLocation(location);
27
+        d.setLng(lng);
28
+        d.setLat(lat);
29
+        d.setStatus("normal");
30
+        d.setCreatedAt(LocalDateTime.now());
31
+        d.setUpdatedAt(LocalDateTime.now());
32
+        deviceMapper.insert(d);
33
+        log.info("Device created: {}", deviceNo);
34
+        return d;
35
+    }
36
+
37
+    public Map<String, Object> list(String deviceType, String status, String keyword,
38
+            int page, int size) {
39
+        LambdaQueryWrapper<PatrolDevice> w = new LambdaQueryWrapper<>();
40
+        if (deviceType != null && !deviceType.isEmpty()) w.eq(PatrolDevice::getDeviceType, deviceType);
41
+        if (status != null && !status.isEmpty()) w.eq(PatrolDevice::getStatus, status);
42
+        if (keyword != null && !keyword.isEmpty()) {
43
+            w.and(q -> q.like(PatrolDevice::getDeviceName, keyword)
44
+                .or().like(PatrolDevice::getDeviceNo, keyword)
45
+                .or().like(PatrolDevice::getLocation, keyword));
46
+        }
47
+        w.orderByDesc(PatrolDevice::getCreatedAt);
48
+
49
+        Long total = deviceMapper.selectCount(w);
50
+        w.last("LIMIT " + size + " OFFSET " + ((page - 1) * size));
51
+        List<PatrolDevice> records = deviceMapper.selectList(w);
52
+
53
+        Map<String, Object> result = new LinkedHashMap<>();
54
+        result.put("total", total);
55
+        result.put("page", page);
56
+        result.put("size", size);
57
+        result.put("records", records);
58
+        return result;
59
+    }
60
+
61
+    public PatrolDevice getDetail(Long id) {
62
+        return deviceMapper.selectById(id);
63
+    }
64
+
65
+    public void updateStatus(Long id, String status) {
66
+        PatrolDevice d = deviceMapper.selectById(id);
67
+        if (d == null) throw new RuntimeException("Device not found: " + id);
68
+        d.setStatus(status);
69
+        d.setUpdatedAt(LocalDateTime.now());
70
+        deviceMapper.updateById(d);
71
+        log.info("Device {} status -> {}", id, status);
72
+    }
73
+
74
+    public void recordMaintenance(Long id) {
75
+        PatrolDevice d = deviceMapper.selectById(id);
76
+        if (d == null) throw new RuntimeException("Device not found: " + id);
77
+        d.setLastMaintenanceDate(LocalDate.now());
78
+        d.setNextMaintenanceDate(LocalDate.now().plusMonths(6));
79
+        d.setStatus("normal");
80
+        d.setUpdatedAt(LocalDateTime.now());
81
+        deviceMapper.updateById(d);
82
+        log.info("Maintenance recorded for device {}", id);
83
+    }
84
+
85
+    public Map<String, Object> getStats() {
86
+        Map<String, Object> r = new LinkedHashMap<>();
87
+        Long total = deviceMapper.selectCount(new LambdaQueryWrapper<>());
88
+        r.put("total", total);
89
+
90
+        LambdaQueryWrapper<PatrolDevice> normal = new LambdaQueryWrapper<PatrolDevice>().eq(PatrolDevice::getStatus, "normal");
91
+        r.put("normal", deviceMapper.selectCount(normal));
92
+
93
+        LambdaQueryWrapper<PatrolDevice> fault = new LambdaQueryWrapper<PatrolDevice>().eq(PatrolDevice::getStatus, "fault");
94
+        r.put("fault", deviceMapper.selectCount(fault));
95
+
96
+        LambdaQueryWrapper<PatrolDevice> offline = new LambdaQueryWrapper<PatrolDevice>().eq(PatrolDevice::getStatus, "offline");
97
+        r.put("offline", deviceMapper.selectCount(offline));
98
+
99
+        return r;
100
+    }
101
+
102
+    public void delete(Long id) {
103
+        deviceMapper.deleteById(id);
104
+        log.info("Device deleted: {}", id);
105
+    }
106
+}

+ 45
- 0
wm-patrol/src/main/java/com/water/patrol/service/PatrolFormService.java Просмотреть файл

@@ -0,0 +1,45 @@
1
+package com.water.patrol.service;
2
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
3
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
4
+import com.water.patrol.entity.PatrolForm; import com.water.patrol.mapper.PatrolFormMapper;
5
+import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j;
6
+import org.springframework.stereotype.Service;
7
+import java.util.*;
8
+@Slf4j @Service @RequiredArgsConstructor
9
+public class PatrolFormService {
10
+    private final PatrolFormMapper mapper;
11
+    public PatrolForm create(String formName, String formCode, String formType, String fieldConfig) {
12
+        PatrolForm f = new PatrolForm(); f.setFormName(formName); f.setFormCode(formCode);
13
+        f.setFormType(formType); f.setFieldConfig(fieldConfig); f.setStatus("active");
14
+        mapper.insert(f); return f;
15
+    }
16
+    public PatrolForm update(Long id, PatrolForm patch) {
17
+        PatrolForm f = mapper.selectById(id);
18
+        if (f == null) throw new RuntimeException("表单不存在: "+id);
19
+        if (patch.getFormName() != null) f.setFormName(patch.getFormName());
20
+        if (patch.getFieldConfig() != null) f.setFieldConfig(patch.getFieldConfig());
21
+        if (patch.getFormType() != null) f.setFormType(patch.getFormType());
22
+        if (patch.getRemark() != null) f.setRemark(patch.getRemark());
23
+        mapper.updateById(f); return f;
24
+    }
25
+    public void delete(Long id) { mapper.deleteById(id); }
26
+    public PatrolForm getDetail(Long id) { return mapper.selectById(id); }
27
+    public Map<String, Object> list(String keyword, String formType, String status, int page, int size) {
28
+        LambdaQueryWrapper<PatrolForm> w = new LambdaQueryWrapper<>();
29
+        if (keyword != null && !keyword.isEmpty()) w.and(q -> q.like(PatrolForm::getFormName, keyword).or().like(PatrolForm::getFormCode, keyword));
30
+        if (formType != null && !formType.isEmpty()) w.eq(PatrolForm::getFormType, formType);
31
+        if (status != null && !status.isEmpty()) w.eq(PatrolForm::getStatus, status);
32
+        w.orderByDesc(PatrolForm::getCreatedAt);
33
+        Page<PatrolForm> p = mapper.selectPage(new Page<>(page, size), w);
34
+        Map<String, Object> r = new LinkedHashMap<>();
35
+        r.put("records", p.getRecords()); r.put("total", p.getTotal());
36
+        r.put("page", page); r.put("size", size);
37
+        return r;
38
+    }
39
+    public Map<String, Object> getStats() {
40
+        Map<String, Object> r = new LinkedHashMap<>();
41
+        r.put("total", mapper.selectCount(null));
42
+        r.put("active", mapper.selectCount(new LambdaQueryWrapper<PatrolForm>().eq(PatrolForm::getStatus,"active")));
43
+        return r;
44
+    }
45
+}

+ 50
- 0
wm-patrol/src/main/java/com/water/patrol/service/PatrolLedgerService.java Просмотреть файл

@@ -0,0 +1,50 @@
1
+package com.water.patrol.service;
2
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
3
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
4
+import com.water.patrol.entity.PatrolTask; import com.water.patrol.mapper.PatrolTaskMapper;
5
+import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j;
6
+import org.springframework.stereotype.Service;
7
+import java.time.LocalDateTime; import java.util.*;
8
+@Slf4j @Service @RequiredArgsConstructor
9
+public class PatrolLedgerService {
10
+    private final PatrolTaskMapper mapper;
11
+    public Map<String, Object> listTasks(String status, String workerName, String startDate, String endDate, int page, int size) {
12
+        LambdaQueryWrapper<PatrolTask> w = new LambdaQueryWrapper<>();
13
+        if (status != null && !status.isEmpty()) w.eq(PatrolTask::getStatus, status);
14
+        if (workerName != null && !workerName.isEmpty()) w.like(PatrolTask::getWorkerName, workerName);
15
+        if (startDate != null && !startDate.isEmpty()) w.ge(PatrolTask::getCreatedAt, startDate+"T00:00:00");
16
+        if (endDate != null && !endDate.isEmpty()) w.le(PatrolTask::getCreatedAt, endDate+"T23:59:59");
17
+        w.orderByDesc(PatrolTask::getCreatedAt);
18
+        Page<PatrolTask> p = mapper.selectPage(new Page<>(page, size), w);
19
+        Map<String, Object> r = new LinkedHashMap<>();
20
+        r.put("records", p.getRecords()); r.put("total", p.getTotal());
21
+        r.put("page", page); r.put("size", size); r.put("pages", p.getPages());
22
+        return r;
23
+    }
24
+    public PatrolTask getDetail(Long id) { return mapper.selectById(id); }
25
+    public PatrolTask createTask(String taskName, Long routeId, Long workerId, String workerName) {
26
+        PatrolTask t = new PatrolTask(); t.setTaskNo("PAT-"+System.currentTimeMillis());
27
+        t.setTaskName(taskName); t.setRouteId(routeId); t.setWorkerId(workerId); t.setWorkerName(workerName);
28
+        t.setStatus("pending"); t.setCheckpoints(0); t.setCompletedCheckpoints(0);
29
+        t.setAbnormalCount(0); t.setDistance(0.0); mapper.insert(t); return t;
30
+    }
31
+    public void startTask(Long id) {
32
+        PatrolTask t = mapper.selectById(id);
33
+        if (t == null) throw new RuntimeException("任务不存在: "+id);
34
+        t.setStatus("in_progress"); t.setStartTime(LocalDateTime.now()); mapper.updateById(t);
35
+    }
36
+    public void completeTask(Long id, Integer cc, Double dist) {
37
+        PatrolTask t = mapper.selectById(id);
38
+        if (t == null) throw new RuntimeException("任务不存在: "+id);
39
+        t.setStatus("completed"); t.setEndTime(LocalDateTime.now());
40
+        t.setCompletedCheckpoints(cc); t.setDistance(dist); mapper.updateById(t);
41
+    }
42
+    public Map<String, Object> getStats() {
43
+        Map<String, Object> r = new LinkedHashMap<>();
44
+        r.put("total", mapper.selectCount(null));
45
+        r.put("completed", mapper.selectCount(new LambdaQueryWrapper<PatrolTask>().eq(PatrolTask::getStatus,"completed")));
46
+        r.put("inProgress", mapper.selectCount(new LambdaQueryWrapper<PatrolTask>().eq(PatrolTask::getStatus,"in_progress")));
47
+        r.put("pending", mapper.selectCount(new LambdaQueryWrapper<PatrolTask>().eq(PatrolTask::getStatus,"pending")));
48
+        return r;
49
+    }
50
+}

+ 30
- 0
wm-patrol/src/main/java/com/water/patrol/service/PatrolOverviewService.java Просмотреть файл

@@ -0,0 +1,30 @@
1
+package com.water.patrol.service;
2
+import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j;
3
+import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service;
4
+import java.time.LocalDate; import java.util.*;
5
+@Slf4j @Service @RequiredArgsConstructor
6
+public class PatrolOverviewService {
7
+    private final JdbcTemplate jdbc;
8
+    public Map<String, Object> getOverview() {
9
+        LocalDate today = LocalDate.now();
10
+        Map<String, Object> r = new LinkedHashMap<>();
11
+        r.put("todayTasks", jdbc.queryForObject("SELECT COUNT(*) FROM pat_task WHERE DATE(created_at)=?", Integer.class, today));
12
+        r.put("completedTasks", jdbc.queryForObject("SELECT COUNT(*) FROM pat_task WHERE DATE(created_at)=? AND status='completed'", Integer.class, today));
13
+        int t = (Integer) r.get("todayTasks"), c = (Integer) r.get("completedTasks");
14
+        r.put("completionRate", t > 0 ? Math.round(c * 100.0 / t) : 0);
15
+        r.put("abnormalCount", jdbc.queryForObject("SELECT COUNT(*) FROM pat_work_order WHERE DATE(created_at)=? AND status!='closed'", Integer.class, today));
16
+        r.put("onlineWorkers", jdbc.queryForObject("SELECT COUNT(DISTINCT worker_id) FROM pat_task WHERE status='in_progress'", Integer.class));
17
+        r.put("monthDistance", jdbc.queryForObject("SELECT COALESCE(SUM(distance),0) FROM pat_task WHERE DATE_PART('month',created_at)=DATE_PART('month',?::date)", Double.class, today.toString()));
18
+        r.put("pendingOrders", jdbc.queryForObject("SELECT COUNT(*) FROM pat_work_order WHERE status IN ('pending','assigned')", Integer.class));
19
+        return r;
20
+    }
21
+    public Map<String, Object> getMonthlyStats(int year, int month) {
22
+        String s = String.format("%d-%02d-01", year, month), e = String.format("%d-%02d-31", year, month);
23
+        Map<String, Object> r = new LinkedHashMap<>();
24
+        r.put("totalTasks", jdbc.queryForObject("SELECT COUNT(*) FROM pat_task WHERE created_at BETWEEN ?::date AND ?::date", Integer.class, s, e));
25
+        r.put("completedTasks", jdbc.queryForObject("SELECT COUNT(*) FROM pat_task WHERE created_at BETWEEN ?::date AND ?::date AND status='completed'", Integer.class, s, e));
26
+        r.put("totalDistance", jdbc.queryForObject("SELECT COALESCE(SUM(distance),0) FROM pat_task WHERE created_at BETWEEN ?::date AND ?::date", Double.class, s, e));
27
+        r.put("totalAbnormals", jdbc.queryForObject("SELECT COUNT(*) FROM pat_work_order WHERE created_at BETWEEN ?::date AND ?::date", Integer.class, s, e));
28
+        return r;
29
+    }
30
+}

+ 82
- 0
wm-patrol/src/main/java/com/water/patrol/service/PatrolProblemService.java Просмотреть файл

@@ -0,0 +1,82 @@
1
+package com.water.patrol.service;
2
+
3
+import com.water.patrol.entity.PatrolProblem;
4
+import java.util.List;
5
+
6
+public interface PatrolProblemService {
7
+    
8
+    /**
9
+     * 创建巡检问题
10
+     */
11
+    PatrolProblem createProblem(PatrolProblem patrolProblem);
12
+    
13
+    /**
14
+     * 根据ID查询问题
15
+     */
16
+    PatrolProblem getProblemById(Long id);
17
+    
18
+    /**
19
+     * 根据问题编号查询
20
+     */
21
+    PatrolProblem getProblemByProblemNo(String problemNo);
22
+    
23
+    /**
24
+     * 根据任务ID查询问题列表
25
+     */
26
+    List<PatrolProblem> getProblemsByTaskId(Long taskId);
27
+    
28
+    /**
29
+     * 根据状态查询问题列表
30
+     */
31
+    List<PatrolProblem> getProblemsByStatus(String status);
32
+    
33
+    /**
34
+     * 根据设备ID查询问题列表
35
+     */
36
+    List<PatrolProblem> getProblemsByDeviceId(Long deviceId);
37
+    
38
+    /**
39
+     * 更新问题信息
40
+     */
41
+    PatrolProblem updateProblem(PatrolProblem patrolProblem);
42
+    
43
+    /**
44
+     * 更新问题状态
45
+     */
46
+    boolean updateProblemStatus(Long id, String status, Long workOrderId);
47
+    
48
+    /**
49
+     * 删除问题
50
+     */
51
+    boolean deleteProblem(Long id);
52
+    
53
+    /**
54
+     * 自动创建工单
55
+     */
56
+    boolean autoCreateWorkOrder(Long problemId);
57
+    
58
+    /**
59
+     * 获取问题统计信息
60
+     */
61
+    ProblemStatistics getProblemStatistics();
62
+    
63
+    class ProblemStatistics {
64
+        private int totalProblems;
65
+        private int reportedCount;
66
+        private int processingCount;
67
+        private int completedCount;
68
+        private int closedCount;
69
+        
70
+        // Getters and setters
71
+        public int getTotalProblems() { return totalProblems; }
72
+        public void setTotalProblems(int totalProblems) { this.totalProblems = totalProblems; }
73
+        public int getReportedCount() { return reportedCount; }
74
+        public void setReportedCount(int reportedCount) { this.reportedCount = reportedCount; }
75
+        public int getProcessingCount() { return processingCount; }
76
+        public void setProcessingCount(int processingCount) { this.processingCount = processingCount; }
77
+        public int getCompletedCount() { return completedCount; }
78
+        public void setCompletedCount(int completedCount) { this.completedCount = completedCount; }
79
+        public int getClosedCount() { return closedCount; }
80
+        public void setClosedCount(int closedCount) { this.closedCount = closedCount; }
81
+    }
82
+}

+ 55
- 0
wm-patrol/src/main/java/com/water/patrol/service/PatrolRouteSetupService.java Просмотреть файл

@@ -0,0 +1,55 @@
1
+package com.water.patrol.service;
2
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
3
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
4
+import com.water.patrol.entity.PatrolRouteSetup; import com.water.patrol.mapper.PatrolRouteSetupMapper;
5
+import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j;
6
+import org.springframework.stereotype.Service;
7
+import java.util.*;
8
+@Slf4j @Service @RequiredArgsConstructor
9
+public class PatrolRouteSetupService {
10
+    private final PatrolRouteSetupMapper mapper;
11
+    public PatrolRouteSetup create(String routeName, String routeCode, Long areaId, String areaName,
12
+            Integer checkpointCount, String checkpointList, Double totalDistance, Integer estimatedMinutes) {
13
+        PatrolRouteSetup r = new PatrolRouteSetup(); r.setRouteName(routeName); r.setRouteCode(routeCode);
14
+        r.setAreaId(areaId); r.setAreaName(areaName); r.setCheckpointCount(checkpointCount);
15
+        r.setCheckpointList(checkpointList); r.setTotalDistance(totalDistance);
16
+        r.setEstimatedMinutes(estimatedMinutes); r.setStatus("active");
17
+        mapper.insert(r); return r;
18
+    }
19
+    public PatrolRouteSetup update(Long id, PatrolRouteSetup patch) {
20
+        PatrolRouteSetup r = mapper.selectById(id);
21
+        if (r == null) throw new RuntimeException("路线不存在: "+id);
22
+        if (patch.getRouteName() != null) r.setRouteName(patch.getRouteName());
23
+        if (patch.getCheckpointCount() != null) r.setCheckpointCount(patch.getCheckpointCount());
24
+        if (patch.getCheckpointList() != null) r.setCheckpointList(patch.getCheckpointList());
25
+        if (patch.getTotalDistance() != null) r.setTotalDistance(patch.getTotalDistance());
26
+        if (patch.getEstimatedMinutes() != null) r.setEstimatedMinutes(patch.getEstimatedMinutes());
27
+        if (patch.getRemark() != null) r.setRemark(patch.getRemark());
28
+        mapper.updateById(r); return r;
29
+    }
30
+    public void delete(Long id) { mapper.deleteById(id); }
31
+    public PatrolRouteSetup getDetail(Long id) { return mapper.selectById(id); }
32
+    public Map<String, Object> list(String keyword, String status, Long areaId, int page, int size) {
33
+        LambdaQueryWrapper<PatrolRouteSetup> w = new LambdaQueryWrapper<>();
34
+        if (keyword != null && !keyword.isEmpty()) w.and(q -> q.like(PatrolRouteSetup::getRouteName, keyword).or().like(PatrolRouteSetup::getRouteCode, keyword));
35
+        if (status != null && !status.isEmpty()) w.eq(PatrolRouteSetup::getStatus, status);
36
+        if (areaId != null) w.eq(PatrolRouteSetup::getAreaId, areaId);
37
+        w.orderByDesc(PatrolRouteSetup::getCreatedAt);
38
+        Page<PatrolRouteSetup> p = mapper.selectPage(new Page<>(page, size), w);
39
+        Map<String, Object> result = new LinkedHashMap<>();
40
+        result.put("records", p.getRecords()); result.put("total", p.getTotal());
41
+        result.put("page", page); result.put("size", size);
42
+        return result;
43
+    }
44
+    public void updateStatus(Long id, String status) {
45
+        PatrolRouteSetup r = mapper.selectById(id);
46
+        if (r == null) throw new RuntimeException("路线不存在: "+id);
47
+        r.setStatus(status); mapper.updateById(r);
48
+    }
49
+    public Map<String, Object> getStats() {
50
+        Map<String, Object> r = new LinkedHashMap<>();
51
+        r.put("total", mapper.selectCount(null));
52
+        r.put("active", mapper.selectCount(new LambdaQueryWrapper<PatrolRouteSetup>().eq(PatrolRouteSetup::getStatus,"active")));
53
+        return r;
54
+    }
55
+}

+ 118
- 0
wm-patrol/src/main/java/com/water/patrol/service/PatrolService.java Просмотреть файл

@@ -0,0 +1,118 @@
1
+package com.water.patrol.service;
2
+
3
+import lombok.RequiredArgsConstructor;
4
+import lombok.extern.slf4j.Slf4j;
5
+import org.springframework.jdbc.core.JdbcTemplate;
6
+import org.springframework.stereotype.Service;
7
+
8
+import java.time.LocalDate;
9
+import java.util.*;
10
+
11
+@Slf4j
12
+@Service
13
+@RequiredArgsConstructor
14
+public class PatrolService {
15
+
16
+    private final JdbcTemplate jdbc;
17
+
18
+    // ========== 路线管理 ==========
19
+    public Map<String, Object> createRoute(String routeName, String area, List<Map<String, Object>> points, int estimDuration) {
20
+        jdbc.update("INSERT INTO patrol_route (route_name, area, route_points, estim_duration) VALUES (?,?,?::jsonb,?)",
21
+            routeName, area, points.toString(), estimDuration);
22
+        return Map.of("routeName", routeName, "area", area, "points", points.size());
23
+    }
24
+
25
+    public List<Map<String, Object>> getRoutes(String area) {
26
+        return jdbc.queryForList("SELECT * FROM patrol_route WHERE area = ? AND status = 1", area);
27
+    }
28
+
29
+    // ========== 任务管理 ==========
30
+    public Map<String, Object> createTask(Long routeId, Long assigneeId, String taskDate) {
31
+        jdbc.update(
32
+            "INSERT INTO patrol_task (route_id, assignee_id, task_name, task_date, plan_start, plan_end, status) " +
33
+            "SELECT ?, ?, route_name, ?, CAST(? AS TIMESTAMP), CAST(? AS TIMESTAMP) + (estim_duration || ' minutes')::INTERVAL, 'pending' " +
34
+            "FROM patrol_route WHERE id = ?",
35
+            routeId, assigneeId, taskDate, taskDate + " 09:00:00", taskDate + " 09:00:00", routeId);
36
+        return Map.of("routeId", routeId, "assigneeId", assigneeId, "date", taskDate, "status", "created");
37
+    }
38
+
39
+    public List<Map<String, Object>> getTodayTasks(Long userId) {
40
+        return jdbc.queryForList(
41
+            "SELECT pt.*, pr.route_name, pr.area FROM patrol_task pt " +
42
+            "LEFT JOIN patrol_route pr ON pt.route_id = pr.id " +
43
+            "WHERE pt.task_date = CURRENT_DATE AND pt.assignee_id = ? " +
44
+            "ORDER BY pt.plan_start", userId);
45
+    }
46
+
47
+    public Map<String, Object> startTask(Long taskId) {
48
+        jdbc.update("UPDATE patrol_task SET status = 'in_progress', actual_start = NOW() WHERE id = ?", taskId);
49
+        return Map.of("taskId", taskId, "status", "in_progress", "startedAt", new Date());
50
+    }
51
+
52
+    public Map<String, Object> completeTask(Long taskId, double distance) {
53
+        jdbc.update(
54
+            "UPDATE patrol_task SET status = 'completed', actual_end = NOW(), distance = ? WHERE id = ?",
55
+            distance, taskId);
56
+        return Map.of("taskId", taskId, "status", "completed", "distance", distance);
57
+    }
58
+
59
+    // ========== 巡检记录 ==========
60
+    public Map<String, Object> recordCheck(Long taskId, int pointSeq, Long deviceId,
61
+                                            List<Map<String, Object>> checkItems,
62
+                                            double lng, double lat) {
63
+        jdbc.update(
64
+            "INSERT INTO patrol_record (task_id, point_seq, device_id, check_items, gps_lng, gps_lat, record_time) " +
65
+            "VALUES (?,?,?,?::jsonb,?,?,NOW())",
66
+            taskId, pointSeq, deviceId, checkItems.toString(), lng, lat);
67
+        return Map.of("taskId", taskId, "pointSeq", pointSeq, "recorded", true);
68
+    }
69
+
70
+    public List<Map<String, Object>> getTaskRecords(Long taskId) {
71
+        return jdbc.queryForList(
72
+            "SELECT * FROM patrol_record WHERE task_id = ? ORDER BY point_seq", taskId);
73
+    }
74
+
75
+    // ========== 问题上报(巡检APP) ==========
76
+    public Map<String, Object> reportIssue(Long taskId, Long deviceId, String issueType,
77
+                                            String description, List<String> photoUrls,
78
+                                            double lng, double lat) {
79
+        // 自动创建工单
80
+        jdbc.update(
81
+            "INSERT INTO patrol_task (task_name, assignee_id, task_date, status) " +
82
+            "SELECT CONCAT('问题处理: ', ?), assignee_id, CURRENT_DATE, 'pending' FROM patrol_task WHERE id = ?",
83
+            issueType + ": " + description.substring(0, Math.min(description.length(), 50)), taskId);
84
+
85
+        log.info("Issue reported: type={} desc={}", issueType, description);
86
+        return Map.of("reported", true, "issueType", issueType, "photos", photoUrls);
87
+    }
88
+
89
+    // ========== 统计分析 ==========
90
+    public Map<String, Object> getStats(String area, LocalDate start, LocalDate end) {
91
+        Map<String, Object> stats = new LinkedHashMap<>();
92
+
93
+        // 任务执行率
94
+        stats.put("completionRate", jdbc.queryForMap(
95
+            "SELECT COUNT(*) as total, SUM(CASE WHEN status='completed' THEN 1 ELSE 0 END) as completed " +
96
+            "FROM patrol_task WHERE task_date BETWEEN ? AND ?", start, end));
97
+
98
+        // 人员里程
99
+        stats.put("personDistance", jdbc.queryForList(
100
+            "SELECT u.real_name, SUM(pt.distance) as total_km " +
101
+            "FROM patrol_task pt JOIN sys_user u ON pt.assignee_id = u.id " +
102
+            "WHERE pt.task_date BETWEEN ? AND ? GROUP BY u.id, u.real_name", start, end));
103
+
104
+        // 巡检工作量
105
+        stats.put("workload", jdbc.queryForList(
106
+            "SELECT task_date, COUNT(*) as tasks, SUM(distance) as total_km " +
107
+            "FROM patrol_task WHERE task_date BETWEEN ? AND ? GROUP BY task_date ORDER BY task_date",
108
+            start, end));
109
+
110
+        // 问题分类统计
111
+        stats.put("issueStats", jdbc.queryForList(
112
+            "SELECT SUBSTRING(task_name FROM '^[^:]+') as issue_type, COUNT(*) as count " +
113
+            "FROM patrol_task WHERE task_name LIKE '%问题处理:%' AND task_date BETWEEN ? AND ? GROUP BY 1",
114
+            start, end));
115
+
116
+        return stats;
117
+    }
118
+}

+ 59
- 0
wm-patrol/src/main/java/com/water/patrol/service/PatrolTemplateService.java Просмотреть файл

@@ -0,0 +1,59 @@
1
+package com.water.patrol.service;
2
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
3
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
4
+import com.water.patrol.entity.PatrolTemplate; import com.water.patrol.mapper.PatrolTemplateMapper;
5
+import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j;
6
+import org.springframework.stereotype.Service;
7
+import java.util.*;
8
+@Slf4j @Service @RequiredArgsConstructor
9
+public class PatrolTemplateService {
10
+    private final PatrolTemplateMapper mapper;
11
+    public PatrolTemplate create(String templateName, String templateCode, Long routeId, String routeName,
12
+            Long formId, String formName, String scheduleType, String scheduleConfig) {
13
+        PatrolTemplate t = new PatrolTemplate(); t.setTemplateName(templateName); t.setTemplateCode(templateCode);
14
+        t.setRouteId(routeId); t.setRouteName(routeName); t.setFormId(formId); t.setFormName(formName);
15
+        t.setScheduleType(scheduleType); t.setScheduleConfig(scheduleConfig); t.setStatus("active");
16
+        mapper.insert(t); return t;
17
+    }
18
+    public PatrolTemplate update(Long id, PatrolTemplate patch) {
19
+        PatrolTemplate t = mapper.selectById(id);
20
+        if (t == null) throw new RuntimeException("模板不存在: "+id);
21
+        if (patch.getTemplateName() != null) t.setTemplateName(patch.getTemplateName());
22
+        if (patch.getRouteId() != null) t.setRouteId(patch.getRouteId());
23
+        if (patch.getRouteName() != null) t.setRouteName(patch.getRouteName());
24
+        if (patch.getFormId() != null) t.setFormId(patch.getFormId());
25
+        if (patch.getFormName() != null) t.setFormName(patch.getFormName());
26
+        if (patch.getScheduleType() != null) t.setScheduleType(patch.getScheduleType());
27
+        if (patch.getScheduleConfig() != null) t.setScheduleConfig(patch.getScheduleConfig());
28
+        if (patch.getRemark() != null) t.setRemark(patch.getRemark());
29
+        mapper.updateById(t); return t;
30
+    }
31
+    public void delete(Long id) { mapper.deleteById(id); }
32
+    public PatrolTemplate getDetail(Long id) { return mapper.selectById(id); }
33
+    public Map<String, Object> list(String keyword, String status, String scheduleType, int page, int size) {
34
+        LambdaQueryWrapper<PatrolTemplate> w = new LambdaQueryWrapper<>();
35
+        if (keyword != null && !keyword.isEmpty()) w.and(q -> q.like(PatrolTemplate::getTemplateName, keyword).or().like(PatrolTemplate::getTemplateCode, keyword));
36
+        if (status != null && !status.isEmpty()) w.eq(PatrolTemplate::getStatus, status);
37
+        if (scheduleType != null && !scheduleType.isEmpty()) w.eq(PatrolTemplate::getScheduleType, scheduleType);
38
+        w.orderByDesc(PatrolTemplate::getCreatedAt);
39
+        Page<PatrolTemplate> p = mapper.selectPage(new Page<>(page, size), w);
40
+        Map<String, Object> r = new LinkedHashMap<>();
41
+        r.put("records", p.getRecords()); r.put("total", p.getTotal());
42
+        r.put("page", page); r.put("size", size);
43
+        return r;
44
+    }
45
+    public void updateStatus(Long id, String status) {
46
+        PatrolTemplate t = mapper.selectById(id);
47
+        if (t == null) throw new RuntimeException("模板不存在: "+id);
48
+        t.setStatus(status); mapper.updateById(t);
49
+    }
50
+    public Map<String, Object> getStats() {
51
+        Map<String, Object> r = new LinkedHashMap<>();
52
+        r.put("total", mapper.selectCount(null));
53
+        r.put("active", mapper.selectCount(new LambdaQueryWrapper<PatrolTemplate>().eq(PatrolTemplate::getStatus,"active")));
54
+        r.put("daily", mapper.selectCount(new LambdaQueryWrapper<PatrolTemplate>().eq(PatrolTemplate::getScheduleType,"daily")));
55
+        r.put("weekly", mapper.selectCount(new LambdaQueryWrapper<PatrolTemplate>().eq(PatrolTemplate::getScheduleType,"weekly")));
56
+        r.put("monthly", mapper.selectCount(new LambdaQueryWrapper<PatrolTemplate>().eq(PatrolTemplate::getScheduleType,"monthly")));
57
+        return r;
58
+    }
59
+}

+ 47
- 0
wm-patrol/src/main/java/com/water/patrol/service/PatrolTrackService.java Просмотреть файл

@@ -0,0 +1,47 @@
1
+package com.water.patrol.service;
2
+import com.water.patrol.entity.PatrolTrackPoint; import com.water.patrol.mapper.PatrolTrackPointMapper;
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j;
5
+import org.springframework.stereotype.Service;
6
+import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.*;
7
+@Slf4j @Service @RequiredArgsConstructor
8
+public class PatrolTrackService {
9
+    private final PatrolTrackPointMapper mapper;
10
+    public PatrolTrackPoint recordPoint(Long taskId, Long workerId, BigDecimal lng, BigDecimal lat, BigDecimal speed, BigDecimal accuracy, String address) {
11
+        PatrolTrackPoint p = new PatrolTrackPoint();
12
+        p.setTaskId(taskId); p.setWorkerId(workerId); p.setLng(lng); p.setLat(lat);
13
+        p.setSpeed(speed); p.setAccuracy(accuracy); p.setAddress(address); p.setTimestamp(LocalDateTime.now());
14
+        mapper.insert(p); return p;
15
+    }
16
+    public List<PatrolTrackPoint> getTrack(Long taskId) {
17
+        return mapper.selectList(new LambdaQueryWrapper<PatrolTrackPoint>().eq(PatrolTrackPoint::getTaskId, taskId).orderByAsc(PatrolTrackPoint::getTimestamp));
18
+    }
19
+    public Map<String, Object> getTrackSummary(Long taskId) {
20
+        List<PatrolTrackPoint> pts = getTrack(taskId);
21
+        Map<String, Object> r = new LinkedHashMap<>();
22
+        r.put("totalPoints", pts.size());
23
+        if (!pts.isEmpty()) {
24
+            r.put("startTime", pts.get(0).getTimestamp()); r.put("endTime", pts.get(pts.size()-1).getTimestamp());
25
+            double d = 0; for (int i = 1; i < pts.size(); i++) d += haversine(pts.get(i-1), pts.get(i));
26
+            r.put("totalDistance", Math.round(d));
27
+        }
28
+        return r;
29
+    }
30
+    public List<Map<String, Object>> exportTrack(Long taskId) {
31
+        List<Map<String, Object>> result = new ArrayList<>();
32
+        for (PatrolTrackPoint p : getTrack(taskId)) {
33
+            Map<String, Object> m = new LinkedHashMap<>();
34
+            m.put("lng", p.getLng()); m.put("lat", p.getLat()); m.put("speed", p.getSpeed());
35
+            m.put("timestamp", p.getTimestamp()); m.put("address", p.getAddress());
36
+            result.add(m);
37
+        }
38
+        return result;
39
+    }
40
+    private double haversine(PatrolTrackPoint a, PatrolTrackPoint b) {
41
+        double R=6371000, dLat=Math.toRadians(b.getLat().doubleValue()-a.getLat().doubleValue()),
42
+               dLng=Math.toRadians(b.getLng().doubleValue()-a.getLng().doubleValue()),
43
+               la1=Math.toRadians(a.getLat().doubleValue()), la2=Math.toRadians(b.getLat().doubleValue());
44
+        double x=Math.sin(dLat/2)*Math.sin(dLat/2)+Math.cos(la1)*Math.cos(la2)*Math.sin(dLng/2)*Math.sin(dLng/2);
45
+        return R*2*Math.atan2(Math.sqrt(x),Math.sqrt(1-x));
46
+    }
47
+}

+ 122
- 0
wm-patrol/src/main/java/com/water/patrol/service/PatrolWoService.java Просмотреть файл

@@ -0,0 +1,122 @@
1
+package com.water.patrol.service;
2
+
3
+import lombok.RequiredArgsConstructor;
4
+import lombok.extern.slf4j.Slf4j;
5
+import org.springframework.jdbc.core.JdbcTemplate;
6
+import org.springframework.stereotype.Service;
7
+
8
+import java.util.*;
9
+
10
+/**
11
+ * 巡检工单服务 - Issue #86
12
+ * PAT-05 工单管理, PAT-06 工单处理
13
+ */
14
+@Slf4j
15
+@Service
16
+@RequiredArgsConstructor
17
+public class PatrolWoService {
18
+
19
+    private final JdbcTemplate jdbc;
20
+
21
+    /** 创建巡检工单 */
22
+    public Map<String, Object> create(String issueType, String description, String severity,
23
+                                       String location, Double lng, Double lat, Long taskId) {
24
+        String orderNo = "PWO-" + System.currentTimeMillis();
25
+        jdbc.update(
26
+            "INSERT INTO patrol_work_order (order_no, task_id, issue_type, description, severity, " +
27
+            "location, lng, lat, status) VALUES (?,?,?,?,?,?,?,?,?)",
28
+            orderNo, taskId, issueType, description,
29
+            severity != null ? severity : "medium",
30
+            location, lng, lat, "pending");
31
+        Map<String, Object> result = new LinkedHashMap<>();
32
+        result.put("orderNo", orderNo);
33
+        result.put("issueType", issueType);
34
+        result.put("status", "pending");
35
+        result.put("created", true);
36
+        return result;
37
+    }
38
+
39
+    /** 分派工单 */
40
+    public Map<String, Object> assign(Long woId, Long assigneeId, String assigneeName) {
41
+        int rows = jdbc.update(
42
+            "UPDATE patrol_work_order SET assignee_id = ?, assignee_name = ?, status = 'assigned' " +
43
+            "WHERE id = ?", assigneeId, assigneeName, woId);
44
+        return Map.of("success", rows > 0, "woId", woId);
45
+    }
46
+
47
+    /** 处理工单 */
48
+    public Map<String, Object> process(Long woId, String resolution) {
49
+        int rows = jdbc.update(
50
+            "UPDATE patrol_work_order SET status = 'processing', resolution = ? WHERE id = ?",
51
+            resolution, woId);
52
+        return Map.of("success", rows > 0, "woId", woId);
53
+    }
54
+
55
+    /** 解决工单 */
56
+    public Map<String, Object> resolve(Long woId) {
57
+        int rows = jdbc.update(
58
+            "UPDATE patrol_work_order SET status = 'resolved', resolved_at = NOW() WHERE id = ?", woId);
59
+        return Map.of("success", rows > 0, "woId", woId);
60
+    }
61
+
62
+    /** 工单详情 */
63
+    public Map<String, Object> getDetail(Long woId) {
64
+        List<Map<String, Object>> rows = jdbc.queryForList(
65
+            "SELECT * FROM patrol_work_order WHERE id = ?", woId);
66
+        return rows.isEmpty() ? null : rows.get(0);
67
+    }
68
+
69
+    /** 工单列表 */
70
+    public Map<String, Object> list(String status, String severity, int page, int size) {
71
+        StringBuilder sql = new StringBuilder("SELECT * FROM patrol_work_order WHERE 1=1");
72
+        List<Object> params = new ArrayList<>();
73
+        if (status != null && !status.isEmpty()) {
74
+            sql.append(" AND status = ?");
75
+            params.add(status);
76
+        }
77
+        if (severity != null && !severity.isEmpty()) {
78
+            sql.append(" AND severity = ?");
79
+            params.add(severity);
80
+        }
81
+        sql.append(" ORDER BY created_at DESC LIMIT ? OFFSET ?");
82
+        params.add(size);
83
+        params.add((page - 1) * size);
84
+
85
+        List<Map<String, Object>> records = jdbc.queryForList(sql.toString(), params.toArray());
86
+
87
+        StringBuilder countSql = new StringBuilder("SELECT COUNT(*) FROM patrol_work_order WHERE 1=1");
88
+        List<Object> countParams = new ArrayList<>();
89
+        if (status != null && !status.isEmpty()) {
90
+            countSql.append(" AND status = ?");
91
+            countParams.add(status);
92
+        }
93
+        if (severity != null && !severity.isEmpty()) {
94
+            countSql.append(" AND severity = ?");
95
+            countParams.add(severity);
96
+        }
97
+        long total = countQuery(countSql.toString(), countParams.toArray());
98
+
99
+        Map<String, Object> result = new LinkedHashMap<>();
100
+        result.put("total", total);
101
+        result.put("page", page);
102
+        result.put("size", size);
103
+        result.put("records", records);
104
+        return result;
105
+    }
106
+
107
+    /** 工单统计 */
108
+    public Map<String, Object> stats() {
109
+        List<Map<String, Object>> rows = jdbc.queryForList(
110
+            "SELECT status, COUNT(*) as count FROM patrol_work_order GROUP BY status");
111
+        Map<String, Object> result = new LinkedHashMap<>();
112
+        for (Map<String, Object> row : rows) {
113
+            result.put(String.valueOf(row.get("status")), ((Number) row.get("count")).longValue());
114
+        }
115
+        return result;
116
+    }
117
+
118
+    private long countQuery(String sql, Object... args) {
119
+        Long count = jdbc.queryForObject(sql, Long.class, args);
120
+        return count != null ? count : 0;
121
+    }
122
+}

+ 114
- 0
wm-patrol/src/main/java/com/water/patrol/service/PatrolWorkOrderService.java Просмотреть файл

@@ -0,0 +1,114 @@
1
+package com.water.patrol.service;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.water.patrol.entity.PatrolWorkOrder;
5
+import com.water.patrol.mapper.PatrolWorkOrderMapper;
6
+import lombok.RequiredArgsConstructor;
7
+import lombok.extern.slf4j.Slf4j;
8
+import org.springframework.stereotype.Service;
9
+import java.time.LocalDateTime;
10
+import java.util.*;
11
+
12
+@Slf4j
13
+@Service
14
+@RequiredArgsConstructor
15
+public class PatrolWorkOrderService {
16
+    private final PatrolWorkOrderMapper woMapper;
17
+
18
+    public PatrolWorkOrder create(Long taskId, String issueType, String description,
19
+            String photos, String severity) {
20
+        PatrolWorkOrder wo = new PatrolWorkOrder();
21
+        wo.setOrderNo("PWO-" + System.currentTimeMillis());
22
+        wo.setTaskId(taskId);
23
+        wo.setIssueType(issueType);
24
+        wo.setDescription(description);
25
+        wo.setPhotos(photos);
26
+        wo.setSeverity(severity != null ? severity : "medium");
27
+        wo.setStatus("pending");
28
+        wo.setCreatedAt(LocalDateTime.now());
29
+        wo.setUpdatedAt(LocalDateTime.now());
30
+        woMapper.insert(wo);
31
+        log.info("Work order created: {}", wo.getOrderNo());
32
+        return wo;
33
+    }
34
+
35
+    public Map<String, Object> list(String status, String severity, String keyword,
36
+            int page, int size) {
37
+        LambdaQueryWrapper<PatrolWorkOrder> w = new LambdaQueryWrapper<>();
38
+        if (status != null && !status.isEmpty()) w.eq(PatrolWorkOrder::getStatus, status);
39
+        if (severity != null && !severity.isEmpty()) w.eq(PatrolWorkOrder::getSeverity, severity);
40
+        if (keyword != null && !keyword.isEmpty()) {
41
+            w.and(q -> q.like(PatrolWorkOrder::getDescription, keyword)
42
+                .or().like(PatrolWorkOrder::getOrderNo, keyword));
43
+        }
44
+        w.orderByDesc(PatrolWorkOrder::getCreatedAt);
45
+
46
+        Long total = woMapper.selectCount(w);
47
+        w.last("LIMIT " + size + " OFFSET " + ((page - 1) * size));
48
+        List<PatrolWorkOrder> records = woMapper.selectList(w);
49
+
50
+        Map<String, Object> result = new LinkedHashMap<>();
51
+        result.put("total", total);
52
+        result.put("page", page);
53
+        result.put("size", size);
54
+        result.put("records", records);
55
+        return result;
56
+    }
57
+
58
+    public PatrolWorkOrder getDetail(Long id) {
59
+        return woMapper.selectById(id);
60
+    }
61
+
62
+    public void assign(Long id, Long assigneeId, String assigneeName) {
63
+        PatrolWorkOrder wo = woMapper.selectById(id);
64
+        if (wo == null) throw new RuntimeException("Work order not found: " + id);
65
+        wo.setAssigneeId(assigneeId);
66
+        wo.setAssigneeName(assigneeName);
67
+        wo.setStatus("assigned");
68
+        wo.setUpdatedAt(LocalDateTime.now());
69
+        woMapper.updateById(wo);
70
+        log.info("Work order {} assigned to {}", id, assigneeName);
71
+    }
72
+
73
+    public void startProcess(Long id) {
74
+        PatrolWorkOrder wo = woMapper.selectById(id);
75
+        if (wo == null) throw new RuntimeException("Work order not found: " + id);
76
+        wo.setStatus("in_progress");
77
+        wo.setUpdatedAt(LocalDateTime.now());
78
+        woMapper.updateById(wo);
79
+        log.info("Work order {} started", id);
80
+    }
81
+
82
+    public void resolve(Long id, String resolution) {
83
+        PatrolWorkOrder wo = woMapper.selectById(id);
84
+        if (wo == null) throw new RuntimeException("Work order not found: " + id);
85
+        wo.setStatus("resolved");
86
+        wo.setResolution(resolution);
87
+        wo.setResolvedAt(LocalDateTime.now());
88
+        wo.setUpdatedAt(LocalDateTime.now());
89
+        woMapper.updateById(wo);
90
+        log.info("Work order {} resolved", id);
91
+    }
92
+
93
+    public void close(Long id) {
94
+        PatrolWorkOrder wo = woMapper.selectById(id);
95
+        if (wo == null) throw new RuntimeException("Work order not found: " + id);
96
+        wo.setStatus("closed");
97
+        wo.setUpdatedAt(LocalDateTime.now());
98
+        woMapper.updateById(wo);
99
+        log.info("Work order {} closed", id);
100
+    }
101
+
102
+    public Map<String, Object> getStats() {
103
+        Map<String, Object> r = new LinkedHashMap<>();
104
+        Long total = woMapper.selectCount(new LambdaQueryWrapper<>());
105
+        r.put("total", total);
106
+
107
+        for (String s : List.of("pending", "assigned", "in_progress", "resolved", "closed")) {
108
+            LambdaQueryWrapper<PatrolWorkOrder> w = new LambdaQueryWrapper<PatrolWorkOrder>()
109
+                .eq(PatrolWorkOrder::getStatus, s);
110
+            r.put(s, woMapper.selectCount(w));
111
+        }
112
+        return r;
113
+    }
114
+}

+ 100
- 0
wm-patrol/src/main/java/com/water/patrol/service/WorkOrderService.java Просмотреть файл

@@ -0,0 +1,100 @@
1
+package com.water.patrol.service;
2
+
3
+import com.water.patrol.entity.WorkOrder;
4
+import java.util.List;
5
+
6
+public interface WorkOrderService {
7
+    
8
+    /**
9
+     * 从巡检问题创建工单
10
+     */
11
+    boolean createFromProblem(PatrolProblem problem);
12
+    
13
+    /**
14
+     * 根据ID查询工单
15
+     */
16
+    WorkOrder getWorkOrderById(Long id);
17
+    
18
+    /**
19
+     * 根据工单编号查询
20
+     */
21
+    WorkOrder getWorkOrderByOrderNo(String orderNo);
22
+    
23
+    /**
24
+     * 根据问题ID查询工单
25
+     */
26
+    WorkOrder getWorkOrderByProblemId(Long problemId);
27
+    
28
+    /**
29
+     * 根据状态查询工单列表
30
+     */
31
+    List<WorkOrder> getWorkOrdersByStatus(String status);
32
+    
33
+    /**
34
+     * 根据处理人查询工单列表
35
+     */
36
+    List<WorkOrder> getWorkOrdersByAssigneeId(Long assigneeId);
37
+    
38
+    /**
39
+     * 根据处理状态查询工单列表
40
+     */
41
+    List<WorkOrder> getWorkOrdersByProcessStatus(String processStatus);
42
+    
43
+    /**
44
+     * 更新工单信息
45
+     */
46
+    WorkOrder updateWorkOrder(WorkOrder workOrder);
47
+    
48
+    /**
49
+     * 更新工单状态
50
+     */
51
+    boolean updateWorkOrderStatus(Long id, String status, String processStatus);
52
+    
53
+    /**
54
+     * 分派工单
55
+     */
56
+    boolean assignWorkOrder(Long id, Long assigneeId, String assigneeName);
57
+    
58
+    /**
59
+     * 开始处理工单
60
+     */
61
+    boolean startWorkOrder(Long id);
62
+    
63
+    /**
64
+     * 完成工单
65
+     */
66
+    boolean completeWorkOrder(Long id, String solutionResult);
67
+    
68
+    /**
69
+     * 删除工单
70
+     */
71
+    boolean deleteWorkOrder(Long id);
72
+    
73
+    /**
74
+     * 获取工单统计信息
75
+     */
76
+    WorkOrderStatistics getWorkOrderStatistics();
77
+    
78
+    class WorkOrderStatistics {
79
+        private int totalOrders;
80
+        private int pendingCount;
81
+        private int assignedCount;
82
+        private int processingCount;
83
+        private int completedCount;
84
+        private int cancelledCount;
85
+        
86
+        // Getters and setters
87
+        public int getTotalOrders() { return totalOrders; }
88
+        public void setTotalOrders(int totalOrders) { this.totalOrders = totalOrders; }
89
+        public int getPendingCount() { return pendingCount; }
90
+        public void setPendingCount(int pendingCount) { this.pendingCount = pendingCount; }
91
+        public int getAssignedCount() { return assignedCount; }
92
+        public void setAssignedCount(int assignedCount) { this.assignedCount = assignedCount; }
93
+        public int getProcessingCount() { return processingCount; }
94
+        public void setProcessingCount(int processingCount) { this.processingCount = processingCount; }
95
+        public int getCompletedCount() { return completedCount; }
96
+        public void setCompletedCount(int completedCount) { this.completedCount = completedCount; }
97
+        public int getCancelledCount() { return cancelledCount; }
98
+        public void setCancelledCount(int cancelledCount) { this.cancelledCount = cancelledCount; }
99
+    }
100
+}

+ 109
- 0
wm-patrol/src/main/java/com/water/patrol/service/impl/PatrolProblemServiceImpl.java Просмотреть файл

@@ -0,0 +1,109 @@
1
+package com.water.patrol.service.impl;
2
+
3
+import com.water.patrol.entity.PatrolProblem;
4
+import com.water.patrol.mapper.PatrolProblemMapper;
5
+import com.water.patrol.service.PatrolProblemService;
6
+import com.water.patrol.service.WorkOrderService;
7
+import org.springframework.beans.factory.annotation.Autowired;
8
+import org.springframework.stereotype.Service;
9
+import org.springframework.transaction.annotation.Transactional;
10
+
11
+import java.time.LocalDateTime;
12
+import java.util.List;
13
+
14
+@Service
15
+public class PatrolProblemServiceImpl implements PatrolProblemService {
16
+
17
+    @Autowired
18
+    private PatrolProblemMapper patrolProblemMapper;
19
+    
20
+    @Autowired
21
+    private WorkOrderService workOrderService;
22
+
23
+    @Override
24
+    @Transactional
25
+    public PatrolProblem createProblem(PatrolProblem patrolProblem) {
26
+        // 生成问题编号
27
+        String problemNo = patrolProblemMapper.generateProblemNo();
28
+        patrolProblem.setProblemNo(problemNo);
29
+        patrolProblem.setReportTime(LocalDateTime.now());
30
+        patrolProblem.setStatus("reported");
31
+        patrolProblem.setCreatedAt(LocalDateTime.now());
32
+        patrolProblem.setUpdatedAt(LocalDateTime.now());
33
+        
34
+        patrolProblemMapper.insert(patrolProblem);
35
+        
36
+        // 自动创建工单
37
+        autoCreateWorkOrder(patrolProblem.getId());
38
+        
39
+        return patrolProblem;
40
+    }
41
+
42
+    @Override
43
+    public PatrolProblem getProblemById(Long id) {
44
+        return patrolProblemMapper.selectById(id);
45
+    }
46
+
47
+    @Override
48
+    public PatrolProblem getProblemByProblemNo(String problemNo) {
49
+        return patrolProblemMapper.selectByProblemNo(problemNo);
50
+    }
51
+
52
+    @Override
53
+    public List<PatrolProblem> getProblemsByTaskId(Long taskId) {
54
+        return patrolProblemMapper.selectByTaskId(taskId);
55
+    }
56
+
57
+    @Override
58
+    public List<PatrolProblem> getProblemsByStatus(String status) {
59
+        return patrolProblemMapper.selectByStatus(status);
60
+    }
61
+
62
+    @Override
63
+    public List<PatrolProblem> getProblemsByDeviceId(Long deviceId) {
64
+        return patrolProblemMapper.selectByDeviceId(deviceId);
65
+    }
66
+
67
+    @Override
68
+    @Transactional
69
+    public PatrolProblem updateProblem(PatrolProblem patrolProblem) {
70
+        patrolProblem.setUpdatedAt(LocalDateTime.now());
71
+        patrolProblemMapper.update(patrolProblem);
72
+        return patrolProblem;
73
+    }
74
+
75
+    @Override
76
+    @Transactional
77
+    public boolean updateProblemStatus(Long id, String status, Long workOrderId) {
78
+        LocalDateTime now = LocalDateTime.now();
79
+        return patrolProblemMapper.updateStatus(id, status, workOrderId) > 0;
80
+    }
81
+
82
+    @Override
83
+    @Transactional
84
+    public boolean deleteProblem(Long id) {
85
+        return patrolProblemMapper.deleteById(id) > 0;
86
+    }
87
+
88
+    @Override
89
+    @Transactional
90
+    public boolean autoCreateWorkOrder(Long problemId) {
91
+        PatrolProblem problem = getProblemById(problemId);
92
+        if (problem == null) {
93
+            return false;
94
+        }
95
+        
96
+        return workOrderService.createFromProblem(problem);
97
+    }
98
+
99
+    @Override
100
+    public ProblemStatistics getProblemStatistics() {
101
+        ProblemStatistics statistics = new ProblemStatistics();
102
+        statistics.setTotalProblems(patrolProblemMapper.countAll());
103
+        statistics.setReportedCount(patrolProblemMapper.countByStatus("reported"));
104
+        statistics.setProcessingCount(patrolProblemMapper.countByStatus("processing"));
105
+        statistics.setCompletedCount(patrolProblemMapper.countByStatus("completed"));
106
+        statistics.setClosedCount(patrolProblemMapper.countByStatus("closed"));
107
+        return statistics;
108
+    }
109
+}

+ 241
- 0
wm-patrol/src/main/java/com/water/patrol/service/impl/WorkOrderServiceImpl.java Просмотреть файл

@@ -0,0 +1,241 @@
1
+package com.water.patrol.service.impl;
2
+
3
+import com.water.patrol.entity.PatrolProblem;
4
+import com.water.patrol.entity.WorkOrder;
5
+import com.water.patrol.entity.WorkOrderProcess;
6
+import com.water.patrol.mapper.WorkOrderMapper;
7
+import com.water.patrol.mapper.WorkOrderProcessMapper;
8
+import com.water.patrol.service.WorkOrderService;
9
+import org.springframework.beans.factory.annotation.Autowired;
10
+import org.springframework.stereotype.Service;
11
+import org.springframework.transaction.annotation.Transactional;
12
+
13
+import java.time.LocalDateTime;
14
+import java.util.List;
15
+
16
+@Service
17
+public class WorkOrderServiceImpl implements WorkOrderService {
18
+
19
+    @Autowired
20
+    private WorkOrderMapper workOrderMapper;
21
+    
22
+    @Autowired
23
+    private WorkOrderProcessMapper workOrderProcessMapper;
24
+
25
+    @Override
26
+    @Transactional
27
+    public boolean createFromProblem(PatrolProblem problem) {
28
+        WorkOrder workOrder = new WorkOrder();
29
+        workOrder.setOrderNo(workOrderMapper.generateOrderNo());
30
+        workOrder.setProblemId(problem.getId());
31
+        workOrder.setOrderType(determineOrderType(problem.getProblemType()));
32
+        workOrder.setPriority(determinePriority(problem.getProblemLevel()));
33
+        workOrder.setTitle(problem.getProblemTitle());
34
+        workOrder.setDescription(problem.getProblemDescription());
35
+        workOrder.setLocation(problem.getLocation());
36
+        workOrder.setReporterId(problem.getReporterId());
37
+        workOrder.setReporterName(problem.getReporterName());
38
+        workOrder.setStatus("pending");
39
+        workOrder.setProcessStatus("created");
40
+        workOrder.setEstimatedDuration(30); // 默认30分钟
41
+        workOrder.setCreatedAt(LocalDateTime.now());
42
+        workOrder.setUpdatedAt(LocalDateTime.now());
43
+        
44
+        int result = workOrderMapper.insert(workOrder);
45
+        
46
+        if (result > 0) {
47
+            // 创建处理记录
48
+            WorkOrderProcess process = new WorkOrderProcess();
49
+            process.setWorkOrderId(workOrder.getId());
50
+            process.setProcessStep("created");
51
+            process.setProcessorId(problem.getReporterId());
52
+            process.setProcessorName(problem.getReporterName());
53
+            process.setAction("create");
54
+            process.setComment("工单自动创建");
55
+            process.setCreatedAt(LocalDateTime.now());
56
+            workOrderProcessMapper.insert(process);
57
+            
58
+            // 更新问题状态
59
+            workOrderMapper.selectByProblemId(problem.getId()).setWorkOrderId(workOrder.getId());
60
+            return true;
61
+        }
62
+        
63
+        return false;
64
+    }
65
+
66
+    @Override
67
+    public WorkOrder getWorkOrderById(Long id) {
68
+        return workOrderMapper.selectById(id);
69
+    }
70
+
71
+    @Override
72
+    public WorkOrder getWorkOrderByOrderNo(String orderNo) {
73
+        return workOrderMapper.selectByOrderNo(orderNo);
74
+    }
75
+
76
+    @Override
77
+    public WorkOrder getWorkOrderByProblemId(Long problemId) {
78
+        return workOrderMapper.selectByProblemId(problemId);
79
+    }
80
+
81
+    @Override
82
+    public List<WorkOrder> getWorkOrdersByStatus(String status) {
83
+        return workOrderMapper.selectByStatus(status);
84
+    }
85
+
86
+    @Override
87
+    public List<WorkOrder> getWorkOrdersByAssigneeId(Long assigneeId) {
88
+        return workOrderMapper.selectByAssigneeId(assigneeId);
89
+    }
90
+
91
+    @Override
92
+    public List<WorkOrder> getWorkOrdersByProcessStatus(String processStatus) {
93
+        return workOrderMapper.selectByProcessStatus(processStatus);
94
+    }
95
+
96
+    @Override
97
+    @Transactional
98
+    public WorkOrder updateWorkOrder(WorkOrder workOrder) {
99
+        workOrder.setUpdatedAt(LocalDateTime.now());
100
+        workOrderMapper.update(workOrder);
101
+        return workOrder;
102
+    }
103
+
104
+    @Override
105
+    @Transactional
106
+    public boolean updateWorkOrderStatus(Long id, String status, String processStatus) {
107
+        LocalDateTime now = LocalDateTime.now();
108
+        return workOrderMapper.updateStatus(id, status, processStatus) > 0;
109
+    }
110
+
111
+    @Override
112
+    @Transactional
113
+    public boolean assignWorkOrder(Long id, Long assigneeId, String assigneeName) {
114
+        LocalDateTime now = LocalDateTime.now();
115
+        
116
+        // 更新工单
117
+        boolean updated = workOrderMapper.updateAssignee(id, assigneeId, assigneeName) > 0;
118
+        
119
+        if (updated) {
120
+            // 创建处理记录
121
+            WorkOrder workOrder = getWorkOrderById(id);
122
+            WorkOrderProcess process = new WorkOrderProcess();
123
+            process.setWorkOrderId(id);
124
+            process.setProcessStep("accepted");
125
+            process.setProcessorId(assigneeId);
126
+            process.setProcessorName(assigneeName);
127
+            process.setAction("assign");
128
+            process.setComment("工单已分配");
129
+            process.setCreatedAt(now);
130
+            workOrderProcessMapper.insert(process);
131
+            
132
+            // 更新工单状态
133
+            updateWorkOrderStatus(id, "assigned", "accepted");
134
+        }
135
+        
136
+        return updated;
137
+    }
138
+
139
+    @Override
140
+    @Transactional
141
+    public boolean startWorkOrder(Long id) {
142
+        LocalDateTime now = LocalDateTime.now();
143
+        
144
+        // 更新工单
145
+        boolean updated = workOrderMapper.updateStatus(id, "processing", "in_progress") > 0;
146
+        
147
+        if (updated) {
148
+            // 创建处理记录
149
+            WorkOrder workOrder = getWorkOrderById(id);
150
+            WorkOrderProcess process = new WorkOrderProcess();
151
+            process.setWorkOrderId(id);
152
+            process.setProcessStep("in_progress");
153
+            process.setProcessorId(workOrder.getAssigneeId());
154
+            process.setProcessorName(workOrder.getAssigneeName());
155
+            process.setAction("start");
156
+            process.setComment("开始处理工单");
157
+            process.setCreatedAt(now);
158
+            workOrderProcessMapper.insert(process);
159
+        }
160
+        
161
+        return updated;
162
+    }
163
+
164
+    @Override
165
+    @Transactional
166
+    public boolean completeWorkOrder(Long id, String solutionResult) {
167
+        LocalDateTime now = LocalDateTime.now();
168
+        
169
+        // 更新工单
170
+        boolean updated = workOrderMapper.updateCompletion(id, now, now, solutionResult) > 0;
171
+        
172
+        if (updated) {
173
+            // 创建处理记录
174
+            WorkOrder workOrder = getWorkOrderById(id);
175
+            WorkOrderProcess process = new WorkOrderProcess();
176
+            process.setWorkOrderId(id);
177
+            process.setProcessStep("completed");
178
+            process.setProcessorId(workOrder.getAssigneeId());
179
+            process.setProcessorName(workOrder.getAssigneeName());
180
+            process.setAction("complete");
181
+            process.setComment("工单已完成");
182
+            process.setCreatedAt(now);
183
+            workOrderProcessMapper.insert(process);
184
+            
185
+            // 更新工单状态
186
+            updateWorkOrderStatus(id, "completed", "completed");
187
+        }
188
+        
189
+        return updated;
190
+    }
191
+
192
+    @Override
193
+    @Transactional
194
+    public boolean deleteWorkOrder(Long id) {
195
+        return workOrderMapper.deleteById(id) > 0;
196
+    }
197
+
198
+    @Override
199
+    public WorkOrderStatistics getWorkOrderStatistics() {
200
+        WorkOrderStatistics statistics = new WorkOrderStatistics();
201
+        statistics.setTotalOrders(workOrderMapper.countAll());
202
+        statistics.setPendingCount(workOrderMapper.countByStatus("pending"));
203
+        statistics.setAssignedCount(workOrderMapper.countByStatus("assigned"));
204
+        statistics.setProcessingCount(workOrderMapper.countByStatus("processing"));
205
+        statistics.setCompletedCount(workOrderMapper.countByStatus("completed"));
206
+        statistics.setCancelledCount(workOrderMapper.countByStatus("cancelled"));
207
+        return statistics;
208
+    }
209
+    
210
+    /**
211
+     * 根据问题类型确定工单类型
212
+     */
213
+    private String determineOrderType(String problemType) {
214
+        if (problemType.contains("设备")) {
215
+            return "设备维修";
216
+        } else if (problemType.contains("水质")) {
217
+            return "水质处理";
218
+        } else if (problemType.contains("安全")) {
219
+            return "安全隐患处理";
220
+        } else if (problemType.contains("环境")) {
221
+            return "清洁";
222
+        } else {
223
+            return "其他";
224
+        }
225
+    }
226
+    
227
+    /**
228
+     * 根据问题级别确定优先级
229
+     */
230
+    private String determinePriority(String problemLevel) {
231
+        if (problemLevel.equals("critical")) {
232
+            return "critical";
233
+        } else if (problemLevel.equals("high")) {
234
+            return "high";
235
+        } else if (problemLevel.equals("normal")) {
236
+            return "normal";
237
+        } else {
238
+            return "low";
239
+        }
240
+    }
241
+}

+ 17
- 0
wm-patrol/src/main/resources/application.yml Просмотреть файл

@@ -0,0 +1,17 @@
1
+server:
2
+  port: 8087
3
+
4
+spring:
5
+  application:
6
+    name: wm-patrol
7
+  datasource:
8
+    url: jdbc:postgresql://${PG_HOST:127.0.0.1}:5432/water_management
9
+    username: ${PG_USER:water}
10
+    password: ${PG_PASS:water123}
11
+  cloud:
12
+    nacos:
13
+      discovery:
14
+        server-addr: ${NACOS_HOST:127.0.0.1}:8848
15
+
16
+mybatis-plus:
17
+  mapper-locations: classpath*:/mapper/**/*.xml

+ 103
- 0
wm-patrol/src/main/resources/mapper/PatrolProblemMapper.xml Просмотреть файл

@@ -0,0 +1,103 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
3
+<mapper namespace="com.water.patrol.mapper.PatrolProblemMapper">
4
+
5
+    <resultMap id="BaseResultMap" type="com.water.patrol.entity.PatrolProblem">
6
+        <id column="id" property="id" />
7
+        <result column="problem_no" property="problemNo" />
8
+        <result column="task_id" property="taskId" />
9
+        <result column="point_seq" property="pointSeq" />
10
+        <result column="device_id" property="deviceId" />
11
+        <result column="device_name" property="deviceName" />
12
+        <result column="problem_type" property="problemType" />
13
+        <result column="problem_level" property="problemLevel" />
14
+        <result column="problem_title" property="problemTitle" />
15
+        <result column="problem_description" property="problemDescription" />
16
+        <result column="location" property="location" />
17
+        <result column="lng" property="lng" />
18
+        <result column="lat" property="lat" />
19
+        <result column="photo_urls" property="photoUrls" typeHandler="com.water.common.handler.JsonListTypeHandler" />
20
+        <result column="reporter_id" property="reporterId" />
21
+        <result column="reporter_name" property="reporterName" />
22
+        <result column="report_time" property="reportTime" />
23
+        <result column="status" property="status" />
24
+        <result column="work_order_id" property="workOrderId" />
25
+        <result column="created_at" property="createdAt" />
26
+        <result column="updated_at" property="updatedAt" />
27
+    </resultMap>
28
+
29
+    <insert id="insert" parameterType="com.water.patrol.entity.PatrolProblem">
30
+        INSERT INTO patrol_problem (
31
+            problem_no, task_id, point_seq, device_id, device_name,
32
+            problem_type, problem_level, problem_title, problem_description,
33
+            location, lng, lat, photo_urls, reporter_id, reporter_name,
34
+            report_time, status, work_order_id, created_at, updated_at
35
+        ) VALUES (
36
+            #{problemNo}, #{taskId}, #{pointSeq}, #{deviceId}, #{deviceName},
37
+            #{problemType}, #{problemLevel}, #{problemTitle}, #{problemDescription},
38
+            #{location}, #{lng}, #{lat}, #{photoUrls}, #{reporterId}, #{reporterName},
39
+            #{reportTime}, #{status}, #{workOrderId}, #{createdAt}, #{updatedAt}
40
+        )
41
+    </insert>
42
+
43
+    <select id="selectById" resultMap="BaseResultMap">
44
+        SELECT * FROM patrol_problem WHERE id = #{id}
45
+    </select>
46
+
47
+    <select id="selectByProblemNo" resultMap="BaseResultMap">
48
+        SELECT * FROM patrol_problem WHERE problem_no = #{problemNo}
49
+    </select>
50
+
51
+    <select id="selectByTaskId" resultMap="BaseResultMap">
52
+        SELECT * FROM patrol_problem WHERE task_id = #{taskId} ORDER BY created_at DESC
53
+    </select>
54
+
55
+    <select id="selectByStatus" resultMap="BaseResultMap">
56
+        SELECT * FROM patrol_problem WHERE status = #{status} ORDER BY created_at DESC
57
+    </select>
58
+
59
+    <select id="selectByDeviceId" resultMap="BaseResultMap">
60
+        SELECT * FROM patrol_problem WHERE device_id = #{deviceId} ORDER BY created_at DESC
61
+    </select>
62
+
63
+    <update id="update" parameterType="com.water.patrol.entity.PatrolProblem">
64
+        UPDATE patrol_problem SET
65
+            problem_type = #{problemType},
66
+            problem_level = #{problemLevel},
67
+            problem_title = #{problemTitle},
68
+            problem_description = #{problemDescription},
69
+            location = #{location},
70
+            lng = #{lng},
71
+            lat = #{lat},
72
+            photo_urls = #{photoUrls},
73
+            status = #{status},
74
+            work_order_id = #{workOrderId},
75
+            updated_at = #{updatedAt}
76
+        WHERE id = #{id}
77
+    </update>
78
+
79
+    <update id="updateStatus">
80
+        UPDATE patrol_problem SET
81
+            status = #{status},
82
+            work_order_id = #{workOrderId},
83
+            updated_at = #{updatedAt}
84
+        WHERE id = #{id}
85
+    </update>
86
+
87
+    <delete id="deleteById">
88
+        DELETE FROM patrol_problem WHERE id = #{id}
89
+    </delete>
90
+
91
+    <select id="generateProblemNo" resultType="string">
92
+        SELECT 'WQ' || to_char(NOW(), 'YYYY') || '-' || LPAD(nextval('seq_patrol_problem')::text, 3, '0')
93
+    </select>
94
+
95
+    <select id="countAll" resultType="int">
96
+        SELECT COUNT(*) FROM patrol_problem
97
+    </select>
98
+
99
+    <select id="countByStatus" resultType="int">
100
+        SELECT COUNT(*) FROM patrol_problem WHERE status = #{status}
101
+    </select>
102
+
103
+</mapper>

+ 148
- 0
wm-patrol/src/main/resources/mapper/WorkOrderMapper.xml Просмотреть файл

@@ -0,0 +1,148 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
3
+<mapper namespace="com.water.patrol.mapper.WorkOrderMapper">
4
+
5
+    <resultMap id="BaseResultMap" type="com.water.patrol.entity.WorkOrder">
6
+        <id column="id" property="id" />
7
+        <result column="order_no" property="orderNo" />
8
+        <result column="problem_id" property="problemId" />
9
+        <result column="order_type" property="orderType" />
10
+        <result column="priority" property="priority" />
11
+        <result column="title" property="title" />
12
+        <result column="description" property="description" />
13
+        <result column="location" property="location" />
14
+        <result column="contact_person" property="contactPerson" />
15
+        <result column="contact_phone" property="contactPhone" />
16
+        <result column="reporter_id" property="reporterId" />
17
+        <result column="reporter_name" property="reporterName" />
18
+        <result column="assignee_id" property="assigneeId" />
19
+        <result column="assignee_name" property="assigneeName" />
20
+        <result column="status" property="status" />
21
+        <result column="process_status" property="processStatus" />
22
+        <result column="estimated_duration" property="estimatedDuration" />
23
+        <result column="actual_start_time" property="actualStartTime" />
24
+        <result column="actual_end_time" property="actualEndTime" />
25
+        <result column="completion_time" property="completionTime" />
26
+        <result column="photos_before" property="photosBefore" typeHandler="com.water.common.handler.JsonListTypeHandler" />
27
+        <result column="photos_after" property="photosAfter" typeHandler="com.water.common.handler.JsonListTypeHandler" />
28
+        <result column="solution_description" property="solutionDescription" />
29
+        <result column="solution_result" property="solutionResult" />
30
+        <result column="customer_feedback" property="customerFeedback" />
31
+        <result column="created_at" property="createdAt" />
32
+        <result column="updated_at" property="updatedAt" />
33
+    </resultMap>
34
+
35
+    <insert id="insert" parameterType="com.water.patrol.entity.WorkOrder">
36
+        INSERT INTO work_order (
37
+            order_no, problem_id, order_type, priority, title, description,
38
+            location, contact_person, contact_phone, reporter_id, reporter_name,
39
+            assignee_id, assignee_name, status, process_status,
40
+            estimated_duration, actual_start_time, actual_end_time, completion_time,
41
+            photos_before, photos_after, solution_description, solution_result,
42
+            customer_feedback, created_at, updated_at
43
+        ) VALUES (
44
+            #{orderNo}, #{problemId}, #{orderType}, #{priority}, #{title}, #{description},
45
+            #{location}, #{contactPerson}, #{contactPhone}, #{reporterId}, #{reporterName},
46
+            #{assigneeId}, #{assigneeName}, #{status}, #{processStatus},
47
+            #{estimatedDuration}, #{actualStartTime}, #{actualEndTime}, #{completionTime},
48
+            #{photosBefore}, #{photosAfter}, #{solutionDescription}, #{solutionResult},
49
+            #{customerFeedback}, #{createdAt}, #{updatedAt}
50
+        )
51
+    </insert>
52
+
53
+    <select id="selectById" resultMap="BaseResultMap">
54
+        SELECT * FROM work_order WHERE id = #{id}
55
+    </select>
56
+
57
+    <select id="selectByOrderNo" resultMap="BaseResultMap">
58
+        SELECT * FROM work_order WHERE order_no = #{orderNo}
59
+    </select>
60
+
61
+    <select id="selectByProblemId" resultMap="BaseResultMap">
62
+        SELECT * FROM work_order WHERE problem_id = #{problemId}
63
+    </select>
64
+
65
+    <select id="selectByStatus" resultMap="BaseResultMap">
66
+        SELECT * FROM work_order WHERE status = #{status} ORDER BY created_at DESC
67
+    </select>
68
+
69
+    <select id="selectByAssigneeId" resultMap="BaseResultMap">
70
+        SELECT * FROM work_order WHERE assignee_id = #{assigneeId} ORDER BY created_at DESC
71
+    </select>
72
+
73
+    <select id="selectByProcessStatus" resultMap="BaseResultMap">
74
+        SELECT * FROM work_order WHERE process_status = #{processStatus} ORDER BY created_at DESC
75
+    </select>
76
+
77
+    <update id="update" parameterType="com.water.patrol.entity.WorkOrder">
78
+        UPDATE work_order SET
79
+            order_type = #{orderType},
80
+            priority = #{priority},
81
+            title = #{title},
82
+            description = #{description},
83
+            location = #{location},
84
+            contact_person = #{contactPerson},
85
+            contact_phone = #{contactPhone},
86
+            assignee_id = #{assigneeId},
87
+            assignee_name = #{assigneeName},
88
+            status = #{status},
89
+            process_status = #{processStatus},
90
+            estimated_duration = #{estimatedDuration},
91
+            actual_start_time = #{actualStartTime},
92
+            actual_end_time = #{actualEndTime},
93
+            completion_time = #{completionTime},
94
+            photos_before = #{photosBefore},
95
+            photos_after = #{photosAfter},
96
+            solution_description = #{solutionDescription},
97
+            solution_result = #{solutionResult},
98
+            customer_feedback = #{customerFeedback},
99
+            updated_at = #{updatedAt}
100
+        WHERE id = #{id}
101
+    </update>
102
+
103
+    <update id="updateStatus">
104
+        UPDATE work_order SET
105
+            status = #{status},
106
+            process_status = #{processStatus},
107
+            updated_at = #{updatedAt}
108
+        WHERE id = #{id}
109
+    </update>
110
+
111
+    <update id="updateAssignee">
112
+        UPDATE work_order SET
113
+            assignee_id = #{assigneeId},
114
+            assignee_name = #{assigneeName},
115
+            updated_at = #{updatedAt}
116
+        WHERE id = #{id}
117
+    </update>
118
+
119
+    <update id="updateCompletion">
120
+        UPDATE work_order SET
121
+            actual_end_time = #{actualEndTime},
122
+            completion_time = #{completionTime},
123
+            solution_result = #{solutionResult},
124
+            updated_at = #{updatedAt}
125
+        WHERE id = #{id}
126
+    </update>
127
+
128
+    <delete id="deleteById">
129
+        DELETE FROM work_order WHERE id = #{id}
130
+    </delete>
131
+
132
+    <select id="generateOrderNo" resultType="string">
133
+        SELECT 'WO' || to_char(NOW(), 'YYYY') || '-' || LPAD(nextval('seq_work_order')::text, 3, '0')
134
+    </select>
135
+
136
+    <select id="countAll" resultType="int">
137
+        SELECT COUNT(*) FROM work_order
138
+    </select>
139
+
140
+    <select id="countByStatus" resultType="int">
141
+        SELECT COUNT(*) FROM work_order WHERE status = #{status}
142
+    </select>
143
+
144
+    <select id="countByProcessStatus" resultType="int">
145
+        SELECT COUNT(*) FROM work_order WHERE process_status = #{processStatus}
146
+    </select>
147
+
148
+</mapper>

+ 45
- 0
wm-patrol/src/main/resources/mapper/WorkOrderProcessMapper.xml Просмотреть файл

@@ -0,0 +1,45 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
3
+<mapper namespace="com.water.patrol.mapper.WorkOrderProcessMapper">
4
+
5
+    <resultMap id="BaseResultMap" type="com.water.patrol.entity.WorkOrderProcess">
6
+        <id column="id" property="id" />
7
+        <result column="work_order_id" property="workOrderId" />
8
+        <result column="process_step" property="processStep" />
9
+        <result column="processor_id" property="processorId" />
10
+        <result column="processor_name" property="processorName" />
11
+        <result column="action" property="action" />
12
+        <result column="comment" property="comment" />
13
+        <result column="photos" property="photos" typeHandler="com.water.common.handler.JsonListTypeHandler" />
14
+        <result column="created_at" property="createdAt" />
15
+    </resultMap>
16
+
17
+    <insert id="insert" parameterType="com.water.patrol.entity.WorkOrderProcess">
18
+        INSERT INTO work_order_process (
19
+            work_order_id, process_step, processor_id, processor_name,
20
+            action, comment, photos, created_at
21
+        ) VALUES (
22
+            #{workOrderId}, #{processStep}, #{processorId}, #{processorName},
23
+            #{action}, #{comment}, #{photos}, #{createdAt}
24
+        )
25
+    </insert>
26
+
27
+    <select id="selectById" resultMap="BaseResultMap">
28
+        SELECT * FROM work_order_process WHERE id = #{id}
29
+    </select>
30
+
31
+    <select id="selectByWorkOrderId" resultMap="BaseResultMap">
32
+        SELECT * FROM work_order_process WHERE work_order_id = #{workOrderId} ORDER BY created_at DESC
33
+    </select>
34
+
35
+    <select id="selectLatestByWorkOrderId" resultMap="BaseResultMap">
36
+        SELECT * FROM work_order_process 
37
+        WHERE work_order_id = #{workOrderId} 
38
+        ORDER BY created_at DESC LIMIT 1
39
+    </select>
40
+
41
+    <delete id="deleteById">
42
+        DELETE FROM work_order_process WHERE id = #{id}
43
+    </delete>
44
+
45
+</mapper>

+ 35
- 0
wm-patrol/src/main/resources/sql/V86__patrol_core.sql Просмотреть файл

@@ -0,0 +1,35 @@
1
+CREATE TABLE IF NOT EXISTS pat_track_point (
2
+    id BIGSERIAL PRIMARY KEY, task_id BIGINT NOT NULL, worker_id BIGINT,
3
+    lng NUMERIC(10,6), lat NUMERIC(10,6), speed NUMERIC(8,2), accuracy NUMERIC(8,2),
4
+    address VARCHAR(200), timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW()
5
+);
6
+CREATE INDEX IF NOT EXISTS idx_pat_track_task ON pat_track_point(task_id);
7
+
8
+CREATE TABLE IF NOT EXISTS pat_task (
9
+    id BIGSERIAL PRIMARY KEY, task_no VARCHAR(32) UNIQUE NOT NULL, task_name VARCHAR(100),
10
+    route_id BIGINT, worker_id BIGINT, worker_name VARCHAR(50), status VARCHAR(20) DEFAULT 'pending',
11
+    start_time TIMESTAMPTZ, end_time TIMESTAMPTZ, checkpoints INT DEFAULT 0,
12
+    completed_checkpoints INT DEFAULT 0, abnormal_count INT DEFAULT 0,
13
+    distance DOUBLE PRECISION DEFAULT 0, remark VARCHAR(500),
14
+    created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW()
15
+);
16
+CREATE INDEX IF NOT EXISTS idx_pat_task_status ON pat_task(status);
17
+CREATE INDEX IF NOT EXISTS idx_pat_task_worker ON pat_task(worker_id);
18
+
19
+CREATE TABLE IF NOT EXISTS pat_device (
20
+    id BIGSERIAL PRIMARY KEY, device_no VARCHAR(32) UNIQUE NOT NULL, device_name VARCHAR(100),
21
+    device_type VARCHAR(30), location VARCHAR(200), lng NUMERIC(10,6), lat NUMERIC(10,6),
22
+    status VARCHAR(20) DEFAULT 'normal', last_maintenance_date DATE, next_maintenance_date DATE,
23
+    remark VARCHAR(500), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW()
24
+);
25
+CREATE INDEX IF NOT EXISTS idx_pat_device_type ON pat_device(device_type);
26
+CREATE INDEX IF NOT EXISTS idx_pat_device_status ON pat_device(status);
27
+
28
+CREATE TABLE IF NOT EXISTS pat_work_order (
29
+    id BIGSERIAL PRIMARY KEY, order_no VARCHAR(32) UNIQUE NOT NULL, task_id BIGINT,
30
+    issue_type VARCHAR(30), description TEXT, photos TEXT, severity VARCHAR(20) DEFAULT 'medium',
31
+    assignee_id BIGINT, assignee_name VARCHAR(50), status VARCHAR(20) DEFAULT 'pending',
32
+    resolution TEXT, resolved_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW()
33
+);
34
+CREATE INDEX IF NOT EXISTS idx_pat_wo_status ON pat_work_order(status);
35
+CREATE INDEX IF NOT EXISTS idx_pat_wo_task ON pat_work_order(task_id);

+ 24
- 0
wm-patrol/src/main/resources/sql/V87__patrol_setup.sql Просмотреть файл

@@ -0,0 +1,24 @@
1
+CREATE TABLE IF NOT EXISTS pat_area (
2
+    id BIGSERIAL PRIMARY KEY, area_name VARCHAR(100), area_code VARCHAR(32) UNIQUE,
3
+    description VARCHAR(500), center_lng NUMERIC(10,6), center_lat NUMERIC(10,6),
4
+    boundary TEXT, radius DOUBLE PRECISION, status VARCHAR(20) DEFAULT 'active',
5
+    created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW()
6
+);
7
+CREATE TABLE IF NOT EXISTS pat_route_setup (
8
+    id BIGSERIAL PRIMARY KEY, route_name VARCHAR(100), route_code VARCHAR(32) UNIQUE,
9
+    area_id BIGINT, area_name VARCHAR(100), checkpoint_count INT DEFAULT 0,
10
+    checkpoint_list TEXT, total_distance DOUBLE PRECISION, estimated_minutes INT,
11
+    status VARCHAR(20) DEFAULT 'active', remark VARCHAR(500),
12
+    created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW()
13
+);
14
+CREATE TABLE IF NOT EXISTS pat_form (
15
+    id BIGSERIAL PRIMARY KEY, form_name VARCHAR(100), form_code VARCHAR(32) UNIQUE,
16
+    form_type VARCHAR(30), field_config TEXT, status VARCHAR(20) DEFAULT 'active',
17
+    remark VARCHAR(500), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW()
18
+);
19
+CREATE TABLE IF NOT EXISTS pat_template (
20
+    id BIGSERIAL PRIMARY KEY, template_name VARCHAR(100), template_code VARCHAR(32) UNIQUE,
21
+    route_id BIGINT, route_name VARCHAR(100), form_id BIGINT, form_name VARCHAR(100),
22
+    schedule_type VARCHAR(20), schedule_config TEXT, status VARCHAR(20) DEFAULT 'active',
23
+    remark VARCHAR(500), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW()
24
+);

+ 23
- 0
wm-patrol/src/main/resources/sql/V88__patrol_app.sql Просмотреть файл

@@ -0,0 +1,23 @@
1
+-- V88: 巡查APP相关表 - Issue #88
2
+-- PAT-21~PAT-29
3
+
4
+-- 用户巡检统计(按月汇总)
5
+CREATE TABLE IF NOT EXISTS patrol_user_stats_monthly (
6
+    id              BIGSERIAL PRIMARY KEY,
7
+    user_id         BIGINT NOT NULL,
8
+    year_month      VARCHAR(7) NOT NULL,  -- '2026-06'
9
+    completed_tasks INT DEFAULT 0,
10
+    total_distance  DOUBLE PRECISION DEFAULT 0,
11
+    issue_count     INT DEFAULT 0,
12
+    patrol_hours    DOUBLE PRECISION DEFAULT 0,
13
+    created_at      TIMESTAMPTZ DEFAULT NOW(),
14
+    updated_at      TIMESTAMPTZ DEFAULT NOW(),
15
+    UNIQUE(user_id, year_month)
16
+);
17
+CREATE INDEX IF NOT EXISTS idx_patrol_stats_user ON patrol_user_stats_monthly(user_id, year_month);
18
+
19
+-- 问题上报表加 voice_url 字段(patrol_problem 已在 problem_reporting.sql 创建)
20
+ALTER TABLE patrol_problem ADD COLUMN IF NOT EXISTS voice_url VARCHAR(500);
21
+
22
+-- 用户头像(如果 sys_user 表无 avatar 字段则添加)
23
+ALTER TABLE sys_user ADD COLUMN IF NOT EXISTS avatar VARCHAR(500);

+ 24
- 0
wm-patrol/src/test/java/com/water/patrol/PatrolAppTest.java Просмотреть файл

@@ -0,0 +1,24 @@
1
+package com.water.patrol;
2
+import com.water.patrol.entity.*; import com.water.patrol.mapper.*; import com.water.patrol.service.*;
3
+import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith;
4
+import org.mockito.*; import org.mockito.junit.jupiter.MockitoExtension;
5
+import java.math.BigDecimal; import java.util.*;
6
+import static org.junit.jupiter.api.Assertions.*;
7
+import static org.mockito.Mockito.*;
8
+@ExtendWith(MockitoExtension.class)
9
+class PatrolAppTest {
10
+    @Mock PatrolTaskMapper taskMapper; @Mock PatrolWorkOrderMapper woMapper; @Mock PatrolIssueReportMapper irMapper;
11
+    @InjectMocks PatrolAppTaskService taskSvc; @InjectMocks PatrolAppWorkOrderService woSvc;
12
+    @InjectMocks PatrolAppIssueService issueSvc; @InjectMocks PatrolAppProfileService profileSvc;
13
+
14
+    @Test void testMyTasks() { when(taskMapper.selectList(any())).thenReturn(Collections.emptyList()); when(taskMapper.selectCount(any())).thenReturn(0L); Map<String,Object> r=taskSvc.myTasks(100L,null); assertNotNull(r); assertEquals(0,r.get("totalCount")); }
15
+    @Test void testAcceptTask() { PatrolTask t=new PatrolTask(); t.setId(1L); t.setStatus("pending"); when(taskMapper.selectById(1L)).thenReturn(t); when(taskMapper.updateById(any())).thenReturn(1); taskSvc.accept(1L); assertEquals("in_progress",t.getStatus()); assertNotNull(t.getStartTime()); }
16
+    @Test void testCompleteTask() { PatrolTask t=new PatrolTask(); t.setId(1L); t.setStatus("in_progress"); when(taskMapper.selectById(1L)).thenReturn(t); when(taskMapper.updateById(any())).thenReturn(1); taskSvc.complete(1L,5,2000.0,"完成"); assertEquals("completed",t.getStatus()); assertEquals(5,(int)t.getCompletedCheckpoints()); }
17
+    @Test void testMyWorkOrders() { when(woMapper.selectList(any())).thenReturn(Collections.emptyList()); when(woMapper.selectCount(any())).thenReturn(0L); Map<String,Object> r=woSvc.myWorkOrders(100L,null); assertNotNull(r); }
18
+    @Test void testAcceptWO() { PatrolWorkOrder wo=new PatrolWorkOrder(); wo.setId(1L); wo.setStatus("assigned"); when(woMapper.selectById(1L)).thenReturn(wo); when(woMapper.updateById(any())).thenReturn(1); woSvc.accept(1L); assertEquals("in_progress",wo.getStatus()); }
19
+    @Test void testResolveWO() { PatrolWorkOrder wo=new PatrolWorkOrder(); wo.setId(1L); wo.setStatus("in_progress"); when(woMapper.selectById(1L)).thenReturn(wo); when(woMapper.updateById(any())).thenReturn(1); woSvc.resolve(1L,"已修复",null); assertEquals("resolved",wo.getStatus()); assertNotNull(wo.getResolvedAt()); }
20
+    @Test void testSubmitIssue() { when(irMapper.insert(any())).thenReturn(1); PatrolIssueReport r=issueSvc.submit(1L,100L,"张三","leak","管道漏水",null,null,new BigDecimal("116"),new BigDecimal("39"),"A路","high"); assertTrue(r.getReportNo().startsWith("PIR-")); assertEquals("submitted",r.getStatus()); }
21
+    @Test void testMyReports() { when(irMapper.selectList(any())).thenReturn(Collections.emptyList()); when(irMapper.selectCount(any())).thenReturn(0L); Map<String,Object> r=issueSvc.myReports(100L); assertNotNull(r); assertEquals(0,r.get("totalCount")); }
22
+    @Test void testProfile() { when(taskMapper.selectCount(any())).thenReturn(10L); when(woMapper.selectCount(any())).thenReturn(5L); Map<String,Object> r=profileSvc.getProfile(100L); assertEquals(100L,r.get("workerId")); }
23
+    @Test void testMyStats() { when(taskMapper.selectCount(any())).thenReturn(5L); Map<String,Object> r=taskSvc.myStats(100L); assertNotNull(r); }
24
+}

+ 27
- 0
wm-patrol/src/test/java/com/water/patrol/PatrolCoreTest.java Просмотреть файл

@@ -0,0 +1,27 @@
1
+package com.water.patrol;
2
+import com.water.patrol.entity.*; import com.water.patrol.mapper.*; import com.water.patrol.service.*;
3
+import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith;
4
+import org.mockito.*; import org.mockito.junit.jupiter.MockitoExtension;
5
+import org.springframework.jdbc.core.JdbcTemplate;
6
+import java.math.BigDecimal; import java.util.*;
7
+import static org.junit.jupiter.api.Assertions.*;
8
+import static org.mockito.Mockito.*;
9
+@ExtendWith(MockitoExtension.class)
10
+class PatrolCoreTest {
11
+    @Mock JdbcTemplate jdbc; @Mock PatrolTaskMapper taskMapper; @Mock PatrolTrackPointMapper trackMapper;
12
+    @Mock PatrolDeviceMapper deviceMapper; @Mock PatrolWorkOrderMapper woMapper;
13
+    @InjectMocks PatrolOverviewService overviewSvc; @InjectMocks PatrolLedgerService ledgerSvc;
14
+    @InjectMocks PatrolTrackService trackSvc; @InjectMocks PatrolDeviceService deviceSvc;
15
+    @InjectMocks PatrolWorkOrderService woSvc;
16
+
17
+    @Test void testOverview() { when(jdbc.queryForObject(anyString(),eq(Integer.class),any())).thenReturn(10); when(jdbc.queryForObject(anyString(),eq(Double.class),any())).thenReturn(5000.0); assertNotNull(overviewSvc.getOverview()); }
18
+    @Test void testCreateTask() { when(taskMapper.insert(any())).thenReturn(1); PatrolTask t=ledgerSvc.createTask("巡检",1L,100L,"张三"); assertTrue(t.getTaskNo().startsWith("PAT-")); assertEquals("pending",t.getStatus()); }
19
+    @Test void testStartTask() { PatrolTask t=new PatrolTask(); t.setId(1L); t.setStatus("pending"); when(taskMapper.selectById(1L)).thenReturn(t); when(taskMapper.updateById(any())).thenReturn(1); ledgerSvc.startTask(1L); assertEquals("in_progress",t.getStatus()); }
20
+    @Test void testRecordGps() { when(trackMapper.insert(any())).thenReturn(1); PatrolTrackPoint p=trackSvc.recordPoint(1L,100L,new BigDecimal("116.4"),new BigDecimal("39.9"),null,null,"test"); assertNotNull(p); assertEquals(1L,p.getTaskId()); }
21
+    @Test void testCreateDevice() { when(deviceMapper.insert(any())).thenReturn(1); PatrolDevice d=deviceSvc.create("DEV-001","阀门A","valve","A区",new BigDecimal("116"),new BigDecimal("39")); assertEquals("normal",d.getStatus()); }
22
+    @Test void testDeviceStatus() { PatrolDevice d=new PatrolDevice(); d.setId(1L); d.setStatus("normal"); when(deviceMapper.selectById(1L)).thenReturn(d); when(deviceMapper.updateById(any())).thenReturn(1); deviceSvc.updateStatus(1L,"fault"); assertEquals("fault",d.getStatus()); }
23
+    @Test void testCreateWO() { when(woMapper.insert(any())).thenReturn(1); PatrolWorkOrder wo=woSvc.create(1L,"leak","管道漏水",null,"high"); assertTrue(wo.getOrderNo().startsWith("PWO-")); assertEquals("high",wo.getSeverity()); }
24
+    @Test void testAssignWO() { PatrolWorkOrder wo=new PatrolWorkOrder(); wo.setId(1L); wo.setStatus("pending"); when(woMapper.selectById(1L)).thenReturn(wo); when(woMapper.updateById(any())).thenReturn(1); woSvc.assign(1L,200L,"李四"); assertEquals("assigned",wo.getStatus()); }
25
+    @Test void testResolveWO() { PatrolWorkOrder wo=new PatrolWorkOrder(); wo.setId(1L); wo.setStatus("in_progress"); when(woMapper.selectById(1L)).thenReturn(wo); when(woMapper.updateById(any())).thenReturn(1); woSvc.resolve(1L,"已修复"); assertEquals("resolved",wo.getStatus()); assertNotNull(wo.getResolvedAt()); }
26
+    @Test void testCloseWO() { PatrolWorkOrder wo=new PatrolWorkOrder(); wo.setId(1L); wo.setStatus("resolved"); when(woMapper.selectById(1L)).thenReturn(wo); when(woMapper.updateById(any())).thenReturn(1); woSvc.close(1L); assertEquals("closed",wo.getStatus()); }
27
+}

+ 25
- 0
wm-patrol/src/test/java/com/water/patrol/PatrolSetupTest.java Просмотреть файл

@@ -0,0 +1,25 @@
1
+package com.water.patrol;
2
+import com.water.patrol.entity.*; import com.water.patrol.mapper.*; import com.water.patrol.service.*;
3
+import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith;
4
+import org.mockito.*; import org.mockito.junit.jupiter.MockitoExtension;
5
+import java.math.BigDecimal; import java.util.*;
6
+import static org.junit.jupiter.api.Assertions.*;
7
+import static org.mockito.Mockito.*;
8
+@ExtendWith(MockitoExtension.class)
9
+class PatrolSetupTest {
10
+    @Mock PatrolAreaMapper areaMapper; @Mock PatrolRouteSetupMapper routeMapper;
11
+    @Mock PatrolFormMapper formMapper; @Mock PatrolTemplateMapper tplMapper;
12
+    @InjectMocks PatrolAreaService areaSvc; @InjectMocks PatrolRouteSetupService routeSvc;
13
+    @InjectMocks PatrolFormService formSvc; @InjectMocks PatrolTemplateService tplSvc;
14
+
15
+    @Test void testCreateArea() { when(areaMapper.insert(any())).thenReturn(1); PatrolArea a=areaSvc.create("A区","AREA-001","供水主管",new BigDecimal("116"),new BigDecimal("39"),null,500.0); assertEquals("active",a.getStatus()); assertEquals("A区",a.getAreaName()); }
16
+    @Test void testUpdateArea() { PatrolArea a=new PatrolArea(); a.setId(1L); a.setAreaName("A区"); a.setStatus("active"); when(areaMapper.selectById(1L)).thenReturn(a); when(areaMapper.updateById(any())).thenReturn(1); PatrolArea p=new PatrolArea(); p.setAreaName("B区"); assertEquals("B区",areaSvc.update(1L,p).getAreaName()); }
17
+    @Test void testAreaStatus() { PatrolArea a=new PatrolArea(); a.setId(1L); a.setStatus("active"); when(areaMapper.selectById(1L)).thenReturn(a); when(areaMapper.updateById(any())).thenReturn(1); areaSvc.updateStatus(1L,"inactive"); assertEquals("inactive",a.getStatus()); }
18
+    @Test void testCreateRoute() { when(routeMapper.insert(any())).thenReturn(1); PatrolRouteSetup r=routeSvc.create("主管线","ROUTE-001",1L,"A区",5,null,2000.0,60); assertEquals("active",r.getStatus()); assertEquals(5,(int)r.getCheckpointCount()); }
19
+    @Test void testUpdateRoute() { PatrolRouteSetup r=new PatrolRouteSetup(); r.setId(1L); r.setRouteName("R1"); when(routeMapper.selectById(1L)).thenReturn(r); when(routeMapper.updateById(any())).thenReturn(1); PatrolRouteSetup p=new PatrolRouteSetup(); p.setRouteName("R2"); assertEquals("R2",routeSvc.update(1L,p).getRouteName()); }
20
+    @Test void testRouteStatus() { PatrolRouteSetup r=new PatrolRouteSetup(); r.setId(1L); r.setStatus("active"); when(routeMapper.selectById(1L)).thenReturn(r); when(routeMapper.updateById(any())).thenReturn(1); routeSvc.updateStatus(1L,"inactive"); assertEquals("inactive",r.getStatus()); }
21
+    @Test void testCreateForm() { when(formMapper.insert(any())).thenReturn(1); PatrolForm f=formSvc.create("日常巡检表","FORM-001","checklist","{\"fields\":[]}"); assertEquals("active",f.getStatus()); assertEquals("checklist",f.getFormType()); }
22
+    @Test void testUpdateForm() { PatrolForm f=new PatrolForm(); f.setId(1L); f.setFormName("F1"); when(formMapper.selectById(1L)).thenReturn(f); when(formMapper.updateById(any())).thenReturn(1); PatrolForm p=new PatrolForm(); p.setFormName("F2"); assertEquals("F2",formSvc.update(1L,p).getFormName()); }
23
+    @Test void testCreateTemplate() { when(tplMapper.insert(any())).thenReturn(1); PatrolTemplate t=tplSvc.create("日常模板","TPL-001",1L,"主管线",1L,"日常巡检表","daily","08:00"); assertEquals("active",t.getStatus()); assertEquals("daily",t.getScheduleType()); }
24
+    @Test void testUpdateTemplate() { PatrolTemplate t=new PatrolTemplate(); t.setId(1L); t.setTemplateName("T1"); when(tplMapper.selectById(1L)).thenReturn(t); when(tplMapper.updateById(any())).thenReturn(1); PatrolTemplate p=new PatrolTemplate(); p.setTemplateName("T2"); assertEquals("T2",tplSvc.update(1L,p).getTemplateName()); }
25
+}