为什么要做对账?

做支付系统的开发者都知道一个残酷的现实:支付一定会出问题。网络延迟导致回调丢失、支付宝/微信账单与本地订单金额不一致、重复支付、退款状态不同步……这些问题几乎每天都在发生。

对账(Reconciliation)就是解决这些问题的最后一道防线。它的核心逻辑很简单:拿你的订单数据和支付渠道的账单逐笔比对,找出差异并自动修复。没有对账机制的支付系统,就像没有审计的财务部门——表面上一切正常,但资金缺口可能已经悄然扩大。

今天我们就来完整拆解一套生产级支付对账系统的设计与实现。

对账的整体流程

一个完整的日终对账流程包含以下步骤:

  1. 数据准备 — 从支付宝/微信下载前一天的结算账单(通常为 CSV 格式)
  2. 数据清洗 — 解析账单文件,统一字段格式和时区
  3. 逐笔比对 — 以本地订单为基准,与渠道账单一一匹配
  4. 差异分类 — 长款、短款、金额差异、状态差异
  5. 自动修复 — 对明确差异自动处理(如补发货、提交退款)
  6. 人工复核 — 对无法自动处理的差异生成工单
# 对账核心流程伪代码
def daily_reconciliation(date: str):
    # 1. 获取本地订单
    local_orders = get_local_orders(date)
    # 2. 获取渠道账单
    channel_bills = download_alipay_bill(date)
    # 3. 建立索引
    channel_index = {b['trade_no']: b for b in channel_bills}
    # 4. 逐笔比对
    diffs = []
    for order in local_orders:
        bill = channel_index.get(order['out_trade_no'])
        if not bill:
            diffs.append({'type': 'LONG',       # 本地有,渠道无
                          'order': order})
            continue
        if order['amount'] != bill['amount']:
            diffs.append({'type': 'AMOUNT_MISMATCH',
                          'order': order, 'bill': bill})
    # 5. 找出渠道有但本地无的记录(短款)
    local_trade_nos = {o['out_trade_no'] for o in local_orders}
    for bill in channel_bills:
        if bill['trade_no'] not in local_trade_nos:
            diffs.append({'type': 'SHORT', 'bill': bill})
    return diffs

数据库表结构设计

对账系统需要几张核心表来记录对账过程和结果:

-- 对账批次表:记录每次对账的执行情况
CREATE TABLE reconciliation_batch (
    id            BIGINT AUTO_INCREMENT PRIMARY KEY,
    batch_date    DATE NOT NULL,          -- 对账日期
    channel       VARCHAR(32) NOT NULL,   -- 渠道: alipay/wechat
    total_local   INT DEFAULT 0,          -- 本地订单总数
    total_channel INT DEFAULT 0,          -- 渠道账单总数
    matched       INT DEFAULT 0,          -- 匹配成功数
    long_count    INT DEFAULT 0,          -- 长款数
    short_count   INT DEFAULT 0,          -- 短款数
    amount_diff   INT DEFAULT 0,          -- 金额差异数
    status        VARCHAR(16) DEFAULT 'pending',
    created_at    DATETIME DEFAULT NOW(),
    finished_at   DATETIME,
    INDEX(batch_date, channel)
);

-- 对账差异明细表
CREATE TABLE reconciliation_diff (
    id              BIGINT AUTO_INCREMENT PRIMARY KEY,
    batch_id        BIGINT NOT NULL,
    diff_type       VARCHAR(20) NOT NULL,  -- LONG/SHORT/AMOUNT_MISMATCH/STATUS_MISMATCH
    trade_no        VARCHAR(64),           -- 本地订单号
    channel_trade_no VARCHAR(64),          -- 渠道交易号
    local_amount    DECIMAL(10,2),
    channel_amount  DECIMAL(10,2),
    local_status    VARCHAR(16),
    channel_status  VARCHAR(16),
    remark          TEXT,
    resolved        TINYINT DEFAULT 0,     -- 是否已处理
    created_at      DATETIME DEFAULT NOW(),
    FOREIGN KEY(batch_id) REFERENCES reconciliation_batch(id)
);

Python 对账引擎实现

下面实现一个完整的对账引擎,支持支付宝和微信两种渠道:

# reconciliation_engine.py — 对账引擎
import csv
import io
from dataclasses import dataclass
from typing import List, Optional
from datetime import date, datetime

@dataclass
class OrderRecord:
    """本地订单记录"""
    trade_no: str
    amount: float
    status: str          # paid / refunded / closed
    paid_at: datetime
    product_name: str

@dataclass
class BillRecord:
    """渠道账单记录"""
    trade_no: str
    channel_trade_no: str
    amount: float
    status: str          # TRADE_SUCCESS / REFUND / TRADE_CLOSED
    time: datetime

@dataclass
class DiffRecord:
    diff_type: str       # LONG / SHORT / AMOUNT / STATUS
    trade_no: Optional[str] = None
    channel_trade: Optional[str] = None
    local_amount: Optional[float] = None
    channel_amount: Optional[float] = None
    local_status: Optional[str] = None
    channel_status: Optional[str] = None
    message: str = ''

class ReconciliationEngine:
    """对账引擎核心"""

    def parse_alipay_bill(self, csv_content: str) -> List[BillRecord]:
        """解析支付宝账单 CSV"""
        records = []
        reader = csv.DictReader(io.StringIO(csv_content))
        for row in reader:
            records.append(BillRecord(
                trade_no=row['merchant_order_no'],
                channel_trade_no=row['trade_no'],
                amount=float(row['order_amount']),
                status=row['trade_status'],
                time=datetime.strptime(row['gmt_create'], '%Y-%m-%d %H:%M:%S')
            ))
        return records

    def reconcile(self, local_orders: List[OrderRecord],
                     channel_bills: List[BillRecord]) -> List[DiffRecord]:
        """执行对账比对,返回差异列表"""
        diffs: List[DiffRecord] = []

        # 建立渠道账单索引
        bill_index = {b.trade_no: b for b in channel_bills}

        # 遍历本地订单
        for order in local_orders:
            bill = bill_index.pop(order.trade_no, None)

            if bill is None:
                # 长款:本地有记录,渠道没有 → 可能回调丢失
                diffs.append(DiffRecord(
                    diff_type='LONG',
                    trade_no=order.trade_no,
                    local_amount=order.amount,
                    local_status=order.status,
                    message='本地订单存在但渠道账单中未找到'
                ))
                continue

            # 比对金额(容差 0.01 元)
            if abs(order.amount - bill.amount) > 0.01:
                diffs.append(DiffRecord(
                    diff_type='AMOUNT',
                    trade_no=order.trade_no,
                    channel_trade=bill.channel_trade_no,
                    local_amount=order.amount,
                    channel_amount=bill.amount,
                    local_status=order.status,
                    channel_status=bill.status,
                    message=f'金额不一致: 本地={order.amount}, 渠道={bill.amount}'
                ))
                continue

            # 比对状态
            if not self._status_match(order.status, bill.status):
                diffs.append(DiffRecord(
                    diff_type='STATUS',
                    trade_no=order.trade_no,
                    channel_trade=bill.channel_trade_no,
                    local_amount=order.amount,
                    channel_amount=bill.amount,
                    local_status=order.status,
                    channel_status=bill.status,
                    message=f'状态不一致: 本地={order.status}, 渠道={bill.status}'
                ))

        # 剩余的渠道账单 → 短款(渠道有,本地无)
        for trade_no, bill in bill_index.items():
            diffs.append(DiffRecord(
                diff_type='SHORT',
                channel_trade=bill.channel_trade_no,
                channel_amount=bill.amount,
                channel_status=bill.status,
                message='渠道账单存在但本地未找到对应订单(可能是测试或盗刷)'
            ))

        return diffs

    def _status_match(self, local: str, channel: str) -> bool:
        """状态映射匹配"""
        mapping = {
            'paid':    ['TRADE_SUCCESS', 'TRADE_FINISHED'],
            'refunded': ['REFUND', 'TRADE_CLOSED'],
            'closed':  ['TRADE_CLOSED'],
        }
        return channel in mapping.get(local, [channel])

自动化修复策略

发现差异只是第一步,更重要的是自动修复。以下是针对不同差异类型的处理逻辑:

长款处理(本地有订单,渠道无账单)

这种情况通常是支付回调没有到达服务器,但钱确实扣了。处理方案:

  • 查询支付宝/微信的 trade查询 接口,确认交易的真实状态
  • 如果确认交易成功但本地未收到回调 → 补充回调处理,完成发货
  • 如果交易失败 → 将本地订单标记为「已关闭」并释放库存
# auto_fix.py — 自动修复长款
def fix_long_diff(diff: DiffRecord, alipay_sdk) -> str:
    """修复长款:主动查询支付宝确认交易状态"""
    result = alipay_sdk.query(trade_no=diff.trade_no)
    if result['code'] == '10000' and \
       result['trade_status'] == 'TRADE_SUCCESS':
        # 确实支付成功,补发货
        deliver_product(diff.trade_no)
        update_order_status(diff.trade_no, 'paid')
        return 'FIXED: 补发货成功'
    elif result['trade_status'] == 'TRADE_CLOSED':
        # 交易已关闭,标记本地订单关闭
        update_order_status(diff.trade_no, 'closed')
        release_inventory(diff.trade_no)
        return 'FIXED: 交易已关闭,本地订单同步关闭'
    else:
        return 'PENDING: 需要人工核查'

金额差异处理

这种情况比较少见但最严重。可能的原因:

  • 优惠券/满减导致本地记录的是原价,支付宝记录的是实际支付金额
  • 部分退款后状态未同步
  • 系统 bug 导致写入金额错误
金额差异通常不能自动修复,必须生成工单由财务人工复核。但可以自动汇总差异金额,在告警中直接提示"今日对账金额差异总计:¥1,234.56",方便快速定位。

短款处理(渠道有账单,本地无订单)

这是最危险的情况——用户付了钱但系统没记录。可能原因:

  • 用户支付后本地订单创建失败(数据库异常、服务重启等)
  • 测试交易或刷单
  • 支付后用户关闭了浏览器但支付已完成

处理方案:先通过渠道的 trade_no 查询交易详情,确认买家信息。如果能找到对应买家(通过付款方账号或 userId),则补建订单并发货。

# 短款修复:通过渠道交易号补建订单
def fix_short_diff(diff: DiffRecord, alipay_sdk) -> str:
    """修复短款:获取交易详情,尝试补单"""
    detail = alipay_sdk.query(trade_no=diff.channel_trade)
    if detail['code'] != '10000':
        return 'PENDING: 支付宝查询失败,需人工核查'

    buyer_id = detail.get('buyer_id')
    # 在本地查找是否有未完成的待支付订单
    pending = find_pending_order(buyer_id, detail['total_amount'])
    if pending:
        # 找到匹配的待支付订单,补充支付回调
        complete_payment(pending['id'], detail)
        deliver_product(pending['id'])
        return 'FIXED: 找到待支付订单并补发货'
    else:
        # 无法匹配,记录日志等待人工处理
        create_manual_ticket(diff)
        return 'PENDING: 无法自动匹配,已生成人工工单'

定时调度与告警

对账通常是每天凌晨自动执行。我们用 Linux cron 来调度:

# 每天凌晨 3:00 执行对账
0 3 * * * cd /opt/payment/reconciliation && python3 run_reconciliation.py --date=yesterday

# run_reconciliation.py 核心逻辑
import sys, json
from datetime import date, timedelta

def main():
    target_date = date.today() - timedelta(days=1)
    if '--date' in sys.argv:
        target_date = parse_date(sys.argv[sys.argv.index('--date') + 1])

    engine = ReconciliationEngine()
    orders = fetch_local_orders(target_date)
    bills = download_channel_bills(target_date, channel='alipay')
    diffs = engine.reconcile(orders, bills)

    # 写入数据库
    batch_id = create_batch(target_date, len(orders), len(bills), len(diffs))
    for d in diffs:
        save_diff(batch_id, d)

    # 自动修复
    auto_fix(diffs)

    # 发送对账报告到钉钉/飞书/微信
    report = generate_report(batch_id, diffs)
    send_alert(report)

if __name__ == '__main__':
    main()

对账报告示例(钉钉机器人消息格式):

# 钉钉 Webhook 告警
import requests, json

def send_alert(report: dict):
    webhook_url = 'https://oapi.dingtalk.com/robot/send?access_token=xxx'
    msg = {
        'msgtype': 'markdown',
        'markdown': {
            'title': '对账报告',
            'text': f"""## 📊 对账报告 ({report['date']})

**渠道**: 支付宝
**本地订单**: {report['total_local']} 笔
**渠道账单**: {report['total_channel']} 笔
**匹配成功**: {report['matched']} 笔 ✅
**长款**: {report['long_count']} 笔 ⚠️
**短款**: {report['short_count']} 笔 ❗
**金额差异**: {report['amount_diff']} 笔 🔴
**自动修复**: {report['fixed']} 笔 ✅
**待人工处理**: {report['pending']} 笔 👤

[查看详情](https://admin.greenfield.ltd/reconciliation/{report['batch_id']})
"""
        }
    }
    requests.post(webhook_url, json=msg)

生产环境最佳实践

  • T+1 对账:不要在当天零点立即对账,建议延迟到凌晨 3:00 之后,确保渠道账单已生成完毕。支付宝账单一般在 02:00-03:00 生成
  • 分渠道独立运行:支付宝和微信的对账应分开执行,互不影响。每个渠道有自己的账单解析器和重试策略
  • 对账结果持久化:每次对账结果写入 reconciliation_batchreconciliation_diff 表,至少保留 90 天,方便追溯历史差异
  • 渐进式告警:差异数少于 5 笔时发钉钉/飞书消息;超过 5 笔时追加电话告警;超过 50 笔时触发 P0 级事件响应
  • 重试机制:如果对账过程中支付宝账单下载失败,应自动重试 3 次(间隔 5 分钟),全部失败后告警通知运维
  • 幂等性:同一天的对账可以重复执行而不会产生重复记录。使用 batch_date + channel 做唯一索引,已存在的批次会先删除旧记录再写入

总结

支付对账是支付系统的最后一道安全防线。一个完整的对账系统应该做到:每天自动跑、差异自动分、长款短款自动修、无法处理的自动转人工。没有对账的支付系统,资金安全就无从谈起。

在实际业务中,对账系统的质量直接决定了你能否及时发现支付通道的问题、能否在用户投诉前主动修复异常。我们源码商城的 转卡码系统 v3 内置了完整的对账模块,支持支付宝和微信的双渠道自动对账、差异自动修复和钉钉告警,帮助上千位站长守住了资金安全的底线。