feat: 完善裁判成绩修正与画板附件闭环

This commit is contained in:
ltx 2026-09-08 14:07:59 +08:00
parent c95f85f99f
commit 3277a76730
14 changed files with 730 additions and 122 deletions

View File

@ -75,6 +75,9 @@ public class JudgeBoardSheetScoreOutput
/// 是否由该裁判判定取消比赛资格
/// </summary>
public bool IsDisqualified { get; set; }
/// <summary>裁判记录画板地址。</summary>
public string? AnnotationUrl { get; set; }
}
/// <summary>

View File

@ -62,6 +62,7 @@ public class JudgeScoreService : IDynamicApiController, ITransient
private readonly ISqlSugarRepository<EventJudgeScoreSheet> _sheetRepository;
private readonly ISqlSugarRepository<EventScoreCorrection> _correctionRepository;
private readonly ISqlSugarRepository<EventJudgeMessage> _messageRepository;
private readonly ISqlSugarRepository<EventJudgeScoreAttachment> _attachmentRepository;
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Referee> _refereeRepository;
private readonly ISqlSugarRepository<EventResult> _resultRepository;
private readonly EventScoreCalculationService _calculationService;
@ -75,6 +76,7 @@ public class JudgeScoreService : IDynamicApiController, ITransient
ISqlSugarRepository<EventJudgeScoreSheet> sheetRepository,
ISqlSugarRepository<EventScoreCorrection> correctionRepository,
ISqlSugarRepository<EventJudgeMessage> messageRepository,
ISqlSugarRepository<EventJudgeScoreAttachment> attachmentRepository,
ISqlSugarRepository<YD_Event.Core.Entities.Referee> refereeRepository,
ISqlSugarRepository<EventResult> 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<EntrySymbolScore>())
.SelectMany(x => x.Judges).Select(x => x.ScoreSheetId).Distinct().ToList();
var annotationUrls = sheetIds.Count == 0
? new Dictionary<long, string>()
: (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();
}

View File

@ -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<EventJudgeScoreSheet>()
: 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());

View File

@ -426,6 +426,9 @@ public class JudgeScoreSheetOutput
/// </summary>
public long ScoreSheetId { get; set; }
/// <summary>评分单版本;未产生评分单时为下一待提交版本。</summary>
public int SheetVersion { get; set; }
/// <summary>
/// 评分单状态:0-草稿,10-已提交,20-已确认,30-已被修正,90-已作废
/// </summary>
@ -817,6 +820,9 @@ public class JudgeSymbolScoreDto
/// </summary>
public class JudgeSymbolJudgeDto
{
/// <summary>评分单ID。</summary>
public long ScoreSheetId { get; set; }
/// <summary>
/// 裁判席位编码
/// </summary>
@ -831,6 +837,9 @@ public class JudgeSymbolJudgeDto
/// 该裁判的得分
/// </summary>
public string Value { get; set; } = string.Empty;
/// <summary>该评分单当前记录画板地址。</summary>
public string? AnnotationUrl { get; set; }
}
/// <summary>
@ -838,6 +847,9 @@ public class JudgeSymbolJudgeDto
/// </summary>
public class JudgeCorrectionCandidateDto
{
/// <summary>最近一次修正流程ID。</summary>
public long? CorrectionId { get; set; }
/// <summary>
/// 评分单ID
/// </summary>
@ -878,6 +890,12 @@ public class JudgeCorrectionCandidateDto
/// </summary>
public string CorrectionStatusName { get; set; } = string.Empty;
/// <summary>修正申请原因。</summary>
public string? ApplyReason { get; set; }
/// <summary>审批意见。</summary>
public string? ApproveRemark { get; set; }
/// <summary>
/// 是否可以申请修正
/// </summary>
@ -940,6 +958,15 @@ public class JudgeMessageDto
/// </summary>
public long? RelationId { get; set; }
/// <summary>关联场次ID。</summary>
public long? SessionId { get; set; }
/// <summary>关联出场记录ID。</summary>
public long? SessionEntryId { get; set; }
/// <summary>关联评分单ID。</summary>
public long? ScoreSheetId { get; set; }
/// <summary>
/// 是否已读
/// </summary>
@ -961,3 +988,23 @@ public class ReadJudgeMessageInput
/// </summary>
public List<long> MessageIds { get; set; } = new();
}
/// <summary>记录画板 PNG 上传输入。</summary>
public class UploadJudgeScoreAnnotationInput
{
public long ScoreSheetId { get; set; }
public string IdempotencyKey { get; set; } = null!;
public IFormFile File { get; set; } = null!;
}
/// <summary>记录画板附件。</summary>
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; }
}

View File

@ -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;
/// 裁判提交的分值一律在服务端按计分规则重算,客户端上传的合计值不参与落库。
/// </summary>
[ApiDescriptionSettings("打分系统")]
[Route("api")]
[Authorize]
public class JudgeScoringController : IDynamicApiController
{
@ -33,11 +36,13 @@ public class JudgeScoringController : IDynamicApiController
private readonly ISqlSugarRepository<EventJudgeScoreDetail> _detailRepository;
private readonly ISqlSugarRepository<EventScoreCorrection> _correctionRepository;
private readonly ISqlSugarRepository<EventJudgeMessage> _messageRepository;
private readonly ISqlSugarRepository<EventJudgeScoreAttachment> _attachmentRepository;
private readonly ISqlSugarRepository<EventResult> _resultRepository;
private readonly ISqlSugarRepository<EventRegistrationPerson> _personRepository;
private readonly EventScoreCalculationService _calculationService;
private readonly AuthManager _authManager;
private readonly IIdGenerator _idGenerator;
private readonly FileService _fileService;
public JudgeScoringController(
ISqlSugarRepository<YD_Event.Core.Entities.AppUser> userRepository,
@ -55,11 +60,13 @@ public class JudgeScoringController : IDynamicApiController
ISqlSugarRepository<EventJudgeScoreDetail> detailRepository,
ISqlSugarRepository<EventScoreCorrection> correctionRepository,
ISqlSugarRepository<EventJudgeMessage> messageRepository,
ISqlSugarRepository<EventJudgeScoreAttachment> attachmentRepository,
ISqlSugarRepository<EventResult> resultRepository,
ISqlSugarRepository<EventRegistrationPerson> 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;
}
/// <summary>
@ -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<List<JudgeEntryResultDto>> 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<EventResult>()
: await _resultRepository.GetListAsync(x => entryIds.Contains(x.SessionEntryId));
var scoreSheetIds = outcomes.SelectMany(x => x.Aggregate?.Symbols ?? new List<EntrySymbolScore>())
.SelectMany(x => x.Judges).Select(x => x.ScoreSheetId).Distinct().ToList();
var annotationUrls = scoreSheetIds.Count == 0
? new Dictionary<long, string>()
: (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<JudgeCorrectionCandidateDto>();
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<long, EventScoreCorrection>()
: (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);
}
/// <summary>上传当前裁判评分单的记录画板 PNG。</summary>
[HttpPost("judge-scoring/annotations/upload")]
[DisplayName("上传裁判记录画板")]
[UnitOfWork]
public async Task<JudgeScoreAttachmentDto> 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);
}
/// <summary>查询评分单当前记录画板。</summary>
[HttpGet("judge-scoring/annotations/{scoreSheetId}")]
[DisplayName("查询裁判记录画板")]
public async Task<JudgeScoreAttachmentDto?> 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);
}
/// <summary>
/// 校验当前登录用户的裁判身份并取出裁判档案
/// </summary>
@ -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
};
/// <summary>
/// 取(或按需创建)本裁判在该出场上的评分任务
/// </summary>
@ -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<EntryRoleProgress>()).Select(ToProgressDto).ToList()
};
@ -938,6 +1087,10 @@ public class JudgeScoringController : IDynamicApiController
/// </summary>
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];
/// <summary>
/// 出场状态:退赛
/// </summary>

View File

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

View File

@ -10614,6 +10614,9 @@
是否由该裁判判定取消比赛资格
</summary>
</member>
<member name="P:YD_Event.Application.Scoring.Dtos.JudgeBoardSheetScoreOutput.AnnotationUrl">
<summary>裁判记录画板地址。</summary>
</member>
<member name="T:YD_Event.Application.Scoring.Dtos.JudgeSymbolScoreOutput">
<summary>
后台记分台:单个计分符号的聚合得分
@ -12326,6 +12329,9 @@
评分单ID(尚未保存过时为0)
</summary>
</member>
<member name="P:YD_Event.Application.Scoring.Dtos.JudgeScoreSheetOutput.SheetVersion">
<summary>评分单版本;未产生评分单时为下一待提交版本。</summary>
</member>
<member name="P:YD_Event.Application.Scoring.Dtos.JudgeScoreSheetOutput.SheetStatus">
<summary>
评分单状态:0-草稿,10-已提交,20-已确认,30-已被修正,90-已作废
@ -12706,6 +12712,9 @@
参与聚合的单个裁判得分
</summary>
</member>
<member name="P:YD_Event.Application.Scoring.Dtos.JudgeSymbolJudgeDto.ScoreSheetId">
<summary>评分单ID。</summary>
</member>
<member name="P:YD_Event.Application.Scoring.Dtos.JudgeSymbolJudgeDto.JudgeRole">
<summary>
裁判席位编码
@ -12721,11 +12730,17 @@
该裁判的得分
</summary>
</member>
<member name="P:YD_Event.Application.Scoring.Dtos.JudgeSymbolJudgeDto.AnnotationUrl">
<summary>该评分单当前记录画板地址。</summary>
</member>
<member name="T:YD_Event.Application.Scoring.Dtos.JudgeCorrectionCandidateDto">
<summary>
可申请成绩修正的评分单
</summary>
</member>
<member name="P:YD_Event.Application.Scoring.Dtos.JudgeCorrectionCandidateDto.CorrectionId">
<summary>最近一次修正流程ID。</summary>
</member>
<member name="P:YD_Event.Application.Scoring.Dtos.JudgeCorrectionCandidateDto.ScoreSheetId">
<summary>
评分单ID
@ -12766,6 +12781,12 @@
修正申请状态名称
</summary>
</member>
<member name="P:YD_Event.Application.Scoring.Dtos.JudgeCorrectionCandidateDto.ApplyReason">
<summary>修正申请原因。</summary>
</member>
<member name="P:YD_Event.Application.Scoring.Dtos.JudgeCorrectionCandidateDto.ApproveRemark">
<summary>审批意见。</summary>
</member>
<member name="P:YD_Event.Application.Scoring.Dtos.JudgeCorrectionCandidateDto.CanApply">
<summary>
是否可以申请修正
@ -12826,6 +12847,15 @@
关联业务ID
</summary>
</member>
<member name="P:YD_Event.Application.Scoring.Dtos.JudgeMessageDto.SessionId">
<summary>关联场次ID。</summary>
</member>
<member name="P:YD_Event.Application.Scoring.Dtos.JudgeMessageDto.SessionEntryId">
<summary>关联出场记录ID。</summary>
</member>
<member name="P:YD_Event.Application.Scoring.Dtos.JudgeMessageDto.ScoreSheetId">
<summary>关联评分单ID。</summary>
</member>
<member name="P:YD_Event.Application.Scoring.Dtos.JudgeMessageDto.IsRead">
<summary>
是否已读
@ -12846,6 +12876,12 @@
消息ID,为空表示全部标记已读
</summary>
</member>
<member name="T:YD_Event.Application.Scoring.Dtos.UploadJudgeScoreAnnotationInput">
<summary>记录画板 PNG 上传输入。</summary>
</member>
<member name="T:YD_Event.Application.Scoring.Dtos.JudgeScoreAttachmentDto">
<summary>记录画板附件。</summary>
</member>
<member name="T:YD_Event.Application.Scoring.Dtos.ScoringContext">
<summary>
一个场次的计分上下文:场次、项目、计分规则与评分项。裁判端取评分表、后台算成绩都基于同一份上下文。
@ -13330,6 +13366,12 @@
标记消息已读,不传消息ID表示全部标记已读
</summary>
</member>
<member name="M:YD_Event.Application.Scoring.JudgeScoringController.UploadAnnotation(YD_Event.Application.Scoring.Dtos.UploadJudgeScoreAnnotationInput)">
<summary>上传当前裁判评分单的记录画板 PNG。</summary>
</member>
<member name="M:YD_Event.Application.Scoring.JudgeScoringController.GetAnnotation(System.Int64)">
<summary>查询评分单当前记录画板。</summary>
</member>
<member name="M:YD_Event.Application.Scoring.JudgeScoringController.EnsureRefereeAsync">
<summary>
校验当前登录用户的裁判身份并取出裁判档案
@ -15568,34 +15610,54 @@
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookEntryRow.Seq">
<summary>序号(仅便于人工核对,导入时不参与落库)</summary>
<summary>
序号(仅便于人工核对,导入时不参与落库)
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookEntryRow.VenueName">
<summary>场地名称(必填)</summary>
<summary>
场地名称(必填)
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookEntryRow.ProjectName">
<summary>项目名称(必填,需与赛事已配置项目一致,不存在将自动建档)</summary>
<summary>
项目名称(必填,需与赛事已配置项目一致,不存在将自动建档)
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookEntryRow.GroupName">
<summary>组别名称(可空,空则归入“公开组”)</summary>
<summary>
组别名称(可空,空则归入"公开组")
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookEntryRow.EntryNo">
<summary>出场编号(可空,空则按同场次顺序自动生成)</summary>
<summary>
出场编号(可空,空则按同场次顺序自动生成)
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookEntryRow.ParticipantName">
<summary>姓名/队名(必填)</summary>
<summary>
姓名/队名(必填)
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookEntryRow.UnitName">
<summary>所属单位(可空,仅用于核对展示)</summary>
<summary>
所属单位(可空,仅用于核对展示)
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookEntryRow.Mobile">
<summary>联系电话(可空)</summary>
<summary>
联系电话(可空)
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookEntryRow.IdCard">
<summary>证件号(可空)</summary>
<summary>
证件号(可空)
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookEntryRow.Remark">
<summary>备注(可空)</summary>
<summary>
备注(可空)
</summary>
</member>
<member name="T:YD_Event.Application.OrderBook.Dtos.OrderBookRefereeRow">
<summary>
@ -15603,25 +15665,39 @@
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookRefereeRow.Seq">
<summary>序号(仅便于人工核对)</summary>
<summary>
序号(仅便于人工核对)
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookRefereeRow.Name">
<summary>裁判姓名(必填)</summary>
<summary>
裁判姓名(必填)
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookRefereeRow.Mobile">
<summary>手机号(必填,用于匹配/创建裁判档案及打分账号)</summary>
<summary>
手机号(必填,用于匹配/创建裁判档案及打分账号)
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookRefereeRow.RoleText">
<summary>裁判角色(可空,如:主裁/难度裁判/表现裁判)</summary>
<summary>
裁判角色(可空,如:主裁/难度裁判/表现裁判)
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookRefereeRow.LevelText">
<summary>裁判等级(可空,如:国际级/国家级/一级/二级/三级)</summary>
<summary>
裁判等级(可空,如:国际级/国家级/一级/二级/三级)
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookRefereeRow.VenueName">
<summary>负责场地(可空,对应出场秩序中的场地名称)</summary>
<summary>
负责场地(可空,对应出场秩序中的场地名称)
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookRefereeRow.ProjectName">
<summary>负责项目(可空)</summary>
<summary>
负责项目(可空)
</summary>
</member>
<member name="T:YD_Event.Application.OrderBook.Dtos.OrderBookImportOutput">
<summary>
@ -15629,28 +15705,44 @@
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookImportOutput.BatchId">
<summary>导入批次ID(用于确认导入)</summary>
<summary>
导入批次ID(用于确认导入)
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookImportOutput.Status">
<summary>解析状态:0-解析中,10-待确认,20-存在错误</summary>
<summary>
解析状态:0-解析中,10-待确认,20-存在错误
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookImportOutput.TotalRows">
<summary>原始数据行数(不含表头)</summary>
<summary>
原始数据行数(不含表头)
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookImportOutput.ValidRows">
<summary>有效行数</summary>
<summary>
有效行数
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookImportOutput.ErrorCount">
<summary>错误数量(存在错误时不可确认)</summary>
<summary>
错误数量(存在错误时不可确认)
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookImportOutput.WarningCount">
<summary>警告数量(不阻断确认)</summary>
<summary>
警告数量(不阻断确认)
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookImportOutput.Preview">
<summary>解析预览统计</summary>
<summary>
解析预览统计
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookImportOutput.Errors">
<summary>问题清单(错误/警告)</summary>
<summary>
问题清单(错误/警告)
</summary>
</member>
<member name="T:YD_Event.Application.OrderBook.Dtos.OrderBookPreview">
<summary>
@ -15658,16 +15750,24 @@
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookPreview.VenueCount">
<summary>场地数量</summary>
<summary>
场地数量
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookPreview.SessionCount">
<summary>场次数量(项目×组别×场地)</summary>
<summary>
场次数量(项目×组别×场地)
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookPreview.EntryCount">
<summary>出场记录数量</summary>
<summary>
出场记录数量
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookPreview.RefereeCount">
<summary>裁判数量</summary>
<summary>
裁判数量
</summary>
</member>
<member name="T:YD_Event.Application.OrderBook.Dtos.OrderBookParseIssue">
<summary>
@ -15675,16 +15775,24 @@
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookParseIssue.Row">
<summary>所在行号(Excel 物理行号,含表头)</summary>
<summary>
所在行号(Excel 物理行号,含表头)
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookParseIssue.Column">
<summary>所在列名</summary>
<summary>
所在列名
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookParseIssue.Message">
<summary>问题描述</summary>
<summary>
问题描述
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookParseIssue.Level">
<summary>级别:error-错误(阻断确认),warning-警告(不阻断)</summary>
<summary>
级别:error-错误(阻断确认),warning-警告(不阻断)
</summary>
</member>
<member name="T:YD_Event.Application.OrderBook.Dtos.OrderBookConfirmInput">
<summary>
@ -15692,13 +15800,19 @@
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookConfirmInput.BatchId">
<summary>导入批次ID(必填,由导入接口返回)</summary>
<summary>
导入批次ID(必填,由导入接口返回)
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookConfirmInput.GenerateJudgeAccount">
<summary>是否为裁判生成小程序打分账号</summary>
<summary>
是否为裁判生成小程序打分账号
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookConfirmInput.DefaultPassword">
<summary>打分账号初始密码(生成账号时必填,长度6-32位)</summary>
<summary>
打分账号初始密码(生成账号时必填,长度6-32位)
</summary>
</member>
<member name="T:YD_Event.Application.OrderBook.Dtos.OrderBookConfirmOutput">
<summary>
@ -15706,19 +15820,29 @@
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookConfirmOutput.OrderBookVersion">
<summary>秩序册版本号</summary>
<summary>
秩序册版本号
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookConfirmOutput.VenueCount">
<summary>生成场地数量</summary>
<summary>
生成场地数量
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookConfirmOutput.SessionCount">
<summary>生成场次数量</summary>
<summary>
生成场次数量
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookConfirmOutput.EntryCount">
<summary>生成出场记录数量</summary>
<summary>
生成出场记录数量
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookConfirmOutput.RefereeCount">
<summary>纳入裁判数量</summary>
<summary>
纳入裁判数量
</summary>
</member>
<member name="T:YD_Event.Application.OrderBook.Dtos.OrderBookDraft">
<summary>
@ -15726,28 +15850,44 @@
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookDraft.BatchId">
<summary>批次ID</summary>
<summary>
批次ID
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookDraft.CreatedUserId">
<summary>创建该导入批次的后台用户ID</summary>
<summary>
创建该导入批次的后台用户ID
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookDraft.HasErrors">
<summary>解析结果是否包含阻断确认的错误</summary>
<summary>
解析结果是否包含阻断确认的错误
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookDraft.EventId">
<summary>赛事ID(当前版本要求赛事已预先创建)</summary>
<summary>
赛事ID(当前版本要求赛事已预先创建)
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookDraft.ImportMode">
<summary>导入模式:append-追加(默认),overwrite-覆盖该赛事既有编排</summary>
<summary>
导入模式:append-追加(默认),overwrite-覆盖该赛事既有编排
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookDraft.CreatedAt">
<summary>解析时间</summary>
<summary>
解析时间
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookDraft.Entries">
<summary>出场明细(已通过基础校验)</summary>
<summary>
出场明细(已通过基础校验)
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookDraft.Referees">
<summary>裁判明细</summary>
<summary>
裁判明细
</summary>
</member>
<member name="T:YD_Event.Application.OrderBook.Dtos.OrderBookDraftEntry">
<summary>
@ -15755,28 +15895,44 @@
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookDraftEntry.Row">
<summary>Excel 物理行号</summary>
<summary>
Excel 物理行号
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookDraftEntry.VenueName">
<summary>场地名称</summary>
<summary>
场地名称
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookDraftEntry.ProjectName">
<summary>项目名称</summary>
<summary>
项目名称
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookDraftEntry.GroupName">
<summary>组别名称</summary>
<summary>
组别名称
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookDraftEntry.EntryNo">
<summary>原始出场编号(可空)</summary>
<summary>
原始出场编号(可空)
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookDraftEntry.ParticipantName">
<summary>姓名/队名</summary>
<summary>
姓名/队名
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookDraftEntry.UnitName">
<summary>单位</summary>
<summary>
单位
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookDraftEntry.Mobile">
<summary>联系电话</summary>
<summary>
联系电话
</summary>
</member>
<member name="T:YD_Event.Application.OrderBook.Dtos.OrderBookDraftReferee">
<summary>
@ -15784,25 +15940,39 @@
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookDraftReferee.Row">
<summary>Excel 物理行号</summary>
<summary>
Excel 物理行号
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookDraftReferee.Name">
<summary>裁判姓名</summary>
<summary>
裁判姓名
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookDraftReferee.Mobile">
<summary>手机号</summary>
<summary>
手机号
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookDraftReferee.RoleText">
<summary>裁判角色文本</summary>
<summary>
裁判角色文本
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookDraftReferee.LevelText">
<summary>裁判等级文本</summary>
<summary>
裁判等级文本
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookDraftReferee.VenueName">
<summary>负责场地名称</summary>
<summary>
负责场地名称
</summary>
</member>
<member name="P:YD_Event.Application.OrderBook.Dtos.OrderBookDraftReferee.ProjectName">
<summary>负责项目名称</summary>
<summary>
负责项目名称
</summary>
</member>
<member name="T:YD_Event.Application.OrderBook.OrderBookService">
<summary>
@ -15813,31 +15983,24 @@
<summary>
下载秩序册导入模板
</summary>
<param name="projectMode"></param>
<returns></returns>
</member>
<member name="M:YD_Event.Application.OrderBook.OrderBookService.Import(System.Nullable{System.Int64},System.String,Microsoft.AspNetCore.Http.IFormFile)">
<summary>
上传并同步解析秩序册(不产生业务数据,结果缓存待确认)
</summary>
<param name="eventId"></param>
<param name="importMode"></param>
<param name="file"></param>
<returns></returns>
</member>
<member name="M:YD_Event.Application.OrderBook.OrderBookService.Confirm(YD_Event.Application.OrderBook.Dtos.OrderBookConfirmInput)">
<summary>
确认导入:按解析结果生成场地、场次、出场及裁判名单(幂等,同一批次只生成一次)
</summary>
</member>
<member name="M:YD_Event.Application.OrderBook.OrderBookService.EnsureProjectsAsync(YD_Event.Application.OrderBook.Dtos.OrderBookDraft,System.Int64,System.Int64,System.DateTime)">
<summary>项目(含赛事项目、组别)建档,返回 (项目名,组别名) -> 上下文 的映射</summary>
</member>
<member name="M:YD_Event.Application.OrderBook.OrderBookService.EnsureVenuesAsync(YD_Event.Application.OrderBook.Dtos.OrderBookDraft,System.Int64,System.Int64,System.DateTime)">
<summary>场地建档(追加模式按名称复用),返回场地名 -> 场地上下文 的映射</summary>
</member>
<member name="M:YD_Event.Application.OrderBook.OrderBookService.EnsureSessionsAsync(YD_Event.Application.OrderBook.Dtos.OrderBookDraft,System.Int64,System.Collections.Generic.Dictionary{System.String,YD_Event.Application.OrderBook.OrderBookService.OrderBookProjectContext},System.Collections.Generic.Dictionary{System.String,YD_Event.Application.OrderBook.OrderBookService.OrderBookVenueContext},System.Int64,System.DateTime)">
<summary>按 项目×组别×场地 生成场次,返回组合键 -> 场次 的映射</summary>
</member>
<member name="M:YD_Event.Application.OrderBook.OrderBookService.CreateEntriesAsync(YD_Event.Application.OrderBook.Dtos.OrderBookDraft,System.Collections.Generic.Dictionary{System.String,YD_Event.Application.OrderBook.OrderBookService.OrderBookProjectContext},System.Collections.Generic.Dictionary{System.String,YD_Event.Application.OrderBook.OrderBookService.OrderBookVenueContext},System.Collections.Generic.Dictionary{System.String,YD_Event.Core.Entities.EventSession},System.Int64,System.DateTime)">
<summary>生成出场记录,返回数量</summary>
</member>
<member name="M:YD_Event.Application.OrderBook.OrderBookService.EnsureRefereesAsync(YD_Event.Application.OrderBook.Dtos.OrderBookDraft,System.Int64,YD_Event.Application.OrderBook.Dtos.OrderBookConfirmInput,System.Int64,System.DateTime)">
<summary>裁判档案 / 赛事裁判关系 / 可选打分账号,返回纳入的裁判数量</summary>
<param name="input"></param>
<returns></returns>
</member>
<member name="T:YD_Event.Application.OrderBook.OrderBookTemplateExporter">
<summary>
@ -19237,19 +19400,29 @@
</summary>
</member>
<member name="P:YD_Event.Application.Workbench.Dtos.ResultExportQueryInput.EventId">
<summary>赛事ID(必填)</summary>
<summary>
赛事ID(必填)
</summary>
</member>
<member name="P:YD_Event.Application.Workbench.Dtos.ResultExportQueryInput.EventProjectId">
<summary>赛事项目ID(可选)</summary>
<summary>
赛事项目ID(可选)
</summary>
</member>
<member name="P:YD_Event.Application.Workbench.Dtos.ResultExportQueryInput.GroupName">
<summary>组别名称(可选,模糊匹配)</summary>
<summary>
组别名称(可选,模糊匹配)
</summary>
</member>
<member name="P:YD_Event.Application.Workbench.Dtos.ResultExportQueryInput.ResultStatus">
<summary>成绩状态(可选,默认20-已确认;0-计算中,10-待复核,20-已确认,90-已作废)</summary>
<summary>
成绩状态(可选,默认20-已确认;0-计算中,10-待复核,20-已确认,90-已作废)
</summary>
</member>
<member name="P:YD_Event.Application.Workbench.Dtos.ResultExportQueryInput.IncludeDq">
<summary>是否包含被取消比赛资格(DQ)的记录,默认不包含</summary>
<summary>
是否包含被取消比赛资格(DQ)的记录,默认不包含
</summary>
</member>
<member name="T:YD_Event.Application.Workbench.Dtos.ScoreSheetExportQueryInput">
<summary>
@ -19257,13 +19430,19 @@
</summary>
</member>
<member name="P:YD_Event.Application.Workbench.Dtos.ScoreSheetExportQueryInput.EventId">
<summary>赛事ID(必填)</summary>
<summary>
赛事ID(必填)
</summary>
</member>
<member name="P:YD_Event.Application.Workbench.Dtos.ScoreSheetExportQueryInput.SessionId">
<summary>场次ID(与赛事项目ID至少传一个)</summary>
<summary>
场次ID(与赛事项目ID至少传一个)
</summary>
</member>
<member name="P:YD_Event.Application.Workbench.Dtos.ScoreSheetExportQueryInput.EventProjectId">
<summary>赛事项目ID(与场次ID至少传一个)</summary>
<summary>
赛事项目ID(与场次ID至少传一个)
</summary>
</member>
<member name="T:YD_Event.Application.Workbench.Dtos.AuditExportQueryInput">
<summary>
@ -19271,19 +19450,29 @@
</summary>
</member>
<member name="P:YD_Event.Application.Workbench.Dtos.AuditExportQueryInput.EventId">
<summary>赛事ID(必填,按请求参数中的赛事ID匹配)</summary>
<summary>
赛事ID(必填,按请求参数中的赛事ID匹配)
</summary>
</member>
<member name="P:YD_Event.Application.Workbench.Dtos.AuditExportQueryInput.SessionId">
<summary>场次ID(可选)</summary>
<summary>
场次ID(可选)
</summary>
</member>
<member name="P:YD_Event.Application.Workbench.Dtos.AuditExportQueryInput.StartTime">
<summary>开始时间(可选)</summary>
<summary>
开始时间(可选)
</summary>
</member>
<member name="P:YD_Event.Application.Workbench.Dtos.AuditExportQueryInput.EndTime">
<summary>结束时间(可选)</summary>
<summary>
结束时间(可选)
</summary>
</member>
<member name="P:YD_Event.Application.Workbench.Dtos.AuditExportQueryInput.OperatorId">
<summary>操作人ID(可选)</summary>
<summary>
操作人ID(可选)
</summary>
</member>
<member name="T:YD_Event.Application.Workbench.WorkbenchController">
<summary>

View File

@ -0,0 +1,39 @@
namespace YD_Event.Core.Entities;
/// <summary>
/// 裁判评分单附件。每次上传保留一个版本,IsCurrent 指向评分单当前画板。
/// </summary>
[SugarTable("EventJudgeScoreAttachment")]
public class EventJudgeScoreAttachment : Entity<long>, 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; }
}

View File

@ -50,9 +50,16 @@ public class EventScoreCorrection : Entity<long>, ISoftDelete, ICreatedUserId, I
public long? NewScoreSheetId { get; set; }
/// <summary>
/// 申请人ID
/// 后台申请人ID(SysUser)。裁判端申请时为空。
/// </summary>
public long ApplyUserId { get; set; }
[SugarColumn(IsNullable = true)]
public long? ApplyUserId { get; set; }
/// <summary>
/// 裁判端申请人ID(Referee)。后台发起时为空。
/// </summary>
[SugarColumn(IsNullable = true)]
public long? ApplyRefereeId { get; set; }
/// <summary>
/// 申请修正原因

View File

@ -1873,6 +1873,11 @@
更新时间
</summary>
</member>
<member name="T:YD_Event.Core.Entities.EventJudgeScoreAttachment">
<summary>
裁判评分单附件。每次上传保留一个版本,IsCurrent 指向评分单当前画板。
</summary>
</member>
<member name="T:YD_Event.Core.Entities.EventJudgeScoreDetail">
<summary>
裁判评分明细表
@ -3580,7 +3585,12 @@
</member>
<member name="P:YD_Event.Core.Entities.EventScoreCorrection.ApplyUserId">
<summary>
申请人ID
后台申请人ID(SysUser)。裁判端申请时为空。
</summary>
</member>
<member name="P:YD_Event.Core.Entities.EventScoreCorrection.ApplyRefereeId">
<summary>
裁判端申请人ID(Referee)。后台发起时为空。
</summary>
</member>
<member name="P:YD_Event.Core.Entities.EventScoreCorrection.ApplyReason">
@ -8638,6 +8648,11 @@
拦截器
</summary>
</member>
<member name="M:SqlSugar.SqlSugarExtensions.ResolveUpdatedUserId(System.String,System.Nullable{System.Int64})">
<summary>
有有效登录用户时以当前用户为准;无用户上下文时保留后台任务显式传入的操作者。
</summary>
</member>
<member name="M:SqlSugar.SqlSugarExtensions.InitData(SqlSugar.SqlSugarScope)">
<summary>
初始化基础数据

View File

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

View File

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

View File

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

View File

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