资讯动态

收银台设计核心:状态机、幂等与回调处理

发布时间:2026/10/15 7:54:56 来源:尧图企业网站定制
简介《图解收银台支付系统承上启下的关键应用》是《百图解码支付系统设计与实现》专栏第11篇的整理修订版面向支付系统研发、架构设计人员及想深入理解支付流程的读者。内容以收银台为切入点用大量手绘风格图示直观呈现收银核心与支付引擎的协同关系系统拆解支付咨询与支付受理两大阶段涵盖基础检查、资产/渠道/额度咨询、支付方式组装与排序以及提交支付后的风控校验、渠道路由、真实扣款和结果轮询等关键环节。同时梳理了快捷支付、代扣、卡支付、网银、VA、OTC等常见支付方式及其适用场景帮助读者建立从业务到设计的整体认知。压缩包内共1个PDF文件容量5.2MB内容精炼且配图丰富既可作为支付系统初学者的入门读物也能为资深研发人员提供架构设计参考。目前已有101人学习/下载是理解支付系统前端核心模块的实用资料。1. 图解收银台承上启下到底是承什么、启什么单看《图解收银台支付系统承上启下的关键应用_V20240124.pdf》这个标题很多人第一反应是这不就是那个支付页面嘛。我做支付系统这些年最深的体会是收银台确实会渲染到页面上但它本质是支付系统里标准的技术中间层。上游交易系统把订单、金额、买家信息交过来下游渠道网关把支付请求送到各家支付渠道收银台在中间负责选渠道、发起支付、接回调、同步状态掉单时还得主动把账找平。它不直接碰钱但所有和钱相关的订单状态都在它这里收敛。这份标题里最值得琢磨的是承上启下四个字以及图解背后的设计主线谁在什么时机调谁、状态怎么流转、回调失败怎么办。下面我把这类图解文档通常会讲清楚的内容翻译成一套可落地的模块边界、核心表结构和核心代码。适合正在搭收银台的团队也适合第一次接手支付系统、想搞明白这个中间层到底该管多少事的后端开发者。照着走一遍你手上会多出一版可以评审、可以排期的收银台设计方案。2. 从图解文档到系统模块边界、核心表与两个关键时序一份合格的收银台图解通常先画一张总览图应用端和商户后台在左渠道网关在右收银台在中间靠上下方挂着回调中心、延迟队列和定时任务。看懂这张图等于回答三个问题——收银台从上游拿什么、自己处理什么、向下游输出什么。这里先给结论收银台不拥有订单它只处理支付单。一笔业务订单可能对应多张支付单比如用户先选了A渠道没付成功换B渠道又来一笔收银台脑子里只有支付单和支付单对应的状态。所以从图上最先要拉出来的边界是交易域负责订单语义渠道域负责渠道报文收银台负责两者之间的所有转换和状态仲裁。2.1 收银台的上下边界承交易域、启渠道域把承上启下这条线翻译成边界对应三件事。上游交易域会传 order_no、业务类型、应付金额、买家标识、过期时间收银台用它创建支付单自身维护 payment_no支付单号做渠道预检查、路由选择、调用渠道网关、接收回调、推动状态流转下游渠道网关只认渠道侧的交易号和签名收银台要把它返回的 channel_trade_no、支付串、错误码翻译成内部状态变更。交界处最容易翻车的是支付结果和结算结果被混为一谈。交易域认为支付成功就发货但收银台的 SUCCESS 只表示渠道侧受理成功、资金在途不代表清结算已经落账。设计上要把这两个结果分开向上游透出否则订单发货了、对账又追回整个业务链路就乱了。我一般会在支付结果事件里带上资金阶段字段让交易域自己决定是否立刻履约。关注点交易域收银台渠道网关核心对象订单 order_no支付单 payment_no渠道交易号 channel_trade_no状态口径待支付/已支付/已取消INIT/PAYING/SUCCESS/CLOSED/FAIL渠道各自的成功/失败/处理中对外事件订单已支付支付结果事件 差错事件回执 / 异步回调收银台接收的输入永远带业务上下文但对外只输出两个可靠结论支付成功资金已在途或者支付失败未产生支付。中间态全部收在收银台内部消化这是它和交易域之间最清晰的契约。2.2 一张架构图拆开收银台服务、渠道网关与回调中心大多数图解文档的第二张图是部署视角。常见做法是拆成四个部分收银台服务承担下单、支付、查询、关闭它是唯一碰支付单表的服务渠道网关给每个渠道做一个适配器统一处理签名、加密、报文转换和异步结果解析收银台不直接依赖渠道SDK回调中心接收渠道异步通知验签后转交收银台同时管理通知重试和死信定时任务和延迟队列负责扫描超时未支付、支付成功但未通知的支付单触发主动查单和关单。这里有一个架构取舍要注意如果图解里画了收银台直连多个渠道的连线落地时最好改成收银台只连渠道网关。渠道SDK的版本冲突、线程模型、超时调优都集中在网关这一层改动面不会扩散到收银台核心链路。某支付团队曾经图省事让收银台直连渠道一个渠道SDK升级导致加载冲突整个收银台启动失败——这种坑在架构图阶段看不出来等接第五个渠道时才会爆发。2.3 复现图中的时序先定义支付状态机图解里的时序图核心就是状态机。我按最常用的版本拆出六个状态INIT已创建未发起支付、PAYING已请求渠道等待结果、SUCCESS支付成功终态、FAIL支付失败终态、CLOSED超时关闭终态、REFUNDING退款中只在逆向链路出现不影响正向主流程。从到触发条件注意事项INITPAYING收到支付请求并成功调起渠道同一支付单不能并发调两个渠道PAYINGSUCCESS收到成功回调或主动查单确认成功必须验金额、验渠道交易号PAYINGFAIL渠道明确返回失败渠道扣了钱但没确认成功走人工PAYINGCLOSED超时且渠道侧确认无支付关单前要主动查一次渠道INITCLOSED超时未支付渠道侧没有单只关本地不调渠道这条状态机的关键约束是SUCCESS 只能从 PAYING 进入且进入后不允许再变化。代码里所有状态更新都要带 where status旧状态靠数据库行锁保证并发下只有一条变更生效。图解时序图画到渠道回调后支付单变 SUCCESS落地就是一条带状态条件的 update 语句。状态机这条主线拉通收银台的代码边界就清楚了。3. 收银台核心代码落地下单、支付与回调的最小闭环边界和状态机定完之后下一步是让收银台真正跑起来。我会按先建表、再写创建支付单、再写发起支付、最后写回调处理的顺序展开这是一套可以直接跑的最小闭环也是接手一个收银台项目时最值得先读的四段代码。3.1 支付单表设计字段、索引与乐观锁支付单表是收银台的唯一事实来源。金额一律用 BIGINT 存分不用浮点数这是避免金额玄学问题的最基本手段。expire_time 用 datetime 统一按 UTC 或东八区存代码里保证先转化再写入避免不同环境时区错乱导致超时关单误判。CREATE TABLE payment_order ( id bigint(20) NOT NULL AUTO_INCREMENT, payment_no varchar(32) NOT NULL COMMENT 收银台支付单号对外唯一, order_no varchar(32) NOT NULL COMMENT 业务订单号来自交易域, biz_code varchar(20) NOT NULL COMMENT 业务线标识, buyer_id varchar(64) NOT NULL COMMENT 买家标识, pay_amount bigint(20) NOT NULL COMMENT 支付金额单位分, fee_amount bigint(20) NOT NULL DEFAULT 0 COMMENT 渠道手续费单位分, channel_code varchar(20) NOT NULL COMMENT 最终选择的渠道, channel_trade_no varchar(64) DEFAULT NULL COMMENT 渠道交易号, status varchar(16) NOT NULL COMMENT 状态机INIT/PAYING/SUCCESS/FAIL/CLOSED, expire_time datetime NOT NULL COMMENT 支付过期时间, notify_url varchar(255) DEFAULT NULL COMMENT 支付结果通知地址, notify_count int(11) NOT NULL DEFAULT 0 COMMENT 已通知上游次数, notify_time datetime DEFAULT NULL COMMENT 最近一次通知成功时间, version int(11) NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, create_time datetime NOT NULL COMMENT 创建时间, update_time datetime NOT NULL COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_payment_no (payment_no), UNIQUE KEY uk_order_no_biz (order_no,biz_code), KEY idx_status_expire (status,expire_time), KEY idx_channel_trade_no (channel_trade_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT收银台支付单表;字段说明uk_order_no_biz 唯一键是幂等的最后一道兜底同一个业务线同一个订单只能创建一张支付单并发插入时谁后到谁撞锁idx_status_expire 给超时扫描和主动查单服务用避免全表扫描channel_trade_no 单独建索引对账和渠道侧查询都靠它。version 字段用于乐观锁在状态流转冲突多的时候兜底普通场景靠条件更新就够。3.2 创建支付单幂等与渠道预检查创建支付单的逻辑不复杂但幂等必须做扎实。这里强调一点幂等不能只靠先查后插因为并发下两个请求可能同时查到记录不存在。真正兜底的是 uk_order_no_biz 唯一键插入冲突后捕获重复键异常再查一次返回已有支付单即可。Transactional public PaymentDTO createPayment(PaymentCreateReq req) { // 基础校验金额必须为正且不超过单笔上限 if (req.getAmount() null || req.getAmount() 0 || req.getAmount() payLimit(req.getBizCode())) { throw new BizException(PAY_AMOUNT_INVALID, 金额不在允许范围); } // 幂等同一业务线同一订单已有支付单则直接返回 PaymentOrder exist paymentOrderMapper.selectByOrderNo(req.getOrderNo(), req.getBizCode()); if (exist ! null) { return buildDTO(exist); } PaymentOrder order new PaymentOrder(); order.setPaymentNo(genPaymentNo(req.getBizCode())); order.setOrderNo(req.getOrderNo()); order.setBizCode(req.getBizCode()); order.setBuyerId(req.getBuyerId()); order.setPayAmount(req.getAmount()); order.setStatus(INIT); order.setExpireTime(genExpireTime(req.getExpireInMinutes())); try { paymentOrderMapper.insert(order); } catch (DuplicateKeyException e) { // 并发创建时唯一键兜底返回已有的支付单 return buildDTO(paymentOrderMapper.selectByOrderNo(req.getOrderNo(), req.getBizCode())); } return buildDTO(order); }参数说明req.getAmount() 是 long 类型、单位分payLimit 从渠道配置表读取每个业务线可以配不同的单笔上限genPaymentNo 一般拼成业务码日期随机数校验位不要用自增ID避免被外部猜到单号规律expireInMinutes 默认给 15 分钟扫码类场景可以放宽到 30 分钟。这段代码表面上只是建了一条 INIT 记录实际把幂等、限额、单号规则全部收口了。3.3 发起支付与回调处理状态机拦住重复回调支付动作的关键只有一行把 INIT 置成 PAYING只有这一步成功才允许调渠道。这样两个并发支付请求同时到达时第二个必然失败不会出现一单同时调两个渠道的情况。public PayResult pay(String paymentNo, String channelCode) { PaymentOrder order paymentOrderMapper.selectByPaymentNo(paymentNo); if (order null) { throw new BizException(PAYMENT_NOT_FOUND, 支付单不存在); } // 状态机只有 INIT 允许发起支付CAS 保证并发安全 int affected paymentOrderMapper.casStatus(paymentNo, INIT, PAYING); if (affected 0) { throw new BizException(PAYMENT_STATUS_NOT_INIT, 支付单已处理请勿重复发起); } ChannelResponse response channelGateway.pay(buildChannelRequest(order, channelCode)); if (response.isSuccess()) { paymentOrderMapper.updateChannelInfo(paymentNo, response.getChannelTradeNo(), channelCode, response.getPayParams()); return PayResult.waiting(response.getPayParams()); } // 渠道明确失败置为 FAIL 并释放支付单 paymentOrderMapper.casStatus(paymentNo, PAYING, FAIL); return PayResult.failed(response.getFailReason()); }casStatus 对应的 SQL 是 update payment_order set statusPAYING where payment_no? and statusINIT。先改状态再调渠道是为了避免支付单一直挂在 INIT 时被超时关单任务误杀改成 PAYING 之后再调渠道就算渠道超时也有主动查单兜底。channelCode 可以由调用方传也可以走路由这里预留手动指定方便联调时固定渠道。回调处理是收银台最容易被重推击穿的地方。渠道推回调不是一次性的重复通知、乱序通知都是常态所以处理逻辑必须天然幂等且不允许把终态改坏。public NotifyResult handleNotify(String paymentNo, String channelTradeNo, long amount, String channelCode) { PaymentOrder order paymentOrderMapper.selectByPaymentNo(paymentNo); if (order null) { return NotifyResult.reject(PAYMENT_NOT_FOUND); } // 金额校验渠道回调金额必须与本地一致 if (order.getPayAmount() ! amount) { return NotifyResult.reject(AMOUNT_NOT_MATCH); } // 状态机只有 PAYING 才能变 SUCCESS int affected paymentOrderMapper.casStatusToSuccess(paymentNo, PAYING, channelTradeNo, channelCode); if (affected 1) { notifyProducer.send(PaymentSuccessEvent.build(order)); return NotifyResult.accept(); } // 已成功过幂等返回成功避免渠道重推导致重复处理 if (SUCCESS.equals(order.getStatus())) { return NotifyResult.accept(); } // CLOSED/FAIL 之后渠道还回调记录差错单交给对账 riskRecordMapper.insert(RiskRecord.build(order, channelTradeNo)); return NotifyResult.reject(STATUS_INVALID); }逻辑说明验签通常在渠道网关统一收口做业务层只校验金额和单号casStatusToSuccess 对应 update ... set statusSUCCESS, channel_trade_no? where payment_no? and statusPAYING影响行数为 1 才是真正完成支付。第 4 步尤其重要渠道重推时不能因为状态已成功就把回调丢掉或返回失败否则渠道会一直重推。第 5 步把异常状态下的回调落成差错记录给对账和人工留证据。3.4 让回调可靠重试表与延迟队列兜底回调处理完之后还需要一张通知记录表把每一次回调留痕同时支撑重试和排查。我通常会让回调中心先落一条 payment_notify_record再执行业务处理处理失败就按重试计划捞起重放。CREATE TABLE payment_notify_record ( id bigint(20) NOT NULL AUTO_INCREMENT, payment_no varchar(32) NOT NULL, channel_code varchar(20) NOT NULL, notify_content text COMMENT 渠道原始回调报文, handle_status varchar(16) NOT NULL COMMENT SUCCESS/FAIL/RETRY, retry_count int(11) NOT NULL DEFAULT 0, next_retry_time datetime DEFAULT NULL, create_time datetime NOT NULL, update_time datetime NOT NULL, PRIMARY KEY (id), KEY idx_payment_no (payment_no), KEY idx_next_retry (handle_status,next_retry_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT回调处理记录;重试策略我一般按 1s、5s、30s、5min、30min 递增超过 8 次置为 FAIL最后对账阶段统一捞。与此同时延迟队列要负责两件定时兜底的事PAYING 超过 2 分钟没收到回调的支付单主动调用渠道查询接口INIT 且超过 expire_time 的支付单做关单。这两条兜底链路和回调链路共用同一段状态流转代码入口不同、逻辑相同才不会出现回调改成功、查单改失败的分叉。4. 渠道路由与限额控制把选渠道从人肉变成策略收银台接了多个渠道之后下一个绕不开的问题是这笔支付走哪条渠道。渠道路由做得好支付成功率能提升两三个点做不好就会出现看起来配置齐全但大额单一直失败的诡异现象。4.1 为什么需要路由限额、费率与成功率三者权衡没有完美的渠道。银行类渠道通常限额高、费率低但接口慢、对异常场景处理保守聚合支付类渠道体验好、成功率高但单笔限额低、费率相对贵。路由要做的不是选最好的渠道而是结合这次支付单的金额、买家特征、渠道实时可用状态选一条成功率高且成本可接受的渠道。举个常见例子一笔 500 元的订单A 渠道限额 2000、费率 0.2%、最近成功率 98%B 渠道限额 1000、费率 0.6%、最近成功率 99.5%。只按成功率选 B每笔多花 2 元只按费率选 A渠道一旦波动失败率立刻上升。实际路由要把两者加权还要能自动降级满足平时省钱、渠道抖动时优先保成功。4.2 路由规则配置表与条件表达路由规则我习惯用过滤 打分两段式先按规则过滤掉不能用的渠道再对剩下的渠道打分排序。规则表只表达什么条件命中什么渠道不掺打分逻辑这样配置同学也能维护。CREATE TABLE channel_route_rule ( id bigint(20) NOT NULL AUTO_INCREMENT, rule_name varchar(50) NOT NULL, priority int(11) NOT NULL DEFAULT 0 COMMENT 优先级数值越大越优先, channel_code varchar(20) NOT NULL COMMENT 命中后使用的渠道, amount_min bigint(20) NOT NULL DEFAULT 0 COMMENT 适用金额下限单位分, amount_max bigint(20) NOT NULL DEFAULT 0 COMMENT 适用金额上限单位分0表示不限, buyer_tag varchar(64) DEFAULT NULL COMMENT 买家标签例如 v1/v2 灰度组, time_start varchar(4) DEFAULT NULL COMMENT 生效时段开始 HHmm, time_end varchar(4) DEFAULT NULL COMMENT 生效时段结束 HHmm, status tinyint(4) NOT NULL DEFAULT 1, create_time datetime NOT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT渠道路由规则;这套表最适合表达小额走 A、大额走 B、对某个灰度组走 C、夜间维护时段走 D这类规则。注意 amount_max 用 0 表示不限避免和上限也是 0的语义冲突。配置变更要支持热加载渠道网关配置中心推送后 1 分钟内生效不要因为路由缓存导致新渠道迟迟用不上。4.3 一个可抄的加权路由伪代码路由核心代码不复杂关键是权重和 fallback 边界。下面这段伪代码可以直接落成线上服务权重系数先用经验值等有历史数据再校准。def select_channel(candidates, pay_req): scored [] for ch in candidates: # 第一层过滤掉限额不匹配、维护中、被临时降权的渠道 if not precheck(ch, pay_req): continue # 第二层打分排序权重先人工粗配再用历史数据滚动校准 score 0.0 score ch.priority * 10.0 # 人工指定的强偏好 score ch.success_rate * 2.0 # 近30分钟真实成功率取值0~1 score - ch.fee_rate * 0.5 # 费率千分位数值越小越好 if pay_req.get(buyer_tag) v1: # 灰度组直接抬权重 score 5.0 scored.append((score, ch)) if not scored: raise RouteError(no_channel_after_filters) scored.sort(keylambda x: x[0], reverseTrue) last_error None for _, ch in scored: try: resp call_gateway(ch.channel_code, pay_req) if resp.ok: return ch.channel_code, resp.channel_trade_no except ChannelException as e: last_error e # 记一笔失败下一单的实时成功率会降下来 record_route_fail(ch.channel_code, pay_req.get(payment_no)) continue raise RouteError(all_channel_failed: %s % last_error)逻辑说明precheck 要做的事包括金额是否在渠道限额区间、渠道是否在维护时间窗、渠道是否有临时降权标记。打分阶段success_rate 建议用滚动 30 分钟窗口计算渠道故障往往是突发的按天统计的成功率会掩盖最近几分钟的异常。fallback 最多试 2 到 3 家渠道不能把候选列表全部打一遍。注意同一笔支付单的 fallback 必须按 payment_no 加分布式锁。否则用户连续点击支付按钮时多个请求会并发地把每个渠道都打一遍渠道侧可能出现同一支付单的重复交易。4.4 路由参数从哪来成功率与成本统计口径路由效果好不好取决于喂给它的数据。我一般维护两张统计表channel_daily_stat 存每个渠道每天的交易量、成功量、失败分布、平均耗时channel_cost 存合约费率、单笔固定成本、实际历史手续费。成功率用滚动窗口渠道成本按天更新这是两套不同节奏的刷新策略。冷启动阶段没有历史数据权重就用上面代码里的经验值先跑上线后每周复盘一次路由命中分布。观察指标是路由降级触发次数和因选错渠道导致的失败率如果某渠道被选中但失败率高说明它的 realtime 成功率权重偏低了需要调大 success_rate 的系数或者对渠道做临时降权。5. 收银台常见问题排查掉单、重复支付、回调丢失怎么查收银台这个环节的故障绝大多数不是代码写不出来而是看起来一切正常但订单状态就是不对。这一章把我反复见过的几类现场按现象 → 原因 → 解决拆开讲每一类都是可以直接抄的排查路径。5.1 用户支付成功但订单未更新掉单怎么查现象用户在渠道侧完成了支付收银台这边的订单一直停在支付中直到超时被关单用户已扣款但业务未发货。这是收银台最典型、也是影响最大的一类问题。原因分两种渠道的回调没到或者回调到了但处理失败了。回调丢失是常态渠道侧不会保证 100% 通知处理失败常见于验签不过、业务处理抛异常导致消费线程退出且没有重试机制。排查时先看 payment_notify_record如果 handle_statusFAIL 且 retry_count 停在一个固定数字说明业务处理反复异常如果没有任何回调记录说明渠道压根没推过来。解决不要只依赖回调。给 PAYING 状态加延迟任务支付发起后 2 分钟、10 分钟、30 分钟各主动查一次渠道查询确认成功后走与回调完全相同的状态流转代码。排查时先用下面这类 SQL 捞出可疑单子-- 找出超过30分钟还停在PAYING的单子 select payment_no, channel_code, create_time from payment_order where status PAYING and expire_time now() and create_time date_sub(now(), interval 30 minute);捞出来后逐个调渠道查询接口确认真实状态渠道侧成功就走 SUCCESS渠道侧确实失败才允许置 FAIL。这条主动查单链路是收银台最后的安全网必须和回调共用同一段状态更新代码。5.2 同一订单被支付两笔重复支付的现场现象同一张支付单在渠道侧出现两笔扣款用户多付了一笔客诉直接打爆。原因有三层叠加用户开了两个端同时点支付收银台并发请求没有拦住回调慢的时候前端以为失败用户又点了一次路由 fallback 自动换了另一家渠道把两个不同的渠道都打了一遍渠道侧没有用相同单号做幂等。解决三层都堵住。收银台内部用状态机保证同一支付单只有一条 PAYING第二次 pay 直接拒绝路由 fallback 时同一个 payment_no 加分布式锁保证一笔支付单同一时刻只在一个渠道尝试流程里最终防线是渠道侧用我方 payment_no 作为 out_trade_no渠道存在在途单时重复请求会返回原单号。排查重复支付时拿 payment_order 和渠道账单按 channel_trade_no 关联正常情况一个支付单只能关联到一笔渠道成功单。5.3 回调乱序覆盖终态FAIL 把 SUCCESS 改坏了现象用户支付成功、支付单已经变成 SUCCESS几分钟后一条延迟到达的支付失败通知把状态改成 FAIL订单被自动取消用户投诉付了钱订单却没了。原因渠道异步通知无序或者渠道在重推历史报文。失败通知先发出但因为网络延迟比成功通知更晚到达收银台。如果更新逻辑里没有状态条件后到的消息就会覆盖先到的正确状态。解决所有状态更新必须带前置状态条件。FAIL 更新同样写 update ... set statusFAIL where payment_no? and statusPAYINGSUCCESS 状态的行不会被 FAIL 更新命中。如果同一时刻存在并发竞争再加 version 乐观锁更新时同时比对 version冲突后重读。我之前见过只靠应用层判断、不带 SQL 条件的写法等于把状态机全交给了运气这类代码必须按状态机条件重写。5.4 大额支付反复失败路由没有做限额预检查现象一笔一万元的订单每次都被路由选中一家单笔限额 5000 的渠道用户反复看到超出单笔限额支付成功率和客诉一起恶化。原因路由只按优先级、费率排序没有在打分前过滤渠道限额。这是典型的配置完备但结果错误——规则表里其实有限额字段但代码没用它做第一层过滤。解决precheck 里把 amount_min / amount_max 的过滤放到打分之前限额数据从渠道配置实时读缓存时间不要超过 5 分钟。还有一个容易被忽略的点当渠道返回超限这类确定性错误时要立刻给该渠道打一个 5 到 10 分钟的临时降权标记否则同一笔支付单 fallback 时可能又选中同一家渠道陷入反复失败的死循环。5.5 对账不平渠道侧有、我方没有现象渠道账单里有一笔成功交易本地支付单表查不到对应记录或者金额对不上日终对账文件一批红色告警。原因常见三种支付单创建成功但没真正调起渠道渠道侧没有单形成本地有渠道无渠道回调成功但本地更新失败比如碰上唯一键冲突或状态条件不满足形成渠道有本地无测试数据、退款单被当成支付单拉进了对账范围。解决对账不能只比对有和无要做金额和时间窗匹配。渠道有本地无时先查 payment_notify_record 和主动查单记录都没有就是渠道侧单边需要人工发起撤销。本地有渠道无时多半是渠道没受理属于渠道异常标记后重推或撤销。对账结果落成 daily_chargeback 表由负责资金的同学逐个勾对不要做自动冲正自动冲正在差错场景里只会让账更乱。6. 上线前的进阶验证对账脚本和压测这几件事先做6.1 对账脚本把单边记录按月捞出来收银台上线后最怕的是看起来稳月底账不平。我习惯维护一个极简对账脚本每天把渠道账单和本地支付单做一次逐笔匹配只输出三类异常渠道有本地无、本地有渠道无、状态和金额不一致。脚本不需要做自动冲正它只负责把问题暴露出来。import pymysql import sys conn pymysql.connect(host127.0.0.1, userpay, password*****, dbpayment) cur conn.cursor() # 渠道账单格式channel_trade_no,amount,status金额单位是分 for line in open(sys.argv[1], r, encodingutf-8): trade_no, amount, status line.strip().split(,) cur.execute( select status, pay_amount from payment_order where channel_trade_no%s, (trade_no,), ) row cur.fetchone() if row is None: print(CHANNEL_SINGLE:, trade_no) # 渠道有本地无 elif row[0] ! SUCCESS or row[1] ! int(amount): print(STATE_OR_AMOUNT:, trade_no, row) # 状态或金额不一致 conn.close()脚本说明CHANNEL_SINGLE 是最需要警惕的这类记录往往对应掉单或回调处理失败STATE_OR_AMOUNT 多半是回调金额校验没拦住、或者退款状态污染了对账范围。脚本可以挂到定时任务里输出文件直接当工单每天处理完清零。6.2 压测与上线前习惯压测这块有几条踩坑经验值得提前说。压测时渠道一定要用 mock 网关固定返回成功、失败两种结果不要拿沙箱渠道压沙箱不稳定压出来的数字全是噪音。收银台最容易先挂的往往是回调线程池被打满而不是接口本身所以回调处理线程池要和下单线程池隔离拒绝策略选丢弃并保留重试记录不能抛异常让渠道反复重推。最后上线前找时间做一次故障演练渠道维护、回调积压、数据库慢查询各模拟一遍验证主动查单和对账能不能把掉单兜回来。我刚接手收银台那几个月经常被渠道回调丢了这种话带偏后来把所有丢单的口子都补上回调进重试表、超时主动查单、每日对账捞单边记录。现在判断一个收银台健不健康只看两个数字——PAYING 超过 30 分钟的单量和日对账单边笔数两个都趋近于零才敢说稳。这个方向值得投入但前提一定是先把状态机和幂等这两块地基打牢希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑