# 卡种管理 (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
1. **次卡 (TIMES)**: Class count-based (e.g., 10 classes)
2. **月卡 (DURATION)**: Time period-based (e.g., 30 days)
3. **体验卡 (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()` sets `showModal = true`
- Modal renders in the same event tick
- Tap event propagates to `modal-mask` element
- `@tap.self="closeModal"` fires immediately
- Modal closes
**Current Code (Buggy)**:
```vue
编辑
```
**Recommended Fix (Option 1 - Simplest)**:
```vue
```
**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=true` cards
---
## 📊 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 `isActive` for 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**:
```typescript
// 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)
1. **Locate file**: `packages/app/src/pages/admin/card-types.vue`
2. **Find**: Line 67 with ``
3. **Change**: `@tap="openEdit(ct)"` → `@tap.stop="openEdit(ct)"`
4. **Also check**: Lines 6 and 77 (other buttons that might have same issue)
5. **Test**: Try editing a card - modal should stay open
---
## 📖 How to Use This Documentation
1. **Quick lookup**: Start with `CARD_TYPES_QUICK_REFERENCE.md`
2. **Understanding architecture**: Read `CARD_TYPES_FLOW_DIAGRAM.txt`
3. **Deep dive**: Consult `CARD_TYPES_ANALYSIS.md` for detailed information
4. **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/`