Files
2026-10-31 14:43:15 +08:00

99 lines
4.9 KiB
JavaScript

// 算法版本固定;不得改成 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() };
}