Files
affiliate_dash/backend/internal/service/rock_bindlink.go
T
yml2213andCursor fcbd7715c7 fix(rock): 按链接签名时间提前刷新二维码,扫码查询改为 5 秒
腾讯活动链接大约 10 分钟后签名过期。从链接 timestamp 起满 8 分钟就换新码,避免买家扫到失效二维码。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-07 09:49:26 +08:00

123 lines
3.4 KiB
Go

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