赛事成绩导入
This commit is contained in:
parent
ed13bb9c7f
commit
63be3e0144
@ -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
|
||||
{
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -201,6 +201,11 @@ public class RegistrationListOutput
|
||||
/// </summary>
|
||||
public class RegistrationItemDetailDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 报名项目ID(EventRegistrationItem的ID)
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 参与者信息
|
||||
/// </summary>
|
||||
|
||||
@ -238,9 +238,9 @@ public class IssueCertificateResult
|
||||
public class ScoreImportDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 报名项目唯一码(唯一标识符)
|
||||
/// 报名ID(唯一标识符)
|
||||
/// </summary>
|
||||
[ExcelColumnName("报名项目唯一码")]
|
||||
[ExcelColumnName("报名ID")]
|
||||
public long? RegistrationItemId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
{
|
||||
|
||||
@ -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">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user