429 lines
12 KiB
Markdown
429 lines
12 KiB
Markdown
# 卡种管理 (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
|
||
<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)**:
|
||
```vue
|
||
<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=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 `<view class="ct-action-btn edit-btn" @tap="openEdit(ct)">`
|
||
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/`
|
||
|