赛事成绩导入

This commit is contained in:
马梓彭 2026-04-22 14:47:50 +08:00
parent ed13bb9c7f
commit 63be3e0144
8 changed files with 108 additions and 71 deletions

View File

@ -172,11 +172,6 @@ public class AppUserService : BaseService<AppUserEntity>, ITransient
var (userId, merchantCertificationId) = await GetCurrentUserAndMerchantCertificationId(); var (userId, merchantCertificationId) = await GetCurrentUserAndMerchantCertificationId();
// 基于手机号的全局唯一性校验(不区分PersonType和权限)
var existingByPhone = await _appUserRep.AsQueryable()
.Where(x => x.Mobile == input.Mobile && !x.DeleteMark)
.FirstAsync();
AppUserEntity entity; AppUserEntity entity;
bool isNewRecord = false; bool isNewRecord = false;
@ -187,29 +182,40 @@ public class AppUserService : BaseService<AppUserEntity>, ITransient
if (entity == null || entity.DeleteMark) throw Oops.Bah("成员不存在"); if (entity == null || entity.DeleteMark) throw Oops.Bah("成员不存在");
await CheckPermission(entity); await CheckPermission(entity);
// 检查手机号是否被其他记录占用 // 编辑模式下:检查手机号是否被本单位内同类型的其他记录占用
if (existingByPhone != null && existingByPhone.Id != input.Id.Value) var duplicateInSameUnit = await _appUserRep.AsQueryable()
.Where(x => x.Mobile == input.Mobile
&& x.PersonType == input.PersonType
&& x.MerchantCertificationId == merchantCertificationId
&& !x.DeleteMark
&& x.Id != input.Id.Value)
.FirstAsync();
if (duplicateInSameUnit != null)
{ {
throw Oops.Bah($"该手机号已被其他成员使用"); throw Oops.Bah($"该手机号在本单位已存在相同类型的成员");
} }
entity.UpdatedUserId = _authManager.UserId; entity.UpdatedUserId = _authManager.UserId;
entity.UpdatedTime = DateTime.Now; entity.UpdatedTime = DateTime.Now;
} }
else if (existingByPhone != null)
{
// 手机号已存在,更新现有记录
entity = existingByPhone;
// 检查权限
await CheckPermission(entity);
entity.UpdatedUserId = _authManager.UserId;
entity.UpdatedTime = DateTime.Now;
}
else else
{ {
// 手机号不存在,创建新记录 // 新增模式:检查本单位内是否有相同手机号且相同类型的记录
var existingInSameUnit = await _appUserRep.AsQueryable()
.Where(x => x.Mobile == input.Mobile
&& x.PersonType == input.PersonType
&& x.MerchantCertificationId == merchantCertificationId
&& !x.DeleteMark)
.FirstAsync();
if (existingInSameUnit != null)
{
// 本单位内已有相同手机号且相同类型的记录,不允许重复添加
throw Oops.Bah($"该手机号在本单位已存在相同类型的成员");
}
// 新增记录(即使手机号在其他单位或其他类型中已存在,也允许新增)
isNewRecord = true; isNewRecord = true;
entity = new AppUserEntity entity = new AppUserEntity
{ {

View File

@ -64,6 +64,12 @@ public class OAuthController : IDynamicApiController
} }
} }
// 检查用户类型 - 仅允许运动员登录
if (appUser.PersonType != PersonType.Athlete)
{
throw Oops.Bah("未注册用户");
}
if (isPasswordLogin) if (isPasswordLogin)
{ {
var encryptedPassword = MD5Encryption.Encrypt($"{_idGenerator.Encode(appUser.Id)}{input.Password}"); var encryptedPassword = MD5Encryption.Encrypt($"{_idGenerator.Encode(appUser.Id)}{input.Password}");
@ -158,7 +164,7 @@ public class OAuthController : IDynamicApiController
Avatar = input.Avatar, Avatar = input.Avatar,
Mobile = input.Mobile, Mobile = input.Mobile,
Password = encryptedPassword, Password = encryptedPassword,
PersonType = PersonType.User, PersonType = PersonType.Athlete,
Status = AvailabilityStatus.Enable, Status = AvailabilityStatus.Enable,
AuditStatus = AuditStatus.PendingSubmission, AuditStatus = AuditStatus.PendingSubmission,
CreatedTime = DateTime.Now, CreatedTime = DateTime.Now,
@ -208,6 +214,12 @@ public class OAuthController : IDynamicApiController
throw Oops.Bah("用户不存在"); throw Oops.Bah("用户不存在");
} }
// 检查用户类型 - 仅允许运动员重置密码
if (appUser.PersonType != PersonType.Athlete)
{
throw Oops.Bah("用户不存在");
}
var encryptedPassword = MD5Encryption.Encrypt($"{_idGenerator.Encode(appUser.Id)}{input.NewPassword}"); var encryptedPassword = MD5Encryption.Encrypt($"{_idGenerator.Encode(appUser.Id)}{input.NewPassword}");
appUser.Password = encryptedPassword; appUser.Password = encryptedPassword;
appUser.UpdatedTime = DateTime.Now; appUser.UpdatedTime = DateTime.Now;

View File

@ -201,6 +201,11 @@ public class RegistrationListOutput
/// </summary> /// </summary>
public class RegistrationItemDetailDto public class RegistrationItemDetailDto
{ {
/// <summary>
/// 报名项目ID(EventRegistrationItem的ID)
/// </summary>
public long Id { get; set; }
/// <summary> /// <summary>
/// 参与者信息 /// 参与者信息
/// </summary> /// </summary>

View File

@ -238,9 +238,9 @@ public class IssueCertificateResult
public class ScoreImportDto public class ScoreImportDto
{ {
/// <summary> /// <summary>
/// 报名项目唯一码(唯一标识符) /// 报名ID(唯一标识符)
/// </summary> /// </summary>
[ExcelColumnName("报名项目唯一码")] [ExcelColumnName("报名ID")]
public long? RegistrationItemId { get; set; } public long? RegistrationItemId { get; set; }
/// <summary> /// <summary>

View File

@ -520,7 +520,7 @@ namespace YD_Event.Application.Event;
SetMergeTitle(_eventInfo?.EventName ?? "赛事编排表", 1, 1, 1, 9, titleColor, 14); SetMergeTitle(_eventInfo?.EventName ?? "赛事编排表", 1, 1, 1, 9, titleColor, 14);
// 第二行:列标题 // 第二行:列标题
var headers = new[] { "报名项目唯一码", "时间", "参赛项目", "参赛组别", "参赛队伍", "运动员姓名", "单位", "场次", "场地" }; var headers = new[] { "报名ID", "时间", "参赛项目", "参赛组别", "参赛队伍", "运动员姓名", "单位", "场次", "场地" };
var headerColor = Color.FromArgb(204, 255, 204); var headerColor = Color.FromArgb(204, 255, 204);
SetHeaders(headers, 2, headerColor, 11); SetHeaders(headers, 2, headerColor, 11);

View File

@ -251,6 +251,7 @@ public class EventRegistrationService : BaseService<EventRegistration>, ITransie
var detail = new RegistrationItemDetailDto var detail = new RegistrationItemDetailDto
{ {
Id = item.Id,
ProjectName = item.ProjectName, ProjectName = item.ProjectName,
GroupName = item.GroupName, GroupName = item.GroupName,
ParticipantInfo = personInfo ParticipantInfo = personInfo

View File

@ -368,12 +368,20 @@ public class EventScoreService : BaseService<EventResult>, ITransient
var startTime = DateTime.Now; var startTime = DateTime.Now;
using var stream = file.OpenReadStream(); using var stream = file.OpenReadStream();
var rows = stream.Query<ScoreImportDto>().ToList(); var allRows = stream.Query<ScoreImportDto>().ToList();
_logger.LogInformation("解析Excel完成,共 {RowCount} 条记录", rows.Count); _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()) if (!rows.Any())
{ {
throw Oops.Bah("Excel文件中没有数据"); throw Oops.Bah("Excel文件中没有有效数据,请确保填写了报名ID");
} }
// 验证导入数据 // 验证导入数据
@ -411,10 +419,10 @@ public class EventScoreService : BaseService<EventResult>, ITransient
var allProjects = await _projectRepository.GetListAsync(x => allProjectIds.Contains(x.Id)); var allProjects = await _projectRepository.GetListAsync(x => allProjectIds.Contains(x.Id));
var allProjectDict = allProjects.ToDictionary(p => p.Id, p => p); var allProjectDict = allProjects.ToDictionary(p => p.Id, p => p);
// 步骤4:使用字典建立人员索引(建立三种索引) // 步骤4:使用字典建立人员索引(按RegistrationItemId索引,取第一个人作为代表)
var personByItemIdDict = allPersonsWithTeam.ToDictionary(p => p.RegistrationItemId); // 注意:团队项目中一个 RegistrationItemId 可能对应多个运动员,但只需要第一个作为代表
var personByNameDict = allPersonsWithTeam var personByItemIdDict = allPersonsWithTeam
.GroupBy(p => $"{p.Name}_{p.ProjectName}_{p.GroupName}") .GroupBy(p => p.RegistrationItemId)
.ToDictionary(g => g.Key, g => g.First()); .ToDictionary(g => g.Key, g => g.First());
// 预加载 EventRegistration 信息,用于获取归属单位 ID // 预加载 EventRegistration 信息,用于获取归属单位 ID
@ -437,21 +445,11 @@ public class EventScoreService : BaseService<EventResult>, ITransient
foreach(var row in validationResult.ValidRows) foreach(var row in validationResult.ValidRows)
{ {
// 优先使用 RegistrationItemId 进行匹配(项目唯一码) // 使用报名ID进行匹配
var person = row.RegistrationItemId.HasValue && personByItemIdDict.TryGetValue(row.RegistrationItemId.Value, out var pByItemId) if (!personByItemIdDict.TryGetValue(row.RegistrationItemId.Value, out var person))
? pByItemId
: null;
// 如果 RegistrationItemId 未匹配到,再使用姓名+项目+组别的方式(向后兼容)
if (person == null)
{ {
var key = $"{row.Name}_{row.ProjectName}_{row.GroupName}"; notFoundPersons.Add($"报名ID:{row.RegistrationItemId.Value}");
if (!personByNameDict.TryGetValue(key, out var pByName)) continue;
{
notFoundPersons.Add($"姓名:{row.Name}, 项目:{row.ProjectName}, 组别:{row.GroupName}");
continue;
}
person = pByName;
} }
// 获取项目信息,判断是否为团队项目 // 获取项目信息,判断是否为团队项目
@ -470,15 +468,19 @@ public class EventScoreService : BaseService<EventResult>, ITransient
string combinedNames = person.Name; string combinedNames = person.Name;
if (isTeamProject) if (isTeamProject)
{ {
// 查找该报名项目的所有成员
teamMembers = allPersonsWithTeam teamMembers = allPersonsWithTeam
.Where(p => p.ProjectId == person.ProjectId && .Where(p => p.RegistrationItemId == person.RegistrationItemId)
p.GroupName == person.GroupName && .OrderBy(p => (string)p.Name)
p.TeamName == person.TeamName &&
p.RegistrationItemId == person.RegistrationItemId)
.OrderBy(p => p.Name)
.ToList<dynamic>(); .ToList<dynamic>();
combinedNames = string.Join(", ", teamMembers.Select(p => p.Name)); // 收集所有姓名
var namesList = new List<string>();
foreach (var member in teamMembers)
{
namesList.Add((string)member.Name);
}
combinedNames = string.Join(", ", namesList);
} }
// 处理成绩记录 // 处理成绩记录
@ -530,7 +532,8 @@ public class EventScoreService : BaseService<EventResult>, ITransient
if (notFoundPersons.Any()) if (notFoundPersons.Any())
{ {
_logger.LogWarning("未找到匹配的参赛人员:{Count} 条", notFoundPersons.Count); _logger.LogWarning("未找到匹配的参赛人员:{Count} 条,未找到的报名ID:{Ids}",
notFoundPersons.Count, string.Join(", ", notFoundPersons));
} }
_logger.LogInformation("数据处理完成:新增 {InsertCount} 条,更新 {UpdateCount} 条,未找到 {NotFoundCount} 条", _logger.LogInformation("数据处理完成:新增 {InsertCount} 条,更新 {UpdateCount} 条,未找到 {NotFoundCount} 条",
@ -710,15 +713,18 @@ public class EventScoreService : BaseService<EventResult>, ITransient
// 如果是团队项目,找到该团队的所有成员并合并姓名 // 如果是团队项目,找到该团队的所有成员并合并姓名
if (isTeamProject) if (isTeamProject)
{ {
// 回到原来的方法,但确保不会出错
var teamMembers = allPersonsWithTeam var teamMembers = allPersonsWithTeam
.Where(p => p.ProjectId == person.ProjectId && .Where(p => p.RegistrationItemId == person.RegistrationItemId)
p.GroupName == person.GroupName && .OrderBy(p => (string)p.Name)
p.TeamName == person.TeamName &&
p.RegistrationItemId == person.RegistrationItemId)
.OrderBy(p => p.Name)
.ToList(); .ToList();
combinedNames = string.Join(", ", teamMembers.Select(p => p.Name)); var namesList = new List<string>();
foreach (var member in teamMembers)
{
namesList.Add((string)member.Name);
}
combinedNames = string.Join(", ", namesList);
} }
} }
@ -1122,7 +1128,7 @@ public class EventScoreService : BaseService<EventResult>, ITransient
CreateWorksheet("成绩导入模板"); CreateWorksheet("成绩导入模板");
var headers = new string[] { var headers = new string[] {
"报名项目唯一码", "报名ID",
"赛事地址", "赛事地址",
"项目", "项目",
"组别", "组别",
@ -1138,8 +1144,8 @@ public class EventScoreService : BaseService<EventResult>, ITransient
SetHeaders(headers, 1, headerColor, 11); SetHeaders(headers, 1, headerColor, 11);
// 设置第1列为文本格式,防止科学计数法显示 // 设置第1列为文本格式,防止科学计数法显示
// (虽然模板中没有数据,但为了一致性,提前设置格式) // 只设置前20行的格式,避免MiniExcel读取过多空行
for (int r = 2; r <= 100; r++) for (int r = 2; r <= 20; r++)
{ {
Worksheet.Cells[r, 1].Style.Numberformat.Format = "@"; Worksheet.Cells[r, 1].Style.Numberformat.Format = "@";
} }
@ -1212,6 +1218,7 @@ public class EventScoreService : BaseService<EventResult>, ITransient
{ {
var result = new ImportScoreValidationResult(); var result = new ImportScoreValidationResult();
var errors = new List<string>(); var errors = new List<string>();
var maxErrorsToShow = 20; // 限制最多显示20个错误,避免错误信息过长
for (int i = 0; i < rows.Count; i++) for (int i = 0; i < rows.Count; i++)
{ {
@ -1219,14 +1226,8 @@ public class EventScoreService : BaseService<EventResult>, ITransient
var rowErrors = new List<string>(); var rowErrors = new List<string>();
var rowNumber = i + 2; var rowNumber = i + 2;
if (string.IsNullOrWhiteSpace(row.Name)) if (!row.RegistrationItemId.HasValue || row.RegistrationItemId.Value <= 0)
rowErrors.Add("姓名不能为空"); rowErrors.Add("报名ID不能为空且必须为有效的数字");
if (string.IsNullOrWhiteSpace(row.ProjectName))
rowErrors.Add("项目不能为空");
if (string.IsNullOrWhiteSpace(row.GroupName))
rowErrors.Add("组别不能为空");
if (string.IsNullOrWhiteSpace(row.Score)) if (string.IsNullOrWhiteSpace(row.Score))
rowErrors.Add("成绩不能为空"); rowErrors.Add("成绩不能为空");
@ -1236,7 +1237,14 @@ public class EventScoreService : BaseService<EventResult>, ITransient
if (rowErrors.Any()) if (rowErrors.Any())
{ {
errors.Add($"第 {rowNumber} 行:{string.Join(",", rowErrors)}"); if (errors.Count < maxErrorsToShow)
{
errors.Add($"第 {rowNumber} 行:{string.Join(",", rowErrors)}");
}
else if (errors.Count == maxErrorsToShow)
{
errors.Add($"... 还有 {rows.Count - maxErrorsToShow} 条错误未显示");
}
} }
else else
{ {

View File

@ -5765,6 +5765,11 @@
报名项目详情 报名项目详情
</summary> </summary>
</member> </member>
<member name="P:YD_Event.Application.Event.Dtos.RegistrationItemDetailDto.Id">
<summary>
报名项目ID(EventRegistrationItem的ID)
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.RegistrationItemDetailDto.ParticipantInfo"> <member name="P:YD_Event.Application.Event.Dtos.RegistrationItemDetailDto.ParticipantInfo">
<summary> <summary>
参与者信息 参与者信息
@ -6068,7 +6073,7 @@
</member> </member>
<member name="P:YD_Event.Application.Event.Dtos.ScoreImportDto.RegistrationItemId"> <member name="P:YD_Event.Application.Event.Dtos.ScoreImportDto.RegistrationItemId">
<summary> <summary>
报名项目唯一码(唯一标识符) 报名ID(唯一标识符)
</summary> </summary>
</member> </member>
<member name="P:YD_Event.Application.Event.Dtos.ScoreImportDto.EventAddress"> <member name="P:YD_Event.Application.Event.Dtos.ScoreImportDto.EventAddress">