141 lines
3.4 KiB
Go
141 lines
3.4 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. 更新订单为已支付
|
|
_, 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, "更新订单状态失败")
|
|
}
|
|
|
|
// 8. 履约
|
|
return FulfillOrder(ctx, order.ID)
|
|
}
|
|
|
|
return nil
|
|
}
|