package app import ( "bufio" "fmt" "os" "path/filepath" "strings" "time" "douyu_go/internal/logger" "douyu_go/internal/types" "douyu_go/internal/utils" ) // accountLine 账号文件的一行解析结果。 type accountLine struct { Raw string // 原始行文本 Name string // 解析出的账号名(注释/空行为空) Comment bool // 是否为注释或空行 } // parseAccountFile 读取账号文件并按行解析账号名。 func parseAccountFile(path string) ([]accountLine, error) { data, err := os.ReadFile(path) if err != nil { return nil, err } var lines []accountLine scanner := bufio.NewScanner(strings.NewReader(string(data))) for scanner.Scan() { raw := scanner.Text() trimmed := strings.TrimSpace(raw) if trimmed == "" || strings.HasPrefix(trimmed, "#") { lines = append(lines, accountLine{Raw: raw, Comment: true}) continue } name := "" if parts := strings.SplitN(trimmed, "----", 3); len(parts) >= 1 { name = strings.TrimSpace(parts[0]) } lines = append(lines, accountLine{Raw: raw, Name: name}) } return lines, scanner.Err() } // ExportAccounts 导出指定账号到文件。 func (a *App) ExportAccounts(accountNames []string, filename string) types.ExportAccountsResult { if len(accountNames) == 0 { return types.ExportAccountsResult{Error: "没有要导出的账号"} } accountFile := a.scheduler.AccountFilePath() lines, err := parseAccountFile(accountFile) if err != nil { return types.ExportAccountsResult{Error: "读取内部账号库失败: " + err.Error()} } nameSet := make(map[string]bool) for _, name := range accountNames { nameSet[name] = true } var matchedLines []string for _, line := range lines { if line.Comment { continue } if nameSet[line.Name] { matchedLines = append(matchedLines, line.Raw) } } if len(matchedLines) == 0 { return types.ExportAccountsResult{Error: "没有找到匹配的账号行"} } // 生成导出文件路径 exportPath := filepath.Join(filepath.Dir(accountFile), filename) content := strings.Join(matchedLines, "\n") + "\n" if err := os.MkdirAll(filepath.Dir(exportPath), 0700); err != nil { return types.ExportAccountsResult{Error: "创建导出目录失败: " + err.Error()} } if err := os.WriteFile(exportPath, []byte(content), 0600); err != nil { return types.ExportAccountsResult{Error: "写入文件失败: " + err.Error()} } logger.LogEvent("导出", "success", "已导出 %d 个账号到: %s", len(matchedLines), exportPath) return types.ExportAccountsResult{ Success: true, Count: len(matchedLines), FilePath: exportPath, } } // ExportAccountsWithProduct 导出指定账号到文件,自动使用中文商品名和时间戳命名。 func (a *App) ExportAccountsWithProduct(accountNames []string, commodityID, commodityName string) types.ExportAccountsResult { // 生成文件名: user_已抢到_商品中文名_20260626_150405.txt now := time.Now() timestamp := now.Format("20060102_150405") // 清理商品名中的特殊字符(复用 utils.SafeFilename,避免与 utils 包重复实现) safeName := utils.SafeFilename(commodityName) 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) types.OrganizeAccountsResult { accounts := a.scheduler.AccountSnapshot() hitAccounts := make([]string, 0) missAccounts := make([]string, 0) lookups := a.lookupExchangeRecords("整理", accounts) for _, lookup := range lookups { if lookup.EmptyCookie || lookup.Err != nil { missAccounts = append(missAccounts, lookup.Account.Name) continue } if hasCommodityRecord(lookup.Records, commodityID, startTime, endTime) { hitAccounts = append(hitAccounts, lookup.Account.Name) } else { missAccounts = append(missAccounts, lookup.Account.Name) } } // 导出已抢到的账号 var exportResult types.ExportAccountsResult if len(hitAccounts) > 0 { exportResult = a.ExportAccountsWithProduct(hitAccounts, commodityID, commodityName) if !exportResult.Success { return types.OrganizeAccountsResult{Error: "导出已抢到账号失败,已取消整理: " + exportResult.Error, ExportResult: exportResult} } } // 更新原文件,只保留未抢到的 accountFile := a.scheduler.AccountFilePath() lines, err := parseAccountFile(accountFile) if err != nil { return types.OrganizeAccountsResult{Error: "读取内部账号库失败: " + err.Error()} } missSet := make(map[string]bool) for _, name := range missAccounts { missSet[name] = true } var keepLines []string for _, line := range lines { if line.Comment { keepLines = append(keepLines, line.Raw) continue } if missSet[line.Name] { keepLines = append(keepLines, line.Raw) } } // 写回原文件前先备份,再用同目录临时文件替换,避免半写入损坏账号文件。 newContent := strings.Join(keepLines, "\r\n") + "\r\n" backupPath, err := writeFileWithBackup(accountFile, []byte(newContent)) if err != nil { return types.OrganizeAccountsResult{Error: "更新内部账号库失败: " + err.Error()} } logger.LogEvent("整理", "success", "内部账号库已安全更新,备份=%s", backupPath) // 重新加载账号 a.scheduler.LoadAccounts() logger.LogEvent("整理", "success", "整理完成: 已抢到=%d, 未抢到=%d", len(hitAccounts), len(missAccounts)) return types.OrganizeAccountsResult{ Success: true, HitCount: len(hitAccounts), MissCount: len(missAccounts), HitAccounts: hitAccounts, MissAccounts: missAccounts, ExportResult: exportResult, } } func writeFileWithBackup(path string, content []byte) (string, error) { info, err := os.Stat(path) if err != nil { return "", err } mode := info.Mode().Perm() if mode == 0 { mode = 0644 } original, err := os.ReadFile(path) if err != nil { return "", err } backupPath, err := createFileBackup(path, original, mode) if err != nil { return "", err } if err := replaceFileWithTemp(path, content, mode); err != nil { return backupPath, err } return backupPath, nil } func createFileBackup(path string, content []byte, mode os.FileMode) (string, error) { timestamp := time.Now().Format("20060102_150405.000") basePath := filepath.Join(filepath.Dir(path), filepath.Base(path)+"."+timestamp+".bak") for i := 0; i < 100; i++ { backupPath := basePath if i > 0 { backupPath = fmt.Sprintf("%s.%d", basePath, i) } file, err := os.OpenFile(backupPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, mode) if os.IsExist(err) { continue } if err != nil { return "", err } if _, err := file.Write(content); err != nil { _ = file.Close() _ = os.Remove(backupPath) return "", err } if err := file.Close(); err != nil { _ = os.Remove(backupPath) return "", err } return backupPath, nil } return "", fmt.Errorf("备份文件名连续冲突: %s", basePath) } func replaceFileWithTemp(path string, content []byte, mode os.FileMode) error { dir := filepath.Dir(path) tmp, err := os.CreateTemp(dir, "."+filepath.Base(path)+".*.tmp") if err != nil { return err } tmpPath := tmp.Name() defer func() { if tmpPath != "" { _ = os.Remove(tmpPath) } }() if _, err := tmp.Write(content); err != nil { _ = tmp.Close() return err } if err := tmp.Chmod(mode); err != nil { _ = tmp.Close() return err } if err := tmp.Sync(); err != nil { _ = tmp.Close() return err } if err := tmp.Close(); err != nil { return err } if err := os.Rename(tmpPath, path); err != nil { return err } tmpPath = "" return nil }