feat: 支持批量上传关卡
This commit is contained in:
871
EXPLORATION_REPORT.md
Normal file
871
EXPLORATION_REPORT.md
Normal file
@@ -0,0 +1,871 @@
|
||||
# MemeStudio Server Project - Thorough Exploration Report
|
||||
|
||||
## 1. PROJECT OVERVIEW
|
||||
|
||||
**Project Name:** Meme Studio (Homophone Pun Game Operation Platform)
|
||||
**Framework:** Next.js 14 (App Router)
|
||||
**Type:** Backend/Admin Dashboard for a wordplay game
|
||||
**Location:** `/Users/richard/Documents/code/xieyingeng/MemeStudio`
|
||||
|
||||
### Tech Stack
|
||||
- **Framework**: Next.js 14 App Router
|
||||
- **Runtime**: Node.js (Standalone output)
|
||||
- **Language**: TypeScript
|
||||
- **ORM**: Prisma v6.5.0
|
||||
- **Database**: MySQL
|
||||
- **Authentication**: Better Auth v1.2.7 (email/password with Prisma adapter)
|
||||
- **Frontend UI**: shadcn/ui + Tailwind CSS
|
||||
- **State Management**: TanStack Query v5.69.0
|
||||
- **Drag & Drop**: @dnd-kit/sortable
|
||||
- **File Upload**: Tencent COS (Cloud Object Storage)
|
||||
- **Password Hashing**: bcryptjs v3.0.2
|
||||
- **Process Manager**: PM2 (ecosystem.config.js)
|
||||
|
||||
---
|
||||
|
||||
## 2. PROJECT STRUCTURE
|
||||
|
||||
```
|
||||
MemeStudio/
|
||||
├── app/ # Next.js App Router
|
||||
│ ├── (auth)/ # Auth routes (no sidebar)
|
||||
│ │ ├── layout.tsx
|
||||
│ │ └── login/page.tsx
|
||||
│ ├── (dashboard)/ # Protected routes (with sidebar)
|
||||
│ │ ├── layout.tsx
|
||||
│ │ ├── levels/page.tsx # Game levels management
|
||||
│ │ ├── users/page.tsx # Admin user management
|
||||
│ │ └── wx-users/page.tsx # WeChat mini program users
|
||||
│ ├── api/ # API routes
|
||||
│ │ ├── auth/[...all]/ # Better Auth endpoints
|
||||
│ │ ├── levels/ # Level CRUD
|
||||
│ │ ├── users/ # User CRUD
|
||||
│ │ ├── wx-users/ # WeChat user endpoints
|
||||
│ │ ├── cos/temp-key/ # Tencent COS credentials
|
||||
│ │ └── v1/wechat-game/ # (empty, future API)
|
||||
│ ├── layout.tsx # Root layout
|
||||
│ ├── page.tsx # Root page (redirects to /dashboard)
|
||||
│ ├── providers.tsx # Client providers (Query, etc)
|
||||
│ └── globals.css
|
||||
├── lib/ # Core utilities
|
||||
│ ├── auth.ts # Better Auth config
|
||||
│ ├── auth-client.ts # Client-side auth hooks
|
||||
│ ├── prisma.ts # Prisma client singleton
|
||||
│ ├── api.ts # API fetch wrapper with basePath
|
||||
│ ├── cos.ts # Tencent COS utilities
|
||||
│ └── utils.ts # UI utilities (cn())
|
||||
├── components/ # React components
|
||||
│ ├── ui/ # shadcn/ui components (button, card, dialog, input, etc)
|
||||
│ ├── layout/ # Header, sidebar
|
||||
│ ├── levels/ # Level-related components
|
||||
│ ├── users/ # User dialog
|
||||
│ └── wx-users/ # WeChat user detail dialog
|
||||
├── types/index.ts # TypeScript interfaces
|
||||
├── middleware.ts # Next.js middleware (session check)
|
||||
├── prisma/
|
||||
│ ├── schema.prisma # Prisma schema
|
||||
│ └── seed.ts # Admin user seeding
|
||||
├── public/ # Static assets
|
||||
├── next.config.js # Next.js config (basePath, COS remotePatterns)
|
||||
├── tsconfig.json
|
||||
├── tailwind.config.ts
|
||||
├── package.json
|
||||
├── ecosystem.config.js # PM2 config
|
||||
└── deploy.sh # Deployment script
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. AUTHENTICATION & MIDDLEWARE
|
||||
|
||||
### Middleware Pattern (`middleware.ts`)
|
||||
|
||||
```typescript
|
||||
// Non-blocking: API routes, static files, login page
|
||||
// Blocking: All dashboard pages require session cookie
|
||||
|
||||
// Cookie detection:
|
||||
- 'better-auth.session_token' (HTTP)
|
||||
- '__Secure-better-auth.session_token' (HTTPS)
|
||||
|
||||
// Redirect pattern:
|
||||
- Unauthenticated users → /login?callbackUrl=/original/path
|
||||
- Note: basePath (/studio) is NOT included in pathname within middleware
|
||||
```
|
||||
|
||||
### Auth Implementation (`lib/auth.ts`)
|
||||
|
||||
```typescript
|
||||
betterAuth({
|
||||
database: prismaAdapter(prisma),
|
||||
emailAndPassword: { enabled: true },
|
||||
basePath: '/api/auth', // CRITICAL: NO basePath prefix here
|
||||
secret: process.env.BETTER_AUTH_SECRET,
|
||||
session: {
|
||||
expiresIn: 7 days,
|
||||
updateAge: 1 day,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Key Auth Files
|
||||
- `lib/auth.ts` - Server-side Better Auth configuration
|
||||
- `lib/auth-client.ts` - Client-side hooks: `useSession`, `signIn`, `signOut`
|
||||
- `middleware.ts` - Edge Runtime middleware (cannot use Prisma, only cookie check)
|
||||
- `app/api/auth/[...all]/route.ts` - Better Auth endpoints
|
||||
|
||||
---
|
||||
|
||||
## 4. DATABASE SCHEMA (Prisma)
|
||||
|
||||
### Models
|
||||
|
||||
#### 1. **User** (Admin/staff users)
|
||||
```prisma
|
||||
model User {
|
||||
id String @id @default(uuid())
|
||||
email String @unique
|
||||
emailVerified Boolean @default(false)
|
||||
name String?
|
||||
image String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
sessions Session[]
|
||||
accounts Account[]
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. **Session** (Better Auth)
|
||||
```prisma
|
||||
model Session {
|
||||
id String @id
|
||||
expiresAt DateTime
|
||||
token String @unique # Critical for session validation
|
||||
ipAddress String?
|
||||
userAgent String?
|
||||
userId String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. **Account** (Provider credentials)
|
||||
```prisma
|
||||
model Account {
|
||||
id String @id @default(uuid())
|
||||
accountId String
|
||||
providerId String # 'credential' for email/password
|
||||
userId String
|
||||
password String? # Only for 'credential' provider
|
||||
accessToken String?
|
||||
refreshToken String?
|
||||
# ... expires, scope, tokens
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. **Verification** (Email verification tokens)
|
||||
```prisma
|
||||
model Verification {
|
||||
id String @id @default(uuid())
|
||||
identifier String
|
||||
value String
|
||||
expiresAt DateTime
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
```
|
||||
|
||||
#### 5. **Level** (Game levels)
|
||||
```prisma
|
||||
model Level {
|
||||
id String @id @default(uuid())
|
||||
imageUrl String # COS URL
|
||||
answer String # Correct answer (Chinese pun text)
|
||||
hint1 String?
|
||||
hint2 String?
|
||||
hint3 String?
|
||||
sortOrder Int @default(0) # Display order
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
userProgress WxUserLevelProgress[]
|
||||
}
|
||||
```
|
||||
|
||||
#### 6. **WxUser** (WeChat mini program players)
|
||||
```prisma
|
||||
model WxUser {
|
||||
id String @id @default(uuid())
|
||||
openid String @unique # WeChat OpenID
|
||||
sessionKey String? # WeChat session key
|
||||
nickname String?
|
||||
avatarUrl String?
|
||||
points Int @default(10)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
levelProgress WxUserLevelProgress[]
|
||||
}
|
||||
```
|
||||
|
||||
#### 7. **WxUserLevelProgress** (Game progress tracking)
|
||||
```prisma
|
||||
model WxUserLevelProgress {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
levelId String
|
||||
completedAt DateTime @default(now()) # When level was completed
|
||||
|
||||
user WxUser @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
level Level @relation(fields: [levelId], references: [id])
|
||||
}
|
||||
```
|
||||
|
||||
### Database Configuration
|
||||
- **Provider**: MySQL
|
||||
- **Binary Targets**: `["native", "rhel-openssl-3.0.x"]` (macOS dev → Linux server compatibility)
|
||||
- **Environment Variable**: `DATABASE_URL=mysql://...`
|
||||
|
||||
---
|
||||
|
||||
## 5. API ROUTES & ENDPOINTS
|
||||
|
||||
### 5.1 Authentication Endpoints
|
||||
**Route:** `/api/auth/[...all]`
|
||||
**Handler:** Better Auth (`toNextJsHandler(auth)`)
|
||||
**Methods:** GET, POST
|
||||
**Auth Required:** No (this is the auth endpoint itself)
|
||||
|
||||
Common endpoints:
|
||||
- `POST /api/auth/sign-in/email` - Email sign in
|
||||
- `POST /api/auth/sign-out` - Sign out
|
||||
- `POST /api/auth/sign-up/email` - Create new account (might be disabled)
|
||||
|
||||
---
|
||||
|
||||
### 5.2 Levels API
|
||||
**Route:** `/api/levels`
|
||||
**Auth Required:** Yes (session check)
|
||||
|
||||
#### GET - Fetch all levels
|
||||
```typescript
|
||||
GET /api/levels
|
||||
Response: Level[]
|
||||
Sorting: By sortOrder (ASC)
|
||||
```
|
||||
|
||||
#### POST - Create level
|
||||
```typescript
|
||||
POST /api/levels
|
||||
Body: {
|
||||
imageUrl: string (required),
|
||||
answer: string (required),
|
||||
hint1?: string,
|
||||
hint2?: string,
|
||||
hint3?: string
|
||||
}
|
||||
Response: Level (with auto-generated UUID, sortOrder = max+1)
|
||||
```
|
||||
|
||||
#### PUT - Update level
|
||||
```typescript
|
||||
PUT /api/levels
|
||||
Body: {
|
||||
id: string (required),
|
||||
imageUrl?: string,
|
||||
answer?: string,
|
||||
hint1?: string,
|
||||
hint2?: string,
|
||||
hint3?: string
|
||||
}
|
||||
Response: Updated Level
|
||||
```
|
||||
|
||||
#### DELETE - Delete level
|
||||
```typescript
|
||||
DELETE /api/levels?id=<levelId>
|
||||
Response: { success: true }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5.3 Levels Reorder API
|
||||
**Route:** `/api/levels/reorder`
|
||||
**Auth Required:** Yes
|
||||
|
||||
#### PUT - Batch update sort order
|
||||
```typescript
|
||||
PUT /api/levels/reorder
|
||||
Body: {
|
||||
orders: Array<{ id: string, sortOrder: number }>
|
||||
}
|
||||
Response: { success: true }
|
||||
Implementation: Prisma $transaction (atomic operation)
|
||||
```
|
||||
|
||||
**Use Case:** Drag & drop reordering in UI
|
||||
|
||||
---
|
||||
|
||||
### 5.4 Users API (Admin)
|
||||
**Route:** `/api/users`
|
||||
**Auth Required:** Yes
|
||||
**Purpose:** Manage admin/staff accounts
|
||||
|
||||
#### GET - Fetch all users
|
||||
```typescript
|
||||
GET /api/users
|
||||
Response: Array<{
|
||||
id, email, emailVerified, name, image, createdAt, updatedAt
|
||||
}>
|
||||
Sorting: By createdAt DESC
|
||||
```
|
||||
|
||||
#### POST - Create new admin user
|
||||
```typescript
|
||||
POST /api/users
|
||||
Body: {
|
||||
email: string (required),
|
||||
password: string (required),
|
||||
name?: string
|
||||
}
|
||||
Response: User object
|
||||
- Auto-generates UUID for user ID
|
||||
- Hashes password with better-auth/crypto
|
||||
- Creates User + Account records in transaction
|
||||
- Error: "该邮箱已被注册" if email exists
|
||||
```
|
||||
|
||||
#### PUT - Update user
|
||||
```typescript
|
||||
PUT /api/users
|
||||
Body: {
|
||||
id: string (required),
|
||||
email?: string,
|
||||
password?: string,
|
||||
name?: string
|
||||
}
|
||||
Response: Updated User
|
||||
- Can update email (checks uniqueness vs other users)
|
||||
- Can update password (hashes with better-auth)
|
||||
- Transaction: Updates User + Account records
|
||||
```
|
||||
|
||||
#### DELETE - Delete user
|
||||
```typescript
|
||||
DELETE /api/users?id=<userId>
|
||||
Response: { success: true }
|
||||
- Prevents self-deletion: "不能删除自己的账户"
|
||||
- Cascading delete via Prisma relations
|
||||
```
|
||||
|
||||
**Password Hashing:** Always uses `hashPassword` from `better-auth/crypto` for consistency
|
||||
|
||||
---
|
||||
|
||||
### 5.5 WeChat Users API
|
||||
**Route:** `/api/wx-users`
|
||||
**Auth Required:** Yes
|
||||
**Purpose:** Manage WeChat mini program player accounts
|
||||
|
||||
#### GET - List WeChat users (paginated + search)
|
||||
```typescript
|
||||
GET /api/wx-users?search=<text>&page=<1>&limit=<20>
|
||||
Response: {
|
||||
users: Array<WxUser>,
|
||||
meta: { total, page, limit, totalPages }
|
||||
}
|
||||
Filtering: Search by nickname OR openid (contains search)
|
||||
Sorting: By createdAt DESC
|
||||
Dynamic Rendering: export const dynamic = 'force-dynamic'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5.6 WeChat User Detail
|
||||
**Route:** `/api/wx-users/[id]`
|
||||
**Auth Required:** Yes
|
||||
**Method:** GET
|
||||
|
||||
```typescript
|
||||
GET /api/wx-users/<userId>
|
||||
Response: WxUser with nested levelProgress array
|
||||
Include:
|
||||
- All WxUser fields
|
||||
- levelProgress: Array of {
|
||||
id,
|
||||
userId,
|
||||
levelId,
|
||||
completedAt,
|
||||
level: { id, answer }
|
||||
}
|
||||
Sorting: levelProgress by completedAt DESC
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5.7 WeChat User Level Progress
|
||||
**Route:** `/api/wx-users/level-progress`
|
||||
**Auth Required:** Yes
|
||||
**Method:** DELETE
|
||||
|
||||
```typescript
|
||||
DELETE /api/wx-users/level-progress
|
||||
Body: {
|
||||
ids: Array<string> # Array of progress record IDs
|
||||
}
|
||||
Response: { deleted: number }
|
||||
Validation: ids must be non-empty array of strings
|
||||
```
|
||||
|
||||
**Use Case:** Batch delete player progress (e.g., reset levels)
|
||||
|
||||
---
|
||||
|
||||
### 5.8 Tencent COS Credentials
|
||||
**Route:** `/api/cos/temp-key`
|
||||
**Auth Required:** Yes
|
||||
**Method:** GET
|
||||
|
||||
```typescript
|
||||
GET /api/cos/temp-key
|
||||
Response: {
|
||||
credentials: {
|
||||
tmpSecretId: string,
|
||||
tmpSecretKey: string,
|
||||
sessionToken: string
|
||||
},
|
||||
startTime: number,
|
||||
expiredTime: number,
|
||||
bucket: string,
|
||||
region: string
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation Details:**
|
||||
- Uses `qcloud-cos-sts` library
|
||||
- Policy allows: `cos:PutObject`, `cos:PostObject`
|
||||
- Limited to: `mini_game/images/*` directory
|
||||
- Duration: 30 minutes (1800 seconds)
|
||||
- Returns bucket config for frontend upload
|
||||
|
||||
---
|
||||
|
||||
### 5.9 Empty/Stub Routes
|
||||
- `/api/v1/wechat-game/` - Directory structure exists but no handlers
|
||||
|
||||
---
|
||||
|
||||
## 6. LIB UTILITIES
|
||||
|
||||
### 6.1 `lib/auth.ts` - Better Auth Configuration
|
||||
```typescript
|
||||
export const auth = betterAuth({
|
||||
database: prismaAdapter(prisma),
|
||||
emailAndPassword: { enabled: true },
|
||||
basePath: '/api/auth', // CRITICAL GOTCHA
|
||||
trustedOrigins: [process.env.BETTER_AUTH_URL],
|
||||
secret: process.env.BETTER_AUTH_SECRET,
|
||||
session: { expiresIn: 7 days, updateAge: 1 day }
|
||||
})
|
||||
export type Auth = typeof auth
|
||||
```
|
||||
|
||||
**Critical Gotchas:**
|
||||
1. `basePath` must be `/api/auth` (NOT `/studio/api/auth`)
|
||||
2. `BETTER_AUTH_URL` must be origin-only (NOT include path)
|
||||
3. Better Auth's `withPath()` silently ignores basePath if URL has path component
|
||||
|
||||
---
|
||||
|
||||
### 6.2 `lib/auth-client.ts` - Client Auth Hooks
|
||||
```typescript
|
||||
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || '/studio'
|
||||
const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3001'
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
baseURL: `${appUrl}${basePath}/api/auth`
|
||||
})
|
||||
|
||||
export const { signIn, signOut, useSession } = authClient
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
- `useSession()` - Hook to get current session
|
||||
- `signIn(email, password)` - Email/password login
|
||||
- `signOut()` - Logout
|
||||
|
||||
---
|
||||
|
||||
### 6.3 `lib/prisma.ts` - Prisma Singleton
|
||||
```typescript
|
||||
const globalForPrisma = globalThis as unknown as {
|
||||
prisma: PrismaClient | undefined
|
||||
}
|
||||
|
||||
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
|
||||
|
||||
if (process.env.NODE_ENV !== 'production')
|
||||
globalForPrisma.prisma = prisma
|
||||
```
|
||||
|
||||
**Purpose:** Prevent multiple Prisma client instances in dev mode
|
||||
|
||||
---
|
||||
|
||||
### 6.4 `lib/api.ts` - API Fetch Helper
|
||||
```typescript
|
||||
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || ''
|
||||
|
||||
export function apiFetch(input: string, init?: RequestInit): Promise<Response> {
|
||||
const url = input.startsWith('/') ? `${basePath}${input}` : input
|
||||
return fetch(url, init)
|
||||
}
|
||||
```
|
||||
|
||||
**Purpose:** Wrapper that auto-prepends basePath to relative URLs (client-side)
|
||||
|
||||
---
|
||||
|
||||
### 6.5 `lib/cos.ts` - Tencent COS Utilities
|
||||
```typescript
|
||||
interface TempKeyResult {
|
||||
credentials: {
|
||||
tmpSecretId: string
|
||||
tmpSecretKey: string
|
||||
sessionToken: string
|
||||
}
|
||||
startTime: number
|
||||
expiredTime: number
|
||||
}
|
||||
|
||||
export function getBucketName(): string
|
||||
// Returns "bucket-appid" format
|
||||
|
||||
export async function getTempKey(): Promise<TempKeyResult>
|
||||
// STS.getCredential with 30-min duration
|
||||
// Policy: PUT/POST to mini_game/images/*
|
||||
|
||||
export function getBucketConfig()
|
||||
// Returns { bucket, region }
|
||||
```
|
||||
|
||||
**Environment Variables:**
|
||||
- `COS_SECRET_ID`
|
||||
- `COS_SECRET_KEY`
|
||||
- `COS_BUCKET`
|
||||
- `COS_REGION` (default: ap-guangzhou)
|
||||
- `COS_APPID`
|
||||
|
||||
---
|
||||
|
||||
### 6.6 `lib/utils.ts` - UI Utilities
|
||||
```typescript
|
||||
export function cn(...inputs: ClassValue[])
|
||||
// Combines clsx + tailwind-merge for class deduplication
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. ENVIRONMENT VARIABLES
|
||||
|
||||
**Required Variables:**
|
||||
```
|
||||
DATABASE_URL=mysql://user:pass@host:port/dbname
|
||||
BETTER_AUTH_SECRET=<32+ chars, openssl rand -base64 32>
|
||||
BETTER_AUTH_URL=http://localhost:3001 # Origin only, NO path
|
||||
NEXT_PUBLIC_APP_URL=http://localhost:3001
|
||||
NEXT_PUBLIC_BASE_PATH=/studio
|
||||
ADMIN_EMAIL=admin@example.com
|
||||
ADMIN_PASSWORD=password123
|
||||
COS_SECRET_ID=<tencent cloud secret>
|
||||
COS_SECRET_KEY=<tencent cloud secret>
|
||||
COS_BUCKET=bucket-name
|
||||
COS_REGION=ap-guangzhou
|
||||
COS_APPID=<tencent app id>
|
||||
```
|
||||
|
||||
**Production Considerations:**
|
||||
- `.env.production` exists alongside `.env`
|
||||
- `BETTER_AUTH_SECRET` should be 32+ characters
|
||||
- HTTPS deployment adds `__Secure-` cookie prefix
|
||||
- Database should use SSL connection
|
||||
|
||||
---
|
||||
|
||||
## 8. DEPLOYMENT ARCHITECTURE
|
||||
|
||||
### Standalone Output
|
||||
```javascript
|
||||
// next.config.js
|
||||
module.exports = {
|
||||
output: 'standalone', # Self-contained binary
|
||||
basePath: '/studio', # Reverse proxy path
|
||||
images: {
|
||||
remotePatterns: [{
|
||||
protocol: 'https',
|
||||
hostname: '*.myqcloud.com' # Tencent COS pattern
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Deployment Flow (deploy.sh)
|
||||
1. Build locally: `next build` (creates .next/standalone)
|
||||
2. rsync files to server (excluding node_modules)
|
||||
3. SSH to server: `npm install --production`
|
||||
4. Generate Prisma: `npx prisma generate`
|
||||
5. PM2 restart: `pm2 restart ecosystem.config.js`
|
||||
|
||||
### Server Details
|
||||
- **Host:** root@119.91.211.52
|
||||
- **Path:** /root/apps/meme-studio
|
||||
- **Process Manager:** PM2
|
||||
- **Config:** ecosystem.config.js
|
||||
|
||||
### PM2 Configuration
|
||||
```javascript
|
||||
// ecosystem.config.js (typical setup)
|
||||
module.exports = {
|
||||
apps: [{
|
||||
name: 'meme-studio',
|
||||
script: './.next/standalone/server.js',
|
||||
instances: 1,
|
||||
exec_mode: 'cluster'
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. CRITICAL GOTCHAS & IMPORTANT NOTES
|
||||
|
||||
### 9.1 basePath Handling
|
||||
- ✅ `next.config.js` sets `basePath: '/studio'`
|
||||
- ✅ All pages/API routes served under `/studio/...`
|
||||
- ❌ **NEVER** include `/studio` in `BETTER_AUTH_URL` or hardcoded paths
|
||||
- ⚠️ `request.nextUrl.pathname` in middleware EXCLUDES basePath (shows `/levels` not `/studio/levels`)
|
||||
- ⚠️ Next.js strips basePath from `request.url` in route handlers
|
||||
|
||||
### 9.2 Authentication Patterns
|
||||
- ✅ Session validation: `await auth.api.getSession({ headers: request.headers })`
|
||||
- ✅ Session check in middleware uses cookies (Edge Runtime compatible)
|
||||
- ⚠️ Password hashing: ALWAYS use `hashPassword` from `better-auth/crypto`
|
||||
- ⚠️ Session token field has unique constraint in database
|
||||
|
||||
### 9.3 Database & ORM
|
||||
- ✅ Prisma adapter with MySQL provider
|
||||
- ✅ Binary targets: `["native", "rhel-openssl-3.0.x"]` for cross-platform deployment
|
||||
- ⚠️ Middleware cannot use Prisma (Edge Runtime incompatible)
|
||||
- ⚠️ Transaction usage for consistency: `prisma.$transaction([...])`
|
||||
|
||||
### 9.4 File Upload (COS)
|
||||
- ✅ Frontend gets temporary credentials via `/api/cos/temp-key`
|
||||
- ✅ Browser uploads directly to COS (S3-compatible)
|
||||
- ✅ Limited to `mini_game/images/*` directory by policy
|
||||
- ⚠️ Policy duration: 30 minutes (1800 seconds)
|
||||
|
||||
### 9.5 Git & Development
|
||||
- ⚠️ **Commit messages must be written in Chinese**
|
||||
- ✅ ESLint configured: `npm run lint`
|
||||
- ✅ Dev server runs on port 3001: `npm run dev`
|
||||
- ✅ Database seeding: `npm run db:seed` (creates/updates admin user)
|
||||
|
||||
---
|
||||
|
||||
## 10. SHARE/INVITE LOGIC
|
||||
|
||||
**Current Status:** ❌ **NO existing share or invite functionality**
|
||||
|
||||
No database models, API endpoints, or UI components for:
|
||||
- Sharing levels with other users
|
||||
- Generating shareable links/tokens
|
||||
- Inviting users to teams or projects
|
||||
- Publishing/unpublishing levels
|
||||
- Level access control
|
||||
|
||||
**All levels are:**
|
||||
- Managed by admin users
|
||||
- Accessible to all authenticated admins
|
||||
- Automatically available in the WeChat mini program
|
||||
|
||||
---
|
||||
|
||||
## 11. KEY PATTERNS USED
|
||||
|
||||
### 11.1 API Route Pattern
|
||||
All protected API routes follow:
|
||||
```typescript
|
||||
import { auth } from '@/lib/auth'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const session = await auth.api.getSession({ headers: request.headers })
|
||||
if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
|
||||
// ... handler logic
|
||||
}
|
||||
```
|
||||
|
||||
### 11.2 Error Handling
|
||||
- 400: Bad request (validation errors)
|
||||
- 401: Unauthorized (no session)
|
||||
- 404: Not found (resource doesn't exist)
|
||||
- 500: Server error (logged to console)
|
||||
|
||||
### 11.3 Response Format
|
||||
- Success: `NextResponse.json(data)`
|
||||
- Error: `NextResponse.json({ error: string }, { status: number })`
|
||||
- Batch operations: `{ success: true }` or `{ deleted: count }`
|
||||
|
||||
### 11.4 Transaction Pattern
|
||||
```typescript
|
||||
await prisma.$transaction(async (tx) => {
|
||||
const user = await tx.user.create({ data: {...} })
|
||||
await tx.account.create({ data: {...} })
|
||||
return user
|
||||
})
|
||||
```
|
||||
|
||||
### 11.5 Pagination Pattern
|
||||
```typescript
|
||||
const skip = (page - 1) * limit
|
||||
const [data, total] = await Promise.all([
|
||||
prisma.model.findMany({ skip, take: limit }),
|
||||
prisma.model.count()
|
||||
])
|
||||
return { data, meta: { total, page, limit, totalPages: Math.ceil(total/limit) } }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. FILE STRUCTURE SUMMARY
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `middleware.ts` | Session validation, redirect to login |
|
||||
| `next.config.js` | basePath, COS image patterns, standalone output |
|
||||
| `lib/auth.ts` | Better Auth server configuration |
|
||||
| `lib/auth-client.ts` | Client-side auth hooks |
|
||||
| `lib/prisma.ts` | Prisma singleton |
|
||||
| `lib/api.ts` | Fetch wrapper with basePath |
|
||||
| `lib/cos.ts` | Tencent COS STS credentials |
|
||||
| `prisma/schema.prisma` | Database schema (7 models) |
|
||||
| `prisma/seed.ts` | Admin user seeding |
|
||||
| `app/api/auth/[...all]/route.ts` | Better Auth endpoints |
|
||||
| `app/api/levels/route.ts` | Level CRUD (GET, POST, PUT, DELETE) |
|
||||
| `app/api/levels/reorder/route.ts` | Batch reorder (PUT) |
|
||||
| `app/api/users/route.ts` | Admin user CRUD (GET, POST, PUT, DELETE) |
|
||||
| `app/api/wx-users/route.ts` | List WeChat users (GET, paginated) |
|
||||
| `app/api/wx-users/[id]/route.ts` | Get user + progress (GET) |
|
||||
| `app/api/wx-users/level-progress/route.ts` | Delete progress (DELETE) |
|
||||
| `app/api/cos/temp-key/route.ts` | Get COS credentials (GET) |
|
||||
| `app/(dashboard)/levels/page.tsx` | Levels management UI |
|
||||
| `app/(dashboard)/users/page.tsx` | Admin users management UI |
|
||||
| `app/(dashboard)/wx-users/page.tsx` | WeChat users viewer UI |
|
||||
| `app/(auth)/login/page.tsx` | Login page |
|
||||
| `types/index.ts` | TypeScript interfaces (Level, User, WxUser, etc) |
|
||||
|
||||
---
|
||||
|
||||
## 13. COMPONENT STRUCTURE
|
||||
|
||||
**UI Components (shadcn/ui):**
|
||||
- `button.tsx` - Button component
|
||||
- `card.tsx` - Card container
|
||||
- `dialog.tsx` - Modal dialog
|
||||
- `input.tsx` - Text input
|
||||
- `label.tsx` - Form label
|
||||
- `textarea.tsx` - Textarea input
|
||||
- `spinner.tsx` - Loading spinner
|
||||
|
||||
**Layout Components:**
|
||||
- `header.tsx` - Top navigation header
|
||||
- `sidebar.tsx` - Left sidebar with navigation
|
||||
|
||||
**Feature Components:**
|
||||
- `levels/level-dialog.tsx` - Create/edit level modal
|
||||
- `levels/level-list.tsx` - Drag-and-drop sortable list
|
||||
- `levels/level-card.tsx` - Individual level card
|
||||
- `levels/image-uploader.tsx` - COS image upload
|
||||
- `users/user-dialog.tsx` - Create/edit admin user modal
|
||||
- `wx-users/wx-user-detail-dialog.tsx` - View WeChat user progress
|
||||
|
||||
---
|
||||
|
||||
## 14. DATA FLOW EXAMPLES
|
||||
|
||||
### Example 1: Create Level
|
||||
1. Admin clicks "Add Level" button
|
||||
2. UI opens `level-dialog.tsx` modal
|
||||
3. Admin fills form (imageUrl, answer, hints)
|
||||
4. `image-uploader.tsx` gets temp COS credentials from `/api/cos/temp-key`
|
||||
5. Browser uploads image directly to COS
|
||||
6. Admin submits form
|
||||
7. API calls `POST /api/levels` with imageUrl, answer, hints
|
||||
8. Backend validates session, creates Level record
|
||||
9. sortOrder auto-calculated (max+1)
|
||||
10. Level appears in list (sorted by sortOrder)
|
||||
|
||||
### Example 2: Reorder Levels
|
||||
1. Admin drags level up/down in `level-list.tsx`
|
||||
2. @dnd-kit/sortable updates UI state
|
||||
3. Admin confirms or auto-save triggers
|
||||
4. API calls `PUT /api/levels/reorder` with reordered IDs
|
||||
5. Backend updates sortOrder for all levels in transaction
|
||||
6. List re-renders in new order
|
||||
|
||||
### Example 3: View WeChat Player
|
||||
1. Admin opens "WeChat Users" dashboard page
|
||||
2. `wx-users/page.tsx` calls `GET /api/wx-users?page=1&limit=20`
|
||||
3. Backend queries paginated WxUser list
|
||||
4. Admin clicks on player name
|
||||
5. Opens `wx-user-detail-dialog.tsx`
|
||||
6. Component calls `GET /api/wx-users/<id>`
|
||||
7. Backend returns WxUser + nested levelProgress array
|
||||
8. Dialog displays player info and completed levels
|
||||
|
||||
---
|
||||
|
||||
## 15. SECURITY CONSIDERATIONS
|
||||
|
||||
### Implemented
|
||||
- ✅ Session-based authentication (Better Auth)
|
||||
- ✅ Password hashing (bcryptjs via Better Auth)
|
||||
- ✅ CSRF protection (Better Auth built-in)
|
||||
- ✅ Middleware session validation
|
||||
- ✅ COS temporary credentials (limited scope, 30-min expiry)
|
||||
- ✅ SQL injection protection (Prisma ORM)
|
||||
- ✅ Email uniqueness constraints
|
||||
|
||||
### Not Implemented
|
||||
- ❌ Role-based access control (all authenticated users have admin access)
|
||||
- ❌ Audit logging
|
||||
- ❌ Rate limiting
|
||||
- ❌ Input validation/sanitization (rely on Prisma types)
|
||||
- ❌ API key authentication (only cookie-based)
|
||||
- ❌ CORS configuration (not needed for same-origin)
|
||||
|
||||
---
|
||||
|
||||
## SUMMARY
|
||||
|
||||
MemeStudio is a **well-structured Next.js admin dashboard** for managing a WeChat mini program game. It has:
|
||||
|
||||
- **Clean Auth**: Better Auth with Prisma adapter, session-based
|
||||
- **Full CRUD**: Levels, admin users, WeChat player tracking
|
||||
- **Cloud Integration**: Tencent COS for image uploads
|
||||
- **Scalable ORM**: Prisma with MySQL
|
||||
- **No Sharing Logic**: All data is globally accessible to admins
|
||||
|
||||
**For Adding Share/Invite Feature:**
|
||||
- Add `SharedLevel` or `LevelAccess` model (permissions)
|
||||
- Add `InviteToken` model (temporary join links)
|
||||
- Add `/api/invites` endpoints (generate, accept, list)
|
||||
- Add role/permission fields to User model
|
||||
- Add UI for managing shares/invites
|
||||
|
||||
Reference in New Issue
Block a user