Files
gym-manage/docs/design/LLD-付费订阅版系统详细设计.md
T
2026-03-05 13:48:13 +08:00

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 官方文档