diff --git a/app.go b/app.go index ec7df9d..8df3055 100644 --- a/app.go +++ b/app.go @@ -1,9 +1,13 @@ package main import ( + "bufio" "context" "fmt" "os" + "path/filepath" + "strconv" + "strings" "sync" "time" @@ -286,6 +290,247 @@ func (a *App) VerifyAllExchange() map[string]any { return results } +// VerifyAndExport 复验并导出结果。 +func (a *App) VerifyAndExport(commodityID string) map[string]any { + accounts := a.scheduler.AccountSnapshot() + results := make(map[string]any) + hitAccounts := make([]string, 0) + missAccounts := make([]string, 0) + + for i, account := range accounts { + if i > 0 { + time.Sleep(200 * time.Millisecond) + } + + if account.Cookie == "" { + missAccounts = append(missAccounts, account.Name) + results[account.Name] = map[string]any{ + "success": false, + "error": "Cookie 为空", + } + continue + } + + records, err := a.client.ExchangeList(account.Cookie) + if err != nil { + missAccounts = append(missAccounts, account.Name) + results[account.Name] = map[string]any{ + "success": false, + "error": "查询失败: " + err.Error(), + } + continue + } + + // 检查是否抢到了指定商品 + hit := false + for _, r := range records { + if r.CommodityID == commodityID { + hit = true + break + } + } + + if hit { + hitAccounts = append(hitAccounts, account.Name) + results[account.Name] = map[string]any{ + "success": true, + "hit": true, + } + } else { + missAccounts = append(missAccounts, account.Name) + results[account.Name] = map[string]any{ + "success": true, + "hit": false, + } + } + } + + return map[string]any{ + "success": true, + "hitCount": len(hitAccounts), + "missCount": len(missAccounts), + "hitAccounts": hitAccounts, + "missAccounts": missAccounts, + "details": results, + } +} + +// ExportAccounts 导出指定账号到文件。 +func (a *App) ExportAccounts(accountNames []string, filename string) map[string]any { + if len(accountNames) == 0 { + return map[string]any{"success": false, "error": "没有要导出的账号"} + } + + // 读取原账号文件 + accountFile := a.scheduler.AccountFilePath() + lines, err := os.ReadFile(accountFile) + if err != nil { + return map[string]any{"success": false, "error": "读取账号文件失败: " + err.Error()} + } + + // 解析账号行 + nameSet := make(map[string]bool) + for _, name := range accountNames { + nameSet[name] = true + } + + var matchedLines []string + scanner := bufio.NewScanner(strings.NewReader(string(lines))) + for scanner.Scan() { + line := scanner.Text() + trimmed := strings.TrimSpace(line) + if trimmed == "" || strings.HasPrefix(trimmed, "#") { + continue + } + parts := strings.SplitN(trimmed, "----", 3) + if len(parts) >= 1 { + name := strings.TrimSpace(parts[0]) + if nameSet[name] { + matchedLines = append(matchedLines, line) + } + } + } + + if len(matchedLines) == 0 { + return map[string]any{"success": false, "error": "没有找到匹配的账号行"} + } + + // 生成导出文件路径 + exportPath := filepath.Join(filepath.Dir(accountFile), filename) + content := strings.Join(matchedLines, "\n") + "\n" + + if err := os.WriteFile(exportPath, []byte(content), 0644); err != nil { + return map[string]any{"success": false, "error": "写入文件失败: " + err.Error()} + } + + logger.LogEvent("导出", "success", "已导出 %d 个账号到: %s", len(matchedLines), exportPath) + + return map[string]any{ + "success": true, + "count": len(matchedLines), + "filePath": exportPath, + } +} + +// ExportAccountsWithProduct 导出指定账号到文件,自动使用中文商品名和时间戳命名。 +func (a *App) ExportAccountsWithProduct(accountNames []string, commodityID, commodityName string) map[string]any { + // 生成文件名: user_已抢到_商品中文名_20260626_150405.txt + now := time.Now() + timestamp := now.Format("20060102_150405") + // 清理商品名中的特殊字符 + safeName := strings.ReplaceAll(commodityName, "/", "_") + safeName = strings.ReplaceAll(safeName, "\\", "_") + safeName = strings.ReplaceAll(safeName, ":", "_") + safeName = strings.ReplaceAll(safeName, "*", "_") + safeName = strings.ReplaceAll(safeName, "?", "_") + safeName = strings.ReplaceAll(safeName, "\"", "_") + safeName = strings.ReplaceAll(safeName, "<", "_") + safeName = strings.ReplaceAll(safeName, ">", "_") + safeName = strings.ReplaceAll(safeName, "|", "_") + filename := fmt.Sprintf("user_已抢到_%s_%s.txt", safeName, timestamp) + + return a.ExportAccounts(accountNames, filename) +} + +// OrganizeAccounts 整理账号,把已抢到指定商品的账号分离出来。 +func (a *App) OrganizeAccounts(commodityID, commodityName string, startTime, endTime int64) map[string]any { + accounts := a.scheduler.AccountSnapshot() + hitAccounts := make([]string, 0) + missAccounts := make([]string, 0) + + for i, account := range accounts { + if i > 0 { + time.Sleep(200 * time.Millisecond) + } + + if account.Cookie == "" { + missAccounts = append(missAccounts, account.Name) + continue + } + + records, err := a.client.ExchangeList(account.Cookie) + if err != nil { + missAccounts = append(missAccounts, account.Name) + continue + } + + // 检查是否在指定时间段内抢到了指定商品 + hit := false + for _, r := range records { + if r.CommodityID == commodityID { + // 检查时间范围 + recordTime, _ := strconv.ParseInt(r.Time, 10, 64) + if startTime == 0 || (recordTime >= startTime && recordTime <= endTime) { + hit = true + break + } + } + } + + if hit { + hitAccounts = append(hitAccounts, account.Name) + } else { + missAccounts = append(missAccounts, account.Name) + } + } + + // 导出已抢到的账号 + var exportResult map[string]any + if len(hitAccounts) > 0 { + exportResult = a.ExportAccountsWithProduct(hitAccounts, commodityID, commodityName) + } + + // 更新原文件,只保留未抢到的 + accountFile := a.scheduler.AccountFilePath() + lines, err := os.ReadFile(accountFile) + if err != nil { + return map[string]any{"success": false, "error": "读取账号文件失败: " + err.Error()} + } + + missSet := make(map[string]bool) + for _, name := range missAccounts { + missSet[name] = true + } + + var keepLines []string + scanner := bufio.NewScanner(strings.NewReader(string(lines))) + for scanner.Scan() { + line := scanner.Text() + trimmed := strings.TrimSpace(line) + if trimmed == "" || strings.HasPrefix(trimmed, "#") { + keepLines = append(keepLines, line) + continue + } + parts := strings.SplitN(trimmed, "----", 3) + if len(parts) >= 1 { + name := strings.TrimSpace(parts[0]) + if missSet[name] { + keepLines = append(keepLines, line) + } + } + } + + // 写回原文件 + newContent := strings.Join(keepLines, "\n") + "\n" + if err := os.WriteFile(accountFile, []byte(newContent), 0644); err != nil { + return map[string]any{"success": false, "error": "更新账号文件失败: " + err.Error()} + } + + // 重新加载账号 + a.scheduler.LoadAccounts() + + logger.LogEvent("整理", "success", "整理完成: 已抢到=%d, 未抢到=%d", len(hitAccounts), len(missAccounts)) + + return map[string]any{ + "success": true, + "hitCount": len(hitAccounts), + "missCount": len(missAccounts), + "hitAccounts": hitAccounts, + "missAccounts": missAccounts, + "exportResult": exportResult, + } +} + // GetVersion 返回应用版本号。 func (a *App) GetVersion() string { return constants.Version diff --git a/frontend/src/components/ck/CKManager.vue b/frontend/src/components/ck/CKManager.vue new file mode 100644 index 0000000..3fc1a59 --- /dev/null +++ b/frontend/src/components/ck/CKManager.vue @@ -0,0 +1,523 @@ + + + + + diff --git a/frontend/src/components/layout/RightConsole.vue b/frontend/src/components/layout/RightConsole.vue index 50bcd78..3568591 100644 --- a/frontend/src/components/layout/RightConsole.vue +++ b/frontend/src/components/layout/RightConsole.vue @@ -7,6 +7,7 @@ import ControlStrip from '@/components/console/ControlStrip.vue' import AccountTable from '@/components/account/AccountTable.vue' import LogPanel from '@/components/log/LogPanel.vue' import SettingsModal from '@/components/modal/SettingsModal.vue' +import CKManager from '@/components/ck/CKManager.vue' defineProps<{ serverTime: string @@ -16,6 +17,7 @@ const { theme, toggleTheme } = useTheme() const { runState, countdownSeconds, formatRemain } = useScheduler() const settingsModalRef = ref | null>(null) +const currentPage = ref<'console' | 'ck'>('console') const openSettings = () => { settingsModalRef.value?.open() @@ -27,11 +29,23 @@ const openSettings = () => {
-
-
抢购控制台
-
{{ runState }}
+ - + {{ formatRemain(countdownSeconds) }}
@@ -48,19 +62,29 @@ const openSettings = () => {
- - + + + + + @@ -99,6 +123,35 @@ const openSettings = () => { letter-spacing: -.01em; } +.nav-tabs { + display: flex; + gap: 4px; +} + +.nav-tab { + height: 28px; + padding: 0 12px; + border-radius: var(--radius-sm); + border: 0; + background: transparent; + color: var(--muted); + cursor: pointer; + font-size: 13px; + font-weight: 500; + transition: .12s; +} + +.nav-tab:hover { + color: var(--text-strong); + background: var(--surface-2); +} + +.nav-tab.active { + background: var(--accent-soft); + color: var(--accent); + font-weight: 600; +} + .headline-meta { font-size: 12px; color: var(--muted); @@ -143,6 +196,12 @@ const openSettings = () => { height: 100%; } +.ck-page { + flex: 1; + overflow-y: auto; + min-height: 0; +} + @media (max-width: 1180px) { .right { overflow: visible; diff --git a/frontend/src/env.d.ts b/frontend/src/env.d.ts index c46867a..b561442 100644 --- a/frontend/src/env.d.ts +++ b/frontend/src/env.d.ts @@ -27,6 +27,10 @@ interface Window { SaveSettings: (cfg: any) => Promise VerifyExchange: (accountName: string) => Promise VerifyAllExchange: () => Promise + VerifyAndExport: (commodityID: string) => Promise + ExportAccounts: (accountNames: string[], filename: string) => Promise + ExportAccountsWithProduct: (accountNames: string[], commodityID: string, commodityName: string) => Promise + OrganizeAccounts: (commodityID: string, commodityName: string, startTime: number, endTime: number) => Promise } } } diff --git a/internal/constants/constants.go b/internal/constants/constants.go index 6dea19a..5776712 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.0.3" +const Version = "0.1.0" const ( ManualID = "G4KA4Qnz4LDp7" @@ -18,7 +18,7 @@ const ( BatchLimitURL = BaseURL + "/wgapi/ordnc/activity/peace/batchExchangeLimit" PreExchangeURL = BaseURL + "/wgapi/ordnc/activity/peace/preExchange" ExchangeURL = BaseURL + "/wgapi/ordnc/activity/peace/exchange" - ExchangeListURL = BaseURL + "/wgapi/ordnc/activity/peace/exchangeList?manualID=" + ManualID + "&limit=50&offset=0" + ExchangeListURL = BaseURL + "/wgapi/ordnc/activity/peace/exchangeList?manualID=" + ManualID + "&limit=100&offset=0" BindInfoURL = BaseURL + "/japi/carnivalApi/v2/tencent/bindInfo?actAlias=" + ActAlias ScoreURL = BaseURL + "/japi/oms/web/peace/credit/balance" UserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36" diff --git a/internal/seckill/scheduler.go b/internal/seckill/scheduler.go index 6a208f6..ab7b675 100644 --- a/internal/seckill/scheduler.go +++ b/internal/seckill/scheduler.go @@ -160,6 +160,11 @@ func (s *Scheduler) AccountCount() int { return len(s.accounts) } +// AccountFilePath 返回账号文件路径。 +func (s *Scheduler) AccountFilePath() string { + return s.paths.AccountFile +} + // AccountSnapshot 复制一份账号列表。 func (s *Scheduler) AccountSnapshot() []types.Account { s.accountsMu.RLock() diff --git a/wails.json b/wails.json index 6975c36..873f9d7 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.0.4", + "version": "0.1.0", "frontend:dir": "frontend", "frontend:install": "npm install", "frontend:build": "npm run build",