package service import ( "context" "net/url" "strconv" "strings" "time" "affiliate_dash/internal/model" ) type rockRefreshCacheItem struct { response *RockRefreshResponse cachedAt time.Time } // 腾讯「查询绑定」按链接里的 timestamp 验签,大约 10 分钟后返回「签名已过期」。 // 从签名时间起满 8 分钟就换一条,留出余量,买家不会扫到已经失效的码。 const rockBindLinkMaxAge = 8 * time.Minute // 上游若仍返回同一条旧签名,不要每个轮询都再打一次。 const rockBindLinkRetryGap = 15 * time.Second type rockBindLinkCacheItem struct { bindURL string qrURL string cachedAt time.Time // 链接上没有 timestamp 时,用写入时间代替签名时间 fetchedAt time.Time } func rockBindLinkIssuedAt(bindURL string) (time.Time, bool) { parsed, err := url.Parse(strings.TrimSpace(bindURL)) if err != nil { return time.Time{}, false } raw := strings.TrimSpace(parsed.Query().Get("timestamp")) if raw == "" { return time.Time{}, false } value, err := strconv.ParseInt(raw, 10, 64) if err != nil || value <= 0 { return time.Time{}, false } if value >= 1_000_000_000_000 { return time.UnixMilli(value), true } return time.Unix(value, 0), true } func rockBindLinkStillFresh(item rockBindLinkCacheItem, now time.Time) bool { if strings.TrimSpace(item.bindURL) == "" { return false } issued, ok := rockBindLinkIssuedAt(item.bindURL) if !ok { issued = item.cachedAt } if issued.IsZero() { return false } return now.Sub(issued) < rockBindLinkMaxAge } func (s *RockService) rememberBindLink(tokenID uint, bindURL, qrURL string) { bindURL = strings.TrimSpace(bindURL) if s == nil || tokenID == 0 || bindURL == "" { return } qrURL = strings.TrimSpace(qrURL) if qrURL == "" { qrURL = qrCodeURL(bindURL) } now := time.Now() s.bindLinkCache.Store(tokenID, rockBindLinkCacheItem{ bindURL: bindURL, qrURL: qrURL, cachedAt: now, fetchedAt: now, }) } // freshBindLink 只为仍在扫码的同一提货号重取活动链接。过期前复用缓存,避免每轮轮询都打 0666。 func (s *RockService) freshBindLink(ctx context.Context, tokenID uint, orderNo string) (string, string) { if s == nil || s.client == nil || tokenID == 0 { return "", "" } var token model.RockToken if err := s.db.WithContext(ctx).Unscoped().First(&token, tokenID).Error; err != nil { return "", "" } if token.Status != model.RockStatusUsing || strings.TrimSpace(token.RedeemCode) == "" { return "", "" } if orderNo = strings.TrimSpace(orderNo); orderNo != "" && token.AssignedOrderNo != "" && token.AssignedOrderNo != orderNo { return "", "" } if item, ok := s.bindLinkCache.Load(token.ID); ok { cached := item.(rockBindLinkCacheItem) if rockBindLinkStillFresh(cached, time.Now()) { return cached.bindURL, cached.qrURL } if !cached.fetchedAt.IsZero() && time.Since(cached.fetchedAt) < rockBindLinkRetryGap { return cached.bindURL, cached.qrURL } } detail, err := s.client.GetRedeemDetail(ctx, token.RedeemCode) if err != nil || detail == nil || strings.TrimSpace(detail.BindURL) == "" { if item, ok := s.bindLinkCache.Load(token.ID); ok { cached := item.(rockBindLinkCacheItem) return cached.bindURL, cached.qrURL } return "", "" } bindURL := strings.TrimSpace(detail.BindURL) qrURL := strings.TrimSpace(detail.QRURL) s.rememberBindLink(token.ID, bindURL, qrURL) if qrURL == "" { qrURL = qrCodeURL(bindURL) } return bindURL, qrURL }