54 KiB
54 KiB
健身房管理系统付费订阅版详细设计文档(LLD)
文档编号: GYM-LLD-SUBSCRIPTION-001
版本: v1.0
日期: 2026-03-04
作者: 张翔
状态: 初稿
文档修订历史
| 版本 | 日期 | 作者 | 修订内容 |
|---|---|---|---|
| v1.0 | 2026-03-04 | 张翔 | 创建付费订阅版详细设计 |
参考文档
- 《健身房管理系统付费订阅版产品设计文档》 GYM-PRD-SUBSCRIPTION-001
- 《健身房管理系统付费订阅版业务概要设计文档》 GYM-HLD-SUBSCRIPTION-001
- 《健身房管理系统详细设计文档》 GYM-LLD-000
- 《订阅与配置模块详细设计文档》 GYM-LLD-004
- Spring Boot 3 官方文档
- R2DBC 规范文档
- PostgreSQL 官方文档
一、系统架构设计
1.1 总体架构
采用分层架构 + 模块化设计的单体应用:
┌─────────────────────────────────────────────────────────────────────────┐
│ 付费订阅版单体应用架构 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 客户端层 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 会员小程序 (uniapp+Vue3) │ │
│ │ • 教练端App (uniapp+Vue3) │ │
│ │ • 管理后台PC (Vue3+Vite) │ │
│ │ • 硬件设备 (人脸/NFC) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Presentation Layer (WebFlux) │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • Controller • Router • Filter • Validator │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Application Layer (业务编排) │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • Service • Facade • Orchestrator • 事务管理 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Domain Layer (领域模型) │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • Entity • Value Object • Domain Service • Repository │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Infrastructure Layer (基础设施) │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • Repository (R2DBC) • Cache (Redis) │ │
│ │ • Message (RabbitMQ) • Search (Elasticsearch) │ │
│ │ • File (OSS) • Distributed Lock │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 外部服务层 │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • PostgreSQL • Redis • RabbitMQ • Elasticsearch │ │
│ │ • 微信开放平台 • 短信服务 • 支付服务 • OSS存储 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
二、订阅与配置模块设计
2.1 模块概述
订阅与配置模块是付费订阅版的核心基础设施模块,负责:
- 产品版本管理(基础版 + 订阅模块)
- 租户级和门店级配置管理
- 配置继承与覆盖机制
- 订阅计费与生命周期管理
2.2 数据模型设计
租户模块配置表 (tenant_module_config)
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)
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)
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 核心服务设计
配置查询服务
@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();
}
}
配置合并服务
@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();
}
}
订阅服务
@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)
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 核心服务设计
私教预约服务
@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)
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)
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)
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)
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)
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 核心服务设计
营销活动服务
@Service
public class MarketingActivityService {
@Autowired
private MarketingActivityRepository marketingActivityRepository;
@Autowired
private MarketingActivityParticipantRepository participantRepository;
@Autowired
private BenefitService benefitService;
/**
* 创建营销活动
*/
@Transactional
public MarketingActivity createActivity(MarketingActivityCreateRequest request) {
validateActivityTime(request.getStartDate(), request.getEndDate());
MarketingActivity activity = new MarketingActivity();
activity.setTenantId(request.getTenantId());
activity.setName(request.getName());
activity.setType(request.getType());
activity.setDescription(request.getDescription());
activity.setStartDate(request.getStartDate());
activity.setEndDate(request.getEndDate());
activity.setRules(request.getRules());
activity.setRewards(request.getRewards());
activity.setStatus(1);
return marketingActivityRepository.save(activity);
}
/**
* 参与营销活动
*/
@Transactional
public MarketingActivityParticipant participate(Long activityId, Long memberId) {
MarketingActivity activity = marketingActivityRepository.findById(activityId)
.orElseThrow(() -> new BusinessException("活动不存在"));
validateActivityStatus(activity);
validateActivityTime(activity);
validateParticipant(activityId, memberId);
MarketingActivityParticipant participant = new MarketingActivityParticipant();
participant.setTenantId(activity.getTenantId());
participant.setActivityId(activityId);
participant.setMemberId(memberId);
participant.setParticipatedAt(LocalDateTime.now());
participant.setRewardStatus(1);
participant = participantRepository.save(participant);
grantReward(activity, memberId);
return participant;
}
/**
* 验证活动时间
*/
private void validateActivityTime(LocalDate startDate, LocalDate endDate) {
if (startDate.isAfter(endDate)) {
throw new BusinessException("活动开始时间不能晚于结束时间");
}
}
/**
* 验证活动状态
*/
private void validateActivityStatus(MarketingActivity activity) {
if (activity.getStatus() != 1) {
throw new BusinessException("活动未开始或已结束");
}
}
/**
* 验证活动时间
*/
private void validateActivityTime(MarketingActivity activity) {
LocalDate now = LocalDate.now();
if (now.isBefore(activity.getStartDate()) || now.isAfter(activity.getEndDate())) {
throw new BusinessException("活动未开始或已结束");
}
}
/**
* 验证参与者
*/
private void validateParticipant(Long activityId, Long memberId) {
if (participantRepository.existsByActivityIdAndMemberId(activityId, memberId)) {
throw new BusinessException("已参与过该活动");
}
}
/**
* 发放奖励
*/
private void grantReward(MarketingActivity activity, Long memberId) {
Map<String, Object> rewards = activity.getRewards();
String rewardType = (String) rewards.get("type");
Object rewardValue = rewards.get("value");
switch (rewardType) {
case "card":
benefitService.grantCard(memberId, (String) rewardValue);
break;
case "points":
benefitService.grantPoints(memberId, (Integer) rewardValue);
break;
case "coupon":
benefitService.grantCoupon(memberId, (String) rewardValue);
break;
default:
throw new BusinessException("不支持的奖励类型");
}
}
}
五、数据智能类模块设计
5.1 高级数据分析模块
5.1.1 核心服务设计
高级数据分析服务
@Service
public class AdvancedDataAnalysisService {
@Autowired
private MemberRepository memberRepository;
@Autowired
private BookingRecordRepository bookingRecordRepository;
@Autowired
private CheckInRecordRepository checkInRecordRepository;
/**
* 会员留存分析
*/
public MemberRetentionAnalysis analyzeMemberRetention(Long tenantId, Long storeId, LocalDate startDate, LocalDate endDate) {
List<Member> members = memberRepository.findByTenantIdAndStoreIdAndCreatedAtBetween(
tenantId, storeId, startDate.atStartOfDay(), endDate.atTime(23, 59, 59));
Map<Integer, Long> retentionData = new HashMap<>();
for (Member member : members) {
int retentionDays = calculateRetentionDays(member.getCreatedAt(), LocalDate.now());
retentionData.put(retentionDays, retentionData.getOrDefault(retentionDays, 0L) + 1);
}
MemberRetentionAnalysis analysis = new MemberRetentionAnalysis();
analysis.setTotalMembers(members.size());
analysis.setRetentionData(retentionData);
return analysis;
}
/**
* 计算留存天数
*/
private int calculateRetentionDays(LocalDateTime createdAt, LocalDate now) {
return (int) ChronoUnit.DAYS.between(createdAt.toLocalDate(), now);
}
/**
* 预约转化率分析
*/
public BookingConversionAnalysis analyzeBookingConversion(Long tenantId, Long storeId, LocalDate startDate, LocalDate endDate) {
long totalBookings = bookingRecordRepository.countByTenantIdAndStoreIdAndBookedAtBetween(
tenantId, storeId, startDate.atStartOfDay(), endDate.atTime(23, 59, 59));
long totalCheckIns = checkInRecordRepository.countByTenantIdAndStoreIdAndCheckInAtBetween(
tenantId, storeId, startDate.atStartOfDay(), endDate.atTime(23, 59, 59));
BookingConversionAnalysis analysis = new BookingConversionAnalysis();
analysis.setTotalBookings(totalBookings);
analysis.setTotalCheckIns(totalCheckIns);
analysis.setConversionRate(totalBookings > 0 ? (double) totalCheckIns / totalBookings : 0.0);
return analysis;
}
}
六、营销分析与预测模块设计
6.1 模块概述
营销分析与预测模块是付费订阅版的高级功能模块,负责:
- 营销精算模型预测促销策略
- 多维度自定义促销活动
- 促销活动效果预测
6.2 数据模型设计
营销预测表 (marketing_prediction)
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)
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 核心服务设计
营销精算模型服务
@Service
public class MarketingActuarialModelService {
@Autowired
private MarketingPredictionRepository marketingPredictionRepository;
@Autowired
private MemberRepository memberRepository;
@Autowired
private BookingRecordRepository bookingRecordRepository;
@Autowired
private CheckInRecordRepository checkInRecordRepository;
/**
* 预测促销策略
*/
@Transactional
public MarketingPrediction predictPromotionStrategy(PromotionPredictionRequest request) {
Map<String, Object> historicalData = collectHistoricalData(request.getTenantId(), request.getStoreId());
Map<String, Object> predictedData = runPredictionModel(historicalData, request.getParameters());
MarketingPrediction prediction = new MarketingPrediction();
prediction.setTenantId(request.getTenantId());
prediction.setPredictionNo(generatePredictionNo(request.getTenantId()));
prediction.setActivityType(request.getActivityType());
prediction.setParameters(request.getParameters());
prediction.setPredictedData(predictedData);
prediction.setAccuracy(calculateAccuracy(historicalData, predictedData));
prediction.setModelVersion("v1.0");
return marketingPredictionRepository.save(prediction);
}
/**
* 收集历史数据
*/
private Map<String, Object> collectHistoricalData(Long tenantId, Long storeId) {
Map<String, Object> historicalData = new HashMap<>();
LocalDate endDate = LocalDate.now();
LocalDate startDate = endDate.minusMonths(6);
long totalMembers = memberRepository.countByTenantIdAndStoreIdAndCreatedAtBetween(
tenantId, storeId, startDate.atStartOfDay(), endDate.atTime(23, 59, 59));
long totalBookings = bookingRecordRepository.countByTenantIdAndStoreIdAndBookedAtBetween(
tenantId, storeId, startDate.atStartOfDay(), endDate.atTime(23, 59, 59));
long totalCheckIns = checkInRecordRepository.countByTenantIdAndStoreIdAndCheckInAtBetween(
tenantId, storeId, startDate.atStartOfDay(), endDate.atTime(23, 59, 59));
historicalData.put("totalMembers", totalMembers);
historicalData.put("totalBookings", totalBookings);
historicalData.put("totalCheckIns", totalCheckIns);
return historicalData;
}
/**
* 运行预测模型
*/
private Map<String, Object> runPredictionModel(Map<String, Object> historicalData, Map<String, Object> parameters) {
Map<String, Object> predictedData = new HashMap<>();
Long totalMembers = (Long) historicalData.get("totalMembers");
Long totalBookings = (Long) historicalData.get("totalBookings");
Long totalCheckIns = (Long) historicalData.get("totalCheckIns");
Double discountRate = (Double) parameters.get("discountRate");
Integer durationDays = (Integer) parameters.get("durationDays");
double predictedNewMembers = totalMembers * 0.1 * (1 + discountRate * 2);
double predictedBookings = totalBookings * 1.2 * (1 + discountRate * 1.5);
double predictedCheckIns = totalCheckIns * 1.15 * (1 + discountRate * 1.3);
predictedData.put("predictedNewMembers", Math.round(predictedNewMembers));
predictedData.put("predictedBookings", Math.round(predictedBookings));
predictedData.put("predictedCheckIns", Math.round(predictedCheckIns));
predictedData.put("predictedRevenue", predictedBookings * 100 * (1 - discountRate));
return predictedData;
}
/**
* 计算准确率
*/
private BigDecimal calculateAccuracy(Map<String, Object> historicalData, Map<String, Object> predictedData) {
return BigDecimal.valueOf(0.85);
}
/**
* 生成预测号
*/
private String generatePredictionNo(Long tenantId) {
String prefix = "P" + tenantId;
String timestamp = String.valueOf(System.currentTimeMillis());
String random = String.valueOf(new Random().nextInt(1000));
return prefix + timestamp.substring(timestamp.length() - 8) + random;
}
}
促销活动服务
@Service
public class PromotionActivityService {
@Autowired
private PromotionActivityRepository promotionActivityRepository;
@Autowired
private MarketingActuarialModelService marketingActuarialModelService;
/**
* 创建促销活动
*/
@Transactional
public PromotionActivity createActivity(PromotionActivityCreateRequest request) {
PromotionActivity activity = new PromotionActivity();
activity.setTenantId(request.getTenantId());
activity.setName(request.getName());
activity.setType(request.getType());
activity.setDescription(request.getDescription());
activity.setStartDate(request.getStartDate());
activity.setEndDate(request.getEndDate());
activity.setTargetAudience(request.getTargetAudience());
activity.setDiscountRule(request.getDiscountRule());
activity.setBudget(request.getBudget());
activity.setStatus(1);
activity = promotionActivityRepository.save(activity);
if (request.isPredictEffect()) {
predictActivityEffect(activity);
}
return activity;
}
/**
* 预测活动效果
*/
private void predictActivityEffect(PromotionActivity activity) {
PromotionPredictionRequest predictionRequest = new PromotionPredictionRequest();
predictionRequest.setTenantId(activity.getTenantId());
predictionRequest.setActivityType(activity.getType());
predictionRequest.setParameters(activity.getDiscountRule());
MarketingPrediction prediction = marketingActuarialModelService.predictPromotionStrategy(predictionRequest);
activity.setPredictedData(prediction.getPredictedData());
promotionActivityRepository.save(activity);
}
}
七、缓存策略
7.1 缓存设计
┌─────────────────────────────────────────────────────────────────────────┐
│ 缓存策略 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 本地缓存 (Caffeine) │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 会员信息缓存 (TTL: 30分钟) │ │
│ │ • 会员卡缓存 (TTL: 30分钟) │ │
│ │ • 课程信息缓存 (TTL: 1小时) │ │
│ │ • 配置信息缓存 (TTL: 1小时) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 分布式缓存 (Redis) │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 验证码缓存 (TTL: 5分钟) │ │
│ │ • 令牌缓存 (TTL: 24小时) │ │
│ │ • 限流计数器 (TTL: 1分钟) │ │
│ │ • 预测结果缓存 (TTL: 1小时) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
八、API设计
8.1 订阅与配置模块API
8.1.1 订阅模块
POST /api/v1/subscriptions/subscribe
Request:
{
"tenantId": 1,
"moduleCode": "private_class",
"billingCycle": 1,
"amount": 299.00,
"discountAmount": 0.00,
"actualAmount": 299.00
}
Response:
{
"code": 200,
"message": "订阅成功",
"data": {
"id": 1,
"subscriptionNo": "S10000000000000001",
"moduleCode": "private_class",
"moduleName": "私教管理模块",
"startDate": "2026-03-04",
"endDate": "2026-04-03",
"status": 1
}
}
8.1.2 配置查询
GET /api/v1/configs/module?tenantId=1&storeId=1&moduleCode=private_class
Response:
{
"code": 200,
"message": "查询成功",
"data": {
"moduleCode": "private_class",
"enabled": true,
"configData": {
"maxBookingDays": 7,
"cancelHours": 24
}
}
}
8.2 营销分析与预测模块API
8.2.1 预测促销策略
POST /api/v1/marketing/predict
Request:
{
"tenantId": 1,
"storeId": 1,
"activityType": 1,
"parameters": {
"discountRate": 0.2,
"durationDays": 30,
"targetAudience": "new_members"
}
}
Response:
{
"code": 200,
"message": "预测成功",
"data": {
"id": 1,
"predictionNo": "P10000000000000001",
"activityType": 1,
"predictedData": {
"predictedNewMembers": 120,
"predictedBookings": 600,
"predictedCheckIns": 920,
"predictedRevenue": 48000.00
},
"accuracy": 0.85
}
}
九、测试用例
9.1 订阅与配置模块测试用例
9.1.1 订阅模块测试
| 测试用例 | 输入 | 预期输出 |
|---|---|---|
| 正常订阅 | 租户ID、模块代码、计费周期 | 订阅成功 |
| 重复订阅 | 已订阅的模块 | 提示该模块已订阅 |
| 支付失败 | 支付失败 | 提示支付失败 |
9.1.2 配置查询测试
| 测试用例 | 输入 | 预期输出 |
|---|---|---|
| 查询租户配置 | 租户ID、模块代码 | 返回租户配置 |
| 查询门店配置 | 租户ID、门店ID、模块代码 | 返回合并后的配置 |
| 查询默认配置 | 不存在的配置 | 返回默认配置 |
9.2 营销分析与预测模块测试用例
9.2.1 预测促销策略测试
| 测试用例 | 输入 | 预期输出 |
|---|---|---|
| 正常预测 | 租户ID、活动类型、参数 | 预测成功 |
| 历史数据不足 | 新租户 | 提示历史数据不足 |
| 参数无效 | 无效的参数 | 提示参数无效 |
十、部署与运维
10.1 部署架构
┌─────────────────────────────────────────────────────────────────────────┐
│ 部署架构 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 负载均衡 (Nginx) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 应用服务器 (Kubernetes) │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • Pod 1 • Pod 2 • Pod 3 • Pod 4 • Pod 5 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 数据库 (PostgreSQL) │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 主库 • 从库 • 从库 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 缓存 (Redis) │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 主节点 • 从节点 • 从节点 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 搜索引擎 (Elasticsearch) │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ • 节点1 • 节点2 • 节点3 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
10.2 监控指标
| 指标类型 | 指标名称 | 阈值 |
|---|---|---|
| 系统指标 | CPU使用率 | ≤ 80% |
| 系统指标 | 内存使用率 | ≤ 80% |
| 系统指标 | 磁盘使用率 | ≤ 80% |
| 应用指标 | API响应时间 | ≤ 500ms |
| 应用指标 | 错误率 | ≤ 1% |
| 应用指标 | 并发数 | ≤ 500 |
| 业务指标 | 订阅成功率 | ≥ 98% |
| 业务指标 | 预测准确率 | ≥ 75% |
十一、附录
11.1 术语定义
| 术语 | 定义 |
|---|---|
| 订阅模块 | 按需订阅的增值功能模块 |
| 配置继承 | 门店配置继承租户配置的机制 |
| 私教管理 | 私教课程管理、私教预约、私教签到等功能 |
| 营销活动 | 吸引新会员和提升会员活跃度的活动 |
| 营销精算模型 | 基于历史数据预测促销策略的模型 |
| 促销活动效果预测 | 基于历史数据预测促销活动效果 |
11.2 参考文档
- 《健身房管理系统付费订阅版产品设计文档》 GYM-PRD-SUBSCRIPTION-001
- 《健身房管理系统付费订阅版业务概要设计文档》 GYM-HLD-SUBSCRIPTION-001
- 《健身房管理系统详细设计文档》 GYM-LLD-000
- 《订阅与配置模块详细设计文档》 GYM-LLD-004
- Spring Boot 3 官方文档
- R2DBC 规范文档
- PostgreSQL 官方文档