Files
gym-manage/docs/design/technical/DB-数据库设计.md
T
2026-03-08 21:41:16 +08:00

506 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 健身房管理系统数据库设计文档
> 文档编号:GYM-DB-DESIGN-001
> 版本:v1.0
> 创建日期:2026-03-08
> 最后更新日期:2026-03-08
> 作者:张翔
> 状态:正式发布
## 文档修订历史
| 版本 | 日期 | 作者 | 修订内容 |
| ---- | ---------- | ---- | -------- |
| v1.0 | 2026-03-08 | 张翔 | 创建数据库设计文档 |
## 参考文档
- 《健身房管理系统基础版技术实现详细设计文档》 GYM-T-ILD-BASIC-001
- 《健身房管理系统付费订阅版技术实现详细设计文档》 GYM-T-ILD-SUBSCRIPTION-001
- PostgreSQL 官方文档
- R2DBC 规范文档
---
## 一、数据库架构设计
### 1.1 多租户架构设计
本系统采用**共享数据库、共享 Schema、租户 ID 隔离**的多租户架构:
```
租户层级:
租户 (Tenant) → 门店 (Store) → 业务数据
```
**租户隔离策略**
- 所有业务表包含 `tenant_id` 字段
- 查询时强制添加 `tenant_id` 过滤条件
- 通过数据库视图实现租户级数据隔离
**门店隔离策略**
- 门店级业务表包含 `store_id` 字段
- 支持跨店约课的多门店数据关联
- 通过配置继承实现门店级个性化
### 1.2 分库分表策略
**分库策略**(未来扩展):
- 按租户分库:大型租户(月交易额>100 万)独立数据库
- 按业务分库:交易库、日志库、分析库分离
**分表策略**
- 按时间分表:签到记录、预约记录按月分表
- 按租户分表:大型租户数据独立表空间
### 1.3 数据库选型
**核心数据库**PostgreSQL 15+
- 完全支持 R2DBC 响应式驱动
- JSONB 支持灵活的配置管理
- 全文搜索支持
- ACID 事务保证
**缓存数据库**Redis 7+
- 响应式缓存支持
- 分布式锁
- 过期策略
**搜索引擎**Elasticsearch 8+(可选)
- 全文搜索
- 复杂查询
- 数据分析
---
## 二、核心表结构设计
### 2.1 会员域
#### 2.1.1 会员基础信息表(member
```sql
CREATE TABLE member (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
store_id BIGINT NOT NULL,
phone VARCHAR(11) NOT NULL,
name VARCHAR(50) NOT NULL,
gender SMALLINT NOT NULL,
birthday DATE,
height DECIMAL(5,2),
weight DECIMAL(5,2),
fitness_goal VARCHAR(100),
avatar_url VARCHAR(500),
wechat_openid VARCHAR(100),
status SMALLINT DEFAULT 1,
level SMALLINT DEFAULT 1,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
deleted_at TIMESTAMP DEFAULT NULL,
CONSTRAINT uk_member_phone UNIQUE (tenant_id, phone),
CONSTRAINT fk_member_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id),
CONSTRAINT fk_member_store FOREIGN KEY (store_id) REFERENCES store(id)
);
-- 索引设计
CREATE INDEX idx_member_tenant_store ON member(tenant_id, store_id);
CREATE INDEX idx_member_phone ON member(phone);
CREATE INDEX idx_member_status ON member(status);
CREATE INDEX idx_member_level ON member(level);
```
**字段说明**
- `tenant_id`: 租户 ID,多租户隔离
- `store_id`: 门店 ID,单店运营
- `phone`: 手机号,唯一索引
- `status`: 1-正常,2-沉默,3-流失预警,4-流失
- `level`: 会员等级,1-普通,2-VIP3-SVIP
#### 2.1.2 会员卡表(member_card
```sql
CREATE TABLE member_card (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
member_id BIGINT NOT NULL,
card_type SMALLINT NOT NULL,
card_name VARCHAR(100) NOT NULL,
price DECIMAL(10,2) NOT NULL,
duration_days INT,
duration_times INT,
balance DECIMAL(10,2) DEFAULT 0.00,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
status SMALLINT DEFAULT 1,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
deleted_at TIMESTAMP DEFAULT NULL,
CONSTRAINT fk_card_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id),
CONSTRAINT fk_card_member FOREIGN KEY (member_id) REFERENCES member(id)
);
-- 索引设计
CREATE INDEX idx_card_member ON member_card(member_id);
CREATE INDEX idx_card_status ON member_card(status);
CREATE INDEX idx_card_end_date ON member_card(end_date);
```
**字段说明**
- `card_type`: 1-时长卡,2-次卡,3-储值卡,4-组合卡
- `duration_days`: 时长卡天数
- `duration_times`: 次卡次数
- `balance`: 储值卡余额
- `status`: 1-有效,2-已过期,3-已退款
#### 2.1.3 会员权益表(member_benefit
```sql
CREATE TABLE member_benefit (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
member_id BIGINT NOT NULL,
card_id BIGINT NOT NULL,
benefit_type SMALLINT NOT NULL,
benefit_value DECIMAL(10,2) NOT NULL,
used_value DECIMAL(10,2) DEFAULT 0.00,
remaining_value DECIMAL(10,2) NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
CONSTRAINT fk_benefit_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id),
CONSTRAINT fk_benefit_member FOREIGN KEY (member_id) REFERENCES member(id),
CONSTRAINT fk_benefit_card FOREIGN KEY (card_id) REFERENCES member_card(id)
);
-- 索引设计
CREATE INDEX idx_benefit_member ON member_benefit(member_id);
CREATE INDEX idx_benefit_type ON member_benefit(benefit_type);
```
**字段说明**
- `benefit_type`: 1-时长权益,2-次数权益,3-储值权益,4-等级权益
- `benefit_value`: 权益总值
- `used_value`: 已使用值
- `remaining_value`: 剩余值
#### 2.1.4 会员生命周期表(member_lifecycle
```sql
CREATE TABLE member_lifecycle (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
member_id BIGINT NOT NULL,
stage SMALLINT NOT NULL,
enter_time TIMESTAMP NOT NULL,
exit_time TIMESTAMP,
exit_reason VARCHAR(200),
intervention VARCHAR(500),
intervention_result SMALLINT,
created_at TIMESTAMP DEFAULT NOW(),
CONSTRAINT fk_lifecycle_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id),
CONSTRAINT fk_lifecycle_member FOREIGN KEY (member_id) REFERENCES member(id)
);
-- 索引设计
CREATE INDEX idx_lifecycle_member ON member_lifecycle(member_id);
CREATE INDEX idx_lifecycle_stage ON member_lifecycle(stage);
```
**字段说明**
- `stage`: 1-新会员,2-活跃期,3-沉默期,4-流失预警,5-流失
- `intervention`: 干预措施
- `intervention_result`: 1-成功,2-失败
---
### 2.2 预约域
#### 2.2.1 可预约资源表(booking_resource
```sql
CREATE TABLE booking_resource (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
store_id BIGINT NOT NULL,
resource_type SMALLINT NOT NULL,
resource_name VARCHAR(100) NOT NULL,
description VARCHAR(500),
capacity INT NOT NULL,
duration_minutes INT NOT NULL,
status SMALLINT DEFAULT 1,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
deleted_at TIMESTAMP DEFAULT NULL,
CONSTRAINT fk_resource_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id),
CONSTRAINT fk_resource_store FOREIGN KEY (store_id) REFERENCES store(id)
);
-- 索引设计
CREATE INDEX idx_resource_tenant_store ON booking_resource(tenant_id, store_id);
CREATE INDEX idx_resource_type ON booking_resource(resource_type);
```
**字段说明**
- `resource_type`: 1-团课,2-私教,3-器械,4-场地
- `capacity`: 容量(人数)
- `status`: 1-启用,2-停用
#### 2.2.2 时段表(booking_slot
```sql
CREATE TABLE booking_slot (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
resource_id BIGINT NOT NULL,
coach_id BIGINT,
start_time TIMESTAMP NOT NULL,
end_time TIMESTAMP NOT NULL,
booked_count INT DEFAULT 0,
status SMALLINT DEFAULT 1,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
deleted_at TIMESTAMP DEFAULT NULL,
CONSTRAINT fk_slot_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id),
CONSTRAINT fk_slot_resource FOREIGN KEY (resource_id) REFERENCES booking_resource(id)
);
-- 索引设计
CREATE INDEX idx_slot_resource ON booking_slot(resource_id);
CREATE INDEX idx_slot_start_time ON booking_slot(start_time);
CREATE INDEX idx_slot_status ON booking_slot(status);
```
**字段说明**
- `coach_id`: 教练 ID(私教课必填)
- `booked_count`: 已预约人数
- `status`: 1-可预约,2-已满,3-已取消
#### 2.2.3 预约记录表(booking_record
```sql
CREATE TABLE booking_record (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
member_id BIGINT NOT NULL,
slot_id BIGINT NOT NULL,
booking_time TIMESTAMP NOT NULL,
status SMALLINT DEFAULT 1,
cancel_time TIMESTAMP,
cancel_reason VARCHAR(200),
checkin_time TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
CONSTRAINT fk_booking_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id),
CONSTRAINT fk_booking_member FOREIGN KEY (member_id) REFERENCES member(id),
CONSTRAINT fk_booking_slot FOREIGN KEY (slot_id) REFERENCES booking_slot(id)
);
-- 索引设计
CREATE INDEX idx_booking_member ON booking_record(member_id);
CREATE INDEX idx_booking_slot ON booking_record(slot_id);
CREATE INDEX idx_booking_status ON booking_record(status);
```
**字段说明**
- `booking_time`: 预约时间
- `status`: 1-已预约,2-已签到,3-已取消,4-已爽约
- `checkin_time`: 签到时间
---
### 2.3 订阅域
#### 2.3.1 租户模块配置表(tenant_module_config
```sql
CREATE TABLE tenant_module_config (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
module_code VARCHAR(32) NOT NULL,
enabled BOOLEAN NOT NULL,
config_data JSONB,
version INT DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
created_by BIGINT,
updated_by BIGINT,
deleted_at TIMESTAMP DEFAULT NULL,
CONSTRAINT uk_tenant_module UNIQUE (tenant_id, module_code),
CONSTRAINT fk_tenant_module_config FOREIGN KEY (tenant_id) REFERENCES tenant(id)
);
-- 索引设计
CREATE INDEX idx_tenant_module ON tenant_module_config(tenant_id, module_code);
```
#### 2.3.2 门店模块配置表(store_module_config
```sql
CREATE TABLE store_module_config (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
store_id BIGINT NOT NULL,
module_code VARCHAR(32) NOT NULL,
inherit_mode SMALLINT NOT NULL,
enabled BOOLEAN NOT NULL,
config_data JSONB,
version INT DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
created_by BIGINT,
updated_by BIGINT,
deleted_at TIMESTAMP DEFAULT NULL,
CONSTRAINT uk_store_module UNIQUE (store_id, module_code),
CONSTRAINT fk_store_module_config FOREIGN KEY (store_id) REFERENCES store(id)
);
-- 索引设计
CREATE INDEX idx_store_module ON store_module_config(store_id, module_code);
```
#### 2.3.3 订阅记录表(subscription_record
```sql
CREATE TABLE subscription_record (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
subscription_no VARCHAR(32) NOT NULL,
module_code VARCHAR(32) NOT NULL,
billing_cycle SMALLINT NOT NULL,
amount DECIMAL(10,2) NOT NULL,
discount_amount DECIMAL(10,2) DEFAULT 0.00,
actual_amount DECIMAL(10,2) NOT NULL,
start_date DATE NOT NULL,
end_date DATE 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 uk_subscription_no UNIQUE (subscription_no),
CONSTRAINT fk_subscription_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id)
);
-- 索引设计
CREATE INDEX idx_subscription_tenant ON subscription_record(tenant_id);
CREATE INDEX idx_subscription_status ON subscription_record(status);
```
---
## 三、索引设计优化
### 3.1 核心索引清单
| 表名 | 索引字段 | 索引类型 | 说明 |
|------|---------|---------|------|
| member | (tenant_id, phone) | 唯一索引 | 租户级手机号唯一 |
| member | (tenant_id, store_id) | 复合索引 | 租户 + 门店查询 |
| member | (status) | 单列索引 | 会员状态筛选 |
| member_card | (member_id) | 复合索引 | 会员卡片查询 |
| member_card | (end_date) | 单列索引 | 到期提醒查询 |
| booking_record | (member_id, status) | 复合索引 | 会员预约查询 |
| booking_slot | (resource_id, start_time) | 复合索引 | 资源时段查询 |
### 3.2 索引优化建议
1. **避免过度索引**:单表索引数不超过 5 个
2. **优先复合索引**:将高频查询字段组合
3. **定期分析索引使用**:通过 pg_stat_user_indexes 监控
4. **及时删除无用索引**:减少写入开销
---
## 四、数据迁移策略
### 4.1 版本化管理
使用 **Flyway** 进行数据库版本管理:
```
db/
├── migration/
│ ├── V1__initial_schema.sql
│ ├── V2__add_member_lifecycle.sql
│ ├── V3__add_booking_resource.sql
│ └── ...
```
### 4.2 数据迁移流程
1. **开发环境**:创建迁移脚本 → 测试迁移 → 提交代码
2. **测试环境**:自动执行迁移 → 验证数据 → 回归测试
3. **生产环境**:备份数据 → 执行迁移 → 验证数据 → 监控告警
### 4.3 回滚策略
- 每个迁移脚本配备回滚脚本
- 回滚前必须备份当前数据
- 回滚后验证数据一致性
---
## 五、性能优化
### 5.1 查询优化
1. **避免 N+1 查询**:使用 JOIN 或批量查询
2. **使用覆盖索引**:减少回表查询
3. **分页优化**:使用游标分页替代 OFFSET
### 5.2 连接池配置
```yaml
spring:
r2dbc:
pool:
max-size: 20 # 基础版
max-size: 100 # 付费订阅版
initial-size: 10
max-idle-time: 30m
max-life-time: 60m
```
### 5.3 监控指标
- 连接池使用率
- 慢查询(>1s
- 锁等待时间
- 表空间使用率
---
## 六、安全设计
### 6.1 数据加密
- 手机号:AES-256 加密
- 身份证:AES-256 加密
- 银行卡:AES-256 加密
### 6.2 数据脱敏
- 日志中的手机号:138****1234
- 接口响应中的身份证:110101********1234
### 6.3 审计日志
- 关键操作记录(插入、更新、删除)
- 操作人、操作时间、IP 地址
- 保留 180 天
---
**文档结束**