Files
campus-activity-system/server/docs/软件设计说明书.md
2026-01-19 23:15:20 +08:00

108 KiB
Raw Permalink Blame History

校园活动组织与报名系统 - 软件设计说明书

项目名称: 校园活动组织与报名系统

编写团队: 第14组

文档版本: V1.0

编写日期: 2026年1月15日

完成时间: 2025年1月14日


文档修订历史

版本 修订日期 修订人 修订说明
V1.0 2026-01-15 第14组 初始版本,完成软件设计

目录


第一章 引言

1.1 编写目的

本文档旨在详细描述"校园活动组织与报名系统"的软件设计方案,为开发人员提供明确的技术实现指导,为测试人员提供测试依据,为项目管理人员提供技术决策参考。

本文档的主要目标读者包括:

  • 开发人员:了解系统架构、模块设计、接口设计,指导代码实现
  • 测试人员:了解系统设计细节,制定测试计划和测试用例
  • 项目管理人员:了解技术架构和设计方案,进行技术决策和项目管理
  • 维护人员:了解系统设计原理,便于后续维护和升级

1.2 文档范围

本文档涵盖以下设计内容:

  • 系统架构设计:总体架构、技术架构、部署架构
  • 数据库设计:概念结构、逻辑结构、物理结构、完整性约束
  • 详细设计:各功能模块的类结构、算法设计、状态机设计
  • 接口设计所有RESTful API接口的详细设计
  • 安全设计:认证机制、权限控制、数据安全、接口安全
  • 性能设计:数据库优化、缓存策略、并发控制
  • 异常处理设计:异常分类、处理机制、错误码、日志设计
  • 测试设计:测试策略、测试用例
  • 部署设计:部署架构、部署步骤、配置管理

本文档不包含以下内容:

  • 功能需求分析(详见《软件需求分析说明书》)
  • 用户操作手册(详见《软件使用手册》)

1.3 术语定义

术语/缩写 全称 说明
JWT JSON Web Token 用于身份认证的令牌
RESTful Representational State Transfer 一种Web服务架构风格
BCrypt - 一种密码加密算法
ER图 Entity-Relationship Diagram 实体关系图
VO View Object 视图对象,用于前端展示
DTO Data Transfer Object 数据传输对象
API Application Programming Interface 应用程序编程接口
PDF Portable Document Format 便携式文档格式
QR Code Quick Response Code 二维码
RBAC Role-Based Access Control 基于角色的访问控制
CORS Cross-Origin Resource Sharing 跨域资源共享
CSRF Cross-Site Request Forgery 跨站请求伪造
XSS Cross-Site Scripting 跨站脚本攻击
MTBF Mean Time Between Failures 平均无故障时间
MTTR Mean Time To Repair 平均修复时间

1.4 参考资料

[1] 张海藩,牟勇敏.《软件工程导论第6版》 2008

[2] Cay S. Horstmann《Java核心技术 卷原书第11版》2019

[3] 霍春阳《Vue.js设计与实现》2022

[4] Abraham Silberschatz.《数据库系统概念原书第7版》2021

[5] 许晓斌《Maven实战》2011

[6] 《校园活动组织与报名系统 - 软件需求分析说明书》

[7] 《校园活动组织与报名系统 - 数据库设计》

[8] 《校园活动组织与报名系统 - API接口设计》

[9] 《校园活动组织与报名系统 - 后端开发规范》

[10] 《校园活动组织与报名系统 - 业务流程图》


第二章 系统架构设计

2.1 总体架构

系统采用经典的分层架构设计,遵循高内聚、低耦合的设计原则,确保系统的可维护性和可扩展性。

graph TB
    subgraph "表现层 Presentation Layer"
        A[Web前端<br/>Vue.js + TypeScript]
    end

    subgraph "应用层 Application Layer"
        B[Controller层<br/>RESTful API]
    end

    subgraph "业务逻辑层 Business Logic Layer"
        C[Service层<br/>业务逻辑处理]
    end

    subgraph "数据访问层 Data Access Layer"
        D[Mapper层<br/>MyBatis-Plus]
    end

    subgraph "数据存储层 Data Storage Layer"
        E[(MySQL数据库)]
    end

    subgraph "安全层 Security Layer"
        F[JWT认证]
        G[权限控制]
    end

    A -->|HTTP/HTTPS| B
    B --> C
    C --> D
    D --> E
    F --> B
    G --> B
    F --> C

架构层次说明

  1. 表现层Presentation Layer

    • 负责用户界面展示和用户交互
    • 使用Vue.js + TypeScript开发
    • 通过HTTP/HTTPS协议与后端通信
  2. 应用层Application Layer - Controller

    • 接收前端HTTP请求
    • 参数校验和转换
    • 调用业务逻辑层
    • 返回统一响应格式
  3. 业务逻辑层Business Logic Layer - Service

    • 实现核心业务逻辑
    • 事务管理
    • 业务规则验证
    • 调用数据访问层
  4. 数据访问层Data Access Layer - Mapper

    • 与数据库交互
    • SQL映射和执行
    • 结果集映射
  5. 数据存储层Data Storage Layer

    • MySQL数据库
    • 数据持久化存储
  6. 安全层Security Layer

    • JWT令牌认证
    • 基于角色的权限控制
    • 横切所有层

2.2 技术架构

2.2.1 前端技术架构

graph LR
    A[Vue.js 3.x] --> B[TypeScript]
    A --> C[Vue Router]
    A --> D[Pinia]
    A --> E[Axios]
    A --> F[Element Plus]
    A --> G[Bootstrap CSS]

前端技术栈

技术 版本 说明
Vue.js 3.x 前端框架
TypeScript 5.x 类型安全
Vue Router 4.x 路由管理
Pinia 2.x 状态管理
Axios 1.x HTTP客户端
Element Plus 2.x UI组件库
Bootstrap CSS 5.x CSS框架
Vite 5.x 构建工具

2.2.2 后端技术架构

graph LR
    A[Spring Boot 3.2.x] --> B[Spring Security 6.x]
    A --> C[MyBatis-Plus 3.5.x]
    A --> D[Spring Validation]
    A --> E[Knife4j 4.x]
    A --> F[Hutool 5.8.x]
    A --> G[Lombok 1.18.x]

后端技术栈

技术 版本 说明
Java 21 编程语言
Spring Boot 3.2.x 应用框架
Spring Security 6.x 安全框架
MyBatis-Plus 3.5.x ORM框架
Spring Validation - 参数校验
Knife4j 4.x API文档
Hutool 5.8.x 工具类库
Lombok 1.18.x 代码简化
JWT 0.12.x (jjwt) 令牌认证
ZXing 3.5.x 二维码生成
iText 7.x PDF生成
EasyExcel 3.3.x Excel导出

2.2.3 数据存储架构

graph LR
    A[MySQL 8.0+] --> B[InnoDB引擎]
    A --> C[utf8mb4字符集]
    A --> D[逻辑删除]
    A --> E[索引优化]

数据库技术栈

技术 版本 说明
MySQL 8.0+ 关系型数据库
InnoDB - 存储引擎
utf8mb4 - 字符集
逻辑删除 - 数据删除策略
索引 - 查询优化

2.3 部署架构

graph TB
    subgraph "客户端 Client"
        A1[PC浏览器]
        A2[移动浏览器]
    end

    subgraph "网络层 Network"
        B[HTTPS<br/>防火墙]
    end

    subgraph "应用服务器 Application Server"
        C1[Nginx<br/>反向代理]
        C2[Spring Boot应用<br/>端口:8080]
        C3[静态资源<br/>Vue.js打包]
    end

    subgraph "数据服务器 Data Server"
        D1[(MySQL数据库<br/>端口:3306)]
    end

    A1 --> B
    A2 --> B
    B --> C1
    C1 --> C2
    C1 --> C3
    C2 --> D1

部署架构说明

  1. 客户端层

    • PC浏览器Chrome、Firefox、Edge等
    • 移动浏览器iOS Safari、Chrome Mobile
  2. 网络层

    • HTTPS加密传输
    • 防火墙安全防护
  3. 应用服务器

    • Nginx反向代理、负载均衡、静态文件服务
    • Spring Boot应用后端API服务端口8080
    • 静态资源Vue.js打包后的前端文件
  4. 数据服务器

    • MySQL数据库数据持久化存储端口3306

2.4 模块划分

系统划分为以下功能模块:

graph TB
    A[校园活动系统] --> B[认证模块 Auth]
    A --> C[活动管理模块 Activity]
    A --> D[报名管理模块 Registration]
    A --> E[签到管理模块 CheckIn]
    A --> F[评价管理模块 Review]
    A --> G[统计模块 Statistics]

    B --> B1[用户注册]
    B --> B2[用户登录]
    B --> B3[Token刷新]
    B --> B4[获取用户信息]
    B --> B5[修改密码]

    C --> C1[创建活动]
    C --> C2[更新活动]
    C --> C3[删除活动]
    C --> C4[查询活动列表]
    C --> C5[查询活动详情]
    C --> C6[日历视图]
    C --> C7[时间冲突检测]

    D --> D1[报名活动]
    D --> D2[取消报名]
    D --> D3[我的报名]
    D --> D4[活动报名列表]
    D --> D5[下载电子票]

    E --> E1[生成签到二维码]
    E --> E2[学生扫码签到]
    E --> E3[管理员扫票签到]
    E --> E4[签到列表]

    F --> F1[提交评价]
    F --> F2[活动评价列表]
    F --> F3[我的评价]

    G --> G1[活动统计]
    G --> G2[数据导出]
    G --> G3[总体统计]

模块职责说明

模块 职责
认证模块 用户注册、登录、Token管理、密码管理
活动管理模块 活动的增删改查、时间冲突检测、状态管理
报名管理模块 活动报名、取消报名、电子票生成
签到管理模块 二维码生成、扫码签到、签到记录管理
评价管理模块 活动评价、评分统计、评价查询
统计模块 数据统计、报表生成、数据导出

第三章 数据库设计

3.1 概念结构设计

系统包含5个核心实体用户User、活动Activity、报名Registration、签到CheckIn、评价Review

erDiagram
    USER ||--o{ ACTIVITY : creates
    USER ||--o{ REGISTRATION : makes
    USER ||--o{ REVIEW : writes
    USER ||--o{ CHECK_IN : performs

    ACTIVITY ||--o{ REGISTRATION : has
    ACTIVITY ||--o{ REVIEW : receives
    ACTIVITY ||--o{ CHECK_IN : requires

    REGISTRATION ||--|| CHECK_IN : generates

    USER {
        bigint id
        varchar username
        varchar password
        varchar name
        varchar student_id
        varchar email
        varchar phone
        varchar avatar
        tinyint role
        tinyint status
        datetime created_at
        datetime updated_at
        tinyint deleted
    }

    ACTIVITY {
        bigint id
        varchar title
        text description
        varchar cover_image
        datetime start_time
        datetime end_time
        datetime registration_deadline
        varchar location
        int max_participants
        int current_participants
        tinyint status
        varchar category
        bigint admin_id
        varchar qr_code
        int version
        datetime created_at
        datetime updated_at
        tinyint deleted
    }

    REGISTRATION {
        bigint id
        bigint user_id
        bigint activity_id
        varchar ticket_code
        varchar ticket_pdf_url
        tinyint status
        datetime created_at
        datetime updated_at
        datetime canceled_at
    }

    CHECK_IN {
        bigint id
        bigint registration_id
        bigint user_id
        bigint activity_id
        datetime check_in_time
        tinyint check_in_method
    }

    REVIEW {
        bigint id
        bigint user_id
        bigint activity_id
        tinyint rating
        text content
        datetime created_at
        datetime updated_at
    }

3.2 逻辑结构设计

3.2.1 用户表 (user)

存储系统用户信息,包括学生和管理员。

字段名 类型 长度 必填 默认值 约束/索引 说明
id BIGINT - - PK, AI 用户ID
username VARCHAR 50 - Unique 登录用户名
password VARCHAR 255 - - BCrypt加密密码
name VARCHAR 50 - - 真实姓名
student_id VARCHAR 20 NULL Unique 学号
email VARCHAR 100 NULL - 邮箱
phone VARCHAR 20 NULL - 手机号
avatar VARCHAR 255 NULL - 头像URL
role TINYINT - 0 Index 角色0-学生1-管理员
status TINYINT - 1 - 状态0-禁用1-正常
created_at DATETIME - CURRENT_TIMESTAMP - 创建时间
updated_at DATETIME - - - 更新时间
deleted TINYINT - 0 - 逻辑删除0-否1-是

索引设计

  • 主键索引:id
  • 唯一索引:uk_username (username)
  • 唯一索引:uk_student_id (student_id)
  • 普通索引:idx_role (role)

3.2.2 活动表 (activity)

存储活动信息。

字段名 类型 长度 必填 默认值 约束/索引 说明
id BIGINT - - PK, AI 活动ID
title VARCHAR 100 - - 活动名称
description TEXT - NULL - 活动简介
cover_image VARCHAR 255 NULL - 封面图片URL
start_time DATETIME - - Index 开始时间
end_time DATETIME - - - 结束时间
registration_deadline DATETIME - NULL - 报名截止时间
location VARCHAR 200 - - 活动地点
max_participants INT - - - 报名人数上限
current_participants INT - 0 - 当前报名人数
status TINYINT - 0 Index 状态0-未开始1-报名中2-进行中3-已结束
category VARCHAR 50 NULL - 活动分类
admin_id BIGINT - - FK, Index 创建者ID
qr_code VARCHAR 255 NULL - 签到二维码URL
version INT - 0 - 乐观锁版本号
created_at DATETIME - CURRENT_TIMESTAMP - 创建时间
updated_at DATETIME - - - 更新时间
deleted TINYINT - 0 - 逻辑删除

索引设计

  • 主键索引:id
  • 普通索引:idx_status (status)
  • 普通索引:idx_start_time (start_time)
  • 普通索引:idx_admin_id (admin_id)
  • 外键约束:fk_activity_admin (admin_id) → user(id)
  • 乐观锁字段:version(用于并发控制)

3.2.3 报名表 (registration)

存储学生活动报名信息。

字段名 类型 长度 必填 默认值 约束/索引 说明
id BIGINT - - PK, AI 报名ID
user_id BIGINT - - FK, Unique(1) 用户ID
activity_id BIGINT - - FK, Unique(2) 活动ID
ticket_code VARCHAR 100 NULL Unique 电子票唯一码
ticket_pdf_url VARCHAR 255 NULL - 电子票PDF URL
status TINYINT - 1 - 状态0-已取消1-已报名2-已签到
created_at DATETIME - CURRENT_TIMESTAMP - 报名时间
updated_at DATETIME - - - 更新时间
canceled_at DATETIME - NULL - 取消时间

索引设计

  • 主键索引:id
  • 唯一索引:uk_user_activity (user_id, activity_id)
  • 唯一索引:uk_ticket_code (ticket_code)
  • 普通索引:idx_activity_id (activity_id)
  • 外键约束:fk_registration_user (user_id) → user(id)
  • 外键约束:fk_registration_activity (activity_id) → activity(id)

3.2.4 签到表 (check_in)

存储签到记录。

字段名 类型 长度 必填 默认值 约束/索引 说明
id BIGINT - - PK, AI 签到ID
registration_id BIGINT - - FK, Unique 报名ID
user_id BIGINT - - FK, Index 用户ID
activity_id BIGINT - - FK, Index 活动ID
check_in_time DATETIME - CURRENT_TIMESTAMP - 签到时间
check_in_method TINYINT - 0 - 方式0-扫码1-管理员代签

索引设计

  • 主键索引:id
  • 唯一索引:uk_registration_id (registration_id)
  • 普通索引:idx_activity_id (activity_id)
  • 普通索引:idx_user_id (user_id)
  • 外键约束:fk_checkin_registration (registration_id) → registration(id)
  • 外键约束:fk_checkin_user (user_id) → user(id)
  • 外键约束:fk_checkin_activity (activity_id) → activity(id)

3.2.5 评价表 (review)

存储活动评价信息。

字段名 类型 长度 必填 默认值 约束/索引 说明
id BIGINT - - PK, AI 评价ID
user_id BIGINT - - FK, Unique(1) 用户ID
activity_id BIGINT - - FK, Unique(2) 活动ID
rating TINYINT - - - 评分1-5
content TEXT - NULL - 评论内容
created_at DATETIME - CURRENT_TIMESTAMP - 评价时间
updated_at DATETIME - - - 更新时间

索引设计

  • 主键索引:id
  • 唯一索引:uk_user_activity (user_id, activity_id)
  • 普通索引:idx_activity_id (activity_id)
  • 外键约束:fk_review_user (user_id) → user(id)
  • 外键约束:fk_review_activity (activity_id) → activity(id)

3.3 物理结构设计

3.3.1 存储引擎

  • 所有表使用 InnoDB 存储引擎
  • 支持事务、行级锁、外键约束

3.3.2 字符集和排序规则

  • 字符集:utf8mb4
  • 排序规则:utf8mb4_general_ci
  • 支持存储emoji表情和特殊字符

3.3.3 索引策略

索引设计原则

  1. 主键索引所有表使用自增BIGINT主键
  2. 唯一索引:业务唯一性约束(用户名、学号、电子票码等)
  3. 普通索引频繁查询字段status、start_time、admin_id等
  4. 联合索引多条件查询优化user_id + activity_id

索引使用场景

  • idx_status:按状态筛选活动
  • idx_start_time:按时间排序活动
  • idx_admin_id:查询管理员创建的活动
  • idx_activity_id:查询活动的报名、签到、评价记录
  • uk_user_activity:防止重复报名、重复评价

3.3.4 分区策略

当前数据量较小,暂不采用表分区。如数据量增长到百万级别,可考虑:

  • 按时间范围分区activity表按created_at
  • 按用户ID哈希分区registration表

3.4 数据完整性约束

3.4.1 实体完整性

  • 所有表设置主键约束
  • 主键字段不允许为NULL
  • 使用自增策略保证主键唯一性

3.4.2 参照完整性

  • 外键约束确保关联数据一致性
  • 级联删除策略:删除用户时,相关报名记录保留(逻辑删除)
  • 级联更新策略更新用户ID时相关记录同步更新

3.4.3 域完整性

  • 字段类型约束VARCHAR长度、INT范围、TINYINT范围
  • 非空约束关键字段不允许为NULL
  • 默认值约束设置合理的默认值status=1, deleted=0

3.4.4 用户定义完整性

  • 唯一约束username、student_id、ticket_code唯一
  • 检查约束rating范围1-5status范围0-3
  • 业务约束:同一用户不能重复报名同一活动

第四章 详细设计

4.1 认证模块设计

4.1.1 JWT认证机制设计

Token设计

graph LR
    A[用户登录] --> B[验证用户名密码]
    B --> C[生成Access Token<br/>有效期2小时]
    B --> D[生成Refresh Token<br/>有效期7天]
    C --> E[返回Token给客户端]
    D --> E
    E --> F[客户端存储Token]
    F --> G[请求携带Access Token]
    G --> H[服务端验证Token]
    H --> I{Token有效?}
    I -->|是| J[允许访问]
    I -->|否| K[使用Refresh Token刷新]
    K --> L[生成新Access Token]
    L --> J

Access Token结构

{
  "header": {
    "alg": "HS256",
    "typ": "JWT"
  },
  "payload": {
    "userId": 1,
    "username": "student001",
    "role": 0,
    "iat": 1736620800,
    "exp": 1736628000
  }
}

Refresh Token结构

{
  "header": {
    "alg": "HS256",
    "typ": "JWT"
  },
  "payload": {
    "userId": 1,
    "iat": 1736620800,
    "exp": 1737225600
  }
}

认证流程

  1. 用户登录成功后服务端生成Access Token和Refresh Token
  2. 客户端存储两个TokenlocalStorage或Cookie
  3. 每次请求携带Access TokenHeader: Authorization: Bearer {token}
  4. 服务端验证Token有效性
  5. Access Token过期时使用Refresh Token获取新Access Token
  6. Refresh Token过期时用户需重新登录

4.1.2 类结构设计

classDiagram
    class JwtTokenProvider {
        -String secret
        -long accessTokenExpiration
        -long refreshTokenExpiration
        +generateAccessToken(userId, username, role)
        +generateRefreshToken(userId)
        +validateToken(token)
        +getUserIdFromToken(token)
        +getUsernameFromToken(token)
        +getRoleFromToken(token)
    }

    class JwtAuthenticationFilter {
        -JwtTokenProvider jwtTokenProvider
        +doFilterInternal(request, response, chain)
        -getJwtFromRequest(request)
    }

    class UserDetailsServiceImpl {
        -UserMapper userMapper
        +loadUserByUsername(username)
    }

    class AuthController {
        -AuthService authService
        +register(request)
        +login(request)
        +refreshToken(request)
        +getCurrentUser()
        +changePassword(request)
    }

    class AuthServiceImpl {
        -UserMapper userMapper
        -JwtTokenProvider jwtTokenProvider
        -PasswordEncoder passwordEncoder
        +register(request)
        +login(request)
        +refreshToken(request)
        +getCurrentUser(userId)
        +changePassword(userId, request)
    }

    JwtAuthenticationFilter --> JwtTokenProvider
    AuthController --> AuthServiceImpl
    AuthServiceImpl --> JwtTokenProvider
    AuthServiceImpl --> UserMapper
    UserDetailsServiceImpl --> UserMapper

核心类说明

类名 职责
JwtTokenProvider JWT Token的生成、解析、验证
JwtAuthenticationFilter JWT认证过滤器拦截请求验证Token
UserDetailsServiceImpl Spring Security用户详情服务
AuthController 认证接口控制器
AuthServiceImpl 认证业务逻辑实现

4.1.3 密码加密算法设计

加密算法BCrypt

加密流程

graph LR
    A[用户输入密码] --> B[生成随机盐值]
    B --> C[BCrypt哈希运算]
    C --> D[存储加密密码]

加密特点

  • 自动生成随机盐值,防止彩虹表攻击
  • 可调整计算强度cost factor默认10
  • 相同密码每次加密结果不同
  • 验证时自动提取盐值进行比对

示例代码

// 加密
String encodedPassword = passwordEncoder.encode("123456");

// 验证
boolean matches = passwordEncoder.matches("123456", encodedPassword);

4.2 活动管理模块设计

4.2.1 活动状态机设计

stateDiagram-v2
    [*] --> 草稿: 创建活动
    草稿 --> 报名中: 发布活动
    报名中 --> 进行中: 活动开始
    进行中 --> 已结束: 活动结束
    报名中 --> 已取消: 取消活动
    进行中 --> 已取消: 取消活动
    草稿 --> 已删除: 删除活动
    报名中 --> 已删除: 删除活动
    进行中 --> 已删除: 删除活动
    已结束 --> 已删除: 删除活动
    已取消 --> 已删除: 删除活动
    已结束 --> [*]
    已取消 --> [*]
    已删除 --> [*]

状态说明

状态值 状态名称 说明 可执行操作
0 草稿 活动未发布,仅管理员可见 修改、发布、删除
1 报名中 活动已发布,学生可报名 修改、取消、删除
2 进行中 活动进行中 查看详情
3 已结束 活动已结束,学生可评价 查看详情、删除
4 已取消 活动已取消 查看详情、删除

状态转换规则

  1. 草稿 → 报名中:管理员发布活动
  2. 报名中 → 进行中:到达活动开始时间(定时任务自动更新)
  3. 进行中 → 已结束:到达活动结束时间(定时任务自动更新)
  4. 报名中/进行中 → 已取消:管理员取消活动
  5. 任意状态 → 已删除:管理员删除活动(逻辑删除)

4.2.2 时间冲突检测算法

冲突判断标准

两个活动时间区间 [start1, end1][start2, end2] 存在冲突的条件:

!(end1 < start2 || end2 < start1)

即:两个时间区间有交集。

检测算法

SELECT * FROM activity
WHERE deleted = 0
  AND status IN (1, 2, 3)
  AND id != ?  -- 排除当前活动(更新时)
  AND location = ?  -- 同一地点
  AND (
    (start_time <= ? AND end_time >= ?)  -- 新活动开始时间在旧活动时间范围内
    OR (start_time <= ? AND end_time >= ?)  -- 新活动结束时间在旧活动时间范围内
    OR (start_time >= ? AND end_time <= ?)  -- 新活动完全包含旧活动
  )

检测流程

graph TD
    A[输入活动时间] --> B[查询同地点活动]
    B --> C{遍历活动列表}
    C -->|有活动| D{时间区间有交集?}
    D -->|是| E[记录冲突活动]
    D -->|否| C
    C -->|无活动| F[无冲突]
    E --> G[返回冲突列表]
    F --> H[返回无冲突]

4.2.3 类结构设计

classDiagram
    class ActivityController {
        -ActivityService activityService
        +pageActivities(page, params)
        +getActivityById(id)
        +createActivity(request)
        +updateActivity(id, request)
        +deleteActivity(id)
        +getCalendarActivities(year, month)
        +checkConflict(request)
    }

    class ActivityService {
        <<interface>>
        +pageActivities(page, params)
        +getActivityById(id)
        +createActivity(request, adminId)
        +updateActivity(id, request, adminId)
        +deleteActivity(id, adminId)
        +getCalendarActivities(year, month)
        +checkConflict(startTime, endTime, excludeId)
        +updateActivityStatus()
    }

    class ActivityServiceImpl {
        -ActivityMapper activityMapper
        -UserMapper userMapper
        -RegistrationMapper registrationMapper
        +pageActivities(page, params)
        +getActivityById(id)
        +createActivity(request, adminId)
        +updateActivity(id, request, adminId)
        +deleteActivity(id, adminId)
        +getCalendarActivities(year, month)
        +checkConflict(startTime, endTime, excludeId)
        +updateActivityStatus()
    }

    class ActivityMapper {
        +selectPage(page, params)
        +selectById(id)
        +insert(activity)
        +updateById(activity)
        +deleteById(id)
        +selectByMonth(year, month)
        +selectConflictActivities(startTime, endTime, excludeId)
        +selectActivitiesByStatus(status)
    }

    class Activity {
        -Long id
        -String title
        -String description
        -String coverImage
        -LocalDateTime startTime
        -LocalDateTime endTime
        -LocalDateTime registrationDeadline
        -String location
        -Integer maxParticipants
        -Integer currentParticipants
        -Integer status
        -String category
        -Long adminId
        -String qrCode
        -Integer version
        -LocalDateTime createdAt
        -LocalDateTime updatedAt
        -Integer deleted
    }

    ActivityController --> ActivityService
    ActivityService <|.. ActivityServiceImpl
    ActivityServiceImpl --> ActivityMapper
    ActivityMapper --> Activity

4.3 报名管理模块设计

4.3.1 报名流程设计

flowchart TD
    A[学生发起报名] --> B{活动存在?}
    B -->|否| C[返回活动不存在]
    B -->|是| D{活动在报名中?}
    D -->|否| E[返回活动不在报名中]
    D -->|是| F{报名已截止?}
    F -->|是| E
    F -->|否| G{报名人数已满?}
    G -->|是| H[返回报名人数已满]
    G -->|否| I{已报名该活动?}
    I -->|是| J[返回已报名]
    I -->|否| K{时间冲突?}
    K -->|是| L[返回时间冲突]
    K -->|否| M[生成电子票码]
    M --> N[创建报名记录]
    N --> O[更新活动报名人数]
    O --> P[生成电子票PDF]
    P --> Q[返回报名成功]

报名验证规则

  1. 活动必须存在且未删除
  2. 活动状态必须为"报名中"
  3. 当前时间必须在报名截止时间之前
  4. 当前报名人数必须小于报名人数上限
  5. 用户不能重复报名同一活动
  6. 用户不能报名时间冲突的活动

4.3.2 电子票生成算法

电子票唯一码生成

格式:TICKET{YYYYMMDD}{6位随机数}

示例:TICKET202601150001234

生成流程

graph LR
    A[获取当前日期] --> B[生成6位随机数]
    B --> C[拼接成电子票码]
    C --> D{检查唯一性}
    D -->|已存在| B
    D -->|不存在| E[返回电子票码]

PDF电子票生成

使用iText库生成PDF文件包含以下内容

  1. 活动信息:名称、时间、地点
  2. 学生信息:姓名、学号
  3. 电子票码
  4. 二维码(包含电子票码)

PDF布局设计

┌─────────────────────────────────────┐
│         校园活动电子票               │
├─────────────────────────────────────┤
│ 活动名称: 校园音乐节                │
│ 活动时间: 2026-01-15 19:00-22:00   │
│ 活动地点: 大礼堂                    │
│                                     │
│ 学生姓名: 张三                      │
│ 学    号: 2021000001                │
│                                     │
│ 电子票号: TICKET202601150001234     │
│                                     │
│        [二维码图片]                 │
├─────────────────────────────────────┤
│  请凭此票入场,请勿转借他人         │
└─────────────────────────────────────┘

4.3.3 并发控制设计

并发报名问题

  • 多个用户同时报名同一活动
  • 可能导致报名人数超过上限

解决方案:使用数据库乐观锁

乐观锁实现

-- 报名时更新报名人数使用version乐观锁
UPDATE activity
SET current_participants = current_participants + 1,
    version = version + 1
WHERE id = ?
  AND current_participants < max_participants
  AND version = ?;

并发控制流程

sequenceDiagram
    participant User1 as 用户1
    participant User2 as 用户2
    participant DB as 数据库

    User1->>DB: 查询活动信息version=1
    User2->>DB: 查询活动信息version=1

    User1->>DB: 更新报名人数<br/>WHERE version=1
    DB-->>User1: 更新成功version=2

    User2->>DB: 更新报名人数<br/>WHERE version=1
    DB-->>User2: 更新失败version已变
    User2->>User2: 重新查询活动信息
    User2->>DB: 查询活动信息version=2
    User2->>DB: 更新报名人数<br/>WHERE version=2
    DB-->>User2: 更新成功version=3

4.3.4 类结构设计

classDiagram
    class RegistrationController {
        -RegistrationService registrationService
        +register(request)
        +cancelRegistration(id)
        +getMyRegistrations(page, status)
        +getActivityRegistrations(activityId, page)
        +downloadTicketPdf(id)
    }

    class RegistrationService {
        <<interface>>
        +register(userId, activityId)
        +cancelRegistration(userId, registrationId)
        +getMyRegistrations(userId, page, status)
        +getActivityRegistrations(activityId, page)
        +downloadTicketPdf(userId, registrationId)
        +checkTimeConflict(userId, activityId)
    }

    class RegistrationServiceImpl {
        -RegistrationMapper registrationMapper
        -ActivityMapper activityMapper
        -PdfUtil pdfUtil
        +register(userId, activityId)
        +cancelRegistration(userId, registrationId)
        +getMyRegistrations(userId, page, status)
        +getActivityRegistrations(activityId, page)
        +downloadTicketPdf(userId, registrationId)
        +checkTimeConflict(userId, activityId)
        +generateTicketCode()
    }

    class RegistrationMapper {
        +selectOne(userId, activityId)
        +insert(registration)
        +updateById(registration)
        +selectMyPage(userId, page, status)
        +selectActivityPage(activityId, page)
        +selectByTicketCode(ticketCode)
        +selectUserRegistrations(userId, status)
    }

    class Registration {
        -Long id
        -Long userId
        -Long activityId
        -String ticketCode
        -String ticketPdfUrl
        -Integer status
        -LocalDateTime createdAt
        -LocalDateTime updatedAt
        -LocalDateTime canceledAt
    }

    class PdfUtil {
        +generateTicketPdf(user, activity, registration)
    }

    RegistrationController --> RegistrationService
    RegistrationService <|.. RegistrationServiceImpl
    RegistrationServiceImpl --> RegistrationMapper
    RegistrationServiceImpl --> PdfUtil
    RegistrationMapper --> Registration

4.4 签到管理模块设计

4.4.1 二维码生成与解析设计

签到二维码内容格式

CHECKIN:{activityId}:{timestamp}:{signature}

示例:

CHECKIN:1:1736928000:a1b2c3d4e5f6

二维码生成流程

graph TD
    A[管理员生成签到二维码] --> B[获取活动ID]
    B --> C[生成时间戳]
    C --> D[生成签名<br/>HMAC-SHA256]
    D --> E[拼接二维码内容]
    E --> F[使用ZXing生成二维码图片]
    F --> G[保存二维码图片]
    G --> H[返回二维码URL]

签名生成算法

String signature = HMACSHA256(
    secretKey,
    activityId + ":" + timestamp
);

二维码解析流程

graph TD
    A[扫描二维码] --> B[解析二维码内容]
    B --> C{格式正确?}
    C -->|否| D[返回二维码无效]
    C -->|是| E[提取activityId]
    E --> F[提取timestamp]
    F --> G[提取signature]
    G --> H{签名验证通过?}
    H -->|否| D
    H -->|是| I{时间有效?}
    I -->|否| J[返回二维码已过期]
    I -->|是| K[返回活动ID]

时间有效性判断

  • 二维码有效期活动开始前1小时至活动结束
  • 判断条件:startTime - 1hour <= now <= endTime

4.4.2 签到流程设计

学生扫码签到流程

flowchart TD
    A[学生扫描签到二维码] --> B[解析二维码]
    B --> C{二维码有效?}
    C -->|否| D[返回二维码无效]
    C -->|是| E{活动存在?}
    E -->|否| F[返回活动不存在]
    E -->|是| G{签到时间有效?}
    G -->|否| H[返回签到时间已过期]
    G -->|是| I{已报名该活动?}
    I -->|否| J[返回未报名该活动]
    I -->|是| K{已签到?}
    K -->|是| L[返回已签到]
    K -->|否| M[创建签到记录]
    M --> N[更新报名状态为已签到]
    N --> O[返回签到成功]

管理员扫票签到流程

flowchart TD
    A[管理员扫描学生电子票] --> B[解析二维码]
    B --> C[获取报名记录ID]
    C --> D{报名记录存在?}
    D -->|否| E[返回电子票无效]
    D -->|是| F{活动匹配?}
    F -->|否| E
    F -->|是| G{签到时间有效?}
    G -->|否| H[返回签到时间已过期]
    G -->|是| I{已签到?}
    I -->|是| J[返回已签到]
    I -->|否| K[创建签到记录]
    K --> L[更新报名状态为已签到]
    L --> M[记录签到方式和签到人]
    M --> N[返回签到成功]

4.4.3 类结构设计

classDiagram
    class CheckInController {
        -CheckInService checkInService
        +generateQrCode(activityId)
        +scanCheckIn(request)
        +ticketCheckIn(request)
        +getActivityCheckIns(activityId, page)
    }

    class CheckInService {
        <<interface>>
        +generateQrCode(activityId, adminId)
        +scanCheckIn(userId, qrCodeContent)
        +ticketCheckIn(adminId, activityId, ticketCode)
        +getActivityCheckIns(activityId, page)
    }

    class CheckInServiceImpl {
        -CheckInMapper checkInMapper
        -RegistrationMapper registrationMapper
        -ActivityMapper activityMapper
        -QrCodeUtil qrCodeUtil
        +generateQrCode(activityId, adminId)
        +scanCheckIn(userId, qrCodeContent)
        +ticketCheckIn(adminId, activityId, ticketCode)
        +getActivityCheckIns(activityId, page)
        -validateCheckInTime(activity)
    }

    class CheckInMapper {
        +selectOne(registrationId)
        +insert(checkIn)
        +selectActivityPage(activityId, page)
        +selectUserCheckIns(userId)
    }

    class QrCodeUtil {
        +generateQrCode(content)
        +parseQrCode(image)
        +generateSignature(activityId, timestamp)
        +validateSignature(content, signature)
    }

    class CheckIn {
        -Long id
        -Long registrationId
        -Long userId
        -Long activityId
        -LocalDateTime checkInTime
        -Integer checkInMethod
    }

    CheckInController --> CheckInService
    CheckInService <|.. CheckInServiceImpl
    CheckInServiceImpl --> CheckInMapper
    CheckInServiceImpl --> QrCodeUtil
    CheckInMapper --> CheckIn

4.5 评价管理模块设计

4.5.1 评分计算算法

平均评分计算

SELECT AVG(rating) as average_rating,
       COUNT(*) as review_count
FROM review
WHERE activity_id = ?;

评分分布统计

SELECT rating,
       COUNT(*) as count
FROM review
WHERE activity_id = ?
GROUP BY rating
ORDER BY rating;

评分星级显示

  • 1星
  • 2星
  • 3星
  • 4星
  • 5星

4.5.2 类结构设计

classDiagram
    class ReviewController {
        -ReviewService reviewService
        +createReview(request)
        +getActivityReviews(activityId, page)
        +getMyReviews(page)
    }

    class ReviewService {
        <<interface>>
        +createReview(userId, request)
        +getActivityReviews(activityId, page)
        +getMyReviews(userId, page)
        +calculateAverageRating(activityId)
    }

    class ReviewServiceImpl {
        -ReviewMapper reviewMapper
        -CheckInMapper checkInMapper
        -ActivityMapper activityMapper
        +createReview(userId, request)
        +getActivityReviews(activityId, page)
        +getMyReviews(userId, page)
        +calculateAverageRating(activityId)
    }

    class ReviewMapper {
        +selectOne(userId, activityId)
        +insert(review)
        +selectActivityPage(activityId, page)
        +selectMyPage(userId, page)
        +selectRatingDistribution(activityId)
    }

    class Review {
        -Long id
        -Long userId
        -Long activityId
        -Integer rating
        -String content
        -LocalDateTime createdAt
        -LocalDateTime updatedAt
    }

    ReviewController --> ReviewService
    ReviewService <|.. ReviewServiceImpl
    ReviewServiceImpl --> ReviewMapper
    ReviewMapper --> Review

4.6 统计模块设计

4.6.1 统计指标计算

活动统计指标

  1. 报名数据

    • 总报名人数:COUNT(*) FROM registration WHERE activity_id = ? AND status != 0
  2. 签到数据

    • 总签到人数:COUNT(*) FROM check_in WHERE activity_id = ?
    • 签到率:签到人数 / 报名人数
  3. 评价数据

    • 总评价人数:COUNT(*) FROM review WHERE activity_id = ?
    • 评价率:评价人数 / 签到人数
    • 平均评分:AVG(rating) FROM review WHERE activity_id = ?
    • 评分分布:各星级评价数量

总体统计指标

  1. 活动数据

    • 总活动数:COUNT(*) FROM activity WHERE deleted = 0
  2. 报名数据

    • 总报名数:COUNT(*) FROM registration WHERE status != 0
  3. 签到数据

    • 总签到数:COUNT(*) FROM check_in
  4. 评价数据

    • 总评价数:COUNT(*) FROM review
    • 平均评分:AVG(rating) FROM review
  5. 月度统计

    • 按月份统计活动数和报名数

4.6.2 数据导出设计

导出格式Excel (.xlsx)

导出内容

  1. 活动报名数据导出

    • 学生信息:姓名、学号、手机号、邮箱
    • 报名信息:报名时间、报名状态、电子票号
    • 签到信息:签到时间、签到方式
  2. 活动签到数据导出

    • 学生信息:姓名、学号
    • 签到信息:签到时间、签到方式
  3. 活动评价数据导出

    • 学生信息:姓名
    • 评价信息:评分、评论内容、评价时间

导出流程

graph TD
    A[管理员发起导出] --> B[查询活动数据]
    B --> C[组装导出数据]
    C --> D[使用EasyExcel生成Excel]
    D --> E[返回文件流]
    E --> F[客户端下载文件]

第五章 接口设计

5.1 接口设计规范

5.1.1 RESTful API规范

操作 HTTP方法 URL示例 说明
创建 POST /api/v1/activities 创建资源
查询 GET /api/v1/activities/{id} 查询单个资源
更新 PUT /api/v1/activities/{id} 更新资源
删除 DELETE /api/v1/activities/{id} 删除资源
列表 GET /api/v1/activities 查询资源列表

5.1.2 统一响应格式

成功响应

{
  "code": 200,
  "message": "success",
  "data": {},
  "timestamp": 1736620800000
}

错误响应

{
  "code": 400,
  "message": "请求参数错误",
  "data": null,
  "timestamp": 1736620800000
}

5.1.3 状态码说明

状态码 说明
200 成功
400 请求参数错误
401 未认证或Token已过期
403 无权限访问
404 资源不存在
409 业务冲突
500 服务器内部错误

5.1.4 业务错误码

错误码 说明
1001 用户不存在
1002 用户已存在
1003 密码错误
1004 用户名或密码错误
1005 学号已存在
2001 活动不存在
2002 活动已开始,无法取消报名
2003 活动时间冲突
2004 活动报名人数已满
3001 报名记录不存在
3002 您已报名该活动
3003 您未报名该活动
4001 签到失败
4002 已签到
4003 签到时间已过期
5001 您已评价该活动
5002 评价记录不存在
5003 您未参加该活动,无法评价

5.2 认证接口设计

5.2.1 用户注册接口

接口地址POST /api/v1/auth/register

请求权限:无需认证

请求参数

{
  "username": "student001",
  "password": "123456",
  "name": "张三",
  "studentId": "2021000001",
  "email": "zhangsan@example.com",
  "phone": "13800138000"
}

响应示例

{
  "code": 200,
  "message": "注册成功",
  "data": null,
  "timestamp": 1736620800000
}

5.2.2 用户登录接口

接口地址POST /api/v1/auth/login

请求权限:无需认证

请求参数

{
  "username": "student001",
  "password": "123456"
}

响应示例

{
  "code": 200,
  "message": "登录成功",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "expiresIn": 3600,
    "tokenType": "Bearer",
    "userInfo": {
      "id": 1,
      "username": "student001",
      "name": "张三",
      "role": 0,
      "avatar": null
    }
  },
  "timestamp": 1736620800000
}

5.2.3 Token刷新接口

接口地址POST /api/v1/auth/refresh

请求权限:无需认证

请求参数

{
  "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

响应示例

{
  "code": 200,
  "message": "success",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "expiresIn": 3600
  },
  "timestamp": 1736620800000
}

5.2.4 获取用户信息接口

接口地址GET /api/v1/auth/me

请求权限:需要认证

请求头

Authorization: Bearer {accessToken}

响应示例

{
  "code": 200,
  "message": "success",
  "data": {
    "id": 1,
    "username": "student001",
    "password": "$2a$10$...",
    "name": "张三",
    "studentId": "2021000001",
    "email": "zhangsan@example.com",
    "phone": "13800138000",
    "avatar": null,
    "role": 0,
    "status": 1,
    "createdAt": "2026-01-01T00:00:00",
    "updatedAt": "2026-01-01T00:00:00"
  },
  "timestamp": 1736620800000
}

5.3 活动接口设计

5.3.1 活动列表查询接口

接口地址GET /api/v1/activities

请求权限:无需认证

请求参数

参数名 类型 必填 说明 默认值
current Long 当前页码 1
size Long 每页数量 10
status Integer 活动状态 -
keyword String 关键词搜索 -
category String 活动分类 -
startDate LocalDateTime 开始时间 -
endDate LocalDateTime 结束时间 -

响应示例

{
  "code": 200,
  "message": "success",
  "data": {
    "records": [
      {
        "id": 1,
        "title": "校园音乐节",
        "description": "一年一度的校园音乐盛会",
        "coverImage": "https://example.com/cover.jpg",
        "startTime": "2026-01-15T19:00:00",
        "endTime": "2026-01-15T22:00:00",
        "registrationDeadline": "2026-01-14T18:00:00",
        "location": "大礼堂",
        "maxParticipants": 500,
        "currentParticipants": 300,
        "status": 1,
        "category": "文艺活动",
        "adminId": 1,
        "adminName": "管理员",
        "averageRating": 4.5,
        "reviewCount": 20,
        "isRegistered": false,
        "createdAt": "2026-01-01T00:00:00"
      }
    ],
    "total": 1,
    "pages": 1,
    "current": 1,
    "size": 10
  },
  "timestamp": 1736620800000
}

5.3.2 活动详情查询接口

接口地址GET /api/v1/activities/{id}

请求权限:无需认证

路径参数

参数名 类型 必填 说明
id Long 活动ID

响应示例

{
  "code": 200,
  "message": "success",
  "data": {
    "id": 1,
    "title": "校园音乐节",
    "description": "一年一度的校园音乐盛会",
    "coverImage": "https://example.com/cover.jpg",
    "startTime": "2026-01-15T19:00:00",
    "endTime": "2026-01-15T22:00:00",
    "registrationDeadline": "2026-01-14T18:00:00",
    "location": "大礼堂",
    "maxParticipants": 500,
    "currentParticipants": 300,
    "status": 1,
    "category": "文艺活动",
    "adminId": 1,
    "adminName": "管理员",
    "averageRating": 4.5,
    "reviewCount": 20,
    "isRegistered": false,
    "createdAt": "2026-01-01T00:00:00"
  },
  "timestamp": 1736620800000
}

5.3.3 创建活动接口

接口地址POST /api/v1/activities

请求权限:管理员 (ADMIN)

请求参数

{
  "title": "校园音乐节",
  "description": "一年一度的校园音乐盛会",
  "coverImage": "https://example.com/cover.jpg",
  "startTime": "2026-01-15T19:00:00",
  "endTime": "2026-01-15T22:00:00",
  "registrationDeadline": "2026-01-14T18:00:00",
  "location": "大礼堂",
  "maxParticipants": 500,
  "category": "文艺活动"
}

响应示例

{
  "code": 200,
  "message": "创建成功",
  "data": 1,
  "timestamp": 1736620800000
}

5.3.4 更新活动接口

接口地址PUT /api/v1/activities/{id}

请求权限:管理员 (ADMIN)

路径参数

参数名 类型 必填 说明
id Long 活动ID

请求参数

{
  "title": "校园音乐节",
  "description": "一年一度的校园音乐盛会",
  "coverImage": "https://example.com/cover.jpg",
  "startTime": "2026-01-15T19:00:00",
  "endTime": "2026-01-15T22:00:00",
  "registrationDeadline": "2026-01-14T18:00:00",
  "location": "大礼堂",
  "maxParticipants": 500,
  "status": 1,
  "category": "文艺活动"
}

响应示例

{
  "code": 200,
  "message": "更新成功",
  "data": null,
  "timestamp": 1736620800000
}

5.3.5 删除活动接口

接口地址DELETE /api/v1/activities/{id}

请求权限:管理员 (ADMIN)

路径参数

参数名 类型 必填 说明
id Long 活动ID

响应示例

{
  "code": 200,
  "message": "删除成功",
  "data": null,
  "timestamp": 1736620800000
}

5.3.6 日历视图接口

接口地址GET /api/v1/activities/calendar

请求权限:无需认证

请求参数

参数名 类型 必填 说明
year Integer 年份
month Integer 月份

响应示例

{
  "code": 200,
  "message": "success",
  "data": [
    {
      "id": 1,
      "title": "校园音乐节",
      "description": "一年一度的校园音乐盛会",
      "coverImage": "https://example.com/cover.jpg",
      "startTime": "2026-01-15T19:00:00",
      "endTime": "2026-01-15T22:00:00",
      "registrationDeadline": "2026-01-14T18:00:00",
      "location": "大礼堂",
      "maxParticipants": 500,
      "currentParticipants": 300,
      "status": 1,
      "category": "文艺活动",
      "adminId": 1,
      "adminName": "管理员",
      "averageRating": 4.5,
      "reviewCount": 20,
      "isRegistered": false,
      "createdAt": "2026-01-01T00:00:00"
    }
  ],
  "timestamp": 1736620800000
}

5.3.7 时间冲突检测接口

接口地址POST /api/v1/activities/check-conflict

请求权限:管理员 (ADMIN)

请求参数

{
  "startTime": "2026-01-15T19:00:00",
  "endTime": "2026-01-15T22:00:00",
  "excludeActivityId": 1
}

响应示例

{
  "code": 200,
  "message": "success",
  "data": {
    "hasConflict": true,
    "conflictActivities": [
      {
        "id": 2,
        "title": "校园篮球赛",
        "startTime": "2026-01-15T18:00:00",
        "endTime": "2026-01-15T20:00:00"
      }
    ]
  },
  "timestamp": 1736620800000
}

5.4 报名接口设计

5.4.1 报名活动接口

接口地址POST /api/v1/registrations

请求权限:需要认证

请求参数

{
  "activityId": 1
}

响应示例

{
  "code": 200,
  "message": "报名成功",
  "data": {
    "id": 1,
    "activityId": 1,
    "activityTitle": "校园音乐节",
    "activityStartTime": "2026-01-15T19:00:00",
    "activityEndTime": "2026-01-15T22:00:00",
    "activityLocation": "大礼堂",
    "ticketCode": "TICKET202601150001",
    "ticketPdfUrl": "https://example.com/tickets/TICKET202601150001.pdf",
    "status": 1,
    "createdAt": "2026-01-10T10:00:00",
    "canceledAt": null
  },
  "timestamp": 1736620800000
}

5.4.2 取消报名接口

接口地址DELETE /api/v1/registrations/{id}

请求权限:需要认证

路径参数

参数名 类型 必填 说明
id Long 报名记录ID

响应示例

{
  "code": 200,
  "message": "取消成功",
  "data": null,
  "timestamp": 1736620800000
}

5.4.3 我的报名列表接口

接口地址GET /api/v1/registrations/my

请求权限:需要认证

请求参数

参数名 类型 必填 说明 默认值
current Long 当前页码 1
size Long 每页数量 10
status Integer 报名状态 -

响应示例

{
  "code": 200,
  "message": "success",
  "data": {
    "records": [
      {
        "id": 1,
        "activityId": 1,
        "activityTitle": "校园音乐节",
        "activityStartTime": "2026-01-15T19:00:00",
        "activityEndTime": "2026-01-15T22:00:00",
        "activityLocation": "大礼堂",
        "ticketCode": "TICKET202601150001",
        "ticketPdfUrl": "https://example.com/tickets/TICKET202601150001.pdf",
        "status": 1,
        "createdAt": "2026-01-10T10:00:00",
        "canceledAt": null
      }
    ],
    "total": 1,
    "pages": 1,
    "current": 1,
    "size": 10
  },
  "timestamp": 1736620800000
}

5.4.4 活动报名列表接口

接口地址GET /api/v1/registrations/activity/{activityId}

请求权限:管理员 (ADMIN)

路径参数

参数名 类型 必填 说明
activityId Long 活动ID

请求参数

参数名 类型 必填 说明 默认值
current Long 当前页码 1
size Long 每页数量 10

响应示例

{
  "code": 200,
  "message": "success",
  "data": {
    "records": [
      {
        "id": 1,
        "activityId": 1,
        "activityTitle": "校园音乐节",
        "activityStartTime": "2026-01-15T19:00:00",
        "activityEndTime": "2026-01-15T22:00:00",
        "activityLocation": "大礼堂",
        "ticketCode": "TICKET202601150001",
        "ticketPdfUrl": "https://example.com/tickets/TICKET202601150001.pdf",
        "status": 1,
        "createdAt": "2026-01-10T10:00:00",
        "canceledAt": null
      }
    ],
    "total": 1,
    "pages": 1,
    "current": 1,
    "size": 10
  },
  "timestamp": 1736620800000
}

5.4.5 下载电子票接口

接口地址GET /api/v1/registrations/{id}/ticket

请求权限:需要认证

路径参数

参数名 类型 必填 说明
id Long 报名记录ID

响应PDF文件流

响应头

Content-Disposition: attachment; filename=ticket.pdf
Content-Type: application/pdf

5.5 签到接口设计

5.5.1 生成签到二维码接口

接口地址POST /api/v1/checkin/qrcode/{activityId}

请求权限:管理员 (ADMIN)

路径参数

参数名 类型 必填 说明
activityId Long 活动ID

响应示例

{
  "code": 200,
  "message": "success",
  "data": {
    "qrCodeUrl": "https://example.com/qrcode/ACTIVITY1.png",
    "qrCodeContent": "CHECKIN:1:1736928000:a1b2c3d4e5f6",
    "expiresAt": "2026-01-15T23:00:00"
  },
  "timestamp": 1736620800000
}

5.5.2 学生扫码签到接口

接口地址POST /api/v1/checkin/scan

请求权限:需要认证

请求参数

{
  "qrCodeContent": "CHECKIN:1:1736928000:a1b2c3d4e5f6"
}

响应示例

{
  "code": 200,
  "message": "签到成功",
  "data": {
    "id": 1,
    "userId": 1,
    "userName": "张三",
    "studentId": "2021000001",
    "activityId": 1,
    "checkInTime": "2026-01-15T19:30:00",
    "checkInMethod": 1
  },
  "timestamp": 1736620800000
}

5.5.3 管理员扫票签到接口

接口地址POST /api/v1/checkin/ticket

请求权限:管理员 (ADMIN)

请求参数

{
  "activityId": 1,
  "ticketCode": "TICKET202601150001"
}

响应示例

{
  "code": 200,
  "message": "签到成功",
  "data": {
    "id": 1,
    "userId": 1,
    "userName": "张三",
    "studentId": "2021000001",
    "activityId": 1,
    "checkInTime": "2026-01-15T19:30:00",
    "checkInMethod": 2
  },
  "timestamp": 1736620800000
}

5.5.4 签到列表查询接口

接口地址GET /api/v1/checkin/activity/{activityId}

请求权限:管理员 (ADMIN)

路径参数

参数名 类型 必填 说明
activityId Long 活动ID

请求参数

参数名 类型 必填 说明 默认值
current Long 当前页码 1
size Long 每页数量 10

响应示例

{
  "code": 200,
  "message": "success",
  "data": {
    "records": [
      {
        "id": 1,
        "userId": 1,
        "userName": "张三",
        "studentId": "2021000001",
        "activityId": 1,
        "checkInTime": "2026-01-15T19:30:00",
        "checkInMethod": 1
      }
    ],
    "total": 1,
    "pages": 1,
    "current": 1,
    "size": 10
  },
  "timestamp": 1736620800000
}

5.6 评价接口设计

5.6.1 提交评价接口

接口地址POST /api/v1/reviews

请求权限:需要认证

请求参数

{
  "activityId": 1,
  "rating": 5,
  "content": "非常精彩的活动!"
}

响应示例

{
  "code": 200,
  "message": "评价成功",
  "data": {
    "id": 1,
    "userId": 1,
    "userName": "张三",
    "userAvatar": null,
    "activityId": 1,
    "activityTitle": "校园音乐节",
    "rating": 5,
    "content": "非常精彩的活动!",
    "createdAt": "2026-01-16T10:00:00"
  },
  "timestamp": 1736620800000
}

5.6.2 活动评价列表接口

接口地址GET /api/v1/reviews/activity/{activityId}

请求权限:无需认证

路径参数

参数名 类型 必填 说明
activityId Long 活动ID

请求参数

参数名 类型 必填 说明 默认值
current Long 当前页码 1
size Long 每页数量 10

响应示例

{
  "code": 200,
  "message": "success",
  "data": {
    "records": [
      {
        "id": 1,
        "userId": 1,
        "userName": "张三",
        "userAvatar": null,
        "activityId": 1,
        "activityTitle": "校园音乐节",
        "rating": 5,
        "content": "非常精彩的活动!",
        "createdAt": "2026-01-16T10:00:00"
      }
    ],
    "total": 1,
    "pages": 1,
    "current": 1,
    "size": 10
  },
  "timestamp": 1736620800000
}

5.6.3 我的评价列表接口

接口地址GET /api/v1/reviews/my

请求权限:需要认证

请求参数

参数名 类型 必填 说明 默认值
current Long 当前页码 1
size Long 每页数量 10

响应示例

{
  "code": 200,
  "message": "success",
  "data": {
    "records": [
      {
        "id": 1,
        "userId": 1,
        "userName": "张三",
        "userAvatar": null,
        "activityId": 1,
        "activityTitle": "校园音乐节",
        "rating": 5,
        "content": "非常精彩的活动!",
        "createdAt": "2026-01-16T10:00:00"
      }
    ],
    "total": 1,
    "pages": 1,
    "current": 1,
    "size": 10
  },
  "timestamp": 1736620800000
}

5.7 统计接口设计

5.7.1 活动统计接口

接口地址GET /api/v1/statistics/activity/{activityId}

请求权限:管理员 (ADMIN)

路径参数

参数名 类型 必填 说明
activityId Long 活动ID

响应示例

{
  "code": 200,
  "message": "success",
  "data": {
    "activityId": 1,
    "activityTitle": "校园音乐节",
    "registeredCount": 300,
    "checkedInCount": 250,
    "checkInRate": 0.8333,
    "reviewCount": 20,
    "averageRating": 4.5,
    "ratingDistribution": {
      "1": 0,
      "2": 1,
      "3": 3,
      "4": 6,
      "5": 10
    }
  },
  "timestamp": 1736620800000
}

5.7.2 数据导出接口

接口地址GET /api/v1/statistics/activity/{activityId}/export

请求权限:管理员 (ADMIN)

路径参数

参数名 类型 必填 说明
activityId Long 活动ID

请求参数

参数名 类型 必填 说明 默认值
format String 导出格式 excel

响应:文件流 (Excel或PDF)

5.7.3 总体统计接口

接口地址GET /api/v1/statistics/overview

请求权限:管理员 (ADMIN)

响应示例

{
  "code": 200,
  "message": "success",
  "data": {
    "totalActivities": 50,
    "totalRegistrations": 1000,
    "totalCheckIns": 800,
    "totalReviews": 500,
    "averageRating": 4.3,
    "monthlyStats": [
      {
        "month": "2026-01",
        "activityCount": 10,
        "registrationCount": 200
      },
      {
        "month": "2026-02",
        "activityCount": 15,
        "registrationCount": 300
      }
    ]
  },
  "timestamp": 1736620800000
}

第六章 安全设计

6.1 认证机制设计

6.1.1 JWT Token生成

Access Token生成

public String generateAccessToken(Long userId, String username, Integer role) {
    Date now = new Date();
    Date expiryDate = new Date(now.getTime() + accessTokenExpiration);

    return Jwts.builder()
            .setSubject(String.valueOf(userId))
            .claim("username", username)
            .claim("role", role)
            .setIssuedAt(now)
            .setExpiration(expiryDate)
            .signWith(SignatureAlgorithm.HS256, secret)
            .compact();
}

Refresh Token生成

public String generateRefreshToken(Long userId) {
    Date now = new Date();
    Date expiryDate = new Date(now.getTime() + refreshTokenExpiration);

    return Jwts.builder()
            .setSubject(String.valueOf(userId))
            .setIssuedAt(now)
            .setExpiration(expiryDate)
            .signWith(SignatureAlgorithm.HS256, secret)
            .compact();
}

6.1.2 JWT Token验证

Token验证流程

public boolean validateToken(String token) {
    try {
        Jwts.parser()
            .setSigningKey(secret)
            .parseClaimsJws(token);
        return true;
    } catch (SignatureException ex) {
        // 签名无效
    } catch (MalformedJwtException ex) {
        // Token格式错误
    } catch (ExpiredJwtException ex) {
        // Token已过期
    } catch (UnsupportedJwtException ex) {
        // 不支持的Token
    } catch (IllegalArgumentException ex) {
        // Token为空
    }
    return false;
}

6.1.3 Token刷新策略

Token刷新流程

sequenceDiagram
    participant Client as 客户端
    participant Server as 服务端
    participant DB as 数据库

    Client->>Server: 请求携带Access Token
    Server->>Server: 验证Access Token
    alt Access Token有效
        Server-->>Client: 返回数据
    else Access Token过期
        Client->>Server: 使用Refresh Token刷新
        Server->>DB: 验证Refresh Token
        alt Refresh Token有效
            Server->>Server: 生成新Access Token
            Server-->>Client: 返回新Access Token
        else Refresh Token过期
            Server-->>Client: 返回401需重新登录
        end
    end

6.2 权限控制设计

6.2.1 基于角色的访问控制RBAC

角色定义

角色 角色值 说明
学生 0 普通学生用户
管理员 1 活动管理员

权限配置

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/v1/auth/register", "/api/v1/auth/login").permitAll()
                .requestMatchers("/api/v1/activities/**").permitAll()
                .requestMatchers("/api/v1/registrations/**").authenticated()
                .requestMatchers("/api/v1/reviews/**").authenticated()
                .requestMatchers("/api/v1/checkin/**").authenticated()
                .requestMatchers("/api/v1/statistics/**").hasRole("ADMIN")
                .anyRequest().authenticated())
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

6.2.2 接口权限注解

权限注解使用

// 仅管理员可访问
@PreAuthorize("hasRole('ADMIN')")
@PostMapping
public Result<Long> createActivity(@RequestBody ActivityCreateRequest request) {
    // ...
}

// 已登录用户可访问
@PreAuthorize("isAuthenticated()")
@GetMapping("/my")
public Result<IPage<RegistrationVO>> getMyRegistrations(
    @RequestParam(defaultValue = "1") Long current,
    @RequestParam(defaultValue = "10") Long size) {
    // ...
}

6.3 数据安全设计

6.3.1 密码加密存储

加密算法BCrypt

密码强度要求

  • 最少6个字符
  • 最多20个字符
  • 建议包含字母和数字

加密实现

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

6.3.2 敏感数据传输加密

HTTPS加密

  • 使用HTTPS协议传输所有数据
  • SSL/TLS证书加密
  • 防止中间人攻击

敏感字段脱敏

@Data
public class UserVO {
    private Long id;
    private String username;
    private String name;
    private String studentId;  // 可选脱敏
    private String email;      // 可选脱敏
    private String phone;      // 可选脱敏
    private String avatar;
    private Integer role;
    private Integer status;
}

6.3.3 SQL注入防护

防护措施

  1. 使用MyBatis-Plus的参数化查询
  2. 使用#{}占位符而非${}
  3. 输入参数校验

安全查询示例

// 安全:使用参数化查询
@Select("SELECT * FROM user WHERE username = #{username}")
User selectByUsername(@Param("username") String username);

// 危险:字符串拼接(禁止使用)
@Select("SELECT * FROM user WHERE username = '${username}'")
User selectByUsername(@Param("username") String username);

6.3.4 XSS攻击防护

防护措施

  1. 前端输入校验和转义
  2. 后端输出转义
  3. Content-Security-Policy头

输入校验

@NotBlank(message = "活动名称不能为空")
@Size(max = 100, message = "活动名称不能超过100个字符")
@Pattern(regexp = "^[\\u4e00-\\u9fa5a-zA-Z0-9\\s]+$", message = "活动名称包含非法字符")
private String title;

6.4 接口安全设计

6.4.1 CORS配置

跨域配置

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOriginPatterns("*")
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(3600);
    }
}

6.4.2 请求频率限制

限流策略

  • 登录接口5次/分钟
  • 报名接口10次/分钟
  • 其他接口100次/分钟

限流实现使用Redis

@Component
public class RateLimitInterceptor implements HandlerInterceptor {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String key = "rate_limit:" + request.getRemoteAddr() + ":" + request.getRequestURI();
        Long count = redisTemplate.opsForValue().increment(key);

        if (count == 1) {
            redisTemplate.expire(key, 1, TimeUnit.MINUTES);
        }

        if (count > 100) {
            response.setStatus(429);
            return false;
        }

        return true;
    }
}

6.4.3 CSRF防护

防护措施

  1. 使用JWT Token无状态天然防护CSRF
  2. SameSite Cookie属性
  3. 验证Referer头

SameSite Cookie配置

server.servlet.session.cookie.same-site=strict

第七章 性能设计

7.1 数据库性能优化

7.1.1 索引优化策略

索引设计原则

  1. 为频繁查询的字段创建索引
  2. 为JOIN操作的外键字段创建索引
  3. 为WHERE条件、ORDER BY、GROUP BY字段创建索引
  4. 避免过度索引,影响写入性能

索引优化示例

-- 活动表索引
CREATE INDEX idx_status ON activity(status);
CREATE INDEX idx_start_time ON activity(start_time);
CREATE INDEX idx_admin_id ON activity(admin_id);

-- 报名表索引
CREATE UNIQUE INDEX uk_user_activity ON registration(user_id, activity_id);
CREATE INDEX idx_activity_id ON registration(activity_id);
CREATE UNIQUE INDEX uk_ticket_code ON registration(ticket_code);

-- 评价表索引
CREATE UNIQUE INDEX uk_user_activity ON review(user_id, activity_id);
CREATE INDEX idx_activity_id ON review(activity_id);

7.1.2 查询优化方案

分页查询优化

// 使用MyBatis-Plus分页插件
Page<Activity> page = new Page<>(current, size);
IPage<ActivityVO> result = activityMapper.selectActivityPage(page, params);

复杂查询优化

-- 使用子查询优化
SELECT a.*, COUNT(r.id) as registration_count
FROM activity a
LEFT JOIN registration r ON a.id = r.activity_id AND r.status != 0
WHERE a.deleted = 0
GROUP BY a.id
ORDER BY a.start_time DESC;

7.1.3 分页查询设计

分页参数

  • current当前页码从1开始
  • size每页数量默认10最大100

分页响应

{
  "records": [],
  "total": 100,
  "pages": 10,
  "current": 1,
  "size": 10
}

7.2 缓存设计

缓存策略使用Redis缓存热点数据

缓存数据类型

  1. 活动列表缓存5分钟
  2. 活动详情缓存10分钟
  3. 用户信息缓存30分钟
  4. Token黑名单缓存至Token过期

缓存实现

@Service
public class ActivityServiceImpl implements ActivityService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Cacheable(value = "activity", key = "#id")
    public ActivityVO getActivityById(Long id) {
        // ...
    }

    @CacheEvict(value = "activity", key = "#activity.id")
    public void updateActivity(Activity activity) {
        // ...
    }
}

7.3 并发控制设计

7.3.1 报名并发控制

乐观锁实现

UPDATE activity
SET current_participants = current_participants + 1,
    version = version + 1
WHERE id = ?
  AND current_participants < max_participants
  AND version = ?;

乐观锁流程

sequenceDiagram
    participant User as 用户
    participant Service as 服务
    participant DB as 数据库

    User->>Service: 发起报名
    Service->>DB: 查询活动信息version=1
    DB-->>Service: 返回活动
    Service->>Service: 验证报名条件
    Service->>DB: 更新报名人数<br/>WHERE version=1
    alt 更新成功
        DB-->>Service: 更新成功version=2
        Service-->>User: 报名成功
    else 更新失败
        DB-->>Service: 更新失败
        Service->>Service: 重新查询活动
        Service->>DB: 查询活动信息version=2
        Service->>DB: 更新报名人数<br/>WHERE version=2
        DB-->>Service: 更新成功
        Service-->>User: 报名成功
    end

7.3.2 签到并发控制

唯一约束防止重复签到

CREATE UNIQUE INDEX uk_registration_id ON check_in(registration_id);

签到流程

@Transactional
public void checkIn(Long registrationId) {
    // 检查是否已签到(唯一约束保证)
    CheckIn existing = checkInMapper.selectOne(registrationId);
    if (existing != null) {
        throw new BusinessException(4002, "已签到");
    }

    // 创建签到记录
    CheckIn checkIn = new CheckIn();
    checkIn.setRegistrationId(registrationId);
    checkInMapper.insert(checkIn);

    // 更新报名状态
    Registration registration = registrationMapper.selectById(registrationId);
    registration.setStatus(2); // 已签到
    registrationMapper.updateById(registration);
}

7.4 文件处理优化

7.4.1 PDF生成优化

异步生成PDF

@Async
public CompletableFuture<String> generateTicketPdfAsync(Registration registration) {
    String pdfUrl = pdfUtil.generateTicketPdf(registration);
    return CompletableFuture.completedFuture(pdfUrl);
}

PDF模板缓存

@Component
public class PdfTemplateCache {
    private final Map<String, byte[]> templateCache = new ConcurrentHashMap<>();

    public byte[] getTemplate(String templateName) {
        return templateCache.computeIfAbsent(templateName, name -> {
            // 加载PDF模板
            return loadPdfTemplate(name);
        });
    }
}

7.4.2 文件上传下载优化

文件存储策略

  • 本地存储:开发环境
  • 云存储生产环境阿里云OSS、腾讯云COS

文件压缩

@PostMapping("/upload")
public Result<String> uploadFile(@RequestParam("file") MultipartFile file) {
    // 文件大小限制
    if (file.getSize() > 5 * 1024 * 1024) {
        return Result.error(400, "文件大小不能超过5MB");
    }

    // 文件类型校验
    String contentType = file.getContentType();
    if (!"image/jpeg".equals(contentType) && !"image/png".equals(contentType)) {
        return Result.error(400, "仅支持JPG和PNG格式");
    }

    // 压缩图片
    byte[] compressed = ImageUtil.compress(file.getBytes());
    String url = fileService.upload(compressed, file.getOriginalFilename());

    return Result.success(url);
}

第八章 异常处理设计

8.1 异常分类

异常类型

异常类型 说明 处理方式
系统异常 运行时异常、数据库异常等 全局异常处理器捕获
业务异常 业务规则违反(如已报名、人数已满) 抛出BusinessException
参数异常 请求参数校验失败 返回400错误
认证异常 未认证、Token过期 返回401错误
权限异常 无权限访问 返回403错误

8.2 异常处理机制

自定义业务异常

@Getter
public class BusinessException extends RuntimeException {
    private final Integer code;

    public BusinessException(Integer code, String message) {
        super(message);
        this.code = code;
    }
}

全局异常处理器

@RestControllerAdvice
public class GlobalExceptionHandler {

    // 业务异常
    @ExceptionHandler(BusinessException.class)
    public Result<?> handleBusinessException(BusinessException e) {
        return Result.error(e.getCode(), e.getMessage());
    }

    // 参数校验异常
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<?> handleValidationException(MethodArgumentNotValidException e) {
        String message = e.getBindingResult().getFieldErrors().stream()
            .map(FieldError::getDefaultMessage)
            .collect(Collectors.joining(", "));
        return Result.error(400, message);
    }

    // 认证异常
    @ExceptionHandler(AccessDeniedException.class)
    public Result<?> handleAccessDeniedException(AccessDeniedException e) {
        return Result.error(403, "无权限访问");
    }

    // 系统异常
    @ExceptionHandler(Exception.class)
    public Result<?> handleException(Exception e) {
        log.error("系统异常", e);
        return Result.error(500, "服务器内部错误");
    }
}

8.3 错误码设计

错误码规范

  • 1xxx用户相关错误
  • 2xxx活动相关错误
  • 3xxx报名相关错误
  • 4xxx签到相关错误
  • 5xxx评价相关错误

错误码定义

public enum ResultCode {
    // 通用错误码
    SUCCESS(200, "success"),
    BAD_REQUEST(400, "请求参数错误"),
    UNAUTHORIZED(401, "未认证或Token已过期"),
    FORBIDDEN(403, "无权限访问"),
    NOT_FOUND(404, "资源不存在"),
    CONFLICT(409, "业务冲突"),
    INTERNAL_ERROR(500, "服务器内部错误"),

    // 用户相关错误码 (1xxx)
    USER_NOT_FOUND(1001, "用户不存在"),
    USER_ALREADY_EXISTS(1002, "用户已存在"),
    PASSWORD_ERROR(1003, "密码错误"),
    USERNAME_OR_PASSWORD_ERROR(1004, "用户名或密码错误"),
    STUDENT_ID_ALREADY_EXISTS(1005, "学号已存在"),

    // 活动相关错误码 (2xxx)
    ACTIVITY_NOT_FOUND(2001, "活动不存在"),
    ACTIVITY_ALREADY_STARTED(2002, "活动已开始,无法取消报名"),
    ACTIVITY_TIME_CONFLICT(2003, "活动时间冲突"),
    ACTIVITY_FULL(2004, "活动报名人数已满"),

    // 报名相关错误码 (3xxx)
    REGISTRATION_NOT_FOUND(3001, "报名记录不存在"),
    ALREADY_REGISTERED(3002, "您已报名该活动"),
    NOT_REGISTERED(3003, "您未报名该活动"),

    // 签到相关错误码 (4xxx)
    CHECKIN_FAILED(4001, "签到失败"),
    ALREADY_CHECKED_IN(4002, "已签到"),
    CHECKIN_TIME_EXPIRED(4003, "签到时间已过期"),

    // 评价相关错误码 (5xxx)
    REVIEW_ALREADY_EXISTS(5001, "您已评价该活动"),
    REVIEW_NOT_FOUND(5002, "评价记录不存在"),
    NOT_PARTICIPATED(5003, "您未参加该活动,无法评价");

    private final Integer code;
    private final String message;

    ResultCode(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
}

8.4 日志设计

8.4.1 日志级别定义

日志级别 说明 使用场景
ERROR 错误 系统错误、异常
WARN 警告 潜在问题、业务异常
INFO 信息 重要业务操作
DEBUG 调试 调试信息(生产环境关闭)

8.4.2 日志格式设计

日志格式

%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n

日志示例

2026-01-15 10:00:00 [http-nio-8080-exec-1] INFO  c.c.a.controller.AuthController - 用户登录成功: student001
2026-01-15 10:00:01 [http-nio-8080-exec-2] ERROR c.c.a.exception.GlobalExceptionHandler - 业务异常: 活动报名人数已满

8.4.3 日志存储策略

日志文件配置

logging:
  file:
    name: logs/campus-activity.log
  logback:
    rollingpolicy:
      max-file-size: 10MB
      max-history: 30
      total-size-cap: 1GB

日志保留策略

  • 单个日志文件最大10MB
  • 保留最近30天的日志
  • 日志总大小不超过1GB

第九章 测试设计

9.1 测试策略

测试类型

测试类型 测试内容 测试工具
单元测试 Service层业务逻辑 JUnit 5, Mockito
集成测试 接口集成测试 TestContainers, MockMvc
接口测试 API接口功能测试 Postman, Knife4j

测试覆盖率目标

  • Service层≥ 80%
  • Controller层≥ 70%
  • Mapper层≥ 60%

9.2 测试用例设计

9.2.1 认证模块测试用例

用例编号 测试场景 输入数据 预期结果
AUTH-001 用户注册成功 username: "test001", password: "123456", name: "测试" 注册成功返回200
AUTH-002 用户名已存在 username: "admin", password: "123456", name: "测试" 注册失败返回1002
AUTH-003 学号已存在 username: "test002", studentId: "2021000001" 注册失败返回1005
AUTH-004 用户登录成功 username: "admin", password: "admin123" 登录成功返回Token
AUTH-005 密码错误 username: "admin", password: "wrongpassword" 登录失败返回1004
AUTH-006 Token刷新成功 refreshToken: 有效的refreshToken 刷新成功返回新Token
AUTH-007 Token过期 refreshToken: 过期的refreshToken 刷新失败返回401

9.2.2 活动管理模块测试用例

用例编号 测试场景 输入数据 预期结果
ACT-001 创建活动成功 title: "测试活动", startTime: "2026-02-01 10:00:00" 创建成功返回活动ID
ACT-002 活动时间冲突 startTime: "2026-01-15 19:00:00", endTime: "2026-01-15 22:00:00" 创建失败返回2003
ACT-003 查询活动列表 status: 1, current: 1, size: 10 返回活动列表
ACT-004 查询活动详情 id: 1 返回活动详情
ACT-005 活动不存在 id: 99999 返回404
ACT-006 更新活动成功 id: 1, title: "更新后的活动" 更新成功
ACT-007 删除活动成功 id: 1 删除成功

9.2.3 报名管理模块测试用例

用例编号 测试场景 输入数据 预期结果
REG-001 报名成功 activityId: 1 报名成功,生成电子票
REG-002 活动不存在 activityId: 99999 报名失败返回2001
REG-003 已报名该活动 activityId: 1已报名 报名失败返回3002
REG-004 报名人数已满 activityId: 1已满 报名失败返回2004
REG-005 时间冲突 activityId: 2时间冲突 报名失败返回2003
REG-006 取消报名成功 id: 1 取消成功
REG-007 活动已开始 id: 1活动已开始 取消失败返回2002

9.2.4 签到管理模块测试用例

用例编号 测试场景 输入数据 预期结果
CHK-001 生成签到二维码 activityId: 1 生成成功返回二维码URL
CHK-002 扫码签到成功 qrCodeContent: 有效的二维码 签到成功
CHK-003 二维码无效 qrCodeContent: "invalid" 签到失败返回400
CHK-004 未报名该活动 qrCodeContent: 有效的二维码(未报名) 签到失败返回3003
CHK-005 已签到 qrCodeContent: 有效的二维码(已签到) 签到失败返回4002
CHK-006 签到时间已过期 qrCodeContent: 有效的二维码(已过期) 签到失败返回4003
CHK-007 管理员扫票签到 activityId: 1, ticketCode: "TICKET202601150001" 签到成功

9.2.5 评价管理模块测试用例

用例编号 测试场景 输入数据 预期结果
REV-001 提交评价成功 activityId: 1, rating: 5, content: "很好" 评价成功
REV-002 已评价该活动 activityId: 1已评价 评价失败返回5001
REV-003 未参加该活动 activityId: 1未参加 评价失败返回5003
REV-004 评分超出范围 activityId: 1, rating: 6 参数校验失败
REV-005 查询活动评价列表 activityId: 1 返回评价列表
REV-006 查询我的评价列表 - 返回我的评价列表

第十章 部署设计

10.1 部署架构图

graph TB
    subgraph "外网 Internet"
        User[用户浏览器]
    end

    subgraph "DMZ区"
        Firewall[防火墙]
        Nginx[Nginx<br/>反向代理]
    end

    subgraph "内网 Intranet"
        AppServer[应用服务器<br/>Spring Boot<br/>端口:8080]
        DBServer[数据库服务器<br/>MySQL<br/>端口:3306]
        FileServer[文件服务器<br/>静态资源]
    end

    User --> Firewall
    Firewall --> Nginx
    Nginx --> AppServer
    Nginx --> FileServer
    AppServer --> DBServer

10.2 部署步骤

10.2.1 数据库部署

1. 安装MySQL

# Ubuntu/Debian
sudo apt-get update
sudo apt-get install mysql-server

# CentOS/RHEL
sudo yum install mysql-server

2. 创建数据库

CREATE DATABASE campus_activity
DEFAULT CHARACTER SET utf8mb4
COLLATE utf8mb4_general_ci;

3. 执行建表脚本

mysql -u root -p campus_activity < init.sql

4. 创建数据库用户

CREATE USER 'campus_user'@'%' IDENTIFIED BY 'your_password';
GRANT ALL PRIVILEGES ON campus_activity.* TO 'campus_user'@'%';
FLUSH PRIVILEGES;

10.2.2 后端部署

1. 打包应用

cd server
mvn clean package -DskipTests

2. 上传jar包

scp target/campus-activity-system-1.0.0.jar user@server:/opt/app/

3. 创建启动脚本

#!/bin/bash
APP_NAME="campus-activity-system"
JAR_NAME="$APP_NAME-1.0.0.jar"
PID=$(ps -ef | grep $JAR_NAME | grep -v grep | awk '{print $2}')

if [ -n "$PID" ]; then
    echo "Stopping $APP_NAME (PID: $PID)..."
    kill -15 $PID
    sleep 5
fi

echo "Starting $APP_NAME..."
nohup java -jar $JAR_NAME \
    --spring.profiles.active=prod \
    > /dev/null 2>&1 &

echo "$APP_NAME started successfully!"

4. 配置systemd服务

[Unit]
Description=Campus Activity System
After=network.target

[Service]
Type=simple
User=appuser
WorkingDirectory=/opt/app
ExecStart=/usr/bin/java -jar /opt/app/campus-activity-system-1.0.0.jar --spring.profiles.active=prod
Restart=on-failure
RestartSec=10

[Install]
WantedBy=multi-user.target

5. 启动服务

sudo systemctl daemon-reload
sudo systemctl enable campus-activity
sudo systemctl start campus-activity
sudo systemctl status campus-activity

10.2.3 前端部署

1. 打包前端

cd web
npm run build

2. 上传静态文件

scp -r dist/* user@server:/var/www/campus-activity/

3. 配置Nginx

server {
    listen 80;
    server_name campus.example.com;

    # 前端静态文件
    location / {
        root /var/www/campus-activity;
        index index.html;
        try_files $uri $uri/ /index.html;
    }

    # 后端API代理
    location /api/ {
        proxy_pass http://localhost:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # 静态资源缓存
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
}

4. 重启Nginx

sudo nginx -t
sudo systemctl reload nginx

10.3 配置管理

10.3.1 开发环境配置

application-dev.yml

server:
  port: 8080

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/campus_activity?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: root
    password: root

  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: Asia/Shanghai

mybatis-plus:
  mapper-locations: classpath:/mapper/**/*.xml
  global-config:
    db-config:
      id-type: auto
      logic-delete-field: deleted
      logic-delete-value: 1
      logic-not-delete-value: 0

jwt:
  secret: dev-secret-key-change-in-production
  expiration: 7200000
  refresh-expiration: 604800000

logging:
  level:
    root: INFO
    com.campus.activity: DEBUG

10.3.2 测试环境配置

application-test.yml

server:
  port: 8080

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://test-db-server:3306/campus_activity?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: campus_test
    password: test_password

  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: Asia/Shanghai

mybatis-plus:
  mapper-locations: classpath:/mapper/**/*.xml
  global-config:
    db-config:
      id-type: auto
      logic-delete-field: deleted
      logic-delete-value: 1
      logic-not-delete-value: 0

jwt:
  secret: test-secret-key
  expiration: 7200000
  refresh-expiration: 604800000

logging:
  level:
    root: INFO
    com.campus.activity: DEBUG
  file:
    name: logs/campus-activity-test.log

10.3.3 生产环境配置

application-prod.yml

server:
  port: 8080

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://prod-db-server:3306/campus_activity?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: campus_prod
    password: ${DB_PASSWORD}

  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: Asia/Shanghai

mybatis-plus:
  mapper-locations: classpath:/mapper/**/*.xml
  global-config:
    db-config:
      id-type: auto
      logic-delete-field: deleted
      logic-delete-value: 1
      logic-not-delete-value: 0

jwt:
  secret: ${JWT_SECRET}
  expiration: 7200000
  refresh-expiration: 604800000

logging:
  level:
    root: WARN
    com.campus.activity: INFO
  file:
    name: /var/log/campus-activity/campus-activity.log
  logback:
    rollingpolicy:
      max-file-size: 10MB
      max-history: 30
      total-size-cap: 1GB

环境变量配置

# /etc/profile.d/campus-activity.sh
export DB_PASSWORD=your_secure_password
export JWT_SECRET=your_jwt_secret_key

附录

附录A 完整数据库建表SQL脚本

-- 创建数据库
CREATE DATABASE IF NOT EXISTS `campus_activity`
DEFAULT CHARACTER SET utf8mb4
COLLATE utf8mb4_general_ci;

USE `campus_activity`;

-- 创建用户表
CREATE TABLE `user` (
    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '用户ID',
    `username` VARCHAR(50) NOT NULL COMMENT '登录用户名',
    `password` VARCHAR(255) NOT NULL COMMENT '密码',
    `name` VARCHAR(50) NOT NULL COMMENT '真实姓名',
    `student_id` VARCHAR(20) DEFAULT NULL COMMENT '学号',
    `email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱',
    `phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号',
    `avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像URL',
    `role` TINYINT NOT NULL DEFAULT 0 COMMENT '角色0-学生1-管理员',
    `status` TINYINT DEFAULT 1 COMMENT '状态0-禁用1-正常',
    `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updated_at` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `deleted` TINYINT DEFAULT 0 COMMENT '逻辑删除',
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_username` (`username`),
    UNIQUE KEY `uk_student_id` (`student_id`),
    KEY `idx_role` (`role`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户表';

-- 创建活动表
CREATE TABLE `activity` (
    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '活动ID',
    `title` VARCHAR(100) NOT NULL COMMENT '活动名称',
    `description` TEXT DEFAULT NULL COMMENT '活动简介',
    `cover_image` VARCHAR(255) DEFAULT NULL COMMENT '封面图片',
    `start_time` DATETIME NOT NULL COMMENT '开始时间',
    `end_time` DATETIME NOT NULL COMMENT '结束时间',
    `registration_deadline` DATETIME DEFAULT NULL COMMENT '报名截止时间',
    `location` VARCHAR(200) NOT NULL COMMENT '活动地点',
    `max_participants` INT NOT NULL COMMENT '报名人数上限',
    `current_participants` INT DEFAULT 0 COMMENT '当前报名人数',
    `status` TINYINT DEFAULT 0 COMMENT '状态0-未开始1-报名中2-进行中3-已结束',
    `category` VARCHAR(50) DEFAULT NULL COMMENT '活动分类',
    `admin_id` BIGINT NOT NULL COMMENT '创建者ID',
    `qr_code` VARCHAR(255) DEFAULT NULL COMMENT '签到二维码',
    `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updated_at` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `deleted` TINYINT DEFAULT 0 COMMENT '逻辑删除',
    `version` INT DEFAULT 0 COMMENT '乐观锁版本号',
    PRIMARY KEY (`id`),
    KEY `idx_status` (`status`),
    KEY `idx_start_time` (`start_time`),
    KEY `idx_admin_id` (`admin_id`),
    CONSTRAINT `fk_activity_admin` FOREIGN KEY (`admin_id`) REFERENCES `user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='活动表';

-- 创建报名表
CREATE TABLE `registration` (
    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '报名ID',
    `user_id` BIGINT NOT NULL COMMENT '用户ID',
    `activity_id` BIGINT NOT NULL COMMENT '活动ID',
    `ticket_code` VARCHAR(100) DEFAULT NULL COMMENT '电子票唯一码',
    `ticket_pdf_url` VARCHAR(255) DEFAULT NULL COMMENT '电子票PDF地址',
    `status` TINYINT DEFAULT 1 COMMENT '状态0-已取消1-已报名2-已签到',
    `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '报名时间',
    `updated_at` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `canceled_at` DATETIME DEFAULT NULL COMMENT '取消时间',
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_user_activity` (`user_id`, `activity_id`),
    UNIQUE KEY `uk_ticket_code` (`ticket_code`),
    KEY `idx_activity_id` (`activity_id`),
    CONSTRAINT `fk_registration_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`),
    CONSTRAINT `fk_registration_activity` FOREIGN KEY (`activity_id`) REFERENCES `activity` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='报名表';

-- 创建签到表
CREATE TABLE `check_in` (
    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '签到ID',
    `registration_id` BIGINT NOT NULL COMMENT '报名ID',
    `user_id` BIGINT NOT NULL COMMENT '用户ID',
    `activity_id` BIGINT NOT NULL COMMENT '活动ID',
    `check_in_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '签到时间',
    `check_in_method` TINYINT DEFAULT 0 COMMENT '签到方式0-扫码1-管理员代签',
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_registration_id` (`registration_id`),
    KEY `idx_activity_id` (`activity_id`),
    KEY `idx_user_id` (`user_id`),
    CONSTRAINT `fk_checkin_registration` FOREIGN KEY (`registration_id`) REFERENCES `registration` (`id`),
    CONSTRAINT `fk_checkin_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`),
    CONSTRAINT `fk_checkin_activity` FOREIGN KEY (`activity_id`) REFERENCES `activity` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='签到表';

-- 创建评价表
CREATE TABLE `review` (
    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '评价ID',
    `user_id` BIGINT NOT NULL COMMENT '用户ID',
    `activity_id` BIGINT NOT NULL COMMENT '活动ID',
    `rating` TINYINT NOT NULL COMMENT '评分1-5',
    `content` TEXT DEFAULT NULL COMMENT '评论内容',
    `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '评价时间',
    `updated_at` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_user_activity` (`user_id`, `activity_id`),
    KEY `idx_activity_id` (`activity_id`),
    CONSTRAINT `fk_review_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`),
    CONSTRAINT `fk_review_activity` FOREIGN KEY (`activity_id`) REFERENCES `activity` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='评价表';

-- 插入测试管理员账号密码admin123BCrypt加密
INSERT INTO `user` (`username`, `password`, `name`, `role`) VALUES
('admin', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iKXP6X3SqmI8Q0WJLoUbWAHZJ.5i', '系统管理员', 1);

附录B 关键业务流程时序图

B.1 用户认证流程时序图

sequenceDiagram
    autonumber
    participant Client as 客户端
    participant Controller as AuthController
    participant Service as AuthService
    participant Security as SecurityConfig
    participant JwtProvider as JwtTokenProvider
    participant Mapper as UserMapper
    participant DB as MySQL数据库

    Note over Client,DB: 用户注册流程
    Client->>Controller: POST /api/v1/auth/register
    Controller->>Service: register(request)
    Service->>Mapper: 检查用户名是否存在
    Mapper->>DB: SELECT COUNT(*) FROM user WHERE username = ?
    DB-->>Mapper: 返回数量
    Mapper-->>Service: 返回数量
    alt 用户名已存在
        Service-->>Controller: 抛出异常 USER_ALREADY_EXISTS
        Controller-->>Client: 400 错误
    else 用户名不存在
        Service->>Mapper: 检查学号是否存在
        Mapper->>DB: SELECT COUNT(*) FROM user WHERE student_id = ?
        DB-->>Mapper: 返回数量
        Mapper-->>Service: 返回数量
        alt 学号已存在
            Service-->>Controller: 抛出异常 STUDENT_ID_ALREADY_EXISTS
            Controller-->>Client: 400 错误
        else 学号不存在
            Service->>Service: 创建用户对象 passwordEncoder.encode()
            Service->>Mapper: insert(user)
            Mapper->>DB: INSERT INTO user(...)
            DB-->>Mapper: 返回插入ID
            Mapper-->>Service: 返回结果
            Service-->>Controller: 注册成功
            Controller-->>Client: 200 成功
        end
    end

    Note over Client,DB: 用户登录流程
    Client->>Controller: POST /api/v1/auth/login
    Controller->>Security: authenticationManager.authenticate()
    Security->>Mapper: loadUserByUsername()
    Mapper->>DB: SELECT * FROM user WHERE username = ?
    DB-->>Mapper: 返回用户信息
    Mapper-->>Security: 返回UserDetails
    Security->>Security: 验证密码 passwordEncoder.matches()
    alt 密码错误
        Security-->>Controller: 抛出异常
        Controller-->>Client: 401 未授权
    else 密码正确
        Security-->>Controller: 认证成功
        Controller->>Service: login(request)
        Service->>Mapper: selectById(username)
        Mapper->>DB: SELECT * FROM user WHERE username = ?
        DB-->>Mapper: 返回用户信息
        Mapper-->>Service: 返回User对象
        Service->>JwtProvider: generateToken(userId, username, role)
        JwtProvider-->>Service: 返回accessToken
        Service->>JwtProvider: generateRefreshToken(userId)
        JwtProvider-->>Service: 返回refreshToken
        Service-->>Controller: 返回LoginResponse
        Controller-->>Client: 200 成功
    end

B.2 活动报名流程时序图

sequenceDiagram
    autonumber
    participant Client as 客户端
    participant Controller as RegistrationController
    participant Service as RegistrationService
    participant ActMapper as ActivityMapper
    participant RegMapper as RegistrationMapper
    participant Auth as AuthService
    participant PdfUtil as PdfUtil
    participant DB as MySQL数据库

    Note over Client,DB: 报名活动流程
    Client->>Controller: POST /api/v1/registrations
    Controller->>Service: register(request)
    Service->>Auth: getCurrentUser()
    Auth-->>Service: 返回当前用户
    Service->>ActMapper: selectById(activityId)
    ActMapper->>DB: SELECT * FROM activity WHERE id = ?
    DB-->>ActMapper: 返回活动信息
    ActMapper-->>Service: 返回Activity对象
    alt 活动不存在或已删除
        Service-->>Controller: 抛出异常 ACTIVITY_NOT_FOUND
        Controller-->>Client: 404 未找到
    else 活动存在
        alt 活动不在报名中
            Service-->>Controller: 抛出异常 BAD_REQUEST
            Controller-->>Client: 400 错误
        else 活动在报名中
            alt 报名已截止
                Service-->>Controller: 抛出异常 BAD_REQUEST
                Controller-->>Client: 400 错误
            else 报名未截止
                alt 报名人数已满
                    Service-->>Controller: 抛出异常 ACTIVITY_FULL
                    Controller-->>Client: 409 冲突
                else 报名未满
                    Service->>RegMapper: 检查是否已报名
                    RegMapper->>DB: SELECT * FROM registration WHERE user_id = ? AND activity_id = ?
                    DB-->>RegMapper: 返回报名记录
                    RegMapper-->>Service: 返回Registration对象
                    alt 已报名该活动
                        Service-->>Controller: 抛出异常 ALREADY_REGISTERED
                        Controller-->>Client: 409 冲突
                    else 未报名该活动
                        Service->>Service: 检查时间冲突
                        alt 存在时间冲突
                            Service-->>Controller: 抛出异常 ACTIVITY_TIME_CONFLICT
                            Controller-->>Client: 409 冲突
                        else 无时间冲突
                            Service->>Service: 生成电子票码
                            Service->>RegMapper: insert(registration)
                            RegMapper->>DB: INSERT INTO registration(...)
                            DB-->>RegMapper: 返回插入ID
                            RegMapper-->>Service: 返回registrationId
                            Service->>ActMapper: 更新活动报名人数
                            ActMapper->>DB: UPDATE activity SET current_participants = current_participants + 1
                            DB-->>ActMapper: 返回更新结果
                            ActMapper-->>Service: 返回成功
                            Service->>PdfUtil: 生成电子票PDF
                            PdfUtil-->>Service: 返回PDF URL
                            Service-->>Controller: 返回RegistrationVO
                            Controller-->>Client: 200 成功
                        end
                    end
                end
            end
        end
    end

B.3 签到管理流程时序图

sequenceDiagram
    autonumber
    participant Client as 客户端
    participant Controller as CheckInController
    participant Service as CheckInService
    participant Mapper as CheckInMapper
    participant RegMapper as RegistrationMapper
    participant ActMapper as ActivityMapper
    participant Auth as AuthService
    participant QrUtil as QrCodeUtil
    participant DB as MySQL数据库

    Note over Client,DB: 学生扫码签到流程
    Client->>Controller: POST /api/v1/checkin/scan
    Controller->>Service: scanCheckIn(request)
    Service->>Auth: getCurrentUser()
    Auth-->>Service: 返回当前用户
    Service->>QrUtil: parseActivityIdFromQrCode(qrCodeContent)
    QrUtil-->>Service: 返回activityId
    alt 二维码无效
        Service-->>Controller: 抛出异常 BAD_REQUEST
        Controller-->>Client: 400 错误
    else 二维码有效
        Service->>ActMapper: selectById(activityId)
        ActMapper->>DB: SELECT * FROM activity WHERE id = ?
        DB-->>ActMapper: 返回活动信息
        ActMapper-->>Service: 返回Activity对象
        alt 活动不存在或已删除
            Service-->>Controller: 抛出异常 ACTIVITY_NOT_FOUND
            Controller-->>Client: 404 未找到
        else 活动存在
            Service->>Service: 检查签到时间
            alt 签到时间已过期
                Service-->>Controller: 抛出异常 CHECKIN_TIME_EXPIRED
                Controller-->>Client: 400 错误
            else 签到时间有效
                Service->>RegMapper: 查询报名记录
                RegMapper->>DB: SELECT * FROM registration WHERE user_id = ? AND activity_id = ? AND status = 1
                DB-->>RegMapper: 返回报名记录
                RegMapper-->>Service: 返回Registration对象
                alt 未报名该活动
                    Service-->>Controller: 抛出异常 NOT_REGISTERED
                    Controller-->>Client: 400 错误
                else 已报名该活动
                    Service->>Mapper: 检查是否已签到
                    Mapper->>DB: SELECT * FROM check_in WHERE registration_id = ?
                    DB-->>Mapper: 返回签到记录
                    Mapper-->>Service: 返回CheckIn对象
                    alt 已签到
                        Service-->>Controller: 抛出异常 ALREADY_CHECKED_IN
                        Controller-->>Client: 409 冲突
                    else 未签到
                        Service->>Mapper: insert(checkIn)
                        Mapper->>DB: INSERT INTO check_in(...)
                        DB-->>Mapper: 返回插入ID
                        Mapper-->>Service: 返回checkInId
                        Service->>RegMapper: 更新报名状态为已签到
                        RegMapper->>DB: UPDATE registration SET status = 2
                        DB-->>RegMapper: 返回更新结果
                        RegMapper-->>Service: 返回成功
                        Service-->>Controller: 返回CheckInVO
                        Controller-->>Client: 200 成功
                    end
                end
            end
        end
    end

附录C 核心类图设计

classDiagram
    class AuthController {
        -AuthService authService
        +register(request)
        +login(request)
        +refreshToken(request)
        +getCurrentUser()
        +changePassword(request)
    }

    class ActivityController {
        -ActivityService activityService
        +pageActivities(page, params)
        +getActivityById(id)
        +createActivity(request)
        +updateActivity(id, request)
        +deleteActivity(id)
        +getCalendarActivities(year, month)
        +checkConflict(request)
    }

    class RegistrationController {
        -RegistrationService registrationService
        +register(request)
        +cancelRegistration(id)
        +getMyRegistrations(page, status)
        +getActivityRegistrations(activityId, page)
        +downloadTicketPdf(id)
    }

    class CheckInController {
        -CheckInService checkInService
        +generateQrCode(activityId)
        +scanCheckIn(request)
        +ticketCheckIn(request)
        +getActivityCheckIns(activityId, page)
    }

    class ReviewController {
        -ReviewService reviewService
        +createReview(request)
        +getActivityReviews(activityId, page)
        +getMyReviews(page)
    }

    class StatisticsController {
        -StatisticsService statisticsService
        +getActivityStatistics(activityId)
        +exportActivityData(activityId, format)
        +getOverallStatistics()
    }

    class ActivityService {
        <<interface>>
        +pageActivities(page, params)
        +getActivityById(id)
        +createActivity(request, adminId)
        +updateActivity(id, request, adminId)
        +deleteActivity(id, adminId)
    }

    class RegistrationService {
        <<interface>>
        +register(userId, activityId)
        +cancelRegistration(userId, registrationId)
    }

    class CheckInService {
        <<interface>>
        +generateQrCode(activityId, adminId)
        +scanCheckIn(userId, qrCodeContent)
    }

    class ReviewService {
        <<interface>>
        +createReview(userId, request)
        +getActivityReviews(activityId, page)
    }

    class StatisticsService {
        <<interface>>
        +getActivityStatistics(activityId)
        +exportActivityData(activityId, format)
    }

    AuthController --> AuthService
    ActivityController --> ActivityService
    RegistrationController --> RegistrationService
    CheckInController --> CheckInService
    ReviewController --> ReviewService
    StatisticsController --> StatisticsService

文档结束