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

227 lines
6.8 KiB
Go

package service
import (
"context"
"errors"
"fmt"
"strings"
"time"
"affiliate_dash/internal/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
const rockTokenBatchRestoreMax = 500
// 恢复被跳过的原因。
const (
RockRestoreSkipNotDeleted = "not_deleted"
RockRestoreSkipDuplicateUpstream = "duplicate_upstream"
RockRestoreSkipOrderConflict = "order_conflict"
)
// RockTokenRestoreSkipItem 表示恢复时被跳过的记录及原因。
type RockTokenRestoreSkipItem struct {
ID uint `json:"id"`
Status string `json:"status,omitempty"`
Reason string `json:"reason"`
}
// RockTokenRestoreResult 是批量恢复结果。
type RockTokenRestoreResult struct {
Requested int `json:"requested"`
Restored int `json:"restored"`
Skipped int `json:"skipped"`
StatusCounts []RockTokenDeleteStatusCount `json:"status_counts"`
SkippedItems []RockTokenRestoreSkipItem `json:"skipped_items"`
}
// ListDeletedRockTokens 分页查询回收站(deleted_at 非空)中的 Token。
func (s *RockService) ListDeletedRockTokens(ctx context.Context, page, pageSize int, status, search string) ([]model.RockToken, int64, error) {
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 20
}
status = strings.TrimSpace(status)
if !isKnownRockTokenStatus(status) {
return nil, 0, errors.New("无效的 Token 状态筛选")
}
db := applyRockTokenListFilters(
s.db.WithContext(ctx).Unscoped().Model(&model.RockToken{}).Where("deleted_at IS NOT NULL"),
status, search,
)
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var tokens []model.RockToken
if err := db.Order("deleted_at DESC, id DESC").
Offset((page - 1) * pageSize).
Limit(pageSize).
Find(&tokens).Error; err != nil {
return nil, 0, err
}
for i := range tokens {
tokens[i].TokenCode = tokens[i].Token
if tokens[i].DeletedAt.Valid {
tokens[i].DeletedAtText = tokens[i].DeletedAt.Time.Format(time.RFC3339)
}
}
return tokens, total, nil
}
// RestoreRockTokens 恢复已软删除的 Token。
// 同一上游编号已在用、或同一订单已有在用 Token 时跳过该条,其余正常恢复(部分成功)。
func (s *RockService) RestoreRockTokens(ctx context.Context, ids []uint, actorUserID uint) (*RockTokenRestoreResult, error) {
unique := make([]uint, 0, len(ids))
seen := make(map[uint]struct{}, len(ids))
for _, id := range ids {
if id == 0 {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
unique = append(unique, id)
}
if len(unique) == 0 {
return nil, errors.New("请选择要恢复的 Token")
}
if len(unique) > rockTokenBatchRestoreMax {
return nil, fmt.Errorf("单次最多恢复 %d 条 Token", rockTokenBatchRestoreMax)
}
result := &RockTokenRestoreResult{Requested: len(unique), SkippedItems: []RockTokenRestoreSkipItem{}}
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var rows []model.RockToken
if err := tx.Unscoped().Clauses(clause.Locking{Strength: "UPDATE"}).
Where("id IN ?", unique).
Order("id ASC").
Find(&rows).Error; err != nil {
return err
}
found := make(map[uint]model.RockToken, len(rows))
for _, row := range rows {
found[row.ID] = row
}
// 先按请求顺序过滤出仍在回收站里的记录。
candidates := make([]model.RockToken, 0, len(unique))
for _, id := range unique {
row, ok := found[id]
if !ok || !row.DeletedAt.Valid {
reason := RockRestoreSkipNotDeleted
item := RockTokenRestoreSkipItem{ID: id, Reason: reason}
if ok {
item.Status = row.Status
}
result.SkippedItems = append(result.SkippedItems, item)
continue
}
candidates = append(candidates, row)
}
// 冲突 1:同一 upstream_token_id 已存在未删除记录(部分唯一索引会拒绝恢复)。
upstreamIDs := make([]string, 0, len(candidates))
for _, row := range candidates {
if value := strings.TrimSpace(row.UpstreamTokenID); value != "" {
upstreamIDs = append(upstreamIDs, value)
}
}
activeUpstream := make(map[string]struct{}, len(upstreamIDs))
if len(upstreamIDs) > 0 {
var values []string
if err := tx.Model(&model.RockToken{}).
Where("upstream_token_id IN ?", upstreamIDs).
Pluck("upstream_token_id", &values).Error; err != nil {
return err
}
for _, value := range values {
activeUpstream[value] = struct{}{}
}
}
// 冲突 2:同一订单已经有其它在用/已发货 Token,恢复会造成一单两号。
orderNos := make([]string, 0, len(candidates))
for _, row := range candidates {
if value := strings.TrimSpace(row.AssignedOrderNo); value != "" {
orderNos = append(orderNos, value)
}
}
activeOrders := make(map[string]struct{}, len(orderNos))
if len(orderNos) > 0 {
var values []string
if err := tx.Model(&model.RockToken{}).
Where("assigned_order_no IN ?", orderNos).
Pluck("assigned_order_no", &values).Error; err != nil {
return err
}
for _, value := range values {
activeOrders[value] = struct{}{}
}
}
restorable := make([]model.RockToken, 0, len(candidates))
for _, row := range candidates {
if upstream := strings.TrimSpace(row.UpstreamTokenID); upstream != "" {
if _, conflict := activeUpstream[upstream]; conflict {
result.SkippedItems = append(result.SkippedItems, RockTokenRestoreSkipItem{
ID: row.ID, Status: row.Status, Reason: RockRestoreSkipDuplicateUpstream,
})
continue
}
}
if orderNo := strings.TrimSpace(row.AssignedOrderNo); orderNo != "" {
if _, conflict := activeOrders[orderNo]; conflict {
result.SkippedItems = append(result.SkippedItems, RockTokenRestoreSkipItem{
ID: row.ID, Status: row.Status, Reason: RockRestoreSkipOrderConflict,
})
continue
}
}
restorable = append(restorable, row)
}
if len(restorable) > 0 {
restoreIDs := make([]uint, 0, len(restorable))
for _, row := range restorable {
restoreIDs = append(restoreIDs, row.ID)
}
if err := tx.Unscoped().Model(&model.RockToken{}).
Where("id IN ?", restoreIDs).
Updates(map[string]interface{}{"deleted_at": nil}).Error; err != nil {
return err
}
result.Restored = len(restorable)
result.StatusCounts = rockTokenStatusCountsFromRows(restorable)
metadata := map[string]interface{}{
"requested": result.Requested,
"restored": result.Restored,
"skipped": len(result.SkippedItems),
"status_counts": result.StatusCounts,
"ids": restoreIDs,
"skipped_items": result.SkippedItems,
}
if err := writeAudit(tx, nil, &actorUserID, nil, "rock_token.batch_restore", "rock_token",
fmt.Sprintf("batch:%d", len(restoreIDs)), metadata); err != nil {
return err
}
}
result.Skipped = len(result.SkippedItems)
return nil
})
if err != nil {
return nil, err
}
return result, nil
}