核心结论
系统已具备“特约商户进件—小程序下单—付款码收款—回调入账—退款—结算”的完整业务能力,但主交易链路仍基于微信支付 V2,V3 只在进件、资料上传、点金计划等局部使用。当前最大的风险不是“不能支付”,而是密钥日志泄露、双版本回调契约不一致、主动查单与对账补偿不完整。
本文基于一次支付代码静态审计,不代表发生了一起已确认的资金损失事故。复盘目标是把历史演进中积累的技术债变成可验证、可排期的工程改造项。
对外发布版已自动脱敏:真实域名、数据库名、表前缀、商户号、AppID、证书路径、回调地址和业务 ID 均使用了示例值;官方 API 路径、技术架构和问题机制保持不变。
1. 背景与问题
该平台服务多个餐饮商户和门店,微信支付采用服务商模式:平台持有服务商商户号,门店作为特约子商户,支付请求同时携带服务商与子商户参数。随着业务增长,小程序支付、堂食机付款码支付、会员充值、跑腿余额充值、退款、个人结算、微信支付进件等功能先后接入。
此次审计的直接问题是:系统究竟接入了哪些微信支付官方 API,哪些正在生产链路使用,哪些只是预留代码,以及当前架构是否具备资金系统应有的安全性与最终一致性能力。
- 同时存在 V2 XML/MD5 与 V3 JSON/RSA 两套调用方式。
- 统一下单、付款码支付、查单和退款仍以 V2 SDK 为主。
- V3 JSAPI、V3 商家转账和 V3 分账已有工具层代码,但没有有效业务调用。
- 回调处理承担大量业务状态变更,普通 JSAPI 支付的主动查单补偿不完整。
- 代码存在输出 V2 API Key 到日志的高危行为。
2. 目标与约束
2.1 复盘目标
- 建立微信支付 API 能力清单,区分“已使用”“已实现未启用”和“尚未接入”。
- 还原支付、付款码收款、退款、进件、转账与回调的完整数据流。
- 评估 V2/V3 混合对安全、回调、证书、幂等和维护成本的影响。
- 给出不停服、可分阶段实施的演进路线和验收标准。
2.2 现实约束
- 支付能力被多个微服务复用,无法一次性替换所有调用点。
- 存量订单、退款单、商户绑定关系和微信交易号必须继续可查。
- V2 与 V3 的签名、证书、通知格式和错误语义不同,不能仅替换 URL。
- 本次是静态审计,没有直接修改生产代码或轮换密钥。
3. 系统架构

图 1:当前服务商模式支付架构(已脱敏)
服务商模式的关键是参数有两层:平台层使用服务商 AppID 与商户号,商户层使用特约商户号 sub_mch_id 与商户小程序 sub_appid。顾客的 OpenID 也必须使用子小程序维度的 sub_openid。这种模式便于平台统一进件和接入,但对绑定关系、密钥、证书和商户隔离的要求更高。
4. 微信支付 API 接入清单
4.1 生产业务已使用
| 业务能力 | 官方 API | 版本 | 实际用途 |
|---|---|---|---|
| 小程序统一下单 | /pay/unifiedorder | V2 | 普通订单、堂食、会员充值、平台充值、直接收款等 |
| 付款码支付 | /pay/micropay | V2 | 堂食机扫描顾客付款码、会员充值 |
| 支付查单 | /pay/orderquery | V2 | 付款码支付中间状态轮询 |
| 申请退款 | /secapi/pay/refund | V2 | 全额、部分、充值取消和差额退款 |
| 绑定子商户 AppID | /secapi/mch/addsubdevconfig | V2 | 特约商户号与子小程序绑定 |
| 查询子商户开发配置 | /secapi/mch/querysubdevconfig | V2 | 后台检查绑定结果 |
| 特约商户进件 | /v3/applyment4sub/applyment/ | V3 | 个体户、企业和小微商户进件 |
| 查询进件状态 | /v3/applyment4sub/applyment/applyment_id/{id} | V3 | 获取审核状态和特约商户号 |
| 商户资料上传 | /v3/merchant/media/upload | V3 | 执照、身份证、门店照片 |
| 企业付款到零钱 | /mmpaymkttransfers/promotion/transfers | V2 | 个人结算与提现 |
| 查询付款状态 | /mmpaymkttransfers/gettransferinfo | V2 | 结算结果补查 |
| 开通点金计划 | /v3/goldplan/merchants/changegoldplanstatus | V3 | 定时为符合条件的子商户开通 |
4.2 已实现但尚未启用
| 能力 | 官方 API | 审计结论 |
|---|---|---|
| V3 服务商 JSAPI 下单 | /v3/pay/partner/transactions/jsapi | 方法已实现,业务调用被注释;回调地址与现有 V2 XML 处理器不匹配 |
| V3 批量转账 | /v3/transfer/batches | 创建批次和按商家单号查询明细的工具已有,业务调用被注释 |
| V3 分账接收方 | /v3/profitsharing/receivers/add|delete | 只有工具方法,没有生产业务调用 |
| V3 请求分账 | /v3/profitsharing/orders | 已实现请求、查询与回退方法,未形成完整账务链路 |
4.3 尚未发现的重要能力
- 普通支付主动关单
closeorder。 - 付款码支付撤销
reverse。 - 微信退款主动查询
refundquery。 - 交易账单和资金账单下载、解析与自动对账。
- V3 主交易查单、V3 退款与 V3 通知的统一生产链路。
- 支付回调死信、重放、对账差异工单和自动修复。
5. 核心设计与数据流
5.1 JSAPI 支付

图 2:小程序 JSAPI 支付主时序
业务服务先生成待支付订单和 out_trade_no,然后调用 V2 统一下单获取 prepay_id。客户端完成支付后,微信异步通知统一回调服务。回调服务验签后,通过 attach.business_type 区分普通订单、充值、服务开通、平台订单和直接收款等业务。
设计优点
统一回调入口降低了多业务对微信协议的重复实现;商户交易号和微信交易号都被持久化,便于追溯。
设计代价
统一回调中的 switch 承担了太多跨域状态变更;一旦回调丢失、解析失败或单个分支异常,就需要主动查单、幂等重放和对账来收敛。
5.2 付款码支付
- 堂食机扫描顾客付款码,获得
auth_code。 - 业务服务调用 V2
/pay/micropay同步扣款。 - 返回
SUCCESS时直接完成本地订单。 - 返回
USERPAYING等中间状态时,使用/pay/orderquery轮询。 - 超过业务等待窗口后,应通过撤销或后台补偿收敛;当前未发现
reverse接入。
5.3 退款
系统使用 V2 证书接口发起退款,并把 out_refund_no、退款金额、原交易号和退款原因保存到本地退款记录。微信退款通知到达后,解密 req_info,修改退款状态、订单累计退款金额、商品可退数量和账务流水。
这条链路已支持全额、部分、充值取消和配送差额退款,但未发现微信退款查询作为通知丢失的补偿手段。
6. 数据模型与 ERD

图 3:支付核心表与业务键关联(表名已脱敏)
| 脱敏表名 | 职责 | 主键与业务关联键 | 一致性要求 |
|---|---|---|---|
merchant_subject | 商户主体、AppID、认证与进件资料 | subject_id / appid | 主体与授权 AppID 唯一映射 |
merchant_store | 门店与特约商户支付配置 | merchant_id / subject_id / sub_mch_id | 下单前必须验证商户状态与 AppID 绑定 |
order_main | 订单金额、支付状态、微信交易号 | order_id / out_trade_no / transaction_id | 支付回调必须幂等,不允许状态回退 |
order_item | 下单快照、数量、金额和可退数量 | item_id / order_id | 部分退款需与退款明细同步 |
refund_record | 退款申请、金额、明细与状态 | refund_id / order_id / out_refund_no | 退款单号唯一,回调重放不得重复入账 |
member_recharge | 会员和跑腿余额充值 | recharge_id / out_trade_no | 支付与账户加款必须可补偿 |
direct_pay_record | 商户收款码直接收款 | direct_pay_id / out_trade_no | 支付回调只能将记录由待支付推进一次 |
account_ledger | 会员或商户资金流水 | ledger_id / business_id | 应以业务唯一键防止重复记账 |
数据模型的核心矛盾
微信回调只带有交易与附加信息,而本地业务横跨普通订单、充值、服务开通、平台订单和直接收款。当前使用 attach 进行多态分发,却没有独立支付尝试表统一记录请求、回调和查单状态,这会加大对账与排障成本。
7. 关键实现分析
7.1 签名与证书
| 项目 | V2 | V3 |
|---|---|---|
| 请求体 | XML / Map | JSON |
| 签名 | API Key + MD5 | 商户私钥 + RSA-SHA256 |
| 证书 | 退款、转账等接口使用客户端证书 | 商户证书序列号、平台证书自动更新 |
| 回调 | XML + sign,退款通知解密 req_info | JSON resource + AEAD_AES_256_GCM |
| 当前用途 | 主支付、付款码、查单、退款、转账 | 进件、媒体上传、点金计划 |
7.2 回调业务分发
payment_notify
-> verify_v2_signature
-> parse(out_trade_no, transaction_id, attach)
-> switch attach.business_type
1: recharge
2: service_activation
3: normal_order
4: points_mall
5: member_recharge
6: personal_order
7: platform_order
8: canteen_order
9: stall_activation
10: delivery_balance_recharge
11: direct_payment
-> idempotent_business_update
-> ACK SUCCESS
这种设计能够快速复用同一支付通道,但也形成了明显的集中式耦合。更适合的方式是回调层只负责验签、解密、落入支付事件和幂等 ACK,各业务域通过事件消费者更新自己的状态。
8. 方案取舍
| 方案 | 优点 | 缺点 | 结论 |
|---|---|---|---|
| 继续维持 V2 | 改动小,存量链路稳定 | 旧版密钥与 XML 维护成本高,新能力持续分裂 | 只适合短期维稳 |
| 一次性全量切 V3 | 技术栈统一,可利用官方新 SDK | 调用点多、回调契约变化大,资金风险集中 | 不建议 |
| 支付域收口 + 双轨迁移 | 可逐业务切换,可灰度、可回退、可对账 | 过渡期需维持双轨和适配层 | 推荐 |
9. Troubleshooting:Symptoms → Investigation → Root Cause → Resolution → Verification
9.1 Symptoms
- 代码中同时存在 V2 和 V3 SDK,且不同功能使用不同证书与通知处理方式。
- V3 JSAPI 工具方法已存在,但业务仍调用 V2 统一下单。
- 普通 JSAPI 支付回调丢失时,没有完整的定时查单和对账修复闭环。
- 退款主要依赖微信通知,缺少退款查询补偿。
- 部分签名代码把 V2 API Key 写入日志。
9.2 Investigation
- 扫描所有微信支付官方域名、V2 SDK 方法和 V3 SDK Service 调用。
- 从支付工具类反向搜索业务调用点,识别正式链路、注释代码和仅有 main 方法的测试实现。
- 核对下单请求中的服务商、子商户、AppID、OpenID、回调地址和 attach 参数。
- 追踪支付和退款回调的验签、解密、幂等判断与表更新顺序。
- 搜索关单、撤销、退款查询、账单下载和对账任务,确认能力缺口。
- 检查密钥、证书路径、日志内容和 V3 回调契约。
9.3 Root Cause

图 4:V2/V3 混合架构的 5 Whys 根因链
根因不是某个开发人员选错了 API,而是支付能力随业务逐步增长时,没有同步建立“支付域”这个明确边界。下单、退款、转账和进件被以工具函数方式分散添加,最终形成多 SDK、多签名、多回调契约共存的过渡态。
9.4 Resolution
状态说明
以下内容是本次复盘给出的整改方案,并非已在生产环境实施完成。
P0:密钥与生产风险止血
- 立即删除所有输出 API Key、私钥、完整签名参数的日志。
- 搜索历史日志和日志平台;若确认密钥已落盘,按泄露事件轮换 V2 API Key。
- 将商户号、证书序列号、APIv3 Key 和私钥路径收口到密钥管理或受控配置中心。
- 在 V3 回调处理器完成前,禁止启用已存在的 V3 JSAPI 工具方法。
P1:支付域收口与最终一致性
- 建立统一
PaymentGateway接口,业务层不再直接依赖 V2/V3 SDK。 - 新增支付尝试表,统一记录请求版本、商户、本地交易号、微信交易号、最后查询状态与回调次数。
- 实现 JSAPI 查单补偿、付款码撤销、退款查询补偿与通知死信重放。
- 支付回调只落入幂等支付事件,各业务域通过事件消费器更新订单、账户和打印状态。
- 为
out_trade_no、out_refund_no和账务流水业务键建立唯一约束。
P2:V3 双轨迁移
- 先在支付域内实现 V3 下单、查单、关单、退款和通知解密,对业务服务保持同一接口。
- 从低风险业务开始按商户或流量灰度,V2 作为可回退通道。
- 同步比对 V2/V3 请求金额、子商户、交易号和最终状态,但不发起双份真实扣款。
- 先切换主动查单和退款补偿,再切换下单与通知,最后下线 V2 API Key 和 XML 回调。
9.5 Verification
| 验证项 | 本次审计结果 | 整改后验收标准 |
|---|---|---|
| 服务商模式 | 已确认使用平台商户号 + sub_mch_id + sub_appid | 参数校验与商户隔离自动化测试通过 |
| 主支付版本 | 已确认生产主链路为 V2 | 灰度商户可切换 V3,失败时可一键回退 |
| 支付幂等 | 回调中有订单状态检查,但跨业务统一约束不完整 | 同一通知重放 10 次,订单、账户、流水均只变更一次 |
| 回调丢失 | 普通 JSAPI 未发现完整补偿闭环 | 主动丢弃通知后,定时查单能在 SLA 内修复状态 |
| 退款通知丢失 | 未发现微信退款查询补偿 | 丢弃退款通知后,补偿任务能收敛退款和账务状态 |
| 密钥日志 | 发现 V2 API Key 明文日志 | 代码扫描、日志抽检与秘密扫描均为零命中 |
| 对账 | 未发现官方账单自动对账 | 每日生成差异报告,支持人工确认和幂等修复 |
10. 典型问题排查手册
场景 A:用户显示已扣款,本地订单仍待支付
- 用
out_trade_no查询本地订单、支付回调日志和微信交易号。 - 检查回调是否到达、验签是否通过、attach 是否可解析。
- 检查该 business_type 分支是否发生事务异常或幂等拦截。
- 通过微信支付查单确认最终状态,禁止仅依赖前端返回结果修改订单。
- 若微信已支付,使用幂等补单通道重放支付成功事件。
场景 B:付款码支付长时间 USERPAYING
- 按本地交易号调用订单查询,区分 SUCCESS、USERPAYING、PAYERROR 与 NOTPAY。
- 在有限时间内使用退避策略重查,不要高频无限轮询。
- 超时后执行付款码撤销并再次查单;当前系统需要补齐撤销能力。
- 在状态未确定前,不得重新生成另一个订单号重复扣款。
场景 C:退款长时间停留在处理中
- 检查本地退款记录的
out_refund_no、原交易号、金额和通知时间。 - 检查退款通知是否解密失败,以及是否因为本地订单或退款记录不存在而提前返回。
- 使用微信退款查询获得最终结果;当前需先通过商户平台人工查询,后续应程序化。
- 按退款业务键幂等补写退款状态、订单累计退款和账务流水。
11. Lessons Learned
- 支付是独立的核心域,不应长期以分散工具函数形态存在。
- 支付成功以服务端通知或主动查单为准,前端成功页面不是账务事实。
- 回调是至少一次投递,所有订单、退款和流水更新必须幂等。
- 异步通知不是唯一的状态收敛方式,查单、关单、撤销、退款查询和账单对账必须形成闭环。
- 密钥绝不能出现在日志、异常消息、前端响应和普通配置文件中。
- V2 迁移 V3 不是 URL 替换,而是签名、证书、通知、错误语义和运维流程的整体迁移。
- 未启用的支付代码不是免费资产;它会制造误用风险,应删除、隔离或通过特性开关明确管理。
12. 后续优化路线
| 优先级 | 工作项 | 预期结果 | 验收方式 |
|---|---|---|---|
| P0 | 删除密钥日志并评估是否轮换 V2 Key | 阻断凭证泄露风险 | 代码扫描 + 历史日志抽检 |
| P0 | 禁用未配套 V3 通知的 JSAPI 入口 | 防止误开启后支付不入账 | 特性开关默认关闭 |
| P1 | 接入 JSAPI 查单、付款码撤销、退款查询 | 通知丢失仍能最终收敛 | 故意丢弃通知演练 |
| P1 | 建立支付尝试和支付事件表 | 统一追踪请求、回调、查单和重试 | 任意交易号可查全链路 |
| P1 | 官方账单日对账 | 发现本地与微信资金差异 | 每日生成差异报告与工单 |
| P2 | 统一 PaymentGateway 并按商户灰度 V3 | 业务与微信版本解耦 | 同一测试用例覆盖 V2/V3 适配器 |
| P2 | 回调事件化与死信重放 | 单个业务失败不阻断通知落盘 | 注入消费失败并重放 |
| P3 | 完成 V3 分账和新版商家转账业务化 | 替换预留工具代码,形成可对账链路 | 分账回退、转账查询和账务对账演练 |
13. 总结
这套微信支付接入已经支撑了复杂的多商户餐饮场景:服务商进件、子商户绑定、小程序支付、堂食付款码支付、退款、充值和个人结算都有真实业务落地。从功能完整度看,它不是一个“半成品支付”;从资金基础设施的稳健性看,它又处在从业务工具向支付域演进的中间阶段。
真正的优先级不是立即把所有 V2 URL 替换为 V3,而是先消除密钥日志,再补齐查单、撤销、退款查询和账单对账,同时建立统一支付域边界。只有当幂等、补偿、对账和可观测性先稳定下来,V3 迁移才是一次可控的技术升级,而不是新一轮资金风险。
最终原则
支付系统的正确性不能由“请求成功”证明,而要由“重复通知不重复入账、通知丢失能自动修复、本地流水与官方账单最终一致”来证明。
本文基于静态代码审计,系统梳理了服务商模式下微信支付V2与V3 API的接入现状,识别出主交易链路仍依赖V2、V3仅局部使用的问题。重点揭示密钥日志泄露、双版本回调契约不一致、主动查单与对账补偿缺失等核心风险。提出P0密钥止血、P1支付域收口与最终一致性建设、P2双轨迁移三阶段演进路径,强调以幂等性、补偿闭环和账单一致性为支付系统正确性基石。

65

被折叠的 条评论
为什么被折叠?



