# 健身房管理系统付费订阅版技术实现详细设计文档(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[客户端层
• 会员小程序 uniapp+Vue3
• 教练端App uniapp+Vue3
• 管理后台PC Vue3+Vite
• 硬件设备 人脸/NFC]
B[Presentation Layer WebFlux
• Controller
• Router
• Filter
• Validator]
C[Application Layer 业务编排
• Service
• Facade
• Orchestrator
• 事务管理]
D[Domain Layer 领域模型
• Entity
• Value Object
• Domain Service
• Repository]
E[Infrastructure Layer 基础设施
• Repository R2DBC
• Cache Redis
• Message RabbitMQ
• Search Elasticsearch
• File OSS
• Distributed Lock]
F[外部服务层
• PostgreSQL
• Redis
• RabbitMQ
• Elasticsearch
• 微信开放平台
• 短信服务
• 支付服务
• 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 redisTemplate;
private static final String CACHE_PREFIX = "config:";
private static final Duration CACHE_TTL = Duration.ofMinutes(30);
/**
* 获取模块配置(门店 → 租户 → 默认)
*/
public Mono 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 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 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 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 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 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 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 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 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 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 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 validateMemberBenefit(Long memberId) {
return benefitService.hasBenefit(memberId)
.flatMap(hasBenefit -> {
if (!Boolean.TRUE.equals(hasBenefit)) {
return Mono.error(new BusinessException("会员权益不足"));
}
return Mono.empty();
});
}
/**
* 创建私教课程
*/
private Mono 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 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 members = memberRepository.findByTenantIdAndStoreIdAndCreatedAtBetween(
tenantId, storeId, startDate.atStartOfDay(), endDate.atTime(23, 59, 59));
Map 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 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 historicalData = collectHistoricalData(request.getTenantId(), request.getStoreId());
Map 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 collectHistoricalData(Long tenantId, Long storeId) {
Map 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 historicalData, Map 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["会员信息缓存
TTL: 30分钟"]
LC2["会员卡缓存
TTL: 30分钟"]
LC3["课程信息缓存
TTL: 1小时"]
LC4["配置信息缓存
TTL: 1小时"]
end
subgraph RedisCache["分布式缓存 (Redis)"]
RC1["验证码缓存
TTL: 5分钟"]
RC2["令牌缓存
TTL: 24小时"]
RC3["限流计数器
TTL: 1分钟"]
RC4["预测结果缓存
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["负载均衡
(Nginx)"]
subgraph K8S["应用服务器
(Kubernetes)"]
P1["Pod 1"]
P2["Pod 2"]
P3["Pod 3"]
P4["Pod 4"]
P5["Pod 5"]
end
subgraph PG["数据库
(PostgreSQL)"]
PG1["主库"]
PG2["从库"]
PG3["从库"]
end
subgraph Redis["缓存
(Redis)"]
R1["主节点"]
R2["从节点"]
R3["从节点"]
end
subgraph ES["搜索引擎
(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 官方文档