using YD_Event.Application.Event.Dtos; using YD_Event.Application.Auth; using YD_Event.Core.Entities; using YD_Event.Core.Enum; using YD_Event.Core.Domain; using Microsoft.AspNetCore.Mvc; using OfficeOpenXml; using OfficeOpenXml.Style; using System.Drawing; namespace YD_Event.Application.Event; /// /// 报名管理服务 /// public class EventRegistrationService : BaseService, ITransient { private readonly ISqlSugarRepository _repository; private readonly ISqlSugarRepository _itemRepository; private readonly ISqlSugarRepository _personRepository; private readonly ISqlSugarRepository _paymentRepository; private readonly ISqlSugarRepository _orderRepository; private readonly ISqlSugarRepository _teamMemberRepository; private readonly ISqlSugarRepository _appUserRepository; private readonly ISqlSugarRepository _merchantCertificationRepository; private readonly ISqlSugarRepository _eventInfoRepository; private readonly ISqlSugarRepository _eventProjectRepository; private readonly ISqlSugarRepository _projectRepository; private readonly IIdGenerator _idGenerator; private readonly OrderNoGeneratorService _orderNoGeneratorService; private readonly AuthManager _authManager; public EventRegistrationService( ISqlSugarRepository repository, ISqlSugarRepository itemRepository, ISqlSugarRepository personRepository, ISqlSugarRepository paymentRepository, ISqlSugarRepository orderRepository, ISqlSugarRepository teamMemberRepository, ISqlSugarRepository appUserRepository, ISqlSugarRepository merchantCertificationRepository, ISqlSugarRepository eventInfoRepository, ISqlSugarRepository eventProjectRepository, ISqlSugarRepository projectRepository, IIdGenerator idGenerator, OrderNoGeneratorService orderNoGeneratorService, AuthManager authManager) : base(repository) { _repository = repository; _itemRepository = itemRepository; _personRepository = personRepository; _paymentRepository = paymentRepository; _orderRepository = orderRepository; _teamMemberRepository = teamMemberRepository; _appUserRepository = appUserRepository; _merchantCertificationRepository = merchantCertificationRepository; _eventInfoRepository = eventInfoRepository; _eventProjectRepository = eventProjectRepository; _projectRepository = projectRepository; _idGenerator = idGenerator; _orderNoGeneratorService = orderNoGeneratorService; _authManager = authManager; } /// /// 分页查询报名 /// [DisplayName("分页查询报名")] public async Task> Page(RegistrationQueryInput input) { var query = _repository.AsQueryable() // 排除软删除的报名记录 .Where(x => !x.DeleteMark) // 线上支付只显示已支付(1),线下支付显示待审核(0)和已支付(1) .Where(x => (x.PaymentMethod == 2 && x.PaymentStatus == 1) || (x.PaymentMethod == 1 && (x.PaymentStatus == 0 || x.PaymentStatus == 1))) .WhereIF(input.EventId.HasValue, x => x.EventId == input.EventId) .WhereIF(input.RegistrationMode.HasValue, x => x.RegistrationMode == input.RegistrationMode) .WhereIF(!string.IsNullOrWhiteSpace(input.UnitName), x => x.TeamName.Contains(input.UnitName)) .WhereIF(!string.IsNullOrWhiteSpace(input.UserName), x => x.ContactName.Contains(input.UserName)) .WhereIF(!string.IsNullOrWhiteSpace(input.ContactPhone), x => x.ContactPhone.Contains(input.ContactPhone)) .WhereIF(input.IsUploadMaterial.HasValue, x => (input.IsUploadMaterial.Value ? !string.IsNullOrEmpty(x.MaterialUrl) : string.IsNullOrEmpty(x.MaterialUrl))) .WhereIF(input.PaymentMethod.HasValue, x => x.PaymentMethod == input.PaymentMethod) .WhereIF(input.PaymentStatus.HasValue, x => x.PaymentStatus == input.PaymentStatus) .WhereIF(input.RegistrationTimeStart.HasValue, x => x.RegistrationTime >= input.RegistrationTimeStart) .WhereIF(input.RegistrationTimeEnd.HasValue, x => x.RegistrationTime <= input.RegistrationTimeEnd) .OrderByDescending(x => x.RegistrationTime) .Select(x => new RegistrationListOutput { Id = x.Id, RegistrationMode = x.RegistrationMode, CreatedUserId = x.CreatedUserId, // 保存创建用户ID以便后续查询单位名称 MerchantCertificationId = x.MerchantCertificationId, // 保存单位认证ID以便后续查询单位名称 UnitName = null, // 不初始化为 TeamName,只从单位认证信息中获取 ContactName = x.ContactName, ContactPhone = x.ContactPhone, TotalPersonCount = x.TotalPersonCount, IsUploadMaterial = !string.IsNullOrEmpty(x.MaterialUrl), PaymentMethod = x.PaymentMethod, PaymentMethodType = x.PaymentMethodType, OrderNo = x.OrderNo, TotalAmount = x.TotalAmount, PaymentStatus = x.PaymentStatus, PaymentProofUrl = x.PaymentProofUrl, RegistrationTime = x.RegistrationTime }); var result = await query.ToPagedListAsync(input); // 获取所有报名记录的 MerchantCertificationId var regIds = result.Rows.Select(x => x.Id).ToList(); var merchantCertIds = result.Rows .Where(r => r.MerchantCertificationId.HasValue) .Select(r => r.MerchantCertificationId.Value) .Distinct() .ToList(); // 获取单位认证信息 Dictionary merchantNameDict = new Dictionary(); if (merchantCertIds.Any()) { var merchantCerts = await _merchantCertificationRepository.AsQueryable() .Where(mc => merchantCertIds.Contains(mc.Id) && !mc.DeleteMark) .ToListAsync(); merchantNameDict = merchantCerts.ToDictionary(mc => mc.Id, mc => mc.MerchantName); } // 填充下拉详情 if (result.Rows.Any()) { var items = await _itemRepository.GetListAsync(x => regIds.Contains(x.RegistrationId) && !x.DeleteMark); // 使用关联查询获取报名人员及其关联的用户信息 var persons = await _personRepository.AsQueryable() .Includes(p => p.User) // 关联查询用户信息 .Where(x => regIds.Contains(x.RegistrationId) && !x.DeleteMark) .ToListAsync(); // 获取所有赛事项目和项目信息,用于判断项目类型 var eventProjectIds = items.Select(x => x.ProjectId).Distinct().ToList(); var eventProjects = await _eventProjectRepository.GetListAsync(x => eventProjectIds.Contains(x.Id)); var projectIds = eventProjects.Select(x => x.ProjectId).Distinct().ToList(); var projects = await _projectRepository.GetListAsync(x => projectIds.Contains(x.Id)); // 创建映射字典 var eventProjectToProjectDict = eventProjects.ToDictionary(x => x.Id, x => x.ProjectId); Dictionary projectDict = projects.ToDictionary(x => x.Id, x => x); // 获取所有报名人员的 UserId var allAthleteUserIds = persons .Where(p => p.PersonType == YD_Event.Core.Enum.PersonType.Athlete && p.UserId > 0) .Select(p => p.UserId) .Distinct() .ToList(); // 步骤1:通过运动员的 UserId 找到对应的 TeamId var teamIds = new List(); if (allAthleteUserIds.Any()) { teamIds = await _teamMemberRepository.AsQueryable() .Where(tm => allAthleteUserIds.Contains(tm.UserId) && !tm.DeleteMark) .Select(tm => tm.TeamId) .Distinct() .ToListAsync(); } // 步骤2:通过 TeamId 获取同团队的教练/领队的 UserId,并获取用户详情 Dictionary> teamLeadersDict = new(); Dictionary> teamCoachesDict = new(); if (teamIds.Any()) { var teamMembers = await _teamMemberRepository.AsQueryable() .Where(tm => teamIds.Contains(tm.TeamId) && !tm.DeleteMark) .ToListAsync(); var coachLeaderUserIds = teamMembers .Where(tm => tm.PersonType == YD_Event.Core.Enum.PersonType.Leader || tm.PersonType == YD_Event.Core.Enum.PersonType.Coach) .Select(tm => tm.UserId) .Distinct() .ToList(); if (coachLeaderUserIds.Any()) { var coachLeaderUsers = await _appUserRepository.AsQueryable() .Where(u => coachLeaderUserIds.Contains(u.Id) && !u.DeleteMark) .ToListAsync(); var userDict = coachLeaderUsers.ToDictionary(u => u.Id); // 按团队分组 foreach (var teamId in teamIds) { var teamLeaders = teamMembers .Where(tm => tm.TeamId == teamId && tm.PersonType == YD_Event.Core.Enum.PersonType.Leader) .Select(tm => userDict.TryGetValue(tm.UserId, out var user) ? user : null) .Where(u => u != null) .Cast() .ToList(); var teamCoaches = teamMembers .Where(tm => tm.TeamId == teamId && tm.PersonType == YD_Event.Core.Enum.PersonType.Coach) .Select(tm => userDict.TryGetValue(tm.UserId, out var user) ? user : null) .Where(u => u != null) .Cast() .ToList(); if (teamLeaders.Any()) teamLeadersDict[teamId] = teamLeaders; if (teamCoaches.Any()) teamCoachesDict[teamId] = teamCoaches; } } } foreach (var dto in result.Rows) { // 获取当前报名的所有人员 var regPersons = persons.Where(p => p.RegistrationId == dto.Id).ToList(); // 计算去重后的人数(按UserId去重) var uniqueUserIds = regPersons .Where(p => p.PersonType == PersonType.Athlete && p.UserId > 0) .Select(p => p.UserId) .Distinct() .ToList(); // 更新TotalPersonCount为去重后的人数 dto.TotalPersonCount = uniqueUserIds.Count > 0 ? uniqueUserIds.Count : regPersons.Count(p => p.PersonType == PersonType.Athlete); // 根据报名记录的 MerchantCertificationId 获取单位名称 if (dto.MerchantCertificationId.HasValue && merchantNameDict.TryGetValue(dto.MerchantCertificationId.Value, out var unitName)) { dto.UnitName = unitName; } else { dto.UnitName = null; } var myItems = items.Where(x => x.RegistrationId == dto.Id).ToList(); dto.Items = new List(); foreach (var item in myItems) { var myPersons = persons.Where(p => p.RegistrationItemId == item.Id).ToList(); // 判断项目类型 bool isIndividualProject = false; if (eventProjectToProjectDict.TryGetValue(item.ProjectId, out var projectId) && projectDict.TryGetValue(projectId, out var project)) { isIndividualProject = YD_Event.Core.Extensions.ProjectTypeExtensions.IsIndividualProject(project); } if (isIndividualProject) { // 个人项目:为每个运动员创建一个条目,使用 EventRegistrationPerson.Id foreach (var person in myPersons.Where(p => p.PersonType == PersonType.Athlete)) { // 构造参与人信息字符串 var genderText = person.Gender switch { YD_Event.Core.Enum.Gender.Male => "男", YD_Event.Core.Enum.Gender.Female => "女", _ => "未知" }; var mobile = person.User?.Mobile ?? person.Phone ?? ""; var personInfo = $"{person.Name}/{genderText}/{person.Age}/{mobile}"; var detail = new RegistrationItemDetailDto { Id = person.Id, // 使用 EventRegistrationPerson.Id ProjectName = item.ProjectName, GroupName = item.GroupName, ParticipantInfo = personInfo }; dto.Items.Add(detail); } } else { // 团队项目:保持原有逻辑,使用 EventRegistrationItem.Id // 构造参与人信息字符串,统一格式为:姓名/性别/年龄/手机号 var personInfo = string.Join(";", myPersons.Select(p => { // 将性别转换为中文显示 var genderText = p.Gender switch { YD_Event.Core.Enum.Gender.Male => "男", YD_Event.Core.Enum.Gender.Female => "女", _ => "未知" }; // 获取手机号,优先使用关联用户信息,否则使用自身的 Phone 字段 var mobile = p.User?.Mobile ?? p.Phone ?? ""; // 统一格式:姓名/性别/年龄/手机号 return $"{p.Name}/{genderText}/{p.Age}/{mobile}"; })); if (personInfo.Length > 100) personInfo = personInfo.Substring(0, 97) + "..."; // 截断 var detail = new RegistrationItemDetailDto { Id = item.Id, // 使用 EventRegistrationItem.Id ProjectName = item.ProjectName, GroupName = item.GroupName, ParticipantInfo = personInfo }; dto.Items.Add(detail); } } // 设置领队信息和教练信息 - 从团队中获取 var regAthleteUserIds = regPersons .Where(p => p.PersonType == YD_Event.Core.Enum.PersonType.Athlete && p.UserId > 0) .Select(p => p.UserId) .Distinct() .ToList(); // 获取这些运动员所属的团队 var regTeamIds = new List(); if (regAthleteUserIds.Any()) { regTeamIds = await _teamMemberRepository.AsQueryable() .Where(tm => regAthleteUserIds.Contains(tm.UserId) && !tm.DeleteMark) .Select(tm => tm.TeamId) .Distinct() .ToListAsync(); } // 收集所有相关团队的领队和教练 var allLeaders = new List(); var allCoaches = new List(); foreach (var teamId in regTeamIds) { if (teamLeadersDict.TryGetValue(teamId, out var leaders)) allLeaders.AddRange(leaders); if (teamCoachesDict.TryGetValue(teamId, out var coaches)) allCoaches.AddRange(coaches); } // 去重 allLeaders = allLeaders.GroupBy(l => l.Id).Select(g => g.First()).ToList(); allCoaches = allCoaches.GroupBy(c => c.Id).Select(g => g.First()).ToList(); // 如果从团队中没有找到,再回退到从报名人员中查找 if (!allLeaders.Any()) { var regLeaders = regPersons.Where(p => p.PersonType == YD_Event.Core.Enum.PersonType.Leader).ToList(); allLeaders = regLeaders.Select(l => new YD_Event.Core.Entities.AppUser { Name = l.Name, Mobile = l.Phone, Gender = l.Gender }).ToList(); } if (!allCoaches.Any()) { var regCoaches = regPersons.Where(p => p.PersonType == YD_Event.Core.Enum.PersonType.Coach).ToList(); allCoaches = regCoaches.Select(c => new YD_Event.Core.Entities.AppUser { Name = c.Name, Mobile = c.Phone, Gender = c.Gender }).ToList(); } // 辅助函数:获取性别文本 Func getGenderText = (gender) => { return gender switch { YD_Event.Core.Enum.Gender.Male => "男", YD_Event.Core.Enum.Gender.Female => "女", _ => "未知" }; }; dto.TeamLeaderInfo = allLeaders.Any() ? string.Join(",", allLeaders.Select(l => $"{l.Name}/{getGenderText(l.Gender)}/{l.Mobile}")) : null; dto.CoachInfo = allCoaches.Any() ? string.Join(",", allCoaches.Select(c => $"{c.Name}/{getGenderText(c.Gender)}/{c.Mobile}")) : null; } } return result; } /// /// 为报名记录创建订单(如果不存在) /// private async Task CreateOrderIfNotExistsAsync(EventRegistration registration, DateTime now) { // 检查是否已存在订单 var existingOrder = await _orderRepository.GetFirstAsync(o => o.RegistrationId == registration.Id && !o.DeleteMark); if (existingOrder == null) { // 不存在订单,先检查是否有订单号 if (string.IsNullOrEmpty(registration.OrderNo)) { registration.OrderNo = await _orderNoGeneratorService.GenerateOrderNoAsync(); await _repository.UpdateAsync(registration); } // 获取赛事信息 var eventInfo = await _eventInfoRepository.GetByIdAsync(registration.EventId); // 获取用户单位认证信息 var certification = await _merchantCertificationRepository.GetFirstAsync(c => c.UserId == registration.CreatedUserId && c.AuditStatus == AuditStatus.Pass && !c.DeleteMark); // 创建订单 var order = new YD_Event.Core.Entities.Order { Id = _idGenerator.NewLong(), OrderNo = registration.OrderNo, OrderType = 1, MerchantId = eventInfo?.MerchantId, MerchantCertificationId = certification?.Id, EventId = registration.EventId, RegistrationId = registration.Id, OriginalAmount = registration.TotalAmount, ActualAmount = registration.TotalAmount, DiscountAmount = 0, CouponId = null, OrderStatus = OrderStatus.Paid, PaymentTime = now, CreatedTime = now, CreatedUserId = _authManager.UserId, UpdatedTime = now, UpdatedUserId = _authManager.UserId, Status = AvailabilityStatus.Enable }; await _orderRepository.InsertAsync(order); } else { // 订单已存在,只更新状态为已支付 if (existingOrder.OrderStatus != OrderStatus.Paid) { existingOrder.OrderStatus = OrderStatus.Paid; existingOrder.PaymentTime = now; existingOrder.UpdatedTime = now; existingOrder.UpdatedUserId = _authManager.UserId; await _orderRepository.UpdateAsync(existingOrder); } } } /// /// 批量审核 /// [UnitOfWork] [DisplayName("批量审核")] public async Task BatchAudit(BatchAuditInput input) { if (input.Ids == null || !input.Ids.Any()) return; var now = DateTime.Now; // 如果审核通过(PaymentStatus = 1),先处理订单创建和更新 if (input.PaymentStatus == 1) { // 获取需要更新的报名记录 var registrations = await _repository.GetListAsync(x => input.Ids.Contains(x.Id)); foreach (var registration in registrations) { // 确保订单存在 await CreateOrderIfNotExistsAsync(registration, now); // 更新报名记录的支付状态 registration.PaymentStatus = input.PaymentStatus; registration.ReviewRemark = input.ReviewRemark; registration.ReviewTime = now; registration.PaymentTime = now; registration.UpdatedTime = now; registration.UpdatedUserId = _authManager.UserId; await _repository.UpdateAsync(registration); } } else { // 审核不通过,只更新状态 await _repository.UpdateAsync(x => new EventRegistration { PaymentStatus = input.PaymentStatus, ReviewRemark = input.ReviewRemark, ReviewTime = now }, x => input.Ids.Contains(x.Id)); } } /// /// 导出报名信息 /// [DisplayName("导出报名")] public async Task Export(RegistrationQueryInput input) { var registrations = await _repository.AsQueryable() .Where(x => !x.DeleteMark) .Where(x => (x.PaymentMethod == 2 && x.PaymentStatus == 1) || (x.PaymentMethod == 1 && (x.PaymentStatus == 0 || x.PaymentStatus == 1))) .WhereIF(input.EventId.HasValue, x => x.EventId == input.EventId) .WhereIF(input.RegistrationMode.HasValue, x => x.RegistrationMode == input.RegistrationMode) .WhereIF(!string.IsNullOrWhiteSpace(input.UnitName), x => x.TeamName.Contains(input.UnitName)) .WhereIF(!string.IsNullOrWhiteSpace(input.UserName), x => x.ContactName.Contains(input.UserName)) .WhereIF(!string.IsNullOrWhiteSpace(input.ContactPhone), x => x.ContactPhone.Contains(input.ContactPhone)) .WhereIF(input.PaymentMethod.HasValue, x => x.PaymentMethod == input.PaymentMethod) .WhereIF(input.PaymentStatus.HasValue, x => x.PaymentStatus == input.PaymentStatus) .WhereIF(input.RegistrationTimeStart.HasValue, x => x.RegistrationTime >= input.RegistrationTimeStart) .WhereIF(input.RegistrationTimeEnd.HasValue, x => x.RegistrationTime <= input.RegistrationTimeEnd) .OrderBy(x => x.RegistrationMode, OrderByType.Asc) .OrderBy(x => x.RegistrationTime, OrderByType.Desc) .ToListAsync(); if (!registrations.Any()) { throw Oops.Bah("暂无数据可导出"); } var regIds = registrations.Select(r => r.Id).ToList(); var merchantCertIds = registrations .Where(r => r.MerchantCertificationId.HasValue) .Select(r => r.MerchantCertificationId.Value) .Distinct() .ToList(); Dictionary merchantNameDict = new Dictionary(); if (merchantCertIds.Any()) { var merchantCerts = await _merchantCertificationRepository.AsQueryable() .Where(mc => merchantCertIds.Contains(mc.Id) && !mc.DeleteMark) .ToListAsync(); merchantNameDict = merchantCerts.ToDictionary(mc => mc.Id, mc => mc.MerchantName); } var allPersons = await _personRepository.AsQueryable() .Includes(p => p.User) .Where(p => regIds.Contains(p.RegistrationId) && !p.DeleteMark) .ToListAsync(); var athleteUserIds = allPersons .Where(p => p.PersonType == PersonType.Athlete && p.UserId > 0) .Select(p => p.UserId) .Distinct() .ToList(); var allTeamMembers = new List(); Dictionary> teamLeadersDict = new(); Dictionary> teamCoachesDict = new(); if (athleteUserIds.Any()) { allTeamMembers = await _teamMemberRepository.AsQueryable() .Where(tm => athleteUserIds.Contains(tm.UserId) && !tm.DeleteMark) .ToListAsync(); var allTeamIds = allTeamMembers.Select(tm => tm.TeamId).Distinct().ToList(); if (allTeamIds.Any()) { var fullTeamMembers = await _teamMemberRepository.AsQueryable() .Where(tm => allTeamIds.Contains(tm.TeamId) && !tm.DeleteMark) .ToListAsync(); var coachLeaderUserIds = fullTeamMembers .Where(tm => tm.PersonType == PersonType.Leader || tm.PersonType == PersonType.Coach) .Select(tm => tm.UserId) .Distinct() .ToList(); if (coachLeaderUserIds.Any()) { var coachLeaderUsers = await _appUserRepository.AsQueryable() .Where(u => coachLeaderUserIds.Contains(u.Id) && !u.DeleteMark) .ToListAsync(); var userDict = coachLeaderUsers.ToDictionary(u => u.Id); foreach (var teamId in allTeamIds) { var teamLeaders = fullTeamMembers .Where(tm => tm.TeamId == teamId && tm.PersonType == PersonType.Leader) .Select(tm => userDict.TryGetValue(tm.UserId, out var user) ? user : null) .Where(u => u != null) .Cast() .ToList(); var teamCoaches = fullTeamMembers .Where(tm => tm.TeamId == teamId && tm.PersonType == PersonType.Coach) .Select(tm => userDict.TryGetValue(tm.UserId, out var user) ? user : null) .Where(u => u != null) .Cast() .ToList(); if (teamLeaders.Any()) teamLeadersDict[teamId] = teamLeaders; if (teamCoaches.Any()) teamCoachesDict[teamId] = teamCoaches; } } } } var userToTeamIdsDict = allTeamMembers .GroupBy(tm => tm.UserId) .ToDictionary(g => g.Key, g => g.Select(tm => tm.TeamId).Distinct().ToList()); ExcelPackage.LicenseContext = OfficeOpenXml.LicenseContext.NonCommercial; using var package = new ExcelPackage(); BuildTeamSheet(package, registrations, allPersons, merchantNameDict, teamLeadersDict, teamCoachesDict, userToTeamIdsDict); BuildPersonSheet(package, registrations, allPersons, merchantNameDict); var bytes = await package.GetAsByteArrayAsync(); return new FileContentResult(bytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") { FileDownloadName = $"报名信息_{DateTime.Now:yyyyMMddHHmmss}.xlsx" }; } private void BuildTeamSheet( ExcelPackage package, List registrations, List allPersons, Dictionary merchantNameDict, Dictionary> teamLeadersDict, Dictionary> teamCoachesDict, Dictionary> userToTeamIdsDict) { var ws = package.Workbook.Worksheets.Add("队伍信息"); string[] headers = { "类型", "参赛单位", "队伍名称/运动员名称", "领队", "联系方式", "教练", "联系方式", "人数", "缴费情况" }; for (int i = 0; i < headers.Length; i++) { ws.Cells[1, i + 1].Value = headers[i]; } var headerRange = ws.Cells[1, 1, 1, headers.Length]; headerRange.Style.Font.Bold = true; headerRange.Style.Font.Size = 14; headerRange.Style.Font.Name = "宋体"; headerRange.Style.HorizontalAlignment = ExcelHorizontalAlignment.Center; headerRange.Style.VerticalAlignment = ExcelVerticalAlignment.Center; headerRange.Style.Fill.PatternType = ExcelFillStyle.Solid; headerRange.Style.Fill.BackgroundColor.SetColor(Color.FromArgb(217, 226, 243)); headerRange.Style.Border.Top.Style = ExcelBorderStyle.Thin; headerRange.Style.Border.Bottom.Style = ExcelBorderStyle.Thin; headerRange.Style.Border.Left.Style = ExcelBorderStyle.Thin; headerRange.Style.Border.Right.Style = ExcelBorderStyle.Thin; ws.Row(1).Height = 30; double[] colWidths = { 13.4, 44.6, 38.5, 23.1, 30.4, 13, 24.9, 15.3, 22.7 }; for (int i = 0; i < colWidths.Length; i++) { ws.Column(i + 1).Width = colWidths[i]; } int row = 2; var personRegGroups = allPersons.GroupBy(p => p.RegistrationId).ToDictionary(g => g.Key, g => g.ToList()); foreach (var reg in registrations) { var regPersons = personRegGroups.TryGetValue(reg.Id, out var rp) ? rp : new List(); var athletes = regPersons.Where(p => p.PersonType == PersonType.Athlete).ToList(); bool isPersonal = reg.RegistrationMode == 1; string type = isPersonal ? "个人" : "单位"; string unitName = "/"; string teamName = reg.TeamName ?? ""; string leaderName = reg.ContactName ?? ""; string leaderPhone = reg.ContactPhone ?? ""; string coachNames = "/"; string coachPhones = "/"; int personCount = 1; string paymentStatus = reg.PaymentStatus == 1 ? "已支付" : "未支付"; if (isPersonal) { var athlete = athletes.FirstOrDefault(); if (athlete != null) { teamName = athlete.Name ?? reg.TeamName ?? ""; leaderName = athlete.Name ?? reg.ContactName ?? ""; leaderPhone = athlete.User?.Mobile ?? athlete.Phone ?? reg.ContactPhone ?? ""; } personCount = 1; } else { if (reg.MerchantCertificationId.HasValue && merchantNameDict.TryGetValue(reg.MerchantCertificationId.Value, out var un)) { unitName = un; } var regAthleteUserIds = athletes.Where(p => p.UserId > 0).Select(p => p.UserId).Distinct().ToList(); var regTeamIds = new HashSet(); foreach (var uid in regAthleteUserIds) { if (userToTeamIdsDict.TryGetValue(uid, out var tids)) { foreach (var tid in tids) regTeamIds.Add(tid); } } var allLeaders = new List(); var allCoaches = new List(); foreach (var tid in regTeamIds) { if (teamLeadersDict.TryGetValue(tid, out var leaders)) allLeaders.AddRange(leaders); if (teamCoachesDict.TryGetValue(tid, out var coaches)) allCoaches.AddRange(coaches); } allLeaders = allLeaders.GroupBy(l => l.Id).Select(g => g.First()).ToList(); allCoaches = allCoaches.GroupBy(c => c.Id).Select(g => g.First()).ToList(); var regLeaders = regPersons.Where(p => p.PersonType == PersonType.Leader).ToList(); if (!allLeaders.Any() && regLeaders.Any()) { allLeaders = regLeaders.Select(l => new YD_Event.Core.Entities.AppUser { Name = l.Name, Mobile = l.Phone }).ToList(); } var regCoaches = regPersons.Where(p => p.PersonType == PersonType.Coach).ToList(); if (!allCoaches.Any() && regCoaches.Any()) { allCoaches = regCoaches.Select(c => new YD_Event.Core.Entities.AppUser { Name = c.Name, Mobile = c.Phone }).ToList(); } if (allLeaders.Any()) { leaderName = string.Join("\n", allLeaders.Select(l => l.Name ?? "").Where(n => !string.IsNullOrEmpty(n))); leaderPhone = string.Join("\n", allLeaders.Select(l => l.Mobile ?? "").Where(m => !string.IsNullOrEmpty(m))); } if (allCoaches.Any()) { coachNames = string.Join("\n", allCoaches.Select(c => c.Name ?? "").Where(n => !string.IsNullOrEmpty(n))); coachPhones = string.Join("\n", allCoaches.Select(c => c.Mobile ?? "").Where(m => !string.IsNullOrEmpty(m))); } var uniqueIds = athletes.Where(p => p.UserId > 0).Select(p => p.UserId).Distinct().Count(); personCount = uniqueIds > 0 ? uniqueIds : athletes.Count; if (personCount == 0) personCount = reg.TotalPersonCount; } ws.Cells[row, 1].Value = type; ws.Cells[row, 2].Value = unitName; ws.Cells[row, 3].Value = teamName; ws.Cells[row, 4].Value = leaderName; ws.Cells[row, 5].Value = leaderPhone; ws.Cells[row, 6].Value = coachNames; ws.Cells[row, 7].Value = coachPhones; ws.Cells[row, 8].Value = personCount; ws.Cells[row, 9].Value = paymentStatus; var dataRange = ws.Cells[row, 1, row, headers.Length]; dataRange.Style.Font.Size = 14; dataRange.Style.Font.Name = "宋体"; dataRange.Style.HorizontalAlignment = ExcelHorizontalAlignment.Center; dataRange.Style.VerticalAlignment = ExcelVerticalAlignment.Center; dataRange.Style.WrapText = true; dataRange.Style.Border.Top.Style = ExcelBorderStyle.Thin; dataRange.Style.Border.Bottom.Style = ExcelBorderStyle.Thin; dataRange.Style.Border.Left.Style = ExcelBorderStyle.Thin; dataRange.Style.Border.Right.Style = ExcelBorderStyle.Thin; ws.Row(row).Height = 30; row++; } } private void BuildPersonSheet( ExcelPackage package, List registrations, List allPersons, Dictionary merchantNameDict) { var ws = package.Workbook.Worksheets.Add("人员信息"); string[] headers = { "类型", "参赛单位", "参赛队伍", "参赛队员", "身份证号", "出生年月日", "性别", "项目", "组别" }; for (int i = 0; i < headers.Length; i++) { ws.Cells[1, i + 1].Value = headers[i]; } var headerRange = ws.Cells[1, 1, 1, headers.Length]; headerRange.Style.Font.Bold = true; headerRange.Style.Font.Size = 14; headerRange.Style.Font.Name = "宋体"; headerRange.Style.HorizontalAlignment = ExcelHorizontalAlignment.Center; headerRange.Style.VerticalAlignment = ExcelVerticalAlignment.Center; headerRange.Style.Fill.PatternType = ExcelFillStyle.Solid; headerRange.Style.Fill.BackgroundColor.SetColor(Color.FromArgb(217, 226, 243)); headerRange.Style.Border.Top.Style = ExcelBorderStyle.Thin; headerRange.Style.Border.Bottom.Style = ExcelBorderStyle.Thin; headerRange.Style.Border.Left.Style = ExcelBorderStyle.Thin; headerRange.Style.Border.Right.Style = ExcelBorderStyle.Thin; ws.Row(1).Height = 30; double[] colWidths = { 13.4, 30.4, 29.7, 26.4, 32.1, 23.1, 13, 56.8, 23.1 }; for (int i = 0; i < colWidths.Length; i++) { ws.Column(i + 1).Width = colWidths[i]; } ws.Column(5).Style.Numberformat.Format = "@"; int row = 2; var personGroups = allPersons .Where(p => p.PersonType == PersonType.Athlete) .GroupBy(p => p.RegistrationId) .ToDictionary(g => g.Key, g => g.ToList()); foreach (var reg in registrations) { bool isPersonal = reg.RegistrationMode == 1; string type = isPersonal ? "个人" : "单位"; string unitName = "/"; if (!isPersonal && reg.MerchantCertificationId.HasValue && merchantNameDict.TryGetValue(reg.MerchantCertificationId.Value, out var un)) { unitName = un; } string teamName = reg.TeamName ?? "/"; if (!personGroups.TryGetValue(reg.Id, out var athletes)) continue; var personProjectGroups = new Dictionary>(); foreach (var athlete in athletes) { string key = athlete.UserId > 0 ? $"u_{athlete.UserId}" : $"c_{(athlete.IdCard ?? athlete.Name ?? "")}_{athlete.Name ?? ""}"; if (!personProjectGroups.ContainsKey(key)) personProjectGroups[key] = new List(); personProjectGroups[key].Add(athlete); } foreach (var kvp in personProjectGroups) { var projects = kvp.Value; var firstPerson = projects.OrderBy(p => p.Id).First(); string certNumber = firstPerson.IdCard ?? ""; string personName = firstPerson.Name ?? ""; bool isIdCard = !string.IsNullOrEmpty(certNumber) && certNumber.Length == 18; for (int projIdx = 0; projIdx < projects.Count; projIdx++) { var projPerson = projects[projIdx]; bool isFirstRow = projIdx == 0; if (isFirstRow) { ws.Cells[row, 1].Value = type; ws.Cells[row, 2].Value = unitName; ws.Cells[row, 3].Value = teamName; ws.Cells[row, 4].Value = personName; ws.Cells[row, 5].Value = certNumber; if (isIdCard) { ws.Cells[row, 6].Formula = $"=DATE(MID(E{row},7,4),MID(E{row},11,2),MID(E{row},13,2))"; ws.Cells[row, 6].Style.Numberformat.Format = "yyyy-MM-dd"; ws.Cells[row, 7].Formula = $"=IF(MOD(MID($E{row},17,1),2),\"男\",\"女\")"; } else { string genderText = firstPerson.Gender switch { Gender.Male => "男", Gender.Female => "女", _ => "" }; ws.Cells[row, 6].Value = firstPerson.BirthDate.ToString("yyyy-MM-dd"); ws.Cells[row, 7].Value = genderText; } } ws.Cells[row, 8].Value = projPerson.ProjectName ?? ""; ws.Cells[row, 9].Value = projPerson.GroupName ?? ""; var dataRange = ws.Cells[row, 1, row, headers.Length]; dataRange.Style.Font.Size = 14; dataRange.Style.Font.Name = "宋体"; dataRange.Style.HorizontalAlignment = ExcelHorizontalAlignment.Center; dataRange.Style.VerticalAlignment = ExcelVerticalAlignment.Center; dataRange.Style.WrapText = true; dataRange.Style.Border.Top.Style = ExcelBorderStyle.Thin; dataRange.Style.Border.Bottom.Style = ExcelBorderStyle.Thin; dataRange.Style.Border.Left.Style = ExcelBorderStyle.Thin; dataRange.Style.Border.Right.Style = ExcelBorderStyle.Thin; ws.Row(row).Height = 35; row++; } } } } /// /// 获取素材下载链接 /// [DisplayName("获取素材下载链接")] public async Task> MaterialUrls(List ids) { if (ids == null || !ids.Any()) return new List(); return await _repository.AsQueryable() .Where(x => ids.Contains(x.Id) && !string.IsNullOrEmpty(x.MaterialUrl)) .Select(x => x.MaterialUrl) .ToListAsync(); } /// /// 单条支付审核 /// [UnitOfWork] [DisplayName("单条支付审核")] public async Task PaymentAudit(PaymentAuditInput input) { var registration = await _repository.GetByIdAsync(input.RegistrationId); if (registration == null) throw Oops.Bah("报名记录不存在"); if (registration.PaymentStatus != 0) { throw Oops.Bah("该记录已完成支付审核,无需重复操作"); } var now = DateTime.Now; registration.PaymentStatus = input.PaymentStatus; registration.ReviewRemark = input.ReviewRemark; registration.ReviewTime = now; registration.Reviewer = _authManager.Account; registration.UpdatedTime = now; registration.UpdatedUserId = _authManager.UserId; if (input.PaymentStatus == 1) { registration.PaymentTime = now; // 确保订单存在 await CreateOrderIfNotExistsAsync(registration, now); } await _repository.UpdateAsync(registration); } /// /// 批量支付审核 /// [UnitOfWork] [DisplayName("批量支付审核")] public async Task BatchPaymentAudit(BatchPaymentAuditInput input) { if (input.Ids == null || !input.Ids.Any()) return; var registrations = await _repository.GetListAsync(x => input.Ids.Contains(x.Id)); if (!registrations.Any()) throw Oops.Bah("未找到有效的报名记录"); var invalidRegistrations = registrations .Where(r => r.PaymentStatus != 0) .ToList(); if (invalidRegistrations.Any()) { throw Oops.Bah("存在非待审核状态的记录,无法进行支付审核"); } var now = DateTime.Now; // 如果审核通过(PaymentStatus = 1),先处理订单创建 if (input.PaymentStatus == 1) { foreach (var registration in registrations) { // 确保订单存在 await CreateOrderIfNotExistsAsync(registration, now); } } // 更新报名记录的状态 await _repository.UpdateAsync(x => new EventRegistration { PaymentStatus = input.PaymentStatus, ReviewRemark = input.ReviewRemark, ReviewTime = now, Reviewer = _authManager.Account, UpdatedTime = now, UpdatedUserId = _authManager.UserId, PaymentTime = input.PaymentStatus == 1 ? now : x.PaymentTime }, x => input.Ids.Contains(x.Id)); } /// /// 获取报名统计数据 /// [DisplayName("获取报名统计数据")] [HttpGet("stats/{eventId}")] public async Task GetStats(long eventId) { var stats = new RegistrationStatsOutput(); // 获取赛事项目信息 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)); var eventProjectToProjectDict = eventProjects.ToDictionary(x => x.Id, x => x.ProjectId); var projectDict = projects.ToDictionary(x => x.Id, x => x); // 统计符合条件的所有运动员(与赛事编排页面保持一致的筛选规则) var athletes = await _personRepository.AsQueryable() .InnerJoin((p, r) => p.RegistrationId == r.Id) .Where((p, r) => r.EventId == eventId && !r.DeleteMark // 排除软删除的报名记录 // 线上支付只显示已支付(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 { PersonId = p.Id, EventProjectId = p.ProjectId, GroupName = p.GroupName, TeamName = r.TeamName, RegistrationItemId = p.RegistrationItemId, UserId = p.UserId, RegistrationId = r.Id, PaymentMethod = r.PaymentMethod, PaymentStatus = r.PaymentStatus }) .ToListAsync(); if (!athletes.Any()) { // 待审核数量:线下支付(1)且支付状态为0(待审核) var pendingRegistrations = await _repository.GetListAsync(x => x.EventId == eventId && !x.DeleteMark && x.PaymentMethod == 1 && x.PaymentStatus == 0); stats.PendingReview = pendingRegistrations.Count; return stats; } // 计算总人数(所有符合条件的运动员) stats.TotalParticipants = athletes.Select(x => x.UserId > 0 ? x.UserId : x.PersonId).Distinct().Count(); // 分别计算个人项目和团队项目的统计数据 var individualSet = new HashSet(); var teamSet = new HashSet<(long EventProjectId, string GroupName, string TeamName, long RegistrationItemId)>(); var teamPersonSet = new HashSet(); foreach (var athlete in athletes) { // 判断项目类型 bool isIndividualProject = false; if (eventProjectToProjectDict.TryGetValue(athlete.EventProjectId, out var projectId) && projectDict.TryGetValue(projectId, out var project)) { isIndividualProject = YD_Event.Core.Extensions.ProjectTypeExtensions.IsIndividualProject(project); } var personKey = athlete.UserId > 0 ? athlete.UserId : athlete.PersonId; if (isIndividualProject) { // 个人项目:统计人数 individualSet.Add(personKey); } else { // 团队项目:统计团队数(与赛事编排一致,按 EventProjectId、GroupName、TeamName、RegistrationItemId 分组) if (!string.IsNullOrEmpty(athlete.TeamName)) { teamSet.Add(( athlete.EventProjectId, athlete.GroupName ?? "", athlete.TeamName, athlete.RegistrationItemId )); } // 团队项目人数 teamPersonSet.Add(personKey); } } stats.IndividualParticipants = individualSet.Count; stats.TeamCount = teamSet.Count; stats.TeamParticipants = teamPersonSet.Count; // 待审核数量:线下支付(1)且支付状态为0(待审核) var pendingRegs = await _repository.GetListAsync(x => x.EventId == eventId && !x.DeleteMark && x.PaymentMethod == 1 && x.PaymentStatus == 0); stats.PendingReview = pendingRegs.Count; return stats; } }