feat(kami_gateway): 新增卡密订单提交与回调功能

- 新增 kami_gateway 包,实现订单提交接口
- 实现 SubmitOrder 方法,支持签名生成与表单提交
- 新增 model 定义 SubmitOrderReq 和 SubmitOrderResponse 结构体
- 新增工具函数 GetMD5SignMF 用于生成 MD5 签名
- 修改订单回调逻辑,使用新的网关接口替代原有 HTTP 请求
- 更新京东订单状态变更类型,增加 Callback 类型
- 调整过期订单清理时间从 24 小时改为30 分钟- 移除冗余的订单创建前释放过期订单逻辑
- 删除无用的 ValueIsNil 错误处理函数
- 更新 boot_enums.go 中的枚举定义,增加 jd_order_change_type 的 callback 类型
- 优化 cron任务中的京东支付状态监控逻辑,提前执行过期订单清理
- 修复 jd_cookie 包中查询 jd_order时缺少 limit 条件的问题
This commit is contained in:
danial
2025-10-20 21:38:41 +08:00
parent 79c8a28c26
commit 0e157e1e61
10 changed files with 233 additions and 54 deletions

File diff suppressed because one or more lines are too long

View File

@@ -92,28 +92,30 @@ const (
type JdOrderChangeType string
const (
JdOrderChangeTypeCreate JdOrderChangeType = "create" // 创建
JdOrderChangeTypeBind JdOrderChangeType = "bind" // 绑定
JdOrderChangeTypeUnbind JdOrderChangeType = "unbind" // 解绑
JdOrderChangeTypePendPay JdOrderChangeType = "pendPay" // 待支付
JdOrderChangeTypePay JdOrderChangeType = "pay" // 支付
JdOrderChangeTypeExpire JdOrderChangeType = "expire" // 过期
JdOrderChangeTypeInvalid JdOrderChangeType = "invalid" // 失效(新增)
JdOrderChangeTypeSend JdOrderChangeType = "send" // 发货
JdOrderChangeTypeReplace JdOrderChangeType = "replace" // 换绑
JdOrderChangeTypeCreate JdOrderChangeType = "create" // 创建
JdOrderChangeTypeBind JdOrderChangeType = "bind" // 绑定
JdOrderChangeTypeUnbind JdOrderChangeType = "unbind" // 解绑
JdOrderChangeTypePendPay JdOrderChangeType = "pendPay" // 待支付
JdOrderChangeTypePay JdOrderChangeType = "pay" // 支付
JdOrderChangeTypeExpire JdOrderChangeType = "expire" // 过期
JdOrderChangeTypeInvalid JdOrderChangeType = "invalid" // 失效(新增)
JdOrderChangeTypeSend JdOrderChangeType = "send" // 发货
JdOrderChangeTypeReplace JdOrderChangeType = "replace" // 换绑
JdOrderChangeTypeCallback JdOrderChangeType = "callback" // 回调
)
// JdOrderChangeTypeText 京东订单变更类型文本映射
var JdOrderChangeTypeText = map[JdOrderChangeType]string{
JdOrderChangeTypeCreate: "创建",
JdOrderChangeTypeBind: "绑定",
JdOrderChangeTypeUnbind: "解绑",
JdOrderChangeTypePay: "支付",
JdOrderChangeTypeExpire: "过期",
JdOrderChangeTypeInvalid: "失效",
JdOrderChangeTypeSend: "发货",
JdOrderChangeTypeReplace: "换绑",
JdOrderChangeTypePendPay: "待支付",
JdOrderChangeTypeCreate: "创建",
JdOrderChangeTypeBind: "绑定",
JdOrderChangeTypeUnbind: "解绑",
JdOrderChangeTypePay: "支付",
JdOrderChangeTypeExpire: "过期",
JdOrderChangeTypeInvalid: "失效",
JdOrderChangeTypeSend: "发货",
JdOrderChangeTypeReplace: "换绑",
JdOrderChangeTypePendPay: "待支付",
JdOrderChangeTypeCallback: "回调",
}
// OrderChangeType 订单变更类型

View File

@@ -20,8 +20,6 @@ import (
// 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("用户订单号不能为空")
}

View File

@@ -11,12 +11,12 @@ import (
"kami/internal/model/entity"
"kami/utility/cache"
"kami/utility/config"
"kami/utility/integration/kami_gateway"
"slices"
"time"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/gclient"
"github.com/gogf/gf/v2/os/glog"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
@@ -521,40 +521,59 @@ func (s *sJdCookie) ExtractCardInfo(ctx context.Context, jdOrderId string) error
_ = s.RecordJdOrderHistory(ctx, jdOrderId, consts.JdOrderChangeTypeSend, jdOrder.OrderId, jdOrder.WxPayUrl, "")
//提取成功要回调
go s.callback(ctx, jdOrder.OrderId)
glog.Info(ctx, "卡密提取成功", g.Map{
"jdOrderId": jdOrderId,
"cardNo": resp.CardNo,
"orderId": jdOrder.OrderId,
})
//提取成功要回调
go s.callback(ctx, jdOrder, resp.CardNo, resp.CardPassword)
return nil
}
// callback TODO:临时的回调
func (s *sJdCookie) callback(ctx context.Context, orderId string) {
var order *entity.V1JdCookieOrder
// callback
func (s *sJdCookie) callback(ctx context.Context, jdOrder *entity.V1JdCookieJdOrder, cardNo, cardPassword string) {
var cookieOrder *entity.V1JdCookieOrder
if err := dao.V1JdCookieOrder.Ctx(ctx).DB(config.GetDatabaseV1()).
Where(dao.V1JdCookieOrder.Columns().OrderId, orderId).Scan(&order); err != nil || order == nil || order.Id == 0 {
glog.Error(ctx, "查询订单失败", g.Map{"orderId": orderId, "err": err})
Where(dao.V1JdCookieOrder.Columns().OrderId, jdOrder.OrderId).Scan(&cookieOrder); err != nil || cookieOrder == nil || cookieOrder.Id == 0 {
glog.Error(ctx, "查询订单失败", g.Map{"orderId": jdOrder.OrderId, "err": err})
return
}
var data *entity.V1OrderInfo
if err := dao.V1OrderInfo.Ctx(ctx).DB(config.GetDatabaseV1()).Where(dao.V1OrderInfo.Columns().BankOrderId, order.UserOrderId).Scan(&data); err != nil || data == nil || data.Id == 0 {
glog.Error(ctx, "查询订单失败", g.Map{"userOrderId": order.UserOrderId, "err": err})
var orderInfo *entity.V1OrderInfo
if err := dao.V1OrderInfo.Ctx(ctx).DB(config.GetDatabaseV1()).Where(dao.V1OrderInfo.Columns().BankOrderId, cookieOrder.UserOrderId).Scan(&orderInfo); err != nil || orderInfo == nil || orderInfo.Id == 0 {
glog.Error(ctx, "查询订单失败", g.Map{"userOrderId": cookieOrder.UserOrderId, "err": err})
return
}
response, _ := gclient.New().Get(ctx, "http://kami_gateway:12309/appleCard/notify", g.Map{
"attach": data.BankOrderId,
"merchantId": orderId,
"amount": order.Amount,
"status": "1",
"sign": "123456",
var merchantInfo *entity.V1MerchantInfo
if err := dao.V1MerchantInfo.Ctx(ctx).DB(config.GetDatabaseV1()).Where(dao.V1MerchantInfo.Columns().MerchantUid, orderInfo.MerchantUid).Scan(&merchantInfo); err != nil || merchantInfo == nil || merchantInfo.Id == 0 {
glog.Error(ctx, "查询商户信息失败", g.Map{"merchantId": orderInfo.MerchantUid, "err": err})
return
}
_, err := kami_gateway.SubmitOrder(ctx, &kami_gateway.SubmitOrderReq{
OrderPeriod: 24,
NotifyUrl: "https://baidu.com",
OrderPrice: orderInfo.OrderAmount.String(),
OrderNo: orderInfo.MerchantOrderId,
ProductCode: orderInfo.PayProductCode,
ExValue: (&kami_gateway.RedeemCardInfo{
FaceType: orderInfo.OrderAmount.String(),
RecoveryType: "8",
Data: cardNo,
CardNo: cardPassword,
}).ToJson(),
Ip: "127.0.0.1",
PayKey: merchantInfo.MerchantKey,
PaySecret: merchantInfo.MerchantSecret,
Url: "http://kami_gateway",
})
glog.Info(ctx, "回调成功", g.Map{"response": response.ReadAllString()})
if err != nil {
_ = s.RecordJdOrderHistory(ctx, jdOrder.JdOrderId, consts.JdOrderChangeTypeCallback, "", "", "回调失败")
} else {
_ = s.RecordJdOrderHistory(ctx, jdOrder.JdOrderId, consts.JdOrderChangeTypeCallback, "", "", "回调成功")
}
}
// shouldExtractCard 判断是否需要提取卡密
@@ -596,8 +615,8 @@ func (s *sJdCookie) CleanupExpiredOrders(ctx context.Context) error {
_, err = jdOrderModel.
Where(dao.V1JdCookieJdOrder.Columns().Status, int(consts.JdOrderStatusPending)).
WhereLT(dao.V1JdCookieJdOrder.Columns().OrderExpireAt, gtime.Now()).
Update(g.Map{
dao.V1JdCookieJdOrder.Columns().Status: int(consts.JdOrderStatusExpired),
Update(do.V1JdCookieJdOrder{
Status: int(consts.JdOrderStatusExpired),
})
if err != nil {
glog.Error(ctx, "清理过期京东订单失败", err)
@@ -626,7 +645,7 @@ func (s *sJdCookie) CleanupExpiredOrders(ctx context.Context) error {
var expiredOrders []*entity.V1JdCookieOrder
err = orderModel.
Where(dao.V1JdCookieOrder.Columns().Status, int(consts.OrderStatusPending)).
WhereLT(dao.V1JdCookieOrder.Columns().CreatedAt, gtime.Now().Add(-time.Hour*24)).
WhereLT(dao.V1JdCookieOrder.Columns().CreatedAt, gtime.Now().Add(-time.Hour*30)).
Scan(&expiredOrders)
if err != nil {
glog.Error(ctx, "查询过期用户订单失败", err)
@@ -636,8 +655,8 @@ func (s *sJdCookie) CleanupExpiredOrders(ctx context.Context) error {
// 批量更新过期状态
_, err = orderModel.
Where(dao.V1JdCookieOrder.Columns().Status, int(consts.OrderStatusPending)).
WhereLT(dao.V1JdCookieOrder.Columns().CreatedAt, gtime.Now().Add(-time.Hour*24)).
Update(do.V1JdCookieJdOrder{
WhereLT(dao.V1JdCookieOrder.Columns().CreatedAt, gtime.Now().Add(-time.Minute*30)).
Update(do.V1JdCookieOrder{
Status: consts.OrderStatusExpired,
})
if err != nil {

View File

@@ -248,7 +248,6 @@ func (s *sJdCookie) findReusableJdOrder(ctx context.Context, amount float64, cat
WhereNull(dao.V1JdCookieJdOrder.Columns().OrderId).
WhereGT(dao.V1JdCookieJdOrder.Columns().OrderExpireAt, gtime.Now()).
OrderAsc(dao.V1JdCookieJdOrder.Columns().CreatedAt).
Limit(1).
Scan(&jdOrder)
return

View File

@@ -55,7 +55,12 @@ func Register(ctx context.Context) {
tracer := gtrace.NewTracer("京东支付状态监控任务")
ctx, span := tracer.Start(ctx, "京东支付状态监控任务", trace.WithNewRoot())
defer span.End()
if err := service.JdCookie().ReleaseExpiredJdOrders(ctx); err != nil {
glog.Error(ctx, "释放过期京东订单失败", err)
}
if err := service.JdCookie().CleanupExpiredOrders(ctx); err != nil {
glog.Error(ctx, "清理过期订单失败", err)
}
glog.Debug(ctx, "开始执行京东支付状态监控任务")
if err := service.JdCookie().BatchCheckPaymentStatus(ctx); err != nil {
glog.Error(ctx, "京东支付状态监控任务失败", err)

View File

@@ -0,0 +1,47 @@
package kami_gateway
import (
"context"
"encoding/json"
"fmt"
"github.com/duke-git/lancet/v2/convertor"
"github.com/gogf/gf/v2/net/gclient"
"github.com/gogf/gf/v2/net/gtrace"
"github.com/gogf/gf/v2/os/glog"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
func SubmitOrder(ctx context.Context, input *SubmitOrderReq) (*SubmitOrderResponse, error) {
ctx, span := gtrace.NewSpan(ctx, "submitOrder", trace.WithAttributes(
attribute.String("input", fmt.Sprintf("%+v", input))),
)
defer span.End()
input.NotifyUrl = input.GetNotifyUrl()
params := input.ToStrMap()
params["sign"] = GetMD5SignMF(params, input.PaySecret)
paramsStr := map[string]string{}
for k, v := range params {
paramsStr[k] = convertor.ToString(v)
}
response, err := gclient.New().PostForm(ctx, input.GetUrl(), paramsStr)
if err != nil {
glog.Error(ctx, "提交订单失败", "url", input.GetUrl(), "params", params, "error", err)
return nil, err
}
respData := response.ReadAll()
glog.Info(ctx, "提交订单成功", "url", input.GetUrl(), "params", params, "response", string(respData))
submitOrderResponse := SubmitOrderResponse{}
err = json.Unmarshal(respData, &submitOrderResponse)
if err != nil {
glog.Error(ctx, "解析订单失败", "error", err)
return nil, err
}
return &submitOrderResponse, nil
}

View File

@@ -0,0 +1,65 @@
package kami_gateway
import (
"encoding/json"
"fmt"
"strconv"
)
type SubmitOrderReq struct {
OrderPeriod int `json:"orderPeriod"`
NotifyUrl string `json:"notifyUrl"`
OrderPrice string `json:"orderPrice"`
OrderNo string `json:"orderNo"`
ProductCode string `json:"productCode"`
ExValue string `json:"exValue"`
Ip string `json:"ip"`
PayKey string `json:"payKey"`
PaySecret string `json:"paySecret"`
Url string `json:"url"`
}
func (s *SubmitOrderReq) GetUrl() string {
return fmt.Sprintf("%s/gateway/scan", "http://kami_gateway")
}
func (s *SubmitOrderReq) ToStrMap() map[string]any {
data := map[string]any{
"orderPeriod": strconv.Itoa(s.OrderPeriod),
"notifyUrl": s.GetNotifyUrl(),
"orderPrice": s.OrderPrice,
"orderNo": s.OrderNo,
"productCode": s.ProductCode,
"exValue": s.ExValue,
"ip": s.Ip,
"payKey": s.PayKey,
}
return data
}
func (s *SubmitOrderReq) GetNotifyUrl() string {
notifyUrl := s.NotifyUrl
if notifyUrl != "" {
return notifyUrl
}
return fmt.Sprintf("%s/myself/notify", "http://kami_gateway")
}
type SubmitOrderResponse struct {
PayKey string `json:"payKey"`
StatusCode string `json:"statusCode"`
Msg string `json:"msg"`
Code int `json:"code"`
}
type RedeemCardInfo struct {
FaceType string `json:"faceType"` // 面额
RecoveryType string `json:"RecoveryType,omitempty"` // 类型 2 仅卡密 8 卡号卡密
Data string `json:"data"` // 卡密
CardNo string `json:"cardNo,omitempty"` // 卡号
}
func (d *RedeemCardInfo) ToJson() string {
jsonStr, _ := json.Marshal(d)
return string(jsonStr)
}

View File

@@ -0,0 +1,51 @@
package kami_gateway
import (
"crypto/md5"
"encoding/hex"
"sort"
"github.com/duke-git/lancet/v2/convertor"
)
func GetMD5SignMF(params map[string]any, paySecret string) string {
strArr := SortMap(params)
signStr := ""
for i := range strArr {
k := strArr[i]
if len(convertor.ToString(params[k])) == 0 {
signStr += k
} else {
signStr += k + convertor.ToString(params[k])
}
}
signStr += paySecret
return GetMd5Lower(signStr)
}
// SortMap 对map的key值进行排序
func SortMap(m map[string]any) []string {
var arr []string
for k := range m {
arr = append(arr, k)
}
sort.Strings(arr)
return arr
}
// SortMapByKeys 按照key的ascii值从小到大给map排序
func SortMapByKeys(m map[string]any) map[string]any {
keys := SortMap(m)
tmp := make(map[string]any)
for _, key := range keys {
tmp[key] = convertor.ToString(m[key])
}
return tmp
}
// GetMd5Lower 获取小写的MD5
func GetMd5Lower(s string) string {
h := md5.New()
h.Write([]byte(s))
return hex.EncodeToString(h.Sum(nil))
}

View File

@@ -18,10 +18,3 @@ func ErrIsNil(ctx context.Context, err error, msg ...string) {
}
}
}
// ValueIsNil 判断值是否是nil
func ValueIsNil(value interface{}, msg string) {
if g.IsNil(value) {
panic(msg)
}
}