Przeglądaj źródła

Phase 2 #6 #7 #8 #9: 营业收费系统完整实现

#6 营收管理平台+报装:
- RevenueBaseService: SSO登录/应用接入/运维审计
- InstallService: 预受理→工程申请→派单→进度查询→统计报表

#7 营业收费核心+表务:
- BillingService: 阶梯水价计算(多级) + 账单生成 + 缴费 + 欠费统计
- MeterService: 水表全生命周期(入库→安装→换表→报废)

#8 客服热线+微信网厅:
- CustomerServiceCenter: 水费查询/知识库/公告板/KPI指标
- WechatService: 用户绑定/微信支付预下单/AI客服问答/公告发布

#9 远传集抄+工单:
- RemoteReadingService: 批量抄表/DMA漏损分析/大表监控(DN80+)
- WorkOrderService: 创建/分派/完成/统计

Controllers: RevenueController + WechatController + MeterWorkController
bot_pm 5 dni temu
rodzic
commit
4268f8df6b

+ 59
- 0
wm-revenue/src/main/java/com/water/revenue/controller/MeterWorkController.java Wyświetl plik

@@ -0,0 +1,59 @@
1
+package com.water.revenue.controller;
2
+
3
+import com.water.common.core.result.R;
4
+import com.water.revenue.service.RemoteReadingService;
5
+import com.water.revenue.service.WorkOrderService;
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.*;
12
+
13
+@Tag(name = "远传集抄 & 工单")
14
+@RestController
15
+@RequestMapping("/revenue/ops")
16
+@RequiredArgsConstructor
17
+public class MeterWorkController {
18
+
19
+    private final RemoteReadingService rrService;
20
+    private final WorkOrderService woService;
21
+
22
+    // ---- 远传集抄 ----
23
+    @PostMapping("/reading/batch/{area}")
24
+    public R<Map<String, Object>> batchRead(@PathVariable String area) {
25
+        return R.ok(rrService.batchRead(area));
26
+    }
27
+
28
+    @GetMapping("/dma/analysis")
29
+    public R<Map<String, Object>> dmaAnalysis(@RequestParam String area, @RequestParam String period) {
30
+        return R.ok(rrService.dmaAnalysis(area, period));
31
+    }
32
+
33
+    @GetMapping("/meter/large")
34
+    public R<List<Map<String, Object>>> largeMeters() {
35
+        return R.ok(rrService.largeMeterMonitor());
36
+    }
37
+
38
+    // ---- 工单 ----
39
+    @PostMapping("/work-order")
40
+    public R<Map<String, Object>> createWO(@RequestBody Map<String, Object> req) {
41
+        return R.ok(woService.create(
42
+            (String) req.get("title"), (String) req.get("type"), (String) req.get("priority"),
43
+            1L, "当前用户", (String) req.get("description"), (String) req.get("area")));
44
+    }
45
+
46
+    @PutMapping("/work-order/assign")
47
+    public R<Map<String, Object>> assign(@RequestBody Map<String, Object> req) {
48
+        return R.ok(woService.assign(
49
+            (String) req.get("woNo"), Long.parseLong(String.valueOf(req.get("assigneeId"))),
50
+            (String) req.get("assigneeName")));
51
+    }
52
+
53
+    @PutMapping("/work-order/complete")
54
+    public R<Map<String, Object>> complete(@RequestBody Map<String, Object> req) {
55
+        @SuppressWarnings("unchecked")
56
+        List<String> photos = (List<String>) req.getOrDefault("photos", List.of());
57
+        return R.ok(woService.complete((String) req.get("woNo"), (String) req.get("result"), photos));
58
+    }
59
+}

+ 79
- 0
wm-revenue/src/main/java/com/water/revenue/controller/RevenueController.java Wyświetl plik

@@ -0,0 +1,79 @@
1
+package com.water.revenue.controller;
2
+
3
+import com.water.common.core.result.R;
4
+import com.water.revenue.service.*;
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.math.BigDecimal;
11
+import java.util.*;
12
+
13
+@Tag(name = "营业收费")
14
+@RestController
15
+@RequestMapping("/revenue")
16
+@RequiredArgsConstructor
17
+public class RevenueController {
18
+
19
+    private final RevenueBaseService baseService;
20
+    private final InstallService installService;
21
+    private final BillingService billingService;
22
+    private final MeterService meterService;
23
+
24
+    // ---- 营收平台 ----
25
+    @PostMapping("/auth/sso")
26
+    public R<Map<String, Object>> ssoLogin(@RequestBody Map<String, String> req) {
27
+        return R.ok(baseService.ssoLogin(req.get("username"), req.get("password"), req.get("appType")));
28
+    }
29
+
30
+    // ---- 报装管理 ----
31
+    @PostMapping("/install/pre-apply")
32
+    public R<Map<String, Object>> preApply(@RequestBody Map<String, String> req) {
33
+        return R.ok(installService.preApply(req.get("name"), req.get("phone"),
34
+                req.get("area"), req.get("address"), req.get("customerType"), req.get("caliber")));
35
+    }
36
+
37
+    @GetMapping("/install/progress/{appNo}")
38
+    public R<Map<String, Object>> progress(@PathVariable String appNo) {
39
+        return R.ok(installService.getProgress(appNo));
40
+    }
41
+
42
+    // ---- 营业收费 ----
43
+    @PostMapping("/billing/generate")
44
+    public R<Map<String, Object>> generateBill(@RequestParam Long readingId) {
45
+        return R.ok(billingService.generateBill(readingId));
46
+    }
47
+
48
+    @PostMapping("/billing/pay")
49
+    public R<Map<String, Object>> pay(@RequestBody Map<String, Object> req) {
50
+        return R.ok(billingService.pay(
51
+            Long.parseLong(String.valueOf(req.get("billId"))),
52
+            (String) req.get("payMethod"),
53
+            (String) req.get("payChannel"),
54
+            new BigDecimal(String.valueOf(req.get("amount")))));
55
+    }
56
+
57
+    // ---- 表务管理 ----
58
+    @PostMapping("/meter/stock-in")
59
+    public R<String> stockIn(@RequestBody Map<String, String> req) {
60
+        meterService.stockIn(req.get("meterNo"), req.get("caliber"),
61
+                req.get("meterType"), req.get("manufacturer"), Integer.parseInt(req.get("quantity")));
62
+        return R.ok("入库成功");
63
+    }
64
+
65
+    @PostMapping("/meter/replace")
66
+    public R<String> replace(@RequestBody Map<String, Object> req) {
67
+        meterService.replace(
68
+            Long.parseLong(String.valueOf(req.get("oldMeterId"))),
69
+            Long.parseLong(String.valueOf(req.get("newMeterId"))),
70
+            new BigDecimal(String.valueOf(req.get("oldReading"))),
71
+            (String) req.get("remark"));
72
+        return R.ok("换表成功");
73
+    }
74
+
75
+    @GetMapping("/meter/lifecycle/{meterId}")
76
+    public R<List<Map<String, Object>>> lifecycle(@PathVariable Long meterId) {
77
+        return R.ok(meterService.getLifecycle(meterId));
78
+    }
79
+}

+ 65
- 0
wm-revenue/src/main/java/com/water/revenue/controller/WechatController.java Wyświetl plik

@@ -0,0 +1,65 @@
1
+package com.water.revenue.controller;
2
+
3
+import com.water.common.core.result.R;
4
+import com.water.revenue.service.CustomerServiceCenter;
5
+import com.water.revenue.service.WechatService;
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.math.BigDecimal;
12
+import java.util.*;
13
+
14
+@Tag(name = "客服热线 & 微信网厅")
15
+@RestController
16
+@RequestMapping("/revenue/customer")
17
+@RequiredArgsConstructor
18
+public class WechatController {
19
+
20
+    private final CustomerServiceCenter csc;
21
+    private final WechatService wechatService;
22
+
23
+    // ---- 客服 ----
24
+    @GetMapping("/bills/query")
25
+    public R<List<Map<String, Object>>> queryBills(@RequestParam String phoneOrNo) {
26
+        return R.ok(csc.queryBills(phoneOrNo));
27
+    }
28
+
29
+    @GetMapping("/knowledge/search")
30
+    public R<List<Map<String, Object>>> searchKnowledge(@RequestParam String keyword) {
31
+        return R.ok(csc.searchKnowledge(keyword));
32
+    }
33
+
34
+    @GetMapping("/notices/{type}")
35
+    public R<List<Map<String, Object>>> notices(@PathVariable String type) {
36
+        return R.ok(csc.getNotices(type));
37
+    }
38
+
39
+    @GetMapping("/kpi")
40
+    public R<Map<String, Object>> kpi() { return R.ok(csc.getKpi()); }
41
+
42
+    // ---- 微信网厅 ----
43
+    @PostMapping("/wechat/bind")
44
+    public R<Map<String, Object>> bind(@RequestBody Map<String, String> req) {
45
+        return R.ok(wechatService.bindUser(req.get("openId"), req.get("customerNo"), req.get("phone")));
46
+    }
47
+
48
+    @PostMapping("/wechat/pay/prepay")
49
+    public R<Map<String, Object>> prepay(@RequestBody Map<String, String> req) {
50
+        return R.ok(wechatService.wechatPayPrepay(
51
+            req.get("customerNo"), req.get("billPeriod"),
52
+            new BigDecimal(req.get("amount"))));
53
+    }
54
+
55
+    @GetMapping("/wechat/ai-answer")
56
+    public R<String> aiAnswer(@RequestParam String question) {
57
+        return R.ok(wechatService.aiAnswer(question));
58
+    }
59
+
60
+    @PostMapping("/wechat/notice/publish")
61
+    public R<String> publishNotice(@RequestBody Map<String, String> req) {
62
+        wechatService.publishNotice(req.get("type"), req.get("title"), req.get("content"));
63
+        return R.ok("公告已发布");
64
+    }
65
+}

+ 96
- 0
wm-revenue/src/main/java/com/water/revenue/service/BillingService.java Wyświetl plik

@@ -0,0 +1,96 @@
1
+package com.water.revenue.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
+import org.springframework.transaction.annotation.Transactional;
8
+
9
+import java.math.BigDecimal;
10
+import java.math.RoundingMode;
11
+import java.time.LocalDate;
12
+import java.time.YearMonth;
13
+import java.time.format.DateTimeFormatter;
14
+import java.util.*;
15
+
16
+@Slf4j
17
+@Service
18
+@RequiredArgsConstructor
19
+public class BillingService {
20
+
21
+    private final JdbcTemplate jdbcTemplate;
22
+
23
+    /** 根据抄表记录生成账单 */
24
+    @Transactional
25
+    public Map<String, Object> generateBill(Long readingId) {
26
+        Map<String, Object> reading = jdbcTemplate.queryForMap(
27
+            "SELECT r.*, m.customer_id, m.caliber, c.customer_type FROM rev_reading r " +
28
+            "JOIN rev_meter m ON r.meter_id = m.id " +
29
+            "JOIN rev_customer c ON m.customer_id = c.id " +
30
+            "WHERE r.id = ?", readingId);
31
+
32
+        BigDecimal consumption = (BigDecimal) reading.get("consumption");
33
+        String customerType = (String) reading.get("customer_type");
34
+        String period = (String) reading.get("reading_period");
35
+
36
+        // 查询阶梯水价
37
+        List<Map<String, Object>> prices = jdbcTemplate.queryForList(
38
+            "SELECT * FROM rev_water_price WHERE customer_type = ? AND effective_date <= CURRENT_DATE ORDER BY tier_no",
39
+            customerType);
40
+
41
+        BigDecimal waterFee = BigDecimal.ZERO;
42
+        BigDecimal remaining = consumption;
43
+
44
+        for (Map<String, Object> p : prices) {
45
+            BigDecimal rangeEnd = p.get("range_end") != null ? (BigDecimal) p.get("range_end") : BigDecimal.valueOf(99999);
46
+            BigDecimal price = (BigDecimal) p.get("water_price");
47
+            BigDecimal tierUsage = remaining.min(rangeEnd);
48
+            waterFee = waterFee.add(tierUsage.multiply(price));
49
+            remaining = remaining.subtract(tierUsage);
50
+            if (remaining.compareTo(BigDecimal.ZERO) <= 0) break;
51
+        }
52
+
53
+        // 污水处理费(用水量的80%)
54
+        BigDecimal sewageFee = consumption.multiply(((BigDecimal) prices.get(0).getOrDefault("sewage_price", BigDecimal.ZERO)))
55
+                .multiply(BigDecimal.valueOf(0.8));
56
+
57
+        BigDecimal totalFee = waterFee.add(sewageFee);
58
+        String billNo = "BILL-" + System.currentTimeMillis();
59
+        Date dueDate = java.sql.Date.valueOf(LocalDate.now().plusDays(30));
60
+
61
+        jdbcTemplate.update(
62
+            "INSERT INTO rev_bill (bill_no, customer_id, meter_id, reading_id, bill_period, prev_reading, curr_reading, consumption, water_fee, sewage_fee, total_fee, status, due_date) " +
63
+            "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)",
64
+            billNo, reading.get("customer_id"), reading.get("meter_id"), readingId,
65
+            period, reading.get("prev_reading"), reading.get("curr_reading"),
66
+            consumption, waterFee, sewageFee, totalFee, "pending", dueDate);
67
+
68
+        log.info("Bill generated: {} total={}", billNo, totalFee);
69
+        return Map.of("billNo", billNo, "totalFee", totalFee, "waterFee", waterFee, "sewageFee", sewageFee);
70
+    }
71
+
72
+    /** 缴费 */
73
+    @Transactional
74
+    public Map<String, Object> pay(long billId, String payMethod, String payChannel, BigDecimal amount) {
75
+        String paymentNo = "PAY-" + System.currentTimeMillis();
76
+        jdbcTemplate.update(
77
+            "INSERT INTO rev_payment (bill_id, customer_id, payment_no, amount, pay_method, pay_channel) " +
78
+            "SELECT ?, customer_id, ?, ?, ?, ? FROM rev_bill WHERE id = ?",
79
+            billId, paymentNo, amount, payMethod, payChannel, billId);
80
+
81
+        jdbcTemplate.update(
82
+            "UPDATE rev_bill SET paid_fee = paid_fee + ?, status = CASE WHEN paid_fee >= total_fee THEN 'paid' ELSE 'partial' END, paid_at = NOW() WHERE id = ?",
83
+            amount, billId);
84
+
85
+        return Map.of("paymentNo", paymentNo, "amount", amount, "status", "success");
86
+    }
87
+
88
+    /** 欠费统计 */
89
+    public List<Map<String, Object>> getOverdueBills(String area) {
90
+        return jdbcTemplate.queryForList(
91
+            "SELECT b.*, c.customer_name, c.phone FROM rev_bill b " +
92
+            "JOIN rev_customer c ON b.customer_id = c.id " +
93
+            "WHERE b.status IN ('pending','partial') AND c.area = ? AND b.due_date < CURRENT_DATE",
94
+            area);
95
+    }
96
+}

+ 52
- 0
wm-revenue/src/main/java/com/water/revenue/service/CustomerServiceCenter.java Wyświetl plik

@@ -0,0 +1,52 @@
1
+package com.water.revenue.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
+@Slf4j
11
+@Service
12
+@RequiredArgsConstructor
13
+public class CustomerServiceCenter {
14
+
15
+    private final JdbcTemplate jdbcTemplate;
16
+
17
+    /** 水费查询(语音/在线) */
18
+    public List<Map<String, Object>> queryBills(String phoneOrCustomerNo) {
19
+        return jdbcTemplate.queryForList(
20
+            "SELECT b.*, c.customer_name, c.phone FROM rev_bill b " +
21
+            "JOIN rev_customer c ON b.customer_id = c.id " +
22
+            "WHERE c.phone = ? OR c.customer_no = ? ORDER BY b.bill_period DESC LIMIT 12",
23
+            phoneOrCustomerNo, phoneOrCustomerNo);
24
+    }
25
+
26
+    /** 知识库管理 */
27
+    public List<Map<String, Object>> searchKnowledge(String keyword) {
28
+        return jdbcTemplate.queryForList(
29
+            "SELECT dict_label, dict_value FROM sys_dict_data WHERE dict_type_id = " +
30
+            "(SELECT id FROM sys_dict_type WHERE dict_key = 'knowledge_base') " +
31
+            "AND dict_label LIKE ?", "%" + keyword + "%");
32
+    }
33
+
34
+    /** 公告板 */
35
+    public List<Map<String, Object>> getNotices(String noticeType) {
36
+        return jdbcTemplate.queryForList(
37
+            "SELECT dict_label, dict_value, created_at FROM sys_dict_data WHERE dict_type_id = " +
38
+            "(SELECT id FROM sys_dict_type WHERE dict_key = ?) ORDER BY created_at DESC LIMIT 10",
39
+            "notice_" + noticeType);  // notice_water_stop, notice_water_quality, etc.
40
+    }
41
+
42
+    /** KPI 指标 */
43
+    public Map<String, Object> getKpi() {
44
+        return jdbcTemplate.queryForMap("""
45
+            SELECT 
46
+                (SELECT COUNT(*) FROM rev_bill WHERE status = 'pending' AND created_at > CURRENT_DATE - 30) AS pending_bills,
47
+                (SELECT COUNT(*) FROM rev_install WHERE status IN ('pre_apply','engineering')) AS pending_installs,
48
+                (SELECT ROUND(AVG(EXTRACT(EPOCH FROM (updated_at - created_at))/3600)::numeric, 2) 
49
+                 FROM rev_install WHERE status = 'completed' AND created_at > CURRENT_DATE - 30) AS avg_install_hours
50
+            """);
51
+    }
52
+}

+ 56
- 0
wm-revenue/src/main/java/com/water/revenue/service/InstallService.java Wyświetl plik

@@ -0,0 +1,56 @@
1
+package com.water.revenue.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 InstallService {
15
+
16
+    private final JdbcTemplate jdbcTemplate;
17
+
18
+    /** 预受理申请 */
19
+    public Map<String, Object> preApply(String name, String phone, String area, String address, String customerType, String caliber) {
20
+        String appNo = "INS-" + System.currentTimeMillis();
21
+        jdbcTemplate.update(
22
+            "INSERT INTO rev_install (application_no, applicant_name, applicant_phone, area, address, customer_type, caliber, status) VALUES (?,?,?,?,?,?,?,?)",
23
+            appNo, name, phone, area, address, customerType, caliber, "pre_apply");
24
+        return Map.of("applicationNo", appNo, "status", "pre_apply");
25
+    }
26
+
27
+    /** 工程申请 */
28
+    public Map<String, Object> engineeringApply(String appNo, Map<String, Object> engData) {
29
+        jdbcTemplate.update(
30
+            "UPDATE rev_install SET status = 'engineering', updated_at = NOW() WHERE application_no = ?",
31
+            appNo);
32
+        return Map.of("applicationNo", appNo, "status", "engineering");
33
+    }
34
+
35
+    /** 派单到施工 */
36
+    public Map<String, Object> assignTask(String appNo, Long assigneeId) {
37
+        jdbcTemplate.update(
38
+            "UPDATE rev_install SET status = 'pending_review', updated_at = NOW() WHERE application_no = ?",
39
+            appNo);
40
+        return Map.of("applicationNo", appNo, "status", "pending_review", "assigneeId", assigneeId);
41
+    }
42
+
43
+    /** 查询报装进度 */
44
+    public Map<String, Object> getProgress(String appNo) {
45
+        return jdbcTemplate.queryForMap(
46
+            "SELECT application_no, applicant_name, applicant_phone, area, address, customer_type, caliber, status, created_at, updated_at FROM rev_install WHERE application_no = ?",
47
+            appNo);
48
+    }
49
+
50
+    /** 报装统计报表 */
51
+    public List<Map<String, Object>> getStatsReport(String area, LocalDate start, LocalDate end) {
52
+        return jdbcTemplate.queryForList(
53
+            "SELECT area, customer_type, status, COUNT(*) as count FROM rev_install WHERE created_at BETWEEN ? AND ? GROUP BY area, customer_type, status",
54
+            start, end);
55
+    }
56
+}

+ 74
- 0
wm-revenue/src/main/java/com/water/revenue/service/MeterService.java Wyświetl plik

@@ -0,0 +1,74 @@
1
+package com.water.revenue.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
+import org.springframework.transaction.annotation.Transactional;
8
+
9
+import java.math.BigDecimal;
10
+import java.util.*;
11
+
12
+@Slf4j
13
+@Service
14
+@RequiredArgsConstructor
15
+public class MeterService {
16
+
17
+    private final JdbcTemplate jdbcTemplate;
18
+
19
+    /** 水表入库 */
20
+    public void stockIn(String meterNo, String caliber, String meterType, String manufacturer, int quantity) {
21
+        for (int i = 0; i < quantity; i++) {
22
+            String no = meterNo + "-" + (i + 1);
23
+            jdbcTemplate.update(
24
+                "INSERT INTO rev_meter (meter_no, caliber, meter_type, manufacturer, status) VALUES (?,?,?,?,?)",
25
+                no, caliber, meterType, manufacturer, "warehouse");
26
+        }
27
+        log.info("Meter stock in: {} x{}", meterNo, quantity);
28
+    }
29
+
30
+    /** 水表出库安装 */
31
+    @Transactional
32
+    public void install(Long meterId, Long customerId, String address, BigDecimal initialReading) {
33
+        jdbcTemplate.update(
34
+            "UPDATE rev_meter SET customer_id = ?, install_address = ?, initial_reading = ?, current_reading = ?, status = 'active', install_date = CURRENT_DATE WHERE id = ?",
35
+            customerId, address, initialReading, initialReading, meterId);
36
+        jdbcTemplate.update(
37
+            "INSERT INTO rev_meter_log (meter_id, operation_type, old_reading, new_reading, remark) VALUES (?,?,?,?,?)",
38
+            meterId, "install", BigDecimal.ZERO, initialReading, "新表安装");
39
+    }
40
+
41
+    /** 故障换表 */
42
+    @Transactional
43
+    public void replace(Long oldMeterId, Long newMeterId, BigDecimal oldReading, String remark) {
44
+        // 旧表拆除
45
+        jdbcTemplate.update("UPDATE rev_meter SET status = 'dismantled', current_reading = ? WHERE id = ?", oldReading, oldMeterId);
46
+        jdbcTemplate.update(
47
+            "INSERT INTO rev_meter_log (meter_id, operation_type, old_reading, remark) VALUES (?,?,?,?)",
48
+            oldMeterId, "dismantle", oldReading, remark);
49
+
50
+        // 新表安装(继承客户信息)
51
+        Map<String, Object> oldMeter = jdbcTemplate.queryForMap("SELECT customer_id, install_address FROM rev_meter WHERE id = ?", oldMeterId);
52
+        jdbcTemplate.update(
53
+            "UPDATE rev_meter SET customer_id = ?, install_address = ?, status = 'active', install_date = CURRENT_DATE WHERE id = ?",
54
+            oldMeter.get("customer_id"), oldMeter.get("install_address"), newMeterId);
55
+        jdbcTemplate.update(
56
+            "INSERT INTO rev_meter_log (meter_id, operation_type, new_meter_no, remark) VALUES (?,?,?,?)",
57
+            newMeterId, "change", String.valueOf(newMeterId), "替换旧表 #" + oldMeterId);
58
+    }
59
+
60
+    /** 水表报废 */
61
+    public void scrap(Long meterId, String reason) {
62
+        jdbcTemplate.update("UPDATE rev_meter SET status = 'scrapped' WHERE id = ?", meterId);
63
+        jdbcTemplate.update(
64
+            "INSERT INTO rev_meter_log (meter_id, operation_type, remark) VALUES (?,?,?)",
65
+            meterId, "scrap", reason);
66
+    }
67
+
68
+    /** 查询水表生命周期记录 */
69
+    public List<Map<String, Object>> getLifecycle(Long meterId) {
70
+        return jdbcTemplate.queryForList(
71
+            "SELECT * FROM rev_meter_log WHERE meter_id = ? ORDER BY created_at",
72
+            meterId);
73
+    }
74
+}

+ 78
- 0
wm-revenue/src/main/java/com/water/revenue/service/RemoteReadingService.java Wyświetl plik

@@ -0,0 +1,78 @@
1
+package com.water.revenue.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.math.BigDecimal;
9
+import java.math.RoundingMode;
10
+import java.util.*;
11
+
12
+@Slf4j
13
+@Service
14
+@RequiredArgsConstructor
15
+public class RemoteReadingService {
16
+
17
+    private final JdbcTemplate jdbcTemplate;
18
+
19
+    /** 远传集抄:批量采集 */
20
+    public Map<String, Object> batchRead(String area) {
21
+        List<Map<String, Object>> meters = jdbcTemplate.queryForList(
22
+            "SELECT rm.id, rm.meter_no, rm.current_reading, i.device_sn " +
23
+            "FROM rev_meter rm LEFT JOIN iot_device i ON rm.device_id = i.id " +
24
+            "JOIN rev_customer c ON rm.customer_id = c.id " +
25
+            "WHERE rm.status = 'active' AND c.area = ?", area);
26
+
27
+        int success = 0, failed = 0;
28
+        String period = java.time.YearMonth.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM"));
29
+
30
+        for (Map<String, Object> m : meters) {
31
+            try {
32
+                String deviceSn = (String) m.get("device_sn");
33
+                // 从 IoT 平台获取实时读数(mock: 随机增量)
34
+                BigDecimal prev = m.get("current_reading") != null ? (BigDecimal) m.get("current_reading") : BigDecimal.ZERO;
35
+                BigDecimal curr = prev.add(BigDecimal.valueOf(new Random().nextDouble() * 50));
36
+                BigDecimal consumption = curr.subtract(prev);
37
+                if (consumption.compareTo(BigDecimal.ZERO) < 0) consumption = BigDecimal.ZERO;
38
+
39
+                jdbcTemplate.update(
40
+                    "INSERT INTO rev_reading (meter_id, reading_date, reading_period, prev_reading, curr_reading, consumption, read_type) " +
41
+                    "VALUES (?, CURRENT_DATE, ?, ?, ?, ?, 'remote')",
42
+                    m.get("id"), period, prev, curr, consumption);
43
+                jdbcTemplate.update("UPDATE rev_meter SET current_reading = ? WHERE id = ?", curr, m.get("id"));
44
+                success++;
45
+            } catch (Exception e) {
46
+                failed++;
47
+                log.warn("Read failed for meter {}: {}", m.get("meter_no"), e.getMessage());
48
+            }
49
+        }
50
+        log.info("Batch read: area={} success={} failed={}", area, success, failed);
51
+        return Map.of("area", area, "success", success, "failed", failed, "period", period);
52
+    }
53
+
54
+    /** DMA 分区漏损分析 */
55
+    public Map<String, Object> dmaAnalysis(String area, String dateStr) {
56
+        List<Map<String, Object>> result = jdbcTemplate.queryForList(
57
+            "SELECT area, SUM(consumption) as total_consumption, COUNT(DISTINCT rm.id) as meter_count " +
58
+            "FROM rev_reading rr JOIN rev_meter rm ON rr.meter_id = rm.id " +
59
+            "JOIN rev_customer c ON rm.customer_id = c.id " +
60
+            "WHERE c.area = ? AND rr.reading_period = ? GROUP BY area",
61
+            area, dateStr);
62
+
63
+        // 漏损率 = 1 - (售水量/供水量)
64
+        Map<String, Object> dma = new HashMap<>(result.isEmpty() ? Map.of() : result.get(0));
65
+        dma.put("supplyEstimate", 1000);  // TODO: 从水厂出水量获取
66
+        dma.put("leakRate", "分析中");
67
+        return dma;
68
+    }
69
+
70
+    /** 大表监控 (DN80+) */
71
+    public List<Map<String, Object>> largeMeterMonitor() {
72
+        return jdbcTemplate.queryForList(
73
+            "SELECT rm.*, c.customer_name, c.area, i.device_sn, i.status as device_status " +
74
+            "FROM rev_meter rm JOIN rev_customer c ON rm.customer_id = c.id " +
75
+            "LEFT JOIN iot_device i ON rm.device_id = i.id " +
76
+            "WHERE rm.caliber IN ('DN80','DN100','DN150','DN200','DN300','DN400') AND rm.status = 'active'");
77
+    }
78
+}

+ 49
- 0
wm-revenue/src/main/java/com/water/revenue/service/RevenueBaseService.java Wyświetl plik

@@ -0,0 +1,49 @@
1
+package com.water.revenue.service;
2
+
3
+import cn.dev33.satoken.secure.BCrypt;
4
+import lombok.RequiredArgsConstructor;
5
+import lombok.extern.slf4j.Slf4j;
6
+import org.springframework.jdbc.core.JdbcTemplate;
7
+import org.springframework.stereotype.Service;
8
+
9
+import java.util.*;
10
+
11
+@Slf4j
12
+@Service
13
+@RequiredArgsConstructor
14
+public class RevenueBaseService {
15
+
16
+    private final JdbcTemplate jdbcTemplate;
17
+
18
+    // ========== SSO 单点登录 ==========
19
+    public Map<String, Object> ssoLogin(String username, String password, String appType) {
20
+        // 从统一用户表验证
21
+        Map<String, Object> user = jdbcTemplate.queryForMap(
22
+            "SELECT id, username, real_name, phone, status FROM sys_user WHERE username = ? AND status = 1",
23
+            username);
24
+        if (user == null) throw new RuntimeException("用户不存在");
25
+        // 生成 SSO Token
26
+        String ssoToken = UUID.randomUUID().toString();
27
+        jdbcTemplate.update(
28
+            "INSERT INTO sys_oper_log (user_id, username, module, operation, request_url) VALUES (?,?,?,?,?)",
29
+            user.get("id"), username, "revenue", "sso_login", "/revenue/auth/sso");
30
+        user.put("ssoToken", ssoToken);
31
+        user.put("appType", appType);
32
+        return user;
33
+    }
34
+
35
+    // ========== 应用接入管理 ==========
36
+    public void registerApp(String appName, String appKey, String appSecret, String redirectUri) {
37
+        jdbcTemplate.update(
38
+            "INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value) " +
39
+            "SELECT id, ?, ? FROM sys_dict_type WHERE dict_key = 'app_config'",
40
+            appName, appKey + ":" + appSecret + ":" + redirectUri);
41
+    }
42
+
43
+    // ========== 运维审计 ==========
44
+    public void auditLog(Long userId, String action, String target, String detail) {
45
+        jdbcTemplate.update(
46
+            "INSERT INTO sys_oper_log (user_id, module, operation, request_url, request_params) VALUES (?,?,?,?,?)",
47
+            userId, "revenue_audit", action, target, detail);
48
+    }
49
+}

+ 68
- 0
wm-revenue/src/main/java/com/water/revenue/service/WechatService.java Wyświetl plik

@@ -0,0 +1,68 @@
1
+package com.water.revenue.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.math.BigDecimal;
9
+import java.util.*;
10
+
11
+@Slf4j
12
+@Service
13
+@RequiredArgsConstructor
14
+public class WechatService {
15
+
16
+    private final JdbcTemplate jdbcTemplate;
17
+
18
+    /** 微信用户绑定 */
19
+    public Map<String, Object> bindUser(String wechatOpenId, String customerNo, String phone) {
20
+        jdbcTemplate.update(
21
+            "UPDATE rev_customer SET phone = COALESCE(phone, ?) WHERE customer_no = ?",
22
+            phone, customerNo);
23
+        // 存储微信绑定关系
24
+        jdbcTemplate.update(
25
+            "INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value) " +
26
+            "SELECT id, ?, ? FROM sys_dict_type WHERE dict_key = 'wechat_bind' " +
27
+            "ON CONFLICT DO NOTHING",
28
+            wechatOpenId, customerNo);
29
+        return Map.of("openId", wechatOpenId, "customerNo", customerNo, "status", "bound");
30
+    }
31
+
32
+    /** 在线缴费(微信支付预下单) */
33
+    public Map<String, Object> wechatPayPrepay(String customerNo, String billPeriod, BigDecimal amount) {
34
+        String orderNo = "WXPAY-" + System.currentTimeMillis();
35
+        Map<String, Object> bill = jdbcTemplate.queryForMap(
36
+            "SELECT id, total_fee FROM rev_bill WHERE customer_id = " +
37
+            "(SELECT id FROM rev_customer WHERE customer_no = ?) AND bill_period = ? AND status IN ('pending','partial')",
38
+            customerNo, billPeriod);
39
+        // 微信支付统一下单(mock)
40
+        log.info("WeChat Pay prepay: order={} bill={} amount={}", orderNo, bill.get("id"), amount);
41
+        return Map.of("orderNo", orderNo, "prepayId", "wx" + orderNo, "amount", amount);
42
+    }
43
+
44
+    /** AI 客服问答 */
45
+    public String aiAnswer(String question) {
46
+        // 简易关键词匹配
47
+        Map<String, String> qa = new LinkedHashMap<>();
48
+        qa.put("水费", "您可以发送户号查询水费账单,或通过在线缴费功能直接支付。");
49
+        qa.put("停水", "请查看停水公告了解最新停水计划。如有紧急停水,请拨打客服热线。");
50
+        qa.put("报装", "新装水表可通过网上营业厅-业务办理-报装申请提交,我们会安排现场踏勘。");
51
+        qa.put("水质", "水质报告每月更新,详见水质公告。如发现水质异常请立即联系我们。");
52
+        qa.put("发票", "缴费后可在电子发票中申请开具电子发票,发送到您的微信或邮箱。");
53
+        qa.put("过户", "房屋买卖后请携带房产证和身份证前往营业厅办理过户手续。");
54
+
55
+        for (Map.Entry<String, String> e : qa.entrySet()) {
56
+            if (question.contains(e.getKey())) return e.getValue();
57
+        }
58
+        return "您好!我是智慧水务AI客服,您可以问我关于水费、报装、停水、水质、发票等问题。如需人工服务请转接客服热线。";
59
+    }
60
+
61
+    /** 后台公告管理 */
62
+    public void publishNotice(String type, String title, String content) {
63
+        jdbcTemplate.update(
64
+            "INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value) " +
65
+            "SELECT id, ?, ? FROM sys_dict_type WHERE dict_key = ?",
66
+            title, content, "notice_" + type);
67
+    }
68
+}

+ 49
- 0
wm-revenue/src/main/java/com/water/revenue/service/WorkOrderService.java Wyświetl plik

@@ -0,0 +1,49 @@
1
+package com.water.revenue.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
+@Slf4j
11
+@Service
12
+@RequiredArgsConstructor
13
+public class WorkOrderService {
14
+
15
+    private final JdbcTemplate jdbcTemplate;
16
+
17
+    /** 创建工单 */
18
+    public Map<String, Object> create(String title, String type, String priority, Long reporterId,
19
+                                       String reporterName, String description, String area) {
20
+        String woNo = "WO-" + System.currentTimeMillis();
21
+        jdbcTemplate.update(
22
+            "INSERT INTO patrol_task (task_name, task_date, status) VALUES (?, CURRENT_DATE, 'pending')",
23
+            title + "[" + woNo + "]");
24
+        log.info("WorkOrder created: {} type={} priority={}", woNo, type, priority);
25
+        return Map.of("woNo", woNo, "title", title, "status", "pending", "area", area);
26
+    }
27
+
28
+    /** 工单分派 */
29
+    public Map<String, Object> assign(String woNo, Long assigneeId, String assigneeName) {
30
+        jdbcTemplate.update(
31
+            "UPDATE patrol_task SET assignee_id = ?, status = 'in_progress' WHERE task_name LIKE ?",
32
+            assigneeId, "%" + woNo + "%");
33
+        return Map.of("woNo", woNo, "assigneeId", assigneeId, "status", "assigned");
34
+    }
35
+
36
+    /** 工单处理完成 */
37
+    public Map<String, Object> complete(String woNo, String result, List<String> photoUrls) {
38
+        jdbcTemplate.update(
39
+            "UPDATE patrol_task SET status = 'completed', actual_end = NOW() WHERE task_name LIKE ?",
40
+            "%" + woNo + "%");
41
+        return Map.of("woNo", woNo, "status", "completed", "photos", photoUrls);
42
+    }
43
+
44
+    /** 工单统计 */
45
+    public Map<String, Object> stats(String area) {
46
+        return jdbcTemplate.queryForMap(
47
+            "SELECT status, COUNT(*) as count FROM patrol_task WHERE task_date >= CURRENT_DATE - 30 GROUP BY status");
48
+    }
49
+}