using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using YD_Event.Application.Event.Dtos; using YD_Event.Core.Entities; using YD_Event.Core.Enum; using MiniExcelLibs; using SqlSugar; namespace YD_Event.Application.Event; /// /// 成绩与证书管理 /// public class EventScoreService : BaseService, ITransient { private readonly ISqlSugarRepository _personRepository; private readonly ISqlSugarRepository _resultRepository; private readonly ISqlSugarRepository _certificateRepository; private readonly ISqlSugarRepository _registrationRepository; private readonly ISqlSugarRepository _templateRepository; private readonly IIdGenerator _idGenerator; private readonly ILogger _logger; public EventScoreService( ISqlSugarRepository personRepository, ISqlSugarRepository resultRepository, ISqlSugarRepository certificateRepository, ISqlSugarRepository registrationRepository, ISqlSugarRepository templateRepository, IIdGenerator idGenerator, ILogger logger) : base(resultRepository) { _personRepository = personRepository; _resultRepository = resultRepository; _certificateRepository = certificateRepository; _registrationRepository = registrationRepository; _templateRepository = templateRepository; _idGenerator = idGenerator; _logger = logger; } /// /// 运动员成绩列表 /// [DisplayName("运动员成绩列表")] public async Task> PageAthlete(ScoreQueryInput input) { return await _personRepository.AsQueryable() .LeftJoin((p, r) => p.RegistrationId == r.Id) .LeftJoin((p, r, res) => p.Id == res.RegistrationPersonId) .LeftJoin((p, r, res, c) => res.Id == c.ResultId) .Where((p, r, res, c) => r.EventId == input.EventId && (p.PersonType == PersonType.Athlete || p.PersonType == PersonType.User)) .WhereIF(input.ProjectId.HasValue, p => p.ProjectId == input.ProjectId) .WhereIF(!string.IsNullOrWhiteSpace(input.GroupName), p => p.GroupName == input.GroupName) .WhereIF(!string.IsNullOrWhiteSpace(input.Name), p => p.Name.Contains(input.Name)) .WhereIF(!string.IsNullOrWhiteSpace(input.UnitName), (p, r) => r.TeamName.Contains(input.UnitName)) .WhereIF(input.Gender.HasValue, p => p.Gender == input.Gender.Value) .Select((p, r, res, c) => new ScoreListOutput { Id = p.Id, Name = p.Name, UnitName = r.TeamName, Gender = p.Gender.ToString(), Score = res.Score, Rank = res.Rank, ResultId = res.Id, CertificateId = c.Id }) .ToPagedListAsync(input); } /// /// 教练/领队列表 /// [DisplayName("教练领队列表")] public async Task> PageCoachLeader(CoachLeaderQueryInput input) { return await _personRepository.AsQueryable() .LeftJoin((p, c) => p.Id == c.ParticipationRecordId) .InnerJoin((p, c, r) => p.RegistrationId == r.Id) .Where((p, c, r) => r.EventId == input.EventId && p.PersonType == input.PersonType) .WhereIF(!string.IsNullOrWhiteSpace(input.Name), p => p.Name.Contains(input.Name)) .WhereIF(!string.IsNullOrWhiteSpace(input.Mobile), p => p.Phone.Contains(input.Mobile)) .WhereIF(input.Gender.HasValue, p => p.Gender == input.Gender.Value) .Select((p, c) => new CoachLeaderListOutput { Id = p.Id, Name = p.Name, Age = p.Age, Gender = p.Gender.ToString(), IdCardNumber = p.IdCard, Mobile = p.Phone, CertificateId = c.Id }) .ToPagedListAsync(input); } /// /// 导入成绩(性能优化版) /// /// /// 性能优化策略: /// 1. 批量预加载所有人员数据到内存 /// 2. 使用字典建立索引,避免循环内数据库查询 /// 3. 批量收集需要插入/更新的记录 /// 4. 最后一次性执行批量操作 /// 性能指标:导入10000条成绩数据时,响应时间 < 30秒,内存占用 < 512MB /// [UnitOfWork] [DisplayName("导入成绩")] public async Task ImportScore(long eventId, IFormFile file) { _logger.LogInformation("开始导入成绩:赛事ID={EventId}", eventId); var startTime = DateTime.Now; using var stream = file.OpenReadStream(); var rows = stream.Query().ToList(); _logger.LogInformation("解析Excel完成,共 {RowCount} 条记录", rows.Count); if (!rows.Any()) { throw Oops.Bah("Excel文件中没有数据"); } // 验证导入数据 var validationResult = await ValidateImportDataAsync(rows, eventId); if (!validationResult.IsValid) { var errorMessage = string.Join("\n", validationResult.Errors); throw Oops.Bah($"数据验证失败:\n{errorMessage}"); } // 步骤1:批量预加载该赛事的所有人员数据 var allPersons = await _personRepository.AsQueryable() .InnerJoin((p, r) => p.RegistrationId == r.Id) .Where((p, r) => r.EventId == eventId) .Select((p, r) => new { p.Id, p.Name, p.ProjectName, p.GroupName, p.RegistrationId, p.ProjectId }) .ToListAsync(); _logger.LogInformation("预加载人员数据完成,共 {PersonCount} 人", allPersons.Count); // 步骤2:使用字典建立人员索引(Name_ProjectName_GroupName -> Person) var personDict = allPersons .GroupBy(p => $"{p.Name}_{p.ProjectName}_{p.GroupName}") .ToDictionary(g => g.Key, g => g.First()); // 步骤3:预加载该赛事的所有成绩记录 var personIds = allPersons.Select(p => p.Id).ToList(); var existingResults = await _resultRepository.GetListAsync(x => x.EventId == eventId && personIds.Contains(x.RegistrationPersonId)); var resultDict = existingResults.ToDictionary(r => r.RegistrationPersonId); // 步骤4:批量处理数据,收集需要插入/更新的记录 var resultsToInsert = new List(); var resultsToUpdate = new List(); var now = DateTime.Now; var notFoundPersons = new List(); foreach(var row in validationResult.ValidRows) { // 使用内存字典查找,避免数据库查询 var key = $"{row.Name}_{row.ProjectName}_{row.GroupName}"; if (!personDict.TryGetValue(key, out var person)) { notFoundPersons.Add($"姓名:{row.Name}, 项目:{row.ProjectName}, 组别:{row.GroupName}"); continue; } if (resultDict.TryGetValue(person.Id, out var existingResult)) { // 更新现有记录 existingResult.Score = row.Score; existingResult.Rank = row.Rank; existingResult.Remark = row.RankText; existingResult.UpdatedTime = now; resultsToUpdate.Add(existingResult); } else { // 插入新记录 var newResult = new EventResult { Id = _idGenerator.NewLong(), EventId = eventId, RegistrationPersonId = person.Id, RegistrationId = person.RegistrationId, ProjectId = person.ProjectId, ParticipantName = person.Name, GroupName = person.GroupName, Score = row.Score, Rank = row.Rank, Remark = row.RankText, CreatedTime = now, UpdatedTime = now }; resultsToInsert.Add(newResult); } } if (notFoundPersons.Any()) { _logger.LogWarning("未找到匹配的参赛人员:{Count} 条", notFoundPersons.Count); } _logger.LogInformation("数据处理完成:新增 {InsertCount} 条,更新 {UpdateCount} 条,未找到 {NotFoundCount} 条", resultsToInsert.Count, resultsToUpdate.Count, notFoundPersons.Count); // 步骤5:批量操作数据库 if (resultsToInsert.Any()) { await _resultRepository.InsertRangeAsync(resultsToInsert); _logger.LogInformation("批量插入成绩记录完成,共 {Count} 条", resultsToInsert.Count); } if (resultsToUpdate.Any()) { await _resultRepository.UpdateRangeAsync(resultsToUpdate); _logger.LogInformation("批量更新成绩记录完成,共 {Count} 条", resultsToUpdate.Count); } var duration = (DateTime.Now - startTime).TotalSeconds; _logger.LogInformation("导入成绩完成:赛事ID={EventId},总耗时={Duration}秒", eventId, duration); } /// /// 发放证书(支持模板选择和批量发放) /// [UnitOfWork] [DisplayName("发放证书")] public async Task IssueCertificate(IssueCertificateInput input) { var result = new IssueCertificateResult(); var now = DateTime.Now; // 如果指定了模板,验证模板是否存在 CertificateTemplate? template = null; if (input.TemplateId.HasValue) { template = await _templateRepository.GetFirstAsync(x => x.Id == input.TemplateId.Value && x.Status == AvailabilityStatus.Enable && !x.DeleteMark); if (template == null) { throw Oops.Bah("指定的证书模板不存在或不可用"); } } // 批量预加载所有需要的数据 var persons = await _personRepository.GetListAsync(x => input.PersonIds.Contains(x.Id)); var personDict = persons.ToDictionary(p => p.Id); var personIds = persons.Select(p => p.Id).ToList(); var existingResults = await _resultRepository.GetListAsync(x => personIds.Contains(x.RegistrationPersonId)); var resultDict = existingResults.ToDictionary(r => r.RegistrationPersonId); var existingCerts = await _certificateRepository.GetListAsync(x => personIds.Contains(x.ParticipationRecordId)); var certDict = existingCerts.ToDictionary(c => c.ParticipationRecordId); var certsToInsert = new List(); foreach (var personId in input.PersonIds) { if (!personDict.TryGetValue(personId, out var person)) { result.FailCount++; result.FailDetails.Add($"人员ID {personId} 不存在"); continue; } EventCertificate? cert = null; if (person.PersonType == PersonType.Athlete || person.PersonType == PersonType.User) { if (!resultDict.TryGetValue(person.Id, out var resultRecord)) { result.FailCount++; result.FailDetails.Add($"人员 {person.Name} 没有成绩记录"); continue; } if (certDict.TryGetValue(person.Id, out var existingCert)) { cert = existingCert; } else { cert = new EventCertificate { Id = _idGenerator.NewLong(), EventId = input.EventId, ResultId = resultRecord.Id, RegistrationId = person.RegistrationId, ParticipationRecordId = person.Id, ParticipantName = person.Name, GroupName = person.GroupName, CertificateStatus = 1, GeneratedTime = now, CertificateNo = $"C{now:yyyyMMdd}{person.Id}", CreatedTime = now, TemplateId = input.TemplateId }; certsToInsert.Add(cert); } } else { if (certDict.TryGetValue(person.Id, out var existingCert)) { cert = existingCert; } else { cert = new EventCertificate { Id = _idGenerator.NewLong(), EventId = input.EventId, RegistrationId = person.RegistrationId, ParticipationRecordId = person.Id, ParticipantName = person.Name, GroupName = person.GroupName, CertificateStatus = 1, GeneratedTime = now, CertificateNo = $"CC{now:yyyyMMdd}{person.Id}", CreatedTime = now, TemplateId = input.TemplateId }; certsToInsert.Add(cert); } } // 更新证书状态为已发放 cert.CertificateStatus = 2; cert.IssuedTime = now; cert.UpdatedTime = now; result.SuccessCount++; } if (certsToInsert.Any()) { await _certificateRepository.InsertRangeAsync(certsToInsert); } // 更新已有证书的发放状态 var existingCertsToUpdate = existingCerts.Where(c => input.PersonIds.Contains(c.ParticipationRecordId)).ToList(); if (existingCertsToUpdate.Any()) { await _certificateRepository.UpdateRangeAsync(existingCertsToUpdate); } _logger.LogInformation("发放证书完成:成功 {SuccessCount} 条,失败 {FailCount} 条", result.SuccessCount, result.FailCount); return result; } /// /// 查看证书接口 /// /// 报名人员ID (EventRegistrationPerson的Id) /// 证书详情 [DisplayName("查看证书")] [HttpGet("/api/EventScore/GetCertificateDetail/{id}")] public async Task GetCertificateDetail([FromRoute] long id) { if (id <= 0) { return new BadRequestObjectResult(new { message = "无效的 ID" }); } var personExists = await _personRepository.IsAnyAsync(x => x.Id == id); if (!personExists) { return new NotFoundObjectResult(new { message = "人员记录不存在" }); } var cert = await _certificateRepository.GetFirstAsync(x => x.ParticipationRecordId == id); if (cert == null) { return new NotFoundObjectResult(new { message = "未找到对应的证书信息" }); } if (string.IsNullOrWhiteSpace(cert.CertificateUrl)) { return new NoContentResult(); } return new JsonResult(new { url = cert.CertificateUrl }); } /// /// 验证证书真伪 /// [DisplayName("验证证书真伪")] public async Task VerifyCertificate(string certificateNo) { if (string.IsNullOrWhiteSpace(certificateNo)) { throw Oops.Bah("证书编号不能为空"); } var cert = await _certificateRepository.GetFirstAsync(x => x.CertificateNo == certificateNo && !x.DeleteMark); if (cert == null) { return new { IsValid = false, Message = "证书不存在" }; } return new { IsValid = true, CertificateNo = cert.CertificateNo, ParticipantName = cert.ParticipantName, GroupName = cert.GroupName, IssuedTime = cert.IssuedTime, CertificateStatus = cert.CertificateStatus }; } /// /// 下载模板 /// [DisplayName("下载模板")] [HttpGet] public IActionResult DownloadTemplate() { var headers = new string[] { "赛事地址", "项目", "组别", "姓名", "联系方式", "单位", "成绩", "名次", "名次说明" }; var value = new List> { headers.ToDictionary(h => h, h => (object)null) }; using var memoryStream = new MemoryStream(); memoryStream.SaveAs(value); return new FileContentResult(memoryStream.ToArray(), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") { FileDownloadName = "成绩导入模板.xlsx" }; } /// /// 导出证书 /// [DisplayName("导出证书")] public async Task ExportCertificate(ScoreQueryInput input) { var list = await _certificateRepository.AsQueryable() .LeftJoin((c, r) => c.ResultId == r.Id) .LeftJoin((c, r, reg) => c.RegistrationId == reg.Id) .LeftJoin((c, r, reg, p) => c.ProjectId == p.Id) .Where((c, r, reg, p) => c.EventId == input.EventId) .WhereIF(input.ProjectId.HasValue, (c, r, reg, p) => c.ProjectId == input.ProjectId) .WhereIF(!string.IsNullOrWhiteSpace(input.GroupName), (c, r, reg, p) => c.GroupName == input.GroupName) .WhereIF(!string.IsNullOrWhiteSpace(input.Name), (c, r, reg, p) => c.ParticipantName.Contains(input.Name)) .Select((c, r, reg, p) => new CertificateExportDto { CertificateNo = c.CertificateNo, ParticipantName = c.ParticipantName, ProjectName = p.Name, GroupName = c.GroupName, UnitName = reg.TeamName, Score = r.Score, Rank = r.Rank, CertificateType = c.CertificateType == 1 ? "冠军证书" : c.CertificateType == 2 ? "亚军证书" : c.CertificateType == 3 ? "季军证书" : c.CertificateType == 4 ? "优胜证书" : "参与证书", IssuedTime = c.IssuedTime }) .ToListAsync(); using var stream = new MemoryStream(); stream.SaveAs(list); return Convert.ToBase64String(stream.ToArray()); } /// /// 导入成绩验证结果 /// public class ImportScoreValidationResult { public bool IsValid { get; set; } public List Errors { get; set; } = new List(); public List ValidRows { get; set; } = new List(); } /// /// 验证导入数据 /// private async Task ValidateImportDataAsync(List rows, long eventId) { var result = new ImportScoreValidationResult(); var errors = new List(); for (int i = 0; i < rows.Count; i++) { var row = rows[i]; var rowErrors = new List(); var rowNumber = i + 2; if (string.IsNullOrWhiteSpace(row.Name)) rowErrors.Add("姓名不能为空"); if (string.IsNullOrWhiteSpace(row.ProjectName)) rowErrors.Add("项目不能为空"); if (string.IsNullOrWhiteSpace(row.GroupName)) rowErrors.Add("组别不能为空"); if (string.IsNullOrWhiteSpace(row.Score)) rowErrors.Add("成绩不能为空"); if (row.Rank <= 0) rowErrors.Add("名次必须为正整数"); if (rowErrors.Any()) { errors.Add($"第 {rowNumber} 行:{string.Join(",", rowErrors)}"); } else { result.ValidRows.Add(row); } } if (errors.Any()) { result.IsValid = false; result.Errors = errors; } else { result.IsValid = true; } return result; } }