docs: 统一文档日期和状态规范
This commit is contained in:
Vendored
BIN
Binary file not shown.
@@ -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
@@ -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 部署要点
|
||||||
|
|||||||
@@ -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-01,2026-03-08触发沉默期干预
|
||||||
|
- ✅ 场景2:会员最后到店2026-03-01,2026-03-09仍处于沉默期
|
||||||
|
- ✅ 场景3:会员沉默期干预成功,到店后重新计算活跃期
|
||||||
|
- ❌ 场景4:会员最后到店2026-03-01,2026-03-07未触发沉默期干预
|
||||||
|
|
||||||
|
**沉默期干预策略**
|
||||||
|
- 发送个性化关怀短信
|
||||||
|
- 提供专属优惠券
|
||||||
|
- 推荐适合的团课
|
||||||
|
- 教练主动联系
|
||||||
|
- ✅ 场景1:会员沉默7天,发送关怀短信"好久不见,期待您的到来"
|
||||||
|
- ✅ 场景2:会员沉默7天,提供专属优惠券"限时9折优惠"
|
||||||
|
- ✅ 场景3:会员沉默7天,推荐适合的团课"瑜伽课程适合您"
|
||||||
|
- ✅ 场景4:会员沉默7天,教练主动电话联系
|
||||||
|
- ❌ 场景5:会员沉默7天,未采取任何干预措施
|
||||||
|
|
||||||
|
**流失预警规则**
|
||||||
|
- 30天未到店触发流失预警
|
||||||
|
- ✅ 场景1:会员最后到店2026-02-01,2026-03-03触发流失预警
|
||||||
|
- ✅ 场景2:会员最后到店2026-02-01,2026-03-04启动挽回流程
|
||||||
|
- ✅ 场景3:会员挽回成功,到店后重新计算活跃期
|
||||||
|
- ❌ 场景4:会员最后到店2026-02-01,2026-03-02未触发流失预警
|
||||||
|
|
||||||
|
**流失定义规则**
|
||||||
|
- 90天未到店且未消费
|
||||||
|
- ✅ 场景1:会员最后到店2026-01-01,2026-04-01标记为流失状态
|
||||||
|
- ✅ 场景2:会员最后到店2026-01-01,2026-03-31仍处于流失预警期
|
||||||
|
- ✅ 场景3:会员90天内未到店但消费1次,不标记为流失
|
||||||
|
- ❌ 场景4:会员最后到店2026-01-01,2026-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队列:多队列集群模式
|
||||||
|
- Elasticsearch:3节点集群部署
|
||||||
|
|
||||||
|
**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
@@ -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> = {
|
|
||||||
'&': '&',
|
|
||||||
'<': '<',
|
|
||||||
'>': '>',
|
|
||||||
'"': '"',
|
|
||||||
"'": '''
|
|
||||||
}
|
|
||||||
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
@@ -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
@@ -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 模块划分
|
||||||
|
|||||||
@@ -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、提高系统稳定性。
|
|
||||||
Reference in New Issue
Block a user