核对到的积分余额同步刷新主界面积分列,发布0.3.6

后端在"导入账号/手动查询/抢购后核对"时都会写入积分基线,但界面一直没有读它:
积分列的 scoreMap 只由"查询积分"按钮填充,所以自动核对完余额后主界面不动,
用户必须再点一次"查询积分"才看到新余额。

- 新增 GetScoreBaselines 绑定:一次性返回各账号最近一次已记录的积分余额
  (Scheduler.ScoreBaselines → ScoreBaselineStore.Snapshot),界面直接读取,
  不必再为每个账号单独发请求
- 前端 useAccounts 新增 syncScoresFromBackend:把后端基线写入 scoreMap 与
  localStorage(与手动查询同一份状态);导入场景下会短暂轮询等待后端异步查询写完
- 抢购状态回到 idle 时自动同步一次(对应抢购结束后的核对结果)
- 导入账号成功后按导入的账号名同步一次
- 新增断言:核对后 ScoreBaselines 返回最新余额
This commit is contained in:
yml2213
2026-10-03 20:06:03 +08:00
parent 5da35ab65e
commit 75eb0e8ce2
10 changed files with 95 additions and 5 deletions
+13 -1
View File
@@ -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 {
+28
View File
@@ -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<string, string>
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,
+3 -1
View File
@@ -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()
}
}
+8
View File
@@ -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 == "" {
+1 -1
View File
@@ -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"
+14
View File
@@ -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 {
+4
View File
@@ -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 没有基线时先记录余额,不误报"未扣分"。
+14
View File
@@ -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 {
+8
View File
@@ -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"`
+2 -2
View File
@@ -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"
}
}