diff --git a/backend/src/ai-3d/utils/zip-handler.ts b/backend/src/ai-3d/utils/zip-handler.ts index 010c071..4751730 100644 --- a/backend/src/ai-3d/utils/zip-handler.ts +++ b/backend/src/ai-3d/utils/zip-handler.ts @@ -1,5 +1,6 @@ import * as fs from 'fs'; import * as path from 'path'; +import * as os from 'os'; import AdmZip from 'adm-zip'; import axios from 'axios'; import { Logger } from '@nestjs/common'; @@ -10,8 +11,8 @@ export class ZipHandler { /** * 下载并解压.zip文件,提取3D模型文件 * @param zipUrl ZIP文件的URL - * @param outputDir 输出目录(默认为 backend/uploads/ai-3d) - * @returns 提取的3D模型文件路径和预览图路径 + * @param outputDir 输出目录(默认为系统临时目录) + * @returns 提取的3D模型文件路径、预览图路径和文件Buffer */ static async downloadAndExtract( zipUrl: string, @@ -19,31 +20,32 @@ export class ZipHandler { ): Promise<{ modelPath: string; previewPath?: string; - modelUrl: string; - previewUrl?: string; + modelBuffer: Buffer; + previewBuffer?: Buffer; }> { + // 使用系统临时目录 + const baseDir = + outputDir || + path.join(os.tmpdir(), 'ai-3d', Date.now().toString()); + try { - // 1. 设置输出目录 - const baseDir = - outputDir || - path.join(process.cwd(), 'uploads', 'ai-3d', Date.now().toString()); if (!fs.existsSync(baseDir)) { fs.mkdirSync(baseDir, { recursive: true }); } - // 2. 下载ZIP文件 + // 1. 下载ZIP文件 this.logger.log(`开始下载ZIP文件: ${zipUrl}`); const zipPath = path.join(baseDir, 'model.zip'); await this.downloadFile(zipUrl, zipPath); this.logger.log(`ZIP文件下载完成: ${zipPath}`); - // 3. 解压ZIP文件 + // 2. 解压ZIP文件 this.logger.log(`开始解压ZIP文件`); const extractDir = path.join(baseDir, 'extracted'); await this.extractZip(zipPath, extractDir); this.logger.log(`ZIP文件解压完成: ${extractDir}`); - // 4. 查找3D模型文件和预览图 + // 3. 查找3D模型文件和预览图 const files = this.getAllFiles(extractDir); const modelFile = this.findModelFile(files); const previewFile = this.findPreviewImage(files); @@ -57,41 +59,29 @@ export class ZipHandler { this.logger.log(`找到预览图: ${previewFile}`); } - // 5. 生成可访问的URL - // 假设模型文件在 uploads/ai-3d/timestamp/extracted/model.glb - // URL应该是 /api/uploads/ai-3d/timestamp/extracted/model.glb - const relativeModelPath = path.relative( - path.join(process.cwd(), 'uploads'), - modelFile, - ); - const modelUrl = `/api/uploads/${relativeModelPath.replace(/\\/g, '/')}`; - - let previewUrl: string | undefined; - if (previewFile) { - const relativePreviewPath = path.relative( - path.join(process.cwd(), 'uploads'), - previewFile, - ); - previewUrl = `/api/uploads/${relativePreviewPath.replace(/\\/g, '/')}`; - } - - // 6. 删除原始ZIP文件以节省空间 - try { - fs.unlinkSync(zipPath); - this.logger.log(`已删除原始ZIP文件: ${zipPath}`); - } catch (err) { - this.logger.warn(`删除ZIP文件失败: ${err.message}`); - } + // 4. 读取文件Buffer(用于上传到COS) + const modelBuffer = fs.readFileSync(modelFile); + const previewBuffer = previewFile ? fs.readFileSync(previewFile) : undefined; return { modelPath: modelFile, previewPath: previewFile, - modelUrl, - previewUrl, + modelBuffer, + previewBuffer, }; } catch (error) { this.logger.error(`处理ZIP文件失败: ${error.message}`, error.stack); throw error; + } finally { + // 清理临时目录 + try { + if (fs.existsSync(baseDir)) { + fs.rmSync(baseDir, { recursive: true, force: true }); + this.logger.log(`已清理临时目录: ${baseDir}`); + } + } catch (err) { + this.logger.warn(`清理临时目录失败: ${err.message}`); + } } } diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 23d07d2..0a4bf9a 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -1,8 +1,6 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; -import { ServeStaticModule } from '@nestjs/serve-static'; import { APP_GUARD, APP_INTERCEPTOR, APP_FILTER } from '@nestjs/core'; -import { join } from 'path'; import { PrismaModule } from './prisma/prisma.module'; import { AuthModule } from './auth/auth.module'; import { UsersModule } from './users/users.module'; @@ -37,14 +35,6 @@ import { HttpExceptionFilter } from './common/filters/http-exception.filter'; `.env.${process.env.NODE_ENV || 'development'}`, // 优先加载 ], }), - // 静态文件服务 - 提供 uploads 目录的访问 - ServeStaticModule.forRoot({ - rootPath: join(process.cwd(), 'uploads'), - serveRoot: '/api/uploads', - serveStaticOptions: { - index: false, - }, - }), PrismaModule, AuthModule, UsersModule, diff --git a/backend/src/contests/contests/contests.service.ts b/backend/src/contests/contests/contests.service.ts index c242a32..1466668 100644 --- a/backend/src/contests/contests/contests.service.ts +++ b/backend/src/contests/contests/contests.service.ts @@ -270,11 +270,41 @@ export class ContestsService { } } - // 解析 contestTenants JSON 字符串为数组 - const parsedList = filteredList.map((contest) => ({ - ...contest, - contestTenants: this.parseContestTenants(contest.contestTenants), - })); + // 解析 contestTenants JSON 字符串为数组,并计算评审统计数据 + const parsedList = await Promise.all( + filteredList.map(async (contest) => { + // 计算总作品数(已提交的作品) + const totalWorksCount = await this.prisma.contestWork.count({ + where: { + contestId: contest.id, + status: 'submitted', + isLatest: true, + }, + }); + + // 计算已完成评审的作品数(所有评委都评分的作品) + // 简化逻辑:统计有评分记录的作品数 + const reviewedCount = await this.prisma.contestWork.count({ + where: { + contestId: contest.id, + status: 'submitted', + isLatest: true, + scores: { + some: { + validState: 1, + }, + }, + }, + }); + + return { + ...contest, + contestTenants: this.parseContestTenants(contest.contestTenants), + totalWorksCount, + reviewedCount, + }; + }), + ); return { list: parsedList, @@ -526,14 +556,32 @@ export class ContestsService { } } + // 特殊处理:开始评审时的友好提示 + const dto = updateContestDto as any; + if (dto.reviewStartTime && Object.keys(dto).length === 1) { + const newReviewStartTime = new Date(dto.reviewStartTime); + if (newReviewStartTime < contest.submitEndTime) { + const submitEndTimeStr = contest.submitEndTime.toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + }); + throw new BadRequestException( + `还未到评审时间,作品提交将于 ${submitEndTimeStr} 结束`, + ); + } + } + // 验证时间顺序(如果提供了时间字段) if ( - (updateContestDto as any).registerStartTime || - (updateContestDto as any).registerEndTime || - (updateContestDto as any).submitStartTime || - (updateContestDto as any).submitEndTime || - (updateContestDto as any).reviewStartTime || - (updateContestDto as any).reviewEndTime + dto.registerStartTime || + dto.registerEndTime || + dto.submitStartTime || + dto.submitEndTime || + dto.reviewStartTime || + dto.reviewEndTime ) { // 合并现有数据和更新数据,确保所有必需字段都存在 const mergedDto = { @@ -568,7 +616,6 @@ export class ContestsService { } const data: any = {}; - const dto = updateContestDto as any; if (dto.contestName !== undefined) { data.contestName = dto.contestName; diff --git a/backend/src/contests/reviews/dto/create-score.dto.ts b/backend/src/contests/reviews/dto/create-score.dto.ts index 1ea50dd..ba6ed75 100644 --- a/backend/src/contests/reviews/dto/create-score.dto.ts +++ b/backend/src/contests/reviews/dto/create-score.dto.ts @@ -8,7 +8,8 @@ export class CreateScoreDto { assignmentId: number; @IsObject() - dimensionScores: any; // JSON object + @IsOptional() + dimensionScores?: any; // JSON object @IsNumber() @Min(0) diff --git a/backend/src/contests/reviews/reviews.controller.ts b/backend/src/contests/reviews/reviews.controller.ts index 063eb7b..f4722b1 100644 --- a/backend/src/contests/reviews/reviews.controller.ts +++ b/backend/src/contests/reviews/reviews.controller.ts @@ -29,7 +29,7 @@ export class ReviewsController { @Query('contestId', ParseIntPipe) contestId: number, @Request() req, ) { - const creatorId = req.user?.id; + const creatorId = req.user?.userId; return this.reviewsService.assignWork(assignWorkDto, contestId, creatorId); } @@ -40,7 +40,7 @@ export class ReviewsController { @Query('contestId', ParseIntPipe) contestId: number, @Request() req, ) { - const creatorId = req.user?.id; + const creatorId = req.user?.userId; return this.reviewsService.batchAssignWorks( contestId, batchAssignDto.workIds, @@ -55,7 +55,7 @@ export class ReviewsController { @Query('contestId', ParseIntPipe) contestId: number, @Request() req, ) { - const creatorId = req.user?.id; + const creatorId = req.user?.userId; return this.reviewsService.autoAssignWorks(contestId, creatorId); } @@ -66,7 +66,10 @@ export class ReviewsController { if (!tenantId) { throw new Error('无法确定租户信息'); } - const judgeId = req.user?.id; + const judgeId = req.user?.userId; + if (!judgeId) { + throw new Error('无法确定评委信息'); + } return this.reviewsService.score(createScoreDto, judgeId, tenantId); } @@ -77,7 +80,7 @@ export class ReviewsController { @Body() updateScoreDto: Partial, @Request() req, ) { - const judgeId = req.user?.id; + const judgeId = req.user?.userId; return this.reviewsService.updateScore(scoreId, updateScoreDto, judgeId); } @@ -87,10 +90,42 @@ export class ReviewsController { @Query('contestId', ParseIntPipe) contestId: number, @Request() req, ) { - const judgeId = req.user?.id; + const judgeId = req.user?.userId; return this.reviewsService.getAssignedWorks(judgeId, contestId); } + @Get('judge/contests') + @RequirePermission('review:score') + getJudgeContests(@Request() req) { + const judgeId = req.user?.userId; + console.log('getJudgeContests - judgeId:', judgeId, 'user:', req.user); + if (!judgeId) { + throw new Error('无法确定评委信息'); + } + return this.reviewsService.getJudgeContests(judgeId); + } + + @Get('judge/contests/:contestId/works') + @RequirePermission('review:score') + getJudgeContestWorks( + @Param('contestId', ParseIntPipe) contestId: number, + @Query('page') page: string, + @Query('pageSize') pageSize: string, + @Query('workNo') workNo: string, + @Query('accountNo') accountNo: string, + @Query('reviewStatus') reviewStatus: string, + @Request() req, + ) { + const judgeId = req.user?.userId; + return this.reviewsService.getJudgeContestWorks(judgeId, contestId, { + page: page ? parseInt(page, 10) : 1, + pageSize: pageSize ? parseInt(pageSize, 10) : 10, + workNo, + accountNo, + reviewStatus, + }); + } + @Get('progress/:contestId') @RequirePermission('review:read') getReviewProgress(@Param('contestId', ParseIntPipe) contestId: number) { diff --git a/backend/src/contests/reviews/reviews.service.ts b/backend/src/contests/reviews/reviews.service.ts index c3e1339..b483590 100644 --- a/backend/src/contests/reviews/reviews.service.ts +++ b/backend/src/contests/reviews/reviews.service.ts @@ -157,9 +157,9 @@ export class ReviewsService { assignmentId: createScoreDto.assignmentId, judgeId, judgeName: judge?.nickname || judge?.username || '', - dimensionScores: JSON.stringify(createScoreDto.dimensionScores), + dimensionScores: createScoreDto.dimensionScores || {}, totalScore: createScoreDto.totalScore, - comments: createScoreDto.comments, + comments: createScoreDto.comments || '', scoreTime: new Date(), creator: judgeId, }; @@ -311,11 +311,198 @@ export class ReviewsService { }); } + // 获取评委参与的赛事列表 + async getJudgeContests(judgeId: number) { + console.log('getJudgeContests - judgeId:', judgeId); + + // 获取评委参与的所有赛事(通过作品分配表) + const assignments = await this.prisma.contestWorkJudgeAssignment.findMany({ + where: { + judgeId, + }, + select: { + contestId: true, + }, + distinct: ['contestId'], + }); + + console.log('getJudgeContests - assignments:', assignments); + const contestIds = assignments.map((a) => a.contestId); + + if (contestIds.length === 0) { + return []; + } + + // 获取赛事详情和评审统计 + const contests = await this.prisma.contest.findMany({ + where: { + id: { in: contestIds }, + validState: 1, + }, + include: { + reviewRule: true, + }, + orderBy: { + createTime: 'desc', + }, + }); + + // 计算每个赛事的评审进度 + const result = await Promise.all( + contests.map(async (contest) => { + // 获取分配给该评委的作品数量 + const totalAssigned = await this.prisma.contestWorkJudgeAssignment.count({ + where: { + contestId: contest.id, + judgeId, + }, + }); + + // 获取已评审的作品数量 + const reviewed = await this.prisma.contestWorkScore.count({ + where: { + contestId: contest.id, + judgeId, + validState: 1, + }, + }); + + return { + ...contest, + totalAssigned, + reviewed, + pending: totalAssigned - reviewed, + }; + }), + ); + + return result; + } + + // 获取评委在某个赛事下分配的作品列表(带分页和搜索) + async getJudgeContestWorks( + judgeId: number, + contestId: number, + query: { + page?: number; + pageSize?: number; + workNo?: string; + accountNo?: string; + reviewStatus?: string; // 'reviewed' | 'pending' + }, + ) { + const { page = 1, pageSize = 10, workNo, accountNo, reviewStatus } = query; + + // 构建查询条件 + const where: any = { + judgeId, + contestId, + }; + + // 如果需要按评审状态筛选,需要特殊处理 + const allAssignments = await this.prisma.contestWorkJudgeAssignment.findMany({ + where, + include: { + work: { + include: { + registration: { + include: { + user: { + select: { + id: true, + username: true, + nickname: true, + }, + }, + }, + }, + }, + }, + scores: { + where: { validState: 1 }, + orderBy: { scoreTime: 'desc' }, + take: 1, + }, + }, + orderBy: { + assignmentTime: 'desc', + }, + }); + + // 按 workId 去重,保留第一条(最新的) + const workIdSet = new Set(); + let assignments = allAssignments.filter((a) => { + if (workIdSet.has(a.workId)) { + return false; + } + workIdSet.add(a.workId); + return true; + }); + + // 应用搜索条件 + if (workNo) { + assignments = assignments.filter((a) => + a.work.workNo?.toLowerCase().includes(workNo.toLowerCase()), + ); + } + + if (accountNo) { + assignments = assignments.filter( + (a) => + a.work.registration?.user?.username + ?.toLowerCase() + .includes(accountNo.toLowerCase()) || + a.work.submitterAccountNo + ?.toLowerCase() + .includes(accountNo.toLowerCase()), + ); + } + + // 应用评审状态筛选 + if (reviewStatus === 'reviewed') { + assignments = assignments.filter((a) => a.scores.length > 0); + } else if (reviewStatus === 'pending') { + assignments = assignments.filter((a) => a.scores.length === 0); + } + + const total = assignments.length; + + // 分页 + const start = (page - 1) * pageSize; + const paginatedAssignments = assignments.slice(start, start + pageSize); + + // 转换为前端期望的格式 + const list = paginatedAssignments.map((assignment) => { + const latestScore = assignment.scores[0]; + return { + id: assignment.id, + workId: assignment.workId, + workNo: assignment.work.workNo, + accountNo: + assignment.work.submitterAccountNo || + assignment.work.registration?.user?.username || + '-', + nickname: assignment.work.registration?.user?.nickname || '-', + score: latestScore?.totalScore ?? null, + reviewStatus: latestScore ? 'reviewed' : 'pending', + assignmentTime: assignment.assignmentTime, + work: assignment.work, + }; + }); + + return { + list, + total, + page, + pageSize, + }; + } + async getWorkScores(workId: number) { - return this.prisma.contestWorkScore.findMany({ + // 获取分配给作品的所有评委及其评分记录 + const assignments = await this.prisma.contestWorkJudgeAssignment.findMany({ where: { workId, - validState: 1, }, include: { judge: { @@ -323,13 +510,46 @@ export class ReviewsService { id: true, username: true, nickname: true, + phone: true, + tenant: { + select: { + id: true, + name: true, + }, + }, }, }, + scores: { + where: { + validState: 1, + }, + orderBy: { + scoreTime: 'desc', + }, + take: 1, // 只取最新的有效评分 + }, }, orderBy: { - scoreTime: 'desc', + assignmentTime: 'desc', }, }); + + // 转换为前端期望的格式 + return assignments.map((assignment) => { + const latestScore = assignment.scores[0]; + return { + id: assignment.id, + assignmentId: assignment.id, + workId: assignment.workId, + judgeId: assignment.judgeId, + judge: assignment.judge, + score: latestScore?.totalScore ?? null, + scoreTime: latestScore?.scoreTime ?? null, + comments: latestScore?.comments ?? null, + status: assignment.status, + assignmentTime: assignment.assignmentTime, + }; + }); } async calculateFinalScore(workId: number) { diff --git a/backend/src/upload/upload.controller.ts b/backend/src/upload/upload.controller.ts index 03de0da..ac54871 100644 --- a/backend/src/upload/upload.controller.ts +++ b/backend/src/upload/upload.controller.ts @@ -1,23 +1,16 @@ import { Controller, Post, - Get, - Param, - Res, UseInterceptors, UploadedFile, UseGuards, Request, BadRequestException, - NotFoundException, } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; -import { Response } from 'express'; import { memoryStorage } from 'multer'; import { UploadService } from './upload.service'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; -import * as path from 'path'; -import * as fs from 'fs'; @Controller('upload') export class UploadController { @@ -27,7 +20,7 @@ export class UploadController { @UseGuards(JwtAuthGuard) @UseInterceptors( FileInterceptor('file', { - storage: memoryStorage(), // 使用内存存储,确保 file.buffer 可用 + storage: memoryStorage(), limits: { fileSize: 100 * 1024 * 1024, // 限制文件大小为 100MB }, @@ -54,61 +47,3 @@ export class UploadController { } } -/** - * 专门用于静态文件服务的控制器 - * 处理 /api/uploads/* 路径的文件请求 - */ -@Controller('uploads') -export class UploadsController { - private readonly uploadDir: string; - - constructor() { - this.uploadDir = path.join(process.cwd(), 'uploads'); - } - - @Get('*') - async serveFile(@Param() params: any, @Res() res: Response) { - // 获取文件路径(从通配符参数中) - const filePath = params[0] || ''; - const fullPath = path.join(this.uploadDir, filePath); - - // 安全检查:防止路径遍历攻击 - const normalizedPath = path.normalize(fullPath); - if (!normalizedPath.startsWith(this.uploadDir)) { - throw new BadRequestException('无效的文件路径'); - } - - // 检查文件是否存在 - if (!fs.existsSync(normalizedPath)) { - console.error('文件不存在:', normalizedPath); - throw new NotFoundException('文件不存在'); - } - - // 获取文件扩展名以设置正确的 Content-Type - const ext = path.extname(normalizedPath).toLowerCase(); - const mimeTypes: Record = { - '.glb': 'model/gltf-binary', - '.gltf': 'model/gltf+json', - '.obj': 'text/plain', - '.fbx': 'application/octet-stream', - '.stl': 'model/stl', - '.png': 'image/png', - '.jpg': 'image/jpeg', - '.jpeg': 'image/jpeg', - '.gif': 'image/gif', - '.webp': 'image/webp', - '.pdf': 'application/pdf', - '.mp4': 'video/mp4', - '.webm': 'video/webm', - }; - - const contentType = mimeTypes[ext] || 'application/octet-stream'; - - res.setHeader('Content-Type', contentType); - res.setHeader('Access-Control-Allow-Origin', '*'); - - // 发送文件 - res.sendFile(normalizedPath); - } -} - diff --git a/backend/src/upload/upload.module.ts b/backend/src/upload/upload.module.ts index f48e572..0eb4428 100644 --- a/backend/src/upload/upload.module.ts +++ b/backend/src/upload/upload.module.ts @@ -1,11 +1,11 @@ import { Module } from '@nestjs/common'; -import { UploadController, UploadsController } from './upload.controller'; +import { UploadController } from './upload.controller'; import { UploadService } from './upload.service'; import { OssModule } from '../oss/oss.module'; @Module({ imports: [OssModule], - controllers: [UploadController, UploadsController], + controllers: [UploadController], providers: [UploadService], exports: [UploadService], }) diff --git a/backend/src/upload/upload.service.ts b/backend/src/upload/upload.service.ts index bb35082..09d416f 100644 --- a/backend/src/upload/upload.service.ts +++ b/backend/src/upload/upload.service.ts @@ -1,34 +1,11 @@ import { Injectable, BadRequestException } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; import { OssService } from '../oss/oss.service'; -import * as fs from 'fs'; -import * as path from 'path'; -import { randomBytes } from 'crypto'; @Injectable() export class UploadService { - private readonly uploadDir: string; - private readonly useOss: boolean; - - constructor( - private configService: ConfigService, - private ossService: OssService, - ) { - // 本地上传文件存储目录(作为 COS 的备用方案) - this.uploadDir = path.join(process.cwd(), 'uploads'); - - // 确保本地上传目录存在 - if (!fs.existsSync(this.uploadDir)) { - fs.mkdirSync(this.uploadDir, { recursive: true }); - } - - // 检查是否使用 COS - this.useOss = this.ossService.isEnabled(); - - if (this.useOss) { - console.log('文件上传将使用腾讯云 COS'); - } else { - console.log('文件上传将使用本地存储,目录:', this.uploadDir); + constructor(private ossService: OssService) { + if (!this.ossService.isEnabled()) { + console.warn('警告: COS 未配置,文件上传功能可能无法正常使用'); } } @@ -41,92 +18,20 @@ export class UploadService { throw new BadRequestException('文件不存在'); } - // 优先使用 COS - if (this.useOss) { - return this.uploadToOss(file, tenantId, userId); + if (!this.ossService.isEnabled()) { + throw new BadRequestException('文件存储服务未配置,请联系管理员'); } - // 备用方案:本地存储 - return this.uploadToLocal(file, tenantId, userId); - } - - /** - * 上传到腾讯云 COS - */ - private async uploadToOss( - file: Express.Multer.File, - tenantId?: number, - userId?: number, - ): Promise<{ url: string; fileName: string; size: number }> { - try { - const result = await this.ossService.uploadFile( - file.buffer, - file.originalname, - tenantId, - userId, - ); - - return { - url: result.url, - fileName: result.fileName, - size: file.size, - }; - } catch (error: any) { - console.error('COS 上传失败,尝试本地存储:', error.message); - // COS 失败时回退到本地存储 - return this.uploadToLocal(file, tenantId, userId); - } - } - - /** - * 上传到本地存储 - */ - private async uploadToLocal( - file: Express.Multer.File, - tenantId?: number, - userId?: number, - ): Promise<{ url: string; fileName: string; size: number }> { - // 生成唯一文件名 - const fileExt = path.extname(file.originalname); - const uniqueId = randomBytes(16).toString('hex'); - const fileName = `${uniqueId}${fileExt}`; - - // 根据租户ID和用户ID创建目录结构:uploads/tenantId/userId/ - let targetDir = this.uploadDir; - if (tenantId) { - targetDir = path.join(targetDir, `tenant_${tenantId}`); - if (!fs.existsSync(targetDir)) { - fs.mkdirSync(targetDir, { recursive: true }); - } - - if (userId) { - targetDir = path.join(targetDir, `user_${userId}`); - if (!fs.existsSync(targetDir)) { - fs.mkdirSync(targetDir, { recursive: true }); - } - } - } - - // 文件保存路径 - const filePath = path.join(targetDir, fileName); - - // 保存文件 - fs.writeFileSync(filePath, file.buffer); - - // 生成访问URL - // 格式:/api/uploads/tenantId/userId/fileName 或 /api/uploads/fileName - let urlPath = '/api/uploads'; - if (tenantId) { - urlPath += `/tenant_${tenantId}`; - if (userId) { - urlPath += `/user_${userId}`; - } - } - urlPath += `/${fileName}`; + const result = await this.ossService.uploadFile( + file.buffer, + file.originalname, + tenantId, + userId, + ); return { - url: urlPath, - fileName: file.originalname, + url: result.url, + fileName: result.fileName, size: file.size, }; } diff --git a/backend/uploads/tenant_1/user_1/bdfabf2ad6aaf080eb11e99c55ba52cb.png b/backend/uploads/tenant_1/user_1/bdfabf2ad6aaf080eb11e99c55ba52cb.png deleted file mode 100644 index b351085..0000000 Binary files a/backend/uploads/tenant_1/user_1/bdfabf2ad6aaf080eb11e99c55ba52cb.png and /dev/null differ diff --git a/backend/uploads/tenant_1/user_1/fb661034af3721f15d62ad90507a44a2.png b/backend/uploads/tenant_1/user_1/fb661034af3721f15d62ad90507a44a2.png deleted file mode 100644 index b351085..0000000 Binary files a/backend/uploads/tenant_1/user_1/fb661034af3721f15d62ad90507a44a2.png and /dev/null differ diff --git a/backend/uploads/tenant_1/user_4/00fcf889621022810ca955e1d1dd179e.glb b/backend/uploads/tenant_1/user_4/00fcf889621022810ca955e1d1dd179e.glb deleted file mode 100644 index f2c7e04..0000000 Binary files a/backend/uploads/tenant_1/user_4/00fcf889621022810ca955e1d1dd179e.glb and /dev/null differ diff --git a/backend/uploads/tenant_1/user_4/442d2715232818d0fbad6b4cf9ac59ee.glb b/backend/uploads/tenant_1/user_4/442d2715232818d0fbad6b4cf9ac59ee.glb deleted file mode 100644 index f2c7e04..0000000 Binary files a/backend/uploads/tenant_1/user_4/442d2715232818d0fbad6b4cf9ac59ee.glb and /dev/null differ diff --git a/backend/uploads/tenant_1/user_4/4eebcacd2f4c92d6154fd062b8dbe070.glb b/backend/uploads/tenant_1/user_4/4eebcacd2f4c92d6154fd062b8dbe070.glb deleted file mode 100644 index f2c7e04..0000000 Binary files a/backend/uploads/tenant_1/user_4/4eebcacd2f4c92d6154fd062b8dbe070.glb and /dev/null differ diff --git a/backend/uploads/tenant_1/user_4/5f9eeecc32f3915918b5f2b4178c0dea.glb b/backend/uploads/tenant_1/user_4/5f9eeecc32f3915918b5f2b4178c0dea.glb deleted file mode 100644 index f2c7e04..0000000 Binary files a/backend/uploads/tenant_1/user_4/5f9eeecc32f3915918b5f2b4178c0dea.glb and /dev/null differ diff --git a/backend/uploads/tenant_1/user_4/798638c80d2cda432e6e698cd0c404df.glb b/backend/uploads/tenant_1/user_4/798638c80d2cda432e6e698cd0c404df.glb deleted file mode 100644 index f2c7e04..0000000 Binary files a/backend/uploads/tenant_1/user_4/798638c80d2cda432e6e698cd0c404df.glb and /dev/null differ diff --git a/backend/uploads/tenant_1/user_4/87fd8b028538e6b685d86636c9a8d0d1.glb b/backend/uploads/tenant_1/user_4/87fd8b028538e6b685d86636c9a8d0d1.glb deleted file mode 100644 index f2c7e04..0000000 Binary files a/backend/uploads/tenant_1/user_4/87fd8b028538e6b685d86636c9a8d0d1.glb and /dev/null differ diff --git a/backend/uploads/tenant_1/user_4/c31b1d63868df7fb26a0e3576497ec87.glb b/backend/uploads/tenant_1/user_4/c31b1d63868df7fb26a0e3576497ec87.glb deleted file mode 100644 index f2c7e04..0000000 Binary files a/backend/uploads/tenant_1/user_4/c31b1d63868df7fb26a0e3576497ec87.glb and /dev/null differ diff --git a/backend/uploads/tenant_1/user_4/c686a457b8087a7974cecf6a3508477e.glb b/backend/uploads/tenant_1/user_4/c686a457b8087a7974cecf6a3508477e.glb deleted file mode 100644 index f2c7e04..0000000 Binary files a/backend/uploads/tenant_1/user_4/c686a457b8087a7974cecf6a3508477e.glb and /dev/null differ diff --git a/backend/uploads/tenant_1/user_4/d9337dc9078a28d75790a5a723e82c90.glb b/backend/uploads/tenant_1/user_4/d9337dc9078a28d75790a5a723e82c90.glb deleted file mode 100644 index f2c7e04..0000000 Binary files a/backend/uploads/tenant_1/user_4/d9337dc9078a28d75790a5a723e82c90.glb and /dev/null differ diff --git a/backend/uploads/tenant_1/user_4/e15f022876aff941c9bf7c1a0c56cac9.glb b/backend/uploads/tenant_1/user_4/e15f022876aff941c9bf7c1a0c56cac9.glb deleted file mode 100644 index f2c7e04..0000000 Binary files a/backend/uploads/tenant_1/user_4/e15f022876aff941c9bf7c1a0c56cac9.glb and /dev/null differ diff --git a/backend/uploads/tenant_1/user_4/f3bc06e8e0af0f979f7ccfdae0825439.glb b/backend/uploads/tenant_1/user_4/f3bc06e8e0af0f979f7ccfdae0825439.glb deleted file mode 100644 index f2c7e04..0000000 Binary files a/backend/uploads/tenant_1/user_4/f3bc06e8e0af0f979f7ccfdae0825439.glb and /dev/null differ diff --git a/frontend/src/api/contests.ts b/frontend/src/api/contests.ts index b3d3221..ad5349f 100644 --- a/frontend/src/api/contests.ts +++ b/frontend/src/api/contests.ts @@ -439,7 +439,7 @@ export interface AssignWorkForm { export interface CreateScoreForm { workId: number; assignmentId: number; - dimensionScores: any; + dimensionScores?: any; totalScore: number; comments?: string; } @@ -1116,6 +1116,32 @@ export const reviewsApi = { { assignmentId, newJudgeId } ); }, + + // 获取评委参与的赛事列表 + getJudgeContests: async (): Promise => { + const response = await request.get( + `/contests/reviews/judge/contests` + ); + return response; + }, + + // 获取评委在某个赛事下分配的作品列表 + getJudgeContestWorks: async ( + contestId: number, + params: { + page?: number; + pageSize?: number; + workNo?: string; + accountNo?: string; + reviewStatus?: string; + } + ): Promise<{ list: any[]; total: number; page: number; pageSize: number }> => { + const response = await request.get< + any, + { list: any[]; total: number; page: number; pageSize: number } + >(`/contests/reviews/judge/contests/${contestId}/works`, { params }); + return response; + }, }; // 公告管理 diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 93969cc..e6bf097 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -178,6 +178,16 @@ const baseRoutes: RouteRecordRaw[] = [ permissions: ["activity:read"], }, }, + // 评委评审详情页 + { + path: "activities/review/:id", + name: "ReviewDetail", + component: () => import("@/views/activities/ReviewDetail.vue"), + meta: { + title: "作品评审", + requiresAuth: true, + }, + }, // 3D建模实验室路由(工作台模块下) { path: "workbench/3d-lab", diff --git a/frontend/src/utils/menu.ts b/frontend/src/utils/menu.ts index 7e4204d..e24962c 100644 --- a/frontend/src/utils/menu.ts +++ b/frontend/src/utils/menu.ts @@ -51,6 +51,7 @@ const componentMap: Record Promise> = { // 赛事活动模块(教师/评委) "activities/Guidance": () => import("@/views/activities/Guidance.vue"), "activities/Review": () => import("@/views/activities/Review.vue"), + "activities/ReviewDetail": () => import("@/views/activities/ReviewDetail.vue"), "activities/Comments": () => import("@/views/activities/Comments.vue"), // 系统管理模块 "system/users/Index": () => import("@/views/system/users/Index.vue"), diff --git a/frontend/src/views/activities/Review.vue b/frontend/src/views/activities/Review.vue index f1b9a56..9c19dc0 100644 --- a/frontend/src/views/activities/Review.vue +++ b/frontend/src/views/activities/Review.vue @@ -4,73 +4,38 @@ - - - - - - - - - - - 待评审 - 已评审 - - - - - - 搜索 - - - - 重置 - - - - -