Files
gym-manage/docs/design/LLD-基础版系统详细设计.md
T
张翔 971d51cb36 feat: 同步UI模版定制功能到PRD、HLD、LLD文档
PRD更新:
- 新增2.6 UI模版定制模块
- 包含品牌定制、布局调整、预设模板、配置历史、可视化配置器五个子模块
- 每个子模块包含功能描述、用户故事、功能点、业务规则、验收标准

HLD更新:
- 业务范围中新增UI模版定制模块
- 新增3.5 UI模版定制流程(业务场景、业务流程、业务规则、异常处理)
- 新增4.6 UI模版定制规则(品牌元素应用、Logo格式限制、颜色格式限制等8条规则)

LLD更新:
- 新增2.6 UI模版定制模块(模块概述、数据模型设计、核心业务逻辑)
- 数据模型包含4个表:tenant_ui_config、ui_template、ui_config_history、ui_resource
- 核心业务逻辑包含4个Service:BrandConfigService、LayoutConfigService、TemplateService、ConfigHistoryService
- 新增3.5 UI模版定制模块API(10个API接口,涵盖品牌定制、布局调整、模板管理、配置历史)

所有文档已保持一致性,UI模版定制功能已完整同步到产品需求、概要设计、详细设计文档中
2026-03-07 16:59:32 +08:00

73 KiB

健身房管理系统基础版详细设计文档(LLD)

文档编号: GYM-LLD-BASIC-001
版本: v1.0
日期: 2026-03-04
作者: 张翔
状态: 初稿


文档修订历史

版本 日期 作者 修订内容
v1.0 2026-03-04 张翔 创建基础版详细设计

参考文档

  • 《健身房管理系统基础版产品设计文档》 GYM-PRD-BASIC-001
  • 《健身房管理系统基础版业务概要设计文档》 GYM-HLD-BASIC-001
  • Spring Boot 3 官方文档
  • R2DBC 规范文档
  • PostgreSQL 官方文档

一、系统架构设计

1.1 总体架构

采用分层架构 + 模块化设计:

┌─────────────────────────────────────────────────────────────────────────┐
│                          基础版总体架构                                │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │ 客户端层                                                 │   │
│  ├─────────────────────────────────────────────────────────────────┤   │
│  │ • 会员小程序 (uniapp+Vue3)                                │   │
│  │ • 教练端App (uniapp+Vue3)                                 │   │
│  │ • 管理后台PC (Vue3+Vite)                                  │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                              │                                      │
│                              ▼                                      │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │ API Gateway 统一网关                                      │   │
│  ├─────────────────────────────────────────────────────────────────┤   │
│  │ • 路由转发  • 认证鉴权  • 限流熔断  • 日志追踪           │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                              │                                      │
│                              ▼                                      │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │ 业务层                                                   │   │
│  ├─────────────────────────────────────────────────────────────────┤   │
│  │ • 会员服务 (Member Service)                                 │   │
│  │ • 预约服务 (Booking Service)                                │   │
│  │ • 签到服务 (CheckIn Service)                               │   │
│  │ • 数据服务 (Data Service)                                   │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                              │                                      │
│                              ▼                                      │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │ 公共服务层                                               │   │
│  ├─────────────────────────────────────────────────────────────────┤   │
│  │ • 认证服务  • 消息服务  • 文件服务  • 缓存服务            │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                              │                                      │
│                              ▼                                      │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │ 基础设施层                                             │   │
│  ├─────────────────────────────────────────────────────────────────┤   │
│  │ • PostgreSQL  • R2DBC  • Caffeine  • Redis(可选)          │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                              │                                      │
│                              ▼                                      │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │ 外部服务层                                               │   │
│  ├─────────────────────────────────────────────────────────────────┤   │
│  │ • 微信开放平台  • 短信服务  • 支付服务  • OSS存储        │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

1.2 技术架构

┌─────────────────────────────────────────────────────────────────────────┐
│                          技术架构                                     │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │ 前端技术栈                                               │   │
│  ├─────────────────────────────────────────────────────────────────┤   │
│  │ • uniapp (跨平台小程序)  • Vue3 (前端框架)                   │   │
│  │ • Vite (构建工具)  • TypeScript (类型安全)                   │   │
│  │ • Pinia (状态管理)  • Element Plus (UI组件库)                │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                                                                         │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │ 后端技术栈                                               │   │
│  ├─────────────────────────────────────────────────────────────────┤   │
│  │ • Spring Boot 3.2 (应用框架)  • Spring Security (安全框架)     │   │
│  │ • R2DBC (响应式数据库)  • Spring WebFlux (响应式Web)          │   │
│  │ • Caffeine (本地缓存)  • Redis (分布式缓存)                  │   │
│  │ • PostgreSQL (关系型数据库)  • MyBatis (ORM框架)              │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                                                                         │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │ 部署架构                                               │   │
│  ├─────────────────────────────────────────────────────────────────┤   │
│  │ • Docker (容器化)  • Kubernetes (容器编排)                    │   │
│  │ • Nginx (反向代理)  • ELK (日志收集)                         │   │
│  │ • Prometheus (监控)  • Grafana (可视化)                       │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

二、模块设计

2.1 会员模块

2.1.1 模块概述

会员模块是基础版的核心基础模块,负责管理会员全生命周期,包括:

  • 会员注册与信息管理
  • 会员卡购买与管理
  • 会员权益(时长/次数/储值/等级)管理

2.1.2 数据模型设计

会员表 (member)

CREATE TABLE member (
    id              BIGSERIAL       PRIMARY KEY,
    tenant_id       BIGINT          NOT NULL,
    store_id        BIGINT          NOT NULL,
    member_no       VARCHAR(32)     NOT NULL,
    name            VARCHAR(64),
    phone           VARCHAR(64)     NOT NULL,
    phone_mask      VARCHAR(20),
    avatar          VARCHAR(512),
    gender          SMALLINT,
    birthday        DATE,
    height          INT,
    weight          DECIMAL(5,2),
    fitness_goal    VARCHAR(64),
    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_member_tenant_no UNIQUE (tenant_id, member_no),
    CONSTRAINT uk_member_phone UNIQUE (phone),
    CONSTRAINT fk_member_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id),
    CONSTRAINT fk_member_store FOREIGN KEY (store_id) REFERENCES store(id)
);

会员卡表 (member_card)

CREATE TABLE member_card (
    id              BIGSERIAL       PRIMARY KEY,
    tenant_id       BIGINT          NOT NULL,
    store_id        BIGINT          NOT NULL,
    member_id       BIGINT          NOT NULL,
    card_no         VARCHAR(32)     NOT NULL,
    card_type       SMALLINT        NOT NULL,
    card_name       VARCHAR(64)     NOT NULL,
    total_amount    DECIMAL(10,2),
    balance         DECIMAL(10,2),
    total_count     INT,
    balance_count   INT,
    valid_days      INT,
    valid_from      DATE,
    valid_to        DATE,
    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_member_card_no UNIQUE (card_no),
    CONSTRAINT fk_member_card_member FOREIGN KEY (member_id) REFERENCES member(id)
);

2.1.3 核心服务设计

会员注册服务

@Service
public class MemberRegistrationService {
    
    @Autowired
    private MemberRepository memberRepository;
    
    @Autowired
    private SmsService smsService;
    
    @Autowired
    private MemberCardService memberCardService;
    
    /**
     * 会员注册
     */
    @Transactional
    public Member register(MemberRegistrationRequest request) {
        validatePhone(request.getPhone());
        validateSmsCode(request.getPhone(), request.getSmsCode());
        
        Member member = new Member();
        member.setTenantId(request.getTenantId());
        member.setStoreId(request.getStoreId());
        member.setMemberNo(generateMemberNo(request.getTenantId()));
        member.setName(request.getName());
        member.setPhone(encryptPhone(request.getPhone()));
        member.setPhoneMask(maskPhone(request.getPhone()));
        member.setGender(request.getGender());
        member.setBirthday(request.getBirthday());
        member.setHeight(request.getHeight());
        member.setWeight(request.getWeight());
        member.setFitnessGoal(request.getFitnessGoal());
        member.setStatus(1);
        
        return memberRepository.save(member);
    }
    
    /**
     * 验证手机号
     */
    private void validatePhone(String phone) {
        if (!memberRepository.findByPhone(phone).isEmpty()) {
            throw new BusinessException("手机号已存在");
        }
    }
    
    /**
     * 验证短信验证码
     */
    private void validateSmsCode(String phone, String smsCode) {
        if (!smsService.verifySmsCode(phone, smsCode)) {
            throw new BusinessException("验证码错误");
        }
    }
    
    /**
     * 生成会员号
     */
    private String generateMemberNo(Long tenantId) {
        String prefix = "M" + tenantId;
        String timestamp = String.valueOf(System.currentTimeMillis());
        String random = String.valueOf(new Random().nextInt(1000));
        return prefix + timestamp.substring(timestamp.length() - 8) + random;
    }
    
    /**
     * 加密手机号
     */
    private String encryptPhone(String phone) {
        return AESUtil.encrypt(phone);
    }
    
    /**
     * 脱敏手机号
     */
    private String maskPhone(String phone) {
        return phone.substring(0, 3) + "****" + phone.substring(7);
    }
}

会员卡购买服务

@Service
public class MemberCardPurchaseService {
    
    @Autowired
    private MemberCardRepository memberCardRepository;
    
    @Autowired
    private PaymentService paymentService;
    
    @Autowired
    private BenefitService benefitService;
    
    /**
     * 购买会员卡
     */
    @Transactional
    public MemberCard purchase(MemberCardPurchaseRequest request) {
        Member member = memberRepository.findById(request.getMemberId())
            .orElseThrow(() -> new BusinessException("会员不存在"));
        
        Payment payment = paymentService.createPayment(request);
        
        MemberCard memberCard = new MemberCard();
        memberCard.setTenantId(member.getTenantId());
        memberCard.setStoreId(member.getStoreId());
        memberCard.setMemberId(member.getId());
        memberCard.setCardNo(generateCardNo(member.getTenantId()));
        memberCard.setCardType(request.getCardType());
        memberCard.setCardName(request.getCardName());
        memberCard.setTotalAmount(request.getAmount());
        memberCard.setBalance(request.getAmount());
        memberCard.setTotalCount(request.getCount());
        memberCard.setBalanceCount(request.getCount());
        memberCard.setValidDays(request.getValidDays());
        memberCard.setValidFrom(LocalDate.now());
        memberCard.setValidTo(LocalDate.now().plusDays(request.getValidDays()));
        memberCard.setStatus(1);
        
        memberCard = memberCardRepository.save(memberCard);
        
        benefitService.createBenefits(memberCard);
        
        return memberCard;
    }
    
    /**
     * 生成卡号
     */
    private String generateCardNo(Long tenantId) {
        String prefix = "C" + tenantId;
        String timestamp = String.valueOf(System.currentTimeMillis());
        String random = String.valueOf(new Random().nextInt(1000));
        return prefix + timestamp.substring(timestamp.length() - 8) + random;
    }
}

2.2 预约模块

2.2.1 模块概述

预约模块是基础版的核心业务模块,负责管理团课预约,包括:

  • 团课管理
  • 团课预约
  • 预约取消

2.2.2 数据模型设计

课程表 (course)

CREATE TABLE course (
    id              BIGSERIAL       PRIMARY KEY,
    tenant_id       BIGINT          NOT NULL,
    name            VARCHAR(128)    NOT NULL,
    code            VARCHAR(32),
    type            SMALLINT        NOT NULL,
    category        VARCHAR(64),
    description     TEXT,
    cover_image     VARCHAR(512),
    duration        INT             NOT NULL,
    capacity        INT             DEFAULT 20,
    min_capacity    INT             DEFAULT 1,
    difficulty      SMALLINT        DEFAULT 1,
    calories        INT,
    equipment       VARCHAR(256),
    benefits        JSONB,
    price           DECIMAL(10,2),
    price_type      SMALLINT        DEFAULT 1,
    price_value     DECIMAL(10,2),
    advance_days    INT             DEFAULT 7,
    cancel_hours    INT             DEFAULT 2,
    cancel_penalty  DECIMAL(3,2)    DEFAULT 0.00,
    status          SMALLINT        DEFAULT 1,
    sort_order      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 fk_course_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id)
);

课程时段表 (course_slot)

CREATE TABLE course_slot (
    id              BIGSERIAL       PRIMARY KEY,
    tenant_id       BIGINT          NOT NULL,
    store_id        BIGINT          NOT NULL,
    course_id       BIGINT          NOT NULL,
    coach_id        BIGINT,
    slot_date       DATE            NOT NULL,
    start_time      TIME            NOT NULL,
    end_time        TIME            NOT NULL,
    capacity        INT             DEFAULT 20,
    booked_count    INT             DEFAULT 0,
    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_course_slot_course FOREIGN KEY (course_id) REFERENCES course(id),
    CONSTRAINT fk_course_slot_coach FOREIGN KEY (coach_id) REFERENCES coach(id)
);

预约记录表 (booking_record)

CREATE TABLE booking_record (
    id              BIGSERIAL       PRIMARY KEY,
    tenant_id       BIGINT          NOT NULL,
    store_id        BIGINT          NOT NULL,
    member_id       BIGINT          NOT NULL,
    course_id       BIGINT          NOT NULL,
    slot_id         BIGINT          NOT NULL,
    booking_no      VARCHAR(32)     NOT NULL,
    status          SMALLINT        DEFAULT 1,
    booked_at       TIMESTAMP       DEFAULT NOW(),
    cancelled_at    TIMESTAMP,
    cancel_reason   VARCHAR(256),
    created_at      TIMESTAMP       DEFAULT NOW(),
    updated_at      TIMESTAMP       DEFAULT NOW(),
    created_by      BIGINT,
    updated_by      BIGINT,
    deleted_at      TIMESTAMP       DEFAULT NULL,
    
    CONSTRAINT uk_booking_no UNIQUE (booking_no),
    CONSTRAINT fk_booking_member FOREIGN KEY (member_id) REFERENCES member(id),
    CONSTRAINT fk_booking_slot FOREIGN KEY (slot_id) REFERENCES course_slot(id)
);

2.2.3 核心服务设计

团课预约服务

@Service
public class CourseBookingService {
    
    @Autowired
    private BookingRecordRepository bookingRecordRepository;
    
    @Autowired
    private CourseSlotRepository courseSlotRepository;
    
    @Autowired
    private BenefitService benefitService;
    
    @Autowired
    private MessageService messageService;
    
    /**
     * 预约团课
     */
    @Transactional
    public BookingRecord book(CourseBookingRequest request) {
        validateBookingTime(request.getSlotId());
        validateCapacity(request.getSlotId());
        validateMemberBenefit(request.getMemberId(), request.getSlotId());
        
        CourseSlot slot = courseSlotRepository.findById(request.getSlotId())
            .orElseThrow(() -> new BusinessException("课程时段不存在"));
        
        BookingRecord record = new BookingRecord();
        record.setTenantId(slot.getTenantId());
        record.setStoreId(slot.getStoreId());
        record.setMemberId(request.getMemberId());
        record.setCourseId(slot.getCourseId());
        record.setSlotId(slot.getId());
        record.setBookingNo(generateBookingNo(slot.getTenantId()));
        record.setStatus(1);
        
        record = bookingRecordRepository.save(record);
        
        slot.setBookedCount(slot.getBookedCount() + 1);
        courseSlotRepository.save(slot);
        
        benefitService.deductBenefit(request.getMemberId(), slot.getCourseId());
        
        messageService.sendBookingNotification(record);
        
        return record;
    }
    
    /**
     * 验证预约时间
     */
    private void validateBookingTime(Long slotId) {
        CourseSlot slot = courseSlotRepository.findById(slotId)
            .orElseThrow(() -> new BusinessException("课程时段不存在"));
        
        LocalDateTime slotDateTime = LocalDateTime.of(slot.getSlotDate(), slot.getStartTime());
        if (slotDateTime.isBefore(LocalDateTime.now().plusMinutes(30))) {
            throw new BusinessException("预约时间过短");
        }
    }
    
    /**
     * 验证容量
     */
    private void validateCapacity(Long slotId) {
        CourseSlot slot = courseSlotRepository.findById(slotId)
            .orElseThrow(() -> new BusinessException("课程时段不存在"));
        
        if (slot.getBookedCount() >= slot.getCapacity()) {
            throw new BusinessException("课程已满");
        }
    }
    
    /**
     * 验证会员权益
     */
    private void validateMemberBenefit(Long memberId, Long slotId) {
        if (!benefitService.hasBenefit(memberId, slotId)) {
            throw new BusinessException("会员权益不足");
        }
    }
    
    /**
     * 生成预约号
     */
    private String generateBookingNo(Long tenantId) {
        String prefix = "B" + tenantId;
        String timestamp = String.valueOf(System.currentTimeMillis());
        String random = String.valueOf(new Random().nextInt(1000));
        return prefix + timestamp.substring(timestamp.length() - 8) + random;
    }
}

2.3 签到模块

2.3.1 模块概述

签到模块是基础版的核心业务模块,负责管理会员的入场签到,支持:

  • 二维码签到
  • 签到记录管理

2.3.2 数据模型设计

签到记录表 (checkin_record)

CREATE TABLE checkin_record (
    id              BIGSERIAL       PRIMARY KEY,
    tenant_id       BIGINT          NOT NULL,
    store_id        BIGINT          NOT NULL,
    member_id       BIGINT          NOT NULL,
    booking_id      BIGINT,
    type            SMALLINT        NOT NULL,
    method          SMALLINT        NOT NULL,
    status          SMALLINT        DEFAULT 1,
    checkin_at      TIMESTAMP       NOT NULL,
    checkin_date    DATE            NOT NULL,
    created_at      TIMESTAMP       DEFAULT NOW(),
    updated_at      TIMESTAMP       DEFAULT NOW(),
    created_by      BIGINT,
    updated_by      BIGINT,
    deleted_at      TIMESTAMP       DEFAULT NULL,
    
    CONSTRAINT fk_checkin_member FOREIGN KEY (member_id) REFERENCES member(id),
    CONSTRAINT fk_checkin_booking FOREIGN KEY (booking_id) REFERENCES booking_record(id)
);

2.3.3 核心服务设计

扫码签到服务

@Service
public class QRCodeCheckInService {
    
    @Autowired
    private CheckInRecordRepository checkInRecordRepository;
    
    @Autowired
    private MemberRepository memberRepository;
    
    @Autowired
    private MemberCardRepository memberCardRepository;
    
    /**
     * 扫码签到
     */
    @Transactional
    public CheckInRecord checkIn(QRCodeCheckInRequest request) {
        Member member = memberRepository.findByQrCode(request.getQrCode())
            .orElseThrow(() -> new BusinessException("会员不存在"));
        
        validateMemberCard(member.getId());
        
        CheckInRecord record = new CheckInRecord();
        record.setTenantId(member.getTenantId());
        record.setStoreId(member.getStoreId());
        record.setMemberId(member.getId());
        record.setType(1);
        record.setMethod(1);
        record.setStatus(1);
        record.setCheckInAt(LocalDateTime.now());
        record.setCheckInDate(LocalDate.now());
        
        return checkInRecordRepository.save(record);
    }
    
    /**
     * 验证会员卡
     */
    private void validateMemberCard(Long memberId) {
        List<MemberCard> cards = memberCardRepository.findByMemberId(memberId);
        if (cards.isEmpty()) {
            throw new BusinessException("会员卡不存在");
        }
        
        boolean hasValidCard = cards.stream()
            .anyMatch(card -> card.getStatus() == 1 && 
                (card.getValidTo() == null || card.getValidTo().isAfter(LocalDate.now())));
        
        if (!hasValidCard) {
            throw new BusinessException("会员卡无效");
        }
    }
}

2.4 数据统计模块

2.4.1 模块概述

数据统计模块是基础版的核心业务模块,负责提供基础数据统计,包括:

  • 会员数据统计
  • 预约数据统计
  • 签到数据统计

2.4.2 核心服务设计

数据统计服务

@Service
public class DataStatisticsService {
    
    @Autowired
    private MemberRepository memberRepository;
    
    @Autowired
    private BookingRecordRepository bookingRecordRepository;
    
    @Autowired
    private CheckInRecordRepository checkInRecordRepository;
    
    /**
     * 获取会员数据统计
     */
    public MemberStatistics getMemberStatistics(Long tenantId, Long storeId, LocalDate startDate, LocalDate endDate) {
        long totalMembers = memberRepository.countByTenantIdAndStoreIdAndCreatedAtBetween(
            tenantId, storeId, startDate.atStartOfDay(), endDate.atTime(23, 59, 59));
        
        long activeMembers = memberRepository.countActiveMembers(tenantId, storeId, startDate, endDate);
        
        MemberStatistics statistics = new MemberStatistics();
        statistics.setTotalMembers(totalMembers);
        statistics.setActiveMembers(activeMembers);
        statistics.setNewMembers(totalMembers);
        
        return statistics;
    }
    
    /**
     * 获取预约数据统计
     */
    public BookingStatistics getBookingStatistics(Long tenantId, Long storeId, LocalDate startDate, LocalDate endDate) {
        long totalBookings = bookingRecordRepository.countByTenantIdAndStoreIdAndBookedAtBetween(
            tenantId, storeId, startDate.atStartOfDay(), endDate.atTime(23, 59, 59));
        
        long cancelledBookings = bookingRecordRepository.countCancelledBookings(tenantId, storeId, startDate, endDate);
        
        BookingStatistics statistics = new BookingStatistics();
        statistics.setTotalBookings(totalBookings);
        statistics.setCancelledBookings(cancelledBookings);
        statistics.setSuccessBookings(totalBookings - cancelledBookings);
        
        return statistics;
    }
    
    /**
     * 获取签到数据统计
     */
    public CheckInStatistics getCheckInStatistics(Long tenantId, Long storeId, LocalDate startDate, LocalDate endDate) {
        long totalCheckIns = checkInRecordRepository.countByTenantIdAndStoreIdAndCheckInAtBetween(
            tenantId, storeId, startDate.atStartOfDay(), endDate.atTime(23, 59, 59));
        
        CheckInStatistics statistics = new CheckInStatistics();
        statistics.setTotalCheckIns(totalCheckIns);
        
        return statistics;
    }
}

2.5 系统管理模块

2.5.1 模块概述

系统管理模块是基础版的核心基础模块,负责管理系统用户和权限,包括:

  • 用户管理
  • 角色权限管理

2.5.2 数据模型设计

用户表 (user)

CREATE TABLE user (
    id              BIGSERIAL       PRIMARY KEY,
    tenant_id       BIGINT          NOT NULL,
    store_id        BIGINT,
    username        VARCHAR(64)     NOT NULL,
    password        VARCHAR(256)    NOT NULL,
    name            VARCHAR(64)     NOT NULL,
    phone           VARCHAR(64),
    email           VARCHAR(128),
    avatar          VARCHAR(512),
    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_user_username UNIQUE (username),
    CONSTRAINT fk_user_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id),
    CONSTRAINT fk_user_store FOREIGN KEY (store_id) REFERENCES store(id)
);

角色表 (role)

CREATE TABLE role (
    id              BIGSERIAL       PRIMARY KEY,
    tenant_id       BIGINT          NOT NULL,
    name            VARCHAR(64)     NOT NULL,
    code            VARCHAR(32)     NOT NULL,
    description     VARCHAR(256),
    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_role_code UNIQUE (code),
    CONSTRAINT fk_role_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id)
);

用户角色关联表 (user_role)

CREATE TABLE user_role (
    id              BIGSERIAL       PRIMARY KEY,
    user_id         BIGINT          NOT NULL,
    role_id         BIGINT          NOT NULL,
    created_at      TIMESTAMP       DEFAULT NOW(),
    created_by      BIGINT,
    
    CONSTRAINT uk_user_role UNIQUE (user_id, role_id),
    CONSTRAINT fk_user_role_user FOREIGN KEY (user_id) REFERENCES user(id),
    CONSTRAINT fk_user_role_role FOREIGN KEY (role_id) REFERENCES role(id)
);

2.5.3 核心服务设计

用户管理服务

@Service
public class UserService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private UserRoleRepository userRoleRepository;
    
    @Autowired
    private PasswordEncoder passwordEncoder;
    
    /**
     * 创建用户
     */
    @Transactional
    public User createUser(UserCreateRequest request) {
        validateUsername(request.getUsername());
        
        User user = new User();
        user.setTenantId(request.getTenantId());
        user.setStoreId(request.getStoreId());
        user.setUsername(request.getUsername());
        user.setPassword(passwordEncoder.encode(request.getPassword()));
        user.setName(request.getName());
        user.setPhone(request.getPhone());
        user.setEmail(request.getEmail());
        user.setStatus(1);
        
        user = userRepository.save(user);
        
        assignRoles(user.getId(), request.getRoleIds());
        
        return user;
    }
    
    /**
     * 验证用户名
     */
    private void validateUsername(String username) {
        if (userRepository.findByUsername(username).isPresent()) {
            throw new BusinessException("用户名已存在");
        }
    }
    
    /**
     * 分配角色
     */
    private void assignRoles(Long userId, List<Long> roleIds) {
        if (roleIds == null || roleIds.isEmpty()) {
            return;
        }
        
        List<UserRole> userRoles = roleIds.stream()
            .map(roleId -> {
                UserRole userRole = new UserRole();
                userRole.setUserId(userId);
                userRole.setRoleId(roleId);
                return userRole;
            })
            .collect(Collectors.toList());
        
        userRoleRepository.saveAll(userRoles);
    }
}

2.6 UI模版定制模块

2.6.1 模块概述

UI模版定制模块是基础版的核心基础模块,负责管理租户的UI定制配置,包括:

  • 品牌定制(Logo、颜色、背景图等)
  • 布局调整(模块顺序、模块隐藏等)
  • 预设模板(模板选择、模板应用等)
  • 配置历史(配置回滚、配置对比等)

2.6.2 数据模型设计

租户UI配置表 (tenant_ui_config)

CREATE TABLE tenant_ui_config (
    id              BIGSERIAL       PRIMARY KEY,
    tenant_id       BIGINT          NOT NULL,
    version         INT             NOT NULL DEFAULT 1,
    brand_config    JSONB           NOT NULL DEFAULT '{}',
    layout_config   JSONB           NOT NULL DEFAULT '{}',
    template_id     BIGINT,
    is_active       BOOLEAN         DEFAULT TRUE,
    created_at      TIMESTAMP       DEFAULT NOW(),
    updated_at      TIMESTAMP       DEFAULT NOW(),
    created_by      BIGINT,
    updated_by      BIGINT,
    deleted_at      TIMESTAMP       DEFAULT NULL,
    
    CONSTRAINT uk_tenant_ui_config UNIQUE (tenant_id, version),
    CONSTRAINT fk_tenant_ui_config_template FOREIGN KEY (template_id) REFERENCES ui_template(id)
);

CREATE INDEX idx_tenant_ui_config_tenant ON tenant_ui_config(tenant_id);
CREATE INDEX idx_tenant_ui_config_is_active ON tenant_ui_config(is_active);

预设模板表 (ui_template)

CREATE TABLE ui_template (
    id              BIGSERIAL       PRIMARY KEY,
    name            VARCHAR(128)    NOT NULL,
    code            VARCHAR(64)     NOT NULL,
    type            VARCHAR(32)     NOT NULL,
    description     VARCHAR(512),
    thumbnail       VARCHAR(512),
    preview_image   VARCHAR(512),
    config          JSONB           NOT NULL DEFAULT '{}',
    status          SMALLINT        DEFAULT 1,
    sort_order      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_ui_template_code UNIQUE (code)
);

CREATE INDEX idx_ui_template_type ON ui_template(type);
CREATE INDEX idx_ui_template_status ON ui_template(status);

配置历史表 (ui_config_history)

CREATE TABLE ui_config_history (
    id              BIGSERIAL       PRIMARY KEY,
    tenant_id       BIGINT          NOT NULL,
    config_id       BIGINT          NOT NULL,
    version         INT             NOT NULL,
    brand_config    JSONB           NOT NULL DEFAULT '{}',
    layout_config   JSONB           NOT NULL DEFAULT '{}',
    template_id     BIGINT,
    change_reason   VARCHAR(512),
    created_at      TIMESTAMP       DEFAULT NOW(),
    created_by      BIGINT,
    
    CONSTRAINT fk_ui_config_history_config FOREIGN KEY (config_id) REFERENCES tenant_ui_config(id),
    CONSTRAINT fk_ui_config_history_template FOREIGN KEY (template_id) REFERENCES ui_template(id)
);

CREATE INDEX idx_ui_config_history_tenant ON ui_config_history(tenant_id);
CREATE INDEX idx_ui_config_history_config ON ui_config_history(config_id);

资源文件表 (ui_resource)

CREATE TABLE ui_resource (
    id              BIGSERIAL       PRIMARY KEY,
    tenant_id       BIGINT          NOT NULL,
    resource_type   VARCHAR(32)     NOT NULL,
    resource_name   VARCHAR(128)    NOT NULL,
    file_path       VARCHAR(512)    NOT NULL,
    file_size       BIGINT,
    file_type       VARCHAR(32),
    width           INT,
    height          INT,
    created_at      TIMESTAMP       DEFAULT NOW(),
    created_by      BIGINT,
    deleted_at      TIMESTAMP       DEFAULT NULL,
    
    CONSTRAINT uk_ui_resource UNIQUE (tenant_id, resource_type, resource_name)
);

CREATE INDEX idx_ui_resource_tenant ON ui_resource(tenant_id);
CREATE INDEX idx_ui_resource_type ON ui_resource(resource_type);

2.6.3 核心业务逻辑

品牌配置Service

@Service
public class BrandConfigService {
    
    @Autowired
    private TenantUiConfigRepository tenantUiConfigRepository;
    
    @Autowired
    private UiResourceRepository uiResourceRepository;
    
    @Autowired
    private FileStorageService fileStorageService;
    
    /**
     * 上传Logo
     */
    @Transactional
    public UiResource uploadLogo(Long tenantId, MultipartFile file, Long userId) {
        validateLogoFile(file);
        
        String filePath = fileStorageService.upload(file);
        
        UiResource resource = new UiResource();
        resource.setTenantId(tenantId);
        resource.setResourceType("logo");
        resource.setResourceName(file.getOriginalFilename());
        resource.setFilePath(filePath);
        resource.setFileSize(file.getSize());
        resource.setFileType(file.getContentType());
        
        BufferedImage image = ImageIO.read(file.getInputStream());
        resource.setWidth(image.getWidth());
        resource.setHeight(image.getHeight());
        
        resource = uiResourceRepository.save(resource);
        
        updateBrandConfig(tenantId, "logo", filePath, userId);
        
        return resource;
    }
    
    /**
     * 验证Logo文件
     */
    private void validateLogoFile(MultipartFile file) {
        if (file.getSize() > 2 * 1024 * 1024) {
            throw new BusinessException("Logo文件大小不能超过2MB");
        }
        
        String contentType = file.getContentType();
        if (!"image/png".equals(contentType) && !"image/jpeg".equals(contentType)) {
            throw new BusinessException("Logo文件格式只支持PNG或JPG");
        }
    }
    
    /**
     * 更新品牌配置
     */
    @Transactional
    public void updateBrandConfig(Long tenantId, String key, String value, Long userId) {
        TenantUiConfig config = tenantUiConfigRepository.findActiveByTenantId(tenantId)
            .orElseThrow(() -> new BusinessException("租户配置不存在"));
        
        JsonObject brandConfig = config.getBrandConfig();
        brandConfig.addProperty(key, value);
        
        config.setBrandConfig(brandConfig);
        config.setUpdatedBy(userId);
        
        tenantUiConfigRepository.save(config);
    }
    
    /**
     * 设置品牌颜色
     */
    @Transactional
    public void setBrandColor(Long tenantId, String colorType, String color, Long userId) {
        validateColorFormat(color);
        
        TenantUiConfig config = tenantUiConfigRepository.findActiveByTenantId(tenantId)
            .orElseThrow(() -> new BusinessException("租户配置不存在"));
        
        JsonObject brandConfig = config.getBrandConfig();
        brandConfig.addProperty(colorType, color);
        
        config.setBrandConfig(brandConfig);
        config.setUpdatedBy(userId);
        
        tenantUiConfigRepository.save(config);
    }
    
    /**
     * 验证颜色格式
     */
    private void validateColorFormat(String color) {
        if (color.matches("^#[0-9A-Fa-f]{6}$")) {
            return;
        }
        
        if (color.matches("^rgb\\(\\d{1,3},\\s*\\d{1,3},\\s*\\d{1,3}\\)$")) {
            return;
        }
        
        throw new BusinessException("颜色格式不正确,请使用HEX或RGB格式");
    }
}

布局配置Service

@Service
public class LayoutConfigService {
    
    @Autowired
    private TenantUiConfigRepository tenantUiConfigRepository;
    
    /**
     * 更新模块顺序
     */
    @Transactional
    public void updateModuleOrder(Long tenantId, List<String> moduleOrder, Long userId) {
        TenantUiConfig config = tenantUiConfigRepository.findActiveByTenantId(tenantId)
            .orElseThrow(() -> new BusinessException("租户配置不存在"));
        
        JsonObject layoutConfig = config.getLayoutConfig();
        JsonArray moduleOrderArray = new JsonArray();
        moduleOrder.forEach(moduleOrderArray::add);
        
        layoutConfig.add("moduleOrder", moduleOrderArray);
        
        config.setLayoutConfig(layoutConfig);
        config.setUpdatedBy(userId);
        
        tenantUiConfigRepository.save(config);
    }
    
    /**
     * 隐藏/显示模块
     */
    @Transactional
    public void toggleModuleVisibility(Long tenantId, String moduleCode, boolean visible, Long userId) {
        TenantUiConfig config = tenantUiConfigRepository.findActiveByTenantId(tenantId)
            .orElseThrow(() -> new BusinessException("租户配置不存在"));
        
        JsonObject layoutConfig = config.getLayoutConfig();
        JsonArray hiddenModules = layoutConfig.has("hiddenModules") 
            ? layoutConfig.getAsJsonArray("hiddenModules") 
            : new JsonArray();
        
        if (visible) {
            hiddenModules.removeIf(element -> element.getAsString().equals(moduleCode));
        } else {
            if (!hiddenModules.contains(new JsonPrimitive(moduleCode))) {
                hiddenModules.add(moduleCode);
            }
        }
        
        layoutConfig.add("hiddenModules", hiddenModules);
        
        config.setLayoutConfig(layoutConfig);
        config.setUpdatedBy(userId);
        
        tenantUiConfigRepository.save(config);
    }
    
    /**
     * 设置首页布局类型
     */
    @Transactional
    public void setHomeLayoutType(Long tenantId, String layoutType, Long userId) {
        TenantUiConfig config = tenantUiConfigRepository.findActiveByTenantId(tenantId)
            .orElseThrow(() -> new BusinessException("租户配置不存在"));
        
        JsonObject layoutConfig = config.getLayoutConfig();
        layoutConfig.addProperty("homeLayoutType", layoutType);
        
        config.setLayoutConfig(layoutConfig);
        config.setUpdatedBy(userId);
        
        tenantUiConfigRepository.save(config);
    }
}

模板管理Service

@Service
public class TemplateService {
    
    @Autowired
    private UiTemplateRepository uiTemplateRepository;
    
    @Autowired
    private TenantUiConfigRepository tenantUiConfigRepository;
    
    @Autowired
    private UiConfigHistoryRepository uiConfigHistoryRepository;
    
    /**
     * 获取所有可用模板
     */
    public List<UiTemplate> getAvailableTemplates() {
        return uiTemplateRepository.findByStatus(1);
    }
    
    /**
     * 应用模板
     */
    @Transactional
    public void applyTemplate(Long tenantId, Long templateId, Long userId) {
        UiTemplate template = uiTemplateRepository.findById(templateId)
            .orElseThrow(() -> new BusinessException("模板不存在"));
        
        if (template.getStatus() != 1) {
            throw new BusinessException("模板不可用");
        }
        
        TenantUiConfig config = tenantUiConfigRepository.findActiveByTenantId(tenantId)
            .orElseThrow(() -> new BusinessException("租户配置不存在"));
        
        saveConfigToHistory(config);
        
        JsonObject templateConfig = template.getConfig();
        JsonObject layoutConfig = templateConfig.getAsJsonObject("layoutConfig");
        
        config.setLayoutConfig(layoutConfig);
        config.setTemplateId(templateId);
        config.setUpdatedBy(userId);
        
        tenantUiConfigRepository.save(config);
    }
    
    /**
     * 保存配置到历史
     */
    private void saveConfigToHistory(TenantUiConfig config) {
        UiConfigHistory history = new UiConfigHistory();
        history.setTenantId(config.getTenantId());
        history.setConfigId(config.getId());
        history.setVersion(config.getVersion());
        history.setBrandConfig(config.getBrandConfig());
        history.setLayoutConfig(config.getLayoutConfig());
        history.setTemplateId(config.getTemplateId());
        
        uiConfigHistoryRepository.save(history);
    }
    
    /**
     * 获取模板详情
     */
    public UiTemplate getTemplateDetail(Long templateId) {
        return uiTemplateRepository.findById(templateId)
            .orElseThrow(() -> new BusinessException("模板不存在"));
    }
}

配置历史Service

@Service
public class ConfigHistoryService {
    
    @Autowired
    private UiConfigHistoryRepository uiConfigHistoryRepository;
    
    @Autowired
    private TenantUiConfigRepository tenantUiConfigRepository;
    
    /**
     * 获取配置历史列表
     */
    public List<UiConfigHistory> getConfigHistory(Long tenantId) {
        return uiConfigHistoryRepository.findByTenantIdOrderByCreatedAtDesc(tenantId);
    }
    
    /**
     * 回滚到历史版本
     */
    @Transactional
    public void rollbackToVersion(Long tenantId, Long historyId, Long userId) {
        UiConfigHistory history = uiConfigHistoryRepository.findById(historyId)
            .orElseThrow(() -> new BusinessException("历史版本不存在"));
        
        if (!history.getTenantId().equals(tenantId)) {
            throw new BusinessException("无权访问该历史版本");
        }
        
        TenantUiConfig config = tenantUiConfigRepository.findActiveByTenantId(tenantId)
            .orElseThrow(() -> new BusinessException("租户配置不存在"));
        
        config.setBrandConfig(history.getBrandConfig());
        config.setLayoutConfig(history.getLayoutConfig());
        config.setTemplateId(history.getTemplateId());
        config.setVersion(config.getVersion() + 1);
        config.setUpdatedBy(userId);
        
        tenantUiConfigRepository.save(config);
    }
    
    /**
     * 对比配置
     */
    public Map<String, Object> compareConfigs(Long tenantId, Long historyId) {
        TenantUiConfig currentConfig = tenantUiConfigRepository.findActiveByTenantId(tenantId)
            .orElseThrow(() -> new BusinessException("租户配置不存在"));
        
        UiConfigHistory historyConfig = uiConfigHistoryRepository.findById(historyId)
            .orElseThrow(() -> new BusinessException("历史版本不存在"));
        
        Map<String, Object> diff = new HashMap<>();
        diff.put("brandConfigDiff", compareJsonObjects(
            currentConfig.getBrandConfig(), 
            historyConfig.getBrandConfig()
        ));
        diff.put("layoutConfigDiff", compareJsonObjects(
            currentConfig.getLayoutConfig(), 
            historyConfig.getLayoutConfig()
        ));
        
        return diff;
    }
    
    /**
     * 比较JSON对象
     */
    private Map<String, Object> compareJsonObjects(JsonObject obj1, JsonObject obj2) {
        Map<String, Object> diff = new HashMap<>();
        
        Set<String> allKeys = new HashSet<>();
        obj1.keySet().forEach(allKeys::add);
        obj2.keySet().forEach(allKeys::add);
        
        for (String key : allKeys) {
            if (!obj1.has(key)) {
                diff.put(key, "新增: " + obj2.get(key));
            } else if (!obj2.has(key)) {
                diff.put(key, "删除: " + obj1.get(key));
            } else if (!obj1.get(key).equals(obj2.get(key))) {
                diff.put(key, "修改: " + obj1.get(key) + " -> " + obj2.get(key));
            }
        }
        
        return diff;
    }
}

三、API设计

3.1 会员模块API

3.1.1 会员注册

POST /api/v1/members/register

Request:
{
  "tenantId": 1,
  "storeId": 1,
  "phone": "13800138000",
  "smsCode": "123456",
  "name": "张三",
  "gender": 1,
  "birthday": "1990-01-01",
  "height": 175,
  "weight": 70.5,
  "fitnessGoal": "减脂"
}

Response:
{
  "code": 200,
  "message": "注册成功",
  "data": {
    "id": 1,
    "memberNo": "M10000000000000001",
    "name": "张三",
    "phoneMask": "138****8000",
    "status": 1
  }
}

3.1.2 购买会员卡

POST /api/v1/member-cards/purchase

Request:
{
  "memberId": 1,
  "cardType": 1,
  "cardName": "月卡",
  "amount": 299.00,
  "count": 30,
  "validDays": 30
}

Response:
{
  "code": 200,
  "message": "购买成功",
  "data": {
    "id": 1,
    "cardNo": "C10000000000000001",
    "cardName": "月卡",
    "balance": 299.00,
    "balanceCount": 30,
    "validFrom": "2026-03-04",
    "validTo": "2026-04-03",
    "status": 1
  }
}

3.2 预约模块API

3.2.1 预约团课

POST /api/v1/bookings/course

Request:
{
  "memberId": 1,
  "slotId": 1
}

Response:
{
  "code": 200,
  "message": "预约成功",
  "data": {
    "id": 1,
    "bookingNo": "B10000000000000001",
    "courseName": "瑜伽",
    "slotDate": "2026-03-05",
    "startTime": "10:00:00",
    "endTime": "11:00:00",
    "status": 1
  }
}

3.3 签到模块API

3.3.1 扫码签到

POST /api/v1/checkins/qrcode

Request:
{
  "qrCode": "M10000000000000001"
}

Response:
{
  "code": 200,
  "message": "签到成功",
  "data": {
    "id": 1,
    "memberName": "张三",
    "checkInAt": "2026-03-04T10:00:00",
    "status": 1
  }
}

3.4 数据统计模块API

3.4.1 获取数据统计

GET /api/v1/statistics/overview?tenantId=1&storeId=1&startDate=2026-03-01&endDate=2026-03-31

Response:
{
  "code": 200,
  "message": "查询成功",
  "data": {
    "memberStatistics": {
      "totalMembers": 100,
      "activeMembers": 80,
      "newMembers": 20
    },
    "bookingStatistics": {
      "totalBookings": 500,
      "cancelledBookings": 50,
      "successBookings": 450
    },
    "checkInStatistics": {
      "totalCheckIns": 800
    }
  }
}

3.5 UI模版定制模块API

POST /api/v1/ui-config/logo
Content-Type: multipart/form-data

Request:
{
  "file": <binary>,
  "tenantId": 1
}

Response:
{
  "code": 200,
  "message": "上传成功",
  "data": {
    "id": 1,
    "resourceType": "logo",
    "resourceName": "logo.png",
    "filePath": "/uploads/logo/1/logo.png",
    "fileSize": 102400,
    "fileType": "image/png",
    "width": 200,
    "height": 200,
    "createdAt": "2026-03-07T10:00:00Z"
  }
}

3.5.2 设置品牌颜色

POST /api/v1/ui-config/brand/color

Request:
{
  "tenantId": 1,
  "colorType": "primaryColor",
  "color": "#FF5733"
}

Response:
{
  "code": 200,
  "message": "设置成功",
  "data": {
    "primaryColor": "#FF5733",
    "secondaryColor": "#FFC300",
    "updatedAt": "2026-03-07T10:00:00Z"
  }
}

3.5.3 更新模块顺序

POST /api/v1/ui-config/layout/module-order

Request:
{
  "tenantId": 1,
  "moduleOrder": ["dashboard", "member", "booking", "checkin", "statistics"]
}

Response:
{
  "code": 200,
  "message": "更新成功",
  "data": {
    "moduleOrder": ["dashboard", "member", "booking", "checkin", "statistics"],
    "updatedAt": "2026-03-07T10:00:00Z"
  }
}

3.5.4 隐藏/显示模块

POST /api/v1/ui-config/layout/module-visibility

Request:
{
  "tenantId": 1,
  "moduleCode": "statistics",
  "visible": false
}

Response:
{
  "code": 200,
  "message": "更新成功",
  "data": {
    "hiddenModules": ["statistics"],
    "updatedAt": "2026-03-07T10:00:00Z"
  }
}

3.5.5 获取可用模板列表

GET /api/v1/ui-config/templates

Response:
{
  "code": 200,
  "message": "查询成功",
  "data": [
    {
      "id": 1,
      "name": "简约风格",
      "code": "simple",
      "type": "简约",
      "description": "简洁清爽的设计风格",
      "thumbnail": "/templates/simple/thumbnail.png",
      "previewImage": "/templates/simple/preview.png",
      "sortOrder": 1
    },
    {
      "id": 2,
      "name": "运动风格",
      "code": "sport",
      "type": "运动",
      "description": "活力四射的运动风格",
      "thumbnail": "/templates/sport/thumbnail.png",
      "previewImage": "/templates/sport/preview.png",
      "sortOrder": 2
    }
  ]
}

3.5.6 应用模板

POST /api/v1/ui-config/template/apply

Request:
{
  "tenantId": 1,
  "templateId": 1
}

Response:
{
  "code": 200,
  "message": "应用成功",
  "data": {
    "templateId": 1,
    "templateName": "简约风格",
    "appliedAt": "2026-03-07T10:00:00Z"
  }
}

3.5.7 获取配置历史

GET /api/v1/ui-config/history?tenantId=1

Response:
{
  "code": 200,
  "message": "查询成功",
  "data": [
    {
      "id": 1,
      "version": 1,
      "templateId": 1,
      "changeReason": "应用简约风格模板",
      "createdAt": "2026-03-07T10:00:00Z",
      "createdBy": 1
    },
    {
      "id": 2,
      "version": 2,
      "templateId": 2,
      "changeReason": "切换到运动风格模板",
      "createdAt": "2026-03-07T11:00:00Z",
      "createdBy": 1
    }
  ]
}

3.5.8 回滚到历史版本

POST /api/v1/ui-config/history/rollback

Request:
{
  "tenantId": 1,
  "historyId": 1
}

Response:
{
  "code": 200,
  "message": "回滚成功",
  "data": {
    "version": 3,
    "rolledBackFrom": 1,
    "rolledBackAt": "2026-03-07T12:00:00Z"
  }
}

3.5.9 对比配置

GET /api/v1/ui-config/history/compare?tenantId=1&historyId=1

Response:
{
  "code": 200,
  "message": "查询成功",
  "data": {
    "brandConfigDiff": {
      "primaryColor": "修改: #FF5733 -> #FFC300",
      "logo": "删除: /uploads/logo/1/logo.png"
    },
    "layoutConfigDiff": {
      "moduleOrder": "修改: [\"dashboard\", \"member\"] -> [\"member\", \"dashboard\"]",
      "homeLayoutType": "新增: card"
    }
  }
}

3.5.10 获取当前配置

GET /api/v1/ui-config/current?tenantId=1

Response:
{
  "code": 200,
  "message": "查询成功",
  "data": {
    "id": 1,
    "tenantId": 1,
    "version": 3,
    "brandConfig": {
      "logo": "/uploads/logo/1/logo.png",
      "primaryColor": "#FF5733",
      "secondaryColor": "#FFC300",
      "brandName": "我的健身房",
      "slogan": "健康生活,从现在开始"
    },
    "layoutConfig": {
      "moduleOrder": ["dashboard", "member", "booking", "checkin"],
      "hiddenModules": ["statistics"],
      "homeLayoutType": "card"
    },
    "templateId": 1,
    "isActive": true,
    "updatedAt": "2026-03-07T10:00:00Z"
  }
}

四、缓存策略

4.1 缓存设计

┌─────────────────────────────────────────────────────────────────────────┐
│                          缓存策略                                     │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │ 本地缓存 (Caffeine)                                    │   │
│  ├─────────────────────────────────────────────────────────────────┤   │
│  │ • 会员信息缓存 (TTL: 30分钟)                                  │   │
│  │ • 会员卡缓存 (TTL: 30分钟)                                   │   │
│  │ • 课程信息缓存 (TTL: 1小时)                                   │   │
│  │ • 课程时段缓存 (TTL: 30分钟)                                 │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                                                                         │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │ 分布式缓存 (Redis)                                    │   │
│  ├─────────────────────────────────────────────────────────────────┤   │
│  │ • 验证码缓存 (TTL: 5分钟)                                     │   │
│  │ • 令牌缓存 (TTL: 24小时)                                     │   │
│  │ • 限流计数器 (TTL: 1分钟)                                    │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

4.2 缓存实现

会员缓存服务

@Service
public class MemberCacheService {
    
    @Autowired
    private CacheManager cacheManager;
    
    private static final String MEMBER_CACHE = "member";
    private static final String MEMBER_CARD_CACHE = "memberCard";
    
    /**
     * 获取会员缓存
     */
    public Optional<Member> getMember(Long memberId) {
        Cache cache = cacheManager.getCache(MEMBER_CACHE);
        if (cache != null) {
            return Optional.ofNullable(cache.get(memberId, Member.class));
        }
        return Optional.empty();
    }
    
    /**
     * 设置会员缓存
     */
    public void setMember(Member member) {
        Cache cache = cacheManager.getCache(MEMBER_CACHE);
        if (cache != null) {
            cache.put(member.getId(), member);
        }
    }
    
    /**
     * 删除会员缓存
     */
    public void evictMember(Long memberId) {
        Cache cache = cacheManager.getCache(MEMBER_CACHE);
        if (cache != null) {
            cache.evict(memberId);
        }
    }
    
    /**
     * 获取会员卡缓存
     */
    public Optional<MemberCard> getMemberCard(Long cardId) {
        Cache cache = cacheManager.getCache(MEMBER_CARD_CACHE);
        if (cache != null) {
            return Optional.ofNullable(cache.get(cardId, MemberCard.class));
        }
        return Optional.empty();
    }
    
    /**
     * 设置会员卡缓存
     */
    public void setMemberCard(MemberCard card) {
        Cache cache = cacheManager.getCache(MEMBER_CARD_CACHE);
        if (cache != null) {
            cache.put(card.getId(), card);
        }
    }
    
    /**
     * 删除会员卡缓存
     */
    public void evictMemberCard(Long cardId) {
        Cache cache = cacheManager.getCache(MEMBER_CARD_CACHE);
        if (cache != null) {
            cache.evict(cardId);
        }
    }
}

五、异常处理

5.1 异常分类

┌─────────────────────────────────────────────────────────────────────────┐
│                          异常分类                                     │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │ 业务异常 (BusinessException)                          │   │
│  ├─────────────────────────────────────────────────────────────────┤   │
│  │ • 会员不存在  • 会员卡无效  • 预约失败  • 签到失败       │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                                                                         │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │ 参数异常 (ValidationException)                        │   │
│  ├─────────────────────────────────────────────────────────────────┤   │
│  │ • 参数为空  • 参数格式错误  • 参数超出范围               │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                                                                         │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │ 权限异常 (PermissionException)                          │   │
│  ├─────────────────────────────────────────────────────────────────┤   │
│  │ • 无权限  • 权限不足  • 令牌过期                   │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                                                                         │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │ 系统异常 (SystemException)                              │   │
│  ├─────────────────────────────────────────────────────────────────┤   │
│  │ • 数据库异常  • 网络异常  • 服务异常                 │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

5.2 异常处理实现

全局异常处理器

@RestControllerAdvice
public class GlobalExceptionHandler {
    
    /**
     * 业务异常
     */
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) {
        ErrorResponse response = new ErrorResponse();
        response.setCode(e.getCode());
        response.setMessage(e.getMessage());
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
    }
    
    /**
     * 参数异常
     */
    @ExceptionHandler(ValidationException.class)
    public ResponseEntity<ErrorResponse> handleValidationException(ValidationException e) {
        ErrorResponse response = new ErrorResponse();
        response.setCode(400);
        response.setMessage(e.getMessage());
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
    }
    
    /**
     * 权限异常
     */
    @ExceptionHandler(PermissionException.class)
    public ResponseEntity<ErrorResponse> handlePermissionException(PermissionException e) {
        ErrorResponse response = new ErrorResponse();
        response.setCode(403);
        response.setMessage(e.getMessage());
        return ResponseEntity.status(HttpStatus.FORBIDDEN).body(response);
    }
    
    /**
     * 系统异常
     */
    @ExceptionHandler(SystemException.class)
    public ResponseEntity<ErrorResponse> handleSystemException(SystemException e) {
        ErrorResponse response = new ErrorResponse();
        response.setCode(500);
        response.setMessage("系统异常");
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
    }
}

六、测试用例

6.1 会员模块测试用例

6.1.1 会员注册测试

测试用例 输入 预期输出
正常注册 手机号、验证码、姓名 注册成功
手机号已存在 已存在的手机号 提示手机号已存在
验证码错误 错误的验证码 提示验证码错误
验证码过期 过期的验证码 提示验证码过期

6.1.2 购买会员卡测试

测试用例 输入 预期输出
正常购买 会员ID、卡类型、金额 购买成功
会员不存在 不存在的会员ID 提示会员不存在
支付失败 支付失败 提示支付失败

6.2 预约模块测试用例

6.2.1 预约团课测试

测试用例 输入 预期输出
正常预约 会员ID、课程时段ID 预约成功
预约时间过短 课程开始前30分钟内 提示预约时间过短
课程已满 已满的课程时段 提示课程已满
权益不足 权益不足的会员 提示权益不足

6.3 签到模块测试用例

6.3.1 扫码签到测试

测试用例 输入 预期输出
正常签到 有效的二维码 签到成功
会员不存在 无效的二维码 提示会员不存在
会员卡无效 会员卡无效 提示会员卡无效

七、部署与运维

7.1 部署架构

┌─────────────────────────────────────────────────────────────────────────┐
│                          部署架构                                     │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │ 负载均衡 (Nginx)                                        │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                              │                                      │
│                              ▼                                      │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │ 应用服务器 (Kubernetes)                                 │   │
│  ├─────────────────────────────────────────────────────────────────┤   │
│  │ • Pod 1  • Pod 2  • Pod 3                               │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                              │                                      │
│                              ▼                                      │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │ 数据库 (PostgreSQL)                                   │   │
│  ├─────────────────────────────────────────────────────────────────┤   │
│  │ • 主库  • 从库                                          │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                              │                                      │
│                              ▼                                      │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │ 缓存 (Redis)                                           │   │
│  ├─────────────────────────────────────────────────────────────────┤   │
│  │ • 主节点  • 从节点                                      │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

7.2 监控指标

指标类型 指标名称 阈值
系统指标 CPU使用率 ≤ 80%
系统指标 内存使用率 ≤ 80%
系统指标 磁盘使用率 ≤ 80%
应用指标 API响应时间 ≤ 500ms
应用指标 错误率 ≤ 1%
应用指标 并发用户数 ≤ 100

八、附录

8.1 术语定义

术语 定义
会员 在健身房注册的用户
会员卡 会员购买的权益卡,包括时长卡、次卡、储值卡
权益 会员卡包含的时长、次数、储值、等级等权益
团课 集体课程,由教练带领多个会员一起上课
预约 会员预约团课
签到 会员到店记录

8.2 参考文档

  • 《健身房管理系统基础版产品设计文档》 GYM-PRD-BASIC-001
  • 《健身房管理系统基础版业务概要设计文档》 GYM-HLD-BASIC-001
  • Spring Boot 3 官方文档
  • R2DBC 规范文档
  • PostgreSQL 官方文档