赛事报名限制

This commit is contained in:
马梓彭 2026-04-13 03:44:49 +08:00
parent 3d641112fd
commit 814c5672f2
8 changed files with 279 additions and 106 deletions

View File

@ -266,7 +266,7 @@ public class AppUserService : BaseService<AppUserEntity>, ITransient
// 直接查询单位认证表,检查是否有审核通过的认证记录
var certification = await _db.Queryable<MerchantCertification>()
.FirstAsync(c => c.UserId == userId && c.AuditStatus == AuditStatus.Pass && !c.DeleteMark);
.FirstAsync(c => c.UserId == userId && !c.DeleteMark);
// 有审核通过的认证就返回认证ID,否则返回null
return (userId, certification?.Id);

View File

@ -361,6 +361,11 @@ public class EventProjectDto
/// </summary>
public string TypeName { get; set; }
/// <summary>
/// 项目类型枚举值
/// </summary>
public long ProjectType { get; set; }
/// <summary>
/// 报名费用
/// </summary>

View File

@ -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

View File

@ -342,6 +342,11 @@ public class UserEventProjectListOutput
/// </summary>
public int CompletionCount { get; set; }
/// <summary>
/// 项目类型
/// </summary>
public long ProjectType { get; set; }
/// <summary>
/// 项目类型名称
/// </summary>

View File

@ -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<ValidationDetail>()
};
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;
}
/// <summary>
/// 验证年龄跨组
/// 支持小年龄段向大年龄段跨组报名,需满足两个条件:
/// ①团队中至少有一名成员属于目标年龄段;
/// ②仅允许跨一个年龄段
/// </summary>
private RegistrationValidationResult ValidateAgeCrossGroup(List<UnifiedPersonInput> persons, EventProjectGroup targetGroup)
{
var result = new RegistrationValidationResult
{
IsValid = true,
Details = new List<ValidationDetail>()
};
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<string, List<(long GroupId, string GroupName, string ProjectName)>>();
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;
}

View File

@ -1952,6 +1952,11 @@
类型名称 (个人/团队)
</summary>
</member>
<member name="P:YD_Event.Application.Client.Dtos.EventProjectDto.ProjectType">
<summary>
项目类型枚举值
</summary>
</member>
<member name="P:YD_Event.Application.Client.Dtos.EventProjectDto.Fee">
<summary>
报名费用
@ -6175,6 +6180,11 @@
完成人数
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.UserEventProjectListOutput.ProjectType">
<summary>
项目类型
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.UserEventProjectListOutput.ProjectTypeName">
<summary>
项目类型名称
@ -7282,14 +7292,6 @@
验证队员资质(年龄和性别)
</summary>
</member>
<member name="M:YD_Event.Application.Event.UnifiedRegistrationService.ValidateAgeCrossGroup(System.Collections.Generic.List{YD_Event.Application.Event.Dtos.UnifiedPersonInput},YD_Event.Core.Entities.EventProjectGroup)">
<summary>
验证年龄跨组
支持小年龄段向大年龄段跨组报名,需满足两个条件:
①团队中至少有一名成员属于目标年龄段;
②仅允许跨一个年龄段
</summary>
</member>
<member name="M:YD_Event.Application.Event.UnifiedRegistrationService.CreateRegistrationDetailsAsync(YD_Event.Core.Entities.EventRegistration,YD_Event.Application.Event.Dtos.UnifiedRegistrationInput)">
<summary>
创建报名明细和参赛人员

View File

@ -0,0 +1,52 @@
using YD_Event.Core.Enum;
using YD_Event.Core.Entities;
namespace YD_Event.Core.Extensions;
/// <summary>
/// 项目类型判断扩展方法
/// </summary>
public static class ProjectTypeExtensions
{
/// <summary>
/// 判断是否为个人项目
/// </summary>
/// <param name="projectType">项目类型枚举</param>
/// <returns>如果是个人速度或个人花样则返回true</returns>
public static bool IsIndividualProject(this ProjectTypeEnum projectType)
{
return projectType == ProjectTypeEnum.PersonalSpeed ||
projectType == ProjectTypeEnum.PersonalFreestyle;
}
/// <summary>
/// 判断是否为团队项目
/// </summary>
/// <param name="projectType">项目类型枚举</param>
/// <returns>如果是团队速度或团队花样则返回true</returns>
public static bool IsTeamProject(this ProjectTypeEnum projectType)
{
return projectType == ProjectTypeEnum.TeamSpeed ||
projectType == ProjectTypeEnum.TeamFreestyle;
}
/// <summary>
/// 判断是否为个人项目(扩展方法版本)
/// </summary>
/// <param name="project">项目实体</param>
/// <returns>如果是个人项目则返回true</returns>
public static bool IsIndividualProject(this Project project)
{
return project != null && project.ProjectType.IsIndividualProject();
}
/// <summary>
/// 判断是否为团队项目(扩展方法版本)
/// </summary>
/// <param name="project">项目实体</param>
/// <returns>如果是团队项目则返回true</returns>
public static bool IsTeamProject(this Project project)
{
return project != null && project.ProjectType.IsTeamProject();
}
}

View File

@ -4647,6 +4647,39 @@
赞助单位
</summary>
</member>
<member name="T:YD_Event.Core.Extensions.ProjectTypeExtensions">
<summary>
项目类型判断扩展方法
</summary>
</member>
<member name="M:YD_Event.Core.Extensions.ProjectTypeExtensions.IsIndividualProject(YD_Event.Core.Enum.ProjectTypeEnum)">
<summary>
判断是否为个人项目
</summary>
<param name="projectType">项目类型枚举</param>
<returns>如果是个人速度或个人花样则返回true</returns>
</member>
<member name="M:YD_Event.Core.Extensions.ProjectTypeExtensions.IsTeamProject(YD_Event.Core.Enum.ProjectTypeEnum)">
<summary>
判断是否为团队项目
</summary>
<param name="projectType">项目类型枚举</param>
<returns>如果是团队速度或团队花样则返回true</returns>
</member>
<member name="M:YD_Event.Core.Extensions.ProjectTypeExtensions.IsIndividualProject(YD_Event.Core.Entities.Project)">
<summary>
判断是否为个人项目(扩展方法版本)
</summary>
<param name="project">项目实体</param>
<returns>如果是个人项目则返回true</returns>
</member>
<member name="M:YD_Event.Core.Extensions.ProjectTypeExtensions.IsTeamProject(YD_Event.Core.Entities.Project)">
<summary>
判断是否为团队项目(扩展方法版本)
</summary>
<param name="project">项目实体</param>
<returns>如果是团队项目则返回true</returns>
</member>
<member name="T:YD_Event.Core.Infrastructure.Aliyun.Ocr.IdCardRecognitionResult">
<summary>
身份证识别结果