Files
affiliate_dash/backend/internal/service/rock_snapshot.go
T
yml2213 80e2bbf875 fix(rock): 上游地址改为以数据库配置为准,并消除 baseURL 数据竞争
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 下通过;全量测试通过。
2026-10-06 19:42:58 +08:00

117 lines
3.1 KiB
Go

package service
import (
"encoding/json"
"strings"
"time"
"affiliate_dash/internal/model"
)
// rockBuyerNewlyBound 判断上游展示的绑定是不是本次领取之后新扫上的。
// 和分配快照一致,或者绑定时间仍为空且发货记录都早于本次领取,都是历史残留,链接可以再次使用。
// 没有快照、也没有更早的发货记录时,上游已有角色按新绑定处理,避免把刚扫上的号放回库存。
func rockBuyerNewlyBound(token *model.RockToken, ref *RockRefreshResponse) bool {
if ref == nil || !ref.IsBound() || strings.TrimSpace(ref.GetRoleID()) == "" {
return false
}
snap, hasSnap := bindingSnapshot(token)
if hasSnap && snap.Bound && snap.RoleID == ref.GetRoleID() && snap.BindChangedAt == strings.TrimSpace(ref.Token.BindChangedAt) {
return false
}
if deliveryAfterClaim(token, ref) || bindChangedAfterClaim(token, ref.Token.BindChangedAt) {
return true
}
if hasSnap && snap.RoleID != "" && snap.RoleID != ref.GetRoleID() {
return true
}
if strings.TrimSpace(ref.Token.BindChangedAt) == "" && historicalDeliveriesOnly(token, ref) {
return false
}
if hasSnap {
return false
}
return true
}
func historicalDeliveriesOnly(token *model.RockToken, ref *RockRefreshResponse) bool {
if ref == nil || len(ref.DeliveryList) == 0 {
return false
}
return !deliveryAfterClaim(token, ref)
}
func bindingSnapshot(token *model.RockToken) (BindingSnapshot, bool) {
if token == nil || strings.TrimSpace(token.InitialBoundSnapshot) == "" {
return BindingSnapshot{}, false
}
var snap BindingSnapshot
if err := json.Unmarshal([]byte(token.InitialBoundSnapshot), &snap); err != nil {
return BindingSnapshot{}, false
}
snap.RoleID = strings.TrimSpace(snap.RoleID)
snap.BindChangedAt = strings.TrimSpace(snap.BindChangedAt)
return snap, true
}
func rockClaimBaseline(token *model.RockToken) time.Time {
if token == nil {
return time.Time{}
}
if token.ClaimedAt != nil && !token.ClaimedAt.IsZero() {
return token.ClaimedAt.UTC()
}
return token.CreatedAt.UTC()
}
func parseRockTime(raw string) (time.Time, bool) {
raw = strings.TrimSpace(raw)
if raw == "" {
return time.Time{}, false
}
layouts := []string{
time.RFC3339Nano,
"2006-01-02T15:04:05.999999999",
"2006-01-02T15:04:05",
"2006-01-02 15:04:05",
}
for _, layout := range layouts {
if t, err := time.ParseInLocation(layout, raw, time.Local); err == nil {
return t.UTC(), true
}
}
return time.Time{}, false
}
func bindChangedAfterClaim(token *model.RockToken, changedAt string) bool {
changed, ok := parseRockTime(changedAt)
if !ok {
return false
}
baseline := rockClaimBaseline(token)
if baseline.IsZero() {
return true
}
return !changed.Before(baseline.Add(-time.Minute))
}
func deliveryAfterClaim(token *model.RockToken, ref *RockRefreshResponse) bool {
if ref == nil {
return false
}
baseline := rockClaimBaseline(token)
if baseline.IsZero() {
return false
}
for _, item := range ref.DeliveryList {
deliveredAt, ok := parseRockTime(item.Time)
if !ok {
continue
}
if !deliveredAt.Before(baseline.Add(-time.Minute)) {
return true
}
}
return false
}