前言
作为一个普通公司的后端,最近在负责 SaaS 的对账系统重构。理想很丰满,现实很骨感。今天想聊聊小公司做对账系统时,可能会遇到的一些真实问题。
一、理想与现实的差距
1.1 理想中的数据分层架构
按照数仓建设的最佳实践,一个完善的对账数据体系应该是这样的:
ODS (Operational Data Store) - 操作数据层
↓
DWD (Data Warehouse Detail) - 明细数据层
↓
DWS (Data Warehouse Service) - 汇总数据层
↓
ADS (Application Data Service) - 应用数据层
每一层都有明确的职责:
- ODS 层:存储原始业务数据,保持与源系统一致
- DWD 层:对数据进行清洗、规范化,建立一致性维度
- DWS 层:按主题进行汇总统计,构建各类指标
- ADS 层:面向具体应用场景的数据集市
1.2 小公司的现实情况
但现实是什么样的呢?
ODS
↓
基础汇总表 (日统计数据)
↓
后端代码硬算 (WHERE + GROUP BY + SUM)
为什么会这样?
- 人力资源不足:小团队可能只有 1-2 个人负责数据相关工作
- 排期压力大:业务需求排期紧张,DWS 层建设"没有那个排期"
- 优先级问题:对账功能能用就行,而且需求也不会整理得特别详细
所以最终只能:
- 基于源数据构建一个 ODS 层
- 从 ODS 清洗出一份基础的日统计数据
- 剩下的统计查询,后端代码自己算,这样子就能灵活适用任何场景
二、核心痛点
这种"能跑就行"的架构,会带来两个核心问题。
2.1 痛点一:数据查询性能压力
由于缺少汇总层(DWS),所有的多维度查询都需要在基础数据表上进行实时计算。
典型场景:
-- 查询最近7天的对账汇总数据
SELECT
DATE(order_time) as stat_date,
channel_id,
SUM(amount) as total_amount,
SUM(refund_amount) as total_refund,
COUNT(*) as order_count
FROM reconciliation_daily_base
WHERE stat_date BETWEEN '2025-11-01' AND '2025-11-07'
AND merchant_id = 12345
GROUP BY DATE(order_time), channel_id;
性能瓶颈:
- 即使建了索引
(merchant_id, stat_date),但GROUP BY和SUM仍需要大量计算 - 查询时间段越长,扫描的数据量越大
- 多个维度的组合查询(如按渠道、门店、支付方式等多维度统计),性能急剧下降
- 并发查询时,数据库压力倍增
实际体验:
| 查询范围 | 响应时间 | 体验 |
|---|---|---|
| 近 7 天 | 200-500ms | 勉强可用 |
| 近 30 天 | 1-3s | 开始变慢 |
| 近 90 天 | 5s+ | 基本超时,暂不支持 |
2.1.2 为什么索引也救不了?
-- 索引能优化 WHERE 条件的过滤
-- 但 GROUP BY 和聚合函数需要读取并处理大量数据行
EXPLAIN SELECT
channel_id,
SUM(amount)
FROM reconciliation_daily_base
WHERE merchant_id = 12345
AND stat_date >= '2025-10-01'
GROUP BY channel_id;
-- 执行计划可能显示:
-- Using index condition (索引有效)
-- Using temporary; Using filesort (需要临时表和排序,性能杀手)
2.2 痛点二:数据质量问题
2.2.1 问题根源
由于是重构,元数据来自老旧的业务表,这些表往往:
- 经历过多次迭代和重构
- 字段语义不清晰
- 存在历史遗留的脏数据
- 业务逻辑复杂且文档缺失
- 往往这类的任务,要求的时间也紧急
2.2.2 典型数据质量问题
测试覆盖不完备
支付场景比较多:
| 场景类型 | 说明 | 容易遗漏的原因 |
|---|---|---|
| 隔日退款 | 今天支付,明天退款 | 测试通常当天完成,跨日场景容易忽略 |
| 部分退款 | 100元订单退50元 | 测试关注全额退款,部分退忽视 |
| 多次退款 | 同一订单分多次退 | 业务规则未明确限制 |
| 跨支付方式退款 | 微信支付,现金退款 | 线下退款场景难以模拟 |
| 换货退款 | 先退后买的组合操作 | 涉及多个业务流程 |
| 超时关单 | 未支付订单超时关闭 | 状态流转边界 case |
对账数据偏差示例:
-- 业务数据统计: 应退款金额 5000元
SELECT SUM(refund_amount) FROM orders WHERE refund_status = 'REFUNDED';
-- 结果: 5000
-- 第三方对账单: 实际退款 4500元
SELECT SUM(refund_amount) FROM wechat_reconciliation WHERE type = 'REFUND';
-- 结果: 4500
-- 差异: 500元 (现金退款部分未记录)
2 数据一致性问题
-- 场景: 订单主表 vs 支付流水表
-- 主表显示: 订单已支付
SELECT * FROM t_order WHERE order_no = 'OD123' AND pay_status = 1;
-- 支付表: 找不到支付记录
SELECT * FROM t_payment WHERE order_no = 'OD123';
-- 结果: 空
-- 原因可能:
-- 1. 主表状态手工修改
-- 2. 支付回调失败但业务补偿了
-- 3. 历史数据迁移时丢失
三、我们如何尽量预防
既然资源有限,就要用有限的资源解决核心问题。
3.1 方案一:性能问题的优化思路
核心策略
-
限制查询范围
- 提前跟业务方沟通,限制查询时间范围
- 别想着花那么多时间精力去维护多维度的汇总统计,这件事吃力不讨好
- 限制时间范围能解决大部分问题
-
异步导出长时间查询
- 如果业务方真的有需求,提供异步导出的功能
- 避免长时间查询阻塞接口
-
不要缓存!不要缓存!
- 对账是一件很精细的事情
- 如果有问题也需要我们及时调整,去重新维护数据
- 增加缓存就是增加一致性处理的工作量
3.2 方案二:数据质量的保障措施(重点)
由于时间紧迫,业务逻辑复杂,所以监控是重中之重。能提前发现对账差异,方便我们及时维护。
3.2.1 建立对账差异预警机制
核心思路:使用定时任务对比业务数据和对账数据,发现差异及时告警
@Slf4j
@Component
@JobHandler("dailyTransactionAmountValidationHandler")
public class DailyTransactionAmountValidationHandler extends IJobHandler {
@Resource
private ExtraOrderDAO extraOrderDAO;
@Resource
private ExtraFsOrderSummaryDayDAO extraFsOrderSummaryDayDAO;
@Override
public ReturnT execute(String param) throws Exception {
log.info("校验近三日总交易额任务开始执行");
try {
List summaries = new ArrayList<>();
// 获取近三天的数据进行对账
for (int i = 1; i <= 3; i++) {
Date targetDate = DateUtil.offsetDay(new Date(), -i);
String dateStr = DateUtil.format(targetDate, "yyyy-MM-dd");
// 1. 获取业务数据 - 每个商户的实际销售额
List
关键实现点:
| 要点 | 说明 |
|---|---|
| 多日对账 | 不只检查昨天,而是近 3 天,避免遗漏隔日退款等跨日场景 |
| 双向检查 | 既检查对账库多余的数据,也检查订单表独有的数据 |
| 智能告警 | 无差异不告警,减少噪音;差异按金额大小排序,优先处理大额差异;分类统计差异类型 |
| 详细日志 | 每个差异都记录日志,方便后续排查 |
3.2.2 数据清洗规则显性化
把容易出问题的业务规则,通过配置或代码明确出来:
@Configuration
public class ReconciliationRuleConfig {
/**
* 跨渠道退款规则
*/
@Bean
public ReconciliationRule crossChannelRefundRule() {
return ReconciliationRule.builder()
.ruleName("跨渠道退款检测")
.ruleType(RuleType.VALIDATION)
.condition(order ->
!order.getPayChannel().equals(order.getRefundChannel()))
.action(order -> {
// 记录差异
logDifference(order, "跨渠道退款",
String.format("支付渠道:%s, 退款渠道:%s",
order.getPayChannel(),
order.getRefundChannel()));
// 只在支付渠道统计支付,不统计退款
// 退款金额单独记录到调整项
return ReconciliationAdjustment.builder()
.type("CROSS_CHANNEL_REFUND")
.amount(order.getRefundAmount())
.build();
})
.build();
}
/**
* 隔日退款规则
*/
@Bean
public ReconciliationRule crossDayRefundRule() {
return ReconciliationRule.builder()
.ruleName("隔日退款处理")
.ruleType(RuleType.MAPPING)
.condition(order ->
!order.getPayDate().equals(order.getRefundDate()))
.action(order -> {
// 退款金额计入退款发生日,而非支付日
return ReconciliationMapping.builder()
.statDate(order.getRefundDate())
.type("REFUND")
.amount(order.getRefundAmount())
.build();
})
.build();
}
}
四、总结
小公司做对账系统,不是技术能力不行,而是资源有限的现实约束。
性能问题的本质
- 不是没有 DWS 层就一定慢
- 而是要在有限资源下,找到性价比最高的优化点
- 限制查询范围 + 异步导出,已经能解决 80% 的问题
数据质量的本质
- 不是测试覆盖率 100% 就没问题
- 而是要建立发现问题、解决问题的机制
- 差异预警 + 核心场景回归 + 人工复核,形成闭环