增加中文注释
This commit is contained in:
@@ -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
@@ -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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
||||
// escHtml 转义商品名中的 HTML 特殊字符,避免拼接到 onclick 时破坏页面结构。
|
||||
function escHtml(s){ return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
||||
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user