677 lines
18 KiB
Go
677 lines
18 KiB
Go
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
|
|
}
|