# 健身房管理系统付费订阅版详细设计文档(LLD) > 文档编号: GYM-LLD-SUBSCRIPTION-001 > 版本: v1.0 > 日期: 2026-03-04 > 作者: 张翔 > 状态: 初稿 --- ## 文档修订历史 | 版本 | 日期 | 作者 | 修订内容 | | ---- | ---------- | ---- | -------- | | v1.0 | 2026-03-04 | 张翔 | 创建付费订阅版详细设计 | --- ## 参考文档 - 《健身房管理系统付费订阅版产品设计文档》 GYM-PRD-SUBSCRIPTION-001 - 《健身房管理系统付费订阅版业务概要设计文档》 GYM-HLD-SUBSCRIPTION-001 - 《健身房管理系统详细设计文档》 GYM-LLD-000 - 《订阅与配置模块详细设计文档》 GYM-LLD-004 - Spring Boot 3 官方文档 - R2DBC 规范文档 - PostgreSQL 官方文档 --- ## 一、系统架构设计 ### 1.1 总体架构 采用分层架构 + 模块化设计的单体应用: ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ 付费订阅版单体应用架构 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ 客户端层 │ │ │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ • 会员小程序 (uniapp+Vue3) │ │ │ │ • 教练端App (uniapp+Vue3) │ │ │ │ • 管理后台PC (Vue3+Vite) │ │ │ │ • 硬件设备 (人脸/NFC) │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ Presentation Layer (WebFlux) │ │ │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ • Controller • Router • Filter • Validator │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ Application Layer (业务编排) │ │ │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ • Service • Facade • Orchestrator • 事务管理 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ Domain Layer (领域模型) │ │ │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ • Entity • Value Object • Domain Service • Repository │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ Infrastructure Layer (基础设施) │ │ │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ • Repository (R2DBC) • Cache (Redis) │ │ │ │ • Message (RabbitMQ) • Search (Elasticsearch) │ │ │ │ • File (OSS) • Distributed Lock │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ 外部服务层 │ │ │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ • PostgreSQL • Redis • RabbitMQ • Elasticsearch │ │ │ │ • 微信开放平台 • 短信服务 • 支付服务 • OSS存储 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` --- ## 二、订阅与配置模块设计 ### 2.1 模块概述 订阅与配置模块是付费订阅版的核心基础设施模块,负责: - 产品版本管理(基础版 + 订阅模块) - 租户级和门店级配置管理 - 配置继承与覆盖机制 - 订阅计费与生命周期管理 ### 2.2 数据模型设计 **租户模块配置表 (tenant_module_config)** ```sql CREATE TABLE tenant_module_config ( id BIGSERIAL PRIMARY KEY, tenant_id BIGINT NOT NULL, module_code VARCHAR(32) NOT NULL, enabled BOOLEAN NOT NULL, config_data JSONB, version INT DEFAULT 0, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), created_by BIGINT, updated_by BIGINT, deleted_at TIMESTAMP DEFAULT NULL, CONSTRAINT uk_tenant_module UNIQUE (tenant_id, module_code), CONSTRAINT fk_tenant_module_config FOREIGN KEY (tenant_id) REFERENCES tenant(id) ); ``` **门店模块配置表 (store_module_config)** ```sql CREATE TABLE store_module_config ( id BIGSERIAL PRIMARY KEY, tenant_id BIGINT NOT NULL, store_id BIGINT NOT NULL, module_code VARCHAR(32) NOT NULL, inherit_mode SMALLINT NOT NULL, enabled BOOLEAN NOT NULL, config_data JSONB, version INT DEFAULT 0, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), created_by BIGINT, updated_by BIGINT, deleted_at TIMESTAMP DEFAULT NULL, CONSTRAINT uk_store_module UNIQUE (store_id, module_code), CONSTRAINT fk_store_module_config FOREIGN KEY (store_id) REFERENCES store(id) ); ``` **订阅记录表 (subscription_record)** ```sql CREATE TABLE subscription_record ( id BIGSERIAL PRIMARY KEY, tenant_id BIGINT NOT NULL, subscription_no VARCHAR(32) NOT NULL, module_code VARCHAR(32) NOT NULL, billing_cycle SMALLINT NOT NULL, amount DECIMAL(10,2) NOT NULL, discount_amount DECIMAL(10,2) DEFAULT 0.00, actual_amount DECIMAL(10,2) NOT NULL, start_date DATE NOT NULL, end_date DATE NOT NULL, status SMALLINT DEFAULT 1, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), created_by BIGINT, updated_by BIGINT, deleted_at TIMESTAMP DEFAULT NULL, CONSTRAINT uk_subscription_no UNIQUE (subscription_no), CONSTRAINT fk_subscription_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id) ); ``` ### 2.3 核心服务设计 **配置查询服务** ```java @Service @Slf4j @RequiredArgsConstructor public class ConfigQueryService { private final TenantModuleConfigRepository tenantModuleConfigRepository; private final StoreModuleConfigRepository storeModuleConfigRepository; private final ConfigMerger configMerger; private final ReactiveRedisTemplate 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("不支持的奖励类型"); } } } ``` --- ## 五、数据智能类模块设计 ### 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; } } ``` --- ## 六、营销分析与预测模块设计 ### 6.1 模块概述 营销分析与预测模块是付费订阅版的高级功能模块,负责: - 营销精算模型预测促销策略 - 多维度自定义促销活动 - 促销活动效果预测 ### 6.2 数据模型设计 **营销预测表 (marketing_prediction)** ```sql CREATE TABLE marketing_prediction ( id BIGSERIAL PRIMARY KEY, tenant_id BIGINT NOT NULL, prediction_no VARCHAR(32) NOT NULL, activity_type SMALLINT NOT NULL, parameters JSONB NOT NULL, predicted_data JSONB NOT NULL, accuracy DECIMAL(5,4), model_version VARCHAR(32), created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), created_by BIGINT, updated_by BIGINT, deleted_at TIMESTAMP DEFAULT NULL, CONSTRAINT uk_marketing_prediction_no UNIQUE (prediction_no), CONSTRAINT fk_marketing_prediction_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id) ); ``` **促销活动表 (promotion_activity)** ```sql CREATE TABLE promotion_activity ( id BIGSERIAL PRIMARY KEY, tenant_id BIGINT NOT NULL, name VARCHAR(128) NOT NULL, type SMALLINT NOT NULL, description TEXT, start_date DATE NOT NULL, end_date DATE NOT NULL, target_audience JSONB NOT NULL, discount_rule JSONB NOT NULL, budget DECIMAL(10,2), predicted_data JSONB, actual_data JSONB, status SMALLINT DEFAULT 1, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), created_by BIGINT, updated_by BIGINT, deleted_at TIMESTAMP DEFAULT NULL, CONSTRAINT fk_promotion_activity_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id) ); ``` ### 6.3 核心服务设计 **营销精算模型服务** ```java @Service public class MarketingActuarialModelService { @Autowired private MarketingPredictionRepository marketingPredictionRepository; @Autowired private MemberRepository memberRepository; @Autowired private BookingRecordRepository bookingRecordRepository; @Autowired private CheckInRecordRepository checkInRecordRepository; /** * 预测促销策略 */ @Transactional public MarketingPrediction predictPromotionStrategy(PromotionPredictionRequest request) { Map historicalData = collectHistoricalData(request.getTenantId(), request.getStoreId()); Map 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 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 Map runPredictionModel(Map historicalData, Map parameters) { Map 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 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 缓存设计 ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ 缓存策略 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ 本地缓存 (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 官方文档