YD_Event.Admin/YD_Event.Application/Event/EventArrangementService.cs

661 lines
28 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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();
}
}
}