|
|
@@ -0,0 +1,134 @@
|
|
|
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.LocalDate;
|
|
|
11
|
+import java.util.*;
|
|
|
12
|
+
|
|
|
13
|
+/**
|
|
|
14
|
+ * 账单对账服务 - Issue #51
|
|
|
15
|
+ * 支持按渠道/日期对账,自动发现差异
|
|
|
16
|
+ */
|
|
|
17
|
+@Slf4j
|
|
|
18
|
+@Service
|
|
|
19
|
+@RequiredArgsConstructor
|
|
|
20
|
+public class BillingReconciliationService {
|
|
|
21
|
+
|
|
|
22
|
+ private final JdbcTemplate jdbcTemplate;
|
|
|
23
|
+
|
|
|
24
|
+ /** 执行指定日期、指定渠道的对账 */
|
|
|
25
|
+ @Transactional
|
|
|
26
|
+ public Map<String, Object> reconcile(LocalDate reconDate, String channelCode) {
|
|
|
27
|
+ log.info("Starting reconciliation: date={} channel={}", reconDate, channelCode);
|
|
|
28
|
+
|
|
|
29
|
+ // 查询系统侧该渠道当日缴费汇总
|
|
|
30
|
+ Map<String, Object> systemSummary = jdbcTemplate.queryForMap(
|
|
|
31
|
+ "SELECT COUNT(*) as total_count, COALESCE(SUM(amount), 0) as total_amount " +
|
|
|
32
|
+ "FROM rev_payment WHERE pay_channel = ? AND DATE(paid_at) = ?",
|
|
|
33
|
+ channelCode, reconDate);
|
|
|
34
|
+
|
|
|
35
|
+ int totalCount = ((Number) systemSummary.get("total_count")).intValue();
|
|
|
36
|
+ BigDecimal totalAmount = (BigDecimal) systemSummary.get("total_amount");
|
|
|
37
|
+
|
|
|
38
|
+ // 模拟渠道侧对账数据 (实际应从渠道API拉取)
|
|
|
39
|
+ Map<String, Object> channelData = simulateChannelData(reconDate, channelCode);
|
|
|
40
|
+ int channelCount = (int) channelData.get("count");
|
|
|
41
|
+ BigDecimal channelAmount = (BigDecimal) channelData.get("amount");
|
|
|
42
|
+
|
|
|
43
|
+ int diffCount = Math.abs(totalCount - channelCount);
|
|
|
44
|
+ BigDecimal diffAmount = totalAmount.subtract(channelAmount).abs();
|
|
|
45
|
+ String status = (diffCount == 0 && diffAmount.compareTo(BigDecimal.ZERO) == 0) ? "matched" : "has_diff";
|
|
|
46
|
+
|
|
|
47
|
+ // 保存对账记录
|
|
|
48
|
+ jdbcTemplate.update(
|
|
|
49
|
+ "INSERT INTO rev_reconciliation (recon_date, channel_code, total_count, total_amount, " +
|
|
|
50
|
+ "matched_count, matched_amount, diff_count, diff_amount, status) " +
|
|
|
51
|
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) " +
|
|
|
52
|
+ "ON CONFLICT (recon_date, channel_code) DO UPDATE SET " +
|
|
|
53
|
+ "total_count=EXCLUDED.total_count, total_amount=EXCLUDED.total_amount, " +
|
|
|
54
|
+ "matched_count=EXCLUDED.matched_count, matched_amount=EXCLUDED.matched_amount, " +
|
|
|
55
|
+ "diff_count=EXCLUDED.diff_count, diff_amount=EXCLUDED.diff_amount, status=EXCLUDED.status",
|
|
|
56
|
+ reconDate, channelCode, totalCount, totalAmount,
|
|
|
57
|
+ Math.min(totalCount, channelCount), totalAmount.min(channelAmount),
|
|
|
58
|
+ diffCount, diffAmount, status);
|
|
|
59
|
+
|
|
|
60
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
61
|
+ result.put("reconDate", reconDate.toString());
|
|
|
62
|
+ result.put("channelCode", channelCode);
|
|
|
63
|
+ result.put("systemCount", totalCount);
|
|
|
64
|
+ result.put("systemAmount", totalAmount);
|
|
|
65
|
+ result.put("channelCount", channelCount);
|
|
|
66
|
+ result.put("channelAmount", channelAmount);
|
|
|
67
|
+ result.put("diffCount", diffCount);
|
|
|
68
|
+ result.put("diffAmount", diffAmount);
|
|
|
69
|
+ result.put("status", status);
|
|
|
70
|
+
|
|
|
71
|
+ log.info("Reconciliation done: {} {} status={} diff={}/{}",
|
|
|
72
|
+ reconDate, channelCode, status, diffCount, diffAmount);
|
|
|
73
|
+ return result;
|
|
|
74
|
+ }
|
|
|
75
|
+
|
|
|
76
|
+ /** 批量对账 - 对所有启用的渠道执行对账 */
|
|
|
77
|
+ public List<Map<String, Object>> reconcileAll(LocalDate reconDate) {
|
|
|
78
|
+ List<Map<String, Object>> channels = jdbcTemplate.queryForList(
|
|
|
79
|
+ "SELECT channel_code FROM rev_pay_channel_config WHERE enabled = TRUE ORDER BY sort_order");
|
|
|
80
|
+
|
|
|
81
|
+ List<Map<String, Object>> results = new ArrayList<>();
|
|
|
82
|
+ for (Map<String, Object> ch : channels) {
|
|
|
83
|
+ try {
|
|
|
84
|
+ results.add(reconcile(reconDate, (String) ch.get("channel_code")));
|
|
|
85
|
+ } catch (Exception e) {
|
|
|
86
|
+ log.error("Reconciliation failed for channel {}: {}", ch.get("channel_code"), e.getMessage());
|
|
|
87
|
+ results.add(Map.of(
|
|
|
88
|
+ "channelCode", ch.get("channel_code"),
|
|
|
89
|
+ "status", "error",
|
|
|
90
|
+ "error", e.getMessage()));
|
|
|
91
|
+ }
|
|
|
92
|
+ }
|
|
|
93
|
+ return results;
|
|
|
94
|
+ }
|
|
|
95
|
+
|
|
|
96
|
+ /** 查询对账记录 */
|
|
|
97
|
+ public List<Map<String, Object>> getReconRecords(LocalDate startDate, LocalDate endDate) {
|
|
|
98
|
+ return jdbcTemplate.queryForList(
|
|
|
99
|
+ "SELECT * FROM rev_reconciliation WHERE recon_date BETWEEN ? AND ? ORDER BY recon_date DESC, channel_code",
|
|
|
100
|
+ startDate, endDate);
|
|
|
101
|
+ }
|
|
|
102
|
+
|
|
|
103
|
+ /** 查询有差异的对账记录 */
|
|
|
104
|
+ public List<Map<String, Object>> getDiffRecords() {
|
|
|
105
|
+ return jdbcTemplate.queryForList(
|
|
|
106
|
+ "SELECT * FROM rev_reconciliation WHERE status = 'has_diff' ORDER BY recon_date DESC");
|
|
|
107
|
+ }
|
|
|
108
|
+
|
|
|
109
|
+ /** 获取支付渠道配置列表 */
|
|
|
110
|
+ public List<Map<String, Object>> getPayChannels() {
|
|
|
111
|
+ return jdbcTemplate.queryForList(
|
|
|
112
|
+ "SELECT * FROM rev_pay_channel_config ORDER BY sort_order");
|
|
|
113
|
+ }
|
|
|
114
|
+
|
|
|
115
|
+ /** 启用/禁用支付渠道 */
|
|
|
116
|
+ @Transactional
|
|
|
117
|
+ public Map<String, Object> toggleChannel(String channelCode, boolean enabled) {
|
|
|
118
|
+ jdbcTemplate.update(
|
|
|
119
|
+ "UPDATE rev_pay_channel_config SET enabled = ?, updated_at = NOW() WHERE channel_code = ?",
|
|
|
120
|
+ enabled, channelCode);
|
|
|
121
|
+ return Map.of("channelCode", channelCode, "enabled", enabled);
|
|
|
122
|
+ }
|
|
|
123
|
+
|
|
|
124
|
+ /** 模拟渠道侧对账数据(实际应调用渠道API) */
|
|
|
125
|
+ private Map<String, Object> simulateChannelData(LocalDate reconDate, String channelCode) {
|
|
|
126
|
+ // 真实场景: 调用支付宝/微信/银行对账API获取数据
|
|
|
127
|
+ // 这里直接取系统数据模拟(假设渠道侧完全匹配)
|
|
|
128
|
+ Map<String, Object> result = jdbcTemplate.queryForMap(
|
|
|
129
|
+ "SELECT COUNT(*) as count, COALESCE(SUM(amount), 0) as amount " +
|
|
|
130
|
+ "FROM rev_payment WHERE pay_channel = ? AND DATE(paid_at) = ?",
|
|
|
131
|
+ channelCode, reconDate);
|
|
|
132
|
+ return result;
|
|
|
133
|
+ }
|
|
|
134
|
+}
|