Compare commits

...

2 Commits

Author SHA1 Message Date
aid
096d06af3d Java 后端完整转写:NestJS/Prisma → Spring Boot/MyBatis-Plus
## 技术栈
- Spring Boot 3.2 + Java 17 + MyBatis-Plus 3.5
- Spring Security + JWT 认证(与 NestJS 兼容)
- MapStruct + Knife4j + Druid + Hutool + FastJSON2
- 腾讯云 COS 文件上传

## 项目规模
- 239 个 Java 文件,246 个文件总计
- 39 个实体类映射到现有数据库
- ~256 个 API 端点,与 NestJS 完全兼容

## 模块清单
- Phase 0: 脚手架 + 基础框架(BaseEntity, Result, JWT, Security, AOP权限)
- Phase 1: 认证/用户/角色/权限/租户(~35 接口)
- Phase 2: 菜单/字典/配置/日志(~25 接口)
- Phase 3: 赛事核心 — 赛事/报名/作品/团队/附件/公告(~46 接口)
- Phase 4: 评审/计分/成果 — 评审规则/评委/分配/评分/排名/奖项/发布(~52 接口)
- Phase 5: 作业 — 作业/提交/评分/评审规则(~20 接口)
- Phase 6: 公众端 — 注册/登录/画廊/活动/作品库/子女/互动/内容审核(~55 接口)
- Phase 7: UGC — 作品/绘本页/标签/点赞/收藏/评论/举报/审核日志(~15 接口)
- Phase 8: 文件上传/OSS(1 接口)

## 验证结果
- mvn compile 零错误,1.8秒启动
- 62 个 API 端点手动测试通过(GET + POST/PATCH/DELETE)
- 20 个前端页面 Playwright 自动化测试通过,69+ API 调用零错误
- 核心业务全流程验证:登录→赛事→报名→作品→评审→计分→排名→奖项→发布

## 数据库适配
- 使用现有表名(Flyway 暂禁用,待正式切换时启用)
- Flyway V1/V2 迁移脚本已准备(表重命名+新审计字段)
- 修复:configs/t_contest_registration 添加 valid_state 列
- 修复:所有表 modify_time 添加 DEFAULT CURRENT_TIMESTAMP

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:05:41 +08:00
aid
bead1cf4dc 剥离学校端/教师端/学生端/3D建模模块,清理跨模块引用
- 移除 backend: school/ (schools, departments, grades, classes, teachers, students)
- 移除 backend: ai-3d/ (controller, service, providers, utils)
- 移除 frontend: views/school/, views/workbench/ai-3d/, views/model/
- 移除 prisma schema: School, Grade, Department, Class, Teacher, Student, StudentInterestClass, AI3DTask 共8个模型
- 移除 app.module.ts: SchoolModule, AI3DModule 导入
- 移除 router/index.ts: 3D建模4条路由
- 移除 menu.ts: componentMap 中学校/3D映射
- 修复 registrations.service.ts: 教师判断从 Teacher 模型改为角色判断
- 修复 results.service.ts: 移除 student include
- 修复 homework services: 移除 student/class/grade 相关 Prisma 查询
- 保留 students.ts/teachers.ts/ai-3d.ts 最小类型存根供赛事组件引用
- 原始代码备份至 competition-management-system-stripped-modules/

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:04:40 +08:00
311 changed files with 16150 additions and 15305 deletions

5
backend-java/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
target/
uploads/
*.log
.idea/
*.iml

198
backend-java/pom.xml Normal file
View 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.5.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>
<cos.version>5.6.227</cos.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>
<!-- 腾讯云 COS -->
<dependency>
<groupId>com.qcloud</groupId>
<artifactId>cos_api</artifactId>
<version>${cos.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>

View File

@ -0,0 +1,22 @@
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"
})
public class CompetitionApplication {
public static void main(String[] args) {
SpringApplication.run(CompetitionApplication.class, args);
}
}

View File

@ -0,0 +1,28 @@
package com.competition.common.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
/**
* 跨域配置
*/
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOriginPattern("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.addExposedHeader("X-Trace-Id");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}

View File

@ -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"));
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,22 @@
package com.competition.common.config;
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;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(traceIdInterceptor).addPathPatterns("/**");
}
}

View File

@ -0,0 +1,55 @@
package com.competition.common.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 基础实体类所有实体继承此类
* 当前阶段使用现有数据库列名新审计字段暂不映射 Flyway 迁移后启用
*/
@Data
public abstract class BaseEntity implements Serializable {
/** 主键 ID自增 */
@TableId(type = IdType.AUTO)
private Long id;
// ====== 新审计字段列尚未存在暂不映射到数据库 ======
/** 创建人账号(待 Flyway V2 后启用) */
@TableField(exist = false)
private String createBy;
/** 更新人账号(待 Flyway V2 后启用) */
@TableField(exist = false)
private String updateBy;
/** 逻辑删除标识(待 Flyway V2 后启用) */
@TableField(exist = false)
private Integer deleted;
// ====== 现有审计字段与当前数据库一致 ======
/** 创建人 ID */
@TableField(value = "creator", fill = FieldFill.INSERT)
private Integer creator;
/** 修改人 ID */
@TableField(value = "modifier", fill = FieldFill.INSERT_UPDATE)
private Integer modifier;
/** 创建时间 */
@TableField(value = "create_time", fill = FieldFill.INSERT)
private LocalDateTime createTime;
/** 修改时间(数据库列名为 modify_time */
@TableField(value = "modify_time", fill = FieldFill.INSERT_UPDATE)
private LocalDateTime modifyTime;
/** 有效状态1-有效2-失效 */
@TableField(value = "valid_state", fill = FieldFill.INSERT)
private Integer validState;
}

View File

@ -0,0 +1,23 @@
package com.competition.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 错误码枚举
*/
@Getter
@AllArgsConstructor
public enum ErrorCode {
SUCCESS(200, "success"),
BAD_REQUEST(400, "请求参数错误"),
UNAUTHORIZED(401, "未登录或 Token 已过期"),
FORBIDDEN(403, "没有访问权限"),
NOT_FOUND(404, "资源不存在"),
CONFLICT(409, "数据冲突"),
INTERNAL_ERROR(500, "系统内部错误");
private final Integer code;
private final String message;
}

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -0,0 +1,45 @@
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();
Long userId = SecurityUtil.getCurrentUserIdOrNull();
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();
Long userId = SecurityUtil.getCurrentUserIdOrNull();
this.strictUpdateFill(metaObject, "modifyTime", LocalDateTime.class, now);
if (userId != null) {
this.strictUpdateFill(metaObject, "modifier", Integer.class, userId.intValue());
}
}
}

View File

@ -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);
}
}

View File

@ -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()
);
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -0,0 +1,102 @@
package com.competition.modules.biz.contest.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.competition.common.result.PageResult;
import com.competition.common.result.Result;
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.util.List;
@Tag(name = "赛事公告")
@RestController
@RequestMapping("/contests/notices")
@RequiredArgsConstructor
public class ContestNoticeController {
private final IContestNoticeService noticeService;
@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(LocalDateTime.parse(dto.getPublishTime()));
}
noticeService.save(notice);
return Result.success(notice);
}
@GetMapping("/contest/{contestId}")
@RequirePermission("notice:read")
@Operation(summary = "查询赛事下的公告列表")
public Result<List<BizContestNotice>> findByContest(@PathVariable Long contestId) {
List<BizContestNotice> list = noticeService.list(
new LambdaQueryWrapper<BizContestNotice>()
.eq(BizContestNotice::getContestId, contestId)
.orderByDesc(BizContestNotice::getCreateTime));
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) {
LambdaQueryWrapper<BizContestNotice> wrapper = new LambdaQueryWrapper<BizContestNotice>()
.like(StringUtils.hasText(title), BizContestNotice::getTitle, title)
.orderByDesc(BizContestNotice::getCreateTime);
Page<BizContestNotice> result = noticeService.page(new Page<>(page, pageSize), wrapper);
return Result.success(PageResult.from(result));
}
@GetMapping("/{id}")
@RequirePermission("notice:read")
@Operation(summary = "查询公告详情")
public Result<BizContestNotice> findDetail(@PathVariable Long id) {
return Result.success(noticeService.getById(id));
}
@PatchMapping("/{id}")
@RequirePermission("notice:update")
@Operation(summary = "更新公告")
public Result<Void> update(@PathVariable Long id, @RequestBody CreateNoticeDto dto) {
BizContestNotice notice = new BizContestNotice();
notice.setId(id);
notice.setTitle(dto.getTitle());
notice.setContent(dto.getContent());
notice.setNoticeType(dto.getNoticeType());
notice.setPriority(dto.getPriority());
if (StringUtils.hasText(dto.getPublishTime())) {
notice.setPublishTime(LocalDateTime.parse(dto.getPublishTime()));
}
noticeService.updateById(notice);
return Result.success();
}
@DeleteMapping("/{id}")
@RequirePermission("notice:delete")
@Operation(summary = "删除公告")
public Result<Void> remove(@PathVariable Long id) {
noticeService.removeById(id);
return Result.success();
}
}

View File

@ -0,0 +1,125 @@
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) {
return Result.success(registrationService.getStats(contestId, SecurityUtil.getCurrentTenantId()));
}
@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();
}
}

View File

@ -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();
}
}

View File

@ -0,0 +1,87 @@
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()));
}
@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();
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -0,0 +1,152 @@
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 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_contest", autoResultMap = true)
public class BizContest extends BaseEntity {
/** 赛事名称(唯一) */
@TableField("contest_name")
private String contestName;
/** 赛事类型individual/team */
@TableField("contest_type")
private String contestType;
/** 赛事发布状态unpublished/published */
@TableField("contest_state")
private String contestState;
/** 赛事进度状态ongoing/finished */
private String status;
/** 开始时间 */
@TableField("start_time")
private LocalDateTime startTime;
/** 结束时间 */
@TableField("end_time")
private LocalDateTime endTime;
/** 线下地址 */
private String address;
/** 赛事详情(富文本) */
private String content;
/** 可见范围public/designated/internal */
private String visibility;
// ====== 授权租户JSON ======
/** 授权租户 ID 数组 */
@TableField(value = "contest_tenants", typeHandler = JacksonTypeHandler.class)
private List<Integer> contestTenants;
// ====== 封面和联系方式 ======
@TableField("cover_url")
private String coverUrl;
@TableField("poster_url")
private String posterUrl;
@TableField("contact_name")
private String contactName;
@TableField("contact_phone")
private String contactPhone;
@TableField("contact_qrcode")
private String contactQrcode;
// ====== 主办/协办/赞助JSON ======
@TableField(value = "organizers", typeHandler = JacksonTypeHandler.class)
private Object organizers;
@TableField(value = "co_organizers", typeHandler = JacksonTypeHandler.class)
private Object coOrganizers;
@TableField(value = "sponsors", typeHandler = JacksonTypeHandler.class)
private Object sponsors;
// ====== 报名配置 ======
@TableField("register_start_time")
private LocalDateTime registerStartTime;
@TableField("register_end_time")
private LocalDateTime registerEndTime;
@TableField("register_state")
private String registerState;
@TableField("require_audit")
private Boolean requireAudit;
@TableField(value = "allowed_grades", typeHandler = JacksonTypeHandler.class)
private List<Integer> allowedGrades;
@TableField(value = "allowed_classes", typeHandler = JacksonTypeHandler.class)
private List<Integer> allowedClasses;
@TableField("team_min_members")
private Integer teamMinMembers;
@TableField("team_max_members")
private Integer teamMaxMembers;
// ====== 目标筛选 ======
@TableField(value = "target_cities", typeHandler = JacksonTypeHandler.class)
private List<String> targetCities;
@TableField("age_min")
private Integer ageMin;
@TableField("age_max")
private Integer ageMax;
// ====== 提交配置 ======
@TableField("submit_rule")
private String submitRule;
@TableField("submit_start_time")
private LocalDateTime submitStartTime;
@TableField("submit_end_time")
private LocalDateTime submitEndTime;
@TableField("work_type")
private String workType;
@TableField("work_requirement")
private String workRequirement;
// ====== 评审配置 ======
@TableField("review_rule_id")
private Long reviewRuleId;
@TableField("review_start_time")
private LocalDateTime reviewStartTime;
@TableField("review_end_time")
private LocalDateTime reviewEndTime;
// ====== 成果发布 ======
@TableField("result_state")
private String resultState;
@TableField("result_publish_time")
private LocalDateTime resultPublishTime;
}

View File

@ -0,0 +1,29 @@
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 lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("t_contest_attachment")
public class BizContestAttachment extends BaseEntity {
@TableField("contest_id")
private Long contestId;
@TableField("file_name")
private String fileName;
@TableField("file_url")
private String fileUrl;
private String format;
@TableField("file_type")
private String fileType;
private String size;
}

View File

@ -0,0 +1,31 @@
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 lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("t_contest_notice")
public class BizContestNotice extends BaseEntity {
@TableField("contest_id")
private Long contestId;
private String title;
private String content;
/** system/manual/urgent */
@TableField("notice_type")
private String noticeType;
private Integer priority;
@TableField("publish_time")
private LocalDateTime publishTime;
}

View File

@ -0,0 +1,71 @@
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 lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("t_contest_registration")
public class BizContestRegistration extends BaseEntity {
@TableField("contest_id")
private Long contestId;
@TableField("tenant_id")
private Long tenantId;
/** 报名类型individual/team */
@TableField("registration_type")
private String registrationType;
@TableField("team_id")
private Long teamId;
/** 团队名称快照 */
@TableField("team_name")
private String teamName;
@TableField("user_id")
private Long userId;
/** 账号快照 */
@TableField("account_no")
private String accountNo;
@TableField("account_name")
private String accountName;
/** 角色快照leader/member/mentor */
private String role;
/** 报名状态pending/passed/rejected/withdrawn */
@TableField("registration_state")
private String registrationState;
/** 参与者类型self/child */
@TableField("participant_type")
private String participantType;
@TableField("child_id")
private Long childId;
/** 实际提交人 ID */
private Integer registrant;
@TableField("registration_time")
private LocalDateTime registrationTime;
/** 审核原因 */
private String reason;
/** 审核操作人 */
private Integer operator;
@TableField("operation_date")
private LocalDateTime operationDate;
}

View File

@ -0,0 +1,36 @@
package com.competition.modules.biz.contest.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@TableName("t_contest_registration_teacher")
public class BizContestRegistrationTeacher implements Serializable {
@TableId(type = IdType.AUTO)
private Long id;
@TableField("registration_id")
private Long registrationId;
@TableField("tenant_id")
private Long tenantId;
@TableField("user_id")
private Long userId;
@TableField("is_default")
private Boolean isDefault;
private Integer creator;
private Integer modifier;
@TableField("create_time")
private LocalDateTime createTime;
@TableField("modify_time")
private LocalDateTime modifyTime;
}

View File

@ -0,0 +1,28 @@
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 lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("t_contest_team")
public class BizContestTeam extends BaseEntity {
@TableField("tenant_id")
private Long tenantId;
@TableField("contest_id")
private Long contestId;
@TableField("team_name")
private String teamName;
@TableField("leader_user_id")
private Long leaderUserId;
@TableField("max_members")
private Integer maxMembers;
}

View File

@ -0,0 +1,36 @@
package com.competition.modules.biz.contest.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@TableName("t_contest_team_member")
public class BizContestTeamMember implements Serializable {
@TableId(type = IdType.AUTO)
private Long id;
@TableField("tenant_id")
private Long tenantId;
@TableField("team_id")
private Long teamId;
@TableField("user_id")
private Long userId;
/** 角色member/leader/mentor */
private String role;
private Integer creator;
private Integer modifier;
@TableField("create_time")
private LocalDateTime createTime;
@TableField("modify_time")
private LocalDateTime modifyTime;
}

View File

@ -0,0 +1,88 @@
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 lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName(value = "t_contest_work", autoResultMap = true)
public class BizContestWork extends BaseEntity {
@TableField("tenant_id")
private Long tenantId;
@TableField("contest_id")
private Long contestId;
@TableField("registration_id")
private Long registrationId;
/** 作品编号(唯一展示编号) */
@TableField("work_no")
private String workNo;
private String title;
private String description;
@TableField(value = "files", typeHandler = JacksonTypeHandler.class)
private Object files;
private Integer version;
@TableField("is_latest")
private Boolean isLatest;
/** submitted/locked/reviewing/rejected/accepted */
private String status;
@TableField("submit_time")
private LocalDateTime submitTime;
@TableField("submitter_user_id")
private Long submitterUserId;
@TableField("submitter_account_no")
private String submitterAccountNo;
/** teacher/student/team_leader */
@TableField("submit_source")
private String submitSource;
@TableField("preview_url")
private String previewUrl;
@TableField(value = "preview_urls", typeHandler = JacksonTypeHandler.class)
private List<String> previewUrls;
@TableField(value = "ai_model_meta", typeHandler = JacksonTypeHandler.class)
private Object aiModelMeta;
@TableField("user_work_id")
private Long userWorkId;
// ====== 赛果字段 ======
@TableField("final_score")
private BigDecimal finalScore;
@TableField("`rank`")
private Integer rank;
/** first/second/third/excellent/none */
@TableField("award_level")
private String awardLevel;
@TableField("award_name")
private String awardName;
@TableField("certificate_url")
private String certificateUrl;
}

View File

@ -0,0 +1,46 @@
package com.competition.modules.biz.contest.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@TableName("t_contest_work_attachment")
public class BizContestWorkAttachment implements Serializable {
@TableId(type = IdType.AUTO)
private Long id;
@TableField("tenant_id")
private Long tenantId;
@TableField("contest_id")
private Long contestId;
@TableField("work_id")
private Long workId;
@TableField("file_name")
private String fileName;
@TableField("file_url")
private String fileUrl;
private String format;
@TableField("file_type")
private String fileType;
private String size;
private Integer creator;
private Integer modifier;
@TableField("create_time")
private LocalDateTime createTime;
@TableField("modify_time")
private LocalDateTime modifyTime;
}

View File

@ -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> {
}

View File

@ -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> {
}

View File

@ -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> {
}

View File

@ -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> {
}

View File

@ -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> {
}

View File

@ -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> {
}

View File

@ -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> {
}

View File

@ -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> {
}

View File

@ -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> {
}

View File

@ -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> {
}

View File

@ -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.BizContestNotice;
public interface IContestNoticeService extends IService<BizContestNotice> {
}

View File

@ -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);
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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -0,0 +1,27 @@
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> {
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);
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);
}

View File

@ -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 {
}

View File

@ -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.BizContestNotice;
import com.competition.modules.biz.contest.mapper.ContestNoticeMapper;
import com.competition.modules.biz.contest.service.IContestNoticeService;
import org.springframework.stereotype.Service;
@Service
public class ContestNoticeServiceImpl extends ServiceImpl<ContestNoticeMapper, BizContestNotice> implements IContestNoticeService {
}

View File

@ -0,0 +1,332 @@
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.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 (!"published".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("pending");
registration.setParticipantType("self");
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) {
log.info("获取报名统计赛事ID{}", contestId);
LambdaQueryWrapper<BizContestRegistration> baseWrapper = new LambdaQueryWrapper<>();
if (contestId != null) {
baseWrapper.eq(BizContestRegistration::getContestId, contestId);
}
long total = count(baseWrapper);
LambdaQueryWrapper<BizContestRegistration> pendingWrapper = new LambdaQueryWrapper<>();
if (contestId != null) {
pendingWrapper.eq(BizContestRegistration::getContestId, contestId);
}
pendingWrapper.eq(BizContestRegistration::getRegistrationState, "pending");
long pending = count(pendingWrapper);
LambdaQueryWrapper<BizContestRegistration> passedWrapper = new LambdaQueryWrapper<>();
if (contestId != null) {
passedWrapper.eq(BizContestRegistration::getContestId, contestId);
}
passedWrapper.eq(BizContestRegistration::getRegistrationState, "passed");
long passed = count(passedWrapper);
LambdaQueryWrapper<BizContestRegistration> rejectedWrapper = new LambdaQueryWrapper<>();
if (contestId != null) {
rejectedWrapper.eq(BizContestRegistration::getContestId, contestId);
}
rejectedWrapper.eq(BizContestRegistration::getRegistrationState, "rejected");
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("pending");
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;
}
}

View File

@ -0,0 +1,463 @@
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.exception.BusinessException;
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 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 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 static final DateTimeFormatter DT_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
@Override
public BizContest createContest(CreateContestDto dto, Long creatorId) {
log.info("开始创建赛事,名称:{}", dto.getContestName());
BizContest entity = new BizContest();
mapDtoToEntity(dto, entity);
// 默认状态
entity.setContestState("unpublished");
entity.setStatus("ongoing");
entity.setResultState("unpublished");
if (!StringUtils.hasText(entity.getSubmitRule())) {
entity.setSubmitRule("once");
}
entity.setCreator(creatorId != null ? creatorId.intValue() : null);
save(entity);
log.info("赛事创建成功ID{}", entity.getId());
return entity;
}
@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());
}
// 阶段筛选
if (StringUtils.hasText(dto.getStage())) {
LocalDateTime now = LocalDateTime.now();
switch (dto.getStage()) {
case "registering":
wrapper.le(BizContest::getRegisterStartTime, now)
.ge(BizContest::getRegisterEndTime, now);
break;
case "submitting":
wrapper.le(BizContest::getSubmitStartTime, now)
.ge(BizContest::getSubmitEndTime, now);
break;
case "reviewing":
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<Map<String, Object>> voList = result.getRecords().stream()
.map(this::entityToMap)
.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, "published");
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);
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, "赛事不存在");
}
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, "赛事不存在");
}
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, "赛事不存在");
}
entity.setStatus("finished");
updateById(entity);
log.info("赛事已结束ID{}", id);
}
@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, "published");
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(LocalDateTime.parse(dto.getStartTime(), DT_FORMATTER));
}
if (StringUtils.hasText(dto.getEndTime())) {
entity.setEndTime(LocalDateTime.parse(dto.getEndTime(), DT_FORMATTER));
}
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(LocalDateTime.parse(dto.getRegisterStartTime(), DT_FORMATTER));
}
if (StringUtils.hasText(dto.getRegisterEndTime())) {
entity.setRegisterEndTime(LocalDateTime.parse(dto.getRegisterEndTime(), DT_FORMATTER));
}
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(LocalDateTime.parse(dto.getSubmitStartTime(), DT_FORMATTER));
}
if (StringUtils.hasText(dto.getSubmitEndTime())) {
entity.setSubmitEndTime(LocalDateTime.parse(dto.getSubmitEndTime(), DT_FORMATTER));
}
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(LocalDateTime.parse(dto.getReviewStartTime(), DT_FORMATTER));
}
if (StringUtils.hasText(dto.getReviewEndTime())) {
entity.setReviewEndTime(LocalDateTime.parse(dto.getReviewEndTime(), DT_FORMATTER));
}
if (StringUtils.hasText(dto.getResultPublishTime())) {
entity.setResultPublishTime(LocalDateTime.parse(dto.getResultPublishTime(), DT_FORMATTER));
}
}
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;
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,388 @@
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.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 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.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;
@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 (!"passed".equals(registration.getRegistrationState())) {
throw BusinessException.of(ErrorCode.BAD_REQUEST, "报名未通过审核,无法提交作品");
}
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 = generateWorkNo(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("submitted");
work.setSubmitTime(LocalDateTime.now());
work.setSubmitterUserId(submitterId);
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);
}
if (StringUtils.hasText(dto.getKeyword())) {
wrapper.and(w -> w.like(BizContestWork::getTitle, dto.getKeyword())
.or().like(BizContestWork::getWorkNo, dto.getKeyword()));
}
if (StringUtils.hasText(dto.getSubmitStartTime())) {
wrapper.ge(BizContestWork::getSubmitTime,
LocalDateTime.parse(dto.getSubmitStartTime(), DateTimeFormatter.ISO_LOCAL_DATE_TIME));
}
if (StringUtils.hasText(dto.getSubmitEndTime())) {
wrapper.le(BizContestWork::getSubmitTime,
LocalDateTime.parse(dto.getSubmitEndTime(), DateTimeFormatter.ISO_LOCAL_DATE_TIME));
}
// 默认只查最新版本
wrapper.eq(BizContestWork::getIsLatest, true);
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());
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, BizContestRegistration> finalRegistrationMap = registrationMap;
List<Map<String, Object>> voList = result.getRecords().stream()
.map(work -> {
Map<String, Object> map = workToMap(work);
BizContestRegistration reg = finalRegistrationMap.get(work.getRegistrationId());
if (reg != null) {
map.put("accountNo", reg.getAccountNo());
map.put("accountName", reg.getAccountName());
map.put("userId", reg.getUserId());
}
return map;
})
.collect(Collectors.toList());
return PageResult.from(result, voList);
}
@Override
public Map<String, Object> getStats(Long contestId, Long tenantId) {
log.info("获取作品统计赛事ID{}", contestId);
LambdaQueryWrapper<BizContestWork> baseWrapper = new LambdaQueryWrapper<>();
if (contestId != null) {
baseWrapper.eq(BizContestWork::getContestId, contestId);
}
baseWrapper.eq(BizContestWork::getIsLatest, true);
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::getStatus, "submitted");
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::getStatus, "reviewing");
long reviewing = count(reviewingWrapper);
LambdaQueryWrapper<BizContestWork> acceptedWrapper = new LambdaQueryWrapper<>();
if (contestId != null) {
acceptedWrapper.eq(BizContestWork::getContestId, contestId);
}
acceptedWrapper.eq(BizContestWork::getIsLatest, true);
acceptedWrapper.eq(BizContestWork::getStatus, "accepted");
long accepted = count(acceptedWrapper);
LambdaQueryWrapper<BizContestWork> rejectedWrapper = new LambdaQueryWrapper<>();
if (contestId != null) {
rejectedWrapper.eq(BizContestWork::getContestId, contestId);
}
rejectedWrapper.eq(BizContestWork::getIsLatest, true);
rejectedWrapper.eq(BizContestWork::getStatus, "rejected");
long rejected = count(rejectedWrapper);
Map<String, Object> stats = new LinkedHashMap<>();
stats.put("total", total);
stats.put("submitted", submitted);
stats.put("reviewing", reviewing);
stats.put("accepted", accepted);
stats.put("rejected", rejected);
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);
}
// ====== 私有辅助方法 ======
private String generateWorkNo(Long contestId) {
LambdaQueryWrapper<BizContestWork> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(BizContestWork::getContestId, contestId);
long count = count(wrapper);
return "W" + contestId + "-" + (count + 1);
}
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;
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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));
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -0,0 +1,44 @@
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 lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName(value = "t_homework", autoResultMap = true)
public class BizHomework extends BaseEntity {
@TableField("tenant_id")
private Long tenantId;
private String name;
private String content;
@TableField("submit_start_time")
private LocalDateTime submitStartTime;
@TableField("submit_end_time")
private LocalDateTime submitEndTime;
@TableField(value = "attachments", typeHandler = JacksonTypeHandler.class)
private Object attachments;
@TableField(value = "publish_scope", typeHandler = JacksonTypeHandler.class)
private Object publishScope;
@TableField("review_rule_id")
private Long reviewRuleId;
/** unpublished / published */
private String status;
@TableField("publish_time")
private LocalDateTime publishTime;
}

View File

@ -0,0 +1,25 @@
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 lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName(value = "t_homework_review_rule", autoResultMap = true)
public class BizHomeworkReviewRule extends BaseEntity {
@TableField("tenant_id")
private Long tenantId;
private String name;
private String description;
/** JSON array of {name, maxScore, description} */
@TableField(value = "criteria", typeHandler = JacksonTypeHandler.class)
private Object criteria;
}

View File

@ -0,0 +1,37 @@
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 lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName(value = "t_homework_score", autoResultMap = true)
public class BizHomeworkScore extends BaseEntity {
@TableField("tenant_id")
private Long tenantId;
@TableField("submission_id")
private Long submissionId;
@TableField("reviewer_id")
private Long reviewerId;
@TableField(value = "dimension_scores", typeHandler = JacksonTypeHandler.class)
private Object dimensionScores;
@TableField("total_score")
private BigDecimal totalScore;
private String comments;
@TableField("score_time")
private LocalDateTime scoreTime;
}

View File

@ -0,0 +1,50 @@
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 lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName(value = "t_homework_submission", autoResultMap = true)
public class BizHomeworkSubmission extends BaseEntity {
@TableField("tenant_id")
private Long tenantId;
@TableField("homework_id")
private Long homeworkId;
@TableField("student_id")
private Long studentId;
@TableField("work_no")
private String workNo;
@TableField("work_name")
private String workName;
@TableField("work_description")
private String workDescription;
@TableField(value = "files", typeHandler = JacksonTypeHandler.class)
private Object files;
@TableField(value = "attachments", typeHandler = JacksonTypeHandler.class)
private Object attachments;
@TableField("submit_time")
private LocalDateTime submitTime;
/** pending / reviewed */
private String status;
@TableField("total_score")
private BigDecimal totalScore;
}

View File

@ -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> {
}

View File

@ -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> {
}

View File

@ -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> {
}

View File

@ -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> {
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -0,0 +1,28 @@
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.CreateHomeworkDto;
import com.competition.modules.biz.homework.entity.BizHomework;
import java.util.List;
import java.util.Map;
public interface IHomeworkService extends IService<BizHomework> {
BizHomework create(CreateHomeworkDto dto, Long tenantId);
PageResult<Map<String, Object>> findAll(Long page, Long pageSize, Long tenantId, String name, String status);
PageResult<Map<String, Object>> findMyHomeworks(Long page, Long pageSize, Long userId, Long tenantId, String name);
Map<String, Object> findDetail(Long id);
BizHomework update(Long id, CreateHomeworkDto dto);
void publish(Long id, List<Integer> publishScope);
void unpublish(Long id);
void remove(Long id);
}

View File

@ -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.CreateSubmissionDto;
import com.competition.modules.biz.homework.entity.BizHomeworkSubmission;
import java.util.List;
import java.util.Map;
public interface IHomeworkSubmissionService extends IService<BizHomeworkSubmission> {
BizHomeworkSubmission create(CreateSubmissionDto dto, Long studentId, Long tenantId);
PageResult<Map<String, Object>> findAll(Long page, Long pageSize, Long tenantId, Long homeworkId,
String workNo, String workName, String studentAccount,
String studentName, String status);
Map<String, Object> findDetail(Long id);
Map<String, Object> findMySubmission(Long homeworkId, Long userId);
List<Map<String, Object>> getClassTree(Long tenantId);
}

View File

@ -0,0 +1,139 @@
package com.competition.modules.biz.homework.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.exception.BusinessException;
import com.competition.common.result.PageResult;
import com.competition.modules.biz.homework.dto.CreateHomeworkReviewRuleDto;
import com.competition.modules.biz.homework.entity.BizHomeworkReviewRule;
import com.competition.modules.biz.homework.mapper.HomeworkReviewRuleMapper;
import com.competition.modules.biz.homework.service.IHomeworkReviewRuleService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.*;
import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
public class HomeworkReviewRuleServiceImpl extends ServiceImpl<HomeworkReviewRuleMapper, BizHomeworkReviewRule> implements IHomeworkReviewRuleService {
private final HomeworkReviewRuleMapper homeworkReviewRuleMapper;
@Override
public BizHomeworkReviewRule create(CreateHomeworkReviewRuleDto dto, Long tenantId) {
log.info("创建作业评审规则,名称:{},租户:{}", dto.getName(), tenantId);
BizHomeworkReviewRule entity = new BizHomeworkReviewRule();
entity.setTenantId(tenantId);
entity.setName(dto.getName());
entity.setDescription(dto.getDescription());
entity.setCriteria(dto.getCriteria());
save(entity);
log.info("作业评审规则创建成功ID{}", entity.getId());
return entity;
}
@Override
public PageResult<Map<String, Object>> findAll(Long page, Long pageSize, Long tenantId, String name) {
log.info("查询作业评审规则列表,页码:{},每页:{}", page, pageSize);
LambdaQueryWrapper<BizHomeworkReviewRule> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(BizHomeworkReviewRule::getValidState, 1);
if (tenantId != null) {
wrapper.eq(BizHomeworkReviewRule::getTenantId, tenantId);
}
if (StringUtils.hasText(name)) {
wrapper.like(BizHomeworkReviewRule::getName, name);
}
wrapper.orderByDesc(BizHomeworkReviewRule::getCreateTime);
Page<BizHomeworkReviewRule> pageObj = new Page<>(page, pageSize);
Page<BizHomeworkReviewRule> result = homeworkReviewRuleMapper.selectPage(pageObj, wrapper);
List<Map<String, Object>> voList = result.getRecords().stream()
.map(this::entityToMap)
.collect(Collectors.toList());
return PageResult.from(result, voList);
}
@Override
public List<Map<String, Object>> findAllForSelect(Long tenantId) {
log.info("查询作业评审规则下拉列表,租户:{}", tenantId);
LambdaQueryWrapper<BizHomeworkReviewRule> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(BizHomeworkReviewRule::getValidState, 1);
if (tenantId != null) {
wrapper.eq(BizHomeworkReviewRule::getTenantId, tenantId);
}
wrapper.orderByDesc(BizHomeworkReviewRule::getCreateTime);
List<BizHomeworkReviewRule> list = homeworkReviewRuleMapper.selectList(wrapper);
return list.stream().map(this::entityToMap).collect(Collectors.toList());
}
@Override
public Map<String, Object> findDetail(Long id) {
log.info("查询作业评审规则详情ID{}", id);
BizHomeworkReviewRule entity = getById(id);
if (entity == null) {
throw BusinessException.of(ErrorCode.NOT_FOUND, "作业评审规则不存在");
}
return entityToMap(entity);
}
@Override
public BizHomeworkReviewRule update(Long id, CreateHomeworkReviewRuleDto dto) {
log.info("更新作业评审规则ID{}", id);
BizHomeworkReviewRule entity = getById(id);
if (entity == null) {
throw BusinessException.of(ErrorCode.NOT_FOUND, "作业评审规则不存在");
}
if (StringUtils.hasText(dto.getName())) {
entity.setName(dto.getName());
}
if (dto.getDescription() != null) {
entity.setDescription(dto.getDescription());
}
if (dto.getCriteria() != null) {
entity.setCriteria(dto.getCriteria());
}
updateById(entity);
log.info("作业评审规则更新成功ID{}", id);
return entity;
}
@Override
public void remove(Long id) {
log.info("删除作业评审规则ID{}", id);
removeById(id);
log.info("作业评审规则删除成功ID{}", id);
}
// ====== 私有辅助方法 ======
private Map<String, Object> entityToMap(BizHomeworkReviewRule entity) {
Map<String, Object> map = new LinkedHashMap<>();
map.put("id", entity.getId());
map.put("tenantId", entity.getTenantId());
map.put("name", entity.getName());
map.put("description", entity.getDescription());
map.put("criteria", entity.getCriteria());
map.put("createTime", entity.getCreateTime());
map.put("modifyTime", entity.getModifyTime());
return map;
}
}

View File

@ -0,0 +1,93 @@
package com.competition.modules.biz.homework.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.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.mapper.HomeworkScoreMapper;
import com.competition.modules.biz.homework.service.IHomeworkScoreService;
import com.competition.modules.biz.homework.service.IHomeworkSubmissionService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
public class HomeworkScoreServiceImpl extends ServiceImpl<HomeworkScoreMapper, BizHomeworkScore> implements IHomeworkScoreService {
private final HomeworkScoreMapper homeworkScoreMapper;
@Lazy
private final IHomeworkSubmissionService homeworkSubmissionService;
@Override
@Transactional
public BizHomeworkScore create(CreateHomeworkScoreDto dto, Long reviewerId, Long tenantId) {
log.info("创建作业评分提交ID{},评审人:{}", dto.getSubmissionId(), reviewerId);
// 验证提交存在
BizHomeworkSubmission submission = homeworkSubmissionService.getById(dto.getSubmissionId());
if (submission == null) {
throw BusinessException.of(ErrorCode.NOT_FOUND, "作业提交不存在");
}
BizHomeworkScore entity = new BizHomeworkScore();
entity.setTenantId(tenantId);
entity.setSubmissionId(dto.getSubmissionId());
entity.setReviewerId(reviewerId);
entity.setDimensionScores(dto.getDimensionScores());
entity.setTotalScore(dto.getTotalScore());
entity.setComments(dto.getComments());
entity.setScoreTime(LocalDateTime.now());
save(entity);
log.info("作业评分创建成功ID{}", entity.getId());
// 更新提交的总分和状态
submission.setTotalScore(dto.getTotalScore());
submission.setStatus("reviewed");
homeworkSubmissionService.updateById(submission);
log.info("作业提交状态已更新为reviewed提交ID{}", submission.getId());
return entity;
}
@Override
public List<Map<String, Object>> findBySubmission(Long submissionId) {
log.info("查询作业提交的评分记录提交ID{}", submissionId);
LambdaQueryWrapper<BizHomeworkScore> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(BizHomeworkScore::getSubmissionId, submissionId);
wrapper.eq(BizHomeworkScore::getValidState, 1);
wrapper.orderByDesc(BizHomeworkScore::getScoreTime);
List<BizHomeworkScore> list = homeworkScoreMapper.selectList(wrapper);
return list.stream().map(this::entityToMap).collect(Collectors.toList());
}
// ====== 私有辅助方法 ======
private Map<String, Object> entityToMap(BizHomeworkScore entity) {
Map<String, Object> map = new LinkedHashMap<>();
map.put("id", entity.getId());
map.put("tenantId", entity.getTenantId());
map.put("submissionId", entity.getSubmissionId());
map.put("reviewerId", entity.getReviewerId());
map.put("dimensionScores", entity.getDimensionScores());
map.put("totalScore", entity.getTotalScore());
map.put("comments", entity.getComments());
map.put("scoreTime", entity.getScoreTime());
map.put("createTime", entity.getCreateTime());
map.put("modifyTime", entity.getModifyTime());
return map;
}
}

View File

@ -0,0 +1,216 @@
package com.competition.modules.biz.homework.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.exception.BusinessException;
import com.competition.common.result.PageResult;
import com.competition.modules.biz.homework.dto.CreateHomeworkDto;
import com.competition.modules.biz.homework.entity.BizHomework;
import com.competition.modules.biz.homework.mapper.HomeworkMapper;
import com.competition.modules.biz.homework.service.IHomeworkService;
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 HomeworkServiceImpl extends ServiceImpl<HomeworkMapper, BizHomework> implements IHomeworkService {
private final HomeworkMapper homeworkMapper;
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public BizHomework create(CreateHomeworkDto dto, Long tenantId) {
log.info("创建作业,名称:{},租户:{}", dto.getName(), tenantId);
BizHomework entity = new BizHomework();
entity.setTenantId(tenantId);
entity.setName(dto.getName());
entity.setContent(dto.getContent());
entity.setSubmitStartTime(LocalDateTime.parse(dto.getSubmitStartTime(), DATE_TIME_FORMATTER));
entity.setSubmitEndTime(LocalDateTime.parse(dto.getSubmitEndTime(), DATE_TIME_FORMATTER));
entity.setAttachments(dto.getAttachments());
entity.setPublishScope(dto.getPublishScope());
entity.setReviewRuleId(dto.getReviewRuleId());
entity.setStatus("unpublished");
save(entity);
log.info("作业创建成功ID{}", entity.getId());
return entity;
}
@Override
public PageResult<Map<String, Object>> findAll(Long page, Long pageSize, Long tenantId, String name, String status) {
log.info("查询作业列表,页码:{},每页:{}", page, pageSize);
LambdaQueryWrapper<BizHomework> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(BizHomework::getValidState, 1);
if (tenantId != null) {
wrapper.eq(BizHomework::getTenantId, tenantId);
}
if (StringUtils.hasText(name)) {
wrapper.like(BizHomework::getName, name);
}
if (StringUtils.hasText(status)) {
wrapper.eq(BizHomework::getStatus, status);
}
wrapper.orderByDesc(BizHomework::getCreateTime);
Page<BizHomework> pageObj = new Page<>(page, pageSize);
Page<BizHomework> result = homeworkMapper.selectPage(pageObj, wrapper);
List<Map<String, Object>> voList = result.getRecords().stream()
.map(this::entityToMap)
.collect(Collectors.toList());
return PageResult.from(result, voList);
}
@Override
public PageResult<Map<String, Object>> findMyHomeworks(Long page, Long pageSize, Long userId, Long tenantId, String name) {
log.info("查询我的作业列表,用户:{},租户:{}", userId, tenantId);
LambdaQueryWrapper<BizHomework> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(BizHomework::getValidState, 1);
wrapper.eq(BizHomework::getStatus, "published");
if (tenantId != null) {
wrapper.eq(BizHomework::getTenantId, tenantId);
}
if (StringUtils.hasText(name)) {
wrapper.like(BizHomework::getName, name);
}
wrapper.orderByDesc(BizHomework::getPublishTime);
Page<BizHomework> pageObj = new Page<>(page, pageSize);
Page<BizHomework> result = homeworkMapper.selectPage(pageObj, 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);
BizHomework entity = getById(id);
if (entity == null) {
throw BusinessException.of(ErrorCode.NOT_FOUND, "作业不存在");
}
return entityToMap(entity);
}
@Override
public BizHomework update(Long id, CreateHomeworkDto dto) {
log.info("更新作业ID{}", id);
BizHomework entity = getById(id);
if (entity == null) {
throw BusinessException.of(ErrorCode.NOT_FOUND, "作业不存在");
}
if (StringUtils.hasText(dto.getName())) {
entity.setName(dto.getName());
}
if (dto.getContent() != null) {
entity.setContent(dto.getContent());
}
if (StringUtils.hasText(dto.getSubmitStartTime())) {
entity.setSubmitStartTime(LocalDateTime.parse(dto.getSubmitStartTime(), DATE_TIME_FORMATTER));
}
if (StringUtils.hasText(dto.getSubmitEndTime())) {
entity.setSubmitEndTime(LocalDateTime.parse(dto.getSubmitEndTime(), DATE_TIME_FORMATTER));
}
if (dto.getAttachments() != null) {
entity.setAttachments(dto.getAttachments());
}
if (dto.getPublishScope() != null) {
entity.setPublishScope(dto.getPublishScope());
}
if (dto.getReviewRuleId() != null) {
entity.setReviewRuleId(dto.getReviewRuleId());
}
updateById(entity);
log.info("作业更新成功ID{}", id);
return entity;
}
@Override
public void publish(Long id, List<Integer> publishScope) {
log.info("发布作业ID{}", id);
BizHomework entity = getById(id);
if (entity == null) {
throw BusinessException.of(ErrorCode.NOT_FOUND, "作业不存在");
}
entity.setStatus("published");
entity.setPublishTime(LocalDateTime.now());
if (publishScope != null) {
entity.setPublishScope(publishScope);
}
updateById(entity);
log.info("作业发布成功ID{}", id);
}
@Override
public void unpublish(Long id) {
log.info("取消发布作业ID{}", id);
BizHomework entity = getById(id);
if (entity == null) {
throw BusinessException.of(ErrorCode.NOT_FOUND, "作业不存在");
}
entity.setStatus("unpublished");
entity.setPublishTime(null);
updateById(entity);
log.info("作业取消发布成功ID{}", id);
}
@Override
public void remove(Long id) {
log.info("删除作业ID{}", id);
removeById(id);
log.info("作业删除成功ID{}", id);
}
// ====== 私有辅助方法 ======
private Map<String, Object> entityToMap(BizHomework entity) {
Map<String, Object> map = new LinkedHashMap<>();
map.put("id", entity.getId());
map.put("tenantId", entity.getTenantId());
map.put("name", entity.getName());
map.put("content", entity.getContent());
map.put("submitStartTime", entity.getSubmitStartTime());
map.put("submitEndTime", entity.getSubmitEndTime());
map.put("attachments", entity.getAttachments());
map.put("publishScope", entity.getPublishScope());
map.put("reviewRuleId", entity.getReviewRuleId());
map.put("status", entity.getStatus());
map.put("publishTime", entity.getPublishTime());
map.put("createTime", entity.getCreateTime());
map.put("modifyTime", entity.getModifyTime());
return map;
}
}

View File

@ -0,0 +1,162 @@
package com.competition.modules.biz.homework.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.exception.BusinessException;
import com.competition.common.result.PageResult;
import com.competition.modules.biz.homework.dto.CreateSubmissionDto;
import com.competition.modules.biz.homework.entity.BizHomework;
import com.competition.modules.biz.homework.entity.BizHomeworkSubmission;
import com.competition.modules.biz.homework.mapper.HomeworkSubmissionMapper;
import com.competition.modules.biz.homework.service.IHomeworkService;
import com.competition.modules.biz.homework.service.IHomeworkSubmissionService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
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 HomeworkSubmissionServiceImpl extends ServiceImpl<HomeworkSubmissionMapper, BizHomeworkSubmission> implements IHomeworkSubmissionService {
private final HomeworkSubmissionMapper homeworkSubmissionMapper;
@Lazy
private final IHomeworkService homeworkService;
@Override
public BizHomeworkSubmission create(CreateSubmissionDto dto, Long studentId, Long tenantId) {
log.info("提交作业作业ID{}学生ID{}", dto.getHomeworkId(), studentId);
// 验证作业已发布
BizHomework homework = homeworkService.getById(dto.getHomeworkId());
if (homework == null) {
throw BusinessException.of(ErrorCode.NOT_FOUND, "作业不存在");
}
if (!"published".equals(homework.getStatus())) {
throw BusinessException.of(ErrorCode.BAD_REQUEST, "作业未发布,无法提交");
}
// 检查是否已提交过
LambdaQueryWrapper<BizHomeworkSubmission> checkWrapper = new LambdaQueryWrapper<>();
checkWrapper.eq(BizHomeworkSubmission::getHomeworkId, dto.getHomeworkId());
checkWrapper.eq(BizHomeworkSubmission::getStudentId, studentId);
checkWrapper.eq(BizHomeworkSubmission::getValidState, 1);
long existCount = count(checkWrapper);
if (existCount > 0) {
throw BusinessException.of(ErrorCode.CONFLICT, "您已提交过该作业");
}
BizHomeworkSubmission entity = new BizHomeworkSubmission();
entity.setTenantId(tenantId);
entity.setHomeworkId(dto.getHomeworkId());
entity.setStudentId(studentId);
entity.setWorkName(dto.getWorkName());
entity.setWorkDescription(dto.getWorkDescription());
entity.setFiles(dto.getFiles());
entity.setSubmitTime(LocalDateTime.now());
entity.setStatus("pending");
save(entity);
log.info("作业提交成功ID{}", entity.getId());
return entity;
}
@Override
public PageResult<Map<String, Object>> findAll(Long page, Long pageSize, Long tenantId, Long homeworkId,
String workNo, String workName, String studentAccount,
String studentName, String status) {
log.info("查询作业提交列表,页码:{},每页:{}", page, pageSize);
LambdaQueryWrapper<BizHomeworkSubmission> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(BizHomeworkSubmission::getValidState, 1);
if (tenantId != null) {
wrapper.eq(BizHomeworkSubmission::getTenantId, tenantId);
}
if (homeworkId != null) {
wrapper.eq(BizHomeworkSubmission::getHomeworkId, homeworkId);
}
if (StringUtils.hasText(workNo)) {
wrapper.like(BizHomeworkSubmission::getWorkNo, workNo);
}
if (StringUtils.hasText(workName)) {
wrapper.like(BizHomeworkSubmission::getWorkName, workName);
}
if (StringUtils.hasText(status)) {
wrapper.eq(BizHomeworkSubmission::getStatus, status);
}
wrapper.orderByDesc(BizHomeworkSubmission::getSubmitTime);
Page<BizHomeworkSubmission> pageObj = new Page<>(page, pageSize);
Page<BizHomeworkSubmission> result = homeworkSubmissionMapper.selectPage(pageObj, 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);
BizHomeworkSubmission entity = getById(id);
if (entity == null) {
throw BusinessException.of(ErrorCode.NOT_FOUND, "作业提交不存在");
}
return entityToMap(entity);
}
@Override
public Map<String, Object> findMySubmission(Long homeworkId, Long userId) {
log.info("查询我的作业提交作业ID{}用户ID{}", homeworkId, userId);
LambdaQueryWrapper<BizHomeworkSubmission> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(BizHomeworkSubmission::getHomeworkId, homeworkId);
wrapper.eq(BizHomeworkSubmission::getStudentId, userId);
wrapper.eq(BizHomeworkSubmission::getValidState, 1);
BizHomeworkSubmission entity = getOne(wrapper);
if (entity == null) {
return null;
}
return entityToMap(entity);
}
@Override
public List<Map<String, Object>> getClassTree(Long tenantId) {
log.info("获取班级树,租户:{}(学校模块已剥离,返回空列表)", tenantId);
return Collections.emptyList();
}
// ====== 私有辅助方法 ======
private Map<String, Object> entityToMap(BizHomeworkSubmission entity) {
Map<String, Object> map = new LinkedHashMap<>();
map.put("id", entity.getId());
map.put("tenantId", entity.getTenantId());
map.put("homeworkId", entity.getHomeworkId());
map.put("studentId", entity.getStudentId());
map.put("workNo", entity.getWorkNo());
map.put("workName", entity.getWorkName());
map.put("workDescription", entity.getWorkDescription());
map.put("files", entity.getFiles());
map.put("attachments", entity.getAttachments());
map.put("submitTime", entity.getSubmitTime());
map.put("status", entity.getStatus());
map.put("totalScore", entity.getTotalScore());
map.put("createTime", entity.getCreateTime());
map.put("modifyTime", entity.getModifyTime());
return map;
}
}

View File

@ -0,0 +1,90 @@
package com.competition.modules.biz.judge.controller;
import com.competition.common.result.PageResult;
import com.competition.common.result.Result;
import com.competition.common.util.SecurityUtil;
import com.competition.modules.biz.judge.service.IJudgesManagementService;
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;
import java.util.Map;
@Tag(name = "评委库管理")
@RestController
@RequestMapping("/judges-management")
@RequiredArgsConstructor
public class JudgesManagementController {
private final IJudgesManagementService judgesManagementService;
@PostMapping
@RequirePermission("judge:create")
@Operation(summary = "创建评委账号")
public Result<Map<String, Object>> create(@RequestBody Map<String, Object> body) {
return Result.success(judgesManagementService.createJudge(body));
}
@GetMapping
@RequirePermission("judge:read")
@Operation(summary = "查询评委列表")
public Result<PageResult<Map<String, Object>>> findAll(
@RequestParam(defaultValue = "1") Long page,
@RequestParam(defaultValue = "10") Long pageSize,
@RequestParam(required = false) String keyword,
@RequestParam(required = false) String status) {
return Result.success(judgesManagementService.findAll(page, pageSize, keyword, status));
}
@GetMapping("/{id}")
@RequirePermission("judge:read")
@Operation(summary = "查询评委详情")
public Result<Map<String, Object>> findDetail(@PathVariable Long id) {
return Result.success(judgesManagementService.findDetail(id));
}
@PatchMapping("/{id}")
@RequirePermission("judge:update")
@Operation(summary = "更新评委信息")
public Result<Map<String, Object>> update(@PathVariable Long id, @RequestBody Map<String, Object> body) {
return Result.success(judgesManagementService.updateJudge(id, body));
}
@PatchMapping("/{id}/freeze")
@RequirePermission("judge:update")
@Operation(summary = "冻结评委账号")
public Result<Void> freeze(@PathVariable Long id) {
judgesManagementService.freeze(id);
return Result.success();
}
@PatchMapping("/{id}/unfreeze")
@RequirePermission("judge:update")
@Operation(summary = "解冻评委账号")
public Result<Void> unfreeze(@PathVariable Long id) {
judgesManagementService.unfreeze(id);
return Result.success();
}
@DeleteMapping("/{id}")
@RequirePermission("judge:delete")
@Operation(summary = "删除评委")
public Result<Void> remove(@PathVariable Long id) {
judgesManagementService.remove(id);
return Result.success();
}
@PostMapping("/batch-delete")
@RequirePermission("judge:delete")
@Operation(summary = "批量删除评委")
@SuppressWarnings("unchecked")
public Result<Void> batchDelete(@RequestBody Map<String, Object> body) {
List<Long> ids = ((List<Number>) body.get("ids"))
.stream().map(Number::longValue).toList();
judgesManagementService.batchDelete(ids);
return Result.success();
}
}

View File

@ -0,0 +1,49 @@
package com.competition.modules.biz.judge.service;
import com.competition.common.result.PageResult;
import java.util.List;
import java.util.Map;
public interface IJudgesManagementService {
/**
* 创建评委账号
*/
Map<String, Object> createJudge(Map<String, Object> params);
/**
* 分页查询评委列表
*/
PageResult<Map<String, Object>> findAll(Long page, Long pageSize, String keyword, String status);
/**
* 查询评委详情
*/
Map<String, Object> findDetail(Long id);
/**
* 更新评委信息
*/
Map<String, Object> updateJudge(Long id, Map<String, Object> params);
/**
* 冻结评委
*/
void freeze(Long id);
/**
* 解冻评委
*/
void unfreeze(Long id);
/**
* 删除评委
*/
void remove(Long id);
/**
* 批量删除评委
*/
void batchDelete(List<Long> ids);
}

View File

@ -0,0 +1,281 @@
package com.competition.modules.biz.judge.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.competition.common.enums.ErrorCode;
import com.competition.common.exception.BusinessException;
import com.competition.common.result.PageResult;
import com.competition.modules.biz.judge.service.IJudgesManagementService;
import com.competition.modules.sys.entity.SysRole;
import com.competition.modules.sys.entity.SysTenant;
import com.competition.modules.sys.entity.SysUser;
import com.competition.modules.sys.entity.SysUserRole;
import com.competition.modules.sys.mapper.SysRoleMapper;
import com.competition.modules.sys.mapper.SysTenantMapper;
import com.competition.modules.sys.mapper.SysUserMapper;
import com.competition.modules.sys.mapper.SysUserRoleMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
public class JudgesManagementServiceImpl implements IJudgesManagementService {
private final SysUserMapper sysUserMapper;
private final SysUserRoleMapper sysUserRoleMapper;
private final SysRoleMapper sysRoleMapper;
private final SysTenantMapper sysTenantMapper;
private final PasswordEncoder passwordEncoder;
/**
* 获取评委专属租户 ID
*/
private Long getJudgeTenantId() {
LambdaQueryWrapper<SysTenant> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysTenant::getCode, "judge");
SysTenant tenant = sysTenantMapper.selectOne(wrapper);
if (tenant == null) {
throw BusinessException.of(ErrorCode.BAD_REQUEST, "评委租户不存在,请先创建 code='judge' 的租户");
}
return tenant.getId();
}
/**
* 获取评委角色 ID
*/
private Long getJudgeRoleId(Long tenantId) {
LambdaQueryWrapper<SysRole> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysRole::getCode, "judge");
wrapper.eq(SysRole::getTenantId, tenantId);
SysRole role = sysRoleMapper.selectOne(wrapper);
if (role == null) {
throw BusinessException.of(ErrorCode.BAD_REQUEST, "评委角色不存在,请先在评委租户下创建 code='judge' 的角色");
}
return role.getId();
}
/**
* SysUser 转为前端需要的 Map
*/
private Map<String, Object> toMap(SysUser user) {
Map<String, Object> map = new LinkedHashMap<>();
map.put("id", user.getId());
map.put("username", user.getUsername());
map.put("nickname", user.getNickname());
map.put("email", user.getEmail());
map.put("phone", user.getPhone());
map.put("organization", user.getOrganization());
map.put("avatar", user.getAvatar());
map.put("status", user.getStatus());
map.put("userSource", user.getUserSource());
map.put("createTime", user.getCreateTime());
map.put("modifyTime", user.getModifyTime());
return map;
}
@Override
@Transactional
public Map<String, Object> createJudge(Map<String, Object> params) {
String username = (String) params.get("username");
String password = (String) params.get("password");
String nickname = (String) params.get("nickname");
String email = (String) params.get("email");
String phone = (String) params.get("phone");
String organization = (String) params.get("organization");
if (username == null || username.isBlank()) {
throw BusinessException.of(ErrorCode.BAD_REQUEST, "用户名不能为空");
}
if (password == null || password.isBlank()) {
throw BusinessException.of(ErrorCode.BAD_REQUEST, "密码不能为空");
}
Long judgeTenantId = getJudgeTenantId();
// 检查用户名在评委租户内唯一
LambdaQueryWrapper<SysUser> dupWrapper = new LambdaQueryWrapper<>();
dupWrapper.eq(SysUser::getTenantId, judgeTenantId);
dupWrapper.eq(SysUser::getUsername, username);
if (sysUserMapper.selectCount(dupWrapper) > 0) {
throw BusinessException.of(ErrorCode.BAD_REQUEST, "该用户名已存在");
}
// 创建用户
SysUser user = new SysUser();
user.setTenantId(judgeTenantId);
user.setUsername(username);
user.setPassword(passwordEncoder.encode(password));
user.setNickname(nickname);
user.setEmail(email);
user.setPhone(phone);
user.setOrganization(organization);
user.setUserSource("admin_created");
user.setUserType("adult");
user.setStatus("enabled");
sysUserMapper.insert(user);
// 分配评委角色
Long judgeRoleId = getJudgeRoleId(judgeTenantId);
SysUserRole userRole = new SysUserRole();
userRole.setUserId(user.getId());
userRole.setRoleId(judgeRoleId);
sysUserRoleMapper.insert(userRole);
log.info("评委账号创建成功ID{},用户名:{}", user.getId(), username);
return toMap(user);
}
@Override
public PageResult<Map<String, Object>> findAll(Long page, Long pageSize, String keyword, String status) {
Long judgeTenantId = getJudgeTenantId();
LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysUser::getTenantId, judgeTenantId);
if (keyword != null && !keyword.isBlank()) {
wrapper.and(w -> w
.like(SysUser::getUsername, keyword)
.or().like(SysUser::getNickname, keyword)
.or().like(SysUser::getOrganization, keyword));
}
if (status != null && !status.isBlank()) {
wrapper.eq(SysUser::getStatus, status);
}
wrapper.orderByDesc(SysUser::getCreateTime);
Page<SysUser> pageParam = new Page<>(page, pageSize);
Page<SysUser> result = sysUserMapper.selectPage(pageParam, wrapper);
List<Map<String, Object>> list = result.getRecords().stream()
.map(this::toMap)
.collect(Collectors.toList());
return new PageResult<>(list, result.getTotal(), result.getCurrent(), result.getSize());
}
@Override
public Map<String, Object> findDetail(Long id) {
SysUser user = sysUserMapper.selectById(id);
if (user == null) {
throw BusinessException.of(ErrorCode.NOT_FOUND, "评委不存在");
}
Long judgeTenantId = getJudgeTenantId();
if (!judgeTenantId.equals(user.getTenantId())) {
throw BusinessException.of(ErrorCode.NOT_FOUND, "评委不存在");
}
return toMap(user);
}
@Override
@Transactional
public Map<String, Object> updateJudge(Long id, Map<String, Object> params) {
SysUser user = sysUserMapper.selectById(id);
if (user == null) {
throw BusinessException.of(ErrorCode.NOT_FOUND, "评委不存在");
}
Long judgeTenantId = getJudgeTenantId();
if (!judgeTenantId.equals(user.getTenantId())) {
throw BusinessException.of(ErrorCode.NOT_FOUND, "评委不存在");
}
if (params.containsKey("nickname")) {
user.setNickname((String) params.get("nickname"));
}
if (params.containsKey("email")) {
user.setEmail((String) params.get("email"));
}
if (params.containsKey("phone")) {
user.setPhone((String) params.get("phone"));
}
if (params.containsKey("organization")) {
user.setOrganization((String) params.get("organization"));
}
if (params.containsKey("password")) {
String newPassword = (String) params.get("password");
if (newPassword != null && !newPassword.isBlank()) {
user.setPassword(passwordEncoder.encode(newPassword));
}
}
sysUserMapper.updateById(user);
log.info("评委信息更新成功ID{}", id);
return toMap(user);
}
@Override
public void freeze(Long id) {
updateJudgeStatus(id, "disabled");
log.info("评委账号已冻结ID{}", id);
}
@Override
public void unfreeze(Long id) {
updateJudgeStatus(id, "enabled");
log.info("评委账号已解冻ID{}", id);
}
private void updateJudgeStatus(Long id, String status) {
SysUser user = sysUserMapper.selectById(id);
if (user == null) {
throw BusinessException.of(ErrorCode.NOT_FOUND, "评委不存在");
}
Long judgeTenantId = getJudgeTenantId();
if (!judgeTenantId.equals(user.getTenantId())) {
throw BusinessException.of(ErrorCode.NOT_FOUND, "评委不存在");
}
user.setStatus(status);
sysUserMapper.updateById(user);
}
@Override
public void remove(Long id) {
SysUser user = sysUserMapper.selectById(id);
if (user == null) {
throw BusinessException.of(ErrorCode.NOT_FOUND, "评委不存在");
}
Long judgeTenantId = getJudgeTenantId();
if (!judgeTenantId.equals(user.getTenantId())) {
throw BusinessException.of(ErrorCode.NOT_FOUND, "评委不存在");
}
sysUserMapper.deleteById(id);
log.info("评委已删除ID{}", id);
}
@Override
@Transactional
public void batchDelete(List<Long> ids) {
if (ids == null || ids.isEmpty()) {
return;
}
Long judgeTenantId = getJudgeTenantId();
// 校验所有 ID 都属于评委租户
LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
wrapper.in(SysUser::getId, ids);
wrapper.eq(SysUser::getTenantId, judgeTenantId);
Long count = sysUserMapper.selectCount(wrapper);
if (count != ids.size()) {
throw BusinessException.of(ErrorCode.BAD_REQUEST, "部分评委不存在或不属于评委库");
}
sysUserMapper.deleteBatchIds(ids);
log.info("批量删除评委成功,数量:{}", ids.size());
}
}

View File

@ -0,0 +1,40 @@
package com.competition.modules.biz.review.controller;
import com.competition.common.result.Result;
import com.competition.common.util.SecurityUtil;
import com.competition.modules.biz.review.service.AnalyticsService;
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.Map;
@Tag(name = "数据分析")
@RestController
@RequestMapping("/analytics")
@RequiredArgsConstructor
public class AnalyticsController {
private final AnalyticsService analyticsService;
@GetMapping("/overview")
@RequirePermission("contest:read")
@Operation(summary = "数据概览")
public Result<Map<String, Object>> getOverview(
@RequestParam(required = false) String timeRange,
@RequestParam(required = false) Long contestId) {
Long tenantId = SecurityUtil.getCurrentTenantId();
return Result.success(analyticsService.getOverview(tenantId, contestId));
}
@GetMapping("/review")
@RequirePermission("contest:read")
@Operation(summary = "评审分析")
public Result<Map<String, Object>> getReviewAnalysis(
@RequestParam(required = false) Long contestId) {
Long tenantId = SecurityUtil.getCurrentTenantId();
return Result.success(analyticsService.getReviewAnalysis(tenantId, contestId));
}
}

View File

@ -0,0 +1,67 @@
package com.competition.modules.biz.review.controller;
import com.competition.common.result.Result;
import com.competition.modules.biz.review.entity.BizContestJudge;
import com.competition.modules.biz.review.service.IContestJudgeService;
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.math.BigDecimal;
import java.util.List;
import java.util.Map;
@Tag(name = "赛事评委")
@RestController
@RequestMapping("/contests/judges")
@RequiredArgsConstructor
public class ContestJudgeController {
private final IContestJudgeService contestJudgeService;
@PostMapping
@RequirePermission("contest:update")
@Operation(summary = "添加赛事评委")
public Result<BizContestJudge> createJudge(@RequestBody Map<String, Object> body) {
Long contestId = Long.valueOf(body.get("contestId").toString());
Long judgeId = Long.valueOf(body.get("judgeId").toString());
String specialty = (String) body.get("specialty");
BigDecimal weight = body.get("weight") != null ? new BigDecimal(body.get("weight").toString()) : null;
String description = (String) body.get("description");
return Result.success(contestJudgeService.createJudge(contestId, judgeId, specialty, weight, description));
}
@GetMapping("/contest/{contestId}")
@RequirePermission("contest:read")
@Operation(summary = "查询赛事评委列表")
public Result<List<Map<String, Object>>> findByContest(@PathVariable Long contestId) {
return Result.success(contestJudgeService.findByContest(contestId));
}
@GetMapping("/{id}")
@RequirePermission("contest:read")
@Operation(summary = "查询评委详情")
public Result<Map<String, Object>> findDetail(@PathVariable Long id) {
return Result.success(contestJudgeService.findDetail(id));
}
@PatchMapping("/{id}")
@RequirePermission("contest:update")
@Operation(summary = "更新评委信息")
public Result<BizContestJudge> updateJudge(@PathVariable Long id, @RequestBody Map<String, Object> body) {
String specialty = (String) body.get("specialty");
BigDecimal weight = body.get("weight") != null ? new BigDecimal(body.get("weight").toString()) : null;
String description = (String) body.get("description");
return Result.success(contestJudgeService.updateJudge(id, specialty, weight, description));
}
@DeleteMapping("/{id}")
@RequirePermission("contest:update")
@Operation(summary = "移除评委")
public Result<Void> removeJudge(@PathVariable Long id) {
contestJudgeService.removeJudge(id);
return Result.success();
}
}

View File

@ -0,0 +1,100 @@
package com.competition.modules.biz.review.controller;
import com.competition.common.result.PageResult;
import com.competition.common.result.Result;
import com.competition.modules.biz.review.dto.AutoSetAwardsDto;
import com.competition.modules.biz.review.dto.BatchSetAwardsDto;
import com.competition.modules.biz.review.dto.SetAwardDto;
import com.competition.modules.biz.review.service.IContestResultService;
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/results")
@RequiredArgsConstructor
public class ContestResultController {
private final IContestResultService contestResultService;
@PostMapping("/{contestId}/calculate-scores")
@RequirePermission("contest:update")
@Operation(summary = "计算所有最终得分")
public Result<Map<String, Object>> calculateAllFinalScores(@PathVariable Long contestId) {
return Result.success(contestResultService.calculateAllFinalScores(contestId));
}
@PostMapping("/{contestId}/calculate-rankings")
@RequirePermission("contest:update")
@Operation(summary = "计算排名")
public Result<Map<String, Object>> calculateRankings(@PathVariable Long contestId) {
return Result.success(contestResultService.calculateRankings(contestId));
}
@PatchMapping("/work/{workId}/award")
@RequirePermission("contest:update")
@Operation(summary = "设置奖项")
public Result<Void> setAward(@PathVariable Long workId, @Valid @RequestBody SetAwardDto dto) {
contestResultService.setAward(workId, dto);
return Result.success();
}
@PostMapping("/{contestId}/batch-set-awards")
@RequirePermission("contest:update")
@Operation(summary = "批量设置奖项")
public Result<Map<String, Object>> batchSetAwards(
@PathVariable Long contestId,
@Valid @RequestBody BatchSetAwardsDto dto) {
return Result.success(contestResultService.batchSetAwards(contestId, dto));
}
@PostMapping("/{contestId}/auto-set-awards")
@RequirePermission("contest:update")
@Operation(summary = "自动设置奖项")
public Result<Map<String, Object>> autoSetAwards(
@PathVariable Long contestId,
@Valid @RequestBody AutoSetAwardsDto dto) {
return Result.success(contestResultService.autoSetAwards(contestId, dto));
}
@PostMapping("/{contestId}/publish")
@RequirePermission("contest:update")
@Operation(summary = "发布成果")
public Result<Void> publishResults(@PathVariable Long contestId) {
contestResultService.publishResults(contestId);
return Result.success();
}
@PostMapping("/{contestId}/unpublish")
@RequirePermission("contest:update")
@Operation(summary = "撤回成果发布")
public Result<Void> unpublishResults(@PathVariable Long contestId) {
contestResultService.unpublishResults(contestId);
return Result.success();
}
@GetMapping("/{contestId}")
@RequirePermission("contest:read")
@Operation(summary = "查询成果列表")
public Result<PageResult<Map<String, Object>>> getResults(
@PathVariable Long contestId,
@RequestParam(defaultValue = "1") Long page,
@RequestParam(defaultValue = "10") Long pageSize,
@RequestParam(required = false) String workNo,
@RequestParam(required = false) String accountNo) {
return Result.success(contestResultService.getResults(contestId, page, pageSize, workNo, accountNo));
}
@GetMapping("/{contestId}/summary")
@RequirePermission("contest:read")
@Operation(summary = "查询成果统计摘要")
public Result<Map<String, Object>> getResultsSummary(@PathVariable Long contestId) {
return Result.success(contestResultService.getResultsSummary(contestId));
}
}

View File

@ -0,0 +1,132 @@
package com.competition.modules.biz.review.controller;
import com.competition.common.result.PageResult;
import com.competition.common.result.Result;
import com.competition.common.util.SecurityUtil;
import com.competition.modules.biz.review.dto.AssignWorkDto;
import com.competition.modules.biz.review.dto.BatchAssignDto;
import com.competition.modules.biz.review.dto.CreateScoreDto;
import com.competition.modules.biz.review.service.IContestReviewService;
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/reviews")
@RequiredArgsConstructor
public class ContestReviewController {
private final IContestReviewService contestReviewService;
@PostMapping("/assign")
@RequirePermission("review:assign")
@Operation(summary = "分配作品给评委")
public Result<Map<String, Object>> assignWork(
@RequestParam Long contestId,
@Valid @RequestBody AssignWorkDto dto) {
Long creatorId = SecurityUtil.getCurrentUserId();
return Result.success(contestReviewService.assignWork(contestId, dto.getWorkId(), dto.getJudgeIds(), creatorId));
}
@PostMapping("/batch-assign")
@RequirePermission("review:assign")
@Operation(summary = "批量分配作品给评委")
public Result<Map<String, Object>> batchAssignWorks(
@RequestParam Long contestId,
@Valid @RequestBody BatchAssignDto dto) {
Long creatorId = SecurityUtil.getCurrentUserId();
return Result.success(contestReviewService.batchAssignWorks(contestId, dto.getWorkIds(), dto.getJudgeIds(), creatorId));
}
@PostMapping("/auto-assign")
@RequirePermission("review:assign")
@Operation(summary = "自动分配作品")
public Result<Map<String, Object>> autoAssignWorks(@RequestParam Long contestId) {
Long creatorId = SecurityUtil.getCurrentUserId();
return Result.success(contestReviewService.autoAssignWorks(contestId, creatorId));
}
@PostMapping("/score")
@RequirePermission("review:score")
@Operation(summary = "评分")
public Result<Map<String, Object>> score(@Valid @RequestBody CreateScoreDto dto) {
Long judgeId = SecurityUtil.getCurrentUserId();
Long tenantId = SecurityUtil.getCurrentTenantId();
return Result.success(contestReviewService.score(dto, judgeId, tenantId));
}
@PatchMapping("/score/{id}")
@RequirePermission("review:score")
@Operation(summary = "修改评分")
public Result<Map<String, Object>> updateScore(
@PathVariable Long id,
@Valid @RequestBody CreateScoreDto dto) {
Long judgeId = SecurityUtil.getCurrentUserId();
return Result.success(contestReviewService.updateScore(id, dto, judgeId));
}
@GetMapping("/assigned")
@RequirePermission("review:read")
@Operation(summary = "查询已分配作品")
public Result<List<Map<String, Object>>> getAssignedWorks(@RequestParam Long contestId) {
Long judgeId = SecurityUtil.getCurrentUserId();
return Result.success(contestReviewService.getAssignedWorks(judgeId, contestId));
}
@GetMapping("/judge/contests")
@RequirePermission("review:score")
@Operation(summary = "获取评委参与的赛事列表")
public Result<List<Map<String, Object>>> getJudgeContests() {
Long judgeId = SecurityUtil.getCurrentUserId();
return Result.success(contestReviewService.getJudgeContests(judgeId));
}
@GetMapping("/judge/contests/{contestId}/works")
@RequirePermission("review:score")
@Operation(summary = "获取评委赛事作品列表")
public Result<PageResult<Map<String, Object>>> getJudgeContestWorks(
@PathVariable Long contestId,
@RequestParam(defaultValue = "1") Long page,
@RequestParam(defaultValue = "10") Long pageSize,
@RequestParam(required = false) String workNo,
@RequestParam(required = false) String accountNo,
@RequestParam(required = false) String reviewStatus) {
Long judgeId = SecurityUtil.getCurrentUserId();
return Result.success(contestReviewService.getJudgeContestWorks(judgeId, contestId, page, pageSize, workNo, accountNo, reviewStatus));
}
@GetMapping("/progress/{contestId}")
@RequirePermission("review:read")
@Operation(summary = "获取评审进度")
public Result<Map<String, Object>> getReviewProgress(@PathVariable Long contestId) {
return Result.success(contestReviewService.getReviewProgress(contestId));
}
@GetMapping("/work-status/{contestId}")
@RequirePermission("review:read")
@Operation(summary = "获取作品评审状态统计")
public Result<Map<String, Object>> getWorkStatusStats(@PathVariable Long contestId) {
return Result.success(contestReviewService.getWorkStatusStats(contestId));
}
@GetMapping("/work/{workId}/scores")
@RequirePermission("review:read")
@Operation(summary = "查询作品评分记录")
public Result<List<Map<String, Object>>> getWorkScores(@PathVariable Long workId) {
return Result.success(contestReviewService.getWorkScores(workId));
}
@GetMapping("/work/{workId}/final-score")
@RequirePermission("review:read")
@Operation(summary = "计算作品最终得分")
public Result<Map<String, Object>> calculateFinalScore(@PathVariable Long workId) {
return Result.success(contestReviewService.calculateFinalScore(workId));
}
}

View File

@ -0,0 +1,75 @@
package com.competition.modules.biz.review.controller;
import com.competition.common.result.PageResult;
import com.competition.common.result.Result;
import com.competition.common.util.SecurityUtil;
import com.competition.modules.biz.review.dto.CreateReviewRuleDto;
import com.competition.modules.biz.review.entity.BizContestReviewRule;
import com.competition.modules.biz.review.service.IContestReviewRuleService;
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/review-rules")
@RequiredArgsConstructor
public class ContestReviewRuleController {
private final IContestReviewRuleService contestReviewRuleService;
@GetMapping
@RequirePermission("contest:read")
@Operation(summary = "查询评审规则列表")
public Result<PageResult<Map<String, Object>>> findAll(
@RequestParam(defaultValue = "1") Long page,
@RequestParam(defaultValue = "10") Long pageSize,
@RequestParam(required = false) String ruleName) {
Long tenantId = SecurityUtil.getCurrentTenantId();
return Result.success(contestReviewRuleService.findAll(page, pageSize, ruleName, tenantId));
}
@GetMapping("/select")
@RequirePermission("contest:read")
@Operation(summary = "查询评审规则选项列表")
public Result<List<Map<String, Object>>> findAllForSelect() {
Long tenantId = SecurityUtil.getCurrentTenantId();
return Result.success(contestReviewRuleService.findAllForSelect(tenantId));
}
@PostMapping
@RequirePermission("contest:update")
@Operation(summary = "创建评审规则")
public Result<BizContestReviewRule> createRule(@Valid @RequestBody CreateReviewRuleDto dto) {
Long tenantId = SecurityUtil.getCurrentTenantId();
return Result.success(contestReviewRuleService.createRule(dto, tenantId));
}
@GetMapping("/{id}")
@RequirePermission("contest:read")
@Operation(summary = "查询评审规则详情")
public Result<Map<String, Object>> findDetail(@PathVariable Long id) {
return Result.success(contestReviewRuleService.findDetail(id));
}
@PatchMapping("/{id}")
@RequirePermission("contest:update")
@Operation(summary = "更新评审规则")
public Result<BizContestReviewRule> updateRule(@PathVariable Long id, @RequestBody CreateReviewRuleDto dto) {
return Result.success(contestReviewRuleService.updateRule(id, dto));
}
@DeleteMapping("/{id}")
@RequirePermission("contest:update")
@Operation(summary = "删除评审规则")
public Result<Void> removeRule(@PathVariable Long id) {
contestReviewRuleService.removeRule(id);
return Result.success();
}
}

View File

@ -0,0 +1,95 @@
package com.competition.modules.biz.review.controller;
import com.competition.common.result.Result;
import com.competition.common.util.SecurityUtil;
import com.competition.modules.biz.review.dto.CreatePresetCommentDto;
import com.competition.modules.biz.review.entity.BizPresetComment;
import com.competition.modules.biz.review.service.IPresetCommentService;
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/preset-comments")
@RequiredArgsConstructor
public class PresetCommentController {
private final IPresetCommentService presetCommentService;
@PostMapping
@Operation(summary = "创建预设评语")
public Result<BizPresetComment> createComment(@Valid @RequestBody CreatePresetCommentDto dto) {
Long judgeId = SecurityUtil.getCurrentUserId();
return Result.success(presetCommentService.createComment(dto, judgeId));
}
@GetMapping
@Operation(summary = "查询预设评语列表")
public Result<List<Map<String, Object>>> findAll(@RequestParam(required = false) Long contestId) {
Long judgeId = SecurityUtil.getCurrentUserId();
return Result.success(presetCommentService.findAll(contestId, judgeId));
}
@GetMapping("/judge/contests")
@Operation(summary = "获取评委参与的赛事列表")
public Result<List<Map<String, Object>>> getJudgeContests() {
Long judgeId = SecurityUtil.getCurrentUserId();
return Result.success(presetCommentService.getJudgeContests(judgeId));
}
@GetMapping("/{id}")
@Operation(summary = "查询预设评语详情")
public Result<Map<String, Object>> findDetail(@PathVariable Long id) {
Long judgeId = SecurityUtil.getCurrentUserId();
return Result.success(presetCommentService.findDetail(id, judgeId));
}
@PatchMapping("/{id}")
@Operation(summary = "更新预设评语")
public Result<BizPresetComment> updateComment(@PathVariable Long id, @RequestBody CreatePresetCommentDto dto) {
Long judgeId = SecurityUtil.getCurrentUserId();
return Result.success(presetCommentService.updateComment(id, dto, judgeId));
}
@DeleteMapping("/{id}")
@Operation(summary = "删除预设评语")
public Result<Void> removeComment(@PathVariable Long id) {
Long judgeId = SecurityUtil.getCurrentUserId();
presetCommentService.removeComment(id, judgeId);
return Result.success();
}
@SuppressWarnings("unchecked")
@PostMapping("/batch-delete")
@Operation(summary = "批量删除预设评语")
public Result<Void> batchDelete(@RequestBody Map<String, Object> body) {
List<Long> ids = (List<Long>) body.get("ids");
Long judgeId = SecurityUtil.getCurrentUserId();
presetCommentService.batchDelete(ids, judgeId);
return Result.success();
}
@PostMapping("/sync")
@Operation(summary = "同步评语到其他赛事")
public Result<Map<String, Object>> syncComments(@RequestBody Map<String, Object> body) {
Long sourceContestId = Long.valueOf(body.get("sourceContestId").toString());
@SuppressWarnings("unchecked")
List<Long> targetContestIds = (List<Long>) body.get("targetContestIds");
Long judgeId = SecurityUtil.getCurrentUserId();
return Result.success(presetCommentService.syncComments(sourceContestId, targetContestIds, judgeId));
}
@PostMapping("/{id}/use")
@Operation(summary = "增加评语使用次数")
public Result<Void> incrementUseCount(@PathVariable Long id) {
Long judgeId = SecurityUtil.getCurrentUserId();
presetCommentService.incrementUseCount(id, judgeId);
return Result.success();
}
}

View File

@ -0,0 +1,20 @@
package com.competition.modules.biz.review.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
@Data
@Schema(description = "分配作品给评委DTO")
public class AssignWorkDto {
@NotNull(message = "作品ID不能为空")
@Schema(description = "作品ID")
private Long workId;
@NotNull(message = "评委ID列表不能为空")
@Schema(description = "评委ID列表")
private List<Long> judgeIds;
}

View File

@ -0,0 +1,25 @@
package com.competition.modules.biz.review.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
@Schema(description = "自动设置奖项DTO")
public class AutoSetAwardsDto {
@Schema(description = "奖项层级列表")
private List<AwardTier> awards;
@Data
@Schema(description = "奖项层级")
public static class AwardTier {
@Schema(description = "奖项名称")
private String name;
@Schema(description = "数量")
private Integer count;
}
}

View File

@ -0,0 +1,20 @@
package com.competition.modules.biz.review.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
@Data
@Schema(description = "批量分配作品给评委DTO")
public class BatchAssignDto {
@NotNull(message = "作品ID列表不能为空")
@Schema(description = "作品ID列表")
private List<Long> workIds;
@NotNull(message = "评委ID列表不能为空")
@Schema(description = "评委ID列表")
private List<Long> judgeIds;
}

View File

@ -0,0 +1,28 @@
package com.competition.modules.biz.review.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
@Schema(description = "批量设置奖项DTO")
public class BatchSetAwardsDto {
@Schema(description = "奖项列表")
private List<AwardItem> awards;
@Data
@Schema(description = "奖项条目")
public static class AwardItem {
@Schema(description = "作品ID")
private Long workId;
@Schema(description = "奖项等级")
private String awardLevel;
@Schema(description = "奖项名称")
private String awardName;
}
}

View File

@ -0,0 +1,27 @@
package com.competition.modules.biz.review.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
@Data
@Schema(description = "创建预设评语DTO")
public class CreatePresetCommentDto {
@NotNull(message = "赛事ID不能为空")
@Schema(description = "赛事ID")
private Long contestId;
@NotBlank(message = "评语内容不能为空")
@Schema(description = "评语内容")
private String content;
@Schema(description = "关联分数")
private BigDecimal score;
@Schema(description = "排序")
private Integer sortOrder;
}

View File

@ -0,0 +1,29 @@
package com.competition.modules.biz.review.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 CreateReviewRuleDto {
@NotBlank(message = "规则名称不能为空")
@Schema(description = "规则名称")
private String ruleName;
@Schema(description = "规则描述")
private String ruleDescription;
@NotNull(message = "评委人数不能为空")
@Schema(description = "评委人数")
private Integer judgeCount;
@NotNull(message = "评分维度不能为空")
@Schema(description = "评分维度JSON数组")
private Object dimensions;
@Schema(description = "计算规则average/remove_max_min/remove_min/max/weighted")
private String calculationRule;
}

View File

@ -0,0 +1,30 @@
package com.competition.modules.biz.review.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 CreateScoreDto {
@NotNull(message = "作品ID不能为空")
@Schema(description = "作品ID")
private Long workId;
@NotNull(message = "分配记录ID不能为空")
@Schema(description = "分配记录ID")
private Long assignmentId;
@Schema(description = "维度评分JSON对象")
private Object dimensionScores;
@NotNull(message = "总分不能为空")
@Schema(description = "总分")
private BigDecimal totalScore;
@Schema(description = "评语")
private String comments;
}

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