661 lines
28 KiB
C#
661 lines
28 KiB
C#
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;
|
||
|
||
/// <summary>
|
||
/// 赛事编排服务
|
||
/// </summary>
|
||
public class EventArrangementService : BaseService<YD_Event.Core.Entities.Project>, ITransient
|
||
{
|
||
private readonly ISqlSugarRepository<EventProject> _eventProjectRepository;
|
||
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Project> _projectRepository;
|
||
private readonly ISqlSugarRepository<ProjectGroup> _groupRepository;
|
||
private readonly ISqlSugarRepository<EventRegistrationPerson> _personRepository;
|
||
private readonly ISqlSugarRepository<EventRegistration> _registrationRepository;
|
||
private readonly ISqlSugarRepository<EventInfo> _eventInfoRepository;
|
||
private readonly ISqlSugarRepository<YD_Event.Core.Entities.AppUser> _appUserRepository;
|
||
private readonly ISqlSugarRepository<MerchantCertification> _merchantCertificationRepository;
|
||
|
||
public EventArrangementService(
|
||
ISqlSugarRepository<EventProject> eventProjectRepository,
|
||
ISqlSugarRepository<YD_Event.Core.Entities.Project> projectRepository,
|
||
ISqlSugarRepository<ProjectGroup> groupRepository,
|
||
ISqlSugarRepository<EventRegistrationPerson> personRepository,
|
||
ISqlSugarRepository<EventRegistration> registrationRepository,
|
||
ISqlSugarRepository<EventInfo> eventInfoRepository,
|
||
ISqlSugarRepository<YD_Event.Core.Entities.AppUser> appUserRepository,
|
||
ISqlSugarRepository<MerchantCertification> merchantCertificationRepository) : base(projectRepository)
|
||
{
|
||
_eventProjectRepository = eventProjectRepository;
|
||
_projectRepository = projectRepository;
|
||
_groupRepository = groupRepository;
|
||
_personRepository = personRepository;
|
||
_registrationRepository = registrationRepository;
|
||
_eventInfoRepository = eventInfoRepository;
|
||
_appUserRepository = appUserRepository;
|
||
_merchantCertificationRepository = merchantCertificationRepository;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 格式化时长显示(符号格式:X'X")
|
||
/// </summary>
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取编排项目列表
|
||
/// </summary>
|
||
[DisplayName("获取编排项目列表")]
|
||
public async Task<List<ArrangementProjectListOutput>> 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<EventRegistration>((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<EventRegistration>((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<ArrangementProjectListOutput>();
|
||
// 按照排序后的项目顺序处理
|
||
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<ArrangementGroupDto>()
|
||
};
|
||
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新编排设置
|
||
/// </summary>
|
||
[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);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 编排预览
|
||
/// </summary>
|
||
[DisplayName("编排预览")]
|
||
public async Task<List<ArrangementPreviewOutput>> 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<EventRegistration>((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<EventRegistration>((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<ArrangementPreviewOutput>();
|
||
|
||
// 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<EventRegistration>((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,
|
||
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<long, MerchantCertification> merchantDict = new Dictionary<long, MerchantCertification>();
|
||
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;
|
||
|
||
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
|
||
{
|
||
RegistrationItemId = firstPerson.RegistrationItemId,
|
||
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
|
||
{
|
||
RegistrationItemId = 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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据单位认证状态获取显示的单位名称
|
||
/// </summary>
|
||
private string GetDisplayUnitName<T>(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 ?? "";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 导出编排
|
||
/// </summary>
|
||
[DisplayName("导出编排")]
|
||
public async Task<byte[]> 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();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 编排导出器
|
||
/// </summary>
|
||
private class ArrangementExporter : ExcelExportBase
|
||
{
|
||
private readonly List<ArrangementPreviewOutput> _data;
|
||
private readonly EventInfo _eventInfo;
|
||
|
||
public ArrangementExporter(List<ArrangementPreviewOutput> data, EventInfo eventInfo)
|
||
{
|
||
_data = data ?? new List<ArrangementPreviewOutput>();
|
||
_eventInfo = eventInfo;
|
||
}
|
||
|
||
public override async Task<byte[]> 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();
|
||
}
|
||
}
|
||
}
|