YD_Event.Admin/YD_Event.Application/Event/EventArrangementService.cs
2026-04-27 08:23:31 +08:00

621 lines
26 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
&& r.PaymentStatus != 4 // 排除已取消的报名
&& !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
&& r.PaymentStatus != 4
&& !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);
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
&& r.PaymentStatus != 4
&& !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
&& r.PaymentStatus != 4
&& !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();
// 获取参赛人员,并关联用户和单位认证信息
var persons = await _personRepository.AsQueryable()
.InnerJoin<EventRegistration>((p, r) => p.RegistrationId == r.Id)
.LeftJoin<YD_Event.Core.Entities.AppUser>((p, r, u) => p.UserId == u.Id)
.LeftJoin<MerchantCertification>((p, r, u, m) => u.MerchantCertificationId == m.Id)
.Where((p, r, u, m) => p.ProjectId == eventProj.Id
&& r.PaymentStatus != 4
&& !p.DeleteMark
&& p.PersonType == PersonType.Athlete)
.OrderBy(p => p.GroupName)
.OrderBy(p => p.Id)
.Select((p, r, u, m) => new {
RegistrationItemId = p.RegistrationItemId,
p.UserId,
p.Name,
p.GroupName,
r.TeamName,
MerchantName = m.MerchantName ?? "",
//m.AuditStatus,
Duration = projTotalDuration
})
.ToListAsync();
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,
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.RegistrationItemId,
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;
AuditStatus? auditStatus = auditStatusProp?.GetValue(personData) as AuditStatus?;
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();
}
}
}