- 实例配置独立:支持为每个实例单独配置 http/https/socks5 代理,避免单机多开挤兑触发斗鱼出口 IP 限流 - 动态 API 提取:原生支持星空代理等平台的 API 提取(兼容 JSON/文本格式),提供前端一键提取填入与连通性/延迟测速 - 定时前自动拉取:针对 3 分钟短效代理,支持在开抢前 15 秒自动提取并绑定最新 IP,并在预热前完成热连接与时间复校 - 修复复校竞态:将 resyncBeforeTrigger 从 3s 提升至 8s,确保三轮采样在开抢前顺利落盘,消除时钟漂移 - 验证方式:通过 go test ./... 全量测试与前端 vue-tsc/vite 编译,实测通过星空代理 API 解析测试用例
140 lines
4.9 KiB
Go
140 lines
4.9 KiB
Go
package app
|
|
|
|
import (
|
|
"douyu_go/internal/activity"
|
|
"douyu_go/internal/client"
|
|
"douyu_go/internal/config"
|
|
"douyu_go/internal/constants"
|
|
"douyu_go/internal/logger"
|
|
"douyu_go/internal/types"
|
|
)
|
|
|
|
// GetActivities 返回所有已注册的手册活动,供前端下拉选择。
|
|
func (a *App) GetActivities() []types.ActivityInfo {
|
|
list := activity.List()
|
|
out := make([]types.ActivityInfo, 0, len(list))
|
|
for _, act := range list {
|
|
out = append(out, types.ActivityInfo{ID: act.ID, DisplayName: act.DisplayName})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// GetCurrentActivity 返回当前生效的活动。
|
|
func (a *App) GetCurrentActivity() types.ActivityInfo {
|
|
cli := a.getClient()
|
|
act := cli.Activity
|
|
return types.ActivityInfo{ID: act.ID, DisplayName: act.DisplayName}
|
|
}
|
|
|
|
// SwitchActivity 切换当前活动并持久化到配置;切换后重建客户端与时间基准。
|
|
func (a *App) SwitchActivity(id string) types.SwitchActivityResult {
|
|
act := activity.Get(id)
|
|
if act == nil {
|
|
return types.SwitchActivityResult{Error: "未知活动: " + id}
|
|
}
|
|
cfg := a.getCfg()
|
|
cfg.Activity = act.ID
|
|
// 仅当用户未自定义时间校准接口时才回退默认值,避免切换活动静默覆盖用户在设置里填的 timeApiUrl。
|
|
if cfg.TimeAPIURL == "" {
|
|
cfg.TimeAPIURL = constants.DefaultTimeAPI
|
|
}
|
|
if err := config.SaveConfig(a.paths.ConfigFile, cfg); err != nil {
|
|
logger.LogEvent("活动", "error", "切换活动配置保存失败: %v", err)
|
|
return types.SwitchActivityResult{Error: "配置保存失败: " + err.Error()}
|
|
}
|
|
a.setCfg(cfg)
|
|
newClient := client.New(act, cfg.TimeAPIURL, a.getTrace(), a.effectiveProxyURL(cfg))
|
|
a.setClient(newClient)
|
|
if !a.scheduler.UpdateClient(newClient) {
|
|
logger.LogEvent("活动", "warn", "抢购任务正在运行,调度器客户端将在下次抢购时生效")
|
|
}
|
|
if sample, err := newClient.ServerTimeSample(); err == nil {
|
|
logger.SetDouyuOffset(sample.Offset)
|
|
}
|
|
logger.LogEvent("活动", "success", "已切换到活动: %s", act.DisplayName)
|
|
return types.SwitchActivityResult{Success: true, Activity: act.ID, DisplayName: act.DisplayName}
|
|
}
|
|
|
|
// GetSettings 返回当前配置。
|
|
func (a *App) GetSettings() types.Config {
|
|
return a.getCfg()
|
|
}
|
|
|
|
// SaveSettings 保存配置并运行时生效。
|
|
func (a *App) SaveSettings(cfg types.Config) types.SimpleResult {
|
|
oldCfg := a.getCfg()
|
|
if cfg.Activity == "" {
|
|
cfg.Activity = oldCfg.Activity
|
|
}
|
|
act := activity.Get(cfg.Activity)
|
|
if act == nil {
|
|
unknownActivity := cfg.Activity
|
|
act = activity.Default()
|
|
cfg.Activity = act.ID
|
|
logger.LogEvent("配置", "warn", "保存设置时活动 %q 未识别,已回退到默认活动 %s", unknownActivity, act.DisplayName)
|
|
}
|
|
if cfg.TimeAPIURL == "" {
|
|
cfg.TimeAPIURL = constants.DefaultTimeAPI
|
|
}
|
|
if cfg.TraceMaxBody < 0 {
|
|
cfg.TraceMaxBody = 0
|
|
}
|
|
if cfg.XiaodianPageSize <= 0 || cfg.XiaodianPageSize > 200 {
|
|
cfg.XiaodianPageSize = 200
|
|
}
|
|
|
|
// 校验
|
|
if cfg.Concurrency <= 0 {
|
|
cfg.Concurrency = constants.DefaultConcurrency
|
|
}
|
|
if cfg.Attempts <= 0 {
|
|
cfg.Attempts = constants.DefaultAttempts
|
|
}
|
|
if cfg.LogLevel == "" {
|
|
cfg.LogLevel = "info"
|
|
}
|
|
|
|
// 写入文件
|
|
if err := config.SaveConfig(a.paths.ConfigFile, cfg); err != nil {
|
|
logger.LogEvent("配置", "error", "配置保存失败: %v", err)
|
|
return types.SimpleResult{}
|
|
}
|
|
|
|
traceChanged := cfg.TraceEnabled != oldCfg.TraceEnabled || cfg.TraceMaxBody != oldCfg.TraceMaxBody
|
|
proxyChanged := cfg.ProxyEnabled != oldCfg.ProxyEnabled || cfg.ProxyURL != oldCfg.ProxyURL
|
|
clientChanged := traceChanged || cfg.TimeAPIURL != oldCfg.TimeAPIURL || cfg.Activity != oldCfg.Activity || proxyChanged
|
|
if traceChanged {
|
|
a.replaceTrace(cfg)
|
|
}
|
|
|
|
// 运行时生效
|
|
a.setCfg(cfg)
|
|
logger.SetMinLevel(cfg.LogLevel)
|
|
a.scheduler.UpdateSettings(cfg.Concurrency, cfg.Attempts, cfg.RetryStages, cfg.SeckillMode)
|
|
a.scheduler.UpdateProxySettings(cfg.ProxyEnabled, cfg.ProxyURL, cfg.ProxyAPI, cfg.AutoFetchProxyOnSchedule)
|
|
if clientChanged {
|
|
proxyURL := a.effectiveProxyURL(cfg)
|
|
newClient := client.New(act, cfg.TimeAPIURL, a.getTrace(), proxyURL)
|
|
a.setClient(newClient)
|
|
if !a.scheduler.UpdateClient(newClient) {
|
|
logger.LogEvent("配置", "warn", "抢购任务正在运行,客户端配置将在下次抢购时完整生效")
|
|
}
|
|
if proxyChanged {
|
|
if proxyURL != "" {
|
|
logger.LogEvent("代理", "success", "代理已生效: %s", proxyURL)
|
|
} else {
|
|
logger.LogEvent("代理", "info", "代理已停用,恢复直连")
|
|
}
|
|
}
|
|
if sample, err := newClient.ServerTimeSample(); err == nil {
|
|
logger.SetDouyuOffset(sample.Offset)
|
|
} else {
|
|
logger.LogEvent("时间", "warn", "保存配置后服务端时间校准失败: %v", err)
|
|
}
|
|
}
|
|
|
|
logger.LogEvent("配置", "success", "配置已保存并生效: 日志等级=%s 并发=%d 重试=%d 重试阶段=%d 模式=%d trace=%v 代理=%v",
|
|
cfg.LogLevel, cfg.Concurrency, cfg.Attempts, len(cfg.RetryStages), cfg.SeckillMode, cfg.TraceEnabled, cfg.ProxyEnabled)
|
|
return types.SimpleResult{Success: true}
|
|
}
|