feat(camel_oil): 支持Token管理与卡密绑定功能

- 新增CamelOilToken和CamelOilCardBinding数据库表,实现Token及卡密绑定记录管理
- 在service层增加Token的创建、查询、更新、删除及分页功能
- 实现卡密与Token绑定的业务逻辑,支持基于Token的卡密管理
- 在API层新增Token和卡密绑定相关接口:创建Token、获取Token详情、删除Token、列出Token及根据Token查询绑定卡密
- camel_oil_api新增绑卡接口,支持绑卡状态分类及错误处理
- 在定时任务中增加卡密绑定任务,实现自动处理已支付订单的卡密绑定
- 优化订单提交及支付流程,包含日志调整和请求参数随机扰动
- 统一调整camel_oil模块多控制器实现,完成账号状态查询及订单相关接口实现
- 注册更多camel_oil定时任务,包括订单支付检查、账号日重置和待回调订单处理任务
This commit is contained in:
danial
2025-11-23 00:08:35 +08:00
parent 0f19ea2a33
commit 3588bf9af6
36 changed files with 1301 additions and 69 deletions

View File

@@ -21,4 +21,9 @@ type ICamelOilV1 interface {
OrderHistory(ctx context.Context, req *v1.OrderHistoryReq) (res *v1.OrderHistoryRes, err error)
AccountOrderList(ctx context.Context, req *v1.AccountOrderListReq) (res *v1.AccountOrderListRes, err error)
OrderCallback(ctx context.Context, req *v1.OrderCallbackReq) (res *v1.OrderCallbackRes, err error)
CreateToken(ctx context.Context, req *v1.CreateTokenReq) (res *v1.CreateTokenRes, err error)
GetToken(ctx context.Context, req *v1.GetTokenReq) (res *v1.GetTokenRes, err error)
ListTokens(ctx context.Context, req *v1.ListTokensReq) (res *v1.ListTokensRes, err error)
DeleteToken(ctx context.Context, req *v1.DeleteTokenReq) (res *v1.DeleteTokenRes, err error)
ListCardBindingsByToken(ctx context.Context, req *v1.ListCardBindingsByTokenReq) (res *v1.ListCardBindingsByTokenRes, err error)
}

106
api/camel_oil/v1/token.go Normal file
View File

@@ -0,0 +1,106 @@
package v1
import (
"kami/api/commonApi"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"github.com/shopspring/decimal"
)
// ====================================================================================
// Token 管理 API 请求/响应结构
// ====================================================================================
// CreateTokenReq 创建 Token 请求
type CreateTokenReq struct {
g.Meta `path:"/token/create" tags:"JD V2 Token Management" method:"post" summary:"创建 Token"`
TokenName string `json:"tokenName" v:"required" description:"Token名称"`
TokenValue string `json:"tokenValue" v:"required" description:"Token值"`
Phone string `json:"phone" description:"绑定的手机号"`
Remark string `json:"remark" description:"备注"`
}
// CreateTokenRes 创建 Token 响应
type CreateTokenRes struct {
TokenId int64 `json:"tokenId" description:"Token ID"`
}
// GetTokenReq 获取 Token 信息请求
type GetTokenReq struct {
g.Meta `path:"/token/get" tags:"JD V2 Token Management" method:"get" summary:"获取 Token 信息"`
TokenId int64 `json:"tokenId" v:"required" description:"Token ID"`
}
// TokenInfo Token 信息
type TokenInfo struct {
Id int64 `json:"id" description:"Token ID"`
TokenName string `json:"tokenName" description:"Token名称"`
TokenValue string `json:"tokenValue" description:"Token值"`
Phone string `json:"phone" description:"绑定的手机号"`
Status int `json:"status" description:"状态"`
BindCount int `json:"bindCount" description:"已绑定卡密数量"`
TotalBindAmount decimal.Decimal `json:"totalBindAmount" description:"累计绑定金额"`
LastBindAt *gtime.Time `json:"lastBindAt" description:"最后绑定时间"`
LastUsedAt *gtime.Time `json:"lastUsedAt" description:"最后使用时间"`
Remark string `json:"remark" description:"备注"`
CreatedAt *gtime.Time `json:"createdAt" description:"创建时间"`
UpdatedAt *gtime.Time `json:"updatedAt" description:"更新时间"`
}
// GetTokenRes 获取 Token 信息响应
type GetTokenRes struct {
Token TokenInfo `json:"token" description:"Token信息"`
}
// ListTokensRes 列出 Token 响应
type ListTokensRes struct {
commonApi.CommonPageRes[TokenInfo]
}
// ListTokensReq 列出 Token 请求
type ListTokensReq struct {
g.Meta `path:"/token/list" tags:"JD V2 Token Management" method:"get" summary:"列出 Token"`
commonApi.CommonPageReq
TokenName string `json:"tokenName" description:"Token名称"`
Status int `json:"status" description:"状态"`
}
// DeleteTokenReq 删除 Token 请求
type DeleteTokenReq struct {
g.Meta `path:"/token/delete" tags:"JD V2 Token Management" method:"post" summary:"删除 Token"`
TokenId int64 `json:"tokenId" v:"required" description:"Token ID"`
}
// DeleteTokenRes 删除 Token 响应
type DeleteTokenRes struct {
Message string `json:"message" description:"信息"`
}
// ====================================================================================
// 卡密绑定 API 请求/响应结构
// ====================================================================================
// CardBindingInfo 卡密绑定信息
type CardBindingInfo struct {
Id int64 `json:"id" description:"绑定记录ID"`
TokenId int64 `json:"tokenId" description:"Token ID"`
TokenName string `json:"tokenName" description:"Token名称"`
OrderId int64 `json:"orderId" description:"订单ID"`
CardNumber string `json:"cardNumber" description:"卡号"`
CardPassword string `json:"cardPassword" description:"卡密"`
Amount decimal.Decimal `json:"amount" description:"绑定金额"`
CreatedAt *gtime.Time `json:"createdAt" description:"创建时间"`
}
// ListCardBindingsByTokenReq 根据 tokenId 查询绑卡记录请求
type ListCardBindingsByTokenReq struct {
g.Meta `path:"/card-binding/list-by-token" tags:"JD V2 Token Management" method:"get" summary:"查询 Token 绑卡记录"`
commonApi.CommonPageReq
TokenId int64 `json:"tokenId" v:"required" description:"Token ID"`
}
// ListCardBindingsByTokenRes 根据 tokenId 查询绑卡记录响应
type ListCardBindingsByTokenRes struct {
commonApi.CommonPageRes[CardBindingInfo]
}

View File

@@ -7,7 +7,7 @@ package card_info_apple
import (
"context"
v1 "kami/api/card_info_apple/v1"
"kami/api/card_info_apple/v1"
)
type ICardInfoAppleV1 interface {

View File

@@ -76,6 +76,22 @@ var CamelOilCallbackStatusText = map[CamelOilNotifyStatus]string{
CamelOilCallbackStatusFailed: "回调失败",
}
// CamelOilTokenStatus Token管理状态枚举
type CamelOilTokenStatus int
const (
CamelOilTokenStatusAvailable CamelOilTokenStatus = 1 // 可用
CamelOilTokenStatusDisabled CamelOilTokenStatus = 2 // 已禁用
CamelOilTokenStatusExpired CamelOilTokenStatus = 3 // 已过期
)
// CamelOilTokenStatusText Token状态文本映射
var CamelOilTokenStatusText = map[CamelOilTokenStatus]string{
CamelOilTokenStatusAvailable: "可用",
CamelOilTokenStatusDisabled: "已禁用",
CamelOilTokenStatusExpired: "已过期",
}
// ====================================================================================
// 变更类型常量定义
// ====================================================================================

View File

@@ -3,12 +3,10 @@ package camel_oil
import (
"context"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"kami/api/camel_oil/v1"
v1 "kami/api/camel_oil/v1"
"kami/internal/service"
)
func (c *ControllerV1) AccountHistory(ctx context.Context, req *v1.AccountHistoryReq) (res *v1.AccountHistoryRes, err error) {
return nil, gerror.NewCode(gcode.CodeNotImplemented)
return service.CamelOil().GetAccountHistory(ctx, req)
}

View File

@@ -3,12 +3,10 @@ package camel_oil
import (
"context"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"kami/api/camel_oil/v1"
v1 "kami/api/camel_oil/v1"
"kami/internal/service"
)
func (c *ControllerV1) AccountOrderList(ctx context.Context, req *v1.AccountOrderListReq) (res *v1.AccountOrderListRes, err error) {
return nil, gerror.NewCode(gcode.CodeNotImplemented)
return service.CamelOil().GetAccountOrders(ctx, req)
}

View File

@@ -3,12 +3,10 @@ package camel_oil
import (
"context"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"kami/api/camel_oil/v1"
v1 "kami/api/camel_oil/v1"
"kami/internal/service"
)
func (c *ControllerV1) AccountStatistics(ctx context.Context, req *v1.AccountStatisticsReq) (res *v1.AccountStatisticsRes, err error) {
return nil, gerror.NewCode(gcode.CodeNotImplemented)
return service.CamelOil().GetAccountStatistics(ctx, req)
}

View File

@@ -3,12 +3,41 @@ package camel_oil
import (
"context"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"kami/api/camel_oil/v1"
v1 "kami/api/camel_oil/v1"
"kami/internal/consts"
"kami/internal/service"
)
func (c *ControllerV1) CheckAccount(ctx context.Context, req *v1.CheckAccountReq) (res *v1.CheckAccountRes, err error) {
return nil, gerror.NewCode(gcode.CodeNotImplemented)
account, err := service.CamelOil().GetAccountInfo(ctx, req.AccountId)
if err != nil {
return nil, err
}
res = &v1.CheckAccountRes{
IsOnline: account.Status == int(consts.CamelOilAccountStatusOnline),
Status: consts.CamelOilAccountStatus(account.Status),
StatusText: getAccountStatusTextForStatus(account.Status),
FailureReason: account.FailureReason,
}
return res, nil
}
// getAccountStatusTextForStatus 获取账号状态文本
func getAccountStatusTextForStatus(status int) string {
switch status {
case 1:
return "待登录"
case 2:
return "在线"
case 3:
return "暂停"
case 4:
return "已失效"
case 5:
return "登录失败"
default:
return "未知"
}
}

View File

@@ -0,0 +1,19 @@
package camel_oil
import (
"context"
v1 "kami/api/camel_oil/v1"
"kami/internal/service"
)
func (c *ControllerV1) CreateToken(ctx context.Context, req *v1.CreateTokenReq) (res *v1.CreateTokenRes, err error) {
tokenId, err := service.CamelOil().CreateToken(ctx, req.TokenName, req.TokenValue, req.Phone, req.Remark)
if err != nil {
return nil, err
}
return &v1.CreateTokenRes{
TokenId: tokenId,
}, nil
}

View File

@@ -0,0 +1,18 @@
package camel_oil
import (
"context"
v1 "kami/api/camel_oil/v1"
"kami/internal/service"
)
func (c *ControllerV1) DeleteToken(ctx context.Context, req *v1.DeleteTokenReq) (res *v1.DeleteTokenRes, err error) {
err = service.CamelOil().DeleteToken(ctx, req.TokenId)
if err != nil {
return nil, err
}
return &v1.DeleteTokenRes{
Message: "Token deleted successfully",
}, nil
}

View File

@@ -0,0 +1,31 @@
package camel_oil
import (
"context"
v1 "kami/api/camel_oil/v1"
"kami/internal/service"
)
func (c *ControllerV1) GetToken(ctx context.Context, req *v1.GetTokenReq) (res *v1.GetTokenRes, err error) {
token, err := service.CamelOil().GetTokenInfo(ctx, req.TokenId)
if err != nil {
return nil, err
}
return &v1.GetTokenRes{
Token: v1.TokenInfo{
Id: token.Id,
TokenName: token.TokenName,
TokenValue: token.TokenValue,
Phone: token.Phone,
Status: token.Status,
BindCount: token.BindCount,
TotalBindAmount: token.TotalBindAmount,
LastBindAt: token.LastBindAt,
LastUsedAt: token.LastUsedAt,
Remark: token.Remark,
CreatedAt: token.CreatedAt,
UpdatedAt: token.UpdatedAt,
},
}, nil
}

View File

@@ -0,0 +1,36 @@
package camel_oil
import (
"context"
v1 "kami/api/camel_oil/v1"
"kami/api/commonApi"
"kami/internal/service"
)
func (c *ControllerV1) ListCardBindingsByToken(ctx context.Context, req *v1.ListCardBindingsByTokenReq) (res *v1.ListCardBindingsByTokenRes, err error) {
bindings, total, err := service.CamelOil().GetCardBindingsByToken(ctx, req.TokenId, req.Current, req.PageSize)
if err != nil {
return nil, err
}
bindingInfos := make([]v1.CardBindingInfo, 0, len(bindings))
for _, binding := range bindings {
bindingInfos = append(bindingInfos, v1.CardBindingInfo{
Id: binding.Id,
TokenId: binding.TokenId,
OrderId: binding.OrderId,
CardNumber: binding.CardNumber,
CardPassword: binding.CardPassword,
Amount: binding.Amount,
CreatedAt: binding.CreatedAt,
})
}
return &v1.ListCardBindingsByTokenRes{
CommonPageRes: commonApi.CommonPageRes[v1.CardBindingInfo]{
Total: total,
CommonDataRes: commonApi.CommonDataRes[v1.CardBindingInfo]{
List: bindingInfos,
},
},
}, nil
}

View File

@@ -3,7 +3,7 @@ package camel_oil
import (
"context"
"kami/api/camel_oil/v1"
v1 "kami/api/camel_oil/v1"
"kami/internal/service"
)

View File

@@ -0,0 +1,41 @@
package camel_oil
import (
"context"
v1 "kami/api/camel_oil/v1"
"kami/api/commonApi"
"kami/internal/service"
)
func (c *ControllerV1) ListTokens(ctx context.Context, req *v1.ListTokensReq) (res *v1.ListTokensRes, err error) {
tokens, total, err := service.CamelOil().ListTokensWithPagination(ctx, req.CommonPageReq, req.TokenName, req.Status)
if err != nil {
return nil, err
}
tokenInfos := make([]v1.TokenInfo, 0, len(tokens))
for _, token := range tokens {
tokenInfos = append(tokenInfos, v1.TokenInfo{
Id: token.Id,
TokenName: token.TokenName,
TokenValue: token.TokenValue,
Phone: token.Phone,
Status: token.Status,
BindCount: token.BindCount,
TotalBindAmount: token.TotalBindAmount,
LastBindAt: token.LastBindAt,
LastUsedAt: token.LastUsedAt,
Remark: token.Remark,
CreatedAt: token.CreatedAt,
UpdatedAt: token.UpdatedAt,
})
}
return &v1.ListTokensRes{
CommonPageRes: commonApi.CommonPageRes[v1.TokenInfo]{
Total: total,
CommonDataRes: commonApi.CommonDataRes[v1.TokenInfo]{
List: tokenInfos,
},
},
}, nil
}

View File

@@ -3,12 +3,10 @@ package camel_oil
import (
"context"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"kami/api/camel_oil/v1"
v1 "kami/api/camel_oil/v1"
"kami/internal/service"
)
func (c *ControllerV1) OrderCallback(ctx context.Context, req *v1.OrderCallbackReq) (res *v1.OrderCallbackRes, err error) {
return nil, gerror.NewCode(gcode.CodeNotImplemented)
return service.CamelOil().TriggerOrderCallback(ctx, req)
}

View File

@@ -3,7 +3,7 @@ package camel_oil
import (
"context"
"kami/api/camel_oil/v1"
v1 "kami/api/camel_oil/v1"
"kami/internal/service"
)

View File

@@ -3,12 +3,10 @@ package camel_oil
import (
"context"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"kami/api/camel_oil/v1"
v1 "kami/api/camel_oil/v1"
"kami/internal/service"
)
func (c *ControllerV1) OrderHistory(ctx context.Context, req *v1.OrderHistoryReq) (res *v1.OrderHistoryRes, err error) {
return nil, gerror.NewCode(gcode.CodeNotImplemented)
return service.CamelOil().GetOrderHistory(ctx, req)
}

View File

@@ -3,7 +3,7 @@ package camel_oil
import (
"context"
"kami/api/camel_oil/v1"
v1 "kami/api/camel_oil/v1"
"kami/internal/service"
)

View File

@@ -0,0 +1,97 @@
// ==========================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// ==========================================================================
package internal
import (
"context"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
)
// V1CamelOilCardBindingDao is the data access object for the table camel_oil_card_binding.
type V1CamelOilCardBindingDao struct {
table string // table is the underlying table name of the DAO.
group string // group is the database configuration group name of the current DAO.
columns V1CamelOilCardBindingColumns // columns contains all the column names of Table for convenient usage.
handlers []gdb.ModelHandler // handlers for customized model modification.
}
// V1CamelOilCardBindingColumns defines and stores column names for the table camel_oil_card_binding.
type V1CamelOilCardBindingColumns struct {
Id string // 主键ID
TokenId string // Token ID
OrderId string // 订单ID
CardNumber string // 卡号
CardPassword string // 卡密
Amount string // 绑定金额
Remark string // 备注
CreatedAt string // 创建时间
UpdatedAt string // 更新时间
DeletedAt string // 删除时间(软删除)
}
// v1CamelOilCardBindingColumns holds the columns for the table camel_oil_card_binding.
var v1CamelOilCardBindingColumns = V1CamelOilCardBindingColumns{
Id: "id",
TokenId: "token_id",
OrderId: "order_id",
CardNumber: "card_number",
CardPassword: "card_password",
Amount: "amount",
Remark: "remark",
CreatedAt: "created_at",
UpdatedAt: "updated_at",
DeletedAt: "deleted_at",
}
// NewV1CamelOilCardBindingDao creates and returns a new DAO object for table data access.
func NewV1CamelOilCardBindingDao(handlers ...gdb.ModelHandler) *V1CamelOilCardBindingDao {
return &V1CamelOilCardBindingDao{
group: "default",
table: "camel_oil_card_binding",
columns: v1CamelOilCardBindingColumns,
handlers: handlers,
}
}
// DB retrieves and returns the underlying raw database management object of the current DAO.
func (dao *V1CamelOilCardBindingDao) DB() gdb.DB {
return g.DB(dao.group)
}
// Table returns the table name of the current DAO.
func (dao *V1CamelOilCardBindingDao) Table() string {
return dao.table
}
// Columns returns all column names of the current DAO.
func (dao *V1CamelOilCardBindingDao) Columns() V1CamelOilCardBindingColumns {
return dao.columns
}
// Group returns the database configuration group name of the current DAO.
func (dao *V1CamelOilCardBindingDao) Group() string {
return dao.group
}
// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation.
func (dao *V1CamelOilCardBindingDao) Ctx(ctx context.Context) *gdb.Model {
model := dao.DB().Model(dao.table)
for _, handler := range dao.handlers {
model = handler(model)
}
return model.Safe().Ctx(ctx)
}
// Transaction wraps the transaction logic using function f.
// It rolls back the transaction and returns the error if function f returns a non-nil error.
// It commits the transaction and returns nil if function f returns nil.
//
// Note: Do not commit or roll back the transaction in function f,
// as it is automatically handled by this function.
func (dao *V1CamelOilCardBindingDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) {
return dao.Ctx(ctx).Transaction(ctx, f)
}

View File

@@ -0,0 +1,103 @@
// ==========================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// ==========================================================================
package internal
import (
"context"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
)
// V1CamelOilTokenDao is the data access object for the table camel_oil_token.
type V1CamelOilTokenDao struct {
table string // table is the underlying table name of the DAO.
group string // group is the database configuration group name of the current DAO.
columns V1CamelOilTokenColumns // columns contains all the column names of Table for convenient usage.
handlers []gdb.ModelHandler // handlers for customized model modification.
}
// V1CamelOilTokenColumns defines and stores column names for the table camel_oil_token.
type V1CamelOilTokenColumns struct {
Id string // 主键ID
TokenName string // Token名称/标识
TokenValue string // Token具体值
Phone string // 绑定的手机号
Status string // 状态1可用 2已禁用 3已过期
BindCount string // 已绑定卡密数量
TotalBindAmount string // 累计绑定金额
LastBindAt string // 最后绑定时间
LastUsedAt string // 最后使用时间
Remark string // 备注
CreatedAt string // 创建时间
UpdatedAt string // 更新时间
DeletedAt string // 删除时间(软删除)
}
// v1CamelOilTokenColumns holds the columns for the table camel_oil_token.
var v1CamelOilTokenColumns = V1CamelOilTokenColumns{
Id: "id",
TokenName: "token_name",
TokenValue: "token_value",
Phone: "phone",
Status: "status",
BindCount: "bind_count",
TotalBindAmount: "total_bind_amount",
LastBindAt: "last_bind_at",
LastUsedAt: "last_used_at",
Remark: "remark",
CreatedAt: "created_at",
UpdatedAt: "updated_at",
DeletedAt: "deleted_at",
}
// NewV1CamelOilTokenDao creates and returns a new DAO object for table data access.
func NewV1CamelOilTokenDao(handlers ...gdb.ModelHandler) *V1CamelOilTokenDao {
return &V1CamelOilTokenDao{
group: "default",
table: "camel_oil_token",
columns: v1CamelOilTokenColumns,
handlers: handlers,
}
}
// DB retrieves and returns the underlying raw database management object of the current DAO.
func (dao *V1CamelOilTokenDao) DB() gdb.DB {
return g.DB(dao.group)
}
// Table returns the table name of the current DAO.
func (dao *V1CamelOilTokenDao) Table() string {
return dao.table
}
// Columns returns all column names of the current DAO.
func (dao *V1CamelOilTokenDao) Columns() V1CamelOilTokenColumns {
return dao.columns
}
// Group returns the database configuration group name of the current DAO.
func (dao *V1CamelOilTokenDao) Group() string {
return dao.group
}
// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation.
func (dao *V1CamelOilTokenDao) Ctx(ctx context.Context) *gdb.Model {
model := dao.DB().Model(dao.table)
for _, handler := range dao.handlers {
model = handler(model)
}
return model.Safe().Ctx(ctx)
}
// Transaction wraps the transaction logic using function f.
// It rolls back the transaction and returns the error if function f returns a non-nil error.
// It commits the transaction and returns nil if function f returns nil.
//
// Note: Do not commit or roll back the transaction in function f,
// as it is automatically handled by this function.
func (dao *V1CamelOilTokenDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) {
return dao.Ctx(ctx).Transaction(ctx, f)
}

View File

@@ -0,0 +1,22 @@
// =================================================================================
// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed.
// =================================================================================
package dao
import (
"kami/internal/dao/internal"
)
// v1CamelOilCardBindingDao is the data access object for the table camel_oil_card_binding.
// You can define custom methods on it to extend its functionality as needed.
type v1CamelOilCardBindingDao struct {
*internal.V1CamelOilCardBindingDao
}
var (
// V1CamelOilCardBinding is a globally accessible object for table camel_oil_card_binding operations.
V1CamelOilCardBinding = v1CamelOilCardBindingDao{internal.NewV1CamelOilCardBindingDao()}
)
// Add your custom methods and functionality below.

View File

@@ -0,0 +1,22 @@
// =================================================================================
// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed.
// =================================================================================
package dao
import (
"kami/internal/dao/internal"
)
// v1CamelOilTokenDao is the data access object for the table camel_oil_token.
// You can define custom methods on it to extend its functionality as needed.
type v1CamelOilTokenDao struct {
*internal.V1CamelOilTokenDao
}
var (
// V1CamelOilToken is a globally accessible object for table camel_oil_token operations.
V1CamelOilToken = v1CamelOilTokenDao{internal.NewV1CamelOilTokenDao()}
)
// Add your custom methods and functionality below.

View File

@@ -138,6 +138,7 @@ func (s *sCamelOil) CronOrderPaymentCheckTask(ctx context.Context) error {
PayStatus: consts.CamelOilPaymentStatusPaid,
})
_ = s.RecordOrderHistory(ctx, order.OrderNo, consts.CamelOilOrderChangeTypePaid, "", fmt.Sprintf("支付成功,金额: %.2f", queryResult.Balance))
paidCount++
}
continue
@@ -162,7 +163,7 @@ func (s *sCamelOil) CronOrderPaymentCheckTask(ctx context.Context) error {
return nil
}
// CronAccountDailyResetTask 账号日重置任务 - 由cron调度器在每日00:05调用
// CronAccountDailyResetTask 账号日重置任务 - 由cron调度器在每日00:00调用
func (s *sCamelOil) CronAccountDailyResetTask(ctx context.Context) error {
glog.Info(ctx, "开始执行账号日重置任务")
@@ -307,3 +308,68 @@ func (s *sCamelOil) CronVerifyCodeCheckTask(ctx context.Context) error {
glog.Infof(ctx, "验证码检测任务完成: 成功=%d, 失败=%d", successCount, failCount)
return nil
}
// CronCardBindingTask 卡密绑定定时任务 - 由cron调度器定期调用
// 流程:处理已支付但未绑定 Token 的订单,进行卡密绑定
func (s *sCamelOil) CronCardBindingTask(ctx context.Context) error {
glog.Info(ctx, "开始执行卡密绑定任务")
// 查询已支付但未绑定 Token 的订单
var orders []*entity.V1CamelOilOrder
err := dao.V1CamelOilOrder.Ctx(ctx).DB(config.GetDatabaseV1()).
Where(dao.V1CamelOilOrder.Columns().PayStatus, consts.CamelOilPaymentStatusPaid).
WhereNotIn(dao.V1CamelOilOrder.Columns().Id,
dao.V1CamelOilCardBinding.Ctx(ctx).DB(config.GetDatabaseV1()).
Fields(dao.V1CamelOilCardBinding.Columns().OrderId)).
Limit(50).
Scan(&orders)
if err != nil {
glog.Error(ctx, "查询待绑定订单失败", err)
return err
}
if len(orders) == 0 {
glog.Debug(ctx, "无待绑定订单")
return nil
}
glog.Infof(ctx, "查询到 %d 个待绑定订单", len(orders))
successCount := 0
failCount := 0
for _, order := range orders {
// 检查是否有可用的 Token
availableTokenCount, err := dao.V1CamelOilToken.Ctx(ctx).DB(config.GetDatabaseV1()).
Where(dao.V1CamelOilToken.Columns().Status, consts.CamelOilTokenStatusAvailable).
Count()
if err != nil || availableTokenCount == 0 {
glog.Warningf(ctx, "无可用 Token订单 ID: %d 无法绑定", order.Id)
failCount++
continue
}
// 检查卡号和卡密是否存在
if order.CardNumber == "" || order.CardPassword == "" {
glog.Warningf(ctx, "订单 %d 卡号或卡密未填写,无法绑定", order.Id)
failCount++
continue
}
// 尝试绑定卡密到 Token
_, err = s.BindCardToToken(ctx, order.Id, order.CardNumber, order.CardPassword, order.Amount)
if err != nil {
glog.Errorf(ctx, "绑定卡密到 Token 失败,订单 ID: %d, 错误: %v", order.Id, err)
failCount++
continue
}
glog.Infof(ctx, "订单 %d 卡密绑定成功", order.Id)
successCount++
}
glog.Infof(ctx, "卡密绑定任务完成: 成功=%d, 失败=%d", successCount, failCount)
return nil
}

View File

@@ -103,6 +103,7 @@ func (s *sCamelOil) TriggerOrderCallback(ctx context.Context, req *v1.OrderCallb
// executeCallback 执行回调(内部方法)
func (s *sCamelOil) executeCallback(ctx context.Context, order *entity.V1CamelOilOrder) (err error) {
// 查询订单信息
var orderInfo *entity.V1OrderInfo
if err = dao.V1OrderInfo.Ctx(ctx).DB(config.GetDatabaseV1()).Where(dao.V1OrderInfo.Columns().BankOrderId, order.MerchantOrderId).Scan(&orderInfo); err != nil || orderInfo == nil || orderInfo.Id == 0 {
glog.Error(ctx, "查询订单失败", g.Map{"userOrderId": order.MerchantOrderId, "err": err})
@@ -113,7 +114,7 @@ func (s *sCamelOil) executeCallback(ctx context.Context, order *entity.V1CamelOi
glog.Error(ctx, "查询商户信息失败", g.Map{"merchantId": orderInfo.MerchantUid, "err": err})
return errors.New("商户不存在")
}
// 发送HTTP回调请求
// 发送 HTTP 回调请求
_, _ = gclient.New().Get(ctx, "http://kami_gateway:12309/myself/notify", g.Map{
"orderNo": order.OrderNo,
"orderPrice": order.Amount,
@@ -129,12 +130,17 @@ func (s *sCamelOil) executeCallback(ctx context.Context, order *entity.V1CamelOi
// ProcessPendingCallbacks 处理待回调订单(定时任务使用)
func (s *sCamelOil) ProcessPendingCallbacks(ctx context.Context) error {
// 查询需要回调的订单
// 查询需要回调的订单(仅查询已绑定到 Token 的订单)
var orders []*entity.V1CamelOilOrder
err := dao.V1CamelOilOrder.Ctx(ctx).DB(config.GetDatabaseV1()).
InnerJoin(dao.V1CamelOilCardBinding.Table(), fmt.Sprintf("%s.%s = %s.%s",
dao.V1CamelOilOrder.Table(), dao.V1CamelOilOrder.Columns().Id,
dao.V1CamelOilCardBinding.Table(), dao.V1CamelOilCardBinding.Columns().OrderId)).
Where(dao.V1CamelOilOrder.Columns().PayStatus, consts.CamelOilPaymentStatusPaid).
Where(dao.V1CamelOilOrder.Columns().NotifyStatus, consts.CamelOilCallbackStatusPending).
WhereLTE(dao.V1CamelOilOrder.Columns().NotifyCount, 3).
Fields(dao.V1CamelOilOrder.Table() + ".*").
Distinct().
Limit(50).
Scan(&orders)

View File

@@ -0,0 +1,359 @@
package camel_oil
import (
"context"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/util/gconv"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/glog"
"github.com/gogf/gf/v2/os/gtime"
"github.com/shopspring/decimal"
"kami/api/commonApi"
"kami/internal/consts"
"kami/internal/dao"
"kami/internal/model/do"
"kami/internal/model/entity"
"kami/utility/config"
"kami/utility/integration/camel_oil_api"
)
// ====================================================================================
// Token 管理相关方法
// ====================================================================================
// CreateToken 创建 Token
func (s *sCamelOil) CreateToken(ctx context.Context, tokenName string, tokenValue string, phone string, remark string) (tokenId int64, err error) {
m := dao.V1CamelOilToken.Ctx(ctx).DB(config.GetDatabaseV1())
result, err := m.Insert(&do.V1CamelOilToken{
TokenName: tokenName,
TokenValue: tokenValue,
Phone: phone,
Status: int(consts.CamelOilTokenStatusAvailable),
BindCount: 0,
Remark: remark,
})
if err != nil {
return 0, gerror.Wrap(err, "创建 Token失败")
}
tokenId, _ = result.LastInsertId()
glog.Infof(ctx, "Token创建成功: tokenId=%d, tokenName=%s, phone=%s", tokenId, tokenName, phone)
return tokenId, nil
}
// GetTokenInfo 获取 Token 信息
func (s *sCamelOil) GetTokenInfo(ctx context.Context, tokenId int64) (token *entity.V1CamelOilToken, err error) {
m := dao.V1CamelOilToken.Ctx(ctx).DB(config.GetDatabaseV1())
err = m.Where(dao.V1CamelOilToken.Columns().Id, tokenId).Scan(&token)
if err != nil {
return nil, gerror.Wrap(err, "查询Token信息失败")
}
if token == nil {
return nil, gerror.New("Token不存在")
}
return token, nil
}
// ListTokens 列出所有可用的 Token
func (s *sCamelOil) ListTokens(ctx context.Context) (tokens []*entity.V1CamelOilToken, err error) {
m := dao.V1CamelOilToken.Ctx(ctx).DB(config.GetDatabaseV1())
err = m.Where(dao.V1CamelOilToken.Columns().Status, consts.CamelOilTokenStatusAvailable).
OrderAsc(dao.V1CamelOilToken.Columns().LastUsedAt).
OrderAsc(dao.V1CamelOilToken.Columns().LastBindAt).
Scan(&tokens)
if err != nil {
return nil, gerror.Wrap(err, "查询Token列表失败")
}
return tokens, nil
}
// ListTokensWithPagination 列出所有可用的 Token支持分页和查询条件
func (s *sCamelOil) ListTokensWithPagination(ctx context.Context, pageReq commonApi.CommonPageReq, tokenName string, status int) (tokens []*entity.V1CamelOilToken, total int, err error) {
m := dao.V1CamelOilToken.Ctx(ctx).DB(config.GetDatabaseV1())
query := m.Where(dao.V1CamelOilToken.Columns().DeletedAt, nil)
// 添加查询条件
if tokenName != "" {
query = query.WhereLike(dao.V1CamelOilToken.Columns().TokenName, "%"+tokenName+"%")
}
if status > 0 {
query = query.Where(dao.V1CamelOilToken.Columns().Status, status)
}
// 获取总数
totalCount, err := query.Count()
if err != nil {
return nil, 0, gerror.Wrap(err, "查询Token总数失败")
}
// 分页查询
err = query.
Offset((pageReq.Current - 1) * pageReq.PageSize).
Limit(pageReq.PageSize).
OrderDesc(dao.V1CamelOilToken.Columns().CreatedAt).
Scan(&tokens)
if err != nil {
return nil, 0, gerror.Wrap(err, "查询Token列表失败")
}
return tokens, int(totalCount), nil
}
// UpdateToken 更新 Token 信息
func (s *sCamelOil) UpdateToken(ctx context.Context, tokenId int64, tokenName string, status int, remark string) error {
m := dao.V1CamelOilToken.Ctx(ctx).DB(config.GetDatabaseV1())
_, err := m.Where(dao.V1CamelOilToken.Columns().Id, tokenId).Update(&do.V1CamelOilToken{
TokenName: tokenName,
Status: status,
Remark: remark,
UpdatedAt: gtime.Now(),
})
if err != nil {
return gerror.Wrap(err, "更新Token失败")
}
glog.Infof(ctx, "Token更新成功: tokenId=%d", tokenId)
return nil
}
// DeleteToken 删除 Token软删除
func (s *sCamelOil) DeleteToken(ctx context.Context, tokenId int64) error {
m := dao.V1CamelOilToken.Ctx(ctx).DB(config.GetDatabaseV1())
_, err := m.Where(dao.V1CamelOilToken.Columns().Id, tokenId).
Update(&do.V1CamelOilToken{
DeletedAt: gtime.Now(),
})
if err != nil {
return gerror.Wrap(err, "删除Token失败")
}
glog.Infof(ctx, "Token删除成功: tokenId=%d", tokenId)
return nil
}
// UpdateTokenStatus 更新 Token 状态并记录日志
func (s *sCamelOil) UpdateTokenStatus(ctx context.Context, tokenId int64, newStatus consts.CamelOilTokenStatus, remark string) error {
m := dao.V1CamelOilToken.Ctx(ctx).DB(config.GetDatabaseV1())
// 获取当前 Token 信息
var token *entity.V1CamelOilToken
err := m.Where(dao.V1CamelOilToken.Columns().Id, tokenId).Scan(&token)
if err != nil {
return gerror.Wrap(err, "查询Token失败")
}
if token == nil {
return gerror.New("Token不存在")
}
oldStatus := consts.CamelOilTokenStatus(token.Status)
// 如果状态没有变化,则不更新
if oldStatus == newStatus {
return nil
}
// 更新 Token 状态
_, err = m.Where(dao.V1CamelOilToken.Columns().Id, tokenId).Update(&do.V1CamelOilToken{
Status: int(newStatus),
})
if err != nil {
return gerror.Wrap(err, "更新Token状态失败")
}
glog.Infof(ctx, "Token状态更新成功: tokenId=%d, 原状态=%s, 新状态=%s, 备注=%s", tokenId, consts.CamelOilTokenStatusText[oldStatus], consts.CamelOilTokenStatusText[newStatus], remark)
return nil
}
// ====================================================================================
// 卡密绑定相关方法
// ====================================================================================
// BindCardToToken 绑定卡密到 Token使用轮询算法选择 Token
func (s *sCamelOil) BindCardToToken(ctx context.Context, orderId int64, cardNumber string, cardPassword string, amount decimal.Decimal) (bindingId int64, err error) {
// 1. 获取订单信息
var order *entity.V1CamelOilOrder
err = dao.V1CamelOilOrder.Ctx(ctx).DB(config.GetDatabaseV1()).
Where(dao.V1CamelOilOrder.Columns().Id, orderId).
Scan(&order)
if err != nil {
return 0, gerror.Wrap(err, "查询订单失败")
}
if order == nil {
return 0, gerror.New("订单不存在")
}
// 2. 获取所有可用的 Token
tokens, err := s.ListTokens(ctx)
if err != nil {
return 0, gerror.Wrap(err, "查询Token列表失败")
}
if len(tokens) == 0 {
return 0, gerror.New("没有可用的Token")
}
// 3. 使用轮询算法选择 Token选择绑定金额最少的 Token
var selectedToken *entity.V1CamelOilToken
minAmount := tokens[0].TotalBindAmount
selectedToken = tokens[0]
for _, token := range tokens {
if token.TotalBindAmount.Cmp(minAmount) < 0 {
minAmount = token.TotalBindAmount
selectedToken = token
}
}
// 4.2 调用绑卡接口(使用选中 Token 的 TokenValue
rechargeErrType, rechargeErr := camel_oil_api.NewClient().RechargeCard(ctx, selectedToken.TokenValue, selectedToken.Phone, cardPassword)
if rechargeErr != nil {
switch rechargeErrType {
case camel_oil_api.RechargeCardErrorCode:
// 卡密错误:标记订单为绑定失败
glog.Warningf(ctx, "卡密错误: %v", rechargeErr)
// 调用已实现的方法更新订单状态
_ = s.UpdateOrderStatus(ctx, orderId, consts.CamelOilOrderStatusFailed, consts.CamelOilOrderChangeTypeFail, "", "卡密样检失败")
return 0, gerror.Wrap(rechargeErr, "卡密样检失败")
case camel_oil_api.RechargeCardErrorToken:
// Token 过期/无效:标记 Token 为已过期
glog.Warningf(ctx, "Token 过期: %v", rechargeErr)
// 调用已实现的方法更新 Token 状态
_ = s.UpdateTokenStatus(ctx, selectedToken.Id, consts.CamelOilTokenStatusExpired, "Token已过期")
return 0, gerror.Wrap(rechargeErr, "Token过期需要重新登录")
default:
// 网络或其他错误:不更新状态,由定时任务重试
glog.Errorf(ctx, "绑卡失败(网络或其他错误): %v", rechargeErr)
return 0, gerror.Wrap(rechargeErr, "绑卡操作失败,稍后重试")
}
}
// 5. 创建绑定记录
bindingResult, err := dao.V1CamelOilCardBinding.Ctx(ctx).DB(config.GetDatabaseV1()).
Insert(&do.V1CamelOilCardBinding{
TokenId: selectedToken.Id,
OrderId: orderId,
CardNumber: cardNumber,
CardPassword: cardPassword,
Amount: amount,
})
if err != nil {
return 0, gerror.Wrap(err, "创建绑定记录失败")
}
bindingId, _ = bindingResult.LastInsertId()
_, _ = dao.V1CamelOilToken.Ctx(ctx).DB(config.GetDatabaseV1()).Where(dao.V1CamelOilToken.Columns().Id, selectedToken.Id).
Data(dao.V1CamelOilToken.Columns().TotalBindAmount, &gdb.Counter{
Field: dao.V1CamelOilToken.Columns().TotalBindAmount,
Value: gconv.Float64(amount),
}).Data(dao.V1CamelOilToken.Columns().BindCount, &gdb.Counter{
Field: dao.V1CamelOilToken.Columns().BindCount,
Value: gconv.Float64(amount),
}).Data(dao.V1CamelOilToken.Columns().LastBindAt, gtime.Now()).Data(dao.V1CamelOilToken.Columns().LastUsedAt, gtime.Now()).Update()
glog.Infof(ctx, "卡密绑定成功: bindingId=%d, tokenId=%d, 订单ID=%d, 绑定金额=%.2f",
bindingId, selectedToken.Id, orderId, amount.InexactFloat64())
return bindingId, nil
}
// GetCardBindingInfo 获取卡密绑定信息
func (s *sCamelOil) GetCardBindingInfo(ctx context.Context, bindingId int64) (binding *entity.V1CamelOilCardBinding, err error) {
m := dao.V1CamelOilCardBinding.Ctx(ctx).DB(config.GetDatabaseV1())
err = m.Where(dao.V1CamelOilCardBinding.Columns().Id, bindingId).Scan(&binding)
if err != nil {
return nil, gerror.Wrap(err, "查询绑定信息失败")
}
if binding == nil {
return nil, gerror.New("绑定记录不存在")
}
return binding, nil
}
// GetCardBindingByOrder 获取订单绑定的卡密信息
func (s *sCamelOil) GetCardBindingByOrder(ctx context.Context, orderId int64) (binding *entity.V1CamelOilCardBinding, err error) {
m := dao.V1CamelOilCardBinding.Ctx(ctx).DB(config.GetDatabaseV1())
err = m.Where(dao.V1CamelOilCardBinding.Columns().OrderId, orderId).Scan(&binding)
if err != nil {
return nil, gerror.Wrap(err, "查询绑定信息失败")
}
return binding, nil
}
// GetCardBindingsByToken 根据 tokenId 查询绑定的卡密信息
func (s *sCamelOil) GetCardBindingsByToken(ctx context.Context, tokenId int64, current int, pageSize int) (bindings []*entity.V1CamelOilCardBinding, total int, err error) {
m := dao.V1CamelOilCardBinding.Ctx(ctx).DB(config.GetDatabaseV1())
count, err := m.Where(dao.V1CamelOilCardBinding.Columns().TokenId, tokenId).Count()
if err != nil {
return nil, 0, gerror.Wrap(err, "查询绑定记录计数失败")
}
err = m.Where(dao.V1CamelOilCardBinding.Columns().TokenId, tokenId).
Page(current, pageSize).
Scan(&bindings)
if err != nil {
return nil, 0, gerror.Wrap(err, "查询绑定记录失败")
}
return bindings, int(count), nil
}
// GetTokenBindingStats 获取 Token 的绑定统计
func (s *sCamelOil) GetTokenBindingStats(ctx context.Context, tokenId int64) (bindCount int, totalAmount decimal.Decimal, err error) {
m := dao.V1CamelOilCardBinding.Ctx(ctx).DB(config.GetDatabaseV1())
var stats struct {
BindCount int
TotalAmount decimal.Decimal
}
err = m.Where(dao.V1CamelOilCardBinding.Columns().TokenId, tokenId).
Fields("COUNT(*) as bind_count, SUM(amount) as total_amount").
Scan(&stats)
if err != nil {
return 0, decimal.Zero, gerror.Wrap(err, "查询Token绑定统计失败")
}
return stats.BindCount, stats.TotalAmount, nil
}
// CalculateTotalBindingAmount 计算所有 Token 的累计绑定金额
func (s *sCamelOil) CalculateTotalBindingAmount(ctx context.Context) (totalAmount decimal.Decimal, err error) {
m := dao.V1CamelOilCardBinding.Ctx(ctx).DB(config.GetDatabaseV1())
var result struct {
Total decimal.Decimal
}
err = m.Fields("SUM(amount) as total").Scan(&result)
if err != nil {
return decimal.Zero, gerror.Wrap(err, "计算累计绑定金额失败")
}
return result.Total, nil
}

View File

@@ -46,6 +46,7 @@ func whiteListAuth(r *ghttp.Request) gcode.Code {
"/api/restriction/collection/userInfo",
"/api/cookieInfo/jd/order/placeOrder",
"/api/jd-cookie/order/create",
"/api/jd-v2/order/submit",
}
internalWhiteListAuth := []string{
"/api/aes/encryption/params",

View File

@@ -0,0 +1,25 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package do
import (
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
// V1CamelOilCardBinding is the golang structure of table camel_oil_card_binding for DAO operations like Where/Data.
type V1CamelOilCardBinding struct {
g.Meta `orm:"table:camel_oil_card_binding, do:true"`
Id any // 主键ID
TokenId any // Token ID
OrderId any // 订单ID
CardNumber any // 卡号
CardPassword any // 卡密
Amount any // 绑定金额
Remark any // 备注
CreatedAt *gtime.Time // 创建时间
UpdatedAt *gtime.Time // 更新时间
DeletedAt *gtime.Time // 删除时间(软删除)
}

View File

@@ -0,0 +1,28 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package do
import (
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
// V1CamelOilToken is the golang structure of table camel_oil_token for DAO operations like Where/Data.
type V1CamelOilToken struct {
g.Meta `orm:"table:camel_oil_token, do:true"`
Id any // 主键ID
TokenName any // Token名称/标识
TokenValue any // Token具体值
Phone any // 绑定的手机号
Status any // 状态1可用 2已禁用 3已过期
BindCount any // 已绑定卡密数量
TotalBindAmount any // 累计绑定金额
LastBindAt *gtime.Time // 最后绑定时间
LastUsedAt *gtime.Time // 最后使用时间
Remark any // 备注
CreatedAt *gtime.Time // 创建时间
UpdatedAt *gtime.Time // 更新时间
DeletedAt *gtime.Time // 删除时间(软删除)
}

View File

@@ -0,0 +1,24 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package entity
import (
"github.com/gogf/gf/v2/os/gtime"
"github.com/shopspring/decimal"
)
// V1CamelOilCardBinding is the golang structure for table v1camel_oil_card_binding.
type V1CamelOilCardBinding struct {
Id int64 `json:"id" orm:"id" description:"主键ID"`
TokenId int64 `json:"tokenId" orm:"token_id" description:"Token ID"`
OrderId int64 `json:"orderId" orm:"order_id" description:"订单ID"`
CardNumber string `json:"cardNumber" orm:"card_number" description:"卡号"`
CardPassword string `json:"cardPassword" orm:"card_password" description:"卡密"`
Amount decimal.Decimal `json:"amount" orm:"amount" description:"绑定金额"`
Remark string `json:"remark" orm:"remark" description:"备注"`
CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:"创建时间"`
UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:"更新时间"`
DeletedAt *gtime.Time `json:"deletedAt" orm:"deleted_at" description:"删除时间(软删除)"`
}

View File

@@ -0,0 +1,27 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package entity
import (
"github.com/gogf/gf/v2/os/gtime"
"github.com/shopspring/decimal"
)
// V1CamelOilToken is the golang structure for table v1camel_oil_token.
type V1CamelOilToken struct {
Id int64 `json:"id" orm:"id" description:"主键ID"`
TokenName string `json:"tokenName" orm:"token_name" description:"Token名称/标识"`
TokenValue string `json:"tokenValue" orm:"token_value" description:"Token具体值"`
Phone string `json:"phone" orm:"phone" description:"绑定的手机号"`
Status int `json:"status" orm:"status" description:"状态1可用 2已禁用 3已过期"`
BindCount int `json:"bindCount" orm:"bind_count" description:"已绑定卡密数量"`
TotalBindAmount decimal.Decimal `json:"totalBindAmount" orm:"total_bind_amount" description:"累计绑定金额"`
LastBindAt *gtime.Time `json:"lastBindAt" orm:"last_bind_at" description:"最后绑定时间"`
LastUsedAt *gtime.Time `json:"lastUsedAt" orm:"last_used_at" description:"最后使用时间"`
Remark string `json:"remark" orm:"remark" description:"备注"`
CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:"创建时间"`
UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:"更新时间"`
DeletedAt *gtime.Time `json:"deletedAt" orm:"deleted_at" description:"删除时间(软删除)"`
}

View File

@@ -7,10 +7,14 @@ package service
import (
"context"
v1 "kami/api/camel_oil/v1"
commonApi "kami/api/commonApi"
"kami/internal/consts"
"kami/internal/model"
"kami/internal/model/entity"
"github.com/shopspring/decimal"
)
type (
@@ -65,10 +69,10 @@ type (
// CronAccountDailyResetTask 账号日重置任务 - 由cron调度器在每日00:05调用
CronAccountDailyResetTask(ctx context.Context) error
CronVerifyCodeCheckTask(ctx context.Context) error
// SubmitOrder 提交订单并返回支付宝支付链接
SubmitOrder(ctx context.Context, req *v1.SubmitOrderReq) (res *v1.SubmitOrderRes, err error)
// UpdateOrderStatus 更新订单状态并记录历史
UpdateOrderStatus(ctx context.Context, orderId int64, newStatus consts.CamelOilOrderStatus, operationType consts.CamelOilOrderChangeType, rawData string, description string) (err error)
// SubmitOrder 提交订单并返回支付宝支付链接
SubmitOrder(ctx context.Context, req *v1.SubmitOrderReq) (res *v1.SubmitOrderRes, err error)
// TriggerOrderCallback 触发订单回调
TriggerOrderCallback(ctx context.Context, req *v1.OrderCallbackReq) (res *v1.OrderCallbackRes, err error)
// ProcessPendingCallbacks 处理待回调订单(定时任务使用)
@@ -99,6 +103,36 @@ type (
RecordPrefetchOrderHistory(ctx context.Context, prefetchId int64, changeType consts.CamelOilPrefetchOrderChangeType, accountId int64, accountName string, remark string) error
// CleanExpiredPrefetchOrders 清理过期的预拉取订单
CleanExpiredPrefetchOrders(ctx context.Context) (cleanedCount int, err error)
// ====================================================================================
// Token 管理相关方法
// ====================================================================================
// CreateToken 创建 Token
CreateToken(ctx context.Context, tokenName string, tokenValue string, phone string, remark string) (tokenId int64, err error)
// GetTokenInfo 获取 Token 信息
GetTokenInfo(ctx context.Context, tokenId int64) (token *entity.V1CamelOilToken, err error)
// ListTokens 列出所有可用的 Token
ListTokens(ctx context.Context) (tokens []*entity.V1CamelOilToken, err error)
// ListTokensWithPagination 列出所有可用的 Token支持分页和查询条件
ListTokensWithPagination(ctx context.Context, pageReq commonApi.CommonPageReq, tokenName string, status int) (tokens []*entity.V1CamelOilToken, total int, err error)
// UpdateToken 更新 Token 信息
UpdateToken(ctx context.Context, tokenId int64, tokenName string, status int, remark string) error
// DeleteToken 删除 Token软删除
DeleteToken(ctx context.Context, tokenId int64) error
// BindCardToToken 绑定卡密到 Token使用轮询算法选择 Token
BindCardToToken(ctx context.Context, orderId int64, cardNumber string, cardPassword string, amount decimal.Decimal) (bindingId int64, err error)
// GetCardBindingInfo 获取卡密绑定信息
GetCardBindingInfo(ctx context.Context, bindingId int64) (binding *entity.V1CamelOilCardBinding, err error)
// GetCardBindingByOrder 获取订单绑定的卡密信息
GetCardBindingByOrder(ctx context.Context, orderId int64) (binding *entity.V1CamelOilCardBinding, err error)
// GetCardBindingsByToken 根据 tokenId 查询绑定的卡密信息
GetCardBindingsByToken(ctx context.Context, tokenId int64, current int, pageSize int) (bindings []*entity.V1CamelOilCardBinding, total int, err error)
// GetTokenBindingStats 获取 Token 的绑定统计
GetTokenBindingStats(ctx context.Context, tokenId int64) (bindCount int, totalAmount decimal.Decimal, err error)
// CalculateTotalBindingAmount 计算所有 Token 的累计绑定金额
CalculateTotalBindingAmount(ctx context.Context) (totalAmount decimal.Decimal, err error)
}
)

View File

@@ -171,3 +171,55 @@ CREATE TABLE `camel_oil_prefetch_order_history` (
KEY `idx_created_at` (`created_at`),
KEY `idx_deleted_at` (`deleted_at`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '骆驼加油预拉取订单历史表';
-- 7. 骆驼加油 Token 管理表
DROP TABLE IF EXISTS `camel_oil_token`;
CREATE TABLE `camel_oil_token` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`token_name` varchar(128) NOT NULL COMMENT 'Token名称/标识',
`token_value` text NOT NULL COMMENT 'Token具体值',
`phone` varchar(20) DEFAULT NULL COMMENT '绑定的手机号',
`status` tinyint NOT NULL DEFAULT 1 COMMENT '状态1可用 2已禁用 3已过期',
`bind_count` int NOT NULL DEFAULT 0 COMMENT '已绑定卡密数量',
`total_bind_amount` decimal(15,2) NOT NULL DEFAULT 0 COMMENT '累计绑定金额',
`last_bind_at` datetime DEFAULT NULL COMMENT '最后绑定时间',
`last_used_at` datetime DEFAULT NULL COMMENT '最后使用时间',
`remark` text DEFAULT NULL COMMENT '备注',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted_at` datetime DEFAULT NULL COMMENT '删除时间软删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_token_value` (`token_value`(255)),
KEY `idx_token_name` (`token_name`),
KEY `idx_phone` (`phone`),
KEY `idx_status` (`status`),
KEY `idx_bind_count` (`bind_count`),
KEY `idx_total_bind_amount` (`total_bind_amount`),
KEY `idx_last_bind_at` (`last_bind_at`),
KEY `idx_deleted_at` (`deleted_at`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '骆驼加油Token管理表';
-- 8. 骆驼加油卡密绑定记录表
DROP TABLE IF EXISTS `camel_oil_card_binding`;
CREATE TABLE `camel_oil_card_binding` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`token_id` bigint NOT NULL COMMENT 'Token ID',
`order_id` bigint NOT NULL COMMENT '订单ID',
`card_number` varchar(256) DEFAULT NULL COMMENT '卡号',
`card_password` varchar(256) DEFAULT NULL COMMENT '卡密',
`amount` decimal(10,2) NOT NULL COMMENT '绑定金额',
`remark` text DEFAULT NULL COMMENT '备注',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted_at` datetime DEFAULT NULL COMMENT '删除时间软删除',
PRIMARY KEY (`id`),
KEY `idx_token_id` (`token_id`),
CONSTRAINT `fk_camel_oil_card_binding_token_id` FOREIGN KEY (`token_id`) REFERENCES `camel_oil_token` (`id`) ON DELETE RESTRICT,
KEY `idx_order_id` (`order_id`),
CONSTRAINT `fk_camel_oil_card_binding_order_id` FOREIGN KEY (`order_id`) REFERENCES `camel_oil_order` (`id`) ON DELETE CASCADE,
KEY `idx_amount` (`amount`),
KEY `idx_created_at` (`created_at`),
KEY `idx_deleted_at` (`deleted_at`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '骆驼加油卡密绑定记录表';

View File

@@ -76,20 +76,25 @@ func registerMainTasks(ctx context.Context) {
// registerCamelOilTasks 注册骆驼加油模块的定时任务
func registerCamelOilTasks(ctx context.Context) {
// 账户预拉取任务 - 每30秒执行一次
// 流程:并发拉取账户到指定数量
//_, _ = gcron.AddSingleton(ctx, "@every 10s", func(ctx context.Context) {
// _ = service.CamelOil().CronAccountPrefetchTask(ctx)
//}, "CamelOilAccountPrefetch")
//
//_, _ = gcron.AddSingleton(ctx, "@every 10s", func(ctx context.Context) {
// _ = service.CamelOil().CronVerifyCodeCheckTask(ctx)
//}, "CamelOilAccountVerifyCodeCheck")
_, _ = gcron.AddSingleton(ctx, "@every 10s", func(ctx context.Context) {
_ = service.CamelOil().CronAccountPrefetchTask(ctx)
}, "CamelOilAccountPrefetch")
//// 流程:检查预拉取订单库存,不足时补充
//_, _ = gcron.AddSingleton(ctx, "@every 10s", func(ctx context.Context) {
// _ = service.CamelOil().CronPrefetchOrderSupplementTask(ctx)
//}, "CamelOilPrefetchOrderSupplement")
_, _ = gcron.AddSingleton(ctx, "@every 10s", func(ctx context.Context) {
_ = service.CamelOil().CronVerifyCodeCheckTask(ctx)
}, "CamelOilAccountVerifyCodeCheck")
_, _ = gcron.AddSingleton(ctx, "@every 10s", func(ctx context.Context) {
_ = service.CamelOil().CronOrderPaymentCheckTask(ctx)
}, "CamelOilOrderPaymentCheck")
_, _ = gcron.AddSingleton(ctx, "0 1 0 * * ?", func(ctx context.Context) {
_ = service.CamelOil().CronAccountDailyResetTask(ctx)
}, "CamelOilAccountDailyReset")
_, _ = gcron.AddSingleton(ctx, "@every 10s", func(ctx context.Context) {
_ = service.CamelOil().ProcessPendingCallbacks(ctx)
}, "CamelOilProcessPendingCallbacks")
glog.Info(ctx, "骆驼加油模块定时任务注册完成")
}

View File

@@ -4,10 +4,20 @@ import (
"context"
"encoding/json"
"errors"
"math/rand/v2"
"strings"
"github.com/gogf/gf/v2/net/gclient"
"github.com/gogf/gf/v2/os/glog"
"github.com/google/uuid"
"strings"
)
// 绑卡错误类型枚举
const (
RechargeCardSuccess = 0 // 绑卡成功
RechargeCardErrorCode = 1 // 卡密错误
RechargeCardErrorToken = 2 // Token 过期/无效
RechargeCardErrorNetwork = 3 // 网络或其他错误
)
type Client struct {
@@ -142,14 +152,14 @@ func (c *Client) CreateOrder(ctx context.Context, phone, token string, amount fl
Brand string `json:"brand"`
DeviceId string `json:"deviceId"`
}{
OpenId: "app2511181557205741495",
OpenId: "app2511221747117503039",
Phone: phone,
GoodId: goodId,
GoodNum: 1,
BindPhone: phone,
PayType: "appAli",
ParamY: 26.996671,
ParamX: 77.450347,
ParamY: 31.2304 + (rand.Float64()-0.5)*2,
ParamX: 121.4737 + (rand.Float64()-0.5)*2,
Yanqian: true,
MobileOperatingPlatform: "iOS",
SysVersion: "iOS 15.7",
@@ -157,8 +167,9 @@ func (c *Client) CreateOrder(ctx context.Context, phone, token string, amount fl
NetWork: "unknown",
Platform: "ios",
Brand: "apple",
DeviceId: strings.Replace(uuid.NewString(), "-", "", -1),
DeviceId: strings.ToUpper(strings.ReplaceAll(uuid.NewString(), "-", "")),
}
pubkey := "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCkc6Xr/JhWEx/WPxG2q3VHLQ+FYk/oCmQ1y14B5j4xOJY+mAWoDOOti3sAXg0Kk662gWjWET1nLI6YED4wb9HWon1NAZn47lgc5ohIpEdU91Jao85X/kgkD3NvTTvhFicttepUOsrYUZN8rAQCE7AhzwGgKnCiIRY/kE8jOCCeZQIDAQAB"
body, _ := json.Marshal(&bodyStr)
bodyS, _ := EncryptWithPublicKey(pubkey, string(body))
@@ -176,7 +187,7 @@ func (c *Client) CreateOrder(ctx context.Context, phone, token string, amount fl
return "", "", err
}
respStr := resp.ReadAllString()
glog.Info(ctx, "登录", req, respStr)
glog.Info(ctx, "登录", bodyStr, respStr)
respStruct := struct {
Code string `json:"code"`
Message string `json:"message"`
@@ -195,9 +206,14 @@ func (c *Client) CreateOrder(ctx context.Context, phone, token string, amount fl
if respStruct.Code == "auth_error" {
return "", "", errors.New("auth_error")
}
base64PrivateKey := "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKRzpev8mFYTH9Y/EbardUctD4ViT+gKZDXLXgHmPjE4lj6YBagM462LewBeDqTrraBaNYRPWcsjpgQPjBv0daifU0BmfjuWBzmiEikR1T3Ulqjzlf+SCQPc29NO+EWJy216l6ythRk3ysBAITsCHPAaAqcKIhFj+QTyM4IJ5lAgMBAAECgYEAg/vlMI7b3EkhBhw8JTVavLMnf8+1fe/JGXuMiU22oF5gBwCPmZ4upLwLDfJt2Q1J7WPTNetEMqgKEXUH1GwKJkFGm2tSEMHSHdTmUTQ3w6bS1C0peZghyhmlWRXUlpKk5tDOQ24sWO268YrwZyueXnVGKJ4s0hY0KOiZIU2trUECQQDxA+lzq/t/L09M/bUybjsiP6eb/HBeZeu+2+JnHekb8z9BMXTOKTHqAI0Cs9UvE6BDT3aU9IJbWHbRogIMypT9AkEArq0fccphwWtAIyS0+fns4Hqs4On7yTfXSXWiAbSVif1LxP60b5n5Xm8lo12oHkdwOvKaesvgDpnIGUM9xjFfiQJASTZrABxKNYRljnmzRTJ+/BRiEdxJNiO3zS52Q+SuHzNxD5i6ZrXU18R7EUsXg0lu8YN9/hmYT687yMpx3Pjc8QJAZBs1lSouQgIsPLfRvB1+otvLbg7KzPPivufak+2hcfanUNvEHt14a6V5RZnsOoYojK/y1oM3AkchxVCi+43aOQJAC0gI6qsZ3VaPu9QDddrHPJ1dCHTXyfcNJ0op3srCVF92HoBWX54pzeagj+9g/Z4oUT9IhaO0Q3YE07N03HuVrQ=="
respData, _ := DecryptWithPrivateKey(base64PrivateKey, respStruct.OrderRes.Body)
return respStruct.OrderId, respData, err
if respStruct.Code == "error" && strings.Contains(respStruct.Message, "系统繁忙") {
continue
}
if respStruct.Code == "success" {
base64PrivateKey := "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKRzpev8mFYTH9Y/EbardUctD4ViT+gKZDXLXgHmPjE4lj6YBagM462LewBeDqTrraBaNYRPWcsjpgQPjBv0daifU0BmfjuWBzmiEikR1T3Ulqjzlf+SCQPc29NO+EWJy216l6ythRk3ysBAITsCHPAaAqcKIhFj+QTyM4IJ5lAgMBAAECgYEAg/vlMI7b3EkhBhw8JTVavLMnf8+1fe/JGXuMiU22oF5gBwCPmZ4upLwLDfJt2Q1J7WPTNetEMqgKEXUH1GwKJkFGm2tSEMHSHdTmUTQ3w6bS1C0peZghyhmlWRXUlpKk5tDOQ24sWO268YrwZyueXnVGKJ4s0hY0KOiZIU2trUECQQDxA+lzq/t/L09M/bUybjsiP6eb/HBeZeu+2+JnHekb8z9BMXTOKTHqAI0Cs9UvE6BDT3aU9IJbWHbRogIMypT9AkEArq0fccphwWtAIyS0+fns4Hqs4On7yTfXSXWiAbSVif1LxP60b5n5Xm8lo12oHkdwOvKaesvgDpnIGUM9xjFfiQJASTZrABxKNYRljnmzRTJ+/BRiEdxJNiO3zS52Q+SuHzNxD5i6ZrXU18R7EUsXg0lu8YN9/hmYT687yMpx3Pjc8QJAZBs1lSouQgIsPLfRvB1+otvLbg7KzPPivufak+2hcfanUNvEHt14a6V5RZnsOoYojK/y1oM3AkchxVCi+43aOQJAC0gI6qsZ3VaPu9QDddrHPJ1dCHTXyfcNJ0op3srCVF92HoBWX54pzeagj+9g/Z4oUT9IhaO0Q3YE07N03HuVrQ=="
respData, _ := DecryptWithPrivateKey(base64PrivateKey, respStruct.OrderRes.Body)
return respStruct.OrderId, respData, err
}
}
return "", "", errors.New("创建订单超时")
}
@@ -285,3 +301,61 @@ func (c *Client) QueryOrder(ctx context.Context, phone, token, orderId string) (
glog.Warningf(ctx, "查询订单%s超过最大页数%d未找到", orderId, maxPages)
return nil, nil
}
// RechargeCard 绑卡接口
// 返回值说明:
// - errType == RechargeCardSuccess: 绑卡成功
// - errType == RechargeCardErrorCode: 卡密错误
// - errType == RechargeCardErrorToken: token 过期/无效
// - errType == RechargeCardErrorNetwork: 网络或其他错误
func (c *Client) RechargeCard(ctx context.Context, token, phone, eCardCode string) (errType int, err error) {
c.Client.SetHeader("Authorization", "Bearer "+c.getAuth(ctx, token))
req := struct {
ECardCode string `json:"eCardCode"`
OpenId string `json:"openId"`
Phone string `json:"phone"`
Channel string `json:"channel"`
}{
ECardCode: eCardCode,
OpenId: "app2511181557205741495",
Phone: phone,
Channel: "app",
}
resp, err := c.Client.ContentJson().Post(ctx, "https://recharge3.bac365.com/camel_wechat_mini_oil_server/eCardMall/eCardRecharge", req)
if err != nil {
glog.Errorf(ctx, "绑卡请求失败,错误: %v", err)
return RechargeCardErrorNetwork, err
}
respStruct := struct {
Code string `json:"code"`
Message string `json:"message"`
}{}
err = json.Unmarshal(resp.ReadAll(), &respStruct)
if err != nil {
glog.Errorf(ctx, "解析绑卡响应失败,错误: %v", err)
return RechargeCardErrorNetwork, err
}
// 根据不同的错误码进行分类
switch respStruct.Code {
case "success":
glog.Infof(ctx, "卡密绑卡成功,手机号: %s", phone)
return RechargeCardSuccess, nil
case "codeError":
err = errors.New(respStruct.Message)
glog.Warningf(ctx, "卡密错误: %v", err)
return RechargeCardErrorCode, err
case "auth_error", "unauthorized":
err = errors.New(respStruct.Message)
glog.Warningf(ctx, "Token 错误或已过期: %v", err)
return RechargeCardErrorToken, err
default:
err = errors.New(respStruct.Message)
glog.Errorf(ctx, "绑卡失败: %v", err)
return RechargeCardErrorNetwork, err
}
}

View File

@@ -5,4 +5,7 @@ import (
)
func TestClient_SendCaptcha(t *testing.T) {
for range 10 {
NewClient().CreateOrder(t.Context(), "18627350353", "X/c/tYh6aFvm7AeBeMlCkiPaTtOGrMpI1NPwL93RqUkb0JKYjK1Qe8OUIa85y621qHauOJBaEl977RmtJU8YHPAaLYhHi7BB1nhosYJOyIO6035w7uvWRc8Rg4lhA2TgUU4XnF+4jCvt8s+qWzEQLCNOd7VzpC2oYx60RqKvN60=", 100)
}
}

View File

@@ -38,8 +38,7 @@ func TestDec(t *testing.T) {
prikey := "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKRzpev8mFYTH9Y/EbardUctD4ViT+gKZDXLXgHmPjE4lj6YBagM462LewBeDQqTrraBaNYRPWcsjpgQPjBv0daifU0BmfjuWBzmiEikR1T3Ulqjzlf+SCQPc29NO+EWJy216lQ6ythRk3ysBAITsCHPAaAqcKIhFj+QTyM4IJ5lAgMBAAECgYEAg/vlMI7b3EkhBhw8JTVavLMnf8+1fe/JGXuMiU22oF5gBwCPmZ4upLwLDfJt2Q1J7WPTNetEMqgKEXUH1GwKJkFGm2tSEMHSHdTmUTQ3w6bS1C0peZghyhmlWRXUlpKk5tDOQ24sWO268YrwZyueXnVGKJ4s0hY0KOiZIU2trUECQQDxA+lzq/t/L09M/bUybjsiP6eb/HBeZeu+2+JnHekb8z9BMXTOKTHqAI0Cs9UvE6BDT3aU9IJbWHbRogIMypT9AkEArq0fccphwWtAIyS0+fns4Hqs4On7yTfXSXWiAbSVif1LxP60b5n5Xm8lo12oHkdwOvKaesvgDpnIGUM9xjFfiQJASTZrABxKNYRljnmzRTJ+/BRiEdxJNiO3zS52Q+SuHzNxD5i6ZrXU18R7EUsXg0lu8YN9/hmYT687yMpx3Pjc8QJAZBs1lSouQgIsPLfRvB1+otvLbg7KzPPivufak+2hcfanUNvEHt14a6V5RZnsOoYojK/y1oM3AkchxVCi+43aOQJAC0gI6qsZ3VaPu9QDddrHPJ1dCHTXyfcNJ0op3srCVF92HoBWX54pzeagj+9g/Z4oUT9IhaO0Q3YE07N03HuVrQ=="
//encryptedBody := "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"
encryptedBody := "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"
encryptedBody := "DiOna3ZZzIhriSHzQFMAyfRvt/aye8pkC/RBC6wKYoyrsjDQ+MjWYBkb8eml8S9zTyiuyRmsB/4Shr0O5HeFecA+XfyAIlk4tuVHnjK5eOLF6Ovfr2LlZ+yR23kB5jmJAVG7+n0Jovx9/UqMzl9pPafx47Rz3Ab+kOaL8ffjBspT5sRH7H+P4VgbQgTAPMAZNh7A+hREQneAPlhEefYs1bIyYof8E1QQk7K6HaoRCd9bwO1l3EiTsp8goOIH4LFFJISAsuAm63O1r9eIcCqWgR7DogD62mHCf4aCOwbGOrAojnq1nN1P2ITxJH78NjlFZvmMgwZrS8sOi5zuYyV+xCoyDokSe5sQxjG6QxnVT2qI38yuBJE4BYEev5bFVPEbVs0DOVMYnqULMX3yk8ggqTcx8C4lwlW5FYL0PJRiOpBQeomOzdH2Yp1ZE6s5o6EuQLiAvJoLAjf722jnYDYy6q0ZqgRdGp8HE/NsniZf+PASDD0e8JrhcvJ+lJbCpB9WksVT7fRXR8i1Pv2bQvNJtp3/M+C9XwvrMK/O/8FJZ51MKelMZi9l+YafvxboB0ryC9kmHO3P2aTZQnKrtYwbIIiuTgbdtMbVQAVvkMtBmC6i0apJTvxo1VA3j46agu6Mhi5jpo+mQXWqMbbMGZ+Sof2FmwZyBFjfLRntueyoAtU="
// Use DecryptWithPrivateKey function to decrypt
decrypted, err := DecryptWithPrivateKey(prikey, encryptedBody)
if err != nil {
@@ -47,10 +46,4 @@ func TestDec(t *testing.T) {
}
t.Logf("Decrypted: %s", decrypted)
t.Log("Test passed! Decryption matches Java result.")
// const orderStr = 'alipay_sdk=xxx...';
//const encodedPayUrl = encodeURIComponent(orderStr);
//alipay: //platformapi/startapp?appId=20000067&url=alipay-sdk-java-4.40.33.ALL%26app_id%3d2021005196642063%26biz_content%3d%257B%2522business_params%2522%253A%2522%257B%255C%2522outTradeRiskInfo%255C%2522%253A%255C%2522%257B%255C%255C%255C%2522sysVersion%255C%255C%255C%2522%253A%255C%255C%255C%2522iOS%2b26.2%255C%255C%255C%2522%252C%255C%255C%255C%2522mcCreateTradePackage%255C%255C%255C%2522%253A%255C%255C%255C%2522com.ltjy.uni1005507%255C%255C%255C%2522%252C%255C%255C%255C%2522mcCreateTradeTime%255C%255C%255C%2522%253A%255C%255C%255C%25222025-11-22%2b01%253A39%253A56%255C%255C%255C%2522%252C%255C%255C%255C%2522mcCreateTradeLbs%255C%255C%255C%2522%253A%255C%255C%255C%2522118.9885703919527%252C36.36133429662592%255C%255C%255C%2522%252C%255C%255C%255C%2522mobileOperatingPlatform%255C%255C%255C%2522%253A%255C%255C%255C%2522ios%255C%255C%255C%2522%252C%255C%255C%255C%2522platformType%255C%255C%255C%2522%253A%255C%255C%255C%2522iPad%2bPro%2b%252812.9-inch%2529%2b%25283rd%2bgeneration%2529%255C%255C%255C%2522%252C%255C%255C%255C%2522mcCreateTradeChannel%255C%255C%255C%2522%253A%255C%255C%255C%2522app%255C%255C%255C%2522%252C%255C%255C%255C%2522extraAccountRegTime%255C%255C%255C%2522%253A%255C%255C%255C%25222025-11-18%2b15%253A57%253A21%255C%255C%255C%2522%252C%255C%255C%255C%2522extraAccountPhone%255C%255C%255C%2522%253A%255C%255C%255C%252217862666120%255C%255C%255C%2522%252C%255C%255C%255C%2522netWork%255C%255C%255C%2522%253A%255C%255C%255C%2522unknown%255C%255C%255C%2522%257D%255C%2522%252C%255C%2522mc_create_trade_ip%255C%2522%253A%255C%2522123.168.253.227%255C%2522%257D%2522%252C%2522out_trade_no%2522%253A%25222511220139557299472%2522%252C%2522subject%2522%253A%2522%25E9%25AA%2586%25E9%25A9%25BC%25E6%258A%25B5%25E6%2589%25A3%25E5%2588%25B8-50%25E5%2585%2583%25E9%259D%25A2%25E5%2580%25BC%2522%252C%2522timeout_express%2522%253A%252210m%2522%252C%2522total_amount%2522%253A%252250.00%2522%257D%26charset%3dUTF-8%26format%3djson%26method%3dalipay.trade.app.pay%26notify_url%3dhttps%253A%252F%252Frecharge3.bac365.com%252Fpayment%252Falipay%252Fnotify%26sign%3dDQ0Kf5KCZ1jUHptXuAriStVEHDbKdeNWofxZr%252FAL%252BAA%252FYytu0kXNTDkviGLKMG9je2wS419dKGqX8inT9y7G%252FtcactTKoLavi3gy6RLys0Yof97B1umkopzqIhQ2nA9lxDsaLEfqXfdv33zaRetQFgPGzIv5KrOPRY18RWvwp5PqKodSp9AzbJ5bAGELTzbJ8Oj4YOeNSFelsOUaD0J4Ecw%252B7qmDIDb8UDIBYXFaNUquPtWE%252FjAZ%252FGM9PWtMYx7%252Fiq2mmbJJTe5ErN7L%252BtsXGV51axZ2f%252F0EZKISMKkgI0I5ECnHZjGa2pw%252FByMeMREYF05ZuF%252Fowcn53Snag3j0aw%253D%253D%26sign_type%3dRSA2%26timestamp%3d2025-11-22%2b01%253A39%253A56%26version%3d1.0
//window.location.href = alipayScheme;
}