diff --git a/main.go b/main.go index 2604c68..8e50c80 100644 --- a/main.go +++ b/main.go @@ -87,6 +87,7 @@ var ( fontMu sync.Mutex ) +// main 是程序入口,负责初始化配置、账号、字体、HTTP 路由,并启动本地 Web 服务。 func main() { // 设置日志格式,显示毫秒 log.SetFlags(log.LstdFlags | log.Lmicroseconds) // ← 加这行 @@ -122,6 +123,7 @@ func main() { log.Fatal(http.ListenAndServe(":2169", nil)) } +// initFont 预加载微软雅黑字体,供成功兑换后生成结果图片时复用。 func initFont() { fontMu.Lock() defer fontMu.Unlock() @@ -142,7 +144,7 @@ func initFont() { log.Printf("[OK] 字体加载成功") } -// 新增加载配置函数 +// loadConfig 读取 config.json 中的时间校准接口地址;读取或解析失败时使用默认商品详情接口。 func loadConfig() { f, err := os.Open("config.json") if err != nil { @@ -225,6 +227,7 @@ func getServerTime() time.Time { // ==================== API ==================== +// handleClearResults 清空内存中的抢购结果,通常在前端启动时调用以避免展示旧结果。 func handleClearResults(w http.ResponseWriter, _ *http.Request) { resultsMu.Lock() results = make(map[string]string) @@ -232,7 +235,7 @@ func handleClearResults(w http.ResponseWriter, _ *http.Request) { json.NewEncoder(w).Encode(map[string]string{"ok": "true"}) } -// 查询积分 +// getScore 使用账号 Cookie 请求斗鱼积分余额接口,返回积分数字字符串或失败提示。 func getScore(cookie string) string { body := strings.NewReader(`{}`) req, _ := http.NewRequest("POST", JIFEN_HOST, body) @@ -256,6 +259,7 @@ func getScore(cookie string) string { return fmt.Sprintf("%d", result.Data.Balance) } +// handleQueryScore 接收前端传入的 Cookie,查询该账号积分并以 JSON 返回。 func handleQueryScore(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) cookie := string(body) @@ -267,6 +271,7 @@ func handleQueryScore(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(map[string]interface{}{"score": score}) } +// loadAccounts 从 user.txt 读取账号列表,格式为“账号----密码----Cookie”,并刷新全局账号缓存。 func loadAccounts() { accountsMu.Lock() defer accountsMu.Unlock() @@ -291,6 +296,7 @@ func loadAccounts() { log.Printf("加载 %d 个账号", len(accounts)) } +// handleTime 返回斗鱼服务端时间和本地时间,供前端展示与定时抢购校准使用。 func handleTime(w http.ResponseWriter, _ *http.Request) { loc := time.FixedZone("CST", 8*3600) serverTime := getServerTime() @@ -301,12 +307,14 @@ func handleTime(w http.ResponseWriter, _ *http.Request) { }) } +// handleProducts 返回当前内存缓存的商品列表,不主动访问斗鱼接口。 func handleProducts(w http.ResponseWriter, _ *http.Request) { productsMu.RLock() defer productsMu.RUnlock() json.NewEncoder(w).Encode(products) } +// handleLoadProducts 请求斗鱼和平小店商品列表接口,并把商品数据写入全局缓存。 func handleLoadProducts(w http.ResponseWriter, _ *http.Request) { url := fmt.Sprintf("https://www.douyu.com/wgapi/ordnc/activity/peace/storehome?manualID=%s&rid=%s", MANUAL_ID, ROOM_ID) req, _ := http.NewRequest("GET", url, nil) @@ -342,6 +350,7 @@ func handleLoadProducts(w http.ResponseWriter, _ *http.Request) { json.NewEncoder(w).Encode(map[string]interface{}{"success": true, "count": len(products)}) } +// handleAccounts 返回前端可展示的账号列表,仅暴露账号序号、名称和 Cookie。 func handleAccounts(w http.ResponseWriter, _ *http.Request) { accountsMu.RLock() defer accountsMu.RUnlock() @@ -357,11 +366,13 @@ func handleAccounts(w http.ResponseWriter, _ *http.Request) { json.NewEncoder(w).Encode(list) } +// handleReloadAccounts 重新读取 user.txt,并返回最新账号数量。 func handleReloadAccounts(w http.ResponseWriter, _ *http.Request) { loadAccounts() json.NewEncoder(w).Encode(map[string]interface{}{"success": true, "count": len(accounts)}) } +// handleSeckill 接收账号到商品的映射,异步启动立即抢购任务。 func handleSeckill(w http.ResponseWriter, r *http.Request) { var req SeckillRequest json.NewDecoder(r.Body).Decode(&req) @@ -373,6 +384,7 @@ func handleSeckill(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(map[string]string{"success": "true", "msg": "抢购已启动"}) } +// handleSchedule 根据斗鱼服务端时间计算触发延迟,并注册一次性定时抢购任务。 func handleSchedule(w http.ResponseWriter, r *http.Request) { var req SeckillRequest json.NewDecoder(r.Body).Decode(&req) @@ -420,6 +432,7 @@ func handleSchedule(w http.ResponseWriter, r *http.Request) { loc := time.FixedZone("CST", 8*3600) timerMu.Lock() + // 定时器回调会在到达触发时间后执行实际抢购逻辑。 seckillTimer = time.AfterFunc(remain, func() { //// ✅ 到点后再次获取最新的服务端时间,确保准时 //now := getServerTime() @@ -445,6 +458,7 @@ func handleSchedule(w http.ResponseWriter, r *http.Request) { }) } +// handleResults 返回最新抢购结果映射,供前端定时轮询并更新状态。 func handleResults(w http.ResponseWriter, _ *http.Request) { resultsMu.RLock() defer resultsMu.RUnlock() @@ -452,6 +466,7 @@ func handleResults(w http.ResponseWriter, _ *http.Request) { } // ==================== 抢购执行 ==================== +// executeSeckill 按账号和商品映射批量发起兑换请求,并统计成功、失败数量。 func executeSeckill(ap map[string]string) { var wg sync.WaitGroup var ok, fail int32 @@ -477,9 +492,11 @@ func executeSeckill(ap map[string]string) { for i := 0; i < CONCURRENCY; i++ { wg.Add(1) + // 每个 goroutine 负责一次兑换尝试,并通过 limit 控制全局并发量。 go func(a Account, cid string, csrf string, idx int) { defer wg.Done() limit <- struct{}{} + // 退出时释放一个并发名额,避免后续兑换任务被永久阻塞。 defer func() { <-limit }() res := doExchange(a, cid, csrf) resultsMu.Lock() @@ -504,6 +521,7 @@ func executeSeckill(ap map[string]string) { log.Printf("抢购完成: 成功%d 失败%d", ok, fail) } +// doExchange 使用单个账号、商品 ID 和 CSRF Token 调用斗鱼兑换接口,返回本次兑换结果。 func doExchange(a Account, cid string, csrf string) string { //csrf := getCsrf(a.Cookie) if csrf == "" { @@ -552,6 +570,7 @@ func doExchange(a Account, cid string, csrf string) string { // ==================== 辅助函数 ==================== +// getCsrf 访问斗鱼 CSRF 生成接口,并从响应 Cookie 中提取 cvl_csrf_token。 func getCsrf(cookie string) string { req, _ := http.NewRequest("POST", CSRF_URL, nil) req.Header.Set("User-Agent", "Mozilla/5.0") @@ -570,6 +589,7 @@ func getCsrf(cookie string) string { return "" } +// getRoleName 查询腾讯游戏绑定信息,并从响应中提取游戏角色名。 func getRoleName(cookie string) string { req, _ := http.NewRequest("GET", BIND_INFO, nil) req.Header.Set("User-Agent", "Mozilla/5.0") @@ -585,6 +605,7 @@ func getRoleName(cookie string) string { return extractMid(string(b), `"roleName":"`, `"`) } +// saveImg 在抢购成功后生成一张包含商品、时间、角色名和账号的结果图片。 func saveImg(a Account, cid, roleName string) { productsMu.RLock() var pname string @@ -655,6 +676,7 @@ func saveImg(a Account, cid, roleName string) { log.Printf("✅ 图片已保存: %s", fn) } +// extractMid 从字符串 s 中截取 start 和 end 之间的内容;找不到起点时返回空字符串。 func extractMid(s, start, end string) string { i := strings.Index(s, start) if i == -1 { @@ -668,6 +690,7 @@ func extractMid(s, start, end string) string { return s[i : i+j] } +// trunc 将过长字符串截断到指定长度,并追加省略号,便于日志展示。 func trunc(s string, n int) string { if len(s) > n { return s[:n] + "..." diff --git a/static/index.html b/static/index.html index 8fad9a0..4a24114 100644 --- a/static/index.html +++ b/static/index.html @@ -153,7 +153,8 @@ let scheduleActive=false, countdownTimer=null, lastResults={}; let statusMap={}; // ← 加这行 - async function init(){ + // init 初始化页面状态:恢复本地缓存、加载账号和商品、启动时间与结果轮询。 + async function init(){ // 恢复缓存的选择 let savedAccs=localStorage.getItem('selAccs'); let savedProd=localStorage.getItem('selProd'); @@ -188,26 +189,29 @@ setInterval(checkResults,2000); } - function updateTime(){ - fetch('/api/time').then(r=>r.json()).then(d=>{ - document.getElementById('svrTime').textContent=d.serverTime||'--'; - }).catch(()=>{}); + // updateTime 从后端获取斗鱼服务端时间,并刷新顶部时间展示。 + function updateTime(){ + // then 回调依次负责解析 JSON 和更新 DOM;catch 回调忽略短暂网络失败。 + fetch('/api/time').then(r=>r.json()).then(d=>{ + document.getElementById('svrTime').textContent=d.serverTime||'--'; + }).catch(()=>{}); } - //加载账号 - async function reloadAccounts(){ + // reloadAccounts 通知后端重新读取 user.txt,然后刷新前端账号表格。 + async function reloadAccounts(){ let r=await fetch('/api/reload-accounts'); let d=await r.json(); await loadAccounts(); toast('✅ 账号已刷新',`共 ${d.count} 个账号`,'success'); } - //并发查询函数 - async function queryAllScore() { - addLog('正在并发查询积分...', 'i'); - let tasks = []; - for (let i = 0; i < accounts.length; i++) { - tasks.push((async (idx) => { + // queryAllScore 并发查询所有账号积分,并把积分按账号名缓存到 localStorage。 + async function queryAllScore() { + addLog('正在并发查询积分...', 'i'); + let tasks = []; + for (let i = 0; i < accounts.length; i++) { + // 每个账号单独启动一个异步查询任务,避免账号多时串行等待过久。 + tasks.push((async (idx) => { let a = accounts[idx]; try { console.log('账号对象:', JSON.stringify(a)); @@ -234,25 +238,27 @@ // ==================== Toast提示 ==================== - function toast(title, body, type, duration){ + // toast 创建右侧滑入提示,按类型控制颜色,并在指定时间后自动移除。 + function toast(title, body, type, duration){ type=type||'info'; duration=duration||4000; let container=document.getElementById('toastContainer'); - let div=document.createElement('div'); - div.className='toast toast-'+type; - div.innerHTML=`