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:
Maxim Dolgolyov
2026-06-12 22:08:02 +03:00
parent 646e93cf46
commit 09c6c2b21d
4 changed files with 34 additions and 6 deletions
+13 -5
View File
@@ -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 });
}