升级版本并改用内部账号存储
This commit is contained in:
@@ -21,7 +21,7 @@ const loading = ref(false)
|
||||
const verifyResult = ref<any>(null)
|
||||
const organizeResult = ref<any>(null)
|
||||
|
||||
// 账号文件管理状态
|
||||
// 内部账号库管理状态
|
||||
const accountContent = ref('')
|
||||
const accountLoading = ref(false)
|
||||
const selectedAccountNames = ref<string[]>([])
|
||||
@@ -46,7 +46,7 @@ const importAccountText = async (content: string) => {
|
||||
try {
|
||||
const result = await ImportAccounts(content)
|
||||
if (!result.success) {
|
||||
showToast('导入失败', result.error || '账号文件未更新', 'error')
|
||||
showToast('导入失败', result.error || '内部账号库未更新', 'error')
|
||||
return
|
||||
}
|
||||
accountContent.value = ''
|
||||
@@ -115,7 +115,7 @@ const saveEdit = async () => {
|
||||
try {
|
||||
const result = await UpdateAccount(editingAccount.value.name, editName.value, editCookie.value)
|
||||
if (!result.success) {
|
||||
showToast('保存失败', result.error || '账号文件未更新', 'error')
|
||||
showToast('保存失败', result.error || '内部账号库未更新', 'error')
|
||||
return
|
||||
}
|
||||
await refreshManagedAccounts()
|
||||
@@ -139,7 +139,7 @@ const deleteManagedAccounts = async (names: string[]) => {
|
||||
try {
|
||||
const result = await DeleteAccounts(targetNames)
|
||||
if (!result.success) {
|
||||
showToast('删除失败', result.error || '账号文件未更新', 'error')
|
||||
showToast('删除失败', result.error || '内部账号库未更新', 'error')
|
||||
return
|
||||
}
|
||||
await refreshManagedAccounts()
|
||||
|
||||
+7
-1
@@ -42,7 +42,7 @@ type App struct {
|
||||
products []types.Product
|
||||
productsAt time.Time
|
||||
|
||||
// accountsMu 串行化账号文件修改,避免并发导入、编辑或删除互相覆盖。
|
||||
// accountsMu 串行化内部账号库修改,避免并发导入、编辑或删除互相覆盖。
|
||||
accountsMu sync.Mutex
|
||||
|
||||
closeLog func()
|
||||
@@ -123,8 +123,14 @@ func (a *App) Startup(ctx context.Context) {
|
||||
a.ctx = ctx
|
||||
|
||||
a.paths = config.DefaultAppPaths()
|
||||
configMigrated, configMigrationErr := config.MigrateLegacyFile(a.paths.ConfigFile, a.paths.LegacyConfigFile)
|
||||
a.logStore = logger.NewMemoryLogStore(800)
|
||||
a.closeLog = logger.InitLogging(a.logStore, a.paths.LogDir)
|
||||
if configMigrationErr != nil {
|
||||
logger.LogEvent("配置", "warn", "旧配置迁移失败: %s: %v", a.paths.LegacyConfigFile, configMigrationErr)
|
||||
} else if configMigrated {
|
||||
logger.LogEvent("配置", "success", "已迁移旧配置到应用内部目录: %s", a.paths.ConfigFile)
|
||||
}
|
||||
|
||||
// 加载配置并应用运行时设置
|
||||
a.cfg = config.LoadConfig(a.paths.ConfigFile)
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"douyu_go/internal/types"
|
||||
)
|
||||
|
||||
// ImportAccounts 将剪贴板中的账号行追加到账号文件。每行格式必须为
|
||||
// ImportAccounts 将剪贴板中的账号行追加到内部账号库。每行格式必须为
|
||||
// 账号名----密码----Cookie,账号名不能与已有账号重复。
|
||||
func (a *App) ImportAccounts(content string) types.AccountMutationResult {
|
||||
entries, err := parseAccountImport(content)
|
||||
@@ -22,7 +22,7 @@ func (a *App) ImportAccounts(content string) types.AccountMutationResult {
|
||||
accountFile := a.scheduler.AccountFilePath()
|
||||
lines, err := parseAccountFile(accountFile)
|
||||
if err != nil {
|
||||
return types.AccountMutationResult{Error: "读取账号文件失败: " + err.Error()}
|
||||
return types.AccountMutationResult{Error: "读取内部账号库失败: " + err.Error()}
|
||||
}
|
||||
|
||||
existingNames := make(map[string]struct{}, len(lines))
|
||||
@@ -46,7 +46,7 @@ func (a *App) ImportAccounts(content string) types.AccountMutationResult {
|
||||
return types.AccountMutationResult{Success: true, Count: len(entries)}
|
||||
}
|
||||
|
||||
// UpdateAccount 更新账号名或 Cookie。newCookie 留空时保留文件中的原 Cookie。
|
||||
// UpdateAccount 更新账号名或 Cookie。newCookie 留空时保留内部账号库中的原 Cookie。
|
||||
func (a *App) UpdateAccount(originalName, newName, newCookie string) types.AccountMutationResult {
|
||||
originalName = strings.TrimSpace(originalName)
|
||||
newName = strings.TrimSpace(newName)
|
||||
@@ -64,7 +64,7 @@ func (a *App) UpdateAccount(originalName, newName, newCookie string) types.Accou
|
||||
accountFile := a.scheduler.AccountFilePath()
|
||||
lines, err := parseAccountFile(accountFile)
|
||||
if err != nil {
|
||||
return types.AccountMutationResult{Error: "读取账号文件失败: " + err.Error()}
|
||||
return types.AccountMutationResult{Error: "读取内部账号库失败: " + err.Error()}
|
||||
}
|
||||
|
||||
targetIndex := -1
|
||||
@@ -108,7 +108,7 @@ func (a *App) UpdateAccount(originalName, newName, newCookie string) types.Accou
|
||||
return types.AccountMutationResult{Success: true, Count: 1}
|
||||
}
|
||||
|
||||
// DeleteAccounts 从账号文件中删除指定账号,可一次删除多个账号。
|
||||
// DeleteAccounts 从内部账号库中删除指定账号,可一次删除多个账号。
|
||||
func (a *App) DeleteAccounts(accountNames []string) types.AccountMutationResult {
|
||||
nameSet := make(map[string]struct{}, len(accountNames))
|
||||
for _, name := range accountNames {
|
||||
@@ -126,7 +126,7 @@ func (a *App) DeleteAccounts(accountNames []string) types.AccountMutationResult
|
||||
accountFile := a.scheduler.AccountFilePath()
|
||||
lines, err := parseAccountFile(accountFile)
|
||||
if err != nil {
|
||||
return types.AccountMutationResult{Error: "读取账号文件失败: " + err.Error()}
|
||||
return types.AccountMutationResult{Error: "读取内部账号库失败: " + err.Error()}
|
||||
}
|
||||
kept := make([]accountLine, 0, len(lines))
|
||||
deleted := 0
|
||||
@@ -194,7 +194,7 @@ func (a *App) saveAccountLines(accountFile string, lines []accountLine) error {
|
||||
content += "\r\n"
|
||||
}
|
||||
if _, err := writeFileWithBackup(accountFile, []byte(content)); err != nil {
|
||||
return fmt.Errorf("更新账号文件失败: %w", err)
|
||||
return fmt.Errorf("更新内部账号库失败: %w", err)
|
||||
}
|
||||
a.scheduler.LoadAccounts()
|
||||
return nil
|
||||
|
||||
@@ -53,7 +53,7 @@ func (a *App) ExportAccounts(accountNames []string, filename string) types.Expor
|
||||
accountFile := a.scheduler.AccountFilePath()
|
||||
lines, err := parseAccountFile(accountFile)
|
||||
if err != nil {
|
||||
return types.ExportAccountsResult{Error: "读取账号文件失败: " + err.Error()}
|
||||
return types.ExportAccountsResult{Error: "读取内部账号库失败: " + err.Error()}
|
||||
}
|
||||
|
||||
nameSet := make(map[string]bool)
|
||||
@@ -79,7 +79,10 @@ func (a *App) ExportAccounts(accountNames []string, filename string) types.Expor
|
||||
exportPath := filepath.Join(filepath.Dir(accountFile), filename)
|
||||
content := strings.Join(matchedLines, "\n") + "\n"
|
||||
|
||||
if err := os.WriteFile(exportPath, []byte(content), 0644); err != nil {
|
||||
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()}
|
||||
}
|
||||
|
||||
@@ -137,7 +140,7 @@ func (a *App) OrganizeAccounts(commodityID, commodityName string, startTime, end
|
||||
accountFile := a.scheduler.AccountFilePath()
|
||||
lines, err := parseAccountFile(accountFile)
|
||||
if err != nil {
|
||||
return types.OrganizeAccountsResult{Error: "读取账号文件失败: " + err.Error()}
|
||||
return types.OrganizeAccountsResult{Error: "读取内部账号库失败: " + err.Error()}
|
||||
}
|
||||
|
||||
missSet := make(map[string]bool)
|
||||
@@ -160,9 +163,9 @@ func (a *App) OrganizeAccounts(commodityID, commodityName string, startTime, end
|
||||
newContent := strings.Join(keepLines, "\r\n") + "\r\n"
|
||||
backupPath, err := writeFileWithBackup(accountFile, []byte(newContent))
|
||||
if err != nil {
|
||||
return types.OrganizeAccountsResult{Error: "更新账号文件失败: " + err.Error()}
|
||||
return types.OrganizeAccountsResult{Error: "更新内部账号库失败: " + err.Error()}
|
||||
}
|
||||
logger.LogEvent("整理", "success", "账号文件已安全更新,备份=%s", backupPath)
|
||||
logger.LogEvent("整理", "success", "内部账号库已安全更新,备份=%s", backupPath)
|
||||
|
||||
// 重新加载账号
|
||||
a.scheduler.LoadAccounts()
|
||||
|
||||
@@ -55,7 +55,7 @@ func (a *App) GetAccounts() []types.AccountView {
|
||||
return list
|
||||
}
|
||||
|
||||
// ReloadAccounts 重新加载账号文件。
|
||||
// ReloadAccounts 重新加载内部账号库。
|
||||
func (a *App) ReloadAccounts() types.ReloadAccountsResult {
|
||||
a.scheduler.LoadAccounts()
|
||||
return types.ReloadAccountsResult{Success: true, Count: a.scheduler.AccountCount()}
|
||||
|
||||
@@ -15,15 +15,43 @@ import (
|
||||
)
|
||||
|
||||
// DefaultAppPaths 收集运行时路径,支持环境变量覆盖。
|
||||
// 所有相对路径会基于当前工作目录解析为绝对路径,避免打包后工作目录不同导致路径错位。
|
||||
// 默认数据存放在当前用户的应用配置目录,而非工作目录,避免 macOS 应用包目录无写权限。
|
||||
func DefaultAppPaths() types.AppPaths {
|
||||
return types.AppPaths{
|
||||
ConfigFile: resolvePath(envOrDefault("DOUYU_CONFIG", constants.DefaultConfigFile)),
|
||||
AccountFile: resolvePath(envOrDefault("DOUYU_USERS", constants.DefaultAccountFile)),
|
||||
LogDir: resolvePath(envOrDefault("DOUYU_LOG_DIR", constants.DefaultLogDir)),
|
||||
SuccessDir: resolvePath(envOrDefault("DOUYU_SUCCESS_DIR", constants.DefaultSuccessDir)),
|
||||
FontFiles: imgpkg.CollectFontCandidates(),
|
||||
dataDir := resolvePath(envOrDefault("DOUYU_DATA_DIR", defaultAppDataDir()))
|
||||
configFile := filepath.Join(dataDir, constants.DefaultConfigFile)
|
||||
accountFile := filepath.Join(dataDir, constants.DefaultAccountFile)
|
||||
legacyConfigFile := resolvePath(constants.LegacyDefaultConfigFile)
|
||||
legacyAccountFile := resolvePath(constants.LegacyDefaultAccountFile)
|
||||
|
||||
if value := os.Getenv("DOUYU_CONFIG"); value != "" {
|
||||
configFile = resolvePath(value)
|
||||
legacyConfigFile = ""
|
||||
}
|
||||
if value := os.Getenv("DOUYU_USERS"); value != "" {
|
||||
accountFile = resolvePath(value)
|
||||
legacyAccountFile = ""
|
||||
}
|
||||
|
||||
return types.AppPaths{
|
||||
DataDir: dataDir,
|
||||
ConfigFile: configFile,
|
||||
AccountFile: accountFile,
|
||||
LegacyConfigFile: legacyConfigFile,
|
||||
LegacyAccountFile: legacyAccountFile,
|
||||
LogDir: resolvePath(envOrDefault("DOUYU_LOG_DIR", filepath.Join(dataDir, constants.DefaultLogDir))),
|
||||
SuccessDir: resolvePath(envOrDefault("DOUYU_SUCCESS_DIR", filepath.Join(dataDir, constants.DefaultSuccessDir))),
|
||||
FontFiles: imgpkg.CollectFontCandidates(),
|
||||
}
|
||||
}
|
||||
|
||||
func defaultAppDataDir() string {
|
||||
if dir, err := os.UserConfigDir(); err == nil && dir != "" {
|
||||
return filepath.Join(dir, constants.DefaultAppDataDirName)
|
||||
}
|
||||
if home, err := os.UserHomeDir(); err == nil && home != "" {
|
||||
return filepath.Join(home, "."+constants.DefaultAppDataDirName)
|
||||
}
|
||||
return "." + constants.DefaultAppDataDirName
|
||||
}
|
||||
|
||||
// resolvePath 将相对路径解析为绝对路径。
|
||||
@@ -44,6 +72,38 @@ func envOrDefault(key, fallback string) string {
|
||||
return fallback
|
||||
}
|
||||
|
||||
// MigrateLegacyFile 在目标文件不存在时复制旧版本数据,不删除旧文件。
|
||||
// 返回值表示是否完成了迁移。
|
||||
func MigrateLegacyFile(target, legacy string) (bool, error) {
|
||||
if target == "" || legacy == "" || samePath(target, legacy) {
|
||||
return false, nil
|
||||
}
|
||||
if _, err := os.Stat(target); err == nil {
|
||||
return false, nil
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(legacy)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(target), 0700); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := os.WriteFile(target, content, 0600); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func samePath(left, right string) bool {
|
||||
return filepath.Clean(left) == filepath.Clean(right)
|
||||
}
|
||||
|
||||
// LoadTraceConfig 读取 trace 开关;默认关闭,避免保存敏感调试数据。
|
||||
func LoadTraceConfig() types.TraceConfig {
|
||||
return types.TraceConfig{
|
||||
@@ -160,5 +220,8 @@ func SaveConfig(path string, cfg types.Config) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, data, 0644)
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, data, 0600)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMigrateLegacyFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
legacy := filepath.Join(dir, "legacy", "config.json")
|
||||
target := filepath.Join(dir, "internal", "config.json")
|
||||
content := []byte(`{"activity":"xiaodian"}`)
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(legacy), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(legacy, content, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
migrated, err := MigrateLegacyFile(target, legacy)
|
||||
if err != nil {
|
||||
t.Fatalf("迁移失败: %v", err)
|
||||
}
|
||||
if !migrated {
|
||||
t.Fatal("应迁移旧文件")
|
||||
}
|
||||
got, err := os.ReadFile(target)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(got) != string(content) {
|
||||
t.Fatalf("迁移内容 = %q, want %q", got, content)
|
||||
}
|
||||
if _, err := os.Stat(legacy); err != nil {
|
||||
t.Fatalf("旧文件不应被删除: %v", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(legacy, []byte(`{"activity":"other"}`), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
migrated, err = MigrateLegacyFile(target, legacy)
|
||||
if err != nil {
|
||||
t.Fatalf("重复迁移失败: %v", err)
|
||||
}
|
||||
if migrated {
|
||||
t.Fatal("目标已存在时不应覆盖迁移")
|
||||
}
|
||||
got, err = os.ReadFile(target)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(got) != string(content) {
|
||||
t.Fatalf("目标文件被意外覆盖: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultAppPathsUseInternalDataDir(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("DOUYU_DATA_DIR", dir)
|
||||
t.Setenv("DOUYU_CONFIG", "")
|
||||
t.Setenv("DOUYU_USERS", "")
|
||||
t.Setenv("DOUYU_LOG_DIR", "")
|
||||
t.Setenv("DOUYU_SUCCESS_DIR", "")
|
||||
|
||||
paths := DefaultAppPaths()
|
||||
if paths.DataDir != dir {
|
||||
t.Fatalf("DataDir = %q, want %q", paths.DataDir, dir)
|
||||
}
|
||||
if paths.ConfigFile != filepath.Join(dir, "config.json") {
|
||||
t.Fatalf("ConfigFile = %q", paths.ConfigFile)
|
||||
}
|
||||
if paths.AccountFile != filepath.Join(dir, "accounts.txt") {
|
||||
t.Fatalf("AccountFile = %q", paths.AccountFile)
|
||||
}
|
||||
if paths.LogDir != filepath.Join(dir, "logs") || paths.SuccessDir != filepath.Join(dir, "success_images") {
|
||||
t.Fatalf("应用数据子目录异常: logs=%q success=%q", paths.LogDir, paths.SuccessDir)
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package constants
|
||||
import "time"
|
||||
|
||||
// Version 应用版本号,与 wails.json version 同步。
|
||||
const Version = "0.1.5"
|
||||
const Version = "0.1.6"
|
||||
|
||||
// BuildTime 构建时间,由编译时 ldflags 注入。
|
||||
var BuildTime = "unknown"
|
||||
@@ -15,10 +15,15 @@ const (
|
||||
DefaultConcurrency = 150
|
||||
ProductCacheTTL = 30 * time.Second
|
||||
|
||||
DefaultConfigFile = "config.json"
|
||||
DefaultAccountFile = "data/user.txt"
|
||||
DefaultLogDir = "logs"
|
||||
DefaultSuccessDir = "success_images"
|
||||
DefaultAppDataDirName = "douyu-go"
|
||||
DefaultConfigFile = "config.json"
|
||||
DefaultAccountFile = "accounts.txt"
|
||||
DefaultLogDir = "logs"
|
||||
DefaultSuccessDir = "success_images"
|
||||
|
||||
// LegacyDefault* 是旧版本相对工作目录存储的位置,仅用于首次迁移。
|
||||
LegacyDefaultConfigFile = "config.json"
|
||||
LegacyDefaultAccountFile = "data/user.txt"
|
||||
|
||||
// DefaultTimeAPI 服务端时间校准接口。
|
||||
// 斗鱼服务器时间与活动无关,统一用精英手册的 storedetail(能稳定返回
|
||||
|
||||
@@ -8,26 +8,32 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"douyu_go/internal/config"
|
||||
"douyu_go/internal/constants"
|
||||
"douyu_go/internal/logger"
|
||||
"douyu_go/internal/types"
|
||||
)
|
||||
|
||||
// AccountStore 账号存储,负责从账号文件加载、查询账号信息。
|
||||
// AccountStore 账号存储,负责从应用内部账号库加载、查询账号信息。
|
||||
// 账号文件格式:每行 `账号名----密码----Cookie`(密码段保留以兼容历史文件,
|
||||
// 但程序不读取也不使用密码,鉴权完全基于 Cookie)。
|
||||
type AccountStore struct {
|
||||
mu sync.RWMutex
|
||||
accounts []types.Account
|
||||
filePath string
|
||||
mu sync.RWMutex
|
||||
accounts []types.Account
|
||||
filePath string
|
||||
legacyFilePath string
|
||||
}
|
||||
|
||||
// NewAccountStore 创建账号存储,filePath 为账号文件路径。
|
||||
func NewAccountStore(filePath string) *AccountStore {
|
||||
// NewAccountStore 创建账号存储。legacyFilePaths 仅在内部账号库首次创建时用于迁移旧文件。
|
||||
func NewAccountStore(filePath string, legacyFilePaths ...string) *AccountStore {
|
||||
if filePath == "" {
|
||||
filePath = constants.DefaultAccountFile
|
||||
}
|
||||
return &AccountStore{filePath: filePath}
|
||||
legacyFilePath := ""
|
||||
if len(legacyFilePaths) > 0 {
|
||||
legacyFilePath = legacyFilePaths[0]
|
||||
}
|
||||
return &AccountStore{filePath: filePath, legacyFilePath: legacyFilePath}
|
||||
}
|
||||
|
||||
// FilePath 返回账号文件路径。
|
||||
@@ -35,23 +41,33 @@ func (a *AccountStore) FilePath() string {
|
||||
return a.filePath
|
||||
}
|
||||
|
||||
// Load 从账号文件加载账号。
|
||||
// Load 从应用内部账号库加载账号。
|
||||
func (a *AccountStore) Load() {
|
||||
path := a.filePath
|
||||
|
||||
// 文件不存在时自动创建目录和空文件,方便用户直接粘贴 Cookie
|
||||
// 首次启动时迁移旧版本工作目录中的账号文件,保留原文件作为备份。
|
||||
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, 0755); mkErr != nil {
|
||||
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), 0644); wrErr != nil {
|
||||
if wrErr := os.WriteFile(path, []byte(hint), 0600); wrErr != nil {
|
||||
logger.LogEvent("账号", "warn", "账号文件创建失败: %s: %v", path, wrErr)
|
||||
} else {
|
||||
logger.LogEvent("账号", "info", "已创建空账号文件: %s,请粘贴 Cookie 后重新加载", path)
|
||||
logger.LogEvent("账号", "info", "已创建空账号库: %s,请在 CK 管理中导入账号", path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,13 +85,16 @@ func (a *AccountStore) Load() {
|
||||
normalized, converted := normalizeLineEndings(content)
|
||||
if converted {
|
||||
// 保存转换后的内容
|
||||
if err := os.WriteFile(path, normalized, 0644); err != nil {
|
||||
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))
|
||||
@@ -102,7 +121,7 @@ func (a *AccountStore) Load() {
|
||||
a.mu.Lock()
|
||||
a.accounts = list
|
||||
a.mu.Unlock()
|
||||
logger.LogEvent("账号", "success", "账号加载完成: %d 个,来源=%s", len(list), path)
|
||||
logger.LogEvent("账号", "success", "账号加载完成: %d 个,内部存储=%s", len(list), path)
|
||||
}
|
||||
|
||||
// Count 返回当前账号数。
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package seckill
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAccountStoreMigratesLegacyFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
legacy := filepath.Join(dir, "data", "user.txt")
|
||||
target := filepath.Join(dir, "app-data", "accounts.txt")
|
||||
content := []byte("# 旧账号\r\nfoo----password----cookie-a\r\n")
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(legacy), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(legacy, content, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
store := NewAccountStore(target, legacy)
|
||||
store.Load()
|
||||
accounts := store.Snapshot()
|
||||
if len(accounts) != 1 || accounts[0].Name != "foo" || accounts[0].Cookie != "cookie-a" {
|
||||
t.Fatalf("迁移后的账号 = %#v", accounts)
|
||||
}
|
||||
if _, err := os.Stat(legacy); err != nil {
|
||||
t.Fatalf("旧账号文件不应被删除: %v", err)
|
||||
}
|
||||
info, err := os.Stat(target)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if info.Mode().Perm() != 0600 {
|
||||
t.Fatalf("账号库权限 = %o, want 0600", info.Mode().Perm())
|
||||
}
|
||||
}
|
||||
@@ -129,7 +129,7 @@ func New(cli *client.DouyuClient, paths types.AppPaths, font *truetype.Font, con
|
||||
client: cli,
|
||||
paths: paths,
|
||||
font: font,
|
||||
accounts: NewAccountStore(paths.AccountFile),
|
||||
accounts: NewAccountStore(paths.AccountFile, paths.LegacyAccountFile),
|
||||
successImages: image.NewSuccessImageSaver(
|
||||
font,
|
||||
paths.SuccessDir,
|
||||
@@ -260,7 +260,7 @@ func normalizeSeckillMode(mode types.SeckillMode) types.SeckillMode {
|
||||
|
||||
// ==================== 账号转发(AccountStore) ====================
|
||||
|
||||
// LoadAccounts 从账号文件加载账号。
|
||||
// LoadAccounts 从应用内部账号库加载账号。
|
||||
func (s *Scheduler) LoadAccounts() {
|
||||
s.accounts.Load()
|
||||
}
|
||||
@@ -270,7 +270,7 @@ func (s *Scheduler) AccountCount() int {
|
||||
return s.accounts.Count()
|
||||
}
|
||||
|
||||
// AccountFilePath 返回账号文件路径。
|
||||
// AccountFilePath 返回内部账号库路径。
|
||||
func (s *Scheduler) AccountFilePath() string {
|
||||
return s.accounts.FilePath()
|
||||
}
|
||||
|
||||
@@ -72,11 +72,14 @@ const (
|
||||
|
||||
// AppPaths 运行时路径。
|
||||
type AppPaths struct {
|
||||
ConfigFile string
|
||||
AccountFile string
|
||||
LogDir string
|
||||
SuccessDir string
|
||||
FontFiles []string
|
||||
DataDir string
|
||||
ConfigFile string
|
||||
AccountFile string
|
||||
LegacyConfigFile string
|
||||
LegacyAccountFile string
|
||||
LogDir string
|
||||
SuccessDir string
|
||||
FontFiles []string
|
||||
}
|
||||
|
||||
// TraceConfig 追踪配置。
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"$schema": "https://wails.io/schemas/config.v2.json",
|
||||
"name": "douyu_go",
|
||||
"outputfilename": "douyu_go",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.6",
|
||||
"frontend:dir": "frontend",
|
||||
"frontend:install": "npm install",
|
||||
"frontend:build": "npm run build",
|
||||
|
||||
Reference in New Issue
Block a user