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

304 lines
10 KiB
Go

package service
import (
"context"
"errors"
"fmt"
"strings"
"affiliate_dash/internal/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
const (
rockTokenBatchDeleteMax = 500
rockTokenDeleteAllMax = 5000
)
var rockTokenDangerousStatuses = map[string]bool{
model.RockStatusUsing: true,
model.RockStatusBound: true,
model.RockStatusMismatch: true,
}
// RockTokenDeleteStatusCount 是删除预览中的状态分布。
type RockTokenDeleteStatusCount struct {
Status string `json:"status"`
Count int64 `json:"count"`
}
// RockTokenDeletePreview 是删除前的风险预览。
type RockTokenDeletePreview struct {
Total int64 `json:"total"`
Dangerous int64 `json:"dangerous"`
Available int64 `json:"available"`
RequiresForce bool `json:"requires_force"`
StatusCounts []RockTokenDeleteStatusCount `json:"status_counts"`
DangerousItems []RockTokenDeleteItem `json:"dangerous_items"`
}
// RockTokenDeleteItem 是删除风险提示中的单条记录。
type RockTokenDeleteItem struct {
ID uint `json:"id"`
Status string `json:"status"`
AssignedOrderNo string `json:"assigned_order_no,omitempty"`
}
// RockTokenDeleteResult 是软删除结果。
type RockTokenDeleteResult struct {
Requested int `json:"requested"`
Deleted int `json:"deleted"`
Forced bool `json:"forced"`
StatusCounts []RockTokenDeleteStatusCount `json:"status_counts"`
}
// RockTokenForceRequiredError 表示本次删除包含使用中/已绑定/绑定错误 Token,需要强制确认。
type RockTokenForceRequiredError struct {
Dangerous int64 `json:"dangerous"`
StatusCounts []RockTokenDeleteStatusCount `json:"status_counts"`
DangerousItems []RockTokenDeleteItem `json:"dangerous_items"`
}
func (e *RockTokenForceRequiredError) Error() string {
return fmt.Sprintf("本次操作包含 %d 条使用中/已绑定/绑定错误的 Token,必须二次确认后强制删除", e.Dangerous)
}
// RockTokenDeleteConflictError 表示预览后数据已变化。
type RockTokenDeleteConflictError struct {
Message string
}
func (e *RockTokenDeleteConflictError) Error() string {
if strings.TrimSpace(e.Message) != "" {
return e.Message
}
return "删除条件对应的数据已变化,请刷新后重试"
}
func isKnownRockTokenStatus(status string) bool {
switch status {
case "", model.RockStatusAvailable, model.RockStatusUsing, model.RockStatusBound,
model.RockStatusRedeemed, model.RockStatusMismatch, model.RockStatusAbnormal:
return true
default:
return false
}
}
// applyRockTokenListFilters 与 Token 列表使用同一套筛选条件,避免“看到多少、删多少”不一致。
func applyRockTokenListFilters(db *gorm.DB, status, search string) *gorm.DB {
if value := strings.TrimSpace(status); value != "" {
db = db.Where("status = ?", value)
}
if value := strings.TrimSpace(search); value != "" {
term := "%" + value + "%"
db = db.Where("CAST(id AS TEXT) = ? OR upstream_token_id = ? OR upstream_token_id LIKE ? OR assigned_order_no LIKE ? OR expected_uid LIKE ? OR actual_role_name LIKE ? OR actual_role_id LIKE ? OR redeem_code LIKE ? OR remark LIKE ? OR CAST(package_id AS TEXT) LIKE ? OR token LIKE ? OR assigned_order_no IN (SELECT order_no FROM fulfillment_orders WHERE client_order_no LIKE ?)",
value, value, term, term, term, term, term, term, term, term, term, term)
}
return db
}
func rockTokenStatusCountsFromRows(rows []model.RockToken) []RockTokenDeleteStatusCount {
counts := map[string]int64{}
for _, row := range rows {
counts[row.Status]++
}
order := []string{
model.RockStatusAvailable,
model.RockStatusUsing,
model.RockStatusBound,
model.RockStatusRedeemed,
model.RockStatusMismatch,
model.RockStatusAbnormal,
}
result := make([]RockTokenDeleteStatusCount, 0, len(counts))
for _, status := range order {
if count := counts[status]; count > 0 {
result = append(result, RockTokenDeleteStatusCount{Status: status, Count: count})
}
}
return result
}
func rockTokenDangerousCount(rows []model.RockToken) int64 {
var total int64
for _, row := range rows {
if rockTokenDangerousStatuses[row.Status] {
total++
}
}
return total
}
func rockTokenDangerousItems(rows []model.RockToken, limit int) []RockTokenDeleteItem {
items := make([]RockTokenDeleteItem, 0, limit)
for _, row := range rows {
if !rockTokenDangerousStatuses[row.Status] {
continue
}
items = append(items, RockTokenDeleteItem{
ID: row.ID,
Status: row.Status,
AssignedOrderNo: row.AssignedOrderNo,
})
if len(items) >= limit {
break
}
}
return items
}
// PreviewRockTokenDelete 返回筛选条件下的删除风险预览,不修改任何数据。
func (s *RockService) PreviewRockTokenDelete(ctx context.Context, status, search string) (*RockTokenDeletePreview, error) {
status = strings.TrimSpace(status)
if !isKnownRockTokenStatus(status) {
return nil, errors.New("无效的 Token 状态筛选")
}
var rows []model.RockToken
query := applyRockTokenListFilters(s.db.WithContext(ctx).Model(&model.RockToken{}), status, search)
if err := query.Order("id DESC").Find(&rows).Error; err != nil {
return nil, err
}
var available int64
for _, row := range rows {
if row.Status == model.RockStatusAvailable {
available++
}
}
dangerous := rockTokenDangerousCount(rows)
return &RockTokenDeletePreview{
Total: int64(len(rows)),
Dangerous: dangerous,
Available: available,
RequiresForce: dangerous > 0,
StatusCounts: rockTokenStatusCountsFromRows(rows),
DangerousItems: rockTokenDangerousItems(rows, 10),
}, nil
}
// DeleteRockTokens 软删除指定 Token;包含危险状态时必须 force=true。
func (s *RockService) DeleteRockTokens(ctx context.Context, ids []uint, force bool, actorUserID uint) (*RockTokenDeleteResult, 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) > rockTokenBatchDeleteMax {
return nil, fmt.Errorf("单次最多删除 %d 条 Token", rockTokenBatchDeleteMax)
}
result := &RockTokenDeleteResult{Requested: len(unique), Forced: force}
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var rows []model.RockToken
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("id IN ?", unique).
Order("id ASC").
Find(&rows).Error; err != nil {
return err
}
if len(rows) != len(unique) {
return &RockTokenDeleteConflictError{Message: "部分 Token 不存在或已被删除,请刷新列表后重试"}
}
result.StatusCounts = rockTokenStatusCountsFromRows(rows)
dangerous := rockTokenDangerousCount(rows)
if dangerous > 0 && !force {
return &RockTokenForceRequiredError{
Dangerous: dangerous,
StatusCounts: result.StatusCounts,
DangerousItems: rockTokenDangerousItems(rows, 10),
}
}
deleteResult := tx.Where("id IN ?", unique).Delete(&model.RockToken{})
if deleteResult.Error != nil {
return deleteResult.Error
}
result.Deleted = int(deleteResult.RowsAffected)
metadata := map[string]interface{}{
"requested": result.Requested,
"deleted": result.Deleted,
"forced": force,
"status_counts": result.StatusCounts,
"ids": unique,
}
return writeAudit(tx, nil, &actorUserID, nil, "rock_token.batch_delete", "rock_token",
fmt.Sprintf("batch:%d", len(unique)), metadata)
})
if err != nil {
return nil, err
}
return result, nil
}
// DeleteAllRockTokens 按当前筛选条件软删除全部 Token;包含危险状态时必须 force=true。
func (s *RockService) DeleteAllRockTokens(ctx context.Context, status, search string, expectedCount int64, force bool, actorUserID uint) (*RockTokenDeleteResult, error) {
status = strings.TrimSpace(status)
if !isKnownRockTokenStatus(status) {
return nil, errors.New("无效的 Token 状态筛选")
}
result := &RockTokenDeleteResult{Forced: force}
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var rows []model.RockToken
query := applyRockTokenListFilters(tx.Model(&model.RockToken{}), status, search)
if err := query.Clauses(clause.Locking{Strength: "UPDATE"}).Order("id ASC").Find(&rows).Error; err != nil {
return err
}
if len(rows) > rockTokenDeleteAllMax {
return fmt.Errorf("单次最多删除 %d 条 Token,请缩小筛选范围", rockTokenDeleteAllMax)
}
if expectedCount > 0 && int64(len(rows)) != expectedCount {
return &RockTokenDeleteConflictError{Message: fmt.Sprintf("筛选结果已变化(预览 %d 条,当前 %d 条),请刷新后重试", expectedCount, len(rows))}
}
if len(rows) == 0 {
return errors.New("当前筛选条件下没有可删除的 Token")
}
result.Requested = len(rows)
result.StatusCounts = rockTokenStatusCountsFromRows(rows)
dangerous := rockTokenDangerousCount(rows)
if dangerous > 0 && !force {
return &RockTokenForceRequiredError{
Dangerous: dangerous,
StatusCounts: result.StatusCounts,
DangerousItems: rockTokenDangerousItems(rows, 10),
}
}
ids := make([]uint, 0, len(rows))
for _, row := range rows {
ids = append(ids, row.ID)
}
deleteResult := tx.Where("id IN ?", ids).Delete(&model.RockToken{})
if deleteResult.Error != nil {
return deleteResult.Error
}
result.Deleted = int(deleteResult.RowsAffected)
metadata := map[string]interface{}{
"requested": result.Requested,
"deleted": result.Deleted,
"forced": force,
"status_filter": status,
"search": strings.TrimSpace(search),
"status_counts": result.StatusCounts,
}
return writeAudit(tx, nil, &actorUserID, nil, "rock_token.delete_all", "rock_token",
fmt.Sprintf("filter:%s:%s", status, strings.TrimSpace(search)), metadata)
})
if err != nil {
return nil, err
}
return result, nil
}