Files
gym-manage/docs/design/LLD-基础版系统详细设计.md
T
2026-03-05 13:48:13 +08:00

1347 lines
54 KiB
Markdown

# 健身房管理系统基础版详细设计文档(LLD)
> 文档编号: GYM-LLD-BASIC-001
> 版本: v1.0
> 日期: 2026-03-04
> 作者: 张翔
> 状态: 初稿
---
## 文档修订历史
| 版本 | 日期 | 作者 | 修订内容 |
| ---- | ---------- | ---- | -------- |
| v1.0 | 2026-03-04 | 张翔 | 创建基础版详细设计 |
---
## 参考文档
- 《健身房管理系统基础版产品设计文档》 GYM-PRD-BASIC-001
- 《健身房管理系统基础版业务概要设计文档》 GYM-HLD-BASIC-001
- 《健身房管理系统详细设计文档》 GYM-LLD-000
- Spring Boot 3 官方文档
- R2DBC 规范文档
- PostgreSQL 官方文档
---
## 一、系统架构设计
### 1.1 总体架构
采用分层架构 + 模块化设计:
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 基础版总体架构 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 客户端层 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 会员小程序 (uniapp+Vue3) │ │
│ │ • 教练端App (uniapp+Vue3) │ │
│ │ • 管理后台PC (Vue3+Vite) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ API Gateway 统一网关 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 路由转发 • 认证鉴权 • 限流熔断 • 日志追踪 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 业务层 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 会员服务 (Member Service) │ │
│ │ • 预约服务 (Booking Service) │ │
│ │ • 签到服务 (CheckIn Service) │ │
│ │ • 数据服务 (Data Service) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 公共服务层 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 认证服务 • 消息服务 • 文件服务 • 缓存服务 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 基础设施层 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • PostgreSQL • R2DBC • Caffeine • Redis(可选) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 外部服务层 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 微信开放平台 • 短信服务 • 支付服务 • OSS存储 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
### 1.2 技术架构
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 技术架构 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 前端技术栈 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • uniapp (跨平台小程序) • Vue3 (前端框架) │ │
│ │ • Vite (构建工具) • TypeScript (类型安全) │ │
│ │ • Pinia (状态管理) • Element Plus (UI组件库) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 后端技术栈 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • Spring Boot 3.2 (应用框架) • Spring Security (安全框架) │ │
│ │ • R2DBC (响应式数据库) • Spring WebFlux (响应式Web) │ │
│ │ • Caffeine (本地缓存) • Redis (分布式缓存) │ │
│ │ • PostgreSQL (关系型数据库) • MyBatis (ORM框架) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 部署架构 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • Docker (容器化) • Kubernetes (容器编排) │ │
│ │ • Nginx (反向代理) • ELK (日志收集) │ │
│ │ • Prometheus (监控) • Grafana (可视化) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
---
## 二、模块设计
### 2.1 会员模块
#### 2.1.1 模块概述
会员模块是基础版的核心基础模块,负责管理会员全生命周期,包括:
- 会员注册与信息管理
- 会员卡购买与管理
- 会员权益(时长/次数/储值/等级)管理
#### 2.1.2 数据模型设计
**会员表 (member)**
```sql
CREATE TABLE member (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
store_id BIGINT NOT NULL,
member_no VARCHAR(32) NOT NULL,
name VARCHAR(64),
phone VARCHAR(64) NOT NULL,
phone_mask VARCHAR(20),
avatar VARCHAR(512),
gender SMALLINT,
birthday DATE,
height INT,
weight DECIMAL(5,2),
fitness_goal VARCHAR(64),
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_member_tenant_no UNIQUE (tenant_id, member_no),
CONSTRAINT uk_member_phone UNIQUE (phone),
CONSTRAINT fk_member_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id),
CONSTRAINT fk_member_store FOREIGN KEY (store_id) REFERENCES store(id)
);
```
**会员卡表 (member_card)**
```sql
CREATE TABLE member_card (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
store_id BIGINT NOT NULL,
member_id BIGINT NOT NULL,
card_no VARCHAR(32) NOT NULL,
card_type SMALLINT NOT NULL,
card_name VARCHAR(64) NOT NULL,
total_amount DECIMAL(10,2),
balance DECIMAL(10,2),
total_count INT,
balance_count INT,
valid_days INT,
valid_from DATE,
valid_to DATE,
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_member_card_no UNIQUE (card_no),
CONSTRAINT fk_member_card_member FOREIGN KEY (member_id) REFERENCES member(id)
);
```
#### 2.1.3 核心服务设计
**会员注册服务**
```java
@Service
public class MemberRegistrationService {
@Autowired
private MemberRepository memberRepository;
@Autowired
private SmsService smsService;
@Autowired
private MemberCardService memberCardService;
/**
* 会员注册
*/
@Transactional
public Member register(MemberRegistrationRequest request) {
validatePhone(request.getPhone());
validateSmsCode(request.getPhone(), request.getSmsCode());
Member member = new Member();
member.setTenantId(request.getTenantId());
member.setStoreId(request.getStoreId());
member.setMemberNo(generateMemberNo(request.getTenantId()));
member.setName(request.getName());
member.setPhone(encryptPhone(request.getPhone()));
member.setPhoneMask(maskPhone(request.getPhone()));
member.setGender(request.getGender());
member.setBirthday(request.getBirthday());
member.setHeight(request.getHeight());
member.setWeight(request.getWeight());
member.setFitnessGoal(request.getFitnessGoal());
member.setStatus(1);
return memberRepository.save(member);
}
/**
* 验证手机号
*/
private void validatePhone(String phone) {
if (!memberRepository.findByPhone(phone).isEmpty()) {
throw new BusinessException("手机号已存在");
}
}
/**
* 验证短信验证码
*/
private void validateSmsCode(String phone, String smsCode) {
if (!smsService.verifySmsCode(phone, smsCode)) {
throw new BusinessException("验证码错误");
}
}
/**
* 生成会员号
*/
private String generateMemberNo(Long tenantId) {
String prefix = "M" + tenantId;
String timestamp = String.valueOf(System.currentTimeMillis());
String random = String.valueOf(new Random().nextInt(1000));
return prefix + timestamp.substring(timestamp.length() - 8) + random;
}
/**
* 加密手机号
*/
private String encryptPhone(String phone) {
return AESUtil.encrypt(phone);
}
/**
* 脱敏手机号
*/
private String maskPhone(String phone) {
return phone.substring(0, 3) + "****" + phone.substring(7);
}
}
```
**会员卡购买服务**
```java
@Service
public class MemberCardPurchaseService {
@Autowired
private MemberCardRepository memberCardRepository;
@Autowired
private PaymentService paymentService;
@Autowired
private BenefitService benefitService;
/**
* 购买会员卡
*/
@Transactional
public MemberCard purchase(MemberCardPurchaseRequest request) {
Member member = memberRepository.findById(request.getMemberId())
.orElseThrow(() -> new BusinessException("会员不存在"));
Payment payment = paymentService.createPayment(request);
MemberCard memberCard = new MemberCard();
memberCard.setTenantId(member.getTenantId());
memberCard.setStoreId(member.getStoreId());
memberCard.setMemberId(member.getId());
memberCard.setCardNo(generateCardNo(member.getTenantId()));
memberCard.setCardType(request.getCardType());
memberCard.setCardName(request.getCardName());
memberCard.setTotalAmount(request.getAmount());
memberCard.setBalance(request.getAmount());
memberCard.setTotalCount(request.getCount());
memberCard.setBalanceCount(request.getCount());
memberCard.setValidDays(request.getValidDays());
memberCard.setValidFrom(LocalDate.now());
memberCard.setValidTo(LocalDate.now().plusDays(request.getValidDays()));
memberCard.setStatus(1);
memberCard = memberCardRepository.save(memberCard);
benefitService.createBenefits(memberCard);
return memberCard;
}
/**
* 生成卡号
*/
private String generateCardNo(Long tenantId) {
String prefix = "C" + tenantId;
String timestamp = String.valueOf(System.currentTimeMillis());
String random = String.valueOf(new Random().nextInt(1000));
return prefix + timestamp.substring(timestamp.length() - 8) + random;
}
}
```
---
### 2.2 预约模块
#### 2.2.1 模块概述
预约模块是基础版的核心业务模块,负责管理团课预约,包括:
- 团课管理
- 团课预约
- 预约取消
#### 2.2.2 数据模型设计
**课程表 (course)**
```sql
CREATE TABLE course (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
name VARCHAR(128) NOT NULL,
code VARCHAR(32),
type SMALLINT NOT NULL,
category VARCHAR(64),
description TEXT,
cover_image VARCHAR(512),
duration INT NOT NULL,
capacity INT DEFAULT 20,
min_capacity INT DEFAULT 1,
difficulty SMALLINT DEFAULT 1,
calories INT,
equipment VARCHAR(256),
benefits JSONB,
price DECIMAL(10,2),
price_type SMALLINT DEFAULT 1,
price_value DECIMAL(10,2),
advance_days INT DEFAULT 7,
cancel_hours INT DEFAULT 2,
cancel_penalty DECIMAL(3,2) DEFAULT 0.00,
status SMALLINT DEFAULT 1,
sort_order 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 fk_course_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id)
);
```
**课程时段表 (course_slot)**
```sql
CREATE TABLE course_slot (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
store_id BIGINT NOT NULL,
course_id BIGINT NOT NULL,
coach_id BIGINT,
slot_date DATE NOT NULL,
start_time TIME NOT NULL,
end_time TIME NOT NULL,
capacity INT DEFAULT 20,
booked_count INT DEFAULT 0,
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_course_slot_course FOREIGN KEY (course_id) REFERENCES course(id),
CONSTRAINT fk_course_slot_coach FOREIGN KEY (coach_id) REFERENCES coach(id)
);
```
**预约记录表 (booking_record)**
```sql
CREATE TABLE booking_record (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
store_id BIGINT NOT NULL,
member_id BIGINT NOT NULL,
course_id BIGINT NOT NULL,
slot_id BIGINT NOT NULL,
booking_no VARCHAR(32) NOT NULL,
status SMALLINT DEFAULT 1,
booked_at TIMESTAMP DEFAULT NOW(),
cancelled_at TIMESTAMP,
cancel_reason VARCHAR(256),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
created_by BIGINT,
updated_by BIGINT,
deleted_at TIMESTAMP DEFAULT NULL,
CONSTRAINT uk_booking_no UNIQUE (booking_no),
CONSTRAINT fk_booking_member FOREIGN KEY (member_id) REFERENCES member(id),
CONSTRAINT fk_booking_slot FOREIGN KEY (slot_id) REFERENCES course_slot(id)
);
```
#### 2.2.3 核心服务设计
**团课预约服务**
```java
@Service
public class CourseBookingService {
@Autowired
private BookingRecordRepository bookingRecordRepository;
@Autowired
private CourseSlotRepository courseSlotRepository;
@Autowired
private BenefitService benefitService;
@Autowired
private MessageService messageService;
/**
* 预约团课
*/
@Transactional
public BookingRecord book(CourseBookingRequest request) {
validateBookingTime(request.getSlotId());
validateCapacity(request.getSlotId());
validateMemberBenefit(request.getMemberId(), request.getSlotId());
CourseSlot slot = courseSlotRepository.findById(request.getSlotId())
.orElseThrow(() -> new BusinessException("课程时段不存在"));
BookingRecord record = new BookingRecord();
record.setTenantId(slot.getTenantId());
record.setStoreId(slot.getStoreId());
record.setMemberId(request.getMemberId());
record.setCourseId(slot.getCourseId());
record.setSlotId(slot.getId());
record.setBookingNo(generateBookingNo(slot.getTenantId()));
record.setStatus(1);
record = bookingRecordRepository.save(record);
slot.setBookedCount(slot.getBookedCount() + 1);
courseSlotRepository.save(slot);
benefitService.deductBenefit(request.getMemberId(), slot.getCourseId());
messageService.sendBookingNotification(record);
return record;
}
/**
* 验证预约时间
*/
private void validateBookingTime(Long slotId) {
CourseSlot slot = courseSlotRepository.findById(slotId)
.orElseThrow(() -> new BusinessException("课程时段不存在"));
LocalDateTime slotDateTime = LocalDateTime.of(slot.getSlotDate(), slot.getStartTime());
if (slotDateTime.isBefore(LocalDateTime.now().plusMinutes(30))) {
throw new BusinessException("预约时间过短");
}
}
/**
* 验证容量
*/
private void validateCapacity(Long slotId) {
CourseSlot slot = courseSlotRepository.findById(slotId)
.orElseThrow(() -> new BusinessException("课程时段不存在"));
if (slot.getBookedCount() >= slot.getCapacity()) {
throw new BusinessException("课程已满");
}
}
/**
* 验证会员权益
*/
private void validateMemberBenefit(Long memberId, Long slotId) {
if (!benefitService.hasBenefit(memberId, slotId)) {
throw new BusinessException("会员权益不足");
}
}
/**
* 生成预约号
*/
private String generateBookingNo(Long tenantId) {
String prefix = "B" + tenantId;
String timestamp = String.valueOf(System.currentTimeMillis());
String random = String.valueOf(new Random().nextInt(1000));
return prefix + timestamp.substring(timestamp.length() - 8) + random;
}
}
```
---
### 2.3 签到模块
#### 2.3.1 模块概述
签到模块是基础版的核心业务模块,负责管理会员的入场签到,支持:
- 二维码签到
- 签到记录管理
#### 2.3.2 数据模型设计
**签到记录表 (checkin_record)**
```sql
CREATE TABLE checkin_record (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
store_id BIGINT NOT NULL,
member_id BIGINT NOT NULL,
booking_id BIGINT,
type SMALLINT NOT NULL,
method SMALLINT NOT NULL,
status SMALLINT DEFAULT 1,
checkin_at TIMESTAMP NOT NULL,
checkin_date DATE NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
created_by BIGINT,
updated_by BIGINT,
deleted_at TIMESTAMP DEFAULT NULL,
CONSTRAINT fk_checkin_member FOREIGN KEY (member_id) REFERENCES member(id),
CONSTRAINT fk_checkin_booking FOREIGN KEY (booking_id) REFERENCES booking_record(id)
);
```
#### 2.3.3 核心服务设计
**扫码签到服务**
```java
@Service
public class QRCodeCheckInService {
@Autowired
private CheckInRecordRepository checkInRecordRepository;
@Autowired
private MemberRepository memberRepository;
@Autowired
private MemberCardRepository memberCardRepository;
/**
* 扫码签到
*/
@Transactional
public CheckInRecord checkIn(QRCodeCheckInRequest request) {
Member member = memberRepository.findByQrCode(request.getQrCode())
.orElseThrow(() -> new BusinessException("会员不存在"));
validateMemberCard(member.getId());
CheckInRecord record = new CheckInRecord();
record.setTenantId(member.getTenantId());
record.setStoreId(member.getStoreId());
record.setMemberId(member.getId());
record.setType(1);
record.setMethod(1);
record.setStatus(1);
record.setCheckInAt(LocalDateTime.now());
record.setCheckInDate(LocalDate.now());
return checkInRecordRepository.save(record);
}
/**
* 验证会员卡
*/
private void validateMemberCard(Long memberId) {
List<MemberCard> cards = memberCardRepository.findByMemberId(memberId);
if (cards.isEmpty()) {
throw new BusinessException("会员卡不存在");
}
boolean hasValidCard = cards.stream()
.anyMatch(card -> card.getStatus() == 1 &&
(card.getValidTo() == null || card.getValidTo().isAfter(LocalDate.now())));
if (!hasValidCard) {
throw new BusinessException("会员卡无效");
}
}
}
```
---
### 2.4 数据统计模块
#### 2.4.1 模块概述
数据统计模块是基础版的核心业务模块,负责提供基础数据统计,包括:
- 会员数据统计
- 预约数据统计
- 签到数据统计
#### 2.4.2 核心服务设计
**数据统计服务**
```java
@Service
public class DataStatisticsService {
@Autowired
private MemberRepository memberRepository;
@Autowired
private BookingRecordRepository bookingRecordRepository;
@Autowired
private CheckInRecordRepository checkInRecordRepository;
/**
* 获取会员数据统计
*/
public MemberStatistics getMemberStatistics(Long tenantId, Long storeId, LocalDate startDate, LocalDate endDate) {
long totalMembers = memberRepository.countByTenantIdAndStoreIdAndCreatedAtBetween(
tenantId, storeId, startDate.atStartOfDay(), endDate.atTime(23, 59, 59));
long activeMembers = memberRepository.countActiveMembers(tenantId, storeId, startDate, endDate);
MemberStatistics statistics = new MemberStatistics();
statistics.setTotalMembers(totalMembers);
statistics.setActiveMembers(activeMembers);
statistics.setNewMembers(totalMembers);
return statistics;
}
/**
* 获取预约数据统计
*/
public BookingStatistics getBookingStatistics(Long tenantId, Long storeId, LocalDate startDate, LocalDate endDate) {
long totalBookings = bookingRecordRepository.countByTenantIdAndStoreIdAndBookedAtBetween(
tenantId, storeId, startDate.atStartOfDay(), endDate.atTime(23, 59, 59));
long cancelledBookings = bookingRecordRepository.countCancelledBookings(tenantId, storeId, startDate, endDate);
BookingStatistics statistics = new BookingStatistics();
statistics.setTotalBookings(totalBookings);
statistics.setCancelledBookings(cancelledBookings);
statistics.setSuccessBookings(totalBookings - cancelledBookings);
return statistics;
}
/**
* 获取签到数据统计
*/
public CheckInStatistics getCheckInStatistics(Long tenantId, Long storeId, LocalDate startDate, LocalDate endDate) {
long totalCheckIns = checkInRecordRepository.countByTenantIdAndStoreIdAndCheckInAtBetween(
tenantId, storeId, startDate.atStartOfDay(), endDate.atTime(23, 59, 59));
CheckInStatistics statistics = new CheckInStatistics();
statistics.setTotalCheckIns(totalCheckIns);
return statistics;
}
}
```
---
### 2.5 系统管理模块
#### 2.5.1 模块概述
系统管理模块是基础版的核心基础模块,负责管理系统用户和权限,包括:
- 用户管理
- 角色权限管理
#### 2.5.2 数据模型设计
**用户表 (user)**
```sql
CREATE TABLE user (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
store_id BIGINT,
username VARCHAR(64) NOT NULL,
password VARCHAR(256) NOT NULL,
name VARCHAR(64) NOT NULL,
phone VARCHAR(64),
email VARCHAR(128),
avatar VARCHAR(512),
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_user_username UNIQUE (username),
CONSTRAINT fk_user_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id),
CONSTRAINT fk_user_store FOREIGN KEY (store_id) REFERENCES store(id)
);
```
**角色表 (role)**
```sql
CREATE TABLE role (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
name VARCHAR(64) NOT NULL,
code VARCHAR(32) NOT NULL,
description VARCHAR(256),
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_role_code UNIQUE (code),
CONSTRAINT fk_role_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id)
);
```
**用户角色关联表 (user_role)**
```sql
CREATE TABLE user_role (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
created_by BIGINT,
CONSTRAINT uk_user_role UNIQUE (user_id, role_id),
CONSTRAINT fk_user_role_user FOREIGN KEY (user_id) REFERENCES user(id),
CONSTRAINT fk_user_role_role FOREIGN KEY (role_id) REFERENCES role(id)
);
```
#### 2.5.3 核心服务设计
**用户管理服务**
```java
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private UserRoleRepository userRoleRepository;
@Autowired
private PasswordEncoder passwordEncoder;
/**
* 创建用户
*/
@Transactional
public User createUser(UserCreateRequest request) {
validateUsername(request.getUsername());
User user = new User();
user.setTenantId(request.getTenantId());
user.setStoreId(request.getStoreId());
user.setUsername(request.getUsername());
user.setPassword(passwordEncoder.encode(request.getPassword()));
user.setName(request.getName());
user.setPhone(request.getPhone());
user.setEmail(request.getEmail());
user.setStatus(1);
user = userRepository.save(user);
assignRoles(user.getId(), request.getRoleIds());
return user;
}
/**
* 验证用户名
*/
private void validateUsername(String username) {
if (userRepository.findByUsername(username).isPresent()) {
throw new BusinessException("用户名已存在");
}
}
/**
* 分配角色
*/
private void assignRoles(Long userId, List<Long> roleIds) {
if (roleIds == null || roleIds.isEmpty()) {
return;
}
List<UserRole> userRoles = roleIds.stream()
.map(roleId -> {
UserRole userRole = new UserRole();
userRole.setUserId(userId);
userRole.setRoleId(roleId);
return userRole;
})
.collect(Collectors.toList());
userRoleRepository.saveAll(userRoles);
}
}
```
---
## 三、API设计
### 3.1 会员模块API
#### 3.1.1 会员注册
```
POST /api/v1/members/register
Request:
{
"tenantId": 1,
"storeId": 1,
"phone": "13800138000",
"smsCode": "123456",
"name": "张三",
"gender": 1,
"birthday": "1990-01-01",
"height": 175,
"weight": 70.5,
"fitnessGoal": "减脂"
}
Response:
{
"code": 200,
"message": "注册成功",
"data": {
"id": 1,
"memberNo": "M10000000000000001",
"name": "张三",
"phoneMask": "138****8000",
"status": 1
}
}
```
#### 3.1.2 购买会员卡
```
POST /api/v1/member-cards/purchase
Request:
{
"memberId": 1,
"cardType": 1,
"cardName": "月卡",
"amount": 299.00,
"count": 30,
"validDays": 30
}
Response:
{
"code": 200,
"message": "购买成功",
"data": {
"id": 1,
"cardNo": "C10000000000000001",
"cardName": "月卡",
"balance": 299.00,
"balanceCount": 30,
"validFrom": "2026-03-04",
"validTo": "2026-04-03",
"status": 1
}
}
```
### 3.2 预约模块API
#### 3.2.1 预约团课
```
POST /api/v1/bookings/course
Request:
{
"memberId": 1,
"slotId": 1
}
Response:
{
"code": 200,
"message": "预约成功",
"data": {
"id": 1,
"bookingNo": "B10000000000000001",
"courseName": "瑜伽",
"slotDate": "2026-03-05",
"startTime": "10:00:00",
"endTime": "11:00:00",
"status": 1
}
}
```
### 3.3 签到模块API
#### 3.3.1 扫码签到
```
POST /api/v1/checkins/qrcode
Request:
{
"qrCode": "M10000000000000001"
}
Response:
{
"code": 200,
"message": "签到成功",
"data": {
"id": 1,
"memberName": "张三",
"checkInAt": "2026-03-04T10:00:00",
"status": 1
}
}
```
### 3.4 数据统计模块API
#### 3.4.1 获取数据统计
```
GET /api/v1/statistics/overview?tenantId=1&storeId=1&startDate=2026-03-01&endDate=2026-03-31
Response:
{
"code": 200,
"message": "查询成功",
"data": {
"memberStatistics": {
"totalMembers": 100,
"activeMembers": 80,
"newMembers": 20
},
"bookingStatistics": {
"totalBookings": 500,
"cancelledBookings": 50,
"successBookings": 450
},
"checkInStatistics": {
"totalCheckIns": 800
}
}
}
```
---
## 四、缓存策略
### 4.1 缓存设计
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 缓存策略 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 本地缓存 (Caffeine) │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 会员信息缓存 (TTL: 30分钟) │ │
│ │ • 会员卡缓存 (TTL: 30分钟) │ │
│ │ • 课程信息缓存 (TTL: 1小时) │ │
│ │ • 课程时段缓存 (TTL: 30分钟) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 分布式缓存 (Redis) │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 验证码缓存 (TTL: 5分钟) │ │
│ │ • 令牌缓存 (TTL: 24小时) │ │
│ │ • 限流计数器 (TTL: 1分钟) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
### 4.2 缓存实现
**会员缓存服务**
```java
@Service
public class MemberCacheService {
@Autowired
private CacheManager cacheManager;
private static final String MEMBER_CACHE = "member";
private static final String MEMBER_CARD_CACHE = "memberCard";
/**
* 获取会员缓存
*/
public Optional<Member> getMember(Long memberId) {
Cache cache = cacheManager.getCache(MEMBER_CACHE);
if (cache != null) {
return Optional.ofNullable(cache.get(memberId, Member.class));
}
return Optional.empty();
}
/**
* 设置会员缓存
*/
public void setMember(Member member) {
Cache cache = cacheManager.getCache(MEMBER_CACHE);
if (cache != null) {
cache.put(member.getId(), member);
}
}
/**
* 删除会员缓存
*/
public void evictMember(Long memberId) {
Cache cache = cacheManager.getCache(MEMBER_CACHE);
if (cache != null) {
cache.evict(memberId);
}
}
/**
* 获取会员卡缓存
*/
public Optional<MemberCard> getMemberCard(Long cardId) {
Cache cache = cacheManager.getCache(MEMBER_CARD_CACHE);
if (cache != null) {
return Optional.ofNullable(cache.get(cardId, MemberCard.class));
}
return Optional.empty();
}
/**
* 设置会员卡缓存
*/
public void setMemberCard(MemberCard card) {
Cache cache = cacheManager.getCache(MEMBER_CARD_CACHE);
if (cache != null) {
cache.put(card.getId(), card);
}
}
/**
* 删除会员卡缓存
*/
public void evictMemberCard(Long cardId) {
Cache cache = cacheManager.getCache(MEMBER_CARD_CACHE);
if (cache != null) {
cache.evict(cardId);
}
}
}
```
---
## 五、异常处理
### 5.1 异常分类
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 异常分类 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 业务异常 (BusinessException) │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 会员不存在 • 会员卡无效 • 预约失败 • 签到失败 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 参数异常 (ValidationException) │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 参数为空 • 参数格式错误 • 参数超出范围 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 权限异常 (PermissionException) │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 无权限 • 权限不足 • 令牌过期 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 系统异常 (SystemException) │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 数据库异常 • 网络异常 • 服务异常 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
### 5.2 异常处理实现
**全局异常处理器**
```java
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 业务异常
*/
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) {
ErrorResponse response = new ErrorResponse();
response.setCode(e.getCode());
response.setMessage(e.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
/**
* 参数异常
*/
@ExceptionHandler(ValidationException.class)
public ResponseEntity<ErrorResponse> handleValidationException(ValidationException e) {
ErrorResponse response = new ErrorResponse();
response.setCode(400);
response.setMessage(e.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
/**
* 权限异常
*/
@ExceptionHandler(PermissionException.class)
public ResponseEntity<ErrorResponse> handlePermissionException(PermissionException e) {
ErrorResponse response = new ErrorResponse();
response.setCode(403);
response.setMessage(e.getMessage());
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(response);
}
/**
* 系统异常
*/
@ExceptionHandler(SystemException.class)
public ResponseEntity<ErrorResponse> handleSystemException(SystemException e) {
ErrorResponse response = new ErrorResponse();
response.setCode(500);
response.setMessage("系统异常");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}
}
```
---
## 六、测试用例
### 6.1 会员模块测试用例
#### 6.1.1 会员注册测试
| 测试用例 | 输入 | 预期输出 |
|---------|------|---------|
| 正常注册 | 手机号、验证码、姓名 | 注册成功 |
| 手机号已存在 | 已存在的手机号 | 提示手机号已存在 |
| 验证码错误 | 错误的验证码 | 提示验证码错误 |
| 验证码过期 | 过期的验证码 | 提示验证码过期 |
#### 6.1.2 购买会员卡测试
| 测试用例 | 输入 | 预期输出 |
|---------|------|---------|
| 正常购买 | 会员ID、卡类型、金额 | 购买成功 |
| 会员不存在 | 不存在的会员ID | 提示会员不存在 |
| 支付失败 | 支付失败 | 提示支付失败 |
### 6.2 预约模块测试用例
#### 6.2.1 预约团课测试
| 测试用例 | 输入 | 预期输出 |
|---------|------|---------|
| 正常预约 | 会员ID、课程时段ID | 预约成功 |
| 预约时间过短 | 课程开始前30分钟内 | 提示预约时间过短 |
| 课程已满 | 已满的课程时段 | 提示课程已满 |
| 权益不足 | 权益不足的会员 | 提示权益不足 |
### 6.3 签到模块测试用例
#### 6.3.1 扫码签到测试
| 测试用例 | 输入 | 预期输出 |
|---------|------|---------|
| 正常签到 | 有效的二维码 | 签到成功 |
| 会员不存在 | 无效的二维码 | 提示会员不存在 |
| 会员卡无效 | 会员卡无效 | 提示会员卡无效 |
---
## 七、部署与运维
### 7.1 部署架构
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 部署架构 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 负载均衡 (Nginx) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 应用服务器 (Kubernetes) │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • Pod 1 • Pod 2 • Pod 3 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 数据库 (PostgreSQL) │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 主库 • 从库 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 缓存 (Redis) │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 主节点 • 从节点 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
### 7.2 监控指标
| 指标类型 | 指标名称 | 阈值 |
|---------|---------|------|
| 系统指标 | CPU使用率 | ≤ 80% |
| 系统指标 | 内存使用率 | ≤ 80% |
| 系统指标 | 磁盘使用率 | ≤ 80% |
| 应用指标 | API响应时间 | ≤ 500ms |
| 应用指标 | 错误率 | ≤ 1% |
| 应用指标 | 并发数 | ≤ 100 |
---
## 八、附录
### 8.1 术语定义
| 术语 | 定义 |
|------|------|
| 会员 | 在健身房注册的用户 |
| 会员卡 | 会员购买的权益卡,包括时长卡、次卡、储值卡 |
| 权益 | 会员卡包含的时长、次数、储值、等级等权益 |
| 团课 | 集体课程,由教练带领多个会员一起上课 |
| 预约 | 会员预约团课 |
| 签到 | 会员到店记录 |
### 8.2 参考文档
- 《健身房管理系统基础版产品设计文档》 GYM-PRD-BASIC-001
- 《健身房管理系统基础版业务概要设计文档》 GYM-HLD-BASIC-001
- 《健身房管理系统详细设计文档》 GYM-LLD-000
- Spring Boot 3 官方文档
- R2DBC 规范文档
- PostgreSQL 官方文档