Compare commits
36 Commits
c5fad30849
...
df7eae6125
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df7eae6125 | ||
|
|
197064820b | ||
|
|
180c22fe49 | ||
| bc7c17b281 | |||
| 9b5c24c49c | |||
| a660493cf3 | |||
| 922f650365 | |||
| 9ad9f5b237 | |||
|
|
2f84ac16d3 | ||
|
|
7a039e8403 | ||
|
|
1d43501983 | ||
|
|
f2c10d5e32 | ||
|
|
170d904081 | ||
|
|
3fd7002e2a | ||
|
|
633e46e663 | ||
|
|
2b83c9c78a | ||
|
|
eb409398f3 | ||
|
|
015f8718c4 | ||
|
|
2f521c7249 | ||
|
|
1c63cb21e5 | ||
|
|
0252f25acd | ||
| 3c24cc3102 | |||
|
|
a5909f98be | ||
|
|
15581e04ae | ||
|
|
1003776dd3 | ||
|
|
3ef05de193 | ||
|
|
d68322f24a | ||
|
|
63c564a03b | ||
|
|
ff25e41243 | ||
|
|
bee5152a2d | ||
|
|
b3954ffcf3 | ||
|
|
4a70bc7d43 | ||
|
|
764f6eec4b | ||
|
|
7afb57c9bf | ||
|
|
78cd956ab5 | ||
|
|
f223e9bd41 |
180
CLAUDE.md
Normal file
180
CLAUDE.md
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## 项目概述
|
||||||
|
|
||||||
|
多租户少儿绘本创作活动/竞赛管理平台,前后端分离架构。
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
| 目录 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `backend-java/` | Spring Boot 后端(实际开发目录) |
|
||||||
|
| `frontend/` | Vue 3 前端(实际开发目录) |
|
||||||
|
| `lesingle-aicreate-client/` | AI 绘本创作客户端(独立模块) |
|
||||||
|
|
||||||
|
## 常用命令
|
||||||
|
|
||||||
|
### 后端 (backend-java/)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend-java
|
||||||
|
mvn spring-boot:run -Dspring.profiles.active=dev # 开发启动(端口 8580,上下文 /api)
|
||||||
|
mvn flyway:migrate # 执行数据库迁移
|
||||||
|
mvn clean package # 构建打包
|
||||||
|
```
|
||||||
|
|
||||||
|
### 前端 (frontend/)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run dev # 开发模式(端口 3000,代理 /api → localhost:8580)
|
||||||
|
npm run build # 生产构建(base: /web/)
|
||||||
|
npm run build:test # 测试环境构建(base: /web-test/)
|
||||||
|
npm run lint # ESLint 检查
|
||||||
|
```
|
||||||
|
|
||||||
|
### AI创作客户端 (lesingle-aicreate-client/)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd lesingle-aicreate-client
|
||||||
|
npm install && npm run dev # 独立启动
|
||||||
|
```
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
| 层级 | 技术 |
|
||||||
|
|------|------|
|
||||||
|
| 后端框架 | Spring Boot 3.2.5 + Java 17 |
|
||||||
|
| ORM | MyBatis-Plus 3.5.7 |
|
||||||
|
| 数据库 | MySQL 8.0 + Flyway 迁移 |
|
||||||
|
| 认证 | Spring Security + JWT |
|
||||||
|
| 缓存 | Redis |
|
||||||
|
| 工具库 | Hutool 5.8 + FastJSON2 + Knife4j 4.4(API文档) |
|
||||||
|
| 前端框架 | Vue 3 + TypeScript + Vite 5 |
|
||||||
|
| UI | Ant Design Vue 4.1 |
|
||||||
|
| 状态管理 | Pinia |
|
||||||
|
| 样式 | Tailwind CSS + SCSS |
|
||||||
|
| 表单验证 | VeeValidate + Zod |
|
||||||
|
| 富文本 | WangEditor |
|
||||||
|
| 图表 | ECharts |
|
||||||
|
|
||||||
|
## 后端架构 (backend-java/)
|
||||||
|
|
||||||
|
### 基础包: `com.competition`
|
||||||
|
|
||||||
|
### 三层架构
|
||||||
|
|
||||||
|
| 层级 | 职责 | 规范 |
|
||||||
|
|------|------|------|
|
||||||
|
| Controller | HTTP 请求处理、参数校验、Entity↔VO 转换 | 统一返回 `Result<T>` |
|
||||||
|
| Service | 业务逻辑、事务控制 | 接口 `I{Module}Service`,继承 `IService<T>` |
|
||||||
|
| Mapper | 数据库 CRUD | 继承 `BaseMapper<T>` |
|
||||||
|
|
||||||
|
**核心原则**: Service/Mapper 层使用 Entity,VO 转换只在 Controller 层。
|
||||||
|
|
||||||
|
### 模块划分
|
||||||
|
|
||||||
|
```
|
||||||
|
com.competition.modules/
|
||||||
|
├── biz/
|
||||||
|
│ ├── contest/ # 赛事管理(/contests)
|
||||||
|
│ ├── homework/ # 作业管理(/homework)
|
||||||
|
│ ├── judge/ # 评委管理
|
||||||
|
│ └── review/ # 评审管理(/contest-reviews, /contest-results)
|
||||||
|
├── sys/ # 系统管理(用户/角色/权限/租户,/sys/*)
|
||||||
|
├── user/ # 用户模块
|
||||||
|
├── ugc/ # 用户生成内容
|
||||||
|
├── pub/ # 公开接口(/public/*,无需认证)
|
||||||
|
└── oss/ # 对象存储(/oss/upload)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 实体基类 (BaseEntity)
|
||||||
|
|
||||||
|
所有实体继承 `BaseEntity`,包含字段:`id`、`createBy`、`updateBy`、`createTime`、`modifyTime`、`deleted`、`validState`。
|
||||||
|
|
||||||
|
表名规范:业务表 `t_biz_*`、系统表 `t_sys_*`、用户表 `t_user_*`。
|
||||||
|
|
||||||
|
### 多租户
|
||||||
|
|
||||||
|
- 所有业务表包含 `tenant_id` 字段
|
||||||
|
- 获取租户ID: `SecurityUtil.getCurrentTenantId()`
|
||||||
|
- 超级管理员 `isSuperAdmin()` 可访问所有租户数据
|
||||||
|
- 请求头通过 `X-Tenant-Code`、`X-Tenant-Id` 传递
|
||||||
|
|
||||||
|
### 认证与权限
|
||||||
|
|
||||||
|
- JWT Token payload: `{sub: userId, username, tenantId}`
|
||||||
|
- 公开接口: `@Public` 注解或路径 `/public/**`
|
||||||
|
- 权限控制: `@RequirePermission` 注解
|
||||||
|
|
||||||
|
### 统一响应格式
|
||||||
|
|
||||||
|
```java
|
||||||
|
Result<T> → {code, message, data, timestamp, path}
|
||||||
|
PageResult<T> → {list, total, page, pageSize}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 数据库迁移
|
||||||
|
|
||||||
|
- 位置: `src/main/resources/db/migration/`
|
||||||
|
- 命名: `V{number}__description.sql`(注意双下划线)
|
||||||
|
- 不使用外键约束,关联关系通过代码控制
|
||||||
|
|
||||||
|
## 前端架构 (frontend/)
|
||||||
|
|
||||||
|
### 路由与多租户
|
||||||
|
|
||||||
|
- 路由路径包含租户编码: `/:tenantCode/login`、`/:tenantCode/dashboard`
|
||||||
|
- 动态路由: 根据用户权限菜单动态生成
|
||||||
|
- 双模式: 管理端(需认证)+ 公众端(无需认证)
|
||||||
|
|
||||||
|
### 三种布局
|
||||||
|
|
||||||
|
| 布局 | 用途 |
|
||||||
|
|------|------|
|
||||||
|
| BasicLayout | 管理端(侧边栏+顶栏+面包屑) |
|
||||||
|
| PublicLayout | 公众端(简洁导航) |
|
||||||
|
| EmptyLayout | 全屏页面 |
|
||||||
|
|
||||||
|
### API 调用模式
|
||||||
|
|
||||||
|
API 模块位于 `src/api/`,Axios 实例在 `src/utils/request.ts`:
|
||||||
|
- 请求拦截器自动添加 Authorization token 和租户头
|
||||||
|
- 响应拦截器统一错误处理(401 跳转登录,403 提示)
|
||||||
|
- 函数命名: `getXxx`、`createXxx`、`updateXxx`、`deleteXxx`
|
||||||
|
|
||||||
|
### 权限控制
|
||||||
|
|
||||||
|
- 路由级: `meta.permissions`
|
||||||
|
- 组件级: `v-permission` 自定义指令
|
||||||
|
- 方法级: `hasPermission()`、`hasAnyPermission()`、`isSuperAdmin()`
|
||||||
|
|
||||||
|
### 状态管理
|
||||||
|
|
||||||
|
- auth Store: 用户信息、token、菜单、权限检查
|
||||||
|
- Token 存储在 Cookie 中
|
||||||
|
|
||||||
|
## 开发规范
|
||||||
|
|
||||||
|
- **日志/注释使用中文**
|
||||||
|
- **Git 提交格式**: `类型: 描述`(如 `feat: 添加XX功能`、`fix: 修复XX问题`)
|
||||||
|
- **组件语法**: `<script setup lang="ts">`
|
||||||
|
- **文件命名**: 组件 PascalCase,其他 kebab-case
|
||||||
|
- **数据库**: 不使用外键,关联通过代码控制
|
||||||
|
- **逻辑删除**: `deleted` 字段
|
||||||
|
|
||||||
|
## 环境配置
|
||||||
|
|
||||||
|
| 环境 | 后端 Profile | 前端 base | 后端端口 |
|
||||||
|
|------|-------------|-----------|---------|
|
||||||
|
| 开发 | dev | `/` | 8580 |
|
||||||
|
| 测试 | test | `/web-test/` | 8580 |
|
||||||
|
| 生产 | prod | `/web/` | 8580 |
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- `.cursor/rules/` 中部分规范(如 NestJS、Prisma)是旧版配置,当前后端已迁移至 Spring Boot + MyBatis-Plus,以本文件为准
|
||||||
|
- 当前项目未配置测试框架
|
||||||
|
- 当前项目未配置 i18n,所有文本为中文硬编码
|
||||||
@ -12,7 +12,8 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
|
|||||||
"com.competition.modules.biz.homework.mapper",
|
"com.competition.modules.biz.homework.mapper",
|
||||||
"com.competition.modules.biz.judge.mapper",
|
"com.competition.modules.biz.judge.mapper",
|
||||||
"com.competition.modules.user.mapper",
|
"com.competition.modules.user.mapper",
|
||||||
"com.competition.modules.ugc.mapper"
|
"com.competition.modules.ugc.mapper",
|
||||||
|
"com.competition.modules.leai.mapper"
|
||||||
})
|
})
|
||||||
public class CompetitionApplication {
|
public class CompetitionApplication {
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,24 @@
|
|||||||
|
package com.competition.common.config;
|
||||||
|
|
||||||
|
import org.flywaydb.core.Flyway;
|
||||||
|
import org.springframework.boot.autoconfigure.flyway.FlywayMigrationStrategy;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flyway 修复配置
|
||||||
|
* 启动时自动修复失败的迁移记录,然后执行迁移
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class FlywayRepairConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public FlywayMigrationStrategy flywayMigrationStrategy() {
|
||||||
|
return flyway -> {
|
||||||
|
// 先修复失败的迁移记录
|
||||||
|
flyway.repair();
|
||||||
|
// 然后执行迁移
|
||||||
|
flyway.migrate();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package com.competition.common.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RestTemplate 配置
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class RestTemplateConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public RestTemplate restTemplate() {
|
||||||
|
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
|
||||||
|
factory.setConnectTimeout(10_000);
|
||||||
|
factory.setReadTimeout(10_000);
|
||||||
|
return new RestTemplate(factory);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
package com.competition.common.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定时任务配置
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@EnableScheduling
|
||||||
|
public class SchedulingConfig {
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
package com.competition.common.entity;
|
package com.competition.common.entity;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.*;
|
import com.baomidou.mybatisplus.annotation.*;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
@ -11,23 +12,28 @@ import java.time.LocalDateTime;
|
|||||||
* 包含新审计字段(Java规范)和旧审计字段(过渡期兼容)
|
* 包含新审计字段(Java规范)和旧审计字段(过渡期兼容)
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
|
@Schema(description = "基础实体")
|
||||||
public abstract class BaseEntity implements Serializable {
|
public abstract class BaseEntity implements Serializable {
|
||||||
|
|
||||||
/** 主键 ID(自增) */
|
/** 主键 ID(自增) */
|
||||||
|
@Schema(description = "主键ID")
|
||||||
@TableId(type = IdType.AUTO)
|
@TableId(type = IdType.AUTO)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
// ====== 新审计字段(Java 规范) ======
|
// ====== 新审计字段(Java 规范) ======
|
||||||
|
|
||||||
/** 创建人账号 */
|
/** 创建人账号 */
|
||||||
|
@Schema(description = "创建人账号")
|
||||||
@TableField(value = "create_by", fill = FieldFill.INSERT)
|
@TableField(value = "create_by", fill = FieldFill.INSERT)
|
||||||
private String createBy;
|
private String createBy;
|
||||||
|
|
||||||
/** 更新人账号 */
|
/** 更新人账号 */
|
||||||
|
@Schema(description = "更新人账号")
|
||||||
@TableField(value = "update_by", fill = FieldFill.INSERT_UPDATE)
|
@TableField(value = "update_by", fill = FieldFill.INSERT_UPDATE)
|
||||||
private String updateBy;
|
private String updateBy;
|
||||||
|
|
||||||
/** 逻辑删除标识(0-未删除,1-已删除) */
|
/** 逻辑删除标识(0-未删除,1-已删除) */
|
||||||
|
@Schema(description = "逻辑删除标识:0-未删除,1-已删除")
|
||||||
@TableLogic
|
@TableLogic
|
||||||
@TableField(value = "deleted", fill = FieldFill.INSERT)
|
@TableField(value = "deleted", fill = FieldFill.INSERT)
|
||||||
private Integer deleted;
|
private Integer deleted;
|
||||||
@ -35,22 +41,27 @@ public abstract class BaseEntity implements Serializable {
|
|||||||
// ====== 旧审计字段(过渡期保留) ======
|
// ====== 旧审计字段(过渡期保留) ======
|
||||||
|
|
||||||
/** 创建人 ID */
|
/** 创建人 ID */
|
||||||
|
@Schema(description = "创建人ID")
|
||||||
@TableField(value = "creator", fill = FieldFill.INSERT)
|
@TableField(value = "creator", fill = FieldFill.INSERT)
|
||||||
private Integer creator;
|
private Integer creator;
|
||||||
|
|
||||||
/** 修改人 ID */
|
/** 修改人 ID */
|
||||||
|
@Schema(description = "修改人ID")
|
||||||
@TableField(value = "modifier", fill = FieldFill.INSERT_UPDATE)
|
@TableField(value = "modifier", fill = FieldFill.INSERT_UPDATE)
|
||||||
private Integer modifier;
|
private Integer modifier;
|
||||||
|
|
||||||
/** 创建时间 */
|
/** 创建时间 */
|
||||||
|
@Schema(description = "创建时间")
|
||||||
@TableField(value = "create_time", fill = FieldFill.INSERT)
|
@TableField(value = "create_time", fill = FieldFill.INSERT)
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
/** 修改时间 */
|
/** 修改时间 */
|
||||||
|
@Schema(description = "修改时间")
|
||||||
@TableField(value = "modify_time", fill = FieldFill.INSERT_UPDATE)
|
@TableField(value = "modify_time", fill = FieldFill.INSERT_UPDATE)
|
||||||
private LocalDateTime modifyTime;
|
private LocalDateTime modifyTime;
|
||||||
|
|
||||||
/** 有效状态:1-有效,2-失效 */
|
/** 有效状态:1-有效,2-失效 */
|
||||||
|
@Schema(description = "有效状态:1-有效,2-失效")
|
||||||
@TableField(value = "valid_state", fill = FieldFill.INSERT)
|
@TableField(value = "valid_state", fill = FieldFill.INSERT)
|
||||||
private Integer validState;
|
private Integer validState;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.competition.common.result.PageResult;
|
import com.competition.common.result.PageResult;
|
||||||
import com.competition.common.result.Result;
|
import com.competition.common.result.Result;
|
||||||
|
import com.competition.common.util.SecurityUtil;
|
||||||
import com.competition.modules.biz.contest.dto.CreateNoticeDto;
|
import com.competition.modules.biz.contest.dto.CreateNoticeDto;
|
||||||
import com.competition.modules.biz.contest.entity.BizContestNotice;
|
import com.competition.modules.biz.contest.entity.BizContestNotice;
|
||||||
import com.competition.modules.biz.contest.service.IContestNoticeService;
|
import com.competition.modules.biz.contest.service.IContestNoticeService;
|
||||||
@ -16,6 +17,7 @@ import org.springframework.util.StringUtils;
|
|||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Tag(name = "赛事公告")
|
@Tag(name = "赛事公告")
|
||||||
@ -26,6 +28,34 @@ public class ContestNoticeController {
|
|||||||
|
|
||||||
private final IContestNoticeService noticeService;
|
private final IContestNoticeService noticeService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析日期时间字符串,兼容 ISO 格式(带毫秒和 Z 时区标记)
|
||||||
|
*/
|
||||||
|
private LocalDateTime parseDateTime(String dateTime) {
|
||||||
|
if (!StringUtils.hasText(dateTime)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// 尝试 ISO 格式(yyyy-MM-dd'T'HH:mm:ss.SSSZ 或 yyyy-MM-dd'T'HH:mm:ss'Z')
|
||||||
|
try {
|
||||||
|
// 处理带 Z 的 ISO 格式
|
||||||
|
if (dateTime.endsWith("Z")) {
|
||||||
|
return LocalDateTime.parse(dateTime.substring(0, dateTime.length() - 1));
|
||||||
|
}
|
||||||
|
// 处理带毫秒的格式
|
||||||
|
if (dateTime.contains(".") && dateTime.indexOf(".") + 4 == dateTime.length()) {
|
||||||
|
return LocalDateTime.parse(dateTime.substring(0, dateTime.indexOf(".")));
|
||||||
|
}
|
||||||
|
return LocalDateTime.parse(dateTime);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 尝试空格分隔格式
|
||||||
|
try {
|
||||||
|
return LocalDateTime.parse(dateTime, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||||
|
} catch (Exception ex) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
@RequirePermission("notice:create")
|
@RequirePermission("notice:create")
|
||||||
@Operation(summary = "创建公告")
|
@Operation(summary = "创建公告")
|
||||||
@ -37,8 +67,10 @@ public class ContestNoticeController {
|
|||||||
notice.setNoticeType(dto.getNoticeType());
|
notice.setNoticeType(dto.getNoticeType());
|
||||||
notice.setPriority(dto.getPriority());
|
notice.setPriority(dto.getPriority());
|
||||||
if (StringUtils.hasText(dto.getPublishTime())) {
|
if (StringUtils.hasText(dto.getPublishTime())) {
|
||||||
notice.setPublishTime(LocalDateTime.parse(dto.getPublishTime()));
|
notice.setPublishTime(parseDateTime(dto.getPublishTime()));
|
||||||
}
|
}
|
||||||
|
// 设置当前租户 ID(租户隔离)
|
||||||
|
notice.setTenantId(SecurityUtil.getCurrentTenantId());
|
||||||
noticeService.save(notice);
|
noticeService.save(notice);
|
||||||
return Result.success(notice);
|
return Result.success(notice);
|
||||||
}
|
}
|
||||||
@ -47,9 +79,11 @@ public class ContestNoticeController {
|
|||||||
@RequirePermission("notice:read")
|
@RequirePermission("notice:read")
|
||||||
@Operation(summary = "查询赛事下的公告列表")
|
@Operation(summary = "查询赛事下的公告列表")
|
||||||
public Result<List<BizContestNotice>> findByContest(@PathVariable Long contestId) {
|
public Result<List<BizContestNotice>> findByContest(@PathVariable Long contestId) {
|
||||||
|
Long tenantId = SecurityUtil.getCurrentTenantId();
|
||||||
List<BizContestNotice> list = noticeService.list(
|
List<BizContestNotice> list = noticeService.list(
|
||||||
new LambdaQueryWrapper<BizContestNotice>()
|
new LambdaQueryWrapper<BizContestNotice>()
|
||||||
.eq(BizContestNotice::getContestId, contestId)
|
.eq(BizContestNotice::getContestId, contestId)
|
||||||
|
.eq(BizContestNotice::getTenantId, tenantId)
|
||||||
.orderByDesc(BizContestNotice::getCreateTime));
|
.orderByDesc(BizContestNotice::getCreateTime));
|
||||||
return Result.success(list);
|
return Result.success(list);
|
||||||
}
|
}
|
||||||
@ -60,10 +94,21 @@ public class ContestNoticeController {
|
|||||||
public Result<PageResult<BizContestNotice>> findAll(
|
public Result<PageResult<BizContestNotice>> findAll(
|
||||||
@RequestParam(defaultValue = "1") Long page,
|
@RequestParam(defaultValue = "1") Long page,
|
||||||
@RequestParam(defaultValue = "10") Long pageSize,
|
@RequestParam(defaultValue = "10") Long pageSize,
|
||||||
@RequestParam(required = false) String title) {
|
@RequestParam(required = false) String title,
|
||||||
|
@RequestParam(required = false) String status) {
|
||||||
|
Long tenantId = SecurityUtil.getCurrentTenantId();
|
||||||
LambdaQueryWrapper<BizContestNotice> wrapper = new LambdaQueryWrapper<BizContestNotice>()
|
LambdaQueryWrapper<BizContestNotice> wrapper = new LambdaQueryWrapper<BizContestNotice>()
|
||||||
.like(StringUtils.hasText(title), BizContestNotice::getTitle, title)
|
.eq(BizContestNotice::getTenantId, tenantId) // 租户隔离
|
||||||
.orderByDesc(BizContestNotice::getCreateTime);
|
.like(StringUtils.hasText(title), BizContestNotice::getTitle, title);
|
||||||
|
|
||||||
|
// 发布状态过滤
|
||||||
|
if ("published".equals(status)) {
|
||||||
|
wrapper.isNotNull(BizContestNotice::getPublishTime);
|
||||||
|
} else if ("unpublished".equals(status)) {
|
||||||
|
wrapper.isNull(BizContestNotice::getPublishTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper.orderByDesc(BizContestNotice::getCreateTime);
|
||||||
Page<BizContestNotice> result = noticeService.page(new Page<>(page, pageSize), wrapper);
|
Page<BizContestNotice> result = noticeService.page(new Page<>(page, pageSize), wrapper);
|
||||||
return Result.success(PageResult.from(result));
|
return Result.success(PageResult.from(result));
|
||||||
}
|
}
|
||||||
@ -86,7 +131,7 @@ public class ContestNoticeController {
|
|||||||
notice.setNoticeType(dto.getNoticeType());
|
notice.setNoticeType(dto.getNoticeType());
|
||||||
notice.setPriority(dto.getPriority());
|
notice.setPriority(dto.getPriority());
|
||||||
if (StringUtils.hasText(dto.getPublishTime())) {
|
if (StringUtils.hasText(dto.getPublishTime())) {
|
||||||
notice.setPublishTime(LocalDateTime.parse(dto.getPublishTime()));
|
notice.setPublishTime(parseDateTime(dto.getPublishTime()));
|
||||||
}
|
}
|
||||||
noticeService.updateById(notice);
|
noticeService.updateById(notice);
|
||||||
return Result.success();
|
return Result.success();
|
||||||
|
|||||||
@ -37,7 +37,9 @@ public class ContestRegistrationController {
|
|||||||
@RequirePermission("contest:read")
|
@RequirePermission("contest:read")
|
||||||
@Operation(summary = "获取报名统计")
|
@Operation(summary = "获取报名统计")
|
||||||
public Result<Map<String, Object>> getStats(@RequestParam(required = false) Long contestId) {
|
public Result<Map<String, Object>> getStats(@RequestParam(required = false) Long contestId) {
|
||||||
return Result.success(registrationService.getStats(contestId, SecurityUtil.getCurrentTenantId()));
|
Long tenantId = SecurityUtil.getCurrentTenantId();
|
||||||
|
boolean isSuperAdmin = SecurityUtil.isSuperAdmin();
|
||||||
|
return Result.success(registrationService.getStats(contestId, tenantId, isSuperAdmin));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||||
import com.competition.common.entity.BaseEntity;
|
import com.competition.common.entity.BaseEntity;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
@ -17,136 +18,166 @@ import java.util.List;
|
|||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
@TableName(value = "t_biz_contest", autoResultMap = true)
|
@TableName(value = "t_biz_contest", autoResultMap = true)
|
||||||
|
@Schema(description = "赛事实体")
|
||||||
public class BizContest extends BaseEntity {
|
public class BizContest extends BaseEntity {
|
||||||
|
|
||||||
/** 赛事名称(唯一) */
|
@Schema(description = "赛事名称")
|
||||||
@TableField("contest_name")
|
@TableField("contest_name")
|
||||||
private String contestName;
|
private String contestName;
|
||||||
|
|
||||||
/** 赛事类型:individual/team */
|
@Schema(description = "赛事类型:individual/team")
|
||||||
@TableField("contest_type")
|
@TableField("contest_type")
|
||||||
private String contestType;
|
private String contestType;
|
||||||
|
|
||||||
/** 赛事发布状态:unpublished/published */
|
@Schema(description = "赛事发布状态:unpublished/published")
|
||||||
@TableField("contest_state")
|
@TableField("contest_state")
|
||||||
private String contestState;
|
private String contestState;
|
||||||
|
|
||||||
/** 赛事进度状态:ongoing/finished */
|
@Schema(description = "赛事进度状态:ongoing/finished")
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
/** 开始时间 */
|
@Schema(description = "开始时间")
|
||||||
@TableField("start_time")
|
@TableField("start_time")
|
||||||
private LocalDateTime startTime;
|
private LocalDateTime startTime;
|
||||||
|
|
||||||
/** 结束时间 */
|
@Schema(description = "结束时间")
|
||||||
@TableField("end_time")
|
@TableField("end_time")
|
||||||
private LocalDateTime endTime;
|
private LocalDateTime endTime;
|
||||||
|
|
||||||
/** 线下地址 */
|
@Schema(description = "线下地址")
|
||||||
private String address;
|
private String address;
|
||||||
|
|
||||||
/** 赛事详情(富文本) */
|
@Schema(description = "赛事详情(富文本)")
|
||||||
private String content;
|
private String content;
|
||||||
|
|
||||||
/** 可见范围:public/designated/internal */
|
@Schema(description = "可见范围:public/designated/internal")
|
||||||
private String visibility;
|
private String visibility;
|
||||||
|
|
||||||
// ====== 授权租户(JSON) ======
|
// ====== 授权租户(JSON) ======
|
||||||
/** 授权租户 ID 数组 */
|
@Schema(description = "授权租户ID数组")
|
||||||
@TableField(value = "contest_tenants", typeHandler = JacksonTypeHandler.class)
|
@TableField(value = "contest_tenants", typeHandler = JacksonTypeHandler.class)
|
||||||
private List<Integer> contestTenants;
|
private List<Integer> contestTenants;
|
||||||
|
|
||||||
// ====== 封面和联系方式 ======
|
// ====== 封面和联系方式 ======
|
||||||
|
@Schema(description = "封面图URL")
|
||||||
@TableField("cover_url")
|
@TableField("cover_url")
|
||||||
private String coverUrl;
|
private String coverUrl;
|
||||||
|
|
||||||
|
@Schema(description = "海报URL")
|
||||||
@TableField("poster_url")
|
@TableField("poster_url")
|
||||||
private String posterUrl;
|
private String posterUrl;
|
||||||
|
|
||||||
|
@Schema(description = "联系人姓名")
|
||||||
@TableField("contact_name")
|
@TableField("contact_name")
|
||||||
private String contactName;
|
private String contactName;
|
||||||
|
|
||||||
|
@Schema(description = "联系电话")
|
||||||
@TableField("contact_phone")
|
@TableField("contact_phone")
|
||||||
private String contactPhone;
|
private String contactPhone;
|
||||||
|
|
||||||
|
@Schema(description = "联系二维码")
|
||||||
@TableField("contact_qrcode")
|
@TableField("contact_qrcode")
|
||||||
private String contactQrcode;
|
private String contactQrcode;
|
||||||
|
|
||||||
// ====== 主办/协办/赞助(JSON) ======
|
// ====== 主办/协办/赞助(JSON) ======
|
||||||
|
@Schema(description = "主办方信息(JSON)")
|
||||||
@TableField(value = "organizers", typeHandler = JacksonTypeHandler.class)
|
@TableField(value = "organizers", typeHandler = JacksonTypeHandler.class)
|
||||||
private Object organizers;
|
private Object organizers;
|
||||||
|
|
||||||
|
@Schema(description = "协办方信息(JSON)")
|
||||||
@TableField(value = "co_organizers", typeHandler = JacksonTypeHandler.class)
|
@TableField(value = "co_organizers", typeHandler = JacksonTypeHandler.class)
|
||||||
private Object coOrganizers;
|
private Object coOrganizers;
|
||||||
|
|
||||||
|
@Schema(description = "赞助方信息(JSON)")
|
||||||
@TableField(value = "sponsors", typeHandler = JacksonTypeHandler.class)
|
@TableField(value = "sponsors", typeHandler = JacksonTypeHandler.class)
|
||||||
private Object sponsors;
|
private Object sponsors;
|
||||||
|
|
||||||
// ====== 报名配置 ======
|
// ====== 报名配置 ======
|
||||||
|
@Schema(description = "报名开始时间")
|
||||||
@TableField("register_start_time")
|
@TableField("register_start_time")
|
||||||
private LocalDateTime registerStartTime;
|
private LocalDateTime registerStartTime;
|
||||||
|
|
||||||
|
@Schema(description = "报名结束时间")
|
||||||
@TableField("register_end_time")
|
@TableField("register_end_time")
|
||||||
private LocalDateTime registerEndTime;
|
private LocalDateTime registerEndTime;
|
||||||
|
|
||||||
|
@Schema(description = "报名状态")
|
||||||
@TableField("register_state")
|
@TableField("register_state")
|
||||||
private String registerState;
|
private String registerState;
|
||||||
|
|
||||||
|
@Schema(description = "是否需要审核")
|
||||||
@TableField("require_audit")
|
@TableField("require_audit")
|
||||||
private Boolean requireAudit;
|
private Boolean requireAudit;
|
||||||
|
|
||||||
|
@Schema(description = "允许参赛的年级(JSON数组)")
|
||||||
@TableField(value = "allowed_grades", typeHandler = JacksonTypeHandler.class)
|
@TableField(value = "allowed_grades", typeHandler = JacksonTypeHandler.class)
|
||||||
private List<Integer> allowedGrades;
|
private List<Integer> allowedGrades;
|
||||||
|
|
||||||
|
@Schema(description = "允许参赛的班级(JSON数组)")
|
||||||
@TableField(value = "allowed_classes", typeHandler = JacksonTypeHandler.class)
|
@TableField(value = "allowed_classes", typeHandler = JacksonTypeHandler.class)
|
||||||
private List<Integer> allowedClasses;
|
private List<Integer> allowedClasses;
|
||||||
|
|
||||||
|
@Schema(description = "团队最小人数")
|
||||||
@TableField("team_min_members")
|
@TableField("team_min_members")
|
||||||
private Integer teamMinMembers;
|
private Integer teamMinMembers;
|
||||||
|
|
||||||
|
@Schema(description = "团队最大人数")
|
||||||
@TableField("team_max_members")
|
@TableField("team_max_members")
|
||||||
private Integer teamMaxMembers;
|
private Integer teamMaxMembers;
|
||||||
|
|
||||||
// ====== 目标筛选 ======
|
// ====== 目标筛选 ======
|
||||||
|
@Schema(description = "目标城市(JSON数组)")
|
||||||
@TableField(value = "target_cities", typeHandler = JacksonTypeHandler.class)
|
@TableField(value = "target_cities", typeHandler = JacksonTypeHandler.class)
|
||||||
private List<String> targetCities;
|
private List<String> targetCities;
|
||||||
|
|
||||||
|
@Schema(description = "最小年龄")
|
||||||
@TableField("age_min")
|
@TableField("age_min")
|
||||||
private Integer ageMin;
|
private Integer ageMin;
|
||||||
|
|
||||||
|
@Schema(description = "最大年龄")
|
||||||
@TableField("age_max")
|
@TableField("age_max")
|
||||||
private Integer ageMax;
|
private Integer ageMax;
|
||||||
|
|
||||||
// ====== 提交配置 ======
|
// ====== 提交配置 ======
|
||||||
|
@Schema(description = "提交规则")
|
||||||
@TableField("submit_rule")
|
@TableField("submit_rule")
|
||||||
private String submitRule;
|
private String submitRule;
|
||||||
|
|
||||||
|
@Schema(description = "提交开始时间")
|
||||||
@TableField("submit_start_time")
|
@TableField("submit_start_time")
|
||||||
private LocalDateTime submitStartTime;
|
private LocalDateTime submitStartTime;
|
||||||
|
|
||||||
|
@Schema(description = "提交结束时间")
|
||||||
@TableField("submit_end_time")
|
@TableField("submit_end_time")
|
||||||
private LocalDateTime submitEndTime;
|
private LocalDateTime submitEndTime;
|
||||||
|
|
||||||
|
@Schema(description = "作品类型")
|
||||||
@TableField("work_type")
|
@TableField("work_type")
|
||||||
private String workType;
|
private String workType;
|
||||||
|
|
||||||
|
@Schema(description = "作品要求")
|
||||||
@TableField("work_requirement")
|
@TableField("work_requirement")
|
||||||
private String workRequirement;
|
private String workRequirement;
|
||||||
|
|
||||||
// ====== 评审配置 ======
|
// ====== 评审配置 ======
|
||||||
|
@Schema(description = "评审规则ID")
|
||||||
@TableField("review_rule_id")
|
@TableField("review_rule_id")
|
||||||
private Long reviewRuleId;
|
private Long reviewRuleId;
|
||||||
|
|
||||||
|
@Schema(description = "评审开始时间")
|
||||||
@TableField("review_start_time")
|
@TableField("review_start_time")
|
||||||
private LocalDateTime reviewStartTime;
|
private LocalDateTime reviewStartTime;
|
||||||
|
|
||||||
|
@Schema(description = "评审结束时间")
|
||||||
@TableField("review_end_time")
|
@TableField("review_end_time")
|
||||||
private LocalDateTime reviewEndTime;
|
private LocalDateTime reviewEndTime;
|
||||||
|
|
||||||
// ====== 成果发布 ======
|
// ====== 成果发布 ======
|
||||||
|
@Schema(description = "成绩发布状态")
|
||||||
@TableField("result_state")
|
@TableField("result_state")
|
||||||
private String resultState;
|
private String resultState;
|
||||||
|
|
||||||
|
@Schema(description = "成绩发布时间")
|
||||||
@TableField("result_publish_time")
|
@TableField("result_publish_time")
|
||||||
private LocalDateTime resultPublishTime;
|
private LocalDateTime resultPublishTime;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,27 +3,35 @@ package com.competition.modules.biz.contest.entity;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import com.competition.common.entity.BaseEntity;
|
import com.competition.common.entity.BaseEntity;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
@TableName("t_biz_contest_attachment")
|
@TableName("t_biz_contest_attachment")
|
||||||
|
@Schema(description = "赛事附件实体")
|
||||||
public class BizContestAttachment extends BaseEntity {
|
public class BizContestAttachment extends BaseEntity {
|
||||||
|
|
||||||
|
@Schema(description = "赛事ID")
|
||||||
@TableField("contest_id")
|
@TableField("contest_id")
|
||||||
private Long contestId;
|
private Long contestId;
|
||||||
|
|
||||||
|
@Schema(description = "文件名称")
|
||||||
@TableField("file_name")
|
@TableField("file_name")
|
||||||
private String fileName;
|
private String fileName;
|
||||||
|
|
||||||
|
@Schema(description = "文件URL")
|
||||||
@TableField("file_url")
|
@TableField("file_url")
|
||||||
private String fileUrl;
|
private String fileUrl;
|
||||||
|
|
||||||
|
@Schema(description = "文件格式")
|
||||||
private String format;
|
private String format;
|
||||||
|
|
||||||
|
@Schema(description = "文件类型")
|
||||||
@TableField("file_type")
|
@TableField("file_type")
|
||||||
private String fileType;
|
private String fileType;
|
||||||
|
|
||||||
|
@Schema(description = "文件大小")
|
||||||
private String size;
|
private String size;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,29 +3,43 @@ package com.competition.modules.biz.contest.entity;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import com.competition.common.entity.BaseEntity;
|
import com.competition.common.entity.BaseEntity;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 赛事公告实体
|
||||||
|
*/
|
||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
@TableName("t_biz_contest_notice")
|
@TableName("t_biz_contest_notice")
|
||||||
|
@Schema(description = "赛事公告实体")
|
||||||
public class BizContestNotice extends BaseEntity {
|
public class BizContestNotice extends BaseEntity {
|
||||||
|
|
||||||
|
@Schema(description = "赛事ID")
|
||||||
@TableField("contest_id")
|
@TableField("contest_id")
|
||||||
private Long contestId;
|
private Long contestId;
|
||||||
|
|
||||||
|
@Schema(description = "租户ID")
|
||||||
|
@TableField("tenant_id")
|
||||||
|
private Long tenantId;
|
||||||
|
|
||||||
|
@Schema(description = "公告标题")
|
||||||
private String title;
|
private String title;
|
||||||
|
|
||||||
|
@Schema(description = "公告内容(富文本)")
|
||||||
private String content;
|
private String content;
|
||||||
|
|
||||||
/** system/manual/urgent */
|
@Schema(description = "公告类型:system/manual/urgent")
|
||||||
@TableField("notice_type")
|
@TableField("notice_type")
|
||||||
private String noticeType;
|
private String noticeType;
|
||||||
|
|
||||||
|
@Schema(description = "优先级")
|
||||||
private Integer priority;
|
private Integer priority;
|
||||||
|
|
||||||
|
@Schema(description = "发布时间")
|
||||||
@TableField("publish_time")
|
@TableField("publish_time")
|
||||||
private LocalDateTime publishTime;
|
private LocalDateTime publishTime;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package com.competition.modules.biz.contest.entity;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import com.competition.common.entity.BaseEntity;
|
import com.competition.common.entity.BaseEntity;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
@ -11,61 +12,70 @@ import java.time.LocalDateTime;
|
|||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
@TableName("t_biz_contest_registration")
|
@TableName("t_biz_contest_registration")
|
||||||
|
@Schema(description = "赛事报名实体")
|
||||||
public class BizContestRegistration extends BaseEntity {
|
public class BizContestRegistration extends BaseEntity {
|
||||||
|
|
||||||
|
@Schema(description = "赛事ID")
|
||||||
@TableField("contest_id")
|
@TableField("contest_id")
|
||||||
private Long contestId;
|
private Long contestId;
|
||||||
|
|
||||||
|
@Schema(description = "租户ID")
|
||||||
@TableField("tenant_id")
|
@TableField("tenant_id")
|
||||||
private Long tenantId;
|
private Long tenantId;
|
||||||
|
|
||||||
/** 报名类型:individual/team */
|
@Schema(description = "报名类型:individual/team")
|
||||||
@TableField("registration_type")
|
@TableField("registration_type")
|
||||||
private String registrationType;
|
private String registrationType;
|
||||||
|
|
||||||
|
@Schema(description = "团队ID")
|
||||||
@TableField("team_id")
|
@TableField("team_id")
|
||||||
private Long teamId;
|
private Long teamId;
|
||||||
|
|
||||||
/** 团队名称快照 */
|
@Schema(description = "团队名称快照")
|
||||||
@TableField("team_name")
|
@TableField("team_name")
|
||||||
private String teamName;
|
private String teamName;
|
||||||
|
|
||||||
|
@Schema(description = "用户ID")
|
||||||
@TableField("user_id")
|
@TableField("user_id")
|
||||||
private Long userId;
|
private Long userId;
|
||||||
|
|
||||||
/** 账号快照 */
|
@Schema(description = "账号快照")
|
||||||
@TableField("account_no")
|
@TableField("account_no")
|
||||||
private String accountNo;
|
private String accountNo;
|
||||||
|
|
||||||
|
@Schema(description = "账号名称")
|
||||||
@TableField("account_name")
|
@TableField("account_name")
|
||||||
private String accountName;
|
private String accountName;
|
||||||
|
|
||||||
/** 角色快照:leader/member/mentor */
|
@Schema(description = "角色:leader/member/mentor")
|
||||||
private String role;
|
private String role;
|
||||||
|
|
||||||
/** 报名状态:pending/passed/rejected/withdrawn */
|
@Schema(description = "报名状态:pending/passed/rejected/withdrawn")
|
||||||
@TableField("registration_state")
|
@TableField("registration_state")
|
||||||
private String registrationState;
|
private String registrationState;
|
||||||
|
|
||||||
/** 参与者类型:self/child */
|
@Schema(description = "参与者类型:self/child")
|
||||||
@TableField("participant_type")
|
@TableField("participant_type")
|
||||||
private String participantType;
|
private String participantType;
|
||||||
|
|
||||||
|
@Schema(description = "子女ID")
|
||||||
@TableField("child_id")
|
@TableField("child_id")
|
||||||
private Long childId;
|
private Long childId;
|
||||||
|
|
||||||
/** 实际提交人 ID */
|
@Schema(description = "实际提交人ID")
|
||||||
private Integer registrant;
|
private Integer registrant;
|
||||||
|
|
||||||
|
@Schema(description = "报名时间")
|
||||||
@TableField("registration_time")
|
@TableField("registration_time")
|
||||||
private LocalDateTime registrationTime;
|
private LocalDateTime registrationTime;
|
||||||
|
|
||||||
/** 审核原因 */
|
@Schema(description = "审核原因")
|
||||||
private String reason;
|
private String reason;
|
||||||
|
|
||||||
/** 审核操作人 */
|
@Schema(description = "审核操作人")
|
||||||
private Integer operator;
|
private Integer operator;
|
||||||
|
|
||||||
|
@Schema(description = "操作日期")
|
||||||
@TableField("operation_date")
|
@TableField("operation_date")
|
||||||
private LocalDateTime operationDate;
|
private LocalDateTime operationDate;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package com.competition.modules.biz.contest.entity;
|
package com.competition.modules.biz.contest.entity;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.*;
|
import com.baomidou.mybatisplus.annotation.*;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
@ -8,29 +9,40 @@ import java.time.LocalDateTime;
|
|||||||
|
|
||||||
@Data
|
@Data
|
||||||
@TableName("t_biz_contest_registration_teacher")
|
@TableName("t_biz_contest_registration_teacher")
|
||||||
|
@Schema(description = "赛事报名老师关联实体")
|
||||||
public class BizContestRegistrationTeacher implements Serializable {
|
public class BizContestRegistrationTeacher implements Serializable {
|
||||||
|
|
||||||
|
@Schema(description = "主键ID")
|
||||||
@TableId(type = IdType.AUTO)
|
@TableId(type = IdType.AUTO)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "报名ID")
|
||||||
@TableField("registration_id")
|
@TableField("registration_id")
|
||||||
private Long registrationId;
|
private Long registrationId;
|
||||||
|
|
||||||
|
@Schema(description = "租户ID")
|
||||||
@TableField("tenant_id")
|
@TableField("tenant_id")
|
||||||
private Long tenantId;
|
private Long tenantId;
|
||||||
|
|
||||||
|
@Schema(description = "用户ID")
|
||||||
@TableField("user_id")
|
@TableField("user_id")
|
||||||
private Long userId;
|
private Long userId;
|
||||||
|
|
||||||
|
@Schema(description = "是否默认")
|
||||||
@TableField("is_default")
|
@TableField("is_default")
|
||||||
private Boolean isDefault;
|
private Boolean isDefault;
|
||||||
|
|
||||||
|
@Schema(description = "创建人ID")
|
||||||
private Integer creator;
|
private Integer creator;
|
||||||
|
|
||||||
|
@Schema(description = "修改人ID")
|
||||||
private Integer modifier;
|
private Integer modifier;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
@TableField("create_time")
|
@TableField("create_time")
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
@Schema(description = "修改时间")
|
||||||
@TableField("modify_time")
|
@TableField("modify_time")
|
||||||
private LocalDateTime modifyTime;
|
private LocalDateTime modifyTime;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,26 +3,33 @@ package com.competition.modules.biz.contest.entity;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import com.competition.common.entity.BaseEntity;
|
import com.competition.common.entity.BaseEntity;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
@TableName("t_biz_contest_team")
|
@TableName("t_biz_contest_team")
|
||||||
|
@Schema(description = "赛事团队实体")
|
||||||
public class BizContestTeam extends BaseEntity {
|
public class BizContestTeam extends BaseEntity {
|
||||||
|
|
||||||
|
@Schema(description = "租户ID")
|
||||||
@TableField("tenant_id")
|
@TableField("tenant_id")
|
||||||
private Long tenantId;
|
private Long tenantId;
|
||||||
|
|
||||||
|
@Schema(description = "赛事ID")
|
||||||
@TableField("contest_id")
|
@TableField("contest_id")
|
||||||
private Long contestId;
|
private Long contestId;
|
||||||
|
|
||||||
|
@Schema(description = "团队名称")
|
||||||
@TableField("team_name")
|
@TableField("team_name")
|
||||||
private String teamName;
|
private String teamName;
|
||||||
|
|
||||||
|
@Schema(description = "队长用户ID")
|
||||||
@TableField("leader_user_id")
|
@TableField("leader_user_id")
|
||||||
private Long leaderUserId;
|
private Long leaderUserId;
|
||||||
|
|
||||||
|
@Schema(description = "最大成员数")
|
||||||
@TableField("max_members")
|
@TableField("max_members")
|
||||||
private Integer maxMembers;
|
private Integer maxMembers;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package com.competition.modules.biz.contest.entity;
|
package com.competition.modules.biz.contest.entity;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.*;
|
import com.baomidou.mybatisplus.annotation.*;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
@ -8,29 +9,39 @@ import java.time.LocalDateTime;
|
|||||||
|
|
||||||
@Data
|
@Data
|
||||||
@TableName("t_biz_contest_team_member")
|
@TableName("t_biz_contest_team_member")
|
||||||
|
@Schema(description = "赛事团队成员实体")
|
||||||
public class BizContestTeamMember implements Serializable {
|
public class BizContestTeamMember implements Serializable {
|
||||||
|
|
||||||
|
@Schema(description = "主键ID")
|
||||||
@TableId(type = IdType.AUTO)
|
@TableId(type = IdType.AUTO)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "租户ID")
|
||||||
@TableField("tenant_id")
|
@TableField("tenant_id")
|
||||||
private Long tenantId;
|
private Long tenantId;
|
||||||
|
|
||||||
|
@Schema(description = "团队ID")
|
||||||
@TableField("team_id")
|
@TableField("team_id")
|
||||||
private Long teamId;
|
private Long teamId;
|
||||||
|
|
||||||
|
@Schema(description = "用户ID")
|
||||||
@TableField("user_id")
|
@TableField("user_id")
|
||||||
private Long userId;
|
private Long userId;
|
||||||
|
|
||||||
/** 角色:member/leader/mentor */
|
@Schema(description = "角色:member/leader/mentor")
|
||||||
private String role;
|
private String role;
|
||||||
|
|
||||||
|
@Schema(description = "创建人ID")
|
||||||
private Integer creator;
|
private Integer creator;
|
||||||
|
|
||||||
|
@Schema(description = "修改人ID")
|
||||||
private Integer modifier;
|
private Integer modifier;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
@TableField("create_time")
|
@TableField("create_time")
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
@Schema(description = "修改时间")
|
||||||
@TableField("modify_time")
|
@TableField("modify_time")
|
||||||
private LocalDateTime modifyTime;
|
private LocalDateTime modifyTime;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||||
import com.competition.common.entity.BaseEntity;
|
import com.competition.common.entity.BaseEntity;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
@ -14,75 +15,95 @@ import java.util.List;
|
|||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
@TableName(value = "t_biz_contest_work", autoResultMap = true)
|
@TableName(value = "t_biz_contest_work", autoResultMap = true)
|
||||||
|
@Schema(description = "赛事作品实体")
|
||||||
public class BizContestWork extends BaseEntity {
|
public class BizContestWork extends BaseEntity {
|
||||||
|
|
||||||
|
@Schema(description = "租户ID")
|
||||||
@TableField("tenant_id")
|
@TableField("tenant_id")
|
||||||
private Long tenantId;
|
private Long tenantId;
|
||||||
|
|
||||||
|
@Schema(description = "赛事ID")
|
||||||
@TableField("contest_id")
|
@TableField("contest_id")
|
||||||
private Long contestId;
|
private Long contestId;
|
||||||
|
|
||||||
|
@Schema(description = "报名ID")
|
||||||
@TableField("registration_id")
|
@TableField("registration_id")
|
||||||
private Long registrationId;
|
private Long registrationId;
|
||||||
|
|
||||||
/** 作品编号(唯一展示编号) */
|
@Schema(description = "作品编号")
|
||||||
@TableField("work_no")
|
@TableField("work_no")
|
||||||
private String workNo;
|
private String workNo;
|
||||||
|
|
||||||
|
@Schema(description = "作品标题")
|
||||||
private String title;
|
private String title;
|
||||||
|
|
||||||
|
@Schema(description = "作品描述")
|
||||||
private String description;
|
private String description;
|
||||||
|
|
||||||
|
@Schema(description = "作品文件(JSON)")
|
||||||
@TableField(value = "files", typeHandler = JacksonTypeHandler.class)
|
@TableField(value = "files", typeHandler = JacksonTypeHandler.class)
|
||||||
private Object files;
|
private Object files;
|
||||||
|
|
||||||
|
@Schema(description = "版本号")
|
||||||
private Integer version;
|
private Integer version;
|
||||||
|
|
||||||
|
@Schema(description = "是否最新版本")
|
||||||
@TableField("is_latest")
|
@TableField("is_latest")
|
||||||
private Boolean isLatest;
|
private Boolean isLatest;
|
||||||
|
|
||||||
/** submitted/locked/reviewing/rejected/accepted */
|
@Schema(description = "作品状态:submitted/locked/reviewing/rejected/accepted")
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
|
@Schema(description = "提交时间")
|
||||||
@TableField("submit_time")
|
@TableField("submit_time")
|
||||||
private LocalDateTime submitTime;
|
private LocalDateTime submitTime;
|
||||||
|
|
||||||
|
@Schema(description = "提交人用户ID")
|
||||||
@TableField("submitter_user_id")
|
@TableField("submitter_user_id")
|
||||||
private Long submitterUserId;
|
private Long submitterUserId;
|
||||||
|
|
||||||
|
@Schema(description = "提交人账号")
|
||||||
@TableField("submitter_account_no")
|
@TableField("submitter_account_no")
|
||||||
private String submitterAccountNo;
|
private String submitterAccountNo;
|
||||||
|
|
||||||
/** teacher/student/team_leader */
|
@Schema(description = "提交来源:teacher/student/team_leader")
|
||||||
@TableField("submit_source")
|
@TableField("submit_source")
|
||||||
private String submitSource;
|
private String submitSource;
|
||||||
|
|
||||||
|
@Schema(description = "预览图URL")
|
||||||
@TableField("preview_url")
|
@TableField("preview_url")
|
||||||
private String previewUrl;
|
private String previewUrl;
|
||||||
|
|
||||||
|
@Schema(description = "预览图URL列表(JSON)")
|
||||||
@TableField(value = "preview_urls", typeHandler = JacksonTypeHandler.class)
|
@TableField(value = "preview_urls", typeHandler = JacksonTypeHandler.class)
|
||||||
private List<String> previewUrls;
|
private List<String> previewUrls;
|
||||||
|
|
||||||
|
@Schema(description = "AI模型元数据(JSON)")
|
||||||
@TableField(value = "ai_model_meta", typeHandler = JacksonTypeHandler.class)
|
@TableField(value = "ai_model_meta", typeHandler = JacksonTypeHandler.class)
|
||||||
private Object aiModelMeta;
|
private Object aiModelMeta;
|
||||||
|
|
||||||
|
@Schema(description = "用户作品ID")
|
||||||
@TableField("user_work_id")
|
@TableField("user_work_id")
|
||||||
private Long userWorkId;
|
private Long userWorkId;
|
||||||
|
|
||||||
// ====== 赛果字段 ======
|
// ====== 赛果字段 ======
|
||||||
|
@Schema(description = "最终得分")
|
||||||
@TableField("final_score")
|
@TableField("final_score")
|
||||||
private BigDecimal finalScore;
|
private BigDecimal finalScore;
|
||||||
|
|
||||||
|
@Schema(description = "排名")
|
||||||
@TableField("`rank`")
|
@TableField("`rank`")
|
||||||
private Integer rank;
|
private Integer rank;
|
||||||
|
|
||||||
/** first/second/third/excellent/none */
|
@Schema(description = "获奖等级:first/second/third/excellent/none")
|
||||||
@TableField("award_level")
|
@TableField("award_level")
|
||||||
private String awardLevel;
|
private String awardLevel;
|
||||||
|
|
||||||
|
@Schema(description = "奖项名称")
|
||||||
@TableField("award_name")
|
@TableField("award_name")
|
||||||
private String awardName;
|
private String awardName;
|
||||||
|
|
||||||
|
@Schema(description = "证书URL")
|
||||||
@TableField("certificate_url")
|
@TableField("certificate_url")
|
||||||
private String certificateUrl;
|
private String certificateUrl;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package com.competition.modules.biz.contest.entity;
|
package com.competition.modules.biz.contest.entity;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.*;
|
import com.baomidou.mybatisplus.annotation.*;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
@ -8,39 +9,54 @@ import java.time.LocalDateTime;
|
|||||||
|
|
||||||
@Data
|
@Data
|
||||||
@TableName("t_biz_contest_work_attachment")
|
@TableName("t_biz_contest_work_attachment")
|
||||||
|
@Schema(description = "赛事作品附件实体")
|
||||||
public class BizContestWorkAttachment implements Serializable {
|
public class BizContestWorkAttachment implements Serializable {
|
||||||
|
|
||||||
|
@Schema(description = "主键ID")
|
||||||
@TableId(type = IdType.AUTO)
|
@TableId(type = IdType.AUTO)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "租户ID")
|
||||||
@TableField("tenant_id")
|
@TableField("tenant_id")
|
||||||
private Long tenantId;
|
private Long tenantId;
|
||||||
|
|
||||||
|
@Schema(description = "赛事ID")
|
||||||
@TableField("contest_id")
|
@TableField("contest_id")
|
||||||
private Long contestId;
|
private Long contestId;
|
||||||
|
|
||||||
|
@Schema(description = "作品ID")
|
||||||
@TableField("work_id")
|
@TableField("work_id")
|
||||||
private Long workId;
|
private Long workId;
|
||||||
|
|
||||||
|
@Schema(description = "文件名称")
|
||||||
@TableField("file_name")
|
@TableField("file_name")
|
||||||
private String fileName;
|
private String fileName;
|
||||||
|
|
||||||
|
@Schema(description = "文件URL")
|
||||||
@TableField("file_url")
|
@TableField("file_url")
|
||||||
private String fileUrl;
|
private String fileUrl;
|
||||||
|
|
||||||
|
@Schema(description = "文件格式")
|
||||||
private String format;
|
private String format;
|
||||||
|
|
||||||
|
@Schema(description = "文件类型")
|
||||||
@TableField("file_type")
|
@TableField("file_type")
|
||||||
private String fileType;
|
private String fileType;
|
||||||
|
|
||||||
|
@Schema(description = "文件大小")
|
||||||
private String size;
|
private String size;
|
||||||
|
|
||||||
|
@Schema(description = "创建人ID")
|
||||||
private Integer creator;
|
private Integer creator;
|
||||||
|
|
||||||
|
@Schema(description = "修改人ID")
|
||||||
private Integer modifier;
|
private Integer modifier;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
@TableField("create_time")
|
@TableField("create_time")
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
@Schema(description = "修改时间")
|
||||||
@TableField("modify_time")
|
@TableField("modify_time")
|
||||||
private LocalDateTime modifyTime;
|
private LocalDateTime modifyTime;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,7 @@ public interface IContestRegistrationService extends IService<BizContestRegistra
|
|||||||
|
|
||||||
PageResult<Map<String, Object>> findAll(QueryRegistrationDto dto, Long tenantId, boolean isSuperTenant);
|
PageResult<Map<String, Object>> findAll(QueryRegistrationDto dto, Long tenantId, boolean isSuperTenant);
|
||||||
|
|
||||||
Map<String, Object> getStats(Long contestId, Long tenantId);
|
Map<String, Object> getStats(Long contestId, Long tenantId, boolean isSuperAdmin);
|
||||||
|
|
||||||
Map<String, Object> findDetail(Long id, Long tenantId);
|
Map<String, Object> findDetail(Long id, Long tenantId);
|
||||||
|
|
||||||
|
|||||||
@ -106,6 +106,7 @@ public class ContestRegistrationServiceImpl extends ServiceImpl<ContestRegistrat
|
|||||||
.or().like(BizContestRegistration::getAccountName, dto.getKeyword()));
|
.or().like(BizContestRegistration::getAccountName, dto.getKeyword()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 租户过滤
|
||||||
if (!isSuperTenant && tenantId != null) {
|
if (!isSuperTenant && tenantId != null) {
|
||||||
wrapper.eq(BizContestRegistration::getTenantId, tenantId);
|
wrapper.eq(BizContestRegistration::getTenantId, tenantId);
|
||||||
}
|
}
|
||||||
@ -123,20 +124,28 @@ public class ContestRegistrationServiceImpl extends ServiceImpl<ContestRegistrat
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, Object> getStats(Long contestId, Long tenantId) {
|
public Map<String, Object> getStats(Long contestId, Long tenantId, boolean isSuperAdmin) {
|
||||||
log.info("获取报名统计,赛事ID:{}", contestId);
|
log.info("获取报名统计,赛事ID:{},租户ID:{},超管:{}", contestId, tenantId, isSuperAdmin);
|
||||||
|
|
||||||
|
// 非超管需要按租户过滤
|
||||||
|
boolean needTenantFilter = !isSuperAdmin && tenantId != null;
|
||||||
|
|
||||||
LambdaQueryWrapper<BizContestRegistration> baseWrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<BizContestRegistration> baseWrapper = new LambdaQueryWrapper<>();
|
||||||
if (contestId != null) {
|
if (contestId != null) {
|
||||||
baseWrapper.eq(BizContestRegistration::getContestId, contestId);
|
baseWrapper.eq(BizContestRegistration::getContestId, contestId);
|
||||||
}
|
}
|
||||||
|
if (needTenantFilter) {
|
||||||
|
baseWrapper.eq(BizContestRegistration::getTenantId, tenantId);
|
||||||
|
}
|
||||||
long total = count(baseWrapper);
|
long total = count(baseWrapper);
|
||||||
|
|
||||||
LambdaQueryWrapper<BizContestRegistration> pendingWrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<BizContestRegistration> pendingWrapper = new LambdaQueryWrapper<>();
|
||||||
if (contestId != null) {
|
if (contestId != null) {
|
||||||
pendingWrapper.eq(BizContestRegistration::getContestId, contestId);
|
pendingWrapper.eq(BizContestRegistration::getContestId, contestId);
|
||||||
}
|
}
|
||||||
|
if (needTenantFilter) {
|
||||||
|
pendingWrapper.eq(BizContestRegistration::getTenantId, tenantId);
|
||||||
|
}
|
||||||
pendingWrapper.eq(BizContestRegistration::getRegistrationState, "pending");
|
pendingWrapper.eq(BizContestRegistration::getRegistrationState, "pending");
|
||||||
long pending = count(pendingWrapper);
|
long pending = count(pendingWrapper);
|
||||||
|
|
||||||
@ -144,6 +153,9 @@ public class ContestRegistrationServiceImpl extends ServiceImpl<ContestRegistrat
|
|||||||
if (contestId != null) {
|
if (contestId != null) {
|
||||||
passedWrapper.eq(BizContestRegistration::getContestId, contestId);
|
passedWrapper.eq(BizContestRegistration::getContestId, contestId);
|
||||||
}
|
}
|
||||||
|
if (needTenantFilter) {
|
||||||
|
passedWrapper.eq(BizContestRegistration::getTenantId, tenantId);
|
||||||
|
}
|
||||||
passedWrapper.eq(BizContestRegistration::getRegistrationState, "passed");
|
passedWrapper.eq(BizContestRegistration::getRegistrationState, "passed");
|
||||||
long passed = count(passedWrapper);
|
long passed = count(passedWrapper);
|
||||||
|
|
||||||
@ -151,6 +163,9 @@ public class ContestRegistrationServiceImpl extends ServiceImpl<ContestRegistrat
|
|||||||
if (contestId != null) {
|
if (contestId != null) {
|
||||||
rejectedWrapper.eq(BizContestRegistration::getContestId, contestId);
|
rejectedWrapper.eq(BizContestRegistration::getContestId, contestId);
|
||||||
}
|
}
|
||||||
|
if (needTenantFilter) {
|
||||||
|
rejectedWrapper.eq(BizContestRegistration::getTenantId, tenantId);
|
||||||
|
}
|
||||||
rejectedWrapper.eq(BizContestRegistration::getRegistrationState, "rejected");
|
rejectedWrapper.eq(BizContestRegistration::getRegistrationState, "rejected");
|
||||||
long rejected = count(rejectedWrapper);
|
long rejected = count(rejectedWrapper);
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
|||||||
import com.competition.common.enums.ErrorCode;
|
import com.competition.common.enums.ErrorCode;
|
||||||
import com.competition.common.exception.BusinessException;
|
import com.competition.common.exception.BusinessException;
|
||||||
import com.competition.common.result.PageResult;
|
import com.competition.common.result.PageResult;
|
||||||
|
import com.competition.common.util.SecurityUtil;
|
||||||
import com.competition.modules.biz.contest.dto.CreateContestDto;
|
import com.competition.modules.biz.contest.dto.CreateContestDto;
|
||||||
import com.competition.modules.biz.contest.dto.QueryContestDto;
|
import com.competition.modules.biz.contest.dto.QueryContestDto;
|
||||||
import com.competition.modules.biz.contest.entity.BizContest;
|
import com.competition.modules.biz.contest.entity.BizContest;
|
||||||
@ -17,6 +18,10 @@ import com.competition.modules.biz.contest.mapper.ContestMapper;
|
|||||||
import com.competition.modules.biz.contest.mapper.ContestRegistrationMapper;
|
import com.competition.modules.biz.contest.mapper.ContestRegistrationMapper;
|
||||||
import com.competition.modules.biz.contest.mapper.ContestWorkMapper;
|
import com.competition.modules.biz.contest.mapper.ContestWorkMapper;
|
||||||
import com.competition.modules.biz.contest.service.IContestService;
|
import com.competition.modules.biz.contest.service.IContestService;
|
||||||
|
import com.competition.modules.biz.review.entity.BizContestWorkJudgeAssignment;
|
||||||
|
import com.competition.modules.biz.review.mapper.ContestWorkJudgeAssignmentMapper;
|
||||||
|
import com.competition.modules.sys.entity.SysTenant;
|
||||||
|
import com.competition.modules.sys.mapper.SysTenantMapper;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@ -36,28 +41,66 @@ public class ContestServiceImpl extends ServiceImpl<ContestMapper, BizContest> i
|
|||||||
private final ContestAttachmentMapper contestAttachmentMapper;
|
private final ContestAttachmentMapper contestAttachmentMapper;
|
||||||
private final ContestRegistrationMapper contestRegistrationMapper;
|
private final ContestRegistrationMapper contestRegistrationMapper;
|
||||||
private final ContestWorkMapper contestWorkMapper;
|
private final ContestWorkMapper contestWorkMapper;
|
||||||
|
private final ContestWorkJudgeAssignmentMapper contestWorkJudgeAssignmentMapper;
|
||||||
|
private final SysTenantMapper sysTenantMapper;
|
||||||
|
|
||||||
private static final DateTimeFormatter DT_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
|
// 支持两种日期格式:ISO 格式 (T 分隔) 和空格分隔格式
|
||||||
|
private static final DateTimeFormatter ISO_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
|
||||||
|
private static final DateTimeFormatter SPACE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析日期时间字符串,兼容 ISO 格式和空格分隔格式
|
||||||
|
*/
|
||||||
|
private LocalDateTime parseDateTime(String dateTime) {
|
||||||
|
if (!StringUtils.hasText(dateTime)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// 尝试 ISO 格式 (yyyy-MM-ddTHH:mm:ss)
|
||||||
|
try {
|
||||||
|
return LocalDateTime.parse(dateTime, ISO_FORMATTER);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 尝试空格分隔格式 (yyyy-MM-dd HH:mm:ss)
|
||||||
|
try {
|
||||||
|
return LocalDateTime.parse(dateTime, SPACE_FORMATTER);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.warn("日期格式解析失败:{}", dateTime, ex);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public BizContest createContest(CreateContestDto dto, Long creatorId) {
|
public BizContest createContest(CreateContestDto dto, Long creatorId) {
|
||||||
log.info("开始创建赛事,名称:{}", dto.getContestName());
|
log.info("开始创建赛事,名称:{}", dto.getContestName());
|
||||||
|
|
||||||
BizContest entity = new BizContest();
|
try {
|
||||||
mapDtoToEntity(dto, entity);
|
BizContest entity = new BizContest();
|
||||||
|
mapDtoToEntity(dto, entity);
|
||||||
|
|
||||||
// 默认状态
|
// 默认状态
|
||||||
entity.setContestState("unpublished");
|
entity.setContestState("unpublished");
|
||||||
entity.setStatus("ongoing");
|
entity.setStatus("ongoing");
|
||||||
entity.setResultState("unpublished");
|
entity.setResultState("unpublished");
|
||||||
if (!StringUtils.hasText(entity.getSubmitRule())) {
|
if (!StringUtils.hasText(entity.getSubmitRule())) {
|
||||||
entity.setSubmitRule("once");
|
entity.setSubmitRule("once");
|
||||||
|
}
|
||||||
|
entity.setCreator(creatorId != null ? creatorId.intValue() : null);
|
||||||
|
|
||||||
|
// 如果没有设置授权租户,默认添加当前租户
|
||||||
|
if (entity.getContestTenants() == null || entity.getContestTenants().isEmpty()) {
|
||||||
|
Long currentTenantId = SecurityUtil.getCurrentTenantId();
|
||||||
|
if (currentTenantId != null) {
|
||||||
|
entity.setContestTenants(Collections.singletonList(currentTenantId.intValue()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
save(entity);
|
||||||
|
log.info("赛事创建成功,ID:{}, 名称:{}", entity.getId(), entity.getContestName());
|
||||||
|
return entity;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("创建赛事失败,名称:{}", dto.getContestName(), e);
|
||||||
|
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "创建赛事失败:" + e.getMessage());
|
||||||
}
|
}
|
||||||
entity.setCreator(creatorId != null ? creatorId.intValue() : null);
|
|
||||||
|
|
||||||
save(entity);
|
|
||||||
log.info("赛事创建成功,ID:{}", entity.getId());
|
|
||||||
return entity;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -114,8 +157,60 @@ public class ContestServiceImpl extends ServiceImpl<ContestMapper, BizContest> i
|
|||||||
Page<BizContest> page = new Page<>(dto.getPage(), dto.getPageSize());
|
Page<BizContest> page = new Page<>(dto.getPage(), dto.getPageSize());
|
||||||
Page<BizContest> result = contestMapper.selectPage(page, wrapper);
|
Page<BizContest> result = contestMapper.selectPage(page, wrapper);
|
||||||
|
|
||||||
|
// 批量查询报名数和作品数
|
||||||
|
List<Long> contestIds = result.getRecords().stream()
|
||||||
|
.map(BizContest::getId).toList();
|
||||||
|
|
||||||
|
Map<Long, Long> registrationCountMap = new HashMap<>();
|
||||||
|
Map<Long, Long> workCountMap = new HashMap<>();
|
||||||
|
Map<Long, Long> reviewedWorkCountMap = new HashMap<>();
|
||||||
|
if (!contestIds.isEmpty()) {
|
||||||
|
// 报名数(所有状态)
|
||||||
|
contestRegistrationMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<BizContestRegistration>()
|
||||||
|
.in(BizContestRegistration::getContestId, contestIds))
|
||||||
|
.stream()
|
||||||
|
.collect(Collectors.groupingBy(BizContestRegistration::getContestId, Collectors.counting()))
|
||||||
|
.forEach(registrationCountMap::put);
|
||||||
|
|
||||||
|
// 作品:最新有效版本;评审完成数 = 已分配且该作品全部分配记录均为 completed(与评委端、ProgressDetail 一致)
|
||||||
|
List<BizContestWork> contestWorks = contestWorkMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<BizContestWork>()
|
||||||
|
.in(BizContestWork::getContestId, contestIds)
|
||||||
|
.eq(BizContestWork::getIsLatest, true)
|
||||||
|
.eq(BizContestWork::getValidState, 1));
|
||||||
|
Set<Long> workIdSet = contestWorks.stream().map(BizContestWork::getId).collect(Collectors.toSet());
|
||||||
|
Map<Long, List<BizContestWorkJudgeAssignment>> assignByWorkId = new HashMap<>();
|
||||||
|
if (!workIdSet.isEmpty()) {
|
||||||
|
List<BizContestWorkJudgeAssignment> allAssign = contestWorkJudgeAssignmentMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<BizContestWorkJudgeAssignment>()
|
||||||
|
.in(BizContestWorkJudgeAssignment::getWorkId, workIdSet));
|
||||||
|
assignByWorkId = allAssign.stream()
|
||||||
|
.collect(Collectors.groupingBy(BizContestWorkJudgeAssignment::getWorkId));
|
||||||
|
}
|
||||||
|
for (BizContestWork w : contestWorks) {
|
||||||
|
Long cid = w.getContestId();
|
||||||
|
workCountMap.merge(cid, 1L, Long::sum);
|
||||||
|
List<BizContestWorkJudgeAssignment> assigns = assignByWorkId.getOrDefault(w.getId(), Collections.emptyList());
|
||||||
|
if (!assigns.isEmpty() && assigns.stream().allMatch(a -> "completed".equals(a.getStatus()))) {
|
||||||
|
reviewedWorkCountMap.merge(cid, 1L, Long::sum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
List<Map<String, Object>> voList = result.getRecords().stream()
|
List<Map<String, Object>> voList = result.getRecords().stream()
|
||||||
.map(this::entityToMap)
|
.map(entity -> {
|
||||||
|
Map<String, Object> map = entityToMap(entity);
|
||||||
|
Map<String, Object> countMap = new LinkedHashMap<>();
|
||||||
|
long works = workCountMap.getOrDefault(entity.getId(), 0L);
|
||||||
|
long reviewedWorks = reviewedWorkCountMap.getOrDefault(entity.getId(), 0L);
|
||||||
|
countMap.put("registrations", registrationCountMap.getOrDefault(entity.getId(), 0L));
|
||||||
|
countMap.put("works", works);
|
||||||
|
map.put("_count", countMap);
|
||||||
|
map.put("totalWorksCount", works);
|
||||||
|
map.put("reviewedCount", reviewedWorks);
|
||||||
|
return map;
|
||||||
|
})
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
return PageResult.from(result, voList);
|
return PageResult.from(result, voList);
|
||||||
@ -167,6 +262,24 @@ public class ContestServiceImpl extends ServiceImpl<ContestMapper, BizContest> i
|
|||||||
List<BizContestAttachment> attachments = contestAttachmentMapper.selectList(attWrapper);
|
List<BizContestAttachment> attachments = contestAttachmentMapper.selectList(attWrapper);
|
||||||
result.put("attachments", attachments);
|
result.put("attachments", attachments);
|
||||||
|
|
||||||
|
// 查询授权租户的详细信息
|
||||||
|
List<Integer> tenantIds = contest.getContestTenants();
|
||||||
|
if (tenantIds != null && !tenantIds.isEmpty()) {
|
||||||
|
List<Map<String, Object>> tenantInfoList = new ArrayList<>();
|
||||||
|
for (Integer tenantId : tenantIds) {
|
||||||
|
SysTenant tenant = sysTenantMapper.selectById(tenantId.longValue());
|
||||||
|
if (tenant != null) {
|
||||||
|
Map<String, Object> tenantInfo = new LinkedHashMap<>();
|
||||||
|
tenantInfo.put("id", tenant.getId());
|
||||||
|
tenantInfo.put("name", tenant.getName());
|
||||||
|
tenantInfo.put("code", tenant.getCode());
|
||||||
|
tenantInfo.put("tenantType", tenant.getTenantType());
|
||||||
|
tenantInfoList.add(tenantInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.put("contestTenantInfos", tenantInfoList);
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -323,10 +436,10 @@ public class ContestServiceImpl extends ServiceImpl<ContestMapper, BizContest> i
|
|||||||
entity.setAgeMax(dto.getAgeMax());
|
entity.setAgeMax(dto.getAgeMax());
|
||||||
}
|
}
|
||||||
if (StringUtils.hasText(dto.getStartTime())) {
|
if (StringUtils.hasText(dto.getStartTime())) {
|
||||||
entity.setStartTime(LocalDateTime.parse(dto.getStartTime(), DT_FORMATTER));
|
entity.setStartTime(parseDateTime(dto.getStartTime()));
|
||||||
}
|
}
|
||||||
if (StringUtils.hasText(dto.getEndTime())) {
|
if (StringUtils.hasText(dto.getEndTime())) {
|
||||||
entity.setEndTime(LocalDateTime.parse(dto.getEndTime(), DT_FORMATTER));
|
entity.setEndTime(parseDateTime(dto.getEndTime()));
|
||||||
}
|
}
|
||||||
if (StringUtils.hasText(dto.getAddress())) {
|
if (StringUtils.hasText(dto.getAddress())) {
|
||||||
entity.setAddress(dto.getAddress());
|
entity.setAddress(dto.getAddress());
|
||||||
@ -362,10 +475,10 @@ public class ContestServiceImpl extends ServiceImpl<ContestMapper, BizContest> i
|
|||||||
entity.setSponsors(dto.getSponsors());
|
entity.setSponsors(dto.getSponsors());
|
||||||
}
|
}
|
||||||
if (StringUtils.hasText(dto.getRegisterStartTime())) {
|
if (StringUtils.hasText(dto.getRegisterStartTime())) {
|
||||||
entity.setRegisterStartTime(LocalDateTime.parse(dto.getRegisterStartTime(), DT_FORMATTER));
|
entity.setRegisterStartTime(parseDateTime(dto.getRegisterStartTime()));
|
||||||
}
|
}
|
||||||
if (StringUtils.hasText(dto.getRegisterEndTime())) {
|
if (StringUtils.hasText(dto.getRegisterEndTime())) {
|
||||||
entity.setRegisterEndTime(LocalDateTime.parse(dto.getRegisterEndTime(), DT_FORMATTER));
|
entity.setRegisterEndTime(parseDateTime(dto.getRegisterEndTime()));
|
||||||
}
|
}
|
||||||
if (StringUtils.hasText(dto.getRegisterState())) {
|
if (StringUtils.hasText(dto.getRegisterState())) {
|
||||||
entity.setRegisterState(dto.getRegisterState());
|
entity.setRegisterState(dto.getRegisterState());
|
||||||
@ -389,10 +502,10 @@ public class ContestServiceImpl extends ServiceImpl<ContestMapper, BizContest> i
|
|||||||
entity.setSubmitRule(dto.getSubmitRule());
|
entity.setSubmitRule(dto.getSubmitRule());
|
||||||
}
|
}
|
||||||
if (StringUtils.hasText(dto.getSubmitStartTime())) {
|
if (StringUtils.hasText(dto.getSubmitStartTime())) {
|
||||||
entity.setSubmitStartTime(LocalDateTime.parse(dto.getSubmitStartTime(), DT_FORMATTER));
|
entity.setSubmitStartTime(parseDateTime(dto.getSubmitStartTime()));
|
||||||
}
|
}
|
||||||
if (StringUtils.hasText(dto.getSubmitEndTime())) {
|
if (StringUtils.hasText(dto.getSubmitEndTime())) {
|
||||||
entity.setSubmitEndTime(LocalDateTime.parse(dto.getSubmitEndTime(), DT_FORMATTER));
|
entity.setSubmitEndTime(parseDateTime(dto.getSubmitEndTime()));
|
||||||
}
|
}
|
||||||
if (StringUtils.hasText(dto.getWorkType())) {
|
if (StringUtils.hasText(dto.getWorkType())) {
|
||||||
entity.setWorkType(dto.getWorkType());
|
entity.setWorkType(dto.getWorkType());
|
||||||
@ -404,13 +517,13 @@ public class ContestServiceImpl extends ServiceImpl<ContestMapper, BizContest> i
|
|||||||
entity.setReviewRuleId(dto.getReviewRuleId());
|
entity.setReviewRuleId(dto.getReviewRuleId());
|
||||||
}
|
}
|
||||||
if (StringUtils.hasText(dto.getReviewStartTime())) {
|
if (StringUtils.hasText(dto.getReviewStartTime())) {
|
||||||
entity.setReviewStartTime(LocalDateTime.parse(dto.getReviewStartTime(), DT_FORMATTER));
|
entity.setReviewStartTime(parseDateTime(dto.getReviewStartTime()));
|
||||||
}
|
}
|
||||||
if (StringUtils.hasText(dto.getReviewEndTime())) {
|
if (StringUtils.hasText(dto.getReviewEndTime())) {
|
||||||
entity.setReviewEndTime(LocalDateTime.parse(dto.getReviewEndTime(), DT_FORMATTER));
|
entity.setReviewEndTime(parseDateTime(dto.getReviewEndTime()));
|
||||||
}
|
}
|
||||||
if (StringUtils.hasText(dto.getResultPublishTime())) {
|
if (StringUtils.hasText(dto.getResultPublishTime())) {
|
||||||
entity.setResultPublishTime(LocalDateTime.parse(dto.getResultPublishTime(), DT_FORMATTER));
|
entity.setResultPublishTime(parseDateTime(dto.getResultPublishTime()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -18,12 +18,25 @@ import com.competition.modules.biz.contest.mapper.ContestRegistrationMapper;
|
|||||||
import com.competition.modules.biz.contest.mapper.ContestWorkAttachmentMapper;
|
import com.competition.modules.biz.contest.mapper.ContestWorkAttachmentMapper;
|
||||||
import com.competition.modules.biz.contest.mapper.ContestWorkMapper;
|
import com.competition.modules.biz.contest.mapper.ContestWorkMapper;
|
||||||
import com.competition.modules.biz.contest.service.IContestWorkService;
|
import com.competition.modules.biz.contest.service.IContestWorkService;
|
||||||
|
import com.competition.modules.biz.review.entity.BizContestJudge;
|
||||||
|
import com.competition.modules.biz.review.entity.BizContestReviewRule;
|
||||||
|
import com.competition.modules.biz.review.entity.BizContestWorkJudgeAssignment;
|
||||||
|
import com.competition.modules.biz.review.entity.BizContestWorkScore;
|
||||||
|
import com.competition.modules.biz.review.mapper.ContestJudgeMapper;
|
||||||
|
import com.competition.modules.biz.review.mapper.ContestReviewRuleMapper;
|
||||||
|
import com.competition.modules.biz.review.mapper.ContestWorkJudgeAssignmentMapper;
|
||||||
|
import com.competition.modules.biz.review.mapper.ContestWorkScoreMapper;
|
||||||
|
import com.competition.modules.biz.review.util.ContestFinalScoreCalculator;
|
||||||
|
import com.competition.modules.sys.entity.SysUser;
|
||||||
|
import com.competition.modules.sys.mapper.SysUserMapper;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
@ -39,6 +52,11 @@ public class ContestWorkServiceImpl extends ServiceImpl<ContestWorkMapper, BizCo
|
|||||||
private final ContestWorkAttachmentMapper contestWorkAttachmentMapper;
|
private final ContestWorkAttachmentMapper contestWorkAttachmentMapper;
|
||||||
private final ContestRegistrationMapper contestRegistrationMapper;
|
private final ContestRegistrationMapper contestRegistrationMapper;
|
||||||
private final ContestMapper contestMapper;
|
private final ContestMapper contestMapper;
|
||||||
|
private final ContestWorkJudgeAssignmentMapper assignmentMapper;
|
||||||
|
private final SysUserMapper sysUserMapper;
|
||||||
|
private final ContestWorkScoreMapper contestWorkScoreMapper;
|
||||||
|
private final ContestReviewRuleMapper contestReviewRuleMapper;
|
||||||
|
private final ContestJudgeMapper contestJudgeMapper;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@ -106,6 +124,7 @@ public class ContestWorkServiceImpl extends ServiceImpl<ContestWorkMapper, BizCo
|
|||||||
work.setStatus("submitted");
|
work.setStatus("submitted");
|
||||||
work.setSubmitTime(LocalDateTime.now());
|
work.setSubmitTime(LocalDateTime.now());
|
||||||
work.setSubmitterUserId(submitterId);
|
work.setSubmitterUserId(submitterId);
|
||||||
|
work.setSubmitterAccountNo(registration.getAccountNo());
|
||||||
work.setPreviewUrl(dto.getPreviewUrl());
|
work.setPreviewUrl(dto.getPreviewUrl());
|
||||||
work.setPreviewUrls(dto.getPreviewUrls());
|
work.setPreviewUrls(dto.getPreviewUrls());
|
||||||
work.setAiModelMeta(dto.getAiModelMeta());
|
work.setAiModelMeta(dto.getAiModelMeta());
|
||||||
@ -160,6 +179,57 @@ public class ContestWorkServiceImpl extends ServiceImpl<ContestWorkMapper, BizCo
|
|||||||
} else if (!isSuperTenant && tenantId != null) {
|
} else if (!isSuperTenant && tenantId != null) {
|
||||||
wrapper.eq(BizContestWork::getTenantId, tenantId);
|
wrapper.eq(BizContestWork::getTenantId, tenantId);
|
||||||
}
|
}
|
||||||
|
// username 筛选:对应报名表的 account_no
|
||||||
|
if (StringUtils.hasText(dto.getUsername())) {
|
||||||
|
LambdaQueryWrapper<BizContestRegistration> userRegWrapper = new LambdaQueryWrapper<>();
|
||||||
|
if (dto.getContestId() != null) {
|
||||||
|
userRegWrapper.eq(BizContestRegistration::getContestId, dto.getContestId());
|
||||||
|
}
|
||||||
|
if (dto.getTenantId() != null) {
|
||||||
|
userRegWrapper.eq(BizContestRegistration::getTenantId, dto.getTenantId());
|
||||||
|
} else if (!isSuperTenant && tenantId != null) {
|
||||||
|
userRegWrapper.eq(BizContestRegistration::getTenantId, tenantId);
|
||||||
|
}
|
||||||
|
userRegWrapper.eq(BizContestRegistration::getValidState, 1);
|
||||||
|
userRegWrapper.like(BizContestRegistration::getAccountNo, dto.getUsername());
|
||||||
|
List<BizContestRegistration> userRegs = contestRegistrationMapper.selectList(userRegWrapper);
|
||||||
|
Set<Long> userRegIds = userRegs.stream()
|
||||||
|
.map(BizContestRegistration::getId)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
if (!userRegIds.isEmpty()) {
|
||||||
|
wrapper.in(BizContestWork::getRegistrationId, userRegIds);
|
||||||
|
} else {
|
||||||
|
// 没有匹配的报名记录,返回空结果
|
||||||
|
wrapper.eq(BizContestWork::getId, -1L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// name 筛选:对应报名表的 account_name
|
||||||
|
if (StringUtils.hasText(dto.getName())) {
|
||||||
|
LambdaQueryWrapper<BizContestRegistration> nameRegWrapper = new LambdaQueryWrapper<>();
|
||||||
|
if (dto.getContestId() != null) {
|
||||||
|
nameRegWrapper.eq(BizContestRegistration::getContestId, dto.getContestId());
|
||||||
|
}
|
||||||
|
if (dto.getTenantId() != null) {
|
||||||
|
nameRegWrapper.eq(BizContestRegistration::getTenantId, dto.getTenantId());
|
||||||
|
} else if (!isSuperTenant && tenantId != null) {
|
||||||
|
nameRegWrapper.eq(BizContestRegistration::getTenantId, tenantId);
|
||||||
|
}
|
||||||
|
nameRegWrapper.eq(BizContestRegistration::getValidState, 1);
|
||||||
|
nameRegWrapper.like(BizContestRegistration::getAccountName, dto.getName());
|
||||||
|
List<BizContestRegistration> nameRegs = contestRegistrationMapper.selectList(nameRegWrapper);
|
||||||
|
Set<Long> nameRegIds = nameRegs.stream()
|
||||||
|
.map(BizContestRegistration::getId)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
if (!nameRegIds.isEmpty()) {
|
||||||
|
wrapper.in(BizContestWork::getRegistrationId, nameRegIds);
|
||||||
|
} else {
|
||||||
|
wrapper.eq(BizContestWork::getId, -1L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Set<Long> keywordRegistrationIds = Collections.emptySet();
|
Set<Long> keywordRegistrationIds = Collections.emptySet();
|
||||||
if (StringUtils.hasText(dto.getKeyword())) {
|
if (StringUtils.hasText(dto.getKeyword())) {
|
||||||
String keyword = dto.getKeyword();
|
String keyword = dto.getKeyword();
|
||||||
@ -200,12 +270,34 @@ public class ContestWorkServiceImpl extends ServiceImpl<ContestWorkMapper, BizCo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (StringUtils.hasText(dto.getSubmitStartTime())) {
|
if (StringUtils.hasText(dto.getSubmitStartTime())) {
|
||||||
wrapper.ge(BizContestWork::getSubmitTime,
|
wrapper.ge(BizContestWork::getSubmitTime, parseDateTime(dto.getSubmitStartTime(), true));
|
||||||
LocalDateTime.parse(dto.getSubmitStartTime(), DateTimeFormatter.ISO_LOCAL_DATE_TIME));
|
|
||||||
}
|
}
|
||||||
if (StringUtils.hasText(dto.getSubmitEndTime())) {
|
if (StringUtils.hasText(dto.getSubmitEndTime())) {
|
||||||
wrapper.le(BizContestWork::getSubmitTime,
|
wrapper.le(BizContestWork::getSubmitTime, parseDateTime(dto.getSubmitEndTime(), false));
|
||||||
LocalDateTime.parse(dto.getSubmitEndTime(), DateTimeFormatter.ISO_LOCAL_DATE_TIME));
|
}
|
||||||
|
|
||||||
|
// assignStatus 筛选:基于分配表判断已分配/未分配
|
||||||
|
if (StringUtils.hasText(dto.getAssignStatus()) && dto.getContestId() != null) {
|
||||||
|
LambdaQueryWrapper<BizContestWorkJudgeAssignment> assignQueryWrapper = new LambdaQueryWrapper<>();
|
||||||
|
assignQueryWrapper.eq(BizContestWorkJudgeAssignment::getContestId, dto.getContestId());
|
||||||
|
assignQueryWrapper.select(BizContestWorkJudgeAssignment::getWorkId);
|
||||||
|
assignQueryWrapper.groupBy(BizContestWorkJudgeAssignment::getWorkId);
|
||||||
|
List<BizContestWorkJudgeAssignment> assignedRecords = assignmentMapper.selectList(assignQueryWrapper);
|
||||||
|
Set<Long> assignedWorkIds = assignedRecords.stream()
|
||||||
|
.map(BizContestWorkJudgeAssignment::getWorkId)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
if ("assigned".equals(dto.getAssignStatus())) {
|
||||||
|
if (assignedWorkIds.isEmpty()) {
|
||||||
|
wrapper.eq(BizContestWork::getId, -1L);
|
||||||
|
} else {
|
||||||
|
wrapper.in(BizContestWork::getId, assignedWorkIds);
|
||||||
|
}
|
||||||
|
} else if ("unassigned".equals(dto.getAssignStatus())) {
|
||||||
|
if (!assignedWorkIds.isEmpty()) {
|
||||||
|
wrapper.notIn(BizContestWork::getId, assignedWorkIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 默认只查最新版本
|
// 默认只查最新版本
|
||||||
@ -241,8 +333,82 @@ public class ContestWorkServiceImpl extends ServiceImpl<ContestWorkMapper, BizCo
|
|||||||
.collect(Collectors.toMap(BizContest::getId, c -> c));
|
.collect(Collectors.toMap(BizContest::getId, c -> c));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 批量查询分配信息
|
||||||
|
Set<Long> workIds = result.getRecords().stream()
|
||||||
|
.map(BizContestWork::getId)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
Map<Long, List<BizContestWorkJudgeAssignment>> assignmentMap = new HashMap<>();
|
||||||
|
Map<Long, String> judgeNameMap = new HashMap<>();
|
||||||
|
if (!workIds.isEmpty()) {
|
||||||
|
LambdaQueryWrapper<BizContestWorkJudgeAssignment> assignWrapper = new LambdaQueryWrapper<>();
|
||||||
|
assignWrapper.in(BizContestWorkJudgeAssignment::getWorkId, workIds);
|
||||||
|
if (dto.getContestId() != null) {
|
||||||
|
assignWrapper.eq(BizContestWorkJudgeAssignment::getContestId, dto.getContestId());
|
||||||
|
}
|
||||||
|
List<BizContestWorkJudgeAssignment> allAssignments = assignmentMapper.selectList(assignWrapper);
|
||||||
|
assignmentMap = allAssignments.stream()
|
||||||
|
.collect(Collectors.groupingBy(BizContestWorkJudgeAssignment::getWorkId));
|
||||||
|
|
||||||
|
// 批量查询评委用户信息
|
||||||
|
Set<Long> judgeIds = allAssignments.stream()
|
||||||
|
.map(BizContestWorkJudgeAssignment::getJudgeId)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
if (!judgeIds.isEmpty()) {
|
||||||
|
List<SysUser> judgeUsers = sysUserMapper.selectBatchIds(judgeIds);
|
||||||
|
for (SysUser u : judgeUsers) {
|
||||||
|
judgeNameMap.put(u.getId(), u.getNickname());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Map<Long, BizContestRegistration> finalRegistrationMap = registrationMap;
|
Map<Long, BizContestRegistration> finalRegistrationMap = registrationMap;
|
||||||
Map<Long, BizContest> finalContestMap = contestMap;
|
Map<Long, BizContest> finalContestMap = contestMap;
|
||||||
|
Map<Long, List<BizContestWorkJudgeAssignment>> finalAssignmentMap = assignmentMap;
|
||||||
|
Map<Long, String> finalJudgeNameMap = judgeNameMap;
|
||||||
|
|
||||||
|
// 批量评分:列表「评委评分」与详情抽屉一致;作品表 final_score 未落库时按评审规则从评分表回算
|
||||||
|
Map<Long, List<BizContestWorkScore>> scoresByWorkId = new HashMap<>();
|
||||||
|
if (!workIds.isEmpty()) {
|
||||||
|
LambdaQueryWrapper<BizContestWorkScore> scoreWrapper = new LambdaQueryWrapper<>();
|
||||||
|
scoreWrapper.in(BizContestWorkScore::getWorkId, workIds);
|
||||||
|
scoreWrapper.eq(BizContestWorkScore::getValidState, 1);
|
||||||
|
List<BizContestWorkScore> allPageScores = contestWorkScoreMapper.selectList(scoreWrapper);
|
||||||
|
scoresByWorkId = allPageScores.stream()
|
||||||
|
.collect(Collectors.groupingBy(BizContestWorkScore::getWorkId));
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<Long, String> contestCalculationRuleCache = new HashMap<>();
|
||||||
|
Map<Long, Map<Long, BigDecimal>> contestWeightMapCache = new HashMap<>();
|
||||||
|
for (Long cid : contestIds) {
|
||||||
|
BizContest c = contestMap.get(cid);
|
||||||
|
if (c == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String calculationRule = "average";
|
||||||
|
if (c.getReviewRuleId() != null) {
|
||||||
|
BizContestReviewRule rule = contestReviewRuleMapper.selectById(c.getReviewRuleId());
|
||||||
|
if (rule != null && StringUtils.hasText(rule.getCalculationRule())) {
|
||||||
|
calculationRule = rule.getCalculationRule();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
contestCalculationRuleCache.put(cid, calculationRule);
|
||||||
|
|
||||||
|
LambdaQueryWrapper<BizContestJudge> judgeWrapper = new LambdaQueryWrapper<>();
|
||||||
|
judgeWrapper.eq(BizContestJudge::getContestId, cid);
|
||||||
|
judgeWrapper.eq(BizContestJudge::getValidState, 1);
|
||||||
|
List<BizContestJudge> judges = contestJudgeMapper.selectList(judgeWrapper);
|
||||||
|
Map<Long, BigDecimal> weightMap = new HashMap<>();
|
||||||
|
for (BizContestJudge j : judges) {
|
||||||
|
weightMap.put(j.getJudgeId(), j.getWeight() != null ? j.getWeight() : BigDecimal.ONE);
|
||||||
|
}
|
||||||
|
contestWeightMapCache.put(cid, weightMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<Long, List<BizContestWorkScore>> finalScoresByWorkId = scoresByWorkId;
|
||||||
|
Map<Long, String> finalContestCalculationRuleCache = contestCalculationRuleCache;
|
||||||
|
Map<Long, Map<Long, BigDecimal>> finalContestWeightMapCache = contestWeightMapCache;
|
||||||
|
|
||||||
List<Map<String, Object>> voList = result.getRecords().stream()
|
List<Map<String, Object>> voList = result.getRecords().stream()
|
||||||
.map(work -> {
|
.map(work -> {
|
||||||
Map<String, Object> map = workToMap(work);
|
Map<String, Object> map = workToMap(work);
|
||||||
@ -279,6 +445,49 @@ public class ContestWorkServiceImpl extends ServiceImpl<ContestWorkMapper, BizCo
|
|||||||
map.put("accountName", reg.getAccountName());
|
map.put("accountName", reg.getAccountName());
|
||||||
map.put("userId", reg.getUserId());
|
map.put("userId", reg.getUserId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 分配信息
|
||||||
|
List<BizContestWorkJudgeAssignment> workAssignments = finalAssignmentMap.getOrDefault(work.getId(), Collections.emptyList());
|
||||||
|
List<Map<String, Object>> assignmentVoList = workAssignments.stream().map(a -> {
|
||||||
|
Map<String, Object> assignVo = new LinkedHashMap<>();
|
||||||
|
assignVo.put("id", a.getId());
|
||||||
|
assignVo.put("judgeId", a.getJudgeId());
|
||||||
|
assignVo.put("status", a.getStatus());
|
||||||
|
assignVo.put("assignmentTime", a.getAssignmentTime());
|
||||||
|
Map<String, Object> judgeVo = new LinkedHashMap<>();
|
||||||
|
judgeVo.put("id", a.getJudgeId());
|
||||||
|
judgeVo.put("nickname", finalJudgeNameMap.getOrDefault(a.getJudgeId(), ""));
|
||||||
|
assignVo.put("judge", judgeVo);
|
||||||
|
return assignVo;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
map.put("assignments", assignmentVoList);
|
||||||
|
|
||||||
|
// _count 用于分配状态判断
|
||||||
|
Map<String, Object> countVo = new LinkedHashMap<>();
|
||||||
|
countVo.put("assignments", workAssignments.size());
|
||||||
|
map.put("_count", countVo);
|
||||||
|
|
||||||
|
int totalJudgesCount = workAssignments.size();
|
||||||
|
long reviewedCount = workAssignments.stream()
|
||||||
|
.filter(a -> "completed".equals(a.getStatus()))
|
||||||
|
.count();
|
||||||
|
map.put("totalJudgesCount", totalJudgesCount);
|
||||||
|
map.put("reviewedCount", reviewedCount);
|
||||||
|
|
||||||
|
BigDecimal displayFinal = work.getFinalScore();
|
||||||
|
if (displayFinal == null) {
|
||||||
|
List<BizContestWorkScore> scores = finalScoresByWorkId.getOrDefault(work.getId(), Collections.emptyList());
|
||||||
|
if (!scores.isEmpty()) {
|
||||||
|
String rule = finalContestCalculationRuleCache.getOrDefault(work.getContestId(), "average");
|
||||||
|
Map<Long, BigDecimal> wm = finalContestWeightMapCache.getOrDefault(work.getContestId(), Collections.emptyMap());
|
||||||
|
displayFinal = ContestFinalScoreCalculator.compute(scores, rule, wm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (displayFinal != null) {
|
||||||
|
map.put("finalScore", displayFinal);
|
||||||
|
map.put("averageScore", displayFinal);
|
||||||
|
}
|
||||||
|
|
||||||
return map;
|
return map;
|
||||||
})
|
})
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
@ -290,13 +499,16 @@ public class ContestWorkServiceImpl extends ServiceImpl<ContestWorkMapper, BizCo
|
|||||||
public Map<String, Object> getStats(Long contestId, Long tenantId, boolean isSuperTenant) {
|
public Map<String, Object> getStats(Long contestId, Long tenantId, boolean isSuperTenant) {
|
||||||
log.info("获取作品统计,赛事ID:{}", contestId);
|
log.info("获取作品统计,赛事ID:{}", contestId);
|
||||||
|
|
||||||
|
// 租户过滤
|
||||||
|
boolean needTenantFilter = !isSuperTenant && tenantId != null;
|
||||||
|
|
||||||
LambdaQueryWrapper<BizContestWork> baseWrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<BizContestWork> baseWrapper = new LambdaQueryWrapper<>();
|
||||||
if (contestId != null) {
|
if (contestId != null) {
|
||||||
baseWrapper.eq(BizContestWork::getContestId, contestId);
|
baseWrapper.eq(BizContestWork::getContestId, contestId);
|
||||||
}
|
}
|
||||||
baseWrapper.eq(BizContestWork::getIsLatest, true);
|
baseWrapper.eq(BizContestWork::getIsLatest, true);
|
||||||
baseWrapper.eq(BizContestWork::getValidState, 1);
|
baseWrapper.eq(BizContestWork::getValidState, 1);
|
||||||
if (!isSuperTenant && tenantId != null) {
|
if (needTenantFilter) {
|
||||||
baseWrapper.eq(BizContestWork::getTenantId, tenantId);
|
baseWrapper.eq(BizContestWork::getTenantId, tenantId);
|
||||||
}
|
}
|
||||||
long total = count(baseWrapper);
|
long total = count(baseWrapper);
|
||||||
@ -307,7 +519,7 @@ public class ContestWorkServiceImpl extends ServiceImpl<ContestWorkMapper, BizCo
|
|||||||
}
|
}
|
||||||
submittedWrapper.eq(BizContestWork::getIsLatest, true);
|
submittedWrapper.eq(BizContestWork::getIsLatest, true);
|
||||||
submittedWrapper.eq(BizContestWork::getValidState, 1);
|
submittedWrapper.eq(BizContestWork::getValidState, 1);
|
||||||
if (!isSuperTenant && tenantId != null) {
|
if (needTenantFilter) {
|
||||||
submittedWrapper.eq(BizContestWork::getTenantId, tenantId);
|
submittedWrapper.eq(BizContestWork::getTenantId, tenantId);
|
||||||
}
|
}
|
||||||
submittedWrapper.eq(BizContestWork::getStatus, "submitted");
|
submittedWrapper.eq(BizContestWork::getStatus, "submitted");
|
||||||
@ -319,7 +531,7 @@ public class ContestWorkServiceImpl extends ServiceImpl<ContestWorkMapper, BizCo
|
|||||||
}
|
}
|
||||||
reviewingWrapper.eq(BizContestWork::getIsLatest, true);
|
reviewingWrapper.eq(BizContestWork::getIsLatest, true);
|
||||||
reviewingWrapper.eq(BizContestWork::getValidState, 1);
|
reviewingWrapper.eq(BizContestWork::getValidState, 1);
|
||||||
if (!isSuperTenant && tenantId != null) {
|
if (needTenantFilter) {
|
||||||
reviewingWrapper.eq(BizContestWork::getTenantId, tenantId);
|
reviewingWrapper.eq(BizContestWork::getTenantId, tenantId);
|
||||||
}
|
}
|
||||||
reviewingWrapper.eq(BizContestWork::getStatus, "reviewing");
|
reviewingWrapper.eq(BizContestWork::getStatus, "reviewing");
|
||||||
@ -331,10 +543,9 @@ public class ContestWorkServiceImpl extends ServiceImpl<ContestWorkMapper, BizCo
|
|||||||
}
|
}
|
||||||
reviewedWrapper.eq(BizContestWork::getIsLatest, true);
|
reviewedWrapper.eq(BizContestWork::getIsLatest, true);
|
||||||
reviewedWrapper.eq(BizContestWork::getValidState, 1);
|
reviewedWrapper.eq(BizContestWork::getValidState, 1);
|
||||||
if (!isSuperTenant && tenantId != null) {
|
if (needTenantFilter) {
|
||||||
reviewedWrapper.eq(BizContestWork::getTenantId, tenantId);
|
reviewedWrapper.eq(BizContestWork::getTenantId, tenantId);
|
||||||
}
|
}
|
||||||
// 已评完口径:兼容 accepted/awarded 两种结果状态
|
|
||||||
reviewedWrapper.in(BizContestWork::getStatus, Arrays.asList("accepted", "awarded"));
|
reviewedWrapper.in(BizContestWork::getStatus, Arrays.asList("accepted", "awarded"));
|
||||||
long reviewed = count(reviewedWrapper);
|
long reviewed = count(reviewedWrapper);
|
||||||
|
|
||||||
@ -433,6 +644,18 @@ public class ContestWorkServiceImpl extends ServiceImpl<ContestWorkMapper, BizCo
|
|||||||
|
|
||||||
// ====== 私有辅助方法 ======
|
// ====== 私有辅助方法 ======
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析时间参数,支持 "yyyy-MM-dd" 和 "yyyy-MM-ddTHH:mm:ss" 两种格式。
|
||||||
|
* isStart=true 时纯日期补 00:00:00,isStart=false 时纯日期补 23:59:59。
|
||||||
|
*/
|
||||||
|
private LocalDateTime parseDateTime(String value, boolean isStart) {
|
||||||
|
if (value.contains("T")) {
|
||||||
|
return LocalDateTime.parse(value, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
|
||||||
|
}
|
||||||
|
LocalDate date = LocalDate.parse(value, DateTimeFormatter.ISO_LOCAL_DATE);
|
||||||
|
return isStart ? date.atStartOfDay() : date.atTime(23, 59, 59);
|
||||||
|
}
|
||||||
|
|
||||||
private String generateWorkNo(Long contestId) {
|
private String generateWorkNo(Long contestId) {
|
||||||
LambdaQueryWrapper<BizContestWork> wrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<BizContestWork> wrapper = new LambdaQueryWrapper<>();
|
||||||
wrapper.eq(BizContestWork::getContestId, contestId);
|
wrapper.eq(BizContestWork::getContestId, contestId);
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||||
import com.competition.common.entity.BaseEntity;
|
import com.competition.common.entity.BaseEntity;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
@ -12,33 +13,43 @@ import java.time.LocalDateTime;
|
|||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
@TableName(value = "t_biz_homework", autoResultMap = true)
|
@TableName(value = "t_biz_homework", autoResultMap = true)
|
||||||
|
@Schema(description = "作业实体")
|
||||||
public class BizHomework extends BaseEntity {
|
public class BizHomework extends BaseEntity {
|
||||||
|
|
||||||
|
@Schema(description = "租户ID")
|
||||||
@TableField("tenant_id")
|
@TableField("tenant_id")
|
||||||
private Long tenantId;
|
private Long tenantId;
|
||||||
|
|
||||||
|
@Schema(description = "作业名称")
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
|
@Schema(description = "作业内容")
|
||||||
private String content;
|
private String content;
|
||||||
|
|
||||||
|
@Schema(description = "提交开始时间")
|
||||||
@TableField("submit_start_time")
|
@TableField("submit_start_time")
|
||||||
private LocalDateTime submitStartTime;
|
private LocalDateTime submitStartTime;
|
||||||
|
|
||||||
|
@Schema(description = "提交结束时间")
|
||||||
@TableField("submit_end_time")
|
@TableField("submit_end_time")
|
||||||
private LocalDateTime submitEndTime;
|
private LocalDateTime submitEndTime;
|
||||||
|
|
||||||
|
@Schema(description = "附件(JSON)")
|
||||||
@TableField(value = "attachments", typeHandler = JacksonTypeHandler.class)
|
@TableField(value = "attachments", typeHandler = JacksonTypeHandler.class)
|
||||||
private Object attachments;
|
private Object attachments;
|
||||||
|
|
||||||
|
@Schema(description = "发布范围(JSON)")
|
||||||
@TableField(value = "publish_scope", typeHandler = JacksonTypeHandler.class)
|
@TableField(value = "publish_scope", typeHandler = JacksonTypeHandler.class)
|
||||||
private Object publishScope;
|
private Object publishScope;
|
||||||
|
|
||||||
|
@Schema(description = "评审规则ID")
|
||||||
@TableField("review_rule_id")
|
@TableField("review_rule_id")
|
||||||
private Long reviewRuleId;
|
private Long reviewRuleId;
|
||||||
|
|
||||||
/** unpublished / published */
|
@Schema(description = "状态:unpublished/published")
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
|
@Schema(description = "发布时间")
|
||||||
@TableField("publish_time")
|
@TableField("publish_time")
|
||||||
private LocalDateTime publishTime;
|
private LocalDateTime publishTime;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,22 +4,27 @@ import com.baomidou.mybatisplus.annotation.TableField;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||||
import com.competition.common.entity.BaseEntity;
|
import com.competition.common.entity.BaseEntity;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
@TableName(value = "t_biz_homework_review_rule", autoResultMap = true)
|
@TableName(value = "t_biz_homework_review_rule", autoResultMap = true)
|
||||||
|
@Schema(description = "作业评审规则实体")
|
||||||
public class BizHomeworkReviewRule extends BaseEntity {
|
public class BizHomeworkReviewRule extends BaseEntity {
|
||||||
|
|
||||||
|
@Schema(description = "租户ID")
|
||||||
@TableField("tenant_id")
|
@TableField("tenant_id")
|
||||||
private Long tenantId;
|
private Long tenantId;
|
||||||
|
|
||||||
|
@Schema(description = "规则名称")
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
|
@Schema(description = "规则描述")
|
||||||
private String description;
|
private String description;
|
||||||
|
|
||||||
/** JSON array of {name, maxScore, description} */
|
@Schema(description = "评分标准(JSON数组)")
|
||||||
@TableField(value = "criteria", typeHandler = JacksonTypeHandler.class)
|
@TableField(value = "criteria", typeHandler = JacksonTypeHandler.class)
|
||||||
private Object criteria;
|
private Object criteria;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||||
import com.competition.common.entity.BaseEntity;
|
import com.competition.common.entity.BaseEntity;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
@ -13,25 +14,33 @@ import java.time.LocalDateTime;
|
|||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
@TableName(value = "t_biz_homework_score", autoResultMap = true)
|
@TableName(value = "t_biz_homework_score", autoResultMap = true)
|
||||||
|
@Schema(description = "作业评分实体")
|
||||||
public class BizHomeworkScore extends BaseEntity {
|
public class BizHomeworkScore extends BaseEntity {
|
||||||
|
|
||||||
|
@Schema(description = "租户ID")
|
||||||
@TableField("tenant_id")
|
@TableField("tenant_id")
|
||||||
private Long tenantId;
|
private Long tenantId;
|
||||||
|
|
||||||
|
@Schema(description = "提交ID")
|
||||||
@TableField("submission_id")
|
@TableField("submission_id")
|
||||||
private Long submissionId;
|
private Long submissionId;
|
||||||
|
|
||||||
|
@Schema(description = "评审人ID")
|
||||||
@TableField("reviewer_id")
|
@TableField("reviewer_id")
|
||||||
private Long reviewerId;
|
private Long reviewerId;
|
||||||
|
|
||||||
|
@Schema(description = "各维度得分(JSON)")
|
||||||
@TableField(value = "dimension_scores", typeHandler = JacksonTypeHandler.class)
|
@TableField(value = "dimension_scores", typeHandler = JacksonTypeHandler.class)
|
||||||
private Object dimensionScores;
|
private Object dimensionScores;
|
||||||
|
|
||||||
|
@Schema(description = "总分")
|
||||||
@TableField("total_score")
|
@TableField("total_score")
|
||||||
private BigDecimal totalScore;
|
private BigDecimal totalScore;
|
||||||
|
|
||||||
|
@Schema(description = "评语")
|
||||||
private String comments;
|
private String comments;
|
||||||
|
|
||||||
|
@Schema(description = "评分时间")
|
||||||
@TableField("score_time")
|
@TableField("score_time")
|
||||||
private LocalDateTime scoreTime;
|
private LocalDateTime scoreTime;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||||
import com.competition.common.entity.BaseEntity;
|
import com.competition.common.entity.BaseEntity;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
@ -13,38 +14,49 @@ import java.time.LocalDateTime;
|
|||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
@TableName(value = "t_biz_homework_submission", autoResultMap = true)
|
@TableName(value = "t_biz_homework_submission", autoResultMap = true)
|
||||||
|
@Schema(description = "作业提交实体")
|
||||||
public class BizHomeworkSubmission extends BaseEntity {
|
public class BizHomeworkSubmission extends BaseEntity {
|
||||||
|
|
||||||
|
@Schema(description = "租户ID")
|
||||||
@TableField("tenant_id")
|
@TableField("tenant_id")
|
||||||
private Long tenantId;
|
private Long tenantId;
|
||||||
|
|
||||||
|
@Schema(description = "作业ID")
|
||||||
@TableField("homework_id")
|
@TableField("homework_id")
|
||||||
private Long homeworkId;
|
private Long homeworkId;
|
||||||
|
|
||||||
|
@Schema(description = "学生ID")
|
||||||
@TableField("student_id")
|
@TableField("student_id")
|
||||||
private Long studentId;
|
private Long studentId;
|
||||||
|
|
||||||
|
@Schema(description = "作品编号")
|
||||||
@TableField("work_no")
|
@TableField("work_no")
|
||||||
private String workNo;
|
private String workNo;
|
||||||
|
|
||||||
|
@Schema(description = "作品名称")
|
||||||
@TableField("work_name")
|
@TableField("work_name")
|
||||||
private String workName;
|
private String workName;
|
||||||
|
|
||||||
|
@Schema(description = "作品描述")
|
||||||
@TableField("work_description")
|
@TableField("work_description")
|
||||||
private String workDescription;
|
private String workDescription;
|
||||||
|
|
||||||
|
@Schema(description = "作品文件(JSON)")
|
||||||
@TableField(value = "files", typeHandler = JacksonTypeHandler.class)
|
@TableField(value = "files", typeHandler = JacksonTypeHandler.class)
|
||||||
private Object files;
|
private Object files;
|
||||||
|
|
||||||
|
@Schema(description = "附件(JSON)")
|
||||||
@TableField(value = "attachments", typeHandler = JacksonTypeHandler.class)
|
@TableField(value = "attachments", typeHandler = JacksonTypeHandler.class)
|
||||||
private Object attachments;
|
private Object attachments;
|
||||||
|
|
||||||
|
@Schema(description = "提交时间")
|
||||||
@TableField("submit_time")
|
@TableField("submit_time")
|
||||||
private LocalDateTime submitTime;
|
private LocalDateTime submitTime;
|
||||||
|
|
||||||
/** pending / reviewed */
|
@Schema(description = "状态:pending/reviewed")
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
|
@Schema(description = "总分")
|
||||||
@TableField("total_score")
|
@TableField("total_score")
|
||||||
private BigDecimal totalScore;
|
private BigDecimal totalScore;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|||||||
import com.competition.common.enums.ErrorCode;
|
import com.competition.common.enums.ErrorCode;
|
||||||
import com.competition.common.exception.BusinessException;
|
import com.competition.common.exception.BusinessException;
|
||||||
import com.competition.common.result.PageResult;
|
import com.competition.common.result.PageResult;
|
||||||
|
import com.competition.common.util.SecurityUtil;
|
||||||
import com.competition.modules.biz.judge.service.IJudgesManagementService;
|
import com.competition.modules.biz.judge.service.IJudgesManagementService;
|
||||||
import com.competition.modules.sys.entity.SysRole;
|
import com.competition.modules.sys.entity.SysRole;
|
||||||
import com.competition.modules.sys.entity.SysTenant;
|
import com.competition.modules.sys.entity.SysTenant;
|
||||||
@ -12,6 +13,7 @@ import com.competition.modules.sys.entity.SysUser;
|
|||||||
import com.competition.modules.sys.entity.SysUserRole;
|
import com.competition.modules.sys.entity.SysUserRole;
|
||||||
import com.competition.modules.sys.mapper.SysRoleMapper;
|
import com.competition.modules.sys.mapper.SysRoleMapper;
|
||||||
import com.competition.modules.sys.mapper.SysTenantMapper;
|
import com.competition.modules.sys.mapper.SysTenantMapper;
|
||||||
|
import com.competition.modules.sys.config.JudgeRolePermissionConfigurer;
|
||||||
import com.competition.modules.sys.mapper.SysUserMapper;
|
import com.competition.modules.sys.mapper.SysUserMapper;
|
||||||
import com.competition.modules.sys.mapper.SysUserRoleMapper;
|
import com.competition.modules.sys.mapper.SysUserRoleMapper;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@ -33,6 +35,7 @@ public class JudgesManagementServiceImpl implements IJudgesManagementService {
|
|||||||
private final SysRoleMapper sysRoleMapper;
|
private final SysRoleMapper sysRoleMapper;
|
||||||
private final SysTenantMapper sysTenantMapper;
|
private final SysTenantMapper sysTenantMapper;
|
||||||
private final PasswordEncoder passwordEncoder;
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
private final JudgeRolePermissionConfigurer judgeRolePermissionConfigurer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取评委专属租户 ID
|
* 获取评委专属租户 ID
|
||||||
@ -48,23 +51,58 @@ public class JudgesManagementServiceImpl implements IJudgesManagementService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取评委角色 ID
|
* 获取或自动创建评委角色 ID
|
||||||
*/
|
*/
|
||||||
private Long getJudgeRoleId(Long tenantId) {
|
private Long getOrCreateJudgeRoleId(Long tenantId) {
|
||||||
LambdaQueryWrapper<SysRole> wrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<SysRole> wrapper = new LambdaQueryWrapper<>();
|
||||||
wrapper.eq(SysRole::getCode, "judge");
|
wrapper.eq(SysRole::getCode, "judge");
|
||||||
wrapper.eq(SysRole::getTenantId, tenantId);
|
wrapper.eq(SysRole::getTenantId, tenantId);
|
||||||
SysRole role = sysRoleMapper.selectOne(wrapper);
|
SysRole role = sysRoleMapper.selectOne(wrapper);
|
||||||
if (role == null) {
|
if (role != null) {
|
||||||
throw BusinessException.of(ErrorCode.BAD_REQUEST, "评委角色不存在,请先在评委租户下创建 code='judge' 的角色");
|
judgeRolePermissionConfigurer.ensureJudgeRolePermissions(tenantId, role.getId());
|
||||||
|
return role.getId();
|
||||||
}
|
}
|
||||||
|
// 自动创建 judge 角色
|
||||||
|
role = new SysRole();
|
||||||
|
role.setTenantId(tenantId);
|
||||||
|
role.setCode("judge");
|
||||||
|
role.setName("评委");
|
||||||
|
role.setDescription("评委角色");
|
||||||
|
sysRoleMapper.insert(role);
|
||||||
|
log.info("自动创建评委角色,租户ID:{},角色ID:{}", tenantId, role.getId());
|
||||||
|
judgeRolePermissionConfigurer.ensureJudgeRolePermissions(tenantId, role.getId());
|
||||||
return role.getId();
|
return role.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证评委是否可被当前租户操作(查看/编辑/删除)
|
||||||
|
* 返回 true 表示是平台评委(只读),false 表示是本租户评委
|
||||||
|
*/
|
||||||
|
private boolean checkJudgeOwnership(SysUser user) {
|
||||||
|
Long currentTenantId = SecurityUtil.getCurrentTenantId();
|
||||||
|
Long judgeTenantId = getJudgeTenantId();
|
||||||
|
|
||||||
|
// 不属于当前租户也不属于平台评委租户
|
||||||
|
if (!currentTenantId.equals(user.getTenantId()) && !judgeTenantId.equals(user.getTenantId())) {
|
||||||
|
throw BusinessException.of(ErrorCode.NOT_FOUND, "评委不存在");
|
||||||
|
}
|
||||||
|
return judgeTenantId.equals(user.getTenantId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证评委可被修改操作(非平台评委 + 属于当前租户)
|
||||||
|
*/
|
||||||
|
private void checkJudgeWritable(SysUser user) {
|
||||||
|
if (checkJudgeOwnership(user)) {
|
||||||
|
throw BusinessException.of(ErrorCode.FORBIDDEN, "平台评委不允许修改");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将 SysUser 转为前端需要的 Map
|
* 将 SysUser 转为前端需要的 Map
|
||||||
*/
|
*/
|
||||||
private Map<String, Object> toMap(SysUser user) {
|
private Map<String, Object> toMap(SysUser user) {
|
||||||
|
Long judgeTenantId = getJudgeTenantId();
|
||||||
Map<String, Object> map = new LinkedHashMap<>();
|
Map<String, Object> map = new LinkedHashMap<>();
|
||||||
map.put("id", user.getId());
|
map.put("id", user.getId());
|
||||||
map.put("username", user.getUsername());
|
map.put("username", user.getUsername());
|
||||||
@ -77,6 +115,8 @@ public class JudgesManagementServiceImpl implements IJudgesManagementService {
|
|||||||
map.put("userSource", user.getUserSource());
|
map.put("userSource", user.getUserSource());
|
||||||
map.put("createTime", user.getCreateTime());
|
map.put("createTime", user.getCreateTime());
|
||||||
map.put("modifyTime", user.getModifyTime());
|
map.put("modifyTime", user.getModifyTime());
|
||||||
|
map.put("tenantId", user.getTenantId());
|
||||||
|
map.put("isPlatform", judgeTenantId.equals(user.getTenantId()));
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,19 +137,19 @@ public class JudgesManagementServiceImpl implements IJudgesManagementService {
|
|||||||
throw BusinessException.of(ErrorCode.BAD_REQUEST, "密码不能为空");
|
throw BusinessException.of(ErrorCode.BAD_REQUEST, "密码不能为空");
|
||||||
}
|
}
|
||||||
|
|
||||||
Long judgeTenantId = getJudgeTenantId();
|
Long currentTenantId = SecurityUtil.getCurrentTenantId();
|
||||||
|
|
||||||
// 检查用户名在评委租户内唯一
|
// 检查用户名在当前租户内唯一
|
||||||
LambdaQueryWrapper<SysUser> dupWrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<SysUser> dupWrapper = new LambdaQueryWrapper<>();
|
||||||
dupWrapper.eq(SysUser::getTenantId, judgeTenantId);
|
dupWrapper.eq(SysUser::getTenantId, currentTenantId);
|
||||||
dupWrapper.eq(SysUser::getUsername, username);
|
dupWrapper.eq(SysUser::getUsername, username);
|
||||||
if (sysUserMapper.selectCount(dupWrapper) > 0) {
|
if (sysUserMapper.selectCount(dupWrapper) > 0) {
|
||||||
throw BusinessException.of(ErrorCode.BAD_REQUEST, "该用户名已存在");
|
throw BusinessException.of(ErrorCode.BAD_REQUEST, "该用户名已存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建用户
|
// 创建用户(归属当前租户)
|
||||||
SysUser user = new SysUser();
|
SysUser user = new SysUser();
|
||||||
user.setTenantId(judgeTenantId);
|
user.setTenantId(currentTenantId);
|
||||||
user.setUsername(username);
|
user.setUsername(username);
|
||||||
user.setPassword(passwordEncoder.encode(password));
|
user.setPassword(passwordEncoder.encode(password));
|
||||||
user.setNickname(nickname);
|
user.setNickname(nickname);
|
||||||
@ -121,8 +161,8 @@ public class JudgesManagementServiceImpl implements IJudgesManagementService {
|
|||||||
user.setStatus("enabled");
|
user.setStatus("enabled");
|
||||||
sysUserMapper.insert(user);
|
sysUserMapper.insert(user);
|
||||||
|
|
||||||
// 分配评委角色
|
// 分配评委角色(在当前租户下查找或自动创建 judge 角色)
|
||||||
Long judgeRoleId = getJudgeRoleId(judgeTenantId);
|
Long judgeRoleId = getOrCreateJudgeRoleId(currentTenantId);
|
||||||
SysUserRole userRole = new SysUserRole();
|
SysUserRole userRole = new SysUserRole();
|
||||||
userRole.setUserId(user.getId());
|
userRole.setUserId(user.getId());
|
||||||
userRole.setRoleId(judgeRoleId);
|
userRole.setRoleId(judgeRoleId);
|
||||||
@ -134,10 +174,41 @@ public class JudgesManagementServiceImpl implements IJudgesManagementService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PageResult<Map<String, Object>> findAll(Long page, Long pageSize, String keyword, String status) {
|
public PageResult<Map<String, Object>> findAll(Long page, Long pageSize, String keyword, String status) {
|
||||||
|
Long currentTenantId = SecurityUtil.getCurrentTenantId();
|
||||||
Long judgeTenantId = getJudgeTenantId();
|
Long judgeTenantId = getJudgeTenantId();
|
||||||
|
|
||||||
|
// 查询当前租户和平台评委租户的 judge 角色 ID
|
||||||
|
Set<Long> judgeRoleIds = new HashSet<>();
|
||||||
|
for (Long tid : List.of(currentTenantId, judgeTenantId)) {
|
||||||
|
if (tid == null) continue;
|
||||||
|
LambdaQueryWrapper<SysRole> roleWrapper = new LambdaQueryWrapper<>();
|
||||||
|
roleWrapper.eq(SysRole::getCode, "judge");
|
||||||
|
roleWrapper.eq(SysRole::getTenantId, tid);
|
||||||
|
SysRole role = sysRoleMapper.selectOne(roleWrapper);
|
||||||
|
if (role != null) {
|
||||||
|
judgeRoleIds.add(role.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询拥有 judge 角色的用户 ID
|
||||||
|
Set<Long> judgeUserIds = new HashSet<>();
|
||||||
|
if (!judgeRoleIds.isEmpty()) {
|
||||||
|
LambdaQueryWrapper<SysUserRole> urWrapper = new LambdaQueryWrapper<>();
|
||||||
|
urWrapper.in(SysUserRole::getRoleId, judgeRoleIds);
|
||||||
|
List<SysUserRole> userRoles = sysUserRoleMapper.selectList(urWrapper);
|
||||||
|
for (SysUserRole ur : userRoles) {
|
||||||
|
judgeUserIds.add(ur.getUserId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (judgeUserIds.isEmpty()) {
|
||||||
|
return new PageResult<>(Collections.emptyList(), 0L, page, pageSize);
|
||||||
|
}
|
||||||
|
|
||||||
LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
|
||||||
wrapper.eq(SysUser::getTenantId, judgeTenantId);
|
// 只查询拥有 judge 角色且属于当前租户或平台评委租户的用户
|
||||||
|
wrapper.in(SysUser::getId, judgeUserIds);
|
||||||
|
wrapper.in(SysUser::getTenantId, List.of(currentTenantId, judgeTenantId));
|
||||||
|
|
||||||
if (keyword != null && !keyword.isBlank()) {
|
if (keyword != null && !keyword.isBlank()) {
|
||||||
wrapper.and(w -> w
|
wrapper.and(w -> w
|
||||||
@ -168,12 +239,7 @@ public class JudgesManagementServiceImpl implements IJudgesManagementService {
|
|||||||
if (user == null) {
|
if (user == null) {
|
||||||
throw BusinessException.of(ErrorCode.NOT_FOUND, "评委不存在");
|
throw BusinessException.of(ErrorCode.NOT_FOUND, "评委不存在");
|
||||||
}
|
}
|
||||||
|
checkJudgeOwnership(user);
|
||||||
Long judgeTenantId = getJudgeTenantId();
|
|
||||||
if (!judgeTenantId.equals(user.getTenantId())) {
|
|
||||||
throw BusinessException.of(ErrorCode.NOT_FOUND, "评委不存在");
|
|
||||||
}
|
|
||||||
|
|
||||||
return toMap(user);
|
return toMap(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,11 +250,7 @@ public class JudgesManagementServiceImpl implements IJudgesManagementService {
|
|||||||
if (user == null) {
|
if (user == null) {
|
||||||
throw BusinessException.of(ErrorCode.NOT_FOUND, "评委不存在");
|
throw BusinessException.of(ErrorCode.NOT_FOUND, "评委不存在");
|
||||||
}
|
}
|
||||||
|
checkJudgeWritable(user);
|
||||||
Long judgeTenantId = getJudgeTenantId();
|
|
||||||
if (!judgeTenantId.equals(user.getTenantId())) {
|
|
||||||
throw BusinessException.of(ErrorCode.NOT_FOUND, "评委不存在");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.containsKey("nickname")) {
|
if (params.containsKey("nickname")) {
|
||||||
user.setNickname((String) params.get("nickname"));
|
user.setNickname((String) params.get("nickname"));
|
||||||
@ -231,11 +293,7 @@ public class JudgesManagementServiceImpl implements IJudgesManagementService {
|
|||||||
if (user == null) {
|
if (user == null) {
|
||||||
throw BusinessException.of(ErrorCode.NOT_FOUND, "评委不存在");
|
throw BusinessException.of(ErrorCode.NOT_FOUND, "评委不存在");
|
||||||
}
|
}
|
||||||
|
checkJudgeWritable(user);
|
||||||
Long judgeTenantId = getJudgeTenantId();
|
|
||||||
if (!judgeTenantId.equals(user.getTenantId())) {
|
|
||||||
throw BusinessException.of(ErrorCode.NOT_FOUND, "评委不存在");
|
|
||||||
}
|
|
||||||
|
|
||||||
user.setStatus(status);
|
user.setStatus(status);
|
||||||
sysUserMapper.updateById(user);
|
sysUserMapper.updateById(user);
|
||||||
@ -247,11 +305,7 @@ public class JudgesManagementServiceImpl implements IJudgesManagementService {
|
|||||||
if (user == null) {
|
if (user == null) {
|
||||||
throw BusinessException.of(ErrorCode.NOT_FOUND, "评委不存在");
|
throw BusinessException.of(ErrorCode.NOT_FOUND, "评委不存在");
|
||||||
}
|
}
|
||||||
|
checkJudgeWritable(user);
|
||||||
Long judgeTenantId = getJudgeTenantId();
|
|
||||||
if (!judgeTenantId.equals(user.getTenantId())) {
|
|
||||||
throw BusinessException.of(ErrorCode.NOT_FOUND, "评委不存在");
|
|
||||||
}
|
|
||||||
|
|
||||||
sysUserMapper.deleteById(id);
|
sysUserMapper.deleteById(id);
|
||||||
log.info("评委已删除,ID:{}", id);
|
log.info("评委已删除,ID:{}", id);
|
||||||
@ -264,15 +318,15 @@ public class JudgesManagementServiceImpl implements IJudgesManagementService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Long judgeTenantId = getJudgeTenantId();
|
Long currentTenantId = SecurityUtil.getCurrentTenantId();
|
||||||
|
|
||||||
// 校验所有 ID 都属于评委租户
|
// 校验所有 ID 都属于当前租户(不允许删除平台评委)
|
||||||
LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
|
||||||
wrapper.in(SysUser::getId, ids);
|
wrapper.in(SysUser::getId, ids);
|
||||||
wrapper.eq(SysUser::getTenantId, judgeTenantId);
|
wrapper.eq(SysUser::getTenantId, currentTenantId);
|
||||||
Long count = sysUserMapper.selectCount(wrapper);
|
Long count = sysUserMapper.selectCount(wrapper);
|
||||||
if (count != ids.size()) {
|
if (count != ids.size()) {
|
||||||
throw BusinessException.of(ErrorCode.BAD_REQUEST, "部分评委不存在或不属于评委库");
|
throw BusinessException.of(ErrorCode.BAD_REQUEST, "部分评委不存在或不属于当前机构");
|
||||||
}
|
}
|
||||||
|
|
||||||
sysUserMapper.deleteBatchIds(ids);
|
sysUserMapper.deleteBatchIds(ids);
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package com.competition.modules.biz.review.controller;
|
package com.competition.modules.biz.review.controller;
|
||||||
|
|
||||||
import com.competition.common.result.Result;
|
import com.competition.common.result.Result;
|
||||||
|
import com.competition.modules.biz.review.dto.ContestJudgesForContestVo;
|
||||||
import com.competition.modules.biz.review.entity.BizContestJudge;
|
import com.competition.modules.biz.review.entity.BizContestJudge;
|
||||||
import com.competition.modules.biz.review.service.IContestJudgeService;
|
import com.competition.modules.biz.review.service.IContestJudgeService;
|
||||||
import com.competition.security.annotation.RequirePermission;
|
import com.competition.security.annotation.RequirePermission;
|
||||||
@ -10,7 +11,6 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@Tag(name = "赛事评委")
|
@Tag(name = "赛事评委")
|
||||||
@ -35,8 +35,9 @@ public class ContestJudgeController {
|
|||||||
|
|
||||||
@GetMapping("/contest/{contestId}")
|
@GetMapping("/contest/{contestId}")
|
||||||
@RequirePermission("contest:read")
|
@RequirePermission("contest:read")
|
||||||
@Operation(summary = "查询赛事评委列表")
|
@Operation(summary = "查询赛事评委列表",
|
||||||
public Result<List<Map<String, Object>>> findByContest(@PathVariable Long contestId) {
|
description = "返回 assigned(显式关联)与 implicitPool(平台隐式池)。添加评委抽屉仅用 assigned 回显;作品分配可选池为 assigned ∪ implicitPool(前端合并)。")
|
||||||
|
public Result<ContestJudgesForContestVo> findByContest(@PathVariable Long contestId) {
|
||||||
return Result.success(contestJudgeService.findByContest(contestId));
|
return Result.success(contestJudgeService.findByContest(contestId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -129,4 +129,12 @@ public class ContestReviewController {
|
|||||||
public Result<Map<String, Object>> calculateFinalScore(@PathVariable Long workId) {
|
public Result<Map<String, Object>> calculateFinalScore(@PathVariable Long workId) {
|
||||||
return Result.success(contestReviewService.calculateFinalScore(workId));
|
return Result.success(contestReviewService.calculateFinalScore(workId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/judge/contests/{contestId}/detail")
|
||||||
|
@RequirePermission("review:read")
|
||||||
|
@Operation(summary = "获取评委视角的赛事详情(含评审规则)")
|
||||||
|
public Result<Map<String, Object>> getJudgeContestDetail(@PathVariable Long contestId) {
|
||||||
|
Long judgeId = SecurityUtil.getCurrentUserId();
|
||||||
|
return Result.success(contestReviewService.getJudgeContestDetail(judgeId, contestId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package com.competition.modules.biz.review.controller;
|
|||||||
import com.competition.common.result.Result;
|
import com.competition.common.result.Result;
|
||||||
import com.competition.common.util.SecurityUtil;
|
import com.competition.common.util.SecurityUtil;
|
||||||
import com.competition.modules.biz.review.dto.CreatePresetCommentDto;
|
import com.competition.modules.biz.review.dto.CreatePresetCommentDto;
|
||||||
|
import com.competition.modules.biz.review.dto.SyncPresetCommentsDto;
|
||||||
import com.competition.modules.biz.review.entity.BizPresetComment;
|
import com.competition.modules.biz.review.entity.BizPresetComment;
|
||||||
import com.competition.modules.biz.review.service.IPresetCommentService;
|
import com.competition.modules.biz.review.service.IPresetCommentService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
@ -65,11 +66,10 @@ public class PresetCommentController {
|
|||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
@PostMapping("/batch-delete")
|
@PostMapping("/batch-delete")
|
||||||
@Operation(summary = "批量删除预设评语")
|
@Operation(summary = "批量删除预设评语")
|
||||||
public Result<Void> batchDelete(@RequestBody Map<String, Object> body) {
|
public Result<Void> batchDelete(@RequestBody Map<String, List<Long>> body) {
|
||||||
List<Long> ids = (List<Long>) body.get("ids");
|
List<Long> ids = body.get("ids");
|
||||||
Long judgeId = SecurityUtil.getCurrentUserId();
|
Long judgeId = SecurityUtil.getCurrentUserId();
|
||||||
presetCommentService.batchDelete(ids, judgeId);
|
presetCommentService.batchDelete(ids, judgeId);
|
||||||
return Result.success();
|
return Result.success();
|
||||||
@ -77,12 +77,9 @@ public class PresetCommentController {
|
|||||||
|
|
||||||
@PostMapping("/sync")
|
@PostMapping("/sync")
|
||||||
@Operation(summary = "同步评语到其他赛事")
|
@Operation(summary = "同步评语到其他赛事")
|
||||||
public Result<Map<String, Object>> syncComments(@RequestBody Map<String, Object> body) {
|
public Result<Map<String, Object>> syncComments(@Valid @RequestBody SyncPresetCommentsDto dto) {
|
||||||
Long sourceContestId = Long.valueOf(body.get("sourceContestId").toString());
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
List<Long> targetContestIds = (List<Long>) body.get("targetContestIds");
|
|
||||||
Long judgeId = SecurityUtil.getCurrentUserId();
|
Long judgeId = SecurityUtil.getCurrentUserId();
|
||||||
return Result.success(presetCommentService.syncComments(sourceContestId, targetContestIds, judgeId));
|
return Result.success(presetCommentService.syncComments(dto.getSourceContestId(), dto.getTargetContestIds(), judgeId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/{id}/use")
|
@PostMapping("/{id}/use")
|
||||||
|
|||||||
@ -0,0 +1,21 @@
|
|||||||
|
package com.competition.modules.biz.review.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 某赛事下的评委数据:显式关联与平台隐式池分离,避免扁平列表语义混淆。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "赛事评委查询结果")
|
||||||
|
public class ContestJudgesForContestVo {
|
||||||
|
|
||||||
|
@Schema(description = "机构为该赛事显式添加的评委(t_biz_contest_judge),每条必有 id、judgeId;添加评委抽屉回显与提交差集仅基于此列表")
|
||||||
|
private List<Map<String, Object>> assigned;
|
||||||
|
|
||||||
|
@Schema(description = "平台评委租户下对该赛事默认可用、未写入关联表的用户(id 为 null,isPlatform 为 true);可与 assigned 合并作为作品分配可选池")
|
||||||
|
private List<Map<String, Object>> implicitPool;
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package com.competition.modules.biz.review.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "同步预设评语DTO")
|
||||||
|
public class SyncPresetCommentsDto {
|
||||||
|
|
||||||
|
@NotNull(message = "源赛事ID不能为空")
|
||||||
|
@Schema(description = "源赛事ID")
|
||||||
|
private Long sourceContestId;
|
||||||
|
|
||||||
|
@NotEmpty(message = "目标赛事列表不能为空")
|
||||||
|
@Schema(description = "目标赛事ID列表")
|
||||||
|
private List<Long> targetContestIds;
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@ package com.competition.modules.biz.review.entity;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import com.competition.common.entity.BaseEntity;
|
import com.competition.common.entity.BaseEntity;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
@ -11,19 +12,23 @@ import java.math.BigDecimal;
|
|||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
@TableName("t_biz_contest_judge")
|
@TableName("t_biz_contest_judge")
|
||||||
|
@Schema(description = "赛事评委实体")
|
||||||
public class BizContestJudge extends BaseEntity {
|
public class BizContestJudge extends BaseEntity {
|
||||||
|
|
||||||
|
@Schema(description = "赛事ID")
|
||||||
@TableField("contest_id")
|
@TableField("contest_id")
|
||||||
private Long contestId;
|
private Long contestId;
|
||||||
|
|
||||||
/** 用户 ID */
|
@Schema(description = "评委用户ID")
|
||||||
@TableField("judge_id")
|
@TableField("judge_id")
|
||||||
private Long judgeId;
|
private Long judgeId;
|
||||||
|
|
||||||
|
@Schema(description = "专业特长")
|
||||||
private String specialty;
|
private String specialty;
|
||||||
|
|
||||||
/** 评委权重 0-1, Decimal(3,2) */
|
@Schema(description = "评委权重(0-1)")
|
||||||
private BigDecimal weight;
|
private BigDecimal weight;
|
||||||
|
|
||||||
|
@Schema(description = "评委描述")
|
||||||
private String description;
|
private String description;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,31 +4,37 @@ import com.baomidou.mybatisplus.annotation.TableField;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||||
import com.competition.common.entity.BaseEntity;
|
import com.competition.common.entity.BaseEntity;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
@TableName(value = "t_biz_contest_review_rule", autoResultMap = true)
|
@TableName(value = "t_biz_contest_review_rule", autoResultMap = true)
|
||||||
|
@Schema(description = "赛事评审规则实体")
|
||||||
public class BizContestReviewRule extends BaseEntity {
|
public class BizContestReviewRule extends BaseEntity {
|
||||||
|
|
||||||
|
@Schema(description = "租户ID")
|
||||||
@TableField("tenant_id")
|
@TableField("tenant_id")
|
||||||
private Long tenantId;
|
private Long tenantId;
|
||||||
|
|
||||||
|
@Schema(description = "规则名称")
|
||||||
@TableField("rule_name")
|
@TableField("rule_name")
|
||||||
private String ruleName;
|
private String ruleName;
|
||||||
|
|
||||||
|
@Schema(description = "规则描述")
|
||||||
@TableField("rule_description")
|
@TableField("rule_description")
|
||||||
private String ruleDescription;
|
private String ruleDescription;
|
||||||
|
|
||||||
|
@Schema(description = "评委人数")
|
||||||
@TableField("judge_count")
|
@TableField("judge_count")
|
||||||
private Integer judgeCount;
|
private Integer judgeCount;
|
||||||
|
|
||||||
/** JSON array of {name, percentage, description} */
|
@Schema(description = "评分维度(JSON数组)")
|
||||||
@TableField(value = "dimensions", typeHandler = JacksonTypeHandler.class)
|
@TableField(value = "dimensions", typeHandler = JacksonTypeHandler.class)
|
||||||
private Object dimensions;
|
private Object dimensions;
|
||||||
|
|
||||||
/** average/remove_max_min/remove_min/max/weighted */
|
@Schema(description = "计算规则:average/remove_max_min/remove_min/max/weighted")
|
||||||
@TableField("calculation_rule")
|
@TableField("calculation_rule")
|
||||||
private String calculationRule;
|
private String calculationRule;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.IdType;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
@ -11,33 +12,43 @@ import java.time.LocalDateTime;
|
|||||||
|
|
||||||
@Data
|
@Data
|
||||||
@TableName("t_biz_contest_work_judge_assignment")
|
@TableName("t_biz_contest_work_judge_assignment")
|
||||||
|
@Schema(description = "赛事作品评委分配实体")
|
||||||
public class BizContestWorkJudgeAssignment implements Serializable {
|
public class BizContestWorkJudgeAssignment implements Serializable {
|
||||||
|
|
||||||
|
@Schema(description = "主键ID")
|
||||||
@TableId(type = IdType.AUTO)
|
@TableId(type = IdType.AUTO)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "赛事ID")
|
||||||
@TableField("contest_id")
|
@TableField("contest_id")
|
||||||
private Long contestId;
|
private Long contestId;
|
||||||
|
|
||||||
|
@Schema(description = "作品ID")
|
||||||
@TableField("work_id")
|
@TableField("work_id")
|
||||||
private Long workId;
|
private Long workId;
|
||||||
|
|
||||||
|
@Schema(description = "评委ID")
|
||||||
@TableField("judge_id")
|
@TableField("judge_id")
|
||||||
private Long judgeId;
|
private Long judgeId;
|
||||||
|
|
||||||
|
@Schema(description = "分配时间")
|
||||||
@TableField("assignment_time")
|
@TableField("assignment_time")
|
||||||
private LocalDateTime assignmentTime;
|
private LocalDateTime assignmentTime;
|
||||||
|
|
||||||
/** assigned/reviewing/completed */
|
@Schema(description = "状态:assigned/reviewing/completed")
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
|
@Schema(description = "创建人ID")
|
||||||
private Integer creator;
|
private Integer creator;
|
||||||
|
|
||||||
|
@Schema(description = "修改人ID")
|
||||||
private Integer modifier;
|
private Integer modifier;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
@TableField("create_time")
|
@TableField("create_time")
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
@Schema(description = "修改时间")
|
||||||
@TableField("modify_time")
|
@TableField("modify_time")
|
||||||
private LocalDateTime modifyTime;
|
private LocalDateTime modifyTime;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||||
import com.competition.common.entity.BaseEntity;
|
import com.competition.common.entity.BaseEntity;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
@ -13,34 +14,45 @@ import java.time.LocalDateTime;
|
|||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
@TableName(value = "t_biz_contest_work_score", autoResultMap = true)
|
@TableName(value = "t_biz_contest_work_score", autoResultMap = true)
|
||||||
|
@Schema(description = "赛事作品评分实体")
|
||||||
public class BizContestWorkScore extends BaseEntity {
|
public class BizContestWorkScore extends BaseEntity {
|
||||||
|
|
||||||
|
@Schema(description = "租户ID")
|
||||||
@TableField("tenant_id")
|
@TableField("tenant_id")
|
||||||
private Long tenantId;
|
private Long tenantId;
|
||||||
|
|
||||||
|
@Schema(description = "赛事ID")
|
||||||
@TableField("contest_id")
|
@TableField("contest_id")
|
||||||
private Long contestId;
|
private Long contestId;
|
||||||
|
|
||||||
|
@Schema(description = "作品ID")
|
||||||
@TableField("work_id")
|
@TableField("work_id")
|
||||||
private Long workId;
|
private Long workId;
|
||||||
|
|
||||||
|
@Schema(description = "分配ID")
|
||||||
@TableField("assignment_id")
|
@TableField("assignment_id")
|
||||||
private Long assignmentId;
|
private Long assignmentId;
|
||||||
|
|
||||||
|
@Schema(description = "评委ID")
|
||||||
@TableField("judge_id")
|
@TableField("judge_id")
|
||||||
private Long judgeId;
|
private Long judgeId;
|
||||||
|
|
||||||
|
@Schema(description = "评委姓名")
|
||||||
@TableField("judge_name")
|
@TableField("judge_name")
|
||||||
private String judgeName;
|
private String judgeName;
|
||||||
|
|
||||||
|
@Schema(description = "各维度得分(JSON)")
|
||||||
@TableField(value = "dimension_scores", typeHandler = JacksonTypeHandler.class)
|
@TableField(value = "dimension_scores", typeHandler = JacksonTypeHandler.class)
|
||||||
private Object dimensionScores;
|
private Object dimensionScores;
|
||||||
|
|
||||||
|
@Schema(description = "总分")
|
||||||
@TableField("total_score")
|
@TableField("total_score")
|
||||||
private BigDecimal totalScore;
|
private BigDecimal totalScore;
|
||||||
|
|
||||||
|
@Schema(description = "评语")
|
||||||
private String comments;
|
private String comments;
|
||||||
|
|
||||||
|
@Schema(description = "评分时间")
|
||||||
@TableField("score_time")
|
@TableField("score_time")
|
||||||
private LocalDateTime scoreTime;
|
private LocalDateTime scoreTime;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package com.competition.modules.biz.review.entity;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import com.competition.common.entity.BaseEntity;
|
import com.competition.common.entity.BaseEntity;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
@ -11,22 +12,28 @@ import java.math.BigDecimal;
|
|||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
@TableName("t_biz_preset_comment")
|
@TableName("t_biz_preset_comment")
|
||||||
|
@Schema(description = "预设评语实体")
|
||||||
public class BizPresetComment extends BaseEntity {
|
public class BizPresetComment extends BaseEntity {
|
||||||
|
|
||||||
|
@Schema(description = "赛事ID")
|
||||||
@TableField("contest_id")
|
@TableField("contest_id")
|
||||||
private Long contestId;
|
private Long contestId;
|
||||||
|
|
||||||
|
@Schema(description = "评委ID")
|
||||||
@TableField("judge_id")
|
@TableField("judge_id")
|
||||||
private Long judgeId;
|
private Long judgeId;
|
||||||
|
|
||||||
|
@Schema(description = "评语内容")
|
||||||
private String content;
|
private String content;
|
||||||
|
|
||||||
/** Decimal(10,2) */
|
@Schema(description = "对应分数")
|
||||||
private BigDecimal score;
|
private BigDecimal score;
|
||||||
|
|
||||||
|
@Schema(description = "排序序号")
|
||||||
@TableField("sort_order")
|
@TableField("sort_order")
|
||||||
private Integer sortOrder;
|
private Integer sortOrder;
|
||||||
|
|
||||||
|
@Schema(description = "使用次数")
|
||||||
@TableField("use_count")
|
@TableField("use_count")
|
||||||
private Integer useCount;
|
private Integer useCount;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,21 @@
|
|||||||
package com.competition.modules.biz.review.service;
|
package com.competition.modules.biz.review.service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
import com.competition.modules.biz.review.dto.ContestJudgesForContestVo;
|
||||||
import com.competition.modules.biz.review.entity.BizContestJudge;
|
import com.competition.modules.biz.review.entity.BizContestJudge;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public interface IContestJudgeService extends IService<BizContestJudge> {
|
public interface IContestJudgeService extends IService<BizContestJudge> {
|
||||||
|
|
||||||
BizContestJudge createJudge(Long contestId, Long judgeId, String specialty, BigDecimal weight, String description);
|
BizContestJudge createJudge(Long contestId, Long judgeId, String specialty, BigDecimal weight, String description);
|
||||||
|
|
||||||
List<Map<String, Object>> findByContest(Long contestId);
|
/**
|
||||||
|
* 查询某赛事评委:{@link ContestJudgesForContestVo#getAssigned()} 为显式关联;
|
||||||
|
* {@link ContestJudgesForContestVo#getImplicitPool()} 为平台默认可用未落库项。
|
||||||
|
*/
|
||||||
|
ContestJudgesForContestVo findByContest(Long contestId);
|
||||||
|
|
||||||
Map<String, Object> findDetail(Long id);
|
Map<String, Object> findDetail(Long id);
|
||||||
|
|
||||||
|
|||||||
@ -31,4 +31,6 @@ public interface IContestReviewService {
|
|||||||
List<Map<String, Object>> getWorkScores(Long workId);
|
List<Map<String, Object>> getWorkScores(Long workId);
|
||||||
|
|
||||||
Map<String, Object> calculateFinalScore(Long workId);
|
Map<String, Object> calculateFinalScore(Long workId);
|
||||||
|
|
||||||
|
Map<String, Object> getJudgeContestDetail(Long judgeId, Long contestId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,10 +4,15 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import com.competition.common.enums.ErrorCode;
|
import com.competition.common.enums.ErrorCode;
|
||||||
import com.competition.common.exception.BusinessException;
|
import com.competition.common.exception.BusinessException;
|
||||||
|
import com.competition.modules.biz.review.dto.ContestJudgesForContestVo;
|
||||||
import com.competition.modules.biz.review.entity.BizContestJudge;
|
import com.competition.modules.biz.review.entity.BizContestJudge;
|
||||||
|
import com.competition.modules.biz.review.entity.BizContestWorkJudgeAssignment;
|
||||||
import com.competition.modules.biz.review.mapper.ContestJudgeMapper;
|
import com.competition.modules.biz.review.mapper.ContestJudgeMapper;
|
||||||
|
import com.competition.modules.biz.review.mapper.ContestWorkJudgeAssignmentMapper;
|
||||||
import com.competition.modules.biz.review.service.IContestJudgeService;
|
import com.competition.modules.biz.review.service.IContestJudgeService;
|
||||||
|
import com.competition.modules.sys.entity.SysTenant;
|
||||||
import com.competition.modules.sys.entity.SysUser;
|
import com.competition.modules.sys.entity.SysUser;
|
||||||
|
import com.competition.modules.sys.mapper.SysTenantMapper;
|
||||||
import com.competition.modules.sys.mapper.SysUserMapper;
|
import com.competition.modules.sys.mapper.SysUserMapper;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@ -23,7 +28,9 @@ import java.util.stream.Collectors;
|
|||||||
public class ContestJudgeServiceImpl extends ServiceImpl<ContestJudgeMapper, BizContestJudge> implements IContestJudgeService {
|
public class ContestJudgeServiceImpl extends ServiceImpl<ContestJudgeMapper, BizContestJudge> implements IContestJudgeService {
|
||||||
|
|
||||||
private final ContestJudgeMapper contestJudgeMapper;
|
private final ContestJudgeMapper contestJudgeMapper;
|
||||||
|
private final ContestWorkJudgeAssignmentMapper assignmentMapper;
|
||||||
private final SysUserMapper sysUserMapper;
|
private final SysUserMapper sysUserMapper;
|
||||||
|
private final SysTenantMapper sysTenantMapper;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public BizContestJudge createJudge(Long contestId, Long judgeId, String specialty, BigDecimal weight, String description) {
|
public BizContestJudge createJudge(Long contestId, Long judgeId, String specialty, BigDecimal weight, String description) {
|
||||||
@ -51,9 +58,13 @@ public class ContestJudgeServiceImpl extends ServiceImpl<ContestJudgeMapper, Biz
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Map<String, Object>> findByContest(Long contestId) {
|
public ContestJudgesForContestVo findByContest(Long contestId) {
|
||||||
log.info("查询赛事评委列表,赛事ID:{}", contestId);
|
log.info("查询赛事评委列表,赛事ID:{}", contestId);
|
||||||
|
|
||||||
|
// 获取平台评委租户 ID
|
||||||
|
Long judgeTenantId = getJudgeTenantId();
|
||||||
|
|
||||||
|
// 1. 查询已显式分配的评委
|
||||||
LambdaQueryWrapper<BizContestJudge> wrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<BizContestJudge> wrapper = new LambdaQueryWrapper<>();
|
||||||
wrapper.eq(BizContestJudge::getContestId, contestId);
|
wrapper.eq(BizContestJudge::getContestId, contestId);
|
||||||
wrapper.eq(BizContestJudge::getValidState, 1);
|
wrapper.eq(BizContestJudge::getValidState, 1);
|
||||||
@ -61,17 +72,51 @@ public class ContestJudgeServiceImpl extends ServiceImpl<ContestJudgeMapper, Biz
|
|||||||
|
|
||||||
List<BizContestJudge> judges = contestJudgeMapper.selectList(wrapper);
|
List<BizContestJudge> judges = contestJudgeMapper.selectList(wrapper);
|
||||||
|
|
||||||
|
// 收集所有需要查询的用户 ID(已分配评委)
|
||||||
|
Set<Long> assignedJudgeIds = judges.stream().map(BizContestJudge::getJudgeId).collect(Collectors.toSet());
|
||||||
|
|
||||||
|
// 2. 查询平台评委(自动对所有赛事可用)
|
||||||
|
List<SysUser> platformJudges = new ArrayList<>();
|
||||||
|
if (judgeTenantId != null) {
|
||||||
|
LambdaQueryWrapper<SysUser> platformWrapper = new LambdaQueryWrapper<>();
|
||||||
|
platformWrapper.eq(SysUser::getTenantId, judgeTenantId);
|
||||||
|
platformWrapper.eq(SysUser::getStatus, "enabled");
|
||||||
|
platformJudges = sysUserMapper.selectList(platformWrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 平台评委中去掉已显式分配的(避免重复)
|
||||||
|
Set<Long> platformOnlyIds = platformJudges.stream()
|
||||||
|
.map(SysUser::getId)
|
||||||
|
.filter(id -> !assignedJudgeIds.contains(id))
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
// 合并所有需要查询的用户 ID
|
||||||
|
Set<Long> allUserIds = new HashSet<>(assignedJudgeIds);
|
||||||
|
allUserIds.addAll(platformOnlyIds);
|
||||||
|
|
||||||
// 批量查询用户信息
|
// 批量查询用户信息
|
||||||
Set<Long> userIds = judges.stream().map(BizContestJudge::getJudgeId).collect(Collectors.toSet());
|
|
||||||
Map<Long, SysUser> userMap = new HashMap<>();
|
Map<Long, SysUser> userMap = new HashMap<>();
|
||||||
if (!userIds.isEmpty()) {
|
if (!allUserIds.isEmpty()) {
|
||||||
List<SysUser> users = sysUserMapper.selectBatchIds(userIds);
|
List<SysUser> users = sysUserMapper.selectBatchIds(allUserIds);
|
||||||
for (SysUser user : users) {
|
for (SysUser user : users) {
|
||||||
userMap.put(user.getId(), user);
|
userMap.put(user.getId(), user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return judges.stream().map(j -> {
|
// 批量查询每个评委在该赛事下的已分配作品数
|
||||||
|
Map<Long, Long> assignedCountMap = new HashMap<>();
|
||||||
|
for (Long judgeId : allUserIds) {
|
||||||
|
LambdaQueryWrapper<BizContestWorkJudgeAssignment> assignWrapper = new LambdaQueryWrapper<>();
|
||||||
|
assignWrapper.eq(BizContestWorkJudgeAssignment::getContestId, contestId);
|
||||||
|
assignWrapper.eq(BizContestWorkJudgeAssignment::getJudgeId, judgeId);
|
||||||
|
assignedCountMap.put(judgeId, assignmentMapper.selectCount(assignWrapper));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Map<String, Object>> assigned = new ArrayList<>();
|
||||||
|
List<Map<String, Object>> implicitPool = new ArrayList<>();
|
||||||
|
|
||||||
|
// 构建已显式分配的评委数据
|
||||||
|
for (BizContestJudge j : judges) {
|
||||||
Map<String, Object> map = new LinkedHashMap<>();
|
Map<String, Object> map = new LinkedHashMap<>();
|
||||||
map.put("id", j.getId());
|
map.put("id", j.getId());
|
||||||
map.put("contestId", j.getContestId());
|
map.put("contestId", j.getContestId());
|
||||||
@ -80,14 +125,59 @@ public class ContestJudgeServiceImpl extends ServiceImpl<ContestJudgeMapper, Biz
|
|||||||
map.put("weight", j.getWeight());
|
map.put("weight", j.getWeight());
|
||||||
map.put("description", j.getDescription());
|
map.put("description", j.getDescription());
|
||||||
map.put("createTime", j.getCreateTime());
|
map.put("createTime", j.getCreateTime());
|
||||||
|
map.put("assignedCount", assignedCountMap.getOrDefault(j.getJudgeId(), 0L));
|
||||||
|
|
||||||
SysUser user = userMap.get(j.getJudgeId());
|
SysUser user = userMap.get(j.getJudgeId());
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
map.put("judgeName", user.getNickname());
|
map.put("judgeName", user.getNickname());
|
||||||
map.put("judgeUsername", user.getUsername());
|
map.put("judgeUsername", user.getUsername());
|
||||||
|
map.put("tenantId", user.getTenantId());
|
||||||
|
map.put("status", user.getStatus());
|
||||||
|
map.put("organization", user.getOrganization());
|
||||||
|
map.put("isPlatform", judgeTenantId != null && judgeTenantId.equals(user.getTenantId()));
|
||||||
|
} else {
|
||||||
|
map.put("isPlatform", false);
|
||||||
}
|
}
|
||||||
return map;
|
assigned.add(map);
|
||||||
}).collect(Collectors.toList());
|
}
|
||||||
|
|
||||||
|
// 未显式分配的平台评委(隐式池)
|
||||||
|
for (SysUser platformJudge : platformJudges) {
|
||||||
|
if (assignedJudgeIds.contains(platformJudge.getId())) {
|
||||||
|
continue; // 已在显式分配列表中,跳过
|
||||||
|
}
|
||||||
|
Map<String, Object> map = new LinkedHashMap<>();
|
||||||
|
map.put("id", null);
|
||||||
|
map.put("contestId", contestId);
|
||||||
|
map.put("judgeId", platformJudge.getId());
|
||||||
|
map.put("specialty", null);
|
||||||
|
map.put("weight", null);
|
||||||
|
map.put("description", null);
|
||||||
|
map.put("createTime", null);
|
||||||
|
map.put("assignedCount", assignedCountMap.getOrDefault(platformJudge.getId(), 0L));
|
||||||
|
map.put("judgeName", platformJudge.getNickname());
|
||||||
|
map.put("judgeUsername", platformJudge.getUsername());
|
||||||
|
map.put("tenantId", platformJudge.getTenantId());
|
||||||
|
map.put("status", platformJudge.getStatus());
|
||||||
|
map.put("organization", platformJudge.getOrganization());
|
||||||
|
map.put("isPlatform", true);
|
||||||
|
implicitPool.add(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
ContestJudgesForContestVo vo = new ContestJudgesForContestVo();
|
||||||
|
vo.setAssigned(assigned);
|
||||||
|
vo.setImplicitPool(implicitPool);
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取平台评委租户 ID
|
||||||
|
*/
|
||||||
|
private Long getJudgeTenantId() {
|
||||||
|
LambdaQueryWrapper<SysTenant> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(SysTenant::getCode, "judge");
|
||||||
|
SysTenant tenant = sysTenantMapper.selectOne(wrapper);
|
||||||
|
return tenant != null ? tenant.getId() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@ -6,8 +6,10 @@ import com.competition.common.enums.ErrorCode;
|
|||||||
import com.competition.common.exception.BusinessException;
|
import com.competition.common.exception.BusinessException;
|
||||||
import com.competition.common.result.PageResult;
|
import com.competition.common.result.PageResult;
|
||||||
import com.competition.modules.biz.contest.entity.BizContest;
|
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.BizContestWork;
|
||||||
import com.competition.modules.biz.contest.mapper.ContestMapper;
|
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.mapper.ContestWorkMapper;
|
||||||
import com.competition.modules.biz.review.dto.CreateScoreDto;
|
import com.competition.modules.biz.review.dto.CreateScoreDto;
|
||||||
import com.competition.modules.biz.review.entity.BizContestJudge;
|
import com.competition.modules.biz.review.entity.BizContestJudge;
|
||||||
@ -42,6 +44,7 @@ public class ContestReviewServiceImpl implements IContestReviewService {
|
|||||||
private final ContestJudgeMapper judgeMapper;
|
private final ContestJudgeMapper judgeMapper;
|
||||||
private final ContestWorkMapper workMapper;
|
private final ContestWorkMapper workMapper;
|
||||||
private final ContestMapper contestMapper;
|
private final ContestMapper contestMapper;
|
||||||
|
private final ContestRegistrationMapper registrationMapper;
|
||||||
private final ContestReviewRuleMapper reviewRuleMapper;
|
private final ContestReviewRuleMapper reviewRuleMapper;
|
||||||
private final SysUserMapper sysUserMapper;
|
private final SysUserMapper sysUserMapper;
|
||||||
|
|
||||||
@ -310,28 +313,55 @@ public class ContestReviewServiceImpl implements IContestReviewService {
|
|||||||
public List<Map<String, Object>> getJudgeContests(Long judgeId) {
|
public List<Map<String, Object>> getJudgeContests(Long judgeId) {
|
||||||
log.info("查询评委关联赛事,评委ID:{}", judgeId);
|
log.info("查询评委关联赛事,评委ID:{}", judgeId);
|
||||||
|
|
||||||
LambdaQueryWrapper<BizContestJudge> wrapper = new LambdaQueryWrapper<>();
|
Set<Long> contestIds = new LinkedHashSet<>();
|
||||||
wrapper.eq(BizContestJudge::getJudgeId, judgeId);
|
|
||||||
wrapper.eq(BizContestJudge::getValidState, 1);
|
|
||||||
|
|
||||||
List<BizContestJudge> judgeRecords = judgeMapper.selectList(wrapper);
|
LambdaQueryWrapper<BizContestJudge> judgeQw = new LambdaQueryWrapper<>();
|
||||||
Set<Long> contestIds = judgeRecords.stream().map(BizContestJudge::getContestId).collect(Collectors.toSet());
|
judgeQw.eq(BizContestJudge::getJudgeId, judgeId);
|
||||||
|
judgeQw.eq(BizContestJudge::getValidState, 1);
|
||||||
|
for (BizContestJudge r : judgeMapper.selectList(judgeQw)) {
|
||||||
|
contestIds.add(r.getContestId());
|
||||||
|
}
|
||||||
|
|
||||||
|
LambdaQueryWrapper<BizContestWorkJudgeAssignment> assignQw = new LambdaQueryWrapper<>();
|
||||||
|
assignQw.eq(BizContestWorkJudgeAssignment::getJudgeId, judgeId);
|
||||||
|
assignQw.select(BizContestWorkJudgeAssignment::getContestId);
|
||||||
|
for (BizContestWorkJudgeAssignment a : assignmentMapper.selectList(assignQw)) {
|
||||||
|
contestIds.add(a.getContestId());
|
||||||
|
}
|
||||||
|
|
||||||
if (contestIds.isEmpty()) {
|
if (contestIds.isEmpty()) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<BizContest> contests = contestMapper.selectBatchIds(contestIds);
|
List<BizContest> contests = contestMapper.selectBatchIds(contestIds);
|
||||||
return contests.stream().map(c -> {
|
return contests.stream()
|
||||||
Map<String, Object> map = new LinkedHashMap<>();
|
.sorted(Comparator.comparing(BizContest::getId).reversed())
|
||||||
map.put("contestId", c.getId());
|
.map(c -> {
|
||||||
map.put("contestName", c.getContestName());
|
Map<String, Object> map = new LinkedHashMap<>();
|
||||||
map.put("contestState", c.getContestState());
|
map.put("contestId", c.getId());
|
||||||
map.put("status", c.getStatus());
|
map.put("contestName", c.getContestName());
|
||||||
map.put("reviewStartTime", c.getReviewStartTime());
|
map.put("contestState", c.getContestState());
|
||||||
map.put("reviewEndTime", c.getReviewEndTime());
|
map.put("status", c.getStatus());
|
||||||
return map;
|
map.put("reviewStartTime", c.getReviewStartTime());
|
||||||
}).collect(Collectors.toList());
|
map.put("reviewEndTime", c.getReviewEndTime());
|
||||||
|
|
||||||
|
LambdaQueryWrapper<BizContestWorkJudgeAssignment> totalW = new LambdaQueryWrapper<>();
|
||||||
|
totalW.eq(BizContestWorkJudgeAssignment::getContestId, c.getId());
|
||||||
|
totalW.eq(BizContestWorkJudgeAssignment::getJudgeId, judgeId);
|
||||||
|
long totalAssigned = assignmentMapper.selectCount(totalW);
|
||||||
|
|
||||||
|
LambdaQueryWrapper<BizContestWorkJudgeAssignment> doneW = new LambdaQueryWrapper<>();
|
||||||
|
doneW.eq(BizContestWorkJudgeAssignment::getContestId, c.getId());
|
||||||
|
doneW.eq(BizContestWorkJudgeAssignment::getJudgeId, judgeId);
|
||||||
|
doneW.eq(BizContestWorkJudgeAssignment::getStatus, "completed");
|
||||||
|
long reviewed = assignmentMapper.selectCount(doneW);
|
||||||
|
|
||||||
|
map.put("totalAssigned", totalAssigned);
|
||||||
|
map.put("reviewed", reviewed);
|
||||||
|
map.put("pending", totalAssigned - reviewed);
|
||||||
|
return map;
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -345,7 +375,13 @@ public class ContestReviewServiceImpl implements IContestReviewService {
|
|||||||
wrapper.eq(BizContestWorkJudgeAssignment::getContestId, contestId);
|
wrapper.eq(BizContestWorkJudgeAssignment::getContestId, contestId);
|
||||||
|
|
||||||
if (StringUtils.hasText(reviewStatus)) {
|
if (StringUtils.hasText(reviewStatus)) {
|
||||||
wrapper.eq(BizContestWorkJudgeAssignment::getStatus, reviewStatus);
|
if ("reviewed".equalsIgnoreCase(reviewStatus)) {
|
||||||
|
wrapper.eq(BizContestWorkJudgeAssignment::getStatus, "completed");
|
||||||
|
} else if ("pending".equalsIgnoreCase(reviewStatus)) {
|
||||||
|
wrapper.ne(BizContestWorkJudgeAssignment::getStatus, "completed");
|
||||||
|
} else {
|
||||||
|
wrapper.eq(BizContestWorkJudgeAssignment::getStatus, reviewStatus);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
wrapper.orderByAsc(BizContestWorkJudgeAssignment::getAssignmentTime);
|
wrapper.orderByAsc(BizContestWorkJudgeAssignment::getAssignmentTime);
|
||||||
@ -377,19 +413,39 @@ public class ContestReviewServiceImpl implements IContestReviewService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 批量查询报名信息,用于补充submitterAccountNo
|
||||||
|
Set<Long> registrationIds = workMap.values().stream()
|
||||||
|
.map(BizContestWork::getRegistrationId)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
Map<Long, String> regAccountMap = new HashMap<>();
|
||||||
|
if (!registrationIds.isEmpty()) {
|
||||||
|
List<BizContestRegistration> registrations = registrationMapper.selectBatchIds(registrationIds);
|
||||||
|
for (BizContestRegistration r : registrations) {
|
||||||
|
regAccountMap.put(r.getId(), r.getAccountNo());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 组装VO并应用筛选
|
// 组装VO并应用筛选
|
||||||
List<Map<String, Object>> voList = new ArrayList<>();
|
List<Map<String, Object>> voList = new ArrayList<>();
|
||||||
for (BizContestWorkJudgeAssignment a : assignments) {
|
for (BizContestWorkJudgeAssignment a : assignments) {
|
||||||
BizContestWork work = workMap.get(a.getWorkId());
|
BizContestWork work = workMap.get(a.getWorkId());
|
||||||
if (work == null) continue;
|
if (work == null) continue;
|
||||||
|
|
||||||
|
// 优先取作品上的账号,为空则从报名记录补充
|
||||||
|
String submitterAcc = work.getSubmitterAccountNo();
|
||||||
|
if (submitterAcc == null && work.getRegistrationId() != null) {
|
||||||
|
submitterAcc = regAccountMap.get(work.getRegistrationId());
|
||||||
|
}
|
||||||
|
|
||||||
// workNo筛选
|
// workNo筛选
|
||||||
if (StringUtils.hasText(workNo) && !work.getWorkNo().contains(workNo)) {
|
if (StringUtils.hasText(workNo) && (work.getWorkNo() == null
|
||||||
|
|| !work.getWorkNo().contains(workNo))) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// accountNo筛选
|
// accountNo筛选
|
||||||
if (StringUtils.hasText(accountNo) && work.getSubmitterAccountNo() != null
|
if (StringUtils.hasText(accountNo) && (submitterAcc == null
|
||||||
&& !work.getSubmitterAccountNo().contains(accountNo)) {
|
|| !submitterAcc.contains(accountNo))) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -402,7 +458,7 @@ public class ContestReviewServiceImpl implements IContestReviewService {
|
|||||||
map.put("title", work.getTitle());
|
map.put("title", work.getTitle());
|
||||||
map.put("previewUrl", work.getPreviewUrl());
|
map.put("previewUrl", work.getPreviewUrl());
|
||||||
map.put("previewUrls", work.getPreviewUrls());
|
map.put("previewUrls", work.getPreviewUrls());
|
||||||
map.put("submitterAccountNo", work.getSubmitterAccountNo());
|
map.put("submitterAccountNo", submitterAcc);
|
||||||
|
|
||||||
BizContestWorkScore scoreRecord = scoreMap.get(a.getId());
|
BizContestWorkScore scoreRecord = scoreMap.get(a.getId());
|
||||||
if (scoreRecord != null) {
|
if (scoreRecord != null) {
|
||||||
@ -645,4 +701,57 @@ public class ContestReviewServiceImpl implements IContestReviewService {
|
|||||||
result.put("calculationRule", calculationRule);
|
result.put("calculationRule", calculationRule);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ====== 评委赛事详情 ======
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> getJudgeContestDetail(Long judgeId, Long contestId) {
|
||||||
|
log.info("获取评委赛事详情,评委ID:{},赛事ID:{}", judgeId, contestId);
|
||||||
|
|
||||||
|
// 显式评委 或 已有作品分配记录
|
||||||
|
LambdaQueryWrapper<BizContestJudge> judgeWrapper = new LambdaQueryWrapper<>();
|
||||||
|
judgeWrapper.eq(BizContestJudge::getContestId, contestId);
|
||||||
|
judgeWrapper.eq(BizContestJudge::getJudgeId, judgeId);
|
||||||
|
judgeWrapper.eq(BizContestJudge::getValidState, 1);
|
||||||
|
boolean explicitJudge = judgeMapper.selectCount(judgeWrapper) > 0;
|
||||||
|
if (!explicitJudge) {
|
||||||
|
LambdaQueryWrapper<BizContestWorkJudgeAssignment> assignCheck = new LambdaQueryWrapper<>();
|
||||||
|
assignCheck.eq(BizContestWorkJudgeAssignment::getContestId, contestId);
|
||||||
|
assignCheck.eq(BizContestWorkJudgeAssignment::getJudgeId, judgeId);
|
||||||
|
if (assignmentMapper.selectCount(assignCheck) == 0) {
|
||||||
|
throw BusinessException.of(ErrorCode.FORBIDDEN, "您不是该赛事的评委");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BizContest contest = contestMapper.selectById(contestId);
|
||||||
|
if (contest == null) {
|
||||||
|
throw BusinessException.of(ErrorCode.NOT_FOUND, "赛事不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
result.put("contestId", contest.getId());
|
||||||
|
result.put("contestName", contest.getContestName());
|
||||||
|
result.put("contestType", contest.getContestType());
|
||||||
|
result.put("contestState", contest.getContestState());
|
||||||
|
result.put("status", contest.getStatus());
|
||||||
|
result.put("reviewStartTime", contest.getReviewStartTime());
|
||||||
|
result.put("reviewEndTime", contest.getReviewEndTime());
|
||||||
|
|
||||||
|
// 评审规则(前端ReviewWorkModal需要此数据)
|
||||||
|
if (contest.getReviewRuleId() != null) {
|
||||||
|
BizContestReviewRule rule = reviewRuleMapper.selectById(contest.getReviewRuleId());
|
||||||
|
if (rule != null) {
|
||||||
|
Map<String, Object> ruleMap = new LinkedHashMap<>();
|
||||||
|
ruleMap.put("id", rule.getId());
|
||||||
|
ruleMap.put("ruleName", rule.getRuleName());
|
||||||
|
ruleMap.put("ruleDescription", rule.getRuleDescription());
|
||||||
|
ruleMap.put("judgeCount", rule.getJudgeCount());
|
||||||
|
ruleMap.put("dimensions", rule.getDimensions());
|
||||||
|
ruleMap.put("calculationRule", rule.getCalculationRule());
|
||||||
|
result.put("reviewRule", ruleMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
|||||||
import com.competition.common.enums.ErrorCode;
|
import com.competition.common.enums.ErrorCode;
|
||||||
import com.competition.common.exception.BusinessException;
|
import com.competition.common.exception.BusinessException;
|
||||||
import com.competition.modules.biz.review.dto.CreatePresetCommentDto;
|
import com.competition.modules.biz.review.dto.CreatePresetCommentDto;
|
||||||
|
import com.competition.modules.biz.contest.entity.BizContest;
|
||||||
|
import com.competition.modules.biz.contest.mapper.ContestMapper;
|
||||||
import com.competition.modules.biz.review.entity.BizContestJudge;
|
import com.competition.modules.biz.review.entity.BizContestJudge;
|
||||||
import com.competition.modules.biz.review.entity.BizPresetComment;
|
import com.competition.modules.biz.review.entity.BizPresetComment;
|
||||||
import com.competition.modules.biz.review.mapper.ContestJudgeMapper;
|
import com.competition.modules.biz.review.mapper.ContestJudgeMapper;
|
||||||
@ -25,6 +27,7 @@ public class PresetCommentServiceImpl extends ServiceImpl<PresetCommentMapper, B
|
|||||||
|
|
||||||
private final PresetCommentMapper presetCommentMapper;
|
private final PresetCommentMapper presetCommentMapper;
|
||||||
private final ContestJudgeMapper contestJudgeMapper;
|
private final ContestJudgeMapper contestJudgeMapper;
|
||||||
|
private final ContestMapper contestMapper;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public BizPresetComment createComment(CreatePresetCommentDto dto, Long judgeId) {
|
public BizPresetComment createComment(CreatePresetCommentDto dto, Long judgeId) {
|
||||||
@ -162,7 +165,14 @@ public class PresetCommentServiceImpl extends ServiceImpl<PresetCommentMapper, B
|
|||||||
|
|
||||||
return judgeRecords.stream().map(j -> {
|
return judgeRecords.stream().map(j -> {
|
||||||
Map<String, Object> map = new LinkedHashMap<>();
|
Map<String, Object> map = new LinkedHashMap<>();
|
||||||
map.put("contestId", j.getContestId());
|
map.put("id", j.getContestId());
|
||||||
|
// 查询赛事详情获取名称和状态
|
||||||
|
BizContest contest = contestMapper.selectById(j.getContestId());
|
||||||
|
if (contest != null) {
|
||||||
|
map.put("contestName", contest.getContestName());
|
||||||
|
map.put("contestState", contest.getContestState());
|
||||||
|
map.put("status", contest.getStatus());
|
||||||
|
}
|
||||||
return map;
|
return map;
|
||||||
}).collect(Collectors.toList());
|
}).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
@ -201,9 +211,8 @@ public class PresetCommentServiceImpl extends ServiceImpl<PresetCommentMapper, B
|
|||||||
log.info("预设评语同步完成,新建数量:{}", created);
|
log.info("预设评语同步完成,新建数量:{}", created);
|
||||||
|
|
||||||
Map<String, Object> result = new LinkedHashMap<>();
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
result.put("sourceCount", sourceComments.size());
|
result.put("message", "同步成功");
|
||||||
result.put("targetContests", targetContestIds.size());
|
result.put("count", created);
|
||||||
result.put("createdCount", created);
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,72 @@
|
|||||||
|
package com.competition.modules.biz.review.util;
|
||||||
|
|
||||||
|
import com.competition.modules.biz.review.entity.BizContestWorkScore;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 与 {@code ContestReviewServiceImpl#calculateFinalScore}、成果发布计算逻辑一致的终分计算(列表场景对不足条数规则做温和回退)。
|
||||||
|
*/
|
||||||
|
public final class ContestFinalScoreCalculator {
|
||||||
|
|
||||||
|
private ContestFinalScoreCalculator() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BigDecimal compute(
|
||||||
|
List<BizContestWorkScore> scores,
|
||||||
|
String calculationRule,
|
||||||
|
Map<Long, BigDecimal> weightMap) {
|
||||||
|
if (scores == null || scores.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String rule = calculationRule != null ? calculationRule : "average";
|
||||||
|
Map<Long, BigDecimal> wm = weightMap != null ? weightMap : Collections.emptyMap();
|
||||||
|
|
||||||
|
List<BigDecimal> scoreValues = scores.stream()
|
||||||
|
.map(BizContestWorkScore::getTotalScore)
|
||||||
|
.sorted()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
switch (rule) {
|
||||||
|
case "max":
|
||||||
|
return Collections.max(scoreValues);
|
||||||
|
case "min":
|
||||||
|
return Collections.min(scoreValues);
|
||||||
|
case "weighted":
|
||||||
|
BigDecimal weightedSum = BigDecimal.ZERO;
|
||||||
|
BigDecimal totalWeight = BigDecimal.ZERO;
|
||||||
|
for (BizContestWorkScore s : scores) {
|
||||||
|
BigDecimal w = wm.getOrDefault(s.getJudgeId(), BigDecimal.ONE);
|
||||||
|
weightedSum = weightedSum.add(s.getTotalScore().multiply(w));
|
||||||
|
totalWeight = totalWeight.add(w);
|
||||||
|
}
|
||||||
|
return totalWeight.compareTo(BigDecimal.ZERO) > 0
|
||||||
|
? weightedSum.divide(totalWeight, 2, RoundingMode.HALF_UP)
|
||||||
|
: BigDecimal.ZERO;
|
||||||
|
case "remove_max_min":
|
||||||
|
if (scoreValues.size() < 3) {
|
||||||
|
BigDecimal sum = scoreValues.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
return sum.divide(BigDecimal.valueOf(scoreValues.size()), 2, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
List<BigDecimal> trimmed = scoreValues.subList(1, scoreValues.size() - 1);
|
||||||
|
BigDecimal trimmedSum = trimmed.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
return trimmedSum.divide(BigDecimal.valueOf(trimmed.size()), 2, RoundingMode.HALF_UP);
|
||||||
|
case "remove_min":
|
||||||
|
if (scoreValues.size() < 2) {
|
||||||
|
return scoreValues.get(0);
|
||||||
|
}
|
||||||
|
List<BigDecimal> withoutMin = scoreValues.subList(1, scoreValues.size());
|
||||||
|
BigDecimal withoutMinSum = withoutMin.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
return withoutMinSum.divide(BigDecimal.valueOf(withoutMin.size()), 2, RoundingMode.HALF_UP);
|
||||||
|
case "average":
|
||||||
|
default:
|
||||||
|
BigDecimal sum = scoreValues.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
return sum.divide(BigDecimal.valueOf(scoreValues.size()), 2, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
package com.competition.modules.leai.config;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 乐读派 AI 创作系统配置
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Configuration
|
||||||
|
@ConfigurationProperties(prefix = "leai")
|
||||||
|
public class LeaiConfig {
|
||||||
|
|
||||||
|
/** 机构ID(乐读派管理后台分配) */
|
||||||
|
// private String orgId = "LESINGLE888888888";
|
||||||
|
private String orgId = "gdlib";
|
||||||
|
|
||||||
|
/** 机构密钥(乐读派管理后台分配) */
|
||||||
|
private String appSecret = "leai_mnoi9q1a_mtcawrn8y";
|
||||||
|
// private String appSecret = "leai_test_secret_2026_abc123xyz";
|
||||||
|
|
||||||
|
/** 乐读派后端 API 地址 */
|
||||||
|
private String apiUrl = "http://192.168.1.72:8080";
|
||||||
|
|
||||||
|
/** 乐读派 H5 前端地址 */
|
||||||
|
private String h5Url = "http://192.168.1.72:3001";
|
||||||
|
}
|
||||||
@ -0,0 +1,147 @@
|
|||||||
|
package com.competition.modules.leai.controller;
|
||||||
|
|
||||||
|
import com.competition.common.exception.BusinessException;
|
||||||
|
import com.competition.common.result.Result;
|
||||||
|
import com.competition.common.util.SecurityUtil;
|
||||||
|
import com.competition.modules.leai.config.LeaiConfig;
|
||||||
|
import com.competition.modules.leai.dto.LeaiAuthRedirectDTO;
|
||||||
|
import com.competition.modules.leai.service.LeaiApiClient;
|
||||||
|
import com.competition.modules.leai.vo.LeaiTokenVO;
|
||||||
|
import com.competition.modules.sys.entity.SysUser;
|
||||||
|
import com.competition.modules.sys.service.ISysUserService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 乐读派认证入口控制器
|
||||||
|
* 前端 iframe 模式的主入口
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Tag(name = "乐读派认证")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/leai-auth")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class LeaiAuthController {
|
||||||
|
|
||||||
|
private final LeaiApiClient leaiApiClient;
|
||||||
|
private final LeaiConfig leaiConfig;
|
||||||
|
private final ISysUserService sysUserService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 前端 iframe 主入口:返回 token 信息 JSON
|
||||||
|
* 需要登录认证
|
||||||
|
*/
|
||||||
|
@GetMapping("/token")
|
||||||
|
@Operation(summary = "获取乐读派创作 Token")
|
||||||
|
public Result<LeaiTokenVO> getToken() {
|
||||||
|
Long userId = SecurityUtil.getCurrentUserId();
|
||||||
|
SysUser user = sysUserService.getById(userId);
|
||||||
|
if (user == null) {
|
||||||
|
throw new BusinessException(404, "用户不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
String phone = user.getPhone();
|
||||||
|
if (phone == null || phone.isEmpty()) {
|
||||||
|
throw new BusinessException(400, "用户未绑定手机号,无法使用创作功能");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String token = leaiApiClient.exchangeToken(phone);
|
||||||
|
|
||||||
|
// Entity → VO 转换(Controller 层负责)
|
||||||
|
// 注意: orgId 对应本项目的租户 code(tenant_code)
|
||||||
|
LeaiTokenVO vo = new LeaiTokenVO();
|
||||||
|
vo.setToken(token);
|
||||||
|
vo.setOrgId(leaiConfig.getOrgId()); // 即租户 tenant_code
|
||||||
|
vo.setH5Url(leaiConfig.getH5Url());
|
||||||
|
vo.setPhone(phone);
|
||||||
|
|
||||||
|
log.info("[乐读派] 获取创作Token成功, userId={}, phone={}", userId, phone);
|
||||||
|
return Result.success(vo);
|
||||||
|
|
||||||
|
} catch (BusinessException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[乐读派] 获取创作Token失败, userId={}", userId, e);
|
||||||
|
throw new BusinessException(500, "获取创作Token失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转模式备选:换 token + 302 重定向到 H5
|
||||||
|
* 需要登录认证
|
||||||
|
*/
|
||||||
|
@GetMapping
|
||||||
|
@Operation(summary = "重定向到乐读派 H5 创作页")
|
||||||
|
public void authRedirect(LeaiAuthRedirectDTO dto, HttpServletResponse response) throws IOException {
|
||||||
|
Long userId = SecurityUtil.getCurrentUserId();
|
||||||
|
SysUser user = sysUserService.getById(userId);
|
||||||
|
if (user == null || user.getPhone() == null || user.getPhone().isEmpty()) {
|
||||||
|
response.sendError(401, "请先登录并绑定手机号");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String phone = user.getPhone();
|
||||||
|
try {
|
||||||
|
String token = leaiApiClient.exchangeToken(phone);
|
||||||
|
|
||||||
|
StringBuilder url = new StringBuilder(leaiConfig.getH5Url())
|
||||||
|
.append("/?token=").append(URLEncoder.encode(token, StandardCharsets.UTF_8))
|
||||||
|
.append("&orgId=").append(URLEncoder.encode(leaiConfig.getOrgId(), StandardCharsets.UTF_8))
|
||||||
|
.append("&phone=").append(URLEncoder.encode(phone, StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
if (dto.getReturnPath() != null && !dto.getReturnPath().isEmpty()) {
|
||||||
|
url.append("&returnPath=").append(URLEncoder.encode(dto.getReturnPath(), StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("[乐读派] 重定向到H5, userId={}, phone={}", userId, phone);
|
||||||
|
response.sendRedirect(url.toString());
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[乐读派] 重定向失败, userId={}", userId, e);
|
||||||
|
response.sendError(500, "跳转乐读派失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* iframe 内 Token 刷新接口
|
||||||
|
* 前端 JS 在收到 TOKEN_EXPIRED postMessage 时调用此接口
|
||||||
|
*/
|
||||||
|
@GetMapping("/refresh-token")
|
||||||
|
@Operation(summary = "刷新乐读派 Token")
|
||||||
|
public Result<LeaiTokenVO> refreshToken() {
|
||||||
|
Long userId = SecurityUtil.getCurrentUserId();
|
||||||
|
SysUser user = sysUserService.getById(userId);
|
||||||
|
if (user == null || user.getPhone() == null || user.getPhone().isEmpty()) {
|
||||||
|
throw new BusinessException(401, "请先登录并绑定手机号");
|
||||||
|
}
|
||||||
|
|
||||||
|
String phone = user.getPhone();
|
||||||
|
try {
|
||||||
|
String token = leaiApiClient.exchangeToken(phone);
|
||||||
|
|
||||||
|
// Entity → VO 转换(Controller 层负责)
|
||||||
|
LeaiTokenVO vo = new LeaiTokenVO();
|
||||||
|
vo.setToken(token);
|
||||||
|
vo.setOrgId(leaiConfig.getOrgId());
|
||||||
|
vo.setPhone(phone);
|
||||||
|
|
||||||
|
log.info("[乐读派] Token刷新成功, userId={}", userId);
|
||||||
|
return Result.success(vo);
|
||||||
|
|
||||||
|
} catch (BusinessException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[乐读派] Token刷新失败, userId={}", userId, e);
|
||||||
|
throw new BusinessException(500, "Token刷新失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,111 @@
|
|||||||
|
package com.competition.modules.leai.controller;
|
||||||
|
|
||||||
|
import com.competition.common.exception.BusinessException;
|
||||||
|
import com.competition.modules.leai.service.ILeaiSyncService;
|
||||||
|
import com.competition.modules.leai.service.ILeaiWebhookEventService;
|
||||||
|
import com.competition.modules.leai.service.LeaiApiClient;
|
||||||
|
import com.competition.modules.leai.util.LeaiUtil;
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 乐读派 Webhook 接收控制器
|
||||||
|
* 无需认证(由乐读派服务端调用,通过 HMAC 签名验证)
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Tag(name = "乐读派 Webhook")
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class LeaiWebhookController {
|
||||||
|
|
||||||
|
private final LeaiApiClient leaiApiClient;
|
||||||
|
private final ILeaiSyncService leaiSyncService;
|
||||||
|
private final ILeaiWebhookEventService webhookEventService;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收乐读派 Webhook 回调
|
||||||
|
* POST /webhook/leai
|
||||||
|
* <p>
|
||||||
|
* Header:
|
||||||
|
* X-Webhook-Id: 事件唯一 ID
|
||||||
|
* X-Webhook-Event: 事件类型 (work.status_changed / work.progress)
|
||||||
|
* X-Webhook-Timestamp: 毫秒时间戳
|
||||||
|
* X-Webhook-Signature: HMAC-SHA256={hex}
|
||||||
|
*/
|
||||||
|
@PostMapping("/webhook/leai")
|
||||||
|
@Operation(summary = "接收乐读派 Webhook 回调")
|
||||||
|
public Map<String, String> webhook(
|
||||||
|
@RequestBody String rawBody,
|
||||||
|
@RequestHeader("X-Webhook-Id") String webhookId,
|
||||||
|
@RequestHeader("X-Webhook-Event") String webhookEvent,
|
||||||
|
@RequestHeader("X-Webhook-Timestamp") String timestamp,
|
||||||
|
@RequestHeader("X-Webhook-Signature") String signature) {
|
||||||
|
|
||||||
|
log.info("[Webhook] 收到事件: event={}, id={}", webhookEvent, webhookId);
|
||||||
|
|
||||||
|
// 1. 时间窗口检查(5分钟,防重放攻击)
|
||||||
|
long ts;
|
||||||
|
try {
|
||||||
|
ts = Long.parseLong(timestamp);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
log.warn("[Webhook] 时间戳格式错误: {}", timestamp);
|
||||||
|
throw new BusinessException(400, "时间戳格式错误");
|
||||||
|
}
|
||||||
|
if (Math.abs(System.currentTimeMillis() - ts) > 300_000) {
|
||||||
|
log.warn("[Webhook] 时间戳已过期: {}", timestamp);
|
||||||
|
throw new BusinessException(400, "时间戳已过期");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 幂等去重
|
||||||
|
if (webhookEventService.existsByEventId(webhookId)) {
|
||||||
|
log.info("[Webhook] 重复事件,跳过: {}", webhookId);
|
||||||
|
return Collections.singletonMap("status", "duplicate");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 验证 HMAC-SHA256 签名
|
||||||
|
if (!leaiApiClient.verifyWebhookSignature(webhookId, timestamp, rawBody, signature)) {
|
||||||
|
log.warn("[Webhook] 签名验证失败! webhookId={}", webhookId);
|
||||||
|
throw new BusinessException(401, "签名验证失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 解析事件 payload
|
||||||
|
Map<String, Object> payload;
|
||||||
|
try {
|
||||||
|
payload = objectMapper.readValue(rawBody, new TypeReference<Map<String, Object>>() {});
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[Webhook] payload 解析失败", e);
|
||||||
|
throw new BusinessException(400, "payload 解析失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
String event = LeaiUtil.toString(payload.get("event"), "");
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> data = (Map<String, Object>) payload.get("data");
|
||||||
|
if (data == null) {
|
||||||
|
data = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
String remoteWorkId = LeaiUtil.toString(data.get("work_id"), null);
|
||||||
|
|
||||||
|
// 5. 按 V4.0 同步规则处理
|
||||||
|
if (remoteWorkId != null && !remoteWorkId.isEmpty()) {
|
||||||
|
try {
|
||||||
|
leaiSyncService.syncWork(remoteWorkId, data, "Webhook[" + event + "]");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[Webhook] 同步处理异常: remoteWorkId={}", remoteWorkId, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 记录事件(幂等去重)
|
||||||
|
webhookEventService.saveEvent(webhookId, webhookEvent, remoteWorkId, payload);
|
||||||
|
|
||||||
|
return Collections.singletonMap("status", "ok");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
package com.competition.modules.leai.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 乐读派重定向请求 DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "乐读派重定向请求")
|
||||||
|
public class LeaiAuthRedirectDTO {
|
||||||
|
|
||||||
|
@Schema(description = "重定向后的路径")
|
||||||
|
private String returnPath;
|
||||||
|
}
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
package com.competition.modules.leai.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 乐读派 Webhook 事件去重表
|
||||||
|
* <p>
|
||||||
|
* 注意:此表为追加-only日志表,不需要 BaseEntity 的审计字段(updateBy、modifyTime、deleted 等),
|
||||||
|
* 因此独立定义 id 和 createTime,不继承 BaseEntity。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@TableName(value = "t_leai_webhook_event", autoResultMap = true)
|
||||||
|
@Schema(description = "乐读派 Webhook 事件")
|
||||||
|
public class LeaiWebhookEvent {
|
||||||
|
|
||||||
|
@Schema(description = "主键ID")
|
||||||
|
@TableField("id")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "事件唯一ID(X-Webhook-Id)")
|
||||||
|
@TableField("event_id")
|
||||||
|
private String eventId;
|
||||||
|
|
||||||
|
@Schema(description = "事件类型")
|
||||||
|
@TableField("event_type")
|
||||||
|
private String eventType;
|
||||||
|
|
||||||
|
@Schema(description = "乐读派作品ID")
|
||||||
|
@TableField("remote_work_id")
|
||||||
|
private String remoteWorkId;
|
||||||
|
|
||||||
|
@Schema(description = "事件原始载荷")
|
||||||
|
@TableField(value = "payload", typeHandler = JacksonTypeHandler.class)
|
||||||
|
private Object payload;
|
||||||
|
|
||||||
|
@Schema(description = "是否已处理:0-未处理,1-已处理")
|
||||||
|
@TableField("processed")
|
||||||
|
private Integer processed;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
|
@TableField("create_time")
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
package com.competition.modules.leai.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.competition.modules.leai.entity.LeaiWebhookEvent;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 乐读派 Webhook 事件 Mapper
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface LeaiWebhookEventMapper extends BaseMapper<LeaiWebhookEvent> {
|
||||||
|
}
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
package com.competition.modules.leai.service;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 乐读派作品同步 Service 接口
|
||||||
|
* <p>
|
||||||
|
* Webhook 回调和 B3 对账共用此服务
|
||||||
|
*/
|
||||||
|
public interface ILeaiSyncService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步乐读派作品到本地
|
||||||
|
* <p>
|
||||||
|
* 同步规则:
|
||||||
|
* - remoteStatus == -1 → 强制更新(FAILED)
|
||||||
|
* - remoteStatus == 2 → 强制更新(PROCESSING 进度变化)
|
||||||
|
* - remoteStatus > localStatus → 全量更新(状态前进)
|
||||||
|
* - else → 忽略
|
||||||
|
*
|
||||||
|
* @param remoteWorkId 乐读派作品ID
|
||||||
|
* @param remoteData 远程作品数据(来自 Webhook payload 或 B2/B3 查询结果)
|
||||||
|
* @param source 来源标识(用于日志,如 "Webhook[work.status_changed]")
|
||||||
|
*/
|
||||||
|
void syncWork(String remoteWorkId, Map<String, Object> remoteData, String source);
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
package com.competition.modules.leai.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
import com.competition.modules.leai.entity.LeaiWebhookEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 乐读派 Webhook 事件 Service 接口
|
||||||
|
*/
|
||||||
|
public interface ILeaiWebhookEventService extends IService<LeaiWebhookEvent> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查事件是否已存在(幂等去重)
|
||||||
|
*
|
||||||
|
* @param eventId 事件唯一ID
|
||||||
|
* @return true-已存在(重复事件)
|
||||||
|
*/
|
||||||
|
boolean existsByEventId(String eventId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存 Webhook 事件记录
|
||||||
|
*
|
||||||
|
* @param eventId 事件唯一ID
|
||||||
|
* @param eventType 事件类型
|
||||||
|
* @param remoteWorkId 乐读派作品ID
|
||||||
|
* @param payload 事件载荷
|
||||||
|
*/
|
||||||
|
void saveEvent(String eventId, String eventType, String remoteWorkId, Object payload);
|
||||||
|
}
|
||||||
@ -0,0 +1,261 @@
|
|||||||
|
package com.competition.modules.leai.service;
|
||||||
|
|
||||||
|
import com.competition.common.exception.BusinessException;
|
||||||
|
import com.competition.modules.leai.config.LeaiConfig;
|
||||||
|
import com.competition.modules.leai.util.LeaiUtil;
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.*;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 乐读派 API 客户端
|
||||||
|
* 使用 RestTemplate + Jackson 对接乐读派后端
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class LeaiApiClient {
|
||||||
|
|
||||||
|
private final RestTemplate restTemplate;
|
||||||
|
private final LeaiConfig leaiConfig;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
// ── 状态常量(V4.0 数值状态机) ──
|
||||||
|
public static final int STATUS_FAILED = -1;
|
||||||
|
public static final int STATUS_DRAFT = 0;
|
||||||
|
public static final int STATUS_PENDING = 1;
|
||||||
|
public static final int STATUS_PROCESSING = 2;
|
||||||
|
public static final int STATUS_COMPLETED = 3;
|
||||||
|
public static final int STATUS_CATALOGED = 4;
|
||||||
|
public static final int STATUS_DUBBED = 5;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 换取 Session Token
|
||||||
|
* POST /api/v1/auth/session
|
||||||
|
*/
|
||||||
|
public String exchangeToken(String phone) {
|
||||||
|
String url = leaiConfig.getApiUrl() + "/api/v1/auth/session";
|
||||||
|
|
||||||
|
Map<String, String> body = new HashMap<>();
|
||||||
|
body.put("orgId", leaiConfig.getOrgId());
|
||||||
|
body.put("appSecret", leaiConfig.getAppSecret());
|
||||||
|
body.put("phone", phone);
|
||||||
|
|
||||||
|
try {
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
|
|
||||||
|
HttpEntity<String> entity = new HttpEntity<>(objectMapper.writeValueAsString(body), headers);
|
||||||
|
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
|
||||||
|
|
||||||
|
Map<String, Object> result = objectMapper.readValue(response.getBody(),
|
||||||
|
new TypeReference<Map<String, Object>>() {});
|
||||||
|
|
||||||
|
int code = LeaiUtil.toInt(result.get("code"), 0);
|
||||||
|
if (code != 200) {
|
||||||
|
throw new BusinessException(502, "令牌交换失败: code=" + code
|
||||||
|
+ ", msg=" + LeaiUtil.toString(result.get("msg"), "unknown"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> data = (Map<String, Object>) result.get("data");
|
||||||
|
if (data == null) {
|
||||||
|
throw new BusinessException(502, "令牌交换失败: data 为 null");
|
||||||
|
}
|
||||||
|
|
||||||
|
String sessionToken = LeaiUtil.toString(data.get("sessionToken"), null);
|
||||||
|
if (sessionToken == null || sessionToken.isEmpty()) {
|
||||||
|
throw new BusinessException(502, "令牌交换失败: sessionToken 为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("[乐读派] 令牌交换成功, phone={}, expiresIn={}s", phone, data.get("expiresIn"));
|
||||||
|
return sessionToken;
|
||||||
|
|
||||||
|
} catch (BusinessException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new BusinessException(502, "令牌交换请求失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* B2 查询作品详情
|
||||||
|
* GET /api/v1/query/work/{workId}
|
||||||
|
*/
|
||||||
|
public Map<String, Object> fetchWorkDetail(String workId) {
|
||||||
|
Map<String, String> queryParams = new TreeMap<>();
|
||||||
|
queryParams.put("orgId", leaiConfig.getOrgId());
|
||||||
|
|
||||||
|
Map<String, String> hmacHeaders = buildHmacHeaders(queryParams);
|
||||||
|
|
||||||
|
try {
|
||||||
|
String url = leaiConfig.getApiUrl() + "/api/v1/query/work/"
|
||||||
|
+ URLEncoder.encode(workId, StandardCharsets.UTF_8)
|
||||||
|
+ "?orgId=" + URLEncoder.encode(leaiConfig.getOrgId(), StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
hmacHeaders.forEach(headers::set);
|
||||||
|
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
|
||||||
|
|
||||||
|
HttpEntity<Void> entity = new HttpEntity<>(headers);
|
||||||
|
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
|
||||||
|
|
||||||
|
Map<String, Object> result = objectMapper.readValue(response.getBody(),
|
||||||
|
new TypeReference<Map<String, Object>>() {});
|
||||||
|
|
||||||
|
int code = LeaiUtil.toInt(result.get("code"), 0);
|
||||||
|
if (code != 200) {
|
||||||
|
log.warn("[乐读派] B2查询失败: workId={}, code={}", workId, code);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> data = (Map<String, Object>) result.get("data");
|
||||||
|
return data;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[乐读派] B2查询异常: workId={}", workId, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* B3 批量查询作品
|
||||||
|
* GET /api/v1/query/works
|
||||||
|
*/
|
||||||
|
public List<Map<String, Object>> queryWorks(String updatedAfter) {
|
||||||
|
Map<String, String> queryParams = new TreeMap<>();
|
||||||
|
queryParams.put("orgId", leaiConfig.getOrgId());
|
||||||
|
queryParams.put("updatedAfter", updatedAfter);
|
||||||
|
queryParams.put("page", "1");
|
||||||
|
queryParams.put("size", "100");
|
||||||
|
|
||||||
|
Map<String, String> hmacHeaders = buildHmacHeaders(queryParams);
|
||||||
|
|
||||||
|
try {
|
||||||
|
StringBuilder queryString = new StringBuilder();
|
||||||
|
for (Map.Entry<String, String> e : queryParams.entrySet()) {
|
||||||
|
if (queryString.length() > 0) queryString.append("&");
|
||||||
|
queryString.append(URLEncoder.encode(e.getKey(), StandardCharsets.UTF_8))
|
||||||
|
.append("=")
|
||||||
|
.append(URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
String url = leaiConfig.getApiUrl() + "/api/v1/query/works?" + queryString;
|
||||||
|
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
hmacHeaders.forEach(headers::set);
|
||||||
|
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
|
||||||
|
|
||||||
|
HttpEntity<Void> entity = new HttpEntity<>(headers);
|
||||||
|
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
|
||||||
|
|
||||||
|
Map<String, Object> result = objectMapper.readValue(response.getBody(),
|
||||||
|
new TypeReference<Map<String, Object>>() {});
|
||||||
|
|
||||||
|
int code = LeaiUtil.toInt(result.get("code"), 0);
|
||||||
|
if (code != 200) {
|
||||||
|
log.warn("[乐读派] B3查询失败: code={}, msg={}", code, result.get("msg"));
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> dataMap = (Map<String, Object>) result.get("data");
|
||||||
|
if (dataMap == null) return Collections.emptyList();
|
||||||
|
|
||||||
|
Object recordsObj = dataMap.get("records");
|
||||||
|
if (recordsObj instanceof List) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<Map<String, Object>> records = (List<Map<String, Object>>) recordsObj;
|
||||||
|
return records;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Collections.emptyList();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[乐读派] B3查询异常", e);
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成 HMAC 签名请求头
|
||||||
|
*/
|
||||||
|
public Map<String, String> buildHmacHeaders(Map<String, String> queryParams) {
|
||||||
|
String ts = String.valueOf(System.currentTimeMillis());
|
||||||
|
String nonce = Long.toHexString(System.currentTimeMillis()) + Long.toHexString(System.nanoTime());
|
||||||
|
|
||||||
|
TreeMap<String, String> allParams = new TreeMap<>(queryParams);
|
||||||
|
allParams.put("timestamp", ts);
|
||||||
|
allParams.put("nonce", nonce);
|
||||||
|
|
||||||
|
StringBuilder signStr = new StringBuilder();
|
||||||
|
for (Map.Entry<String, String> entry : allParams.entrySet()) {
|
||||||
|
if (signStr.length() > 0) signStr.append("&");
|
||||||
|
signStr.append(entry.getKey()).append("=").append(entry.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
String sig = hmacSha256(signStr.toString(), leaiConfig.getAppSecret());
|
||||||
|
|
||||||
|
Map<String, String> headers = new LinkedHashMap<>();
|
||||||
|
headers.put("X-App-Key", leaiConfig.getOrgId());
|
||||||
|
headers.put("X-Timestamp", ts);
|
||||||
|
headers.put("X-Nonce", nonce);
|
||||||
|
headers.put("X-Signature", sig);
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证 Webhook 签名
|
||||||
|
*/
|
||||||
|
public boolean verifyWebhookSignature(String webhookId, String timestamp, String rawBody, String signature) {
|
||||||
|
String signData = webhookId + "." + timestamp + "." + rawBody;
|
||||||
|
String expected = "HMAC-SHA256=" + hmacSha256(signData, leaiConfig.getAppSecret());
|
||||||
|
return MessageDigest.isEqual(
|
||||||
|
expected.getBytes(StandardCharsets.UTF_8),
|
||||||
|
signature.getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HMAC-SHA256 签名
|
||||||
|
*/
|
||||||
|
private String hmacSha256(String data, String secret) {
|
||||||
|
try {
|
||||||
|
Mac mac = Mac.getInstance("HmacSHA256");
|
||||||
|
SecretKeySpec keySpec = new SecretKeySpec(
|
||||||
|
secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
|
||||||
|
mac.init(keySpec);
|
||||||
|
byte[] hash = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
|
||||||
|
StringBuilder sb = new StringBuilder(hash.length * 2);
|
||||||
|
for (byte b : hash) {
|
||||||
|
sb.append(String.format("%02x", b & 0xff));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new BusinessException(500, "HMAC-SHA256 签名失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 2 小时前的 UTC 时间 ISO 格式(用于 B3 对账)
|
||||||
|
*/
|
||||||
|
public static String getUtcTwoHoursAgoIso() {
|
||||||
|
return ZonedDateTime.ofInstant(Instant.now().minusSeconds(7200), ZoneOffset.UTC)
|
||||||
|
.format(DateTimeFormatter.ISO_INSTANT);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,278 @@
|
|||||||
|
package com.competition.modules.leai.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||||
|
import com.competition.modules.leai.util.LeaiUtil;
|
||||||
|
import com.competition.modules.ugc.entity.UgcWork;
|
||||||
|
import com.competition.modules.ugc.entity.UgcWorkPage;
|
||||||
|
import com.competition.modules.ugc.mapper.UgcWorkMapper;
|
||||||
|
import com.competition.modules.ugc.mapper.UgcWorkPageMapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V4.0 状态同步核心服务
|
||||||
|
* Webhook 和 B3 对账共用此服务
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class LeaiSyncService implements ILeaiSyncService {
|
||||||
|
|
||||||
|
private final UgcWorkMapper ugcWorkMapper;
|
||||||
|
private final UgcWorkPageMapper ugcWorkPageMapper;
|
||||||
|
private final LeaiApiClient leaiApiClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V4.0 核心同步逻辑
|
||||||
|
* <p>
|
||||||
|
* 同步规则:
|
||||||
|
* - remoteStatus == -1 → 强制更新(FAILED)
|
||||||
|
* - remoteStatus == 2 → 强制更新(PROCESSING 进度变化)
|
||||||
|
* - remoteStatus > localStatus → 全量更新(状态前进)
|
||||||
|
* - else → 忽略
|
||||||
|
*
|
||||||
|
* @param remoteWorkId 乐读派作品ID
|
||||||
|
* @param remoteData 远程作品数据(来自 Webhook payload 或 B2/B3 查询结果)
|
||||||
|
* @param source 来源标识(用于日志,如 "Webhook[work.status_changed]")
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void syncWork(String remoteWorkId, Map<String, Object> remoteData, String source) {
|
||||||
|
int remoteStatus = LeaiUtil.toInt(remoteData.get("status"), 0);
|
||||||
|
String phone = LeaiUtil.toString(remoteData.get("phone"), null);
|
||||||
|
|
||||||
|
// 查找本地作品(通过 remoteWorkId)
|
||||||
|
UgcWork localWork = findByRemoteWorkId(remoteWorkId);
|
||||||
|
|
||||||
|
if (localWork == null) {
|
||||||
|
// ★ 新作品:首次收到此 remoteWorkId,创建本地记录
|
||||||
|
insertNewWork(remoteWorkId, remoteData, phone);
|
||||||
|
log.info("[{}] 新增作品 remoteWorkId={}, status={}, phone={}", source, remoteWorkId, remoteStatus, phone);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int localStatus = localWork.getStatus() != null ? localWork.getStatus() : 0;
|
||||||
|
|
||||||
|
if (remoteStatus == LeaiApiClient.STATUS_FAILED) {
|
||||||
|
// ★ FAILED(-1): 创作失败,强制更新
|
||||||
|
updateFailed(localWork, remoteData);
|
||||||
|
log.info("[{}] 强制更新(FAILED) remoteWorkId={}", source, remoteWorkId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remoteStatus == LeaiApiClient.STATUS_PROCESSING) {
|
||||||
|
// ★ PROCESSING(2): 创作进行中,强制更新进度
|
||||||
|
updateProcessing(localWork, remoteData);
|
||||||
|
log.info("[{}] 强制更新(PROCESSING) remoteWorkId={}, progress={}",
|
||||||
|
source, remoteWorkId, remoteData.get("progress"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remoteStatus > localStatus) {
|
||||||
|
// ★ 状态前进: 全量更新
|
||||||
|
// status=3(COMPLETED): pageList 包含图片 URL
|
||||||
|
// status=4(CATALOGED): title/author 已更新
|
||||||
|
// status=5(DUBBED): pageList 中 audioUrl 已填充
|
||||||
|
updateStatusForward(localWork, remoteData, remoteStatus);
|
||||||
|
log.info("[{}] 状态更新 remoteWorkId={}: {} -> {}", source, remoteWorkId, localStatus, remoteStatus);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 旧数据或重复推送,忽略
|
||||||
|
log.debug("[{}] 跳过 remoteWorkId={}, remote={} <= local={}", source, remoteWorkId, remoteStatus, localStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增作品记录
|
||||||
|
*/
|
||||||
|
private void insertNewWork(String remoteWorkId, Map<String, Object> remoteData, String phone) {
|
||||||
|
UgcWork work = new UgcWork();
|
||||||
|
work.setRemoteWorkId(remoteWorkId);
|
||||||
|
work.setTitle(LeaiUtil.toString(remoteData.get("title"), "未命名作品"));
|
||||||
|
work.setStatus(LeaiUtil.toInt(remoteData.get("status"), LeaiApiClient.STATUS_PENDING));
|
||||||
|
work.setVisibility("private");
|
||||||
|
work.setIsDeleted(0);
|
||||||
|
work.setIsRecommended(false);
|
||||||
|
work.setViewCount(0);
|
||||||
|
work.setLikeCount(0);
|
||||||
|
work.setFavoriteCount(0);
|
||||||
|
work.setCommentCount(0);
|
||||||
|
work.setShareCount(0);
|
||||||
|
work.setProgress(LeaiUtil.toInt(remoteData.get("progress"), 0));
|
||||||
|
work.setProgressMessage(LeaiUtil.toString(remoteData.get("progressMessage"), null));
|
||||||
|
work.setStyle(LeaiUtil.toString(remoteData.get("style"), null));
|
||||||
|
work.setAuthorName(LeaiUtil.toString(remoteData.get("author"), null));
|
||||||
|
work.setFailReason(LeaiUtil.toString(remoteData.get("failReason"), null));
|
||||||
|
work.setCreateTime(LocalDateTime.now());
|
||||||
|
work.setModifyTime(LocalDateTime.now());
|
||||||
|
|
||||||
|
// 设置封面图
|
||||||
|
Object coverUrl = remoteData.get("coverUrl");
|
||||||
|
if (coverUrl == null) coverUrl = remoteData.get("cover_url");
|
||||||
|
if (coverUrl != null) work.setCoverUrl(coverUrl.toString());
|
||||||
|
|
||||||
|
// 设置原始图片
|
||||||
|
Object originalImageUrl = remoteData.get("originalImageUrl");
|
||||||
|
if (originalImageUrl == null) originalImageUrl = remoteData.get("original_image_url");
|
||||||
|
if (originalImageUrl != null) work.setOriginalImageUrl(originalImageUrl.toString());
|
||||||
|
|
||||||
|
// 通过手机号查找用户ID(多租户场景)
|
||||||
|
if (phone != null && work.getUserId() == null) {
|
||||||
|
Long userId = findUserIdByPhone(phone);
|
||||||
|
if (userId != null) {
|
||||||
|
work.setUserId(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ugcWorkMapper.insert(work);
|
||||||
|
|
||||||
|
// 如果有 pageList 且 status >= 3,保存页面数据
|
||||||
|
if (work.getStatus() != null && work.getStatus() >= LeaiApiClient.STATUS_COMPLETED) {
|
||||||
|
savePageList(work.getId(), remoteData.get("pageList"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新失败状态
|
||||||
|
*/
|
||||||
|
private void updateFailed(UgcWork work, Map<String, Object> remoteData) {
|
||||||
|
LambdaUpdateWrapper<UgcWork> wrapper = new LambdaUpdateWrapper<>();
|
||||||
|
wrapper.eq(UgcWork::getId, work.getId())
|
||||||
|
.set(UgcWork::getStatus, LeaiApiClient.STATUS_FAILED)
|
||||||
|
.set(UgcWork::getFailReason, LeaiUtil.toString(remoteData.get("failReason"), "未知错误"))
|
||||||
|
.set(UgcWork::getModifyTime, LocalDateTime.now());
|
||||||
|
ugcWorkMapper.update(null, wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新处理中状态(进度变化)
|
||||||
|
*/
|
||||||
|
private void updateProcessing(UgcWork work, Map<String, Object> remoteData) {
|
||||||
|
LambdaUpdateWrapper<UgcWork> wrapper = new LambdaUpdateWrapper<>();
|
||||||
|
wrapper.eq(UgcWork::getId, work.getId())
|
||||||
|
.set(UgcWork::getStatus, LeaiApiClient.STATUS_PROCESSING);
|
||||||
|
|
||||||
|
if (remoteData.containsKey("progress")) {
|
||||||
|
wrapper.set(UgcWork::getProgress, LeaiUtil.toInt(remoteData.get("progress"), 0));
|
||||||
|
}
|
||||||
|
if (remoteData.containsKey("progressMessage")) {
|
||||||
|
wrapper.set(UgcWork::getProgressMessage, LeaiUtil.toString(remoteData.get("progressMessage"), null));
|
||||||
|
}
|
||||||
|
wrapper.set(UgcWork::getModifyTime, LocalDateTime.now());
|
||||||
|
|
||||||
|
ugcWorkMapper.update(null, wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态前进:全量更新
|
||||||
|
*/
|
||||||
|
private void updateStatusForward(UgcWork work, Map<String, Object> remoteData, int remoteStatus) {
|
||||||
|
// CAS 乐观锁:确保并发安全,只有当前 status < remoteStatus 时才更新
|
||||||
|
LambdaUpdateWrapper<UgcWork> wrapper = new LambdaUpdateWrapper<>();
|
||||||
|
wrapper.eq(UgcWork::getId, work.getId())
|
||||||
|
.lt(UgcWork::getStatus, remoteStatus)
|
||||||
|
.set(UgcWork::getStatus, remoteStatus);
|
||||||
|
|
||||||
|
// 更新可变字段
|
||||||
|
if (remoteData.containsKey("title")) {
|
||||||
|
wrapper.set(UgcWork::getTitle, LeaiUtil.toString(remoteData.get("title"), null));
|
||||||
|
}
|
||||||
|
if (remoteData.containsKey("progress")) {
|
||||||
|
wrapper.set(UgcWork::getProgress, LeaiUtil.toInt(remoteData.get("progress"), 0));
|
||||||
|
}
|
||||||
|
if (remoteData.containsKey("progressMessage")) {
|
||||||
|
wrapper.set(UgcWork::getProgressMessage, LeaiUtil.toString(remoteData.get("progressMessage"), null));
|
||||||
|
}
|
||||||
|
if (remoteData.containsKey("style")) {
|
||||||
|
wrapper.set(UgcWork::getStyle, LeaiUtil.toString(remoteData.get("style"), null));
|
||||||
|
}
|
||||||
|
if (remoteData.containsKey("author")) {
|
||||||
|
wrapper.set(UgcWork::getAuthorName, LeaiUtil.toString(remoteData.get("author"), null));
|
||||||
|
}
|
||||||
|
if (remoteData.containsKey("failReason")) {
|
||||||
|
wrapper.set(UgcWork::getFailReason, LeaiUtil.toString(remoteData.get("failReason"), null));
|
||||||
|
}
|
||||||
|
Object coverUrl = remoteData.get("coverUrl");
|
||||||
|
if (coverUrl == null) coverUrl = remoteData.get("cover_url");
|
||||||
|
if (coverUrl != null) {
|
||||||
|
wrapper.set(UgcWork::getCoverUrl, coverUrl.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper.set(UgcWork::getModifyTime, LocalDateTime.now());
|
||||||
|
|
||||||
|
int updated = ugcWorkMapper.update(null, wrapper);
|
||||||
|
if (updated == 0) {
|
||||||
|
log.warn("CAS乐观锁冲突,跳过更新: remoteWorkId={}, remoteStatus={}", work.getRemoteWorkId(), remoteStatus);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// status >= 3 时保存 pageList
|
||||||
|
if (remoteStatus >= LeaiApiClient.STATUS_COMPLETED) {
|
||||||
|
savePageList(work.getId(), remoteData.get("pageList"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存作品页面数据
|
||||||
|
* status=3(COMPLETED): pageList 包含 imageUrl
|
||||||
|
* status=5(DUBBED): pageList 中 audioUrl 已填充
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private void savePageList(Long workId, Object pageListObj) {
|
||||||
|
if (pageListObj == null) return;
|
||||||
|
|
||||||
|
List<Map<String, Object>> pageList;
|
||||||
|
if (pageListObj instanceof List) {
|
||||||
|
pageList = (List<Map<String, Object>>) pageListObj;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageList.isEmpty()) return;
|
||||||
|
|
||||||
|
// 先删除旧页面数据
|
||||||
|
LambdaQueryWrapper<UgcWorkPage> deleteWrapper = new LambdaQueryWrapper<>();
|
||||||
|
deleteWrapper.eq(UgcWorkPage::getWorkId, workId);
|
||||||
|
ugcWorkPageMapper.delete(deleteWrapper);
|
||||||
|
|
||||||
|
// 插入新页面数据
|
||||||
|
for (int i = 0; i < pageList.size(); i++) {
|
||||||
|
Map<String, Object> pageData = pageList.get(i);
|
||||||
|
UgcWorkPage page = new UgcWorkPage();
|
||||||
|
page.setWorkId(workId);
|
||||||
|
page.setPageNo(i + 1);
|
||||||
|
page.setImageUrl(LeaiUtil.toString(pageData.get("imageUrl"), null));
|
||||||
|
page.setText(LeaiUtil.toString(pageData.get("text"), null));
|
||||||
|
page.setAudioUrl(LeaiUtil.toString(pageData.get("audioUrl"), null));
|
||||||
|
ugcWorkPageMapper.insert(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("[乐读派] 保存作品页面数据: workId={}, 页数={}", workId, pageList.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过 remoteWorkId 查找本地作品
|
||||||
|
*/
|
||||||
|
private UgcWork findByRemoteWorkId(String remoteWorkId) {
|
||||||
|
LambdaQueryWrapper<UgcWork> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(UgcWork::getRemoteWorkId, remoteWorkId)
|
||||||
|
.eq(UgcWork::getIsDeleted, 0);
|
||||||
|
return ugcWorkMapper.selectOne(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过手机号查找用户ID
|
||||||
|
* 多租户场景:需要确定租户后查找
|
||||||
|
*/
|
||||||
|
private Long findUserIdByPhone(String phone) {
|
||||||
|
// TODO: 如果需要按租户隔离,需要传入 orgId 查找租户再查找用户
|
||||||
|
// 当前简化处理:直接通过手机号查用户
|
||||||
|
return null; // 暂时不自动关联用户,后续通过 phone + orgId 查询
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
package com.competition.modules.leai.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.competition.modules.leai.entity.LeaiWebhookEvent;
|
||||||
|
import com.competition.modules.leai.mapper.LeaiWebhookEventMapper;
|
||||||
|
import com.competition.modules.leai.service.ILeaiWebhookEventService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 乐读派 Webhook 事件 Service 实现
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class LeaiWebhookEventServiceImpl extends ServiceImpl<LeaiWebhookEventMapper, LeaiWebhookEvent>
|
||||||
|
implements ILeaiWebhookEventService {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean existsByEventId(String eventId) {
|
||||||
|
LambdaQueryWrapper<LeaiWebhookEvent> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(LeaiWebhookEvent::getEventId, eventId);
|
||||||
|
return count(wrapper) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void saveEvent(String eventId, String eventType, String remoteWorkId, Object payload) {
|
||||||
|
LeaiWebhookEvent event = new LeaiWebhookEvent();
|
||||||
|
event.setEventId(eventId);
|
||||||
|
event.setEventType(eventType);
|
||||||
|
event.setRemoteWorkId(remoteWorkId);
|
||||||
|
event.setPayload(payload);
|
||||||
|
event.setProcessed(1);
|
||||||
|
event.setCreateTime(LocalDateTime.now());
|
||||||
|
save(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,77 @@
|
|||||||
|
package com.competition.modules.leai.task;
|
||||||
|
|
||||||
|
import com.competition.modules.leai.service.ILeaiSyncService;
|
||||||
|
import com.competition.modules.leai.service.LeaiApiClient;
|
||||||
|
import com.competition.modules.leai.util.LeaiUtil;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* B3 定时对账任务
|
||||||
|
* 每 30 分钟调用 B3 接口对账,补偿 Webhook 遗漏
|
||||||
|
* <p>
|
||||||
|
* 查询范围:最近 2 小时内更新的作品(覆盖 2 个对账周期,确保不遗漏边界数据)
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class LeaiReconcileTask {
|
||||||
|
|
||||||
|
private final LeaiApiClient leaiApiClient;
|
||||||
|
private final ILeaiSyncService leaiSyncService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每30分钟执行一次,初始延迟60秒
|
||||||
|
*/
|
||||||
|
@Scheduled(fixedRate = 30 * 60 * 1000, initialDelay = 60 * 1000)
|
||||||
|
public void reconcile() {
|
||||||
|
log.info("[B3对账] 开始执行...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
String updatedAfter = LeaiApiClient.getUtcTwoHoursAgoIso();
|
||||||
|
|
||||||
|
List<Map<String, Object>> works = leaiApiClient.queryWorks(updatedAfter);
|
||||||
|
if (works.isEmpty()) {
|
||||||
|
log.info("[B3对账] 未查询到变更作品");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int synced = 0;
|
||||||
|
for (Map<String, Object> work : works) {
|
||||||
|
String workId = LeaiUtil.toString(work.get("workId"), null);
|
||||||
|
if (workId == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试调 B2 获取完整数据
|
||||||
|
Map<String, Object> fullData = leaiApiClient.fetchWorkDetail(workId);
|
||||||
|
if (fullData != null) {
|
||||||
|
try {
|
||||||
|
leaiSyncService.syncWork(workId, fullData, "B3对账");
|
||||||
|
synced++;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[B3对账] 同步失败: workId={}", workId, e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// B2 失败时用 B3 摘要数据做简易同步
|
||||||
|
try {
|
||||||
|
leaiSyncService.syncWork(workId, work, "B3对账(摘要)");
|
||||||
|
synced++;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[B3对账] 摘要同步失败: workId={}", workId, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("[B3对账] 完成: 检查 {} 个作品, 同步 {} 个", works.size(), synced);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[B3对账] 执行异常", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
package com.competition.modules.leai.util;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 乐读派模块共享工具类
|
||||||
|
* 提取多处重复使用的类型转换方法
|
||||||
|
*/
|
||||||
|
public final class LeaiUtil {
|
||||||
|
|
||||||
|
private LeaiUtil() {
|
||||||
|
// 工具类禁止实例化
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安全转换为 int
|
||||||
|
*
|
||||||
|
* @param obj 待转换对象
|
||||||
|
* @param defaultVal 默认值
|
||||||
|
* @return 转换结果
|
||||||
|
*/
|
||||||
|
public static int toInt(Object obj, int defaultVal) {
|
||||||
|
if (obj == null) {
|
||||||
|
return defaultVal;
|
||||||
|
}
|
||||||
|
if (obj instanceof Number) {
|
||||||
|
return ((Number) obj).intValue();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(obj.toString());
|
||||||
|
} catch (Exception e) {
|
||||||
|
return defaultVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安全转换为 String
|
||||||
|
*
|
||||||
|
* @param obj 待转换对象
|
||||||
|
* @param defaultVal 默认值
|
||||||
|
* @return 转换结果
|
||||||
|
*/
|
||||||
|
public static String toString(Object obj, String defaultVal) {
|
||||||
|
return obj != null ? obj.toString() : defaultVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package com.competition.modules.leai.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 乐读派 Token 响应 VO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "乐读派 Token 信息")
|
||||||
|
public class LeaiTokenVO {
|
||||||
|
|
||||||
|
@Schema(description = "Session Token")
|
||||||
|
private String token;
|
||||||
|
|
||||||
|
@Schema(description = "机构ID(对应本项目的租户 code,即 tenant_code)")
|
||||||
|
private String orgId;
|
||||||
|
|
||||||
|
@Schema(description = "H5 前端地址")
|
||||||
|
private String h5Url;
|
||||||
|
|
||||||
|
@Schema(description = "用户手机号")
|
||||||
|
private String phone;
|
||||||
|
}
|
||||||
@ -44,11 +44,20 @@ public class PublicActivityController {
|
|||||||
|
|
||||||
@GetMapping("/{id}/my-registration")
|
@GetMapping("/{id}/my-registration")
|
||||||
@Operation(summary = "查询我的报名信息")
|
@Operation(summary = "查询我的报名信息")
|
||||||
public Result<BizContestRegistration> getMyRegistration(@PathVariable Long id) {
|
public Result<Map<String, Object>> getMyRegistration(@PathVariable Long id) {
|
||||||
Long userId = SecurityUtil.getCurrentUserId();
|
Long userId = SecurityUtil.getCurrentUserId();
|
||||||
return Result.success(publicActivityService.getMyRegistration(id, userId));
|
return Result.success(publicActivityService.getMyRegistration(id, userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/mine/registrations")
|
||||||
|
@Operation(summary = "我的报名列表")
|
||||||
|
public Result<Map<String, Object>> getMyRegistrations(
|
||||||
|
@RequestParam(defaultValue = "1") int page,
|
||||||
|
@RequestParam(defaultValue = "10") int pageSize) {
|
||||||
|
Long userId = SecurityUtil.getCurrentUserId();
|
||||||
|
return Result.success(publicActivityService.getMyRegistrations(userId, page, pageSize));
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/{id}/register")
|
@PostMapping("/{id}/register")
|
||||||
@Operation(summary = "报名活动")
|
@Operation(summary = "报名活动")
|
||||||
public Result<BizContestRegistration> register(
|
public Result<BizContestRegistration> register(
|
||||||
|
|||||||
@ -12,6 +12,12 @@ import com.competition.modules.biz.contest.mapper.ContestMapper;
|
|||||||
import com.competition.modules.biz.contest.mapper.ContestRegistrationMapper;
|
import com.competition.modules.biz.contest.mapper.ContestRegistrationMapper;
|
||||||
import com.competition.modules.biz.contest.mapper.ContestWorkMapper;
|
import com.competition.modules.biz.contest.mapper.ContestWorkMapper;
|
||||||
import com.competition.modules.pub.dto.PublicRegisterActivityDto;
|
import com.competition.modules.pub.dto.PublicRegisterActivityDto;
|
||||||
|
import com.competition.modules.ugc.entity.UgcWork;
|
||||||
|
import com.competition.modules.ugc.entity.UgcWorkPage;
|
||||||
|
import com.competition.modules.ugc.mapper.UgcWorkMapper;
|
||||||
|
import com.competition.modules.ugc.mapper.UgcWorkPageMapper;
|
||||||
|
import com.competition.modules.user.entity.UserChild;
|
||||||
|
import com.competition.modules.user.mapper.UserChildMapper;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@ -29,6 +35,9 @@ public class PublicActivityService {
|
|||||||
private final ContestMapper contestMapper;
|
private final ContestMapper contestMapper;
|
||||||
private final ContestRegistrationMapper contestRegistrationMapper;
|
private final ContestRegistrationMapper contestRegistrationMapper;
|
||||||
private final ContestWorkMapper contestWorkMapper;
|
private final ContestWorkMapper contestWorkMapper;
|
||||||
|
private final UserChildMapper userChildMapper;
|
||||||
|
private final UgcWorkMapper ugcWorkMapper;
|
||||||
|
private final UgcWorkPageMapper ugcWorkPageMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 活动列表(公开)
|
* 活动列表(公开)
|
||||||
@ -64,6 +73,7 @@ public class PublicActivityService {
|
|||||||
result.put("contestType", contest.getContestType());
|
result.put("contestType", contest.getContestType());
|
||||||
result.put("contestState", contest.getContestState());
|
result.put("contestState", contest.getContestState());
|
||||||
result.put("status", contest.getStatus());
|
result.put("status", contest.getStatus());
|
||||||
|
result.put("visibility", contest.getVisibility());
|
||||||
result.put("startTime", contest.getStartTime());
|
result.put("startTime", contest.getStartTime());
|
||||||
result.put("endTime", contest.getEndTime());
|
result.put("endTime", contest.getEndTime());
|
||||||
result.put("coverUrl", contest.getCoverUrl());
|
result.put("coverUrl", contest.getCoverUrl());
|
||||||
@ -81,6 +91,9 @@ public class PublicActivityService {
|
|||||||
result.put("registerState", contest.getRegisterState());
|
result.put("registerState", contest.getRegisterState());
|
||||||
result.put("submitStartTime", contest.getSubmitStartTime());
|
result.put("submitStartTime", contest.getSubmitStartTime());
|
||||||
result.put("submitEndTime", contest.getSubmitEndTime());
|
result.put("submitEndTime", contest.getSubmitEndTime());
|
||||||
|
result.put("submitRule", contest.getSubmitRule());
|
||||||
|
result.put("reviewStartTime", contest.getReviewStartTime());
|
||||||
|
result.put("reviewEndTime", contest.getReviewEndTime());
|
||||||
result.put("workType", contest.getWorkType());
|
result.put("workType", contest.getWorkType());
|
||||||
result.put("workRequirement", contest.getWorkRequirement());
|
result.put("workRequirement", contest.getWorkRequirement());
|
||||||
result.put("resultState", contest.getResultState());
|
result.put("resultState", contest.getResultState());
|
||||||
@ -89,14 +102,167 @@ public class PublicActivityService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询当前用户的报名信息
|
* 查询当前用户的报名信息(包含作品提交状态)
|
||||||
*/
|
*/
|
||||||
public BizContestRegistration getMyRegistration(Long contestId, Long userId) {
|
public Map<String, Object> getMyRegistration(Long contestId, Long userId) {
|
||||||
return contestRegistrationMapper.selectOne(
|
BizContestRegistration reg = contestRegistrationMapper.selectOne(
|
||||||
new LambdaQueryWrapper<BizContestRegistration>()
|
new LambdaQueryWrapper<BizContestRegistration>()
|
||||||
.eq(BizContestRegistration::getContestId, contestId)
|
.eq(BizContestRegistration::getContestId, contestId)
|
||||||
.eq(BizContestRegistration::getUserId, userId)
|
.eq(BizContestRegistration::getUserId, userId)
|
||||||
.last("LIMIT 1"));
|
.last("LIMIT 1"));
|
||||||
|
|
||||||
|
if (reg == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
result.put("id", reg.getId());
|
||||||
|
result.put("contestId", reg.getContestId());
|
||||||
|
result.put("userId", reg.getUserId());
|
||||||
|
result.put("registrationType", reg.getRegistrationType());
|
||||||
|
result.put("registrationState", reg.getRegistrationState());
|
||||||
|
result.put("registrationTime", reg.getRegistrationTime());
|
||||||
|
|
||||||
|
// 查询是否已提交作品(只统计最新版本)
|
||||||
|
Long workCount = contestWorkMapper.selectCount(
|
||||||
|
new LambdaQueryWrapper<BizContestWork>()
|
||||||
|
.eq(BizContestWork::getRegistrationId, reg.getId())
|
||||||
|
.eq(BizContestWork::getIsLatest, true));
|
||||||
|
result.put("hasSubmittedWork", workCount > 0);
|
||||||
|
result.put("workCount", workCount);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 我的报名列表
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getMyRegistrations(Long userId, int page, int pageSize) {
|
||||||
|
// 查询报名列表
|
||||||
|
LambdaQueryWrapper<BizContestRegistration> regWrapper = new LambdaQueryWrapper<>();
|
||||||
|
regWrapper.eq(BizContestRegistration::getUserId, userId)
|
||||||
|
.orderByDesc(BizContestRegistration::getRegistrationTime);
|
||||||
|
|
||||||
|
IPage<BizContestRegistration> regPage = contestRegistrationMapper.selectPage(new Page<>(page, pageSize), regWrapper);
|
||||||
|
|
||||||
|
// 获取所有 contestId
|
||||||
|
List<Long> contestIds = regPage.getRecords().stream()
|
||||||
|
.map(BizContestRegistration::getContestId)
|
||||||
|
.distinct()
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
// 批量查询活动信息
|
||||||
|
Map<Long, BizContest> contestMap = new HashMap<>();
|
||||||
|
if (!contestIds.isEmpty()) {
|
||||||
|
List<BizContest> contests = contestMapper.selectBatchIds(contestIds);
|
||||||
|
for (BizContest contest : contests) {
|
||||||
|
contestMap.put(contest.getId(), contest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询所有报名对应的作品
|
||||||
|
List<BizContestRegistration> registrations = regPage.getRecords();
|
||||||
|
List<Long> registrationIds = registrations.stream()
|
||||||
|
.map(BizContestRegistration::getId)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
Map<Long, List<BizContestWork>> worksMap = new HashMap<>();
|
||||||
|
if (!registrationIds.isEmpty()) {
|
||||||
|
LambdaQueryWrapper<BizContestWork> workWrapper = new LambdaQueryWrapper<>();
|
||||||
|
workWrapper.in(BizContestWork::getRegistrationId, registrationIds);
|
||||||
|
List<BizContestWork> works = contestWorkMapper.selectList(workWrapper);
|
||||||
|
for (BizContestWork work : works) {
|
||||||
|
worksMap.computeIfAbsent(work.getRegistrationId(), k -> new ArrayList<>()).add(work);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有 childId
|
||||||
|
List<Long> childIds = registrations.stream()
|
||||||
|
.map(BizContestRegistration::getChildId)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.distinct()
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
// 批量查询子女信息
|
||||||
|
Map<Long, UserChild> childMap = new HashMap<>();
|
||||||
|
if (!childIds.isEmpty()) {
|
||||||
|
List<UserChild> children = userChildMapper.selectBatchIds(childIds);
|
||||||
|
for (UserChild child : children) {
|
||||||
|
childMap.put(child.getId(), child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组装返回数据
|
||||||
|
List<Map<String, Object>> list = new ArrayList<>();
|
||||||
|
for (BizContestRegistration reg : registrations) {
|
||||||
|
Map<String, Object> item = new LinkedHashMap<>();
|
||||||
|
item.put("id", reg.getId());
|
||||||
|
item.put("contestId", reg.getContestId());
|
||||||
|
item.put("userId", reg.getUserId());
|
||||||
|
item.put("registrationType", reg.getRegistrationType());
|
||||||
|
item.put("registrationState", reg.getRegistrationState());
|
||||||
|
item.put("registrationTime", reg.getRegistrationTime());
|
||||||
|
item.put("participantType", reg.getParticipantType());
|
||||||
|
item.put("childId", reg.getChildId());
|
||||||
|
item.put("teamId", reg.getTeamId());
|
||||||
|
|
||||||
|
// 关联子女信息
|
||||||
|
if (reg.getChildId() != null) {
|
||||||
|
UserChild child = childMap.get(reg.getChildId());
|
||||||
|
if (child != null) {
|
||||||
|
Map<String, Object> childInfo = new LinkedHashMap<>();
|
||||||
|
childInfo.put("id", child.getId());
|
||||||
|
childInfo.put("name", child.getName());
|
||||||
|
childInfo.put("gender", child.getGender());
|
||||||
|
childInfo.put("grade", child.getGrade());
|
||||||
|
item.put("child", childInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关联活动信息
|
||||||
|
BizContest contest = contestMap.get(reg.getContestId());
|
||||||
|
if (contest != null) {
|
||||||
|
Map<String, Object> contestInfo = new LinkedHashMap<>();
|
||||||
|
contestInfo.put("id", contest.getId());
|
||||||
|
contestInfo.put("contestName", contest.getContestName());
|
||||||
|
contestInfo.put("contestType", contest.getContestType());
|
||||||
|
contestInfo.put("coverUrl", contest.getCoverUrl());
|
||||||
|
contestInfo.put("posterUrl", contest.getPosterUrl());
|
||||||
|
contestInfo.put("startTime", contest.getStartTime());
|
||||||
|
contestInfo.put("endTime", contest.getEndTime());
|
||||||
|
contestInfo.put("submitStartTime", contest.getSubmitStartTime());
|
||||||
|
contestInfo.put("submitEndTime", contest.getSubmitEndTime());
|
||||||
|
item.put("contest", contestInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关联作品信息
|
||||||
|
List<BizContestWork> works = worksMap.get(reg.getId());
|
||||||
|
if (works != null && !works.isEmpty()) {
|
||||||
|
List<Map<String, Object>> worksList = new ArrayList<>();
|
||||||
|
for (BizContestWork work : works) {
|
||||||
|
Map<String, Object> workInfo = new LinkedHashMap<>();
|
||||||
|
workInfo.put("id", work.getId());
|
||||||
|
workInfo.put("title", work.getTitle());
|
||||||
|
workInfo.put("description", work.getDescription());
|
||||||
|
workInfo.put("previewUrl", work.getPreviewUrl());
|
||||||
|
workInfo.put("previewUrls", work.getPreviewUrls());
|
||||||
|
workInfo.put("attachments", work.getFiles());
|
||||||
|
workInfo.put("createTime", work.getCreateTime());
|
||||||
|
worksList.add(workInfo);
|
||||||
|
}
|
||||||
|
item.put("works", worksList);
|
||||||
|
}
|
||||||
|
|
||||||
|
list.add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
result.put("list", list);
|
||||||
|
result.put("total", regPage.getTotal());
|
||||||
|
result.put("page", regPage.getCurrent());
|
||||||
|
result.put("pageSize", regPage.getSize());
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -104,6 +270,8 @@ public class PublicActivityService {
|
|||||||
*/
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public BizContestRegistration register(Long contestId, Long userId, Long tenantId, PublicRegisterActivityDto dto) {
|
public BizContestRegistration register(Long contestId, Long userId, Long tenantId, PublicRegisterActivityDto dto) {
|
||||||
|
log.info("开始报名活动,contestId: {}, userId: {}, tenantId: {}", contestId, userId, tenantId);
|
||||||
|
|
||||||
// 检查是否已报名
|
// 检查是否已报名
|
||||||
Long existCount = contestRegistrationMapper.selectCount(
|
Long existCount = contestRegistrationMapper.selectCount(
|
||||||
new LambdaQueryWrapper<BizContestRegistration>()
|
new LambdaQueryWrapper<BizContestRegistration>()
|
||||||
@ -125,14 +293,26 @@ public class PublicActivityService {
|
|||||||
BizContestRegistration reg = new BizContestRegistration();
|
BizContestRegistration reg = new BizContestRegistration();
|
||||||
reg.setContestId(contestId);
|
reg.setContestId(contestId);
|
||||||
reg.setUserId(userId);
|
reg.setUserId(userId);
|
||||||
reg.setTenantId(tenantId);
|
// 使用活动的授权租户ID(管理端按租户查询报名数据)
|
||||||
|
if (contest.getContestTenants() != null && !contest.getContestTenants().isEmpty()) {
|
||||||
|
reg.setTenantId(contest.getContestTenants().get(0).longValue());
|
||||||
|
} else {
|
||||||
|
reg.setTenantId(tenantId);
|
||||||
|
}
|
||||||
reg.setRegistrationType(contest.getContestType());
|
reg.setRegistrationType(contest.getContestType());
|
||||||
reg.setParticipantType(dto.getParticipantType() != null ? dto.getParticipantType() : "self");
|
reg.setParticipantType(dto.getParticipantType() != null ? dto.getParticipantType() : "self");
|
||||||
reg.setChildId(dto.getChildId());
|
reg.setChildId(dto.getChildId());
|
||||||
reg.setTeamId(dto.getTeamId());
|
reg.setTeamId(dto.getTeamId());
|
||||||
reg.setRegistrationState(Boolean.TRUE.equals(contest.getRequireAudit()) ? "pending" : "passed");
|
reg.setRegistrationState(Boolean.TRUE.equals(contest.getRequireAudit()) ? "pending" : "passed");
|
||||||
reg.setRegistrationTime(LocalDateTime.now());
|
reg.setRegistrationTime(LocalDateTime.now());
|
||||||
|
// 设置必填字段
|
||||||
|
reg.setRegistrant(userId.intValue());
|
||||||
|
reg.setAccountNo("user_" + userId);
|
||||||
|
reg.setAccountName("user_" + userId);
|
||||||
|
|
||||||
|
log.info("保存报名记录:{}", reg);
|
||||||
contestRegistrationMapper.insert(reg);
|
contestRegistrationMapper.insert(reg);
|
||||||
|
log.info("报名成功,ID: {}", reg.getId());
|
||||||
return reg;
|
return reg;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,30 +321,115 @@ public class PublicActivityService {
|
|||||||
*/
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public BizContestWork submitWork(Long contestId, Long userId, Long tenantId, Map<String, Object> dto) {
|
public BizContestWork submitWork(Long contestId, Long userId, Long tenantId, Map<String, Object> dto) {
|
||||||
// 检查报名状态
|
// 检查报名状态(区分不同状态给出明确提示)
|
||||||
BizContestRegistration reg = contestRegistrationMapper.selectOne(
|
BizContestRegistration anyReg = contestRegistrationMapper.selectOne(
|
||||||
new LambdaQueryWrapper<BizContestRegistration>()
|
new LambdaQueryWrapper<BizContestRegistration>()
|
||||||
.eq(BizContestRegistration::getContestId, contestId)
|
.eq(BizContestRegistration::getContestId, contestId)
|
||||||
.eq(BizContestRegistration::getUserId, userId)
|
.eq(BizContestRegistration::getUserId, userId)
|
||||||
.eq(BizContestRegistration::getRegistrationState, "passed"));
|
.last("LIMIT 1"));
|
||||||
if (reg == null) {
|
if (anyReg == null) {
|
||||||
throw new BusinessException(400, "未报名或报名未通过");
|
throw new BusinessException(400, "您尚未报名该活动,请先报名");
|
||||||
}
|
}
|
||||||
|
if (!"passed".equals(anyReg.getRegistrationState())) {
|
||||||
|
String state = anyReg.getRegistrationState();
|
||||||
|
String msg = switch (state) {
|
||||||
|
case "pending" -> "报名审核中,请等待审核通过后再提交作品";
|
||||||
|
case "rejected" -> "报名已被拒绝,无法提交作品";
|
||||||
|
case "withdrawn" -> "报名已撤回,请重新报名";
|
||||||
|
default -> "报名状态异常(" + state + "),无法提交作品";
|
||||||
|
};
|
||||||
|
throw new BusinessException(400, msg);
|
||||||
|
}
|
||||||
|
BizContestRegistration reg = anyReg;
|
||||||
|
|
||||||
|
// 查询活动提交规则
|
||||||
|
BizContest contest = contestMapper.selectById(contestId);
|
||||||
|
String submitRule = contest != null ? contest.getSubmitRule() : "once";
|
||||||
|
|
||||||
|
// 查询已有作品
|
||||||
|
BizContestWork existingWork = contestWorkMapper.selectOne(
|
||||||
|
new LambdaQueryWrapper<BizContestWork>()
|
||||||
|
.eq(BizContestWork::getContestId, contestId)
|
||||||
|
.eq(BizContestWork::getRegistrationId, reg.getId())
|
||||||
|
.eq(BizContestWork::getIsLatest, true)
|
||||||
|
.orderByDesc(BizContestWork::getVersion)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
|
||||||
|
if (existingWork != null) {
|
||||||
|
if ("once".equals(submitRule)) {
|
||||||
|
throw new BusinessException(400, "该活动仅允许提交一次作品");
|
||||||
|
}
|
||||||
|
// resubmit 模式:将旧作品标记为非最新
|
||||||
|
existingWork.setIsLatest(false);
|
||||||
|
contestWorkMapper.updateById(existingWork);
|
||||||
|
}
|
||||||
|
|
||||||
|
int nextVersion = (existingWork != null) ? existingWork.getVersion() + 1 : 1;
|
||||||
|
|
||||||
BizContestWork work = new BizContestWork();
|
BizContestWork work = new BizContestWork();
|
||||||
work.setContestId(contestId);
|
work.setContestId(contestId);
|
||||||
work.setRegistrationId(reg.getId());
|
work.setRegistrationId(reg.getId());
|
||||||
work.setTenantId(tenantId);
|
// 使用报名记录的租户ID(已在 register 时设置为活动的租户,确保管理端可见)
|
||||||
work.setTitle((String) dto.get("title"));
|
work.setTenantId(reg.getTenantId());
|
||||||
work.setDescription((String) dto.get("description"));
|
|
||||||
work.setFiles(dto.get("files"));
|
|
||||||
work.setSubmitterUserId(userId);
|
work.setSubmitterUserId(userId);
|
||||||
work.setStatus("submitted");
|
work.setStatus("submitted");
|
||||||
work.setSubmitTime(LocalDateTime.now());
|
work.setSubmitTime(LocalDateTime.now());
|
||||||
work.setVersion(1);
|
work.setVersion(nextVersion);
|
||||||
work.setIsLatest(true);
|
work.setIsLatest(true);
|
||||||
|
|
||||||
|
// 从作品库选择作品提交(快照复制)
|
||||||
if (dto.get("userWorkId") != null) {
|
if (dto.get("userWorkId") != null) {
|
||||||
work.setUserWorkId(Long.valueOf(dto.get("userWorkId").toString()));
|
Long userWorkId = Long.valueOf(dto.get("userWorkId").toString());
|
||||||
|
work.setUserWorkId(userWorkId);
|
||||||
|
|
||||||
|
// 查询用户作品
|
||||||
|
UgcWork ugcWork = ugcWorkMapper.selectById(userWorkId);
|
||||||
|
if (ugcWork == null || ugcWork.getIsDeleted() == 1) {
|
||||||
|
throw new BusinessException(404, "作品不存在");
|
||||||
|
}
|
||||||
|
if (!ugcWork.getUserId().equals(userId)) {
|
||||||
|
throw new BusinessException(403, "无权使用该作品");
|
||||||
|
}
|
||||||
|
if ("rejected".equals(ugcWork.getStatus()) || "taken_down".equals(ugcWork.getStatus())) {
|
||||||
|
throw new BusinessException(400, "该作品状态不可提交");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询绘本分页
|
||||||
|
List<UgcWorkPage> pages = ugcWorkPageMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<UgcWorkPage>()
|
||||||
|
.eq(UgcWorkPage::getWorkId, userWorkId)
|
||||||
|
.orderByAsc(UgcWorkPage::getPageNo));
|
||||||
|
|
||||||
|
// 复制快照字段
|
||||||
|
work.setTitle(ugcWork.getTitle());
|
||||||
|
work.setDescription(ugcWork.getDescription());
|
||||||
|
work.setPreviewUrl(ugcWork.getCoverUrl());
|
||||||
|
work.setAiModelMeta(ugcWork.getAiMeta());
|
||||||
|
|
||||||
|
// previewUrls = 所有页面图片
|
||||||
|
List<String> previewUrls = pages.stream()
|
||||||
|
.map(UgcWorkPage::getImageUrl)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.toList();
|
||||||
|
work.setPreviewUrls(previewUrls);
|
||||||
|
|
||||||
|
// files = 分页完整快照
|
||||||
|
List<Map<String, Object>> filesSnapshot = pages.stream()
|
||||||
|
.map(p -> {
|
||||||
|
Map<String, Object> m = new LinkedHashMap<>();
|
||||||
|
m.put("pageNo", p.getPageNo());
|
||||||
|
m.put("imageUrl", p.getImageUrl());
|
||||||
|
m.put("text", p.getText());
|
||||||
|
m.put("audioUrl", p.getAudioUrl());
|
||||||
|
return m;
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
work.setFiles(filesSnapshot);
|
||||||
|
} else {
|
||||||
|
// 旧逻辑:直接上传
|
||||||
|
work.setTitle((String) dto.get("title"));
|
||||||
|
work.setDescription((String) dto.get("description"));
|
||||||
|
work.setFiles(dto.get("files"));
|
||||||
}
|
}
|
||||||
contestWorkMapper.insert(work);
|
contestWorkMapper.insert(work);
|
||||||
return work;
|
return work;
|
||||||
|
|||||||
@ -101,7 +101,7 @@ public class PublicContentReviewService {
|
|||||||
if (work == null) {
|
if (work == null) {
|
||||||
throw new BusinessException(404, "作品不存在");
|
throw new BusinessException(404, "作品不存在");
|
||||||
}
|
}
|
||||||
work.setStatus("published");
|
work.setStatus(3); // 3=COMPLETED/PUBLISHED
|
||||||
work.setReviewNote(note);
|
work.setReviewNote(note);
|
||||||
work.setReviewerId(operatorId);
|
work.setReviewerId(operatorId);
|
||||||
work.setReviewTime(LocalDateTime.now());
|
work.setReviewTime(LocalDateTime.now());
|
||||||
@ -120,7 +120,7 @@ public class PublicContentReviewService {
|
|||||||
if (work == null) {
|
if (work == null) {
|
||||||
throw new BusinessException(404, "作品不存在");
|
throw new BusinessException(404, "作品不存在");
|
||||||
}
|
}
|
||||||
work.setStatus("rejected");
|
work.setStatus(-1); // -1=FAILED/REJECTED
|
||||||
work.setReviewNote(note);
|
work.setReviewNote(note);
|
||||||
work.setReviewerId(operatorId);
|
work.setReviewerId(operatorId);
|
||||||
work.setReviewTime(LocalDateTime.now());
|
work.setReviewTime(LocalDateTime.now());
|
||||||
@ -158,7 +158,7 @@ public class PublicContentReviewService {
|
|||||||
if (work == null) {
|
if (work == null) {
|
||||||
throw new BusinessException(404, "作品不存在");
|
throw new BusinessException(404, "作品不存在");
|
||||||
}
|
}
|
||||||
work.setStatus("pending_review");
|
work.setStatus(1); // 1=PENDING
|
||||||
work.setModifyTime(LocalDateTime.now());
|
work.setModifyTime(LocalDateTime.now());
|
||||||
ugcWorkMapper.updateById(work);
|
ugcWorkMapper.updateById(work);
|
||||||
createLog(id, "revoke", null, null, operatorId);
|
createLog(id, "revoke", null, null, operatorId);
|
||||||
@ -173,7 +173,7 @@ public class PublicContentReviewService {
|
|||||||
if (work == null) {
|
if (work == null) {
|
||||||
throw new BusinessException(404, "作品不存在");
|
throw new BusinessException(404, "作品不存在");
|
||||||
}
|
}
|
||||||
work.setStatus("taken_down");
|
work.setStatus(-2); // -2=TAKEN_DOWN
|
||||||
work.setModifyTime(LocalDateTime.now());
|
work.setModifyTime(LocalDateTime.now());
|
||||||
ugcWorkMapper.updateById(work);
|
ugcWorkMapper.updateById(work);
|
||||||
createLog(id, "takedown", reason, null, operatorId);
|
createLog(id, "takedown", reason, null, operatorId);
|
||||||
@ -188,7 +188,7 @@ public class PublicContentReviewService {
|
|||||||
if (work == null) {
|
if (work == null) {
|
||||||
throw new BusinessException(404, "作品不存在");
|
throw new BusinessException(404, "作品不存在");
|
||||||
}
|
}
|
||||||
work.setStatus("published");
|
work.setStatus(3); // 3=COMPLETED/PUBLISHED
|
||||||
work.setModifyTime(LocalDateTime.now());
|
work.setModifyTime(LocalDateTime.now());
|
||||||
ugcWorkMapper.updateById(work);
|
ugcWorkMapper.updateById(work);
|
||||||
createLog(id, "restore", null, null, operatorId);
|
createLog(id, "restore", null, null, operatorId);
|
||||||
|
|||||||
@ -6,7 +6,9 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|||||||
import com.competition.common.exception.BusinessException;
|
import com.competition.common.exception.BusinessException;
|
||||||
import com.competition.common.result.PageResult;
|
import com.competition.common.result.PageResult;
|
||||||
import com.competition.modules.ugc.entity.UgcWork;
|
import com.competition.modules.ugc.entity.UgcWork;
|
||||||
|
import com.competition.modules.ugc.entity.UgcWorkPage;
|
||||||
import com.competition.modules.ugc.mapper.UgcWorkMapper;
|
import com.competition.modules.ugc.mapper.UgcWorkMapper;
|
||||||
|
import com.competition.modules.ugc.mapper.UgcWorkPageMapper;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@ -20,14 +22,16 @@ import java.util.*;
|
|||||||
public class PublicCreationService {
|
public class PublicCreationService {
|
||||||
|
|
||||||
private final UgcWorkMapper ugcWorkMapper;
|
private final UgcWorkMapper ugcWorkMapper;
|
||||||
|
private final UgcWorkPageMapper ugcWorkPageMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提交 AI 创作
|
* 提交 AI 创作(保留但降级为辅助接口)
|
||||||
*/
|
*/
|
||||||
public UgcWork submit(Long userId, String originalImageUrl, String voiceInputUrl, String textInput) {
|
public UgcWork submit(Long userId, String originalImageUrl, String voiceInputUrl, String textInput) {
|
||||||
UgcWork work = new UgcWork();
|
UgcWork work = new UgcWork();
|
||||||
work.setUserId(userId);
|
work.setUserId(userId);
|
||||||
work.setStatus("draft");
|
work.setTitle("未命名作品");
|
||||||
|
work.setStatus(0); // DRAFT
|
||||||
work.setOriginalImageUrl(originalImageUrl);
|
work.setOriginalImageUrl(originalImageUrl);
|
||||||
work.setVoiceInputUrl(voiceInputUrl);
|
work.setVoiceInputUrl(voiceInputUrl);
|
||||||
work.setTextInput(textInput);
|
work.setTextInput(textInput);
|
||||||
@ -47,6 +51,7 @@ public class PublicCreationService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取创作状态
|
* 获取创作状态
|
||||||
|
* 返回 INT 类型的 status + progress + progressMessage
|
||||||
*/
|
*/
|
||||||
public Map<String, Object> getStatus(Long id, Long userId) {
|
public Map<String, Object> getStatus(Long id, Long userId) {
|
||||||
UgcWork work = ugcWorkMapper.selectById(id);
|
UgcWork work = ugcWorkMapper.selectById(id);
|
||||||
@ -56,29 +61,48 @@ public class PublicCreationService {
|
|||||||
Map<String, Object> result = new LinkedHashMap<>();
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
result.put("id", work.getId());
|
result.put("id", work.getId());
|
||||||
result.put("status", work.getStatus());
|
result.put("status", work.getStatus());
|
||||||
result.put("aiMeta", work.getAiMeta());
|
result.put("progress", work.getProgress());
|
||||||
|
result.put("progressMessage", work.getProgressMessage());
|
||||||
|
result.put("remoteWorkId", work.getRemoteWorkId());
|
||||||
|
result.put("title", work.getTitle());
|
||||||
|
result.put("coverUrl", work.getCoverUrl());
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取创作结果
|
* 获取创作结果(包含 pageList 的完整数据)
|
||||||
*/
|
*/
|
||||||
public Map<String, Object> getResult(Long id, Long userId) {
|
public Map<String, Object> getResult(Long id, Long userId) {
|
||||||
UgcWork work = ugcWorkMapper.selectById(id);
|
UgcWork work = ugcWorkMapper.selectById(id);
|
||||||
if (work == null || !work.getUserId().equals(userId)) {
|
if (work == null || !work.getUserId().equals(userId)) {
|
||||||
throw new BusinessException(404, "创作记录不存在");
|
throw new BusinessException(404, "创作记录不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, Object> result = new LinkedHashMap<>();
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
result.put("id", work.getId());
|
result.put("id", work.getId());
|
||||||
result.put("title", work.getTitle());
|
result.put("title", work.getTitle());
|
||||||
result.put("coverUrl", work.getCoverUrl());
|
result.put("coverUrl", work.getCoverUrl());
|
||||||
result.put("description", work.getDescription());
|
result.put("description", work.getDescription());
|
||||||
result.put("status", work.getStatus());
|
result.put("status", work.getStatus());
|
||||||
|
result.put("progress", work.getProgress());
|
||||||
|
result.put("progressMessage", work.getProgressMessage());
|
||||||
result.put("originalImageUrl", work.getOriginalImageUrl());
|
result.put("originalImageUrl", work.getOriginalImageUrl());
|
||||||
result.put("voiceInputUrl", work.getVoiceInputUrl());
|
result.put("voiceInputUrl", work.getVoiceInputUrl());
|
||||||
result.put("textInput", work.getTextInput());
|
result.put("textInput", work.getTextInput());
|
||||||
result.put("aiMeta", work.getAiMeta());
|
result.put("aiMeta", work.getAiMeta());
|
||||||
|
result.put("style", work.getStyle());
|
||||||
|
result.put("authorName", work.getAuthorName());
|
||||||
|
result.put("failReason", work.getFailReason());
|
||||||
|
result.put("remoteWorkId", work.getRemoteWorkId());
|
||||||
result.put("createTime", work.getCreateTime());
|
result.put("createTime", work.getCreateTime());
|
||||||
|
|
||||||
|
// 查询页面列表
|
||||||
|
LambdaQueryWrapper<UgcWorkPage> pageWrapper = new LambdaQueryWrapper<>();
|
||||||
|
pageWrapper.eq(UgcWorkPage::getWorkId, id)
|
||||||
|
.orderByAsc(UgcWorkPage::getPageNo);
|
||||||
|
List<UgcWorkPage> pages = ugcWorkPageMapper.selectList(pageWrapper);
|
||||||
|
result.put("pages", pages);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -42,7 +42,7 @@ public class PublicUserWorkService {
|
|||||||
work.setCoverUrl(coverUrl);
|
work.setCoverUrl(coverUrl);
|
||||||
work.setDescription(description);
|
work.setDescription(description);
|
||||||
work.setVisibility(visibility != null ? visibility : "private");
|
work.setVisibility(visibility != null ? visibility : "private");
|
||||||
work.setStatus("draft");
|
work.setStatus(0); // 0=DRAFT
|
||||||
work.setViewCount(0);
|
work.setViewCount(0);
|
||||||
work.setLikeCount(0);
|
work.setLikeCount(0);
|
||||||
work.setFavoriteCount(0);
|
work.setFavoriteCount(0);
|
||||||
@ -154,10 +154,10 @@ public class PublicUserWorkService {
|
|||||||
if (work == null || work.getIsDeleted() == 1 || !work.getUserId().equals(userId)) {
|
if (work == null || work.getIsDeleted() == 1 || !work.getUserId().equals(userId)) {
|
||||||
throw new BusinessException(404, "作品不存在或无权操作");
|
throw new BusinessException(404, "作品不存在或无权操作");
|
||||||
}
|
}
|
||||||
if (!"draft".equals(work.getStatus()) && !"rejected".equals(work.getStatus())) {
|
if (work.getStatus() != 0 && work.getStatus() != -1) {
|
||||||
throw new BusinessException(400, "当前状态不可发布");
|
throw new BusinessException(400, "当前状态不可发布");
|
||||||
}
|
}
|
||||||
work.setStatus("pending_review");
|
work.setStatus(1); // 1=PENDING
|
||||||
work.setModifyTime(LocalDateTime.now());
|
work.setModifyTime(LocalDateTime.now());
|
||||||
ugcWorkMapper.updateById(work);
|
ugcWorkMapper.updateById(work);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,136 @@
|
|||||||
|
package com.competition.modules.sys.config;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.competition.modules.sys.entity.SysPermission;
|
||||||
|
import com.competition.modules.sys.entity.SysRole;
|
||||||
|
import com.competition.modules.sys.entity.SysRolePermission;
|
||||||
|
import com.competition.modules.sys.mapper.SysPermissionMapper;
|
||||||
|
import com.competition.modules.sys.mapper.SysRoleMapper;
|
||||||
|
import com.competition.modules.sys.mapper.SysRolePermissionMapper;
|
||||||
|
import com.competition.modules.sys.mapper.SysTenantMapper;
|
||||||
|
import com.competition.modules.sys.entity.SysTenant;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.boot.ApplicationArguments;
|
||||||
|
import org.springframework.boot.ApplicationRunner;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评委角色权限:与 docs/design/menu-config.md「评委端权限码」一致,并从评委租户或 gdlib 模板复制缺失的权限定义。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Order(100)
|
||||||
|
public class JudgeRolePermissionConfigurer implements ApplicationRunner {
|
||||||
|
|
||||||
|
/** 与 menu-config 评委端权限码一致 */
|
||||||
|
public static final List<String> JUDGE_PERMISSION_CODES = List.of(
|
||||||
|
"review:score", "review:read", "review:create", "review:update",
|
||||||
|
"activity:read", "judge:read", "judge:assign", "work:read",
|
||||||
|
"notice:read", "workbench:read"
|
||||||
|
);
|
||||||
|
|
||||||
|
private static final String TEMPLATE_TENANT_CODE_GDLIB = "gdlib";
|
||||||
|
private static final String JUDGE_TENANT_CODE = "judge";
|
||||||
|
|
||||||
|
private final SysPermissionMapper permissionMapper;
|
||||||
|
private final SysRolePermissionMapper rolePermissionMapper;
|
||||||
|
private final SysRoleMapper roleMapper;
|
||||||
|
private final SysTenantMapper tenantMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run(ApplicationArguments args) {
|
||||||
|
List<SysRole> judgeRoles = roleMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<SysRole>()
|
||||||
|
.eq(SysRole::getCode, "judge")
|
||||||
|
.eq(SysRole::getValidState, 1));
|
||||||
|
for (SysRole role : judgeRoles) {
|
||||||
|
try {
|
||||||
|
ensureJudgeRolePermissions(role.getTenantId(), role.getId());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("启动时补全评委角色权限失败 tenantId={} roleId={}: {}",
|
||||||
|
role.getTenantId(), role.getId(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!judgeRoles.isEmpty()) {
|
||||||
|
log.info("评委角色权限补全完成,共 {} 个 judge 角色", judgeRoles.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确保指定租户下评委角色拥有 menu-config 所列权限;缺失的权限从评委租户或 gdlib 复制到本租户后再绑定。
|
||||||
|
*/
|
||||||
|
public void ensureJudgeRolePermissions(Long tenantId, Long roleId) {
|
||||||
|
if (tenantId == null || roleId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (String code : JUDGE_PERMISSION_CODES) {
|
||||||
|
SysPermission perm = permissionMapper.selectOne(
|
||||||
|
new LambdaQueryWrapper<SysPermission>()
|
||||||
|
.eq(SysPermission::getTenantId, tenantId)
|
||||||
|
.eq(SysPermission::getCode, code)
|
||||||
|
.eq(SysPermission::getValidState, 1));
|
||||||
|
if (perm == null) {
|
||||||
|
SysPermission template = findTemplatePermission(code);
|
||||||
|
if (template == null) {
|
||||||
|
log.debug("评委权限模板中无编码 {},跳过", code);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
perm = new SysPermission();
|
||||||
|
perm.setTenantId(tenantId);
|
||||||
|
perm.setName(template.getName());
|
||||||
|
perm.setCode(template.getCode());
|
||||||
|
perm.setResource(template.getResource());
|
||||||
|
perm.setAction(template.getAction());
|
||||||
|
perm.setDescription(template.getDescription());
|
||||||
|
permissionMapper.insert(perm);
|
||||||
|
}
|
||||||
|
Long permId = perm.getId();
|
||||||
|
long exists = rolePermissionMapper.selectCount(
|
||||||
|
new LambdaQueryWrapper<SysRolePermission>()
|
||||||
|
.eq(SysRolePermission::getRoleId, roleId)
|
||||||
|
.eq(SysRolePermission::getPermissionId, permId));
|
||||||
|
if (exists == 0) {
|
||||||
|
SysRolePermission rp = new SysRolePermission();
|
||||||
|
rp.setRoleId(roleId);
|
||||||
|
rp.setPermissionId(permId);
|
||||||
|
rolePermissionMapper.insert(rp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SysPermission findTemplatePermission(String code) {
|
||||||
|
Long judgeTid = tenantIdByCode(JUDGE_TENANT_CODE);
|
||||||
|
if (judgeTid != null) {
|
||||||
|
SysPermission p = permissionMapper.selectOne(
|
||||||
|
new LambdaQueryWrapper<SysPermission>()
|
||||||
|
.eq(SysPermission::getTenantId, judgeTid)
|
||||||
|
.eq(SysPermission::getCode, code)
|
||||||
|
.eq(SysPermission::getValidState, 1));
|
||||||
|
if (p != null) {
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Long gdlibTid = tenantIdByCode(TEMPLATE_TENANT_CODE_GDLIB);
|
||||||
|
if (gdlibTid != null) {
|
||||||
|
return permissionMapper.selectOne(
|
||||||
|
new LambdaQueryWrapper<SysPermission>()
|
||||||
|
.eq(SysPermission::getTenantId, gdlibTid)
|
||||||
|
.eq(SysPermission::getCode, code)
|
||||||
|
.eq(SysPermission::getValidState, 1));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long tenantIdByCode(String code) {
|
||||||
|
SysTenant t = tenantMapper.selectOne(
|
||||||
|
new LambdaQueryWrapper<SysTenant>()
|
||||||
|
.eq(SysTenant::getCode, code)
|
||||||
|
.eq(SysTenant::getValidState, 1));
|
||||||
|
return t != null ? t.getId() : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@ package com.competition.modules.sys.entity;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import com.competition.common.entity.BaseEntity;
|
import com.competition.common.entity.BaseEntity;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
@ -12,19 +13,20 @@ import lombok.EqualsAndHashCode;
|
|||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
@TableName("t_sys_config")
|
@TableName("t_sys_config")
|
||||||
|
@Schema(description = "系统配置实体")
|
||||||
public class SysConfig extends BaseEntity {
|
public class SysConfig extends BaseEntity {
|
||||||
|
|
||||||
/** 租户 ID */
|
@Schema(description = "租户ID")
|
||||||
@TableField("tenant_id")
|
@TableField("tenant_id")
|
||||||
private Long tenantId;
|
private Long tenantId;
|
||||||
|
|
||||||
/** 配置键(租户内唯一) */
|
@Schema(description = "配置键(租户内唯一)")
|
||||||
@TableField("`key`")
|
@TableField("`key`")
|
||||||
private String key;
|
private String key;
|
||||||
|
|
||||||
/** 配置值 */
|
@Schema(description = "配置值")
|
||||||
private String value;
|
private String value;
|
||||||
|
|
||||||
/** 描述 */
|
@Schema(description = "描述")
|
||||||
private String description;
|
private String description;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package com.competition.modules.sys.entity;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import com.competition.common.entity.BaseEntity;
|
import com.competition.common.entity.BaseEntity;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
@ -14,22 +15,23 @@ import java.util.List;
|
|||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
@TableName("t_sys_dict")
|
@TableName("t_sys_dict")
|
||||||
|
@Schema(description = "数据字典实体")
|
||||||
public class SysDict extends BaseEntity {
|
public class SysDict extends BaseEntity {
|
||||||
|
|
||||||
/** 租户 ID */
|
@Schema(description = "租户ID")
|
||||||
@TableField("tenant_id")
|
@TableField("tenant_id")
|
||||||
private Long tenantId;
|
private Long tenantId;
|
||||||
|
|
||||||
/** 字典名称 */
|
@Schema(description = "字典名称")
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
/** 字典编码(租户内唯一) */
|
@Schema(description = "字典编码(租户内唯一)")
|
||||||
private String code;
|
private String code;
|
||||||
|
|
||||||
/** 描述 */
|
@Schema(description = "描述")
|
||||||
private String description;
|
private String description;
|
||||||
|
|
||||||
/** 字典项(非数据库字段) */
|
@Schema(description = "字典项列表")
|
||||||
@TableField(exist = false)
|
@TableField(exist = false)
|
||||||
private List<SysDictItem> items;
|
private List<SysDictItem> items;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package com.competition.modules.sys.entity;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import com.competition.common.entity.BaseEntity;
|
import com.competition.common.entity.BaseEntity;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
@ -12,18 +13,19 @@ import lombok.EqualsAndHashCode;
|
|||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
@TableName("t_sys_dict_item")
|
@TableName("t_sys_dict_item")
|
||||||
|
@Schema(description = "数据字典项实体")
|
||||||
public class SysDictItem extends BaseEntity {
|
public class SysDictItem extends BaseEntity {
|
||||||
|
|
||||||
/** 字典 ID */
|
@Schema(description = "字典ID")
|
||||||
@TableField("dict_id")
|
@TableField("dict_id")
|
||||||
private Long dictId;
|
private Long dictId;
|
||||||
|
|
||||||
/** 标签 */
|
@Schema(description = "标签")
|
||||||
private String label;
|
private String label;
|
||||||
|
|
||||||
/** 值 */
|
@Schema(description = "值")
|
||||||
private String value;
|
private String value;
|
||||||
|
|
||||||
/** 排序 */
|
@Schema(description = "排序")
|
||||||
private Integer sort;
|
private Integer sort;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,39 +1,42 @@
|
|||||||
package com.competition.modules.sys.entity;
|
package com.competition.modules.sys.entity;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.*;
|
import com.baomidou.mybatisplus.annotation.*;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 系统日志实体(不继承 BaseEntity,字段结构不同)
|
* 系统日志实体
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@TableName("t_sys_log")
|
@TableName("t_sys_log")
|
||||||
|
@Schema(description = "系统日志实体")
|
||||||
public class SysLog implements Serializable {
|
public class SysLog implements Serializable {
|
||||||
|
|
||||||
|
@Schema(description = "主键ID")
|
||||||
@TableId(type = IdType.AUTO)
|
@TableId(type = IdType.AUTO)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
/** 用户 ID */
|
@Schema(description = "用户ID")
|
||||||
@TableField("user_id")
|
@TableField("user_id")
|
||||||
private Long userId;
|
private Long userId;
|
||||||
|
|
||||||
/** 操作 */
|
@Schema(description = "操作")
|
||||||
private String action;
|
private String action;
|
||||||
|
|
||||||
/** 内容 */
|
@Schema(description = "内容")
|
||||||
private String content;
|
private String content;
|
||||||
|
|
||||||
/** IP 地址 */
|
@Schema(description = "IP地址")
|
||||||
private String ip;
|
private String ip;
|
||||||
|
|
||||||
/** User Agent */
|
@Schema(description = "用户代理")
|
||||||
@TableField("user_agent")
|
@TableField("user_agent")
|
||||||
private String userAgent;
|
private String userAgent;
|
||||||
|
|
||||||
/** 创建时间 */
|
@Schema(description = "创建时间")
|
||||||
@TableField("create_time")
|
@TableField("create_time")
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package com.competition.modules.sys.entity;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import com.competition.common.entity.BaseEntity;
|
import com.competition.common.entity.BaseEntity;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
@ -14,31 +15,32 @@ import java.util.List;
|
|||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
@TableName("t_sys_menu")
|
@TableName("t_sys_menu")
|
||||||
|
@Schema(description = "系统菜单实体")
|
||||||
public class SysMenu extends BaseEntity {
|
public class SysMenu extends BaseEntity {
|
||||||
|
|
||||||
/** 菜单名称 */
|
@Schema(description = "菜单名称")
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
/** 路由路径 */
|
@Schema(description = "路由路径")
|
||||||
private String path;
|
private String path;
|
||||||
|
|
||||||
/** 图标 */
|
@Schema(description = "图标")
|
||||||
private String icon;
|
private String icon;
|
||||||
|
|
||||||
/** 前端组件路径 */
|
@Schema(description = "前端组件路径")
|
||||||
private String component;
|
private String component;
|
||||||
|
|
||||||
/** 父菜单 ID */
|
@Schema(description = "父菜单ID")
|
||||||
@TableField("parent_id")
|
@TableField("parent_id")
|
||||||
private Long parentId;
|
private Long parentId;
|
||||||
|
|
||||||
/** 权限标识 */
|
@Schema(description = "权限标识")
|
||||||
private String permission;
|
private String permission;
|
||||||
|
|
||||||
/** 排序 */
|
@Schema(description = "排序")
|
||||||
private Integer sort;
|
private Integer sort;
|
||||||
|
|
||||||
/** 子菜单(非数据库字段) */
|
@Schema(description = "子菜单列表")
|
||||||
@TableField(exist = false)
|
@TableField(exist = false)
|
||||||
private List<SysMenu> children;
|
private List<SysMenu> children;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package com.competition.modules.sys.entity;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import com.competition.common.entity.BaseEntity;
|
import com.competition.common.entity.BaseEntity;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
@ -12,24 +13,25 @@ import lombok.EqualsAndHashCode;
|
|||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
@TableName("t_sys_permission")
|
@TableName("t_sys_permission")
|
||||||
|
@Schema(description = "系统权限实体")
|
||||||
public class SysPermission extends BaseEntity {
|
public class SysPermission extends BaseEntity {
|
||||||
|
|
||||||
/** 租户 ID */
|
@Schema(description = "租户ID")
|
||||||
@TableField("tenant_id")
|
@TableField("tenant_id")
|
||||||
private Long tenantId;
|
private Long tenantId;
|
||||||
|
|
||||||
/** 权限名称 */
|
@Schema(description = "权限名称")
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
/** 权限编码(格式:resource:action) */
|
@Schema(description = "权限编码(格式:resource:action)")
|
||||||
private String code;
|
private String code;
|
||||||
|
|
||||||
/** 资源 */
|
@Schema(description = "资源")
|
||||||
private String resource;
|
private String resource;
|
||||||
|
|
||||||
/** 操作 */
|
@Schema(description = "操作")
|
||||||
private String action;
|
private String action;
|
||||||
|
|
||||||
/** 描述 */
|
@Schema(description = "描述")
|
||||||
private String description;
|
private String description;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package com.competition.modules.sys.entity;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import com.competition.common.entity.BaseEntity;
|
import com.competition.common.entity.BaseEntity;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
@ -12,18 +13,19 @@ import lombok.EqualsAndHashCode;
|
|||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
@TableName("t_sys_role")
|
@TableName("t_sys_role")
|
||||||
|
@Schema(description = "系统角色实体")
|
||||||
public class SysRole extends BaseEntity {
|
public class SysRole extends BaseEntity {
|
||||||
|
|
||||||
/** 租户 ID */
|
@Schema(description = "租户ID")
|
||||||
@TableField("tenant_id")
|
@TableField("tenant_id")
|
||||||
private Long tenantId;
|
private Long tenantId;
|
||||||
|
|
||||||
/** 角色名称 */
|
@Schema(description = "角色名称")
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
/** 角色编码 */
|
@Schema(description = "角色编码")
|
||||||
private String code;
|
private String code;
|
||||||
|
|
||||||
/** 角色描述 */
|
@Schema(description = "角色描述")
|
||||||
private String description;
|
private String description;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package com.competition.modules.sys.entity;
|
package com.competition.modules.sys.entity;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.*;
|
import com.baomidou.mybatisplus.annotation.*;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
@ -10,14 +11,18 @@ import java.io.Serializable;
|
|||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@TableName("t_sys_role_permission")
|
@TableName("t_sys_role_permission")
|
||||||
|
@Schema(description = "角色权限关联实体")
|
||||||
public class SysRolePermission implements Serializable {
|
public class SysRolePermission implements Serializable {
|
||||||
|
|
||||||
|
@Schema(description = "主键ID")
|
||||||
@TableId(type = IdType.AUTO)
|
@TableId(type = IdType.AUTO)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "角色ID")
|
||||||
@TableField("role_id")
|
@TableField("role_id")
|
||||||
private Long roleId;
|
private Long roleId;
|
||||||
|
|
||||||
|
@Schema(description = "权限ID")
|
||||||
@TableField("permission_id")
|
@TableField("permission_id")
|
||||||
private Long permissionId;
|
private Long permissionId;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package com.competition.modules.sys.entity;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import com.competition.common.entity.BaseEntity;
|
import com.competition.common.entity.BaseEntity;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
@ -12,25 +13,26 @@ import lombok.EqualsAndHashCode;
|
|||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
@TableName("t_sys_tenant")
|
@TableName("t_sys_tenant")
|
||||||
|
@Schema(description = "系统租户实体")
|
||||||
public class SysTenant extends BaseEntity {
|
public class SysTenant extends BaseEntity {
|
||||||
|
|
||||||
/** 租户名称 */
|
@Schema(description = "租户名称")
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
/** 租户编码(唯一,用于访问链接) */
|
@Schema(description = "租户编码(唯一,用于访问链接)")
|
||||||
private String code;
|
private String code;
|
||||||
|
|
||||||
/** 租户域名(可选) */
|
@Schema(description = "租户域名(可选)")
|
||||||
private String domain;
|
private String domain;
|
||||||
|
|
||||||
/** 租户描述 */
|
@Schema(description = "租户描述")
|
||||||
private String description;
|
private String description;
|
||||||
|
|
||||||
/** 是否为超级租户:0-否,1-是 */
|
@Schema(description = "是否为超级租户:0-否,1-是")
|
||||||
@TableField("is_super")
|
@TableField("is_super")
|
||||||
private Integer isSuper;
|
private Integer isSuper;
|
||||||
|
|
||||||
/** 租户类型:platform/library/kindergarten/school/institution/other */
|
@Schema(description = "租户类型:platform/library/kindergarten/school/institution/other")
|
||||||
@TableField("tenant_type")
|
@TableField("tenant_type")
|
||||||
private String tenantType;
|
private String tenantType;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package com.competition.modules.sys.entity;
|
package com.competition.modules.sys.entity;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.*;
|
import com.baomidou.mybatisplus.annotation.*;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
@ -10,14 +11,18 @@ import java.io.Serializable;
|
|||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@TableName("t_sys_tenant_menu")
|
@TableName("t_sys_tenant_menu")
|
||||||
|
@Schema(description = "租户菜单关联实体")
|
||||||
public class SysTenantMenu implements Serializable {
|
public class SysTenantMenu implements Serializable {
|
||||||
|
|
||||||
|
@Schema(description = "主键ID")
|
||||||
@TableId(type = IdType.AUTO)
|
@TableId(type = IdType.AUTO)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "租户ID")
|
||||||
@TableField("tenant_id")
|
@TableField("tenant_id")
|
||||||
private Long tenantId;
|
private Long tenantId;
|
||||||
|
|
||||||
|
@Schema(description = "菜单ID")
|
||||||
@TableField("menu_id")
|
@TableField("menu_id")
|
||||||
private Long menuId;
|
private Long menuId;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package com.competition.modules.sys.entity;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import com.competition.common.entity.BaseEntity;
|
import com.competition.common.entity.BaseEntity;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
@ -14,58 +15,59 @@ import java.time.LocalDate;
|
|||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
@TableName("t_sys_user")
|
@TableName("t_sys_user")
|
||||||
|
@Schema(description = "系统用户实体")
|
||||||
public class SysUser extends BaseEntity {
|
public class SysUser extends BaseEntity {
|
||||||
|
|
||||||
/** 租户 ID */
|
@Schema(description = "租户ID")
|
||||||
@TableField("tenant_id")
|
@TableField("tenant_id")
|
||||||
private Long tenantId;
|
private Long tenantId;
|
||||||
|
|
||||||
/** 用户名(在租户内唯一) */
|
@Schema(description = "用户名(租户内唯一)")
|
||||||
private String username;
|
private String username;
|
||||||
|
|
||||||
/** 密码(加密存储) */
|
@Schema(description = "密码(加密存储)")
|
||||||
private String password;
|
private String password;
|
||||||
|
|
||||||
/** 昵称 */
|
@Schema(description = "昵称")
|
||||||
private String nickname;
|
private String nickname;
|
||||||
|
|
||||||
/** 邮箱 */
|
@Schema(description = "邮箱")
|
||||||
private String email;
|
private String email;
|
||||||
|
|
||||||
/** 手机号(全局唯一) */
|
@Schema(description = "手机号(全局唯一)")
|
||||||
private String phone;
|
private String phone;
|
||||||
|
|
||||||
/** 微信 OpenID */
|
@Schema(description = "微信OpenID")
|
||||||
@TableField("wx_openid")
|
@TableField("wx_openid")
|
||||||
private String wxOpenid;
|
private String wxOpenid;
|
||||||
|
|
||||||
/** 微信 UnionID */
|
@Schema(description = "微信UnionID")
|
||||||
@TableField("wx_unionid")
|
@TableField("wx_unionid")
|
||||||
private String wxUnionid;
|
private String wxUnionid;
|
||||||
|
|
||||||
/** 用户来源:admin_created/self_registered/child_migrated */
|
@Schema(description = "用户来源:admin_created/self_registered/child_migrated")
|
||||||
@TableField("user_source")
|
@TableField("user_source")
|
||||||
private String userSource;
|
private String userSource;
|
||||||
|
|
||||||
/** 用户类型:adult/child */
|
@Schema(description = "用户类型:adult/child")
|
||||||
@TableField("user_type")
|
@TableField("user_type")
|
||||||
private String userType;
|
private String userType;
|
||||||
|
|
||||||
/** 所在城市 */
|
@Schema(description = "所在城市")
|
||||||
private String city;
|
private String city;
|
||||||
|
|
||||||
/** 出生日期 */
|
@Schema(description = "出生日期")
|
||||||
private LocalDate birthday;
|
private LocalDate birthday;
|
||||||
|
|
||||||
/** 性别 */
|
@Schema(description = "性别")
|
||||||
private String gender;
|
private String gender;
|
||||||
|
|
||||||
/** 头像 URL */
|
@Schema(description = "头像URL")
|
||||||
private String avatar;
|
private String avatar;
|
||||||
|
|
||||||
/** 所属单位 */
|
@Schema(description = "所属单位")
|
||||||
private String organization;
|
private String organization;
|
||||||
|
|
||||||
/** 账号状态:enabled/disabled */
|
@Schema(description = "账号状态:enabled/disabled")
|
||||||
private String status;
|
private String status;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package com.competition.modules.sys.entity;
|
package com.competition.modules.sys.entity;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.*;
|
import com.baomidou.mybatisplus.annotation.*;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
@ -10,14 +11,18 @@ import java.io.Serializable;
|
|||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@TableName("t_sys_user_role")
|
@TableName("t_sys_user_role")
|
||||||
|
@Schema(description = "用户角色关联实体")
|
||||||
public class SysUserRole implements Serializable {
|
public class SysUserRole implements Serializable {
|
||||||
|
|
||||||
|
@Schema(description = "主键ID")
|
||||||
@TableId(type = IdType.AUTO)
|
@TableId(type = IdType.AUTO)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "用户ID")
|
||||||
@TableField("user_id")
|
@TableField("user_id")
|
||||||
private Long userId;
|
private Long userId;
|
||||||
|
|
||||||
|
@Schema(description = "角色ID")
|
||||||
@TableField("role_id")
|
@TableField("role_id")
|
||||||
private Long roleId;
|
private Long roleId;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -59,7 +59,14 @@ public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> impl
|
|||||||
// 获取租户分配的菜单 ID
|
// 获取租户分配的菜单 ID
|
||||||
List<SysTenantMenu> tenantMenus = tenantMenuMapper.selectList(
|
List<SysTenantMenu> tenantMenus = tenantMenuMapper.selectList(
|
||||||
new LambdaQueryWrapper<SysTenantMenu>().eq(SysTenantMenu::getTenantId, tenantId));
|
new LambdaQueryWrapper<SysTenantMenu>().eq(SysTenantMenu::getTenantId, tenantId));
|
||||||
Set<Long> tenantMenuIds = tenantMenus.stream().map(SysTenantMenu::getMenuId).collect(Collectors.toSet());
|
Set<Long> tenantMenuIds = new HashSet<>(tenantMenus.stream().map(SysTenantMenu::getMenuId).collect(Collectors.toSet()));
|
||||||
|
|
||||||
|
List<String> roles = isSuperAdmin ? Collections.emptyList() : userMapper.selectRolesByUserId(userId);
|
||||||
|
|
||||||
|
// 租户评委与平台评委共用「我的评审」菜单树:机构租户未在 t_sys_tenant_menu 中配置时,按角色合并评委端菜单
|
||||||
|
if (!isSuperAdmin && roles != null && roles.contains("judge")) {
|
||||||
|
tenantMenuIds.addAll(collectJudgePortalMenuIds(allMenus));
|
||||||
|
}
|
||||||
|
|
||||||
if (isSuperAdmin) {
|
if (isSuperAdmin) {
|
||||||
// 超管:按租户菜单过滤,但不做权限码过滤
|
// 超管:按租户菜单过滤,但不做权限码过滤
|
||||||
@ -80,10 +87,14 @@ public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> impl
|
|||||||
List<String> userPermissions = userMapper.selectPermissionsByUserId(userId);
|
List<String> userPermissions = userMapper.selectPermissionsByUserId(userId);
|
||||||
Set<String> permSet = new HashSet<>(userPermissions);
|
Set<String> permSet = new HashSet<>(userPermissions);
|
||||||
|
|
||||||
|
// 纯评委角色不展示机构端「评委管理」(与 judge:read 权限码重叠,否则侧栏会出现该菜单)
|
||||||
|
boolean hideTenantJudgeMgmtForPureJudge = shouldHideTenantJudgeManagementMenuForJudge(roles);
|
||||||
|
|
||||||
// 过滤:菜单必须属于租户,且用户有对应权限(无权限要求的菜单直接放行)
|
// 过滤:菜单必须属于租户,且用户有对应权限(无权限要求的菜单直接放行)
|
||||||
List<SysMenu> filteredMenus = allMenus.stream()
|
List<SysMenu> filteredMenus = allMenus.stream()
|
||||||
.filter(menu -> tenantMenuIds.contains(menu.getId()))
|
.filter(menu -> tenantMenuIds.contains(menu.getId()))
|
||||||
.filter(menu -> menu.getPermission() == null || menu.getPermission().isBlank() || permSet.contains(menu.getPermission()))
|
.filter(menu -> menu.getPermission() == null || menu.getPermission().isBlank() || permSet.contains(menu.getPermission()))
|
||||||
|
.filter(menu -> !hideTenantJudgeMgmtForPureJudge || !isTenantJudgeManagementMenu(menu))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
// 补全父菜单(确保树结构完整)
|
// 补全父菜单(确保树结构完整)
|
||||||
@ -168,6 +179,53 @@ public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> impl
|
|||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 机构租户「评委管理」页(非评委工作台) */
|
||||||
|
private static final String TENANT_JUDGE_MANAGEMENT_COMPONENT = "contests/judges/Index";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仅评委角色(无 tenant_admin / super_admin)不展示机构端评委管理菜单
|
||||||
|
*/
|
||||||
|
private boolean shouldHideTenantJudgeManagementMenuForJudge(List<String> roles) {
|
||||||
|
if (roles == null || roles.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!roles.contains("judge")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !roles.contains("tenant_admin") && !roles.contains("super_admin");
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isTenantJudgeManagementMenu(SysMenu menu) {
|
||||||
|
return TENANT_JUDGE_MANAGEMENT_COMPONENT.equals(menu.getComponent());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评委端菜单:评审任务、预设评语及其父级(与 docs/design/menu-config 一致,不依赖固定菜单 ID)
|
||||||
|
*/
|
||||||
|
private Set<Long> collectJudgePortalMenuIds(List<SysMenu> allMenus) {
|
||||||
|
Set<Long> ids = new HashSet<>();
|
||||||
|
Set<String> leafComponents = Set.of("activities/Review", "activities/PresetComments");
|
||||||
|
for (SysMenu m : allMenus) {
|
||||||
|
if (m.getComponent() != null && leafComponents.contains(m.getComponent())) {
|
||||||
|
ids.add(m.getId());
|
||||||
|
Long pid = m.getParentId();
|
||||||
|
while (pid != null) {
|
||||||
|
if (ids.contains(pid)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ids.add(pid);
|
||||||
|
final Long currentPid = pid;
|
||||||
|
SysMenu parent = allMenus.stream()
|
||||||
|
.filter(x -> x.getId().equals(currentPid))
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
pid = parent != null ? parent.getParentId() : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
/** 递归补全父菜单 */
|
/** 递归补全父菜单 */
|
||||||
private void addParentsIfMissing(SysMenu menu, List<SysMenu> allMenus, List<SysMenu> filtered, Set<Long> filteredIds) {
|
private void addParentsIfMissing(SysMenu menu, List<SysMenu> allMenus, List<SysMenu> filtered, Set<Long> filteredIds) {
|
||||||
if (menu.getParentId() == null || filteredIds.contains(menu.getParentId())) return;
|
if (menu.getParentId() == null || filteredIds.contains(menu.getParentId())) return;
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.IdType;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
@ -11,29 +12,39 @@ import java.time.LocalDateTime;
|
|||||||
|
|
||||||
@Data
|
@Data
|
||||||
@TableName("t_ugc_review_log")
|
@TableName("t_ugc_review_log")
|
||||||
|
@Schema(description = "UGC审核日志实体")
|
||||||
public class UgcReviewLog implements Serializable {
|
public class UgcReviewLog implements Serializable {
|
||||||
|
|
||||||
|
@Schema(description = "主键ID")
|
||||||
@TableId(type = IdType.AUTO)
|
@TableId(type = IdType.AUTO)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "目标类型")
|
||||||
@TableField("target_type")
|
@TableField("target_type")
|
||||||
private String targetType;
|
private String targetType;
|
||||||
|
|
||||||
|
@Schema(description = "目标ID")
|
||||||
@TableField("target_id")
|
@TableField("target_id")
|
||||||
private Long targetId;
|
private Long targetId;
|
||||||
|
|
||||||
|
@Schema(description = "作品ID")
|
||||||
@TableField("work_id")
|
@TableField("work_id")
|
||||||
private Long workId;
|
private Long workId;
|
||||||
|
|
||||||
|
@Schema(description = "操作动作")
|
||||||
private String action;
|
private String action;
|
||||||
|
|
||||||
|
@Schema(description = "原因")
|
||||||
private String reason;
|
private String reason;
|
||||||
|
|
||||||
|
@Schema(description = "备注")
|
||||||
private String note;
|
private String note;
|
||||||
|
|
||||||
|
@Schema(description = "操作人ID")
|
||||||
@TableField("operator_id")
|
@TableField("operator_id")
|
||||||
private Long operatorId;
|
private Long operatorId;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
@TableField("create_time")
|
@TableField("create_time")
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.IdType;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
@ -11,27 +12,37 @@ import java.time.LocalDateTime;
|
|||||||
|
|
||||||
@Data
|
@Data
|
||||||
@TableName("t_ugc_tag")
|
@TableName("t_ugc_tag")
|
||||||
|
@Schema(description = "UGC标签实体")
|
||||||
public class UgcTag implements Serializable {
|
public class UgcTag implements Serializable {
|
||||||
|
|
||||||
|
@Schema(description = "主键ID")
|
||||||
@TableId(type = IdType.AUTO)
|
@TableId(type = IdType.AUTO)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "标签名称")
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
|
@Schema(description = "标签分类")
|
||||||
private String category;
|
private String category;
|
||||||
|
|
||||||
|
@Schema(description = "标签颜色")
|
||||||
private String color;
|
private String color;
|
||||||
|
|
||||||
|
@Schema(description = "排序")
|
||||||
private Integer sort;
|
private Integer sort;
|
||||||
|
|
||||||
|
@Schema(description = "状态")
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
|
@Schema(description = "使用次数")
|
||||||
@TableField("usage_count")
|
@TableField("usage_count")
|
||||||
private Integer usageCount;
|
private Integer usageCount;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
@TableField("create_time")
|
@TableField("create_time")
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
@Schema(description = "修改时间")
|
||||||
@TableField("modify_time")
|
@TableField("modify_time")
|
||||||
private LocalDateTime modifyTime;
|
private LocalDateTime modifyTime;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
@ -12,79 +13,128 @@ import java.time.LocalDateTime;
|
|||||||
|
|
||||||
@Data
|
@Data
|
||||||
@TableName(value = "t_ugc_work", autoResultMap = true)
|
@TableName(value = "t_ugc_work", autoResultMap = true)
|
||||||
|
@Schema(description = "UGC作品实体")
|
||||||
public class UgcWork implements Serializable {
|
public class UgcWork implements Serializable {
|
||||||
|
|
||||||
|
@Schema(description = "主键ID")
|
||||||
@TableId(type = IdType.AUTO)
|
@TableId(type = IdType.AUTO)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "用户ID")
|
||||||
@TableField("user_id")
|
@TableField("user_id")
|
||||||
private Long userId;
|
private Long userId;
|
||||||
|
|
||||||
|
@Schema(description = "乐读派远程作品ID")
|
||||||
|
@TableField("remote_work_id")
|
||||||
|
private String remoteWorkId;
|
||||||
|
|
||||||
|
@Schema(description = "作品标题")
|
||||||
private String title;
|
private String title;
|
||||||
|
|
||||||
|
@Schema(description = "封面图URL")
|
||||||
@TableField("cover_url")
|
@TableField("cover_url")
|
||||||
private String coverUrl;
|
private String coverUrl;
|
||||||
|
|
||||||
|
@Schema(description = "作品描述")
|
||||||
private String description;
|
private String description;
|
||||||
|
|
||||||
|
@Schema(description = "可见范围")
|
||||||
private String visibility;
|
private String visibility;
|
||||||
|
|
||||||
private String status;
|
@Schema(description = "作品状态:-1=FAILED, 0=DRAFT, 1=PENDING, 2=PROCESSING, 3=COMPLETED, 4=CATALOGED, 5=DUBBED")
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
@Schema(description = "审核备注")
|
||||||
@TableField("review_note")
|
@TableField("review_note")
|
||||||
private String reviewNote;
|
private String reviewNote;
|
||||||
|
|
||||||
|
@Schema(description = "审核时间")
|
||||||
@TableField("review_time")
|
@TableField("review_time")
|
||||||
private LocalDateTime reviewTime;
|
private LocalDateTime reviewTime;
|
||||||
|
|
||||||
|
@Schema(description = "审核人ID")
|
||||||
@TableField("reviewer_id")
|
@TableField("reviewer_id")
|
||||||
private Long reviewerId;
|
private Long reviewerId;
|
||||||
|
|
||||||
|
@Schema(description = "机审结果")
|
||||||
@TableField("machine_review_result")
|
@TableField("machine_review_result")
|
||||||
private String machineReviewResult;
|
private String machineReviewResult;
|
||||||
|
|
||||||
|
@Schema(description = "机审备注")
|
||||||
@TableField("machine_review_note")
|
@TableField("machine_review_note")
|
||||||
private String machineReviewNote;
|
private String machineReviewNote;
|
||||||
|
|
||||||
|
@Schema(description = "是否推荐")
|
||||||
@TableField("is_recommended")
|
@TableField("is_recommended")
|
||||||
private Boolean isRecommended;
|
private Boolean isRecommended;
|
||||||
|
|
||||||
|
@Schema(description = "浏览数")
|
||||||
@TableField("view_count")
|
@TableField("view_count")
|
||||||
private Integer viewCount;
|
private Integer viewCount;
|
||||||
|
|
||||||
|
@Schema(description = "点赞数")
|
||||||
@TableField("like_count")
|
@TableField("like_count")
|
||||||
private Integer likeCount;
|
private Integer likeCount;
|
||||||
|
|
||||||
|
@Schema(description = "收藏数")
|
||||||
@TableField("favorite_count")
|
@TableField("favorite_count")
|
||||||
private Integer favoriteCount;
|
private Integer favoriteCount;
|
||||||
|
|
||||||
|
@Schema(description = "评论数")
|
||||||
@TableField("comment_count")
|
@TableField("comment_count")
|
||||||
private Integer commentCount;
|
private Integer commentCount;
|
||||||
|
|
||||||
|
@Schema(description = "分享数")
|
||||||
@TableField("share_count")
|
@TableField("share_count")
|
||||||
private Integer shareCount;
|
private Integer shareCount;
|
||||||
|
|
||||||
|
@Schema(description = "原图URL")
|
||||||
@TableField("original_image_url")
|
@TableField("original_image_url")
|
||||||
private String originalImageUrl;
|
private String originalImageUrl;
|
||||||
|
|
||||||
|
@Schema(description = "语音输入URL")
|
||||||
@TableField("voice_input_url")
|
@TableField("voice_input_url")
|
||||||
private String voiceInputUrl;
|
private String voiceInputUrl;
|
||||||
|
|
||||||
|
@Schema(description = "文本输入")
|
||||||
@TableField("text_input")
|
@TableField("text_input")
|
||||||
private String textInput;
|
private String textInput;
|
||||||
|
|
||||||
|
@Schema(description = "AI元数据(JSON)")
|
||||||
@TableField(value = "ai_meta", typeHandler = JacksonTypeHandler.class)
|
@TableField(value = "ai_meta", typeHandler = JacksonTypeHandler.class)
|
||||||
private Object aiMeta;
|
private Object aiMeta;
|
||||||
|
|
||||||
|
@Schema(description = "AI创作进度百分比")
|
||||||
|
private Integer progress;
|
||||||
|
|
||||||
|
@Schema(description = "进度描述")
|
||||||
|
@TableField("progress_message")
|
||||||
|
private String progressMessage;
|
||||||
|
|
||||||
|
@Schema(description = "创作风格")
|
||||||
|
private String style;
|
||||||
|
|
||||||
|
@Schema(description = "作者")
|
||||||
|
@TableField("author_name")
|
||||||
|
private String authorName;
|
||||||
|
|
||||||
|
@Schema(description = "失败原因")
|
||||||
|
@TableField("fail_reason")
|
||||||
|
private String failReason;
|
||||||
|
|
||||||
|
@Schema(description = "发布时间")
|
||||||
@TableField("publish_time")
|
@TableField("publish_time")
|
||||||
private LocalDateTime publishTime;
|
private LocalDateTime publishTime;
|
||||||
|
|
||||||
|
@Schema(description = "是否删除:0-未删除,1-已删除")
|
||||||
@TableField("is_deleted")
|
@TableField("is_deleted")
|
||||||
private Integer isDeleted;
|
private Integer isDeleted;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
@TableField("create_time")
|
@TableField("create_time")
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
@Schema(description = "修改时间")
|
||||||
@TableField("modify_time")
|
@TableField("modify_time")
|
||||||
private LocalDateTime modifyTime;
|
private LocalDateTime modifyTime;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.IdType;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
@ -11,24 +12,32 @@ import java.time.LocalDateTime;
|
|||||||
|
|
||||||
@Data
|
@Data
|
||||||
@TableName("t_ugc_work_comment")
|
@TableName("t_ugc_work_comment")
|
||||||
|
@Schema(description = "作品评论实体")
|
||||||
public class UgcWorkComment implements Serializable {
|
public class UgcWorkComment implements Serializable {
|
||||||
|
|
||||||
|
@Schema(description = "主键ID")
|
||||||
@TableId(type = IdType.AUTO)
|
@TableId(type = IdType.AUTO)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "作品ID")
|
||||||
@TableField("work_id")
|
@TableField("work_id")
|
||||||
private Long workId;
|
private Long workId;
|
||||||
|
|
||||||
|
@Schema(description = "用户ID")
|
||||||
@TableField("user_id")
|
@TableField("user_id")
|
||||||
private Long userId;
|
private Long userId;
|
||||||
|
|
||||||
|
@Schema(description = "父评论ID")
|
||||||
@TableField("parent_id")
|
@TableField("parent_id")
|
||||||
private Long parentId;
|
private Long parentId;
|
||||||
|
|
||||||
|
@Schema(description = "评论内容")
|
||||||
private String content;
|
private String content;
|
||||||
|
|
||||||
|
@Schema(description = "状态")
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
@TableField("create_time")
|
@TableField("create_time")
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.IdType;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
@ -11,17 +12,22 @@ import java.time.LocalDateTime;
|
|||||||
|
|
||||||
@Data
|
@Data
|
||||||
@TableName("t_ugc_work_favorite")
|
@TableName("t_ugc_work_favorite")
|
||||||
|
@Schema(description = "作品收藏实体")
|
||||||
public class UgcWorkFavorite implements Serializable {
|
public class UgcWorkFavorite implements Serializable {
|
||||||
|
|
||||||
|
@Schema(description = "主键ID")
|
||||||
@TableId(type = IdType.AUTO)
|
@TableId(type = IdType.AUTO)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "用户ID")
|
||||||
@TableField("user_id")
|
@TableField("user_id")
|
||||||
private Long userId;
|
private Long userId;
|
||||||
|
|
||||||
|
@Schema(description = "作品ID")
|
||||||
@TableField("work_id")
|
@TableField("work_id")
|
||||||
private Long workId;
|
private Long workId;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
@TableField("create_time")
|
@TableField("create_time")
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.IdType;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
@ -11,17 +12,22 @@ import java.time.LocalDateTime;
|
|||||||
|
|
||||||
@Data
|
@Data
|
||||||
@TableName("t_ugc_work_like")
|
@TableName("t_ugc_work_like")
|
||||||
|
@Schema(description = "作品点赞实体")
|
||||||
public class UgcWorkLike implements Serializable {
|
public class UgcWorkLike implements Serializable {
|
||||||
|
|
||||||
|
@Schema(description = "主键ID")
|
||||||
@TableId(type = IdType.AUTO)
|
@TableId(type = IdType.AUTO)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "用户ID")
|
||||||
@TableField("user_id")
|
@TableField("user_id")
|
||||||
private Long userId;
|
private Long userId;
|
||||||
|
|
||||||
|
@Schema(description = "作品ID")
|
||||||
@TableField("work_id")
|
@TableField("work_id")
|
||||||
private Long workId;
|
private Long workId;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
@TableField("create_time")
|
@TableField("create_time")
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,28 +4,36 @@ import com.baomidou.mybatisplus.annotation.IdType;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@TableName("t_ugc_work_page")
|
@TableName("t_ugc_work_page")
|
||||||
|
@Schema(description = "作品页面实体")
|
||||||
public class UgcWorkPage implements Serializable {
|
public class UgcWorkPage implements Serializable {
|
||||||
|
|
||||||
|
@Schema(description = "主键ID")
|
||||||
@TableId(type = IdType.AUTO)
|
@TableId(type = IdType.AUTO)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "作品ID")
|
||||||
@TableField("work_id")
|
@TableField("work_id")
|
||||||
private Long workId;
|
private Long workId;
|
||||||
|
|
||||||
|
@Schema(description = "页码")
|
||||||
@TableField("page_no")
|
@TableField("page_no")
|
||||||
private Integer pageNo;
|
private Integer pageNo;
|
||||||
|
|
||||||
|
@Schema(description = "图片URL")
|
||||||
@TableField("image_url")
|
@TableField("image_url")
|
||||||
private String imageUrl;
|
private String imageUrl;
|
||||||
|
|
||||||
|
@Schema(description = "文本内容")
|
||||||
private String text;
|
private String text;
|
||||||
|
|
||||||
|
@Schema(description = "音频URL")
|
||||||
@TableField("audio_url")
|
@TableField("audio_url")
|
||||||
private String audioUrl;
|
private String audioUrl;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.IdType;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
@ -11,41 +12,55 @@ import java.time.LocalDateTime;
|
|||||||
|
|
||||||
@Data
|
@Data
|
||||||
@TableName("t_ugc_work_report")
|
@TableName("t_ugc_work_report")
|
||||||
|
@Schema(description = "作品举报实体")
|
||||||
public class UgcWorkReport implements Serializable {
|
public class UgcWorkReport implements Serializable {
|
||||||
|
|
||||||
|
@Schema(description = "主键ID")
|
||||||
@TableId(type = IdType.AUTO)
|
@TableId(type = IdType.AUTO)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "举报人ID")
|
||||||
@TableField("reporter_id")
|
@TableField("reporter_id")
|
||||||
private Long reporterId;
|
private Long reporterId;
|
||||||
|
|
||||||
|
@Schema(description = "目标类型")
|
||||||
@TableField("target_type")
|
@TableField("target_type")
|
||||||
private String targetType;
|
private String targetType;
|
||||||
|
|
||||||
|
@Schema(description = "目标ID")
|
||||||
@TableField("target_id")
|
@TableField("target_id")
|
||||||
private Long targetId;
|
private Long targetId;
|
||||||
|
|
||||||
|
@Schema(description = "被举报用户ID")
|
||||||
@TableField("target_user_id")
|
@TableField("target_user_id")
|
||||||
private Long targetUserId;
|
private Long targetUserId;
|
||||||
|
|
||||||
|
@Schema(description = "举报原因")
|
||||||
private String reason;
|
private String reason;
|
||||||
|
|
||||||
|
@Schema(description = "举报描述")
|
||||||
private String description;
|
private String description;
|
||||||
|
|
||||||
|
@Schema(description = "状态")
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
|
@Schema(description = "处理动作")
|
||||||
@TableField("handle_action")
|
@TableField("handle_action")
|
||||||
private String handleAction;
|
private String handleAction;
|
||||||
|
|
||||||
|
@Schema(description = "处理备注")
|
||||||
@TableField("handle_note")
|
@TableField("handle_note")
|
||||||
private String handleNote;
|
private String handleNote;
|
||||||
|
|
||||||
|
@Schema(description = "处理人ID")
|
||||||
@TableField("handler_id")
|
@TableField("handler_id")
|
||||||
private Long handlerId;
|
private Long handlerId;
|
||||||
|
|
||||||
|
@Schema(description = "处理时间")
|
||||||
@TableField("handle_time")
|
@TableField("handle_time")
|
||||||
private LocalDateTime handleTime;
|
private LocalDateTime handleTime;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
@TableField("create_time")
|
@TableField("create_time")
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,20 +4,25 @@ import com.baomidou.mybatisplus.annotation.IdType;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@TableName("t_ugc_work_tag")
|
@TableName("t_ugc_work_tag")
|
||||||
|
@Schema(description = "作品标签关联实体")
|
||||||
public class UgcWorkTag implements Serializable {
|
public class UgcWorkTag implements Serializable {
|
||||||
|
|
||||||
|
@Schema(description = "主键ID")
|
||||||
@TableId(type = IdType.AUTO)
|
@TableId(type = IdType.AUTO)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "作品ID")
|
||||||
@TableField("work_id")
|
@TableField("work_id")
|
||||||
private Long workId;
|
private Long workId;
|
||||||
|
|
||||||
|
@Schema(description = "标签ID")
|
||||||
@TableField("tag_id")
|
@TableField("tag_id")
|
||||||
private Long tagId;
|
private Long tagId;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.IdType;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
@ -12,35 +13,48 @@ import java.time.LocalDateTime;
|
|||||||
|
|
||||||
@Data
|
@Data
|
||||||
@TableName("t_user_child")
|
@TableName("t_user_child")
|
||||||
|
@Schema(description = "用户子女实体")
|
||||||
public class UserChild implements Serializable {
|
public class UserChild implements Serializable {
|
||||||
|
|
||||||
|
@Schema(description = "主键ID")
|
||||||
@TableId(type = IdType.AUTO)
|
@TableId(type = IdType.AUTO)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "家长ID")
|
||||||
@TableField("parent_id")
|
@TableField("parent_id")
|
||||||
private Long parentId;
|
private Long parentId;
|
||||||
|
|
||||||
|
@Schema(description = "姓名")
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
|
@Schema(description = "性别")
|
||||||
private String gender;
|
private String gender;
|
||||||
|
|
||||||
|
@Schema(description = "出生日期")
|
||||||
private LocalDate birthday;
|
private LocalDate birthday;
|
||||||
|
|
||||||
|
@Schema(description = "年级")
|
||||||
private String grade;
|
private String grade;
|
||||||
|
|
||||||
|
@Schema(description = "城市")
|
||||||
private String city;
|
private String city;
|
||||||
|
|
||||||
|
@Schema(description = "学校名称")
|
||||||
@TableField("school_name")
|
@TableField("school_name")
|
||||||
private String schoolName;
|
private String schoolName;
|
||||||
|
|
||||||
|
@Schema(description = "头像")
|
||||||
private String avatar;
|
private String avatar;
|
||||||
|
|
||||||
|
@Schema(description = "是否删除:0-未删除,1-已删除")
|
||||||
@TableField("is_deleted")
|
@TableField("is_deleted")
|
||||||
private Integer isDeleted;
|
private Integer isDeleted;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
@TableField("create_time")
|
@TableField("create_time")
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
@Schema(description = "修改时间")
|
||||||
@TableField("modify_time")
|
@TableField("modify_time")
|
||||||
private LocalDateTime modifyTime;
|
private LocalDateTime modifyTime;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.IdType;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
@ -11,22 +12,29 @@ import java.time.LocalDateTime;
|
|||||||
|
|
||||||
@Data
|
@Data
|
||||||
@TableName("t_user_parent_child")
|
@TableName("t_user_parent_child")
|
||||||
|
@Schema(description = "家长子女关联实体")
|
||||||
public class UserParentChild implements Serializable {
|
public class UserParentChild implements Serializable {
|
||||||
|
|
||||||
|
@Schema(description = "主键ID")
|
||||||
@TableId(type = IdType.AUTO)
|
@TableId(type = IdType.AUTO)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "家长用户ID")
|
||||||
@TableField("parent_user_id")
|
@TableField("parent_user_id")
|
||||||
private Long parentUserId;
|
private Long parentUserId;
|
||||||
|
|
||||||
|
@Schema(description = "子女用户ID")
|
||||||
@TableField("child_user_id")
|
@TableField("child_user_id")
|
||||||
private Long childUserId;
|
private Long childUserId;
|
||||||
|
|
||||||
|
@Schema(description = "关系")
|
||||||
private String relationship;
|
private String relationship;
|
||||||
|
|
||||||
|
@Schema(description = "管控模式")
|
||||||
@TableField("control_mode")
|
@TableField("control_mode")
|
||||||
private String controlMode;
|
private String controlMode;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
@TableField("create_time")
|
@TableField("create_time")
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,6 +53,8 @@ public class SecurityConfig {
|
|||||||
.requestMatchers(HttpMethod.GET, "/public/gallery", "/public/gallery/**").permitAll()
|
.requestMatchers(HttpMethod.GET, "/public/gallery", "/public/gallery/**").permitAll()
|
||||||
.requestMatchers(HttpMethod.GET, "/public/tags", "/public/tags/**").permitAll()
|
.requestMatchers(HttpMethod.GET, "/public/tags", "/public/tags/**").permitAll()
|
||||||
.requestMatchers(HttpMethod.GET, "/public/users/*/works").permitAll()
|
.requestMatchers(HttpMethod.GET, "/public/users/*/works").permitAll()
|
||||||
|
// 乐读派 Webhook 回调(无用户上下文,由乐读派服务端调用)
|
||||||
|
.requestMatchers("/webhook/leai").permitAll()
|
||||||
// Knife4j 文档
|
// Knife4j 文档
|
||||||
.requestMatchers("/doc.html", "/webjars/**", "/v3/api-docs/**", "/swagger-resources/**").permitAll()
|
.requestMatchers("/doc.html", "/webjars/**", "/v3/api-docs/**", "/swagger-resources/**").permitAll()
|
||||||
// Druid 监控
|
// Druid 监控
|
||||||
|
|||||||
@ -35,7 +35,7 @@ mybatis-plus:
|
|||||||
|
|
||||||
oss:
|
oss:
|
||||||
secret-id: ${COS_SECRET_ID:}
|
secret-id: ${COS_SECRET_ID:}
|
||||||
secret-key: ${COS_SECRET_KEY:}
|
secret-key: ${COS_SECRET_KEY:},
|
||||||
bucket: ${COS_BUCKET:}
|
bucket: ${COS_BUCKET:}
|
||||||
region: ${COS_REGION:ap-guangzhou}
|
region: ${COS_REGION:ap-guangzhou}
|
||||||
url-prefix: ${COS_URL_PREFIX:}
|
url-prefix: ${COS_URL_PREFIX:}
|
||||||
@ -43,3 +43,10 @@ oss:
|
|||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
com.competition: debug
|
com.competition: debug
|
||||||
|
|
||||||
|
# 乐读派 AI 创作系统配置
|
||||||
|
leai:
|
||||||
|
org-id: ${LEAI_ORG_ID:gdlib}
|
||||||
|
app-secret: ${LEAI_APP_SECRET:leai_mnoi9q1a_mtcawrn8y}
|
||||||
|
api-url: ${LEAI_API_URL:http://192.168.1.72:8080}
|
||||||
|
h5-url: ${LEAI_H5_URL:http://192.168.1.72:3001}
|
||||||
|
|||||||
52
backend-java/src/main/resources/application-test.yml
Normal file
52
backend-java/src/main/resources/application-test.yml
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
spring:
|
||||||
|
datasource:
|
||||||
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
|
url: jdbc:mysql://192.168.1.250:3306/competition_management?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||||
|
username: lesingle-creation-test
|
||||||
|
password: 8ErFZiPBGbyrTHsy
|
||||||
|
type: com.alibaba.druid.pool.DruidDataSource
|
||||||
|
druid:
|
||||||
|
initial-size: 5
|
||||||
|
min-idle: 5
|
||||||
|
max-active: 20
|
||||||
|
max-wait: 60000
|
||||||
|
|
||||||
|
data:
|
||||||
|
redis:
|
||||||
|
host: ${REDIS_HOST:192.168.1.250}
|
||||||
|
port: ${REDIS_PORT:6379}
|
||||||
|
password: ${REDIS_PASSWORD:QWErty123}
|
||||||
|
database: ${REDIS_DB:8}
|
||||||
|
timeout: 5000ms
|
||||||
|
lettuce:
|
||||||
|
pool:
|
||||||
|
max-active: 20
|
||||||
|
max-idle: 20
|
||||||
|
min-idle: 5
|
||||||
|
max-wait: -1ms
|
||||||
|
|
||||||
|
flyway:
|
||||||
|
clean-disabled: false
|
||||||
|
|
||||||
|
# 开发环境开启 SQL 日志
|
||||||
|
mybatis-plus:
|
||||||
|
configuration:
|
||||||
|
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||||
|
|
||||||
|
oss:
|
||||||
|
secret-id: ${COS_SECRET_ID:}
|
||||||
|
secret-key: ${COS_SECRET_KEY:}
|
||||||
|
bucket: ${COS_BUCKET:}
|
||||||
|
region: ${COS_REGION:ap-guangzhou}
|
||||||
|
url-prefix: ${COS_URL_PREFIX:}
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level:
|
||||||
|
com.competition: debug
|
||||||
|
|
||||||
|
# 乐读派 AI 创作系统配置
|
||||||
|
leai:
|
||||||
|
org-id: ${LEAI_ORG_ID:gdlib}
|
||||||
|
app-secret: ${LEAI_APP_SECRET:leai_mnoi9q1a_mtcawrn8y}
|
||||||
|
api-url: ${LEAI_API_URL:http://192.168.1.72:8080}
|
||||||
|
h5-url: ${LEAI_H5_URL:http://192.168.1.72:3001}
|
||||||
@ -15,7 +15,7 @@ spring:
|
|||||||
|
|
||||||
# Flyway 数据库迁移
|
# Flyway 数据库迁移
|
||||||
flyway:
|
flyway:
|
||||||
enabled: false
|
enabled: true
|
||||||
locations: classpath:db/migration
|
locations: classpath:db/migration
|
||||||
baseline-on-migrate: true
|
baseline-on-migrate: true
|
||||||
baseline-version: 0
|
baseline-version: 0
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -1,21 +0,0 @@
|
|||||||
-- Flyway migration:
|
|
||||||
-- 兼容部分环境没有跑过最新的 init.sql,
|
|
||||||
-- 导致 t_biz_contest_work 缺少 deleted 字段,从而逻辑删除查询报错。
|
|
||||||
SET @column_cnt := (
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE table_schema = DATABASE()
|
|
||||||
AND table_name = 't_biz_contest_work'
|
|
||||||
AND column_name = 'deleted'
|
|
||||||
);
|
|
||||||
|
|
||||||
SET @sql := IF(
|
|
||||||
@column_cnt = 0,
|
|
||||||
'ALTER TABLE t_biz_contest_work ADD COLUMN deleted tinyint NOT NULL DEFAULT ''0'' COMMENT ''逻辑删除:0-未删除,1-已删除''',
|
|
||||||
'SELECT 1'
|
|
||||||
);
|
|
||||||
|
|
||||||
PREPARE stmt FROM @sql;
|
|
||||||
EXECUTE stmt;
|
|
||||||
DEALLOCATE PREPARE stmt;
|
|
||||||
|
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
-- 活动公告表添加租户隔离字段
|
||||||
|
-- 执行时间:2026-04-03
|
||||||
|
|
||||||
|
-- 1. 添加 tenant_id 字段
|
||||||
|
ALTER TABLE t_biz_contest_notice
|
||||||
|
ADD COLUMN tenant_id BIGINT COMMENT '租户 ID(用于租户隔离)' AFTER contest_id;
|
||||||
|
|
||||||
|
-- 2. 为现有数据设置默认租户 ID(根据实际业务,可能需要手动设置)
|
||||||
|
-- 假设现有公告都属于当前租户(需要根据实际情况修改 tenant_id)
|
||||||
|
UPDATE t_biz_contest_notice
|
||||||
|
SET tenant_id = 9
|
||||||
|
WHERE tenant_id IS NULL;
|
||||||
|
|
||||||
|
-- 3. 添加索引优化查询性能
|
||||||
|
ALTER TABLE t_biz_contest_notice
|
||||||
|
ADD INDEX idx_tenant_id (tenant_id);
|
||||||
|
|
||||||
|
-- 4. 添加联合索引优化按租户和赛事查询
|
||||||
|
ALTER TABLE t_biz_contest_notice
|
||||||
|
ADD INDEX idx_tenant_contest (tenant_id, contest_id);
|
||||||
@ -0,0 +1,135 @@
|
|||||||
|
-- 评审模块重建表脚本(修复 V3 建表字段不全问题)
|
||||||
|
-- 执行时间:2026-04-07
|
||||||
|
-- 对齐实体:BizContestJudge, BizContestReviewRule, BizContestWorkJudgeAssignment, BizContestWorkScore, BizPresetComment
|
||||||
|
-- 使用 DROP + CREATE 确保表结构完整,因为评审模块尚未有生产数据
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 1. 赛事评委表(对应实体 BizContestJudge,继承 BaseEntity)
|
||||||
|
-- ============================================================
|
||||||
|
DROP TABLE IF EXISTS t_biz_contest_judge;
|
||||||
|
CREATE TABLE t_biz_contest_judge (
|
||||||
|
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
|
contest_id BIGINT NOT NULL COMMENT '赛事ID',
|
||||||
|
judge_id BIGINT NOT NULL COMMENT '评委用户ID',
|
||||||
|
specialty VARCHAR(100) DEFAULT NULL COMMENT '专业领域',
|
||||||
|
weight DECIMAL(3,2) DEFAULT 1.00 COMMENT '评委权重 0-1',
|
||||||
|
description VARCHAR(500) DEFAULT NULL COMMENT '评委描述',
|
||||||
|
-- BaseEntity 审计字段
|
||||||
|
create_by VARCHAR(64) DEFAULT NULL COMMENT '创建人账号',
|
||||||
|
update_by VARCHAR(64) DEFAULT NULL COMMENT '更新人账号',
|
||||||
|
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除:0-未删除,1-已删除',
|
||||||
|
creator INT DEFAULT NULL COMMENT '创建人ID',
|
||||||
|
modifier INT DEFAULT NULL COMMENT '修改人ID',
|
||||||
|
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
modify_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
|
||||||
|
valid_state TINYINT NOT NULL DEFAULT 1 COMMENT '有效状态:1-有效,2-失效',
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
INDEX idx_contest_id (contest_id),
|
||||||
|
INDEX idx_judge_id (judge_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='赛事评委表';
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 2. 评审规则表(对应实体 BizContestReviewRule,继承 BaseEntity)
|
||||||
|
-- ============================================================
|
||||||
|
DROP TABLE IF EXISTS t_biz_contest_review_rule;
|
||||||
|
CREATE TABLE t_biz_contest_review_rule (
|
||||||
|
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
|
tenant_id BIGINT DEFAULT NULL COMMENT '租户ID',
|
||||||
|
rule_name VARCHAR(200) NOT NULL COMMENT '规则名称',
|
||||||
|
rule_description VARCHAR(500) DEFAULT NULL COMMENT '规则描述',
|
||||||
|
judge_count INT NOT NULL DEFAULT 3 COMMENT '评委数量',
|
||||||
|
dimensions JSON DEFAULT NULL COMMENT '评分维度 JSON [{name,percentage,description}]',
|
||||||
|
calculation_rule VARCHAR(50) NOT NULL DEFAULT 'average' COMMENT '计算规则:average/remove_max_min/remove_min/max/weighted',
|
||||||
|
-- BaseEntity 审计字段
|
||||||
|
create_by VARCHAR(64) DEFAULT NULL COMMENT '创建人账号',
|
||||||
|
update_by VARCHAR(64) DEFAULT NULL COMMENT '更新人账号',
|
||||||
|
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除:0-未删除,1-已删除',
|
||||||
|
creator INT DEFAULT NULL COMMENT '创建人ID',
|
||||||
|
modifier INT DEFAULT NULL COMMENT '修改人ID',
|
||||||
|
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
modify_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
|
||||||
|
valid_state TINYINT NOT NULL DEFAULT 1 COMMENT '有效状态:1-有效,2-失效',
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
INDEX idx_tenant_id (tenant_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='评审规则表';
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 3. 作品评委分配表(对应实体 BizContestWorkJudgeAssignment,不继承 BaseEntity)
|
||||||
|
-- 注意:该实体只有 id/creator/modifier/createTime/modifyTime,无 deleted/valid_state/create_by/update_by
|
||||||
|
-- ============================================================
|
||||||
|
DROP TABLE IF EXISTS t_biz_contest_work_judge_assignment;
|
||||||
|
CREATE TABLE t_biz_contest_work_judge_assignment (
|
||||||
|
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
|
contest_id BIGINT NOT NULL COMMENT '赛事ID',
|
||||||
|
work_id BIGINT NOT NULL COMMENT '作品ID',
|
||||||
|
judge_id BIGINT NOT NULL COMMENT '评委用户ID',
|
||||||
|
assignment_time DATETIME DEFAULT NULL COMMENT '分配时间',
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'assigned' COMMENT '状态:assigned/reviewing/completed',
|
||||||
|
creator INT DEFAULT NULL COMMENT '创建人ID',
|
||||||
|
modifier INT DEFAULT NULL COMMENT '修改人ID',
|
||||||
|
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
modify_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
INDEX idx_contest_work (contest_id, work_id),
|
||||||
|
INDEX idx_judge_status (judge_id, status),
|
||||||
|
UNIQUE INDEX uk_contest_work_judge (contest_id, work_id, judge_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='作品评委分配表';
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 4. 作品评分表(对应实体 BizContestWorkScore,继承 BaseEntity)
|
||||||
|
-- ============================================================
|
||||||
|
DROP TABLE IF EXISTS t_biz_contest_work_score;
|
||||||
|
CREATE TABLE t_biz_contest_work_score (
|
||||||
|
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
|
tenant_id BIGINT DEFAULT NULL COMMENT '租户ID',
|
||||||
|
contest_id BIGINT NOT NULL COMMENT '赛事ID',
|
||||||
|
work_id BIGINT NOT NULL COMMENT '作品ID',
|
||||||
|
assignment_id BIGINT DEFAULT NULL COMMENT '分配记录ID',
|
||||||
|
judge_id BIGINT NOT NULL COMMENT '评委用户ID',
|
||||||
|
judge_name VARCHAR(100) DEFAULT NULL COMMENT '评委姓名',
|
||||||
|
dimension_scores JSON DEFAULT NULL COMMENT '维度评分 JSON [{dimension,score}]',
|
||||||
|
total_score DECIMAL(10,2) DEFAULT NULL COMMENT '总分',
|
||||||
|
comments TEXT DEFAULT NULL COMMENT '评语',
|
||||||
|
score_time DATETIME DEFAULT NULL COMMENT '评分时间',
|
||||||
|
-- BaseEntity 审计字段
|
||||||
|
create_by VARCHAR(64) DEFAULT NULL COMMENT '创建人账号',
|
||||||
|
update_by VARCHAR(64) DEFAULT NULL COMMENT '更新人账号',
|
||||||
|
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除:0-未删除,1-已删除',
|
||||||
|
creator INT DEFAULT NULL COMMENT '创建人ID',
|
||||||
|
modifier INT DEFAULT NULL COMMENT '修改人ID',
|
||||||
|
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
modify_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
|
||||||
|
valid_state TINYINT NOT NULL DEFAULT 1 COMMENT '有效状态:1-有效,2-失效',
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
INDEX idx_work_id (work_id),
|
||||||
|
INDEX idx_contest_id (contest_id),
|
||||||
|
INDEX idx_judge_id (judge_id),
|
||||||
|
INDEX idx_assignment_id (assignment_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='作品评分表';
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 5. 预设评语表(对应实体 BizPresetComment,继承 BaseEntity)
|
||||||
|
-- ============================================================
|
||||||
|
DROP TABLE IF EXISTS t_biz_preset_comment;
|
||||||
|
CREATE TABLE t_biz_preset_comment (
|
||||||
|
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
|
contest_id BIGINT DEFAULT NULL COMMENT '赛事ID(null 表示全局)',
|
||||||
|
judge_id BIGINT DEFAULT NULL COMMENT '评委ID(null 表示通用)',
|
||||||
|
content VARCHAR(1000) NOT NULL COMMENT '评语文本',
|
||||||
|
category VARCHAR(50) DEFAULT NULL COMMENT '评语分类',
|
||||||
|
score DECIMAL(10,2) DEFAULT NULL COMMENT '关联分数',
|
||||||
|
sort_order INT DEFAULT 0 COMMENT '排序',
|
||||||
|
use_count INT NOT NULL DEFAULT 0 COMMENT '使用次数',
|
||||||
|
-- BaseEntity 审计字段
|
||||||
|
create_by VARCHAR(64) DEFAULT NULL COMMENT '创建人账号',
|
||||||
|
update_by VARCHAR(64) DEFAULT NULL COMMENT '更新人账号',
|
||||||
|
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除:0-未删除,1-已删除',
|
||||||
|
creator INT DEFAULT NULL COMMENT '创建人ID',
|
||||||
|
modifier INT DEFAULT NULL COMMENT '修改人ID',
|
||||||
|
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
modify_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
|
||||||
|
valid_state TINYINT NOT NULL DEFAULT 1 COMMENT '有效状态:1-有效,2-失效',
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
INDEX idx_contest_id (contest_id),
|
||||||
|
INDEX idx_judge_id (judge_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='预设评语表';
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- V5: 乐读派 AI 绘本创作系统集成
|
||||||
|
-- 1. t_ugc_work:status VARCHAR → INT(先转换旧数据再改类型)
|
||||||
|
-- 2. 新增乐读派关联字段
|
||||||
|
-- 3. 新增索引
|
||||||
|
-- 4. Webhook 幂等去重表
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- 1. t_ugc_work:status VARCHAR → INT
|
||||||
|
-- 先将旧字符串状态值转换为整数值
|
||||||
|
UPDATE t_ugc_work SET status = '0' WHERE status = 'draft';
|
||||||
|
UPDATE t_ugc_work SET status = '1' WHERE status = 'pending_review';
|
||||||
|
UPDATE t_ugc_work SET status = '2' WHERE status = 'processing';
|
||||||
|
UPDATE t_ugc_work SET status = '3' WHERE status = 'published';
|
||||||
|
UPDATE t_ugc_work SET status = '-1' WHERE status = 'rejected';
|
||||||
|
UPDATE t_ugc_work SET status = '-2' WHERE status = 'taken_down';
|
||||||
|
-- 其他未识别的值统一设为 0(DRAFT)
|
||||||
|
UPDATE t_ugc_work SET status = '0' WHERE status NOT REGEXP '^-?[0-9]+$';
|
||||||
|
|
||||||
|
ALTER TABLE t_ugc_work MODIFY COLUMN status INT NOT NULL DEFAULT 0
|
||||||
|
COMMENT '创作状态:-1=FAILED, 0=DRAFT, 1=PENDING, 2=PROCESSING, 3=COMPLETED, 4=CATALOGED, 5=DUBBED';
|
||||||
|
|
||||||
|
-- 2. 新增乐读派关联字段
|
||||||
|
ALTER TABLE t_ugc_work ADD COLUMN remote_work_id VARCHAR(64) DEFAULT NULL COMMENT '乐读派远程作品ID' AFTER user_id;
|
||||||
|
ALTER TABLE t_ugc_work ADD COLUMN progress INT DEFAULT 0 COMMENT 'AI创作进度百分比' AFTER ai_meta;
|
||||||
|
ALTER TABLE t_ugc_work ADD COLUMN progress_message VARCHAR(200) DEFAULT NULL COMMENT '进度描述' AFTER progress;
|
||||||
|
ALTER TABLE t_ugc_work ADD COLUMN style VARCHAR(100) DEFAULT NULL COMMENT '创作风格' AFTER progress_message;
|
||||||
|
ALTER TABLE t_ugc_work ADD COLUMN author_name VARCHAR(100) DEFAULT NULL COMMENT '作者' AFTER style;
|
||||||
|
ALTER TABLE t_ugc_work ADD COLUMN fail_reason VARCHAR(500) DEFAULT NULL COMMENT '失败原因' AFTER author_name;
|
||||||
|
|
||||||
|
-- 3. 新增索引
|
||||||
|
ALTER TABLE t_ugc_work ADD UNIQUE INDEX uk_remote_work_id (remote_work_id);
|
||||||
|
ALTER TABLE t_ugc_work ADD INDEX idx_user_status (user_id, status);
|
||||||
|
|
||||||
|
-- 4. Webhook 幂等去重表
|
||||||
|
CREATE TABLE IF NOT EXISTS t_leai_webhook_event (
|
||||||
|
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
|
event_id VARCHAR(128) NOT NULL COMMENT '事件唯一ID (X-Webhook-Id)',
|
||||||
|
event_type VARCHAR(64) NOT NULL COMMENT '事件类型',
|
||||||
|
remote_work_id VARCHAR(64) DEFAULT NULL COMMENT '乐读派作品ID',
|
||||||
|
payload JSON DEFAULT NULL COMMENT '事件原始载荷',
|
||||||
|
processed TINYINT NOT NULL DEFAULT 1 COMMENT '是否已处理',
|
||||||
|
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE INDEX uk_event_id (event_id),
|
||||||
|
INDEX idx_remote_work_id (remote_work_id),
|
||||||
|
INDEX idx_create_time (create_time)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='乐读派Webhook事件去重表';
|
||||||
@ -31,4 +31,6 @@
|
|||||||
|
|
||||||
## 评委端
|
## 评委端
|
||||||
|
|
||||||
(暂无)
|
| 文档 | 模块 | 状态 | 日期 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| [评审任务](./judge-portal/review-tasks.md) | 评审任务 / 作品列表 | 已实现 | 2026-04-08 |
|
||||||
|
|||||||
60
docs/design/judge-portal/review-tasks.md
Normal file
60
docs/design/judge-portal/review-tasks.md
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
# 评委端:评审任务
|
||||||
|
|
||||||
|
> 所属端:评委端(`tenant_id` 对应评委租户,如 `code=judge`)
|
||||||
|
> 菜单与定位见 [菜单配置说明](../menu-config.md) 中「评委端」章节:仅能查看**被分配**的活动与作品。
|
||||||
|
|
||||||
|
## 活动列表
|
||||||
|
|
||||||
|
**接口**:`GET /contests/reviews/judge/contests`
|
||||||
|
|
||||||
|
**活动来源(并集)**:
|
||||||
|
|
||||||
|
1. `t_biz_contest_judge` 中 `judge_id` 且 `valid_state=1` 的赛事(机构显式添加的评委);
|
||||||
|
2. `t_biz_contest_work_judge_assignment` 中该评委出现过的 `contest_id`(含仅通过作品分配参与的隐式场景)。
|
||||||
|
|
||||||
|
**响应字段(与前端 `activities/Review.vue` 对齐)**:
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `contestId` | 活动 ID |
|
||||||
|
| `contestName` | 活动名称 |
|
||||||
|
| `contestState` / `status` | 活动状态 |
|
||||||
|
| `reviewStartTime` / `reviewEndTime` | 评审时间窗 |
|
||||||
|
| `totalAssigned` | 该评委在该活动下的分配记录总数 |
|
||||||
|
| `reviewed` | 其中 `status=completed` 的数量(已提交评分) |
|
||||||
|
| `pending` | `totalAssigned - reviewed`(待评审) |
|
||||||
|
|
||||||
|
## 活动下作品列表
|
||||||
|
|
||||||
|
**接口**:`GET /contests/reviews/judge/contests/{contestId}/works`
|
||||||
|
|
||||||
|
**分配状态 `reviewStatus` 查询参数(与库表兼容)**:
|
||||||
|
|
||||||
|
- 库中 `t_biz_contest_work_judge_assignment.status` 实际使用:`assigned`(已分配未评完)、`completed`(已评审)。
|
||||||
|
- 前端下拉「未评审」传 `pending` → 后端按 **`status != completed`** 筛选。
|
||||||
|
- 前端「已评审」传 `reviewed` → 后端按 **`status = completed`** 筛选。
|
||||||
|
|
||||||
|
**作品编号**:`workNo` 为空时,前端可用作品 `workId` 展示兜底(如 `#123`)。
|
||||||
|
|
||||||
|
## 活动详情(含评审规则)
|
||||||
|
|
||||||
|
**接口**:`GET /contests/reviews/judge/contests/{contestId}/detail`
|
||||||
|
|
||||||
|
**权限**:满足以下**任一**即可:
|
||||||
|
|
||||||
|
- 存在有效的 `t_biz_contest_judge` 关联;或
|
||||||
|
- 存在该 `contestId` + `judgeId` 的作品分配记录。
|
||||||
|
|
||||||
|
避免「列表能进、详情 403」与隐式评委场景不一致。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 与租户端「评审进度」的口径对齐
|
||||||
|
|
||||||
|
| 维度 | 租户机构端 `contests/reviews/progress`(活动列表行) | 评委端 `activities/review`(上表) |
|
||||||
|
|------|------------------------------------------------------|-------------------------------------|
|
||||||
|
| 数据来源 | `GET /contests` 列表项中的 `reviewedCount` / `totalWorksCount` | `GET /contests/reviews/judge/contests` |
|
||||||
|
| 含义 | `totalWorksCount`:该活动最新有效作品总数。`reviewedCount`:**该活动下已分配评委且全部分配记录均为 `completed` 的作品数**(与分配表 `t_biz_contest_work_judge_assignment` 一致,与作品表 `accepted`/`awarded` 终态无关) | **评委维度**:`reviewed`/`totalAssigned`/`pending` 为该评委在分配表上的任务数;与租户「整作品是否全部评委评完」为不同聚合粒度 |
|
||||||
|
| 作品列表/详情 | `GET /contests/works` 每条作品含 `reviewedCount`/`totalJudgesCount`(按该作品分配条数统计) | 评委在单活动下作品列表同样基于分配 `completed` |
|
||||||
|
|
||||||
|
说明:顶部「作品统计」卡片若仍按作品 `status` 汇总,可能与逐活动行「分配完成作品数」不完全同数,属汇总维度不同;列表/详情/评委任务以分配表为准。
|
||||||
@ -1,6 +1,6 @@
|
|||||||
# 各端菜单配置规范
|
# 各端菜单配置规范
|
||||||
|
|
||||||
> 最后更新:2026-04-02
|
> 最后更新:2026-04-08
|
||||||
> 维护人:开发团队
|
> 维护人:开发团队
|
||||||
|
|
||||||
本文档记录各端的正确菜单配置,是菜单分配的**唯一权威来源**。修改菜单时必须对照此文档。
|
本文档记录各端的正确菜单配置,是菜单分配的**唯一权威来源**。修改菜单时必须对照此文档。
|
||||||
@ -112,6 +112,13 @@
|
|||||||
9, 10, 11, 12, 14, 15, 16, 20, 23, 24, 25, 26, 27, 50, 51, 52, 53, 54
|
9, 10, 11, 12, 14, 15, 16, 20, 23, 24, 25, 26, 27, 50, 51, 52, 53, 54
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 租户评委与平台评委(菜单一致)
|
||||||
|
|
||||||
|
- **平台评委**:在评委租户(`code=judge`)登录,见第三节「评委端」,`t_sys_tenant_menu` 仅含 **34、35、36**。
|
||||||
|
- **租户评委**:在机构租户(如 `tenantCode=test2`)登录,角色为 `judge`,与平台评委使用**同一套**「我的评审」菜单(仍为 **34、35、36** 对应的 `component`:`activities/Review`、`activities/PresetComments` 及父级)。
|
||||||
|
- **实现**:`GET /api/menus/user-menus` 在 `SysMenuServiceImpl.getUserMenus` 中,若当前用户角色含 `judge`,会在 `t_sys_tenant_menu` 基础上**合并**评委端菜单(按组件路径识别,不依赖固定 ID);评委角色权限由 `JudgeRolePermissionConfigurer` 与 `JudgesManagementServiceImpl` 保证与上表「评委端权限码」一致,以便 `/api/auth/user-info` 的 `permissions` 与菜单 `permission` 字段匹配。纯评委(仅有 `judge`、无 `tenant_admin`/`super_admin`)**不展示**机构端「评委管理」菜单(`component=contests/judges/Index`),避免与 `judge:read` 权限码重叠导致误显。
|
||||||
|
- **可选**:若希望机构租户在「菜单管理」中显式看到评委菜单,也可在 `t_sys_tenant_menu` 中手工追加 **34、35、36**(与第三节一致),与合并逻辑效果相同。
|
||||||
|
|
||||||
### 租户端系统设置不包含的子菜单
|
### 租户端系统设置不包含的子菜单
|
||||||
|
|
||||||
| 菜单 | ID | 原因 |
|
| 菜单 | ID | 原因 |
|
||||||
@ -130,7 +137,9 @@
|
|||||||
|
|
||||||
## 三、评委端(tenant_id=7, code='judge')— 3条
|
## 三、评委端(tenant_id=7, code='judge')— 3条
|
||||||
|
|
||||||
**定位**:评委评审工作台,只能看到自己被分配的活动和作品。
|
**定位**:评委评审工作台,只能看到自己被分配的活动和作品。租户评委(机构租户下的 `judge` 角色)与平台评委共用本节菜单与权限码。
|
||||||
|
|
||||||
|
**详细接口与字段说明**:[评委端评审任务](./judge-portal/review-tasks.md)。
|
||||||
|
|
||||||
**一级菜单**:1 个(我的评审)
|
**一级菜单**:1 个(我的评审)
|
||||||
|
|
||||||
@ -265,7 +274,7 @@
|
|||||||
```
|
```
|
||||||
1. 查询所有 valid_state=1 的菜单
|
1. 查询所有 valid_state=1 的菜单
|
||||||
2. 查询当前用户 tenant_id 对应的 t_sys_tenant_menu
|
2. 查询当前用户 tenant_id 对应的 t_sys_tenant_menu
|
||||||
3. 按 tenant_menus 过滤菜单
|
3. 按 tenant_menus 过滤菜单;若用户角色含 judge(且非超管),合并评委端菜单 ID(评审任务/预设评语及其父级,按 component 识别)
|
||||||
4. 如果是超管(isSuperAdmin):不做权限码过滤
|
4. 如果是超管(isSuperAdmin):不做权限码过滤
|
||||||
5. 如果是普通用户:按用户权限码过滤(菜单.permission 字段匹配用户 permissions)
|
5. 如果是普通用户:按用户权限码过滤(菜单.permission 字段匹配用户 permissions)
|
||||||
6. 补全父菜单(确保树结构完整)
|
6. 补全父菜单(确保树结构完整)
|
||||||
@ -274,6 +283,8 @@
|
|||||||
|
|
||||||
**⚠️ 重要**:超管也必须按 tenant_menus 过滤,不能返回全部菜单。之前的 bug 就是超管返回全部 52 个菜单导致错乱。
|
**⚠️ 重要**:超管也必须按 tenant_menus 过滤,不能返回全部菜单。之前的 bug 就是超管返回全部 52 个菜单导致错乱。
|
||||||
|
|
||||||
|
**评委角色**:`judge` 角色须绑定「评委端权限码」中的权限(见 `JudgeRolePermissionConfigurer`),否则 `permissions` 为空会导致第 5 步过滤掉所有菜单。
|
||||||
|
|
||||||
### 登录时租户识别
|
### 登录时租户识别
|
||||||
|
|
||||||
前端通过 URL 提取 tenantCode(如 `/gdlib/login` → `tenantCode=gdlib`),登录请求:
|
前端通过 URL 提取 tenantCode(如 `/gdlib/login` → `tenantCode=gdlib`),登录请求:
|
||||||
@ -295,6 +306,7 @@ Java 后端 `AuthService.login` 支持两种方式确定租户:
|
|||||||
| 来源 | 用途 |
|
| 来源 | 用途 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `docs/design/menu-config.md` | **本文档**,菜单配置唯一权威 |
|
| `docs/design/menu-config.md` | **本文档**,菜单配置唯一权威 |
|
||||||
|
| `backend-java/.../JudgeRolePermissionConfigurer.java` | 评委角色权限码补全、`judge`/`gdlib` 模板复制 |
|
||||||
| `backend/data/menus.json` | 菜单定义(所有菜单的字段) |
|
| `backend/data/menus.json` | 菜单定义(所有菜单的字段) |
|
||||||
| `backend/scripts/init-menus.ts` | 菜单初始化脚本(SUPER_TENANT_MENUS / NORMAL_TENANT_MENUS) |
|
| `backend/scripts/init-menus.ts` | 菜单初始化脚本(SUPER_TENANT_MENUS / NORMAL_TENANT_MENUS) |
|
||||||
| `t_sys_menu` 表 | 数据库中的菜单数据 |
|
| `t_sys_menu` 表 | 数据库中的菜单数据 |
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
> 所属端:租户端(机构管理员视角)
|
> 所属端:租户端(机构管理员视角)
|
||||||
> 状态:已优化
|
> 状态:已优化
|
||||||
> 创建日期:2026-03-31
|
> 创建日期:2026-03-31
|
||||||
> 最后更新:2026-03-31
|
> 最后更新:2026-04-08
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -58,6 +58,12 @@
|
|||||||
- [x] 主色调统一 #6366f1
|
- [x] 主色调统一 #6366f1
|
||||||
- [x] 冻结/解冻二次确认
|
- [x] 冻结/解冻二次确认
|
||||||
|
|
||||||
|
#### 赛事评委接口(`GET /contests/judges/contest/:id`)
|
||||||
|
|
||||||
|
- 响应为结构化对象,包含两部分:**`assigned`**(机构在该赛事下**显式添加**的评委,对应 `t_biz_contest_judge`,每条均有 `id`、`judgeId` 等)与 **`implicitPool`**(平台评委租户下对该赛事**默认可用**、尚未写入关联表的用户,`id` 为 null,`isPlatform` 为 true)。
|
||||||
|
- **添加评委抽屉**:「已选」回显与提交时的增删差集**仅基于 `assigned`**;可选评委仍来自评委管理分页接口。
|
||||||
|
- **作品分配**:可选评委池为 **`assigned` ∪ `implicitPool`**(前端合并);表格行键与选中状态统一使用 **`judgeId`**,与分配接口提交的 `judgeIds` 一致。
|
||||||
|
|
||||||
### 报名管理(Index)
|
### 报名管理(Index)
|
||||||
- [x] 去掉个人/团队 Tab,合并展示加类型列
|
- [x] 去掉个人/团队 Tab,合并展示加类型列
|
||||||
- [x] 统计概览(总报名/待审核/已通过/已拒绝)
|
- [x] 统计概览(总报名/待审核/已通过/已拒绝)
|
||||||
@ -93,6 +99,7 @@
|
|||||||
- [x] 评审状态改用实际完成率(无作品/未开始/进行中/已完成)
|
- [x] 评审状态改用实际完成率(无作品/未开始/进行中/已完成)
|
||||||
- [x] 进度数字颜色区分
|
- [x] 进度数字颜色区分
|
||||||
- [x] 评审进度详情页筛选修复(评审进度前端过滤生效)
|
- [x] 评审进度详情页筛选修复(评审进度前端过滤生效)
|
||||||
|
- 活动列表接口 `GET /contests` 为每行返回 `reviewedCount`(该活动下**已分配且全部分配均为 completed** 的作品数)与 `totalWorksCount`(最新有效作品总数),与分配表及评委端评审任务口径一致;见 [评委端评审任务](../judge-portal/review-tasks.md#与租户端评审进度的口径对齐)。
|
||||||
|
|
||||||
### 评审规则
|
### 评审规则
|
||||||
- [x] 组件映射修复
|
- [x] 组件映射修复
|
||||||
|
|||||||
@ -275,7 +275,7 @@ JWT 改造:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### P0-12. 活动提交联动
|
#### P0-12. 活动提交联动 ✅ 已实现 (2026-04-07)
|
||||||
|
|
||||||
**改动范围**:活动报名+提交流程改造
|
**改动范围**:活动报名+提交流程改造
|
||||||
|
|
||||||
@ -283,14 +283,39 @@ JWT 改造:
|
|||||||
后端改动:
|
后端改动:
|
||||||
├── POST /api/public/activities/:id/submit-work — 改造:支持从作品库选择作品
|
├── POST /api/public/activities/:id/submit-work — 改造:支持从作品库选择作品
|
||||||
│ 新增参数:userWorkId(用户作品ID)
|
│ 新增参数:userWorkId(用户作品ID)
|
||||||
│ 逻辑:根据 userWorkId 复制快照到 contest_works
|
│ 逻辑:根据 userWorkId 从 UgcWork + UgcWorkPage 复制快照到 ContestWork
|
||||||
└── contest_works 表 — 新增 user_work_id 字段
|
│ 校验:归属当前用户、未删除、非 rejected/taken_down 状态
|
||||||
|
└── contest_works 表 — user_work_id 字段已存在
|
||||||
|
|
||||||
前端改动:
|
前端改动:
|
||||||
├── 活动详情页 提交作品流程 — 改造:弹出作品库选择器,从"我的作品库"选择
|
├── ActivityDetail.vue — 替换文件上传弹窗为 WorkSelector 作品选择器
|
||||||
├── 作品库选择器组件 — 网格展示可选作品(已发布+私密均可选),确认后提交
|
├── WorkSelector.vue — 已有组件,新增 redirectUrl prop 支持创作后返回活动页
|
||||||
|
└── public.ts — submitWork API 新增 userWorkId 参数
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**快照字段映射**:
|
||||||
|
|
||||||
|
| UgcWork / UgcWorkPage 字段 | ContestWork 字段 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| title | title | 直接复制 |
|
||||||
|
| description | description | 直接复制 |
|
||||||
|
| coverUrl | previewUrl | 封面 → 预览图 |
|
||||||
|
| aiMeta | aiModelMeta | AI 元数据 |
|
||||||
|
| (所有 page.imageUrl) | previewUrls | 所有页面图片 URL 列表 |
|
||||||
|
| (所有 page 数据) | files | 分页快照 [{pageNo, imageUrl, text, audioUrl}] |
|
||||||
|
|
||||||
|
**用户交互流程**:
|
||||||
|
1. 用户在活动详情页点击"从作品库选择"
|
||||||
|
2. 弹出 WorkSelector 展示用户所有作品(排除 rejected/taken_down)
|
||||||
|
3. 用户选择作品后确认提交 → 后端复制快照到 ContestWork
|
||||||
|
4. 若作品库为空,显示"去创作"按钮 → 跳转创作页 → 完成后 redirect 回活动详情页
|
||||||
|
5. 支持 resubmit 模式:可重新选择不同作品提交
|
||||||
|
|
||||||
|
**关键设计**:
|
||||||
|
- 快照不可变性:提交后 ContestWork 数据与 UgcWork 解耦,后续修改/删除作品不影响活动中的作品
|
||||||
|
- 向后兼容:userWorkId 为 null 时走旧逻辑(直接上传)
|
||||||
|
- 无需数据库变更:t_biz_contest_work 已有所需字段
|
||||||
|
|
||||||
**依赖**:P0-4 + P0-6 + 现有活动模块
|
**依赖**:P0-4 + P0-6 + 现有活动模块
|
||||||
**产出**:用户可从作品库选作品参与活动
|
**产出**:用户可从作品库选作品参与活动
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user