Files
affiliate_dash/backend/internal/service/rock_snapshot.go
T

140 lines
4.1 KiB
Go

package service
import (
"encoding/json"
"strings"
"time"
"affiliate_dash/internal/model"
"affiliate_dash/internal/pkg/timeutil"
)
// 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
}
// rockUpstreamBound 判断上游「当前」是否已绑定。
//
// 只看本次实时返回的绑定信号(status / bind_changed_at / binding),
// 不用 bound_account_id 判断:上游可能保留上一次绑定的账号残留,
// 用它会导致刷新按钮把「未扫码」误判成已绑定,也无法把真实绑定同步回本地。
func rockUpstreamBound(ref *RockRefreshResponse) bool {
if ref == nil {
return false
}
switch strings.ToLower(strings.TrimSpace(ref.Token.Status)) {
case model.RockStatusBound, model.RockStatusRedeemed:
return true
}
if strings.TrimSpace(ref.Token.BindChangedAt) != "" {
return true
}
return ref.Binding.Bound || strings.TrimSpace(ref.Binding.RoleID) != ""
}
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()
}
// parseRockTime 解析上游返回的时间串。
// 上游(0666.pro)返回的是不带偏移的北京时间,必须以业务时区解析,
// 不能依赖 time.Local——否则进程时区一变,绑定时序判断就会整体偏移 8 小时。
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, timeutil.Location()); 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
}