- 抽离账号管理为独立 AccountStore,Scheduler 只持有引用, 职责单一化(scheduler.go 减少 ~110 行) - Wails 绑定方法返回结构体替代 map[string]any(新增 responses.go), 前端获得强类型,字段名保持兼容无需改前端 - ExchangeRecord 移至 types 包,消除 types↔client 循环依赖 - extractAPIStatus 用 apiResponder 接口替代 reflect 逐字段查找, types.APIResponse 与 ExchangeListResponse 实现 APIStatus 方法 - 删除 types.Account.Password 死字段(鉴权仅用 Cookie) - 重新生成 Wails 绑定,前端 vue-tsc 类型检查通过
162 lines
4.9 KiB
Go
162 lines
4.9 KiB
Go
package seckill
|
||
|
||
import (
|
||
"bufio"
|
||
"bytes"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"sync"
|
||
|
||
"douyu_go/internal/constants"
|
||
"douyu_go/internal/logger"
|
||
"douyu_go/internal/types"
|
||
)
|
||
|
||
// AccountStore 账号存储,负责从账号文件加载、查询账号信息。
|
||
// 账号文件格式:每行 `账号名----密码----Cookie`(密码段保留以兼容历史文件,
|
||
// 但程序不读取也不使用密码,鉴权完全基于 Cookie)。
|
||
type AccountStore struct {
|
||
mu sync.RWMutex
|
||
accounts []types.Account
|
||
filePath string
|
||
}
|
||
|
||
// NewAccountStore 创建账号存储,filePath 为账号文件路径。
|
||
func NewAccountStore(filePath string) *AccountStore {
|
||
if filePath == "" {
|
||
filePath = constants.DefaultAccountFile
|
||
}
|
||
return &AccountStore{filePath: filePath}
|
||
}
|
||
|
||
// FilePath 返回账号文件路径。
|
||
func (a *AccountStore) FilePath() string {
|
||
return a.filePath
|
||
}
|
||
|
||
// Load 从账号文件加载账号。
|
||
func (a *AccountStore) Load() {
|
||
path := a.filePath
|
||
|
||
// 文件不存在时自动创建目录和空文件,方便用户直接粘贴 Cookie
|
||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||
if dir := filepath.Dir(path); dir != "" && dir != "." {
|
||
if mkErr := os.MkdirAll(dir, 0755); mkErr != nil {
|
||
logger.LogEvent("账号", "warn", "账号目录创建失败: %s: %v", dir, mkErr)
|
||
}
|
||
}
|
||
// 使用 Windows 换行格式 \r\n
|
||
hint := "# 格式: 账号名----密码----Cookie\r\n# 每行一个账号,粘贴后保存即可\r\n"
|
||
if wrErr := os.WriteFile(path, []byte(hint), 0644); wrErr != nil {
|
||
logger.LogEvent("账号", "warn", "账号文件创建失败: %s: %v", path, wrErr)
|
||
} else {
|
||
logger.LogEvent("账号", "info", "已创建空账号文件: %s,请粘贴 Cookie 后重新加载", path)
|
||
}
|
||
}
|
||
|
||
// 读取文件内容,自动转换换行格式为 Windows 格式
|
||
content, err := os.ReadFile(path)
|
||
if err != nil {
|
||
logger.LogEvent("账号", "error", "读取账号文件失败: %s: %v", path, err)
|
||
a.mu.Lock()
|
||
a.accounts = nil
|
||
a.mu.Unlock()
|
||
return
|
||
}
|
||
|
||
// 检测并转换换行格式
|
||
normalized, converted := normalizeLineEndings(content)
|
||
if converted {
|
||
// 保存转换后的内容
|
||
if err := os.WriteFile(path, normalized, 0644); err != nil {
|
||
logger.LogEvent("账号", "warn", "账号文件换行格式转换失败: %s: %v", path, err)
|
||
} else {
|
||
logger.LogEvent("账号", "info", "账号文件已自动转换为 Windows 换行格式: %s", path)
|
||
}
|
||
content = normalized
|
||
}
|
||
|
||
var list []types.Account
|
||
scanner := bufio.NewScanner(bytes.NewReader(content))
|
||
scanner.Buffer(make([]byte, 1024), 1024*1024)
|
||
for scanner.Scan() {
|
||
line := strings.TrimSpace(scanner.Text())
|
||
if line == "" || strings.HasPrefix(line, "#") {
|
||
continue
|
||
}
|
||
parts := strings.SplitN(line, "----", 3)
|
||
if len(parts) != 3 {
|
||
logger.LogEvent("账号", "warn", "账号行格式错误,已跳过: %s", line)
|
||
continue
|
||
}
|
||
list = append(list, types.Account{
|
||
Name: strings.TrimSpace(parts[0]),
|
||
Cookie: strings.TrimSpace(parts[2]),
|
||
})
|
||
}
|
||
if err := scanner.Err(); err != nil {
|
||
logger.LogEvent("账号", "error", "读取账号文件中断: %v", err)
|
||
}
|
||
|
||
a.mu.Lock()
|
||
a.accounts = list
|
||
a.mu.Unlock()
|
||
logger.LogEvent("账号", "success", "账号加载完成: %d 个,来源=%s", len(list), path)
|
||
}
|
||
|
||
// Count 返回当前账号数。
|
||
func (a *AccountStore) Count() int {
|
||
a.mu.RLock()
|
||
defer a.mu.RUnlock()
|
||
return len(a.accounts)
|
||
}
|
||
|
||
// Snapshot 复制一份账号列表。
|
||
func (a *AccountStore) Snapshot() []types.Account {
|
||
a.mu.RLock()
|
||
defer a.mu.RUnlock()
|
||
list := make([]types.Account, len(a.accounts))
|
||
copy(list, a.accounts)
|
||
return list
|
||
}
|
||
|
||
// CookieByName 按账号名查找 Cookie。
|
||
func (a *AccountStore) CookieByName(name string) (string, bool) {
|
||
a.mu.RLock()
|
||
defer a.mu.RUnlock()
|
||
for _, account := range a.accounts {
|
||
if account.Name == name {
|
||
return account.Cookie, true
|
||
}
|
||
}
|
||
return "", false
|
||
}
|
||
|
||
// normalizeLineEndings 检测并转换换行格式为 Windows 格式(\r\n)。
|
||
// 返回转换后的内容和是否进行了转换。
|
||
func normalizeLineEndings(content []byte) ([]byte, bool) {
|
||
// 检查是否已经是 Windows 格式(包含 \r\n)
|
||
if bytes.Contains(content, []byte("\r\n")) {
|
||
// 已经是 Windows 格式,检查是否还有单独的 \n
|
||
// 先将所有 \r\n 替换为占位符,检查是否还有单独的 \n
|
||
temp := bytes.ReplaceAll(content, []byte("\r\n"), []byte("__RN_PLACEHOLDER__"))
|
||
if !bytes.Contains(temp, []byte("\n")) && !bytes.Contains(temp, []byte("\r")) {
|
||
// 完全是 Windows 格式,不需要转换
|
||
return content, false
|
||
}
|
||
// 混合格式,需要统一转换
|
||
}
|
||
|
||
// 转换逻辑:先统一为 \n,再转换为 \r\n
|
||
normalized := content
|
||
// 1. 将 \r\n 替换为 \n
|
||
normalized = bytes.ReplaceAll(normalized, []byte("\r\n"), []byte("\n"))
|
||
// 2. 将单独的 \r 替换为 \n
|
||
normalized = bytes.ReplaceAll(normalized, []byte("\r"), []byte("\n"))
|
||
// 3. 将 \n 替换为 \r\n
|
||
normalized = bytes.ReplaceAll(normalized, []byte("\n"), []byte("\r\n"))
|
||
|
||
return normalized, true
|
||
}
|