fix(reliability): multer-ошибки, process-хендлеры, анти-гонка питомца, flashcards (Спринт2)
- errorHandler: MulterError → 413 «слишком большой» / 400 (а не 500). - server: process.on(unhandledRejection/uncaughtException) — глобальная страховка с логированием, процесс не падает от единичной асинхронной ошибки. - pet: атомарный CAS на кулдаунах petAction/starCatch/feedPet (UPDATE ... WHERE last IS ?, начисление только при changes=1) — нет двойного начисления при параллельных запросах. Проверено на семантике node:sqlite. - assistant.flashcardsFromText: await callLLMFailover в try/catch → 502 вместо необработанного отклонения промиса. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -304,10 +304,13 @@ function petAction(req, res) {
|
||||
streak = 1;
|
||||
}
|
||||
|
||||
try { awardCoins(req.user.id, 2, 'pet_petting'); } catch {}
|
||||
db.prepare('UPDATE users SET pet_last_petted=?, pet_petting_streak=? WHERE id=?')
|
||||
.run(now.toISOString(), streak, req.user.id);
|
||||
// CAS: апдейт проходит, только если pet_last_petted не изменился с момента чтения
|
||||
// (IS — null-safe). Защита от гонки двойного начисления при параллельных запросах.
|
||||
const claim = db.prepare('UPDATE users SET pet_last_petted=?, pet_petting_streak=? WHERE id=? AND pet_last_petted IS ?')
|
||||
.run(now.toISOString(), streak, req.user.id, user.pet_last_petted);
|
||||
if (claim.changes === 0) return res.status(429).json({ error: 'cooldown', remaining: 1 });
|
||||
|
||||
try { awardCoins(req.user.id, 2, 'pet_petting'); } catch {}
|
||||
res.json({ ok: true, coins: 2, pettingStreak: streak });
|
||||
}
|
||||
|
||||
@@ -424,8 +427,10 @@ function starCatch(req, res) {
|
||||
const diff = (now - new Date(user.pet_last_star)) / 1000;
|
||||
if (diff < 3600) return res.status(429).json({ error: 'cooldown', remaining: Math.ceil(3600 - diff) });
|
||||
}
|
||||
const claim = db.prepare('UPDATE users SET pet_last_star=? WHERE id=? AND pet_last_star IS ?')
|
||||
.run(now.toISOString(), req.user.id, user.pet_last_star);
|
||||
if (claim.changes === 0) return res.status(429).json({ error: 'cooldown', remaining: 1 });
|
||||
try { awardCoins(req.user.id, 5, 'star_catch'); } catch {}
|
||||
db.prepare('UPDATE users SET pet_last_star=? WHERE id=?').run(now.toISOString(), req.user.id);
|
||||
res.json({ ok: true, coins: 5 });
|
||||
}
|
||||
|
||||
@@ -443,11 +448,14 @@ function feedPet(req, res) {
|
||||
return res.status(429).json({ error: 'cooldown', remaining: Math.ceil(COOLDOWN_SEC - diff) });
|
||||
}
|
||||
}
|
||||
// CAS-«застолбить» кулдаун ДО начисления XP (анти-гонка двойного начисления)
|
||||
const claim = db.prepare('UPDATE users SET pet_last_fed=? WHERE id=? AND pet_last_fed IS ?')
|
||||
.run(now.toISOString(), req.user.id, user.pet_last_fed);
|
||||
if (claim.changes === 0) return res.status(429).json({ error: 'cooldown', remaining: 1 });
|
||||
try {
|
||||
const { awardXP } = require('./gamificationController');
|
||||
awardXP(req.user.id, 15, 'pet_feeding');
|
||||
} catch (e) { console.error('[feedPet] awardXP:', e.message); }
|
||||
db.prepare('UPDATE users SET pet_last_fed=? WHERE id=?').run(now.toISOString(), req.user.id);
|
||||
const updated = db.prepare('SELECT xp, coins FROM users WHERE id=?').get(req.user.id);
|
||||
res.json({ ok: true, xpAwarded: 15, xp: updated.xp, coins: updated.coins });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user