Files
mp-pilates/docs/superpowers/plans/2026-04-02-pilates-booking-miniprogram.md
richarjiang 90b54d1138 feat: scaffold monorepo with shared types and NestJS server
- pnpm workspace with packages/app, packages/server, packages/shared
- @mp-pilates/shared: enums, constants, TypeScript interfaces for all 8 data models
- @mp-pilates/server: NestJS bootstrap with health check, validation pipe, CORS
- Base TypeScript config with strict mode
2026-04-02 11:37:35 +08:00

36 KiB

普拉提私教约课小程序 — 实施计划

Spec: 2026-04-02-pilates-booking-miniprogram-design.md Created: 2026-04-02


Phase 1: Monorepo 基础设施

Task 1.1: 初始化 pnpm monorepo

Files to create/modify:

  • package.json (root)
  • pnpm-workspace.yaml
  • tsconfig.base.json
  • .gitignore
  • .nvmrc
  • .npmrc

Steps:

  1. Create root package.json:
{
  "name": "mp-pilates",
  "private": true,
  "scripts": {
    "dev:server": "pnpm --filter @mp-pilates/server dev",
    "dev:app": "pnpm --filter @mp-pilates/app dev:mp-weixin",
    "build:shared": "pnpm --filter @mp-pilates/shared build",
    "build:server": "pnpm --filter @mp-pilates/server build",
    "build:app": "pnpm --filter @mp-pilates/app build:mp-weixin",
    "test": "pnpm -r test",
    "lint": "pnpm -r lint"
  },
  "engines": {
    "node": ">=18",
    "pnpm": ">=8"
  }
}
  1. Create pnpm-workspace.yaml:
packages:
  - 'packages/*'
  1. Create tsconfig.base.json with shared compiler options (strict, ESNext, paths for @mp-pilates/shared).

  2. Create .gitignore (node_modules, dist, .env*, .DS_Store, *.local, dist-ssr).

  3. Create .nvmrc with 20.

  4. Create .npmrc with shamefully-hoist=true (needed for uni-app).

Verify: pnpm install runs without error from root.

Commit: chore: initialize pnpm monorepo structure


Task 1.2: 创建 shared 包

Files to create:

  • packages/shared/package.json
  • packages/shared/tsconfig.json
  • packages/shared/src/index.ts
  • packages/shared/src/enums.ts
  • packages/shared/src/constants.ts
  • packages/shared/src/types/index.ts
  • packages/shared/src/types/user.ts
  • packages/shared/src/types/card-type.ts
  • packages/shared/src/types/membership.ts
  • packages/shared/src/types/time-slot.ts
  • packages/shared/src/types/booking.ts
  • packages/shared/src/types/order.ts
  • packages/shared/src/types/studio.ts
  • packages/shared/src/types/week-template.ts
  • packages/shared/src/types/api.ts

Steps:

  1. Create packages/shared/package.json:
{
  "name": "@mp-pilates/shared",
  "version": "0.1.0",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "scripts": {
    "build": "tsc",
    "dev": "tsc --watch"
  }
}
  1. Define enums in src/enums.ts:
export enum UserRole { MEMBER = 'MEMBER', ADMIN = 'ADMIN' }
export enum CardTypeCategory { TIMES = 'TIMES', DURATION = 'DURATION', TRIAL = 'TRIAL' }
export enum MembershipStatus { ACTIVE = 'ACTIVE', EXPIRED = 'EXPIRED', USED_UP = 'USED_UP' }
export enum TimeSlotStatus { OPEN = 'OPEN', FULL = 'FULL', CLOSED = 'CLOSED' }
export enum TimeSlotSource { TEMPLATE = 'TEMPLATE', MANUAL = 'MANUAL' }
export enum BookingStatus { CONFIRMED = 'CONFIRMED', CANCELLED = 'CANCELLED', COMPLETED = 'COMPLETED', NO_SHOW = 'NO_SHOW' }
export enum OrderStatus { PENDING = 'PENDING', PAID = 'PAID', REFUNDED = 'REFUNDED' }
  1. Define constants in src/constants.ts:
export const DEFAULT_CANCEL_HOURS_LIMIT = 2
export const DEFAULT_SLOT_CAPACITY = 1
export const SLOT_GENERATION_DAYS = 14
export const TIME_PERIODS = { MORNING: { start: '06:00', end: '12:00' }, AFTERNOON: { start: '12:00', end: '18:00' }, EVENING: { start: '18:00', end: '22:00' } } as const
  1. Define TypeScript interfaces for all data models in src/types/ — one file per model, matching the spec's data model section. Each interface has both the full model (User) and creation DTO (CreateUserDto), response DTO (UserResponse).

  2. Define API response types in src/types/api.ts:

export interface ApiResponse<T> {
  readonly success: boolean
  readonly data: T | null
  readonly message: string | null
}
export interface PaginatedResponse<T> extends ApiResponse<T[]> {
  readonly total: number
  readonly page: number
  readonly limit: number
}
  1. Re-export all from src/index.ts.

Verify: cd packages/shared && pnpm build compiles without error and produces dist/.

Commit: feat(shared): add shared types, enums, and constants


Phase 2: 后端基础

Task 2.1: 初始化 NestJS 服务端

Files to create:

  • packages/server/package.json
  • packages/server/tsconfig.json
  • packages/server/tsconfig.build.json
  • packages/server/nest-cli.json
  • packages/server/src/main.ts
  • packages/server/src/app.module.ts
  • packages/server/src/app.controller.ts (health check)
  • packages/server/.env.example
  • packages/server/.env

Steps:

  1. Create package.json with dependencies:

    • @nestjs/core, @nestjs/common, @nestjs/platform-express
    • @nestjs/config, @nestjs/jwt, @nestjs/schedule
    • @prisma/client, prisma
    • @mp-pilates/shared (workspace reference)
    • Dev: @nestjs/testing, jest, ts-jest, @types/jest, supertest
  2. Create NestJS bootstrap in src/main.ts:

    • Global validation pipe (class-validator, class-transformer)
    • CORS config
    • Global prefix /api
    • Port from env (default 3000)
  3. Create AppModule with ConfigModule.forRoot(), health check endpoint.

  4. Create .env.example:

DATABASE_URL=postgresql://user:pass@localhost:5432/mp_pilates
JWT_SECRET=change-me
WX_APPID=your-appid
WX_SECRET=your-secret
WX_MCH_ID=your-mch-id
WX_MCH_KEY=your-mch-key
PORT=3000

Verify: cd packages/server && pnpm dev starts the server, curl localhost:3000/api/health returns 200.

Commit: feat(server): initialize NestJS project with health check


Task 2.2: Prisma schema 和数据库

Files to create:

  • packages/server/prisma/schema.prisma
  • packages/server/src/prisma/prisma.module.ts
  • packages/server/src/prisma/prisma.service.ts

Steps:

  1. Write prisma/schema.prisma with all 8 models from spec:
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

enum UserRole {
  MEMBER
  ADMIN
}

enum CardTypeCategory {
  TIMES
  DURATION
  TRIAL
}

enum MembershipStatus {
  ACTIVE
  EXPIRED
  USED_UP
}

enum TimeSlotStatus {
  OPEN
  FULL
  CLOSED
}

enum TimeSlotSource {
  TEMPLATE
  MANUAL
}

enum BookingStatus {
  CONFIRMED
  CANCELLED
  COMPLETED
  NO_SHOW
}

enum OrderStatus {
  PENDING
  PAID
  REFUNDED
}

model User {
  id        String   @id @default(uuid())
  openid    String   @unique
  unionid   String?
  phone     String?
  nickname  String   @default("")
  avatarUrl String?
  role      UserRole @default(MEMBER)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  memberships Membership[]
  bookings    Booking[]
  orders      Order[]

  @@map("users")
}

model CardType {
  id            String           @id @default(uuid())
  name          String
  type          CardTypeCategory
  totalTimes    Int?
  durationDays  Int
  price         Decimal          @db.Decimal(10, 0)
  originalPrice Decimal?         @db.Decimal(10, 0)
  description   String?
  isActive      Boolean          @default(true)
  sortOrder     Int              @default(0)
  createdAt     DateTime         @default(now())
  updatedAt     DateTime         @updatedAt

  memberships Membership[]
  orders      Order[]

  @@map("card_types")
}

model Membership {
  id             String           @id @default(uuid())
  userId         String
  cardTypeId     String
  remainingTimes Int?
  startDate      DateTime
  expireDate     DateTime
  status         MembershipStatus @default(ACTIVE)
  createdAt      DateTime         @default(now())
  updatedAt      DateTime         @updatedAt

  user     User     @relation(fields: [userId], references: [id])
  cardType CardType @relation(fields: [cardTypeId], references: [id])
  bookings Booking[]

  @@map("memberships")
}

model WeekTemplate {
  id        String   @id @default(uuid())
  dayOfWeek Int
  startTime String
  endTime   String
  capacity  Int      @default(1)
  isActive  Boolean  @default(true)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  timeSlots TimeSlot[]

  @@map("week_templates")
}

model TimeSlot {
  id          String         @id @default(uuid())
  date        DateTime       @db.Date
  startTime   String
  endTime     String
  capacity    Int            @default(1)
  bookedCount Int            @default(0)
  status      TimeSlotStatus @default(OPEN)
  source      TimeSlotSource @default(TEMPLATE)
  templateId  String?
  createdAt   DateTime       @default(now())
  updatedAt   DateTime       @updatedAt

  template WeekTemplate? @relation(fields: [templateId], references: [id])
  bookings Booking[]

  @@unique([date, startTime, endTime])
  @@map("time_slots")
}

model Booking {
  id           String        @id @default(uuid())
  userId       String
  timeSlotId   String
  membershipId String
  status       BookingStatus @default(CONFIRMED)
  cancelledAt  DateTime?
  createdAt    DateTime      @default(now())
  updatedAt    DateTime      @updatedAt

  user       User       @relation(fields: [userId], references: [id])
  timeSlot   TimeSlot   @relation(fields: [timeSlotId], references: [id])
  membership Membership @relation(fields: [membershipId], references: [id])

  @@unique([userId, timeSlotId])
  @@map("bookings")
}

model Order {
  id              String      @id @default(uuid())
  userId          String
  cardTypeId      String
  orderNo         String      @unique
  amount          Decimal     @db.Decimal(10, 0)
  status          OrderStatus @default(PENDING)
  wxTransactionId String?
  paidAt          DateTime?
  createdAt       DateTime    @default(now())
  updatedAt       DateTime    @updatedAt

  user     User     @relation(fields: [userId], references: [id])
  cardType CardType @relation(fields: [cardTypeId], references: [id])

  @@map("orders")
}

model StudioConfig {
  id               String   @id @default(uuid())
  name             String
  logo             String?
  bannerUrl        String?
  address          String   @default("")
  phone            String   @default("")
  latitude         Decimal? @db.Decimal(10, 7)
  longitude        Decimal? @db.Decimal(10, 7)
  cancelHoursLimit Int      @default(2)
  photos           Json     @default("[]")
  updatedAt        DateTime @updatedAt

  @@map("studio_config")
}
  1. Create PrismaService extending PrismaClient with onModuleInit / onModuleDestroy.

  2. Create PrismaModule as a global module.

  3. Add Prisma scripts to server package.json:

"prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate dev",
"prisma:seed": "ts-node prisma/seed.ts"

Verify:

cd packages/server
pnpm prisma generate
pnpm prisma migrate dev --name init

Database created with all tables.

Commit: feat(server): add Prisma schema with all data models


Task 2.3: 认证模块 (Auth)

Files to create:

  • packages/server/src/auth/auth.module.ts
  • packages/server/src/auth/auth.controller.ts
  • packages/server/src/auth/auth.service.ts
  • packages/server/src/auth/jwt.strategy.ts
  • packages/server/src/auth/jwt-auth.guard.ts
  • packages/server/src/auth/roles.guard.ts
  • packages/server/src/auth/roles.decorator.ts
  • packages/server/src/auth/dto/login.dto.ts
  • packages/server/src/auth/dto/bind-phone.dto.ts
  • packages/server/src/auth/wechat.service.ts
  • packages/server/src/auth/__tests__/auth.service.spec.ts
  • packages/server/src/auth/__tests__/auth.controller.spec.ts

Steps:

  1. WechatService: encapsulates code2session API call to exchange wx code for openid/session_key. Mock-able for tests.

  2. AuthService:

    • login(code: string): call WechatService → find or create User → sign JWT with { sub: user.id, role: user.role }
    • bindPhone(userId: string, encryptedData: string, iv: string): decrypt phone → update user
  3. AuthController:

    • POST /auth/login{ code } → returns { token, user }
    • POST /auth/phone{ encryptedData, iv } → returns updated user (requires auth)
  4. JwtStrategy + JwtAuthGuard: standard Passport JWT strategy.

  5. RolesGuard + @Roles() decorator: check user.role against required roles.

Tests (TDD):

  • Auth service: mock WechatService + PrismaService, test login creates user, returns JWT
  • Auth service: test login with existing user returns existing user
  • Auth controller: e2e test with supertest

Verify: cd packages/server && pnpm test -- --testPathPattern=auth

Commit: feat(server): add auth module with WeChat login and JWT


Task 2.4: 用户模块 (User)

Files to create:

  • packages/server/src/user/user.module.ts
  • packages/server/src/user/user.controller.ts
  • packages/server/src/user/user.service.ts
  • packages/server/src/user/__tests__/user.service.spec.ts

Steps:

  1. UserService:

    • getProfile(userId): return user with active memberships count
    • updateProfile(userId, dto): update nickname/avatarUrl (immutable — return new object)
    • getStats(userId): query bookings for training stats (total count, this month count, total days, this month days, this month hours)
  2. UserController (all routes require auth):

    • GET /user/profile
    • PUT /user/profile
    • GET /user/stats

Tests: Mock Prisma, verify stats calculation, verify profile update returns new data.

Verify: pnpm test -- --testPathPattern=user

Commit: feat(server): add user module with profile and stats


Task 2.5: 工作室信息模块 (Studio)

Files to create:

  • packages/server/src/studio/studio.module.ts
  • packages/server/src/studio/studio.controller.ts
  • packages/server/src/studio/studio.service.ts
  • packages/server/src/studio/__tests__/studio.service.spec.ts
  • packages/server/prisma/seed.ts

Steps:

  1. StudioService:

    • getInfo(): return the single StudioConfig record (create default if not exists)
    • updateInfo(dto): update studio config (ADMIN only)
  2. StudioController:

    • GET /studio/info — public
    • PUT /admin/studio/info — ADMIN only
  3. Seed script: create default StudioConfig record.

Tests: Verify get creates default, verify update returns new record.

Verify: pnpm test -- --testPathPattern=studio

Commit: feat(server): add studio module with config management


Task 2.6: 卡种与会员卡模块 (Membership)

Files to create:

  • packages/server/src/membership/membership.module.ts
  • packages/server/src/membership/membership.controller.ts
  • packages/server/src/membership/membership.service.ts
  • packages/server/src/membership/dto/create-card-type.dto.ts
  • packages/server/src/membership/dto/update-card-type.dto.ts
  • packages/server/src/membership/__tests__/membership.service.spec.ts

Steps:

  1. MembershipService:

    • getActiveCardTypes(): list card types where isActive=true, ordered by sortOrder
    • getUserMemberships(userId): user's memberships with cardType info
    • getValidMembership(userId): find first ACTIVE membership with remaining times/valid date
    • deductMembership(membershipId): decrement remainingTimes (TIMES/TRIAL) or noop (DURATION), mark USED_UP if times=0
    • restoreMembership(membershipId): increment remainingTimes for cancellation refund, restore status from USED_UP to ACTIVE
    • Admin CRUD for card types
  2. MembershipController:

    • GET /membership/card-types — public
    • GET /membership/my — authed
    • GET /admin/card-types, POST, PUT :id, DELETE :id — ADMIN

Tests:

  • deductMembership: times card decrements, used up marks status
  • deductMembership: duration card does nothing to times
  • restoreMembership: restores count and status
  • getValidMembership: returns first valid card, skips expired

Verify: pnpm test -- --testPathPattern=membership

Commit: feat(server): add membership module with card types and deduction logic


Task 2.7: 时段与排课模块 (TimeSlot)

Files to create:

  • packages/server/src/time-slot/time-slot.module.ts
  • packages/server/src/time-slot/time-slot.controller.ts
  • packages/server/src/time-slot/time-slot.service.ts
  • packages/server/src/time-slot/slot-generator.service.ts
  • packages/server/src/time-slot/dto/query-slots.dto.ts
  • packages/server/src/time-slot/__tests__/time-slot.service.spec.ts
  • packages/server/src/time-slot/__tests__/slot-generator.service.spec.ts

Steps:

  1. TimeSlotService:

    • getAvailableSlots(date: string): query TimeSlots for date, include booking status for current user
    • getSlotById(id): single slot with details
    • Admin operations: create manual slot, close slot
  2. SlotGeneratorService:

    • generateSlots(daysAhead: number = 14): read active WeekTemplates → generate TimeSlots for future dates, skip if [date, startTime, endTime] already exists
    • cleanupExpiredSlots(): mark past OPEN slots as CLOSED
    • checkExpiredMemberships(): mark expired memberships as EXPIRED, used-up as USED_UP
  3. Admin controller endpoints:

    • GET /admin/week-template
    • PUT /admin/week-template (full replacement of all templates)
    • POST /admin/time-slot/manual
    • PUT /admin/time-slot/:id/close
    • POST /admin/generate-slots (manual trigger)

Tests:

  • SlotGenerator: generates correct slots from templates
  • SlotGenerator: skips existing date+time combinations
  • SlotGenerator: marks past slots as CLOSED
  • TimeSlotService: returns slots with user booking status

Verify: pnpm test -- --testPathPattern=time-slot

Commit: feat(server): add time-slot module with template-based generation


Task 2.8: 预约模块 (Booking)

Files to create:

  • packages/server/src/booking/booking.module.ts
  • packages/server/src/booking/booking.controller.ts
  • packages/server/src/booking/booking.service.ts
  • packages/server/src/booking/dto/create-booking.dto.ts
  • packages/server/src/booking/__tests__/booking.service.spec.ts

Steps:

  1. BookingService:

    • createBooking(userId, timeSlotId, membershipId):
      • Validate: slot is OPEN, not already booked by user, membership is valid
      • Transaction: create Booking, increment slot bookedCount, deduct membership
      • If bookedCount >= capacity → set slot status to FULL
    • cancelBooking(userId, bookingId):
      • Check ownership, check booking status is CONFIRMED
      • Calculate if within cancelHoursLimit
      • Transaction: update booking to CANCELLED, decrement bookedCount, restore membership (if within limit), reset slot to OPEN if was FULL
    • getMyBookings(userId, status?): paginated list
    • getUpcomingBookings(userId): future confirmed bookings
    • Admin: getAllBookings(filters): all bookings with user info
  2. BookingController:

    • POST /booking{ timeSlotId, membershipId }
    • PUT /booking/:id/cancel
    • GET /booking/my
    • GET /booking/my/upcoming
    • GET /admin/bookings — ADMIN

Tests (critical business logic):

  • createBooking: success path — booking created, count incremented, membership deducted
  • createBooking: fail — slot full
  • createBooking: fail — duplicate booking
  • createBooking: fail — expired membership
  • cancelBooking: within limit — refunds membership
  • cancelBooking: past limit — no refund, still cancels
  • cancelBooking: slot status changes from FULL to OPEN

Verify: pnpm test -- --testPathPattern=booking

Commit: feat(server): add booking module with reservation and cancellation logic


Task 2.9: 支付模块 (Payment)

Files to create:

  • packages/server/src/payment/payment.module.ts
  • packages/server/src/payment/payment.controller.ts
  • packages/server/src/payment/payment.service.ts
  • packages/server/src/payment/wechat-pay.service.ts
  • packages/server/src/payment/dto/create-order.dto.ts
  • packages/server/src/payment/__tests__/payment.service.spec.ts

Steps:

  1. WechatPayService: encapsulate WeChat Pay unified order API, sign verification, callback parsing. Mock-able for tests.

  2. PaymentService:

    • createOrder(userId, cardTypeId):
      • Generate order number (timestamp + random)
      • Create Order (PENDING)
      • Call WechatPay unified order → return payment params
    • handleWxNotify(xml):
      • Parse and verify signature
      • Find order by orderNo
      • Transaction: update Order to PAID, create Membership (calculate expireDate from cardType.durationDays, set remainingTimes from cardType.totalTimes)
    • getMyOrders(userId): paginated order list
    • getOrdersByAdmin(): ADMIN view
  3. PaymentController:

    • POST /payment/create-order — authed
    • POST /payment/wx-notify — public (WeChat callback, no auth)
    • GET /payment/orders — authed
    • GET /admin/orders — ADMIN

Tests:

  • createOrder: creates PENDING order with correct amount
  • handleWxNotify: valid callback → order PAID, membership created with correct expiry
  • handleWxNotify: duplicate callback → idempotent (no duplicate membership)
  • handleWxNotify: invalid signature → rejected

Verify: pnpm test -- --testPathPattern=payment

Commit: feat(server): add payment module with WeChat Pay integration


Task 2.10: 定时任务

Files to create:

  • packages/server/src/scheduler/scheduler.module.ts
  • packages/server/src/scheduler/scheduler.service.ts
  • packages/server/src/scheduler/__tests__/scheduler.service.spec.ts

Steps:

  1. Use @nestjs/schedule with @Cron:

    • Daily 2:00 AM: SlotGeneratorService.generateSlots(14) — generate slots for next 14 days
    • Daily 2:30 AM: SlotGeneratorService.cleanupExpiredSlots() — close past slots
    • Daily 3:00 AM: SlotGeneratorService.checkExpiredMemberships() — expire memberships
    • Daily 22:00: Mark today's CONFIRMED bookings as COMPLETED (lesson is done)
  2. Register ScheduleModule.forRoot() in AppModule.

Tests: Mock cron, verify each task calls the right service method.

Verify: pnpm test -- --testPathPattern=scheduler

Commit: feat(server): add scheduled tasks for slot generation and cleanup


Task 2.11: 数据库 seed 脚本

Files to create:

  • packages/server/prisma/seed.ts

Steps:

  1. Create seed data:

    • 1 StudioConfig record with placeholder values
    • 3 CardType records: 「私教10次卡」(TIMES, 10次, 90天, 2999元), 「月卡」(DURATION, 30天, 999元), 「体验课」(TRIAL, 1次, 7天, 99元)
    • 1 ADMIN user (with a test openid)
    • WeekTemplate for weekdays 09:00-10:00, 10:00-11:00, 14:00-15:00, 15:00-16:00
  2. Use prisma.$transaction for atomicity, skip if records already exist.

Verify: pnpm prisma db seed runs successfully.

Commit: feat(server): add database seed script


Phase 3: 前端基础

Task 3.1: 初始化 uni-app 项目

Files to create:

  • packages/app/package.json
  • packages/app/tsconfig.json
  • packages/app/vite.config.ts
  • packages/app/src/main.ts
  • packages/app/src/App.vue
  • packages/app/src/pages.json
  • packages/app/src/manifest.json
  • packages/app/src/uni.scss
  • packages/app/index.html

Steps:

  1. Initialize with @dcloudio/uni-app (Vue 3 + Vite + TypeScript template).

  2. Install dependencies:

    • @dcloudio/uni-app, @dcloudio/uni-mp-weixin, @dcloudio/uni-ui
    • pinia, @mp-pilates/shared
  3. Configure pages.json with tab bar (3 tabs) and all sub-pages:

{
  "pages": [
    { "path": "pages/home/index", "style": { "navigationBarTitleText": "首页" } },
    { "path": "pages/booking/index", "style": { "navigationBarTitleText": "预约课程" } },
    { "path": "pages/profile/index", "style": { "navigationBarTitleText": "我的" } }
  ],
  "subPackages": [
    {
      "root": "pages/card",
      "pages": [
        { "path": "detail", "style": { "navigationBarTitleText": "购买会员卡" } }
      ]
    },
    {
      "root": "pages/profile",
      "pages": [
        { "path": "membership", "style": { "navigationBarTitleText": "我的会员卡" } },
        { "path": "bookings", "style": { "navigationBarTitleText": "我的预约" } },
        { "path": "info", "style": { "navigationBarTitleText": "个人信息" } }
      ]
    },
    {
      "root": "pages/admin",
      "pages": [
        { "path": "index", "style": { "navigationBarTitleText": "管理中心" } },
        { "path": "week-template", "style": { "navigationBarTitleText": "排课设置" } },
        { "path": "slot-adjust", "style": { "navigationBarTitleText": "时段调整" } },
        { "path": "members", "style": { "navigationBarTitleText": "会员管理" } },
        { "path": "orders", "style": { "navigationBarTitleText": "订单管理" } },
        { "path": "card-types", "style": { "navigationBarTitleText": "卡种管理" } },
        { "path": "studio", "style": { "navigationBarTitleText": "工作室设置" } }
      ]
    }
  ],
  "tabBar": {
    "color": "#999",
    "selectedColor": "#333",
    "list": [
      { "pagePath": "pages/home/index", "text": "首页", "iconPath": "static/tab/home.png", "selectedIconPath": "static/tab/home-active.png" },
      { "pagePath": "pages/booking/index", "text": "预约", "iconPath": "static/tab/booking.png", "selectedIconPath": "static/tab/booking-active.png" },
      { "pagePath": "pages/profile/index", "text": "我的", "iconPath": "static/tab/profile.png", "selectedIconPath": "static/tab/profile-active.png" }
    ]
  }
}
  1. Configure manifest.json with WeChat appid placeholder and permission settings.

Verify: cd packages/app && pnpm dev:mp-weixin compiles without error.

Commit: feat(app): initialize uni-app project with page routing


Task 3.2: 前端基础设施 (HTTP, Auth, Store)

Files to create:

  • packages/app/src/utils/request.ts
  • packages/app/src/utils/auth.ts
  • packages/app/src/stores/user.ts
  • packages/app/src/stores/studio.ts

Steps:

  1. utils/request.ts — 封装 uni.request:
    • Base URL from env config
    • Auto attach JWT token header
    • Response interceptor: handle 401 (redirect to login), extract data from ApiResponse
    • Request/response typing with generics
export function request<T>(options: RequestOptions): Promise<T> {
  return new Promise((resolve, reject) => {
    const token = uni.getStorageSync('token')
    uni.request({
      url: `${BASE_URL}${options.url}`,
      method: options.method || 'GET',
      data: options.data,
      header: {
        'Content-Type': 'application/json',
        ...(token ? { Authorization: `Bearer ${token}` } : {}),
        ...options.header,
      },
      success: (res) => {
        if (res.statusCode === 401) {
          uni.removeStorageSync('token')
          // redirect to login
          reject(new Error('Unauthorized'))
          return
        }
        const body = res.data as ApiResponse<T>
        if (body.success) {
          resolve(body.data as T)
        } else {
          reject(new Error(body.message || 'Request failed'))
        }
      },
      fail: reject,
    })
  })
}
  1. utils/auth.ts — WeChat login helper:

    • wxLogin(): call wx.login(), send code to /auth/login, store JWT
    • wxGetPhone(e): extract encrypted data, send to /auth/phone
    • checkAuth(): check token exists, validate
  2. stores/user.ts (Pinia):

    • State: user, token, stats, memberships
    • Actions: login(), fetchProfile(), fetchStats(), fetchMemberships()
    • Getters: isLoggedIn, isAdmin, hasValidMembership, activeMembership
  3. stores/studio.ts (Pinia):

    • State: studioInfo
    • Actions: fetchStudioInfo()

Verify: Build compiles. Stores are importable and typed.

Commit: feat(app): add HTTP client, auth utils, and Pinia stores


Phase 4: 前端页面

Task 4.1: 首页

Files to create:

  • packages/app/src/pages/home/index.vue
  • packages/app/src/components/BrandBanner.vue
  • packages/app/src/components/StudioInfo.vue
  • packages/app/src/components/QuickEntry.vue
  • packages/app/src/components/UpcomingBooking.vue
  • packages/app/src/components/CardShop.vue

Steps:

  1. BrandBanner: display logo, studio name, background image from StudioConfig.
  2. StudioInfo: photo swiper, address (tap → map), phone (tap → call).
  3. QuickEntry: conditional display based on user state:
    • Not logged in → 「微信登录」
    • New user (no memberships) → 「预约体验课」
    • Has valid card → 「一键约课」
    • Card running low (<=2 remaining) → 「续卡提醒」
    • No valid card → 「购买会员卡」
  4. UpcomingBooking: show next 1-2 upcoming bookings with date/time.
  5. CardShop: horizontal scrollable card type list, tap → card detail page.

Verify: Page renders in WeChat DevTools. Quick entries change based on mock data.

Commit: feat(app): implement home page with smart entry points


Task 4.2: 课程预约页

Files to create:

  • packages/app/src/pages/booking/index.vue
  • packages/app/src/components/DateSelector.vue
  • packages/app/src/components/TimePeriodFilter.vue
  • packages/app/src/components/SlotCard.vue
  • packages/app/src/components/BookingConfirmPopup.vue
  • packages/app/src/stores/booking.ts

Steps:

  1. DateSelector: horizontal scroll, 7 days, highlight today, show weekday. Active date fetches slots.
  2. TimePeriodFilter: tabs for 全部/上午/下午/晚上, filter displayed slots.
  3. SlotCard: display time range, status (available/full/booked). Tap → booking flow.
  4. BookingConfirmPopup: uni-popup showing slot info + membership to deduct + confirm/cancel buttons.
  5. stores/booking.ts: fetch slots by date, create booking, cancel booking.

Booking flow in component:

tap "预约" → check login → check membership
  → no membership → navigate to card purchase
  → has membership → show BookingConfirmPopup
    → confirm → call createBooking API → success toast → refresh slots

Verify: Page loads slots, date switching works, booking popup shows.

Commit: feat(app): implement booking page with date selector and reservation flow


Task 4.3: 个人中心页

Files to create:

  • packages/app/src/pages/profile/index.vue
  • packages/app/src/components/UserCard.vue
  • packages/app/src/components/TrainingStats.vue
  • packages/app/src/components/ProfileMenu.vue

Steps:

  1. UserCard: avatar, nickname, phone, total training days/count.
  2. TrainingStats: this month's count/days/hours in a card layout.
  3. ProfileMenu: list items — 我的会员卡, 我的预约, 个人信息. If ADMIN → 管理中心 entry at bottom.
  4. Login prompt if not authenticated.

Verify: Page renders, menu items navigate correctly.

Commit: feat(app): implement profile page with stats and menu


Task 4.4: 子页面 — 会员卡购买、我的会员卡、我的预约、个人信息

Files to create:

  • packages/app/src/pages/card/detail.vue
  • packages/app/src/pages/profile/membership.vue
  • packages/app/src/pages/profile/bookings.vue
  • packages/app/src/pages/profile/info.vue

Steps:

  1. 卡种详情/购买 (card/detail.vue):

    • Receive cardTypeId via route param
    • Display card info: name, type, price (with original price strikethrough), description, duration
    • "立即购买" button → call createOrder → wx.requestPayment → success → navigate to my memberships
  2. 我的会员卡 (profile/membership.vue):

    • List all memberships grouped: ACTIVE first, then EXPIRED/USED_UP
    • Each card shows: name, remaining times or expire date, status badge
  3. 我的预约 (profile/bookings.vue):

    • Two tabs: 即将上课 / 历史记录
    • Upcoming: show date, time, cancel button (with time check)
    • History: show status (COMPLETED, CANCELLED, NO_SHOW)
  4. 个人信息 (profile/info.vue):

    • Edit nickname (text input)
    • Bind/change phone (wx.getPhoneNumber button)
    • Avatar display (from WeChat)

Verify: Each page renders and connects to API correctly.

Commit: feat(app): implement card purchase, membership, bookings, and profile info pages


Task 4.5: 管理页面

Files to create:

  • packages/app/src/pages/admin/index.vue
  • packages/app/src/pages/admin/week-template.vue
  • packages/app/src/pages/admin/slot-adjust.vue
  • packages/app/src/pages/admin/members.vue
  • packages/app/src/pages/admin/orders.vue
  • packages/app/src/pages/admin/card-types.vue
  • packages/app/src/pages/admin/studio.vue
  • packages/app/src/stores/admin.ts

Steps:

  1. 管理中心 (admin/index.vue): grid menu with icons — 排课设置, 时段调整, 会员管理, 订单管理, 卡种管理, 工作室设置.

  2. 周模板管理 (admin/week-template.vue):

    • 7-column grid (Mon-Sun)
    • Each day shows time slots as tags
    • Add button per day → time picker (start/end) → add template
    • Toggle switch per slot to enable/disable
    • Save → PUT /admin/week-template (full replacement)
  3. 临时时段调整 (admin/slot-adjust.vue):

    • Calendar date picker
    • Show generated slots for selected date
    • Close slot button, Add manual slot form
    • "重新生成" button → POST /admin/generate-slots
  4. 会员管理 (admin/members.vue):

    • Search by nickname/phone
    • List: avatar, nickname, active card info, total bookings
    • Tap → member detail (memberships + booking history)
  5. 订单管理 (admin/orders.vue):

    • List: order no, user, card type, amount (format from cents), status, time
    • Status filter tabs (ALL, PENDING, PAID, REFUNDED)
  6. 卡种管理 (admin/card-types.vue):

    • List current card types with edit/delete
    • Add new card type form: name, type, times, duration, price, original price, description
    • Toggle isActive
  7. 工作室设置 (admin/studio.vue):

    • Form: name, address, phone, cancelHoursLimit
    • Image upload for logo, banner, photos
    • Map picker for lat/lng (optional, use wx.chooseLocation)
  8. stores/admin.ts: admin-specific API calls for all admin endpoints.

Verify: All admin pages render and CRUD operations work.

Commit: feat(app): implement admin management pages


Phase 5: 集成与完善

Task 5.1: 端到端联调

Steps:

  1. Start server with seed data
  2. Open mini-program in WeChat DevTools
  3. Test complete flows:
    • Login → home page → quick entries display correctly
    • Purchase card → payment flow (mock mode)
    • Book a slot → confirm → check deduction
    • Cancel booking → verify refund logic
    • Admin: create week template → generate slots → verify on booking page
  4. Fix any integration issues

Verify: All flows work end-to-end in WeChat DevTools.

Commit: fix: resolve integration issues from e2e testing


Task 5.2: 错误处理与 loading 状态

Files to create/modify:

  • packages/app/src/components/LoadingState.vue
  • packages/app/src/components/EmptyState.vue
  • packages/app/src/components/ErrorToast.vue

Steps:

  1. Add loading skeletons to list pages (booking, memberships, admin lists).
  2. Add empty state illustrations for: no bookings, no memberships, no slots.
  3. Add error handling: network error toast, retry mechanism.
  4. Add pull-to-refresh on list pages.
  5. Add uni.showLoading / uni.hideLoading for mutation operations (book, cancel, purchase).

Verify: Loading states display, errors are caught and shown, empty states render.

Commit: feat(app): add loading states, empty states, and error handling


Task 5.3: 服务端全量测试

Steps:

  1. Run full test suite: cd packages/server && pnpm test
  2. Check coverage: pnpm test -- --coverage
  3. Ensure ≥80% coverage on:
    • booking.service.ts (critical business logic)
    • membership.service.ts (deduction logic)
    • payment.service.ts (order + callback)
    • slot-generator.service.ts (slot generation)
    • auth.service.ts (login flow)
  4. Add missing tests if below threshold.

Verify: pnpm test -- --coverage shows ≥80% on critical modules.

Commit: test(server): ensure 80%+ coverage on critical business modules


Execution Notes

Dependency Order

  • Phase 1 → Phase 2 → Phase 3 → Phase 4 → Phase 5
  • Within Phase 2: Tasks 2.1, 2.2 must come first. Tasks 2.3-2.5 can be parallelized. Tasks 2.6-2.8 depend on 2.3 (auth). Task 2.9 depends on 2.6. Task 2.10 depends on 2.7.
  • Within Phase 4: Task 4.1-4.3 can be parallelized after 3.2. Task 4.4 depends on 4.1-4.3 stores. Task 4.5 depends on all backend modules.

Parallelizable Tasks

  • Phase 2: Auth (2.3) + Studio (2.5) + User (2.4) can run in parallel after 2.2
  • Phase 4: Home (4.1) + Booking (4.2) + Profile (4.3) can run in parallel after 3.2

Key Risks

  1. WeChat Pay integration: Requires real merchant credentials for testing. Use mock mode first, real integration last.
  2. uni-app + pnpm workspace: shamefully-hoist=true is needed. May need to adjust vite.config.ts for shared package resolution.
  3. Prisma in monorepo: Need to ensure prisma generate runs before server build. Add as postinstall script.

Total Commits: ~17

Estimated Tasks: 16 main tasks across 5 phases