From 3277a76730a8c4a2ea014394f5f970a5267397dd Mon Sep 17 00:00:00 2001 From: ltx <1006158920@qq.com> Date: Tue, 8 Sep 2026 14:07:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E8=A3=81=E5=88=A4?= =?UTF-8?q?=E6=88=90=E7=BB=A9=E4=BF=AE=E6=AD=A3=E4=B8=8E=E7=94=BB=E6=9D=BF?= =?UTF-8?q?=E9=99=84=E4=BB=B6=E9=97=AD=E7=8E=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Event/Dtos/JudgeScoreDtos.cs | 3 + .../Event/JudgeScoreService.cs | 13 +- .../Scoring/EventScoreCalculationService.cs | 10 +- .../Judge/Dtos/JudgeScoringDtos.cs | 47 +++ .../Judge/JudgeScoringController.cs | 183 ++++++++- .../Workbench/WorkbenchController.cs | 25 +- YD_Event.Application/YD_Event.Application.xml | 373 +++++++++++++----- .../Entities/EventJudgeScoreAttachment.cs | 39 ++ .../Entities/EventScoreCorrection.cs | 11 +- YD_Event.Core/YD_Event.Core.xml | 17 +- 二期需求/20260903_add_score_attachments.sql | 29 ++ ...20260904_support_referee_score_corrections.sql | 28 ++ .../20260907_fix_score_correction_open_index.sql | 25 ++ 二期需求/dbo.sql | 49 ++- 14 files changed, 730 insertions(+), 122 deletions(-) create mode 100644 YD_Event.Core/Entities/EventJudgeScoreAttachment.cs create mode 100644 二期需求/20260903_add_score_attachments.sql create mode 100644 二期需求/20260904_support_referee_score_corrections.sql create mode 100644 二期需求/20260907_fix_score_correction_open_index.sql diff --git a/YD_Event.Application/Event/Dtos/JudgeScoreDtos.cs b/YD_Event.Application/Event/Dtos/JudgeScoreDtos.cs index 5f2bea0..4242490 100644 --- a/YD_Event.Application/Event/Dtos/JudgeScoreDtos.cs +++ b/YD_Event.Application/Event/Dtos/JudgeScoreDtos.cs @@ -75,6 +75,9 @@ public class JudgeBoardSheetScoreOutput /// 是否由该裁判判定取消比赛资格 /// public bool IsDisqualified { get; set; } + + /// 裁判记录画板地址。 + public string? AnnotationUrl { get; set; } } /// diff --git a/YD_Event.Application/Event/JudgeScoreService.cs b/YD_Event.Application/Event/JudgeScoreService.cs index 3643feb..75829d5 100644 --- a/YD_Event.Application/Event/JudgeScoreService.cs +++ b/YD_Event.Application/Event/JudgeScoreService.cs @@ -62,6 +62,7 @@ public class JudgeScoreService : IDynamicApiController, ITransient private readonly ISqlSugarRepository _sheetRepository; private readonly ISqlSugarRepository _correctionRepository; private readonly ISqlSugarRepository _messageRepository; + private readonly ISqlSugarRepository _attachmentRepository; private readonly ISqlSugarRepository _refereeRepository; private readonly ISqlSugarRepository _resultRepository; private readonly EventScoreCalculationService _calculationService; @@ -75,6 +76,7 @@ public class JudgeScoreService : IDynamicApiController, ITransient ISqlSugarRepository sheetRepository, ISqlSugarRepository correctionRepository, ISqlSugarRepository messageRepository, + ISqlSugarRepository attachmentRepository, ISqlSugarRepository refereeRepository, ISqlSugarRepository resultRepository, EventScoreCalculationService calculationService, IIdGenerator idGenerator, AuthManager authManager) @@ -82,6 +84,7 @@ public class JudgeScoreService : IDynamicApiController, ITransient _sessionRepository = sessionRepository; _entryRepository = entryRepository; _venueRepository = venueRepository; _ruleRepository = ruleRepository; _assignmentRepository = assignmentRepository; _sheetRepository = sheetRepository; _correctionRepository = correctionRepository; _messageRepository = messageRepository; + _attachmentRepository = attachmentRepository; _refereeRepository = refereeRepository; _resultRepository = resultRepository; _calculationService = calculationService; _idGenerator = idGenerator; _authManager = authManager; } @@ -387,6 +390,13 @@ public class JudgeScoreService : IDynamicApiController, ITransient var entryIds = outcomes.Select(x => (long?)x.SessionEntryId).ToList(); var results = await _resultRepository.GetListAsync(x => entryIds.Contains(x.SessionEntryId) && x.ResultStatus != EventScoreCalculationService.ResultStatusVoided); + var sheetIds = outcomes.SelectMany(x => x.Aggregate?.Symbols ?? new List()) + .SelectMany(x => x.Judges).Select(x => x.ScoreSheetId).Distinct().ToList(); + var annotationUrls = sheetIds.Count == 0 + ? new Dictionary() + : (await _attachmentRepository.GetListAsync(x => sheetIds.Contains(x.ScoreSheetId) + && x.IsCurrent && x.AttachmentType == "score_annotation_png")) + .GroupBy(x => x.ScoreSheetId).ToDictionary(x => x.Key, x => x.OrderByDescending(y => y.CreatedTime).First().FileUrl); // 一个出场(队伍)的多名运动员共享同一名次,取任意一条即可。 var resultByEntry = results.Where(x => x.SessionEntryId.HasValue) .GroupBy(x => x.SessionEntryId!.Value).ToDictionary(x => x.Key, x => x.First()); @@ -428,7 +438,8 @@ public class JudgeScoreService : IDynamicApiController, ITransient ScoreSheetId = judge.ScoreSheetId, RefereeId = judge.RefereeId, RefereeName = judge.JudgeName ?? string.Empty, JudgeRole = judge.JudgeRole, JudgeRoleType = judge.JudgeRoleType, Value = ctx.Config.Format(judge.Value), - IsDisqualified = judge.IsDisqualified + IsDisqualified = judge.IsDisqualified, + AnnotationUrl = annotationUrls.GetValueOrDefault(judge.ScoreSheetId) }).ToList() }).ToList(); } diff --git a/YD_Event.Application/Event/Scoring/EventScoreCalculationService.cs b/YD_Event.Application/Event/Scoring/EventScoreCalculationService.cs index 4f3b2cd..f93d5bb 100644 --- a/YD_Event.Application/Event/Scoring/EventScoreCalculationService.cs +++ b/YD_Event.Application/Event/Scoring/EventScoreCalculationService.cs @@ -173,8 +173,14 @@ public class EventScoreCalculationService : ITransient // 已提交裁判分配即本场的应到裁判名单,只统计计分规则里参与计分的类型。 var assignments = await _assignmentRepository.GetListAsync(x => x.SessionId == sessionId && x.AssignmentStatus == 1); var entryIds = entries.Select(x => x.Id).ToList(); - var sheets = await _sheetRepository.GetListAsync(x => entryIds.Contains(x.SessionEntryId) - && x.IsCurrent && x.SheetStatus >= SheetStatusSubmitted); + var assignmentIds = assignments.Select(x => x.Id).ToList(); + // 历史席位被替换后,其旧评分单仍需留档,但不能继续参与当前场次汇总。 + // 否则同一角色会出现超过规则席位数的评分,并污染平均分与最终成绩。 + var sheets = assignmentIds.Count == 0 + ? new List() + : await _sheetRepository.GetListAsync(x => entryIds.Contains(x.SessionEntryId) + && assignmentIds.Contains(x.JudgeAssignmentId) + && x.IsCurrent && x.SheetStatus >= SheetStatusSubmitted); var judgeNames = await LoadJudgeNamesAsync(assignments); var sheetsByEntry = sheets.GroupBy(x => x.SessionEntryId).ToDictionary(x => x.Key, x => x.ToList()); diff --git a/YD_Event.Application/Judge/Dtos/JudgeScoringDtos.cs b/YD_Event.Application/Judge/Dtos/JudgeScoringDtos.cs index f217f97..5dc82c9 100644 --- a/YD_Event.Application/Judge/Dtos/JudgeScoringDtos.cs +++ b/YD_Event.Application/Judge/Dtos/JudgeScoringDtos.cs @@ -426,6 +426,9 @@ public class JudgeScoreSheetOutput /// public long ScoreSheetId { get; set; } + /// 评分单版本;未产生评分单时为下一待提交版本。 + public int SheetVersion { get; set; } + /// /// 评分单状态:0-草稿,10-已提交,20-已确认,30-已被修正,90-已作废 /// @@ -817,6 +820,9 @@ public class JudgeSymbolScoreDto /// public class JudgeSymbolJudgeDto { + /// 评分单ID。 + public long ScoreSheetId { get; set; } + /// /// 裁判席位编码 /// @@ -831,6 +837,9 @@ public class JudgeSymbolJudgeDto /// 该裁判的得分 /// public string Value { get; set; } = string.Empty; + + /// 该评分单当前记录画板地址。 + public string? AnnotationUrl { get; set; } } /// @@ -838,6 +847,9 @@ public class JudgeSymbolJudgeDto /// public class JudgeCorrectionCandidateDto { + /// 最近一次修正流程ID。 + public long? CorrectionId { get; set; } + /// /// 评分单ID /// @@ -878,6 +890,12 @@ public class JudgeCorrectionCandidateDto /// public string CorrectionStatusName { get; set; } = string.Empty; + /// 修正申请原因。 + public string? ApplyReason { get; set; } + + /// 审批意见。 + public string? ApproveRemark { get; set; } + /// /// 是否可以申请修正 /// @@ -940,6 +958,15 @@ public class JudgeMessageDto /// public long? RelationId { get; set; } + /// 关联场次ID。 + public long? SessionId { get; set; } + + /// 关联出场记录ID。 + public long? SessionEntryId { get; set; } + + /// 关联评分单ID。 + public long? ScoreSheetId { get; set; } + /// /// 是否已读 /// @@ -961,3 +988,23 @@ public class ReadJudgeMessageInput /// public List MessageIds { get; set; } = new(); } + +/// 记录画板 PNG 上传输入。 +public class UploadJudgeScoreAnnotationInput +{ + public long ScoreSheetId { get; set; } + public string IdempotencyKey { get; set; } = null!; + public IFormFile File { get; set; } = null!; +} + +/// 记录画板附件。 +public class JudgeScoreAttachmentDto +{ + public long Id { get; set; } + public long ScoreSheetId { get; set; } + public string AttachmentType { get; set; } = string.Empty; + public string FileName { get; set; } = string.Empty; + public string FileUrl { get; set; } = string.Empty; + public long FileSize { get; set; } + public DateTime UploadedTime { get; set; } +} diff --git a/YD_Event.Application/Judge/JudgeScoringController.cs b/YD_Event.Application/Judge/JudgeScoringController.cs index a8bcbdc..2af9573 100644 --- a/YD_Event.Application/Judge/JudgeScoringController.cs +++ b/YD_Event.Application/Judge/JudgeScoringController.cs @@ -1,5 +1,7 @@ using YD_Event.Application.Auth; +using YD_Event.Application.Common; using YD_Event.Application.Scoring.Dtos; +using System.Text.RegularExpressions; namespace YD_Event.Application.Scoring; @@ -15,6 +17,7 @@ namespace YD_Event.Application.Scoring; /// 裁判提交的分值一律在服务端按计分规则重算,客户端上传的合计值不参与落库。 /// [ApiDescriptionSettings("打分系统")] +[Route("api")] [Authorize] public class JudgeScoringController : IDynamicApiController { @@ -33,11 +36,13 @@ public class JudgeScoringController : IDynamicApiController private readonly ISqlSugarRepository _detailRepository; private readonly ISqlSugarRepository _correctionRepository; private readonly ISqlSugarRepository _messageRepository; + private readonly ISqlSugarRepository _attachmentRepository; private readonly ISqlSugarRepository _resultRepository; private readonly ISqlSugarRepository _personRepository; private readonly EventScoreCalculationService _calculationService; private readonly AuthManager _authManager; private readonly IIdGenerator _idGenerator; + private readonly FileService _fileService; public JudgeScoringController( ISqlSugarRepository userRepository, @@ -55,11 +60,13 @@ public class JudgeScoringController : IDynamicApiController ISqlSugarRepository detailRepository, ISqlSugarRepository correctionRepository, ISqlSugarRepository messageRepository, + ISqlSugarRepository attachmentRepository, ISqlSugarRepository resultRepository, ISqlSugarRepository personRepository, EventScoreCalculationService calculationService, AuthManager authManager, - IIdGenerator idGenerator) + IIdGenerator idGenerator, + FileService fileService) { _userRepository = userRepository; _refereeRepository = refereeRepository; @@ -76,11 +83,13 @@ public class JudgeScoringController : IDynamicApiController _detailRepository = detailRepository; _correctionRepository = correctionRepository; _messageRepository = messageRepository; + _attachmentRepository = attachmentRepository; _resultRepository = resultRepository; _personRepository = personRepository; _calculationService = calculationService; _authManager = authManager; _idGenerator = idGenerator; + _fileService = fileService; } /// @@ -253,6 +262,7 @@ public class JudgeScoringController : IDynamicApiController SessionId = ctx.Session.Id, JudgeTaskId = draft?.JudgeTaskId ?? submitted?.JudgeTaskId ?? 0, ScoreSheetId = source?.Id ?? 0, + SheetVersion = source?.SheetVersion ?? nextVersion, SheetStatus = source?.SheetStatus ?? SheetStatusDraft, CanEdit = canEdit, LockReason = canEdit ? null : "已提交评分,如需修改请先申请成绩修正", @@ -346,6 +356,19 @@ public class JudgeScoringController : IDynamicApiController var nextVersion = (submitted?.SheetVersion ?? 0) + 1; await UpsertSheetAsync(draft, entry, ctx, assignment, roleType, task, calculation, SheetStatusDraft, nextVersion, $"{assignment.Id}-{entry.Id}-v{nextVersion}-draft", null, null, null); + if (submitted != null) + { + var correction = await _correctionRepository.GetFirstAsync(x => x.OriginalScoreSheetId == submitted.Id + && x.CorrectionStatus == CorrectionStatusApproved); + if (correction != null) + { + correction.CorrectionTaskId = task.Id; + correction.CorrectionStatus = CorrectionStatusEditing; + correction.UpdatedUserId = _authManager.UserId; + correction.UpdatedTime = DateTime.Now; + await _correctionRepository.UpdateAsync(correction); + } + } return ToPreviewDto(calculation, ctx.Config); } @@ -387,23 +410,24 @@ public class JudgeScoringController : IDynamicApiController var draft = await _sheetRepository.GetFirstAsync(x => x.SessionEntryId == entry.Id && x.JudgeAssignmentId == assignment.Id && x.SheetStatus == SheetStatusDraft); var nextVersion = (submitted?.SheetVersion ?? 0) + 1; - var sheet = await UpsertSheetAsync(draft, entry, ctx, assignment, roleType, task, calculation, - EventScoreCalculationService.SheetStatusSubmitted, nextVersion, - idempotencyKey ?? $"{assignment.Id}-{entry.Id}-v{nextVersion}", - input.DisqualifyRemark, submitted?.Id, correction?.ApplyReason); - var now = DateTime.Now; if (submitted != null) { - // 修正后的评分单接替原评分单,原评分单留档但不再参与计分。 + // 当前版本唯一索引要求先让原评分单退出当前态;UnitOfWork 会在后续失败时整体回滚。 submitted.IsCurrent = false; submitted.SheetStatus = SheetStatusCorrected; submitted.UpdatedUserId = _authManager.UserId; submitted.UpdatedTime = now; await _sheetRepository.UpdateAsync(submitted); } + var sheet = await UpsertSheetAsync(draft, entry, ctx, assignment, roleType, task, calculation, + EventScoreCalculationService.SheetStatusSubmitted, nextVersion, + idempotencyKey ?? $"{assignment.Id}-{entry.Id}-v{nextVersion}", + input.DisqualifyRemark, submitted?.Id, correction?.ApplyReason); + if (correction != null) { + correction.CorrectionTaskId = task.Id; correction.NewScoreSheetId = sheet.Id; correction.CorrectionStatus = CorrectionStatusCompleted; correction.CompletedTime = now; @@ -432,13 +456,24 @@ public class JudgeScoringController : IDynamicApiController public async Task> GetResults(long sessionId) { var referee = await EnsureRefereeAsync(); - await GetMyAssignmentAsync(sessionId, referee.Id); + var assignment = await GetMyAssignmentAsync(sessionId, referee.Id); + var roleType = JudgeRoleSchemaService.NormalizeRoleType( + string.IsNullOrWhiteSpace(assignment.JudgeRoleType) ? assignment.JudgeRole : assignment.JudgeRoleType); + if (!string.Equals(roleType, JudgeRoleSchemaService.RoleChief, StringComparison.OrdinalIgnoreCase)) + throw Oops.Bah("仅主裁判可查看场次最终成绩"); var ctx = await _calculationService.LoadContextAsync(sessionId); var outcomes = await _calculationService.EvaluateSessionAsync(sessionId, ctx); var entryIds = outcomes.Select(x => (long?)x.SessionEntryId).ToList(); var results = entryIds.Count == 0 ? new List() : await _resultRepository.GetListAsync(x => entryIds.Contains(x.SessionEntryId)); + var scoreSheetIds = outcomes.SelectMany(x => x.Aggregate?.Symbols ?? new List()) + .SelectMany(x => x.Judges).Select(x => x.ScoreSheetId).Distinct().ToList(); + var annotationUrls = scoreSheetIds.Count == 0 + ? new Dictionary() + : (await _attachmentRepository.GetListAsync(x => scoreSheetIds.Contains(x.ScoreSheetId) + && x.IsCurrent && x.AttachmentType == AnnotationAttachmentType)) + .GroupBy(x => x.ScoreSheetId).ToDictionary(x => x.Key, x => x.OrderByDescending(y => y.CreatedTime).First().FileUrl); return outcomes.Select(outcome => { @@ -461,9 +496,11 @@ public class JudgeScoringController : IDynamicApiController Value = ctx.Config.Format(symbol.Value), Judges = symbol.Judges.Select(judge => new JudgeSymbolJudgeDto { + ScoreSheetId = judge.ScoreSheetId, JudgeRole = judge.JudgeRole, JudgeName = judge.JudgeName ?? string.Empty, - Value = ctx.Config.Format(judge.Value) + Value = ctx.Config.Format(judge.Value), + AnnotationUrl = annotationUrls.GetValueOrDefault(judge.ScoreSheetId) }).ToList() }).ToList(), Factors = !ctx.Config.IsFactorTotal || outcome.Aggregate == null @@ -497,18 +534,22 @@ public class JudgeScoringController : IDynamicApiController if (sheets.Count == 0) return new List(); var sheetIds = sheets.Select(x => x.Id).ToList(); - var corrections = await _correctionRepository.GetListAsync(x => sheetIds.Contains(x.OriginalScoreSheetId)); + var corrections = await _correctionRepository.GetListAsync(x => sheetIds.Contains(x.OriginalScoreSheetId) + || (x.NewScoreSheetId.HasValue && sheetIds.Contains(x.NewScoreSheetId.Value))); var entryIds = sheets.Select(x => x.SessionEntryId).Distinct().ToList(); var entries = (await _entryRepository.GetListAsync(x => entryIds.Contains(x.Id))).ToDictionary(x => x.Id); return sheets.Select(sheet => { var entry = entries.GetValueOrDefault(sheet.SessionEntryId); - var correction = corrections.Where(x => x.OriginalScoreSheetId == sheet.Id) + // 修正完成后当前评分单已经变成 NewScoreSheetId,仍需返回最近一次流程状态, + // 否则 App 会把刚完成的修正误显示成“可首次申请”。 + var correction = corrections.Where(x => x.OriginalScoreSheetId == sheet.Id || x.NewScoreSheetId == sheet.Id) .OrderByDescending(x => x.CreatedTime).FirstOrDefault(); var status = correction?.CorrectionStatus ?? -1; return new JudgeCorrectionCandidateDto { + CorrectionId = correction?.Id, ScoreSheetId = sheet.Id, SessionEntryId = sheet.SessionEntryId, EntryNo = entry?.EntryNo ?? string.Empty, @@ -518,6 +559,8 @@ public class JudgeScoringController : IDynamicApiController SubmittedTime = sheet.SubmittedTime, CorrectionStatus = status, CorrectionStatusName = CorrectionStatusName(status), + ApplyReason = correction?.ApplyReason, + ApproveRemark = correction?.ApproveRemark, // 已在流程中的评分单不能重复申请,被拒绝或已完成的可以再次申请。 CanApply = status is -1 or CorrectionStatusRejected or CorrectionStatusCompleted or CorrectionStatusCancelled }; @@ -552,7 +595,7 @@ public class JudgeScoringController : IDynamicApiController OriginalScoreSheetId = sheet.Id, JudgeAssignmentId = sheet.JudgeAssignmentId, JudgeRole = sheet.JudgeRole, - ApplyUserId = _authManager.UserId, + ApplyRefereeId = referee.Id, ApplyReason = input.ApplyReason.Trim(), CorrectionStatus = ctx.Config.CorrectionNeedApproval ? CorrectionStatusApplied : CorrectionStatusApproved, CreatedUserId = _authManager.UserId, @@ -581,10 +624,21 @@ public class JudgeScoringController : IDynamicApiController .WhereIF(onlyUnread == true, x => !x.IsRead) .ToListAsync()) .OrderByDescending(x => x.CreatedTime).ToList(); + var correctionIds = messages + .Where(x => string.Equals(x.RelationType, nameof(EventScoreCorrection), StringComparison.OrdinalIgnoreCase) + && x.RelationId.HasValue) + .Select(x => x.RelationId!.Value).Distinct().ToList(); + var corrections = correctionIds.Count == 0 + ? new Dictionary() + : (await _correctionRepository.GetListAsync(x => correctionIds.Contains(x.Id))).ToDictionary(x => x.Id); return messages.Select(x => new JudgeMessageDto { Id = x.Id, EventId = x.EventId, MessageType = x.MessageType, Title = x.Title, Content = x.Content, - RelationType = x.RelationType, RelationId = x.RelationId, IsRead = x.IsRead, CreatedTime = x.CreatedTime + RelationType = x.RelationType, RelationId = x.RelationId, + SessionId = x.RelationId.HasValue ? corrections.GetValueOrDefault(x.RelationId.Value)?.SessionId : null, + SessionEntryId = x.RelationId.HasValue ? corrections.GetValueOrDefault(x.RelationId.Value)?.SessionEntryId : null, + ScoreSheetId = x.RelationId.HasValue ? corrections.GetValueOrDefault(x.RelationId.Value)?.OriginalScoreSheetId : null, + IsRead = x.IsRead, CreatedTime = x.CreatedTime }).ToList(); } @@ -607,6 +661,82 @@ public class JudgeScoringController : IDynamicApiController await _messageRepository.UpdateRangeAsync(messages); } + /// 上传当前裁判评分单的记录画板 PNG。 + [HttpPost("judge-scoring/annotations/upload")] + [DisplayName("上传裁判记录画板")] + [UnitOfWork] + public async Task UploadAnnotation([FromForm] UploadJudgeScoreAnnotationInput input) + { + var referee = await EnsureRefereeAsync(); + var sheet = await _sheetRepository.GetFirstAsync(x => x.Id == input.ScoreSheetId && x.RefereeId == referee.Id); + if (sheet == null) throw Oops.Bah("评分单不存在或不属于当前裁判"); + if (sheet.SheetStatus < EventScoreCalculationService.SheetStatusSubmitted) + throw Oops.Bah("请先提交成绩,再上传记录画板"); + var key = input.IdempotencyKey?.Trim() ?? string.Empty; + if (key.Length is < 8 or > 64 || !Regex.IsMatch(key, "^[A-Za-z0-9_-]+$")) + throw Oops.Bah("画板上传幂等键无效"); + var replay = await _attachmentRepository.GetFirstAsync(x => x.ScoreSheetId == sheet.Id && x.IdempotencyKey == key); + if (replay != null) return ToAttachmentDto(replay); + + var file = input.File; + if (file == null || file.Length == 0) throw Oops.Bah("请选择画板 PNG 文件"); + if (file.Length > MaxAnnotationBytes) throw Oops.Bah("画板图片不能超过5MB"); + if (!string.Equals(Path.GetExtension(file.FileName), ".png", StringComparison.OrdinalIgnoreCase) + || !string.Equals(file.ContentType, "image/png", StringComparison.OrdinalIgnoreCase)) + throw Oops.Bah("画板仅支持PNG格式"); + await using var stream = file.OpenReadStream(); + var signature = new byte[8]; + if (await stream.ReadAsync(signature.AsMemory(0, signature.Length)) != signature.Length + || !signature.SequenceEqual(PngSignature)) + 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); + + var now = DateTime.Now; + var current = await _attachmentRepository.GetListAsync(x => x.ScoreSheetId == sheet.Id + && x.AttachmentType == AnnotationAttachmentType && x.IsCurrent); + foreach (var old in current) + { + old.IsCurrent = false; + old.UpdatedUserId = _authManager.UserId; + old.UpdatedTime = now; + } + if (current.Count > 0) await _attachmentRepository.UpdateRangeAsync(current); + var attachment = new EventJudgeScoreAttachment + { + Id = _idGenerator.NextId(), EventId = sheet.EventId, SessionId = sheet.SessionId, + SessionEntryId = sheet.SessionEntryId, ScoreSheetId = sheet.Id, + JudgeAssignmentId = sheet.JudgeAssignmentId, RefereeId = referee.Id, + AttachmentType = AnnotationAttachmentType, FileName = Path.GetFileName(file.FileName), + FileUrl = url, FileSize = file.Length, ContentType = "image/png", IdempotencyKey = key, + IsCurrent = true, CreatedUserId = _authManager.UserId, CreatedTime = now + }; + await _attachmentRepository.InsertAsync(attachment); + return ToAttachmentDto(attachment); + } + + /// 查询评分单当前记录画板。 + [HttpGet("judge-scoring/annotations/{scoreSheetId}")] + [DisplayName("查询裁判记录画板")] + public async Task GetAnnotation(long scoreSheetId) + { + var referee = await EnsureRefereeAsync(); + var sheet = await _sheetRepository.GetFirstAsync(x => x.Id == scoreSheetId); + if (sheet == null) throw Oops.Bah("评分单不存在"); + if (sheet.RefereeId != referee.Id) + { + var assignment = await GetMyAssignmentAsync(sheet.SessionId, referee.Id); + var roleType = JudgeRoleSchemaService.NormalizeRoleType( + string.IsNullOrWhiteSpace(assignment.JudgeRoleType) ? assignment.JudgeRole : assignment.JudgeRoleType); + if (!string.Equals(roleType, JudgeRoleSchemaService.RoleChief, StringComparison.OrdinalIgnoreCase)) + throw Oops.Bah("无权查看该评分单画板"); + } + var attachment = await _attachmentRepository.GetFirstAsync(x => x.ScoreSheetId == scoreSheetId + && x.AttachmentType == AnnotationAttachmentType && x.IsCurrent); + return attachment == null ? null : ToAttachmentDto(attachment); + } + /// /// 校验当前登录用户的裁判身份并取出裁判档案 /// @@ -668,6 +798,17 @@ public class JudgeScoringController : IDynamicApiController || x.CorrectionStatus == CorrectionStatusApproved || x.CorrectionStatus == CorrectionStatusEditing)); + private static JudgeScoreAttachmentDto ToAttachmentDto(EventJudgeScoreAttachment attachment) => new() + { + Id = attachment.Id, + ScoreSheetId = attachment.ScoreSheetId, + AttachmentType = attachment.AttachmentType, + FileName = attachment.FileName, + FileUrl = attachment.FileUrl, + FileSize = attachment.FileSize, + UploadedTime = attachment.CreatedTime + }; + /// /// 取(或按需创建)本裁判在该出场上的评分任务 /// @@ -675,7 +816,8 @@ public class JudgeScoringController : IDynamicApiController EventJudgeAssignment assignment, string roleType, int taskType) { var task = await _taskRepository.GetFirstAsync(x => x.SessionEntryId == entry.Id - && x.JudgeAssignmentId == assignment.Id && x.TaskType == taskType && x.TaskStatus != TaskStatusVoided); + && x.JudgeAssignmentId == assignment.Id && x.TaskType == taskType && x.TaskStatus != TaskStatusVoided + && (taskType != TaskTypeCorrection || x.TaskStatus != TaskStatusSubmitted)); var now = DateTime.Now; if (task != null) { @@ -690,6 +832,9 @@ public class JudgeScoringController : IDynamicApiController return task; } + var taskHistory = await _taskRepository.GetListAsync(x => x.SessionEntryId == entry.Id + && x.JudgeAssignmentId == assignment.Id && !x.DeleteMark); + var previousTask = taskHistory.OrderByDescending(x => x.TaskVersion).FirstOrDefault(); task = new EventJudgeTask { Id = _idGenerator.NextId(), @@ -718,6 +863,8 @@ public class JudgeScoringController : IDynamicApiController scope = ctx.Scope }), TaskType = taskType, + TaskVersion = (previousTask?.TaskVersion ?? 0) + 1, + OriginalTaskId = taskType == TaskTypeCorrection ? previousTask?.Id : null, TaskStatus = TaskStatusAccepted, DispatchedTime = now, AcceptedTime = now, @@ -826,7 +973,9 @@ public class JudgeScoringController : IDynamicApiController IsDuplicate = isDuplicate, MyScore = ctx.Config.Format(sheet.TotalScore), ResultCompleted = outcome?.IsCompleted ?? false, - TotalScore = outcome == null || !outcome.IsCompleted ? null + TotalScore = !string.Equals(JudgeRoleSchemaService.NormalizeRoleType(sheet.JudgeRoleType), + JudgeRoleSchemaService.RoleChief, StringComparison.OrdinalIgnoreCase) + || outcome == null || !outcome.IsCompleted ? null : outcome.IsDisqualified ? "取消资格" : ctx.Config.Format(outcome.TotalScore), Progress = (outcome?.Roles ?? new List()).Select(ToProgressDto).ToList() }; @@ -938,6 +1087,10 @@ public class JudgeScoringController : IDynamicApiController /// private const int CorrectionStatusCancelled = 90; + private const string AnnotationAttachmentType = "score_annotation_png"; + private const long MaxAnnotationBytes = 5L * 1024 * 1024; + private static readonly byte[] PngSignature = [137, 80, 78, 71, 13, 10, 26, 10]; + /// /// 出场状态:退赛 /// diff --git a/YD_Event.Application/Workbench/WorkbenchController.cs b/YD_Event.Application/Workbench/WorkbenchController.cs index cb2c791..195751a 100644 --- a/YD_Event.Application/Workbench/WorkbenchController.cs +++ b/YD_Event.Application/Workbench/WorkbenchController.cs @@ -601,7 +601,10 @@ public class WorkbenchController : IDynamicApiController var sessionIds = corrections.Select(c => c.SessionId).Distinct().ToList(); var entryIds = corrections.Select(c => c.SessionEntryId).Distinct().ToList(); - var applyUserIds = corrections.Select(c => c.ApplyUserId).Distinct().ToList(); + var applyUserIds = corrections.Where(c => c.ApplyUserId.HasValue) + .Select(c => c.ApplyUserId!.Value).Distinct().ToList(); + var applyRefereeIds = corrections.Where(c => c.ApplyRefereeId.HasValue) + .Select(c => c.ApplyRefereeId!.Value).Distinct().ToList(); var approveUserIds = corrections.Where(c => c.ApproveUserId.HasValue).Select(c => c.ApproveUserId!.Value).Distinct().ToList(); var sessions = (await _sessionRepository.AsQueryable() @@ -619,11 +622,16 @@ public class WorkbenchController : IDynamicApiController var eventInfo = await _eventRepository.GetFirstAsync(e => e.Id == input.EventId); var allUserIds = applyUserIds.Concat(approveUserIds).Distinct().ToList(); - var users = (await _userRepository.AsQueryable() + var users = (await _sysUserRepository.AsQueryable() .Where(u => allUserIds.Contains(u.Id)) .Select(u => new { u.Id, u.Name }) .ToListAsync()) .ToDictionary(u => u.Id, u => u.Name); + var applyingReferees = (await _refereeRepository.AsQueryable() + .Where(r => applyRefereeIds.Contains(r.Id)) + .Select(r => new { r.Id, r.Name }) + .ToListAsync()) + .ToDictionary(r => r.Id, r => r.Name); return corrections.Select(c => { @@ -636,7 +644,9 @@ public class WorkbenchController : IDynamicApiController EntryNo = entry?.EntryNo ?? "", ParticipantName = entry?.ParticipantName ?? "", JudgeRole = c.JudgeRole, - ApplyUserName = users.GetValueOrDefault(c.ApplyUserId, ""), + ApplyUserName = c.ApplyRefereeId.HasValue + ? applyingReferees.GetValueOrDefault(c.ApplyRefereeId.Value, "") + : c.ApplyUserId.HasValue ? users.GetValueOrDefault(c.ApplyUserId.Value, "") : "", ApplyReason = c.ApplyReason, CreatedTime = c.CreatedTime, CorrectionStatus = c.CorrectionStatus, @@ -679,7 +689,12 @@ public class WorkbenchController : IDynamicApiController var eventProject = await _eventProjectRepository.GetByIdAsync(session.EventProjectId); var project = eventProject != null ? await _projectRepository.GetByIdAsync(eventProject.ProjectId) : null; - var applyUser = await _userRepository.GetByIdAsync(correction.ApplyUserId); + var applyUser = correction.ApplyUserId.HasValue + ? await _sysUserRepository.GetByIdAsync(correction.ApplyUserId.Value) + : null; + var applyReferee = correction.ApplyRefereeId.HasValue + ? await _refereeRepository.GetByIdAsync(correction.ApplyRefereeId.Value) + : null; var approveUser = correction.ApproveUserId.HasValue ? await _userRepository.GetByIdAsync(correction.ApproveUserId.Value) : null; @@ -765,7 +780,7 @@ public class WorkbenchController : IDynamicApiController SubmittedTime = newSheet.SubmittedTime, HasPendingCorrection = false } : null, - ApplyUserName = applyUser?.Name ?? "", + ApplyUserName = applyReferee?.Name ?? applyUser?.Name ?? "", ApplyReason = correction.ApplyReason, CreatedTime = correction.CreatedTime, CorrectionStatus = correction.CorrectionStatus, diff --git a/YD_Event.Application/YD_Event.Application.xml b/YD_Event.Application/YD_Event.Application.xml index 0d4348a..82d3682 100644 --- a/YD_Event.Application/YD_Event.Application.xml +++ b/YD_Event.Application/YD_Event.Application.xml @@ -10614,6 +10614,9 @@ 是否由该裁判判定取消比赛资格 + + 裁判记录画板地址。 + 后台记分台:单个计分符号的聚合得分 @@ -12326,6 +12329,9 @@ 评分单ID(尚未保存过时为0) + + 评分单版本;未产生评分单时为下一待提交版本。 + 评分单状态:0-草稿,10-已提交,20-已确认,30-已被修正,90-已作废 @@ -12706,6 +12712,9 @@ 参与聚合的单个裁判得分 + + 评分单ID。 + 裁判席位编码 @@ -12721,11 +12730,17 @@ 该裁判的得分 + + 该评分单当前记录画板地址。 + 可申请成绩修正的评分单 + + 最近一次修正流程ID。 + 评分单ID @@ -12766,6 +12781,12 @@ 修正申请状态名称 + + 修正申请原因。 + + + 审批意见。 + 是否可以申请修正 @@ -12826,6 +12847,15 @@ 关联业务ID + + 关联场次ID。 + + + 关联出场记录ID。 + + + 关联评分单ID。 + 是否已读 @@ -12846,6 +12876,12 @@ 消息ID,为空表示全部标记已读 + + 记录画板 PNG 上传输入。 + + + 记录画板附件。 + 一个场次的计分上下文:场次、项目、计分规则与评分项。裁判端取评分表、后台算成绩都基于同一份上下文。 @@ -13330,6 +13366,12 @@ 标记消息已读,不传消息ID表示全部标记已读 + + 上传当前裁判评分单的记录画板 PNG。 + + + 查询评分单当前记录画板。 + 校验当前登录用户的裁判身份并取出裁判档案 @@ -15568,34 +15610,54 @@ - 序号(仅便于人工核对,导入时不参与落库) + + 序号(仅便于人工核对,导入时不参与落库) + - 场地名称(必填) + + 场地名称(必填) + - 项目名称(必填,需与赛事已配置项目一致,不存在将自动建档) + + 项目名称(必填,需与赛事已配置项目一致,不存在将自动建档) + - 组别名称(可空,空则归入“公开组”) + + 组别名称(可空,空则归入"公开组") + - 出场编号(可空,空则按同场次顺序自动生成) + + 出场编号(可空,空则按同场次顺序自动生成) + - 姓名/队名(必填) + + 姓名/队名(必填) + - 所属单位(可空,仅用于核对展示) + + 所属单位(可空,仅用于核对展示) + - 联系电话(可空) + + 联系电话(可空) + - 证件号(可空) + + 证件号(可空) + - 备注(可空) + + 备注(可空) + @@ -15603,25 +15665,39 @@ - 序号(仅便于人工核对) + + 序号(仅便于人工核对) + - 裁判姓名(必填) + + 裁判姓名(必填) + - 手机号(必填,用于匹配/创建裁判档案及打分账号) + + 手机号(必填,用于匹配/创建裁判档案及打分账号) + - 裁判角色(可空,如:主裁/难度裁判/表现裁判) + + 裁判角色(可空,如:主裁/难度裁判/表现裁判) + - 裁判等级(可空,如:国际级/国家级/一级/二级/三级) + + 裁判等级(可空,如:国际级/国家级/一级/二级/三级) + - 负责场地(可空,对应出场秩序中的场地名称) + + 负责场地(可空,对应出场秩序中的场地名称) + - 负责项目(可空) + + 负责项目(可空) + @@ -15629,28 +15705,44 @@ - 导入批次ID(用于确认导入) + + 导入批次ID(用于确认导入) + - 解析状态:0-解析中,10-待确认,20-存在错误 + + 解析状态:0-解析中,10-待确认,20-存在错误 + - 原始数据行数(不含表头) + + 原始数据行数(不含表头) + - 有效行数 + + 有效行数 + - 错误数量(存在错误时不可确认) + + 错误数量(存在错误时不可确认) + - 警告数量(不阻断确认) + + 警告数量(不阻断确认) + - 解析预览统计 + + 解析预览统计 + - 问题清单(错误/警告) + + 问题清单(错误/警告) + @@ -15658,16 +15750,24 @@ - 场地数量 + + 场地数量 + - 场次数量(项目×组别×场地) + + 场次数量(项目×组别×场地) + - 出场记录数量 + + 出场记录数量 + - 裁判数量 + + 裁判数量 + @@ -15675,16 +15775,24 @@ - 所在行号(Excel 物理行号,含表头) + + 所在行号(Excel 物理行号,含表头) + - 所在列名 + + 所在列名 + - 问题描述 + + 问题描述 + - 级别:error-错误(阻断确认),warning-警告(不阻断) + + 级别:error-错误(阻断确认),warning-警告(不阻断) + @@ -15692,13 +15800,19 @@ - 导入批次ID(必填,由导入接口返回) + + 导入批次ID(必填,由导入接口返回) + - 是否为裁判生成小程序打分账号 + + 是否为裁判生成小程序打分账号 + - 打分账号初始密码(生成账号时必填,长度6-32位) + + 打分账号初始密码(生成账号时必填,长度6-32位) + @@ -15706,19 +15820,29 @@ - 秩序册版本号 + + 秩序册版本号 + - 生成场地数量 + + 生成场地数量 + - 生成场次数量 + + 生成场次数量 + - 生成出场记录数量 + + 生成出场记录数量 + - 纳入裁判数量 + + 纳入裁判数量 + @@ -15726,28 +15850,44 @@ - 批次ID + + 批次ID + - 创建该导入批次的后台用户ID + + 创建该导入批次的后台用户ID + - 解析结果是否包含阻断确认的错误 + + 解析结果是否包含阻断确认的错误 + - 赛事ID(当前版本要求赛事已预先创建) + + 赛事ID(当前版本要求赛事已预先创建) + - 导入模式:append-追加(默认),overwrite-覆盖该赛事既有编排 + + 导入模式:append-追加(默认),overwrite-覆盖该赛事既有编排 + - 解析时间 + + 解析时间 + - 出场明细(已通过基础校验) + + 出场明细(已通过基础校验) + - 裁判明细 + + 裁判明细 + @@ -15755,28 +15895,44 @@ - Excel 物理行号 + + Excel 物理行号 + - 场地名称 + + 场地名称 + - 项目名称 + + 项目名称 + - 组别名称 + + 组别名称 + - 原始出场编号(可空) + + 原始出场编号(可空) + - 姓名/队名 + + 姓名/队名 + - 单位 + + 单位 + - 联系电话 + + 联系电话 + @@ -15784,25 +15940,39 @@ - Excel 物理行号 + + Excel 物理行号 + - 裁判姓名 + + 裁判姓名 + - 手机号 + + 手机号 + - 裁判角色文本 + + 裁判角色文本 + - 裁判等级文本 + + 裁判等级文本 + - 负责场地名称 + + 负责场地名称 + - 负责项目名称 + + 负责项目名称 + @@ -15813,31 +15983,24 @@ 下载秩序册导入模板 + + 上传并同步解析秩序册(不产生业务数据,结果缓存待确认) + + + + 确认导入:按解析结果生成场地、场次、出场及裁判名单(幂等,同一批次只生成一次) - - - 项目(含赛事项目、组别)建档,返回 (项目名,组别名) -> 上下文 的映射 - - - 场地建档(追加模式按名称复用),返回场地名 -> 场地上下文 的映射 - - - 按 项目×组别×场地 生成场次,返回组合键 -> 场次 的映射 - - - 生成出场记录,返回数量 - - - 裁判档案 / 赛事裁判关系 / 可选打分账号,返回纳入的裁判数量 + + @@ -19237,19 +19400,29 @@ - 赛事ID(必填) + + 赛事ID(必填) + - 赛事项目ID(可选) + + 赛事项目ID(可选) + - 组别名称(可选,模糊匹配) + + 组别名称(可选,模糊匹配) + - 成绩状态(可选,默认20-已确认;0-计算中,10-待复核,20-已确认,90-已作废) + + 成绩状态(可选,默认20-已确认;0-计算中,10-待复核,20-已确认,90-已作废) + - 是否包含被取消比赛资格(DQ)的记录,默认不包含 + + 是否包含被取消比赛资格(DQ)的记录,默认不包含 + @@ -19257,13 +19430,19 @@ - 赛事ID(必填) + + 赛事ID(必填) + - 场次ID(与赛事项目ID至少传一个) + + 场次ID(与赛事项目ID至少传一个) + - 赛事项目ID(与场次ID至少传一个) + + 赛事项目ID(与场次ID至少传一个) + @@ -19271,19 +19450,29 @@ - 赛事ID(必填,按请求参数中的赛事ID匹配) + + 赛事ID(必填,按请求参数中的赛事ID匹配) + - 场次ID(可选) + + 场次ID(可选) + - 开始时间(可选) + + 开始时间(可选) + - 结束时间(可选) + + 结束时间(可选) + - 操作人ID(可选) + + 操作人ID(可选) + diff --git a/YD_Event.Core/Entities/EventJudgeScoreAttachment.cs b/YD_Event.Core/Entities/EventJudgeScoreAttachment.cs new file mode 100644 index 0000000..96d2aad --- /dev/null +++ b/YD_Event.Core/Entities/EventJudgeScoreAttachment.cs @@ -0,0 +1,39 @@ +namespace YD_Event.Core.Entities; + +/// +/// 裁判评分单附件。每次上传保留一个版本,IsCurrent 指向评分单当前画板。 +/// +[SugarTable("EventJudgeScoreAttachment")] +public class EventJudgeScoreAttachment : Entity, ISoftDelete, ICreatedUserId, ICreatedTime, IUpdatedUserId, IUpdatedTime +{ + public long EventId { get; set; } + public long SessionId { get; set; } + public long SessionEntryId { get; set; } + public long ScoreSheetId { get; set; } + public long JudgeAssignmentId { get; set; } + public long RefereeId { get; set; } + + [SugarColumn(Length = 32)] + public string AttachmentType { get; set; } = "score_annotation_png"; + + [SugarColumn(Length = 128)] + public string FileName { get; set; } = null!; + + [SugarColumn(Length = 1000)] + public string FileUrl { get; set; } = null!; + + public long FileSize { get; set; } + + [SugarColumn(Length = 64)] + public string ContentType { get; set; } = "image/png"; + + [SugarColumn(Length = 64)] + public string IdempotencyKey { get; set; } = null!; + + public bool IsCurrent { get; set; } = true; + public bool DeleteMark { get; set; } + public long CreatedUserId { get; set; } + public DateTime CreatedTime { get; set; } + [SugarColumn(IsNullable = true)] public long? UpdatedUserId { get; set; } + [SugarColumn(IsNullable = true)] public DateTime? UpdatedTime { get; set; } +} diff --git a/YD_Event.Core/Entities/EventScoreCorrection.cs b/YD_Event.Core/Entities/EventScoreCorrection.cs index 8b6b9de..60a3fec 100644 --- a/YD_Event.Core/Entities/EventScoreCorrection.cs +++ b/YD_Event.Core/Entities/EventScoreCorrection.cs @@ -50,9 +50,16 @@ public class EventScoreCorrection : Entity, ISoftDelete, ICreatedUserId, I public long? NewScoreSheetId { get; set; } /// - /// 申请人ID + /// 后台申请人ID(SysUser)。裁判端申请时为空。 /// - public long ApplyUserId { get; set; } + [SugarColumn(IsNullable = true)] + public long? ApplyUserId { get; set; } + + /// + /// 裁判端申请人ID(Referee)。后台发起时为空。 + /// + [SugarColumn(IsNullable = true)] + public long? ApplyRefereeId { get; set; } /// /// 申请修正原因 diff --git a/YD_Event.Core/YD_Event.Core.xml b/YD_Event.Core/YD_Event.Core.xml index 97e2925..6f04b17 100644 --- a/YD_Event.Core/YD_Event.Core.xml +++ b/YD_Event.Core/YD_Event.Core.xml @@ -1873,6 +1873,11 @@ 更新时间 + + + 裁判评分单附件。每次上传保留一个版本,IsCurrent 指向评分单当前画板。 + + 裁判评分明细表 @@ -3580,7 +3585,12 @@ - 申请人ID + 后台申请人ID(SysUser)。裁判端申请时为空。 + + + + + 裁判端申请人ID(Referee)。后台发起时为空。 @@ -8638,6 +8648,11 @@ 拦截器 + + + 有有效登录用户时以当前用户为准;无用户上下文时保留后台任务显式传入的操作者。 + + 初始化基础数据 diff --git a/二期需求/20260903_add_score_attachments.sql b/二期需求/20260903_add_score_attachments.sql new file mode 100644 index 0000000..7e0ef71 --- /dev/null +++ b/二期需求/20260903_add_score_attachments.sql @@ -0,0 +1,29 @@ +SET XACT_ABORT ON; +BEGIN TRANSACTION; + +IF OBJECT_ID(N'dbo.[EventJudgeScoreAttachment]','U') IS NULL +BEGIN + CREATE TABLE dbo.[EventJudgeScoreAttachment] ( + [Id] BIGINT NOT NULL CONSTRAINT [PK_EventJudgeScoreAttachment] PRIMARY KEY, + [EventId] BIGINT NOT NULL, [SessionId] BIGINT NOT NULL, + [SessionEntryId] BIGINT NOT NULL, [ScoreSheetId] BIGINT NOT NULL, + [JudgeAssignmentId] BIGINT NOT NULL, [RefereeId] BIGINT NOT NULL, + [AttachmentType] NVARCHAR(32) NOT NULL, [FileName] NVARCHAR(128) NOT NULL, + [FileUrl] NVARCHAR(1000) NOT NULL, [FileSize] BIGINT NOT NULL, + [ContentType] NVARCHAR(64) NOT NULL, [IdempotencyKey] NVARCHAR(64) NOT NULL, + [IsCurrent] BIT NOT NULL CONSTRAINT [DF_EventJudgeScoreAttachment_IsCurrent] DEFAULT (1), + [DeleteMark] BIT NOT NULL CONSTRAINT [DF_EventJudgeScoreAttachment_DeleteMark] DEFAULT (0), + [CreatedUserId] BIGINT NOT NULL, [CreatedTime] DATETIME2(3) NOT NULL, + [UpdatedUserId] BIGINT NULL, [UpdatedTime] DATETIME2(3) NULL + ); +END; + +IF OBJECT_ID(N'dbo.[FK_EventJudgeScoreAttachment_Sheet]','F') IS NULL + ALTER TABLE dbo.[EventJudgeScoreAttachment] WITH CHECK + ADD CONSTRAINT [FK_EventJudgeScoreAttachment_Sheet] FOREIGN KEY([ScoreSheetId]) REFERENCES dbo.[EventJudgeScoreSheet]([Id]); +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; + +COMMIT TRANSACTION; diff --git a/二期需求/20260904_support_referee_score_corrections.sql b/二期需求/20260904_support_referee_score_corrections.sql new file mode 100644 index 0000000..622f1c2 --- /dev/null +++ b/二期需求/20260904_support_referee_score_corrections.sql @@ -0,0 +1,28 @@ +SET XACT_ABORT ON; +BEGIN TRANSACTION; + +IF COL_LENGTH(N'dbo.EventScoreCorrection', N'ApplyRefereeId') IS NULL + EXEC(N'ALTER TABLE dbo.EventScoreCorrection ADD ApplyRefereeId BIGINT NULL;'); + +IF EXISTS ( + SELECT 1 + FROM sys.columns + WHERE object_id = OBJECT_ID(N'dbo.EventScoreCorrection') + AND name = N'ApplyUserId' + AND is_nullable = 0 +) + ALTER TABLE dbo.EventScoreCorrection ALTER COLUMN ApplyUserId BIGINT NULL; + +IF OBJECT_ID(N'dbo.FK_EventScoreCorrection_ApplyReferee', N'F') IS NULL + EXEC(N'ALTER TABLE dbo.EventScoreCorrection WITH CHECK + ADD CONSTRAINT FK_EventScoreCorrection_ApplyReferee + FOREIGN KEY (ApplyRefereeId) REFERENCES dbo.Referee(Id);'); + +IF OBJECT_ID(N'dbo.CK_EventScoreCorrection_Applicant', N'C') IS NULL + EXEC(N'ALTER TABLE dbo.EventScoreCorrection WITH CHECK + ADD CONSTRAINT CK_EventScoreCorrection_Applicant CHECK ( + (ApplyUserId IS NOT NULL AND ApplyRefereeId IS NULL) + OR (ApplyUserId IS NULL AND ApplyRefereeId IS NOT NULL) + );'); + +COMMIT TRANSACTION; diff --git a/二期需求/20260907_fix_score_correction_open_index.sql b/二期需求/20260907_fix_score_correction_open_index.sql new file mode 100644 index 0000000..2a93e1e --- /dev/null +++ b/二期需求/20260907_fix_score_correction_open_index.sql @@ -0,0 +1,25 @@ +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; +SET XACT_ABORT ON; +BEGIN TRANSACTION; + +IF EXISTS ( + SELECT 1 FROM sys.indexes + WHERE object_id = OBJECT_ID(N'dbo.EventScoreCorrection') + AND name = N'UX_EventScoreCorrection_OriginalSheet_Open' +) + DROP INDEX UX_EventScoreCorrection_OriginalSheet_Open ON dbo.EventScoreCorrection; + +CREATE UNIQUE NONCLUSTERED INDEX UX_EventScoreCorrection_OriginalSheet_Open +ON dbo.EventScoreCorrection(OriginalScoreSheetId) +WHERE DeleteMark = 0 + AND CorrectionStatus >= 0 + AND CorrectionStatus <= 30 + AND CorrectionStatus <> 20; + +COMMIT TRANSACTION; diff --git a/二期需求/dbo.sql b/二期需求/dbo.sql index 7939861..2f0bfcf 100644 --- a/二期需求/dbo.sql +++ b/二期需求/dbo.sql @@ -34,6 +34,14 @@ BEGIN TRY EXEC @LockResult=sys.sp_getapplock @Resource=N'YD_Event_Phase2_Production_Deployment_v3',@LockMode='Exclusive',@LockOwner='Transaction',@LockTimeout=10000; IF @LockResult<0 THROW 51002,N'无法取得二期部署排他锁,可能有另一个部署正在执行。',1; + /* EventScoreCorrection:同时支持后台账号与裁判账号发起修正。 */ + IF OBJECT_ID(N'dbo.EventScoreCorrection','U') IS NOT NULL AND COL_LENGTH(N'dbo.EventScoreCorrection', N'ApplyRefereeId') IS NULL + ALTER TABLE dbo.EventScoreCorrection ADD ApplyRefereeId BIGINT NULL; + IF OBJECT_ID(N'dbo.EventScoreCorrection','U') IS NOT NULL AND EXISTS ( + SELECT 1 FROM sys.columns WHERE object_id=OBJECT_ID(N'dbo.EventScoreCorrection') AND name=N'ApplyUserId' AND is_nullable=0 + ) + ALTER TABLE dbo.EventScoreCorrection ALTER COLUMN ApplyUserId BIGINT NULL; + /* ==================================================================================== [1/9] 新增表 · 创建二期业务表(共 19 张;IF OBJECT_ID 判空,可重复执行) 说明:新表外键只引用一期已有表主键(Id),不依赖 [2/9] 新增列,因此置于“改表结构”之前。 @@ -441,6 +449,32 @@ BEGIN TRY ); END; + /* EventJudgeScoreAttachment:评分单记录画板,按上传版本留档。 */ + IF OBJECT_ID(N'dbo.[EventJudgeScoreAttachment]','U') IS NULL + BEGIN + CREATE TABLE dbo.[EventJudgeScoreAttachment] ( + [Id] BIGINT NOT NULL CONSTRAINT [PK_EventJudgeScoreAttachment] PRIMARY KEY, + [EventId] BIGINT NOT NULL, + [SessionId] BIGINT NOT NULL, + [SessionEntryId] BIGINT NOT NULL, + [ScoreSheetId] BIGINT NOT NULL, + [JudgeAssignmentId] BIGINT NOT NULL, + [RefereeId] BIGINT NOT NULL, + [AttachmentType] NVARCHAR(32) NOT NULL, + [FileName] NVARCHAR(128) NOT NULL, + [FileUrl] NVARCHAR(1000) NOT NULL, + [FileSize] BIGINT NOT NULL, + [ContentType] NVARCHAR(64) NOT NULL, + [IdempotencyKey] NVARCHAR(64) NOT NULL, + [IsCurrent] BIT NOT NULL CONSTRAINT [DF_EventJudgeScoreAttachment_IsCurrent] DEFAULT (1), + [DeleteMark] BIT NOT NULL CONSTRAINT [DF_EventJudgeScoreAttachment_DeleteMark] DEFAULT (0), + [CreatedUserId] BIGINT NOT NULL, + [CreatedTime] DATETIME2(3) NOT NULL, + [UpdatedUserId] BIGINT NULL, + [UpdatedTime] DATETIME2(3) NULL + ); + END; + /* EventJudgeScoreDetail:评分单逐项录入和计算明细。 */ IF OBJECT_ID(N'dbo.[EventJudgeScoreDetail]','U') IS NULL BEGIN @@ -514,7 +548,8 @@ BEGIN TRY [JudgeRole] NVARCHAR(32) NOT NULL, -- 裁判席位编码 [CorrectionTaskId] BIGINT NULL, -- 修正任务Id [NewScoreSheetId] BIGINT NULL, -- 修正完成后产生的新评分单Id - [ApplyUserId] BIGINT NOT NULL, -- 申请人Id + [ApplyUserId] BIGINT NULL, -- 后台申请人Id(SysUser),裁判端申请时为空 + [ApplyRefereeId] BIGINT NULL, -- 裁判端申请人Id(Referee),后台发起时为空 [ApplyReason] NVARCHAR(512) NOT NULL, -- 申请修正原因 [ApproveUserId] BIGINT NULL, -- 审批人Id [ApproveRemark] NVARCHAR(512) NULL, -- 审批意见 @@ -637,6 +672,7 @@ BEGIN TRY IF OBJECT_ID('CK_EventSession_Time','C') IS NULL ALTER TABLE dbo.EventSession WITH CHECK ADD CONSTRAINT CK_EventSession_Time CHECK(EndTime IS NULL OR StartTime IS NULL OR EndTime>=StartTime); IF OBJECT_ID('CK_ScoreRule_Version','C') IS NULL ALTER TABLE dbo.ScoreRule WITH CHECK ADD CONSTRAINT CK_ScoreRule_Version CHECK(RuleVersion>=1); IF OBJECT_ID('CK_EventJudgeScoreSheet_Version','C') IS NULL ALTER TABLE dbo.EventJudgeScoreSheet WITH CHECK ADD CONSTRAINT CK_EventJudgeScoreSheet_Version CHECK(RuleVersion>=1 AND SheetVersion>=1); + IF OBJECT_ID('CK_EventScoreCorrection_Applicant','C') IS NULL ALTER TABLE dbo.EventScoreCorrection WITH CHECK ADD CONSTRAINT CK_EventScoreCorrection_Applicant CHECK((ApplyUserId IS NOT NULL AND ApplyRefereeId IS NULL) OR (ApplyUserId IS NULL AND ApplyRefereeId IS NOT NULL)); /* 3.2 外键:二期表引用一期表主键、以及二期表之间的引用关系 */ IF OBJECT_ID(N'dbo.[FK_RefereeApplication_Event]','F') IS NULL ALTER TABLE dbo.[RefereeApplication] WITH CHECK ADD CONSTRAINT [FK_RefereeApplication_Event] FOREIGN KEY([EventId]) REFERENCES dbo.[EventInfo]([Id]); IF OBJECT_ID(N'dbo.[FK_RefereeApplication_AppUser]','F') IS NULL ALTER TABLE dbo.[RefereeApplication] WITH CHECK ADD CONSTRAINT [FK_RefereeApplication_AppUser] FOREIGN KEY([RefereeUserId]) REFERENCES dbo.[AppUser]([Id]); @@ -658,12 +694,14 @@ BEGIN TRY IF OBJECT_ID(N'dbo.[FK_EventJudgeScoreSheet_Task]','F') IS NULL ALTER TABLE dbo.[EventJudgeScoreSheet] WITH CHECK ADD CONSTRAINT [FK_EventJudgeScoreSheet_Task] FOREIGN KEY([JudgeTaskId]) REFERENCES dbo.[EventJudgeTask]([Id]); IF OBJECT_ID(N'dbo.[FK_EventJudgeScoreSheet_Entry]','F') IS NULL ALTER TABLE dbo.[EventJudgeScoreSheet] WITH CHECK ADD CONSTRAINT [FK_EventJudgeScoreSheet_Entry] FOREIGN KEY([SessionEntryId]) REFERENCES dbo.[EventSessionEntry]([Id]); IF OBJECT_ID(N'dbo.[FK_EventJudgeScoreSheet_Rule]','F') IS NULL ALTER TABLE dbo.[EventJudgeScoreSheet] WITH CHECK ADD CONSTRAINT [FK_EventJudgeScoreSheet_Rule] FOREIGN KEY([ScoreRuleId]) REFERENCES dbo.[ScoreRule]([Id]); + IF OBJECT_ID(N'dbo.[FK_EventJudgeScoreAttachment_Sheet]','F') IS NULL ALTER TABLE dbo.[EventJudgeScoreAttachment] WITH CHECK ADD CONSTRAINT [FK_EventJudgeScoreAttachment_Sheet] FOREIGN KEY([ScoreSheetId]) REFERENCES dbo.[EventJudgeScoreSheet]([Id]); IF OBJECT_ID(N'dbo.[FK_EventJudgeScoreDetail_Sheet]','F') IS NULL ALTER TABLE dbo.[EventJudgeScoreDetail] WITH CHECK ADD CONSTRAINT [FK_EventJudgeScoreDetail_Sheet] FOREIGN KEY([ScoreSheetId]) REFERENCES dbo.[EventJudgeScoreSheet]([Id]); IF OBJECT_ID(N'dbo.[FK_EventResultVersion_Result]','F') IS NULL ALTER TABLE dbo.[EventResultVersion] WITH CHECK ADD CONSTRAINT [FK_EventResultVersion_Result] FOREIGN KEY([EventResultId]) REFERENCES dbo.[EventResult]([Id]); IF OBJECT_ID(N'dbo.[FK_EventResultVersion_Entry]','F') IS NULL ALTER TABLE dbo.[EventResultVersion] WITH CHECK ADD CONSTRAINT [FK_EventResultVersion_Entry] FOREIGN KEY([SessionEntryId]) REFERENCES dbo.[EventSessionEntry]([Id]); IF OBJECT_ID(N'dbo.[FK_EventResultVersion_Rule]','F') IS NULL ALTER TABLE dbo.[EventResultVersion] WITH CHECK ADD CONSTRAINT [FK_EventResultVersion_Rule] FOREIGN KEY([ScoreRuleId]) REFERENCES dbo.[ScoreRule]([Id]); IF OBJECT_ID(N'dbo.[FK_EventScoreCorrection_Entry]','F') IS NULL ALTER TABLE dbo.[EventScoreCorrection] WITH CHECK ADD CONSTRAINT [FK_EventScoreCorrection_Entry] FOREIGN KEY([SessionEntryId]) REFERENCES dbo.[EventSessionEntry]([Id]); IF OBJECT_ID(N'dbo.[FK_EventScoreCorrection_OriginalSheet]','F') IS NULL ALTER TABLE dbo.[EventScoreCorrection] WITH CHECK ADD CONSTRAINT [FK_EventScoreCorrection_OriginalSheet] FOREIGN KEY([OriginalScoreSheetId]) REFERENCES dbo.[EventJudgeScoreSheet]([Id]); + IF OBJECT_ID(N'dbo.[FK_EventScoreCorrection_ApplyReferee]','F') IS NULL ALTER TABLE dbo.[EventScoreCorrection] WITH CHECK ADD CONSTRAINT [FK_EventScoreCorrection_ApplyReferee] FOREIGN KEY([ApplyRefereeId]) REFERENCES dbo.[Referee]([Id]); IF OBJECT_ID(N'dbo.[FK_EventResultScreenCast_Event]','F') IS NULL ALTER TABLE dbo.[EventResultScreenCast] WITH CHECK ADD CONSTRAINT [FK_EventResultScreenCast_Event] FOREIGN KEY([EventId]) REFERENCES dbo.[EventInfo]([Id]); /* 3.3 索引:UX_ 唯一索引 / IX_ 非聚集索引;WHERE DeleteMark=0 的过滤索引仅约束有效数据 */ IF NOT EXISTS(SELECT 1 FROM sys.indexes WHERE object_id=OBJECT_ID(N'dbo.[Referee]') AND name=N'UX_Referee_Mobile_Active') CREATE UNIQUE NONCLUSTERED INDEX [UX_Referee_Mobile_Active] ON dbo.[Referee]([Mobile]) WHERE [DeleteMark]=0; @@ -680,10 +718,13 @@ BEGIN TRY IF NOT EXISTS(SELECT 1 FROM sys.indexes WHERE object_id=OBJECT_ID(N'dbo.[EventJudgeAssignment]') AND name=N'UX_EventJudgeAssignment_Session_Role_Active') CREATE UNIQUE NONCLUSTERED INDEX [UX_EventJudgeAssignment_Session_Role_Active] ON dbo.[EventJudgeAssignment]([SessionId],[JudgeRole]) WHERE [DeleteMark]=0 AND [Status]=0; IF NOT EXISTS(SELECT 1 FROM sys.indexes WHERE object_id=OBJECT_ID(N'dbo.[EventJudgeTask]') AND name=N'IX_EventJudgeTask_User_Status') CREATE NONCLUSTERED INDEX [IX_EventJudgeTask_User_Status] ON dbo.[EventJudgeTask]([JudgeUserId],[TaskStatus],[ExpiresTime]) WHERE [DeleteMark]=0; IF NOT EXISTS(SELECT 1 FROM sys.indexes WHERE object_id=OBJECT_ID(N'dbo.[EventJudgeTask]') AND name=N'UX_EventJudgeTask_Business_Version') CREATE UNIQUE NONCLUSTERED INDEX [UX_EventJudgeTask_Business_Version] ON dbo.[EventJudgeTask]([SessionEntryId],[JudgeAssignmentId],[TaskVersion]) WHERE [DeleteMark]=0; + IF NOT EXISTS(SELECT 1 FROM sys.indexes WHERE object_id=OBJECT_ID(N'dbo.[EventScoreCorrection]') AND name=N'UX_EventScoreCorrection_OriginalSheet_Open') CREATE UNIQUE NONCLUSTERED INDEX [UX_EventScoreCorrection_OriginalSheet_Open] ON dbo.[EventScoreCorrection]([OriginalScoreSheetId]) WHERE [DeleteMark]=0 AND [CorrectionStatus]>=0 AND [CorrectionStatus]<=30 AND [CorrectionStatus]<>20; IF NOT EXISTS(SELECT 1 FROM sys.indexes WHERE object_id=OBJECT_ID(N'dbo.[ScoreRule]') AND name=N'UX_ScoreRule_Code_Active') CREATE UNIQUE NONCLUSTERED INDEX [UX_ScoreRule_Code_Active] ON dbo.[ScoreRule]([RuleCode]) WHERE [DeleteMark]=0; IF NOT EXISTS(SELECT 1 FROM sys.indexes WHERE object_id=OBJECT_ID(N'dbo.[ScoreRuleItem]') AND name=N'UX_ScoreRuleItem_Rule_Role_Code_Active') CREATE UNIQUE NONCLUSTERED INDEX [UX_ScoreRuleItem_Rule_Role_Code_Active] ON dbo.[ScoreRuleItem]([ScoreRuleId],[JudgeRoleType],[ItemCode]) WHERE [DeleteMark]=0; IF NOT EXISTS(SELECT 1 FROM sys.indexes WHERE object_id=OBJECT_ID(N'dbo.[EventJudgeScoreSheet]') AND name=N'UX_EventJudgeScoreSheet_Idempotency') CREATE UNIQUE NONCLUSTERED INDEX [UX_EventJudgeScoreSheet_Idempotency] ON dbo.[EventJudgeScoreSheet]([IdempotencyKey]); IF NOT EXISTS(SELECT 1 FROM sys.indexes WHERE object_id=OBJECT_ID(N'dbo.[EventJudgeScoreSheet]') AND name=N'UX_EventJudgeScoreSheet_Current') CREATE UNIQUE NONCLUSTERED INDEX [UX_EventJudgeScoreSheet_Current] ON dbo.[EventJudgeScoreSheet]([SessionEntryId],[JudgeAssignmentId]) WHERE [IsCurrent]=1 AND [DeleteMark]=0; + 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]); 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; @@ -1047,7 +1088,7 @@ BEGIN SET NOCOUNT ON; SELECT * FROM dbo.vw_Phase2_SessionScoreProgress WHERE Ses EXEC(N'CREATE OR ALTER PROCEDURE dbo.usp_Phase2_ValidateDeployment AS BEGIN SET NOCOUNT ON; SELECT N''Table'' AS ObjectType,t.name AS ObjectName FROM sys.tables t WHERE t.name IN -(''RefereeApplication'',''CheckinTask'',''CheckinRecord'',''Referee'',''EventReferee'',''EventJudgeAssignment'',''EventJudgeTask'',''EventVenue'',''EventSession'',''EventSessionEntry'',''EventSessionEntryRematch'',''EventJudgeMessage'',''ScoreRule'',''ScoreRuleItem'',''EventJudgeScoreSheet'',''EventJudgeScoreDetail'',''EventResultVersion'',''EventScoreCorrection'',''EventResultScreenCast'') +(''RefereeApplication'',''CheckinTask'',''CheckinRecord'',''Referee'',''EventReferee'',''EventJudgeAssignment'',''EventJudgeTask'',''EventVenue'',''EventSession'',''EventSessionEntry'',''EventSessionEntryRematch'',''EventJudgeMessage'',''ScoreRule'',''ScoreRuleItem'',''EventJudgeScoreSheet'',''EventJudgeScoreAttachment'',''EventJudgeScoreDetail'',''EventResultVersion'',''EventScoreCorrection'',''EventResultScreenCast'') UNION ALL SELECT N''View'',name FROM sys.views WHERE name LIKE ''vw_Phase2[_]%'' UNION ALL SELECT N''Routine'',name FROM sys.objects WHERE type IN(''P'',''FN'') AND (name LIKE ''usp_Phase2[_]%'' OR name LIKE ''fn_Phase2[_]%''); SELECT r.RuleCode,COUNT(i.Id) AS ItemCount FROM dbo.ScoreRule r LEFT JOIN dbo.ScoreRuleItem i ON i.ScoreRuleId=r.Id AND i.DeleteMark=0 WHERE r.RuleCode IN(''ROPE_PRESCRIBED'',''ROPE_FREESTYLE_WHEEL'',''ROPE_FREESTYLE_WIRE'',''ROPE_SHOWCASE'') AND r.DeleteMark=0 GROUP BY r.RuleCode; END'); @@ -1085,7 +1126,7 @@ SELECT r.RuleCode,COUNT(i.Id) AS ItemCount FROM dbo.ScoreRule r LEFT JOIN dbo.Sc DECLARE business_tables CURSOR LOCAL FAST_FORWARD FOR SELECT [Name] FROM (VALUES (N'RefereeApplication'),(N'CheckinTask'),(N'CheckinRecord'),(N'Referee'),(N'EventReferee'),(N'EventJudgeAssignment'), (N'EventJudgeTask'),(N'EventVenue'),(N'EventSession'),(N'EventSessionEntry'),(N'EventSessionEntryRematch'), - (N'EventJudgeMessage'),(N'ScoreRule'),(N'ScoreRuleItem'),(N'EventJudgeScoreSheet'),(N'EventJudgeScoreDetail'), + (N'EventJudgeMessage'),(N'ScoreRule'),(N'ScoreRuleItem'),(N'EventJudgeScoreSheet'),(N'EventJudgeScoreAttachment'),(N'EventJudgeScoreDetail'), (N'EventResultVersion'),(N'EventScoreCorrection'),(N'EventResultScreenCast'))x([Name]); OPEN business_tables; FETCH NEXT FROM business_tables INTO @BusinessTable; WHILE @@FETCH_STATUS=0 @@ -1104,7 +1145,7 @@ SELECT r.RuleCode,COUNT(i.Id) AS ItemCount FROM dbo.ScoreRule r LEFT JOIN dbo.Sc PRINT N'[7/9] 执行后置校验'; DECLARE @Validation TABLE([Category] NVARCHAR(32),[Item] NVARCHAR(128),[Passed] BIT,[Actual] NVARCHAR(1000)); DECLARE @ExpectedTables TABLE(Name SYSNAME); - INSERT @ExpectedTables VALUES(N'RefereeApplication'),(N'CheckinTask'),(N'CheckinRecord'),(N'Referee'),(N'EventReferee'),(N'EventJudgeAssignment'),(N'EventJudgeTask'),(N'EventVenue'),(N'EventSession'),(N'EventSessionEntry'),(N'EventSessionEntryRematch'),(N'EventJudgeMessage'),(N'ScoreRule'),(N'ScoreRuleItem'),(N'EventJudgeScoreSheet'),(N'EventJudgeScoreDetail'),(N'EventResultVersion'),(N'EventScoreCorrection'),(N'EventResultScreenCast'); + INSERT @ExpectedTables VALUES(N'RefereeApplication'),(N'CheckinTask'),(N'CheckinRecord'),(N'Referee'),(N'EventReferee'),(N'EventJudgeAssignment'),(N'EventJudgeTask'),(N'EventVenue'),(N'EventSession'),(N'EventSessionEntry'),(N'EventSessionEntryRematch'),(N'EventJudgeMessage'),(N'ScoreRule'),(N'ScoreRuleItem'),(N'EventJudgeScoreSheet'),(N'EventJudgeScoreAttachment'),(N'EventJudgeScoreDetail'),(N'EventResultVersion'),(N'EventScoreCorrection'),(N'EventResultScreenCast'); INSERT @Validation SELECT N'TABLE',Name,CASE WHEN OBJECT_ID(N'dbo.'+QUOTENAME(Name),'U') IS NOT NULL THEN 1 ELSE 0 END,N'dbo.'+Name FROM @ExpectedTables; DECLARE @ExpectedColumns TABLE(TableName SYSNAME,ColumnName SYSNAME);