Files
gym-manage/docs/design/technical/T-ILD-付费订阅版-技术实现详细设计.md
T
2026-03-08 22:00:52 +08:00

1895 lines
58 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 健身房管理系统付费订阅版技术实现详细设计文档(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队列:多队列集群模式
- Elasticsearch3节点集群部署
**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 官方文档