refactor: 兑换机制改为一图章一奖品并引入库存

- 废弃 RedemptionRule(集 N 换 1),新增 Prize 表与 Stamp 1:1 关联
- Redemption 记录直接绑定到 stampId + prizeId + prizeName 快照
- 兑换事务用 updateMany + stock>0 条件作乐观锁
- 兑换后保留 Collection 记录,图章持续彩色点亮并标记"已兑换"
- 用户端入口改为点击已收集图章弹出兑换,库存为 0 时按钮禁用
- 管理后台删除 /admin/rules,奖品编辑嵌入 StampForm

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-20 15:30:28 +08:00
parent 52169ac71d
commit 394b643304
20 changed files with 581 additions and 642 deletions

View File

@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Overview
CityWalk 图章收集系统 — 移动端 H5 + 管理后台。游客在城市点位扫描二维码收集图章,集满可兑换奖品兑换后图章清空,支持重复收集
CityWalk 图章收集系统 — 移动端 H5 + 管理后台。游客在城市点位扫描二维码收集图章,每枚图章绑定一个特定奖品(带库存),已收集的图章可兑换对应奖品兑换后图章保持彩色点亮并标记为"已兑换",同一图章不可二次收集或二次兑换
## Commands
@@ -17,7 +17,7 @@ pnpm dev:web # Vite dev server on :5173
pnpm db:generate # Generate Prisma client after schema changes
pnpm db:migrate # Create and apply migrations (prisma migrate dev)
pnpm db:push # Push schema directly (dev only, no migration file)
pnpm db:seed # Seed sample data (9 stamps + 4 redemption rules)
pnpm db:seed # Seed sample data (16 stamps, each with a Prize of stock 100)
# Build
pnpm build # Build all packages
@@ -48,7 +48,6 @@ All endpoints return: `{ success: boolean, data?: T, error?: { code: string, mes
/collect/:stampId → Redirects to /?stamp={stampId} (QR code entry point)
/admin → AdminLogin
/admin/stamps → Stamp CRUD + QR code generation
/admin/rules → Redemption rule CRUD
/admin/redemptions → Redemption history + stats
```
@@ -58,7 +57,7 @@ QR codes encode `/collect/{stampId}` → redirects to `/?stamp={stampId}` → La
### Redemption Transaction
Atomic: `prisma.$transaction` creates Redemption record + deletes all user Collections. The `@@unique([userId, stampId])` constraint resets after deletion, allowing re-collection.
Each `Stamp` has an optional `Prize` (1:1, `Prize.stampId @unique`). Redemption is atomic: inside `prisma.$transaction` we check the user has a `Collection` for the stamp, no existing `Redemption` for (user, stamp), the prize is `enabled`, then `prisma.prize.updateMany({ where: { id, stock: { gt: 0 } }, data: { stock: { decrement: 1 } } })` acts as a stock lock (throws `OUT_OF_STOCK` if zero rows updated) before creating the `Redemption` record with a `prizeName` snapshot. `Collection` rows are **not** deleted — the `@@unique([userId, stampId])` constraints on both `Collection` and `Redemption` naturally block re-collection and re-redemption of the same stamp.
## Critical: Tailwind CSS v4 Layer Architecture