SMARTPAY

菲律宾支付对账系统怎么搭建?订单、通道、银行三方数据核对的技术方案(2026)

我在宿务负责一家跨境电商团队的后端,菲律宾本地收款走的是 GCash、Maya 和 QRPH,结算进本地银行账户。最早对账是财务同事每周导三份表,在电子表格里手工比对,一到大促就彻底崩盘:订单几万笔,通道报表和银行流水各有各的格式,差异一堆却说不清哪笔对哪笔。后来我们花了三个迭代把对账做成了系统。关于对账为什么重要、流程上怎么分工,菲律宾收款对账怎么做和GCash 商户对账怎么做已经讨论过;这篇文章专讲怎么"搭":数据从哪里来、底表怎么设计、匹配规则怎么分层、差异怎么闭环,以及菲律宾通道在时间窗口上有哪些特殊之处。

为什么一定是三方,而不是两方

很多团队的对账只有两方:自己的订单表和支付通道的交易报表。两方对账能回答"每笔订单有没有收到钱",但回答不了"钱有没有真的进到我的银行账户,进了多少"。通道报表里的成功交易,到结算时会减去手续费、合并成批次、受结算周期和节假日影响,再以几笔汇总金额的形式出现在银行流水里。中间任何一环出问题,两方对账都看不出来。所以完整的对账是三方:订单系统的交易记录、支付通道或网关的交易与结算报表、银行账户的入账流水。三方之间有两条核对关系:订单与通道逐笔核对,通道结算批次与银行入账按批次核对。两条关系都对平,才能说这个月的钱是清楚的。

三方对账要回答的问题也分三层:交易层回答"每一笔订单是否有对应的成功支付,金额是否一致";结算层回答"每个结算批次的净额是否与银行入账一致,手续费是否与合同一致";账务层回答"期末应收、在途、已入账三者之和是否等于订单总额"。系统设计时,这三层对应三组核对任务,不能混在一个流程里。

数据源:三方数据分别怎么拿、拿什么

数据源的质量决定了整个系统的上限。下表是我们目前接入的三方数据及其获取方式,注意每一类数据的"关键字段",它们是后面做匹配的钥匙。

数据源获取方式频率关键字段常见坑
订单系统交易记录内部数据库直读或事件流实时商户订单号、金额、币种、通道、创建与支付时间、本地状态状态由回调驱动,回调丢失会让本地状态滞后
网关实时回调Webhook 推送,落盘原始报文实时商户订单号、网关流水号、通道流水号、支付时间、状态重复推送、乱序、验签失败;详见GCash 商户回调怎么做
网关交易报表报表接口拉取或后台导出 CSV每日网关流水号、通道流水号、金额、手续费、状态、结算批次号报表切日时间与你的业务日不一致;状态终态可能晚于回调
网关结算报表报表接口或后台导出每个结算周期结算批次号、批次总额、手续费合计、净额、结算日期、目标账户一个批次可能对应多个业务日,或一个业务日拆成多个批次
银行入账流水网银导出 CSV、银行对账单,或银行企业接口每日入账时间、金额、付款方名称、备注或参考号备注字段可能被截断,InstaPay 与 PESONet 入账的参考号格式不同

两点经验。第一,原始数据一律原样落盘,解析和标准化放在下一步,这样任何时候出现争议都能回到原始报文。第二,银行流水是三方中最不规整的一方,参考号、付款方名称的格式因银行而异,我们单独维护了一套按银行区分的解析规则,而不是指望用一个正则搞定所有情况。

数据模型:一张统一的对账底表

三方数据格式各不相同,系统的第一步是把它们标准化进同一张底表,之后所有匹配都在这张表上进行。核心思路是:每一条来源记录都变成一行,带上来源标识、业务日、匹配键和金额,匹配结果和差异状态单独记录。下面是我们底表的简化 DDL:

-- 统一对账底表(简化)
CREATE TABLE recon_entry (
  id              BIGSERIAL PRIMARY KEY,
  source          VARCHAR(16) NOT NULL,     -- ORDER / GATEWAY_TXN / GATEWAY_SETTLE / BANK
  biz_date        DATE        NOT NULL,     -- 统一后的业务日
  merchant_ref    VARCHAR(64),              -- 商户订单号(订单、回调、交易报表有)
  gateway_ref     VARCHAR(64),              -- 网关流水号
  channel_ref     VARCHAR(64),              -- 通道流水号(GCash/Maya/QRPH 侧)
  settle_batch    VARCHAR(64),              -- 结算批次号(交易报表、结算报表、银行备注解析得到)
  channel         VARCHAR(16),              -- GCASH / MAYA / QRPH / BANK
  amount          NUMERIC(18,2) NOT NULL,   -- 交易金额或入账金额
  fee             NUMERIC(18,2),            -- 手续费(仅通道侧有)
  status          VARCHAR(16),              -- 来源方的状态
  occurred_at     TIMESTAMPTZ NOT NULL,
  raw_ref         BIGINT,                   -- 指向原始报文表
  match_group     BIGINT,                   -- 匹配成功后归入同一组
  recon_status    VARCHAR(16) DEFAULT 'OPEN' -- OPEN / MATCHED / PENDING / EXCEPTION / RESOLVED
);
CREATE INDEX idx_recon_merchant_ref ON recon_entry (merchant_ref);
CREATE INDEX idx_recon_batch ON recon_entry (settle_batch);
CREATE INDEX idx_recon_open ON recon_entry (recon_status, biz_date);

这张表有两个设计要点。一是 biz_date 必须是"统一后"的业务日,各来源的原始时间先转到同一时区,再按你定义的切日规则换算,否则跨日交易会制造大量假差异。二是匹配键不止一个:订单与网关交易用 merchant_ref 或 gateway_ref,网关交易与结算用 settle_batch,结算与银行用 settle_batch 加金额加日期,所以三组键都要保留。

匹配引擎:三层规则,从精确到模糊

匹配不能只有一条规则。我们把它分成三层,逐层放宽,每一层匹配成功的记录就打上 match_group 并退出后续层。第一层是精确键匹配:订单与网关交易按 merchant_ref 一对一匹配,金额必须相等,状态都为成功。第二层是组合键匹配:当 merchant_ref 缺失或被截断时,用 channel_ref 加金额加支付时间窗口匹配,时间窗口按通道特性设置,这一层会产生少量误匹配,所以结果标记为"待确认"而非"已匹配"。第三层是批次匹配:结算报表的批次净额与银行入账按批次号、金额、日期匹配;批次号在银行备注里解析不出来时,退化为金额加日期窗口匹配。下面是第一、二层的逻辑骨架:

// 匹配引擎(Node.js 示意)
async function matchOrdersToGateway(bizDate) {
  // 第一层:精确键
  await db.query(`
    UPDATE recon_entry o
    SET match_group = g.id, recon_status = 'MATCHED'
    FROM recon_entry g
    WHERE o.source = 'ORDER' AND g.source = 'GATEWAY_TXN'
      AND o.biz_date = $1 AND g.biz_date = $1
      AND o.merchant_ref = g.merchant_ref
      AND o.amount = g.amount
      AND o.status = 'PAID' AND g.status = 'SUCCESS'
      AND o.recon_status = 'OPEN' AND g.recon_status = 'OPEN'
  `, [bizDate]);

  // 第二层:组合键 + 时间窗口,结果标记待确认
  const open = await db.query(
    `SELECT * FROM recon_entry WHERE source='ORDER' AND biz_date=$1 AND recon_status='OPEN'`, [bizDate]);
  for (const o of open.rows) {
    const cand = await db.query(`
      SELECT id FROM recon_entry
      WHERE source='GATEWAY_TXN' AND recon_status='OPEN'
        AND channel = $1 AND amount = $2
        AND occurred_at BETWEEN $3::timestamptz - interval '30 minutes'
                            AND $3::timestamptz + interval '30 minutes'
      LIMIT 2`, [o.channel, o.amount, o.occurred_at]);
    if (cand.rowCount === 1) {
      await markPending(o.id, cand.rows[0].id);   // 唯一候选,待人工或次日确认
    } else {
      await markException(o.id, cand.rowCount === 0 ? 'ORDER_ONLY' : 'AMBIGUOUS');
    }
  }
}

第三层批次匹配的逻辑类似,只是对象换成结算报表与银行流水,键换成 settle_batch 与金额、日期。要特别注意结算净额等于批次总额减手续费,银行入账对应的是净额而不是总额,很多团队第一次做批次对账对不上,原因就是拿总额去比入账。

差异处理:异常队列与工单闭环

匹配引擎跑完,剩下的 OPEN 和 EXCEPTION 记录就是差异。差异处理的关键不是"找到差异",而是"每一条差异都有归属和终态"。我们按差异类型定义了自动处理与人工处理的边界:

差异类型表现常见原因自动处理人工处理
订单单边本地有支付成功记录,网关无对应交易本地误标状态、测试数据混入、网关报表延迟次日报表到达后自动重跑匹配连续两日仍单边则进工单核查
网关单边网关有成功交易,本地无记录或为未支付回调丢失或验签失败、订单号生成异常自动调用订单查询接口补状态,并补写本地记录补状态失败的转开发排查
金额差两边都有,金额不等手续费扣法理解错误、付款人少付或多付、币种换算差额等于合同手续费的自动归类为手续费其余差额转财务确认退补
批次差结算批次净额与银行入账不等批次拆分、跨日合并、银行侧费用、退款冲抵尝试多批次合并匹配合并后仍不平的联系网关对账专员
时间差两边都有且金额一致,但业务日不同切日规则不一致、跨时区、通道终态延迟允许业务日相差一日的自动匹配并标注超过一日的进入复核

每条差异进入异常队列后,系统会生成一张工单,带上三方的原始记录链接、差异类型、建议动作和责任人。工单只有三种终态:已自动修复、已人工确认、已核销。所有终态都写回 recon_entry 的 recon_status,并保留处理人和时间。这样月末财务要的不是"对账结果",而是一份差异台账:每条差异是什么、为什么、怎么处理的,审计和对外争议时都能直接拿出来用。

菲律宾通道的时间窗口:切日、周期与节假日

对账系统在菲律宾落地,最容易被低估的是时间问题。第一,不同通道的结算周期不同,有的支持当日结算,有的按工作日滚动,同一个业务日的交易可能分散在不同的结算批次里;结算周期与银行链路的细节可参考GCash 商户结算怎么走。第二,InstaPay 全天运行,PESONet 则按批次清算窗口处理,银行入账的时间分布完全不同,批次匹配的时间窗口要按通道分别设置。第三,菲律宾的法定假日与地方性假日较多,结算日遇假顺延,系统里要维护一份节假日日历,否则每逢假日就会冒出一批假的批次差。第四,网关报表的切日时间与你的业务日切日可能不一致,这个偏差要在标准化进底表时就处理掉,而不是在匹配阶段用放宽窗口去凑。

我们的做法是在系统里显式建模"结算日历":每个通道一条规则,描述它的切日时间、结算周期、节假日处理方式,批次匹配时按规则计算预期入账日,再在预期日前后设窄窗口匹配。这样做之后,时间差类的假差异明显减少,人工复核的工作量才真正降下来。

还有一个前提经常被忽略:对账的基石是通道方给出的结算报表,而结算报表的可信度取决于谁在持有和结算你的资金。承接商户资金清算与结算的主体,应当是持有 BSP 颁发的 OPS 或 EMI 资质的机构,这类机构有义务提供规范的交易与结算记录。如果服务商给不出结构化的结算报表,或者结算账户不是其公司名下的受监管账户,对账系统再精巧也只是在核对一份无法验证的数据。

落地路径:从脚本到系统的三个阶段

不建议一上来就做完整系统。我们走了三个阶段。第一阶段是脚本化:用定时任务拉三方数据,做第一层精确匹配,把差异导出成表给财务,目标只是替代手工比对,一个迭代就能上线。第二阶段是服务化:底表、三层匹配、异常队列、工单全部进系统,财务从"做对账"变成"处理差异",这个阶段的重点是把切日、结算日历、按银行的解析规则做扎实。第三阶段是可观测:对账指标进监控,匹配率、差异量、差异平均处理时长按通道和门店维度展示,差异量突增自动告警,这时候对账就从财务工具变成了支付系统质量的体检仪。报表与数据维度的设计可以参考菲律宾支付交易数据报表怎么用。

常见问题

没有银行企业接口,银行流水只能手工导出,还能做三方对账吗?

可以。银行流水的获取频率可以低于另两方,每日或每个结算周期导出一次 CSV 导入系统即可,关键是解析规则要按银行维护好,并保证导入的幂等性,重复导入不会产生重复记录。

对账应该用回调数据还是网关报表数据?

都要用,但角色不同。回调用于实时更新本地状态,报表用于对账的权威依据。回调会丢、会重复,报表是通道方的正式记录,以报表为准、用回调做实时性补充是比较稳妥的分工。

手续费怎么对?

把手续费当作一类独立的差异处理:金额差恰好等于按合同计算的手续费时自动归类,不等时才进人工队列。这样既能发现通道方的计费异常,也不会让正常手续费淹没真正的差异。

多通道、多门店、多币种怎么扩展?

底表里保留 channel、门店标识和币种字段,匹配规则按通道参数化,结算日历按通道配置,门店和币种只作为分组维度,不改变匹配逻辑。设计时把"规则"和"数据"分开,扩展就不需要改代码。

差异多久没处理算超时?

按差异类型分别定义。单边账给报表延迟留出一到两个业务日的自动重跑窗口,金额差和批次差应当在下一个结算周期前处理完,以免滚雪球。具体时限按你的结算周期和财务要求定,系统里把它做成可配置项。

有了网关的商户后台对账功能,还需要自建吗?

网关后台通常只能做订单与通道的两方核对,银行入账那一段仍然要自己做。如果你只接一个通道、交易量小,后台足够;多通道、多账户或有审计要求的商户,自建或引入对账系统是必要的。

写在最后

对账系统不是财务工具,而是支付系统的质量底座:它能告诉你回调丢了多少、配置错在哪里、通道方有没有多扣钱、钱什么时候真正到了账上。先把三方数据原样落盘,再标准化进统一底表,用三层规则匹配,把差异做成有归属、有终态的工单,最后把菲律宾通道的时间规则显式建模,这条路径我们走了三个迭代,踩过的坑基本都写在上面了。如果你正在设计或重构对账系统,想对一下底表结构或匹配规则,欢迎通过 Telegram 联系@LouisSMARTPAY,或邮件联系[email protected]。

相关推荐