From 990cdcf02d2b16497d15af2dcc6789281ec51e0c Mon Sep 17 00:00:00 2001 From: "CJACK." Date: Sun, 22 Mar 2026 00:49:53 +0800 Subject: [PATCH] refactor config: keep account test status runtime-only --- internal/admin/deps.go | 1 + internal/admin/handler_accounts_crud.go | 3 +- .../admin/handler_accounts_testing_test.go | 5 ++- internal/config/config.go | 9 ++--- internal/config/config_test.go | 37 +++++++++++++++++++ internal/config/store.go | 21 ++++++++++- internal/config/store_index.go | 24 ++++++++++++ 7 files changed, 90 insertions(+), 10 deletions(-) diff --git a/internal/admin/deps.go b/internal/admin/deps.go index 997c42b..d95eecf 100644 --- a/internal/admin/deps.go +++ b/internal/admin/deps.go @@ -17,6 +17,7 @@ type ConfigStore interface { FindAccount(identifier string) (config.Account, bool) UpdateAccountToken(identifier, token string) error UpdateAccountTestStatus(identifier, status string) error + AccountTestStatus(identifier string) (string, bool) Update(mutator func(*config.Config) error) error ExportJSONAndBase64() (string, string, error) IsEnvBacked() bool diff --git a/internal/admin/handler_accounts_crud.go b/internal/admin/handler_accounts_crud.go index 6536760..3761a7a 100644 --- a/internal/admin/handler_accounts_crud.go +++ b/internal/admin/handler_accounts_crud.go @@ -54,6 +54,7 @@ func (h *Handler) listAccounts(w http.ResponseWriter, r *http.Request) { } items := make([]map[string]any, 0, end-start) for _, acc := range accounts[start:end] { + testStatus, _ := h.Store.AccountTestStatus(acc.Identifier()) token := strings.TrimSpace(acc.Token) preview := "" if token != "" { @@ -70,7 +71,7 @@ func (h *Handler) listAccounts(w http.ResponseWriter, r *http.Request) { "has_password": acc.Password != "", "has_token": token != "", "token_preview": preview, - "test_status": acc.TestStatus, + "test_status": testStatus, }) } writeJSON(w, http.StatusOK, map[string]any{"items": items, "total": total, "page": page, "page_size": pageSize, "total_pages": totalPages}) diff --git a/internal/admin/handler_accounts_testing_test.go b/internal/admin/handler_accounts_testing_test.go index b07afaa..961794e 100644 --- a/internal/admin/handler_accounts_testing_test.go +++ b/internal/admin/handler_accounts_testing_test.go @@ -93,8 +93,9 @@ func TestTestAccount_BatchModeOnlyCreatesSession(t *testing.T) { if updated.Token != "new-token" { t.Fatalf("expected refreshed token to be persisted, got %q", updated.Token) } - if updated.TestStatus != "ok" { - t.Fatalf("expected test status ok, got %q", updated.TestStatus) + testStatus, ok := store.AccountTestStatus("batch@example.com") + if !ok || testStatus != "ok" { + t.Fatalf("expected runtime test status ok, got %q (ok=%v)", testStatus, ok) } } diff --git a/internal/config/config.go b/internal/config/config.go index 8c50f8e..7ab4587 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -19,11 +19,10 @@ type Config struct { } type Account struct { - Email string `json:"email,omitempty"` - Mobile string `json:"mobile,omitempty"` - Password string `json:"password,omitempty"` - Token string `json:"token,omitempty"` - TestStatus string `json:"test_status,omitempty"` + Email string `json:"email,omitempty"` + Mobile string `json:"mobile,omitempty"` + Password string `json:"password,omitempty"` + Token string `json:"token,omitempty"` } func (c *Config) ClearAccountTokens() { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index bd8b714..5429bb8 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -3,6 +3,7 @@ package config import ( "encoding/base64" "os" + "strings" "testing" ) @@ -147,3 +148,39 @@ func TestLoadConfigOnVercelWithoutConfigFileFallsBackToMemory(t *testing.T) { t.Fatalf("expected empty bootstrap config, got keys=%d accounts=%d", len(cfg.Keys), len(cfg.Accounts)) } } + +func TestAccountTestStatusIsRuntimeOnlyAndNotPersisted(t *testing.T) { + tmp, err := os.CreateTemp(t.TempDir(), "config-*.json") + if err != nil { + t.Fatalf("create temp config: %v", err) + } + defer tmp.Close() + if _, err := tmp.WriteString(`{ + "accounts":[{"email":"u@example.com","password":"p","test_status":"ok"}] + }`); err != nil { + t.Fatalf("write temp config: %v", err) + } + + t.Setenv("DS2API_CONFIG_JSON", "") + t.Setenv("CONFIG_JSON", "") + t.Setenv("DS2API_CONFIG_PATH", tmp.Name()) + + store := LoadStore() + if got, ok := store.AccountTestStatus("u@example.com"); ok || got != "" { + t.Fatalf("expected no runtime status loaded from config, got %q", got) + } + if err := store.UpdateAccountTestStatus("u@example.com", "ok"); err != nil { + t.Fatalf("update test status: %v", err) + } + if got, ok := store.AccountTestStatus("u@example.com"); !ok || got != "ok" { + t.Fatalf("expected runtime status to be available, got %q (ok=%v)", got, ok) + } + + content, err := os.ReadFile(tmp.Name()) + if err != nil { + t.Fatalf("read config: %v", err) + } + if strings.Contains(string(content), "test_status") { + t.Fatalf("expected test_status to stay out of persisted config, got: %s", content) + } +} diff --git a/internal/config/store.go b/internal/config/store.go index c607f81..d212594 100644 --- a/internal/config/store.go +++ b/internal/config/store.go @@ -17,6 +17,7 @@ type Store struct { fromEnv bool keyMap map[string]struct{} // O(1) API key lookup index accMap map[string]int // O(1) account lookup: identifier -> slice index + accTest map[string]string // runtime-only account test status cache } func LoadStore() *Store { @@ -58,6 +59,11 @@ func loadConfig() (Config, bool, error) { return Config{}, false, err } cfg.DropInvalidAccounts() + if strings.Contains(string(content), `"test_status"`) && !IsVercel() { + if b, err := json.MarshalIndent(cfg, "", " "); err == nil { + _ = os.WriteFile(ConfigPath(), b, 0o644) + } + } if IsVercel() { // Vercel filesystem is ephemeral/read-only for runtime writes; avoid save errors. return cfg, true, nil @@ -108,8 +114,19 @@ func (s *Store) UpdateAccountTestStatus(identifier, status string) error { if !ok { return errors.New("account not found") } - s.cfg.Accounts[idx].TestStatus = status - return s.saveLocked() + s.setAccountTestStatusLocked(s.cfg.Accounts[idx], status, identifier) + return nil +} + +func (s *Store) AccountTestStatus(identifier string) (string, bool) { + identifier = strings.TrimSpace(identifier) + if identifier == "" { + return "", false + } + s.mu.RLock() + defer s.mu.RUnlock() + status, ok := s.accTest[identifier] + return status, ok } func (s *Store) UpdateAccountToken(identifier, token string) error { diff --git a/internal/config/store_index.go b/internal/config/store_index.go index 7d0f62a..a0e6638 100644 --- a/internal/config/store_index.go +++ b/internal/config/store_index.go @@ -2,15 +2,20 @@ package config // rebuildIndexes must be called with the lock already held (or during init). func (s *Store) rebuildIndexes() { + prevStatus := s.accTest s.keyMap = make(map[string]struct{}, len(s.cfg.Keys)) for _, k := range s.cfg.Keys { s.keyMap[k] = struct{}{} } s.accMap = make(map[string]int, len(s.cfg.Accounts)) + s.accTest = make(map[string]string, len(s.cfg.Accounts)) for i, acc := range s.cfg.Accounts { id := acc.Identifier() if id != "" { s.accMap[id] = i + if status, ok := prevStatus[id]; ok { + s.setAccountTestStatusLocked(acc, status, "") + } } } } @@ -29,3 +34,22 @@ func (s *Store) findAccountIndexLocked(identifier string) (int, bool) { } return -1, false } + +func (s *Store) setAccountTestStatusLocked(acc Account, status, hintedIdentifier string) { + status = lower(status) + if status == "" { + return + } + if id := acc.Identifier(); id != "" { + s.accTest[id] = status + } + if email := acc.Email; email != "" { + s.accTest[email] = status + } + if mobile := CanonicalMobileKey(acc.Mobile); mobile != "" { + s.accTest[mobile] = status + } + if hintedIdentifier = lower(hintedIdentifier); hintedIdentifier != "" { + s.accTest[hintedIdentifier] = status + } +}