227 lines
6.8 KiB
Go
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
|
|
}
|