# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

CityWalk 图章收集系统 — 移动端 H5 + 管理后台。游客在城市点位扫描二维码收集图章，每枚图章绑定一个特定奖品（带库存），已收集的图章可兑换对应奖品。兑换后图章保持彩色点亮并标记为"已兑换"，同一图章不可二次收集或二次兑换。

## Commands

```bash
# Development (需要同时运行两个服务)
pnpm dev:server    # Express API on :3000
pnpm dev:web       # Vite dev server on :5173

# Database
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 (16 stamps, each with a Prize of stock 100)

# Build
pnpm build         # Build all packages
```

## Architecture

pnpm monorepo with 3 packages sharing `tsconfig.base.json` (target ES2022, moduleResolution bundler):

- **`packages/shared`** — Prisma client singleton + shared TypeScript types (`ApiResponse<T>`, `StampWithStatus`, etc.)
- **`packages/server`** (port 3000) — Express 5 + Zod validation + JWT auth. Routes: auth, stamps, collection, redemption, admin. File uploads via multer to `packages/server/uploads/`.
- **`packages/web`** (port 5173) — React 19 + Vite 8 + Tailwind CSS 4. Dual interface: mobile H5 (/, /album) + PC admin (/admin/*). Vite proxies `/api` and `/uploads` to server.

### API Response Format

All endpoints return: `{ success: boolean, data?: T, error?: { code: string, message: string } }`

### Authentication

- **Users**: JWT (7-day expiry, single token in localStorage `stamp_token`). Middleware: `requireAuth`, `optionalAuth`.
- **Admin**: API key via `X-Admin-Key` header (matched against `ADMIN_API_KEY` env var). Middleware: `requireAdmin`.

### Frontend Routing

```
/                    → LandingPage (also handles /?stamp={id} for collection popup)
/album               → AlbumPage (stamp grid + redemption)
/collect/:stampId    → Redirects to /?stamp={stampId} (QR code entry point)
/admin               → AdminLogin
/admin/stamps        → Stamp CRUD + QR code generation
/admin/redemptions   → Redemption history + stats
```

### Collection Flow

QR codes encode `/collect/{stampId}` → redirects to `/?stamp={stampId}` → LandingPage shows collection overlay. All interactions (register, collect, close) happen as modals on top of the landing page. The stamp ID is stored in `sessionStorage` during registration to resume collection after auth.

### Redemption Transaction

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

Custom CSS in `packages/web/src/index.css` **must** use `@layer` to avoid overriding Tailwind utilities:

```css
@import "tailwindcss";

@layer base {        /* Reset, CSS variables, body/heading fonts */}
@keyframes ... { }   /* Keyframes stay OUTSIDE layers */
@layer components {  /* .animate-*, .stagger-children, .paper-texture, etc. */}
```

**Why**: In Tailwind CSS v4, unlayered styles always beat `@layer utilities`. If base resets like `* { padding: 0 }` are unlayered, they override Tailwind's `px-4`, `py-3`, etc., breaking all utility spacing across the app.

## Environment

```
DATABASE_URL="file:./dev.db"         # SQLite (relative to project root)
JWT_SECRET="..."                     # JWT signing key
ADMIN_API_KEY="..."                  # Admin panel access key
SERVER_PORT=3000
SITE_URL="http://localhost:5173"     # Used in QR code URL generation
```
