diff --git a/backend-java/.gitignore b/backend-java/.gitignore
new file mode 100644
index 0000000..a391884
--- /dev/null
+++ b/backend-java/.gitignore
@@ -0,0 +1,5 @@
+target/
+uploads/
+*.log
+.idea/
+*.iml
diff --git a/backend-java/pom.xml b/backend-java/pom.xml
new file mode 100644
index 0000000..f26012d
--- /dev/null
+++ b/backend-java/pom.xml
@@ -0,0 +1,198 @@
+
+
+ 4.0.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.2.5
+
+
+
+ com.competition
+ competition-management-system
+ 1.0.0
+ competition-management-system
+ 少儿绘本创作活动管理平台 - Java 后端
+
+
+ 17
+ 3.5.7
+ 1.2.23
+ 0.12.6
+ 4.5.0
+ 1.5.5.Final
+ 5.8.32
+ 2.0.53
+ 5.6.227
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
+
+ org.springframework.boot
+ spring-boot-starter-aop
+
+
+
+
+ com.baomidou
+ mybatis-plus-spring-boot3-starter
+ ${mybatis-plus.version}
+
+
+
+
+ com.alibaba
+ druid-spring-boot-3-starter
+ ${druid.version}
+
+
+
+
+ com.mysql
+ mysql-connector-j
+ runtime
+
+
+
+
+ org.flywaydb
+ flyway-core
+
+
+ org.flywaydb
+ flyway-mysql
+
+
+
+
+ io.jsonwebtoken
+ jjwt-api
+ ${jjwt.version}
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ ${jjwt.version}
+ runtime
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ ${jjwt.version}
+ runtime
+
+
+
+
+ com.github.xiaoymin
+ knife4j-openapi3-jakarta-spring-boot-starter
+ ${knife4j.version}
+
+
+
+
+ org.mapstruct
+ mapstruct
+ ${mapstruct.version}
+
+
+
+
+ cn.hutool
+ hutool-all
+ ${hutool.version}
+
+
+
+
+ com.alibaba.fastjson2
+ fastjson2
+ ${fastjson2.version}
+
+
+
+
+ com.qcloud
+ cos_api
+ ${cos.version}
+
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ ${java.version}
+ ${java.version}
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
+ org.mapstruct
+ mapstruct-processor
+ ${mapstruct.version}
+
+
+
+ org.projectlombok
+ lombok-mapstruct-binding
+ 0.2.0
+
+
+
+
+
+
+
+
diff --git a/backend-java/src/main/java/com/competition/CompetitionApplication.java b/backend-java/src/main/java/com/competition/CompetitionApplication.java
new file mode 100644
index 0000000..3a68f24
--- /dev/null
+++ b/backend-java/src/main/java/com/competition/CompetitionApplication.java
@@ -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);
+ }
+}
diff --git a/backend-java/src/main/java/com/competition/common/config/CorsConfig.java b/backend-java/src/main/java/com/competition/common/config/CorsConfig.java
new file mode 100644
index 0000000..b207f95
--- /dev/null
+++ b/backend-java/src/main/java/com/competition/common/config/CorsConfig.java
@@ -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);
+ }
+}
diff --git a/backend-java/src/main/java/com/competition/common/config/Knife4jConfig.java b/backend-java/src/main/java/com/competition/common/config/Knife4jConfig.java
new file mode 100644
index 0000000..2cbd578
--- /dev/null
+++ b/backend-java/src/main/java/com/competition/common/config/Knife4jConfig.java
@@ -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"));
+ }
+}
diff --git a/backend-java/src/main/java/com/competition/common/config/MyBatisPlusConfig.java b/backend-java/src/main/java/com/competition/common/config/MyBatisPlusConfig.java
new file mode 100644
index 0000000..497e298
--- /dev/null
+++ b/backend-java/src/main/java/com/competition/common/config/MyBatisPlusConfig.java
@@ -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;
+ }
+}
diff --git a/backend-java/src/main/java/com/competition/common/config/WebMvcConfig.java b/backend-java/src/main/java/com/competition/common/config/WebMvcConfig.java
new file mode 100644
index 0000000..334dd67
--- /dev/null
+++ b/backend-java/src/main/java/com/competition/common/config/WebMvcConfig.java
@@ -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("/**");
+ }
+}
diff --git a/backend-java/src/main/java/com/competition/common/entity/BaseEntity.java b/backend-java/src/main/java/com/competition/common/entity/BaseEntity.java
new file mode 100644
index 0000000..404b7f1
--- /dev/null
+++ b/backend-java/src/main/java/com/competition/common/entity/BaseEntity.java
@@ -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;
+}
diff --git a/backend-java/src/main/java/com/competition/common/enums/ErrorCode.java b/backend-java/src/main/java/com/competition/common/enums/ErrorCode.java
new file mode 100644
index 0000000..59df20e
--- /dev/null
+++ b/backend-java/src/main/java/com/competition/common/enums/ErrorCode.java
@@ -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;
+}
diff --git a/backend-java/src/main/java/com/competition/common/exception/BusinessException.java b/backend-java/src/main/java/com/competition/common/exception/BusinessException.java
new file mode 100644
index 0000000..266bbfe
--- /dev/null
+++ b/backend-java/src/main/java/com/competition/common/exception/BusinessException.java
@@ -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);
+ }
+}
diff --git a/backend-java/src/main/java/com/competition/common/exception/GlobalExceptionHandler.java b/backend-java/src/main/java/com/competition/common/exception/GlobalExceptionHandler.java
new file mode 100644
index 0000000..465890f
--- /dev/null
+++ b/backend-java/src/main/java/com/competition/common/exception/GlobalExceptionHandler.java
@@ -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 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 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 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 handleAuthenticationException(AuthenticationException e, HttpServletRequest request) {
+ return Result.error(401, ErrorCode.UNAUTHORIZED.getMessage(), request.getRequestURI());
+ }
+
+ /** 授权异常 */
+ @ExceptionHandler(AccessDeniedException.class)
+ @ResponseStatus(HttpStatus.FORBIDDEN)
+ public Result handleAccessDeniedException(AccessDeniedException e, HttpServletRequest request) {
+ return Result.error(403, ErrorCode.FORBIDDEN.getMessage(), request.getRequestURI());
+ }
+
+ /** 资源不存在 */
+ @ExceptionHandler(NoResourceFoundException.class)
+ @ResponseStatus(HttpStatus.NOT_FOUND)
+ public Result 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 handleException(Exception e, HttpServletRequest request) {
+ log.error("系统异常,路径:{},消息:{}", request.getRequestURI(), e.getMessage(), e);
+ return Result.error(500, ErrorCode.INTERNAL_ERROR.getMessage(), request.getRequestURI());
+ }
+}
diff --git a/backend-java/src/main/java/com/competition/common/handler/AuditMetaObjectHandler.java b/backend-java/src/main/java/com/competition/common/handler/AuditMetaObjectHandler.java
new file mode 100644
index 0000000..a03adfe
--- /dev/null
+++ b/backend-java/src/main/java/com/competition/common/handler/AuditMetaObjectHandler.java
@@ -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());
+ }
+ }
+}
diff --git a/backend-java/src/main/java/com/competition/common/interceptor/TraceIdInterceptor.java b/backend-java/src/main/java/com/competition/common/interceptor/TraceIdInterceptor.java
new file mode 100644
index 0000000..ed177d5
--- /dev/null
+++ b/backend-java/src/main/java/com/competition/common/interceptor/TraceIdInterceptor.java
@@ -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);
+ }
+}
diff --git a/backend-java/src/main/java/com/competition/common/result/PageResult.java b/backend-java/src/main/java/com/competition/common/result/PageResult.java
new file mode 100644
index 0000000..2818815
--- /dev/null
+++ b/backend-java/src/main/java/com/competition/common/result/PageResult.java
@@ -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 implements Serializable {
+
+ /** 数据列表 */
+ private List list;
+
+ /** 总记录数 */
+ private Long total;
+
+ /** 当前页码 */
+ private Long page;
+
+ /** 每页大小 */
+ private Long pageSize;
+
+ public PageResult() {
+ }
+
+ public PageResult(List list, Long total, Long page, Long pageSize) {
+ this.list = list;
+ this.total = total;
+ this.page = page;
+ this.pageSize = pageSize;
+ }
+
+ /**
+ * 从 MyBatis-Plus 的 IPage 转换
+ */
+ public static PageResult from(IPage page) {
+ return new PageResult<>(
+ page.getRecords(),
+ page.getTotal(),
+ page.getCurrent(),
+ page.getSize()
+ );
+ }
+
+ /**
+ * 从 MyBatis-Plus 的 IPage 转换,支持 VO 列表替换
+ */
+ public static PageResult from(IPage> page, List voList) {
+ return new PageResult<>(
+ voList,
+ page.getTotal(),
+ page.getCurrent(),
+ page.getSize()
+ );
+ }
+}
diff --git a/backend-java/src/main/java/com/competition/common/result/Result.java b/backend-java/src/main/java/com/competition/common/result/Result.java
new file mode 100644
index 0000000..aaa47d9
--- /dev/null
+++ b/backend-java/src/main/java/com/competition/common/result/Result.java
@@ -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 implements Serializable {
+
+ private Integer code;
+ private String message;
+ private T data;
+ private String timestamp;
+ private String path;
+
+ private Result() {
+ }
+
+ public static Result success(T data) {
+ Result result = new Result<>();
+ result.setCode(200);
+ result.setMessage("success");
+ result.setData(data);
+ return result;
+ }
+
+ public static Result success() {
+ return success(null);
+ }
+
+ public static Result success(String message, T data) {
+ Result result = new Result<>();
+ result.setCode(200);
+ result.setMessage(message);
+ result.setData(data);
+ return result;
+ }
+
+ public static Result error(Integer code, String message) {
+ Result result = new Result<>();
+ result.setCode(code);
+ result.setMessage(message);
+ result.setTimestamp(LocalDateTime.now().toString());
+ return result;
+ }
+
+ public static Result error(ErrorCode errorCode) {
+ return error(errorCode.getCode(), errorCode.getMessage());
+ }
+
+ public static Result error(Integer code, String message, String path) {
+ Result result = error(code, message);
+ result.setPath(path);
+ return result;
+ }
+}
diff --git a/backend-java/src/main/java/com/competition/common/util/SecurityUtil.java b/backend-java/src/main/java/com/competition/common/util/SecurityUtil.java
new file mode 100644
index 0000000..3be7085
--- /dev/null
+++ b/backend-java/src/main/java/com/competition/common/util/SecurityUtil.java
@@ -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();
+ }
+}
diff --git a/backend-java/src/main/java/com/competition/modules/biz/contest/controller/ContestAttachmentController.java b/backend-java/src/main/java/com/competition/modules/biz/contest/controller/ContestAttachmentController.java
new file mode 100644
index 0000000..80e235a
--- /dev/null
+++ b/backend-java/src/main/java/com/competition/modules/biz/contest/controller/ContestAttachmentController.java
@@ -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 create(@RequestBody BizContestAttachment attachment) {
+ attachmentService.save(attachment);
+ return Result.success(attachment);
+ }
+
+ @GetMapping("/contest/{contestId}")
+ @RequirePermission("contest:read")
+ @Operation(summary = "查询赛事下的附件列表")
+ public Result> findByContest(@PathVariable Long contestId) {
+ List list = attachmentService.list(
+ new LambdaQueryWrapper()
+ .eq(BizContestAttachment::getContestId, contestId)
+ .orderByDesc(BizContestAttachment::getCreateTime));
+ return Result.success(list);
+ }
+
+ @GetMapping("/{id}")
+ @RequirePermission("contest:read")
+ @Operation(summary = "查询附件详情")
+ public Result findDetail(@PathVariable Long id) {
+ return Result.success(attachmentService.getById(id));
+ }
+
+ @PatchMapping("/{id}")
+ @RequirePermission("contest:update")
+ @Operation(summary = "更新附件")
+ public Result 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 remove(@PathVariable Long id) {
+ attachmentService.removeById(id);
+ return Result.success();
+ }
+}
diff --git a/backend-java/src/main/java/com/competition/modules/biz/contest/controller/ContestController.java b/backend-java/src/main/java/com/competition/modules/biz/contest/controller/ContestController.java
new file mode 100644
index 0000000..b5effd2
--- /dev/null
+++ b/backend-java/src/main/java/com/competition/modules/biz/contest/controller/ContestController.java
@@ -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 create(@Valid @RequestBody CreateContestDto dto) {
+ return Result.success(contestService.createContest(dto, SecurityUtil.getCurrentUserId()));
+ }
+
+ @GetMapping("/stats")
+ @RequirePermission("contest:read")
+ @Operation(summary = "获取赛事统计")
+ public Result