Compare commits
82 Commits
9215465bd5
...
8638876fa9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8638876fa9 | ||
|
|
cd8de97f79 | ||
|
|
951346a7a8 | ||
| 87ac3b5ed9 | |||
| f03991819d | |||
|
|
f7f97c64e4 | ||
|
|
7a8d909df3 | ||
|
|
d5657d8d23 | ||
|
|
c4f4613c49 | ||
|
|
b19acbd6d5 | ||
|
|
937f0650f0 | ||
| c1113c937c | |||
| 5bb159358f | |||
| 6365dd8dd0 | |||
|
|
88ca6264a1 | ||
|
|
cc5a5fb4e3 | ||
|
|
e8da2ee3f8 | ||
|
|
593f7977eb | ||
|
|
328533e805 | ||
|
|
3fa1ef95ac | ||
| b9ed5e17c6 | |||
|
|
9f036eb81f | ||
|
|
36cd01c585 | ||
|
|
430dce1f09 | ||
| 1eb76979c4 | |||
| fa42eca339 | |||
|
|
7d7ef9820b | ||
|
|
df7eae6125 | ||
|
|
197064820b | ||
|
|
180c22fe49 | ||
| bc7c17b281 | |||
| 9b5c24c49c | |||
| a660493cf3 | |||
| 922f650365 | |||
| 9ad9f5b237 | |||
|
|
2f84ac16d3 | ||
|
|
7a039e8403 | ||
|
|
1d43501983 | ||
|
|
f2c10d5e32 | ||
|
|
170d904081 | ||
|
|
3fd7002e2a | ||
|
|
633e46e663 | ||
|
|
2b83c9c78a | ||
|
|
eb409398f3 | ||
|
|
015f8718c4 | ||
|
|
2f521c7249 | ||
|
|
1c63cb21e5 | ||
|
|
0252f25acd | ||
| 3c24cc3102 | |||
|
|
a5909f98be | ||
|
|
15581e04ae | ||
|
|
1003776dd3 | ||
|
|
3ef05de193 | ||
|
|
d68322f24a | ||
|
|
63c564a03b | ||
|
|
ff25e41243 | ||
|
|
bee5152a2d | ||
|
|
b3954ffcf3 | ||
|
|
4a70bc7d43 | ||
|
|
764f6eec4b | ||
|
|
7afb57c9bf | ||
|
|
78cd956ab5 | ||
|
|
f223e9bd41 | ||
|
|
c5fad30849 | ||
|
|
d19d7d9a2c | ||
|
|
dcaa7e1779 | ||
|
|
3c4100c231 | ||
|
|
8154628d3d | ||
|
|
ea65b55332 | ||
|
|
c99738fc46 | ||
|
|
ab5bd36cec | ||
|
|
0b989b047a | ||
|
|
e9676ea924 | ||
|
|
fd9c739cf5 | ||
|
|
5b5af63f58 | ||
|
|
483740f10b | ||
|
|
249e73d252 | ||
|
|
5c0d87d4a6 | ||
|
|
58c232dadc | ||
|
|
a6b056520d | ||
|
|
096d06af3d | ||
|
|
bead1cf4dc |
180
CLAUDE.md
Normal file
180
CLAUDE.md
Normal file
@ -0,0 +1,180 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## 项目概述
|
||||
|
||||
多租户少儿绘本创作活动/竞赛管理平台,前后端分离架构。
|
||||
|
||||
## 目录结构
|
||||
|
||||
| 目录 | 说明 |
|
||||
|------|------|
|
||||
| `backend-java/` | Spring Boot 后端(实际开发目录) |
|
||||
| `frontend/` | Vue 3 前端(实际开发目录) |
|
||||
| `lesingle-aicreate-client/` | AI 绘本创作客户端(独立模块) |
|
||||
|
||||
## 常用命令
|
||||
|
||||
### 后端 (backend-java/)
|
||||
|
||||
```bash
|
||||
cd backend-java
|
||||
mvn spring-boot:run -Dspring.profiles.active=dev # 开发启动(端口 8580,上下文 /api)
|
||||
mvn flyway:migrate # 执行数据库迁移
|
||||
mvn clean package # 构建打包
|
||||
```
|
||||
|
||||
### 前端 (frontend/)
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm run dev # 开发模式(端口 3000,代理 /api → localhost:8580)
|
||||
npm run build # 生产构建(base: /web/)
|
||||
npm run build:test # 测试环境构建(base: /web-test/)
|
||||
npm run lint # ESLint 检查
|
||||
```
|
||||
|
||||
### AI创作客户端 (lesingle-aicreate-client/)
|
||||
|
||||
```bash
|
||||
cd lesingle-aicreate-client
|
||||
npm install && npm run dev # 独立启动
|
||||
```
|
||||
|
||||
## 技术栈
|
||||
|
||||
| 层级 | 技术 |
|
||||
|------|------|
|
||||
| 后端框架 | Spring Boot 3.2.5 + Java 17 |
|
||||
| ORM | MyBatis-Plus 3.5.7 |
|
||||
| 数据库 | MySQL 8.0 + Flyway 迁移 |
|
||||
| 认证 | Spring Security + JWT |
|
||||
| 缓存 | Redis |
|
||||
| 工具库 | Hutool 5.8 + FastJSON2 + Knife4j 4.4(API文档) |
|
||||
| 前端框架 | Vue 3 + TypeScript + Vite 5 |
|
||||
| UI | Ant Design Vue 4.1 |
|
||||
| 状态管理 | Pinia |
|
||||
| 样式 | Tailwind CSS + SCSS |
|
||||
| 表单验证 | VeeValidate + Zod |
|
||||
| 富文本 | WangEditor |
|
||||
| 图表 | ECharts |
|
||||
|
||||
## 后端架构 (backend-java/)
|
||||
|
||||
### 基础包: `com.competition`
|
||||
|
||||
### 三层架构
|
||||
|
||||
| 层级 | 职责 | 规范 |
|
||||
|------|------|------|
|
||||
| Controller | HTTP 请求处理、参数校验、Entity↔VO 转换 | 统一返回 `Result<T>` |
|
||||
| Service | 业务逻辑、事务控制 | 接口 `I{Module}Service`,继承 `IService<T>` |
|
||||
| Mapper | 数据库 CRUD | 继承 `BaseMapper<T>` |
|
||||
|
||||
**核心原则**: Service/Mapper 层使用 Entity,VO 转换只在 Controller 层。
|
||||
|
||||
### 模块划分
|
||||
|
||||
```
|
||||
com.competition.modules/
|
||||
├── biz/
|
||||
│ ├── contest/ # 赛事管理(/contests)
|
||||
│ ├── homework/ # 作业管理(/homework)
|
||||
│ ├── judge/ # 评委管理
|
||||
│ └── review/ # 评审管理(/contest-reviews, /contest-results)
|
||||
├── sys/ # 系统管理(用户/角色/权限/租户,/sys/*)
|
||||
├── user/ # 用户模块
|
||||
├── ugc/ # 用户生成内容
|
||||
├── pub/ # 公开接口(/public/*,无需认证)
|
||||
└── oss/ # 对象存储(/oss/upload)
|
||||
```
|
||||
|
||||
### 实体基类 (BaseEntity)
|
||||
|
||||
所有实体继承 `BaseEntity`,包含字段:`id`、`createBy`、`updateBy`、`createTime`、`modifyTime`、`deleted`、`validState`。
|
||||
|
||||
表名规范:业务表 `t_biz_*`、系统表 `t_sys_*`、用户表 `t_user_*`。
|
||||
|
||||
### 多租户
|
||||
|
||||
- 所有业务表包含 `tenant_id` 字段
|
||||
- 获取租户ID: `SecurityUtil.getCurrentTenantId()`
|
||||
- 超级管理员 `isSuperAdmin()` 可访问所有租户数据
|
||||
- 请求头通过 `X-Tenant-Code`、`X-Tenant-Id` 传递
|
||||
|
||||
### 认证与权限
|
||||
|
||||
- JWT Token payload: `{sub: userId, username, tenantId}`
|
||||
- 公开接口: `@Public` 注解或路径 `/public/**`
|
||||
- 权限控制: `@RequirePermission` 注解
|
||||
|
||||
### 统一响应格式
|
||||
|
||||
```java
|
||||
Result<T> → {code, message, data, timestamp, path}
|
||||
PageResult<T> → {list, total, page, pageSize}
|
||||
```
|
||||
|
||||
### 数据库迁移
|
||||
|
||||
- 位置: `src/main/resources/db/migration/`
|
||||
- 命名: `V{number}__description.sql`(注意双下划线)
|
||||
- 不使用外键约束,关联关系通过代码控制
|
||||
|
||||
## 前端架构 (frontend/)
|
||||
|
||||
### 路由与多租户
|
||||
|
||||
- 路由路径包含租户编码: `/:tenantCode/login`、`/:tenantCode/dashboard`
|
||||
- 动态路由: 根据用户权限菜单动态生成
|
||||
- 双模式: 管理端(需认证)+ 公众端(无需认证)
|
||||
|
||||
### 三种布局
|
||||
|
||||
| 布局 | 用途 |
|
||||
|------|------|
|
||||
| BasicLayout | 管理端(侧边栏+顶栏+面包屑) |
|
||||
| PublicLayout | 公众端(简洁导航) |
|
||||
| EmptyLayout | 全屏页面 |
|
||||
|
||||
### API 调用模式
|
||||
|
||||
API 模块位于 `src/api/`,Axios 实例在 `src/utils/request.ts`:
|
||||
- 请求拦截器自动添加 Authorization token 和租户头
|
||||
- 响应拦截器统一错误处理(401 跳转登录,403 提示)
|
||||
- 函数命名: `getXxx`、`createXxx`、`updateXxx`、`deleteXxx`
|
||||
|
||||
### 权限控制
|
||||
|
||||
- 路由级: `meta.permissions`
|
||||
- 组件级: `v-permission` 自定义指令
|
||||
- 方法级: `hasPermission()`、`hasAnyPermission()`、`isSuperAdmin()`
|
||||
|
||||
### 状态管理
|
||||
|
||||
- auth Store: 用户信息、token、菜单、权限检查
|
||||
- Token 存储在 Cookie 中
|
||||
|
||||
## 开发规范
|
||||
|
||||
- **日志/注释使用中文**
|
||||
- **Git 提交格式**: `类型: 描述`(如 `feat: 添加XX功能`、`fix: 修复XX问题`)
|
||||
- **组件语法**: `<script setup lang="ts">`
|
||||
- **文件命名**: 组件 PascalCase,其他 kebab-case
|
||||
- **数据库**: 不使用外键,关联通过代码控制
|
||||
- **逻辑删除**: `deleted` 字段
|
||||
|
||||
## 环境配置
|
||||
|
||||
| 环境 | 后端 Profile | 前端 base | 后端端口 |
|
||||
|------|-------------|-----------|---------|
|
||||
| 开发 | dev | `/` | 8580 |
|
||||
| 测试 | test | `/web-test/` | 8580 |
|
||||
| 生产 | prod | `/web/` | 8580 |
|
||||
|
||||
## 注意事项
|
||||
|
||||
- `.cursor/rules/` 中部分规范(如 NestJS、Prisma)是旧版配置,当前后端已迁移至 Spring Boot + MyBatis-Plus,以本文件为准
|
||||
- 当前项目未配置测试框架
|
||||
- 当前项目未配置 i18n,所有文本为中文硬编码
|
||||
5
backend-java/.gitignore
vendored
Normal file
5
backend-java/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
target/
|
||||
uploads/
|
||||
*.log
|
||||
.idea/
|
||||
*.iml
|
||||
198
backend-java/pom.xml
Normal file
198
backend-java/pom.xml
Normal file
@ -0,0 +1,198 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.2.5</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>com.competition</groupId>
|
||||
<artifactId>competition-management-system</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<name>competition-management-system</name>
|
||||
<description>少儿绘本创作活动管理平台 - Java 后端</description>
|
||||
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
<mybatis-plus.version>3.5.7</mybatis-plus.version>
|
||||
<druid.version>1.2.23</druid.version>
|
||||
<jjwt.version>0.12.6</jjwt.version>
|
||||
<knife4j.version>4.4.0</knife4j.version>
|
||||
<mapstruct.version>1.5.5.Final</mapstruct.version>
|
||||
<hutool.version>5.8.32</hutool.version>
|
||||
<fastjson2.version>2.0.53</fastjson2.version>
|
||||
<aliyun-oss.version>3.17.1</aliyun-oss.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Boot Starters -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MyBatis-Plus -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||
<version>${mybatis-plus.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Druid 连接池 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>druid-spring-boot-3-starter</artifactId>
|
||||
<version>${druid.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- MySQL 驱动 -->
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Flyway 数据库迁移 -->
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-mysql</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JWT -->
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
<version>${jjwt.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<version>${jjwt.version}</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<version>${jjwt.version}</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Knife4j API 文档 -->
|
||||
<dependency>
|
||||
<groupId>com.github.xiaoymin</groupId>
|
||||
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
|
||||
<version>${knife4j.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- MapStruct -->
|
||||
<dependency>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct</artifactId>
|
||||
<version>${mapstruct.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Hutool 工具集 -->
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>${hutool.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- FastJSON2 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.fastjson2</groupId>
|
||||
<artifactId>fastjson2</artifactId>
|
||||
<version>${fastjson2.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 阿里云 OSS -->
|
||||
<dependency>
|
||||
<groupId>com.aliyun.oss</groupId>
|
||||
<artifactId>aliyun-sdk-oss</artifactId>
|
||||
<version>${aliyun-oss.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Lombok -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Test -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<source>${java.version}</source>
|
||||
<target>${java.version}</target>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>${lombok.version}</version>
|
||||
</path>
|
||||
<path>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct-processor</artifactId>
|
||||
<version>${mapstruct.version}</version>
|
||||
</path>
|
||||
<!-- Lombok-MapStruct 绑定,确保 MapStruct 能识别 Lombok 生成的方法 -->
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok-mapstruct-binding</artifactId>
|
||||
<version>0.2.0</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@ -0,0 +1,23 @@
|
||||
package com.competition;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
@MapperScan({
|
||||
"com.competition.modules.sys.mapper",
|
||||
"com.competition.modules.biz.contest.mapper",
|
||||
"com.competition.modules.biz.review.mapper",
|
||||
"com.competition.modules.biz.homework.mapper",
|
||||
"com.competition.modules.biz.judge.mapper",
|
||||
"com.competition.modules.user.mapper",
|
||||
"com.competition.modules.ugc.mapper",
|
||||
"com.competition.modules.leai.mapper"
|
||||
})
|
||||
public class CompetitionApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(CompetitionApplication.class, args);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
package com.competition.common.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 接口速率限制注解
|
||||
* 用于公开接口防止恶意调用
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface RateLimit {
|
||||
|
||||
/**
|
||||
* 时间窗口内允许的最大请求次数
|
||||
*/
|
||||
int permits() default 10;
|
||||
|
||||
/**
|
||||
* 时间窗口大小
|
||||
*/
|
||||
long duration() default 1;
|
||||
|
||||
/**
|
||||
* 时间单位
|
||||
*/
|
||||
TimeUnit timeUnit() default TimeUnit.SECONDS;
|
||||
|
||||
/**
|
||||
* 限制维度:ip / user
|
||||
*/
|
||||
String key() default "ip";
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
package com.competition.common.config;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 数据库启动时轻量修复:
|
||||
* 由于本项目部分环境可能没有跑过最新的 init.sql,
|
||||
* 导致 `t_biz_contest_work` 缺少 `deleted` 字段,从而 MyBatis-Plus 逻辑删除查询报 500。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class ContestWorkSchemaRepair implements ApplicationRunner {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) {
|
||||
try {
|
||||
Integer columnCnt = jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(*) " +
|
||||
"FROM information_schema.columns " +
|
||||
"WHERE table_schema = DATABASE() " +
|
||||
"AND table_name = 't_biz_contest_work' " +
|
||||
"AND column_name = 'deleted'",
|
||||
Integer.class
|
||||
);
|
||||
|
||||
if (columnCnt == null || columnCnt == 0) {
|
||||
log.warn("检测到表 `t_biz_contest_work` 缺少字段 `deleted`,尝试补齐...");
|
||||
jdbcTemplate.execute(
|
||||
"ALTER TABLE t_biz_contest_work " +
|
||||
"ADD COLUMN deleted tinyint NOT NULL DEFAULT '0' " +
|
||||
"COMMENT '逻辑删除:0-未删除,1-已删除'"
|
||||
);
|
||||
log.info("补齐字段 `t_biz_contest_work.deleted` 完成");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 不阻断启动,给调用方避免 500 需要依赖数据库权限/执行成功
|
||||
log.warn("补齐 `t_biz_contest_work.deleted` 失败:{}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,36 @@
|
||||
package com.competition.common.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
import org.springframework.web.filter.CorsFilter;
|
||||
|
||||
/**
|
||||
* 跨域配置
|
||||
* 允许所有来源访问,方便前后端分离开发部署
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class CorsConfig {
|
||||
|
||||
@Bean
|
||||
public CorsFilter corsFilter() {
|
||||
CorsConfiguration config = new CorsConfiguration();
|
||||
config.setAllowCredentials(true);
|
||||
|
||||
// 允许所有来源
|
||||
config.addAllowedOriginPattern("*");
|
||||
|
||||
config.addAllowedHeader("*");
|
||||
config.addAllowedMethod("*");
|
||||
config.addExposedHeader("X-Trace-Id");
|
||||
|
||||
log.info("CORS 配置:允许所有来源(*)");
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", config);
|
||||
return new CorsFilter(source);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
package com.competition.common.config;
|
||||
|
||||
import org.flywaydb.core.Flyway;
|
||||
import org.springframework.boot.autoconfigure.flyway.FlywayMigrationStrategy;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Flyway 修复配置
|
||||
* 启动时自动修复失败的迁移记录,然后执行迁移
|
||||
*/
|
||||
@Configuration
|
||||
public class FlywayRepairConfig {
|
||||
|
||||
@Bean
|
||||
public FlywayMigrationStrategy flywayMigrationStrategy() {
|
||||
return flyway -> {
|
||||
// 先修复失败的迁移记录
|
||||
flyway.repair();
|
||||
// 然后执行迁移
|
||||
flyway.migrate();
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
package com.competition.common.config;
|
||||
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Knife4j / OpenAPI 配置
|
||||
*/
|
||||
@Configuration
|
||||
public class Knife4jConfig {
|
||||
|
||||
@Bean
|
||||
public OpenAPI openAPI() {
|
||||
return new OpenAPI()
|
||||
.info(new Info()
|
||||
.title("少儿绘本创作活动管理平台 API")
|
||||
.description("Competition Management System - Java Backend")
|
||||
.version("1.0.0"))
|
||||
.addSecurityItem(new SecurityRequirement().addList("Bearer"))
|
||||
.schemaRequirement("Bearer", new SecurityScheme()
|
||||
.type(SecurityScheme.Type.HTTP)
|
||||
.scheme("bearer")
|
||||
.bearerFormat("JWT"));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
package com.competition.common.config;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* MyBatis-Plus 配置
|
||||
*/
|
||||
@Configuration
|
||||
public class MyBatisPlusConfig {
|
||||
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
// 分页插件
|
||||
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
|
||||
return interceptor;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
package com.competition.common.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* RestTemplate 配置
|
||||
*/
|
||||
@Configuration
|
||||
public class RestTemplateConfig {
|
||||
|
||||
@Bean
|
||||
public RestTemplate restTemplate() {
|
||||
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
|
||||
factory.setConnectTimeout(10_000);
|
||||
factory.setReadTimeout(10_000);
|
||||
return new RestTemplate(factory);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
package com.competition.common.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
/**
|
||||
* 定时任务配置
|
||||
*/
|
||||
@Configuration
|
||||
@EnableScheduling
|
||||
public class SchedulingConfig {
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package com.competition.common.config;
|
||||
|
||||
import com.competition.common.interceptor.RateLimitInterceptor;
|
||||
import com.competition.common.interceptor.TraceIdInterceptor;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* WebMvc 配置
|
||||
*/
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
private final TraceIdInterceptor traceIdInterceptor;
|
||||
private final RateLimitInterceptor rateLimitInterceptor;
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(traceIdInterceptor).addPathPatterns("/**");
|
||||
// 速率限制拦截器,仅对公开接口生效
|
||||
registry.addInterceptor(rateLimitInterceptor)
|
||||
.addPathPatterns("/public/**", "/webhook/**");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
package com.competition.common.constants;
|
||||
|
||||
/**
|
||||
* BaseEntity 基础字段常量
|
||||
*/
|
||||
public final class BaseEntityConstants {
|
||||
|
||||
private BaseEntityConstants() {}
|
||||
|
||||
/** 未删除 */
|
||||
public static final int NOT_DELETED = 0;
|
||||
|
||||
/** 已删除 */
|
||||
public static final int DELETED = 1;
|
||||
|
||||
/** 有效状态 */
|
||||
public static final int VALID = 1;
|
||||
|
||||
/** 无效状态 */
|
||||
public static final int INVALID = 2;
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
package com.competition.common.constants;
|
||||
|
||||
/**
|
||||
* 缓存相关常量
|
||||
*/
|
||||
public final class CacheConstants {
|
||||
|
||||
private CacheConstants() {}
|
||||
|
||||
/** 用户角色缓存 key 前缀 */
|
||||
public static final String USER_ROLES_PREFIX = "user:roles:";
|
||||
|
||||
/** 用户权限缓存 key 前缀 */
|
||||
public static final String USER_PERMS_PREFIX = "user:perms:";
|
||||
|
||||
/** 认证缓存天数 */
|
||||
public static final int AUTH_CACHE_DAYS = 7;
|
||||
|
||||
/** Token 黑名单 key 前缀(用于登出/密码修改后使旧 Token 失效) */
|
||||
public static final String TOKEN_BLACKLIST_PREFIX = "token:blacklist:";
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
package com.competition.common.constants;
|
||||
|
||||
/**
|
||||
* 角色相关常量
|
||||
*/
|
||||
public final class RoleConstants {
|
||||
|
||||
private RoleConstants() {}
|
||||
|
||||
/** 超级管理员角色编码 */
|
||||
public static final String SUPER_ADMIN = "super_admin";
|
||||
|
||||
/** 公众用户角色编码 */
|
||||
public static final String PUBLIC_USER = "public_user";
|
||||
|
||||
/** 评委角色编码 */
|
||||
public static final String JUDGE = "judge";
|
||||
|
||||
/** 租户管理员角色编码 */
|
||||
public static final String TENANT_ADMIN = "tenant_admin";
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
package com.competition.common.constants;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 租户相关常量
|
||||
*/
|
||||
public final class TenantConstants {
|
||||
|
||||
private TenantConstants() {}
|
||||
|
||||
/** 超级管理员租户编码 */
|
||||
public static final String CODE_SUPER = "super";
|
||||
|
||||
/** 公众端租户编码 */
|
||||
public static final String CODE_PUBLIC = "public";
|
||||
|
||||
/** 评委租户编码 */
|
||||
public static final String CODE_JUDGE = "judge";
|
||||
|
||||
/** 内部系统租户编码集合(不可删除、不可注册) */
|
||||
public static final Set<String> INTERNAL_TENANT_CODES = Set.of(CODE_SUPER, CODE_PUBLIC, CODE_JUDGE, "school", "teacher", "student");
|
||||
}
|
||||
@ -0,0 +1,69 @@
|
||||
package com.competition.common.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 基础实体类,所有实体继承此类
|
||||
* 包含新审计字段(Java规范)和旧审计字段(过渡期兼容)
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "基础实体")
|
||||
public abstract class BaseEntity implements Serializable {
|
||||
|
||||
/** 主键 ID(自增) */
|
||||
@Schema(description = "主键ID")
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
// ====== 新审计字段(Java 规范) ======
|
||||
|
||||
/** 创建人账号 */
|
||||
@Schema(description = "创建人账号")
|
||||
@TableField(value = "create_by", fill = FieldFill.INSERT)
|
||||
private String createBy;
|
||||
|
||||
/** 更新人账号 */
|
||||
@Schema(description = "更新人账号")
|
||||
@TableField(value = "update_by", fill = FieldFill.INSERT_UPDATE)
|
||||
private String updateBy;
|
||||
|
||||
/** 逻辑删除标识(0-未删除,1-已删除) */
|
||||
@Schema(description = "逻辑删除标识:0-未删除,1-已删除")
|
||||
@TableLogic
|
||||
@TableField(value = "deleted", fill = FieldFill.INSERT)
|
||||
private Integer deleted;
|
||||
|
||||
// ====== 旧审计字段(过渡期保留,请使用 createBy/updateBy) ======
|
||||
|
||||
/** 创建人 ID(已弃用,请使用 createBy) */
|
||||
@Deprecated
|
||||
@Schema(description = "创建人ID(已弃用,请使用 createBy)")
|
||||
@TableField(value = "creator", fill = FieldFill.INSERT)
|
||||
private Integer creator;
|
||||
|
||||
/** 修改人 ID(已弃用,请使用 updateBy) */
|
||||
@Deprecated
|
||||
@Schema(description = "修改人ID(已弃用,请使用 updateBy)")
|
||||
@TableField(value = "modifier", fill = FieldFill.INSERT_UPDATE)
|
||||
private Integer modifier;
|
||||
|
||||
/** 创建时间 */
|
||||
@Schema(description = "创建时间")
|
||||
@TableField(value = "create_time", fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/** 修改时间 */
|
||||
@Schema(description = "修改时间")
|
||||
@TableField(value = "modify_time", fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime modifyTime;
|
||||
|
||||
/** 有效状态:1-有效,2-失效 */
|
||||
@Schema(description = "有效状态:1-有效,2-失效")
|
||||
@TableField(value = "valid_state", fill = FieldFill.INSERT)
|
||||
private Integer validState;
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
package com.competition.common.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 通用状态枚举(启用/禁用)
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum CommonStatus {
|
||||
|
||||
ENABLED("enabled", "启用"),
|
||||
DISABLED("disabled", "禁用");
|
||||
|
||||
private final String value;
|
||||
private final String description;
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
package com.competition.common.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 错误码枚举
|
||||
* HTTP 状态码级别 + 业务错误码分组
|
||||
* 10xx 用户模块 / 20xx 活动模块 / 30xx 评审模块 / 40xx 作品模块 / 50xx 系统模块
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum ErrorCode {
|
||||
|
||||
// ====== HTTP 状态码级别 ======
|
||||
SUCCESS(200, "success"),
|
||||
BAD_REQUEST(400, "请求参数错误"),
|
||||
UNAUTHORIZED(401, "未登录或 Token 已过期"),
|
||||
FORBIDDEN(403, "没有访问权限"),
|
||||
NOT_FOUND(404, "资源不存在"),
|
||||
CONFLICT(409, "数据冲突"),
|
||||
INTERNAL_ERROR(500, "系统内部错误"),
|
||||
|
||||
// ====== 用户模块 10xx ======
|
||||
USER_NOT_FOUND(1001, "用户不存在"),
|
||||
USER_DISABLED(1002, "用户已被禁用"),
|
||||
USER_PASSWORD_ERROR(1003, "密码错误"),
|
||||
USER_DUPLICATE(1004, "用户名已存在"),
|
||||
USER_PHONE_DUPLICATE(1005, "手机号已注册"),
|
||||
|
||||
// ====== 活动模块 20xx ======
|
||||
CONTEST_NOT_FOUND(2001, "活动不存在"),
|
||||
CONTEST_ALREADY_PUBLISHED(2002, "活动已发布"),
|
||||
CONTEST_NOT_PUBLISHED(2003, "活动未发布"),
|
||||
CONTEST_TIME_INVALID(2004, "活动时间配置无效"),
|
||||
CONTEST_REGISTRATION_CLOSED(2005, "报名已截止"),
|
||||
CONTEST_SUBMIT_CLOSED(2006, "提交已截止"),
|
||||
CONTEST_ALREADY_FINISHED(2007, "活动已结束"),
|
||||
CONTEST_REVIEW_INCOMPLETE(2008, "评审未完成"),
|
||||
|
||||
// ====== 评审模块 30xx ======
|
||||
REVIEW_NOT_FOUND(3001, "评审记录不存在"),
|
||||
REVIEW_ALREADY_SCORED(3002, "已评分,请勿重复提交"),
|
||||
REVIEW_FINAL_SCORE_LOCKED(3003, "终分已锁定,无法修改评分"),
|
||||
REVIEW_NOT_ASSIGNED(3004, "作品未分配给该评委"),
|
||||
|
||||
// ====== 作品模块 40xx ======
|
||||
WORK_NOT_FOUND(4001, "作品不存在"),
|
||||
WORK_ALREADY_SUBMITTED(4002, "作品已提交"),
|
||||
WORK_RESUBMIT_NOT_ALLOWED(4003, "不允许重新提交"),
|
||||
|
||||
// ====== 文件上传模块 50xx ======
|
||||
FILE_TYPE_NOT_ALLOWED(5001, "不支持的文件类型"),
|
||||
FILE_SIZE_EXCEEDED(5002, "文件大小超限");
|
||||
|
||||
private final Integer code;
|
||||
private final String message;
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
package com.competition.common.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 参赛者类型枚举
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum ParticipantType {
|
||||
|
||||
SELF("self", "本人"),
|
||||
CHILD("child", "儿童");
|
||||
|
||||
private final String value;
|
||||
private final String description;
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
package com.competition.common.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 发布状态枚举
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum PublishStatus {
|
||||
|
||||
PUBLISHED("published", "已发布"),
|
||||
UNPUBLISHED("unpublished", "未发布");
|
||||
|
||||
private final String value;
|
||||
private final String description;
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package com.competition.common.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 报名状态枚举
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum RegistrationStatus {
|
||||
|
||||
PENDING("pending", "待审核"),
|
||||
PASSED("passed", "已通过"),
|
||||
REJECTED("rejected", "已拒绝");
|
||||
|
||||
private final String value;
|
||||
private final String description;
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
package com.competition.common.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 提交规则枚举
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum SubmitRule {
|
||||
|
||||
ONCE("once", "仅一次"),
|
||||
RESUBMIT("resubmit", "可重提交");
|
||||
|
||||
private final String value;
|
||||
private final String description;
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
package com.competition.common.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 租户类型枚举
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum TenantType {
|
||||
|
||||
PLATFORM("platform", "平台"),
|
||||
LIBRARY("library", "图书馆"),
|
||||
KINDERGARTEN("kindergarten", "幼儿园"),
|
||||
SCHOOL("school", "学校"),
|
||||
INSTITUTION("institution", "机构"),
|
||||
OTHER("other", "其他");
|
||||
|
||||
private final String value;
|
||||
private final String description;
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package com.competition.common.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 用户来源枚举
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum UserSource {
|
||||
|
||||
ADMIN_CREATED("admin_created", "管理员创建"),
|
||||
SELF_REGISTERED("self_registered", "自主注册"),
|
||||
CHILD_MIGRATED("child_migrated", "儿童迁移");
|
||||
|
||||
private final String value;
|
||||
private final String description;
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
package com.competition.common.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 用户类型枚举
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum UserType {
|
||||
|
||||
ADULT("adult", "成人"),
|
||||
CHILD("child", "儿童");
|
||||
|
||||
private final String value;
|
||||
private final String description;
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
package com.competition.common.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 可见性枚举
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum Visibility {
|
||||
|
||||
PUBLIC("public", "公开"),
|
||||
DESIGNATED("designated", "指定"),
|
||||
INTERNAL("internal", "内部"),
|
||||
PRIVATE("private", "私有");
|
||||
|
||||
private final String value;
|
||||
private final String description;
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
package com.competition.common.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 作品状态枚举
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum WorkStatus {
|
||||
|
||||
SUBMITTED("submitted", "已提交"),
|
||||
LOCKED("locked", "已锁定"),
|
||||
REVIEWING("reviewing", "评审中"),
|
||||
REJECTED("rejected", "已拒绝"),
|
||||
ACCEPTED("accepted", "已采纳"),
|
||||
AWARDED("awarded", "已获奖"),
|
||||
TAKEN_DOWN("taken_down", "已下架");
|
||||
|
||||
private final String value;
|
||||
private final String description;
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
package com.competition.common.exception;
|
||||
|
||||
import com.competition.common.enums.ErrorCode;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 业务异常
|
||||
*/
|
||||
@Getter
|
||||
public class BusinessException extends RuntimeException {
|
||||
|
||||
private final Integer code;
|
||||
|
||||
public BusinessException(Integer code, String message) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public BusinessException(ErrorCode errorCode) {
|
||||
super(errorCode.getMessage());
|
||||
this.code = errorCode.getCode();
|
||||
}
|
||||
|
||||
public BusinessException(ErrorCode errorCode, String message) {
|
||||
super(message);
|
||||
this.code = errorCode.getCode();
|
||||
}
|
||||
|
||||
public static BusinessException of(ErrorCode errorCode) {
|
||||
return new BusinessException(errorCode);
|
||||
}
|
||||
|
||||
public static BusinessException of(ErrorCode errorCode, String message) {
|
||||
return new BusinessException(errorCode, message);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,83 @@
|
||||
package com.competition.common.exception;
|
||||
|
||||
import com.competition.common.enums.ErrorCode;
|
||||
import com.competition.common.result.Result;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.servlet.resource.NoResourceFoundException;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 全局异常处理器
|
||||
*/
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
/** 业务异常 */
|
||||
@ExceptionHandler(BusinessException.class)
|
||||
public Result<Void> handleBusinessException(BusinessException e, HttpServletRequest request) {
|
||||
log.warn("业务异常,路径:{},消息:{}", request.getRequestURI(), e.getMessage());
|
||||
return Result.error(e.getCode(), e.getMessage(), request.getRequestURI());
|
||||
}
|
||||
|
||||
/** 参数校验异常(@Valid) */
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public Result<Void> handleValidationException(MethodArgumentNotValidException e, HttpServletRequest request) {
|
||||
String message = e.getBindingResult().getFieldErrors().stream()
|
||||
.map(error -> error.getField() + ": " + error.getDefaultMessage())
|
||||
.collect(Collectors.joining(", "));
|
||||
log.warn("参数校验失败,路径:{},消息:{}", request.getRequestURI(), message);
|
||||
return Result.error(400, message, request.getRequestURI());
|
||||
}
|
||||
|
||||
/** 参数绑定异常 */
|
||||
@ExceptionHandler(BindException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public Result<Void> handleBindException(BindException e, HttpServletRequest request) {
|
||||
String message = e.getFieldErrors().stream()
|
||||
.map(error -> error.getField() + ": " + error.getDefaultMessage())
|
||||
.collect(Collectors.joining(", "));
|
||||
log.warn("参数绑定失败,路径:{},消息:{}", request.getRequestURI(), message);
|
||||
return Result.error(400, message, request.getRequestURI());
|
||||
}
|
||||
|
||||
/** 认证异常 */
|
||||
@ExceptionHandler(AuthenticationException.class)
|
||||
@ResponseStatus(HttpStatus.UNAUTHORIZED)
|
||||
public Result<Void> handleAuthenticationException(AuthenticationException e, HttpServletRequest request) {
|
||||
return Result.error(401, ErrorCode.UNAUTHORIZED.getMessage(), request.getRequestURI());
|
||||
}
|
||||
|
||||
/** 授权异常 */
|
||||
@ExceptionHandler(AccessDeniedException.class)
|
||||
@ResponseStatus(HttpStatus.FORBIDDEN)
|
||||
public Result<Void> handleAccessDeniedException(AccessDeniedException e, HttpServletRequest request) {
|
||||
return Result.error(403, ErrorCode.FORBIDDEN.getMessage(), request.getRequestURI());
|
||||
}
|
||||
|
||||
/** 资源不存在 */
|
||||
@ExceptionHandler(NoResourceFoundException.class)
|
||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||
public Result<Void> handleNotFoundException(NoResourceFoundException e, HttpServletRequest request) {
|
||||
return Result.error(404, ErrorCode.NOT_FOUND.getMessage(), request.getRequestURI());
|
||||
}
|
||||
|
||||
/** 兜底:未知异常 */
|
||||
@ExceptionHandler(Exception.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public Result<Void> handleException(Exception e, HttpServletRequest request) {
|
||||
log.error("系统异常,路径:{},消息:{}", request.getRequestURI(), e.getMessage(), e);
|
||||
return Result.error(500, ErrorCode.INTERNAL_ERROR.getMessage(), request.getRequestURI());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,57 @@
|
||||
package com.competition.common.handler;
|
||||
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import com.competition.common.util.SecurityUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.ibatis.reflection.MetaObject;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* MyBatis-Plus 审计字段自动填充
|
||||
* 同时填充新字段(create_by/update_by/deleted)和旧字段(creator/modifier/valid_state)
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class AuditMetaObjectHandler implements MetaObjectHandler {
|
||||
|
||||
@Override
|
||||
public void insertFill(MetaObject metaObject) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
String username = SecurityUtil.getCurrentUsername();
|
||||
Long userId = SecurityUtil.getCurrentUserIdOrNull();
|
||||
|
||||
// 新审计字段
|
||||
this.strictInsertFill(metaObject, "createBy", String.class, username);
|
||||
this.strictInsertFill(metaObject, "updateBy", String.class, username);
|
||||
this.strictInsertFill(metaObject, "deleted", Integer.class, 0);
|
||||
|
||||
// 旧审计字段
|
||||
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, now);
|
||||
this.strictInsertFill(metaObject, "modifyTime", LocalDateTime.class, now);
|
||||
this.strictInsertFill(metaObject, "validState", Integer.class, 1);
|
||||
|
||||
if (userId != null) {
|
||||
this.strictInsertFill(metaObject, "creator", Integer.class, userId.intValue());
|
||||
this.strictInsertFill(metaObject, "modifier", Integer.class, userId.intValue());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFill(MetaObject metaObject) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
String username = SecurityUtil.getCurrentUsername();
|
||||
Long userId = SecurityUtil.getCurrentUserIdOrNull();
|
||||
|
||||
// 新审计字段
|
||||
this.strictUpdateFill(metaObject, "updateBy", String.class, username);
|
||||
|
||||
// 旧审计字段
|
||||
this.strictUpdateFill(metaObject, "modifyTime", LocalDateTime.class, now);
|
||||
|
||||
if (userId != null) {
|
||||
this.strictUpdateFill(metaObject, "modifier", Integer.class, userId.intValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,101 @@
|
||||
package com.competition.common.interceptor;
|
||||
|
||||
import com.competition.common.annotation.RateLimit;
|
||||
import com.competition.common.result.Result;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 速率限制拦截器
|
||||
* 基于 Redis 实现滑动窗口限流
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class RateLimitInterceptor implements HandlerInterceptor {
|
||||
|
||||
private final StringRedisTemplate redisTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
private static final String RATE_LIMIT_PREFIX = "rate_limit:";
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||
if (!(handler instanceof HandlerMethod handlerMethod)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
RateLimit rateLimit = handlerMethod.getMethodAnnotation(RateLimit.class);
|
||||
if (rateLimit == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String key = buildKey(request, rateLimit);
|
||||
String redisKey = RATE_LIMIT_PREFIX + key;
|
||||
|
||||
// 获取当前计数
|
||||
String countStr = redisTemplate.opsForValue().get(redisKey);
|
||||
long currentCount = countStr != null ? Long.parseLong(countStr) : 0;
|
||||
|
||||
if (currentCount >= rateLimit.permits()) {
|
||||
log.warn("接口速率限制触发:key={},已请求 {} 次,限制 {} 次", key, currentCount, rateLimit.permits());
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
response.setStatus(429);
|
||||
response.getWriter().write(objectMapper.writeValueAsString(
|
||||
Result.error(429, "请求过于频繁,请稍后再试")));
|
||||
return false;
|
||||
}
|
||||
|
||||
// 增加计数
|
||||
Long newCount = redisTemplate.opsForValue().increment(redisKey);
|
||||
if (newCount != null && newCount == 1) {
|
||||
// 首次请求,设置过期时间
|
||||
redisTemplate.expire(redisKey, rateLimit.duration(), rateLimit.timeUnit());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建限流 key
|
||||
*/
|
||||
private String buildKey(HttpServletRequest request, RateLimit rateLimit) {
|
||||
String identity;
|
||||
if ("user".equals(rateLimit.key())) {
|
||||
// 基于 User-ID(需要认证后才有)
|
||||
identity = request.getHeader("X-User-Id");
|
||||
if (identity == null) identity = request.getRemoteAddr();
|
||||
} else {
|
||||
// 基于 IP
|
||||
identity = getClientIp(request);
|
||||
}
|
||||
return request.getMethod() + ":" + request.getRequestURI() + ":" + identity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端真实 IP
|
||||
*/
|
||||
private String getClientIp(HttpServletRequest request) {
|
||||
String ip = request.getHeader("X-Forwarded-For");
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("X-Real-IP");
|
||||
}
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getRemoteAddr();
|
||||
}
|
||||
// 多级代理取第一个
|
||||
if (ip != null && ip.contains(",")) {
|
||||
ip = ip.split(",")[0].trim();
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
package com.competition.common.interceptor;
|
||||
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
/**
|
||||
* TraceId 链路追踪拦截器
|
||||
*/
|
||||
@Component
|
||||
public class TraceIdInterceptor implements HandlerInterceptor {
|
||||
|
||||
private static final String TRACE_ID = "traceId";
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
||||
String traceId = request.getHeader("X-Trace-Id");
|
||||
if (traceId == null || traceId.isBlank()) {
|
||||
traceId = IdUtil.fastSimpleUUID();
|
||||
}
|
||||
MDC.put(TRACE_ID, traceId);
|
||||
response.setHeader("X-Trace-Id", traceId);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
|
||||
MDC.remove(TRACE_ID);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
package com.competition.common.result;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 分页结果,格式与前端完全兼容:{ list, total, page, pageSize }
|
||||
*/
|
||||
@Data
|
||||
public class PageResult<T> implements Serializable {
|
||||
|
||||
/** 数据列表 */
|
||||
private List<T> list;
|
||||
|
||||
/** 总记录数 */
|
||||
private Long total;
|
||||
|
||||
/** 当前页码 */
|
||||
private Long page;
|
||||
|
||||
/** 每页大小 */
|
||||
private Long pageSize;
|
||||
|
||||
public PageResult() {
|
||||
}
|
||||
|
||||
public PageResult(List<T> list, Long total, Long page, Long pageSize) {
|
||||
this.list = list;
|
||||
this.total = total;
|
||||
this.page = page;
|
||||
this.pageSize = pageSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 MyBatis-Plus 的 IPage 转换
|
||||
*/
|
||||
public static <T> PageResult<T> from(IPage<T> page) {
|
||||
return new PageResult<>(
|
||||
page.getRecords(),
|
||||
page.getTotal(),
|
||||
page.getCurrent(),
|
||||
page.getSize()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 MyBatis-Plus 的 IPage 转换,支持 VO 列表替换
|
||||
*/
|
||||
public static <T> PageResult<T> from(IPage<?> page, List<T> voList) {
|
||||
return new PageResult<>(
|
||||
voList,
|
||||
page.getTotal(),
|
||||
page.getCurrent(),
|
||||
page.getSize()
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
package com.competition.common.result;
|
||||
|
||||
import com.competition.common.enums.ErrorCode;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 统一响应结果
|
||||
* 格式与 NestJS 前端完全兼容:{ code, message, data, timestamp, path }
|
||||
*/
|
||||
@Data
|
||||
public class Result<T> implements Serializable {
|
||||
|
||||
private Integer code;
|
||||
private String message;
|
||||
private T data;
|
||||
private String timestamp;
|
||||
private String path;
|
||||
|
||||
private Result() {
|
||||
}
|
||||
|
||||
public static <T> Result<T> success(T data) {
|
||||
Result<T> result = new Result<>();
|
||||
result.setCode(200);
|
||||
result.setMessage("success");
|
||||
result.setData(data);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static <T> Result<T> success() {
|
||||
return success(null);
|
||||
}
|
||||
|
||||
public static <T> Result<T> success(String message, T data) {
|
||||
Result<T> result = new Result<>();
|
||||
result.setCode(200);
|
||||
result.setMessage(message);
|
||||
result.setData(data);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static <T> Result<T> error(Integer code, String message) {
|
||||
Result<T> result = new Result<>();
|
||||
result.setCode(code);
|
||||
result.setMessage(message);
|
||||
result.setTimestamp(LocalDateTime.now().toString());
|
||||
return result;
|
||||
}
|
||||
|
||||
public static <T> Result<T> error(ErrorCode errorCode) {
|
||||
return error(errorCode.getCode(), errorCode.getMessage());
|
||||
}
|
||||
|
||||
public static <T> Result<T> error(Integer code, String message, String path) {
|
||||
Result<T> result = error(code, message);
|
||||
result.setPath(path);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,68 @@
|
||||
package com.competition.common.util;
|
||||
|
||||
import com.competition.security.model.LoginUser;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
||||
/**
|
||||
* 安全工具类 - 获取当前登录用户信息
|
||||
*/
|
||||
public final class SecurityUtil {
|
||||
|
||||
private SecurityUtil() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录用户
|
||||
*/
|
||||
public static LoginUser getCurrentUser() {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (authentication != null && authentication.getPrincipal() instanceof LoginUser loginUser) {
|
||||
return loginUser;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户 ID(未登录返回 null)
|
||||
*/
|
||||
public static Long getCurrentUserIdOrNull() {
|
||||
LoginUser user = getCurrentUser();
|
||||
return user != null ? user.getUserId() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户 ID(未登录抛异常)
|
||||
*/
|
||||
public static Long getCurrentUserId() {
|
||||
LoginUser user = getCurrentUser();
|
||||
if (user == null) {
|
||||
throw new RuntimeException("用户未登录");
|
||||
}
|
||||
return user.getUserId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户名(未登录返回 "system")
|
||||
*/
|
||||
public static String getCurrentUsername() {
|
||||
LoginUser user = getCurrentUser();
|
||||
return user != null ? user.getUsername() : "system";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前租户 ID(未登录返回 null)
|
||||
*/
|
||||
public static Long getCurrentTenantId() {
|
||||
LoginUser user = getCurrentUser();
|
||||
return user != null ? user.getTenantId() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前用户是否为超级管理员
|
||||
*/
|
||||
public static boolean isSuperAdmin() {
|
||||
LoginUser user = getCurrentUser();
|
||||
return user != null && user.isSuperAdmin();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
package com.competition.common.util;
|
||||
|
||||
/**
|
||||
* 敏感信息脱敏工具类
|
||||
* 用于日志输出时对手机号、身份证号、Token 等进行脱敏处理
|
||||
*/
|
||||
public final class SensitiveUtil {
|
||||
|
||||
private SensitiveUtil() {}
|
||||
|
||||
/**
|
||||
* 手机号脱敏:保留前3位和后4位
|
||||
* 例:13812345678 → 138****5678
|
||||
*/
|
||||
public static String phone(String phone) {
|
||||
if (phone == null || phone.length() < 7) {
|
||||
return "***";
|
||||
}
|
||||
return phone.substring(0, 3) + "****" + phone.substring(phone.length() - 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Token 脱敏:只显示前8位和后4位
|
||||
* 例:eyJhbGciOi...xyz → eyJhbGci...xyz
|
||||
*/
|
||||
public static String token(String token) {
|
||||
if (token == null || token.length() < 12) {
|
||||
return "***";
|
||||
}
|
||||
return token.substring(0, 8) + "..." + token.substring(token.length() - 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* 身份证号脱敏:保留前3位和后4位
|
||||
*/
|
||||
public static String idCard(String idCard) {
|
||||
if (idCard == null || idCard.length() < 7) {
|
||||
return "***";
|
||||
}
|
||||
return idCard.substring(0, 3) + "***********" + idCard.substring(idCard.length() - 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用脱敏:只显示前后各 n 位
|
||||
*/
|
||||
public static String mask(String value, int keepChars) {
|
||||
if (value == null) return "***";
|
||||
if (value.length() <= keepChars * 2) return "***";
|
||||
return value.substring(0, keepChars) + "***" + value.substring(value.length() - keepChars);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
package com.competition.modules.biz.contest.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.competition.common.result.Result;
|
||||
import com.competition.modules.biz.contest.entity.BizContestAttachment;
|
||||
import com.competition.modules.biz.contest.service.IContestAttachmentService;
|
||||
import com.competition.security.annotation.RequirePermission;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "活动附件")
|
||||
@RestController
|
||||
@RequestMapping("/contests/attachments")
|
||||
@RequiredArgsConstructor
|
||||
public class ContestAttachmentController {
|
||||
|
||||
private final IContestAttachmentService attachmentService;
|
||||
|
||||
@PostMapping
|
||||
@RequirePermission("contest:update")
|
||||
@Operation(summary = "上传附件")
|
||||
public Result<BizContestAttachment> create(@RequestBody BizContestAttachment attachment) {
|
||||
attachmentService.save(attachment);
|
||||
return Result.success(attachment);
|
||||
}
|
||||
|
||||
@GetMapping("/contest/{contestId}")
|
||||
@RequirePermission("contest:read")
|
||||
@Operation(summary = "查询活动下的附件列表")
|
||||
public Result<List<BizContestAttachment>> findByContest(@PathVariable Long contestId) {
|
||||
List<BizContestAttachment> list = attachmentService.list(
|
||||
new LambdaQueryWrapper<BizContestAttachment>()
|
||||
.eq(BizContestAttachment::getContestId, contestId)
|
||||
.orderByDesc(BizContestAttachment::getCreateTime));
|
||||
return Result.success(list);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@RequirePermission("contest:read")
|
||||
@Operation(summary = "查询附件详情")
|
||||
public Result<BizContestAttachment> findDetail(@PathVariable Long id) {
|
||||
return Result.success(attachmentService.getById(id));
|
||||
}
|
||||
|
||||
@PatchMapping("/{id}")
|
||||
@RequirePermission("contest:update")
|
||||
@Operation(summary = "更新附件")
|
||||
public Result<Void> update(@PathVariable Long id, @RequestBody BizContestAttachment attachment) {
|
||||
attachment.setId(id);
|
||||
attachmentService.updateById(attachment);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@RequirePermission("contest:update")
|
||||
@Operation(summary = "删除附件")
|
||||
public Result<Void> remove(@PathVariable Long id) {
|
||||
attachmentService.removeById(id);
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,115 @@
|
||||
package com.competition.modules.biz.contest.controller;
|
||||
|
||||
import com.competition.common.result.PageResult;
|
||||
import com.competition.common.result.Result;
|
||||
import com.competition.common.util.SecurityUtil;
|
||||
import com.competition.modules.biz.contest.dto.CreateContestDto;
|
||||
import com.competition.modules.biz.contest.dto.QueryContestDto;
|
||||
import com.competition.modules.biz.contest.entity.BizContest;
|
||||
import com.competition.modules.biz.contest.service.IContestService;
|
||||
import com.competition.modules.sys.service.ISysTenantService;
|
||||
import com.competition.security.annotation.RequirePermission;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "活动管理")
|
||||
@RestController
|
||||
@RequestMapping("/contests")
|
||||
@RequiredArgsConstructor
|
||||
public class ContestController {
|
||||
|
||||
private final IContestService contestService;
|
||||
private final ISysTenantService tenantService;
|
||||
|
||||
@PostMapping
|
||||
@RequirePermission("contest:create")
|
||||
@Operation(summary = "创建活动")
|
||||
public Result<BizContest> create(@Valid @RequestBody CreateContestDto dto) {
|
||||
return Result.success(contestService.createContest(dto, SecurityUtil.getCurrentUserId()));
|
||||
}
|
||||
|
||||
@GetMapping("/stats")
|
||||
@RequirePermission("contest:read")
|
||||
@Operation(summary = "获取活动统计")
|
||||
public Result<Map<String, Object>> getStats() {
|
||||
Long tenantId = SecurityUtil.getCurrentTenantId();
|
||||
boolean isSuperTenant = tenantService.isSuperTenant(tenantId);
|
||||
return Result.success(contestService.getStats(tenantId, isSuperTenant));
|
||||
}
|
||||
|
||||
@GetMapping("/dashboard")
|
||||
@RequirePermission("contest:read")
|
||||
@Operation(summary = "获取活动看板")
|
||||
public Result<Map<String, Object>> getDashboard() {
|
||||
return Result.success(contestService.getDashboard(SecurityUtil.getCurrentTenantId()));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@RequirePermission("contest:read")
|
||||
@Operation(summary = "查询活动列表")
|
||||
public Result<PageResult<Map<String, Object>>> findAll(QueryContestDto dto) {
|
||||
Long tenantId = SecurityUtil.getCurrentTenantId();
|
||||
boolean isSuperTenant = tenantService.isSuperTenant(tenantId);
|
||||
return Result.success(contestService.findAll(dto, tenantId, isSuperTenant));
|
||||
}
|
||||
|
||||
@GetMapping("/my-contests")
|
||||
@RequirePermission({"contest:read", "contest:activity:read"})
|
||||
@Operation(summary = "获取我的活动")
|
||||
public Result<PageResult<Map<String, Object>>> getMyContests(QueryContestDto dto) {
|
||||
Long userId = SecurityUtil.getCurrentUserId();
|
||||
Long tenantId = SecurityUtil.getCurrentTenantId();
|
||||
return Result.success(contestService.getMyContests(dto, userId, tenantId));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@RequirePermission("contest:read")
|
||||
@Operation(summary = "查询活动详情")
|
||||
public Result<Map<String, Object>> findDetail(@PathVariable Long id) {
|
||||
return Result.success(contestService.findDetail(id));
|
||||
}
|
||||
|
||||
@PatchMapping("/{id}")
|
||||
@RequirePermission("contest:update")
|
||||
@Operation(summary = "更新活动")
|
||||
public Result<BizContest> update(@PathVariable Long id, @RequestBody CreateContestDto dto) {
|
||||
return Result.success(contestService.updateContest(id, dto));
|
||||
}
|
||||
|
||||
@PatchMapping("/{id}/publish")
|
||||
@RequirePermission("contest:publish")
|
||||
@Operation(summary = "发布/撤回活动")
|
||||
public Result<Void> publish(@PathVariable Long id, @RequestBody Map<String, String> body) {
|
||||
contestService.publishContest(id, body.get("contestState"));
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@PatchMapping("/{id}/finish")
|
||||
@RequirePermission("contest:update")
|
||||
@Operation(summary = "结束活动")
|
||||
public Result<Void> finish(@PathVariable Long id) {
|
||||
contestService.finishContest(id);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@PatchMapping("/{id}/reopen")
|
||||
@RequirePermission("contest:update")
|
||||
@Operation(summary = "重新开放活动")
|
||||
public Result<Void> reopen(@PathVariable Long id) {
|
||||
contestService.reopenContest(id);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@RequirePermission("contest:delete")
|
||||
@Operation(summary = "删除活动")
|
||||
public Result<Void> remove(@PathVariable Long id) {
|
||||
contestService.removeContest(id);
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,217 @@
|
||||
package com.competition.modules.biz.contest.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.competition.common.enums.ErrorCode;
|
||||
import com.competition.common.enums.PublishStatus;
|
||||
import com.competition.common.exception.BusinessException;
|
||||
import com.competition.common.result.PageResult;
|
||||
import com.competition.common.result.Result;
|
||||
import com.competition.common.util.SecurityUtil;
|
||||
import com.competition.modules.biz.contest.dto.CreateNoticeDto;
|
||||
import com.competition.modules.biz.contest.entity.BizContestNotice;
|
||||
import com.competition.modules.biz.contest.service.IContestNoticeService;
|
||||
import com.competition.security.annotation.RequirePermission;
|
||||
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.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "活动公告")
|
||||
@RestController
|
||||
@RequestMapping("/contests/notices")
|
||||
@RequiredArgsConstructor
|
||||
public class ContestNoticeController {
|
||||
|
||||
private final IContestNoticeService noticeService;
|
||||
|
||||
/**
|
||||
* 解析日期时间字符串,兼容 ISO 格式(带毫秒和 Z 时区标记)
|
||||
*/
|
||||
private LocalDateTime parseDateTime(String dateTime) {
|
||||
if (!StringUtils.hasText(dateTime)) {
|
||||
return null;
|
||||
}
|
||||
// 尝试 ISO 格式(yyyy-MM-dd'T'HH:mm:ss.SSSZ 或 yyyy-MM-dd'T'HH:mm:ss'Z')
|
||||
try {
|
||||
// 处理带 Z 的 ISO 格式
|
||||
if (dateTime.endsWith("Z")) {
|
||||
return LocalDateTime.parse(dateTime.substring(0, dateTime.length() - 1));
|
||||
}
|
||||
// 处理带毫秒的格式
|
||||
if (dateTime.contains(".") && dateTime.indexOf(".") + 4 == dateTime.length()) {
|
||||
return LocalDateTime.parse(dateTime.substring(0, dateTime.indexOf(".")));
|
||||
}
|
||||
return LocalDateTime.parse(dateTime);
|
||||
} catch (Exception e) {
|
||||
// 尝试空格分隔格式
|
||||
try {
|
||||
return LocalDateTime.parse(dateTime, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||
} catch (Exception ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@RequirePermission("notice:create")
|
||||
@Operation(summary = "创建公告")
|
||||
public Result<BizContestNotice> create(@Valid @RequestBody CreateNoticeDto dto) {
|
||||
BizContestNotice notice = new BizContestNotice();
|
||||
notice.setContestId(dto.getContestId());
|
||||
notice.setTitle(dto.getTitle());
|
||||
notice.setContent(dto.getContent());
|
||||
notice.setNoticeType(dto.getNoticeType());
|
||||
notice.setPriority(dto.getPriority());
|
||||
if (StringUtils.hasText(dto.getPublishTime())) {
|
||||
notice.setPublishTime(parseDateTime(dto.getPublishTime()));
|
||||
}
|
||||
// 设置当前租户 ID(租户隔离)
|
||||
notice.setTenantId(SecurityUtil.getCurrentTenantId());
|
||||
noticeService.save(notice);
|
||||
return Result.success(notice);
|
||||
}
|
||||
|
||||
@GetMapping("/contest/{contestId}")
|
||||
@RequirePermission("notice:read")
|
||||
@Operation(summary = "查询活动下的公告列表")
|
||||
public Result<List<BizContestNotice>> findByContest(@PathVariable Long contestId) {
|
||||
Long tenantId = SecurityUtil.getCurrentTenantId();
|
||||
List<BizContestNotice> list = noticeService.list(
|
||||
new LambdaQueryWrapper<BizContestNotice>()
|
||||
.eq(BizContestNotice::getContestId, contestId)
|
||||
.eq(BizContestNotice::getTenantId, tenantId)
|
||||
.orderByDesc(BizContestNotice::getCreateTime));
|
||||
noticeService.fillContestInfo(list);
|
||||
return Result.success(list);
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@RequirePermission("notice:read")
|
||||
@Operation(summary = "分页查询公告列表")
|
||||
public Result<PageResult<BizContestNotice>> findAll(
|
||||
@RequestParam(defaultValue = "1") Long page,
|
||||
@RequestParam(defaultValue = "10") Long pageSize,
|
||||
@RequestParam(required = false) String title,
|
||||
@RequestParam(required = false) String status) {
|
||||
Long tenantId = SecurityUtil.getCurrentTenantId();
|
||||
LambdaQueryWrapper<BizContestNotice> wrapper = new LambdaQueryWrapper<BizContestNotice>()
|
||||
.eq(BizContestNotice::getTenantId, tenantId) // 租户隔离
|
||||
.like(StringUtils.hasText(title), BizContestNotice::getTitle, title);
|
||||
|
||||
// 发布状态过滤
|
||||
if (PublishStatus.PUBLISHED.getValue().equals(status)) {
|
||||
wrapper.isNotNull(BizContestNotice::getPublishTime);
|
||||
} else if (PublishStatus.UNPUBLISHED.getValue().equals(status)) {
|
||||
wrapper.isNull(BizContestNotice::getPublishTime);
|
||||
}
|
||||
|
||||
wrapper.orderByDesc(BizContestNotice::getCreateTime);
|
||||
Page<BizContestNotice> result = noticeService.page(new Page<>(page, pageSize), wrapper);
|
||||
noticeService.fillContestInfo(result.getRecords());
|
||||
return Result.success(PageResult.from(result));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@RequirePermission("notice:read")
|
||||
@Operation(summary = "查询公告详情")
|
||||
public Result<BizContestNotice> findDetail(@PathVariable Long id) {
|
||||
BizContestNotice notice = noticeService.getById(id);
|
||||
if (notice != null) {
|
||||
noticeService.fillContestInfo(Collections.singletonList(notice));
|
||||
}
|
||||
return Result.success(notice);
|
||||
}
|
||||
|
||||
@PatchMapping("/{id}")
|
||||
@RequirePermission("notice:update")
|
||||
@Operation(summary = "更新公告(部分字段;publishTime 为 null 或空串表示取消发布并写入数据库 NULL)")
|
||||
public Result<Void> update(@PathVariable Long id, @RequestBody Map<String, Object> body) {
|
||||
Long tenantId = SecurityUtil.getCurrentTenantId();
|
||||
BizContestNotice existing = noticeService.getOne(
|
||||
new LambdaQueryWrapper<BizContestNotice>()
|
||||
.eq(BizContestNotice::getId, id)
|
||||
.eq(BizContestNotice::getTenantId, tenantId));
|
||||
if (existing == null) {
|
||||
throw BusinessException.of(ErrorCode.NOT_FOUND, "公告不存在");
|
||||
}
|
||||
|
||||
LambdaUpdateWrapper<BizContestNotice> uw = new LambdaUpdateWrapper<>();
|
||||
uw.eq(BizContestNotice::getId, id);
|
||||
uw.eq(BizContestNotice::getTenantId, tenantId);
|
||||
|
||||
boolean hasUpdate = false;
|
||||
if (body.containsKey("title")) {
|
||||
Object v = body.get("title");
|
||||
if (v != null) {
|
||||
uw.set(BizContestNotice::getTitle, String.valueOf(v));
|
||||
hasUpdate = true;
|
||||
}
|
||||
}
|
||||
if (body.containsKey("content")) {
|
||||
Object v = body.get("content");
|
||||
if (v != null) {
|
||||
uw.set(BizContestNotice::getContent, String.valueOf(v));
|
||||
hasUpdate = true;
|
||||
}
|
||||
}
|
||||
if (body.containsKey("noticeType")) {
|
||||
Object v = body.get("noticeType");
|
||||
if (v != null) {
|
||||
uw.set(BizContestNotice::getNoticeType, String.valueOf(v));
|
||||
hasUpdate = true;
|
||||
}
|
||||
}
|
||||
if (body.containsKey("priority")) {
|
||||
Object v = body.get("priority");
|
||||
if (v instanceof Number) {
|
||||
uw.set(BizContestNotice::getPriority, ((Number) v).intValue());
|
||||
hasUpdate = true;
|
||||
}
|
||||
}
|
||||
if (body.containsKey("contestId")) {
|
||||
Object v = body.get("contestId");
|
||||
if (v instanceof Number) {
|
||||
uw.set(BizContestNotice::getContestId, ((Number) v).longValue());
|
||||
hasUpdate = true;
|
||||
}
|
||||
}
|
||||
// 仅当请求体包含 publishTime 键时才改发布时间:null / 空串 = 取消发布(必须写入 SQL NULL)
|
||||
if (body.containsKey("publishTime")) {
|
||||
Object v = body.get("publishTime");
|
||||
if (v == null || (v instanceof String && !StringUtils.hasText((String) v))) {
|
||||
uw.set(BizContestNotice::getPublishTime, null);
|
||||
hasUpdate = true;
|
||||
} else if (v instanceof String && StringUtils.hasText((String) v)) {
|
||||
LocalDateTime pt = parseDateTime((String) v);
|
||||
if (pt != null) {
|
||||
uw.set(BizContestNotice::getPublishTime, pt);
|
||||
hasUpdate = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasUpdate) {
|
||||
return Result.success();
|
||||
}
|
||||
noticeService.getBaseMapper().update(null, uw);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@RequirePermission("notice:delete")
|
||||
@Operation(summary = "删除公告")
|
||||
public Result<Void> remove(@PathVariable Long id) {
|
||||
noticeService.removeById(id);
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,127 @@
|
||||
package com.competition.modules.biz.contest.controller;
|
||||
|
||||
import com.competition.common.result.PageResult;
|
||||
import com.competition.common.result.Result;
|
||||
import com.competition.common.util.SecurityUtil;
|
||||
import com.competition.modules.biz.contest.dto.CreateRegistrationDto;
|
||||
import com.competition.modules.biz.contest.dto.QueryRegistrationDto;
|
||||
import com.competition.modules.biz.contest.service.IContestRegistrationService;
|
||||
import com.competition.security.annotation.RequirePermission;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "报名管理")
|
||||
@RestController
|
||||
@RequestMapping("/contests/registrations")
|
||||
@RequiredArgsConstructor
|
||||
public class ContestRegistrationController {
|
||||
|
||||
private final IContestRegistrationService registrationService;
|
||||
|
||||
@PostMapping
|
||||
@RequirePermission("contest:register")
|
||||
@Operation(summary = "创建报名")
|
||||
public Result<Map<String, Object>> create(@Valid @RequestBody CreateRegistrationDto dto) {
|
||||
Long tenantId = SecurityUtil.getCurrentTenantId();
|
||||
Long userId = SecurityUtil.getCurrentUserId();
|
||||
return Result.success(registrationService.createRegistration(dto, tenantId, userId));
|
||||
}
|
||||
|
||||
@GetMapping("/stats")
|
||||
@RequirePermission("contest:read")
|
||||
@Operation(summary = "获取报名统计")
|
||||
public Result<Map<String, Object>> getStats(@RequestParam(required = false) Long contestId) {
|
||||
Long tenantId = SecurityUtil.getCurrentTenantId();
|
||||
boolean isSuperAdmin = SecurityUtil.isSuperAdmin();
|
||||
return Result.success(registrationService.getStats(contestId, tenantId, isSuperAdmin));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@RequirePermission("contest:read")
|
||||
@Operation(summary = "查询报名列表")
|
||||
public Result<PageResult<Map<String, Object>>> findAll(QueryRegistrationDto dto) {
|
||||
Long tenantId = SecurityUtil.getCurrentTenantId();
|
||||
boolean isSuperTenant = SecurityUtil.isSuperAdmin();
|
||||
return Result.success(registrationService.findAll(dto, tenantId, isSuperTenant));
|
||||
}
|
||||
|
||||
@GetMapping("/my/{contestId}")
|
||||
@RequirePermission("contest:read")
|
||||
@Operation(summary = "获取我的报名信息")
|
||||
public Result<Map<String, Object>> getMyRegistration(@PathVariable Long contestId) {
|
||||
Long userId = SecurityUtil.getCurrentUserId();
|
||||
Long tenantId = SecurityUtil.getCurrentTenantId();
|
||||
return Result.success(registrationService.getMyRegistration(contestId, userId, tenantId));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@RequirePermission("contest:read")
|
||||
@Operation(summary = "查询报名详情")
|
||||
public Result<Map<String, Object>> findDetail(@PathVariable Long id) {
|
||||
return Result.success(registrationService.findDetail(id, SecurityUtil.getCurrentTenantId()));
|
||||
}
|
||||
|
||||
@PatchMapping("/{id}/review")
|
||||
@RequirePermission("contest:update")
|
||||
@Operation(summary = "审核报名")
|
||||
public Result<Void> review(@PathVariable Long id, @RequestBody Map<String, String> body) {
|
||||
Long operatorId = SecurityUtil.getCurrentUserId();
|
||||
Long tenantId = SecurityUtil.getCurrentTenantId();
|
||||
registrationService.reviewRegistration(id, body.get("registrationState"), body.get("reason"), operatorId, tenantId);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@PatchMapping("/{id}/revoke")
|
||||
@RequirePermission("contest:update")
|
||||
@Operation(summary = "撤回审核")
|
||||
public Result<Void> revoke(@PathVariable Long id) {
|
||||
registrationService.revokeReview(id, SecurityUtil.getCurrentTenantId());
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@PostMapping("/batch-review")
|
||||
@RequirePermission("contest:update")
|
||||
@Operation(summary = "批量审核报名")
|
||||
@SuppressWarnings("unchecked")
|
||||
public Result<Void> batchReview(@RequestBody Map<String, Object> body) {
|
||||
List<Long> ids = (List<Long>) body.get("ids");
|
||||
String registrationState = (String) body.get("registrationState");
|
||||
String reason = (String) body.get("reason");
|
||||
Long operatorId = SecurityUtil.getCurrentUserId();
|
||||
Long tenantId = SecurityUtil.getCurrentTenantId();
|
||||
registrationService.batchReview(ids, registrationState, reason, operatorId, tenantId);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/teachers")
|
||||
@RequirePermission("contest:update")
|
||||
@Operation(summary = "添加指导老师")
|
||||
public Result<Void> addTeacher(@PathVariable Long id, @RequestBody Map<String, Long> body) {
|
||||
Long tenantId = SecurityUtil.getCurrentTenantId();
|
||||
Long creatorId = SecurityUtil.getCurrentUserId();
|
||||
registrationService.addTeacher(id, body.get("teacherUserId"), tenantId, creatorId);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}/teachers/{teacherUserId}")
|
||||
@RequirePermission("contest:update")
|
||||
@Operation(summary = "移除指导老师")
|
||||
public Result<Void> removeTeacher(@PathVariable Long id, @PathVariable Long teacherUserId) {
|
||||
registrationService.removeTeacher(id, teacherUserId, SecurityUtil.getCurrentTenantId());
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@RequirePermission("contest:update")
|
||||
@Operation(summary = "删除报名")
|
||||
public Result<Void> remove(@PathVariable Long id) {
|
||||
registrationService.removeRegistration(id, SecurityUtil.getCurrentTenantId());
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
package com.competition.modules.biz.contest.controller;
|
||||
|
||||
import com.competition.common.result.Result;
|
||||
import com.competition.common.util.SecurityUtil;
|
||||
import com.competition.modules.biz.contest.dto.CreateTeamDto;
|
||||
import com.competition.modules.biz.contest.entity.BizContestTeam;
|
||||
import com.competition.modules.biz.contest.service.IContestTeamService;
|
||||
import com.competition.security.annotation.RequirePermission;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "团队管理")
|
||||
@RestController
|
||||
@RequestMapping("/contests/teams")
|
||||
@RequiredArgsConstructor
|
||||
public class ContestTeamController {
|
||||
|
||||
private final IContestTeamService teamService;
|
||||
|
||||
@PostMapping
|
||||
@RequirePermission("team:create")
|
||||
@Operation(summary = "创建团队")
|
||||
public Result<Map<String, Object>> create(@Valid @RequestBody CreateTeamDto dto) {
|
||||
Long tenantId = SecurityUtil.getCurrentTenantId();
|
||||
Long creatorId = SecurityUtil.getCurrentUserId();
|
||||
return Result.success(teamService.createTeam(dto, tenantId, creatorId));
|
||||
}
|
||||
|
||||
@GetMapping("/contest/{contestId}")
|
||||
@RequirePermission("team:read")
|
||||
@Operation(summary = "查询活动下的团队列表")
|
||||
public Result<List<Map<String, Object>>> findByContest(@PathVariable Long contestId) {
|
||||
return Result.success(teamService.findByContest(contestId, SecurityUtil.getCurrentTenantId()));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@RequirePermission("team:read")
|
||||
@Operation(summary = "查询团队详情")
|
||||
public Result<Map<String, Object>> findDetail(@PathVariable Long id) {
|
||||
return Result.success(teamService.findDetail(id));
|
||||
}
|
||||
|
||||
@PatchMapping("/{id}")
|
||||
@RequirePermission("team:update")
|
||||
@Operation(summary = "更新团队")
|
||||
public Result<BizContestTeam> update(@PathVariable Long id, @RequestBody CreateTeamDto dto) {
|
||||
return Result.success(teamService.updateTeam(id, dto));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/members")
|
||||
@RequirePermission("team:update")
|
||||
@Operation(summary = "添加团队成员")
|
||||
public Result<Void> addMember(@PathVariable Long id, @RequestBody Map<String, Object> body) {
|
||||
Long userId = ((Number) body.get("userId")).longValue();
|
||||
String role = (String) body.get("role");
|
||||
teamService.addMember(id, userId, role, SecurityUtil.getCurrentTenantId());
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}/members/{userId}")
|
||||
@RequirePermission("team:update")
|
||||
@Operation(summary = "移除团队成员")
|
||||
public Result<Void> removeMember(@PathVariable Long id, @PathVariable Long userId) {
|
||||
teamService.removeMember(id, userId);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@RequirePermission("team:delete")
|
||||
@Operation(summary = "删除团队")
|
||||
public Result<Void> remove(@PathVariable Long id) {
|
||||
teamService.removeTeam(id);
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,89 @@
|
||||
package com.competition.modules.biz.contest.controller;
|
||||
|
||||
import com.competition.common.result.PageResult;
|
||||
import com.competition.common.result.Result;
|
||||
import com.competition.common.util.SecurityUtil;
|
||||
import com.competition.modules.biz.contest.dto.QueryWorkDto;
|
||||
import com.competition.modules.biz.contest.dto.SubmitWorkDto;
|
||||
import com.competition.modules.biz.contest.service.IContestWorkService;
|
||||
import com.competition.security.annotation.RequirePermission;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "作品管理")
|
||||
@RestController
|
||||
@RequestMapping("/contests/works")
|
||||
@RequiredArgsConstructor
|
||||
public class ContestWorkController {
|
||||
|
||||
private final IContestWorkService workService;
|
||||
|
||||
@PostMapping("/submit")
|
||||
@RequirePermission("work:submit")
|
||||
@Operation(summary = "提交作品")
|
||||
public Result<Map<String, Object>> submit(@Valid @RequestBody SubmitWorkDto dto) {
|
||||
Long tenantId = SecurityUtil.getCurrentTenantId();
|
||||
Long submitterId = SecurityUtil.getCurrentUserId();
|
||||
return Result.success(workService.submitWork(dto, tenantId, submitterId));
|
||||
}
|
||||
|
||||
@GetMapping("/stats")
|
||||
@RequirePermission("work:read")
|
||||
@Operation(summary = "获取作品统计")
|
||||
public Result<Map<String, Object>> getStats(@RequestParam(required = false) Long contestId) {
|
||||
return Result.success(
|
||||
workService.getStats(contestId, SecurityUtil.getCurrentTenantId(), SecurityUtil.isSuperAdmin())
|
||||
);
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@RequirePermission("work:read")
|
||||
@Operation(summary = "查询作品列表")
|
||||
public Result<PageResult<Map<String, Object>>> findAll(QueryWorkDto dto) {
|
||||
Long tenantId = SecurityUtil.getCurrentTenantId();
|
||||
boolean isSuperTenant = SecurityUtil.isSuperAdmin();
|
||||
return Result.success(workService.findAll(dto, tenantId, isSuperTenant));
|
||||
}
|
||||
|
||||
@GetMapping("/guided")
|
||||
@RequirePermission("activity:read")
|
||||
@Operation(summary = "查询辅导作品")
|
||||
public Result<PageResult<Map<String, Object>>> getGuidedWorks(
|
||||
@RequestParam(required = false) Long contestId,
|
||||
@RequestParam(required = false) String workNo,
|
||||
@RequestParam(required = false) String playerName,
|
||||
@RequestParam(required = false) String accountNo,
|
||||
@RequestParam(defaultValue = "1") Long page,
|
||||
@RequestParam(defaultValue = "10") Long pageSize) {
|
||||
Long userId = SecurityUtil.getCurrentUserId();
|
||||
return Result.success(workService.getGuidedWorks(contestId, workNo, playerName, accountNo, page, pageSize, userId));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@RequirePermission("work:read")
|
||||
@Operation(summary = "查询作品详情")
|
||||
public Result<Map<String, Object>> findDetail(@PathVariable Long id) {
|
||||
return Result.success(workService.findDetail(id));
|
||||
}
|
||||
|
||||
@GetMapping("/registration/{registrationId}/versions")
|
||||
@RequirePermission("work:read")
|
||||
@Operation(summary = "查询作品版本历史")
|
||||
public Result<List<Map<String, Object>>> getWorkVersions(@PathVariable Long registrationId) {
|
||||
return Result.success(workService.getWorkVersions(registrationId));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@RequirePermission("work:update")
|
||||
@Operation(summary = "删除作品")
|
||||
public Result<Void> remove(@PathVariable Long id) {
|
||||
workService.removeWork(id);
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,130 @@
|
||||
package com.competition.modules.biz.contest.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Schema(description = "创建活动DTO")
|
||||
public class CreateContestDto {
|
||||
|
||||
@NotBlank(message = "活动名称不能为空")
|
||||
@Schema(description = "活动名称")
|
||||
private String contestName;
|
||||
|
||||
@NotBlank(message = "活动类型不能为空")
|
||||
@Schema(description = "活动类型")
|
||||
private String contestType;
|
||||
|
||||
@Schema(description = "可见性")
|
||||
private String visibility;
|
||||
|
||||
@Schema(description = "目标城市列表")
|
||||
private List<String> targetCities;
|
||||
|
||||
@Schema(description = "最小年龄")
|
||||
private Integer ageMin;
|
||||
|
||||
@Schema(description = "最大年龄")
|
||||
private Integer ageMax;
|
||||
|
||||
@NotBlank(message = "开始时间不能为空")
|
||||
@Schema(description = "开始时间")
|
||||
private String startTime;
|
||||
|
||||
@NotBlank(message = "结束时间不能为空")
|
||||
@Schema(description = "结束时间")
|
||||
private String 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 Object organizers;
|
||||
|
||||
@Schema(description = "协办方")
|
||||
private Object coOrganizers;
|
||||
|
||||
@Schema(description = "赞助方")
|
||||
private Object sponsors;
|
||||
|
||||
@NotBlank(message = "报名开始时间不能为空")
|
||||
@Schema(description = "报名开始时间")
|
||||
private String registerStartTime;
|
||||
|
||||
@NotBlank(message = "报名结束时间不能为空")
|
||||
@Schema(description = "报名结束时间")
|
||||
private String registerEndTime;
|
||||
|
||||
@Schema(description = "报名状态")
|
||||
private String registerState;
|
||||
|
||||
@Schema(description = "是否需要审核")
|
||||
private Boolean requireAudit;
|
||||
|
||||
@Schema(description = "允许的年级列表")
|
||||
private List<Integer> allowedGrades;
|
||||
|
||||
@Schema(description = "允许的班级列表")
|
||||
private List<Integer> allowedClasses;
|
||||
|
||||
@Schema(description = "团队最小人数")
|
||||
private Integer teamMinMembers;
|
||||
|
||||
@Schema(description = "团队最大人数")
|
||||
private Integer teamMaxMembers;
|
||||
|
||||
@Schema(description = "提交规则")
|
||||
private String submitRule;
|
||||
|
||||
@NotBlank(message = "提交开始时间不能为空")
|
||||
@Schema(description = "提交开始时间")
|
||||
private String submitStartTime;
|
||||
|
||||
@NotBlank(message = "提交结束时间不能为空")
|
||||
@Schema(description = "提交结束时间")
|
||||
private String submitEndTime;
|
||||
|
||||
@Schema(description = "作品类型")
|
||||
private String workType;
|
||||
|
||||
@Schema(description = "作品要求")
|
||||
private String workRequirement;
|
||||
|
||||
@Schema(description = "评审规则ID")
|
||||
private Long reviewRuleId;
|
||||
|
||||
@NotBlank(message = "评审开始时间不能为空")
|
||||
@Schema(description = "评审开始时间")
|
||||
private String reviewStartTime;
|
||||
|
||||
@NotBlank(message = "评审结束时间不能为空")
|
||||
@Schema(description = "评审结束时间")
|
||||
private String reviewEndTime;
|
||||
|
||||
@Schema(description = "结果发布时间")
|
||||
private String resultPublishTime;
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
package com.competition.modules.biz.contest.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "创建公告DTO")
|
||||
public class CreateNoticeDto {
|
||||
|
||||
@NotNull(message = "活动ID不能为空")
|
||||
@Schema(description = "活动ID")
|
||||
private Long contestId;
|
||||
|
||||
@NotBlank(message = "公告标题不能为空")
|
||||
@Schema(description = "公告标题")
|
||||
private String title;
|
||||
|
||||
@NotBlank(message = "公告内容不能为空")
|
||||
@Schema(description = "公告内容")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "公告类型")
|
||||
private String noticeType;
|
||||
|
||||
@Schema(description = "优先级")
|
||||
private Integer priority;
|
||||
|
||||
@Schema(description = "发布时间")
|
||||
private String publishTime;
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
package com.competition.modules.biz.contest.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "创建报名DTO")
|
||||
public class CreateRegistrationDto {
|
||||
|
||||
@NotNull(message = "活动ID不能为空")
|
||||
@Schema(description = "活动ID")
|
||||
private Long contestId;
|
||||
|
||||
@NotBlank(message = "报名类型不能为空")
|
||||
@Schema(description = "报名类型")
|
||||
private String registrationType;
|
||||
|
||||
@Schema(description = "团队ID")
|
||||
private Long teamId;
|
||||
|
||||
@NotNull(message = "用户ID不能为空")
|
||||
@Schema(description = "用户ID")
|
||||
private Long userId;
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
package com.competition.modules.biz.contest.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Schema(description = "创建团队DTO")
|
||||
public class CreateTeamDto {
|
||||
|
||||
@NotNull(message = "活动ID不能为空")
|
||||
@Schema(description = "活动ID")
|
||||
private Long contestId;
|
||||
|
||||
@NotBlank(message = "团队名称不能为空")
|
||||
@Schema(description = "团队名称")
|
||||
private String teamName;
|
||||
|
||||
@NotNull(message = "队长ID不能为空")
|
||||
@Schema(description = "队长ID")
|
||||
private Long leaderId;
|
||||
|
||||
@Schema(description = "成员ID列表")
|
||||
private List<Long> memberIds;
|
||||
|
||||
@Schema(description = "指导老师ID列表")
|
||||
private List<Long> teacherIds;
|
||||
|
||||
@Schema(description = "最大成员数")
|
||||
private Integer maxMembers;
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
package com.competition.modules.biz.contest.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "查询活动DTO")
|
||||
public class QueryContestDto {
|
||||
|
||||
@Schema(description = "页码", defaultValue = "1")
|
||||
private Long page = 1L;
|
||||
|
||||
@Schema(description = "每页条数", defaultValue = "10")
|
||||
private Long pageSize = 10L;
|
||||
|
||||
@Schema(description = "活动名称")
|
||||
private String contestName;
|
||||
|
||||
@Schema(description = "活动状态")
|
||||
private String contestState;
|
||||
|
||||
@Schema(description = "状态")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "活动类型")
|
||||
private String contestType;
|
||||
|
||||
@Schema(description = "可见性")
|
||||
private String visibility;
|
||||
|
||||
@Schema(description = "活动阶段")
|
||||
private String stage;
|
||||
|
||||
@Schema(description = "创建者租户ID")
|
||||
private Long creatorTenantId;
|
||||
|
||||
@Schema(description = "角色")
|
||||
private String role;
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
package com.competition.modules.biz.contest.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "查询报名DTO")
|
||||
public class QueryRegistrationDto {
|
||||
|
||||
@Schema(description = "页码", defaultValue = "1")
|
||||
private Long page = 1L;
|
||||
|
||||
@Schema(description = "每页条数", defaultValue = "10")
|
||||
private Long pageSize = 10L;
|
||||
|
||||
@Schema(description = "活动ID")
|
||||
private Long contestId;
|
||||
|
||||
@Schema(description = "报名状态")
|
||||
private String registrationState;
|
||||
|
||||
@Schema(description = "报名类型")
|
||||
private String registrationType;
|
||||
|
||||
@Schema(description = "用户ID")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "参赛者类型")
|
||||
private String participantType;
|
||||
|
||||
@Schema(description = "关键词")
|
||||
private String keyword;
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
package com.competition.modules.biz.contest.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "查询作品DTO")
|
||||
public class QueryWorkDto {
|
||||
|
||||
@Schema(description = "页码", defaultValue = "1")
|
||||
private Long page = 1L;
|
||||
|
||||
@Schema(description = "每页条数", defaultValue = "10")
|
||||
private Long pageSize = 10L;
|
||||
|
||||
@Schema(description = "活动ID")
|
||||
private Long contestId;
|
||||
|
||||
@Schema(description = "报名ID")
|
||||
private Long registrationId;
|
||||
|
||||
@Schema(description = "状态")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "作品标题")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "作品编号")
|
||||
private String workNo;
|
||||
|
||||
@Schema(description = "用户名")
|
||||
private String username;
|
||||
|
||||
@Schema(description = "关键词")
|
||||
private String keyword;
|
||||
|
||||
@Schema(description = "姓名")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "分配状态")
|
||||
private String assignStatus;
|
||||
|
||||
@Schema(description = "租户ID")
|
||||
private Long tenantId;
|
||||
|
||||
@Schema(description = "提交开始时间")
|
||||
private String submitStartTime;
|
||||
|
||||
@Schema(description = "提交结束时间")
|
||||
private String submitEndTime;
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
package com.competition.modules.biz.contest.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Schema(description = "提交作品DTO")
|
||||
public class SubmitWorkDto {
|
||||
|
||||
@NotNull(message = "报名ID不能为空")
|
||||
@Schema(description = "报名ID")
|
||||
private Long registrationId;
|
||||
|
||||
@NotBlank(message = "作品标题不能为空")
|
||||
@Schema(description = "作品标题")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "作品描述")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "文件信息")
|
||||
private Object files;
|
||||
|
||||
@Schema(description = "预览图URL")
|
||||
private String previewUrl;
|
||||
|
||||
@Schema(description = "预览图URL列表")
|
||||
private List<String> previewUrls;
|
||||
|
||||
@Schema(description = "AI模型元数据")
|
||||
private Object aiModelMeta;
|
||||
|
||||
@Schema(description = "附件列表")
|
||||
private List<AttachmentItem> attachments;
|
||||
|
||||
@Data
|
||||
@Schema(description = "附件项")
|
||||
public static class AttachmentItem {
|
||||
|
||||
@Schema(description = "文件名")
|
||||
private String fileName;
|
||||
|
||||
@Schema(description = "文件URL")
|
||||
private String fileUrl;
|
||||
|
||||
@Schema(description = "文件类型")
|
||||
private String fileType;
|
||||
|
||||
@Schema(description = "文件大小")
|
||||
private String size;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,183 @@
|
||||
package com.competition.modules.biz.contest.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import com.competition.common.entity.BaseEntity;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 活动实体(35+ 字段,7 个 JSON 列)
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName(value = "t_biz_contest", autoResultMap = true)
|
||||
@Schema(description = "活动实体")
|
||||
public class BizContest extends BaseEntity {
|
||||
|
||||
@Schema(description = "活动名称")
|
||||
@TableField("contest_name")
|
||||
private String contestName;
|
||||
|
||||
@Schema(description = "活动类型:individual/team")
|
||||
@TableField("contest_type")
|
||||
private String contestType;
|
||||
|
||||
@Schema(description = "活动发布状态", allowableValues = {"published", "unpublished"})
|
||||
@TableField("contest_state")
|
||||
private String contestState;
|
||||
|
||||
@Schema(description = "活动进度状态:ongoing/finished")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "开始时间")
|
||||
@TableField("start_time")
|
||||
private LocalDateTime startTime;
|
||||
|
||||
@Schema(description = "结束时间")
|
||||
@TableField("end_time")
|
||||
private LocalDateTime endTime;
|
||||
|
||||
@Schema(description = "线下地址")
|
||||
private String address;
|
||||
|
||||
@Schema(description = "活动详情(富文本)")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "可见范围", allowableValues = {"public", "designated", "internal", "private"})
|
||||
private String visibility;
|
||||
|
||||
// ====== 授权租户(JSON) ======
|
||||
@Schema(description = "授权租户ID数组")
|
||||
@TableField(value = "contest_tenants", typeHandler = JacksonTypeHandler.class)
|
||||
private List<Integer> contestTenants;
|
||||
|
||||
// ====== 封面和联系方式 ======
|
||||
@Schema(description = "封面图URL")
|
||||
@TableField("cover_url")
|
||||
private String coverUrl;
|
||||
|
||||
@Schema(description = "海报URL")
|
||||
@TableField("poster_url")
|
||||
private String posterUrl;
|
||||
|
||||
@Schema(description = "联系人姓名")
|
||||
@TableField("contact_name")
|
||||
private String contactName;
|
||||
|
||||
@Schema(description = "联系电话")
|
||||
@TableField("contact_phone")
|
||||
private String contactPhone;
|
||||
|
||||
@Schema(description = "联系二维码")
|
||||
@TableField("contact_qrcode")
|
||||
private String contactQrcode;
|
||||
|
||||
// ====== 主办/协办/赞助(JSON) ======
|
||||
@Schema(description = "主办方信息(JSON)")
|
||||
@TableField(value = "organizers", typeHandler = JacksonTypeHandler.class)
|
||||
private Object organizers;
|
||||
|
||||
@Schema(description = "协办方信息(JSON)")
|
||||
@TableField(value = "co_organizers", typeHandler = JacksonTypeHandler.class)
|
||||
private Object coOrganizers;
|
||||
|
||||
@Schema(description = "赞助方信息(JSON)")
|
||||
@TableField(value = "sponsors", typeHandler = JacksonTypeHandler.class)
|
||||
private Object sponsors;
|
||||
|
||||
// ====== 报名配置 ======
|
||||
@Schema(description = "报名开始时间")
|
||||
@TableField("register_start_time")
|
||||
private LocalDateTime registerStartTime;
|
||||
|
||||
@Schema(description = "报名结束时间")
|
||||
@TableField("register_end_time")
|
||||
private LocalDateTime registerEndTime;
|
||||
|
||||
@Schema(description = "报名状态")
|
||||
@TableField("register_state")
|
||||
private String registerState;
|
||||
|
||||
@Schema(description = "是否需要审核")
|
||||
@TableField("require_audit")
|
||||
private Boolean requireAudit;
|
||||
|
||||
@Schema(description = "允许参赛的年级(JSON数组)")
|
||||
@TableField(value = "allowed_grades", typeHandler = JacksonTypeHandler.class)
|
||||
private List<Integer> allowedGrades;
|
||||
|
||||
@Schema(description = "允许参赛的班级(JSON数组)")
|
||||
@TableField(value = "allowed_classes", typeHandler = JacksonTypeHandler.class)
|
||||
private List<Integer> allowedClasses;
|
||||
|
||||
@Schema(description = "团队最小人数")
|
||||
@TableField("team_min_members")
|
||||
private Integer teamMinMembers;
|
||||
|
||||
@Schema(description = "团队最大人数")
|
||||
@TableField("team_max_members")
|
||||
private Integer teamMaxMembers;
|
||||
|
||||
// ====== 目标筛选 ======
|
||||
@Schema(description = "目标城市(JSON数组)")
|
||||
@TableField(value = "target_cities", typeHandler = JacksonTypeHandler.class)
|
||||
private List<String> targetCities;
|
||||
|
||||
@Schema(description = "最小年龄")
|
||||
@TableField("age_min")
|
||||
private Integer ageMin;
|
||||
|
||||
@Schema(description = "最大年龄")
|
||||
@TableField("age_max")
|
||||
private Integer ageMax;
|
||||
|
||||
// ====== 提交配置 ======
|
||||
@Schema(description = "提交规则", allowableValues = {"once", "resubmit"})
|
||||
@TableField("submit_rule")
|
||||
private String submitRule;
|
||||
|
||||
@Schema(description = "提交开始时间")
|
||||
@TableField("submit_start_time")
|
||||
private LocalDateTime submitStartTime;
|
||||
|
||||
@Schema(description = "提交结束时间")
|
||||
@TableField("submit_end_time")
|
||||
private LocalDateTime submitEndTime;
|
||||
|
||||
@Schema(description = "作品类型")
|
||||
@TableField("work_type")
|
||||
private String workType;
|
||||
|
||||
@Schema(description = "作品要求")
|
||||
@TableField("work_requirement")
|
||||
private String workRequirement;
|
||||
|
||||
// ====== 评审配置 ======
|
||||
@Schema(description = "评审规则ID")
|
||||
@TableField("review_rule_id")
|
||||
private Long reviewRuleId;
|
||||
|
||||
@Schema(description = "评审开始时间")
|
||||
@TableField("review_start_time")
|
||||
private LocalDateTime reviewStartTime;
|
||||
|
||||
@Schema(description = "评审结束时间")
|
||||
@TableField("review_end_time")
|
||||
private LocalDateTime reviewEndTime;
|
||||
|
||||
// ====== 成果发布 ======
|
||||
@Schema(description = "成绩发布状态", allowableValues = {"published", "unpublished"})
|
||||
@TableField("result_state")
|
||||
private String resultState;
|
||||
|
||||
@Schema(description = "成绩发布时间")
|
||||
@TableField("result_publish_time")
|
||||
private LocalDateTime resultPublishTime;
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
package com.competition.modules.biz.contest.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.competition.common.entity.BaseEntity;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("t_biz_contest_attachment")
|
||||
@Schema(description = "活动附件实体")
|
||||
public class BizContestAttachment extends BaseEntity {
|
||||
|
||||
@Schema(description = "活动ID")
|
||||
@TableField("contest_id")
|
||||
private Long contestId;
|
||||
|
||||
@Schema(description = "文件名称")
|
||||
@TableField("file_name")
|
||||
private String fileName;
|
||||
|
||||
@Schema(description = "文件URL")
|
||||
@TableField("file_url")
|
||||
private String fileUrl;
|
||||
|
||||
@Schema(description = "文件格式")
|
||||
private String format;
|
||||
|
||||
@Schema(description = "文件类型")
|
||||
@TableField("file_type")
|
||||
private String fileType;
|
||||
|
||||
@Schema(description = "文件大小")
|
||||
private String size;
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
package com.competition.modules.biz.contest.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.competition.common.entity.BaseEntity;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 活动公告实体
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("t_biz_contest_notice")
|
||||
@Schema(description = "活动公告实体")
|
||||
public class BizContestNotice extends BaseEntity {
|
||||
|
||||
@Schema(description = "活动ID")
|
||||
@TableField("contest_id")
|
||||
private Long contestId;
|
||||
|
||||
@Schema(description = "租户ID")
|
||||
@TableField("tenant_id")
|
||||
private Long tenantId;
|
||||
|
||||
@Schema(description = "公告标题")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "公告内容(富文本)")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "公告类型:system/manual/urgent")
|
||||
@TableField("notice_type")
|
||||
private String noticeType;
|
||||
|
||||
@Schema(description = "优先级")
|
||||
private Integer priority;
|
||||
|
||||
@Schema(description = "发布时间")
|
||||
@TableField("publish_time")
|
||||
private LocalDateTime publishTime;
|
||||
|
||||
/** 关联活动(仅查询接口填充,不落库) */
|
||||
@Schema(description = "关联活动")
|
||||
@TableField(exist = false)
|
||||
private BizContest contest;
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
package com.competition.modules.biz.contest.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.competition.common.entity.BaseEntity;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("t_biz_contest_registration")
|
||||
@Schema(description = "活动报名实体")
|
||||
public class BizContestRegistration extends BaseEntity {
|
||||
|
||||
@Schema(description = "活动ID")
|
||||
@TableField("contest_id")
|
||||
private Long contestId;
|
||||
|
||||
@Schema(description = "租户ID")
|
||||
@TableField("tenant_id")
|
||||
private Long tenantId;
|
||||
|
||||
@Schema(description = "报名类型:individual/team")
|
||||
@TableField("registration_type")
|
||||
private String registrationType;
|
||||
|
||||
@Schema(description = "团队ID")
|
||||
@TableField("team_id")
|
||||
private Long teamId;
|
||||
|
||||
@Schema(description = "团队名称快照")
|
||||
@TableField("team_name")
|
||||
private String teamName;
|
||||
|
||||
@Schema(description = "用户ID")
|
||||
@TableField("user_id")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "账号快照")
|
||||
@TableField("account_no")
|
||||
private String accountNo;
|
||||
|
||||
@Schema(description = "账号名称")
|
||||
@TableField("account_name")
|
||||
private String accountName;
|
||||
|
||||
@Schema(description = "角色:leader/member/mentor")
|
||||
private String role;
|
||||
|
||||
@Schema(description = "报名状态", allowableValues = {"pending", "passed", "rejected"})
|
||||
@TableField("registration_state")
|
||||
private String registrationState;
|
||||
|
||||
@Schema(description = "参与者类型", allowableValues = {"self", "child"})
|
||||
@TableField("participant_type")
|
||||
private String participantType;
|
||||
|
||||
@Schema(description = "子女ID")
|
||||
@TableField("child_id")
|
||||
private Long childId;
|
||||
|
||||
@Schema(description = "实际提交人ID")
|
||||
private Integer registrant;
|
||||
|
||||
@Schema(description = "报名时间")
|
||||
@TableField("registration_time")
|
||||
private LocalDateTime registrationTime;
|
||||
|
||||
@Schema(description = "审核原因")
|
||||
private String reason;
|
||||
|
||||
@Schema(description = "审核操作人")
|
||||
private Integer operator;
|
||||
|
||||
@Schema(description = "操作日期")
|
||||
@TableField("operation_date")
|
||||
private LocalDateTime operationDate;
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
package com.competition.modules.biz.contest.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("t_biz_contest_registration_teacher")
|
||||
@Schema(description = "活动报名老师关联实体")
|
||||
public class BizContestRegistrationTeacher implements Serializable {
|
||||
|
||||
@Schema(description = "主键ID")
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "报名ID")
|
||||
@TableField("registration_id")
|
||||
private Long registrationId;
|
||||
|
||||
@Schema(description = "租户ID")
|
||||
@TableField("tenant_id")
|
||||
private Long tenantId;
|
||||
|
||||
@Schema(description = "用户ID")
|
||||
@TableField("user_id")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "是否默认")
|
||||
@TableField("is_default")
|
||||
private Boolean isDefault;
|
||||
|
||||
@Schema(description = "创建人ID")
|
||||
private Integer creator;
|
||||
|
||||
@Schema(description = "修改人ID")
|
||||
private Integer modifier;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
@TableField("create_time")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Schema(description = "修改时间")
|
||||
@TableField("modify_time")
|
||||
private LocalDateTime modifyTime;
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
package com.competition.modules.biz.contest.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.competition.common.entity.BaseEntity;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("t_biz_contest_team")
|
||||
@Schema(description = "活动团队实体")
|
||||
public class BizContestTeam extends BaseEntity {
|
||||
|
||||
@Schema(description = "租户ID")
|
||||
@TableField("tenant_id")
|
||||
private Long tenantId;
|
||||
|
||||
@Schema(description = "活动ID")
|
||||
@TableField("contest_id")
|
||||
private Long contestId;
|
||||
|
||||
@Schema(description = "团队名称")
|
||||
@TableField("team_name")
|
||||
private String teamName;
|
||||
|
||||
@Schema(description = "队长用户ID")
|
||||
@TableField("leader_user_id")
|
||||
private Long leaderUserId;
|
||||
|
||||
@Schema(description = "最大成员数")
|
||||
@TableField("max_members")
|
||||
private Integer maxMembers;
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
package com.competition.modules.biz.contest.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("t_biz_contest_team_member")
|
||||
@Schema(description = "活动团队成员实体")
|
||||
public class BizContestTeamMember implements Serializable {
|
||||
|
||||
@Schema(description = "主键ID")
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "租户ID")
|
||||
@TableField("tenant_id")
|
||||
private Long tenantId;
|
||||
|
||||
@Schema(description = "团队ID")
|
||||
@TableField("team_id")
|
||||
private Long teamId;
|
||||
|
||||
@Schema(description = "用户ID")
|
||||
@TableField("user_id")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "角色:member/leader/mentor")
|
||||
private String role;
|
||||
|
||||
@Schema(description = "创建人ID")
|
||||
private Integer creator;
|
||||
|
||||
@Schema(description = "修改人ID")
|
||||
private Integer modifier;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
@TableField("create_time")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Schema(description = "修改时间")
|
||||
@TableField("modify_time")
|
||||
private LocalDateTime modifyTime;
|
||||
}
|
||||
@ -0,0 +1,109 @@
|
||||
package com.competition.modules.biz.contest.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import com.competition.common.entity.BaseEntity;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName(value = "t_biz_contest_work", autoResultMap = true)
|
||||
@Schema(description = "活动作品实体")
|
||||
public class BizContestWork extends BaseEntity {
|
||||
|
||||
@Schema(description = "租户ID")
|
||||
@TableField("tenant_id")
|
||||
private Long tenantId;
|
||||
|
||||
@Schema(description = "活动ID")
|
||||
@TableField("contest_id")
|
||||
private Long contestId;
|
||||
|
||||
@Schema(description = "报名ID")
|
||||
@TableField("registration_id")
|
||||
private Long registrationId;
|
||||
|
||||
@Schema(description = "作品编号")
|
||||
@TableField("work_no")
|
||||
private String workNo;
|
||||
|
||||
@Schema(description = "作品标题")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "作品描述")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "作品文件(JSON)")
|
||||
@TableField(value = "files", typeHandler = JacksonTypeHandler.class)
|
||||
private Object files;
|
||||
|
||||
@Schema(description = "版本号")
|
||||
private Integer version;
|
||||
|
||||
@Schema(description = "是否最新版本")
|
||||
@TableField("is_latest")
|
||||
private Boolean isLatest;
|
||||
|
||||
@Schema(description = "作品状态", allowableValues = {"submitted", "locked", "reviewing", "rejected", "accepted", "awarded", "taken_down"})
|
||||
private String status;
|
||||
|
||||
@Schema(description = "提交时间")
|
||||
@TableField("submit_time")
|
||||
private LocalDateTime submitTime;
|
||||
|
||||
@Schema(description = "提交人用户ID")
|
||||
@TableField("submitter_user_id")
|
||||
private Long submitterUserId;
|
||||
|
||||
@Schema(description = "提交人账号")
|
||||
@TableField("submitter_account_no")
|
||||
private String submitterAccountNo;
|
||||
|
||||
@Schema(description = "提交来源:teacher/student/team_leader")
|
||||
@TableField("submit_source")
|
||||
private String submitSource;
|
||||
|
||||
@Schema(description = "预览图URL")
|
||||
@TableField("preview_url")
|
||||
private String previewUrl;
|
||||
|
||||
@Schema(description = "预览图URL列表(JSON)")
|
||||
@TableField(value = "preview_urls", typeHandler = JacksonTypeHandler.class)
|
||||
private List<String> previewUrls;
|
||||
|
||||
@Schema(description = "AI模型元数据(JSON)")
|
||||
@TableField(value = "ai_model_meta", typeHandler = JacksonTypeHandler.class)
|
||||
private Object aiModelMeta;
|
||||
|
||||
@Schema(description = "用户作品ID")
|
||||
@TableField("user_work_id")
|
||||
private Long userWorkId;
|
||||
|
||||
// ====== 成果字段 ======
|
||||
@Schema(description = "最终得分")
|
||||
@TableField("final_score")
|
||||
private BigDecimal finalScore;
|
||||
|
||||
@Schema(description = "排名")
|
||||
@TableField("`rank`")
|
||||
private Integer rank;
|
||||
|
||||
@Schema(description = "获奖等级:first/second/third/excellent/none")
|
||||
@TableField("award_level")
|
||||
private String awardLevel;
|
||||
|
||||
@Schema(description = "奖项名称")
|
||||
@TableField("award_name")
|
||||
private String awardName;
|
||||
|
||||
@Schema(description = "证书URL")
|
||||
@TableField("certificate_url")
|
||||
private String certificateUrl;
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
package com.competition.modules.biz.contest.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("t_biz_contest_work_attachment")
|
||||
@Schema(description = "活动作品附件实体")
|
||||
public class BizContestWorkAttachment implements Serializable {
|
||||
|
||||
@Schema(description = "主键ID")
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "租户ID")
|
||||
@TableField("tenant_id")
|
||||
private Long tenantId;
|
||||
|
||||
@Schema(description = "活动ID")
|
||||
@TableField("contest_id")
|
||||
private Long contestId;
|
||||
|
||||
@Schema(description = "作品ID")
|
||||
@TableField("work_id")
|
||||
private Long workId;
|
||||
|
||||
@Schema(description = "文件名称")
|
||||
@TableField("file_name")
|
||||
private String fileName;
|
||||
|
||||
@Schema(description = "文件URL")
|
||||
@TableField("file_url")
|
||||
private String fileUrl;
|
||||
|
||||
@Schema(description = "文件格式")
|
||||
private String format;
|
||||
|
||||
@Schema(description = "文件类型")
|
||||
@TableField("file_type")
|
||||
private String fileType;
|
||||
|
||||
@Schema(description = "文件大小")
|
||||
private String size;
|
||||
|
||||
@Schema(description = "创建人ID")
|
||||
private Integer creator;
|
||||
|
||||
@Schema(description = "修改人ID")
|
||||
private Integer modifier;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
@TableField("create_time")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Schema(description = "修改时间")
|
||||
@TableField("modify_time")
|
||||
private LocalDateTime modifyTime;
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.competition.modules.biz.contest.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.competition.modules.biz.contest.entity.BizContestAttachment;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface ContestAttachmentMapper extends BaseMapper<BizContestAttachment> {
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.competition.modules.biz.contest.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.competition.modules.biz.contest.entity.BizContest;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface ContestMapper extends BaseMapper<BizContest> {
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.competition.modules.biz.contest.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.competition.modules.biz.contest.entity.BizContestNotice;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface ContestNoticeMapper extends BaseMapper<BizContestNotice> {
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.competition.modules.biz.contest.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.competition.modules.biz.contest.entity.BizContestRegistration;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface ContestRegistrationMapper extends BaseMapper<BizContestRegistration> {
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.competition.modules.biz.contest.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.competition.modules.biz.contest.entity.BizContestRegistrationTeacher;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface ContestRegistrationTeacherMapper extends BaseMapper<BizContestRegistrationTeacher> {
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.competition.modules.biz.contest.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.competition.modules.biz.contest.entity.BizContestTeam;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface ContestTeamMapper extends BaseMapper<BizContestTeam> {
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.competition.modules.biz.contest.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.competition.modules.biz.contest.entity.BizContestTeamMember;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface ContestTeamMemberMapper extends BaseMapper<BizContestTeamMember> {
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.competition.modules.biz.contest.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.competition.modules.biz.contest.entity.BizContestWorkAttachment;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface ContestWorkAttachmentMapper extends BaseMapper<BizContestWorkAttachment> {
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.competition.modules.biz.contest.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.competition.modules.biz.contest.entity.BizContestWork;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface ContestWorkMapper extends BaseMapper<BizContestWork> {
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package com.competition.modules.biz.contest.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.competition.modules.biz.contest.entity.BizContestAttachment;
|
||||
|
||||
public interface IContestAttachmentService extends IService<BizContestAttachment> {
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
package com.competition.modules.biz.contest.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.competition.modules.biz.contest.entity.BizContestNotice;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface IContestNoticeService extends IService<BizContestNotice> {
|
||||
|
||||
/**
|
||||
* 批量填充关联活动名称(仅设置 id、contestName,供前端展示)
|
||||
*/
|
||||
void fillContestInfo(List<BizContestNotice> notices);
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
package com.competition.modules.biz.contest.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.competition.common.result.PageResult;
|
||||
import com.competition.modules.biz.contest.dto.CreateRegistrationDto;
|
||||
import com.competition.modules.biz.contest.dto.QueryRegistrationDto;
|
||||
import com.competition.modules.biz.contest.entity.BizContestRegistration;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface IContestRegistrationService extends IService<BizContestRegistration> {
|
||||
|
||||
Map<String, Object> createRegistration(CreateRegistrationDto dto, Long tenantId, Long creatorId);
|
||||
|
||||
PageResult<Map<String, Object>> findAll(QueryRegistrationDto dto, Long tenantId, boolean isSuperTenant);
|
||||
|
||||
Map<String, Object> getStats(Long contestId, Long tenantId, boolean isSuperAdmin);
|
||||
|
||||
Map<String, Object> findDetail(Long id, Long tenantId);
|
||||
|
||||
Map<String, Object> getMyRegistration(Long contestId, Long userId, Long tenantId);
|
||||
|
||||
void reviewRegistration(Long id, String state, String reason, Long operatorId, Long tenantId);
|
||||
|
||||
void revokeReview(Long id, Long tenantId);
|
||||
|
||||
void batchReview(List<Long> ids, String state, String reason, Long operatorId, Long tenantId);
|
||||
|
||||
void addTeacher(Long registrationId, Long teacherUserId, Long tenantId, Long creatorId);
|
||||
|
||||
void removeTeacher(Long registrationId, Long teacherUserId, Long tenantId);
|
||||
|
||||
void removeRegistration(Long id, Long tenantId);
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
package com.competition.modules.biz.contest.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.competition.common.result.PageResult;
|
||||
import com.competition.modules.biz.contest.dto.CreateContestDto;
|
||||
import com.competition.modules.biz.contest.dto.QueryContestDto;
|
||||
import com.competition.modules.biz.contest.entity.BizContest;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public interface IContestService extends IService<BizContest> {
|
||||
|
||||
BizContest createContest(CreateContestDto dto, Long creatorId);
|
||||
|
||||
PageResult<Map<String, Object>> findAll(QueryContestDto dto, Long tenantId, boolean isSuperTenant);
|
||||
|
||||
PageResult<Map<String, Object>> getMyContests(QueryContestDto dto, Long userId, Long tenantId);
|
||||
|
||||
Map<String, Object> findDetail(Long id);
|
||||
|
||||
BizContest updateContest(Long id, CreateContestDto dto);
|
||||
|
||||
void publishContest(Long id, String contestState);
|
||||
|
||||
void finishContest(Long id);
|
||||
|
||||
void reopenContest(Long id);
|
||||
|
||||
void removeContest(Long id);
|
||||
|
||||
Map<String, Object> getStats(Long tenantId, boolean isSuperTenant);
|
||||
|
||||
Map<String, Object> getDashboard(Long tenantId);
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
package com.competition.modules.biz.contest.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.competition.modules.biz.contest.dto.CreateTeamDto;
|
||||
import com.competition.modules.biz.contest.entity.BizContestTeam;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface IContestTeamService extends IService<BizContestTeam> {
|
||||
|
||||
Map<String, Object> createTeam(CreateTeamDto dto, Long tenantId, Long creatorId);
|
||||
|
||||
List<Map<String, Object>> findByContest(Long contestId, Long tenantId);
|
||||
|
||||
Map<String, Object> findDetail(Long id);
|
||||
|
||||
BizContestTeam updateTeam(Long id, CreateTeamDto dto);
|
||||
|
||||
void addMember(Long teamId, Long userId, String role, Long tenantId);
|
||||
|
||||
void removeMember(Long teamId, Long userId);
|
||||
|
||||
void removeTeam(Long id);
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
package com.competition.modules.biz.contest.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.competition.common.result.PageResult;
|
||||
import com.competition.modules.biz.contest.dto.QueryWorkDto;
|
||||
import com.competition.modules.biz.contest.dto.SubmitWorkDto;
|
||||
import com.competition.modules.biz.contest.entity.BizContestWork;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface IContestWorkService extends IService<BizContestWork> {
|
||||
|
||||
/**
|
||||
* 为指定活动生成下一个作品编号(与 {@link #submitWork} 所用规则一致:W{contestId}-{序号})。
|
||||
*/
|
||||
String nextContestWorkNo(Long contestId);
|
||||
|
||||
Map<String, Object> submitWork(SubmitWorkDto dto, Long tenantId, Long submitterId);
|
||||
|
||||
PageResult<Map<String, Object>> findAll(QueryWorkDto dto, Long tenantId, boolean isSuperTenant);
|
||||
|
||||
Map<String, Object> getStats(Long contestId, Long tenantId, boolean isSuperTenant);
|
||||
|
||||
Map<String, Object> findDetail(Long id);
|
||||
|
||||
List<Map<String, Object>> getWorkVersions(Long registrationId);
|
||||
|
||||
PageResult<Map<String, Object>> getGuidedWorks(Long contestId, String workNo, String playerName, String accountNo, Long page, Long pageSize, Long userId);
|
||||
|
||||
void removeWork(Long id);
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package com.competition.modules.biz.contest.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.competition.modules.biz.contest.entity.BizContestAttachment;
|
||||
import com.competition.modules.biz.contest.mapper.ContestAttachmentMapper;
|
||||
import com.competition.modules.biz.contest.service.IContestAttachmentService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class ContestAttachmentServiceImpl extends ServiceImpl<ContestAttachmentMapper, BizContestAttachment> implements IContestAttachmentService {
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
package com.competition.modules.biz.contest.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.competition.modules.biz.contest.entity.BizContest;
|
||||
import com.competition.modules.biz.contest.entity.BizContestNotice;
|
||||
import com.competition.modules.biz.contest.mapper.ContestNoticeMapper;
|
||||
import com.competition.modules.biz.contest.service.IContestNoticeService;
|
||||
import com.competition.modules.biz.contest.service.IContestService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ContestNoticeServiceImpl extends ServiceImpl<ContestNoticeMapper, BizContestNotice> implements IContestNoticeService {
|
||||
|
||||
private final IContestService contestService;
|
||||
|
||||
@Override
|
||||
public void fillContestInfo(List<BizContestNotice> notices) {
|
||||
if (notices == null || notices.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Set<Long> contestIds = notices.stream()
|
||||
.map(BizContestNotice::getContestId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
if (contestIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<BizContest> contests = contestService.listByIds(contestIds);
|
||||
Map<Long, BizContest> map = contests.stream()
|
||||
.collect(Collectors.toMap(BizContest::getId, Function.identity(), (a, b) -> a));
|
||||
for (BizContestNotice notice : notices) {
|
||||
Long cid = notice.getContestId();
|
||||
if (cid == null) {
|
||||
continue;
|
||||
}
|
||||
BizContest c = map.get(cid);
|
||||
if (c != null) {
|
||||
BizContest brief = new BizContest();
|
||||
brief.setId(c.getId());
|
||||
brief.setContestName(c.getContestName());
|
||||
notice.setContest(brief);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,350 @@
|
||||
package com.competition.modules.biz.contest.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.competition.common.enums.ErrorCode;
|
||||
import com.competition.common.enums.ParticipantType;
|
||||
import com.competition.common.enums.PublishStatus;
|
||||
import com.competition.common.enums.RegistrationStatus;
|
||||
import com.competition.common.exception.BusinessException;
|
||||
import com.competition.common.result.PageResult;
|
||||
import com.competition.modules.biz.contest.dto.CreateRegistrationDto;
|
||||
import com.competition.modules.biz.contest.dto.QueryRegistrationDto;
|
||||
import com.competition.modules.biz.contest.entity.BizContest;
|
||||
import com.competition.modules.biz.contest.entity.BizContestRegistration;
|
||||
import com.competition.modules.biz.contest.entity.BizContestRegistrationTeacher;
|
||||
import com.competition.modules.biz.contest.mapper.ContestMapper;
|
||||
import com.competition.modules.biz.contest.mapper.ContestRegistrationMapper;
|
||||
import com.competition.modules.biz.contest.mapper.ContestRegistrationTeacherMapper;
|
||||
import com.competition.modules.biz.contest.service.IContestRegistrationService;
|
||||
import com.competition.modules.sys.entity.SysUser;
|
||||
import com.competition.modules.sys.mapper.SysUserMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ContestRegistrationServiceImpl extends ServiceImpl<ContestRegistrationMapper, BizContestRegistration>
|
||||
implements IContestRegistrationService {
|
||||
|
||||
private final ContestRegistrationMapper contestRegistrationMapper;
|
||||
private final ContestRegistrationTeacherMapper contestRegistrationTeacherMapper;
|
||||
private final ContestMapper contestMapper;
|
||||
private final SysUserMapper sysUserMapper;
|
||||
|
||||
@Override
|
||||
public Map<String, Object> createRegistration(CreateRegistrationDto dto, Long tenantId, Long creatorId) {
|
||||
log.info("开始创建报名,活动ID:{},用户ID:{}", dto.getContestId(), dto.getUserId());
|
||||
|
||||
// 验证活动存在且已发布
|
||||
BizContest contest = contestMapper.selectById(dto.getContestId());
|
||||
if (contest == null) {
|
||||
throw BusinessException.of(ErrorCode.NOT_FOUND, "活动不存在");
|
||||
}
|
||||
if (!PublishStatus.PUBLISHED.getValue().equals(contest.getContestState())) {
|
||||
throw BusinessException.of(ErrorCode.BAD_REQUEST, "活动未发布,无法报名");
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
SysUser user = sysUserMapper.selectById(dto.getUserId());
|
||||
String accountNo = user != null ? user.getUsername() : null;
|
||||
String accountName = user != null ? user.getNickname() : null;
|
||||
|
||||
BizContestRegistration registration = new BizContestRegistration();
|
||||
registration.setContestId(dto.getContestId());
|
||||
registration.setTenantId(tenantId);
|
||||
registration.setRegistrationType(dto.getRegistrationType());
|
||||
registration.setTeamId(dto.getTeamId());
|
||||
registration.setUserId(dto.getUserId());
|
||||
registration.setAccountNo(accountNo);
|
||||
registration.setAccountName(accountName);
|
||||
registration.setRegistrationState(RegistrationStatus.PENDING.getValue());
|
||||
registration.setParticipantType(ParticipantType.SELF.getValue());
|
||||
registration.setRegistrationTime(LocalDateTime.now());
|
||||
registration.setRegistrant(creatorId != null ? creatorId.intValue() : null);
|
||||
registration.setCreator(creatorId != null ? creatorId.intValue() : null);
|
||||
|
||||
save(registration);
|
||||
log.info("报名创建成功,ID:{}", registration.getId());
|
||||
|
||||
// 如果创建者是教师角色,自动添加为默认指导教师
|
||||
// 简化实现:此处由调用方根据角色判断后调用 addTeacher
|
||||
// TODO: 检查创建者角色,自动关联教师
|
||||
|
||||
Map<String, Object> result = registrationToMap(registration);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<Map<String, Object>> findAll(QueryRegistrationDto dto, Long tenantId, boolean isSuperTenant) {
|
||||
log.info("查询报名列表,活动ID:{},页码:{}", dto.getContestId(), dto.getPage());
|
||||
|
||||
LambdaQueryWrapper<BizContestRegistration> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
if (dto.getContestId() != null) {
|
||||
wrapper.eq(BizContestRegistration::getContestId, dto.getContestId());
|
||||
}
|
||||
if (StringUtils.hasText(dto.getRegistrationState())) {
|
||||
wrapper.eq(BizContestRegistration::getRegistrationState, dto.getRegistrationState());
|
||||
}
|
||||
if (StringUtils.hasText(dto.getRegistrationType())) {
|
||||
wrapper.eq(BizContestRegistration::getRegistrationType, dto.getRegistrationType());
|
||||
}
|
||||
if (StringUtils.hasText(dto.getParticipantType())) {
|
||||
wrapper.eq(BizContestRegistration::getParticipantType, dto.getParticipantType());
|
||||
}
|
||||
if (dto.getUserId() != null) {
|
||||
wrapper.eq(BizContestRegistration::getUserId, dto.getUserId());
|
||||
}
|
||||
if (StringUtils.hasText(dto.getKeyword())) {
|
||||
wrapper.and(w -> w.like(BizContestRegistration::getAccountNo, dto.getKeyword())
|
||||
.or().like(BizContestRegistration::getAccountName, dto.getKeyword()));
|
||||
}
|
||||
|
||||
// 租户过滤
|
||||
if (!isSuperTenant && tenantId != null) {
|
||||
wrapper.eq(BizContestRegistration::getTenantId, tenantId);
|
||||
}
|
||||
|
||||
wrapper.orderByDesc(BizContestRegistration::getRegistrationTime);
|
||||
|
||||
Page<BizContestRegistration> page = new Page<>(dto.getPage(), dto.getPageSize());
|
||||
Page<BizContestRegistration> result = contestRegistrationMapper.selectPage(page, wrapper);
|
||||
|
||||
List<Map<String, Object>> voList = result.getRecords().stream()
|
||||
.map(this::registrationToMap)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return PageResult.from(result, voList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getStats(Long contestId, Long tenantId, boolean isSuperAdmin) {
|
||||
log.info("获取报名统计,活动ID:{},租户ID:{},超管:{}", contestId, tenantId, isSuperAdmin);
|
||||
|
||||
// 非超管需要按租户过滤
|
||||
boolean needTenantFilter = !isSuperAdmin && tenantId != null;
|
||||
|
||||
LambdaQueryWrapper<BizContestRegistration> baseWrapper = new LambdaQueryWrapper<>();
|
||||
if (contestId != null) {
|
||||
baseWrapper.eq(BizContestRegistration::getContestId, contestId);
|
||||
}
|
||||
if (needTenantFilter) {
|
||||
baseWrapper.eq(BizContestRegistration::getTenantId, tenantId);
|
||||
}
|
||||
long total = count(baseWrapper);
|
||||
|
||||
LambdaQueryWrapper<BizContestRegistration> pendingWrapper = new LambdaQueryWrapper<>();
|
||||
if (contestId != null) {
|
||||
pendingWrapper.eq(BizContestRegistration::getContestId, contestId);
|
||||
}
|
||||
if (needTenantFilter) {
|
||||
pendingWrapper.eq(BizContestRegistration::getTenantId, tenantId);
|
||||
}
|
||||
pendingWrapper.eq(BizContestRegistration::getRegistrationState, RegistrationStatus.PENDING.getValue());
|
||||
long pending = count(pendingWrapper);
|
||||
|
||||
LambdaQueryWrapper<BizContestRegistration> passedWrapper = new LambdaQueryWrapper<>();
|
||||
if (contestId != null) {
|
||||
passedWrapper.eq(BizContestRegistration::getContestId, contestId);
|
||||
}
|
||||
if (needTenantFilter) {
|
||||
passedWrapper.eq(BizContestRegistration::getTenantId, tenantId);
|
||||
}
|
||||
passedWrapper.eq(BizContestRegistration::getRegistrationState, RegistrationStatus.PASSED.getValue());
|
||||
long passed = count(passedWrapper);
|
||||
|
||||
LambdaQueryWrapper<BizContestRegistration> rejectedWrapper = new LambdaQueryWrapper<>();
|
||||
if (contestId != null) {
|
||||
rejectedWrapper.eq(BizContestRegistration::getContestId, contestId);
|
||||
}
|
||||
if (needTenantFilter) {
|
||||
rejectedWrapper.eq(BizContestRegistration::getTenantId, tenantId);
|
||||
}
|
||||
rejectedWrapper.eq(BizContestRegistration::getRegistrationState, RegistrationStatus.REJECTED.getValue());
|
||||
long rejected = count(rejectedWrapper);
|
||||
|
||||
Map<String, Object> stats = new LinkedHashMap<>();
|
||||
stats.put("total", total);
|
||||
stats.put("pending", pending);
|
||||
stats.put("passed", passed);
|
||||
stats.put("rejected", rejected);
|
||||
return stats;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> findDetail(Long id, Long tenantId) {
|
||||
log.info("查询报名详情,ID:{}", id);
|
||||
|
||||
BizContestRegistration registration = getById(id);
|
||||
if (registration == null) {
|
||||
throw BusinessException.of(ErrorCode.NOT_FOUND, "报名记录不存在");
|
||||
}
|
||||
|
||||
Map<String, Object> result = registrationToMap(registration);
|
||||
|
||||
// 查询用户详情
|
||||
if (registration.getUserId() != null) {
|
||||
SysUser user = sysUserMapper.selectById(registration.getUserId());
|
||||
if (user != null) {
|
||||
Map<String, Object> userInfo = new LinkedHashMap<>();
|
||||
userInfo.put("id", user.getId());
|
||||
userInfo.put("username", user.getUsername());
|
||||
userInfo.put("nickname", user.getNickname());
|
||||
userInfo.put("phone", user.getPhone());
|
||||
userInfo.put("email", user.getEmail());
|
||||
userInfo.put("avatar", user.getAvatar());
|
||||
result.put("userInfo", userInfo);
|
||||
}
|
||||
}
|
||||
|
||||
// 查询指导教师列表
|
||||
LambdaQueryWrapper<BizContestRegistrationTeacher> teacherWrapper = new LambdaQueryWrapper<>();
|
||||
teacherWrapper.eq(BizContestRegistrationTeacher::getRegistrationId, id);
|
||||
List<BizContestRegistrationTeacher> teachers = contestRegistrationTeacherMapper.selectList(teacherWrapper);
|
||||
result.put("teachers", teachers);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getMyRegistration(Long contestId, Long userId, Long tenantId) {
|
||||
log.info("查询我的报名,活动ID:{},用户ID:{}", contestId, userId);
|
||||
|
||||
LambdaQueryWrapper<BizContestRegistration> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(BizContestRegistration::getContestId, contestId);
|
||||
wrapper.eq(BizContestRegistration::getUserId, userId);
|
||||
wrapper.last("LIMIT 1");
|
||||
|
||||
BizContestRegistration registration = getOne(wrapper, false);
|
||||
if (registration == null) {
|
||||
return null;
|
||||
}
|
||||
return registrationToMap(registration);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reviewRegistration(Long id, String state, String reason, Long operatorId, Long tenantId) {
|
||||
log.info("审核报名,ID:{},状态:{}", id, state);
|
||||
|
||||
BizContestRegistration registration = getById(id);
|
||||
if (registration == null) {
|
||||
throw BusinessException.of(ErrorCode.NOT_FOUND, "报名记录不存在");
|
||||
}
|
||||
|
||||
registration.setRegistrationState(state);
|
||||
registration.setReason(reason);
|
||||
registration.setOperator(operatorId != null ? operatorId.intValue() : null);
|
||||
registration.setOperationDate(LocalDateTime.now());
|
||||
updateById(registration);
|
||||
|
||||
log.info("报名审核完成,ID:{},结果:{}", id, state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void revokeReview(Long id, Long tenantId) {
|
||||
log.info("撤回审核,ID:{}", id);
|
||||
|
||||
BizContestRegistration registration = getById(id);
|
||||
if (registration == null) {
|
||||
throw BusinessException.of(ErrorCode.NOT_FOUND, "报名记录不存在");
|
||||
}
|
||||
|
||||
registration.setRegistrationState(RegistrationStatus.PENDING.getValue());
|
||||
registration.setReason(null);
|
||||
registration.setOperator(null);
|
||||
registration.setOperationDate(null);
|
||||
updateById(registration);
|
||||
|
||||
log.info("审核已撤回,ID:{}", id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void batchReview(List<Long> ids, String state, String reason, Long operatorId, Long tenantId) {
|
||||
log.info("批量审核报名,数量:{},状态:{}", ids.size(), state);
|
||||
|
||||
for (Long id : ids) {
|
||||
reviewRegistration(id, state, reason, operatorId, tenantId);
|
||||
}
|
||||
|
||||
log.info("批量审核完成,共处理 {} 条", ids.size());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addTeacher(Long registrationId, Long teacherUserId, Long tenantId, Long creatorId) {
|
||||
log.info("添加指导教师,报名ID:{},教师用户ID:{}", registrationId, teacherUserId);
|
||||
|
||||
// 检查重复
|
||||
LambdaQueryWrapper<BizContestRegistrationTeacher> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(BizContestRegistrationTeacher::getRegistrationId, registrationId);
|
||||
wrapper.eq(BizContestRegistrationTeacher::getUserId, teacherUserId);
|
||||
Long existCount = contestRegistrationTeacherMapper.selectCount(wrapper);
|
||||
if (existCount > 0) {
|
||||
throw BusinessException.of(ErrorCode.CONFLICT, "该教师已关联此报名");
|
||||
}
|
||||
|
||||
BizContestRegistrationTeacher teacher = new BizContestRegistrationTeacher();
|
||||
teacher.setRegistrationId(registrationId);
|
||||
teacher.setTenantId(tenantId);
|
||||
teacher.setUserId(teacherUserId);
|
||||
teacher.setIsDefault(false);
|
||||
teacher.setCreator(creatorId != null ? creatorId.intValue() : null);
|
||||
teacher.setCreateTime(LocalDateTime.now());
|
||||
contestRegistrationTeacherMapper.insert(teacher);
|
||||
|
||||
log.info("指导教师添加成功,报名ID:{},教师用户ID:{}", registrationId, teacherUserId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeTeacher(Long registrationId, Long teacherUserId, Long tenantId) {
|
||||
log.info("移除指导教师,报名ID:{},教师用户ID:{}", registrationId, teacherUserId);
|
||||
|
||||
LambdaQueryWrapper<BizContestRegistrationTeacher> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(BizContestRegistrationTeacher::getRegistrationId, registrationId);
|
||||
wrapper.eq(BizContestRegistrationTeacher::getUserId, teacherUserId);
|
||||
contestRegistrationTeacherMapper.delete(wrapper);
|
||||
|
||||
log.info("指导教师移除成功,报名ID:{},教师用户ID:{}", registrationId, teacherUserId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeRegistration(Long id, Long tenantId) {
|
||||
log.info("删除报名,ID:{}", id);
|
||||
removeById(id);
|
||||
log.info("报名删除成功,ID:{}", id);
|
||||
}
|
||||
|
||||
// ====== 私有辅助方法 ======
|
||||
|
||||
private Map<String, Object> registrationToMap(BizContestRegistration entity) {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("id", entity.getId());
|
||||
map.put("contestId", entity.getContestId());
|
||||
map.put("tenantId", entity.getTenantId());
|
||||
map.put("registrationType", entity.getRegistrationType());
|
||||
map.put("teamId", entity.getTeamId());
|
||||
map.put("teamName", entity.getTeamName());
|
||||
map.put("userId", entity.getUserId());
|
||||
map.put("accountNo", entity.getAccountNo());
|
||||
map.put("accountName", entity.getAccountName());
|
||||
map.put("role", entity.getRole());
|
||||
map.put("registrationState", entity.getRegistrationState());
|
||||
map.put("participantType", entity.getParticipantType());
|
||||
map.put("childId", entity.getChildId());
|
||||
map.put("registrant", entity.getRegistrant());
|
||||
map.put("registrationTime", entity.getRegistrationTime());
|
||||
map.put("reason", entity.getReason());
|
||||
map.put("operator", entity.getOperator());
|
||||
map.put("operationDate", entity.getOperationDate());
|
||||
map.put("createTime", entity.getCreateTime());
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,637 @@
|
||||
package com.competition.modules.biz.contest.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.competition.common.enums.ErrorCode;
|
||||
import com.competition.common.enums.PublishStatus;
|
||||
import com.competition.common.enums.SubmitRule;
|
||||
import com.competition.common.exception.BusinessException;
|
||||
import com.competition.common.result.PageResult;
|
||||
import com.competition.common.util.SecurityUtil;
|
||||
import com.competition.modules.biz.contest.dto.CreateContestDto;
|
||||
import com.competition.modules.biz.contest.dto.QueryContestDto;
|
||||
import com.competition.modules.biz.contest.entity.BizContest;
|
||||
import com.competition.modules.biz.contest.entity.BizContestAttachment;
|
||||
import com.competition.modules.biz.contest.entity.BizContestRegistration;
|
||||
import com.competition.modules.biz.contest.entity.BizContestWork;
|
||||
import com.competition.modules.biz.contest.mapper.ContestAttachmentMapper;
|
||||
import com.competition.modules.biz.contest.mapper.ContestMapper;
|
||||
import com.competition.modules.biz.contest.mapper.ContestRegistrationMapper;
|
||||
import com.competition.modules.biz.contest.mapper.ContestWorkMapper;
|
||||
import com.competition.modules.biz.contest.service.IContestService;
|
||||
import com.competition.modules.biz.review.entity.BizContestWorkJudgeAssignment;
|
||||
import com.competition.modules.biz.review.mapper.ContestWorkJudgeAssignmentMapper;
|
||||
import com.competition.modules.sys.entity.SysTenant;
|
||||
import com.competition.modules.sys.mapper.SysTenantMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ContestServiceImpl extends ServiceImpl<ContestMapper, BizContest> implements IContestService {
|
||||
|
||||
private final ContestMapper contestMapper;
|
||||
private final ContestAttachmentMapper contestAttachmentMapper;
|
||||
private final ContestRegistrationMapper contestRegistrationMapper;
|
||||
private final ContestWorkMapper contestWorkMapper;
|
||||
private final ContestWorkJudgeAssignmentMapper contestWorkJudgeAssignmentMapper;
|
||||
private final SysTenantMapper sysTenantMapper;
|
||||
|
||||
// 支持两种日期格式:ISO 格式 (T 分隔) 和空格分隔格式
|
||||
private static final DateTimeFormatter ISO_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
|
||||
private static final DateTimeFormatter SPACE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
/**
|
||||
* 解析日期时间字符串,兼容 ISO 格式和空格分隔格式
|
||||
*/
|
||||
private LocalDateTime parseDateTime(String dateTime) {
|
||||
if (!StringUtils.hasText(dateTime)) {
|
||||
return null;
|
||||
}
|
||||
// 尝试 ISO 格式 (yyyy-MM-ddTHH:mm:ss)
|
||||
try {
|
||||
return LocalDateTime.parse(dateTime, ISO_FORMATTER);
|
||||
} catch (Exception e) {
|
||||
// 尝试空格分隔格式 (yyyy-MM-dd HH:mm:ss)
|
||||
try {
|
||||
return LocalDateTime.parse(dateTime, SPACE_FORMATTER);
|
||||
} catch (Exception ex) {
|
||||
log.warn("日期格式解析失败:{}", dateTime, ex);
|
||||
throw new BusinessException(ErrorCode.BAD_REQUEST, "日期格式无效:" + dateTime + ",请使用 yyyy-MM-ddTHH:mm:ss 或 yyyy-MM-dd HH:mm:ss 格式");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public BizContest createContest(CreateContestDto dto, Long creatorId) {
|
||||
log.info("开始创建活动,名称:{}", dto.getContestName());
|
||||
|
||||
try {
|
||||
BizContest entity = new BizContest();
|
||||
mapDtoToEntity(dto, entity);
|
||||
|
||||
// 默认状态
|
||||
entity.setContestState(PublishStatus.UNPUBLISHED.getValue());
|
||||
entity.setStatus("ongoing");
|
||||
entity.setResultState(PublishStatus.UNPUBLISHED.getValue());
|
||||
if (!StringUtils.hasText(entity.getSubmitRule())) {
|
||||
entity.setSubmitRule(SubmitRule.ONCE.getValue());
|
||||
}
|
||||
entity.setCreator(creatorId != null ? creatorId.intValue() : null);
|
||||
|
||||
// 如果没有设置授权租户,默认添加当前租户
|
||||
if (entity.getContestTenants() == null || entity.getContestTenants().isEmpty()) {
|
||||
Long currentTenantId = SecurityUtil.getCurrentTenantId();
|
||||
if (currentTenantId != null) {
|
||||
entity.setContestTenants(Collections.singletonList(currentTenantId.intValue()));
|
||||
}
|
||||
}
|
||||
|
||||
save(entity);
|
||||
log.info("活动创建成功,ID:{}, 名称:{}", entity.getId(), entity.getContestName());
|
||||
return entity;
|
||||
} catch (Exception e) {
|
||||
log.error("创建活动失败,名称:{}", dto.getContestName(), e);
|
||||
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "创建活动失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<Map<String, Object>> findAll(QueryContestDto dto, Long tenantId, boolean isSuperTenant) {
|
||||
log.info("查询活动列表,页码:{},每页:{}", dto.getPage(), dto.getPageSize());
|
||||
|
||||
LambdaQueryWrapper<BizContest> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(BizContest::getValidState, 1);
|
||||
|
||||
if (StringUtils.hasText(dto.getContestName())) {
|
||||
wrapper.like(BizContest::getContestName, dto.getContestName());
|
||||
}
|
||||
if (StringUtils.hasText(dto.getContestState())) {
|
||||
wrapper.eq(BizContest::getContestState, dto.getContestState());
|
||||
}
|
||||
if (StringUtils.hasText(dto.getStatus())) {
|
||||
wrapper.eq(BizContest::getStatus, dto.getStatus());
|
||||
}
|
||||
if (StringUtils.hasText(dto.getVisibility())) {
|
||||
wrapper.eq(BizContest::getVisibility, dto.getVisibility());
|
||||
}
|
||||
if (StringUtils.hasText(dto.getContestType())) {
|
||||
wrapper.eq(BizContest::getContestType, dto.getContestType());
|
||||
}
|
||||
|
||||
// 阶段筛选(与前端活动列表「活动阶段」一致:unpublished/finished/registering/submitting/reviewing)
|
||||
if (StringUtils.hasText(dto.getStage())) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
switch (dto.getStage()) {
|
||||
case "unpublished":
|
||||
wrapper.eq(BizContest::getContestState, PublishStatus.UNPUBLISHED.getValue());
|
||||
break;
|
||||
case "finished":
|
||||
wrapper.eq(BizContest::getStatus, "finished");
|
||||
break;
|
||||
case "registering":
|
||||
wrapper.eq(BizContest::getContestState, PublishStatus.PUBLISHED.getValue());
|
||||
wrapper.le(BizContest::getRegisterStartTime, now)
|
||||
.ge(BizContest::getRegisterEndTime, now);
|
||||
break;
|
||||
case "submitting":
|
||||
wrapper.eq(BizContest::getContestState, PublishStatus.PUBLISHED.getValue());
|
||||
wrapper.le(BizContest::getSubmitStartTime, now)
|
||||
.ge(BizContest::getSubmitEndTime, now);
|
||||
break;
|
||||
case "reviewing":
|
||||
wrapper.eq(BizContest::getContestState, PublishStatus.PUBLISHED.getValue());
|
||||
wrapper.le(BizContest::getReviewStartTime, now)
|
||||
.ge(BizContest::getReviewEndTime, now);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 非超级租户按授权租户过滤
|
||||
if (!isSuperTenant && tenantId != null) {
|
||||
wrapper.apply("JSON_CONTAINS(contest_tenants, CAST({0} AS JSON))", tenantId);
|
||||
}
|
||||
|
||||
wrapper.orderByDesc(BizContest::getCreateTime);
|
||||
|
||||
Page<BizContest> page = new Page<>(dto.getPage(), dto.getPageSize());
|
||||
Page<BizContest> result = contestMapper.selectPage(page, wrapper);
|
||||
|
||||
// 批量查询报名数和作品数
|
||||
List<Long> contestIds = result.getRecords().stream()
|
||||
.map(BizContest::getId).toList();
|
||||
|
||||
Map<Long, Long> registrationCountMap = new HashMap<>();
|
||||
Map<Long, Long> workCountMap = new HashMap<>();
|
||||
Map<Long, Long> reviewedWorkCountMap = new HashMap<>();
|
||||
if (!contestIds.isEmpty()) {
|
||||
// 报名数(所有状态)
|
||||
contestRegistrationMapper.selectList(
|
||||
new LambdaQueryWrapper<BizContestRegistration>()
|
||||
.in(BizContestRegistration::getContestId, contestIds))
|
||||
.stream()
|
||||
.collect(Collectors.groupingBy(BizContestRegistration::getContestId, Collectors.counting()))
|
||||
.forEach(registrationCountMap::put);
|
||||
|
||||
// 作品:最新有效版本;评审完成数 = 已分配且该作品全部分配记录均为 completed(与评委端、ProgressDetail 一致)
|
||||
List<BizContestWork> contestWorks = contestWorkMapper.selectList(
|
||||
new LambdaQueryWrapper<BizContestWork>()
|
||||
.in(BizContestWork::getContestId, contestIds)
|
||||
.eq(BizContestWork::getIsLatest, true)
|
||||
.eq(BizContestWork::getValidState, 1));
|
||||
Set<Long> workIdSet = contestWorks.stream().map(BizContestWork::getId).collect(Collectors.toSet());
|
||||
Map<Long, List<BizContestWorkJudgeAssignment>> assignByWorkId = new HashMap<>();
|
||||
if (!workIdSet.isEmpty()) {
|
||||
List<BizContestWorkJudgeAssignment> allAssign = contestWorkJudgeAssignmentMapper.selectList(
|
||||
new LambdaQueryWrapper<BizContestWorkJudgeAssignment>()
|
||||
.in(BizContestWorkJudgeAssignment::getWorkId, workIdSet));
|
||||
assignByWorkId = allAssign.stream()
|
||||
.collect(Collectors.groupingBy(BizContestWorkJudgeAssignment::getWorkId));
|
||||
}
|
||||
for (BizContestWork w : contestWorks) {
|
||||
Long cid = w.getContestId();
|
||||
workCountMap.merge(cid, 1L, Long::sum);
|
||||
List<BizContestWorkJudgeAssignment> assigns = assignByWorkId.getOrDefault(w.getId(), Collections.emptyList());
|
||||
if (!assigns.isEmpty() && assigns.stream().allMatch(a -> "completed".equals(a.getStatus()))) {
|
||||
reviewedWorkCountMap.merge(cid, 1L, Long::sum);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<Map<String, Object>> voList = result.getRecords().stream()
|
||||
.map(entity -> {
|
||||
Map<String, Object> map = entityToMap(entity);
|
||||
Map<String, Object> countMap = new LinkedHashMap<>();
|
||||
long works = workCountMap.getOrDefault(entity.getId(), 0L);
|
||||
long reviewedWorks = reviewedWorkCountMap.getOrDefault(entity.getId(), 0L);
|
||||
countMap.put("registrations", registrationCountMap.getOrDefault(entity.getId(), 0L));
|
||||
countMap.put("works", works);
|
||||
map.put("_count", countMap);
|
||||
map.put("totalWorksCount", works);
|
||||
map.put("reviewedCount", reviewedWorks);
|
||||
return map;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return PageResult.from(result, voList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<Map<String, Object>> getMyContests(QueryContestDto dto, Long userId, Long tenantId) {
|
||||
log.info("查询我的活动,用户ID:{},租户ID:{}", userId, tenantId);
|
||||
|
||||
LambdaQueryWrapper<BizContest> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(BizContest::getValidState, 1);
|
||||
wrapper.eq(BizContest::getContestState, PublishStatus.PUBLISHED.getValue());
|
||||
|
||||
if (StringUtils.hasText(dto.getContestName())) {
|
||||
wrapper.like(BizContest::getContestName, dto.getContestName());
|
||||
}
|
||||
|
||||
// 按租户过滤
|
||||
if (tenantId != null) {
|
||||
wrapper.apply("JSON_CONTAINS(contest_tenants, CAST({0} AS JSON))", tenantId);
|
||||
}
|
||||
|
||||
wrapper.orderByDesc(BizContest::getCreateTime);
|
||||
|
||||
Page<BizContest> page = new Page<>(dto.getPage(), dto.getPageSize());
|
||||
Page<BizContest> result = contestMapper.selectPage(page, wrapper);
|
||||
|
||||
List<Map<String, Object>> voList = result.getRecords().stream()
|
||||
.map(this::entityToMap)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return PageResult.from(result, voList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> findDetail(Long id) {
|
||||
log.info("查询活动详情,ID:{}", id);
|
||||
|
||||
BizContest contest = getById(id);
|
||||
if (contest == null) {
|
||||
throw BusinessException.of(ErrorCode.NOT_FOUND, "活动不存在");
|
||||
}
|
||||
|
||||
Map<String, Object> result = entityToMap(contest);
|
||||
|
||||
// 查询附件列表
|
||||
LambdaQueryWrapper<BizContestAttachment> attWrapper = new LambdaQueryWrapper<>();
|
||||
attWrapper.eq(BizContestAttachment::getContestId, id);
|
||||
List<BizContestAttachment> attachments = contestAttachmentMapper.selectList(attWrapper);
|
||||
result.put("attachments", attachments);
|
||||
|
||||
// 查询授权租户的详细信息
|
||||
List<Integer> tenantIds = contest.getContestTenants();
|
||||
if (tenantIds != null && !tenantIds.isEmpty()) {
|
||||
List<Map<String, Object>> tenantInfoList = new ArrayList<>();
|
||||
for (Integer tenantId : tenantIds) {
|
||||
SysTenant tenant = sysTenantMapper.selectById(tenantId.longValue());
|
||||
if (tenant != null) {
|
||||
Map<String, Object> tenantInfo = new LinkedHashMap<>();
|
||||
tenantInfo.put("id", tenant.getId());
|
||||
tenantInfo.put("name", tenant.getName());
|
||||
tenantInfo.put("code", tenant.getCode());
|
||||
tenantInfo.put("tenantType", tenant.getTenantType());
|
||||
tenantInfoList.add(tenantInfo);
|
||||
}
|
||||
}
|
||||
result.put("contestTenantInfos", tenantInfoList);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BizContest updateContest(Long id, CreateContestDto dto) {
|
||||
log.info("更新活动,ID:{}", id);
|
||||
|
||||
BizContest entity = getById(id);
|
||||
if (entity == null) {
|
||||
throw BusinessException.of(ErrorCode.NOT_FOUND, "活动不存在");
|
||||
}
|
||||
|
||||
// 已发布的活动限制编辑,需先撤回
|
||||
if (PublishStatus.PUBLISHED.getValue().equals(entity.getContestState())) {
|
||||
throw BusinessException.of(ErrorCode.BAD_REQUEST, "活动已发布,请先撤回后再编辑");
|
||||
}
|
||||
|
||||
mapDtoToEntity(dto, entity);
|
||||
updateById(entity);
|
||||
|
||||
log.info("活动更新成功,ID:{}", id);
|
||||
return entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publishContest(Long id, String contestState) {
|
||||
log.info("发布/撤回活动,ID:{},状态:{}", id, contestState);
|
||||
|
||||
BizContest entity = getById(id);
|
||||
if (entity == null) {
|
||||
throw BusinessException.of(ErrorCode.NOT_FOUND, "活动不存在");
|
||||
}
|
||||
|
||||
// 发布时校验关键时间字段的完整性和合理性
|
||||
if (PublishStatus.PUBLISHED.getValue().equals(contestState)) {
|
||||
validateContestTimes(entity);
|
||||
}
|
||||
|
||||
entity.setContestState(contestState);
|
||||
updateById(entity);
|
||||
log.info("活动状态更新成功,ID:{},新状态:{}", id, contestState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finishContest(Long id) {
|
||||
log.info("结束活动,ID:{}", id);
|
||||
|
||||
BizContest entity = getById(id);
|
||||
if (entity == null) {
|
||||
throw BusinessException.of(ErrorCode.NOT_FOUND, "活动不存在");
|
||||
}
|
||||
|
||||
// 检查是否有未完成的评审任务
|
||||
LambdaQueryWrapper<BizContestWorkJudgeAssignment> pendingWrapper = new LambdaQueryWrapper<>();
|
||||
pendingWrapper.eq(BizContestWorkJudgeAssignment::getContestId, id);
|
||||
pendingWrapper.ne(BizContestWorkJudgeAssignment::getStatus, "completed");
|
||||
long pendingCount = contestWorkJudgeAssignmentMapper.selectCount(pendingWrapper);
|
||||
if (pendingCount > 0) {
|
||||
throw BusinessException.of(ErrorCode.BAD_REQUEST,
|
||||
String.format("还有 %d 个评审任务未完成,无法结束活动", pendingCount));
|
||||
}
|
||||
|
||||
entity.setStatus("finished");
|
||||
updateById(entity);
|
||||
log.info("活动已结束,ID:{}", id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验活动时间合理性
|
||||
*/
|
||||
private void validateContestTimes(BizContest entity) {
|
||||
// 检查报名时间是否完整
|
||||
if (entity.getRegisterStartTime() == null || entity.getRegisterEndTime() == null) {
|
||||
throw BusinessException.of(ErrorCode.BAD_REQUEST, "请先设置完整的报名时间");
|
||||
}
|
||||
// 检查时间顺序:报名开始 < 报名结束
|
||||
if (entity.getRegisterStartTime().isAfter(entity.getRegisterEndTime())) {
|
||||
throw BusinessException.of(ErrorCode.BAD_REQUEST, "报名开始时间不能晚于报名结束时间");
|
||||
}
|
||||
// 检查提交时间
|
||||
if (entity.getSubmitStartTime() != null && entity.getSubmitEndTime() != null) {
|
||||
if (entity.getSubmitStartTime().isAfter(entity.getSubmitEndTime())) {
|
||||
throw BusinessException.of(ErrorCode.BAD_REQUEST, "提交开始时间不能晚于提交结束时间");
|
||||
}
|
||||
// 提交开始应该在报名结束之后或同时
|
||||
if (entity.getSubmitStartTime().isBefore(entity.getRegisterEndTime())) {
|
||||
log.warn("提交开始时间早于报名结束时间,允许报名与提交重叠");
|
||||
}
|
||||
}
|
||||
// 检查评审时间
|
||||
if (entity.getReviewStartTime() != null && entity.getReviewEndTime() != null) {
|
||||
if (entity.getReviewStartTime().isAfter(entity.getReviewEndTime())) {
|
||||
throw BusinessException.of(ErrorCode.BAD_REQUEST, "评审开始时间不能晚于评审结束时间");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reopenContest(Long id) {
|
||||
log.info("重新开启活动,ID:{}", id);
|
||||
|
||||
BizContest entity = getById(id);
|
||||
if (entity == null) {
|
||||
throw BusinessException.of(ErrorCode.NOT_FOUND, "活动不存在");
|
||||
}
|
||||
|
||||
entity.setStatus("ongoing");
|
||||
updateById(entity);
|
||||
log.info("活动已重新开启,ID:{}", id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeContest(Long id) {
|
||||
log.info("删除活动,ID:{}", id);
|
||||
removeById(id);
|
||||
log.info("活动删除成功,ID:{}", id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getStats(Long tenantId, boolean isSuperTenant) {
|
||||
log.info("获取活动统计,租户ID:{}", tenantId);
|
||||
|
||||
LambdaQueryWrapper<BizContest> baseWrapper = new LambdaQueryWrapper<>();
|
||||
baseWrapper.eq(BizContest::getValidState, 1);
|
||||
if (!isSuperTenant && tenantId != null) {
|
||||
baseWrapper.apply("JSON_CONTAINS(contest_tenants, CAST({0} AS JSON))", tenantId);
|
||||
}
|
||||
|
||||
long total = count(baseWrapper);
|
||||
|
||||
LambdaQueryWrapper<BizContest> publishedWrapper = new LambdaQueryWrapper<>();
|
||||
publishedWrapper.eq(BizContest::getValidState, 1);
|
||||
publishedWrapper.eq(BizContest::getContestState, PublishStatus.PUBLISHED.getValue());
|
||||
if (!isSuperTenant && tenantId != null) {
|
||||
publishedWrapper.apply("JSON_CONTAINS(contest_tenants, CAST({0} AS JSON))", tenantId);
|
||||
}
|
||||
long published = count(publishedWrapper);
|
||||
|
||||
LambdaQueryWrapper<BizContest> ongoingWrapper = new LambdaQueryWrapper<>();
|
||||
ongoingWrapper.eq(BizContest::getValidState, 1);
|
||||
ongoingWrapper.eq(BizContest::getStatus, "ongoing");
|
||||
if (!isSuperTenant && tenantId != null) {
|
||||
ongoingWrapper.apply("JSON_CONTAINS(contest_tenants, CAST({0} AS JSON))", tenantId);
|
||||
}
|
||||
long ongoing = count(ongoingWrapper);
|
||||
|
||||
LambdaQueryWrapper<BizContest> finishedWrapper = new LambdaQueryWrapper<>();
|
||||
finishedWrapper.eq(BizContest::getValidState, 1);
|
||||
finishedWrapper.eq(BizContest::getStatus, "finished");
|
||||
if (!isSuperTenant && tenantId != null) {
|
||||
finishedWrapper.apply("JSON_CONTAINS(contest_tenants, CAST({0} AS JSON))", tenantId);
|
||||
}
|
||||
long finished = count(finishedWrapper);
|
||||
|
||||
Map<String, Object> stats = new LinkedHashMap<>();
|
||||
stats.put("total", total);
|
||||
stats.put("published", published);
|
||||
stats.put("ongoing", ongoing);
|
||||
stats.put("finished", finished);
|
||||
return stats;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getDashboard(Long tenantId) {
|
||||
log.info("获取仪表盘数据,租户ID:{}", tenantId);
|
||||
|
||||
LambdaQueryWrapper<BizContest> contestWrapper = new LambdaQueryWrapper<>();
|
||||
contestWrapper.eq(BizContest::getValidState, 1);
|
||||
if (tenantId != null) {
|
||||
contestWrapper.apply("JSON_CONTAINS(contest_tenants, CAST({0} AS JSON))", tenantId);
|
||||
}
|
||||
long totalContests = count(contestWrapper);
|
||||
|
||||
long totalRegistrations = contestRegistrationMapper.selectCount(new LambdaQueryWrapper<BizContestRegistration>());
|
||||
|
||||
long totalWorks = contestWorkMapper.selectCount(new LambdaQueryWrapper<BizContestWork>());
|
||||
|
||||
Map<String, Object> dashboard = new LinkedHashMap<>();
|
||||
dashboard.put("totalContests", totalContests);
|
||||
dashboard.put("totalRegistrations", totalRegistrations);
|
||||
dashboard.put("totalWorks", totalWorks);
|
||||
return dashboard;
|
||||
}
|
||||
|
||||
// ====== 私有辅助方法 ======
|
||||
|
||||
private void mapDtoToEntity(CreateContestDto dto, BizContest entity) {
|
||||
if (StringUtils.hasText(dto.getContestName())) {
|
||||
entity.setContestName(dto.getContestName());
|
||||
}
|
||||
if (StringUtils.hasText(dto.getContestType())) {
|
||||
entity.setContestType(dto.getContestType());
|
||||
}
|
||||
if (StringUtils.hasText(dto.getVisibility())) {
|
||||
entity.setVisibility(dto.getVisibility());
|
||||
}
|
||||
if (dto.getTargetCities() != null) {
|
||||
entity.setTargetCities(dto.getTargetCities());
|
||||
}
|
||||
if (dto.getAgeMin() != null) {
|
||||
entity.setAgeMin(dto.getAgeMin());
|
||||
}
|
||||
if (dto.getAgeMax() != null) {
|
||||
entity.setAgeMax(dto.getAgeMax());
|
||||
}
|
||||
if (StringUtils.hasText(dto.getStartTime())) {
|
||||
entity.setStartTime(parseDateTime(dto.getStartTime()));
|
||||
}
|
||||
if (StringUtils.hasText(dto.getEndTime())) {
|
||||
entity.setEndTime(parseDateTime(dto.getEndTime()));
|
||||
}
|
||||
if (StringUtils.hasText(dto.getAddress())) {
|
||||
entity.setAddress(dto.getAddress());
|
||||
}
|
||||
if (dto.getContent() != null) {
|
||||
entity.setContent(dto.getContent());
|
||||
}
|
||||
if (dto.getContestTenants() != null) {
|
||||
entity.setContestTenants(dto.getContestTenants());
|
||||
}
|
||||
if (StringUtils.hasText(dto.getCoverUrl())) {
|
||||
entity.setCoverUrl(dto.getCoverUrl());
|
||||
}
|
||||
if (StringUtils.hasText(dto.getPosterUrl())) {
|
||||
entity.setPosterUrl(dto.getPosterUrl());
|
||||
}
|
||||
if (StringUtils.hasText(dto.getContactName())) {
|
||||
entity.setContactName(dto.getContactName());
|
||||
}
|
||||
if (StringUtils.hasText(dto.getContactPhone())) {
|
||||
entity.setContactPhone(dto.getContactPhone());
|
||||
}
|
||||
if (StringUtils.hasText(dto.getContactQrcode())) {
|
||||
entity.setContactQrcode(dto.getContactQrcode());
|
||||
}
|
||||
if (dto.getOrganizers() != null) {
|
||||
entity.setOrganizers(dto.getOrganizers());
|
||||
}
|
||||
if (dto.getCoOrganizers() != null) {
|
||||
entity.setCoOrganizers(dto.getCoOrganizers());
|
||||
}
|
||||
if (dto.getSponsors() != null) {
|
||||
entity.setSponsors(dto.getSponsors());
|
||||
}
|
||||
if (StringUtils.hasText(dto.getRegisterStartTime())) {
|
||||
entity.setRegisterStartTime(parseDateTime(dto.getRegisterStartTime()));
|
||||
}
|
||||
if (StringUtils.hasText(dto.getRegisterEndTime())) {
|
||||
entity.setRegisterEndTime(parseDateTime(dto.getRegisterEndTime()));
|
||||
}
|
||||
if (StringUtils.hasText(dto.getRegisterState())) {
|
||||
entity.setRegisterState(dto.getRegisterState());
|
||||
}
|
||||
if (dto.getRequireAudit() != null) {
|
||||
entity.setRequireAudit(dto.getRequireAudit());
|
||||
}
|
||||
if (dto.getAllowedGrades() != null) {
|
||||
entity.setAllowedGrades(dto.getAllowedGrades());
|
||||
}
|
||||
if (dto.getAllowedClasses() != null) {
|
||||
entity.setAllowedClasses(dto.getAllowedClasses());
|
||||
}
|
||||
if (dto.getTeamMinMembers() != null) {
|
||||
entity.setTeamMinMembers(dto.getTeamMinMembers());
|
||||
}
|
||||
if (dto.getTeamMaxMembers() != null) {
|
||||
entity.setTeamMaxMembers(dto.getTeamMaxMembers());
|
||||
}
|
||||
if (StringUtils.hasText(dto.getSubmitRule())) {
|
||||
entity.setSubmitRule(dto.getSubmitRule());
|
||||
}
|
||||
if (StringUtils.hasText(dto.getSubmitStartTime())) {
|
||||
entity.setSubmitStartTime(parseDateTime(dto.getSubmitStartTime()));
|
||||
}
|
||||
if (StringUtils.hasText(dto.getSubmitEndTime())) {
|
||||
entity.setSubmitEndTime(parseDateTime(dto.getSubmitEndTime()));
|
||||
}
|
||||
if (StringUtils.hasText(dto.getWorkType())) {
|
||||
entity.setWorkType(dto.getWorkType());
|
||||
}
|
||||
if (dto.getWorkRequirement() != null) {
|
||||
entity.setWorkRequirement(dto.getWorkRequirement());
|
||||
}
|
||||
if (dto.getReviewRuleId() != null) {
|
||||
entity.setReviewRuleId(dto.getReviewRuleId());
|
||||
}
|
||||
if (StringUtils.hasText(dto.getReviewStartTime())) {
|
||||
entity.setReviewStartTime(parseDateTime(dto.getReviewStartTime()));
|
||||
}
|
||||
if (StringUtils.hasText(dto.getReviewEndTime())) {
|
||||
entity.setReviewEndTime(parseDateTime(dto.getReviewEndTime()));
|
||||
}
|
||||
if (StringUtils.hasText(dto.getResultPublishTime())) {
|
||||
entity.setResultPublishTime(parseDateTime(dto.getResultPublishTime()));
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> entityToMap(BizContest entity) {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("id", entity.getId());
|
||||
map.put("contestName", entity.getContestName());
|
||||
map.put("contestType", entity.getContestType());
|
||||
map.put("contestState", entity.getContestState());
|
||||
map.put("status", entity.getStatus());
|
||||
map.put("visibility", entity.getVisibility());
|
||||
map.put("startTime", entity.getStartTime());
|
||||
map.put("endTime", entity.getEndTime());
|
||||
map.put("address", entity.getAddress());
|
||||
map.put("content", entity.getContent());
|
||||
map.put("contestTenants", entity.getContestTenants());
|
||||
map.put("coverUrl", entity.getCoverUrl());
|
||||
map.put("posterUrl", entity.getPosterUrl());
|
||||
map.put("contactName", entity.getContactName());
|
||||
map.put("contactPhone", entity.getContactPhone());
|
||||
map.put("contactQrcode", entity.getContactQrcode());
|
||||
map.put("organizers", entity.getOrganizers());
|
||||
map.put("coOrganizers", entity.getCoOrganizers());
|
||||
map.put("sponsors", entity.getSponsors());
|
||||
map.put("registerStartTime", entity.getRegisterStartTime());
|
||||
map.put("registerEndTime", entity.getRegisterEndTime());
|
||||
map.put("registerState", entity.getRegisterState());
|
||||
map.put("requireAudit", entity.getRequireAudit());
|
||||
map.put("allowedGrades", entity.getAllowedGrades());
|
||||
map.put("allowedClasses", entity.getAllowedClasses());
|
||||
map.put("teamMinMembers", entity.getTeamMinMembers());
|
||||
map.put("teamMaxMembers", entity.getTeamMaxMembers());
|
||||
map.put("targetCities", entity.getTargetCities());
|
||||
map.put("ageMin", entity.getAgeMin());
|
||||
map.put("ageMax", entity.getAgeMax());
|
||||
map.put("submitRule", entity.getSubmitRule());
|
||||
map.put("submitStartTime", entity.getSubmitStartTime());
|
||||
map.put("submitEndTime", entity.getSubmitEndTime());
|
||||
map.put("workType", entity.getWorkType());
|
||||
map.put("workRequirement", entity.getWorkRequirement());
|
||||
map.put("reviewRuleId", entity.getReviewRuleId());
|
||||
map.put("reviewStartTime", entity.getReviewStartTime());
|
||||
map.put("reviewEndTime", entity.getReviewEndTime());
|
||||
map.put("resultState", entity.getResultState());
|
||||
map.put("resultPublishTime", entity.getResultPublishTime());
|
||||
map.put("createTime", entity.getCreateTime());
|
||||
map.put("modifyTime", entity.getModifyTime());
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,215 @@
|
||||
package com.competition.modules.biz.contest.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.competition.common.enums.ErrorCode;
|
||||
import com.competition.common.exception.BusinessException;
|
||||
import com.competition.modules.biz.contest.dto.CreateTeamDto;
|
||||
import com.competition.modules.biz.contest.entity.BizContestTeam;
|
||||
import com.competition.modules.biz.contest.entity.BizContestTeamMember;
|
||||
import com.competition.modules.biz.contest.mapper.ContestTeamMapper;
|
||||
import com.competition.modules.biz.contest.mapper.ContestTeamMemberMapper;
|
||||
import com.competition.modules.biz.contest.service.IContestTeamService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ContestTeamServiceImpl extends ServiceImpl<ContestTeamMapper, BizContestTeam>
|
||||
implements IContestTeamService {
|
||||
|
||||
private final ContestTeamMapper contestTeamMapper;
|
||||
private final ContestTeamMemberMapper contestTeamMemberMapper;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Map<String, Object> createTeam(CreateTeamDto dto, Long tenantId, Long creatorId) {
|
||||
log.info("开始创建团队,活动ID:{},团队名称:{}", dto.getContestId(), dto.getTeamName());
|
||||
|
||||
BizContestTeam team = new BizContestTeam();
|
||||
team.setTenantId(tenantId);
|
||||
team.setContestId(dto.getContestId());
|
||||
team.setTeamName(dto.getTeamName());
|
||||
team.setLeaderUserId(dto.getLeaderId());
|
||||
team.setMaxMembers(dto.getMaxMembers());
|
||||
team.setCreator(creatorId != null ? creatorId.intValue() : null);
|
||||
|
||||
save(team);
|
||||
log.info("团队创建成功,ID:{}", team.getId());
|
||||
|
||||
// 添加队长为成员
|
||||
addMemberInternal(team.getId(), dto.getLeaderId(), "leader", tenantId, creatorId);
|
||||
|
||||
// 添加其他成员
|
||||
if (dto.getMemberIds() != null) {
|
||||
for (Long memberId : dto.getMemberIds()) {
|
||||
if (!memberId.equals(dto.getLeaderId())) {
|
||||
addMemberInternal(team.getId(), memberId, "member", tenantId, creatorId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加指导老师
|
||||
if (dto.getTeacherIds() != null) {
|
||||
for (Long teacherId : dto.getTeacherIds()) {
|
||||
addMemberInternal(team.getId(), teacherId, "mentor", tenantId, creatorId);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("团队成员添加完成,团队ID:{}", team.getId());
|
||||
return teamToMap(team, getMemberList(team.getId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> findByContest(Long contestId, Long tenantId) {
|
||||
log.info("查询活动团队列表,活动ID:{}", contestId);
|
||||
|
||||
LambdaQueryWrapper<BizContestTeam> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(BizContestTeam::getContestId, contestId);
|
||||
if (tenantId != null) {
|
||||
wrapper.eq(BizContestTeam::getTenantId, tenantId);
|
||||
}
|
||||
wrapper.orderByDesc(BizContestTeam::getCreateTime);
|
||||
|
||||
List<BizContestTeam> teams = list(wrapper);
|
||||
|
||||
return teams.stream()
|
||||
.map(team -> teamToMap(team, getMemberList(team.getId())))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> findDetail(Long id) {
|
||||
log.info("查询团队详情,ID:{}", id);
|
||||
|
||||
BizContestTeam team = getById(id);
|
||||
if (team == null) {
|
||||
throw BusinessException.of(ErrorCode.NOT_FOUND, "团队不存在");
|
||||
}
|
||||
|
||||
return teamToMap(team, getMemberList(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public BizContestTeam updateTeam(Long id, CreateTeamDto dto) {
|
||||
log.info("更新团队,ID:{}", id);
|
||||
|
||||
BizContestTeam team = getById(id);
|
||||
if (team == null) {
|
||||
throw BusinessException.of(ErrorCode.NOT_FOUND, "团队不存在");
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(dto.getTeamName())) {
|
||||
team.setTeamName(dto.getTeamName());
|
||||
}
|
||||
if (dto.getLeaderId() != null) {
|
||||
team.setLeaderUserId(dto.getLeaderId());
|
||||
}
|
||||
if (dto.getMaxMembers() != null) {
|
||||
team.setMaxMembers(dto.getMaxMembers());
|
||||
}
|
||||
|
||||
updateById(team);
|
||||
log.info("团队更新成功,ID:{}", id);
|
||||
return team;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addMember(Long teamId, Long userId, String role, Long tenantId) {
|
||||
log.info("添加团队成员,团队ID:{},用户ID:{},角色:{}", teamId, userId, role);
|
||||
|
||||
// 检查重复
|
||||
LambdaQueryWrapper<BizContestTeamMember> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(BizContestTeamMember::getTeamId, teamId);
|
||||
wrapper.eq(BizContestTeamMember::getUserId, userId);
|
||||
Long existCount = contestTeamMemberMapper.selectCount(wrapper);
|
||||
if (existCount > 0) {
|
||||
throw BusinessException.of(ErrorCode.CONFLICT, "该用户已在团队中");
|
||||
}
|
||||
|
||||
addMemberInternal(teamId, userId, role, tenantId, null);
|
||||
log.info("团队成员添加成功,团队ID:{},用户ID:{}", teamId, userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeMember(Long teamId, Long userId) {
|
||||
log.info("移除团队成员,团队ID:{},用户ID:{}", teamId, userId);
|
||||
|
||||
LambdaQueryWrapper<BizContestTeamMember> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(BizContestTeamMember::getTeamId, teamId);
|
||||
wrapper.eq(BizContestTeamMember::getUserId, userId);
|
||||
contestTeamMemberMapper.delete(wrapper);
|
||||
|
||||
log.info("团队成员移除成功,团队ID:{},用户ID:{}", teamId, userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeTeam(Long id) {
|
||||
log.info("删除团队,ID:{}", id);
|
||||
|
||||
// 删除团队成员
|
||||
LambdaQueryWrapper<BizContestTeamMember> memberWrapper = new LambdaQueryWrapper<>();
|
||||
memberWrapper.eq(BizContestTeamMember::getTeamId, id);
|
||||
contestTeamMemberMapper.delete(memberWrapper);
|
||||
|
||||
removeById(id);
|
||||
log.info("团队删除成功,ID:{}", id);
|
||||
}
|
||||
|
||||
// ====== 私有辅助方法 ======
|
||||
|
||||
private void addMemberInternal(Long teamId, Long userId, String role, Long tenantId, Long creatorId) {
|
||||
BizContestTeamMember member = new BizContestTeamMember();
|
||||
member.setTeamId(teamId);
|
||||
member.setTenantId(tenantId);
|
||||
member.setUserId(userId);
|
||||
member.setRole(role);
|
||||
member.setCreator(creatorId != null ? creatorId.intValue() : null);
|
||||
member.setCreateTime(LocalDateTime.now());
|
||||
contestTeamMemberMapper.insert(member);
|
||||
}
|
||||
|
||||
private List<BizContestTeamMember> getMemberList(Long teamId) {
|
||||
LambdaQueryWrapper<BizContestTeamMember> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(BizContestTeamMember::getTeamId, teamId);
|
||||
wrapper.orderByAsc(BizContestTeamMember::getCreateTime);
|
||||
return contestTeamMemberMapper.selectList(wrapper);
|
||||
}
|
||||
|
||||
private Map<String, Object> teamToMap(BizContestTeam entity, List<BizContestTeamMember> members) {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("id", entity.getId());
|
||||
map.put("tenantId", entity.getTenantId());
|
||||
map.put("contestId", entity.getContestId());
|
||||
map.put("teamName", entity.getTeamName());
|
||||
map.put("leaderUserId", entity.getLeaderUserId());
|
||||
map.put("maxMembers", entity.getMaxMembers());
|
||||
map.put("createTime", entity.getCreateTime());
|
||||
|
||||
if (members != null) {
|
||||
List<Map<String, Object>> memberList = members.stream()
|
||||
.map(m -> {
|
||||
Map<String, Object> mMap = new LinkedHashMap<>();
|
||||
mMap.put("id", m.getId());
|
||||
mMap.put("teamId", m.getTeamId());
|
||||
mMap.put("userId", m.getUserId());
|
||||
mMap.put("role", m.getRole());
|
||||
mMap.put("createTime", m.getCreateTime());
|
||||
return mMap;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
map.put("members", memberList);
|
||||
map.put("memberCount", memberList.size());
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,745 @@
|
||||
package com.competition.modules.biz.contest.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.competition.common.enums.ErrorCode;
|
||||
import com.competition.common.enums.WorkStatus;
|
||||
import com.competition.common.enums.RegistrationStatus;
|
||||
import com.competition.common.exception.BusinessException;
|
||||
import com.competition.common.result.PageResult;
|
||||
import com.competition.modules.biz.contest.dto.QueryWorkDto;
|
||||
import com.competition.modules.biz.contest.dto.SubmitWorkDto;
|
||||
import com.competition.modules.biz.contest.entity.BizContest;
|
||||
import com.competition.modules.biz.contest.entity.BizContestRegistration;
|
||||
import com.competition.modules.biz.contest.entity.BizContestWork;
|
||||
import com.competition.modules.biz.contest.entity.BizContestWorkAttachment;
|
||||
import com.competition.modules.biz.contest.mapper.ContestMapper;
|
||||
import com.competition.modules.biz.contest.mapper.ContestRegistrationMapper;
|
||||
import com.competition.modules.biz.contest.mapper.ContestWorkAttachmentMapper;
|
||||
import com.competition.modules.biz.contest.mapper.ContestWorkMapper;
|
||||
import com.competition.modules.biz.contest.service.IContestWorkService;
|
||||
import com.competition.modules.biz.review.entity.BizContestJudge;
|
||||
import com.competition.modules.biz.review.entity.BizContestReviewRule;
|
||||
import com.competition.modules.biz.review.entity.BizContestWorkJudgeAssignment;
|
||||
import com.competition.modules.biz.review.entity.BizContestWorkScore;
|
||||
import com.competition.modules.biz.review.mapper.ContestJudgeMapper;
|
||||
import com.competition.modules.biz.review.mapper.ContestReviewRuleMapper;
|
||||
import com.competition.modules.biz.review.mapper.ContestWorkJudgeAssignmentMapper;
|
||||
import com.competition.modules.biz.review.mapper.ContestWorkScoreMapper;
|
||||
import com.competition.modules.biz.review.util.ContestFinalScoreCalculator;
|
||||
import com.competition.modules.sys.entity.SysUser;
|
||||
import com.competition.modules.sys.mapper.SysUserMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ContestWorkServiceImpl extends ServiceImpl<ContestWorkMapper, BizContestWork>
|
||||
implements IContestWorkService {
|
||||
|
||||
private final ContestWorkMapper contestWorkMapper;
|
||||
private final ContestWorkAttachmentMapper contestWorkAttachmentMapper;
|
||||
private final ContestRegistrationMapper contestRegistrationMapper;
|
||||
private final ContestMapper contestMapper;
|
||||
private final ContestWorkJudgeAssignmentMapper assignmentMapper;
|
||||
private final SysUserMapper sysUserMapper;
|
||||
private final ContestWorkScoreMapper contestWorkScoreMapper;
|
||||
private final ContestReviewRuleMapper contestReviewRuleMapper;
|
||||
private final ContestJudgeMapper contestJudgeMapper;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Map<String, Object> submitWork(SubmitWorkDto dto, Long tenantId, Long submitterId) {
|
||||
log.info("开始提交作品,报名ID:{},提交者:{}", dto.getRegistrationId(), submitterId);
|
||||
|
||||
// 验证报名存在且已通过
|
||||
BizContestRegistration registration = contestRegistrationMapper.selectById(dto.getRegistrationId());
|
||||
if (registration == null) {
|
||||
throw BusinessException.of(ErrorCode.NOT_FOUND, "报名记录不存在");
|
||||
}
|
||||
if (!RegistrationStatus.PASSED.getValue().equals(registration.getRegistrationState())) {
|
||||
throw BusinessException.of(ErrorCode.BAD_REQUEST, "报名未通过审核,无法提交作品");
|
||||
}
|
||||
|
||||
// 校验报名归属:只有报名者本人才能提交作品
|
||||
if (registration.getUserId() != null && !registration.getUserId().equals(submitterId)) {
|
||||
log.warn("越权提交尝试:报名用户ID={},提交者ID={},报名ID={}", registration.getUserId(), submitterId, dto.getRegistrationId());
|
||||
throw BusinessException.of(ErrorCode.FORBIDDEN, "无权替他人提交作品");
|
||||
}
|
||||
|
||||
Long contestId = registration.getContestId();
|
||||
|
||||
// 查询活动提交规则
|
||||
BizContest contest = contestMapper.selectById(contestId);
|
||||
String submitRule = contest != null ? contest.getSubmitRule() : "once";
|
||||
|
||||
// 计算版本号
|
||||
int version = 1;
|
||||
if ("resubmit".equals(submitRule)) {
|
||||
// 将旧版本标记为非最新
|
||||
LambdaUpdateWrapper<BizContestWork> updateWrapper = new LambdaUpdateWrapper<>();
|
||||
updateWrapper.eq(BizContestWork::getRegistrationId, dto.getRegistrationId())
|
||||
.eq(BizContestWork::getIsLatest, true)
|
||||
.set(BizContestWork::getIsLatest, false);
|
||||
update(updateWrapper);
|
||||
|
||||
// 查询当前最大版本号
|
||||
LambdaQueryWrapper<BizContestWork> versionWrapper = new LambdaQueryWrapper<>();
|
||||
versionWrapper.eq(BizContestWork::getRegistrationId, dto.getRegistrationId());
|
||||
versionWrapper.orderByDesc(BizContestWork::getVersion);
|
||||
versionWrapper.last("LIMIT 1");
|
||||
BizContestWork latestWork = getOne(versionWrapper, false);
|
||||
if (latestWork != null) {
|
||||
version = latestWork.getVersion() + 1;
|
||||
}
|
||||
} else {
|
||||
// once 规则:检查是否已有作品
|
||||
LambdaQueryWrapper<BizContestWork> existWrapper = new LambdaQueryWrapper<>();
|
||||
existWrapper.eq(BizContestWork::getRegistrationId, dto.getRegistrationId());
|
||||
long existCount = count(existWrapper);
|
||||
if (existCount > 0) {
|
||||
throw BusinessException.of(ErrorCode.CONFLICT, "该报名已提交作品,不允许重复提交");
|
||||
}
|
||||
}
|
||||
|
||||
// 生成作品编号
|
||||
String workNo = nextContestWorkNo(contestId);
|
||||
|
||||
// 创建作品
|
||||
BizContestWork work = new BizContestWork();
|
||||
work.setTenantId(tenantId);
|
||||
work.setContestId(contestId);
|
||||
work.setRegistrationId(dto.getRegistrationId());
|
||||
work.setWorkNo(workNo);
|
||||
work.setTitle(dto.getTitle());
|
||||
work.setDescription(dto.getDescription());
|
||||
work.setFiles(dto.getFiles());
|
||||
work.setVersion(version);
|
||||
work.setIsLatest(true);
|
||||
work.setStatus(WorkStatus.SUBMITTED.getValue());
|
||||
work.setSubmitTime(LocalDateTime.now());
|
||||
work.setSubmitterUserId(submitterId);
|
||||
work.setSubmitterAccountNo(registration.getAccountNo());
|
||||
work.setPreviewUrl(dto.getPreviewUrl());
|
||||
work.setPreviewUrls(dto.getPreviewUrls());
|
||||
work.setAiModelMeta(dto.getAiModelMeta());
|
||||
|
||||
save(work);
|
||||
log.info("作品提交成功,ID:{},编号:{}", work.getId(), workNo);
|
||||
|
||||
// 保存附件
|
||||
if (dto.getAttachments() != null && !dto.getAttachments().isEmpty()) {
|
||||
for (SubmitWorkDto.AttachmentItem item : dto.getAttachments()) {
|
||||
BizContestWorkAttachment attachment = new BizContestWorkAttachment();
|
||||
attachment.setTenantId(tenantId);
|
||||
attachment.setContestId(contestId);
|
||||
attachment.setWorkId(work.getId());
|
||||
attachment.setFileName(item.getFileName());
|
||||
attachment.setFileUrl(item.getFileUrl());
|
||||
attachment.setFileType(item.getFileType());
|
||||
attachment.setSize(item.getSize());
|
||||
attachment.setCreator(submitterId != null ? submitterId.intValue() : null);
|
||||
attachment.setCreateTime(LocalDateTime.now());
|
||||
contestWorkAttachmentMapper.insert(attachment);
|
||||
}
|
||||
log.info("作品附件保存成功,数量:{}", dto.getAttachments().size());
|
||||
}
|
||||
|
||||
return workToMap(work);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<Map<String, Object>> findAll(QueryWorkDto dto, Long tenantId, boolean isSuperTenant) {
|
||||
log.info("查询作品列表,活动ID:{},页码:{}", dto.getContestId(), dto.getPage());
|
||||
|
||||
LambdaQueryWrapper<BizContestWork> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
if (dto.getContestId() != null) {
|
||||
wrapper.eq(BizContestWork::getContestId, dto.getContestId());
|
||||
}
|
||||
if (StringUtils.hasText(dto.getStatus())) {
|
||||
wrapper.eq(BizContestWork::getStatus, dto.getStatus());
|
||||
}
|
||||
if (StringUtils.hasText(dto.getTitle())) {
|
||||
wrapper.like(BizContestWork::getTitle, dto.getTitle());
|
||||
}
|
||||
if (StringUtils.hasText(dto.getWorkNo())) {
|
||||
wrapper.eq(BizContestWork::getWorkNo, dto.getWorkNo());
|
||||
}
|
||||
if (dto.getRegistrationId() != null) {
|
||||
wrapper.eq(BizContestWork::getRegistrationId, dto.getRegistrationId());
|
||||
}
|
||||
if (dto.getTenantId() != null) {
|
||||
wrapper.eq(BizContestWork::getTenantId, dto.getTenantId());
|
||||
} else if (!isSuperTenant && tenantId != null) {
|
||||
wrapper.eq(BizContestWork::getTenantId, tenantId);
|
||||
}
|
||||
// username 筛选:对应报名表的 account_no
|
||||
if (StringUtils.hasText(dto.getUsername())) {
|
||||
LambdaQueryWrapper<BizContestRegistration> userRegWrapper = new LambdaQueryWrapper<>();
|
||||
if (dto.getContestId() != null) {
|
||||
userRegWrapper.eq(BizContestRegistration::getContestId, dto.getContestId());
|
||||
}
|
||||
if (dto.getTenantId() != null) {
|
||||
userRegWrapper.eq(BizContestRegistration::getTenantId, dto.getTenantId());
|
||||
} else if (!isSuperTenant && tenantId != null) {
|
||||
userRegWrapper.eq(BizContestRegistration::getTenantId, tenantId);
|
||||
}
|
||||
userRegWrapper.eq(BizContestRegistration::getValidState, 1);
|
||||
userRegWrapper.like(BizContestRegistration::getAccountNo, dto.getUsername());
|
||||
List<BizContestRegistration> userRegs = contestRegistrationMapper.selectList(userRegWrapper);
|
||||
Set<Long> userRegIds = userRegs.stream()
|
||||
.map(BizContestRegistration::getId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
if (!userRegIds.isEmpty()) {
|
||||
wrapper.in(BizContestWork::getRegistrationId, userRegIds);
|
||||
} else {
|
||||
// 没有匹配的报名记录,返回空结果
|
||||
wrapper.eq(BizContestWork::getId, -1L);
|
||||
}
|
||||
}
|
||||
|
||||
// name 筛选:对应报名表的 account_name
|
||||
if (StringUtils.hasText(dto.getName())) {
|
||||
LambdaQueryWrapper<BizContestRegistration> nameRegWrapper = new LambdaQueryWrapper<>();
|
||||
if (dto.getContestId() != null) {
|
||||
nameRegWrapper.eq(BizContestRegistration::getContestId, dto.getContestId());
|
||||
}
|
||||
if (dto.getTenantId() != null) {
|
||||
nameRegWrapper.eq(BizContestRegistration::getTenantId, dto.getTenantId());
|
||||
} else if (!isSuperTenant && tenantId != null) {
|
||||
nameRegWrapper.eq(BizContestRegistration::getTenantId, tenantId);
|
||||
}
|
||||
nameRegWrapper.eq(BizContestRegistration::getValidState, 1);
|
||||
nameRegWrapper.like(BizContestRegistration::getAccountName, dto.getName());
|
||||
List<BizContestRegistration> nameRegs = contestRegistrationMapper.selectList(nameRegWrapper);
|
||||
Set<Long> nameRegIds = nameRegs.stream()
|
||||
.map(BizContestRegistration::getId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
if (!nameRegIds.isEmpty()) {
|
||||
wrapper.in(BizContestWork::getRegistrationId, nameRegIds);
|
||||
} else {
|
||||
wrapper.eq(BizContestWork::getId, -1L);
|
||||
}
|
||||
}
|
||||
|
||||
Set<Long> keywordRegistrationIds = Collections.emptySet();
|
||||
if (StringUtils.hasText(dto.getKeyword())) {
|
||||
String keyword = dto.getKeyword();
|
||||
|
||||
// keyword 命中口径:作品编号(work_no)/作者姓名(account_name)/报名账号(account_no)/队伍名称(team_name)
|
||||
LambdaQueryWrapper<BizContestRegistration> regWrapper = new LambdaQueryWrapper<>();
|
||||
if (dto.getContestId() != null) {
|
||||
regWrapper.eq(BizContestRegistration::getContestId, dto.getContestId());
|
||||
}
|
||||
regWrapper.eq(BizContestRegistration::getValidState, 1);
|
||||
|
||||
if (dto.getTenantId() != null) {
|
||||
regWrapper.eq(BizContestRegistration::getTenantId, dto.getTenantId());
|
||||
} else if (!isSuperTenant && tenantId != null) {
|
||||
regWrapper.eq(BizContestRegistration::getTenantId, tenantId);
|
||||
}
|
||||
|
||||
regWrapper.and(w -> w.like(BizContestRegistration::getAccountNo, keyword)
|
||||
.or().like(BizContestRegistration::getAccountName, keyword)
|
||||
.or().like(BizContestRegistration::getTeamName, keyword));
|
||||
|
||||
List<BizContestRegistration> regs = contestRegistrationMapper.selectList(regWrapper);
|
||||
keywordRegistrationIds = regs.stream()
|
||||
.map(BizContestRegistration::getId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
if (!keywordRegistrationIds.isEmpty()) {
|
||||
// 使用 apply 拼接 IN 条件,避免当前 MyBatis-Plus LambdaQueryWrapper 的
|
||||
// or().in() 链式泛型推断导致的编译失败。
|
||||
String registrationIdIn = keywordRegistrationIds.stream()
|
||||
.map(String::valueOf)
|
||||
.collect(Collectors.joining(","));
|
||||
wrapper.and(w -> w.like(BizContestWork::getWorkNo, keyword)
|
||||
.or().apply("registration_id IN (" + registrationIdIn + ")"));
|
||||
} else {
|
||||
wrapper.and(w -> w.like(BizContestWork::getWorkNo, keyword));
|
||||
}
|
||||
}
|
||||
if (StringUtils.hasText(dto.getSubmitStartTime())) {
|
||||
wrapper.ge(BizContestWork::getSubmitTime, parseDateTime(dto.getSubmitStartTime(), true));
|
||||
}
|
||||
if (StringUtils.hasText(dto.getSubmitEndTime())) {
|
||||
wrapper.le(BizContestWork::getSubmitTime, parseDateTime(dto.getSubmitEndTime(), false));
|
||||
}
|
||||
|
||||
// assignStatus 筛选:基于分配表判断已分配/未分配
|
||||
if (StringUtils.hasText(dto.getAssignStatus()) && dto.getContestId() != null) {
|
||||
LambdaQueryWrapper<BizContestWorkJudgeAssignment> assignQueryWrapper = new LambdaQueryWrapper<>();
|
||||
assignQueryWrapper.eq(BizContestWorkJudgeAssignment::getContestId, dto.getContestId());
|
||||
assignQueryWrapper.select(BizContestWorkJudgeAssignment::getWorkId);
|
||||
assignQueryWrapper.groupBy(BizContestWorkJudgeAssignment::getWorkId);
|
||||
List<BizContestWorkJudgeAssignment> assignedRecords = assignmentMapper.selectList(assignQueryWrapper);
|
||||
Set<Long> assignedWorkIds = assignedRecords.stream()
|
||||
.map(BizContestWorkJudgeAssignment::getWorkId)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
if ("assigned".equals(dto.getAssignStatus())) {
|
||||
if (assignedWorkIds.isEmpty()) {
|
||||
wrapper.eq(BizContestWork::getId, -1L);
|
||||
} else {
|
||||
wrapper.in(BizContestWork::getId, assignedWorkIds);
|
||||
}
|
||||
} else if ("unassigned".equals(dto.getAssignStatus())) {
|
||||
if (!assignedWorkIds.isEmpty()) {
|
||||
wrapper.notIn(BizContestWork::getId, assignedWorkIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 默认只查最新版本
|
||||
wrapper.eq(BizContestWork::getIsLatest, true);
|
||||
wrapper.eq(BizContestWork::getValidState, 1);
|
||||
wrapper.orderByDesc(BizContestWork::getSubmitTime);
|
||||
|
||||
Page<BizContestWork> page = new Page<>(dto.getPage(), dto.getPageSize());
|
||||
Page<BizContestWork> result = contestWorkMapper.selectPage(page, wrapper);
|
||||
|
||||
// 批量查询报名/活动信息
|
||||
Set<Long> registrationIds = result.getRecords().stream()
|
||||
.map(BizContestWork::getRegistrationId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
Set<Long> contestIds = result.getRecords().stream()
|
||||
.map(BizContestWork::getContestId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
Map<Long, BizContestRegistration> registrationMap = new HashMap<>();
|
||||
if (!registrationIds.isEmpty()) {
|
||||
List<BizContestRegistration> registrations = contestRegistrationMapper.selectBatchIds(registrationIds);
|
||||
registrationMap = registrations.stream()
|
||||
.collect(Collectors.toMap(BizContestRegistration::getId, r -> r));
|
||||
}
|
||||
|
||||
Map<Long, BizContest> contestMap = new HashMap<>();
|
||||
if (!contestIds.isEmpty()) {
|
||||
List<BizContest> contests = contestMapper.selectBatchIds(contestIds);
|
||||
contestMap = contests.stream()
|
||||
.collect(Collectors.toMap(BizContest::getId, c -> c));
|
||||
}
|
||||
|
||||
// 批量查询分配信息
|
||||
Set<Long> workIds = result.getRecords().stream()
|
||||
.map(BizContestWork::getId)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
Map<Long, List<BizContestWorkJudgeAssignment>> assignmentMap = new HashMap<>();
|
||||
Map<Long, String> judgeNameMap = new HashMap<>();
|
||||
if (!workIds.isEmpty()) {
|
||||
LambdaQueryWrapper<BizContestWorkJudgeAssignment> assignWrapper = new LambdaQueryWrapper<>();
|
||||
assignWrapper.in(BizContestWorkJudgeAssignment::getWorkId, workIds);
|
||||
if (dto.getContestId() != null) {
|
||||
assignWrapper.eq(BizContestWorkJudgeAssignment::getContestId, dto.getContestId());
|
||||
}
|
||||
List<BizContestWorkJudgeAssignment> allAssignments = assignmentMapper.selectList(assignWrapper);
|
||||
assignmentMap = allAssignments.stream()
|
||||
.collect(Collectors.groupingBy(BizContestWorkJudgeAssignment::getWorkId));
|
||||
|
||||
// 批量查询评委用户信息
|
||||
Set<Long> judgeIds = allAssignments.stream()
|
||||
.map(BizContestWorkJudgeAssignment::getJudgeId)
|
||||
.collect(Collectors.toSet());
|
||||
if (!judgeIds.isEmpty()) {
|
||||
List<SysUser> judgeUsers = sysUserMapper.selectBatchIds(judgeIds);
|
||||
for (SysUser u : judgeUsers) {
|
||||
judgeNameMap.put(u.getId(), u.getNickname());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Map<Long, BizContestRegistration> finalRegistrationMap = registrationMap;
|
||||
Map<Long, BizContest> finalContestMap = contestMap;
|
||||
Map<Long, List<BizContestWorkJudgeAssignment>> finalAssignmentMap = assignmentMap;
|
||||
Map<Long, String> finalJudgeNameMap = judgeNameMap;
|
||||
|
||||
// 批量评分:列表「评委评分」与详情抽屉一致;作品表 final_score 未落库时按评审规则从评分表回算
|
||||
Map<Long, List<BizContestWorkScore>> scoresByWorkId = new HashMap<>();
|
||||
if (!workIds.isEmpty()) {
|
||||
LambdaQueryWrapper<BizContestWorkScore> scoreWrapper = new LambdaQueryWrapper<>();
|
||||
scoreWrapper.in(BizContestWorkScore::getWorkId, workIds);
|
||||
scoreWrapper.eq(BizContestWorkScore::getValidState, 1);
|
||||
List<BizContestWorkScore> allPageScores = contestWorkScoreMapper.selectList(scoreWrapper);
|
||||
scoresByWorkId = allPageScores.stream()
|
||||
.collect(Collectors.groupingBy(BizContestWorkScore::getWorkId));
|
||||
}
|
||||
|
||||
Map<Long, String> contestCalculationRuleCache = new HashMap<>();
|
||||
Map<Long, Map<Long, BigDecimal>> contestWeightMapCache = new HashMap<>();
|
||||
|
||||
// 批量预加载评审规则,避免 N+1 查询
|
||||
Set<Long> ruleIds = contestIds.stream()
|
||||
.map(cid -> finalContestMap.get(cid))
|
||||
.filter(c -> c != null && c.getReviewRuleId() != null)
|
||||
.map(BizContest::getReviewRuleId)
|
||||
.collect(Collectors.toSet());
|
||||
Map<Long, BizContestReviewRule> ruleMap = new HashMap<>();
|
||||
if (!ruleIds.isEmpty()) {
|
||||
LambdaQueryWrapper<BizContestReviewRule> ruleWrapper = new LambdaQueryWrapper<>();
|
||||
ruleWrapper.in(BizContestReviewRule::getId, ruleIds);
|
||||
contestReviewRuleMapper.selectList(ruleWrapper)
|
||||
.forEach(r -> ruleMap.put(r.getId(), r));
|
||||
}
|
||||
|
||||
// 批量预加载所有相关活动的评委权重
|
||||
LambdaQueryWrapper<BizContestJudge> allJudgesWrapper = new LambdaQueryWrapper<>();
|
||||
allJudgesWrapper.in(BizContestJudge::getContestId, contestIds);
|
||||
allJudgesWrapper.eq(BizContestJudge::getValidState, 1);
|
||||
List<BizContestJudge> allJudges = contestJudgeMapper.selectList(allJudgesWrapper);
|
||||
Map<Long, List<BizContestJudge>> judgesByContestId = allJudges.stream()
|
||||
.collect(Collectors.groupingBy(BizContestJudge::getContestId));
|
||||
|
||||
for (Long cid : contestIds) {
|
||||
BizContest c = finalContestMap.get(cid);
|
||||
if (c == null) {
|
||||
continue;
|
||||
}
|
||||
String calculationRule = "average";
|
||||
if (c.getReviewRuleId() != null) {
|
||||
BizContestReviewRule rule = ruleMap.get(c.getReviewRuleId());
|
||||
if (rule != null && StringUtils.hasText(rule.getCalculationRule())) {
|
||||
calculationRule = rule.getCalculationRule();
|
||||
}
|
||||
}
|
||||
contestCalculationRuleCache.put(cid, calculationRule);
|
||||
|
||||
List<BizContestJudge> judges = judgesByContestId.getOrDefault(cid, Collections.emptyList());
|
||||
Map<Long, BigDecimal> weightMap = new HashMap<>();
|
||||
for (BizContestJudge j : judges) {
|
||||
weightMap.put(j.getJudgeId(), j.getWeight() != null ? j.getWeight() : BigDecimal.ONE);
|
||||
}
|
||||
contestWeightMapCache.put(cid, weightMap);
|
||||
}
|
||||
|
||||
Map<Long, List<BizContestWorkScore>> finalScoresByWorkId = scoresByWorkId;
|
||||
Map<Long, String> finalContestCalculationRuleCache = contestCalculationRuleCache;
|
||||
Map<Long, Map<Long, BigDecimal>> finalContestWeightMapCache = contestWeightMapCache;
|
||||
|
||||
List<Map<String, Object>> voList = result.getRecords().stream()
|
||||
.map(work -> {
|
||||
Map<String, Object> map = workToMap(work);
|
||||
|
||||
BizContest contest = finalContestMap.get(work.getContestId());
|
||||
if (contest != null) {
|
||||
Map<String, Object> contestVo = new LinkedHashMap<>();
|
||||
contestVo.put("id", contest.getId());
|
||||
contestVo.put("contestName", contest.getContestName());
|
||||
map.put("contest", contestVo);
|
||||
}
|
||||
|
||||
BizContestRegistration reg = finalRegistrationMap.get(work.getRegistrationId());
|
||||
if (reg != null) {
|
||||
Map<String, Object> userVo = new LinkedHashMap<>();
|
||||
userVo.put("id", reg.getUserId());
|
||||
userVo.put("username", reg.getAccountNo());
|
||||
userVo.put("nickname", reg.getAccountName());
|
||||
|
||||
Map<String, Object> regVo = new LinkedHashMap<>();
|
||||
regVo.put("id", reg.getId());
|
||||
regVo.put("user", userVo);
|
||||
|
||||
if (StringUtils.hasText(reg.getTeamName()) || reg.getTeamId() != null) {
|
||||
Map<String, Object> teamVo = new LinkedHashMap<>();
|
||||
teamVo.put("teamName", reg.getTeamName());
|
||||
regVo.put("team", teamVo);
|
||||
}
|
||||
|
||||
map.put("registration", regVo);
|
||||
|
||||
// 兼容旧字段:保留扁平账号信息
|
||||
map.put("accountNo", reg.getAccountNo());
|
||||
map.put("accountName", reg.getAccountName());
|
||||
map.put("userId", reg.getUserId());
|
||||
}
|
||||
|
||||
// 分配信息
|
||||
List<BizContestWorkJudgeAssignment> workAssignments = finalAssignmentMap.getOrDefault(work.getId(), Collections.emptyList());
|
||||
List<Map<String, Object>> assignmentVoList = workAssignments.stream().map(a -> {
|
||||
Map<String, Object> assignVo = new LinkedHashMap<>();
|
||||
assignVo.put("id", a.getId());
|
||||
assignVo.put("judgeId", a.getJudgeId());
|
||||
assignVo.put("status", a.getStatus());
|
||||
assignVo.put("assignmentTime", a.getAssignmentTime());
|
||||
Map<String, Object> judgeVo = new LinkedHashMap<>();
|
||||
judgeVo.put("id", a.getJudgeId());
|
||||
judgeVo.put("nickname", finalJudgeNameMap.getOrDefault(a.getJudgeId(), ""));
|
||||
assignVo.put("judge", judgeVo);
|
||||
return assignVo;
|
||||
}).collect(Collectors.toList());
|
||||
map.put("assignments", assignmentVoList);
|
||||
|
||||
// _count 用于分配状态判断
|
||||
Map<String, Object> countVo = new LinkedHashMap<>();
|
||||
countVo.put("assignments", workAssignments.size());
|
||||
map.put("_count", countVo);
|
||||
|
||||
int totalJudgesCount = workAssignments.size();
|
||||
long reviewedCount = workAssignments.stream()
|
||||
.filter(a -> "completed".equals(a.getStatus()))
|
||||
.count();
|
||||
map.put("totalJudgesCount", totalJudgesCount);
|
||||
map.put("reviewedCount", reviewedCount);
|
||||
|
||||
BigDecimal displayFinal = work.getFinalScore();
|
||||
if (displayFinal == null) {
|
||||
List<BizContestWorkScore> scores = finalScoresByWorkId.getOrDefault(work.getId(), Collections.emptyList());
|
||||
if (!scores.isEmpty()) {
|
||||
String rule = finalContestCalculationRuleCache.getOrDefault(work.getContestId(), "average");
|
||||
Map<Long, BigDecimal> wm = finalContestWeightMapCache.getOrDefault(work.getContestId(), Collections.emptyMap());
|
||||
displayFinal = ContestFinalScoreCalculator.compute(scores, rule, wm);
|
||||
}
|
||||
}
|
||||
if (displayFinal != null) {
|
||||
map.put("finalScore", displayFinal);
|
||||
map.put("averageScore", displayFinal);
|
||||
}
|
||||
|
||||
return map;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return PageResult.from(result, voList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getStats(Long contestId, Long tenantId, boolean isSuperTenant) {
|
||||
log.info("获取作品统计,活动ID:{}", contestId);
|
||||
|
||||
// 租户过滤
|
||||
boolean needTenantFilter = !isSuperTenant && tenantId != null;
|
||||
|
||||
LambdaQueryWrapper<BizContestWork> baseWrapper = new LambdaQueryWrapper<>();
|
||||
if (contestId != null) {
|
||||
baseWrapper.eq(BizContestWork::getContestId, contestId);
|
||||
}
|
||||
baseWrapper.eq(BizContestWork::getIsLatest, true);
|
||||
baseWrapper.eq(BizContestWork::getValidState, 1);
|
||||
if (needTenantFilter) {
|
||||
baseWrapper.eq(BizContestWork::getTenantId, tenantId);
|
||||
}
|
||||
long total = count(baseWrapper);
|
||||
|
||||
LambdaQueryWrapper<BizContestWork> submittedWrapper = new LambdaQueryWrapper<>();
|
||||
if (contestId != null) {
|
||||
submittedWrapper.eq(BizContestWork::getContestId, contestId);
|
||||
}
|
||||
submittedWrapper.eq(BizContestWork::getIsLatest, true);
|
||||
submittedWrapper.eq(BizContestWork::getValidState, 1);
|
||||
if (needTenantFilter) {
|
||||
submittedWrapper.eq(BizContestWork::getTenantId, tenantId);
|
||||
}
|
||||
submittedWrapper.eq(BizContestWork::getStatus, WorkStatus.SUBMITTED.getValue());
|
||||
long submitted = count(submittedWrapper);
|
||||
|
||||
LambdaQueryWrapper<BizContestWork> reviewingWrapper = new LambdaQueryWrapper<>();
|
||||
if (contestId != null) {
|
||||
reviewingWrapper.eq(BizContestWork::getContestId, contestId);
|
||||
}
|
||||
reviewingWrapper.eq(BizContestWork::getIsLatest, true);
|
||||
reviewingWrapper.eq(BizContestWork::getValidState, 1);
|
||||
if (needTenantFilter) {
|
||||
reviewingWrapper.eq(BizContestWork::getTenantId, tenantId);
|
||||
}
|
||||
reviewingWrapper.eq(BizContestWork::getStatus, WorkStatus.REVIEWING.getValue());
|
||||
long reviewing = count(reviewingWrapper);
|
||||
|
||||
LambdaQueryWrapper<BizContestWork> reviewedWrapper = new LambdaQueryWrapper<>();
|
||||
if (contestId != null) {
|
||||
reviewedWrapper.eq(BizContestWork::getContestId, contestId);
|
||||
}
|
||||
reviewedWrapper.eq(BizContestWork::getIsLatest, true);
|
||||
reviewedWrapper.eq(BizContestWork::getValidState, 1);
|
||||
if (needTenantFilter) {
|
||||
reviewedWrapper.eq(BizContestWork::getTenantId, tenantId);
|
||||
}
|
||||
reviewedWrapper.in(BizContestWork::getStatus, Arrays.asList(WorkStatus.ACCEPTED.getValue(), WorkStatus.AWARDED.getValue()));
|
||||
long reviewed = count(reviewedWrapper);
|
||||
|
||||
Map<String, Object> stats = new LinkedHashMap<>();
|
||||
stats.put("total", total);
|
||||
stats.put("submitted", submitted);
|
||||
stats.put("reviewing", reviewing);
|
||||
stats.put("reviewed", reviewed);
|
||||
return stats;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> findDetail(Long id) {
|
||||
log.info("查询作品详情,ID:{}", id);
|
||||
|
||||
BizContestWork work = getById(id);
|
||||
if (work == null) {
|
||||
throw BusinessException.of(ErrorCode.NOT_FOUND, "作品不存在");
|
||||
}
|
||||
|
||||
Map<String, Object> result = workToMap(work);
|
||||
|
||||
// 查询报名信息
|
||||
if (work.getRegistrationId() != null) {
|
||||
BizContestRegistration registration = contestRegistrationMapper.selectById(work.getRegistrationId());
|
||||
if (registration != null) {
|
||||
Map<String, Object> regInfo = new LinkedHashMap<>();
|
||||
regInfo.put("id", registration.getId());
|
||||
regInfo.put("userId", registration.getUserId());
|
||||
regInfo.put("accountNo", registration.getAccountNo());
|
||||
regInfo.put("accountName", registration.getAccountName());
|
||||
regInfo.put("registrationType", registration.getRegistrationType());
|
||||
regInfo.put("participantType", registration.getParticipantType());
|
||||
result.put("registration", regInfo);
|
||||
}
|
||||
}
|
||||
|
||||
// 查询附件
|
||||
LambdaQueryWrapper<BizContestWorkAttachment> attWrapper = new LambdaQueryWrapper<>();
|
||||
attWrapper.eq(BizContestWorkAttachment::getWorkId, id);
|
||||
List<BizContestWorkAttachment> attachments = contestWorkAttachmentMapper.selectList(attWrapper);
|
||||
result.put("attachments", attachments);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> getWorkVersions(Long registrationId) {
|
||||
log.info("查询作品版本历史,报名ID:{}", registrationId);
|
||||
|
||||
LambdaQueryWrapper<BizContestWork> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(BizContestWork::getRegistrationId, registrationId);
|
||||
wrapper.orderByDesc(BizContestWork::getVersion);
|
||||
|
||||
List<BizContestWork> works = list(wrapper);
|
||||
return works.stream()
|
||||
.map(this::workToMap)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<Map<String, Object>> getGuidedWorks(Long contestId, String workNo, String playerName,
|
||||
String accountNo, Long page, Long pageSize, Long userId) {
|
||||
log.info("查询指导作品,活动ID:{},教师用户ID:{}", contestId, userId);
|
||||
|
||||
// 简化实现:查询当前教师指导的报名ID列表,再查对应作品
|
||||
// 完整实现需要关联 t_biz_contest_registration_teacher 表
|
||||
LambdaQueryWrapper<BizContestWork> wrapper = new LambdaQueryWrapper<>();
|
||||
if (contestId != null) {
|
||||
wrapper.eq(BizContestWork::getContestId, contestId);
|
||||
}
|
||||
wrapper.eq(BizContestWork::getIsLatest, true);
|
||||
|
||||
if (StringUtils.hasText(workNo)) {
|
||||
wrapper.eq(BizContestWork::getWorkNo, workNo);
|
||||
}
|
||||
|
||||
wrapper.orderByDesc(BizContestWork::getSubmitTime);
|
||||
|
||||
Page<BizContestWork> pageObj = new Page<>(page != null ? page : 1L, pageSize != null ? pageSize : 10L);
|
||||
Page<BizContestWork> result = contestWorkMapper.selectPage(pageObj, wrapper);
|
||||
|
||||
List<Map<String, Object>> voList = result.getRecords().stream()
|
||||
.map(this::workToMap)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return PageResult.from(result, voList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeWork(Long id) {
|
||||
log.info("删除作品,ID:{}", id);
|
||||
removeById(id);
|
||||
log.info("作品删除成功,ID:{}", id);
|
||||
}
|
||||
|
||||
// ====== 私有辅助方法 ======
|
||||
|
||||
/**
|
||||
* 解析时间参数,支持 "yyyy-MM-dd" 和 "yyyy-MM-ddTHH:mm:ss" 两种格式。
|
||||
* isStart=true 时纯日期补 00:00:00,isStart=false 时纯日期补 23:59:59。
|
||||
*/
|
||||
private LocalDateTime parseDateTime(String value, boolean isStart) {
|
||||
if (value.contains("T")) {
|
||||
return LocalDateTime.parse(value, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
|
||||
}
|
||||
LocalDate date = LocalDate.parse(value, DateTimeFormatter.ISO_LOCAL_DATE);
|
||||
return isStart ? date.atStartOfDay() : date.atTime(23, 59, 59);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized String nextContestWorkNo(Long contestId) {
|
||||
// 使用 MAX(work_no) 查询避免并发编号重复
|
||||
LambdaQueryWrapper<BizContestWork> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(BizContestWork::getContestId, contestId);
|
||||
wrapper.likeRight(BizContestWork::getWorkNo, "W" + contestId + "-");
|
||||
wrapper.orderByDesc(BizContestWork::getWorkNo);
|
||||
wrapper.last("LIMIT 1");
|
||||
BizContestWork lastWork = getOne(wrapper, false);
|
||||
|
||||
int nextSeq = 1;
|
||||
if (lastWork != null && lastWork.getWorkNo() != null) {
|
||||
try {
|
||||
String no = lastWork.getWorkNo();
|
||||
int dashIndex = no.lastIndexOf("-");
|
||||
if (dashIndex > 0) {
|
||||
nextSeq = Integer.parseInt(no.substring(dashIndex + 1)) + 1;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("解析作品编号失败:{},将使用默认序号", lastWork.getWorkNo());
|
||||
// 降级:使用 count + 1
|
||||
LambdaQueryWrapper<BizContestWork> countWrapper = new LambdaQueryWrapper<>();
|
||||
countWrapper.eq(BizContestWork::getContestId, contestId);
|
||||
nextSeq = (int) count(countWrapper) + 1;
|
||||
}
|
||||
}
|
||||
return "W" + contestId + "-" + nextSeq;
|
||||
}
|
||||
|
||||
private Map<String, Object> workToMap(BizContestWork entity) {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("id", entity.getId());
|
||||
map.put("tenantId", entity.getTenantId());
|
||||
map.put("contestId", entity.getContestId());
|
||||
map.put("registrationId", entity.getRegistrationId());
|
||||
map.put("workNo", entity.getWorkNo());
|
||||
map.put("title", entity.getTitle());
|
||||
map.put("description", entity.getDescription());
|
||||
map.put("files", entity.getFiles());
|
||||
map.put("version", entity.getVersion());
|
||||
map.put("isLatest", entity.getIsLatest());
|
||||
map.put("status", entity.getStatus());
|
||||
map.put("submitTime", entity.getSubmitTime());
|
||||
map.put("submitterUserId", entity.getSubmitterUserId());
|
||||
map.put("submitterAccountNo", entity.getSubmitterAccountNo());
|
||||
map.put("submitSource", entity.getSubmitSource());
|
||||
map.put("previewUrl", entity.getPreviewUrl());
|
||||
map.put("previewUrls", entity.getPreviewUrls());
|
||||
map.put("aiModelMeta", entity.getAiModelMeta());
|
||||
map.put("userWorkId", entity.getUserWorkId());
|
||||
map.put("finalScore", entity.getFinalScore());
|
||||
map.put("rank", entity.getRank());
|
||||
map.put("awardLevel", entity.getAwardLevel());
|
||||
map.put("awardName", entity.getAwardName());
|
||||
map.put("certificateUrl", entity.getCertificateUrl());
|
||||
map.put("createTime", entity.getCreateTime());
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,96 @@
|
||||
package com.competition.modules.biz.homework.controller;
|
||||
|
||||
import com.competition.common.result.PageResult;
|
||||
import com.competition.common.result.Result;
|
||||
import com.competition.common.util.SecurityUtil;
|
||||
import com.competition.modules.biz.homework.dto.CreateHomeworkDto;
|
||||
import com.competition.modules.biz.homework.entity.BizHomework;
|
||||
import com.competition.modules.biz.homework.service.IHomeworkService;
|
||||
import com.competition.security.annotation.RequirePermission;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "作业管理")
|
||||
@RestController
|
||||
@RequestMapping("/homework/homeworks")
|
||||
@RequiredArgsConstructor
|
||||
public class HomeworkController {
|
||||
|
||||
private final IHomeworkService homeworkService;
|
||||
|
||||
@PostMapping
|
||||
@RequirePermission("homework:create")
|
||||
@Operation(summary = "创建作业")
|
||||
public Result<BizHomework> create(@Valid @RequestBody CreateHomeworkDto dto) {
|
||||
Long tenantId = SecurityUtil.getCurrentTenantId();
|
||||
return Result.success(homeworkService.create(dto, tenantId));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@RequirePermission("homework:read")
|
||||
@Operation(summary = "查询作业列表")
|
||||
public Result<PageResult<Map<String, Object>>> findAll(
|
||||
@RequestParam(defaultValue = "1") Long page,
|
||||
@RequestParam(defaultValue = "10") Long pageSize,
|
||||
@RequestParam(required = false) String name,
|
||||
@RequestParam(required = false) String status) {
|
||||
Long tenantId = SecurityUtil.getCurrentTenantId();
|
||||
return Result.success(homeworkService.findAll(page, pageSize, tenantId, name, status));
|
||||
}
|
||||
|
||||
@GetMapping("/my")
|
||||
@RequirePermission({"homework:read", "homework:student:read"})
|
||||
@Operation(summary = "查询我的作业列表")
|
||||
public Result<PageResult<Map<String, Object>>> findMyHomeworks(
|
||||
@RequestParam(defaultValue = "1") Long page,
|
||||
@RequestParam(defaultValue = "10") Long pageSize,
|
||||
@RequestParam(required = false) String name) {
|
||||
Long userId = SecurityUtil.getCurrentUserId();
|
||||
Long tenantId = SecurityUtil.getCurrentTenantId();
|
||||
return Result.success(homeworkService.findMyHomeworks(page, pageSize, userId, tenantId, name));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@RequirePermission({"homework:read", "homework:student:read"})
|
||||
@Operation(summary = "查询作业详情")
|
||||
public Result<Map<String, Object>> findDetail(@PathVariable Long id) {
|
||||
return Result.success(homeworkService.findDetail(id));
|
||||
}
|
||||
|
||||
@PatchMapping("/{id}")
|
||||
@RequirePermission("homework:update")
|
||||
@Operation(summary = "更新作业")
|
||||
public Result<BizHomework> update(@PathVariable Long id, @RequestBody CreateHomeworkDto dto) {
|
||||
return Result.success(homeworkService.update(id, dto));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/publish")
|
||||
@RequirePermission("homework:update")
|
||||
@Operation(summary = "发布作业")
|
||||
public Result<Void> publish(@PathVariable Long id, @RequestBody(required = false) List<Integer> publishScope) {
|
||||
homeworkService.publish(id, publishScope);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/unpublish")
|
||||
@RequirePermission("homework:update")
|
||||
@Operation(summary = "取消发布作业")
|
||||
public Result<Void> unpublish(@PathVariable Long id) {
|
||||
homeworkService.unpublish(id);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@RequirePermission("homework:delete")
|
||||
@Operation(summary = "删除作业")
|
||||
public Result<Void> remove(@PathVariable Long id) {
|
||||
homeworkService.remove(id);
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
package com.competition.modules.biz.homework.controller;
|
||||
|
||||
import com.competition.common.result.PageResult;
|
||||
import com.competition.common.result.Result;
|
||||
import com.competition.common.util.SecurityUtil;
|
||||
import com.competition.modules.biz.homework.dto.CreateHomeworkReviewRuleDto;
|
||||
import com.competition.modules.biz.homework.entity.BizHomeworkReviewRule;
|
||||
import com.competition.modules.biz.homework.service.IHomeworkReviewRuleService;
|
||||
import com.competition.security.annotation.RequirePermission;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "作业评审规则")
|
||||
@RestController
|
||||
@RequestMapping("/homework/review-rules")
|
||||
@RequiredArgsConstructor
|
||||
public class HomeworkReviewRuleController {
|
||||
|
||||
private final IHomeworkReviewRuleService homeworkReviewRuleService;
|
||||
|
||||
@GetMapping
|
||||
@RequirePermission("homework:read")
|
||||
@Operation(summary = "查询作业评审规则列表")
|
||||
public Result<PageResult<Map<String, Object>>> findAll(
|
||||
@RequestParam(defaultValue = "1") Long page,
|
||||
@RequestParam(defaultValue = "10") Long pageSize,
|
||||
@RequestParam(required = false) String name) {
|
||||
Long tenantId = SecurityUtil.getCurrentTenantId();
|
||||
return Result.success(homeworkReviewRuleService.findAll(page, pageSize, tenantId, name));
|
||||
}
|
||||
|
||||
@GetMapping("/select")
|
||||
@RequirePermission("homework:read")
|
||||
@Operation(summary = "查询作业评审规则选项列表")
|
||||
public Result<List<Map<String, Object>>> findAllForSelect() {
|
||||
Long tenantId = SecurityUtil.getCurrentTenantId();
|
||||
return Result.success(homeworkReviewRuleService.findAllForSelect(tenantId));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@RequirePermission("homework:update")
|
||||
@Operation(summary = "创建作业评审规则")
|
||||
public Result<BizHomeworkReviewRule> create(@Valid @RequestBody CreateHomeworkReviewRuleDto dto) {
|
||||
Long tenantId = SecurityUtil.getCurrentTenantId();
|
||||
return Result.success(homeworkReviewRuleService.create(dto, tenantId));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@RequirePermission("homework:read")
|
||||
@Operation(summary = "查询作业评审规则详情")
|
||||
public Result<Map<String, Object>> findDetail(@PathVariable Long id) {
|
||||
return Result.success(homeworkReviewRuleService.findDetail(id));
|
||||
}
|
||||
|
||||
@PatchMapping("/{id}")
|
||||
@RequirePermission("homework:update")
|
||||
@Operation(summary = "更新作业评审规则")
|
||||
public Result<BizHomeworkReviewRule> update(@PathVariable Long id, @RequestBody CreateHomeworkReviewRuleDto dto) {
|
||||
return Result.success(homeworkReviewRuleService.update(id, dto));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@RequirePermission("homework:update")
|
||||
@Operation(summary = "删除作业评审规则")
|
||||
public Result<Void> remove(@PathVariable Long id) {
|
||||
homeworkReviewRuleService.remove(id);
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
package com.competition.modules.biz.homework.controller;
|
||||
|
||||
import com.competition.common.enums.ErrorCode;
|
||||
import com.competition.common.exception.BusinessException;
|
||||
import com.competition.common.result.Result;
|
||||
import com.competition.common.util.SecurityUtil;
|
||||
import com.competition.modules.biz.homework.dto.CreateHomeworkScoreDto;
|
||||
import com.competition.modules.biz.homework.entity.BizHomeworkScore;
|
||||
import com.competition.modules.biz.homework.entity.BizHomeworkSubmission;
|
||||
import com.competition.modules.biz.homework.service.IHomeworkScoreService;
|
||||
import com.competition.modules.biz.homework.service.IHomeworkSubmissionService;
|
||||
import com.competition.security.annotation.RequirePermission;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@Slf4j
|
||||
@Tag(name = "作业评分")
|
||||
@RestController
|
||||
@RequestMapping("/homework/scores")
|
||||
@RequiredArgsConstructor
|
||||
public class HomeworkScoreController {
|
||||
|
||||
private final IHomeworkScoreService homeworkScoreService;
|
||||
private final IHomeworkSubmissionService homeworkSubmissionService;
|
||||
|
||||
@PostMapping
|
||||
@RequirePermission("homework:update")
|
||||
@Operation(summary = "创建评分")
|
||||
public Result<BizHomeworkScore> create(@Valid @RequestBody CreateHomeworkScoreDto dto) {
|
||||
Long reviewerId = SecurityUtil.getCurrentUserId();
|
||||
Long tenantId = SecurityUtil.getCurrentTenantId();
|
||||
return Result.success(homeworkScoreService.create(dto, reviewerId, tenantId));
|
||||
}
|
||||
|
||||
@PostMapping("/{submissionId}/violation")
|
||||
@RequirePermission("homework:update")
|
||||
@Operation(summary = "标记违规")
|
||||
public Result<Void> markViolation(@PathVariable Long submissionId) {
|
||||
log.info("标记作业提交违规,提交ID:{}", submissionId);
|
||||
|
||||
BizHomeworkSubmission submission = homeworkSubmissionService.getById(submissionId);
|
||||
if (submission == null) {
|
||||
throw BusinessException.of(ErrorCode.NOT_FOUND, "作业提交不存在");
|
||||
}
|
||||
|
||||
submission.setStatus("violation");
|
||||
homeworkSubmissionService.updateById(submission);
|
||||
log.info("作业提交已标记为违规,提交ID:{}", submissionId);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@PostMapping("/{submissionId}/reset")
|
||||
@RequirePermission("homework:update")
|
||||
@Operation(summary = "重置评分状态")
|
||||
public Result<Void> resetStatus(@PathVariable Long submissionId) {
|
||||
log.info("重置作业提交评分状态,提交ID:{}", submissionId);
|
||||
|
||||
BizHomeworkSubmission submission = homeworkSubmissionService.getById(submissionId);
|
||||
if (submission == null) {
|
||||
throw BusinessException.of(ErrorCode.NOT_FOUND, "作业提交不存在");
|
||||
}
|
||||
|
||||
submission.setStatus("pending");
|
||||
submission.setTotalScore(null);
|
||||
homeworkSubmissionService.updateById(submission);
|
||||
log.info("作业提交评分状态已重置,提交ID:{}", submissionId);
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
package com.competition.modules.biz.homework.controller;
|
||||
|
||||
import com.competition.common.result.PageResult;
|
||||
import com.competition.common.result.Result;
|
||||
import com.competition.common.util.SecurityUtil;
|
||||
import com.competition.modules.biz.homework.dto.CreateSubmissionDto;
|
||||
import com.competition.modules.biz.homework.entity.BizHomeworkSubmission;
|
||||
import com.competition.modules.biz.homework.service.IHomeworkSubmissionService;
|
||||
import com.competition.security.annotation.RequirePermission;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "作业提交")
|
||||
@RestController
|
||||
@RequestMapping("/homework/submissions")
|
||||
@RequiredArgsConstructor
|
||||
public class HomeworkSubmissionController {
|
||||
|
||||
private final IHomeworkSubmissionService homeworkSubmissionService;
|
||||
|
||||
@GetMapping
|
||||
@RequirePermission("homework:read")
|
||||
@Operation(summary = "查询作业提交列表")
|
||||
public Result<PageResult<Map<String, Object>>> findAll(
|
||||
@RequestParam(defaultValue = "1") Long page,
|
||||
@RequestParam(defaultValue = "10") Long pageSize,
|
||||
@RequestParam(required = false) Long homeworkId,
|
||||
@RequestParam(required = false) String workNo,
|
||||
@RequestParam(required = false) String workName,
|
||||
@RequestParam(required = false) String studentAccount,
|
||||
@RequestParam(required = false) String studentName,
|
||||
@RequestParam(required = false) String status) {
|
||||
Long tenantId = SecurityUtil.getCurrentTenantId();
|
||||
return Result.success(homeworkSubmissionService.findAll(page, pageSize, tenantId, homeworkId,
|
||||
workNo, workName, studentAccount, studentName, status));
|
||||
}
|
||||
|
||||
@GetMapping("/class-tree")
|
||||
@RequirePermission("homework:read")
|
||||
@Operation(summary = "获取班级树")
|
||||
public Result<List<Map<String, Object>>> getClassTree() {
|
||||
Long tenantId = SecurityUtil.getCurrentTenantId();
|
||||
return Result.success(homeworkSubmissionService.getClassTree(tenantId));
|
||||
}
|
||||
|
||||
@GetMapping("/my/{homeworkId}")
|
||||
@RequirePermission({"homework:read", "homework:student:read"})
|
||||
@Operation(summary = "查询我的作业提交")
|
||||
public Result<Map<String, Object>> findMySubmission(@PathVariable Long homeworkId) {
|
||||
Long userId = SecurityUtil.getCurrentUserId();
|
||||
return Result.success(homeworkSubmissionService.findMySubmission(homeworkId, userId));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@RequirePermission({"homework:read", "homework:student:read"})
|
||||
@Operation(summary = "提交作业")
|
||||
public Result<BizHomeworkSubmission> create(@Valid @RequestBody CreateSubmissionDto dto) {
|
||||
Long studentId = SecurityUtil.getCurrentUserId();
|
||||
Long tenantId = SecurityUtil.getCurrentTenantId();
|
||||
return Result.success(homeworkSubmissionService.create(dto, studentId, tenantId));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@RequirePermission("homework:read")
|
||||
@Operation(summary = "查询作业提交详情")
|
||||
public Result<Map<String, Object>> findDetail(@PathVariable Long id) {
|
||||
return Result.success(homeworkSubmissionService.findDetail(id));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
package com.competition.modules.biz.homework.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Schema(description = "创建作业DTO")
|
||||
public class CreateHomeworkDto {
|
||||
|
||||
@NotBlank(message = "作业名称不能为空")
|
||||
@Schema(description = "作业名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "作业内容")
|
||||
private String content;
|
||||
|
||||
@NotBlank(message = "提交开始时间不能为空")
|
||||
@Schema(description = "提交开始时间")
|
||||
private String submitStartTime;
|
||||
|
||||
@NotBlank(message = "提交截止时间不能为空")
|
||||
@Schema(description = "提交截止时间")
|
||||
private String submitEndTime;
|
||||
|
||||
@Schema(description = "附件")
|
||||
private Object attachments;
|
||||
|
||||
@Schema(description = "发布范围")
|
||||
private List<Integer> publishScope;
|
||||
|
||||
@Schema(description = "评审规则ID")
|
||||
private Long reviewRuleId;
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
package com.competition.modules.biz.homework.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "创建作业评审规则DTO")
|
||||
public class CreateHomeworkReviewRuleDto {
|
||||
|
||||
@NotBlank(message = "规则名称不能为空")
|
||||
@Schema(description = "规则名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "规则描述")
|
||||
private String description;
|
||||
|
||||
@NotNull(message = "评分标准不能为空")
|
||||
@Schema(description = "评分标准(JSON数组)")
|
||||
private Object criteria;
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
package com.competition.modules.biz.homework.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Data
|
||||
@Schema(description = "创建作业评分DTO")
|
||||
public class CreateHomeworkScoreDto {
|
||||
|
||||
@NotNull(message = "提交ID不能为空")
|
||||
@Schema(description = "提交ID")
|
||||
private Long submissionId;
|
||||
|
||||
@Schema(description = "维度评分(JSON)")
|
||||
private Object dimensionScores;
|
||||
|
||||
@NotNull(message = "总分不能为空")
|
||||
@Schema(description = "总分")
|
||||
private BigDecimal totalScore;
|
||||
|
||||
@Schema(description = "评语")
|
||||
private String comments;
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
package com.competition.modules.biz.homework.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "创建作业提交DTO")
|
||||
public class CreateSubmissionDto {
|
||||
|
||||
@NotNull(message = "作业ID不能为空")
|
||||
@Schema(description = "作业ID")
|
||||
private Long homeworkId;
|
||||
|
||||
@NotBlank(message = "作品名称不能为空")
|
||||
@Schema(description = "作品名称")
|
||||
private String workName;
|
||||
|
||||
@Schema(description = "作品描述")
|
||||
private String workDescription;
|
||||
|
||||
@Schema(description = "作品文件")
|
||||
private Object files;
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
package com.competition.modules.biz.homework.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import com.competition.common.entity.BaseEntity;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName(value = "t_biz_homework", autoResultMap = true)
|
||||
@Schema(description = "作业实体")
|
||||
public class BizHomework extends BaseEntity {
|
||||
|
||||
@Schema(description = "租户ID")
|
||||
@TableField("tenant_id")
|
||||
private Long tenantId;
|
||||
|
||||
@Schema(description = "作业名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "作业内容")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "提交开始时间")
|
||||
@TableField("submit_start_time")
|
||||
private LocalDateTime submitStartTime;
|
||||
|
||||
@Schema(description = "提交结束时间")
|
||||
@TableField("submit_end_time")
|
||||
private LocalDateTime submitEndTime;
|
||||
|
||||
@Schema(description = "附件(JSON)")
|
||||
@TableField(value = "attachments", typeHandler = JacksonTypeHandler.class)
|
||||
private Object attachments;
|
||||
|
||||
@Schema(description = "发布范围(JSON)")
|
||||
@TableField(value = "publish_scope", typeHandler = JacksonTypeHandler.class)
|
||||
private Object publishScope;
|
||||
|
||||
@Schema(description = "评审规则ID")
|
||||
@TableField("review_rule_id")
|
||||
private Long reviewRuleId;
|
||||
|
||||
@Schema(description = "状态:unpublished/published")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "发布时间")
|
||||
@TableField("publish_time")
|
||||
private LocalDateTime publishTime;
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
package com.competition.modules.biz.homework.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import com.competition.common.entity.BaseEntity;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName(value = "t_biz_homework_review_rule", autoResultMap = true)
|
||||
@Schema(description = "作业评审规则实体")
|
||||
public class BizHomeworkReviewRule extends BaseEntity {
|
||||
|
||||
@Schema(description = "租户ID")
|
||||
@TableField("tenant_id")
|
||||
private Long tenantId;
|
||||
|
||||
@Schema(description = "规则名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "规则描述")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "评分标准(JSON数组)")
|
||||
@TableField(value = "criteria", typeHandler = JacksonTypeHandler.class)
|
||||
private Object criteria;
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
package com.competition.modules.biz.homework.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import com.competition.common.entity.BaseEntity;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName(value = "t_biz_homework_score", autoResultMap = true)
|
||||
@Schema(description = "作业评分实体")
|
||||
public class BizHomeworkScore extends BaseEntity {
|
||||
|
||||
@Schema(description = "租户ID")
|
||||
@TableField("tenant_id")
|
||||
private Long tenantId;
|
||||
|
||||
@Schema(description = "提交ID")
|
||||
@TableField("submission_id")
|
||||
private Long submissionId;
|
||||
|
||||
@Schema(description = "评审人ID")
|
||||
@TableField("reviewer_id")
|
||||
private Long reviewerId;
|
||||
|
||||
@Schema(description = "各维度得分(JSON)")
|
||||
@TableField(value = "dimension_scores", typeHandler = JacksonTypeHandler.class)
|
||||
private Object dimensionScores;
|
||||
|
||||
@Schema(description = "总分")
|
||||
@TableField("total_score")
|
||||
private BigDecimal totalScore;
|
||||
|
||||
@Schema(description = "评语")
|
||||
private String comments;
|
||||
|
||||
@Schema(description = "评分时间")
|
||||
@TableField("score_time")
|
||||
private LocalDateTime scoreTime;
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
package com.competition.modules.biz.homework.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import com.competition.common.entity.BaseEntity;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName(value = "t_biz_homework_submission", autoResultMap = true)
|
||||
@Schema(description = "作业提交实体")
|
||||
public class BizHomeworkSubmission extends BaseEntity {
|
||||
|
||||
@Schema(description = "租户ID")
|
||||
@TableField("tenant_id")
|
||||
private Long tenantId;
|
||||
|
||||
@Schema(description = "作业ID")
|
||||
@TableField("homework_id")
|
||||
private Long homeworkId;
|
||||
|
||||
@Schema(description = "学生ID")
|
||||
@TableField("student_id")
|
||||
private Long studentId;
|
||||
|
||||
@Schema(description = "作品编号")
|
||||
@TableField("work_no")
|
||||
private String workNo;
|
||||
|
||||
@Schema(description = "作品名称")
|
||||
@TableField("work_name")
|
||||
private String workName;
|
||||
|
||||
@Schema(description = "作品描述")
|
||||
@TableField("work_description")
|
||||
private String workDescription;
|
||||
|
||||
@Schema(description = "作品文件(JSON)")
|
||||
@TableField(value = "files", typeHandler = JacksonTypeHandler.class)
|
||||
private Object files;
|
||||
|
||||
@Schema(description = "附件(JSON)")
|
||||
@TableField(value = "attachments", typeHandler = JacksonTypeHandler.class)
|
||||
private Object attachments;
|
||||
|
||||
@Schema(description = "提交时间")
|
||||
@TableField("submit_time")
|
||||
private LocalDateTime submitTime;
|
||||
|
||||
@Schema(description = "状态:pending/reviewed")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "总分")
|
||||
@TableField("total_score")
|
||||
private BigDecimal totalScore;
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.competition.modules.biz.homework.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.competition.modules.biz.homework.entity.BizHomework;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface HomeworkMapper extends BaseMapper<BizHomework> {
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.competition.modules.biz.homework.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.competition.modules.biz.homework.entity.BizHomeworkReviewRule;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface HomeworkReviewRuleMapper extends BaseMapper<BizHomeworkReviewRule> {
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.competition.modules.biz.homework.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.competition.modules.biz.homework.entity.BizHomeworkScore;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface HomeworkScoreMapper extends BaseMapper<BizHomeworkScore> {
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.competition.modules.biz.homework.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.competition.modules.biz.homework.entity.BizHomeworkSubmission;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface HomeworkSubmissionMapper extends BaseMapper<BizHomeworkSubmission> {
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
package com.competition.modules.biz.homework.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.competition.common.result.PageResult;
|
||||
import com.competition.modules.biz.homework.dto.CreateHomeworkReviewRuleDto;
|
||||
import com.competition.modules.biz.homework.entity.BizHomeworkReviewRule;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface IHomeworkReviewRuleService extends IService<BizHomeworkReviewRule> {
|
||||
|
||||
BizHomeworkReviewRule create(CreateHomeworkReviewRuleDto dto, Long tenantId);
|
||||
|
||||
PageResult<Map<String, Object>> findAll(Long page, Long pageSize, Long tenantId, String name);
|
||||
|
||||
List<Map<String, Object>> findAllForSelect(Long tenantId);
|
||||
|
||||
Map<String, Object> findDetail(Long id);
|
||||
|
||||
BizHomeworkReviewRule update(Long id, CreateHomeworkReviewRuleDto dto);
|
||||
|
||||
void remove(Long id);
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
package com.competition.modules.biz.homework.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.competition.modules.biz.homework.dto.CreateHomeworkScoreDto;
|
||||
import com.competition.modules.biz.homework.entity.BizHomeworkScore;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface IHomeworkScoreService extends IService<BizHomeworkScore> {
|
||||
|
||||
BizHomeworkScore create(CreateHomeworkScoreDto dto, Long reviewerId, Long tenantId);
|
||||
|
||||
List<Map<String, Object>> findBySubmission(Long submissionId);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user