using YD_Event.Application.Event.Dtos; using YD_Event.Core.Domain.Export; using YD_Event.Core.Entities; using YD_Event.Core.Extensions; using MiniExcelLibs; using SqlSugar; using OfficeOpenXml; using OfficeOpenXml.Style; using System.Drawing; namespace YD_Event.Application.Event; /// /// 赛事编排服务 /// public class EventArrangementService : BaseService, ITransient { private readonly ISqlSugarRepository _eventProjectRepository; private readonly ISqlSugarRepository _projectRepository; private readonly ISqlSugarRepository _groupRepository; private readonly ISqlSugarRepository _personRepository; private readonly ISqlSugarRepository _registrationRepository; private readonly ISqlSugarRepository _eventInfoRepository; private readonly ISqlSugarRepository _appUserRepository; private readonly ISqlSugarRepository _merchantCertificationRepository; public EventArrangementService( ISqlSugarRepository eventProjectRepository, ISqlSugarRepository projectRepository, ISqlSugarRepository groupRepository, ISqlSugarRepository personRepository, ISqlSugarRepository registrationRepository, ISqlSugarRepository eventInfoRepository, ISqlSugarRepository appUserRepository, ISqlSugarRepository merchantCertificationRepository) : base(projectRepository) { _eventProjectRepository = eventProjectRepository; _projectRepository = projectRepository; _groupRepository = groupRepository; _personRepository = personRepository; _registrationRepository = registrationRepository; _eventInfoRepository = eventInfoRepository; _appUserRepository = appUserRepository; _merchantCertificationRepository = merchantCertificationRepository; } /// /// 格式化时长显示(符号格式:X'X") /// private string FormatDurationSymbol(int totalSeconds) { if (totalSeconds <= 0) { return "0'0\""; } var hours = totalSeconds / 3600; var remainingSecondsAfterHours = totalSeconds % 3600; var minutes = remainingSecondsAfterHours / 60; var seconds = remainingSecondsAfterHours % 60; if (hours == 0) { if (seconds == 0) { return $"{minutes}'"; } else { var paddedSeconds = seconds.ToString().PadLeft(2, '0'); return $"{minutes}'{paddedSeconds}\""; } } else { var result = $"{hours}h"; if (minutes > 0 || seconds > 0) { result += $"{minutes}'"; if (seconds > 0) { var paddedSeconds = seconds.ToString().PadLeft(2, '0'); result += $"{paddedSeconds}\""; } } return result; } } /// /// 获取编排项目列表 /// [DisplayName("获取编排项目列表")] public async Task> GetList(long eventId) { var eventProjects = await _eventProjectRepository.GetListAsync(x => x.EventId == eventId); var projectIds = eventProjects.Select(x => x.ProjectId).Distinct().ToList(); var projects = await _projectRepository.GetListAsync(x => projectIds.Contains(x.Id)); // 按照 SortIndex 对 projects 排序 var sortedProjects = projects.OrderBy(x => x.SortIndex).ToList(); var groups = await _groupRepository.GetListAsync(x => projectIds.Contains(x.ProjectId)); // 统计各组别报名人数(统计所有非取消状态的报名,并且只统计运动员 var counts = await _personRepository.AsQueryable() .InnerJoin((p, r) => p.RegistrationId == r.Id) .Where((p, r) => r.EventId == eventId // 线上支付只显示已支付(1),线下支付显示待审核(0)和已支付(1) && ((r.PaymentMethod == 2 && r.PaymentStatus == 1) || (r.PaymentMethod == 1 && (r.PaymentStatus == 0 || r.PaymentStatus == 1))) && !p.DeleteMark // 排除已删除的人员 && p.PersonType == PersonType.Athlete) // 只统计运动员 .GroupBy((p, r) => new { p.ProjectId, p.GroupName }) .Select((p, r) => new { p.ProjectId, p.GroupName, Count = SqlFunc.AggregateCount(p.Id) }) .ToListAsync(); // 获取所有相关的人员和报名记录(用于统计团队数) var personWithTeam = await _personRepository.AsQueryable() .InnerJoin((p, r) => p.RegistrationId == r.Id) .Where((p, r) => r.EventId == eventId // 线上支付只显示已支付(1),线下支付显示待审核(0)和已支付(1) && ((r.PaymentMethod == 2 && r.PaymentStatus == 1) || (r.PaymentMethod == 1 && (r.PaymentStatus == 0 || r.PaymentStatus == 1))) && !p.DeleteMark && p.PersonType == PersonType.Athlete && r.TeamName != null) .Select((p, r) => new { p.ProjectId, p.GroupName, r.TeamName, p.RegistrationItemId }) .ToListAsync(); // 在内存中统计团队数(增加 RegistrationItemId 作为分组条件,与 Preview 方法保持一致) var teamCounts = personWithTeam .GroupBy(x => new { x.ProjectId, x.GroupName, x.TeamName, x.RegistrationItemId }) .GroupBy(g => new { g.Key.ProjectId, g.Key.GroupName }) .Select(g => new { g.Key.ProjectId, g.Key.GroupName, Count = g.Count() }) .ToList(); var result = new List(); // 按照排序后的项目顺序处理 foreach (var proj in sortedProjects) { var eventProj = eventProjects.FirstOrDefault(x => x.ProjectId == proj.Id); if (eventProj == null) continue; var dto = new ArrangementProjectListOutput { ProjectId = eventProj.Id, ProjectName = proj.Name, ProjectType = proj.ProjectType.Description(), DurationSeconds = proj.GetTotalDurationInSeconds(), VenueCount = proj.VenueCount, SortIndex = proj.SortIndex, Groups = new List() }; var projGroups = groups.Where(g => g.ProjectId == proj.Id).ToList(); foreach (var group in projGroups) { int displayCount; if (proj.IsTeamProject()) { // 团队项目:显示团队数 var teamCountObj = teamCounts.FirstOrDefault(tc => tc.ProjectId == eventProj.Id && tc.GroupName == group.GroupName); displayCount = teamCountObj?.Count ?? 0; } else { // 个人项目:显示人数 var countObj = counts.FirstOrDefault(c => c.ProjectId == eventProj.Id && c.GroupName == group.GroupName); displayCount = countObj?.Count ?? 0; } dto.Groups.Add(new ArrangementGroupDto { GroupName = group.GroupName, ParticipantCount = displayCount }); } // 根据项目类型计算总时长 int venueCount = proj.VenueCount > 0 ? proj.VenueCount : 1; // 重新计算总参与人数(用于后续判断) int totalParticipants = 0; foreach (var group in projGroups) { var countObj = counts.FirstOrDefault(c => c.ProjectId == eventProj.Id && c.GroupName == group.GroupName); totalParticipants += countObj?.Count ?? 0; } int totalTeams = 0; // 如果是团队项目,计算团队总数 if (proj.IsTeamProject()) { totalTeams = teamCounts .Where(tc => tc.ProjectId == eventProj.Id) .Sum(tc => tc.Count); } // 检查是否有有效参与者:个人项目检查报名人数,团队项目检查团队数 bool hasValidParticipants; if (proj.IsTeamProject()) { hasValidParticipants = totalTeams > 0; } else { hasValidParticipants = totalParticipants > 0; } // 如果没有有效参与者,跳过这个项目 if (!hasValidParticipants) { continue; } int totalDurationSeconds; if (proj.IsTeamProject()) { // 团队项目:(队伍数 ÷ 场地数) × 项目时长 int teamCount = totalTeams > 0 ? totalTeams : 1; int rounds = (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); totalDurationSeconds = rounds * proj.GetTotalDurationInSeconds(); } // 设置总时长秒数和分钟数 dto.TotalDurationSeconds = totalDurationSeconds; dto.TotalDurationMinutes = (int)Math.Ceiling((double)totalDurationSeconds / 60); dto.TotalTeams = totalTeams; result.Add(dto); } return result; } /// /// 更新编排设置 /// [UnitOfWork] [DisplayName("更新编排设置")] public async Task Update(ArrangementUpdateInput input) { var eventProjectIds = input.Settings.Select(x => x.ProjectId).ToList(); var eventProjects = await _eventProjectRepository.GetListAsync(x => eventProjectIds.Contains(x.Id)); foreach (var setting in input.Settings) { var ep = eventProjects.FirstOrDefault(x => x.Id == setting.ProjectId); if (ep != null) { var project = await _projectRepository.GetByIdAsync(ep.ProjectId); if (project != null) { project.VenueCount = setting.VenueCount; project.SortIndex = setting.SortIndex; await _projectRepository.UpdateAsync(project); } } } } /// /// 编排预览 /// [DisplayName("编排预览")] public async Task> Preview(long eventId) { // 1. 获取项目列表和统计数据 var eventProjects = await _eventProjectRepository.GetListAsync(x => x.EventId == eventId); var projectIds = eventProjects.Select(x => x.ProjectId).Distinct().ToList(); var projects = await _projectRepository.GetListAsync(x => projectIds.Contains(x.Id)); // 按照 SortIndex 对 projects 排序 var sortedProjects = projects.OrderBy(x => x.SortIndex).ToList(); var groups = await _groupRepository.GetListAsync(x => projectIds.Contains(x.ProjectId)); // 统计各组别报名人数 var counts = await _personRepository.AsQueryable() .InnerJoin((p, r) => p.RegistrationId == r.Id) .Where((p, r) => r.EventId == eventId // 线上支付只显示已支付(1),线下支付显示待审核(0)和已支付(1) && ((r.PaymentMethod == 2 && r.PaymentStatus == 1) || (r.PaymentMethod == 1 && (r.PaymentStatus == 0 || r.PaymentStatus == 1))) && !p.DeleteMark && p.PersonType == PersonType.Athlete) .GroupBy((p, r) => new { p.ProjectId, p.GroupName }) .Select((p, r) => new { p.ProjectId, p.GroupName, Count = SqlFunc.AggregateCount(p.Id) }) .ToListAsync(); // 获取团队数 var personWithTeam = await _personRepository.AsQueryable() .InnerJoin((p, r) => p.RegistrationId == r.Id) .Where((p, r) => r.EventId == eventId // 线上支付只显示已支付(1),线下支付显示待审核(0)和已支付(1) && ((r.PaymentMethod == 2 && r.PaymentStatus == 1) || (r.PaymentMethod == 1 && (r.PaymentStatus == 0 || r.PaymentStatus == 1))) && !p.DeleteMark && p.PersonType == PersonType.Athlete && r.TeamName != null) .Select((p, r) => new { p.ProjectId, p.GroupName, r.TeamName, p.RegistrationItemId }) .ToListAsync(); var teamCounts = personWithTeam .GroupBy(x => new { x.ProjectId, x.GroupName, x.TeamName, x.RegistrationItemId }) .GroupBy(g => new { g.Key.ProjectId, g.Key.GroupName }) .Select(g => new { g.Key.ProjectId, g.Key.GroupName, Count = g.Count() }) .ToList(); var result = new List(); // 2. 生成排期 - 按照排序后的项目顺序处理 foreach (var proj in sortedProjects) { var eventProj = eventProjects.FirstOrDefault(x => x.ProjectId == proj.Id); if (eventProj == null) continue; int venueCount = proj.VenueCount > 0 ? proj.VenueCount : 1; // 计算项目总时长(在查询前计算,避免在SQL中使用C#方法) int projTotalDuration = proj.GetTotalDurationInSeconds(); // 第一步:先查询主要数据和相关的 MerchantCertificationId var basicPersons = await _personRepository.AsQueryable() .InnerJoin((p, r) => p.RegistrationId == r.Id) .Where((p, r) => p.ProjectId == eventProj.Id // 线上支付只显示已支付(1),线下支付显示待审核(0)和已支付(1) && ((r.PaymentMethod == 2 && r.PaymentStatus == 1) || (r.PaymentMethod == 1 && (r.PaymentStatus == 0 || r.PaymentStatus == 1))) && !p.DeleteMark && p.PersonType == PersonType.Athlete) .OrderBy(p => p.GroupName) .OrderBy(p => p.Id) .Select((p, r) => new { Id = p.Id, RegistrationItemId = p.RegistrationItemId, p.UserId, p.Name, p.GroupName, r.TeamName, r.MerchantCertificationId, Duration = projTotalDuration }) .ToListAsync(); // 第二步:收集所有需要查询的 MerchantCertificationId var merchantCertIds = basicPersons .Where(x => x.MerchantCertificationId.HasValue) .Select(x => x.MerchantCertificationId.Value) .Distinct() .ToList(); // 第三步:批量查询 MerchantCertification Dictionary merchantDict = new Dictionary(); if (merchantCertIds.Any()) { var merchants = await _merchantCertificationRepository.GetListAsync(x => merchantCertIds.Contains(x.Id)); merchantDict = merchants.ToDictionary(x => x.Id, x => x); } // 第四步:在内存中合并数据 var persons = basicPersons.Select(bp => new { bp.Id, bp.RegistrationItemId, bp.UserId, bp.Name, bp.GroupName, bp.TeamName, MerchantName = bp.MerchantCertificationId.HasValue && merchantDict.ContainsKey(bp.MerchantCertificationId.Value) ? merchantDict[bp.MerchantCertificationId.Value].MerchantName : null, AuditStatus = bp.MerchantCertificationId.HasValue && merchantDict.ContainsKey(bp.MerchantCertificationId.Value) ? merchantDict[bp.MerchantCertificationId.Value].AuditStatus : (AuditStatus?)null, bp.Duration }).ToList(); if (!persons.Any()) continue; // 计算项目总时长 int totalParticipants = 0; var projGroups = groups.Where(g => g.ProjectId == proj.Id).ToList(); foreach (var group in projGroups) { var countObj = counts.FirstOrDefault(c => c.ProjectId == eventProj.Id && c.GroupName == group.GroupName); totalParticipants += countObj?.Count ?? 0; } int totalTeams = 0; if (proj.IsTeamProject()) { totalTeams = teamCounts .Where(tc => tc.ProjectId == eventProj.Id) .Sum(tc => tc.Count); } int projectTotalDurationSeconds; if (proj.IsTeamProject()) { int teamCount = totalTeams > 0 ? totalTeams : 1; int rounds = (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); projectTotalDurationSeconds = rounds * proj.GetTotalDurationInSeconds(); } string projectTotalDuration = FormatDurationSymbol(projectTotalDurationSeconds); // 分组逻辑 int currentRound = 1; int currentVenue = 1; if (proj.IsTeamProject()) { // 团队项目:按团队分组,同一团队的运动员姓名用逗号分隔 // 增加 RegistrationItemId 作为分组条件,确保同一条报名记录的多个小组被正确区分 var teamsGroup = persons .GroupBy(p => new { p.GroupName, p.TeamName, p.RegistrationItemId }) .OrderBy(g => g.Key.GroupName) .ThenBy(g => g.Key.TeamName) .ThenBy(g => g.Key.RegistrationItemId); foreach (var teamGroup in teamsGroup) { var athleteNames = teamGroup.Select(p => p.Name).Distinct().OrderBy(n => n).ToList(); string combinedAthleteNames = string.Join(", ", athleteNames); // 获取团队中第一个人的认证信息作为团队单位信息 var firstPerson = teamGroup.First(); string displayUnitName = GetDisplayUnitName(firstPerson); result.Add(new ArrangementPreviewOutput { RegistrationItemId = firstPerson.RegistrationItemId, // 团队项目保持使用 EventRegistrationItem.Id TimeRange = $"{proj.GetTotalDurationInSeconds()}''", ProjectName = proj.Name, ProjectType = proj.ProjectType.Description(), GroupName = teamGroup.Key.GroupName, TeamName = teamGroup.Key.TeamName, AthleteName = combinedAthleteNames, UnitName = displayUnitName, SessionIndex = currentRound, VenueIndex = currentVenue, ProjectTotalDurationSeconds = projectTotalDurationSeconds, ProjectTotalDuration = projectTotalDuration }); currentVenue++; if (currentVenue > venueCount) { currentVenue = 1; currentRound++; } } } else { // 个人项目:根据认证状态显示单位信息 foreach (var person in persons) { string displayUnitName = GetDisplayUnitName(person); result.Add(new ArrangementPreviewOutput { RegistrationItemId = person.Id, // 个人项目使用 EventRegistrationPerson.Id TimeRange = $"{proj.GetTotalDurationInSeconds()}''", ProjectName = proj.Name, ProjectType = proj.ProjectType.Description(), GroupName = person.GroupName, TeamName = person.TeamName, AthleteName = person.Name, UnitName = displayUnitName, SessionIndex = currentRound, VenueIndex = currentVenue, ProjectTotalDurationSeconds = projectTotalDurationSeconds, ProjectTotalDuration = projectTotalDuration }); currentVenue++; if (currentVenue > venueCount) { currentVenue = 1; currentRound++; } } } } return result; } /// /// 根据单位认证状态获取显示的单位名称 /// private string GetDisplayUnitName(T personData) where T : class { // 使用反射获取属性值 var merchantNameProp = personData.GetType().GetProperty("MerchantName"); var auditStatusProp = personData.GetType().GetProperty("AuditStatus"); var teamNameProp = personData.GetType().GetProperty("TeamName"); string? merchantName = merchantNameProp?.GetValue(personData) as string; var auditStatusValue = auditStatusProp?.GetValue(personData); AuditStatus? auditStatus = null; if (auditStatusValue != null && auditStatusValue is AuditStatus status) { auditStatus = status; } string? teamName = teamNameProp?.GetValue(personData) as string; // 优先使用认证通过的单位名称 if (!string.IsNullOrEmpty(merchantName) && auditStatus == AuditStatus.Pass) { return merchantName; } // 如果没有认证或认证未通过,使用团队名称 return teamName ?? ""; } /// /// 导出编排 /// [DisplayName("导出编排")] public async Task Export(long eventId) { var eventInfo = await _eventInfoRepository.GetFirstAsync(x => x.Id == eventId); var data = await Preview(eventId); var exporter = new ArrangementExporter(data, eventInfo); return await exporter.ExportAsync(); } /// /// 编排导出器 /// private class ArrangementExporter : ExcelExportBase { private readonly List _data; private readonly EventInfo _eventInfo; public ArrangementExporter(List data, EventInfo eventInfo) { _data = data ?? new List(); _eventInfo = eventInfo; } public override async Task ExportAsync() { CreateWorksheet("编排表"); // 第一行:赛事标题(合并单元格 A1:I1) var titleColor = Color.FromArgb(255, 217, 179); SetMergeTitle(_eventInfo?.EventName ?? "赛事编排表", 1, 1, 1, 9, titleColor, 14); // 第二行:列标题 var headers = new[] { "报名ID", "时间", "参赛项目", "参赛组别", "参赛队伍", "运动员姓名", "单位", "场次", "场地" }; var headerColor = Color.FromArgb(204, 255, 204); SetHeaders(headers, 2, headerColor, 11); // 设置第1列为文本格式,防止科学计数法显示 for (int r = 3; r <= _data.Count + 2; r++) { Worksheet.Cells[r, 1].Style.Numberformat.Format = "@"; // @符号表示文本格式 } // 填充数据行 var alternateBgColor = Color.FromArgb(240, 255, 240); // 按项目分组处理,合并时间列(时间列现在是第2列) string currentProjectName = null; int projectStartRow = 0; for (int i = 0; i < _data.Count; i++) { var item = _data[i]; int rowIndex = i + 3; // 如果是新项目或最后一行,合并前面的项目时间列 if (currentProjectName != item.ProjectName) { // 如果不是第一个项目,合并前一个项目的时间列 if (currentProjectName != null && projectStartRow > 0 && i - 1 >= 0) { int mergeEndRow = (i - 1) + 3; if (mergeEndRow > projectStartRow) { var range = Worksheet.Cells[projectStartRow, 2, mergeEndRow, 2]; range.Merge = true; // 设置时间列居中对齐 for (int r = projectStartRow; r <= mergeEndRow; r++) { Worksheet.Cells[r, 2].Style.HorizontalAlignment = ExcelHorizontalAlignment.Center; Worksheet.Cells[r, 2].Style.VerticalAlignment = ExcelVerticalAlignment.Center; } } } currentProjectName = item.ProjectName; projectStartRow = rowIndex; // 设置项目总时长 Worksheet.Cells[rowIndex, 2].Value = item.ProjectTotalDuration ?? ""; } else { // 同一项目的其他行不设置时间值(合并后会自动应用) } // 设置所有单元格值,确保没有 null 值 // 将报名项目唯一码转换为字符串格式,防止科学计数法 Worksheet.Cells[rowIndex, 1].Value = item.RegistrationItemId.ToString(); Worksheet.Cells[rowIndex, 3].Value = item.ProjectName ?? ""; Worksheet.Cells[rowIndex, 4].Value = item.GroupName ?? ""; Worksheet.Cells[rowIndex, 5].Value = item.TeamName ?? ""; Worksheet.Cells[rowIndex, 6].Value = item.AthleteName ?? ""; Worksheet.Cells[rowIndex, 7].Value = item.UnitName ?? ""; Worksheet.Cells[rowIndex, 8].Value = item.SessionIndex; Worksheet.Cells[rowIndex, 9].Value = item.VenueIndex; SetDataRowStyle(rowIndex, 9, alternateBgColor, i % 2 == 1); } // 处理最后一个项目的合并 if (currentProjectName != null && projectStartRow > 0 && _data.Count > 0) { int mergeEndRow = (_data.Count - 1) + 3; if (mergeEndRow > projectStartRow) { var range = Worksheet.Cells[projectStartRow, 2, mergeEndRow, 2]; range.Merge = true; // 设置时间列居中对齐 for (int r = projectStartRow; r <= mergeEndRow; r++) { Worksheet.Cells[r, 2].Style.HorizontalAlignment = ExcelHorizontalAlignment.Center; Worksheet.Cells[r, 2].Style.VerticalAlignment = ExcelVerticalAlignment.Center; } } } // 设置列宽 var widths = new double[] { 12, 12, 18, 15, 25, 15, 20, 8, 8 }; SetColumnWidths(widths); // 设置行高 SetRowHeight(1, 30); SetRowHeight(2, 25); // 冻结首行和首列 FreezePanes(3, 1); return await GetAsByteArrayAsync(); } } }