// ==UserScript== // @name 深圳大学体育场馆自动抢票 // @namespace http://tampermonkey.net/ // @version 1.1.7 // @description 深圳大学体育场馆自动预约脚本 - iOS、安卓、移动端、桌面端完全兼容 // @author zskfree // @match https://ehall.szu.edu.cn/qljfwapp/sys/lwSzuCgyy/* // @match https://ehall-443.webvpn.szu.edu.cn/qljfwapp/sys/lwSzuCgyy/* // @icon 🎾 // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @connect qyapi.weixin.qq.com // @run-at document-end // @license MIT // @downloadURL none // ==/UserScript== (function () { 'use strict'; // ==================== 设备检测模块 ==================== const Device = (() => { const ua = navigator.userAgent; const platform = navigator.platform; const maxTouch = navigator.maxTouchPoints; return { isMobile: /iPhone|iPad|iPod|Android|Mobile/i.test(ua), isIOS: /iPhone|iPad|iPod/i.test(ua), isIPad: /iPad/i.test(ua) || (platform === 'MacIntel' && maxTouch > 1), hasPointer: !!window.PointerEvent, get isTouch() { return this.isMobile || this.isIPad || (maxTouch > 0 && /Android|Mobile/i.test(ua)); } }; })(); // ==================== 样式管理器 ==================== const Styles = { getSize: (desktop, mobile, iPad) => Device.isIPad ? iPad : (Device.isMobile ? mobile : desktop), get input() { const padding = this.getSize('8px', '12px', '14px'); const fontSize = this.getSize('14px', '16px', '18px'); return `width:100%;padding:${padding};border:none;border-radius:6px;background:rgba(255,255,255,0.95);color:#333;font-size:${fontSize};box-sizing:border-box;-webkit-appearance:none;appearance:none;outline:none;`; }, get button() { const padding = this.getSize('12px', '15px', '18px'); const fontSize = this.getSize('16px', '18px', '20px'); return `width:100%;padding:${padding};border:none;border-radius:8px;cursor:pointer;font-size:${fontSize};font-weight:bold;transition:all 0.3s;text-shadow:1px 1px 2px rgba(0,0,0,0.3);-webkit-appearance:none;appearance:none;outline:none;-webkit-tap-highlight-color:transparent;`; } }; // ==================== 存储管理器 ==================== const Storage = { prefix: 'szu_sports_', maxAge: 7 * 24 * 60 * 60 * 1000, version: '1.1.7', set(key, value) { const data = { value, timestamp: Date.now(), version: this.version }; const fullKey = this.prefix + key; try { localStorage.setItem(fullKey, JSON.stringify(data)); return true; } catch { try { sessionStorage.setItem(fullKey, JSON.stringify(data)); return true; } catch { if (!window.memoryStorage) window.memoryStorage = new Map(); window.memoryStorage.set(fullKey, data); return true; } } }, get(key, defaultValue = null) { const fullKey = this.prefix + key; const now = Date.now(); const tryParse = (item) => { if (!item) return null; try { const data = JSON.parse(item); if (data.version !== this.version || (data.timestamp && now - data.timestamp > this.maxAge)) { this.remove(key); return null; } return data.value !== undefined ? data.value : data; } catch { this.remove(key); return null; } }; // 尝试 localStorage const localItem = tryParse(localStorage.getItem(fullKey)); if (localItem !== null) return localItem; // 尝试 sessionStorage const sessionItem = tryParse(sessionStorage.getItem(fullKey)); if (sessionItem !== null) return sessionItem; // 尝试内存存储 if (window.memoryStorage?.has(fullKey)) { const data = window.memoryStorage.get(fullKey); return data.value !== undefined ? data.value : data; } return defaultValue; }, remove(key) { const fullKey = this.prefix + key; try { localStorage.removeItem(fullKey); } catch { } try { sessionStorage.removeItem(fullKey); } catch { } window.memoryStorage?.delete(fullKey); }, cleanup() { const now = Date.now(); let count = 0; [localStorage, sessionStorage].forEach(storage => { try { for (let i = storage.length - 1; i >= 0; i--) { const key = storage.key(i); if (key?.startsWith(this.prefix)) { try { const data = JSON.parse(storage.getItem(key)); if (data.timestamp && now - data.timestamp > this.maxAge) { storage.removeItem(key); count++; } } catch { storage.removeItem(key); count++; } } } } catch { } }); return count; } }; // ==================== 网络错误处理器 ==================== const NetworkErrorHandler = { categorize(error, response = null) { if (response) { if (response.status === 429) return 'rate_limit'; if (response.status >= 500) return 'server_error'; if (response.status === 401 || response.status === 403) return 'auth_error'; if (response.status >= 400) return 'client_error'; } if (error.name === 'AbortError' || error.message.includes('超时')) return 'timeout'; if (error.message.includes('网络')) return 'network_error'; return 'unknown_error'; }, shouldRetry(errorType, retryCount = 0) { const maxRetries = { rate_limit: 3, server_error: 5, network_error: 3, timeout: 3, unknown_error: 2 }; const noRetry = ['auth_error', 'client_error']; return !noRetry.includes(errorType) && retryCount < (maxRetries[errorType] || 1); }, getRetryDelay(errorType, retryCount = 0) { const baseDelays = { rate_limit: 5000, server_error: 3000, network_error: 2000, timeout: 1000, unknown_error: 2000 }; return Math.min((baseDelays[errorType] || 2000) * Math.pow(1.5, retryCount), 30000); }, async handle(error, response = null, retryCount = 0) { const errorType = this.categorize(error, response); const errorMsg = response ? `HTTP ${response.status}` : error.message; addLog(`❌ 请求失败: ${errorMsg}`, 'error'); if (errorType === 'auth_error') { addLog(`🔐 认证失败,请检查登录状态`, 'error'); if (isRunning) stopBooking(); return { shouldStop: true, shouldRetry: false }; } return { shouldStop: false, shouldRetry: this.shouldRetry(errorType, retryCount), retryDelay: this.shouldRetry(errorType, retryCount) ? this.getRetryDelay(errorType, retryCount) : 0, errorType }; } }; // ==================== 请求频率控制器 ==================== const RequestThrottler = { requests: [], maxPerSecond: 2, maxConcurrent: 3, current: 0, cleanup() { const now = Date.now(); this.requests = this.requests.filter(time => now - time < 1000); }, canRequest() { this.cleanup(); return this.requests.length < this.maxPerSecond && this.current < this.maxConcurrent; }, async wait() { while (!this.canRequest()) { const waitTime = this.current >= this.maxConcurrent ? 1000 : (this.requests.length >= this.maxPerSecond ? Math.max(0, 1000 - (Date.now() - Math.min(...this.requests))) : 0); if (waitTime > 0) await new Promise(resolve => setTimeout(resolve, waitTime)); } }, onStart() { this.requests.push(Date.now()); this.current++; }, onEnd() { this.current = Math.max(0, this.current - 1); }, reset() { this.requests = []; this.current = 0; addLog(`🔄 请求频率已重置`, 'info'); } }; // ==================== 智能重试机制 ==================== const SmartRetry = { failures: 0, lastSuccess: Date.now(), reset() { this.failures = 0; this.lastSuccess = Date.now(); }, onSuccess() { if (this.failures > 0) addLog(`✅ 恢复正常`, 'success'); this.reset(); }, onFailure() { this.failures++; if (this.failures >= 15) addLog(`⚠️ 连续失败${this.failures}次`, 'warning'); } }; // ==================== 移动端优化 ==================== const MobileOptimization = { wakeLock: null, async init() { if (!Device.isMobile) return; addLog(`📱 启用移动端优化`, 'info'); await this.requestWakeLock(); this.setupVisibility(); this.optimizeScrolling(); }, async requestWakeLock() { if ('wakeLock' in navigator) { try { this.wakeLock = await navigator.wakeLock.request('screen'); addLog(`🔆 屏幕保持唤醒`, 'success'); } catch { } } }, setupVisibility() { document.addEventListener('visibilitychange', () => { if (!document.hidden && isRunning) this.requestWakeLock(); }); }, optimizeScrolling() { const style = document.createElement('style'); style.textContent = ` #status-area { -webkit-overflow-scrolling: touch; overscroll-behavior: contain; } * { touch-action: manipulation; } #auto-booking-panel { -webkit-user-select: none; user-select: none; -webkit-tap-highlight-color: transparent; } #auto-booking-panel input, #auto-booking-panel select { -webkit-user-select: auto; user-select: auto; } `; document.head.appendChild(style); }, cleanup() { this.wakeLock?.release(); this.wakeLock = null; } }; // ==================== 企业微信推送 ==================== const WeChatNotifier = { url: 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=4a1965fb-7559-4229-95ab-cc5a34066b6b', enabled: true, async sendSuccess(info) { if (!this.enabled || typeof GM_xmlhttpRequest === 'undefined') return false; const message = `🎉 深大体育场馆预约成功! 👤 ${info.userName} (${info.userId}) 📅 ${info.date} | 🏟️ ${info.sport} | 🏫 ${info.campus} 📍 ${info.venueName} | ⏰ ${info.timeSlot} 📋 ${info.dhid}`; return new Promise(resolve => { GM_xmlhttpRequest({ method: 'POST', url: this.url, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ msgtype: 'text', text: { content: message } }), timeout: 10000, onload: (res) => resolve(res.status === 200), onerror: () => resolve(false), ontimeout: () => resolve(false) }); }); } }; // ==================== 常量定义 ==================== const SPORT_CODES = { "羽毛球": "001", "排球": "003", "网球": "004", "篮球": "005", "乒乓球": "013", "桌球": "016" }; const CAMPUS_CODES = { "粤海": "1", "丽湖": "2" }; const TIME_SLOTS = ["08:00-09:00", "09:00-10:00", "10:00-11:00", "11:00-12:00", "12:00-13:00", "13:00-14:00", "14:00-15:00", "15:00-16:00", "16:00-17:00", "17:00-18:00", "18:00-19:00", "19:00-20:00", "20:00-21:00", "21:00-22:00"]; // ==================== 配置管理 ==================== function getTomorrowDate() { const d = new Date(); d.setDate(d.getDate() + 1); return d.toISOString().split('T')[0]; } // 新增: 根据运动项目和校区获取正确的YYLX function getYYLX(sport, campus) { // 粤海篮球需要使用团体预约模式 if (sport === "篮球" && campus === "粤海") { return "2.0"; } // 其他情况使用单人散场模式 return "1.0"; } const DEFAULT_CONFIG = { USER_INFO: { YYRGH: "2300123999", YYRXM: "张三" }, TARGET_DATE: getTomorrowDate(), SPORT: "羽毛球", CAMPUS: "丽湖", PREFERRED_VENUE: "至畅", PREFERRED_TIMES: ["20:00-21:00", "21:00-22:00"], RETRY_INTERVAL: 1, MAX_RETRY_TIMES: 20000, REQUEST_TIMEOUT: 10, YYLX: "1.0" }; function loadConfig() { const saved = Storage.get('bookingConfig', null); const config = saved ? { ...DEFAULT_CONFIG, ...saved } : DEFAULT_CONFIG; config.TARGET_DATE = getTomorrowDate(); // 根据当前配置更新YYLX config.YYLX = getYYLX(config.SPORT, config.CAMPUS); return config; } // ==================== 定时任务管理器 ==================== const ScheduledTask = { timerId: null, targetTime: null, set(targetTime) { this.clear(); this.targetTime = targetTime; Storage.set('scheduledTime', targetTime); const now = Date.now(); const delay = targetTime - now; if (delay > 0) { this.timerId = setTimeout(() => { addLog(`⏰ 定时任务触发,开始抢票!`, 'success'); if (!isRunning) { updateConfigFromUI(); if (validateConfig()) startBooking(); } this.clear(); }, delay); const targetDate = new Date(targetTime); addLog(`⏰ 已设置定时任务: ${targetDate.toLocaleString()}`, 'success'); return true; } else { addLog(`❌ 定时时间必须晚于当前时间`, 'error'); return false; } }, clear() { if (this.timerId) { clearTimeout(this.timerId); this.timerId = null; } this.targetTime = null; Storage.remove('scheduledTime'); }, getRemaining() { if (!this.targetTime) return null; const remaining = Math.max(0, this.targetTime - Date.now()); return remaining; }, formatRemaining() { const remaining = this.getRemaining(); if (!remaining) return '未设置'; const hours = Math.floor(remaining / 3600000); const minutes = Math.floor((remaining % 3600000) / 60000); const seconds = Math.floor((remaining % 60000) / 1000); if (hours > 0) return `${hours}时${minutes}分${seconds}秒`; if (minutes > 0) return `${minutes}分${seconds}秒`; return `${seconds}秒`; }, restore() { const savedTime = Storage.get('scheduledTime'); if (savedTime && savedTime > Date.now()) { this.set(savedTime); return true; } return false; } }; // ==================== 全局变量 ==================== let CONFIG = loadConfig(); let isRunning = false; let retryCount = 0; let startTime = null; let successfulBookings = []; let controlPanel = null; let floatingButton = null; let isPanelVisible = Storage.get('panelVisible', true); let countdownInterval = null; // 新增: 倒计时更新定时器 function getMaxBookings() { return Math.min(CONFIG.PREFERRED_TIMES.length, 2); } // ==================== 交互处理器 ==================== const Interaction = { bind(el, handler) { if (!Device.isTouch) { el.addEventListener('click', handler); return; } let pressed = false, startTime = 0; if (Device.hasPointer) { el.addEventListener('pointerdown', (e) => { if (!e.isPrimary) return; pressed = true; startTime = Date.now(); }); el.addEventListener('pointerup', (e) => { if (!pressed || !e.isPrimary) return; if (Date.now() - startTime < 800) { e.preventDefault(); handler(); } pressed = false; }); } else { el.addEventListener('touchstart', () => { pressed = true; startTime = Date.now(); }, { passive: true }); el.addEventListener('touchend', (e) => { if (!pressed) return; if (Date.now() - startTime < 800) { e.preventDefault(); handler(); } pressed = false; }); } } }; // ==================== UI 创建 ==================== function createFloatingButton() { const btn = document.createElement('div'); btn.id = 'floating-toggle-btn'; const size = Styles.getSize('60px', '70px', '80px'); const fontSize = Styles.getSize('24px', '28px', '32px'); btn.style.cssText = `position:fixed;top:20px;right:20px;width:${size};height:${size};background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);border-radius:50%;display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:10001;box-shadow:0 4px 15px rgba(0,0,0,0.3);transition:all 0.3s;border:3px solid rgba(255,255,255,0.2);font-size:${fontSize};user-select:none;-webkit-tap-highlight-color:transparent;touch-action:manipulation;`; btn.innerHTML = '🎾'; btn.title = '显示/隐藏抢票面板'; Interaction.bind(btn, togglePanel); if (!Device.isTouch) { btn.addEventListener('mouseenter', () => btn.style.transform = 'scale(1.1)'); btn.addEventListener('mouseleave', () => btn.style.transform = 'scale(1)'); } document.body.appendChild(btn); return btn; } function createControlPanel() { const panel = document.createElement('div'); panel.id = 'auto-booking-panel'; const mobileStyles = Device.isMobile ? `width:calc(100vw - 30px);max-width:${Device.isIPad ? '500px' : '380px'};top:${Device.isIPad ? '120px' : '100px'};left:50%;font-size:${Device.isIPad ? '18px' : '16px'};max-height:calc(100vh - 150px);` : `width:400px;top:20px;right:90px;max-height:90vh;`; panel.style.cssText = `position:fixed;${mobileStyles}background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);border-radius:15px;padding:20px;box-shadow:0 10px 30px rgba(0,0,0,0.3);z-index:10000;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Hiragino Sans GB','Microsoft YaHei',sans-serif;color:white;border:2px solid rgba(255,255,255,0.2);overflow-y:auto;transition:opacity 0.3s ease,transform 0.3s ease;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;`; // 新增: 获取今天的日期字符串 const getTodayDate = () => { const d = new Date(); return d.toISOString().split('T')[0]; }; panel.innerHTML = `

🎾 自动抢票助手 v1.1.7

${TIME_SLOTS.map(slot => ``).join('')}
👤 ${CONFIG.USER_INFO.YYRXM} (${CONFIG.USER_INFO.YYRGH})
📅 ${CONFIG.TARGET_DATE} | 🏟️ ${CONFIG.SPORT} | 🏫 ${CONFIG.CAMPUS}
🏟️ 优先场馆: ${CONFIG.PREFERRED_VENUE}
${CONFIG.PREFERRED_TIMES.join(', ')}
⚙️ 间隔:${CONFIG.RETRY_INTERVAL}s | 重试:${CONFIG.MAX_RETRY_TIMES} | 超时:${CONFIG.REQUEST_TIMEOUT}s
🎯 进度: 0/${getMaxBookings()} 个时段
⏰ 定时抢票
未设置定时任务
🔧 等待开始...
${Device.isMobile ? '📱 触摸优化版本' : '⚡ 快捷键: Ctrl+Shift+S 开始/停止'}
`; document.body.appendChild(panel); const transforms = Device.isMobile ? { visible: 'translateX(-50%) translateY(0)', hidden: 'translateX(-50%) translateY(-30px)' } : { visible: 'translateX(0)', hidden: 'translateX(100%)' }; if (isPanelVisible) { panel.style.display = 'block'; panel.style.opacity = '1'; panel.style.transform = transforms.visible; } else { panel.style.display = 'none'; panel.style.opacity = '0'; panel.style.transform = transforms.hidden; } bindEvents(panel); return panel; } function togglePanel() { isPanelVisible = !isPanelVisible; Storage.set('panelVisible', isPanelVisible); const transforms = Device.isMobile ? { visible: 'translateX(-50%) translateY(0)', hidden: 'translateX(-50%) translateY(-30px)' } : { visible: 'translateX(0)', hidden: 'translateX(100%)' }; if (isPanelVisible) { controlPanel.style.display = 'block'; controlPanel.style.transform = transforms.hidden; controlPanel.style.opacity = '0'; setTimeout(() => { controlPanel.style.opacity = '1'; controlPanel.style.transform = transforms.visible; }, 10); } else { controlPanel.style.opacity = '0'; controlPanel.style.transform = transforms.hidden; setTimeout(() => { if (!isPanelVisible) controlPanel.style.display = 'none'; }, 300); } if (floatingButton) { floatingButton.style.background = isPanelVisible ? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' : 'linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%)'; floatingButton.innerHTML = isPanelVisible ? '🎾' : '📱'; } } function bindEvents(panel) { Interaction.bind(panel.querySelector('#close-panel'), togglePanel); Interaction.bind(panel.querySelector('#toggle-config'), () => { const area = panel.querySelector('#config-area'); const btn = panel.querySelector('#toggle-config'); if (area.style.display === 'none') { area.style.display = 'block'; btn.textContent = '⚙️ 隐藏配置'; } else { area.style.display = 'none'; btn.textContent = '⚙️ 显示配置'; } }); const updateVenueDisplay = () => { const sport = panel.querySelector('#sport-type').value; const campus = panel.querySelector('#campus').value; const venueSelection = panel.querySelector('#venue-selection'); const venueDisplay = panel.querySelector('#venue-display'); const show = sport === '羽毛球' && campus === '丽湖'; if (venueSelection) venueSelection.style.display = show ? 'block' : 'none'; if (venueDisplay) venueDisplay.style.display = show ? 'block' : 'none'; }; panel.querySelector('#sport-type').addEventListener('change', updateVenueDisplay); panel.querySelector('#campus').addEventListener('change', updateVenueDisplay); Interaction.bind(panel.querySelector('#save-config'), () => { updateConfigFromUI(); updateDisplayConfig(); addLog('✅ 配置已保存', 'success'); const area = panel.querySelector('#config-area'); const btn = panel.querySelector('#toggle-config'); area.style.display = 'none'; btn.textContent = '⚙️ 显示配置'; }); Interaction.bind(panel.querySelector('#start-btn'), () => { if (isRunning) { stopBooking(); } else { updateConfigFromUI(); if (validateConfig()) startBooking(); } }); // 新增: 定时任务按钮事件 Interaction.bind(panel.querySelector('#set-schedule-btn'), () => { const dateInput = panel.querySelector('#scheduled-date').value; const timeInput = panel.querySelector('#scheduled-time').value; if (!dateInput || !timeInput) { addLog('❌ 请选择定时日期和时间', 'error'); return; } const targetTime = new Date(`${dateInput} ${timeInput}`).getTime(); if (ScheduledTask.set(targetTime)) { startCountdown(); } }); Interaction.bind(panel.querySelector('#cancel-schedule-btn'), () => { ScheduledTask.clear(); stopCountdown(); updateCountdownDisplay('未设置定时任务'); addLog('❌ 已取消定时任务', 'info'); }); if (!Device.isMobile) { document.addEventListener('keydown', (e) => { if (e.ctrlKey && e.shiftKey && e.key === 'S') { e.preventDefault(); panel.querySelector('#start-btn').click(); } else if (e.ctrlKey && e.shiftKey && e.key === 'H') { e.preventDefault(); togglePanel(); } }); } } // 新增: 倒计时更新函数 function updateCountdownDisplay(text) { const display = document.getElementById('countdown-display'); if (display) display.textContent = text; } function startCountdown() { stopCountdown(); countdownInterval = setInterval(() => { const remaining = ScheduledTask.formatRemaining(); if (remaining === '未设置') { stopCountdown(); updateCountdownDisplay('未设置定时任务'); } else { updateCountdownDisplay(`⏰ 倒计时: ${remaining}`); } }, 1000); } function stopCountdown() { if (countdownInterval) { clearInterval(countdownInterval); countdownInterval = null; } } // ==================== 配置和日志 ==================== function updateConfigFromUI() { const selectedTimes = Array.from(document.querySelectorAll('#time-slots-container input:checked')).map(cb => cb.value); const campus = document.getElementById('campus').value; const sport = document.getElementById('sport-type').value; let venue = '至畅'; if (sport === '羽毛球' && campus === '丽湖') { venue = document.getElementById('preferred-venue')?.value || '至畅'; } else if (sport === '羽毛球' && campus === '粤海') { venue = '全部'; } CONFIG = { USER_INFO: { YYRGH: document.getElementById('user-id').value.trim(), YYRXM: document.getElementById('user-name').value.trim() }, TARGET_DATE: document.getElementById('target-date').value, SPORT: sport, CAMPUS: campus, PREFERRED_VENUE: venue, PREFERRED_TIMES: selectedTimes, RETRY_INTERVAL: parseInt(document.getElementById('retry-interval').value), MAX_RETRY_TIMES: parseInt(document.getElementById('max-retry').value), REQUEST_TIMEOUT: parseInt(document.getElementById('request-timeout').value), YYLX: getYYLX(sport, campus) // 使用动态获取的YYLX值 }; Storage.set('bookingConfig', CONFIG); updateProgress(); // 添加日志显示当前使用的YYLX addLog(`⚙️ 预约模式: ${CONFIG.YYLX === "2.0" ? "团体预约" : "单人散场"}`, 'info'); } function updateDisplayConfig() { document.getElementById('display-user').textContent = `${CONFIG.USER_INFO.YYRXM} (${CONFIG.USER_INFO.YYRGH})`; document.getElementById('display-date').textContent = CONFIG.TARGET_DATE; document.getElementById('display-sport').textContent = CONFIG.SPORT; document.getElementById('display-campus').textContent = CONFIG.CAMPUS; document.getElementById('display-venue').textContent = CONFIG.PREFERRED_VENUE; document.getElementById('display-times').textContent = CONFIG.PREFERRED_TIMES.join(', '); document.getElementById('display-interval').textContent = CONFIG.RETRY_INTERVAL; document.getElementById('display-retry').textContent = CONFIG.MAX_RETRY_TIMES; document.getElementById('display-timeout').textContent = CONFIG.REQUEST_TIMEOUT; } function validateConfig() { const errors = []; if (!CONFIG.USER_INFO.YYRGH || !CONFIG.USER_INFO.YYRXM) errors.push('请填写用户信息'); if (!/^\d{8,12}$/.test(CONFIG.USER_INFO.YYRGH)) errors.push('学号格式不正确'); if (!/^[\u4e00-\u9fa5]{2,10}$/.test(CONFIG.USER_INFO.YYRXM)) errors.push('姓名格式不正确'); if (!CONFIG.TARGET_DATE) errors.push('请选择日期'); if (!CONFIG.PREFERRED_TIMES.length) errors.push('请选择时间段'); errors.forEach(e => addLog(`❌ ${e}`, 'error')); if (!errors.length) addLog(`✅ 配置验证通过`, 'success'); return !errors.length; } function addLog(msg, type = 'info') { const area = document.getElementById('status-area'); if (!area) return; const colors = { info: '#e3f2fd', success: '#c8e6c9', warning: '#fff3e0', error: '#ffcdd2' }; const entry = document.createElement('div'); entry.style.cssText = `color:${colors[type]};margin-bottom:3px;border-left:3px solid ${colors[type]};padding-left:8px;`; entry.innerHTML = `[${new Date().toLocaleTimeString()}] ${msg}`; area.appendChild(entry); area.scrollTop = area.scrollHeight; while (area.children.length > 50) area.removeChild(area.firstChild); } function updateProgress() { const el = document.getElementById('booking-progress'); if (el) el.textContent = `${successfulBookings.length}/${getMaxBookings()} 个时段`; } // ==================== 网络请求 ==================== function getBaseUrl() { return window.location.href.includes('webvpn') ? 'https://ehall-443.webvpn.szu.edu.cn' : 'https://ehall.szu.edu.cn'; } async function fetchWithTimeout(url, options, timeout = CONFIG.REQUEST_TIMEOUT * 1000) { const startTime = Date.now(); let retry = 0; while (retry <= 3) { await RequestThrottler.wait(); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); try { RequestThrottler.onStart(); const response = await fetch(url, { ...options, signal: controller.signal, credentials: 'same-origin', mode: 'cors', cache: 'no-cache' }); clearTimeout(timeoutId); RequestThrottler.onEnd(); if (!response.ok) { const result = await NetworkErrorHandler.handle(new Error(`HTTP ${response.status}`), response, retry); if (result.shouldStop) throw new Error('请求终止'); if (result.shouldRetry && retry < 3) { retry++; await new Promise(r => setTimeout(r, result.retryDelay)); continue; } throw new Error(`HTTP ${response.status}`); } return response; } catch (error) { clearTimeout(timeoutId); RequestThrottler.onEnd(); if (retry >= 3) throw error; const result = await NetworkErrorHandler.handle(error, null, retry); if (result.shouldStop || !result.shouldRetry) throw error; retry++; await new Promise(r => setTimeout(r, result.retryDelay)); } } } async function getAvailableSlots() { try { const slots = []; const baseUrl = getBaseUrl(); const remaining = CONFIG.PREFERRED_TIMES.filter(t => !successfulBookings.some(b => b.timeSlot === t)); if (!remaining.length) return []; for (const timeSlot of remaining) { const [start, end] = timeSlot.split("-"); const payload = new URLSearchParams({ XMDM: SPORT_CODES[CONFIG.SPORT], YYRQ: CONFIG.TARGET_DATE, YYLX: CONFIG.YYLX, KSSJ: start, JSSJ: end, XQDM: CAMPUS_CODES[CONFIG.CAMPUS] }); const res = await fetchWithTimeout(`${baseUrl}/qljfwapp/sys/lwSzuCgyy/modules/sportVenue/getOpeningRoom.do`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, body: payload }); const data = await res.json(); if (data.code !== "0") { addLog(`⚠️ ${timeSlot} 查询失败: ${data.msg || '未知错误'}`, 'warning'); continue; } const rooms = data.datas?.getOpeningRoom?.rows || []; rooms.forEach(room => { let isAvailable = false; if (room.disabled === true || room.disabled === "true") { return; } const textValue = String(room.text || '').trim(); if (textValue === "可预约") { isAvailable = true; } else if (/^\d+\/\d+$/.test(textValue)) { const [remaining, total] = textValue.split('/').map(n => parseInt(n.trim())); isAvailable = remaining > 0 && remaining <= total; } if (!isAvailable) return; const fullName = room.CGBM_DISPLAY || room.CDMC || ''; if (CONFIG.SPORT === "羽毛球" && CONFIG.CAMPUS === "丽湖" && CONFIG.PREFERRED_VENUE !== "全部") { if ((CONFIG.PREFERRED_VENUE === "至畅" && !fullName.includes("至畅")) || (CONFIG.PREFERRED_VENUE === "至快" && !fullName.includes("至快"))) { return; } } let venuePriority = 2, courtPriority = 0; if (CONFIG.CAMPUS === "丽湖" && CONFIG.SPORT === "羽毛球") { if (fullName.includes("至畅")) { venuePriority = 0; const name = room.CDMC || ''; if (name.includes("5号场") || name.includes("五号场")) courtPriority = -2; else if (name.includes("10号场") || name.includes("十号场")) courtPriority = -1; else if (name.match(/[^0-9]1号场|^1号场|一号场/) || name.includes("6号场") || name.includes("六号场")) courtPriority = 2; } else if (fullName.includes("至快")) { venuePriority = 1; } } else if (CONFIG.SPORT === "篮球") { venuePriority = 0; courtPriority = 0; } let availableCount = null; if (/^\d+\/\d+$/.test(textValue)) { const [remaining, total] = textValue.split('/').map(n => parseInt(n.trim())); availableCount = remaining; } slots.push({ wid: room.WID, timeSlot, startTime: start, endTime: end, venueName: room.CDMC || '', venueFullName: fullName, venueCode: room.CGBM || '', priority: CONFIG.PREFERRED_TIMES.indexOf(timeSlot), venuePriority, courtPriority, availableCount }); }); } slots.sort((a, b) => a.courtPriority - b.courtPriority || a.venuePriority - b.venuePriority || a.priority - b.priority); return slots; } catch (error) { addLog(`🔥 获取场地失败: ${error.message}`, 'error'); return []; } } async function bookSlot(slot) { try { const { wid, timeSlot, startTime, endTime, venueName, venueCode, venueFullName } = slot; if (!timeSlot || !venueCode) { addLog(`❌ 预约参数缺失`, 'error'); return false; } const payload = new URLSearchParams({ DHID: "", YYRGH: CONFIG.USER_INFO.YYRGH, CYRS: "", YYRXM: CONFIG.USER_INFO.YYRXM, CGDM: venueCode, CDWID: wid, XMDM: SPORT_CODES[CONFIG.SPORT], XQWID: CAMPUS_CODES[CONFIG.CAMPUS], KYYSJD: timeSlot, YYRQ: CONFIG.TARGET_DATE, YYLX: CONFIG.YYLX, YYKS: `${CONFIG.TARGET_DATE} ${startTime}`, YYJS: `${CONFIG.TARGET_DATE} ${endTime}`, PC_OR_PHONE: "pc" }); const res = await fetchWithTimeout(`${getBaseUrl()}/qljfwapp/sys/lwSzuCgyy/sportVenue/insertVenueBookingInfo.do`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, body: payload }); const contentType = res.headers.get('content-type') || ''; // 检查是否返回HTML错误页面 if (contentType.includes('text/html')) { const html = await res.text(); // 尝试从HTML中提取错误信息 const errorMatch = html.match(/

出错信息:<\/h4>[\s\S]*?
\s*(.*?)\s*<\/div>/); const errorMsg = errorMatch ? errorMatch[1].trim() : '系统异常'; addLog(`⚠️ ${errorMsg}`, 'warning'); // 判断是否为已预约相同时段 if (errorMsg.includes('已预约该场地的相同时间段') || errorMsg.includes('已预约')) { addLog(`📌 ${timeSlot} 已预约过,跳过`, 'info'); // 标记该时段为已预约,避免重复尝试 successfulBookings.push({ timeSlot, venueName: '已预约', dhid: 'duplicate', slotName: `${timeSlot} (重复)` }); updateProgress(); return 'already_booked'; } return false; } // 正常JSON响应 const result = await res.json(); if (result.code === "0" && result.msg === "成功") { const dhid = result.data?.DHID || "Unknown"; const displayName = venueFullName ? `${venueFullName}-${venueName}` : venueName; addLog(`🎉 预约成功!场地:${displayName}`, 'success'); addLog(`📋 预约单号:${dhid}`, 'success'); successfulBookings.push({ timeSlot, venueName: displayName, dhid, slotName: displayName }); updateProgress(); WeChatNotifier.sendSuccess({ userName: CONFIG.USER_INFO.YYRXM, userId: CONFIG.USER_INFO.YYRGH, date: CONFIG.TARGET_DATE, sport: CONFIG.SPORT, campus: CONFIG.CAMPUS, venueName: displayName, timeSlot, dhid }); return true; } else { addLog(`❌ 预约失败:${result.msg}`, 'error'); if (result.msg?.includes("只能预订2次") || result.msg?.includes("超过限制")) { addLog(`🎊 已达预约上限`, 'success'); return 'limit_reached'; } return false; } } catch (error) { // 捕获JSON解析错误 if (error.message.includes('JSON') || error.message.includes('Unexpected token')) { addLog(`⚠️ 服务器返回异常格式`, 'warning'); return false; } addLog(`💥 预约异常: ${error.message}`, 'error'); return false; } } // ==================== 主流程 ==================== async function startBooking() { if (isRunning) return; isRunning = true; retryCount = 0; startTime = new Date(); const max = getMaxBookings(); SmartRetry.reset(); const btn = document.getElementById('start-btn'); if (btn) { btn.textContent = '⏹️ 停止抢票'; btn.style.background = 'linear-gradient(45deg, #f44336, #d32f2f)'; } addLog(`🚀 开始抢票!`, 'success'); addLog(`📊 ${CONFIG.SPORT} | ${CONFIG.CAMPUS} | ${CONFIG.TARGET_DATE}`, 'info'); try { while (isRunning && retryCount < CONFIG.MAX_RETRY_TIMES) { if (successfulBookings.length >= max) { addLog(`🎊 成功预约 ${max} 个时段`, 'success'); break; } retryCount++; if (retryCount === 1 || retryCount % 10 === 0) { addLog(`🔍 第 ${retryCount} 次查询 (${successfulBookings.length}/${max})`); } try { const slots = await getAvailableSlots(); if (slots.length) { SmartRetry.onSuccess(); addLog(`🎉 找到 ${slots.length} 个可预约场地`, 'success'); const groups = {}; slots.forEach(s => { if (!groups[s.timeSlot]) groups[s.timeSlot] = []; groups[s.timeSlot].push(s); }); for (const time of CONFIG.PREFERRED_TIMES) { if (successfulBookings.length >= max) break; if (successfulBookings.some(b => b.timeSlot === time)) continue; if (groups[time]) { groups[time].sort((a, b) => a.courtPriority - b.courtPriority || a.venuePriority - b.venuePriority); const result = await bookSlot(groups[time][0]); // 处理各种预约结果 if (result === 'limit_reached') { addLog(`🏁 已达预约上限,停止抢票`, 'success'); break; } if (result === 'already_booked') { // 该时段已预约,继续下一个时段 continue; } await new Promise(r => setTimeout(r, 500)); } } } else { SmartRetry.onFailure(); if (retryCount <= 3 || retryCount % 20 === 0) { addLog(`🔍 暂无可预约场地`, 'warning'); } } } catch (error) { SmartRetry.onFailure(); if (NetworkErrorHandler.categorize(error) === 'auth_error') { addLog(`🔐 认证错误`, 'error'); break; } } if (successfulBookings.length < max && isRunning) { const interval = CONFIG.RETRY_INTERVAL * 1000 + (Math.random() * 200 - 100); await new Promise(r => setTimeout(r, Math.max(100, interval))); } } } finally { stopBooking(); } } function stopBooking() { if (!isRunning) return; isRunning = false; if (Device.isMobile) MobileOptimization.cleanup(); const btn = document.getElementById('start-btn'); if (btn) { btn.textContent = '🚀 开始抢票'; btn.style.background = 'linear-gradient(45deg, #ff6b6b, #ee5a52)'; } const max = getMaxBookings(); const realBookings = successfulBookings.filter(b => b.dhid !== 'duplicate'); if (realBookings.length) { addLog(`🎉 成功预约 ${realBookings.length}/${max} 个时段`, 'success'); realBookings.forEach((b, i) => addLog(`${i + 1}. ${b.slotName} (${b.dhid})`, 'success')); } else { addLog(`😢 未成功预约`, 'warning'); } const elapsed = startTime ? Math.round((new Date() - startTime) / 1000) : 0; addLog(`📊 运行${elapsed}秒,查询${retryCount}次`, 'info'); } // ==================== 初始化 ==================== function init() { const url = window.location.href; if (!url.includes('ehall.szu.edu.cn/qljfwapp/sys/lwSzuCgyy') && !url.includes('ehall-443.webvpn.szu.edu.cn/qljfwapp/sys/lwSzuCgyy')) return; const cleaned = Storage.cleanup(); if (cleaned) addLog(`🧹 清理 ${cleaned} 个过期项`, 'info'); if (Device.isMobile) MobileOptimization.init(); SmartRetry.reset(); CONFIG.TARGET_DATE = getTomorrowDate(); floatingButton = createFloatingButton(); controlPanel = createControlPanel(); updateDisplayConfig(); document.getElementById('target-date').value = getTomorrowDate(); // 新增: 恢复定时任务 if (ScheduledTask.restore()) { startCountdown(); } addLog(`🎮 抢票助手已就绪 (${Device.isIPad ? 'iPad' : (Device.isMobile ? '移动端' : '桌面端')})`, 'success'); } document.addEventListener('visibilitychange', () => { if (!document.hidden) { const newDate = getTomorrowDate(); if (CONFIG.TARGET_DATE !== newDate) { CONFIG.TARGET_DATE = newDate; document.getElementById('target-date').value = newDate; updateDisplayConfig(); Storage.set('bookingConfig', CONFIG); addLog(`📅 日期已更新: ${newDate}`, 'info'); } } }); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { setTimeout(init, 100); } })();