增加中文注释

This commit is contained in:
yml2213
2026-06-19 09:13:39 +08:00
parent 355dba342a
commit 817f05886a
2 changed files with 130 additions and 75 deletions
+25 -2
View File
@@ -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] + "..."
+105 -73
View File
@@ -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=`<div class="toast-title">${title}</div><div class="toast-body">${body}</div>`;
container.appendChild(div);
// 触发滑入
setTimeout(()=>container.classList.add('show'),10);
// 自动移除
setTimeout(()=>{
div.style.opacity='0';
div.style.transition='opacity .3s';
let div=document.createElement('div');
div.className='toast toast-'+type;
div.innerHTML=`<div class="toast-title">${title}</div><div class="toast-body">${body}</div>`;
container.appendChild(div);
// 短延迟回调用于触发 CSS 过渡,让提示从右侧滑入。
setTimeout(()=>container.classList.add('show'),10);
// 外层回调控制停留时长,内层回调等待淡出动画结束后移除节点。
setTimeout(()=>{
div.style.opacity='0';
div.style.transition='opacity .3s';
setTimeout(()=>{div.remove();if(container.children.length===0)container.classList.remove('show');},300);
},duration);
}
// ==================== 商品 ====================
async function loadProducts(){
// loadProducts 触发后端加载斗鱼商品接口,然后拉取缓存商品并重新渲染列表。
async function loadProducts(){
addLog('正在从斗鱼加载商品...','i');
try{
await fetch('/api/load-products');
@@ -265,9 +271,11 @@
toast('❌ 加载失败','请检查网络连接','error');
}
}
function renderProducts(){
let h='';
products.forEach(p=>{
// renderProducts 根据当前商品数组绘制左侧商品列表,并标记已选商品。
function renderProducts(){
let h='';
// 遍历商品缓存,生成每个商品的点击项和库存、积分信息。
products.forEach(p=>{
h+=`<div class="prod-item${selProdId==p.commodityId?' active':''}" onclick="selProduct('${p.commodityId}','${escHtml(p.commodityName)}')">
<img src="${p.pic}" onerror="this.style.display='none'">
<div class="pinfo"><div class="pname">${p.commodityName}</div>
@@ -277,14 +285,16 @@
document.getElementById('prodList').innerHTML=h||'<div style="text-align:center;color:#8b949e;padding:40px">暂无商品</div>';
}
function selProduct(id, name) {
// selProduct 记录当前选择的商品,并把它绑定到所有已勾选账号。
function selProduct(id, name) {
selProdId = id;
selProdName = name;
document.getElementById('selProd').textContent = name;
// ✅ 用账号名存商品绑定
selAccs.forEach(i => {
let accountName = accounts[i].name; // 用账号名作为 key
localStorage.setItem('accProd_' + accountName, id);
selProdName = name;
document.getElementById('selProd').textContent = name;
// ✅ 用账号名存商品绑定
// 该回调把当前商品写入每个已选账号的本地绑定记录。
selAccs.forEach(i => {
let accountName = accounts[i].name; // 用账号名作为 key
localStorage.setItem('accProd_' + accountName, id);
localStorage.setItem('accProdName_' + accountName, name);
});
@@ -300,11 +310,13 @@
// renderAccounts();
// }
async function loadAccounts() {
let r = await fetch('/api/accounts');
accounts = await r.json();
let validNames = new Set(accounts.map(a => a.name));
// loadAccounts 从后端获取账号列表,清理无效本地缓存后重新渲染账号表格。
async function loadAccounts() {
let r = await fetch('/api/accounts');
accounts = await r.json();
// map 回调抽取账号名,用于判断 localStorage 中的旧缓存是否仍然有效。
let validNames = new Set(accounts.map(a => a.name));
for (let key in localStorage) {
// 只清理商品和积分缓存,保留 selAccs、statusMap、targetTime、advance
@@ -320,9 +332,11 @@
}
function renderAccounts() {
document.getElementById('accCnt').textContent = accounts.length;
let h = accounts.map((a, i) => {
// renderAccounts 根据账号、选择状态、商品绑定、积分和抢购结果绘制账号表格。
function renderAccounts() {
document.getElementById('accCnt').textContent = accounts.length;
// 将账号数组映射为表格行 HTML,行内状态从 localStorage 和 statusMap 读取。
let h = accounts.map((a, i) => {
let isSel = selAccs.has(i);
// ✅ 用账号名读取商品、积分、状态
let prodName = localStorage.getItem('accProdName_' + a.name) || '-';
@@ -351,23 +365,27 @@
}
function toggleAll(){
let ck=document.getElementById('chkAll').checked;
if(ck) accounts.forEach((_,i)=>selAccs.add(i));
else selAccs.clear();
// toggleAll 根据表头复选框批量选择或取消所有账号,并持久化选择结果。
function toggleAll(){
let ck=document.getElementById('chkAll').checked;
// 批量选择时的回调把每个账号下标加入选择集合。
if(ck) accounts.forEach((_,i)=>selAccs.add(i));
else selAccs.clear();
localStorage.setItem('selAccs',JSON.stringify([...selAccs]));
renderAccounts();
}
function togAcc(i){
// togAcc 切换单个账号的选择状态,并同步更新 localStorage。
function togAcc(i){
if(selAccs.has(i)) selAccs.delete(i); else selAccs.add(i);
localStorage.setItem('selAccs',JSON.stringify([...selAccs]));
renderAccounts();
}
// ==================== 立即抢购(不弹窗) ====================
async function startNow(){
// startNow 构建账号商品映射后,调用后端立即启动抢购。
async function startNow(){
let ap=buildAP();
if(!ap) return;
addLog(`⚡ 立即抢购: ${Object.keys(ap).length}个账号 → ${selProdName}`,'i');
@@ -378,7 +396,8 @@
}
// ==================== 定时抢购 ====================
async function startSchedule(){
// startSchedule 构建定时参数并提交给后端,由后端按服务端时间安排抢购。
async function startSchedule(){
let ap=buildAP();
if(!ap) return;
let timeStr=document.getElementById('targetTime').value.trim();
@@ -410,7 +429,8 @@
toast('❌ 定时失败',d.error||'未知错误','error');
}
}
async function cancelSchedule(){
// cancelSchedule 只恢复前端定时状态和按钮显示;当前后端没有对应取消接口。
async function cancelSchedule(){
scheduleActive=false;
// 清除缓存的定时
localStorage.removeItem('targetTime');
@@ -424,41 +444,47 @@
toast('⏹ 定时已停止','抢购任务已取消','warn');
}
function startCountdown(sec) {
// startCountdown 启动前端倒计时徽标,用后端返回的剩余秒数做展示。
function startCountdown(sec) {
stopCountdown();
let badge = document.getElementById('cdBadge');
badge.style.display = 'inline-block';
let startTime = Date.now();
let endTime = startTime + sec * 1000;
countdownTimer = setInterval(() => {
// 每 500ms 刷新一次显示,确保用户能看到接近触发时的状态变化。
countdownTimer = setInterval(() => {
let now = Date.now();
let remain = Math.max(0, Math.ceil((endTime - now) / 1000));
if (remain <= 0) {
badge.textContent = '🚀 触发中...';
stopCountdown();
scheduleActive = false;
document.getElementById('btnSchedule').style.display = 'inline-block';
document.getElementById('btnCancel').style.display = 'none';
setTimeout(() => badge.style.display = 'none', 5000);
} else {
badge.textContent = `⏱ ${formatRemain(remain)}`;
document.getElementById('btnSchedule').style.display = 'inline-block';
document.getElementById('btnCancel').style.display = 'none';
// 延迟回调让“触发中”状态短暂停留,避免用户看不到最终状态。
setTimeout(() => badge.style.display = 'none', 5000);
} else {
badge.textContent = `⏱ ${formatRemain(remain)}`;
}
}, 500);
}
function stopCountdown(){
// stopCountdown 停止倒计时定时器,并隐藏倒计时徽标。
function stopCountdown(){
clearInterval(countdownTimer);
document.getElementById('cdBadge').style.display='none';
}
function formatRemain(sec){
// formatRemain 把秒数格式化为 h:mm:ss,供定时提示和倒计时徽标使用。
function formatRemain(sec){
let h=Math.floor(sec/3600),m=Math.floor((sec%3600)/60),s=sec%60;
return `${h}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`;
}
// ==================== 结果刷新 ====================
async function checkResults() {
// checkResults 轮询后端抢购结果,更新日志区、账号状态和本地状态缓存。
async function checkResults() {
try {
let r = await fetch('/api/results');
let d = await r.json();
@@ -486,15 +512,17 @@
function buildAP() {
// buildAP 根据已勾选账号和账号绑定商品,生成后端需要的账号到商品 ID 映射。
function buildAP() {
if (selAccs.size === 0) {
toast('⚠️ 提示', '请勾选要抢购的账号', 'warn');
return null;
}
let ap = {};
selAccs.forEach(i => {
let accountName = accounts[i].name; // ← 先取账号名
let cid = localStorage.getItem('accProd_' + accountName); // ← 用账号名读
return null;
}
let ap = {};
// 遍历已选账号,把存在商品绑定的账号转换成后端需要的映射。
selAccs.forEach(i => {
let accountName = accounts[i].name; // ← 先取账号名
let cid = localStorage.getItem('accProd_' + accountName); // ← 用账号名读
if (cid) {
ap[accountName] = cid;
}
@@ -507,14 +535,16 @@
}
function hasAnyAccProd(){
// hasAnyAccProd 判断当前账号列表中是否存在任意商品绑定。
function hasAnyAccProd(){
for(let i=0; i<accounts.length; i++){
if (localStorage.getItem('accProd_' + accounts[i].name)) return true;
}
return false;
}
function addLog(msg, type = 'i') {
// addLog 向底部日志区追加一条带毫秒时间戳的运行日志。
function addLog(msg, type = 'i') {
let el = document.getElementById('logBody');
let now = new Date();
let timeStr = now.toLocaleTimeString() + '.' + String(now.getMilliseconds()).padStart(3, '0');
@@ -523,14 +553,16 @@
}
function clearLog(){
// clearLog 清空前端日志显示,并重置最近一次结果快照。
function clearLog(){
document.getElementById('logBody').innerHTML='<div class="log-i">[系统] 就绪,等待操作...</div>';
lastResults={};
}
function escHtml(s){ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
// escHtml 转义商品名中的 HTML 特殊字符,避免拼接到 onclick 时破坏页面结构。
function escHtml(s){ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
init();
</script>
</body>
</html>
</html>