diff --git a/server/.idea/.gitignore b/server/.idea/.gitignore new file mode 100644 index 0000000..35410ca --- /dev/null +++ b/server/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/server/.idea/compiler.xml b/server/.idea/compiler.xml new file mode 100644 index 0000000..f5e9b39 --- /dev/null +++ b/server/.idea/compiler.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/server/.idea/encodings.xml b/server/.idea/encodings.xml new file mode 100644 index 0000000..63e9001 --- /dev/null +++ b/server/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/server/.idea/jarRepositories.xml b/server/.idea/jarRepositories.xml new file mode 100644 index 0000000..712ab9d --- /dev/null +++ b/server/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/server/.idea/misc.xml b/server/.idea/misc.xml new file mode 100644 index 0000000..76bd8cd --- /dev/null +++ b/server/.idea/misc.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/server/.idea/vcs.xml b/server/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/server/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..bf2e3fd --- /dev/null +++ b/server/README.md @@ -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 \ No newline at end of file diff --git a/server/docs/API接口设计.md b/server/docs/API接口设计.md new file mode 100644 index 0000000..b14ff08 --- /dev/null +++ b/server/docs/API接口设计.md @@ -0,0 +1,1208 @@ +# 校园活动系统 - 后端 API 接口文档 + +## 基本信息 + +- **基础路径**: `/api/v1` +- **认证方式**: JWT Token (Bearer Token) +- **响应格式**: JSON + +## 统一响应格式 + +所有接口返回统一的响应格式: + +```json +{ + "code": 200, + "message": "success", + "data": {}, + "timestamp": 1736620800000 +} +``` + +### 状态码说明 + +| 状态码 | 说明 | +|--------|------| +| 200 | 成功 | +| 400 | 请求参数错误 | +| 401 | 未认证或Token已过期 | +| 403 | 无权限访问 | +| 404 | 资源不存在 | +| 409 | 业务冲突 | +| 500 | 服务器内部错误 | + +### 业务错误码 + +| 错误码 | 说明 | +|--------|------| +| 1001 | 用户不存在 | +| 1002 | 用户已存在 | +| 1003 | 密码错误 | +| 1004 | 用户名或密码错误 | +| 1005 | 学号已存在 | +| 2001 | 活动不存在 | +| 2002 | 活动已开始,无法取消报名 | +| 2003 | 活动时间冲突 | +| 2004 | 活动报名人数已满 | +| 3001 | 报名记录不存在 | +| 3002 | 您已报名该活动 | +| 3003 | 您未报名该活动 | +| 4001 | 签到失败 | +| 4002 | 已签到 | +| 4003 | 签到时间已过期 | +| 5001 | 您已评价该活动 | +| 5002 | 评价记录不存在 | +| 5003 | 您未参加该活动,无法评价 | + +--- + +## 一、认证模块 (`/api/v1/auth`) + +### 1.1 用户注册 + +**接口地址**: `POST /api/v1/auth/register` + +**请求权限**: 无需认证 + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | 验证规则 | +|--------|------|------|------|----------| +| username | String | 是 | 用户名 | 3-50个字符 | +| password | String | 是 | 密码 | 6-20个字符 | +| name | String | 是 | 姓名 | 最多50个字符 | +| studentId | String | 否 | 学号 | 10-20位数字 | +| email | String | 否 | 邮箱 | 邮箱格式 | +| phone | String | 否 | 手机号 | 11位手机号 | + +**请求示例**: +```json +{ + "username": "student001", + "password": "123456", + "name": "张三", + "studentId": "2021000001", + "email": "zhangsan@example.com", + "phone": "13800138000" +} +``` + +**响应示例**: +```json +{ + "code": 200, + "message": "注册成功", + "data": null, + "timestamp": 1736620800000 +} +``` + +--- + +### 1.2 用户登录 + +**接口地址**: `POST /api/v1/auth/login` + +**请求权限**: 无需认证 + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| username | String | 是 | 用户名 | +| password | String | 是 | 密码 | + +**请求示例**: +```json +{ + "username": "student001", + "password": "123456" +} +``` + +**响应示例**: +```json +{ + "code": 200, + "message": "登录成功", + "data": { + "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "expiresIn": 3600, + "tokenType": "Bearer", + "userInfo": { + "id": 1, + "username": "student001", + "name": "张三", + "role": 0, + "avatar": null + } + }, + "timestamp": 1736620800000 +} +``` + +--- + +### 1.3 刷新Token + +**接口地址**: `POST /api/v1/auth/refresh` + +**请求权限**: 无需认证 + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| refreshToken | String | 是 | 刷新Token | + +**请求示例**: +```json +{ + "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +} +``` + +**响应示例**: +```json +{ + "code": 200, + "message": "success", + "data": { + "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "expiresIn": 3600 + }, + "timestamp": 1736620800000 +} +``` + +--- + +### 1.4 获取当前用户信息 + +**接口地址**: `GET /api/v1/auth/me` + +**请求权限**: 需要认证 + +**请求头**: +``` +Authorization: Bearer {accessToken} +``` + +**响应示例**: +```json +{ + "code": 200, + "message": "success", + "data": { + "id": 1, + "username": "student001", + "password": "$2a$10$...", + "name": "张三", + "studentId": "2021000001", + "email": "zhangsan@example.com", + "phone": "13800138000", + "avatar": null, + "role": 0, + "status": 1, + "createdAt": "2026-01-01T00:00:00", + "updatedAt": "2026-01-01T00:00:00" + }, + "timestamp": 1736620800000 +} +``` + +--- + +### 1.5 修改密码 + +**接口地址**: `PUT /api/v1/auth/password` + +**请求权限**: 需要认证 + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | 验证规则 | +|--------|------|------|------|----------| +| oldPassword | String | 是 | 旧密码 | 不能为空 | +| newPassword | String | 是 | 新密码 | 6-20个字符 | + +**请求示例**: +```json +{ + "oldPassword": "123456", + "newPassword": "654321" +} +``` + +**响应示例**: +```json +{ + "code": 200, + "message": "密码修改成功", + "data": null, + "timestamp": 1736620800000 +} +``` + +--- + +## 二、活动模块 (`/api/v1/activities`) + +### 2.1 获取活动列表 + +**接口地址**: `GET /api/v1/activities` + +**请求权限**: 无需认证 + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | 默认值 | +|--------|------|------|------|--------| +| current | Long | 否 | 当前页码 | 1 | +| size | Long | 否 | 每页数量 | 10 | +| status | Integer | 否 | 活动状态 | - | +| keyword | String | 否 | 关键词搜索 | - | +| category | String | 否 | 活动分类 | - | +| startDate | LocalDateTime | 否 | 开始时间(yyyy-MM-dd HH:mm:ss) | - | +| endDate | LocalDateTime | 否 | 结束时间(yyyy-MM-dd HH:mm:ss) | - | + +**响应示例**: +```json +{ + "code": 200, + "message": "success", + "data": { + "records": [ + { + "id": 1, + "title": "校园音乐节", + "description": "一年一度的校园音乐盛会", + "coverImage": "https://example.com/cover.jpg", + "startTime": "2026-01-15T19:00:00", + "endTime": "2026-01-15T22:00:00", + "registrationDeadline": "2026-01-14T18:00:00", + "location": "大礼堂", + "maxParticipants": 500, + "currentParticipants": 300, + "status": 1, + "category": "文艺活动", + "adminId": 1, + "adminName": "管理员", + "averageRating": 4.5, + "reviewCount": 20, + "isRegistered": false, + "createdAt": "2026-01-01T00:00:00" + } + ], + "total": 1, + "pages": 1, + "current": 1, + "size": 10 + }, + "timestamp": 1736620800000 +} +``` + +--- + +### 2.2 获取活动详情 + +**接口地址**: `GET /api/v1/activities/{id}` + +**请求权限**: 无需认证 + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | Long | 是 | 活动ID | + +**响应示例**: +```json +{ + "code": 200, + "message": "success", + "data": { + "id": 1, + "title": "校园音乐节", + "description": "一年一度的校园音乐盛会", + "coverImage": "https://example.com/cover.jpg", + "startTime": "2026-01-15T19:00:00", + "endTime": "2026-01-15T22:00:00", + "registrationDeadline": "2026-01-14T18:00:00", + "location": "大礼堂", + "maxParticipants": 500, + "currentParticipants": 300, + "status": 1, + "category": "文艺活动", + "adminId": 1, + "adminName": "管理员", + "averageRating": 4.5, + "reviewCount": 20, + "isRegistered": false, + "createdAt": "2026-01-01T00:00:00" + }, + "timestamp": 1736620800000 +} +``` + +--- + +### 2.3 创建活动(管理员) + +**接口地址**: `POST /api/v1/activities` + +**请求权限**: 管理员 (ADMIN) + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | 验证规则 | +|--------|------|------|------|----------| +| title | String | 是 | 活动名称 | 最多100个字符 | +| description | String | 否 | 活动简介 | 最多2000个字符 | +| coverImage | String | 否 | 封面图片URL | - | +| startTime | LocalDateTime | 是 | 开始时间 | 必须是未来时间 | +| endTime | LocalDateTime | 是 | 结束时间 | - | +| registrationDeadline | LocalDateTime | 否 | 报名截止时间 | - | +| location | String | 是 | 活动地点 | 最多200个字符 | +| maxParticipants | Integer | 是 | 报名人数上限 | 1-10000 | +| category | String | 否 | 活动分类 | 最多50个字符 | + +**请求示例**: +```json +{ + "title": "校园音乐节", + "description": "一年一度的校园音乐盛会", + "coverImage": "https://example.com/cover.jpg", + "startTime": "2026-01-15T19:00:00", + "endTime": "2026-01-15T22:00:00", + "registrationDeadline": "2026-01-14T18:00:00", + "location": "大礼堂", + "maxParticipants": 500, + "category": "文艺活动" +} +``` + +**响应示例**: +```json +{ + "code": 200, + "message": "创建成功", + "data": 1, + "timestamp": 1736620800000 +} +``` + +--- + +### 2.4 更新活动(管理员) + +**接口地址**: `PUT /api/v1/activities/{id}` + +**请求权限**: 管理员 (ADMIN) + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | Long | 是 | 活动ID | + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | 验证规则 | +|--------|------|------|------|----------| +| title | String | 是 | 活动名称 | 最多100个字符 | +| description | String | 否 | 活动简介 | 最多2000个字符 | +| coverImage | String | 否 | 封面图片URL | - | +| startTime | LocalDateTime | 是 | 开始时间 | - | +| endTime | LocalDateTime | 是 | 结束时间 | - | +| registrationDeadline | LocalDateTime | 否 | 报名截止时间 | - | +| location | String | 是 | 活动地点 | 最多200个字符 | +| maxParticipants | Integer | 是 | 报名人数上限 | 1-10000 | +| status | Integer | 否 | 活动状态 | - | +| category | String | 否 | 活动分类 | 最多50个字符 | + +**响应示例**: +```json +{ + "code": 200, + "message": "更新成功", + "data": null, + "timestamp": 1736620800000 +} +``` + +--- + +### 2.5 删除活动(管理员) + +**接口地址**: `DELETE /api/v1/activities/{id}` + +**请求权限**: 管理员 (ADMIN) + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | Long | 是 | 活动ID | + +**响应示例**: +```json +{ + "code": 200, + "message": "删除成功", + "data": null, + "timestamp": 1736620800000 +} +``` + +--- + +### 2.6 获取日历视图活动 + +**接口地址**: `GET /api/v1/activities/calendar` + +**请求权限**: 无需认证 + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| year | Integer | 是 | 年份 | +| month | Integer | 是 | 月份 | + +**响应示例**: +```json +{ + "code": 200, + "message": "success", + "data": [ + { + "id": 1, + "title": "校园音乐节", + "description": "一年一度的校园音乐盛会", + "coverImage": "https://example.com/cover.jpg", + "startTime": "2026-01-15T19:00:00", + "endTime": "2026-01-15T22:00:00", + "registrationDeadline": "2026-01-14T18:00:00", + "location": "大礼堂", + "maxParticipants": 500, + "currentParticipants": 300, + "status": 1, + "category": "文艺活动", + "adminId": 1, + "adminName": "管理员", + "averageRating": 4.5, + "reviewCount": 20, + "isRegistered": false, + "createdAt": "2026-01-01T00:00:00" + } + ], + "timestamp": 1736620800000 +} +``` + +--- + +### 2.7 检测时间冲突(管理员) + +**接口地址**: `POST /api/v1/activities/check-conflict` + +**请求权限**: 管理员 (ADMIN) + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| startTime | LocalDateTime | 是 | 开始时间 | +| endTime | LocalDateTime | 是 | 结束时间 | +| excludeActivityId | Long | 否 | 排除的活动ID(更新时使用) | + +**请求示例**: +```json +{ + "startTime": "2026-01-15T19:00:00", + "endTime": "2026-01-15T22:00:00", + "excludeActivityId": 1 +} +``` + +**响应示例**: +```json +{ + "code": 200, + "message": "success", + "data": { + "hasConflict": true, + "conflictActivities": [ + { + "id": 2, + "title": "校园篮球赛", + "startTime": "2026-01-15T18:00:00", + "endTime": "2026-01-15T20:00:00" + } + ] + }, + "timestamp": 1736620800000 +} +``` + +--- + +## 三、报名模块 (`/api/v1/registrations`) + +### 3.1 报名活动 + +**接口地址**: `POST /api/v1/registrations` + +**请求权限**: 需要认证 + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| activityId | Long | 是 | 活动ID | + +**请求示例**: +```json +{ + "activityId": 1 +} +``` + +**响应示例**: +```json +{ + "code": 200, + "message": "报名成功", + "data": { + "id": 1, + "activityId": 1, + "activityTitle": "校园音乐节", + "activityStartTime": "2026-01-15T19:00:00", + "activityEndTime": "2026-01-15T22:00:00", + "activityLocation": "大礼堂", + "ticketCode": "TICKET202601150001", + "ticketPdfUrl": "https://example.com/tickets/TICKET202601150001.pdf", + "status": 1, + "createdAt": "2026-01-10T10:00:00", + "canceledAt": null + }, + "timestamp": 1736620800000 +} +``` + +--- + +### 3.2 取消报名 + +**接口地址**: `DELETE /api/v1/registrations/{id}` + +**请求权限**: 需要认证 + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | Long | 是 | 报名记录ID | + +**响应示例**: +```json +{ + "code": 200, + "message": "取消成功", + "data": null, + "timestamp": 1736620800000 +} +``` + +--- + +### 3.3 获取我的报名列表 + +**接口地址**: `GET /api/v1/registrations/my` + +**请求权限**: 需要认证 + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | 默认值 | +|--------|------|------|------|--------| +| current | Long | 否 | 当前页码 | 1 | +| size | Long | 否 | 每页数量 | 10 | +| status | Integer | 否 | 报名状态 | - | + +**响应示例**: +```json +{ + "code": 200, + "message": "success", + "data": { + "records": [ + { + "id": 1, + "activityId": 1, + "activityTitle": "校园音乐节", + "activityStartTime": "2026-01-15T19:00:00", + "activityEndTime": "2026-01-15T22:00:00", + "activityLocation": "大礼堂", + "ticketCode": "TICKET202601150001", + "ticketPdfUrl": "https://example.com/tickets/TICKET202601150001.pdf", + "status": 1, + "createdAt": "2026-01-10T10:00:00", + "canceledAt": null + } + ], + "total": 1, + "pages": 1, + "current": 1, + "size": 10 + }, + "timestamp": 1736620800000 +} +``` + +--- + +### 3.4 获取活动报名列表(管理员) + +**接口地址**: `GET /api/v1/registrations/activity/{activityId}` + +**请求权限**: 管理员 (ADMIN) + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| activityId | Long | 是 | 活动ID | + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | 默认值 | +|--------|------|------|------|--------| +| current | Long | 否 | 当前页码 | 1 | +| size | Long | 否 | 每页数量 | 10 | + +**响应示例**: +```json +{ + "code": 200, + "message": "success", + "data": { + "records": [ + { + "id": 1, + "activityId": 1, + "activityTitle": "校园音乐节", + "activityStartTime": "2026-01-15T19:00:00", + "activityEndTime": "2026-01-15T22:00:00", + "activityLocation": "大礼堂", + "ticketCode": "TICKET202601150001", + "ticketPdfUrl": "https://example.com/tickets/TICKET202601150001.pdf", + "status": 1, + "createdAt": "2026-01-10T10:00:00", + "canceledAt": null + } + ], + "total": 1, + "pages": 1, + "current": 1, + "size": 10 + }, + "timestamp": 1736620800000 +} +``` + +--- + +### 3.5 下载电子票PDF + +**接口地址**: `GET /api/v1/registrations/{id}/ticket` + +**请求权限**: 需要认证 + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | Long | 是 | 报名记录ID | + +**响应**: PDF文件流 + +**响应头**: +``` +Content-Disposition: attachment; filename=ticket.pdf +Content-Type: application/pdf +``` + +--- + +## 四、签到模块 (`/api/v1/checkin`) + +### 4.1 生成签到二维码(管理员) + +**接口地址**: `POST /api/v1/checkin/qrcode/{activityId}` + +**请求权限**: 管理员 (ADMIN) + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| activityId | Long | 是 | 活动ID | + +**响应示例**: +```json +{ + "code": 200, + "message": "success", + "data": { + "qrCodeUrl": "https://example.com/qrcode/ACTIVITY1.png", + "qrCodeContent": "activity:1:2026-01-15T19:00:00", + "expiresAt": "2026-01-15T23:00:00" + }, + "timestamp": 1736620800000 +} +``` + +--- + +### 4.2 学生扫码签到 + +**接口地址**: `POST /api/v1/checkin/scan` + +**请求权限**: 需要认证 + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| qrCodeContent | String | 是 | 二维码内容 | + +**请求示例**: +```json +{ + "qrCodeContent": "activity:1:2026-01-15T19:00:00" +} +``` + +**响应示例**: +```json +{ + "code": 200, + "message": "签到成功", + "data": { + "id": 1, + "userId": 1, + "userName": "张三", + "studentId": "2021000001", + "activityId": 1, + "checkInTime": "2026-01-15T19:30:00", + "checkInMethod": 1 + }, + "timestamp": 1736620800000 +} +``` + +--- + +### 4.3 管理员扫学生票签到 + +**接口地址**: `POST /api/v1/checkin/ticket` + +**请求权限**: 管理员 (ADMIN) + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| activityId | Long | 是 | 活动ID | +| ticketCode | String | 是 | 电子票号 | + +**请求示例**: +```json +{ + "activityId": 1, + "ticketCode": "TICKET202601150001" +} +``` + +**响应示例**: +```json +{ + "code": 200, + "message": "签到成功", + "data": { + "id": 1, + "userId": 1, + "userName": "张三", + "studentId": "2021000001", + "activityId": 1, + "checkInTime": "2026-01-15T19:30:00", + "checkInMethod": 2 + }, + "timestamp": 1736620800000 +} +``` + +--- + +### 4.4 获取活动签到列表(管理员) + +**接口地址**: `GET /api/v1/checkin/activity/{activityId}` + +**请求权限**: 管理员 (ADMIN) + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| activityId | Long | 是 | 活动ID | + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | 默认值 | +|--------|------|------|------|--------| +| current | Long | 否 | 当前页码 | 1 | +| size | Long | 否 | 每页数量 | 10 | + +**响应示例**: +```json +{ + "code": 200, + "message": "success", + "data": { + "records": [ + { + "id": 1, + "userId": 1, + "userName": "张三", + "studentId": "2021000001", + "activityId": 1, + "checkInTime": "2026-01-15T19:30:00", + "checkInMethod": 1 + } + ], + "total": 1, + "pages": 1, + "current": 1, + "size": 10 + }, + "timestamp": 1736620800000 +} +``` + +--- + +## 五、评价模块 (`/api/v1/reviews`) + +### 5.1 提交评价 + +**接口地址**: `POST /api/v1/reviews` + +**请求权限**: 需要认证 + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | 验证规则 | +|--------|------|------|------|----------| +| activityId | Long | 是 | 活动ID | - | +| rating | Integer | 是 | 评分 | 1-5分 | +| content | String | 否 | 评论内容 | 最多500个字符 | + +**请求示例**: +```json +{ + "activityId": 1, + "rating": 5, + "content": "非常精彩的活动!" +} +``` + +**响应示例**: +```json +{ + "code": 200, + "message": "评价成功", + "data": { + "id": 1, + "userId": 1, + "userName": "张三", + "userAvatar": null, + "activityId": 1, + "activityTitle": "校园音乐节", + "rating": 5, + "content": "非常精彩的活动!", + "createdAt": "2026-01-16T10:00:00" + }, + "timestamp": 1736620800000 +} +``` + +--- + +### 5.2 获取活动评价列表 + +**接口地址**: `GET /api/v1/reviews/activity/{activityId}` + +**请求权限**: 无需认证 + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| activityId | Long | 是 | 活动ID | + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | 默认值 | +|--------|------|------|------|--------| +| current | Long | 否 | 当前页码 | 1 | +| size | Long | 否 | 每页数量 | 10 | + +**响应示例**: +```json +{ + "code": 200, + "message": "success", + "data": { + "records": [ + { + "id": 1, + "userId": 1, + "userName": "张三", + "userAvatar": null, + "activityId": 1, + "activityTitle": "校园音乐节", + "rating": 5, + "content": "非常精彩的活动!", + "createdAt": "2026-01-16T10:00:00" + } + ], + "total": 1, + "pages": 1, + "current": 1, + "size": 10 + }, + "timestamp": 1736620800000 +} +``` + +--- + +### 5.3 获取我的评价列表 + +**接口地址**: `GET /api/v1/reviews/my` + +**请求权限**: 需要认证 + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | 默认值 | +|--------|------|------|------|--------| +| current | Long | 否 | 当前页码 | 1 | +| size | Long | 否 | 每页数量 | 10 | + +**响应示例**: +```json +{ + "code": 200, + "message": "success", + "data": { + "records": [ + { + "id": 1, + "userId": 1, + "userName": "张三", + "userAvatar": null, + "activityId": 1, + "activityTitle": "校园音乐节", + "rating": 5, + "content": "非常精彩的活动!", + "createdAt": "2026-01-16T10:00:00" + } + ], + "total": 1, + "pages": 1, + "current": 1, + "size": 10 + }, + "timestamp": 1736620800000 +} +``` + +--- + +## 六、统计模块 (`/api/v1/statistics`) + +### 6.1 获取活动统计数据(管理员) + +**接口地址**: `GET /api/v1/statistics/activity/{activityId}` + +**请求权限**: 管理员 (ADMIN) + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| activityId | Long | 是 | 活动ID | + +**响应示例**: +```json +{ + "code": 200, + "message": "success", + "data": { + "activityId": 1, + "activityTitle": "校园音乐节", + "registeredCount": 300, + "checkedInCount": 250, + "checkInRate": 0.8333, + "reviewCount": 20, + "averageRating": 4.5, + "ratingDistribution": { + "1": 0, + "2": 1, + "3": 3, + "4": 6, + "5": 10 + } + }, + "timestamp": 1736620800000 +} +``` + +--- + +### 6.2 导出活动数据(管理员) + +**接口地址**: `GET /api/v1/statistics/activity/{activityId}/export` + +**请求权限**: 管理员 (ADMIN) + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| activityId | Long | 是 | 活动ID | + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | 默认值 | +|--------|------|------|------|--------| +| format | String | 否 | 导出格式 | excel | + +**响应**: 文件流 (Excel或PDF) + +--- + +### 6.3 获取总体统计(管理员) + +**接口地址**: `GET /api/v1/statistics/overview` + +**请求权限**: 管理员 (ADMIN) + +**响应示例**: +```json +{ + "code": 200, + "message": "success", + "data": { + "totalActivities": 50, + "totalRegistrations": 1000, + "totalCheckIns": 800, + "totalReviews": 500, + "averageRating": 4.3, + "monthlyStats": [ + { + "month": "2026-01", + "activityCount": 10, + "registrationCount": 200 + }, + { + "month": "2026-02", + "activityCount": 15, + "registrationCount": 300 + } + ] + }, + "timestamp": 1736620800000 +} +``` + +--- + +## 附录:数据模型说明 + +### 用户角色 (role) + +| 值 | 说明 | +|----|------| +| 0 | 学生 | +| 1 | 管理员 | + +### 用户状态 (status) + +| 值 | 说明 | +|----|------| +| 0 | 禁用 | +| 1 | 正常 | + +### 活动状态 (status) + +| 值 | 说明 | +|----|------| +| 0 | 草稿 | +| 1 | 已发布 | +| 2 | 进行中 | +| 3 | 已结束 | +| 4 | 已取消 | + +### 报名状态 (status) + +| 值 | 说明 | +|----|------| +| 0 | 已取消 | +| 1 | 已报名 | + +### 签到方式 (checkInMethod) + +| 值 | 说明 | +|----|------| +| 1 | 学生扫码签到 | +| 2 | 管理员扫票签到 | + +--- + +## 接口权限汇总 + +| 接口路径 | 方法 | 学生 | 管理员 | 说明 | +|----------|------|------|--------|------| +| `/auth/register` | POST | ✓ | ✓ | 公开 | +| `/auth/login` | POST | ✓ | ✓ | 公开 | +| `/auth/refresh` | POST | ✓ | ✓ | 公开 | +| `/auth/me` | GET | ✓ | ✓ | 需要认证 | +| `/auth/password` | PUT | ✓ | ✓ | 需要认证 | +| `/activities` | GET | ✓ | ✓ | 公开 | +| `/activities/{id}` | GET | ✓ | ✓ | 公开 | +| `/activities` | POST | ✗ | ✓ | 仅管理员 | +| `/activities/{id}` | PUT | ✗ | ✓ | 仅管理员 | +| `/activities/{id}` | DELETE | ✗ | ✓ | 仅管理员 | +| `/activities/calendar` | GET | ✓ | ✓ | 公开 | +| `/activities/check-conflict` | POST | ✗ | ✓ | 仅管理员 | +| `/registrations` | POST | ✓ | ✗ | 需要认证 | +| `/registrations/{id}` | DELETE | ✓ | ✗ | 需要认证 | +| `/registrations/my` | GET | ✓ | ✗ | 需要认证 | +| `/registrations/activity/{id}` | GET | ✗ | ✓ | 仅管理员 | +| `/registrations/{id}/ticket` | GET | ✓ | ✓ | 需要认证 | +| `/checkin/qrcode/{id}` | POST | ✗ | ✓ | 仅管理员 | +| `/checkin/scan` | POST | ✓ | ✗ | 需要认证 | +| `/checkin/ticket` | POST | ✗ | ✓ | 仅管理员 | +| `/checkin/activity/{id}` | GET | ✗ | ✓ | 仅管理员 | +| `/reviews` | POST | ✓ | ✗ | 需要认证 | +| `/reviews/activity/{id}` | GET | ✓ | ✓ | 公开 | +| `/reviews/my` | GET | ✓ | ✗ | 需要认证 | +| `/statistics/activity/{id}` | GET | ✗ | ✓ | 仅管理员 | +| `/statistics/activity/{id}/export` | GET | ✗ | ✓ | 仅管理员 | +| `/statistics/overview` | GET | ✗ | ✓ | 仅管理员 | \ No newline at end of file diff --git a/server/docs/init.sql b/server/docs/init.sql new file mode 100644 index 0000000..b898e22 --- /dev/null +++ b/server/docs/init.sql @@ -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); \ No newline at end of file diff --git a/server/docs/sample_data.sql b/server/docs/sample_data.sql new file mode 100644 index 0000000..a1f3fa2 --- /dev/null +++ b/server/docs/sample_data.sql @@ -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; \ No newline at end of file diff --git a/server/docs/前端开发规范.md b/server/docs/前端开发规范.md new file mode 100644 index 0000000..c300384 --- /dev/null +++ b/server/docs/前端开发规范.md @@ -0,0 +1,1343 @@ +# 校园活动组织与报名系统 - 前端开发规范 + +## 1. 命名规范 + +### 1.1 文件命名 + +| 类型 | 规范 | 示例 | +|------|------|------| +| Vue 组件 | PascalCase | `ActivityCard.vue`, `NavBar.vue` | +| TypeScript 文件 | camelCase | `useAuth.ts`, `request.ts` | +| 样式文件 | kebab-case | `global.scss`, `variables.scss` | +| 常量文件 | camelCase | `constant.ts` | +| 类型定义文件 | kebab-case.d.ts | `user.d.ts`, `activity.d.ts` | + +### 1.2 变量命名 + +```typescript +// 常量:全大写下划线分隔 +const API_BASE_URL = '/api/v1' +const MAX_FILE_SIZE = 1024 * 1024 * 5 + +// 变量:小驼峰 +const userName = 'zhangsan' +const activityList = [] + +// 布尔值:is/has/can 前缀 +const isLoading = false +const hasPermission = true +const canEdit = false + +// 私有属性:下划线前缀 +const _privateData = {} + +// 组件 props:小驼峰 +const props = defineProps<{ + activityId: number + showDetail: boolean +}>() + +// 事件名:on 前缀 +const emit = defineEmits<{ + onSubmit: [data: FormData] + onCancel: [] +}>() +``` + +### 1.3 函数命名 + +```typescript +// 获取数据:get 前缀 +function getActivityList() {} +function getUserInfo() {} + +// 设置数据:set 前缀 +function setToken(token: string) {} + +// 处理事件:handle 前缀 +function handleSubmit() {} +function handleClick() {} + +// 判断逻辑:is/has/can 前缀 +function isAdmin() {} +function hasRegistered() {} +function canCheckIn() {} + +// 格式化:format 前缀 +function formatDate(date: Date) {} +function formatPrice(price: number) {} + +// 转换:to 前缀 +function toArray(data: any) {} +function toString(value: any) {} + +// 异步操作:async/await +async function fetchActivities() {} +async function submitRegistration() {} +``` + +### 1.4 CSS 类命名 (BEM 规范) + +```scss +// Block__Element--Modifier + +// 活动卡片 +.activity-card { + // Element + &__header {} + &__title {} + &__content {} + &__footer {} + &__image {} + + // Modifier + &--active {} + &--disabled {} + &--loading {} +} + +// 示例 +
+
+

活动标题

+
+
...
+
+``` + +### 1.5 路由命名 + +```typescript +// 路由 name:PascalCase +{ + path: '/activities', + name: 'ActivityList', + component: () => import('@/views/activity/List.vue') +} + +// 路由 path:kebab-case +{ + path: '/my/registrations', + name: 'MyRegistrations', + component: () => import('@/views/registration/MyList.vue') +} +``` + +--- + +## 2. 代码风格规范 + +### 2.1 Vue 组件结构 + +```vue + + + + + +``` + +### 2.2 TypeScript 规范 + +```typescript +// 1. 明确类型声明,避免使用 any +// Bad +const data: any = {} + +// Good +interface ActivityData { + id: number + title: string +} +const data: ActivityData = { id: 1, title: '活动' } + +// 2. 使用 interface 定义对象类型 +interface User { + id: number + username: string + name: string + role: 0 | 1 +} + +// 3. 使用 type 定义联合类型 +type ActivityStatus = 0 | 1 | 2 | 3 +type RequestMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' + +// 4. 使用枚举定义常量集合 +enum ActivityStatusEnum { + PENDING = 0, // 未开始 + OPEN = 1, // 报名中 + ONGOING = 2, // 进行中 + ENDED = 3 // 已结束 +} + +// 5. 泛型使用 +interface ApiResponse { + code: number + message: string + data: T + timestamp: number +} + +// 6. 可选属性使用 ? +interface CreateActivityParams { + title: string + description?: string + coverImage?: string +} + +// 7. 只读属性使用 readonly +interface Config { + readonly apiUrl: string + readonly timeout: number +} +``` + +### 2.3 API 请求规范 + +```typescript +// src/api/activity.ts +import request from '@/utils/request' +import type { Activity, ActivityListParams, CreateActivityParams } from '@/types/activity' +import type { ApiResponse, PageResult } from '@/types/api' + +/** + * 获取活动列表 + * @param params 查询参数 + */ +export function getActivityList(params: ActivityListParams) { + return request.get>>('/activities', { params }) +} + +/** + * 获取活动详情 + * @param id 活动ID + */ +export function getActivityDetail(id: number) { + return request.get>(`/activities/${id}`) +} + +/** + * 创建活动(管理员) + * @param data 活动数据 + */ +export function createActivity(data: CreateActivityParams) { + return request.post>('/activities', data) +} + +/** + * 更新活动(管理员) + * @param id 活动ID + * @param data 更新数据 + */ +export function updateActivity(id: number, data: Partial) { + return request.put>(`/activities/${id}`, data) +} + +/** + * 删除活动(管理员) + * @param id 活动ID + */ +export function deleteActivity(id: number) { + return request.delete>(`/activities/${id}`) +} +``` + +### 2.4 Store 规范 + +```typescript +// src/stores/user.ts +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import type { UserInfo } from '@/types/user' +import { login, getUserInfo, refreshToken } from '@/api/auth' + +export const useUserStore = defineStore('user', () => { + // State + const token = ref(localStorage.getItem('accessToken')) + const refreshTokenValue = ref(localStorage.getItem('refreshToken')) + const userInfo = ref(null) + + // Getters + const isLoggedIn = computed(() => !!token.value) + const isAdmin = computed(() => userInfo.value?.role === 1) + const userName = computed(() => userInfo.value?.name || '') + + // Actions + async function loginAction(username: string, password: string) { + try { + const res = await login({ username, password }) + token.value = res.data.accessToken + refreshTokenValue.value = res.data.refreshToken + userInfo.value = res.data.userInfo + + localStorage.setItem('accessToken', res.data.accessToken) + localStorage.setItem('refreshToken', res.data.refreshToken) + + return true + } catch (error) { + return false + } + } + + function logout() { + token.value = null + refreshTokenValue.value = null + userInfo.value = null + + localStorage.removeItem('accessToken') + localStorage.removeItem('refreshToken') + } + + async function fetchUserInfo() { + try { + const res = await getUserInfo() + userInfo.value = res.data + } catch (error) { + console.error('获取用户信息失败', error) + } + } + + return { + // State + token, + userInfo, + // Getters + isLoggedIn, + isAdmin, + userName, + // Actions + loginAction, + logout, + fetchUserInfo + } +}) +``` + +### 2.5 Composable 规范 + +```typescript +// src/composables/usePagination.ts +import { ref, computed } from 'vue' + +interface PaginationOptions { + defaultPage?: number + defaultSize?: number +} + +export function usePagination(options: PaginationOptions = {}) { + const { defaultPage = 1, defaultSize = 10 } = options + + const currentPage = ref(defaultPage) + const pageSize = ref(defaultSize) + const total = ref(0) + + const totalPages = computed(() => Math.ceil(total.value / pageSize.value)) + const hasMore = computed(() => currentPage.value < totalPages.value) + + function setTotal(value: number) { + total.value = value + } + + function nextPage() { + if (hasMore.value) { + currentPage.value++ + } + } + + function prevPage() { + if (currentPage.value > 1) { + currentPage.value-- + } + } + + function reset() { + currentPage.value = defaultPage + total.value = 0 + } + + return { + currentPage, + pageSize, + total, + totalPages, + hasMore, + setTotal, + nextPage, + prevPage, + reset + } +} +``` + +--- + +## 3. 组件开发规范 + +### 3.1 组件分类 + +| 类型 | 位置 | 说明 | +|------|------|------| +| 基础组件 | components/common/ | 通用 UI 组件,如 NavBar、TabBar | +| 业务组件 | components/[module]/ | 特定业务模块组件 | +| 页面组件 | views/[module]/ | 路由对应的页面组件 | +| 布局组件 | layouts/ | 页面布局组件 | + +### 3.2 组件设计原则 + +```vue + + + + + + + + + + + + + + + +``` + +### 3.3 组件 Props 规范 + +```typescript +// 1. 使用 TypeScript 定义 Props +interface Props { + // 必填属性 + activityId: number + // 可选属性,带默认值 + showImage?: boolean + // 复杂类型 + activity: Activity +} + +const props = withDefaults(defineProps(), { + showImage: true +}) + +// 2. Props 校验(可选,TS 已提供类型检查) +const props = defineProps({ + activityId: { + type: Number, + required: true + }, + status: { + type: Number, + default: 0, + validator: (value: number) => [0, 1, 2, 3].includes(value) + } +}) +``` + +### 3.4 组件样式规范 + +```vue + +``` + +--- + +## 4. 类型定义规范 + +### 4.1 API 响应类型 + +```typescript +// src/types/api.d.ts + +// 统一响应格式 +interface ApiResponse { + code: number + message: string + data: T + timestamp: number +} + +// 分页响应 +interface PageResult { + records: T[] + total: number + pages: number + current: number + size: number +} + +// 分页请求参数 +interface PageParams { + page?: number + size?: number +} +``` + +### 4.2 业务类型定义 + +```typescript +// src/types/user.d.ts +interface UserInfo { + id: number + username: string + name: string + studentId: string | null + email: string | null + phone: string | null + avatar: string | null + role: 0 | 1 // 0-学生,1-管理员 +} + +interface LoginRequest { + username: string + password: string +} + +interface LoginResponse { + accessToken: string + refreshToken: string + expiresIn: number + tokenType: string + userInfo: UserInfo +} + +// src/types/activity.d.ts +interface Activity { + id: number + title: string + description: string + coverImage: string | null + startTime: string + endTime: string + registrationDeadline: string | null + location: string + maxParticipants: number + currentParticipants: number + status: ActivityStatus + category: string | null + adminId: number + adminName?: string + averageRating?: number + reviewCount?: number + isRegistered?: boolean + createdAt: string +} + +type ActivityStatus = 0 | 1 | 2 | 3 + +interface ActivityListParams extends PageParams { + status?: ActivityStatus + keyword?: string + category?: string + startDate?: string + endDate?: string +} + +interface CreateActivityParams { + title: string + description?: string + coverImage?: string + startTime: string + endTime: string + registrationDeadline?: string + location: string + maxParticipants: number + category?: string +} + +// src/types/registration.d.ts +interface Registration { + id: number + activityId: number + activityTitle: string + activityStartTime: string + activityLocation: string + ticketCode: string + ticketPdfUrl: string + status: RegistrationStatus + createdAt: string +} + +type RegistrationStatus = 0 | 1 | 2 // 0-已取消,1-已报名,2-已签到 + +// src/types/review.d.ts +interface Review { + id: number + userId: number + userName: string + userAvatar: string | null + activityId: number + rating: 1 | 2 | 3 | 4 | 5 + content: string + createdAt: string +} + +interface CreateReviewParams { + activityId: number + rating: number + content: string +} +``` + +--- + +## 5. 错误处理规范 + +### 5.1 API 错误处理 + +```typescript +// src/utils/request.ts +import axios from 'axios' +import { showToast, showDialog } from 'vant' +import router from '@/router' +import { useUserStore } from '@/stores/user' + +const instance = axios.create({ + baseURL: import.meta.env.VITE_API_BASE_URL, + timeout: 10000 +}) + +// 响应拦截器 +instance.interceptors.response.use( + response => { + const { code, message, data } = response.data + + // 业务成功 + if (code === 200) { + return response.data + } + + // 业务错误 + showToast(message || '操作失败') + return Promise.reject(new Error(message)) + }, + error => { + const { response } = error + + if (response) { + const { status, data } = response + + switch (status) { + case 400: + showToast(data.message || '请求参数错误') + break + case 401: + // Token 过期,清除登录状态,跳转登录页 + const userStore = useUserStore() + userStore.logout() + router.push('/login') + showToast('登录已过期,请重新登录') + break + case 403: + showToast('无权限访问') + break + case 404: + showToast('请求的资源不存在') + break + case 409: + // 业务冲突,显示具体原因 + showToast(data.message || '操作冲突') + break + case 500: + showToast('服务器异常,请稍后重试') + break + default: + showToast('网络异常') + } + } else { + showToast('网络连接失败') + } + + return Promise.reject(error) + } +) +``` + +### 5.2 组件错误处理 + +```vue + +``` + +### 5.3 表单校验 + +```vue + + + +``` + +--- + +## 6. 注释规范 + +### 6.1 文件头注释 + +```typescript +/** + * @file 活动相关 API 接口 + * @author Your Name + * @description 包含活动的增删改查、日历视图、冲突检测等接口 + */ +``` + +### 6.2 函数注释 + +```typescript +/** + * 格式化日期时间 + * @param date - 日期字符串或 Date 对象 + * @param format - 格式化模板,默认 'YYYY-MM-DD HH:mm' + * @returns 格式化后的日期字符串 + * @example + * formatDateTime('2025-06-01T09:00:00') // '2025-06-01 09:00' + * formatDateTime(new Date(), 'YYYY年MM月DD日') // '2025年06月01日' + */ +export function formatDateTime( + date: string | Date, + format: string = 'YYYY-MM-DD HH:mm' +): string { + return dayjs(date).format(format) +} +``` + +### 6.3 组件注释 + +```vue + +``` + +### 6.4 复杂逻辑注释 + +```typescript +async function handleRegister() { + // 1. 检查是否已登录 + if (!userStore.isLoggedIn) { + router.push('/login') + return + } + + // 2. 检查报名状态 + // - 已报名:提示已报名 + // - 已满员:提示名额已满 + // - 已截止:提示报名已截止 + if (activity.value.isRegistered) { + showToast('您已报名该活动') + return + } + + // 3. 发起报名请求 + // ... +} +``` + +--- + +## 7. Git 提交规范 + +### 7.1 提交信息格式 + +``` +(): + + + +