feat: 完善后端基础架构和登录功能

- 添加 Lombok 配置支持
- 完善枚举类和常量定义
- 新增工具类(TraceId、限流、OSS 等)
- 添加切面(日志、限流、TraceId)
- 更新数据库索引规范(应用层防重)
- 登录页面样式优化
- 前后端项目文档补充
This commit is contained in:
En 2026-03-31 13:58:28 +08:00
parent 48fc71b41d
commit b805f456a6
435 changed files with 31891 additions and 1549 deletions

View File

@ -2,6 +2,24 @@
本项目 Java 后端开发规范,基于 Spring Boot + MyBatis-Plus 技术栈。 本项目 Java 后端开发规范,基于 Spring Boot + MyBatis-Plus 技术栈。
## JDK 版本要求(重要)
**必须使用 JDK 17** 进行编译和运行。
如果系统环境变量配置的是 JDK 1.8,请在编译前设置 `JAVA_HOME`
```bash
# Windows (Git Bash) - 根据实际安装路径调整
export JAVA_HOME="/f/Java/jdk-17"
export PATH="$JAVA_HOME/bin:$PATH"
# 编译项目
mvn clean compile -DskipTests
# 或者在启动时指定
mvn spring-boot:run -Djava.home="/f/Java/jdk-17"
```
## 核心原则 ## 核心原则
1. **OpenAPI 规范驱动** - 前后端通过接口规范对齐 1. **OpenAPI 规范驱动** - 前后端通过接口规范对齐
@ -139,6 +157,92 @@ Result<T> {
- `lock:{resource}:{id}` - 分布式锁 - `lock:{resource}:{id}` - 分布式锁
- `rate_limit:{key}` - 限流计数器 - `rate_limit:{key}` - 限流计数器
## 数据库索引规范(重要)
### 唯一索引处理原则
**项目使用逻辑删除,唯一索引通过应用层控制,而非数据库唯一约束。**
#### 背景问题
当表有逻辑删除字段(`deleted`)时,数据库唯一索引会导致:
1. 删除后无法重新添加相同数据
2. 错误信息不友好,直接返回数据库异常
#### 解决方案
| 层级 | 职责 | 实现方式 |
|------|------|----------|
| **数据库层** | 普通索引 | `KEY idx_xxx (xxx)` 而非 `UNIQUE KEY uk_xxx (xxx)` |
| **应用层** | 重复校验 | Service 层查询 + 悲观锁 `FOR UPDATE` |
| **异常处理** | 兜底处理 | 全局异常处理器捕获 `DuplicateKeyException` |
#### 实现示例
**1. 数据库迁移Flyway**
```sql
-- ❌ 错误:使用唯一索引
UNIQUE KEY `uk_username` (`username`)
-- ✅ 正确:使用普通索引
KEY `idx_username` (`username`)
```
**2. Mapper 层(悲观锁查询)**
```java
/**
* 根据用户名查询用户(悲观锁,用于创建时防并发)
*/
@Select("SELECT * FROM t_user WHERE username = #{username} AND tenant_id = #{tenantId} AND deleted = 0 FOR UPDATE")
User getUserByUsernameForUpdate(@Param("username") String username, @Param("tenantId") Long tenantId);
```
**3. Service 层(事务 + 校验)**
```java
@Transactional(rollbackFor = Exception.class)
public UserVO createUser(CreateUserDTO dto, Long tenantId, Long operatorId) {
log.info("开始创建用户,用户名:{}", dto.getUsername());
// 使用悲观锁检查用户名是否已存在(防止并发)
User existingUser = userMapper.getUserByUsernameForUpdate(dto.getUsername(), tenantId);
if (existingUser != null) {
throw new BusinessException("用户名已存在");
}
// 插入用户
User user = convert(dto);
userMapper.insert(user);
return convertToVO(user);
}
```
**4. 需要应用层校验的字段**
以下字段需要添加应用层重复校验:
| 表 | 字段 | 说明 |
|-----|------|------|
| t_user | username, email, phone | 用户名、邮箱、手机号 |
| t_role | code | 角色编码 |
| t_permission | code | 权限编码 |
| t_dict | code + tenant_id | 字典编码 |
| t_tenant | code | 租户编码 |
| t_sys_config | config_key | 配置键 |
### 关联表索引规范
对于多对多关联表,使用普通索引而非唯一索引:
```sql
-- t_user_role: 用户角色关联表
KEY `idx_user_role` (`user_id`, `role_id`)
-- t_role_permission: 角色权限关联表
KEY `idx_role_permission` (`role_id`, `permission_id`)
```
**注意**:关联表的重复数据控制在应用层业务逻辑中处理。
### 核心环境变量 ### 核心环境变量
- `SPRING_PROFILES_ACTIVE` - 活跃环境 - `SPRING_PROFILES_ACTIVE` - 活跃环境

312
CLAUDE.md
View File

@ -1,31 +1,60 @@
# CLAUDE.md # CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 本文档为 Claude Code 在本项目中工作提供指导。
## 项目目录结构(重要)
| 目录 | 说明 |
|------|------|
| `java-backend/` | **后端项目目录** - Spring Boot 应用 |
| `java-frontend/` | **前端项目目录** - Vue 3 应用 |
**所有后续开发都基于这两个项目进行。**
## JDK 版本要求(重要)
**必须使用 JDK 17** 进行编译和运行。
如果系统环境变量配置的是 JDK 1.8,请在编译前设置 `JAVA_HOME`
```bash
# Windows (Git Bash) - 根据实际安装路径调整
export JAVA_HOME="/f/Java/jdk-17"
export PATH="$JAVA_HOME/bin:$PATH"
# 编译项目
cd java-backend
mvn clean compile -DskipTests
# 启动项目
mvn spring-boot:run -Djava.home="/f/Java/jdk-17"
```
## 快速开始 ## 快速开始
```bash ```bash
# 安装所有依赖(前端 + 后端) # 前端 - 安装依赖并启动
cd java-frontend
pnpm install pnpm install
# 同时启动前后端开发服务器
pnpm dev pnpm dev
# 或分别启动 # 后端 - 安装依赖并启动
pnpm dev:frontend # 前端 http://localhost:3000 cd java-backend
pnpm dev:backend # 后端 http://localhost:3001 mvn clean install
mvn spring-boot:run -Dspring.profiles.active=dev
``` ```
## 技术栈 ## 技术栈
### 后端 ### 后端 (java-backend/)
- **框架**: NestJS + TypeScript - **框架**: Spring Boot 3.2 + Java 17
- **数据库**: MySQL 8.0 + Prisma ORM - **持久层**: MyBatis-Plus 3.5+
- **认证**: JWT + RBAC (基于角色的访问控制) - **数据库**: MySQL 8.0 + Flyway 迁移
- **多租户**: 数据隔离架构(每个租户独立 tenantId - **认证**: Spring Security + JWT
- **缓存**: Redis
- **对象映射**: MapStruct 1.5+
### 前端 ### 前端 (java-frontend/)
- **框架**: Vue 3 + TypeScript + Vite - **框架**: Vue 3 + TypeScript + Vite
- **UI 组件**: Ant Design Vue - **UI 组件**: Ant Design Vue
- **状态管理**: Pinia - **状态管理**: Pinia
@ -33,41 +62,35 @@ pnpm dev:backend # 后端 http://localhost:3001
## 核心命令 ## 核心命令
### 开发 ### 后端开发 (java-backend/)
```bash ```bash
# 根目录 cd java-backend
pnpm dev # 同时启动前后端
pnpm dev:frontend # 只启动前端
pnpm dev:backend # 只启动后端
# 前端目录 # 编译
pnpm dev # 启动前端 mvn clean compile -DskipTests
# 后端目录 # 启动
pnpm start:dev # 启动后端 mvn spring-boot:run -Dspring.profiles.active=dev
pnpm prisma:studio # Prisma 数据库可视化
# 打包
mvn clean package -DskipTests
# 数据库迁移
mvn flyway:migrate
``` ```
### 数据库迁移 ### 前端开发 (java-frontend/)
```bash ```bash
cd backend cd java-frontend
pnpm prisma:generate # 生成 Prisma Client
pnpm prisma:migrate # 开发环境迁移
pnpm prisma:migrate:deploy # 生产环境部署
```
### 构建 # 安装依赖
```bash pnpm install
pnpm build # 构建前后端
pnpm build:frontend # 只构建前端
pnpm build:backend # 只构建后端
```
### 测试 # 启动开发服务器
```bash pnpm dev
cd backend
pnpm test # 运行单元测试 # 构建
pnpm test:cov # 测试覆盖率 pnpm build
``` ```
## 架构概览 ## 架构概览
@ -75,122 +98,88 @@ pnpm test:cov # 测试覆盖率
### 目录结构 ### 目录结构
``` ```
library-picturebook-activity/ library-picturebook-activity/
├── backend/ # NestJS 后端 ├── java-backend/ # Spring Boot 后端
│ ├── prisma/ # Prisma schema 和 migrations │ ├── src/main/java/
│ ├── src/ │ │ └── com/lesingle/creation/
│ │ ├── auth/ # 认证模块 (JWT) │ │ ├── common/ # 公共模块(常量、异常、工具)
│ │ ├── users/ # 用户管理 │ │ ├── config/ # 配置类
│ │ ├── roles/ # 角色权限 │ │ ├── controller/ # REST 控制器
│ │ ├── menus/ # 菜单管理 │ │ ├── dto/ # 数据传输对象
│ │ ├── tenants/ # 租户管理 │ │ ├── entity/ # 实体类
│ │ ├── contests/ # 竞赛模块 │ │ ├── mapper/ # MyBatis Mapper
│ │ │ ├── contests/ # 竞赛管理 │ │ ├── service/ # 业务逻辑层
│ │ │ ├── works/ # 作品管理 │ │ │ └── impl/ # Service 实现
│ │ │ ├── teams/ # 团队管理 │ │ └── vo/ # 视图对象
│ │ │ ├── registrations/ # 报名管理 │ └── src/main/resources/
│ │ │ └── reviews/ # 评审管理 │ ├── application*.yml # 配置文件
│ │ ├── school/ # 学校模块 │ ├── db/migration/ # Flyway 迁移脚本
│ │ │ ├── schools/ │ └── mapper/ # MyBatis XML
│ │ │ ├── grades/
│ │ │ ├── classes/
│ │ │ ├── teachers/
│ │ │ └── students/
│ │ └── prisma/ # Prisma 服务
│ └── package.json
└── frontend/ # Vue 3 前端 └── java-frontend/ # Vue 3 前端
├── src/ ├── src/
│ ├── api/ # API 接口 │ ├── api/ # API 接口
│ ├── views/ # 页面组件 │ ├── views/ # 页面组件
│ ├── components/ # 公共组件 │ ├── components/ # 公共组件
│ ├── stores/ # Pinia 状态 │ ├── stores/ # Pinia 状态
│ ├── router/ # 路由配置 │ ├── router/ # 路由配置
│ └── composables/ # 组合式函数 │ ├── composables/ # 组合式函数
└── package.json │ └── utils/ # 工具函数
└── public/
``` ```
### 模块结构(后端) ### 后端模块结构
每个功能模块包含: 每个功能模块包含:
- `module.ts` - 模块定义 - `XxxController.java` - REST 控制器
- `controller.ts` - REST 控制器 - `XxxService.java` / `XxxServiceImpl.java` - 业务逻辑
- `service.ts` - 业务逻辑 - `XxxMapper.java` - 数据访问层
- `dto/` - 数据传输对象 - `Xxx.java` (entity/) - 实体类
- `XxxDTO.java` / `XxxVO.java` - 数据传输对象
### 多租户架构
- 所有业务数据必须包含 `tenantId` 字段
- 查询必须包含租户隔离条件
- 超级租户(`isSuper = 1`)可访问所有数据
## 关键开发规范 ## 关键开发规范
### 后端规范 ### 后端规范
1. **租户隔离(强制)**:所有数据库查询必须包含 `tenantId` 1. **三层架构原则**
```typescript - Controller 层DTO ↔ VO 转换
// ✅ 正确 - Service 层:使用 Entity继承 `IService<T>`
const data = await prisma.model.findMany({ where: { tenantId } }); - Mapper 层:使用 Entity
// ❌ 错误 - 缺少 tenantId 2. **日志规范**
const data = await prisma.model.findMany({}); - **所有日志必须使用中文**
``` - 使用 MDC 实现 TraceId 链路追踪
- 开发/测试环境DEBUG 级别
- 生产环境INFO/WARN 级别
2. **审计字段**:所有表必须包含 3. **权限控制**:使用权限注解进行接口保护
- `tenantId` - 租户 ID
- `creator`/`modifier` - 创建/修改人
- `createTime`/`modifyTime` - 时间戳
- `validState` - 有效状态1-有效2-失效)
3. **权限控制**:使用 `@RequirePermission('module:action')` 装饰器 4. **DTO 验证**:使用 Validation 注解
4. **DTO 验证**:使用 `class-validator` 装饰器
### 前端规范 ### 前端规范
1. **路由包含租户编码**`/:tenantCode/xxx` 1. **API 调用**:放在 `src/api/` 目录,按模块组织
2. **API 调用**:放在 `api/` 目录,按模块组织 2. **状态管理**:使用 Piniastore 命名 `xxxStore`
3. **状态管理**:使用 Piniastore 命名 `xxxStore` 3. **组件语法**:使用 `<script setup lang="ts">`
4. **组件语法**:使用 `<script setup lang="ts">` 4. **路由**:使用 Vue Router 4
## 环境变量 ## 环境变量
### 后端 (.env) ### 后端 (java-backend/src/main/resources/)
```env - `application.yml` - 主配置
DATABASE_URL="mysql://user:password@localhost:3306/db_name" - `application-dev.yml` - 开发环境
JWT_SECRET="your-secret-key" - `application-test.yml` - 测试环境
PORT=3001 - `application-prod.yml` - 生产环境
```
### 前端 (.env.development) ### 前端 (java-frontend/)
```env - `.env.development` - 开发环境
VITE_API_BASE_URL=/api - `.env.test` - 测试环境
``` - `.env.production` - 生产环境
## 初始化脚本(后端)
```bash
pnpm init:admin # 初始化管理员账户
pnpm init:menus # 初始化菜单
pnpm init:super-tenant # 初始化超级租户
pnpm init:tenant-admin # 初始化租户管理员
pnpm init:roles:all # 初始化所有角色权限
```
## Cursor Rules
项目使用 `.cursor/rules/` 目录定义开发规范:
- `project-overview.mdc` - 项目概述
- `backend-architecture.mdc` - 后端架构规范
- `frontend-architecture.mdc` - 前端架构规范
- `database-design.mdc` - 数据库设计规范
- `multi-tenant.mdc` - 多租户数据隔离规范
- `code-review-checklist.mdc` - 代码审查清单
## Git 提交规范 ## Git 提交规范
格式:`类型: 描述` 格式:`类型:描述`
类型: 类型:
- `feat` - 新功能 - `feat` - 新功能
@ -200,3 +189,64 @@ pnpm init:roles:all # 初始化所有角色权限
- `refactor` - 代码重构 - `refactor` - 代码重构
- `test` - 测试相关 - `test` - 测试相关
- `chore` - 构建/工具相关 - `chore` - 构建/工具相关
## 编码规范
- **语言**:所有注释、日志、提示信息使用中文
- **路径**:使用 Unix 风格路径 `/f/...` 而非 `F:\...`
- **代码**:尽量使用中文注释
## 数据库索引规范(重要)
**项目使用逻辑删除,唯一索引通过应用层控制,而非数据库唯一约束。**
### 背景问题
当表有逻辑删除字段(`deleted`)时,数据库唯一索引会导致:
1. 删除后无法重新添加相同数据
2. 错误信息不友好,直接返回数据库异常
### 解决方案
| 层级 | 职责 | 实现方式 |
|------|------|----------|
| **数据库层** | 普通索引 | `KEY idx_xxx (xxx)` 而非 `UNIQUE KEY uk_xxx (xxx)` |
| **应用层** | 重复校验 | Service 层查询 + 悲观锁 `FOR UPDATE` |
| **异常处理** | 兜底处理 | 全局异常处理器捕获 `DuplicateKeyException` |
### 需要应用层校验的字段
以下字段需要添加应用层重复校验(使用悲观锁):
| 表 | 字段 | 说明 |
|-----|------|------|
| t_user | username, email, phone | 用户名、邮箱、手机号 |
| t_role | code | 角色编码 |
| t_permission | code | 权限编码 |
| t_dict | code + tenant_id | 字典编码 |
| t_tenant | code | 租户编码 |
| t_sys_config | config_key | 配置键 |
### 实现示例
**Mapper 层(悲观锁查询)**
```java
@Select("SELECT * FROM t_user WHERE username = #{username} AND tenant_id = #{tenantId} AND deleted = 0 FOR UPDATE")
User getUserByUsernameForUpdate(@Param("username") String username, @Param("tenantId") Long tenantId);
```
**Service 层(事务 + 校验)**
```java
@Transactional(rollbackFor = Exception.class)
public UserVO createUser(CreateUserDTO dto, Long tenantId, Long operatorId) {
// 使用悲观锁检查用户名是否已存在(防止并发)
User existingUser = userMapper.getUserByUsernameForUpdate(dto.getUsername(), tenantId);
if (existingUser != null) {
throw new BusinessException("用户名已存在");
}
// 插入用户
...
}
```
详细规范参考:`.claude/memory/java-backend.md`

View File

@ -0,0 +1,951 @@
# NestJS 后端项目分析报告
> 项目路径:`backend/`
> 分析日期2026-03-28
> 项目类型:多租户竞赛/活动管理系统
---
## 一、技术栈概览
| 组件 | 技术选型 | 版本 |
|------|----------|------|
| **框架** | NestJS | 10.3.3 |
| **语言** | TypeScript | 5.3.3 |
| **数据库** | MySQL | 8.0+ |
| **ORM** | Prisma | 6.19.0 |
| **认证** | JWT + Passport | - |
| **加密** | bcrypt | 6.0.0 |
| **文件存储** | 腾讯云 COS | - |
| **部署** | PM2 | - |
| **AI 集成** | 腾讯混元 | - |
### 核心依赖
```json
{
"@nestjs/common": "^10.3.3",
"@nestjs/core": "^10.3.3",
"@nestjs/jwt": "^10.2.0",
"@nestjs/passport": "^10.0.3",
"@prisma/client": "^6.19.0",
"passport-jwt": "^4.0.1",
"bcrypt": "^6.0.0",
"class-validator": "^0.14.1",
"class-transformer": "^0.5.1",
"cos-nodejs-sdk-v5": "^2.15.4"
}
```
---
## 二、项目架构
### 目录结构
```
backend/
├── src/
│ ├── ai-3d/ # AI 3D 生成模块
│ ├── auth/ # 认证授权模块
│ ├── common/ # 公共模块(过滤器、拦截器)
│ ├── config/ # 系统配置模块
│ ├── contests/ # 竞赛管理模块(核心)
│ │ ├── contests/ # 活动管理
│ │ ├── attachments/ # 附件管理
│ │ ├── judges/ # 评委管理
│ │ ├── notices/ # 公告管理
│ │ ├── preset-comments/ # 预设评语库
│ │ ├── registrations/ # 报名管理
│ │ ├── results/ # 结果管理
│ │ ├── review-rules/ # 评审规则
│ │ ├── reviews/ # 评审管理
│ │ ├── teams/ # 团队管理
│ │ └── works/ # 作品管理
│ ├── dict/ # 数据字典模块
│ ├── homework/ # 作业模块
│ │ ├── homeworks/ # 作业管理
│ │ ├── review-rules/ # 评审规则
│ │ ├── scores/ # 评分管理
│ │ └── submissions/ # 提交管理
│ ├── judges-management/ # 评委管理模块
│ ├── logs/ # 日志模块
│ ├── menus/ # 菜单管理模块
│ ├── oss/ # 对象存储模块
│ ├── permissions/ # 权限管理模块
│ ├── prisma/ # Prisma 服务
│ ├── public/ # 公共接口模块
│ ├── roles/ # 角色管理模块
│ ├── school/ # 学校管理模块
│ │ ├── schools/ # 学校管理
│ │ ├── grades/ # 年级管理
│ │ ├── classes/ # 班级管理
│ │ ├── departments/ # 部门管理
│ │ ├── teachers/ # 教师管理
│ │ └── students/ # 学生管理
│ ├── tenants/ # 租户管理模块
│ ├── upload/ # 文件上传模块
│ ├── users/ # 用户管理模块
│ ├── app.module.ts # 根模块
│ └── main.ts # 入口文件
├── prisma/
│ ├── schema.prisma # 数据库模型定义
│ └── migrations/ # 数据库迁移文件
├── scripts/ # 初始化脚本
├── test/ # 测试文件
└── package.json
```
---
## 三、核心功能模块详解
### 1. 认证授权模块 (`auth/`)
**文件结构:**
```
auth/
├── auth.controller.ts # 认证控制器
├── auth.service.ts # 认证服务
├── auth.module.ts # 认证模块
├── decorators/ # 装饰器
│ ├── current-tenant-id.decorator.ts # 当前租户 ID 装饰器
│ ├── public.decorator.ts # 公开接口装饰器
│ ├── require-permission.decorator.ts # 权限装饰器
│ └── roles.decorator.ts # 角色装饰器
├── guards/ # 守卫
│ ├── jwt-auth.guard.ts # JWT 认证守卫
│ ├── roles.guard.ts # 角色守卫
│ └── permissions.guard.ts # 权限守卫
└── strategies/ # Passport 策略
├── jwt.strategy.ts # JWT 策略
└── local.strategy.ts # 本地策略
```
**核心功能:**
- 用户名密码登录
- JWT Token 签发与验证
- 权限验证(基于装饰器)
- 角色验证
- 多租户识别
**核心代码示例:**
```typescript
// auth.service.ts - 登录逻辑
async login(user: any, tenantId?: number) {
// 验证租户有效
const tenant = await this.prisma.tenant.findUnique({
where: { id: finalTenantId },
});
// 签发 JWT Token
const payload = {
username: user.username,
sub: user.id,
tenantId: finalTenantId,
};
return {
token: this.jwtService.sign(payload),
user: {
id: user.id,
username: user.username,
roles: user.roles?.map((ur: any) => ur.role.code) || [],
permissions: await this.getUserPermissions(user.id),
},
};
}
```
---
### 2. 用户管理模块 (`users/`)
**核心功能:**
- 用户 CRUD 操作
- 用户状态管理(启用/禁用)
- 关键字搜索(用户名、昵称、邮箱、手机号)
- 超级租户跨租户查询
- 用户统计分析
**接口列表:**
| 接口 | 方法 | 权限 | 说明 |
|------|------|------|------|
| `POST /users` | POST | `user:create` | 创建用户 |
| `GET /users` | GET | `user:view` | 用户列表(分页) |
| `GET /users/stats` | GET | `super_admin` | 用户统计 |
| `GET /users/:id` | GET | `user:view` | 用户详情 |
| `PUT /users/:id` | PUT | `user:update` | 更新用户 |
| `PUT /users/:id/status` | PUT | `user:manage` | 切换状态 |
| `DELETE /users/:id` | DELETE | `user:delete` | 删除用户 |
**特色功能:**
```typescript
// 用户类型统计(仅超管)
async getStats() {
const superTenantIds = ...; // 平台租户 ID
const orgTenantIds = ...; // 机构租户 ID
const judgeTenantIds = ...; // 评委租户 ID
return {
total, // 总用户数
platform, // 平台用户数
org, // 机构用户数
judge, // 评委用户数
public: publicCount, // 公共用户数
};
}
```
---
### 3. 竞赛/活动管理模块 (`contests/`) ⭐核心业务
**模块结构:**
```
contests/
├── contests/ # 活动主体管理
├── registrations/ # 报名管理
├── teams/ # 团队管理
├── works/ # 作品管理
├── reviews/ # 评审管理
├── judges/ # 评委管理
├── results/ # 结果管理
├── review-rules/ # 评审规则
├── preset-comments/ # 预设评语库
├── notices/ # 公告管理
└── attachments/ # 附件管理
```
#### 3.1 活动管理 (`contests/contests/`)
**活动生命周期:**
```
┌─────────────┐
│ 未发布 │
│ unpublished │
└──────┬──────┘
│ 发布
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 已发布 │ ──► │ 报名中 │ ──► │ 作品提交中 │ ──► │ 评审中 │
│ published │ │ registering │ │ submitting │ │ reviewing │
└─────────────┘ └─────────────┘ └─────────────┘ └──────┬──────┘
┌─────────────┐ │
│ 已完结 │ ◄────────────────┘
│ finished │
└─────────────┘
```
**核心服务方法:**
```typescript
// 判断活动当前所处阶段
getContestStage(contest: any): string {
const now = new Date();
const regStart = new Date(contest.registerStartTime);
const regEnd = new Date(contest.registerEndTime);
const subStart = new Date(contest.submitStartTime);
const subEnd = new Date(contest.submitEndTime);
const revStart = new Date(contest.reviewStartTime);
const revEnd = new Date(contest.reviewEndTime);
if (now >= regStart && now <= regEnd) return 'registering';
if (now >= subStart && now <= subEnd) return 'submitting';
if (now >= revStart && now <= revEnd) return 'reviewing';
if (revEnd && now > revEnd) return 'finished';
return 'published';
}
```
#### 3.2 报名管理 (`registrations/`)
**功能:**
- 个人/团队报名
- 报名审核(需要审核的活动)
- 报名状态管理
#### 3.3 作品管理 (`works/`)
**功能:**
- 作品提交(支持多次提交配置)
- 作品版本管理(`isLatest` 标识最新版本)
- 作品状态流转
#### 3.4 评审管理 (`reviews/`)
**功能:**
- 评委分配(手动/批量/自动)
- 作品评分
- 分数统计
**评分 DTO**
```typescript
// create-score.dto.ts
export class CreateScoreDto {
@IsInt()
workId: number;
@IsInt()
judgeId: number;
@IsNumber()
score: number;
@IsString()
@IsOptional()
comment: string; // 评语
}
```
#### 3.5 结果管理 (`results/`)
**功能:**
- 手动设置奖项
- 批量设置奖项
- 自动设置奖项(按分数排名)
```typescript
// 自动设置奖项 DTO
export class AutoSetAwardsDto {
@IsInt()
contestId: number;
@IsInt()
firstPrizeCount: number; // 一等奖数量
@IsInt()
secondPrizeCount: number; // 二等奖数量
@IsInt()
thirdPrizeCount: number; // 三等奖数量
}
```
---
### 4. 学校管理模块 (`school/`)
**模块结构:**
```
school/
├── schools/ # 学校信息
├── grades/ # 年级管理
├── classes/ # 班级管理
├── departments/ # 部门管理
├── teachers/ # 教师管理
└── students/ # 学生管理
```
**数据库关系:**
```
Tenant (1) ── (1) School
Tenant (1) ── (N) Grade
Tenant (1) ── (N) Class
Tenant (1) ── (N) Teacher
Tenant (1) ── (N) Student
```
---
### 5. AI 3D 生成模块 (`ai-3d/`) ⭐特色功能
**文件结构:**
```
ai-3d/
├── ai-3d.controller.ts
├── ai-3d.service.ts
├── ai-3d.module.ts
├── providers/
│ ├── ai-3d-provider.interface.ts # AI 提供商接口
│ ├── hunyuan.provider.ts # 腾讯混元实现
│ └── mock.provider.ts # Mock 实现
└── utils/
├── tencent-cloud-sign.ts # 腾讯云签名
└── zip-handler.ts # ZIP 处理
```
**功能:**
- AI 生成 3D 模型(腾讯混元 API 集成)
- 任务创建与状态查询
- 模型文件压缩与下载
**DTO**
```typescript
// create-task.dto.ts
export class CreateTaskDto {
@IsString()
prompt: string; // 生成提示词
@IsString()
@IsOptional()
style?: string; // 风格(写实/卡通/低多边形)
@IsString()
@IsOptional()
negativePrompt?: string; // 负面提示词
}
```
---
### 6. 作业模块 (`homework/`)
**功能:**
- 作业发布与提交
- 作业评审
- 评分管理
**与竞赛模块的区别:**
- 作业更轻量化,适合日常教学场景
- 评审流程简化
- 支持班级维度布置
---
### 7. 系统管理模块
#### 7.1 角色权限 (`roles/`, `permissions/`)
**RBAC 模型:**
```
User ──(N)── UserRole ──(1)── Role
Role ──(N)── RolePermission ──(1)── Permission
```
**权限装饰器:**
```typescript
@RequirePermission('user:create')
async createUser(...) {
// 只有拥有 user:create 权限的用户才能访问
}
```
#### 7.2 菜单管理 (`menus/`)
**功能:**
- 动态菜单配置
- 菜单权限绑定
- 租户菜单定制(`TenantMenu`
#### 7.3 数据字典 (`dict/`)
**功能:**
- 常用数据字典配置
- 支持租户自定义字典
#### 7.4 系统配置 (`config/`)
**功能:**
- 系统参数配置
- 租户级配置隔离
- 配置验证接口
---
### 8. 文件存储模块 (`upload/`, `oss/`)
**腾讯云 COS 集成:**
```typescript
// oss.service.ts
async uploadFile(file: Express.Multer.File, dir: string) {
const client = new COS({
SecretId: this.secretId,
SecretKey: this.secretKey,
});
const key = `${dir}/${Date.now()}-${file.originalname}`;
await client.putObject({
Bucket: this.bucket,
Region: this.region,
Key: key,
Body: file.buffer,
});
return `https://${this.bucket}.cos.${this.region}.myqcloud.com/${key}`;
}
```
---
## 四、数据库设计
### 核心数据表
#### 1. 租户表 (`Tenant`)
```prisma
model Tenant {
id Int @id @default(autoincrement())
name String // 租户名称
code String @unique // 租户编码
domain String? @unique // 租户域名
description String?
isSuper Int @default(0) // 是否超级租户
tenantType String @default("other")
validState Int @default(1)
createTime DateTime @default(now())
modifyTime DateTime @updatedAt
users User[]
roles Role[]
contests Contest[]
school School?
// ...
}
```
#### 2. 用户表 (`User`)
```prisma
model User {
id Int @id @default(autoincrement())
tenantId Int
username String
password String
nickname String?
email String?
phone String?
avatar String?
status String @default("enabled")
validState Int @default(1)
createTime DateTime @default(now())
tenant Tenant @relation(fields: [tenantId], references: [id])
roles UserRole[]
children User[] // 家长 - 孩子关联
parent User? @relation("ParentChild", fields: [parentId], references: [id])
contestRegistrations ContestRegistration[]
contestWorks ContestWork[]
// ...
}
```
#### 3. 活动表 (`Contest`)
```prisma
model Contest {
id Int @id @default(autoincrement())
contestName String
contestType String // 竞赛类型
contestState String @default("unpublished")
status String @default("ongoing")
visibility String @default("designated")
contestTenants String? // JSON: 可见租户 ID 列表
registerStartTime DateTime
registerEndTime DateTime
submitStartTime DateTime
submitEndTime DateTime
reviewStartTime DateTime
reviewEndTime DateTime
resultPublishTime DateTime?
teamMinMembers Int @default(1)
teamMaxMembers Int @default(1)
requireAudit Boolean @default(true)
attachments ContestAttachment[]
registrations ContestRegistration[]
teams ContestTeam[]
works ContestWork[]
judges ContestJudge[]
reviewRule ContestReviewRule?
notices ContestNotice[]
// ...
}
```
#### 4. 作品表 (`ContestWork`)
```prisma
model ContestWork {
id Int @id @default(autoincrement())
contestId Int
userId Int
teamId Int?
title String
description String?
coverUrl String?
workUrl String?
workType String // 作品类型
version Int @default(1)
isLatest Boolean @default(true)
status String @default("submitted")
contest Contest @relation(fields: [contestId], references: [id])
author User @relation(fields: [userId], references: [id])
team ContestTeam?
scores ContestWorkScore[]
attachments ContestWorkAttachment[]
// ...
}
```
#### 5. 评分表 (`ContestWorkScore`)
```prisma
model ContestWorkScore {
id Int @id @default(autoincrement())
workId Int
judgeId Int
contestId Int
score Decimal
comment String?
work ContestWork @relation(fields: [workId], references: [id])
judge User @relation(fields: [judgeId], references: [id])
contest Contest @relation(fields: [contestId], references: [id])
@@unique([workId, judgeId]) // 同一评委对同一作品只能评分一次
}
```
### 完整 ER 图(核心部分)
```
┌─────────────┐
│ Tenant │
│ (租户表) │
└──────┬──────┘
├───► ┌─────────────┐ ┌─────────────┐
│ │ User │──────►│ Role │
│ │ (用户表) │ │ (角色表) │
│ └──────┬──────┘ └─────────────┘
│ │
│ ├───► ┌─────────────┐ ┌─────────────┐
│ │ │ Contest │──────►│ContestWork │
│ │ │ (活动表) │ │ (作品表) │
│ │ └──────┬──────┘ └──────┬──────┘
│ │ │ │
│ │ ├───► ┌─────────────┐ │
│ │ │ │ContestTeam │◄┘
│ │ │ │ (团队表) │
│ │ │ └─────────────┘
│ │ │
│ │ ├───► ┌─────────────┐
│ │ │ │ContestJudge │
│ │ │ │ (评委表) │
│ │ │ └─────────────┘
│ │ │
│ │ └───► ┌─────────────┐
│ │ │ContestWork │
│ │ │ Score │
│ │ │ (评分表) │
│ │ └─────────────┘
│ │
│ └───► ┌─────────────┐
│ │ School │
│ │ (学校表) │
│ └──────┬──────┘
│ │
│ ├───► ┌─────────────┐
│ │ │ Teacher │
│ │ │ (教师表) │
│ │ └─────────────┘
│ │
│ └───► ┌─────────────┐
│ │ Student │
│ │ (学生表) │
│ └─────────────┘
└───► ┌─────────────┐
│ Permission │
│ (权限表) │
└─────────────┘
```
---
## 五、核心业务流程
### 1. 活动创建流程
```
1. 管理员创建活动
2. 配置报名参数(时间、人数限制、需审核等)
3. 配置评审规则(评分维度、权重)
4. 设置可见范围(公开/指定租户)
5. 发布活动
```
### 2. 用户参赛流程
```
1. 用户查看活动列表
2. 个人报名 / 创建团队
3. 等待审核(如需)
4. 提交作品(可多次提交,保留最新版本)
5. 等待评审
6. 查看结果
```
### 3. 作品评审流程
```
1. 系统/管理员分配评委到作品
2. 评委登录系统查看分配的作品
3. 评委评分(填写分数 + 评语)
4. 系统统计总分/平均分
5. 自动生成获奖名单
```
### 4. AI 3D 生成流程
```
1. 用户提交生成请求(提示词)
2. 系统调用腾讯混元 API
3. 轮询任务状态
4. 下载生成的 3D 模型
5. 可选:压缩为 ZIP 格式
```
---
## 六、多租户架构
### 租户类型
| 类型 | 说明 | 权限 |
|------|------|------|
| **超级租户** | 平台管理员 | 访问所有数据 |
| **机构租户** | 学校/图书馆等 | 访问本机构数据 |
| **评委租户** | 评委专用 | 访问评审相关数据 |
| **公共租户** | 公共资源 | 访问公开数据 |
### 数据隔离实现
```typescript
// 所有业务查询必须包含租户 ID
const contests = await prisma.contest.findMany({
where: {
tenantId: currentTenantId,
validState: 1,
},
});
// 超级租户跨租户查询
const allContests = await prisma.contest.findMany({
where: isSuperTenant
? { validState: 1 } // 超管查询全部
: { tenantId: currentTenantId, validState: 1 }, // 普通租户仅查询本租户
});
```
---
## 七、中间件与拦截器
### 1. JWT 认证守卫 (`JwtAuthGuard`)
```typescript
@Injectable()
export class JwtAuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) throw new UnauthorizedException();
try {
const payload = this.jwtService.verify(token);
request['user'] = payload;
} catch {
throw new UnauthorizedException();
}
return true;
}
}
```
### 2. 权限守卫 (`PermissionsGuard`)
```typescript
@Injectable()
export class PermissionsGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const requiredPermission = this.reflector.get<string[]>(
REQUIRE_PERMISSION_KEY,
context.getHandler()
);
if (!requiredPermission) return true;
const { user } = context.switchToHttp().getRequest();
const permissions = await this.authService.getUserPermissions(user.sub);
return permissions.some(p => requiredPermission.includes(p));
}
}
```
### 3. 响应转换拦截器 (`TransformInterceptor`)
```typescript
@Injectable()
export class TransformInterceptor<T>
implements NestInterceptor<T, Response<T>> {
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<Response<T>> {
return next.handle().pipe(
map(data => ({
code: 200,
message: '操作成功',
data: data.data || data,
total: data.total,
})),
);
}
}
```
---
## 八、初始化脚本
项目提供了丰富的初始化脚本:
```bash
# 初始化管理员账户
pnpm init:admin
# 初始化菜单
pnpm init:menus
# 初始化超级租户
pnpm init:super-tenant
# 初始化租户管理员
pnpm init:tenant-admin
# 初始化角色权限
pnpm init:roles:all
# 数据库迁移
pnpm prisma:migrate
# Prisma Studio数据库可视化
pnpm prisma:studio
```
---
## 九、API 接口概览
### 认证相关
| 接口 | 方法 | 说明 |
|------|------|------|
| `POST /auth/login` | POST | 用户登录 |
| `POST /auth/logout` | POST | 用户登出 |
| `GET /auth/info` | GET | 获取当前用户信息 |
### 用户管理
| 接口 | 方法 | 说明 |
|------|------|------|
| `GET /users` | GET | 用户列表 |
| `POST /users` | POST | 创建用户 |
| `GET /users/:id` | GET | 用户详情 |
| `PUT /users/:id` | PUT | 更新用户 |
| `PUT /users/:id/status` | PUT | 切换状态 |
| `DELETE /users/:id` | DELETE | 删除用户 |
### 活动管理
| 接口 | 方法 | 说明 |
|------|------|------|
| `GET /contests` | GET | 活动列表 |
| `POST /contests` | POST | 创建活动 |
| `GET /contests/:id` | GET | 活动详情 |
| `PUT /contests/:id` | PUT | 更新活动 |
| `POST /contests/:id/publish` | POST | 发布/取消发布 |
| `DELETE /contests/:id` | DELETE | 删除活动 |
### 报名管理
| 接口 | 方法 | 说明 |
|------|------|------|
| `GET /contests/:contestId/registrations` | GET | 报名列表 |
| `POST /contests/:contestId/registrations` | POST | 创建报名 |
| `PUT /registrations/:id/review` | PUT | 审核报名 |
### 作品管理
| 接口 | 方法 | 说明 |
|------|------|------|
| `GET /contests/:contestId/works` | GET | 作品列表 |
| `POST /contests/:contestId/works` | POST | 提交作品 |
| `PUT /works/:id` | PUT | 更新作品 |
### 评审管理
| 接口 | 方法 | 说明 |
|------|------|------|
| `POST /contests/:contestId/reviews/assign` | POST | 分配评审 |
| `POST /reviews/scores` | POST | 提交评分 |
| `GET /contests/:contestId/reviews/stats` | GET | 评审统计 |
### 结果管理
| 接口 | 方法 | 说明 |
|------|------|------|
| `GET /contests/:contestId/results` | GET | 结果列表 |
| `POST /contests/:contestId/results/set-awards` | POST | 设置奖项 |
| `POST /contests/:contestId/results/auto-set-awards` | POST | 自动设置奖项 |
### AI 3D 生成
| 接口 | 方法 | 说明 |
|------|------|------|
| `POST /ai-3d/tasks` | POST | 创建生成任务 |
| `GET /ai-3d/tasks/:id` | GET | 查询任务状态 |
---
## 十、总结
### 项目特点
1. **完整的多租户 SaaS 架构** - 支持平台、机构、评委等多角色
2. **丰富的竞赛管理功能** - 从活动创建到评审颁奖全流程覆盖
3. **灵活的 RBAC 权限系统** - 支持细粒度权限控制
4. **AI 能力集成** - 腾讯混元 3D 模型生成
5. **完善的初始化脚本** - 快速部署和配置
### 适用场景
- 📚 图书馆绘本创作比赛
- 🏫 学校各类竞赛活动
- 🎨 艺术创作比赛
- 📖 作文/阅读比赛
- 🤖 科技创新大赛
### 技术亮点
- NestJS 模块化架构
- Prisma ORM 类型安全
- JWT 无状态认证
- 事务处理保证数据一致性
- 软删除保留数据追溯
---
> 文档生成时间2026-03-28
> 分析人AI Assistant

View File

@ -118,7 +118,7 @@ const isDev = import.meta.env.DEV
// //
const tenantTabs = [ const tenantTabs = [
{ code: "super", name: "平台超管", icon: SafetyOutlined, username: "admin", password: "admin@super" }, { code: "super", name: "平台超管", icon: SafetyOutlined, username: "admin", password: "admin123" },
{ code: "gdlib", name: "广东省图", icon: BankOutlined, username: "admin", password: "admin@gdlib" }, { code: "gdlib", name: "广东省图", icon: BankOutlined, username: "admin", password: "admin@gdlib" },
{ code: "judge", name: "评委端", icon: TrophyOutlined, username: "admin", password: "admin@judge" }, { code: "judge", name: "评委端", icon: TrophyOutlined, username: "admin", password: "admin@judge" },
] ]

View File

@ -0,0 +1,897 @@
# Vue 3 前端项目分析报告
> 项目路径:`frontend/`
> 分析日期2026-03-28
> 项目类型:多租户竞赛/活动管理系统前端
---
## 一、技术栈概览
| 组件 | 技术选型 | 版本 |
|------|----------|------|
| **框架** | Vue 3 | 3.4.21 |
| **构建工具** | Vite | 5.1.6 |
| **语言** | TypeScript | 5.4.3 |
| **状态管理** | Pinia | 2.1.7 |
| **路由** | Vue Router | 4.3.0 |
| **UI 组件库** | Ant Design Vue | 4.1.1 |
| **图标** | @ant-design/icons-vue | 7.0.1 |
| **HTTP 客户端** | Axios | 1.6.7 |
| **表单验证** | Vee Validate + Zod | 4.12.4 / 3.22.4 |
| **富文本编辑器** | WangEditor | 5.1.12 |
| **3D 渲染** | Three.js | 0.182.0 |
| **样式** | Tailwind CSS + SCSS | 3.4.1 |
| **日期处理** | Day.js | 1.11.10 |
### 完整依赖列表
```json
{
"dependencies": {
"@ant-design/icons-vue": "^7.0.1",
"@vee-validate/zod": "^4.12.4",
"@wangeditor/editor": "^5.1.23",
"@wangeditor-editor-for-vue": "^5.1.12",
"ant-design-vue": "^4.1.1",
"axios": "^1.6.7",
"dayjs": "^1.11.10",
"pinia": "^2.1.7",
"three": "^0.182.0",
"vee-validate": "^4.12.4",
"vue": "^3.4.21",
"vue-router": "^4.3.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.4",
"autoprefixer": "^10.4.18",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.22.0",
"postcss": "^8.4.35",
"sass": "^1.71.1",
"tailwindcss": "^3.4.1",
"typescript": "^5.4.3",
"vite": "^5.1.6",
"vue-tsc": "^3.2.2"
}
}
```
---
## 二、项目架构
### 目录结构
```
frontend/
├── src/
│ ├── api/ # API 接口层
│ │ ├── ai-3d.ts # AI 3D 任务接口
│ │ ├── auth.ts # 认证接口
│ │ ├── classes.ts # 班级管理接口
│ │ ├── config.ts # 系统配置接口
│ │ ├── contests.ts # 竞赛管理接口(核心)
│ │ ├── departments.ts # 部门管理接口
│ │ ├── dict.ts # 数据字典接口
│ │ ├── grades.ts # 年级管理接口
│ │ ├── homework.ts # 作业管理接口
│ │ ├── judges-management.ts # 评委管理接口
│ │ ├── logs.ts # 日志管理接口
│ │ ├── menus.ts # 菜单管理接口
│ │ ├── permissions.ts # 权限管理接口
│ │ ├── preset-comments.ts # 预设评语接口
│ │ ├── public.ts # 公共接口
│ │ ├── roles.ts # 角色管理接口
│ │ ├── schools.ts # 学校管理接口
│ │ ├── students.ts # 学生管理接口
│ │ ├── teachers.ts # 教师管理接口
│ │ ├── tenants.ts # 租户管理接口
│ │ ├── upload.ts # 文件上传接口
│ │ └── users.ts # 用户管理接口
│ │
│ ├── components/ # 公共组件
│ │ ├── ModelViewer.vue # 3D 模型预览组件
│ │ └── RichTextEditor.vue # 富文本编辑器组件
│ │
│ ├── composables/ # 组合式函数
│ │ ├── useListRequest.ts # 列表请求 Hook
│ │ └── useSimpleListRequest.ts # 简单列表请求 Hook
│ │
│ ├── directives/ # 自定义指令
│ │ └── permission.ts # 权限指令
│ │
│ ├── layouts/ # 布局组件
│ │ ├── BasicLayout.vue # 管理端基础布局
│ │ ├── EmptyLayout.vue # 空白布局
│ │ └── PublicLayout.vue # 公众端布局
│ │
│ ├── router/ # 路由配置
│ │ └── index.ts # 路由主文件(含动态路由)
│ │
│ ├── stores/ # Pinia 状态管理
│ │ └── auth.ts # 认证状态
│ │
│ ├── types/ # TypeScript 类型定义
│ │ ├── api.ts # API 通用类型
│ │ ├── auth.ts # 认证相关类型
│ │ └── router.ts # 路由扩展类型
│ │
│ ├── utils/ # 工具函数
│ │ ├── auth.ts # 认证工具
│ │ ├── avatar.ts # 头像工具
│ │ ├── menu.ts # 菜单工具
│ │ └── request.ts # Axios 封装
│ │
│ └── views/ # 页面组件
│ ├── activities/ # 活动评审页面
│ ├── auth/ # 认证页面
│ ├── content/ # 内容管理页面
│ ├── contests/ # 竞赛管理页面(核心)
│ ├── error/ # 错误页面
│ ├── homework/ # 作业管理页面
│ ├── model/ # 3D 模型页面
│ ├── public/ # 公众端页面
│ ├── school/ # 学校管理页面
│ ├── system/ # 系统管理页面
│ └── workbench/ # 工作台页面
├── scripts/ # 构建脚本
└── package.json
```
---
## 三、核心功能模块详解
### 1. 认证授权模块 (`auth/`)
**文件结构:**
```
src/stores/auth.ts # Pinia Store
src/api/auth.ts # API 接口
src/views/auth/Login.vue # 登录页面
src/utils/auth.ts # 认证工具
```
**核心功能:**
- JWT Token 认证
- 用户信息获取
- 动态菜单加载
- 权限/角色判断
- 租户编码管理
**核心代码示例:**
```typescript
// src/stores/auth.ts
export const useAuthStore = defineStore("auth", () => {
const user = ref<User | null>(null)
const token = ref<string>(getToken() || "")
const menus = ref<Menu[]>([])
const isAuthenticated = computed(() => !!token.value)
// 检查是否为超级管理员
const isSuperAdmin = (): boolean => {
return user.value?.roles?.includes('super_admin') ?? false
}
// 检查是否有指定权限(超管自动拥有所有权限)
const hasPermission = (permission: string): boolean => {
if (isSuperAdmin()) return true
return user.value?.permissions?.includes(permission) ?? false
}
const login = async (form: LoginForm) => {
const response = await authApi.login(form)
token.value = response.token
user.value = response.user
await fetchUserMenus() // 获取用户菜单
return response
}
// ...
})
```
---
### 2. 路由系统
**核心特性:**
- 基于动态菜单的路由加载
- 租户编码路径前缀(`/:tenantCode/`
- 权限守卫
- 公众端与管理端分离
**路由结构:**
```typescript
// 基础路由
const baseRoutes = [
// 管理端登录
{ path: "/:tenantCode/login", component: Login },
// 公众端路由(/p 前缀)
{
path: "/p",
children: [
{ path: "gallery", name: "PublicGallery" }, // 作品广场
{ path: "activities", name: "PublicActivities" }, // 活动大厅
{ path: "mine", name: "PublicMine" }, // 个人中心
{ path: "create", name: "PublicCreate" }, // 绘本创作
]
},
// 管理端路由(动态加载)
{
path: "/:tenantCode",
name: "Main",
component: BasicLayout,
children: [
// 动态路由将通过菜单 API 添加
]
}
]
```
**路由守卫逻辑:**
```typescript
router.beforeEach(async (to, _from, next) => {
// 公众端路由直接放行
if (to.path.startsWith("/p/")) {
next()
return
}
const authStore = useAuthStore()
// 未登录跳转登录页
if (!authStore.token && to.meta.requiresAuth !== false) {
next({ name: "Login", query: { redirect: to.fullPath } })
return
}
// 已登录自动获取用户信息和菜单
if (authStore.token && !authStore.user) {
await authStore.fetchUserInfo()
await addDynamicRoutes() // 添加动态路由
}
// 权限检查
const requiredPermissions = to.meta.permissions
if (requiredPermissions && !authStore.hasAnyPermission(requiredPermissions)) {
next({ name: "Forbidden" })
return
}
next()
})
```
---
### 3. 竞赛管理模块 (`contests/`) ⭐核心业务
**页面结构:**
```
contests/
├── Index.vue # 活动列表页
├── Create.vue # 创建/编辑活动
├── Detail.vue # 活动详情页
├── SuperDetail.vue # 超管活动详情页
├── RegisterIndividual.vue # 个人报名页
├── RegisterTeam.vue # 团队报名页
├── components/
│ ├── AddJudgeDrawer.vue # 添加评委抽屉
│ ├── AddParticipantDrawer.vue # 添加参与者抽屉
│ ├── AddTeacherDrawer.vue # 添加指导老师抽屉
│ ├── SubmitWorkDrawer.vue # 提交作品抽屉
│ ├── ViewWorkDrawer.vue # 查看作品抽屉
│ └── WorkDetailModal.vue # 作品详情弹窗
├── judges/
│ └── Index.vue # 评委管理
├── notices/
│ └── Index.vue # 公告管理
├── registrations/
│ ├── Index.vue # 报名管理列表
│ └── Records.vue # 报名记录
├── reviews/
│ ├── Index.vue # 评审管理
│ ├── Progress.vue # 评审进度
│ ├── ProgressDetail.vue # 评审进度详情
│ └── Tasks.vue # 评审任务
├── results/
│ ├── Index.vue # 成果管理列表
│ └── Detail.vue # 成果发布详情
├── works/
│ ├── Index.vue # 作品管理列表
│ └── WorksDetail.vue # 参赛作品详情
└── Guidance.vue # 我的指导(教师视角)
```
#### 3.1 活动列表页 (`Index.vue`)
**功能:**
- 活动列表展示(分页)
- 活动阶段筛选(未发布/报名中/提交中/评审中/已完结)
- 活动统计卡片
- 创建活动入口
- 活动状态管理(发布/完结/重新开启)
#### 3.2 创建活动页 (`Create.vue`)
**表单配置:**
```typescript
interface CreateContestForm {
contestName: string; // 活动名称
contestType: "individual" | "team"; // 活动类型
startTime: string; // 活动开始时间
endTime: string; // 活动结束时间
// 报名配置
registerStartTime: string; // 报名开始时间
registerEndTime: string; // 报名结束时间
requireAudit: boolean; // 是否需要审核
teamMinMembers?: number; // 团队最少人数
teamMaxMembers?: number; // 团队最多人数
// 作品配置
submitStartTime: string; // 作品提交开始时间
submitEndTime: string; // 作品提交结束时间
submitRule: "once" | "resubmit"; // 提交规则
workType: "image" | "video" | "document" | "code" | "other";
// 评审配置
reviewStartTime: string; // 评审开始时间
reviewEndTime: string; // 评审结束时间
reviewRuleId?: number; // 评审规则 ID
// 成果配置
resultPublishTime?: string; // 结果发布时间
}
```
#### 3.3 报名管理模块
**功能:**
- 报名列表(支持按状态筛选)
- 报名审核(通过/拒绝)
- 报名统计(待审核/已通过/已拒绝)
- 添加指导老师
- 报名记录查看
#### 3.4 评审管理模块
**功能:**
- 评委分配(手动/批量/自动)
- 评审进度监控
- 作品评分界面
- 评审统计(评委进度/作品状态)
**评审进度数据结构:**
```typescript
interface ReviewProgress {
contest: { /* 活动信息 */ };
summary: {
totalWorks: number; // 总作品数
assignedWorksCount: number; // 已分配作品数
scoredWorksCount: number; // 已评分作品数
totalJudges: number; // 评委总数
};
progress: {
assignmentProgress: number; // 分配进度百分比
scoringProgress: number; // 评分进度百分比
overallProgress: number; // 整体进度百分比
};
judgeProgress: JudgeProgressItem[]; // 评委进度列表
unassignedWorks: UnassignedWork[]; // 未分配作品列表
}
```
#### 3.5 成果管理模块
**功能:**
- 计算最终得分(支持多种计算规则)
- 自动排名
- 设置奖项(单个/批量/自动)
- 成果发布/撤回
**API 示例:**
```typescript
// 自动设置奖项
resultsApi.autoSetAwards(contestId, {
first: 3, // 一等奖 3 名
second: 6, // 二等奖 6 名
third: 10, // 三等奖 10 名
excellent: 20 // 优秀奖 20 名
})
// 批量设置奖项
resultsApi.batchSetAwards(contestId, {
awards: [
{ workId: 1, awardLevel: 'first' },
{ workId: 2, awardLevel: 'second' },
// ...
]
})
```
---
### 4. 用户管理模块 (`system/users/`)
**页面:** `src/views/system/users/Index.vue`
**功能:**
- 用户列表(分页 + 搜索)
- 用户类型筛选(平台/机构/评委/公共)
- 用户统计卡片
- 创建/编辑用户
- 用户状态切换(启用/禁用)
- 角色分配
**查询参数:**
```typescript
interface UserQueryParams {
page?: number;
pageSize?: number;
keyword?: string; // 关键字搜索
userType?: "platform" | "org" | "judge" | "public";
filterTenantId?: number; // 按租户筛选
userSource?: "admin_created" | "self_registered";
status?: "enabled" | "disabled";
}
```
**用户统计接口:**
```typescript
interface UserStats {
total: number; // 总用户数
platform: number; // 平台用户
org: number; // 机构用户
judge: number; // 评委用户
public: number; // 公共用户
}
```
---
### 5. AI 3D 创作模块 (`workbench/ai-3d/`) ⭐特色功能
**文件结构:**
```
workbench/ai-3d/
├── Index.vue # 3D 建模实验室首页
├── Generate.vue # 模型生成页面
└── History.vue # 创作历史
```
**功能:**
- AI 文生 3D 模型(腾讯混元 API
- 生成类型选择Normal/LowPoly/Geometry/Sketch
- 任务状态轮询
- 多结果展示4 个不同角度)
- 模型下载
- 创作历史记录
**任务状态:**
```typescript
type AI3DTaskStatus = "pending" | "processing" | "completed" | "failed" | "timeout";
interface AI3DTask {
id: number;
inputType: "text" | "image";
inputContent: string;
status: AI3DTaskStatus;
resultUrls?: string[]; // 多结果 URL
previewUrls?: string[]; // 预览图 URL
queuePosition?: number; // 队列位置
createTime: string;
completeTime?: string;
}
```
**生成参数:**
```typescript
interface CreateAI3DTaskParams {
inputType: "text" | "image";
inputContent: string;
generateType?: "Normal" | "LowPoly" | "Geometry" | "Sketch";
faceCount?: number; // 模型面数 (10000-1500000)
}
```
---
### 6. 学校管理模块 (`school/`)
**子模块:**
```
school/
├── schools/ # 学校信息
├── grades/ # 年级管理
├── classes/ # 班级管理
├── teachers/ # 教师管理
└── students/ # 学生管理
```
**功能:**
- 学校信息 CRUD
- 年级管理(支持排序)
- 班级管理(关联年级)
- 教师列表(支持搜索)
- 学生列表(支持按班级筛选)
---
### 7. 系统管理模块 (`system/`)
**子模块:**
| 页面 | 功能 |
|------|------|
| `users/` | 用户管理 |
| `roles/` | 角色管理 |
| `permissions/` | 权限管理 |
| `menus/` | 菜单管理 |
| `tenants/` | 租户管理 |
| `dict/` | 数据字典 |
| `config/` | 系统配置 |
| `logs/` | 系统日志 |
---
### 8. 公众端模块 (`public/`)
**页面结构:**
```
public/
├── Login.vue # 公众端登录
├── Gallery.vue # 作品广场
├── Activities.vue # 活动大厅
├── ActivityDetail.vue # 活动详情
├── mine/
│ ├── Index.vue # 个人中心
│ ├── Registrations.vue # 我的报名
│ ├── Works.vue # 我的作品
│ └── Children.vue # 子女账号
├── create/
│ ├── Index.vue # 绘本创作首页
│ └── Generating.vue # 生成中页面
├── works/
│ ├── Index.vue # 我的作品库
│ ├── Detail.vue # 作品详情
│ └── Publish.vue # 发布作品
└── components/
└── WorkSelector.vue # 作品选择器
```
**核心功能:**
- 公众端独立登录
- 作品广场展示
- 活动大厅浏览
- 个人中心(报名/作品/子女管理)
- AI 绘本创作
---
### 9. 作业管理模块 (`homework/`)
**页面:**
- `Index.vue` - 作业列表
- `ReviewRules.vue` - 评审规则
- `Submissions.vue` - 提交记录
- `StudentDetail.vue` - 学生作业详情
- `StudentList.vue` - 学生列表
**功能:**
- 作业发布与查看
- 作业提交记录
- 作业评审
- 成绩查看
---
## 四、公共组件
### 1. RichTextEditor.vue - 富文本编辑器
**功能:**
- 基于 WangEditor
- 支持图文混排
- 支持文件上传
- 表单验证集成
**使用示例:**
```vue
<template>
<RichTextEditor v-model="content" :height="400" />
</template>
```
### 2. ModelViewer.vue - 3D 模型预览
**功能:**
- 基于 Three.js
- 支持 GLB/GLTF 格式
- 轨道控制器(旋转/缩放/平移)
- 自动加载/卸载
---
## 五、API 封装
### Axios 封装 (`utils/request.ts`)
**核心特性:**
- 请求拦截器(自动添加 Token
- 响应拦截器(统一错误处理)
- 多租户支持
- TypeScript 泛型支持
**请求配置:**
```typescript
const request = axios.create({
baseURL: '/api',
timeout: 30000,
})
// 请求拦截器
request.interceptors.request.use(config => {
const token = getToken()
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// 响应拦截器
request.interceptors.response.use(
response => response.data,
error => {
if (error.response?.status === 401) {
// Token 过期,跳转登录
}
return Promise.reject(error)
}
)
```
### API 类型定义
```typescript
// types/api.ts
export interface PaginationParams {
page?: number
pageSize?: number
}
export interface PaginationResponse<T> {
list: T[]
total: number
page: number
pageSize: number
}
```
---
## 六、状态管理
### Auth Store (`stores/auth.ts`)
**State**
```typescript
{
user: User | null, // 用户信息
token: string, // JWT Token
menus: Menu[], // 用户菜单
loading: boolean // 加载状态
}
```
**Getters**
```typescript
{
isAuthenticated: boolean, // 是否已登录
tenantCode: string, // 租户编码
isSuperAdmin: () => boolean, // 是否超管
hasPermission: (p: string) => boolean, // 权限检查
hasAnyRole: (roles: string[]) => boolean // 角色检查
}
```
**Actions**
```typescript
{
login: (form: LoginForm) => Promise<LoginResponse>,
logout: () => Promise<void>,
fetchUserInfo: () => Promise<User>,
fetchUserMenus: () => Promise<Menu[]>,
updateToken: (newToken: string) => void,
initAuth: () => Promise<void>,
}
```
---
## 七、组合式函数 (Composables)
### useListRequest
**用途:** 列表数据请求通用逻辑
**使用示例:**
```typescript
const {
loading,
list,
total,
page,
pageSize,
fetchList,
handlePageChange,
handleSizeChange,
} = useListRequest(contestsApi.getList, initialParams)
```
### useSimpleListRequest
**用途:** 简化版列表请求(无分页)
---
## 八、自定义指令
### v-permission 权限指令
**使用示例:**
```vue
<template>
<button v-permission="['user:create']">创建用户</button>
<button v-permission="'admin'">超管功能</button>
</template>
```
**实现:**
```typescript
// directives/permission.ts
export const permission = {
mounted(el, binding) {
const { hasPermission } = useAuthStore()
const value = binding.value
const required = Array.isArray(value) ? value : [value]
if (!hasPermission(required)) {
el.parentNode?.removeChild(el)
}
}
}
```
---
## 九、核心业务流程
### 1. 用户登录流程
```
1. 输入用户名密码 →
2. 调用 login API →
3. 存储 Token按租户隔离
4. 获取用户信息 →
5. 获取用户菜单 →
6. 添加动态路由 →
7. 跳转到首页
```
### 2. 活动创建流程
```
1. 填写活动基本信息 →
2. 配置报名参数 →
3. 配置作品参数 →
4. 配置评审规则 →
5. 提交创建 →
6. 跳转到活动详情页
```
### 3. 作品提交流程
```
1. 选择活动 →
2. 点击提交作品 →
3. 填写作品信息(标题/描述) →
4. 上传作品文件 →
5. 上传预览图 →
6. 提交成功 →
7. 可查看/编辑(在截止时间前)
```
### 4. 评审工作流程
```
1. 管理员分配作品给评委 →
2. 评委登录后查看待评审作品 →
3. 查看作品详情 →
4. 按维度评分 →
5. 填写评语 →
6. 提交评分
```
### 5. AI 3D 模型生成流程
```
1. 输入提示词 →
2. 选择生成类型 →
3. 提交任务 →
4. 轮询任务状态 →
5. 生成完成后展示 4 个角度模型 →
6. 下载模型文件
```
---
## 十、总结
### 项目特点
1. **完整的前后端分离架构** - Vue 3 + TypeScript + Vite
2. **动态路由系统** - 基于菜单动态加载路由
3. **多租户支持** - 租户编码路径前缀 + Token 隔离
4. **细粒度权限控制** - 权限指令 + 路由守卫
5. **丰富的竞赛管理功能** - 从创建到评审全流程
6. **AI 能力集成** - 腾讯混元 3D 模型生成
7. **公众端独立入口** - /p 前缀区分
### 适用场景
- 📚 图书馆绘本创作比赛
- 🏫 学校各类竞赛活动
- 🎨 艺术创作比赛
- 📖 作文/阅读比赛
- 🤖 科技创新大赛
### 技术亮点
- Vue 3 Composition API
- TypeScript 类型安全
- Pinia 状态管理
- Ant Design Vue 组件库
- Tailwind CSS 原子化 CSS
- Three.js 3D 渲染
- WangEditor 富文本
---
> 文档生成时间2026-03-28
> 分析人AI Assistant

View File

@ -0,0 +1 @@
lombok.addLombokGeneratedAnnotation = true

View File

@ -23,11 +23,14 @@
<druid.version>1.2.20</druid.version> <druid.version>1.2.20</druid.version>
<mysql.version>8.0.33</mysql.version> <mysql.version>8.0.33</mysql.version>
<mapstruct.version>1.5.5.Final</mapstruct.version> <mapstruct.version>1.5.5.Final</mapstruct.version>
<lombok.version>1.18.30</lombok.version> <lombok.version>1.18.34</lombok.version>
<hutool.version>5.8.26</hutool.version> <hutool.version>5.8.26</hutool.version>
<fastjson2.version>2.0.43</fastjson2.version> <fastjson2.version>2.0.43</fastjson2.version>
<jjwt.version>0.12.3</jjwt.version> <jjwt.version>0.12.3</jjwt.version>
<flyway.version>10.10.0</flyway.version> <flyway.version>10.10.0</flyway.version>
<aliyun.oss.version>3.17.2</aliyun.oss.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties> </properties>
<!-- 添加 Maven 仓库 --> <!-- 添加 Maven 仓库 -->
@ -100,6 +103,13 @@
<version>${flyway.version}</version> <version>${flyway.version}</version>
</dependency> </dependency>
<!-- Flyway MySQL 支持 -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
<version>${flyway.version}</version>
</dependency>
<!-- SpringDoc OpenAPI (API 文档) --> <!-- SpringDoc OpenAPI (API 文档) -->
<dependency> <dependency>
<groupId>org.springdoc</groupId> <groupId>org.springdoc</groupId>
@ -135,6 +145,13 @@
<version>${hutool.version}</version> <version>${hutool.version}</version>
</dependency> </dependency>
<!-- 阿里云 OSS -->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>${aliyun.oss.version}</version>
</dependency>
<!-- FastJSON2 --> <!-- FastJSON2 -->
<dependency> <dependency>
<groupId>com.alibaba.fastjson2</groupId> <groupId>com.alibaba.fastjson2</groupId>
@ -196,14 +213,10 @@
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
<configuration> <configuration>
<source>${java.version}</source> <source>17</source>
<target>${java.version}</target> <target>17</target>
<encoding>UTF-8</encoding>
<annotationProcessorPaths> <annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
<path> <path>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
@ -214,6 +227,11 @@
<artifactId>lombok-mapstruct-binding</artifactId> <artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version> <version>0.2.0</version>
</path> </path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
</annotationProcessorPaths> </annotationProcessorPaths>
</configuration> </configuration>
</plugin> </plugin>

View File

@ -0,0 +1,45 @@
package com.lesingle.creation.common.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 限流注解
* <p>
* 基于 Redis 滑动窗口实现限流
*
* @author lesingle
* @since 1.0.0
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiter {
/**
* 限流 Key
* 支持 SpEL 表达式 "#username"
*/
String key() default "";
/**
* 限流时间窗口
* 默认 60
*/
int time() default 60;
/**
* 时间窗口内最大请求数
* 默认 100
*/
int count() default 100;
/**
* 提示信息
*/
String message() default "请求过于频繁,请稍后再试";
}

View File

@ -0,0 +1,104 @@
package com.lesingle.creation.common.aspect;
import com.lesingle.creation.common.util.TraceIdUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import jakarta.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
* 日志切面
* <p>
* 拦截 Controller 层所有方法记录请求日志
* 包含TraceId请求方法请求路径IP耗时
*
* @author lesingle
* @since 1.0.0
*/
@Slf4j
@Aspect
@Component
public class LogAspect {
/**
* 定义切点拦截 Controller 层所有方法
*/
@Pointcut("execution(* com.lesingle.creation.controller..*.*(..))")
public void controllerPointcut() {
}
/**
* 环绕通知记录请求耗时
*/
@Around("controllerPointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
return joinPoint.proceed();
}
HttpServletRequest request = attributes.getRequest();
String traceId = TraceIdUtil.getTraceId();
String method = request.getMethod();
String uri = request.getRequestURI();
String ip = getIpAddress(request);
long startTime = System.currentTimeMillis();
log.info("【{}】请求开始 | 方法:{} | 路径:{} | IP{}", traceId, method, uri, ip);
Object result;
try {
result = joinPoint.proceed();
long endTime = System.currentTimeMillis();
log.info("【{}】请求成功 | 方法:{} | 路径:{} | 耗时:{}ms",
traceId, method, uri, (endTime - startTime));
} catch (Throwable e) {
long endTime = System.currentTimeMillis();
log.error("【{}】请求异常 | 方法:{} | 路径:{} | 耗时:{}ms | 异常:{}",
traceId, method, uri, (endTime - startTime), e.getMessage(), e);
throw e;
}
return result;
}
/**
* 获取请求 IP 地址
*/
private String getIpAddress(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
// 如果是本地 IPv6转换为 IPv4
if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) {
try {
ip = InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException e) {
log.error("获取 IP 地址失败:{}", e.getMessage());
}
}
}
// 多个代理时取第一个 IP
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
}

View File

@ -0,0 +1,120 @@
package com.lesingle.creation.common.aspect;
import com.lesingle.creation.common.annotation.RateLimiter;
import com.lesingle.creation.common.exception.BusinessException;
import com.lesingle.creation.common.util.RateLimiterUtil;
import com.lesingle.creation.common.util.TraceIdUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import jakarta.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
* 限流切面
* <p>
* 拦截标注了 @RateLimiter 注解的方法实现限流控制
*
* @author lesingle
* @since 1.0.0
*/
@Slf4j
@Aspect
@Component
public class RateLimiterAspect {
private static final ExpressionParser PARSER = new SpelExpressionParser();
/**
* 前置通知执行限流检查
*/
@Before("@annotation(rateLimiter)")
public void before(JoinPoint joinPoint, RateLimiter rateLimiter) {
// 获取注解参数
String keyExpression = rateLimiter.key();
int time = rateLimiter.time();
int count = rateLimiter.count();
String message = rateLimiter.message();
// 解析 Key
String key = parseKey(keyExpression, joinPoint);
// 执行限流检查
boolean allowed = RateLimiterUtil.tryAcquire(key, time, count);
if (!allowed) {
String traceId = TraceIdUtil.getTraceId();
log.warn("【{}】请求被限流 | Key: {} | 限制:{}/{}s", traceId, key, count, time);
throw new BusinessException(message);
}
log.debug("限流检查通过 | Key: {} | 当前计数:{}", key, RateLimiterUtil.getCurrentCount(key));
}
/**
* 解析限流 Key
* 支持 SpEL 表达式
*/
private String parseKey(String keyExpression, JoinPoint joinPoint) {
if (keyExpression.isEmpty()) {
// 默认使用 IP 地址作为 key
return "rate_limit:" + getIpAddress();
}
// 解析 SpEL 表达式
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Object[] args = joinPoint.getArgs();
String[] paramNames = signature.getParameterNames();
StandardEvaluationContext context = new StandardEvaluationContext();
for (int i = 0; i < args.length; i++) {
context.setVariable(paramNames[i], args[i]);
}
try {
Expression expression = PARSER.parseExpression(keyExpression);
Object value = expression.getValue(context);
return "rate_limit:" + (value != null ? value.toString() : "unknown");
} catch (Exception e) {
log.error("解析限流 Key 失败:{}", keyExpression, e);
return "rate_limit:error";
}
}
/**
* 获取请求 IP 地址
*/
private String getIpAddress() {
try {
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) {
ip = InetAddress.getLocalHost().getHostAddress();
}
return ip;
}
} catch (UnknownHostException e) {
log.error("获取 IP 地址失败:{}", e.getMessage());
}
return "unknown";
}
}

View File

@ -0,0 +1,48 @@
package com.lesingle.creation.common.aspect;
import com.lesingle.creation.common.util.TraceIdUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
/**
* TraceId 日志链路追踪切面
* <p>
* Controller 层拦截请求生成 TraceId 并放入 MDC实现全链路日志追踪
*
* @author lesingle
* @since 1.0.0
*/
@Slf4j
@Aspect
@Component
public class TraceIdAspect {
/**
* 环绕通知拦截 Controller 层所有方法
*/
@Around("execution(* com.lesingle.creation.controller..*.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
try {
// 生成 TraceId 并放入 MDC
TraceIdUtil.setTraceId();
log.debug("请求开始TraceId: {}", TraceIdUtil.getTraceId());
// 执行目标方法
Object result = joinPoint.proceed();
long costTime = System.currentTimeMillis() - startTime;
log.debug("请求结束TraceId: {}, 耗时:{}ms", TraceIdUtil.getTraceId(), costTime);
return result;
} finally {
// 清除 MDC防止内存泄漏
TraceIdUtil.clear();
}
}
}

View File

@ -0,0 +1,59 @@
package com.lesingle.creation.common.base;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 实体基类
* <p>
* 包含基础字段idcreateBycreateTimeupdateByupdateTimedeleted
* 所有实体类应继承此类
*
* @author lesingle
* @since 1.0.0
*/
@Data
public abstract class BaseEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键 ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 创建人账号
*/
@TableField(fill = FieldFill.INSERT)
private String createBy;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新人账号
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateBy;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 逻辑删除标识0-未删除1-已删除
*/
@TableLogic
private Integer deleted;
}

View File

@ -1,15 +1,11 @@
package com.lesingle.creation.common.config; package com.lesingle.creation.common.config;
import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import java.time.LocalDateTime;
/** /**
* MyBatis-Plus 配置类 * MyBatis-Plus 配置类
* *
@ -29,26 +25,4 @@ public class MybatisPlusConfig {
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor; return interceptor;
} }
/**
* 自动填充处理器
* 用于自动填充创建时间更新时间等字段
*/
@Bean
public MetaObjectHandler metaObjectHandler() {
return new MetaObjectHandler() {
@Override
public void insertFill(MetaObject metaObject) {
// 插入时自动填充 createTime updateTime
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
@Override
public void updateFill(MetaObject metaObject) {
// 更新时自动填充 updateTime
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
};
}
} }

View File

@ -0,0 +1,51 @@
package com.lesingle.creation.common.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 阿里云 OSS 配置属性
*
* @author lesingle
* @since 1.0.0
*/
@Data
@Component
@ConfigurationProperties(prefix = "aliyun.oss")
public class OssProperties {
/**
* OSS Endpoint
* oss-cn-hangzhou.aliyuncs.com
*/
private String endpoint;
/**
* Access Key ID
*/
private String accessKeyId;
/**
* Access Key Secret
*/
private String accessKeySecret;
/**
* Bucket 名称
*/
private String bucketName;
/**
* 自定义域名可选
* https://cdn.example.com
*/
private String customDomain;
/**
* 文件上传根目录
* uploads/
*/
private String rootDir = "";
}

View File

@ -0,0 +1,92 @@
package com.lesingle.creation.common.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
/**
* Redis 配置类
* <p>
* 配置 RedisTemplate CacheManager
*
* @author lesingle
* @since 1.0.0
*/
@Configuration
@EnableCaching
public class RedisConfig {
/**
* 配置 RedisTemplate
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 配置 JSON 序列化器
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// String 序列化器
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// Key 采用 String 序列化
template.setKeySerializer(stringRedisSerializer);
template.setHashKeySerializer(stringRedisSerializer);
// Value 采用 JSON 序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
/**
* 配置缓存管理器
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
// JSON 序列化器
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 缓存配置
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
// 缓存过期时间默认 30 分钟
.entryTtl(Duration.ofMinutes(30))
// Key 前缀
.prefixCacheNameWith("lesingle:")
// 禁用空值缓存
.disableCachingNullValues()
// 序列化配置
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
}
}

View File

@ -14,7 +14,7 @@ import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@ -31,11 +31,14 @@ import java.util.List;
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@EnableMethodSecurity @EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig { public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter; private final JwtAuthenticationFilter jwtAuthenticationFilter;
public SecurityConfig(@org.springframework.context.annotation.Lazy JwtAuthenticationFilter jwtAuthenticationFilter) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
}
/** /**
* 密码编码器 * 密码编码器
*/ */
@ -72,15 +75,19 @@ public class SecurityConfig {
.requestMatchers("/favicon.ico", "/error").permitAll() .requestMatchers("/favicon.ico", "/error").permitAll()
// 放行 API 文档 // 放行 API 文档
.requestMatchers("/doc.html", "/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**").permitAll() .requestMatchers("/doc.html", "/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**").permitAll()
// 放行登录注册接口 // 放行登录注册接口支持 Vite 代理后/auth/** 和直接访问/api/auth/**
.requestMatchers("/api/auth/**").permitAll() .requestMatchers("/auth/**", "/api/auth/**").permitAll()
// 放行菜单接口支持 Vite 代理后/menu/** 和直接访问/api/menu/**
.requestMatchers("/menu/**", "/api/menu/**").permitAll()
// 放行公开用户接口支持 Vite 代理后/public/** 和直接访问/api/public/**
.requestMatchers("/public/**", "/api/public/**").permitAll()
// 其他请求需要认证 // 其他请求需要认证
.anyRequest().authenticated() .anyRequest().authenticated()
) )
// 配置跨域 // 配置跨域
.cors(cors -> cors.configurationSource(corsConfigurationSource())) .cors(cors -> cors.configurationSource(corsConfigurationSource()))
// 添加 JWT 过滤器 // 添加 JWT 过滤器 BasicAuthenticationFilter 之前
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); .addFilterBefore(jwtAuthenticationFilter, BasicAuthenticationFilter.class);
return http.build(); return http.build();
} }

View File

@ -58,7 +58,55 @@ public enum ErrorCode {
DB_QUERY_ERROR(2003, "数据库查询失败"), DB_QUERY_ERROR(2003, "数据库查询失败"),
DB_UPDATE_ERROR(2004, "数据库更新失败"), DB_UPDATE_ERROR(2004, "数据库更新失败"),
DB_DELETE_ERROR(2005, "数据库删除失败"), DB_DELETE_ERROR(2005, "数据库删除失败"),
DB_INSERT_ERROR(2006, "数据库插入失败"); DB_INSERT_ERROR(2006, "数据库插入失败"),
// ========== 竞赛业务错误3xxx ==========
CONTEST_NOT_FOUND(3001, "竞赛不存在"),
CONTEST_ALREADY_PUBLISHED(3002, "竞赛已发布"),
CONTEST_NOT_STARTED(3003, "竞赛未开始"),
CONTEST_ALREADY_FINISHED(3004, "竞赛已结束"),
CONTEST_NAME_DUPLICATE(3005, "竞赛名称已存在"),
REGISTRATION_NOT_FOUND(3101, "报名记录不存在"),
REGISTRATION_ALREADY_EXISTS(3102, "已报名,不能重复报名"),
REGISTRATION_AUDIT_PENDING(3103, "报名待审核"),
REGISTRATION_CLOSED(3104, "报名已截止"),
REGISTRATION_NOT_ALLOWED(3105, "不允许报名"),
WORK_NOT_FOUND(3201, "作品不存在"),
WORK_SUBMISSION_CLOSED(3202, "作品提交已截止"),
WORK_ALREADY_SUBMITTED(3203, "作品已提交"),
WORK_NOT_LATEST_VERSION(3204, "不是最新版本"),
REVIEW_NOT_ASSIGNED(3301, "未分配评审任务"),
REVIEW_ALREADY_SCORED(3302, "已评分,不能重复评分"),
REVIEW_SCORE_INVALID(3303, "评分无效"),
REVIEW_NOT_STARTED(3304, "评审未开始"),
TEAM_NOT_FOUND(3401, "团队不存在"),
TEAM_MEMBER_FULL(3402, "团队成员已满"),
TEAM_MEMBER_DUPLICATE(3403, "用户已在团队中"),
JUDGE_NOT_FOUND(3501, "评委不存在"),
JUDGE_ALREADY_ASSIGNED(3502, "评委已分配"),
AWARD_NOT_FOUND(3601, "奖项不存在"),
AWARD_ALREADY_SET(3602, "奖项已设置"),
NOTICE_NOT_FOUND(3701, "公告不存在"),
// ========== 学校业务错误4xxx ==========
SCHOOL_NOT_FOUND(4001, "学校不存在"),
SCHOOL_CODE_DUPLICATE(4002, "学校编码已存在"),
GRADE_NOT_FOUND(4101, "年级不存在"),
GRADE_DUPLICATE(4102, "年级名称已存在"),
CLASS_NOT_FOUND(4201, "班级不存在"),
CLASS_DUPLICATE(4202, "班级名称已存在"),
TEACHER_NOT_FOUND(4301, "教师不存在"),
STUDENT_NOT_FOUND(4401, "学生不存在");
/** /**
* 错误码 * 错误码

View File

@ -0,0 +1,32 @@
package com.lesingle.creation.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* AI3D 生成类型枚举
*/
@Getter
@AllArgsConstructor
public enum AI3DGenerateTypeEnum {
NORMAL("Normal", "带纹理"),
GEOMETRY("Geometry", "白模"),
LOW_POLY("LowPoly", "低多边形"),
SKETCH("Sketch", "草图");
private final String code;
private final String desc;
public static AI3DGenerateTypeEnum getByCode(String code) {
if (code == null) {
return NORMAL;
}
for (AI3DGenerateTypeEnum type : values()) {
if (type.getCode().equals(code)) {
return type;
}
}
return NORMAL;
}
}

View File

@ -0,0 +1,33 @@
package com.lesingle.creation.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* AI3D 任务状态枚举
*/
@Getter
@AllArgsConstructor
public enum AI3DTaskStatusEnum {
PENDING("pending", "待处理"),
PROCESSING("processing", "处理中"),
COMPLETED("completed", "已完成"),
FAILED("failed", "失败"),
TIMEOUT("timeout", "超时");
private final String code;
private final String desc;
public static AI3DTaskStatusEnum getByCode(String code) {
if (code == null) {
return PENDING;
}
for (AI3DTaskStatusEnum status : values()) {
if (status.getCode().equals(code)) {
return status;
}
}
return PENDING;
}
}

View File

@ -0,0 +1,31 @@
package com.lesingle.creation.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 评审分配状态枚举
*/
@Getter
@AllArgsConstructor
public enum AssignmentStatusEnum {
ASSIGNED("assigned", "已分配"),
REVIEWING("reviewing", "评审中"),
COMPLETED("completed", "已完成");
private final String code;
private final String desc;
public static AssignmentStatusEnum getByCode(String code) {
if (code == null) {
return ASSIGNED;
}
for (AssignmentStatusEnum status : values()) {
if (status.getCode().equals(code)) {
return status;
}
}
return ASSIGNED;
}
}

View File

@ -0,0 +1,33 @@
package com.lesingle.creation.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 奖项等级枚举
*/
@Getter
@AllArgsConstructor
public enum AwardLevelEnum {
FIRST("first", "一等奖"),
SECOND("second", "二等奖"),
THIRD("third", "三等奖"),
EXCELLENT("excellent", "优秀奖"),
NONE("none", "无奖项");
private final String code;
private final String desc;
public static AwardLevelEnum getByCode(String code) {
if (code == null) {
return NONE;
}
for (AwardLevelEnum level : values()) {
if (level.getCode().equals(code)) {
return level;
}
}
return NONE;
}
}

View File

@ -0,0 +1,31 @@
package com.lesingle.creation.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 评论状态枚举
*/
@Getter
@AllArgsConstructor
public enum CommentStatusEnum {
PENDING("pending", "待审核"),
APPROVED("approved", "已通过"),
REJECTED("rejected", "已拒绝");
private final String code;
private final String desc;
public static CommentStatusEnum getByCode(String code) {
if (code == null) {
return PENDING;
}
for (CommentStatusEnum status : values()) {
if (status.getCode().equals(code)) {
return status;
}
}
return PENDING;
}
}

View File

@ -0,0 +1,34 @@
package com.lesingle.creation.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 竞赛状态枚举
*/
@Getter
@AllArgsConstructor
public enum ContestStateEnum {
UNPUBLISHED("unpublished", "未发布"),
PUBLISHED("published", "已发布"),
REGISTERING("registering", "报名中"),
SUBMITTING("submitting", "作品提交中"),
REVIEWING("reviewing", "评审中"),
FINISHED("finished", "已完结");
private final String code;
private final String desc;
public static ContestStateEnum getByCode(String code) {
if (code == null) {
return UNPUBLISHED;
}
for (ContestStateEnum state : values()) {
if (state.getCode().equals(code)) {
return state;
}
}
return UNPUBLISHED;
}
}

View File

@ -0,0 +1,30 @@
package com.lesingle.creation.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 竞赛进度状态枚举
*/
@Getter
@AllArgsConstructor
public enum ContestStatusEnum {
ONGOING("ongoing", "进行中"),
FINISHED("finished", "已完结");
private final String code;
private final String desc;
public static ContestStatusEnum getByCode(String code) {
if (code == null) {
return ONGOING;
}
for (ContestStatusEnum status : values()) {
if (status.getCode().equals(code)) {
return status;
}
}
return ONGOING;
}
}

View File

@ -0,0 +1,30 @@
package com.lesingle.creation.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 竞赛类型枚举
*/
@Getter
@AllArgsConstructor
public enum ContestTypeEnum {
INDIVIDUAL("individual", "个人赛"),
TEAM("team", "团队赛");
private final String code;
private final String desc;
public static ContestTypeEnum getByCode(String code) {
if (code == null) {
return INDIVIDUAL;
}
for (ContestTypeEnum type : values()) {
if (type.getCode().equals(code)) {
return type;
}
}
return INDIVIDUAL;
}
}

View File

@ -0,0 +1,47 @@
package com.lesingle.creation.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 性别枚举
*/
@Getter
@AllArgsConstructor
public enum GenderEnum {
MALE("male", ""),
FEMALE("female", "");
private final String code;
private final String desc;
public static GenderEnum getByCode(String code) {
if (code == null) {
return null;
}
for (GenderEnum gender : values()) {
if (gender.getCode().equals(code)) {
return gender;
}
}
return null;
}
/**
* 根据数字代码获取性别
* @param code 1-2-
* @return 性别枚举
*/
public static GenderEnum getByIntCode(Integer code) {
if (code == null) {
return null;
}
if (code == 1) {
return MALE;
} else if (code == 2) {
return FEMALE;
}
return null;
}
}

View File

@ -0,0 +1,30 @@
package com.lesingle.creation.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 作业状态枚举
*/
@Getter
@AllArgsConstructor
public enum HomeworkStatusEnum {
UNPUBLISHED("unpublished", "未发布"),
PUBLISHED("published", "已发布");
private final String code;
private final String desc;
public static HomeworkStatusEnum getByCode(String code) {
if (code == null) {
return UNPUBLISHED;
}
for (HomeworkStatusEnum status : values()) {
if (status.getCode().equals(code)) {
return status;
}
}
return UNPUBLISHED;
}
}

View File

@ -0,0 +1,31 @@
package com.lesingle.creation.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 作业提交状态枚举
*/
@Getter
@AllArgsConstructor
public enum HomeworkSubmissionStatusEnum {
PENDING("pending", "待评审"),
REVIEWED("reviewed", "已评审"),
REJECTED("rejected", "已拒绝");
private final String code;
private final String desc;
public static HomeworkSubmissionStatusEnum getByCode(String code) {
if (code == null) {
return PENDING;
}
for (HomeworkSubmissionStatusEnum status : values()) {
if (status.getCode().equals(code)) {
return status;
}
}
return PENDING;
}
}

View File

@ -0,0 +1,30 @@
package com.lesingle.creation.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 参与者类型枚举
*/
@Getter
@AllArgsConstructor
public enum ParticipantTypeEnum {
SELF("self", "自己"),
CHILD("child", "代子女");
private final String code;
private final String desc;
public static ParticipantTypeEnum getByCode(String code) {
if (code == null) {
return SELF;
}
for (ParticipantTypeEnum type : values()) {
if (type.getCode().equals(code)) {
return type;
}
}
return SELF;
}
}

View File

@ -0,0 +1,30 @@
package com.lesingle.creation.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 报名任务状态枚举
*/
@Getter
@AllArgsConstructor
public enum RegisterTaskStateEnum {
STARTED("started", "已开始"),
CLOSED("closed", "已截止");
private final String code;
private final String desc;
public static RegisterTaskStateEnum getByCode(String code) {
if (code == null) {
return STARTED;
}
for (RegisterTaskStateEnum state : values()) {
if (state.getCode().equals(code)) {
return state;
}
}
return STARTED;
}
}

View File

@ -0,0 +1,32 @@
package com.lesingle.creation.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 报名状态枚举
*/
@Getter
@AllArgsConstructor
public enum RegistrationStateEnum {
PENDING("pending", "待审核"),
PASSED("passed", "已通过"),
REJECTED("rejected", "已拒绝"),
WITHDRAWN("withdrawn", "已撤回");
private final String code;
private final String desc;
public static RegistrationStateEnum getByCode(String code) {
if (code == null) {
return PENDING;
}
for (RegistrationStateEnum state : values()) {
if (state.getCode().equals(code)) {
return state;
}
}
return PENDING;
}
}

View File

@ -0,0 +1,32 @@
package com.lesingle.creation.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 举报处理动作枚举
*/
@Getter
@AllArgsConstructor
public enum ReportActionEnum {
TAKE_DOWN("takedown", "下架"),
WARN("warn", "警告"),
BAN("ban", "封禁"),
IGNORE("ignore", "忽略");
private final String code;
private final String desc;
public static ReportActionEnum getByCode(String code) {
if (code == null) {
return IGNORE;
}
for (ReportActionEnum action : values()) {
if (action.getCode().equals(code)) {
return action;
}
}
return IGNORE;
}
}

View File

@ -0,0 +1,31 @@
package com.lesingle.creation.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 举报状态枚举
*/
@Getter
@AllArgsConstructor
public enum ReportStatusEnum {
PENDING("pending", "待处理"),
HANDLED("handled", "已处理"),
IGNORED("ignored", "已忽略");
private final String code;
private final String desc;
public static ReportStatusEnum getByCode(String code) {
if (code == null) {
return PENDING;
}
for (ReportStatusEnum status : values()) {
if (status.getCode().equals(code)) {
return status;
}
}
return PENDING;
}
}

View File

@ -0,0 +1,31 @@
package com.lesingle.creation.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 评审状态枚举
*/
@Getter
@AllArgsConstructor
public enum ReviewStatusEnum {
PENDING("pending", "待评审"),
REVIEWING("reviewing", "评审中"),
COMPLETED("completed", "已完成");
private final String code;
private final String desc;
public static ReviewStatusEnum getByCode(String code) {
if (code == null) {
return PENDING;
}
for (ReviewStatusEnum status : values()) {
if (status.getCode().equals(code)) {
return status;
}
}
return PENDING;
}
}

View File

@ -0,0 +1,31 @@
package com.lesingle.creation.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 团队角色枚举
*/
@Getter
@AllArgsConstructor
public enum TeamRoleEnum {
MEMBER("member", "队员"),
LEADER("leader", "队长"),
MENTOR("mentor", "指导老师");
private final String code;
private final String desc;
public static TeamRoleEnum getByCode(String code) {
if (code == null) {
return MEMBER;
}
for (TeamRoleEnum role : values()) {
if (role.getCode().equals(code)) {
return role;
}
}
return MEMBER;
}
}

View File

@ -0,0 +1,34 @@
package com.lesingle.creation.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 租户类型枚举
*/
@Getter
@AllArgsConstructor
public enum TenantTypeEnum {
PLATFORM("platform", "平台"),
LIBRARY("library", "图书馆"),
KINDERGARTEN("kindergarten", "幼儿园"),
SCHOOL("school", "学校"),
INSTITUTION("institution", "机构"),
OTHER("other", "其他");
private final String code;
private final String desc;
public static TenantTypeEnum getByCode(String code) {
if (code == null) {
return OTHER;
}
for (TenantTypeEnum type : values()) {
if (type.getCode().equals(code)) {
return type;
}
}
return OTHER;
}
}

View File

@ -0,0 +1,31 @@
package com.lesingle.creation.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 用户来源枚举
*/
@Getter
@AllArgsConstructor
public enum UserSourceEnum {
ADMIN_CREATED("admin_created", "管理员创建"),
SELF_REGISTERED("self_registered", "自主注册"),
CHILD_MIGRATED("child_migrated", "子女账号迁移");
private final String code;
private final String desc;
public static UserSourceEnum getByCode(String code) {
if (code == null) {
return ADMIN_CREATED;
}
for (UserSourceEnum source : values()) {
if (source.getCode().equals(code)) {
return source;
}
}
return ADMIN_CREATED;
}
}

View File

@ -0,0 +1,30 @@
package com.lesingle.creation.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 用户状态枚举
*/
@Getter
@AllArgsConstructor
public enum UserStatusEnum {
ENABLED("enabled", "启用"),
DISABLED("disabled", "禁用");
private final String code;
private final String desc;
public static UserStatusEnum getByCode(String code) {
if (code == null) {
return ENABLED;
}
for (UserStatusEnum status : values()) {
if (status.getCode().equals(code)) {
return status;
}
}
return ENABLED;
}
}

View File

@ -0,0 +1,30 @@
package com.lesingle.creation.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 用户类型枚举
*/
@Getter
@AllArgsConstructor
public enum UserTypeEnum {
ADULT("adult", "成人"),
CHILD("child", "子女");
private final String code;
private final String desc;
public static UserTypeEnum getByCode(String code) {
if (code == null) {
return ADULT;
}
for (UserTypeEnum type : values()) {
if (type.getCode().equals(code)) {
return type;
}
}
return ADULT;
}
}

View File

@ -0,0 +1,33 @@
package com.lesingle.creation.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* UGC 作品状态枚举
*/
@Getter
@AllArgsConstructor
public enum UserWorkStatusEnum {
DRAFT("draft", "草稿"),
PENDING_REVIEW("pending_review", "待审核"),
PUBLISHED("published", "已发布"),
REJECTED("rejected", "已拒绝"),
TAKEN_DOWN("taken_down", "已下架");
private final String code;
private final String desc;
public static UserWorkStatusEnum getByCode(String code) {
if (code == null) {
return DRAFT;
}
for (UserWorkStatusEnum status : values()) {
if (status.getCode().equals(code)) {
return status;
}
}
return DRAFT;
}
}

View File

@ -0,0 +1,30 @@
package com.lesingle.creation.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 有效状态枚举
*/
@Getter
@AllArgsConstructor
public enum ValidStateEnum {
VALID(1, "有效"),
INVALID(2, "失效");
private final Integer code;
private final String desc;
public static ValidStateEnum getByCode(Integer code) {
if (code == null) {
return VALID;
}
for (ValidStateEnum state : values()) {
if (state.getCode().equals(code)) {
return state;
}
}
return VALID;
}
}

View File

@ -0,0 +1,33 @@
package com.lesingle.creation.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 可见性枚举
*/
@Getter
@AllArgsConstructor
public enum VisibilityEnum {
PUBLIC("public", "公开"),
PRIVATE("private", "私有"),
DESIGNATED("designated", "指定"),
INTERNAL("internal", "内部"),
FRIENDS("friends", "好友可见");
private final String code;
private final String desc;
public static VisibilityEnum getByCode(String code) {
if (code == null) {
return PRIVATE;
}
for (VisibilityEnum visibility : values()) {
if (visibility.getCode().equals(code)) {
return visibility;
}
}
return PRIVATE;
}
}

View File

@ -0,0 +1,33 @@
package com.lesingle.creation.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 作品状态枚举
*/
@Getter
@AllArgsConstructor
public enum WorkStatusEnum {
SUBMITTED("submitted", "已提交"),
LOCKED("locked", "已锁定"),
REVIEWING("reviewing", "评审中"),
REJECTED("rejected", "已拒绝"),
ACCEPTED("accepted", "已接受");
private final String code;
private final String desc;
public static WorkStatusEnum getByCode(String code) {
if (code == null) {
return SUBMITTED;
}
for (WorkStatusEnum status : values()) {
if (status.getCode().equals(code)) {
return status;
}
}
return SUBMITTED;
}
}

View File

@ -0,0 +1,33 @@
package com.lesingle.creation.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 作品类型枚举
*/
@Getter
@AllArgsConstructor
public enum WorkTypeEnum {
IMAGE("image", "图片"),
VIDEO("video", "视频"),
DOCUMENT("document", "文档"),
CODE("code", "代码"),
OTHER("other", "其他");
private final String code;
private final String desc;
public static WorkTypeEnum getByCode(String code) {
if (code == null) {
return OTHER;
}
for (WorkTypeEnum type : values()) {
if (type.getCode().equals(code)) {
return type;
}
}
return OTHER;
}
}

View File

@ -1,6 +1,7 @@
package com.lesingle.creation.common.filter; package com.lesingle.creation.common.filter;
import com.lesingle.creation.common.config.JwtProperties; import com.lesingle.creation.common.config.JwtProperties;
import com.lesingle.creation.common.security.UserPrincipal;
import com.lesingle.creation.common.util.JwtTokenUtil; import com.lesingle.creation.common.util.JwtTokenUtil;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
@ -47,24 +48,26 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
String username = jwtTokenUtil.getUsernameFromToken(token); String username = jwtTokenUtil.getUsernameFromToken(token);
if (StringUtils.hasText(username) && SecurityContextHolder.getContext().getAuthentication() == null) { if (StringUtils.hasText(username) && SecurityContextHolder.getContext().getAuthentication() == null) {
// 从数据库加载用户信息 // 从数据库加载用户信息UserDetailsServiceImpl 返回的是 UserPrincipal
UserDetails userDetails = userDetailsService.loadUserByUsername(username); UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 验证 Token 是否有效 // 验证 Token 是否有效
if (jwtTokenUtil.isTokenValid(token, userDetails)) { if (jwtTokenUtil.isTokenValid(token, userDetails)) {
// 创建认证对象 // 创建认证对象principal 使用 UserPrincipal 类型
UsernamePasswordAuthenticationToken authentication = UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken( new UsernamePasswordAuthenticationToken(
userDetails, userDetails, // 这就是 UserPrincipal 类型
null, null,
userDetails.getAuthorities() userDetails.getAuthorities()
); );
authentication.setDetails( authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request) new WebAuthenticationDetailsSource().buildDetails(request)
); );
// 设置到 SecurityContext // 设置到 SecurityContext
SecurityContextHolder.getContext().setAuthentication(authentication); SecurityContextHolder.getContext().setAuthentication(authentication);
log.debug("用户认证成功:{}", username); log.debug("用户认证成功:{}, userId={}",
userDetails.getUsername(),
userDetails instanceof UserPrincipal ? ((UserPrincipal) userDetails).getUserId() : "unknown");
} }
} }
} catch (Exception e) { } catch (Exception e) {

View File

@ -0,0 +1,70 @@
package com.lesingle.creation.common.handler;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.lesingle.creation.util.SecurityUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
/**
* MyBatis-Plus 自动填充处理器
* <p>
* 自动填充创建人创建时间更新人更新时间字段
*
* @author lesingle
* @since 1.0.0
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class MyMetaObjectHandler implements MetaObjectHandler {
private final SecurityUtils securityUtils;
@Override
public void insertFill(MetaObject metaObject) {
log.debug("执行插入操作,自动填充审计字段");
LocalDateTime now = LocalDateTime.now();
// 填充创建时间
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, now);
// 填充创建人 UserContext 获取当前用户名
this.strictInsertFill(metaObject, "createBy", String.class, getCurrentUsername());
// 填充更新时间
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, now);
// 填充更新人
this.strictInsertFill(metaObject, "updateBy", String.class, getCurrentUsername());
}
@Override
public void updateFill(MetaObject metaObject) {
log.debug("执行更新操作,自动填充审计字段");
LocalDateTime now = LocalDateTime.now();
// 填充更新时间
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, now);
// 填充更新人
this.strictUpdateFill(metaObject, "updateBy", String.class, getCurrentUsername());
}
/**
* 获取当前登录用户名
* SecurityUtils 获取非登录场景返回 "system"
*/
private String getCurrentUsername() {
try {
return securityUtils.getCurrentUsernameStr();
} catch (Exception e) {
log.warn("获取当前用户名失败,使用默认值:{}", e.getMessage());
return "system";
}
}
}

View File

@ -0,0 +1,82 @@
package com.lesingle.creation.common.security;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
/**
* 用户 Principal 实现类
*
* @author lesingle
* @since 1.0.0
*/
@Getter
public class UserPrincipal implements UserDetails {
private final Long userId;
private final String username; // 用户名用于显示
private final String password;
private final Collection<? extends GrantedAuthority> authorities;
private final Long tenantId;
private final String tenantCode; // 租户编码
private final boolean isSuperTenant; // 是否超级租户
public UserPrincipal(Long userId, String username, String password,
Collection<? extends GrantedAuthority> authorities,
Long tenantId, String tenantCode, boolean isSuperTenant) {
this.userId = userId;
this.username = username;
this.password = password;
this.authorities = authorities;
this.tenantId = tenantId;
this.tenantCode = tenantCode;
this.isSuperTenant = isSuperTenant;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return String.valueOf(this.userId); // 返回用户 ID 字符串方便 SecurityUtils 获取
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
/**
* 获取显示用户名用于日志等
*
* @return 显示用户名
*/
public String getDisplayName() {
return this.username;
}
}

View File

@ -5,6 +5,7 @@ import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts; import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -21,6 +22,7 @@ import java.util.function.Function;
* @author lesingle * @author lesingle
* @since 1.0.0 * @since 1.0.0
*/ */
@Slf4j
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
public class JwtTokenUtil { public class JwtTokenUtil {
@ -42,6 +44,22 @@ public class JwtTokenUtil {
return getClaimFromToken(token, Claims::getSubject); return getClaimFromToken(token, Claims::getSubject);
} }
/**
* Token 中获取用户 ID
*
* @param token JWT Token
* @return 用户 ID
*/
public Long getUserIdFromToken(String token) {
String username = getUsernameFromToken(token);
try {
return Long.parseLong(username);
} catch (NumberFormatException e) {
log.warn("无法解析用户 ID{}", username);
return null;
}
}
/** /**
* Token 中获取指定声明 * Token 中获取指定声明
*/ */
@ -76,6 +94,54 @@ public class JwtTokenUtil {
return createToken(claims, username); return createToken(claims, username);
} }
/**
* 生成带租户信息的 Token
*
* @param userId 用户 ID
* @param tenantId 租户 ID
* @param tenantCode 租户编码
* @return JWT Token
*/
public String generateToken(Long userId, Long tenantId, String tenantCode) {
Map<String, Object> claims = new HashMap<>();
claims.put("tenantId", tenantId);
claims.put("tenantCode", tenantCode);
return createToken(claims, String.valueOf(userId));
}
/**
* 生成带用户名的 Token用于公众用户
*
* @param userId 用户 ID
* @param username 用户名
* @return JWT Token
*/
public String generateToken(Long userId, String username) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId);
return createToken(claims, username);
}
/**
* Token 中获取租户 ID
*
* @param token JWT Token
* @return 租户 ID
*/
public Long getTenantIdFromToken(String token) {
return getClaimFromToken(token, claims -> claims.get("tenantId", Long.class));
}
/**
* Token 中获取租户编码
*
* @param token JWT Token
* @return 租户编码
*/
public String getTenantCodeFromToken(String token) {
return getClaimFromToken(token, claims -> claims.get("tenantCode", String.class));
}
/** /**
* 创建 Token * 创建 Token
*/ */

View File

@ -0,0 +1,274 @@
package com.lesingle.creation.common.util;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.model.PutObjectRequest;
import com.lesingle.creation.common.config.OssProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
/**
* 阿里云 OSS 文件上传工具类
*
* @author lesingle
* @since 1.0.0
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class OssUtil {
private final OssProperties ossProperties;
/**
* 允许上传的文件类型
*/
private static final List<String> ALLOWED_TYPES = Arrays.asList(
"image/jpeg", "image/png", "image/gif", "image/webp",
"application/pdf", "application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
);
/**
* 最大文件大小100MB
*/
private static final long MAX_FILE_SIZE = 100 * 1024 * 1024;
/**
* 上传文件
*
* @param file 上传的文件
* @return 文件访问 URL
* @throws IOException 上传失败抛出异常
*/
public String upload(MultipartFile file) throws IOException {
// 校验文件
validateFile(file);
// 生成文件名
String fileName = generateFileName(file.getOriginalFilename());
// 生成文件路径按日期分目录
String objectKey = buildObjectKey(fileName);
// 获取 OSS 客户端
OSS ossClient = createOssClient();
try {
// 上传文件
InputStream inputStream = file.getInputStream();
PutObjectRequest putObjectRequest = new PutObjectRequest(
ossProperties.getBucketName(),
objectKey,
inputStream
);
ossClient.putObject(putObjectRequest);
// 返回文件 URL
String fileUrl = getFileUrl(objectKey);
log.info("文件上传成功:{}, URL: {}", objectKey, fileUrl);
return fileUrl;
} finally {
// 关闭客户端
ossClient.shutdown();
}
}
/**
* 上传文件自定义路径
*
* @param file 上传的文件
* @param customPath 自定义路径avatars/
* @return 文件访问 URL
* @throws IOException 上传失败抛出异常
*/
public String upload(MultipartFile file, String customPath) throws IOException {
// 校验文件
validateFile(file);
// 生成文件名
String fileName = generateFileName(file.getOriginalFilename());
// 生成文件路径
String objectKey = customPath + (customPath.endsWith("/") ? "" : "/") + fileName;
// 获取 OSS 客户端
OSS ossClient = createOssClient();
try {
// 上传文件
InputStream inputStream = file.getInputStream();
PutObjectRequest putObjectRequest = new PutObjectRequest(
ossProperties.getBucketName(),
objectKey,
inputStream
);
ossClient.putObject(putObjectRequest);
// 返回文件 URL
String fileUrl = getFileUrl(objectKey);
log.info("文件上传成功:{}, URL: {}", objectKey, fileUrl);
return fileUrl;
} finally {
// 关闭客户端
ossClient.shutdown();
}
}
/**
* 上传字节数组
*
* @param bytes 文件字节数组
* @param fileName 文件名
* @param customPath 自定义路径
* @return 文件访问 URL
* @throws IOException 上传失败抛出异常
*/
public String upload(byte[] bytes, String fileName, String customPath) throws IOException {
String finalFileName = generateFileName(fileName);
String objectKey = customPath + (customPath.endsWith("/") ? "" : "/") + finalFileName;
OSS ossClient = createOssClient();
try {
InputStream inputStream = new ByteArrayInputStream(bytes);
PutObjectRequest putObjectRequest = new PutObjectRequest(
ossProperties.getBucketName(),
objectKey,
inputStream
);
ossClient.putObject(putObjectRequest);
return getFileUrl(objectKey);
} finally {
ossClient.shutdown();
}
}
/**
* 删除文件
*
* @param fileUrl 文件 URL
*/
public void delete(String fileUrl) {
String objectKey = extractObjectKey(fileUrl);
if (objectKey == null) {
log.warn("无法解析文件 Key: {}", fileUrl);
return;
}
OSS ossClient = createOssClient();
try {
ossClient.deleteObject(ossProperties.getBucketName(), objectKey);
log.info("文件删除成功:{}", objectKey);
} catch (Exception e) {
log.error("文件删除失败:{}", objectKey, e);
} finally {
ossClient.shutdown();
}
}
/**
* 校验文件
*/
private void validateFile(MultipartFile file) {
// 检查文件是否为空
if (file == null || file.isEmpty()) {
throw new IllegalArgumentException("上传文件不能为空");
}
// 检查文件大小
if (file.getSize() > MAX_FILE_SIZE) {
throw new IllegalArgumentException("文件大小超过限制:" + (MAX_FILE_SIZE / 1024 / 1024) + "MB");
}
// 检查文件类型
String contentType = file.getContentType();
if (contentType == null || !ALLOWED_TYPES.contains(contentType.toLowerCase())) {
throw new IllegalArgumentException("不支持的文件类型:" + contentType);
}
}
/**
* 生成文件名使用 UUID
*/
private String generateFileName(String originalFilename) {
if (originalFilename == null || originalFilename.isEmpty()) {
return UUID.randomUUID().toString();
}
int dotIndex = originalFilename.lastIndexOf(".");
if (dotIndex > 0) {
String extension = originalFilename.substring(dotIndex);
return UUID.randomUUID().toString() + extension;
}
return UUID.randomUUID().toString();
}
/**
* 构建文件路径按日期分目录
*/
private String buildObjectKey(String fileName) {
String datePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd/"));
String rootDir = ossProperties.getRootDir();
return (rootDir != null && !rootDir.isEmpty() ? rootDir + "/" : "")
+ datePath + fileName;
}
/**
* 获取文件访问 URL
*/
private String getFileUrl(String objectKey) {
if (ossProperties.getCustomDomain() != null && !ossProperties.getCustomDomain().isEmpty()) {
return ossProperties.getCustomDomain() + "/" + objectKey;
}
return "https://" + ossProperties.getBucketName() + "." + ossProperties.getEndpoint() + "/" + objectKey;
}
/**
* URL 中提取 Object Key
*/
private String extractObjectKey(String fileUrl) {
if (fileUrl == null || fileUrl.isEmpty()) {
return null;
}
// 尝试从自定义域名解析
if (ossProperties.getCustomDomain() != null && fileUrl.startsWith(ossProperties.getCustomDomain())) {
return fileUrl.substring(ossProperties.getCustomDomain().length() + 1);
}
// OSS 默认域名解析
String bucketDomain = ossProperties.getBucketName() + "." + ossProperties.getEndpoint();
if (fileUrl.contains(bucketDomain)) {
return fileUrl.substring(fileUrl.indexOf(bucketDomain) + bucketDomain.length() + 1);
}
return null;
}
/**
* 创建 OSS 客户端
*/
private OSS createOssClient() {
return new OSSClientBuilder().build(
ossProperties.getEndpoint(),
ossProperties.getAccessKeyId(),
ossProperties.getAccessKeySecret()
);
}
}

View File

@ -0,0 +1,66 @@
package com.lesingle.creation.common.util;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* Redis 限流工具类
* <p>
* 基于滑动窗口算法实现限流
*
* @author lesingle
* @since 1.0.0
*/
@Slf4j
@Component
public class RateLimiterUtil {
private static RedisTemplate<String, Object> redisTemplate;
public RateLimiterUtil(RedisTemplate<String, Object> redisTemplate) {
RateLimiterUtil.redisTemplate = redisTemplate;
}
/**
* 判断是否允许请求通过
*
* @param key 限流 key
* @param time 时间窗口
* @param count 时间窗口内最大请求数
* @return true-允许通过false-拒绝请求
*/
public static boolean tryAcquire(String key, int time, int count) {
try {
Long currentCount = redisTemplate.opsForValue().increment(key);
if (currentCount == 1) {
// 第一次请求设置过期时间
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return currentCount <= count;
} catch (Exception e) {
log.error("Redis 限流异常key: {}", key, e);
// Redis 异常时放行
return true;
}
}
/**
* 获取当前请求数
*
* @param key 限流 key
* @return 当前请求数
*/
public static Long getCurrentCount(String key) {
try {
String value = (String) redisTemplate.opsForValue().get(key);
return value != null ? Long.parseLong(value) : 0L;
} catch (Exception e) {
log.error("获取限流计数异常key: {}", key, e);
return 0L;
}
}
}

View File

@ -0,0 +1,51 @@
package com.lesingle.creation.common.util;
import cn.hutool.core.util.IdUtil;
import org.slf4j.MDC;
/**
* TraceId 工具类
* <p>
* 用于生成和管理链路追踪 ID实现全链路日志追踪
*
* @author lesingle
* @since 1.0.0
*/
public class TraceIdUtil {
/**
* MDC TraceId key
*/
private static final String TRACE_ID_KEY = "traceId";
/**
* 生成 TraceId 并放入 MDC
*/
public static void setTraceId() {
MDC.put(TRACE_ID_KEY, IdUtil.fastSimpleUUID());
}
/**
* 获取当前 TraceId
*
* @return TraceId不存在则返回 "unknown"
*/
public static String getTraceId() {
return MDC.get(TRACE_ID_KEY) != null ? MDC.get(TRACE_ID_KEY) : "unknown";
}
/**
* 清除 MDC 中的 TraceId
*/
public static void clearTraceId() {
MDC.remove(TRACE_ID_KEY);
}
/**
* 清除所有 MDC 内容
*/
public static void clear() {
MDC.clear();
}
}

View File

@ -0,0 +1,92 @@
package com.lesingle.creation.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.lesingle.creation.common.core.Result;
import com.lesingle.creation.common.security.UserPrincipal;
import com.lesingle.creation.dto.ai3d.AI3DTaskQueryDTO;
import com.lesingle.creation.dto.ai3d.CreateAI3DTaskDTO;
import com.lesingle.creation.service.AI3DTaskService;
import com.lesingle.creation.vo.ai3d.AI3DTaskVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/**
* AI 3D 生成控制器
*/
@Tag(name = "AI 3D 生成")
@RestController
@RequestMapping("/api/ai-3d")
@RequiredArgsConstructor
public class AI3DTaskController {
private final AI3DTaskService ai3dTaskService;
@PostMapping
@Operation(summary = "创建 AI 3D 任务")
public Result<AI3DTaskVO> create(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Validated CreateAI3DTaskDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long userId = userPrincipal.getUserId();
AI3DTaskVO result = ai3dTaskService.create(dto, tenantId, userId);
return Result.success(result);
}
@GetMapping("/{id}")
@Operation(summary = "获取任务详情")
public Result<AI3DTaskVO> getDetail(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
AI3DTaskVO result = ai3dTaskService.getDetail(id, tenantId);
return Result.success(result);
}
@GetMapping("/page")
@Operation(summary = "分页查询任务列表")
public Result<Page<AI3DTaskVO>> pageQuery(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@ModelAttribute AI3DTaskQueryDTO queryDTO) {
Long tenantId = userPrincipal.getTenantId();
Long userId = userPrincipal.getUserId(); // 查询自己的任务
Page<AI3DTaskVO> result = ai3dTaskService.pageQuery(queryDTO, tenantId, userId);
return Result.success(result);
}
@PutMapping("/{id}/cancel")
@Operation(summary = "取消任务")
public Result<Void> cancel(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
ai3dTaskService.cancel(id, tenantId);
return Result.success(null);
}
@PutMapping("/{id}/retry")
@Operation(summary = "重试失败的任务")
public Result<AI3DTaskVO> retry(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
AI3DTaskVO result = ai3dTaskService.retry(id, tenantId);
return Result.success(result);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除 AI 3D 任务")
@PreAuthorize("hasAuthority('ai-3d:delete')")
public Result<Void> delete(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
ai3dTaskService.delete(id, tenantId);
return Result.success(null);
}
}

View File

@ -1,101 +1,102 @@
package com.lesingle.creation.controller; package com.lesingle.creation.controller;
import com.lesingle.creation.common.core.Result; import com.lesingle.creation.common.core.Result;
import com.lesingle.creation.dto.LoginRequest; import com.lesingle.creation.common.security.UserPrincipal;
import com.lesingle.creation.dto.RegisterRequest; import com.lesingle.creation.dto.auth.LoginDTO;
import com.lesingle.creation.entity.User;
import com.lesingle.creation.service.AuthService; import com.lesingle.creation.service.AuthService;
import com.lesingle.creation.service.RoleService; import com.lesingle.creation.vo.auth.LoginResponseVO;
import com.lesingle.creation.service.UserDetailsServiceImpl; import com.lesingle.creation.vo.auth.UserInfoVO;
import com.lesingle.creation.vo.LoginResponse;
import com.lesingle.creation.vo.UserVO;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.List;
/** /**
* 认证控制器 * 认证控制器
*
* @author lesingle
* @since 1.0.0
*/ */
@Slf4j @Slf4j
@Tag(name = "认证管理") @Tag(name = "认证管理", description = "用户登录、登出、刷新 Token 等接口")
@RestController @RestController
@RequestMapping("/api/auth") @RequestMapping("/api/auth")
@RequiredArgsConstructor @RequiredArgsConstructor
public class AuthController { public class AuthController {
private final AuthService authService; private final AuthService authService;
private final UserDetailsServiceImpl userDetailsService;
private final RoleService roleService;
/** /**
* 用户登录 * 用户登录
*/ */
@Operation(summary = "用户登录") @Operation(summary = "用户登录")
@PostMapping("/login") @PostMapping("/login")
public Result<LoginResponse> login(@Valid @RequestBody LoginRequest request) { public Result<LoginResponseVO> login(@RequestBody LoginDTO loginDTO) {
String token = authService.login(request); LoginResponseVO response = authService.login(loginDTO);
// 查询用户信息
User user = ((User) SecurityContextHolder.getContext().getAuthentication().getPrincipal());
// 获取用户角色
List<String> roles = roleService.getRoleCodesByUserId(user.getId());
LoginResponse response = LoginResponse.builder()
.token(token)
.tokenType("Bearer")
.expiresIn(86400000L)
.userId(user.getId())
.username(user.getUsername())
.nickname(user.getNickname())
.avatar(user.getAvatar())
.roles(roles)
.build();
return Result.success(response); return Result.success(response);
} }
/**
* 用户注册
*/
@Operation(summary = "用户注册")
@PostMapping("/register")
public Result<Long> register(@Valid @RequestBody RegisterRequest request) {
Long userId = authService.register(request);
return Result.success(userId);
}
/** /**
* 获取当前用户信息 * 获取当前用户信息
*/ */
@Operation(summary = "获取当前用户信息") @Operation(summary = "获取当前用户信息")
@GetMapping("/me") @GetMapping("/me")
public Result<UserVO> getCurrentUser() { public Result<UserInfoVO> getCurrentUser() {
String username = SecurityContextHolder.getContext().getAuthentication().getName(); Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
User user = ((User) SecurityContextHolder.getContext().getAuthentication().getPrincipal()); log.debug("Authentication: {}", authentication);
if (authentication == null || !authentication.isAuthenticated()) {
return Result.fail(401, "未授权");
}
List<String> roles = roleService.getRoleCodesByUserId(user.getId()); Object principal = authentication.getPrincipal();
log.debug("Principal type: {}", principal != null ? principal.getClass().getName() : "null");
Long userId;
UserVO userVO = UserVO.builder() if (principal instanceof UserPrincipal) {
.id(user.getId()) userId = ((UserPrincipal) principal).getUserId();
.username(user.getUsername()) log.info("UserPrincipal userId: {}", userId);
.nickname(user.getNickname()) } else if (principal instanceof UserDetails) {
.email(user.getEmail()) // UserDetails 获取用户名然后通过用户名查询用户
.phone(user.getPhone()) String username = ((UserDetails) principal).getUsername();
.avatar(user.getAvatar()) log.debug("Principal 是 UserDetails 类型,用户名:{}", username);
.status(user.getStatus()) // 尝试从 authorities 获取用户 ID如果有
.roles(roles) userId = null;
.build(); } else {
log.warn("无法识别的 Principal 类型:{}", principal != null ? principal.getClass().getName() : "null");
return Result.fail(401, "无法获取用户信息");
}
return Result.success(userVO); if (userId == null) {
return Result.fail(401, "无法获取用户 ID");
}
UserInfoVO userInfo = authService.getUserInfo(userId);
return Result.success(userInfo);
}
/**
* 用户登出
*/
@Operation(summary = "用户登出")
@PostMapping("/logout")
public Result<Void> logout() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getPrincipal() instanceof UserDetails) {
String username = ((UserDetails) authentication.getPrincipal()).getUsername();
log.info("用户登出:{}", username);
}
// TODO: 可以将 token 加入黑名单使用 Redis
return Result.success(null);
}
/**
* 刷新 Token
*/
@Operation(summary = "刷新 Token")
@PostMapping("/refresh-token")
public Result<LoginResponseVO> refreshToken(@RequestHeader("Authorization") String token) {
LoginResponseVO response = authService.refreshToken(token);
return Result.success(response);
} }
} }

View File

@ -0,0 +1,98 @@
package com.lesingle.creation.controller;
import com.lesingle.creation.common.core.Result;
import com.lesingle.creation.common.security.UserPrincipal;
import com.lesingle.creation.dto.config.CreateConfigDTO;
import com.lesingle.creation.dto.config.UpdateConfigDTO;
import com.lesingle.creation.service.ConfigService;
import com.lesingle.creation.vo.config.ConfigDetailVO;
import com.lesingle.creation.vo.config.ConfigListVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/**
* 系统配置管理控制器
*/
@Tag(name = "系统配置管理")
@RestController
@RequestMapping("/api/config")
@RequiredArgsConstructor
public class ConfigController {
private final ConfigService configService;
@PostMapping
@Operation(summary = "创建配置")
@PreAuthorize("hasAuthority('config:create')")
public Result<ConfigDetailVO> create(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Validated CreateConfigDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long operatorId = userPrincipal.getUserId();
ConfigDetailVO result = configService.create(dto, tenantId, operatorId);
return Result.success(result);
}
@GetMapping
@Operation(summary = "配置列表")
@PreAuthorize("hasAuthority('config:read')")
public Result<com.baomidou.mybatisplus.extension.plugins.pagination.Page<ConfigListVO>> pageList(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int pageSize) {
Long tenantId = userPrincipal.getTenantId();
com.baomidou.mybatisplus.extension.plugins.pagination.Page<ConfigListVO> result =
configService.pageList(tenantId, page, pageSize);
return Result.success(result);
}
@GetMapping("/key/{key}")
@Operation(summary = "根据键查询配置")
@PreAuthorize("hasAuthority('config:read')")
public Result<ConfigDetailVO> getByKey(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable String key) {
Long tenantId = userPrincipal.getTenantId();
ConfigDetailVO result = configService.getByKey(key, tenantId);
return Result.success(result);
}
@GetMapping("/{id}")
@Operation(summary = "配置详情")
@PreAuthorize("hasAuthority('config:read')")
public Result<ConfigDetailVO> detail(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
ConfigDetailVO result = configService.detail(id, tenantId);
return Result.success(result);
}
@PutMapping("/{id}")
@Operation(summary = "更新配置")
@PreAuthorize("hasAuthority('config:update')")
public Result<ConfigDetailVO> update(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id,
@RequestBody @Validated UpdateConfigDTO dto) {
Long tenantId = userPrincipal.getTenantId();
ConfigDetailVO result = configService.update(id, dto, tenantId);
return Result.success(result);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除配置")
@PreAuthorize("hasAuthority('config:delete')")
public Result<Void> delete(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
configService.delete(id, tenantId);
return Result.success();
}
}

View File

@ -0,0 +1,135 @@
package com.lesingle.creation.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.lesingle.creation.common.core.Result;
import com.lesingle.creation.common.security.UserPrincipal;
import com.lesingle.creation.dto.contest.*;
import com.lesingle.creation.service.ContestService;
import com.lesingle.creation.vo.contest.ContestDetailVO;
import com.lesingle.creation.vo.contest.ContestListVO;
import com.lesingle.creation.vo.contest.ContestStatsVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/**
* 竞赛管理控制器
*/
@Tag(name = "竞赛管理")
@RestController
@RequestMapping("/api/contests")
@RequiredArgsConstructor
public class ContestController {
private final ContestService contestService;
@PostMapping
@Operation(summary = "创建竞赛")
@PreAuthorize("hasAuthority('contest:create')")
public Result<ContestDetailVO> create(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Validated CreateContestDTO dto) {
Long tenantId = userPrincipal.getTenantId();
ContestDetailVO result = contestService.create(dto, tenantId);
return Result.success(result);
}
@GetMapping("/stats")
@Operation(summary = "竞赛统计")
@PreAuthorize("hasAuthority('contest:read')")
public Result<ContestStatsVO> getStats() {
ContestStatsVO result = contestService.getStats();
return Result.success(result);
}
@GetMapping
@Operation(summary = "竞赛列表")
@PreAuthorize("hasAuthority('contest:read')")
public Result<Page<ContestListVO>> pageList(
@AuthenticationPrincipal UserPrincipal userPrincipal,
ContestQueryDTO queryDTO) {
Long tenantId = userPrincipal.getTenantId();
Page<ContestListVO> result = contestService.pageList(queryDTO, tenantId);
return Result.success(result);
}
@GetMapping("/my-contests")
@Operation(summary = "我参与的活动列表")
public Result<Page<ContestListVO>> myContests(
@AuthenticationPrincipal UserPrincipal userPrincipal,
ContestQueryDTO queryDTO) {
Long userId = userPrincipal.getUserId();
Long tenantId = userPrincipal.getTenantId();
Page<ContestListVO> result = contestService.myContests(queryDTO, userId, tenantId);
return Result.success(result);
}
@GetMapping("/{id}")
@Operation(summary = "竞赛详情")
@PreAuthorize("hasAuthority('contest:read')")
public Result<ContestDetailVO> detail(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
ContestDetailVO result = contestService.detail(id, tenantId);
return Result.success(result);
}
@PutMapping("/{id}")
@Operation(summary = "更新竞赛")
@PreAuthorize("hasAuthority('contest:update')")
public Result<ContestDetailVO> update(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id,
@RequestBody @Validated UpdateContestDTO dto) {
Long tenantId = userPrincipal.getTenantId();
ContestDetailVO result = contestService.update(id, dto, tenantId);
return Result.success(result);
}
@PatchMapping("/{id}/publish")
@Operation(summary = "发布/取消发布竞赛")
@PreAuthorize("hasAuthority('contest:publish')")
public Result<ContestDetailVO> publish(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id,
@RequestBody @Validated PublishContestDTO dto) {
Long tenantId = userPrincipal.getTenantId();
ContestDetailVO result = contestService.publish(id, dto.getContestState(), tenantId);
return Result.success(result);
}
@PatchMapping("/{id}/finish")
@Operation(summary = "标记竞赛为完结")
@PreAuthorize("hasAuthority('contest:update')")
public Result<ContestDetailVO> finish(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
ContestDetailVO result = contestService.finish(id, tenantId);
return Result.success(result);
}
@PatchMapping("/{id}/reopen")
@Operation(summary = "重新开启竞赛")
@PreAuthorize("hasAuthority('contest:update')")
public Result<ContestDetailVO> reopen(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
ContestDetailVO result = contestService.reopen(id, tenantId);
return Result.success(result);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除竞赛")
@PreAuthorize("hasAuthority('contest:delete')")
public Result<Void> delete(@PathVariable Long id) {
contestService.delete(id);
return Result.success();
}
}

View File

@ -0,0 +1,102 @@
package com.lesingle.creation.controller;
import com.lesingle.creation.common.core.Result;
import com.lesingle.creation.common.security.UserPrincipal;
import com.lesingle.creation.dto.judge.CreateJudgeDTO;
import com.lesingle.creation.dto.judge.UpdateJudgeDTO;
import com.lesingle.creation.service.ContestJudgeService;
import com.lesingle.creation.vo.judge.JudgeVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 竞赛评委管理控制器
*/
@Tag(name = "竞赛评委管理")
@RestController
@RequestMapping("/api/contests/judges")
@RequiredArgsConstructor
public class ContestJudgeController {
private final ContestJudgeService judgeService;
@PostMapping
@Operation(summary = "创建评委")
@PreAuthorize("hasAuthority('contest:judge:create')")
public Result<JudgeVO> create(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Validated CreateJudgeDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long creatorId = userPrincipal.getUserId();
JudgeVO result = judgeService.create(dto, creatorId);
return Result.success(result);
}
@PutMapping("/{id}")
@Operation(summary = "更新评委")
@PreAuthorize("hasAuthority('contest:judge:update')")
public Result<JudgeVO> update(
@PathVariable Long id,
@RequestBody @Validated UpdateJudgeDTO dto) {
JudgeVO result = judgeService.update(id, dto);
return Result.success(result);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除评委")
@PreAuthorize("hasAuthority('contest:judge:delete')")
public Result<Void> delete(@PathVariable Long id) {
judgeService.delete(id);
return Result.success(null);
}
@GetMapping("/{id}")
@Operation(summary = "获取评委详情")
@PreAuthorize("hasAuthority('contest:read')")
public Result<JudgeVO> getDetail(@PathVariable Long id) {
JudgeVO result = judgeService.getDetail(id);
return Result.success(result);
}
@GetMapping("/contest/{contestId}")
@Operation(summary = "查询活动的评委列表")
@PreAuthorize("hasAuthority('contest:read')")
public Result<List<JudgeVO>> listByContest(@PathVariable Long contestId) {
List<JudgeVO> result = judgeService.listByContest(contestId);
return Result.success(result);
}
@PostMapping("/freeze")
@Operation(summary = "冻结评委")
@PreAuthorize("hasAuthority('contest:judge:update')")
public Result<JudgeVO> freeze(@RequestBody Map<String, Long> params) {
Long id = params.get("id");
JudgeVO result = judgeService.freeze(id);
return Result.success(result);
}
@PostMapping("/unfreeze")
@Operation(summary = "解冻评委")
@PreAuthorize("hasAuthority('contest:judge:update')")
public Result<JudgeVO> unfreeze(@RequestBody Map<String, Long> params) {
Long id = params.get("id");
JudgeVO result = judgeService.unfreeze(id);
return Result.success(result);
}
@PostMapping("/batch-delete")
@Operation(summary = "批量删除评委")
@PreAuthorize("hasAuthority('contest:judge:delete')")
public Result<Void> batchDelete(@RequestBody List<Long> ids) {
judgeService.batchDelete(ids);
return Result.success(null);
}
}

View File

@ -0,0 +1,83 @@
package com.lesingle.creation.controller;
import com.lesingle.creation.common.core.Result;
import com.lesingle.creation.common.security.UserPrincipal;
import com.lesingle.creation.dto.notice.CreateNoticeDTO;
import com.lesingle.creation.dto.notice.UpdateNoticeDTO;
import com.lesingle.creation.service.ContestNoticeService;
import com.lesingle.creation.vo.notice.NoticeVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 竞赛公告管理控制器
*/
@Tag(name = "竞赛公告管理")
@RestController
@RequestMapping("/api/contests/notices")
@RequiredArgsConstructor
public class ContestNoticeController {
private final ContestNoticeService noticeService;
@PostMapping
@Operation(summary = "创建公告")
@PreAuthorize("hasAuthority('contest:notice:create')")
public Result<NoticeVO> create(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Validated CreateNoticeDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long creatorId = userPrincipal.getUserId();
NoticeVO result = noticeService.create(dto, creatorId);
return Result.success(result);
}
@PutMapping("/{id}")
@Operation(summary = "更新公告")
@PreAuthorize("hasAuthority('contest:notice:update')")
public Result<NoticeVO> update(
@PathVariable Long id,
@RequestBody @Validated UpdateNoticeDTO dto) {
NoticeVO result = noticeService.update(id, dto);
return Result.success(result);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除公告")
@PreAuthorize("hasAuthority('contest:notice:delete')")
public Result<Void> delete(@PathVariable Long id) {
noticeService.delete(id);
return Result.success(null);
}
@PostMapping("/{id}/publish")
@Operation(summary = "发布公告")
@PreAuthorize("hasAuthority('contest:notice:publish')")
public Result<NoticeVO> publish(@PathVariable Long id) {
NoticeVO result = noticeService.publish(id);
return Result.success(result);
}
@GetMapping("/{id}")
@Operation(summary = "获取公告详情")
@PreAuthorize("hasAuthority('contest:read')")
public Result<NoticeVO> getDetail(@PathVariable Long id) {
NoticeVO result = noticeService.getDetail(id);
return Result.success(result);
}
@GetMapping("/contest/{contestId}")
@Operation(summary = "查询活动的公告列表")
@PreAuthorize("hasAuthority('contest:read')")
public Result<List<NoticeVO>> listByContest(@PathVariable Long contestId) {
List<NoticeVO> result = noticeService.listByContest(contestId);
return Result.success(result);
}
}

View File

@ -0,0 +1,76 @@
package com.lesingle.creation.controller;
import com.lesingle.creation.common.core.Result;
import com.lesingle.creation.common.security.UserPrincipal;
import com.lesingle.creation.dto.presetcomment.CreatePresetCommentDTO;
import com.lesingle.creation.service.ContestPresetCommentService;
import com.lesingle.creation.vo.presetcomment.PresetCommentVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 竞赛预设评语管理控制器
*/
@Tag(name = "竞赛预设评语管理")
@RestController
@RequestMapping("/api/contests/preset-comments")
@RequiredArgsConstructor
public class ContestPresetCommentController {
private final ContestPresetCommentService presetCommentService;
@PostMapping
@Operation(summary = "创建预设评语")
@PreAuthorize("hasAuthority('contest:preset-comment:create')")
public Result<PresetCommentVO> create(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Validated CreatePresetCommentDTO dto) {
Long tenantId = userPrincipal.getTenantId();
PresetCommentVO result = presetCommentService.create(dto, tenantId);
return Result.success(result);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除预设评语")
@PreAuthorize("hasAuthority('contest:preset-comment:delete')")
public Result<Void> delete(@PathVariable Long id) {
presetCommentService.delete(id);
return Result.success(null);
}
@GetMapping("/{id}")
@Operation(summary = "获取预设评语详情")
@PreAuthorize("hasAuthority('contest:read')")
public Result<PresetCommentVO> getDetail(@PathVariable Long id) {
PresetCommentVO result = presetCommentService.getDetail(id);
return Result.success(result);
}
@GetMapping("/contest/{contestId}")
@Operation(summary = "查询活动的预设评语列表")
@PreAuthorize("hasAuthority('contest:read')")
public Result<List<PresetCommentVO>> listByContest(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long contestId) {
Long tenantId = userPrincipal.getTenantId();
List<PresetCommentVO> result = presetCommentService.listByContest(contestId, tenantId);
return Result.success(result);
}
@GetMapping("/common")
@Operation(summary = "查询通用评语列表")
@PreAuthorize("hasAuthority('contest:read')")
public Result<List<PresetCommentVO>> listCommon(
@AuthenticationPrincipal UserPrincipal userPrincipal) {
Long tenantId = userPrincipal.getTenantId();
List<PresetCommentVO> result = presetCommentService.listCommon(tenantId);
return Result.success(result);
}
}

View File

@ -0,0 +1,130 @@
package com.lesingle.creation.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.lesingle.creation.common.core.Result;
import com.lesingle.creation.common.security.UserPrincipal;
import com.lesingle.creation.dto.registration.*;
import com.lesingle.creation.service.ContestRegistrationService;
import com.lesingle.creation.vo.registration.RegistrationStatsVO;
import com.lesingle.creation.vo.registration.RegistrationVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/**
* 竞赛报名管理控制器
*/
@Tag(name = "竞赛报名管理")
@RestController
@RequestMapping("/api/contests/registrations")
@RequiredArgsConstructor
public class ContestRegistrationController {
private final ContestRegistrationService registrationService;
@PostMapping
@Operation(summary = "创建报名")
@PreAuthorize("hasAuthority('contest:register')")
public Result<RegistrationVO> create(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Validated CreateRegistrationDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long creatorId = userPrincipal.getUserId();
RegistrationVO result = registrationService.create(dto, tenantId, creatorId);
return Result.success(result);
}
@GetMapping("/stats")
@Operation(summary = "报名统计")
@PreAuthorize("hasAuthority('contest:read')")
public Result<RegistrationStatsVO> getStats(@RequestParam(required = false) Long contestId) {
RegistrationStatsVO result = registrationService.getStats(contestId);
return Result.success(result);
}
@GetMapping
@Operation(summary = "分页查询报名列表")
@PreAuthorize("hasAuthority('contest:read')")
public Result<Page<RegistrationVO>> pageQuery(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@ModelAttribute RegistrationQueryDTO queryDTO) {
Long tenantId = userPrincipal.getTenantId();
Page<RegistrationVO> result = registrationService.pageQuery(queryDTO, tenantId);
return Result.success(result);
}
@GetMapping("/my/{contestId}")
@Operation(summary = "获取用户在某活动中的报名记录")
@PreAuthorize("hasAuthority('contest:read')")
public Result<RegistrationVO> getMyRegistration(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long contestId) {
Long userId = userPrincipal.getUserId();
RegistrationVO result = registrationService.getMyRegistration(contestId, userId);
return Result.success(result);
}
@GetMapping("/{id}")
@Operation(summary = "获取报名详情")
@PreAuthorize("hasAuthority('contest:read')")
public Result<RegistrationVO> getDetail(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
RegistrationVO result = registrationService.getDetail(id, tenantId);
return Result.success(result);
}
@PatchMapping("/{id}/review")
@Operation(summary = "审核报名")
@PreAuthorize("hasAuthority('contest:update')")
public Result<RegistrationVO> review(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id,
@RequestBody @Validated ReviewRegistrationDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long modifierId = userPrincipal.getUserId();
RegistrationVO result = registrationService.review(id, dto, tenantId, modifierId);
return Result.success(result);
}
@PostMapping("/{id}/teachers")
@Operation(summary = "添加指导老师")
@PreAuthorize("hasAuthority('contest:update')")
public Result<RegistrationVO> addTeacher(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id,
@RequestBody @Validated AddTeacherDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long operatorId = userPrincipal.getUserId();
RegistrationVO result = registrationService.addTeacher(id, dto.getTeacherUserId(), tenantId, operatorId);
return Result.success(result);
}
@DeleteMapping("/{id}/teachers/{teacherUserId}")
@Operation(summary = "移除指导老师")
@PreAuthorize("hasAuthority('contest:update')")
public Result<Void> removeTeacher(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id,
@PathVariable Long teacherUserId) {
Long tenantId = userPrincipal.getTenantId();
registrationService.removeTeacher(id, teacherUserId, tenantId);
return Result.success(null);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除报名")
@PreAuthorize("hasAuthority('contest:update')")
public Result<Void> delete(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
registrationService.delete(id, tenantId);
return Result.success(null);
}
}

View File

@ -0,0 +1,90 @@
package com.lesingle.creation.controller;
import com.lesingle.creation.common.core.Result;
import com.lesingle.creation.common.security.UserPrincipal;
import com.lesingle.creation.dto.result.CreateResultDTO;
import com.lesingle.creation.service.ContestResultService;
import com.lesingle.creation.vo.result.ResultVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 竞赛结果管理控制器
*/
@Tag(name = "竞赛结果管理")
@RestController
@RequestMapping("/api/contests/results")
@RequiredArgsConstructor
public class ContestResultController {
private final ContestResultService resultService;
@PostMapping
@Operation(summary = "创建竞赛结果")
@PreAuthorize("hasAuthority('contest:result:create')")
public Result<ResultVO> create(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Validated CreateResultDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long creatorId = userPrincipal.getUserId();
ResultVO result = resultService.create(dto, creatorId);
return Result.success(result);
}
@PutMapping("/{id}")
@Operation(summary = "更新竞赛结果")
@PreAuthorize("hasAuthority('contest:result:update')")
public Result<ResultVO> update(
@PathVariable Long id,
@RequestBody @Validated CreateResultDTO dto) {
ResultVO result = resultService.update(id, dto);
return Result.success(result);
}
@PostMapping("/{id}/publish")
@Operation(summary = "发布竞赛结果")
@PreAuthorize("hasAuthority('contest:result:publish')")
public Result<ResultVO> publish(@PathVariable Long id) {
ResultVO result = resultService.publish(id);
return Result.success(result);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除竞赛结果")
@PreAuthorize("hasAuthority('contest:result:delete')")
public Result<Void> delete(@PathVariable Long id) {
resultService.delete(id);
return Result.success(null);
}
@GetMapping("/{id}")
@Operation(summary = "获取竞赛结果详情")
@PreAuthorize("hasAuthority('contest:read')")
public Result<ResultVO> getDetail(@PathVariable Long id) {
ResultVO result = resultService.getDetail(id);
return Result.success(result);
}
@GetMapping("/contest/{contestId}")
@Operation(summary = "查询活动的竞赛结果列表")
@PreAuthorize("hasAuthority('contest:read')")
public Result<List<ResultVO>> listByContest(@PathVariable Long contestId) {
List<ResultVO> result = resultService.listByContest(contestId);
return Result.success(result);
}
@GetMapping("/contest/{contestId}/winners")
@Operation(summary = "查询获奖结果列表")
@PreAuthorize("hasAuthority('contest:read')")
public Result<List<ResultVO>> listWinners(@PathVariable Long contestId) {
List<ResultVO> result = resultService.listWinners(contestId);
return Result.success(result);
}
}

View File

@ -0,0 +1,214 @@
package com.lesingle.creation.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.lesingle.creation.common.core.Result;
import com.lesingle.creation.common.security.UserPrincipal;
import com.lesingle.creation.dto.review.*;
import com.lesingle.creation.service.ContestReviewService;
import com.lesingle.creation.vo.review.ReviewAssignmentVO;
import com.lesingle.creation.vo.review.ReviewStatsVO;
import com.lesingle.creation.vo.review.WorkScoreVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 竞赛评审管理控制器
*/
@Tag(name = "竞赛评审管理")
@RestController
@RequestMapping("/api/contests/reviews")
@RequiredArgsConstructor
public class ContestReviewController {
private final ContestReviewService reviewService;
@PostMapping("/assign")
@Operation(summary = "分配作品给评委")
@PreAuthorize("hasAuthority('contest:review:assign')")
public Result<List<ReviewAssignmentVO>> assignWork(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestParam Long contestId,
@RequestBody @Validated AssignWorkDTO dto) {
Long tenantId = userPrincipal.getTenantId();
List<ReviewAssignmentVO> result = reviewService.assignWork(contestId, dto, tenantId);
return Result.success(result);
}
@PostMapping("/batch-assign")
@Operation(summary = "批量分配作品")
@PreAuthorize("hasAuthority('contest:review:assign')")
public Result<List<ReviewAssignmentVO>> batchAssign(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Validated BatchAssignDTO dto) {
Long tenantId = userPrincipal.getTenantId();
List<ReviewAssignmentVO> result = reviewService.batchAssign(dto, tenantId);
return Result.success(result);
}
@PostMapping("/score")
@Operation(summary = "评分")
@PreAuthorize("hasAuthority('contest:review:score')")
public Result<WorkScoreVO> score(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Validated CreateScoreDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long judgeId = userPrincipal.getUserId();
WorkScoreVO result = reviewService.score(dto, judgeId, tenantId);
return Result.success(result);
}
@PutMapping("/score/{scoreId}")
@Operation(summary = "更新评分")
@PreAuthorize("hasAuthority('contest:review:score')")
public Result<WorkScoreVO> updateScore(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long scoreId,
@RequestBody @Validated CreateScoreDTO dto) {
Long tenantId = userPrincipal.getTenantId();
WorkScoreVO result = reviewService.updateScore(scoreId, dto, tenantId);
return Result.success(result);
}
@GetMapping("/assignments/page")
@Operation(summary = "分页查询评审分配列表")
@PreAuthorize("hasAuthority('contest:read')")
public Result<Page<ReviewAssignmentVO>> pageQuery(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@ModelAttribute ReviewAssignmentQueryDTO queryDTO) {
Long tenantId = userPrincipal.getTenantId();
Page<ReviewAssignmentVO> result = reviewService.pageQuery(queryDTO, tenantId);
return Result.success(result);
}
@GetMapping("/my-assignments")
@Operation(summary = "获取评委待评审作品列表")
@PreAuthorize("hasAuthority('contest:review:score')")
public Result<List<ReviewAssignmentVO>> getMyAssignments(
@AuthenticationPrincipal UserPrincipal userPrincipal) {
Long judgeId = userPrincipal.getUserId();
Long tenantId = userPrincipal.getTenantId();
List<ReviewAssignmentVO> result = reviewService.getMyAssignments(judgeId, tenantId);
return Result.success(result);
}
@GetMapping("/stats")
@Operation(summary = "评审统计")
@PreAuthorize("hasAuthority('contest:read')")
public Result<ReviewStatsVO> getStats(@RequestParam(required = false) Long contestId) {
ReviewStatsVO result = reviewService.getStats(contestId);
return Result.success(result);
}
@GetMapping("/works/{workId}/scores")
@Operation(summary = "获取作品的评分列表")
@PreAuthorize("hasAuthority('contest:read')")
public Result<List<WorkScoreVO>> getWorkScores(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long workId) {
Long tenantId = userPrincipal.getTenantId();
List<WorkScoreVO> result = reviewService.getWorkScores(workId, tenantId);
return Result.success(result);
}
@GetMapping("/works/{workId}/average-score")
@Operation(summary = "获取作品的平均分")
@PreAuthorize("hasAuthority('contest:read')")
public Result<java.math.BigDecimal> getAverageScore(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long workId) {
Long tenantId = userPrincipal.getTenantId();
java.math.BigDecimal result = reviewService.getAverageScore(workId, tenantId);
return Result.success(result);
}
@PostMapping("/auto-assign")
@Operation(summary = "自动分配作品给评委")
@PreAuthorize("hasAuthority('contest:review:assign')")
public Result<String> autoAssign(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestParam Long contestId) {
Long tenantId = userPrincipal.getTenantId();
String result = reviewService.autoAssign(contestId, tenantId);
return Result.success(result);
}
@GetMapping("/progress/{contestId}")
@Operation(summary = "获取评审进度统计")
@PreAuthorize("hasAuthority('contest:read')")
public Result<com.lesingle.creation.vo.review.ReviewProgressVO> getReviewProgress(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long contestId) {
Long tenantId = userPrincipal.getTenantId();
com.lesingle.creation.vo.review.ReviewProgressVO result =
reviewService.getReviewProgress(contestId, tenantId);
return Result.success(result);
}
@GetMapping("/work-status/{contestId}")
@Operation(summary = "获取作品状态统计")
@PreAuthorize("hasAuthority('contest:read')")
public Result<com.lesingle.creation.vo.review.WorkStatusStatsVO> getWorkStatusStats(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long contestId) {
Long tenantId = userPrincipal.getTenantId();
com.lesingle.creation.vo.review.WorkStatusStatsVO result =
reviewService.getWorkStatusStats(contestId, tenantId);
return Result.success(result);
}
@GetMapping("/works/{workId}/final-score")
@Operation(summary = "获取作品的最终得分")
@PreAuthorize("hasAuthority('contest:read')")
public Result<com.lesingle.creation.vo.review.FinalScoreVO> getFinalScore(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long workId) {
Long tenantId = userPrincipal.getTenantId();
com.lesingle.creation.vo.review.FinalScoreVO result =
reviewService.getFinalScore(workId, tenantId);
return Result.success(result);
}
@PostMapping("/replace-judge")
@Operation(summary = "替换评委")
@PreAuthorize("hasAuthority('contest:review:assign')")
public Result<Void> replaceJudge(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestParam Long assignmentId,
@RequestParam Long newJudgeId) {
Long tenantId = userPrincipal.getTenantId();
reviewService.replaceJudge(assignmentId, newJudgeId, tenantId);
return Result.success(null);
}
@GetMapping("/judge/contests")
@Operation(summary = "获取评委参与的活动列表")
public Result<java.util.List<com.lesingle.creation.vo.review.JudgeContestVO>> getJudgeContests(
@AuthenticationPrincipal UserPrincipal userPrincipal) {
Long judgeId = userPrincipal.getUserId();
Long tenantId = userPrincipal.getTenantId();
java.util.List<com.lesingle.creation.vo.review.JudgeContestVO> result =
reviewService.getJudgeContests(judgeId, tenantId);
return Result.success(result);
}
@GetMapping("/judge/contests/{contestId}/works")
@Operation(summary = "获取评委在某个活动下的作品列表")
public Result<com.baomidou.mybatisplus.extension.plugins.pagination.Page<com.lesingle.creation.vo.review.JudgeWorkVO>>
getJudgeContestWorks(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long contestId,
@ModelAttribute com.lesingle.creation.dto.review.JudgeWorkQueryDTO queryDTO) {
Long judgeId = userPrincipal.getUserId();
Long tenantId = userPrincipal.getTenantId();
com.baomidou.mybatisplus.extension.plugins.pagination.Page<com.lesingle.creation.vo.review.JudgeWorkVO> result =
reviewService.getJudgeContestWorks(contestId, judgeId, tenantId, queryDTO);
return Result.success(result);
}
}

View File

@ -0,0 +1,92 @@
package com.lesingle.creation.controller;
import com.lesingle.creation.common.core.Result;
import com.lesingle.creation.common.security.UserPrincipal;
import com.lesingle.creation.dto.reviewrule.CreateReviewDimensionDTO;
import com.lesingle.creation.dto.reviewrule.CreateReviewRuleDTO;
import com.lesingle.creation.service.ContestReviewRuleService;
import com.lesingle.creation.vo.reviewrule.ReviewRuleVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 竞赛评审规则管理控制器
*/
@Tag(name = "竞赛评审规则管理")
@RestController
@RequestMapping("/api/contests/review-rules")
@RequiredArgsConstructor
public class ContestReviewRuleController {
private final ContestReviewRuleService reviewRuleService;
@PostMapping
@Operation(summary = "创建评审规则")
@PreAuthorize("hasAuthority('contest:review-rule:create')")
public Result<ReviewRuleVO> create(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Validated CreateReviewRuleDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long creatorId = userPrincipal.getUserId();
ReviewRuleVO result = reviewRuleService.create(dto, tenantId, creatorId);
return Result.success(result);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除评审规则")
@PreAuthorize("hasAuthority('contest:review-rule:delete')")
public Result<Void> delete(@PathVariable Long id) {
reviewRuleService.delete(id);
return Result.success(null);
}
@GetMapping("/{id}")
@Operation(summary = "获取评审规则详情")
@PreAuthorize("hasAuthority('contest:read')")
public Result<ReviewRuleVO> getDetail(@PathVariable Long id) {
ReviewRuleVO result = reviewRuleService.getDetail(id);
return Result.success(result);
}
@GetMapping("/list")
@Operation(summary = "查询评审规则列表")
@PreAuthorize("hasAuthority('contest:read')")
public Result<List<ReviewRuleVO>> list(
@AuthenticationPrincipal UserPrincipal userPrincipal) {
Long tenantId = userPrincipal.getTenantId();
List<ReviewRuleVO> result = reviewRuleService.list(tenantId);
return Result.success(result);
}
@GetMapping("/select")
@Operation(summary = "获取可选的评审规则")
public Result<List<ReviewRuleVO>> listForSelect(
@AuthenticationPrincipal UserPrincipal userPrincipal) {
Long tenantId = userPrincipal.getTenantId();
List<ReviewRuleVO> result = reviewRuleService.listForSelect(tenantId);
return Result.success(result);
}
@PostMapping("/dimensions")
@Operation(summary = "添加评审维度")
@PreAuthorize("hasAuthority('contest:review-rule:update')")
public Result<ReviewRuleVO> addDimension(@RequestBody @Validated CreateReviewDimensionDTO dto) {
ReviewRuleVO result = reviewRuleService.addDimension(dto);
return Result.success(result);
}
@DeleteMapping("/dimensions/{id}")
@Operation(summary = "删除评审维度")
@PreAuthorize("hasAuthority('contest:review-rule:update')")
public Result<Void> deleteDimension(@PathVariable Long id) {
reviewRuleService.deleteDimension(id);
return Result.success(null);
}
}

View File

@ -0,0 +1,133 @@
package com.lesingle.creation.controller;
import com.lesingle.creation.common.core.Result;
import com.lesingle.creation.common.security.UserPrincipal;
import com.lesingle.creation.dto.team.*;
import com.lesingle.creation.service.ContestTeamService;
import com.lesingle.creation.vo.team.TeamVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 竞赛团队管理控制器
*/
@Tag(name = "竞赛团队管理")
@RestController
@RequestMapping("/api/contests/teams")
@RequiredArgsConstructor
public class ContestTeamController {
private final ContestTeamService teamService;
@PostMapping
@Operation(summary = "创建团队")
@PreAuthorize("hasAuthority('contest:team:create')")
public Result<TeamVO> create(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Validated CreateTeamDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long creatorId = userPrincipal.getUserId();
TeamVO result = teamService.create(dto, tenantId, creatorId);
return Result.success(result);
}
@PutMapping("/{id}")
@Operation(summary = "更新团队")
@PreAuthorize("hasAuthority('contest:team:update')")
public Result<TeamVO> update(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id,
@RequestBody @Validated UpdateTeamDTO dto) {
Long tenantId = userPrincipal.getTenantId();
TeamVO result = teamService.update(id, dto, tenantId);
return Result.success(result);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除团队")
@PreAuthorize("hasAuthority('contest:team:delete')")
public Result<Void> delete(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
teamService.delete(id, tenantId);
return Result.success(null);
}
@GetMapping("/{id}")
@Operation(summary = "获取团队详情")
@PreAuthorize("hasAuthority('contest:read')")
public Result<TeamVO> getDetail(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
TeamVO result = teamService.getDetail(id, tenantId);
return Result.success(result);
}
@GetMapping("/list")
@Operation(summary = "查询团队列表")
@PreAuthorize("hasAuthority('contest:read')")
public Result<List<TeamVO>> list(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestParam(required = false) Long contestId) {
Long tenantId = userPrincipal.getTenantId();
List<TeamVO> result = teamService.list(contestId, tenantId);
return Result.success(result);
}
@GetMapping("/contest/{contestId}")
@Operation(summary = "按竞赛查询团队列表")
@PreAuthorize("hasAuthority('contest:read')")
public Result<List<TeamVO>> listByContest(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long contestId) {
Long tenantId = userPrincipal.getTenantId();
List<TeamVO> result = teamService.listByContest(contestId, tenantId);
return Result.success(result);
}
@GetMapping("/my/{contestId}")
@Operation(summary = "获取用户在某活动中的团队")
@PreAuthorize("hasAuthority('contest:read')")
public Result<TeamVO> getMyTeam(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long contestId) {
Long userId = userPrincipal.getUserId();
Long tenantId = userPrincipal.getTenantId();
TeamVO result = teamService.getMyTeam(contestId, userId, tenantId);
return result != null ? Result.success(result) : Result.success(null);
}
@PostMapping("/{id}/members")
@Operation(summary = "邀请成员")
@PreAuthorize("hasAuthority('contest:team:update')")
public Result<Void> inviteMember(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id,
@RequestBody @Validated InviteMemberDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long creatorId = userPrincipal.getUserId();
teamService.inviteMember(id, dto, tenantId, creatorId);
return Result.success(null);
}
@DeleteMapping("/{id}/members/{userId}")
@Operation(summary = "移除成员")
@PreAuthorize("hasAuthority('contest:team:update')")
public Result<Void> removeMember(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id,
@PathVariable Long userId) {
Long tenantId = userPrincipal.getTenantId();
teamService.removeMember(id, userId, tenantId);
return Result.success(null);
}
}

View File

@ -0,0 +1,129 @@
package com.lesingle.creation.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.lesingle.creation.common.core.Result;
import com.lesingle.creation.common.security.UserPrincipal;
import com.lesingle.creation.dto.work.SubmitWorkDTO;
import com.lesingle.creation.dto.work.WorkQueryDTO;
import com.lesingle.creation.service.ContestWorkService;
import com.lesingle.creation.vo.work.WorkStatsVO;
import com.lesingle.creation.vo.work.WorkVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 竞赛作品管理控制器
*/
@Tag(name = "竞赛作品管理")
@RestController
@RequestMapping("/api/contests/works")
@RequiredArgsConstructor
public class ContestWorkController {
private final ContestWorkService workService;
@PostMapping
@Operation(summary = "提交作品")
@PreAuthorize("hasAuthority('contest:work:submit')")
public Result<WorkVO> submit(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Validated SubmitWorkDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long submitterUserId = userPrincipal.getUserId();
WorkVO result = workService.submit(dto, tenantId, submitterUserId);
return Result.success(result);
}
@GetMapping("/stats")
@Operation(summary = "作品统计")
@PreAuthorize("hasAuthority('contest:read')")
public Result<WorkStatsVO> getStats(@RequestParam(required = false) Long contestId) {
WorkStatsVO result = workService.getStats(contestId);
return Result.success(result);
}
@GetMapping("/page")
@Operation(summary = "分页查询作品列表")
@PreAuthorize("hasAuthority('contest:read')")
public Result<Page<WorkVO>> pageQuery(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@ModelAttribute WorkQueryDTO queryDTO) {
Long tenantId = userPrincipal.getTenantId();
Page<WorkVO> result = workService.pageQuery(queryDTO, tenantId);
return Result.success(result);
}
@GetMapping("/{id}")
@Operation(summary = "获取作品详情")
@PreAuthorize("hasAuthority('contest:read')")
public Result<WorkVO> getDetail(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
WorkVO result = workService.getDetail(id, tenantId);
return Result.success(result);
}
@PutMapping("/{id}/return")
@Operation(summary = "退回作品")
@PreAuthorize("hasAuthority('contest:work:update')")
public Result<WorkVO> returnWork(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id,
@RequestParam String reason) {
Long tenantId = userPrincipal.getTenantId();
WorkVO result = workService.returnWork(id, tenantId, reason);
return Result.success(result);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除作品")
@PreAuthorize("hasAuthority('contest:work:delete')")
public Result<Void> delete(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
workService.delete(id, tenantId);
return Result.success(null);
}
@GetMapping("/registration/{registrationId}")
@Operation(summary = "查询某报名的所有作品")
@PreAuthorize("hasAuthority('contest:read')")
public Result<List<WorkVO>> listByRegistration(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long registrationId) {
Long tenantId = userPrincipal.getTenantId();
List<WorkVO> result = workService.listByRegistration(registrationId, tenantId);
return Result.success(result);
}
@GetMapping("/registration/{registrationId}/versions")
@Operation(summary = "查询某报名的所有作品版本")
@PreAuthorize("hasAuthority('contest:read')")
public Result<List<WorkVO>> getVersionsByRegistration(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long registrationId) {
Long tenantId = userPrincipal.getTenantId();
List<WorkVO> result = workService.getVersionsByRegistration(registrationId, tenantId);
return Result.success(result);
}
@GetMapping("/guided")
@Operation(summary = "查询教师指导的作品列表")
public Result<Page<WorkVO>> getGuidedWorks(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@ModelAttribute WorkQueryDTO queryDTO) {
Long userId = userPrincipal.getUserId();
Long tenantId = userPrincipal.getTenantId();
Page<WorkVO> result = workService.getGuidedWorks(queryDTO, userId, tenantId);
return Result.success(result);
}
}

View File

@ -0,0 +1,97 @@
package com.lesingle.creation.controller;
import com.lesingle.creation.common.core.Result;
import com.lesingle.creation.common.security.UserPrincipal;
import com.lesingle.creation.dto.department.CreateDepartmentDTO;
import com.lesingle.creation.dto.department.UpdateDepartmentDTO;
import com.lesingle.creation.service.DepartmentService;
import com.lesingle.creation.vo.department.DepartmentVO;
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.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 部门管理控制器
*/
@Tag(name = "部门管理", description = "部门 CRUD 和树形结构接口")
@RestController
@RequestMapping("/api/departments")
@RequiredArgsConstructor
public class DepartmentController {
private final DepartmentService departmentService;
@PostMapping
@Operation(summary = "创建部门")
@PreAuthorize("hasAuthority('department:create')")
public Result<DepartmentVO> create(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Valid CreateDepartmentDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long creatorId = userPrincipal.getUserId();
DepartmentVO result = departmentService.create(dto, tenantId, creatorId);
return Result.success(result);
}
@GetMapping
@Operation(summary = "查询部门列表")
@PreAuthorize("hasAuthority('department:read')")
public Result<List<DepartmentVO>> list(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestParam(required = false) Long parentId) {
Long tenantId = userPrincipal.getTenantId();
List<DepartmentVO> result = departmentService.list(tenantId, parentId);
return Result.success(result);
}
@GetMapping("/tree")
@Operation(summary = "查询部门树形结构")
@PreAuthorize("hasAuthority('department:read')")
public Result<List<DepartmentVO>> tree(
@AuthenticationPrincipal UserPrincipal userPrincipal) {
Long tenantId = userPrincipal.getTenantId();
List<DepartmentVO> result = departmentService.tree(tenantId);
return Result.success(result);
}
@GetMapping("/{id}")
@Operation(summary = "获取部门详情")
@PreAuthorize("hasAuthority('department:read')")
public Result<DepartmentVO> getDetail(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
DepartmentVO result = departmentService.getDetail(id, tenantId);
return Result.success(result);
}
@PutMapping("/{id}")
@Operation(summary = "更新部门")
@PreAuthorize("hasAuthority('department:update')")
public Result<DepartmentVO> update(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id,
@RequestBody @Valid UpdateDepartmentDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long modifierId = userPrincipal.getUserId();
DepartmentVO result = departmentService.update(id, dto, tenantId, modifierId);
return Result.success(result);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除部门")
@PreAuthorize("hasAuthority('department:delete')")
public Result<Void> delete(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
departmentService.delete(id, tenantId);
return Result.success(null);
}
}

View File

@ -0,0 +1,100 @@
package com.lesingle.creation.controller;
import com.lesingle.creation.common.core.Result;
import com.lesingle.creation.common.security.UserPrincipal;
import com.lesingle.creation.dto.dict.CreateDictDTO;
import com.lesingle.creation.dto.dict.UpdateDictDTO;
import com.lesingle.creation.service.DictService;
import com.lesingle.creation.vo.dict.DictDetailVO;
import com.lesingle.creation.vo.dict.DictListVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 字典管理控制器
*/
@Tag(name = "字典管理")
@RestController
@RequestMapping("/api/dict")
@RequiredArgsConstructor
public class DictController {
private final DictService dictService;
@PostMapping
@Operation(summary = "创建字典")
@PreAuthorize("hasAuthority('dict:create')")
public Result<DictDetailVO> create(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Validated CreateDictDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long operatorId = userPrincipal.getUserId();
DictDetailVO result = dictService.create(dto, tenantId, operatorId);
return Result.success(result);
}
@GetMapping
@Operation(summary = "字典列表")
@PreAuthorize("hasAuthority('dict:read')")
public Result<com.baomidou.mybatisplus.extension.plugins.pagination.Page<DictListVO>> pageList(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int pageSize) {
Long tenantId = userPrincipal.getTenantId();
com.baomidou.mybatisplus.extension.plugins.pagination.Page<DictListVO> result =
dictService.pageList(tenantId, page, pageSize);
return Result.success(result);
}
@GetMapping("/code/{code}")
@Operation(summary = "根据编码查询字典")
@PreAuthorize("hasAuthority('dict:read')")
public Result<DictDetailVO> getByCode(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable String code) {
Long tenantId = userPrincipal.getTenantId();
DictDetailVO result = dictService.getByCode(code, tenantId);
return Result.success(result);
}
@GetMapping("/{id}")
@Operation(summary = "字典详情")
@PreAuthorize("hasAuthority('dict:read')")
public Result<DictDetailVO> detail(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
DictDetailVO result = dictService.detail(id, tenantId);
return Result.success(result);
}
@PutMapping("/{id}")
@Operation(summary = "更新字典")
@PreAuthorize("hasAuthority('dict:update')")
public Result<DictDetailVO> update(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id,
@RequestBody @Validated UpdateDictDTO dto) {
Long tenantId = userPrincipal.getTenantId();
DictDetailVO result = dictService.update(id, dto, tenantId);
return Result.success(result);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除字典")
@PreAuthorize("hasAuthority('dict:delete')")
public Result<Void> delete(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
dictService.delete(id, tenantId);
return Result.success();
}
}

View File

@ -0,0 +1,87 @@
package com.lesingle.creation.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.lesingle.creation.common.core.Result;
import com.lesingle.creation.common.security.UserPrincipal;
import com.lesingle.creation.dto.grade.CreateGradeDTO;
import com.lesingle.creation.dto.grade.UpdateGradeDTO;
import com.lesingle.creation.service.GradeService;
import com.lesingle.creation.vo.grade.GradeVO;
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.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
/**
* 年级管理控制器
*/
@Tag(name = "年级管理", description = "年级 CRUD 接口")
@RestController
@RequestMapping("/api/grades")
@RequiredArgsConstructor
public class GradeController {
private final GradeService gradeService;
@PostMapping
@Operation(summary = "创建年级")
@PreAuthorize("hasAuthority('grade:create')")
public Result<GradeVO> create(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Valid CreateGradeDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long creatorId = userPrincipal.getUserId();
GradeVO result = gradeService.create(dto, tenantId, creatorId);
return Result.success(result);
}
@GetMapping
@Operation(summary = "分页查询年级列表")
@PreAuthorize("hasAuthority('grade:read')")
public Result<Page<GradeVO>> pageQuery(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestParam(required = false, defaultValue = "1") Integer pageNum,
@RequestParam(required = false, defaultValue = "10") Integer pageSize) {
Long tenantId = userPrincipal.getTenantId();
Page<GradeVO> result = gradeService.pageQuery(pageNum, pageSize, tenantId);
return Result.success(result);
}
@GetMapping("/{id}")
@Operation(summary = "获取年级详情")
@PreAuthorize("hasAuthority('grade:read')")
public Result<GradeVO> getDetail(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
GradeVO result = gradeService.getDetail(id, tenantId);
return Result.success(result);
}
@PutMapping("/{id}")
@Operation(summary = "更新年级")
@PreAuthorize("hasAuthority('grade:update')")
public Result<GradeVO> update(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id,
@RequestBody @Valid UpdateGradeDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long modifierId = userPrincipal.getUserId();
GradeVO result = gradeService.update(id, dto, tenantId, modifierId);
return Result.success(result);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除年级")
@PreAuthorize("hasAuthority('grade:delete')")
public Result<Void> delete(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
gradeService.delete(id, tenantId);
return Result.success(null);
}
}

View File

@ -0,0 +1,290 @@
package com.lesingle.creation.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.lesingle.creation.common.core.Result;
import com.lesingle.creation.dto.homework.*;
import com.lesingle.creation.vo.school.ClassTreeNodeVO;
import com.lesingle.creation.service.HomeworkService;
import com.lesingle.creation.vo.homework.HomeworkDetailVO;
import com.lesingle.creation.vo.homework.HomeworkListVO;
import com.lesingle.creation.vo.homework.ReviewRuleVO;
import com.lesingle.creation.vo.homework.SubmissionVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import com.lesingle.creation.common.security.UserPrincipal;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 作业管理控制器
*/
@Tag(name = "作业管理")
@RestController
@RequestMapping("/api/homeworks")
@RequiredArgsConstructor
public class HomeworkController {
private final HomeworkService homeworkService;
@PostMapping
@Operation(summary = "创建作业")
@PreAuthorize("hasAuthority('homework:create')")
public Result<HomeworkDetailVO> create(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Validated CreateHomeworkDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long creatorId = userPrincipal.getUserId();
HomeworkDetailVO result = homeworkService.create(dto, tenantId, creatorId);
return Result.success(result);
}
@PutMapping("/{id}")
@Operation(summary = "更新作业")
@PreAuthorize("hasAuthority('homework:update')")
public Result<HomeworkDetailVO> update(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id,
@RequestBody @Validated UpdateHomeworkDTO dto) {
Long tenantId = userPrincipal.getTenantId();
HomeworkDetailVO result = homeworkService.update(id, dto, tenantId);
return Result.success(result);
}
@PostMapping("/{id}/publish")
@Operation(summary = "发布作业")
@PreAuthorize("hasAuthority('homework:publish')")
public Result<HomeworkDetailVO> publish(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
HomeworkDetailVO result = homeworkService.publish(id, tenantId);
return Result.success(result);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除作业")
@PreAuthorize("hasAuthority('homework:delete')")
public Result<Void> delete(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
homeworkService.delete(id, tenantId);
return Result.success(null);
}
@GetMapping("/{id}")
@Operation(summary = "获取作业详情")
public Result<HomeworkDetailVO> getDetail(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
HomeworkDetailVO result = homeworkService.getDetail(id, tenantId);
return Result.success(result);
}
@GetMapping("/page")
@Operation(summary = "分页查询作业列表")
public Result<Page<HomeworkListVO>> pageQuery(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@ModelAttribute HomeworkQueryDTO queryDTO) {
Long tenantId = userPrincipal.getTenantId();
Page<HomeworkListVO> result = homeworkService.pageQuery(queryDTO, tenantId);
return Result.success(result);
}
@PostMapping("/submit")
@Operation(summary = "提交作业")
public Result<SubmissionVO> submit(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Validated SubmitHomeworkDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long studentId = userPrincipal.getUserId();
SubmissionVO result = homeworkService.submit(dto, tenantId, studentId);
return Result.success(result);
}
@PostMapping("/review")
@Operation(summary = "批改作业")
@PreAuthorize("hasAuthority('homework:review')")
public Result<SubmissionVO> review(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Validated ReviewHomeworkDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long reviewerId = userPrincipal.getUserId();
SubmissionVO result = homeworkService.review(dto, tenantId, reviewerId);
return Result.success(result);
}
@GetMapping("/{homeworkId}/submissions")
@Operation(summary = "获取作业提交列表")
@PreAuthorize("hasAuthority('homework:review')")
public Result<List<SubmissionVO>> getSubmissions(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long homeworkId) {
Long tenantId = userPrincipal.getTenantId();
List<SubmissionVO> result = homeworkService.getSubmissions(homeworkId, tenantId);
return Result.success(result);
}
@PostMapping("/review-rules")
@Operation(summary = "创建评审规则")
@PreAuthorize("hasAuthority('homework:review-rule:create')")
public Result<ReviewRuleVO> createReviewRule(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestParam String ruleName,
@RequestParam(required = false, defaultValue = "custom") String ruleType,
@RequestParam(required = false) java.math.BigDecimal totalScore,
@RequestParam(required = false) String description) {
Long tenantId = userPrincipal.getTenantId();
ReviewRuleVO result = homeworkService.createReviewRule(
ruleName, ruleType, totalScore, description, tenantId);
return Result.success(result);
}
@GetMapping("/review-rules")
@Operation(summary = "获取评审规则列表")
public Result<List<ReviewRuleVO>> getReviewRules(
@AuthenticationPrincipal UserPrincipal userPrincipal) {
Long tenantId = userPrincipal.getTenantId();
List<ReviewRuleVO> result = homeworkService.getReviewRules(tenantId);
return Result.success(result);
}
@GetMapping("/review-rules/select")
@Operation(summary = "获取可选的评审规则")
public Result<List<ReviewRuleVO>> getReviewRulesForSelect(
@AuthenticationPrincipal UserPrincipal userPrincipal) {
Long tenantId = userPrincipal.getTenantId();
List<ReviewRuleVO> result = homeworkService.getReviewRulesForSelect(tenantId);
return Result.success(result);
}
@PutMapping("/review-rules/{id}")
@Operation(summary = "更新评审规则")
@PreAuthorize("hasAuthority('homework:review-rule:update')")
public Result<ReviewRuleVO> updateReviewRule(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id,
@RequestBody @Validated UpdateReviewRuleDTO dto) {
Long tenantId = userPrincipal.getTenantId();
ReviewRuleVO result = homeworkService.updateReviewRule(id, dto, tenantId);
return Result.success(result);
}
@DeleteMapping("/review-rules/{id}")
@Operation(summary = "删除评审规则")
@PreAuthorize("hasAuthority('homework:review-rule:delete')")
public Result<Void> deleteReviewRule(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
homeworkService.deleteReviewRule(id, tenantId);
return Result.success(null);
}
@GetMapping("/my")
@Operation(summary = "我的作业列表(学生端)")
public Result<Page<HomeworkListVO>> getMyHomeworks(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@ModelAttribute HomeworkQueryDTO queryDTO) {
Long userId = userPrincipal.getUserId();
Long tenantId = userPrincipal.getTenantId();
Page<HomeworkListVO> result = homeworkService.getMyHomeworks(queryDTO, userId, tenantId);
return Result.success(result);
}
@PostMapping("/{id}/unpublish")
@Operation(summary = "取消发布作业")
@PreAuthorize("hasAuthority('homework:publish')")
public Result<HomeworkDetailVO> unpublish(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
HomeworkDetailVO result = homeworkService.unpublish(id, tenantId);
return Result.success(result);
}
@GetMapping("/submissions")
@Operation(summary = "获取提交记录列表")
@PreAuthorize("hasAuthority('homework:review')")
public Result<Page<SubmissionVO>> getSubmissionsList(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@ModelAttribute SubmissionQueryDTO queryDTO) {
Long tenantId = userPrincipal.getTenantId();
Page<SubmissionVO> result = homeworkService.getSubmissionsList(queryDTO, tenantId);
return Result.success(result);
}
@GetMapping("/submissions/{id}")
@Operation(summary = "获取提交记录详情")
@PreAuthorize("hasAuthority('homework:review')")
public Result<SubmissionVO> getSubmissionDetail(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
SubmissionVO result = homeworkService.getSubmissionDetail(id, tenantId);
return Result.success(result);
}
@GetMapping("/submissions/class-tree")
@Operation(summary = "获取班级树结构")
@PreAuthorize("hasAuthority('homework:review')")
public Result<List<ClassTreeNodeVO>> getClassTree(
@AuthenticationPrincipal UserPrincipal userPrincipal) {
Long tenantId = userPrincipal.getTenantId();
List<ClassTreeNodeVO> result = homeworkService.getClassTree(tenantId);
return Result.success(result);
}
@GetMapping("/submissions/my/{homeworkId}")
@Operation(summary = "获取我的提交记录")
public Result<SubmissionVO> getMySubmission(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long homeworkId) {
Long userId = userPrincipal.getUserId();
Long tenantId = userPrincipal.getTenantId();
SubmissionVO result = homeworkService.getMySubmission(homeworkId, userId, tenantId);
return result != null ? Result.success(result) : Result.success(null);
}
@PostMapping("/scores")
@Operation(summary = "提交评分")
@PreAuthorize("hasAuthority('homework:review')")
public Result<SubmissionVO> createScore(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Validated CreateScoreDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long reviewerId = userPrincipal.getUserId();
SubmissionVO result = homeworkService.createScore(dto, tenantId, reviewerId);
return Result.success(result);
}
@PostMapping("/scores/{submissionId}/violation")
@Operation(summary = "标记作品违规")
@PreAuthorize("hasAuthority('homework:review')")
public Result<Void> markViolation(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long submissionId,
@RequestParam(required = false) String reason) {
Long tenantId = userPrincipal.getTenantId();
homeworkService.markViolation(submissionId, reason, tenantId);
return Result.success(null);
}
@PostMapping("/scores/{submissionId}/reset")
@Operation(summary = "重置评分")
@PreAuthorize("hasAuthority('homework:review')")
public Result<Void> resetScore(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long submissionId) {
Long tenantId = userPrincipal.getTenantId();
homeworkService.resetScore(submissionId, tenantId);
return Result.success(null);
}
}

View File

@ -0,0 +1,86 @@
package com.lesingle.creation.controller;
import com.lesingle.creation.common.core.Result;
import com.lesingle.creation.common.security.UserPrincipal;
import com.lesingle.creation.dto.menu.CreateMenuDTO;
import com.lesingle.creation.dto.menu.UpdateMenuDTO;
import com.lesingle.creation.service.MenuService;
import com.lesingle.creation.vo.menu.MenuDetailVO;
import com.lesingle.creation.vo.menu.MenuTreeVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 菜单管理控制器
*/
@Tag(name = "菜单管理")
@RestController
@RequestMapping("/api/menus")
@RequiredArgsConstructor
public class MenuController {
private final MenuService menuService;
@PostMapping
@Operation(summary = "创建菜单")
@PreAuthorize("hasAuthority('menu:create')")
public Result<MenuDetailVO> create(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Validated CreateMenuDTO dto) {
Long operatorId = userPrincipal.getUserId();
MenuDetailVO result = menuService.create(dto, operatorId);
return Result.success(result);
}
@GetMapping
@Operation(summary = "菜单树")
@PreAuthorize("hasAuthority('menu:read')")
public Result<List<MenuTreeVO>> tree() {
List<MenuTreeVO> result = menuService.tree();
return Result.success(result);
}
@GetMapping("/user-menus")
@Operation(summary = "当前用户菜单")
@PreAuthorize("hasAuthority('menu:read')")
public Result<List<MenuTreeVO>> userMenus(
@AuthenticationPrincipal UserPrincipal userPrincipal) {
Long userId = userPrincipal.getUserId();
Long tenantId = userPrincipal.getTenantId();
List<MenuTreeVO> result = menuService.getUserMenus(userId, tenantId);
return Result.success(result);
}
@GetMapping("/{id}")
@Operation(summary = "菜单详情")
@PreAuthorize("hasAuthority('menu:read')")
public Result<MenuDetailVO> detail(@PathVariable Long id) {
MenuDetailVO result = menuService.detail(id);
return Result.success(result);
}
@PutMapping("/{id}")
@Operation(summary = "更新菜单")
@PreAuthorize("hasAuthority('menu:update')")
public Result<MenuDetailVO> update(
@PathVariable Long id,
@RequestBody @Validated UpdateMenuDTO dto) {
MenuDetailVO result = menuService.update(id, dto);
return Result.success(result);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除菜单")
@PreAuthorize("hasAuthority('menu:delete')")
public Result<Void> delete(@PathVariable Long id) {
menuService.delete(id);
return Result.success();
}
}

View File

@ -0,0 +1,94 @@
package com.lesingle.creation.controller;
import com.lesingle.creation.common.core.Result;
import com.lesingle.creation.common.security.UserPrincipal;
import com.lesingle.creation.dto.permission.CreatePermissionDTO;
import com.lesingle.creation.dto.permission.UpdatePermissionDTO;
import com.lesingle.creation.service.PermissionService;
import com.lesingle.creation.vo.permission.PermissionDetailVO;
import com.lesingle.creation.vo.permission.PermissionListVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 权限管理控制器
*/
@Tag(name = "权限管理")
@RestController
@RequestMapping("/api/permissions")
@RequiredArgsConstructor
public class PermissionController {
private final PermissionService permissionService;
@PostMapping
@Operation(summary = "创建权限")
@PreAuthorize("hasAuthority('super_admin')")
public Result<PermissionDetailVO> create(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Validated CreatePermissionDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long operatorId = userPrincipal.getUserId();
PermissionDetailVO result = permissionService.create(dto, tenantId, operatorId);
return Result.success(result);
}
@GetMapping
@Operation(summary = "权限列表")
@PreAuthorize("hasAuthority('permission:read')")
public Result<com.baomidou.mybatisplus.extension.plugins.pagination.Page<PermissionListVO>> pageList(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int pageSize) {
Long tenantId = userPrincipal.getTenantId();
com.baomidou.mybatisplus.extension.plugins.pagination.Page<PermissionListVO> result =
permissionService.pageList(tenantId, page, pageSize);
return Result.success(result);
}
@GetMapping("/all")
@Operation(summary = "所有权限列表")
@PreAuthorize("hasAuthority('permission:read')")
public Result<List<PermissionListVO>> list(
@AuthenticationPrincipal UserPrincipal userPrincipal) {
Long tenantId = userPrincipal.getTenantId();
List<PermissionListVO> result = permissionService.list(tenantId);
return Result.success(result);
}
@GetMapping("/{id}")
@Operation(summary = "权限详情")
@PreAuthorize("hasAuthority('permission:read')")
public Result<PermissionDetailVO> detail(@PathVariable Long id) {
PermissionDetailVO result = permissionService.detail(id);
return Result.success(result);
}
@PutMapping("/{id}")
@Operation(summary = "更新权限")
@PreAuthorize("hasAuthority('super_admin')")
public Result<PermissionDetailVO> update(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id,
@RequestBody @Validated UpdatePermissionDTO dto) {
Long tenantId = userPrincipal.getTenantId();
PermissionDetailVO result = permissionService.update(id, dto, tenantId);
return Result.success(result);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除权限")
@PreAuthorize("hasAuthority('super_admin')")
public Result<Void> delete(
@PathVariable Long id) {
permissionService.delete(id);
return Result.success();
}
}

View File

@ -0,0 +1,176 @@
package com.lesingle.creation.controller;
import com.lesingle.creation.common.core.Result;
import com.lesingle.creation.common.security.UserPrincipal;
import com.lesingle.creation.dto.child.CreateChildDTO;
import com.lesingle.creation.dto.child.UpdateChildDTO;
import com.lesingle.creation.dto.publicuser.PublicLoginDTO;
import com.lesingle.creation.dto.publicuser.PublicRegisterDTO;
import com.lesingle.creation.dto.publicuser.PublicUserUpdateDTO;
import com.lesingle.creation.service.PublicService;
import com.lesingle.creation.vo.child.ChildVO;
import com.lesingle.creation.vo.publicuser.PublicUserVO;
import com.lesingle.creation.vo.publicuser.LoginResponseVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
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.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 公共接口控制器
* 提供公众用户注册登录个人信息子女管理等功能
*/
@Tag(name = "公共接口", description = "公众用户相关接口")
@RestController
@RequestMapping("/api/public")
@RequiredArgsConstructor
public class PublicController {
private final PublicService publicService;
// ==================== 注册 & 登录公开接口 ====================
@PostMapping("/auth/register")
@Operation(summary = "用户注册")
public Result<LoginResponseVO> register(@RequestBody @Valid PublicRegisterDTO dto) {
return Result.success(publicService.register(dto));
}
@PostMapping("/auth/login")
@Operation(summary = "用户登录")
public Result<LoginResponseVO> login(@RequestBody @Valid PublicLoginDTO dto) {
return Result.success(publicService.login(dto));
}
// ==================== 个人信息需要登录 ====================
@GetMapping("/mine/profile")
@Operation(summary = "获取个人信息")
public Result<PublicUserVO> getProfile(@AuthenticationPrincipal UserPrincipal userPrincipal) {
Long userId = userPrincipal.getUserId();
return Result.success(publicService.getUserInfo(userId));
}
@PutMapping("/mine/profile")
@Operation(summary = "更新个人信息")
public Result<PublicUserVO> updateProfile(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody PublicUserUpdateDTO dto) {
Long userId = userPrincipal.getUserId();
return Result.success(publicService.updateProfile(userId, dto));
}
// ==================== 子女管理需要登录 ====================
@GetMapping("/mine/children")
@Operation(summary = "获取子女列表")
public Result<List<ChildVO>> getChildren(@AuthenticationPrincipal UserPrincipal userPrincipal) {
Long userId = userPrincipal.getUserId();
return Result.success(publicService.getChildren(userId));
}
@PostMapping("/mine/children")
@Operation(summary = "创建子女")
public Result<ChildVO> createChild(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Valid CreateChildDTO dto) {
Long userId = userPrincipal.getUserId();
return Result.success(publicService.createChild(userId, dto));
}
@GetMapping("/mine/children/{id}")
@Operation(summary = "获取子女详情")
public Result<ChildVO> getChild(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long userId = userPrincipal.getUserId();
return Result.success(publicService.getChild(userId, id));
}
@PutMapping("/mine/children/{id}")
@Operation(summary = "更新子女信息")
public Result<ChildVO> updateChild(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id,
@RequestBody UpdateChildDTO dto) {
Long userId = userPrincipal.getUserId();
return Result.success(publicService.updateChild(userId, id, dto));
}
@DeleteMapping("/mine/children/{id}")
@Operation(summary = "删除子女")
public Result<Void> deleteChild(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long userId = userPrincipal.getUserId();
publicService.deleteChild(userId, id);
return Result.success(null);
}
// ==================== 子女独立账号管理 ====================
@PostMapping("/children/create-account")
@Operation(summary = "创建子女独立账号")
public Result<ChildVO> createChildAccount(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Valid CreateChildDTO dto) {
Long userId = userPrincipal.getUserId();
return Result.success(publicService.createChildAccount(userId, dto));
}
@GetMapping("/children/accounts")
@Operation(summary = "获取子女账号列表")
public Result<List<ChildVO>> getChildAccounts(@AuthenticationPrincipal UserPrincipal userPrincipal) {
Long userId = userPrincipal.getUserId();
return Result.success(publicService.getChildAccounts(userId));
}
@PutMapping("/children/accounts/{id}")
@Operation(summary = "更新子女账号信息")
public Result<ChildVO> updateChildAccount(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id,
@RequestBody UpdateChildDTO dto) {
Long userId = userPrincipal.getUserId();
return Result.success(publicService.updateChildAccount(userId, id, dto));
}
@GetMapping("/mine/parent-info")
@Operation(summary = "获取监护人信息")
public Result<PublicUserVO> getParentInfo(@AuthenticationPrincipal UserPrincipal userPrincipal) {
Long userId = userPrincipal.getUserId();
return Result.success(publicService.getParentInfo(userId));
}
// ==================== 活动浏览公开接口 ====================
@GetMapping("/activities")
@Operation(summary = "获取活动列表")
public Result getActivities(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "12") Integer pageSize,
@RequestParam(required = false) String keyword,
@RequestParam(required = false) String contestType,
@AuthenticationPrincipal UserPrincipal userPrincipal) {
Long userId = userPrincipal != null ? userPrincipal.getUserId() : null;
return Result.success(publicService.getPublicActivities(page, pageSize, keyword, contestType, userId));
}
@GetMapping("/activities/{id}")
@Operation(summary = "获取活动详情")
public Result getActivityDetail(@PathVariable Long id) {
return Result.success(publicService.getActivityDetail(id));
}
}

View File

@ -0,0 +1,94 @@
package com.lesingle.creation.controller;
import com.lesingle.creation.common.core.Result;
import com.lesingle.creation.common.security.UserPrincipal;
import com.lesingle.creation.dto.role.CreateRoleDTO;
import com.lesingle.creation.dto.role.UpdateRoleDTO;
import com.lesingle.creation.service.RoleService;
import com.lesingle.creation.vo.role.RoleDetailVO;
import com.lesingle.creation.vo.role.RoleListVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 角色管理控制器
*/
@Tag(name = "角色管理")
@RestController
@RequestMapping("/api/roles")
@RequiredArgsConstructor
public class RoleController {
private final RoleService roleService;
@PostMapping
@Operation(summary = "创建角色")
@PreAuthorize("hasAuthority('role:create')")
public Result<RoleDetailVO> create(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Validated CreateRoleDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long operatorId = userPrincipal.getUserId();
RoleDetailVO result = roleService.create(dto, tenantId, operatorId);
return Result.success(result);
}
@GetMapping
@Operation(summary = "角色列表")
@PreAuthorize("hasAuthority('role:read')")
public Result<com.baomidou.mybatisplus.extension.plugins.pagination.Page<RoleListVO>> pageList(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int pageSize) {
Long tenantId = userPrincipal.getTenantId();
com.baomidou.mybatisplus.extension.plugins.pagination.Page<RoleListVO> result =
roleService.pageList(tenantId, page, pageSize);
return Result.success(result);
}
@GetMapping("/all")
@Operation(summary = "所有角色列表")
@PreAuthorize("hasAuthority('role:read')")
public Result<List<RoleListVO>> list(
@AuthenticationPrincipal UserPrincipal userPrincipal) {
Long tenantId = userPrincipal.getTenantId();
List<RoleListVO> result = roleService.list(tenantId);
return Result.success(result);
}
@GetMapping("/{id}")
@Operation(summary = "角色详情")
@PreAuthorize("hasAuthority('role:read')")
public Result<RoleDetailVO> detail(@PathVariable Long id) {
RoleDetailVO result = roleService.detail(id);
return Result.success(result);
}
@PutMapping("/{id}")
@Operation(summary = "更新角色")
@PreAuthorize("hasAuthority('role:update')")
public Result<RoleDetailVO> update(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id,
@RequestBody @Validated UpdateRoleDTO dto) {
Long tenantId = userPrincipal.getTenantId();
RoleDetailVO result = roleService.update(id, dto, tenantId);
return Result.success(result);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除角色")
@PreAuthorize("hasAuthority('role:delete')")
public Result<Void> delete(
@PathVariable Long id) {
roleService.delete(id);
return Result.success();
}
}

View File

@ -0,0 +1,89 @@
package com.lesingle.creation.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.lesingle.creation.common.core.Result;
import com.lesingle.creation.common.security.UserPrincipal;
import com.lesingle.creation.dto.schoolclass.CreateClassDTO;
import com.lesingle.creation.dto.schoolclass.UpdateClassDTO;
import com.lesingle.creation.service.SchoolClassService;
import com.lesingle.creation.vo.schoolclass.ClassVO;
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.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
/**
* 班级管理控制器
*/
@Tag(name = "班级管理", description = "班级 CRUD 接口")
@RestController
@RequestMapping("/api/classes")
@RequiredArgsConstructor
public class SchoolClassController {
private final SchoolClassService schoolClassService;
@PostMapping
@Operation(summary = "创建班级")
@PreAuthorize("hasAuthority('class:create')")
public Result<ClassVO> create(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Valid CreateClassDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long creatorId = userPrincipal.getUserId();
ClassVO result = schoolClassService.create(dto, tenantId, creatorId);
return Result.success(result);
}
@GetMapping
@Operation(summary = "分页查询班级列表")
@PreAuthorize("hasAuthority('class:read')")
public Result<Page<ClassVO>> pageQuery(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestParam(required = false, defaultValue = "1") Integer pageNum,
@RequestParam(required = false, defaultValue = "10") Integer pageSize,
@RequestParam(required = false) Long gradeId,
@RequestParam(required = false) Integer type) {
Long tenantId = userPrincipal.getTenantId();
Page<ClassVO> result = schoolClassService.pageQuery(pageNum, pageSize, tenantId, gradeId, type);
return Result.success(result);
}
@GetMapping("/{id}")
@Operation(summary = "获取班级详情")
@PreAuthorize("hasAuthority('class:read')")
public Result<ClassVO> getDetail(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
ClassVO result = schoolClassService.getDetail(id, tenantId);
return Result.success(result);
}
@PutMapping("/{id}")
@Operation(summary = "更新班级")
@PreAuthorize("hasAuthority('class:update')")
public Result<ClassVO> update(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id,
@RequestBody @Valid UpdateClassDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long modifierId = userPrincipal.getUserId();
ClassVO result = schoolClassService.update(id, dto, tenantId, modifierId);
return Result.success(result);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除班级")
@PreAuthorize("hasAuthority('class:delete')")
public Result<Void> delete(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
schoolClassService.delete(id, tenantId);
return Result.success(null);
}
}

View File

@ -0,0 +1,71 @@
package com.lesingle.creation.controller;
import com.lesingle.creation.common.core.Result;
import com.lesingle.creation.common.security.UserPrincipal;
import com.lesingle.creation.dto.school.CreateSchoolDTO;
import com.lesingle.creation.dto.school.UpdateSchoolDTO;
import com.lesingle.creation.service.SchoolService;
import com.lesingle.creation.vo.school.SchoolVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/**
* 学校管理控制器
*/
@Tag(name = "学校管理")
@RestController
@RequestMapping("/api/schools")
@RequiredArgsConstructor
public class SchoolController {
private final SchoolService schoolService;
@PostMapping
@Operation(summary = "创建学校")
@PreAuthorize("hasAuthority('school:create')")
public Result<SchoolVO> create(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Validated CreateSchoolDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long creatorId = userPrincipal.getUserId();
SchoolVO result = schoolService.create(dto, tenantId, creatorId);
return Result.success(result);
}
@GetMapping
@Operation(summary = "获取学校信息")
@PreAuthorize("hasAuthority('school:read')")
public Result<SchoolVO> get(
@AuthenticationPrincipal UserPrincipal userPrincipal) {
Long tenantId = userPrincipal.getTenantId();
SchoolVO result = schoolService.getByTenantId(tenantId);
return Result.success(result);
}
@PutMapping
@Operation(summary = "更新学校")
@PreAuthorize("hasAuthority('school:update')")
public Result<SchoolVO> update(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Validated UpdateSchoolDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long modifierId = userPrincipal.getUserId();
SchoolVO result = schoolService.update(dto, tenantId, modifierId);
return Result.success(result);
}
@DeleteMapping
@Operation(summary = "删除学校")
@PreAuthorize("hasAuthority('school:delete')")
public Result<Void> delete(
@AuthenticationPrincipal UserPrincipal userPrincipal) {
Long tenantId = userPrincipal.getTenantId();
schoolService.delete(tenantId);
return Result.success();
}
}

View File

@ -0,0 +1,100 @@
package com.lesingle.creation.controller;
import com.lesingle.creation.common.core.Result;
import com.lesingle.creation.common.security.UserPrincipal;
import com.lesingle.creation.dto.student.CreateStudentDTO;
import com.lesingle.creation.dto.student.UpdateStudentDTO;
import com.lesingle.creation.service.StudentService;
import com.lesingle.creation.vo.student.StudentVO;
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.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 学生管理控制器
*/
@Tag(name = "学生管理", description = "学生 CRUD 和查询接口")
@RestController
@RequestMapping("/api/students")
@RequiredArgsConstructor
public class StudentController {
private final StudentService studentService;
@PostMapping
@Operation(summary = "创建学生")
@PreAuthorize("hasAuthority('student:create')")
public Result<StudentVO> create(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Valid CreateStudentDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long creatorId = userPrincipal.getUserId();
StudentVO result = studentService.create(dto, tenantId, creatorId);
return Result.success(result);
}
@GetMapping
@Operation(summary = "查询学生列表")
@PreAuthorize("hasAuthority('student:read')")
public Result<List<StudentVO>> list(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) Long classId) {
Long tenantId = userPrincipal.getTenantId();
List<StudentVO> result = studentService.list(tenantId, page, pageSize, classId);
return Result.success(result);
}
@GetMapping("/{id}")
@Operation(summary = "获取学生详情")
@PreAuthorize("hasAuthority('student:read')")
public Result<StudentVO> getDetail(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
StudentVO result = studentService.getDetail(id, tenantId);
return Result.success(result);
}
@GetMapping("/user/{userId}")
@Operation(summary = "根据用户 ID 获取学生信息")
@PreAuthorize("hasAuthority('student:read')")
public Result<StudentVO> getByUserId(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long userId) {
Long tenantId = userPrincipal.getTenantId();
StudentVO result = studentService.getByUserId(userId, tenantId);
return Result.success(result);
}
@PutMapping("/{id}")
@Operation(summary = "更新学生")
@PreAuthorize("hasAuthority('student:update')")
public Result<StudentVO> update(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id,
@RequestBody @Valid UpdateStudentDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long modifierId = userPrincipal.getUserId();
StudentVO result = studentService.update(id, dto, tenantId, modifierId);
return Result.success(result);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除学生")
@PreAuthorize("hasAuthority('student:delete')")
public Result<Void> delete(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
studentService.delete(id, tenantId);
return Result.success(null);
}
}

View File

@ -0,0 +1,96 @@
package com.lesingle.creation.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.lesingle.creation.common.core.Result;
import com.lesingle.creation.common.security.UserPrincipal;
import com.lesingle.creation.dto.log.LogQueryDTO;
import com.lesingle.creation.service.SysLogService;
import com.lesingle.creation.vo.log.LogStatisticsVO;
import com.lesingle.creation.vo.log.LogVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 系统日志控制器
*/
@Tag(name = "系统日志", description = "日志查询、统计等接口")
@RestController
@RequestMapping("/api/sys-log")
@RequiredArgsConstructor
public class SysLogController {
private final SysLogService sysLogService;
@Operation(summary = "分页查询日志列表")
@GetMapping("/page")
@PreAuthorize("hasAuthority('log:read')")
public Result<Page<LogVO>> pageQuery(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@ModelAttribute LogQueryDTO queryDTO) {
Long tenantId = userPrincipal.getTenantId();
Page<LogVO> result = sysLogService.pageQuery(queryDTO, tenantId);
return Result.success(result);
}
/**
* 获取日志详情
*/
@Operation(summary = "获取日志详情")
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('log:read')")
public Result<LogVO> getDetail(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
LogVO result = sysLogService.getDetail(id, tenantId);
return Result.success(result);
}
/**
* 获取日志统计信息
*/
@Operation(summary = "获取日志统计信息")
@GetMapping("/statistics")
@PreAuthorize("hasAuthority('log:read')")
public Result<LogStatisticsVO> getStatistics(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestParam(required = false, defaultValue = "7") Integer days) {
Long tenantId = userPrincipal.getTenantId();
LogStatisticsVO result = sysLogService.getStatistics(days, tenantId);
return Result.success(result);
}
/**
* 批量删除日志
*/
@Operation(summary = "批量删除日志")
@DeleteMapping
@PreAuthorize("hasAuthority('log:delete')")
public Result<Integer> deleteByIds(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestParam("ids") List<Long> ids) {
Long tenantId = userPrincipal.getTenantId();
int result = sysLogService.deleteByIds(ids, tenantId);
return Result.success(result);
}
/**
* 清理过期日志
*/
@Operation(summary = "清理过期日志")
@PostMapping("/clean")
@PreAuthorize("hasAuthority('log:delete')")
public Result<Integer> cleanOldLogs(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestParam(required = false) Integer days) {
Long tenantId = userPrincipal.getTenantId();
int result = sysLogService.cleanOldLogs(days, tenantId);
return Result.success(result);
}
}

View File

@ -0,0 +1,100 @@
package com.lesingle.creation.controller;
import com.lesingle.creation.common.core.Result;
import com.lesingle.creation.common.security.UserPrincipal;
import com.lesingle.creation.dto.teacher.CreateTeacherDTO;
import com.lesingle.creation.dto.teacher.UpdateTeacherDTO;
import com.lesingle.creation.service.TeacherService;
import com.lesingle.creation.vo.teacher.TeacherVO;
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.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 教师管理控制器
*/
@Tag(name = "教师管理", description = "教师 CRUD 和查询接口")
@RestController
@RequestMapping("/api/teachers")
@RequiredArgsConstructor
public class TeacherController {
private final TeacherService teacherService;
@PostMapping
@Operation(summary = "创建教师")
@PreAuthorize("hasAuthority('teacher:create')")
public Result<TeacherVO> create(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Valid CreateTeacherDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long creatorId = userPrincipal.getUserId();
TeacherVO result = teacherService.create(dto, tenantId, creatorId);
return Result.success(result);
}
@GetMapping
@Operation(summary = "查询教师列表")
@PreAuthorize("hasAuthority('teacher:read')")
public Result<List<TeacherVO>> list(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestParam(required = false) Long departmentId,
@RequestParam(required = false) String nickname,
@RequestParam(required = false) String username) {
Long tenantId = userPrincipal.getTenantId();
List<TeacherVO> result = teacherService.list(tenantId, departmentId, nickname, username);
return Result.success(result);
}
@GetMapping("/{id}")
@Operation(summary = "获取教师详情")
@PreAuthorize("hasAuthority('teacher:read')")
public Result<TeacherVO> getDetail(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
TeacherVO result = teacherService.getDetail(id, tenantId);
return Result.success(result);
}
@GetMapping("/user/{userId}")
@Operation(summary = "根据用户 ID 获取教师信息")
@PreAuthorize("hasAuthority('teacher:read')")
public Result<TeacherVO> getByUserId(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long userId) {
Long tenantId = userPrincipal.getTenantId();
TeacherVO result = teacherService.getByUserId(userId, tenantId);
return Result.success(result);
}
@PutMapping("/{id}")
@Operation(summary = "更新教师")
@PreAuthorize("hasAuthority('teacher:update')")
public Result<TeacherVO> update(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id,
@RequestBody @Valid UpdateTeacherDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long modifierId = userPrincipal.getUserId();
TeacherVO result = teacherService.update(id, dto, tenantId, modifierId);
return Result.success(result);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除教师")
@PreAuthorize("hasAuthority('teacher:delete')")
public Result<Void> delete(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
teacherService.delete(id, tenantId);
return Result.success(null);
}
}

View File

@ -0,0 +1,88 @@
package com.lesingle.creation.controller;
import com.lesingle.creation.common.core.Result;
import com.lesingle.creation.common.security.UserPrincipal;
import com.lesingle.creation.dto.tenant.CreateTenantDTO;
import com.lesingle.creation.dto.tenant.UpdateTenantDTO;
import com.lesingle.creation.service.TenantService;
import com.lesingle.creation.vo.tenant.TenantDetailVO;
import com.lesingle.creation.vo.tenant.TenantListVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 租户管理控制器
*/
@Tag(name = "租户管理")
@RestController
@RequestMapping("/api/tenants")
@RequiredArgsConstructor
public class TenantController {
private final TenantService tenantService;
@PostMapping
@Operation(summary = "创建租户")
@PreAuthorize("hasAuthority('tenant:create')")
public Result<TenantDetailVO> create(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Validated CreateTenantDTO dto) {
Long creatorId = userPrincipal.getUserId();
TenantDetailVO result = tenantService.create(dto, creatorId);
return Result.success(result);
}
@GetMapping
@Operation(summary = "租户列表")
@PreAuthorize("hasAuthority('tenant:read')")
public Result<com.baomidou.mybatisplus.extension.plugins.pagination.Page<TenantListVO>> pageList(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int pageSize) {
com.baomidou.mybatisplus.extension.plugins.pagination.Page<TenantListVO> result =
tenantService.pageList(page, pageSize);
return Result.success(result);
}
@GetMapping("/{id}")
@Operation(summary = "租户详情")
@PreAuthorize("hasAuthority('tenant:read')")
public Result<TenantDetailVO> detail(@PathVariable Long id) {
TenantDetailVO result = tenantService.detail(id);
return Result.success(result);
}
@PutMapping("/{id}")
@Operation(summary = "更新租户")
@PreAuthorize("hasAuthority('tenant:update')")
public Result<TenantDetailVO> update(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id,
@RequestBody @Validated UpdateTenantDTO dto) {
Long modifierId = userPrincipal.getUserId();
TenantDetailVO result = tenantService.update(id, dto, modifierId);
return Result.success(result);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除租户")
@PreAuthorize("hasAuthority('tenant:delete')")
public Result<Void> delete(@PathVariable Long id) {
tenantService.delete(id);
return Result.success();
}
@GetMapping("/{id}/menus")
@Operation(summary = "获取租户菜单")
@PreAuthorize("hasAuthority('tenant:read')")
public Result<List<?>> getTenantMenus(@PathVariable Long id) {
List<?> result = tenantService.getTenantMenus(id);
return Result.success(result);
}
}

View File

@ -0,0 +1,78 @@
package com.lesingle.creation.controller;
import com.lesingle.creation.common.core.Result;
import com.lesingle.creation.common.exception.BusinessException;
import com.lesingle.creation.common.security.UserPrincipal;
import com.lesingle.creation.common.util.OssUtil;
import com.lesingle.creation.vo.upload.UploadResponseVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* 文件上传控制器
*/
@Tag(name = "文件上传", description = "文件上传、删除等接口")
@Slf4j
@RestController
@RequestMapping("/api/upload")
@RequiredArgsConstructor
public class UploadController {
private final OssUtil ossUtil;
/**
* 上传文件
*/
@Operation(summary = "上传文件")
@PostMapping
public Result<UploadResponseVO> uploadFile(
@RequestParam("file") MultipartFile file,
@AuthenticationPrincipal UserPrincipal userPrincipal) {
try {
if (file == null || file.isEmpty()) {
throw new BusinessException("请选择要上传的文件");
}
// 调用 OssUtil 上传文件
String fileUrl = ossUtil.upload(file);
// 构建响应
UploadResponseVO response = new UploadResponseVO();
response.setUrl(fileUrl);
response.setFileName(file.getOriginalFilename());
response.setSize(file.getSize());
log.info("文件上传成功,文件名={}, 大小={}", file.getOriginalFilename(), file.getSize());
return Result.success(response);
} catch (IOException e) {
log.error("文件上传失败", e);
throw new BusinessException("文件上传失败:" + e.getMessage());
}
}
/**
* 删除文件
*/
@Operation(summary = "删除文件")
@DeleteMapping
public Result<Void> deleteFile(@RequestParam("url") String fileUrl) {
try {
ossUtil.delete(fileUrl);
return Result.success(null);
} catch (Exception e) {
log.error("文件删除失败", e);
throw new BusinessException("文件删除失败:" + e.getMessage());
}
}
}

View File

@ -0,0 +1,124 @@
package com.lesingle.creation.controller;
import com.lesingle.creation.common.core.Result;
import com.lesingle.creation.common.security.UserPrincipal;
import com.lesingle.creation.dto.user.CreateUserDTO;
import com.lesingle.creation.dto.user.UpdateUserDTO;
import com.lesingle.creation.dto.user.UserQueryDTO;
import com.lesingle.creation.service.UserService;
import com.lesingle.creation.vo.user.UserDetailVO;
import com.lesingle.creation.vo.user.UserListVO;
import com.lesingle.creation.vo.user.UserStatsVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/**
* 用户管理控制器
*/
@Tag(name = "用户管理")
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@PostMapping
@Operation(summary = "创建用户")
@PreAuthorize("hasAuthority('user:create')")
public Result<UserDetailVO> create(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Validated CreateUserDTO dto) {
Long tenantId = userPrincipal.getTenantId();
Long operatorId = userPrincipal.getUserId();
UserDetailVO result = userService.create(dto, tenantId, operatorId);
return Result.success(result);
}
@GetMapping("/stats")
@Operation(summary = "用户统计(仅超管)")
@PreAuthorize("hasAuthority('super_admin')")
public Result<UserStatsVO> getStats() {
UserStatsVO result = userService.getStats();
return Result.success(result);
}
@GetMapping
@Operation(summary = "用户列表")
@PreAuthorize("hasAuthority('user:read')")
public Result<com.baomidou.mybatisplus.extension.plugins.pagination.Page<UserListVO>> pageList(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestParam(required = false) String keyword,
@RequestParam(required = false) String userType,
@RequestParam(required = false) String userSource,
@RequestParam(required = false) String status,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int pageSize) {
Long tenantId = userPrincipal.getTenantId();
boolean isSuperTenant = userPrincipal.isSuperTenant();
UserQueryDTO queryDTO = new UserQueryDTO();
queryDTO.setKeyword(keyword);
queryDTO.setUserType(userType);
queryDTO.setUserSource(userSource);
queryDTO.setStatus(status);
queryDTO.setPage(page);
queryDTO.setPageSize(pageSize);
com.baomidou.mybatisplus.extension.plugins.pagination.Page<UserListVO> result =
userService.pageList(queryDTO, tenantId, isSuperTenant);
return Result.success(result);
}
@GetMapping("/{id}")
@Operation(summary = "用户详情")
@PreAuthorize("hasAuthority('user:read')")
public Result<UserDetailVO> detail(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
boolean isSuperTenant = userPrincipal.isSuperTenant();
UserDetailVO result = userService.detail(id, tenantId, isSuperTenant);
return Result.success(result);
}
@PatchMapping("/{id}/status")
@Operation(summary = "更新用户状态")
@PreAuthorize("hasAuthority('user:manage')")
public Result<UserDetailVO> updateStatus(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id,
@RequestParam String status) {
Long operatorId = userPrincipal.getUserId();
UserDetailVO result = userService.updateStatus(id, status, operatorId);
return Result.success(result);
}
@PutMapping("/{id}")
@Operation(summary = "更新用户")
@PreAuthorize("hasAuthority('user:update')")
public Result<UserDetailVO> update(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id,
@RequestBody @Validated UpdateUserDTO dto) {
Long tenantId = userPrincipal.getTenantId();
UserDetailVO result = userService.update(id, dto, tenantId);
return Result.success(result);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除用户")
@PreAuthorize("hasAuthority('user:delete')")
public Result<Void> delete(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long id) {
Long tenantId = userPrincipal.getTenantId();
userService.delete(id, tenantId);
return Result.success();
}
}

View File

@ -1,27 +0,0 @@
package com.lesingle.creation.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 登录请求 DTO
*
* @author lesingle
* @since 1.0.0
*/
@Data
public class LoginRequest {
/**
* 用户名
*/
@NotBlank(message = "用户名不能为空")
private String username;
/**
* 密码
*/
@NotBlank(message = "密码不能为空")
private String password;
}

View File

@ -1,47 +0,0 @@
package com.lesingle.creation.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
/**
* 注册请求 DTO
*
* @author lesingle
* @since 1.0.0
*/
@Data
public class RegisterRequest {
/**
* 用户名
*/
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 50, message = "用户名长度必须在 3-50 个字符之间")
private String username;
/**
* 密码
*/
@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 100, message = "密码长度必须在 6-100 个字符之间")
private String password;
/**
* 邮箱
*/
@Email(message = "邮箱格式不正确")
private String email;
/**
* 手机号
*/
private String phone;
/**
* 昵称
*/
private String nickname;
}

View File

@ -0,0 +1,24 @@
package com.lesingle.creation.dto.ai3d;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* AI 3D 任务查询 DTO
*/
@Data
@Schema(description = "AI 3D 任务查询参数")
public class AI3DTaskQueryDTO {
@Schema(description = "页码", example = "1")
private Integer pageNum = 1;
@Schema(description = "每页数量", example = "10")
private Integer pageSize = 10;
@Schema(description = "任务状态pending/processing/completed/failed/timeout")
private String status;
@Schema(description = "输入类型text/image")
private String inputType;
}

View File

@ -0,0 +1,26 @@
package com.lesingle.creation.dto.ai3d;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
/**
* 创建 AI 3D 任务请求 DTO
*/
@Data
@Schema(description = "创建 AI 3D 任务请求")
public class CreateAI3DTaskDTO {
@NotBlank(message = "输入内容不能为空")
@Schema(description = "输入类型text/image", example = "text")
private String inputType;
@NotBlank(message = "输入内容不能为空")
@Schema(description = "输入内容:文字描述或图片 URL", example = "一只可爱的小猫")
private String inputContent;
@Schema(description = "生成类型Normal/Geometry/LowPoly/Sketch", example = "Normal")
private String generateType = "Normal";
}

View File

@ -0,0 +1,28 @@
package com.lesingle.creation.dto.auth;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
/**
* 登录请求 DTO
*/
@Data
@Schema(description = "登录请求")
public class LoginDTO {
@NotBlank(message = "用户名不能为空")
@Schema(description = "用户名")
private String username;
@NotBlank(message = "密码不能为空")
@Schema(description = "密码")
private String password;
@Schema(description = "租户编码")
private String tenantCode;
@Schema(description = "验证码")
private String captcha;
}

View File

@ -0,0 +1,42 @@
package com.lesingle.creation.dto.child;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.time.LocalDate;
/**
* 创建子女 DTO
*/
@Data
@Schema(description = "创建子女请求")
public class CreateChildDTO {
@NotBlank(message = "子女用户名不能为空")
@Schema(description = "子女用户名")
private String username;
@NotBlank(message = "密码不能为空")
@Schema(description = "密码")
private String password;
@NotBlank(message = "昵称不能为空")
@Schema(description = "昵称")
private String nickname;
@Schema(description = "性别male/female")
private String gender;
@Schema(description = "出生日期")
private LocalDate birthday;
@Schema(description = "所在城市")
private String city;
@Schema(description = "头像 URL")
private String avatar;
@Schema(description = "与监护人关系")
private String relationship;
}

View File

@ -0,0 +1,35 @@
package com.lesingle.creation.dto.child;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDate;
/**
* 更新子女 DTO
*/
@Data
@Schema(description = "更新子女请求")
public class UpdateChildDTO {
@Schema(description = "昵称")
private String nickname;
@Schema(description = "密码")
private String password;
@Schema(description = "性别male/female")
private String gender;
@Schema(description = "出生日期")
private LocalDate birthday;
@Schema(description = "所在城市")
private String city;
@Schema(description = "头像 URL")
private String avatar;
@Schema(description = "管控模式full/control/free")
private String controlMode;
}

View File

@ -0,0 +1,28 @@
package com.lesingle.creation.dto.config;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
/**
* 创建配置 DTO
*/
@Data
@Schema(description = "创建配置请求")
public class CreateConfigDTO {
@NotBlank(message = "配置键不能为空")
@Schema(description = "配置键", example = "system.title")
private String configKey;
@NotBlank(message = "配置值不能为空")
@Schema(description = "配置值", example = "图书馆创作平台")
private String configValue;
@Schema(description = "配置名称", example = "系统标题")
private String configName;
@Schema(description = "配置描述")
private String description;
}

View File

@ -0,0 +1,21 @@
package com.lesingle.creation.dto.config;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 更新配置 DTO
*/
@Data
@Schema(description = "更新配置请求")
public class UpdateConfigDTO {
@Schema(description = "配置值")
private String configValue;
@Schema(description = "配置名称")
private String configName;
@Schema(description = "配置描述")
private String description;
}

View File

@ -0,0 +1,39 @@
package com.lesingle.creation.dto.contest;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 竞赛查询 DTO
*/
@Data
@Schema(description = "竞赛查询参数")
public class ContestQueryDTO {
@Schema(description = "页码", example = "1")
private Integer page = 1;
@Schema(description = "每页大小", example = "10")
private Integer pageSize = 10;
@Schema(description = "竞赛名称关键字")
private String contestName;
@Schema(description = "竞赛状态unpublished/published", example = "published")
private String contestState;
@Schema(description = "竞赛类型individual/team", example = "individual")
private String contestType;
@Schema(description = "可见范围public/designated/internal", example = "designated")
private String visibility;
@Schema(description = "进度状态ongoing/finished", example = "ongoing")
private String status;
@Schema(description = "阶段unpublished/registering/submitting/reviewing/finished", example = "registering")
private String stage;
@Schema(description = "创建者租户 ID")
private Long creatorTenantId;
}

View File

@ -0,0 +1,135 @@
package com.lesingle.creation.dto.contest;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDateTime;
import java.util.List;
/**
* 创建竞赛 DTO
*/
@Data
@Schema(description = "创建竞赛请求")
public class CreateContestDTO {
@NotBlank(message = "竞赛名称不能为空")
@Schema(description = "竞赛名称", example = "第十届创意绘画比赛")
private String contestName;
@NotBlank(message = "竞赛类型不能为空")
@Schema(description = "竞赛类型individual-个人赛/team-团队赛", example = "individual")
private String contestType;
@Schema(description = "可见范围public-公开/designated-指定机构/internal-仅内部", example = "designated")
private String visibility;
@Schema(description = "目标城市列表(定向推送时使用)")
private List<String> targetCities;
@Schema(description = "最小年龄限制", example = "6")
private Integer ageMin;
@Schema(description = "最大年龄限制", example = "18")
private Integer ageMax;
@NotNull(message = "竞赛开始时间不能为空")
@Schema(description = "竞赛开始时间")
private LocalDateTime startTime;
@NotNull(message = "竞赛结束时间不能为空")
@Schema(description = "竞赛结束时间")
private LocalDateTime endTime;
@Schema(description = "线下地址")
private String address;
@Schema(description = "竞赛详情")
private String content;
@Schema(description = "授权租户 ID 列表")
private List<Integer> contestTenants;
@Schema(description = "封面 URL")
private String coverUrl;
@Schema(description = "海报 URL")
private String posterUrl;
@Schema(description = "联系人")
private String contactName;
@Schema(description = "联系电话")
private String contactPhone;
@Schema(description = "联系人二维码")
private String contactQrcode;
@Schema(description = "主办单位")
private String organizers;
@Schema(description = "协办单位")
private String coOrganizers;
@Schema(description = "赞助单位")
private String sponsors;
@NotNull(message = "报名开始时间不能为空")
@Schema(description = "报名开始时间")
private LocalDateTime registerStartTime;
@NotNull(message = "报名结束时间不能为空")
@Schema(description = "报名结束时间")
private LocalDateTime registerEndTime;
@Schema(description = "报名任务状态open-开放/closed-关闭", example = "open")
private String registerState;
@Schema(description = "是否需要审核", example = "true")
private Boolean requireAudit;
@Schema(description = "允许报名的年级 ID 列表")
private List<Integer> allowedGrades;
@Schema(description = "允许报名的班级 ID 列表")
private List<Integer> allowedClasses;
@Schema(description = "团队最少成员数", example = "1")
private Integer teamMinMembers;
@Schema(description = "团队最多成员数", example = "5")
private Integer teamMaxMembers;
@Schema(description = "提交规则once-仅一次/resubmit-可重复提交", example = "once")
private String submitRule;
@NotNull(message = "作品提交开始时间不能为空")
@Schema(description = "作品提交开始时间")
private LocalDateTime submitStartTime;
@NotNull(message = "作品提交结束时间不能为空")
@Schema(description = "作品提交结束时间")
private LocalDateTime submitEndTime;
@Schema(description = "作品类型image/video/document/code/other", example = "image")
private String workType;
@Schema(description = "作品要求说明")
private String workRequirement;
@Schema(description = "评审规则 ID")
private Long reviewRuleId;
@NotNull(message = "评审开始时间不能为空")
@Schema(description = "评审开始时间")
private LocalDateTime reviewStartTime;
@NotNull(message = "评审结束时间不能为空")
@Schema(description = "评审结束时间")
private LocalDateTime reviewEndTime;
@Schema(description = "结果发布时间")
private LocalDateTime resultPublishTime;
}

View File

@ -0,0 +1,15 @@
package com.lesingle.creation.dto.contest;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 发布/取消发布竞赛 DTO
*/
@Data
@Schema(description = "发布竞赛请求")
public class PublishContestDTO {
@Schema(description = "竞赛状态unpublished/published", example = "published")
private String contestState;
}

View File

@ -0,0 +1,126 @@
package com.lesingle.creation.dto.contest;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
/**
* 更新竞赛 DTO
*/
@Data
@Schema(description = "更新竞赛请求")
public class UpdateContestDTO {
@Schema(description = "竞赛名称")
private String contestName;
@Schema(description = "竞赛类型individual/team")
private String contestType;
@Schema(description = "可见范围public/designated/internal")
private String visibility;
@Schema(description = "目标城市列表")
private List<String> targetCities;
@Schema(description = "最小年龄限制")
private Integer ageMin;
@Schema(description = "最大年龄限制")
private Integer ageMax;
@Schema(description = "竞赛开始时间")
private LocalDateTime startTime;
@Schema(description = "竞赛结束时间")
private LocalDateTime endTime;
@Schema(description = "线下地址")
private String address;
@Schema(description = "竞赛详情")
private String content;
@Schema(description = "授权租户 ID 列表")
private List<Integer> contestTenants;
@Schema(description = "封面 URL")
private String coverUrl;
@Schema(description = "海报 URL")
private String posterUrl;
@Schema(description = "联系人")
private String contactName;
@Schema(description = "联系电话")
private String contactPhone;
@Schema(description = "联系人二维码")
private String contactQrcode;
@Schema(description = "主办单位")
private String organizers;
@Schema(description = "协办单位")
private String coOrganizers;
@Schema(description = "赞助单位")
private String sponsors;
@Schema(description = "报名开始时间")
private LocalDateTime registerStartTime;
@Schema(description = "报名结束时间")
private LocalDateTime registerEndTime;
@Schema(description = "报名任务状态open/closed")
private String registerState;
@Schema(description = "是否需要审核")
private Boolean requireAudit;
@Schema(description = "允许报名的年级 ID 列表")
private List<Integer> allowedGrades;
@Schema(description = "允许报名的班级 ID 列表")
private List<Integer> allowedClasses;
@Schema(description = "团队最少成员数")
private Integer teamMinMembers;
@Schema(description = "团队最多成员数")
private Integer teamMaxMembers;
@Schema(description = "提交规则once/resubmit")
private String submitRule;
@Schema(description = "作品提交开始时间")
private LocalDateTime submitStartTime;
@Schema(description = "作品提交结束时间")
private LocalDateTime submitEndTime;
@Schema(description = "作品类型")
private String workType;
@Schema(description = "作品要求说明")
private String workRequirement;
@Schema(description = "评审规则 ID")
private Long reviewRuleId;
@Schema(description = "评审开始时间")
private LocalDateTime reviewStartTime;
@Schema(description = "评审结束时间")
private LocalDateTime reviewEndTime;
@Schema(description = "结果发布时间")
private LocalDateTime resultPublishTime;
@Schema(description = "竞赛状态unpublished/published")
private String contestState;
}

View File

@ -0,0 +1,30 @@
package com.lesingle.creation.dto.department;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 创建部门 DTO
*/
@Data
@Schema(description = "创建部门请求")
public class CreateDepartmentDTO {
@NotBlank(message = "部门名称不能为空")
@Schema(description = "部门名称")
private String name;
@NotBlank(message = "部门编码不能为空")
@Schema(description = "部门编码")
private String code;
@Schema(description = "父部门 ID")
private Long parentId;
@Schema(description = "部门描述")
private String description;
@Schema(description = "排序")
private Integer sort = 0;
}

View File

@ -0,0 +1,30 @@
package com.lesingle.creation.dto.department;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 更新部门 DTO
*/
@Data
@Schema(description = "更新部门请求")
public class UpdateDepartmentDTO {
@Schema(description = "部门名称")
private String name;
@Schema(description = "部门编码")
private String code;
@Schema(description = "父部门 ID")
private Long parentId;
@Schema(description = "部门描述")
private String description;
@Schema(description = "排序")
private Integer sort;
@Schema(description = "有效状态1-有效2-失效")
private Integer validState;
}

View File

@ -0,0 +1,29 @@
package com.lesingle.creation.dto.dict;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
import java.util.List;
/**
* 创建字典 DTO
*/
@Data
@Schema(description = "创建字典请求")
public class CreateDictDTO {
@NotBlank(message = "字典名称不能为空")
@Schema(description = "字典名称", example = "性别")
private String name;
@NotBlank(message = "字典编码不能为空")
@Schema(description = "字典编码", example = "gender")
private String code;
@Schema(description = "字典描述")
private String description;
@Schema(description = "字典项列表")
private List<CreateDictItemDTO> items;
}

View File

@ -0,0 +1,26 @@
package com.lesingle.creation.dto.dict;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
/**
* 创建字典项 DTO
*/
@Data
@Schema(description = "创建字典项请求")
public class CreateDictItemDTO {
@NotBlank(message = "字典项标签不能为空")
@Schema(description = "字典项标签", example = "")
private String label;
@NotBlank(message = "字典项值不能为空")
@Schema(description = "字典项值", example = "male")
private String value;
@Schema(description = "排序", example = "1")
private Integer sort;
}

View File

@ -0,0 +1,26 @@
package com.lesingle.creation.dto.dict;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
/**
* 更新字典 DTO
*/
@Data
@Schema(description = "更新字典请求")
public class UpdateDictDTO {
@Schema(description = "字典名称")
private String name;
@Schema(description = "字典编码")
private String code;
@Schema(description = "字典描述")
private String description;
@Schema(description = "字典项列表")
private List<UpdateDictItemDTO> items;
}

View File

@ -0,0 +1,26 @@
package com.lesingle.creation.dto.dict;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
/**
* 更新字典项 DTO
*/
@Data
@Schema(description = "更新字典项请求")
public class UpdateDictItemDTO {
@Schema(description = "字典项 ID")
private Long id;
@Schema(description = "字典项标签")
private String label;
@Schema(description = "字典项值")
private String value;
@Schema(description = "排序")
private Integer sort;
}

View File

@ -0,0 +1,29 @@
package com.lesingle.creation.dto.grade;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 创建年级 DTO
*/
@Data
@Schema(description = "创建年级请求")
public class CreateGradeDTO {
@NotBlank(message = "年级名称不能为空")
@Schema(description = "年级名称")
private String name;
@NotBlank(message = "年级编码不能为空")
@Schema(description = "年级编码")
private String code;
@NotNull(message = "年级级别不能为空")
@Schema(description = "年级级别")
private Integer level;
@Schema(description = "年级描述")
private String description;
}

Some files were not shown because too many files have changed in this diff Show More