feat(camelOil): 使用智能调度算法优化Token绑定逻辑

- 移除旧的可用Token检查和轮询绑定逻辑
- 新增GetOptimalTokenForBinding方法,按上传时间顺序选择最优Token
- 增加isTokenEligible方法,全面校验Token有效性和用户余额
- 绑定卡密操作改为调用智能调度算法选出的最优Token进行绑定
- 完善绑定失败和异常处理,增加日志记录细节信息
- 提升绑卡选Token的准确性和业务鲁棒性
This commit is contained in:
danial
2025-12-11 23:13:59 +08:00
parent 74d892fad9
commit eda5ff8fed
2 changed files with 103 additions and 32 deletions

View File

@@ -327,17 +327,6 @@ func (s *sCamelOil) CronCardBindingTask(ctx context.Context) error {
failCount := 0
for _, order := range orders {
// 检查是否有可用的 Token
availableTokenCount, err2 := dao.V1CamelOilToken.Ctx(ctx).DB(config.GetDatabaseV1()).
Where(dao.V1CamelOilToken.Columns().Status, consts.CamelOilTokenStatusAvailable).
Count()
if err2 != nil || availableTokenCount == 0 {
glog.Warningf(ctx, "无可用 Token订单 ID: %d 无法绑定", order.Id)
failCount++
continue
}
// 检查卡号和卡密是否存在
if order.CardNumber == "" || order.CardPassword == "" {
glog.Warningf(ctx, "订单 %d 卡号或卡密未填写,无法绑定", order.Id)
@@ -345,7 +334,7 @@ func (s *sCamelOil) CronCardBindingTask(ctx context.Context) error {
continue
}
// 尝试绑定卡密到 Token
// 尝试绑定卡密到 Token(使用智能调度算法)
_, err = s.BindCardToToken(ctx, &model.CamelOilCardBindInput{
OrderId: order.Id,
CardNumber: order.CardNumber,

View File

@@ -152,6 +152,100 @@ func (s *sCamelOil) ListTokensWithPagination(ctx context.Context, req *model.Cam
return tokens, totalCount, nil
}
// GetOptimalTokenForBinding 获取最优Token用于绑卡按上传时间顺序选择
func (s *sCamelOil) GetOptimalTokenForBinding(ctx context.Context, amount decimal.Decimal) (*entity.V1CamelOilToken, error) {
// 获取所有普通用户上传的可用的Token按上传时间顺序最早上传的在前
var tokens []*entity.V1CamelOilToken
err := dao.V1CamelOilToken.Ctx(ctx).DB(config.GetDatabaseV1()).
Where(dao.V1CamelOilToken.Columns().Status, consts.CamelOilTokenStatusAvailable).
WhereNot(dao.V1CamelOilToken.Columns().UserId, ""). // 只获取普通用户上传的TokenUserId不为空
OrderAsc(dao.V1CamelOilToken.Columns().CreatedAt). // 按上传时间排序,最早上传的在前
Scan(&tokens)
if err != nil {
return nil, gerror.Wrap(err, "查询Token列表失败")
}
if len(tokens) == 0 {
return nil, gerror.New("没有可用的用户Token")
}
// 按上传时间顺序筛选第一个符合条件的Token
for _, token := range tokens {
if s.isTokenEligible(ctx, token, amount) {
glog.Infof(ctx, "按上传时间顺序选择用户Token: tokenId=%d, phone=%s, name=%s, userId=%s, 金额=%.2f",
token.Id, token.Phone, token.Name, token.UserId, amount.InexactFloat64())
return token, nil
}
}
return nil, gerror.New("没有符合条件的用户Token")
}
// isTokenEligible 检查Token是否符合使用条件简化版
func (s *sCamelOil) isTokenEligible(ctx context.Context, token *entity.V1CamelOilToken, amount decimal.Decimal) bool {
// 基本状态检查
if token.Status != int(consts.CamelOilTokenStatusAvailable) {
return false
}
// 检查Token有效性是否过期
if token.LoginTokenExpiresAt != nil && token.LoginTokenExpiresAt.Before(gtime.Now()) {
return false
}
// 检查充值限制:总绑卡数量或金额任一超过限制就停止调度
// 1. 充值金额限制检查(检查本次充值后是否会超过限制)
if token.RechargeLimitAmount.Cmp(decimal.Zero) > 0 {
newTotalAmount := token.TotalBindAmount.Add(amount)
if newTotalAmount.Cmp(token.RechargeLimitAmount) > 0 {
glog.Debugf(ctx, "Token本次充值后将超限tokenId=%d, 已充值=%.2f, 本次=%.2f, 合计=%.2f, 限制=%.2f",
token.Id, token.TotalBindAmount.InexactFloat64(), amount.InexactFloat64(),
newTotalAmount.InexactFloat64(), token.RechargeLimitAmount.InexactFloat64())
return false
}
}
// 2. 充值次数限制检查
if token.RechargeLimitCount > 0 {
if token.BindCount >= token.RechargeLimitCount {
glog.Debugf(ctx, "Token充值次数已达上限tokenId=%d, 已使用=%d, 限制=%d",
token.Id, token.BindCount, token.RechargeLimitCount)
return false
}
}
// 获取用户余额信息和状态检查
if token.UserId != "" {
// 1. 检查用户状态是否正常
isNormal, err := service.SysUser().CheckUserNormal(ctx, token.UserId)
if err != nil {
glog.Warningf(ctx, "检查用户状态失败userId=%s: %v", token.UserId, err)
return false
}
if !isNormal {
glog.Debugf(ctx, "用户状态异常userId=%s", token.UserId)
return false
}
// 2. 检查用户余额是否充足
isEnough, err := service.SysUserPayment().CheckBalanceEnough(ctx, &model.SysUserPaymentCheckBalanceInput{
UserId: token.UserId,
Amount: amount,
})
if err != nil {
glog.Warningf(ctx, "检查用户余额失败userId=%s: %v", token.UserId, err)
return false
}
if !isEnough {
glog.Debugf(ctx, "用户余额不足userId=%s, 金额=%.2f", token.UserId, amount.InexactFloat64())
return false
}
}
return true
}
// UpdateTokenInfo 修改 Token 基本信息(不包括 tokenValue
func (s *sCamelOil) UpdateTokenInfo(ctx context.Context, req *model.CamelOilTokenUpdateInput) error {
m := dao.V1CamelOilToken.Ctx(ctx).DB(config.GetDatabaseV1())
@@ -269,12 +363,13 @@ func (s *sCamelOil) UpdateTokenStatus(ctx context.Context, req *model.CamelOilTo
// 卡密绑定相关方法
// ====================================================================================
// BindCardToToken 绑定卡密到 Token使用轮询算法选择 Token
// BindCardToToken 绑定卡密到 Token使用智能调度算法选择 Token
func (s *sCamelOil) BindCardToToken(ctx context.Context, req *model.CamelOilCardBindInput) (bindingId int64, err error) {
orderId := req.OrderId
cardNumber := req.CardNumber
cardPassword := req.CardPassword
amount := req.Amount
// 1. 获取订单信息
var order *entity.V1CamelOilOrder
err = dao.V1CamelOilOrder.Ctx(ctx).DB(config.GetDatabaseV1()).
@@ -287,26 +382,13 @@ func (s *sCamelOil) BindCardToToken(ctx context.Context, req *model.CamelOilCard
return 0, gerror.New("订单不存在")
}
// 2. 获取所有可用的 Token
tokens, err := s.ListTokens(ctx)
// 2. 使用智能调度算法选择最优 Token
selectedToken, err := s.GetOptimalTokenForBinding(ctx, amount)
if err != nil {
return 0, gerror.Wrap(err, "查询Token列表失败")
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
}
if selectedToken == nil {
return 0, gerror.New("没有符合条件的Token")
}
// 4.2 调用绑卡接口(使用选中 Token 的 LoginToken
@@ -436,7 +518,7 @@ func (s *sCamelOil) GetCardBindingsByToken(ctx context.Context, req *model.Camel
return nil, 0, gerror.Wrap(err, "查询绑定记录失败")
}
return bindings, int(count), nil
return bindings, count, nil
}
// GetTokenBindingStats 获取 Token 的绑定统计