1413 lines
57 KiB
C#
Raw Normal View History

2026-02-08 18:09:05 +08:00
using Microsoft.AspNetCore.Http;
2026-03-19 23:36:06 +08:00
using Microsoft.AspNetCore.Mvc;
2026-04-06 09:21:47 +08:00
using Microsoft.Extensions.Logging;
2026-03-08 15:46:19 +08:00
using YD_Event.Application.Event.Dtos;
2026-04-15 15:14:52 +08:00
using YD_Event.Application.Certificate;
2026-02-08 18:09:05 +08:00
using YD_Event.Core.Entities;
2026-03-20 02:00:34 +08:00
using YD_Event.Core.Enum;
2026-04-14 15:11:41 +08:00
using YD_Event.Core.Domain.Export;
2026-02-08 18:09:05 +08:00
using MiniExcelLibs;
using SqlSugar;
2026-04-14 15:11:41 +08:00
using OfficeOpenXml;
using OfficeOpenXml.Style;
using System.Drawing;
2026-04-14 15:20:12 +08:00
using static SKIT.FlurlHttpClient.Wechat.TenpayV3.Models.DepositMarketingMemberCardOpenCardCodesResponse.Types;
using YD_Event.Application.AppUser.Dtos;
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 EventScoreService : BaseService<EventResult>, ITransient
2026-02-08 18:09:05 +08:00
{
2026-04-14 15:11:41 +08:00
private static readonly string[] ChineseNumbers = { "零", "一", "二", "三", "四", "五", "六", "七", "八", "九", "十" };
/// <summary>
/// 将数字名次转换为汉字形式
/// </summary>
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]}名";
}
2026-02-08 18:09:05 +08:00
private readonly ISqlSugarRepository<EventRegistrationPerson> _personRepository;
private readonly ISqlSugarRepository<EventResult> _resultRepository;
private readonly ISqlSugarRepository<EventCertificate> _certificateRepository;
private readonly ISqlSugarRepository<EventRegistration> _registrationRepository;
private readonly ISqlSugarRepository<CertificateTemplate> _templateRepository;
2026-04-15 14:28:22 +08:00
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Certificate> _certificateMasterRepository;
2026-04-14 15:11:41 +08:00
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Project> _projectRepository;
2026-04-22 11:40:12 +08:00
private readonly ISqlSugarRepository<EventProject> _eventProjectRepository;
2026-04-14 15:11:41 +08:00
private readonly ISqlSugarRepository<TeamMember> _teamMemberRepository;
private readonly ISqlSugarRepository<EventInfo> _eventRepository;
private readonly ISqlSugarRepository<PointsRule> _pointsRuleRepository;
private readonly ISqlSugarRepository<PointsRuleDetail> _pointsRuleDetailRepository;
private readonly ISqlSugarRepository<PointsRecord> _pointsRecordRepository;
private readonly ISqlSugarRepository<YD_Event.Core.Entities.AppUser> _appUserRepository;
2026-04-15 14:28:22 +08:00
private readonly CertificateGenerateService _certificateGenerateService;
2026-02-08 18:09:05 +08:00
private readonly IIdGenerator _idGenerator;
2026-04-06 09:21:47 +08:00
private readonly ILogger<EventScoreService> _logger;
2026-02-08 18:09:05 +08:00
public EventScoreService(
ISqlSugarRepository<EventRegistrationPerson> personRepository,
ISqlSugarRepository<EventResult> resultRepository,
ISqlSugarRepository<EventCertificate> certificateRepository,
ISqlSugarRepository<EventRegistration> registrationRepository,
ISqlSugarRepository<CertificateTemplate> templateRepository,
2026-04-15 14:28:22 +08:00
ISqlSugarRepository<YD_Event.Core.Entities.Certificate> certificateMasterRepository,
2026-04-14 15:11:41 +08:00
ISqlSugarRepository<YD_Event.Core.Entities.Project> projectRepository,
2026-04-22 11:40:12 +08:00
ISqlSugarRepository<EventProject> eventProjectRepository,
2026-04-14 15:11:41 +08:00
ISqlSugarRepository<TeamMember> teamMemberRepository,
ISqlSugarRepository<EventInfo> eventRepository,
ISqlSugarRepository<PointsRule> pointsRuleRepository,
ISqlSugarRepository<PointsRuleDetail> pointsRuleDetailRepository,
ISqlSugarRepository<PointsRecord> pointsRecordRepository,
ISqlSugarRepository<YD_Event.Core.Entities.AppUser> appUserRepository,
2026-04-15 14:28:22 +08:00
CertificateGenerateService certificateGenerateService,
2026-04-06 09:21:47 +08:00
IIdGenerator idGenerator,
ILogger<EventScoreService> logger) : base(resultRepository)
2026-02-08 18:09:05 +08:00
{
_personRepository = personRepository;
_resultRepository = resultRepository;
_certificateRepository = certificateRepository;
_registrationRepository = registrationRepository;
_templateRepository = templateRepository;
2026-04-15 14:28:22 +08:00
_certificateMasterRepository = certificateMasterRepository;
2026-04-14 15:11:41 +08:00
_projectRepository = projectRepository;
2026-04-22 11:40:12 +08:00
_eventProjectRepository = eventProjectRepository;
2026-04-14 15:11:41 +08:00
_teamMemberRepository = teamMemberRepository;
_eventRepository = eventRepository;
_pointsRuleRepository = pointsRuleRepository;
_pointsRuleDetailRepository = pointsRuleDetailRepository;
_pointsRecordRepository = pointsRecordRepository;
_appUserRepository = appUserRepository;
2026-04-15 14:28:22 +08:00
_certificateGenerateService = certificateGenerateService;
2026-02-08 18:09:05 +08:00
_idGenerator = idGenerator;
2026-04-06 09:21:47 +08:00
_logger = logger;
2026-02-08 18:09:05 +08:00
}
/// <summary>
/// 运动员成绩列表
/// </summary>
[DisplayName("运动员成绩列表")]
public async Task<PageResult<ScoreListOutput>> PageAthlete(ScoreQueryInput input)
{
2026-04-15 14:28:22 +08:00
_logger.LogInformation("开始查询运动员成绩列表,赛事ID={EventId}, 项目ID={ProjectId}, 赛事项目组别ID={EventProjectGroupId}, 组别名称={GroupName}",
input.EventId, input.ProjectId, input.EventProjectGroupId, input.GroupName);
2026-04-15 10:41:07 +08:00
2026-04-22 11:40:12 +08:00
try
2026-04-15 10:41:07 +08:00
{
2026-04-22 11:40:12 +08:00
// 步骤1:获取所有 EventProject 信息,建立 EventProjectId -> ProjectId 映射
var eventProjects = await _eventProjectRepository.GetListAsync(x => x.EventId == input.EventId);
var eventProjectDictToProjectIdMap = eventProjects.ToDictionary(x => x.Id, x => x.ProjectId);
// 获取所有相关的 Project 信息
var allProjectIds = eventProjects.Select(x => x.ProjectId).Distinct().ToList();
var projectsList = await _projectRepository.GetListAsync(x => allProjectIds.Contains(x.Id));
var projectDict = projectsList.ToDictionary(x => x.Id, x => x);
// 步骤2:查询基本数据
var baseQuery = _personRepository.AsQueryable()
.LeftJoin<EventRegistration>((p, r) => p.RegistrationId == r.Id)
.LeftJoin<EventResult>((p, r, res) => p.Id == res.RegistrationPersonId)
.LeftJoin<EventCertificate>((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));
// 应用筛选条件
if (input.ProjectId.HasValue)
{
baseQuery = baseQuery.Where((p, r, res, c) => p.ProjectId == input.ProjectId.Value);
}
if (input.EventProjectGroupId.HasValue)
{
baseQuery = baseQuery.Where((p, r, res, c) => p.EventProjectGroupId == input.EventProjectGroupId.Value);
}
else if (!string.IsNullOrWhiteSpace(input.GroupName))
{
baseQuery = baseQuery.Where((p, r, res, c) => p.GroupName == input.GroupName);
}
if (!string.IsNullOrWhiteSpace(input.Name))
{
baseQuery = baseQuery.Where((p, r, res, c) => p.Name.Contains(input.Name));
}
if (!string.IsNullOrWhiteSpace(input.UnitName))
{
baseQuery = baseQuery.Where((p, r, res, c) => r.TeamName.Contains(input.UnitName));
}
if (input.Gender.HasValue)
{
baseQuery = baseQuery.Where((p, r, res, c) => p.Gender == input.Gender.Value);
}
// 查询基本数据到内存
var baseData = await baseQuery
.OrderBy((p, r, res, c) => res.Rank)
.Select((p, r, res, c) => new
{
Id = p.Id,
Name = p.Name,
UnitName = r.TeamName ?? string.Empty,
Gender = (int)p.Gender,
Score = res.Score,
Rank = res.Rank,
ResultId = res.Id,
CertificateId = c.Id,
EventProjectId = p.ProjectId, // 注意:这里 p.ProjectId 实际上是 EventProject 的 Id
GroupName = p.GroupName ?? string.Empty,
TeamName = r.TeamName ?? string.Empty,
RegistrationItemId = p.RegistrationItemId // 增加报名项目唯一码,用于分组
})
.ToListAsync();
2026-04-15 10:41:07 +08:00
2026-04-22 11:40:12 +08:00
// 步骤3:进行分组处理(与赛事编排页面保持一致)
var processedData = new List<ScoreListOutput>();
// 先按 EventProjectId 和 GroupName 分组
var eventProjectGroups = baseData.GroupBy(x => new { x.EventProjectId, x.GroupName });
foreach (var eventProjectGroup in eventProjectGroups)
2026-02-08 18:09:05 +08:00
{
2026-04-22 11:40:12 +08:00
var firstItem = eventProjectGroup.First();
var isTeamProject = false;
// 查找对应的 Project 信息
if (eventProjectDictToProjectIdMap.TryGetValue(firstItem.EventProjectId, out var projectId) &&
projectDict.TryGetValue(projectId, out var project))
{
isTeamProject = project.ProjectType == ProjectTypeEnum.TeamSpeed ||
project.ProjectType == ProjectTypeEnum.TeamFreestyle;
}
if (isTeamProject)
{
// 团队项目:按 GroupName、TeamName 和 RegistrationItemId 分组(与赛事编排页面一致)
// 增加 RegistrationItemId 作为分组条件,确保同一条报名记录的多个小组被正确区分
var teamGroups = eventProjectGroup.GroupBy(x => new { x.GroupName, x.TeamName, x.RegistrationItemId });
foreach (var teamGroup in teamGroups)
{
var teamItems = teamGroup.ToList();
var names = string.Join(", ", teamItems.Select(x => x.Name).Distinct().OrderBy(x => x));
var representative = teamItems.OrderBy(x => x.Rank).First();
processedData.Add(new ScoreListOutput
{
Id = representative.Id,
Name = names,
UnitName = representative.UnitName,
Gender = representative.Gender,
Score = representative.Score,
Rank = representative.Rank,
ResultId = representative.ResultId,
CertificateId = representative.CertificateId,
RankText = ConvertRankToChinese(representative.Rank)
});
}
}
else
{
// 个人项目:直接添加
foreach (var item in eventProjectGroup)
{
processedData.Add(new ScoreListOutput
{
Id = item.Id,
Name = item.Name,
UnitName = item.UnitName,
Gender = item.Gender,
Score = item.Score,
Rank = item.Rank,
ResultId = item.ResultId,
CertificateId = item.CertificateId,
RankText = ConvertRankToChinese(item.Rank)
});
}
}
}
// 步骤4:手动进行分页
var total = processedData.Count;
var pageSize = input.PageSize > 0 ? input.PageSize : 10;
var pageNo = input.PageNo > 0 ? input.PageNo - 1 : 0;
var pagedData = processedData.Skip(pageNo * pageSize).Take(pageSize).ToList();
var pageResult = new PageResult<ScoreListOutput>
{
PageNo = input.PageNo,
PageSize = pageSize,
Total = total,
Pages = (int)Math.Ceiling((double)total / pageSize),
Rows = pagedData
};
_logger.LogInformation("查询运动员成绩列表完成,共{Count}条记录", pageResult.Total);
return pageResult;
}
catch (Exception ex)
2026-04-14 15:11:41 +08:00
{
2026-04-22 11:40:12 +08:00
_logger.LogError(ex, "查询运动员成绩列表失败");
throw;
2026-04-14 15:11:41 +08:00
}
2026-02-08 18:09:05 +08:00
}
/// <summary>
/// 教练/领队列表
/// </summary>
2026-04-14 15:11:41 +08:00
/// <remarks>
/// 通过运动员所在的团队关联获取教练和领队人员信息
/// </remarks>
2026-02-08 18:09:05 +08:00
[DisplayName("教练领队列表")]
public async Task<PageResult<CoachLeaderListOutput>> PageCoachLeader(CoachLeaderQueryInput input)
{
2026-04-14 15:11:41 +08:00
// 步骤1:获取该赛事的所有运动员的 UserId
var athleteUserIds = await _personRepository.AsQueryable()
.InnerJoin<EventRegistration>((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<CoachLeaderListOutput>();
}
// 步骤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<CoachLeaderListOutput>();
}
// 步骤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<CoachLeaderListOutput>();
}
// 步骤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
2026-02-08 18:09:05 +08:00
{
Id = p.Id,
Name = p.Name,
Age = p.Age,
2026-04-17 12:47:13 +08:00
Gender = (int)p.Gender,
BirthDate = p.BirthDate,
IdCardNumber = p.IdCardNumber,
IdCardImageUrl = p.IdCardImageUrl,
IdCardBackImageUrl = p.IdCardBackImageUrl,
2026-04-14 15:20:12 +08:00
Mobile = p.Mobile,
2026-04-14 15:11:41 +08:00
CertificateId = 0 // 需要单独查询,此处先设为0
2026-02-08 18:09:05 +08:00
})
.ToPagedListAsync(input);
2026-04-14 15:11:41 +08:00
2026-04-14 15:20:12 +08:00
foreach (var item in pageResult.Rows)
{
if (item.Age.HasValue)
{
continue;
}
if (item.BirthDate.HasValue)
{
item.Age = MemberOutput.CalculateAgeFromBirthDate(item.BirthDate);
}
}
2026-04-14 15:11:41 +08:00
return pageResult;
2026-02-08 18:09:05 +08:00
}
/// <summary>
2026-04-10 22:39:32 +08:00
/// 导入成绩(性能优化版)
2026-02-08 18:09:05 +08:00
/// </summary>
2026-04-10 22:39:32 +08:00
/// <remarks>
/// 性能优化策略:
/// 1. 批量预加载所有人员数据到内存
/// 2. 使用字典建立索引,避免循环内数据库查询
/// 3. 批量收集需要插入/更新的记录
/// 4. 最后一次性执行批量操作
/// 性能指标:导入10000条成绩数据时,响应时间 < 30秒,内存占用 < 512MB
2026-04-22 11:40:12 +08:00
/// 团队项目支持:团队项目按团队维度导入,一个团队对应一个完整证书
2026-04-10 22:39:32 +08:00
/// </remarks>
2026-02-08 18:09:05 +08:00
[UnitOfWork]
[DisplayName("导入成绩")]
public async Task ImportScore(long eventId, IFormFile file)
{
2026-04-10 22:39:32 +08:00
_logger.LogInformation("开始导入成绩:赛事ID={EventId}", eventId);
var startTime = DateTime.Now;
2026-02-08 18:09:05 +08:00
using var stream = file.OpenReadStream();
2026-04-22 14:47:50 +08:00
var allRows = stream.Query<ScoreImportDto>().ToList();
_logger.LogInformation("解析Excel完成,共 {RowCount} 条原始记录", allRows.Count);
// 过滤掉空行(没有报名ID的行)
var rows = allRows.Where(r =>
r.RegistrationItemId.HasValue &&
r.RegistrationItemId.Value > 0
).ToList();
_logger.LogInformation("过滤空行后,共 {RowCount} 条有效记录", rows.Count);
2026-04-10 22:39:32 +08:00
if (!rows.Any())
{
2026-04-22 14:47:50 +08:00
throw Oops.Bah("Excel文件中没有有效数据,请确保填写了报名ID");
}
// 验证导入数据
var validationResult = await ValidateImportDataAsync(rows, eventId);
if (!validationResult.IsValid)
{
var errorMessage = string.Join("\n", validationResult.Errors);
throw Oops.Bah($"数据验证失败:\n{errorMessage}");
2026-04-10 22:39:32 +08:00
}
2026-04-22 11:40:12 +08:00
// 步骤1:获取赛事的所有 EventProject 信息
var eventProjects = await _eventProjectRepository.GetListAsync(x => x.EventId == eventId);
var eventProjectDictToProjectId = eventProjects.ToDictionary(x => x.Id, x => x.ProjectId);
2026-04-10 22:39:32 +08:00
2026-04-22 11:40:12 +08:00
// 步骤2:批量预加载该赛事的所有人员数据(包含团队信息)
var allPersonsWithTeam = await _personRepository.AsQueryable()
.InnerJoin<EventRegistration>((p, r) => p.RegistrationId == r.Id)
.Where((p, r) => r.EventId == eventId)
.Select((p, r) => new {
p.Id,
p.RegistrationItemId,
p.Name,
p.ProjectName,
p.GroupName,
p.RegistrationId,
p.ProjectId,
r.TeamName
})
.ToListAsync();
2026-04-10 22:39:32 +08:00
2026-04-22 11:40:12 +08:00
_logger.LogInformation("预加载人员数据完成,共 {PersonCount} 人", allPersonsWithTeam.Count);
// 步骤3:获取所有相关项目信息,用于判断项目类型
var allProjectIds = eventProjects.Select(x => x.ProjectId).Distinct().ToList();
var allProjects = await _projectRepository.GetListAsync(x => allProjectIds.Contains(x.Id));
var allProjectDict = allProjects.ToDictionary(p => p.Id, p => p);
2026-04-22 14:47:50 +08:00
// 步骤4:使用字典建立人员索引(按RegistrationItemId索引,取第一个人作为代表)
// 注意:团队项目中一个 RegistrationItemId 可能对应多个运动员,但只需要第一个作为代表
var personByItemIdDict = allPersonsWithTeam
.GroupBy(p => p.RegistrationItemId)
2026-04-22 11:40:12 +08:00
.ToDictionary(g => g.Key, g => g.First());
2026-04-14 15:11:41 +08:00
2026-04-22 11:40:12 +08:00
// 预加载 EventRegistration 信息,用于获取归属单位 ID
var registrationIdsList = allPersonsWithTeam.Select(p => p.RegistrationId).Distinct().ToList();
var registrations = await _registrationRepository.GetListAsync(r => registrationIdsList.Contains(r.Id));
var registrationDict = registrations.ToDictionary(r => r.Id);
2026-04-10 22:39:32 +08:00
2026-04-22 11:40:12 +08:00
// 步骤5:预加载该赛事的所有成绩记录
var personIds = allPersonsWithTeam.Select(p => p.Id).ToList();
2026-04-10 22:39:32 +08:00
var existingResults = await _resultRepository.GetListAsync(x =>
x.EventId == eventId && personIds.Contains(x.RegistrationPersonId));
var resultDict = existingResults.ToDictionary(r => r.RegistrationPersonId);
2026-04-22 11:40:12 +08:00
// 步骤6:批量处理数据,收集需要插入/更新的记录
2026-04-10 22:39:32 +08:00
var resultsToInsert = new List<EventResult>();
var resultsToUpdate = new List<EventResult>();
var now = DateTime.Now;
var notFoundPersons = new List<string>();
2026-04-10 22:39:32 +08:00
foreach(var row in validationResult.ValidRows)
2026-04-22 11:40:12 +08:00
{
2026-04-22 14:47:50 +08:00
// 使用报名ID进行匹配
if (!personByItemIdDict.TryGetValue(row.RegistrationItemId.Value, out var person))
2026-02-08 18:09:05 +08:00
{
2026-04-22 14:47:50 +08:00
notFoundPersons.Add($"报名ID:{row.RegistrationItemId.Value}");
continue;
2026-04-22 11:40:12 +08:00
}
// 获取项目信息,判断是否为团队项目
bool isTeamProject = false;
string projectTypeName = string.Empty;
if (eventProjectDictToProjectId.TryGetValue(person.ProjectId, out var projectId) &&
allProjectDict.TryGetValue(projectId, out var project))
{
isTeamProject = project.ProjectType == ProjectTypeEnum.TeamSpeed ||
project.ProjectType == ProjectTypeEnum.TeamFreestyle;
projectTypeName = project.ProjectType.Description();
}
// 如果是团队项目,找到该团队的所有成员
List<dynamic> teamMembers = null;
string combinedNames = person.Name;
if (isTeamProject)
{
2026-04-22 14:47:50 +08:00
// 查找该报名项目的所有成员
2026-04-22 11:40:12 +08:00
teamMembers = allPersonsWithTeam
2026-04-22 14:47:50 +08:00
.Where(p => p.RegistrationItemId == person.RegistrationItemId)
.OrderBy(p => (string)p.Name)
2026-04-22 11:40:12 +08:00
.ToList<dynamic>();
2026-04-22 14:47:50 +08:00
// 收集所有姓名
var namesList = new List<string>();
foreach (var member in teamMembers)
{
namesList.Add((string)member.Name);
}
combinedNames = string.Join(", ", namesList);
2026-04-22 11:40:12 +08:00
}
2026-04-10 22:39:32 +08:00
2026-04-22 11:40:12 +08:00
// 处理成绩记录
2026-04-10 22:39:32 +08:00
if (resultDict.TryGetValue(person.Id, out var existingResult))
{
// 更新现有记录
existingResult.Score = row.Score;
existingResult.Rank = row.Rank;
existingResult.Remark = row.RankText;
2026-04-22 11:40:12 +08:00
existingResult.ProjectTypeName = projectTypeName;
// 对于团队项目,更新 ParticipantName 为团队所有成员姓名
if (isTeamProject)
{
existingResult.ParticipantName = combinedNames;
}
2026-04-10 22:39:32 +08:00
existingResult.UpdatedTime = now;
resultsToUpdate.Add(existingResult);
}
else
{
2026-04-22 11:40:12 +08:00
// 从 EventRegistration 获取归属单位 ID
long? merchantCertId = null;
if (registrationDict.TryGetValue(person.RegistrationId, out var reg))
{
merchantCertId = reg.MerchantCertificationId;
}
2026-04-10 22:39:32 +08:00
// 插入新记录
var newResult = new EventResult
2026-02-08 18:09:05 +08:00
{
Id = _idGenerator.NewLong(),
EventId = eventId,
RegistrationPersonId = person.Id,
RegistrationId = person.RegistrationId,
ProjectId = person.ProjectId,
2026-04-22 11:40:12 +08:00
ParticipantName = isTeamProject ? combinedNames : person.Name,
2026-02-08 18:09:05 +08:00
GroupName = person.GroupName,
2026-04-22 11:40:12 +08:00
MerchantCertificationId = merchantCertId,
ProjectTypeName = projectTypeName,
2026-04-10 22:39:32 +08:00
Score = row.Score,
Rank = row.Rank,
Remark = row.RankText,
CreatedTime = now,
UpdatedTime = now
2026-02-08 18:09:05 +08:00
};
2026-04-10 22:39:32 +08:00
resultsToInsert.Add(newResult);
2026-02-08 18:09:05 +08:00
}
}
2026-04-10 22:39:32 +08:00
if (notFoundPersons.Any())
{
2026-04-22 14:47:50 +08:00
_logger.LogWarning("未找到匹配的参赛人员:{Count} 条,未找到的报名ID:{Ids}",
notFoundPersons.Count, string.Join(", ", notFoundPersons));
}
_logger.LogInformation("数据处理完成:新增 {InsertCount} 条,更新 {UpdateCount} 条,未找到 {NotFoundCount} 条",
resultsToInsert.Count, resultsToUpdate.Count, notFoundPersons.Count);
2026-04-10 22:39:32 +08:00
2026-04-22 11:40:12 +08:00
// 步骤7:批量操作数据库
2026-04-10 22:39:32 +08:00
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);
2026-02-08 18:09:05 +08:00
}
/// <summary>
/// 发放证书(支持模板选择和批量发放)
2026-02-08 18:09:05 +08:00
/// </summary>
[UnitOfWork]
[DisplayName("发放证书")]
public async Task<IssueCertificateResult> IssueCertificate(IssueCertificateInput input)
2026-02-08 18:09:05 +08:00
{
var result = new IssueCertificateResult();
var now = DateTime.Now;
2026-04-15 14:28:22 +08:00
// 验证证书是否存在并获取证书信息
var certificate = await _certificateMasterRepository.GetFirstAsync(x =>
x.Id == input.CertificateId &&
x.Status == AvailabilityStatus.Enable &&
!x.DeleteMark);
if (certificate == null)
{
2026-04-15 14:28:22 +08:00
throw Oops.Bah("指定的证书不存在或不可用");
}
// 根据证书获取对应的模板ID
var templateId = certificate.TemplateId;
// 验证模板是否存在
var template = await _templateRepository.GetFirstAsync(x =>
x.Id == templateId &&
x.Status == AvailabilityStatus.Enable &&
!x.DeleteMark);
if (template == null)
{
throw Oops.Bah("证书关联的模板不存在或不可用");
}
2026-04-16 15:42:29 +08:00
// 根据页面类型确定允许的人员类型
List<PersonType> allowedPersonTypes = GetAllowedPersonTypes(input.PageType);
_logger.LogInformation("发放证书 - 页面类型: {PageType}, 允许的人员类型: {AllowedTypes}",
input.PageType, string.Join(", ", allowedPersonTypes));
2026-04-22 11:40:12 +08:00
// 步骤1:获取赛事的所有 EventProject 信息
var eventProjects = await _eventProjectRepository.GetListAsync(x => x.EventId == input.EventId);
var eventProjectDictToProjectId = eventProjects.ToDictionary(x => x.Id, x => x.ProjectId);
// 步骤2:获取所有相关项目信息
var allProjectIds = eventProjects.Select(x => x.ProjectId).Distinct().ToList();
var allProjects = await _projectRepository.GetListAsync(x => allProjectIds.Contains(x.Id));
var allProjectDict = allProjects.ToDictionary(p => p.Id, p => p);
// 步骤3:批量预加载该赛事的所有人员数据(包含团队信息)
var allPersonsWithTeam = await _personRepository.AsQueryable()
.InnerJoin<EventRegistration>((p, r) => p.RegistrationId == r.Id)
.Where((p, r) => r.EventId == input.EventId && !p.DeleteMark)
.Select((p, r) => new {
p.Id,
p.PersonType,
p.UserId,
p.Name,
p.ProjectName,
p.GroupName,
p.RegistrationId,
p.ProjectId,
p.RegistrationItemId,
p.EventProjectGroupId,
p.DeleteMark,
r.TeamName,
r.MerchantCertificationId,
Person = p
})
.ToListAsync();
2026-04-16 15:42:29 +08:00
// 构建最终的人员字典
2026-04-22 11:40:12 +08:00
var personDict = new Dictionary<long, dynamic>();
2026-04-16 15:42:29 +08:00
2026-04-17 09:19:21 +08:00
// 根据页面类型区分人员信息获取逻辑
if (input.PageType == 2 || input.PageType == 3)
2026-04-16 15:42:29 +08:00
{
2026-04-17 09:19:21 +08:00
// 页面类型为教练(2)或领队(3)时,从团队关系数据中获取对应人员ID
_logger.LogInformation("页面类型为教练/领队,从团队关系数据中获取人员信息");
2026-04-16 15:42:29 +08:00
2026-04-22 11:40:12 +08:00
// 建立 UserId 到人员的映射
var userToPersonMap = allPersonsWithTeam
2026-04-16 15:42:29 +08:00
.Where(p => p.UserId > 0)
.GroupBy(p => p.UserId)
.ToDictionary(g => g.Key, g => g.First());
2026-04-22 11:40:12 +08:00
// 通过 UserId 查找对应的人员
2026-04-17 09:19:21 +08:00
foreach (var userId in input.PersonIds)
2026-04-16 15:42:29 +08:00
{
2026-04-17 09:19:21 +08:00
if (userToPersonMap.TryGetValue(userId, out var person))
2026-04-16 15:42:29 +08:00
{
2026-04-17 09:19:21 +08:00
personDict[userId] = person;
2026-04-22 11:40:12 +08:00
_logger.LogInformation("通过 UserId {UserId} 找到对应的人员: {PersonId}", userId, person.Id);
2026-04-16 15:42:29 +08:00
}
else
{
2026-04-22 11:40:12 +08:00
_logger.LogWarning("无法通过 UserId {UserId} 找到对应的人员", userId);
2026-04-16 15:42:29 +08:00
}
}
}
2026-04-17 09:19:21 +08:00
else
{
// 页面类型为运动员(1)或其他,直接获取人员ID
_logger.LogInformation("页面类型为运动员,直接获取人员信息");
2026-04-22 11:40:12 +08:00
// 直接从人员列表中查找
foreach (var person in allPersonsWithTeam.Where(p => input.PersonIds.Contains(p.Id)))
2026-04-17 09:19:21 +08:00
{
personDict[person.Id] = person;
2026-04-22 11:40:12 +08:00
_logger.LogInformation("直接获取人员: {PersonId}", person.Id);
2026-04-17 09:19:21 +08:00
}
}
2026-04-16 15:42:29 +08:00
// 使用最终找到的人员ID列表
2026-04-22 11:40:12 +08:00
var foundPersonIds = personDict.Values.Select(p => (long)p.Id).ToList();
2026-04-16 15:42:29 +08:00
var existingResults = await _resultRepository.GetListAsync(x => foundPersonIds.Contains(x.RegistrationPersonId));
var resultDict = existingResults.ToDictionary(r => r.RegistrationPersonId);
var existingCerts = await _certificateRepository.GetListAsync(x =>
2026-04-16 15:42:29 +08:00
foundPersonIds.Contains(x.ParticipationRecordId));
var certDict = existingCerts.ToDictionary(c => c.ParticipationRecordId);
var certsToInsert = new List<EventCertificate>();
2026-04-15 15:14:52 +08:00
var certsToUpdate = new List<EventCertificate>();
2026-02-08 18:09:05 +08:00
foreach (var personId in input.PersonIds)
{
if (!personDict.TryGetValue(personId, out var person))
{
result.FailCount++;
result.FailDetails.Add($"人员ID {personId} 不存在");
continue;
}
2026-04-16 15:42:29 +08:00
// 验证人员类型是否与当前页面类型匹配
if (!allowedPersonTypes.Contains(person.PersonType))
{
result.FailCount++;
result.FailDetails.Add($"人员 {person.Name} 的类型 ({person.PersonType}) 与当前页面类型不匹配");
continue;
}
2026-04-22 11:40:12 +08:00
// 获取项目信息,判断是否为团队项目
bool isTeamProject = false;
string combinedNames = person.Name;
if (person.PersonType == PersonType.Athlete || person.PersonType == PersonType.User)
{
long projectId;
Core.Entities.Project project = null;
if (eventProjectDictToProjectId.TryGetValue(person.ProjectId, out projectId) &&
allProjectDict.TryGetValue(projectId, out project))
{
isTeamProject = project.ProjectType == ProjectTypeEnum.TeamSpeed ||
project.ProjectType == ProjectTypeEnum.TeamFreestyle;
}
// 如果是团队项目,找到该团队的所有成员并合并姓名
if (isTeamProject)
{
2026-04-22 14:47:50 +08:00
// 回到原来的方法,但确保不会出错
2026-04-22 11:40:12 +08:00
var teamMembers = allPersonsWithTeam
2026-04-22 14:47:50 +08:00
.Where(p => p.RegistrationItemId == person.RegistrationItemId)
.OrderBy(p => (string)p.Name)
2026-04-22 11:40:12 +08:00
.ToList();
2026-04-22 14:47:50 +08:00
var namesList = new List<string>();
foreach (var member in teamMembers)
{
namesList.Add((string)member.Name);
}
combinedNames = string.Join(", ", namesList);
2026-04-22 11:40:12 +08:00
}
}
2026-04-16 15:42:29 +08:00
// 获取实际的 EventRegistrationPerson ID(如果通过 UserId 映射过来的,需要使用实际的 Id)
var actualPersonId = person.Id;
2026-04-22 11:40:12 +08:00
var merchantCertId = person.MerchantCertificationId;
2026-04-16 15:42:29 +08:00
EventCertificate? cert = null;
2026-04-15 15:14:52 +08:00
bool isNewCert = false;
2026-02-08 18:09:05 +08:00
2026-03-20 02:00:34 +08:00
if (person.PersonType == PersonType.Athlete || person.PersonType == PersonType.User)
2026-02-08 18:09:05 +08:00
{
2026-04-22 11:40:12 +08:00
EventResult resultRecord;
if (!resultDict.TryGetValue(actualPersonId, out resultRecord))
{
result.FailCount++;
result.FailDetails.Add($"人员 {person.Name} 没有成绩记录");
continue;
}
2026-04-22 11:40:12 +08:00
EventCertificate existingCert;
if (certDict.TryGetValue(actualPersonId, out existingCert))
{
cert = existingCert;
}
else
2026-02-08 18:09:05 +08:00
{
cert = new EventCertificate
{
Id = _idGenerator.NewLong(),
EventId = input.EventId,
ResultId = resultRecord.Id,
2026-02-08 18:09:05 +08:00
RegistrationId = person.RegistrationId,
2026-04-16 15:42:29 +08:00
ParticipationRecordId = actualPersonId,
ProjectId = person.ProjectId,
UserId = person.UserId,
2026-04-22 11:40:12 +08:00
ParticipantName = combinedNames, // 团队项目显示所有成员姓名
2026-02-08 18:09:05 +08:00
GroupName = person.GroupName,
2026-04-22 11:40:12 +08:00
MerchantCertificationId = merchantCertId,
2026-02-08 18:09:05 +08:00
CertificateStatus = 1,
GeneratedTime = now,
2026-04-16 15:42:29 +08:00
CertificateNo = $"C{now:yyyyMMdd}{actualPersonId}",
CreatedTime = now,
2026-04-15 14:28:22 +08:00
TemplateId = templateId,
2026-04-15 15:14:52 +08:00
CertificateUrl = string.Empty
2026-02-08 18:09:05 +08:00
};
certsToInsert.Add(cert);
2026-04-15 15:14:52 +08:00
isNewCert = true;
}
2026-04-22 11:40:12 +08:00
// 构建占位符数据(传递合并后的姓名)
var placeholderData = BuildPlaceholderData(person.Person, resultRecord, combinedNames);
2026-04-15 15:14:52 +08:00
// 生成 PDF 证书
try
{
var generateResult = await _certificateGenerateService.GeneratePdf(
new YD_Event.Application.Certificate.Dtos.GeneratePdfCertificateInput
{
CertificateId = certificate.Id,
TemplateId = templateId,
Data = placeholderData,
GenerateQRCode = true,
QRCodeSize = 150
});
if (generateResult.IsSuccess && !string.IsNullOrEmpty(generateResult.PdfUrl))
{
cert.CertificateUrl = generateResult.PdfUrl;
cert.CertificateStatus = 1; // 已生成
cert.GeneratedTime = now;
2026-04-22 11:40:12 +08:00
cert.ParticipantName = combinedNames; // 更新证书上的姓名
2026-04-15 15:14:52 +08:00
_logger.LogInformation("PDF证书生成成功: EventCertificateId={Id}, Url={Url}", cert.Id, generateResult.PdfUrl);
}
else
{
_logger.LogWarning("PDF证书生成失败: {Message}", generateResult.ErrorMessage);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "PDF证书生成异常: EventCertificateId={Id}", cert.Id);
2026-02-08 18:09:05 +08:00
}
}
else
{
2026-04-22 11:40:12 +08:00
EventCertificate existingCert;
if (certDict.TryGetValue(actualPersonId, out existingCert))
{
cert = existingCert;
}
else
2026-02-08 18:09:05 +08:00
{
cert = new EventCertificate
{
Id = _idGenerator.NewLong(),
EventId = input.EventId,
RegistrationId = person.RegistrationId,
2026-04-16 15:42:29 +08:00
ParticipationRecordId = actualPersonId,
ProjectId = person.ProjectId,
UserId = person.UserId,
2026-02-08 18:09:05 +08:00
ParticipantName = person.Name,
GroupName = person.GroupName,
2026-04-22 11:40:12 +08:00
MerchantCertificationId = merchantCertId,
2026-02-08 18:09:05 +08:00
CertificateStatus = 1,
GeneratedTime = now,
2026-04-16 15:42:29 +08:00
CertificateNo = $"CC{now:yyyyMMdd}{actualPersonId}",
CreatedTime = now,
2026-04-15 14:28:22 +08:00
TemplateId = templateId,
2026-04-15 15:14:52 +08:00
CertificateUrl = string.Empty
2026-02-08 18:09:05 +08:00
};
certsToInsert.Add(cert);
2026-04-15 15:14:52 +08:00
isNewCert = true;
}
// 构建占位符数据(教练/领队没有成绩记录)
2026-04-22 11:40:12 +08:00
var placeholderData = BuildPlaceholderDataForCoachLeader(person.Person);
2026-04-15 15:14:52 +08:00
// 生成 PDF 证书
try
{
var generateResult = await _certificateGenerateService.GeneratePdf(
new YD_Event.Application.Certificate.Dtos.GeneratePdfCertificateInput
{
CertificateId = certificate.Id,
TemplateId = templateId,
Data = placeholderData,
GenerateQRCode = true,
QRCodeSize = 150
});
if (generateResult.IsSuccess && !string.IsNullOrEmpty(generateResult.PdfUrl))
{
cert.CertificateUrl = generateResult.PdfUrl;
cert.CertificateStatus = 1; // 已生成
cert.GeneratedTime = now;
_logger.LogInformation("PDF证书生成成功: EventCertificateId={Id}, Url={Url}", cert.Id, generateResult.PdfUrl);
}
else
{
_logger.LogWarning("PDF证书生成失败: {Message}", generateResult.ErrorMessage);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "PDF证书生成异常: EventCertificateId={Id}", cert.Id);
2026-02-08 18:09:05 +08:00
}
}
// 更新证书状态为已发放
2026-04-15 15:14:52 +08:00
cert.CertificateStatus = 2; // 已发放
cert.IssuedTime = now;
cert.UpdatedTime = now;
2026-04-15 14:28:22 +08:00
// 确保 CertificateUrl 不为 null
if (string.IsNullOrEmpty(cert.CertificateUrl))
{
cert.CertificateUrl = string.Empty;
}
2026-04-15 15:14:52 +08:00
if (!isNewCert)
{
certsToUpdate.Add(cert);
}
result.SuccessCount++;
}
if (certsToInsert.Any())
{
await _certificateRepository.InsertRangeAsync(certsToInsert);
}
// 更新已有证书的发放状态
2026-04-15 15:14:52 +08:00
if (certsToUpdate.Any())
{
2026-04-15 15:14:52 +08:00
await _certificateRepository.UpdateRangeAsync(certsToUpdate);
2026-02-08 18:09:05 +08:00
}
_logger.LogInformation("发放证书完成:成功 {SuccessCount} 条,失败 {FailCount} 条",
result.SuccessCount, result.FailCount);
return result;
2026-02-08 18:09:05 +08:00
}
2026-04-15 15:14:52 +08:00
2026-04-16 15:42:29 +08:00
/// <summary>
/// 根据页面类型获取允许的人员类型列表
/// </summary>
2026-04-17 09:19:21 +08:00
/// <param name="pageType">页面类型:1-运动员页面,2-教练页面,3-领队页面</param>
2026-04-16 15:42:29 +08:00
/// <returns>允许的人员类型列表</returns>
2026-04-17 09:19:21 +08:00
private List<PersonType> GetAllowedPersonTypes(int pageType)
2026-04-16 15:42:29 +08:00
{
return pageType switch
{
2026-04-17 09:19:21 +08:00
1 => new List<PersonType> { PersonType.Athlete, PersonType.User },
2 => new List<PersonType> { PersonType.Coach },
3 => new List<PersonType> { PersonType.Leader },
2026-04-16 15:42:29 +08:00
_ => new List<PersonType> { PersonType.Athlete, PersonType.User, PersonType.Coach, PersonType.Leader }
};
}
2026-04-15 15:14:52 +08:00
/// <summary>
/// 构建运动员占位符数据
/// </summary>
2026-04-22 11:40:12 +08:00
private Dictionary<string, string> BuildPlaceholderData(EventRegistrationPerson person, EventResult result, string combinedNames = null)
2026-04-15 15:14:52 +08:00
{
2026-04-22 11:40:12 +08:00
var displayName = string.IsNullOrEmpty(combinedNames) ? person.Name : combinedNames;
2026-04-15 15:14:52 +08:00
var data = new Dictionary<string, string>
{
2026-04-22 11:40:12 +08:00
["name"] = displayName,
["姓名"] = displayName,
2026-04-16 02:56:50 +08:00
//["group"] = person.GroupName,
//["组别"] = person.GroupName,
["项目组别"] = $"{person.ProjectName}-{person.GroupName}",
//["project"] = result.ProjectTypeName,
//["项目"] = result.ProjectTypeName,
2026-04-15 15:14:52 +08:00
["score"] = result.Score,
["成绩"] = result.Score,
["rank"] = result.Rank.ToString(),
["名次"] = GetRankText(result.Rank),
["danwei"] = person.ProjectName ?? "个人",
2026-04-16 02:56:50 +08:00
["单位"] = "上海跃动文化科技有限公司",
["参赛单位"] = "上海跃动文化科技有限公司"
2026-04-15 15:14:52 +08:00
};
2026-04-16 02:56:50 +08:00
2026-04-15 15:14:52 +08:00
// 添加日期信息
var now = DateTime.Now;
data["year"] = now.Year.ToString();
data["month"] = now.Month.ToString();
data["day"] = now.Day.ToString();
data["date"] = now.ToString("yyyy年MM月dd日");
_logger.LogDebug("构建占位符数据: {Data}", string.Join(", ", data.Select(kv => $"{kv.Key}={kv.Value}")));
return data;
}
/// <summary>
/// 构建教练/领队占位符数据
/// </summary>
private Dictionary<string, string> BuildPlaceholderDataForCoachLeader(EventRegistrationPerson person)
{
var data = new Dictionary<string, string>
{
["name"] = person.Name,
["姓名"] = person.Name,
["group"] = person.GroupName,
["组别"] = person.GroupName,
["项目组别"] = person.GroupName,
["danwei"] = person.ProjectName ?? "个人",
["单位"] = person.ProjectName ?? "个人",
["参赛单位"] = person.ProjectName ?? "个人"
};
// 添加日期信息
var now = DateTime.Now;
data["year"] = now.Year.ToString();
data["month"] = now.Month.ToString();
data["day"] = now.Day.ToString();
data["date"] = now.ToString("yyyy年MM月dd日");
_logger.LogDebug("构建教练/领队占位符数据: {Data}", string.Join(", ", data.Select(kv => $"{kv.Key}={kv.Value}")));
return data;
}
/// <summary>
/// 获取名次文本
/// </summary>
private string GetRankText(int rank)
{
if (rank <= 0) return "";
var chineseNumbers = new[] { "零", "一", "二", "三", "四", "五", "六", "七", "八", "九", "十" };
if (rank <= 10)
{
return $"第{chineseNumbers[rank]}名";
}
else if (rank < 20)
{
return $"第十{chineseNumbers[rank % 10]}名";
}
else if (rank < 100)
{
var tens = rank / 10;
var ones = rank % 10;
if (ones == 0)
{
return $"第{chineseNumbers[tens]}十名";
}
return $"第{chineseNumbers[tens]}十{chineseNumbers[ones]}名";
}
return $"第{rank}名";
}
2026-02-08 18:09:05 +08:00
2026-03-19 23:36:06 +08:00
/// <summary>
/// 查看证书接口
/// </summary>
2026-04-14 15:11:41 +08:00
/// <param name="input">输入参数</param>
2026-03-19 23:36:06 +08:00
/// <returns>证书详情</returns>
[DisplayName("查看证书")]
2026-04-16 15:42:29 +08:00
[HttpPost("getcertificatedetail")]
public async Task<object> GetCertificateDetail([FromBody] YD_Event.Application.Certificate.Dtos.IdInput input)
2026-03-19 23:36:06 +08:00
{
2026-04-14 15:11:41 +08:00
if (input.Id <= 0)
2026-03-19 23:36:06 +08:00
{
2026-04-14 15:11:41 +08:00
throw Oops.Bah("无效的 ID");
2026-03-19 23:36:06 +08:00
}
2026-04-20 13:01:39 +08:00
// 首先尝试直接通过 ParticipationRecordId(EventRegistrationPerson.Id)查找证书
var cert = await _certificateRepository.GetFirstAsync(x => x.ParticipationRecordId == input.Id);
// 如果找不到,则尝试通过 UserId 查找对应的 EventRegistrationPerson,再查找证书
if (cert == null)
2026-03-19 23:36:06 +08:00
{
2026-04-20 13:01:39 +08:00
_logger.LogInformation("未通过 ParticipationRecordId={Id} 找到证书,尝试通过 UserId 查找", input.Id);
// 查找任意一个与该 UserId 关联的 EventRegistrationPerson 记录
var person = await _personRepository.GetFirstAsync(x => x.UserId == input.Id && !x.DeleteMark);
if (person != null)
{
_logger.LogInformation("找到 UserId={UserId} 对应的 EventRegistrationPerson.Id={PersonId}", input.Id, person.Id);
// 用找到的 EventRegistrationPerson.Id 再次尝试查找证书
cert = await _certificateRepository.GetFirstAsync(x => x.ParticipationRecordId == person.Id);
}
2026-03-19 23:36:06 +08:00
}
2026-04-20 13:01:39 +08:00
2026-03-19 23:36:06 +08:00
if (cert == null)
{
2026-04-14 15:11:41 +08:00
throw Oops.Bah("未找到对应的证书信息");
2026-03-19 23:36:06 +08:00
}
2026-04-16 15:42:29 +08:00
return new
{
Id = cert.Id,
CertificateNo = cert.CertificateNo,
CertificateUrl = cert.CertificateUrl,
CertificateStatus = cert.CertificateStatus,
IssuedTime = cert.IssuedTime,
ParticipantName = cert.ParticipantName,
GroupName = cert.GroupName
};
2026-03-19 23:36:06 +08:00
}
/// <summary>
/// 验证证书真伪
/// </summary>
[DisplayName("验证证书真伪")]
public async Task<object> 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
};
}
2026-02-08 18:09:05 +08:00
/// <summary>
/// 下载模板
/// </summary>
2026-03-19 23:36:06 +08:00
[DisplayName("下载模板")]
[HttpGet]
public IActionResult DownloadTemplate()
2026-02-08 18:09:05 +08:00
{
2026-04-14 15:11:41 +08:00
var exporter = new ScoreTemplateExporter();
var bytes = exporter.ExportAsync().GetAwaiter().GetResult();
return new FileContentResult(bytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
{
FileDownloadName = "成绩导入模板.xlsx"
2026-03-19 23:36:06 +08:00
};
2026-04-14 15:11:41 +08:00
}
/// <summary>
/// 成绩模板导出器
/// </summary>
private class ScoreTemplateExporter : ExcelExportBase
{
public override async Task<byte[]> ExportAsync()
2026-03-19 23:36:06 +08:00
{
2026-04-14 15:11:41 +08:00
CreateWorksheet("成绩导入模板");
var headers = new string[] {
2026-04-22 14:47:50 +08:00
"报名ID",
2026-04-14 15:11:41 +08:00
"赛事地址",
"项目",
"组别",
"姓名",
"联系方式",
"单位",
"成绩",
"名次(汉字)",
"名次(数字)"
};
var headerColor = Color.FromArgb(204, 255, 204);
SetHeaders(headers, 1, headerColor, 11);
2026-03-19 23:36:06 +08:00
2026-04-21 23:56:08 +08:00
// 设置第1列为文本格式,防止科学计数法显示
2026-04-22 14:47:50 +08:00
// 只设置前20行的格式,避免MiniExcel读取过多空行
for (int r = 2; r <= 20; r++)
2026-04-21 23:56:08 +08:00
{
Worksheet.Cells[r, 1].Style.Numberformat.Format = "@";
}
var widths = new double[] { 12, 15, 15, 15, 12, 15, 20, 15, 15, 15 };
2026-04-14 15:11:41 +08:00
SetColumnWidths(widths);
2026-03-19 23:36:06 +08:00
2026-04-14 15:11:41 +08:00
SetRowHeight(1, 25);
return await GetAsByteArrayAsync();
}
2026-02-08 18:09:05 +08:00
}
2026-03-08 15:46:19 +08:00
/// <summary>
/// 导出证书
/// </summary>
[DisplayName("导出证书")]
2026-04-16 15:42:29 +08:00
[HttpGet]
public async Task<IActionResult> ExportCertificate([FromQuery] ScoreQueryInput input)
2026-03-08 15:46:19 +08:00
{
var list = await _certificateRepository.AsQueryable()
.LeftJoin<EventResult>((c, r) => c.ResultId == r.Id)
.LeftJoin<EventRegistration>((c, r, reg) => c.RegistrationId == reg.Id)
.LeftJoin<YD_Event.Core.Entities.Project>((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();
2026-04-16 15:42:29 +08:00
await stream.SaveAsAsync(list);
var bytes = stream.ToArray();
return new FileContentResult(bytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
{
FileDownloadName = $"证书列表_{DateTime.Now:yyyyMMddHHmmss}.xlsx"
};
2026-03-08 15:46:19 +08:00
}
/// <summary>
/// 导入成绩验证结果
/// </summary>
public class ImportScoreValidationResult
{
public bool IsValid { get; set; }
public List<string> Errors { get; set; } = new List<string>();
public List<ScoreImportDto> ValidRows { get; set; } = new List<ScoreImportDto>();
}
/// <summary>
/// 验证导入数据
/// </summary>
private async Task<ImportScoreValidationResult> ValidateImportDataAsync(List<ScoreImportDto> rows, long eventId)
{
var result = new ImportScoreValidationResult();
var errors = new List<string>();
2026-04-22 14:47:50 +08:00
var maxErrorsToShow = 20; // 限制最多显示20个错误,避免错误信息过长
for (int i = 0; i < rows.Count; i++)
{
var row = rows[i];
var rowErrors = new List<string>();
var rowNumber = i + 2;
2026-04-22 14:47:50 +08:00
if (!row.RegistrationItemId.HasValue || row.RegistrationItemId.Value <= 0)
rowErrors.Add("报名ID不能为空且必须为有效的数字");
if (string.IsNullOrWhiteSpace(row.Score))
rowErrors.Add("成绩不能为空");
if (row.Rank <= 0)
rowErrors.Add("名次必须为正整数");
if (rowErrors.Any())
{
2026-04-22 14:47:50 +08:00
if (errors.Count < maxErrorsToShow)
{
errors.Add($"第 {rowNumber} 行:{string.Join(",", rowErrors)}");
}
else if (errors.Count == maxErrorsToShow)
{
errors.Add($"... 还有 {rows.Count - maxErrorsToShow} 条错误未显示");
}
}
else
{
result.ValidRows.Add(row);
}
}
if (errors.Any())
{
result.IsValid = false;
result.Errors = errors;
}
else
{
result.IsValid = true;
}
return result;
}
2026-04-14 15:11:41 +08:00
/// <summary>
/// 发放积分输入模型
/// </summary>
public class IssuePointsInput
{
public long EventId { get; set; }
}
/// <summary>
/// 发放积分
/// </summary>
[DisplayName("发放积分")]
[HttpPost("issuePoints")]
[UnitOfWork]
public async Task<object> 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<EventRegistrationPerson>((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,
2026-04-22 11:40:12 +08:00
ProjectIdForRecord = r.ProjectId,
RegistrationId = p.RegistrationId
2026-04-14 15:11:41 +08:00
})
.ToListAsync();
if (!results.Any())
{
throw Oops.Bah("没有可发放积分的成绩记录");
}
2026-04-22 11:40:12 +08:00
// 批量获取所有 RegistrationId 对应的 EventRegistration(包含归属单位 ID)
var registrationIdsList = results.Select(r => r.RegistrationId).Distinct().ToList();
var registrations = await _registrationRepository.GetListAsync(r => registrationIdsList.Contains(r.Id));
var registrationDict = registrations.ToDictionary(r => r.Id);
2026-04-15 09:58:05 +08:00
var processedResultIdsList = await _pointsRecordRepository.AsQueryable()
.Where(x => x.EventId == input.EventId && x.ResultId.HasValue)
.Select(x => x.ResultId.Value)
2026-04-14 15:11:41 +08:00
.ToListAsync();
2026-04-15 09:58:05 +08:00
var processedResultIds = new HashSet<long>(processedResultIdsList);
2026-04-14 15:11:41 +08:00
int successCount = 0;
int skipCount = 0;
var pointsRecords = new List<PointsRecord>();
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)
{
2026-04-22 11:40:12 +08:00
// 从 EventRegistration 获取归属单位 ID
long? merchantCertId = null;
if (registrationDict.TryGetValue(result.RegistrationId, out var reg))
{
merchantCertId = reg.MerchantCertificationId;
}
2026-04-14 15:11:41 +08:00
var record = new PointsRecord
{
Id = _idGenerator.NextId(),
UserId = result.UserId,
Type = PointsOperationType.Acquire,
EventId = input.EventId,
ProjectId = result.ProjectIdForRecord,
2026-04-15 09:58:05 +08:00
ResultId = result.ResultId,
2026-04-22 11:40:12 +08:00
MerchantCertificationId = merchantCertId,
2026-04-14 15:11:41 +08:00
Points = pointsToAward,
2026-04-15 09:58:05 +08:00
Remark = eventInfo.EventName,
2026-04-14 15:11:41 +08:00
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} 条已发放记录"
};
}
2026-02-08 18:09:05 +08:00
}