退款必须在数据库事务中执行,涵盖资金回退、库存返还、积分冲正等多步操作,任一失败需全部回滚;须用db.Begin()开启事务并加SELECT FOR UPDATE锁,校验金额时以数据库为准且用int64(分),支付结果以异步回调为准并做幂等处理。

退款逻辑必须在事务中执行
订单退款不是简单减余额、改状态,涉及资金回退、库存返还、积分冲正等多步操作,任何一步失败都得全部回滚。Gin本身不提供数据库事务能力,得靠底层驱动(如database/sql)显式控制。
常见错误是直接在HTTP handler里调用多个Update或Insert语句,没包在tx.Begin()和tx.Commit()里——一旦库存扣减成功但余额更新失败,就出现“钱退了货没还”的资损。
- 用
db.Begin()开启事务,所有DB操作走tx对象,不是db - 退款前先
SELECT ... FOR UPDATE锁定订单行,防止并发重复处理(尤其定时任务触发时) - 事务内避免调用外部API(如支付网关退钱),否则超时或网络抖动会导致事务卡住;应先本地落库,再异步通知
- Go的
defer tx.Rollback()要放在Begin()之后立即写,且确保只在出错时才Commit()
退款金额校验不能只信前端传参
用户请求里带的refund_amount字段完全不可信。攻击者可能篡改JSON,把1元订单改成退10000元。
必须从数据库重新查原始订单:确认订单状态是paid或shipped,未被全额退过,且本次退款额≤剩余可退金额(total_amount - refunded_amount)。
立即学习“go语言免费学习笔记(深入)”;
- 校验逻辑写在handler开头,用
queryRow查出order_status、total_amount、refunded_amount三个字段 - 别用
float64存金额——浮点误差会导致校验绕过;统一用int64(单位:分) - 如果订单含优惠券、满减分摊,需按原始分摊比例计算本次可退各部分金额,不能简单按比例折算
支付渠道回调与本地状态必须最终一致
调第三方支付接口(如微信secapi/pay/refund、支付宝alipay.trade.refund)成功,不代表钱真退了。对方可能异步失败、延迟到账,甚至部分退款。
所以本地不能只靠http.StatusOK就认为退款完成,必须监听支付平台的服务器回调,并以回调为准更新最终状态。
- 退款请求发出后,立即在本地插入一条
refund_record记录,状态设为pending - 支付回调接口(如
POST /api/v1/webhook/alipay/refund)要做幂等:用out_refund_no查记录,存在则只更新状态,不重复处理 - 加个定时任务每5分钟扫
status = 'pending'且创建超15分钟的记录,主动查支付方接口确认结果
Gin中间件里别做退款主逻辑
有人把退款代码塞进gin.HandlerFunc中间件,想复用鉴权、日志等逻辑——这会让退款流程和HTTP生命周期强耦合,难以单元测试,也容易在重定向、超时等场景下漏执行。
退款是业务核心动作,应该封装成独立函数(如service.RefundOrder(ctx, orderID, amount)),由handler调用。中间件只负责前置检查(JWT解析、IP限流、防重放)。
- 中间件里禁止调用
db.Exec或发HTTP请求 - 退款函数参数用结构体传入(如
RefundReq{OrderID: "xxx", Amount: 100}),别依赖c.Param或c.PostForm - handler里捕获
service.RefundOrder返回的自定义错误(如ErrInsufficientBalance),转成对应HTTP状态码和JSON
实际最难的不是写代码,是理清“谁先改状态、谁负责兜底、超时怎么补偿”——这些规则得和财务、支付团队对齐,代码只是把共识落地。


















