From 75eb0e8ce2d1929f356208797926fad91decdc64 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 3 Oct 2026 20:06:03 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A0=B8=E5=AF=B9=E5=88=B0=E7=9A=84=E7=A7=AF?= =?UTF-8?q?=E5=88=86=E4=BD=99=E9=A2=9D=E5=90=8C=E6=AD=A5=E5=88=B7=E6=96=B0?= =?UTF-8?q?=E4=B8=BB=E7=95=8C=E9=9D=A2=E7=A7=AF=E5=88=86=E5=88=97=EF=BC=8C?= =?UTF-8?q?=E5=8F=91=E5=B8=830.3.6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端在"导入账号/手动查询/抢购后核对"时都会写入积分基线,但界面一直没有读它: 积分列的 scoreMap 只由"查询积分"按钮填充,所以自动核对完余额后主界面不动, 用户必须再点一次"查询积分"才看到新余额。 - 新增 GetScoreBaselines 绑定:一次性返回各账号最近一次已记录的积分余额 (Scheduler.ScoreBaselines → ScoreBaselineStore.Snapshot),界面直接读取, 不必再为每个账号单独发请求 - 前端 useAccounts 新增 syncScoresFromBackend:把后端基线写入 scoreMap 与 localStorage(与手动查询同一份状态);导入场景下会短暂轮询等待后端异步查询写完 - 抢购状态回到 idle 时自动同步一次(对应抢购结束后的核对结果) - 导入账号成功后按导入的账号名同步一次 - 新增断言:核对后 ScoreBaselines 返回最新余额 --- frontend/src/components/ck/CKManager.vue | 14 +++++++++++- frontend/src/composables/useAccounts.ts | 28 ++++++++++++++++++++++++ frontend/src/composables/useScheduler.ts | 4 +++- internal/app/app_verify.go | 8 +++++++ internal/constants/constants.go | 2 +- internal/seckill/scheduler.go | 14 ++++++++++++ internal/seckill/scheduler_test.go | 4 ++++ internal/seckill/score_baseline.go | 14 ++++++++++++ internal/types/responses.go | 8 +++++++ wails.json | 4 ++-- 10 files changed, 95 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/ck/CKManager.vue b/frontend/src/components/ck/CKManager.vue index b1b2c2a..c5c229e 100644 --- a/frontend/src/components/ck/CKManager.vue +++ b/frontend/src/components/ck/CKManager.vue @@ -14,7 +14,7 @@ import { const { selectedProductId, selectedProductName, selectedProduct } = useProducts() const { showToast } = useToast() -const { accounts, loadAccounts, clearSelection } = useAccounts() +const { accounts, loadAccounts, clearSelection, syncScoresFromBackend } = useAccounts() // 状态 const loading = ref(false) @@ -41,6 +41,16 @@ const refreshManagedAccounts = async (resetSelection = true) => { } } +// parseImportedNames 从粘贴内容里取出账号名(每行 账号名----密码----Cookie)。 +const parseImportedNames = (content: string): string[] => { + return content + .split(/\r?\n/) + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith('#')) + .map((line) => line.split('----')[0]?.trim() || '') + .filter(Boolean) +} + const importAccountText = async (content: string) => { accountLoading.value = true try { @@ -52,6 +62,8 @@ const importAccountText = async (content: string) => { accountContent.value = '' await refreshManagedAccounts() showToast('导入成功', `已添加 ${result.count} 个账号`, 'success') + // 后端会在导入后自动记一次积分基线,这里等它写完并刷新积分列 + void syncScoresFromBackend(parseImportedNames(content)) } catch (e: any) { showToast('导入失败', e?.message || '', 'error') } finally { diff --git a/frontend/src/composables/useAccounts.ts b/frontend/src/composables/useAccounts.ts index 40dad45..52674fb 100644 --- a/frontend/src/composables/useAccounts.ts +++ b/frontend/src/composables/useAccounts.ts @@ -7,6 +7,7 @@ import { GetXiaodianBindQRCode, BindXiaodianRole, DeleteAccounts, + GetScoreBaselines, QueryScore, ReloadAccounts, VerifyAllExchange, @@ -150,6 +151,32 @@ export function useAccounts() { } } + // syncScoresFromBackend 用后端已记录的积分余额刷新积分列。 + // + // 后端在"导入账号""手动查询积分""抢购结束后核对"时都会写入基线, + // 这里直接读取结果,不需要再逐账号重新请求接口。 + // expectNames 非空时会短暂轮询等待后端异步查询完成(导入刚返回时可能还没写完)。 + const syncScoresFromBackend = async (expectNames: string[] = [], attempts = 4, delayMs = 600) => { + const tries = expectNames.length > 0 ? attempts : 1 + for (let i = 0; i < tries; i++) { + try { + const d = await GetScoreBaselines() + const scores = (d?.scores || {}) as Record + Object.keys(scores).forEach((name) => { + scoreMap.value[name] = String(scores[name]) + localStorage.setItem('accScore_' + name, String(scores[name])) + }) + if (expectNames.every((name) => name in scores)) { + return true + } + } catch {} + if (i < tries - 1) { + await new Promise((resolve) => setTimeout(resolve, delayMs)) + } + } + return false + } + const queryAllScore = async () => { showToast('查询积分', '正在并发查询...', 'info') const tasks = accounts.value.map(async (a) => { @@ -358,6 +385,7 @@ export function useAccounts() { selectedIndices, statusMap, scoreMap, + syncScoresFromBackend, xiaodianAssetsMap, xiaodianRolesMap, xiaodianQRCode, diff --git a/frontend/src/composables/useScheduler.ts b/frontend/src/composables/useScheduler.ts index 31c3954..7bdce1c 100644 --- a/frontend/src/composables/useScheduler.ts +++ b/frontend/src/composables/useScheduler.ts @@ -17,7 +17,7 @@ let unsubscribeSeckillState: (() => void) | null = null export function useScheduler() { const { showToast } = useToast() - const { resetStatus, buildAccountProducts, markSelectedPending, markAccountNamesPending, markSelectedStatus, queryXiaodianAssets } = useAccounts() + const { resetStatus, buildAccountProducts, markSelectedPending, markAccountNamesPending, markSelectedStatus, queryXiaodianAssets, syncScoresFromBackend } = useAccounts() const { selectedProductId, selectedProductName } = useProducts() const { currentActivity } = useActivity() const { addLocalLog, clearLocalResults } = useLogs() @@ -49,6 +49,8 @@ export function useScheduler() { } if (state === 'idle') { void refreshXiaodianAssetsAfterRun() + // 抢购结束后的自动核对已经把新余额写进后端基线,这里同步到积分列。 + void syncScoresFromBackend() } } diff --git a/internal/app/app_verify.go b/internal/app/app_verify.go index 5c6b22a..b8fe11a 100644 --- a/internal/app/app_verify.go +++ b/internal/app/app_verify.go @@ -21,6 +21,14 @@ type exchangeLookupResult struct { EmptyCookie bool } +// GetScoreBaselines 返回各账号最近一次已记录的积分余额。 +// +// 抢购结束后的自动核对会把新余额写进基线,界面据此刷新积分列, +// 不必再为每个账号单独发一次查询请求。 +func (a *App) GetScoreBaselines() types.ScoreBaselineResult { + return types.ScoreBaselineResult{Success: true, Scores: a.scheduler.ScoreBaselines()} +} + // QueryScore 查询单个账号的积分。 func (a *App) QueryScore(accountName string) types.ScoreResult { if accountName == "" { diff --git a/internal/constants/constants.go b/internal/constants/constants.go index 7ee9c0b..fab6ffe 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -3,7 +3,7 @@ package constants import "time" // Version 应用版本号,与 wails.json version 同步。 -const Version = "0.3.5" +const Version = "0.3.6" // BuildTime 构建时间,由编译时 ldflags 注入。 var BuildTime = "unknown" diff --git a/internal/seckill/scheduler.go b/internal/seckill/scheduler.go index 3670565..af0db09 100644 --- a/internal/seckill/scheduler.go +++ b/internal/seckill/scheduler.go @@ -4,6 +4,7 @@ import ( "fmt" "path/filepath" "sort" + "strconv" "strings" "sync" "time" @@ -549,6 +550,19 @@ func (s *Scheduler) ScoreBaseline(accountName string) (int, bool, time.Time) { return s.scoreBaselines.Get(accountName) } +// ScoreBaselines 返回各账号最新一次已记录的积分余额(字符串形式,供界面展示)。 +func (s *Scheduler) ScoreBaselines() map[string]string { + if s.scoreBaselines == nil { + return map[string]string{} + } + snapshot := s.scoreBaselines.Snapshot() + out := make(map[string]string, len(snapshot)) + for name, score := range snapshot { + out[name] = strconv.Itoa(score) + } + return out +} + // RecordScoreBaseline 记录账号的最新积分余额(落盘)。 func (s *Scheduler) RecordScoreBaseline(accountName string, score int) { if s.scoreBaselines != nil { diff --git a/internal/seckill/scheduler_test.go b/internal/seckill/scheduler_test.go index 0d261d4..4b8911a 100644 --- a/internal/seckill/scheduler_test.go +++ b/internal/seckill/scheduler_test.go @@ -1081,6 +1081,10 @@ func TestVerifyRunResultsConfirmsScoreDeduction(t *testing.T) { if after, ok, _ := scheduler.ScoreBaseline("a"); !ok || after != 800 { t.Fatalf("核对后应把 800 存回基线, got %d ok=%v", after, ok) } + // 界面通过 ScoreBaselines 读取,用于刷新积分列(无需再逐账号请求接口) + if got := scheduler.ScoreBaselines()["a"]; got != "800" { + t.Fatalf("ScoreBaselines[a] = %q, want 800", got) + } } // TestVerifyRunResultsWithoutBaseline 没有基线时先记录余额,不误报"未扣分"。 diff --git a/internal/seckill/score_baseline.go b/internal/seckill/score_baseline.go index 324c97b..967f6ff 100644 --- a/internal/seckill/score_baseline.go +++ b/internal/seckill/score_baseline.go @@ -112,6 +112,20 @@ func (s *ScoreBaselineStore) Forget(accountNames []string) { s.flush(snapshot) } +// Snapshot 返回全部账号的最新积分余额副本。 +func (s *ScoreBaselineStore) Snapshot() map[string]int { + if s == nil { + return nil + } + s.mu.RLock() + defer s.mu.RUnlock() + out := make(map[string]int, len(s.entries)) + for name, entry := range s.entries { + out[name] = entry.Score + } + return out +} + // Names 返回已记录基线的账号名(按字典序),便于测试与排查。 func (s *ScoreBaselineStore) Names() []string { if s == nil { diff --git a/internal/types/responses.go b/internal/types/responses.go index e6b9c0d..9874fd2 100644 --- a/internal/types/responses.go +++ b/internal/types/responses.go @@ -63,6 +63,14 @@ type TimeResult struct { Source string `json:"source"` } +// ScoreBaselineResult 各账号最近一次已记录的积分余额,供界面刷新积分列。 +// 这些值来自"导入账号/手动查询积分/抢购后核对",不需要再逐账号请求接口。 +type ScoreBaselineResult struct { + Success bool `json:"success"` + Scores map[string]string `json:"scores"` + Error string `json:"error,omitempty"` +} + // ScoreResult 积分查询结果。score 为字符串:成功时为数字字符串,失败时为错误描述。 type ScoreResult struct { Score string `json:"score"` diff --git a/wails.json b/wails.json index b1bf07b..c38563d 100644 --- a/wails.json +++ b/wails.json @@ -2,7 +2,7 @@ "$schema": "https://wails.io/schemas/config.v2.json", "name": "douyu_go", "outputfilename": "douyu_go", - "version": "0.3.5", + "version": "0.3.6", "frontend:dir": "frontend", "frontend:install": "npm install", "frontend:build": "npm run build", @@ -15,6 +15,6 @@ "email": "yml@example.com" }, "info": { - "productVersion": "0.3.5" + "productVersion": "0.3.6" } }