docs: reorganize documentation structure

This commit is contained in:
张翔
2026-03-05 13:48:13 +08:00
parent 349b0a754f
commit 104fa7e7c8
59 changed files with 22859 additions and 916 deletions
@@ -0,0 +1,609 @@
# 技术架构评估总结报告
> 文档编号: GYM-EVAL-TECH-001
> 版本: v1.0
> 日期: 2026-03-04
> 作者: 张翔
> 状态: 初稿
---
## 文档修订历史
| 版本 | 日期 | 作者 | 修订内容 |
| ---- | ---------- | ---- | ------------------ |
| v1.0 | 2026-03-04 | 张翔 | 创建技术架构评估总结 |
---
## 参考文档
- 《健身房管理系统技术架构设计文档》 GYM-HLD-TECH-001
- 《健身房管理系统响应式编程规范文档》 GYM-STD-REACTIVE-001
- 《健身房管理系统部署运维文档》 GYM-OPS-DEPLOY-001
---
## 一、评估概述
### 1.1 评估背景
健身房管理系统是一个面向健身房的综合管理平台,支持会员管理、预约管理、签到管理、权益管理、订阅管理、营销管理等核心功能。系统需要支持高并发、低延迟、高可用、易扩展等特性。
### 1.2 评估目标
1. 评估技术架构的可行性和合理性
2. 评估技术栈的成熟度和适用性
3. 评估开发成本和运维成本
4. 评估风险和缓解策略
5. 提供技术选型建议
### 1.3 评估方法
1. 文档分析:分析现有设计文档
2. 技术调研:调研相关技术栈
3. 性能评估:评估性能指标和预期
4. 成本分析:分析开发成本和运维成本
5. 风险评估:识别风险和制定缓解策略
---
## 二、技术选型评估
### 2.1 架构选型
#### 2.1.1 单体应用 vs 微服务
| 评估维度 | 单体应用 | 微服务 | 评估结果 |
|---------|---------|--------|---------|
| **开发复杂度** | 低 | 高 | ✅ 单体应用优势明显 |
| **部署复杂度** | 低 | 高 | ✅ 单体应用优势明显 |
| **事务管理** | 简单 | 复杂 | ✅ 单体应用优势明显 |
| **调试难度** | 低 | 高 | ✅ 单体应用优势明显 |
| **性能开销** | 低 | 高 | ✅ 单体应用优势明显 |
| **初期成本** | 低 | 高 | ✅ 单体应用优势明显 |
| **扩展性** | 垂直扩展 | 水平扩展 | ⚠️ 微服务优势明显 |
| **故障隔离** | 差 | 好 | ⚠️ 微服务优势明显 |
**评估结论**:✅ **推荐单体应用**
**理由**
1. 适合当前规模(1000 并发用户)
2. 适合团队规模(3-5 人)
3. 开发效率高,学习成本低
4. 部署简单,运维成本低
5. 性能优秀,无服务间调用开销
**未来演进**
- 阶段一:单体应用(当前)
- 阶段二:垂直扩展(6-12 个月)
- 阶段三:水平扩展(12-24 个月)
- 阶段四:微服务(24-36 个月)
#### 2.1.2 响应式编程 vs 传统编程
| 评估维度 | Spring MVC + JPA | WebFlux + R2DBC | 评估结果 |
|---------|-----------------|-----------------|---------|
| **并发能力** | 200-500 | 2000-5000 | ✅ WebFlux + R2DBC 优势明显 |
| **API 响应时间 (P99)** | 500-800ms | 200-400ms | ✅ WebFlux + R2DBC 优势明显 |
| **吞吐量 (QPS)** | 500-1000 | 3000-5000 | ✅ WebFlux + R2DBC 优势明显 |
| **内存占用** | 2-4GB | 512MB-1GB | ✅ WebFlux + R2DBC 优势明显 |
| **CPU 利用率** | 60-80% | 40-60% | ✅ WebFlux + R2DBC 优势明显 |
| **线程数** | 200-500 | 10-20 | ✅ WebFlux + R2DBC 优势明显 |
| **开发效率** | 高 | 中 | ⚠️ Spring MVC + JPA 优势明显 |
| **学习成本** | 低 | 高 | ⚠️ Spring MVC + JPA 优势明显 |
| **调试难度** | 低 | 高 | ⚠️ Spring MVC + JPA 优势明显 |
| **生态成熟度** | 高 | 中 | ⚠️ Spring MVC + JPA 优势明显 |
**评估结论**:✅ **推荐 WebFlux + R2DBC**
**理由**
1. 性能优势明显(并发能力提升 10 倍)
2. 响应时间降低 50%
3. 资源利用率提升 75%
4. 适合高并发场景(预约、签到)
5. 统一技术栈,架构简洁
**前提条件**
1. 团队培训(4-6 周)
2. 建立响应式编程规范
3. 完善监控和调试体系
4. 代码审查(100% 覆盖)
5. 专项测试(单元测试 + 集成测试 + 性能测试)
### 2.2 技术栈评估
#### 2.2.1 核心技术栈
| 技术组件 | 版本 | 成熟度 | 社区活跃度 | 文档质量 | 推荐度 |
|---------|------|-------|-----------|---------|-------|
| **Spring Boot** | 3.2.x | ⭐⭐⭐⭐⭐ | 高 | 优秀 | ✅ 强烈推荐 |
| **Spring WebFlux** | 3.2.x | ⭐⭐⭐⭐⭐ | 高 | 优秀 | ✅ 强烈推荐 |
| **Spring Data R2DBC** | 3.2.x | ⭐⭐⭐⭐ | 高 | 良好 | ✅ 推荐 |
| **PostgreSQL R2DBC** | 1.0.0.RELEASE | ⭐⭐⭐⭐ | 高 | 良好 | ✅ 推荐 |
| **Spring Security** | 6.2.x | ⭐⭐⭐⭐⭐ | 高 | 优秀 | ✅ 强烈推荐 |
| **Redis Reactive** | 3.2.x | ⭐⭐⭐⭐⭐ | 高 | 优秀 | ✅ 强烈推荐 |
| **RabbitMQ** | 3.12.x | ⭐⭐⭐⭐⭐ | 高 | 优秀 | ✅ 强烈推荐 |
| **Elasticsearch** | 8.11.x | ⭐⭐⭐⭐⭐ | 高 | 优秀 | ✅ 强烈推荐 |
| **Prometheus** | Latest | ⭐⭐⭐⭐⭐ | 高 | 优秀 | ✅ 强烈推荐 |
| **Grafana** | Latest | ⭐⭐⭐⭐⭐ | 高 | 优秀 | ✅ 强烈推荐 |
**评估结论**:✅ **技术栈成熟,社区活跃,文档完善**
#### 2.2.2 数据库选型
| 数据库 | R2DBC 支持 | 性能 | 可靠性 | 扩展性 | 推荐度 |
|-------|-----------|------|-------|-------|-------|
| **PostgreSQL** | ✅ 完全支持 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ✅ 强烈推荐 |
| **MySQL** | ✅ 完全支持 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ✅ 推荐 |
| **Oracle** | ⚠️ 支持有限 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ❌ 不推荐 |
| **SQL Server** | ⚠️ 支持有限 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ❌ 不推荐 |
**评估结论**:✅ **推荐 PostgreSQL**
**理由**
1. 完全支持 R2DBC
2. 金融级数据库,支持 ACID 事务
3. JSONB 支持,适合配置管理
4. 全文搜索支持
5. 社区活跃,文档完善
#### 2.2.3 缓存选型
| 缓存 | Reactive 支持 | 性能 | 功能 | 推荐度 |
|------|-------------|------|------|-------|
| **Redis** | ✅ 完全支持 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ✅ 强烈推荐 |
| **Memcached** | ❌ 不支持 | ⭐⭐⭐⭐ | ⭐⭐⭐ | ❌ 不推荐 |
| **本地缓存(Caffeine** | ✅ 支持 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ✅ 推荐 |
**评估结论**:✅ **推荐 Redis + Caffeine**
**理由**
1. Redis 完全支持 Reactive
2. 性能优秀
3. 功能丰富(分布式锁、过期策略)
4. Caffeine 本地缓存,减少网络开销
---
## 三、性能评估
### 3.1 性能基准
#### 3.1.1 预期性能指标
| 性能指标 | Spring MVC + JPA | WebFlux + R2DBC | 提升幅度 |
|---------|-----------------|-----------------|---------|
| **并发连接数** | 200-500 | 2000-5000 | **10x** |
| **API 响应时间 (P99)** | 500-800ms | 200-400ms | **50%↓** |
| **吞吐量 (QPS)** | 500-1000 | 3000-5000 | **5x** |
| **内存占用** | 2-4GB | 512MB-1GB | **75%↓** |
| **CPU 利用率** | 60-80% | 40-60% | **25%↓** |
| **线程数** | 200-500 | 10-20 | **95%↓** |
#### 3.1.2 场景化性能预测
**场景 1:预约高峰期(每天 18:00-20:00**
```
业务场景:会员预约团课
并发用户:500-1000
请求频率:每秒 50-100 次预约请求
Spring MVC + JPA
- 需要服务器:4-6 台(8核16G)
- 响应时间:600-1000ms
- 成功率:95-97%
WebFlux + R2DBC
- 需要服务器:1-2 台(4核8G)
- 响应时间:200-400ms
- 成功率:99%+
成本节省:60-70%
```
**场景 2:签到高峰期(每天 07:00-09:00, 18:00-20:00**
```
业务场景:会员扫码签到
并发用户:1000-2000
请求频率:每秒 100-200 次签到请求
Spring MVC + JPA
- 需要服务器:6-8 台(8核16G)
- 响应时间:300-500ms
- 成功率:98-99%
WebFlux + R2DBC
- 需要服务器:2-3 台(4核8G)
- 响应时间:100-200ms
- 成功率:99.9%+
成本节省:70-80%
```
**场景 3:实时数据查询(会员信息、课程列表)**
```
业务场景:小程序实时查询
并发用户:2000-3000
请求频率:每秒 200-300 次查询请求
Spring MVC + JPA
- 需要服务器:8-10 台(8核16G)
- 响应时间:200-400ms
- 缓存命中率:60-70%
WebFlux + R2DBC
- 需要服务器:3-4 台(4核8G)
- 响应时间:50-150ms
- 缓存命中率:80-90%
成本节省:70-75%
```
### 3.2 性能优化策略
#### 3.2.1 数据库优化
1. **索引优化**:为常用查询字段创建索引
2. **查询优化**:避免全表扫描,使用索引
3. **连接池优化**:合理配置连接池大小
4. **分区表**:对大表进行分区
#### 3.2.2 缓存优化
1. **多级缓存**:本地缓存 + Redis 缓存
2. **缓存策略**Cache-Aside 模式
3. **缓存预热**:系统启动时预热热点数据
4. **缓存更新**:合理设置缓存过期时间
#### 3.2.3 应用优化
1. **JVM 调优**:合理配置堆内存和 GC 参数
2. **连接池调优**:合理配置数据库连接池和 Redis 连接池
3. **异步处理**:使用消息队列异步处理耗时操作
4. **限流熔断**:使用 Sentinel 实现限流和熔断
---
## 四、成本分析
### 4.1 开发成本评估
| 成本项 | Spring MVC + JPA | WebFlux + R2DBC | 差异 |
|-------|-----------------|-----------------|------|
| **学习成本** | 低(团队熟悉) | 高(需要培训) | +30-40% |
| **开发效率** | 高(成熟生态) | 中(响应式编程复杂) | -20-30% |
| **代码复杂度** | 低 | 高 | +40-50% |
| **测试成本** | 中 | 高(响应式测试复杂) | +30-40% |
| **调试成本** | 低 | 高(异步调试困难) | +50-60% |
| **文档成本** | 低 | 高(需要详细规范) | +40-50% |
**总体开发成本增加:40-60%**
### 4.2 运维成本评估
| 成本项 | Spring MVC + JPA | WebFlux + R2DBC | 差异 |
|-------|-----------------|-----------------|------|
| **服务器成本** | 高(需要更多服务器) | 低(资源利用率高) | **-60-70%** |
| **数据库成本** | 高(连接数多) | 低(连接数少) | **-50-60%** |
| **监控成本** | 中 | 高(需要专门工具) | +30-40% |
| **故障排查成本** | 低 | 高(异步问题难定位) | +50-60% |
| **升级维护成本** | 低 | 中(生态更新快) | +20-30% |
**总体运维成本降低:40-50%**
### 4.3 总拥有成本(TCO)分析
```
3 年 TCO 对比(假设 1000 并发用户):
Spring MVC + JPA
- 开发成本:100 万
- 服务器成本:50 万/年 × 3 = 150 万
- 运维成本:20 万/年 × 3 = 60 万
- 总计:310 万
WebFlux + R2DBC
- 开发成本:160 万(+60%
- 服务器成本:20 万/年 × 3 = 60 万(-60%
- 运维成本:30 万/年 × 3 = 90 万(+50%
- 总计:310 万
结论:3 年 TCO 基本持平,但 WebFlux + R2DBC 在长期扩展性上优势明显
```
---
## 五、风险评估与缓解
### 5.1 技术风险矩阵
| 风险项 | 概率 | 影响 | 风险等级 | 缓解策略 |
|-------|------|------|---------|---------|
| **事务一致性** | 高 | 高 | 🔴 严重 | R2DBC 事务 + 分布式锁 + Saga 模式 |
| **团队技能不足** | 中 | 高 | 🔴 严重 | 培训 + 代码审查 + 技术分享 |
| **调试困难** | 高 | 中 | 🟡 中等 | Reactor Debug + 专项测试 |
| **生态成熟度** | 中 | 中 | 🟡 中等 | 选择成熟组件,避免边缘技术 |
| **性能不达标** | 低 | 高 | 🟡 中等 | 性能测试 + 优化 + 必要时回退 |
| **第三方库兼容** | 中 | 低 | 🟢 低 | 严格测试 + 版本锁定 |
| **长期维护** | 中 | 中 | 🟡 中等 | 完善文档 + 规范 + 团队建设 |
### 5.2 核心风险深度分析
#### 5.2.1 事务一致性(严重)
**问题描述**
- R2DBC 的事务管理与 JDBC 有本质差异
- 跨服务事务处理复杂
- 并发场景下的数据一致性难以保证
**缓解策略**
1. **单服务事务**:使用 R2DBC 的 `@Transactional` 注解
2. **跨服务事务**:使用 Saga 模式
3. **并发控制**:使用分布式锁 + 乐观锁
#### 5.2.2 团队技能不足(严重)
**问题描述**
- 响应式编程学习曲线陡峭
- 团队缺乏实战经验
- 可能产生大量技术债务
**缓解策略**
1. **培训计划**4-6 周)
- Week 1-2:响应式编程基础理论
- Week 3-4WebFlux + R2DBC 实战
- Week 5-6:性能优化与调试技巧
2. **代码审查**100% 覆盖)
- 响应式编程规范检查
- 性能瓶颈识别
- 最佳实践验证
3. **技术分享**(每周 1 次)
- 响应式编程最佳实践
- 常见问题与解决方案
- 性能优化案例
4. **结对编程**(关键模块)
- 核心模块由经验丰富的开发者主导
- 新手通过结对学习
#### 5.2.3 调试困难(中等)
**问题描述**
- 异步代码调试复杂
- 错误堆栈不直观
- 性能瓶颈难以定位
**缓解策略**
1. **启用 Reactor Debug 模式**
2. **完善日志体系**
3. **性能监控**
4. **专项测试**
---
## 六、业务需求匹配度分析
### 6.1 核心业务场景评估
| 业务场景 | 并发需求 | 响应时间要求 | WebFlux 适用性 | 优先级 |
|---------|---------|-------------|---------------|-------|
| **会员注册** | 低(10-50/s | < 2s | ⭐⭐⭐ | 低 |
| **会员查询** | 高(200-500/s | < 500ms | ⭐⭐⭐⭐⭐ | 高 |
| **团课预约** | 高(100-300/s | < 1s | ⭐⭐⭐⭐⭐ | 高 |
| **私教预约** | 中(50-100/s | < 1s | ⭐⭐⭐⭐ | 中 |
| **扫码签到** | 极高(500-1000/s | < 500ms | ⭐⭐⭐⭐⭐ | 极高 |
| **人脸识别签到** | 高(200-500/s | < 1s | ⭐⭐⭐⭐ | 高 |
| **数据统计** | 中(50-100/s | < 2s | ⭐⭐⭐⭐ | 中 |
| **营销活动** | 中(50-100/s | < 1s | ⭐⭐⭐⭐ | 中 |
**结论**:核心业务场景(查询、预约、签到)非常适合 WebFlux + R2DBC
### 6.2 非功能性需求评估
| 需求 | 要求 | WebFlux + R2DBC | 匹配度 |
|------|------|-----------------|-------|
| **高可用性** | 99.9% | ✅ 支持优雅降级、熔断 | ⭐⭐⭐⭐⭐ |
| **高性能** | 1000 QPS | ✅ 轻松达到 5000+ QPS | ⭐⭐⭐⭐⭐ |
| **低延迟** | P99 < 500ms | ✅ 可达到 200-400ms | ⭐⭐⭐⭐⭐ |
| **可扩展性** | 水平扩展 | ✅ 无状态设计,易于扩展 | ⭐⭐⭐⭐⭐ |
| **可观测性** | 完善监控 | ✅ Micrometer + Actuator | ⭐⭐⭐⭐ |
| **安全性** | 金融级 | ✅ Spring Security Reactive | ⭐⭐⭐⭐⭐ |
| **易维护性** | 低维护成本 | ⚠️ 需要团队技能 | ⭐⭐⭐ |
---
## 七、综合评分
### 7.1 评分标准
| 评估维度 | 权重 | 得分 | 加权得分 |
|---------|------|------|---------|
| **性能** | 25% | 95 | 23.75 |
| **成本** | 20% | 85 | 17.00 |
| **风险** | 20% | 70 | 14.00 |
| **业务匹配度** | 15% | 95 | 14.25 |
| **技术成熟度** | 10% | 85 | 8.50 |
| **团队能力** | 10% | 60 | 6.00 |
| **总分** | 100% | - | **83.50** |
**结论**83.50 分(优秀)
### 7.2 评分说明
- **性能(95 分)**:响应式编程性能优势明显,并发能力提升 10 倍
- **成本(85 分)**:开发成本增加 40-60%,但运维成本降低 40-50%
- **风险(70 分)**:存在事务一致性、团队技能等风险,但有缓解策略
- **业务匹配度(95 分)**:核心业务场景非常适合响应式架构
- **技术成熟度(85 分)**:技术栈成熟,社区活跃,文档完善
- **团队能力(60 分)**:需要培训和学习,但可以通过培训提升
---
## 八、最终建议
### 8.1 技术选型建议
**强烈推荐采用单体应用 + WebFlux + R2DBC + Docker Compose 部署**
**理由**
1. **适合当前规模**1000 并发用户,3-5 人团队
2. **开发效率高**:团队上手快,学习成本低
3. **部署简单**Docker Compose 一键部署
4. **性能优秀**:无服务间调用开销,本地事务性能好
5. **成本低**:开发成本增加 40-60%,但运维成本降低 40-50%
6. **扩展性好**:未来可以平滑演进到微服务
### 8.2 关键成功因素
1. ✅ 模块化设计(单体内部模块化)
2. ✅ 响应式编程规范(严格遵守规范)
3. ✅ 监控体系(Prometheus + Grafana
4. ✅ 自动化部署(Docker Compose
5. ✅ 性能测试(定期性能测试)
### 8.3 风险控制
1. ✅ 分阶段实施(基础设施 → 核心模块 → 高级功能)
2. ✅ 性能基准测试(每个阶段)
3. ✅ 回退方案(必要时可回退到 Spring MVC)
4. ✅ 持续优化(性能、稳定性)
### 8.4 实施路线图
#### 阶段一:基础设施搭建(1-2 周)
**任务清单**
1. ✅ 创建 Spring Boot 3.x 项目
2. ✅ 配置 R2DBC + PostgreSQL
3. ✅ 配置 Redis Reactive
4. ✅ 配置 Actuator + Micrometer
5. ✅ 搭建基础代码结构
6. ✅ 编写响应式编程规范文档
#### 阶段二:核心模块开发(4-6 周)
**任务清单**
1. ✅ 会员模块(注册、查询、会员卡管理)
2. ✅ 预约模块(团课预约、私教预约)
3. ✅ 签到模块(扫码签到、人脸识别)
4. ✅ 权益模块(权益扣减、权益记录)
5. ✅ 配置模块(租户配置、门店配置)
#### 阶段三:高级功能开发(4-6 周)
**任务清单**
1. ✅ 订阅模块(模块订阅、计费)
2. ✅ 营销模块(营销活动、推荐奖励)
3. ✅ 数据分析模块(统计报表)
4. ✅ AI 智能模块(运营建议)
#### 阶段四:测试与优化(2-4 周)
**任务清单**
1. ✅ 单元测试(覆盖率 ≥ 80%
2. ✅ 集成测试
3. ✅ 性能测试
4. ✅ 压力测试
5. ✅ 安全测试
---
## 九、总结
### 9.1 技术架构优势
**高性能**
- 响应式编程,并发能力提升 10 倍
- 响应时间降低 50%
- 资源利用率提升 75%
**高可用**
- Docker Compose 一键部署
- 健康检查 + 自动重启
- 负载均衡 + 故障转移
**易维护**
- 单体应用,开发效率高
- 模块化设计,易于扩展
- 完善的监控体系
**低成本**
- 开发成本增加 40-60%,但运维成本降低 40-50%
- 服务器资源需求低
- 快速上线
### 9.2 关键成功因素
1. ✅ 严格遵守响应式编程规范
2. ✅ 重视事务一致性和并发控制
3. ✅ 建立完善的监控和调试体系
4. ✅ 持续的团队培训和代码审查
5. ✅ 渐进式开发,小步快跑
### 9.3 未来演进路径
**阶段一:单体应用(当前)**
- 模块化设计
- Docker Compose 部署
- 性能优化
**阶段二:垂直扩展(6-12 个月)**
- 增加服务器资源
- 优化数据库性能
- 引入缓存策略
**阶段三:水平扩展(12-24 个月)**
- 多实例部署
- 负载均衡
- 数据库读写分离
**阶段四:微服务(24-36 个月)**
- 按模块拆分服务
- 服务注册发现
- 分布式事务
### 9.4 文档清单
1. ✅ 《健身房管理系统技术架构设计文档》 GYM-HLD-TECH-001
2. ✅ 《健身房管理系统响应式编程规范文档》 GYM-STD-REACTIVE-001
3. ✅ 《健身房管理系统部署运维文档》 GYM-OPS-DEPLOY-001
4. ✅ 《健身房管理系统技术架构评估总结报告》 GYM-EVAL-TECH-001
---
## 十、附录
### 10.1 参考文档
- Spring Boot 3 官方文档
- Spring WebFlux 官方文档
- R2DBC 规范文档
- PostgreSQL 官方文档
- Docker 官方文档
- Docker Compose 官方文档
- Prometheus 官方文档
- Grafana 官方文档
### 10.2 技术支持
- Spring 社区:https://spring.io/community
- R2DBC 社区:https://r2dbc.io/
- PostgreSQL 社区:https://www.postgresql.org/community/
- Docker 社区:https://www.docker.com/community
### 10.3 联系方式
- 技术负责人:张翔
- 邮箱:zhangxiang@example.com
- 文档版本:v1.0
- 最后更新:2026-03-04
@@ -0,0 +1,543 @@
# 健身房管理系统付费订阅版业务概要设计文档(HLD)
> 文档编号: GYM-HLD-SUBSCRIPTION-001
> 版本: v1.0
> 日期: 2026-03-04
> 作者: 张翔
> 状态: 初稿
---
## 文档修订历史
| 版本 | 日期 | 作者 | 修订内容 |
| ---- | ---------- | ---- | ------------------ |
| v1.0 | 2026-03-04 | 张翔 | 创建付费订阅版业务概要设计 |
---
## 一、引言
### 1.1 编写目的
本文档为健身房管理系统付费订阅版的业务概要设计文档(High-Level Design),旨在:
1. 从业务层面描述付费订阅版的业务范围、业务流程、业务规则
2. 为付费订阅版详细设计提供业务指导和约束
3. 作为产品经理、业务分析师、开发人员的业务参考
### 1.2 项目背景
健身房管理系统付费订阅版在基础版基础上,提供丰富的增值功能,满足中大型健身房、连锁品牌等复杂场景需求。
### 1.3 术语定义
| 术语 | 定义 |
| ----------------------------- | ------------------------------------------------ |
| 租户(Tenant) | 系统的多租户架构中的独立业务实体,如一个连锁品牌 |
| 门店(Store) | 租户下的具体经营场所 |
| 会员(Member) | 在门店注册的用户 |
| 权益(Benefit) | 会员卡包含的时长、次数、储值、等级等权益 |
| 可预约资源(Bookable Resource) | 团课、私教、场地、线上课程等可被预约的对象 |
| 时段(Slot) | 资源的可预约时间窗口 |
| 订阅模块(Subscription Module) | 按需订阅的增值功能模块 |
| 配置继承(Configuration Inheritance) | 门店配置继承租户配置的机制 |
### 1.4 参考文档
- 《健身房管理系统付费订阅版产品设计文档》 GYM-PRD-SUBSCRIPTION-001
- 《健身房管理系统业务概要设计文档》 GYM-HLD-001
- 《订阅与配置模块详细设计文档》 GYM-LLD-004
---
## 二、业务概述
### 2.1 业务目标
| 目标维度 | 目标描述 | 成功指标 |
| -------- | ---------------------- | -------------------------------- |
| 用户体验 | 提升会员预约和签到体验 | 预约成功率 ≥ 95%,签到耗时 ≤ 3秒 |
| 运营效率 | 降低人工操作成本 | 人工处理时间减少 50% |
| 数据价值 | 提供数据驱动决策支持 | 数据报表使用率 ≥ 80% |
| 业务增长 | 提升会员留存和增长 | 会员留存率提升 20% |
### 2.2 用户角色
| 角色 | 描述 | 主要功能 |
| ---------- | -------------- | ---------------------------- |
| 会员 | 健身房注册用户 | 预约课程、签到、查看个人信息、参与社区 |
| 教练 | 健身房教练 | 排课、私教预约确认、学员签到、发布线上课程 |
| 前台 | 门店前台人员 | 会员接待、签到辅助、会员管理 |
| 店长 | 门店管理者 | 单店全功能管理、数据查看、营销活动管理 |
| 运营管理员 | 平台运营人员 | 营销活动配置、数据分析、AI运营建议查看 |
| 财务专员 | 财务人员 | 账单管理、财务报表 |
| 超级管理员 | 平台最高权限 | 全平台管理、系统配置 |
### 2.3 业务范围
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 付费订阅版业务范围 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 基础功能(包含基础版所有功能) │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 会员管理 • 预约管理 • 签到管理 • 数据统计 • 系统管理 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 订阅与配置管理 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 订阅管理 • 配置管理 • 套餐管理 • 计费管理 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 业务扩展类模块 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 私教管理 • 场地预约 • 线上课程 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 体验升级类模块 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 人脸识别签到 • NFC签到 • 智能储物柜 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 营销增长类模块 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 营销活动 • 会员推荐奖励 • 会员互动社区 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 数据智能类模块 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 高级数据分析 • 智能报表 • AI运营建议 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 营销分析与预测模块 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 营销精算模型 • 促销策略预测 • 促销活动效果预测 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
---
## 三、核心业务流程
### 3.1 订阅流程
#### 3.1.1 业务场景
租户管理员通过管理后台订阅增值模块。
#### 3.1.2 业务流程
```
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ 租户管理 │ → │ 查看订阅 │ → │ 选择订阅 │ → │ 确认订阅 │ → │ 模块立即 │
│ 员登录 │ │ 套餐 │ │ 模块 │ │ │ │ 启用 │
└─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘
```
#### 3.1.3 业务规则
- 订阅成功后模块立即启用
- 年付享受最大折扣
- 支持多种支付方式
- 订阅成功后发送通知
#### 3.1.4 异常处理
| 异常场景 | 处理方式 |
|---------|---------|
| 支付失败 | 提示用户重新支付 |
| 支付超时 | 提示用户重新发起支付 |
---
### 3.2 配置继承流程
#### 3.2.1 业务场景
门店管理员配置门店级参数,可以选择继承租户级配置。
#### 3.2.2 业务流程
```
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ 门店管理 │ → │ 查看租户 │ → │ 选择继承 │ → │ 配置门店 │ → │ 配置立即 │
│ 员登录 │ │ 级配置 │ │ 模式 │ │ 级参数 │ │ 生效 │
└─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘
```
#### 3.2.3 业务规则
- 查询优先级:门店配置 → 租户配置 → 默认配置
- 支持三种继承模式(继承/继承+覆盖/自定义)
- 配置变更后立即生效
- 配置变更记录版本,支持回滚
#### 3.2.4 异常处理
| 异常场景 | 处理方式 |
|---------|---------|
| 配置冲突 | 提示用户选择覆盖或合并 |
| 配置无效 | 提示用户重新配置 |
---
### 3.3 私教预约流程
#### 3.3.1 业务场景
会员通过小程序预约私教课程。
#### 3.3.2 业务流程
```
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ 会员打开 │ → │ 查看私教 │ → │ 选择私教 │ → │ 确认预约 │ → │ 预约成功 │
│ 小程序 │ │ 课程列表 │ │ 课程 │ │ │ │ │
└─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘
```
#### 3.3.3 业务规则
- 私教预约需提前至少24小时
- 私教取消需提前至少12小时
- 私教签到后记录考勤
#### 3.3.4 异常处理
| 异常场景 | 处理方式 |
|---------|---------|
| 教练时间冲突 | 提示用户选择其他时间 |
| 会员卡权益不足 | 提示用户购买会员卡 |
---
### 3.4 营销活动创建流程
#### 3.4.1 业务场景
运营管理员通过管理后台创建营销活动。
#### 3.4.2 业务流程
```
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ 运营管理 │ → │ 创建营销 │ → │ 配置活动 │ → │ 发布活动 │ → │ 活动生效 │
│ 员登录 │ │ 活动 │ │ 规则 │ │ │ │ │
└─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘
```
#### 3.4.3 业务规则
- 营销活动需指定时间、规则、奖励
- 营销活动发布后不可修改规则
- 营销活动统计按活动、时间维度
#### 3.4.4 异常处理
| 异常场景 | 处理方式 |
|---------|---------|
| 活动时间冲突 | 提示用户调整活动时间 |
| 活动规则无效 | 提示用户重新配置 |
---
### 3.5 营销分析与预测流程
#### 3.5.1 业务场景
运营管理员使用营销精算模型预测促销策略。
#### 3.5.2 业务流程
```
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ 运营管理 │ → │ 选择营销 │ → │ 配置促销 │ → │ 预测效果 │ → │ 查看预测 │
│ 员登录 │ │ 精算模型 │ │ 参数 │ │ │ │ 结果 │
└─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘
```
#### 3.5.3 业务规则
- 营销精算模型基于历史数据
- 促销策略预测提供多种方案
- 促销活动效果预测基于历史数据
#### 3.5.4 异常处理
| 异常场景 | 处理方式 |
|---------|---------|
| 历史数据不足 | 提示用户积累更多数据 |
| 预测失败 | 提示用户调整参数 |
---
## 四、核心业务规则
### 4.1 订阅管理规则
| 规则 | 描述 |
|------|------|
| 订阅生效 | 订阅成功后模块立即启用 |
| 计费周期 | 支持月付、季付、半年付、年付 |
| 试用政策 | 不同模块类型提供不同试用时长 |
| 组合套餐 | 支持组合套餐,享受更多优惠 |
### 4.2 配置管理规则
| 规则 | 描述 |
|------|------|
| 配置继承 | 支持门店配置继承租户配置 |
| 继承模式 | 支持继承、继承+覆盖、自定义三种模式 |
| 配置优先级 | 门店配置 → 租户配置 → 默认配置 |
| 配置版本 | 配置变更记录版本,支持回滚 |
### 4.3 私教管理规则
| 规则 | 描述 |
|------|------|
| 私教预约时间 | 私教预约需提前至少24小时 |
| 私教取消时间 | 私教取消需提前至少12小时 |
| 私教考勤 | 私教签到后记录考勤 |
### 4.4 营销活动规则
| 规则 | 描述 |
|------|------|
| 活动规则 | 营销活动需指定时间、规则、奖励 |
| 活动修改 | 营销活动发布后不可修改规则 |
| 活动统计 | 营销活动统计按活动、时间维度 |
### 4.5 营销分析与预测规则
| 规则 | 描述 |
|------|------|
| 模型基础 | 营销精算模型基于历史数据 |
| 预测方案 | 促销策略预测提供多种方案 |
| 效果预测 | 促销活动效果预测基于历史数据 |
---
## 五、业务场景
### 5.1 租户订阅场景
**场景描述**
租户A是一家连锁健身房品牌,想启用私教管理和营销活动模块,租户管理员登录管理后台,查看订阅套餐,选择私教管理模块和营销活动模块,选择年付方式,查看优惠信息,确认订阅,支付成功,模块立即启用,租户开始使用新功能。
**业务流程**
1. 租户管理员登录管理后台
2. 查看订阅套餐
3. 选择订阅模块
4. 选择计费方式
5. 查看优惠信息
6. 确认订阅
7. 支付成功
8. 模块立即启用
9. 开始使用新功能
**涉及的业务规则**
- 订阅成功后模块立即启用,无需重启
- 年付享受最大折扣
- 支持多种支付方式
- 订阅成功后发送通知
---
### 5.2 门店配置继承场景
**场景描述**
租户A配置了团课、私教、营销模块,门店1想完全继承租户配置,门店2想在租户配置基础上覆盖签到方式(增加人脸识别),门店3想完全自定义配置。各门店管理员登录管理后台,选择继承模式,配置门店级参数,保存配置,配置立即生效。
**业务流程**
1. 门店管理员登录管理后台
2. 查看租户级配置
3. 选择继承模式(继承/继承+覆盖/自定义)
4. 配置门店级参数
5. 保存配置
6. 配置立即生效
7. 验证配置生效
**涉及的业务规则**
- 查询优先级:门店配置 → 租户配置 → 默认配置
- 支持三种继承模式
- 配置变更后立即生效
- 配置变更记录版本,支持回滚
---
### 5.3 私教预约场景
**场景描述**
会员张三想预约私教课程,通过小程序查看私教课程列表,选择教练李四,选择时间,确认预约,预约成功,接收提醒。
**业务流程**
1. 张三打开小程序
2. 查看私教课程列表
3. 选择教练李四
4. 选择时间
5. 确认预约
6. 预约成功
7. 接收提醒
**涉及的业务规则**
- 私教预约需提前至少24小时
- 私教取消需提前至少12小时
- 私教签到后记录考勤
---
### 5.4 营销活动创建场景
**场景描述**
运营管理员王五想创建一个新会员注册送月卡的活动,登录管理后台,创建营销活动,配置活动规则(新会员注册送月卡),配置活动奖励(月卡一张),发布活动,活动生效,开始监控活动效果。
**业务流程**
1. 王五登录管理后台
2. 创建营销活动
3. 配置活动规则(新会员注册送月卡)
4. 配置活动奖励(月卡一张)
5. 发布活动
6. 活动生效
7. 开始监控活动效果
**涉及的业务规则**
- 营销活动需指定时间、规则、奖励
- 营销活动发布后不可修改规则
- 营销活动统计按活动、时间维度
---
### 5.5 营销分析与预测场景
**场景描述**
运营管理员赵六想预测一个新会员注册送月卡活动的效果,登录管理后台,选择营销精算模型,配置促销参数(活动时间、目标人群、奖励金额),预测活动效果,查看预测结果(预计新增会员数、预计成本、预计收益)。
**业务流程**
1. 赵六登录管理后台
2. 选择营销精算模型
3. 配置促销参数(活动时间、目标人群、奖励金额)
4. 预测活动效果
5. 查看预测结果(预计新增会员数、预计成本、预计收益)
**涉及的业务规则**
- 营销精算模型基于历史数据
- 促销策略预测提供多种方案
- 促销活动效果预测基于历史数据
---
## 六、数据模型
### 6.1 核心实体
| 实体 | 描述 |
|------|------|
| 租户(Tenant) | 系统的多租户架构中的独立业务实体 |
| 门店(Store) | 租户下的具体经营场所 |
| 会员(Member) | 在门店注册的用户 |
| 会员卡(MemberCard) | 会员购买的权益卡 |
| 权益(Benefit) | 会员卡包含的权益 |
| 团课(GroupClass) | 集体课程 |
| 私教课程(PrivateClass) | 私教课程 |
| 预约(Booking) | 会员预约记录 |
| 签到(CheckIn) | 会员签到记录 |
| 订阅(Subscription) | 租户订阅记录 |
| 配置(Config) | 租户或门店配置 |
| 营销活动(MarketingActivity) | 营销活动 |
| 营销预测(MarketingPrediction) | 营销预测结果 |
### 6.2 实体关系
```
租户(Tenant) ──1:N── 门店(Store)
租户(Tenant) ──1:N── 订阅(Subscription)
租户(Tenant) ──1:N── 配置(Config)
门店(Store) ──1:N── 会员(Member)
门店(Store) ──1:N── 配置(Config)
会员(Member) ──1:N── 会员卡(MemberCard)
会员(Member) ──1:N── 预约(Booking)
会员(Member) ──1:N── 签到(CheckIn)
会员卡(MemberCard) ──1:N── 权益(Benefit)
团课(GroupClass) ──1:N── 预约(Booking)
私教课程(PrivateClass) ──1:N── 预约(Booking)
营销活动(MarketingActivity) ──1:N── 营销预测(MarketingPrediction)
```
---
## 七、技术约束
### 7.1 性能约束
| 指标 | 要求 |
|------|------|
| API响应时间 | ≤ 500ms |
| 并发用户 | 支持500并发用户 |
| 数据库查询 | 查询响应时间 ≤ 1s |
### 7.2 可用性约束
| 指标 | 要求 |
|------|------|
| 系统可用性 | SLA ≥ 99.9% |
| 故障恢复时间 | MTTR ≤ 30分钟 |
### 7.3 安全性约束
| 指标 | 要求 |
|------|------|
| 数据加密 | 敏感数据加密存储 |
| 访问控制 | 基于角色的访问控制 |
| 操作审计 | 关键操作记录审计日志 |
| 支付安全 | 支持安全支付通道 |
### 7.4 可扩展性约束
| 指标 | 要求 |
|------|------|
| 会员数量 | 不限制 |
| 门店数量 | 支持多门店 |
| 团课容量 | 不限制 |
| 数据保留 | 永久保存 |
---
## 八、附录
### 8.1 术语定义
| 术语 | 定义 |
|------|------|
| 订阅模块 | 按需订阅的增值功能模块 |
| 私教管理 | 私教课程管理、私教预约、私教签到等功能 |
| 营销活动 | 吸引新会员和提升会员活跃度的活动 |
| 营销精算模型 | 基于历史数据预测促销策略的模型 |
| 促销活动效果预测 | 基于历史数据预测促销活动效果 |
| 配置继承 | 门店配置继承租户配置的机制 |
### 8.2 参考文档
- 《健身房管理系统付费订阅版产品设计文档》 GYM-PRD-SUBSCRIPTION-001
- 《健身房管理系统业务概要设计文档》 GYM-HLD-001
- 《订阅与配置模块详细设计文档》 GYM-LLD-004
@@ -0,0 +1,474 @@
# 健身房管理系统基础版业务概要设计文档(HLD)
> 文档编号: GYM-HLD-BASIC-001
> 版本: v1.0
> 日期: 2026-03-04
> 作者: 张翔
> 状态: 初稿
---
## 文档修订历史
| 版本 | 日期 | 作者 | 修订内容 |
| ---- | ---------- | ---- | ------------------ |
| v1.0 | 2026-03-04 | 张翔 | 创建基础版业务概要设计 |
---
## 一、引言
### 1.1 编写目的
本文档为健身房管理系统基础版的业务概要设计文档(High-Level Design),旨在:
1. 从业务层面描述基础版的业务范围、业务流程、业务规则
2. 为基础版详细设计提供业务指导和约束
3. 作为产品经理、业务分析师、开发人员的业务参考
### 1.2 项目背景
健身房管理系统基础版是面向小型工作室、个人教练等场景的核心版本,保证业务闭环,提供完整的会员管理、预约、签到等核心功能。
### 1.3 术语定义
| 术语 | 定义 |
| ----------------------------- | ------------------------------------------------ |
| 租户(Tenant) | 系统的多租户架构中的独立业务实体,如一个连锁品牌 |
| 门店(Store) | 租户下的具体经营场所 |
| 会员(Member) | 在门店注册的用户 |
| 权益(Benefit) | 会员卡包含的时长、次数、储值、等级等权益 |
| 可预约资源(Bookable Resource) | 团课等可被预约的对象 |
| 时段(Slot) | 资源的可预约时间窗口 |
### 1.4 参考文档
- 《健身房管理系统基础版产品设计文档》 GYM-PRD-BASIC-001
- 《健身房管理系统业务概要设计文档》 GYM-HLD-001
---
## 二、业务概述
### 2.1 业务目标
| 目标维度 | 目标描述 | 成功指标 |
| -------- | ---------------------- | -------------------------------- |
| 用户体验 | 提升会员预约和签到体验 | 预约成功率 ≥ 95%,签到耗时 ≤ 3秒 |
| 运营效率 | 降低人工操作成本 | 人工处理时间减少 50% |
| 数据价值 | 提供基础数据支持 | 数据报表使用率 ≥ 80% |
### 2.2 用户角色
| 角色 | 描述 | 主要功能 |
| ---------- | -------------- | ---------------------------- |
| 会员 | 健身房注册用户 | 预约课程、签到、查看个人信息 |
| 教练 | 健身房教练 | 排课、团课签到管理 |
| 前台 | 门店前台人员 | 会员接待、签到辅助、会员管理 |
| 店长 | 门店管理者 | 单店全功能管理、数据查看 |
| 超级管理员 | 平台最高权限 | 全平台管理、系统配置 |
### 2.3 业务范围
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 基础版业务范围 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 会员管理 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 会员注册 • 会员卡管理 • 权益管理 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 预约管理 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 团课预约 • 团课管理 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 签到管理 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 扫码签到 • 签到记录管理 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 数据统计 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 基础数据统计 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 系统管理 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 用户管理 • 角色权限管理 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
---
## 三、核心业务流程
### 3.1 会员注册流程
#### 3.1.1 业务场景
新用户通过小程序或前台进行注册,成为健身房会员。
#### 3.1.2 业务流程
```
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ 用户打开 │ → │ 填写手机 │ → │ 验证手机 │ → │ 填写基本 │ → │ 注册成功 │
│ 小程序 │ │ 号 │ │ 号 │ │ 信息 │ │ │
└─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘
```
#### 3.1.3 业务规则
- 手机号需验证唯一性
- 手机号需通过短信验证码验证
- 支持微信授权快速注册
- 注册成功后自动创建会员档案
#### 3.1.4 异常处理
| 异常场景 | 处理方式 |
|---------|---------|
| 手机号已存在 | 提示用户直接登录 |
| 验证码错误 | 提示用户重新输入 |
| 验证码过期 | 提示用户重新获取 |
---
### 3.2 团课预约流程
#### 3.2.1 业务场景
会员通过小程序预约团课,教练通过管理后台创建团课。
#### 3.2.2 业务流程
**会员预约团课**
```
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ 会员打开 │ → │ 查看团课 │ → │ 选择团课 │ → │ 确认预约 │ → │ 预约成功 │
│ 小程序 │ │ 列表 │ │ │ │ │ │ │
└─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘
```
**教练创建团课**
```
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ 教练打开 │ → │ 点击创建 │ → │ 填写团课 │ → │ 发布团课 │ → │ 发布成功 │
│ 管理后台 │ │ 团课 │ │ 信息 │ │ │ │ │
└─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘
```
#### 3.2.3 业务规则
- 预约需在课程开始前至少30分钟
- 取消预约需在课程开始前至少2小时
- 每节课最多20人
- 预约成功后发送提醒
- 预约成功后扣减权益
- 团课需指定教练、时间、地点
- 团课取消需提前24小时通知
- 团课取消后自动退款
#### 3.2.4 异常处理
| 异常场景 | 处理方式 |
|---------|---------|
| 课程已满 | 提示用户选择其他课程 |
| 会员卡权益不足 | 提示用户购买会员卡 |
| 预约时间过短 | 提示用户提前预约 |
---
### 3.3 签到流程
#### 3.3.1 业务场景
会员到店后通过扫码进行签到,记录到店信息。
#### 3.3.2 业务流程
```
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ 会员到店 │ → │ 扫描签到 │ → │ 验证会员 │ → │ 签到成功 │ → │ 记录到店 │
│ │ │ 码 │ │ 卡 │ │ │ │ 时间 │
└─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘
```
#### 3.3.3 业务规则
- 签到需验证会员卡有效性
- 签到需验证预约信息(如有)
- 签到成功后记录到店时间
- 签到失败后提示原因
#### 3.3.4 异常处理
| 异常场景 | 处理方式 |
|---------|---------|
| 会员卡无效 | 提示用户购买会员卡 |
| 会员卡过期 | 提示用户续费 |
| 签到码无效 | 提示用户重新扫描 |
---
### 3.4 会员卡购买流程
#### 3.4.1 业务场景
会员通过小程序购买会员卡,获得相应权益。
#### 3.4.2 业务流程
```
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ 会员打开 │ → │ 查看会员 │ → │ 选择会员 │ → │ 确认购买 │ → │ 购买成功 │
│ 小程序 │ │ 卡列表 │ │ 卡 │ │ │ │ │
└─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘
```
#### 3.4.3 业务规则
- 支持时长卡、次卡、储值卡
- 会员卡到期前7天提醒
- 会员卡续费后权益立即生效
- 会员卡使用记录永久保存
#### 3.4.4 异常处理
| 异常场景 | 处理方式 |
|---------|---------|
| 支付失败 | 提示用户重新支付 |
| 支付超时 | 提示用户重新发起支付 |
---
## 四、核心业务规则
### 4.1 会员管理规则
| 规则 | 描述 |
|------|------|
| 会员唯一性 | 手机号作为会员唯一标识 |
| 会员信息完整性 | 必填字段:手机号、姓名、性别 |
| 会员信息修改权限 | 会员只能编辑自己的基本信息,前台和店长可以编辑所有信息 |
### 4.2 会员卡管理规则
| 规则 | 描述 |
|------|------|
| 会员卡类型 | 支持时长卡、次卡、储值卡 |
| 会员卡有效期 | 时长卡有有效期,次卡和储值卡无有效期 |
| 会员卡到期提醒 | 到期前7天提醒 |
| 会员卡续费 | 续费后权益立即生效 |
### 4.3 预约管理规则
| 规则 | 描述 |
|------|------|
| 预约时间限制 | 预约需在课程开始前至少30分钟 |
| 取消预约时间限制 | 取消预约需在课程开始前至少2小时 |
| 团课容量限制 | 每节课最多20人 |
| 预约权益扣减 | 预约成功后扣减权益 |
### 4.4 签到管理规则
| 规则 | 描述 |
|------|------|
| 签到验证 | 签到需验证会员卡有效性 |
| 签到预约验证 | 签到需验证预约信息(如有) |
| 签到记录 | 签到成功后记录到店时间 |
### 4.5 数据统计规则
| 规则 | 描述 |
|------|------|
| 数据保留期限 | 数据保留30天 |
| 统计维度 | 支持按日、周、月统计 |
| 数据导出 | 支持数据导出 |
---
## 五、业务场景
### 5.1 会员注册场景
**场景描述**
新用户张三通过小程序注册成为健身房会员。
**业务流程**
1. 张三打开小程序
2. 点击注册
3. 填写手机号
4. 验证手机号
5. 填写基本信息(姓名、性别、生日、身高体重、健身目标)
6. 注册成功
7. 自动创建会员档案
**涉及的业务规则**
- 手机号需验证唯一性
- 手机号需通过短信验证码验证
- 注册成功后自动创建会员档案
---
### 5.2 团课预约场景
**场景描述**
会员李四通过小程序预约团课。
**业务流程**
1. 李四打开小程序
2. 查看团课列表
3. 选择团课
4. 查看详情
5. 确认预约
6. 预约成功
7. 接收提醒
**涉及的业务规则**
- 预约需在课程开始前至少30分钟
- 取消预约需在课程开始前至少2小时
- 每节课最多20人
- 预约成功后发送提醒
- 预约成功后扣减权益
---
### 5.3 签到场景
**场景描述**
会员王五到店后通过扫码进行签到。
**业务流程**
1. 王五到店
2. 扫描签到码
3. 验证会员卡
4. 签到成功
5. 记录到店时间
**涉及的业务规则**
- 签到需验证会员卡有效性
- 签到需验证预约信息(如有)
- 签到成功后记录到店时间
- 签到失败后提示原因
---
### 5.4 会员卡购买场景
**场景描述**
会员赵六通过小程序购买会员卡。
**业务流程**
1. 赵六打开小程序
2. 查看会员卡列表
3. 选择会员卡
4. 确认购买
5. 购买成功
**涉及的业务规则**
- 支持时长卡、次卡、储值卡
- 会员卡到期前7天提醒
- 会员卡续费后权益立即生效
---
## 六、数据模型
### 6.1 核心实体
| 实体 | 描述 |
|------|------|
| 会员(Member) | 健身房注册用户 |
| 会员卡(MemberCard) | 会员购买的权益卡 |
| 权益(Benefit) | 会员卡包含的权益 |
| 团课(GroupClass) | 集体课程 |
| 预约(Booking) | 会员预约记录 |
| 签到(CheckIn) | 会员签到记录 |
### 6.2 实体关系
```
会员(Member) ──1:N── 会员卡(MemberCard)
会员(Member) ──1:N── 预约(Booking)
会员(Member) ──1:N── 签到(CheckIn)
会员卡(MemberCard) ──1:N── 权益(Benefit)
团课(GroupClass) ──1:N── 预约(Booking)
```
---
## 七、技术约束
### 7.1 性能约束
| 指标 | 要求 |
|------|------|
| API响应时间 (P99) | 200-400ms |
| 并发用户 | 支持1000并发用户 |
| 吞吐量 (QPS) | 3000-5000 |
| 数据库查询 | 查询响应时间 ≤ 500ms |
### 7.2 可用性约束
| 指标 | 要求 |
|------|------|
| 系统可用性 | SLA ≥ 99.9% |
| 故障恢复时间 | MTTR ≤ 30分钟 |
### 7.3 安全性约束
| 指标 | 要求 |
|------|------|
| 数据加密 | 敏感数据加密存储 |
| 访问控制 | 基于角色的访问控制 |
| 操作审计 | 关键操作记录审计日志 |
### 7.4 可扩展性约束
| 指标 | 要求 |
|------|------|
| 会员数量 | 最多500人 |
| 门店数量 | 单门店 |
| 团课容量 | 每节课最多20人 |
| 数据保留 | 保留30天 |
---
## 八、附录
### 8.1 术语定义
| 术语 | 定义 |
|------|------|
| 会员 | 在健身房注册的用户 |
| 会员卡 | 会员购买的权益卡,包括时长卡、次卡、储值卡 |
| 权益 | 会员卡包含的时长、次数、储值、等级等权益 |
| 团课 | 集体课程,由教练带领多个会员一起上课 |
| 预约 | 会员预约团课 |
| 签到 | 会员到店记录 |
### 8.2 参考文档
- 《健身房管理系统基础版产品设计文档》 GYM-PRD-BASIC-001
- 《健身房管理系统业务概要设计文档》 GYM-HLD-001
File diff suppressed because it is too large Load Diff
-681
View File
@@ -1,681 +0,0 @@
# 健身房管理系统业务概要设计文档(HLD)
> 文档编号: GYM-HLD-001
> 版本: v1.0
> 日期: 2026-03-04
> 作者: 张翔
> 状态: 初稿
---
## 文档修订历史
| 版本 | 日期 | 作者 | 修订内容 |
| ---- | ---------- | ---- | ------------------ |
| v1.0 | 2026-03-04 | 张翔 | 重构为业务概要设计 |
---
## 一、引言
### 1.1 编写目的
本文档为健身房管理系统的业务概要设计文档(High-Level Design),旨在:
1. 从业务层面描述系统的业务范围、业务流程、业务规则
2. 为详细设计提供业务指导和约束
3. 作为产品经理、业务分析师、开发人员的业务参考
### 1.2 项目背景
健身房管理系统是一款面向综合型健身俱乐部、精品工作室、连锁品牌的全场景管理平台,核心解决:
- 会员端:一站式查看个人所有信息,便捷预约签到
- 管理后台:全维度数据整理与分析,支撑运营决策
- 多业态支持:灵活适配不同规模和类型的健身场所
### 1.3 术语定义
| 术语 | 定义 |
| ----------------------------- | ------------------------------------------------ |
| 租户(Tenant) | 系统的多租户架构中的独立业务实体,如一个连锁品牌 |
| 门店(Store) | 租户下的具体经营场所 |
| 会员(Member) | 在门店注册的用户 |
| 权益(Benefit) | 会员卡包含的时长、次数、储值、等级等权益 |
| 可预约资源(Bookable Resource) | 团课、私教、场地、线上课程等可被预约的对象 |
| 时段(Slot) | 资源的可预约时间窗口 |
### 1.4 参考文档
- 《健身房管理系统产品设计文档》 GYM-PRD-001
---
## 二、业务概述
### 2.1 业务目标
| 目标维度 | 目标描述 | 成功指标 |
| -------- | ---------------------- | -------------------------------- |
| 用户体验 | 提升会员预约和签到体验 | 预约成功率 ≥ 95%,签到耗时 ≤ 3秒 |
| 运营效率 | 降低人工操作成本 | 人工处理时间减少 50% |
| 数据价值 | 提供数据驱动决策支持 | 数据报表使用率 ≥ 80% |
| 业务扩展 | 支持多业态灵活适配 | 支持至少3种业态场景 |
### 2.2 用户角色
| 角色 | 描述 | 主要功能 |
| ---------- | -------------- | ---------------------------- |
| 会员 | 健身房注册用户 | 预约课程、签到、查看个人信息 |
| 教练 | 健身房教练 | 排课、私教预约确认、学员签到 |
| 前台 | 门店前台人员 | 会员接待、签到辅助、会员管理 |
| 店长 | 门店管理者 | 单店全功能管理、数据查看 |
| 运营管理员 | 平台运营人员 | 营销活动配置、数据分析 |
| 财务专员 | 财务人员 | 账单管理、财务报表 |
| 超级管理员 | 平台最高权限 | 全平台管理、系统配置 |
### 2.3 业务范围
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 业务范围 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 会员管理 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 会员注册 • 会员卡管理 • 权益管理 • 等级管理 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 预约管理 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 团课预约 • 私教预约 • 场地预约 • 线上课程 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 签到管理 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 扫码签到 • 刷脸签到 • NFC签到 • 教练代签 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 课程管理 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 课程类型 • 课程排期 • 场地管理 • 价格配置 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 教练管理 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 教练信息 • 排班管理 • 课时统计 • 评价管理 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 财务管理 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 营收统计 • 账单管理 • 退款管理 • 对账管理 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 计划中心 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 训练计划 • 课程排期 • 会员目标 • 教练排班 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 数据分析 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 会员分析 • 课程分析 • 财务分析 • 运营分析 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 系统管理 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 租户管理 • 门店管理 • 权限管理 • 系统配置 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
---
## 三、核心业务流程
### 3.1 会员注册与入会流程
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 会员注册与入会流程 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 新用户 │────▶│ 手机号 │────▶│ 验证码 │────▶│ 注册 │ │
│ │ 访问 │ │ 输入 │ │ 验证 │ │ 成功 │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ 信息 │ │
│ │ 完善 │ │
│ └──────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ 购买 │ │
│ │ 会员卡 │ │
│ └──────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ 入会 │ │
│ │ 完成 │ │
│ └──────────┘ │
│ │
│ 业务规则: │
│ • 手机号必须唯一,一个手机号只能注册一个会员 │
│ • 验证码有效期60秒,同一手机号60秒内只能发送一次 │
│ • 会员信息完善后才能购买会员卡 │
│ • 会员卡购买成功后立即生效,权益即时可用 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
### 3.2 课程预约流程
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 课程预约流程 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 会员 │────▶│ 浏览 │────▶│ 选择 │────▶│ 确认 │ │
│ │ 登录 │ │ 课程 │ │ 时段 │ │ 预约 │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ 权益 │ │
│ │ 检查 │ │
│ └──────────┘ │
│ │ │
│ ┌────────────┴────────┐ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │ 预约 │ │ 提示 │ │
│ │ 成功 │ │ 失败 │ │
│ └──────────┘ └──────────┘ │
│ │
│ 业务规则: │
│ • 会员必须拥有足够的权益才能预约(次数、时长、储值等) │
│ • 同一时段只能预约一个课程,预约冲突时提示用户 │
│ • 预约成功后发送通知(微信、短信) │
│ • 预约取消时间限制:开课前2小时内不能取消 │
│ • 热门课程支持候补机制,满员后自动进入候补队列 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
### 3.3 签到流程
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 签到流程 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 会员 │────▶│ 到达 │────▶│ 选择 │────▶│ 验证 │ │
│ │ 到达 │ │ 门店 │ │ 签到方式│ │ 身份 │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │ │
│ ┌────────────┴────────┐ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │ 扫码 │ │ 刷脸 │ │
│ │ 签到 │ │ 签到 │ │
│ └──────────┘ └──────────┘ │
│ │ │ │
│ └──────────┬──────────┘ │
│ ▼ │
│ ┌──────────┐ │
│ │ 预约 │ │
│ │ 检查 │ │
│ └──────────┘ │
│ │ │
│ ┌────────────┴────────┐ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │ 签到 │ │ 手动 │ │
│ │ 成功 │ │ 处理 │ │
│ └──────────┘ └──────────┘ │
│ │
│ 业务规则: │
│ • 签到时验证会员身份和预约信息 │
│ • 有预约的会员优先签到,自动扣减权益 │
│ • 无预约的会员可以临时签到,需前台确认 │
│ • 签到成功后记录签到时间、设备信息 │
│ • 支持教练代签,教练可以确认学员签到 │
│ • 签到失败时提供明确的错误提示(如:预约不存在、权益不足) │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
### 3.4 会员卡购买与激活流程
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 会员卡购买与激活流程 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 会员 │────▶│ 浏览 │────▶│ 选择 │────▶│ 支付 │ │
│ │ 登录 │ │ 会员卡 │ │ 卡类型 │ │ 订单 │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ 支付 │ │
│ │ 成功 │ │
│ └──────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ 会员卡 │ │
│ │ 激活 │ │
│ └──────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ 权益 │ │
│ │ 到账 │ │
│ └──────────┘ │
│ │
│ 业务规则: │
│ • 会员卡类型包括:时长卡、次卡、储值卡、等级卡 │
│ • 支付成功后会员卡立即激活,权益即时到账 │
│ • 会员卡有效期从激活日开始计算 │
│ • 支持会员卡转让功能(可选,需店长审批) │
│ • 会员卡到期前7天发送提醒通知 │
│ • 支持会员卡续费,续费后权益累加 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
---
## 四、业务规则
### 4.1 会员管理规则
#### 4.1.1 会员注册规则
- 手机号必须唯一,一个手机号只能注册一个会员
- 验证码有效期60秒,同一手机号60秒内只能发送一次
- 会员信息完善后才能购买会员卡
- 支持微信一键登录,自动关联手机号
#### 4.1.2 会员卡规则
- 会员卡类型:时长卡、次卡、储值卡、等级卡
- 会员卡支付成功后立即激活,权益即时到账
- 会员卡有效期从激活日开始计算
- 支持会员卡续费,续费后权益累加
- 会员卡到期前7天发送提醒通知
- 支持会员卡转让功能(可选,需店长审批)
#### 4.1.3 权益管理规则
- 权益类型:时长、次数、储值、等级
- 权益使用时优先级:储值 > 次数 > 时长 > 等级
- 权益扣减时先检查余额,余额不足时提示用户
- 权益使用记录永久保存,支持查询
- 权益到期后自动失效,不可使用
### 4.2 预约管理规则
#### 4.2.1 预约规则
- 会员必须拥有足够的权益才能预约(次数、时长、储值等)
- 同一时段只能预约一个课程,预约冲突时提示用户
- 预约成功后发送通知(微信、短信)
- 预约取消时间限制:开课前2小时内不能取消
- 热门课程支持候补机制,满员后自动进入候补队列
- 候补队列按预约时间排序,有人取消时自动补位
#### 4.2.2 课程排期规则
- 课程排期需提前至少24小时发布
- 课程排期修改需通知已预约会员
- 课程取消需提前2小时通知已预约会员
- 课程满员后自动开启候补
- 教练请假需提前24小时通知,系统自动调整排期
### 4.3 签到管理规则
#### 4.3.1 签到规则
- 签到时验证会员身份和预约信息
- 有预约的会员优先签到,自动扣减权益
- 无预约的会员可以临时签到,需前台确认
- 签到成功后记录签到时间、设备信息
- 支持教练代签,教练可以确认学员签到
- 签到失败时提供明确的错误提示(如:预约不存在、权益不足)
#### 4.3.2 签到时间规则
- 团课签到时间:开课前30分钟至开课后10分钟
- 私教签到时间:预约时间前后15分钟内
- 临时签到时间:门店营业时间内
- 迟到超过10分钟视为缺勤,不扣减权益
### 4.4 财务管理规则
#### 4.4.1 支付规则
- 支持多种支付方式:微信支付、支付宝、银行卡
- 支付成功后立即到账,实时更新财务数据
- 支持退款,退款需店长审批
- 退款原路返回,到账时间取决于支付渠道
- 支持对账功能,每日自动对账
#### 4.4.2 账单规则
- 账单实时生成,支持查询和导出
- 账单包含:订单号、金额、支付方式、时间、状态
- 账单状态:待支付、已支付、已退款、已取消
- 支持按时间、门店、会员、类型筛选账单
- 账单数据永久保存,支持审计
### 4.5 数据分析规则
#### 4.5.1 数据统计规则
- 数据实时统计,支持实时查询
- 数据按天、周、月、季度、年度汇总
- 支持多维度数据分析:会员、课程、财务、运营
- 数据报表支持导出:Excel、PDF
- 数据可视化:图表、趋势图、排行榜
#### 4.5.2 数据权限规则
- 超级管理员:查看全平台数据
- 运营管理员:查看负责区域数据
- 店长:查看本店数据
- 财务专员:查看财务数据
- 其他角色:按权限查看对应数据
---
## 五、业务场景
### 5.1 典型业务场景
#### 5.1.1 会员预约团课场景
**场景描述**
会员小李想预约明天晚上7点的瑜伽课程,他打开会员小程序,浏览课程列表,找到瑜伽课程,查看课程详情,确认教练、场地、时间,检查自己的会员权益(次卡剩余5次),确认可以预约,点击预约按钮,系统验证权益余额,预约成功,收到微信通知。
**业务流程**
1. 会员登录小程序
2. 浏览课程列表
3. 选择瑜伽课程
4. 查看课程详情
5. 检查会员权益
6. 确认预约
7. 系统验证权益
8. 预约成功
9. 发送通知
**涉及的业务规则**
- 会员必须拥有足够的权益才能预约
- 同一时段只能预约一个课程
- 预约成功后发送通知
#### 5.1.2 会员签到场景
**场景描述**
会员小李到达健身房,打开会员小程序,点击签到按钮,选择刷脸签到,系统识别人脸,验证身份,检查预约信息,确认有预约,签到成功,自动扣减权益(次卡剩余4次),记录签到时间和设备信息。
**业务流程**
1. 会员到达健身房
2. 打开会员小程序
3. 点击签到按钮
4. 选择刷脸签到
5. 系统识别人脸
6. 验证身份
7. 检查预约信息
8. 签到成功
9. 扣减权益
10. 记录签到信息
**涉及的业务规则**
- 签到时验证会员身份和预约信息
- 有预约的会员优先签到,自动扣减权益
- 签到成功后记录签到时间、设备信息
#### 5.1.3 教练排课场景
**场景描述**
教练王老师想安排下周的私教课程,他打开教练端App,查看自己的排班表,选择下周三下午2点到3点的时间段,选择私教课程,填写课程名称、课程描述,选择场地,设置价格,发布课程,系统自动生成预约时段,会员可以开始预约。
**业务流程**
1. 教练登录教练端App
2. 查看排班表
3. 选择时间段
4. 选择课程类型
5. 填写课程信息
6. 选择场地
7. 设置价格
8. 发布课程
9. 系统生成预约时段
10. 会员可以预约
**涉及的业务规则**
- 课程排期需提前至少24小时发布
- 课程排期修改需通知已预约会员
- 课程满员后自动开启候补
#### 5.1.4 店长查看数据场景
**场景描述**
店长张经理想查看今天的运营数据,他打开管理后台,点击数据看板,查看今日概览(会员数、预约数、签到数、营收),查看趋势数据(近7天预约趋势、近30天营收趋势),查看排行数据(热门课程排行、活跃会员排行),导出数据报表。
**业务流程**
1. 店长登录管理后台
2. 点击数据看板
3. 查看今日概览
4. 查看趋势数据
5. 查看排行数据
6. 导出数据报表
**涉及的业务规则**
- 数据实时统计,支持实时查询
- 数据按天、周、月、季度、年度汇总
- 支持多维度数据分析
- 数据报表支持导出
### 5.2 特殊业务场景
#### 5.2.1 热门课程抢课场景
**场景描述**
热门课程(如普拉提)只有10个名额,但有多名会员同时预约,系统采用先到先得的原则,前10名预约成功的会员获得名额,其他会员自动进入候补队列,有会员取消预约时,候补队列中的会员自动补位。
**业务流程**
1. 多名会员同时预约热门课程
2. 系统处理预约请求
3. 前10名预约成功
4. 其他会员进入候补队列
5. 有会员取消预约
6. 候补队列中的会员自动补位
7. 发送补位通知
**涉及的业务规则**
- 同一时段只能预约一个课程
- 热门课程支持候补机制
- 候补队列按预约时间排序
- 有人取消时自动补位
#### 5.2.2 会员卡过期续费场景
**场景描述**
会员小李的会员卡即将过期,系统提前7天发送提醒通知,小李收到通知后,打开会员小程序,查看会员卡信息,点击续费按钮,选择续费时长,支付成功,会员卡续费成功,权益累加,有效期延长。
**业务流程**
1. 系统检测会员卡即将过期
2. 提前7天发送提醒通知
3. 会员收到通知
4. 打开会员小程序
5. 查看会员卡信息
6. 点击续费按钮
7. 选择续费时长
8. 支付成功
9. 会员卡续费成功
10. 权益累加,有效期延长
**涉及的业务规则**
- 会员卡到期前7天发送提醒通知
- 支持会员卡续费,续费后权益累加
- 会员卡有效期从续费成功日开始计算
#### 5.2.3 签到异常处理场景
**场景描述**
会员小李到达健身房,尝试刷脸签到,但系统无法识别人脸,小李选择扫码签到,扫描二维码,系统验证身份,但发现没有预约,前台工作人员手动处理,确认会员身份,临时签到成功。
**业务流程**
1. 会员到达健身房
2. 尝试刷脸签到
3. 系统无法识别人脸
4. 选择扫码签到
5. 扫描二维码
6. 系统验证身份
7. 发现没有预约
8. 前台手动处理
9. 确认会员身份
10. 临时签到成功
**涉及的业务规则**
- 签到时验证会员身份和预约信息
- 无预约的会员可以临时签到,需前台确认
- 签到失败时提供明确的错误提示
---
## 六、业务约束
### 6.1 数据约束
- 会员手机号必须唯一
- 会员ID全局唯一
- 预约ID全局唯一
- 签到记录ID全局唯一
- 会员卡ID全局唯一
- 订单ID全局唯一
### 6.2 时间约束
- 验证码有效期60秒
- 预约取消时间限制:开课前2小时内不能取消
- 课程排期需提前至少24小时发布
- 课程取消需提前2小时通知已预约会员
- 教练请假需提前24小时通知
- 团课签到时间:开课前30分钟至开课后10分钟
- 私教签到时间:预约时间前后15分钟内
- 会员卡到期前7天发送提醒通知
### 6.3 权益约束
- 会员必须拥有足够的权益才能预约
- 权益使用时优先级:储值 > 次数 > 时长 > 等级
- 权益扣减时先检查余额,余额不足时提示用户
- 权益到期后自动失效,不可使用
- 权益使用记录永久保存,支持查询
### 6.4 并发约束
- 同一时段只能预约一个课程
- 热门课程支持候补机制
- 候补队列按预约时间排序
- 有人取消时自动补位
- 支持高并发场景(QPS ≥ 1000)
---
## 七、业务指标
### 7.1 用户体验指标
| 指标名称 | 目标值 | 测量方法 |
| ---------- | ------ | --------------------------- |
| 预约成功率 | ≥ 95% | 预约成功次数 / 预约总次数 |
| 签到耗时 | ≤ 3秒 | 签到完成时间 - 签到开始时间 |
| 注册成功率 | ≥ 98% | 注册成功次数 / 注册总次数 |
| 支付成功率 | ≥ 99% | 支付成功次数 / 支付总次数 |
### 7.2 运营效率指标
| 指标名称 | 目标值 | 测量方法 |
| ---------------- | ------ | -------------------------------------------------------------- |
| 人工处理时间减少 | ≥ 50% | (优化前人工处理时间 - 优化后人工处理时间) / 优化前人工处理时间 |
| 预约取消率 | ≤ 10% | 预约取消次数 / 预约总次数 |
| 签到成功率 | ≥ 98% | 签到成功次数 / 签到总次数 |
| 会员活跃度 | ≥ 60% | 活跃会员数 / 总会员数 |
### 7.3 数据价值指标
| 指标名称 | 目标值 | 测量方法 |
| -------------- | ------ | ------------------------------- |
| 数据报表使用率 | ≥ 80% | 使用数据报表的用户数 / 总用户数 |
| 数据准确性 | ≥ 99% | 数据准确记录数 / 数据总记录数 |
| 数据实时性 | ≤ 1秒 | 数据更新时间 - 数据产生时间 |
### 7.4 系统性能指标
| 指标名称 | 目标值 | 测量方法 |
| ------------ | ---------- | ---------------------------- |
| 系统可用性 | ≥ 99.9% | (总时间 - 故障时间) / 总时间 |
| 响应时间 | ≤ 2秒 | 请求响应时间 |
| 并发处理能力 | ≥ 1000 QPS | 每秒处理请求数 |
---
## 八、附录
### 8.1 业务术语表
| 术语 | 定义 |
| ----------------------------- | ------------------------------------------------ |
| 租户(Tenant) | 系统的多租户架构中的独立业务实体,如一个连锁品牌 |
| 门店(Store) | 租户下的具体经营场所 |
| 会员(Member) | 在门店注册的用户 |
| 权益(Benefit) | 会员卡包含的时长、次数、储值、等级等权益 |
| 可预约资源(Bookable Resource) | 团课、私教、场地、线上课程等可被预约的对象 |
| 时段(Slot) | 资源的可预约时间窗口 |
| 预约(Booking) | 会员预订课程或场地的行为 |
| 签到(Check-in) | 会员到达健身房并记录到达时间的行为 |
| 会员卡(Member Card) | 会员购买的权益载体,包含时长、次数、储值等 |
| 候补(Waitlist) | 课程满员后,会员进入等待队列,有空位时自动补位 |
### 8.2 参考文档
- 《健身房管理系统产品设计文档》 GYM-PRD-001
- 《健身房管理系统详细设计文档》 GYM-LLD-001
---
**文档结束**
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-796
View File
@@ -1,796 +0,0 @@
# 健身房管理系统详细设计文档(LLD)
> 文档编号: GYM-LLD-000
> 版本: v1.0
> 日期: 2026-03-04
> 作者: 张翔
> 状态: 初稿
---
## 文档修订历史
| 版本 | 日期 | 作者 | 修订内容 |
| ---- | ---------- | ---- | -------- |
| v1.0 | 2026-03-04 | 张翔 | 创建系统详细设计文档 |
---
## 参考文档
- 《健身房管理系统产品设计文档》 GYM-PRD-001
- 《健身房管理系统业务概要设计文档》 GYM-HLD-001
- Spring Boot 3 官方文档
- R2DBC 规范文档
- PostgreSQL 官方文档
---
## 一、系统架构设计
### 1.1 总体架构
采用分层架构 + 微服务思想的模块化设计:
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 总体架构 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 客户端层 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 会员小程序 (uniapp+Vue3) │ │
│ │ • 教练端App (uniapp+Vue3) │ │
│ │ • 管理后台PC (Vue3+Vite) │ │
│ │ • 硬件设备 (人脸/NFC) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ API Gateway 统一网关 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 路由转发 • 认证鉴权 • 限流熔断 • 日志追踪 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 业务层 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 会员服务 (Member Service) │ │
│ │ • 预约服务 (Booking Service) │ │
│ │ • 数据服务 (Data Service) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 公共服务层 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 认证服务 • 消息服务 • 文件服务 • 缓存服务 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 基础设施层 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • PostgreSQL • R2DBC • Caffeine • Redis(可选) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 外部服务层 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 微信开放平台 • 短信服务 • 支付服务 • OSS存储 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
### 1.2 技术架构
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 技术架构 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 前端技术栈 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • uniapp (跨平台小程序) • Vue3 (前端框架) │ │
│ │ • Vite (构建工具) • TypeScript (类型安全) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 后端技术栈 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • Spring Boot 3 (应用框架) • WebFlux (响应式编程) │ │
│ │ • JDK 21+ (运行环境) • R2DBC (响应式数据库访问) │ │
│ │ • Spring Security (安全框架) • Caffeine (本地缓存) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 数据库技术栈 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • PostgreSQL 15+ (主数据库) • Redis (可选缓存) │ │
│ │ • Flyway (数据库版本管理) • R2DBC PostgreSQL Driver │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 开发工具栈 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • Maven (依赖管理) • Git (版本控制) • Docker (容器化) │ │
│ │ • IDEA (开发IDE) • Postman (接口测试) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
### 1.3 部署架构
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 部署架构 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 客户端层 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 微信小程序 • 教练端App • 管理后台PC • 硬件设备 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ CDN层 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 静态资源加速 • 图片优化 • 视频加速 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 负载均衡层 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • Nginx (反向代理) • 负载均衡策略 • SSL/TLS │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 应用服务器层 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • Spring Boot应用 (多实例部署) • Docker容器化 │ │
│ │ • 健康检查 • 自动扩缩容 • 滚动更新 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 数据库层 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • PostgreSQL主从复制 • Redis集群 (可选) • 备份策略 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 监控运维层 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 日志收集 • 性能监控 • 告警通知 • 自动化运维 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
---
## 二、模块设计
### 2.1 模块划分
```
┌─────────────────────────────────────────────────────────────────────────┐
│ gym-manage-server 父工程 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ gym-common 公共模块 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • gym-common-core (核心工具类、常量、枚举) │ │
│ │ • gym-common-redis (Redis配置可选) │ │
│ │ • gym-common-security (安全认证公共组件) │ │
│ │ • gym-common-log (日志公共组件) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ gym-api API网关模块 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • controller (HTTP接口) • dto (数据传输对象) │ │
│ │ • vo (视图对象) • config (API配置) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ gym-service 业务服务模块 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • gym-service-member (会员服务) │ │
│ │ • gym-service-booking (预约服务) │ │
│ │ • gym-service-checkin (签到服务) │ │
│ │ • gym-service-course (课程服务) │ │
│ │ • gym-service-coach (教练服务) │ │
│ │ • gym-service-finance (财务服务) │ │
│ │ • gym-service-data (数据服务) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ gym-domain 领域模型模块 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • model (领域模型) • event (领域事件) • service (领域服务) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ gym-infrastructure 基础设施模块 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • repository (数据仓储) • cache (缓存配置) │ │
│ │ • external (外部服务集成) • config (基础配置) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ gym-starter 启动模块 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • gym-admin (管理后台启动器) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
### 2.2 模块职责
| 模块 | 职责 | 依赖 |
| ------------------- | ---------------------------------- | ------------------------------ |
| gym-common-core | 提供通用工具类、常量定义、异常处理 | 无 |
| gym-common-security | 提供JWT认证、权限校验 | gym-common-core |
| gym-common-redis | 提供Redis缓存支持(可选) | gym-common-core |
| gym-common-log | 提供统一日志记录 | gym-common-core |
| gym-api | 提供HTTP接口、路由转发 | gym-service-* |
| gym-service-member | 会员管理业务逻辑 | gym-domain, gym-infrastructure |
| gym-service-booking | 预约管理业务逻辑 | gym-domain, gym-infrastructure |
| gym-service-checkin | 签到管理业务逻辑 | gym-domain, gym-infrastructure |
| gym-service-course | 课程管理业务逻辑 | gym-domain, gym-infrastructure |
| gym-service-coach | 教练管理业务逻辑 | gym-domain, gym-infrastructure |
| gym-service-finance | 财务管理业务逻辑 | gym-domain, gym-infrastructure |
| gym-service-data | 数据分析业务逻辑 | gym-domain, gym-infrastructure |
| gym-domain | 领域模型、领域事件、领域服务 | 无 |
| gym-infrastructure | 数据仓储、缓存、外部服务集成 | gym-domain |
| gym-starter | 应用启动器 | 所有业务模块 |
### 2.3 模块交互
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 模块交互 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ │
│ │ 客户端 │ │
│ └─────┬────┘ │
│ │ HTTP/HTTPS │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ gym-api (API网关) │ │
│ └─────────────┬───────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────┼─────────┐ │
│ ▼ ▼ ▼ │
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │member │ │booking │ │checkin │ │
│ │service │ │service │ │service │ │
│ └───┬────┘ └───┬────┘ └───┬────┘ │
│ │ │ │ │
│ └───────────┼───────────┘ │
│ ▼ │
│ ┌───────────────┐ │
│ │ gym-domain │ │
│ └───────┬───────┘ │
│ │ │
│ ▼ │
│ ┌───────────────┐ │
│ │ gym-infra │ │
│ └───────┬───────┘ │
│ │ │
│ ┌───────────┼───────────┐ │
│ ▼ ▼ ▼ │
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ PG │ │Redis │ │External│ │
│ └────────┘ └────────┘ └────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
---
## 三、接口设计
### 3.1 接口规范
#### 3.1.1 RESTful API设计原则
- 使用HTTP标准方法:GET(查询)、POST(创建)、PUT(更新)、DELETE(删除)
- 使用HTTP状态码表示请求结果:200(成功)、400(请求错误)、401(未认证)、403(无权限)、404(资源不存在)、500(服务器错误)
- 使用JSON格式进行数据交换
- 使用统一的响应结构
#### 3.1.2 统一响应结构
```json
{
"code": 200,
"message": "success",
"data": {},
"timestamp": 1234567890
}
```
#### 3.1.3 错误响应结构
```json
{
"code": 400,
"message": "参数错误",
"data": null,
"timestamp": 1234567890
}
```
#### 3.1.4 分页响应结构
```json
{
"code": 200,
"message": "success",
"data": {
"list": [],
"total": 100,
"page": 1,
"pageSize": 10
},
"timestamp": 1234567890
}
```
### 3.2 接口分组
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 接口分组 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 认证接口 /v1/auth │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • POST /login (登录) • POST /logout (登出) │ │
│ │ • POST /refresh (刷新Token) • POST /wechat-login (微信登录) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 会员接口 /v1/members │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • GET / (会员列表) • GET /{id} (会员详情) │ │
│ │ • POST / (创建会员) • PUT /{id} (更新会员) │ │
│ │ • GET /{id}/cards (会员卡列表) • GET /{id}/benefits (权益列表)│ │
│ │ • GET /{id}/bookings (预约记录) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 课程接口 /v1/courses │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • GET / (课程列表) • GET /{id} (课程详情) │ │
│ │ • POST / (创建课程) • PUT /{id} (更新课程) │ │
│ │ • GET /{id}/slots (可预约时段) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 预约接口 /v1/bookings │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • GET / (预约列表) • GET /{id} (预约详情) │ │
│ │ • POST / (创建预约) • POST /{id}/cancel (取消预约) │ │
│ │ • GET /my (我的预约) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 签到接口 /v1/checkins │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • GET / (签到列表) • POST /scan (扫码签到) │ │
│ │ • POST /manual (手动签到) • GET /my (我的签到) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 教练接口 /v1/coaches │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • GET / (教练列表) • GET /{id} (教练详情) │ │
│ │ • GET /{id}/schedule (教练排班) • GET /{id}/slots (可预约时段)│ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 数据看板 /v1/dashboard │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • GET /overview (今日概览) • GET /trends (趋势数据) │ │
│ │ • GET /rankings (排行数据) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
### 3.3 接口版本管理
#### 3.3.1 版本策略
- 采用URL路径版本控制:/v1/、/v2/
- 主版本号变更表示不兼容的API变更
- 次版本号变更表示向后兼容的功能新增
- 修订版本号变更表示向后兼容的问题修复
#### 3.3.2 版本兼容性
- 新版本API发布后,旧版本API至少维护6个月
- 废弃API在响应头中添加Warning字段
- 提供API版本迁移指南
---
## 四、安全设计
### 4.1 认证机制
#### 4.1.1 JWT认证
```
┌─────────────────────────────────────────────────────────────────────────┐
│ JWT认证流程 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 用户 │────▶│ 登录 │────▶│ 验证 │────▶│ 生成 │ │
│ │ 登录 │ │ 请求 │ │ 凭证 │ │ Token │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ 返回 │ │
│ │ Token │ │
│ └──────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ 后续 │ │
│ │ 请求 │ │
│ └──────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ 携带 │ │
│ │ Token │ │
│ └──────────┘ │
│ │
│ Token结构: │
│ • Header: 算法、类型 │
│ • Payload: 用户ID、角色、过期时间 │
│ • Signature: 签名 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
#### 4.1.2 Token刷新机制
- Access Token有效期:2小时
- Refresh Token有效期:7天
- Access Token过期时使用Refresh Token刷新
- Refresh Token过期时需要重新登录
### 4.2 权限控制
#### 4.2.1 RBAC权限模型
```
┌─────────────────────────────────────────────────────────────────────────┐
│ RBAC权限模型 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 用户 │──▶│ 角色 │──▶│ 权限 │──▶│ 资源 │ │
│ │ User │ │ Role │ │Permission│ │ Resource │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ 权限示例: │
│ • member:view (查看会员) │
│ • member:edit (编辑会员) │
│ • member:delete (删除会员) │
│ • booking:create (创建预约) │
│ • booking:cancel (取消预约) │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
#### 4.2.2 权限校验流程
1. 用户登录后获取角色和权限
2. 每次请求时校验权限
3. 无权限时返回403错误
4. 支持权限继承和组合
### 4.3 数据安全
#### 4.3.1 数据加密
- 敏感数据(手机号、身份证)使用AES-256加密存储
- 密码使用BCrypt加密
- 传输数据使用HTTPS加密
- 数据库连接使用SSL/TLS
#### 4.3.2 数据脱敏
- 手机号脱敏:138****5678
- 身份证脱敏:110101********1234
- 银行卡脱敏:6222************1234
#### 4.3.3 数据备份
- 每日全量备份
- 每小时增量备份
- 备份数据加密存储
- 备份数据异地容灾
### 4.4 接口安全
#### 4.4.1 防重放攻击
- 每个请求携带时间戳
- 时间戳有效期:5分钟
- 请求签名验证
#### 4.4.2 防SQL注入
- 使用参数化查询
- 使用R2DBC响应式数据库访问
- 输入参数校验和过滤
#### 4.4.3 防XSS攻击
- 输入内容过滤和转义
- 响应头设置Content-Security-Policy
- 使用白名单过滤
---
## 五、性能设计
### 5.1 性能目标
| 指标 | 目标值 | 测量方法 |
| -------------- | ---------- | ---------------------- |
| 响应时间 | ≤ 2秒 | 请求响应时间 |
| 并发处理能力 | ≥ 1000 QPS | 每秒处理请求数 |
| 数据库查询时间 | ≤ 100ms | SQL执行时间 |
| 缓存命中率 | ≥ 80% | 缓存命中次数/总请求次数 |
| 系统可用性 | ≥ 99.9% | (总时间-故障时间)/总时间 |
### 5.2 性能优化策略
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 性能优化策略 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 缓存策略 │
│ ├── Caffeine本地缓存 (热点数据) │
│ ├── Redis分布式缓存 (可选) │
│ ├── 数据库查询结果缓存 │
│ └── 缓存预热和失效策略 │
│ │
│ 2. 数据库优化 │
│ ├── 索引优化 │
│ ├── 查询优化 │
│ ├── 分页查询 │
│ └── 读写分离 (后期) │
│ │
│ 3. 响应式编程 │
│ ├── WebFlux非阻塞IO │
│ ├── R2DBC响应式数据库访问 │
│ └── 异步处理 │
│ │
│ 4. 前端优化 │
│ ├── 资源压缩 │
│ ├── CDN加速 │
│ ├── 懒加载 │
│ └── 防抖节流 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
### 5.3 高并发场景处理
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 高并发场景处理 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 限流保护 │
│ └── 令牌桶限流,防止系统过载 │
│ │
│ 2. 熔断降级 │
│ └── 服务异常时快速失败,防止雪崩 │
│ │
│ 3. 分布式锁 │
│ └── Redis分布式锁,保证数据一致性 │
│ │
│ 4. 乐观锁 │
│ └── 版本号控制,冲突时重试 │
│ │
│ 5. 排队机制 │
│ └── 请求进入队列,异步处理结果 │
│ │
│ 6. 候补机制 │
│ └── 满员后自动进入候补队列,有人取消时自动补位 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
---
## 六、可扩展性设计
### 6.1 水平扩展
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 水平扩展方案 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 无状态设计 │
│ ├── Session外置到Redis │
│ ├── 本地不存储用户状态 │
│ └── 任意实例可处理任意请求 │
│ │
│ 2. 负载均衡 │
│ ├── 轮询: 默认策略 │
│ ├── 加权轮询: 根据服务器性能分配权重 │
│ └── 最少连接: 请求分配给连接数最少的服务器 │
│ │
│ 3. 服务拆分(后期) │
│ ├── 会员服务独立部署 │
│ ├── 预约服务独立部署 │
│ └── 数据服务独立部署 │
│ │
│ 4. 数据库扩展(后期) │
│ ├── 读写分离 │
│ ├── 分库分表 │
│ └── 多活架构 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
### 6.2 功能扩展
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 功能扩展点 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 支付扩展 │
│ ├── 预留支付接口抽象 │
│ ├── 支持微信支付、支付宝、银联等 │
│ └── 可扩展其他支付渠道 │
│ │
│ 2. 硬件扩展 │
│ ├── 签到网关抽象设计 │
│ ├── 支持多种签到设备 │
│ └── 可扩展智能硬件 │
│ │
│ 3. 消息扩展 │
│ ├── 消息模板可配置 │
│ ├── 支持多渠道推送 │
│ └── 可扩展新的消息渠道 │
│ │
│ 4. 报表扩展 │
│ ├── 报表模板可配置 │
│ ├── 支持自定义报表 │
│ └── 可扩展BI工具对接 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
---
## 七、监控与运维
### 7.1 监控体系
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 监控体系 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 基础监控 │
│ ├── CPU使用率 │
│ ├── 内存使用率 │
│ ├── 磁盘使用率 │
│ ├── 网络IO │
│ └── 进程状态 │
│ │
│ 2. 应用监控 │
│ ├── JVM监控(GC、堆内存、线程) │
│ ├── HTTP请求监控(QPS、响应时间、错误率) │
│ ├── 数据库连接池监控 │
│ └── 缓存命中率监控 │
│ │
│ 3. 业务监控 │
│ ├── 会员注册数 │
│ ├── 预约成功率 │
│ ├── 签到成功率 │
│ └── 支付成功率 │
│ │
│ 4. 告警机制 │
│ ├── 告警规则配置 │
│ ├── 告警通知方式(邮件、短信、钉钉) │
│ └── 告警升级策略 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
### 7.2 日志规范
#### 7.2.1 日志级别
- ERROR:错误日志,系统异常
- WARN:警告日志,潜在问题
- INFO:信息日志,关键业务操作
- DEBUG:调试日志,开发调试使用
#### 7.2.2 日志格式
```
[时间] [级别] [线程] [类名] - 日志内容
[2026-03-04 10:00:00] [INFO] [http-nio-8080-exec-1] [com.gym.controller.MemberController] - 会员登录成功, memberId=12345
```
#### 7.2.3 日志存储
- 日志文件按日期滚动
- 日志文件保留30天
- 错误日志单独存储
- 支持日志查询和导出
---
## 八、附录
### 8.1 技术选型清单
| 技术类别 | 技术选型 | 版本 | 用途 |
| -------------- | --------------------------- | -------- | ------------------ |
| 前端框架 | Vue3 | 3.x | 前端开发 |
| 前端构建 | Vite | 5.x | 前端构建 |
| 跨平台框架 | uniapp | 3.x | 小程序开发 |
| 后端框架 | Spring Boot | 3.x | 应用框架 |
| 响应式编程 | Spring WebFlux | 3.x | 响应式Web开发 |
| 数据库 | PostgreSQL | 15+ | 主数据库 |
| 数据库访问 | R2DBC | 1.x | 响应式数据库访问 |
| 缓存 | Caffeine | 3.x | 本地缓存 |
| 缓存(可选) | Redis | 7.x | 分布式缓存 |
| 安全框架 | Spring Security | 6.x | 安全认证授权 |
| 数据库版本管理 | Flyway | 9.x | 数据库版本管理 |
| 容器化 | Docker | 24+ | 容器化部署 |
| 负载均衡 | Nginx | 1.24+ | 反向代理负载均衡 |
### 8.2 术语表
| 术语 | 定义 |
| ----------------------------- | ------------------------------------------------ |
| 租户(Tenant) | 系统的多租户架构中的独立业务实体,如一个连锁品牌 |
| 门店(Store) | 租户下的具体经营场所 |
| 会员(Member) | 在门店注册的用户 |
| 权益(Benefit) | 会员卡包含的时长、次数、储值、等级等权益 |
| 可预约资源(Bookable Resource) | 团课、私教、场地、线上课程等可被预约的对象 |
| 时段(Slot) | 资源的可预约时间窗口 |
| JWT | JSON Web Token,用于身份认证 |
| RBAC | Role-Based Access Control,基于角色的访问控制 |
| QPS | Queries Per Second,每秒查询数 |
| SLA | Service Level Agreement,服务等级协议 |
---
**文档结束**
+861
View File
@@ -0,0 +1,861 @@
# 部署运维文档
> 文档编号: GYM-OPS-DEPLOY-001
> 版本: v1.0
> 日期: 2026-03-04
> 作者: 张翔
> 状态: 初稿
---
## 文档修订历史
| 版本 | 日期 | 作者 | 修订内容 |
| ---- | ---------- | ---- | ------------------ |
| v1.0 | 2026-03-04 | 张翔 | 创建部署运维文档 |
---
## 参考文档
- 《健身房管理系统技术架构设计文档》 GYM-HLD-TECH-001
- 《健身房管理系统响应式编程规范文档》 GYM-STD-REACTIVE-001
- Docker 官方文档
- Docker Compose 官方文档
---
## 一、部署架构
### 1.1 部署拓扑
```
┌─────────────────────────────────────────────────────────────────┐
│ 部署架构拓扑 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 用户层 │ │
│ ├─────────────────────────────────────────────────────────┤ │
│ │ • 会员小程序 • 教练端App • 管理后台PC │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 负载均衡层 (Nginx) │ │
│ ├─────────────────────────────────────────────────────────┤ │
│ │ • 负载均衡 • SSL 终止 • 静态资源 • 限流 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 应用层 (Docker Compose) │ │
│ ├─────────────────────────────────────────────────────────┤ │
│ │ • gym-manage (应用) • postgres (数据库) │ │
│ │ • redis (缓存) • rabbitmq (消息队列) │ │
│ │ • elasticsearch (搜索引擎) • prometheus (监控) │ │
│ │ • grafana (可视化) • kibana (日志可视化) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 监控层 (Prometheus + Grafana) │ │
│ ├─────────────────────────────────────────────────────────┤ │
│ │ • 指标采集 • 告警规则 • 可视化仪表板 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
```
### 1.2 服务器配置
#### 1.2.1 生产环境配置
| 组件 | CPU | 内存 | 磁盘 | 用途 |
|------|------|------|------|
| **应用服务器** | 4 核 | 8GB | 100GB | 运行应用 |
| **数据库服务器** | 8 核 | 16GB | 500GB | PostgreSQL |
| **缓存服务器** | 2 核 | 4GB | 50GB | Redis |
| **消息队列服务器** | 2 核 | 4GB | 100GB | RabbitMQ |
| **搜索服务器** | 4 核 | 8GB | 200GB | Elasticsearch |
| **监控服务器** | 2 核 | 4GB | 50GB | Prometheus + Grafana |
**推荐配置**
- 初期:应用 + 数据库 + 缓存部署在同一台服务器(8 核 16GB)
- 中期:应用独立部署(4 核 8GB),数据库独立部署(8 核 16GB)
- 长期:各组件独立部署,提高可用性
#### 1.2.2 开发环境配置
| 组件 | CPU | 内存 | 磁盘 | 用途 |
|------|------|------|------|
| **开发服务器** | 4 核 | 8GB | 100GB | 开发测试 |
---
## 二、环境准备
### 2.1 系统要求
#### 2.1.1 操作系统
- **推荐**Ubuntu 20.04 LTS / 22.04 LTS
- **兼容**CentOS 7+ / Debian 10+
- **内核版本**>= 4.15
#### 2.1.2 软件依赖
| 软件 | 版本 | 用途 |
|------|------|------|
| **Docker** | 24.x+ | 容器化部署 |
| **Docker Compose** | 2.20.x+ | 容器编排 |
| **Git** | 2.30+ | 版本控制 |
| **JDK** | 17+ | 运行环境 |
| **Maven** | 3.9.x+ | 项目构建 |
### 2.2 环境安装
#### 2.2.1 安装 Docker
```bash
# Ubuntu/Debian
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
# 启动 Docker 服务
sudo systemctl start docker
sudo systemctl enable docker
# 验证安装
docker --version
docker info
```
#### 2.2.2 安装 Docker Compose
```bash
# 下载 Docker Compose
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
# 添加执行权限
sudo chmod +x /usr/local/bin/docker-compose
# 验证安装
docker-compose --version
```
#### 2.2.3 安装 JDK
```bash
# Ubuntu/Debian
sudo apt update
sudo apt install -y openjdk-17-jdk
# 验证安装
java -version
```
#### 2.2.4 安装 Maven
```bash
# 下载 Maven
wget https://dlcdn.apache.org/maven/maven-3/3.9.5/binaries/apache-maven-3.9.5-bin.tar.gz
# 解压
tar -xzf apache-maven-3.9.5-bin.tar.gz
# 移动到 /opt
sudo mv apache-maven-3.9.5 /opt/maven
# 配置环境变量
echo 'export PATH=/opt/maven/bin:$PATH' >> ~/.bashrc
source ~/.bashrc
# 验证安装
mvn -version
```
---
## 三、部署流程
### 3.1 代码部署
#### 3.1.1 克隆代码
```bash
# 克隆代码仓库
git clone <repository-url>
cd gym-manage
# 查看分支
git branch -a
# 切换到生产分支
git checkout production
# 拉取最新代码
git pull origin production
```
#### 3.1.2 配置环境变量
```bash
# 复制环境变量模板
cp .env.example .env
# 编辑环境变量
vim .env
```
**.env 文件示例**
```bash
# 数据库配置
DB_USERNAME=postgres
DB_PASSWORD=your-strong-password
# Redis 配置
REDIS_PASSWORD=your-strong-password
# RabbitMQ 配置
MQ_USERNAME=admin
MQ_PASSWORD=your-strong-password
# Grafana 配置
GRAFANA_USER=admin
GRAFANA_PASSWORD=your-strong-password
# Spring 配置
SPRING_PROFILES_ACTIVE=prod
# JVM 配置 (响应式编程最佳实践)
JAVA_OPTS=-Xms512m -Xmx1024m -XX:+UseZGC -XX:ZAllocationSpikeTolerance=5 -XX:+UnlockExperimentalVMOptions -XX:+UseTransparentHugePages -XX:+AlwaysPreTouch
```
#### 3.1.3 构建镜像
```bash
# 构建应用镜像
docker-compose build gym-manage
# 查看镜像
docker images | grep gym-manage
```
### 3.2 服务部署
#### 3.2.1 启动所有服务
```bash
# 启动所有服务
docker-compose up -d
# 查看服务状态
docker-compose ps
# 查看日志
docker-compose logs -f gym-manage
```
#### 3.2.2 启动单个服务
```bash
# 启动数据库
docker-compose up -d postgres
# 启动应用
docker-compose up -d gym-manage
# 查看应用日志
docker-compose logs -f gym-manage
```
#### 3.2.3 健康检查
```bash
# 检查应用健康状态
curl http://localhost:8080/actuator/health
# 检查数据库连接
docker-compose exec postgres pg_isready -U postgres
# 检查 Redis 连接
docker-compose exec redis redis-cli ping
# 检查 RabbitMQ 连接
curl http://localhost:15672/api/overview -u admin:admin123
```
### 3.3 数据库初始化
#### 3.3.1 创建数据库
```bash
# 连接到 PostgreSQL
docker-compose exec postgres psql -U postgres
# 创建数据库
CREATE DATABASE gym_manage;
# 创建用户
CREATE USER gym_manage WITH PASSWORD 'your-password';
# 授权
GRANT ALL PRIVILEGES ON DATABASE gym_manage TO gym_manage;
# 退出
\q
```
#### 3.3.2 执行初始化脚本
```bash
# 执行初始化脚本
docker-compose exec -T postgres psql -U postgres -d gym_manage < sql/init.sql
```
---
## 四、更新部署
### 4.1 代码更新
#### 4.1.1 拉取最新代码
```bash
# 拉取最新代码
git pull origin production
# 查看变更
git log --oneline -5
```
#### 4.1.2 重新构建
```bash
# 停止服务
docker-compose down
# 重新构建镜像
docker-compose build gym-manage
# 启动服务
docker-compose up -d
```
### 4.2 滚动更新
#### 4.2.1 零停机更新
```bash
# 启动新实例
docker-compose up -d --scale gym-manage=2
# 等待新实例就绪
sleep 30
# 停止旧实例
docker-compose up -d --scale gym-manage=1
```
### 4.3 回滚部署
#### 4.3.1 快速回滚
```bash
# 回滚到上一个版本
git checkout HEAD~1
# 重新构建
docker-compose build gym-manage
# 启动服务
docker-compose up -d
```
#### 4.3.2 使用 Docker 镜像回滚
```bash
# 查看镜像历史
docker images | grep gym-manage
# 使用上一个镜像
docker-compose up -d --no-deps gym-manage
```
---
## 五、监控运维
### 5.1 监控体系
#### 5.1.1 Prometheus 监控
**访问地址**http://your-server:9090
**主要功能**
- 指标采集
- 数据存储
- 告警规则
- 查询接口
#### 5.1.2 Grafana 可视化
**访问地址**http://your-server:3000
**默认账号**
- 用户名:admin
- 密码:admin123
**主要功能**
- 数据可视化
- 仪表板配置
- 告警通知
- 用户管理
#### 5.1.3 Kibana 日志可视化
**访问地址**http://your-server:5601
**主要功能**
- 日志查询
- 日志分析
- 可视化图表
- 告警配置
### 5.2 日志管理
#### 5.2.1 应用日志
```bash
# 查看实时日志
docker-compose logs -f gym-manage
# 查看最近 100 行日志
docker-compose logs --tail=100 gym-manage
# 查看特定时间的日志
docker-compose logs --since 2024-01-01T00:00:00 gym-manage
```
#### 5.2.2 日志文件
```bash
# 查看日志文件
tail -f logs/gym-manage.log
# 查看错误日志
grep ERROR logs/gym-manage.log
# 统计错误数量
grep -c ERROR logs/gym-manage.log
```
### 5.3 告警配置
#### 5.3.1 告警规则
**文件位置**`monitoring/alerts.yml`
**告警类型**
- 高错误率
- 高响应时间
- 高内存使用率
- 数据库连接池耗尽
- 缓存命中率低
#### 5.3.2 告警通知
**通知方式**
- 邮件通知
- 钉钉通知
- 企业微信通知
- 短信通知
**配置示例**
```yaml
alertmanager:
receivers:
- name: 'email'
email_configs:
- to: 'your-email@example.com'
from: 'alertmanager@example.com'
smarthost: 'smtp.example.com:587'
auth_username: 'your-email@example.com'
auth_password: 'your-password'
```
---
## 六、性能优化
### 6.1 应用优化
#### 6.1.1 JVM 参数调优
```bash
# 生产环境推荐参数 (响应式编程最佳实践)
JAVA_OPTS=-Xms1024m -Xmx2048m -XX:+UseZGC -XX:ZAllocationSpikeTolerance=5 -XX:+UnlockExperimentalVMOptions -XX:+UseTransparentHugePages -XX:+AlwaysPreTouch -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/app/logs/heapdump.hprof
```
**参数说明**
- `-Xms`:初始堆内存大小
- `-Xmx`:最大堆内存大小
- `-XX:+UseZGC`:使用 ZGC 垃圾回收器(响应式编程推荐)
- `-XX:ZAllocationSpikeTolerance`:分配峰值容忍度
- `-XX:+UnlockExperimentalVMOptions`:解锁实验性选项
- `-XX:+UseTransparentHugePages`:使用透明大页
- `-XX:+AlwaysPreTouch`:预分配内存
- `-XX:+HeapDumpOnOutOfMemoryError`:内存溢出时生成堆转储
- `-XX:HeapDumpPath`:堆转储文件路径
**ZGC 优势**
- 低延迟:GC 暂停时间通常 < 10ms
- 高吞吐量:适合响应式编程的高并发场景
- 大堆支持:支持 TB 级堆内存
- 自适应:自动调整 GC 参数
#### 6.1.2 连接池调优
```yaml
# application-prod.yml (响应式编程最佳实践)
spring:
r2dbc:
pool:
initial-size: 5 # 初始连接数(响应式编程推荐较少连接)
max-size: 20 # 最大连接数(响应式编程推荐较少连接)
max-idle-time: 30m # 最大空闲时间
max-life-time: 1h # 最大生命周期
acquire-timeout: 10s # 获取连接超时时间(响应式编程推荐较长超时)
max-create-connection-time: 30s # 创建连接最大时间
max-validation-time: 5s # 验证连接最大时间
```
**连接池配置说明**
- 响应式编程使用较少的连接数(5-20)即可支持高并发
- 连接获取超时时间设置为 10s,避免快速失败
- 使用连接池复用,减少连接创建开销
### 6.2 数据库优化
#### 6.2.1 PostgreSQL 配置(响应式编程优化)
```bash
# postgresql.conf (响应式编程最佳实践)
# 内存配置
shared_buffers = 512MB # 共享缓冲区(响应式编程推荐较大值)
effective_cache_size = 2GB # 有效缓存大小
maintenance_work_mem = 128MB # 维护工作内存
work_mem = 32MB # 工作内存(响应式编程推荐较大值)
# WAL 配置
wal_buffers = 64MB # WAL 缓冲区
min_wal_size = 2GB # 最小 WAL 大小
max_wal_size = 8GB # 最大 WAL 大小
checkpoint_completion_target = 0.9 # 检查点完成目标
# 并发配置
max_connections = 200 # 最大连接数(响应式编程推荐较少连接)
max_worker_processes = 8 # 最大工作进程数
max_parallel_workers_per_gather = 4 # 每个查询的最大并行工作进程数
max_parallel_workers = 8 # 最大并行工作进程数
# IO 配置
random_page_cost = 1.1 # 随机页面成本(SSD 优化)
effective_io_concurrency = 300 # 有效 IO 并发数(SSD 优化)
max_io_concurrency = 200 # 最大 IO 并发数
# 查询优化
default_statistics_target = 100 # 默认统计目标
from_collapse_limit = 8 # FROM 子句折叠限制
join_collapse_limit = 8 # JOIN 子句折叠限制
# 日志配置
log_min_duration_statement = 1000 # 记录执行时间超过 1s 的语句
log_line_prefix = '%t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%h ' # 日志前缀
log_checkpoints = on # 记录检查点
log_connections = on # 记录连接
log_disconnections = on # 记录断开连接
log_lock_waits = on # 记录锁等待
```
#### 6.2.2 索引优化
```sql
-- 查看索引使用情况
SELECT schemaname, tablename, attname, n_distinct, correlation
FROM pg_stats
WHERE schemaname = 'public'
ORDER BY correlation DESC;
-- 查看慢查询
SELECT query, mean_exec_time, calls
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;
```
### 6.3 缓存优化
#### 6.3.1 Redis 配置
```bash
# redis.conf
maxmemory 2gb
maxmemory-policy allkeys-lru
save 900 1
save 300 10
save 60 10000
```
**参数说明**
- `maxmemory`:最大内存使用量
- `maxmemory-policy`:内存淘汰策略
- `save`RDB 持久化策略
---
## 七、故障排查
### 7.1 常见问题
#### 7.1.1 应用启动失败
**症状**:应用无法启动
**排查步骤**
```bash
# 查看应用日志
docker-compose logs gym-manage
# 检查配置文件
cat application-prod.yml
# 检查环境变量
docker-compose config
# 检查数据库连接
docker-compose exec postgres pg_isready -U postgres
```
**常见原因**
- 数据库连接失败
- 配置文件错误
- 端口冲突
- 内存不足
#### 7.1.2 数据库连接失败
**症状**:应用无法连接数据库
**排查步骤**
```bash
# 检查数据库状态
docker-compose ps postgres
# 查看数据库日志
docker-compose logs postgres
# 测试数据库连接
docker-compose exec postgres psql -U postgres -d gym_manage -c "SELECT 1;"
# 检查网络连接
docker-compose exec gym-manage ping postgres
```
**常见原因**
- 数据库未启动
- 网络不通
- 用户名密码错误
- 数据库不存在
#### 7.1.3 性能下降
**症状**:响应时间变长
**排查步骤**
```bash
# 查看应用日志
docker-compose logs gym-manage | grep "Slow query"
# 查看数据库慢查询
docker-compose exec postgres psql -U postgres -d gym_manage -c "SELECT * FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 10;"
# 查看系统资源
top
htop
# 查看数据库连接数
docker-compose exec postgres psql -U postgres -d gym_manage -c "SELECT count(*) FROM pg_stat_activity;"
```
**常见原因**
- 慢查询
- 数据库连接池耗尽
- 缓存命中率低
- 系统资源不足
### 7.2 应急处理
#### 7.2.1 重启服务
```bash
# 重启应用
docker-compose restart gym-manage
# 重启数据库
docker-compose restart postgres
# 重启所有服务
docker-compose restart
```
#### 7.2.2 回滚版本
```bash
# 回滚到上一个版本
git checkout HEAD~1
# 重新构建
docker-compose build gym-manage
# 启动服务
docker-compose up -d
```
#### 7.2.3 扩容
```bash
# 增加应用实例
docker-compose up -d --scale gym-manage=2
# 增加数据库资源
docker-compose up -d --scale postgres=2
```
---
## 八、备份恢复
### 8.1 数据备份
#### 8.1.1 数据库备份
```bash
# 备份数据库
docker-compose exec postgres pg_dump -U postgres gym_manage > backup/gym_manage_$(date +%Y%m%d_%H%M%S).sql
# 压缩备份文件
gzip backup/gym_manage_$(date +%Y%m%d_%H%M%S).sql
```
#### 8.1.2 定时备份
```bash
# 添加 crontab 任务
crontab -e
# 每天凌晨 2 点备份数据库
0 2 * * * docker-compose exec -T postgres pg_dump -U postgres gym_manage > backup/gym_manage_$(date +\%Y\%m\%d_\%H\%M\%S).sql
# 每周日凌晨 3 点清理 7 天前的备份
0 3 * * 0 find backup -name "gym_manage_*.sql" -mtime +7 -delete
```
### 8.2 数据恢复
#### 8.2.1 数据库恢复
```bash
# 停止应用
docker-compose stop gym-manage
# 恢复数据库
docker-compose exec -T postgres psql -U postgres gym_manage < backup/gym_manage_20240101_020000.sql
# 启动应用
docker-compose start gym-manage
```
---
## 九、安全加固
### 9.1 网络安全
#### 9.1.1 防火墙配置
```bash
# 配置防火墙
sudo ufw allow 22/tcp # SSH
sudo ufw allow 80/tcp # HTTP
sudo ufw allow 443/tcp # HTTPS
sudo ufw enable
```
#### 9.1.2 SSL 证书
```bash
# 使用 Let's Encrypt 获取免费 SSL 证书
sudo apt install certbot
sudo certbot certonly --standalone -d your-domain.com
# 配置 Nginx SSL
vim nginx/nginx.conf
```
### 9.2 应用安全
#### 9.2.1 敏感数据加密
```bash
# 配置环境变量
export DB_PASSWORD=$(openssl rand -base64 32)
export REDIS_PASSWORD=$(openssl rand -base64 32)
export MQ_PASSWORD=$(openssl rand -base64 32)
```
#### 9.2.2 权限控制
```yaml
# application-prod.yml
spring:
security:
user:
name: admin
password: ${ADMIN_PASSWORD}
roles: ADMIN
```
---
## 十、总结
### 10.1 部署要点
1. ✅ 使用 Docker Compose 一键部署
2. ✅ 配置健康检查和自动重启
3. ✅ 完善的监控和告警体系
4. ✅ 定期备份数据
5. ✅ 安全加固和权限控制
### 10.2 运维要点
1. ✅ 定期查看日志和监控
2. ✅ 及时处理告警
3. ✅ 定期备份数据
4. ✅ 定期更新系统和依赖
5. ✅ 定期进行安全审计
### 10.3 持续改进
1. ✅ 性能监控和优化
2. ✅ 故障复盘和改进
3. ✅ 文档更新和维护
4. ✅ 团队培训和知识分享
5. ✅ 自动化运维工具开发
+945
View File
@@ -0,0 +1,945 @@
# 响应式编程规范文档
> 文档编号: GYM-STD-REACTIVE-001
> 版本: v1.0
> 日期: 2026-03-04
> 作者: 张翔
> 状态: 初稿
---
## 文档修订历史
| 版本 | 日期 | 作者 | 修订内容 |
| ---- | ---------- | ---- | ------------------ |
| v1.0 | 2026-03-04 | 张翔 | 创建响应式编程规范文档 |
---
## 参考文档
- 《健身房管理系统技术架构设计文档》 GYM-HLD-TECH-001
- Project Reactor 官方文档
- Spring WebFlux 官方文档
- R2DBC 官方文档
---
## 一、概述
### 1.1 目的
本文档旨在为健身房管理系统项目制定响应式编程规范,确保团队成员正确使用响应式编程技术栈,避免常见的反模式,提高代码质量和系统性能。
### 1.2 适用范围
本规范适用于所有使用 Spring WebFlux + R2DBC 技术栈的代码开发。
### 1.3 核心原则
1. **永不阻塞**:禁止在响应式流中使用阻塞操作
2. **链式调用**:使用操作符链式调用,避免嵌套
3. **错误处理**:使用响应式错误处理机制,避免 try-catch
4. **背压处理**:正确处理背压,避免内存溢出
5. **资源释放**:确保所有资源正确释放,避免资源泄漏
---
## 二、响应式编程基础
### 2.1 核心概念
#### 2.1.1 Mono
**定义**:表示 0-1 个元素的异步序列,返回单个对象或空。
**适用场景**
- 查询单个对象
- 保存单个对象
- 更新单个对象
- 删除单个对象
**示例**
```java
// 查询单个会员
public Mono<Member> getMember(Long id) {
return memberRepository.findById(id);
}
// 保存单个会员
public Mono<Member> saveMember(Member member) {
return memberRepository.save(member);
}
```
#### 2.1.2 Flux
**定义**:表示 0-N 个元素的异步序列,返回多个对象。
**适用场景**
- 查询列表
- 批量操作
- 流式处理
- 实时数据推送
**示例**
```java
// 查询会员列表
public Flux<Member> listMembers(Long tenantId) {
return memberRepository.findByTenantId(tenantId);
}
// 批量保存会员
public Flux<Member> saveMembers(List<Member> members) {
return Flux.fromIterable(members)
.flatMap(memberRepository::save);
}
```
#### 2.1.3 Scheduler
**定义**:控制响应式操作的执行线程。
**常用 Scheduler**
| Scheduler | 用途 | 示例 |
|-----------|------|------|
| **Schedulers.parallel()** | CPU 密集型操作 | 数据计算、转换 |
| **Schedulers.boundedElastic()** | 阻塞 I/O 操作 | 文件读写、网络请求 |
| **Schedulers.single()** | 单线程顺序执行 | 顺序处理任务 |
| **Schedulers.immediate()** | 当前线程执行 | 简单操作 |
**示例**
```java
// CPU 密集型操作
public Flux<Member> processMembers(Flux<Member> members) {
return members.publishOn(Schedulers.parallel())
.map(this::calculateLevel);
}
// 阻塞 I/O 操作
public Mono<String> readFile(String path) {
return Mono.fromCallable(() -> Files.readString(Paths.get(path)))
.subscribeOn(Schedulers.boundedElastic());
}
```
### 2.2 常用操作符
#### 2.2.1 转换操作符
| 操作符 | 功能 | 示例 |
|-------|------|------|
| **map** | 一对一转换 | `.map(member -> member.getName())` |
| **flatMap** | 一对多转换(异步) | `.flatMap(member -> loadCards(member.getId()))` |
| **flatMapMany** | 一对多转换(返回 Flux) | `.flatMapMany(member -> listBenefits(member.getId()))` |
| **filter** | 过滤元素 | `.filter(member -> member.getStatus() == 1)` |
**示例**
```java
// map:一对一转换
public Flux<String> getMemberNames(Long tenantId) {
return memberRepository.findByTenantId(tenantId)
.map(Member::getName);
}
// flatMap:一对多转换(异步)
public Mono<Member> getMemberWithCards(Long id) {
return memberRepository.findById(id)
.flatMap(member -> memberCardRepository.findByMemberId(member.getId())
.collectList()
.map(cards -> {
member.setCards(cards);
return member;
}));
}
// filter:过滤元素
public Flux<Member> getActiveMembers(Long tenantId) {
return memberRepository.findByTenantId(tenantId)
.filter(member -> member.getStatus() == 1);
}
```
#### 2.2.2 条件操作符
| 操作符 | 功能 | 示例 |
|-------|------|------|
| **switchIfEmpty** | 序列为空时返回备选 | `.switchIfEmpty(Mono.error(new BusinessException("会员不存在")))` |
| **defaultIfEmpty** | 序列为空时返回默认值 | `.defaultIfEmpty(Member.builder().build())` |
| **take** | 取前 N 个元素 | `.take(10)` |
| **skip** | 跳过前 N 个元素 | `.skip(10)` |
**示例**
```java
// switchIfEmpty:序列为空时返回备选
public Mono<Member> getMember(Long id) {
return memberRepository.findById(id)
.switchIfEmpty(Mono.error(new BusinessException("会员不存在")));
}
// defaultIfEmpty:序列为空时返回默认值
public Flux<Member> listMembers(Long tenantId) {
return memberRepository.findByTenantId(tenantId)
.defaultIfEmpty(Member.builder().build());
}
// take:取前 10 个元素
public Flux<Member> listMembers(Long tenantId, int limit) {
return memberRepository.findByTenantId(tenantId)
.take(limit);
}
```
#### 2.2.3 错误处理操作符
| 操作符 | 功能 | 示例 |
|-------|------|------|
| **onErrorResume** | 捕获错误并返回备选序列 | `.onErrorResume(e -> Mono.empty())` |
| **onErrorReturn** | 捕获错误并返回默认值 | `.onErrorReturn(Member.builder().build())` |
| **doOnError** | 错误时执行副作用 | `.doOnError(e -> log.error("查询失败", e))` |
| **retry** | 重试 | `.retry(3)` |
| **retryWhen** | 高级重试 | `.retryWhen(Retry.backoff(3, Duration.ofSeconds(1)))` |
**示例**
```java
// onErrorResume:捕获错误并返回备选序列
public Mono<Member> getMember(Long id) {
return memberRepository.findById(id)
.onErrorResume(DataAccessException.class, e -> {
log.error("数据库查询失败: memberId={}", id, e);
return Mono.empty();
});
}
// onErrorReturn:捕获错误并返回默认值
public Mono<Member> getMember(Long id) {
return memberRepository.findById(id)
.onErrorReturn(Member.builder().build());
}
// doOnError:错误时执行副作用
public Mono<Member> getMember(Long id) {
return memberRepository.findById(id)
.doOnError(e -> log.error("查询会员失败: memberId={}", id, e));
}
// retry:重试 3 次
public Mono<Member> getMember(Long id) {
return memberRepository.findById(id)
.retry(3);
}
// retryWhen:高级重试(指数退避)
public Mono<Member> getMember(Long id) {
return memberRepository.findById(id)
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1))
.filter(throwable -> throwable instanceof TimeoutException)
.doBeforeRetry(signal -> log.warn("重试: attempt={}", signal.totalRetries())));
}
```
#### 2.2.4 生命周期操作符
| 操作符 | 功能 | 示例 |
|-------|------|------|
| **doOnSubscribe** | 订阅时执行 | `.doOnSubscribe(s -> log.debug("开始查询"))` |
| **doOnNext** | 每个元素到达时执行 | `.doOnNext(member -> log.debug("查询到会员: {}", member.getName()))` |
| **doOnComplete** | 完成时执行 | `.doOnComplete(() -> log.debug("查询完成"))` |
| **doOnError** | 错误时执行 | `.doOnError(e -> log.error("查询失败", e))` |
| **doOnTerminate** | 终止时执行(无论成功或失败) | `.doOnTerminate(() -> log.debug("查询结束"))` |
**示例**
```java
public Mono<Member> getMember(Long id) {
return memberRepository.findById(id)
.doOnSubscribe(s -> log.debug("开始查询会员: memberId={}", id))
.doOnNext(member -> log.debug("查询到会员: memberId={}, name={}", member.getId(), member.getName()))
.doOnComplete(() -> log.debug("查询会员完成: memberId={}", id))
.doOnError(e -> log.error("查询会员失败: memberId={}", id, e))
.doOnTerminate(() -> log.debug("查询会员结束: memberId={}", id));
}
```
---
## 三、编码规范
### 3.1 基本原则
#### 3.1.1 永不阻塞
**规则**:禁止在响应式流中使用阻塞操作。
**✅ 正确示例**
```java
public Mono<Member> getMember(Long id) {
return memberRepository.findById(id)
.flatMap(member -> loadMemberCards(member.getId()));
}
```
**❌ 错误示例**
```java
public Mono<Member> getMember(Long id) {
return memberRepository.findById(id)
.flatMap(member -> {
// 错误:使用 block() 阻塞
List<MemberCard> cards = memberCardRepository.findByMemberId(member.getId())
.collectList().block();
member.setCards(cards);
return Mono.just(member);
});
}
```
#### 3.1.2 链式调用
**规则**:使用操作符链式调用,避免嵌套。
**✅ 正确示例**
```java
public Mono<Member> getMemberWithCardsAndBenefits(Long id) {
return memberRepository.findById(id)
.flatMap(member -> loadMemberCards(member.getId())
.map(cards -> {
member.setCards(cards);
return member;
}))
.flatMap(member -> loadMemberBenefits(member.getId())
.map(benefits -> {
member.setBenefits(benefits);
return member;
}));
}
```
**❌ 错误示例**
```java
public Mono<Member> getMemberWithCardsAndBenefits(Long id) {
return memberRepository.findById(id)
.flatMap(member -> {
return loadMemberCards(member.getId())
.map(cards -> {
member.setCards(cards);
return member;
})
.flatMap(memberWithCards -> {
return loadMemberBenefits(memberWithCards.getId())
.map(benefits -> {
memberWithCards.setBenefits(benefits);
return memberWithCards;
});
});
});
}
```
#### 3.1.3 错误处理
**规则**:使用响应式错误处理机制,避免 try-catch。
**✅ 正确示例**
```java
public Mono<Member> getMember(Long id) {
return memberRepository.findById(id)
.switchIfEmpty(Mono.error(new BusinessException("会员不存在")))
.onErrorResume(DataAccessException.class, e -> {
log.error("数据库查询失败: memberId={}", id, e);
return Mono.error(new SystemException("系统错误"));
});
}
```
**❌ 错误示例**
```java
public Mono<Member> getMember(Long id) {
try {
return memberRepository.findById(id)
.switchIfEmpty(Mono.error(new BusinessException("会员不存在")));
} catch (Exception e) {
// 错误:try-catch 无法捕获响应式异常
log.error("查询失败", e);
return Mono.error(new SystemException("系统错误"));
}
}
```
### 3.2 Service 层规范
#### 3.2.1 基本结构
```java
@Service
@Slf4j
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
private final MemberCardRepository memberCardRepository;
private final BenefitService benefitService;
/**
* 查询会员
*/
public Mono<Member> getMember(Long id) {
return memberRepository.findById(id)
.switchIfEmpty(Mono.error(new BusinessException("会员不存在")))
.doOnSubscribe(s -> log.debug("开始查询会员: memberId={}", id))
.doOnNext(member -> log.debug("查询到会员: memberId={}, name={}", member.getId(), member.getName()))
.doOnError(e -> log.error("查询会员失败: memberId={}", id, e))
.doOnTerminate(() -> log.debug("查询会员结束: memberId={}", id));
}
/**
* 查询会员列表
*/
public Flux<Member> listMembers(Long tenantId, Long storeId) {
return memberRepository.findByTenantIdAndStoreId(tenantId, storeId)
.filter(member -> member.getStatus() == 1)
.sort(Comparator.comparing(Member::getCreatedAt).reversed());
}
/**
* 创建会员
*/
@Transactional
public Mono<Member> createMember(MemberCreateRequest request) {
return validateMemberCreateRequest(request)
.flatMap(v -> buildMember(request))
.flatMap(memberRepository::save)
.flatMap(member -> createDefaultMemberCard(member))
.doOnSuccess(member -> log.info("创建会员成功: memberId={}", member.getId()))
.doOnError(e -> log.error("创建会员失败: {}", e.getMessage()));
}
private Mono<Void> validateMemberCreateRequest(MemberCreateRequest request) {
return memberRepository.findByPhoneAndTenantId(request.getPhone(), request.getTenantId())
.flatMap(existing -> Mono.<Void>error(new BusinessException("手机号已注册")))
.switchIfEmpty(Mono.empty());
}
private Mono<Member> buildMember(MemberCreateRequest request) {
Member member = Member.builder()
.tenantId(request.getTenantId())
.storeId(request.getStoreId())
.memberNo(generateMemberNo(request.getTenantId()))
.name(request.getName())
.phone(encryptPhone(request.getPhone()))
.phoneMask(maskPhone(request.getPhone()))
.gender(request.getGender())
.birthday(request.getBirthday())
.status(1)
.build();
return Mono.just(member);
}
private Mono<Member> createDefaultMemberCard(Member member) {
MemberCard card = MemberCard.builder()
.tenantId(member.getTenantId())
.memberId(member.getId())
.cardNo(generateCardNo(member.getTenantId()))
.status(1)
.build();
return memberCardRepository.save(card)
.thenReturn(member);
}
}
```
#### 3.2.2 事务管理
```java
@Service
@Slf4j
public class BookingService {
private final BookingRecordRepository bookingRecordRepository;
private final BookingSlotRepository bookingSlotRepository;
private final BenefitService benefitService;
/**
* 预约时段
*/
@Transactional
public Mono<BookingRecord> bookSlot(BookingRequest request) {
return validateBooking(request)
.flatMap(v -> checkSlotAvailability(request.getSlotId()))
.flatMap(slot -> deductBenefit(request.getMemberId(), slot))
.flatMap(benefit -> createBookingRecord(request, benefit))
.flatMap(booking -> updateSlotBookedCount(request.getSlotId()))
.doOnSuccess(booking -> log.info("预约成功: bookingId={}", booking.getId()))
.doOnError(e -> log.error("预约失败: {}", e.getMessage()));
}
private Mono<BookingSlot> checkSlotAvailability(Long slotId) {
return bookingSlotRepository.findById(slotId)
.switchIfEmpty(Mono.error(new BusinessException("时段不存在")))
.filter(slot -> slot.getStatus() == 1)
.switchIfEmpty(Mono.error(new BusinessException("时段不可预约")))
.filter(slot -> slot.getBookedCount() < slot.getCapacity())
.switchIfEmpty(Mono.error(new BusinessException("时段已满")));
}
private Mono<MemberBenefit> deductBenefit(Long memberId, BookingSlot slot) {
return benefitService.deductBenefit(memberId,
slot.getPriceType(), slot.getPriceValue())
.switchIfEmpty(Mono.error(new BusinessException("权益不足")));
}
private Mono<BookingRecord> createBookingRecord(BookingRequest request,
MemberBenefit benefit) {
BookingRecord record = BookingRecord.builder()
.tenantId(request.getTenantId())
.storeId(request.getStoreId())
.memberId(request.getMemberId())
.slotId(request.getSlotId())
.bookingNo(generateBookingNo(request.getTenantId()))
.status(1)
.benefitId(benefit.getId())
.build();
return bookingRecordRepository.save(record);
}
private Mono<Void> updateSlotBookedCount(Long slotId) {
return bookingSlotRepository.findById(slotId)
.flatMap(slot -> {
slot.setBookedCount(slot.getBookedCount() + 1);
if (slot.getBookedCount() >= slot.getCapacity()) {
slot.setStatus(2); // 已满
}
return bookingSlotRepository.save(slot);
})
.then();
}
}
```
### 3.3 Controller 层规范
#### 3.3.1 基本结构
```java
@RestController
@RequestMapping("/members")
@RequiredArgsConstructor
@Slf4j
public class MemberController {
private final MemberService memberService;
/**
* 查询会员
*/
@GetMapping("/{id}")
public Mono<ResponseEntity<ApiResponse<Member>>> getMember(@PathVariable Long id) {
return memberService.getMember(id)
.map(member -> ResponseEntity.ok(ApiResponse.success(member)))
.switchIfEmpty(Mono.just(ResponseEntity.notFound().build()))
.onErrorResume(BusinessException.class, e ->
Mono.just(ResponseEntity.badRequest()
.body(ApiResponse.error(e.getMessage()))))
.onErrorResume(Exception.class, e -> {
log.error("查询会员失败: memberId={}", id, e);
return Mono.just(ResponseEntity.internalServerError()
.body(ApiResponse.error("系统错误")));
});
}
/**
* 查询会员列表
*/
@GetMapping
public Mono<ResponseEntity<ApiResponse<Page<Member>>>> listMembers(
@RequestParam(required = false) Long tenantId,
@RequestParam(required = false) Long storeId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
return memberService.listMembers(tenantId, storeId, page, size)
.map(members -> ResponseEntity.ok(ApiResponse.success(members)));
}
/**
* 创建会员
*/
@PostMapping
public Mono<ResponseEntity<ApiResponse<Member>>> createMember(
@Valid @RequestBody MemberCreateRequest request) {
return memberService.createMember(request)
.map(member -> ResponseEntity.ok(ApiResponse.success(member)))
.onErrorResume(BusinessException.class, e ->
Mono.just(ResponseEntity.badRequest()
.body(ApiResponse.error(e.getMessage()))))
.onErrorResume(Exception.class, e -> {
log.error("创建会员失败", e);
return Mono.just(ResponseEntity.internalServerError()
.body(ApiResponse.error("系统错误")));
});
}
}
```
---
## 四、反模式
### 4.1 阻塞操作
**❌ 反模式**:在响应式流中使用 `block()``blockFirst()``blockLast()`
```java
// 错误示例
public Mono<Member> getMember(Long id) {
return memberRepository.findById(id)
.flatMap(member -> {
// 错误:使用 block() 阻塞
List<MemberCard> cards = memberCardRepository.findByMemberId(member.getId())
.collectList().block();
member.setCards(cards);
return Mono.just(member);
});
}
```
**✅ 正确做法**:使用 `flatMap` 链式调用
```java
// 正确示例
public Mono<Member> getMember(Long id) {
return memberRepository.findById(id)
.flatMap(member -> memberCardRepository.findByMemberId(member.getId())
.collectList()
.map(cards -> {
member.setCards(cards);
return member;
}));
}
```
### 4.2 嵌套订阅
**❌ 反模式**:在 `flatMap` 中使用 `subscribe`
```java
// 错误示例
public Mono<Member> getMember(Long id) {
return memberRepository.findById(id)
.flatMap(member -> {
memberCardRepository.findByMemberId(member.getId())
.collectList()
.subscribe(cards -> {
// 错误:在 flatMap 中使用 subscribe
member.setCards(cards);
});
return Mono.just(member);
});
}
```
**✅ 正确做法**:使用 `map` 转换数据
```java
// 正确示例
public Mono<Member> getMember(Long id) {
return memberRepository.findById(id)
.zipWith(memberCardRepository.findByMemberId(id).collectList())
.map(tuple -> {
Member member = tuple.getT1();
List<MemberCard> cards = tuple.getT2();
member.setCards(cards);
return member;
});
}
```
### 4.3 忽略错误
**❌ 反模式**:忽略错误,不处理
```java
// 错误示例
public Mono<Member> getMember(Long id) {
return memberRepository.findById(id)
.onErrorResume(e -> Mono.empty()); // 错误:忽略错误
}
```
**✅ 正确做法**:记录错误并处理
```java
// 正确示例
public Mono<Member> getMember(Long id) {
return memberRepository.findById(id)
.onErrorResume(e -> {
log.error("查询会员失败: memberId={}", id, e);
return Mono.error(new SystemException("系统错误"));
});
}
```
### 4.4 不处理背压
**❌ 反模式**:不处理背压,可能导致内存溢出
```java
// 错误示例
public Flux<Member> listAllMembers() {
return memberRepository.findAll(); // 错误:可能返回大量数据
}
```
**✅ 正确做法**:使用 `take` 限制数据量
```java
// 正确示例
public Flux<Member> listAllMembers() {
return memberRepository.findAll()
.take(1000); // 限制最多返回 1000 条
}
```
### 4.5 资源泄漏
**❌ 反模式**:不释放资源
```java
// 错误示例
public Mono<String> readFile(String path) {
return Mono.fromCallable(() -> {
// 错误:不释放资源
BufferedReader reader = Files.newBufferedReader(Paths.get(path));
return reader.readLine();
});
}
```
**✅ 正确做法**:使用 `using` 确保资源释放
```java
// 正确示例
public Mono<String> readFile(String path) {
return Mono.using(
() -> Files.newBufferedReader(Paths.get(path)),
reader -> Mono.fromCallable(reader::readLine),
reader -> {
try {
reader.close();
} catch (IOException e) {
log.error("关闭文件失败", e);
}
});
}
```
---
## 五、最佳实践
### 5.1 日志记录
**原则**:使用 `doOnSubscribe``doOnNext``doOnError``doOnTerminate` 记录关键操作。
```java
public Mono<Member> getMember(Long id) {
return memberRepository.findById(id)
.doOnSubscribe(s -> log.debug("开始查询会员: memberId={}", id))
.doOnNext(member -> log.debug("查询到会员: memberId={}, name={}", member.getId(), member.getName()))
.doOnComplete(() -> log.debug("查询会员完成: memberId={}", id))
.doOnError(e -> log.error("查询会员失败: memberId={}", id, e))
.doOnTerminate(() -> log.debug("查询会员结束: memberId={}", id));
}
```
### 5.2 超时控制
**原则**:为所有外部调用设置超时时间。
```java
public Mono<Member> getMember(Long id) {
return memberRepository.findById(id)
.timeout(Duration.ofSeconds(3)) // 3 秒超时
.switchIfEmpty(Mono.error(new BusinessException("会员不存在")));
}
```
### 5.3 重试机制
**原则**:为可重试的操作设置重试机制。
```java
public Mono<Member> getMember(Long id) {
return memberRepository.findById(id)
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1)) // 重试 3 次,间隔 1 秒
.filter(throwable -> throwable instanceof TimeoutException)
.doBeforeRetry(signal -> log.warn("重试: attempt={}", signal.totalRetries())));
}
```
### 5.4 缓存策略
**原则**:使用 Cache-Aside 模式,先查缓存,缓存未命中再查数据库。
```java
public Mono<Member> getMember(Long id) {
String cacheKey = "member:" + id;
return redisTemplate.opsForValue()
.get(cacheKey)
.cast(Member.class)
.switchIfEmpty(
memberRepository.findById(id)
.flatMap(member -> redisTemplate.opsForValue()
.set(cacheKey, member, Duration.ofMinutes(30))
.thenReturn(member))
);
}
```
### 5.5 性能优化
**原则**:使用 `parallel()` 并行处理 CPU 密集型操作。
```java
public Flux<Member> processMembers(Flux<Member> members) {
return members.publishOn(Schedulers.parallel())
.map(this::calculateLevel)
.publishOn(Schedulers.boundedElastic());
}
```
---
## 六、测试规范
### 6.1 单元测试
**原则**:使用 `StepVerifier` 测试响应式流。
```java
@SpringBootTest
class MemberServiceTest {
@Autowired
private MemberService memberService;
@MockBean
private MemberRepository memberRepository;
@Test
void testGetMember() {
Member member = Member.builder()
.id(1L)
.name("张三")
.phone("13800138000")
.build();
when(memberRepository.findById(1L))
.thenReturn(Mono.just(member));
StepVerifier.create(memberService.getMember(1L))
.expectNextMatches(m -> m.getName().equals("张三"))
.verifyComplete();
}
@Test
void testGetMemberNotFound() {
when(memberRepository.findById(1L))
.thenReturn(Mono.empty());
StepVerifier.create(memberService.getMember(1L))
.expectErrorMatches(e -> e instanceof BusinessException)
.verify();
}
}
```
### 6.2 集成测试
**原则**:使用 `WebTestClient` 测试 Controller。
```java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWebTestClient
class MemberControllerTest {
@Autowired
private WebTestClient webTestClient;
@Test
void testGetMember() {
webTestClient.get()
.uri("/api/v1/members/1")
.exchange()
.expectStatus().isOk()
.expectBody(Member.class)
.value(member -> {
assertThat(member.getName()).isEqualTo("张三");
});
}
@Test
void testGetMemberNotFound() {
webTestClient.get()
.uri("/api/v1/members/999")
.exchange()
.expectStatus().isNotFound();
}
}
```
### 6.3 性能测试
**原则**:使用 `StepVerifier.withVirtualTime` 测试性能。
```java
@Test
void testGetMemberPerformance() {
StepVerifier.withVirtualTime(() -> memberService.getMember(1L))
.expectNextCount(1)
.expectComplete()
.verify(Duration.ofMillis(100)); // 100ms 内完成
}
```
---
## 七、总结
### 7.1 核心原则回顾
1.**永不阻塞**:禁止在响应式流中使用阻塞操作
2.**链式调用**:使用操作符链式调用,避免嵌套
3.**错误处理**:使用响应式错误处理机制,避免 try-catch
4.**背压处理**:正确处理背压,避免内存溢出
5.**资源释放**:确保所有资源正确释放,避免资源泄漏
### 7.2 关键成功因素
1. ✅ 严格遵守响应式编程规范
2. ✅ 使用 StepVerifier 进行测试
3. ✅ 完善的日志记录
4. ✅ 合理的超时和重试机制
5. ✅ 正确的缓存策略
### 7.3 持续改进
1. ✅ 定期代码审查
2. ✅ 性能监控和优化
3. ✅ 技术分享和培训
4. ✅ 文档更新和维护
@@ -4,7 +4,10 @@
> 版本: v1.0
> 日期: 2026-02-28
> 作者: 张翔
> 状态: 初稿
> 状态: 初稿
> **归属版本**: 基础版
**说明**:本文档为健身房管理系统**基础版**的会员模块详细设计文档,描述会员管理模块的数据库设计、API设计、业务逻辑实现等技术细节。
---
@@ -4,7 +4,10 @@
> 版本: v1.0
> 日期: 2026-02-28
> 作者: 张翔
> 状态: 初稿
> 状态: 初稿
> **归属版本**: 基础版
**说明**:本文档为健身房管理系统**基础版**的签到模块详细设计文档,描述扫码签到模块的数据库设计、API设计、业务逻辑实现等技术细节。
---
@@ -4,7 +4,10 @@
> 版本: v1.0
> 日期: 2026-02-28
> 作者: 张翔
> 状态: 初稿
> 状态: 初稿
> **归属版本**: 基础版
**说明**:本文档为健身房管理系统**基础版**的预约模块详细设计文档,描述团课预约模块的数据库设计、API设计、业务逻辑实现等技术细节。
---
@@ -820,7 +823,6 @@ public class BookingDomainService {
private final BenefitDomainService benefitService;
private final TransactionalOperator rxtx;
@Transactional
public Mono<BookingRecord> createBooking(Long memberId, Long slotId, String source) {
return Mono.defer(() ->
slotRepository.findById(slotId)
@@ -868,7 +870,6 @@ public class BookingDomainService {
).as(rxtx::transactional);
}
@Transactional
public Mono<Void> cancelBooking(Long bookingId, String reason, Long operatorId) {
return Mono.defer(() ->
recordRepository.findById(bookingId)
+958
View File
@@ -0,0 +1,958 @@
# 健身房管理系统前端安全规范文档
> 文档编号: GYM-FE-SEC-001
> 版本: v1.0
> 日期: 2026-03-04
> 作者: 张翔
> 状态: 初稿
---
## 文档修订历史
| 版本 | 日期 | 作者 | 修订内容 |
| ---- | ---------- | ---- | -------- |
| v1.0 | 2026-03-04 | 张翔 | 创建前端安全规范 |
---
## 参考文档
- 《健身房管理系统前端技术架构详细设计》 GYM-FE-ARCH-001
- OWASP Top 10 Web Application Security Risks
- Content Security Policy (CSP) Level 3
- Web Content Accessibility Guidelines (WCAG) 2.1
---
## 一、安全概述
### 1.1 安全目标
- **数据安全**:保护用户敏感数据(手机号、身份证号、银行卡号等)
- **交易安全**:确保支付和充值操作的安全性
- **身份安全**:防止未授权访问和身份冒用
- **隐私保护**:符合GDPR等隐私法规要求
- **合规性**:符合金融行业安全标准和监管要求
### 1.2 安全原则
| 原则 | 描述 | 实施方式 |
|------|------|----------|
| **最小权限** | 只授予必要的权限 | 基于角色的访问控制(RBAC) |
| **纵深防御** | 多层安全防护 | 输入验证、输出转义、加密传输 |
| **默认安全** | 默认配置安全 | CSP策略、安全Headers |
| **审计追踪** | 记录关键操作 | 操作日志、异常日志 |
| **持续监控** | 实时安全监控 | 错误监控、性能监控 |
### 1.3 安全威胁
| 威胁类型 | 风险等级 | 防护措施 |
|---------|---------|----------|
| **XSS(跨站脚本攻击)** | 高 | 输入过滤、输出转义、CSP |
| **CSRF(跨站请求伪造)** | 高 | Token验证、SameSite Cookie |
| **点击劫持** | 中 | X-Frame-Options、CSP |
| **中间人攻击** | 高 | HTTPS、HSTS |
| **敏感信息泄露** | 高 | 数据加密、脱敏显示 |
| **暴力破解** | 中 | 验证码、登录限制 |
| **会话劫持** | 高 | 安全Cookie、会话超时 |
---
## 二、XSS防护
### 2.1 输入验证
#### 2.1.1 白名单验证
```typescript
// utils/validator.ts
export function sanitizeInput(input: string, allowedChars: RegExp): string {
return input.replace(allowedChars, '')
}
export function validatePhoneNumber(phone: string): boolean {
const phoneRegex = /^1[3-9]\d{9}$/
return phoneRegex.test(phone)
}
export function validateIdCard(idCard: string): boolean {
const idCardRegex = /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/
return idCardRegex.test(idCard)
}
export function validateEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailRegex.test(email)
}
```
#### 2.1.2 输入长度限制
```typescript
// utils/validator.ts
export const MAX_INPUT_LENGTH = {
name: 64,
phone: 11,
idCard: 18,
address: 256,
remark: 512
}
export function validateLength(input: string, maxLength: number): boolean {
return input.length <= maxLength
}
```
### 2.2 输出转义
#### 2.2.1 HTML转义
```typescript
// utils/sanitize.ts
import DOMPurify from 'dompurify'
export function sanitizeHtml(html: string): string {
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u'],
ALLOWED_ATTR: []
})
}
export function escapeHtml(text: string): string {
const map: Record<string, string> = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
}
return text.replace(/[&<>"']/g, (m) => map[m])
}
```
#### 2.2.2 URL转义
```typescript
// utils/sanitize.ts
export function sanitizeUrl(url: string): string {
try {
const parsed = new URL(url)
if (!['http:', 'https:'].includes(parsed.protocol)) {
return '#'
}
return url
} catch {
return '#'
}
}
```
### 2.3 CSP策略
#### 2.3.1 基础CSP配置
```html
<!-- index.html -->
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net;
style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net;
img-src 'self' data: https:;
font-src 'self' https://cdn.jsdelivr.net;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';">
```
#### 2.3.2 动态CSP配置
```typescript
// utils/csp.ts
export function generateCSP(config: CSPConfig): string {
const directives = [
`default-src ${config.defaultSrc.join(' ')}`,
`script-src ${config.scriptSrc.join(' ')}`,
`style-src ${config.styleSrc.join(' ')}`,
`img-src ${config.imgSrc.join(' ')}`,
`connect-src ${config.connectSrc.join(' ')}`
]
return directives.join('; ')
}
// 使用
const csp = generateCSP({
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "https://cdn.jsdelivr.net"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "https://api.example.com"]
})
document.querySelector('meta[http-equiv="Content-Security-Policy"]')
?.setAttribute('content', csp)
```
---
## 三、CSRF防护
### 3.1 Token验证
#### 3.1.1 Token生成与存储
```typescript
// utils/csrf.ts
export function generateCSRFToken(): string {
const array = new Uint8Array(32)
crypto.getRandomValues(array)
return Array.from(array, (byte) => byte.toString(16).padStart(2, '0')).join('')
}
export function setCSRFToken(token: string): void {
localStorage.setItem('csrf_token', token)
}
export function getCSRFToken(): string {
return localStorage.getItem('csrf_token') || ''
}
```
#### 3.1.2 Token注入请求
```typescript
// api/request.ts
import { getCSRFToken } from '@/utils/csrf'
instance.interceptors.request.use((config) => {
const csrfToken = getCSRFToken()
if (csrfToken) {
config.headers['X-CSRF-Token'] = csrfToken
}
return config
})
```
### 3.2 SameSite Cookie
```typescript
// api/request.ts
instance.interceptors.request.use((config) => {
config.withCredentials = true
return config
})
```
### 3.3 双重Cookie提交
```typescript
// utils/csrf.ts
export function setDoubleSubmitCookie(token: string): void {
document.cookie = `csrf_token=${token}; path=/; SameSite=Strict; Secure`
}
export function getDoubleSubmitCookie(): string {
const match = document.cookie.match(/csrf_token=([^;]+)/)
return match ? match[1] : ''
}
```
---
## 四、数据安全
### 4.1 数据加密
#### 4.1.1 AES加密
```typescript
// utils/crypto.ts
import CryptoJS from 'crypto-js'
const SECRET_KEY = import.meta.env.VITE_CRYPTO_SECRET_KEY
export function encrypt(text: string): string {
const key = CryptoJS.enc.Utf8.parse(SECRET_KEY)
const iv = CryptoJS.lib.WordArray.random(16)
const encrypted = CryptoJS.AES.encrypt(text, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
})
return iv.toString() + ':' + encrypted.toString()
}
export function decrypt(ciphertext: string): string {
const key = CryptoJS.enc.Utf8.parse(SECRET_KEY)
const [ivHex, encrypted] = ciphertext.split(':')
const iv = CryptoJS.enc.Hex.parse(ivHex)
const decrypted = CryptoJS.AES.decrypt(encrypted, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
})
return decrypted.toString(CryptoJS.enc.Utf8)
}
```
#### 4.1.2 RSA加密(用于敏感数据)
```typescript
// utils/crypto.ts
import JSEncrypt from 'jsencrypt'
const PUBLIC_KEY = import.meta.env.VITE_RSA_PUBLIC_KEY
export function encryptWithRSA(text: string): string {
const encrypt = new JSEncrypt()
encrypt.setPublicKey(PUBLIC_KEY)
return encrypt.encrypt(text) || ''
}
```
### 4.2 数据脱敏
#### 4.2.1 手机号脱敏
```typescript
// utils/mask.ts
export function maskPhone(phone: string): string {
if (!phone || phone.length !== 11) {
return phone
}
return phone.substring(0, 3) + '****' + phone.substring(7)
}
```
#### 4.2.2 身份证号脱敏
```typescript
// utils/mask.ts
export function maskIdCard(idCard: string): string {
if (!idCard || idCard.length !== 18) {
return idCard
}
return idCard.substring(0, 6) + '********' + idCard.substring(14)
}
```
#### 4.2.3 银行卡号脱敏
```typescript
// utils/mask.ts
export function maskBankCard(bankCard: string): string {
if (!bankCard || bankCard.length < 16) {
return bankCard
}
return bankCard.substring(0, 4) + ' **** **** ' + bankCard.substring(bankCard.length - 4)
}
```
### 4.3 敏感信息存储
#### 4.3.1 安全存储
```typescript
// utils/storage.ts
import { encrypt, decrypt } from './crypto'
export const secureStorage = {
setItem(key: string, value: any): void {
const encrypted = encrypt(JSON.stringify(value))
localStorage.setItem(key, encrypted)
},
getItem<T>(key: string): T | null {
const encrypted = localStorage.getItem(key)
if (!encrypted) return null
try {
return JSON.parse(decrypt(encrypted)) as T
} catch {
return null
}
},
removeItem(key: string): void {
localStorage.removeItem(key)
},
clear(): void {
localStorage.clear()
}
}
```
#### 4.3.2 会话存储
```typescript
// utils/storage.ts
export const sessionStorage = {
setItem(key: string, value: any): void {
const encrypted = encrypt(JSON.stringify(value))
window.sessionStorage.setItem(key, encrypted)
},
getItem<T>(key: string): T | null {
const encrypted = window.sessionStorage.getItem(key)
if (!encrypted) return null
try {
return JSON.parse(decrypt(encrypted)) as T
} catch {
return null
}
},
removeItem(key: string): void {
window.sessionStorage.removeItem(key)
},
clear(): void {
window.sessionStorage.clear()
}
}
```
---
## 五、身份认证与授权
### 5.1 认证安全
#### 5.1.1 密码安全
```typescript
// utils/password.ts
export function validatePassword(password: string): { valid: boolean; message?: string } {
if (password.length < 8) {
return { valid: false, message: '密码长度至少8位' }
}
if (!/[A-Z]/.test(password)) {
return { valid: false, message: '密码必须包含大写字母' }
}
if (!/[a-z]/.test(password)) {
return { valid: false, message: '密码必须包含小写字母' }
}
if (!/[0-9]/.test(password)) {
return { valid: false, message: '密码必须包含数字' }
}
if (!/[!@#$%^&*]/.test(password)) {
return { valid: false, message: '密码必须包含特殊字符' }
}
return { valid: true }
}
```
#### 5.1.2 Token管理
```typescript
// stores/auth.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useAuthStore = defineStore('auth', () => {
const token = ref<string>('')
const refreshToken = ref<string>('')
const tokenExpireTime = ref<number>(0)
const setToken = (newToken: string, expireIn: number) => {
token.value = newToken
tokenExpireTime.value = Date.now() + expireIn * 1000
secureStorage.setItem('auth_token', newToken)
secureStorage.setItem('token_expire_time', tokenExpireTime.value)
}
const setRefreshToken = (newRefreshToken: string) => {
refreshToken.value = newRefreshToken
secureStorage.setItem('refresh_token', newRefreshToken)
}
const isTokenExpired = (): boolean => {
return Date.now() >= tokenExpireTime.value
}
const clearAuth = () => {
token.value = ''
refreshToken.value = ''
tokenExpireTime.value = 0
secureStorage.removeItem('auth_token')
secureStorage.removeItem('refresh_token')
secureStorage.removeItem('token_expire_time')
}
return {
token,
refreshToken,
tokenExpireTime,
setToken,
setRefreshToken,
isTokenExpired,
clearAuth
}
})
```
#### 5.1.3 Token刷新
```typescript
// api/request.ts
import { useAuthStore } from '@/stores/auth'
instance.interceptors.request.use(async (config) => {
const authStore = useAuthStore()
if (authStore.isTokenExpired()) {
try {
const response = await instance.post('/auth/refresh', {
refreshToken: authStore.refreshToken
})
authStore.setToken(response.token, response.expireIn)
authStore.setRefreshToken(response.refreshToken)
config.headers.Authorization = `Bearer ${response.token}`
} catch (error) {
authStore.clearAuth()
window.location.href = '/login'
return Promise.reject(error)
}
} else {
config.headers.Authorization = `Bearer ${authStore.token}`
}
return config
})
```
### 5.2 授权安全
#### 5.2.1 权限验证
```typescript
// utils/permission.ts
export interface Permission {
resource: string
action: string
}
export function hasPermission(userPermissions: string[], required: Permission): boolean {
const permissionString = `${required.resource}:${required.action}`
return userPermissions.includes(permissionString)
}
export function hasAnyPermission(userPermissions: string[], required: Permission[]): boolean {
return required.some(p => hasPermission(userPermissions, p))
}
export function hasAllPermissions(userPermissions: string[], required: Permission[]): boolean {
return required.every(p => hasPermission(userPermissions, p))
}
```
#### 5.2.2 路由权限守卫
```typescript
// router/guards/permission.ts
import { useAuthStore } from '@/stores/auth'
import { usePermissionStore } from '@/stores/permission'
router.beforeEach(async (to, from, next) => {
const authStore = useAuthStore()
const permissionStore = usePermissionStore()
if (to.meta.requiresAuth && !authStore.token) {
next('/login')
return
}
if (to.meta.permission) {
const required = to.meta.permission as Permission
if (!hasPermission(permissionStore.permissions, required)) {
next('/403')
return
}
}
next()
})
```
---
## 六、安全Headers
### 6.1 基础安全Headers
```typescript
// utils/headers.ts
export const securityHeaders = {
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'X-XSS-Protection': '1; mode=block',
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
'Referrer-Policy': 'strict-origin-when-cross-origin',
'Permissions-Policy': 'geolocation=(), microphone=(), camera=()'
}
```
### 6.2 动态设置Headers
```typescript
// api/request.ts
instance.interceptors.request.use((config) => {
Object.assign(config.headers, securityHeaders)
return config
})
```
---
## 七、点击劫持防护
### 7.1 X-Frame-Options
```html
<!-- index.html -->
<meta http-equiv="X-Frame-Options" content="DENY">
```
### 7.2 CSP frame-ancestors
```html
<!-- index.html -->
<meta http-equiv="Content-Security-Policy"
content="frame-ancestors 'none';">
```
### 7.3 JavaScript防护
```typescript
// utils/clickjacking.ts
export function preventClickjacking(): void {
if (window.self !== window.top) {
window.top.location = window.self.location
}
}
// 在应用初始化时调用
preventClickjacking()
```
---
## 八、安全键盘
### 8.1 安全键盘组件
```vue
<!-- components/base/SecureKeyboard.vue -->
<template>
<div class="secure-keyboard">
<div class="keyboard-display">
<span v-for="i in maskedValue.length" :key="i"></span>
</div>
<div class="keyboard-grid">
<button
v-for="key in keys"
:key="key"
@click="handleKeyPress(key)"
class="key-button"
>
{{ key }}
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
const keys = ['1', '2', '3', '4', '5', '6', '7', '8', '9', 'C', '0', '⌫']
const value = ref<string>('')
const maxLength = 6
const maskedValue = computed(() => value.value)
const emit = defineEmits<{
input: [value: string]
complete: [value: string]
}>()
const handleKeyPress = (key: string) => {
if (key === 'C') {
value.value = ''
} else if (key === '⌫') {
value.value = value.value.slice(0, -1)
} else if (value.value.length < maxLength) {
value.value += key
}
emit('input', value.value)
if (value.value.length === maxLength) {
emit('complete', value.value)
}
}
</script>
<style scoped>
.secure-keyboard {
background: #f5f5f5;
padding: 16px;
border-radius: 8px;
}
.keyboard-display {
display: flex;
justify-content: center;
gap: 8px;
margin-bottom: 16px;
font-size: 24px;
}
.keyboard-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.key-button {
padding: 16px;
font-size: 20px;
background: white;
border: 1px solid #ddd;
border-radius: 8px;
cursor: pointer;
}
.key-button:active {
background: #e0e0e0;
}
</style>
```
---
## 九、安全日志与监控
### 9.1 操作日志
```typescript
// utils/logger.ts
interface LogEntry {
timestamp: number
level: 'info' | 'warn' | 'error'
action: string
userId?: number
details?: any
}
export class SecurityLogger {
private logs: LogEntry[] = []
log(action: string, details?: any, level: 'info' | 'warn' | 'error' = 'info') {
const entry: LogEntry = {
timestamp: Date.now(),
level,
action,
userId: this.getUserId(),
details
}
this.logs.push(entry)
this.sendToServer(entry)
}
private getUserId(): number | undefined {
const authStore = useAuthStore()
return authStore.user?.id
}
private async sendToServer(entry: LogEntry) {
try {
await api.post('/security/log', entry)
} catch (error) {
console.error('Failed to send security log:', error)
}
}
}
export const securityLogger = new SecurityLogger()
```
### 9.2 异常监控
```typescript
// utils/sentry.ts
import * as Sentry from '@sentry/vue'
export function setupSentry(app: App) {
Sentry.init({
app,
dsn: import.meta.env.VITE_SENTRY_DSN,
environment: import.meta.env.MODE,
tracesSampleRate: 1.0,
beforeSend(event) {
if (event.request) {
delete event.request.cookies
delete event.request.headers
}
return event
}
})
}
// 全局错误处理
window.addEventListener('error', (event) => {
securityLogger.log('window.error', {
message: event.message,
filename: event.filename,
lineno: event.lineno
}, 'error')
})
window.addEventListener('unhandledrejection', (event) => {
securityLogger.log('unhandledrejection', {
reason: event.reason
}, 'error')
})
```
---
## 十、安全最佳实践
### 10.1 开发阶段
1. **代码审查**
- 所有代码必须经过安全审查
- 使用ESLint安全规则
- 使用npm audit检查依赖漏洞
2. **依赖管理**
- 定期更新依赖包
- 使用npm audit fix修复漏洞
- 使用Snyk等工具监控依赖安全
3. **环境变量**
- 敏感信息使用环境变量
- 不要将密钥提交到代码仓库
- 使用.env文件管理配置
### 10.2 测试阶段
1. **安全测试**
- 使用OWASP ZAP进行安全扫描
- 进行渗透测试
- 测试XSS、CSRF等漏洞
2. **代码扫描**
- 使用SonarQube进行代码质量检查
- 使用ESLint进行代码规范检查
- 使用Prettier进行代码格式化
### 10.3 部署阶段
1. **HTTPS强制**
- 使用SSL证书
- 配置HSTS
- 禁用HTTP访问
2. **安全配置**
- 配置CSP策略
- 配置安全Headers
- 配置防火墙规则
3. **监控告警**
- 配置错误监控
- 配置性能监控
- 配置安全告警
---
## 十一、合规性要求
### 11.1 GDPR合规
1. **数据最小化**
- 只收集必要的用户数据
- 提供数据删除功能
- 提供数据导出功能
2. **用户同意**
- 明确告知数据使用目的
- 获取用户明确同意
- 提供撤回同意的选项
3. **数据保护**
- 加密存储敏感数据
- 限制数据访问权限
- 定期进行安全审计
### 11.2 无障碍合规
1. **WCAG 2.1 AA级标准**
- 键盘导航支持
- 屏幕阅读器支持
- 颜色对比度符合标准
2. **ARIA标签**
- 为交互元素添加ARIA标签
- 为动态内容添加ARIA标签
- 为表单元素添加ARIA标签
---
## 十二、安全检查清单
### 12.1 代码提交前检查
- [ ] 所有用户输入都经过验证和过滤
- [ ] 所有输出都经过转义
- [ ] 敏感数据都经过加密存储
- [ ] 敏感信息都经过脱敏显示
- [ ] 所有API请求都包含CSRF Token
- [ ] 所有页面都配置了CSP策略
- [ ] 所有页面都配置了安全Headers
- [ ] 所有密码都符合复杂度要求
- [ ] 所有权限都经过验证
- [ ] 所有操作都记录了日志
### 12.2 部署前检查
- [ ] 所有依赖包都是最新版本
- [ ] 所有依赖包都没有已知漏洞
- [ ] 所有环境变量都正确配置
- [ ] 所有HTTPS证书都有效
- [ ] 所有监控和告警都正常工作
- [ ] 所有安全策略都正确配置
- [ ] 所有安全测试都通过
- [ ] 所有安全扫描都通过
- [ ] 所有安全文档都完整
- [ ] 所有安全培训都完成
---
## 十三、总结
本文档详细描述了健身房管理系统前端的安全规范,包括:
1. **安全概述**:安全目标、安全原则、安全威胁
2. **XSS防护**:输入验证、输出转义、CSP策略
3. **CSRF防护**Token验证、SameSite Cookie、双重Cookie提交
4. **数据安全**:数据加密、数据脱敏、敏感信息存储
5. **身份认证与授权**:认证安全、授权安全
6. **安全Headers**:基础安全Headers、动态设置Headers
7. **点击劫持防护**X-Frame-Options、CSP frame-ancestors
8. **安全键盘**:安全键盘组件
9. **安全日志与监控**:操作日志、异常监控
10. **安全最佳实践**:开发阶段、测试阶段、部署阶段
11. **合规性要求**GDPR合规、无障碍合规
12. **安全检查清单**:代码提交前检查、部署前检查
通过遵循本文档的安全规范,可以确保健身房管理系统前端的安全性,符合金融级安全标准和监管要求。
+928
View File
@@ -0,0 +1,928 @@
# 健身房管理系统前端工程化建设文档
> 文档编号: GYM-FE-ENG-001
> 版本: v1.0
> 日期: 2026-03-04
> 作者: 张翔
> 状态: 初稿
---
## 文档修订历史
| 版本 | 日期 | 作者 | 修订内容 |
| ---- | ---------- | ---- | -------- |
| v1.0 | 2026-03-04 | 张翔 | 创建前端工程化建设文档 |
---
## 参考文档
- 《健身房管理系统前端技术架构详细设计》 GYM-FE-ARCH-001
- 《健身房管理系统前端开发规范》 GYM-FE-DEV-001
- Vite 官方文档
- GitHub Actions 文档
---
## 一、工程化概述
### 1.1 工程化目标
- **提高开发效率**:自动化重复性工作,减少手动操作
- **保证代码质量**:通过自动化检查和测试,确保代码质量
- **统一开发规范**:通过工具强制执行代码规范
- **简化部署流程**:自动化构建和部署,减少人为错误
- **提升团队协作**:统一开发环境和工具链
### 1.2 工程化体系
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 前端工程化体系 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 开发工具链 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • Node.js • npm/yarn • Git • VSCode │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 构建工具 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • Vite • TypeScript • ESLint • Prettier │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 代码质量工具 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • Husky • Commitlint • Lint-staged • Stylelint │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 测试工具 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • Vitest • Playwright • Coverage • Testing Library │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ CI/CD工具 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • GitHub Actions • Docker • Nginx • CDN │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
---
## 二、构建工具配置
### 2.1 Vite配置
#### 2.1.1 基础配置
```typescript
// vite.config.ts
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd())
return {
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'@components': resolve(__dirname, 'src/components'),
'@utils': resolve(__dirname, 'src/utils'),
'@api': resolve(__dirname, 'src/api'),
'@stores': resolve(__dirname, 'src/stores')
}
},
server: {
port: 5173,
host: true,
open: true,
proxy: {
'/api': {
target: env.VITE_API_BASE_URL,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
build: {
outDir: 'dist',
assetsDir: 'assets',
sourcemap: mode === 'development',
minify: 'terser',
terserOptions: {
compress: {
drop_console: mode === 'production',
drop_debugger: mode === 'production',
pure_funcs: mode === 'production' ? ['console.log', 'console.info'] : []
}
},
rollupOptions: {
output: {
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'element-plus': ['element-plus'],
'utils': ['lodash-es', 'dayjs'],
'crypto': ['crypto-js', 'jsencrypt']
}
}
},
chunkSizeWarningLimit: 1000
},
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "@/assets/styles/variables.scss";`
}
}
}
}
})
```
#### 2.1.2 插件配置
```typescript
// vite.config.ts
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import Compression from 'vite-plugin-compression'
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
vue(),
AutoImport({
imports: ['vue', 'vue-router', 'pinia'],
dts: 'src/auto-imports.d.ts',
eslintrc: {
enabled: true
}
}),
Components({
resolvers: [ElementPlusResolver()],
dts: 'src/components.d.ts'
}),
Compression({
verbose: true,
disable: false,
threshold: 10240,
algorithm: 'gzip',
ext: '.gz'
}),
visualizer({
open: false,
gzipSize: true,
brotliSize: true
})
]
})
```
### 2.2 TypeScript配置
```json
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@utils/*": ["src/utils/*"],
"@api/*": ["src/api/*"],
"@stores/*": ["src/stores/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}
```
### 2.3 环境变量配置
```typescript
// .env.development
VITE_APP_TITLE=()
VITE_API_BASE_URL=http://localhost:8080/api
VITE_UPLOAD_URL=http://localhost:8080/upload
VITE_WS_URL=ws://localhost:8080/ws
VITE_SENTRY_DSN=
VITE_CRYPTO_SECRET_KEY=your-secret-key-here
VITE_RSA_PUBLIC_KEY=your-rsa-public-key-here
// .env.production
VITE_APP_TITLE=
VITE_API_BASE_URL=https://api.example.com/api
VITE_UPLOAD_URL=https://api.example.com/upload
VITE_WS_URL=wss://api.example.com/ws
VITE_SENTRY_DSN=https://xxx@sentry.io/xxx
VITE_CRYPTO_SECRET_KEY=your-production-secret-key-here
VITE_RSA_PUBLIC_KEY=your-production-rsa-public-key-here
// .env.staging
VITE_APP_TITLE=()
VITE_API_BASE_URL=https://staging-api.example.com/api
VITE_UPLOAD_URL=https://staging-api.example.com/upload
VITE_WS_URL=wss://staging-api.example.com/ws
VITE_SENTRY_DSN=https://xxx@sentry.io/xxx
VITE_CRYPTO_SECRET_KEY=your-staging-secret-key-here
VITE_RSA_PUBLIC_KEY=your-staging-rsa-public-key-here
```
---
## 三、代码规范工具
### 3.1 ESLint配置
```json
// .eslintrc.json
{
"extends": [
"plugin:vue/vue3-recommended",
"plugin:@typescript-eslint/recommended",
"prettier",
"plugin:prettier/recommended"
],
"parser": "vue-eslint-parser",
"parserOptions": {
"ecmaVersion": "latest",
"parser": "@typescript-eslint/parser",
"sourceType": "module"
},
"plugins": ["vue", "@typescript-eslint", "prettier"],
"rules": {
"vue/multi-word-component-names": "off",
"vue/no-v-html": "warn",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}
],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"no-console": [
"warn",
{
"allow": ["warn", "error"]
}
],
"no-debugger": "error",
"prettier/prettier": "error"
}
}
```
### 3.2 Prettier配置
```json
// .prettierrc
{
"semi": false,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "es5",
"arrowParens": "avoid",
"endOfLine": "lf",
"tabWidth": 2,
"useTabs": false,
"bracketSpacing": true,
"jsxSingleQuote": false,
"proseWrap": "preserve"
}
```
### 3.3 Stylelint配置
```json
// .stylelintrc.json
{
"extends": ["stylelint-config-standard", "stylelint-config-prettier"],
"rules": {
"selector-class-pattern": "^[a-z][a-zA-Z0-9-__]*$",
"selector-pseudo-class-no-unknown": [
true,
{
"ignorePseudoClasses": ["deep", "global"]
}
],
"selector-pseudo-element-no-unknown": [
true,
{
"ignorePseudoElements": ["v-deep", "v-global", "v-slotted"]
}
]
}
}
```
---
## 四、自动化工具
### 4.1 Husky配置
```json
// package.json
{
"scripts": {
"prepare": "husky install",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
"format": "prettier --write src/",
"lint:style": "stylelint \"src/**/*.{css,scss,vue}\" --fix"
}
}
```
```bash
# 初始化Husky
npx husky install
# 添加pre-commit钩子
npx husky add .husky/pre-commit "npx lint-staged"
# 添加commit-msg钩子
npx husky add .husky/commit-msg "npx commitlint --edit $1"
```
### 4.2 Lint-staged配置
```json
// .lintstagedrc.json
{
"*.{js,jsx,ts,tsx,vue}": [
"eslint --fix",
"prettier --write"
],
"*.{css,scss,vue}": [
"stylelint --fix"
],
"*.{json,md}": [
"prettier --write"
]
}
```
### 4.3 Commitlint配置
```javascript
// commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'chore', 'ci']
],
'type-case': [2, 'always', 'lower-case'],
'type-empty': [2, 'never'],
'scope-case': [2, 'always', 'lower-case'],
'subject-case': [2, 'never', ['sentence-case', 'start-case', 'pascal-case', 'upper-case']],
'subject-empty': [2, 'never'],
'subject-full-stop': [2, 'never', '.'],
'header-max-length': [2, 'always', 100]
}
}
```
---
## 五、CI/CD流程
### 5.1 GitHub Actions配置
```yaml
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run ESLint
run: npm run lint
- name: Run Prettier check
run: npm run format:check
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm run test:unit
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/coverage-final.json
fail_ci_if_error: true
build:
runs-on: ubuntu-latest
needs: [lint, test]
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: dist
path: dist/
```
### 5.2 CD配置
```yaml
# .github/workflows/cd.yml
name: CD
on:
push:
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Deploy to server
uses: easingthemes/ssh-deploy@v3
with:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
REMOTE_HOST: ${{ secrets.REMOTE_HOST }}
REMOTE_USER: ${{ secrets.REMOTE_USER }}
TARGET: /var/www/gym-manage/frontend
SOURCE: dist/
```
---
## 六、项目脚手架
### 6.1 项目初始化
```bash
# 创建新项目
npm create vite@latest gym-manage-frontend -- --template vue-ts
# 进入项目目录
cd gym-manage-frontend
# 安装依赖
npm install
# 安装开发依赖
npm install -D \
@vitejs/plugin-vue \
unplugin-auto-import \
unplugin-vue-components \
sass \
eslint \
@typescript-eslint/parser \
@typescript-eslint/eslint-plugin \
eslint-plugin-vue \
prettier \
eslint-config-prettier \
eslint-plugin-prettier \
husky \
lint-staged \
@commitlint/cli \
@commitlint/config-conventional \
vitest \
@vue/test-utils \
@playwright/test \
rollup-plugin-visualizer \
vite-plugin-compression
# 安装生产依赖
npm install \
vue \
vue-router \
pinia \
axios \
dayjs \
lodash-es \
element-plus \
dompurify \
crypto-js \
jsencrypt \
web-vitals
```
### 6.2 目录结构初始化
```bash
# 创建目录结构
mkdir -p src/{api,assets/{images,icons,styles},components/{base,business,layout},composables,config,directives,hooks,layouts,router,stores,types,utils,views}
mkdir -p src/test/{unit,e2e}
mkdir -p public
# 创建配置文件
touch .env.development .env.production .env.staging
touch .eslintrc.json .prettierrc .stylelintrc.json
touch tsconfig.json tsconfig.node.json
```
### 6.3 基础文件创建
```typescript
// src/main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
import router from './router'
import './assets/styles/main.scss'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.use(router)
app.use(ElementPlus)
app.mount('#app')
```
```typescript
// src/App.vue
<template>
<router-view />
</template>
<script setup lang="ts">
</script>
<style>
#app {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
</style>
```
---
## 七、开发工具链
### 7.1 VSCode配置
```json
// .vscode/settings.json
{
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.fixAll.stylelint": true
},
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue"
],
"typescript.tsdk": "node_modules/typescript/lib",
"volar.takeOverMode.enabled": true,
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
```
```json
// .vscode/extensions.json
{
"recommendations": [
"vue.volar",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"stylelint.vscode-stylelint",
"bradlc.vscode-tailwindcss",
"eamodio.gitlens"
]
}
```
### 7.2 Git配置
```bash
# .gitignore
node_modules
dist
dist-ssr
*.local
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json
.DS_Store
*.log
coverage
.nyc_output
.env.local
.env.*.local
```
### 7.3 NPM脚本
```json
// package.json
{
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"build:staging": "vue-tsc && vite build --mode staging",
"preview": "vite preview",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
"lint:style": "stylelint \"src/**/*.{css,scss,vue}\" --fix",
"format": "prettier --write src/",
"format:check": "prettier --check src/",
"test": "vitest",
"test:unit": "vitest run",
"test:coverage": "vitest run --coverage",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:headed": "playwright test --headed",
"type-check": "vue-tsc --noEmit",
"prepare": "husky install"
}
}
```
---
## 八、最佳实践
### 8.1 依赖管理
#### 8.1.1 依赖版本管理
```json
// package.json
{
"dependencies": {
"vue": "^3.4.0",
"vue-router": "^4.2.0",
"pinia": "^2.1.0"
},
"devDependencies": {
"vite": "^5.0.0",
"typescript": "^5.0.0",
"eslint": "^8.56.0"
}
}
```
#### 8.1.2 依赖安全检查
```bash
# 检查依赖漏洞
npm audit
# 自动修复依赖漏洞
npm audit fix
# 强制修复依赖漏洞
npm audit fix --force
```
### 8.2 性能监控
#### 8.2.1 构建分析
```bash
# 生成构建分析报告
npm run build
# 查看分析报告
open stats.html
```
#### 8.2.2 Bundle大小优化
```typescript
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'element-plus': ['element-plus'],
'utils': ['lodash-es', 'dayjs']
}
}
},
chunkSizeWarningLimit: 500
}
})
```
### 8.3 文档管理
#### 8.3.1 README文档
```markdown
# 健身房管理系统前端
## 项目介绍
健身房管理系统前端项目,基于Vue3 + Vite + TypeScript构建。
## 技术栈
- Vue 3.4+
- TypeScript 5.0+
- Vite 5.0+
- Pinia 2.1+
- Element Plus 2.5+
## 快速开始
### 安装依赖
\`\`\`bash
npm install
\`\`\`
### 开发
\`\`\`bash
npm run dev
\`\`\`
### 构建
\`\`\`bash
npm run build
\`\`\`
### 测试
\`\`\`bash
npm run test
\`\`\`
## 项目结构
\`\`\`
src/
├── api/ # API接口
├── assets/ # 静态资源
├── components/ # 组件
├── composables/ # Composables
├── config/ # 配置
├── router/ # 路由
├── stores/ # 状态管理
├── types/ # 类型定义
├── utils/ # 工具函数
└── views/ # 页面
\`\`\`
## 开发规范
详见 [前端开发规范](./docs/design/前端开发规范.md)
## 许可证
MIT
```
#### 8.3.2 CHANGELOG文档
```markdown
# Changelog
## [1.0.0] - 2026-03-04
### Added
- 会员管理功能
- 课程预约功能
- 扫码签到功能
- 数据统计功能
### Changed
- 升级Vue到3.4版本
- 优化构建配置
### Fixed
- 修复预约时间冲突问题
- 修复签到记录显示问题
### Security
- 添加XSS防护
- 添加CSRF防护
```
---
## 九、总结
本文档详细描述了健身房管理系统前端的工程化建设,包括:
1. **工程化概述**:工程化目标、工程化体系
2. **构建工具配置**Vite配置、TypeScript配置、环境变量配置
3. **代码规范工具**ESLint配置、Prettier配置、Stylelint配置
4. **自动化工具**Husky配置、Lint-staged配置、Commitlint配置
5. **CI/CD流程**GitHub Actions配置、CD配置
6. **项目脚手架**:项目初始化、目录结构初始化、基础文件创建
7. **开发工具链**VSCode配置、Git配置、NPM脚本
8. **最佳实践**:依赖管理、性能监控、文档管理
通过遵循本文档的工程化建设指南,可以建立完善的前端工程化体系,提高开发效率、保证代码质量、简化部署流程。
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+924
View File
@@ -0,0 +1,924 @@
# 健身房管理系统前端测试规范文档
> 文档编号: GYM-FE-TEST-001
> 版本: v1.0
> 日期: 2026-03-04
> 作者: 张翔
> 状态: 初稿
---
## 文档修订历史
| 版本 | 日期 | 作者 | 修订内容 |
| ---- | ---------- | ---- | -------- |
| v1.0 | 2026-03-04 | 张翔 | 创建前端测试规范 |
---
## 参考文档
- 《健身房管理系统前端技术架构详细设计》 GYM-FE-ARCH-001
- Vue Test Utils
- Vitest
- Playwright
---
## 一、测试概述
### 1.1 测试目标
- **代码质量**:确保代码质量,减少bug
- **功能正确性**:验证功能符合需求
- **回归测试**:防止新代码破坏现有功能
- **文档作用**:测试用例作为代码的使用文档
- **重构信心**:为重构提供安全保障
### 1.2 测试金字塔
```
/\
/E2E\ 少量端到端测试
/------\ (关键业务流程)
/ \
/Integration\ 适量集成测试
/------------\ (API集成、状态管理)
/ \
/ Unit Tests \ 大量单元测试
/------------------\ (组件、工具函数、Hooks)
```
| 测试类型 | 数量比例 | 执行速度 | 成本 | 价值 |
|---------|---------|----------|------|------|
| **单元测试** | 70% | 快 | 低 | 高 |
| **集成测试** | 20% | 中 | 中 | 中 |
| **E2E测试** | 10% | 慢 | 高 | 高 |
### 1.3 测试覆盖率目标
| 指标 | 目标值 | 说明 |
|------|--------|------|
| **代码覆盖率** | ≥ 80% | 所有代码的测试覆盖率 |
| **分支覆盖率** | ≥ 75% | 条件分支的测试覆盖率 |
| **函数覆盖率** | ≥ 90% | 函数的测试覆盖率 |
| **语句覆盖率** | ≥ 85% | 语句的测试覆盖率 |
---
## 二、单元测试
### 2.1 测试框架
#### 2.1.1 Vitest配置
```typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'src/test/',
'**/*.d.ts',
'**/*.config.*',
'**/mockData',
'src/main.ts'
]
}
},
resolve: {
alias: {
'@': resolve(__dirname, './src')
}
}
})
```
#### 2.1.2 测试环境设置
```typescript
// src/test/setup.ts
import { vi } from 'vitest'
import { config } from '@vue/test-utils'
config.global.stubs = {
'router-link': true,
'router-view': true
}
// Mock localStorage
const localStorageMock = {
getItem: vi.fn(),
setItem: vi.fn(),
removeItem: vi.fn(),
clear: vi.fn(),
length: 0,
key: vi.fn()
}
global.localStorage = localStorageMock as any
```
### 2.2 组件测试
#### 2.2.1 基础组件测试
```typescript
// components/base/Button.spec.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Button from '@/components/base/Button.vue'
describe('Button', () => {
it('renders correctly', () => {
const wrapper = mount(Button, {
slots: {
default: 'Click me'
}
})
expect(wrapper.text()).toBe('Click me')
})
it('emits click event', async () => {
const wrapper = mount(Button)
await wrapper.trigger('click')
expect(wrapper.emitted('click')).toBeTruthy()
})
it('applies type prop', () => {
const wrapper = mount(Button, {
props: { type: 'primary' }
})
expect(wrapper.classes()).toContain('button--primary')
})
it('disables button when disabled prop is true', () => {
const wrapper = mount(Button, {
props: { disabled: true }
})
expect(wrapper.attributes('disabled')).toBeDefined()
})
it('shows loading state when loading prop is true', () => {
const wrapper = mount(Button, {
props: { loading: true }
})
expect(wrapper.find('.loading-spinner').exists()).toBe(true)
})
})
```
#### 2.2.2 业务组件测试
```typescript
// components/business/MemberCard.spec.ts
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import MemberCard from '@/components/business/MemberCard.vue'
describe('MemberCard', () => {
const mockMember = {
id: 1,
name: '张三',
phone: '138****1234',
level: 3,
avatar: 'https://example.com/avatar.jpg'
}
it('renders member information correctly', () => {
const wrapper = mount(MemberCard, {
props: { member: mockMember }
})
expect(wrapper.find('.member-name').text()).toBe('张三')
expect(wrapper.find('.member-phone').text()).toBe('138****1234')
expect(wrapper.find('.member-avatar').attributes('src')).toBe('https://example.com/avatar.jpg')
})
it('emits click event when clicked', async () => {
const wrapper = mount(MemberCard, {
props: { member: mockMember }
})
await wrapper.trigger('click')
expect(wrapper.emitted('click')).toBeTruthy()
expect(wrapper.emitted('click')[0]).toEqual([mockMember])
})
it('shows level badge', () => {
const wrapper = mount(MemberCard, {
props: { member: mockMember }
})
expect(wrapper.find('.level-badge').exists()).toBe(true)
expect(wrapper.find('.level-badge').text()).toContain('VIP')
})
})
```
### 2.3 工具函数测试
```typescript
// utils/validator.spec.ts
import { describe, it, expect } from 'vitest'
import { validatePhone, validateIdCard, validateEmail } from '@/utils/validator'
describe('Validator', () => {
describe('validatePhone', () => {
it('validates correct phone number', () => {
expect(validatePhone('13800138000')).toBe(true)
})
it('rejects invalid phone number', () => {
expect(validatePhone('12345')).toBe(false)
expect(validatePhone('1380013800')).toBe(false)
expect(validatePhone('138001380000')).toBe(false)
})
it('rejects empty string', () => {
expect(validatePhone('')).toBe(false)
})
})
describe('validateIdCard', () => {
it('validates correct ID card', () => {
expect(validateIdCard('110101199003077892')).toBe(true)
})
it('rejects invalid ID card', () => {
expect(validateIdCard('123456')).toBe(false)
expect(validateIdCard('11010119900307789')).toBe(false)
})
it('rejects empty string', () => {
expect(validateIdCard('')).toBe(false)
})
})
describe('validateEmail', () => {
it('validates correct email', () => {
expect(validateEmail('test@example.com')).toBe(true)
})
it('rejects invalid email', () => {
expect(validateEmail('test')).toBe(false)
expect(validateEmail('test@')).toBe(false)
expect(validateEmail('@example.com')).toBe(false)
})
it('rejects empty string', () => {
expect(validateEmail('')).toBe(false)
})
})
})
```
### 2.4 Composables测试
```typescript
// composables/useAuth.spec.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { useAuth } from '@/composables/useAuth'
import { setActivePinia, createPinia } from 'pinia'
describe('useAuth', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('initializes with empty user', () => {
const { user, isAuthenticated } = useAuth()
expect(user.value).toBeNull()
expect(isAuthenticated.value).toBe(false)
})
it('sets user on login', async () => {
const { user, isAuthenticated, login } = useAuth()
const mockUser = { id: 1, name: '张三' }
vi.spyOn(api, 'login').mockResolvedValue({ user: mockUser, token: 'mock-token' })
await login({ username: 'test', password: 'test' })
expect(user.value).toEqual(mockUser)
expect(isAuthenticated.value).toBe(true)
})
it('clears user on logout', () => {
const { user, isAuthenticated, logout } = useAuth()
logout()
expect(user.value).toBeNull()
expect(isAuthenticated.value).toBe(false)
})
})
```
### 2.5 Store测试
```typescript
// stores/auth.spec.ts
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useAuthStore } from '@/stores/auth'
describe('AuthStore', () => {
beforeEach(() => {
setActivePinia(createPinia())
vi.clearAllMocks()
})
it('initializes with default state', () => {
const store = useAuthStore()
expect(store.token).toBe('')
expect(store.user).toBeNull()
expect(store.isAuthenticated).toBe(false)
})
it('sets token and user on login', async () => {
const store = useAuthStore()
const mockResponse = {
token: 'mock-token',
user: { id: 1, name: '张三' }
}
vi.spyOn(api, 'login').mockResolvedValue(mockResponse)
await store.login({ username: 'test', password: 'test' })
expect(store.token).toBe('mock-token')
expect(store.user).toEqual(mockResponse.user)
expect(store.isAuthenticated).toBe(true)
})
it('clears state on logout', () => {
const store = useAuthStore()
store.logout()
expect(store.token).toBe('')
expect(store.user).toBeNull()
expect(store.isAuthenticated).toBe(false)
})
})
```
---
## 三、集成测试
### 3.1 API集成测试
```typescript
// api/modules/member.spec.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { memberApi } from '@/api/modules/member'
import request from '@/api/request'
describe('Member API', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('getList', () => {
it('fetches member list successfully', async () => {
const mockResponse = {
list: [
{ id: 1, name: '张三', phone: '138****1234' },
{ id: 2, name: '李四', phone: '139****5678' }
],
total: 2
}
vi.spyOn(request, 'get').mockResolvedValue(mockResponse)
const result = await memberApi.getList({ page: 1, pageSize: 10 })
expect(result).toEqual(mockResponse)
expect(request.get).toHaveBeenCalledWith('/member/list', {
params: { page: 1, pageSize: 10 }
})
})
it('handles API error', async () => {
const mockError = new Error('Network error')
vi.spyOn(request, 'get').mockRejectedValue(mockError)
await expect(memberApi.getList({ page: 1, pageSize: 10 }))
.rejects.toThrow('Network error')
})
})
describe('getDetail', () => {
it('fetches member detail successfully', async () => {
const mockMember = { id: 1, name: '张三', phone: '138****1234' }
vi.spyOn(request, 'get').mockResolvedValue(mockMember)
const result = await memberApi.getDetail(1)
expect(result).toEqual(mockMember)
expect(request.get).toHaveBeenCalledWith('/member/1')
})
})
})
```
### 3.2 路由集成测试
```typescript
// router/index.spec.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { createRouter, createWebHistory } from 'vue-router'
import routes from '@/router'
describe('Router', () => {
let router: any
beforeEach(() => {
router = createRouter({
history: createWebHistory(),
routes
})
})
it('navigates to member list', async () => {
await router.push('/member/list')
expect(router.currentRoute.value.path).toBe('/member/list')
})
it('requires authentication for protected routes', async () => {
await router.push('/member/profile')
expect(router.currentRoute.value.path).toBe('/login')
})
it('redirects to 404 for unknown routes', async () => {
await router.push('/unknown-route')
expect(router.currentRoute.value.path).toBe('/404')
})
})
```
---
## 四、E2E测试
### 4.1 Playwright配置
```typescript
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:5173',
trace: 'on-first-retry',
screenshot: 'only-on-failure'
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] }
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] }
}
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI
}
})
```
### 4.2 会员端E2E测试
```typescript
// e2e/member.spec.ts
import { test, expect } from '@playwright/test'
test.describe('Member Login', () => {
test('should login successfully with valid credentials', async ({ page }) => {
await page.goto('/')
await page.click('text=登录')
await page.fill('[data-testid="phone-input"]', '13800138000')
await page.fill('[data-testid="code-input"]', '123456')
await page.click('[data-testid="login-button"]')
await expect(page).toHaveURL('/home')
await expect(page.locator('[data-testid="user-avatar"]')).toBeVisible()
})
test('should show error with invalid credentials', async ({ page }) => {
await page.goto('/login')
await page.fill('[data-testid="phone-input"]', '13800138000')
await page.fill('[data-testid="code-input"]', '000000')
await page.click('[data-testid="login-button"]')
await expect(page.locator('[data-testid="error-message"]')).toBeVisible()
await expect(page.locator('[data-testid="error-message"]')).toContainText('验证码错误')
})
})
test.describe('Course Booking', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login')
await page.fill('[data-testid="phone-input"]', '13800138000')
await page.fill('[data-testid="code-input"]', '123456')
await page.click('[data-testid="login-button"]')
await page.waitForURL('/home')
})
test('should book a course successfully', async ({ page }) => {
await page.goto('/booking/list')
await page.click('[data-testid="course-card"]:first-child')
await page.click('[data-testid="book-button"]')
await expect(page.locator('[data-testid="success-modal"]')).toBeVisible()
await expect(page.locator('[data-testid="success-modal"]')).toContainText('预约成功')
})
test('should show error when course is full', async ({ page }) => {
await page.goto('/booking/list')
await page.click('[data-testid="course-card"][data-full="true"]')
await page.click('[data-testid="book-button"]')
await expect(page.locator('[data-testid="error-message"]')).toBeVisible()
await expect(page.locator('[data-testid="error-message"]')).toContainText('课程已满')
})
})
```
### 4.3 管理后台E2E测试
```typescript
// e2e/admin.spec.ts
import { test, expect } from '@playwright/test'
test.describe('Admin Dashboard', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/admin/login')
await page.fill('[data-testid="username-input"]', 'admin')
await page.fill('[data-testid="password-input"]', 'password123')
await page.click('[data-testid="login-button"]')
await page.waitForURL('/admin/dashboard')
})
test('should display member statistics', async ({ page }) => {
await expect(page.locator('[data-testid="total-members"]')).toBeVisible()
await expect(page.locator('[data-testid="active-members"]')).toBeVisible()
await expect(page.locator('[data-testid="new-members"]')).toBeVisible()
})
test('should navigate to member list', async ({ page }) => {
await page.click('[data-testid="member-menu"]')
await page.click('[data-testid="member-list-link"]')
await expect(page).toHaveURL('/admin/member/list')
await expect(page.locator('[data-testid="member-table"]')).toBeVisible()
})
test('should create new member', async ({ page }) => {
await page.goto('/admin/member/list')
await page.click('[data-testid="add-member-button"]')
await page.fill('[data-testid="name-input"]', '张三')
await page.fill('[data-testid="phone-input"]', '13800138000')
await page.click('[data-testid="submit-button"]')
await expect(page.locator('[data-testid="success-message"]')).toBeVisible()
await expect(page.locator('[data-testid="member-table"]')).toContainText('张三')
})
})
```
---
## 五、测试最佳实践
### 5.1 测试编写原则
#### 5.1.1 AAA模式
```typescript
// Arrange(准备)
const wrapper = mount(Component, {
props: { value: 10 }
})
// Act(执行)
await wrapper.trigger('click')
// Assert(断言)
expect(wrapper.emitted('click')).toBeTruthy()
```
#### 5.1.2 测试独立性
```typescript
// Bad: 测试之间有依赖
let wrapper: any
beforeEach(() => {
wrapper = mount(Component)
})
it('test 1', () => {
wrapper.setData({ count: 1 })
})
it('test 2', () => {
// 依赖test 1的结果
expect(wrapper.vm.count).toBe(1)
})
// Good: 每个测试独立
it('test 1', () => {
const wrapper = mount(Component)
wrapper.setData({ count: 1 })
expect(wrapper.vm.count).toBe(1)
})
it('test 2', () => {
const wrapper = mount(Component)
wrapper.setData({ count: 2 })
expect(wrapper.vm.count).toBe(2)
})
```
#### 5.1.3 测试可读性
```typescript
// Bad: 难以理解
it('works', () => {
const w = mount(C, { p: { a: 1 } })
w.vm.b = 2
expect(w.vm.c).toBe(3)
})
// Good: 清晰易懂
it('calculates sum correctly', () => {
const wrapper = mount(Calculator, {
props: { a: 1 }
})
wrapper.vm.b = 2
expect(wrapper.vm.sum).toBe(3)
})
```
### 5.2 Mock使用
#### 5.2.1 Mock API请求
```typescript
import { vi } from 'vitest'
import { memberApi } from '@/api/modules/member'
describe('MemberService', () => {
it('fetches member list', async () => {
const mockData = { list: [], total: 0 }
vi.spyOn(memberApi, 'getList').mockResolvedValue(mockData)
const result = await memberApi.getList({ page: 1, pageSize: 10 })
expect(result).toEqual(mockData)
expect(memberApi.getList).toHaveBeenCalledWith({ page: 1, pageSize: 10 })
})
})
```
#### 5.2.2 Mock组件
```typescript
import { mount } from '@vue/test-utils'
import ParentComponent from '@/components/ParentComponent.vue'
describe('ParentComponent', () => {
it('renders child component', () => {
const wrapper = mount(ParentComponent, {
global: {
stubs: {
ChildComponent: {
template: '<div>Mock Child</div>'
}
}
}
})
expect(wrapper.html()).toContain('Mock Child')
})
})
```
### 5.3 异步测试
```typescript
// 测试异步操作
it('handles async operation', async () => {
const wrapper = mount(Component)
await wrapper.find('.async-button').trigger('click')
// 等待异步操作完成
await wrapper.vm.$nextTick()
await new Promise(resolve => setTimeout(resolve, 100))
expect(wrapper.vm.data).toBe('loaded')
})
// 使用waitFor
it('waits for element to appear', async ({ page }) => {
await page.goto('/')
await page.click('.load-button')
await page.waitForSelector('.loaded-content')
await expect(page.locator('.loaded-content')).toBeVisible()
})
```
---
## 六、测试覆盖率
### 6.1 生成覆盖率报告
```bash
# 运行测试并生成覆盖率报告
npm run test:coverage
# 查看覆盖率报告
open coverage/index.html
```
### 6.2 覆盖率配置
```typescript
// vitest.config.ts
export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
lines: 85,
functions: 90,
branches: 75,
statements: 85,
exclude: [
'node_modules/',
'src/test/',
'**/*.d.ts',
'**/*.config.*',
'**/mockData',
'src/main.ts'
]
}
}
})
```
### 6.3 覆盖率目标
| 类型 | 目标 | 说明 |
|------|------|------|
| **Lines** | ≥ 85% | 代码行覆盖率 |
| **Functions** | ≥ 90% | 函数覆盖率 |
| **Branches** | ≥ 75% | 分支覆盖率 |
| **Statements** | ≥ 85% | 语句覆盖率 |
---
## 七、CI/CD集成
### 7.1 GitHub Actions配置
```yaml
# .github/workflows/test.yml
name: Test
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x, 20.x]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm run test:unit
- name: Run E2E tests
run: npm run test:e2e
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/coverage-final.json
```
### 7.2 测试命令
```json
// package.json
{
"scripts": {
"test": "vitest",
"test:unit": "vitest run",
"test:coverage": "vitest run --coverage",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:headed": "playwright test --headed"
}
}
```
---
## 八、测试检查清单
### 8.1 单元测试检查清单
- [ ] 组件渲染正确
- [ ] Props传递正确
- [ ] 事件触发正确
- [ ] 计算属性计算正确
- [ ] 方法执行正确
- [ ] 生命周期钩子执行正确
- [ ] 边界情况处理正确
- [ ] 错误处理完善
### 8.2 集成测试检查清单
- [ ] API调用正确
- [ ] 状态管理正确
- [ ] 路由导航正确
- [ ] 组件通信正确
- [ ] 数据流正确
- [ ] 错误处理完善
### 8.3 E2E测试检查清单
- [ ] 关键业务流程覆盖
- [ ] 用户操作流程正确
- [ ] 页面跳转正确
- [ ] 数据提交正确
- [ ] 错误提示正确
- [ ] 加载状态正确
---
## 九、总结
本文档详细描述了健身房管理系统前端的测试规范,包括:
1. **测试概述**:测试目标、测试金字塔、测试覆盖率目标
2. **单元测试**:测试框架、组件测试、工具函数测试、Composables测试、Store测试
3. **集成测试**API集成测试、路由集成测试
4. **E2E测试**Playwright配置、会员端E2E测试、管理后台E2E测试
5. **测试最佳实践**:测试编写原则、Mock使用、异步测试
6. **测试覆盖率**:生成覆盖率报告、覆盖率配置、覆盖率目标
7. **CI/CD集成**GitHub Actions配置、测试命令
8. **测试检查清单**:单元测试检查清单、集成测试检查清单、E2E测试检查清单
通过遵循本文档的测试规范,可以确保代码质量、减少bug、提高系统稳定性。