304 lines
10 KiB
Go
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
|
|
}
|