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

54 KiB

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

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


文档修订历史

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

参考文档

  • 《健身房管理系统基础版产品设计文档》 GYM-PRD-BASIC-001
  • 《健身房管理系统基础版业务概要设计文档》 GYM-HLD-BASIC-001
  • 《健身房管理系统详细设计文档》 GYM-LLD-000
  • 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);
    }
}

三、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
    }
  }
}

四、缓存策略

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
  • 《健身房管理系统详细设计文档》 GYM-LLD-000
  • Spring Boot 3 官方文档
  • R2DBC 规范文档
  • PostgreSQL 官方文档