From 8ba5468c2154b4f40516299c644ccc77b76afa1c Mon Sep 17 00:00:00 2001 From: ltx <1006158920@qq.com> Date: Wed, 16 Sep 2026 15:20:14 +0800 Subject: [PATCH] =?UTF-8?q?update=EF=BC=9A=E6=8E=A5=E5=8F=A3=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Scoring/EventScoreCalculationService.cs | 5 ++- .../Judge/JudgeScoringController.cs | 15 ++++++++ .../Workbench/WorkbenchController.cs | 6 +++- .../20260915_fix_team_result_unique_index.sql | 36 +++++++++++++++++++ 二期需求/dbo.sql | 7 ++++ 5 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 二期需求/20260915_fix_team_result_unique_index.sql diff --git a/YD_Event.Application/Event/Scoring/EventScoreCalculationService.cs b/YD_Event.Application/Event/Scoring/EventScoreCalculationService.cs index a163a02..e70c32f 100644 --- a/YD_Event.Application/Event/Scoring/EventScoreCalculationService.cs +++ b/YD_Event.Application/Event/Scoring/EventScoreCalculationService.cs @@ -295,8 +295,11 @@ public class EventScoreCalculationService : ITransient var userId = operatorUserId ?? 0; var personIds = athletes.Select(x => x.Id).ToList(); + // 同一名运动员可能因重赛等原因在同一项目中产生多条出场记录, + // 成绩必须以“出场 + 运动员”为业务键更新,不能覆盖其他出场的历史成绩。 var existing = await _resultRepository.GetListAsync(x => x.EventId == ctx.Session.EventId - && x.ProjectId == ctx.Project.Id && personIds.Contains(x.RegistrationPersonId)); + && x.ProjectId == ctx.Project.Id && x.SessionEntryId == entry.Id + && personIds.Contains(x.RegistrationPersonId)); var registrationIds = athletes.Select(x => x.RegistrationId).Distinct().ToList(); var registrations = await _registrationRepository.GetListAsync(x => registrationIds.Contains(x.Id)); var certificationByRegistration = registrations.GroupBy(x => x.Id) diff --git a/YD_Event.Application/Judge/JudgeScoringController.cs b/YD_Event.Application/Judge/JudgeScoringController.cs index ea0c48c..74a96c9 100644 --- a/YD_Event.Application/Judge/JudgeScoringController.cs +++ b/YD_Event.Application/Judge/JudgeScoringController.cs @@ -728,6 +728,21 @@ public class JudgeScoringController : IDynamicApiController || !signature.SequenceEqual(PngSignature)) throw Oops.Bah("上传文件不是有效的PNG图片"); stream.Position = 0; + // 文件头可以伪造;在上传 OSS 前完整解码,拒绝截断或损坏的 PNG。 + // 限制解码后的像素数,避免小体积压缩文件造成过量内存分配。 + // 解码器会释放其输入;保留请求流供后续 OSS 上传使用。 + using (var decoderStream = new SkiaSharp.SKManagedStream(stream, false)) + using (var codec = SkiaSharp.SKCodec.Create(decoderStream)) + { + if (codec == null || codec.EncodedFormat != SkiaSharp.SKEncodedImageFormat.Png + || codec.Info.Width <= 0 || codec.Info.Height <= 0 + || (long)codec.Info.Width * codec.Info.Height > 16_777_216) + throw Oops.Bah("画板PNG图片损坏或像素尺寸过大"); + using var bitmap = new SkiaSharp.SKBitmap(codec.Info); + if (codec.GetPixels(bitmap.Info, bitmap.GetPixels()) != SkiaSharp.SKCodecResult.Success) + throw Oops.Bah("画板PNG图片不完整或无法解码"); + } + stream.Position = 0; var objectName = $"judge-score-annotations/{sheet.EventId}/{sheet.Id}/{key}.png"; var url = await _fileService.UploadFileStreamAsync(objectName, stream); diff --git a/YD_Event.Application/Workbench/WorkbenchController.cs b/YD_Event.Application/Workbench/WorkbenchController.cs index 7ffb3d3..420a08c 100644 --- a/YD_Event.Application/Workbench/WorkbenchController.cs +++ b/YD_Event.Application/Workbench/WorkbenchController.cs @@ -382,7 +382,11 @@ public class WorkbenchController : IDynamicApiController .ToListAsync(); var pendingCorrectionSet = pendingCorrections.ToHashSet(); - var resultDict = results.ToDictionary(r => r.SessionEntryId!.Value); + // 集体项目为每名队员留存同一出场成绩,记分台按出场展示一行。 + // 稳定选取一个代表记录,避免同队多名队员导致重复字典键。 + var resultDict = results.GroupBy(r => r.SessionEntryId!.Value) + .ToDictionary(g => g.Key, g => g.OrderByDescending(r => r.ResultVersion) + .ThenBy(r => r.Id).First()); return entries.Select(e => { diff --git a/二期需求/20260915_fix_team_result_unique_index.sql b/二期需求/20260915_fix_team_result_unique_index.sql new file mode 100644 index 0000000..fed1d23 --- /dev/null +++ b/二期需求/20260915_fix_team_result_unique_index.sql @@ -0,0 +1,36 @@ +/* + 修复集体项目成绩写入:EventResult 会为队内每名运动员各保留一条成绩, + 因此活动成绩唯一键应为“出场记录 + 报名人员”,不能只约束出场记录。 + 本脚本可重复执行。 +*/ +SET NOCOUNT ON; +SET XACT_ABORT ON; +SET ANSI_NULLS ON; +SET ANSI_PADDING ON; +SET ANSI_WARNINGS ON; +SET ARITHABORT ON; +SET CONCAT_NULL_YIELDS_NULL ON; +SET QUOTED_IDENTIFIER ON; +SET NUMERIC_ROUNDABORT OFF; + +BEGIN TRY + BEGIN TRANSACTION; + + IF EXISTS ( + SELECT 1 + FROM sys.indexes + WHERE object_id = OBJECT_ID(N'dbo.EventResult') + AND name = N'UX_EventResult_SessionEntry_Active' + ) + DROP INDEX [UX_EventResult_SessionEntry_Active] ON dbo.[EventResult]; + + CREATE UNIQUE NONCLUSTERED INDEX [UX_EventResult_SessionEntry_Active] + ON dbo.[EventResult]([SessionEntryId], [RegistrationPersonId]) + WHERE [SessionEntryId] IS NOT NULL AND [DeleteMark] = 0; + + COMMIT TRANSACTION; +END TRY +BEGIN CATCH + IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION; + THROW; +END CATCH; diff --git a/二期需求/dbo.sql b/二期需求/dbo.sql index bb2903b..41684e2 100644 --- a/二期需求/dbo.sql +++ b/二期需求/dbo.sql @@ -726,6 +726,13 @@ BEGIN TRY IF NOT EXISTS(SELECT 1 FROM sys.indexes WHERE object_id=OBJECT_ID(N'dbo.[EventJudgeScoreAttachment]') AND name=N'UX_EventJudgeScoreAttachment_Idempotency') CREATE UNIQUE NONCLUSTERED INDEX [UX_EventJudgeScoreAttachment_Idempotency] ON dbo.[EventJudgeScoreAttachment]([ScoreSheetId],[IdempotencyKey]); IF NOT EXISTS(SELECT 1 FROM sys.indexes WHERE object_id=OBJECT_ID(N'dbo.[EventJudgeScoreAttachment]') AND name=N'UX_EventJudgeScoreAttachment_Current') CREATE UNIQUE NONCLUSTERED INDEX [UX_EventJudgeScoreAttachment_Current] ON dbo.[EventJudgeScoreAttachment]([ScoreSheetId],[AttachmentType]) WHERE [IsCurrent]=1 AND [DeleteMark]=0; IF NOT EXISTS(SELECT 1 FROM sys.indexes WHERE object_id=OBJECT_ID(N'dbo.[EventJudgeScoreDetail]') AND name=N'IX_EventJudgeScoreDetail_Sheet') CREATE NONCLUSTERED INDEX [IX_EventJudgeScoreDetail_Sheet] ON dbo.[EventJudgeScoreDetail]([ScoreSheetId],[Sort]); + /* 集体项目按队员分别保存成绩;旧版仅以 SessionEntryId 唯一会导致第二名队员写入失败。 */ + IF EXISTS(SELECT 1 FROM sys.indexes WHERE object_id=OBJECT_ID(N'dbo.[EventResult]') AND name=N'UX_EventResult_SessionEntry_Active' + AND (INDEX_COL(N'dbo.EventResult',N'UX_EventResult_SessionEntry_Active',1)<>N'SessionEntryId' + OR ISNULL(INDEX_COL(N'dbo.EventResult',N'UX_EventResult_SessionEntry_Active',2),N'')<>N'RegistrationPersonId')) + DROP INDEX [UX_EventResult_SessionEntry_Active] ON dbo.[EventResult]; + IF NOT EXISTS(SELECT 1 FROM sys.indexes WHERE object_id=OBJECT_ID(N'dbo.[EventResult]') AND name=N'UX_EventResult_SessionEntry_Active') + CREATE UNIQUE NONCLUSTERED INDEX [UX_EventResult_SessionEntry_Active] ON dbo.[EventResult]([SessionEntryId],[RegistrationPersonId]) WHERE [SessionEntryId] IS NOT NULL AND [DeleteMark]=0; IF NOT EXISTS(SELECT 1 FROM sys.indexes WHERE object_id=OBJECT_ID(N'dbo.[EventResultVersion]') AND name=N'UX_EventResultVersion_Result_Version') CREATE UNIQUE NONCLUSTERED INDEX [UX_EventResultVersion_Result_Version] ON dbo.[EventResultVersion]([EventResultId],[ResultVersion]) WHERE [DeleteMark]=0; IF NOT EXISTS(SELECT 1 FROM sys.indexes WHERE object_id=OBJECT_ID(N'dbo.[EventJudgeMessage]') AND name=N'IX_EventJudgeMessage_Receiver_Unread') CREATE NONCLUSTERED INDEX [IX_EventJudgeMessage_Receiver_Unread] ON dbo.[EventJudgeMessage]([ReceiverUserId],[IsRead],[CreatedTime]) WHERE [DeleteMark]=0; IF NOT EXISTS(SELECT 1 FROM sys.indexes WHERE object_id=OBJECT_ID(N'dbo.[EventResultScreenCast]') AND name=N'IX_EventResultScreenCast_Event_Time') CREATE NONCLUSTERED INDEX [IX_EventResultScreenCast_Event_Time] ON dbo.[EventResultScreenCast]([EventId],[CastTime]) WHERE [DeleteMark]=0;