| name | payment-notify-idempotency |
| description | MallBase 支付与退款回调安全规则;修改 backend/route/notify.php、PayNotifyController、NotifyService、WechatPaymentResultService、mb_payment_log,或处理微信/其它渠道 webhook 的验签、解密、防重放、金额校验、幂等、事务与 HTTP 应答时使用。 |
支付回调验签与幂等
当前入口
- 白名单路由:
backend/route/notify.php
- 支付:
POST /api/notify/wechat/pay
- 退款:
POST /api/notify/wechat/refund
- Controller:
backend/app/controller/client/order/PayNotifyController.php
- Service:
NotifyService、WechatPaymentResultService
两个回调入口不挂 JWT/CSRF 中间件,支付与退款保持独立路由。网关必须透传 Wechatpay-Signature、Wechatpay-Serial、Wechatpay-Timestamp、Wechatpay-Nonce 和未经改写的原始请求体。
支付处理顺序
- 用原始 headers/body 构造 PSR-7 Request。
- 用 EasyWeChat Validator 验签,不自行实现微信签名算法。
- 通过 SDK Server 解密
resource,业务只使用解密后的 attributes。
- 用微信 nonce 做 300 秒短窗口防重放。
- 校验
mchid、out_trade_no、transaction_id、trade_state 和整数分金额。
- 以活动 PREPAY 流水的
amount_cents 对比回调 amount.total,不使用浮点元金额。
- 在同一事务内追加 PAID 流水并通过订单 Service/状态机确认支付。
- 成功或已幂等处理返回微信 V3 JSON 成功应答;验签、重放和处理异常按当前协议返回失败状态。
WechatPaymentResultService::applyVerifiedSuccess() 同时供回调和主动查单复用。不要在另一条路径复制金额、商户号或订单状态校验。
持久幂等
mb_payment_log 使用合法的 MySQL 联合唯一索引:
UNIQUE KEY `uk_txn_event` (`transaction_id`, `event_type`)
不要写 PostgreSQL 风格的 ... WHERE transaction_id IS NOT NULL 部分唯一索引。MySQL 唯一索引允许多行 NULL,同一非空交易号与事件类型仍会被唯一约束拦截。
应用层先查 (transaction_id, PAID),数据库唯一键处理并发竞争;命中重复键时按幂等结果处理,不重复推进业务。out_trade_no 的唯一约束继续保护预支付流水。
Redis nonce 是短期减压与重放告警层,数据库唯一键是持久防线。当前 Redis 异常采用可用性优先的放行策略;如要改为失败关闭,必须先评估支付回调可用性,不能顺手改变。
应答与副作用
- 验签失败或重放:返回带 V3 JSON body 的 401。
- 金额、商户、订单或落库处理失败:返回 5xx,让微信按协议重试。
trade_state 非 SUCCESS 且已完成审计处理:按当前实现返回成功,不推进订单。
- 成功:返回 200 和
{"code":"SUCCESS","message":"成功"}。
事务内只做支付流水和订单状态的原子持久化,不调用短信、Webhook 等外部服务。当前支付告警监听器只落日志并预留运维通道;新增真实通知时放到事务外并做队列、限时和异常隔离。
自检