diff --git a/YD_Event.Application/AppUser/AppUserService.cs b/YD_Event.Application/AppUser/AppUserService.cs index 87c895b..dd797ee 100644 --- a/YD_Event.Application/AppUser/AppUserService.cs +++ b/YD_Event.Application/AppUser/AppUserService.cs @@ -172,11 +172,6 @@ public class AppUserService : BaseService, 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, 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 { diff --git a/YD_Event.Application/Client/OAuthController.cs b/YD_Event.Application/Client/OAuthController.cs index 76ad40e..6707dcd 100644 --- a/YD_Event.Application/Client/OAuthController.cs +++ b/YD_Event.Application/Client/OAuthController.cs @@ -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; diff --git a/YD_Event.Application/Event/Dtos/RegistrationDtos.cs b/YD_Event.Application/Event/Dtos/RegistrationDtos.cs index b283568..355a001 100644 --- a/YD_Event.Application/Event/Dtos/RegistrationDtos.cs +++ b/YD_Event.Application/Event/Dtos/RegistrationDtos.cs @@ -201,6 +201,11 @@ public class RegistrationListOutput /// public class RegistrationItemDetailDto { + /// + /// 报名项目ID(EventRegistrationItem的ID) + /// + public long Id { get; set; } + /// /// 参与者信息 /// diff --git a/YD_Event.Application/Event/Dtos/ScoreDtos.cs b/YD_Event.Application/Event/Dtos/ScoreDtos.cs index 9569ec6..3b366e9 100644 --- a/YD_Event.Application/Event/Dtos/ScoreDtos.cs +++ b/YD_Event.Application/Event/Dtos/ScoreDtos.cs @@ -238,9 +238,9 @@ public class IssueCertificateResult public class ScoreImportDto { /// - /// 报名项目唯一码(唯一标识符) + /// 报名ID(唯一标识符) /// - [ExcelColumnName("报名项目唯一码")] + [ExcelColumnName("报名ID")] public long? RegistrationItemId { get; set; } /// diff --git a/YD_Event.Application/Event/EventArrangementService.cs b/YD_Event.Application/Event/EventArrangementService.cs index a7c2670..f99ecee 100644 --- a/YD_Event.Application/Event/EventArrangementService.cs +++ b/YD_Event.Application/Event/EventArrangementService.cs @@ -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); diff --git a/YD_Event.Application/Event/EventRegistrationService.cs b/YD_Event.Application/Event/EventRegistrationService.cs index 5eef9d9..840c28e 100644 --- a/YD_Event.Application/Event/EventRegistrationService.cs +++ b/YD_Event.Application/Event/EventRegistrationService.cs @@ -251,6 +251,7 @@ public class EventRegistrationService : BaseService, ITransie var detail = new RegistrationItemDetailDto { + Id = item.Id, ProjectName = item.ProjectName, GroupName = item.GroupName, ParticipantInfo = personInfo diff --git a/YD_Event.Application/Event/EventScoreService.cs b/YD_Event.Application/Event/EventScoreService.cs index 239789e..dce6e8f 100644 --- a/YD_Event.Application/Event/EventScoreService.cs +++ b/YD_Event.Application/Event/EventScoreService.cs @@ -368,12 +368,20 @@ public class EventScoreService : BaseService, ITransient var startTime = DateTime.Now; using var stream = file.OpenReadStream(); - var rows = stream.Query().ToList(); - _logger.LogInformation("解析Excel完成,共 {RowCount} 条记录", rows.Count); + var allRows = stream.Query().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, 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, 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, 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(); - combinedNames = string.Join(", ", teamMembers.Select(p => p.Name)); + // 收集所有姓名 + var namesList = new List(); + foreach (var member in teamMembers) + { + namesList.Add((string)member.Name); + } + combinedNames = string.Join(", ", namesList); } // 处理成绩记录 @@ -530,7 +532,8 @@ public class EventScoreService : BaseService, 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, 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(); + foreach (var member in teamMembers) + { + namesList.Add((string)member.Name); + } + combinedNames = string.Join(", ", namesList); } } @@ -1122,7 +1128,7 @@ public class EventScoreService : BaseService, ITransient CreateWorksheet("成绩导入模板"); var headers = new string[] { - "报名项目唯一码", + "报名ID", "赛事地址", "项目", "组别", @@ -1138,8 +1144,8 @@ public class EventScoreService : BaseService, 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, ITransient { var result = new ImportScoreValidationResult(); var errors = new List(); + var maxErrorsToShow = 20; // 限制最多显示20个错误,避免错误信息过长 for (int i = 0; i < rows.Count; i++) { @@ -1219,14 +1226,8 @@ public class EventScoreService : BaseService, ITransient var rowErrors = new List(); 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, 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 { diff --git a/YD_Event.Application/YD_Event.Application.xml b/YD_Event.Application/YD_Event.Application.xml index a550761..6fcee31 100644 --- a/YD_Event.Application/YD_Event.Application.xml +++ b/YD_Event.Application/YD_Event.Application.xml @@ -5765,6 +5765,11 @@ 报名项目详情 + + + 报名项目ID(EventRegistrationItem的ID) + + 参与者信息 @@ -6068,7 +6073,7 @@ - 报名项目唯一码(唯一标识符) + 报名ID(唯一标识符)