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

419 lines
14 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/dao"
"kami/internal/model"
"kami/internal/model/do"
"kami/internal/model/entity"
"kami/utility/config"
"kami/utility/integration/originalJd"
"kami/utility/utils"
"strings"
"time"
"github.com/duke-git/lancet/v2/convertor"
"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"
)
// ====================================================================================
// 数据库查询辅助方法
// ====================================================================================
// getOrderByOrderId 根据订单号查询订单
func (s *sJdCookie) getOrderByOrderId(ctx context.Context, orderId string) (order *entity.V1JdCookieOrder, err error) {
m := dao.V1JdCookieOrder.Ctx(ctx).DB(config.GetDatabaseV1())
err = m.Where(dao.V1JdCookieOrder.Columns().OrderId, orderId).Scan(&order)
return
}
// getOrderByUserOrderId 根据用户订单号查询订单
func (s *sJdCookie) getOrderByUserOrderId(ctx context.Context, userOrderId string) (order *entity.V1JdCookieOrder, err error) {
m := dao.V1JdCookieOrder.Ctx(ctx).DB(config.GetDatabaseV1())
err = m.Where(dao.V1JdCookieOrder.Columns().UserOrderId, userOrderId).Scan(&order)
err = utils.HandleNoRowsError(err)
return
}
// getJdOrderByJdOrderId 根据京东订单号查询京东订单
func (s *sJdCookie) getJdOrderByJdOrderId(ctx context.Context, jdOrderId string) (jdOrder *entity.V1JdCookieJdOrder, err error) {
m := dao.V1JdCookieJdOrder.Ctx(ctx).DB(config.GetDatabaseV1())
err = m.Where(dao.V1JdCookieJdOrder.Columns().JdOrderId, jdOrderId).Scan(&jdOrder)
return
}
// getCookieById 根据Cookie ID获取Cookie信息
func (s *sJdCookie) getCookieById(ctx context.Context, cookieId string) (cookie *entity.V1JdCookieAccount, err error) {
m := dao.V1JdCookieAccount.Ctx(ctx).DB(config.GetDatabaseV1())
err = m.Where(dao.V1JdCookieAccount.Columns().CookieId, cookieId).Scan(&cookie)
return
}
// ====================================================================================
// 数据库更新辅助方法
// ====================================================================================
// createJdOrderRecord 创建京东订单记录
func (s *sJdCookie) createJdOrderRecord(ctx context.Context, req *model.CreateJdOrderRecordReq) error {
m := dao.V1JdCookieJdOrder.Ctx(ctx).DB(config.GetDatabaseV1())
_, err := m.Insert(&do.V1JdCookieJdOrder{
JdOrderId: req.JdOrderId,
RealJdOrderId: req.RealJdOrderId,
PayId: req.PayId,
Amount: req.Amount,
Category: req.Category,
CookieId: req.CookieId,
Status: int(consts.JdOrderStatusPending),
WxPayUrl: req.WxPayUrl,
WxPayExpireAt: gtime.Now().Add(time.Minute * consts.WxPayUrlExpireDuration),
OrderExpireAt: gtime.Now().Add(time.Hour * consts.JdOrderExpireDuration),
})
return err
}
// createOrderRecord 创建订单记录(带并发冲突处理)
func (s *sJdCookie) createOrderRecord(ctx context.Context, internalOrderId, userOrderId string, amount float64, category consts.RedeemOrderCardCategory, jdOrderId, wxPayUrl string) error {
m := dao.V1JdCookieOrder.Ctx(ctx).DB(config.GetDatabaseV1())
// 尝试插入订单记录
_, err := m.Insert(&do.V1JdCookieOrder{
OrderId: internalOrderId,
UserOrderId: userOrderId,
Amount: amount,
Category: category,
JdOrderId: jdOrderId,
Status: int(consts.OrderStatusCreated),
})
if err != nil {
// 检查是否是唯一索引冲突错误MySQL duplicate entry error
// 这里可以根据数据库类型调整错误检测逻辑
if strings.Contains(strings.ToLower(err.Error()), "duplicate") {
glog.Warning(ctx, "检测到重复订单,可能是并发创建导致", g.Map{
"userOrderId": userOrderId,
"internalOrderId": internalOrderId,
"error": err,
})
return gerror.New("订单已存在,请勿重复创建")
}
// 其他类型的错误
glog.Error(ctx, "创建订单记录失败", g.Map{
"userOrderId": userOrderId,
"internalOrderId": internalOrderId,
"error": err,
})
return gerror.Wrap(err, "创建订单记录失败")
}
return nil
}
// updateOrderLastRequest 更新订单最后请求时间
func (s *sJdCookie) updateOrderLastRequest(ctx context.Context, orderId string) error {
m := dao.V1JdCookieOrder.Ctx(ctx).DB(config.GetDatabaseV1())
_, err := m.Where(dao.V1JdCookieOrder.Columns().OrderId, orderId).Update(&do.V1JdCookieOrder{
LastRequestAt: gtime.Now(),
})
return err
}
// updateOrderJdOrderId 更新订单关联的京东订单ID
func (s *sJdCookie) updateOrderJdOrderId(ctx context.Context, orderId, jdOrderId string) error {
m := dao.V1JdCookieOrder.Ctx(ctx).DB(config.GetDatabaseV1())
updateData := &do.V1JdCookieOrder{
JdOrderId: jdOrderId,
}
// 注意wxPayUrl 字段需要数据库表支持后才能使用
// 暂时通过 WxPayUrl 字段存储在 jd_order 表中
_, err := m.Where(dao.V1JdCookieOrder.Columns().OrderId, orderId).Update(updateData)
return err
}
// updateOrderFailure 更新订单失败信息
func (s *sJdCookie) updateOrderFailure(ctx context.Context, orderId string) error {
m := dao.V1JdCookieOrder.Ctx(ctx).DB(config.GetDatabaseV1())
updateData := &do.V1JdCookieOrder{
Status: int(consts.OrderStatusJDOrderFailed),
}
// 注意:FailureReason 字段需要数据库表支持后才能使用
// 暂时通过日志记录失败原因
glog.Error(ctx, "订单创建失败", g.Map{
"orderId": orderId,
})
_, err := m.Where(dao.V1JdCookieOrder.Columns().OrderId, orderId).Update(updateData)
return err
}
// updateOrderSuccess 更新订单成功信息(从失败状态恢复)
func (s *sJdCookie) updateOrderSuccess(ctx context.Context, orderId, jdOrderId string) error {
m := dao.V1JdCookieOrder.Ctx(ctx).DB(config.GetDatabaseV1())
updateData := &do.V1JdCookieOrder{
JdOrderId: jdOrderId,
Status: int(consts.OrderStatusPending), // 恢复为待支付状态
}
glog.Info(ctx, "订单重试成功,恢复待支付状态", g.Map{
"orderId": orderId,
"jdOrderId": jdOrderId,
})
_, err := m.Where(dao.V1JdCookieOrder.Columns().OrderId, orderId).Update(updateData)
return err
}
// updateJdOrderPaymentUrl 更新京东订单的支付链接
func (s *sJdCookie) updateJdOrderPaymentUrl(ctx context.Context, jdOrderId, wxPayUrl string) error {
m := dao.V1JdCookieJdOrder.Ctx(ctx).DB(config.GetDatabaseV1())
// 获取更新前的订单信息
var oldOrder *entity.V1JdCookieJdOrder
err := m.Where(dao.V1JdCookieJdOrder.Columns().JdOrderId, jdOrderId).Scan(&oldOrder)
if err != nil || oldOrder == nil {
glog.Warning(ctx, "查询京东订单失败,无法记录历史", err)
// 即使查询失败也继续更新
}
_, err = m.Where(dao.V1JdCookieJdOrder.Columns().JdOrderId, jdOrderId).Update(&do.V1JdCookieJdOrder{
WxPayUrl: wxPayUrl,
WxPayExpireAt: gtime.Now().Add(time.Minute * consts.WxPayUrlExpireDuration),
})
if err != nil {
return err
}
// 记录支付链接更新历史
if oldOrder != nil && oldOrder.WxPayUrl != wxPayUrl {
_ = s.RecordJdOrderHistory(ctx, jdOrderId, consts.JdOrderChangeTypeSend, oldOrder.OrderId, wxPayUrl, "")
}
return err
}
// updateJdOrderId 更新京东订单关联的用户订单ID
func (s *sJdCookie) updateJdOrderId(ctx context.Context, jdOrderId, orderId string) error {
m := dao.V1JdCookieJdOrder.Ctx(ctx).DB(config.GetDatabaseV1())
// 获取更新前的京东订单信息
var oldJdOrder *entity.V1JdCookieJdOrder
err := m.Where(dao.V1JdCookieJdOrder.Columns().JdOrderId, jdOrderId).Scan(&oldJdOrder)
if err != nil || oldJdOrder == nil {
glog.Warning(ctx, "查询京东订单失败,无法记录历史", err)
// 即使查询失败也继续更新
}
// 更新订单ID
_, err = m.Where(dao.V1JdCookieJdOrder.Columns().JdOrderId, jdOrderId).Update(&do.V1JdCookieJdOrder{
OrderId: orderId,
})
if err != nil {
return err
}
// 记录订单绑定历史
var changeType consts.JdOrderChangeType
if oldJdOrder != nil {
if oldJdOrder.OrderId == "" {
changeType = consts.JdOrderChangeTypeBind // 首次绑定
} else if oldJdOrder.OrderId != orderId {
changeType = consts.JdOrderChangeTypeReplace // 更换绑定
}
} else {
changeType = consts.JdOrderChangeTypeBind // 首次绑定
}
// 获取京东订单的支付链接
wxPayUrl := ""
if oldJdOrder != nil {
wxPayUrl = oldJdOrder.WxPayUrl
}
_ = s.RecordJdOrderHistory(ctx, jdOrderId, changeType, orderId, wxPayUrl, "")
return err
}
// ====================================================================================
// 订单复用相关方法
// ====================================================================================
// findReusableJdOrder 查找可复用的京东订单
func (s *sJdCookie) findReusableJdOrder(ctx context.Context, amount float64, category consts.RedeemOrderCardCategory) (jdOrder *entity.V1JdCookieJdOrder, err error) {
m := dao.V1JdCookieJdOrder.Ctx(ctx).DB(config.GetDatabaseV1())
// 查找符合条件的京东订单:
// 1. 金额和品类相同
// 2. 状态为待支付
// 3. 没有关联订单
// 4. 订单未过期
err = m.Where(dao.V1JdCookieJdOrder.Columns().Amount, amount).
Where(dao.V1JdCookieJdOrder.Columns().Category, category).
Where(dao.V1JdCookieJdOrder.Columns().Status, int(consts.JdOrderStatusPending)).
WhereNull(dao.V1JdCookieJdOrder.Columns().OrderId).
WhereGT(dao.V1JdCookieJdOrder.Columns().OrderExpireAt, gtime.Now()).
OrderAsc(dao.V1JdCookieJdOrder.Columns().CreatedAt).
Limit(1).
Scan(&jdOrder)
return
}
// ====================================================================================
// 京东接口调用方法
// ====================================================================================
// callJdCreateOrder 调用京东下单接口
func (s *sJdCookie) callJdCreateOrder(ctx context.Context, req *model.CallJdCreateOrderReq) (res *model.CallJdCreateOrderRes, err error) {
res = &model.CallJdCreateOrderRes{
IsCkError: false,
}
cookie, err := s.getCookieById(ctx, req.CookieId)
if err != nil {
return
}
appResp, err := originalJd.NewClient().AppleRecharge(ctx, &originalJd.AppleRechargeReq{
FacePrice: req.Amount,
Category: req.Category,
OrderNum: req.JdOrderId,
Cookies: cookie.CookieValue,
})
glog.Info(ctx, "下单接口返回", appResp)
if err != nil {
if appResp != nil {
res.Remark = appResp.Msg
if appResp.Code == consts.CardCookieJDStatusStockError {
res.IsStockError = true
}
if appResp.Code == consts.CardCookieJDStatusCkFailed {
res.IsCkError = true
}
}
return
}
res.Remark = appResp.Msg
if appResp.Code != consts.CardCookieJDStatusSuccess || appResp.Deeplink == "" {
return res, gerror.New("下单失败," + appResp.Msg)
}
res = &model.CallJdCreateOrderRes{
RealJdOrderId: appResp.OrderId, // 京东客户端返回的真实订单ID
PayId: appResp.PayId,
WxPayUrl: appResp.Deeplink,
Remark: appResp.Msg,
}
return
}
// refreshPaymentUrl 刷新支付链接
func (s *sJdCookie) refreshPaymentUrl(ctx context.Context, req *model.RefreshPaymentUrlReq) (wxPayUrl string, isCkFailed bool, err error) {
cookie, err := s.getCookieById(ctx, req.CookieId)
if err != nil {
return
}
jdOrder, err := s.getJdOrderByJdOrderId(ctx, req.JdOrderId)
if err != nil {
return
}
jdOrderInt, _ := convertor.ToInt(jdOrder.RealJdOrderId)
appResp, err := originalJd.NewClient().RefreshPayment(ctx, &originalJd.RefreshPaymentReq{
Cookies: cookie.CookieValue,
OrderId: jdOrderInt,
PayId: req.PayId,
UserOrderId: req.JdOrderId,
})
glog.Info(ctx, "刷新支付链接", g.Map{
"appResp": appResp,
"cookie": cookie,
"jdOrderId": req.JdOrderId,
})
if err != nil {
return
}
if appResp.Code == consts.CardCookieJDStatusExpired {
return "", true, gerror.New(appResp.Msg)
}
if appResp.Code != consts.CardCookieJDStatusSuccess || appResp.Deeplink == "" {
return "", false, gerror.New("刷新链接," + appResp.Msg)
}
wxPayUrl = appResp.Deeplink
return
}
// callJdCheckPayment 调用京东接口检查支付状态
func (s *sJdCookie) callJdCheckPayment(ctx context.Context, req *model.CallJdCheckPaymentReq) (*originalJd.AppleRechargeCardInfoResp, error) {
// 创建京东客户端
client := originalJd.NewClient()
// 调用检查支付状态接口使用京东客户端返回的真实订单ID
appReq := &originalJd.AppleRechargeCardInfoReq{
OrderId: req.JdOrderId,
JdOrderId: req.RealJdOrderId,
Cookies: req.CookieValue,
Category: req.Category,
}
resp, err := client.GetCardInfo(ctx, appReq)
if err != nil {
glog.Error(ctx, "调用京东检查支付状态接口失败", g.Map{
"realJdOrderId": req.RealJdOrderId,
"error": err,
})
return resp, err
}
glog.Info(ctx, "京东检查支付状态成功", resp)
return resp, nil
}
// ====================================================================================
// Cookie处理方法
// ====================================================================================
// handleCookieFailure 处理Cookie失败
func (s *sJdCookie) handleCookieFailure(ctx context.Context, userOrderId, cookieId, orderId string, isCKFailed bool, remark string) {
// 获取当前Cookie信息
m := dao.V1JdCookieAccount.Ctx(ctx).DB(config.GetDatabaseV1())
var account *entity.V1JdCookieAccount
err := m.Where(dao.V1JdCookieAccount.Columns().CookieId, cookieId).Scan(&account)
if err != nil || account == nil {
return
}
newFailureCount := account.FailureCount + 1
statusBefore := consts.JdCookieStatus(account.Status)
var statusAfter consts.JdCookieStatus
if newFailureCount >= consts.JdCookieMaxFailureCount || isCKFailed {
// 失败次数过多,标记为失效
statusAfter = consts.JdCookieStatusExpired
_, _ = m.Where(dao.V1JdCookieAccount.Columns().CookieId, cookieId).Update(&do.V1JdCookieAccount{
Status: int(statusAfter),
FailureCount: newFailureCount,
})
} else {
// 暂停Cookie
statusAfter = consts.JdCookieStatusSuspend
suspendUntil := gtime.Now().Add(time.Minute * consts.JdCookieSuspendDuration)
_, _ = m.Where(dao.V1JdCookieAccount.Columns().CookieId, cookieId).Update(&do.V1JdCookieAccount{
Status: int(statusAfter),
FailureCount: newFailureCount,
SuspendUntil: suspendUntil,
})
}
// 记录失败历史
_ = s.RecordCookieHistory(ctx, &model.RecordCookieHistoryReq{
CookieId: cookieId,
ChangeType: consts.CookieChangeTypeFail,
StatusBefore: statusBefore,
StatusAfter: statusAfter,
OrderId: orderId,
UserOrderId: userOrderId,
FailureCount: newFailureCount,
Remark: remark,
})
}