Files
affiliate_dash/backend/internal/service/rock_bindcheck.go
T
yml2213 80e2bbf875 fix(rock): 上游地址改为以数据库配置为准,并消除 baseURL 数据竞争
GetConfig 从不把 rock_configs.base_url 回写到客户端,main.go 又硬编码了
https://0666.pro,导致后台改上游地址「保存那一刻生效、进程重启后失效」,
数据库里的值被静默忽略且无任何提示。

- 新增 model.RockDefaultBaseURL 统一默认值来源
- 新增 RockService.ApplyConfigToClient,启动时把 DB 地址同步到客户端
- UpdateConfig 改用 client.SetBaseURL 立即生效,不再直接写字段
- RockClient.baseURL 原为裸字段,被 HTTP 请求线程与库存巡检 goroutine 并发
  读写(数据竞争),改为 sync.RWMutex 保护并新增 BaseURL/SetBaseURL
- ClaimTokens/RefreshToken/RedeemSkin 的 Origin/Referer 跟随配置地址
- 空值回落默认地址,末尾斜杠自动裁剪

为保持该提交可编译,同一次改动把 rock_service.go(1177 行)按域拆分为
8 个文件(最大 221 行):rock_service / rock_bindlink / rock_stock /
rock_assign / rock_bindcheck / rock_redeem / rock_recover / rock_snapshot。
35 个声明块全部有归属,无遗漏无重复;迁移文件 012 受 checksum 保护故未改动。

验证:TestRockServiceBaseURLFromConfig 覆盖「改地址→立即生效→重启→仍生效」;
并发用例在 -race 下通过;全量测试通过。
2026-10-06 19:42:58 +08:00

214 lines
7.1 KiB
Go

package service
import (
"context"
"fmt"
"strings"
"time"
"affiliate_dash/internal/model"
)
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, orderNo, 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 状态,检查传入的 expectedUID 是否已纠正为扫码角色的 UID
if token.Status == model.RockStatusMismatch {
trimmedExpected := strings.TrimSpace(expectedUID)
if trimmedExpected == "" {
trimmedExpected = strings.TrimSpace(token.ExpectedUID)
}
actualUID := strings.TrimSpace(token.ActualRoleID)
if trimmedExpected != "" && actualUID != "" && trimmedExpected == actualUID {
now := time.Now()
token.Status = model.RockStatusBound
token.ExpectedUID = trimmedExpected
token.MismatchReason = ""
token.MismatchAt = nil
token.BoundAt = &now
_ = s.db.WithContext(ctx).Model(&token).Updates(map[string]interface{}{
"expected_uid": trimmedExpected,
"status": model.RockStatusBound,
"mismatch_reason": "",
"mismatch_at": nil,
"bound_at": now,
"updated_at": now,
}).Error
return &CheckBindingResult{
Bound: true,
RoleName: token.ActualRoleName,
RoleID: actualUID,
Channel: token.ActualChannel,
ExpectedUID: trimmedExpected,
Mismatch: false,
Message: "扫码授权成功,UID核验一致",
}, nil
}
roleDesc := token.ActualRoleName
if roleDesc == "" {
roleDesc = actualUID
}
hintMsg := fmt.Sprintf("扫码角色「%s」UID(%s) 与填写 UID(%s) 不一致。如确系本人账号,请修改填写 UID 为 %s 确认绑定,否则请联系客服处理", roleDesc, actualUID, trimmedExpected, actualUID)
return &CheckBindingResult{
Bound: false,
Mismatch: true,
RoleName: token.ActualRoleName,
RoleID: token.ActualRoleID,
Channel: token.ActualChannel,
ExpectedUID: token.ExpectedUID,
Message: hintMsg,
}, nil
}
// 已释放回库存、或已改派给其他订单的链接,不能再被旧发货页轮询改写。
if token.Status != model.RockStatusUsing && token.Status != model.RockStatusBound {
return &CheckBindingResult{
Bound: false,
Message: "该提货链接已释放回库存,请重新获取二维码",
}, nil
}
if strings.TrimSpace(orderNo) != "" && token.AssignedOrderNo != "" && token.AssignedOrderNo != strings.TrimSpace(orderNo) {
return &CheckBindingResult{
Bound: false,
Message: "该提货链接已不属于当前订单",
}, 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. 历史残留不算本次扫码:角色和绑定时间与分配快照一致,或发货记录都早于本次领取。
if !rockBuyerNewlyBound(&token, ref) {
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
roleDesc := actualRoleName
if roleDesc == "" {
roleDesc = actualRoleID
}
return &CheckBindingResult{
Bound: false,
Mismatch: true,
RoleName: actualRoleName,
RoleID: actualRoleID,
Channel: actualChannel,
ExpectedUID: trimmedExpected,
Message: fmt.Sprintf("扫码角色「%s」UID(%s) 与填写 UID(%s) 不一致。如确系本人账号,请修改填写 UID 为 %s 确认绑定,否则请联系客服处理", roleDesc, actualRoleID, trimmedExpected, actualRoleID),
}, 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
}