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 下通过;全量测试通过。
214 lines
7.1 KiB
Go
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
|
|
}
|