mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-04 00:15:28 +08:00
refactor config: keep account test status runtime-only
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user