Files
kami_backend/internal/logic/jd_cookie/order_create.go
danial 2253dc739a feat(jd-cookie):优化订单创建逻辑与状态管理- 新增订单状态 OrderStatusJDOrderFailed用于标识京东订单获取失败
- 新增订单变更类型 OrderChangeTypeJDOrderFailed 用于记录下单失败事件
- 调整订单创建逻辑,支持失败订单重试机制
- 新增 RecordOrderHistoryReq 结构体统一记录订单变更历史参数
- 修改数据库表结构,优化字段类型和索引
- 更新订单创建逻辑,分离本地订单与京东订单创建流程- 增加失败订单重新创建京东订单的处理逻辑
- 调整订单状态检查逻辑,支持更多状态处理
-优化订单历史记录方式,增加备注信息支持
- 更新数据库字符集为 utf8mb4_unicode_ci 提升兼容性
2025-10-18 23:41:31 +08:00

550 lines
17 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package jd_cookie
import (
"context"
"kami/internal/consts"
"kami/internal/model"
"kami/internal/model/entity"
"kami/utility/cache"
"kami/utility/utils"
"slices"
"time"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/glog"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
)
// CreateOrder 创建订单
func (s *sJdCookie) CreateOrder(ctx context.Context, req *model.CreateOrderReq) (result *model.CreateOrderResult, err error) {
_ = s.ReleaseExpiredJdOrders(ctx)
if req.UserOrderId == "" {
return nil, gerror.New("用户订单号不能为空")
}
if req.Amount <= 0 {
return nil, gerror.New("订单金额必须大于0")
}
// 获取用户订单分布式锁,防止并发创建重复订单
lockKey := consts.OrderLockKeyPrefix + req.UserOrderId
lockValue, err := cache.NewCache().Lock(ctx, lockKey, time.Minute*3, time.Second*60)
if err != nil {
return nil, gerror.Wrap(err, "系统繁忙,请稍后重试")
}
// 确保锁会被释放
defer func() {
if unlockErr := cache.NewCache().Unlock(ctx, lockKey, lockValue); unlockErr != nil {
glog.Warning(ctx, "释放分布式锁失败", g.Map{
"lockKey": lockKey,
"error": unlockErr,
})
}
}()
// 在锁保护下再次检查用户订单是否已存在(双重检查)
existingOrder, err := s.getOrderByUserOrderId(ctx, req.UserOrderId)
if err != nil {
return nil, gerror.Wrap(err, "检查用户订单是否存在失败")
}
if existingOrder != nil {
// 订单已存在,检查订单状态
if !slices.Contains([]consts.OrderStatus{
consts.OrderStatusJDOrderFailed, consts.OrderStatusCreated, consts.OrderStatusPending,
}, consts.OrderStatus(existingOrder.Status)) {
return nil, gerror.New("订单已完成")
}
// 如果订单状态为失败(Ck失败),重新尝试创建京东订单
if consts.OrderStatus(existingOrder.Status) == consts.OrderStatusJDOrderFailed {
glog.Info(ctx, "检测到失败订单,尝试重新创建京东订单", g.Map{
"orderId": existingOrder.OrderId,
"userOrderId": req.UserOrderId,
"status": consts.OrderStatus(existingOrder.Status),
})
// 尝试重新创建京东订单
retryResult, retryErr := s.retryCreateJdOrderForFailedOrder(ctx, existingOrder, req)
if retryErr != nil {
// 重试失败,返回错误
return nil, retryErr
}
// 重试成功,返回新的订单信息
return retryResult, nil
}
// 订单状态正常,尝试获取支付链接
paymentResult, err := s.GetPaymentUrl(ctx, req.UserOrderId, existingOrder.OrderId)
if err != nil {
return nil, err
}
// 转换为CreateOrderResult
return &model.CreateOrderResult{
WxPayUrl: paymentResult.WxPayUrl,
JdOrderId: paymentResult.JdOrderId,
OrderId: paymentResult.OrderId,
}, nil
}
// 生成内部订单ID
internalOrderId := "JD_" + utils.GenerateRandomUUID()
// 先创建本地订单记录状态为待支付京东订单ID暂时为空
err = s.createOrderRecord(ctx, internalOrderId, req.UserOrderId, req.Amount, req.Category, "", "")
if err != nil {
return nil, gerror.Wrap(err, "创建订单记录失败")
}
// 记录订单创建历史
_ = s.RecordOrderHistory(ctx, &model.RecordOrderHistoryReq{
OrderId: internalOrderId,
ChangeType: consts.OrderChangeTypeCreate,
JdOrderId: "",
Remark: "",
})
glog.Info(ctx, "本地订单创建成功,开始创建京东订单", g.Map{
"orderId": internalOrderId,
"userOrderId": req.UserOrderId,
"amount": req.Amount,
})
// 优先尝试复用现有的京东订单
reusableJdOrder, err := s.findReusableJdOrder(ctx, req.Amount, req.Category)
if err != nil {
glog.Warning(ctx, "查找可复用京东订单失败", err)
}
var cookieId, jdOrderId, wxPayUrl string
var jdOrderErr error
var isReused = false
if reusableJdOrder != nil {
// 尝试使用可复用的京东订单
jdOrderId = reusableJdOrder.JdOrderId
cookieId = reusableJdOrder.CookieId
wxPayUrl = reusableJdOrder.WxPayUrl
// 检查支付链接是否过期
if reusableJdOrder.WxPayExpireAt != nil && gtime.Now().After(reusableJdOrder.WxPayExpireAt) {
// 支付链接已过期,尝试刷新
newWxPayUrl, isCkFailed, refreshErr := s.refreshPaymentUrl(ctx, &model.RefreshPaymentUrlReq{
JdOrderId: jdOrderId,
PayId: reusableJdOrder.PayId,
CookieId: cookieId,
})
if isCkFailed {
s.handleCookieFailure(ctx, req.UserOrderId, cookieId, jdOrderId, isCkFailed, refreshErr.Error())
}
if refreshErr != nil {
glog.Warning(ctx, "刷新支付链接失败,将创建新订单", g.Map{
"jdOrderId": jdOrderId,
"error": refreshErr,
})
// 刷新失败,标记为不可复用
_ = s.UpdateJdOrderStatus(ctx, jdOrderId, consts.JdOrderStatusExpired, "", refreshErr.Error())
// 记录Cookie刷新失败历史
_ = s.RecordCookieHistory(ctx, &model.RecordCookieHistoryReq{
CookieId: cookieId,
ChangeType: consts.CookieChangeTypeRefreshFail,
StatusBefore: consts.JdCookieStatusUnknown,
StatusAfter: consts.JdCookieStatusExpired,
UserOrderId: req.UserOrderId,
OrderId: internalOrderId,
FailureCount: 0,
Remark: "刷新支付链接失败",
})
// 清空,准备创建新订单
jdOrderId = ""
cookieId = ""
wxPayUrl = ""
} else {
wxPayUrl = newWxPayUrl
// 更新京东订单的支付链接和过期时间
_ = s.updateJdOrderPaymentUrl(ctx, jdOrderId, wxPayUrl)
isReused = true
}
} else {
isReused = true
}
if isReused {
glog.Info(ctx, "复用现有京东订单", g.Map{
"orderId": internalOrderId,
"jdOrderId": jdOrderId,
"cookieId": cookieId,
})
}
}
// 如果没有成功复用,创建新的京东订单
if jdOrderId == "" {
retryRes, retryErr := s.createNewJdOrderWithRetry(ctx, &model.CreateNewJdOrderWithRetryReq{
OrderId: internalOrderId,
Amount: req.Amount,
Category: req.Category,
UserOrderId: req.UserOrderId,
})
if retryErr != nil {
// 京东订单创建失败,更新本地订单状态和失败原因
jdOrderErr = retryErr
_ = s.updateOrderFailure(ctx, internalOrderId)
// 记录订单创建失败的变更历史
_ = s.RecordOrderHistory(ctx, &model.RecordOrderHistoryReq{
OrderId: internalOrderId,
ChangeType: consts.OrderChangeTypeJDOrderFailed,
JdOrderId: "",
Remark: jdOrderErr.Error(),
})
glog.Error(ctx, "京东订单创建失败", g.Map{
"orderId": internalOrderId,
"userOrderId": req.UserOrderId,
"failureReason": retryErr.Error(),
})
return nil, retryErr
}
jdOrderId = retryRes.JdOrderId
cookieId = retryRes.CookieId
wxPayUrl = retryRes.WxPayUrl
}
// 京东订单创建成功更新本地订单的京东订单ID和支付链接
err = s.updateOrderJdOrderId(ctx, internalOrderId, jdOrderId)
if err != nil {
glog.Error(ctx, "更新订单京东订单ID失败", g.Map{
"orderId": internalOrderId,
"jdOrderId": jdOrderId,
"error": err,
})
return nil, gerror.Wrap(err, "更新订单信息失败")
}
// 京东订单创建成功,更新本地订单状态为待支付,并关联京东订单
err = s.updateOrderSuccess(ctx, internalOrderId, jdOrderId)
if err != nil {
glog.Error(ctx, "更新订单状态失败", g.Map{
"orderId": internalOrderId,
"jdOrderId": jdOrderId,
"error": err,
})
return nil, gerror.Wrap(err, "更新订单信息失败")
}
// 更新京东订单的当前关联订单ID
_ = s.updateJdOrderId(ctx, jdOrderId, internalOrderId)
// 记录Cookie使用历史
_ = s.RecordCookieHistory(ctx, &model.RecordCookieHistoryReq{
CookieId: cookieId,
ChangeType: consts.CookieChangeTypeUse,
StatusBefore: consts.JdCookieStatusUnknown,
StatusAfter: consts.JdCookieStatusNormal,
OrderId: internalOrderId,
UserOrderId: req.UserOrderId,
FailureCount: 0,
Remark: "Cookie用于创建订单",
})
// 记录订单绑定京东订单的变更历史
_ = s.RecordOrderHistory(ctx, &model.RecordOrderHistoryReq{
OrderId: internalOrderId,
ChangeType: consts.OrderChangeTypeRebind,
JdOrderId: jdOrderId,
Remark: "",
})
return &model.CreateOrderResult{
WxPayUrl: wxPayUrl,
JdOrderId: jdOrderId,
OrderId: internalOrderId,
}, nil
}
// createNewJdOrderWithRetry 创建新的京东订单(带重试机制)
func (s *sJdCookie) createNewJdOrderWithRetry(ctx context.Context, req *model.CreateNewJdOrderWithRetryReq) (res *model.CreateNewJdOrderWithRetryRes, err error) {
var lastErr error
var triedCookies []string // 记录已尝试的Cookie
// 不断尝试直到没有Cookie为止
for {
// 获取可用的Cookie
availableCookieId, cookieErr := s.GetAvailableCookie(ctx)
if cookieErr != nil {
glog.Warning(ctx, "获取可用Cookie失败", g.Map{
"orderId": req.OrderId,
"triedCookies": len(triedCookies),
"error": cookieErr,
})
lastErr = cookieErr
break // 没有可用Cookie停止重试
}
// 检查是否已经尝试过这个Cookie
if s.hasCookieBeenTried(triedCookies, availableCookieId) {
glog.Debug(ctx, "Cookie已被尝试过跳过", g.Map{
"cookieId": availableCookieId,
})
continue
}
// 记录已尝试的Cookie
triedCookies = append(triedCookies, availableCookieId)
jdOrderId := utils.GenerateRandomUUID()
// 调用京东下单接口
createOrderRes, lastErr := s.callJdCreateOrder(ctx, &model.CallJdCreateOrderReq{
JdOrderId: jdOrderId,
CookieId: availableCookieId,
Amount: req.Amount,
Category: req.Category,
})
if lastErr != nil {
glog.Warning(ctx, "京东下单失败尝试切换Cookie重试", g.Map{
"orderId": req.OrderId,
"cookieId": availableCookieId,
"triedCookies": len(triedCookies),
"error": lastErr,
"createOrderRes": createOrderRes,
})
isCkFailed := false
remark := ""
if createOrderRes != nil {
remark = createOrderRes.Remark
isCkFailed = createOrderRes.IsCkError
}
if createOrderRes != nil && createOrderRes.IsStockError {
// 记录失败历史
_ = s.RecordCookieHistory(ctx, &model.RecordCookieHistoryReq{
CookieId: availableCookieId,
ChangeType: consts.CookieChangeTypeFail,
StatusBefore: consts.JdCookieStatusNormal,
StatusAfter: consts.JdCookieStatusNormal,
OrderId: req.OrderId,
UserOrderId: req.UserOrderId,
FailureCount: 0,
Remark: remark,
})
err = gerror.New("库存失败" + lastErr.Error())
break
}
// Cookie失败更新状态
s.handleCookieFailure(ctx, req.UserOrderId, availableCookieId, "", isCkFailed, remark)
// 继续下一次重试
continue
}
// 下单成功,创建京东订单记录
err = s.createJdOrderRecord(ctx, &model.CreateJdOrderRecordReq{
JdOrderId: jdOrderId, // 内部订单号
RealJdOrderId: createOrderRes.RealJdOrderId, // 京东客户端返回的真实订单ID
PayId: createOrderRes.PayId,
CookieId: availableCookieId,
Category: req.Category,
Amount: req.Amount,
WxPayUrl: createOrderRes.WxPayUrl,
})
if err != nil {
glog.Error(ctx, "创建京东订单记录失败", g.Map{
"jdOrderId": jdOrderId,
"error": err,
})
return nil, gerror.Wrap(err, "创建京东订单记录失败")
}
// 记录京东订单创建历史
_ = s.RecordJdOrderHistory(ctx, jdOrderId, consts.JdOrderChangeTypeCreate, req.OrderId, createOrderRes.WxPayUrl, "")
glog.Info(ctx, "创建京东订单成功", g.Map{
"orderId": req.OrderId,
"jdOrderId": jdOrderId,
"cookieId": availableCookieId,
"triedCookies": len(triedCookies),
})
// 返回成功结果
return &model.CreateNewJdOrderWithRetryRes{
JdOrderId: jdOrderId,
CookieId: availableCookieId,
WxPayUrl: createOrderRes.WxPayUrl,
}, nil
}
// 所有重试都失败了
if lastErr == nil {
lastErr = gerror.New(consts.ErrCodeCookieNotAvailable)
}
glog.Error(ctx, "创建京东订单失败所有Cookie均不可用", g.Map{
"orderId": req.OrderId,
"triedCookies": triedCookies,
"error": lastErr,
})
return nil, gerror.Wrapf(lastErr, "创建京东订单失败,已尝试%d个Cookie", len(triedCookies))
}
// hasCookieBeenTried 检查Cookie是否已经尝试过
func (s *sJdCookie) hasCookieBeenTried(triedCookies []string, cookieId string) bool {
for _, tried := range triedCookies {
if tried == cookieId {
return true
}
}
return false
}
// retryCreateJdOrderForFailedOrder 为失败的订单重新创建京东订单
func (s *sJdCookie) retryCreateJdOrderForFailedOrder(ctx context.Context, existingOrder *entity.V1JdCookieOrder, req *model.CreateOrderReq) (result *model.CreateOrderResult, err error) {
glog.Info(ctx, "开始为失败订单重新创建京东订单", g.Map{
"orderId": existingOrder.OrderId,
"userOrderId": existingOrder.UserOrderId,
"amount": existingOrder.Amount,
})
// 优先尝试复用现有的京东订单
reusableJdOrder, err := s.findReusableJdOrder(ctx, gconv.Float64(existingOrder.Amount), consts.RedeemOrderCardCategory(existingOrder.Category))
if err != nil {
glog.Warning(ctx, "查找可复用京东订单失败", err)
}
var cookieId, jdOrderId, wxPayUrl string
var isReused = false
if reusableJdOrder != nil {
// 尝试使用可复用的京东订单
jdOrderId = reusableJdOrder.JdOrderId
cookieId = reusableJdOrder.CookieId
wxPayUrl = reusableJdOrder.WxPayUrl
// 检查支付链接是否过期
if reusableJdOrder.WxPayExpireAt != nil && gtime.Now().After(reusableJdOrder.WxPayExpireAt) {
// 支付链接已过期,尝试刷新
newWxPayUrl, isCkFailed, refreshErr := s.refreshPaymentUrl(ctx, &model.RefreshPaymentUrlReq{
JdOrderId: jdOrderId,
PayId: reusableJdOrder.PayId,
CookieId: cookieId,
})
if isCkFailed {
s.handleCookieFailure(ctx, req.UserOrderId, cookieId, jdOrderId, isCkFailed, refreshErr.Error())
}
if refreshErr != nil {
glog.Warning(ctx, "刷新支付链接失败,将创建新订单", g.Map{
"jdOrderId": jdOrderId,
"error": refreshErr,
})
// 刷新失败,标记为不可复用
_ = s.UpdateJdOrderStatus(ctx, jdOrderId, consts.JdOrderStatusExpired, "", refreshErr.Error())
// 清空,准备创建新订单
jdOrderId = ""
cookieId = ""
wxPayUrl = ""
} else {
wxPayUrl = newWxPayUrl
// 更新京东订单的支付链接和过期时间
_ = s.updateJdOrderPaymentUrl(ctx, jdOrderId, wxPayUrl)
isReused = true
}
} else {
isReused = true
}
if isReused {
glog.Info(ctx, "复用现有京东订单", g.Map{
"orderId": existingOrder.OrderId,
"jdOrderId": jdOrderId,
"cookieId": cookieId,
})
}
}
// 如果没有成功复用,创建新的京东订单
if jdOrderId == "" {
retryRes, retryErr := s.createNewJdOrderWithRetry(ctx, &model.CreateNewJdOrderWithRetryReq{
OrderId: existingOrder.OrderId,
Amount: gconv.Float64(existingOrder.Amount),
Category: consts.RedeemOrderCardCategory(existingOrder.Category),
UserOrderId: existingOrder.UserOrderId,
})
if retryErr != nil {
// 京东订单创建失败,更新本地订单失败原因
_ = s.updateOrderFailure(ctx, existingOrder.OrderId)
// 记录订单重试失败的变更历史
_ = s.RecordOrderHistory(ctx, &model.RecordOrderHistoryReq{
OrderId: existingOrder.OrderId,
ChangeType: consts.OrderChangeTypeJDOrderFailed,
JdOrderId: "",
Remark: retryErr.Error(),
})
glog.Error(ctx, "重试创建京东订单失败", g.Map{
"orderId": existingOrder.OrderId,
"userOrderId": existingOrder.UserOrderId,
"failureReason": retryErr.Error(),
})
return nil, retryErr
}
jdOrderId = retryRes.JdOrderId
cookieId = retryRes.CookieId
wxPayUrl = retryRes.WxPayUrl
}
// 京东订单创建成功,更新本地订单状态为待支付,并关联京东订单
err = s.updateOrderSuccess(ctx, existingOrder.OrderId, jdOrderId)
if err != nil {
glog.Error(ctx, "更新订单状态失败", g.Map{
"orderId": existingOrder.OrderId,
"jdOrderId": jdOrderId,
"error": err,
})
return nil, gerror.Wrap(err, "更新订单信息失败")
}
// 更新京东订单的当前关联订单ID
_ = s.updateJdOrderId(ctx, jdOrderId, existingOrder.OrderId)
// 记录Cookie使用历史
_ = s.RecordCookieHistory(ctx, &model.RecordCookieHistoryReq{
CookieId: cookieId,
ChangeType: consts.CookieChangeTypeUse,
StatusBefore: consts.JdCookieStatusUnknown,
StatusAfter: consts.JdCookieStatusNormal,
OrderId: existingOrder.OrderId,
UserOrderId: existingOrder.UserOrderId,
FailureCount: 0,
Remark: "失败订单重试成功",
})
// 记录订单重新绑定历史
_ = s.RecordOrderHistory(ctx, &model.RecordOrderHistoryReq{
OrderId: existingOrder.OrderId,
ChangeType: consts.OrderChangeTypeRebind,
JdOrderId: jdOrderId,
Remark: "",
})
glog.Info(ctx, "失败订单重试成功", g.Map{
"orderId": existingOrder.OrderId,
"jdOrderId": jdOrderId,
"cookieId": cookieId,
})
return &model.CreateOrderResult{
WxPayUrl: wxPayUrl,
JdOrderId: jdOrderId,
OrderId: existingOrder.OrderId,
}, nil
}