Files
affiliate_dash/backend/internal/service/rock_service.go
T

730 lines
23 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 service
import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"strings"
"sync"
"time"
"affiliate_dash/internal/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type rockRefreshCacheItem struct {
response *RockRefreshResponse
cachedAt time.Time
}
// RockService 负责管理 Rock 平台的配置、库存池、采购、UID强一致风控与状态机
type RockService struct {
db *gorm.DB
client *RockClient
mu sync.Mutex
refreshCache sync.Map // key: upstreamTokenID string, value: rockRefreshCacheItem
}
func NewRockService(db *gorm.DB, client *RockClient) *RockService {
return &RockService{
db: db,
client: client,
}
}
// RockStatsResponse 看板与库存统计返回
type RockStatsResponse struct {
AvailableStock int64 `json:"available_stock"`
UsingStock int64 `json:"using_stock"`
BoundStock int64 `json:"bound_stock"`
RedeemedStock int64 `json:"redeemed_stock"`
MismatchStock int64 `json:"mismatch_stock"` // 绑定错误(待排查)
AbnormalStock int64 `json:"abnormal_stock"`
UpstreamPoints float64 `json:"upstream_points"`
AutoDeliveryEnabled bool `json:"auto_delivery_enabled"`
TokenWarningStock int `json:"token_warning_stock"`
AutoPurchaseEnabled bool `json:"auto_purchase_enabled"`
Username string `json:"username"`
LastHeartbeatAt string `json:"last_heartbeat_at,omitempty"`
}
type UpdateRockConfigReq struct {
AutoDeliveryEnabled *bool `json:"auto_delivery_enabled"`
BaseURL *string `json:"base_url"`
BearerToken *string `json:"bearer_token"`
MinPointsWarning *float64 `json:"min_points_warning"`
TokenWarningStock *int `json:"token_warning_stock"`
AutoPurchaseEnabled *bool `json:"auto_purchase_enabled"`
AutoPurchaseCount *int `json:"auto_purchase_count"`
PurchaseCooldownMinutes *int `json:"purchase_cooldown_minutes"`
}
// GetConfig 获取当前配置,若无则初始化
func (s *RockService) GetConfig(ctx context.Context) (*model.RockConfig, error) {
var cfg model.RockConfig
err := s.db.WithContext(ctx).First(&cfg, 1).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
cfg = model.RockConfig{
ID: 1,
AutoDeliveryEnabled: true,
BaseURL: "https://0666.pro",
BearerToken: "",
MinPointsWarning: 1050.00,
TokenWarningStock: 10,
AutoPurchaseEnabled: false,
AutoPurchaseCount: 50,
PurchaseCooldownMinutes: 10,
}
if createErr := s.db.WithContext(ctx).Create(&cfg).Error; createErr != nil {
return nil, createErr
}
return &cfg, nil
}
if err != nil {
return nil, err
}
return &cfg, nil
}
// UpdateConfig 更新系统配置
func (s *RockService) UpdateConfig(ctx context.Context, req UpdateRockConfigReq) (*model.RockConfig, error) {
cfg, err := s.GetConfig(ctx)
if err != nil {
return nil, err
}
updates := make(map[string]interface{})
if req.AutoDeliveryEnabled != nil {
updates["auto_delivery_enabled"] = *req.AutoDeliveryEnabled
}
if req.BaseURL != nil && strings.TrimSpace(*req.BaseURL) != "" {
updates["base_url"] = strings.TrimRight(strings.TrimSpace(*req.BaseURL), "/")
s.client.baseURL = updates["base_url"].(string)
}
if req.BearerToken != nil {
updates["bearer_token"] = strings.TrimSpace(*req.BearerToken)
}
if req.MinPointsWarning != nil {
updates["min_points_warning"] = *req.MinPointsWarning
}
if req.TokenWarningStock != nil {
updates["token_warning_stock"] = *req.TokenWarningStock
}
if req.AutoPurchaseEnabled != nil {
updates["auto_purchase_enabled"] = *req.AutoPurchaseEnabled
}
if req.AutoPurchaseCount != nil {
updates["auto_purchase_count"] = *req.AutoPurchaseCount
}
if req.PurchaseCooldownMinutes != nil {
updates["purchase_cooldown_minutes"] = *req.PurchaseCooldownMinutes
}
if len(updates) > 0 {
updates["updated_at"] = time.Now()
if err := s.db.WithContext(ctx).Model(&model.RockConfig{}).Where("id = ?", cfg.ID).Updates(updates).Error; err != nil {
return nil, err
}
}
return s.GetConfig(ctx)
}
// TestConnection 测试上游 Token 连通性并同步最新用户信息与积分
func (s *RockService) TestConnection(ctx context.Context, bearerToken string) (*RockMeResponse, error) {
token := strings.TrimSpace(bearerToken)
if token == "" {
cfg, err := s.GetConfig(ctx)
if err != nil {
return nil, err
}
token = cfg.BearerToken
}
if token == "" {
return nil, fmt.Errorf("请先输入或配置上游 Bearer Token")
}
me, err := s.client.GetMe(ctx, token)
if err != nil {
return nil, err
}
// 连通成功,顺便异步更新数据库快照
now := time.Now()
_ = s.db.WithContext(ctx).Model(&model.RockConfig{}).Where("id = 1").Updates(map[string]interface{}{
"username": me.User.Username,
"points": me.User.Points.Float64(),
"last_heartbeat_at": now,
}).Error
return me, nil
}
// GetStats 获取概览统计
func (s *RockService) GetStats(ctx context.Context) (*RockStatsResponse, error) {
cfg, err := s.GetConfig(ctx)
if err != nil {
return nil, err
}
var stats RockStatsResponse
stats.AutoDeliveryEnabled = cfg.AutoDeliveryEnabled
stats.TokenWarningStock = cfg.TokenWarningStock
stats.AutoPurchaseEnabled = cfg.AutoPurchaseEnabled
stats.UpstreamPoints = cfg.Points
stats.Username = cfg.Username
if cfg.LastHeartbeatAt != nil {
stats.LastHeartbeatAt = cfg.LastHeartbeatAt.Format("2006-01-02 15:04:05")
}
type CountResult struct {
Status string
Total int64
}
var results []CountResult
err = s.db.WithContext(ctx).Model(&model.RockToken{}).
Select("status, count(*) as total").
Group("status").
Scan(&results).Error
if err != nil {
return nil, err
}
for _, r := range results {
switch r.Status {
case model.RockStatusAvailable:
stats.AvailableStock = r.Total
case model.RockStatusUsing:
stats.UsingStock = r.Total
case model.RockStatusBound:
stats.BoundStock = r.Total
case model.RockStatusRedeemed:
stats.RedeemedStock = r.Total
case model.RockStatusMismatch:
stats.MismatchStock = r.Total
case model.RockStatusAbnormal:
stats.AbnormalStock = r.Total
}
}
return &stats, nil
}
// PurchaseTokens 执行采购并批量入库(通用黑色 Token)
func (s *RockService) PurchaseTokens(ctx context.Context, count int, isAuto bool) ([]model.RockToken, error) {
s.mu.Lock()
defer s.mu.Unlock()
cfg, err := s.GetConfig(ctx)
if err != nil {
return nil, err
}
if cfg.BearerToken == "" {
return nil, fmt.Errorf("尚未配置上游 Bearer Token,无法执行采购")
}
// 自动补货防抖冷却检查
now := time.Now()
if isAuto && cfg.LastPurchasedAt != nil {
cooldown := time.Duration(cfg.PurchaseCooldownMinutes) * time.Minute
if now.Sub(*cfg.LastPurchasedAt) < cooldown {
return nil, fmt.Errorf("自动采购处于冷却中(冷却时间 %d 分钟)", cfg.PurchaseCooldownMinutes)
}
}
// 调上游 Claim 接口(统一下单黑色通用 Token)
tokens, err := s.client.ClaimTokens(ctx, cfg.BearerToken, model.RockColorBlack, count)
if err != nil {
return nil, fmt.Errorf("调用上游采购接口失败: %w", err)
}
if len(tokens) == 0 {
return nil, fmt.Errorf("上游未返回任何有效 Token")
}
var dbTokens []model.RockToken
for _, t := range tokens {
dbTokens = append(dbTokens, model.RockToken{
UpstreamTokenID: fmt.Sprintf("%d", t.ID),
Token: t.Token,
RedeemCode: t.RedeemCode,
Color: model.RockColorBlack,
Region: t.Region,
Status: model.RockStatusAvailable,
ClaimedAt: &now,
})
}
// 事务写入库存表
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&dbTokens).Error; err != nil {
return err
}
// 更新最后采购时间
return tx.Model(&model.RockConfig{}).Where("id = ?", cfg.ID).Update("last_purchased_at", now).Error
})
if err != nil {
return nil, fmt.Errorf("入库采购 Token 失败: %w", err)
}
// 异步更新上游剩余积分快照
go func() {
bgCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
me, meErr := s.client.GetMe(bgCtx, cfg.BearerToken)
if meErr == nil && me != nil {
_ = s.db.Model(&model.RockConfig{}).Where("id = 1").Update("points", me.User.Points.Float64()).Error
}
}()
return dbTokens, nil
}
// CheckStockAndAutoPurchase 水位检查与自动采购触发
func (s *RockService) CheckStockAndAutoPurchase(ctx context.Context) {
cfg, err := s.GetConfig(ctx)
if err != nil {
return
}
var count int64
s.db.WithContext(ctx).Model(&model.RockToken{}).
Where("status = ? AND color = ?", model.RockStatusAvailable, model.RockColorBlack).
Count(&count)
if count <= int64(cfg.TokenWarningStock) {
log.Printf("[RockService] 可用库存预警: 当前仅存 %d 个 (预警阈值 %d)", count, cfg.TokenWarningStock)
if cfg.AutoPurchaseEnabled {
buyCount := cfg.AutoPurchaseCount
if buyCount <= 0 {
buyCount = 50
}
log.Printf("[RockService] 触发自动补货,采购数量: %d", buyCount)
_, err := s.PurchaseTokens(ctx, buyCount, true)
if err != nil {
log.Printf("[RockService] 自动补货失败: %v", err)
} else {
log.Printf("[RockService] 自动补货成功,新增 %d 个 Token", buyCount)
}
}
}
}
// BindingSnapshot 保存分配 Token 时上游的绑定快照
type BindingSnapshot struct {
Bound bool `json:"bound"`
RoleName string `json:"role_name"`
RoleID string `json:"role_id"`
Channel string `json:"channel"`
BindChangedAt string `json:"bind_changed_at"`
}
// AssignTokenForOrder 为指定订单锁定分配一个可用 Token
func (s *RockService) AssignTokenForOrder(ctx context.Context, orderNo, expectedUID string) (*model.RockToken, *RockRedeemDetailResponse, error) {
cfg, err := s.GetConfig(ctx)
if err != nil {
return nil, nil, err
}
if !cfg.AutoDeliveryEnabled {
return nil, nil, fmt.Errorf("Rock 自动发货通道已维护关闭")
}
var token model.RockToken
// 1. 检查订单是否已分配过 Token
err = s.db.WithContext(ctx).
Where("assigned_order_no = ? AND status IN (?, ?)", orderNo, model.RockStatusUsing, model.RockStatusBound).
First(&token).Error
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil, err
}
if err == nil {
trimmedUID := strings.TrimSpace(expectedUID)
if trimmedUID != "" && token.ExpectedUID != trimmedUID {
token.ExpectedUID = trimmedUID
_ = s.db.WithContext(ctx).Model(&token).Update("expected_uid", trimmedUID).Error
}
}
// 2. 若尚未分配,锁定一个可用 Token (行锁防并发超卖)
if errors.Is(err, gorm.ErrRecordNotFound) {
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Clauses(clause.Locking{Strength: "UPDATE", Options: "SKIP LOCKED"}).
Where("status = ? AND color = ?", model.RockStatusAvailable, model.RockColorBlack).
Order("id ASC").
First(&token).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("Rock平台库存不足,请联系管理员补货")
}
return err
}
token.Status = model.RockStatusUsing
token.AssignedOrderNo = orderNo
token.ExpectedUID = strings.TrimSpace(expectedUID)
return tx.Save(&token).Error
})
if err != nil {
return nil, nil, err
}
// 检查水位触发异步自动补货
go s.CheckStockAndAutoPurchase(context.Background())
}
// 3. 调上游获取绑定活动链接
detail, err := s.client.GetRedeemDetail(ctx, token.RedeemCode)
if err != nil {
return nil, nil, fmt.Errorf("获取上游活动绑定链接失败: %w", err)
}
// 4. 首次分配记录上游初始绑定快照(防范 Ghost-Binding 历史残留数据)
if token.InitialBoundSnapshot == "" && cfg.BearerToken != "" {
ref, refErr := s.client.RefreshToken(ctx, cfg.BearerToken, token.UpstreamTokenID)
if refErr == nil && ref != nil {
snap := BindingSnapshot{
Bound: ref.IsBound(),
RoleName: ref.GetRoleName(),
RoleID: ref.GetRoleID(),
Channel: ref.GetChannel(),
BindChangedAt: ref.Token.BindChangedAt,
}
snapBytes, _ := json.Marshal(snap)
token.InitialBoundSnapshot = string(snapBytes)
_ = s.db.WithContext(ctx).Model(&token).Update("initial_bound_snapshot", token.InitialBoundSnapshot).Error
}
}
return &token, detail, nil
}
type CheckBindingResult struct {
Bound bool `json:"bound"`
RoleName string `json:"role_name"`
RoleID string `json:"role_id"`
Channel string `json:"channel"`
ExpectedUID string `json:"expected_uid"`
Mismatch bool `json:"mismatch"`
Message string `json:"message"`
UpstreamFulfillID int64 `json:"upstream_fulfill_id,omitempty"`
}
// CheckBindingAndVerifyUID Anti-Ghost 基线比对与 UID 强一致门禁
func (s *RockService) CheckBindingAndVerifyUID(ctx context.Context, tokenID uint, expectedUID string) (*CheckBindingResult, error) {
cfg, err := s.GetConfig(ctx)
if err != nil {
return nil, err
}
var token model.RockToken
if err := s.db.WithContext(ctx).First(&token, tokenID).Error; err != nil {
return nil, err
}
// 若已经是 mismatch 状态,直接返回阻断提示
if token.Status == model.RockStatusMismatch {
return &CheckBindingResult{
Bound: false,
Mismatch: true,
RoleName: token.ActualRoleName,
RoleID: token.ActualRoleID,
Channel: token.ActualChannel,
ExpectedUID: token.ExpectedUID,
Message: fmt.Sprintf("UID 不匹配阻断: %s", token.MismatchReason),
}, nil
}
// 调上游 refresh 接口(带 800ms 瞬时防抖缓存,避免自动轮询与手动高频点击撞上上游 110001 频控)
var ref *RockRefreshResponse
cacheVal, hasCache := s.refreshCache.Load(token.UpstreamTokenID)
if hasCache {
item := cacheVal.(rockRefreshCacheItem)
if time.Since(item.cachedAt) < 800*time.Millisecond {
ref = item.response
}
}
if ref == nil {
upstreamRef, err := s.client.RefreshToken(ctx, cfg.BearerToken, token.UpstreamTokenID)
if err != nil {
// 若上游返回 110001 "请求过于频繁",进行平滑降级处理,绝不报致命错误打断买家
if strings.Contains(err.Error(), "过于频繁") || strings.Contains(err.Error(), "110001") {
if hasCache {
ref = cacheVal.(rockRefreshCacheItem).response
} else if token.Status == model.RockStatusBound {
return &CheckBindingResult{
Bound: true,
RoleName: token.ActualRoleName,
RoleID: token.ActualRoleID,
Channel: token.ActualChannel,
ExpectedUID: token.ExpectedUID,
Message: "扫码授权成功,UID核验一致",
}, nil
} else {
return &CheckBindingResult{
Bound: false,
Message: "正在同步微信/QQ扫码状态,请稍后再次检查",
}, nil
}
} else {
return nil, fmt.Errorf("刷新绑定状态失败: %w", err)
}
} else {
ref = upstreamRef
s.refreshCache.Store(token.UpstreamTokenID, rockRefreshCacheItem{
response: ref,
cachedAt: time.Now(),
})
}
}
// 1. 检查上游是否已绑定 (上游状态 bound 或 bound_account_id 有值)
if !ref.IsBound() || strings.TrimSpace(ref.GetRoleID()) == "" {
return &CheckBindingResult{
Bound: false,
Message: "等待买家微信/QQ扫码授权",
}, nil
}
actualRoleName := ref.GetRoleName()
actualRoleID := ref.GetRoleID()
actualChannel := ref.GetChannel()
// 2. Anti-Ghost 历史残留比对
if token.InitialBoundSnapshot != "" {
var snap BindingSnapshot
if jsonErr := json.Unmarshal([]byte(token.InitialBoundSnapshot), &snap); jsonErr == nil {
// 若当前绑定的角色、UID 与初始分配时完全一样且变更时间没有更新,说明买家还未扫码,仍是历史旧数据
if snap.Bound && snap.RoleID == actualRoleID && snap.BindChangedAt == ref.Token.BindChangedAt {
return &CheckBindingResult{
Bound: false,
Message: "等待买家扫码(检测到历史绑定残留,已自动忽略)",
}, nil
}
}
}
// 3. 核心门禁:UID 强一致校验
trimmedExpected := strings.TrimSpace(expectedUID)
if trimmedExpected == "" {
trimmedExpected = strings.TrimSpace(token.ExpectedUID)
}
now := time.Now()
if trimmedExpected != "" && actualRoleID != trimmedExpected {
// 严禁发货!跃迁为 mismatch
mismatchReason := fmt.Sprintf("预期UID(%s) 与 扫码UID(%s/%s) 不一致", trimmedExpected, actualRoleName, actualRoleID)
_ = s.db.WithContext(ctx).Model(&token).Updates(map[string]interface{}{
"status": model.RockStatusMismatch,
"actual_role_name": actualRoleName,
"actual_role_id": actualRoleID,
"actual_channel": actualChannel,
"mismatch_at": now,
"mismatch_reason": mismatchReason,
}).Error
return &CheckBindingResult{
Bound: false,
Mismatch: true,
RoleName: actualRoleName,
RoleID: actualRoleID,
Channel: actualChannel,
ExpectedUID: trimmedExpected,
Message: fmt.Sprintf("扫码角色UID(%s)与订单填写UID(%s)不一致,严禁发货!", actualRoleID, trimmedExpected),
}, nil
}
// 4. UID 一致,更新为 bound 成功状态并保存玩家角色名称
_ = s.db.WithContext(ctx).Model(&token).Updates(map[string]interface{}{
"status": model.RockStatusBound,
"actual_role_name": actualRoleName,
"actual_role_id": actualRoleID,
"actual_channel": actualChannel,
"bound_at": now,
}).Error
return &CheckBindingResult{
Bound: true,
RoleName: actualRoleName,
RoleID: actualRoleID,
Channel: actualChannel,
ExpectedUID: trimmedExpected,
Message: "扫码授权成功,UID核验一致",
}, nil
}
// RedeemTokenForOrder 执行最终发货兑换(动态判定皮肤)
func (s *RockService) RedeemTokenForOrder(ctx context.Context, orderNo, sku, expectedUID string) (*RockRedeemResponse, error) {
cfg, err := s.GetConfig(ctx)
if err != nil {
return nil, err
}
if !cfg.AutoDeliveryEnabled {
return nil, fmt.Errorf("Rock 自动发货通道维护中,已关闭自动兑换")
}
var token model.RockToken
err = s.db.WithContext(ctx).
Where("assigned_order_no = ? AND status = ?", orderNo, model.RockStatusBound).
First(&token).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("未找到该订单已绑定的有效 Token,或当前状态不可发货")
}
return nil, err
}
// 最终双重防御:核验 UID
trimmedExpected := strings.TrimSpace(expectedUID)
if trimmedExpected == "" {
trimmedExpected = strings.TrimSpace(token.ExpectedUID)
}
if trimmedExpected != "" && token.ActualRoleID != trimmedExpected {
return nil, fmt.Errorf("终审拦截:扫码UID(%s)与预期UID(%s)不符,禁止发货!", token.ActualRoleID, trimmedExpected)
}
// 根据商品 SKU 动态决定兑换的皮肤名称
targetSkin := model.RockSkinBlackTshirt
if sku == model.RockSKUBlueSportswear {
targetSkin = model.RockSkinBlueSportswear
}
// 确保买家角色名存在
actualRoleName := strings.TrimSpace(token.ActualRoleName)
if actualRoleName == "" {
if ref, refErr := s.client.RefreshToken(ctx, cfg.BearerToken, token.UpstreamTokenID); refErr == nil && ref != nil {
actualRoleName = ref.GetRoleName()
if actualRoleName != "" {
_ = s.db.WithContext(ctx).Model(&token).Update("actual_role_name", actualRoleName).Error
}
}
}
if actualRoleName == "" {
actualRoleName = token.ActualRoleID
}
// 调上游兑换发货接口(携带 Bearer Token 鉴权)
redeemResp, err := s.client.RedeemSkin(ctx, cfg.BearerToken, token.UpstreamTokenID, targetSkin, "幸运币", actualRoleName)
if err != nil {
return nil, fmt.Errorf("调用上游兑换失败: %w", err)
}
// 提取官方 fulfillId
var fulfillID int64
now := time.Now()
ref, refErr := s.client.RefreshToken(ctx, cfg.BearerToken, token.UpstreamTokenID)
if refErr == nil && len(ref.DeliveryList) > 0 {
fulfillID = ref.DeliveryList[len(ref.DeliveryList)-1].FulfillID
}
// 状态跃迁至 redeemed
_ = s.db.WithContext(ctx).Model(&token).Updates(map[string]interface{}{
"status": model.RockStatusRedeemed,
"upstream_fulfill_id": fulfillID,
"target_skin": targetSkin,
"target_currency": "幸运币",
"target_currency_quantity": 1,
"redeemed_at": now,
}).Error
return redeemResp, nil
}
// ListTokens 分页查询 Token 列表
func (s *RockService) ListTokens(ctx context.Context, page, pageSize int, status, search string) ([]model.RockToken, int64, error) {
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 20
}
db := s.db.WithContext(ctx).Model(&model.RockToken{})
if strings.TrimSpace(status) != "" {
db = db.Where("status = ?", strings.TrimSpace(status))
}
if strings.TrimSpace(search) != "" {
term := "%" + strings.TrimSpace(search) + "%"
db = db.Where("assigned_order_no LIKE ? OR expected_uid LIKE ? OR actual_role_name LIKE ? OR actual_role_id LIKE ? OR redeem_code LIKE ? OR assigned_order_no IN (SELECT order_no FROM fulfillment_orders WHERE client_order_no LIKE ?)",
term, term, term, term, term, term)
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var tokens []model.RockToken
err := db.Order("id DESC").
Offset((page - 1) * pageSize).
Limit(pageSize).
Find(&tokens).Error
if err != nil {
return nil, 0, err
}
// 批量反查关联订单的商户单号(创建订单时的 ClientOrderNo)
orderNos := make([]string, 0, len(tokens))
for _, t := range tokens {
if t.AssignedOrderNo != "" {
orderNos = append(orderNos, t.AssignedOrderNo)
}
}
if len(orderNos) > 0 {
var orders []struct {
OrderNo string `gorm:"column:order_no"`
ClientOrderNo string `gorm:"column:client_order_no"`
}
if scanErr := s.db.WithContext(ctx).Table("fulfillment_orders").
Select("order_no, client_order_no").
Where("order_no IN ?", orderNos).
Scan(&orders).Error; scanErr == nil {
clientMap := make(map[string]string, len(orders))
for _, o := range orders {
clientMap[o.OrderNo] = o.ClientOrderNo
}
for i := range tokens {
if cNo, ok := clientMap[tokens[i].AssignedOrderNo]; ok && cNo != "" {
tokens[i].ClientOrderNo = cNo
}
}
}
}
return tokens, total, nil
}
// ResetMismatchToken 客服人工重置解冻绑定错误的 Token
func (s *RockService) ResetMismatchToken(ctx context.Context, tokenID uint) error {
var token model.RockToken
if err := s.db.WithContext(ctx).First(&token, tokenID).Error; err != nil {
return err
}
if token.Status != model.RockStatusMismatch {
return fmt.Errorf("仅 mismatch 状态的 Token 允许人工解冻重置")
}
s.refreshCache.Delete(token.UpstreamTokenID)
// 重置回 available,清空订单关联
return s.db.WithContext(ctx).Model(&token).Updates(map[string]interface{}{
"status": model.RockStatusAvailable,
"assigned_order_no": "",
"expected_uid": "",
"actual_role_name": "",
"actual_role_id": "",
"actual_channel": "",
"initial_bound_snapshot": "",
"mismatch_at": nil,
"mismatch_reason": "",
"updated_at": time.Now(),
}).Error
}