// 算法版本固定;不得改成 Math.random() 或依赖数据库自增 ID。 export function localDay(date = new Date()) { return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; } function seed32(text) { let h = 2166136261; for (let i = 0; i < text.length; i++) h = Math.imul(h ^ text.charCodeAt(i), 16777619); return h >>> 0; } export function shuffled(usernames, day, round) { const items = [...usernames].sort(); // UTF-16 固定排序,独立于 CSV 行顺序、ID 和系统语言。 let seed = seed32(`dww11-v1|${day}|${round}`); function random() { seed = (seed + 0x6D2B79F5) >>> 0; let t = seed; t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; } for (let i = items.length - 1; i > 0; i--) { const j = Math.floor(random() * (i + 1)); [items[i], items[j]] = [items[j], items[i]]; } return items; } export function newSchedule() { return { version: 1, day: '', round: 1, used: [], totalDraws: 0 }; } export function remaining(accounts, schedule, day) { const used = new Set(schedule.used); return shuffled(accounts.filter(a => a.status !== 'invalid').map(a => a.username), day, schedule.round) .filter(username => !used.has(username)); } // 在同一个 readwrite 事务里调用,先持久化 used,再把账号返回给页面。 export function draw(accounts, schedule, day) { if (!accounts.some(a => a.status !== 'invalid')) throw new Error('没有可用账号,请导入账号或在管理页恢复失效账号。'); let choices = remaining(accounts, schedule, day); if (!choices.length) { schedule.round++; schedule.used = []; choices = remaining(accounts, schedule, day); } schedule.day = day; const username = choices[0]; schedule.used.push(username); schedule.totalDraws++; return { account: accounts.find(a => a.username === username), round: schedule.round, position: schedule.used.length }; } export function parseCSV(input) { if (typeof input !== 'string') throw new Error('CSV 内容必须是文本。'); const text = input.replace(/^\uFEFF/, ''); const rows = []; let row = [], cell = '', quoted = false, endedQuote = false; const pushRow = () => { row.push(cell); if (row.some(v => v !== '')) rows.push(row); row = []; cell = ''; endedQuote = false; }; for (let i = 0; i < text.length; i++) { const c = text[i]; if (quoted) { if (c === '"') { if (text[i + 1] === '"') { cell += '"'; i++; } else { quoted = false; endedQuote = true; } } else cell += c; } else if (c === ',') { row.push(cell); cell = ''; endedQuote = false; } else if (c === '\r' || c === '\n') { if (c === '\r' && text[i + 1] === '\n') i++; pushRow(); } else if (c === '"' && !cell && !endedQuote) quoted = true; else { if (endedQuote || c === '"') throw new Error(`CSV 第 ${rows.length + 1} 条记录的引号格式错误。`); cell += c; } } if (quoted) throw new Error('CSV 存在未闭合的双引号。'); if (cell || row.length || endedQuote) pushRow(); if (rows.length < 2) throw new Error('CSV 必须包含表头及至少一个账号。'); const aliases = { username: 'username', account: 'username', '账号': 'username', '帐号': 'username', '用户名': 'username', password: 'password', '密码': 'password' }; const headers = rows.shift().map(h => aliases[h.trim().toLowerCase()] || h.trim().toLowerCase()); for (const name of ['username', 'password']) if (headers.filter(h => h === name).length !== 1) throw new Error('表头必须各包含一列 username,password(或 账号,密码)。'); const users = new Set(); return rows.map((r, i) => { if (r.length !== headers.length) throw new Error(`CSV 第 ${i + 2} 条记录的列数不匹配。`); const username = r[headers.indexOf('username')].trim(); const password = r[headers.indexOf('password')]; // 密码中的空格有意义,不 trim。 if (!username || !password) throw new Error(`CSV 第 ${i + 2} 条记录的账号或密码为空。`); if (username.length > 500 || password.length > 4096) throw new Error(`CSV 第 ${i + 2} 条记录的字段过长。`); if (users.has(username)) throw new Error(`CSV 第 ${i + 2} 条记录的账号重复,整批未导入。`); users.add(username); return { username, password }; }); } export const isRestriction = error => ['account_frozen', 'account_banned', 'account_disabled'].includes(error?.code); export function errorRecord(error) { if (!error) return { code: 'login_failed', message: '账号或密码错误', at: new Date().toISOString() }; if (!['login_failed', 'account_frozen', 'account_banned', 'account_disabled'].includes(error.code) || typeof error.message !== 'string' || !error.message.trim()) throw new Error('账号错误信息不正确。'); return { code: error.code, message: error.message.trim().slice(0, 500), at: new Date().toISOString() }; }