- 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
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.yamltsconfig.base.json.gitignore.nvmrc.npmrc
Steps:
- 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"
}
}
- Create
pnpm-workspace.yaml:
packages:
- 'packages/*'
-
Create
tsconfig.base.jsonwith shared compiler options (strict, ESNext, paths for@mp-pilates/shared). -
Create
.gitignore(node_modules, dist, .env*, .DS_Store, *.local, dist-ssr). -
Create
.nvmrcwith20. -
Create
.npmrcwithshamefully-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.jsonpackages/shared/tsconfig.jsonpackages/shared/src/index.tspackages/shared/src/enums.tspackages/shared/src/constants.tspackages/shared/src/types/index.tspackages/shared/src/types/user.tspackages/shared/src/types/card-type.tspackages/shared/src/types/membership.tspackages/shared/src/types/time-slot.tspackages/shared/src/types/booking.tspackages/shared/src/types/order.tspackages/shared/src/types/studio.tspackages/shared/src/types/week-template.tspackages/shared/src/types/api.ts
Steps:
- 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"
}
}
- 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' }
- 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
-
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). -
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
}
- 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.jsonpackages/server/tsconfig.jsonpackages/server/tsconfig.build.jsonpackages/server/nest-cli.jsonpackages/server/src/main.tspackages/server/src/app.module.tspackages/server/src/app.controller.ts(health check)packages/server/.env.examplepackages/server/.env
Steps:
-
Create
package.jsonwith 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
-
Create NestJS bootstrap in
src/main.ts:- Global validation pipe (
class-validator,class-transformer) - CORS config
- Global prefix
/api - Port from env (default 3000)
- Global validation pipe (
-
Create
AppModulewithConfigModule.forRoot(), health check endpoint. -
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.prismapackages/server/src/prisma/prisma.module.tspackages/server/src/prisma/prisma.service.ts
Steps:
- Write
prisma/schema.prismawith 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")
}
-
Create
PrismaServiceextendingPrismaClientwithonModuleInit/onModuleDestroy. -
Create
PrismaModuleas a global module. -
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.tspackages/server/src/auth/auth.controller.tspackages/server/src/auth/auth.service.tspackages/server/src/auth/jwt.strategy.tspackages/server/src/auth/jwt-auth.guard.tspackages/server/src/auth/roles.guard.tspackages/server/src/auth/roles.decorator.tspackages/server/src/auth/dto/login.dto.tspackages/server/src/auth/dto/bind-phone.dto.tspackages/server/src/auth/wechat.service.tspackages/server/src/auth/__tests__/auth.service.spec.tspackages/server/src/auth/__tests__/auth.controller.spec.ts
Steps:
-
WechatService: encapsulatescode2sessionAPI call to exchange wx code for openid/session_key. Mock-able for tests. -
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
-
AuthController:POST /auth/login→{ code }→ returns{ token, user }POST /auth/phone→{ encryptedData, iv }→ returns updated user (requires auth)
-
JwtStrategy+JwtAuthGuard: standard Passport JWT strategy. -
RolesGuard+@Roles()decorator: checkuser.roleagainst 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.tspackages/server/src/user/user.controller.tspackages/server/src/user/user.service.tspackages/server/src/user/__tests__/user.service.spec.ts
Steps:
-
UserService:getProfile(userId): return user with active memberships countupdateProfile(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)
-
UserController(all routes require auth):GET /user/profilePUT /user/profileGET /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.tspackages/server/src/studio/studio.controller.tspackages/server/src/studio/studio.service.tspackages/server/src/studio/__tests__/studio.service.spec.tspackages/server/prisma/seed.ts
Steps:
-
StudioService:getInfo(): return the single StudioConfig record (create default if not exists)updateInfo(dto): update studio config (ADMIN only)
-
StudioController:GET /studio/info— publicPUT /admin/studio/info— ADMIN only
-
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.tspackages/server/src/membership/membership.controller.tspackages/server/src/membership/membership.service.tspackages/server/src/membership/dto/create-card-type.dto.tspackages/server/src/membership/dto/update-card-type.dto.tspackages/server/src/membership/__tests__/membership.service.spec.ts
Steps:
-
MembershipService:getActiveCardTypes(): list card types whereisActive=true, ordered bysortOrdergetUserMemberships(userId): user's memberships with cardType infogetValidMembership(userId): find first ACTIVE membership with remaining times/valid datedeductMembership(membershipId): decrementremainingTimes(TIMES/TRIAL) or noop (DURATION), mark USED_UP if times=0restoreMembership(membershipId): incrementremainingTimesfor cancellation refund, restore status from USED_UP to ACTIVE- Admin CRUD for card types
-
MembershipController:GET /membership/card-types— publicGET /membership/my— authedGET /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.tspackages/server/src/time-slot/time-slot.controller.tspackages/server/src/time-slot/time-slot.service.tspackages/server/src/time-slot/slot-generator.service.tspackages/server/src/time-slot/dto/query-slots.dto.tspackages/server/src/time-slot/__tests__/time-slot.service.spec.tspackages/server/src/time-slot/__tests__/slot-generator.service.spec.ts
Steps:
-
TimeSlotService:getAvailableSlots(date: string): query TimeSlots for date, include booking status for current usergetSlotById(id): single slot with details- Admin operations: create manual slot, close slot
-
SlotGeneratorService:generateSlots(daysAhead: number = 14): read active WeekTemplates → generate TimeSlots for future dates, skip if[date, startTime, endTime]already existscleanupExpiredSlots(): mark past OPEN slots as CLOSEDcheckExpiredMemberships(): mark expired memberships as EXPIRED, used-up as USED_UP
-
Admin controller endpoints:
GET /admin/week-templatePUT /admin/week-template(full replacement of all templates)POST /admin/time-slot/manualPUT /admin/time-slot/:id/closePOST /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.tspackages/server/src/booking/booking.controller.tspackages/server/src/booking/booking.service.tspackages/server/src/booking/dto/create-booking.dto.tspackages/server/src/booking/__tests__/booking.service.spec.ts
Steps:
-
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 listgetUpcomingBookings(userId): future confirmed bookings- Admin:
getAllBookings(filters): all bookings with user info
-
BookingController:POST /booking—{ timeSlotId, membershipId }PUT /booking/:id/cancelGET /booking/myGET /booking/my/upcomingGET /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.tspackages/server/src/payment/payment.controller.tspackages/server/src/payment/payment.service.tspackages/server/src/payment/wechat-pay.service.tspackages/server/src/payment/dto/create-order.dto.tspackages/server/src/payment/__tests__/payment.service.spec.ts
Steps:
-
WechatPayService: encapsulate WeChat Pay unified order API, sign verification, callback parsing. Mock-able for tests. -
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 listgetOrdersByAdmin(): ADMIN view
-
PaymentController:POST /payment/create-order— authedPOST /payment/wx-notify— public (WeChat callback, no auth)GET /payment/orders— authedGET /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.tspackages/server/src/scheduler/scheduler.service.tspackages/server/src/scheduler/__tests__/scheduler.service.spec.ts
Steps:
-
Use
@nestjs/schedulewith@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)
- Daily 2:00 AM:
-
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:
-
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
-
Use
prisma.$transactionfor 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.jsonpackages/app/tsconfig.jsonpackages/app/vite.config.tspackages/app/src/main.tspackages/app/src/App.vuepackages/app/src/pages.jsonpackages/app/src/manifest.jsonpackages/app/src/uni.scsspackages/app/index.html
Steps:
-
Initialize with
@dcloudio/uni-app(Vue 3 + Vite + TypeScript template). -
Install dependencies:
@dcloudio/uni-app,@dcloudio/uni-mp-weixin,@dcloudio/uni-uipinia,@mp-pilates/shared
-
Configure
pages.jsonwith 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" }
]
}
}
- Configure
manifest.jsonwith 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.tspackages/app/src/utils/auth.tspackages/app/src/stores/user.tspackages/app/src/stores/studio.ts
Steps:
utils/request.ts— 封装uni.request:- Base URL from env config
- Auto attach JWT token header
- Response interceptor: handle 401 (redirect to login), extract
datafrom 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,
})
})
}
-
utils/auth.ts— WeChat login helper:wxLogin(): callwx.login(), send code to/auth/login, store JWTwxGetPhone(e): extract encrypted data, send to/auth/phonecheckAuth(): check token exists, validate
-
stores/user.ts(Pinia):- State:
user,token,stats,memberships - Actions:
login(),fetchProfile(),fetchStats(),fetchMemberships() - Getters:
isLoggedIn,isAdmin,hasValidMembership,activeMembership
- State:
-
stores/studio.ts(Pinia):- State:
studioInfo - Actions:
fetchStudioInfo()
- State:
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.vuepackages/app/src/components/BrandBanner.vuepackages/app/src/components/StudioInfo.vuepackages/app/src/components/QuickEntry.vuepackages/app/src/components/UpcomingBooking.vuepackages/app/src/components/CardShop.vue
Steps:
BrandBanner: display logo, studio name, background image from StudioConfig.StudioInfo: photo swiper, address (tap → map), phone (tap → call).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 → 「购买会员卡」
UpcomingBooking: show next 1-2 upcoming bookings with date/time.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.vuepackages/app/src/components/DateSelector.vuepackages/app/src/components/TimePeriodFilter.vuepackages/app/src/components/SlotCard.vuepackages/app/src/components/BookingConfirmPopup.vuepackages/app/src/stores/booking.ts
Steps:
DateSelector: horizontal scroll, 7 days, highlight today, show weekday. Active date fetches slots.TimePeriodFilter: tabs for 全部/上午/下午/晚上, filter displayed slots.SlotCard: display time range, status (available/full/booked). Tap → booking flow.BookingConfirmPopup: uni-popup showing slot info + membership to deduct + confirm/cancel buttons.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.vuepackages/app/src/components/UserCard.vuepackages/app/src/components/TrainingStats.vuepackages/app/src/components/ProfileMenu.vue
Steps:
UserCard: avatar, nickname, phone, total training days/count.TrainingStats: this month's count/days/hours in a card layout.ProfileMenu: list items — 我的会员卡, 我的预约, 个人信息. If ADMIN → 管理中心 entry at bottom.- 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.vuepackages/app/src/pages/profile/membership.vuepackages/app/src/pages/profile/bookings.vuepackages/app/src/pages/profile/info.vue
Steps:
-
卡种详情/购买 (
card/detail.vue):- Receive
cardTypeIdvia route param - Display card info: name, type, price (with original price strikethrough), description, duration
- "立即购买" button → call createOrder →
wx.requestPayment→ success → navigate to my memberships
- Receive
-
我的会员卡 (
profile/membership.vue):- List all memberships grouped: ACTIVE first, then EXPIRED/USED_UP
- Each card shows: name, remaining times or expire date, status badge
-
我的预约 (
profile/bookings.vue):- Two tabs: 即将上课 / 历史记录
- Upcoming: show date, time, cancel button (with time check)
- History: show status (COMPLETED, CANCELLED, NO_SHOW)
-
个人信息 (
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.vuepackages/app/src/pages/admin/week-template.vuepackages/app/src/pages/admin/slot-adjust.vuepackages/app/src/pages/admin/members.vuepackages/app/src/pages/admin/orders.vuepackages/app/src/pages/admin/card-types.vuepackages/app/src/pages/admin/studio.vuepackages/app/src/stores/admin.ts
Steps:
-
管理中心 (
admin/index.vue): grid menu with icons — 排课设置, 时段调整, 会员管理, 订单管理, 卡种管理, 工作室设置. -
周模板管理 (
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)
-
临时时段调整 (
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
-
会员管理 (
admin/members.vue):- Search by nickname/phone
- List: avatar, nickname, active card info, total bookings
- Tap → member detail (memberships + booking history)
-
订单管理 (
admin/orders.vue):- List: order no, user, card type, amount (format from cents), status, time
- Status filter tabs (ALL, PENDING, PAID, REFUNDED)
-
卡种管理 (
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
-
工作室设置 (
admin/studio.vue):- Form: name, address, phone, cancelHoursLimit
- Image upload for logo, banner, photos
- Map picker for lat/lng (optional, use wx.chooseLocation)
-
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:
- Start server with seed data
- Open mini-program in WeChat DevTools
- 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
- 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.vuepackages/app/src/components/EmptyState.vuepackages/app/src/components/ErrorToast.vue
Steps:
- Add loading skeletons to list pages (booking, memberships, admin lists).
- Add empty state illustrations for: no bookings, no memberships, no slots.
- Add error handling: network error toast, retry mechanism.
- Add pull-to-refresh on list pages.
- Add
uni.showLoading/uni.hideLoadingfor 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:
- Run full test suite:
cd packages/server && pnpm test - Check coverage:
pnpm test -- --coverage - 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)
- 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
- WeChat Pay integration: Requires real merchant credentials for testing. Use mock mode first, real integration last.
- uni-app + pnpm workspace:
shamefully-hoist=trueis needed. May need to adjustvite.config.tsfor shared package resolution. - Prisma in monorepo: Need to ensure
prisma generateruns before server build. Add aspostinstallscript.