12 KiB
卡种管理 (Card Types Management) - Complete Exploration Summary
Date: 2026-04-05
Project: MP-Pilates (WeChat Mini-Program for Pilates Studio Booking)
Focus: Card types (卡种) admin feature
📦 What Was Explored
A comprehensive exploration of the card types management system across all three tiers of the application:
- Frontend (Vue 3 + Uni-app)
- Backend (NestJS)
- Database (Prisma/MySQL)
- Shared Types
Total Files Analyzed: 13 files, ~1,800 lines of code
📚 Documentation Generated
Three comprehensive documentation files have been created in the project root:
1. CARD_TYPES_ANALYSIS.md (Complete Technical Guide)
- Sections: 11 major sections
- Content:
- Database schema details
- Shared types and DTOs
- Server-side implementation (controller, service, DTOs)
- Frontend admin page structure
- Admin store (Pinia) implementation
- Complete workflow flows
- API communication details
- Price handling
- Card type categories
- Field requirements & validation table
- Detailed bug analysis: Edit popup closes immediately
2. CARD_TYPES_FLOW_DIAGRAM.txt (Visual Architecture)
- Content:
- Database tier diagram (CardType model, enums, soft delete)
- API tier diagram (endpoints, validators, DTOs)
- Shared types tier
- Frontend tier (page structure, store, components)
- Complete operation flows (Add, Edit, Toggle, Delete)
- Bug analysis with solutions (3 solution options)
3. CARD_TYPES_QUICK_REFERENCE.md (Quick Lookup)
- Sections: 13 quick-reference sections
- Content:
- File quick links with line numbers
- Key data model
- API endpoints
- DTOs & validation rules
- UI components
- Form fields
- Operations guide
- React refs & state
- Admin store methods
- Bug explanation and solutions
- Price handling notes
- Testing checklist
- Card type categories
🎯 Key Findings
Data Structure
CardType
├── id (UUID)
├── name (卡种名称)
├── type (TIMES | DURATION | TRIAL)
├── totalTimes (次卡的次数)
├── durationDays (有效天数)
├── price (现价,单位:分)
├── originalPrice (原价,可选)
├── description (描述)
├── isActive (上架状态)
├── sortOrder (显示顺序)
└── timestamps
Three Card Type Categories
- 次卡 (TIMES): Class count-based (e.g., 10 classes)
- 月卡 (DURATION): Time period-based (e.g., 30 days)
- 体验卡 (TRIAL): Trial cards
Core Operations
- ✅ Create: Add new card types
- ✅ Read: View all cards (admin) or active cards (public)
- ✅ Update: Edit card details or toggle status
- ✅ Delete: Soft delete (sets isActive=false)
API Endpoints
GET /membership/card-types (public)
GET /admin/card-types (admin only)
POST /admin/card-types (admin only)
PUT /admin/card-types/:id (admin only)
DELETE /admin/card-types/:id (admin only)
🐛 Critical Bug Identified
Edit Modal Closes Immediately on Tap
Symptom: When user taps the [编辑] button, the edit form modal appears and then instantly closes.
Root Cause: Event propagation issue
- User taps [编辑] button
openEdit()setsshowModal = true- Modal renders in the same event tick
- Tap event propagates to
modal-maskelement @tap.self="closeModal"fires immediately- Modal closes
Current Code (Buggy):
<view class="ct-action-btn edit-btn" @tap="openEdit(ct)">
<text>编辑</text>
</view>
<view v-if="showModal" class="modal-mask" @tap.self="closeModal">
<!-- form -->
</view>
Recommended Fix (Option 1 - Simplest):
<view class="ct-action-btn edit-btn" @tap.stop="openEdit(ct)">
<!-- Add .stop modifier to stop event propagation -->
</view>
Alternative Fixes: See CARD_TYPES_QUICK_REFERENCE.md for 2 additional solutions using nextTick() or state guards.
📂 File Inventory
Frontend Files
| File | Purpose | Lines |
|---|---|---|
packages/app/src/pages/admin/card-types.vue |
Admin page (ADD, EDIT, DELETE, TOGGLE) | 607 |
packages/app/src/stores/admin.ts |
Pinia store (state + API calls) | 198 |
packages/app/src/utils/request.ts |
HTTP request utilities | 80 |
packages/app/src/utils/format.ts |
Price & date formatting | 46 |
Backend Files
| File | Purpose | Lines |
|---|---|---|
packages/server/src/membership/membership.controller.ts |
API endpoints | 68 |
packages/server/src/membership/membership.service.ts |
Business logic | 173 |
packages/server/src/membership/dto/create-card-type.dto.ts |
Create validation | 45 |
packages/server/src/membership/dto/update-card-type.dto.ts |
Update validation | 49 |
Database Files
| File | Purpose | Lines |
|---|---|---|
packages/server/prisma/schema.prisma |
DB schema definition | 205 |
Shared/Types Files
| File | Purpose | Lines |
|---|---|---|
packages/shared/src/types/card-type.ts |
CardType, CreateCardTypeDto, UpdateCardTypeDto | 39 |
packages/shared/src/enums.ts |
CardTypeCategory enum | 47 |
packages/shared/src/types/api.ts |
API response types | 20 |
packages/shared/src/types/membership.ts |
Membership types | 19 |
Total: 13 files, ~1,800 lines analyzed
🔄 Complete Workflow
Adding a New Card Type
User → [+ 新增卡种] → openAdd()
↓
Modal appears with empty form
User fills: name, type, price, durationDays
↓
[确认] → submitForm()
↓
Validate inputs → Build payload → adminStore.createCardType()
↓
POST /admin/card-types → Backend creates card
↓
Refetch list → Modal closes → Page updates
Editing a Card Type
User → [编辑] on card → openEdit(card)
↓
Modal appears with card data
User edifies fields
↓
[确认] → submitForm()
↓
Validate inputs → Build payload → adminStore.updateCardType(id, payload)
↓
PUT /admin/card-types/:id → Backend updates card
↓
Refetch list → Modal closes → Page updates
Toggling Status (上架/下架)
User → [上架/下架] button → toggleActive(card)
↓
adminStore.updateCardType(id, { isActive: !current })
↓
PUT /admin/card-types/:id → Backend toggles isActive
↓
Refetch list → Card UI updates (opacity, status tag, button text)
Deleting a Card Type
User → [删除] button → confirmDelete(card)
↓
Confirmation dialog appears
User confirms
↓
adminStore.deleteCardType(id)
↓
DELETE /admin/card-types/:id → Backend soft-deletes (isActive=false)
↓
Refetch list → Page updates
💾 Database Details
CardType Model
- Storage: MySQL table
card_types - Primary Key: UUID
- Important Field:
isActive(boolean, default: true) - Delete Strategy: Soft delete (set isActive=false, not actually removed)
- Relationships:
- One-to-many with Membership
- One-to-many with Order
Indexes
isActive(for filtering active cards)sortOrder(for ordering)
🎨 UI/UX Details
Page Layout
┌─ Toolbar ─────────────┐
│ Count + Add button │
├──────────────────────┤
│ Loading skeleton │ (while loading)
├──────────────────────┤
│ Card List │
├──────────────────────┤
│ Modal (Add/Edit) │ (if showModal=true)
└──────────────────────┘
Card Display
- Header: Colored band (type-specific gradient)
- Status tag: "销售中" or "已下架"
- Content: Name, price, description, meta info
- Actions: 3 buttons (编辑, 上架/下架, 删除)
- Inactive styling: opacity: 0.6 when isActive=false
Modal Form
Title: 新增卡种 / 编辑卡种
Fields:
- 卡种名称 (text input)
- 类型 (picker)
- 现价 (digit)
- 原价 (digit, optional)
- 次数 (number, optional)
- 有效天数 (number, default: 90)
- 排序值 (number, default: 0)
- 描述 (textarea, optional)
Buttons: [取消] [确认/保存中...]
🔐 Security & Auth
Authentication
- All admin endpoints require JWT Bearer token
- Token stored in localStorage and included in all requests
Authorization
- Admin endpoints require
UserRole.ADMIN - Enforced via RolesGuard on backend
Public Endpoints
- GET /membership/card-types (no auth needed)
- Returns only
isActive=truecards
📊 Validation Rules
On Create
| Field | Required | Validation |
|---|---|---|
| name | ✓ | Non-empty string |
| type | ✓ | One of: TIMES, DURATION, TRIAL |
| durationDays | ✓ | Int, Min: 1 |
| price | ✓ | Number, Min: 0 |
| totalTimes | - | Int, Min: 1 (optional) |
| originalPrice | - | Number, Min: 0 (optional) |
| description | - | String, Max: 200 (optional) |
| sortOrder | - | Int, Min: 0 (optional, default: 0) |
On Update
- All fields optional (partial update)
- Can include
isActivefor toggling status
💡 Price Handling
Critical: Prices are stored as integers (cents), not floats
- In DB:
98000(cents) - In API:
{ price: 98000 } - Display:
¥980.00(using formatPrice utility)
Conversion:
// Display
formatPrice(cents: number): string {
return (cents / 100).toFixed(2)
}
// Store (frontend → backend)
// User inputs: "980"
// Send as: 98000 (no need to convert, prices are already in cents in the UI)
🧪 Testing Recommendations
Unit Tests Needed
- CardType service methods (create, update, delete)
- Card type validation (DTO validation)
- Price formatting utilities
Integration Tests Needed
- Admin endpoints require ADMIN role
- Public endpoint returns only active cards
- Soft delete sets isActive=false
E2E Tests Needed (Frontend)
- Create card flow
- Edit card flow (including bug fix)
- Toggle status flow
- Delete card flow
- Modal closes properly on submit
- Modal closes on outside tap
- Modal closes on cancel button
🚀 Next Steps (If Implementing Bug Fix)
- Locate file:
packages/app/src/pages/admin/card-types.vue - Find: Line 67 with
<view class="ct-action-btn edit-btn" @tap="openEdit(ct)"> - Change:
@tap="openEdit(ct)"→@tap.stop="openEdit(ct)" - Also check: Lines 6 and 77 (other buttons that might have same issue)
- Test: Try editing a card - modal should stay open
📖 How to Use This Documentation
- Quick lookup: Start with
CARD_TYPES_QUICK_REFERENCE.md - Understanding architecture: Read
CARD_TYPES_FLOW_DIAGRAM.txt - Deep dive: Consult
CARD_TYPES_ANALYSIS.mdfor detailed information - Bug fix: Find solution in Quick Reference "THE BUG" section
📝 Summary Statistics
| Metric | Value |
|---|---|
| Files Analyzed | 13 |
| Total Lines of Code | ~1,800 |
| Endpoints | 5 |
| Card Type Categories | 3 |
| Core Operations | 4 (CRUD) |
| Bugs Identified | 1 |
| Bug Severity | High (UX-breaking) |
| Documentation Pages | 3 |
| Recommended Solution | @tap.stop modifier |
✅ Exploration Complete
All files related to the card types management feature have been thoroughly reviewed, analyzed, and documented.
Key Achievement: Identified and documented the root cause of the edit popup bug, along with three solution approaches.
Ready to:
- Implement bug fix
- Build additional features
- Optimize performance
- Add tests
- Deploy updates
Generated: 2026-04-05
Location: /Users/richard/Documents/code/pilates/mp-pilates/