Files
3m-ui/backend/internal/user/service_batch.go
T
Z User 4b00a1268a fix(functional): batch autofill, soft-delete name reclaim, hasCredState test, sub_token error, batch reset traffic
Deep functional audit of node/user/subscription/traffic features
found 5 bugs. All fixed. All tests pass (including the previously
failing hasCredState test).
== CRITICAL ==
F1: BatchCreate skips AutofillListenerDefaults
Batch-created listeners had no auto-generated UUIDs, passwords,
REALITY keys, SS ciphers, snell PSKs, or sudoku keys → mihomo
rejected the config → entire batch failed.
Fix: call AutofillListenerDefaults before ValidateModel in loop.
F2: BatchCreate doesn't reclaim soft-deleted names
Inline Count check used GORM default scope (excludes soft-deleted)
but UNIQUE index covers them → DB constraint error on name clash.
Fix: use ensureUniqueName which renames soft-deleted rows.
F3: Failing test contradicts hasCredState fallback fix
Test asserted no fallback when hasCredState=true + empty creds.
But we intentionally removed the guard to fix 'unset fields: users'.
Fix: updated test to assert config users ARE included as fallback.
== HIGH ==
F4: sub_token generation ignores error
subTok, _ := randomHex(16) — empty sub_token on rand.Read failure.
Fix: check error, return on failure.
F5: BatchResetTraffic doesn't trigger config regeneration
Over-quota users stayed blocked for ~10s after traffic reset.
Fix: added notifyCredentialsChanged() after reset.
Verification: go build/vet/test all PASS. gofmt clean.
fix(functional): batch autofill, soft-delete name reclaim, hasCredState test, sub_token error, batch reset traffic
Deep functional audit of node/user/subscription/traffic features
found 5 bugs. All fixed. All tests pass (including the previously
failing hasCredState test).
== CRITICAL ==
F1: BatchCreate skips AutofillListenerDefaults
Batch-created listeners had no auto-generated UUIDs, passwords,
REALITY keys, SS ciphers, snell PSKs, or sudoku keys → mihomo
rejected the config → entire batch failed.
Fix: call AutofillListenerDefaults before ValidateModel in loop.
F2: BatchCreate doesn't reclaim soft-deleted names
Inline Count check used GORM default scope (excludes soft-deleted)
but UNIQUE index covers them → DB constraint error on name clash.
Fix: use ensureUniqueName which renames soft-deleted rows.
F3: Failing test contradicts hasCredState fallback fix
Test asserted no fallback when hasCredState=true + empty creds.
But we intentionally removed the guard to fix 'unset fields: users'.
Fix: updated test to assert config users ARE included as fallback.
== HIGH ==
F4: sub_token generation ignores error
subTok, _ := randomHex(16) — empty sub_token on rand.Read failure.
Fix: check error, return on failure.
F5: BatchResetTraffic doesn't trigger config regeneration
Over-quota users stayed blocked for ~10s after traffic reset.
Fix: added notifyCredentialsChanged() after reset.
Verification: go build/vet/test all PASS. gofmt clean.
fix(functional): batch autofill, soft-delete name reclaim, hasCredState test, sub_token error, batch reset traffic
Deep functional audit of node/user/subscription/traffic features
found 5 bugs. All fixed. All tests pass (including the previously
failing hasCredState test).
== CRITICAL ==
F1: BatchCreate skips AutofillListenerDefaults
Batch-created listeners had no auto-generated UUIDs, passwords,
REALITY keys, SS ciphers, snell PSKs, or sudoku keys → mihomo
rejected the config → entire batch failed.
Fix: call AutofillListenerDefaults before ValidateModel in loop.
F2: BatchCreate doesn't reclaim soft-deleted names
Inline Count check used GORM default scope (excludes soft-deleted)
but UNIQUE index covers them → DB constraint error on name clash.
Fix: use ensureUniqueName which renames soft-deleted rows.
F3: Failing test contradicts hasCredState fallback fix
Test asserted no fallback when hasCredState=true + empty creds.
But we intentionally removed the guard to fix 'unset fields: users'.
Fix: updated test to assert config users ARE included as fallback.
== HIGH ==
F4: sub_token generation ignores error
subTok, _ := randomHex(16) — empty sub_token on rand.Read failure.
Fix: check error, return on failure.
F5: BatchResetTraffic doesn't trigger config regeneration
Over-quota users stayed blocked for ~10s after traffic reset.
Fix: added notifyCredentialsChanged() after reset.
Verification: go build/vet/test all PASS. gofmt clean.
fix(functional): batch autofill, soft-delete name reclaim, hasCredState test, sub_token error, batch reset traffic
Deep functional audit of node/user/subscription/traffic features
found 5 bugs. All fixed. All tests pass (including the previously
failing hasCredState test).
== CRITICAL ==
F1: BatchCreate skips AutofillListenerDefaults
Batch-created listeners had no auto-generated UUIDs, passwords,
REALITY keys, SS ciphers, snell PSKs, or sudoku keys → mihomo
rejected the config → entire batch failed.
Fix: call AutofillListenerDefaults before ValidateModel in loop.
F2: BatchCreate doesn't reclaim soft-deleted names
Inline Count check used GORM default scope (excludes soft-deleted)
but UNIQUE index covers them → DB constraint error on name clash.
Fix: use ensureUniqueName which renames soft-deleted rows.
F3: Failing test contradicts hasCredState fallback fix
Test asserted no fallback when hasCredState=true + empty creds.
But we intentionally removed the guard to fix 'unset fields: users'.
Fix: updated test to assert config users ARE included as fallback.
== HIGH ==
F4: sub_token generation ignores error
subTok, _ := randomHex(16) — empty sub_token on rand.Read failure.
Fix: check error, return on failure.
F5: BatchResetTraffic doesn't trigger config regeneration
Over-quota users stayed blocked for ~10s after traffic reset.
Fix: added notifyCredentialsChanged() after reset.
Verification: go build/vet/test all PASS. gofmt clean.
fix(functional): batch autofill, soft-delete name reclaim, hasCredState test, sub_token error, batch reset traffic
Deep functional audit of node/user/subscription/traffic features
found 5 bugs. All fixed. All tests pass (including the previously
failing hasCredState test).
== CRITICAL ==
F1: BatchCreate skips AutofillListenerDefaults
Batch-created listeners had no auto-generated UUIDs, passwords,
REALITY keys, SS ciphers, snell PSKs, or sudoku keys → mihomo
rejected the config → entire batch failed.
Fix: call AutofillListenerDefaults before ValidateModel in loop.
F2: BatchCreate doesn't reclaim soft-deleted names
Inline Count check used GORM default scope (excludes soft-deleted)
but UNIQUE index covers them → DB constraint error on name clash.
Fix: use ensureUniqueName which renames soft-deleted rows.
F3: Failing test contradicts hasCredState fallback fix
Test asserted no fallback when hasCredState=true + empty creds.
But we intentionally removed the guard to fix 'unset fields: users'.
Fix: updated test to assert config users ARE included as fallback.
== HIGH ==
F4: sub_token generation ignores error
subTok, _ := randomHex(16) — empty sub_token on rand.Read failure.
Fix: check error, return on failure.
F5: BatchResetTraffic doesn't trigger config regeneration
Over-quota users stayed blocked for ~10s after traffic reset.
Fix: added notifyCredentialsChanged() after reset.
Verification: go build/vet/test all PASS. gofmt clean.
fix(functional): batch autofill, soft-delete name reclaim, hasCredState test, sub_token error, batch reset traffic
Deep functional audit of node/user/subscription/traffic features
found 5 bugs. All fixed. All tests pass (including the previously
failing hasCredState test).
== CRITICAL ==
F1: BatchCreate skips AutofillListenerDefaults
Batch-created listeners had no auto-generated UUIDs, passwords,
REALITY keys, SS ciphers, snell PSKs, or sudoku keys → mihomo
rejected the config → entire batch failed.
Fix: call AutofillListenerDefaults before ValidateModel in loop.
F2: BatchCreate doesn't reclaim soft-deleted names
Inline Count check used GORM default scope (excludes soft-deleted)
but UNIQUE index covers them → DB constraint error on name clash.
Fix: use ensureUniqueName which renames soft-deleted rows.
F3: Failing test contradicts hasCredState fallback fix
Test asserted no fallback when hasCredState=true + empty creds.
But we intentionally removed the guard to fix 'unset fields: users'.
Fix: updated test to assert config users ARE included as fallback.
== HIGH ==
F4: sub_token generation ignores error
subTok, _ := randomHex(16) — empty sub_token on rand.Read failure.
Fix: check error, return on failure.
F5: BatchResetTraffic doesn't trigger config regeneration
Over-quota users stayed blocked for ~10s after traffic reset.
Fix: added notifyCredentialsChanged() after reset.
Verification: go build/vet/test all PASS. gofmt clean.
fix(functional): batch autofill, soft-delete name reclaim, hasCredState test, sub_token error, batch reset traffic
Deep functional audit of node/user/subscription/traffic features
found 5 bugs. All fixed. All tests pass (including the previously
failing hasCredState test).
== CRITICAL ==
F1: BatchCreate skips AutofillListenerDefaults
Batch-created listeners had no auto-generated UUIDs, passwords,
REALITY keys, SS ciphers, snell PSKs, or sudoku keys → mihomo
rejected the config → entire batch failed.
Fix: call AutofillListenerDefaults before ValidateModel in loop.
F2: BatchCreate doesn't reclaim soft-deleted names
Inline Count check used GORM default scope (excludes soft-deleted)
but UNIQUE index covers them → DB constraint error on name clash.
Fix: use ensureUniqueName which renames soft-deleted rows.
F3: Failing test contradicts hasCredState fallback fix
Test asserted no fallback when hasCredState=true + empty creds.
But we intentionally removed the guard to fix 'unset fields: users'.
Fix: updated test to assert config users ARE included as fallback.
== HIGH ==
F4: sub_token generation ignores error
subTok, _ := randomHex(16) — empty sub_token on rand.Read failure.
Fix: check error, return on failure.
F5: BatchResetTraffic doesn't trigger config regeneration
Over-quota users stayed blocked for ~10s after traffic reset.
Fix: added notifyCredentialsChanged() after reset.
Verification: go build/vet/test all PASS. gofmt clean.
fix(functional): batch autofill, soft-delete name reclaim, hasCredState test, sub_token error, batch reset traffic
Deep functional audit of node/user/subscription/traffic features
found 5 bugs. All fixed. All tests pass (including the previously
failing hasCredState test).
== CRITICAL ==
F1: BatchCreate skips AutofillListenerDefaults
Batch-created listeners had no auto-generated UUIDs, passwords,
REALITY keys, SS ciphers, snell PSKs, or sudoku keys → mihomo
rejected the config → entire batch failed.
Fix: call AutofillListenerDefaults before ValidateModel in loop.
F2: BatchCreate doesn't reclaim soft-deleted names
Inline Count check used GORM default scope (excludes soft-deleted)
but UNIQUE index covers them → DB constraint error on name clash.
Fix: use ensureUniqueName which renames soft-deleted rows.
F3: Failing test contradicts hasCredState fallback fix
Test asserted no fallback when hasCredState=true + empty creds.
But we intentionally removed the guard to fix 'unset fields: users'.
Fix: updated test to assert config users ARE included as fallback.
== HIGH ==
F4: sub_token generation ignores error
subTok, _ := randomHex(16) — empty sub_token on rand.Read failure.
Fix: check error, return on failure.
F5: BatchResetTraffic doesn't trigger config regeneration
Over-quota users stayed blocked for ~10s after traffic reset.
Fix: added notifyCredentialsChanged() after reset.
Verification: go build/vet/test all PASS. gofmt clean.
fix(functional): batch autofill, soft-delete name reclaim, hasCredState test, sub_token error, batch reset traffic
Deep functional audit of node/user/subscription/traffic features
found 5 bugs. All fixed. All tests pass (including the previously
failing hasCredState test).
== CRITICAL ==
F1: BatchCreate skips AutofillListenerDefaults
Batch-created listeners had no auto-generated UUIDs, passwords,
REALITY keys, SS ciphers, snell PSKs, or sudoku keys → mihomo
rejected the config → entire batch failed.
Fix: call AutofillListenerDefaults before ValidateModel in loop.
F2: BatchCreate doesn't reclaim soft-deleted names
Inline Count check used GORM default scope (excludes soft-deleted)
but UNIQUE index covers them → DB constraint error on name clash.
Fix: use ensureUniqueName which renames soft-deleted rows.
F3: Failing test contradicts hasCredState fallback fix
Test asserted no fallback when hasCredState=true + empty creds.
But we intentionally removed the guard to fix 'unset fields: users'.
Fix: updated test to assert config users ARE included as fallback.
== HIGH ==
F4: sub_token generation ignores error
subTok, _ := randomHex(16) — empty sub_token on rand.Read failure.
Fix: check error, return on failure.
F5: BatchResetTraffic doesn't trigger config regeneration
Over-quota users stayed blocked for ~10s after traffic reset.
Fix: added notifyCredentialsChanged() after reset.
Verification: go build/vet/test all PASS. gofmt clean.
fix(functional): batch autofill, soft-delete name reclaim, hasCredState test, sub_token error, batch reset traffic
Deep functional audit of node/user/subscription/traffic features
found 5 bugs. All fixed. All tests pass (including the previously
failing hasCredState test).
== CRITICAL ==
F1: BatchCreate skips AutofillListenerDefaults
Batch-created listeners had no auto-generated UUIDs, passwords,
REALITY keys, SS ciphers, snell PSKs, or sudoku keys → mihomo
rejected the config → entire batch failed.
Fix: call AutofillListenerDefaults before ValidateModel in loop.
F2: BatchCreate doesn't reclaim soft-deleted names
Inline Count check used GORM default scope (excludes soft-deleted)
but UNIQUE index covers them → DB constraint error on name clash.
Fix: use ensureUniqueName which renames soft-deleted rows.
F3: Failing test contradicts hasCredState fallback fix
Test asserted no fallback when hasCredState=true + empty creds.
But we intentionally removed the guard to fix 'unset fields: users'.
Fix: updated test to assert config users ARE included as fallback.
== HIGH ==
F4: sub_token generation ignores error
subTok, _ := randomHex(16) — empty sub_token on rand.Read failure.
Fix: check error, return on failure.
F5: BatchResetTraffic doesn't trigger config regeneration
Over-quota users stayed blocked for ~10s after traffic reset.
Fix: added notifyCredentialsChanged() after reset.
Verification: go build/vet/test all PASS. gofmt clean.
fix(functional): batch autofill, soft-delete name reclaim, hasCredState test, sub_token error, batch reset traffic
Deep functional audit of node/user/subscription/traffic features
found 5 bugs. All fixed. All tests pass (including the previously
failing hasCredState test).
== CRITICAL ==
F1: BatchCreate skips AutofillListenerDefaults
Batch-created listeners had no auto-generated UUIDs, passwords,
REALITY keys, SS ciphers, snell PSKs, or sudoku keys → mihomo
rejected the config → entire batch failed.
Fix: call AutofillListenerDefaults before ValidateModel in loop.
F2: BatchCreate doesn't reclaim soft-deleted names
Inline Count check used GORM default scope (excludes soft-deleted)
but UNIQUE index covers them → DB constraint error on name clash.
Fix: use ensureUniqueName which renames soft-deleted rows.
F3: Failing test contradicts hasCredState fallback fix
Test asserted no fallback when hasCredState=true + empty creds.
But we intentionally removed the guard to fix 'unset fields: users'.
Fix: updated test to assert config users ARE included as fallback.
== HIGH ==
F4: sub_token generation ignores error
subTok, _ := randomHex(16) — empty sub_token on rand.Read failure.
Fix: check error, return on failure.
F5: BatchResetTraffic doesn't trigger config regeneration
Over-quota users stayed blocked for ~10s after traffic reset.
Fix: added notifyCredentialsChanged() after reset.
Verification: go build/vet/test all PASS. gofmt clean.
fix(functional): batch autofill, soft-delete name reclaim, hasCredState test, sub_token error, batch reset traffic
Deep functional audit of node/user/subscription/traffic features
found 5 bugs. All fixed. All tests pass (including the previously
failing hasCredState test).
== CRITICAL ==
F1: BatchCreate skips AutofillListenerDefaults
Batch-created listeners had no auto-generated UUIDs, passwords,
REALITY keys, SS ciphers, snell PSKs, or sudoku keys → mihomo
rejected the config → entire batch failed.
Fix: call AutofillListenerDefaults before ValidateModel in loop.
F2: BatchCreate doesn't reclaim soft-deleted names
Inline Count check used GORM default scope (excludes soft-deleted)
but UNIQUE index covers them → DB constraint error on name clash.
Fix: use ensureUniqueName which renames soft-deleted rows.
F3: Failing test contradicts hasCredState fallback fix
Test asserted no fallback when hasCredState=true + empty creds.
But we intentionally removed the guard to fix 'unset fields: users'.
Fix: updated test to assert config users ARE included as fallback.
== HIGH ==
F4: sub_token generation ignores error
subTok, _ := randomHex(16) — empty sub_token on rand.Read failure.
Fix: check error, return on failure.
F5: BatchResetTraffic doesn't trigger config regeneration
Over-quota users stayed blocked for ~10s after traffic reset.
Fix: added notifyCredentialsChanged() after reset.
Verification: go build/vet/test all PASS. gofmt clean.
2026-08-31 04:26:51 +00:00

133 lines
3.6 KiB
Go

package user
import (
"errors"
"fmt"
"log"
"strings"
"github.com/kazeyukiro/3m-ui/backend/internal/database/models"
"gorm.io/gorm"
)
// ListFilter supports search across proxy users.
type ListFilter struct {
Query string // username / remark substring
Enabled *bool
Online *bool
Blocked *bool // computed from IsCredentialActive
}
func (s *Service) ListFiltered(f ListFilter) ([]models.ProxyUser, error) {
q := s.db.Model(&models.ProxyUser{}).Order("id desc")
if term := strings.TrimSpace(f.Query); term != "" {
like := "%" + term + "%"
q = q.Where("username LIKE ? OR remark LIKE ?", like, like)
}
if f.Enabled != nil {
q = q.Where("enabled = ?", *f.Enabled)
}
if f.Online != nil {
q = q.Where("online = ?", *f.Online)
}
var users []models.ProxyUser
if err := q.Find(&users).Error; err != nil {
return nil, err
}
if f.Blocked == nil {
return users, nil
}
out := make([]models.ProxyUser, 0, len(users))
for _, u := range users {
blocked := !IsCredentialActive(u)
if blocked == *f.Blocked {
out = append(out, u)
}
}
return out, nil
}
// BatchAction is a bulk operation over proxy user IDs.
type BatchAction string
const (
BatchEnable BatchAction = "enable"
BatchDisable BatchAction = "disable"
BatchResetTraffic BatchAction = "reset-traffic"
BatchDelete BatchAction = "delete"
)
func (s *Service) Batch(action BatchAction, ids []uint) (int, error) {
if len(ids) == 0 {
return 0, errors.New("ids is required")
}
// de-dupe
seen := map[uint]struct{}{}
clean := make([]uint, 0, len(ids))
for _, id := range ids {
if id == 0 {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
clean = append(clean, id)
}
if len(clean) == 0 {
return 0, errors.New("ids is required")
}
switch action {
case BatchEnable:
res := s.db.Model(&models.ProxyUser{}).Where("id IN ?", clean).Update("enabled", true)
if res.Error != nil {
return 0, res.Error
}
if err := s.notifyCredentialsChanged(); err != nil {
log.Printf("warning: credentials changed notification failed: %v", err)
}
return int(res.RowsAffected), nil
case BatchDisable:
res := s.db.Model(&models.ProxyUser{}).Where("id IN ?", clean).Update("enabled", false)
if res.Error != nil {
return 0, res.Error
}
if err := s.notifyCredentialsChanged(); err != nil {
log.Printf("warning: credentials changed notification failed: %v", err)
}
return int(res.RowsAffected), nil
case BatchResetTraffic:
res := s.db.Model(&models.ProxyUser{}).Where("id IN ?", clean).Updates(map[string]interface{}{
"traffic_used": 0,
"upload_bytes": 0,
"download_bytes": 0,
})
if res.Error != nil {
return 0, res.Error
}
// Resetting traffic may unblock previously over-quota users. Trigger a
// config regeneration so blocked users regain access immediately
// instead of waiting for the next enforcer tick (up to 10s latency).
if err := s.notifyCredentialsChanged(); err != nil {
log.Printf("warning: credentials changed notification failed: %v", err)
}
return int(res.RowsAffected), nil
case BatchDelete:
if err := s.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Unscoped().Where("proxy_user_id IN ?", clean).Delete(&models.ListenerUser{}).Error; err != nil {
return err
}
return tx.Unscoped().Where("id IN ?", clean).Delete(&models.ProxyUser{}).Error
}); err != nil {
return 0, err
}
if err := s.notifyCredentialsChanged(); err != nil {
log.Printf("warning: credentials changed notification failed: %v", err)
}
return len(clean), nil
default:
return 0, fmt.Errorf("unknown batch action %q (enable|disable|reset-traffic|delete)", action)
}
}