支付接口需幂等性控制,推荐用select for update加唯一索引拦截重复支付;订单与资金流水须分离存储;库存与余额扣减须事务内原子操作;敏感信息必须token化处理。

支付接口必须有幂等性控制,否则重复请求会扣多次钱
用户点击“支付”后网络卡顿,前端重试、浏览器刷新、后端超时重发——这些都会导致同一笔订单被多次调用 pay_order()。MySQL 不能只靠应用层判断“订单是否已支付”,必须在数据库层面拦截重复执行。
推荐做法是:在订单表加唯一索引 UNIQUE KEY uk_order_no_status (order_no, status),但只对 status = 'paid' 生效(MySQL 8.0+ 支持函数索引;5.7 可改用生成列或业务层配合 INSERT ... ON DUPLICATE KEY UPDATE)。
更稳妥的实操方式:
- 支付前先
SELECT ... FOR UPDATE锁住该order_no行(注意:必须走主键或唯一索引,否则会锁表) - 查出当前
status,若已是'paid'直接返回成功,不更新 - 若为
'unpaid',再执行UPDATE orders SET status = 'paid', paid_at = NOW() WHERE order_no = ? AND status = 'unpaid' - 检查
ROW_COUNT()是否为 1,不是则说明已被并发更新,拒绝本次支付
订单表要分离交易流水,别把支付结果直接写进 orders 表
一个订单可能经历「支付宝支付」「微信退款」「人工冲正」多次资金变动,如果所有状态和金额都堆在 orders 表里,字段会越来越难维护,审计也无从下手。
正确拆分方式:
-
orders表只存业务单据信息:order_no、user_id、total_amount、status(仅表示订单整体状态,如 created/paid/cancelled) - 新建
payment_transactions表,记录每一笔资金动作:tx_id(支付平台返回的交易号)、order_no、channel(alipay/wechat)、amount、type(pay/refund/adjust)、status(processing/success/fail)、created_at -
orders.total_amount和orders.paid_amount应由定时任务或触发器基于payment_transactions汇总更新,而非手动 set
这样设计后,查某笔订单的所有资金流只需 SELECT * FROM payment_transactions WHERE order_no = ? ORDER BY created_at,清晰可追溯。
防超卖和余额不足必须用 SELECT FOR UPDATE + UPDATE 原子组合
用户支付时,要同时扣减商品库存和用户账户余额。这两个操作跨表、跨行,又必须全部成功或全部失败——MySQL 的事务能保证原子性,但得用对姿势。
BJXShop网上购物系统是一个高效、稳定、安全的电子商店销售平台,经过近三年市场的考验,在中国网购系统中属领先水平;完善的订单管理、销售统计系统;网站模版可DIY、亦可导入导出;会员、商品种类和价格均实现无限等级;管理员权限可细分;整合了多种在线支付接口;强有力搜索引擎支持... 程序更新:此版本是伴江行官方商业版程序,已经终止销售,现于免费给大家使用。比其以前的免费版功能增加了:1,整合了论坛
常见错误是先 SELECT stock,再判断,再 UPDATE。这中间有竞态窗口,高并发下必然超卖。
正确顺序(在同一个事务内):
SELECT stock, locked_stock FROM products WHERE product_id = ? FOR UPDATE- 检查
stock - locked_stock >= order_quantity,不满足则 rollback UPDATE products SET locked_stock = locked_stock + ? WHERE product_id = ?SELECT balance FROM user_accounts WHERE user_id = ? FOR UPDATE- 检查
balance >= total_amount,不满足则 rollback UPDATE user_accounts SET balance = balance - ? WHERE user_id = ?
注意:FOR UPDATE 必须在 UPDATE 之前,并且所有涉及行都要在同一事务中锁定。否则释放锁后别人就可能修改。
不要用 MySQL 存敏感支付信息,token 化才是合规做法
银行卡号、CVV、微信 openid、支付宝 user_id —— 这些都不能明文落库,哪怕加了 AES 加密也不符合 PCI DSS 和国内《个人信息保护法》要求。
实操方案只有两个可行路径:
- 支付渠道返回的
pay_token或prepay_id可以存,它们是渠道颁发的、有时效、可作废的临时凭证 - 用户绑定卡信息交由合规的第三方支付网关托管(如支付宝的
bind_card接口),你只存一个binding_id,后续扣款用这个 ID 去调用网关
哪怕只是记录“用户选了微信支付”,也要避免在日志或数据库里拼接出完整 openid 字符串。真正容易被忽略的是:开发环境的 SQL dump、慢查询日志、监控埋点,都可能意外泄露这类字段。









