diff --git a/YD_Event.Application/AppUser/AppUserService.cs b/YD_Event.Application/AppUser/AppUserService.cs index 313f90e..9074a11 100644 --- a/YD_Event.Application/AppUser/AppUserService.cs +++ b/YD_Event.Application/AppUser/AppUserService.cs @@ -266,7 +266,7 @@ public class AppUserService : BaseService, ITransient // 直接查询单位认证表,检查是否有审核通过的认证记录 var certification = await _db.Queryable() - .FirstAsync(c => c.UserId == userId && c.AuditStatus == AuditStatus.Pass && !c.DeleteMark); + .FirstAsync(c => c.UserId == userId && !c.DeleteMark); // 有审核通过的认证就返回认证ID,否则返回null return (userId, certification?.Id); diff --git a/YD_Event.Application/Client/Dtos/EventClientDtos.cs b/YD_Event.Application/Client/Dtos/EventClientDtos.cs index a90aeb4..6d73e77 100644 --- a/YD_Event.Application/Client/Dtos/EventClientDtos.cs +++ b/YD_Event.Application/Client/Dtos/EventClientDtos.cs @@ -361,6 +361,11 @@ public class EventProjectDto /// public string TypeName { get; set; } + /// + /// 项目类型枚举值 + /// + public long ProjectType { get; set; } + /// /// 报名费用 /// diff --git a/YD_Event.Application/Client/EventClientController.cs b/YD_Event.Application/Client/EventClientController.cs index 559b2e6..95b5d5f 100644 --- a/YD_Event.Application/Client/EventClientController.cs +++ b/YD_Event.Application/Client/EventClientController.cs @@ -473,6 +473,7 @@ public class EventClientController : IDynamicApiController Id = ep.Id, Name = bp?.Name ?? "", TypeName = bp?.ProjectType.Description() ?? "", + ProjectType = (long)(bp?.ProjectType ?? 0), Fee = ep.Price / 100m, ProjectLimitCount = pl?.LimitCount, Groups = groups.Where(g => g.ProjectId == ep.ProjectId).Select(g => new EventProjectGroupDto diff --git a/YD_Event.Application/Event/Dtos/UserCompetitionDtos.cs b/YD_Event.Application/Event/Dtos/UserCompetitionDtos.cs index 9733b9a..cde01b2 100644 --- a/YD_Event.Application/Event/Dtos/UserCompetitionDtos.cs +++ b/YD_Event.Application/Event/Dtos/UserCompetitionDtos.cs @@ -342,6 +342,11 @@ public class UserEventProjectListOutput /// public int CompletionCount { get; set; } + /// + /// 项目类型 + /// + public long ProjectType { get; set; } + /// /// 项目类型名称 /// diff --git a/YD_Event.Application/Event/UnifiedRegistrationService.cs b/YD_Event.Application/Event/UnifiedRegistrationService.cs index 92a60e4..3b352ca 100644 --- a/YD_Event.Application/Event/UnifiedRegistrationService.cs +++ b/YD_Event.Application/Event/UnifiedRegistrationService.cs @@ -6,6 +6,7 @@ using YD_Event.Core.Domain.Pricing; using YD_Event.Core.Domain.Pricing.Models; using YD_Event.Core.Entities; using YD_Event.Core.Enum; +using YD_Event.Core.Extensions; using YD_Event.Core.Infrastructure.WeChat; using YD_Event.Core.Infrastructure.WeChat.Dto; using YD_Event.Core.Options; @@ -421,17 +422,6 @@ public class UnifiedRegistrationService : ITransient return result; } result.Details.AddRange(qualificationResult.Details); - - // 2.2 验证年龄跨组 - var ageGroupResult = ValidateAgeCrossGroup(item.Persons, eventProjectGroup); - if (!ageGroupResult.IsValid) - { - result.IsValid = false; - result.ErrorMessage = ageGroupResult.ErrorMessage; - result.Details.AddRange(ageGroupResult.Details); - return result; - } - result.Details.AddRange(ageGroupResult.Details); } return result; @@ -448,10 +438,14 @@ public class UnifiedRegistrationService : ITransient Details = new List() }; + bool hasAtLeastOneInTargetAge = false; + bool hasAnyBelowTargetAge = false; + bool hasAnyAboveTargetAge = false; + foreach (var person in persons) { - // 验证性别 - if (person.Gender != group.Gender) + // 验证性别 - 如果组别是"全部",则允许任何性别 + if (group.Gender != Gender.All && person.Gender != group.Gender) { result.IsValid = false; result.ErrorMessage = $"队员{person.Name}的性别不符合组别要求。要求:{group.Gender.Description()},实际:{person.Gender.Description()}"; @@ -464,12 +458,34 @@ public class UnifiedRegistrationService : ITransient return result; } - // 验证年龄 - var age = CalculateAge(person.BirthDate); - if (person.BirthDate < group.BirthDateStart || person.BirthDate > group.BirthDateEnd) + // 检查年龄状态 + if (person.BirthDate >= group.BirthDateStart && person.BirthDate <= group.BirthDateEnd) + { + hasAtLeastOneInTargetAge = true; + } + else if (person.BirthDate < group.BirthDateStart) + { + hasAnyBelowTargetAge = true; + } + else if (person.BirthDate > group.BirthDateEnd) + { + hasAnyAboveTargetAge = true; + } + } + + // 验证年龄 + // 规则: + // 1. 所有队员年龄都在目标年龄段内 → 允许 + // 2. 有队员年龄在目标年龄段内,且有队员年龄小于目标年龄段(跨组)→ 允许 + // 3. 有队员年龄大于目标年龄段 → 不允许 + if (hasAnyAboveTargetAge) + { + // 找到年龄大于目标年龄段的队员 + var aboveAgePerson = persons.FirstOrDefault(p => p.BirthDate > group.BirthDateEnd); + if (aboveAgePerson != null) { result.IsValid = false; - result.ErrorMessage = $"队员{person.Name}的年龄不符合组别要求。要求出生日期范围:{group.BirthDateStart:yyyy-MM-dd} 至 {group.BirthDateEnd:yyyy-MM-dd},实际:{person.BirthDate:yyyy-MM-dd}"; + result.ErrorMessage = $"队员{aboveAgePerson.Name}的年龄超过组别最大限制。要求出生日期不晚于:{group.BirthDateEnd:yyyy-MM-dd},实际:{aboveAgePerson.BirthDate:yyyy-MM-dd}"; result.Details.Add(new ValidationDetail { ItemName = "年龄验证", @@ -480,49 +496,9 @@ public class UnifiedRegistrationService : ITransient } } - result.Details.Add(new ValidationDetail - { - ItemName = "队员资质", - IsPassed = true, - Message = "所有队员的年龄和性别均符合组别要求" - }); - - return result; - } - - /// - /// 验证年龄跨组 - /// 支持小年龄段向大年龄段跨组报名,需满足两个条件: - /// ①团队中至少有一名成员属于目标年龄段; - /// ②仅允许跨一个年龄段 - /// - private RegistrationValidationResult ValidateAgeCrossGroup(List persons, EventProjectGroup targetGroup) - { - var result = new RegistrationValidationResult - { - IsValid = true, - Details = new List() - }; - - bool hasAtLeastOneInTargetAge = false; - bool hasAnyBelowTargetAge = false; - - foreach (var person in persons) - { - if (person.BirthDate >= targetGroup.BirthDateStart && person.BirthDate <= targetGroup.BirthDateEnd) - { - hasAtLeastOneInTargetAge = true; - } - else if (person.BirthDate < targetGroup.BirthDateStart) - { - hasAnyBelowTargetAge = true; - } - } - - // 如果有队员年龄低于目标年龄段(小年龄跨大年龄) + // 如果有队员年龄小于目标年龄段,需要确保至少有一名队员在目标年龄段内 if (hasAnyBelowTargetAge) { - // 检查是否至少有一名队员在目标年龄段 if (!hasAtLeastOneInTargetAge) { result.IsValid = false; @@ -547,12 +523,19 @@ public class UnifiedRegistrationService : ITransient { result.Details.Add(new ValidationDetail { - ItemName = "年龄跨组", + ItemName = "年龄验证", IsPassed = true, - Message = "无需跨组,所有队员均在目标年龄段内" + Message = "所有队员均在目标年龄段内" }); } + result.Details.Add(new ValidationDetail + { + ItemName = "队员资质", + IsPassed = true, + Message = "所有队员的性别符合组别要求,年龄符合规则" + }); + return result; } @@ -866,12 +849,24 @@ public class UnifiedRegistrationService : ITransient var eventProjects = await _eventProjectRepository.GetListAsync(x => x.EventId == input.EventId); var projects = await _projectRepository.GetListAsync(x => eventProjects.Select(ep => ep.ProjectId).Contains(x.Id) && !x.DeleteMark); var eventProjectGroups = await _eventProjectGroupRepository.GetListAsync(x => x.EventId == input.EventId && !x.DeleteMark); + + // 先获取所有报名信息,供后续检查使用 + var eventRegistrations = await _registrationRepository.GetListAsync(x => + x.EventId == input.EventId && x.PaymentStatus != 4 && !x.DeleteMark); + var eventRegistrationIds = eventRegistrations.Select(r => r.Id).ToList(); + var registrationPersons = await _registrationPersonRepository.GetListAsync(x => + eventRegistrationIds.Contains(x.RegistrationId) && !x.DeleteMark); + var allRegistrationItems = await _registrationItemRepository.GetListAsync(x => + eventRegistrationIds.Contains(x.RegistrationId) && !x.DeleteMark); - // 1. 检查项目组别容量控制(团队报名数量限制) + // 1. 检查本次提交的团队数量是否超过项目限报配置(仅检查本次提交,不统计已报名数量) var groupIds = input.Items.Where(i => i.GroupId.HasValue).Select(i => i.GroupId.Value).ToList(); if (groupIds.Any()) { - foreach (var groupId in groupIds) + // 按组别分组统计本次提交的团队数量 + var groupTeamCount = groupIds.GroupBy(g => g).ToDictionary(g => g.Key, g => g.Count()); + + foreach (var groupId in groupTeamCount.Keys) { var eventProjectGroup = eventProjectGroups.FirstOrDefault(epg => epg.Id == groupId); if (eventProjectGroup == null) continue; @@ -882,21 +877,8 @@ public class UnifiedRegistrationService : ITransient var projectLimit = projectLimits.FirstOrDefault(pl => pl.ProjectId == eventProject.ProjectId); if (projectLimit == null) continue; - // 统计当前组别已报名的团队数量(排除草稿和已取消的) - var registrationIds = await _registrationRepository.AsQueryable() - .Where(r => r.EventId == input.EventId && !r.DeleteMark && r.PaymentStatus != 4) - .Select(r => r.Id) - .ToListAsync(); - - var currentTeams = await _registrationItemRepository.AsQueryable() - .Where(ri => ri.EventProjectGroupId == eventProjectGroup.Id - && !ri.DeleteMark - && registrationIds.Contains(ri.RegistrationId)) - .Select(ri => ri.RegistrationId) - .Distinct() - .CountAsync(); - - if (currentTeams >= projectLimit.LimitCount) + var submittedTeamCount = groupTeamCount[groupId]; + if (submittedTeamCount > projectLimit.LimitCount) { var project = projects.FirstOrDefault(p => p.Id == eventProject.ProjectId); var projectName = project?.Name ?? "未知项目"; @@ -907,7 +889,7 @@ public class UnifiedRegistrationService : ITransient MemberId = 0, MemberName = eventProjectGroup.GroupName ?? "未知组别", LimitType = "ProjectGroupCapacity", - LimitMessage = $"项目\"{projectName}\"的组别\"{eventProjectGroup.GroupName}\"已报名{currentTeams}队,达到限报{projectLimit.LimitCount}队" + LimitMessage = $"项目\"{projectName}\"的组别\"{eventProjectGroup.GroupName}\"限报{projectLimit.LimitCount}队,本次提交{submittedTeamCount}队,超出限报配置" }); } } @@ -918,13 +900,6 @@ public class UnifiedRegistrationService : ITransient if (limit != null && memberIds.Any()) { var members = await _userRepository.GetListAsync(x => memberIds.Contains(x.Id) && !x.DeleteMark); - var eventRegistrations = await _registrationRepository.GetListAsync(x => - x.EventId == input.EventId && x.PaymentStatus != 4 && !x.DeleteMark); - var eventRegistrationIds = eventRegistrations.Select(r => r.Id).ToList(); - var registrationPersons = await _registrationPersonRepository.GetListAsync(x => - eventRegistrationIds.Contains(x.RegistrationId) && !x.DeleteMark); - var allRegistrationItems = await _registrationItemRepository.GetListAsync(x => - eventRegistrationIds.Contains(x.RegistrationId) && !x.DeleteMark); foreach (var member in members) { @@ -947,19 +922,13 @@ public class UnifiedRegistrationService : ITransient if (baseProject == null) continue; // 根据项目类型判断是个人项目还是团队项目 - // 这里假设ProjectType中有个人和团队的区分,您可以根据实际情况调整 - // 暂时根据报名模式统计 - var registration = eventRegistrations.FirstOrDefault(r => r.Id == item.RegistrationId); - if (registration != null) + if (baseProject.IsIndividualProject()) { - if (registration.RegistrationMode == 1) // 个人报名 - { - individualProjectCount++; - } - else // 团队报名 - { - teamProjectCount++; - } + individualProjectCount++; + } + else if (baseProject.IsTeamProject()) + { + teamProjectCount++; } } @@ -991,6 +960,112 @@ public class UnifiedRegistrationService : ITransient } } + // 3. 强化人员-项目唯一性校验:同一人员不得同时在多个团队中报名同一个项目组别 + var personProjectGroupMap = new Dictionary>(); + + foreach (var item in input.Items) + { + if (!item.GroupId.HasValue) continue; + + var eventProjectGroup = eventProjectGroups.FirstOrDefault(epg => epg.Id == item.GroupId.Value); + if (eventProjectGroup == null) continue; + + var eventProject = eventProjects.FirstOrDefault(ep => ep.Id == eventProjectGroup.EventProjectId); + if (eventProject == null) continue; + + var project = projects.FirstOrDefault(p => p.Id == eventProject.ProjectId); + var projectName = project?.Name ?? "未知项目"; + + foreach (var person in item.Persons) + { + var idCard = person.IdCardNumber; + if (string.IsNullOrEmpty(idCard)) continue; + + if (!personProjectGroupMap.ContainsKey(idCard)) + { + personProjectGroupMap[idCard] = new List<(long GroupId, string GroupName, string ProjectName)>(); + } + + personProjectGroupMap[idCard].Add((item.GroupId.Value, eventProjectGroup.GroupName ?? "未知组别", projectName)); + } + } + + // 检查同一人员在同一项目组别中是否出现多次 + foreach (var kvp in personProjectGroupMap) + { + var idCard = kvp.Key; + var groupInfos = kvp.Value; + + // 按组别分组 + var groupDuplicates = groupInfos + .GroupBy(g => g.GroupId) + .Where(g => g.Count() > 1) + .ToList(); + + if (groupDuplicates.Any()) + { + foreach (var duplicate in groupDuplicates) + { + var groupInfo = duplicate.First(); + var person = input.Items.SelectMany(i => i.Persons).FirstOrDefault(p => p.IdCardNumber == idCard); + var personName = person?.Name ?? "未知人员"; + + result.HasLimit = true; + result.LimitDetails.Add(new RegistrationLimitDetail + { + MemberId = 0, + MemberName = personName, + LimitType = "PersonProjectDuplicate", + LimitMessage = $"人员\"{personName}\"(身份证:{idCard})在项目\"{groupInfo.ProjectName}\"的组别\"{groupInfo.GroupName}\"中重复出现,同一人员只能在一个团队中报名同一项目组别" + }); + } + } + } + + // 4. 检查人员是否已在其他报名中报过同一项目组别 + if (eventRegistrationIds.Any()) + { + foreach (var item in input.Items) + { + if (!item.GroupId.HasValue) continue; + + var eventProjectGroup = eventProjectGroups.FirstOrDefault(epg => epg.Id == item.GroupId.Value); + if (eventProjectGroup == null) continue; + + var eventProject = eventProjects.FirstOrDefault(ep => ep.Id == eventProjectGroup.EventProjectId); + if (eventProject == null) continue; + + var project = projects.FirstOrDefault(p => p.Id == eventProject.ProjectId); + var projectName = project?.Name ?? "未知项目"; + + // 查找该组别已有的报名项 + var existingItems = allRegistrationItems.Where(ri => ri.EventProjectGroupId == item.GroupId.Value).ToList(); + if (!existingItems.Any()) continue; + + var existingItemIds = existingItems.Select(ri => ri.Id).ToList(); + var existingPersons = registrationPersons.Where(rp => existingItemIds.Contains(rp.RegistrationItemId)).ToList(); + + foreach (var person in item.Persons) + { + var idCard = person.IdCardNumber; + if (string.IsNullOrEmpty(idCard)) continue; + + var existingPerson = existingPersons.FirstOrDefault(ep => ep.IdCard == idCard); + if (existingPerson != null) + { + result.HasLimit = true; + result.LimitDetails.Add(new RegistrationLimitDetail + { + MemberId = 0, + MemberName = person.Name, + LimitType = "PersonAlreadyRegistered", + LimitMessage = $"人员\"{person.Name}\"(身份证:{idCard})已在项目\"{projectName}\"的组别\"{eventProjectGroup.GroupName}\"中报名过,不能重复报名" + }); + } + } + } + } + return result; } diff --git a/YD_Event.Application/YD_Event.Application.xml b/YD_Event.Application/YD_Event.Application.xml index a72a4a8..98471d9 100644 --- a/YD_Event.Application/YD_Event.Application.xml +++ b/YD_Event.Application/YD_Event.Application.xml @@ -1952,6 +1952,11 @@ 类型名称 (个人/团队) + + + 项目类型枚举值 + + 报名费用 @@ -6175,6 +6180,11 @@ 完成人数 + + + 项目类型 + + 项目类型名称 @@ -7282,14 +7292,6 @@ 验证队员资质(年龄和性别) - - - 验证年龄跨组 - 支持小年龄段向大年龄段跨组报名,需满足两个条件: - ①团队中至少有一名成员属于目标年龄段; - ②仅允许跨一个年龄段 - - 创建报名明细和参赛人员 diff --git a/YD_Event.Core/Extensions/ProjectTypeExtensions.cs b/YD_Event.Core/Extensions/ProjectTypeExtensions.cs new file mode 100644 index 0000000..878cc23 --- /dev/null +++ b/YD_Event.Core/Extensions/ProjectTypeExtensions.cs @@ -0,0 +1,52 @@ +using YD_Event.Core.Enum; +using YD_Event.Core.Entities; + +namespace YD_Event.Core.Extensions; + +/// +/// 项目类型判断扩展方法 +/// +public static class ProjectTypeExtensions +{ + /// + /// 判断是否为个人项目 + /// + /// 项目类型枚举 + /// 如果是个人速度或个人花样则返回true + public static bool IsIndividualProject(this ProjectTypeEnum projectType) + { + return projectType == ProjectTypeEnum.PersonalSpeed || + projectType == ProjectTypeEnum.PersonalFreestyle; + } + + /// + /// 判断是否为团队项目 + /// + /// 项目类型枚举 + /// 如果是团队速度或团队花样则返回true + public static bool IsTeamProject(this ProjectTypeEnum projectType) + { + return projectType == ProjectTypeEnum.TeamSpeed || + projectType == ProjectTypeEnum.TeamFreestyle; + } + + /// + /// 判断是否为个人项目(扩展方法版本) + /// + /// 项目实体 + /// 如果是个人项目则返回true + public static bool IsIndividualProject(this Project project) + { + return project != null && project.ProjectType.IsIndividualProject(); + } + + /// + /// 判断是否为团队项目(扩展方法版本) + /// + /// 项目实体 + /// 如果是团队项目则返回true + public static bool IsTeamProject(this Project project) + { + return project != null && project.ProjectType.IsTeamProject(); + } +} diff --git a/YD_Event.Core/YD_Event.Core.xml b/YD_Event.Core/YD_Event.Core.xml index 943b627..fad338e 100644 --- a/YD_Event.Core/YD_Event.Core.xml +++ b/YD_Event.Core/YD_Event.Core.xml @@ -4647,6 +4647,39 @@ 赞助单位 + + + 项目类型判断扩展方法 + + + + + 判断是否为个人项目 + + 项目类型枚举 + 如果是个人速度或个人花样则返回true + + + + 判断是否为团队项目 + + 项目类型枚举 + 如果是团队速度或团队花样则返回true + + + + 判断是否为个人项目(扩展方法版本) + + 项目实体 + 如果是个人项目则返回true + + + + 判断是否为团队项目(扩展方法版本) + + 项目实体 + 如果是团队项目则返回true + 身份证识别结果