腾讯活动链接大约 10 分钟后签名过期。从链接 timestamp 起满 8 分钟就换新码,避免买家扫到失效二维码。 Co-authored-by: Cursor <cursoragent@cursor.com>
123 lines
3.4 KiB
Go
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
|
|
}
|