chore: 添加 CLAUDE.md 和 .env 配置文件
- 添加项目文档 CLAUDE.md,包含常用命令和架构说明 - 添加 packages/server/.env 环境变量配置文件 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
69
CLAUDE.md
Normal file
69
CLAUDE.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# CLAUDE.md
|
||||
|
||||
本文档为 Claude Code (claude.ai/code) 在本项目中工作时提供指导。
|
||||
|
||||
## 项目概述
|
||||
|
||||
这是一个普拉提预约微信小程序项目,后端采用 NestJS 框架。项目使用 pnpm monorepo 结构,包含 3 个包:
|
||||
|
||||
- **packages/app** - Vue 3 + uni-app(微信小程序前端)
|
||||
- **packages/server** - NestJS(后端 API 服务)
|
||||
- **packages/shared** - TypeScript 类型定义、枚举、常量(前后端共用)
|
||||
|
||||
## 常用命令
|
||||
|
||||
```bash
|
||||
# 开发
|
||||
pnpm dev:server # 启动 NestJS 后端(热重载)
|
||||
pnpm dev:app # 构建 uni-app 为微信小程序
|
||||
|
||||
# 构建
|
||||
pnpm build:shared # 编译共享类型
|
||||
pnpm build:server # 构建 NestJS 后端
|
||||
pnpm build:app # 构建微信小程序
|
||||
|
||||
# 测试与代码检查
|
||||
pnpm test # 运行所有测试(仅 server)
|
||||
pnpm lint # 运行 ESLint(仅 server)
|
||||
|
||||
# 数据库相关(位于 packages/server 目录)
|
||||
cd packages/server
|
||||
pnpm prisma:generate # 生成 Prisma 客户端
|
||||
pnpm prisma:migrate # 执行数据库迁移
|
||||
pnpm prisma:seed # 填充测试数据
|
||||
pnpm test:watch # 监听模式运行测试
|
||||
|
||||
# 部署
|
||||
pnpm deploy:server # 部署后端到生产环境
|
||||
```
|
||||
|
||||
## 架构说明
|
||||
|
||||
### 前端 (packages/app)
|
||||
- 基于 Vue 3 + uni-app 框架,主攻微信小程序平台
|
||||
- 页面目录:`src/pages/`(包含 home、booking、card、profile、admin 等模块)
|
||||
- 组件目录:`src/components/`
|
||||
- 状态管理:Pinia
|
||||
- 样式:SCSS
|
||||
|
||||
### 后端 (packages/server)
|
||||
- 框架:NestJS + Prisma ORM
|
||||
- 核心模块:auth(认证)、user(用户)、booking(预约)、membership(会员卡)、payment(支付)、studio(场馆)、time-slot(时段)、scheduler(定时任务)、admin(管理)
|
||||
- 认证:JWT + 微信登录
|
||||
- 定时任务:@nestjs/schedule
|
||||
- 数据库:SQLite(开发)/ MySQL(生产)
|
||||
|
||||
### 共享包 (packages/shared)
|
||||
- TypeScript 接口和类型定义
|
||||
- 枚举值定义
|
||||
- 前后端共用的 DTO 类型
|
||||
|
||||
### API 结构
|
||||
- 所有接口统一前缀:`/api`
|
||||
- RESTful 风格接口
|
||||
- 全局拦截器:日志记录、响应包装
|
||||
- 全局过滤器:异常处理
|
||||
|
||||
### 数据库
|
||||
- Prisma schema 位于 `packages/server/prisma/schema.prisma`
|
||||
- 核心数据模型:User(用户)、Studio(场馆)、TimeSlot(时段)、Booking(预约)、Membership(会员卡)、CardType(卡种)、Order(订单)
|
||||
@@ -133,7 +133,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import CustomNavBar from '../../components/CustomNavBar.vue'
|
||||
import { getSystemLayout } from '../../utils/system'
|
||||
import { useAdminStore } from '../../stores/admin'
|
||||
@@ -202,9 +202,9 @@ async function loadOrders(reset = false) {
|
||||
if (activeFilter.value) params.status = activeFilter.value
|
||||
const result = await adminStore.fetchAdminOrders(params)
|
||||
if (reset) {
|
||||
orders.value = [...result.data]
|
||||
orders.value = [...result.items]
|
||||
} else {
|
||||
orders.value.push(...result.data)
|
||||
orders.value.push(...result.items)
|
||||
}
|
||||
hasMore.value = orders.value.length < result.total
|
||||
totalCount.value = result.total
|
||||
|
||||
@@ -103,15 +103,15 @@
|
||||
<view class="hero-badge">
|
||||
<text class="hero-badge-text">{{ typeLabel }}</text>
|
||||
</view>
|
||||
<text class="hero-name">{{ card.name }}</text>
|
||||
<text class="hero-name">{{ cardData.name }}</text>
|
||||
<view class="hero-price-row">
|
||||
<text class="hero-currency">¥</text>
|
||||
<text class="hero-price">{{ formatPrice(card.price) }}</text>
|
||||
<text class="hero-price">{{ formatPrice(cardData.price) }}</text>
|
||||
<text
|
||||
v-if="card.originalPrice && card.originalPrice > card.price"
|
||||
v-if="cardData.originalPrice && cardData.originalPrice > cardData.price"
|
||||
class="hero-original"
|
||||
>
|
||||
¥{{ formatPrice(card.originalPrice) }}
|
||||
¥{{ formatPrice(cardData.originalPrice) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -121,28 +121,28 @@
|
||||
<!-- Key info grid -->
|
||||
<view class="info-card">
|
||||
<view class="info-grid">
|
||||
<view class="info-cell" v-if="card.totalTimes">
|
||||
<text class="cell-value">{{ card.totalTimes }}</text>
|
||||
<view class="info-cell" v-if="cardData.totalTimes">
|
||||
<text class="cell-value">{{ cardData.totalTimes }}</text>
|
||||
<text class="cell-label">课时次数</text>
|
||||
</view>
|
||||
<view class="info-cell">
|
||||
<text class="cell-value">{{ card.durationDays }}</text>
|
||||
<text class="cell-value">{{ cardData.durationDays }}</text>
|
||||
<text class="cell-label">有效天数</text>
|
||||
</view>
|
||||
<view class="info-cell">
|
||||
<text class="cell-value">{{ unitPrice }}</text>
|
||||
<text class="cell-label">{{ card.totalTimes ? '每次单价' : '按天均价' }}</text>
|
||||
<text class="cell-label">{{ cardData.totalTimes ? '每次单价' : '按天均价' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Description -->
|
||||
<view v-if="card.description" class="desc-card">
|
||||
<view v-if="cardData.description" class="desc-card">
|
||||
<view class="section-header">
|
||||
<view class="section-dot" />
|
||||
<text class="section-title">课程说明</text>
|
||||
</view>
|
||||
<text class="desc-content">{{ card.description }}</text>
|
||||
<text class="desc-content">{{ cardData.description }}</text>
|
||||
</view>
|
||||
|
||||
<!-- Features list -->
|
||||
@@ -153,13 +153,13 @@
|
||||
</view>
|
||||
<view class="feature-item">
|
||||
<text class="feature-dot">•</text>
|
||||
<text class="feature-text">购买后立即生效,有效期 {{ card.durationDays }} 天</text>
|
||||
<text class="feature-text">购买后立即生效,有效期 {{ cardData.durationDays }} 天</text>
|
||||
</view>
|
||||
<view v-if="card.totalTimes" class="feature-item">
|
||||
<view v-if="cardData.totalTimes" class="feature-item">
|
||||
<text class="feature-dot">•</text>
|
||||
<text class="feature-text">共 {{ card.totalTimes }} 次课时,可灵活安排上课时间</text>
|
||||
<text class="feature-text">共 {{ cardData.totalTimes }} 次课时,可灵活安排上课时间</text>
|
||||
</view>
|
||||
<view v-if="!card.totalTimes" class="feature-item">
|
||||
<view v-if="!cardData.totalTimes" class="feature-item">
|
||||
<text class="feature-dot">•</text>
|
||||
<text class="feature-text">有效期内可无限次预约课程</text>
|
||||
</view>
|
||||
@@ -182,7 +182,7 @@
|
||||
<view class="bottom-bar">
|
||||
<view class="price-summary">
|
||||
<text class="summary-label">实付金额</text>
|
||||
<text class="summary-price">¥{{ formatPrice(card.price) }}</text>
|
||||
<text class="summary-price">¥{{ formatPrice(cardData.price) }}</text>
|
||||
</view>
|
||||
<view
|
||||
class="buy-btn"
|
||||
@@ -250,6 +250,8 @@ const unitPrice = computed(() => {
|
||||
return `¥${(pricePerDay / 100).toFixed(0)}`
|
||||
})
|
||||
|
||||
const cardData = computed<CardType>(() => card.value as CardType)
|
||||
|
||||
// ─── Data loading ─────────────────────────────────────────
|
||||
async function loadCard() {
|
||||
loading.value = true
|
||||
|
||||
@@ -48,8 +48,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick } from 'vue'
|
||||
import { onShow, onUnmount, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
||||
import { ref, nextTick, onUnmounted } from 'vue'
|
||||
import { onShow, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
||||
|
||||
import BrandBanner from '../../components/BrandBanner.vue'
|
||||
import StudioInfo from '../../components/StudioInfo.vue'
|
||||
@@ -93,7 +93,7 @@ uni.$on('scrollToCardShop', () => {
|
||||
pendingScrollToCardShop.value = true
|
||||
})
|
||||
|
||||
onUnmount(() => {
|
||||
onUnmounted(() => {
|
||||
uni.$off('scrollToCardShop')
|
||||
})
|
||||
|
||||
@@ -142,14 +142,12 @@ function scrollToCardShop() {
|
||||
.select(`#${cardShopAnchorId}`)
|
||||
.boundingClientRect()
|
||||
.selectViewport()
|
||||
.scrollOffset()
|
||||
.exec((res) => {
|
||||
if (res && res[0] && res[1]) {
|
||||
const rectTop = (res[0] as UniApp.NodeInfo).top ?? 0
|
||||
const viewportScroll = (res[1] as UniApp.NodeInfo).scrollTop ?? 0
|
||||
scrollTop.value = viewportScroll + rectTop
|
||||
.scrollOffset((res) => {
|
||||
if (res) {
|
||||
scrollTop.value = (res as UniApp.NodeInfo).scrollTop ?? 0
|
||||
}
|
||||
})
|
||||
.exec()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
21
packages/server/.env
Normal file
21
packages/server/.env
Normal file
@@ -0,0 +1,21 @@
|
||||
# Database
|
||||
DATABASE_URL=mysql://root:AK8jyLfsfMA5wNdC@129.204.155.94:13306/db_mp_focus
|
||||
|
||||
# JWT
|
||||
JWT_SECRET=change-me-to-a-secure-random-string
|
||||
|
||||
# WeChat Mini Program
|
||||
WX_APPID=wx3e7a133d2305fa2c
|
||||
WX_SECRET=92f4f91af72ca0705d65e39e605cb98b
|
||||
|
||||
# WeChat Pay
|
||||
WX_MCH_ID=1110530023
|
||||
WX_MCH_KEY=ACbGcH3FNLBacmvmIVR4uWXjNf9h8jQ2
|
||||
WX_MCH_SERIAL_NO=7A90D96A7ED1A129E98DB5FD5F3A84EDC34B2AC6
|
||||
WX_MCH_KEY_PATH=./certs/apiclient_key.pem
|
||||
|
||||
# API Base URL (used for WeChat Pay callback notification)
|
||||
API_BASE_URL=https://focus.richarjiang.com/
|
||||
|
||||
# Server
|
||||
PORT=3000
|
||||
Reference in New Issue
Block a user