1905 lines
82 KiB
C#
1905 lines
82 KiB
C#
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} 条已发放记录"
|
||
};
|
||
}
|
||
}
|