198 lines
6.3 KiB
Go
198 lines
6.3 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"affiliate_dash/internal/model"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// importPackageDetail 把单个包详情幂等写入本地 Token,返回新增/更新/跳过数量。
|
|
func (s *RockService) importPackageDetail(ctx context.Context, detail *RockPackageDetailResponse, forceAvailableIDs map[string]struct{}) (int, int, int, error) {
|
|
if detail == nil || detail.Package.ID == 0 {
|
|
return 0, 0, 0, errors.New("上游未返回 package.id")
|
|
}
|
|
|
|
packageID := detail.Package.ID
|
|
shareCode := strings.TrimSpace(detail.Package.ShareCode)
|
|
inserted, updated, skipped := 0, 0, 0
|
|
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
for _, pt := range detail.Tokens {
|
|
if pt.ID <= 0 {
|
|
skipped++
|
|
continue
|
|
}
|
|
upstreamID := strconv.FormatInt(pt.ID, 10)
|
|
status := normalizeRockPackageStatus(pt.Status)
|
|
raw, marshalErr := json.Marshal(pt)
|
|
if marshalErr != nil {
|
|
raw = []byte(`{}`)
|
|
}
|
|
claimedAt := parseRockPackageTime(pt.CreatedAt)
|
|
boundAt := parseRockPackageTime(pt.BindChangedAt)
|
|
redeemedAt := parseRockPackageTime(pt.RedeemedAt)
|
|
|
|
var existing model.RockToken
|
|
findErr := tx.Unscoped().Where("upstream_token_id = ?", upstreamID).First(&existing).Error
|
|
if errors.Is(findErr, gorm.ErrRecordNotFound) {
|
|
insertStatus := status
|
|
if _, ok := forceAvailableIDs[upstreamID]; ok {
|
|
insertStatus = model.RockStatusAvailable
|
|
}
|
|
// 上游不提供“进入 using”的时间,用 Token 创建时间近似(与 017 回填口径一致)。
|
|
var usingAt *time.Time
|
|
if insertStatus == model.RockStatusUsing {
|
|
usingAt = claimedAt
|
|
if usingAt == nil {
|
|
now := time.Now()
|
|
usingAt = &now
|
|
}
|
|
}
|
|
rec := model.RockToken{
|
|
UpstreamTokenID: upstreamID,
|
|
Token: strings.TrimSpace(pt.Token),
|
|
RedeemCode: strings.TrimSpace(pt.RedeemCode),
|
|
Color: defaultValue(strings.TrimSpace(pt.Color), model.RockColorBlack),
|
|
Region: defaultValue(strings.TrimSpace(pt.Region), "Q/V可用"),
|
|
Status: insertStatus,
|
|
PackageID: &packageID,
|
|
PackageShareCode: shareCode,
|
|
ActualRoleName: strings.TrimSpace(pt.BoundAccount),
|
|
ActualRoleID: strings.TrimSpace(pt.BoundAccountID),
|
|
ActualChannel: strings.TrimSpace(pt.Region),
|
|
TargetSkin: strings.TrimSpace(pt.RedeemSkin),
|
|
TargetCurrency: defaultValue(strings.TrimSpace(pt.RedeemCurrency), "幸运币"),
|
|
TargetCurrencyQuantity: 0,
|
|
RawData: string(raw),
|
|
Source: "package",
|
|
ClaimedAt: claimedAt,
|
|
UsingAt: usingAt,
|
|
BoundAt: boundAt,
|
|
RedeemedAt: redeemedAt,
|
|
}
|
|
if err := tx.Create(&rec).Error; err != nil {
|
|
return err
|
|
}
|
|
inserted++
|
|
continue
|
|
}
|
|
if findErr != nil {
|
|
return findErr
|
|
}
|
|
// 已软删除的 Token 视为“用户主动清理过”,再次导入时不复活、不覆盖。
|
|
if existing.DeletedAt.Valid {
|
|
skipped++
|
|
continue
|
|
}
|
|
|
|
updates := map[string]interface{}{
|
|
"package_id": packageID,
|
|
"raw_data": string(raw),
|
|
}
|
|
if existing.PackageShareCode == "" && shareCode != "" {
|
|
updates["package_share_code"] = shareCode
|
|
}
|
|
// 已存在的本地 Token 只补空字段,避免覆盖手动采购/正在履约的本地状态。
|
|
if existing.Token == "" {
|
|
updates["token"] = strings.TrimSpace(pt.Token)
|
|
}
|
|
if existing.RedeemCode == "" {
|
|
updates["redeem_code"] = strings.TrimSpace(pt.RedeemCode)
|
|
}
|
|
if existing.Color == "" {
|
|
updates["color"] = defaultValue(strings.TrimSpace(pt.Color), model.RockColorBlack)
|
|
}
|
|
if existing.Region == "" {
|
|
updates["region"] = defaultValue(strings.TrimSpace(pt.Region), "Q/V可用")
|
|
}
|
|
if existing.Source == "" {
|
|
updates["source"] = "package"
|
|
}
|
|
|
|
// 打包导入且没有本地订单的记录:允许随上游刷新展示字段;
|
|
// 但手工释放为 available 的记录保持 available,避免全量导入把可售库存回退。
|
|
canRefreshUpstream := existing.Source == "package" &&
|
|
strings.TrimSpace(existing.AssignedOrderNo) == "" &&
|
|
existing.Status != model.RockStatusAvailable
|
|
if canRefreshUpstream {
|
|
updates["status"] = status
|
|
if status == model.RockStatusUsing && existing.UsingAt == nil {
|
|
if claimedAt != nil {
|
|
updates["using_at"] = claimedAt
|
|
} else {
|
|
updates["using_at"] = time.Now()
|
|
}
|
|
}
|
|
if strings.TrimSpace(pt.BoundAccount) != "" {
|
|
updates["actual_role_name"] = strings.TrimSpace(pt.BoundAccount)
|
|
}
|
|
if strings.TrimSpace(pt.BoundAccountID) != "" {
|
|
updates["actual_role_id"] = strings.TrimSpace(pt.BoundAccountID)
|
|
}
|
|
if strings.TrimSpace(pt.Region) != "" {
|
|
updates["actual_channel"] = strings.TrimSpace(pt.Region)
|
|
}
|
|
if strings.TrimSpace(pt.RedeemSkin) != "" {
|
|
updates["target_skin"] = strings.TrimSpace(pt.RedeemSkin)
|
|
}
|
|
if strings.TrimSpace(pt.RedeemCurrency) != "" {
|
|
updates["target_currency"] = strings.TrimSpace(pt.RedeemCurrency)
|
|
}
|
|
if claimedAt != nil {
|
|
updates["claimed_at"] = claimedAt
|
|
}
|
|
if boundAt != nil {
|
|
updates["bound_at"] = boundAt
|
|
}
|
|
if redeemedAt != nil {
|
|
updates["redeemed_at"] = redeemedAt
|
|
}
|
|
}
|
|
if err := tx.Model(&existing).Updates(updates).Error; err != nil {
|
|
return err
|
|
}
|
|
updated++
|
|
}
|
|
return nil
|
|
})
|
|
return inserted, updated, skipped, err
|
|
}
|
|
|
|
func normalizeRockPackageStatus(status string) string {
|
|
switch strings.TrimSpace(status) {
|
|
case model.RockStatusAvailable:
|
|
return model.RockStatusAvailable
|
|
case model.RockStatusUsing:
|
|
return model.RockStatusUsing
|
|
case model.RockStatusBound:
|
|
return model.RockStatusBound
|
|
case model.RockStatusRedeemed:
|
|
return model.RockStatusRedeemed
|
|
case model.RockStatusMismatch:
|
|
return model.RockStatusMismatch
|
|
default:
|
|
// 未知上游状态默认落 abnormal,避免被错误分配为可用库存。
|
|
return model.RockStatusAbnormal
|
|
}
|
|
}
|
|
|
|
func parseRockPackageTime(raw string) *time.Time {
|
|
if t, ok := parseRockTime(raw); ok {
|
|
return &t
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func defaultValue(value, fallback string) string {
|
|
if strings.TrimSpace(value) == "" {
|
|
return fallback
|
|
}
|
|
return value
|
|
}
|