971d51cb36
PRD更新: - 新增2.6 UI模版定制模块 - 包含品牌定制、布局调整、预设模板、配置历史、可视化配置器五个子模块 - 每个子模块包含功能描述、用户故事、功能点、业务规则、验收标准 HLD更新: - 业务范围中新增UI模版定制模块 - 新增3.5 UI模版定制流程(业务场景、业务流程、业务规则、异常处理) - 新增4.6 UI模版定制规则(品牌元素应用、Logo格式限制、颜色格式限制等8条规则) LLD更新: - 新增2.6 UI模版定制模块(模块概述、数据模型设计、核心业务逻辑) - 数据模型包含4个表:tenant_ui_config、ui_template、ui_config_history、ui_resource - 核心业务逻辑包含4个Service:BrandConfigService、LayoutConfigService、TemplateService、ConfigHistoryService - 新增3.5 UI模版定制模块API(10个API接口,涵盖品牌定制、布局调整、模板管理、配置历史) 所有文档已保持一致性,UI模版定制功能已完整同步到产品需求、概要设计、详细设计文档中
2080 lines
73 KiB
Markdown
2080 lines
73 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
|
|
- 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);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 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<String> 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<UiTemplate> 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<UiConfigHistory> 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<String, Object> compareConfigs(Long tenantId, Long historyId) {
|
|
TenantUiConfig currentConfig = tenantUiConfigRepository.findActiveByTenantId(tenantId)
|
|
.orElseThrow(() -> new BusinessException("租户配置不存在"));
|
|
|
|
UiConfigHistory historyConfig = uiConfigHistoryRepository.findById(historyId)
|
|
.orElseThrow(() -> new BusinessException("历史版本不存在"));
|
|
|
|
Map<String, Object> diff = new HashMap<>();
|
|
diff.put("brandConfigDiff", compareJsonObjects(
|
|
currentConfig.getBrandConfig(),
|
|
historyConfig.getBrandConfig()
|
|
));
|
|
diff.put("layoutConfigDiff", compareJsonObjects(
|
|
currentConfig.getLayoutConfig(),
|
|
historyConfig.getLayoutConfig()
|
|
));
|
|
|
|
return diff;
|
|
}
|
|
|
|
/**
|
|
* 比较JSON对象
|
|
*/
|
|
private Map<String, Object> compareJsonObjects(JsonObject obj1, JsonObject obj2) {
|
|
Map<String, Object> diff = new HashMap<>();
|
|
|
|
Set<String> 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": <binary>,
|
|
"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<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
|
|
- Spring Boot 3 官方文档
|
|
- R2DBC 规范文档
|
|
- PostgreSQL 官方文档
|