# 健身房管理系统基础版详细设计文档(LLD) > 文档编号: GYM-LLD-BASIC-001 > 版本: v1.0 > 日期: 2026-03-04 > 作者: 张翔 > 状态: 初稿 --- ## 文档修订历史 | 版本 | 日期 | 作者 | 修订内容 | | ---- | ---------- | ---- | -------- | | v1.0 | 2026-03-04 | 张翔 | 创建基础版详细设计 | --- ## 参考文档 - 《健身房管理系统基础版产品设计文档》 GYM-PRD-BASIC-001 - 《健身房管理系统基础版业务概要设计文档》 GYM-HLD-BASIC-001 - 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 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 roleIds) { if (roleIds == null || roleIds.isEmpty()) { return; } List userRoles = roleIds.stream() .map(roleId -> { UserRole userRole = new UserRole(); userRole.setUserId(userId); userRole.setRoleId(roleId); return userRole; }) .collect(Collectors.toList()); userRoleRepository.saveAll(userRoles); } } ``` --- ### 2.6 UI模版定制模块 #### 2.6.1 模块概述 UI模版定制模块是基础版的核心基础模块,负责管理租户的UI定制配置,包括: - 品牌定制(Logo、颜色、背景图等) - 布局调整(模块顺序、模块隐藏等) - 预设模板(模板选择、模板应用等) - 配置历史(配置回滚、配置对比等) #### 2.6.2 数据模型设计 **租户UI配置表 (tenant_ui_config)** ```sql CREATE TABLE tenant_ui_config ( id BIGSERIAL PRIMARY KEY, tenant_id BIGINT NOT NULL, version INT NOT NULL DEFAULT 1, brand_config JSONB NOT NULL DEFAULT '{}', layout_config JSONB NOT NULL DEFAULT '{}', template_id BIGINT, is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), created_by BIGINT, updated_by BIGINT, deleted_at TIMESTAMP DEFAULT NULL, CONSTRAINT uk_tenant_ui_config UNIQUE (tenant_id, version), CONSTRAINT fk_tenant_ui_config_template FOREIGN KEY (template_id) REFERENCES ui_template(id) ); CREATE INDEX idx_tenant_ui_config_tenant ON tenant_ui_config(tenant_id); CREATE INDEX idx_tenant_ui_config_is_active ON tenant_ui_config(is_active); ``` **预设模板表 (ui_template)** ```sql CREATE TABLE ui_template ( id BIGSERIAL PRIMARY KEY, name VARCHAR(128) NOT NULL, code VARCHAR(64) NOT NULL, type VARCHAR(32) NOT NULL, description VARCHAR(512), thumbnail VARCHAR(512), preview_image VARCHAR(512), config JSONB NOT NULL DEFAULT '{}', 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 uk_ui_template_code UNIQUE (code) ); CREATE INDEX idx_ui_template_type ON ui_template(type); CREATE INDEX idx_ui_template_status ON ui_template(status); ``` **配置历史表 (ui_config_history)** ```sql CREATE TABLE ui_config_history ( id BIGSERIAL PRIMARY KEY, tenant_id BIGINT NOT NULL, config_id BIGINT NOT NULL, version INT NOT NULL, brand_config JSONB NOT NULL DEFAULT '{}', layout_config JSONB NOT NULL DEFAULT '{}', template_id BIGINT, change_reason VARCHAR(512), created_at TIMESTAMP DEFAULT NOW(), created_by BIGINT, CONSTRAINT fk_ui_config_history_config FOREIGN KEY (config_id) REFERENCES tenant_ui_config(id), CONSTRAINT fk_ui_config_history_template FOREIGN KEY (template_id) REFERENCES ui_template(id) ); CREATE INDEX idx_ui_config_history_tenant ON ui_config_history(tenant_id); CREATE INDEX idx_ui_config_history_config ON ui_config_history(config_id); ``` **资源文件表 (ui_resource)** ```sql CREATE TABLE ui_resource ( id BIGSERIAL PRIMARY KEY, tenant_id BIGINT NOT NULL, resource_type VARCHAR(32) NOT NULL, resource_name VARCHAR(128) NOT NULL, file_path VARCHAR(512) NOT NULL, file_size BIGINT, file_type VARCHAR(32), width INT, height INT, created_at TIMESTAMP DEFAULT NOW(), created_by BIGINT, deleted_at TIMESTAMP DEFAULT NULL, CONSTRAINT uk_ui_resource UNIQUE (tenant_id, resource_type, resource_name) ); CREATE INDEX idx_ui_resource_tenant ON ui_resource(tenant_id); CREATE INDEX idx_ui_resource_type ON ui_resource(resource_type); ``` #### 2.6.3 核心业务逻辑 **品牌配置Service** ```java @Service public class BrandConfigService { @Autowired private TenantUiConfigRepository tenantUiConfigRepository; @Autowired private UiResourceRepository uiResourceRepository; @Autowired private FileStorageService fileStorageService; /** * 上传Logo */ @Transactional public UiResource uploadLogo(Long tenantId, MultipartFile file, Long userId) { validateLogoFile(file); String filePath = fileStorageService.upload(file); UiResource resource = new UiResource(); resource.setTenantId(tenantId); resource.setResourceType("logo"); resource.setResourceName(file.getOriginalFilename()); resource.setFilePath(filePath); resource.setFileSize(file.getSize()); resource.setFileType(file.getContentType()); BufferedImage image = ImageIO.read(file.getInputStream()); resource.setWidth(image.getWidth()); resource.setHeight(image.getHeight()); resource = uiResourceRepository.save(resource); updateBrandConfig(tenantId, "logo", filePath, userId); return resource; } /** * 验证Logo文件 */ private void validateLogoFile(MultipartFile file) { if (file.getSize() > 2 * 1024 * 1024) { throw new BusinessException("Logo文件大小不能超过2MB"); } String contentType = file.getContentType(); if (!"image/png".equals(contentType) && !"image/jpeg".equals(contentType)) { throw new BusinessException("Logo文件格式只支持PNG或JPG"); } } /** * 更新品牌配置 */ @Transactional public void updateBrandConfig(Long tenantId, String key, String value, Long userId) { TenantUiConfig config = tenantUiConfigRepository.findActiveByTenantId(tenantId) .orElseThrow(() -> new BusinessException("租户配置不存在")); JsonObject brandConfig = config.getBrandConfig(); brandConfig.addProperty(key, value); config.setBrandConfig(brandConfig); config.setUpdatedBy(userId); tenantUiConfigRepository.save(config); } /** * 设置品牌颜色 */ @Transactional public void setBrandColor(Long tenantId, String colorType, String color, Long userId) { validateColorFormat(color); TenantUiConfig config = tenantUiConfigRepository.findActiveByTenantId(tenantId) .orElseThrow(() -> new BusinessException("租户配置不存在")); JsonObject brandConfig = config.getBrandConfig(); brandConfig.addProperty(colorType, color); config.setBrandConfig(brandConfig); config.setUpdatedBy(userId); tenantUiConfigRepository.save(config); } /** * 验证颜色格式 */ private void validateColorFormat(String color) { if (color.matches("^#[0-9A-Fa-f]{6}$")) { return; } if (color.matches("^rgb\\(\\d{1,3},\\s*\\d{1,3},\\s*\\d{1,3}\\)$")) { return; } throw new BusinessException("颜色格式不正确,请使用HEX或RGB格式"); } } ``` **布局配置Service** ```java @Service public class LayoutConfigService { @Autowired private TenantUiConfigRepository tenantUiConfigRepository; /** * 更新模块顺序 */ @Transactional public void updateModuleOrder(Long tenantId, List moduleOrder, Long userId) { TenantUiConfig config = tenantUiConfigRepository.findActiveByTenantId(tenantId) .orElseThrow(() -> new BusinessException("租户配置不存在")); JsonObject layoutConfig = config.getLayoutConfig(); JsonArray moduleOrderArray = new JsonArray(); moduleOrder.forEach(moduleOrderArray::add); layoutConfig.add("moduleOrder", moduleOrderArray); config.setLayoutConfig(layoutConfig); config.setUpdatedBy(userId); tenantUiConfigRepository.save(config); } /** * 隐藏/显示模块 */ @Transactional public void toggleModuleVisibility(Long tenantId, String moduleCode, boolean visible, Long userId) { TenantUiConfig config = tenantUiConfigRepository.findActiveByTenantId(tenantId) .orElseThrow(() -> new BusinessException("租户配置不存在")); JsonObject layoutConfig = config.getLayoutConfig(); JsonArray hiddenModules = layoutConfig.has("hiddenModules") ? layoutConfig.getAsJsonArray("hiddenModules") : new JsonArray(); if (visible) { hiddenModules.removeIf(element -> element.getAsString().equals(moduleCode)); } else { if (!hiddenModules.contains(new JsonPrimitive(moduleCode))) { hiddenModules.add(moduleCode); } } layoutConfig.add("hiddenModules", hiddenModules); config.setLayoutConfig(layoutConfig); config.setUpdatedBy(userId); tenantUiConfigRepository.save(config); } /** * 设置首页布局类型 */ @Transactional public void setHomeLayoutType(Long tenantId, String layoutType, Long userId) { TenantUiConfig config = tenantUiConfigRepository.findActiveByTenantId(tenantId) .orElseThrow(() -> new BusinessException("租户配置不存在")); JsonObject layoutConfig = config.getLayoutConfig(); layoutConfig.addProperty("homeLayoutType", layoutType); config.setLayoutConfig(layoutConfig); config.setUpdatedBy(userId); tenantUiConfigRepository.save(config); } } ``` **模板管理Service** ```java @Service public class TemplateService { @Autowired private UiTemplateRepository uiTemplateRepository; @Autowired private TenantUiConfigRepository tenantUiConfigRepository; @Autowired private UiConfigHistoryRepository uiConfigHistoryRepository; /** * 获取所有可用模板 */ public List getAvailableTemplates() { return uiTemplateRepository.findByStatus(1); } /** * 应用模板 */ @Transactional public void applyTemplate(Long tenantId, Long templateId, Long userId) { UiTemplate template = uiTemplateRepository.findById(templateId) .orElseThrow(() -> new BusinessException("模板不存在")); if (template.getStatus() != 1) { throw new BusinessException("模板不可用"); } TenantUiConfig config = tenantUiConfigRepository.findActiveByTenantId(tenantId) .orElseThrow(() -> new BusinessException("租户配置不存在")); saveConfigToHistory(config); JsonObject templateConfig = template.getConfig(); JsonObject layoutConfig = templateConfig.getAsJsonObject("layoutConfig"); config.setLayoutConfig(layoutConfig); config.setTemplateId(templateId); config.setUpdatedBy(userId); tenantUiConfigRepository.save(config); } /** * 保存配置到历史 */ private void saveConfigToHistory(TenantUiConfig config) { UiConfigHistory history = new UiConfigHistory(); history.setTenantId(config.getTenantId()); history.setConfigId(config.getId()); history.setVersion(config.getVersion()); history.setBrandConfig(config.getBrandConfig()); history.setLayoutConfig(config.getLayoutConfig()); history.setTemplateId(config.getTemplateId()); uiConfigHistoryRepository.save(history); } /** * 获取模板详情 */ public UiTemplate getTemplateDetail(Long templateId) { return uiTemplateRepository.findById(templateId) .orElseThrow(() -> new BusinessException("模板不存在")); } } ``` **配置历史Service** ```java @Service public class ConfigHistoryService { @Autowired private UiConfigHistoryRepository uiConfigHistoryRepository; @Autowired private TenantUiConfigRepository tenantUiConfigRepository; /** * 获取配置历史列表 */ public List getConfigHistory(Long tenantId) { return uiConfigHistoryRepository.findByTenantIdOrderByCreatedAtDesc(tenantId); } /** * 回滚到历史版本 */ @Transactional public void rollbackToVersion(Long tenantId, Long historyId, Long userId) { UiConfigHistory history = uiConfigHistoryRepository.findById(historyId) .orElseThrow(() -> new BusinessException("历史版本不存在")); if (!history.getTenantId().equals(tenantId)) { throw new BusinessException("无权访问该历史版本"); } TenantUiConfig config = tenantUiConfigRepository.findActiveByTenantId(tenantId) .orElseThrow(() -> new BusinessException("租户配置不存在")); config.setBrandConfig(history.getBrandConfig()); config.setLayoutConfig(history.getLayoutConfig()); config.setTemplateId(history.getTemplateId()); config.setVersion(config.getVersion() + 1); config.setUpdatedBy(userId); tenantUiConfigRepository.save(config); } /** * 对比配置 */ public Map compareConfigs(Long tenantId, Long historyId) { TenantUiConfig currentConfig = tenantUiConfigRepository.findActiveByTenantId(tenantId) .orElseThrow(() -> new BusinessException("租户配置不存在")); UiConfigHistory historyConfig = uiConfigHistoryRepository.findById(historyId) .orElseThrow(() -> new BusinessException("历史版本不存在")); Map diff = new HashMap<>(); diff.put("brandConfigDiff", compareJsonObjects( currentConfig.getBrandConfig(), historyConfig.getBrandConfig() )); diff.put("layoutConfigDiff", compareJsonObjects( currentConfig.getLayoutConfig(), historyConfig.getLayoutConfig() )); return diff; } /** * 比较JSON对象 */ private Map compareJsonObjects(JsonObject obj1, JsonObject obj2) { Map diff = new HashMap<>(); Set allKeys = new HashSet<>(); obj1.keySet().forEach(allKeys::add); obj2.keySet().forEach(allKeys::add); for (String key : allKeys) { if (!obj1.has(key)) { diff.put(key, "新增: " + obj2.get(key)); } else if (!obj2.has(key)) { diff.put(key, "删除: " + obj1.get(key)); } else if (!obj1.get(key).equals(obj2.get(key))) { diff.put(key, "修改: " + obj1.get(key) + " -> " + obj2.get(key)); } } return diff; } } ``` --- ## 三、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 } } } ``` --- ### 3.5 UI模版定制模块API #### 3.5.1 上传Logo ``` POST /api/v1/ui-config/logo Content-Type: multipart/form-data Request: { "file": , "tenantId": 1 } Response: { "code": 200, "message": "上传成功", "data": { "id": 1, "resourceType": "logo", "resourceName": "logo.png", "filePath": "/uploads/logo/1/logo.png", "fileSize": 102400, "fileType": "image/png", "width": 200, "height": 200, "createdAt": "2026-03-07T10:00:00Z" } } ``` #### 3.5.2 设置品牌颜色 ``` POST /api/v1/ui-config/brand/color Request: { "tenantId": 1, "colorType": "primaryColor", "color": "#FF5733" } Response: { "code": 200, "message": "设置成功", "data": { "primaryColor": "#FF5733", "secondaryColor": "#FFC300", "updatedAt": "2026-03-07T10:00:00Z" } } ``` #### 3.5.3 更新模块顺序 ``` POST /api/v1/ui-config/layout/module-order Request: { "tenantId": 1, "moduleOrder": ["dashboard", "member", "booking", "checkin", "statistics"] } Response: { "code": 200, "message": "更新成功", "data": { "moduleOrder": ["dashboard", "member", "booking", "checkin", "statistics"], "updatedAt": "2026-03-07T10:00:00Z" } } ``` #### 3.5.4 隐藏/显示模块 ``` POST /api/v1/ui-config/layout/module-visibility Request: { "tenantId": 1, "moduleCode": "statistics", "visible": false } Response: { "code": 200, "message": "更新成功", "data": { "hiddenModules": ["statistics"], "updatedAt": "2026-03-07T10:00:00Z" } } ``` #### 3.5.5 获取可用模板列表 ``` GET /api/v1/ui-config/templates Response: { "code": 200, "message": "查询成功", "data": [ { "id": 1, "name": "简约风格", "code": "simple", "type": "简约", "description": "简洁清爽的设计风格", "thumbnail": "/templates/simple/thumbnail.png", "previewImage": "/templates/simple/preview.png", "sortOrder": 1 }, { "id": 2, "name": "运动风格", "code": "sport", "type": "运动", "description": "活力四射的运动风格", "thumbnail": "/templates/sport/thumbnail.png", "previewImage": "/templates/sport/preview.png", "sortOrder": 2 } ] } ``` #### 3.5.6 应用模板 ``` POST /api/v1/ui-config/template/apply Request: { "tenantId": 1, "templateId": 1 } Response: { "code": 200, "message": "应用成功", "data": { "templateId": 1, "templateName": "简约风格", "appliedAt": "2026-03-07T10:00:00Z" } } ``` #### 3.5.7 获取配置历史 ``` GET /api/v1/ui-config/history?tenantId=1 Response: { "code": 200, "message": "查询成功", "data": [ { "id": 1, "version": 1, "templateId": 1, "changeReason": "应用简约风格模板", "createdAt": "2026-03-07T10:00:00Z", "createdBy": 1 }, { "id": 2, "version": 2, "templateId": 2, "changeReason": "切换到运动风格模板", "createdAt": "2026-03-07T11:00:00Z", "createdBy": 1 } ] } ``` #### 3.5.8 回滚到历史版本 ``` POST /api/v1/ui-config/history/rollback Request: { "tenantId": 1, "historyId": 1 } Response: { "code": 200, "message": "回滚成功", "data": { "version": 3, "rolledBackFrom": 1, "rolledBackAt": "2026-03-07T12:00:00Z" } } ``` #### 3.5.9 对比配置 ``` GET /api/v1/ui-config/history/compare?tenantId=1&historyId=1 Response: { "code": 200, "message": "查询成功", "data": { "brandConfigDiff": { "primaryColor": "修改: #FF5733 -> #FFC300", "logo": "删除: /uploads/logo/1/logo.png" }, "layoutConfigDiff": { "moduleOrder": "修改: [\"dashboard\", \"member\"] -> [\"member\", \"dashboard\"]", "homeLayoutType": "新增: card" } } } ``` #### 3.5.10 获取当前配置 ``` GET /api/v1/ui-config/current?tenantId=1 Response: { "code": 200, "message": "查询成功", "data": { "id": 1, "tenantId": 1, "version": 3, "brandConfig": { "logo": "/uploads/logo/1/logo.png", "primaryColor": "#FF5733", "secondaryColor": "#FFC300", "brandName": "我的健身房", "slogan": "健康生活,从现在开始" }, "layoutConfig": { "moduleOrder": ["dashboard", "member", "booking", "checkin"], "hiddenModules": ["statistics"], "homeLayoutType": "card" }, "templateId": 1, "isActive": true, "updatedAt": "2026-03-07T10:00:00Z" } } ``` --- ## 四、缓存策略 ### 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 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 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 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 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 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 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 - Spring Boot 3 官方文档 - R2DBC 规范文档 - PostgreSQL 官方文档