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 YD_Event.Core.Domain.Export; using MiniExcelLibs; using SqlSugar; using OfficeOpenXml; using OfficeOpenXml.Style; using System.Drawing; namespace YD_Event.Application.Event; /// /// 成绩与证书管理 /// public class EventScoreService : BaseService, ITransient { private static readonly string[] ChineseNumbers = { "零", "一", "二", "三", "四", "五", "六", "七", "八", "九", "十" }; /// /// 将数字名次转换为汉字形式 /// private static string ConvertRankToChinese(int rank) { if (rank <= 0) return string.Empty; if (rank <= 10) return $"第{ChineseNumbers[rank]}名"; if (rank < 20) return $"第十{ChineseNumbers[rank % 10]}名"; if (rank % 10 == 0) return $"第{ChineseNumbers[rank / 10]}十名"; return $"第{ChineseNumbers[rank / 10]}十{ChineseNumbers[rank % 10]}名"; } private readonly ISqlSugarRepository _personRepository; private readonly ISqlSugarRepository _resultRepository; private readonly ISqlSugarRepository _certificateRepository; private readonly ISqlSugarRepository _registrationRepository; private readonly ISqlSugarRepository _templateRepository; private readonly ISqlSugarRepository _projectRepository; private readonly ISqlSugarRepository _teamMemberRepository; private readonly ISqlSugarRepository _eventRepository; private readonly ISqlSugarRepository _pointsRuleRepository; private readonly ISqlSugarRepository _pointsRuleDetailRepository; private readonly ISqlSugarRepository _pointsRecordRepository; private readonly ISqlSugarRepository _appUserRepository; private readonly IIdGenerator _idGenerator; private readonly ILogger _logger; public EventScoreService( ISqlSugarRepository personRepository, ISqlSugarRepository resultRepository, ISqlSugarRepository certificateRepository, ISqlSugarRepository registrationRepository, ISqlSugarRepository templateRepository, ISqlSugarRepository projectRepository, ISqlSugarRepository teamMemberRepository, ISqlSugarRepository eventRepository, ISqlSugarRepository pointsRuleRepository, ISqlSugarRepository pointsRuleDetailRepository, ISqlSugarRepository pointsRecordRepository, ISqlSugarRepository appUserRepository, IIdGenerator idGenerator, ILogger logger) : base(resultRepository) { _personRepository = personRepository; _resultRepository = resultRepository; _certificateRepository = certificateRepository; _registrationRepository = registrationRepository; _templateRepository = templateRepository; _projectRepository = projectRepository; _teamMemberRepository = teamMemberRepository; _eventRepository = eventRepository; _pointsRuleRepository = pointsRuleRepository; _pointsRuleDetailRepository = pointsRuleDetailRepository; _pointsRecordRepository = pointsRecordRepository; _appUserRepository = appUserRepository; _idGenerator = idGenerator; _logger = logger; } /// /// 运动员成绩列表 /// [DisplayName("运动员成绩列表")] public async Task> PageAthlete(ScoreQueryInput input) { var pageResult = 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); foreach (var item in pageResult.Rows) { item.RankText = ConvertRankToChinese(item.Rank); } return pageResult; } /// /// 教练/领队列表 /// /// /// 通过运动员所在的团队关联获取教练和领队人员信息 /// [DisplayName("教练领队列表")] public async Task> PageCoachLeader(CoachLeaderQueryInput input) { // 步骤1:获取该赛事的所有运动员的 UserId var athleteUserIds = await _personRepository.AsQueryable() .InnerJoin((p, r) => p.RegistrationId == r.Id) .Where((p, r) => r.EventId == input.EventId && p.PersonType == PersonType.Athlete) .Select(p => p.UserId) .ToListAsync(); if (!athleteUserIds.Any()) { return new PageResult(); } // 步骤2:通过运动员的 UserId 找到对应的 TeamId var teamIds = await _teamMemberRepository.AsQueryable() .Where(tm => athleteUserIds.Contains(tm.UserId) && !tm.DeleteMark) .Select(tm => tm.TeamId) .Distinct() .ToListAsync(); if (!teamIds.Any()) { return new PageResult(); } // 步骤3:通过 TeamId 获取同团队的教练/领队的 UserId var coachLeaderUserIds = await _teamMemberRepository.AsQueryable() .Where(tm => teamIds.Contains(tm.TeamId) && tm.PersonType == input.PersonType && !tm.DeleteMark) .Select(tm => tm.UserId) .Distinct() .ToListAsync(); if (!coachLeaderUserIds.Any()) { return new PageResult(); } // 步骤4:从 AppUser 表获取教练/领队的详细信息(需要关联查询) var userQuery = _appUserRepository.AsQueryable() .Where(p => coachLeaderUserIds.Contains(p.Id)) .Where(p=> p.PersonType == input.PersonType); var pageResult = await userQuery .Select(p => new CoachLeaderListOutput { Id = p.Id, Name = p.Name, Age = p.Age, Gender = p.Gender.Description(), IdCardNumber = p.IdCard, Mobile = p.Phone, CertificateId = 0 // 需要单独查询,此处先设为0 }) .ToPagedListAsync(input); return pageResult; } /// /// 导入成绩(性能优化版) /// /// /// 性能优化策略: /// 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()); // 预加载项目信息,用于获取项目类型名称 var projectIds = allPersons.Select(p => p.ProjectId).Distinct().ToList(); var projects = await _projectRepository.GetListAsync(x => projectIds.Contains(x.Id)); var projectDict = projects.ToDictionary(p => p.Id, p => p.ProjectType.Description()); // 步骤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.ProjectTypeName = projectDict.TryGetValue(person.ProjectId, out var projectTypeName) ? projectTypeName : string.Empty; 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, ProjectTypeName = projectDict.TryGetValue(person.ProjectId, out var projectTypeName) ? projectTypeName : string.Empty, 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; } /// /// 查看证书接口 /// /// 输入参数 /// 证书详情 [DisplayName("查看证书")] public async Task GetCertificateDetail(YD_Event.Application.Certificate.Dtos.IdInput input) { if (input.Id <= 0) { throw Oops.Bah("无效的 ID"); } var personExists = await _personRepository.IsAnyAsync(x => x.Id == input.Id); if (!personExists) { throw Oops.Bah("人员记录不存在"); } var cert = await _certificateRepository.GetFirstAsync(x => x.ParticipationRecordId == input.Id); if (cert == null) { throw Oops.Bah("未找到对应的证书信息"); } return 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 exporter = new ScoreTemplateExporter(); var bytes = exporter.ExportAsync().GetAwaiter().GetResult(); return new FileContentResult(bytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") { FileDownloadName = "成绩导入模板.xlsx" }; } /// /// 成绩模板导出器 /// private class ScoreTemplateExporter : ExcelExportBase { public override async Task ExportAsync() { CreateWorksheet("成绩导入模板"); var headers = new string[] { "赛事地址", "项目", "组别", "姓名", "联系方式", "单位", "成绩", "名次(汉字)", "名次(数字)" }; var headerColor = Color.FromArgb(204, 255, 204); SetHeaders(headers, 1, headerColor, 11); var widths = new double[] { 15, 15, 15, 12, 15, 20, 15, 15, 15 }; SetColumnWidths(widths); SetRowHeight(1, 25); return await GetAsByteArrayAsync(); } } /// /// 导出证书 /// [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; } /// /// 发放积分输入模型 /// public class IssuePointsInput { public long EventId { get; set; } } /// /// 发放积分 /// [DisplayName("发放积分")] [HttpPost("issuePoints")] [UnitOfWork] public async Task IssuePoints([FromBody] IssuePointsInput input) { _logger.LogInformation("开始发放积分,赛事ID={EventId}", input.EventId); var eventInfo = await _eventRepository.GetByIdAsync(input.EventId); if (eventInfo == null) { throw Oops.Bah("赛事不存在"); } if (!eventInfo.PointsRuleId.HasValue) { throw Oops.Bah("该赛事未配置积分规则"); } var pointsRule = await _pointsRuleRepository.GetByIdAsync(eventInfo.PointsRuleId.Value); if (pointsRule == null) { throw Oops.Bah("积分规则不存在"); } if (pointsRule.Status != AvailabilityStatus.Enable) { throw Oops.Bah("积分规则未启用"); } var ruleDetails = await _pointsRuleDetailRepository.AsQueryable() .Where(x => x.PointsRuleId == pointsRule.Id) .OrderBy(x => x.StartRank) .ToListAsync(); var results = await _resultRepository.AsQueryable() .InnerJoin((r, p) => r.RegistrationPersonId == p.Id) .Where((r, p) => r.EventId == input.EventId && r.Rank > 0) .Select((r, p) => new { ResultId = r.Id, UserId = p.UserId, ProjectId = r.ProjectId, GroupName = r.GroupName, Rank = r.Rank, ParticipantName = r.ParticipantName, ProjectIdForRecord = r.ProjectId }) .ToListAsync(); if (!results.Any()) { throw Oops.Bah("没有可发放积分的成绩记录"); } var existingRecords = await _pointsRecordRepository.AsQueryable() .Where(x => x.EventId == input.EventId) .Select(x => x.Remark) .ToListAsync(); var processedResultIds = existingRecords .Where(x => !string.IsNullOrEmpty(x) && x.StartsWith("ResultId:")) .Select(x => { var parts = x.Split('|'); var resultIdPart = parts.FirstOrDefault(p => p.StartsWith("ResultId:")); return resultIdPart != null && long.TryParse(resultIdPart.Replace("ResultId:", ""), out var id) ? id : 0; }) .Where(x => x > 0) .ToHashSet(); int successCount = 0; int skipCount = 0; var pointsRecords = new List(); foreach (var result in results) { if (processedResultIds.Contains(result.ResultId)) { skipCount++; continue; } int pointsToAward = 0; if (pointsRule.ParticipationPoints.HasValue && pointsRule.ParticipationPoints.Value > 0) { pointsToAward += pointsRule.ParticipationPoints.Value; } var matchingDetail = ruleDetails.FirstOrDefault(d => result.Rank >= d.StartRank && result.Rank <= d.EndRank); if (matchingDetail != null) { pointsToAward += matchingDetail.Points; } if (pointsToAward > 0) { var record = new PointsRecord { Id = _idGenerator.NextId(), UserId = result.UserId, Type = PointsOperationType.Acquire, EventId = input.EventId, ProjectId = result.ProjectIdForRecord, Points = pointsToAward, Remark = $"ResultId:{result.ResultId}|Rank:{result.Rank}", Status = AvailabilityStatus.Enable, CreatedTime = DateTime.Now }; pointsRecords.Add(record); successCount++; } } if (pointsRecords.Any()) { await _pointsRecordRepository.InsertRangeAsync(pointsRecords); } _logger.LogInformation("积分发放完成:赛事ID={EventId},成功发放={SuccessCount},跳过={SkipCount}", input.EventId, successCount, skipCount); return new { Success = true, Message = $"积分发放成功!已发放 {successCount} 条积分记录,跳过 {skipCount} 条已发放记录" }; } }