赛事成绩导入

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();
// 基于手机号的全局唯一性校验(不区分PersonType和权限)
var existingByPhone = await _appUserRep.AsQueryable()
.Where(x => x.Mobile == input.Mobile && !x.DeleteMark)
.FirstAsync();
AppUserEntity entity;
bool isNewRecord = false;
@ -187,29 +182,40 @@ public class AppUserService : BaseService<AppUserEntity>, ITransient
if (entity == null || entity.DeleteMark) throw Oops.Bah("成员不存在");
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.UpdatedTime = DateTime.Now;
}
else if (existingByPhone != null)
{
// 手机号已存在,更新现有记录
entity = existingByPhone;
// 检查权限
await CheckPermission(entity);
entity.UpdatedUserId = _authManager.UserId;
entity.UpdatedTime = DateTime.Now;
}
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;
entity = new AppUserEntity
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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