209 lines
5.8 KiB
Go
209 lines
5.8 KiB
Go
package service
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"time"
|
|
|
|
"affiliate_dash/internal/model"
|
|
"affiliate_dash/internal/pkg/timeutil"
|
|
)
|
|
|
|
func rockBindingChangedAt(ref *RockRefreshResponse) string {
|
|
if ref == nil {
|
|
return ""
|
|
}
|
|
if value := strings.TrimSpace(ref.Token.BindChangedAt); value != "" {
|
|
return value
|
|
}
|
|
return strings.TrimSpace(ref.Binding.BindTime)
|
|
}
|
|
|
|
// rockRawBindingAfterBaseline 只判断上游是否返回了本轮绑定事件,不参考残留账号。
|
|
func rockRawBindingAfterBaseline(token *model.RockToken, ref *RockRefreshResponse) (time.Time, bool) {
|
|
raw := rockBindingChangedAt(ref)
|
|
if raw == "" {
|
|
return time.Time{}, false
|
|
}
|
|
changedAt, ok := parseRockTime(raw)
|
|
if !ok {
|
|
return time.Time{}, false
|
|
}
|
|
baseline := rockClaimBaseline(token)
|
|
if baseline.IsZero() {
|
|
return time.Time{}, false
|
|
}
|
|
return changedAt, !changedAt.Before(baseline.Add(-time.Minute))
|
|
}
|
|
|
|
// rockCurrentCycleBinding 判断上游绑定事件是否属于当前分配周期。
|
|
func rockCurrentCycleBinding(token *model.RockToken, ref *RockRefreshResponse) (bool, time.Time) {
|
|
if token == nil || ref == nil {
|
|
return false, time.Time{}
|
|
}
|
|
changedAt, current := rockRawBindingAfterBaseline(token, ref)
|
|
if !current {
|
|
return false, time.Time{}
|
|
}
|
|
if rockBindingMatchesInitialSnapshot(token, ref) {
|
|
return false, time.Time{}
|
|
}
|
|
return true, changedAt
|
|
}
|
|
|
|
func rockBindingMatchesInitialSnapshot(token *model.RockToken, ref *RockRefreshResponse) bool {
|
|
snap, ok := bindingSnapshot(token)
|
|
if !ok {
|
|
return false
|
|
}
|
|
return sameRockBindingEvent(snap.BindChangedAt, rockBindingChangedAt(ref))
|
|
}
|
|
|
|
// sameRockBindingEvent 兼容上游同一事件的不同时间格式,避免仅因字符串格式差异
|
|
// 把分配时保存的历史绑定重新识别成本轮绑定。
|
|
func sameRockBindingEvent(snapshotAt, upstreamAt string) bool {
|
|
snapshotAt = strings.TrimSpace(snapshotAt)
|
|
upstreamAt = strings.TrimSpace(upstreamAt)
|
|
if snapshotAt == "" || upstreamAt == "" {
|
|
return false
|
|
}
|
|
snapshotTime, snapshotOK := parseRockTime(snapshotAt)
|
|
upstreamTime, upstreamOK := parseRockTime(upstreamAt)
|
|
if snapshotOK && upstreamOK {
|
|
diff := snapshotTime.Sub(upstreamTime)
|
|
if diff < 0 {
|
|
diff = -diff
|
|
}
|
|
return diff <= time.Second
|
|
}
|
|
return snapshotAt == upstreamAt
|
|
}
|
|
|
|
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.UsingAt != nil && !token.UsingAt.IsZero() {
|
|
return token.UsingAt.UTC()
|
|
}
|
|
if token.ClaimedAt != nil && !token.ClaimedAt.IsZero() {
|
|
return token.ClaimedAt.UTC()
|
|
}
|
|
return time.Time{}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
type rockDeliveryEvidence struct {
|
|
Current bool
|
|
Ambiguous bool
|
|
At time.Time
|
|
FulfillID int64
|
|
Source string
|
|
}
|
|
|
|
// rockCurrentCycleDelivery 只认本轮 using_at 之后产生的兑换时间或发货流水。
|
|
// 明确早于本轮的历史记录忽略;状态为 redeemed 却没有可解析时间时按
|
|
// “已发货、数据待核对”保守处理,禁止释放。
|
|
func rockCurrentCycleDelivery(token *model.RockToken, ref *RockRefreshResponse) rockDeliveryEvidence {
|
|
if token == nil || ref == nil {
|
|
return rockDeliveryEvidence{}
|
|
}
|
|
baseline := rockClaimBaseline(token)
|
|
cutoff := baseline.Add(-time.Minute)
|
|
evidence := rockDeliveryEvidence{}
|
|
|
|
for _, item := range ref.DeliveryList {
|
|
at, ok := parseRockTime(item.Time)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if baseline.IsZero() {
|
|
if !evidence.Ambiguous || at.After(evidence.At) {
|
|
evidence.Ambiguous = true
|
|
evidence.At = at
|
|
evidence.FulfillID = item.FulfillID
|
|
evidence.Source = "delivery_list"
|
|
}
|
|
continue
|
|
}
|
|
if at.Before(cutoff) {
|
|
continue
|
|
}
|
|
if !evidence.Current || at.After(evidence.At) ||
|
|
(at.Equal(evidence.At) && item.FulfillID > evidence.FulfillID) {
|
|
evidence.Current = true
|
|
evidence.Ambiguous = false
|
|
evidence.At = at
|
|
evidence.FulfillID = item.FulfillID
|
|
evidence.Source = "delivery_list"
|
|
}
|
|
}
|
|
|
|
rawRedeemedAt := strings.TrimSpace(ref.Token.RedeemedAt)
|
|
if rawRedeemedAt != "" {
|
|
if at, ok := parseRockTime(rawRedeemedAt); ok {
|
|
if baseline.IsZero() {
|
|
evidence.Ambiguous = true
|
|
if evidence.At.IsZero() || at.After(evidence.At) {
|
|
evidence.At = at
|
|
evidence.Source = "redeemed_at"
|
|
}
|
|
} else if !at.Before(cutoff) &&
|
|
(!evidence.Current || at.After(evidence.At)) {
|
|
evidence.Current = true
|
|
evidence.Ambiguous = false
|
|
evidence.At = at
|
|
evidence.Source = "redeemed_at"
|
|
}
|
|
} else {
|
|
evidence.Ambiguous = true
|
|
if evidence.Source == "" {
|
|
evidence.Source = "redeemed_at"
|
|
}
|
|
}
|
|
}
|
|
|
|
if strings.EqualFold(strings.TrimSpace(ref.Token.Status), model.RockStatusRedeemed) &&
|
|
rawRedeemedAt == "" && !evidence.Current {
|
|
evidence.Ambiguous = true
|
|
if evidence.Source == "" {
|
|
evidence.Source = "status"
|
|
}
|
|
}
|
|
return evidence
|
|
}
|