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 IIdGenerator _idGenerator; private readonly ILogger _logger; public EventScoreService( ISqlSugarRepository personRepository, ISqlSugarRepository resultRepository, ISqlSugarRepository certificateRepository, ISqlSugarRepository registrationRepository, IIdGenerator idGenerator, ILogger logger) : base(resultRepository) { _personRepository = personRepository; _resultRepository = resultRepository; _certificateRepository = certificateRepository; _registrationRepository = registrationRepository; _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()) { _logger.LogWarning("Excel文件中没有数据"); return; } // 步骤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 skipCount = 0; foreach(var row in rows) { // 使用内存字典查找,避免数据库查询 var key = $"{row.Name}_{row.ProjectName}_{row.GroupName}"; if (!personDict.TryGetValue(key, out var person)) { skipCount++; 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); } } _logger.LogInformation("数据处理完成:新增 {InsertCount} 条,更新 {UpdateCount} 条,跳过 {SkipCount} 条", resultsToInsert.Count, resultsToUpdate.Count, skipCount); // 步骤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) { foreach (var personId in input.PersonIds) { var person = await _personRepository.GetByIdAsync(personId); if (person == null) continue; if (person.PersonType == PersonType.Athlete || person.PersonType == PersonType.User) { var result = await _resultRepository.GetFirstAsync(x => x.RegistrationPersonId == person.Id); if (result == null) continue; var cert = await _certificateRepository.GetFirstAsync(x => x.ResultId == result.Id); if (cert == null) { cert = new EventCertificate { Id = _idGenerator.NewLong(), EventId = input.EventId, ResultId = result.Id, RegistrationId = person.RegistrationId, ParticipationRecordId = person.Id, ParticipantName = person.Name, GroupName = person.GroupName, CertificateStatus = 1, GeneratedTime = DateTime.Now, CertificateNo = $"C{DateTime.Now:yyyyMMdd}{person.Id}", CreatedTime = DateTime.Now }; await _certificateRepository.InsertAsync(cert); } } else { var cert = await _certificateRepository.GetFirstAsync(x => x.ParticipationRecordId == person.Id); if (cert == null) { cert = new EventCertificate { Id = _idGenerator.NewLong(), EventId = input.EventId, RegistrationId = person.RegistrationId, ParticipationRecordId = person.Id, ParticipantName = person.Name, GroupName = person.GroupName, CertificateStatus = 1, GeneratedTime = DateTime.Now, CertificateNo = $"CC{DateTime.Now:yyyyMMdd}{person.Id}", CreatedTime = DateTime.Now }; await _certificateRepository.InsertAsync(cert); } } } } /// /// 查看证书接口 /// /// 报名人员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("下载模板")] [HttpGet] public IActionResult DownloadTemplate() { // 动态生成空数据及正确列名、样式的Excel模板 var headers = new string[] { "赛事地址", "项目", "组别", "姓名", "联系方式", "单位", "成绩", "名次", "名次 " }; // 创建一个包含动态表头的列表,MiniExcel可以通过字典或匿名对象集合来生成带指定列名的Excel var value = new List> { // 插入一行空数据以确保生成正确的表头 headers.ToDictionary(h => h, h => (object)null) }; using var memoryStream = new MemoryStream(); // MiniExcel 保存,使用默认样式。如果需要复杂的背景色和边框(如图片中的浅绿色表头), // MiniExcel 目前不支持高级样式导出,因此这里我们使用 OpenXml 直接操作或基础导出。 // 但由于需求要求保持代码清晰并尽量匹配原图,这里先用 MiniExcel 导出带中文表头的文件。 // 为了实现完全一致的浅绿色样式,最好结合 EPPlus 或 ClosedXML。 // 考虑到项目中可能没有引入 EPPlus,使用 MiniExcel 的基础导出,并辅以说明。 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()); } }