Files
team-api/internal/logic/payment/callback.go
T
Qianandchengqian be9e707bfd 修复异步图片导致的多个问题 (#12)
* fix:修复响应污染问题

* fix:修改存储相关文件为中文注释

* fix:修复二次退款问题

* fix:修复多个考虑不全面问题
1.误删用户数据
2.R2存储默认区域
3.删除失败导致孤儿记录
4.网络抖动事变时重试
5.前端无限次请求获取图片
6.文件管理权限

* fix:修复字符串截断异常问题和文件下载超时问题

* fix:修复多图问题

* fix:异步生成图片时未配置oss提示错误信息

* fix:修复关闭异步后同步生成出错问题

* feat:新增文件存储测试功能,防止设置后不生效

* fix:修复同步图片转异步后计费错误

* fix:修复无下限扣款问题

* fix:修复结算重复扣款问题

* fix:修复计费出错导致的0费用结算

* fix:修复回调双重履约

* fix:修复预扣事务

* fix:修复费用字段类型问题

* fix:修复缓存问题

* fix:修复安全问题

* fix:修复数据库操作事务问题

* refactor:重构事务使用方式,改为官方推荐的dao形式

* fix:修复余额更新隐患

* fix:修复N+1查询问题

* fix:修复密钥安全问题

* fix:结算失败时打印错误日志

* improve:优化扣款

* fix:修复用户接口越权

---------

Co-authored-by: chengqian <wuchengqian@kkidc.com>
2026-07-22 17:53:18 +08:00

169 lines
4.8 KiB
Go

package payment
import (
"context"
"net/http"
"sync"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
do "github.com/qianfree/team-api/internal/model/do"
"github.com/qianfree/team-api/internal/dao"
"github.com/qianfree/team-api/internal/logic/common"
)
// 订单级互斥锁,防止回调重复处理。
var (
orderLocks sync.Map
createLock sync.Mutex
)
type refCountedMutex struct {
mu sync.Mutex
refCount int
}
// LockOrder 对订单号加锁。
func LockOrder(orderNo string) {
createLock.Lock()
var rcm *refCountedMutex
if v, ok := orderLocks.Load(orderNo); ok {
rcm = v.(*refCountedMutex)
} else {
rcm = &refCountedMutex{}
orderLocks.Store(orderNo, rcm)
}
rcm.refCount++
createLock.Unlock()
rcm.mu.Lock()
}
// UnlockOrder 释放订单锁。
func UnlockOrder(orderNo string) {
v, ok := orderLocks.Load(orderNo)
if !ok {
return
}
rcm := v.(*refCountedMutex)
rcm.mu.Unlock()
createLock.Lock()
rcm.refCount--
if rcm.refCount == 0 {
orderLocks.Delete(orderNo)
}
createLock.Unlock()
}
// ProcessCallback 统一回调处理流程。
// channelType 为渠道类型字符串(如 "epay")。
func ProcessCallback(ctx context.Context, r *http.Request, channelType string) error {
// 1. 从 sys_options 加载渠道配置
cfg, err := GetChannelConfigAndProvider(ctx, channelType)
if err != nil {
return gerror.Wrapf(err, "加载支付渠道配置失败")
}
// 2. 获取 Provider
provider := GetProvider(channelType)
if provider == nil {
return gerror.Newf("不支持的支付渠道: %s", channelType)
}
// 3. 调用 Provider 验签并解析回调
result, err := provider.HandleCallback(ctx, r, cfg)
if err != nil {
return gerror.Wrapf(err, "回调验证失败")
}
// 4. 订单级加锁
LockOrder(result.OrderNo)
defer UnlockOrder(result.OrderNo)
// 5. 幂等检查:仅处理 pending 状态
var order *struct {
ID int64 `json:"id"`
Status string `json:"status"`
FinalAmount float64 `json:"final_amount"`
ExpiredAt *gtime.Time `json:"expired_at"`
}
err = dao.OrdOrders.Ctx(ctx).
Where("order_no", result.OrderNo).Scan(&order)
if err != nil {
return gerror.Wrapf(err, "查询订单失败")
}
if order == nil {
return common.NewNotFoundError("订单")
}
if order.Status != "pending" {
return nil // 已处理,幂等返回
}
if order.ExpiredAt != nil && !order.ExpiredAt.IsZero() && order.ExpiredAt.Before(gtime.Now()) {
return gerror.Newf("订单已过期: %s", result.OrderNo)
}
// 6. 金额校验:回调金额与订单金额必须一致(容差 0.01 元)
if result.Success && result.PaidAmount > 0 && order.FinalAmount > 0 {
diff := result.PaidAmount - order.FinalAmount
if diff < 0 {
diff = -diff
}
if diff > 0.01 {
g.Log().Warningf(ctx, "[Payment] amount mismatch: order=%s expected=%.2f received=%.2f",
result.OrderNo, order.FinalAmount, result.PaidAmount)
return gerror.Newf("支付金额不一致: 期望 %.2f 实付 %.2f", order.FinalAmount, result.PaidAmount)
}
}
if result.Success {
// 7. 原子领取订单:pending → paid。
// 该条件更新的 RowsAffected 是【跨实例】幂等闸门:多实例并发回调时,两个回调都会
// 通过上面第 5 步的 pending 检查,但 "WHERE status='pending'" 的原子更新只有一个能命中
// (另一个在行锁释放后重读到 status 已是 paid → 0 行)。据此仅让真正把订单从 pending
// 翻成 paid 的那次回调去履约,杜绝重复入账/重复发套餐。进程内 orderLocks 仅单实例有效,
// 不能作为并发保护依赖。
res, err := dao.OrdOrders.Ctx(ctx).
Where("id", order.ID).
Where("status", "pending").
Data(do.OrdOrders{
Status: "paid",
PaidAt: gtime.Now(),
PaymentNo: result.TradeNo,
}).Update()
if err != nil {
return gerror.Wrapf(err, "更新订单状态失败")
}
affected, err := res.RowsAffected()
if err != nil {
return gerror.Wrapf(err, "确认订单状态更新结果失败")
}
if affected == 0 {
// 另一并发回调已抢先领取并负责履约,本次幂等返回,绝不重复履约
g.Log().Infof(ctx, "[Payment] order=%s already claimed by a concurrent callback, skip duplicate fulfill", result.OrderNo)
return nil
}
// 8. 履约(仅领取成功者执行)
return FulfillOrder(ctx, order.ID)
}
return nil
}
// QueryOrderPaid 查询订单是否已完成支付(paid 或 fulfilled)。
// 用于浏览器同步回跳时展示结果,不参与履约处理。
func QueryOrderPaid(ctx context.Context, orderNo string) bool {
if orderNo == "" {
return false
}
var status string
_ = dao.OrdOrders.Ctx(ctx).
Where("order_no", orderNo).
Fields("status").
Scan(&status)
return status == "paid" || status == "fulfilled"
}