181 lines
5.8 KiB
Go
181 lines
5.8 KiB
Go
package seckill
|
||
|
||
import (
|
||
"bufio"
|
||
"bytes"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"sync"
|
||
|
||
"douyu_go/internal/config"
|
||
"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
|
||
legacyFilePath string
|
||
}
|
||
|
||
// NewAccountStore 创建账号存储。legacyFilePaths 仅在内部账号库首次创建时用于迁移旧文件。
|
||
func NewAccountStore(filePath string, legacyFilePaths ...string) *AccountStore {
|
||
if filePath == "" {
|
||
filePath = constants.DefaultAccountFile
|
||
}
|
||
legacyFilePath := ""
|
||
if len(legacyFilePaths) > 0 {
|
||
legacyFilePath = legacyFilePaths[0]
|
||
}
|
||
return &AccountStore{filePath: filePath, legacyFilePath: legacyFilePath}
|
||
}
|
||
|
||
// FilePath 返回账号文件路径。
|
||
func (a *AccountStore) FilePath() string {
|
||
return a.filePath
|
||
}
|
||
|
||
// Load 从应用内部账号库加载账号。
|
||
func (a *AccountStore) Load() {
|
||
path := a.filePath
|
||
|
||
// 首次启动时迁移旧版本工作目录中的账号文件,保留原文件作为备份。
|
||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||
migrated, migrateErr := config.MigrateLegacyFile(path, a.legacyFilePath)
|
||
if migrateErr != nil {
|
||
logger.LogEvent("账号", "warn", "旧账号文件迁移失败: %s: %v", a.legacyFilePath, migrateErr)
|
||
} else if migrated {
|
||
logger.LogEvent("账号", "success", "已迁移旧账号文件到应用内部目录: %s", path)
|
||
}
|
||
}
|
||
|
||
// 应用内部账号库不存在时自动创建目录和空文件,方便用户直接在界面导入。
|
||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||
if dir := filepath.Dir(path); dir != "" && dir != "." {
|
||
if mkErr := os.MkdirAll(dir, 0700); 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), 0600); wrErr != nil {
|
||
logger.LogEvent("账号", "warn", "账号文件创建失败: %s: %v", path, wrErr)
|
||
} else {
|
||
logger.LogEvent("账号", "info", "已创建空账号库: %s,请在 CK 管理中导入账号", 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, 0600); err != nil {
|
||
logger.LogEvent("账号", "warn", "账号文件换行格式转换失败: %s: %v", path, err)
|
||
} else {
|
||
logger.LogEvent("账号", "info", "账号文件已自动转换为 Windows 换行格式: %s", path)
|
||
}
|
||
content = normalized
|
||
}
|
||
if err := os.Chmod(path, 0600); err != nil {
|
||
logger.LogEvent("账号", "warn", "账号库权限收紧失败: %s: %v", path, err)
|
||
}
|
||
|
||
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
|
||
}
|