chore: 添加 CLAUDE.md 和 .env 配置文件

- 添加项目文档 CLAUDE.md,包含常用命令和架构说明
- 添加 packages/server/.env 环境变量配置文件

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
richarjiang
2026-04-06 21:46:15 +08:00
parent f94b48203f
commit 58c7588a96
5 changed files with 117 additions and 27 deletions

69
CLAUDE.md Normal file
View 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订单

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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