Sfoglia il codice sorgente

feat(wm-revenue): #84 用户管理+业务参数设置(用户档案CRUD+水价阶梯+收费配置)

- CustomerArchiveService: 用户档案CRUD/分页查询/销户/统计(JdbcTemplate风格)
- BillingConfigService: 阶梯水价查询更新/收费配置/污水处理费率/全量配置
- CustomerArchiveController: 6个端点 (/revenue/customer/*)
- BillingConfigController: 6个端点 (/revenue/config/*)
- DDL: rev_billing_config/rev_customer/rev_water_price + 默认数据
bot_dev2 4 giorni fa
parent
commit
258e88f69e

+ 36
- 0
wm-revenue/src/main/java/com/water/revenue/controller/BillingConfigController.java Vedi File

@@ -0,0 +1,36 @@
1
+package com.water.revenue.controller;
2
+
3
+import com.water.common.core.result.R;
4
+import com.water.revenue.service.BillingConfigService;
5
+import io.swagger.v3.oas.annotations.Operation;
6
+import io.swagger.v3.oas.annotations.Parameter;
7
+import io.swagger.v3.oas.annotations.tags.Tag;
8
+import lombok.RequiredArgsConstructor;
9
+import org.springframework.web.bind.annotation.*;
10
+import java.util.List;
11
+import java.util.Map;
12
+
13
+@Tag(name = "业务参数设置")
14
+@RestController
15
+@RequestMapping("/revenue/config")
16
+@RequiredArgsConstructor
17
+public class BillingConfigController {
18
+    private final BillingConfigService svc;
19
+
20
+    @Operation(summary = "查询阶梯水价") @GetMapping("/water-price/{customerType}")
21
+    public R<List<Map<String, Object>>> getWaterPrice(@Parameter(description = "residential/commercial/industrial") @PathVariable String customerType) {
22
+        return R.ok(svc.getWaterPrice(customerType));
23
+    }
24
+    @Operation(summary = "更新水价") @PutMapping("/water-price/{customerType}")
25
+    public R<String> updateWaterPrice(@PathVariable String customerType, @RequestBody List<Map<String, Object>> tiers) {
26
+        svc.updateWaterPrice(customerType, tiers); return R.ok("OK");
27
+    }
28
+    @Operation(summary = "查询收费配置") @GetMapping("/billing")
29
+    public R<Map<String, Object>> getBillingConfig() { return R.ok(svc.getBillingConfig()); }
30
+    @Operation(summary = "更新收费配置") @PutMapping("/billing")
31
+    public R<String> updateBillingConfig(@RequestBody Map<String, String> config) { svc.updateBillingConfig(config); return R.ok("OK"); }
32
+    @Operation(summary = "污水处理费率") @GetMapping("/sewage-rate")
33
+    public R<Map<String, Object>> getSewageRate() { return R.ok(svc.getSewageRate()); }
34
+    @Operation(summary = "所有业务参数") @GetMapping("/all")
35
+    public R<List<Map<String, Object>>> listAllConfigs() { return R.ok(svc.listAllConfigs()); }
36
+}

+ 36
- 0
wm-revenue/src/main/java/com/water/revenue/controller/CustomerArchiveController.java Vedi File

@@ -0,0 +1,36 @@
1
+package com.water.revenue.controller;
2
+
3
+import com.water.common.core.result.R;
4
+import com.water.revenue.service.CustomerArchiveService;
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
+import java.util.Map;
10
+
11
+@Tag(name = "用户档案管理")
12
+@RestController
13
+@RequestMapping("/revenue/customer")
14
+@RequiredArgsConstructor
15
+public class CustomerArchiveController {
16
+    private final CustomerArchiveService svc;
17
+
18
+    @Operation(summary = "创建用户档案") @PostMapping
19
+    public R<Map<String, Object>> create(@RequestBody Map<String, String> req) {
20
+        return R.ok(svc.create(req.get("name"), req.get("type"), req.get("area"), req.get("address"), req.get("phone"), req.get("idCard")));
21
+    }
22
+    @Operation(summary = "更新用户") @PutMapping("/{id}")
23
+    public R<String> update(@PathVariable Long id, @RequestBody Map<String, Object> fields) { svc.update(id, fields); return R.ok("OK"); }
24
+    @Operation(summary = "用户详情") @GetMapping("/{id}")
25
+    public R<Map<String, Object>> getById(@PathVariable Long id) { return R.ok(svc.getById(id)); }
26
+    @Operation(summary = "分页列表") @GetMapping("/list")
27
+    public R<Map<String, Object>> list(@RequestParam(required = false) String type, @RequestParam(required = false) String area,
28
+            @RequestParam(required = false) String status, @RequestParam(required = false) String keyword,
29
+            @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "20") int size) {
30
+        return R.ok(svc.list(type, area, status, keyword, page, size));
31
+    }
32
+    @Operation(summary = "销户") @PutMapping("/{id}/cancel")
33
+    public R<String> cancel(@PathVariable Long id) { svc.cancel(id); return R.ok("OK"); }
34
+    @Operation(summary = "统计") @GetMapping("/stats")
35
+    public R<Map<String, Object>> stats(@RequestParam(required = false) String area) { return R.ok(svc.stats(area)); }
36
+}

+ 59
- 0
wm-revenue/src/main/java/com/water/revenue/service/BillingConfigService.java Vedi File

@@ -0,0 +1,59 @@
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
+import java.math.BigDecimal;
9
+import java.time.LocalDateTime;
10
+import java.util.*;
11
+
12
+@Slf4j @Service @RequiredArgsConstructor
13
+public class BillingConfigService {
14
+    private final JdbcTemplate jdbc;
15
+
16
+    public List<Map<String, Object>> getWaterPrice(String customerType) {
17
+        return jdbc.queryForList("SELECT * FROM rev_water_price WHERE customer_type = ? ORDER BY tier_level", customerType);
18
+    }
19
+
20
+    @Transactional
21
+    public void updateWaterPrice(String ct, List<Map<String, Object>> tiers) {
22
+        jdbc.update("DELETE FROM rev_water_price WHERE customer_type = ?", ct);
23
+        for (Map<String, Object> t : tiers) {
24
+            jdbc.update("INSERT INTO rev_water_price (customer_type, tier_level, min_usage, max_usage, price, sewage_fee, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)",
25
+                ct, t.get("tierLevel"), t.get("minUsage"), t.get("maxUsage"),
26
+                new BigDecimal(t.get("price").toString()),
27
+                t.containsKey("sewageFee") ? new BigDecimal(t.get("sewageFee").toString()) : new BigDecimal("0.85"),
28
+                LocalDateTime.now(), LocalDateTime.now());
29
+        }
30
+    }
31
+
32
+    public Map<String, Object> getBillingConfig() {
33
+        Map<String, Object> r = new LinkedHashMap<>();
34
+        jdbc.queryForList("SELECT * FROM rev_billing_config ORDER BY config_key").forEach(c -> r.put(c.get("config_key").toString(), c.get("config_value")));
35
+        return r;
36
+    }
37
+
38
+    @Transactional
39
+    public void updateBillingConfig(Map<String, String> updates) {
40
+        for (var e : updates.entrySet()) {
41
+            int rows = jdbc.update("UPDATE rev_billing_config SET config_value = ?, updated_at = ? WHERE config_key = ?", e.getValue(), LocalDateTime.now(), e.getKey());
42
+            if (rows == 0) jdbc.update("INSERT INTO rev_billing_config (config_key, config_value, config_type, description, updated_at) VALUES (?,?,'string','自定义',?)", e.getKey(), e.getValue(), LocalDateTime.now());
43
+        }
44
+    }
45
+
46
+    public Map<String, Object> getSewageRate() {
47
+        Map<String, Object> r = new LinkedHashMap<>();
48
+        jdbc.queryForList("SELECT customer_type, sewage_fee FROM rev_water_price GROUP BY customer_type, sewage_fee ORDER BY customer_type")
49
+            .forEach(row -> r.put(row.get("customer_type").toString(), row.get("sewage_fee")));
50
+        return r;
51
+    }
52
+
53
+    public List<Map<String, Object>> listAllConfigs() {
54
+        List<Map<String, Object>> all = new ArrayList<>();
55
+        Map<String, Object> s1 = new LinkedHashMap<>(); s1.put("category", "收费配置"); s1.put("items", jdbc.queryForList("SELECT * FROM rev_billing_config ORDER BY config_key")); all.add(s1);
56
+        Map<String, Object> s2 = new LinkedHashMap<>(); s2.put("category", "水价配置"); s2.put("items", jdbc.queryForList("SELECT * FROM rev_water_price ORDER BY customer_type, tier_level")); all.add(s2);
57
+        return all;
58
+    }
59
+}

+ 89
- 0
wm-revenue/src/main/java/com/water/revenue/service/CustomerArchiveService.java Vedi File

@@ -0,0 +1,89 @@
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.time.LocalDateTime;
10
+import java.util.*;
11
+
12
+@Slf4j
13
+@Service
14
+@RequiredArgsConstructor
15
+public class CustomerArchiveService {
16
+
17
+    private final JdbcTemplate jdbcTemplate;
18
+
19
+    @Transactional
20
+    public Map<String, Object> create(String name, String type, String area, String address,
21
+                                       String phone, String idCard) {
22
+        String customerNo = "CUS-" + System.currentTimeMillis();
23
+        LocalDateTime now = LocalDateTime.now();
24
+        jdbcTemplate.update(
25
+            "INSERT INTO rev_customer (customer_no, name, type, area, address, phone, id_card, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, 'active', ?, ?)",
26
+            customerNo, name, type, area, address, phone, idCard, now, now);
27
+        Long id = jdbcTemplate.queryForObject("SELECT id FROM rev_customer WHERE customer_no = ?", Long.class, customerNo);
28
+        log.info("创建用户档案: customerNo={}", customerNo);
29
+        Map<String, Object> r = new LinkedHashMap<>();
30
+        r.put("id", id); r.put("customerNo", customerNo); r.put("name", name);
31
+        r.put("type", type); r.put("area", area); r.put("address", address);
32
+        r.put("phone", phone); r.put("status", "active"); r.put("createdAt", now);
33
+        return r;
34
+    }
35
+
36
+    @Transactional
37
+    public void update(Long id, Map<String, Object> fields) {
38
+        List<String> set = new ArrayList<>(); List<Object> params = new ArrayList<>();
39
+        for (String f : new String[]{"name","type","area","address","phone","id_card","status"}) {
40
+            if (fields.containsKey(f)) { set.add(f + " = ?"); params.add(fields.get(f)); }
41
+        }
42
+        if (set.isEmpty()) throw new RuntimeException("没有可更新的字段");
43
+        set.add("updated_at = ?"); params.add(LocalDateTime.now()); params.add(id);
44
+        jdbcTemplate.update("UPDATE rev_customer SET " + String.join(", ", set) + " WHERE id = ?", params.toArray());
45
+    }
46
+
47
+    public Map<String, Object> getById(Long id) {
48
+        Map<String, Object> c = jdbcTemplate.queryForMap("SELECT * FROM rev_customer WHERE id = ?", id);
49
+        c.put("meters", jdbcTemplate.queryForList("SELECT * FROM rev_water_meter WHERE customer_no = ? ORDER BY install_date DESC", c.get("customer_no")));
50
+        c.put("recentBills", jdbcTemplate.queryForList("SELECT * FROM rev_bill WHERE customer_no = ? ORDER BY bill_period DESC LIMIT 5", c.get("customer_no")));
51
+        return c;
52
+    }
53
+
54
+    public Map<String, Object> list(String type, String area, String status, String keyword, int page, int size) {
55
+        StringBuilder w = new StringBuilder("WHERE 1=1"); List<Object> p = new ArrayList<>();
56
+        if (type != null && !type.isEmpty()) { w.append(" AND type = ?"); p.add(type); }
57
+        if (area != null && !area.isEmpty()) { w.append(" AND area = ?"); p.add(area); }
58
+        if (status != null && !status.isEmpty()) { w.append(" AND status = ?"); p.add(status); }
59
+        if (keyword != null && !keyword.isEmpty()) {
60
+            w.append(" AND (name LIKE ? OR customer_no LIKE ? OR phone LIKE ?)");
61
+            String kw = "%" + keyword + "%"; p.add(kw); p.add(kw); p.add(kw);
62
+        }
63
+        Long total = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM rev_customer " + w, Long.class, p.toArray());
64
+        List<Object> qp = new ArrayList<>(p); qp.add(size); qp.add((page - 1) * size);
65
+        List<Map<String, Object>> records = jdbcTemplate.queryForList(
66
+            "SELECT * FROM rev_customer " + w + " ORDER BY created_at DESC LIMIT ? OFFSET ?", qp.toArray());
67
+        Map<String, Object> r = new LinkedHashMap<>();
68
+        r.put("records", records); r.put("total", total); r.put("page", page);
69
+        r.put("size", size); r.put("pages", (int) Math.ceil((double) total / size));
70
+        return r;
71
+    }
72
+
73
+    @Transactional
74
+    public void cancel(Long id) {
75
+        int rows = jdbcTemplate.update("UPDATE rev_customer SET status = 'cancelled', updated_at = ? WHERE id = ? AND status = 'active'", LocalDateTime.now(), id);
76
+        if (rows == 0) throw new RuntimeException("用户不存在或已销户: " + id);
77
+    }
78
+
79
+    public Map<String, Object> stats(String area) {
80
+        StringBuilder w = new StringBuilder("WHERE 1=1"); List<Object> p = new ArrayList<>();
81
+        if (area != null && !area.isEmpty()) { w.append(" AND area = ?"); p.add(area); }
82
+        Map<String, Object> r = new LinkedHashMap<>();
83
+        r.put("total", jdbcTemplate.queryForObject("SELECT COUNT(*) FROM rev_customer " + w, Long.class, p.toArray()));
84
+        r.put("byType", jdbcTemplate.queryForList("SELECT type, COUNT(*) as count FROM rev_customer " + w + " GROUP BY type", p.toArray()));
85
+        r.put("byArea", jdbcTemplate.queryForList("SELECT area, COUNT(*) as count FROM rev_customer " + w + " GROUP BY area", p.toArray()));
86
+        r.put("byStatus", jdbcTemplate.queryForList("SELECT status, COUNT(*) as count FROM rev_customer " + w + " GROUP BY status", p.toArray()));
87
+        return r;
88
+    }
89
+}

+ 34
- 0
wm-revenue/src/main/resources/sql/V84__customer_billing_config.sql Vedi File

@@ -0,0 +1,34 @@
1
+-- V84: 用户管理 + 业务参数设置
2
+
3
+CREATE TABLE IF NOT EXISTS rev_billing_config (
4
+    id BIGSERIAL PRIMARY KEY, config_key VARCHAR(50) UNIQUE NOT NULL, config_value VARCHAR(200) NOT NULL,
5
+    config_type VARCHAR(20) DEFAULT 'string', description VARCHAR(200), updated_at TIMESTAMPTZ DEFAULT NOW()
6
+);
7
+INSERT INTO rev_billing_config (config_key, config_value, config_type, description) VALUES
8
+    ('billing_cycle_days','30','int','收费周期(天)'), ('overdue_penalty_rate','0.001','decimal','滞纳金日费率'),
9
+    ('min_consumption','5','decimal','最低消费水量(m³)'), ('reading_day_of_month','1','int','每月抄表日'),
10
+    ('bill_due_days','15','int','账单到期天数'), ('invoice_auto_issue','true','boolean','自动开具电子发票')
11
+ON CONFLICT (config_key) DO NOTHING;
12
+
13
+CREATE TABLE IF NOT EXISTS rev_customer (
14
+    id BIGSERIAL PRIMARY KEY, customer_no VARCHAR(32) UNIQUE NOT NULL, name VARCHAR(100) NOT NULL,
15
+    type VARCHAR(20) NOT NULL DEFAULT 'residential', area VARCHAR(50), address VARCHAR(200),
16
+    phone VARCHAR(20), id_card VARCHAR(20), status VARCHAR(20) NOT NULL DEFAULT 'active',
17
+    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
18
+);
19
+CREATE INDEX IF NOT EXISTS idx_rev_customer_type ON rev_customer(type);
20
+CREATE INDEX IF NOT EXISTS idx_rev_customer_area ON rev_customer(area);
21
+CREATE INDEX IF NOT EXISTS idx_rev_customer_status ON rev_customer(status);
22
+
23
+CREATE TABLE IF NOT EXISTS rev_water_price (
24
+    id BIGSERIAL PRIMARY KEY, customer_type VARCHAR(20) NOT NULL, tier_level INTEGER NOT NULL,
25
+    min_usage NUMERIC(10,2), max_usage NUMERIC(10,2), price NUMERIC(10,4) NOT NULL,
26
+    sewage_fee NUMERIC(10,4) DEFAULT 0.85, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
27
+    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
28
+);
29
+CREATE INDEX IF NOT EXISTS idx_water_price_type ON rev_water_price(customer_type);
30
+INSERT INTO rev_water_price (customer_type, tier_level, min_usage, max_usage, price, sewage_fee) VALUES
31
+    ('residential',1,0,15,3.50,0.85), ('residential',2,15,30,5.20,0.85), ('residential',3,30,NULL,8.00,0.85),
32
+    ('commercial',1,0,50,5.80,1.20), ('commercial',2,50,200,7.50,1.20), ('commercial',3,200,NULL,10.00,1.20),
33
+    ('industrial',1,0,100,4.50,1.50), ('industrial',2,100,500,6.80,1.50), ('industrial',3,500,NULL,9.50,1.50)
34
+ON CONFLICT DO NOTHING;