重构: 账号管理抽离、强类型返回、消除反射、删除死字段
- 抽离账号管理为独立 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 类型检查通过
This commit is contained in:
@@ -31,3 +31,5 @@ yml.txt
|
||||
*.app
|
||||
.DS_Store
|
||||
.workbuddy
|
||||
.claude
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ func cloneConfig(c types.Config) types.Config {
|
||||
|
||||
type exchangeLookupResult struct {
|
||||
Account types.Account
|
||||
Records []client.ExchangeRecord
|
||||
Records []types.ExchangeRecord
|
||||
Err error
|
||||
EmptyCookie bool
|
||||
}
|
||||
@@ -191,32 +191,32 @@ func (a *App) GetAccounts() []types.AccountView {
|
||||
}
|
||||
|
||||
// ReloadAccounts 重新加载账号文件。
|
||||
func (a *App) ReloadAccounts() map[string]any {
|
||||
func (a *App) ReloadAccounts() types.ReloadAccountsResult {
|
||||
a.scheduler.LoadAccounts()
|
||||
return map[string]any{"success": true, "count": a.scheduler.AccountCount()}
|
||||
return types.ReloadAccountsResult{Success: true, Count: a.scheduler.AccountCount()}
|
||||
}
|
||||
|
||||
// StartSeckill 立即启动抢购。
|
||||
func (a *App) StartSeckill(accountProducts map[string]string) map[string]any {
|
||||
func (a *App) StartSeckill(accountProducts map[string]string) types.MessageResult {
|
||||
if len(accountProducts) == 0 {
|
||||
return map[string]any{"success": false, "error": "请选择账号和商品"}
|
||||
return types.MessageResult{Error: "请选择账号和商品"}
|
||||
}
|
||||
if !a.scheduler.TryStartRun() {
|
||||
return map[string]any{"success": false, "error": "抢购任务正在运行,请勿重复提交"}
|
||||
return types.MessageResult{Error: "抢购任务正在运行,请勿重复提交"}
|
||||
}
|
||||
go a.scheduler.RunNow(utils.CloneStringMap(accountProducts))
|
||||
return map[string]any{"success": true, "msg": "抢购已启动"}
|
||||
return types.MessageResult{Success: true, Msg: "抢购已启动"}
|
||||
}
|
||||
|
||||
// SetSchedule 设置定时抢购。
|
||||
func (a *App) SetSchedule(req types.SeckillRequest) (map[string]any, error) {
|
||||
func (a *App) SetSchedule(req types.SeckillRequest) (types.ScheduleResult, error) {
|
||||
return a.scheduler.ScheduleRun(req)
|
||||
}
|
||||
|
||||
// CancelSchedule 取消未触发的定时抢购。
|
||||
func (a *App) CancelSchedule() map[string]any {
|
||||
func (a *App) CancelSchedule() types.CancelScheduleResult {
|
||||
stopped := a.scheduler.CancelSchedule()
|
||||
return map[string]any{"success": true, "stopped": stopped}
|
||||
return types.CancelScheduleResult{Success: true, Stopped: stopped}
|
||||
}
|
||||
|
||||
// GetResults 返回当前抢购结果。
|
||||
@@ -225,118 +225,112 @@ func (a *App) GetResults() map[string]string {
|
||||
}
|
||||
|
||||
// GetLogs 返回后端内存日志,支持增量获取。
|
||||
func (a *App) GetLogs(since int64) map[string]any {
|
||||
func (a *App) GetLogs(since int64) types.LogsResult {
|
||||
entries := a.logStore.Since(since)
|
||||
nextSince := since
|
||||
if len(entries) > 0 {
|
||||
nextSince = entries[len(entries)-1].ID
|
||||
}
|
||||
return map[string]any{
|
||||
"success": true,
|
||||
"logs": entries,
|
||||
"since": nextSince,
|
||||
"file": a.logStore.FilePath(),
|
||||
return types.LogsResult{
|
||||
Success: true,
|
||||
Logs: entries,
|
||||
Since: nextSince,
|
||||
File: a.logStore.FilePath(),
|
||||
}
|
||||
}
|
||||
|
||||
// GetTime 返回服务端时间和本地时间。
|
||||
func (a *App) GetTime() map[string]any {
|
||||
func (a *App) GetTime() types.TimeResult {
|
||||
serverTime, err := a.getClient().ServerTime()
|
||||
source := "currentTime"
|
||||
if err != nil {
|
||||
source = "localFallback"
|
||||
logger.LogEvent("时间", "warn", "时间接口降级: %v", err)
|
||||
}
|
||||
return map[string]any{
|
||||
"serverTime": serverTime.In(constants.CST).Format("2006-01-02 15:04:05.000"),
|
||||
"localTime": time.Now().In(constants.CST).Format("2006-01-02 15:04:05.000"),
|
||||
"source": source,
|
||||
return types.TimeResult{
|
||||
ServerTime: serverTime.In(constants.CST).Format("2006-01-02 15:04:05.000"),
|
||||
LocalTime: time.Now().In(constants.CST).Format("2006-01-02 15:04:05.000"),
|
||||
Source: source,
|
||||
}
|
||||
}
|
||||
|
||||
// ClearResults 清空抢购结果。
|
||||
func (a *App) ClearResults() map[string]any {
|
||||
func (a *App) ClearResults() types.SimpleResult {
|
||||
a.scheduler.ClearResults()
|
||||
return map[string]any{"success": true}
|
||||
return types.SimpleResult{Success: true}
|
||||
}
|
||||
|
||||
// QueryScore 查询单个账号的积分。
|
||||
func (a *App) QueryScore(accountName string) map[string]any {
|
||||
func (a *App) QueryScore(accountName string) types.ScoreResult {
|
||||
if accountName == "" {
|
||||
return map[string]any{"score": "缺少账号名"}
|
||||
return types.ScoreResult{Score: "缺少账号名"}
|
||||
}
|
||||
cookie, ok := a.scheduler.AccountCookieByName(accountName)
|
||||
if !ok || cookie == "" {
|
||||
return map[string]any{"score": fmt.Sprintf("账号不存在或 Cookie 为空: %s", accountName)}
|
||||
return types.ScoreResult{Score: fmt.Sprintf("账号不存在或 Cookie 为空: %s", accountName)}
|
||||
}
|
||||
score, err := a.getClient().Score(cookie)
|
||||
if err != nil {
|
||||
return map[string]any{"score": "查询失败: " + err.Error()}
|
||||
return types.ScoreResult{Score: "查询失败: " + err.Error()}
|
||||
}
|
||||
return map[string]any{"score": score}
|
||||
return types.ScoreResult{Score: strconv.Itoa(score)}
|
||||
}
|
||||
|
||||
// VerifyExchange 查询兑换记录,用于复验抢购结果。
|
||||
func (a *App) VerifyExchange(accountName string) map[string]any {
|
||||
func (a *App) VerifyExchange(accountName string) types.ExchangeRecordsResult {
|
||||
if accountName == "" {
|
||||
return map[string]any{"success": false, "error": "缺少账号名"}
|
||||
return types.ExchangeRecordsResult{Error: "缺少账号名"}
|
||||
}
|
||||
cookie, ok := a.scheduler.AccountCookieByName(accountName)
|
||||
if !ok || cookie == "" {
|
||||
return map[string]any{"success": false, "error": fmt.Sprintf("账号不存在或 Cookie 为空: %s", accountName)}
|
||||
return types.ExchangeRecordsResult{Error: fmt.Sprintf("账号不存在或 Cookie 为空: %s", accountName)}
|
||||
}
|
||||
|
||||
records, err := a.getClient().ExchangeList(cookie)
|
||||
if err != nil {
|
||||
return map[string]any{"success": false, "error": "查询失败: " + err.Error()}
|
||||
return types.ExchangeRecordsResult{Error: "查询失败: " + err.Error()}
|
||||
}
|
||||
matched := filterExchangeRecords(records)
|
||||
|
||||
return map[string]any{
|
||||
"success": true,
|
||||
"records": matched,
|
||||
"count": len(matched),
|
||||
return types.ExchangeRecordsResult{
|
||||
Success: true,
|
||||
Records: matched,
|
||||
Count: len(matched),
|
||||
}
|
||||
}
|
||||
|
||||
// VerifyAllExchange 批量查询兑换记录,使用有限并发减少等待时间。
|
||||
func (a *App) VerifyAllExchange() map[string]any {
|
||||
func (a *App) VerifyAllExchange() types.VerifyAllExchangeResult {
|
||||
accounts := a.scheduler.AccountSnapshot()
|
||||
results := make(map[string]any)
|
||||
results := make(map[string]types.AccountExchangeResult)
|
||||
lookups := a.lookupExchangeRecords("复验", accounts)
|
||||
|
||||
for _, lookup := range lookups {
|
||||
if lookup.EmptyCookie {
|
||||
results[lookup.Account.Name] = map[string]any{
|
||||
"success": false,
|
||||
"error": "Cookie 为空",
|
||||
}
|
||||
results[lookup.Account.Name] = types.AccountExchangeResult{Error: "Cookie 为空"}
|
||||
continue
|
||||
}
|
||||
|
||||
if lookup.Err != nil {
|
||||
results[lookup.Account.Name] = map[string]any{
|
||||
"success": false,
|
||||
"error": "查询失败: " + lookup.Err.Error(),
|
||||
}
|
||||
results[lookup.Account.Name] = types.AccountExchangeResult{Error: "查询失败: " + lookup.Err.Error()}
|
||||
continue
|
||||
}
|
||||
|
||||
matched := filterExchangeRecords(lookup.Records)
|
||||
results[lookup.Account.Name] = map[string]any{
|
||||
"success": true,
|
||||
"records": matched,
|
||||
"count": len(matched),
|
||||
results[lookup.Account.Name] = types.AccountExchangeResult{
|
||||
Success: true,
|
||||
Records: matched,
|
||||
Count: len(matched),
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
return types.VerifyAllExchangeResult{Results: results}
|
||||
}
|
||||
|
||||
// VerifyAndExport 复验并导出结果。
|
||||
func (a *App) VerifyAndExport(commodityID string) map[string]any {
|
||||
func (a *App) VerifyAndExport(commodityID string) types.VerifyAndExportResult {
|
||||
accounts := a.scheduler.AccountSnapshot()
|
||||
results := make(map[string]any)
|
||||
results := make(map[string]types.AccountHitResult)
|
||||
hitAccounts := make([]string, 0)
|
||||
missAccounts := make([]string, 0)
|
||||
lookups := a.lookupExchangeRecords("复验", accounts)
|
||||
@@ -344,19 +338,13 @@ func (a *App) VerifyAndExport(commodityID string) map[string]any {
|
||||
for _, lookup := range lookups {
|
||||
if lookup.EmptyCookie {
|
||||
missAccounts = append(missAccounts, lookup.Account.Name)
|
||||
results[lookup.Account.Name] = map[string]any{
|
||||
"success": false,
|
||||
"error": "Cookie 为空",
|
||||
}
|
||||
results[lookup.Account.Name] = types.AccountHitResult{Error: "Cookie 为空"}
|
||||
continue
|
||||
}
|
||||
|
||||
if lookup.Err != nil {
|
||||
missAccounts = append(missAccounts, lookup.Account.Name)
|
||||
results[lookup.Account.Name] = map[string]any{
|
||||
"success": false,
|
||||
"error": "查询失败: " + lookup.Err.Error(),
|
||||
}
|
||||
results[lookup.Account.Name] = types.AccountHitResult{Error: "查询失败: " + lookup.Err.Error()}
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -364,26 +352,20 @@ func (a *App) VerifyAndExport(commodityID string) map[string]any {
|
||||
hit := hasCommodityRecord(lookup.Records, commodityID, 0, 0)
|
||||
if hit {
|
||||
hitAccounts = append(hitAccounts, lookup.Account.Name)
|
||||
results[lookup.Account.Name] = map[string]any{
|
||||
"success": true,
|
||||
"hit": true,
|
||||
}
|
||||
results[lookup.Account.Name] = types.AccountHitResult{Success: true, Hit: true}
|
||||
} else {
|
||||
missAccounts = append(missAccounts, lookup.Account.Name)
|
||||
results[lookup.Account.Name] = map[string]any{
|
||||
"success": true,
|
||||
"hit": false,
|
||||
}
|
||||
results[lookup.Account.Name] = types.AccountHitResult{Success: true}
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"success": true,
|
||||
"hitCount": len(hitAccounts),
|
||||
"missCount": len(missAccounts),
|
||||
"hitAccounts": hitAccounts,
|
||||
"missAccounts": missAccounts,
|
||||
"details": results,
|
||||
return types.VerifyAndExportResult{
|
||||
Success: true,
|
||||
HitCount: len(hitAccounts),
|
||||
MissCount: len(missAccounts),
|
||||
HitAccounts: hitAccounts,
|
||||
MissAccounts: missAccounts,
|
||||
Details: results,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,8 +428,8 @@ func (a *App) lookupExchangeRecords(category string, accounts []types.Account) [
|
||||
return results
|
||||
}
|
||||
|
||||
func filterExchangeRecords(records []client.ExchangeRecord) []client.ExchangeRecord {
|
||||
matched := make([]client.ExchangeRecord, 0, len(records))
|
||||
func filterExchangeRecords(records []types.ExchangeRecord) []types.ExchangeRecord {
|
||||
matched := make([]types.ExchangeRecord, 0, len(records))
|
||||
for _, r := range records {
|
||||
if r.CommodityID != "" {
|
||||
matched = append(matched, r)
|
||||
@@ -456,7 +438,7 @@ func filterExchangeRecords(records []client.ExchangeRecord) []client.ExchangeRec
|
||||
return matched
|
||||
}
|
||||
|
||||
func hasCommodityRecord(records []client.ExchangeRecord, commodityID string, startTime, endTime int64) bool {
|
||||
func hasCommodityRecord(records []types.ExchangeRecord, commodityID string, startTime, endTime int64) bool {
|
||||
if commodityID == "" {
|
||||
return false
|
||||
}
|
||||
@@ -514,15 +496,15 @@ func parseAccountFile(path string) ([]accountLine, error) {
|
||||
}
|
||||
|
||||
// ExportAccounts 导出指定账号到文件。
|
||||
func (a *App) ExportAccounts(accountNames []string, filename string) map[string]any {
|
||||
func (a *App) ExportAccounts(accountNames []string, filename string) types.ExportAccountsResult {
|
||||
if len(accountNames) == 0 {
|
||||
return map[string]any{"success": false, "error": "没有要导出的账号"}
|
||||
return types.ExportAccountsResult{Error: "没有要导出的账号"}
|
||||
}
|
||||
|
||||
accountFile := a.scheduler.AccountFilePath()
|
||||
lines, err := parseAccountFile(accountFile)
|
||||
if err != nil {
|
||||
return map[string]any{"success": false, "error": "读取账号文件失败: " + err.Error()}
|
||||
return types.ExportAccountsResult{Error: "读取账号文件失败: " + err.Error()}
|
||||
}
|
||||
|
||||
nameSet := make(map[string]bool)
|
||||
@@ -541,7 +523,7 @@ func (a *App) ExportAccounts(accountNames []string, filename string) map[string]
|
||||
}
|
||||
|
||||
if len(matchedLines) == 0 {
|
||||
return map[string]any{"success": false, "error": "没有找到匹配的账号行"}
|
||||
return types.ExportAccountsResult{Error: "没有找到匹配的账号行"}
|
||||
}
|
||||
|
||||
// 生成导出文件路径
|
||||
@@ -549,20 +531,20 @@ func (a *App) ExportAccounts(accountNames []string, filename string) map[string]
|
||||
content := strings.Join(matchedLines, "\n") + "\n"
|
||||
|
||||
if err := os.WriteFile(exportPath, []byte(content), 0644); err != nil {
|
||||
return map[string]any{"success": false, "error": "写入文件失败: " + err.Error()}
|
||||
return types.ExportAccountsResult{Error: "写入文件失败: " + err.Error()}
|
||||
}
|
||||
|
||||
logger.LogEvent("导出", "success", "已导出 %d 个账号到: %s", len(matchedLines), exportPath)
|
||||
|
||||
return map[string]any{
|
||||
"success": true,
|
||||
"count": len(matchedLines),
|
||||
"filePath": exportPath,
|
||||
return types.ExportAccountsResult{
|
||||
Success: true,
|
||||
Count: len(matchedLines),
|
||||
FilePath: exportPath,
|
||||
}
|
||||
}
|
||||
|
||||
// ExportAccountsWithProduct 导出指定账号到文件,自动使用中文商品名和时间戳命名。
|
||||
func (a *App) ExportAccountsWithProduct(accountNames []string, commodityID, commodityName string) map[string]any {
|
||||
func (a *App) ExportAccountsWithProduct(accountNames []string, commodityID, commodityName string) types.ExportAccountsResult {
|
||||
// 生成文件名: user_已抢到_商品中文名_20260626_150405.txt
|
||||
now := time.Now()
|
||||
timestamp := now.Format("20060102_150405")
|
||||
@@ -574,7 +556,7 @@ func (a *App) ExportAccountsWithProduct(accountNames []string, commodityID, comm
|
||||
}
|
||||
|
||||
// OrganizeAccounts 整理账号,把已抢到指定商品的账号分离出来。
|
||||
func (a *App) OrganizeAccounts(commodityID, commodityName string, startTime, endTime int64) map[string]any {
|
||||
func (a *App) OrganizeAccounts(commodityID, commodityName string, startTime, endTime int64) types.OrganizeAccountsResult {
|
||||
accounts := a.scheduler.AccountSnapshot()
|
||||
hitAccounts := make([]string, 0)
|
||||
missAccounts := make([]string, 0)
|
||||
@@ -594,7 +576,7 @@ func (a *App) OrganizeAccounts(commodityID, commodityName string, startTime, end
|
||||
}
|
||||
|
||||
// 导出已抢到的账号
|
||||
var exportResult map[string]any
|
||||
var exportResult types.ExportAccountsResult
|
||||
if len(hitAccounts) > 0 {
|
||||
exportResult = a.ExportAccountsWithProduct(hitAccounts, commodityID, commodityName)
|
||||
}
|
||||
@@ -603,7 +585,7 @@ func (a *App) OrganizeAccounts(commodityID, commodityName string, startTime, end
|
||||
accountFile := a.scheduler.AccountFilePath()
|
||||
lines, err := parseAccountFile(accountFile)
|
||||
if err != nil {
|
||||
return map[string]any{"success": false, "error": "读取账号文件失败: " + err.Error()}
|
||||
return types.OrganizeAccountsResult{Error: "读取账号文件失败: " + err.Error()}
|
||||
}
|
||||
|
||||
missSet := make(map[string]bool)
|
||||
@@ -625,7 +607,7 @@ func (a *App) OrganizeAccounts(commodityID, commodityName string, startTime, end
|
||||
// 写回原文件
|
||||
newContent := strings.Join(keepLines, "\n") + "\n"
|
||||
if err := os.WriteFile(accountFile, []byte(newContent), 0644); err != nil {
|
||||
return map[string]any{"success": false, "error": "更新账号文件失败: " + err.Error()}
|
||||
return types.OrganizeAccountsResult{Error: "更新账号文件失败: " + err.Error()}
|
||||
}
|
||||
|
||||
// 重新加载账号
|
||||
@@ -633,13 +615,13 @@ func (a *App) OrganizeAccounts(commodityID, commodityName string, startTime, end
|
||||
|
||||
logger.LogEvent("整理", "success", "整理完成: 已抢到=%d, 未抢到=%d", len(hitAccounts), len(missAccounts))
|
||||
|
||||
return map[string]any{
|
||||
"success": true,
|
||||
"hitCount": len(hitAccounts),
|
||||
"missCount": len(missAccounts),
|
||||
"hitAccounts": hitAccounts,
|
||||
"missAccounts": missAccounts,
|
||||
"exportResult": exportResult,
|
||||
return types.OrganizeAccountsResult{
|
||||
Success: true,
|
||||
HitCount: len(hitAccounts),
|
||||
MissCount: len(missAccounts),
|
||||
HitAccounts: hitAccounts,
|
||||
MissAccounts: missAccounts,
|
||||
ExportResult: exportResult,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -669,10 +651,10 @@ func (a *App) GetCurrentActivity() types.ActivityInfo {
|
||||
}
|
||||
|
||||
// SwitchActivity 切换当前活动并持久化到配置;切换后重建客户端与时间基准。
|
||||
func (a *App) SwitchActivity(id string) map[string]any {
|
||||
func (a *App) SwitchActivity(id string) types.SwitchActivityResult {
|
||||
act := activity.Get(id)
|
||||
if act == nil {
|
||||
return map[string]any{"success": false, "error": "未知活动: " + id}
|
||||
return types.SwitchActivityResult{Error: "未知活动: " + id}
|
||||
}
|
||||
cfg := a.getCfg()
|
||||
cfg.Activity = act.ID
|
||||
@@ -682,7 +664,7 @@ func (a *App) SwitchActivity(id string) map[string]any {
|
||||
}
|
||||
if err := config.SaveConfig(a.paths.ConfigFile, cfg); err != nil {
|
||||
logger.LogEvent("活动", "error", "切换活动配置保存失败: %v", err)
|
||||
return map[string]any{"success": false, "error": "配置保存失败: " + err.Error()}
|
||||
return types.SwitchActivityResult{Error: "配置保存失败: " + err.Error()}
|
||||
}
|
||||
a.setCfg(cfg)
|
||||
newClient := client.New(act, cfg.TimeAPIURL, a.trace)
|
||||
@@ -694,7 +676,7 @@ func (a *App) SwitchActivity(id string) map[string]any {
|
||||
logger.SetDouyuOffset(sample.Offset)
|
||||
}
|
||||
logger.LogEvent("活动", "success", "已切换到活动: %s", act.DisplayName)
|
||||
return map[string]any{"success": true, "activity": act.ID, "displayName": act.DisplayName}
|
||||
return types.SwitchActivityResult{Success: true, Activity: act.ID, DisplayName: act.DisplayName}
|
||||
}
|
||||
|
||||
// GetSettings 返回当前配置。
|
||||
@@ -703,7 +685,7 @@ func (a *App) GetSettings() types.Config {
|
||||
}
|
||||
|
||||
// SaveSettings 保存配置并运行时生效。
|
||||
func (a *App) SaveSettings(cfg types.Config) map[string]any {
|
||||
func (a *App) SaveSettings(cfg types.Config) types.SimpleResult {
|
||||
// 校验
|
||||
if cfg.Concurrency <= 0 {
|
||||
cfg.Concurrency = constants.DefaultConcurrency
|
||||
@@ -718,7 +700,7 @@ func (a *App) SaveSettings(cfg types.Config) map[string]any {
|
||||
// 写入文件
|
||||
if err := config.SaveConfig(a.paths.ConfigFile, cfg); err != nil {
|
||||
logger.LogEvent("配置", "error", "配置保存失败: %v", err)
|
||||
return map[string]any{"success": false, "error": err.Error()}
|
||||
return types.SimpleResult{}
|
||||
}
|
||||
|
||||
// 运行时生效
|
||||
@@ -728,7 +710,7 @@ func (a *App) SaveSettings(cfg types.Config) map[string]any {
|
||||
|
||||
logger.LogEvent("配置", "success", "配置已保存并生效: 日志等级=%s 并发=%d 重试=%d 重试阶段=%d 模式=%d trace=%v",
|
||||
cfg.LogLevel, cfg.Concurrency, cfg.Attempts, len(cfg.RetryStages), cfg.SeckillMode, cfg.TraceEnabled)
|
||||
return map[string]any{"success": true}
|
||||
return types.SimpleResult{Success: true}
|
||||
}
|
||||
|
||||
// ==================== 内部辅助方法 ====================
|
||||
|
||||
+18
-41
@@ -7,7 +7,6 @@ import (
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -498,29 +497,23 @@ func (c *DouyuClient) RoleName(cookie string) (string, error) {
|
||||
return result.Data.GameRole.RoleName, nil
|
||||
}
|
||||
|
||||
// ExchangeRecord 兑换记录。
|
||||
type ExchangeRecord struct {
|
||||
CommodityID string `json:"commodityId"`
|
||||
CommodityName string `json:"commodityName"`
|
||||
NickName string `json:"nickName"`
|
||||
ShowName string `json:"showName"`
|
||||
ExchangeNum int `json:"exchangeNum"`
|
||||
Time string `json:"time"`
|
||||
Score string `json:"score"`
|
||||
}
|
||||
|
||||
// ExchangeListResponse 兑换列表响应。
|
||||
type ExchangeListResponse struct {
|
||||
Error int `json:"error"`
|
||||
Msg string `json:"msg"`
|
||||
Data struct {
|
||||
List []ExchangeRecord `json:"list"`
|
||||
Count int `json:"count"`
|
||||
List []types.ExchangeRecord `json:"list"`
|
||||
Count int `json:"count"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// APIStatus 返回斗鱼 API 的 error/msg 状态,实现 apiResponder 接口。
|
||||
func (r ExchangeListResponse) APIStatus() (int, string, bool) {
|
||||
return r.Error, r.Msg, true
|
||||
}
|
||||
|
||||
// ExchangeList 查询兑换记录列表。
|
||||
func (c *DouyuClient) ExchangeList(cookie string) ([]ExchangeRecord, error) {
|
||||
func (c *DouyuClient) ExchangeList(cookie string) ([]types.ExchangeRecord, error) {
|
||||
var result ExchangeListResponse
|
||||
if err := c.getJSON(c.Activity.ExchangeListURL, cookie, "兑换记录", &result); err != nil {
|
||||
return nil, err
|
||||
@@ -675,36 +668,20 @@ func extractAPIStatus(out any) (int, string, bool) {
|
||||
if out == nil {
|
||||
return 0, "", false
|
||||
}
|
||||
v := reflect.ValueOf(out)
|
||||
if v.Kind() == reflect.Pointer {
|
||||
if v.IsNil() {
|
||||
return 0, "", false
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
if !v.IsValid() {
|
||||
return 0, "", false
|
||||
}
|
||||
if v.Type() == reflect.TypeOf(types.APIResponse{}) {
|
||||
api := v.Interface().(types.APIResponse)
|
||||
return api.Error, api.Msg, true
|
||||
}
|
||||
if v.Kind() != reflect.Struct {
|
||||
return 0, "", false
|
||||
}
|
||||
if field := v.FieldByName("APIResponse"); field.IsValid() && field.CanInterface() {
|
||||
if api, ok := field.Interface().(types.APIResponse); ok {
|
||||
return api.Error, api.Msg, true
|
||||
}
|
||||
}
|
||||
errorField := v.FieldByName("Error")
|
||||
msgField := v.FieldByName("Msg")
|
||||
if errorField.IsValid() && msgField.IsValid() && errorField.CanInt() && msgField.Kind() == reflect.String {
|
||||
return int(errorField.Int()), msgField.String(), true
|
||||
if r, ok := out.(apiResponder); ok {
|
||||
return r.APIStatus()
|
||||
}
|
||||
return 0, "", false
|
||||
}
|
||||
|
||||
// apiResponder 返回斗鱼 API 的 error/msg 状态。
|
||||
// types.APIResponse 通过其方法满足该接口;
|
||||
// 未嵌入 APIResponse 但自带 Error/Msg 字段的结构体(如 ExchangeListResponse)
|
||||
// 通过实现 APIStatus 方法满足该接口,避免使用反射逐字段查找。
|
||||
type apiResponder interface {
|
||||
APIStatus() (int, string, bool)
|
||||
}
|
||||
|
||||
func (c *DouyuClient) setBrowserHeaders(req *http.Request, cookie string) {
|
||||
req.Header.Set("Accept", "application/json, text/plain, */*")
|
||||
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en-US;q=0.6,en;q=0.5")
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
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
|
||||
}
|
||||
+23
-136
@@ -1,13 +1,9 @@
|
||||
package seckill
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -68,8 +64,7 @@ type Scheduler struct {
|
||||
retryStages []types.RetryStage // 重试间隔阶段配置
|
||||
seckillMode types.SeckillMode // 抢购模式
|
||||
|
||||
accountsMu sync.RWMutex
|
||||
accounts []types.Account
|
||||
accounts *AccountStore
|
||||
|
||||
resultsMu sync.RWMutex
|
||||
results map[string]string
|
||||
@@ -103,9 +98,10 @@ func New(cli *client.DouyuClient, paths types.AppPaths, font *truetype.Font, con
|
||||
attempts = constants.DefaultAttempts
|
||||
}
|
||||
return &Scheduler{
|
||||
client: cli,
|
||||
paths: paths,
|
||||
font: font,
|
||||
client: cli,
|
||||
paths: paths,
|
||||
font: font,
|
||||
accounts: NewAccountStore(paths.AccountFile),
|
||||
successImages: image.NewSuccessImageSaver(
|
||||
font,
|
||||
paths.SuccessDir,
|
||||
@@ -227,136 +223,27 @@ func normalizeSeckillMode(mode types.SeckillMode) types.SeckillMode {
|
||||
|
||||
// LoadAccounts 从账号文件加载账号。
|
||||
func (s *Scheduler) LoadAccounts() {
|
||||
path := s.paths.AccountFile
|
||||
if path == "" {
|
||||
path = constants.DefaultAccountFile
|
||||
}
|
||||
|
||||
// 文件不存在时自动创建目录和空文件,方便用户直接粘贴 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)
|
||||
s.accountsMu.Lock()
|
||||
s.accounts = nil
|
||||
s.accountsMu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// 检测并转换换行格式
|
||||
normalized, converted := s.normalizeLineEndings(path, 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]),
|
||||
Password: strings.TrimSpace(parts[1]),
|
||||
Cookie: strings.TrimSpace(parts[2]),
|
||||
})
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
logger.LogEvent("账号", "error", "读取账号文件中断: %v", err)
|
||||
}
|
||||
|
||||
s.accountsMu.Lock()
|
||||
s.accounts = list
|
||||
s.accountsMu.Unlock()
|
||||
logger.LogEvent("账号", "success", "账号加载完成: %d 个,来源=%s", len(list), path)
|
||||
s.accounts.Load()
|
||||
}
|
||||
|
||||
// AccountCount 返回当前账号数。
|
||||
func (s *Scheduler) AccountCount() int {
|
||||
s.accountsMu.RLock()
|
||||
defer s.accountsMu.RUnlock()
|
||||
return len(s.accounts)
|
||||
}
|
||||
|
||||
// normalizeLineEndings 检测并转换换行格式为 Windows 格式(\r\n)。
|
||||
// 返回转换后的内容和是否进行了转换。
|
||||
func (s *Scheduler) normalizeLineEndings(path string, 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
|
||||
return s.accounts.Count()
|
||||
}
|
||||
|
||||
// AccountFilePath 返回账号文件路径。
|
||||
func (s *Scheduler) AccountFilePath() string {
|
||||
return s.paths.AccountFile
|
||||
return s.accounts.FilePath()
|
||||
}
|
||||
|
||||
// AccountSnapshot 复制一份账号列表。
|
||||
func (s *Scheduler) AccountSnapshot() []types.Account {
|
||||
s.accountsMu.RLock()
|
||||
defer s.accountsMu.RUnlock()
|
||||
list := make([]types.Account, len(s.accounts))
|
||||
copy(list, s.accounts)
|
||||
return list
|
||||
return s.accounts.Snapshot()
|
||||
}
|
||||
|
||||
// AccountCookieByName 按账号名查找 Cookie。
|
||||
func (s *Scheduler) AccountCookieByName(name string) (string, bool) {
|
||||
s.accountsMu.RLock()
|
||||
defer s.accountsMu.RUnlock()
|
||||
for _, account := range s.accounts {
|
||||
if account.Name == name {
|
||||
return account.Cookie, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
return s.accounts.CookieByName(name)
|
||||
}
|
||||
|
||||
// Results 返回当前抢购结果快照。
|
||||
@@ -1115,17 +1002,17 @@ func shouldFallbackStandard(errMsg string) bool {
|
||||
}
|
||||
|
||||
// ScheduleRun 注册一次定时抢购。
|
||||
func (s *Scheduler) ScheduleRun(req types.SeckillRequest) (map[string]any, error) {
|
||||
func (s *Scheduler) ScheduleRun(req types.SeckillRequest) (types.ScheduleResult, error) {
|
||||
if len(req.AccountProducts) == 0 {
|
||||
return nil, errors.New("请选择账号和商品")
|
||||
return types.ScheduleResult{}, errors.New("请选择账号和商品")
|
||||
}
|
||||
if strings.TrimSpace(req.TargetTime) == "" {
|
||||
return nil, errors.New("请设置目标时间")
|
||||
return types.ScheduleResult{}, errors.New("请设置目标时间")
|
||||
}
|
||||
|
||||
targetLocal, err := time.ParseInLocation("2006-01-02 15:04:05", req.TargetTime, constants.CST)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("时间格式错误: %w", err)
|
||||
return types.ScheduleResult{}, fmt.Errorf("时间格式错误: %w", err)
|
||||
}
|
||||
targetTime := targetLocal.UTC()
|
||||
|
||||
@@ -1135,7 +1022,7 @@ func (s *Scheduler) ScheduleRun(req types.SeckillRequest) (map[string]any, error
|
||||
triggerTime := targetTime.Add(-internalAdvance)
|
||||
remain := triggerTime.Sub(serverTime)
|
||||
if remain <= 0 {
|
||||
return nil, errors.New("目标时间已过")
|
||||
return types.ScheduleResult{}, errors.New("目标时间已过")
|
||||
}
|
||||
|
||||
ap := utils.CloneStringMap(req.AccountProducts)
|
||||
@@ -1180,14 +1067,14 @@ func (s *Scheduler) ScheduleRun(req types.SeckillRequest) (map[string]any, error
|
||||
remainSecond = 0
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"success": true,
|
||||
"targetTime": targetTime.In(constants.CST).Format("2006-01-02 15:04:05"),
|
||||
"triggerTime": triggerTime.In(constants.CST).Format("2006-01-02 15:04:05"),
|
||||
"remainSecond": remainSecond,
|
||||
"offsetMs": calibration.Offset.Milliseconds(),
|
||||
"errorBoundMs": calibration.ErrorBound.Milliseconds(),
|
||||
"rttMs": calibration.RTT.Milliseconds(),
|
||||
return types.ScheduleResult{
|
||||
Success: true,
|
||||
TargetTime: targetTime.In(constants.CST).Format("2006-01-02 15:04:05"),
|
||||
TriggerTime: triggerTime.In(constants.CST).Format("2006-01-02 15:04:05"),
|
||||
RemainSecond: remainSecond,
|
||||
OffsetMs: calibration.Offset.Milliseconds(),
|
||||
ErrorBoundMs: calibration.ErrorBound.Milliseconds(),
|
||||
RttMs: calibration.RTT.Milliseconds(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
package types
|
||||
|
||||
// 本文件定义 Wails 绑定方法的结构化返回类型,替代原先的 map[string]any。
|
||||
// 字段名与历史 map key 保持一致,前端无需改动。
|
||||
// 使用 omitempty 让 error 在成功时不出现在 JSON 中。
|
||||
|
||||
// SimpleResult 仅含 success 标志的结果。
|
||||
type SimpleResult struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
// ErrorResult 携带错误信息的结果。
|
||||
type ErrorResult struct {
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// MessageResult 携带成功消息的结果。
|
||||
type MessageResult struct {
|
||||
Success bool `json:"success"`
|
||||
Msg string `json:"msg,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// CountResult 携带计数的通用结果。
|
||||
type CountResult struct {
|
||||
Success bool `json:"success"`
|
||||
Count int `json:"count"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// ReloadAccountsResult 重新加载账号结果。
|
||||
type ReloadAccountsResult struct {
|
||||
Success bool `json:"success"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// CancelScheduleResult 取消定时抢购结果。
|
||||
type CancelScheduleResult struct {
|
||||
Success bool `json:"success"`
|
||||
Stopped bool `json:"stopped"`
|
||||
}
|
||||
|
||||
// LogsResult 增量日志查询结果。
|
||||
type LogsResult struct {
|
||||
Success bool `json:"success"`
|
||||
Logs []LogEntry `json:"logs"`
|
||||
Since int64 `json:"since"`
|
||||
File string `json:"file"`
|
||||
}
|
||||
|
||||
// TimeResult 服务端/本地时间查询结果。
|
||||
type TimeResult struct {
|
||||
ServerTime string `json:"serverTime"`
|
||||
LocalTime string `json:"localTime"`
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
// ScoreResult 积分查询结果。score 为字符串:成功时为数字字符串,失败时为错误描述。
|
||||
type ScoreResult struct {
|
||||
Score string `json:"score"`
|
||||
}
|
||||
|
||||
// ExchangeRecordsResult 兑换记录查询结果。
|
||||
type ExchangeRecordsResult struct {
|
||||
Success bool `json:"success"`
|
||||
Records []ExchangeRecord `json:"records,omitempty"`
|
||||
Count int `json:"count"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// AccountExchangeResult 单账号兑换记录查询结果(VerifyAllExchange 的 value)。
|
||||
type AccountExchangeResult struct {
|
||||
Success bool `json:"success"`
|
||||
Records []ExchangeRecord `json:"records,omitempty"`
|
||||
Count int `json:"count"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// AccountHitResult 单账号命中结果(VerifyAndExport 的 value)。
|
||||
type AccountHitResult struct {
|
||||
Success bool `json:"success"`
|
||||
Hit bool `json:"hit,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// VerifyAndExportResult 复验并导出结果。
|
||||
type VerifyAndExportResult struct {
|
||||
Success bool `json:"success"`
|
||||
HitCount int `json:"hitCount"`
|
||||
MissCount int `json:"missCount"`
|
||||
HitAccounts []string `json:"hitAccounts"`
|
||||
MissAccounts []string `json:"missAccounts"`
|
||||
Details map[string]AccountHitResult `json:"details"`
|
||||
}
|
||||
|
||||
// VerifyAllExchangeResult 批量复验结果。
|
||||
type VerifyAllExchangeResult struct {
|
||||
Results map[string]AccountExchangeResult `json:"results"`
|
||||
}
|
||||
|
||||
// ExportAccountsResult 导出账号结果。
|
||||
type ExportAccountsResult struct {
|
||||
Success bool `json:"success"`
|
||||
Count int `json:"count"`
|
||||
FilePath string `json:"filePath,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// OrganizeAccountsResult 整理账号结果。
|
||||
type OrganizeAccountsResult struct {
|
||||
Success bool `json:"success"`
|
||||
HitCount int `json:"hitCount"`
|
||||
MissCount int `json:"missCount"`
|
||||
HitAccounts []string `json:"hitAccounts"`
|
||||
MissAccounts []string `json:"missAccounts"`
|
||||
ExportResult ExportAccountsResult `json:"exportResult,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// SwitchActivityResult 切换活动结果。
|
||||
type SwitchActivityResult struct {
|
||||
Success bool `json:"success"`
|
||||
Activity string `json:"activity,omitempty"`
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// ScheduleResult 定时抢购设置结果。
|
||||
type ScheduleResult struct {
|
||||
Success bool `json:"success"`
|
||||
TargetTime string `json:"targetTime"`
|
||||
TriggerTime string `json:"triggerTime"`
|
||||
RemainSecond int `json:"remainSecond"`
|
||||
OffsetMs int64 `json:"offsetMs"`
|
||||
ErrorBoundMs int64 `json:"errorBoundMs"`
|
||||
RttMs int64 `json:"rttMs"`
|
||||
}
|
||||
+19
-3
@@ -54,10 +54,10 @@ type TraceConfig struct {
|
||||
}
|
||||
|
||||
// Account 账号信息。
|
||||
// 密码段保留在账号文件中以兼容历史格式,但程序不读取也不使用密码,鉴权完全基于 Cookie。
|
||||
type Account struct {
|
||||
Name string `json:"name"`
|
||||
Password string `json:"password"`
|
||||
Cookie string `json:"cookie"`
|
||||
Name string `json:"name"`
|
||||
Cookie string `json:"cookie"`
|
||||
}
|
||||
|
||||
// Product 商品信息。
|
||||
@@ -106,6 +106,11 @@ type APIResponse struct {
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
// APIStatus 返回斗鱼 API 的 error/msg 状态,实现 client.apiResponder 接口。
|
||||
func (a APIResponse) APIStatus() (int, string, bool) {
|
||||
return a.Error, a.Msg, true
|
||||
}
|
||||
|
||||
// LogEntry 日志条目。
|
||||
type LogEntry struct {
|
||||
ID int64 `json:"id"`
|
||||
@@ -193,3 +198,14 @@ type AttemptCache struct {
|
||||
Qualified bool
|
||||
LastCheck time.Time
|
||||
}
|
||||
|
||||
// ExchangeRecord 兑换记录。
|
||||
type ExchangeRecord struct {
|
||||
CommodityID string `json:"commodityId"`
|
||||
CommodityName string `json:"commodityName"`
|
||||
NickName string `json:"nickName"`
|
||||
ShowName string `json:"showName"`
|
||||
ExchangeNum int `json:"exchangeNum"`
|
||||
Time string `json:"time"`
|
||||
Score string `json:"score"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user