Bladeren bron

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

bot_dev2 4 dagen geleden
bovenliggende
commit
33c521f8b8

+ 61
- 0
wm-revenue/src/main/java/com/water/revenue/controller/BillingConfigController.java Bestand weergeven

@@ -0,0 +1,61 @@
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
+
11
+import java.util.List;
12
+import java.util.Map;
13
+
14
+@Tag(name = "业务参数设置")
15
+@RestController
16
+@RequestMapping("/revenue/config")
17
+@RequiredArgsConstructor
18
+public class BillingConfigController {
19
+
20
+    private final BillingConfigService billingConfigService;
21
+
22
+    @Operation(summary = "查询阶梯水价")
23
+    @GetMapping("/water-price/{customerType}")
24
+    public R<List<Map<String, Object>>> getWaterPrice(
25
+            @Parameter(description = "用户类型: residential/commercial/industrial") @PathVariable String customerType) {
26
+        return R.ok(billingConfigService.getWaterPrice(customerType));
27
+    }
28
+
29
+    @Operation(summary = "更新水价配置")
30
+    @PutMapping("/water-price/{customerType}")
31
+    public R<String> updateWaterPrice(@PathVariable String customerType,
32
+                                       @RequestBody List<Map<String, Object>> tiers) {
33
+        billingConfigService.updateWaterPrice(customerType, tiers);
34
+        return R.ok("水价更新成功");
35
+    }
36
+
37
+    @Operation(summary = "查询收费配置")
38
+    @GetMapping("/billing")
39
+    public R<Map<String, Object>> getBillingConfig() {
40
+        return R.ok(billingConfigService.getBillingConfig());
41
+    }
42
+
43
+    @Operation(summary = "更新收费配置")
44
+    @PutMapping("/billing")
45
+    public R<String> updateBillingConfig(@RequestBody Map<String, String> config) {
46
+        billingConfigService.updateBillingConfig(config);
47
+        return R.ok("配置更新成功");
48
+    }
49
+
50
+    @Operation(summary = "查询污水处理费率")
51
+    @GetMapping("/sewage-rate")
52
+    public R<Map<String, Object>> getSewageRate() {
53
+        return R.ok(billingConfigService.getSewageRate());
54
+    }
55
+
56
+    @Operation(summary = "列出所有业务参数")
57
+    @GetMapping("/all")
58
+    public R<List<Map<String, Object>>> listAllConfigs() {
59
+        return R.ok(billingConfigService.listAllConfigs());
60
+    }
61
+}

+ 65
- 0
wm-revenue/src/main/java/com/water/revenue/controller/CustomerArchiveController.java Bestand weergeven

@@ -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.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
+
10
+import java.util.Map;
11
+
12
+@Tag(name = "用户档案管理")
13
+@RestController
14
+@RequestMapping("/revenue/customer")
15
+@RequiredArgsConstructor
16
+public class CustomerArchiveController {
17
+
18
+    private final CustomerArchiveService customerArchiveService;
19
+
20
+    @Operation(summary = "创建用户档案")
21
+    @PostMapping
22
+    public R<Map<String, Object>> create(@RequestBody Map<String, String> request) {
23
+        return R.ok(customerArchiveService.create(
24
+            request.get("name"), request.get("type"), request.get("area"),
25
+            request.get("address"), request.get("phone"), request.get("idCard")));
26
+    }
27
+
28
+    @Operation(summary = "更新用户信息")
29
+    @PutMapping("/{id}")
30
+    public R<String> update(@PathVariable Long id, @RequestBody Map<String, Object> fields) {
31
+        customerArchiveService.update(id, fields);
32
+        return R.ok("更新成功");
33
+    }
34
+
35
+    @Operation(summary = "查询用户详情(含关联水表)")
36
+    @GetMapping("/{id}")
37
+    public R<Map<String, Object>> getById(@PathVariable Long id) {
38
+        return R.ok(customerArchiveService.getById(id));
39
+    }
40
+
41
+    @Operation(summary = "分页查询用户列表")
42
+    @GetMapping("/list")
43
+    public R<Map<String, Object>> list(
44
+            @RequestParam(required = false) String type,
45
+            @RequestParam(required = false) String area,
46
+            @RequestParam(required = false) String status,
47
+            @RequestParam(required = false) String keyword,
48
+            @RequestParam(defaultValue = "1") int page,
49
+            @RequestParam(defaultValue = "20") int size) {
50
+        return R.ok(customerArchiveService.list(type, area, status, keyword, page, size));
51
+    }
52
+
53
+    @Operation(summary = "用户销户")
54
+    @PutMapping("/{id}/cancel")
55
+    public R<String> cancel(@PathVariable Long id) {
56
+        customerArchiveService.cancel(id);
57
+        return R.ok("销户成功");
58
+    }
59
+
60
+    @Operation(summary = "用户统计")
61
+    @GetMapping("/stats")
62
+    public R<Map<String, Object>> stats(@RequestParam(required = false) String area) {
63
+        return R.ok(customerArchiveService.stats(area));
64
+    }
65
+}

+ 62
- 0
wm-revenue/src/main/java/com/water/revenue/controller/RevenueDashboardController.java Bestand weergeven

@@ -0,0 +1,62 @@
1
+package com.water.revenue.controller;
2
+
3
+import com.water.common.core.result.R;
4
+import com.water.revenue.service.RevenueDashboardService;
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
+
11
+import java.util.List;
12
+import java.util.Map;
13
+
14
+@Tag(name = "营收总览")
15
+@RestController
16
+@RequestMapping("/revenue/dashboard")
17
+@RequiredArgsConstructor
18
+public class RevenueDashboardController {
19
+
20
+    private final RevenueDashboardService dashboardService;
21
+
22
+    @GetMapping("/overview")
23
+    @Operation(summary = "首页总览")
24
+    public R<Map<String, Object>> getOverview() {
25
+        return R.ok(dashboardService.getOverview());
26
+    }
27
+
28
+    @GetMapping("/trend")
29
+    @Operation(summary = "营收趋势")
30
+    public R<List<Map<String, Object>>> getRevenueTrend(
31
+            @Parameter(description = "周期:day|week|month")
32
+            @RequestParam(defaultValue = "month") String period) {
33
+        return R.ok(dashboardService.getRevenueTrend(period));
34
+    }
35
+
36
+    @GetMapping("/area/{area}")
37
+    @Operation(summary = "区域营收")
38
+    public R<Map<String, Object>> getAreaRevenue(
39
+            @Parameter(description = "区域") @PathVariable String area) {
40
+        return R.ok(dashboardService.getAreaRevenue(area));
41
+    }
42
+
43
+    @GetMapping("/payment-channels")
44
+    @Operation(summary = "支付渠道统计")
45
+    public R<List<Map<String, Object>>> getPaymentChannelStats() {
46
+        return R.ok(dashboardService.getPaymentChannelStats());
47
+    }
48
+
49
+    @GetMapping("/customer-types")
50
+    @Operation(summary = "客户类型统计")
51
+    public R<List<Map<String, Object>>> getCustomerTypeStats() {
52
+        return R.ok(dashboardService.getCustomerTypeStats());
53
+    }
54
+
55
+    @GetMapping("/top-overdue")
56
+    @Operation(summary = "欠费大户")
57
+    public R<List<Map<String, Object>>> getTopOverdueCustomers(
58
+            @Parameter(description = "数量限制")
59
+            @RequestParam(defaultValue = "10") Integer limit) {
60
+        return R.ok(dashboardService.getTopOverdueCustomers(limit));
61
+    }
62
+}

+ 110
- 0
wm-revenue/src/main/java/com/water/revenue/controller/RevenueQueryController.java Bestand weergeven

@@ -0,0 +1,110 @@
1
+package com.water.revenue.controller;
2
+
3
+import com.water.common.core.result.R;
4
+import com.water.revenue.service.RevenueQueryService;
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
+
11
+import java.math.BigDecimal;
12
+import java.util.HashMap;
13
+import java.util.Map;
14
+
15
+@Tag(name = "营收查询统计")
16
+@RestController
17
+@RequestMapping("/revenue/query")
18
+@RequiredArgsConstructor
19
+public class RevenueQueryController {
20
+
21
+    private final RevenueQueryService queryService;
22
+
23
+    @GetMapping("/bills")
24
+    @Operation(summary = "账单查询")
25
+    public R<Map<String, Object>> queryBills(
26
+            @Parameter(description = "开始日期") @RequestParam(required = false) String startDate,
27
+            @Parameter(description = "结束日期") @RequestParam(required = false) String endDate,
28
+            @Parameter(description = "区域") @RequestParam(required = false) String area,
29
+            @Parameter(description = "客户类型") @RequestParam(required = false) String customerType,
30
+            @Parameter(description = "状态") @RequestParam(required = false) String status,
31
+            @Parameter(description = "最小金额") @RequestParam(required = false) BigDecimal minAmount,
32
+            @Parameter(description = "最大金额") @RequestParam(required = false) BigDecimal maxAmount,
33
+            @Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer page,
34
+            @Parameter(description = "每页数量") @RequestParam(defaultValue = "20") Integer size) {
35
+        
36
+        Map<String, Object> filters = new HashMap<>();
37
+        if (startDate != null) filters.put("startDate", startDate);
38
+        if (endDate != null) filters.put("endDate", endDate);
39
+        if (area != null) filters.put("area", area);
40
+        if (customerType != null) filters.put("customerType", customerType);
41
+        if (status != null) filters.put("status", status);
42
+        if (minAmount != null) filters.put("minAmount", minAmount);
43
+        if (maxAmount != null) filters.put("maxAmount", maxAmount);
44
+        filters.put("page", page);
45
+        filters.put("size", size);
46
+        
47
+        return R.ok(queryService.queryBills(filters));
48
+    }
49
+
50
+    @GetMapping("/payments")
51
+    @Operation(summary = "缴费查询")
52
+    public R<Map<String, Object>> queryPayments(
53
+            @Parameter(description = "开始日期") @RequestParam(required = false) String startDate,
54
+            @Parameter(description = "结束日期") @RequestParam(required = false) String endDate,
55
+            @Parameter(description = "区域") @RequestParam(required = false) String area,
56
+            @Parameter(description = "支付方式") @RequestParam(required = false) String payMethod,
57
+            @Parameter(description = "支付渠道") @RequestParam(required = false) String payChannel,
58
+            @Parameter(description = "最小金额") @RequestParam(required = false) BigDecimal minAmount,
59
+            @Parameter(description = "最大金额") @RequestParam(required = false) BigDecimal maxAmount,
60
+            @Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer page,
61
+            @Parameter(description = "每页数量") @RequestParam(defaultValue = "20") Integer size) {
62
+        
63
+        Map<String, Object> filters = new HashMap<>();
64
+        if (startDate != null) filters.put("startDate", startDate);
65
+        if (endDate != null) filters.put("endDate", endDate);
66
+        if (area != null) filters.put("area", area);
67
+        if (payMethod != null) filters.put("payMethod", payMethod);
68
+        if (payChannel != null) filters.put("payChannel", payChannel);
69
+        if (minAmount != null) filters.put("minAmount", minAmount);
70
+        if (maxAmount != null) filters.put("maxAmount", maxAmount);
71
+        filters.put("page", page);
72
+        filters.put("size", size);
73
+        
74
+        return R.ok(queryService.queryPayments(filters));
75
+    }
76
+
77
+    @GetMapping("/report/monthly")
78
+    @Operation(summary = "月度报表")
79
+    public R<Map<String, Object>> monthlyReport(
80
+            @Parameter(description = "年份") @RequestParam Integer year,
81
+            @Parameter(description = "月份") @RequestParam Integer month) {
82
+        return R.ok(queryService.monthlyReport(year, month));
83
+    }
84
+
85
+    @GetMapping("/report/yearly")
86
+    @Operation(summary = "年度报表")
87
+    public R<Map<String, Object>> yearlyReport(
88
+            @Parameter(description = "年份") @RequestParam Integer year) {
89
+        return R.ok(queryService.yearlyReport(year));
90
+    }
91
+
92
+    @GetMapping("/export/bills")
93
+    @Operation(summary = "导出账单CSV")
94
+    public R<String> exportBills(
95
+            @Parameter(description = "开始日期") @RequestParam(required = false) String startDate,
96
+            @Parameter(description = "结束日期") @RequestParam(required = false) String endDate,
97
+            @Parameter(description = "区域") @RequestParam(required = false) String area,
98
+            @Parameter(description = "客户类型") @RequestParam(required = false) String customerType,
99
+            @Parameter(description = "状态") @RequestParam(required = false) String status) {
100
+        
101
+        Map<String, Object> filters = new HashMap<>();
102
+        if (startDate != null) filters.put("startDate", startDate);
103
+        if (endDate != null) filters.put("endDate", endDate);
104
+        if (area != null) filters.put("area", area);
105
+        if (customerType != null) filters.put("customerType", customerType);
106
+        if (status != null) filters.put("status", status);
107
+        
108
+        return R.ok(queryService.exportBills(filters));
109
+    }
110
+}

+ 90
- 0
wm-revenue/src/main/java/com/water/revenue/service/BillingConfigService.java Bestand weergeven

@@ -0,0 +1,90 @@
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.time.LocalDateTime;
11
+import java.util.*;
12
+
13
+@Slf4j
14
+@Service
15
+@RequiredArgsConstructor
16
+public class BillingConfigService {
17
+
18
+    private final JdbcTemplate jdbcTemplate;
19
+
20
+    public List<Map<String, Object>> getWaterPrice(String customerType) {
21
+        return jdbcTemplate.queryForList(
22
+            "SELECT * FROM rev_water_price WHERE customer_type = ? ORDER BY tier_level ASC", customerType);
23
+    }
24
+
25
+    @Transactional
26
+    public void updateWaterPrice(String customerType, List<Map<String, Object>> tiers) {
27
+        jdbcTemplate.update("DELETE FROM rev_water_price WHERE customer_type = ?", customerType);
28
+        for (Map<String, Object> tier : tiers) {
29
+            jdbcTemplate.update(
30
+                "INSERT INTO rev_water_price (customer_type, tier_level, min_usage, max_usage, price, sewage_fee, created_at, updated_at) " +
31
+                "VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
32
+                customerType, tier.get("tierLevel"), tier.get("minUsage"), tier.get("maxUsage"),
33
+                new BigDecimal(tier.get("price").toString()),
34
+                tier.containsKey("sewageFee") ? new BigDecimal(tier.get("sewageFee").toString()) : new BigDecimal("0.85"),
35
+                LocalDateTime.now(), LocalDateTime.now());
36
+        }
37
+        log.info("更新水价: customerType={}, tiers={}", customerType, tiers.size());
38
+    }
39
+
40
+    public Map<String, Object> getBillingConfig() {
41
+        List<Map<String, Object>> configs = jdbcTemplate.queryForList(
42
+            "SELECT * FROM rev_billing_config ORDER BY config_key");
43
+        Map<String, Object> result = new LinkedHashMap<>();
44
+        for (Map<String, Object> config : configs) {
45
+            result.put(config.get("config_key").toString(), config.get("config_value"));
46
+        }
47
+        return result;
48
+    }
49
+
50
+    @Transactional
51
+    public void updateBillingConfig(Map<String, String> configUpdates) {
52
+        for (Map.Entry<String, String> entry : configUpdates.entrySet()) {
53
+            int rows = jdbcTemplate.update(
54
+                "UPDATE rev_billing_config SET config_value = ?, updated_at = ? WHERE config_key = ?",
55
+                entry.getValue(), LocalDateTime.now(), entry.getKey());
56
+            if (rows == 0) {
57
+                jdbcTemplate.update(
58
+                    "INSERT INTO rev_billing_config (config_key, config_value, config_type, description, updated_at) " +
59
+                    "VALUES (?, ?, 'string', '自定义配置', ?)",
60
+                    entry.getKey(), entry.getValue(), LocalDateTime.now());
61
+            }
62
+        }
63
+        log.info("更新收费配置: keys={}", configUpdates.keySet());
64
+    }
65
+
66
+    public Map<String, Object> getSewageRate() {
67
+        List<Map<String, Object>> rates = jdbcTemplate.queryForList(
68
+            "SELECT customer_type, sewage_fee FROM rev_water_price GROUP BY customer_type, sewage_fee ORDER BY customer_type");
69
+        Map<String, Object> result = new LinkedHashMap<>();
70
+        for (Map<String, Object> rate : rates) {
71
+            result.put(rate.get("customer_type").toString(), rate.get("sewage_fee"));
72
+        }
73
+        return result;
74
+    }
75
+
76
+    public List<Map<String, Object>> listAllConfigs() {
77
+        List<Map<String, Object>> billingConfigs = jdbcTemplate.queryForList(
78
+            "SELECT * FROM rev_billing_config ORDER BY config_key");
79
+        List<Map<String, Object>> waterPrices = jdbcTemplate.queryForList(
80
+            "SELECT * FROM rev_water_price ORDER BY customer_type, tier_level");
81
+        List<Map<String, Object>> all = new ArrayList<>();
82
+        Map<String, Object> s1 = new LinkedHashMap<>();
83
+        s1.put("category", "收费配置"); s1.put("items", billingConfigs);
84
+        all.add(s1);
85
+        Map<String, Object> s2 = new LinkedHashMap<>();
86
+        s2.put("category", "水价配置"); s2.put("items", waterPrices);
87
+        all.add(s2);
88
+        return all;
89
+    }
90
+}

+ 137
- 0
wm-revenue/src/main/java/com/water/revenue/service/CustomerArchiveService.java Bestand weergeven

@@ -0,0 +1,137 @@
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) " +
26
+            "VALUES (?, ?, ?, ?, ?, ?, ?, 'active', ?, ?)",
27
+            customerNo, name, type, area, address, phone, idCard, now, now);
28
+        Long id = jdbcTemplate.queryForObject(
29
+            "SELECT id FROM rev_customer WHERE customer_no = ?", Long.class, customerNo);
30
+        log.info("创建用户档案: customerNo={}, name={}", customerNo, name);
31
+        Map<String, Object> result = new LinkedHashMap<>();
32
+        result.put("id", id);
33
+        result.put("customerNo", customerNo);
34
+        result.put("name", name);
35
+        result.put("type", type);
36
+        result.put("area", area);
37
+        result.put("address", address);
38
+        result.put("phone", phone);
39
+        result.put("status", "active");
40
+        result.put("createdAt", now);
41
+        return result;
42
+    }
43
+
44
+    @Transactional
45
+    public void update(Long id, Map<String, Object> fields) {
46
+        List<String> setClauses = new ArrayList<>();
47
+        List<Object> params = new ArrayList<>();
48
+        String[] allowedFields = {"name", "type", "area", "address", "phone", "id_card", "status"};
49
+        for (String field : allowedFields) {
50
+            if (fields.containsKey(field)) {
51
+                setClauses.add(field + " = ?");
52
+                params.add(fields.get(field));
53
+            }
54
+        }
55
+        if (setClauses.isEmpty()) throw new RuntimeException("没有可更新的字段");
56
+        setClauses.add("updated_at = ?");
57
+        params.add(LocalDateTime.now());
58
+        params.add(id);
59
+        String sql = "UPDATE rev_customer SET " + String.join(", ", setClauses) + " WHERE id = ?";
60
+        int rows = jdbcTemplate.update(sql, params.toArray());
61
+        if (rows == 0) throw new RuntimeException("用户不存在: " + id);
62
+        log.info("更新用户: id={}", id);
63
+    }
64
+
65
+    public Map<String, Object> getById(Long id) {
66
+        Map<String, Object> customer = jdbcTemplate.queryForMap(
67
+            "SELECT * FROM rev_customer WHERE id = ?", id);
68
+        List<Map<String, Object>> meters = jdbcTemplate.queryForList(
69
+            "SELECT * FROM rev_water_meter WHERE customer_no = ? ORDER BY install_date DESC",
70
+            customer.get("customer_no"));
71
+        customer.put("meters", meters);
72
+        List<Map<String, Object>> bills = jdbcTemplate.queryForList(
73
+            "SELECT * FROM rev_bill WHERE customer_no = ? ORDER BY bill_period DESC LIMIT 5",
74
+            customer.get("customer_no"));
75
+        customer.put("recentBills", bills);
76
+        return customer;
77
+    }
78
+
79
+    public Map<String, Object> list(String type, String area, String status,
80
+                                     String keyword, int page, int size) {
81
+        StringBuilder where = new StringBuilder("WHERE 1=1");
82
+        List<Object> params = new ArrayList<>();
83
+        if (type != null && !type.isEmpty()) { where.append(" AND type = ?"); params.add(type); }
84
+        if (area != null && !area.isEmpty()) { where.append(" AND area = ?"); params.add(area); }
85
+        if (status != null && !status.isEmpty()) { where.append(" AND status = ?"); params.add(status); }
86
+        if (keyword != null && !keyword.isEmpty()) {
87
+            where.append(" AND (name LIKE ? OR customer_no LIKE ? OR phone LIKE ?)");
88
+            String kw = "%" + keyword + "%";
89
+            params.add(kw); params.add(kw); params.add(kw);
90
+        }
91
+        Long total = jdbcTemplate.queryForObject(
92
+            "SELECT COUNT(*) FROM rev_customer " + where, Long.class, params.toArray());
93
+        int offset = (page - 1) * size;
94
+        List<Object> queryParams = new ArrayList<>(params);
95
+        queryParams.add(size);
96
+        queryParams.add(offset);
97
+        List<Map<String, Object>> records = jdbcTemplate.queryForList(
98
+            "SELECT * FROM rev_customer " + where + " ORDER BY created_at DESC LIMIT ? OFFSET ?",
99
+            queryParams.toArray());
100
+        Map<String, Object> result = new LinkedHashMap<>();
101
+        result.put("records", records);
102
+        result.put("total", total);
103
+        result.put("page", page);
104
+        result.put("size", size);
105
+        result.put("pages", (int) Math.ceil((double) total / size));
106
+        return result;
107
+    }
108
+
109
+    @Transactional
110
+    public void cancel(Long id) {
111
+        int rows = jdbcTemplate.update(
112
+            "UPDATE rev_customer SET status = 'cancelled', updated_at = ? WHERE id = ? AND status = 'active'",
113
+            LocalDateTime.now(), id);
114
+        if (rows == 0) throw new RuntimeException("用户不存在或已销户: " + id);
115
+        log.info("用户销户: id={}", id);
116
+    }
117
+
118
+    public Map<String, Object> stats(String area) {
119
+        StringBuilder where = new StringBuilder("WHERE 1=1");
120
+        List<Object> params = new ArrayList<>();
121
+        if (area != null && !area.isEmpty()) { where.append(" AND area = ?"); params.add(area); }
122
+        Long total = jdbcTemplate.queryForObject(
123
+            "SELECT COUNT(*) FROM rev_customer " + where, Long.class, params.toArray());
124
+        List<Map<String, Object>> byType = jdbcTemplate.queryForList(
125
+            "SELECT type, COUNT(*) as count FROM rev_customer " + where + " GROUP BY type", params.toArray());
126
+        List<Map<String, Object>> byArea = jdbcTemplate.queryForList(
127
+            "SELECT area, COUNT(*) as count FROM rev_customer " + where + " GROUP BY area", params.toArray());
128
+        List<Map<String, Object>> byStatus = jdbcTemplate.queryForList(
129
+            "SELECT status, COUNT(*) as count FROM rev_customer " + where + " GROUP BY status", params.toArray());
130
+        Map<String, Object> result = new LinkedHashMap<>();
131
+        result.put("total", total);
132
+        result.put("byType", byType);
133
+        result.put("byArea", byArea);
134
+        result.put("byStatus", byStatus);
135
+        return result;
136
+    }
137
+}

+ 238
- 0
wm-revenue/src/main/java/com/water/revenue/service/RevenueDashboardService.java Bestand weergeven

@@ -0,0 +1,238 @@
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.time.LocalDate;
11
+import java.time.format.DateTimeFormatter;
12
+import java.util.*;
13
+
14
+@Slf4j
15
+@Service
16
+@RequiredArgsConstructor
17
+public class RevenueDashboardService {
18
+
19
+    private final JdbcTemplate jdbcTemplate;
20
+
21
+    /**
22
+     * 首页总览
23
+     */
24
+    public Map<String, Object> getOverview() {
25
+        log.info("Getting revenue overview");
26
+        
27
+        // 总营收
28
+        BigDecimal totalRevenue = jdbcTemplate.queryForObject(
29
+            "SELECT COALESCE(SUM(paid_fee), 0) FROM rev_bill", BigDecimal.class);
30
+        
31
+        // 本月营收
32
+        BigDecimal monthRevenue = jdbcTemplate.queryForObject(
33
+            "SELECT COALESCE(SUM(paid_fee), 0) FROM rev_bill WHERE bill_period = ?",
34
+            BigDecimal.class,
35
+            LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")));
36
+        
37
+        // 今日营收
38
+        BigDecimal todayRevenue = jdbcTemplate.queryForObject(
39
+            "SELECT COALESCE(SUM(amount), 0) FROM rev_payment WHERE DATE(paid_at) = CURRENT_DATE",
40
+            BigDecimal.class);
41
+        
42
+        // 待缴账单数
43
+        Integer pendingBills = jdbcTemplate.queryForObject(
44
+            "SELECT COUNT(*) FROM rev_bill WHERE status IN ('pending', 'partial')",
45
+            Integer.class);
46
+        
47
+        // 逾期账单数
48
+        Integer overdueBills = jdbcTemplate.queryForObject(
49
+            "SELECT COUNT(*) FROM rev_bill WHERE status = 'overdue'",
50
+            Integer.class);
51
+        
52
+        // 逾期金额
53
+        BigDecimal overdueAmount = jdbcTemplate.queryForObject(
54
+            "SELECT COALESCE(SUM(total_fee - paid_fee), 0) FROM rev_bill WHERE status = 'overdue'",
55
+            BigDecimal.class);
56
+        
57
+        // 收缴率
58
+        BigDecimal collectionRate = calculateCollectionRate();
59
+        
60
+        // 客户总数
61
+        Integer customerCount = jdbcTemplate.queryForObject(
62
+            "SELECT COUNT(*) FROM rev_customer", Integer.class);
63
+        
64
+        Map<String, Object> overview = new HashMap<>();
65
+        overview.put("totalRevenue", totalRevenue);
66
+        overview.put("monthRevenue", monthRevenue);
67
+        overview.put("todayRevenue", todayRevenue);
68
+        overview.put("pendingBills", pendingBills);
69
+        overview.put("overdueBills", overdueBills);
70
+        overview.put("overdueAmount", overdueAmount);
71
+        overview.put("collectionRate", collectionRate);
72
+        overview.put("customerCount", customerCount);
73
+        
74
+        log.info("Overview retrieved: totalRevenue={}, monthRevenue={}", totalRevenue, monthRevenue);
75
+        return overview;
76
+    }
77
+
78
+    /**
79
+     * 营收趋势(按日/周/月)
80
+     */
81
+    public List<Map<String, Object>> getRevenueTrend(String period) {
82
+        log.info("Getting revenue trend for period: {}", period);
83
+        
84
+        String dateFormat;
85
+        int days;
86
+        
87
+        switch (period.toLowerCase()) {
88
+            case "day":
89
+                dateFormat = "DATE(paid_at)";
90
+                days = 30;
91
+                break;
92
+            case "week":
93
+                dateFormat = "DATE_TRUNC('week', paid_at)";
94
+                days = 84; // 12 weeks
95
+                break;
96
+            case "month":
97
+            default:
98
+                dateFormat = "TO_CHAR(paid_at, 'YYYY-MM')";
99
+                days = 365;
100
+                break;
101
+        }
102
+        
103
+        LocalDate startDate = LocalDate.now().minusDays(days);
104
+        
105
+        String sql = String.format(
106
+            "SELECT %s as period, COALESCE(SUM(amount), 0) as revenue, COUNT(*) as count " +
107
+            "FROM rev_payment " +
108
+            "WHERE paid_at >= ? " +
109
+            "GROUP BY %s " +
110
+            "ORDER BY period",
111
+            dateFormat, dateFormat);
112
+        
113
+        List<Map<String, Object>> trends = jdbcTemplate.queryForList(sql, startDate);
114
+        
115
+        log.info("Revenue trend retrieved: {} data points", trends.size());
116
+        return trends;
117
+    }
118
+
119
+    /**
120
+     * 按区域统计营收
121
+     */
122
+    public Map<String, Object> getAreaRevenue(String area) {
123
+        log.info("Getting area revenue for: {}", area);
124
+        
125
+        Map<String, Object> result = new HashMap<>();
126
+        
127
+        // 区域总营收
128
+        BigDecimal totalRevenue = jdbcTemplate.queryForObject(
129
+            "SELECT COALESCE(SUM(b.paid_fee), 0) FROM rev_bill b " +
130
+            "JOIN rev_customer c ON b.customer_id = c.id " +
131
+            "WHERE c.area = ?",
132
+            BigDecimal.class, area);
133
+        
134
+        // 区域账单数
135
+        Integer billCount = jdbcTemplate.queryForObject(
136
+            "SELECT COUNT(*) FROM rev_bill b " +
137
+            "JOIN rev_customer c ON b.customer_id = c.id " +
138
+            "WHERE c.area = ?",
139
+            Integer.class, area);
140
+        
141
+        // 区域欠费
142
+        BigDecimal overdueAmount = jdbcTemplate.queryForObject(
143
+            "SELECT COALESCE(SUM(b.total_fee - b.paid_fee), 0) FROM rev_bill b " +
144
+            "JOIN rev_customer c ON b.customer_id = c.id " +
145
+            "WHERE c.area = ? AND b.status IN ('pending', 'partial', 'overdue')",
146
+            BigDecimal.class, area);
147
+        
148
+        // 区域客户数
149
+        Integer customerCount = jdbcTemplate.queryForObject(
150
+            "SELECT COUNT(*) FROM rev_customer WHERE area = ?",
151
+            Integer.class, area);
152
+        
153
+        result.put("area", area);
154
+        result.put("totalRevenue", totalRevenue);
155
+        result.put("billCount", billCount);
156
+        result.put("overdueAmount", overdueAmount);
157
+        result.put("customerCount", customerCount);
158
+        
159
+        log.info("Area revenue for {}: totalRevenue={}, billCount={}", area, totalRevenue, billCount);
160
+        return result;
161
+    }
162
+
163
+    /**
164
+     * 按支付渠道统计
165
+     */
166
+    public List<Map<String, Object>> getPaymentChannelStats() {
167
+        log.info("Getting payment channel statistics");
168
+        
169
+        List<Map<String, Object>> stats = jdbcTemplate.queryForList(
170
+            "SELECT pay_channel, COUNT(*) as count, COALESCE(SUM(amount), 0) as total " +
171
+            "FROM rev_payment " +
172
+            "GROUP BY pay_channel " +
173
+            "ORDER BY total DESC");
174
+        
175
+        log.info("Payment channel stats retrieved: {} channels", stats.size());
176
+        return stats;
177
+    }
178
+
179
+    /**
180
+     * 按客户类型统计
181
+     */
182
+    public List<Map<String, Object>> getCustomerTypeStats() {
183
+        log.info("Getting customer type statistics");
184
+        
185
+        List<Map<String, Object>> stats = jdbcTemplate.queryForList(
186
+            "SELECT c.customer_type, COUNT(b.id) as billCount, " +
187
+            "COALESCE(SUM(b.total_fee), 0) as totalAmount, " +
188
+            "COALESCE(SUM(b.paid_fee), 0) as paidAmount " +
189
+            "FROM rev_bill b " +
190
+            "JOIN rev_customer c ON b.customer_id = c.id " +
191
+            "GROUP BY c.customer_type " +
192
+            "ORDER BY totalAmount DESC");
193
+        
194
+        log.info("Customer type stats retrieved: {} types", stats.size());
195
+        return stats;
196
+    }
197
+
198
+    /**
199
+     * 欠费大户排行
200
+     */
201
+    public List<Map<String, Object>> getTopOverdueCustomers(int limit) {
202
+        log.info("Getting top overdue customers, limit: {}", limit);
203
+        
204
+        List<Map<String, Object>> customers = jdbcTemplate.queryForList(
205
+            "SELECT c.id, c.customer_name, c.customer_no, c.area, " +
206
+            "COUNT(b.id) as overdueBillCount, " +
207
+            "COALESCE(SUM(b.total_fee - b.paid_fee), 0) as overdueAmount " +
208
+            "FROM rev_bill b " +
209
+            "JOIN rev_customer c ON b.customer_id = c.id " +
210
+            "WHERE b.status IN ('overdue', 'pending', 'partial') " +
211
+            "GROUP BY c.id, c.customer_name, c.customer_no, c.area " +
212
+            "HAVING SUM(b.total_fee - b.paid_fee) > 0 " +
213
+            "ORDER BY overdueAmount DESC " +
214
+            "LIMIT ?",
215
+            limit);
216
+        
217
+        log.info("Top overdue customers retrieved: {} customers", customers.size());
218
+        return customers;
219
+    }
220
+
221
+    /**
222
+     * 计算收缴率
223
+     */
224
+    private BigDecimal calculateCollectionRate() {
225
+        BigDecimal totalBilled = jdbcTemplate.queryForObject(
226
+            "SELECT COALESCE(SUM(total_fee), 0) FROM rev_bill", BigDecimal.class);
227
+        
228
+        if (totalBilled.compareTo(BigDecimal.ZERO) == 0) {
229
+            return BigDecimal.ZERO;
230
+        }
231
+        
232
+        BigDecimal totalPaid = jdbcTemplate.queryForObject(
233
+            "SELECT COALESCE(SUM(paid_fee), 0) FROM rev_bill", BigDecimal.class);
234
+        
235
+        return totalPaid.multiply(BigDecimal.valueOf(100))
236
+            .divide(totalBilled, 2, RoundingMode.HALF_UP);
237
+    }
238
+}

+ 391
- 0
wm-revenue/src/main/java/com/water/revenue/service/RevenueQueryService.java Bestand weergeven

@@ -0,0 +1,391 @@
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.time.LocalDate;
10
+import java.time.format.DateTimeFormatter;
11
+import java.util.*;
12
+
13
+@Slf4j
14
+@Service
15
+@RequiredArgsConstructor
16
+public class RevenueQueryService {
17
+
18
+    private final JdbcTemplate jdbcTemplate;
19
+
20
+    /**
21
+     * 多维度账单查询
22
+     */
23
+    public Map<String, Object> queryBills(Map<String, Object> filters) {
24
+        log.info("Querying bills with filters: {}", filters);
25
+        
26
+        StringBuilder sql = new StringBuilder(
27
+            "SELECT b.*, c.customer_name, c.customer_no, c.area, c.customer_type, m.meter_no " +
28
+            "FROM rev_bill b " +
29
+            "JOIN rev_customer c ON b.customer_id = c.id " +
30
+            "JOIN rev_meter m ON b.meter_id = m.id " +
31
+            "WHERE 1=1");
32
+        
33
+        List<Object> params = new ArrayList<>();
34
+        
35
+        // 时间范围
36
+        if (filters.get("startDate") != null) {
37
+            sql.append(" AND b.created_at >= ?::timestamp");
38
+            params.add(filters.get("startDate"));
39
+        }
40
+        if (filters.get("endDate") != null) {
41
+            sql.append(" AND b.created_at <= ?::timestamp");
42
+            params.add(filters.get("endDate"));
43
+        }
44
+        
45
+        // 区域
46
+        if (filters.get("area") != null && !filters.get("area").toString().isEmpty()) {
47
+            sql.append(" AND c.area = ?");
48
+            params.add(filters.get("area"));
49
+        }
50
+        
51
+        // 客户类型
52
+        if (filters.get("customerType") != null && !filters.get("customerType").toString().isEmpty()) {
53
+            sql.append(" AND c.customer_type = ?");
54
+            params.add(filters.get("customerType"));
55
+        }
56
+        
57
+        // 状态
58
+        if (filters.get("status") != null && !filters.get("status").toString().isEmpty()) {
59
+            sql.append(" AND b.status = ?");
60
+            params.add(filters.get("status"));
61
+        }
62
+        
63
+        // 金额范围
64
+        if (filters.get("minAmount") != null) {
65
+            sql.append(" AND b.total_fee >= ?");
66
+            params.add(new BigDecimal(filters.get("minAmount").toString()));
67
+        }
68
+        if (filters.get("maxAmount") != null) {
69
+            sql.append(" AND b.total_fee <= ?");
70
+            params.add(new BigDecimal(filters.get("maxAmount").toString()));
71
+        }
72
+        
73
+        sql.append(" ORDER BY b.created_at DESC");
74
+        
75
+        // 分页
76
+        int page = filters.get("page") != null ? Integer.parseInt(filters.get("page").toString()) : 1;
77
+        int size = filters.get("size") != null ? Integer.parseInt(filters.get("size").toString()) : 20;
78
+        int offset = (page - 1) * size;
79
+        
80
+        sql.append(" LIMIT ? OFFSET ?");
81
+        params.add(size);
82
+        params.add(offset);
83
+        
84
+        List<Map<String, Object>> bills = jdbcTemplate.queryForList(sql.toString(), params.toArray());
85
+        
86
+        // 统计总数
87
+        String countSql = sql.toString().replaceFirst("SELECT .*? FROM", "SELECT COUNT(*) FROM")
88
+            .replaceAll(" ORDER BY.*", "");
89
+        Integer total = jdbcTemplate.queryForObject(countSql, Integer.class, 
90
+            params.subList(0, params.size() - 2).toArray());
91
+        
92
+        Map<String, Object> result = new HashMap<>();
93
+        result.put("records", bills);
94
+        result.put("total", total);
95
+        result.put("page", page);
96
+        result.put("size", size);
97
+        result.put("pages", (total + size - 1) / size);
98
+        
99
+        log.info("Bills queried: {} records, total: {}", bills.size(), total);
100
+        return result;
101
+    }
102
+
103
+    /**
104
+     * 多维度缴费查询
105
+     */
106
+    public Map<String, Object> queryPayments(Map<String, Object> filters) {
107
+        log.info("Querying payments with filters: {}", filters);
108
+        
109
+        StringBuilder sql = new StringBuilder(
110
+            "SELECT p.*, b.bill_no, b.total_fee, c.customer_name, c.customer_no, c.area " +
111
+            "FROM rev_payment p " +
112
+            "JOIN rev_bill b ON p.bill_id = b.id " +
113
+            "JOIN rev_customer c ON p.customer_id = c.id " +
114
+            "WHERE 1=1");
115
+        
116
+        List<Object> params = new ArrayList<>();
117
+        
118
+        // 时间范围
119
+        if (filters.get("startDate") != null) {
120
+            sql.append(" AND p.paid_at >= ?::timestamp");
121
+            params.add(filters.get("startDate"));
122
+        }
123
+        if (filters.get("endDate") != null) {
124
+            sql.append(" AND p.paid_at <= ?::timestamp");
125
+            params.add(filters.get("endDate"));
126
+        }
127
+        
128
+        // 区域
129
+        if (filters.get("area") != null && !filters.get("area").toString().isEmpty()) {
130
+            sql.append(" AND c.area = ?");
131
+            params.add(filters.get("area"));
132
+        }
133
+        
134
+        // 支付方式
135
+        if (filters.get("payMethod") != null && !filters.get("payMethod").toString().isEmpty()) {
136
+            sql.append(" AND p.pay_method = ?");
137
+            params.add(filters.get("payMethod"));
138
+        }
139
+        
140
+        // 支付渠道
141
+        if (filters.get("payChannel") != null && !filters.get("payChannel").toString().isEmpty()) {
142
+            sql.append(" AND p.pay_channel = ?");
143
+            params.add(filters.get("payChannel"));
144
+        }
145
+        
146
+        // 金额范围
147
+        if (filters.get("minAmount") != null) {
148
+            sql.append(" AND p.amount >= ?");
149
+            params.add(new BigDecimal(filters.get("minAmount").toString()));
150
+        }
151
+        if (filters.get("maxAmount") != null) {
152
+            sql.append(" AND p.amount <= ?");
153
+            params.add(new BigDecimal(filters.get("maxAmount").toString()));
154
+        }
155
+        
156
+        sql.append(" ORDER BY p.paid_at DESC");
157
+        
158
+        // 分页
159
+        int page = filters.get("page") != null ? Integer.parseInt(filters.get("page").toString()) : 1;
160
+        int size = filters.get("size") != null ? Integer.parseInt(filters.get("size").toString()) : 20;
161
+        int offset = (page - 1) * size;
162
+        
163
+        sql.append(" LIMIT ? OFFSET ?");
164
+        params.add(size);
165
+        params.add(offset);
166
+        
167
+        List<Map<String, Object>> payments = jdbcTemplate.queryForList(sql.toString(), params.toArray());
168
+        
169
+        // 统计总数
170
+        String countSql = sql.toString().replaceFirst("SELECT .*? FROM", "SELECT COUNT(*) FROM")
171
+            .replaceAll(" ORDER BY.*", "");
172
+        Integer total = jdbcTemplate.queryForObject(countSql, Integer.class, 
173
+            params.subList(0, params.size() - 2).toArray());
174
+        
175
+        Map<String, Object> result = new HashMap<>();
176
+        result.put("records", payments);
177
+        result.put("total", total);
178
+        result.put("page", page);
179
+        result.put("size", size);
180
+        result.put("pages", (total + size - 1) / size);
181
+        
182
+        log.info("Payments queried: {} records, total: {}", payments.size(), total);
183
+        return result;
184
+    }
185
+
186
+    /**
187
+     * 月度营收报表
188
+     */
189
+    public Map<String, Object> monthlyReport(int year, int month) {
190
+        log.info("Generating monthly report: {}-{}", year, month);
191
+        
192
+        String period = String.format("%04d-%02d", year, month);
193
+        
194
+        Map<String, Object> report = new HashMap<>();
195
+        report.put("year", year);
196
+        report.put("month", month);
197
+        report.put("period", period);
198
+        
199
+        // 账单统计
200
+        Map<String, Object> billStats = jdbcTemplate.queryForMap(
201
+            "SELECT COUNT(*) as billCount, " +
202
+            "COALESCE(SUM(total_fee), 0) as totalAmount, " +
203
+            "COALESCE(SUM(paid_fee), 0) as paidAmount, " +
204
+            "COALESCE(SUM(total_fee - paid_fee), 0) as unpaidAmount " +
205
+            "FROM rev_bill WHERE bill_period = ?",
206
+            period);
207
+        report.putAll(billStats);
208
+        
209
+        // 缴费统计
210
+        Map<String, Object> paymentStats = jdbcTemplate.queryForMap(
211
+            "SELECT COUNT(*) as paymentCount, " +
212
+            "COALESCE(SUM(amount), 0) as paymentAmount " +
213
+            "FROM rev_payment p " +
214
+            "JOIN rev_bill b ON p.bill_id = b.id " +
215
+            "WHERE b.bill_period = ?",
216
+            period);
217
+        report.putAll(paymentStats);
218
+        
219
+        // 按区域统计
220
+        List<Map<String, Object>> areaStats = jdbcTemplate.queryForList(
221
+            "SELECT c.area, COUNT(b.id) as billCount, " +
222
+            "COALESCE(SUM(b.total_fee), 0) as totalAmount, " +
223
+            "COALESCE(SUM(b.paid_fee), 0) as paidAmount " +
224
+            "FROM rev_bill b " +
225
+            "JOIN rev_customer c ON b.customer_id = c.id " +
226
+            "WHERE b.bill_period = ? " +
227
+            "GROUP BY c.area " +
228
+            "ORDER BY totalAmount DESC",
229
+            period);
230
+        report.put("areaStats", areaStats);
231
+        
232
+        // 按支付方式统计
233
+        List<Map<String, Object>> paymentMethodStats = jdbcTemplate.queryForList(
234
+            "SELECT p.pay_method, COUNT(*) as count, " +
235
+            "COALESCE(SUM(p.amount), 0) as amount " +
236
+            "FROM rev_payment p " +
237
+            "JOIN rev_bill b ON p.bill_id = b.id " +
238
+            "WHERE b.bill_period = ? " +
239
+            "GROUP BY p.pay_method " +
240
+            "ORDER BY amount DESC",
241
+            period);
242
+        report.put("paymentMethodStats", paymentMethodStats);
243
+        
244
+        log.info("Monthly report generated for {}: totalAmount={}, paidAmount={}", 
245
+            period, billStats.get("totalAmount"), billStats.get("paidAmount"));
246
+        return report;
247
+    }
248
+
249
+    /**
250
+     * 年度营收报表
251
+     */
252
+    public Map<String, Object> yearlyReport(int year) {
253
+        log.info("Generating yearly report: {}", year);
254
+        
255
+        Map<String, Object> report = new HashMap<>();
256
+        report.put("year", year);
257
+        
258
+        // 年度总计
259
+        Map<String, Object> yearlyStats = jdbcTemplate.queryForMap(
260
+            "SELECT COUNT(*) as billCount, " +
261
+            "COALESCE(SUM(total_fee), 0) as totalAmount, " +
262
+            "COALESCE(SUM(paid_fee), 0) as paidAmount, " +
263
+            "COALESCE(SUM(total_fee - paid_fee), 0) as unpaidAmount " +
264
+            "FROM rev_bill WHERE EXTRACT(YEAR FROM created_at) = ?",
265
+            year);
266
+        report.putAll(yearlyStats);
267
+        
268
+        // 年度缴费统计
269
+        Map<String, Object> paymentStats = jdbcTemplate.queryForMap(
270
+            "SELECT COUNT(*) as paymentCount, " +
271
+            "COALESCE(SUM(p.amount), 0) as paymentAmount " +
272
+            "FROM rev_payment p " +
273
+            "JOIN rev_bill b ON p.bill_id = b.id " +
274
+            "WHERE EXTRACT(YEAR FROM b.created_at) = ?",
275
+            year);
276
+        report.putAll(paymentStats);
277
+        
278
+        // 按月统计
279
+        List<Map<String, Object>> monthlyStats = jdbcTemplate.queryForList(
280
+            "SELECT bill_period as month, COUNT(*) as billCount, " +
281
+            "COALESCE(SUM(total_fee), 0) as totalAmount, " +
282
+            "COALESCE(SUM(paid_fee), 0) as paidAmount " +
283
+            "FROM rev_bill " +
284
+            "WHERE EXTRACT(YEAR FROM created_at) = ? " +
285
+            "GROUP BY bill_period " +
286
+            "ORDER BY bill_period",
287
+            year);
288
+        report.put("monthlyStats", monthlyStats);
289
+        
290
+        // 按区域统计
291
+        List<Map<String, Object>> areaStats = jdbcTemplate.queryForList(
292
+            "SELECT c.area, COUNT(b.id) as billCount, " +
293
+            "COALESCE(SUM(b.total_fee), 0) as totalAmount, " +
294
+            "COALESCE(SUM(b.paid_fee), 0) as paidAmount " +
295
+            "FROM rev_bill b " +
296
+            "JOIN rev_customer c ON b.customer_id = c.id " +
297
+            "WHERE EXTRACT(YEAR FROM b.created_at) = ? " +
298
+            "GROUP BY c.area " +
299
+            "ORDER BY totalAmount DESC",
300
+            year);
301
+        report.put("areaStats", areaStats);
302
+        
303
+        log.info("Yearly report generated for {}: totalAmount={}, paidAmount={}", 
304
+            year, yearlyStats.get("totalAmount"), yearlyStats.get("paidAmount"));
305
+        return report;
306
+    }
307
+
308
+    /**
309
+     * 导出账单(CSV 格式)
310
+     */
311
+    public String exportBills(Map<String, Object> filters) {
312
+        log.info("Exporting bills with filters: {}", filters);
313
+        
314
+        StringBuilder sql = new StringBuilder(
315
+            "SELECT b.bill_no, c.customer_name, c.customer_no, c.area, c.customer_type, " +
316
+            "m.meter_no, b.bill_period, b.consumption, b.water_fee, b.sewage_fee, " +
317
+            "b.total_fee, b.paid_fee, b.status, b.due_date, b.created_at " +
318
+            "FROM rev_bill b " +
319
+            "JOIN rev_customer c ON b.customer_id = c.id " +
320
+            "JOIN rev_meter m ON b.meter_id = m.id " +
321
+            "WHERE 1=1");
322
+        
323
+        List<Object> params = new ArrayList<>();
324
+        
325
+        // 应用过滤条件(同 queryBills)
326
+        if (filters.get("startDate") != null) {
327
+            sql.append(" AND b.created_at >= ?::timestamp");
328
+            params.add(filters.get("startDate"));
329
+        }
330
+        if (filters.get("endDate") != null) {
331
+            sql.append(" AND b.created_at <= ?::timestamp");
332
+            params.add(filters.get("endDate"));
333
+        }
334
+        if (filters.get("area") != null && !filters.get("area").toString().isEmpty()) {
335
+            sql.append(" AND c.area = ?");
336
+            params.add(filters.get("area"));
337
+        }
338
+        if (filters.get("customerType") != null && !filters.get("customerType").toString().isEmpty()) {
339
+            sql.append(" AND c.customer_type = ?");
340
+            params.add(filters.get("customerType"));
341
+        }
342
+        if (filters.get("status") != null && !filters.get("status").toString().isEmpty()) {
343
+            sql.append(" AND b.status = ?");
344
+            params.add(filters.get("status"));
345
+        }
346
+        
347
+        sql.append(" ORDER BY b.created_at DESC LIMIT 10000"); // 限制导出数量
348
+        
349
+        List<Map<String, Object>> bills = jdbcTemplate.queryForList(sql.toString(), params.toArray());
350
+        
351
+        // 生成 CSV
352
+        StringBuilder csv = new StringBuilder();
353
+        csv.append("账单编号,客户名称,客户编号,区域,客户类型,水表号,账期,用水量,水费,污水处理费,")
354
+           .append("总金额,已缴金额,状态,到期日期,创建时间\n");
355
+        
356
+        for (Map<String, Object> bill : bills) {
357
+            csv.append(escapeCsv(bill.get("bill_no"))).append(",")
358
+               .append(escapeCsv(bill.get("customer_name"))).append(",")
359
+               .append(escapeCsv(bill.get("customer_no"))).append(",")
360
+               .append(escapeCsv(bill.get("area"))).append(",")
361
+               .append(escapeCsv(bill.get("customer_type"))).append(",")
362
+               .append(escapeCsv(bill.get("meter_no"))).append(",")
363
+               .append(escapeCsv(bill.get("bill_period"))).append(",")
364
+               .append(bill.get("consumption")).append(",")
365
+               .append(bill.get("water_fee")).append(",")
366
+               .append(bill.get("sewage_fee")).append(",")
367
+               .append(bill.get("total_fee")).append(",")
368
+               .append(bill.get("paid_fee")).append(",")
369
+               .append(escapeCsv(bill.get("status"))).append(",")
370
+               .append(bill.get("due_date")).append(",")
371
+               .append(bill.get("created_at")).append("\n");
372
+        }
373
+        
374
+        log.info("Bills exported: {} records", bills.size());
375
+        return csv.toString();
376
+    }
377
+
378
+    /**
379
+     * CSV 字段转义
380
+     */
381
+    private String escapeCsv(Object value) {
382
+        if (value == null) {
383
+            return "";
384
+        }
385
+        String str = value.toString();
386
+        if (str.contains(",") || str.contains("\"") || str.contains("\n")) {
387
+            return "\"" + str.replace("\"", "\"\"") + "\"";
388
+        }
389
+        return str;
390
+    }
391
+}

+ 17
- 0
wm-revenue/src/main/resources/sql/V83__revenue_dashboard.sql Bestand weergeven

@@ -0,0 +1,17 @@
1
+-- 营收统计缓存表(定时汇总,加速 Dashboard 查询)
2
+CREATE TABLE IF NOT EXISTS rev_revenue_daily (
3
+    id              BIGSERIAL PRIMARY KEY,
4
+    stat_date       DATE NOT NULL,
5
+    area            VARCHAR(50),
6
+    customer_type   VARCHAR(20),
7
+    total_bills     INT DEFAULT 0,
8
+    total_amount    DECIMAL(14,2) DEFAULT 0,
9
+    paid_amount     DECIMAL(14,2) DEFAULT 0,
10
+    overdue_amount  DECIMAL(14,2) DEFAULT 0,
11
+    new_customers   INT DEFAULT 0,
12
+    created_at      TIMESTAMPTZ DEFAULT NOW(),
13
+    UNIQUE(stat_date, area, customer_type)
14
+);
15
+
16
+CREATE INDEX IF NOT EXISTS idx_rev_daily_date ON rev_revenue_daily(stat_date);
17
+CREATE INDEX IF NOT EXISTS idx_rev_daily_area ON rev_revenue_daily(area);

+ 64
- 0
wm-revenue/src/main/resources/sql/V84__customer_billing_config.sql Bestand weergeven

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