From ac0c38c04ad6268d0aee76ca65c2291d8b0ad934 Mon Sep 17 00:00:00 2001 From: zhangxiaohua <827885272@qq.com> Date: Thu, 22 Jan 2026 15:27:06 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=9B=A2=E9=98=9F=E8=B5=9B?= =?UTF-8?q?=E4=BA=8B=E7=9B=B8=E5=85=B3=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 修复团队成员无法在"我参与的赛事"中看到团队赛事的问题 2. 修复教师作为指导老师无法看到团队赛事的问题 3. 修复上传作品/参赛作品/我的队伍按钮500错误(userId获取方式错误) 4. 修复管理端成员弹框队长名称和成员数显示问题 5. 新增getMyRegistration接口支持团队成员查询报名状态 6. 优化赛事卡片按钮布局 Co-Authored-By: Claude Opus 4.5 --- .../src/contests/contests/contests.service.ts | 50 ++++- .../registrations/registrations.controller.ts | 29 ++- .../registrations/registrations.service.ts | 106 ++++++++++ .../src/contests/teams/teams.controller.ts | 17 +- backend/src/contests/teams/teams.service.ts | 22 +- frontend/src/api/contests.ts | 12 ++ frontend/src/views/contests/Activities.vue | 190 ++++++++++++++---- .../contests/components/SubmitWorkDrawer.vue | 20 +- .../contests/components/ViewWorkDrawer.vue | 15 +- .../views/contests/registrations/Records.vue | 8 +- .../src/views/workbench/ai-3d/Generate.vue | 15 +- 11 files changed, 390 insertions(+), 94 deletions(-) diff --git a/backend/src/contests/contests/contests.service.ts b/backend/src/contests/contests/contests.service.ts index b037ba0..091079a 100644 --- a/backend/src/contests/contests/contests.service.ts +++ b/backend/src/contests/contests/contests.service.ts @@ -363,6 +363,7 @@ export class ContestsService { contestIds = judgeRecords.map((r) => r.contestId); } else if (role === 'teacher') { // 教师:查询作为指导老师参与的赛事 + // 1. 从报名指导老师关联表查询(个人赛) const teacherRecords = await this.prisma.contestRegistrationTeacher.findMany({ where: { userId, @@ -375,9 +376,29 @@ export class ContestsService { }, }, }); - contestIds = Array.from(new Set(teacherRecords.map((r) => r.registration.contestId as number))); + const contestIdsFromRegistration = teacherRecords.map((r) => r.registration.contestId as number); + + // 2. 从团队成员表查询(团队赛,role='mentor') + const mentorRecords = await this.prisma.contestTeamMember.findMany({ + where: { + userId, + role: 'mentor', + }, + select: { + team: { + select: { + contestId: true, + }, + }, + }, + }); + const contestIdsFromTeam = mentorRecords.map((r) => r.team.contestId); + + // 合并去重 + contestIds = Array.from(new Set([...contestIdsFromRegistration, ...contestIdsFromTeam])); } else { // 学生/默认:查询报名的赛事 + // 1. 从报名记录查询(个人赛报名或团队赛队长) const registrationWhere: any = { userId, }; @@ -393,7 +414,32 @@ export class ContestsService { }, distinct: ['contestId'], }); - contestIds = registrations.map((r) => r.contestId); + const contestIdsFromRegistration = registrations.map((r) => r.contestId); + + // 2. 从团队成员表查询(团队赛成员,role='leader' 或 'member') + const teamMemberWhere: any = { + userId, + role: { in: ['leader', 'member'] }, + }; + + if (tenantId) { + teamMemberWhere.tenantId = tenantId; + } + + const teamMembers = await this.prisma.contestTeamMember.findMany({ + where: teamMemberWhere, + select: { + team: { + select: { + contestId: true, + }, + }, + }, + }); + const contestIdsFromTeam = teamMembers.map((r) => r.team.contestId); + + // 合并去重 + contestIds = Array.from(new Set([...contestIdsFromRegistration, ...contestIdsFromTeam])); } if (contestIds.length === 0) { diff --git a/backend/src/contests/registrations/registrations.controller.ts b/backend/src/contests/registrations/registrations.controller.ts index 2a842b1..80e2ad2 100644 --- a/backend/src/contests/registrations/registrations.controller.ts +++ b/backend/src/contests/registrations/registrations.controller.ts @@ -10,6 +10,7 @@ import { UseGuards, Request, ParseIntPipe, + BadRequestException, } from '@nestjs/common'; import { RegistrationsService } from './registrations.service'; import { CreateRegistrationDto } from './dto/create-registration.dto'; @@ -28,9 +29,9 @@ export class RegistrationsController { create(@Body() createRegistrationDto: CreateRegistrationDto, @Request() req) { const tenantId = req.tenantId || req.user?.tenantId; if (!tenantId) { - throw new Error('无法确定租户信息'); + throw new BadRequestException('无法确定租户信息'); } - const creatorId = req.user?.id; + const creatorId = req.user?.userId; return this.registrationsService.create( createRegistrationDto, tenantId, @@ -45,6 +46,22 @@ export class RegistrationsController { return this.registrationsService.findAll(queryDto, tenantId); } + /** + * 获取当前用户在某比赛中的报名记录(包括作为团队成员的情况) + */ + @Get('my/:contestId') + @RequirePermission('contest:read') + getMyRegistration( + @Param('contestId', ParseIntPipe) contestId: number, + @Request() req, + ) { + const userId = req.user?.userId; + if (!userId) { + throw new BadRequestException('用户未登录'); + } + return this.registrationsService.getMyRegistration(contestId, userId); + } + @Get(':id') @RequirePermission('contest:read') findOne(@Param('id', ParseIntPipe) id: number, @Request() req) { @@ -60,7 +77,7 @@ export class RegistrationsController { @Request() req, ) { const tenantId = req.tenantId || req.user?.tenantId; - const operatorId = req.user?.id; + const operatorId = req.user?.userId; return this.registrationsService.review(id, reviewDto, operatorId, tenantId); } @@ -73,9 +90,9 @@ export class RegistrationsController { ) { const tenantId = req.tenantId || req.user?.tenantId; if (!tenantId) { - throw new Error('无法确定租户信息'); + throw new BadRequestException('无法确定租户信息'); } - const creatorId = req.user?.id; + const creatorId = req.user?.userId; return this.registrationsService.addTeacher( id, body.teacherUserId, @@ -93,7 +110,7 @@ export class RegistrationsController { ) { const tenantId = req.tenantId || req.user?.tenantId; if (!tenantId) { - throw new Error('无法确定租户信息'); + throw new BadRequestException('无法确定租户信息'); } return this.registrationsService.removeTeacher(id, teacherUserId, tenantId); } diff --git a/backend/src/contests/registrations/registrations.service.ts b/backend/src/contests/registrations/registrations.service.ts index 313e3f7..5a17100 100644 --- a/backend/src/contests/registrations/registrations.service.ts +++ b/backend/src/contests/registrations/registrations.service.ts @@ -626,5 +626,111 @@ export class RegistrationsService { where: { id: teacherRecord.id }, }); } + + /** + * 获取用户在某比赛中的报名记录(包括作为团队成员的情况) + * @param contestId 比赛ID + * @param userId 用户ID + * @returns 报名记录或null + */ + async getMyRegistration(contestId: number, userId: number) { + try { + // 1. 先查询个人赛报名(直接按 userId 匹配) + const individualReg = await this.prisma.contestRegistration.findFirst({ + where: { + contestId, + userId, + registrationType: 'individual', + registrationState: 'passed', + }, + include: { + contest: { + select: { + id: true, + contestName: true, + contestType: true, + }, + }, + works: { + where: { validState: 1, isLatest: true }, + orderBy: { submitTime: 'desc' }, + }, + }, + }); + + if (individualReg) { + return individualReg; + } + + // 2. 查询用户所属的团队(该比赛的) + const teamMemberships = await this.prisma.contestTeamMember.findMany({ + where: { + userId, + }, + select: { + teamId: true, + team: { + select: { + id: true, + contestId: true, + validState: true, + }, + }, + }, + }); + + // 过滤出该比赛且有效的团队 + const validTeamIds = teamMemberships + .filter((m) => m.team?.contestId === contestId && m.team?.validState === 1) + .map((m) => m.teamId); + + if (validTeamIds.length === 0) { + return null; + } + + // 3. 通过团队ID查询团队报名记录 + const teamReg = await this.prisma.contestRegistration.findFirst({ + where: { + contestId, + teamId: { in: validTeamIds }, + registrationType: 'team', + registrationState: 'passed', + }, + include: { + contest: { + select: { + id: true, + contestName: true, + contestType: true, + }, + }, + team: { + include: { + members: { + include: { + user: { + select: { + id: true, + username: true, + nickname: true, + }, + }, + }, + }, + }, + }, + works: { + where: { validState: 1, isLatest: true }, + orderBy: { submitTime: 'desc' }, + }, + }, + }); + + return teamReg; + } catch (error) { + console.error('getMyRegistration error:', error); + throw error; + } + } } diff --git a/backend/src/contests/teams/teams.controller.ts b/backend/src/contests/teams/teams.controller.ts index 23a67f0..fc94c29 100644 --- a/backend/src/contests/teams/teams.controller.ts +++ b/backend/src/contests/teams/teams.controller.ts @@ -9,6 +9,7 @@ import { UseGuards, Request, ParseIntPipe, + BadRequestException, } from '@nestjs/common'; import { TeamsService } from './teams.service'; import { CreateTeamDto } from './dto/create-team.dto'; @@ -27,9 +28,9 @@ export class TeamsController { create(@Body() createTeamDto: CreateTeamDto, @Request() req) { const tenantId = req.tenantId || req.user?.tenantId; if (!tenantId) { - throw new Error('无法确定租户信息'); + throw new BadRequestException('无法确定租户信息'); } - const creatorId = req.user?.id; + const creatorId = req.user?.userId; return this.teamsService.create(createTeamDto, tenantId, creatorId); } @@ -59,9 +60,9 @@ export class TeamsController { ) { const tenantId = req.tenantId || req.user?.tenantId; if (!tenantId) { - throw new Error('无法确定租户信息'); + throw new BadRequestException('无法确定租户信息'); } - const modifierId = req.user?.id; + const modifierId = req.user?.userId; return this.teamsService.update(id, updateTeamDto, tenantId, modifierId); } @@ -74,9 +75,9 @@ export class TeamsController { ) { const tenantId = req.tenantId || req.user?.tenantId; if (!tenantId) { - throw new Error('无法确定租户信息'); + throw new BadRequestException('无法确定租户信息'); } - const creatorId = req.user?.id; + const creatorId = req.user?.userId; return this.teamsService.inviteMember( teamId, inviteMemberDto, @@ -94,7 +95,7 @@ export class TeamsController { ) { const tenantId = req.tenantId || req.user?.tenantId; if (!tenantId) { - throw new Error('无法确定租户信息'); + throw new BadRequestException('无法确定租户信息'); } return this.teamsService.removeMember(teamId, userId, tenantId); } @@ -104,7 +105,7 @@ export class TeamsController { remove(@Param('id', ParseIntPipe) id: number, @Request() req) { const tenantId = req.tenantId || req.user?.tenantId; if (!tenantId) { - throw new Error('无法确定租户信息'); + throw new BadRequestException('无法确定租户信息'); } return this.teamsService.remove(id, tenantId); } diff --git a/backend/src/contests/teams/teams.service.ts b/backend/src/contests/teams/teams.service.ts index 98f09b9..7421030 100644 --- a/backend/src/contests/teams/teams.service.ts +++ b/backend/src/contests/teams/teams.service.ts @@ -206,7 +206,7 @@ export class TeamsService { where.tenantId = tenantId; } - return this.prisma.contestTeam.findMany({ + const teams = await this.prisma.contestTeam.findMany({ where, orderBy: { createTime: 'desc', @@ -230,6 +230,13 @@ export class TeamsService { }, }, }, + registrations: { + select: { + id: true, + registrationState: true, + }, + take: 1, + }, _count: { select: { members: true, @@ -238,15 +245,24 @@ export class TeamsService { }, }, }); + + // 将报名状态扁平化到团队对象上 + return teams.map((team) => ({ + ...team, + registrationState: team.registrations?.[0]?.registrationState || 'pending', + registrationId: team.registrations?.[0]?.id, + })); } - async findOne(id: number, tenantId?: number) { + async findOne(id: number, tenantId?: number, strictTenantCheck = false) { const where: any = { id, validState: 1, }; - if (tenantId) { + // 只有明确要求严格租户检查时才限制 tenantId + // 通过 ID 查询单个团队时,ID 已经是唯一的,不需要再限制 tenantId + if (tenantId && strictTenantCheck) { where.tenantId = tenantId; } diff --git a/frontend/src/api/contests.ts b/frontend/src/api/contests.ts index 98f9df9..1f591ef 100644 --- a/frontend/src/api/contests.ts +++ b/frontend/src/api/contests.ts @@ -257,6 +257,8 @@ export interface ContestTeam { createTime?: string; modifyTime?: string; validState?: number; + registrationState?: string; // 报名状态 + registrationId?: number; // 报名记录ID leader?: { id: number; username: string; @@ -730,6 +732,16 @@ export const registrationsApi = { return response; }, + // 获取当前用户在某比赛中的报名记录(包括作为团队成员的情况) + getMyRegistration: async ( + contestId: number + ): Promise => { + const response = await request.get( + `/contests/registrations/my/${contestId}` + ); + return response; + }, + // 创建报名 create: async ( data: CreateRegistrationForm diff --git a/frontend/src/views/contests/Activities.vue b/frontend/src/views/contests/Activities.vue index 868b4cc..07182fb 100644 --- a/frontend/src/views/contests/Activities.vue +++ b/frontend/src/views/contests/Activities.vue @@ -105,37 +105,36 @@
- {{ task?.status === 'pending' ? '排队中' : 'AI 生成中' }} + {{ task?.status === "pending" ? "排队中" : "AI 生成中" }}

@@ -83,9 +83,9 @@

预计时间: - {{ formatEstimatedTime(queueInfo.estimatedTime) }} + {{ + formatEstimatedTime(queueInfo.estimatedTime) + }}

@@ -290,10 +290,7 @@ const handleCardClick = (index: number) => { // 存储到 sessionStorage(避免URL过长) if (allResultUrls.length > 1) { - sessionStorage.setItem( - "model-viewer-urls", - JSON.stringify(allResultUrls) - ) + sessionStorage.setItem("model-viewer-urls", JSON.stringify(allResultUrls)) sessionStorage.setItem("model-viewer-index", String(index)) // 清除单URL存储 sessionStorage.removeItem("model-viewer-url") @@ -777,7 +774,7 @@ $gradient-card: linear-gradient( } .progress-bar { - width: 120px; + width: 200px; height: 4px; background: rgba($primary, 0.2); border-radius: 2px;