1345 lines
54 KiB
Markdown
1345 lines
54 KiB
Markdown
# 健身房管理系统付费订阅版详细设计文档(LLD)
|
|
|
|
> 文档编号: GYM-LLD-SUBSCRIPTION-001
|
|
> 版本: v1.0
|
|
> 日期: 2026-03-04
|
|
> 作者: 张翔
|
|
> 状态: 初稿
|
|
|
|
---
|
|
|
|
## 文档修订历史
|
|
|
|
| 版本 | 日期 | 作者 | 修订内容 |
|
|
| ---- | ---------- | ---- | -------- |
|
|
| v1.0 | 2026-03-04 | 张翔 | 创建付费订阅版详细设计 |
|
|
|
|
---
|
|
|
|
## 参考文档
|
|
|
|
- 《健身房管理系统付费订阅版产品设计文档》 GYM-PRD-SUBSCRIPTION-001
|
|
- 《健身房管理系统付费订阅版业务概要设计文档》 GYM-HLD-SUBSCRIPTION-001
|
|
- 《健身房管理系统详细设计文档》 GYM-LLD-000
|
|
- 《订阅与配置模块详细设计文档》 GYM-LLD-004
|
|
- Spring Boot 3 官方文档
|
|
- R2DBC 规范文档
|
|
- PostgreSQL 官方文档
|
|
|
|
---
|
|
|
|
## 一、系统架构设计
|
|
|
|
### 1.1 总体架构
|
|
|
|
采用分层架构 + 模块化设计的单体应用:
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────────────┐
|
|
│ 付费订阅版单体应用架构 │
|
|
├─────────────────────────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
|
│ │ 客户端层 │ │
|
|
│ ├─────────────────────────────────────────────────────────────────┤ │
|
|
│ │ • 会员小程序 (uniapp+Vue3) │ │
|
|
│ │ • 教练端App (uniapp+Vue3) │ │
|
|
│ │ • 管理后台PC (Vue3+Vite) │ │
|
|
│ │ • 硬件设备 (人脸/NFC) │ │
|
|
│ └─────────────────────────────────────────────────────────────────┘ │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
|
│ │ Presentation Layer (WebFlux) │ │
|
|
│ ├─────────────────────────────────────────────────────────────────┤ │
|
|
│ │ • Controller • Router • Filter • Validator │ │
|
|
│ └─────────────────────────────────────────────────────────────────┘ │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
|
│ │ Application Layer (业务编排) │ │
|
|
│ ├─────────────────────────────────────────────────────────────────┤ │
|
|
│ │ • Service • Facade • Orchestrator • 事务管理 │ │
|
|
│ └─────────────────────────────────────────────────────────────────┘ │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
|
│ │ Domain Layer (领域模型) │ │
|
|
│ ├─────────────────────────────────────────────────────────────────┤ │
|
|
│ │ • Entity • Value Object • Domain Service • Repository │ │
|
|
│ └─────────────────────────────────────────────────────────────────┘ │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
|
│ │ Infrastructure Layer (基础设施) │ │
|
|
│ ├─────────────────────────────────────────────────────────────────┤ │
|
|
│ │ • Repository (R2DBC) • Cache (Redis) │ │
|
|
│ │ • Message (RabbitMQ) • Search (Elasticsearch) │ │
|
|
│ │ • File (OSS) • Distributed Lock │ │
|
|
│ └─────────────────────────────────────────────────────────────────┘ │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
|
│ │ 外部服务层 │ │
|
|
│ ├─────────────────────────────────────────────────────────────────┤ │
|
|
│ │ • PostgreSQL • Redis • RabbitMQ • Elasticsearch │ │
|
|
│ │ • 微信开放平台 • 短信服务 • 支付服务 • OSS存储 │ │
|
|
│ └─────────────────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
└─────────────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## 二、订阅与配置模块设计
|
|
|
|
### 2.1 模块概述
|
|
|
|
订阅与配置模块是付费订阅版的核心基础设施模块,负责:
|
|
|
|
- 产品版本管理(基础版 + 订阅模块)
|
|
- 租户级和门店级配置管理
|
|
- 配置继承与覆盖机制
|
|
- 订阅计费与生命周期管理
|
|
|
|
### 2.2 数据模型设计
|
|
|
|
**租户模块配置表 (tenant_module_config)**
|
|
|
|
```sql
|
|
CREATE TABLE tenant_module_config (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
tenant_id BIGINT NOT NULL,
|
|
module_code VARCHAR(32) NOT NULL,
|
|
enabled BOOLEAN NOT NULL,
|
|
config_data JSONB,
|
|
version INT DEFAULT 0,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
updated_at TIMESTAMP DEFAULT NOW(),
|
|
created_by BIGINT,
|
|
updated_by BIGINT,
|
|
deleted_at TIMESTAMP DEFAULT NULL,
|
|
|
|
CONSTRAINT uk_tenant_module UNIQUE (tenant_id, module_code),
|
|
CONSTRAINT fk_tenant_module_config FOREIGN KEY (tenant_id) REFERENCES tenant(id)
|
|
);
|
|
```
|
|
|
|
**门店模块配置表 (store_module_config)**
|
|
|
|
```sql
|
|
CREATE TABLE store_module_config (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
tenant_id BIGINT NOT NULL,
|
|
store_id BIGINT NOT NULL,
|
|
module_code VARCHAR(32) NOT NULL,
|
|
inherit_mode SMALLINT NOT NULL,
|
|
enabled BOOLEAN NOT NULL,
|
|
config_data JSONB,
|
|
version INT DEFAULT 0,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
updated_at TIMESTAMP DEFAULT NOW(),
|
|
created_by BIGINT,
|
|
updated_by BIGINT,
|
|
deleted_at TIMESTAMP DEFAULT NULL,
|
|
|
|
CONSTRAINT uk_store_module UNIQUE (store_id, module_code),
|
|
CONSTRAINT fk_store_module_config FOREIGN KEY (store_id) REFERENCES store(id)
|
|
);
|
|
```
|
|
|
|
**订阅记录表 (subscription_record)**
|
|
|
|
```sql
|
|
CREATE TABLE subscription_record (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
tenant_id BIGINT NOT NULL,
|
|
subscription_no VARCHAR(32) NOT NULL,
|
|
module_code VARCHAR(32) NOT NULL,
|
|
billing_cycle SMALLINT NOT NULL,
|
|
amount DECIMAL(10,2) NOT NULL,
|
|
discount_amount DECIMAL(10,2) DEFAULT 0.00,
|
|
actual_amount DECIMAL(10,2) NOT NULL,
|
|
start_date DATE NOT NULL,
|
|
end_date DATE NOT NULL,
|
|
status SMALLINT DEFAULT 1,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
updated_at TIMESTAMP DEFAULT NOW(),
|
|
created_by BIGINT,
|
|
updated_by BIGINT,
|
|
deleted_at TIMESTAMP DEFAULT NULL,
|
|
|
|
CONSTRAINT uk_subscription_no UNIQUE (subscription_no),
|
|
CONSTRAINT fk_subscription_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id)
|
|
);
|
|
```
|
|
|
|
### 2.3 核心服务设计
|
|
|
|
**配置查询服务**
|
|
|
|
```java
|
|
@Service
|
|
@Slf4j
|
|
@RequiredArgsConstructor
|
|
public class ConfigQueryService {
|
|
|
|
private final TenantModuleConfigRepository tenantModuleConfigRepository;
|
|
private final StoreModuleConfigRepository storeModuleConfigRepository;
|
|
private final ConfigMerger configMerger;
|
|
private final ReactiveRedisTemplate<String, ModuleConfig> redisTemplate;
|
|
|
|
private static final String CACHE_PREFIX = "config:";
|
|
private static final Duration CACHE_TTL = Duration.ofMinutes(30);
|
|
|
|
/**
|
|
* 获取模块配置(门店 → 租户 → 默认)
|
|
*/
|
|
public Mono<ModuleConfig> getModuleConfig(Long tenantId, Long storeId, String moduleCode) {
|
|
String cacheKey = buildCacheKey(tenantId, storeId, moduleCode);
|
|
|
|
return redisTemplate.opsForValue().get(cacheKey)
|
|
.switchIfEmpty(Mono.defer(() -> loadModuleConfig(tenantId, storeId, moduleCode))
|
|
.flatMap(config -> redisTemplate.opsForValue()
|
|
.set(cacheKey, config, CACHE_TTL)
|
|
.thenReturn(config)))
|
|
.doOnSuccess(config -> log.debug("获取模块配置成功: tenantId={}, storeId={}, moduleCode={}",
|
|
tenantId, storeId, moduleCode))
|
|
.doOnError(e -> log.error("获取模块配置失败: tenantId={}, storeId={}, moduleCode={}",
|
|
tenantId, storeId, moduleCode, e));
|
|
}
|
|
|
|
/**
|
|
* 加载模块配置
|
|
*/
|
|
private Mono<ModuleConfig> loadModuleConfig(Long tenantId, Long storeId, String moduleCode) {
|
|
return tenantModuleConfigRepository
|
|
.findByTenantIdAndModuleCode(tenantId, moduleCode)
|
|
.switchIfEmpty(Mono.just(getDefaultModuleConfig(moduleCode)))
|
|
.flatMap(tenantConfig -> storeModuleConfigRepository
|
|
.findByStoreIdAndModuleCode(storeId, moduleCode)
|
|
.map(storeConfig -> configMerger.mergeConfig(tenantConfig, storeConfig))
|
|
.defaultIfEmpty(tenantConfig));
|
|
}
|
|
|
|
/**
|
|
* 构建缓存Key
|
|
*/
|
|
private String buildCacheKey(Long tenantId, Long storeId, String moduleCode) {
|
|
return String.format("%s%d:%d:%s", CACHE_PREFIX, tenantId, storeId, moduleCode);
|
|
}
|
|
|
|
/**
|
|
* 获取默认模块配置
|
|
*/
|
|
private ModuleConfig getDefaultModuleConfig(String moduleCode) {
|
|
return ModuleConfig.builder()
|
|
.moduleCode(moduleCode)
|
|
.enabled(false)
|
|
.configData(new HashMap<>())
|
|
.build();
|
|
}
|
|
}
|
|
```
|
|
|
|
**配置合并服务**
|
|
|
|
```java
|
|
@Service
|
|
public class ConfigMerger {
|
|
|
|
/**
|
|
* 合并配置
|
|
*/
|
|
public ModuleConfig mergeConfig(ModuleConfig tenantConfig, StoreModuleConfig storeConfig) {
|
|
if (storeConfig.getInheritMode() == 1) {
|
|
return tenantConfig;
|
|
}
|
|
|
|
if (storeConfig.getInheritMode() == 2) {
|
|
Map<String, Object> mergedData = new HashMap<>(tenantConfig.getConfigData());
|
|
mergedData.putAll(storeConfig.getConfigData());
|
|
|
|
return ModuleConfig.builder()
|
|
.moduleCode(tenantConfig.getModuleCode())
|
|
.enabled(storeConfig.isEnabled())
|
|
.configData(mergedData)
|
|
.build();
|
|
}
|
|
|
|
return ModuleConfig.builder()
|
|
.moduleCode(tenantConfig.getModuleCode())
|
|
.enabled(storeConfig.isEnabled())
|
|
.configData(storeConfig.getConfigData())
|
|
.build();
|
|
}
|
|
}
|
|
```
|
|
|
|
**订阅服务**
|
|
|
|
```java
|
|
@Service
|
|
@Slf4j
|
|
@RequiredArgsConstructor
|
|
public class SubscriptionService {
|
|
|
|
private final SubscriptionRecordRepository subscriptionRecordRepository;
|
|
private final TenantModuleConfigRepository tenantModuleConfigRepository;
|
|
private final PaymentService paymentService;
|
|
private final MessageService messageService;
|
|
|
|
/**
|
|
* 订阅模块
|
|
*/
|
|
@Transactional
|
|
public Mono<SubscriptionRecord> subscribe(SubscriptionRequest request) {
|
|
return validateSubscription(request)
|
|
.flatMap(v -> paymentService.createPayment(request))
|
|
.flatMap(payment -> createSubscriptionRecord(request))
|
|
.flatMap(record -> enableModule(request.getTenantId(), request.getModuleCode())
|
|
.thenReturn(record))
|
|
.flatMap(record -> messageService.sendSubscriptionNotification(record)
|
|
.thenReturn(record))
|
|
.doOnSuccess(record -> log.info("订阅成功: subscriptionNo={}", record.getSubscriptionNo()))
|
|
.doOnError(e -> log.error("订阅失败: {}", e.getMessage()));
|
|
}
|
|
|
|
/**
|
|
* 验证订阅
|
|
*/
|
|
private Mono<Void> validateSubscription(SubscriptionRequest request) {
|
|
return subscriptionRecordRepository
|
|
.existsActiveSubscription(request.getTenantId(), request.getModuleCode())
|
|
.flatMap(exists -> {
|
|
if (Boolean.TRUE.equals(exists)) {
|
|
return Mono.error(new BusinessException("该模块已订阅"));
|
|
}
|
|
return Mono.empty();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 创建订阅记录
|
|
*/
|
|
private Mono<SubscriptionRecord> createSubscriptionRecord(SubscriptionRequest request) {
|
|
SubscriptionRecord record = SubscriptionRecord.builder()
|
|
.tenantId(request.getTenantId())
|
|
.subscriptionNo(generateSubscriptionNo(request.getTenantId()))
|
|
.moduleCode(request.getModuleCode())
|
|
.billingCycle(request.getBillingCycle())
|
|
.amount(request.getAmount())
|
|
.discountAmount(request.getDiscountAmount())
|
|
.actualAmount(request.getActualAmount())
|
|
.startDate(LocalDate.now())
|
|
.endDate(calculateEndDate(request.getBillingCycle()))
|
|
.status(1)
|
|
.build();
|
|
|
|
return subscriptionRecordRepository.save(record);
|
|
}
|
|
|
|
/**
|
|
* 计算结束日期
|
|
*/
|
|
private LocalDate calculateEndDate(Integer billingCycle) {
|
|
switch (billingCycle) {
|
|
case 1:
|
|
return LocalDate.now().plusMonths(1);
|
|
case 2:
|
|
return LocalDate.now().plusMonths(3);
|
|
case 3:
|
|
return LocalDate.now().plusMonths(6);
|
|
case 4:
|
|
return LocalDate.now().plusYears(1).plusMonths(1);
|
|
default:
|
|
return LocalDate.now().plusMonths(1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 启用模块
|
|
*/
|
|
private Mono<Void> enableModule(Long tenantId, String moduleCode) {
|
|
return tenantModuleConfigRepository
|
|
.findByTenantIdAndModuleCode(tenantId, moduleCode)
|
|
.switchIfEmpty(Mono.just(new TenantModuleConfig()))
|
|
.flatMap(config -> {
|
|
config.setTenantId(tenantId);
|
|
config.setModuleCode(moduleCode);
|
|
config.setEnabled(true);
|
|
config.setConfigData(new HashMap<>());
|
|
config.setVersion(config.getVersion() + 1);
|
|
|
|
return tenantModuleConfigRepository.save(config);
|
|
})
|
|
.then();
|
|
}
|
|
|
|
/**
|
|
* 生成订阅号
|
|
*/
|
|
private String generateSubscriptionNo(Long tenantId) {
|
|
String prefix = "S" + tenantId;
|
|
String timestamp = String.valueOf(System.currentTimeMillis());
|
|
String random = String.valueOf(new Random().nextInt(1000));
|
|
return prefix + timestamp.substring(timestamp.length() - 8) + random;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 三、业务扩展类模块设计
|
|
|
|
### 3.1 私教管理模块
|
|
|
|
#### 3.1.1 数据模型设计
|
|
|
|
**私教课程表 (private_class)**
|
|
|
|
```sql
|
|
CREATE TABLE private_class (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
tenant_id BIGINT NOT NULL,
|
|
store_id BIGINT NOT NULL,
|
|
coach_id BIGINT NOT NULL,
|
|
member_id BIGINT,
|
|
class_date DATE NOT NULL,
|
|
start_time TIME NOT NULL,
|
|
end_time TIME NOT NULL,
|
|
duration INT NOT NULL,
|
|
price DECIMAL(10,2) NOT NULL,
|
|
status SMALLINT DEFAULT 1,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
updated_at TIMESTAMP DEFAULT NOW(),
|
|
created_by BIGINT,
|
|
updated_by BIGINT,
|
|
deleted_at TIMESTAMP DEFAULT NULL,
|
|
|
|
CONSTRAINT fk_private_class_coach FOREIGN KEY (coach_id) REFERENCES coach(id),
|
|
CONSTRAINT fk_private_class_member FOREIGN KEY (member_id) REFERENCES member(id)
|
|
);
|
|
```
|
|
|
|
#### 3.1.2 核心服务设计
|
|
|
|
**私教预约服务**
|
|
|
|
```java
|
|
@Service
|
|
@Slf4j
|
|
@RequiredArgsConstructor
|
|
public class PrivateClassBookingService {
|
|
|
|
private final PrivateClassRepository privateClassRepository;
|
|
private final CoachRepository coachRepository;
|
|
private final BenefitService benefitService;
|
|
private final ReactiveRedisTemplate<String, String> redisTemplate;
|
|
private final DistributedLockService distributedLockService;
|
|
|
|
private static final String LOCK_PREFIX = "lock:private_class:";
|
|
private static final Duration LOCK_TTL = Duration.ofSeconds(30);
|
|
|
|
/**
|
|
* 预约私教
|
|
*/
|
|
@Transactional
|
|
public Mono<PrivateClass> book(PrivateClassBookingRequest request) {
|
|
String lockKey = buildLockKey(request.getCoachId(), request.getClassDate(), request.getStartTime());
|
|
|
|
return distributedLockService.acquireLock(lockKey, LOCK_TTL)
|
|
.flatMap(lock -> validateBookingTime(request.getClassDate(), request.getStartTime())
|
|
.flatMap(v -> validateCoachAvailability(request.getCoachId(), request.getClassDate(),
|
|
request.getStartTime(), request.getEndTime()))
|
|
.flatMap(v -> validateMemberBenefit(request.getMemberId()))
|
|
.flatMap(v -> coachRepository.findById(request.getCoachId())
|
|
.switchIfEmpty(Mono.error(new BusinessException("教练不存在"))))
|
|
.flatMap(coach -> createPrivateClass(coach, request))
|
|
.flatMap(privateClass -> benefitService.deductBenefit(request.getMemberId(), privateClass.getPrice())
|
|
.thenReturn(privateClass))
|
|
.doFinally(signal -> distributedLockService.releaseLock(lockKey)))
|
|
.doOnSuccess(privateClass -> log.info("私教预约成功: privateClassId={}", privateClass.getId()))
|
|
.doOnError(e -> log.error("私教预约失败: {}", e.getMessage()));
|
|
}
|
|
|
|
/**
|
|
* 验证预约时间
|
|
*/
|
|
private Mono<Void> validateBookingTime(LocalDate classDate, LocalTime startTime) {
|
|
LocalDateTime classDateTime = LocalDateTime.of(classDate, startTime);
|
|
if (classDateTime.isBefore(LocalDateTime.now().plusHours(24))) {
|
|
return Mono.error(new BusinessException("预约时间需提前至少24小时"));
|
|
}
|
|
return Mono.empty();
|
|
}
|
|
|
|
/**
|
|
* 验证教练可用性
|
|
*/
|
|
private Mono<Void> validateCoachAvailability(Long coachId, LocalDate classDate,
|
|
LocalTime startTime, LocalTime endTime) {
|
|
return privateClassRepository
|
|
.isCoachAvailable(coachId, classDate, startTime, endTime)
|
|
.flatMap(isAvailable -> {
|
|
if (!Boolean.TRUE.equals(isAvailable)) {
|
|
return Mono.error(new BusinessException("教练时间冲突"));
|
|
}
|
|
return Mono.empty();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 验证会员权益
|
|
*/
|
|
private Mono<Void> validateMemberBenefit(Long memberId) {
|
|
return benefitService.hasBenefit(memberId)
|
|
.flatMap(hasBenefit -> {
|
|
if (!Boolean.TRUE.equals(hasBenefit)) {
|
|
return Mono.error(new BusinessException("会员权益不足"));
|
|
}
|
|
return Mono.empty();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 创建私教课程
|
|
*/
|
|
private Mono<PrivateClass> createPrivateClass(Coach coach, PrivateClassBookingRequest request) {
|
|
PrivateClass privateClass = PrivateClass.builder()
|
|
.tenantId(coach.getTenantId())
|
|
.storeId(coach.getStoreId())
|
|
.coachId(coach.getId())
|
|
.memberId(request.getMemberId())
|
|
.classDate(request.getClassDate())
|
|
.startTime(request.getStartTime())
|
|
.endTime(request.getEndTime())
|
|
.duration(request.getDuration())
|
|
.price(request.getPrice())
|
|
.status(1)
|
|
.build();
|
|
|
|
return privateClassRepository.save(privateClass);
|
|
}
|
|
|
|
/**
|
|
* 构建锁Key
|
|
*/
|
|
private String buildLockKey(Long coachId, LocalDate classDate, LocalTime startTime) {
|
|
return String.format("%s%d:%s:%s", LOCK_PREFIX, coachId, classDate, startTime);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3.2 场地预约模块
|
|
|
|
#### 3.2.1 数据模型设计
|
|
|
|
**场地表 (venue)**
|
|
|
|
```sql
|
|
CREATE TABLE venue (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
tenant_id BIGINT NOT NULL,
|
|
store_id BIGINT NOT NULL,
|
|
name VARCHAR(128) NOT NULL,
|
|
type VARCHAR(64) NOT NULL,
|
|
capacity INT,
|
|
area DECIMAL(10,2),
|
|
equipment VARCHAR(256),
|
|
images JSONB,
|
|
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_venue_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id),
|
|
CONSTRAINT fk_venue_store FOREIGN KEY (store_id) REFERENCES store(id)
|
|
);
|
|
```
|
|
|
|
**场地预约表 (venue_booking)**
|
|
|
|
```sql
|
|
CREATE TABLE venue_booking (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
tenant_id BIGINT NOT NULL,
|
|
store_id BIGINT NOT NULL,
|
|
venue_id BIGINT NOT NULL,
|
|
member_id BIGINT NOT NULL,
|
|
booking_no VARCHAR(32) NOT NULL,
|
|
booking_date DATE NOT NULL,
|
|
start_time TIME NOT NULL,
|
|
end_time TIME NOT NULL,
|
|
duration INT NOT NULL,
|
|
status SMALLINT DEFAULT 1,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
updated_at TIMESTAMP DEFAULT NOW(),
|
|
created_by BIGINT,
|
|
updated_by BIGINT,
|
|
deleted_at TIMESTAMP DEFAULT NULL,
|
|
|
|
CONSTRAINT uk_venue_booking_no UNIQUE (booking_no),
|
|
CONSTRAINT fk_venue_booking_venue FOREIGN KEY (venue_id) REFERENCES venue(id),
|
|
CONSTRAINT fk_venue_booking_member FOREIGN KEY (member_id) REFERENCES member(id)
|
|
);
|
|
```
|
|
|
|
### 3.3 线上课程模块
|
|
|
|
#### 3.3.1 数据模型设计
|
|
|
|
**线上课程表 (online_course)**
|
|
|
|
```sql
|
|
CREATE TABLE online_course (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
tenant_id BIGINT NOT NULL,
|
|
name VARCHAR(128) NOT NULL,
|
|
code VARCHAR(32),
|
|
category VARCHAR(64),
|
|
description TEXT,
|
|
cover_image VARCHAR(512),
|
|
video_url VARCHAR(512),
|
|
duration INT NOT NULL,
|
|
difficulty SMALLINT DEFAULT 1,
|
|
calories INT,
|
|
equipment VARCHAR(256),
|
|
benefits JSONB,
|
|
price DECIMAL(10,2),
|
|
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_online_course_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id)
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
## 四、营销增长类模块设计
|
|
|
|
### 4.1 营销活动模块
|
|
|
|
#### 4.1.1 数据模型设计
|
|
|
|
**营销活动表 (marketing_activity)**
|
|
|
|
```sql
|
|
CREATE TABLE marketing_activity (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
tenant_id BIGINT NOT NULL,
|
|
name VARCHAR(128) NOT NULL,
|
|
type SMALLINT NOT NULL,
|
|
description TEXT,
|
|
start_date DATE NOT NULL,
|
|
end_date DATE NOT NULL,
|
|
rules JSONB NOT NULL,
|
|
rewards JSONB NOT NULL,
|
|
status SMALLINT DEFAULT 1,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
updated_at TIMESTAMP DEFAULT NOW(),
|
|
created_by BIGINT,
|
|
updated_by BIGINT,
|
|
deleted_at TIMESTAMP DEFAULT NULL,
|
|
|
|
CONSTRAINT fk_marketing_activity_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id)
|
|
);
|
|
```
|
|
|
|
**营销活动参与记录表 (marketing_activity_participant)**
|
|
|
|
```sql
|
|
CREATE TABLE marketing_activity_participant (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
tenant_id BIGINT NOT NULL,
|
|
activity_id BIGINT NOT NULL,
|
|
member_id BIGINT NOT NULL,
|
|
participated_at TIMESTAMP NOT NULL,
|
|
reward_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_activity_participant UNIQUE (activity_id, member_id),
|
|
CONSTRAINT fk_activity_participant_activity FOREIGN KEY (activity_id) REFERENCES marketing_activity(id),
|
|
CONSTRAINT fk_activity_participant_member FOREIGN KEY (member_id) REFERENCES member(id)
|
|
);
|
|
```
|
|
|
|
#### 4.1.2 核心服务设计
|
|
|
|
**营销活动服务**
|
|
|
|
```java
|
|
@Service
|
|
public class MarketingActivityService {
|
|
|
|
@Autowired
|
|
private MarketingActivityRepository marketingActivityRepository;
|
|
|
|
@Autowired
|
|
private MarketingActivityParticipantRepository participantRepository;
|
|
|
|
@Autowired
|
|
private BenefitService benefitService;
|
|
|
|
/**
|
|
* 创建营销活动
|
|
*/
|
|
@Transactional
|
|
public MarketingActivity createActivity(MarketingActivityCreateRequest request) {
|
|
validateActivityTime(request.getStartDate(), request.getEndDate());
|
|
|
|
MarketingActivity activity = new MarketingActivity();
|
|
activity.setTenantId(request.getTenantId());
|
|
activity.setName(request.getName());
|
|
activity.setType(request.getType());
|
|
activity.setDescription(request.getDescription());
|
|
activity.setStartDate(request.getStartDate());
|
|
activity.setEndDate(request.getEndDate());
|
|
activity.setRules(request.getRules());
|
|
activity.setRewards(request.getRewards());
|
|
activity.setStatus(1);
|
|
|
|
return marketingActivityRepository.save(activity);
|
|
}
|
|
|
|
/**
|
|
* 参与营销活动
|
|
*/
|
|
@Transactional
|
|
public MarketingActivityParticipant participate(Long activityId, Long memberId) {
|
|
MarketingActivity activity = marketingActivityRepository.findById(activityId)
|
|
.orElseThrow(() -> new BusinessException("活动不存在"));
|
|
|
|
validateActivityStatus(activity);
|
|
validateActivityTime(activity);
|
|
validateParticipant(activityId, memberId);
|
|
|
|
MarketingActivityParticipant participant = new MarketingActivityParticipant();
|
|
participant.setTenantId(activity.getTenantId());
|
|
participant.setActivityId(activityId);
|
|
participant.setMemberId(memberId);
|
|
participant.setParticipatedAt(LocalDateTime.now());
|
|
participant.setRewardStatus(1);
|
|
|
|
participant = participantRepository.save(participant);
|
|
|
|
grantReward(activity, memberId);
|
|
|
|
return participant;
|
|
}
|
|
|
|
/**
|
|
* 验证活动时间
|
|
*/
|
|
private void validateActivityTime(LocalDate startDate, LocalDate endDate) {
|
|
if (startDate.isAfter(endDate)) {
|
|
throw new BusinessException("活动开始时间不能晚于结束时间");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 验证活动状态
|
|
*/
|
|
private void validateActivityStatus(MarketingActivity activity) {
|
|
if (activity.getStatus() != 1) {
|
|
throw new BusinessException("活动未开始或已结束");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 验证活动时间
|
|
*/
|
|
private void validateActivityTime(MarketingActivity activity) {
|
|
LocalDate now = LocalDate.now();
|
|
if (now.isBefore(activity.getStartDate()) || now.isAfter(activity.getEndDate())) {
|
|
throw new BusinessException("活动未开始或已结束");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 验证参与者
|
|
*/
|
|
private void validateParticipant(Long activityId, Long memberId) {
|
|
if (participantRepository.existsByActivityIdAndMemberId(activityId, memberId)) {
|
|
throw new BusinessException("已参与过该活动");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 发放奖励
|
|
*/
|
|
private void grantReward(MarketingActivity activity, Long memberId) {
|
|
Map<String, Object> rewards = activity.getRewards();
|
|
String rewardType = (String) rewards.get("type");
|
|
Object rewardValue = rewards.get("value");
|
|
|
|
switch (rewardType) {
|
|
case "card":
|
|
benefitService.grantCard(memberId, (String) rewardValue);
|
|
break;
|
|
case "points":
|
|
benefitService.grantPoints(memberId, (Integer) rewardValue);
|
|
break;
|
|
case "coupon":
|
|
benefitService.grantCoupon(memberId, (String) rewardValue);
|
|
break;
|
|
default:
|
|
throw new BusinessException("不支持的奖励类型");
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 五、数据智能类模块设计
|
|
|
|
### 5.1 高级数据分析模块
|
|
|
|
#### 5.1.1 核心服务设计
|
|
|
|
**高级数据分析服务**
|
|
|
|
```java
|
|
@Service
|
|
public class AdvancedDataAnalysisService {
|
|
|
|
@Autowired
|
|
private MemberRepository memberRepository;
|
|
|
|
@Autowired
|
|
private BookingRecordRepository bookingRecordRepository;
|
|
|
|
@Autowired
|
|
private CheckInRecordRepository checkInRecordRepository;
|
|
|
|
/**
|
|
* 会员留存分析
|
|
*/
|
|
public MemberRetentionAnalysis analyzeMemberRetention(Long tenantId, Long storeId, LocalDate startDate, LocalDate endDate) {
|
|
List<Member> members = memberRepository.findByTenantIdAndStoreIdAndCreatedAtBetween(
|
|
tenantId, storeId, startDate.atStartOfDay(), endDate.atTime(23, 59, 59));
|
|
|
|
Map<Integer, Long> retentionData = new HashMap<>();
|
|
|
|
for (Member member : members) {
|
|
int retentionDays = calculateRetentionDays(member.getCreatedAt(), LocalDate.now());
|
|
retentionData.put(retentionDays, retentionData.getOrDefault(retentionDays, 0L) + 1);
|
|
}
|
|
|
|
MemberRetentionAnalysis analysis = new MemberRetentionAnalysis();
|
|
analysis.setTotalMembers(members.size());
|
|
analysis.setRetentionData(retentionData);
|
|
|
|
return analysis;
|
|
}
|
|
|
|
/**
|
|
* 计算留存天数
|
|
*/
|
|
private int calculateRetentionDays(LocalDateTime createdAt, LocalDate now) {
|
|
return (int) ChronoUnit.DAYS.between(createdAt.toLocalDate(), now);
|
|
}
|
|
|
|
/**
|
|
* 预约转化率分析
|
|
*/
|
|
public BookingConversionAnalysis analyzeBookingConversion(Long tenantId, Long storeId, LocalDate startDate, LocalDate endDate) {
|
|
long totalBookings = bookingRecordRepository.countByTenantIdAndStoreIdAndBookedAtBetween(
|
|
tenantId, storeId, startDate.atStartOfDay(), endDate.atTime(23, 59, 59));
|
|
|
|
long totalCheckIns = checkInRecordRepository.countByTenantIdAndStoreIdAndCheckInAtBetween(
|
|
tenantId, storeId, startDate.atStartOfDay(), endDate.atTime(23, 59, 59));
|
|
|
|
BookingConversionAnalysis analysis = new BookingConversionAnalysis();
|
|
analysis.setTotalBookings(totalBookings);
|
|
analysis.setTotalCheckIns(totalCheckIns);
|
|
analysis.setConversionRate(totalBookings > 0 ? (double) totalCheckIns / totalBookings : 0.0);
|
|
|
|
return analysis;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 六、营销分析与预测模块设计
|
|
|
|
### 6.1 模块概述
|
|
|
|
营销分析与预测模块是付费订阅版的高级功能模块,负责:
|
|
|
|
- 营销精算模型预测促销策略
|
|
- 多维度自定义促销活动
|
|
- 促销活动效果预测
|
|
|
|
### 6.2 数据模型设计
|
|
|
|
**营销预测表 (marketing_prediction)**
|
|
|
|
```sql
|
|
CREATE TABLE marketing_prediction (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
tenant_id BIGINT NOT NULL,
|
|
prediction_no VARCHAR(32) NOT NULL,
|
|
activity_type SMALLINT NOT NULL,
|
|
parameters JSONB NOT NULL,
|
|
predicted_data JSONB NOT NULL,
|
|
accuracy DECIMAL(5,4),
|
|
model_version VARCHAR(32),
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
updated_at TIMESTAMP DEFAULT NOW(),
|
|
created_by BIGINT,
|
|
updated_by BIGINT,
|
|
deleted_at TIMESTAMP DEFAULT NULL,
|
|
|
|
CONSTRAINT uk_marketing_prediction_no UNIQUE (prediction_no),
|
|
CONSTRAINT fk_marketing_prediction_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id)
|
|
);
|
|
```
|
|
|
|
**促销活动表 (promotion_activity)**
|
|
|
|
```sql
|
|
CREATE TABLE promotion_activity (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
tenant_id BIGINT NOT NULL,
|
|
name VARCHAR(128) NOT NULL,
|
|
type SMALLINT NOT NULL,
|
|
description TEXT,
|
|
start_date DATE NOT NULL,
|
|
end_date DATE NOT NULL,
|
|
target_audience JSONB NOT NULL,
|
|
discount_rule JSONB NOT NULL,
|
|
budget DECIMAL(10,2),
|
|
predicted_data JSONB,
|
|
actual_data JSONB,
|
|
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_promotion_activity_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id)
|
|
);
|
|
```
|
|
|
|
### 6.3 核心服务设计
|
|
|
|
**营销精算模型服务**
|
|
|
|
```java
|
|
@Service
|
|
public class MarketingActuarialModelService {
|
|
|
|
@Autowired
|
|
private MarketingPredictionRepository marketingPredictionRepository;
|
|
|
|
@Autowired
|
|
private MemberRepository memberRepository;
|
|
|
|
@Autowired
|
|
private BookingRecordRepository bookingRecordRepository;
|
|
|
|
@Autowired
|
|
private CheckInRecordRepository checkInRecordRepository;
|
|
|
|
/**
|
|
* 预测促销策略
|
|
*/
|
|
@Transactional
|
|
public MarketingPrediction predictPromotionStrategy(PromotionPredictionRequest request) {
|
|
Map<String, Object> historicalData = collectHistoricalData(request.getTenantId(), request.getStoreId());
|
|
|
|
Map<String, Object> predictedData = runPredictionModel(historicalData, request.getParameters());
|
|
|
|
MarketingPrediction prediction = new MarketingPrediction();
|
|
prediction.setTenantId(request.getTenantId());
|
|
prediction.setPredictionNo(generatePredictionNo(request.getTenantId()));
|
|
prediction.setActivityType(request.getActivityType());
|
|
prediction.setParameters(request.getParameters());
|
|
prediction.setPredictedData(predictedData);
|
|
prediction.setAccuracy(calculateAccuracy(historicalData, predictedData));
|
|
prediction.setModelVersion("v1.0");
|
|
|
|
return marketingPredictionRepository.save(prediction);
|
|
}
|
|
|
|
/**
|
|
* 收集历史数据
|
|
*/
|
|
private Map<String, Object> collectHistoricalData(Long tenantId, Long storeId) {
|
|
Map<String, Object> historicalData = new HashMap<>();
|
|
|
|
LocalDate endDate = LocalDate.now();
|
|
LocalDate startDate = endDate.minusMonths(6);
|
|
|
|
long totalMembers = memberRepository.countByTenantIdAndStoreIdAndCreatedAtBetween(
|
|
tenantId, storeId, startDate.atStartOfDay(), endDate.atTime(23, 59, 59));
|
|
|
|
long totalBookings = bookingRecordRepository.countByTenantIdAndStoreIdAndBookedAtBetween(
|
|
tenantId, storeId, startDate.atStartOfDay(), endDate.atTime(23, 59, 59));
|
|
|
|
long totalCheckIns = checkInRecordRepository.countByTenantIdAndStoreIdAndCheckInAtBetween(
|
|
tenantId, storeId, startDate.atStartOfDay(), endDate.atTime(23, 59, 59));
|
|
|
|
historicalData.put("totalMembers", totalMembers);
|
|
historicalData.put("totalBookings", totalBookings);
|
|
historicalData.put("totalCheckIns", totalCheckIns);
|
|
|
|
return historicalData;
|
|
}
|
|
|
|
/**
|
|
* 运行预测模型
|
|
*/
|
|
private Map<String, Object> runPredictionModel(Map<String, Object> historicalData, Map<String, Object> parameters) {
|
|
Map<String, Object> predictedData = new HashMap<>();
|
|
|
|
Long totalMembers = (Long) historicalData.get("totalMembers");
|
|
Long totalBookings = (Long) historicalData.get("totalBookings");
|
|
Long totalCheckIns = (Long) historicalData.get("totalCheckIns");
|
|
|
|
Double discountRate = (Double) parameters.get("discountRate");
|
|
Integer durationDays = (Integer) parameters.get("durationDays");
|
|
|
|
double predictedNewMembers = totalMembers * 0.1 * (1 + discountRate * 2);
|
|
double predictedBookings = totalBookings * 1.2 * (1 + discountRate * 1.5);
|
|
double predictedCheckIns = totalCheckIns * 1.15 * (1 + discountRate * 1.3);
|
|
|
|
predictedData.put("predictedNewMembers", Math.round(predictedNewMembers));
|
|
predictedData.put("predictedBookings", Math.round(predictedBookings));
|
|
predictedData.put("predictedCheckIns", Math.round(predictedCheckIns));
|
|
predictedData.put("predictedRevenue", predictedBookings * 100 * (1 - discountRate));
|
|
|
|
return predictedData;
|
|
}
|
|
|
|
/**
|
|
* 计算准确率
|
|
*/
|
|
private BigDecimal calculateAccuracy(Map<String, Object> historicalData, Map<String, Object> predictedData) {
|
|
return BigDecimal.valueOf(0.85);
|
|
}
|
|
|
|
/**
|
|
* 生成预测号
|
|
*/
|
|
private String generatePredictionNo(Long tenantId) {
|
|
String prefix = "P" + tenantId;
|
|
String timestamp = String.valueOf(System.currentTimeMillis());
|
|
String random = String.valueOf(new Random().nextInt(1000));
|
|
return prefix + timestamp.substring(timestamp.length() - 8) + random;
|
|
}
|
|
}
|
|
```
|
|
|
|
**促销活动服务**
|
|
|
|
```java
|
|
@Service
|
|
public class PromotionActivityService {
|
|
|
|
@Autowired
|
|
private PromotionActivityRepository promotionActivityRepository;
|
|
|
|
@Autowired
|
|
private MarketingActuarialModelService marketingActuarialModelService;
|
|
|
|
/**
|
|
* 创建促销活动
|
|
*/
|
|
@Transactional
|
|
public PromotionActivity createActivity(PromotionActivityCreateRequest request) {
|
|
PromotionActivity activity = new PromotionActivity();
|
|
activity.setTenantId(request.getTenantId());
|
|
activity.setName(request.getName());
|
|
activity.setType(request.getType());
|
|
activity.setDescription(request.getDescription());
|
|
activity.setStartDate(request.getStartDate());
|
|
activity.setEndDate(request.getEndDate());
|
|
activity.setTargetAudience(request.getTargetAudience());
|
|
activity.setDiscountRule(request.getDiscountRule());
|
|
activity.setBudget(request.getBudget());
|
|
activity.setStatus(1);
|
|
|
|
activity = promotionActivityRepository.save(activity);
|
|
|
|
if (request.isPredictEffect()) {
|
|
predictActivityEffect(activity);
|
|
}
|
|
|
|
return activity;
|
|
}
|
|
|
|
/**
|
|
* 预测活动效果
|
|
*/
|
|
private void predictActivityEffect(PromotionActivity activity) {
|
|
PromotionPredictionRequest predictionRequest = new PromotionPredictionRequest();
|
|
predictionRequest.setTenantId(activity.getTenantId());
|
|
predictionRequest.setActivityType(activity.getType());
|
|
predictionRequest.setParameters(activity.getDiscountRule());
|
|
|
|
MarketingPrediction prediction = marketingActuarialModelService.predictPromotionStrategy(predictionRequest);
|
|
|
|
activity.setPredictedData(prediction.getPredictedData());
|
|
promotionActivityRepository.save(activity);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 七、缓存策略
|
|
|
|
### 7.1 缓存设计
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────────────┐
|
|
│ 缓存策略 │
|
|
├─────────────────────────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
|
│ │ 本地缓存 (Caffeine) │ │
|
|
│ ├─────────────────────────────────────────────────────────────────┤ │
|
|
│ │ • 会员信息缓存 (TTL: 30分钟) │ │
|
|
│ │ • 会员卡缓存 (TTL: 30分钟) │ │
|
|
│ │ • 课程信息缓存 (TTL: 1小时) │ │
|
|
│ │ • 配置信息缓存 (TTL: 1小时) │ │
|
|
│ └─────────────────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
|
│ │ 分布式缓存 (Redis) │ │
|
|
│ ├─────────────────────────────────────────────────────────────────┤ │
|
|
│ │ • 验证码缓存 (TTL: 5分钟) │ │
|
|
│ │ • 令牌缓存 (TTL: 24小时) │ │
|
|
│ │ • 限流计数器 (TTL: 1分钟) │ │
|
|
│ │ • 预测结果缓存 (TTL: 1小时) │ │
|
|
│ └─────────────────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
└─────────────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## 八、API设计
|
|
|
|
### 8.1 订阅与配置模块API
|
|
|
|
#### 8.1.1 订阅模块
|
|
|
|
```
|
|
POST /api/v1/subscriptions/subscribe
|
|
|
|
Request:
|
|
{
|
|
"tenantId": 1,
|
|
"moduleCode": "private_class",
|
|
"billingCycle": 1,
|
|
"amount": 299.00,
|
|
"discountAmount": 0.00,
|
|
"actualAmount": 299.00
|
|
}
|
|
|
|
Response:
|
|
{
|
|
"code": 200,
|
|
"message": "订阅成功",
|
|
"data": {
|
|
"id": 1,
|
|
"subscriptionNo": "S10000000000000001",
|
|
"moduleCode": "private_class",
|
|
"moduleName": "私教管理模块",
|
|
"startDate": "2026-03-04",
|
|
"endDate": "2026-04-03",
|
|
"status": 1
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 8.1.2 配置查询
|
|
|
|
```
|
|
GET /api/v1/configs/module?tenantId=1&storeId=1&moduleCode=private_class
|
|
|
|
Response:
|
|
{
|
|
"code": 200,
|
|
"message": "查询成功",
|
|
"data": {
|
|
"moduleCode": "private_class",
|
|
"enabled": true,
|
|
"configData": {
|
|
"maxBookingDays": 7,
|
|
"cancelHours": 24
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### 8.2 营销分析与预测模块API
|
|
|
|
#### 8.2.1 预测促销策略
|
|
|
|
```
|
|
POST /api/v1/marketing/predict
|
|
|
|
Request:
|
|
{
|
|
"tenantId": 1,
|
|
"storeId": 1,
|
|
"activityType": 1,
|
|
"parameters": {
|
|
"discountRate": 0.2,
|
|
"durationDays": 30,
|
|
"targetAudience": "new_members"
|
|
}
|
|
}
|
|
|
|
Response:
|
|
{
|
|
"code": 200,
|
|
"message": "预测成功",
|
|
"data": {
|
|
"id": 1,
|
|
"predictionNo": "P10000000000000001",
|
|
"activityType": 1,
|
|
"predictedData": {
|
|
"predictedNewMembers": 120,
|
|
"predictedBookings": 600,
|
|
"predictedCheckIns": 920,
|
|
"predictedRevenue": 48000.00
|
|
},
|
|
"accuracy": 0.85
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 九、测试用例
|
|
|
|
### 9.1 订阅与配置模块测试用例
|
|
|
|
#### 9.1.1 订阅模块测试
|
|
|
|
| 测试用例 | 输入 | 预期输出 |
|
|
|---------|------|---------|
|
|
| 正常订阅 | 租户ID、模块代码、计费周期 | 订阅成功 |
|
|
| 重复订阅 | 已订阅的模块 | 提示该模块已订阅 |
|
|
| 支付失败 | 支付失败 | 提示支付失败 |
|
|
|
|
#### 9.1.2 配置查询测试
|
|
|
|
| 测试用例 | 输入 | 预期输出 |
|
|
|---------|------|---------|
|
|
| 查询租户配置 | 租户ID、模块代码 | 返回租户配置 |
|
|
| 查询门店配置 | 租户ID、门店ID、模块代码 | 返回合并后的配置 |
|
|
| 查询默认配置 | 不存在的配置 | 返回默认配置 |
|
|
|
|
### 9.2 营销分析与预测模块测试用例
|
|
|
|
#### 9.2.1 预测促销策略测试
|
|
|
|
| 测试用例 | 输入 | 预期输出 |
|
|
|---------|------|---------|
|
|
| 正常预测 | 租户ID、活动类型、参数 | 预测成功 |
|
|
| 历史数据不足 | 新租户 | 提示历史数据不足 |
|
|
| 参数无效 | 无效的参数 | 提示参数无效 |
|
|
|
|
---
|
|
|
|
## 十、部署与运维
|
|
|
|
### 10.1 部署架构
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────────────┐
|
|
│ 部署架构 │
|
|
├─────────────────────────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
|
│ │ 负载均衡 (Nginx) │ │
|
|
│ └─────────────────────────────────────────────────────────────────┘ │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
|
│ │ 应用服务器 (Kubernetes) │ │
|
|
│ ├─────────────────────────────────────────────────────────────────┤ │
|
|
│ │ • Pod 1 • Pod 2 • Pod 3 • Pod 4 • Pod 5 │ │
|
|
│ └─────────────────────────────────────────────────────────────────┘ │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
|
│ │ 数据库 (PostgreSQL) │ │
|
|
│ ├─────────────────────────────────────────────────────────────────┤ │
|
|
│ │ • 主库 • 从库 • 从库 │ │
|
|
│ └─────────────────────────────────────────────────────────────────┘ │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
|
│ │ 缓存 (Redis) │ │
|
|
│ ├─────────────────────────────────────────────────────────────────┤ │
|
|
│ │ • 主节点 • 从节点 • 从节点 │ │
|
|
│ └─────────────────────────────────────────────────────────────────┘ │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
|
│ │ 搜索引擎 (Elasticsearch) │ │
|
|
│ ├─────────────────────────────────────────────────────────────────┤ │
|
|
│ │ • 节点1 • 节点2 • 节点3 │ │
|
|
│ └─────────────────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
└─────────────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### 10.2 监控指标
|
|
|
|
| 指标类型 | 指标名称 | 阈值 |
|
|
|---------|---------|------|
|
|
| 系统指标 | CPU使用率 | ≤ 80% |
|
|
| 系统指标 | 内存使用率 | ≤ 80% |
|
|
| 系统指标 | 磁盘使用率 | ≤ 80% |
|
|
| 应用指标 | API响应时间 | ≤ 500ms |
|
|
| 应用指标 | 错误率 | ≤ 1% |
|
|
| 应用指标 | 并发数 | ≤ 500 |
|
|
| 业务指标 | 订阅成功率 | ≥ 98% |
|
|
| 业务指标 | 预测准确率 | ≥ 75% |
|
|
|
|
---
|
|
|
|
## 十一、附录
|
|
|
|
### 11.1 术语定义
|
|
|
|
| 术语 | 定义 |
|
|
|------|------|
|
|
| 订阅模块 | 按需订阅的增值功能模块 |
|
|
| 配置继承 | 门店配置继承租户配置的机制 |
|
|
| 私教管理 | 私教课程管理、私教预约、私教签到等功能 |
|
|
| 营销活动 | 吸引新会员和提升会员活跃度的活动 |
|
|
| 营销精算模型 | 基于历史数据预测促销策略的模型 |
|
|
| 促销活动效果预测 | 基于历史数据预测促销活动效果 |
|
|
|
|
### 11.2 参考文档
|
|
|
|
- 《健身房管理系统付费订阅版产品设计文档》 GYM-PRD-SUBSCRIPTION-001
|
|
- 《健身房管理系统付费订阅版业务概要设计文档》 GYM-HLD-SUBSCRIPTION-001
|
|
- 《健身房管理系统详细设计文档》 GYM-LLD-000
|
|
- 《订阅与配置模块详细设计文档》 GYM-LLD-004
|
|
- Spring Boot 3 官方文档
|
|
- R2DBC 规范文档
|
|
- PostgreSQL 官方文档
|