Files
kami_backend/internal/logic/camel_oil/token.go
danial 3588bf9af6 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定时任务,包括订单支付检查、账号日重置和待回调订单处理任务
2025-11-23 00:08:35 +08:00

360 lines
12 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 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
}