The Liquidation Stakes · Next.js + Prisma + MySQL · Luồng nghiệp vụ

Luồng nghiệp vụ — từng bước cụ thể

3 luồng độc lập nhưng ăn khớp nhau: Đăng bài → AI Judge → HP, Cron drain → Kill → Museum, và CPR + Burning Karma. Click vào từng bước để xem chi tiết kỹ thuật, điều kiện, và điểm dễ sai.

spam / quá nhanh hợp lệ async queue score ≥ 6 score ≤ 2 score 3–5 User gửi bài đăng POST /api/groups/:id/posts Express.js route handler Kiểm tra rate limit + hash 3 posts/phút · sha256(content) Là spam? Trùng nội dung? Reject 429 / 409 Trả lỗi, không ghi DB Lưu bài vào DB Queue BullMQ job: ai-judge Trả 202 Accepted ngay UI hiện badge "Đang đánh giá..." AI Judge — LLM chấm điểm Semantic score 0–10 · cache Redis 1h Score? 0–10 Deep Talk +5 HP cho nhóm Parasite −1 HP cho nhóm Neutral 0 HP delta Update GroupEnergy current ±= hpDelta · save PostQualityLog Update lastPostAt = now() Reset đồng hồ drain của nhóm ✅ Bài đã được xử lý
xử lý điều kiện AI / LLM database thành công lỗi / reject
Tóm tắt từng bước — Đăng bài + AI Judge
01
Rate limit + hash check guard — 3 posts/phút/user (Redis counter). Nếu vượt → 429. Tính sha256(content), check Redis cache: nếu đã judge nội dung này → lấy kết quả cũ ngay, không gọi LLM lại.
02
Lưu DB + queue job async — Save post vào DB với qualityScore = null. Push job ai-judge:{postId} vào BullMQ. Trả 202 Accepted ngay — không đợi LLM.
03
UI badge "Đang đánh giá" — Client hiện badge chờ trên bài. Poll GET /api/posts/:id/score mỗi 5s cho đến khi qualityScore != null. Hoặc dùng SSE nhẹ hơn polling.
04
LLM chấm điểm AI — BullMQ worker gọi LLM với prompt cứng, nhận JSON {score, reason}. Cache kết quả theo sha256(content) trong Redis 1h. Parse safe: nếu LLM trả sai shape → score = 5 (neutral, không cộng/trừ HP).
05
HP delta theo score — score ≥ 6 → Deep Talk +5 HP. Score ≤ 2 → Parasite −1 HP. Score 3–5 → 0. Cập nhật GroupEnergy.current với Math.min(max, Math.max(0, current + delta)). Lưu PostQualityLog.
06
Reset lastPostAt DB — Dù bài là spam hay Deep Talk, chỉ cần POST thành công là lastPostAt = now(). Đây là field cron dùng để xác định nhóm có im lặng không — nên cần update sau mỗi post hợp lệ.
có post gần đây im lặng HP = 0 HP > 0 async Update GroupEnergy current −= 5, log drainAt Cron: drain-energy.cron.ts Trigger mỗi giờ đúng node-cron: "0 * * * *" Query nhóm im lặng lastPostAt < now() − 1h · deletedAt = null Nhóm nào im lặng > 1h? Skip — không làm gì Vòng lặp sang nhóm kế Trừ 5 HP từ GroupEnergy Math.max(0, current − 5) HP = 0? Nhóm chết? Soft-delete nhóm deletedAt = now() · ẩn khỏi feed Queue job: gen-soul-stone BullMQ · không block soft-delete LLM tóm tắt "cuộc đời" nhóm Top 10 posts · thống kê · lý do tử vong Lưu GroupSoulStone summary · peakHp · livedDays · topPosts Grant Ashes Badge Cho các thành viên đã sacrifice ✅ Soul Stone live tại /museum
cron job điều kiện AI / LLM database kill path museum
Tóm tắt từng bước — Cron drain + Kill + Museum
01
Cron mỗi giờ cronnode-cron "0 * * * *". Query tất cả nhóm chưa xóa có lastPostAt < now() - 1h. Xử lý từng nhóm theo vòng lặp — không batch update để tránh bỏ sót kill trigger.
02
Trừ HP + tính bù critical — Trừ 5 HP. Nhưng nếu server restart → bỏ qua chu kỳ. Fix: lưu lastDrainAt vào GroupEnergy. Khi cron chạy: tính missedCycles = floor((now - lastDrainAt) / 1h) và trừ bù missedCycles × 5.
03
Soft-delete khi HP = 0 kill — Set deletedAt = now(). Nhóm biến khỏi feed ngay lập tức (query luôn thêm WHERE deletedAt IS NULL). Không xóa vật lý — giữ dữ liệu cho Soul Stone generation.
04
Soul Stone async AI — Queue job riêng để tránh block cron. Job lấy top 10 posts (by qualityScore), thống kê, gọi LLM tóm tắt ~100 chữ văn bia. Museum page filter WHERE soulStone IS NOT NULL nên không có "nhóm chết chưa có stone".
05
Xóa vật lý sau 24h cleanup — Cron purge-dead-groups chạy mỗi đêm: tìm nhóm có deletedAt < now() - 24hsoulStone IS NOT NULL → xóa thật. Đảm bảo Stone đã được gen xong trước khi mất data.
Admin Thành viên đủ HP delta thiếu không đủ đủ karma Phát hiện: HP < 10% (hấp hối) UI hiện trạng thái DYING Glitch effect · nút CPR + Sacrifice xuất hiện Ai kích hoạt? Admin: kích hoạt CPR POST /groups/:id/cpr Tạo CprSession expiresAt = now() + 24h Cửa sổ 24h mở Members comment → AI judge mỗi bài Tổng HP delta ≥ 50? CPR thành công Resolved = true CPR thất bại Nhóm tiếp tục hấp hối User mở Sacrifice Modal Nhập số Karma muốn đốt POST /groups/:id/sacrifice karmaAmount · userId user.karma ≥ amount? 400 Không đủ Trả lỗi, không làm gì prisma.$transaction 1. Trừ karma user 2. Cộng HP nhóm (×10, max 500) Tạo UserRestriction 48h type: POST_LIMIT · không đăng bài được Karma sacrifice xong HP đã được cộng Hồi phục nhóm +50% HP GroupEnergy.current = max × 0.5 Ghi UserKarmaLog karmaSpent · hpGained · groupId Badge "Ashes" cho user Hiển thị trên profile vĩnh viễn ✅ Nhóm được cứu
điều kiện AI judge transaction hồi sinh thất bại trạng thái
Tóm tắt từng bước — CPR + Burning Karma
01
Phát hiện HP < 10% — Client polling energyPercent mỗi 30s. Khi < 10% → áp class .dying lên UI: glitch CSS, blur posts, hiện 2 nút: CPR Post (admin only) và Burning Karma (tất cả members).
02
CPR Post — cửa sổ 24h AI — Admin kích hoạt → tạo CprSession với expiresAt = now() + 24h. Mọi comment trong cửa sổ được AI judge bình thường. Cron check-cpr chạy sau 24h: tính tổng hpDelta của tất cả comments trong session. Nếu ≥ 50 → success.
03
Burning Karma — race condition critical — Nếu 2 user sacrifice cùng lúc → dùng prisma.$transaction với SELECT FOR UPDATE trên row karma. Chỉ 1 transaction chạy qua, cái còn lại rollback → 409 Conflict. Không dùng optimistic locking vì karma là resource critical.
04
Tỷ lệ quy đổi + cap — 1 Karma = 10 HP. Max 500 HP/lần sacrifice (50% HP tối đa). Tính: hpGain = Math.min(karmaAmount × 10, 500). Restriction 48h tạo trong cùng transaction — nếu transaction fail thì restriction cũng không tạo.
05
Ashes Badge reward — Sau khi sacrifice thành công hoặc nhóm được hồi sinh qua CPR (với member đã contribute deep talk), grant badge ASHES_HERO vào UserBadge table. Badge hiển thị vĩnh viễn trên profile, ngay cả sau khi nhóm chết hẳn.