1275 lines
33 KiB
Markdown
1275 lines
33 KiB
Markdown
# 健身房管理系统技术架构设计文档
|
||
|
||
> ⚠️ **归档说明**
|
||
>
|
||
> **归档日期**: 2026-03-08
|
||
> **归档原因**: 文档架构优化,技术架构内容已整合到 T-ILD 文档体系
|
||
> **替代文档**:
|
||
> - [GYM-T-ILD-BASIC-001](technical/T-ILD-基础版 - 技术实现详细设计.md)
|
||
> - [GYM-T-ILD-SUBSCRIPTION-001](technical/T-ILD-付费订阅版 - 技术实现详细设计.md)
|
||
>
|
||
> 本文档仅供历史参考,请以 T-ILD 文档为准。
|
||
|
||
|
||
> 文档编号: GYM-HLD-TECH-001
|
||
> 版本: v1.0
|
||
> 日期: 2026-03-04
|
||
> 作者: 张翔
|
||
> 状态: 已发布
|
||
|
||
---
|
||
|
||
## 文档修订历史
|
||
|
||
| 版本 | 日期 | 作者 | 修订内容 |
|
||
| ---- | ---------- | ---- | ------------------ |
|
||
| v1.0 | 2026-03-04 | 张翔 | 创建技术架构设计文档 |
|
||
|
||
---
|
||
|
||
## 参考文档
|
||
|
||
- 《健身房管理系统基础版产品设计文档》 GYM-PRD-BASIC-001
|
||
- 《健身房管理系统付费订阅版产品设计文档》 GYM-PRD-SUBSCRIPTION-001
|
||
- 《健身房管理系统基础版业务概要设计文档》 GYM-B-HLD-BASIC-001
|
||
- 《健身房管理系统付费订阅版业务概要设计文档》 GYM-B-HLD-SUBSCRIPTION-001
|
||
- Spring Boot 3 官方文档
|
||
- Spring WebFlux 官方文档
|
||
- R2DBC 规范文档
|
||
- PostgreSQL 官方文档
|
||
|
||
---
|
||
|
||
## 一、架构决策
|
||
|
||
### 1.1 架构选型
|
||
|
||
经过深入评估,本系统采用以下架构决策:
|
||
|
||
| 决策项 | 选择方案 | 理由 |
|
||
|-------|---------|------|
|
||
| **应用架构** | 单体应用 | 适合当前规模(基础版100并发用户,付费订阅版500并发用户),开发效率高,部署简单,成本低 |
|
||
| **编程模型** | 响应式编程(WebFlux + R2DBC) | 高并发能力(10x 提升),低延迟(50% 降低),资源利用率高(75% 降低) |
|
||
| **部署方式** | Docker Compose | 一键部署,环境一致性好,回滚快速 |
|
||
| **数据库** | PostgreSQL | 金融级数据库,支持 ACID 事务,JSONB 支持灵活配置 |
|
||
| **缓存** | Redis | 高性能缓存,支持分布式锁 |
|
||
| **消息队列** | RabbitMQ | 成熟稳定,支持延迟消息 |
|
||
| **搜索引擎** | Elasticsearch | 全文搜索,适合复杂查询 |
|
||
| **监控** | Prometheus + Grafana | 完善的监控体系,可视化好 |
|
||
|
||
### 1.2 技术栈
|
||
|
||
#### 核心技术栈
|
||
|
||
| 技术组件 | 版本 | 用途 |
|
||
|---------|------|------|
|
||
| **Spring Boot** | 3.2.x | 应用框架 |
|
||
| **Spring WebFlux** | 3.2.x | 响应式 Web 框架 |
|
||
| **Spring Data R2DBC** | 3.2.x | 响应式数据访问 |
|
||
| **PostgreSQL R2DBC** | 1.0.0.RELEASE | PostgreSQL 响应式驱动 |
|
||
| **Spring Security** | 6.2.x | 安全框架 |
|
||
| **Redis Reactive** | 3.2.x | 响应式缓存 |
|
||
| **RabbitMQ** | 3.12.x | 消息队列 |
|
||
| **Elasticsearch** | 8.11.x | 搜索引擎 |
|
||
| **Prometheus** | Latest | 监控指标采集 |
|
||
| **Grafana** | Latest | 监控可视化 |
|
||
| **Docker** | 24.x | 容器化部署 |
|
||
| **Docker Compose** | 2.20.x | 容器编排 |
|
||
|
||
#### 开发工具
|
||
|
||
| 工具 | 版本 | 用途 |
|
||
|------|------|------|
|
||
| **JDK** | 17+ | 运行环境 |
|
||
| **Maven** | 3.9.x | 项目构建 |
|
||
| **Lombok** | 1.18.x | 代码简化 |
|
||
| **MapStruct** | 1.5.x | 对象映射 |
|
||
| **Micrometer** | 1.12.x | 监控指标 |
|
||
| **SpringDoc OpenAPI** | 2.3.x | API 文档 |
|
||
|
||
---
|
||
|
||
## 二、系统架构设计
|
||
|
||
### 2.1 总体架构
|
||
|
||
采用分层架构 + 模块化设计的单体应用:
|
||
|
||
```mermaid
|
||
flowchart TB
|
||
subgraph 单体应用总体架构
|
||
A[客户端层<br/>• 会员小程序 uniapp+Vue3<br/>• 教练端App uniapp+Vue3<br/>• 管理后台PC Vue3+Vite<br/>• 硬件设备 人脸/NFC]
|
||
B[Nginx 反向代理<br/>• 负载均衡<br/>• SSL 终止<br/>• 静态资源<br/>• 限流]
|
||
C[Presentation Layer WebFlux<br/>• Controller<br/>• Router<br/>• Filter<br/>• Validator]
|
||
D[Application Layer 业务编排<br/>• Service<br/>• Facade<br/>• Orchestrator<br/>• 事务管理]
|
||
E[Domain Layer 领域模型<br/>• Entity<br/>• Value Object<br/>• Domain Service<br/>• Repository]
|
||
F[Infrastructure Layer 基础设施<br/>• Repository R2DBC<br/>• Cache Redis<br/>• Message RabbitMQ<br/>• Search Elasticsearch<br/>• File OSS<br/>• Distributed Lock]
|
||
G[外部服务层<br/>• PostgreSQL<br/>• Redis<br/>• RabbitMQ<br/>• Elasticsearch<br/>• 微信开放平台<br/>• 短信服务<br/>• 支付服务<br/>• OSS存储]
|
||
H[监控与运维层<br/>• Prometheus<br/>• Grafana<br/>• 日志收集<br/>• 告警]
|
||
A --> B
|
||
B --> C
|
||
C --> D
|
||
D --> E
|
||
E --> F
|
||
F --> G
|
||
G --> H
|
||
end
|
||
```
|
||
|
||
### 2.2 分层架构详解
|
||
|
||
#### 2.2.1 Presentation Layer(表现层)
|
||
|
||
**职责**:
|
||
- 接收 HTTP 请求
|
||
- 参数验证
|
||
- 路由转发
|
||
- 响应封装
|
||
- 异常处理
|
||
|
||
**技术实现**:
|
||
- Spring WebFlux Router
|
||
- Spring Validation
|
||
- Spring Security Reactive
|
||
- Global Exception Handler
|
||
|
||
#### 2.2.2 Application Layer(应用层)
|
||
|
||
**职责**:
|
||
- 业务逻辑编排
|
||
- 事务管理
|
||
- 跨模块协调
|
||
- 权限校验
|
||
|
||
**技术实现**:
|
||
- Service 类
|
||
- @Transactional 注解
|
||
- 分布式锁
|
||
- Saga 模式(跨服务事务)
|
||
|
||
#### 2.2.3 Domain Layer(领域层)
|
||
|
||
**职责**:
|
||
- 领域模型定义
|
||
- 业务规则封装
|
||
- 领域服务
|
||
- 仓储接口定义
|
||
|
||
**技术实现**:
|
||
- Entity 类
|
||
- Value Object 类
|
||
- Domain Service 类
|
||
- Repository 接口
|
||
|
||
#### 2.2.4 Infrastructure Layer(基础设施层)
|
||
|
||
**职责**:
|
||
- 数据访问实现
|
||
- 缓存管理
|
||
- 消息队列
|
||
- 文件存储
|
||
- 外部服务调用
|
||
|
||
**技术实现**:
|
||
- R2DBC Repository
|
||
- Redis Reactive
|
||
- RabbitMQ Reactive
|
||
- Elasticsearch Reactive
|
||
- OSS SDK
|
||
|
||
### 2.3 模块化设计
|
||
|
||
单体应用内部采用模块化设计,为未来拆分微服务做准备:
|
||
|
||
```
|
||
gym-manage/
|
||
├── gym-manage-api/ # API 层
|
||
│ ├── controller/
|
||
│ │ ├── member/ # 会员模块 API
|
||
│ │ ├── booking/ # 预约模块 API
|
||
│ │ ├── checkin/ # 签到模块 API
|
||
│ │ ├── benefit/ # 权益模块 API
|
||
│ │ ├── subscription/ # 订阅模块 API
|
||
│ │ ├── marketing/ # 营销模块 API
|
||
│ │ └── analytics/ # 数据分析模块 API
|
||
│ ├── dto/
|
||
│ │ ├── request/ # 请求 DTO
|
||
│ │ └── response/ # 响应 DTO
|
||
│ └── config/
|
||
│ ├── WebFluxConfig.java
|
||
│ ├── SecurityConfig.java
|
||
│ └── R2dbcConfig.java
|
||
│
|
||
├── gym-manage-application/ # 应用层
|
||
│ ├── service/
|
||
│ │ ├── member/
|
||
│ │ ├── booking/
|
||
│ │ ├── checkin/
|
||
│ │ ├── benefit/
|
||
│ │ ├── subscription/
|
||
│ │ ├── marketing/
|
||
│ │ └── analytics/
|
||
│ ├── facade/
|
||
│ └── orchestrator/
|
||
│
|
||
├── gym-manage-domain/ # 领域层
|
||
│ ├── entity/
|
||
│ │ ├── Member.java
|
||
│ │ ├── BookingRecord.java
|
||
│ │ ├── CheckinRecord.java
|
||
│ │ ├── MemberBenefit.java
|
||
│ │ ├── SubscriptionRecord.java
|
||
│ │ └── ...
|
||
│ ├── valueobject/
|
||
│ ├── repository/
|
||
│ │ ├── MemberRepository.java
|
||
│ │ ├── BookingRecordRepository.java
|
||
│ │ └── ...
|
||
│ └── service/
|
||
│ └── DomainService.java
|
||
│
|
||
├── gym-manage-infrastructure/ # 基础设施层
|
||
│ ├── repository/
|
||
│ │ └── impl/
|
||
│ │ ├── MemberRepositoryImpl.java
|
||
│ │ ├── BookingRecordRepositoryImpl.java
|
||
│ │ └── ...
|
||
│ ├── cache/
|
||
│ │ └── RedisCacheService.java
|
||
│ ├── message/
|
||
│ │ └── RabbitMQService.java
|
||
│ ├── search/
|
||
│ │ └── ElasticsearchService.java
|
||
│ ├── lock/
|
||
│ │ └── DistributedLockService.java
|
||
│ └── config/
|
||
│ ├── R2dbcConfiguration.java
|
||
│ ├── RedisConfiguration.java
|
||
│ ├── RabbitMQConfiguration.java
|
||
│ └── ElasticsearchConfiguration.java
|
||
│
|
||
└── gym-manage-main/ # 主启动类
|
||
├── GymManageApplication.java
|
||
└── resources/
|
||
├── application.yml
|
||
├── application-dev.yml
|
||
└── application-prod.yml
|
||
```
|
||
|
||
---
|
||
|
||
## 三、响应式编程架构
|
||
|
||
### 3.1 响应式编程模型
|
||
|
||
本系统采用 Project Reactor 作为响应式编程库:
|
||
|
||
| 组件 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| **Mono** | 0-1 个元素 | 表示异步计算结果,返回单个对象或空 |
|
||
| **Flux** | 0-N 个元素 | 表示异步数据流,返回多个对象 |
|
||
| **Scheduler** | 线程调度器 | 控制异步操作的执行线程 |
|
||
|
||
### 3.2 响应式编程规范
|
||
|
||
#### 3.2.1 基本原则
|
||
|
||
1. **永不阻塞**
|
||
- 禁止在响应式流中使用 `block()`、`blockFirst()`、`blockLast()`
|
||
- 所有 I/O 操作必须使用非阻塞方式
|
||
|
||
2. **链式调用**
|
||
- 使用 `flatMap`、`map`、`filter` 等操作符链式调用
|
||
- 避免嵌套的 `subscribe`
|
||
|
||
3. **错误处理**
|
||
- 使用 `onErrorResume`、`onErrorReturn` 处理错误
|
||
- 避免使用 `try-catch` 捕获响应式异常
|
||
|
||
4. **背压处理**
|
||
- 使用 `onBackpressureBuffer`、`onBackpressureDrop` 处理背压
|
||
- 避免内存溢出
|
||
|
||
#### 3.2.2 代码示例
|
||
|
||
**✅ 正确示例**:
|
||
|
||
```java
|
||
public Mono<Member> getMember(Long id) {
|
||
return memberRepository.findById(id)
|
||
.switchIfEmpty(Mono.error(new BusinessException("会员不存在")))
|
||
.flatMap(member -> loadMemberCards(member.getId()))
|
||
.flatMap(member -> loadMemberBenefits(member.getId()))
|
||
.doOnSuccess(member -> log.info("查询会员成功: memberId={}", member.getId()))
|
||
.doOnError(e -> log.error("查询会员失败: memberId={}", id, e));
|
||
}
|
||
```
|
||
|
||
**❌ 错误示例**:
|
||
|
||
```java
|
||
public Member getMember(Long id) {
|
||
// 错误:使用 block() 阻塞
|
||
return memberRepository.findById(id).block();
|
||
}
|
||
|
||
public Mono<Member> getMember(Long id) {
|
||
return memberRepository.findById(id)
|
||
.flatMap(member -> {
|
||
// 错误:在 flatMap 中使用 block()
|
||
List<MemberCard> cards = memberCardRepository.findByMemberId(member.getId()).collectList().block();
|
||
return Mono.just(member);
|
||
});
|
||
}
|
||
```
|
||
|
||
### 3.3 响应式事务管理
|
||
|
||
#### 3.3.1 本地事务
|
||
|
||
使用 `@Transactional` 注解管理本地事务:
|
||
|
||
```java
|
||
@Service
|
||
public class BookingService {
|
||
|
||
@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()));
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 3.3.2 分布式锁
|
||
|
||
使用 Redis 实现分布式锁:
|
||
|
||
```java
|
||
@Component
|
||
public class RedisDistributedLock {
|
||
|
||
private final ReactiveRedisTemplate<String, String> redisTemplate;
|
||
private static final String LOCK_PREFIX = "lock:";
|
||
private static final long DEFAULT_EXPIRE_TIME = 30;
|
||
|
||
public Mono<Boolean> tryLock(String key, long expireTime) {
|
||
String lockKey = LOCK_PREFIX + key;
|
||
String lockValue = UUID.randomUUID().toString();
|
||
|
||
return redisTemplate.opsForValue()
|
||
.setIfAbsent(lockKey, lockValue, Duration.ofSeconds(expireTime))
|
||
.flatMap(locked -> {
|
||
if (Boolean.TRUE.equals(locked)) {
|
||
log.info("获取锁成功: key={}", lockKey);
|
||
return Mono.just(true);
|
||
} else {
|
||
log.warn("获取锁失败: key={}", lockKey);
|
||
return Mono.just(false);
|
||
}
|
||
});
|
||
}
|
||
|
||
public Mono<Void> unlock(String key) {
|
||
String lockKey = LOCK_PREFIX + key;
|
||
return redisTemplate.delete(lockKey)
|
||
.doOnSuccess(deleted -> {
|
||
if (Boolean.TRUE.equals(deleted)) {
|
||
log.info("释放锁成功: key={}", lockKey);
|
||
}
|
||
})
|
||
.then();
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 3.3.3 Saga 模式(跨模块事务)
|
||
|
||
对于跨模块的事务,使用 Saga 模式:
|
||
|
||
```java
|
||
@Service
|
||
public class BookingSaga {
|
||
|
||
public Mono<BookingRecord> execute(BookingRequest request) {
|
||
return bookSlot(request)
|
||
.flatMap(booking -> sendNotification(booking))
|
||
.flatMap(booking -> updateStatistics(booking))
|
||
.onErrorResume(e -> compensate(request, e));
|
||
}
|
||
|
||
private Mono<BookingRecord> bookSlot(BookingRequest request) {
|
||
// 预约逻辑
|
||
}
|
||
|
||
private Mono<BookingRecord> sendNotification(BookingRecord booking) {
|
||
// 发送通知
|
||
}
|
||
|
||
private Mono<BookingRecord> updateStatistics(BookingRecord booking) {
|
||
// 更新统计
|
||
}
|
||
|
||
private Mono<BookingRecord> compensate(BookingRequest request, Throwable e) {
|
||
// 补偿逻辑
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 四、部署架构
|
||
|
||
### 4.1 Docker Compose 部署
|
||
|
||
#### 4.1.1 完整的 docker-compose.yml
|
||
|
||
```yaml
|
||
version: '3.8'
|
||
|
||
services:
|
||
# PostgreSQL 数据库
|
||
postgres:
|
||
image: postgres:16-alpine
|
||
container_name: gym-postgres
|
||
environment:
|
||
POSTGRES_DB: gym_manage
|
||
POSTGRES_USER: ${DB_USERNAME:-postgres}
|
||
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
|
||
TZ: Asia/Shanghai
|
||
ports:
|
||
- "5432:5432"
|
||
volumes:
|
||
- postgres_data:/var/lib/postgresql/data
|
||
- ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql
|
||
healthcheck:
|
||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||
interval: 10s
|
||
timeout: 5s
|
||
retries: 5
|
||
networks:
|
||
- gym-network
|
||
restart: unless-stopped
|
||
|
||
# Redis 缓存
|
||
redis:
|
||
image: redis:7-alpine
|
||
container_name: gym-redis
|
||
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-redis123}
|
||
ports:
|
||
- "6379:6379"
|
||
volumes:
|
||
- redis_data:/data
|
||
healthcheck:
|
||
test: ["CMD", "redis-cli", "ping"]
|
||
interval: 10s
|
||
timeout: 5s
|
||
retries: 5
|
||
networks:
|
||
- gym-network
|
||
restart: unless-stopped
|
||
|
||
# RabbitMQ 消息队列
|
||
rabbitmq:
|
||
image: rabbitmq:3.12-management-alpine
|
||
container_name: gym-rabbitmq
|
||
environment:
|
||
RABBITMQ_DEFAULT_USER: ${MQ_USERNAME:-admin}
|
||
RABBITMQ_DEFAULT_PASS: ${MQ_PASSWORD:-admin123}
|
||
TZ: Asia/Shanghai
|
||
ports:
|
||
- "5672:5672"
|
||
- "15672:15672"
|
||
volumes:
|
||
- rabbitmq_data:/var/lib/rabbitmq
|
||
healthcheck:
|
||
test: ["CMD", "rabbitmq-diagnostics", "ping"]
|
||
interval: 30s
|
||
timeout: 10s
|
||
retries: 5
|
||
networks:
|
||
- gym-network
|
||
restart: unless-stopped
|
||
|
||
# Elasticsearch 搜索引擎
|
||
elasticsearch:
|
||
image: elasticsearch:8.11.0
|
||
container_name: gym-elasticsearch
|
||
environment:
|
||
discovery.type: single-node
|
||
ES_JAVA_OPTS: -Xms512m -Xmx512m
|
||
xpack.security.enabled: "false"
|
||
TZ: Asia/Shanghai
|
||
ports:
|
||
- "9200:9200"
|
||
- "9300:9300"
|
||
volumes:
|
||
- elasticsearch_data:/usr/share/elasticsearch/data
|
||
healthcheck:
|
||
test: ["CMD-SHELL", "curl -f http://localhost:9200/_cluster/health || exit 1"]
|
||
interval: 30s
|
||
timeout: 10s
|
||
retries: 5
|
||
networks:
|
||
- gym-network
|
||
restart: unless-stopped
|
||
|
||
# Kibana 可视化
|
||
kibana:
|
||
image: kibana:8.11.0
|
||
container_name: gym-kibana
|
||
environment:
|
||
ELASTICSEARCH_HOSTS: http://elasticsearch:9200
|
||
TZ: Asia/Shanghai
|
||
ports:
|
||
- "5601:5601"
|
||
depends_on:
|
||
- elasticsearch
|
||
networks:
|
||
- gym-network
|
||
restart: unless-stopped
|
||
|
||
# Prometheus 监控
|
||
prometheus:
|
||
image: prom/prometheus:latest
|
||
container_name: gym-prometheus
|
||
command:
|
||
- '--config.file=/etc/prometheus/prometheus.yml'
|
||
- '--storage.tsdb.path=/prometheus'
|
||
- '--web.console.libraries=/usr/share/prometheus/console_libraries'
|
||
- '--web.console.templates=/usr/share/prometheus/consoles'
|
||
ports:
|
||
- "9090:9090"
|
||
volumes:
|
||
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
|
||
- prometheus_data:/prometheus
|
||
networks:
|
||
- gym-network
|
||
restart: unless-stopped
|
||
|
||
# Grafana 可视化
|
||
grafana:
|
||
image: grafana/grafana:latest
|
||
container_name: gym-grafana
|
||
environment:
|
||
GF_SECURITY_ADMIN_USER: ${GRAFANA_USER:-admin}
|
||
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD:-admin123}
|
||
TZ: Asia/Shanghai
|
||
ports:
|
||
- "3000:3000"
|
||
volumes:
|
||
- grafana_data:/var/lib/grafana
|
||
- ./monitoring/grafana/dashboards:/etc/grafana/provisioning/dashboards
|
||
- ./monitoring/grafana/datasources:/etc/grafana/provisioning/datasources
|
||
depends_on:
|
||
- prometheus
|
||
networks:
|
||
- gym-network
|
||
restart: unless-stopped
|
||
|
||
# 健身房管理系统应用
|
||
gym-manage:
|
||
build:
|
||
context: .
|
||
dockerfile: Dockerfile
|
||
container_name: gym-manage-app
|
||
environment:
|
||
SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE:-prod}
|
||
DB_HOST: postgres
|
||
DB_PORT: 5432
|
||
DB_NAME: gym_manage
|
||
DB_USERNAME: ${DB_USERNAME:-postgres}
|
||
DB_PASSWORD: ${DB_PASSWORD:-postgres}
|
||
REDIS_HOST: redis
|
||
REDIS_PORT: 6379
|
||
REDIS_PASSWORD: ${REDIS_PASSWORD:-redis123}
|
||
RABBITMQ_HOST: rabbitmq
|
||
RABBITMQ_PORT: 5672
|
||
RABBITMQ_USERNAME: ${MQ_USERNAME:-admin}
|
||
RABBITMQ_PASSWORD: ${MQ_PASSWORD:-admin123}
|
||
ELASTICSEARCH_HOST: elasticsearch
|
||
ELASTICSEARCH_PORT: 9200
|
||
TZ: Asia/Shanghai
|
||
JAVA_OPTS: -Xms512m -Xmx1024m -XX:+UseG1GC
|
||
ports:
|
||
- "8080:8080"
|
||
depends_on:
|
||
postgres:
|
||
condition: service_healthy
|
||
redis:
|
||
condition: service_healthy
|
||
rabbitmq:
|
||
condition: service_healthy
|
||
elasticsearch:
|
||
condition: service_healthy
|
||
volumes:
|
||
- ./logs:/app/logs
|
||
healthcheck:
|
||
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
|
||
interval: 30s
|
||
timeout: 10s
|
||
retries: 3
|
||
networks:
|
||
- gym-network
|
||
restart: unless-stopped
|
||
|
||
# Nginx 反向代理
|
||
nginx:
|
||
image: nginx:alpine
|
||
container_name: gym-nginx
|
||
ports:
|
||
- "80:80"
|
||
- "443:443"
|
||
volumes:
|
||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
|
||
- ./nginx/ssl:/etc/nginx/ssl
|
||
- ./logs/nginx:/var/log/nginx
|
||
depends_on:
|
||
- gym-manage
|
||
networks:
|
||
- gym-network
|
||
restart: unless-stopped
|
||
|
||
volumes:
|
||
postgres_data:
|
||
redis_data:
|
||
rabbitmq_data:
|
||
elasticsearch_data:
|
||
prometheus_data:
|
||
grafana_data:
|
||
|
||
networks:
|
||
gym-network:
|
||
driver: bridge
|
||
```
|
||
|
||
#### 4.1.2 Dockerfile
|
||
|
||
```dockerfile
|
||
# 多阶段构建
|
||
FROM maven:3.9-eclipse-temurin-17 AS builder
|
||
|
||
WORKDIR /app
|
||
|
||
# 复制 pom.xml 并下载依赖(利用 Docker 缓存)
|
||
COPY pom.xml .
|
||
RUN mvn dependency:go-offline -B
|
||
|
||
# 复制源代码
|
||
COPY src ./src
|
||
|
||
# 打包
|
||
RUN mvn clean package -DskipTests -B
|
||
|
||
# 运行阶段
|
||
FROM eclipse-temurin:17-jre-alpine
|
||
|
||
WORKDIR /app
|
||
|
||
# 安装必要的工具
|
||
RUN apk add --no-cache curl
|
||
|
||
# 复制打包好的 jar 文件
|
||
COPY --from=builder /app/target/gym-manage-*.jar app.jar
|
||
|
||
# 创建日志目录
|
||
RUN mkdir -p /app/logs
|
||
|
||
# 暴露端口
|
||
EXPOSE 8080
|
||
|
||
# 健康检查
|
||
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
|
||
CMD curl -f http://localhost:8080/actuator/health || exit 1
|
||
|
||
# 启动应用
|
||
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
|
||
```
|
||
|
||
#### 4.1.3 部署流程
|
||
|
||
```bash
|
||
# 1. 克隆代码
|
||
git clone <repository>
|
||
cd gym-manage
|
||
|
||
# 2. 配置环境变量
|
||
cp .env.example .env
|
||
# 编辑 .env 文件,配置数据库密码等
|
||
|
||
# 3. 构建并启动
|
||
docker-compose up -d
|
||
|
||
# 4. 查看日志
|
||
docker-compose logs -f gym-manage
|
||
|
||
# 5. 健康检查
|
||
curl http://localhost:8080/actuator/health
|
||
```
|
||
|
||
### 4.2 配置管理
|
||
|
||
#### 4.2.1 application-prod.yml
|
||
|
||
```yaml
|
||
spring:
|
||
r2dbc:
|
||
url: r2dbc:postgresql://${DB_HOST:postgres}:${DB_PORT:5432}/${DB_NAME:gym_manage}
|
||
username: ${DB_USERNAME:postgres}
|
||
password: ${DB_PASSWORD:postgres}
|
||
pool:
|
||
initial-size: 5
|
||
max-size: 20
|
||
max-idle-time: 30m
|
||
max-life-time: 1h
|
||
acquire-timeout: 5s
|
||
|
||
data:
|
||
redis:
|
||
host: ${REDIS_HOST:redis}
|
||
port: ${REDIS_PORT:6379}
|
||
password: ${REDIS_PASSWORD:}
|
||
lettuce:
|
||
pool:
|
||
max-active: 20
|
||
max-idle: 10
|
||
min-idle: 5
|
||
|
||
rabbitmq:
|
||
host: ${RABBITMQ_HOST:rabbitmq}
|
||
port: ${RABBITMQ_PORT:5672}
|
||
username: ${RABBITMQ_USERNAME:guest}
|
||
password: ${RABBITMQ_PASSWORD:guest}
|
||
|
||
elasticsearch:
|
||
uris: http://${ELASTICSEARCH_HOST:elasticsearch}:${ELASTICSEARCH_PORT:9200}
|
||
|
||
webflux:
|
||
base-path: /api/v1
|
||
|
||
codec:
|
||
max-in-memory-size: 10MB
|
||
|
||
server:
|
||
port: 8080
|
||
netty:
|
||
connection-timeout: 5s
|
||
|
||
management:
|
||
endpoints:
|
||
web:
|
||
exposure:
|
||
include: health,metrics,prometheus,httptrace
|
||
metrics:
|
||
export:
|
||
prometheus:
|
||
enabled: true
|
||
tags:
|
||
application: gym-manage
|
||
environment: ${SPRING_PROFILES_ACTIVE:prod}
|
||
|
||
logging:
|
||
level:
|
||
root: INFO
|
||
com.gym.manage: DEBUG
|
||
org.springframework.r2dbc: DEBUG
|
||
reactor.netty: INFO
|
||
file:
|
||
name: /app/logs/gym-manage.log
|
||
pattern:
|
||
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
|
||
file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
|
||
```
|
||
|
||
---
|
||
|
||
## 五、监控与运维
|
||
|
||
### 5.1 监控体系
|
||
|
||
#### 5.1.1 监控指标
|
||
|
||
| 指标类型 | 具体指标 | 说明 |
|
||
|---------|---------|------|
|
||
| **应用指标** | QPS、响应时间、错误率 | 应用性能监控 |
|
||
| **JVM 指标** | 堆内存、GC 次数、线程数 | JVM 健康度 |
|
||
| **数据库指标** | 连接数、查询时间、慢查询 | 数据库性能 |
|
||
| **缓存指标** | 命中率、内存使用、连接数 | 缓存效率 |
|
||
| **消息队列指标** | 队列长度、消费速率、积压量 | 消息队列健康度 |
|
||
| **系统指标** | CPU、内存、磁盘、网络 | 系统资源使用 |
|
||
|
||
#### 5.1.2 Prometheus 配置
|
||
|
||
```yaml
|
||
# monitoring/prometheus.yml
|
||
global:
|
||
scrape_interval: 15s
|
||
evaluation_interval: 15s
|
||
|
||
scrape_configs:
|
||
- job_name: 'gym-manage'
|
||
metrics_path: '/actuator/prometheus'
|
||
static_configs:
|
||
- targets: ['gym-manage:8080']
|
||
labels:
|
||
application: 'gym-manage'
|
||
environment: 'prod'
|
||
|
||
- job_name: 'postgres'
|
||
static_configs:
|
||
- targets: ['postgres:5432']
|
||
|
||
- job_name: 'redis'
|
||
static_configs:
|
||
- targets: ['redis:6379']
|
||
|
||
- job_name: 'rabbitmq'
|
||
static_configs:
|
||
- targets: ['rabbitmq:15672']
|
||
```
|
||
|
||
#### 5.1.3 Grafana Dashboard
|
||
|
||
```json
|
||
{
|
||
"dashboard": {
|
||
"title": "健身房管理系统监控",
|
||
"panels": [
|
||
{
|
||
"title": "QPS",
|
||
"targets": [
|
||
{
|
||
"expr": "rate(http_server_requests_seconds_count[1m])"
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"title": "响应时间 (P99)",
|
||
"targets": [
|
||
{
|
||
"expr": "histogram_quantile(0.99, rate(http_server_requests_seconds_bucket[1m]))"
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"title": "错误率",
|
||
"targets": [
|
||
{
|
||
"expr": "rate(http_server_requests_seconds_count{status=~\"5..\"}[1m]) / rate(http_server_requests_seconds_count[1m])"
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"title": "JVM 堆内存",
|
||
"targets": [
|
||
{
|
||
"expr": "jvm_memory_used_bytes{area=\"heap\"}"
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"title": "GC 次数",
|
||
"targets": [
|
||
{
|
||
"expr": "rate(jvm_gc_pause_seconds_count[1m])"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
### 5.2 告警规则
|
||
|
||
#### 5.2.1 告警配置
|
||
|
||
```yaml
|
||
# monitoring/alerts.yml
|
||
groups:
|
||
- name: gym-manage-alerts
|
||
rules:
|
||
- alert: HighErrorRate
|
||
expr: rate(http_server_requests_seconds_count{status=~\"5..\"}[5m]) / rate(http_server_requests_seconds_count[5m]) > 0.05
|
||
for: 5m
|
||
labels:
|
||
severity: critical
|
||
annotations:
|
||
summary: "错误率过高"
|
||
description: "错误率超过 5%,当前值为 {{ $value }}"
|
||
|
||
- alert: HighResponseTime
|
||
expr: histogram_quantile(0.99, rate(http_server_requests_seconds_bucket[5m])) > 1
|
||
for: 5m
|
||
labels:
|
||
severity: warning
|
||
annotations:
|
||
summary: "响应时间过长"
|
||
description: "P99 响应时间超过 1s,当前值为 {{ $value }}s"
|
||
|
||
- alert: HighMemoryUsage
|
||
expr: jvm_memory_used_bytes{area=\"heap\"} / jvm_memory_max_bytes{area=\"heap\"} > 0.8
|
||
for: 5m
|
||
labels:
|
||
severity: warning
|
||
annotations:
|
||
summary: "堆内存使用率过高"
|
||
description: "堆内存使用率超过 80%,当前值为 {{ $value }}"
|
||
|
||
- alert: DatabaseConnectionPoolExhausted
|
||
expr: hikaricp_connections_active / hikaricp_connections_max > 0.9
|
||
for: 5m
|
||
labels:
|
||
severity: critical
|
||
annotations:
|
||
summary: "数据库连接池耗尽"
|
||
description: "数据库连接池使用率超过 90%,当前值为 {{ $value }}"
|
||
```
|
||
|
||
### 5.3 日志管理
|
||
|
||
#### 5.3.1 日志配置
|
||
|
||
```yaml
|
||
logging:
|
||
level:
|
||
root: INFO
|
||
com.gym.manage: DEBUG
|
||
org.springframework.r2dbc: DEBUG
|
||
reactor.netty: INFO
|
||
file:
|
||
name: /app/logs/gym-manage.log
|
||
max-size: 100MB
|
||
max-history: 30
|
||
pattern:
|
||
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
|
||
file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
|
||
```
|
||
|
||
#### 5.3.2 结构化日志
|
||
|
||
```java
|
||
@Slf4j
|
||
public class MemberService {
|
||
|
||
public Mono<Member> getMember(Long id) {
|
||
return memberRepository.findById(id)
|
||
.doOnSubscribe(s -> log.info("开始查询会员: memberId={}", id))
|
||
.doOnNext(m -> log.info("查询到会员: memberId={}, name={}", m.getId(), m.getName()))
|
||
.doOnError(e -> log.error("查询会员失败: memberId={}, error={}", id, e.getMessage()))
|
||
.doOnTerminate(() -> log.info("查询会员完成: memberId={}", id));
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 六、性能优化
|
||
|
||
### 6.1 数据库优化
|
||
|
||
#### 6.1.1 索引优化
|
||
|
||
```sql
|
||
-- 会员表索引
|
||
CREATE INDEX idx_member_tenant ON member(tenant_id) WHERE deleted_at IS NULL;
|
||
CREATE INDEX idx_member_store ON member(store_id) WHERE deleted_at IS NULL;
|
||
CREATE INDEX idx_member_phone ON member(phone) WHERE deleted_at IS NULL;
|
||
CREATE INDEX idx_member_level ON member(level) WHERE deleted_at IS NULL;
|
||
CREATE INDEX idx_member_status ON member(status) WHERE deleted_at IS NULL;
|
||
|
||
-- 预约记录表索引
|
||
CREATE INDEX idx_booking_member ON booking_record(member_id) WHERE deleted_at IS NULL;
|
||
CREATE INDEX idx_booking_slot ON booking_record(slot_id) WHERE deleted_at IS NULL;
|
||
CREATE INDEX idx_booking_coach ON booking_record(coach_id) WHERE deleted_at IS NULL;
|
||
CREATE INDEX idx_booking_status ON booking_record(status) WHERE deleted_at IS NULL;
|
||
CREATE INDEX idx_booking_time ON booking_record(created_at) WHERE deleted_at IS NULL;
|
||
```
|
||
|
||
#### 6.1.2 查询优化
|
||
|
||
```java
|
||
// ✅ 正确:使用索引
|
||
public Flux<Member> listMembers(Long tenantId, Long storeId) {
|
||
return memberRepository.findByTenantIdAndStoreId(tenantId, storeId);
|
||
}
|
||
|
||
// ❌ 错误:全表扫描
|
||
public Flux<Member> listMembers(Long tenantId, Long storeId) {
|
||
return memberRepository.findAll()
|
||
.filter(m -> m.getTenantId().equals(tenantId))
|
||
.filter(m -> m.getStoreId().equals(storeId));
|
||
}
|
||
```
|
||
|
||
### 6.2 缓存优化
|
||
|
||
#### 6.2.1 多级缓存
|
||
|
||
```java
|
||
@Service
|
||
public class MemberService {
|
||
|
||
private final MemberRepository memberRepository;
|
||
private final ReactiveRedisTemplate<String, Object> redisTemplate;
|
||
|
||
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))
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 6.2.2 缓存策略
|
||
|
||
| 数据类型 | 缓存策略 | 过期时间 |
|
||
|---------|---------|---------|
|
||
| **会员信息** | Cache-Aside | 30 分钟 |
|
||
| **会员卡信息** | Cache-Aside | 1 小时 |
|
||
| **课程列表** | Cache-Aside | 10 分钟 |
|
||
| **预约时段** | Cache-Aside | 5 分钟 |
|
||
| **配置信息** | Write-Through | 1 小时 |
|
||
|
||
### 6.3 连接池优化
|
||
|
||
#### 6.3.1 R2DBC 连接池配置
|
||
|
||
```yaml
|
||
spring:
|
||
r2dbc:
|
||
pool:
|
||
initial-size: 5 # 初始连接数
|
||
max-size: 20 # 最大连接数
|
||
max-idle-time: 30m # 最大空闲时间
|
||
max-life-time: 1h # 最大生命周期
|
||
acquire-timeout: 5s # 获取连接超时时间
|
||
```
|
||
|
||
#### 6.3.2 Redis 连接池配置
|
||
|
||
```yaml
|
||
spring:
|
||
data:
|
||
redis:
|
||
lettuce:
|
||
pool:
|
||
max-active: 20 # 最大连接数
|
||
max-idle: 10 # 最大空闲连接数
|
||
min-idle: 5 # 最小空闲连接数
|
||
```
|
||
|
||
---
|
||
|
||
## 七、安全设计
|
||
|
||
### 7.1 认证授权
|
||
|
||
#### 7.1.1 JWT 认证
|
||
|
||
```java
|
||
@Configuration
|
||
@EnableWebFluxSecurity
|
||
public class SecurityConfig {
|
||
|
||
@Bean
|
||
public SecurityWebFilterChain securityWebFilterChain(
|
||
ServerHttpSecurity http,
|
||
JwtAuthenticationFilter jwtAuthenticationFilter) {
|
||
return http
|
||
.authorizeExchange(exchanges -> exchanges
|
||
.pathMatchers("/api/v1/auth/**").permitAll()
|
||
.pathMatchers("/actuator/**").permitAll()
|
||
.anyExchange().authenticated())
|
||
.addFilterBefore(jwtAuthenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION)
|
||
.csrf(csrf -> csrf.disable())
|
||
.httpBasic(httpBasic -> httpBasic.disable())
|
||
.formLogin(formLogin -> formLogin.disable())
|
||
.build();
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 7.1.2 数据加密
|
||
|
||
```java
|
||
@Component
|
||
public class EncryptionService {
|
||
|
||
private static final String AES_KEY = "your-secret-key";
|
||
private static final String AES_IV = "your-iv";
|
||
|
||
public String encrypt(String data) {
|
||
// AES 加密
|
||
}
|
||
|
||
public String decrypt(String encryptedData) {
|
||
// AES 解密
|
||
}
|
||
|
||
public String maskPhone(String phone) {
|
||
if (phone.length() == 11) {
|
||
return phone.substring(0, 3) + "****" + phone.substring(7);
|
||
}
|
||
return phone;
|
||
}
|
||
}
|
||
```
|
||
|
||
### 7.2 数据脱敏
|
||
|
||
```java
|
||
@Entity
|
||
public class Member {
|
||
|
||
@Column(name = "phone")
|
||
private String phone; // 加密存储
|
||
|
||
@Column(name = "phone_mask")
|
||
private String phoneMask; // 脱敏显示
|
||
|
||
@Column(name = "id_card")
|
||
private String idCard; // 加密存储
|
||
|
||
@Column(name = "emergency_phone")
|
||
private String emergencyPhone; // 加密存储
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 八、测试策略
|
||
|
||
### 8.1 单元测试
|
||
|
||
```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();
|
||
}
|
||
}
|
||
```
|
||
|
||
### 8.2 集成测试
|
||
|
||
```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("张三");
|
||
});
|
||
}
|
||
}
|
||
```
|
||
|
||
### 8.3 性能测试
|
||
|
||
```java
|
||
@Test
|
||
void testGetMemberPerformance() {
|
||
StepVerifier.withVirtualTime(() -> memberService.getMember(1L))
|
||
.expectNextCount(1)
|
||
.expectComplete()
|
||
.verify(Duration.ofMillis(100));
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 九、总结
|
||
|
||
### 9.1 架构优势
|
||
|
||
✅ **高性能**
|
||
- 响应式编程,并发能力提升 10 倍
|
||
- 响应时间降低 50%
|
||
- 资源利用率提升 75%
|
||
|
||
✅ **高可用**
|
||
- Docker Compose 一键部署
|
||
- 健康检查 + 自动重启
|
||
- 负载均衡 + 故障转移
|
||
|
||
✅ **易维护**
|
||
- 单体应用,开发效率高
|
||
- 模块化设计,易于扩展
|
||
- 完善的监控体系
|
||
|
||
✅ **低成本**
|
||
- 开发成本降低 37.5%
|
||
- 运维成本降低 40%
|
||
- 服务器资源需求低
|
||
|
||
### 9.2 关键成功因素
|
||
|
||
1. ✅ 响应式编程规范
|
||
2. ✅ 模块化设计
|
||
3. ✅ 完善的监控体系
|
||
4. ✅ 自动化部署
|
||
5. ✅ 性能优化
|
||
|
||
### 9.3 未来演进
|
||
|
||
**阶段一:单体应用(当前)**
|
||
- 模块化设计
|
||
- Docker Compose 部署
|
||
- 性能优化
|
||
|
||
**阶段二:垂直扩展(6-12 个月)**
|
||
- 增加服务器资源
|
||
- 优化数据库性能
|
||
- 引入缓存策略
|
||
|
||
**阶段三:水平扩展(12-24 个月)**
|
||
- 多实例部署
|
||
- 负载均衡
|
||
- 数据库读写分离
|
||
|
||
**阶段四:微服务(24-36 个月)**
|
||
- 按模块拆分服务
|
||
- 服务注册发现
|
||
- 分布式事务
|