update:接口修改

This commit is contained in:
ltx 2026-09-16 15:20:14 +08:00
parent 5114a6cb9f
commit 8ba5468c21
5 changed files with 67 additions and 2 deletions

View File

@ -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)

View File

@ -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);

View File

@ -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 =>
{

View File

@ -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;

View File

@ -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;