2026-05-18 11:49:41 +08:00

1905 lines
82 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using YD_Event.Application.Event.Dtos;
using YD_Event.Application.Certificate;
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;
using static SKIT.FlurlHttpClient.Wechat.TenpayV3.Models.DepositMarketingMemberCardOpenCardCodesResponse.Types;
using YD_Event.Application.AppUser.Dtos;
namespace YD_Event.Application.Event;
/// <summary>
/// 成绩与证书管理
/// </summary>
public class EventScoreService : BaseService<EventResult>, ITransient
{
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]}名";
}
private readonly ISqlSugarRepository<EventRegistrationPerson> _personRepository;
private readonly ISqlSugarRepository<EventResult> _resultRepository;
private readonly ISqlSugarRepository<EventCertificate> _certificateRepository;
private readonly ISqlSugarRepository<EventRegistration> _registrationRepository;
private readonly ISqlSugarRepository<EventRegistrationItem> _itemRepository;
private readonly ISqlSugarRepository<CertificateTemplate> _templateRepository;
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Certificate> _certificateMasterRepository;
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Project> _projectRepository;
private readonly ISqlSugarRepository<EventProject> _eventProjectRepository;
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;
private readonly ISqlSugarRepository<MerchantCertification> _merchantCertificationRepository;
private readonly CertificateGenerateService _certificateGenerateService;
private readonly IIdGenerator _idGenerator;
private readonly ILogger<EventScoreService> _logger;
private readonly EventRuleSnapshotService _ruleSnapshotService;
public EventScoreService(
ISqlSugarRepository<EventRegistrationPerson> personRepository,
ISqlSugarRepository<EventResult> resultRepository,
ISqlSugarRepository<EventCertificate> certificateRepository,
ISqlSugarRepository<EventRegistration> registrationRepository,
ISqlSugarRepository<EventRegistrationItem> itemRepository,
ISqlSugarRepository<CertificateTemplate> templateRepository,
ISqlSugarRepository<YD_Event.Core.Entities.Certificate> certificateMasterRepository,
ISqlSugarRepository<YD_Event.Core.Entities.Project> projectRepository,
ISqlSugarRepository<EventProject> eventProjectRepository,
ISqlSugarRepository<TeamMember> teamMemberRepository,
ISqlSugarRepository<EventInfo> eventRepository,
ISqlSugarRepository<PointsRule> pointsRuleRepository,
ISqlSugarRepository<PointsRuleDetail> pointsRuleDetailRepository,
ISqlSugarRepository<PointsRecord> pointsRecordRepository,
ISqlSugarRepository<YD_Event.Core.Entities.AppUser> appUserRepository,
ISqlSugarRepository<MerchantCertification> merchantCertificationRepository,
CertificateGenerateService certificateGenerateService,
IIdGenerator idGenerator,
ILogger<EventScoreService> logger,
EventRuleSnapshotService ruleSnapshotService) : base(resultRepository)
{
_personRepository = personRepository;
_resultRepository = resultRepository;
_certificateRepository = certificateRepository;
_registrationRepository = registrationRepository;
_itemRepository = itemRepository;
_templateRepository = templateRepository;
_certificateMasterRepository = certificateMasterRepository;
_projectRepository = projectRepository;
_eventProjectRepository = eventProjectRepository;
_teamMemberRepository = teamMemberRepository;
_eventRepository = eventRepository;
_pointsRuleRepository = pointsRuleRepository;
_pointsRuleDetailRepository = pointsRuleDetailRepository;
_pointsRecordRepository = pointsRecordRepository;
_appUserRepository = appUserRepository;
_merchantCertificationRepository = merchantCertificationRepository;
_certificateGenerateService = certificateGenerateService;
_idGenerator = idGenerator;
_logger = logger;
_ruleSnapshotService = ruleSnapshotService;
}
/// <summary>
/// 运动员成绩列表
/// </summary>
[DisplayName("运动员成绩列表")]
public async Task<PageResult<ScoreListOutput>> PageAthlete(ScoreQueryInput input)
{
_logger.LogInformation("开始查询运动员成绩列表,赛事ID={EventId}, 项目ID={ProjectId}, 赛事项目组别ID={EventProjectGroupId}, 组别名称={GroupName}",
input.EventId, input.ProjectId, input.EventProjectGroupId, input.GroupName);
try
{
// 步骤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
// 线上支付只显示已支付(1),线下支付显示待审核(0)和已支付(1)
&& ((r.PaymentMethod == 2 && r.PaymentStatus == 1) || (r.PaymentMethod == 1 && (r.PaymentStatus == 0 || r.PaymentStatus == 1)))
&& (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);
}
// 步骤1.5:获取所有相关的单位认证信息
var merchantCertIds = new List<long>();
// 先查找所有可能的 MerchantCertificationId
var registrationIds = await _registrationRepository.AsQueryable()
.Where(r => r.EventId == input.EventId
// 线上支付只显示已支付(1),线下支付显示待审核(0)和已支付(1)
&& ((r.PaymentMethod == 2 && r.PaymentStatus == 1) || (r.PaymentMethod == 1 && (r.PaymentStatus == 0 || r.PaymentStatus == 1))))
.Select(r => r.MerchantCertificationId)
.ToListAsync();
merchantCertIds.AddRange(registrationIds.Where(id => id.HasValue).Select(id => id.Value).Distinct());
// 获取单位认证信息字典
Dictionary<long, string> merchantNameDict = new Dictionary<long, string>();
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 baseData = await baseQuery
.OrderBy((p, r, res, c) => res.Rank)
.Select((p, r, res, c) => new
{
Id = p.Id,
Name = p.Name,
RegistrationId = r.Id, // 保存报名ID用于后续查找单位名称
MerchantCertificationId = r.MerchantCertificationId, // 保存单位认证ID
UnitName = r.TeamName ?? string.Empty, // 暂时保留TeamName作为后备
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();
// 步骤3:进行分组处理(与赛事编排页面保持一致)
var processedData = new List<ScoreListOutput>();
// 先按 EventProjectId 和 GroupName 分组
var eventProjectGroups = baseData.GroupBy(x => new { x.EventProjectId, x.GroupName });
foreach (var eventProjectGroup in eventProjectGroups)
{
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));
// 优先选择有成绩的人(Rank>0),然后按Rank排序
var representative = teamItems
.OrderByDescending(x => x.Rank > 0) // 有成绩的排在前面
.ThenBy(x => x.Rank > 0 ? x.Rank : int.MaxValue) // 有成绩的按Rank排序
.First();
// 获取正确的单位名称
string unitName = string.Empty;
if (representative.MerchantCertificationId.HasValue &&
merchantNameDict.TryGetValue(representative.MerchantCertificationId.Value, out var merchantName))
{
unitName = merchantName;
}
processedData.Add(new ScoreListOutput
{
Id = representative.Id,
Name = names,
UnitName = 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)
{
// 获取正确的单位名称
string unitName = string.Empty;
if (item.MerchantCertificationId.HasValue &&
merchantNameDict.TryGetValue(item.MerchantCertificationId.Value, out var merchantName))
{
unitName = merchantName;
}
processedData.Add(new ScoreListOutput
{
Id = item.Id,
Name = item.Name,
UnitName = 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)
{
_logger.LogError(ex, "查询运动员成绩列表失败");
throw;
}
}
/// <summary>
/// 教练/领队列表
/// </summary>
/// <remarks>
/// 通过运动员报名记录 -> TeamMember团队关联 -> 找到同团队的教练/领队
/// </remarks>
[DisplayName("教练领队列表")]
public async Task<PageResult<CoachLeaderListOutput>> PageCoachLeader(CoachLeaderQueryInput input)
{
_logger.LogInformation("开始查询教练/领队列表,赛事ID={EventId}, 人员类型={PersonType}",
input.EventId, input.PersonType);
// 先从 EventRegistrationPerson 查找,找不到则通过 TeamMember 关联查找
// 步骤1:获取该赛事的所有运动员的 UserId
var athleteUserIds = await _personRepository.AsQueryable()
.InnerJoin<EventRegistration>((p, r) => p.RegistrationId == r.Id)
.Where((p, r) => r.EventId == input.EventId
// 线上支付只显示已支付(1),线下支付显示待审核(0)和已支付(1)
&& ((r.PaymentMethod == 2 && r.PaymentStatus == 1) || (r.PaymentMethod == 1 && (r.PaymentStatus == 0 || r.PaymentStatus == 1)))
&& p.PersonType == PersonType.Athlete && !p.DeleteMark)
.Select(p => p.UserId)
.ToListAsync();
if (!athleteUserIds.Any())
{
_logger.LogWarning("该赛事没有运动员报名记录");
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())
{
_logger.LogWarning("未找到关联的团队信息");
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())
{
_logger.LogWarning("该团队中没有找到指定类型的人员");
return new PageResult<CoachLeaderListOutput>();
}
// 步骤4:首先尝试从该赛事的 EventRegistrationPerson 中找到这些教练/领队
// 这样可以获取到 EventRegistrationPerson.Id 用于证书发放
var existingPersons = await _personRepository.AsQueryable()
.InnerJoin<EventRegistration>((p, r) => p.RegistrationId == r.Id)
.Where((p, r) => r.EventId == input.EventId
// 线上支付只显示已支付(1),线下支付显示待审核(0)和已支付(1)
&& ((r.PaymentMethod == 2 && r.PaymentStatus == 1) || (r.PaymentMethod == 1 && (r.PaymentStatus == 0 || r.PaymentStatus == 1)))
&& coachLeaderUserIds.Contains(p.UserId) && p.PersonType == input.PersonType && !p.DeleteMark)
.LeftJoin<YD_Event.Core.Entities.AppUser>((p, r, u) => p.UserId == u.Id)
.Select((p, r, u) => new
{
PersonId = p.Id,
UserId = p.UserId,
Name = p.Name,
Age = p.Age,
Gender = (int)p.Gender,
BirthDate = p.BirthDate,
IdCardNumber = p.IdCard,
IdCardImageUrl = u.IdCardImageUrl,
IdCardBackImageUrl = u.IdCardBackImageUrl,
Mobile = p.Phone,
HasPersonRecord = true
})
.ToListAsync();
// 步骤5:找出在 EventRegistrationPerson 中没有记录的教练/领队
var foundUserIds = existingPersons.Select(x => x.UserId).ToList();
var missingUserIds = coachLeaderUserIds.Except(foundUserIds).ToList();
var allResults = existingPersons.Select(x => new CoachLeaderListOutput
{
Id = x.PersonId,
UserId = x.UserId,
Name = x.Name,
Age = x.Age,
Gender = x.Gender,
BirthDate = x.BirthDate,
IdCardNumber = x.IdCardNumber,
IdCardImageUrl = x.IdCardImageUrl,
IdCardBackImageUrl = x.IdCardBackImageUrl,
Mobile = x.Mobile,
CertificateId = 0
}).ToList();
// 步骤6:如果有教练/领队没有 EventRegistrationPerson 记录,则从 AppUser 获取基本信息
// 注意:这种情况下 Id = -UserId(负数),表示没有 EventRegistrationPerson 记录,需要后续处理
if (missingUserIds.Any())
{
var appUsers = await _appUserRepository.AsQueryable()
.Where(u => missingUserIds.Contains(u.Id) && !u.DeleteMark)
.Select(u => new CoachLeaderListOutput
{
Id = -u.Id, // 使用 -UserId 表示没有 EventRegistrationPerson 记录
UserId = u.Id,
Name = u.Name ?? string.Empty,
Age = u.Age,
Gender = (int)u.Gender,
BirthDate = u.BirthDate,
IdCardNumber = u.IdCardNumber,
IdCardImageUrl = u.IdCardImageUrl,
IdCardBackImageUrl = u.IdCardBackImageUrl,
Mobile = u.Mobile,
CertificateId = 0
})
.ToListAsync();
allResults.AddRange(appUsers);
_logger.LogInformation("找到 {Count} 名有报名记录的人员,{MissingCount} 名只有团队关联的人员",
existingPersons.Count, appUsers.Count);
}
// 应用筛选条件(在内存中筛选)
if (!string.IsNullOrWhiteSpace(input.Name))
{
allResults = allResults.Where(x => x.Name.Contains(input.Name)).ToList();
}
if (!string.IsNullOrWhiteSpace(input.Mobile))
{
allResults = allResults.Where(x => x.Mobile.Contains(input.Mobile)).ToList();
}
if (input.Gender.HasValue)
{
allResults = allResults.Where(x => x.Gender == (int)input.Gender.Value).ToList();
}
// 手动分页
var total = allResults.Count;
var pageSize = input.PageSize > 0 ? input.PageSize : 10;
var pageNo = input.PageNo > 0 ? input.PageNo - 1 : 0;
var pagedData = allResults.Skip(pageNo * pageSize).Take(pageSize).ToList();
// 获取所有有 EventRegistrationPerson 记录的证书信息
var personIds = pagedData.Where(x => x.Id > 0).Select(x => x.Id).ToList();
if (personIds.Any())
{
var certs = await _certificateRepository.GetListAsync(x =>
personIds.Contains(x.ParticipationRecordId) && !x.DeleteMark);
var certDict = certs.ToDictionary(c => c.ParticipationRecordId);
foreach (var item in pagedData)
{
if (item.Id > 0 && certDict.TryGetValue(item.Id, out var cert))
{
item.CertificateId = cert.Id;
}
}
}
var pageResult = new PageResult<CoachLeaderListOutput>
{
PageNo = input.PageNo,
PageSize = pageSize,
Total = total,
Pages = (int)Math.Ceiling((double)total / pageSize),
Rows = pagedData
};
_logger.LogInformation("查询教练/领队列表完成,共 {Count} 条记录", total);
return pageResult;
}
/// <summary>
/// 导入成绩(性能优化版)
/// </summary>
/// <remarks>
/// 性能优化策略:
/// 1. 批量预加载所有人员数据到内存
/// 2. 使用字典建立索引,避免循环内数据库查询
/// 3. 批量收集需要插入/更新的记录
/// 4. 最后一次性执行批量操作
/// 性能指标:导入10000条成绩数据时,响应时间 < 30秒,内存占用 < 512MB
/// 团队项目支持:团队项目按团队维度导入,一个团队对应一个完整证书
/// </remarks>
[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 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);
if (!rows.Any())
{
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}");
}
// 步骤1:获取赛事的所有 EventProject 信息
var eventProjects = await _eventProjectRepository.GetListAsync(x => x.EventId == eventId);
var eventProjectDictToProjectId = eventProjects.ToDictionary(x => x.Id, x => x.ProjectId);
// 步骤2:批量预加载该赛事的所有人员数据(包含团队信息)
var allPersonsWithTeam = await _personRepository.AsQueryable()
.InnerJoin<EventRegistration>((p, r) => p.RegistrationId == r.Id)
.Where((p, r) => r.EventId == eventId
// 线上支付只显示已支付(1),线下支付显示待审核(0)和已支付(1)
&& ((r.PaymentMethod == 2 && r.PaymentStatus == 1) || (r.PaymentMethod == 1 && (r.PaymentStatus == 0 || r.PaymentStatus == 1))))
.Select((p, r) => new {
p.Id,
p.RegistrationItemId,
p.Name,
p.ProjectName,
p.GroupName,
p.RegistrationId,
p.ProjectId,
r.TeamName
})
.ToListAsync();
_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);
// 步骤4:使用字典建立人员索引
// 1. 按 EventRegistrationPerson.Id 索引(用于个人项目)
var personByIdDict = allPersonsWithTeam
.ToDictionary(p => p.Id, p => p);
// 2. 按 RegistrationItemId 索引(用于团队项目)
var personByItemIdDict = allPersonsWithTeam
.GroupBy(p => p.RegistrationItemId)
.ToDictionary(g => g.Key, g => g.First());
// 同时预加载所有 EventRegistrationItem 信息
var registrationItemIds = allPersonsWithTeam.Select(p => p.RegistrationItemId).Distinct().ToList();
var registrationItems = await _itemRepository.GetListAsync(x => registrationItemIds.Contains(x.Id));
var registrationItemDict = registrationItems.ToDictionary(x => x.Id, x => x);
// 预加载 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);
// 步骤5:预加载该赛事的所有成绩记录
var personIds = allPersonsWithTeam.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);
// 步骤6:批量处理数据,收集需要插入/更新的记录
var resultsToInsert = new List<EventResult>();
var resultsToUpdate = new List<EventResult>();
var now = DateTime.Now;
var notFoundPersons = new List<string>();
foreach(var row in validationResult.ValidRows)
{
// 首先尝试用 ID 查找 EventRegistrationPerson(用于个人项目)
if (personByIdDict.TryGetValue(row.RegistrationItemId.Value, out var person))
{
// 找到了个人项目的记录,继续处理
}
// 如果没找到,再尝试用 RegistrationItemId 查找(用于团队项目)
else if (personByItemIdDict.TryGetValue(row.RegistrationItemId.Value, out person))
{
// 找到了团队项目的记录,继续处理
}
else
{
notFoundPersons.Add($"报名ID:{row.RegistrationItemId.Value}");
continue;
}
// 获取项目信息,判断是否为团队项目
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)
{
// 查找该报名项目的所有成员
teamMembers = allPersonsWithTeam
.Where(p => p.RegistrationItemId == person.RegistrationItemId)
.OrderBy(p => (string)p.Name)
.ToList<dynamic>();
// 收集所有姓名
var namesList = new List<string>();
foreach (var member in teamMembers)
{
namesList.Add((string)member.Name);
}
combinedNames = string.Join(", ", namesList);
}
// 处理成绩记录
if (isTeamProject && teamMembers != null && teamMembers.Count > 0)
{
// 团队项目:为团队中的所有成员创建/更新成绩记录
foreach (var teamMember in teamMembers)
{
var memberId = (long)teamMember.Id;
var memberRegistrationId = (long)teamMember.RegistrationId;
if (resultDict.TryGetValue(memberId, out var existingResult))
{
// 更新现有记录
existingResult.Score = row.Score;
existingResult.Rank = row.Rank;
existingResult.Remark = row.RankText;
existingResult.ProjectTypeName = projectTypeName;
existingResult.ParticipantName = combinedNames;
existingResult.UpdatedTime = now;
resultsToUpdate.Add(existingResult);
}
else
{
// 从 EventRegistration 获取归属单位 ID
long? merchantCertId = null;
if (registrationDict.TryGetValue(memberRegistrationId, out var reg))
{
merchantCertId = reg.MerchantCertificationId;
}
// 插入新记录
var newResult = new EventResult
{
Id = _idGenerator.NewLong(),
EventId = eventId,
RegistrationPersonId = memberId,
RegistrationId = memberRegistrationId,
ProjectId = person.ProjectId,
ParticipantName = combinedNames,
GroupName = person.GroupName,
MerchantCertificationId = merchantCertId,
ProjectTypeName = projectTypeName,
Score = row.Score,
Rank = row.Rank,
Remark = row.RankText,
CreatedTime = now,
UpdatedTime = now
};
resultsToInsert.Add(newResult);
}
}
}
else
{
// 个人项目:只处理当前这个人
if (resultDict.TryGetValue(person.Id, out var existingResult))
{
// 更新现有记录
existingResult.Score = row.Score;
existingResult.Rank = row.Rank;
existingResult.Remark = row.RankText;
existingResult.ProjectTypeName = projectTypeName;
existingResult.UpdatedTime = now;
resultsToUpdate.Add(existingResult);
}
else
{
// 从 EventRegistration 获取归属单位 ID
long? merchantCertId = null;
if (registrationDict.TryGetValue(person.RegistrationId, out var reg))
{
merchantCertId = reg.MerchantCertificationId;
}
// 插入新记录
var newResult = new EventResult
{
Id = _idGenerator.NewLong(),
EventId = eventId,
RegistrationPersonId = person.Id,
RegistrationId = person.RegistrationId,
ProjectId = person.ProjectId,
ParticipantName = person.Name,
GroupName = person.GroupName,
MerchantCertificationId = merchantCertId,
ProjectTypeName = projectTypeName,
Score = row.Score,
Rank = row.Rank,
Remark = row.RankText,
CreatedTime = now,
UpdatedTime = now
};
resultsToInsert.Add(newResult);
}
}
}
if (notFoundPersons.Any())
{
_logger.LogWarning("未找到匹配的参赛人员:{Count} 条,未找到的报名ID:{Ids}",
notFoundPersons.Count, string.Join(", ", notFoundPersons));
}
_logger.LogInformation("数据处理完成:新增 {InsertCount} 条,更新 {UpdateCount} 条,未找到 {NotFoundCount} 条",
resultsToInsert.Count, resultsToUpdate.Count, notFoundPersons.Count);
// 步骤7:批量操作数据库
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);
}
/// <summary>
/// 发放证书(支持模板选择和批量发放)
/// </summary>
[UnitOfWork]
[DisplayName("发放证书")]
public async Task<IssueCertificateResult> IssueCertificate(IssueCertificateInput input)
{
var result = new IssueCertificateResult();
var now = DateTime.Now;
// 验证证书是否存在并获取证书信息
var certificate = await _certificateMasterRepository.GetFirstAsync(x =>
x.Id == input.CertificateId &&
x.Status == AvailabilityStatus.Enable &&
!x.DeleteMark);
if (certificate == null)
{
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("证书关联的模板不存在或不可用");
}
// 根据页面类型确定允许的人员类型
List<PersonType> allowedPersonTypes = GetAllowedPersonTypes(input.PageType);
_logger.LogInformation("发放证书 - 页面类型: {PageType}, 允许的人员类型: {AllowedTypes}",
input.PageType, string.Join(", ", allowedPersonTypes));
// 步骤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();
// 预加载所有相关的单位认证数据
var merchantCertIds = allPersonsWithTeam
.Where(p => p.MerchantCertificationId.HasValue)
.Select(p => p.MerchantCertificationId.Value)
.Distinct()
.ToList();
Dictionary<long, string> merchantNameDict = new Dictionary<long, string>();
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);
}
// 处理 PersonIds:将负的 Id 转换为 UserId,然后尝试找到或创建对应的 EventRegistrationPerson
var processedPersonIds = new List<long>();
var userIdToNewPersonId = new Dictionary<long, long>();
// 创建一个简单的人员信息类用于内部存储
var personInfoDict = new Dictionary<long, (long Id, PersonType PersonType, long UserId, string Name, string? ProjectName, string? GroupName, long RegistrationId, long ProjectId, long RegistrationItemId, long? EventProjectGroupId, bool DeleteMark, string? TeamName, long? MerchantCertificationId, EventRegistrationPerson Person)>();
// 从查询结果构建字典
foreach (var p in allPersonsWithTeam)
{
personInfoDict[p.Id] = (p.Id, p.PersonType, p.UserId, p.Name, p.ProjectName, p.GroupName, p.RegistrationId, p.ProjectId, p.RegistrationItemId, p.EventProjectGroupId, p.DeleteMark, p.TeamName, p.MerchantCertificationId, p.Person);
}
foreach (var personId in input.PersonIds)
{
if (personId > 0)
{
// 正常的 EventRegistrationPerson.Id
processedPersonIds.Add(personId);
}
else if (personId < 0)
{
// 负的 Id 表示是 UserId 的负数,需要找到或创建 EventRegistrationPerson
var userId = -personId;
// 先尝试从现有数据中找到该 UserId 在该赛事中的 EventRegistrationPerson
var existingPersonEntry = personInfoDict.Values.FirstOrDefault(p => p.UserId == userId);
if (existingPersonEntry.Id != 0)
{
processedPersonIds.Add(existingPersonEntry.Id);
continue;
}
// 如果找不到,我们需要为这个用户创建 EventRegistration 和 EventRegistrationPerson 记录
_logger.LogInformation($"为 UserId {userId} 创建 EventRegistrationPerson 记录");
// 先找到该用户所在 Team 关联的任意一个运动员的 Registration 信息(或者创建一个新的)
var teamIds = await _teamMemberRepository.AsQueryable()
.Where(tm => tm.UserId == userId && !tm.DeleteMark)
.Select(tm => tm.TeamId)
.Distinct()
.ToListAsync();
if (!teamIds.Any())
{
result.FailCount++;
result.FailDetails.Add($"无法为 UserId {userId} 找到关联的团队信息");
continue;
}
// 找到同一 Team 和同一 Event 中的任意一个运动员的 Registration
var relatedAthleteRegistrations = await _personRepository.AsQueryable()
.InnerJoin<EventRegistration>((p, r) => p.RegistrationId == r.Id)
.InnerJoin<TeamMember>((p, r, tm) => p.UserId == tm.UserId)
.Where((p, r, tm) => r.EventId == input.EventId
&& r.PaymentStatus != 5 // 排除预报名记录
&& teamIds.Contains(tm.TeamId) && !p.DeleteMark)
.Select((p, r) => r)
.ToListAsync();
var relatedAthleteRegistration = relatedAthleteRegistrations.FirstOrDefault();
long registrationId;
long? merchantCertId = null;
if (relatedAthleteRegistration != null)
{
// 复用已有的 Registration
registrationId = relatedAthleteRegistration.Id;
merchantCertId = relatedAthleteRegistration.MerchantCertificationId;
}
else
{
// 创建一个新的 Registration 记录(临时)
var appUser = await _appUserRepository.GetFirstAsync(u => u.Id == userId && !u.DeleteMark);
if (appUser == null)
{
result.FailCount++;
result.FailDetails.Add($"无法找到 UserId {userId} 对应的用户信息");
continue;
}
var newRegistration = new EventRegistration
{
Id = _idGenerator.NewLong(),
EventId = input.EventId,
RegistrationMode = 1, // 个人模式
TeamName = null,
ContactName = appUser.Name ?? "未知",
ContactPhone = appUser.Mobile,
ContactEmail = null,
TotalPersonCount = 1,
TotalAmount = 0,
PaymentStatus = 1,
PaymentMethod = 1,
PaymentMethodType = PaymentMethodType.None,
OrderNo = $"TEMP_{DateTime.Now:yyyyMMddHHmmss}",
PaymentTime = null,
Reviewer = null,
ReviewTime = null,
ReviewRemark = null,
RegistrationRemark = "教练/领队证书发放临时报名记录",
Status = AvailabilityStatus.Enable,
DeleteMark = false,
CreatedUserId = 0,
CreatedTime = DateTime.Now,
UpdatedUserId = null,
UpdatedTime = null
};
await _registrationRepository.InsertAsync(newRegistration);
registrationId = newRegistration.Id;
}
// 获取 AppUser 信息
var appUserInfo = await _appUserRepository.GetFirstAsync(u => u.Id == userId && !u.DeleteMark);
if (appUserInfo == null)
{
result.FailCount++;
result.FailDetails.Add($"无法找到 UserId {userId} 对应的用户信息");
continue;
}
// 创建 EventRegistrationPerson 记录
var newPerson = new EventRegistrationPerson
{
Id = _idGenerator.NewLong(),
RegistrationId = registrationId,
UserId = appUserInfo.Id,
PersonType = input.PageType == 2 ? PersonType.Coach : PersonType.Leader,
Name = appUserInfo.Name ?? "未知",
Gender = appUserInfo.Gender,
BirthDate = appUserInfo.BirthDate ?? DateTime.Now,
Age = appUserInfo.Age ?? 0,
IdCard = appUserInfo.IdCardNumber ?? "",
Phone = appUserInfo.Mobile,
ProjectName = null,
GroupName = "",
ProjectId = 0,
EventProjectGroupId = null,
RegistrationItemId = 0,
DeleteMark = false,
CreatedUserId = 0,
CreatedTime = DateTime.Now,
UpdatedUserId = null,
UpdatedTime = null
};
await _personRepository.InsertAsync(newPerson);
// 添加到映射字典和处理列表
userIdToNewPersonId[userId] = newPerson.Id;
processedPersonIds.Add(newPerson.Id);
// 添加到 allPersonsWithTeam 中,以便后续查找
allPersonsWithTeam.Add(new {
Id = newPerson.Id,
PersonType = newPerson.PersonType,
UserId = newPerson.UserId,
Name = newPerson.Name,
ProjectName = newPerson.ProjectName,
GroupName = newPerson.GroupName,
RegistrationId = newPerson.RegistrationId,
ProjectId = newPerson.ProjectId,
RegistrationItemId = newPerson.RegistrationItemId,
EventProjectGroupId = newPerson.EventProjectGroupId,
DeleteMark = false,
TeamName = (string?)null,
MerchantCertificationId = merchantCertId,
Person = newPerson
});
}
}
// 由于现在 PageCoachLeader 返回的就是 EventRegistrationPerson.Id(或者我们刚刚创建的),我们可以直接使用
var personDict = allPersonsWithTeam
.Where(p => processedPersonIds.Contains(p.Id))
.ToDictionary(p => p.Id);
// 使用最终找到的人员ID列表
var foundPersonIds = personDict.Values.Select(p => (long)p.Id).ToList();
var existingResults = await _resultRepository.GetListAsync(x => foundPersonIds.Contains(x.RegistrationPersonId));
var resultDict = existingResults.ToDictionary(r => r.RegistrationPersonId);
var existingCerts = await _certificateRepository.GetListAsync(x =>
foundPersonIds.Contains(x.ParticipationRecordId));
var certDict = existingCerts.ToDictionary(c => c.ParticipationRecordId);
var certsToInsert = new List<EventCertificate>();
var certsToUpdate = new List<EventCertificate>();
foreach (var personId in processedPersonIds)
{
if (!personDict.TryGetValue(personId, out var person))
{
result.FailCount++;
result.FailDetails.Add($"人员ID {personId} 不存在");
continue;
}
// 验证人员类型是否与当前页面类型匹配
if (!allowedPersonTypes.Contains(person.PersonType))
{
result.FailCount++;
result.FailDetails.Add($"人员 {person.Name} 的类型 ({person.PersonType}) 与当前页面类型不匹配");
continue;
}
// 获取项目信息,判断是否为团队项目(仅对运动员有效)
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)
{
var teamMembers = allPersonsWithTeam
.Where(p => p.RegistrationItemId == person.RegistrationItemId)
.OrderBy(p => (string)p.Name)
.ToList();
var namesList = new List<string>();
foreach (var member in teamMembers)
{
namesList.Add((string)member.Name);
}
combinedNames = string.Join(", ", namesList);
}
}
// 获取实际的 EventRegistrationPerson ID
var actualPersonId = person.Id;
var merchantCertId = person.MerchantCertificationId;
EventCertificate? cert = null;
bool isNewCert = false;
if (person.PersonType == PersonType.Athlete || person.PersonType == PersonType.User)
{
EventResult resultRecord;
if (!resultDict.TryGetValue(actualPersonId, out resultRecord))
{
result.FailCount++;
result.FailDetails.Add($"人员 {person.Name} 没有成绩记录");
continue;
}
EventCertificate existingCert;
if (certDict.TryGetValue(actualPersonId, out existingCert))
{
cert = existingCert;
}
else
{
cert = new EventCertificate
{
Id = _idGenerator.NewLong(),
EventId = input.EventId,
ResultId = resultRecord.Id,
RegistrationId = person.RegistrationId,
ParticipationRecordId = actualPersonId,
ProjectId = person.ProjectId,
UserId = person.UserId,
ParticipantName = combinedNames,
GroupName = person.GroupName,
MerchantCertificationId = merchantCertId,
CertificateStatus = 1,
GeneratedTime = now,
CertificateNo = $"C{now:yyyyMMdd}{actualPersonId}",
CreatedTime = now,
TemplateId = templateId,
CertificateUrl = string.Empty
};
certsToInsert.Add(cert);
isNewCert = true;
}
// 获取单位名称
string unitName = string.Empty;
if (person.MerchantCertificationId.HasValue &&
merchantNameDict.TryGetValue(person.MerchantCertificationId.Value, out var merchantName))
{
unitName = merchantName;
}
// 构建占位符数据(传递合并后的姓名)
var placeholderData = BuildPlaceholderData(person.Person, resultRecord, unitName, combinedNames);
// 生成 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;
cert.ParticipantName = combinedNames; // 更新证书上的姓名
_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);
}
}
else
{
EventCertificate existingCert;
if (certDict.TryGetValue(actualPersonId, out existingCert))
{
cert = existingCert;
}
else
{
cert = new EventCertificate
{
Id = _idGenerator.NewLong(),
EventId = input.EventId,
RegistrationId = person.RegistrationId,
ParticipationRecordId = actualPersonId,
ProjectId = person.ProjectId,
UserId = person.UserId,
ParticipantName = person.Name,
GroupName = person.GroupName,
MerchantCertificationId = merchantCertId,
CertificateStatus = 1,
GeneratedTime = now,
CertificateNo = $"CC{now:yyyyMMdd}{actualPersonId}",
CreatedTime = now,
TemplateId = templateId,
CertificateUrl = string.Empty
};
certsToInsert.Add(cert);
isNewCert = true;
}
// 获取单位名称
string unitName = string.Empty;
if (person.MerchantCertificationId.HasValue &&
merchantNameDict.TryGetValue(person.MerchantCertificationId.Value, out var merchantName))
{
unitName = merchantName;
}
// 构建占位符数据(教练/领队没有成绩记录)
var placeholderData = BuildPlaceholderDataForCoachLeader(person.Person, unitName);
// 生成 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);
}
}
// 更新证书状态为已发放
cert.CertificateStatus = 2; // 已发放
cert.IssuedTime = now;
cert.UpdatedTime = now;
// 确保 CertificateUrl 不为 null
if (string.IsNullOrEmpty(cert.CertificateUrl))
{
cert.CertificateUrl = string.Empty;
}
if (!isNewCert)
{
certsToUpdate.Add(cert);
}
result.SuccessCount++;
}
if (certsToInsert.Any())
{
await _certificateRepository.InsertRangeAsync(certsToInsert);
}
// 更新已有证书的发放状态
if (certsToUpdate.Any())
{
await _certificateRepository.UpdateRangeAsync(certsToUpdate);
}
_logger.LogInformation("发放证书完成:成功 {SuccessCount} 条,失败 {FailCount} 条",
result.SuccessCount, result.FailCount);
return result;
}
/// <summary>
/// 根据页面类型获取允许的人员类型列表
/// </summary>
/// <param name="pageType">页面类型:1-运动员页面,2-教练页面,3-领队页面</param>
/// <returns>允许的人员类型列表</returns>
private List<PersonType> GetAllowedPersonTypes(int pageType)
{
return pageType switch
{
1 => new List<PersonType> { PersonType.Athlete, PersonType.User },
2 => new List<PersonType> { PersonType.Coach },
3 => new List<PersonType> { PersonType.Leader },
_ => new List<PersonType> { PersonType.Athlete, PersonType.User, PersonType.Coach, PersonType.Leader }
};
}
/// <summary>
/// 构建运动员占位符数据
/// </summary>
private Dictionary<string, string> BuildPlaceholderData(EventRegistrationPerson person, EventResult result, string unitName, string combinedNames = null)
{
var displayName = string.IsNullOrEmpty(combinedNames) ? person.Name : combinedNames;
var data = new Dictionary<string, string>
{
["name"] = displayName,
["姓名"] = displayName,
//["group"] = person.GroupName,
//["组别"] = person.GroupName,
["项目组别"] = $"{person.ProjectName}-{person.GroupName}",
//["project"] = result.ProjectTypeName,
//["项目"] = result.ProjectTypeName,
["score"] = result.Score,
["成绩"] = result.Score,
["rank"] = result.Rank.ToString(),
["名次"] = GetRankText(result.Rank),
["danwei"] = unitName ?? "个人",
["单位"] = unitName ?? "个人",
["参赛单位"] = unitName ?? "个人"
};
// 添加日期信息
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, string unitName)
{
var data = new Dictionary<string, string>
{
["name"] = person.Name,
["姓名"] = person.Name,
["group"] = person.GroupName,
["组别"] = person.GroupName,
["项目组别"] = person.GroupName,
["danwei"] = unitName ?? "个人",
["单位"] = unitName ?? "个人",
["参赛单位"] = unitName ?? "个人"
};
// 添加日期信息
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}名";
}
/// <summary>
/// 查看证书接口
/// </summary>
/// <param name="input">输入参数</param>
/// <returns>证书详情</returns>
[DisplayName("查看证书")]
[HttpPost("getcertificatedetail")]
public async Task<object> GetCertificateDetail([FromBody] YD_Event.Application.Certificate.Dtos.IdInput input)
{
if (input.Id <= 0)
{
throw Oops.Bah("未生成证书");
}
// 首先尝试直接通过 ParticipationRecordId(EventRegistrationPerson.Id)查找证书
var cert = await _certificateRepository.GetFirstAsync(x => x.ParticipationRecordId == input.Id);
// 如果找不到,则尝试通过 UserId 查找对应的 EventRegistrationPerson,再查找证书
if (cert == null)
{
_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);
}
}
if (cert == null)
{
throw Oops.Bah("未找到对应的证书信息");
}
return new
{
Id = cert.Id,
CertificateNo = cert.CertificateNo,
CertificateUrl = cert.CertificateUrl,
CertificateStatus = cert.CertificateStatus,
IssuedTime = cert.IssuedTime,
ParticipantName = cert.ParticipantName,
GroupName = cert.GroupName
};
}
/// <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
};
}
/// <summary>
/// 下载模板
/// </summary>
[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"
};
}
/// <summary>
/// 成绩模板导出器
/// </summary>
private class ScoreTemplateExporter : ExcelExportBase
{
public override async Task<byte[]> ExportAsync()
{
CreateWorksheet("成绩导入模板");
var headers = new string[] {
"报名ID",
"赛事地址",
"项目",
"组别",
"姓名",
"联系方式",
"单位",
"成绩",
"名次(汉字)",
"名次(数字)"
};
var headerColor = Color.FromArgb(204, 255, 204);
SetHeaders(headers, 1, headerColor, 11);
// 设置第1列为文本格式,防止科学计数法显示
// 只设置前20行的格式,避免MiniExcel读取过多空行
for (int r = 2; r <= 20; r++)
{
Worksheet.Cells[r, 1].Style.Numberformat.Format = "@";
}
var widths = new double[] { 12, 15, 15, 15, 12, 15, 20, 15, 15, 15 };
SetColumnWidths(widths);
SetRowHeight(1, 25);
return await GetAsByteArrayAsync();
}
}
/// <summary>
/// 导出证书
/// </summary>
[DisplayName("导出证书")]
[HttpGet]
public async Task<IActionResult> ExportCertificate([FromQuery] ScoreQueryInput input)
{
var list = await _certificateRepository.AsQueryable()
.LeftJoin<EventResult>((c, r) => c.ResultId == r.Id)
.LeftJoin<EventRegistration>((c, r, reg) => c.RegistrationId == reg.Id)
.LeftJoin<EventProject>((c, r, reg, ep) => c.ProjectId == ep.Id)
.LeftJoin<YD_Event.Core.Entities.Project>((c, r, reg, ep, p) => ep.ProjectId == p.Id)
.Where((c, r, reg, ep, p) => c.EventId == input.EventId)
.WhereIF(input.ProjectId.HasValue, (c, r, reg, ep, p) => c.ProjectId == input.ProjectId)
.WhereIF(!string.IsNullOrWhiteSpace(input.GroupName), (c, r, reg, ep, p) => c.GroupName == input.GroupName)
.WhereIF(!string.IsNullOrWhiteSpace(input.Name), (c, r, reg, ep, p) => c.ParticipantName.Contains(input.Name))
.Select((c, r, reg, ep, 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();
await stream.SaveAsAsync(list);
var bytes = stream.ToArray();
return new FileContentResult(bytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
{
FileDownloadName = $"证书列表_{DateTime.Now:yyyyMMddHHmmss}.xlsx"
};
}
/// <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>();
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;
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())
{
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;
}
/// <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("赛事不存在");
}
// 从快照获取积分规则
var pointsRuleSnapshot = await _ruleSnapshotService.GetPointsRuleSnapshot(input.EventId);
if (pointsRuleSnapshot == null)
{
throw Oops.Bah("该赛事未配置积分规则");
}
// 获取所有 EventProject 和对应的 Project 信息,用于判断项目类型
var eventProjects = await _eventProjectRepository.AsQueryable()
.Where(ep => ep.EventId == input.EventId)
.ToListAsync();
var projectIds = eventProjects.Select(ep => ep.ProjectId).Distinct().ToList();
var projects = await _projectRepository.GetListAsync(p => projectIds.Contains(p.Id));
var projectDict = projects.ToDictionary(p => p.Id);
// 查询所有成绩记录,包含 RegistrationItemId
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,
ProjectGroupId = p.EventProjectGroupId,
RegistrationId = p.RegistrationId,
RegistrationItemId = p.RegistrationItemId
})
.ToListAsync();
if (!results.Any())
{
throw Oops.Bah("没有可发放积分的成绩记录");
}
// 批量获取所有 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);
var processedResultIdsList = await _pointsRecordRepository.AsQueryable()
.Where(x => x.EventId == input.EventId && x.ResultId.HasValue)
.Select(x => x.ResultId.Value)
.ToListAsync();
var processedResultIds = new HashSet<long>(processedResultIdsList);
int successCount = 0;
int skipCount = 0;
var pointsRecords = new List<PointsRecord>();
var processedTeams = new HashSet<string>();
// 先按 ProjectId 分组
var projectGroups = results.GroupBy(r => r.ProjectId);
foreach (var projectGroup in projectGroups)
{
var projectId = projectGroup.Key;
var eventProject = eventProjects.FirstOrDefault(ep => ep.Id == projectId);
if (eventProject == null || !projectDict.TryGetValue(eventProject.ProjectId, out var project))
{
// 找不到项目,跳过
foreach (var result in projectGroup)
{
skipCount++;
}
continue;
}
var isTeamProject = project.ProjectType == ProjectTypeEnum.TeamSpeed ||
project.ProjectType == ProjectTypeEnum.TeamFreestyle;
if (isTeamProject)
{
// 团队项目:按 RegistrationItemId 分组,每组只发放一条记录
var teamGroups = projectGroup.GroupBy(r => new { r.RegistrationItemId, r.ProjectGroupId, r.GroupName });
foreach (var teamGroup in teamGroups)
{
var groupKey = $"team_{teamGroup.Key.RegistrationItemId}_{teamGroup.Key.ProjectGroupId}";
if (processedTeams.Contains(groupKey))
{
// 这个团队已经发过了
skipCount += teamGroup.Count();
continue;
}
// 检查团队中是否有结果已发放
var hasProcessed = teamGroup.Any(r => processedResultIds.Contains(r.ResultId));
if (hasProcessed)
{
skipCount += teamGroup.Count();
continue;
}
// 取团队第一个成员计算积分
var teamResult = teamGroup.OrderBy(r => r.ResultId).First();
int pointsToAward = 0;
if (pointsRuleSnapshot.ParticipationPoints.HasValue && pointsRuleSnapshot.ParticipationPoints.Value > 0)
{
pointsToAward += pointsRuleSnapshot.ParticipationPoints.Value;
}
var matchingDetail = pointsRuleSnapshot.Details.FirstOrDefault(d => teamResult.Rank >= d.StartRank && teamResult.Rank <= d.EndRank);
if (matchingDetail != null)
{
pointsToAward += matchingDetail.Points;
}
if (pointsToAward > 0)
{
// 从 EventRegistration 获取归属单位 ID
long? merchantCertId = null;
if (registrationDict.TryGetValue(teamResult.RegistrationId, out var reg))
{
merchantCertId = reg.MerchantCertificationId;
}
var record = new PointsRecord
{
Id = _idGenerator.NextId(),
UserId = teamResult.UserId,
Type = PointsOperationType.Acquire,
EventId = input.EventId,
ProjectId = teamResult.ProjectId,
ProjectGroupId = teamResult.ProjectGroupId,
ResultId = teamResult.ResultId,
MerchantCertificationId = merchantCertId,
Points = pointsToAward,
Remark = eventInfo.EventName,
Status = AvailabilityStatus.Enable,
CreatedTime = DateTime.Now
};
pointsRecords.Add(record);
successCount++;
processedTeams.Add(groupKey);
}
else
{
skipCount += teamGroup.Count();
}
}
}
else
{
// 个人项目:按原来的逻辑处理
foreach (var result in projectGroup)
{
if (processedResultIds.Contains(result.ResultId))
{
skipCount++;
continue;
}
int pointsToAward = 0;
if (pointsRuleSnapshot.ParticipationPoints.HasValue && pointsRuleSnapshot.ParticipationPoints.Value > 0)
{
pointsToAward += pointsRuleSnapshot.ParticipationPoints.Value;
}
var matchingDetail = pointsRuleSnapshot.Details.FirstOrDefault(d => result.Rank >= d.StartRank && result.Rank <= d.EndRank);
if (matchingDetail != null)
{
pointsToAward += matchingDetail.Points;
}
if (pointsToAward > 0)
{
// 从 EventRegistration 获取归属单位 ID
long? merchantCertId = null;
if (registrationDict.TryGetValue(result.RegistrationId, out var reg))
{
merchantCertId = reg.MerchantCertificationId;
}
var record = new PointsRecord
{
Id = _idGenerator.NextId(),
UserId = result.UserId,
Type = PointsOperationType.Acquire,
EventId = input.EventId,
ProjectId = result.ProjectId,
ProjectGroupId = result.ProjectGroupId,
ResultId = result.ResultId,
MerchantCertificationId = merchantCertId,
Points = pointsToAward,
Remark = eventInfo.EventName,
Status = AvailabilityStatus.Enable,
CreatedTime = DateTime.Now
};
pointsRecords.Add(record);
successCount++;
}
else
{
skipCount++;
}
}
}
}
if (pointsRecords.Any())
{
await _pointsRecordRepository.InsertRangeAsync(pointsRecords);
}
_logger.LogInformation("积分发放完成:赛事ID={EventId},成功发放={SuccessCount},跳过={SkipCount}",
input.EventId, successCount, skipCount);
return new
{
Success = true,
Message = $"积分发放成功!已发放 {successCount} 条积分记录,跳过 {skipCount} 条已发放记录"
};
}
}