1895 lines
58 KiB
Markdown
1895 lines
58 KiB
Markdown
# 健身房管理系统付费订阅版技术实现详细设计文档(T-ILD)
|
||
|
||
> 文档编号: GYM-T-ILD-SUBSCRIPTION-001
|
||
> 版本: v1.0
|
||
> 日期: 2026-03-08
|
||
> 作者: 张翔
|
||
> 状态: 已发布
|
||
|
||
---
|
||
|
||
## 文档修订历史
|
||
|
||
| 版本 | 日期 | 作者 | 修订内容 |
|
||
| ---- | ---------- | ---- | ---------------------- |
|
||
| v1.0 | 2026-03-08 | 张翔 | 创建付费订阅版技术实现详细设计文档 |
|
||
|
||
---
|
||
|
||
## 参考文档
|
||
|
||
- 《健身房管理系统付费订阅版产品设计文档》 GYM-PRD-SUBSCRIPTION-001
|
||
- 《健身房管理系统付费订阅版业务概要设计文档》 GYM-B-HLD-SUBSCRIPTION-001
|
||
- 《健身房管理系统付费订阅版业务详细设计文档》 GYM-B-LLD-SUBSCRIPTION-001
|
||
- Spring Boot 3 官方文档
|
||
- R2DBC 规范文档
|
||
- PostgreSQL 官方文档
|
||
|
||
---
|
||
|
||
## 一、系统架构设计
|
||
|
||
### 1.0 付费订阅版性能与架构特点
|
||
|
||
#### 1.0.1 性能目标
|
||
|
||
| 指标类型 | 指标项 | 目标值 | 说明 |
|
||
|---------|--------|--------|------|
|
||
| **应用指标** | 并发数 | ≤ 500 | 付费订阅版支持500并发用户 |
|
||
| **应用指标** | API响应时间 | ≤ 500ms | 95%请求响应时间 |
|
||
| **应用指标** | 系统可用性 | ≥ 99.9% | 年度目标 |
|
||
| **数据库指标** | 连接池大小 | 100 | R2DBC连接池 |
|
||
| **数据库指标** | 查询响应时间 | ≤ 200ms | 95%查询响应时间 |
|
||
| **缓存指标** | 缓存命中率 | ≥ 85% | Redis缓存 |
|
||
| **缓存指标** | 缓存响应时间 | ≤ 10ms | Redis缓存 |
|
||
|
||
#### 1.0.2 架构特点
|
||
|
||
付费订阅版采用增强型架构设计,满足中大型健身房和连锁品牌的需求:
|
||
|
||
**1. 单体应用架构(可扩展为分布式)**
|
||
- 支持多门店管理
|
||
- 支持跨店数据同步
|
||
- 数据隔离通过租户ID和门店ID实现
|
||
|
||
**2. 增强资源配置**
|
||
- 数据库连接池:100个连接
|
||
- Redis缓存:4GB内存
|
||
- RabbitMQ队列:多队列集群模式
|
||
- Elasticsearch:3节点集群部署
|
||
|
||
**3. 扩展性增强**
|
||
- 支持多门店管理
|
||
- 支持分布式部署(可选)
|
||
- 支持高可用集群(可选)
|
||
- 并发用户数提升至500
|
||
|
||
#### 1.0.3 与基础版的差异
|
||
|
||
| 维度 | 基础版 | 付费订阅版 |
|
||
|------|--------|-----------|
|
||
| **并发用户数** | 100 | 500 |
|
||
| **数据库连接池** | 20 | 100 |
|
||
| **Redis内存** | 1GB | 4GB |
|
||
| **RabbitMQ队列** | 单队列 | 多队列集群 |
|
||
| **Elasticsearch** | 单节点 | 3节点集群 |
|
||
| **多门店支持** | 不支持 | 支持 |
|
||
| **分布式部署** | 不支持 | 支持 |
|
||
| **高可用集群** | 不支持 | 支持 |
|
||
|
||
---
|
||
|
||
### 1.1 总体架构
|
||
|
||
采用分层架构 + 模块化设计的单体应用:
|
||
|
||
```mermaid
|
||
flowchart TB
|
||
subgraph 付费订阅版单体应用架构
|
||
A[客户端层<br/>• 会员小程序 uniapp+Vue3<br/>• 教练端App uniapp+Vue3<br/>• 管理后台PC Vue3+Vite<br/>• 硬件设备 人脸/NFC]
|
||
B[Presentation Layer WebFlux<br/>• Controller<br/>• Router<br/>• Filter<br/>• Validator]
|
||
C[Application Layer 业务编排<br/>• Service<br/>• Facade<br/>• Orchestrator<br/>• 事务管理]
|
||
D[Domain Layer 领域模型<br/>• Entity<br/>• Value Object<br/>• Domain Service<br/>• Repository]
|
||
E[Infrastructure Layer 基础设施<br/>• Repository R2DBC<br/>• Cache Redis<br/>• Message RabbitMQ<br/>• Search Elasticsearch<br/>• File OSS<br/>• Distributed Lock]
|
||
F[外部服务层<br/>• PostgreSQL<br/>• Redis<br/>• RabbitMQ<br/>• Elasticsearch<br/>• 微信开放平台<br/>• 短信服务<br/>• 支付服务<br/>• OSS存储]
|
||
A --> B
|
||
B --> C
|
||
C --> D
|
||
D --> E
|
||
E --> F
|
||
end
|
||
```
|
||
|
||
---
|
||
|
||
## 二、订阅与配置模块设计
|
||
|
||
### 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("不支持的奖励类型");
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 4.2 智能获客工具模块
|
||
|
||
#### 4.2.1 数据模型设计
|
||
|
||
**获客活动表 (customer_acquisition)**
|
||
|
||
```sql
|
||
CREATE TABLE customer_acquisition (
|
||
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,
|
||
config 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_customer_acquisition_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id)
|
||
);
|
||
```
|
||
|
||
**推荐记录表 (referral_record)**
|
||
|
||
```sql
|
||
CREATE TABLE referral_record (
|
||
id BIGSERIAL PRIMARY KEY,
|
||
tenant_id BIGINT NOT NULL,
|
||
activity_id BIGINT,
|
||
referrer_id BIGINT NOT NULL,
|
||
referee_id BIGINT,
|
||
referral_code VARCHAR(32) NOT NULL,
|
||
status SMALLINT DEFAULT 1,
|
||
reward_status SMALLINT DEFAULT 1,
|
||
reward_amount DECIMAL(10, 2),
|
||
created_at TIMESTAMP DEFAULT NOW(),
|
||
updated_at TIMESTAMP DEFAULT NOW(),
|
||
deleted_at TIMESTAMP DEFAULT NULL,
|
||
|
||
CONSTRAINT fk_referral_record_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id),
|
||
CONSTRAINT fk_referral_record_activity FOREIGN KEY (activity_id) REFERENCES customer_acquisition(id),
|
||
CONSTRAINT fk_referral_record_referrer FOREIGN KEY (referrer_id) REFERENCES member(id),
|
||
CONSTRAINT fk_referral_record_referee FOREIGN KEY (referee_id) REFERENCES member(id)
|
||
);
|
||
```
|
||
|
||
#### 4.2.2 核心服务设计
|
||
|
||
**智能获客服务**
|
||
|
||
```java
|
||
@Service
|
||
public class CustomerAcquisitionService {
|
||
|
||
@Autowired
|
||
private CustomerAcquisitionRepository customerAcquisitionRepository;
|
||
|
||
@Autowired
|
||
private ReferralRecordRepository referralRecordRepository;
|
||
|
||
@Autowired
|
||
private MemberRepository memberRepository;
|
||
|
||
@Autowired
|
||
private BenefitService benefitService;
|
||
|
||
/**
|
||
* 创建获客活动
|
||
*/
|
||
public CustomerAcquisition createAcquisitionActivity(Long tenantId, CustomerAcquisitionDTO dto) {
|
||
CustomerAcquisition activity = new CustomerAcquisition();
|
||
activity.setTenantId(tenantId);
|
||
activity.setName(dto.getName());
|
||
activity.setType(dto.getType());
|
||
activity.setDescription(dto.getDescription());
|
||
activity.setStartDate(dto.getStartDate());
|
||
activity.setEndDate(dto.getEndDate());
|
||
activity.setConfig(dto.getConfig());
|
||
activity.setStatus(1);
|
||
|
||
return customerAcquisitionRepository.save(activity);
|
||
}
|
||
|
||
/**
|
||
* 生成推荐码
|
||
*/
|
||
public String generateReferralCode(Long memberId, Long activityId) {
|
||
String referralCode = generateUniqueCode();
|
||
|
||
ReferralRecord record = new ReferralRecord();
|
||
record.setTenantId(getTenantIdByMemberId(memberId));
|
||
record.setActivityId(activityId);
|
||
record.setReferrerId(memberId);
|
||
record.setReferralCode(referralCode);
|
||
record.setStatus(1);
|
||
record.setRewardStatus(1);
|
||
|
||
referralRecordRepository.save(record);
|
||
|
||
return referralCode;
|
||
}
|
||
|
||
/**
|
||
* 处理推荐关系
|
||
*/
|
||
public void processReferral(Long memberId, String referralCode) {
|
||
ReferralRecord record = referralRecordRepository.findByReferralCodeAndStatus(referralCode, 1);
|
||
|
||
if (record == null) {
|
||
throw new BusinessException("推荐码无效");
|
||
}
|
||
|
||
record.setRefereeId(memberId);
|
||
record.setStatus(2);
|
||
|
||
referralRecordRepository.save(record);
|
||
|
||
CustomerAcquisition activity = customerAcquisitionRepository.findById(record.getActivityId()).orElse(null);
|
||
if (activity != null && activity.getConfig() != null) {
|
||
JSONObject config = activity.getConfig();
|
||
if (config.containsKey("rewardAmount")) {
|
||
BigDecimal rewardAmount = config.getBigDecimal("rewardAmount");
|
||
record.setRewardAmount(rewardAmount);
|
||
record.setRewardStatus(2);
|
||
|
||
benefitService.grantPoints(record.getReferrerId(), rewardAmount.intValue());
|
||
}
|
||
}
|
||
|
||
referralRecordRepository.save(record);
|
||
}
|
||
|
||
/**
|
||
* 生成唯一推荐码
|
||
*/
|
||
private String generateUniqueCode() {
|
||
return UUID.randomUUID().toString().replace("-", "").substring(0, 12).toUpperCase();
|
||
}
|
||
|
||
/**
|
||
* 根据会员ID获取租户ID
|
||
*/
|
||
private Long getTenantIdByMemberId(Long memberId) {
|
||
Member member = memberRepository.findById(memberId).orElse(null);
|
||
return member != null ? member.getTenantId() : null;
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 五、数据智能类模块设计
|
||
|
||
### 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;
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 5.2 智能体测数据联动模块
|
||
|
||
#### 5.2.1 数据模型设计
|
||
|
||
**体测数据表 (body_composition)**
|
||
|
||
```sql
|
||
CREATE TABLE body_composition (
|
||
id BIGSERIAL PRIMARY KEY,
|
||
tenant_id BIGINT NOT NULL,
|
||
store_id BIGINT NOT NULL,
|
||
member_id BIGINT NOT NULL,
|
||
device_type VARCHAR(32) NOT NULL,
|
||
device_id VARCHAR(64),
|
||
test_date TIMESTAMP NOT NULL,
|
||
height DECIMAL(5, 2),
|
||
weight DECIMAL(5, 2),
|
||
body_fat_rate DECIMAL(5, 2),
|
||
muscle_mass DECIMAL(5, 2),
|
||
water_content DECIMAL(5, 2),
|
||
bone_mass DECIMAL(5, 2),
|
||
bmi DECIMAL(5, 2),
|
||
raw_data JSONB,
|
||
created_at TIMESTAMP DEFAULT NOW(),
|
||
updated_at TIMESTAMP DEFAULT NOW(),
|
||
deleted_at TIMESTAMP DEFAULT NULL,
|
||
|
||
CONSTRAINT fk_body_composition_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id),
|
||
CONSTRAINT fk_body_composition_store FOREIGN KEY (store_id) REFERENCES store(id),
|
||
CONSTRAINT fk_body_composition_member FOREIGN KEY (member_id) REFERENCES member(id)
|
||
);
|
||
```
|
||
|
||
**体测报告表 (body_test_report)**
|
||
|
||
```sql
|
||
CREATE TABLE body_test_report (
|
||
id BIGSERIAL PRIMARY KEY,
|
||
tenant_id BIGINT NOT NULL,
|
||
store_id BIGINT NOT NULL,
|
||
member_id BIGINT NOT NULL,
|
||
test_data_id BIGINT NOT NULL,
|
||
report_no VARCHAR(32) NOT NULL,
|
||
report_content JSONB NOT NULL,
|
||
analysis_result JSONB NOT NULL,
|
||
suggestions TEXT,
|
||
created_at TIMESTAMP DEFAULT NOW(),
|
||
updated_at TIMESTAMP DEFAULT NOW(),
|
||
deleted_at TIMESTAMP DEFAULT NULL,
|
||
|
||
CONSTRAINT fk_body_test_report_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id),
|
||
CONSTRAINT fk_body_test_report_store FOREIGN KEY (store_id) REFERENCES store(id),
|
||
CONSTRAINT fk_body_test_report_member FOREIGN KEY (member_id) REFERENCES member(id),
|
||
CONSTRAINT fk_body_test_report_test_data FOREIGN KEY (test_data_id) REFERENCES body_composition(id)
|
||
);
|
||
```
|
||
|
||
#### 5.2.2 核心服务设计
|
||
|
||
**体测数据服务**
|
||
|
||
```java
|
||
@Service
|
||
public class BodyCompositionService {
|
||
|
||
@Autowired
|
||
private BodyCompositionRepository bodyCompositionRepository;
|
||
|
||
@Autowired
|
||
private BodyTestReportRepository bodyTestReportRepository;
|
||
|
||
@Autowired
|
||
private MemberRepository memberRepository;
|
||
|
||
/**
|
||
* 接收体测数据
|
||
*/
|
||
@Transactional
|
||
public BodyComposition receiveBodyData(BodyDataDTO dto) {
|
||
BodyComposition bodyData = new BodyComposition();
|
||
bodyData.setTenantId(dto.getTenantId());
|
||
bodyData.setStoreId(dto.getStoreId());
|
||
bodyData.setMemberId(dto.getMemberId());
|
||
bodyData.setDeviceType(dto.getDeviceType());
|
||
bodyData.setDeviceId(dto.getDeviceId());
|
||
bodyData.setTestDate(dto.getTestDate());
|
||
bodyData.setHeight(dto.getHeight());
|
||
bodyData.setWeight(dto.getWeight());
|
||
bodyData.setBodyFatRate(dto.getBodyFatRate());
|
||
bodyData.setMuscleMass(dto.getMuscleMass());
|
||
bodyData.setWaterContent(dto.getWaterContent());
|
||
bodyData.setBoneMass(dto.getBoneMass());
|
||
bodyData.setBmi(calculateBMI(dto.getHeight(), dto.getWeight()));
|
||
bodyData.setRawData(dto.getRawData());
|
||
|
||
return bodyCompositionRepository.save(bodyData);
|
||
}
|
||
|
||
/**
|
||
* 生成体测报告
|
||
*/
|
||
@Transactional
|
||
public BodyTestReport generateReport(Long testDataId) {
|
||
BodyComposition bodyData = bodyCompositionRepository.findById(testDataId).orElse(null);
|
||
|
||
if (bodyData == null) {
|
||
throw new BusinessException("体测数据不存在");
|
||
}
|
||
|
||
BodyTestReport report = new BodyTestReport();
|
||
report.setTenantId(bodyData.getTenantId());
|
||
report.setStoreId(bodyData.getStoreId());
|
||
report.setMemberId(bodyData.getMemberId());
|
||
report.setTestDataId(testDataId);
|
||
report.setReportNo(generateReportNo(bodyData.getTenantId()));
|
||
|
||
JSONObject reportContent = generateReportContent(bodyData);
|
||
report.setReportContent(reportContent);
|
||
|
||
JSONObject analysisResult = analyzeBodyData(bodyData);
|
||
report.setAnalysisResult(analysisResult);
|
||
|
||
String suggestions = generateSuggestions(analysisResult);
|
||
report.setSuggestions(suggestions);
|
||
|
||
return bodyTestReportRepository.save(report);
|
||
}
|
||
|
||
/**
|
||
* 查询会员体测历史
|
||
*/
|
||
public List<BodyComposition> getMemberBodyHistory(Long memberId, LocalDate startDate, LocalDate endDate) {
|
||
return bodyCompositionRepository.findByMemberIdAndTestDateBetweenOrderByTestDateDesc(
|
||
memberId,
|
||
startDate.atStartOfDay(),
|
||
endDate.atTime(23, 59, 59)
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 计算BMI
|
||
*/
|
||
private BigDecimal calculateBMI(BigDecimal height, BigDecimal weight) {
|
||
if (height == null || weight == null || height.compareTo(BigDecimal.ZERO) == 0) {
|
||
return null;
|
||
}
|
||
BigDecimal heightInMeters = height.divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);
|
||
return weight.divide(heightInMeters.multiply(heightInMeters), 2, RoundingMode.HALF_UP);
|
||
}
|
||
|
||
/**
|
||
* 生成报告内容
|
||
*/
|
||
private JSONObject generateReportContent(BodyComposition bodyData) {
|
||
JSONObject content = new JSONObject();
|
||
content.put("height", bodyData.getHeight());
|
||
content.put("weight", bodyData.getWeight());
|
||
content.put("bodyFatRate", bodyData.getBodyFatRate());
|
||
content.put("muscleMass", bodyData.getMuscleMass());
|
||
content.put("waterContent", bodyData.getWaterContent());
|
||
content.put("boneMass", bodyData.getBoneMass());
|
||
content.put("bmi", bodyData.getBmi());
|
||
return content;
|
||
}
|
||
|
||
/**
|
||
* 分析体测数据
|
||
*/
|
||
private JSONObject analyzeBodyData(BodyComposition bodyData) {
|
||
JSONObject analysis = new JSONObject();
|
||
|
||
if (bodyData.getBmi() != null) {
|
||
String bmiStatus = analyzeBMI(bodyData.getBmi());
|
||
analysis.put("bmiStatus", bmiStatus);
|
||
}
|
||
|
||
if (bodyData.getBodyFatRate() != null) {
|
||
String bodyFatStatus = analyzeBodyFatRate(bodyData.getBodyFatRate());
|
||
analysis.put("bodyFatStatus", bodyFatStatus);
|
||
}
|
||
|
||
return analysis;
|
||
}
|
||
|
||
/**
|
||
* 分析BMI
|
||
*/
|
||
private String analyzeBMI(BigDecimal bmi) {
|
||
if (bmi.compareTo(new BigDecimal("18.5")) < 0) {
|
||
return "偏瘦";
|
||
} else if (bmi.compareTo(new BigDecimal("24")) < 0) {
|
||
return "正常";
|
||
} else if (bmi.compareTo(new BigDecimal("28")) < 0) {
|
||
return "偏胖";
|
||
} else {
|
||
return "肥胖";
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 分析体脂率
|
||
*/
|
||
private String analyzeBodyFatRate(BigDecimal bodyFatRate) {
|
||
if (bodyFatRate.compareTo(new BigDecimal("10")) < 0) {
|
||
return "偏低";
|
||
} else if (bodyFatRate.compareTo(new BigDecimal("20")) < 0) {
|
||
return "正常";
|
||
} else if (bodyFatRate.compareTo(new BigDecimal("25")) < 0) {
|
||
return "偏高";
|
||
} else {
|
||
return "过高";
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成建议
|
||
*/
|
||
private String generateSuggestions(JSONObject analysis) {
|
||
StringBuilder suggestions = new StringBuilder();
|
||
|
||
String bmiStatus = analysis.getString("bmiStatus");
|
||
if ("偏瘦".equals(bmiStatus)) {
|
||
suggestions.append("建议增加营养摄入,适当进行力量训练。");
|
||
} else if ("偏胖".equals(bmiStatus) || "肥胖".equals(bmiStatus)) {
|
||
suggestions.append("建议控制饮食,增加有氧运动。");
|
||
}
|
||
|
||
String bodyFatStatus = analysis.getString("bodyFatStatus");
|
||
if ("偏高".equals(bodyFatStatus) || "过高".equals(bodyFatStatus)) {
|
||
suggestions.append("建议进行减脂训练,注意饮食控制。");
|
||
}
|
||
|
||
return suggestions.toString();
|
||
}
|
||
|
||
/**
|
||
* 生成报告编号
|
||
*/
|
||
private String generateReportNo(Long tenantId) {
|
||
String timestamp = String.valueOf(System.currentTimeMillis());
|
||
return "R" + tenantId + timestamp;
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 六、营销分析与预测模块设计
|
||
|
||
### 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;
|
||
|
||
@Autowired
|
||
private MachineLearningModelService machineLearningModelService;
|
||
|
||
/**
|
||
* 预测促销策略
|
||
* 基于机器学习算法进行促销策略预测
|
||
*/
|
||
@Transactional
|
||
public MarketingPrediction predictPromotionStrategy(PromotionPredictionRequest request) {
|
||
Map<String, Object> historicalData = collectHistoricalData(request.getTenantId(), request.getStoreId());
|
||
|
||
Map<String, Object> predictedData = machineLearningModelService.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 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 缓存设计
|
||
|
||
```mermaid
|
||
graph TB
|
||
subgraph LocalCache["本地缓存 (Caffeine)"]
|
||
LC1["会员信息缓存<br/>TTL: 30分钟"]
|
||
LC2["会员卡缓存<br/>TTL: 30分钟"]
|
||
LC3["课程信息缓存<br/>TTL: 1小时"]
|
||
LC4["配置信息缓存<br/>TTL: 1小时"]
|
||
end
|
||
|
||
subgraph RedisCache["分布式缓存 (Redis)"]
|
||
RC1["验证码缓存<br/>TTL: 5分钟"]
|
||
RC2["令牌缓存<br/>TTL: 24小时"]
|
||
RC3["限流计数器<br/>TTL: 1分钟"]
|
||
RC4["预测结果缓存<br/>TTL: 1小时"]
|
||
end
|
||
|
||
style LocalCache fill:#e1f5ff
|
||
style RedisCache fill:#fff4e1
|
||
```
|
||
|
||
---
|
||
|
||
## 八、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
|
||
}
|
||
```
|
||
|
||
### 8.3 智能获客工具模块API
|
||
|
||
#### 8.3.1 创建获客活动
|
||
|
||
```
|
||
POST /api/v1/customer-acquisition/activities
|
||
|
||
Request:
|
||
{
|
||
"tenantId": 1,
|
||
"name": "节后健身潮获客活动",
|
||
"type": 1,
|
||
"description": "针对节后健身潮的获客活动",
|
||
"startDate": "2026-01-01",
|
||
"endDate": "2026-03-31",
|
||
"config": {
|
||
"rewardAmount": 100,
|
||
"maxReferrals": 10
|
||
}
|
||
}
|
||
|
||
Response:
|
||
{
|
||
"code": 200,
|
||
"message": "创建成功",
|
||
"data": {
|
||
"id": 1,
|
||
"name": "节后健身潮获客活动",
|
||
"type": 1,
|
||
"status": 1
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 8.3.2 生成推荐码
|
||
|
||
```
|
||
POST /api/v1/customer-acquisition/referral-codes
|
||
|
||
Request:
|
||
{
|
||
"memberId": 1,
|
||
"activityId": 1
|
||
}
|
||
|
||
Response:
|
||
{
|
||
"code": 200,
|
||
"message": "生成成功",
|
||
"data": {
|
||
"referralCode": "ABC123DEF456"
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 8.3.3 处理推荐关系
|
||
|
||
```
|
||
POST /api/v1/customer-acquisition/referrals
|
||
|
||
Request:
|
||
{
|
||
"memberId": 2,
|
||
"referralCode": "ABC123DEF456"
|
||
}
|
||
|
||
Response:
|
||
{
|
||
"code": 200,
|
||
"message": "处理成功",
|
||
"data": {
|
||
"referrerId": 1,
|
||
"refereeId": 2,
|
||
"rewardAmount": 100
|
||
}
|
||
}
|
||
```
|
||
|
||
### 8.4 智能体测数据联动模块API
|
||
|
||
#### 8.4.1 接收体测数据
|
||
|
||
```
|
||
POST /api/v1/body-composition/data
|
||
|
||
Request:
|
||
{
|
||
"tenantId": 1,
|
||
"storeId": 1,
|
||
"memberId": 1,
|
||
"deviceType": "InBody",
|
||
"deviceId": "IB001",
|
||
"testDate": "2026-03-07T10:00:00",
|
||
"height": 175.5,
|
||
"weight": 70.2,
|
||
"bodyFatRate": 15.5,
|
||
"muscleMass": 55.3,
|
||
"waterContent": 60.2,
|
||
"boneMass": 2.8,
|
||
"rawData": {}
|
||
}
|
||
|
||
Response:
|
||
{
|
||
"code": 200,
|
||
"message": "接收成功",
|
||
"data": {
|
||
"id": 1,
|
||
"bmi": 22.8
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 8.4.2 生成体测报告
|
||
|
||
```
|
||
POST /api/v1/body-composition/reports
|
||
|
||
Request:
|
||
{
|
||
"testDataId": 1
|
||
}
|
||
|
||
Response:
|
||
{
|
||
"code": 200,
|
||
"message": "生成成功",
|
||
"data": {
|
||
"id": 1,
|
||
"reportNo": "R11000000000000001",
|
||
"reportContent": {},
|
||
"analysisResult": {
|
||
"bmiStatus": "正常",
|
||
"bodyFatStatus": "正常"
|
||
},
|
||
"suggestions": "建议保持当前运动和饮食习惯。"
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 8.4.3 查询会员体测历史
|
||
|
||
```
|
||
GET /api/v1/body-composition/history?memberId=1&startDate=2026-01-01&endDate=2026-03-07
|
||
|
||
Response:
|
||
{
|
||
"code": 200,
|
||
"message": "查询成功",
|
||
"data": [
|
||
{
|
||
"id": 1,
|
||
"testDate": "2026-03-07T10:00:00",
|
||
"height": 175.5,
|
||
"weight": 70.2,
|
||
"bodyFatRate": 15.5,
|
||
"muscleMass": 55.3,
|
||
"bmi": 22.8
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 九、测试用例
|
||
|
||
### 9.1 订阅与配置模块测试用例
|
||
|
||
#### 9.1.1 订阅模块测试
|
||
|
||
| 测试用例 | 输入 | 预期输出 |
|
||
| -------- | -------------------------- | ---------------- |
|
||
| 正常订阅 | 租户ID、模块代码、计费周期 | 订阅成功 |
|
||
| 重复订阅 | 已订阅的模块 | 提示该模块已订阅 |
|
||
| 支付失败 | 支付失败 | 提示支付失败 |
|
||
|
||
#### 9.1.2 配置查询测试
|
||
|
||
| 测试用例 | 输入 | 预期输出 |
|
||
| ------------ | ------------------------ | ---------------- |
|
||
| 查询租户配置 | 租户ID、模块代码 | 返回租户配置 |
|
||
| 查询门店配置 | 租户ID、门店ID、模块代码 | 返回合并后的配置 |
|
||
| 查询默认配置 | 不存在的配置 | 返回默认配置 |
|
||
|
||
### 9.2 营销分析与预测模块测试用例
|
||
|
||
#### 9.2.1 预测促销策略测试
|
||
|
||
| 测试用例 | 输入 | 预期输出 |
|
||
| ------------ | ---------------------- | ---------------- |
|
||
| 正常预测 | 租户ID、活动类型、参数 | 预测成功 |
|
||
| 历史数据不足 | 新租户 | 提示历史数据不足 |
|
||
| 参数无效 | 无效的参数 | 提示参数无效 |
|
||
|
||
---
|
||
|
||
## 十、部署与运维
|
||
|
||
### 10.1 部署架构
|
||
|
||
```mermaid
|
||
flowchart TB
|
||
LB["负载均衡<br/>(Nginx)"]
|
||
|
||
subgraph K8S["应用服务器<br/>(Kubernetes)"]
|
||
P1["Pod 1"]
|
||
P2["Pod 2"]
|
||
P3["Pod 3"]
|
||
P4["Pod 4"]
|
||
P5["Pod 5"]
|
||
end
|
||
|
||
subgraph PG["数据库<br/>(PostgreSQL)"]
|
||
PG1["主库"]
|
||
PG2["从库"]
|
||
PG3["从库"]
|
||
end
|
||
|
||
subgraph Redis["缓存<br/>(Redis)"]
|
||
R1["主节点"]
|
||
R2["从节点"]
|
||
R3["从节点"]
|
||
end
|
||
|
||
subgraph ES["搜索引擎<br/>(Elasticsearch)"]
|
||
ES1["节点1"]
|
||
ES2["节点2"]
|
||
ES3["节点3"]
|
||
end
|
||
|
||
LB --> K8S
|
||
K8S --> PG
|
||
PG --> Redis
|
||
Redis --> ES
|
||
|
||
style LB fill:#e1f5ff
|
||
style K8S fill:#fff4e1
|
||
style PG fill:#f0e1ff
|
||
style Redis fill:#e1ffe1
|
||
style ES fill:#ffe1e1
|
||
```
|
||
|
||
### 10.2 监控指标
|
||
|
||
| 指标类型 | 指标名称 | 阈值 |
|
||
| -------- | ----------- | ------- |
|
||
| 系统指标 | CPU使用率 | ≤ 80% |
|
||
| 系统指标 | 内存使用率 | ≤ 80% |
|
||
| 系统指标 | 磁盘使用率 | ≤ 80% |
|
||
| 应用指标 | API响应时间 | ≤ 500ms |
|
||
| 应用指标 | 错误率 | ≤ 1% |
|
||
| 应用指标 | 并发数 | ≤ 500 |
|
||
| 业务指标 | 订阅成功率 | ≥ 98% |
|
||
| 业务指标 | 预测准确率 | ≥ 75% |
|
||
|
||
---
|
||
|
||
## 十一、附录
|
||
|
||
### 11.1 术语定义
|
||
|
||
| 术语 | 定义 |
|
||
| ---------------- | -------------------------------------- |
|
||
| 订阅模块 | 按需订阅的增值功能模块 |
|
||
| 配置继承 | 门店配置继承租户配置的机制 |
|
||
| 私教管理 | 私教课程管理、私教预约、私教签到等功能 |
|
||
| 营销活动 | 吸引新会员和提升会员活跃度的活动 |
|
||
| 营销精算模型 | 基于历史数据预测促销策略的模型 |
|
||
| 促销活动效果预测 | 基于历史数据预测促销活动效果 |
|
||
|
||
### 11.2 参考文档
|
||
|
||
- 《健身房管理系统付费订阅版产品设计文档》 GYM-PRD-SUBSCRIPTION-001
|
||
- 《健身房管理系统付费订阅版业务概要设计文档》 GYM-B-HLD-SUBSCRIPTION-001
|
||
- 《健身房管理系统付费订阅版业务详细设计文档》 GYM-B-LLD-SUBSCRIPTION-001
|
||
- Spring Boot 3 官方文档
|
||
- R2DBC 规范文档
|
||
- PostgreSQL 官方文档
|