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

497 lines
20 KiB
C#
Raw Normal View History

2026-03-08 15:46:19 +08:00
using YD_Event.Application.Event.Dtos;
using YD_Event.Core.Domain.Export;
2026-02-08 18:09:05 +08:00
using YD_Event.Core.Entities;
using YD_Event.Core.Extensions;
2026-02-08 18:09:05 +08:00
using MiniExcelLibs;
using SqlSugar;
2026-03-31 15:54:27 +08:00
using OfficeOpenXml;
using OfficeOpenXml.Style;
using System.Drawing;
2026-02-08 18:09:05 +08:00
2026-03-08 15:46:19 +08:00
namespace YD_Event.Application.Event;
2026-02-08 18:09:05 +08:00
/// <summary>
/// 赛事编排服务
/// </summary>
2026-03-08 15:46:19 +08:00
public class EventArrangementService : BaseService<YD_Event.Core.Entities.Project>, ITransient
2026-02-08 18:09:05 +08:00
{
2026-03-18 16:25:49 +08:00
private readonly ISqlSugarRepository<EventProject> _eventProjectRepository;
2026-03-01 22:39:00 +08:00
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Project> _projectRepository;
private readonly ISqlSugarRepository<ProjectGroup> _groupRepository;
2026-02-08 18:09:05 +08:00
private readonly ISqlSugarRepository<EventRegistrationPerson> _personRepository;
private readonly ISqlSugarRepository<EventRegistration> _registrationRepository;
2026-03-31 15:54:27 +08:00
private readonly ISqlSugarRepository<EventInfo> _eventInfoRepository;
2026-02-08 18:09:05 +08:00
public EventArrangementService(
2026-03-18 16:25:49 +08:00
ISqlSugarRepository<EventProject> eventProjectRepository,
2026-03-01 22:39:00 +08:00
ISqlSugarRepository<YD_Event.Core.Entities.Project> projectRepository,
ISqlSugarRepository<ProjectGroup> groupRepository,
2026-02-08 18:09:05 +08:00
ISqlSugarRepository<EventRegistrationPerson> personRepository,
2026-03-01 22:39:00 +08:00
ISqlSugarRepository<EventRegistration> registrationRepository,
2026-03-31 15:54:27 +08:00
ISqlSugarRepository<EventInfo> eventInfoRepository) : base(projectRepository)
2026-02-08 18:09:05 +08:00
{
2026-03-18 16:25:49 +08:00
_eventProjectRepository = eventProjectRepository;
2026-02-08 18:09:05 +08:00
_projectRepository = projectRepository;
_groupRepository = groupRepository;
_personRepository = personRepository;
_registrationRepository = registrationRepository;
2026-03-31 15:54:27 +08:00
_eventInfoRepository = eventInfoRepository;
2026-02-08 18:09:05 +08:00
}
2026-04-14 23:34:16 +08:00
// 格式化时长显示(符号格式: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;
}
}
2026-02-08 18:09:05 +08:00
/// <summary>
/// 获取编排项目列表
/// </summary>
[DisplayName("获取编排项目列表")]
public async Task<List<ArrangementProjectListOutput>> GetList(long eventId)
{
2026-03-18 16:25:49 +08:00
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));
2026-04-15 09:58:05 +08:00
// 按照 SortIndex 对 projects 排序
var sortedProjects = projects.OrderBy(x => x.SortIndex).ToList();
2026-02-08 18:09:05 +08:00
2026-03-18 16:25:49 +08:00
var groups = await _groupRepository.GetListAsync(x => projectIds.Contains(x.ProjectId));
2026-03-01 22:39:00 +08:00
2026-04-07 09:04:38 +08:00
// 统计各组别报名人数(统计所有非取消状态的报名,并且只统计运动员
2026-02-08 18:09:05 +08:00
var counts = await _personRepository.AsQueryable()
.InnerJoin<EventRegistration>((p, r) => p.RegistrationId == r.Id)
2026-04-07 09:04:38 +08:00
.Where((p, r) => r.EventId == eventId
2026-04-09 23:12:51 +08:00
&& r.PaymentStatus != 4 // 排除已取消的报名
2026-04-07 09:04:38 +08:00
&& !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) })
2026-02-08 18:09:05 +08:00
.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();
2026-02-08 18:09:05 +08:00
var result = new List<ArrangementProjectListOutput>();
2026-04-15 09:58:05 +08:00
// 按照排序后的项目顺序处理
foreach (var proj in sortedProjects)
2026-02-08 18:09:05 +08:00
{
2026-04-15 09:58:05 +08:00
var eventProj = eventProjects.FirstOrDefault(x => x.ProjectId == proj.Id);
if (eventProj == null) continue;
2026-03-18 16:25:49 +08:00
2026-02-08 18:09:05 +08:00
var dto = new ArrangementProjectListOutput
{
2026-03-18 16:25:49 +08:00
ProjectId = eventProj.Id,
2026-03-01 22:39:00 +08:00
ProjectName = proj.Name,
2026-04-12 16:11:20 +08:00
ProjectType = proj.ProjectType.Description(),
2026-02-08 18:09:05 +08:00
DurationSeconds = proj.DurationSeconds,
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)
{
2026-03-18 16:25:49 +08:00
var countObj = counts.FirstOrDefault(c => c.ProjectId == eventProj.Id && c.GroupName == group.GroupName);
2026-02-08 18:09:05 +08:00
dto.Groups.Add(new ArrangementGroupDto
{
GroupName = group.GroupName,
ParticipantCount = countObj?.Count ?? 0
});
}
// 根据项目类型计算总时长
2026-02-08 18:09:05 +08:00
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.DurationSeconds;
}
else
{
// 个人项目:(报名人数 ÷ 场地数) × 项目时长
int participantCount = totalParticipants > 0 ? totalParticipants : 1;
int rounds = (int)Math.Ceiling((double)participantCount / venueCount);
totalDurationSeconds = rounds * proj.DurationSeconds;
}
2026-04-14 17:30:20 +08:00
// 设置总时长秒数和分钟数
dto.TotalDurationSeconds = totalDurationSeconds;
dto.TotalDurationMinutes = (int)Math.Ceiling((double)totalDurationSeconds / 60);
2026-02-08 18:09:05 +08:00
result.Add(dto);
}
return result;
}
/// <summary>
/// 更新编排设置
/// </summary>
[UnitOfWork]
[DisplayName("更新编排设置")]
public async Task Update(ArrangementUpdateInput input)
{
2026-03-18 16:25:49 +08:00
var eventProjectIds = input.Settings.Select(x => x.ProjectId).ToList();
var eventProjects = await _eventProjectRepository.GetListAsync(x => eventProjectIds.Contains(x.Id));
2026-02-08 18:09:05 +08:00
foreach (var setting in input.Settings)
{
2026-03-18 16:25:49 +08:00
var ep = eventProjects.FirstOrDefault(x => x.Id == setting.ProjectId);
if (ep != null)
{
await _projectRepository.UpdateAsync(x => new YD_Event.Core.Entities.Project
{
VenueCount = setting.VenueCount,
SortIndex = setting.SortIndex,
DurationSeconds = setting.DurationSeconds,
DurationMinutes = setting.DurationSeconds / 60
}, x => x.Id == ep.ProjectId);
}
2026-02-08 18:09:05 +08:00
}
}
/// <summary>
/// 编排预览
/// </summary>
[DisplayName("编排预览")]
public async Task<List<ArrangementPreviewOutput>> Preview(long eventId)
{
2026-04-14 23:34:16 +08:00
// 1. 获取项目列表和统计数据
2026-03-18 16:25:49 +08:00
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));
2026-04-15 09:58:05 +08:00
// 按照 SortIndex 对 projects 排序
var sortedProjects = projects.OrderBy(x => x.SortIndex).ToList();
2026-04-14 23:34:16 +08:00
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();
2026-02-08 18:09:05 +08:00
var result = new List<ArrangementPreviewOutput>();
2026-04-15 09:58:05 +08:00
// 2. 生成排期 - 按照排序后的项目顺序
foreach (var proj in sortedProjects)
2026-02-08 18:09:05 +08:00
{
2026-04-15 09:58:05 +08:00
var eventProj = eventProjects.FirstOrDefault(x => x.ProjectId == proj.Id);
if (eventProj == null) continue;
2026-03-18 16:25:49 +08:00
2026-02-08 18:09:05 +08:00
int venueCount = proj.VenueCount > 0 ? proj.VenueCount : 1;
2026-04-14 23:34:16 +08:00
// 获取参赛人员
2026-02-08 18:09:05 +08:00
var persons = await _personRepository.AsQueryable()
.InnerJoin<EventRegistration>((p, r) => p.RegistrationId == r.Id)
2026-04-07 09:04:38 +08:00
.Where((p, r) => p.ProjectId == eventProj.Id
2026-04-09 23:12:51 +08:00
&& r.PaymentStatus != 4
2026-04-07 09:04:38 +08:00
&& !p.DeleteMark
&& p.PersonType == PersonType.Athlete)
2026-02-08 18:09:05 +08:00
.OrderBy(p => p.GroupName)
.OrderBy(p => p.Id)
.Select((p, r) => new {
p.Name, p.GroupName, r.TeamName,
Duration = proj.DurationSeconds
})
.ToListAsync();
if (!persons.Any()) continue;
2026-04-14 23:34:16 +08:00
// 计算项目总时长
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.DurationSeconds;
}
else
{
int participantCount = totalParticipants > 0 ? totalParticipants : 1;
int rounds = (int)Math.Ceiling((double)participantCount / venueCount);
projectTotalDurationSeconds = rounds * proj.DurationSeconds;
}
string projectTotalDuration = FormatDurationSymbol(projectTotalDurationSeconds);
2026-02-08 18:09:05 +08:00
// 分组逻辑
int currentRound = 1;
int currentVenue = 1;
foreach (var person in persons)
{
result.Add(new ArrangementPreviewOutput
{
TimeRange = $"{proj.DurationSeconds}''",
2026-03-01 22:39:00 +08:00
ProjectName = proj.Name,
2026-04-13 05:20:09 +08:00
ProjectType = proj.ProjectType.Description(),
2026-02-08 18:09:05 +08:00
GroupName = person.GroupName,
TeamName = person.TeamName,
AthleteName = person.Name,
SessionIndex = currentRound,
2026-04-14 23:34:16 +08:00
VenueIndex = currentVenue,
ProjectTotalDurationSeconds = projectTotalDurationSeconds,
ProjectTotalDuration = projectTotalDuration
2026-02-08 18:09:05 +08:00
});
currentVenue++;
if (currentVenue > venueCount)
{
currentVenue = 1;
currentRound++;
}
}
}
return result;
}
/// <summary>
/// 导出编排
/// </summary>
[DisplayName("导出编排")]
public async Task<byte[]> Export(long eventId)
{
2026-03-31 15:54:27 +08:00
var eventInfo = await _eventInfoRepository.GetFirstAsync(x => x.Id == eventId);
2026-02-08 18:09:05 +08:00
var data = await Preview(eventId);
2026-03-31 15:54:27 +08:00
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)
2026-03-31 15:54:27 +08:00
{
_data = data ?? new List<ArrangementPreviewOutput>();
_eventInfo = eventInfo;
2026-03-31 15:54:27 +08:00
}
public override async Task<byte[]> ExportAsync()
2026-03-31 15:54:27 +08:00
{
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);
2026-04-14 23:34:16 +08:00
// 按项目分组处理,合并时间列
string currentProjectName = null;
int projectStartRow = 0;
for (int i = 0; i < _data.Count; i++)
2026-03-31 15:54:27 +08:00
{
var item = _data[i];
int rowIndex = i + 3;
2026-04-14 23:34:16 +08:00
// 如果是新项目或最后一行,合并前面的项目时间列
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);
2026-03-31 15:54:27 +08:00
}
2026-04-14 23:34:16 +08:00
// 处理最后一个项目的合并
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();
2026-03-31 15:54:27 +08:00
}
2026-02-08 18:09:05 +08:00
}
}