99 lines
4.9 KiB
JavaScript
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() };
|
|
}
|