YD_Event.Admin/YD_Event.Application/Event/EventArrangementService.cs
2026-04-21 08:38:45 +08:00

501 lines
20 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;
public EventArrangementService(
ISqlSugarRepository<EventProject> eventProjectRepository,
ISqlSugarRepository<YD_Event.Core.Entities.Project> projectRepository,
ISqlSugarRepository<ProjectGroup> groupRepository,
ISqlSugarRepository<EventRegistrationPerson> personRepository,
ISqlSugarRepository<EventRegistration> registrationRepository,
ISqlSugarRepository<EventInfo> eventInfoRepository) : base(projectRepository)
{
_eventProjectRepository = eventProjectRepository;
_projectRepository = projectRepository;
_groupRepository = groupRepository;
_personRepository = personRepository;
_registrationRepository = registrationRepository;
_eventInfoRepository = eventInfoRepository;
}
/// <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 })
.ToListAsync();
// 在内存中统计团队数
var teamCounts = personWithTeam
.GroupBy(x => new { x.ProjectId, x.GroupName, x.TeamName })
.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)
{
var countObj = counts.FirstOrDefault(c => c.ProjectId == eventProj.Id && c.GroupName == group.GroupName);
dto.Groups.Add(new ArrangementGroupDto
{
GroupName = group.GroupName,
ParticipantCount = countObj?.Count ?? 0
});
}
// 根据项目类型计算总时长
int venueCount = proj.VenueCount > 0 ? proj.VenueCount : 1;
int totalParticipants = dto.Groups.Sum(g => g.ParticipantCount);
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;
project.SetTotalDurationInSeconds(setting.DurationSeconds);
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 })
.ToListAsync();
var teamCounts = personWithTeam
.GroupBy(x => new { x.ProjectId, x.GroupName, x.TeamName })
.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;
// 获取参赛人员
var persons = await _personRepository.AsQueryable()
.InnerJoin<EventRegistration>((p, r) => p.RegistrationId == r.Id)
.Where((p, r) => p.ProjectId == eventProj.Id
&& r.PaymentStatus != 4
&& !p.DeleteMark
&& p.PersonType == PersonType.Athlete)
.OrderBy(p => p.GroupName)
.OrderBy(p => p.Id)
.Select((p, r) => new {
p.Name,
p.GroupName,
r.TeamName,
Duration = proj.GetTotalDurationInSeconds()
})
.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;
foreach (var person in persons)
{
result.Add(new ArrangementPreviewOutput
{
TimeRange = $"{proj.GetTotalDurationInSeconds()}''",
ProjectName = proj.Name,
ProjectType = proj.ProjectType.Description(),
GroupName = person.GroupName,
TeamName = person.TeamName,
AthleteName = person.Name,
SessionIndex = currentRound,
VenueIndex = currentVenue,
ProjectTotalDurationSeconds = projectTotalDurationSeconds,
ProjectTotalDuration = projectTotalDuration
});
currentVenue++;
if (currentVenue > venueCount)
{
currentVenue = 1;
currentRound++;
}
}
}
return result;
}
/// <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:G1)
var titleColor = Color.FromArgb(255, 217, 179);
SetMergeTitle(_eventInfo?.EventName ?? "赛事编排表", 1, 1, 1, 7, titleColor, 14);
// 第二行:列标题
var headers = new[] { "时间", "参赛项目", "参赛组别", "参赛队伍", "运动员姓名", "场次", "场地" };
var headerColor = Color.FromArgb(204, 255, 204);
SetHeaders(headers, 2, headerColor, 11);
// 填充数据行
var alternateBgColor = Color.FromArgb(240, 255, 240);
// 按项目分组处理,合并时间列
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, 1, mergeEndRow, 1];
range.Merge = true;
// 设置时间列居中对齐
for (int r = projectStartRow; r <= mergeEndRow; r++)
{
Worksheet.Cells[r, 1].Style.HorizontalAlignment = ExcelHorizontalAlignment.Center;
Worksheet.Cells[r, 1].Style.VerticalAlignment = ExcelVerticalAlignment.Center;
}
}
}
currentProjectName = item.ProjectName;
projectStartRow = rowIndex;
// 设置项目总时长
Worksheet.Cells[rowIndex, 1].Value = item.ProjectTotalDuration;
}
else
{
// 同一项目的其他行不设置时间值(合并后会自动应用)
}
Worksheet.Cells[rowIndex, 2].Value = item.ProjectName;
Worksheet.Cells[rowIndex, 3].Value = item.GroupName;
Worksheet.Cells[rowIndex, 4].Value = item.TeamName ?? "";
Worksheet.Cells[rowIndex, 5].Value = item.AthleteName;
Worksheet.Cells[rowIndex, 6].Value = item.SessionIndex;
Worksheet.Cells[rowIndex, 7].Value = item.VenueIndex;
SetDataRowStyle(rowIndex, 7, 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, 1, mergeEndRow, 1];
range.Merge = true;
// 设置时间列居中对齐
for (int r = projectStartRow; r <= mergeEndRow; r++)
{
Worksheet.Cells[r, 1].Style.HorizontalAlignment = ExcelHorizontalAlignment.Center;
Worksheet.Cells[r, 1].Style.VerticalAlignment = ExcelVerticalAlignment.Center;
}
}
}
// 设置列宽
var widths = new double[] { 12, 18, 15, 25, 15, 8, 8 };
SetColumnWidths(widths);
// 设置行高
SetRowHeight(1, 30);
SetRowHeight(2, 25);
// 冻结首行和首列
FreezePanes(3, 1);
return await GetAsByteArrayAsync();
}
}
}