赛事成绩导入
This commit is contained in:
parent
ed13bb9c7f
commit
63be3e0144
@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user