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
@@ -1,27 +1,26 @@
# 健身房管理系统付费订阅版详细设计文档(LLD # 健身房管理系统付费订阅版技术实现详细设计文档(T-ILD
> 文档编号: GYM-LLD-SUBSCRIPTION-001 > 文档编号: GYM-T-ILD-SUBSCRIPTION-001
> 版本: v1.0 > 版本: v1.0
> 日期: 2026-03-04 > 日期: 2026-03-08
> 作者: 张翔 > 作者: 张翔
> 状态: 初稿 > 状态: 已发布
--- ---
## 文档修订历史 ## 文档修订历史
| 版本 | 日期 | 作者 | 修订内容 | | 版本 | 日期 | 作者 | 修订内容 |
| ---- | ---------- | ---- | -------- | | ---- | ---------- | ---- | ---------------------- |
| v1.0 | 2026-03-04 | 张翔 | 创建付费订阅版详细设计 | | v1.0 | 2026-03-08 | 张翔 | 创建付费订阅版技术实现详细设计文档 |
--- ---
## 参考文档 ## 参考文档
- 《健身房管理系统付费订阅版产品设计文档》 GYM-PRD-SUBSCRIPTION-001 - 《健身房管理系统付费订阅版产品设计文档》 GYM-PRD-SUBSCRIPTION-001
- 《健身房管理系统付费订阅版业务概要设计文档》 GYM-HLD-SUBSCRIPTION-001 - 《健身房管理系统付费订阅版业务概要设计文档》 GYM-B-HLD-SUBSCRIPTION-001
- 《健身房管理系统详细设计文档》 GYM-LLD-000 - 《健身房管理系统付费订阅版业务详细设计文档》 GYM-B-LLD-SUBSCRIPTION-001
- 《订阅与配置模块详细设计文档》 GYM-LLD-004
- Spring Boot 3 官方文档 - Spring Boot 3 官方文档
- R2DBC 规范文档 - R2DBC 规范文档
- PostgreSQL 官方文档 - PostgreSQL 官方文档
@@ -30,63 +29,75 @@
## 一、系统架构设计 ## 一、系统架构设计
### 1.0 付费订阅版性能与架构特点
#### 1.0.1 性能目标
| 指标类型 | 指标项 | 目标值 | 说明 |
|---------|--------|--------|------|
| **应用指标** | 并发数 | ≤ 500 | 付费订阅版支持500并发用户 |
| **应用指标** | API响应时间 | ≤ 500ms | 95%请求响应时间 |
| **应用指标** | 系统可用性 | ≥ 99.9% | 年度目标 |
| **数据库指标** | 连接池大小 | 100 | R2DBC连接池 |
| **数据库指标** | 查询响应时间 | ≤ 200ms | 95%查询响应时间 |
| **缓存指标** | 缓存命中率 | ≥ 85% | Redis缓存 |
| **缓存指标** | 缓存响应时间 | ≤ 10ms | Redis缓存 |
#### 1.0.2 架构特点
付费订阅版采用增强型架构设计,满足中大型健身房和连锁品牌的需求:
**1. 单体应用架构(可扩展为分布式)**
- 支持多门店管理
- 支持跨店数据同步
- 数据隔离通过租户ID和门店ID实现
**2. 增强资源配置**
- 数据库连接池:100个连接
- Redis缓存:4GB内存
- RabbitMQ队列:多队列集群模式
- Elasticsearch3节点集群部署
**3. 扩展性增强**
- 支持多门店管理
- 支持分布式部署(可选)
- 支持高可用集群(可选)
- 并发用户数提升至500
#### 1.0.3 与基础版的差异
| 维度 | 基础版 | 付费订阅版 |
|------|--------|-----------|
| **并发用户数** | 100 | 500 |
| **数据库连接池** | 20 | 100 |
| **Redis内存** | 1GB | 4GB |
| **RabbitMQ队列** | 单队列 | 多队列集群 |
| **Elasticsearch** | 单节点 | 3节点集群 |
| **多门店支持** | 不支持 | 支持 |
| **分布式部署** | 不支持 | 支持 |
| **高可用集群** | 不支持 | 支持 |
---
### 1.1 总体架构 ### 1.1 总体架构
采用分层架构 + 模块化设计的单体应用: 采用分层架构 + 模块化设计的单体应用:
``` ```mermaid
┌─────────────────────────────────────────────────────────────────────────┐ flowchart TB
│ 付费订阅版单体应用架构 │ subgraph 付费订阅版单体应用架构
├─────────────────────────────────────────────────────────────────────────┤ A[客户端层<br/>• 会员小程序 uniapp+Vue3<br/>• 教练端App uniapp+Vue3<br/>• 管理后台PC Vue3+Vite<br/>• 硬件设备 人脸/NFC]
│ │ B[Presentation Layer WebFlux<br/>• Controller<br/>• Router<br/>• Filter<br/>• Validator]
│ ┌─────────────────────────────────────────────────────────────────┐ │ C[Application Layer 业务编排<br/>• Service<br/>• Facade<br/>• Orchestrator<br/>• 事务管理]
│ │ 客户端层 │ │ D[Domain Layer 领域模型<br/>• Entity<br/>• Value Object<br/>• Domain Service<br/>• Repository]
│ ├─────────────────────────────────────────────────────────────────┤ │ E[Infrastructure Layer 基础设施<br/>• Repository R2DBC<br/>• Cache Redis<br/>• Message RabbitMQ<br/>• Search Elasticsearch<br/>• File OSS<br/>• Distributed Lock]
│ │ • 会员小程序 (uniapp+Vue3) │ │ F[外部服务层<br/>• PostgreSQL<br/>• Redis<br/>• RabbitMQ<br/>• Elasticsearch<br/>• 微信开放平台<br/>• 短信服务<br/>• 支付服务<br/>• OSS存储]
│ │ • 教练端App (uniapp+Vue3) │ │ A --> B
│ │ • 管理后台PC (Vue3+Vite) │ │ B --> C
│ │ • 硬件设备 (人脸/NFC) │ │ C --> D
│ └─────────────────────────────────────────────────────────────────┘ │ D --> E
│ │ │ E --> F
│ ▼ │ end
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Presentation Layer (WebFlux) │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • Controller • Router • Filter • Validator │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Application Layer (业务编排) │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • Service • Facade • Orchestrator • 事务管理 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Domain Layer (领域模型) │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • Entity • Value Object • Domain Service • Repository │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Infrastructure Layer (基础设施) │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • Repository (R2DBC) • Cache (Redis) │ │
│ │ • Message (RabbitMQ) • Search (Elasticsearch) │ │
│ │ • File (OSS) • Distributed Lock │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 外部服务层 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • PostgreSQL • Redis • RabbitMQ • Elasticsearch │ │
│ │ • 微信开放平台 • 短信服务 • 支付服务 • OSS存储 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
``` ```
--- ---
@@ -802,6 +813,162 @@ public class MarketingActivityService {
--- ---
### 4.2 智能获客工具模块
#### 4.2.1 数据模型设计
**获客活动表 (customer_acquisition)**
```sql
CREATE TABLE customer_acquisition (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
name VARCHAR(128) NOT NULL,
type SMALLINT NOT NULL,
description TEXT,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
config JSONB NOT NULL,
status SMALLINT DEFAULT 1,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
created_by BIGINT,
updated_by BIGINT,
deleted_at TIMESTAMP DEFAULT NULL,
CONSTRAINT fk_customer_acquisition_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id)
);
```
**推荐记录表 (referral_record)**
```sql
CREATE TABLE referral_record (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
activity_id BIGINT,
referrer_id BIGINT NOT NULL,
referee_id BIGINT,
referral_code VARCHAR(32) NOT NULL,
status SMALLINT DEFAULT 1,
reward_status SMALLINT DEFAULT 1,
reward_amount DECIMAL(10, 2),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
deleted_at TIMESTAMP DEFAULT NULL,
CONSTRAINT fk_referral_record_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id),
CONSTRAINT fk_referral_record_activity FOREIGN KEY (activity_id) REFERENCES customer_acquisition(id),
CONSTRAINT fk_referral_record_referrer FOREIGN KEY (referrer_id) REFERENCES member(id),
CONSTRAINT fk_referral_record_referee FOREIGN KEY (referee_id) REFERENCES member(id)
);
```
#### 4.2.2 核心服务设计
**智能获客服务**
```java
@Service
public class CustomerAcquisitionService {
@Autowired
private CustomerAcquisitionRepository customerAcquisitionRepository;
@Autowired
private ReferralRecordRepository referralRecordRepository;
@Autowired
private MemberRepository memberRepository;
@Autowired
private BenefitService benefitService;
/**
* 创建获客活动
*/
public CustomerAcquisition createAcquisitionActivity(Long tenantId, CustomerAcquisitionDTO dto) {
CustomerAcquisition activity = new CustomerAcquisition();
activity.setTenantId(tenantId);
activity.setName(dto.getName());
activity.setType(dto.getType());
activity.setDescription(dto.getDescription());
activity.setStartDate(dto.getStartDate());
activity.setEndDate(dto.getEndDate());
activity.setConfig(dto.getConfig());
activity.setStatus(1);
return customerAcquisitionRepository.save(activity);
}
/**
* 生成推荐码
*/
public String generateReferralCode(Long memberId, Long activityId) {
String referralCode = generateUniqueCode();
ReferralRecord record = new ReferralRecord();
record.setTenantId(getTenantIdByMemberId(memberId));
record.setActivityId(activityId);
record.setReferrerId(memberId);
record.setReferralCode(referralCode);
record.setStatus(1);
record.setRewardStatus(1);
referralRecordRepository.save(record);
return referralCode;
}
/**
* 处理推荐关系
*/
public void processReferral(Long memberId, String referralCode) {
ReferralRecord record = referralRecordRepository.findByReferralCodeAndStatus(referralCode, 1);
if (record == null) {
throw new BusinessException("推荐码无效");
}
record.setRefereeId(memberId);
record.setStatus(2);
referralRecordRepository.save(record);
CustomerAcquisition activity = customerAcquisitionRepository.findById(record.getActivityId()).orElse(null);
if (activity != null && activity.getConfig() != null) {
JSONObject config = activity.getConfig();
if (config.containsKey("rewardAmount")) {
BigDecimal rewardAmount = config.getBigDecimal("rewardAmount");
record.setRewardAmount(rewardAmount);
record.setRewardStatus(2);
benefitService.grantPoints(record.getReferrerId(), rewardAmount.intValue());
}
}
referralRecordRepository.save(record);
}
/**
* 生成唯一推荐码
*/
private String generateUniqueCode() {
return UUID.randomUUID().toString().replace("-", "").substring(0, 12).toUpperCase();
}
/**
* 根据会员ID获取租户ID
*/
private Long getTenantIdByMemberId(Long memberId) {
Member member = memberRepository.findById(memberId).orElse(null);
return member != null ? member.getTenantId() : null;
}
}
```
---
## 五、数据智能类模块设计 ## 五、数据智能类模块设计
### 5.1 高级数据分析模块 ### 5.1 高级数据分析模块
@@ -873,15 +1040,262 @@ public class AdvancedDataAnalysisService {
--- ---
### 5.2 智能体测数据联动模块
#### 5.2.1 数据模型设计
**体测数据表 (body_composition)**
```sql
CREATE TABLE body_composition (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
store_id BIGINT NOT NULL,
member_id BIGINT NOT NULL,
device_type VARCHAR(32) NOT NULL,
device_id VARCHAR(64),
test_date TIMESTAMP NOT NULL,
height DECIMAL(5, 2),
weight DECIMAL(5, 2),
body_fat_rate DECIMAL(5, 2),
muscle_mass DECIMAL(5, 2),
water_content DECIMAL(5, 2),
bone_mass DECIMAL(5, 2),
bmi DECIMAL(5, 2),
raw_data JSONB,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
deleted_at TIMESTAMP DEFAULT NULL,
CONSTRAINT fk_body_composition_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id),
CONSTRAINT fk_body_composition_store FOREIGN KEY (store_id) REFERENCES store(id),
CONSTRAINT fk_body_composition_member FOREIGN KEY (member_id) REFERENCES member(id)
);
```
**体测报告表 (body_test_report)**
```sql
CREATE TABLE body_test_report (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
store_id BIGINT NOT NULL,
member_id BIGINT NOT NULL,
test_data_id BIGINT NOT NULL,
report_no VARCHAR(32) NOT NULL,
report_content JSONB NOT NULL,
analysis_result JSONB NOT NULL,
suggestions TEXT,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
deleted_at TIMESTAMP DEFAULT NULL,
CONSTRAINT fk_body_test_report_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id),
CONSTRAINT fk_body_test_report_store FOREIGN KEY (store_id) REFERENCES store(id),
CONSTRAINT fk_body_test_report_member FOREIGN KEY (member_id) REFERENCES member(id),
CONSTRAINT fk_body_test_report_test_data FOREIGN KEY (test_data_id) REFERENCES body_composition(id)
);
```
#### 5.2.2 核心服务设计
**体测数据服务**
```java
@Service
public class BodyCompositionService {
@Autowired
private BodyCompositionRepository bodyCompositionRepository;
@Autowired
private BodyTestReportRepository bodyTestReportRepository;
@Autowired
private MemberRepository memberRepository;
/**
* 接收体测数据
*/
@Transactional
public BodyComposition receiveBodyData(BodyDataDTO dto) {
BodyComposition bodyData = new BodyComposition();
bodyData.setTenantId(dto.getTenantId());
bodyData.setStoreId(dto.getStoreId());
bodyData.setMemberId(dto.getMemberId());
bodyData.setDeviceType(dto.getDeviceType());
bodyData.setDeviceId(dto.getDeviceId());
bodyData.setTestDate(dto.getTestDate());
bodyData.setHeight(dto.getHeight());
bodyData.setWeight(dto.getWeight());
bodyData.setBodyFatRate(dto.getBodyFatRate());
bodyData.setMuscleMass(dto.getMuscleMass());
bodyData.setWaterContent(dto.getWaterContent());
bodyData.setBoneMass(dto.getBoneMass());
bodyData.setBmi(calculateBMI(dto.getHeight(), dto.getWeight()));
bodyData.setRawData(dto.getRawData());
return bodyCompositionRepository.save(bodyData);
}
/**
* 生成体测报告
*/
@Transactional
public BodyTestReport generateReport(Long testDataId) {
BodyComposition bodyData = bodyCompositionRepository.findById(testDataId).orElse(null);
if (bodyData == null) {
throw new BusinessException("体测数据不存在");
}
BodyTestReport report = new BodyTestReport();
report.setTenantId(bodyData.getTenantId());
report.setStoreId(bodyData.getStoreId());
report.setMemberId(bodyData.getMemberId());
report.setTestDataId(testDataId);
report.setReportNo(generateReportNo(bodyData.getTenantId()));
JSONObject reportContent = generateReportContent(bodyData);
report.setReportContent(reportContent);
JSONObject analysisResult = analyzeBodyData(bodyData);
report.setAnalysisResult(analysisResult);
String suggestions = generateSuggestions(analysisResult);
report.setSuggestions(suggestions);
return bodyTestReportRepository.save(report);
}
/**
* 查询会员体测历史
*/
public List<BodyComposition> getMemberBodyHistory(Long memberId, LocalDate startDate, LocalDate endDate) {
return bodyCompositionRepository.findByMemberIdAndTestDateBetweenOrderByTestDateDesc(
memberId,
startDate.atStartOfDay(),
endDate.atTime(23, 59, 59)
);
}
/**
* 计算BMI
*/
private BigDecimal calculateBMI(BigDecimal height, BigDecimal weight) {
if (height == null || weight == null || height.compareTo(BigDecimal.ZERO) == 0) {
return null;
}
BigDecimal heightInMeters = height.divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);
return weight.divide(heightInMeters.multiply(heightInMeters), 2, RoundingMode.HALF_UP);
}
/**
* 生成报告内容
*/
private JSONObject generateReportContent(BodyComposition bodyData) {
JSONObject content = new JSONObject();
content.put("height", bodyData.getHeight());
content.put("weight", bodyData.getWeight());
content.put("bodyFatRate", bodyData.getBodyFatRate());
content.put("muscleMass", bodyData.getMuscleMass());
content.put("waterContent", bodyData.getWaterContent());
content.put("boneMass", bodyData.getBoneMass());
content.put("bmi", bodyData.getBmi());
return content;
}
/**
* 分析体测数据
*/
private JSONObject analyzeBodyData(BodyComposition bodyData) {
JSONObject analysis = new JSONObject();
if (bodyData.getBmi() != null) {
String bmiStatus = analyzeBMI(bodyData.getBmi());
analysis.put("bmiStatus", bmiStatus);
}
if (bodyData.getBodyFatRate() != null) {
String bodyFatStatus = analyzeBodyFatRate(bodyData.getBodyFatRate());
analysis.put("bodyFatStatus", bodyFatStatus);
}
return analysis;
}
/**
* 分析BMI
*/
private String analyzeBMI(BigDecimal bmi) {
if (bmi.compareTo(new BigDecimal("18.5")) < 0) {
return "偏瘦";
} else if (bmi.compareTo(new BigDecimal("24")) < 0) {
return "正常";
} else if (bmi.compareTo(new BigDecimal("28")) < 0) {
return "偏胖";
} else {
return "肥胖";
}
}
/**
* 分析体脂率
*/
private String analyzeBodyFatRate(BigDecimal bodyFatRate) {
if (bodyFatRate.compareTo(new BigDecimal("10")) < 0) {
return "偏低";
} else if (bodyFatRate.compareTo(new BigDecimal("20")) < 0) {
return "正常";
} else if (bodyFatRate.compareTo(new BigDecimal("25")) < 0) {
return "偏高";
} else {
return "过高";
}
}
/**
* 生成建议
*/
private String generateSuggestions(JSONObject analysis) {
StringBuilder suggestions = new StringBuilder();
String bmiStatus = analysis.getString("bmiStatus");
if ("偏瘦".equals(bmiStatus)) {
suggestions.append("建议增加营养摄入,适当进行力量训练。");
} else if ("偏胖".equals(bmiStatus) || "肥胖".equals(bmiStatus)) {
suggestions.append("建议控制饮食,增加有氧运动。");
}
String bodyFatStatus = analysis.getString("bodyFatStatus");
if ("偏高".equals(bodyFatStatus) || "过高".equals(bodyFatStatus)) {
suggestions.append("建议进行减脂训练,注意饮食控制。");
}
return suggestions.toString();
}
/**
* 生成报告编号
*/
private String generateReportNo(Long tenantId) {
String timestamp = String.valueOf(System.currentTimeMillis());
return "R" + tenantId + timestamp;
}
}
```
---
## 六、营销分析与预测模块设计 ## 六、营销分析与预测模块设计
### 6.1 模块概述 ### 6.1 模块概述
营销分析与预测模块是付费订阅版的高级功能模块,负责: 营销分析与预测模块是付费订阅版的高级功能模块,负责:
- 营销精算模型预测促销策略 - 基于机器学习算法的营销精算模型预测促销策略
- 多维度自定义促销活动 - 多维度自定义促销活动
- 促销活动效果预测 - 基于深度学习的促销活动效果预测
### 6.2 数据模型设计 ### 6.2 数据模型设计
@@ -955,14 +1369,18 @@ public class MarketingActuarialModelService {
@Autowired @Autowired
private CheckInRecordRepository checkInRecordRepository; private CheckInRecordRepository checkInRecordRepository;
@Autowired
private MachineLearningModelService machineLearningModelService;
/** /**
* 预测促销策略 * 预测促销策略
* 基于机器学习算法进行促销策略预测
*/ */
@Transactional @Transactional
public MarketingPrediction predictPromotionStrategy(PromotionPredictionRequest request) { public MarketingPrediction predictPromotionStrategy(PromotionPredictionRequest request) {
Map<String, Object> historicalData = collectHistoricalData(request.getTenantId(), request.getStoreId()); Map<String, Object> historicalData = collectHistoricalData(request.getTenantId(), request.getStoreId());
Map<String, Object> predictedData = runPredictionModel(historicalData, request.getParameters()); Map<String, Object> predictedData = machineLearningModelService.runPredictionModel(historicalData, request.getParameters());
MarketingPrediction prediction = new MarketingPrediction(); MarketingPrediction prediction = new MarketingPrediction();
prediction.setTenantId(request.getTenantId()); prediction.setTenantId(request.getTenantId());
@@ -1001,31 +1419,6 @@ public class MarketingActuarialModelService {
return historicalData; return historicalData;
} }
/**
* 运行预测模型
*/
private Map<String, Object> runPredictionModel(Map<String, Object> historicalData, Map<String, Object> parameters) {
Map<String, Object> predictedData = new HashMap<>();
Long totalMembers = (Long) historicalData.get("totalMembers");
Long totalBookings = (Long) historicalData.get("totalBookings");
Long totalCheckIns = (Long) historicalData.get("totalCheckIns");
Double discountRate = (Double) parameters.get("discountRate");
Integer durationDays = (Integer) parameters.get("durationDays");
double predictedNewMembers = totalMembers * 0.1 * (1 + discountRate * 2);
double predictedBookings = totalBookings * 1.2 * (1 + discountRate * 1.5);
double predictedCheckIns = totalCheckIns * 1.15 * (1 + discountRate * 1.3);
predictedData.put("predictedNewMembers", Math.round(predictedNewMembers));
predictedData.put("predictedBookings", Math.round(predictedBookings));
predictedData.put("predictedCheckIns", Math.round(predictedCheckIns));
predictedData.put("predictedRevenue", predictedBookings * 100 * (1 - discountRate));
return predictedData;
}
/** /**
* 计算准确率 * 计算准确率
*/ */
@@ -1106,30 +1499,24 @@ public class PromotionActivityService {
### 7.1 缓存设计 ### 7.1 缓存设计
``` ```mermaid
┌─────────────────────────────────────────────────────────────────────────┐ graph TB
│ 缓存策略 │ subgraph LocalCache["本地缓存 (Caffeine)"]
├─────────────────────────────────────────────────────────────────────────┤ LC1["会员信息缓存<br/>TTL: 30分钟"]
│ │ LC2["会员卡缓存<br/>TTL: 30分钟"]
│ ┌─────────────────────────────────────────────────────────────────┐ │ LC3["课程信息缓存<br/>TTL: 1小时"]
│ │ 本地缓存 (Caffeine) │ │ LC4["配置信息缓存<br/>TTL: 1小时"]
│ ├─────────────────────────────────────────────────────────────────┤ │ end
│ │ • 会员信息缓存 (TTL: 30分钟) │ │
│ │ • 会员卡缓存 (TTL: 30分钟) │ │ subgraph RedisCache["分布式缓存 (Redis)"]
│ │ • 课程信息缓存 (TTL: 1小时) │ │ RC1["验证码缓存<br/>TTL: 5分钟"]
│ │ • 配置信息缓存 (TTL: 1小时) │ │ RC2["令牌缓存<br/>TTL: 24小时"]
│ └─────────────────────────────────────────────────────────────────┘ │ RC3["限流计数器<br/>TTL: 1分钟"]
│ │ RC4["预测结果缓存<br/>TTL: 1小时"]
│ ┌─────────────────────────────────────────────────────────────────┐ │ end
│ │ 分布式缓存 (Redis) │ │
│ ├─────────────────────────────────────────────────────────────────┤ │ style LocalCache fill:#e1f5ff
│ │ • 验证码缓存 (TTL: 5分钟) │ │ style RedisCache fill:#fff4e1
│ │ • 令牌缓存 (TTL: 24小时) │ │
│ │ • 限流计数器 (TTL: 1分钟) │ │
│ │ • 预测结果缓存 (TTL: 1小时) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
``` ```
--- ---
@@ -1224,6 +1611,168 @@ Response:
}, },
"accuracy": 0.85 "accuracy": 0.85
} }
```
### 8.3 智能获客工具模块API
#### 8.3.1 创建获客活动
```
POST /api/v1/customer-acquisition/activities
Request:
{
"tenantId": 1,
"name": "节后健身潮获客活动",
"type": 1,
"description": "针对节后健身潮的获客活动",
"startDate": "2026-01-01",
"endDate": "2026-03-31",
"config": {
"rewardAmount": 100,
"maxReferrals": 10
}
}
Response:
{
"code": 200,
"message": "创建成功",
"data": {
"id": 1,
"name": "节后健身潮获客活动",
"type": 1,
"status": 1
}
}
```
#### 8.3.2 生成推荐码
```
POST /api/v1/customer-acquisition/referral-codes
Request:
{
"memberId": 1,
"activityId": 1
}
Response:
{
"code": 200,
"message": "生成成功",
"data": {
"referralCode": "ABC123DEF456"
}
}
```
#### 8.3.3 处理推荐关系
```
POST /api/v1/customer-acquisition/referrals
Request:
{
"memberId": 2,
"referralCode": "ABC123DEF456"
}
Response:
{
"code": 200,
"message": "处理成功",
"data": {
"referrerId": 1,
"refereeId": 2,
"rewardAmount": 100
}
}
```
### 8.4 智能体测数据联动模块API
#### 8.4.1 接收体测数据
```
POST /api/v1/body-composition/data
Request:
{
"tenantId": 1,
"storeId": 1,
"memberId": 1,
"deviceType": "InBody",
"deviceId": "IB001",
"testDate": "2026-03-07T10:00:00",
"height": 175.5,
"weight": 70.2,
"bodyFatRate": 15.5,
"muscleMass": 55.3,
"waterContent": 60.2,
"boneMass": 2.8,
"rawData": {}
}
Response:
{
"code": 200,
"message": "接收成功",
"data": {
"id": 1,
"bmi": 22.8
}
}
```
#### 8.4.2 生成体测报告
```
POST /api/v1/body-composition/reports
Request:
{
"testDataId": 1
}
Response:
{
"code": 200,
"message": "生成成功",
"data": {
"id": 1,
"reportNo": "R11000000000000001",
"reportContent": {},
"analysisResult": {
"bmiStatus": "正常",
"bodyFatStatus": "正常"
},
"suggestions": "建议保持当前运动和饮食习惯。"
}
}
```
#### 8.4.3 查询会员体测历史
```
GET /api/v1/body-composition/history?memberId=1&startDate=2026-01-01&endDate=2026-03-07
Response:
{
"code": 200,
"message": "查询成功",
"data": [
{
"id": 1,
"testDate": "2026-03-07T10:00:00",
"height": 175.5,
"weight": 70.2,
"bodyFatRate": 15.5,
"muscleMass": 55.3,
"bmi": 22.8
}
]
} }
``` ```
@@ -1236,7 +1785,7 @@ Response:
#### 9.1.1 订阅模块测试 #### 9.1.1 订阅模块测试
| 测试用例 | 输入 | 预期输出 | | 测试用例 | 输入 | 预期输出 |
|---------|------|---------| | -------- | -------------------------- | ---------------- |
| 正常订阅 | 租户ID、模块代码、计费周期 | 订阅成功 | | 正常订阅 | 租户ID、模块代码、计费周期 | 订阅成功 |
| 重复订阅 | 已订阅的模块 | 提示该模块已订阅 | | 重复订阅 | 已订阅的模块 | 提示该模块已订阅 |
| 支付失败 | 支付失败 | 提示支付失败 | | 支付失败 | 支付失败 | 提示支付失败 |
@@ -1244,7 +1793,7 @@ Response:
#### 9.1.2 配置查询测试 #### 9.1.2 配置查询测试
| 测试用例 | 输入 | 预期输出 | | 测试用例 | 输入 | 预期输出 |
|---------|------|---------| | ------------ | ------------------------ | ---------------- |
| 查询租户配置 | 租户ID、模块代码 | 返回租户配置 | | 查询租户配置 | 租户ID、模块代码 | 返回租户配置 |
| 查询门店配置 | 租户ID、门店ID、模块代码 | 返回合并后的配置 | | 查询门店配置 | 租户ID、门店ID、模块代码 | 返回合并后的配置 |
| 查询默认配置 | 不存在的配置 | 返回默认配置 | | 查询默认配置 | 不存在的配置 | 返回默认配置 |
@@ -1254,7 +1803,7 @@ Response:
#### 9.2.1 预测促销策略测试 #### 9.2.1 预测促销策略测试
| 测试用例 | 输入 | 预期输出 | | 测试用例 | 输入 | 预期输出 |
|---------|------|---------| | ------------ | ---------------------- | ---------------- |
| 正常预测 | 租户ID、活动类型、参数 | 预测成功 | | 正常预测 | 租户ID、活动类型、参数 | 预测成功 |
| 历史数据不足 | 新租户 | 提示历史数据不足 | | 历史数据不足 | 新租户 | 提示历史数据不足 |
| 参数无效 | 无效的参数 | 提示参数无效 | | 参数无效 | 无效的参数 | 提示参数无效 |
@@ -1265,50 +1814,52 @@ Response:
### 10.1 部署架构 ### 10.1 部署架构
``` ```mermaid
┌─────────────────────────────────────────────────────────────────────────┐ flowchart TB
│ 部署架构 │ LB["负载均衡<br/>(Nginx)"]
├─────────────────────────────────────────────────────────────────────────┤
│ │ subgraph K8S["应用服务器<br/>(Kubernetes)"]
│ ┌─────────────────────────────────────────────────────────────────┐ │ P1["Pod 1"]
│ │ 负载均衡 (Nginx) │ │ P2["Pod 2"]
│ └─────────────────────────────────────────────────────────────────┘ │ P3["Pod 3"]
│ │ │ P4["Pod 4"]
│ ▼ │ P5["Pod 5"]
│ ┌─────────────────────────────────────────────────────────────────┐ │ end
│ │ 应用服务器 (Kubernetes) │ │
│ ├─────────────────────────────────────────────────────────────────┤ │ subgraph PG["数据库<br/>(PostgreSQL)"]
│ │ • Pod 1 • Pod 2 • Pod 3 • Pod 4 • Pod 5 │ │ PG1["主库"]
│ └─────────────────────────────────────────────────────────────────┘ │ PG2["从库"]
│ │ │ PG3["从库"]
│ ▼ │ end
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 数据库 (PostgreSQL) │ │ subgraph Redis["缓存<br/>(Redis)"]
│ ├─────────────────────────────────────────────────────────────────┤ │ R1["主节点"]
│ │ • 主库 • 从库 • 从库 │ │ R2["从节点"]
│ └─────────────────────────────────────────────────────────────────┘ │ R3["从节点"]
│ │ │ end
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │ subgraph ES["搜索引擎<br/>(Elasticsearch)"]
│ │ 缓存 (Redis) │ │ ES1["节点1"]
│ ├─────────────────────────────────────────────────────────────────┤ │ ES2["节点2"]
│ │ • 主节点 • 从节点 • 从节点 │ │ ES3["节点3"]
│ └─────────────────────────────────────────────────────────────────┘ │ end
│ │ │
│ ▼ │ LB --> K8S
│ ┌─────────────────────────────────────────────────────────────────┐ │ K8S --> PG
│ │ 搜索引擎 (Elasticsearch) │ │ PG --> Redis
│ ├─────────────────────────────────────────────────────────────────┤ │ Redis --> ES
│ │ • 节点1 • 节点2 • 节点3 │ │
│ └─────────────────────────────────────────────────────────────────┘ │ style LB fill:#e1f5ff
│ │ style K8S fill:#fff4e1
└─────────────────────────────────────────────────────────────────────────┘ style PG fill:#f0e1ff
style Redis fill:#e1ffe1
style ES fill:#ffe1e1
``` ```
### 10.2 监控指标 ### 10.2 监控指标
| 指标类型 | 指标名称 | 阈值 | | 指标类型 | 指标名称 | 阈值 |
|---------|---------|------| | -------- | ----------- | ------- |
| 系统指标 | CPU使用率 | ≤ 80% | | 系统指标 | CPU使用率 | ≤ 80% |
| 系统指标 | 内存使用率 | ≤ 80% | | 系统指标 | 内存使用率 | ≤ 80% |
| 系统指标 | 磁盘使用率 | ≤ 80% | | 系统指标 | 磁盘使用率 | ≤ 80% |
@@ -1325,7 +1876,7 @@ Response:
### 11.1 术语定义 ### 11.1 术语定义
| 术语 | 定义 | | 术语 | 定义 |
|------|------| | ---------------- | -------------------------------------- |
| 订阅模块 | 按需订阅的增值功能模块 | | 订阅模块 | 按需订阅的增值功能模块 |
| 配置继承 | 门店配置继承租户配置的机制 | | 配置继承 | 门店配置继承租户配置的机制 |
| 私教管理 | 私教课程管理、私教预约、私教签到等功能 | | 私教管理 | 私教课程管理、私教预约、私教签到等功能 |
@@ -1336,9 +1887,8 @@ Response:
### 11.2 参考文档 ### 11.2 参考文档
- 《健身房管理系统付费订阅版产品设计文档》 GYM-PRD-SUBSCRIPTION-001 - 《健身房管理系统付费订阅版产品设计文档》 GYM-PRD-SUBSCRIPTION-001
- 《健身房管理系统付费订阅版业务概要设计文档》 GYM-HLD-SUBSCRIPTION-001 - 《健身房管理系统付费订阅版业务概要设计文档》 GYM-B-HLD-SUBSCRIPTION-001
- 《健身房管理系统详细设计文档》 GYM-LLD-000 - 《健身房管理系统付费订阅版业务详细设计文档》 GYM-B-LLD-SUBSCRIPTION-001
- 《订阅与配置模块详细设计文档》 GYM-LLD-004
- Spring Boot 3 官方文档 - Spring Boot 3 官方文档
- R2DBC 规范文档 - R2DBC 规范文档
- PostgreSQL 官方文档 - PostgreSQL 官方文档
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、提高系统稳定性。