diff --git a/YD_Event.Application/Event/Dtos/ArrangementDtos.cs b/YD_Event.Application/Event/Dtos/ArrangementDtos.cs
index c65d3b5..a907baa 100644
--- a/YD_Event.Application/Event/Dtos/ArrangementDtos.cs
+++ b/YD_Event.Application/Event/Dtos/ArrangementDtos.cs
@@ -187,7 +187,7 @@ public class ArrangementPreviewOutput
public string UnitName { get; set; }
///
- /// 场次索引
+ /// 场次索引(同一赛事内按项目顺序全局连续)
///
public int SessionIndex { get; set; }
diff --git a/YD_Event.Application/Event/EventArrangementService.cs b/YD_Event.Application/Event/EventArrangementService.cs
index 9bcacc5..2179b6f 100644
--- a/YD_Event.Application/Event/EventArrangementService.cs
+++ b/YD_Event.Application/Event/EventArrangementService.cs
@@ -118,7 +118,7 @@ namespace YD_Event.Application.Event;
var projects = await _projectRepository.GetListAsync(x => projectIds.Contains(x.Id));
// 按照 SortIndex 对 projects 排序
- var sortedProjects = projects.OrderBy(x => x.SortIndex).ToList();
+ var sortedProjects = projects.OrderBy(x => x.SortIndex).ThenBy(x => x.Id).ToList();
var groups = await _groupRepository.GetListAsync(x => projectIds.Contains(x.ProjectId));
@@ -238,14 +238,24 @@ namespace YD_Event.Application.Event;
{
// 团队项目:(队伍数 ÷ 场地数) × 项目时长
int teamCount = totalTeams > 0 ? totalTeams : 1;
- int rounds = (int)Math.Ceiling((double)teamCount / venueCount);
+ var groupLoads = projGroups
+ .Select(group => teamCounts.FirstOrDefault(x =>
+ x.ProjectId == eventProj.Id && x.GroupName == group.GroupName)?.Count ?? 0);
+ int rounds = IsFreestyleProject(proj.ProjectType)
+ ? CalculateBalancedVenueLoad(groupLoads, venueCount)
+ : (int)Math.Ceiling((double)teamCount / venueCount);
totalDurationSeconds = rounds * proj.GetTotalDurationInSeconds();
}
else
{
// 个人项目:(报名人数 ÷ 场地数) × 项目时长
int participantCount = totalParticipants > 0 ? totalParticipants : 1;
- int rounds = (int)Math.Ceiling((double)participantCount / venueCount);
+ var groupLoads = projGroups
+ .Select(group => counts.FirstOrDefault(x =>
+ x.ProjectId == eventProj.Id && x.GroupName == group.GroupName)?.Count ?? 0);
+ int rounds = IsFreestyleProject(proj.ProjectType)
+ ? CalculateBalancedVenueLoad(groupLoads, venueCount)
+ : (int)Math.Ceiling((double)participantCount / venueCount);
totalDurationSeconds = rounds * proj.GetTotalDurationInSeconds();
}
@@ -297,7 +307,7 @@ namespace YD_Event.Application.Event;
var projects = await _projectRepository.GetListAsync(x => projectIds.Contains(x.Id));
// 按照 SortIndex 对 projects 排序
- var sortedProjects = projects.OrderBy(x => x.SortIndex).ToList();
+ var sortedProjects = projects.OrderBy(x => x.SortIndex).ThenBy(x => x.Id).ToList();
var groups = await _groupRepository.GetListAsync(x => projectIds.Contains(x.ProjectId));
// 统计各组别报名人数
@@ -333,6 +343,7 @@ namespace YD_Event.Application.Event;
var result = new List();
// 2. 生成排期 - 按照排序后的项目顺序处理
+ var nextSessionIndex = 1;
foreach (var proj in sortedProjects)
{
var eventProj = eventProjects.FirstOrDefault(x => x.ProjectId == proj.Id);
@@ -419,20 +430,29 @@ namespace YD_Event.Application.Event;
if (proj.IsTeamProject())
{
int teamCount = totalTeams > 0 ? totalTeams : 1;
- int rounds = (int)Math.Ceiling((double)teamCount / venueCount);
+ var groupLoads = projGroups
+ .Select(group => teamCounts.FirstOrDefault(x =>
+ x.ProjectId == eventProj.Id && x.GroupName == group.GroupName)?.Count ?? 0);
+ int rounds = IsFreestyleProject(proj.ProjectType)
+ ? CalculateBalancedVenueLoad(groupLoads, venueCount)
+ : (int)Math.Ceiling((double)teamCount / venueCount);
projectTotalDurationSeconds = rounds * proj.GetTotalDurationInSeconds();
}
else
{
int participantCount = totalParticipants > 0 ? totalParticipants : 1;
- int rounds = (int)Math.Ceiling((double)participantCount / venueCount);
+ var groupLoads = projGroups
+ .Select(group => counts.FirstOrDefault(x =>
+ x.ProjectId == eventProj.Id && x.GroupName == group.GroupName)?.Count ?? 0);
+ int rounds = IsFreestyleProject(proj.ProjectType)
+ ? CalculateBalancedVenueLoad(groupLoads, venueCount)
+ : (int)Math.Ceiling((double)participantCount / venueCount);
projectTotalDurationSeconds = rounds * proj.GetTotalDurationInSeconds();
}
string projectTotalDuration = FormatDurationSymbol(projectTotalDurationSeconds);
- int currentRound = 1;
- int currentVenue = 1;
+ var projectEntries = new List();
foreach (var group in projGroups)
{
@@ -454,7 +474,7 @@ namespace YD_Event.Application.Event;
var firstPerson = teamGroup.First();
string displayUnitName = GetDisplayUnitName(firstPerson);
- result.Add(new ArrangementPreviewOutput
+ projectEntries.Add(new ArrangementPreviewOutput
{
EventProjectId = eventProj.Id,
ProjectId = proj.Id,
@@ -470,18 +490,9 @@ namespace YD_Event.Application.Event;
TeamName = teamGroup.Key.TeamName,
AthleteName = combinedAthleteNames,
UnitName = displayUnitName,
- SessionIndex = currentRound,
- VenueIndex = currentVenue,
ProjectTotalDurationSeconds = projectTotalDurationSeconds,
ProjectTotalDuration = projectTotalDuration
});
-
- currentVenue++;
- if (currentVenue > venueCount)
- {
- currentVenue = 1;
- currentRound++;
- }
}
}
else
@@ -490,7 +501,7 @@ namespace YD_Event.Application.Event;
{
string displayUnitName = GetDisplayUnitName(person);
- result.Add(new ArrangementPreviewOutput
+ projectEntries.Add(new ArrangementPreviewOutput
{
EventProjectId = eventProj.Id,
ProjectId = proj.Id,
@@ -504,26 +515,173 @@ namespace YD_Event.Application.Event;
TeamName = person.TeamName,
AthleteName = person.Name,
UnitName = displayUnitName,
- SessionIndex = currentRound,
- VenueIndex = currentVenue,
ProjectTotalDurationSeconds = projectTotalDurationSeconds,
ProjectTotalDuration = projectTotalDuration
});
-
- currentVenue++;
- if (currentVenue > venueCount)
- {
- currentVenue = 1;
- currentRound++;
- }
}
}
}
+
+ if (!projectEntries.Any()) continue;
+
+ if (IsFreestyleProject(proj.ProjectType))
+ {
+ AssignFreestyleVenues(projectEntries, projGroups, venueCount, nextSessionIndex);
+ }
+ else
+ {
+ AssignSpeedSessions(projectEntries, venueCount, nextSessionIndex, eventId, eventProj.Id);
+ }
+
+ result.AddRange(projectEntries
+ .OrderBy(x => x.SessionIndex)
+ .ThenBy(x => x.VenueIndex));
+ nextSessionIndex = projectEntries.Max(x => x.SessionIndex) + 1;
}
return result;
}
+ ///
+ /// 速度项目按组别交叉单位,再依次填入同一场次的各场地。
+ /// 随机种子由赛事和赛事项目确定,避免预览、导出与确认结果反复变化。
+ ///
+ private static void AssignSpeedSessions(
+ List entries,
+ int venueCount,
+ int firstSessionIndex,
+ long eventId,
+ long eventProjectId)
+ {
+ var random = new Random(CreateStableRandomSeed(eventId, eventProjectId));
+ var orderedEntries = new List(entries.Count);
+ string? lastUnitKey = null;
+
+ foreach (var groupEntries in entries.GroupBy(x => x.GroupName))
+ {
+ var unitQueues = groupEntries
+ .GroupBy(GetArrangementUnitKey)
+ .ToDictionary(
+ x => x.Key,
+ x => new Queue(x.OrderBy(_ => random.Next())));
+
+ while (unitQueues.Count > 0)
+ {
+ var candidates = unitQueues
+ .Where(x => x.Key != lastUnitKey)
+ .ToList();
+ if (candidates.Count == 0)
+ {
+ candidates = unitQueues.ToList();
+ }
+
+ var maxRemaining = candidates.Max(x => x.Value.Count);
+ var preferredCandidates = candidates
+ .Where(x => x.Value.Count == maxRemaining)
+ .ToList();
+ var selected = preferredCandidates[random.Next(preferredCandidates.Count)];
+
+ orderedEntries.Add(selected.Value.Dequeue());
+ lastUnitKey = selected.Key;
+ if (selected.Value.Count == 0)
+ {
+ unitQueues.Remove(selected.Key);
+ }
+ }
+ }
+
+ for (var index = 0; index < orderedEntries.Count; index++)
+ {
+ orderedEntries[index].SessionIndex = firstSessionIndex + index / venueCount;
+ orderedEntries[index].VenueIndex = index % venueCount + 1;
+ }
+ }
+
+ ///
+ /// 花样项目以整个组别为不可拆分单元分配场地,优先将大组分配给当前负载最小的场地。
+ /// 同一场地内的组别串行,且每个组别的所有出场记录连续。
+ ///
+ private static void AssignFreestyleVenues(
+ List entries,
+ IReadOnlyCollection projectGroups,
+ int venueCount,
+ int firstSessionIndex)
+ {
+ var groupOrder = projectGroups
+ .Select((group, index) => new { group.GroupName, Index = index })
+ .ToDictionary(x => x.GroupName, x => x.Index);
+ var groups = entries
+ .GroupBy(x => x.GroupName)
+ .Select(x => new
+ {
+ GroupName = x.Key,
+ Entries = x.ToList(),
+ Order = groupOrder.GetValueOrDefault(x.Key, int.MaxValue)
+ })
+ .OrderByDescending(x => x.Entries.Count)
+ .ThenBy(x => x.Order)
+ .ToList();
+
+ var venueLoads = new int[venueCount];
+ var venueGroups = Enumerable.Range(0, venueCount)
+ .ToDictionary(x => x, _ => new List<(int Order, List Entries)>());
+
+ foreach (var group in groups)
+ {
+ var venueIndex = Enumerable.Range(0, venueCount)
+ .OrderBy(x => venueLoads[x])
+ .ThenBy(x => x)
+ .First();
+ venueGroups[venueIndex].Add((group.Order, group.Entries));
+ venueLoads[venueIndex] += group.Entries.Count;
+ }
+
+ foreach (var venue in venueGroups)
+ {
+ var sessionIndex = firstSessionIndex;
+ foreach (var group in venue.Value.OrderBy(x => x.Order))
+ {
+ foreach (var entry in group.Entries)
+ {
+ entry.VenueIndex = venue.Key + 1;
+ entry.SessionIndex = sessionIndex++;
+ }
+ }
+ }
+ }
+
+ private static bool IsFreestyleProject(ProjectTypeEnum projectType)
+ {
+ return projectType is ProjectTypeEnum.PersonalFreestyle or ProjectTypeEnum.TeamFreestyle;
+ }
+
+ private static string GetArrangementUnitKey(ArrangementPreviewOutput entry)
+ {
+ return !string.IsNullOrWhiteSpace(entry.UnitName)
+ ? entry.UnitName.Trim()
+ : $"__ENTRY_{entry.RegistrationItemId}";
+ }
+
+ private static int CalculateBalancedVenueLoad(IEnumerable groupLoads, int venueCount)
+ {
+ var venueLoads = new int[venueCount];
+ foreach (var groupLoad in groupLoads.Where(x => x > 0).OrderByDescending(x => x))
+ {
+ var venueIndex = Enumerable.Range(0, venueCount)
+ .OrderBy(x => venueLoads[x])
+ .ThenBy(x => x)
+ .First();
+ venueLoads[venueIndex] += groupLoad;
+ }
+
+ return venueLoads.Max();
+ }
+
+ private static int CreateStableRandomSeed(long eventId, long eventProjectId)
+ {
+ return unchecked((int)(eventId ^ (eventId >> 32) ^ eventProjectId ^ (eventProjectId >> 32)));
+ }
+
/// 确认编排并固化场地、场次及出场记录。
[HttpPost("confirm/{eventId}"), DisplayName("确认赛事编排"), UnitOfWork]
public async Task Confirm(long eventId)
@@ -557,13 +715,35 @@ namespace YD_Event.Application.Event;
var sessions = new List(); var entries = new List();
var participationRecords = await _participationRepository.AsQueryable().Where(x => x.EventId == eventId).ToListAsync();
var projectMap = projects.ToDictionary(x => x.Id);
- foreach (var group in preview.GroupBy(x => new { x.EventProjectId, x.VenueIndex }))
+ var sessionGroups = preview
+ .GroupBy(x => new
+ {
+ x.EventProjectId,
+ x.VenueIndex,
+ EventProjectGroupId = IsFreestyleProject(projectMap[x.ProjectId].ProjectType)
+ ? x.EventProjectGroupId
+ : null,
+ GroupName = IsFreestyleProject(projectMap[x.ProjectId].ProjectType)
+ ? x.GroupName
+ : null
+ })
+ .OrderBy(x => projectMap[x.First().ProjectId].SortIndex)
+ .ThenBy(x => x.Min(item => item.SessionIndex))
+ .ThenBy(x => x.Key.VenueIndex);
+ foreach (var group in sessionGroups)
{
var first = group.First(); var project = projectMap[first.ProjectId]; var venue = venues.First(x => x.Sort == group.Key.VenueIndex);
+ var isFreestyle = IsFreestyleProject(project.ProjectType);
var session = new EventSession { Id = _idGenerator.NextId(), EventId = eventId, EventProjectId = group.Key.EventProjectId,
- EventProjectGroupId = group.Select(x => x.EventProjectGroupId).Distinct().Count() == 1 ? first.EventProjectGroupId : null,
- VenueId = venue.Id, ScoreRuleId = project.ScoreRuleId!.Value, SessionNo = $"EP{group.Key.EventProjectId}-{group.Key.VenueIndex:00}",
- SessionName = $"{project.Name}-{venue.VenueName}", SessionStatus = 0, Version = 1, Status = AvailabilityStatus.Enable,
+ EventProjectGroupId = group.Key.EventProjectGroupId,
+ VenueId = venue.Id, ScoreRuleId = project.ScoreRuleId!.Value,
+ SessionNo = isFreestyle
+ ? $"EP{group.Key.EventProjectId}-{group.Key.VenueIndex:00}-{group.Min(x => x.SessionIndex):000}"
+ : $"EP{group.Key.EventProjectId}-{group.Key.VenueIndex:00}",
+ SessionName = isFreestyle
+ ? $"{project.Name}-{first.GroupName}-{venue.VenueName}"
+ : $"{project.Name}-{venue.VenueName}",
+ SessionStatus = 0, Version = 1, Status = AvailabilityStatus.Enable,
CreatedUserId = userId, CreatedTime = now };
sessions.Add(session);
var order = 1;
diff --git a/YD_Event.Application/Workbench/WorkbenchController.cs b/YD_Event.Application/Workbench/WorkbenchController.cs
index 420a08c..268b53b 100644
--- a/YD_Event.Application/Workbench/WorkbenchController.cs
+++ b/YD_Event.Application/Workbench/WorkbenchController.cs
@@ -1,5 +1,6 @@
using YD_Event.Application.Auth;
using YD_Event.Application.Scoring;
+using YD_Event.Application.Scoring.Dtos;
using YD_Event.Application.Workbench.Dtos;
using YD_Event.Core.Entities;
using Newtonsoft.Json.Linq;
@@ -683,7 +684,7 @@ public class WorkbenchController : IDynamicApiController
?? throw Oops.Bah("所选组别不属于当前赛事项目");
var groupName = eventProjectGroup.GroupName;
- var results = await _resultRepository.AsQueryable()
+ var rankingQuery = _resultRepository.AsQueryable()
.InnerJoin((r, e) => r.SessionEntryId == e.Id)
.InnerJoin((r, e, s) => e.SessionId == s.Id)
.Where((r, e, s) => !r.DeleteMark && !e.DeleteMark && !s.DeleteMark &&
@@ -693,19 +694,34 @@ public class WorkbenchController : IDynamicApiController
e.GroupName == groupName) &&
// 只投屏有效成绩(已确认20/已投屏30/已更正40),排除DQ成绩
(r.ResultStatus == 20 || r.ResultStatus == 30 || r.ResultStatus == 40) &&
- !r.IsDisqualified)
+ !r.IsDisqualified);
+
+ // 排名按大屏对应场地隔离,避免同项目同组别在多个场地比赛时串榜。
+ // 使用显式分支和值类型局部变量,避免 nullable 条件被 SqlSugar 翻译成非法 SQL。
+ if (input.VenueId.HasValue)
+ {
+ var venueId = input.VenueId.Value;
+ rankingQuery = rankingQuery.Where((r, e, s) => s.VenueId == venueId);
+ }
+
+ var results = await rankingQuery
.OrderBy((r, e, s) => r.Rank)
.Select((r, e, s) => new
{
r.Id,
r.ResultVersion,
+ SessionEntryId = e.Id,
+ s.ScoreRuleId,
e.EntryNo,
e.ParticipantName,
e.TeamName,
e.GroupName,
+ r.DifficultyScore,
+ r.PerformanceScore,
+ r.RequiredScore,
+ r.DeductionScore,
r.TotalScore,
r.Score,
- r.Rank,
r.IsDisqualified
})
.ToListAsync();
@@ -713,7 +729,38 @@ public class WorkbenchController : IDynamicApiController
if (!results.Any())
throw Oops.Bah("当前项目组别没有可投屏的已确认成绩");
- var payload = results.Select(r => new
+ // 落库名次可能是成绩刚生成时连同“待复核”成绩一起计算的。
+ // 投屏时必须以上面已经过状态筛选的有效成绩重新排名,避免待复核成绩隐形占位。
+ var scoreRuleId = results.Select(r => r.ScoreRuleId).First();
+ var scoreRule = await _scoreRuleRepository.GetByIdAsync(scoreRuleId);
+ var rankCandidates = results
+ .GroupBy(r => r.SessionEntryId)
+ .Select(group =>
+ {
+ var row = group.First();
+ return new RankCandidate
+ {
+ SessionEntryId = group.Key,
+ TotalScore = row.TotalScore ?? 0m,
+ DifficultyScore = row.DifficultyScore,
+ PerformanceScore = row.PerformanceScore,
+ RequiredScore = row.RequiredScore,
+ DeductionScore = row.DeductionScore,
+ IsDisqualified = row.IsDisqualified,
+ IsRanked = row.TotalScore.HasValue
+ };
+ })
+ .ToList();
+ EntryResultCalculationService.AssignRanks(rankCandidates,
+ ScoreRuleConfigService.Parse(scoreRule?.RuleConfigJson));
+ var screenRankByEntry = rankCandidates.ToDictionary(x => x.SessionEntryId, x => x.Rank);
+
+ var payload = results
+ .OrderBy(r => screenRankByEntry[r.SessionEntryId] == 0
+ ? int.MaxValue
+ : screenRankByEntry[r.SessionEntryId])
+ .ThenBy(r => r.EntryNo)
+ .Select(r => new
{
r.EntryNo,
r.ParticipantName,
@@ -721,7 +768,7 @@ public class WorkbenchController : IDynamicApiController
r.GroupName,
r.TotalScore,
r.Score,
- r.Rank,
+ Rank = screenRankByEntry[r.SessionEntryId],
r.IsDisqualified
}).ToList();
diff --git a/YD_Event.Application/YD_Event.Application.xml b/YD_Event.Application/YD_Event.Application.xml
index ff1732b..b0bb70c 100644
--- a/YD_Event.Application/YD_Event.Application.xml
+++ b/YD_Event.Application/YD_Event.Application.xml
@@ -4876,7 +4876,7 @@
- 场次索引
+ 场次索引(同一赛事内按项目顺序全局连续)
@@ -9463,6 +9463,18 @@
编排预览
+
+
+ 速度项目按组别交叉单位,再依次填入同一场次的各场地。
+ 随机种子由赛事和赛事项目确定,避免预览、导出与确认结果反复变化。
+
+
+
+
+ 花样项目以整个组别为不可拆分单元分配场地,优先将大组分配给当前负载最小的场地。
+ 同一场地内的组别串行,且每个组别的所有出场记录连续。
+
+
确认编排并固化场地、场次及出场记录。
diff --git a/YD_Event.Application/applicationsettings.json b/YD_Event.Application/applicationsettings.json
index b143542..f9a477c 100644
--- a/YD_Event.Application/applicationsettings.json
+++ b/YD_Event.Application/applicationsettings.json
@@ -54,7 +54,6 @@
"dbconfig": {
"ConnectionStrings": [
"8.153.108.90,Password=yd708090,SyncTimeout=15000,ConnectTimeout=15000,ResponseTimeout=15000,defaultDatabase=7,poolsize=100,ssl=false,writeBuffer=10240,idleTimeout=30000,connectRetry=3,asyncPipeline=false"
- //"8.153.108.90,Password=yd708090,SyncTimeout=15000,ConnectTimeout=15000,ResponseTimeout=15000,defaultDatabase=8,poolsize=100,ssl=false,writeBuffer=10240,idleTimeout=30000,connectRetry=3,asyncPipeline=false"
],
//"Sentinels": [
// "192.169.1.10:26379",
diff --git a/YD_Event.Core/InitData/sysmenu.txt b/YD_Event.Core/InitData/sysmenu.txt
index 8818eb8..a7a7fa3 100644
--- a/YD_Event.Core/InitData/sysmenu.txt
+++ b/YD_Event.Core/InitData/sysmenu.txt
@@ -2128,7 +2128,7 @@
"ParentId": 1086,
"RouteName": "scoringCast",
"Path": "/scoring/cast",
- "Component": "scoring/workbench/index",
+ "Component": "scoring/screen/index",
"Redirect": "",
"Icon": "ele-DataLine",
"IsIframe": "0",
@@ -2138,7 +2138,7 @@
"IsFixed": "0",
"Status": 0,
"Sort": 4,
- "Remark": "成绩投屏操作台",
+ "Remark": "成绩投屏大屏",
"DeleteMark": false,
"CreatedUserId": 1000,
"CreatedTime": "2023-06-01T00:00:00"