upload: 上传java后端成果物

This commit is contained in:
2026-01-12 13:50:43 +08:00
parent 10cd9f1d06
commit d97a4b02d9
95 changed files with 8877 additions and 0 deletions

8
server/.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

18
server/.idea/compiler.xml generated Normal file
View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="campus-activity-system" />
</profile>
</annotationProcessing>
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="campus-activity-system" options="-parameters" />
</option>
</component>
</project>

6
server/.idea/encodings.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
</component>
</project>

20
server/.idea/jarRepositories.xml generated Normal file
View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.maven.apache.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
</component>
</project>

12
server/.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="zulu-21" project-jdk-type="JavaSDK" />
</project>

6
server/.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

156
server/README.md Normal file
View File

@@ -0,0 +1,156 @@
# 校园活动组织与报名系统
基于 Spring Boot 3.x 的校园活动组织与报名系统后端项目。
## 技术栈
- Java 21
- Spring Boot 3.2.5
- Spring Security 6.x
- MyBatis-Plus 3.5.5
- MySQL 8.0+
- JWT (jjwt 0.12.5)
- Knife4j 4.4.0
- Hutool 5.8.25
- ZXing 3.5.2 (二维码)
- iText 8.0.2 (PDF)
- EasyExcel 3.3.4
## 项目结构
```
campus-activity-system/
├── src/main/java/com/campus/activity/
│ ├── CampusActivityApplication.java # 启动类
│ ├── config/ # 配置类
│ ├── controller/ # 控制器层
│ ├── service/ # 服务层
│ ├── mapper/ # 数据访问层
│ ├── entity/ # 实体类
│ ├── dto/ # 数据传输对象
│ ├── vo/ # 视图对象
│ ├── common/ # 公共模块
│ ├── exception/ # 异常处理
│ ├── security/ # 安全模块
│ └── util/ # 工具类
├── src/main/resources/
│ ├── application.yml
│ └── mapper/ # MyBatis XML映射文件
├── docs/
│ └── init.sql # 数据库初始化脚本
└── pom.xml
```
## 快速开始
### 1. 环境要求
- JDK 21+
- MySQL 8.0+
- Maven 3.6+
### 2. 数据库初始化
执行 `docs/init.sql` 脚本创建数据库和表:
```bash
mysql -u root -p < docs/init.sql
```
### 3. 配置数据库连接
修改 `src/main/resources/application.yml` 中的数据库配置:
```yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/campus_activity?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: your_password
```
### 4. 运行项目
```bash
mvn spring-boot:run
```
或者在 IDE 中运行 `CampusActivityApplication.java`
### 5. 访问 API 文档
启动成功后,访问 Knife4j API 文档:
```
http://localhost:8080/doc.html
```
## 默认账号
- 管理员账号:`admin` / `admin123`
## API 接口
### 认证模块
- POST `/api/v1/auth/register` - 用户注册
- POST `/api/v1/auth/login` - 用户登录
- POST `/api/v1/auth/refresh` - 刷新Token
- GET `/api/v1/auth/me` - 获取当前用户信息
- PUT `/api/v1/auth/password` - 修改密码
### 活动模块
- GET `/api/v1/activities` - 活动列表
- GET `/api/v1/activities/{id}` - 活动详情
- POST `/api/v1/activities` - 创建活动(管理员)
- PUT `/api/v1/activities/{id}` - 更新活动(管理员)
- DELETE `/api/v1/activities/{id}` - 删除活动(管理员)
- GET `/api/v1/activities/calendar` - 日历视图
- POST `/api/v1/activities/check-conflict` - 时间冲突检测
### 报名模块
- POST `/api/v1/registrations` - 报名活动
- DELETE `/api/v1/registrations/{id}` - 取消报名
- GET `/api/v1/registrations/my` - 我的报名列表
- GET `/api/v1/registrations/activity/{activityId}` - 活动报名列表(管理员)
- GET `/api/v1/registrations/{id}/ticket` - 下载电子票PDF
### 签到模块
- POST `/api/v1/checkin/qrcode/{activityId}` - 生成签到二维码(管理员)
- POST `/api/v1/checkin/scan` - 学生扫码签到
- POST `/api/v1/checkin/ticket` - 管理员扫票签到
- GET `/api/v1/checkin/activity/{activityId}` - 签到列表(管理员)
### 评价模块
- POST `/api/v1/reviews` - 提交评价
- GET `/api/v1/reviews/activity/{activityId}` - 活动评价列表
- GET `/api/v1/reviews/my` - 我的评价列表
### 统计模块
- GET `/api/v1/statistics/activity/{activityId}` - 活动统计(管理员)
- GET `/api/v1/statistics/activity/{activityId}/export` - 导出活动数据(管理员)
- GET `/api/v1/statistics/overview` - 总体统计(管理员)
## 开发说明
### 代码规范
- 遵循阿里巴巴 Java 开发手册
- 使用 Lombok 简化代码
- 统一异常处理
- 统一响应格式
### 安全说明
- 使用 JWT 进行身份认证
- 密码使用 BCrypt 加密存储
- 接口权限控制基于角色
## 许可证
Apache License 2.0

File diff suppressed because it is too large Load Diff

110
server/docs/init.sql Normal file
View File

@@ -0,0 +1,110 @@
-- 创建数据库
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);

105
server/docs/sample_data.sql Normal file
View File

@@ -0,0 +1,105 @@
-- 校园活动组织与报名系统 - 示例数据
-- 执行前请确保已运行 init.sql 创建数据库和表结构
USE `campus_activity`;
-- ============================================
-- 1. 插入示例学生账户密码123456
-- ============================================
-- 密码 "123456" 的 BCrypt 加密值
INSERT INTO `user` (`username`, `password`, `name`, `student_id`, `email`, `phone`, `role`, `status`) VALUES
('student01', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iKXP6X3SqmI8Q0WJLoUbWAHZJ.5i', '张三', '2024001', 'zhangsan@example.com', '13800138001', 0, 1),
('student02', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iKXP6X3SqmI8Q0WJLoUbWAHZJ.5i', '李四', '2024002', 'lisi@example.com', '13800138002', 0, 1),
('student03', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iKXP6X3SqmI8Q0WJLoUbWAHZJ.5i', '王五', '2024003', 'wangwu@example.com', '13800138003', 0, 1),
('student04', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iKXP6X3SqmI8Q0WJLoUbWAHZJ.5i', '赵六', '2024004', 'zhaoliu@example.com', '13800138004', 0, 1),
('student05', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iKXP6X3SqmI8Q0WJLoUbWAHZJ.5i', '孙七', '2024005', 'sunqi@example.com', '13800138005', 0, 1),
('student06', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iKXP6X3SqmI8Q0WJLoUbWAHZJ.5i', '周八', '2024006', 'zhouba@example.com', '13800138006', 0, 1),
('student07', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iKXP6X3SqmI8Q0WJLoUbWAHZJ.5i', '吴九', '2024007', 'wujiu@example.com', '13800138007', 0, 1),
('student08', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iKXP6X3SqmI8Q0WJLoUbWAHZJ.5i', '郑十', '2024008', 'zhengshi@example.com', '13800138008', 0, 1),
('student09', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iKXP6X3SqmI8Q0WJLoUbWAHZJ.5i', '陈十一', '2024009', 'chenshiyi@example.com', '13800138009', 0, 1),
('student10', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iKXP6X3SqmI8Q0WJLoUbWAHZJ.5i', '刘十二', '2024010', 'liushier@example.com', '13800138010', 0, 1);
-- ============================================
-- 2. 插入示例活动数据
-- ============================================
INSERT INTO `activity` (`title`, `description`, `cover_image`, `start_time`, `end_time`, `registration_deadline`, `location`, `max_participants`, `current_participants`, `status`, `category`, `admin_id`) VALUES
('科技创新大赛', '校园年度科技创新大赛,展示学生创新成果,激发创新热情', 'https://example.com/images/tech.jpg', '2026-01-15 09:00:00', '2026-01-15 17:00:00', '2026-01-14 23:59:59', '学术报告厅', 100, 0, 1, '科技竞赛', 1),
('校园歌手大赛', '展示青春风采,唱响校园旋律', 'https://example.com/images/singer.jpg', '2026-01-20 19:00:00', '2026-01-20 22:00:00', '2026-01-19 18:00:00', '大学生活动中心', 300, 0, 1, '文艺娱乐', 1),
('编程马拉松', '48小时极限编程挑战团队协作完成创新项目', 'https://example.com/images/code.jpg', '2026-01-25 09:00:00', '2026-01-27 09:00:00', '2026-01-23 23:59:59', '计算机学院实验室', 50, 0, 1, '科技竞赛', 1),
('读书分享会', '分享阅读心得,交流思想感悟', 'https://example.com/images/book.jpg', '2026-01-18 14:00:00', '2026-01-18 16:30:00', '2026-01-17 12:00:00', '图书馆报告厅', 80, 0, 1, '学术讲座', 1),
('篮球友谊赛', '增进友谊,强健体魄,展现体育精神', 'https://example.com/images/basketball.jpg', '2026-01-22 16:00:00', '2026-01-22 18:00:00', '2026-01-21 12:00:00', '体育馆', 40, 0, 1, '体育活动', 1),
('创业讲座', '邀请成功企业家分享创业经验', 'https://example.com/images/business.jpg', '2026-01-28 14:00:00', '2026-01-28 17:00:00', '2026-01-27 12:00:00', '商学院报告厅', 150, 0, 1, '学术讲座', 1),
('摄影展', '展示校园风光,记录美好瞬间', 'https://example.com/images/photo.jpg', '2026-01-30 09:00:00', '2026-01-31 18:00:00', '2026-01-29 12:00:00', '艺术楼展厅', 200, 0, 1, '文艺娱乐', 1),
('英语角活动', '提升英语口语能力,结交志同道合的朋友', 'https://example.com/images/english.jpg', '2026-01-16 19:00:00', '2026-01-16 21:00:00', '2026-01-15 18:00:00', '外语学院活动室', 30, 0, 1, '学术讲座', 1);
-- ============================================
-- 3. 插入示例报名数据
-- ============================================
-- 学生报名活动
INSERT INTO `registration` (`user_id`, `activity_id`, `ticket_code`, `status`) VALUES
(2, 1, 'TICKET-20260115001', 1), -- 张三报名科技创新大赛
(3, 1, 'TICKET-20260115002', 1), -- 李四报名科技创新大赛
(4, 1, 'TICKET-20260115003', 1), -- 王五报名科技创新大赛
(2, 2, 'TICKET-20260120001', 1), -- 张三报名校园歌手大赛
(5, 2, 'TICKET-20260120002', 1), -- 赵六报名校园歌手大赛
(6, 2, 'TICKET-20260120003', 1), -- 孙七报名校园歌手大赛
(7, 2, 'TICKET-20260120004', 1), -- 周八报名校园歌手大赛
(3, 3, 'TICKET-20260125001', 1), -- 李四报名编程马拉松
(4, 3, 'TICKET-20260125002', 1), -- 王五报名编程马拉松
(8, 3, 'TICKET-20260125003', 1), -- 吴九报名编程马拉松
(2, 4, 'TICKET-20260118001', 1), -- 张三报名读书分享会
(5, 4, 'TICKET-20260118002', 1), -- 赵六报名读书分享会
(9, 4, 'TICKET-20260118003', 1), -- 郑十报名读书分享会
(3, 5, 'TICKET-20260122001', 1), -- 李四报名篮球友谊赛
(6, 5, 'TICKET-20260122002', 1), -- 孙七报名篮球友谊赛
(10, 5, 'TICKET-20260122003', 1), -- 刘十二报名篮球友谊赛
(4, 6, 'TICKET-20260128001', 1), -- 王五报名创业讲座
(7, 6, 'TICKET-20260128002', 1), -- 周八报名创业讲座
(2, 7, 'TICKET-20260130001', 1), -- 张三报名摄影展
(5, 7, 'TICKET-20260130002', 1), -- 赵六报名摄影展
(8, 7, 'TICKET-20260130003', 1), -- 吴九报名摄影展
(3, 8, 'TICKET-20260116001', 1), -- 李四报名英语角
(9, 8, 'TICKET-20260116002', 1); -- 郑十报名英语角
-- 更新活动的当前报名人数
UPDATE `activity` SET `current_participants` = 3 WHERE `id` = 1;
UPDATE `activity` SET `current_participants` = 4 WHERE `id` = 2;
UPDATE `activity` SET `current_participants` = 3 WHERE `id` = 3;
UPDATE `activity` SET `current_participants` = 3 WHERE `id` = 4;
UPDATE `activity` SET `current_participants` = 3 WHERE `id` = 5;
UPDATE `activity` SET `current_participants` = 2 WHERE `id` = 6;
UPDATE `activity` SET `current_participants` = 3 WHERE `id` = 7;
UPDATE `activity` SET `current_participants` = 2 WHERE `id` = 8;
-- ============================================
-- 4. 插入示例签到数据
-- ============================================
INSERT INTO `check_in` (`registration_id`, `user_id`, `activity_id`, `check_in_method`) VALUES
(1, 2, 1, 0), -- 张三扫码签到科技创新大赛
(2, 3, 1, 0), -- 李四扫码签到科技创新大赛
(4, 2, 2, 1), -- 张三管理员代签校园歌手大赛
(11, 2, 4, 0), -- 张三扫码签到读书分享会
(15, 3, 5, 0); -- 李四扫码签到篮球友谊赛
-- 更新报名状态为已签到
UPDATE `registration` SET `status` = 2 WHERE `id` IN (1, 2, 4, 11, 15);
-- ============================================
-- 5. 插入示例评价数据
-- ============================================
INSERT INTO `review` (`user_id`, `activity_id`, `rating`, `content`) VALUES
(2, 1, 5, '非常有意义的活动,学到了很多新技术!'),
(3, 1, 4, '组织得很好,希望下次能增加更多互动环节'),
(2, 2, 5, '歌手们都很棒,现场气氛热烈!'),
(5, 2, 4, '活动很精彩,就是座位有点紧张'),
(2, 4, 5, '分享的内容很有深度,受益匪浅'),
(5, 4, 4, '书籍推荐很实用,期待下次活动'),
(3, 5, 5, '比赛很激烈,团队合作很重要'),
(6, 5, 4, '裁判很专业,场地也不错');
-- ============================================
-- 数据插入完成
-- ============================================
SELECT '示例数据插入完成!' AS message;
SELECT '管理员账号admin / admin123' AS admin_account;
SELECT '学生账号student01-student10 / 123456' AS student_accounts;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,823 @@
# 校园活动组织与报名系统 - 前端开发计划
## 1. 项目概述
### 1.1 项目背景
本项目为校园活动组织与报名系统的前端部分,采用前后端分离架构,对接已完成的 Spring Boot 后端 API。
### 1.2 系统目标
实现活动从 **发布 → 报名 → 签到 → 评价 → 统计分析** 的一体化管理前端界面。
### 1.3 用户角色
| 角色 | 权限说明 |
|------|----------|
| 普通学生 | 浏览活动、报名/取消报名、签到、评价活动 |
| 活动管理员 | 发布活动、管理报名、查看签到、统计分析、导出数据 |
---
## 2. 技术选型
### 2.1 核心框架
| 技术 | 版本 | 说明 |
|------|------|------|
| Vue.js | 3.4+ | 渐进式 JavaScript 框架 |
| Vite | 5.x | 下一代前端构建工具 |
| TypeScript | 5.x | JavaScript 超集,类型安全 |
| Vue Router | 4.x | 官方路由管理 |
| Pinia | 2.x | 官方状态管理 |
### 2.2 UI 组件库
| 技术 | 说明 |
|------|------|
| Vant 4 | 轻量、可靠的移动端 Vue 组件库(小程序风格) |
| @vant/use | Vant 组合式 API 工具库 |
### 2.3 工具库
| 技术 | 说明 |
|------|------|
| Axios | HTTP 请求库 |
| Day.js | 轻量日期处理库 |
| vue-qrcode-reader | 二维码扫描组件 |
| html2canvas | 截图/生成图片 |
| @vueuse/core | Vue 组合式工具集 |
### 2.4 开发工具
| 技术 | 说明 |
|------|------|
| ESLint | 代码规范检查 |
| Prettier | 代码格式化 |
| Husky | Git Hooks 工具 |
| lint-staged | 暂存区代码检查 |
---
## 3. 项目结构设计
```
campus-activity-frontend/
├── public/ # 静态资源
│ └── favicon.ico
├── src/
│ ├── api/ # API 接口层
│ │ ├── index.ts # Axios 实例配置
│ │ ├── auth.ts # 认证相关 API
│ │ ├── activity.ts # 活动相关 API
│ │ ├── registration.ts # 报名相关 API
│ │ ├── checkin.ts # 签到相关 API
│ │ ├── review.ts # 评价相关 API
│ │ └── statistics.ts # 统计相关 API
│ │
│ ├── assets/ # 静态资源
│ │ ├── images/ # 图片资源
│ │ └── styles/ # 全局样式
│ │ ├── variables.scss # SCSS 变量
│ │ ├── mixins.scss # SCSS 混入
│ │ └── global.scss # 全局样式
│ │
│ ├── components/ # 公共组件
│ │ ├── common/ # 通用组件
│ │ │ ├── NavBar.vue # 导航栏
│ │ │ ├── TabBar.vue # 底部标签栏
│ │ │ ├── Empty.vue # 空状态
│ │ │ ├── Loading.vue # 加载状态
│ │ │ └── ErrorPage.vue # 错误页面
│ │ │
│ │ ├── activity/ # 活动相关组件
│ │ │ ├── ActivityCard.vue # 活动卡片
│ │ │ ├── ActivityList.vue # 活动列表
│ │ │ ├── ActivityFilter.vue # 活动筛选
│ │ │ └── CalendarView.vue # 日历视图
│ │ │
│ │ ├── registration/ # 报名相关组件
│ │ │ ├── TicketCard.vue # 电子票卡片
│ │ │ └── RegistrationList.vue # 报名列表
│ │ │
│ │ ├── review/ # 评价相关组件
│ │ │ ├── ReviewCard.vue # 评价卡片
│ │ │ ├── ReviewForm.vue # 评价表单
│ │ │ └── RatingStars.vue # 评分星星
│ │ │
│ │ └── statistics/ # 统计相关组件
│ │ ├── StatCard.vue # 统计卡片
│ │ └── ChartView.vue # 图表视图
│ │
│ ├── composables/ # 组合式函数
│ │ ├── useAuth.ts # 认证相关
│ │ ├── useActivity.ts # 活动相关
│ │ ├── useRegistration.ts # 报名相关
│ │ ├── usePagination.ts # 分页相关
│ │ └── useToast.ts # 提示消息
│ │
│ ├── layouts/ # 布局组件
│ │ ├── DefaultLayout.vue # 默认布局
│ │ ├── AdminLayout.vue # 管理员布局
│ │ └── BlankLayout.vue # 空白布局
│ │
│ ├── router/ # 路由配置
│ │ ├── index.ts # 路由入口
│ │ ├── routes.ts # 路由定义
│ │ └── guards.ts # 路由守卫
│ │
│ ├── stores/ # 状态管理
│ │ ├── index.ts # Store 入口
│ │ ├── user.ts # 用户状态
│ │ ├── activity.ts # 活动状态
│ │ └── app.ts # 应用状态
│ │
│ ├── types/ # TypeScript 类型定义
│ │ ├── api.d.ts # API 响应类型
│ │ ├── user.d.ts # 用户类型
│ │ ├── activity.d.ts # 活动类型
│ │ ├── registration.d.ts # 报名类型
│ │ ├── checkin.d.ts # 签到类型
│ │ ├── review.d.ts # 评价类型
│ │ └── statistics.d.ts # 统计类型
│ │
│ ├── utils/ # 工具函数
│ │ ├── request.ts # 请求封装
│ │ ├── storage.ts # 本地存储
│ │ ├── format.ts # 格式化工具
│ │ ├── validate.ts # 校验工具
│ │ └── constant.ts # 常量定义
│ │
│ ├── views/ # 页面视图
│ │ ├── auth/ # 认证页面
│ │ │ ├── Login.vue # 登录
│ │ │ └── Register.vue # 注册
│ │ │
│ │ ├── home/ # 首页
│ │ │ └── Index.vue # 首页
│ │ │
│ │ ├── activity/ # 活动页面
│ │ │ ├── List.vue # 活动列表
│ │ │ ├── Detail.vue # 活动详情
│ │ │ ├── Calendar.vue # 日历视图
│ │ │ └── Search.vue # 搜索页面
│ │ │
│ │ ├── registration/ # 报名页面
│ │ │ ├── MyList.vue # 我的报名
│ │ │ └── Ticket.vue # 电子票详情
│ │ │
│ │ ├── checkin/ # 签到页面
│ │ │ └── Scan.vue # 扫码签到
│ │ │
│ │ ├── review/ # 评价页面
│ │ │ ├── Write.vue # 写评价
│ │ │ └── MyList.vue # 我的评价
│ │ │
│ │ ├── user/ # 用户页面
│ │ │ ├── Profile.vue # 个人中心
│ │ │ ├── Settings.vue # 设置
│ │ │ └── ChangePassword.vue # 修改密码
│ │ │
│ │ └── admin/ # 管理员页面
│ │ ├── Dashboard.vue # 数据面板
│ │ ├── ActivityManage.vue # 活动管理
│ │ ├── ActivityForm.vue # 活动表单(新增/编辑)
│ │ ├── RegistrationManage.vue # 报名管理
│ │ ├── CheckInManage.vue # 签到管理
│ │ ├── QRCodeGenerate.vue # 生成签到码
│ │ ├── ReviewManage.vue # 评价管理
│ │ ├── Statistics.vue # 统计分析
│ │ └── Export.vue # 数据导出
│ │
│ ├── App.vue # 根组件
│ └── main.ts # 入口文件
├── .env # 环境变量
├── .env.development # 开发环境变量
├── .env.production # 生产环境变量
├── .eslintrc.cjs # ESLint 配置
├── .prettierrc # Prettier 配置
├── index.html # HTML 入口
├── package.json # 项目依赖
├── tsconfig.json # TypeScript 配置
├── vite.config.ts # Vite 配置
└── README.md # 项目说明
```
---
## 4. 页面设计规划
### 4.1 页面清单
#### 公共页面
| 页面 | 路由 | 说明 |
|------|------|------|
| 登录 | /login | 用户登录 |
| 注册 | /register | 用户注册 |
#### 学生端页面
| 页面 | 路由 | 说明 |
|------|------|------|
| 首页 | / | 推荐活动、快捷入口 |
| 活动列表 | /activities | 活动列表(筛选、搜索) |
| 活动详情 | /activities/:id | 活动详细信息、报名入口 |
| 日历视图 | /calendar | 按日历展示活动 |
| 我的报名 | /my/registrations | 已报名活动列表 |
| 电子票 | /ticket/:id | 电子票详情、二维码 |
| 扫码签到 | /checkin/scan | 扫码签到页面 |
| 写评价 | /review/:activityId | 活动评价表单 |
| 我的评价 | /my/reviews | 我的评价列表 |
| 个人中心 | /profile | 个人信息 |
| 修改密码 | /change-password | 修改密码 |
#### 管理员页面
| 页面 | 路由 | 说明 |
|------|------|------|
| 数据面板 | /admin/dashboard | 统计概览 |
| 活动管理 | /admin/activities | 活动列表管理 |
| 创建活动 | /admin/activities/create | 新建活动 |
| 编辑活动 | /admin/activities/:id/edit | 编辑活动 |
| 报名管理 | /admin/activities/:id/registrations | 活动报名列表 |
| 签到管理 | /admin/activities/:id/checkin | 签到管理、生成二维码 |
| 评价管理 | /admin/activities/:id/reviews | 活动评价列表 |
| 统计分析 | /admin/statistics/:id | 活动数据统计 |
| 数据导出 | /admin/export | 导出 Excel |
### 4.2 UI 风格设计(小程序风格)
#### 配色方案
```scss
// 主色调
$primary-color: #07C160; // 微信绿
$primary-light: #E8F8ED; // 浅绿背景
// 功能色
$success-color: #07C160; // 成功
$warning-color: #FFA500; // 警告
$error-color: #EE0A24; // 错误
$info-color: #1989FA; // 信息
// 中性色
$text-primary: #323233; // 主要文字
$text-secondary: #969799; // 次要文字
$text-placeholder: #C8C9CC; // 占位文字
$border-color: #EBEDF0; // 边框颜色
$background-color: #F7F8FA; // 页面背景
// 活动状态色
$status-pending: #909399; // 未开始
$status-open: #07C160; // 报名中
$status-ongoing: #1989FA; // 进行中
$status-ended: #C8C9CC; // 已结束
```
#### 设计原则
1. **简洁清晰**: 界面简洁,信息层级分明
2. **圆角卡片**: 使用圆角卡片承载内容,间距 12px
3. **底部安全区**: 适配各种手机底部安全区域
4. **下拉刷新**: 列表页支持下拉刷新
5. **骨架屏**: 加载时显示骨架屏,优化体验
6. **空状态**: 无数据时显示友好的空状态提示
---
## 5. 核心功能模块设计
### 5.1 认证模块
#### 登录页面
```
┌─────────────────────────────────┐
│ 校园活动 │
│ │
│ ┌─────────────────────┐ │
│ │ 👤 请输入用户名 │ │
│ └─────────────────────┘ │
│ ┌─────────────────────┐ │
│ │ 🔒 请输入密码 │ │
│ └─────────────────────┘ │
│ │
│ ┌─────────────────────┐ │
│ │ 登 录 │ │
│ └─────────────────────┘ │
│ │
│ 还没有账号?去注册 │
└─────────────────────────────────┘
```
#### Token 管理
- Access Token 存储在 localStorage
- Refresh Token 存储在 localStorage
- Token 过期前自动刷新
- 401 响应自动跳转登录页
### 5.2 活动模块
#### 活动列表页
```
┌─────────────────────────────────┐
│ 🔍 搜索活动 │
├─────────────────────────────────┤
│ [全部] [报名中] [进行中] [已结束]│
├─────────────────────────────────┤
│ ┌─────────────────────────────┐ │
│ │ 🖼️ 封面图 │ │
│ │ 校园篮球赛 │ │
│ │ 📅 2025-06-01 09:00 │ │
│ │ 📍 体育馆 │ │
│ │ ⭐ 4.5 👥 45/100 │ │
│ │ [报名中] │ │
│ └─────────────────────────────┘ │
│ ┌─────────────────────────────┐ │
│ │ ... │ │
│ └─────────────────────────────┘ │
├─────────────────────────────────┤
│ 🏠 📅 ✉️ 👤 │
└─────────────────────────────────┘
```
#### 活动详情页
```
┌─────────────────────────────────┐
│ ← 活动详情 │
├─────────────────────────────────┤
│ ┌─────────────────────────────┐ │
│ │ 封面图片 │ │
│ └─────────────────────────────┘ │
│ │
│ 校园篮球赛 │
│ [体育] [报名中] │
│ │
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
│ 📅 活动时间 │
│ 2025-06-01 09:00-17:00 │
│ │
│ 📍 活动地点 │
│ 体育馆A区 │
│ │
│ 👥 报名人数 │
│ 45/100剩余55个名额
│ │
│ ⏰ 报名截止 │
│ 2025-05-30 23:59 │
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
│ │
│ 活动详情 │
│ 年度篮球比赛详细介绍... │
│ │
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
│ 评价 (20条) ⭐ 4.5 │
│ ┌─────────────────────────┐ │
│ │ 张三 ⭐⭐⭐⭐⭐ │ │
│ │ 活动组织得很好! │ │
│ └─────────────────────────┘ │
│ │
├─────────────────────────────────┤
│ ┌─────────────────────────────┐ │
│ │ 立即报名 │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────┘
```
### 5.3 报名模块
#### 我的报名列表
```
┌─────────────────────────────────┐
│ ← 我的报名 │
├─────────────────────────────────┤
│ [全部] [待签到] [已签到] [已取消]│
├─────────────────────────────────┤
│ ┌─────────────────────────────┐ │
│ │ 🖼️ 校园篮球赛 │ │
│ │ 📅 2025-06-01 09:00 │ │
│ │ 📍 体育馆 │ │
│ │ 票号: TK20250601001 │ │
│ │ [待签到] [查看电子票] → │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────┘
```
#### 电子票页面
```
┌─────────────────────────────────┐
│ ← 电子票 │
├─────────────────────────────────┤
│ │
│ ┌─────────────────────────┐ │
│ │ │ │
│ │ ┌───────────┐ │ │
│ │ │ QR码 │ │ │
│ │ │ │ │ │
│ │ └───────────┘ │ │
│ │ │ │
│ │ 票号: TK20250601001 │ │
│ │ ───────────────────── │ │
│ │ 活动: 校园篮球赛 │ │
│ │ 时间: 2025-06-01 09:00│ │
│ │ 地点: 体育馆 │ │
│ │ 姓名: 张三 │ │
│ │ 学号: 2021001001 │ │
│ │ │ │
│ └─────────────────────────┘ │
│ │
│ ┌─────────────────────────┐ │
│ │ 下载电子票 │ │
│ └─────────────────────────┘ │
│ │
│ ┌─────────────────────────┐ │
│ │ 取消报名 │ │
│ └─────────────────────────┘ │
└─────────────────────────────────┘
```
### 5.4 签到模块
#### 扫码签到页面
```
┌─────────────────────────────────┐
│ ← 扫码签到 │
├─────────────────────────────────┤
│ │
│ │
│ ┌─────────────────────┐ │
│ │ │ │
│ │ 相机取景框 │ │
│ │ 扫描二维码 │ │
│ │ │ │
│ └─────────────────────┘ │
│ │
│ 将二维码放入框内扫描 │
│ │
└─────────────────────────────────┘
```
### 5.5 评价模块
#### 写评价页面
```
┌─────────────────────────────────┐
│ ← 活动评价 │
├─────────────────────────────────┤
│ │
│ 校园篮球赛 │
│ │
│ 评分 │
│ ⭐ ⭐ ⭐ ⭐ ⭐ │
│ │
│ 评价内容 │
│ ┌─────────────────────────┐ │
│ │ 请输入您的评价... │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ └─────────────────────────┘ │
│ │
├─────────────────────────────────┤
│ ┌─────────────────────────────┐ │
│ │ 提交评价 │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────┘
```
### 5.6 管理员模块
#### 数据面板
```
┌─────────────────────────────────┐
│ 数据面板 👤 │
├─────────────────────────────────┤
│ ┌───────┐ ┌───────┐ ┌───────┐ │
│ │ 活动 │ │ 报名 │ │ 签到 │ │
│ │ 50 │ │ 1200 │ │ 1050 │ │
│ └───────┘ └───────┘ └───────┘ │
│ │
│ ┌───────┐ ┌───────┐ │
│ │ 评价 │ │ 平均分 │ │
│ │ 800 │ │ 4.3 │ │
│ └───────┘ └───────┘ │
│ │
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
│ 本月活动趋势 │
│ ┌─────────────────────────┐ │
│ │ 📊 图表 │ │
│ └─────────────────────────┘ │
│ │
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
│ 快捷操作 │
│ [+创建活动] [📊统计] [📥导出] │
├─────────────────────────────────┤
│ 🏠 📋 📊 👤 │
└─────────────────────────────────┘
```
#### 活动管理页面
```
┌─────────────────────────────────┐
│ 活动管理 [+] │
├─────────────────────────────────┤
│ 🔍 搜索活动 │
│ [全部] [报名中] [进行中] [已结束]│
├─────────────────────────────────┤
│ ┌─────────────────────────────┐ │
│ │ 校园篮球赛 │ │
│ │ 📅 2025-06-01 │ │
│ │ 👥 45/100 [报名中] │ │
│ │ ────────────────────────── │ │
│ │ [报名] [签到] [评价] [统计]│ │
│ │ [编辑] [删除] │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────┘
```
---
## 6. API 对接规划
### 6.1 请求封装
```typescript
// src/utils/request.ts
import axios from 'axios'
import type { AxiosInstance, AxiosRequestConfig } from 'axios'
const instance: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
})
// 请求拦截器:添加 Token
instance.interceptors.request.use(config => {
const token = localStorage.getItem('accessToken')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// 响应拦截器:处理错误
instance.interceptors.response.use(
response => response.data,
error => {
if (error.response?.status === 401) {
// Token 过期,尝试刷新或跳转登录
}
return Promise.reject(error)
}
)
```
### 6.2 API 模块划分
| 模块 | 文件 | API 数量 | 说明 |
|------|------|----------|------|
| 认证 | api/auth.ts | 5 | 登录、注册、刷新 Token、获取用户信息、修改密码 |
| 活动 | api/activity.ts | 7 | 活动 CRUD、列表、日历、冲突检测 |
| 报名 | api/registration.ts | 5 | 报名、取消、我的报名、活动报名列表、电子票 |
| 签到 | api/checkin.ts | 4 | 生成二维码、扫码签到、扫票签到、签到列表 |
| 评价 | api/review.ts | 3 | 提交评价、活动评价列表、我的评价 |
| 统计 | api/statistics.ts | 3 | 活动统计、总体统计、数据导出 |
---
## 7. 状态管理设计
### 7.1 用户状态 (stores/user.ts)
```typescript
interface UserState {
token: string | null
refreshToken: string | null
userInfo: UserInfo | null
isLoggedIn: boolean
}
// Actions
- login(username, password)
- logout()
- refreshToken()
- getUserInfo()
- updateUserInfo()
```
### 7.2 活动状态 (stores/activity.ts)
```typescript
interface ActivityState {
activities: Activity[]
currentActivity: Activity | null
loading: boolean
filters: ActivityFilters
pagination: Pagination
}
// Actions
- fetchActivities(params)
- fetchActivityDetail(id)
- createActivity(data)
- updateActivity(id, data)
- deleteActivity(id)
```
### 7.3 应用状态 (stores/app.ts)
```typescript
interface AppState {
loading: boolean
tabBarActive: string
theme: 'light' | 'dark'
}
```
---
## 8. 路由守卫设计
```typescript
// 路由守卫逻辑
router.beforeEach((to, from, next) => {
const userStore = useUserStore()
// 白名单路由
const whiteList = ['/login', '/register']
if (userStore.isLoggedIn) {
if (to.path === '/login') {
next('/')
} else {
// 检查权限
if (to.meta.requiresAdmin && userStore.userInfo?.role !== 1) {
next('/403')
} else {
next()
}
}
} else {
if (whiteList.includes(to.path)) {
next()
} else {
next(`/login?redirect=${to.path}`)
}
}
})
```
---
## 9. 错误处理设计
### 9.1 全局错误处理
- 网络错误:显示"网络异常,请稍后重试"
- 401 错误:跳转登录页
- 403 错误:显示"无权限访问"
- 404 错误:显示"资源不存在"
- 409 错误:显示业务冲突信息(如"已报名"
- 500 错误:显示"服务器异常"
### 9.2 表单校验
使用 Vant 内置的表单校验 + 自定义校验规则
---
## 10. 性能优化方案
### 10.1 代码分割
- 路由懒加载
- 组件按需导入
- Vant 组件按需加载
### 10.2 资源优化
- 图片懒加载
- 列表虚拟滚动(大数据量)
- 骨架屏优化首屏体验
### 10.3 缓存策略
- 活动列表数据缓存
- 用户信息缓存
- API 请求去重
---
## 11. 移动端适配
### 11.1 Viewport 配置
```html
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
```
### 11.2 PostCSS 配置
```javascript
// postcss.config.js
module.exports = {
plugins: {
'postcss-pxtorem': {
rootValue: 37.5, // Vant 官方推荐
propList: ['*'],
},
},
}
```
### 11.3 安全区适配
```scss
// 底部安全区
.safe-area-bottom {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
```
---
## 12. 部署方案
### 12.1 构建命令
```bash
# 开发
npm run dev
# 构建
npm run build
# 预览
npm run preview
```
### 12.2 环境变量
```env
# .env.development
VITE_API_BASE_URL=http://localhost:8080/api/v1
# .env.production
VITE_API_BASE_URL=/api/v1
```
### 12.3 Nginx 配置示例
```nginx
server {
listen 80;
server_name example.com;
root /usr/share/nginx/html;
index index.html;
# 前端路由
location / {
try_files $uri $uri/ /index.html;
}
# API 代理
location /api {
proxy_pass http://backend:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
```
---
## 13. 项目初始化命令
```bash
# 创建项目
npm create vite@latest campus-activity-frontend -- --template vue-ts
# 进入目录
cd campus-activity-frontend
# 安装依赖
npm install
# 安装 UI 组件库
npm install vant @vant/use
# 安装路由和状态管理
npm install vue-router pinia
# 安装工具库
npm install axios dayjs @vueuse/core
# 安装 SCSS
npm install -D sass
# 安装 ESLint + Prettier
npm install -D eslint prettier eslint-plugin-vue @typescript-eslint/eslint-plugin @typescript-eslint/parser
# 安装移动端适配
npm install -D postcss-pxtorem amfe-flexible
# 安装二维码相关
npm install vue-qrcode-reader qrcode
```
---
## 14. 总结
本前端开发计划基于后端 API 接口设计,采用 Vue 3 + TypeScript + Vant 4 技术栈,实现符合中国小程序风格的校园活动管理系统。
**核心特点**
1. 完整覆盖所有后端 API 接口
2. 清晰的项目结构和模块划分
3. 规范的 TypeScript 类型定义
4. 小程序风格的 UI 设计
5. 完善的错误处理机制
6. 良好的移动端适配方案
**下一步**
- 制定详细的开发规范文档
- 分解具体开发任务清单

View File

@@ -0,0 +1,317 @@
# 校园活动组织与报名系统 - 后端开发规范
## 1. 技术选型
### 1.1 核心框架
| 技术 | 版本 | 说明 |
|------|------|------|
| Java | 21 | LTS版本支持现代语法特性 |
| Spring Boot | 3.2.x | 主框架 |
| Spring Security | 6.x | 安全认证框架 |
| MyBatis-Plus | 3.5.x | ORM框架简化CRUD操作 |
| MySQL | 8.0+ | 关系型数据库 |
### 1.2 工具库
| 技术 | 版本 | 说明 |
|------|------|------|
| JWT | 0.12.x (jjwt) | Token认证 |
| ZXing | 3.5.x | 二维码生成与解析 |
| iText | 7.x | PDF电子票生成 |
| EasyExcel | 3.3.x | Excel导出 |
| Knife4j | 4.x | API文档(基于OpenAPI 3) |
| Hutool | 5.8.x | 工具类库 |
| Lombok | 1.18.x | 简化实体类代码 |
### 1.3 项目结构
```
campus-activity-system/
├── src/main/java/com/campus/activity/
│ ├── CampusActivityApplication.java # 启动类
│ ├── config/ # 配置类
│ │ ├── SecurityConfig.java
│ │ ├── MybatisPlusConfig.java
│ │ ├── CorsConfig.java
│ │ └── Knife4jConfig.java
│ ├── controller/ # 控制器层
│ │ ├── AuthController.java
│ │ ├── ActivityController.java
│ │ ├── RegistrationController.java
│ │ ├── CheckInController.java
│ │ ├── ReviewController.java
│ │ └── StatisticsController.java
│ ├── service/ # 服务层
│ │ ├── impl/
│ │ └── ...Service.java
│ ├── mapper/ # 数据访问层
│ ├── entity/ # 实体类
│ ├── dto/ # 数据传输对象
│ │ ├── request/ # 请求DTO
│ │ └── response/ # 响应DTO
│ ├── vo/ # 视图对象
│ ├── common/ # 公共模块
│ │ ├── Result.java # 统一响应
│ │ ├── ResultCode.java # 状态码枚举
│ │ └── PageResult.java # 分页响应
│ ├── exception/ # 异常处理
│ │ ├── BusinessException.java
│ │ └── GlobalExceptionHandler.java
│ ├── security/ # 安全模块
│ │ ├── JwtTokenProvider.java
│ │ ├── JwtAuthenticationFilter.java
│ │ └── UserDetailsServiceImpl.java
│ └── util/ # 工具类
│ ├── QrCodeUtil.java
│ ├── PdfUtil.java
│ └── ExcelUtil.java
├── src/main/resources/
│ ├── application.yml
│ ├── application-dev.yml
│ ├── application-prod.yml
│ └── mapper/ # MyBatis XML映射文件
└── pom.xml
```
## 2. 编码规范
### 2.1 命名规范
#### 类命名
- Controller类`XxxController`
- Service接口`XxxService`
- Service实现`XxxServiceImpl`
- Mapper接口`XxxMapper`
- Entity实体`Xxx`(与表名对应,驼峰命名)
- DTO类`XxxDTO``XxxRequest`/`XxxResponse`
- VO类`XxxVO`
#### 方法命名
| 操作 | 命名规范 | 示例 |
|------|----------|------|
| 新增 | save/add/create | `saveActivity()` |
| 删除 | remove/delete | `removeActivity()` |
| 修改 | update/modify | `updateActivity()` |
| 查询单个 | get/find | `getActivityById()` |
| 查询列表 | list/find | `listActivities()` |
| 分页查询 | page | `pageActivities()` |
### 2.2 统一响应格式
```java
@Data
public class Result<T> {
private Integer code;
private String message;
private T data;
private Long timestamp;
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMessage("success");
result.setData(data);
result.setTimestamp(System.currentTimeMillis());
return result;
}
public static <T> Result<T> error(Integer code, String message) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMessage(message);
result.setTimestamp(System.currentTimeMillis());
return result;
}
}
```
### 2.3 状态码定义
| 状态码 | 说明 |
|--------|------|
| 200 | 成功 |
| 400 | 请求参数错误 |
| 401 | 未认证 |
| 403 | 无权限 |
| 404 | 资源不存在 |
| 409 | 业务冲突(如已报名、时间冲突) |
| 500 | 服务器内部错误 |
### 2.4 分页响应格式
```java
@Data
public class PageResult<T> {
private List<T> records;
private Long total;
private Long pages;
private Long current;
private Long size;
}
```
## 3. 安全规范
### 3.1 JWT Token设计
- Access Token有效期2小时
- Refresh Token有效期7天
- Token存储位置前端localStorage或Cookie(httpOnly)
### 3.2 接口权限控制
```java
// 使用注解控制接口权限
@PreAuthorize("hasRole('ADMIN')") // 仅管理员
@PreAuthorize("hasRole('STUDENT')") // 仅学生
@PreAuthorize("isAuthenticated()") // 已登录用户
```
### 3.3 密码安全
- 使用BCrypt加密存储
- 密码强度至少8位包含字母和数字
## 4. 接口设计规范
### 4.1 RESTful API规范
| 操作 | HTTP方法 | URL示例 |
|------|----------|---------|
| 创建 | POST | `/api/activities` |
| 查询 | GET | `/api/activities/{id}` |
| 更新 | PUT | `/api/activities/{id}` |
| 删除 | DELETE | `/api/activities/{id}` |
| 列表 | GET | `/api/activities` |
### 4.2 URL命名规范
- 使用小写字母和连字符
- 使用复数名词表示资源集合
- 版本号放在URL中`/api/v1/...`
### 4.3 请求参数规范
**查询参数示例**
```
GET /api/activities?status=1&page=1&size=10&keyword=运动
```
**请求体示例**
```json
{
"name": "校园篮球赛",
"description": "年度篮球比赛",
"startTime": "2025-06-01 09:00:00",
"endTime": "2025-06-01 17:00:00",
"location": "体育馆",
"maxParticipants": 100
}
```
## 5. 日志规范
### 5.1 日志级别使用
- ERROR系统错误需要立即处理
- WARN警告信息潜在问题
- INFO重要业务操作日志
- DEBUG调试信息生产环境关闭
### 5.2 日志格式
```yaml
logging:
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
```
## 6. 数据校验规范
使用Jakarta Validation进行参数校验
```java
@Data
public class ActivityCreateRequest {
@NotBlank(message = "活动名称不能为空")
@Size(max = 100, message = "活动名称不能超过100个字符")
private String name;
@NotNull(message = "开始时间不能为空")
@Future(message = "开始时间必须是未来时间")
private LocalDateTime startTime;
@NotNull(message = "结束时间不能为空")
private LocalDateTime endTime;
@Min(value = 1, message = "人数上限至少为1")
@Max(value = 10000, message = "人数上限不能超过10000")
private Integer maxParticipants;
}
```
## 7. 异常处理规范
### 7.1 自定义业务异常
```java
@Getter
public class BusinessException extends RuntimeException {
private final Integer code;
public BusinessException(Integer code, String message) {
super(message);
this.code = code;
}
}
```
### 7.2 全局异常处理
```java
@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);
}
}
```
## 8. 开发环境配置
### 8.1 application.yml 示例
```yaml
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: ${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
configuration:
map-underscore-to-camel-case: true
jwt:
secret: ${JWT_SECRET}
expiration: 7200000
refresh-expiration: 604800000
```

View File

@@ -0,0 +1,300 @@
# 校园活动组织与报名系统 - 开发任务清单
## 项目概述
基于Spring Boot 3.x的前后端分离架构实现校园活动的发布、报名、签到、评价及统计功能。
---
## 阶段一:项目初始化与基础架构
### 1.1 项目搭建
- [ ] 使用Spring Initializr创建Spring Boot 3.x项目
- [ ] 配置pom.xml添加所需依赖
- Spring Boot Starter Web
- Spring Boot Starter Security
- Spring Boot Starter Validation
- MyBatis-Plus Boot Starter
- MySQL Connector
- JWT (jjwt)
- Knife4j (API文档)
- Lombok
- Hutool
- [ ] 创建项目目录结构
- [ ] 配置application.yml开发/生产环境)
### 1.2 数据库初始化
- [ ] 创建MySQL数据库 `campus_activity`
- [ ] 执行建表SQL脚本
- [ ] 插入测试数据(管理员账号)
- [ ] 验证数据库连接
### 1.3 基础配置类
- [ ] 配置MyBatis-Plus分页插件、逻辑删除
- [ ] 配置跨域CORS
- [ ] 配置Jackson日期格式化
- [ ] 配置Knife4j API文档
### 1.4 公共模块开发
- [ ] 统一响应类 `Result<T>`
- [ ] 分页响应类 `PageResult<T>`
- [ ] 状态码枚举 `ResultCode`
- [ ] 自定义业务异常 `BusinessException`
- [ ] 全局异常处理器 `GlobalExceptionHandler`
---
## 阶段二:安全认证模块
### 2.1 JWT工具类
- [ ] JWT Token生成方法
- [ ] JWT Token解析方法
- [ ] JWT Token验证方法
- [ ] Refresh Token逻辑
### 2.2 Spring Security配置
- [ ] SecurityConfig安全配置类
- [ ] JwtAuthenticationFilter过滤器
- [ ] UserDetailsServiceImpl用户详情服务
- [ ] 配置接口权限规则
- [ ] 配置密码加密器BCrypt
### 2.3 认证接口开发
- [ ] **POST** `/api/v1/auth/register` - 用户注册
- [ ] **POST** `/api/v1/auth/login` - 用户登录
- [ ] **POST** `/api/v1/auth/refresh` - 刷新Token
- [ ] **GET** `/api/v1/auth/me` - 获取当前用户信息
- [ ] **PUT** `/api/v1/auth/password` - 修改密码
---
## 阶段三:活动管理模块
### 3.1 实体与数据层
- [ ] Activity实体类
- [ ] ActivityMapper接口
- [ ] ActivityMapper.xml复杂查询
### 3.2 服务层
- [ ] ActivityService接口定义
- [ ] ActivityServiceImpl实现
- [ ] 创建活动
- [ ] 更新活动
- [ ] 删除活动(逻辑删除)
- [ ] 查询活动详情
- [ ] 分页查询活动列表
- [ ] 日历视图查询
- [ ] 时间冲突检测
- [ ] 活动状态自动更新(定时任务)
### 3.3 控制层
- [ ] ActivityController
- [ ] ActivityCreateRequest DTO
- [ ] ActivityUpdateRequest DTO
- [ ] ActivityVO 视图对象
- [ ] 参数校验注解
### 3.4 接口开发
- [ ] **GET** `/api/v1/activities` - 活动列表(分页、筛选)
- [ ] **GET** `/api/v1/activities/{id}` - 活动详情
- [ ] **POST** `/api/v1/activities` - 创建活动(管理员)
- [ ] **PUT** `/api/v1/activities/{id}` - 更新活动(管理员)
- [ ] **DELETE** `/api/v1/activities/{id}` - 删除活动(管理员)
- [ ] **GET** `/api/v1/activities/calendar` - 日历视图
- [ ] **POST** `/api/v1/activities/check-conflict` - 时间冲突检测
---
## 阶段四:报名管理模块
### 4.1 实体与数据层
- [ ] Registration实体类
- [ ] RegistrationMapper接口
### 4.2 服务层
- [ ] RegistrationService接口
- [ ] RegistrationServiceImpl实现
- [ ] 报名活动
- [ ] 取消报名
- [ ] 检查是否已报名
- [ ] 检查报名人数上限
- [ ] 检查时间冲突(与已报名活动)
- [ ] 生成电子票唯一码
### 4.3 电子票功能
- [ ] 电子票唯一码生成UUID或雪花算法
- [ ] 二维码生成工具类ZXing
- [ ] PDF电子票生成iText
- [ ] 包含活动信息
- [ ] 包含学生姓名
- [ ] 包含二维码
### 4.4 接口开发
- [ ] **POST** `/api/v1/registrations` - 报名活动
- [ ] **DELETE** `/api/v1/registrations/{id}` - 取消报名
- [ ] **GET** `/api/v1/registrations/my` - 我的报名列表
- [ ] **GET** `/api/v1/registrations/activity/{activityId}` - 活动报名列表(管理员)
- [ ] **GET** `/api/v1/registrations/{id}/ticket` - 下载电子票PDF
---
## 阶段五:签到管理模块
### 5.1 实体与数据层
- [ ] CheckIn实体类
- [ ] CheckInMapper接口
### 5.2 服务层
- [ ] CheckInService接口
- [ ] CheckInServiceImpl实现
- [ ] 生成活动签到二维码
- [ ] 学生扫码签到
- [ ] 管理员扫票签到
- [ ] 签到状态查询
### 5.3 二维码功能
- [ ] 签到二维码生成含活动ID、时间戳、签名
- [ ] 二维码内容解析与验证
- [ ] 二维码有效期控制
### 5.4 接口开发
- [ ] **POST** `/api/v1/checkin/qrcode/{activityId}` - 生成签到二维码(管理员)
- [ ] **POST** `/api/v1/checkin/scan` - 学生扫码签到
- [ ] **POST** `/api/v1/checkin/ticket` - 管理员扫票签到
- [ ] **GET** `/api/v1/checkin/activity/{activityId}` - 签到列表(管理员)
---
## 阶段六:评价管理模块
### 6.1 实体与数据层
- [ ] Review实体类
- [ ] ReviewMapper接口
### 6.2 服务层
- [ ] ReviewService接口
- [ ] ReviewServiceImpl实现
- [ ] 提交评价
- [ ] 检查是否已评价
- [ ] 检查是否参加过活动
- [ ] 计算平均评分
### 6.3 接口开发
- [ ] **POST** `/api/v1/reviews` - 提交评价
- [ ] **GET** `/api/v1/reviews/activity/{activityId}` - 活动评价列表
- [ ] **GET** `/api/v1/reviews/my` - 我的评价列表
---
## 阶段七:数据统计与导出
### 7.1 统计服务
- [ ] StatisticsService接口
- [ ] StatisticsServiceImpl实现
- [ ] 活动统计(报名人数、签到率、平均评分)
- [ ] 评分分布统计
- [ ] 总体统计数据
- [ ] 月度统计数据
### 7.2 数据导出
- [ ] Excel导出工具类EasyExcel
- [ ] CSV导出工具类
- [ ] 报名数据导出模板
- [ ] 签到数据导出模板
### 7.3 接口开发
- [ ] **GET** `/api/v1/statistics/activity/{activityId}` - 活动统计
- [ ] **GET** `/api/v1/statistics/activity/{activityId}/export` - 导出活动数据
- [ ] **GET** `/api/v1/statistics/overview` - 总体统计
---
## 阶段八:功能完善与优化
### 8.1 定时任务
- [ ] 配置Spring Task
- [ ] 活动状态自动更新任务
- [ ] 报名截止 → 更新状态
- [ ] 活动开始 → 更新状态
- [ ] 活动结束 → 更新状态
### 8.2 文件上传
- [ ] 文件上传配置
- [ ] 活动封面图片上传
- [ ] 用户头像上传
- [ ] 文件存储路径管理
### 8.3 日志与监控
- [ ] 配置日志级别
- [ ] 关键操作日志记录
- [ ] 接口访问日志
---
## 阶段九:测试与部署
### 9.1 单元测试
- [ ] Service层单元测试
- [ ] 工具类单元测试
### 9.2 接口测试
- [ ] 使用Knife4j/Swagger测试所有接口
- [ ] 编写Postman测试集合
### 9.3 部署准备
- [ ] 生产环境配置文件
- [ ] 打包配置
- [ ] 数据库初始化脚本整理
---
## 任务优先级说明
| 优先级 | 阶段 | 说明 |
|--------|------|------|
| P0 | 阶段一、二 | 基础架构,必须首先完成 |
| P1 | 阶段三、四 | 核心业务功能 |
| P2 | 阶段五、六 | 重要辅助功能 |
| P3 | 阶段七 | 数据分析功能 |
| P4 | 阶段八、九 | 优化与部署 |
---
## 技术难点提示
### 1. 时间冲突检测
检查用户报名的活动是否与新报名活动时间重叠:
```sql
SELECT * FROM activity a
INNER JOIN registration r ON a.id = r.activity_id
WHERE r.user_id = ? AND r.status = 1
AND ((a.start_time <= ? AND a.end_time >= ?)
OR (a.start_time <= ? AND a.end_time >= ?)
OR (a.start_time >= ? AND a.end_time <= ?))
```
### 2. 电子票二维码
- 内容格式:`TICKET:{ticketCode}:{userId}:{activityId}`
- 使用签名防止伪造
### 3. 签到二维码
- 内容格式:`CHECKIN:{activityId}:{timestamp}:{signature}`
- 建议设置有效期如5分钟
- 管理员可刷新生成新二维码
### 4. 并发报名控制
- 使用数据库乐观锁或悲观锁
- 或使用Redis分布式锁
- 防止超额报名
---
## 文档清单
| 文档名称 | 状态 | 说明 |
|----------|------|------|
| 需求文档.md | ✅ 已有 | 原始需求 |
| 后端开发规范.md | ✅ 完成 | 技术选型与编码规范 |
| 数据库设计.md | ✅ 完成 | 表结构与SQL脚本 |
| API接口设计.md | ✅ 完成 | RESTful API文档 |
| 开发任务清单.md | ✅ 完成 | 本文档 |

View File

@@ -0,0 +1,393 @@
# 校园活动组织与报名系统 - 数据库设计
## 1. 数据库概述
- **数据库名称**`campus_activity`
- **字符集**`utf8mb4`
- **排序规则**`utf8mb4_general_ci`
## 2. ER图实体关系图
```
┌─────────────┐ ┌─────────────────┐ ┌─────────────┐
│ User │ │ Registration │ │ Activity │
│─────────────│ │─────────────────│ │─────────────│
│ id (PK) │───┐ │ id (PK) │ ┌───│ id (PK) │
│ username │ │ │ user_id (FK) │───┘ │ title │
│ password │ └───│ activity_id(FK) │ │ description │
│ name │ │ status │ │ start_time │
│ student_id │ │ ticket_code │ │ end_time │
│ role │ │ created_at │ │ location │
│ ... │ │ ... │ │ max_num │
└─────────────┘ └─────────────────┘ │ status │
│ │ │ admin_id(FK)│
│ ┌───────┴───────┐ └─────────────┘
│ │ │ │
│ ┌───────┴───┐ ┌───────┴───┐ │
│ │ CheckIn │ │ Review │ │
│ │───────────│ │───────────│ │
│ │ id (PK) │ │ id (PK) │ │
└───────│ reg_id(FK)│ │ user_id │───────────┘
│ check_time│ │ act_id │
└───────────┘ │ rating │
│ content │
└───────────┘
```
## 3. 数据表设计
### 3.1 用户表 (user)
存储系统用户信息,包括学生和管理员。
| 字段名 | 类型 | 约束 | 说明 |
|--------|------|------|------|
| id | BIGINT | PK, AUTO_INCREMENT | 用户ID |
| username | VARCHAR(50) | UNIQUE, NOT NULL | 登录用户名 |
| password | VARCHAR(255) | NOT NULL | 密码BCrypt加密 |
| name | VARCHAR(50) | NOT NULL | 真实姓名 |
| student_id | VARCHAR(20) | UNIQUE | 学号(学生必填) |
| email | VARCHAR(100) | | 邮箱 |
| phone | VARCHAR(20) | | 手机号 |
| avatar | VARCHAR(255) | | 头像URL |
| role | TINYINT | NOT NULL, DEFAULT 0 | 角色0-学生1-管理员 |
| status | TINYINT | DEFAULT 1 | 状态0-禁用1-正常 |
| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | 创建时间 |
| updated_at | DATETIME | ON UPDATE CURRENT_TIMESTAMP | 更新时间 |
| deleted | TINYINT | DEFAULT 0 | 逻辑删除0-未删除1-已删除 |
**索引**
- `idx_username` (username)
- `idx_student_id` (student_id)
```sql
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='用户表';
```
### 3.2 活动表 (activity)
存储活动信息。
| 字段名 | 类型 | 约束 | 说明 |
|--------|------|------|------|
| id | BIGINT | PK, AUTO_INCREMENT | 活动ID |
| title | VARCHAR(100) | NOT NULL | 活动名称 |
| description | TEXT | | 活动简介 |
| cover_image | VARCHAR(255) | | 封面图片URL |
| start_time | DATETIME | NOT NULL | 开始时间 |
| end_time | DATETIME | NOT NULL | 结束时间 |
| registration_deadline | DATETIME | | 报名截止时间 |
| location | VARCHAR(200) | NOT NULL | 活动地点 |
| max_participants | INT | NOT NULL | 报名人数上限 |
| current_participants | INT | DEFAULT 0 | 当前报名人数 |
| status | TINYINT | DEFAULT 0 | 状态0-未开始1-报名中2-进行中3-已结束 |
| category | VARCHAR(50) | | 活动分类 |
| admin_id | BIGINT | FK | 创建者ID管理员 |
| qr_code | VARCHAR(255) | | 签到二维码 |
| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | 创建时间 |
| updated_at | DATETIME | ON UPDATE CURRENT_TIMESTAMP | 更新时间 |
| deleted | TINYINT | DEFAULT 0 | 逻辑删除 |
**索引**
- `idx_status` (status)
- `idx_start_time` (start_time)
- `idx_admin_id` (admin_id)
```sql
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 '逻辑删除',
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='活动表';
```
### 3.3 报名表 (registration)
存储学生活动报名信息。
| 字段名 | 类型 | 约束 | 说明 |
|--------|------|------|------|
| id | BIGINT | PK, AUTO_INCREMENT | 报名ID |
| user_id | BIGINT | FK, NOT NULL | 用户ID |
| activity_id | BIGINT | FK, NOT NULL | 活动ID |
| ticket_code | VARCHAR(100) | UNIQUE | 电子票唯一码 |
| ticket_pdf_url | VARCHAR(255) | | 电子票PDF地址 |
| status | TINYINT | DEFAULT 1 | 状态0-已取消1-已报名2-已签到 |
| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | 报名时间 |
| updated_at | DATETIME | ON UPDATE CURRENT_TIMESTAMP | 更新时间 |
| canceled_at | DATETIME | | 取消时间 |
**索引**
- `uk_user_activity` (user_id, activity_id) UNIQUE
- `idx_activity_id` (activity_id)
- `idx_ticket_code` (ticket_code)
```sql
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 `user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='报名表';
```
### 3.4 签到表 (check_in)
存储签到记录。
| 字段名 | 类型 | 约束 | 说明 |
|--------|------|------|------|
| id | BIGINT | PK, AUTO_INCREMENT | 签到ID |
| registration_id | BIGINT | FK, UNIQUE | 报名ID |
| user_id | BIGINT | FK | 用户ID |
| activity_id | BIGINT | FK | 活动ID |
| check_in_time | DATETIME | DEFAULT CURRENT_TIMESTAMP | 签到时间 |
| check_in_method | TINYINT | DEFAULT 0 | 签到方式0-扫码1-管理员代签 |
**索引**
- `uk_registration_id` (registration_id) UNIQUE
- `idx_activity_id` (activity_id)
```sql
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='签到表';
```
### 3.5 评价表 (review)
存储活动评价信息。
| 字段名 | 类型 | 约束 | 说明 |
|--------|------|------|------|
| id | BIGINT | PK, AUTO_INCREMENT | 评价ID |
| user_id | BIGINT | FK, NOT NULL | 用户ID |
| activity_id | BIGINT | FK, NOT NULL | 活动ID |
| rating | TINYINT | NOT NULL | 评分1-5 |
| content | TEXT | | 评论内容 |
| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | 评价时间 |
| updated_at | DATETIME | ON UPDATE CURRENT_TIMESTAMP | 更新时间 |
**索引**
- `uk_user_activity` (user_id, activity_id) UNIQUE确保每人每活动只能评价一次
- `idx_activity_id` (activity_id)
```sql
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='评价表';
```
## 4. 完整建库脚本
```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 '逻辑删除',
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);
```
## 5. 数据字典
### 5.1 角色类型 (user.role)
| 值 | 说明 |
|----|------|
| 0 | 普通学生 |
| 1 | 活动管理员 |
### 5.2 活动状态 (activity.status)
| 值 | 说明 |
|----|------|
| 0 | 未开始 |
| 1 | 报名中 |
| 2 | 进行中 |
| 3 | 已结束 |
### 5.3 报名状态 (registration.status)
| 值 | 说明 |
|----|------|
| 0 | 已取消 |
| 1 | 已报名 |
| 2 | 已签到 |
### 5.4 签到方式 (check_in.check_in_method)
| 值 | 说明 |
|----|------|
| 0 | 扫码签到 |
| 1 | 管理员代签 |

107
server/docs/需求文档.md Normal file
View File

@@ -0,0 +1,107 @@
# 校园活动组织与报名系统 - 需求文档
## 1. 课程信息
- **课程名称**: 校园活动组织与报名系统
- **课题来源**: 教师自拟
- **课题类型**: 综合型
- **完成时间**: 2025年X月X日
- **课题分组**: 2-3人一组
## 2. 目的和意义
1. 软件工程方法的综合运用能力
2. Java语言解决实际问题的能力
3. 数据库设计与系统集成能力
4. 规范化文档编写能力
## 3. 需求概要
系统面向校园内的活动组织与参与场景,实现活动从发布 → 报名 → 签到 → 评价 → 统计分析的一体化管理。
## 4. 用户角色说明
### 4.1 普通学生用户
- 浏览活动
- 报名/取消报名
- 签到
- 对活动进行评分与评论
### 4.2 活动管理员
- 发布活动
- 管理报名信息
- 查看签到与评价
- 导出活动数据
## 5. 功能模块
### 5.1 活动发布与管理模块
管理员可以创建并管理校园活动,至少包含以下信息:
- 活动名称
- 活动简介
- 活动时间(开始时间、结束时间)
- 活动地点
- 报名人数上限
- 活动状态(未开始/报名中/已结束)
**功能列表**:
- 新增活动
- 修改活动信息
- 删除活动(或逻辑删除)
- 查询活动列表(按时间或状态)
- 以日历形式展示活动
- 检测活动时间冲突并提示
### 5.2 报名与取消报名模块
学生可以对活动进行报名和取消报名操作。
**功能列表**:
- 学生报名活动,报名成功后自动生成电子票(PDF),包含活动信息、学生姓名、二维码
- 系统检查:
- 是否已报名
- 是否时间冲突
- 是否超过人数上限
- 学生取消报名(活动未开始前)
### 5.3 签到管理(二维码)模块
系统需支持活动签到功能。
**功能列表**:
- 管理员为某个活动生成签到二维码
- 学生通过"扫码"或者管理员扫学生电子票完成签到
- 系统记录签到时间
### 5.4 活动评分与评论模块
活动结束后,学生可对参加过的活动进行评价。
**功能列表**:
- 评分(如1-5分)
- 评论内容
- 每个学生对同一活动只能评价一次
- 管理员可查看所有评价
### 5.5 数据统计与导出
系统需具备基本统计能力,例如每个活动的报名人数、实际签到人数、平均评分。管理员可导出活动数据(如CSV或Excel)。
## 6. 提交成果
### 6.1 文档
包括软件需求分析说明书、软件设计说明书、软件使用手册。
### 6.2 作品
可以采用基于Java桌面应用(Swing/JavaFX)、前后端分离系统(后端Java)。
## 7. 评分标准
1. **功能完整性**: 50%
2. **数据库设计**: 20%
3. **软件文档规范性**: 30%
## 8. 答辩要求
答辩时交付纸质软件过程文档(每组1份)。

164
server/pom.xml Normal file
View File

@@ -0,0 +1,164 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.8</version>
<relativePath/>
</parent>
<groupId>com.campus</groupId>
<artifactId>campus-activity-system</artifactId>
<version>1.0.0</version>
<name>campus-activity-system</name>
<description>校园活动组织与报名系统</description>
<properties>
<java.version>21</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<mybatis-plus.version>3.5.5</mybatis-plus.version>
<jjwt.version>0.12.5</jjwt.version>
<knife4j.version>4.4.0</knife4j.version>
<hutool.version>5.8.25</hutool.version>
<zxing.version>3.5.2</zxing.version>
<itext.version>8.0.2</itext.version>
<easyexcel.version>3.3.4</easyexcel.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Spring Boot Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- MySQL Driver -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<!-- Knife4j API文档 -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>${knife4j.version}</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Hutool工具类 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<!-- ZXing二维码 -->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>${zxing.version}</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>${zxing.version}</version>
</dependency>
<!-- iText PDF -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext-core</artifactId>
<version>${itext.version}</version>
<type>pom</type>
</dependency>
<!-- EasyExcel -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>${easyexcel.version}</version>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Security Test -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,14 @@
package com.campus.activity;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.campus.activity.mapper")
public class CampusActivityApplication {
public static void main(String[] args) {
SpringApplication.run(CampusActivityApplication.class, args);
}
}

View File

@@ -0,0 +1,33 @@
package com.campus.activity.common;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
@Data
public class PageResult<T> implements Serializable {
private static final long serialVersionUID = 1L;
private List<T> records;
private Long total;
private Long pages;
private Long current;
private Long size;
public PageResult() {
}
public PageResult(List<T> records, Long total, Long current, Long size) {
this.records = records;
this.total = total;
this.current = current;
this.size = size;
this.pages = (total + size - 1) / size;
}
public static <T> PageResult<T> of(List<T> records, Long total, Long current, Long size) {
return new PageResult<>(records, total, current, size);
}
}

View File

@@ -0,0 +1,59 @@
package com.campus.activity.common;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serializable;
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Result<T> implements Serializable {
private static final long serialVersionUID = 1L;
private Integer code;
private String message;
private T data;
private Long timestamp;
public Result() {
this.timestamp = System.currentTimeMillis();
}
public Result(Integer code, String message) {
this.code = code;
this.message = message;
this.timestamp = System.currentTimeMillis();
}
public Result(Integer code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
this.timestamp = System.currentTimeMillis();
}
public static <T> Result<T> success() {
return new Result<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage());
}
public static <T> Result<T> success(T data) {
return new Result<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data);
}
public static <T> Result<T> success(String message, T data) {
return new Result<>(ResultCode.SUCCESS.getCode(), message, data);
}
public static <T> Result<T> error(ResultCode resultCode) {
return new Result<>(resultCode.getCode(), resultCode.getMessage());
}
public static <T> Result<T> error(Integer code, String message) {
return new Result<>(code, message);
}
public static <T> Result<T> error(String message) {
return new Result<>(ResultCode.ERROR.getCode(), message);
}
}

View File

@@ -0,0 +1,46 @@
package com.campus.activity.common;
import lombok.Getter;
@Getter
public enum ResultCode {
SUCCESS(200, "success"),
ERROR(500, "服务器内部错误"),
BAD_REQUEST(400, "请求参数错误"),
UNAUTHORIZED(401, "未认证或Token已过期"),
FORBIDDEN(403, "无权限访问"),
NOT_FOUND(404, "资源不存在"),
CONFLICT(409, "业务冲突"),
USER_NOT_FOUND(1001, "用户不存在"),
USER_ALREADY_EXISTS(1002, "用户已存在"),
PASSWORD_ERROR(1003, "密码错误"),
USERNAME_OR_PASSWORD_ERROR(1004, "用户名或密码错误"),
STUDENT_ID_ALREADY_EXISTS(1005, "学号已存在"),
ACTIVITY_NOT_FOUND(2001, "活动不存在"),
ACTIVITY_ALREADY_STARTED(2002, "活动已开始,无法取消报名"),
ACTIVITY_TIME_CONFLICT(2003, "活动时间冲突"),
ACTIVITY_FULL(2004, "活动报名人数已满"),
REGISTRATION_NOT_FOUND(3001, "报名记录不存在"),
ALREADY_REGISTERED(3002, "您已报名该活动"),
NOT_REGISTERED(3003, "您未报名该活动"),
CHECKIN_FAILED(4001, "签到失败"),
ALREADY_CHECKED_IN(4002, "已签到"),
CHECKIN_TIME_EXPIRED(4003, "签到时间已过期"),
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;
}
}

View File

@@ -0,0 +1,26 @@
package com.campus.activity.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOriginPattern("*");
config.setAllowCredentials(true);
config.addAllowedMethod("*");
config.addAllowedHeader("*");
config.addExposedHeader("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}

View File

@@ -0,0 +1,27 @@
package com.campus.activity.config;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class Knife4jConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("校园活动组织与报名系统 API")
.version("1.0.0")
.description("校园活动组织与报名系统后端接口文档")
.contact(new Contact()
.name("campus-activity-team")
.email("campus@example.com"))
.license(new License()
.name("Apache 2.0")
.url("https://www.apache.org/licenses/LICENSE-2.0.html")));
}
}

View File

@@ -0,0 +1,22 @@
package com.campus.activity.config;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createdAt", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "updatedAt", LocalDateTime.class, LocalDateTime.now());
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updatedAt", LocalDateTime.class, LocalDateTime.now());
}
}

View File

@@ -0,0 +1,23 @@
package com.campus.activity.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}

View File

@@ -0,0 +1,84 @@
package com.campus.activity.config;
import com.campus.activity.security.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final UserDetailsService userDetailsService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.cors(cors -> cors.configurationSource(request -> {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOriginPattern("*");
config.setAllowCredentials(true);
config.addAllowedMethod("*");
config.addAllowedHeader("*");
config.addExposedHeader("*");
config.setMaxAge(3600L);
return config;
}))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/api/v1/auth/register",
"/api/v1/auth/login",
"/doc.html",
"/webjars/**",
"/swagger-resources/**",
"/v3/api-docs/**",
"/favicon.ico"
).permitAll()
.requestMatchers("/api/v1/activities/**").permitAll()
.requestMatchers("/api/v1/reviews/activity/**").permitAll()
.anyRequest().authenticated()
)
.authenticationProvider(authenticationProvider())
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

View File

@@ -0,0 +1,92 @@
package com.campus.activity.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.campus.activity.common.Result;
import com.campus.activity.dto.request.*;
import com.campus.activity.entity.Activity;
import com.campus.activity.service.ActivityService;
import com.campus.activity.vo.ActivityVO;
import com.campus.activity.vo.ConflictCheckVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.List;
@Tag(name = "活动模块", description = "活动管理相关接口")
@RestController
@RequestMapping("/api/v1/activities")
@RequiredArgsConstructor
public class ActivityController {
private final ActivityService activityService;
@Operation(summary = "获取活动列表")
@GetMapping
public Result<IPage<ActivityVO>> pageActivities(
@RequestParam(defaultValue = "1") Long current,
@RequestParam(defaultValue = "10") Long size,
@RequestParam(required = false) Integer status,
@RequestParam(required = false) String keyword,
@RequestParam(required = false) String category,
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime startDate,
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime endDate) {
Page<Activity> page = new Page<>(current, size);
IPage<ActivityVO> result = activityService.pageActivities(page, status, keyword, category, startDate, endDate);
return Result.success(result);
}
@Operation(summary = "获取活动详情")
@GetMapping("/{id}")
public Result<ActivityVO> getActivityById(@PathVariable Long id) {
ActivityVO activityVO = activityService.getActivityById(id);
return Result.success(activityVO);
}
@Operation(summary = "创建活动(管理员)")
@PostMapping
@PreAuthorize("hasRole('ADMIN')")
public Result<Long> createActivity(@Valid @RequestBody ActivityCreateRequest request) {
Long activityId = activityService.createActivity(request);
return Result.success("创建成功", activityId);
}
@Operation(summary = "更新活动(管理员)")
@PutMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public Result<Void> updateActivity(@PathVariable Long id, @Valid @RequestBody ActivityUpdateRequest request) {
activityService.updateActivity(id, request);
return Result.success("更新成功", null);
}
@Operation(summary = "删除活动(管理员)")
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public Result<Void> deleteActivity(@PathVariable Long id) {
activityService.deleteActivity(id);
return Result.success("删除成功", null);
}
@Operation(summary = "获取日历视图活动")
@GetMapping("/calendar")
public Result<List<ActivityVO>> getCalendarActivities(
@RequestParam Integer year,
@RequestParam Integer month) {
List<ActivityVO> activities = activityService.getCalendarActivities(year, month);
return Result.success(activities);
}
@Operation(summary = "检测时间冲突")
@PostMapping("/check-conflict")
@PreAuthorize("hasRole('ADMIN')")
public Result<ConflictCheckVO> checkConflict(@Valid @RequestBody CheckConflictRequest request) {
ConflictCheckVO result = activityService.checkConflict(request);
return Result.success(result);
}
}

View File

@@ -0,0 +1,57 @@
package com.campus.activity.controller;
import com.campus.activity.common.Result;
import com.campus.activity.dto.request.*;
import com.campus.activity.dto.response.LoginResponse;
import com.campus.activity.dto.response.RefreshTokenResponse;
import com.campus.activity.entity.User;
import com.campus.activity.service.AuthService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@Tag(name = "认证模块", description = "用户注册、登录、Token刷新等接口")
@RestController
@RequestMapping("/api/v1/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
@Operation(summary = "用户注册")
@PostMapping("/register")
public Result<Void> register(@Valid @RequestBody RegisterRequest request) {
authService.register(request);
return Result.success("注册成功", null);
}
@Operation(summary = "用户登录")
@PostMapping("/login")
public Result<LoginResponse> login(@Valid @RequestBody LoginRequest request) {
LoginResponse response = authService.login(request);
return Result.success("登录成功", response);
}
@Operation(summary = "刷新Token")
@PostMapping("/refresh")
public Result<RefreshTokenResponse> refreshToken(@Valid @RequestBody RefreshTokenRequest request) {
RefreshTokenResponse response = authService.refreshToken(request);
return Result.success(response);
}
@Operation(summary = "获取当前用户信息")
@GetMapping("/me")
public Result<User> getCurrentUser() {
User user = authService.getCurrentUser();
return Result.success(user);
}
@Operation(summary = "修改密码")
@PutMapping("/password")
public Result<Void> changePassword(@Valid @RequestBody ChangePasswordRequest request) {
authService.changePassword(request);
return Result.success("密码修改成功", null);
}
}

View File

@@ -0,0 +1,61 @@
package com.campus.activity.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.campus.activity.common.Result;
import com.campus.activity.dto.request.ScanCheckInRequest;
import com.campus.activity.dto.request.TicketCheckInRequest;
import com.campus.activity.entity.CheckIn;
import com.campus.activity.service.CheckInService;
import com.campus.activity.vo.CheckInVO;
import com.campus.activity.vo.QrCodeVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@Tag(name = "签到模块", description = "活动签到相关接口")
@RestController
@RequestMapping("/api/v1/checkin")
@RequiredArgsConstructor
public class CheckInController {
private final CheckInService checkInService;
@Operation(summary = "生成签到二维码(管理员)")
@PostMapping("/qrcode/{activityId}")
@PreAuthorize("hasRole('ADMIN')")
public Result<QrCodeVO> generateQrCode(@PathVariable Long activityId) {
QrCodeVO qrCodeVO = checkInService.generateQrCode(activityId);
return Result.success(qrCodeVO);
}
@Operation(summary = "学生扫码签到")
@PostMapping("/scan")
public Result<CheckInVO> scanCheckIn(@Valid @RequestBody ScanCheckInRequest request) {
CheckInVO checkInVO = checkInService.scanCheckIn(request);
return Result.success("签到成功", checkInVO);
}
@Operation(summary = "管理员扫学生票签到")
@PostMapping("/ticket")
@PreAuthorize("hasRole('ADMIN')")
public Result<CheckInVO> ticketCheckIn(@Valid @RequestBody TicketCheckInRequest request) {
CheckInVO checkInVO = checkInService.ticketCheckIn(request);
return Result.success("签到成功", checkInVO);
}
@Operation(summary = "获取活动签到列表(管理员)")
@GetMapping("/activity/{activityId}")
@PreAuthorize("hasRole('ADMIN')")
public Result<IPage<CheckInVO>> getActivityCheckIns(
@PathVariable Long activityId,
@RequestParam(defaultValue = "1") Long current,
@RequestParam(defaultValue = "10") Long size) {
Page<CheckIn> page = new Page<>(current, size);
IPage<CheckInVO> result = checkInService.getActivityCheckIns(page, activityId);
return Result.success(result);
}
}

View File

@@ -0,0 +1,76 @@
package com.campus.activity.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.campus.activity.common.Result;
import com.campus.activity.dto.request.RegistrationRequest;
import com.campus.activity.entity.Registration;
import com.campus.activity.service.RegistrationService;
import com.campus.activity.vo.RegistrationVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@Tag(name = "报名模块", description = "活动报名相关接口")
@RestController
@RequestMapping("/api/v1/registrations")
@RequiredArgsConstructor
public class RegistrationController {
private final RegistrationService registrationService;
@Operation(summary = "报名活动")
@PostMapping
public Result<RegistrationVO> register(@Valid @RequestBody RegistrationRequest request) {
RegistrationVO registrationVO = registrationService.register(request);
return Result.success("报名成功", registrationVO);
}
@Operation(summary = "取消报名")
@DeleteMapping("/{id}")
public Result<Void> cancelRegistration(@PathVariable Long id) {
registrationService.cancelRegistration(id);
return Result.success("取消成功", null);
}
@Operation(summary = "获取我的报名列表")
@GetMapping("/my")
public Result<IPage<RegistrationVO>> getMyRegistrations(
@RequestParam(defaultValue = "1") Long current,
@RequestParam(defaultValue = "10") Long size,
@RequestParam(required = false) Integer status) {
Page<Registration> page = new Page<>(current, size);
IPage<RegistrationVO> result = registrationService.getMyRegistrations(page, status);
return Result.success(result);
}
@Operation(summary = "获取活动报名列表(管理员)")
@GetMapping("/activity/{activityId}")
@PreAuthorize("hasRole('ADMIN')")
public Result<IPage<RegistrationVO>> getActivityRegistrations(
@PathVariable Long activityId,
@RequestParam(defaultValue = "1") Long current,
@RequestParam(defaultValue = "10") Long size) {
Page<Registration> page = new Page<>(current, size);
IPage<RegistrationVO> result = registrationService.getActivityRegistrations(page, activityId);
return Result.success(result);
}
@Operation(summary = "下载电子票PDF")
@GetMapping("/{id}/ticket")
public ResponseEntity<byte[]> downloadTicket(@PathVariable Long id) {
byte[] pdfBytes = registrationService.generateTicketPdf(id);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=ticket.pdf")
.contentType(MediaType.APPLICATION_PDF)
.contentLength(pdfBytes.length)
.body(pdfBytes);
}
}

View File

@@ -0,0 +1,51 @@
package com.campus.activity.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.campus.activity.common.Result;
import com.campus.activity.dto.request.ReviewRequest;
import com.campus.activity.entity.Review;
import com.campus.activity.service.ReviewService;
import com.campus.activity.vo.ReviewVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@Tag(name = "评价模块", description = "活动评价相关接口")
@RestController
@RequestMapping("/api/v1/reviews")
@RequiredArgsConstructor
public class ReviewController {
private final ReviewService reviewService;
@Operation(summary = "提交评价")
@PostMapping
public Result<ReviewVO> createReview(@Valid @RequestBody ReviewRequest request) {
ReviewVO reviewVO = reviewService.createReview(request);
return Result.success("评价成功", reviewVO);
}
@Operation(summary = "获取活动评价列表")
@GetMapping("/activity/{activityId}")
public Result<IPage<ReviewVO>> getActivityReviews(
@PathVariable Long activityId,
@RequestParam(defaultValue = "1") Long current,
@RequestParam(defaultValue = "10") Long size) {
Page<Review> page = new Page<>(current, size);
IPage<ReviewVO> result = reviewService.getActivityReviews(page, activityId);
return Result.success(result);
}
@Operation(summary = "获取我的评价列表")
@GetMapping("/my")
public Result<IPage<ReviewVO>> getMyReviews(
@RequestParam(defaultValue = "1") Long current,
@RequestParam(defaultValue = "10") Long size) {
Page<Review> page = new Page<>(current, size);
IPage<ReviewVO> result = reviewService.getMyReviews(page);
return Result.success(result);
}
}

View File

@@ -0,0 +1,46 @@
package com.campus.activity.controller;
import com.campus.activity.common.Result;
import com.campus.activity.service.StatisticsService;
import com.campus.activity.vo.ActivityStatisticsVO;
import com.campus.activity.vo.OverviewStatisticsVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@Tag(name = "统计模块", description = "数据统计与导出相关接口")
@RestController
@RequestMapping("/api/v1/statistics")
@RequiredArgsConstructor
public class StatisticsController {
private final StatisticsService statisticsService;
@Operation(summary = "获取活动统计数据(管理员)")
@GetMapping("/activity/{activityId}")
@PreAuthorize("hasRole('ADMIN')")
public Result<ActivityStatisticsVO> getActivityStatistics(@PathVariable Long activityId) {
ActivityStatisticsVO statistics = statisticsService.getActivityStatistics(activityId);
return Result.success(statistics);
}
@Operation(summary = "导出活动数据(管理员)")
@GetMapping("/activity/{activityId}/export")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<byte[]> exportActivityData(
@PathVariable Long activityId,
@RequestParam(defaultValue = "excel") String format) {
return statisticsService.exportActivityData(activityId, format);
}
@Operation(summary = "获取总体统计(管理员)")
@GetMapping("/overview")
@PreAuthorize("hasRole('ADMIN')")
public Result<OverviewStatisticsVO> getOverviewStatistics() {
OverviewStatisticsVO statistics = statisticsService.getOverviewStatistics();
return Result.success(statistics);
}
}

View File

@@ -0,0 +1,40 @@
package com.campus.activity.dto.request;
import jakarta.validation.constraints.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class ActivityCreateRequest {
@NotBlank(message = "活动名称不能为空")
@Size(max = 100, message = "活动名称不能超过100个字符")
private String title;
@Size(max = 2000, message = "活动简介不能超过2000个字符")
private String description;
private String coverImage;
@NotNull(message = "开始时间不能为空")
@Future(message = "开始时间必须是未来时间")
private LocalDateTime startTime;
@NotNull(message = "结束时间不能为空")
private LocalDateTime endTime;
private LocalDateTime registrationDeadline;
@NotBlank(message = "活动地点不能为空")
@Size(max = 200, message = "活动地点不能超过200个字符")
private String location;
@NotNull(message = "报名人数上限不能为空")
@Min(value = 1, message = "报名人数上限至少为1")
@Max(value = 10000, message = "报名人数上限不能超过10000")
private Integer maxParticipants;
@Size(max = 50, message = "活动分类不能超过50个字符")
private String category;
}

View File

@@ -0,0 +1,41 @@
package com.campus.activity.dto.request;
import jakarta.validation.constraints.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class ActivityUpdateRequest {
@NotBlank(message = "活动名称不能为空")
@Size(max = 100, message = "活动名称不能超过100个字符")
private String title;
@Size(max = 2000, message = "活动简介不能超过2000个字符")
private String description;
private String coverImage;
@NotNull(message = "开始时间不能为空")
private LocalDateTime startTime;
@NotNull(message = "结束时间不能为空")
private LocalDateTime endTime;
private LocalDateTime registrationDeadline;
@NotBlank(message = "活动地点不能为空")
@Size(max = 200, message = "活动地点不能超过200个字符")
private String location;
@NotNull(message = "报名人数上限不能为空")
@Min(value = 1, message = "报名人数上限至少为1")
@Max(value = 10000, message = "报名人数上限不能超过10000")
private Integer maxParticipants;
private Integer status;
@Size(max = 50, message = "活动分类不能超过50个字符")
private String category;
}

View File

@@ -0,0 +1,16 @@
package com.campus.activity.dto.request;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Data
public class ChangePasswordRequest {
@NotBlank(message = "旧密码不能为空")
private String oldPassword;
@NotBlank(message = "新密码不能为空")
@Size(min = 6, max = 20, message = "密码长度必须在6-20个字符之间")
private String newPassword;
}

View File

@@ -0,0 +1,20 @@
package com.campus.activity.dto.request;
import jakarta.validation.constraints.NotNull;
import lombok.Builder;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@Builder
public class CheckConflictRequest {
@NotNull(message = "开始时间不能为空")
private LocalDateTime startTime;
@NotNull(message = "结束时间不能为空")
private LocalDateTime endTime;
private Long excludeActivityId;
}

View File

@@ -0,0 +1,14 @@
package com.campus.activity.dto.request;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class LoginRequest {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
private String password;
}

View File

@@ -0,0 +1,11 @@
package com.campus.activity.dto.request;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class RefreshTokenRequest {
@NotBlank(message = "刷新Token不能为空")
private String refreshToken;
}

View File

@@ -0,0 +1,32 @@
package com.campus.activity.dto.request;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Data
public class RegisterRequest {
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 50, message = "用户名长度必须在3-50个字符之间")
private String username;
@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 20, message = "密码长度必须在6-20个字符之间")
private String password;
@NotBlank(message = "姓名不能为空")
@Size(max = 50, message = "姓名不能超过50个字符")
private String name;
@Pattern(regexp = "^\\d{10,20}$", message = "学号格式不正确")
private String studentId;
@Email(message = "邮箱格式不正确")
private String email;
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
}

View File

@@ -0,0 +1,11 @@
package com.campus.activity.dto.request;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
public class RegistrationRequest {
@NotNull(message = "活动ID不能为空")
private Long activityId;
}

View File

@@ -0,0 +1,19 @@
package com.campus.activity.dto.request;
import jakarta.validation.constraints.*;
import lombok.Data;
@Data
public class ReviewRequest {
@NotNull(message = "活动ID不能为空")
private Long activityId;
@NotNull(message = "评分不能为空")
@Min(value = 1, message = "评分最小为1")
@Max(value = 5, message = "评分最大为5")
private Integer rating;
@Size(max = 500, message = "评论内容不能超过500个字符")
private String content;
}

View File

@@ -0,0 +1,11 @@
package com.campus.activity.dto.request;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class ScanCheckInRequest {
@NotBlank(message = "二维码内容不能为空")
private String qrCodeContent;
}

View File

@@ -0,0 +1,15 @@
package com.campus.activity.dto.request;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
public class TicketCheckInRequest {
@NotNull(message = "活动ID不能为空")
private Long activityId;
@NotBlank(message = "电子票号不能为空")
private String ticketCode;
}

View File

@@ -0,0 +1,31 @@
package com.campus.activity.dto.response;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LoginResponse {
private String accessToken;
private String refreshToken;
private Long expiresIn;
private String tokenType;
private UserInfo userInfo;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class UserInfo {
private Long id;
private String username;
private String name;
private Integer role;
private String avatar;
}
}

View File

@@ -0,0 +1,16 @@
package com.campus.activity.dto.response;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RefreshTokenResponse {
private String accessToken;
private Long expiresIn;
}

View File

@@ -0,0 +1,61 @@
package com.campus.activity.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@TableName("activity")
public class Activity implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
private String title;
private String description;
private String coverImage;
private LocalDateTime startTime;
private LocalDateTime endTime;
private LocalDateTime registrationDeadline;
private String location;
private Integer maxParticipants;
private Integer currentParticipants;
private Integer status;
private String category;
private Long adminId;
private String qrCode;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
@TableLogic
private Integer deleted;
@Version
private Integer version;
@TableField(exist = false)
private Double averageRating;
@TableField(exist = false)
private Long reviewCount;
}

View File

@@ -0,0 +1,27 @@
package com.campus.activity.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@TableName("check_in")
public class CheckIn implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
private Long registrationId;
private Long userId;
private Long activityId;
private LocalDateTime checkInTime;
private Integer checkInMethod;
}

View File

@@ -0,0 +1,35 @@
package com.campus.activity.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@TableName("registration")
public class Registration implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
private Long userId;
private Long activityId;
private String ticketCode;
private String ticketPdfUrl;
private Integer status;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
private LocalDateTime canceledAt;
}

View File

@@ -0,0 +1,31 @@
package com.campus.activity.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@TableName("review")
public class Review implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
private Long userId;
private Long activityId;
private Integer rating;
private String content;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,44 @@
package com.campus.activity.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@TableName("user")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String password;
private String name;
private String studentId;
private String email;
private String phone;
private String avatar;
private Integer role;
private Integer status;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
@TableLogic
private Integer deleted;
}

View File

@@ -0,0 +1,30 @@
package com.campus.activity.exception;
import com.campus.activity.common.ResultCode;
import lombok.Getter;
@Getter
public class BusinessException extends RuntimeException {
private final Integer code;
public BusinessException(String message) {
super(message);
this.code = ResultCode.ERROR.getCode();
}
public BusinessException(Integer code, String message) {
super(message);
this.code = code;
}
public BusinessException(ResultCode resultCode) {
super(resultCode.getMessage());
this.code = resultCode.getCode();
}
public BusinessException(ResultCode resultCode, String message) {
super(message);
this.code = resultCode.getCode();
}
}

View File

@@ -0,0 +1,73 @@
package com.campus.activity.exception;
import com.campus.activity.common.Result;
import com.campus.activity.common.ResultCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.NoHandlerFoundException;
import java.util.stream.Collectors;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public Result<?> handleBusinessException(BusinessException e) {
log.warn("业务异常: code={}, message={}", e.getCode(), e.getMessage());
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(", "));
log.warn("参数校验异常: {}", message);
return Result.error(ResultCode.BAD_REQUEST.getCode(), message);
}
@ExceptionHandler(BadCredentialsException.class)
public Result<?> handleBadCredentialsException(BadCredentialsException e) {
log.warn("认证失败: {}", e.getMessage());
return Result.error(ResultCode.USERNAME_OR_PASSWORD_ERROR.getCode(),
ResultCode.USERNAME_OR_PASSWORD_ERROR.getMessage());
}
@ExceptionHandler(AccessDeniedException.class)
public Result<?> handleAccessDeniedException(AccessDeniedException e) {
log.warn("访问拒绝: {}", e.getMessage());
return Result.error(ResultCode.FORBIDDEN.getCode(), ResultCode.FORBIDDEN.getMessage());
}
@ExceptionHandler(NoHandlerFoundException.class)
public Result<?> handleNoHandlerFoundException(NoHandlerFoundException e) {
log.warn("接口不存在: {}", e.getRequestURL());
return Result.error(ResultCode.NOT_FOUND.getCode(), ResultCode.NOT_FOUND.getMessage());
}
@ExceptionHandler(HttpMessageNotReadableException.class)
public Result<?> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
log.warn("请求参数解析失败: {}", e.getMessage());
return Result.error(ResultCode.BAD_REQUEST.getCode(), "请求参数格式错误");
}
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public Result<?> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
log.warn("参数类型错误: {}", e.getMessage());
return Result.error(ResultCode.BAD_REQUEST.getCode(), "参数类型错误");
}
@ExceptionHandler(Exception.class)
public Result<?> handleException(Exception e) {
log.error("系统异常: ", e);
return Result.error(ResultCode.ERROR.getCode(), ResultCode.ERROR.getMessage());
}
}

View File

@@ -0,0 +1,29 @@
package com.campus.activity.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.campus.activity.entity.Activity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDateTime;
import java.util.List;
@Mapper
public interface ActivityMapper extends BaseMapper<Activity> {
IPage<Activity> selectActivityPage(Page<Activity> page,
@Param("status") Integer status,
@Param("keyword") String keyword,
@Param("category") String category,
@Param("startDate") LocalDateTime startDate,
@Param("endDate") LocalDateTime endDate);
List<Activity> selectCalendarActivities(@Param("year") Integer year,
@Param("month") Integer month);
List<Activity> selectConflictActivities(@Param("startTime") LocalDateTime startTime,
@Param("endTime") LocalDateTime endTime,
@Param("excludeActivityId") Long excludeActivityId);
}

View File

@@ -0,0 +1,14 @@
package com.campus.activity.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.campus.activity.entity.CheckIn;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface CheckInMapper extends BaseMapper<CheckIn> {
IPage<com.campus.activity.vo.CheckInVO> selectActivityCheckIns(Page<CheckIn> page, @Param("activityId") Long activityId);
}

View File

@@ -0,0 +1,21 @@
package com.campus.activity.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.campus.activity.entity.Registration;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface RegistrationMapper extends BaseMapper<Registration> {
IPage<com.campus.activity.vo.RegistrationVO> selectMyRegistrations(Page<Registration> page,
@Param("userId") Long userId,
@Param("status") Integer status);
IPage<com.campus.activity.vo.RegistrationVO> selectActivityRegistrations(Page<Registration> page,
@Param("activityId") Long activityId);
Registration selectByTicketCode(@Param("ticketCode") String ticketCode);
}

View File

@@ -0,0 +1,16 @@
package com.campus.activity.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.campus.activity.entity.Review;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface ReviewMapper extends BaseMapper<Review> {
IPage<Review> selectActivityReviews(Page<Review> page, @Param("activityId") Long activityId);
IPage<Review> selectMyReviews(Page<Review> page, @Param("userId") Long userId);
}

View File

@@ -0,0 +1,9 @@
package com.campus.activity.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.campus.activity.entity.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
}

View File

@@ -0,0 +1,57 @@
package com.campus.activity.security;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Collections;
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = extractTokenFromRequest(request);
if (StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) {
Long userId = jwtTokenProvider.getUserIdFromToken(token);
String username = jwtTokenProvider.getUsernameFromToken(token);
Integer role = jwtTokenProvider.getRoleFromToken(token);
String authority = role == 1 ? "ROLE_ADMIN" : "ROLE_STUDENT";
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userId,
null,
Collections.singletonList(new SimpleGrantedAuthority(authority))
);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String extractTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}

View File

@@ -0,0 +1,104 @@
package com.campus.activity.security;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String jwtSecret;
@Value("${jwt.expiration}")
private Long jwtExpiration;
@Value("${jwt.refresh-expiration}")
private Long refreshExpiration;
private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8));
}
public String generateToken(Long userId, String username, Integer role) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId);
claims.put("username", username);
claims.put("role", role);
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtExpiration);
return Jwts.builder()
.claims(claims)
.subject(username)
.issuedAt(now)
.expiration(expiryDate)
.signWith(getSigningKey())
.compact();
}
public String generateRefreshToken(Long userId) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + refreshExpiration);
return Jwts.builder()
.subject(String.valueOf(userId))
.issuedAt(now)
.expiration(expiryDate)
.signWith(getSigningKey())
.compact();
}
public Long getUserIdFromToken(String token) {
Claims claims = Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
return claims.get("userId", Long.class);
}
public String getUsernameFromToken(String token) {
Claims claims = Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
return claims.getSubject();
}
public Integer getRoleFromToken(String token) {
Claims claims = Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
return claims.get("role", Integer.class);
}
public boolean validateToken(String token) {
try {
Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
log.error("Invalid JWT token: {}", e.getMessage());
return false;
}
}
}

View File

@@ -0,0 +1,45 @@
package com.campus.activity.security;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.campus.activity.entity.User;
import com.campus.activity.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Collections;
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.selectOne(
new LambdaQueryWrapper<User>()
.eq(User::getUsername, username)
.eq(User::getDeleted, 0)
);
if (user == null) {
throw new UsernameNotFoundException("用户不存在: " + username);
}
if (user.getStatus() == 0) {
throw new UsernameNotFoundException("用户已被禁用: " + username);
}
String authority = user.getRole() == 1 ? "ROLE_ADMIN" : "ROLE_STUDENT";
return org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword())
.authorities(Collections.singletonList(new SimpleGrantedAuthority(authority)))
.build();
}
}

View File

@@ -0,0 +1,33 @@
package com.campus.activity.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.campus.activity.dto.request.*;
import com.campus.activity.entity.Activity;
import com.campus.activity.vo.ActivityVO;
import com.campus.activity.vo.ConflictCheckVO;
import java.time.LocalDateTime;
import java.util.List;
public interface ActivityService {
IPage<ActivityVO> pageActivities(Page<Activity> page,
Integer status,
String keyword,
String category,
LocalDateTime startDate,
LocalDateTime endDate);
ActivityVO getActivityById(Long id);
Long createActivity(ActivityCreateRequest request);
void updateActivity(Long id, ActivityUpdateRequest request);
void deleteActivity(Long id);
List<ActivityVO> getCalendarActivities(Integer year, Integer month);
ConflictCheckVO checkConflict(CheckConflictRequest request);
}

View File

@@ -0,0 +1,21 @@
package com.campus.activity.service;
import com.campus.activity.dto.request.*;
import com.campus.activity.dto.response.LoginResponse;
import com.campus.activity.dto.response.RefreshTokenResponse;
import com.campus.activity.entity.User;
public interface AuthService {
void register(RegisterRequest request);
LoginResponse login(LoginRequest request);
RefreshTokenResponse refreshToken(RefreshTokenRequest request);
User getCurrentUser();
User getUserById(Long userId);
void changePassword(ChangePasswordRequest request);
}

View File

@@ -0,0 +1,20 @@
package com.campus.activity.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.campus.activity.dto.request.ScanCheckInRequest;
import com.campus.activity.dto.request.TicketCheckInRequest;
import com.campus.activity.entity.CheckIn;
import com.campus.activity.vo.CheckInVO;
import com.campus.activity.vo.QrCodeVO;
public interface CheckInService {
QrCodeVO generateQrCode(Long activityId);
CheckInVO scanCheckIn(ScanCheckInRequest request);
CheckInVO ticketCheckIn(TicketCheckInRequest request);
IPage<CheckInVO> getActivityCheckIns(Page<CheckIn> page, Long activityId);
}

View File

@@ -0,0 +1,22 @@
package com.campus.activity.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.campus.activity.dto.request.RegistrationRequest;
import com.campus.activity.entity.Registration;
import com.campus.activity.vo.RegistrationVO;
import java.util.List;
public interface RegistrationService {
RegistrationVO register(RegistrationRequest request);
void cancelRegistration(Long id);
IPage<RegistrationVO> getMyRegistrations(Page<Registration> page, Integer status);
IPage<RegistrationVO> getActivityRegistrations(Page<Registration> page, Long activityId);
byte[] generateTicketPdf(Long id);
}

View File

@@ -0,0 +1,16 @@
package com.campus.activity.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.campus.activity.dto.request.ReviewRequest;
import com.campus.activity.entity.Review;
import com.campus.activity.vo.ReviewVO;
public interface ReviewService {
ReviewVO createReview(ReviewRequest request);
IPage<ReviewVO> getActivityReviews(Page<Review> page, Long activityId);
IPage<ReviewVO> getMyReviews(Page<Review> page);
}

View File

@@ -0,0 +1,14 @@
package com.campus.activity.service;
import com.campus.activity.vo.ActivityStatisticsVO;
import com.campus.activity.vo.OverviewStatisticsVO;
import org.springframework.http.ResponseEntity;
public interface StatisticsService {
ActivityStatisticsVO getActivityStatistics(Long activityId);
ResponseEntity<byte[]> exportActivityData(Long activityId, String format);
OverviewStatisticsVO getOverviewStatistics();
}

View File

@@ -0,0 +1,224 @@
package com.campus.activity.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.campus.activity.common.ResultCode;
import com.campus.activity.dto.request.*;
import com.campus.activity.entity.Activity;
import com.campus.activity.entity.Registration;
import com.campus.activity.entity.User;
import com.campus.activity.exception.BusinessException;
import com.campus.activity.mapper.ActivityMapper;
import com.campus.activity.mapper.RegistrationMapper;
import com.campus.activity.mapper.UserMapper;
import com.campus.activity.service.ActivityService;
import com.campus.activity.service.AuthService;
import com.campus.activity.vo.ActivityVO;
import com.campus.activity.vo.ConflictCheckVO;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class ActivityServiceImpl implements ActivityService {
private final ActivityMapper activityMapper;
private final UserMapper userMapper;
private final RegistrationMapper registrationMapper;
private final AuthService authService;
@Override
public IPage<ActivityVO> pageActivities(Page<Activity> page,
Integer status,
String keyword,
String category,
LocalDateTime startDate,
LocalDateTime endDate) {
IPage<Activity> activityPage = activityMapper.selectActivityPage(page, status, keyword, category, startDate, endDate);
return activityPage.convert(activity -> {
ActivityVO vo = convertToVO(activity);
return vo;
});
}
@Override
public ActivityVO getActivityById(Long id) {
Activity activity = activityMapper.selectById(id);
if (activity == null || activity.getDeleted() == 1) {
throw new BusinessException(ResultCode.ACTIVITY_NOT_FOUND);
}
ActivityVO vo = convertToVO(activity);
try {
User currentUser = authService.getCurrentUser();
Registration registration = registrationMapper.selectOne(
new LambdaQueryWrapper<Registration>()
.eq(Registration::getUserId, currentUser.getId())
.eq(Registration::getActivityId, id)
.eq(Registration::getStatus, 1)
);
vo.setIsRegistered(registration != null);
} catch (Exception e) {
vo.setIsRegistered(false);
}
return vo;
}
@Override
@Transactional
public Long createActivity(ActivityCreateRequest request) {
User currentUser = authService.getCurrentUser();
if (currentUser.getRole() != 1) {
throw new BusinessException(ResultCode.FORBIDDEN);
}
if (request.getStartTime().isAfter(request.getEndTime())) {
throw new BusinessException(ResultCode.BAD_REQUEST.getCode(), "开始时间不能晚于结束时间");
}
ConflictCheckVO conflictCheck = checkConflict(CheckConflictRequest.builder()
.startTime(request.getStartTime())
.endTime(request.getEndTime())
.build());
if (conflictCheck.getHasConflict()) {
throw new BusinessException(ResultCode.ACTIVITY_TIME_CONFLICT);
}
Activity activity = new Activity();
BeanUtils.copyProperties(request, activity);
activity.setAdminId(currentUser.getId());
activity.setCurrentParticipants(0);
activity.setStatus(0);
if (request.getRegistrationDeadline() == null) {
activity.setRegistrationDeadline(request.getStartTime());
}
activityMapper.insert(activity);
return activity.getId();
}
@Override
@Transactional
public void updateActivity(Long id, ActivityUpdateRequest request) {
User currentUser = authService.getCurrentUser();
if (currentUser.getRole() != 1) {
throw new BusinessException(ResultCode.FORBIDDEN);
}
Activity activity = activityMapper.selectById(id);
if (activity == null || activity.getDeleted() == 1) {
throw new BusinessException(ResultCode.ACTIVITY_NOT_FOUND);
}
if (!activity.getAdminId().equals(currentUser.getId())) {
throw new BusinessException(ResultCode.FORBIDDEN);
}
if (request.getStartTime().isAfter(request.getEndTime())) {
throw new BusinessException(ResultCode.BAD_REQUEST.getCode(), "开始时间不能晚于结束时间");
}
ConflictCheckVO conflictCheck = checkConflict(CheckConflictRequest.builder()
.startTime(request.getStartTime())
.endTime(request.getEndTime())
.excludeActivityId(id)
.build());
if (conflictCheck.getHasConflict()) {
throw new BusinessException(ResultCode.ACTIVITY_TIME_CONFLICT);
}
BeanUtils.copyProperties(request, activity);
activityMapper.updateById(activity);
}
@Override
@Transactional
public void deleteActivity(Long id) {
User currentUser = authService.getCurrentUser();
if (currentUser.getRole() != 1) {
throw new BusinessException(ResultCode.FORBIDDEN);
}
Activity activity = activityMapper.selectById(id);
if (activity == null || activity.getDeleted() == 1) {
throw new BusinessException(ResultCode.ACTIVITY_NOT_FOUND);
}
if (!activity.getAdminId().equals(currentUser.getId())) {
throw new BusinessException(ResultCode.FORBIDDEN);
}
activityMapper.deleteById(id);
}
@Override
public List<ActivityVO> getCalendarActivities(Integer year, Integer month) {
List<Activity> activities = activityMapper.selectCalendarActivities(year, month);
return activities.stream()
.map(this::convertToVO)
.collect(Collectors.toList());
}
@Override
public ConflictCheckVO checkConflict(CheckConflictRequest request) {
if (request.getStartTime().isAfter(request.getEndTime())) {
throw new BusinessException(ResultCode.BAD_REQUEST.getCode(), "开始时间不能晚于结束时间");
}
List<Activity> conflictActivities = activityMapper.selectConflictActivities(
request.getStartTime(),
request.getEndTime(),
request.getExcludeActivityId()
);
return ConflictCheckVO.builder()
.hasConflict(!conflictActivities.isEmpty())
.conflictActivities(conflictActivities.stream()
.map(activity -> ConflictCheckVO.ConflictActivity.builder()
.id(activity.getId())
.title(activity.getTitle())
.startTime(activity.getStartTime())
.endTime(activity.getEndTime())
.build())
.collect(Collectors.toList()))
.build();
}
private ActivityVO convertToVO(Activity activity) {
User admin = userMapper.selectById(activity.getAdminId());
return ActivityVO.builder()
.id(activity.getId())
.title(activity.getTitle())
.description(activity.getDescription())
.coverImage(activity.getCoverImage())
.startTime(activity.getStartTime())
.endTime(activity.getEndTime())
.registrationDeadline(activity.getRegistrationDeadline())
.location(activity.getLocation())
.maxParticipants(activity.getMaxParticipants())
.currentParticipants(activity.getCurrentParticipants())
.status(activity.getStatus())
.category(activity.getCategory())
.adminId(activity.getAdminId())
.adminName(admin != null ? admin.getName() : null)
.averageRating(activity.getAverageRating())
.reviewCount(activity.getReviewCount())
.createdAt(activity.getCreatedAt())
.build();
}
}

View File

@@ -0,0 +1,155 @@
package com.campus.activity.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.campus.activity.common.ResultCode;
import com.campus.activity.dto.request.*;
import com.campus.activity.dto.response.LoginResponse;
import com.campus.activity.dto.response.RefreshTokenResponse;
import com.campus.activity.entity.User;
import com.campus.activity.exception.BusinessException;
import com.campus.activity.mapper.UserMapper;
import com.campus.activity.security.JwtTokenProvider;
import com.campus.activity.service.AuthService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class AuthServiceImpl implements AuthService {
private final UserMapper userMapper;
private final PasswordEncoder passwordEncoder;
private final AuthenticationManager authenticationManager;
private final JwtTokenProvider jwtTokenProvider;
@Override
@Transactional
public void register(RegisterRequest request) {
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUsername, request.getUsername());
if (userMapper.selectCount(queryWrapper) > 0) {
throw new BusinessException(ResultCode.USER_ALREADY_EXISTS);
}
if (request.getStudentId() != null) {
queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getStudentId, request.getStudentId());
if (userMapper.selectCount(queryWrapper) > 0) {
throw new BusinessException(ResultCode.STUDENT_ID_ALREADY_EXISTS);
}
}
User user = new User();
user.setUsername(request.getUsername());
user.setPassword(passwordEncoder.encode(request.getPassword()));
user.setName(request.getName());
user.setStudentId(request.getStudentId());
user.setEmail(request.getEmail());
user.setPhone(request.getPhone());
user.setRole(0);
user.setStatus(1);
userMapper.insert(user);
}
@Override
public LoginResponse login(LoginRequest request) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
);
User user = userMapper.selectOne(
new LambdaQueryWrapper<User>()
.eq(User::getUsername, request.getUsername())
.eq(User::getDeleted, 0)
);
if (user == null) {
throw new BusinessException(ResultCode.USER_NOT_FOUND);
}
String accessToken = jwtTokenProvider.generateToken(user.getId(), user.getUsername(), user.getRole());
String refreshToken = jwtTokenProvider.generateRefreshToken(user.getId());
return LoginResponse.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.expiresIn(7200L)
.tokenType("Bearer")
.userInfo(LoginResponse.UserInfo.builder()
.id(user.getId())
.username(user.getUsername())
.name(user.getName())
.role(user.getRole())
.avatar(user.getAvatar())
.build())
.build();
}
@Override
public RefreshTokenResponse refreshToken(RefreshTokenRequest request) {
if (!jwtTokenProvider.validateToken(request.getRefreshToken())) {
throw new BusinessException(ResultCode.UNAUTHORIZED);
}
Long userId = Long.valueOf(jwtTokenProvider.getUsernameFromToken(request.getRefreshToken()));
User user = userMapper.selectById(userId);
if (user == null || user.getDeleted() == 1) {
throw new BusinessException(ResultCode.USER_NOT_FOUND);
}
String newAccessToken = jwtTokenProvider.generateToken(user.getId(), user.getUsername(), user.getRole());
return RefreshTokenResponse.builder()
.accessToken(newAccessToken)
.expiresIn(7200L)
.build();
}
@Override
public User getCurrentUser() {
Authentication authentication = org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
throw new BusinessException(ResultCode.UNAUTHORIZED);
}
Long userId = (Long) authentication.getPrincipal();
User user = userMapper.selectById(userId);
if (user == null || user.getDeleted() == 1) {
throw new BusinessException(ResultCode.USER_NOT_FOUND);
}
return user;
}
@Override
public User getUserById(Long userId) {
User user = userMapper.selectById(userId);
if (user == null || user.getDeleted() == 1) {
throw new BusinessException(ResultCode.USER_NOT_FOUND);
}
return user;
}
@Override
@Transactional
public void changePassword(ChangePasswordRequest request) {
User user = getCurrentUser();
if (!passwordEncoder.matches(request.getOldPassword(), user.getPassword())) {
throw new BusinessException(ResultCode.PASSWORD_ERROR);
}
user.setPassword(passwordEncoder.encode(request.getNewPassword()));
userMapper.updateById(user);
}
}

View File

@@ -0,0 +1,179 @@
package com.campus.activity.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.campus.activity.common.ResultCode;
import com.campus.activity.dto.request.ScanCheckInRequest;
import com.campus.activity.dto.request.TicketCheckInRequest;
import com.campus.activity.entity.Activity;
import com.campus.activity.entity.CheckIn;
import com.campus.activity.entity.Registration;
import com.campus.activity.entity.User;
import com.campus.activity.exception.BusinessException;
import com.campus.activity.mapper.ActivityMapper;
import com.campus.activity.mapper.CheckInMapper;
import com.campus.activity.mapper.RegistrationMapper;
import com.campus.activity.service.AuthService;
import com.campus.activity.service.CheckInService;
import com.campus.activity.util.QrCodeUtil;
import com.campus.activity.vo.CheckInVO;
import com.campus.activity.vo.QrCodeVO;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
@Service
@RequiredArgsConstructor
public class CheckInServiceImpl implements CheckInService {
private final CheckInMapper checkInMapper;
private final RegistrationMapper registrationMapper;
private final ActivityMapper activityMapper;
private final AuthService authService;
@Override
public QrCodeVO generateQrCode(Long activityId) {
User currentUser = authService.getCurrentUser();
if (currentUser.getRole() != 1) {
throw new BusinessException(ResultCode.FORBIDDEN);
}
Activity activity = activityMapper.selectById(activityId);
if (activity == null || activity.getDeleted() == 1) {
throw new BusinessException(ResultCode.ACTIVITY_NOT_FOUND);
}
if (!activity.getAdminId().equals(currentUser.getId())) {
throw new BusinessException(ResultCode.FORBIDDEN);
}
String qrCodeContent = QrCodeUtil.generateQrCodeContent(activityId);
LocalDateTime expiresAt = activity.getEndTime();
return QrCodeVO.builder()
.qrCodeContent(qrCodeContent)
.expiresAt(expiresAt)
.build();
}
@Override
@Transactional
public CheckInVO scanCheckIn(ScanCheckInRequest request) {
User currentUser = authService.getCurrentUser();
Long activityId = QrCodeUtil.parseActivityIdFromQrCode(request.getQrCodeContent());
if (activityId == null) {
throw new BusinessException(ResultCode.BAD_REQUEST.getCode(), "二维码无效");
}
Activity activity = activityMapper.selectById(activityId);
if (activity == null || activity.getDeleted() == 1) {
throw new BusinessException(ResultCode.ACTIVITY_NOT_FOUND);
}
LocalDateTime now = LocalDateTime.now();
if (now.isBefore(activity.getStartTime()) || now.isAfter(activity.getEndTime())) {
throw new BusinessException(ResultCode.CHECKIN_TIME_EXPIRED);
}
Registration registration = registrationMapper.selectOne(
new LambdaQueryWrapper<Registration>()
.eq(Registration::getUserId, currentUser.getId())
.eq(Registration::getActivityId, activityId)
.eq(Registration::getStatus, 1)
);
if (registration == null) {
throw new BusinessException(ResultCode.NOT_REGISTERED);
}
CheckIn existingCheckIn = checkInMapper.selectOne(
new LambdaQueryWrapper<CheckIn>()
.eq(CheckIn::getRegistrationId, registration.getId())
);
if (existingCheckIn != null) {
throw new BusinessException(ResultCode.ALREADY_CHECKED_IN);
}
return performCheckIn(registration, currentUser, activityId, 0);
}
@Override
@Transactional
public CheckInVO ticketCheckIn(TicketCheckInRequest request) {
User currentUser = authService.getCurrentUser();
if (currentUser.getRole() != 1) {
throw new BusinessException(ResultCode.FORBIDDEN);
}
Activity activity = activityMapper.selectById(request.getActivityId());
if (activity == null || activity.getDeleted() == 1) {
throw new BusinessException(ResultCode.ACTIVITY_NOT_FOUND);
}
if (!activity.getAdminId().equals(currentUser.getId())) {
throw new BusinessException(ResultCode.FORBIDDEN);
}
LocalDateTime now = LocalDateTime.now();
if (now.isBefore(activity.getStartTime()) || now.isAfter(activity.getEndTime())) {
throw new BusinessException(ResultCode.CHECKIN_TIME_EXPIRED);
}
Registration registration = registrationMapper.selectByTicketCode(request.getTicketCode());
if (registration == null) {
throw new BusinessException(ResultCode.BAD_REQUEST.getCode(), "电子票无效");
}
if (!registration.getActivityId().equals(request.getActivityId())) {
throw new BusinessException(ResultCode.BAD_REQUEST.getCode(), "电子票与活动不匹配");
}
CheckIn existingCheckIn = checkInMapper.selectOne(
new LambdaQueryWrapper<CheckIn>()
.eq(CheckIn::getRegistrationId, registration.getId())
);
if (existingCheckIn != null) {
throw new BusinessException(ResultCode.ALREADY_CHECKED_IN);
}
User student = authService.getUserById(registration.getUserId());
return performCheckIn(registration, student, request.getActivityId(), 1);
}
@Override
public IPage<CheckInVO> getActivityCheckIns(Page<CheckIn> page, Long activityId) {
return checkInMapper.selectActivityCheckIns(page, activityId);
}
private CheckInVO performCheckIn(Registration registration, User user, Long activityId, int method) {
CheckIn checkIn = new CheckIn();
checkIn.setRegistrationId(registration.getId());
checkIn.setUserId(user.getId());
checkIn.setActivityId(activityId);
checkIn.setCheckInTime(LocalDateTime.now());
checkIn.setCheckInMethod(method);
checkInMapper.insert(checkIn);
registration.setStatus(2);
registrationMapper.updateById(registration);
return convertToVO(checkIn, user);
}
private CheckInVO convertToVO(CheckIn checkIn, User user) {
CheckInVO vo = new CheckInVO();
BeanUtils.copyProperties(checkIn, vo);
vo.setUserName(user.getName());
vo.setStudentId(user.getStudentId());
return vo;
}
}

View File

@@ -0,0 +1,189 @@
package com.campus.activity.service.impl;
import cn.hutool.core.date.DateUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.campus.activity.common.ResultCode;
import com.campus.activity.dto.request.RegistrationRequest;
import com.campus.activity.entity.Activity;
import com.campus.activity.entity.Registration;
import com.campus.activity.entity.User;
import com.campus.activity.exception.BusinessException;
import com.campus.activity.mapper.ActivityMapper;
import com.campus.activity.mapper.RegistrationMapper;
import com.campus.activity.service.ActivityService;
import com.campus.activity.service.AuthService;
import com.campus.activity.service.RegistrationService;
import com.campus.activity.util.PdfUtil;
import com.campus.activity.vo.RegistrationVO;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
@Service
@RequiredArgsConstructor
public class RegistrationServiceImpl implements RegistrationService {
private final RegistrationMapper registrationMapper;
private final ActivityMapper activityMapper;
private final AuthService authService;
private final ActivityService activityService;
@Override
@Transactional
public RegistrationVO register(RegistrationRequest request) {
User currentUser = authService.getCurrentUser();
Activity activity = activityMapper.selectById(request.getActivityId());
if (activity == null || activity.getDeleted() == 1) {
throw new BusinessException(ResultCode.ACTIVITY_NOT_FOUND);
}
if (activity.getStatus() != 1) {
throw new BusinessException(ResultCode.BAD_REQUEST.getCode(), "活动不在报名中");
}
if (activity.getRegistrationDeadline() != null &&
LocalDateTime.now().isAfter(activity.getRegistrationDeadline())) {
throw new BusinessException(ResultCode.BAD_REQUEST.getCode(), "报名已截止");
}
if (activity.getCurrentParticipants() >= activity.getMaxParticipants()) {
throw new BusinessException(ResultCode.ACTIVITY_FULL);
}
Registration existingRegistration = registrationMapper.selectOne(
new LambdaQueryWrapper<Registration>()
.eq(Registration::getUserId, currentUser.getId())
.eq(Registration::getActivityId, request.getActivityId())
.in(Registration::getStatus, 1, 2)
);
if (existingRegistration != null) {
throw new BusinessException(ResultCode.ALREADY_REGISTERED);
}
List<Registration> myRegistrations = registrationMapper.selectList(
new LambdaQueryWrapper<Registration>()
.eq(Registration::getUserId, currentUser.getId())
.eq(Registration::getStatus, 1)
);
for (Registration reg : myRegistrations) {
Activity myActivity = activityMapper.selectById(reg.getActivityId());
if (isTimeConflict(activity, myActivity)) {
throw new BusinessException(ResultCode.ACTIVITY_TIME_CONFLICT);
}
}
Registration registration = new Registration();
registration.setUserId(currentUser.getId());
registration.setActivityId(request.getActivityId());
registration.setTicketCode(generateTicketCode());
registration.setStatus(1);
registrationMapper.insert(registration);
activity.setCurrentParticipants(activity.getCurrentParticipants() + 1);
activityMapper.updateById(activity);
return convertToVO(registration, activity);
}
@Override
@Transactional
public void cancelRegistration(Long id) {
User currentUser = authService.getCurrentUser();
Registration registration = registrationMapper.selectById(id);
if (registration == null) {
throw new BusinessException(ResultCode.REGISTRATION_NOT_FOUND);
}
if (!registration.getUserId().equals(currentUser.getId())) {
throw new BusinessException(ResultCode.FORBIDDEN);
}
if (registration.getStatus() != 1) {
throw new BusinessException(ResultCode.BAD_REQUEST.getCode(), "无法取消当前报名");
}
Activity activity = activityMapper.selectById(registration.getActivityId());
if (activity == null || activity.getDeleted() == 1) {
throw new BusinessException(ResultCode.ACTIVITY_NOT_FOUND);
}
if (LocalDateTime.now().isAfter(activity.getStartTime())) {
throw new BusinessException(ResultCode.ACTIVITY_ALREADY_STARTED);
}
registration.setStatus(0);
registration.setCanceledAt(LocalDateTime.now());
registrationMapper.updateById(registration);
activity.setCurrentParticipants(Math.max(0, activity.getCurrentParticipants() - 1));
activityMapper.updateById(activity);
}
@Override
public IPage<RegistrationVO> getMyRegistrations(Page<Registration> page, Integer status) {
User currentUser = authService.getCurrentUser();
return registrationMapper.selectMyRegistrations(page, currentUser.getId(), status);
}
@Override
public IPage<RegistrationVO> getActivityRegistrations(Page<Registration> page, Long activityId) {
return registrationMapper.selectActivityRegistrations(page, activityId);
}
@Override
public byte[] generateTicketPdf(Long id) {
User currentUser = authService.getCurrentUser();
Registration registration = registrationMapper.selectById(id);
if (registration == null) {
throw new BusinessException(ResultCode.REGISTRATION_NOT_FOUND);
}
if (!registration.getUserId().equals(currentUser.getId())) {
throw new BusinessException(ResultCode.FORBIDDEN);
}
if (registration.getStatus() != 1) {
throw new BusinessException(ResultCode.BAD_REQUEST.getCode(), "报名状态异常");
}
Activity activity = activityMapper.selectById(registration.getActivityId());
if (activity == null || activity.getDeleted() == 1) {
throw new BusinessException(ResultCode.ACTIVITY_NOT_FOUND);
}
return PdfUtil.generateTicketPdf(currentUser, activity, registration);
}
private String generateTicketCode() {
return "TK" + DateUtil.format(LocalDateTime.now(), "yyyyMMddHHmmss") +
String.format("%04d", (int)(Math.random() * 10000));
}
private boolean isTimeConflict(Activity activity1, Activity activity2) {
return !(activity1.getEndTime().isBefore(activity2.getStartTime()) ||
activity1.getStartTime().isAfter(activity2.getEndTime()));
}
private RegistrationVO convertToVO(Registration registration, Activity activity) {
RegistrationVO vo = new RegistrationVO();
BeanUtils.copyProperties(registration, vo);
vo.setActivityTitle(activity.getTitle());
vo.setActivityStartTime(activity.getStartTime());
vo.setActivityEndTime(activity.getEndTime());
vo.setActivityLocation(activity.getLocation());
return vo;
}
}

View File

@@ -0,0 +1,103 @@
package com.campus.activity.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.campus.activity.common.ResultCode;
import com.campus.activity.dto.request.ReviewRequest;
import com.campus.activity.entity.Activity;
import com.campus.activity.entity.CheckIn;
import com.campus.activity.entity.Review;
import com.campus.activity.entity.User;
import com.campus.activity.exception.BusinessException;
import com.campus.activity.mapper.ActivityMapper;
import com.campus.activity.mapper.CheckInMapper;
import com.campus.activity.mapper.ReviewMapper;
import com.campus.activity.service.AuthService;
import com.campus.activity.service.ReviewService;
import com.campus.activity.vo.ReviewVO;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class ReviewServiceImpl implements ReviewService {
private final ReviewMapper reviewMapper;
private final ActivityMapper activityMapper;
private final CheckInMapper checkInMapper;
private final AuthService authService;
@Override
@Transactional
public ReviewVO createReview(ReviewRequest request) {
User currentUser = authService.getCurrentUser();
Activity activity = activityMapper.selectById(request.getActivityId());
if (activity == null || activity.getDeleted() == 1) {
throw new BusinessException(ResultCode.ACTIVITY_NOT_FOUND);
}
Review existingReview = reviewMapper.selectOne(
new LambdaQueryWrapper<Review>()
.eq(Review::getUserId, currentUser.getId())
.eq(Review::getActivityId, request.getActivityId())
);
if (existingReview != null) {
throw new BusinessException(ResultCode.REVIEW_ALREADY_EXISTS);
}
CheckIn checkIn = checkInMapper.selectOne(
new LambdaQueryWrapper<CheckIn>()
.eq(CheckIn::getUserId, currentUser.getId())
.eq(CheckIn::getActivityId, request.getActivityId())
);
if (checkIn == null) {
throw new BusinessException(ResultCode.NOT_PARTICIPATED);
}
Review review = new Review();
review.setUserId(currentUser.getId());
review.setActivityId(request.getActivityId());
review.setRating(request.getRating());
review.setContent(request.getContent());
reviewMapper.insert(review);
return convertToVO(review, currentUser, activity);
}
@Override
public IPage<ReviewVO> getActivityReviews(Page<Review> page, Long activityId) {
return reviewMapper.selectActivityReviews(page, activityId)
.convert(review -> {
ReviewVO vo = new ReviewVO();
BeanUtils.copyProperties(review, vo);
return vo;
});
}
@Override
public IPage<ReviewVO> getMyReviews(Page<Review> page) {
User currentUser = authService.getCurrentUser();
return reviewMapper.selectMyReviews(page, currentUser.getId())
.convert(review -> {
ReviewVO vo = new ReviewVO();
BeanUtils.copyProperties(review, vo);
return vo;
});
}
private ReviewVO convertToVO(Review review, User user, Activity activity) {
ReviewVO vo = new ReviewVO();
BeanUtils.copyProperties(review, vo);
vo.setUserName(user.getName());
vo.setUserAvatar(user.getAvatar());
vo.setActivityTitle(activity.getTitle());
return vo;
}
}

View File

@@ -0,0 +1,182 @@
package com.campus.activity.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.campus.activity.common.ResultCode;
import com.campus.activity.entity.Activity;
import com.campus.activity.entity.CheckIn;
import com.campus.activity.entity.Registration;
import com.campus.activity.entity.Review;
import com.campus.activity.exception.BusinessException;
import com.campus.activity.mapper.ActivityMapper;
import com.campus.activity.mapper.CheckInMapper;
import com.campus.activity.mapper.RegistrationMapper;
import com.campus.activity.mapper.ReviewMapper;
import com.campus.activity.service.StatisticsService;
import com.campus.activity.util.ExcelUtil;
import com.campus.activity.vo.ActivityStatisticsVO;
import com.campus.activity.vo.OverviewStatisticsVO;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class StatisticsServiceImpl implements StatisticsService {
private final RegistrationMapper registrationMapper;
private final CheckInMapper checkInMapper;
private final ReviewMapper reviewMapper;
private final ActivityMapper activityMapper;
private final ExcelUtil excelUtil;
@Override
public ActivityStatisticsVO getActivityStatistics(Long activityId) {
Activity activity = activityMapper.selectById(activityId);
if (activity == null || activity.getDeleted() == 1) {
throw new BusinessException(ResultCode.ACTIVITY_NOT_FOUND);
}
Long registeredCount = registrationMapper.selectCount(
new LambdaQueryWrapper<Registration>()
.eq(Registration::getActivityId, activityId)
.in(Registration::getStatus, 1, 2)
);
List<CheckIn> checkIns = checkInMapper.selectList(
new LambdaQueryWrapper<CheckIn>()
.eq(CheckIn::getActivityId, activityId)
);
Long checkedInCount = (long) checkIns.size();
Long reviewCount = reviewMapper.selectCount(
new LambdaQueryWrapper<Review>()
.eq(Review::getActivityId, activityId)
);
List<Review> reviews = reviewMapper.selectList(
new LambdaQueryWrapper<Review>()
.eq(Review::getActivityId, activityId)
);
Double averageRating = reviews.isEmpty() ? 0.0 :
reviews.stream().mapToInt(Review::getRating).average().orElse(0.0);
Map<Integer, Long> ratingDistribution = reviews.stream()
.collect(Collectors.groupingBy(Review::getRating, Collectors.counting()));
for (int i = 1; i <= 5; i++) {
ratingDistribution.putIfAbsent(i, 0L);
}
Double checkInRate = registeredCount > 0 ?
(double) checkedInCount / registeredCount : 0.0;
return ActivityStatisticsVO.builder()
.activityId(activityId)
.activityTitle(activity.getTitle())
.registeredCount(registeredCount)
.checkedInCount(checkedInCount)
.checkInRate(Math.round(checkInRate * 100.0) / 100.0)
.reviewCount(reviewCount)
.averageRating(Math.round(averageRating * 10.0) / 10.0)
.ratingDistribution(ratingDistribution)
.build();
}
@Override
public ResponseEntity<byte[]> exportActivityData(Long activityId, String format) {
Activity activity = activityMapper.selectById(activityId);
if (activity == null || activity.getDeleted() == 1) {
throw new BusinessException(ResultCode.ACTIVITY_NOT_FOUND);
}
List<Registration> registrations = registrationMapper.selectList(
new LambdaQueryWrapper<Registration>()
.eq(Registration::getActivityId, activityId)
.eq(Registration::getStatus, 1)
);
byte[] excelBytes = excelUtil.exportActivityData(activity, registrations);
String filename = "activity_" + activityId + "_data.xlsx";
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename)
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.contentLength(excelBytes.length)
.body(excelBytes);
}
@Override
public OverviewStatisticsVO getOverviewStatistics() {
Long totalActivities = activityMapper.selectCount(
new LambdaQueryWrapper<Activity>()
.eq(Activity::getDeleted, 0)
);
Long totalRegistrations = registrationMapper.selectCount(
new LambdaQueryWrapper<Registration>()
.eq(Registration::getStatus, 1)
);
Long totalCheckIns = checkInMapper.selectCount(null);
Long totalReviews = reviewMapper.selectCount(null);
List<Review> allReviews = reviewMapper.selectList(null);
Double averageRating = allReviews.isEmpty() ? 0.0 :
allReviews.stream().mapToInt(Review::getRating).average().orElse(0.0);
List<OverviewStatisticsVO.MonthlyStats> monthlyStats = calculateMonthlyStats();
return OverviewStatisticsVO.builder()
.totalActivities(totalActivities)
.totalRegistrations(totalRegistrations)
.totalCheckIns(totalCheckIns)
.totalReviews(totalReviews)
.averageRating(Math.round(averageRating * 10.0) / 10.0)
.monthlyStats(monthlyStats)
.build();
}
private List<OverviewStatisticsVO.MonthlyStats> calculateMonthlyStats() {
List<OverviewStatisticsVO.MonthlyStats> stats = new ArrayList<>();
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM");
for (int i = 5; i >= 0; i--) {
LocalDateTime monthStart = now.minusMonths(i).withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0);
LocalDateTime monthEnd = monthStart.plusMonths(1).minusSeconds(1);
String monthKey = monthStart.format(formatter);
Long activityCount = activityMapper.selectCount(
new LambdaQueryWrapper<Activity>()
.eq(Activity::getDeleted, 0)
.between(Activity::getCreatedAt, monthStart, monthEnd)
);
Long registrationCount = registrationMapper.selectCount(
new LambdaQueryWrapper<Registration>()
.eq(Registration::getStatus, 1)
.between(Registration::getCreatedAt, monthStart, monthEnd)
);
stats.add(OverviewStatisticsVO.MonthlyStats.builder()
.month(monthKey)
.activityCount(activityCount)
.registrationCount(registrationCount)
.build());
}
return stats;
}
}

View File

@@ -0,0 +1,82 @@
package com.campus.activity.util;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.metadata.style.WriteFont;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
import com.campus.activity.entity.Activity;
import com.campus.activity.entity.Registration;
import com.campus.activity.entity.User;
import com.campus.activity.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.springframework.stereotype.Component;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.List;
@Component
@RequiredArgsConstructor
public class ExcelUtil {
private final UserMapper userMapper;
public byte[] exportActivityData(Activity activity, List<Registration> registrations) {
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
WriteCellStyle headWriteCellStyle = new WriteCellStyle();
WriteFont headWriteFont = new WriteFont();
headWriteFont.setFontHeightInPoints((short) 11);
headWriteFont.setBold(true);
headWriteCellStyle.setWriteFont(headWriteFont);
headWriteCellStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
HorizontalCellStyleStrategy horizontalCellStyleStrategy =
new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);
List<RegistrationExportData> dataList = new ArrayList<>();
for (Registration registration : registrations) {
User user = userMapper.selectById(registration.getUserId());
RegistrationExportData data = new RegistrationExportData();
data.setStudentId(user != null ? user.getStudentId() : "");
data.setName(user != null ? user.getName() : "");
data.setUsername(user != null ? user.getUsername() : "");
data.setEmail(user != null ? user.getEmail() : "");
data.setPhone(user != null ? user.getPhone() : "");
data.setTicketCode(registration.getTicketCode());
data.setRegistrationTime(registration.getCreatedAt());
data.setStatus(registration.getStatus() == 1 ? "已报名" :
registration.getStatus() == 2 ? "已签到" : "已取消");
dataList.add(data);
}
EasyExcel.write(outputStream)
.head(RegistrationExportData.class)
.registerWriteHandler(horizontalCellStyleStrategy)
.sheet("报名数据")
.doWrite(dataList);
return outputStream.toByteArray();
} catch (Exception e) {
throw new RuntimeException("导出Excel失败", e);
}
}
@lombok.Data
public static class RegistrationExportData {
private String studentId;
private String name;
private String username;
private String email;
private String phone;
private String ticketCode;
private java.time.LocalDateTime registrationTime;
private String status;
}
}

View File

@@ -0,0 +1,107 @@
package com.campus.activity.util;
import com.campus.activity.entity.Activity;
import com.campus.activity.entity.Registration;
import com.campus.activity.entity.User;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import com.itextpdf.io.image.ImageDataFactory;
import com.itextpdf.kernel.colors.ColorConstants;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Image;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.element.Table;
import com.itextpdf.layout.properties.TextAlignment;
import com.itextpdf.layout.properties.UnitValue;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class PdfUtil {
public static byte[] generateTicketPdf(User user, Activity activity, Registration registration) {
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
PdfWriter writer = new PdfWriter(outputStream);
PdfDocument pdf = new PdfDocument(writer);
Document document = new Document(pdf);
document.add(new Paragraph("活动电子票")
.setFontSize(24)
.setBold()
.setTextAlignment(TextAlignment.CENTER)
.setMarginBottom(20));
Table table = new Table(UnitValue.createPercentArray(new float[]{1, 2}))
.setMarginBottom(20);
table.addCell(createCell("活动名称:"));
table.addCell(createCell(activity.getTitle()));
table.addCell(createCell("学生姓名:"));
table.addCell(createCell(user.getName()));
table.addCell(createCell("学号:"));
table.addCell(createCell(user.getStudentId()));
table.addCell(createCell("活动时间:"));
table.addCell(createCell(activity.getStartTime() + " ~ " + activity.getEndTime()));
table.addCell(createCell("活动地点:"));
table.addCell(createCell(activity.getLocation()));
table.addCell(createCell("电子票号:"));
table.addCell(createCell(registration.getTicketCode()));
document.add(table);
byte[] qrCodeBytes = generateQrCode(registration.getTicketCode());
Image qrCodeImage = new Image(ImageDataFactory.create(qrCodeBytes))
.setWidth(150)
.setHeight(150)
.setMarginTop(20)
.setTextAlignment(TextAlignment.CENTER);
document.add(qrCodeImage);
document.add(new Paragraph("请妥善保管此电子票,活动当天凭此票签到")
.setFontSize(10)
.setTextAlignment(TextAlignment.CENTER)
.setMarginTop(20));
document.close();
return outputStream.toByteArray();
} catch (Exception e) {
throw new RuntimeException("生成电子票失败", e);
}
}
private static Paragraph createCell(String text) {
return new Paragraph(text)
.setFontSize(12)
.setPadding(5);
}
private static byte[] generateQrCode(String content) throws IOException {
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
QRCodeWriter qrCodeWriter = new QRCodeWriter();
Map<EncodeHintType, Object> hints = new HashMap<>();
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
BitMatrix bitMatrix = qrCodeWriter.encode(content, BarcodeFormat.QR_CODE, 300, 300, hints);
MatrixToImageWriter.writeToStream(bitMatrix, "PNG", outputStream);
return outputStream.toByteArray();
} catch (com.google.zxing.WriterException e) {
throw new RuntimeException("生成二维码失败", e);
}
}
}

View File

@@ -0,0 +1,48 @@
package com.campus.activity.util;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class QrCodeUtil {
public static String generateQrCodeContent(Long activityId) {
long timestamp = System.currentTimeMillis();
return "CHECKIN:" + activityId + ":" + timestamp;
}
public static byte[] generateQrCodeImage(String content) throws IOException {
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
QRCodeWriter qrCodeWriter = new QRCodeWriter();
Map<EncodeHintType, Object> hints = new HashMap<>();
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
BitMatrix bitMatrix = qrCodeWriter.encode(content, BarcodeFormat.QR_CODE, 300, 300, hints);
MatrixToImageWriter.writeToStream(bitMatrix, "PNG", outputStream);
return outputStream.toByteArray();
} catch (com.google.zxing.WriterException e) {
throw new RuntimeException("生成二维码失败", e);
}
}
public static Long parseActivityIdFromQrCode(String qrCodeContent) {
try {
String[] parts = qrCodeContent.split(":");
if (parts.length >= 2 && "CHECKIN".equals(parts[0])) {
return Long.parseLong(parts[1]);
}
} catch (Exception e) {
return null;
}
return null;
}
}

View File

@@ -0,0 +1,24 @@
package com.campus.activity.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ActivityStatisticsVO {
private Long activityId;
private String activityTitle;
private Long registeredCount;
private Long checkedInCount;
private Double checkInRate;
private Long reviewCount;
private Double averageRating;
private Map<Integer, Long> ratingDistribution;
}

View File

@@ -0,0 +1,34 @@
package com.campus.activity.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ActivityVO {
private Long id;
private String title;
private String description;
private String coverImage;
private LocalDateTime startTime;
private LocalDateTime endTime;
private LocalDateTime registrationDeadline;
private String location;
private Integer maxParticipants;
private Integer currentParticipants;
private Integer status;
private String category;
private Long adminId;
private String adminName;
private Double averageRating;
private Long reviewCount;
private Boolean isRegistered;
private LocalDateTime createdAt;
}

View File

@@ -0,0 +1,23 @@
package com.campus.activity.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CheckInVO {
private Long id;
private Long userId;
private String userName;
private String studentId;
private Long activityId;
private LocalDateTime checkInTime;
private Integer checkInMethod;
}

View File

@@ -0,0 +1,30 @@
package com.campus.activity.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ConflictCheckVO {
private Boolean hasConflict;
private List<ConflictActivity> conflictActivities;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class ConflictActivity {
private Long id;
private String title;
private LocalDateTime startTime;
private LocalDateTime endTime;
}
}

View File

@@ -0,0 +1,32 @@
package com.campus.activity.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OverviewStatisticsVO {
private Long totalActivities;
private Long totalRegistrations;
private Long totalCheckIns;
private Long totalReviews;
private Double averageRating;
private List<MonthlyStats> monthlyStats;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class MonthlyStats {
private String month;
private Long activityCount;
private Long registrationCount;
}
}

View File

@@ -0,0 +1,19 @@
package com.campus.activity.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class QrCodeVO {
private String qrCodeUrl;
private String qrCodeContent;
private LocalDateTime expiresAt;
}

View File

@@ -0,0 +1,27 @@
package com.campus.activity.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RegistrationVO {
private Long id;
private Long activityId;
private String activityTitle;
private LocalDateTime activityStartTime;
private LocalDateTime activityEndTime;
private String activityLocation;
private String ticketCode;
private String ticketPdfUrl;
private Integer status;
private LocalDateTime createdAt;
private LocalDateTime canceledAt;
}

View File

@@ -0,0 +1,25 @@
package com.campus.activity.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ReviewVO {
private Long id;
private Long userId;
private String userName;
private String userAvatar;
private Long activityId;
private String activityTitle;
private Integer rating;
private String content;
private LocalDateTime createdAt;
}

View File

@@ -0,0 +1,68 @@
server:
port: 8080
spring:
application:
name: campus-activity-system
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/campus_activity?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: ${DB_PASSWORD:root}
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: Asia/Shanghai
serialization:
write-dates-as-timestamps: false
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
mybatis-plus:
mapper-locations: classpath:/mapper/**/*.xml
type-aliases-package: com.campus.activity.entity
global-config:
db-config:
id-type: auto
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
table-prefix:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
jwt:
secret: ${JWT_SECRET:campus-activity-system-secret-key-for-jwt-token-generation-2025}
expiration: 7200000
refresh-expiration: 604800000
header: Authorization
prefix: Bearer
knife4j:
enable: true
openapi:
title: 校园活动组织与报名系统 API
description: 校园活动组织与报名系统后端接口文档
version: 1.0.0
concat: campus-activity-team
email: campus@example.com
license: Apache 2.0
license-url: https://www.apache.org/licenses/LICENSE-2.0.html
group:
default:
group-name: default
api-rule: package
api-rule-resources:
- com.campus.activity.controller
logging:
level:
com.campus.activity.mapper: debug
org.springframework.security: debug
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.campus.activity.mapper.ActivityMapper">
<select id="selectActivityPage" resultType="com.campus.activity.entity.Activity">
SELECT a.*,
(SELECT AVG(rating) FROM review r WHERE r.activity_id = a.id) as averageRating
FROM activity a
<where>
a.deleted = 0
<if test="status != null">
AND a.status = #{status}
</if>
<if test="keyword != null and keyword != ''">
AND (a.title LIKE CONCAT('%', #{keyword}, '%')
OR a.description LIKE CONCAT('%', #{keyword}, '%'))
</if>
<if test="category != null and category != ''">
AND a.category = #{category}
</if>
<if test="startDate != null">
AND a.start_time >= #{startDate}
</if>
<if test="endDate != null">
AND a.end_time &lt;= #{endDate}
</if>
</where>
ORDER BY a.start_time DESC
</select>
<select id="selectCalendarActivities" resultType="com.campus.activity.entity.Activity">
SELECT *
FROM activity
WHERE deleted = 0
AND YEAR(start_time) = #{year}
AND MONTH(start_time) = #{month}
ORDER BY start_time ASC
</select>
<select id="selectConflictActivities" resultType="com.campus.activity.entity.Activity">
SELECT *
FROM activity
WHERE deleted = 0
AND id != #{excludeActivityId}
AND status != 3
AND (
(start_time &lt;= #{endTime} AND end_time >= #{startTime})
OR (start_time &lt;= #{startTime} AND end_time >= #{startTime})
OR (start_time >= #{startTime} AND end_time &lt;= #{endTime})
)
</select>
</mapper>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.campus.activity.mapper.CheckInMapper">
<select id="selectActivityCheckIns" resultType="com.campus.activity.vo.CheckInVO">
SELECT c.id,
c.user_id as userId,
c.activity_id as activityId,
c.check_in_time as checkInTime,
c.check_in_method as checkInMethod,
u.name as userName,
u.student_id as studentId
FROM check_in c
LEFT JOIN user u ON c.user_id = u.id
WHERE c.activity_id = #{activityId}
ORDER BY c.check_in_time ASC
</select>
</mapper>

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.campus.activity.mapper.RegistrationMapper">
<select id="selectMyRegistrations" resultType="com.campus.activity.vo.RegistrationVO">
SELECT r.id,
r.activity_id as activityId,
r.ticket_code as ticketCode,
r.ticket_pdf_url as ticketPdfUrl,
r.status,
r.created_at as createdAt,
r.canceled_at as canceledAt,
a.title as activityTitle,
a.start_time as activityStartTime,
a.end_time as activityEndTime,
a.location as activityLocation
FROM registration r
LEFT JOIN activity a ON r.activity_id = a.id
WHERE r.user_id = #{userId}
<if test="status != null">
AND r.status = #{status}
</if>
ORDER BY r.created_at DESC
</select>
<select id="selectActivityRegistrations" resultType="com.campus.activity.vo.RegistrationVO">
SELECT r.id,
r.activity_id as activityId,
r.ticket_code as ticketCode,
r.ticket_pdf_url as ticketPdfUrl,
r.status,
r.created_at as createdAt,
r.canceled_at as canceledAt,
a.title as activityTitle,
a.start_time as activityStartTime,
a.end_time as activityEndTime,
a.location as activityLocation
FROM registration r
LEFT JOIN activity a ON r.activity_id = a.id
WHERE r.activity_id = #{activityId}
ORDER BY r.created_at DESC
</select>
<select id="selectByTicketCode" resultType="com.campus.activity.entity.Registration">
SELECT *
FROM registration
WHERE ticket_code = #{ticketCode}
AND status = 1
</select>
</mapper>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.campus.activity.mapper.ReviewMapper">
<select id="selectActivityReviews" resultType="com.campus.activity.entity.Review">
SELECT r.*,
u.username,
u.name as userName,
u.avatar as userAvatar
FROM review r
LEFT JOIN user u ON r.user_id = u.id
WHERE r.activity_id = #{activityId}
ORDER BY r.created_at DESC
</select>
<select id="selectMyReviews" resultType="com.campus.activity.entity.Review">
SELECT r.*,
a.title as activityTitle
FROM review r
LEFT JOIN activity a ON r.activity_id = a.id
WHERE r.user_id = #{userId}
ORDER BY r.created_at DESC
</select>
</mapper>

View File

@@ -0,0 +1,68 @@
server:
port: 8080
spring:
application:
name: campus-activity-system
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/campus_activity?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: ${DB_PASSWORD:root}
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: Asia/Shanghai
serialization:
write-dates-as-timestamps: false
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
mybatis-plus:
mapper-locations: classpath:/mapper/**/*.xml
type-aliases-package: com.campus.activity.entity
global-config:
db-config:
id-type: auto
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
table-prefix:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
jwt:
secret: ${JWT_SECRET:campus-activity-system-secret-key-for-jwt-token-generation-2025}
expiration: 7200000
refresh-expiration: 604800000
header: Authorization
prefix: Bearer
knife4j:
enable: true
openapi:
title: 校园活动组织与报名系统 API
description: 校园活动组织与报名系统后端接口文档
version: 1.0.0
concat: campus-activity-team
email: campus@example.com
license: Apache 2.0
license-url: https://www.apache.org/licenses/LICENSE-2.0.html
group:
default:
group-name: default
api-rule: package
api-rule-resources:
- com.campus.activity.controller
logging:
level:
com.campus.activity.mapper: debug
org.springframework.security: debug
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.campus.activity.mapper.ActivityMapper">
<select id="selectActivityPage" resultType="com.campus.activity.entity.Activity">
SELECT a.*,
(SELECT AVG(rating) FROM review r WHERE r.activity_id = a.id) as averageRating
FROM activity a
<where>
a.deleted = 0
<if test="status != null">
AND a.status = #{status}
</if>
<if test="keyword != null and keyword != ''">
AND (a.title LIKE CONCAT('%', #{keyword}, '%')
OR a.description LIKE CONCAT('%', #{keyword}, '%'))
</if>
<if test="category != null and category != ''">
AND a.category = #{category}
</if>
<if test="startDate != null">
AND a.start_time >= #{startDate}
</if>
<if test="endDate != null">
AND a.end_time &lt;= #{endDate}
</if>
</where>
ORDER BY a.start_time DESC
</select>
<select id="selectCalendarActivities" resultType="com.campus.activity.entity.Activity">
SELECT *
FROM activity
WHERE deleted = 0
AND YEAR(start_time) = #{year}
AND MONTH(start_time) = #{month}
ORDER BY start_time ASC
</select>
<select id="selectConflictActivities" resultType="com.campus.activity.entity.Activity">
SELECT *
FROM activity
WHERE deleted = 0
AND id != #{excludeActivityId}
AND status != 3
AND (
(start_time &lt;= #{endTime} AND end_time >= #{startTime})
OR (start_time &lt;= #{startTime} AND end_time >= #{startTime})
OR (start_time >= #{startTime} AND end_time &lt;= #{endTime})
)
</select>
</mapper>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.campus.activity.mapper.CheckInMapper">
<select id="selectActivityCheckIns" resultType="com.campus.activity.vo.CheckInVO">
SELECT c.id,
c.user_id as userId,
c.activity_id as activityId,
c.check_in_time as checkInTime,
c.check_in_method as checkInMethod,
u.name as userName,
u.student_id as studentId
FROM check_in c
LEFT JOIN user u ON c.user_id = u.id
WHERE c.activity_id = #{activityId}
ORDER BY c.check_in_time ASC
</select>
</mapper>

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.campus.activity.mapper.RegistrationMapper">
<select id="selectMyRegistrations" resultType="com.campus.activity.vo.RegistrationVO">
SELECT r.id,
r.activity_id as activityId,
r.ticket_code as ticketCode,
r.ticket_pdf_url as ticketPdfUrl,
r.status,
r.created_at as createdAt,
r.canceled_at as canceledAt,
a.title as activityTitle,
a.start_time as activityStartTime,
a.end_time as activityEndTime,
a.location as activityLocation
FROM registration r
LEFT JOIN activity a ON r.activity_id = a.id
WHERE r.user_id = #{userId}
<if test="status != null">
AND r.status = #{status}
</if>
ORDER BY r.created_at DESC
</select>
<select id="selectActivityRegistrations" resultType="com.campus.activity.vo.RegistrationVO">
SELECT r.id,
r.activity_id as activityId,
r.ticket_code as ticketCode,
r.ticket_pdf_url as ticketPdfUrl,
r.status,
r.created_at as createdAt,
r.canceled_at as canceledAt,
a.title as activityTitle,
a.start_time as activityStartTime,
a.end_time as activityEndTime,
a.location as activityLocation
FROM registration r
LEFT JOIN activity a ON r.activity_id = a.id
WHERE r.activity_id = #{activityId}
ORDER BY r.created_at DESC
</select>
<select id="selectByTicketCode" resultType="com.campus.activity.entity.Registration">
SELECT *
FROM registration
WHERE ticket_code = #{ticketCode}
AND status = 1
</select>
</mapper>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.campus.activity.mapper.ReviewMapper">
<select id="selectActivityReviews" resultType="com.campus.activity.entity.Review">
SELECT r.*,
u.username,
u.name as userName,
u.avatar as userAvatar
FROM review r
LEFT JOIN user u ON r.user_id = u.id
WHERE r.activity_id = #{activityId}
ORDER BY r.created_at DESC
</select>
<select id="selectMyReviews" resultType="com.campus.activity.entity.Review">
SELECT r.*,
a.title as activityTitle
FROM review r
LEFT JOIN activity a ON r.activity_id = a.id
WHERE r.user_id = #{userId}
ORDER BY r.created_at DESC
</select>
</mapper>

View File

@@ -0,0 +1,84 @@
com\campus\activity\service\CheckInService.class
com\campus\activity\mapper\RegistrationMapper.class
com\campus\activity\service\impl\StatisticsServiceImpl.class
com\campus\activity\vo\ConflictCheckVO$ConflictCheckVOBuilder.class
com\campus\activity\dto\request\CheckConflictRequest$CheckConflictRequestBuilder.class
com\campus\activity\util\QrCodeUtil.class
com\campus\activity\vo\OverviewStatisticsVO$MonthlyStats$MonthlyStatsBuilder.class
com\campus\activity\exception\GlobalExceptionHandler.class
com\campus\activity\vo\ActivityVO$ActivityVOBuilder.class
com\campus\activity\vo\ReviewVO$ReviewVOBuilder.class
com\campus\activity\mapper\ReviewMapper.class
com\campus\activity\vo\QrCodeVO.class
com\campus\activity\config\MyMetaObjectHandler.class
com\campus\activity\security\JwtTokenProvider.class
com\campus\activity\vo\ConflictCheckVO$ConflictActivity$ConflictActivityBuilder.class
com\campus\activity\dto\request\ChangePasswordRequest.class
com\campus\activity\dto\request\RegistrationRequest.class
com\campus\activity\dto\request\ActivityCreateRequest.class
com\campus\activity\dto\response\RefreshTokenResponse$RefreshTokenResponseBuilder.class
com\campus\activity\service\RegistrationService.class
com\campus\activity\dto\response\RefreshTokenResponse.class
com\campus\activity\vo\ActivityStatisticsVO.class
com\campus\activity\vo\CheckInVO$CheckInVOBuilder.class
com\campus\activity\entity\User.class
com\campus\activity\service\impl\ReviewServiceImpl.class
com\campus\activity\config\MybatisPlusConfig.class
com\campus\activity\vo\ConflictCheckVO$ConflictActivity.class
com\campus\activity\vo\ConflictCheckVO.class
com\campus\activity\controller\CheckInController.class
com\campus\activity\util\ExcelUtil$RegistrationExportData.class
com\campus\activity\service\impl\ActivityServiceImpl.class
com\campus\activity\dto\request\RegisterRequest.class
com\campus\activity\util\ExcelUtil.class
com\campus\activity\dto\request\ScanCheckInRequest.class
com\campus\activity\CampusActivityApplication.class
com\campus\activity\service\impl\CheckInServiceImpl.class
com\campus\activity\vo\QrCodeVO$QrCodeVOBuilder.class
com\campus\activity\service\ActivityService.class
com\campus\activity\vo\ActivityStatisticsVO$ActivityStatisticsVOBuilder.class
com\campus\activity\vo\ActivityVO.class
com\campus\activity\dto\request\TicketCheckInRequest.class
com\campus\activity\config\SecurityConfig.class
com\campus\activity\dto\response\LoginResponse.class
com\campus\activity\security\UserDetailsServiceImpl.class
com\campus\activity\dto\request\LoginRequest.class
com\campus\activity\vo\OverviewStatisticsVO.class
com\campus\activity\common\Result.class
com\campus\activity\mapper\CheckInMapper.class
com\campus\activity\common\ResultCode.class
com\campus\activity\util\PdfUtil.class
com\campus\activity\dto\request\RefreshTokenRequest.class
com\campus\activity\config\CorsConfig.class
com\campus\activity\mapper\UserMapper.class
com\campus\activity\dto\request\ActivityUpdateRequest.class
com\campus\activity\dto\response\LoginResponse$UserInfo.class
com\campus\activity\dto\response\LoginResponse$UserInfo$UserInfoBuilder.class
com\campus\activity\entity\CheckIn.class
com\campus\activity\service\impl\RegistrationServiceImpl.class
com\campus\activity\controller\ActivityController.class
com\campus\activity\vo\RegistrationVO.class
com\campus\activity\entity\Activity.class
com\campus\activity\common\PageResult.class
com\campus\activity\security\JwtAuthenticationFilter.class
com\campus\activity\vo\CheckInVO.class
com\campus\activity\dto\request\CheckConflictRequest.class
com\campus\activity\dto\response\LoginResponse$LoginResponseBuilder.class
com\campus\activity\exception\BusinessException.class
com\campus\activity\service\AuthService.class
com\campus\activity\vo\OverviewStatisticsVO$OverviewStatisticsVOBuilder.class
com\campus\activity\controller\RegistrationController.class
com\campus\activity\service\StatisticsService.class
com\campus\activity\mapper\ActivityMapper.class
com\campus\activity\controller\AuthController.class
com\campus\activity\vo\ReviewVO.class
com\campus\activity\entity\Registration.class
com\campus\activity\service\impl\AuthServiceImpl.class
com\campus\activity\dto\request\ReviewRequest.class
com\campus\activity\entity\Review.class
com\campus\activity\service\ReviewService.class
com\campus\activity\controller\ReviewController.class
com\campus\activity\controller\StatisticsController.class
com\campus\activity\vo\RegistrationVO$RegistrationVOBuilder.class
com\campus\activity\config\Knife4jConfig.class
com\campus\activity\vo\OverviewStatisticsVO$MonthlyStats.class

View File

@@ -0,0 +1,66 @@
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\common\PageResult.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\controller\RegistrationController.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\vo\OverviewStatisticsVO.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\service\impl\ReviewServiceImpl.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\dto\request\RefreshTokenRequest.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\config\Knife4jConfig.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\security\UserDetailsServiceImpl.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\entity\User.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\config\MyMetaObjectHandler.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\dto\request\ActivityCreateRequest.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\CampusActivityApplication.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\mapper\ReviewMapper.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\util\PdfUtil.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\service\CheckInService.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\vo\ReviewVO.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\controller\AuthController.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\vo\CheckInVO.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\common\ResultCode.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\config\MybatisPlusConfig.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\security\JwtAuthenticationFilter.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\vo\QrCodeVO.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\util\ExcelUtil.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\controller\StatisticsController.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\dto\request\ChangePasswordRequest.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\dto\request\CheckConflictRequest.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\config\CorsConfig.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\mapper\CheckInMapper.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\vo\ActivityStatisticsVO.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\vo\ActivityVO.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\mapper\RegistrationMapper.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\dto\request\ActivityUpdateRequest.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\service\impl\CheckInServiceImpl.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\entity\Review.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\service\impl\AuthServiceImpl.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\dto\request\RegistrationRequest.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\service\impl\StatisticsServiceImpl.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\util\QrCodeUtil.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\vo\ConflictCheckVO.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\controller\ActivityController.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\entity\CheckIn.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\service\ActivityService.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\service\AuthService.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\entity\Registration.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\dto\request\RegisterRequest.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\entity\Activity.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\exception\GlobalExceptionHandler.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\dto\request\LoginRequest.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\security\JwtTokenProvider.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\dto\response\RefreshTokenResponse.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\controller\ReviewController.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\mapper\ActivityMapper.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\dto\response\LoginResponse.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\service\RegistrationService.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\service\impl\RegistrationServiceImpl.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\common\Result.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\dto\request\ReviewRequest.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\vo\RegistrationVO.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\config\SecurityConfig.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\service\StatisticsService.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\mapper\UserMapper.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\dto\request\TicketCheckInRequest.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\service\impl\ActivityServiceImpl.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\controller\CheckInController.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\service\ReviewService.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\exception\BusinessException.java
C:\Users\shiro\Desktop\campus-activity-system\src\main\java\com\campus\activity\dto\request\ScanCheckInRequest.java