# 健身房管理系统付费订阅版技术实现详细设计文档(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 官方文档