upload: 上传java后端成果物
This commit is contained in:
8
server/.idea/.gitignore
generated
vendored
Normal file
8
server/.idea/.gitignore
generated
vendored
Normal 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
18
server/.idea/compiler.xml
generated
Normal 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
6
server/.idea/encodings.xml
generated
Normal 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
20
server/.idea/jarRepositories.xml
generated
Normal 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
12
server/.idea/misc.xml
generated
Normal 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
6
server/.idea/vcs.xml
generated
Normal 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
156
server/README.md
Normal 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
|
||||
1208
server/docs/API接口设计.md
Normal file
1208
server/docs/API接口设计.md
Normal file
File diff suppressed because it is too large
Load Diff
110
server/docs/init.sql
Normal file
110
server/docs/init.sql
Normal 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='评价表';
|
||||
|
||||
-- 插入测试管理员账号(密码:admin123,BCrypt加密)
|
||||
INSERT INTO `user` (`username`, `password`, `name`, `role`) VALUES
|
||||
('admin', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iKXP6X3SqmI8Q0WJLoUbWAHZJ.5i', '系统管理员', 1);
|
||||
105
server/docs/sample_data.sql
Normal file
105
server/docs/sample_data.sql
Normal 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;
|
||||
1343
server/docs/前端开发规范.md
Normal file
1343
server/docs/前端开发规范.md
Normal file
File diff suppressed because it is too large
Load Diff
823
server/docs/前端开发计划.md
Normal file
823
server/docs/前端开发计划.md
Normal 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. 良好的移动端适配方案
|
||||
|
||||
**下一步**:
|
||||
- 制定详细的开发规范文档
|
||||
- 分解具体开发任务清单
|
||||
317
server/docs/后端开发规范.md
Normal file
317
server/docs/后端开发规范.md
Normal 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
|
||||
```
|
||||
300
server/docs/开发任务清单.md
Normal file
300
server/docs/开发任务清单.md
Normal 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 | ✅ 完成 | 本文档 |
|
||||
393
server/docs/数据库设计.md
Normal file
393
server/docs/数据库设计.md
Normal 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='评价表';
|
||||
|
||||
-- 插入测试管理员账号(密码:admin123,BCrypt加密)
|
||||
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
107
server/docs/需求文档.md
Normal 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
164
server/pom.xml
Normal 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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
59
server/src/main/java/com/campus/activity/common/Result.java
Normal file
59
server/src/main/java/com/campus/activity/common/Result.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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")));
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
27
server/src/main/java/com/campus/activity/entity/CheckIn.java
Normal file
27
server/src/main/java/com/campus/activity/entity/CheckIn.java
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
31
server/src/main/java/com/campus/activity/entity/Review.java
Normal file
31
server/src/main/java/com/campus/activity/entity/Review.java
Normal 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;
|
||||
}
|
||||
44
server/src/main/java/com/campus/activity/entity/User.java
Normal file
44
server/src/main/java/com/campus/activity/entity/User.java
Normal 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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
82
server/src/main/java/com/campus/activity/util/ExcelUtil.java
Normal file
82
server/src/main/java/com/campus/activity/util/ExcelUtil.java
Normal 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;
|
||||
}
|
||||
}
|
||||
107
server/src/main/java/com/campus/activity/util/PdfUtil.java
Normal file
107
server/src/main/java/com/campus/activity/util/PdfUtil.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
34
server/src/main/java/com/campus/activity/vo/ActivityVO.java
Normal file
34
server/src/main/java/com/campus/activity/vo/ActivityVO.java
Normal 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;
|
||||
}
|
||||
23
server/src/main/java/com/campus/activity/vo/CheckInVO.java
Normal file
23
server/src/main/java/com/campus/activity/vo/CheckInVO.java
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
19
server/src/main/java/com/campus/activity/vo/QrCodeVO.java
Normal file
19
server/src/main/java/com/campus/activity/vo/QrCodeVO.java
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
25
server/src/main/java/com/campus/activity/vo/ReviewVO.java
Normal file
25
server/src/main/java/com/campus/activity/vo/ReviewVO.java
Normal 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;
|
||||
}
|
||||
68
server/src/main/resources/application.yml
Normal file
68
server/src/main/resources/application.yml
Normal 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"
|
||||
53
server/src/main/resources/mapper/ActivityMapper.xml
Normal file
53
server/src/main/resources/mapper/ActivityMapper.xml
Normal 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 <= #{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 <= #{endTime} AND end_time >= #{startTime})
|
||||
OR (start_time <= #{startTime} AND end_time >= #{startTime})
|
||||
OR (start_time >= #{startTime} AND end_time <= #{endTime})
|
||||
)
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
19
server/src/main/resources/mapper/CheckInMapper.xml
Normal file
19
server/src/main/resources/mapper/CheckInMapper.xml
Normal 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>
|
||||
51
server/src/main/resources/mapper/RegistrationMapper.xml
Normal file
51
server/src/main/resources/mapper/RegistrationMapper.xml
Normal 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>
|
||||
25
server/src/main/resources/mapper/ReviewMapper.xml
Normal file
25
server/src/main/resources/mapper/ReviewMapper.xml
Normal 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>
|
||||
68
server/target/classes/application.yml
Normal file
68
server/target/classes/application.yml
Normal 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"
|
||||
53
server/target/classes/mapper/ActivityMapper.xml
Normal file
53
server/target/classes/mapper/ActivityMapper.xml
Normal 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 <= #{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 <= #{endTime} AND end_time >= #{startTime})
|
||||
OR (start_time <= #{startTime} AND end_time >= #{startTime})
|
||||
OR (start_time >= #{startTime} AND end_time <= #{endTime})
|
||||
)
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
19
server/target/classes/mapper/CheckInMapper.xml
Normal file
19
server/target/classes/mapper/CheckInMapper.xml
Normal 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>
|
||||
51
server/target/classes/mapper/RegistrationMapper.xml
Normal file
51
server/target/classes/mapper/RegistrationMapper.xml
Normal 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>
|
||||
25
server/target/classes/mapper/ReviewMapper.xml
Normal file
25
server/target/classes/mapper/ReviewMapper.xml
Normal 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>
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user