diff --git a/YD_Event.Application/Event/EventArrangementService.cs b/YD_Event.Application/Event/EventArrangementService.cs index b2f5e0c..08d6954 100644 --- a/YD_Event.Application/Event/EventArrangementService.cs +++ b/YD_Event.Application/Event/EventArrangementService.cs @@ -514,7 +514,9 @@ namespace YD_Event.Application.Event; ProjectName = proj.Name, ProjectType = proj.ProjectType.Description(), GroupName = person.GroupName, - TeamName = person.TeamName, + // 个人项目的出场单元是报名人,单位报名时携带的队伍/单位名不能当成团队名。 + // 否则裁判端会优先显示该名称,导致同单位的多名运动员看起来是重复出场。 + TeamName = null, AthleteName = person.Name, UnitName = displayUnitName, ProjectTotalDurationSeconds = projectTotalDurationSeconds, diff --git a/YD_Event.Application/Event/Scoring/EventScoreCalculationService.cs b/YD_Event.Application/Event/Scoring/EventScoreCalculationService.cs index 2b67d6c..a8eb2b2 100644 --- a/YD_Event.Application/Event/Scoring/EventScoreCalculationService.cs +++ b/YD_Event.Application/Event/Scoring/EventScoreCalculationService.cs @@ -210,7 +210,7 @@ public class EventScoreCalculationService : ITransient if (persist) { - var members = await LoadEntryMembersAsync(entries); + var members = await LoadEntryMembersAsync(entries, ctx.Scope); foreach (var entry in written) { var outcome = outcomes.First(x => x.SessionEntryId == entry.Id); @@ -237,7 +237,7 @@ public class EventScoreCalculationService : ITransient var outcome = new EntryCalculationOutcome { SessionEntryId = entry.Id, EntryNo = entry.EntryNo, - ParticipantName = string.IsNullOrWhiteSpace(entry.TeamName) ? entry.ParticipantName : entry.TeamName!, + ParticipantName = DisplayParticipantName(entry, ctx.Scope), GroupName = entry.GroupName }; @@ -311,6 +311,15 @@ public class EventScoreCalculationService : ITransient var userId = operatorUserId ?? 0; var personIds = athletes.Select(x => x.Id).ToList(); + if (!isTeam) + { + // 修复旧逻辑已经造成的串成绩:个人项一个出场号只允许归属当前报名人。 + // 重算时作废同出场号下误写给其他人的成绩,并保留版本留痕。 + var mismatchedResults = await _resultRepository.GetListAsync(x => x.SessionEntryId == entry.Id + && x.ResultStatus != ResultStatusVoided && !personIds.Contains(x.RegistrationPersonId)); + await VoidResultsAsync(mismatchedResults, operatorUserId); + } + // 同一名运动员可能因重赛等原因在同一项目中产生多条出场记录, // 成绩必须以“出场 + 运动员”为业务键更新,不能覆盖其他出场的历史成绩。 var existing = await _resultRepository.GetListAsync(x => x.EventId == ctx.Session.EventId @@ -407,6 +416,14 @@ public class EventScoreCalculationService : ITransient private async Task VoidResultAsync(EventSessionEntry entry, long? operatorUserId) { var results = await _resultRepository.GetListAsync(x => x.SessionEntryId == entry.Id && x.ResultStatus != ResultStatusVoided); + await VoidResultsAsync(results, operatorUserId); + } + + /// + /// 作废指定成绩及其当前版本,历史版本保留留痕。 + /// + private async Task VoidResultsAsync(List results, long? operatorUserId) + { if (results.Count == 0) return; var now = DateTime.Now; foreach (var result in results) @@ -493,7 +510,8 @@ public class EventScoreCalculationService : ITransient /// /// 取各出场对应的运动员名单。集体项目取同一报名项下的全部运动员,个人项目只有一人。 /// - private async Task>> LoadEntryMembersAsync(List entries) + private async Task>> LoadEntryMembersAsync( + List entries, string scope) { var result = new Dictionary>(); var itemIds = entries.Where(x => x.RegistrationItemId.HasValue).Select(x => x.RegistrationItemId!.Value).Distinct().ToList(); @@ -505,7 +523,10 @@ public class EventScoreCalculationService : ITransient } // 少数出场只关联了参赛记录(如现场补录),按参赛记录反查报名人。 - var recordIds = entries.Where(x => !x.RegistrationItemId.HasValue && x.ParticipationRecordId.HasValue) + // 个人项即使有 RegistrationItemId,也必须优先按 ParticipationRecordId 定位到唯一报名人。 + // 单位报名会让多名个人项运动员共享一个 RegistrationItemId。 + var recordIds = entries.Where(x => x.ParticipationRecordId.HasValue + && (scope == ScoreScope.Personal || !x.RegistrationItemId.HasValue)) .Select(x => x.ParticipationRecordId!.Value).Distinct().ToList(); var personByRecord = new Dictionary(); if (recordIds.Count > 0) @@ -523,7 +544,23 @@ public class EventScoreCalculationService : ITransient foreach (var entry in entries) { var members = new List(); - if (entry.RegistrationItemId.HasValue && personsByItem.TryGetValue(entry.RegistrationItemId.Value, out var candidates)) + if (scope == ScoreScope.Personal) + { + if (entry.ParticipationRecordId.HasValue + && personByRecord.TryGetValue(entry.ParticipationRecordId.Value, out var personal)) + { + members.Add(personal); + } + else if (entry.RegistrationItemId.HasValue + && personsByItem.TryGetValue(entry.RegistrationItemId.Value, out var personalCandidates)) + { + // 只为无有效参赛记录的历史数据做姓名精确回退,绝不将整个报名项写成一个人的成绩。 + var matched = personalCandidates.FirstOrDefault(x => + string.Equals(x.Name, entry.ParticipantName, StringComparison.Ordinal)); + if (matched != null) members.Add(matched); + } + } + else if (entry.RegistrationItemId.HasValue && personsByItem.TryGetValue(entry.RegistrationItemId.Value, out var candidates)) { // 报名人里可能同时含教练、领队,成绩只算运动员;没有标记运动员的历史数据全部计入。 members = candidates.Where(x => x.PersonType == PersonType.Athlete).OrderBy(x => x.Name).ToList(); @@ -537,4 +574,12 @@ public class EventScoreCalculationService : ITransient } return result; } + + /// + /// 个人项始终显示出场人快照,只有集体项才使用队名。 + /// + private static string DisplayParticipantName(EventSessionEntry entry, string scope) => + scope == ScoreScope.Team && !string.IsNullOrWhiteSpace(entry.TeamName) + ? entry.TeamName + : entry.ParticipantName; } diff --git a/YD_Event.Application/Judge/JudgeScoringController.cs b/YD_Event.Application/Judge/JudgeScoringController.cs index f5d6e9f..6006acf 100644 --- a/YD_Event.Application/Judge/JudgeScoringController.cs +++ b/YD_Event.Application/Judge/JudgeScoringController.cs @@ -41,6 +41,7 @@ public class JudgeScoringController : IDynamicApiController private readonly ISqlSugarRepository _attachmentRepository; private readonly ISqlSugarRepository _resultRepository; private readonly ISqlSugarRepository _personRepository; + private readonly ISqlSugarRepository _participationRepository; private readonly EventScoreCalculationService _calculationService; private readonly AuthManager _authManager; private readonly IIdGenerator _idGenerator; @@ -65,6 +66,7 @@ public class JudgeScoringController : IDynamicApiController ISqlSugarRepository attachmentRepository, ISqlSugarRepository resultRepository, ISqlSugarRepository personRepository, + ISqlSugarRepository participationRepository, EventScoreCalculationService calculationService, AuthManager authManager, IIdGenerator idGenerator, @@ -88,6 +90,7 @@ public class JudgeScoringController : IDynamicApiController _attachmentRepository = attachmentRepository; _resultRepository = resultRepository; _personRepository = personRepository; + _participationRepository = participationRepository; _calculationService = calculationService; _authManager = authManager; _idGenerator = idGenerator; @@ -268,8 +271,8 @@ public class JudgeScoringController : IDynamicApiController var assignment = await GetMyAssignmentAsync(sessionId, referee.Id); var session = await _sessionRepository.GetFirstAsync(x => x.Id == sessionId); if (session == null) throw Oops.Bah("比赛场次不存在"); - var config = ScoreRuleConfigService.Parse( - (await _ruleRepository.GetFirstAsync(x => x.Id == session.ScoreRuleId))?.RuleConfigJson); + var ctx = await _calculationService.LoadContextAsync(sessionId); + var config = ctx.Config; var entries = (await _entryRepository.AsQueryable() .Where(x => x.SessionId == sessionId && !x.IsVoided && @@ -298,7 +301,7 @@ public class JudgeScoringController : IDynamicApiController EntryNo = entry.EntryNo, PlayOrder = entry.PlayOrder, ScheduleOrder = entry.SourceType == 1 ? entry.Id : 0, - ParticipantName = string.IsNullOrWhiteSpace(entry.TeamName) ? entry.ParticipantName : entry.TeamName!, + ParticipantName = DisplayParticipantName(entry, ctx.Scope), GroupName = entry.GroupName, EntryStatus = entry.EntryStatus, IsRematch = entry.IsRematch, @@ -348,7 +351,7 @@ public class JudgeScoringController : IDynamicApiController var stageAllowsScoring = CompetitionStagePolicy.CanScore(ctx.Session.SessionStatus, eventInfo.EventStatus); var scoreSheetAllowsEditing = submitted == null || correction != null; var canEdit = stageAllowsScoring && scoreSheetAllowsEditing; - var members = await LoadMemberNamesAsync(entry); + var members = await LoadMemberNamesAsync(entry, ctx.Scope); var venue = await _venueRepository.GetFirstAsync(x => x.Id == ctx.Session.VenueId); var nextVersion = (submitted?.SheetVersion ?? 0) + 1; @@ -379,7 +382,7 @@ public class JudgeScoringController : IDynamicApiController entry.GroupName, venue?.VenueName ?? "-"), EntryNo = entry.EntryNo, - ParticipantName = string.IsNullOrWhiteSpace(entry.TeamName) ? entry.ParticipantName : entry.TeamName!, + ParticipantName = DisplayParticipantName(entry, ctx.Scope), MemberNames = members, JudgeRole = assignment.JudgeRole, JudgeRoleType = roleType, @@ -661,8 +664,7 @@ public class JudgeScoringController : IDynamicApiController ScoreSheetId = sheet.Id, SessionEntryId = sheet.SessionEntryId, EntryNo = entry?.EntryNo ?? string.Empty, - ParticipantName = entry == null ? string.Empty - : string.IsNullOrWhiteSpace(entry.TeamName) ? entry.ParticipantName : entry.TeamName!, + ParticipantName = entry == null ? string.Empty : DisplayParticipantName(entry, ctx.Scope), MyScore = ctx.Config.Format(sheet.TotalScore), SubmittedTime = sheet.SubmittedTime, CorrectionStatus = status, @@ -948,8 +950,28 @@ public class JudgeScoringController : IDynamicApiController /// /// 取出场对应的运动员姓名,用于评分表表头展示 /// - private async Task> LoadMemberNamesAsync(EventSessionEntry entry) + private async Task> LoadMemberNamesAsync(EventSessionEntry entry, string scope) { + // 单位可以在同一报名项下填报多名个人项运动员。 + // 个人项必须通过出场记录关联的参赛记录精确取一人,不能按 RegistrationItemId 整体取人。 + if (scope == ScoreScope.Personal) + { + if (entry.ParticipationRecordId.HasValue) + { + var record = await _participationRepository.GetFirstAsync(x => x.Id == entry.ParticipationRecordId.Value); + if (record != null) + { + var person = await _personRepository.GetFirstAsync(x => x.Id == record.RegistrationPersonId); + if (person != null) return new List { person.Name }; + } + } + + // 历史异常关联不扩大到同报名项的其他人,使用出场快照保证不串人。 + return string.IsNullOrWhiteSpace(entry.ParticipantName) + ? new List() + : new List { entry.ParticipantName }; + } + if (!entry.RegistrationItemId.HasValue) return new List(); var persons = await _personRepository.GetListAsync(x => x.RegistrationItemId == entry.RegistrationItemId.Value); var athletes = persons.Where(x => x.PersonType == PersonType.Athlete).ToList(); @@ -957,6 +979,15 @@ public class JudgeScoringController : IDynamicApiController return athletes.OrderBy(x => x.Name).Select(x => x.Name).ToList(); } + /// + /// 个人项始终显示出场人快照;只有集体项才优先显示队名。 + /// 兼容修复前已经编排、TeamName 被误写为单位名的历史出场记录。 + /// + private static string DisplayParticipantName(EventSessionEntry entry, string scope) => + scope == ScoreScope.Team && !string.IsNullOrWhiteSpace(entry.TeamName) + ? entry.TeamName + : entry.ParticipantName; + /// /// 该评分单是否有正在处理中的修正申请 /// @@ -1025,7 +1056,7 @@ public class JudgeScoringController : IDynamicApiController sessionName = ctx.Session.SessionName ?? ctx.Session.SessionNo, entryNo = entry.EntryNo, groupName = entry.GroupName, - participantName = string.IsNullOrWhiteSpace(entry.TeamName) ? entry.ParticipantName : entry.TeamName, + participantName = DisplayParticipantName(entry, ctx.Scope), judgeRole = assignment.JudgeRole, judgeRoleType = roleType, scope = ctx.Scope