272 lines
7.6 KiB
Go
272 lines
7.6 KiB
Go
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
|
|
}
|