赛事报名限制

This commit is contained in:
马梓彭 2026-04-17 09:19:21 +08:00
parent fc2cc2f2bb
commit c9eb639123
6 changed files with 184 additions and 91 deletions

View File

@ -198,7 +198,7 @@ public class IssueCertificateInput
/// 页面类型:1-运动员页面,2-教练页面,3-领队页面
/// 用于明确区分不同角色页面的证书生成逻辑
/// </summary>
public CertificatePageType PageType { get; set; }
public int PageType { get; set; }
}
/// <summary>

View File

@ -423,25 +423,14 @@ public class EventScoreService : BaseService<EventResult>, ITransient
_logger.LogInformation("发放证书 - 页面类型: {PageType}, 允许的人员类型: {AllowedTypes}",
input.PageType, string.Join(", ", allowedPersonTypes));
// 批量预加载所有需要的数据
// 先尝试直接从 EventRegistrationPerson 表获取所有人员
var directPersons = await _personRepository.GetListAsync(x => input.PersonIds.Contains(x.Id));
var directPersonDict = directPersons.ToDictionary(p => p.Id);
// 找出在 directPersonDict 中找不到的人员ID,这些可能是教练/领队的 AppUser ID
var missingPersonIds = input.PersonIds.Where(id => !directPersonDict.ContainsKey(id)).ToList();
// 构建最终的人员字典
var personDict = new Dictionary<long, EventRegistrationPerson>();
foreach (var kvp in directPersonDict)
{
personDict[kvp.Key] = kvp.Value;
}
// 对于找不到的人员ID,尝试通过团队关系查找对应的 EventRegistrationPerson 记录
if (missingPersonIds.Any())
// 根据页面类型区分人员信息获取逻辑
if (input.PageType == 2 || input.PageType == 3)
{
_logger.LogInformation("查找 {Count} 个缺失的人员ID,尝试通过团队关系获取", missingPersonIds.Count);
// 页面类型为教练(2)或领队(3)时,从团队关系数据中获取对应人员ID
_logger.LogInformation("页面类型为教练/领队,从团队关系数据中获取人员信息");
// 获取该赛事的所有 EventRegistrationPerson 记录
var allEventPersons = await _personRepository.AsQueryable()
@ -456,20 +445,33 @@ public class EventScoreService : BaseService<EventResult>, ITransient
.GroupBy(p => p.UserId)
.ToDictionary(g => g.Key, g => g.First());
// 尝试通过 UserId 匹配
foreach (var missingId in missingPersonIds)
// 通过 UserId 查找对应的 EventRegistrationPerson
foreach (var userId in input.PersonIds)
{
if (userToPersonMap.TryGetValue(missingId, out var person))
if (userToPersonMap.TryGetValue(userId, out var person))
{
personDict[missingId] = person;
_logger.LogInformation("通过 UserId {UserId} 找到对应的 EventRegistrationPerson: {PersonId}", missingId, person.Id);
personDict[userId] = person;
_logger.LogInformation("通过 UserId {UserId} 找到对应的 EventRegistrationPerson: {PersonId}", userId, person.Id);
}
else
{
_logger.LogWarning("无法找到人员ID: {PersonId}", missingId);
_logger.LogWarning("无法通过 UserId {UserId} 找到对应的 EventRegistrationPerson", userId);
}
}
}
else
{
// 页面类型为运动员(1)或其他,直接获取人员ID
_logger.LogInformation("页面类型为运动员,直接获取人员信息");
// 直接从 EventRegistrationPerson 表获取所有人员
var directPersons = await _personRepository.GetListAsync(x => input.PersonIds.Contains(x.Id));
foreach (var person in directPersons)
{
personDict[person.Id] = person;
_logger.LogInformation("直接获取 EventRegistrationPerson: {PersonId}", person.Id);
}
}
// 使用最终找到的人员ID列表
var foundPersonIds = personDict.Values.Select(p => p.Id).ToList();
@ -677,15 +679,15 @@ public class EventScoreService : BaseService<EventResult>, ITransient
/// <summary>
/// 根据页面类型获取允许的人员类型列表
/// </summary>
/// <param name="pageType">页面类型</param>
/// <param name="pageType">页面类型:1-运动员页面,2-教练页面,3-领队页面</param>
/// <returns>允许的人员类型列表</returns>
private List<PersonType> GetAllowedPersonTypes(CertificatePageType pageType)
private List<PersonType> GetAllowedPersonTypes(int pageType)
{
return pageType switch
{
CertificatePageType.Athlete => new List<PersonType> { PersonType.Athlete, PersonType.User },
CertificatePageType.Coach => new List<PersonType> { PersonType.Coach },
CertificatePageType.Leader => new List<PersonType> { PersonType.Leader },
1 => new List<PersonType> { PersonType.Athlete, PersonType.User },
2 => new List<PersonType> { PersonType.Coach },
3 => new List<PersonType> { PersonType.Leader },
_ => new List<PersonType> { PersonType.Athlete, PersonType.User, PersonType.Coach, PersonType.Leader }
};
}

View File

@ -1160,7 +1160,8 @@ public class UnifiedRegistrationService : ITransient
// 2. 再检查成员项目限报 - 检查队员是否超过个人/团队项目的限报数量
// 3. 然后检查人员项目组别重复 - 检查同一人员是否在同一项目的多个组别中重复报名
// 4. 再检查人员是否已报名 - 检查人员是否已在其他报名记录中报过同一项目组别
// 5. 最后检查项目完成人数 - 确保人数与项目的完成人数完全一致
// 5. 再检查团队成员冲突 - 检查同一人员是否在同一项目的多个团队中
// 6. 最后检查项目完成人数 - 确保人数与项目的完成人数完全一致
// 检查1:团队组别容量限制
// 验证是否有组别超过了限报的团队数量(例如:某组别限报2队,但本次报了3队)
@ -1178,7 +1179,11 @@ public class UnifiedRegistrationService : ITransient
// 验证人员是否已在其他已支付的报名记录中报过同一项目组别(避免重复占用名额)
CheckPersonAlreadyRegisteredAsync(input, result, eventProjects, projects, eventProjectGroups, registrationPersons, allRegistrationItems, eventRegistrationIds);
// 检查5:项目完成人数验证
// 检查5:团队成员冲突检测
// 验证同一人员是否在同一项目的多个团队中(例如:同一队员既在1队又在2队)
CheckTeamMemberConflictAsync(input, result, eventProjects, projects, eventProjectGroups);
// 检查6:项目完成人数验证
// 验证用户选择的参与人数必须与项目的"完成人数"完全一致(例如:项目要求完成3人,但只选了2人或选了4人)
CheckProjectCompletionAsync(input, result, eventProjects, projects, eventProjectGroups);
@ -1595,6 +1600,112 @@ public class UnifiedRegistrationService : ITransient
}
}
/// <summary>
/// 检查团队成员冲突
/// 验证同一人员是否在同一项目的多个团队中(例如:同一队员既在1队又在2队)
/// </summary>
private void CheckTeamMemberConflictAsync(
UnifiedRegistrationInput input,
RegistrationLimitCheckOutput result,
List<EventProject> eventProjects,
List<YD_Event.Core.Entities.Project> projects,
List<EventProjectGroup> eventProjectGroups)
{
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);
if (project == null) continue;
// 判断是否为团队项目
var isTeamProject = project.IsTeamProject();
if (!isTeamProject) continue;
// 获取所有团队
var teams = GetTeamsFromItem(item);
if (teams.Count <= 1) continue;
// 建立用户到团队的映射,用于检测冲突
var userTeamMap = new Dictionary<long, List<(int TeamIndex, string MemberName)>>();
foreach (var team in teams)
{
if (team.Persons == null || !team.Persons.Any()) continue;
foreach (var person in team.Persons)
{
if (!person.UserId.HasValue) continue;
var userId = person.UserId.Value;
var memberName = person.Name ?? "未知人员";
if (!userTeamMap.TryGetValue(userId, out var teamsList))
{
teamsList = new List<(int, string)>();
userTeamMap[userId] = teamsList;
}
teamsList.Add((team.TeamIndex, memberName));
}
}
// 检测冲突:同一用户在多个团队中
foreach (var kvp in userTeamMap)
{
if (kvp.Value.Count > 1)
{
var memberName = kvp.Value.First().MemberName;
var firstTeamIndex = kvp.Value.First().TeamIndex;
var projectName = project.Name ?? "未知项目";
// 提示信息格式:"XXX已在xx项目x队"
var errorMessage = $"{memberName}已在{projectName}{GetTeamNumber(firstTeamIndex)}";
_logger.LogWarning("团队成员冲突检测:人员{MemberName}已在项目{ProjectName}的多个团队中",
memberName, projectName);
result.HasLimit = true;
result.LimitDetails.Add(CreateLimitDetail(
kvp.Key,
memberName,
"TeamMemberConflict",
errorMessage));
}
}
}
}
/// <summary>
/// 获取团队序号的中文表示
/// </summary>
private static string GetTeamNumber(int teamIndex)
{
return teamIndex switch
{
1 => "1队",
2 => "2队",
3 => "3队",
4 => "4队",
5 => "5队",
6 => "6队",
7 => "7队",
8 => "8队",
9 => "9队",
10 => "10队",
_ => $"{teamIndex}队"
};
}
/// <summary>
/// 检查项目完成人数
/// </summary>

View File

@ -5897,7 +5897,7 @@
</member>
<member name="P:YD_Event.Application.Event.Dtos.IssueCertificateInput.PageType">
<summary>
页面类型:1-运动员页面,2-教练页面,3-领队页面
页面类型:0-未指定,1-运动员页面,2-教练页面,3-领队页面
用于明确区分不同角色页面的证书生成逻辑
</summary>
</member>
@ -6758,7 +6758,27 @@
</member>
<member name="P:YD_Event.Application.Event.Dtos.UserGroupMemberInput.MemberIds">
<summary>
成员ID列表
成员ID列表(单个团队报名时使用,向后兼容)
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.UserGroupMemberInput.Teams">
<summary>
团队列表(支持多个团队报名同一项目组别)
</summary>
</member>
<member name="T:YD_Event.Application.Event.Dtos.UserTeamInput">
<summary>
用户团队输入DTO
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.UserTeamInput.TeamIndex">
<summary>
团队序号(用于显示,如"团队一"、"团队二")
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.UserTeamInput.MemberIds">
<summary>
该团队的成员ID列表
</summary>
</member>
<member name="T:YD_Event.Application.Event.Dtos.UserOrderPreviewOutput">
@ -7755,11 +7775,18 @@
发放证书(支持模板选择和批量发放)
</summary>
</member>
<member name="M:YD_Event.Application.Event.EventScoreService.GetAllowedPersonTypes(YD_Event.Core.Enum.CertificatePageType)">
<member name="M:YD_Event.Application.Event.EventScoreService.IsValidPageType(System.Int32)">
<summary>
验证 PageType 参数是否有效
</summary>
<param name="pageType">页面类型值</param>
<returns>是否有效</returns>
</member>
<member name="M:YD_Event.Application.Event.EventScoreService.GetAllowedPersonTypes(System.Int32)">
<summary>
根据页面类型获取允许的人员类型列表
</summary>
<param name="pageType">页面类型</param>
<param name="pageType">页面类型:1-运动员页面,2-教练页面,3-领队页面</param>
<returns>允许的人员类型列表</returns>
</member>
<member name="M:YD_Event.Application.Event.EventScoreService.BuildPlaceholderData(YD_Event.Core.Entities.EventRegistrationPerson,YD_Event.Core.Entities.EventResult)">
@ -8143,6 +8170,17 @@
检查人员是否已在其他报名中报过同一项目组别
</summary>
</member>
<member name="M:YD_Event.Application.Event.UnifiedRegistrationService.CheckTeamMemberConflictAsync(YD_Event.Application.Event.Dtos.UnifiedRegistrationInput,YD_Event.Application.Event.Dtos.RegistrationLimitCheckOutput,System.Collections.Generic.List{YD_Event.Core.Entities.EventProject},System.Collections.Generic.List{YD_Event.Core.Entities.Project},System.Collections.Generic.List{YD_Event.Core.Entities.EventProjectGroup})">
<summary>
检查团队成员冲突
验证同一人员是否在同一项目的多个团队中(例如:同一队员既在1队又在2队)
</summary>
</member>
<member name="M:YD_Event.Application.Event.UnifiedRegistrationService.GetTeamNumber(System.Int32)">
<summary>
获取团队序号的中文表示
</summary>
</member>
<member name="M:YD_Event.Application.Event.UnifiedRegistrationService.CheckProjectCompletionAsync(YD_Event.Application.Event.Dtos.UnifiedRegistrationInput,YD_Event.Application.Event.Dtos.RegistrationLimitCheckOutput,System.Collections.Generic.List{YD_Event.Core.Entities.EventProject},System.Collections.Generic.List{YD_Event.Core.Entities.Project},System.Collections.Generic.List{YD_Event.Core.Entities.EventProjectGroup})">
<summary>
检查项目完成人数

View File

@ -1,33 +0,0 @@
using System.ComponentModel;
namespace YD_Event.Core.Enum;
/// <summary>
/// 证书页面类型
/// </summary>
public enum CertificatePageType
{
/// <summary>
/// 未指定(向后兼容)
/// </summary>
[Description("未指定")]
Unspecified = 0,
/// <summary>
/// 运动员页面
/// </summary>
[Description("运动员页面")]
Athlete = 1,
/// <summary>
/// 教练页面
/// </summary>
[Description("教练页面")]
Coach = 2,
/// <summary>
/// 领队页面
/// </summary>
[Description("领队页面")]
Leader = 3
}

View File

@ -4295,31 +4295,6 @@
禁用
</summary>
</member>
<member name="T:YD_Event.Core.Enum.CertificatePageType">
<summary>
证书页面类型
</summary>
</member>
<member name="F:YD_Event.Core.Enum.CertificatePageType.Unspecified">
<summary>
未指定(向后兼容)
</summary>
</member>
<member name="F:YD_Event.Core.Enum.CertificatePageType.Athlete">
<summary>
运动员页面
</summary>
</member>
<member name="F:YD_Event.Core.Enum.CertificatePageType.Coach">
<summary>
教练页面
</summary>
</member>
<member name="F:YD_Event.Core.Enum.CertificatePageType.Leader">
<summary>
领队页面
</summary>
</member>
<member name="T:YD_Event.Core.Enum.ChargeMode">
<summary>
收费模式枚举