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; private readonly ISqlSugarRepository _venueRepository; private readonly ISqlSugarRepository _sessionRepository; private readonly ISqlSugarRepository _sessionEntryRepository; private readonly ISqlSugarRepository _judgeAssignmentRepository; private readonly ISqlSugarRepository _participationRepository; private readonly IIdGenerator _idGenerator; private readonly YD_Event.Application.Auth.AuthManager _authManager; public EventArrangementService( ISqlSugarRepository eventProjectRepository, ISqlSugarRepository projectRepository, ISqlSugarRepository groupRepository, ISqlSugarRepository personRepository, ISqlSugarRepository registrationRepository, ISqlSugarRepository eventInfoRepository, ISqlSugarRepository appUserRepository, ISqlSugarRepository merchantCertificationRepository, ISqlSugarRepository venueRepository, ISqlSugarRepository sessionRepository, ISqlSugarRepository sessionEntryRepository, ISqlSugarRepository judgeAssignmentRepository, ISqlSugarRepository participationRepository, IIdGenerator idGenerator, YD_Event.Application.Auth.AuthManager authManager) : base(projectRepository) { _eventProjectRepository = eventProjectRepository; _projectRepository = projectRepository; _groupRepository = groupRepository; _personRepository = personRepository; _registrationRepository = registrationRepository; _eventInfoRepository = eventInfoRepository; _appUserRepository = appUserRepository; _merchantCertificationRepository = merchantCertificationRepository; _venueRepository = venueRepository; _sessionRepository = sessionRepository; _sessionEntryRepository = sessionEntryRepository; _judgeAssignmentRepository = judgeAssignmentRepository; _participationRepository = participationRepository; _idGenerator = idGenerator; _authManager = authManager; } /// /// 格式化时长显示(符号格式: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) .Select((p, r) => new { Id = p.Id, RegistrationItemId = p.RegistrationItemId, p.UserId, p.Name, p.GroupName, p.EventProjectGroupId, 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.EventProjectGroupId, 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; var projGroups = groups.Where(g => g.ProjectId == proj.Id).OrderBy(g => g.Id).ToList(); int totalParticipants = 0; int totalTeams = 0; foreach (var group in projGroups) { var countObj = counts.FirstOrDefault(c => c.ProjectId == eventProj.Id && c.GroupName == group.GroupName); totalParticipants += countObj?.Count ?? 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; foreach (var group in projGroups) { var groupPersons = persons.Where(p => p.GroupName == group.GroupName).ToList(); if (!groupPersons.Any()) continue; if (proj.IsTeamProject()) { var teamsGroup = groupPersons .GroupBy(p => new { p.TeamName, p.RegistrationItemId }) .OrderBy(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 { EventProjectId = eventProj.Id, ProjectId = proj.Id, EventProjectGroupId = teamGroup.Select(x => x.EventProjectGroupId).Distinct().Count() == 1 ? teamGroup.First().EventProjectGroupId : null, RegistrationItemId = firstPerson.RegistrationItemId, RegistrationPersonIds = teamGroup.Select(x => x.Id).Distinct().ToList(), TimeRange = $"{proj.GetTotalDurationInSeconds()}''", ProjectName = proj.Name, ProjectType = proj.ProjectType.Description(), GroupName = group.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 groupPersons.OrderBy(p => p.TeamName).ThenBy(p => p.Name).ThenBy(p => p.Id)) { string displayUnitName = GetDisplayUnitName(person); result.Add(new ArrangementPreviewOutput { EventProjectId = eventProj.Id, ProjectId = proj.Id, EventProjectGroupId = person.EventProjectGroupId, RegistrationItemId = person.Id, RegistrationPersonIds = new List { person.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; } /// 确认编排并固化场地、场次及出场记录。 [HttpPost("confirm/{eventId}"), DisplayName("确认赛事编排"), UnitOfWork] public async Task Confirm(long eventId) { var eventInfo = await _eventInfoRepository.GetByIdAsync(eventId); if (eventInfo == null) throw Oops.Bah("赛事不存在"); var eventProjects = await _eventProjectRepository.AsQueryable().Where(x => x.EventId == eventId).ToListAsync(); var projects = await _projectRepository.AsQueryable().Where(x => eventProjects.Select(e => e.ProjectId).Contains(x.Id)).ToListAsync(); var missingRules = projects.Where(x => !x.ScoreRuleId.HasValue).Select(x => x.Name).ToList(); if (missingRules.Count > 0) throw Oops.Bah($"以下项目未配置计分规则:{string.Join("、", missingRules)}"); if (await _judgeAssignmentRepository.IsAnyAsync(x => x.EventId == eventId && x.AssignmentStatus == 1)) throw Oops.Bah("裁判分配已提交,不能重新确认编排"); var preview = await Preview(eventId); if (preview.Count == 0) throw Oops.Bah("没有可确认的编排记录"); var oldSessionIds = await _sessionRepository.AsQueryable().Where(x => x.EventId == eventId).Select(x => x.Id).ToListAsync(); if (oldSessionIds.Count > 0) { await _judgeAssignmentRepository.SoftDeleteAsync(x => oldSessionIds.Contains(x.SessionId)); await _sessionEntryRepository.SoftDeleteAsync(x => oldSessionIds.Contains(x.SessionId)); await _sessionRepository.SoftDeleteAsync(x => x.EventId == eventId); } await _venueRepository.SoftDeleteAsync(x => x.EventId == eventId); var now = DateTime.Now; var userId = _authManager.UserId; var maxVenue = preview.Max(x => x.VenueIndex); var venues = Enumerable.Range(1, maxVenue).Select(i => new EventVenue { Id = _idGenerator.NextId(), EventId = eventId, VenueCode = $"V{i:00}", VenueName = $"场地{i}", Sort = i, Status = AvailabilityStatus.Enable, CreatedUserId = userId, CreatedTime = now }).ToList(); await _venueRepository.InsertRangeAsync(venues); 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 first = group.First(); var project = projectMap[first.ProjectId]; var venue = venues.First(x => x.Sort == group.Key.VenueIndex); 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, CreatedUserId = userId, CreatedTime = now }; sessions.Add(session); var order = 1; foreach (var item in group.OrderBy(x => x.SessionIndex)) { var personId = item.RegistrationPersonIds.FirstOrDefault(); var record = participationRecords.FirstOrDefault(x => x.RegistrationPersonId == personId); entries.Add(new EventSessionEntry { Id = _idGenerator.NextId(), SessionId = session.Id, ParticipationRecordId = record?.Id, RegistrationItemId = item.RegistrationItemId, EntryNo = $"{project.SortIndex:000}{group.Key.VenueIndex:00}{order:000}", PlayOrder = order++, EntryStatus = 0, ParticipantName = item.AthleteName, TeamName = item.TeamName, GroupName = item.GroupName, Version = 1, Status = AvailabilityStatus.Enable, CreatedUserId = userId, CreatedTime = now }); } } await _sessionRepository.InsertRangeAsync(sessions); await _sessionEntryRepository.InsertRangeAsync(entries); return new ConfirmArrangementOutput { VenueCount = venues.Count, SessionCount = sessions.Count, EntryCount = entries.Count }; } /// /// 根据单位认证状态获取显示的单位名称 /// 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(); } } }