824 lines
30 KiB
C#
824 lines
30 KiB
C#
using Microsoft.AspNetCore.Http;
|
||
using Microsoft.AspNetCore.Mvc;
|
||
using Microsoft.Extensions.Logging;
|
||
using YD_Event.Application.Event.Dtos;
|
||
using YD_Event.Core.Entities;
|
||
using YD_Event.Core.Enum;
|
||
using YD_Event.Core.Domain.Export;
|
||
using MiniExcelLibs;
|
||
using SqlSugar;
|
||
using OfficeOpenXml;
|
||
using OfficeOpenXml.Style;
|
||
using System.Drawing;
|
||
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<CertificateTemplate> _templateRepository;
|
||
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Project> _projectRepository;
|
||
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 IIdGenerator _idGenerator;
|
||
private readonly ILogger<EventScoreService> _logger;
|
||
|
||
public EventScoreService(
|
||
ISqlSugarRepository<EventRegistrationPerson> personRepository,
|
||
ISqlSugarRepository<EventResult> resultRepository,
|
||
ISqlSugarRepository<EventCertificate> certificateRepository,
|
||
ISqlSugarRepository<EventRegistration> registrationRepository,
|
||
ISqlSugarRepository<CertificateTemplate> templateRepository,
|
||
ISqlSugarRepository<YD_Event.Core.Entities.Project> projectRepository,
|
||
ISqlSugarRepository<TeamMember> teamMemberRepository,
|
||
ISqlSugarRepository<EventInfo> eventRepository,
|
||
ISqlSugarRepository<PointsRule> pointsRuleRepository,
|
||
ISqlSugarRepository<PointsRuleDetail> pointsRuleDetailRepository,
|
||
ISqlSugarRepository<PointsRecord> pointsRecordRepository,
|
||
ISqlSugarRepository<YD_Event.Core.Entities.AppUser> appUserRepository,
|
||
IIdGenerator idGenerator,
|
||
ILogger<EventScoreService> logger) : base(resultRepository)
|
||
{
|
||
_personRepository = personRepository;
|
||
_resultRepository = resultRepository;
|
||
_certificateRepository = certificateRepository;
|
||
_registrationRepository = registrationRepository;
|
||
_templateRepository = templateRepository;
|
||
_projectRepository = projectRepository;
|
||
_teamMemberRepository = teamMemberRepository;
|
||
_eventRepository = eventRepository;
|
||
_pointsRuleRepository = pointsRuleRepository;
|
||
_pointsRuleDetailRepository = pointsRuleDetailRepository;
|
||
_pointsRecordRepository = pointsRecordRepository;
|
||
_appUserRepository = appUserRepository;
|
||
_idGenerator = idGenerator;
|
||
_logger = logger;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 运动员成绩列表
|
||
/// </summary>
|
||
[DisplayName("运动员成绩列表")]
|
||
public async Task<PageResult<ScoreListOutput>> PageAthlete(ScoreQueryInput input)
|
||
{
|
||
var pageResult = await _personRepository.AsQueryable()
|
||
.LeftJoin<EventRegistration>((p, r) => p.RegistrationId == r.Id)
|
||
.LeftJoin<EventResult>((p, r, res) => p.Id == res.RegistrationPersonId)
|
||
.LeftJoin<EventCertificate>((p, r, res, c) => res.Id == c.ResultId)
|
||
.Where((p, r, res, c) => r.EventId == input.EventId && (p.PersonType == PersonType.Athlete || p.PersonType == PersonType.User))
|
||
.WhereIF(input.ProjectId.HasValue, p => p.ProjectId == input.ProjectId)
|
||
.WhereIF(!string.IsNullOrWhiteSpace(input.GroupName), p => p.GroupName == input.GroupName)
|
||
.WhereIF(!string.IsNullOrWhiteSpace(input.Name), p => p.Name.Contains(input.Name))
|
||
.WhereIF(!string.IsNullOrWhiteSpace(input.UnitName), (p, r) => r.TeamName.Contains(input.UnitName))
|
||
.WhereIF(input.Gender.HasValue, p => p.Gender == input.Gender.Value)
|
||
.Select((p, r, res, c) => new ScoreListOutput
|
||
{
|
||
Id = p.Id,
|
||
Name = p.Name,
|
||
UnitName = r.TeamName,
|
||
Gender = p.Gender.ToString(),
|
||
Score = res.Score,
|
||
Rank = res.Rank,
|
||
ResultId = res.Id,
|
||
CertificateId = c.Id
|
||
})
|
||
.ToPagedListAsync(input);
|
||
|
||
foreach (var item in pageResult.Rows)
|
||
{
|
||
item.RankText = ConvertRankToChinese(item.Rank);
|
||
}
|
||
|
||
return pageResult;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 教练/领队列表
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// 通过运动员所在的团队关联获取教练和领队人员信息
|
||
/// </remarks>
|
||
[DisplayName("教练领队列表")]
|
||
public async Task<PageResult<CoachLeaderListOutput>> PageCoachLeader(CoachLeaderQueryInput input)
|
||
{
|
||
// 步骤1:获取该赛事的所有运动员的 UserId
|
||
var athleteUserIds = await _personRepository.AsQueryable()
|
||
.InnerJoin<EventRegistration>((p, r) => p.RegistrationId == r.Id)
|
||
.Where((p, r) => r.EventId == input.EventId && p.PersonType == PersonType.Athlete)
|
||
.Select(p => p.UserId)
|
||
.ToListAsync();
|
||
|
||
if (!athleteUserIds.Any())
|
||
{
|
||
return new PageResult<CoachLeaderListOutput>();
|
||
}
|
||
|
||
// 步骤2:通过运动员的 UserId 找到对应的 TeamId
|
||
var teamIds = await _teamMemberRepository.AsQueryable()
|
||
.Where(tm => athleteUserIds.Contains(tm.UserId) && !tm.DeleteMark)
|
||
.Select(tm => tm.TeamId)
|
||
.Distinct()
|
||
.ToListAsync();
|
||
|
||
if (!teamIds.Any())
|
||
{
|
||
return new PageResult<CoachLeaderListOutput>();
|
||
}
|
||
|
||
// 步骤3:通过 TeamId 获取同团队的教练/领队的 UserId
|
||
var coachLeaderUserIds = await _teamMemberRepository.AsQueryable()
|
||
.Where(tm => teamIds.Contains(tm.TeamId) && tm.PersonType == input.PersonType && !tm.DeleteMark)
|
||
.Select(tm => tm.UserId)
|
||
.Distinct()
|
||
.ToListAsync();
|
||
|
||
if (!coachLeaderUserIds.Any())
|
||
{
|
||
return new PageResult<CoachLeaderListOutput>();
|
||
}
|
||
|
||
// 步骤4:从 AppUser 表获取教练/领队的详细信息(需要关联查询)
|
||
var userQuery = _appUserRepository.AsQueryable()
|
||
.Where(p => coachLeaderUserIds.Contains(p.Id))
|
||
.Where(p=> p.PersonType == input.PersonType);
|
||
|
||
var pageResult = await userQuery
|
||
.Select(p => new CoachLeaderListOutput
|
||
{
|
||
Id = p.Id,
|
||
Name = p.Name,
|
||
Age = p.Age,
|
||
IdCardNumber = p.IdCardImageUrl,
|
||
Mobile = p.Mobile,
|
||
CertificateId = 0 // 需要单独查询,此处先设为0
|
||
})
|
||
.ToPagedListAsync(input);
|
||
|
||
foreach (var item in pageResult.Rows)
|
||
{
|
||
if (item.Age.HasValue)
|
||
{
|
||
continue;
|
||
}
|
||
if (item.BirthDate.HasValue)
|
||
{
|
||
item.Age = MemberOutput.CalculateAgeFromBirthDate(item.BirthDate);
|
||
}
|
||
}
|
||
|
||
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 rows = stream.Query<ScoreImportDto>().ToList();
|
||
_logger.LogInformation("解析Excel完成,共 {RowCount} 条记录", rows.Count);
|
||
|
||
if (!rows.Any())
|
||
{
|
||
throw Oops.Bah("Excel文件中没有数据");
|
||
}
|
||
|
||
// 验证导入数据
|
||
var validationResult = await ValidateImportDataAsync(rows, eventId);
|
||
if (!validationResult.IsValid)
|
||
{
|
||
var errorMessage = string.Join("\n", validationResult.Errors);
|
||
throw Oops.Bah($"数据验证失败:\n{errorMessage}");
|
||
}
|
||
|
||
// 步骤1:批量预加载该赛事的所有人员数据
|
||
var allPersons = await _personRepository.AsQueryable()
|
||
.InnerJoin<EventRegistration>((p, r) => p.RegistrationId == r.Id)
|
||
.Where((p, r) => r.EventId == eventId)
|
||
.Select((p, r) => new
|
||
{
|
||
p.Id,
|
||
p.Name,
|
||
p.ProjectName,
|
||
p.GroupName,
|
||
p.RegistrationId,
|
||
p.ProjectId
|
||
})
|
||
.ToListAsync();
|
||
|
||
_logger.LogInformation("预加载人员数据完成,共 {PersonCount} 人", allPersons.Count);
|
||
|
||
// 步骤2:使用字典建立人员索引(Name_ProjectName_GroupName -> Person)
|
||
var personDict = allPersons
|
||
.GroupBy(p => $"{p.Name}_{p.ProjectName}_{p.GroupName}")
|
||
.ToDictionary(g => g.Key, g => g.First());
|
||
|
||
// 预加载项目信息,用于获取项目类型名称
|
||
var projectIds = allPersons.Select(p => p.ProjectId).Distinct().ToList();
|
||
var projects = await _projectRepository.GetListAsync(x => projectIds.Contains(x.Id));
|
||
var projectDict = projects.ToDictionary(p => p.Id, p => p.ProjectType.Description());
|
||
|
||
// 步骤3:预加载该赛事的所有成绩记录
|
||
var personIds = allPersons.Select(p => p.Id).ToList();
|
||
var existingResults = await _resultRepository.GetListAsync(x =>
|
||
x.EventId == eventId && personIds.Contains(x.RegistrationPersonId));
|
||
|
||
var resultDict = existingResults.ToDictionary(r => r.RegistrationPersonId);
|
||
|
||
// 步骤4:批量处理数据,收集需要插入/更新的记录
|
||
var resultsToInsert = new List<EventResult>();
|
||
var resultsToUpdate = new List<EventResult>();
|
||
var now = DateTime.Now;
|
||
var notFoundPersons = new List<string>();
|
||
|
||
foreach(var row in validationResult.ValidRows)
|
||
{
|
||
// 使用内存字典查找,避免数据库查询
|
||
var key = $"{row.Name}_{row.ProjectName}_{row.GroupName}";
|
||
if (!personDict.TryGetValue(key, out var person))
|
||
{
|
||
notFoundPersons.Add($"姓名:{row.Name}, 项目:{row.ProjectName}, 组别:{row.GroupName}");
|
||
continue;
|
||
}
|
||
|
||
if (resultDict.TryGetValue(person.Id, out var existingResult))
|
||
{
|
||
// 更新现有记录
|
||
existingResult.Score = row.Score;
|
||
existingResult.Rank = row.Rank;
|
||
existingResult.Remark = row.RankText;
|
||
existingResult.ProjectTypeName = projectDict.TryGetValue(person.ProjectId, out var projectTypeName) ? projectTypeName : string.Empty;
|
||
existingResult.UpdatedTime = now;
|
||
resultsToUpdate.Add(existingResult);
|
||
}
|
||
else
|
||
{
|
||
// 插入新记录
|
||
var newResult = new EventResult
|
||
{
|
||
Id = _idGenerator.NewLong(),
|
||
EventId = eventId,
|
||
RegistrationPersonId = person.Id,
|
||
RegistrationId = person.RegistrationId,
|
||
ProjectId = person.ProjectId,
|
||
ParticipantName = person.Name,
|
||
GroupName = person.GroupName,
|
||
ProjectTypeName = projectDict.TryGetValue(person.ProjectId, out var projectTypeName) ? projectTypeName : string.Empty,
|
||
Score = row.Score,
|
||
Rank = row.Rank,
|
||
Remark = row.RankText,
|
||
CreatedTime = now,
|
||
UpdatedTime = now
|
||
};
|
||
resultsToInsert.Add(newResult);
|
||
}
|
||
}
|
||
|
||
if (notFoundPersons.Any())
|
||
{
|
||
_logger.LogWarning("未找到匹配的参赛人员:{Count} 条", notFoundPersons.Count);
|
||
}
|
||
|
||
_logger.LogInformation("数据处理完成:新增 {InsertCount} 条,更新 {UpdateCount} 条,未找到 {NotFoundCount} 条",
|
||
resultsToInsert.Count, resultsToUpdate.Count, notFoundPersons.Count);
|
||
|
||
// 步骤5:批量操作数据库
|
||
if (resultsToInsert.Any())
|
||
{
|
||
await _resultRepository.InsertRangeAsync(resultsToInsert);
|
||
_logger.LogInformation("批量插入成绩记录完成,共 {Count} 条", resultsToInsert.Count);
|
||
}
|
||
|
||
if (resultsToUpdate.Any())
|
||
{
|
||
await _resultRepository.UpdateRangeAsync(resultsToUpdate);
|
||
_logger.LogInformation("批量更新成绩记录完成,共 {Count} 条", resultsToUpdate.Count);
|
||
}
|
||
|
||
var duration = (DateTime.Now - startTime).TotalSeconds;
|
||
_logger.LogInformation("导入成绩完成:赛事ID={EventId},总耗时={Duration}秒", eventId, duration);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 发放证书(支持模板选择和批量发放)
|
||
/// </summary>
|
||
[UnitOfWork]
|
||
[DisplayName("发放证书")]
|
||
public async Task<IssueCertificateResult> IssueCertificate(IssueCertificateInput input)
|
||
{
|
||
var result = new IssueCertificateResult();
|
||
var now = DateTime.Now;
|
||
|
||
// 如果指定了模板,验证模板是否存在
|
||
CertificateTemplate? template = null;
|
||
if (input.TemplateId.HasValue)
|
||
{
|
||
template = await _templateRepository.GetFirstAsync(x =>
|
||
x.Id == input.TemplateId.Value &&
|
||
x.Status == AvailabilityStatus.Enable &&
|
||
!x.DeleteMark);
|
||
if (template == null)
|
||
{
|
||
throw Oops.Bah("指定的证书模板不存在或不可用");
|
||
}
|
||
}
|
||
|
||
// 批量预加载所有需要的数据
|
||
var persons = await _personRepository.GetListAsync(x => input.PersonIds.Contains(x.Id));
|
||
var personDict = persons.ToDictionary(p => p.Id);
|
||
|
||
var personIds = persons.Select(p => p.Id).ToList();
|
||
var existingResults = await _resultRepository.GetListAsync(x => personIds.Contains(x.RegistrationPersonId));
|
||
var resultDict = existingResults.ToDictionary(r => r.RegistrationPersonId);
|
||
|
||
var existingCerts = await _certificateRepository.GetListAsync(x =>
|
||
personIds.Contains(x.ParticipationRecordId));
|
||
var certDict = existingCerts.ToDictionary(c => c.ParticipationRecordId);
|
||
|
||
var certsToInsert = new List<EventCertificate>();
|
||
|
||
foreach (var personId in input.PersonIds)
|
||
{
|
||
if (!personDict.TryGetValue(personId, out var person))
|
||
{
|
||
result.FailCount++;
|
||
result.FailDetails.Add($"人员ID {personId} 不存在");
|
||
continue;
|
||
}
|
||
|
||
EventCertificate? cert = null;
|
||
|
||
if (person.PersonType == PersonType.Athlete || person.PersonType == PersonType.User)
|
||
{
|
||
if (!resultDict.TryGetValue(person.Id, out var resultRecord))
|
||
{
|
||
result.FailCount++;
|
||
result.FailDetails.Add($"人员 {person.Name} 没有成绩记录");
|
||
continue;
|
||
}
|
||
|
||
if (certDict.TryGetValue(person.Id, out var existingCert))
|
||
{
|
||
cert = existingCert;
|
||
}
|
||
else
|
||
{
|
||
cert = new EventCertificate
|
||
{
|
||
Id = _idGenerator.NewLong(),
|
||
EventId = input.EventId,
|
||
ResultId = resultRecord.Id,
|
||
RegistrationId = person.RegistrationId,
|
||
ParticipationRecordId = person.Id,
|
||
ParticipantName = person.Name,
|
||
GroupName = person.GroupName,
|
||
CertificateStatus = 1,
|
||
GeneratedTime = now,
|
||
CertificateNo = $"C{now:yyyyMMdd}{person.Id}",
|
||
CreatedTime = now,
|
||
TemplateId = input.TemplateId
|
||
};
|
||
certsToInsert.Add(cert);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
if (certDict.TryGetValue(person.Id, out var existingCert))
|
||
{
|
||
cert = existingCert;
|
||
}
|
||
else
|
||
{
|
||
cert = new EventCertificate
|
||
{
|
||
Id = _idGenerator.NewLong(),
|
||
EventId = input.EventId,
|
||
RegistrationId = person.RegistrationId,
|
||
ParticipationRecordId = person.Id,
|
||
ParticipantName = person.Name,
|
||
GroupName = person.GroupName,
|
||
CertificateStatus = 1,
|
||
GeneratedTime = now,
|
||
CertificateNo = $"CC{now:yyyyMMdd}{person.Id}",
|
||
CreatedTime = now,
|
||
TemplateId = input.TemplateId
|
||
};
|
||
certsToInsert.Add(cert);
|
||
}
|
||
}
|
||
|
||
// 更新证书状态为已发放
|
||
cert.CertificateStatus = 2;
|
||
cert.IssuedTime = now;
|
||
cert.UpdatedTime = now;
|
||
|
||
result.SuccessCount++;
|
||
}
|
||
|
||
if (certsToInsert.Any())
|
||
{
|
||
await _certificateRepository.InsertRangeAsync(certsToInsert);
|
||
}
|
||
|
||
// 更新已有证书的发放状态
|
||
var existingCertsToUpdate = existingCerts.Where(c =>
|
||
input.PersonIds.Contains(c.ParticipationRecordId)).ToList();
|
||
if (existingCertsToUpdate.Any())
|
||
{
|
||
await _certificateRepository.UpdateRangeAsync(existingCertsToUpdate);
|
||
}
|
||
|
||
_logger.LogInformation("发放证书完成:成功 {SuccessCount} 条,失败 {FailCount} 条",
|
||
result.SuccessCount, result.FailCount);
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 查看证书接口
|
||
/// </summary>
|
||
/// <param name="input">输入参数</param>
|
||
/// <returns>证书详情</returns>
|
||
[DisplayName("查看证书")]
|
||
public async Task<object> GetCertificateDetail(YD_Event.Application.Certificate.Dtos.IdInput input)
|
||
{
|
||
if (input.Id <= 0)
|
||
{
|
||
throw Oops.Bah("无效的 ID");
|
||
}
|
||
|
||
var personExists = await _personRepository.IsAnyAsync(x => x.Id == input.Id);
|
||
if (!personExists)
|
||
{
|
||
throw Oops.Bah("人员记录不存在");
|
||
}
|
||
|
||
var cert = await _certificateRepository.GetFirstAsync(x => x.ParticipationRecordId == input.Id);
|
||
if (cert == null)
|
||
{
|
||
throw Oops.Bah("未找到对应的证书信息");
|
||
}
|
||
|
||
return new { Url = cert.CertificateUrl };
|
||
}
|
||
|
||
/// <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[] {
|
||
"赛事地址",
|
||
"项目",
|
||
"组别",
|
||
"姓名",
|
||
"联系方式",
|
||
"单位",
|
||
"成绩",
|
||
"名次(汉字)",
|
||
"名次(数字)"
|
||
};
|
||
|
||
var headerColor = Color.FromArgb(204, 255, 204);
|
||
SetHeaders(headers, 1, headerColor, 11);
|
||
|
||
var widths = new double[] { 15, 15, 15, 12, 15, 20, 15, 15, 15 };
|
||
SetColumnWidths(widths);
|
||
|
||
SetRowHeight(1, 25);
|
||
|
||
return await GetAsByteArrayAsync();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 导出证书
|
||
/// </summary>
|
||
[DisplayName("导出证书")]
|
||
public async Task<string> ExportCertificate(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<YD_Event.Core.Entities.Project>((c, r, reg, p) => c.ProjectId == p.Id)
|
||
.Where((c, r, reg, p) => c.EventId == input.EventId)
|
||
.WhereIF(input.ProjectId.HasValue, (c, r, reg, p) => c.ProjectId == input.ProjectId)
|
||
.WhereIF(!string.IsNullOrWhiteSpace(input.GroupName), (c, r, reg, p) => c.GroupName == input.GroupName)
|
||
.WhereIF(!string.IsNullOrWhiteSpace(input.Name), (c, r, reg, p) => c.ParticipantName.Contains(input.Name))
|
||
.Select((c, r, reg, p) => new CertificateExportDto
|
||
{
|
||
CertificateNo = c.CertificateNo,
|
||
ParticipantName = c.ParticipantName,
|
||
ProjectName = p.Name,
|
||
GroupName = c.GroupName,
|
||
UnitName = reg.TeamName,
|
||
Score = r.Score,
|
||
Rank = r.Rank,
|
||
CertificateType = c.CertificateType == 1 ? "冠军证书" :
|
||
c.CertificateType == 2 ? "亚军证书" :
|
||
c.CertificateType == 3 ? "季军证书" :
|
||
c.CertificateType == 4 ? "优胜证书" : "参与证书",
|
||
IssuedTime = c.IssuedTime
|
||
})
|
||
.ToListAsync();
|
||
|
||
using var stream = new MemoryStream();
|
||
stream.SaveAs(list);
|
||
return Convert.ToBase64String(stream.ToArray());
|
||
}
|
||
|
||
/// <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>();
|
||
|
||
for (int i = 0; i < rows.Count; i++)
|
||
{
|
||
var row = rows[i];
|
||
var rowErrors = new List<string>();
|
||
var rowNumber = i + 2;
|
||
|
||
if (string.IsNullOrWhiteSpace(row.Name))
|
||
rowErrors.Add("姓名不能为空");
|
||
|
||
if (string.IsNullOrWhiteSpace(row.ProjectName))
|
||
rowErrors.Add("项目不能为空");
|
||
|
||
if (string.IsNullOrWhiteSpace(row.GroupName))
|
||
rowErrors.Add("组别不能为空");
|
||
|
||
if (string.IsNullOrWhiteSpace(row.Score))
|
||
rowErrors.Add("成绩不能为空");
|
||
|
||
if (row.Rank <= 0)
|
||
rowErrors.Add("名次必须为正整数");
|
||
|
||
if (rowErrors.Any())
|
||
{
|
||
errors.Add($"第 {rowNumber} 行:{string.Join(",", rowErrors)}");
|
||
}
|
||
else
|
||
{
|
||
result.ValidRows.Add(row);
|
||
}
|
||
}
|
||
|
||
if (errors.Any())
|
||
{
|
||
result.IsValid = false;
|
||
result.Errors = errors;
|
||
}
|
||
else
|
||
{
|
||
result.IsValid = true;
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 发放积分输入模型
|
||
/// </summary>
|
||
public class IssuePointsInput
|
||
{
|
||
public long EventId { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 发放积分
|
||
/// </summary>
|
||
[DisplayName("发放积分")]
|
||
[HttpPost("issuePoints")]
|
||
[UnitOfWork]
|
||
public async Task<object> IssuePoints([FromBody] IssuePointsInput input)
|
||
{
|
||
_logger.LogInformation("开始发放积分,赛事ID={EventId}", input.EventId);
|
||
|
||
var eventInfo = await _eventRepository.GetByIdAsync(input.EventId);
|
||
if (eventInfo == null)
|
||
{
|
||
throw Oops.Bah("赛事不存在");
|
||
}
|
||
|
||
if (!eventInfo.PointsRuleId.HasValue)
|
||
{
|
||
throw Oops.Bah("该赛事未配置积分规则");
|
||
}
|
||
|
||
var pointsRule = await _pointsRuleRepository.GetByIdAsync(eventInfo.PointsRuleId.Value);
|
||
if (pointsRule == null)
|
||
{
|
||
throw Oops.Bah("积分规则不存在");
|
||
}
|
||
|
||
if (pointsRule.Status != AvailabilityStatus.Enable)
|
||
{
|
||
throw Oops.Bah("积分规则未启用");
|
||
}
|
||
|
||
var ruleDetails = await _pointsRuleDetailRepository.AsQueryable()
|
||
.Where(x => x.PointsRuleId == pointsRule.Id)
|
||
.OrderBy(x => x.StartRank)
|
||
.ToListAsync();
|
||
|
||
var results = await _resultRepository.AsQueryable()
|
||
.InnerJoin<EventRegistrationPerson>((r, p) => r.RegistrationPersonId == p.Id)
|
||
.Where((r, p) => r.EventId == input.EventId && r.Rank > 0)
|
||
.Select((r, p) => new
|
||
{
|
||
ResultId = r.Id,
|
||
UserId = p.UserId,
|
||
ProjectId = r.ProjectId,
|
||
GroupName = r.GroupName,
|
||
Rank = r.Rank,
|
||
ParticipantName = r.ParticipantName,
|
||
ProjectIdForRecord = r.ProjectId
|
||
})
|
||
.ToListAsync();
|
||
|
||
if (!results.Any())
|
||
{
|
||
throw Oops.Bah("没有可发放积分的成绩记录");
|
||
}
|
||
|
||
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>();
|
||
|
||
foreach (var result in results)
|
||
{
|
||
if (processedResultIds.Contains(result.ResultId))
|
||
{
|
||
skipCount++;
|
||
continue;
|
||
}
|
||
|
||
int pointsToAward = 0;
|
||
|
||
if (pointsRule.ParticipationPoints.HasValue && pointsRule.ParticipationPoints.Value > 0)
|
||
{
|
||
pointsToAward += pointsRule.ParticipationPoints.Value;
|
||
}
|
||
|
||
var matchingDetail = ruleDetails.FirstOrDefault(d => result.Rank >= d.StartRank && result.Rank <= d.EndRank);
|
||
if (matchingDetail != null)
|
||
{
|
||
pointsToAward += matchingDetail.Points;
|
||
}
|
||
|
||
if (pointsToAward > 0)
|
||
{
|
||
var record = new PointsRecord
|
||
{
|
||
Id = _idGenerator.NextId(),
|
||
UserId = result.UserId,
|
||
Type = PointsOperationType.Acquire,
|
||
EventId = input.EventId,
|
||
ProjectId = result.ProjectIdForRecord,
|
||
ResultId = result.ResultId,
|
||
Points = pointsToAward,
|
||
Remark = eventInfo.EventName,
|
||
Status = AvailabilityStatus.Enable,
|
||
CreatedTime = DateTime.Now
|
||
};
|
||
pointsRecords.Add(record);
|
||
successCount++;
|
||
}
|
||
}
|
||
|
||
if (pointsRecords.Any())
|
||
{
|
||
await _pointsRecordRepository.InsertRangeAsync(pointsRecords);
|
||
}
|
||
|
||
_logger.LogInformation("积分发放完成:赛事ID={EventId},成功发放={SuccessCount},跳过={SkipCount}",
|
||
input.EventId, successCount, skipCount);
|
||
|
||
return new
|
||
{
|
||
Success = true,
|
||
Message = $"积分发放成功!已发放 {successCount} 条积分记录,跳过 {skipCount} 条已发放记录"
|
||
};
|
||
}
|
||
}
|