docs: 统一文档日期和状态规范

This commit is contained in:
张翔
2026-03-08 22:00:52 +08:00
parent 0087a90b89
commit b9995785ab
22 changed files with 4983 additions and 13609 deletions
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -4,7 +4,7 @@
> 版本: v1.0 > 版本: v1.0
> 日期: 2026-03-04 > 日期: 2026-03-04
> 作者: 张翔 > 作者: 张翔
> 状态: 初稿 > 状态: 正式发布
--- ---
@@ -1,543 +0,0 @@
# 健身房管理系统付费订阅版业务概要设计文档(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
@@ -1,524 +0,0 @@
# 健身房管理系统基础版业务概要设计文档(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
---
## 二、业务概述
### 2.1 业务目标
| 目标维度 | 目标描述 | 成功指标 |
| -------- | ---------------------- | -------------------------------- |
| 用户体验 | 提升会员预约和签到体验 | 预约成功率 ≥ 95%,签到耗时 ≤ 3秒 |
| 运营效率 | 降低人工操作成本 | 人工处理时间减少 50% |
| 数据价值 | 提供基础数据支持 | 数据报表使用率 ≥ 80% |
### 2.2 用户角色
| 角色 | 描述 | 主要功能 |
| ---------- | -------------- | ---------------------------- |
| 会员 | 健身房注册用户 | 预约课程、签到、查看个人信息 |
| 教练 | 健身房教练 | 排课、团课签到管理 |
| 前台 | 门店前台人员 | 会员接待、签到辅助、会员管理 |
| 店长 | 门店管理者 | 单店全功能管理、数据查看 |
| 超级管理员 | 平台最高权限 | 全平台管理、系统配置 |
### 2.3 业务范围
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 基础版业务范围 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 会员管理 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 会员注册 • 会员卡管理 • 权益管理 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 预约管理 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 团课预约 • 团课管理 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 签到管理 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 扫码签到 • 签到记录管理 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 数据统计 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 基础数据统计 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 系统管理 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 用户管理 • 角色权限管理 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ UI模版定制 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 品牌定制 • 布局调整 • 预设模板 • 配置历史 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
---
## 三、核心业务流程
### 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 异常处理
| 异常场景 | 处理方式 |
| -------- | -------------------- |
| 支付失败 | 提示用户重新支付 |
| 支付超时 | 提示用户重新发起支付 |
---
### 3.5 UI模版定制流程
#### 3.5.1 业务场景
租户通过管理后台的可视化配置器定制自己的UI,包括品牌元素、布局结构和预设模板。
#### 3.5.2 业务流程
```
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ 租户登录 │ → │ 打开UI │ → │ 品牌定制 │ → │ 布局调整 │ → │ 配置保存 │
│ 管理后台 │ │ 定制器 │ │ │ │ │ │ │
└─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘
```
#### 3.5.3 业务规则
- 品牌元素应用范围包括小程序和管理后台
- 布局调整支持拖拽排序和模块隐藏
- 预设模板应用后保留品牌配置
- 配置变更实时生效,无需重新部署
- 配置变更自动记录到历史
#### 3.5.4 异常处理
| 异常场景 | 处理方式 |
| ------------ | -------------------- |
| Logo上传失败 | 提示用户重新上传 |
| 配置保存失败 | 提示用户检查配置格式 |
| 模板应用失败 | 提示用户调整品牌配置 |
| 配置回滚失败 | 提示用户选择其他版本 |
---
## 四、核心业务规则
### 4.1 会员管理规则
| 规则 | 描述 |
| ---------------- | ------------------------------------------------------ |
| 会员唯一性 | 手机号作为会员唯一标识 |
| 会员信息完整性 | 必填字段:手机号、姓名、性别 |
| 会员信息修改权限 | 会员只能编辑自己的基本信息,前台和店长可以编辑所有信息 |
### 4.2 会员卡管理规则
| 规则 | 描述 |
| -------------- | ------------------------------------ |
| 会员卡类型 | 支持时长卡、次卡、储值卡 |
| 会员卡有效期 | 时长卡有有效期,次卡和储值卡无有效期 |
| 会员卡到期提醒 | 到期前7天提醒 |
| 会员卡续费 | 续费后权益立即生效 |
### 4.3 预约管理规则
| 规则 | 描述 |
| ---------------- | ------------------------------- |
| 预约时间限制 | 预约需在课程开始前至少30分钟 |
| 取消预约时间限制 | 取消预约需在课程开始前至少2小时 |
| 团课容量限制 | 每节课最多20人 |
| 预约权益扣减 | 预约成功后扣减权益 |
### 4.4 签到管理规则
| 规则 | 描述 |
| ------------ | -------------------------- |
| 签到验证 | 签到需验证会员卡有效性 |
| 签到预约验证 | 签到需验证预约信息(如有) |
| 签到记录 | 签到成功后记录到店时间 |
### 4.5 数据统计规则
| 规则 | 描述 |
| ------------ | -------------------- |
| 数据保留期限 | 数据保留30天 |
| 统计维度 | 支持按日、周、月统计 |
| 数据导出 | 支持数据导出 |
### 4.6 UI模版定制规则
| 规则 | 描述 |
| ------------ | ------------------------------------------ |
| 品牌元素应用 | 品牌元素应用范围包括小程序和管理后台 |
| Logo格式限制 | Logo支持PNG/JPG格式,限制2MB以内 |
| 颜色格式限制 | 颜色支持RGB和HEX格式 |
| 布局调整权限 | 布局调整支持按角色区分(店长、前台、会员) |
| 模板应用规则 | 模板应用后保留租户已有的品牌配置 |
| 配置版本管理 | 每次配置变更自动生成新版本号 |
| 配置历史保留 | 配置历史保留90天 |
| 配置实时生效 | 配置变更实时生效,无需重新部署 |
---
## 五、业务场景
### 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) | ≤ 500ms |
| 并发用户 | 支持100并发用户 |
| 数据库查询 | 查询响应时间 ≤ 1s |
### 7.2 可用性约束
| 指标 | 要求 |
| ------------ | ------------- |
| 系统可用性 | SLA ≥ 99.9% |
| 故障恢复时间 | MTTR ≤ 30分钟 |
### 7.3 安全性约束
| 指标 | 要求 |
| -------- | -------------------- |
| 数据加密 | 敏感数据加密存储 |
| 访问控制 | 基于角色的访问控制 |
| 操作审计 | 关键操作记录审计日志 |
### 7.4 可扩展性约束
| 指标 | 要求 |
| -------- | -------------- |
| 会员数量 | 最多500人 |
| 门店数量 | 单门店 |
| 团课容量 | 每节课最多20人 |
| 数据保留 | 保留30天 |
---
## 八、附录
### 8.1 术语定义
| 术语 | 定义 |
| ------ | ------------------------------------------ |
| 会员 | 在健身房注册的用户 |
| 会员卡 | 会员购买的权益卡,包括时长卡、次卡、储值卡 |
| 权益 | 会员卡包含的时长、次数、储值、等级等权益 |
| 团课 | 集体课程,由教练带领多个会员一起上课 |
| 预约 | 会员预约团课 |
| 签到 | 会员到店记录 |
### 8.2 参考文档
- 《健身房管理系统基础版产品设计文档》 GYM-PRD-BASIC-001
File diff suppressed because it is too large Load Diff
+715 -39
View File
@@ -4,7 +4,7 @@
> 版本: v1.0 > 版本: v1.0
> 日期: 2026-03-04 > 日期: 2026-03-04
> 作者: 张翔 > 作者: 张翔
> 状态: 初稿 > 状态: 正式发布
--- ---
@@ -29,42 +29,18 @@
### 1.1 部署拓扑 ### 1.1 部署拓扑
``` ```mermaid
┌─────────────────────────────────────────────────────────────────┐ flowchart TB
│ 部署架构拓扑 │ subgraph 部署架构拓扑
├─────────────────────────────────────────────────────────────────┤ A[用户层<br/>• 会员小程序<br/>• 教练端App<br/>• 管理后台PC]
│ │ B[负载均衡层 Nginx<br/>• 负载均衡<br/>• SSL 终止<br/>• 静态资源<br/>• 限流]
│ ┌─────────────────────────────────────────────────────────┐ │ C[应用层 Docker Compose<br/>• gym-manage 应用<br/>• postgres 数据库<br/>• redis 缓存<br/>• rabbitmq 消息队列<br/>• elasticsearch 搜索引擎<br/>• prometheus 监控<br/>• grafana 可视化<br/>• kibana 日志可视化]
│ │ 用户层 │ │ D[监控层 Prometheus + Grafana<br/>• 指标采集<br/>• 告警规则<br/>• 可视化仪表板]
│ ├─────────────────────────────────────────────────────────┤ │ end
│ │ • 会员小程序 • 教练端App • 管理后台PC │ │
│ └─────────────────────────────────────────────────────────┘ │ A --> B
│ │ │ B --> C
│ ▼ │ C --> D
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 负载均衡层 (Nginx) │ │
│ ├─────────────────────────────────────────────────────────┤ │
│ │ • 负载均衡 • SSL 终止 • 静态资源 • 限流 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 应用层 (Docker Compose) │ │
│ ├─────────────────────────────────────────────────────────┤ │
│ │ • gym-manage (应用) • postgres (数据库) │ │
│ │ • redis (缓存) • rabbitmq (消息队列) │ │
│ │ • elasticsearch (搜索引擎) • prometheus (监控) │ │
│ │ • grafana (可视化) • kibana (日志可视化) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 监控层 (Prometheus + Grafana) │ │
│ ├─────────────────────────────────────────────────────────┤ │
│ │ • 指标采集 • 告警规则 • 可视化仪表板 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
``` ```
### 1.2 服务器配置 ### 1.2 服务器配置
@@ -436,7 +412,7 @@ docker-compose logs -f gym-manage
docker-compose logs --tail=100 gym-manage docker-compose logs --tail=100 gym-manage
# 查看特定时间的日志 # 查看特定时间的日志
docker-compose logs --since 2024-01-01T00:00:00 gym-manage docker-compose logs --since 2026-01-01T00:00:00 gym-manage
``` ```
#### 5.2.2 日志文件 #### 5.2.2 日志文件
@@ -776,7 +752,7 @@ crontab -e
docker-compose stop gym-manage docker-compose stop gym-manage
# 恢复数据库 # 恢复数据库
docker-compose exec -T postgres psql -U postgres gym_manage < backup/gym_manage_20240101_020000.sql docker-compose exec -T postgres psql -U postgres gym_manage < backup/gym_manage_20260101_020000.sql
# 启动应用 # 启动应用
docker-compose start gym-manage docker-compose start gym-manage
@@ -834,6 +810,706 @@ spring:
--- ---
## 六、监控告警详细配置
### 6.1 Prometheus 监控配置
#### 6.1.1 prometheus.yml 配置
**文件位置**: `monitoring/prometheus.yml`
```yaml
global:
scrape_interval: 15s # 采集间隔
evaluation_interval: 15s # 规则评估间隔
external_labels:
monitor: 'gym-manage'
environment: 'production'
# 告警规则配置
rule_files:
- "alerts.yml"
# 告警管理器配置
alerting:
alertmanagers:
- static_configs:
- targets:
- alertmanager:9093
# 采集配置
scrape_configs:
# Prometheus 自监控
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
labels:
instance: 'prometheus-server'
# 应用监控
- job_name: 'gym-manage'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['gym-manage:8080']
labels:
application: 'gym-manage'
environment: 'production'
scrape_interval: 10s
# Node 导出器
- job_name: 'node-exporter'
static_configs:
- targets: ['node-exporter:9100']
labels:
instance: 'server-node'
# Redis 导出器
- job_name: 'redis-exporter'
static_configs:
- targets: ['redis-exporter:9121']
labels:
instance: 'redis-server'
# PostgreSQL 导出器
- job_name: 'postgres-exporter'
static_configs:
- targets: ['postgres-exporter:9187']
labels:
instance: 'postgres-server'
# RabbitMQ 导出器
- job_name: 'rabbitmq-exporter'
static_configs:
- targets: ['rabbitmq-exporter:9419']
labels:
instance: 'rabbitmq-server'
```
#### 6.1.2 alerts.yml 告警规则
**文件位置**: `monitoring/alerts.yml`
```yaml
groups:
- name: gym-manage-alerts
interval: 30s
rules:
# 应用可用性告警
- alert: ApplicationDown
expr: up{job="gym-manage"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "应用不可用"
description: "应用 {{ $labels.instance }} 已宕机超过 1 分钟"
# 高错误率告警
- alert: HighErrorRate
expr: sum(rate(http_server_requests_seconds_count{status=~"5..", job="gym-manage"}[5m])) / sum(rate(http_server_requests_seconds_count{job="gym-manage"}[5m])) > 0.05
for: 5m
labels:
severity: warning
annotations:
summary: "高错误率"
description: "应用错误率超过 5% (当前值:{{ $value | humanizePercentage }})"
# 高响应时间告警
- alert: HighResponseTime
expr: histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket{job="gym-manage"}[5m])) by (le)) > 1
for: 5m
labels:
severity: warning
annotations:
summary: "高响应时间"
description: "应用 P95 响应时间超过 1 秒 (当前值:{{ $value | humanizeDuration }})"
# 高内存使用率告警
- alert: HighMemoryUsage
expr: (jvm_memory_used_bytes{area="heap", job="gym-manage"} / jvm_memory_max_bytes{area="heap", job="gym-manage"}) > 0.85
for: 5m
labels:
severity: warning
annotations:
summary: "高内存使用率"
description: "JVM 堆内存使用率超过 85% (当前值:{{ $value | humanizePercentage }})"
# OOM 告警
- alert: OutOfMemory
expr: (jvm_memory_used_bytes{area="heap", job="gym-manage"} / jvm_memory_max_bytes{area="heap", job="gym-manage"}) > 0.95
for: 2m
labels:
severity: critical
annotations:
summary: "内存即将耗尽"
description: "JVM 堆内存使用率超过 95% (当前值:{{ $value | humanizePercentage }})"
# 数据库连接池耗尽告警
- alert: DatabaseConnectionPoolExhausted
expr: hikaricp_active_connections{job="gym-manage"} / hikaricp_max_connections{job="gym-manage"} > 0.9
for: 5m
labels:
severity: warning
annotations:
summary: "数据库连接池耗尽"
description: "数据库连接池使用率超过 90% (当前值:{{ $value | humanizePercentage }})"
# Redis 连接失败告警
- alert: RedisConnectionFailed
expr: redis_up{job="redis-exporter"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Redis 连接失败"
description: "Redis {{ $labels.instance }} 连接失败"
# PostgreSQL 连接失败告警
- alert: PostgresConnectionFailed
expr: pg_up{job="postgres-exporter"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "PostgreSQL 连接失败"
description: "PostgreSQL {{ $labels.instance }} 连接失败"
# RabbitMQ 队列堆积告警
- alert: RabbitMQQueueBacklog
expr: rabbitmq_queue_messages{job="rabbitmq-exporter"} > 1000
for: 5m
labels:
severity: warning
annotations:
summary: "消息队列堆积"
description: "队列 {{ $labels.queue }} 消息数量超过 1000 (当前值:{{ $value }})"
# 磁盘空间不足告警
- alert: DiskSpaceLow
expr: (node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}) < 0.15
for: 5m
labels:
severity: warning
annotations:
summary: "磁盘空间不足"
description: "服务器 {{ $labels.instance }} 根分区磁盘空间不足 15% (当前值:{{ $value | humanizePercentage }})"
# CPU 使用率过高告警
- alert: HighCPUUsage
expr: 100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 85
for: 10m
labels:
severity: warning
annotations:
summary: "CPU 使用率过高"
description: "服务器 {{ $labels.instance }} CPU 使用率超过 85% (当前值:{{ $value | humanize }}%)"
```
### 6.2 Grafana 仪表板配置
#### 6.2.1 应用监控仪表板
**仪表板 ID**: `gym-manage-overview`
**主要面板**:
1. **应用健康状态**
- 应用在线状态
- 健康检查状态
- 运行时长
2. **流量指标**
- QPS (每秒请求数)
- 并发连接数
- 网络吞吐量
3. **响应时间**
- 平均响应时间
- P95 响应时间
- P99 响应时间
4. **错误率**
- HTTP 5xx 错误率
- HTTP 4xx 错误率
- 业务错误率
5. **JVM 指标**
- 堆内存使用率
- 非堆内存使用率
- GC 次数和时间
- 线程数
6. **数据库连接池**
- 活跃连接数
- 空闲连接数
- 连接池使用率
- 平均获取连接时间
7. **Redis 缓存**
- 缓存命中率
- 缓存键数量
- 内存使用量
- 命令执行时间
8. **消息队列**
- 队列消息数量
- 消息生产速率
- 消息消费速率
- 消息堆积情况
#### 6.2.2 系统监控仪表板
**仪表板 ID**: `system-overview`
**主要面板**:
1. **CPU 指标**
- CPU 使用率
- CPU 负载 (1/5/15 分钟)
- CPU 核心数
2. **内存指标**
- 内存使用率
- 可用内存
- Swap 使用率
3. **磁盘指标**
- 磁盘使用率
- 磁盘 I/O
- 磁盘读写速率
4. **网络指标**
- 网络流量
- 网络连接数
- 网络错误率
### 6.3 告警通知配置
#### 6.3.1 Alertmanager 配置
**文件位置**: `monitoring/alertmanager.yml`
```yaml
global:
# 邮件配置
smtp_smarthost: 'smtp.example.com:587'
smtp_from: 'alertmanager@example.com'
smtp_auth_username: 'alertmanager@example.com'
smtp_auth_password: 'your-password'
# 钉钉配置
dingtalk_configs:
- url: 'https://oapi.dingtalk.com/robot/send?access_token=YOUR_TOKEN'
secret: 'YOUR_SECRET'
send_resolved: true
# 企业微信配置
wechat_configs:
- corp_id: 'YOUR_CORP_ID'
agent_id: 'YOUR_AGENT_ID'
secret: 'YOUR_SECRET'
to_user: '@all'
send_resolved: true
# 模板配置
templates:
- '/etc/alertmanager/templates/*.tmpl'
# 路由配置
route:
receiver: 'default-receiver'
group_by: ['alertname', 'severity']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
routes:
# 严重告警立即通知
- match:
severity: critical
receiver: 'critical-receiver'
group_wait: 10s
repeat_interval: 1h
# 警告告警延迟通知
- match:
severity: warning
receiver: 'warning-receiver'
group_wait: 5m
repeat_interval: 4h
# 接收器配置
receivers:
- name: 'default-receiver'
email_configs:
- to: 'devops-team@example.com'
send_resolved: true
- name: 'critical-receiver'
email_configs:
- to: 'oncall@example.com'
send_resolved: true
dingtalk_configs:
- send_resolved: true
wechat_configs:
- send_resolved: true
- name: 'warning-receiver'
email_configs:
- to: 'dev-team@example.com'
send_resolved: true
# 抑制规则
inhibit_rules:
# 如果应用宕机,抑制其他告警
- source_match:
alertname: 'ApplicationDown'
target_match:
severity: 'warning'
equal: ['instance']
```
#### 6.3.2 告警升级策略
**升级规则**:
1. **P0 级别 (Critical)**
- 立即通知:钉钉 + 企业微信 + 短信 + 电话
- 15 分钟未响应:升级至技术总监
- 30 分钟未响应:升级至 CTO
2. **P1 级别 (Warning)**
- 立即通知:钉钉 + 企业微信
- 1 小时未响应:升级至部门经理
- 2 小时未响应:升级至技术总监
3. **P2 级别 (Info)**
- 工作时间通知:邮件
- 24 小时未处理:升级为 Warning
#### 6.3.3 告警值班安排
**值班表配置**:
```yaml
# 工作日值班
work_hours:
- Monday to Friday: 09:00-18:00
# 值班人员
on_call_schedule:
- name: "张三"
email: "zhangsan@example.com"
phone: "13800138000"
schedule: "周一,周三"
- name: "李四"
email: "lisi@example.com"
phone: "13900139000"
schedule: "周二,周四"
- name: "王五"
email: "wangwu@example.com"
phone: "13700137000"
schedule: "周五"
# 周末值班
weekend_on_call:
- name: "值班团队"
email: "weekend-team@example.com"
phone: "400-xxx-xxxx"
```
---
## 七、备份恢复详细策略
### 7.1 备份策略
#### 7.1.1 备份类型
**全量备份**:
- 频率:每日凌晨 2 点
- 保留期限:30 天
- 备份内容:完整数据库、配置文件
**增量备份**:
- 频率:每小时
- 保留期限:7 天
- 备份内容:WAL 日志、变更数据
**差异备份**:
- 频率:每 6 小时
- 保留期限:7 天
- 备份内容:自上次全量备份后的变更
#### 7.1.2 备份内容
**数据库备份**:
```bash
# PostgreSQL 全量备份脚本
#!/bin/bash
BACKUP_DIR="/backup/postgres"
DATE=$(date +%Y%m%d_%H%M%S)
DB_NAME="gym_manage"
DB_USER="postgres"
# 创建备份目录
mkdir -p ${BACKUP_DIR}
# 全量备份
pg_dump -U ${DB_USER} -h localhost ${DB_NAME} | gzip > ${BACKUP_DIR}/${DB_NAME}_${DATE}.sql.gz
# 备份 WAL 日志
# 配置 postgresql.conf:
# wal_level = replica
# archive_mode = on
# archive_command = 'cp %p /backup/wal/%f'
# 清理旧备份 (保留 30 天)
find ${BACKUP_DIR} -name "*.sql.gz" -mtime +30 -delete
```
**配置文件备份**:
```bash
# 备份应用配置
#!/bin/bash
BACKUP_DIR="/backup/config"
DATE=$(date +%Y%m%d_%H%M%S)
# 备份配置文件
tar -czf ${BACKUP_DIR}/config_${DATE}.tar.gz application-prod.yml docker-compose.yml nginx/nginx.conf monitoring/prometheus.yml monitoring/alerts.yml
# 备份环境变量
docker-compose exec gym-manage env > ${BACKUP_DIR}/env_${DATE}.txt
```
**数据文件备份**:
```bash
# 备份 Redis 数据
#!/bin/bash
BACKUP_DIR="/backup/redis"
DATE=$(date +%Y%m%d_%H%M%S)
# 触发 RDB 保存
docker-compose exec redis redis-cli BGSAVE
# 等待保存完成
sleep 5
# 复制 RDB 文件
docker cp gym-manage-redis:/data/dump.rdb ${BACKUP_DIR}/dump_${DATE}.rdb
# 备份 Elasticsearch 数据
docker-compose exec elasticsearch elasticsearch-snapshot -repository backup -snapshot gym_manage_${DATE}
```
#### 7.1.3 备份验证
**定期验证**:
- 频率:每周日凌晨 3 点
- 内容:验证备份文件完整性
- 方法:恢复测试
```bash
# 备份验证脚本
#!/bin/bash
BACKUP_DIR="/backup/postgres"
LATEST_BACKUP=$(ls -t ${BACKUP_DIR}/*.sql.gz | head -1)
# 验证备份文件完整性
if gzip -t ${LATEST_BACKUP}; then
echo "备份文件完整: ${LATEST_BACKUP}"
else
echo "备份文件损坏: ${LATEST_BACKUP}"
# 发送告警
curl -X POST "https://alert.example.com/backup-failed"
fi
# 恢复测试 (在测试环境)
# gunzip -c ${LATEST_BACKUP} | psql -U postgres -h test-db gym_manage_test
```
### 7.2 恢复策略
#### 7.2.1 恢复优先级
**P0 - 核心业务恢复** (RTO ≤ 30 分钟):
1. 数据库恢复
2. 应用服务恢复
3. 缓存恢复
**P1 - 重要业务恢复** (RTO ≤ 2 小时):
4. 消息队列恢复
5. 搜索引擎恢复
6. 日志系统恢复
**P2 - 辅助业务恢复** (RTO ≤ 4 小时):
7. 监控系统恢复
8. 报表系统恢复
9. 备份系统恢复
#### 7.2.2 数据库恢复流程
**完整恢复流程**:
```bash
#!/bin/bash
# 数据库恢复脚本
BACKUP_FILE=$1
DB_NAME="gym_manage"
DB_USER="postgres"
echo "开始恢复数据库..."
# 1. 停止应用
echo "停止应用..."
docker-compose stop gym-manage
# 2. 创建临时数据库
echo "创建临时数据库..."
docker-compose exec postgres psql -U postgres -c "CREATE DATABASE ${DB_NAME}_restore;"
# 3. 恢复数据
echo "恢复数据..."
gunzip -c ${BACKUP_FILE} | docker-compose exec -T postgres psql -U postgres ${DB_NAME}_restore
# 4. 验证数据
echo "验证数据..."
docker-compose exec postgres psql -U postgres -d ${DB_NAME}_restore -c "SELECT COUNT(*) FROM members;"
# 5. 备份当前数据库 (如果有)
if docker-compose exec postgres psql -U postgres -lqt | cut -d \| -f 1 | grep -w ${DB_NAME}; then
echo "备份当前数据库..."
docker-compose exec postgres pg_dump -U postgres ${DB_NAME} | gzip > /backup/emergency_${DB_NAME}_$(date +%Y%m%d_%H%M%S).sql.gz
fi
# 6. 删除原数据库
echo "删除原数据库..."
docker-compose exec postgres psql -U postgres -c "DROP DATABASE ${DB_NAME};"
# 7. 重命名恢复的数据库
echo "重命名数据库..."
docker-compose exec postgres psql -U postgres -c "ALTER DATABASE ${DB_NAME}_restore RENAME TO ${DB_NAME};"
# 8. 启动应用
echo "启动应用..."
docker-compose start gym-manage
# 9. 验证应用
echo "验证应用..."
sleep 10
curl -f http://localhost:8080/actuator/health
echo "数据库恢复完成!"
```
#### 7.2.3 应用恢复流程
```bash
#!/bin/bash
# 应用恢复脚本
echo "开始恢复应用..."
# 1. 停止应用
docker-compose stop gym-manage
# 2. 清理旧容器
docker-compose rm -f gym-manage
# 3. 拉取最新镜像
docker-compose pull gym-manage
# 4. 恢复配置
cp backup/application/application-prod.yml.bak ./config/application-prod.yml
# 5. 启动应用
docker-compose up -d gym-manage
# 6. 等待启动
sleep 30
# 7. 健康检查
curl -f http://localhost:8080/actuator/health || exit 1
echo "应用恢复完成!"
```
#### 7.2.4 缓存恢复流程
```bash
#!/bin/bash
# Redis 恢复脚本
echo "开始恢复 Redis..."
# 1. 停止 Redis
docker-compose stop redis
# 2. 清理旧数据
docker-compose run --rm redis rm -rf /data/*
# 3. 恢复 RDB 文件
LATEST_RDB=$(ls -t /backup/redis/dump_*.rdb | head -1)
cp ${LATEST_RDB} docker/redis/data/dump.rdb
# 4. 启动 Redis
docker-compose up -d redis
# 5. 验证
docker-compose exec redis redis-cli PING
echo "Redis 恢复完成!"
```
### 7.3 灾难恢复
#### 7.3.1 灾难恢复场景
**场景 1: 单服务器故障**
- 恢复时间:RTO ≤ 1 小时
- 恢复点:RPO ≤ 15 分钟
- 恢复步骤:
1. 切换到备用服务器
2. 从备份恢复数据
3. 更新 DNS 解析
4. 验证服务可用性
**场景 2: 数据中心故障**
- 恢复时间:RTO ≤ 4 小时
- 恢复点:RPO ≤ 1 小时
- 恢复步骤:
1. 启用异地灾备中心
2. 从异地备份恢复数据
3. 切换流量到灾备中心
4. 验证服务可用性
**场景 3: 数据损坏/丢失**
- 恢复时间:RTO ≤ 2 小时
- 恢复点:RPO ≤ 15 分钟
- 恢复步骤:
1. 确定数据损坏时间点
2. 从损坏前的备份恢复
3. 应用增量备份
4. 验证数据完整性
#### 7.3.2 灾难恢复演练
**演练频率**:
- 桌面推演:每月一次
- 实战演练:每季度一次
- 全链路演练:每半年一次
**演练内容**:
1. 备份恢复验证
2. 故障切换验证
3. 监控告警验证
4. 通讯流程验证
5. 文档更新验证
**演练报告**:
- 演练目标
- 演练过程
- 问题记录
- 改进措施
- 责任人和时间节点
---
## 十、总结 ## 十、总结
### 10.1 部署要点 ### 10.1 部署要点
-945
View File
@@ -1,945 +0,0 @@
# 响应式编程规范文档
> 文档编号: 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. ✅ 文档更新和维护
@@ -0,0 +1,803 @@
# 健身房管理系统付费订阅版业务概要设计文档(B-HLD)
> 文档编号: GYM-B-HLD-SUBSCRIPTION-001
> 版本: v1.0
> 日期: 2026-03-08
> 作者: 张翔
> 状态: 已发布
---
## 文档修订历史
| 版本 | 日期 | 作者 | 修订内容 |
| ---- | ---------- | ---- | -------------------------- |
| v1.0 | 2026-03-08 | 张翔 | 创建付费订阅版业务概要设计文档 |
---
## 一、引言
### 1.1 编写目的
本文档为健身房管理系统付费订阅版的业务概要设计文档(Business 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
---
## 二、业务概述
### 2.1 业务目标
| 目标维度 | 目标描述 | 成功指标 |
| -------- | ---------------------- | -------------------------------- |
| 用户体验 | 提升会员预约和签到体验 | 预约成功率 ≥ 95%,签到耗时 ≤ 3秒 |
| 运营效率 | 降低人工操作成本 | 人工处理时间减少 50% |
| 数据价值 | 提供数据驱动决策支持 | 数据报表使用率 ≥ 80% |
| 业务增长 | 提升会员留存和增长 | 会员留存率提升 20% |
### 2.2 用户角色
| 角色 | 描述 | 主要功能 |
| ---------- | -------------- | ------------------------------------------ |
| 会员 | 健身房注册用户 | 预约课程、签到、查看个人信息、参与社区 |
| 教练 | 健身房教练 | 排课、私教预约确认、学员签到、发布线上课程 |
| 前台 | 门店前台人员 | 会员接待、签到辅助、会员管理 |
| 店长 | 门店管理者 | 单店全功能管理、数据查看、营销活动管理 |
| 运营管理员 | 平台运营人员 | 营销活动配置、数据分析、AI运营建议查看 |
| 财务专员 | 财务人员 | 账单管理、财务报表 |
| 超级管理员 | 平台最高权限 | 全平台管理、系统配置 |
### 2.3 业务范围
```mermaid
graph LR
subgraph 付费订阅版业务范围
A[基础功能<br/>包含基础版所有功能<br/>• 会员管理<br/>• 预约管理<br/>• 签到管理<br/>• 数据统计<br/>• 系统管理]
B[订阅与配置管理<br/>• 订阅管理<br/>• 配置管理<br/>• 套餐管理<br/>• 计费管理]
C[业务扩展类模块<br/>• 私教管理<br/>• 器械预约<br/>• 线上课程]
D[体验升级类模块<br/>• 人脸识别签到<br/>• NFC签到<br/>• 智能储物柜]
E[营销增长类模块<br/>• 营销活动<br/>• 会员推荐奖励<br/>• 会员互动社区<br/>• 智能获客工具]
F[数据智能类模块<br/>• 营销精算模型<br/>• 自定义促销预测<br/>• 高级数据分析<br/>• 智能报表<br/>• AI运营建议<br/>• 智能体测数据联动]
end
```
---
## 三、核心业务流程
### 3.1 订阅流程
#### 3.1.1 业务场景
租户管理员通过管理后台订阅增值模块。
#### 3.1.2 业务流程
```mermaid
flowchart LR
A[租户管理员登录] --> B[查看订阅套餐]
B --> C[选择订阅模块]
C --> D[确认订阅]
D --> E[模块立即启用]
```
#### 3.1.3 业务规则
- 订阅成功后模块立即启用
- 年付享受最大折扣
- 支持多种支付方式
- 订阅成功后发送通知
#### 3.1.4 异常处理
| 异常场景 | 处理方式 |
| -------- | -------------------- |
| 支付失败 | 提示用户重新支付 |
| 支付超时 | 提示用户重新发起支付 |
---
### 3.2 配置继承流程
#### 3.2.1 业务场景
门店管理员配置门店级参数,可以选择继承租户配置。
#### 3.2.2 业务流程
```mermaid
flowchart LR
A[门店管理员登录] --> B[查看租户级配置]
B --> C[选择继承模式]
C --> D[配置门店级参数]
D --> E[配置立即生效]
```
#### 3.2.3 业务规则
- 查询优先级:门店配置 → 租户配置 → 默认配置
- 支持三种继承模式(继承/继承+覆盖/自定义)
- 配置变更后立即生效
- 配置变更记录版本,支持回滚
#### 3.2.4 异常处理
| 异常场景 | 处理方式 |
| -------- | ---------------------- |
| 配置冲突 | 提示用户选择覆盖或合并 |
| 配置无效 | 提示用户重新配置 |
---
### 3.3 私教预约流程
#### 3.3.1 业务场景
会员通过小程序预约私教课程。
#### 3.3.2 业务流程
```mermaid
flowchart LR
A[会员打开小程序] --> B[查看私教课程列表]
B --> C[选择私教课程]
C --> D[确认预约]
D --> E[预约成功]
```
#### 3.3.3 业务规则
- 私教预约需提前至少24小时
- 私教取消需提前至少12小时
- 私教签到后记录考勤
#### 3.3.4 异常处理
| 异常场景 | 处理方式 |
| -------------- | -------------------- |
| 教练时间冲突 | 提示用户选择其他时间 |
| 会员卡权益不足 | 提示用户购买会员卡 |
---
### 3.4 营销活动创建流程
#### 3.4.1 业务场景
运营管理员通过管理后台创建营销活动。
#### 3.4.2 业务流程
```mermaid
flowchart LR
A[运营管理员登录] --> B[创建营销活动]
B --> C[配置活动规则]
C --> D[发布活动]
D --> E[活动生效]
```
#### 3.4.3 业务规则
- 营销活动需指定时间、规则、奖励
- 营销活动发布后不可修改规则
- 营销活动统计按活动、时间维度
#### 3.4.4 异常处理
| 异常场景 | 处理方式 |
| ------------ | -------------------- |
| 活动时间冲突 | 提示用户调整活动时间 |
| 活动规则无效 | 提示用户重新配置 |
---
### 3.5 营销分析与预测流程
#### 3.5.1 业务场景
运营管理员使用营销精算模型预测促销策略。
#### 3.5.2 业务流程
```mermaid
flowchart LR
A[运营管理员登录] --> B[选择营销精算模型]
B --> C[配置促销参数]
C --> D[预测效果]
D --> E[查看预测结果]
```
#### 3.5.3 业务规则
- 营销精算模型基于历史数据
- 促销策略预测提供多种方案
- 促销活动效果预测基于历史数据
#### 3.5.4 异常处理
| 异常场景 | 处理方式 |
| ------------ | -------------------- |
| 历史数据不足 | 提示用户积累更多数据 |
| 预测失败 | 提示用户调整参数 |
---
### 3.6 智能获客流程
#### 3.6.1 业务场景
运营管理员使用智能获客工具进行节后健身潮获客、私域流量获客、推荐裂变获客。
#### 3.6.2 业务流程
**节后健身潮获客**
```mermaid
flowchart LR
A[运营管理员登录] --> B[创建获客活动]
B --> C[配置活动参数]
C --> D[生成海报和文案]
D --> E[分发渠道并追踪]
```
**私域流量获客**
```mermaid
flowchart LR
A[运营管理员登录] --> B[管理私域流量池]
B --> C[精准推送消息]
C --> D[自动化运营]
D --> E[分析转化效果]
```
**推荐裂变获客**
```mermaid
flowchart LR
A[会员打开小程序] --> B[生成推荐码]
B --> C[分享推荐链接]
C --> D[追踪推荐关系链]
D --> E[自动发放奖励]
```
#### 3.6.3 业务规则
- 节后健身潮获客年度流量窗口期自动激活(1月1日-3月31日)
- 私域流量获客基于用户标签精准推送
- 推荐裂变获客支持多级推荐
- 每个渠道的获客效果可追踪
- 推荐奖励自动发放
#### 3.6.4 异常处理
| 异常场景 | 处理方式 |
| ------------ | ---------------- |
| 海报生成失败 | 提示用户重新生成 |
| 文案生成失败 | 提示用户手动编辑 |
| 推荐码失效 | 提示用户重新生成 |
---
### 3.7 智能体测数据联动流程
#### 3.7.1 业务场景
会员进行体测后,体测设备自动上传数据到系统,系统进行数据转换、存储、分析,生成体测报告。
#### 3.7.2 业务流程
```mermaid
flowchart LR
A["会员进行体测"] --> B["设备自动上传数据"]
B --> C["系统数据转换"]
C --> D["数据存储到档案"]
D --> E["生成体测报告"]
style A fill:#e1f5ff
style B fill:#fff4e1
style C fill:#f0e1ff
style D fill:#e1ffe1
style E fill:#ffe1e1
```
#### 3.7.3 业务规则
- 支持主流体测设备(InBody、Tanita等)
- 提供标准API接口,支持任意体测设备对接
- 数据自动上传和转换
- 数据统一存储到会员健康档案
- 支持体测数据查询和分析
- 支持体测报告生成
#### 3.7.4 异常处理
| 异常场景 | 处理方式 |
| ------------ | ------------------------ |
| 设备连接失败 | 提示用户检查设备连接 |
| 数据上传失败 | 提示用户重新上传 |
| 数据转换失败 | 记录错误日志,通知管理员 |
---
### 3.8 器械预约流程
#### 3.8.1 业务场景
会员通过小程序预约器械使用时段,避免等待,提升器械使用效率。
#### 3.8.2 业务流程
```mermaid
flowchart LR
A[会员打开小程序] --> B[查看器械列表]
B --> C[选择器械]
C --> D[查看可用时段]
D --> E[选择时段]
E --> F[确认预约]
F --> G{预约结果}
G -->|成功| H[预约成功]
G -->|失败| I[提示失败原因]
H --> J[接收预约提醒]
J --> K[到店使用器械]
K --> L[使用结束]
L --> M[释放器械]
style A fill:#e1f5ff
style G fill:#fff4e1
style K fill:#e1ffe1
```
#### 3.8.3 业务规则
- **器械预约时间**:器械预约需提前至少30分钟
- **器械取消时间**:器械取消需提前至少1小时
- **器械预约时长**:每次预约时长不超过2小时
- **器械预约冲突**:同一器械同一时段只能预约1人
- **器械使用超时**:超时10分钟自动释放器械
- **器械使用统计**:记录器械使用时长和次数
#### 3.8.4 异常处理
| 异常场景 | 处理方式 |
|---------|---------|
| 器械已被预约 | 提示用户选择其他时段 |
| 预约时间过短 | 提示用户提前预约 |
| 器械维护中 | 提示用户选择其他器械 |
| 预约冲突 | 提示用户选择其他时段 |
---
### 3.9 人脸识别签到流程
#### 3.9.1 业务场景
会员通过人脸识别进行签到,提升签到体验,实现无感通行。
#### 3.9.2 业务流程
```mermaid
flowchart LR
A[会员到店] --> B[人脸识别设备]
B --> C{识别结果}
C -->|成功| D[验证会员卡]
D --> E{验证结果}
E -->|有效| F[签到成功]
E -->|无效| G[提示会员卡无效]
C -->|失败| H[降级为扫码签到]
F --> I[记录到店时间]
G --> H
H --> I
style A fill:#e1f5ff
style C fill:#fff4e1
style E fill:#fff4e1
style H fill:#ffe1e1
```
#### 3.9.3 业务规则
- **人脸信息采集**:人脸信息需会员授权
- **人脸识别准确率**:人脸识别准确率 ≥ 95%
- **人脸识别失败**:人脸识别失败后降级为扫码签到
- **人脸信息存储**:人脸信息加密存储
- **人脸信息管理**:会员可以删除人脸信息
- **人脸识别考勤**:人脸识别签到后记录考勤
#### 3.9.4 异常处理
| 异常场景 | 处理方式 |
|---------|---------|
| 人脸识别失败 | 降级为扫码签到 |
| 会员卡无效 | 提示用户购买会员卡 |
| 人脸信息不存在 | 提示用户采集人脸信息 |
| 设备连接失败 | 提示用户检查设备连接 |
---
### 3.10 NFC签到流程
#### 3.10.1 业务场景
会员通过NFC手环/卡片进行签到,支持储物柜联动,提升签到体验。
#### 3.10.2 业务流程
```mermaid
flowchart LR
A[会员到店] --> B[刷NFC卡]
B --> C[读取NFC信息]
C --> D[验证会员卡]
D --> E{验证结果}
E -->|有效| F[签到成功]
E -->|无效| G[提示会员卡无效]
F --> H{是否需要储物柜}
H -->|是| I[自动开锁储物柜]
H -->|否| J[记录到店时间]
I --> J
G --> K[降级为扫码签到]
K --> J
style A fill:#e1f5ff
style E fill:#fff4e1
style H fill:#fff4e1
style K fill:#ffe1e1
```
#### 3.10.3 业务规则
- **NFC卡绑定**NFC卡需绑定会员
- **NFC签到验证**:NFC签到需验证会员卡有效性
- **NFC签到失败**:NFC签到失败后降级为扫码签到
- **NFC卡管理**:会员可以解绑NFC卡
- **储物柜联动**:支持储物柜自动开锁
- **NFC卡丢失**:NFC卡丢失后可解绑
#### 3.10.4 异常处理
| 异常场景 | 处理方式 |
|---------|---------|
| NFC卡未绑定 | 提示用户绑定NFC卡 |
| 会员卡无效 | 提示用户购买会员卡 |
| NFC卡失效 | 提示用户更换NFC卡 |
| 储物柜故障 | 提示用户使用其他储物柜 |
---
### 3.11 在线课程流程
#### 3.11.1 业务场景
会员通过小程序预约和观看线上课程,拓展线上业务,提升会员活跃度。
#### 3.11.2 业务流程
```mermaid
flowchart LR
A[教练发布线上课程] --> B[填写课程信息]
B --> C[上传课程视频]
C --> D[发布课程]
D --> E[会员查看课程列表]
E --> F[选择课程]
F --> G[预约课程]
G --> H[接收预约提醒]
H --> I[观看课程]
I --> J[课程评价]
J --> K[课程统计]
style A fill:#e1f5ff
style D fill:#fff4e1
style G fill:#fff4e1
style I fill:#e1ffe1
```
#### 3.11.3 业务规则
- **线上课程发布**:线上课程需指定教练、时间、链接
- **线上课程预约**:线上课程预约需提前至少30分钟
- **线上课程观看**:线上课程观看需验证预约
- **线上课程评价**:线上课程观看后可以评价
- **线上课程统计**:线上课程统计按课程、时间维度
- **视频点播**:支持视频点播功能
- **直播课管理**:支持直播课管理
#### 3.11.4 异常处理
| 异常场景 | 处理方式 |
|---------|---------|
| 课程视频上传失败 | 提示教练重新上传 |
| 预约时间过短 | 提示用户提前预约 |
| 课程视频无法播放 | 提示用户检查网络连接 |
| 直播课中断 | 提示用户等待直播恢复 |
---
## 四、用户角色和权限
### 4.1 角色定义
| 角色 | 描述 | 主要功能 |
| ---------- | -------------- | ------------------------------------------ |
| 会员 | 健身房注册用户 | 预约课程、签到、查看个人信息、参与社区 |
| 教练 | 健身房教练 | 排课、私教预约确认、学员签到、发布线上课程 |
| 前台 | 门店前台人员 | 会员接待、签到辅助、会员管理 |
| 店长 | 门店管理者 | 单店全功能管理、数据查看、营销活动管理 |
| 运营管理员 | 平台运营人员 | 营销活动配置、数据分析、AI运营建议查看 |
| 财务专员 | 财务人员 | 账单管理、财务报表 |
| 超级管理员 | 平台最高权限 | 全平台管理、系统配置 |
### 4.2 权限矩阵
| 功能模块 | 会员 | 教练 | 前台 | 店长 | 运营管理员 | 财务专员 | 超级管理员 |
| ------------ | ---- | ---- | ---- | ---- | ---------- | --------- | ---------- |
| 会员信息查看 | 自己 | 所有 | 所有 | 所有 | 所有 | 所有 | 所有 |
| 会员信息编辑 | 自己 | 无 | 所有 | 所有 | 所有 | 无 | 所有 |
| 团课创建 | 无 | 是 | 否 | 是 | 否 | 否 | 是 |
| 团课编辑 | 无 | 自己 | 否 | 所有 | 否 | 否 | 所有 |
| 团课取消 | 无 | 自己 | 否 | 所有 | 否 | 否 | 所有 |
| 私教创建 | 无 | 是 | 否 | 是 | 否 | 否 | 是 |
| 私教编辑 | 无 | 自己 | 否 | 所有 | 否 | 否 | 所有 |
| 私教取消 | 无 | 自己 | 否 | 所有 | 否 | 否 | 所有 |
| 签到管理 | 无 | 是 | 是 | 是 | 否 | 否 | 是 |
| 营销活动创建 | 无 | 无 | 否 | 是 | 是 | 否 | 是 |
| 营销活动编辑 | 无 | 无 | 否 | 自己 | 所有 | 否 | 所有 |
| 营销活动取消 | 无 | 无 | 否 | 自己 | 所有 | 否 | 所有 |
| 数据统计查看 | 自己 | 自己 | 所有 | 所有 | 所有 | 所有 | 所有 |
| 财务报表查看 | 无 | 无 | 否 | 所有 | 所有 | 所有 | 所有 |
| 系统配置 | 无 | 无 | 无 | 无 | 否 | 否 | 是 |
---
## 五、业务规则汇总
### 5.1 订阅管理规则
| 规则 | 描述 |
| -------- | ---------------------------- |
| 订阅生效 | 订阅成功后模块立即启用 |
| 计费周期 | 支持月付、季付、半年付、年付 |
| 试用政策 | 不同模块类型提供不同试用时长 |
| 组合套餐 | 支持组合套餐,享受更多优惠 |
### 5.2 配置管理规则
| 规则 | 描述 |
| ---------- | ----------------------------------- |
| 配置继承 | 支持门店配置继承租户配置 |
| 继承模式 | 支持继承、继承+覆盖、自定义三种模式 |
| 配置优先级 | 门店配置 → 租户配置 → 默认配置 |
| 配置版本 | 配置变更记录版本,支持回滚 |
### 5.3 私教管理规则
| 规则 | 描述 |
| ------------ | ------------------------ |
| 私教预约时间 | 私教预约需提前至少24小时 |
| 私教取消时间 | 私教取消需提前至少12小时 |
| 私教考勤 | 私教签到后记录考勤 |
### 5.4 营销活动规则
| 规则 | 描述 |
| -------- | ------------------------------ |
| 活动规则 | 营销活动需指定时间、规则、奖励 |
| 活动修改 | 营销活动发布后不可修改规则 |
| 活动统计 | 营销活动统计按活动、时间维度 |
### 5.5 营销分析与预测规则
| 规则 | 描述 |
| -------- | ---------------------------- |
| 模型基础 | 营销精算模型基于历史数据 |
| 预测方案 | 促销策略预测提供多种方案 |
| 效果预测 | 促销活动效果预测基于历史数据 |
### 5.6 智能获客工具规则
| 规则 | 描述 |
| -------------- | ---------------------------------------- |
| 节后健身潮获客 | 年度流量窗口期自动激活(1月1日-3月31日) |
| 私域流量获客 | 基于用户标签精准推送 |
| 推荐裂变获客 | 支持多级推荐 |
| 获客效果追踪 | 每个渠道的获客效果可追踪 |
| 推荐奖励发放 | 推荐奖励自动发放 |
### 5.7 智能体测数据联动规则
| 规则 | 描述 |
| -------- | ------------------------------------- |
| 设备对接 | 支持主流体测设备(InBody、Tanita等) |
| API接口 | 提供标准API接口,支持任意体测设备对接 |
| 数据上传 | 数据自动上传和转换 |
| 数据存储 | 数据统一存储到会员健康档案 |
| 数据查询 | 支持体测数据查询和分析 |
| 报告生成 | 支持体测报告生成 |
### 5.8 器械预约规则
| 规则 | 描述 |
|------|------|
| 预约时间 | 器械预约需提前至少30分钟 |
| 取消时间 | 器械取消需提前至少1小时 |
| 预约时长 | 每次预约时长不超过2小时 |
| 预约冲突 | 同一器械同一时段只能预约1人 |
| 使用超时 | 超时10分钟自动释放器械 |
| 使用统计 | 记录器械使用时长和次数 |
### 5.9 人脸识别签到规则
| 规则 | 描述 |
|------|------|
| 人脸信息采集 | 人脸信息需会员授权 |
| 人脸识别准确率 | 人脸识别准确率 ≥ 95% |
| 人脸识别失败 | 人脸识别失败后降级为扫码签到 |
| 人脸信息存储 | 人脸信息加密存储 |
| 人脸信息管理 | 会员可以删除人脸信息 |
| 人脸识别考勤 | 人脸识别签到后记录考勤 |
### 5.10 NFC签到规则
| 规则 | 描述 |
|------|------|
| NFC卡绑定 | NFC卡需绑定会员 |
| NFC签到验证 | NFC签到需验证会员卡有效性 |
| NFC签到失败 | NFC签到失败后降级为扫码签到 |
| NFC卡管理 | 会员可以解绑NFC卡 |
| 储物柜联动 | 支持储物柜自动开锁 |
| NFC卡丢失 | NFC卡丢失后可解绑 |
### 5.11 在线课程规则
| 规则 | 描述 |
|------|------|
| 线上课程发布 | 线上课程需指定教练、时间、链接 |
| 线上课程预约 | 线上课程预约需提前至少30分钟 |
| 线上课程观看 | 线上课程观看需验证预约 |
| 线上课程评价 | 线上课程观看后可以评价 |
| 线上课程统计 | 线上课程统计按课程、时间维度 |
| 视频点播 | 支持视频点播功能 |
| 直播课管理 | 支持直播课管理 |
---
## 六、异常处理汇总
| 异常场景 | 处理方式 |
| ---------------- | ---------------------------- |
| 支付失败 | 提示用户重新支付 |
| 支付超时 | 提示用户重新发起支付 |
| 配置冲突 | 提示用户选择覆盖或合并 |
| 配置无效 | 提示用户重新配置 |
| 教练时间冲突 | 提示用户选择其他时间 |
| 会员卡权益不足 | 提示用户购买会员卡 |
| 活动时间冲突 | 提示用户调整活动时间 |
| 活动规则无效 | 提示用户重新配置 |
| 历史数据不足 | 提示用户积累更多数据 |
| 预测失败 | 提示用户调整参数 |
| 海报生成失败 | 提示用户重新生成 |
| 文案生成失败 | 提示用户手动编辑 |
| 推荐码失效 | 提示用户重新生成 |
| 设备连接失败 | 提示用户检查设备连接 |
| 数据上传失败 | 提示用户重新上传 |
| 数据转换失败 | 记录错误日志,通知管理员 |
| 器械已被预约 | 提示用户选择其他时段 |
| 预约时间过短 | 提示用户提前预约 |
| 器械维护中 | 提示用户选择其他器械 |
| 预约冲突 | 提示用户选择其他时段 |
| 人脸识别失败 | 降级为扫码签到 |
| 会员卡无效 | 提示用户购买会员卡 |
| 人脸信息不存在 | 提示用户采集人脸信息 |
| 设备连接失败 | 提示用户检查设备连接 |
| NFC卡未绑定 | 提示用户绑定NFC卡 |
| NFC卡失效 | 提示用户更换NFC卡 |
| 储物柜故障 | 提示用户使用其他储物柜 |
| 课程视频上传失败 | 提示教练重新上传 |
| 预约时间过短 | 提示用户提前预约 |
| 课程视频无法播放 | 提示用户检查网络连接 |
| 直播课中断 | 提示用户等待直播恢复 |
---
## 七、附录
### 7.1 业务流程图索引
| 流程名称 | 图表位置 |
| ---------------- | ------------ |
| 订阅流程 | 3.1.2 |
| 配置继承流程 | 3.2.2 |
| 私教预约流程 | 3.3.2 |
| 营销活动创建流程 | 3.4.2 |
| 营销分析与预测流程 | 3.5.2 |
| 智能获客流程 | 3.6.2 |
| 智能体测数据联动流程 | 3.7.2 |
| 器械预约流程 | 3.8.2 |
| 人脸识别签到流程 | 3.9.2 |
| NFC签到流程 | 3.10.2 |
| 在线课程流程 | 3.11.2 |
### 7.2 业务规则索引
| 规则分类 | 规则名称 | 图表位置 |
| ---------------- | ---------------- | ------------ |
| 订阅管理规则 | 订阅生效 | 5.1 |
| 订阅管理规则 | 计费周期 | 5.1 |
| 订阅管理规则 | 试用政策 | 5.1 |
| 订阅管理规则 | 组合套餐 | 5.1 |
| 配置管理规则 | 配置继承 | 5.2 |
| 配置管理规则 | 继承模式 | 5.2 |
| 配置管理规则 | 配置优先级 | 5.2 |
| 配置管理规则 | 配置版本 | 5.2 |
| 私教管理规则 | 私教预约时间 | 5.3 |
| 私教管理规则 | 私教取消时间 | 5.3 |
| 私教管理规则 | 私教考勤 | 5.3 |
| 营销活动规则 | 活动规则 | 5.4 |
| 营销活动规则 | 活动修改 | 5.4 |
| 营销活动规则 | 活动统计 | 5.4 |
| 营销分析与预测规则 | 模型基础 | 5.5 |
| 营销分析与预测规则 | 预测方案 | 5.5 |
| 营销分析与预测规则 | 效果预测 | 5.5 |
| 智能获客工具规则 | 节后健身潮获客 | 5.6 |
| 智能获客工具规则 | 私域流量获客 | 5.6 |
| 智能获客工具规则 | 推荐裂变获客 | 5.6 |
| 智能获客工具规则 | 获客效果追踪 | 5.6 |
| 智能获客工具规则 | 推荐奖励发放 | 5.6 |
| 智能体测数据联动规则 | 设备对接 | 5.7 |
| 智能体测数据联动规则 | API接口 | 5.7 |
| 智能体测数据联动规则 | 数据上传 | 5.7 |
| 智能体测数据联动规则 | 数据存储 | 5.7 |
| 智能体测数据联动规则 | 数据查询 | 5.7 |
| 智能体测数据联动规则 | 报告生成 | 5.7 |
| 器械预约规则 | 预约时间 | 5.8 |
| 器械预约规则 | 取消时间 | 5.8 |
| 器械预约规则 | 预约时长 | 5.8 |
| 器械预约规则 | 预约冲突 | 5.8 |
| 器械预约规则 | 使用超时 | 5.8 |
| 器械预约规则 | 使用统计 | 5.8 |
| 人脸识别签到规则 | 人脸信息采集 | 5.9 |
| 人脸识别签到规则 | 人脸识别准确率 | 5.9 |
| 人脸识别签到规则 | 人脸识别失败 | 5.9 |
| 人脸识别签到规则 | 人脸信息存储 | 5.9 |
| 人脸识别签到规则 | 人脸信息管理 | 5.9 |
| 人脸识别签到规则 | 人脸识别考勤 | 5.9 |
| NFC签到规则 | NFC卡绑定 | 5.10 |
| NFC签到规则 | NFC签到验证 | 5.10 |
| NFC签到规则 | NFC签到失败 | 5.10 |
| NFC签到规则 | NFC卡管理 | 5.10 |
| NFC签到规则 | 储物柜联动 | 5.10 |
| NFC签到规则 | NFC卡丢失 | 5.10 |
| 在线课程规则 | 线上课程发布 | 5.11 |
| 在线课程规则 | 线上课程预约 | 5.11 |
| 在线课程规则 | 线上课程观看 | 5.11 |
| 在线课程规则 | 线上课程评价 | 5.11 |
| 在线课程规则 | 线上课程统计 | 5.11 |
| 在线课程规则 | 视频点播 | 5.11 |
| 在线课程规则 | 直播课管理 | 5.11 |
---
**文档结束**
@@ -0,0 +1,477 @@
# 健身房管理系统基础版业务概要设计文档(B-HLD)
> 文档编号: GYM-B-HLD-BASIC-001
> 版本: v1.0
> 日期: 2026-03-08
> 作者: 张翔
> 状态: 已发布
---
## 文档修订历史
| 版本 | 日期 | 作者 | 修订内容 |
| ---- | ---------- | ---- | ---------------------- |
| v1.0 | 2026-03-08 | 张翔 | 创建基础版业务概要设计文档 |
---
## 一、引言
### 1.1 编写目的
本文档为健身房管理系统基础版的业务概要设计文档(Business High-Level Design),旨在:
1. 从业务层面描述基础版的业务范围、核心业务流程、业务规则
2. 为业务详细设计提供业务指导和约束
3. 作为产品经理、业务分析师的业务参考
### 1.2 项目背景
健身房管理系统基础版是面向小型工作室、个人教练等场景的核心版本,保证业务闭环,提供完整的会员管理、预约、签到等核心功能。
### 1.3 术语定义
| 术语 | 定义 |
| ----------------------------- | ------------------------------------------------ |
| 租户(Tenant) | 系统的多租户架构中的独立业务实体,如一个连锁品牌 |
| 门店(Store) | 租户下的具体经营场所 |
| 会员(Member) | 在门店注册的用户 |
| 权益(Benefit) | 会员卡包含的时长、次数、储值、等级等权益 |
| 可预约资源(Bookable Resource) | 团课等可被预约的对象 |
| 时段(Slot) | 资源的可预约时间窗口 |
### 1.4 参考文档
- 《健身房管理系统基础版产品设计文档》 GYM-PRD-BASIC-001
---
## 二、业务概述
### 2.1 业务目标
| 目标维度 | 目标描述 | 成功指标 |
| -------- | ---------------------- | -------------------------------- |
| 用户体验 | 提升会员预约和签到体验 | 预约成功率 ≥ 95%,签到耗时 ≤ 3秒 |
| 运营效率 | 降低人工操作成本 | 人工处理时间减少 50% |
| 数据价值 | 提供基础数据支持 | 数据报表使用率 ≥ 80% |
### 2.2 用户角色
| 角色 | 描述 | 主要功能 |
| ---------- | -------------- | ---------------------------- |
| 会员 | 健身房注册用户 | 预约课程、签到、查看个人信息 |
| 教练 | 健身房教练 | 排课、团课签到管理 |
| 前台 | 门店前台人员 | 会员接待、签到辅助、会员管理 |
| 店长 | 门店管理者 | 单店全功能管理、数据查看 |
| 超级管理员 | 平台最高权限 | 全平台管理、系统配置 |
### 2.3 业务范围
```mermaid
flowchart LR
subgraph "基础版业务范围"
M1[会员管理<br/>• 会员注册<br/>• 会员卡管理<br/>• 权益管理]
M2[预约管理<br/>• 团课预约<br/>• 团课管理]
M3[签到管理<br/>• 扫码签到<br/>• 签到记录管理]
M4[数据统计<br/>• 基础数据统计]
M5[系统管理<br/>• 用户管理<br/>• 角色权限管理]
M6[UI模版定制<br/>• 品牌定制<br/>• 布局调整<br/>• 预设模板<br/>• 配置历史]
end
```
---
## 三、核心业务流程
### 3.1 会员注册流程
#### 3.1.1 业务场景
新用户通过小程序或前台进行注册,成为健身房会员。
#### 3.1.2 业务流程
```mermaid
flowchart LR
A[用户打开小程序] --> B[填写手机号]
B --> C[验证手机号]
C --> D[填写基本信息]
D --> E[注册成功]
```
#### 3.1.3 业务规则
- 手机号需验证唯一性
- 手机号需通过短信验证码验证
- 支持微信授权快速注册
- 注册成功后自动创建会员档案
#### 3.1.4 异常处理
| 异常场景 | 处理方式 |
| ------------ | ---------------- |
| 手机号已存在 | 提示用户直接登录 |
| 验证码错误 | 提示用户重新输入 |
| 验证码过期 | 提示用户重新获取 |
---
### 3.2 团课预约流程
#### 3.2.1 业务场景
会员通过小程序预约团课,教练通过管理后台创建团课。
#### 3.2.2 业务流程
**会员预约团课**
```mermaid
flowchart LR
A[会员打开小程序] --> B[查看团课列表]
B --> C[选择团课]
C --> D[确认预约]
D --> E[预约成功]
```
**教练创建团课**
```mermaid
flowchart LR
A[教练打开管理后台] --> B[点击创建团课]
B --> C[填写团课信息]
C --> D[发布团课]
D --> E[发布成功]
```
#### 3.2.3 业务规则
**预约时间规则**
- 预约需在课程开始前至少30分钟
- ✅ 场景1:团课18:00开始,会员17:30可以预约
- ✅ 场景2:团课18:00开始,会员17:31可以预约
- ❌ 场景3:团课18:00开始,会员17:29无法预约
- ❌ 场景4:团课18:00开始,会员18:00无法预约
**取消预约规则**
- 取消预约需在课程开始前至少2小时
- ✅ 场景1:团课18:00开始,会员16:00可以取消预约
- ✅ 场景2:团课18:00开始,会员15:59可以取消预约
- ❌ 场景3:团课18:00开始,会员16:01无法取消预约
- ❌ 场景4:团课18:00开始,会员17:00无法取消预约
**课程容量规则**
- 每节课最多20人
- ✅ 场景1:团课当前预约19人,第20人可以预约
- ✅ 场景2:团课当前预约18人,2人同时预约,都成功
- ❌ 场景3:团课当前预约20人,第21人无法预约
- ❌ 场景4:团课当前预约19人,2人同时预约,1人成功1人失败
**权益扣减规则**
- 预约成功后扣减权益
- ✅ 场景1:会员有5次团课权益,预约1次后剩余4次
- ✅ 场景2:会员有30天时长卡,预约后时长不变,仅记录预约信息
- ✅ 场景3:会员有储值卡,预约团课费用100元,余额从500元变为400元
- ❌ 场景4:会员权益为0时,无法预约团课
**团课创建规则**
- 团课需指定教练、时间、地点
- ✅ 场景1:教练张三创建团课,指定时间为18:00-19:00,地点为A教室
- ✅ 场景2:教练张三创建团课,指定时间为每周一18:00-19:00,地点为A教室
- ❌ 场景3:创建团课未指定教练,系统提示"请选择教练"
- ❌ 场景4:创建团课未指定时间,系统提示"请选择时间"
**团课取消规则**
- 团课取消需提前24小时通知
- 团课取消后自动退款
- ✅ 场景1:团课18:00开始,教练在前一天16:00取消,已预约会员自动退款
- ✅ 场景2:团课18:00开始,教练在前一天18:00取消,已预约会员自动退款
- ❌ 场景3:团课18:00开始,教练在前一天18:01取消,系统提示"取消时间过晚"
- ❌ 场景4:团课18:00开始,教练在当天17:00取消,系统提示"取消时间过晚"
#### 3.2.4 异常处理
| 异常场景 | 处理方式 |
| -------------- | -------------------- |
| 课程已满 | 提示用户选择其他课程 |
| 会员卡权益不足 | 提示用户购买会员卡 |
| 预约时间过短 | 提示用户提前预约 |
---
### 3.3 签到流程
#### 3.3.1 业务场景
会员到店后通过扫码进行签到,记录到店信息。
#### 3.3.2 业务流程
```mermaid
flowchart LR
A[会员到店] --> B[扫描签到码]
B --> C[验证会员卡]
C --> D[签到成功]
D --> E[记录到店时间]
```
#### 3.3.3 业务规则
**会员卡验证规则**
- 签到需验证会员卡有效性
- ✅ 场景1:会员卡有效期至2026-12-31,今日签到成功
- ✅ 场景2:会员卡有5次权益,签到后剩余4次
- ✅ 场景3:会员卡为30天时长卡,签到后时长不变
- ❌ 场景4:会员卡已过期(2026-01-01到期),签到失败提示"会员卡已过期"
- ❌ 场景5:会员卡权益为0,签到失败提示"会员卡权益不足"
**预约验证规则**
- 签到需验证预约信息(如有)
- ✅ 场景1:会员预约了18:00的团课,18:00签到成功
- ✅ 场景2:会员预约了18:00的团课,17:50签到成功
- ✅ 场景3:会员未预约团课,签到成功记录为自由训练
- ❌ 场景4:会员预约了18:00的团课,19:00签到失败提示"课程已结束"
- ❌ 场景5:会员预约了A教室的团课,在B教室签到失败提示"签到地点错误"
**签到记录规则**
- 签到成功后记录到店时间
- ✅ 场景1:会员18:00:00签到,记录到店时间为2026-03-08 18:00:00
- ✅ 场景2:会员同一天多次签到,记录每次签到时间
- ✅ 场景3:会员签到后离开,再次签到记录新的到店时间
- ❌ 场景4:会员签到失败,不记录到店时间
#### 3.3.4 异常处理
| 异常场景 | 处理方式 |
| ---------- | ------------------ |
| 会员卡无效 | 提示用户购买会员卡 |
| 会员卡过期 | 提示用户续费 |
| 签到码无效 | 提示用户重新扫描 |
---
### 3.4 会员卡购买流程
#### 3.4.1 业务场景
会员通过小程序购买会员卡,获得相应权益。
#### 3.4.2 业务流程
```mermaid
flowchart LR
A[会员打开小程序] --> B[查看会员卡列表]
B --> C[选择会员卡]
C --> D[确认购买]
D --> E[购买成功]
```
#### 3.4.3 业务规则
**会员卡类型规则**
- 支持时长卡、次卡、储值卡
- ✅ 场景1:会员购买30天时长卡,有效期从购买日起30天
- ✅ 场景2:会员购买10次次卡,获得10次团课预约权益
- ✅ 场景3:会员购买1000元储值卡,余额为1000元
- ✅ 场景4:会员购买组合卡(30天时长卡+5次次卡),同时获得时长和次数权益
- ❌ 场景5:会员购买不存在的会员卡类型,系统提示"会员卡类型不存在"
**到期提醒规则**
- 会员卡到期前7天提醒
- ✅ 场景1:会员卡2026-03-15到期,系统在2026-03-08发送提醒
- ✅ 场景2:会员卡2026-03-08到期,系统在2026-03-01发送提醒
- ✅ 场景3:会员卡2026-03-08到期,系统每天发送提醒直到到期
- ❌ 场景4:会员卡2026-03-08到期,系统在2026-03-09发送提醒(已过期)
**续费生效规则**
- 会员卡续费后权益立即生效
- ✅ 场景1:会员卡剩余5次,续费10次后剩余15次
- ✅ 场景2:会员卡2026-03-08到期,续费30天后有效期延长至2026-04-07
- ✅ 场景3:会员卡余额200元,续费500元后余额700元
- ✅ 场景4:会员卡已过期,续费后立即恢复使用
- ❌ 场景5:会员卡续费失败,原权益保持不变
**使用记录规则**
- 会员卡使用记录永久保存
- ✅ 场景1:会员预约团课,记录预约时间、课程信息、权益扣减
- ✅ 场景2:会员签到,记录签到时间、地点、权益扣减
- ✅ 场景3:会员购买会员卡,记录购买时间、金额、权益获得
- ✅ 场景4:会员卡过期,历史使用记录仍可查询
- ✅ 场景5:会员注销账户,使用记录保留用于数据分析
#### 3.4.4 异常处理
| 异常场景 | 处理方式 |
| -------- | -------------------- |
| 支付失败 | 提示用户重新支付 |
| 支付超时 | 提示用户重新发起支付 |
---
### 3.5 UI模版定制流程
#### 3.5.1 业务场景
租户通过管理后台的可视化配置器定制自己的UI,包括品牌元素、布局结构和预设模板。
#### 3.5.2 业务流程
```mermaid
flowchart LR
A[租户登录管理后台] --> B[打开UI定制器]
B --> C[品牌定制]
C --> D[布局调整]
D --> E[配置保存]
```
#### 3.5.3 业务规则
- 品牌元素应用范围包括小程序和管理后台
- 布局调整支持拖拽排序和模块隐藏
- 预设模板应用后保留品牌配置
- 配置变更实时生效,无需重新部署
- 配置变更自动记录到历史
#### 3.5.4 异常处理
| 异常场景 | 处理方式 |
| ------------ | -------------------- |
| Logo上传失败 | 提示用户重新上传 |
| 配置保存失败 | 提示用户检查配置格式 |
| 模板应用失败 | 提示用户调整品牌配置 |
| 配置回滚失败 | 提示用户选择其他版本 |
---
## 四、用户角色和权限
### 4.1 角色定义
| 角色 | 描述 | 主要职责 |
| ---------- | -------------- | ---------------------------- |
| 会员 | 健身房注册用户 | 预约课程、签到、查看个人信息 |
| 教练 | 健身房教练 | 排课、团课签到管理 |
| 前台 | 门店前台人员 | 会员接待、签到辅助、会员管理 |
| 店长 | 门店管理者 | 单店全功能管理、数据查看 |
| 超级管理员 | 平台最高权限 | 全平台管理、系统配置 |
### 4.2 权限矩阵
| 功能模块 | 会员 | 教练 | 前台 | 店长 | 超级管理员 |
| ------------ | ---- | ---- | ---- | ---- | ---------- |
| 会员信息查看 | 自己 | 所有 | 所有 | 所有 | 所有 |
| 会员信息编辑 | 自己 | 无 | 所有 | 所有 | 所有 |
| 团课创建 | 无 | 是 | 否 | 是 | 是 |
| 团课编辑 | 无 | 自己 | 否 | 所有 | 所有 |
| 团课取消 | 无 | 自己 | 否 | 所有 | 所有 |
| 签到管理 | 无 | 是 | 是 | 是 | 是 |
| 数据统计查看 | 自己 | 自己 | 所有 | 所有 | 所有 |
| 系统配置 | 无 | 无 | 无 | 无 | 是 |
---
## 五、业务规则汇总
### 5.1 预约规则
| 规则名称 | 规则描述 |
| ---------------- | ---------------------------- |
| 预约时间限制 | 课程开始前至少30分钟 |
| 取消预约限制 | 课程开始前至少2小时 |
| 课程容量限制 | 每节课最多20人 |
| 权益扣减规则 | 预约成功后扣减权益 |
| 团课创建规则 | 需指定教练、时间、地点 |
| 团课取消规则 | 需提前24小时通知,自动退款 |
### 5.2 签到规则
| 规则名称 | 规则描述 |
| ---------------- | ---------------------------- |
| 会员卡验证规则 | 签到需验证会员卡有效性 |
| 预约验证规则 | 签到需验证预约信息(如有) |
| 签到记录规则 | 签到成功后记录到店时间 |
### 5.3 会员卡规则
| 规则名称 | 规则描述 |
| ---------------- | ---------------------------- |
| 会员卡类型规则 | 支持时长卡、次卡、储值卡 |
| 到期提醒规则 | 会员卡到期前7天提醒 |
| 续费生效规则 | 会员卡续费后权益立即生效 |
| 使用记录规则 | 会员卡使用记录永久保存 |
### 5.4 UI定制规则
| 规则名称 | 规则描述 |
| ---------------- | ---------------------------- |
| 品牌元素应用 | 应用范围包括小程序和管理后台 |
| 布局调整规则 | 支持拖拽排序和模块隐藏 |
| 预设模板规则 | 应用后保留品牌配置 |
| 配置生效规则 | 配置变更实时生效 |
| 配置历史规则 | 配置变更自动记录到历史 |
---
## 六、异常处理汇总
| 异常场景 | 处理方式 |
| ---------------- | ---------------------------- |
| 手机号已存在 | 提示用户直接登录 |
| 验证码错误 | 提示用户重新输入 |
| 验证码过期 | 提示用户重新获取 |
| 课程已满 | 提示用户选择其他课程 |
| 会员卡权益不足 | 提示用户购买会员卡 |
| 预约时间过短 | 提示用户提前预约 |
| 会员卡无效 | 提示用户购买会员卡 |
| 会员卡过期 | 提示用户续费 |
| 签到码无效 | 提示用户重新扫描 |
| 支付失败 | 提示用户重新支付 |
| 支付超时 | 提示用户重新发起支付 |
| Logo上传失败 | 提示用户重新上传 |
| 配置保存失败 | 提示用户检查配置格式 |
| 模板应用失败 | 提示用户调整品牌配置 |
| 配置回滚失败 | 提示用户选择其他版本 |
---
## 七、附录
### 7.1 业务流程图索引
| 流程名称 | 图表位置 |
| ---------------- | ------------ |
| 会员注册流程 | 3.1.2 |
| 会员预约团课流程 | 3.2.2 |
| 教练创建团课流程 | 3.2.2 |
| 签到流程 | 3.3.2 |
| 会员卡购买流程 | 3.4.2 |
| UI模版定制流程 | 3.5.2 |
### 7.2 业务规则索引
| 规则分类 | 规则名称 | 图表位置 |
| ---------------- | ---------------- | ------------ |
| 预约规则 | 预约时间限制 | 5.1 |
| 预约规则 | 取消预约限制 | 5.1 |
| 预约规则 | 课程容量限制 | 5.1 |
| 预约规则 | 权益扣减规则 | 5.1 |
| 预约规则 | 团课创建规则 | 5.1 |
| 预约规则 | 团课取消规则 | 5.1 |
| 签到规则 | 会员卡验证规则 | 5.2 |
| 签到规则 | 预约验证规则 | 5.2 |
| 签到规则 | 签到记录规则 | 5.2 |
| 会员卡规则 | 会员卡类型规则 | 5.3 |
| 会员卡规则 | 到期提醒规则 | 5.3 |
| 会员卡规则 | 续费生效规则 | 5.3 |
| 会员卡规则 | 使用记录规则 | 5.3 |
| UI定制规则 | 品牌元素应用 | 5.4 |
| UI定制规则 | 布局调整规则 | 5.4 |
| UI定制规则 | 预设模板规则 | 5.4 |
| UI定制规则 | 配置生效规则 | 5.4 |
| UI定制规则 | 配置历史规则 | 5.4 |
---
**文档结束**
@@ -0,0 +1,414 @@
# 健身房管理系统付费订阅版业务详细设计文档(B-LLD)
> 文档编号: GYM-B-LLD-SUBSCRIPTION-001
> 版本: v1.0
> 日期: 2026-03-08
> 作者: 张翔
> 状态: 已发布
---
## 文档修订历史
| 版本 | 日期 | 作者 | 修订内容 |
| ---- | ---------- | ---- | -------------------------- |
| v1.0 | 2026-03-08 | 张翔 | 创建付费订阅版业务详细设计文档 |
---
## 一、引言
### 1.1 编写目的
本文档为健身房管理系统付费订阅版的业务详细设计文档(Business Low-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-B-HLD-SUBSCRIPTION-001
---
## 二、业务数据流转
### 2.1 订阅数据流转
```mermaid
flowchart LR
A[租户订阅模块] --> B[创建订阅记录]
B --> C[启用模块功能]
C --> D[模块使用统计]
D --> E[计费周期结算]
E --> F[发送账单]
F --> G[支付确认]
G --> H[续费提醒]
H --> A
style A fill:#e1f5ff
style C fill:#fff4e1
style E fill:#ffe1e1
style G fill:#e1ffe1
```
### 2.2 配置数据流转
```mermaid
flowchart LR
A[租户级配置] --> B[门店继承配置]
B --> C[门店级配置覆盖]
C --> D[配置生效]
D --> E[配置版本记录]
E --> F[配置变更回滚]
style A fill:#e1f5ff
style C fill:#fff4e1
style E fill:#ffe1e1
```
### 2.3 私教预约数据流转
```mermaid
flowchart LR
A[会员预约私教] --> B[创建预约记录]
B --> C[扣减会员权益]
C --> D[发送预约提醒]
D --> E[私教签到]
E --> F[记录考勤]
F --> G[私教评价]
G --> H[数据统计]
style A fill:#e1f5ff
style C fill:#fff4e1
style E fill:#e1ffe1
style H fill:#ffe1e1
```
### 2.4 营销活动数据流转
```mermaid
flowchart LR
A[创建营销活动] --> B[配置活动规则]
B --> C[发布活动]
C --> D[会员参与活动]
D --> E[发放活动奖励]
E --> F[活动效果统计]
F --> G[活动数据分析]
G --> H[生成活动报告]
style A fill:#e1f5ff
style C fill:#fff4e1
style E fill:#e1ffe1
style H fill:#ffe1e1
```
### 2.5 智能获客数据流转
```mermaid
flowchart LR
A[创建获客活动] --> B[生成推广素材]
B --> C[分发到渠道]
C --> D[用户点击链接]
D --> E[记录推荐关系]
E --> F[用户注册]
F --> G[发放推荐奖励]
G --> H[获客效果统计]
H --> I[获客数据分析]
style A fill:#e1f5ff
style C fill:#fff4e1
style E fill:#e1ffe1
style I fill:#ffe1e1
```
### 2.6 智能体测数据流转
```mermaid
flowchart LR
A[会员进行体测] --> B[设备上传数据]
B --> C[系统数据转换]
C --> D[数据存储到档案]
D --> E[数据分析]
E --> F[生成体测报告]
F --> G[会员查看报告]
G --> H[历史数据对比]
style A fill:#e1f5ff
style C fill:#fff4e1
style E fill:#e1ffe1
style H fill:#ffe1e1
```
### 2.7 器械预约数据流转
```mermaid
flowchart LR
A[会员预约器械] --> B[创建预约记录]
B --> C[锁定器械时段]
C --> D[发送预约提醒]
D --> E[会员到店使用]
E --> F[记录使用时长]
F --> G[释放器械]
G --> H[器械使用统计]
style A fill:#e1f5ff
style C fill:#fff4e1
style E fill:#e1ffe1
style H fill:#ffe1e1
```
### 2.8 人脸识别签到数据流转
```mermaid
flowchart LR
A[会员到店] --> B[人脸识别]
B --> C{识别结果}
C -->|成功| D[验证会员卡]
D --> E{验证结果}
E -->|有效| F[签到成功]
E -->|无效| G[提示会员卡无效]
C -->|失败| H[降级为扫码签到]
F --> I[记录到店时间]
G --> H
H --> I
style A fill:#e1f5ff
style C fill:#fff4e1
style E fill:#fff4e1
style H fill:#ffe1e1
```
### 2.9 NFC签到数据流转
```mermaid
flowchart LR
A[会员到店] --> B[刷NFC卡]
B --> C[读取NFC信息]
C --> D[验证会员卡]
D --> E{验证结果}
E -->|有效| F[签到成功]
E -->|无效| G[提示会员卡无效]
F --> H{是否需要储物柜}
H -->|是| I[自动开锁储物柜]
H -->|否| J[记录到店时间]
I --> J
G --> K[降级为扫码签到]
K --> J
style A fill:#e1f5ff
style E fill:#fff4e1
style H fill:#fff4e1
style K fill:#ffe1e1
```
### 2.10 在线课程数据流转
```mermaid
flowchart LR
A[教练发布课程] --> B[上传课程视频]
B --> C[发布课程]
C --> D[会员预约课程]
D --> E[发送预约提醒]
E --> F[会员观看课程]
F --> G[记录观看时长]
G --> H[会员评价课程]
H --> I[课程数据统计]
style A fill:#e1f5ff
style C fill:#fff4e1
style E fill:#e1ffe1
style I fill:#ffe1e1
```
---
## 三、业务指标
### 3.1 核心业务指标
| 指标名称 | 目标值 | 计算方式 |
| ------------------ | ------------ | ---------------------------- |
| 预约成功率 | ≥ 95% | 成功预约次数 / 总预约次数 |
| 签到耗时 | ≤ 3秒 | 签到请求到签到完成的时间 |
| 人工处理时间减少 | 50% | (优化前时间 - 优化后时间) / 优化前时间 |
| 数据报表使用率 | ≥ 80% | 使用报表的用户数 / 总用户数 |
| 新会员激活率 | ≥ 70% | 7天内首次到店的新会员数 / 新会员总数 |
| 会员流失率 | ≤ 10% | 流失会员数 / 总会员数 |
| 投诉处理满意度 | ≥ 90% | 满意投诉数 / 总投诉数 |
| 会员留存率 | ≥ 80% | 留存会员数 / 总会员数 |
### 3.2 运营指标
| 指标名称 | 目标值 | 计算方式 |
| ------------------ | ------------ | ---------------------------- |
| 团课满课率 | ≥ 80% | 满员课程数 / 总课程数 |
| 会员活跃度 | ≥ 60% | 活跃会员数 / 总会员数 |
| 会员续费率 | ≥ 70% | 续费会员数 / 到期会员数 |
| 会员卡使用率 | ≥ 85% | 使用会员卡的会员数 / 持卡会员数 |
| 私教预约成功率 | ≥ 90% | 成功预约私教次数 / 总预约次数 |
| 营销活动参与率 | ≥ 50% | 参与活动的会员数 / 总会员数 |
| 推荐转化率 | ≥ 20% | 推荐成功注册数 / 推荐链接点击数 |
| 获客成本 | ≤ 100元 | 获客总成本 / 新增会员数 |
### 3.3 订阅指标
| 指标名称 | 目标值 | 计算方式 |
| ------------------ | ------------ | ---------------------------- |
| 订阅转化率 | ≥ 30% | 订阅租户数 / 总租户数 |
| 订阅续费率 | ≥ 80% | 续费订阅数 / 到期订阅数 |
| 模块使用率 | ≥ 70% | 使用模块的租户数 / 订阅该模块的租户数 |
| 订阅ARPU | ≥ 1000元 | 订阅总收入 / 订阅租户数 |
### 3.4 技术指标
| 指标名称 | 目标值 | 计算方式 |
| ------------------ | ------------ | ---------------------------- |
| API响应时间 | ≤ 500ms | API请求到响应完成的时间 |
| 系统可用性 | ≥ 99.9% | 系统正常运行时间 / 总时间 |
| 并发用户数 | 500 | 系统支持的最大并发用户数 |
| 数据库查询时间 | ≤ 1s | 数据库查询的响应时间 |
| 人脸识别准确率 | ≥ 95% | 人脸识别成功次数 / 总识别次数 |
---
## 四、业务规则补充
### 4.1 订阅计费规则
| 规则类型 | 规则描述 |
| ------------ | ---------------------------- |
| 基础版月费 | ¥299/月,标准价格 |
| 基础版季费 | ¥269/月,9折优惠 |
| 基础版半年费 | ¥254/月,85折优惠 |
| 基础版年费 | ¥239/月,8折优惠 |
| 订阅模块定价 | ¥199-499/月,按模块定价 |
| 试用时长 | 14天免费试用 |
| 组合折扣 | 订阅模块数量越多折扣越大,详见PRD动态折扣规则 |
### 4.2 营销活动效果评估规则
| 规则类型 | 规则描述 |
| ------------ | ---------------------------- |
| 活动参与率 | 参与活动的会员数 / 目标会员数 |
| 活动转化率 | 完成活动的会员数 / 参与活动的会员数 |
| 活动ROI | 活动收益 / 活动成本 |
| 活动满意度 | 满意会员数 / 参与活动的会员数 |
### 4.3 推荐奖励规则
| 规则类型 | 规则描述 |
| ------------ | ---------------------------- |
| 推荐奖励 | 推荐成功注册,推荐人获得100元优惠券 |
| 被推荐奖励 | 被推荐人注册成功,获得50元优惠券 |
| 多级推荐 | 支持3级推荐,每级奖励递减 |
| 奖励发放 | 推荐成功后24小时内自动发放 |
### 4.4 体测数据管理规则
| 规则类型 | 规则描述 |
| ------------ | ---------------------------- |
| 数据保留期限 | 体测数据永久保存 |
| 数据对比 | 支持最近10次体测数据对比 |
| 报告生成 | 体测完成后10分钟内生成报告 |
| 数据分享 | 支持会员分享体测报告到社交平台 |
### 4.5 器械使用规则
| 规则类型 | 规则描述 |
| ------------ | ---------------------------- |
| 预约超时 | 超时10分钟自动释放器械 |
| 使用统计 | 记录器械使用时长和次数 |
| 维护提醒 | 器械使用达到100小时后提醒维护 |
| 预约取消 | 取消预约后释放器械时段 |
---
## 五、附录
### 5.1 业务术语表
| 术语 | 定义 |
| ----------------------------------- | ------------------------------------------------ |
| 租户(Tenant) | 系统的多租户架构中的独立业务实体,如一个连锁品牌 |
| 门店(Store) | 租户下的具体经营场所 |
| 会员(Member) | 在门店注册的用户 |
| 权益(Benefit) | 会员卡包含的时长、次数、储值、等级等权益 |
| 可预约资源(Bookable Resource) | 团课、私教、场地、线上课程等可被预约的对象 |
| 时段(Slot) | 资源的可预约时间窗口 |
| 订阅模块(Subscription Module) | 按需订阅的增值功能模块 |
| 配置继承(Configuration Inheritance) | 门店配置继承租户配置的机制 |
### 5.2 参考文档
- 《健身房管理系统付费订阅版产品设计文档》 GYM-PRD-SUBSCRIPTION-001
- 《健身房管理系统付费订阅版业务概要设计文档》 GYM-B-HLD-SUBSCRIPTION-001
- 《健身房管理系统付费订阅版技术实现详细设计文档》 GYM-T-ILD-SUBSCRIPTION-001
### 5.3 业务数据流转图索引
| 流程名称 | 图表位置 |
| ---------------- | ------------ |
| 订阅数据流转 | 2.1 |
| 配置数据流转 | 2.2 |
| 私教预约数据流转 | 2.3 |
| 营销活动数据流转 | 2.4 |
| 智能获客数据流转 | 2.5 |
| 智能体测数据流转 | 2.6 |
| 器械预约数据流转 | 2.7 |
| 人脸识别签到数据流转 | 2.8 |
| NFC签到数据流转 | 2.9 |
| 在线课程数据流转 | 2.10 |
### 5.4 业务指标索引
| 指标分类 | 指标名称 | 图表位置 |
| ---------------- | ---------------- | ------------ |
| 核心业务指标 | 预约成功率 | 3.1 |
| 核心业务指标 | 签到耗时 | 3.1 |
| 核心业务指标 | 人工处理时间减少 | 3.1 |
| 核心业务指标 | 数据报表使用率 | 3.1 |
| 核心业务指标 | 新会员激活率 | 3.1 |
| 核心业务指标 | 会员流失率 | 3.1 |
| 核心业务指标 | 投诉处理满意度 | 3.1 |
| 核心业务指标 | 会员留存率 | 3.1 |
| 运营指标 | 团课满课率 | 3.2 |
| 运营指标 | 会员活跃度 | 3.2 |
| 运营指标 | 会员续费率 | 3.2 |
| 运营指标 | 会员卡使用率 | 3.2 |
| 运营指标 | 私教预约成功率 | 3.2 |
| 运营指标 | 营销活动参与率 | 3.2 |
| 运营指标 | 推荐转化率 | 3.2 |
| 运营指标 | 获客成本 | 3.2 |
| 订阅指标 | 订阅转化率 | 3.3 |
| 订阅指标 | 订阅续费率 | 3.3 |
| 订阅指标 | 模块使用率 | 3.3 |
| 订阅指标 | 订阅ARPU | 3.3 |
| 技术指标 | API响应时间 | 3.4 |
| 技术指标 | 系统可用性 | 3.4 |
| 技术指标 | 并发用户数 | 3.4 |
| 技术指标 | 数据库查询时间 | 3.4 |
| 技术指标 | 人脸识别准确率 | 3.4 |
---
**文档结束**
@@ -0,0 +1,654 @@
# 健身房管理系统基础版业务详细设计文档(B-LLD)
> 文档编号: GYM-B-LLD-BASIC-001
> 版本: v1.0
> 日期: 2026-03-08
> 作者: 张翔
> 状态: 已发布
---
## 文档修订历史
| 版本 | 日期 | 作者 | 修订内容 |
| ---- | ---------- | ---- | ---------------------- |
| v1.0 | 2026-03-08 | 张翔 | 创建基础版业务详细设计文档 |
---
## 一、引言
### 1.1 编写目的
本文档为健身房管理系统基础版的业务详细设计文档(Business Low-Level Design),旨在:
1. 详细描述业务流程、业务规则、异常处理
2. 为技术实现提供详细的业务指导
3. 作为业务分析师、开发人员的业务参考
### 1.2 项目背景
健身房管理系统基础版是面向小型工作室、个人教练等场景的核心版本,保证业务闭环,提供完整的会员管理、预约、签到等核心功能。
### 1.3 术语定义
| 术语 | 定义 |
| ----------------------------- | ------------------------------------------------ |
| 租户(Tenant) | 系统的多租户架构中的独立业务实体,如一个连锁品牌 |
| 门店(Store) | 租户下的具体经营场所 |
| 会员(Member) | 在门店注册的用户 |
| 权益(Benefit) | 会员卡包含的时长、次数、储值、等级等权益 |
| 可预约资源(Bookable Resource) | 团课等可被预约的对象 |
| 时段(Slot) | 资源的可预约时间窗口 |
### 1.4 参考文档
- 《健身房管理系统基础版产品设计文档》 GYM-PRD-BASIC-001
- 《健身房管理系统基础版业务概要设计文档》 GYM-B-HLD-BASIC-001
---
## 二、详细业务流程
### 2.1 会员全生命周期流程
#### 2.1.1 业务场景
从会员注册到流失的完整生命周期管理,包括新会员激活、活跃期维护、沉默期干预、流失预警和挽回。
#### 2.1.2 业务流程
```mermaid
flowchart LR
A[新会员注册] --> B[首次到店引导]
B --> C[新会员激活期<br/>7天内完成首次到店]
C --> D[活跃期维护<br/>持续到店和消费]
D --> E{活跃度评估}
E -->|活跃| F[持续运营<br/>推送个性化内容]
E -->|沉默| G[沉默期干预<br/>7天未到店触发]
G --> H{干预效果}
H -->|成功| D
H -->|失败| I[流失预警<br/>30天未到店触发]
I --> J{挽回策略}
J -->|挽回成功| D
J -->|挽回失败| K[会员流失<br/>标记为流失状态]
K --> L[归档分析<br/>流失原因分析]
style A fill:#e1f5ff
style C fill:#fff4e1
style G fill:#ffe1e1
style I fill:#ffe1e1
style K fill:#ffcccc
```
#### 2.1.3 业务规则
**新会员激活期规则**
- 注册后7天内完成首次到店,否则进入沉默期干预
- ✅ 场景1:会员2026-03-01注册,2026-03-07首次到店,激活成功
- ✅ 场景2:会员2026-03-01注册,2026-03-08首次到店,激活成功
- ❌ 场景3:会员2026-03-01注册,2026-03-09首次到店,已进入沉默期干预
- ❌ 场景4:会员2026-03-01注册,2026-03-15首次到店,已进入流失预警
**活跃期定义规则**
- 30天内至少到店2次或消费1次
- ✅ 场景1:会员30天内到店2次,保持活跃状态
- ✅ 场景2:会员30天内到店1次但消费1次,保持活跃状态
- ✅ 场景3:会员30天内到店3次,保持活跃状态
- ❌ 场景4:会员30天内到店1次且未消费,进入沉默期
- ❌ 场景5:会员30天内未到店但消费1次,保持活跃状态
**沉默期触发规则**
- 7天未到店触发沉默期干预
- ✅ 场景1:会员最后到店2026-03-012026-03-08触发沉默期干预
- ✅ 场景2:会员最后到店2026-03-012026-03-09仍处于沉默期
- ✅ 场景3:会员沉默期干预成功,到店后重新计算活跃期
- ❌ 场景4:会员最后到店2026-03-012026-03-07未触发沉默期干预
**沉默期干预策略**
- 发送个性化关怀短信
- 提供专属优惠券
- 推荐适合的团课
- 教练主动联系
- ✅ 场景1:会员沉默7天,发送关怀短信"好久不见,期待您的到来"
- ✅ 场景2:会员沉默7天,提供专属优惠券"限时9折优惠"
- ✅ 场景3:会员沉默7天,推荐适合的团课"瑜伽课程适合您"
- ✅ 场景4:会员沉默7天,教练主动电话联系
- ❌ 场景5:会员沉默7天,未采取任何干预措施
**流失预警规则**
- 30天未到店触发流失预警
- ✅ 场景1:会员最后到店2026-02-012026-03-03触发流失预警
- ✅ 场景2:会员最后到店2026-02-012026-03-04启动挽回流程
- ✅ 场景3:会员挽回成功,到店后重新计算活跃期
- ❌ 场景4:会员最后到店2026-02-012026-03-02未触发流失预警
**流失定义规则**
- 90天未到店且未消费
- ✅ 场景1:会员最后到店2026-01-012026-04-01标记为流失状态
- ✅ 场景2:会员最后到店2026-01-012026-03-31仍处于流失预警期
- ✅ 场景3:会员90天内未到店但消费1次,不标记为流失
- ❌ 场景4:会员最后到店2026-01-012026-03-31标记为流失状态(错误)
**挽回策略规则**
- 根据会员等级和历史行为制定个性化挽回方案
- ✅ 场景1:VIP会员流失预警,提供专属私教课程优惠
- ✅ 场景2:普通会员流失预警,发送关怀短信和优惠券
- ✅ 场景3:高消费会员流失预警,客服主动电话联系
- ❌ 场景4:流失预警会员未制定挽回方案,系统自动发送通用短信
**流失归档规则**
- 流失会员归档保存,用于流失原因分析
- ✅ 场景1:会员标记为流失,归档保存所有历史数据
- ✅ 场景2:会员流失后重新激活,归档数据仍保留用于分析
- ✅ 场景3:定期分析流失会员数据,生成流失原因报告
- ❌ 场景4:会员标记为流失,删除历史数据(错误)
#### 2.1.4 异常处理
| 异常场景 | 处理方式 |
|---------|---------|
| 新会员激活失败 | 发送个性化邀请短信,提供首次到店优惠 |
| 沉默期干预无效 | 升级干预策略,提供专属优惠或服务 |
| 流失预警触发 | 启动挽回流程,由客服主动联系 |
| 会员数据异常 | 标记异常状态,暂停自动化运营,人工介入处理 |
---
### 2.2 支付与退款全流程
#### 2.2.1 业务场景
会员购买会员卡、私教课程等服务的支付流程,以及退款申请、审批、退款、财务对账的完整流程。
#### 2.2.2 业务流程
```mermaid
flowchart TB
subgraph 支付流程
A[会员发起支付] --> B[选择支付方式]
B --> C[创建支付订单]
C --> D[调用支付网关]
D --> E{支付结果}
E -->|成功| F[更新订单状态]
F --> G[发放会员卡权益]
G --> H[发送支付成功通知]
E -->|失败| I[记录支付失败]
I --> J[提示用户重新支付]
end
subgraph 退款流程
K[会员申请退款] --> L[填写退款原因]
L --> M[提交退款申请]
M --> N{退款类型}
N -->|自动退款| O[系统自动审核]
N -->|人工审核| P[店长审核]
P --> Q{审核结果}
Q -->|通过| R[财务专员复核]
Q -->|拒绝| S[通知会员拒绝原因]
O --> R
R --> T{复核结果}
T -->|通过| U[调用退款接口]
T -->|拒绝| S
U --> V[更新订单状态]
V --> W[收回会员卡权益]
W --> X[发送退款成功通知]
X --> Y[财务对账]
end
style E fill:#fff4e1
style Q fill:#fff4e1
style T fill:#fff4e1
style K fill:#e1f5ff
```
#### 2.2.3 业务规则
**支付方式规则**
- 支持微信支付、支付宝、银行卡支付
- ✅ 场景1:会员选择微信支付,调用微信支付接口
- ✅ 场景2:会员选择支付宝,调用支付宝接口
- ✅ 场景3:会员选择银行卡,调用银行卡支付接口
- ❌ 场景4:会员选择不支持的支付方式,提示"暂不支持该支付方式"
**支付超时规则**
- 订单创建后30分钟内未支付自动取消
- ✅ 场景1:订单18:00创建,18:30未支付,订单自动取消
- ✅ 场景2:订单18:00创建,18:29支付,支付成功
- ❌ 场景3:订单18:00创建,18:31支付,支付失败提示"订单已取消"
- ❌ 场景4:订单18:00创建,18:00支付,支付成功
**自动退款条件规则**
- 7天内购买且未使用的会员卡、私教课程
- ✅ 场景1:会员购买会员卡后第1天申请退款,未使用,自动退款
- ✅ 场景2:会员购买会员卡后第7天申请退款,未使用,自动退款
- ❌ 场景3:会员购买会员卡后第8天申请退款,未使用,需人工审核
- ❌ 场景4:会员购买会员卡后第1天申请退款,已使用,需人工审核
**人工审核条件规则**
- 超过7天、已使用部分权益、金额超过1000元
- ✅ 场景1:会员购买会员卡后第8天申请退款,需人工审核
- ✅ 场景2:会员购买会员卡后第1天申请退款,已使用,需人工审核
- ✅ 场景3:会员购买1500元会员卡后第1天申请退款,需人工审核
- ❌ 场景4:会员购买会员卡后第7天申请退款,未使用,金额500元,自动退款
**退款时效规则**
- 审核通过后1-3个工作日到账
- ✅ 场景1:退款审核通过,第1个工作日到账
- ✅ 场景2:退款审核通过,第3个工作日到账
- ❌ 场景3:退款审核通过,第4个工作日到账(超时)
**财务对账规则**
- 每日自动对账,异常订单人工处理
- ✅ 场景1:系统每日凌晨自动对账,生成对账报告
- ✅ 场景2:对账发现异常订单,标记异常,财务专员人工核查
- ❌ 场景3:对账发现异常订单,未标记异常(错误)
**退款手续费规则**
- 7天内无手续费,7-30天收取5%手续费,30天以上收取10%手续费
- ✅ 场景1:会员购买会员卡后第1天申请退款,无手续费
- ✅ 场景2:会员购买会员卡后第15天申请退款,收取5%手续费
- ✅ 场景3:会员购买会员卡后第45天申请退款,收取10%手续费
- ❌ 场景4:会员购买会员卡后第1天申请退款,收取5%手续费(错误)
#### 2.2.4 异常处理
| 异常场景 | 处理方式 |
|---------|---------|
| 支付超时 | 订单自动取消,释放库存和权益 |
| 支付重复 | 检测重复支付,自动退款重复金额 |
| 退款失败 | 重试3次,失败后人工介入处理 |
| 财务对账异常 | 标记异常订单,财务专员人工核查 |
| 退款申请超时 | 退款申请提交后48小时内未处理自动升级 |
---
### 2.3 投诉与反馈处理流程
#### 2.3.1 业务场景
会员提交投诉或反馈,系统自动分类、分配、处理、反馈,并进行满意度调查和归档分析。
#### 2.3.2 业务流程
```mermaid
flowchart LR
A[会员提交投诉/反馈] --> B[填写投诉详情]
B --> C[选择投诉类型]
C --> D[上传相关凭证]
D --> E[提交投诉]
E --> F[系统自动分类]
F --> G{投诉类型}
G -->|服务投诉| H[分配给店长]
G -->|设施投诉| I[分配给运营管理员]
G -->|财务投诉| J[分配给财务专员]
G -->|技术投诉| K[分配给技术支持]
H --> L[处理人接收]
I --> L
J --> L
K --> L
L --> M[调查处理]
M --> N{处理结果}
N -->|解决| O[反馈处理结果]
N -->|无法解决| P[升级处理]
P --> Q[上级介入处理]
Q --> O
O --> R[会员确认]
R --> S{满意度调查}
S -->|满意| T[归档分析]
S -->|不满意| U[重新处理]
U --> M
style A fill:#e1f5ff
style F fill:#fff4e1
style N fill:#fff4e1
style S fill:#ffe1e1
```
#### 2.3.3 业务规则
**投诉分类规则**
- 服务投诉、设施投诉、财务投诉、技术投诉、其他
- ✅ 场景1:会员投诉教练服务态度,分类为服务投诉
- ✅ 场景2:会员投诉器械损坏,分类为设施投诉
- ✅ 场景3:会员投诉退款问题,分类为财务投诉
- ✅ 场景4:会员投诉系统故障,分类为技术投诉
- ✅ 场景5:会员投诉其他问题,分类为其他
**响应时效规则**
- 投诉提交后2小时内响应
- ✅ 场景1:投诉14:00提交,16:00前响应
- ✅ 场景2:投诉14:00提交,15:59响应
- ❌ 场景3:投诉14:00提交,16:01响应(超时)
**处理时效规则**
- 一般投诉24小时内处理完毕,复杂投诉48小时内处理完毕
- ✅ 场景1:一般投诉14:00提交,次日14:00前处理完毕
- ✅ 场景2:复杂投诉14:00提交,后日14:00前处理完毕
- ❌ 场景3:一般投诉14:00提交,次日14:01处理完毕(超时)
**升级机制规则**
- 处理人无法解决时自动升级给上级
- ✅ 场景1:店长无法解决服务投诉,自动升级给运营管理员
- ✅ 场景2:运营管理员无法解决设施投诉,自动升级给超级管理员
- ❌ 场景3:处理人无法解决投诉,未升级(错误)
**满意度调查规则**
- 投诉处理完成后自动发送满意度调查
- ✅ 场景1:投诉处理完成,系统自动发送满意度调查问卷
- ✅ 场景2:会员完成满意度调查,系统记录满意度评分
- ❌ 场景3:投诉处理完成,未发送满意度调查(错误)
**归档分析规则**
- 投诉归档后进行分类统计和原因分析
- ✅ 场景1:投诉归档,系统自动分类统计
- ✅ 场景2:定期分析投诉数据,生成投诉原因报告
- ❌ 场景3:投诉归档,未进行分类统计(错误)
**投诉闭环规则**
- 所有投诉必须闭环处理,不得遗漏
- ✅ 场景1:投诉处理完成,会员确认,归档
- ✅ 场景2:投诉处理完成,会员不满意,重新处理,会员确认,归档
- ❌ 场景3:投诉处理完成,未会员确认,归档(错误)
#### 2.3.4 异常处理
| 异常场景 | 处理方式 |
|---------|---------|
| 投诉信息不完整 | 提示会员补充必要信息 |
| 处理人未响应 | 2小时未响应自动升级给上级 |
| 处理超时 | 24小时未处理自动升级给店长 |
| 会员不满意 | 重新处理,升级处理级别 |
| 投诉重复提交 | 合并重复投诉,关联处理 |
---
## 三、业务数据流转
### 3.1 会员数据流转
```mermaid
flowchart LR
A[会员注册] --> B[创建会员档案]
B --> C[购买会员卡]
C --> D[获得权益]
D --> E[预约团课]
E --> F[扣减权益]
F --> G[签到]
G --> H[记录到店]
H --> I[消费记录]
I --> J[数据统计]
```
### 3.2 权益数据流转
```mermaid
flowchart LR
A[购买会员卡] --> B[发放权益]
B --> C[预约扣减]
C --> D[签到扣减]
D --> E[权益使用记录]
E --> F[权益查询]
F --> G[权益续费]
G --> B
```
---
## 四、业务规则汇总
### 4.1 时间相关规则
| 规则类型 | 时间要求 | 说明 |
| -------------- | ------------------ | ------------------------ |
| 预约时间 | 课程开始前30分钟 | 会员预约团课的最短时间 |
| 取消预约 | 课程开始前2小时 | 会员取消预约的最短时间 |
| 团课取消 | 提前24小时 | 教练取消团课的最短时间 |
| 支付超时 | 30分钟 | 订单未支付自动取消时间 |
| 新会员激活期 | 7天 | 新会员首次到店时间要求 |
| 沉默期触发 | 7天未到店 | 触发沉默期干预的时间 |
| 流失预警 | 30天未到店 | 触发流失预警的时间 |
| 流失定义 | 90天未到店 | 会员流失的时间定义 |
| 投诉响应 | 2小时 | 投诉响应时间要求 |
| 投诉处理 | 24-48小时 | 投诉处理完成时间 |
| 退款时效 | 1-3个工作日 | 退款到账时间 |
### 4.2 数量相关规则
| 规则类型 | 数量限制 | 说明 |
| ------------ | -------- | -------------- |
| 团课容量 | 20人 | 每节课最大人数 |
| 自动退款 | 7天内 | 自动退款条件 |
| 手续费7-30天 | 5% | 退款手续费 |
| 手续费30天以上 | 10% | 退款手续费 |
### 4.3 状态相关规则
| 规则类型 | 状态定义 | 说明 |
| ------------ | -------- | -------------- |
| 活跃期 | 30天内到店2次或消费1次 | 会员活跃状态 |
| 沉默期 | 7天未到店 | 会员沉默状态 |
| 流失预警 | 30天未到店 | 流失预警状态 |
| 流失 | 90天未到店且未消费 | 会员流失状态 |
---
## 五、业务异常处理
### 5.1 会员相关异常
| 异常类型 | 处理方式 |
| ------------ | ---------------------------- |
| 手机号已存在 | 提示用户直接登录 |
| 验证码错误 | 提示用户重新输入 |
| 验证码过期 | 提示用户重新获取 |
| 会员卡无效 | 提示用户购买会员卡 |
| 会员卡过期 | 提示用户续费 |
| 会员卡权益不足 | 提示用户购买会员卡或续费 |
### 5.2 预约相关异常
| 异常类型 | 处理方式 |
| ------------ | ---------------------------- |
| 课程已满 | 提示用户选择其他课程 |
| 会员卡权益不足 | 提示用户购买会员卡 |
| 预约时间过短 | 提示用户提前预约 |
| 团课取消过晚 | 系统提示"取消时间过晚" |
### 5.3 支付相关异常
| 异常类型 | 处理方式 |
| ------------ | ---------------------------- |
| 支付失败 | 提示用户重新支付 |
| 支付超时 | 订单自动取消,释放库存和权益 |
| 支付重复 | 检测重复支付,自动退款重复金额 |
| 退款失败 | 重试3次,失败后人工介入处理 |
| 财务对账异常 | 标记异常订单,财务专员人工核查 |
### 5.4 投诉相关异常
| 异常类型 | 处理方式 |
| -------------- | ---------------------------- |
| 投诉信息不完整 | 提示会员补充必要信息 |
| 处理人未响应 | 2小时未响应自动升级给上级 |
| 处理超时 | 24小时未处理自动升级给店长 |
| 会员不满意 | 重新处理,升级处理级别 |
| 投诉重复提交 | 合并重复投诉,关联处理 |
---
## 六、业务指标
### 6.1 核心业务指标
| 指标名称 | 目标值 | 计算方式 |
| ------------------ | ------------ | ---------------------------- |
| 预约成功率 | ≥ 95% | 成功预约次数 / 总预约次数 |
| 签到耗时 | ≤ 3秒 | 签到请求到签到完成的时间 |
| 人工处理时间减少 | 50% | (优化前时间 - 优化后时间) / 优化前时间 |
| 数据报表使用率 | ≥ 80% | 使用报表的用户数 / 总用户数 |
| 新会员激活率 | ≥ 70% | 7天内首次到店的新会员数 / 新会员总数 |
| 会员流失率 | ≤ 10% | 流失会员数 / 总会员数 |
| 投诉处理满意度 | ≥ 90% | 满意投诉数 / 总投诉数 |
### 6.2 运营指标
| 指标名称 | 目标值 | 计算方式 |
| ------------------ | ------------ | ---------------------------- |
| 团课满课率 | ≥ 80% | 满员课程数 / 总课程数 |
| 会员活跃度 | ≥ 60% | 活跃会员数 / 总会员数 |
| 会员续费率 | ≥ 70% | 续费会员数 / 到期会员数 |
| 会员卡使用率 | ≥ 85% | 使用会员卡的会员数 / 持卡会员数 |
---
## 七、附录
### 7.1 业务术语表
| 术语 | 定义 |
| ----------------------------- | ------------------------------------------------ |
| 租户(Tenant) | 系统的多租户架构中的独立业务实体,如一个连锁品牌 |
| 门店(Store) | 租户下的具体经营场所 |
| 会员(Member) | 在门店注册的用户 |
| 权益(Benefit) | 会员卡包含的时长、次数、储值、等级等权益 |
| 可预约资源(Bookable Resource) | 团课等可被预约的对象 |
| 时段(Slot) | 资源的可预约时间窗口 |
### 7.2 参考文档
- 《健身房管理系统基础版产品设计文档》 GYM-PRD-BASIC-001
- 《健身房管理系统基础版业务概要设计文档》 GYM-B-HLD-BASIC-001
- 《健身房管理系统基础版技术实现详细设计文档》 GYM-T-ILD-BASIC-001
### 7.3 业务流程图索引
| 流程名称 | 图表位置 |
| ---------------- | ------------ |
| 会员全生命周期流程 | 2.1.2 |
| 支付与退款全流程 | 2.2.2 |
| 投诉与反馈处理流程 | 2.3.2 |
| 会员数据流转 | 3.1 |
| 权益数据流转 | 3.2 |
### 7.4 业务规则索引
| 规则分类 | 规则名称 | 图表位置 |
| ---------------- | ---------------- | ------------ |
| 时间相关规则 | 预约时间 | 4.1 |
| 时间相关规则 | 取消预约 | 4.1 |
| 时间相关规则 | 团课取消 | 4.1 |
| 时间相关规则 | 支付超时 | 4.1 |
| 时间相关规则 | 新会员激活期 | 4.1 |
| 时间相关规则 | 沉默期触发 | 4.1 |
| 时间相关规则 | 流失预警 | 4.1 |
| 时间相关规则 | 流失定义 | 4.1 |
| 时间相关规则 | 投诉响应 | 4.1 |
| 时间相关规则 | 投诉处理 | 4.1 |
| 时间相关规则 | 退款时效 | 4.1 |
| 数量相关规则 | 团课容量 | 4.2 |
| 数量相关规则 | 自动退款 | 4.2 |
| 数量相关规则 | 手续费7-30天 | 4.2 |
| 数量相关规则 | 手续费30天以上 | 4.2 |
| 状态相关规则 | 活跃期 | 4.3 |
| 状态相关规则 | 沉默期 | 4.3 |
| 状态相关规则 | 流失预警 | 4.3 |
| 状态相关规则 | 流失 | 4.3 |
---
**文档结束**
---
## 二、详细业务流程(续)
### 2.4 UI 模版定制模块
#### 2.4.1 业务场景
健身房管理者可以根据品牌特色自定义系统界面,包括品牌 Logo、主题色、布局风格等,提升品牌形象和用户体验。
#### 2.4.2 业务数据流转
```mermaid
flowchart TB
subgraph UI 模版定制流程
A[管理员进入 UI 设置] --> B[选择定制类型]
B --> C{定制类型}
C -->|品牌定制 | D[上传品牌 Logo]
C -->|主题色定制 | E[选择主题色]
C -->|布局定制 | F[选择布局模板]
D --> G[预览效果]
E --> G
F --> G
G --> H{确认发布}
H -->|是 | I[保存到数据库]
H -->|否 | B
I --> J[通知所有用户]
J --> K[更新缓存]
K --> L[完成定制]
end
style A fill:#e1f5ff
style G fill:#fff4e1
style I fill:#e8f5e9
style L fill:#e8f5e9
```
#### 2.4.3 业务规则
**品牌定制规则**
- 支持上传 PNG、JPG 格式的 Logo 文件,最大 5MB
- ✅ 场景 1:管理员上传 PNG 格式 Logo(2MB),上传成功
- ✅ 场景 2:管理员上传 JPG 格式 Logo(3MB),上传成功
- ❌ 场景 3:管理员上传 GIF 格式 Logo(1MB),格式不支持
- ❌ 场景 4:管理员上传 PNG 格式 Logo(6MB),文件大小超限
**主题色定制规则**
- 提供预设色板,支持自定义色值输入
- ✅ 场景 1:管理员从预设色板选择蓝色主题,应用成功
- ✅ 场景 2:管理员输入自定义色值#1890FF,应用成功
- ❌ 场景 3:管理员输入无效色值#GGGGGG,提示格式错误
- ❌ 场景 4:管理员选择与 Logo 颜色冲突的主题色,系统提示建议
**布局定制规则**
- 提供 3 种预设布局模板(经典、现代、简约)
- ✅ 场景 1:管理员选择经典布局,应用成功
- ✅ 场景 2:管理员选择现代布局,应用成功
- ✅ 场景 3:管理员选择简约布局,应用成功
- ❌ 场景 4:管理员自定义布局超出预设范围,提示不支持
**预览规则**
- 支持实时预览,预览效果与实际效果一致
- ✅ 场景 1:管理员修改主题色,实时预览更新
- ✅ 场景 2:管理员切换布局模板,实时预览更新
- ✅ 场景 3:管理员上传 Logo,实时预览更新
- ❌ 场景 4:管理员修改后未预览直接发布,系统强制要求预览
**发布规则**
- 发布后即时生效,所有用户端同步更新
- ✅ 场景 1:管理员发布新主题,会员小程序即时更新
- ✅ 场景 2:管理员发布新主题,教练端 App 即时更新
- ✅ 场景 3:管理员发布新主题,管理后台 PC 即时更新
- ❌ 场景 4:管理员发布后部分用户未更新,系统自动清理缓存
#### 2.4.4 异常处理
| 异常场景 | 处理方式 |
|---------|---------|
| Logo 上传失败 | 提示文件大小或格式错误,建议重新上传 |
| 主题色不兼容 | 提示颜色冲突,推荐兼容色板 |
| 预览加载失败 | 重新加载预览,失败则提示网络问题 |
| 发布失败 | 回滚到上一个版本,提示发布失败原因 |
| 缓存更新失败 | 强制清理缓存,通知运维介入 |
#### 2.4.5 业务指标
| 指标名称 | 目标值 | 计算方式 |
|---------|--------|---------|
| UI 定制使用率 | ≥ 60% | 使用定制的门店数 / 总门店数 |
| 定制满意度 | ≥ 85% | 满意评价数 / 总评价数 |
| 预览加载时间 | ≤ 2 秒 | 预览请求到渲染完成的时间 |
| 发布成功率 | ≥ 99% | 成功发布次数 / 总发布次数 |
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
File diff suppressed because it is too large Load Diff
-958
View File
@@ -1,958 +0,0 @@
# 健身房管理系统前端安全规范文档
> 文档编号: GYM-FE-SEC-001
> 版本: v1.0
> 日期: 2026-03-04
> 作者: 张翔
> 状态: 初稿
---
## 文档修订历史
| 版本 | 日期 | 作者 | 修订内容 |
| ---- | ---------- | ---- | -------- |
| v1.0 | 2026-03-04 | 张翔 | 创建前端安全规范 |
---
## 参考文档
- 《健身房管理系统前端技术架构详细设计》 GYM-FE-ARCH-001
- OWASP Top 10 Web Application Security Risks
- Content Security Policy (CSP) Level 3
- Web Content Accessibility Guidelines (WCAG) 2.1
---
## 一、安全概述
### 1.1 安全目标
- **数据安全**:保护用户敏感数据(手机号、身份证号、银行卡号等)
- **交易安全**:确保支付和充值操作的安全性
- **身份安全**:防止未授权访问和身份冒用
- **隐私保护**:符合GDPR等隐私法规要求
- **合规性**:符合金融行业安全标准和监管要求
### 1.2 安全原则
| 原则 | 描述 | 实施方式 |
|------|------|----------|
| **最小权限** | 只授予必要的权限 | 基于角色的访问控制(RBAC) |
| **纵深防御** | 多层安全防护 | 输入验证、输出转义、加密传输 |
| **默认安全** | 默认配置安全 | CSP策略、安全Headers |
| **审计追踪** | 记录关键操作 | 操作日志、异常日志 |
| **持续监控** | 实时安全监控 | 错误监控、性能监控 |
### 1.3 安全威胁
| 威胁类型 | 风险等级 | 防护措施 |
|---------|---------|----------|
| **XSS(跨站脚本攻击)** | 高 | 输入过滤、输出转义、CSP |
| **CSRF(跨站请求伪造)** | 高 | Token验证、SameSite Cookie |
| **点击劫持** | 中 | X-Frame-Options、CSP |
| **中间人攻击** | 高 | HTTPS、HSTS |
| **敏感信息泄露** | 高 | 数据加密、脱敏显示 |
| **暴力破解** | 中 | 验证码、登录限制 |
| **会话劫持** | 高 | 安全Cookie、会话超时 |
---
## 二、XSS防护
### 2.1 输入验证
#### 2.1.1 白名单验证
```typescript
// utils/validator.ts
export function sanitizeInput(input: string, allowedChars: RegExp): string {
return input.replace(allowedChars, '')
}
export function validatePhoneNumber(phone: string): boolean {
const phoneRegex = /^1[3-9]\d{9}$/
return phoneRegex.test(phone)
}
export function validateIdCard(idCard: string): boolean {
const idCardRegex = /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/
return idCardRegex.test(idCard)
}
export function validateEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailRegex.test(email)
}
```
#### 2.1.2 输入长度限制
```typescript
// utils/validator.ts
export const MAX_INPUT_LENGTH = {
name: 64,
phone: 11,
idCard: 18,
address: 256,
remark: 512
}
export function validateLength(input: string, maxLength: number): boolean {
return input.length <= maxLength
}
```
### 2.2 输出转义
#### 2.2.1 HTML转义
```typescript
// utils/sanitize.ts
import DOMPurify from 'dompurify'
export function sanitizeHtml(html: string): string {
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u'],
ALLOWED_ATTR: []
})
}
export function escapeHtml(text: string): string {
const map: Record<string, string> = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
}
return text.replace(/[&<>"']/g, (m) => map[m])
}
```
#### 2.2.2 URL转义
```typescript
// utils/sanitize.ts
export function sanitizeUrl(url: string): string {
try {
const parsed = new URL(url)
if (!['http:', 'https:'].includes(parsed.protocol)) {
return '#'
}
return url
} catch {
return '#'
}
}
```
### 2.3 CSP策略
#### 2.3.1 基础CSP配置
```html
<!-- index.html -->
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net;
style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net;
img-src 'self' data: https:;
font-src 'self' https://cdn.jsdelivr.net;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';">
```
#### 2.3.2 动态CSP配置
```typescript
// utils/csp.ts
export function generateCSP(config: CSPConfig): string {
const directives = [
`default-src ${config.defaultSrc.join(' ')}`,
`script-src ${config.scriptSrc.join(' ')}`,
`style-src ${config.styleSrc.join(' ')}`,
`img-src ${config.imgSrc.join(' ')}`,
`connect-src ${config.connectSrc.join(' ')}`
]
return directives.join('; ')
}
// 使用
const csp = generateCSP({
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "https://cdn.jsdelivr.net"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "https://api.example.com"]
})
document.querySelector('meta[http-equiv="Content-Security-Policy"]')
?.setAttribute('content', csp)
```
---
## 三、CSRF防护
### 3.1 Token验证
#### 3.1.1 Token生成与存储
```typescript
// utils/csrf.ts
export function generateCSRFToken(): string {
const array = new Uint8Array(32)
crypto.getRandomValues(array)
return Array.from(array, (byte) => byte.toString(16).padStart(2, '0')).join('')
}
export function setCSRFToken(token: string): void {
localStorage.setItem('csrf_token', token)
}
export function getCSRFToken(): string {
return localStorage.getItem('csrf_token') || ''
}
```
#### 3.1.2 Token注入请求
```typescript
// api/request.ts
import { getCSRFToken } from '@/utils/csrf'
instance.interceptors.request.use((config) => {
const csrfToken = getCSRFToken()
if (csrfToken) {
config.headers['X-CSRF-Token'] = csrfToken
}
return config
})
```
### 3.2 SameSite Cookie
```typescript
// api/request.ts
instance.interceptors.request.use((config) => {
config.withCredentials = true
return config
})
```
### 3.3 双重Cookie提交
```typescript
// utils/csrf.ts
export function setDoubleSubmitCookie(token: string): void {
document.cookie = `csrf_token=${token}; path=/; SameSite=Strict; Secure`
}
export function getDoubleSubmitCookie(): string {
const match = document.cookie.match(/csrf_token=([^;]+)/)
return match ? match[1] : ''
}
```
---
## 四、数据安全
### 4.1 数据加密
#### 4.1.1 AES加密
```typescript
// utils/crypto.ts
import CryptoJS from 'crypto-js'
const SECRET_KEY = import.meta.env.VITE_CRYPTO_SECRET_KEY
export function encrypt(text: string): string {
const key = CryptoJS.enc.Utf8.parse(SECRET_KEY)
const iv = CryptoJS.lib.WordArray.random(16)
const encrypted = CryptoJS.AES.encrypt(text, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
})
return iv.toString() + ':' + encrypted.toString()
}
export function decrypt(ciphertext: string): string {
const key = CryptoJS.enc.Utf8.parse(SECRET_KEY)
const [ivHex, encrypted] = ciphertext.split(':')
const iv = CryptoJS.enc.Hex.parse(ivHex)
const decrypted = CryptoJS.AES.decrypt(encrypted, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
})
return decrypted.toString(CryptoJS.enc.Utf8)
}
```
#### 4.1.2 RSA加密(用于敏感数据)
```typescript
// utils/crypto.ts
import JSEncrypt from 'jsencrypt'
const PUBLIC_KEY = import.meta.env.VITE_RSA_PUBLIC_KEY
export function encryptWithRSA(text: string): string {
const encrypt = new JSEncrypt()
encrypt.setPublicKey(PUBLIC_KEY)
return encrypt.encrypt(text) || ''
}
```
### 4.2 数据脱敏
#### 4.2.1 手机号脱敏
```typescript
// utils/mask.ts
export function maskPhone(phone: string): string {
if (!phone || phone.length !== 11) {
return phone
}
return phone.substring(0, 3) + '****' + phone.substring(7)
}
```
#### 4.2.2 身份证号脱敏
```typescript
// utils/mask.ts
export function maskIdCard(idCard: string): string {
if (!idCard || idCard.length !== 18) {
return idCard
}
return idCard.substring(0, 6) + '********' + idCard.substring(14)
}
```
#### 4.2.3 银行卡号脱敏
```typescript
// utils/mask.ts
export function maskBankCard(bankCard: string): string {
if (!bankCard || bankCard.length < 16) {
return bankCard
}
return bankCard.substring(0, 4) + ' **** **** ' + bankCard.substring(bankCard.length - 4)
}
```
### 4.3 敏感信息存储
#### 4.3.1 安全存储
```typescript
// utils/storage.ts
import { encrypt, decrypt } from './crypto'
export const secureStorage = {
setItem(key: string, value: any): void {
const encrypted = encrypt(JSON.stringify(value))
localStorage.setItem(key, encrypted)
},
getItem<T>(key: string): T | null {
const encrypted = localStorage.getItem(key)
if (!encrypted) return null
try {
return JSON.parse(decrypt(encrypted)) as T
} catch {
return null
}
},
removeItem(key: string): void {
localStorage.removeItem(key)
},
clear(): void {
localStorage.clear()
}
}
```
#### 4.3.2 会话存储
```typescript
// utils/storage.ts
export const sessionStorage = {
setItem(key: string, value: any): void {
const encrypted = encrypt(JSON.stringify(value))
window.sessionStorage.setItem(key, encrypted)
},
getItem<T>(key: string): T | null {
const encrypted = window.sessionStorage.getItem(key)
if (!encrypted) return null
try {
return JSON.parse(decrypt(encrypted)) as T
} catch {
return null
}
},
removeItem(key: string): void {
window.sessionStorage.removeItem(key)
},
clear(): void {
window.sessionStorage.clear()
}
}
```
---
## 五、身份认证与授权
### 5.1 认证安全
#### 5.1.1 密码安全
```typescript
// utils/password.ts
export function validatePassword(password: string): { valid: boolean; message?: string } {
if (password.length < 8) {
return { valid: false, message: '密码长度至少8位' }
}
if (!/[A-Z]/.test(password)) {
return { valid: false, message: '密码必须包含大写字母' }
}
if (!/[a-z]/.test(password)) {
return { valid: false, message: '密码必须包含小写字母' }
}
if (!/[0-9]/.test(password)) {
return { valid: false, message: '密码必须包含数字' }
}
if (!/[!@#$%^&*]/.test(password)) {
return { valid: false, message: '密码必须包含特殊字符' }
}
return { valid: true }
}
```
#### 5.1.2 Token管理
```typescript
// stores/auth.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useAuthStore = defineStore('auth', () => {
const token = ref<string>('')
const refreshToken = ref<string>('')
const tokenExpireTime = ref<number>(0)
const setToken = (newToken: string, expireIn: number) => {
token.value = newToken
tokenExpireTime.value = Date.now() + expireIn * 1000
secureStorage.setItem('auth_token', newToken)
secureStorage.setItem('token_expire_time', tokenExpireTime.value)
}
const setRefreshToken = (newRefreshToken: string) => {
refreshToken.value = newRefreshToken
secureStorage.setItem('refresh_token', newRefreshToken)
}
const isTokenExpired = (): boolean => {
return Date.now() >= tokenExpireTime.value
}
const clearAuth = () => {
token.value = ''
refreshToken.value = ''
tokenExpireTime.value = 0
secureStorage.removeItem('auth_token')
secureStorage.removeItem('refresh_token')
secureStorage.removeItem('token_expire_time')
}
return {
token,
refreshToken,
tokenExpireTime,
setToken,
setRefreshToken,
isTokenExpired,
clearAuth
}
})
```
#### 5.1.3 Token刷新
```typescript
// api/request.ts
import { useAuthStore } from '@/stores/auth'
instance.interceptors.request.use(async (config) => {
const authStore = useAuthStore()
if (authStore.isTokenExpired()) {
try {
const response = await instance.post('/auth/refresh', {
refreshToken: authStore.refreshToken
})
authStore.setToken(response.token, response.expireIn)
authStore.setRefreshToken(response.refreshToken)
config.headers.Authorization = `Bearer ${response.token}`
} catch (error) {
authStore.clearAuth()
window.location.href = '/login'
return Promise.reject(error)
}
} else {
config.headers.Authorization = `Bearer ${authStore.token}`
}
return config
})
```
### 5.2 授权安全
#### 5.2.1 权限验证
```typescript
// utils/permission.ts
export interface Permission {
resource: string
action: string
}
export function hasPermission(userPermissions: string[], required: Permission): boolean {
const permissionString = `${required.resource}:${required.action}`
return userPermissions.includes(permissionString)
}
export function hasAnyPermission(userPermissions: string[], required: Permission[]): boolean {
return required.some(p => hasPermission(userPermissions, p))
}
export function hasAllPermissions(userPermissions: string[], required: Permission[]): boolean {
return required.every(p => hasPermission(userPermissions, p))
}
```
#### 5.2.2 路由权限守卫
```typescript
// router/guards/permission.ts
import { useAuthStore } from '@/stores/auth'
import { usePermissionStore } from '@/stores/permission'
router.beforeEach(async (to, from, next) => {
const authStore = useAuthStore()
const permissionStore = usePermissionStore()
if (to.meta.requiresAuth && !authStore.token) {
next('/login')
return
}
if (to.meta.permission) {
const required = to.meta.permission as Permission
if (!hasPermission(permissionStore.permissions, required)) {
next('/403')
return
}
}
next()
})
```
---
## 六、安全Headers
### 6.1 基础安全Headers
```typescript
// utils/headers.ts
export const securityHeaders = {
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'X-XSS-Protection': '1; mode=block',
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
'Referrer-Policy': 'strict-origin-when-cross-origin',
'Permissions-Policy': 'geolocation=(), microphone=(), camera=()'
}
```
### 6.2 动态设置Headers
```typescript
// api/request.ts
instance.interceptors.request.use((config) => {
Object.assign(config.headers, securityHeaders)
return config
})
```
---
## 七、点击劫持防护
### 7.1 X-Frame-Options
```html
<!-- index.html -->
<meta http-equiv="X-Frame-Options" content="DENY">
```
### 7.2 CSP frame-ancestors
```html
<!-- index.html -->
<meta http-equiv="Content-Security-Policy"
content="frame-ancestors 'none';">
```
### 7.3 JavaScript防护
```typescript
// utils/clickjacking.ts
export function preventClickjacking(): void {
if (window.self !== window.top) {
window.top.location = window.self.location
}
}
// 在应用初始化时调用
preventClickjacking()
```
---
## 八、安全键盘
### 8.1 安全键盘组件
```vue
<!-- components/base/SecureKeyboard.vue -->
<template>
<div class="secure-keyboard">
<div class="keyboard-display">
<span v-for="i in maskedValue.length" :key="i"></span>
</div>
<div class="keyboard-grid">
<button
v-for="key in keys"
:key="key"
@click="handleKeyPress(key)"
class="key-button"
>
{{ key }}
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
const keys = ['1', '2', '3', '4', '5', '6', '7', '8', '9', 'C', '0', '⌫']
const value = ref<string>('')
const maxLength = 6
const maskedValue = computed(() => value.value)
const emit = defineEmits<{
input: [value: string]
complete: [value: string]
}>()
const handleKeyPress = (key: string) => {
if (key === 'C') {
value.value = ''
} else if (key === '⌫') {
value.value = value.value.slice(0, -1)
} else if (value.value.length < maxLength) {
value.value += key
}
emit('input', value.value)
if (value.value.length === maxLength) {
emit('complete', value.value)
}
}
</script>
<style scoped>
.secure-keyboard {
background: #f5f5f5;
padding: 16px;
border-radius: 8px;
}
.keyboard-display {
display: flex;
justify-content: center;
gap: 8px;
margin-bottom: 16px;
font-size: 24px;
}
.keyboard-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.key-button {
padding: 16px;
font-size: 20px;
background: white;
border: 1px solid #ddd;
border-radius: 8px;
cursor: pointer;
}
.key-button:active {
background: #e0e0e0;
}
</style>
```
---
## 九、安全日志与监控
### 9.1 操作日志
```typescript
// utils/logger.ts
interface LogEntry {
timestamp: number
level: 'info' | 'warn' | 'error'
action: string
userId?: number
details?: any
}
export class SecurityLogger {
private logs: LogEntry[] = []
log(action: string, details?: any, level: 'info' | 'warn' | 'error' = 'info') {
const entry: LogEntry = {
timestamp: Date.now(),
level,
action,
userId: this.getUserId(),
details
}
this.logs.push(entry)
this.sendToServer(entry)
}
private getUserId(): number | undefined {
const authStore = useAuthStore()
return authStore.user?.id
}
private async sendToServer(entry: LogEntry) {
try {
await api.post('/security/log', entry)
} catch (error) {
console.error('Failed to send security log:', error)
}
}
}
export const securityLogger = new SecurityLogger()
```
### 9.2 异常监控
```typescript
// utils/sentry.ts
import * as Sentry from '@sentry/vue'
export function setupSentry(app: App) {
Sentry.init({
app,
dsn: import.meta.env.VITE_SENTRY_DSN,
environment: import.meta.env.MODE,
tracesSampleRate: 1.0,
beforeSend(event) {
if (event.request) {
delete event.request.cookies
delete event.request.headers
}
return event
}
})
}
// 全局错误处理
window.addEventListener('error', (event) => {
securityLogger.log('window.error', {
message: event.message,
filename: event.filename,
lineno: event.lineno
}, 'error')
})
window.addEventListener('unhandledrejection', (event) => {
securityLogger.log('unhandledrejection', {
reason: event.reason
}, 'error')
})
```
---
## 十、安全最佳实践
### 10.1 开发阶段
1. **代码审查**
- 所有代码必须经过安全审查
- 使用ESLint安全规则
- 使用npm audit检查依赖漏洞
2. **依赖管理**
- 定期更新依赖包
- 使用npm audit fix修复漏洞
- 使用Snyk等工具监控依赖安全
3. **环境变量**
- 敏感信息使用环境变量
- 不要将密钥提交到代码仓库
- 使用.env文件管理配置
### 10.2 测试阶段
1. **安全测试**
- 使用OWASP ZAP进行安全扫描
- 进行渗透测试
- 测试XSS、CSRF等漏洞
2. **代码扫描**
- 使用SonarQube进行代码质量检查
- 使用ESLint进行代码规范检查
- 使用Prettier进行代码格式化
### 10.3 部署阶段
1. **HTTPS强制**
- 使用SSL证书
- 配置HSTS
- 禁用HTTP访问
2. **安全配置**
- 配置CSP策略
- 配置安全Headers
- 配置防火墙规则
3. **监控告警**
- 配置错误监控
- 配置性能监控
- 配置安全告警
---
## 十一、合规性要求
### 11.1 GDPR合规
1. **数据最小化**
- 只收集必要的用户数据
- 提供数据删除功能
- 提供数据导出功能
2. **用户同意**
- 明确告知数据使用目的
- 获取用户明确同意
- 提供撤回同意的选项
3. **数据保护**
- 加密存储敏感数据
- 限制数据访问权限
- 定期进行安全审计
### 11.2 无障碍合规
1. **WCAG 2.1 AA级标准**
- 键盘导航支持
- 屏幕阅读器支持
- 颜色对比度符合标准
2. **ARIA标签**
- 为交互元素添加ARIA标签
- 为动态内容添加ARIA标签
- 为表单元素添加ARIA标签
---
## 十二、安全检查清单
### 12.1 代码提交前检查
- [ ] 所有用户输入都经过验证和过滤
- [ ] 所有输出都经过转义
- [ ] 敏感数据都经过加密存储
- [ ] 敏感信息都经过脱敏显示
- [ ] 所有API请求都包含CSRF Token
- [ ] 所有页面都配置了CSP策略
- [ ] 所有页面都配置了安全Headers
- [ ] 所有密码都符合复杂度要求
- [ ] 所有权限都经过验证
- [ ] 所有操作都记录了日志
### 12.2 部署前检查
- [ ] 所有依赖包都是最新版本
- [ ] 所有依赖包都没有已知漏洞
- [ ] 所有环境变量都正确配置
- [ ] 所有HTTPS证书都有效
- [ ] 所有监控和告警都正常工作
- [ ] 所有安全策略都正确配置
- [ ] 所有安全测试都通过
- [ ] 所有安全扫描都通过
- [ ] 所有安全文档都完整
- [ ] 所有安全培训都完成
---
## 十三、总结
本文档详细描述了健身房管理系统前端的安全规范,包括:
1. **安全概述**:安全目标、安全原则、安全威胁
2. **XSS防护**:输入验证、输出转义、CSP策略
3. **CSRF防护**Token验证、SameSite Cookie、双重Cookie提交
4. **数据安全**:数据加密、数据脱敏、敏感信息存储
5. **身份认证与授权**:认证安全、授权安全
6. **安全Headers**:基础安全Headers、动态设置Headers
7. **点击劫持防护**X-Frame-Options、CSP frame-ancestors
8. **安全键盘**:安全键盘组件
9. **安全日志与监控**:操作日志、异常监控
10. **安全最佳实践**:开发阶段、测试阶段、部署阶段
11. **合规性要求**GDPR合规、无障碍合规
12. **安全检查清单**:代码提交前检查、部署前检查
通过遵循本文档的安全规范,可以确保健身房管理系统前端的安全性,符合金融级安全标准和监管要求。
+48 -41
View File
@@ -4,7 +4,7 @@
> 版本: v1.0 > 版本: v1.0
> 日期: 2026-03-04 > 日期: 2026-03-04
> 作者: 张翔 > 作者: 张翔
> 状态: 初稿 > 状态: 正式发布
--- ---
@@ -37,46 +37,53 @@
### 1.2 工程化体系 ### 1.2 工程化体系
``` ```mermaid
┌─────────────────────────────────────────────────────────────────────────┐ flowchart TB
│ 前端工程化体系 │ subgraph DevTools["开发工具链"]
├─────────────────────────────────────────────────────────────────────────┤ DT1["Node.js"]
│ │ DT2["npm/yarn"]
│ ┌─────────────────────────────────────────────────────────────────┐ │ DT3["Git"]
│ │ 开发工具链 │ │ DT4["VSCode"]
│ ├─────────────────────────────────────────────────────────────────┤ │ end
│ │ • Node.js • npm/yarn • Git • VSCode │ │
│ └─────────────────────────────────────────────────────────────────┘ │ subgraph BuildTools["构建工具"]
│ │ │ BT1["Vite"]
│ ▼ │ BT2["TypeScript"]
│ ┌─────────────────────────────────────────────────────────────────┐ │ BT3["ESLint"]
│ │ 构建工具 │ │ BT4["Prettier"]
│ ├─────────────────────────────────────────────────────────────────┤ │ end
│ │ • Vite • TypeScript • ESLint • Prettier │ │
│ └─────────────────────────────────────────────────────────────────┘ │ subgraph QualityTools["代码质量工具"]
│ │ │ QT1["Husky"]
│ ▼ │ QT2["Commitlint"]
│ ┌─────────────────────────────────────────────────────────────────┐ │ QT3["Lint-staged"]
│ │ 代码质量工具 │ │ QT4["Stylelint"]
│ ├─────────────────────────────────────────────────────────────────┤ │ end
│ │ • Husky • Commitlint • Lint-staged • Stylelint │ │
│ └─────────────────────────────────────────────────────────────────┘ │ subgraph TestTools["测试工具"]
│ │ │ TT1["Vitest"]
│ ▼ │ TT2["Playwright"]
│ ┌─────────────────────────────────────────────────────────────────┐ │ TT3["Coverage"]
│ │ 测试工具 │ │ TT4["Testing Library"]
│ ├─────────────────────────────────────────────────────────────────┤ │ end
│ │ • Vitest • Playwright • Coverage • Testing Library │ │
│ └─────────────────────────────────────────────────────────────────┘ │ subgraph CICD["CI/CD工具"]
│ │ │ CD1["GitHub Actions"]
│ ▼ │ CD2["Docker"]
│ ┌─────────────────────────────────────────────────────────────────┐ │ CD3["Nginx"]
│ │ CI/CD工具 │ │ CD4["CDN"]
│ ├─────────────────────────────────────────────────────────────────┤ │ end
│ │ • GitHub Actions • Docker • Nginx • CDN │ │
│ └─────────────────────────────────────────────────────────────────┘ │ DevTools --> BuildTools
│ │ BuildTools --> QualityTools
└─────────────────────────────────────────────────────────────────────────┘ QualityTools --> TestTools
TestTools --> CICD
style DevTools fill:#e1f5ff
style BuildTools fill:#fff4e1
style QualityTools fill:#f0e1ff
style TestTools fill:#e1ffe1
style CICD fill:#ffe1e1
``` ```
--- ---
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+23 -70
View File
@@ -4,7 +4,7 @@
> 版本: v1.0 > 版本: v1.0
> 日期: 2026-03-04 > 日期: 2026-03-04
> 作者: 张翔 > 作者: 张翔
> 状态: 初稿 > 状态: 正式发布
--- ---
@@ -19,8 +19,8 @@
## 参考文档 ## 参考文档
- 《健身房管理系统基础版产品设计文档》 GYM-PRD-BASIC-001 - 《健身房管理系统基础版产品设计文档》 GYM-PRD-BASIC-001
- 《健身房管理系统基础版系统概要设计文档》 GYM-HLD-BASIC-001 - 《健身房管理系统基础版业务概要设计文档》 GYM-B-HLD-BASIC-001
- 《健身房管理系统基础版系统详细设计文档》 GYM-LLD-BASIC-001 - 《健身房管理系统基础版业务详细设计文档》 GYM-B-LLD-BASIC-001
- Vue 3 官方文档 - Vue 3 官方文档
- uniapp 官方文档 - uniapp 官方文档
- TypeScript 官方文档 - TypeScript 官方文档
@@ -39,33 +39,13 @@
### 1.2 客户端架构 ### 1.2 客户端架构
``` ```mermaid
┌─────────────────────────────────────────────────────────────────────────┐ graph LR
│ 前端客户端架构 │ subgraph 前端客户端架构
├─────────────────────────────────────────────────────────────────────────┤ A[会员小程序 uniapp+Vue3<br/>• 会员注册/登录<br/>• 课程预约<br/>• 扫码签到<br/>• 会员卡管理<br/>• 个人中心<br/>• 消息通知<br/>• 数据统计]
│ │ B[教练端App uniapp+Vue3<br/>• 课程管理<br/>• 排班管理<br/>• 会员管理<br/>• 签到管理<br/>• 数据统计<br/>• 消息通知<br/>• 个人中心]
│ ┌─────────────────────────────────────────────────────────────────┐ │ C[管理后台PC Vue3+Vite+Element Plus<br/>• 会员管理<br/>• 课程管理<br/>• 预约管理<br/>• 签到管理<br/>• 财务管理<br/>• 数据统计<br/>• 系统管理<br/>• 订阅管理]
│ │ 会员小程序 (uniapp+Vue3) │ │ end
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 会员注册/登录 • 课程预约 • 扫码签到 • 会员卡管理 │ │
│ │ • 个人中心 • 消息通知 • 数据统计 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 教练端App (uniapp+Vue3) │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 课程管理 • 排班管理 • 会员管理 • 签到管理 │ │
│ │ • 数据统计 • 消息通知 • 个人中心 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 管理后台PC (Vue3+Vite+Element Plus) │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 会员管理 • 课程管理 • 预约管理 • 签到管理 │ │
│ │ • 财务管理 • 数据统计 • 系统管理 • 订阅管理 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
``` ```
### 1.3 技术栈选型 ### 1.3 技术栈选型
@@ -116,46 +96,19 @@
### 2.1 分层架构 ### 2.1 分层架构
``` ```mermaid
┌─────────────────────────────────────────────────────────────────────────┐ flowchart TB
│ 前端分层架构 │ subgraph 前端分层架构
├─────────────────────────────────────────────────────────────────────────┤ A[表现层 Presentation Layer<br/>• 页面组件<br/>• 业务组件<br/>• 基础组件<br/>• 布局组件]
│ │ B[状态管理层 State Management Layer<br/>• 全局状态<br/>• 模块状态<br/>• 组件状态<br/>• 持久化状态]
│ ┌─────────────────────────────────────────────────────────────────┐ │ C[业务逻辑层 Business Logic Layer<br/>• Composables<br/>• Hooks<br/>• Utils<br/>• Validators]
│ │ 表现层 (Presentation Layer) │ │ D[数据访问层 Data Access Layer<br/>• API Service<br/>• WebSocket<br/>• Cache<br/>• Storage]
│ ├─────────────────────────────────────────────────────────────────┤ │ E[基础设施层 Infrastructure Layer<br/>• 路由<br/>• 拦截器<br/>• 错误处理<br/>• 日志<br/>• 监控]
│ │ • 页面组件 • 业务组件 • 基础组件 • 布局组件 │ │ A --> B
│ └─────────────────────────────────────────────────────────────────┘ │ B --> C
│ │ │ C --> D
│ ▼ │ D --> E
│ ┌─────────────────────────────────────────────────────────────────┐ │ end
│ │ 状态管理层 (State Management Layer) │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 全局状态 • 模块状态 • 组件状态 • 持久化状态 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 业务逻辑层 (Business Logic Layer) │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • Composables • Hooks • Utils • Validators │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 数据访问层 (Data Access Layer) │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • API Service • WebSocket • Cache • Storage │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 基础设施层 (Infrastructure Layer) │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 路由 • 拦截器 • 错误处理 • 日志 • 监控 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
``` ```
### 2.2 模块划分 ### 2.2 模块划分
-924
View File
@@ -1,924 +0,0 @@
# 健身房管理系统前端测试规范文档
> 文档编号: 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、提高系统稳定性。