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).Unscoped().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).Unscoped().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).Unscoped().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).Unscoped().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 }