- 新增CamelOilToken和CamelOilCardBinding数据库表,实现Token及卡密绑定记录管理 - 在service层增加Token的创建、查询、更新、删除及分页功能 - 实现卡密与Token绑定的业务逻辑,支持基于Token的卡密管理 - 在API层新增Token和卡密绑定相关接口:创建Token、获取Token详情、删除Token、列出Token及根据Token查询绑定卡密 - camel_oil_api新增绑卡接口,支持绑卡状态分类及错误处理 - 在定时任务中增加卡密绑定任务,实现自动处理已支付订单的卡密绑定 - 优化订单提交及支付流程,包含日志调整和请求参数随机扰动 - 统一调整camel_oil模块多控制器实现,完成账号状态查询及订单相关接口实现 - 注册更多camel_oil定时任务,包括订单支付检查、账号日重置和待回调订单处理任务
360 lines
12 KiB
Go
360 lines
12 KiB
Go
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
|
||
}
|