docs: reorganize documentation structure
This commit is contained in:
@@ -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-4:WebFlux + 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
@@ -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
@@ -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,服务等级协议 |
|
||||
|
||||
---
|
||||
|
||||
**文档结束**
|
||||
@@ -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. ✅ 自动化运维工具开发
|
||||
@@ -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)
|
||||
@@ -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> = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
}
|
||||
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. **安全检查清单**:代码提交前检查、部署前检查
|
||||
|
||||
通过遵循本文档的安全规范,可以确保健身房管理系统前端的安全性,符合金融级安全标准和监管要求。
|
||||
@@ -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
@@ -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、提高系统稳定性。
|
||||
Reference in New Issue
Block a user