package main import ( "bufio" "bytes" "encoding/json" "fmt" "github.com/golang/freetype" "github.com/golang/freetype/truetype" "image" "image/color" "image/png" "io" "log" "mime/multipart" "net/http" "os" "strconv" "strings" "sync" "sync/atomic" "time" ) const ( MANUAL_ID = "G4KA4Qnz4LDp7" ROOM_ID = "9263298" EXCHANGE_URL = "https://www.douyu.com/wgapi/ordnc/activity/peace/exchange" CSRF_URL = "https://www.douyu.com/japi/carnival/nc/common/generateCsrf" BIND_INFO = "https://www.douyu.com/japi/carnivalApi/v2/tencent/bindInfo?actAlias=20260120QYOOB" JIFEN_HOST = "https://www.douyu.com/japi/oms/web/peace/credit/balance" CONCURRENCY = 3 GLOBAL_CONCURRENT = 300 ) type Config struct { TimeApiUrl string `json:"timeApiUrl"` } var config Config type Account struct { Name string `json:"name"` Password string `json:"password"` Cookie string `json:"cookie"` } type Product struct { CommodityID string `json:"commodityId"` CommodityName string `json:"commodityName"` Score int `json:"score"` Count int `json:"count"` Pic string `json:"pic"` } type SeckillRequest struct { AccountProducts map[string]string `json:"accountProducts"` TargetTime string `json:"targetTime"` Advance int `json:"advance"` } var ( accounts []Account accountsMu sync.RWMutex products []Product productsMu sync.RWMutex results = make(map[string]string) resultsMu sync.RWMutex httpClient = &http.Client{ Timeout: 10 * time.Second, // ← 加这一行,防止请求卡死 Transport: &http.Transport{ MaxConnsPerHost: 100, MaxIdleConns: 100, MaxIdleConnsPerHost: 50, IdleConnTimeout: 30 * time.Second, }, } seckillTimer *time.Timer timerMu sync.Mutex cachedFont *truetype.Font fontMu sync.Mutex ) func main() { // 设置日志格式,显示毫秒 log.SetFlags(log.LstdFlags | log.Lmicroseconds) // ← 加这行 loadConfig() // ← 新增,放在最前面 loadAccounts() os.MkdirAll("success_images", 0755) initFont() // ← 加这一行,预加载字体 // 测试一下 currentTime 能不能通 if t := fetchCurrentTime(); !t.IsZero() { log.Printf("✅ currentTime 获取成功: %s", t.In(time.FixedZone("CST", 8*3600)).Format("2006-01-02 15:04:05")) } else { log.Printf("⚠️ currentTime 获取失败,请检查网络") } log.Printf("服务启动,账号数: %d", len(accounts)) http.HandleFunc("/api/products", handleProducts) http.HandleFunc("/api/load-products", handleLoadProducts) http.HandleFunc("/api/accounts", handleAccounts) http.HandleFunc("/api/reload-accounts", handleReloadAccounts) http.HandleFunc("/api/seckill", handleSeckill) http.HandleFunc("/api/schedule", handleSchedule) http.HandleFunc("/api/results", handleResults) http.HandleFunc("/api/time", handleTime) http.HandleFunc("/api/clear-results", handleClearResults) http.HandleFunc("/api/query-score", handleQueryScore) http.Handle("/", http.FileServer(http.Dir("./static"))) log.Printf("✅ 服务启动: http://localhost:2169 | 账号:%d", len(accounts)) log.Fatal(http.ListenAndServe(":2169", nil)) } func initFont() { fontMu.Lock() defer fontMu.Unlock() if cachedFont != nil { return } fontBytes, err := os.ReadFile("Microsoft Yahei.ttf") if err != nil { log.Printf("[ERROR] 字体文件加载失败: %v", err) return } f, err := freetype.ParseFont(fontBytes) if err != nil { log.Printf("[ERROR] 字体解析失败: %v", err) return } cachedFont = f log.Printf("[OK] 字体加载成功") } // 新增加载配置函数 func loadConfig() { f, err := os.Open("config.json") if err != nil { log.Printf("⚠️ 读取 config.json 失败,使用默认URL") config.TimeApiUrl = "https://www.douyu.com/wgapi/ordnc/activity/peace/storedetail?manualID=G4KA4Qnz4LDp7&commodityID=20260120QYOOB_bag274&rid=9263298" return } defer f.Close() decoder := json.NewDecoder(f) if err := decoder.Decode(&config); err != nil { log.Printf("⚠️ 解析 config.json 失败,使用默认URL") config.TimeApiUrl = "https://www.douyu.com/wgapi/ordnc/activity/peace/storedetail?manualID=G4KA4Qnz4LDp7&commodityID=20260120QYOOB_bag274&rid=9263298" return } log.Printf("✅ 加载配置成功: %s", config.TimeApiUrl) } // ==================== 核心:从商品详情接口获取服务端当前时间 ==================== // fetchCurrentTime 从商品详情接口获取服务端当前时间 func fetchCurrentTime() time.Time { url := config.TimeApiUrl + "&_=" + fmt.Sprintf("%d", time.Now().UnixNano()) req, _ := http.NewRequest("GET", url, nil) req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:152.0) Gecko/20100101 Firefox/152.0") req.Header.Set("Accept", "application/json, text/plain, */*") req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,zh-TW;q=0.8,zh-HK;q=0.7,en-US;q=0.6,en;q=0.5") req.Header.Set("Referer", "https://www.douyu.com/pages/live-peace-handbook/web/shop?ditchname=pass0&roomId="+ROOM_ID) req.Header.Set("Connection", "keep-alive") resp, err := httpClient.Do(req) if err != nil { log.Printf("fetchCurrentTime 请求失败: %v", err) return time.Time{} } defer resp.Body.Close() if resp.StatusCode != 200 { log.Printf("fetchCurrentTime HTTP状态码: %d", resp.StatusCode) return time.Time{} } body, _ := io.ReadAll(resp.Body) var result struct { Data struct { PreExchange struct { CurrentTime string `json:"currentTime"` } `json:"preExchange"` } `json:"data"` } if err := json.Unmarshal(body, &result); err != nil { log.Printf("fetchCurrentTime 解析JSON失败: %v", err) return time.Time{} } ts, err := strconv.ParseInt(result.Data.PreExchange.CurrentTime, 10, 64) if err != nil || ts == 0 { log.Printf("fetchCurrentTime 解析时间戳失败: %s", result.Data.PreExchange.CurrentTime) return time.Time{} } return time.Unix(ts, 0).UTC() } // getServerTime 获取服务端当前时间(带重试) func getServerTime() time.Time { for i := 0; i < 3; i++ { t := fetchCurrentTime() if !t.IsZero() { return t } time.Sleep(100 * time.Millisecond) } // 全部失败,降级到本地时间 log.Printf("⚠️ getServerTime 全部失败,降级到本地时间") return time.Now().UTC() } // ==================== API ==================== func handleClearResults(w http.ResponseWriter, _ *http.Request) { resultsMu.Lock() results = make(map[string]string) resultsMu.Unlock() json.NewEncoder(w).Encode(map[string]string{"ok": "true"}) } // 查询积分 func getScore(cookie string) string { body := strings.NewReader(`{}`) req, _ := http.NewRequest("POST", JIFEN_HOST, body) req.Header.Set("Content-Type", "application/json") req.Header.Set("User-Agent", "Mozilla/5.0") req.Header.Set("Cookie", cookie) req.Header.Set("Referer", "https://www.douyu.com/") resp, err := httpClient.Do(req) if err != nil { return "查询失败" } defer resp.Body.Close() b, _ := io.ReadAll(resp.Body) var result struct { Data struct { Balance int `json:"balance"` } `json:"data"` } json.Unmarshal(b, &result) return fmt.Sprintf("%d", result.Data.Balance) } func handleQueryScore(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) cookie := string(body) if cookie == "" { http.Error(w, "缺少cookie", 400) return } score := getScore(cookie) json.NewEncoder(w).Encode(map[string]interface{}{"score": score}) } func loadAccounts() { accountsMu.Lock() defer accountsMu.Unlock() accounts = nil f, err := os.Open("user.txt") if err != nil { log.Printf("读取user.txt失败: %v", err) return } defer f.Close() sc := bufio.NewScanner(f) for sc.Scan() { line := strings.TrimSpace(sc.Text()) if line == "" { continue } parts := strings.SplitN(line, "----", 3) if len(parts) == 3 { accounts = append(accounts, Account{Name: parts[0], Password: parts[1], Cookie: parts[2]}) } } log.Printf("加载 %d 个账号", len(accounts)) } func handleTime(w http.ResponseWriter, _ *http.Request) { loc := time.FixedZone("CST", 8*3600) serverTime := getServerTime() json.NewEncoder(w).Encode(map[string]interface{}{ "serverTime": serverTime.In(loc).Format("2006-01-02 15:04:05.000"), "localTime": time.Now().In(loc).Format("2006-01-02 15:04:05.000"), "source": "currentTime", }) } func handleProducts(w http.ResponseWriter, _ *http.Request) { productsMu.RLock() defer productsMu.RUnlock() json.NewEncoder(w).Encode(products) } 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) req.Header.Set("User-Agent", "Mozilla/5.0") resp, err := httpClient.Do(req) if err != nil { http.Error(w, "请求失败", 500) return } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) var result struct { Data struct { List []struct { CommodityID string `json:"commodityId"` CommodityName string `json:"commodityName"` Score int `json:"score"` Count int `json:"count"` Pic string `json:"pic"` } `json:"list"` } `json:"data"` } json.Unmarshal(body, &result) productsMu.Lock() products = nil for _, p := range result.Data.List { products = append(products, Product{p.CommodityID, p.CommodityName, p.Score, p.Count, p.Pic}) } productsMu.Unlock() log.Printf("加载商品: %d个", len(products)) json.NewEncoder(w).Encode(map[string]interface{}{"success": true, "count": len(products)}) } func handleAccounts(w http.ResponseWriter, _ *http.Request) { accountsMu.RLock() defer accountsMu.RUnlock() type R struct { Index int `json:"index"` Name string `json:"name"` Cookie string `json:"cookie"` } list := []R{} for i, a := range accounts { list = append(list, R{i, a.Name, a.Cookie}) } json.NewEncoder(w).Encode(list) } func handleReloadAccounts(w http.ResponseWriter, _ *http.Request) { loadAccounts() json.NewEncoder(w).Encode(map[string]interface{}{"success": true, "count": len(accounts)}) } func handleSeckill(w http.ResponseWriter, r *http.Request) { var req SeckillRequest json.NewDecoder(r.Body).Decode(&req) if len(req.AccountProducts) == 0 { http.Error(w, "请选择账号和商品", 400) return } go executeSeckill(req.AccountProducts) json.NewEncoder(w).Encode(map[string]string{"success": "true", "msg": "抢购已启动"}) } func handleSchedule(w http.ResponseWriter, r *http.Request) { var req SeckillRequest json.NewDecoder(r.Body).Decode(&req) if req.TargetTime == "" { http.Error(w, "请设置目标时间", 400) return } targetTime, err := time.ParseInLocation("2006-01-02 15:04:05", req.TargetTime, time.Local) if err != nil { http.Error(w, "时间格式错误", 400) return } targetTime = targetTime.UTC() // ✅ 用 currentTime 获取服务端当前时间 serverTime := getServerTime() if serverTime.IsZero() { http.Error(w, "获取服务端时间失败", 500) return } advance := time.Duration(req.Advance) * time.Millisecond triggerTime := targetTime.Add(-advance) remain := triggerTime.Sub(serverTime) log.Printf("📍 目标时间: %s, 服务端时间: %s, 剩余: %v, 提前: %dms", targetTime.In(time.FixedZone("CST", 8*3600)).Format("15:04:05.000"), serverTime.In(time.FixedZone("CST", 8*3600)).Format("15:04:05.000"), remain, req.Advance, ) if remain < 0 { http.Error(w, "目标时间已过", 400) return } timerMu.Lock() if seckillTimer != nil { seckillTimer.Stop() } timerMu.Unlock() ap := req.AccountProducts loc := time.FixedZone("CST", 8*3600) timerMu.Lock() seckillTimer = time.AfterFunc(remain, func() { //// ✅ 到点后再次获取最新的服务端时间,确保准时 //now := getServerTime() // //log.Printf("🚀 触发! 目标:%s 实际:%s 偏差:%v", // targetTime.In(loc).Format("15:04:05.000"), // now.In(loc).Format("15:04:05.000"), // now.Sub(targetTime), //) log.Printf("🚀 触发! 本地时间: %s", time.Now().In(loc).Format("15:04:05.000")) executeSeckill(ap) }) timerMu.Unlock() log.Printf("⏰ 定时: %s(北京时间) 提前%dms", targetTime.In(loc).Format("15:04:05"), req.Advance) json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "targetTime": targetTime.In(loc).Format("2006-01-02 15:04:05"), "triggerTime": triggerTime.In(loc).Format("2006-01-02 15:04:05.000"), "advance": req.Advance, "remainSecond": int(remain.Seconds()), "serverTime": serverTime.In(loc).Format("2006-01-02 15:04:05.000"), }) } func handleResults(w http.ResponseWriter, _ *http.Request) { resultsMu.RLock() defer resultsMu.RUnlock() json.NewEncoder(w).Encode(results) } // ==================== 抢购执行 ==================== func executeSeckill(ap map[string]string) { var wg sync.WaitGroup var ok, fail int32 limit := make(chan struct{}, GLOBAL_CONCURRENT) accountsMu.RLock() accs := make([]Account, len(accounts)) copy(accs, accounts) accountsMu.RUnlock() for _, a := range accs { cid, ex := ap[a.Name] if !ex || cid == "" { continue } // ✅ 每个账号只获取一次 CSRF(移到这里,在循环内部) csrf := getCsrf(a.Cookie) if csrf == "" { log.Printf("❌ %s 获取 CSRF 失败,跳过", a.Name) continue } for i := 0; i < CONCURRENCY; i++ { wg.Add(1) 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() results[fmt.Sprintf("%s_第%d包", a.Name, idx+1)] = res resultsMu.Unlock() if strings.Contains(res, "成功") { atomic.AddInt32(&ok, 1) log.Printf("✅ %s 第%d包 成功: %s", a.Name, idx+1, res) rn := "" if strings.Contains(res, "|") { rn = strings.SplitN(res, "|", 2)[1] } go saveImg(a, cid, rn) // ← 异步保存,不阻塞 } else { atomic.AddInt32(&fail, 1) log.Printf("❌ %s 第%d包 失败: %s", a.Name, idx+1, res) } }(a, cid, csrf, i) // ← 传入 csrf } } wg.Wait() log.Printf("抢购完成: 成功%d 失败%d", ok, fail) } func doExchange(a Account, cid string, csrf string) string { //csrf := getCsrf(a.Cookie) if csrf == "" { return "CSRF失败" } ccn := extractMid(a.Cookie, "acf_ccn=", ";") ck := a.Cookie if ccn != "" { ck += "; acf_ccn=" + ccn } ck += "; cvl_csrf_token=" + csrf ck = strings.ReplaceAll(ck, ";;", ";") var buf bytes.Buffer w := multipart.NewWriter(&buf) w.WriteField("manualID", MANUAL_ID) w.WriteField("commodityID", cid) w.WriteField("rid", ROOM_ID) w.WriteField("csrfToken", csrf) w.Close() req, _ := http.NewRequest("POST", EXCHANGE_URL, &buf) req.Header.Set("Content-Type", w.FormDataContentType()) req.Header.Set("User-Agent", "Mozilla/5.0") req.Header.Set("Origin", "https://www.douyu.com") req.Header.Set("Referer", "https://www.douyu.com/pages/live-peace-handbook/web/shop?roomId=9263298") req.Header.Set("Cookie", ck) resp, err := httpClient.Do(req) if err != nil { return "请求失败" } defer resp.Body.Close() b, _ := io.ReadAll(resp.Body) s := string(b) if strings.Contains(s, `"error":0`) { rn := getRoleName(a.Cookie) if rn != "" { return "成功|" + rn } return "成功" } return "失败|" + trunc(s, 80) } // ==================== 辅助函数 ==================== func getCsrf(cookie string) string { req, _ := http.NewRequest("POST", CSRF_URL, nil) req.Header.Set("User-Agent", "Mozilla/5.0") req.Header.Set("Cookie", cookie) req.Header.Set("Referer", "https://www.douyu.com/") resp, err := httpClient.Do(req) if err != nil { return "" } defer resp.Body.Close() for _, c := range resp.Cookies() { if c.Name == "cvl_csrf_token" { return c.Value } } return "" } func getRoleName(cookie string) string { req, _ := http.NewRequest("GET", BIND_INFO, nil) req.Header.Set("User-Agent", "Mozilla/5.0") req.Header.Set("Cookie", cookie) req.Header.Set("X-Requested-With", "XMLHttpRequest") req.Header.Set("Referer", "https://www.douyu.com/") resp, err := httpClient.Do(req) if err != nil { return "" } defer resp.Body.Close() b, _ := io.ReadAll(resp.Body) return extractMid(string(b), `"roleName":"`, `"`) } func saveImg(a Account, cid, roleName string) { productsMu.RLock() var pname string for _, p := range products { if p.CommodityID == cid { pname = p.CommodityName break } } productsMu.RUnlock() if cachedFont == nil { log.Printf("字体未加载,无法生成图片") return } width, height := 600, 280 img := image.NewRGBA(image.Rect(0, 0, width, height)) bgColor := color.RGBA{213, 239, 253, 255} greenColor := color.RGBA{76, 175, 80, 255} whiteColor := color.RGBA{255, 255, 255, 255} blackColor := color.RGBA{0, 0, 0, 255} // 画背景 for x := 0; x < width; x++ { for y := 0; y < height; y++ { if y < 60 { img.Set(x, y, greenColor) } else { img.Set(x, y, bgColor) } } } c := freetype.NewContext() c.SetDPI(72) c.SetFont(cachedFont) // ← 用缓存的字体,不再读取文件 c.SetClip(img.Bounds()) c.SetDst(img) loc := time.FixedZone("CST", 8*3600) now := time.Now().In(loc) texts := []struct { text string size float64 x, y int color color.Color }{ {pname, 20, 25, 42, whiteColor}, {"兑换时间:" + now.Format("2006年01月02日 15:04:05"), 16, 25, 110, blackColor}, {"游戏昵称:" + roleName, 16, 25, 150, blackColor}, {"兑换账号:" + a.Name, 16, 25, 190, blackColor}, } for _, t := range texts { c.SetFontSize(t.size) c.SetSrc(image.NewUniform(t.color)) pt := freetype.Pt(t.x, t.y) c.DrawString(t.text, pt) } fn := fmt.Sprintf("success_images/%s_%s_%d.png", a.Name, pname, time.Now().Unix()) f2, _ := os.Create(fn) defer f2.Close() png.Encode(f2, img) log.Printf("✅ 图片已保存: %s", fn) } func extractMid(s, start, end string) string { i := strings.Index(s, start) if i == -1 { return "" } i += len(start) j := strings.Index(s[i:], end) if j == -1 { return s[i:] } return s[i : i+j] } func trunc(s string, n int) string { if len(s) > n { return s[:n] + "..." } return s }