一个单位报名一个比赛可以报多个队伍

This commit is contained in:
马梓彭 2026-10-12 14:29:18 +08:00
parent 6982ba8ac5
commit 1669913e1a
12 changed files with 661 additions and 249 deletions

View File

@ -452,6 +452,11 @@ public class CancelPreviewOutput
/// 是否需要填写收款人信息(线下支付且已支付时需要)
/// </summary>
public bool NeedPayeeInfo { get; set; }
/// <summary>
/// 是否允许部分取消(多团队合并报名仅允许整单取消)
/// </summary>
public bool AllowPartialCancel { get; set; } = true;
}
/// <summary>
@ -827,6 +832,21 @@ public class CancelExecuteOutput
/// </summary>
public class RegistrationItemWithMembersDto
{
/// <summary>
/// 来源团队ID
/// </summary>
public long? TeamId { get; set; }
/// <summary>
/// 来源团队名称
/// </summary>
public string? TeamName { get; set; }
/// <summary>
/// 队伍序号
/// </summary>
public int? TeamIndex { get; set; }
/// <summary>
/// 报名项目ID
/// </summary>

View File

@ -352,6 +352,21 @@ public class UserEventProjectGroupOutput
/// </summary>
public class UserEventProjectListOutput
{
/// <summary>
/// 来源团队ID(报名预览时返回)
/// </summary>
public long? TeamId { get; set; }
/// <summary>
/// 来源团队名称(报名预览时返回)
/// </summary>
public string? TeamName { get; set; }
/// <summary>
/// 队伍序号(报名预览时返回)
/// </summary>
public int? TeamIndex { get; set; }
/// <summary>
/// 项目ID
/// </summary>
@ -743,10 +758,9 @@ public class UserSubmitRegistrationInput
public long? CouponId { get; set; }
/// <summary>
/// 团队ID (必填)
/// 团队ID(旧版单团队结构,保留兼容)
/// </summary>
[Required]
public long TeamId { get; set; }
public long? TeamId { get; set; }
}
#region 参赛记录相关 DTOs
@ -1389,6 +1403,11 @@ public class EventLimitDetail
/// </summary>
public class GroupTeamAssignment
{
/// <summary>
/// 来源团队ID
/// </summary>
public long? TeamId { get; set; }
/// <summary>
/// 团队序号(例如:1, 2, 3)
/// </summary>

View File

@ -137,18 +137,32 @@ namespace YD_Event.Application.Event;
// 获取所有相关的人员和报名记录(用于统计团队数)
var personWithTeam = await _personRepository.AsQueryable()
.InnerJoin<EventRegistration>((p, r) => p.RegistrationId == r.Id)
.Where((p, r) => r.EventId == eventId
.InnerJoin<EventRegistrationItem>((p, r, item) => p.RegistrationItemId == item.Id)
.Where((p, r, item) => r.EventId == eventId
// 线上支付只显示已支付(1),线下支付显示待审核(0)和已支付(1)
&& ((r.PaymentMethod == 2 && r.PaymentStatus == 1) || (r.PaymentMethod == 1 && (r.PaymentStatus == 0 || r.PaymentStatus == 1)))
&& !p.DeleteMark
&& p.PersonType == PersonType.Athlete
&& r.TeamName != null)
.Select((p, r) => new { p.ProjectId, p.GroupName, r.TeamName, p.RegistrationItemId })
&& (item.TeamName != null || r.TeamName != null))
.Select((p, r, item) => new
{
p.ProjectId,
p.GroupName,
ItemTeamName = item.TeamName,
RegistrationTeamName = r.TeamName,
p.RegistrationItemId
})
.ToListAsync();
// 在内存中统计团队数(增加 RegistrationItemId 作为分组条件,与 Preview 方法保持一致)
var teamCounts = personWithTeam
.GroupBy(x => new { x.ProjectId, x.GroupName, x.TeamName, x.RegistrationItemId })
.GroupBy(x => new
{
x.ProjectId,
x.GroupName,
TeamName = x.ItemTeamName ?? x.RegistrationTeamName,
x.RegistrationItemId
})
.GroupBy(g => new { g.Key.ProjectId, g.Key.GroupName })
.Select(g => new { g.Key.ProjectId, g.Key.GroupName, Count = g.Count() })
.ToList();
@ -325,17 +339,31 @@ namespace YD_Event.Application.Event;
// 获取团队数
var personWithTeam = await _personRepository.AsQueryable()
.InnerJoin<EventRegistration>((p, r) => p.RegistrationId == r.Id)
.Where((p, r) => r.EventId == eventId
.InnerJoin<EventRegistrationItem>((p, r, item) => p.RegistrationItemId == item.Id)
.Where((p, r, item) => r.EventId == eventId
// 线上支付只显示已支付(1),线下支付显示待审核(0)和已支付(1)
&& ((r.PaymentMethod == 2 && r.PaymentStatus == 1) || (r.PaymentMethod == 1 && (r.PaymentStatus == 0 || r.PaymentStatus == 1)))
&& !p.DeleteMark
&& p.PersonType == PersonType.Athlete
&& r.TeamName != null)
.Select((p, r) => new { p.ProjectId, p.GroupName, r.TeamName, p.RegistrationItemId })
&& (item.TeamName != null || r.TeamName != null))
.Select((p, r, item) => new
{
p.ProjectId,
p.GroupName,
ItemTeamName = item.TeamName,
RegistrationTeamName = r.TeamName,
p.RegistrationItemId
})
.ToListAsync();
var teamCounts = personWithTeam
.GroupBy(x => new { x.ProjectId, x.GroupName, x.TeamName, x.RegistrationItemId })
.GroupBy(x => new
{
x.ProjectId,
x.GroupName,
TeamName = x.ItemTeamName ?? x.RegistrationTeamName,
x.RegistrationItemId
})
.GroupBy(g => new { g.Key.ProjectId, g.Key.GroupName })
.Select(g => new { g.Key.ProjectId, g.Key.GroupName, Count = g.Count() })
.ToList();
@ -357,19 +385,21 @@ namespace YD_Event.Application.Event;
// 第一步:先查询主要数据和相关的 MerchantCertificationId
var basicPersons = await _personRepository.AsQueryable()
.InnerJoin<EventRegistration>((p, r) => p.RegistrationId == r.Id)
.Where((p, r) => p.ProjectId == eventProj.Id
.InnerJoin<EventRegistrationItem>((p, r, item) => p.RegistrationItemId == item.Id)
.Where((p, r, item) => p.ProjectId == eventProj.Id
// 线上支付只显示已支付(1),线下支付显示待审核(0)和已支付(1)
&& ((r.PaymentMethod == 2 && r.PaymentStatus == 1) || (r.PaymentMethod == 1 && (r.PaymentStatus == 0 || r.PaymentStatus == 1)))
&& !p.DeleteMark
&& p.PersonType == PersonType.Athlete)
.Select((p, r) => new {
.Select((p, r, item) => new {
Id = p.Id,
RegistrationItemId = p.RegistrationItemId,
p.UserId,
p.Name,
p.GroupName,
p.EventProjectGroupId,
r.TeamName,
ItemTeamName = item.TeamName,
RegistrationTeamName = r.TeamName,
r.MerchantCertificationId,
Duration = projTotalDuration
})
@ -398,7 +428,7 @@ namespace YD_Event.Application.Event;
bp.Name,
bp.GroupName,
bp.EventProjectGroupId,
bp.TeamName,
TeamName = bp.ItemTeamName ?? bp.RegistrationTeamName,
MerchantName = bp.MerchantCertificationId.HasValue && merchantDict.ContainsKey(bp.MerchantCertificationId.Value)
? merchantDict[bp.MerchantCertificationId.Value].MerchantName
: null,

View File

@ -1019,18 +1019,19 @@ public class EventRegistrationService : BaseService<EventRegistration>, ITransie
// 统计符合条件的所有运动员(与赛事编排页面保持一致的筛选规则)
var athletes = await _personRepository.AsQueryable()
.InnerJoin<EventRegistration>((p, r) => p.RegistrationId == r.Id)
.Where((p, r) => r.EventId == eventId
.InnerJoin<EventRegistrationItem>((p, r, item) => p.RegistrationItemId == item.Id)
.Where((p, r, item) => r.EventId == eventId
&& !r.DeleteMark // 排除软删除的报名记录
// 线上支付只显示已支付(1),线下支付显示待审核(0)和已支付(1)
&& ((r.PaymentMethod == 2 && r.PaymentStatus == 1) || (r.PaymentMethod == 1 && (r.PaymentStatus == 0 || r.PaymentStatus == 1)))
&& !p.DeleteMark // 排除已删除的人员
&& p.PersonType == PersonType.Athlete) // 只统计运动员
.Select((p, r) => new
.Select((p, r, item) => new
{
PersonId = p.Id,
EventProjectId = p.ProjectId,
GroupName = p.GroupName,
TeamName = r.TeamName,
TeamName = item.TeamName ?? r.TeamName,
RegistrationItemId = p.RegistrationItemId,
UserId = p.UserId,
RegistrationId = r.Id,

View File

@ -133,7 +133,8 @@ public class EventScoreService : BaseService<EventResult>, ITransient
.LeftJoin<EventRegistration>((p, r) => p.RegistrationId == r.Id)
.LeftJoin<EventResult>((p, r, res) => p.Id == res.RegistrationPersonId)
.LeftJoin<EventCertificate>((p, r, res, c) => res.Id == c.ResultId)
.Where((p, r, res, c) => r.EventId == input.EventId
.LeftJoin<EventRegistrationItem>((p, r, res, c, item) => p.RegistrationItemId == item.Id)
.Where((p, r, res, c, item) => r.EventId == input.EventId
// 线上支付只显示已支付(1),线下支付显示待审核(0)和已支付(1)
&& ((r.PaymentMethod == 2 && r.PaymentStatus == 1) || (r.PaymentMethod == 1 && (r.PaymentStatus == 0 || r.PaymentStatus == 1)))
&& (p.PersonType == PersonType.Athlete || p.PersonType == PersonType.User));
@ -141,31 +142,32 @@ public class EventScoreService : BaseService<EventResult>, ITransient
// 应用筛选条件
if (input.ProjectId.HasValue)
{
baseQuery = baseQuery.Where((p, r, res, c) => p.ProjectId == input.ProjectId.Value);
baseQuery = baseQuery.Where((p, r, res, c, item) => p.ProjectId == input.ProjectId.Value);
}
if (input.EventProjectGroupId.HasValue)
{
baseQuery = baseQuery.Where((p, r, res, c) => p.EventProjectGroupId == input.EventProjectGroupId.Value);
baseQuery = baseQuery.Where((p, r, res, c, item) => p.EventProjectGroupId == input.EventProjectGroupId.Value);
}
else if (!string.IsNullOrWhiteSpace(input.GroupName))
{
baseQuery = baseQuery.Where((p, r, res, c) => p.GroupName == input.GroupName);
baseQuery = baseQuery.Where((p, r, res, c, item) => p.GroupName == input.GroupName);
}
if (!string.IsNullOrWhiteSpace(input.Name))
{
baseQuery = baseQuery.Where((p, r, res, c) => p.Name.Contains(input.Name));
baseQuery = baseQuery.Where((p, r, res, c, item) => p.Name.Contains(input.Name));
}
if (!string.IsNullOrWhiteSpace(input.UnitName))
{
baseQuery = baseQuery.Where((p, r, res, c) => r.TeamName.Contains(input.UnitName));
baseQuery = baseQuery.Where((p, r, res, c, item) =>
(item.TeamName ?? r.TeamName ?? string.Empty).Contains(input.UnitName));
}
if (input.Gender.HasValue)
{
baseQuery = baseQuery.Where((p, r, res, c) => p.Gender == input.Gender.Value);
baseQuery = baseQuery.Where((p, r, res, c, item) => p.Gender == input.Gender.Value);
}
// 步骤1.5:获取所有相关的单位认证信息
@ -193,8 +195,8 @@ public class EventScoreService : BaseService<EventResult>, ITransient
// 查询基本数据到内存
var baseData = await baseQuery
.OrderBy((p, r, res, c) => res.Rank)
.Select((p, r, res, c) => new
.OrderBy((p, r, res, c, item) => res.Rank)
.Select((p, r, res, c, item) => new
{
Id = p.Id,
Name = p.Name,
@ -208,7 +210,7 @@ public class EventScoreService : BaseService<EventResult>, ITransient
CertificateId = c.Id,
EventProjectId = p.ProjectId, // 注意:这里 p.ProjectId 实际上是 EventProject 的 Id
GroupName = p.GroupName ?? string.Empty,
TeamName = r.TeamName ?? string.Empty,
TeamName = item.TeamName ?? r.TeamName ?? string.Empty,
RegistrationItemId = p.RegistrationItemId // 增加报名项目唯一码,用于分组
})
.ToListAsync();
@ -557,10 +559,11 @@ public class EventScoreService : BaseService<EventResult>, ITransient
// 步骤2:批量预加载该赛事的所有人员数据(包含团队信息)
var allPersonsWithTeam = await _personRepository.AsQueryable()
.InnerJoin<EventRegistration>((p, r) => p.RegistrationId == r.Id)
.Where((p, r) => r.EventId == eventId
.InnerJoin<EventRegistrationItem>((p, r, item) => p.RegistrationItemId == item.Id)
.Where((p, r, item) => r.EventId == eventId
// 线上支付只显示已支付(1),线下支付显示待审核(0)和已支付(1)
&& ((r.PaymentMethod == 2 && r.PaymentStatus == 1) || (r.PaymentMethod == 1 && (r.PaymentStatus == 0 || r.PaymentStatus == 1))))
.Select((p, r) => new {
.Select((p, r, item) => new {
p.Id,
p.RegistrationItemId,
p.Name,
@ -568,7 +571,7 @@ public class EventScoreService : BaseService<EventResult>, ITransient
p.GroupName,
p.RegistrationId,
p.ProjectId,
r.TeamName
TeamName = item.TeamName ?? r.TeamName
})
.ToListAsync();
@ -833,8 +836,9 @@ public class EventScoreService : BaseService<EventResult>, ITransient
// 步骤3:批量预加载该赛事的所有人员数据(包含团队信息)
var allPersonsWithTeam = await _personRepository.AsQueryable()
.InnerJoin<EventRegistration>((p, r) => p.RegistrationId == r.Id)
.Where((p, r) => r.EventId == input.EventId && !p.DeleteMark)
.Select((p, r) => new {
.InnerJoin<EventRegistrationItem>((p, r, item) => p.RegistrationItemId == item.Id)
.Where((p, r, item) => r.EventId == input.EventId && !p.DeleteMark)
.Select((p, r, item) => new {
p.Id,
p.PersonType,
p.UserId,
@ -846,7 +850,7 @@ public class EventScoreService : BaseService<EventResult>, ITransient
p.RegistrationItemId,
p.EventProjectGroupId,
p.DeleteMark,
r.TeamName,
TeamName = item.TeamName ?? r.TeamName,
r.MerchantCertificationId,
Person = p
})

View File

@ -654,6 +654,50 @@ public class UnifiedRegistrationService : ITransient
return persons;
}
/// <summary>
/// 根据已保存的报名明细重建统一报名结构,用于支付确认前再次校验报名限制。
/// 同一项目组别的多个报名明细会还原为多个参赛队伍,避免少算队伍数量。
/// </summary>
private static List<UnifiedRegistrationItemInput> BuildUnifiedItemsFromStoredDetails(
IEnumerable<EventRegistrationItem> items,
IEnumerable<EventRegistrationPerson> persons,
string? fallbackTeamName)
{
var activePersons = persons.Where(person => !person.DeleteMark).ToList();
return items
.Where(item => !item.DeleteMark)
.GroupBy(item => new { item.ProjectId, item.EventProjectGroupId })
.Select(itemGroup => new UnifiedRegistrationItemInput
{
ProjectId = itemGroup.Key.ProjectId,
GroupId = itemGroup.Key.EventProjectGroupId,
Teams = itemGroup
.OrderBy(item => item.TeamIndex ?? int.MaxValue)
.ThenBy(item => item.Id)
.Select((item, index) => new UnifiedTeamInput
{
TeamId = item.TeamId,
TeamName = item.TeamName ?? fallbackTeamName,
TeamIndex = item.TeamIndex.GetValueOrDefault(index + 1),
Persons = activePersons
.Where(person => person.RegistrationItemId == item.Id)
.Select(person => new UnifiedPersonInput
{
UserId = person.UserId,
Name = person.Name,
Gender = person.Gender,
IdCardNumber = person.IdCard,
BirthDate = person.BirthDate,
Mobile = person.Phone
})
.ToList()
})
.ToList()
})
.ToList();
}
/// <summary>
/// 获取人员证件号码(根据认证类型返回身份证号或护照号)
/// </summary>
@ -824,6 +868,11 @@ public class UnifiedRegistrationService : ITransient
RegistrationId = registration.Id,
ProjectId = item.ProjectId,
EventProjectGroupId = item.GroupId,
TeamId = team.TeamId,
TeamName = team.TeamName,
TeamIndex = baseProject?.IsTeamProject() == true
? (team.TeamIndex > 0 ? team.TeamIndex : 1)
: null,
ProjectName = baseProject?.Name ?? "",
GroupName = groupName,
ProjectTypeName = baseProject?.ProjectType.Description() ?? "",
@ -1275,24 +1324,7 @@ public class UnifiedRegistrationService : ITransient
var items = await _registrationItemRepository.GetListAsync(x => x.RegistrationId == registration.Id);
var persons = await _registrationPersonRepository.GetListAsync(x => x.RegistrationId == registration.Id);
unifiedInput.Items = items.Select(item =>
{
var itemPersons = persons.Where(p => p.RegistrationItemId == item.Id).ToList();
return new UnifiedRegistrationItemInput
{
ProjectId = item.ProjectId,
GroupId = item.EventProjectGroupId,
Persons = itemPersons.Select(p => new UnifiedPersonInput
{
UserId = p.UserId,
Name = p.Name,
Gender = p.Gender,
IdCardNumber = p.IdCard,
BirthDate = p.BirthDate,
Mobile = p.Phone
}).ToList()
};
}).ToList();
unifiedInput.Items = BuildUnifiedItemsFromStoredDetails(items, persons, registration.TeamName);
var limitCheck = await CheckRegistrationLimitAsync(unifiedInput, registration.Id);
if (limitCheck.HasLimit)
@ -2492,24 +2524,7 @@ public class UnifiedRegistrationService : ITransient
var items = await _registrationItemRepository.GetListAsync(x => x.RegistrationId == reg.Id);
var persons = await _registrationPersonRepository.GetListAsync(x => x.RegistrationId == reg.Id);
unifiedInput.Items = items.Select(item =>
{
var itemPersons = persons.Where(p => p.RegistrationItemId == item.Id).ToList();
return new UnifiedRegistrationItemInput
{
ProjectId = item.ProjectId,
GroupId = item.EventProjectGroupId,
Persons = itemPersons.Select(p => new UnifiedPersonInput
{
UserId = p.UserId,
Name = p.Name,
Gender = p.Gender,
IdCardNumber = p.IdCard,
BirthDate = p.BirthDate,
Mobile = p.Phone
}).ToList()
};
}).ToList();
unifiedInput.Items = BuildUnifiedItemsFromStoredDetails(items, persons, reg.TeamName);
var limitCheck = await CheckRegistrationLimitAsync(unifiedInput, reg.Id);
if (limitCheck.HasLimit)
@ -2991,6 +3006,20 @@ public class UnifiedRegistrationService : ITransient
return roundedAmount;
}
/// <summary>
/// 判断报名单是否包含多个来源团队。
/// 来源团队与同一项目下的一队、二队不是同一概念,以报名项的 TeamId 为准。
/// </summary>
private static bool IsMultiSourceTeamRegistration(IEnumerable<EventRegistrationItem> items)
{
return items
.Where(x => !x.DeleteMark && x.TeamId.HasValue)
.Select(x => x.TeamId!.Value)
.Distinct()
.Take(2)
.Count() > 1;
}
public async Task<decimal> CalculateRefundAmountAsync(decimal totalAmount, DateTime eventStartTime)
{
var refundAmount = await _refundConfigService.CalculateRefundAmount(totalAmount, eventStartTime);
@ -3117,7 +3146,8 @@ public class UnifiedRegistrationService : ITransient
CurrentRefundAmount = refundableAmount,
PaymentMethod = registration.PaymentMethod,
PaymentStatus = registration.PaymentStatus,
NeedPayeeInfo = registration.PaymentMethod == 1 && registration.PaymentStatus == 1
NeedPayeeInfo = registration.PaymentMethod == 1 && registration.PaymentStatus == 1,
AllowPartialCancel = !IsMultiSourceTeamRegistration(items)
};
foreach (var item in items.Where(x => !x.DeleteMark))
@ -3189,7 +3219,7 @@ public class UnifiedRegistrationService : ITransient
};
// 处理该项目组别下的所有报名项,每个报名项作为一个独立的团队
int teamIndex = 0;
int fallbackTeamIndex = 1;
foreach (var item in groupGroup)
{
// 获取该报名项目下的所有队员 - 只获取未被软删除的
@ -3198,10 +3228,11 @@ public class UnifiedRegistrationService : ITransient
// 每个报名项作为一个独立的团队条目
var teamDto = new CancelPreviewTeamDto
{
TeamName = "",
TeamIndex = teamIndex++,
TeamName = item.TeamName ?? registration.TeamName ?? "",
TeamIndex = item.TeamIndex ?? fallbackTeamIndex,
RegistrationItemId = item.Id
};
fallbackTeamIndex++;
foreach (var person in itemPersons)
{
@ -3279,6 +3310,11 @@ public class UnifiedRegistrationService : ITransient
var items = await _db.Queryable<EventRegistrationItem>().Where(x => x.RegistrationId == input.RegistrationId).ToListAsync();
var persons = await _registrationPersonRepository.GetListAsync(x => x.RegistrationId == input.RegistrationId);
if (input.CancelType == 1 && IsMultiSourceTeamRegistration(items))
{
throw Oops.Bah("多团队报名仅支持整单取消");
}
// 查询订单获取实际支付金额(考虑优惠券抵扣)
var order = await _orderRepository.GetFirstAsync(x => x.RegistrationId == input.RegistrationId && x.OrderType == 1 && !x.DeleteMark);
var actualPaidAmount = order?.ActualAmount ?? registration.TotalAmount;
@ -3655,6 +3691,11 @@ public class UnifiedRegistrationService : ITransient
var items = await _registrationItemRepository.GetListAsync(x => x.RegistrationId == registration.Id);
var persons = await _registrationPersonRepository.GetListAsync(x => x.RegistrationId == registration.Id);
if (input.CancelType == 1 && IsMultiSourceTeamRegistration(items))
{
throw Oops.Bah("多团队报名仅支持整单取消");
}
// 计算实际退款金额
// 【重要】始终信任后端计算结果,前端计算只用于展示
// 不接受前端传递的退款金额,避免资金风险
@ -4130,6 +4171,12 @@ public class UnifiedRegistrationService : ITransient
{
var persons = await _registrationPersonRepository.GetListAsync(x => x.RegistrationId == order.RegistrationId);
var items = await _registrationItemRepository.GetListAsync(x => x.RegistrationId == order.RegistrationId);
if (IsMultiSourceTeamRegistration(items))
{
throw Oops.Bah("多团队报名仅支持整单取消");
}
var totalPersons = persons.Count;
if (totalPersons == 0)
{

View File

@ -713,6 +713,9 @@ public class UserEventRecordService : IDynamicApiController, ITransient
result.Items.Add(new RegistrationItemWithMembersDto
{
TeamId = item.TeamId,
TeamName = item.TeamName ?? registration.TeamName,
TeamIndex = item.TeamIndex,
RegistrationItemId = item.Id,
ProjectId = item.ProjectId,
EventProjectGroupId = item.EventProjectGroupId ?? 0,
@ -775,6 +778,11 @@ public class UserEventRecordService : IDynamicApiController, ITransient
// 获取报名项目信息
var registrationItems = await _registrationItemRepository.GetListAsync(x => x.RegistrationId == input.RegistrationId);
if (registrationItems.Where(item => item.TeamId.HasValue).Select(item => item.TeamId).Distinct().Count() > 1)
{
throw Oops.Bah("多团队报名不支持单独调整运动员,请取消整单后重新报名");
}
var eventProjectIds = registrationItems.Select(x => x.ProjectId).Distinct().ToList();
var eventProjects = await _eventProjectRepository.GetListAsync(x => eventProjectIds.Contains(x.Id));
var eventProjectGroups = await _eventProjectGroupRepository.GetListAsync(x => eventProjectIds.Contains(x.EventProjectId));

View File

@ -507,6 +507,121 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient
return idCard.Substring(0, 6) + "********" + idCard.Substring(idCard.Length - 4);
}
private async Task<Dictionary<long, YD_Event.Core.Entities.Team>> GetAccessibleTeamsAsync(
long userId,
IEnumerable<long> teamIds)
{
var distinctTeamIds = teamIds.Where(id => id > 0).Distinct().ToList();
if (!distinctTeamIds.Any())
{
throw Oops.Bah("请至少选择一个团队");
}
var teams = await _db.Queryable<YD_Event.Core.Entities.Team>()
.Where(team => distinctTeamIds.Contains(team.Id) && !team.DeleteMark)
.ToListAsync();
if (teams.Count != distinctTeamIds.Count)
{
throw Oops.Bah("部分团队不存在或已被删除,请刷新后重试");
}
var certification = await _db.Queryable<MerchantCertification>()
.FirstAsync(item => item.UserId == userId && !item.DeleteMark);
var hasUnauthorizedTeam = certification != null
? teams.Any(team => team.MerchantCertificationId != certification.Id)
: teams.Any(team => team.CreatedUserId != userId || team.MerchantCertificationId != null);
if (hasUnauthorizedTeam)
{
throw Oops.Bah("无权使用所选团队报名");
}
return teams.ToDictionary(team => team.Id);
}
private async Task ValidateTeamMemberAssignmentsAsync(
IEnumerable<(long TeamId, List<long> MemberIds)> assignments)
{
var normalizedAssignments = assignments
.Select(assignment => (
assignment.TeamId,
MemberIds: assignment.MemberIds.Where(id => id > 0).Distinct().ToList()))
.ToList();
var allMemberIds = normalizedAssignments.SelectMany(assignment => assignment.MemberIds).Distinct().ToList();
if (!allMemberIds.Any())
{
return;
}
var conflictingMember = normalizedAssignments
.SelectMany(assignment => assignment.MemberIds.Select(memberId => new { assignment.TeamId, MemberId = memberId }))
.GroupBy(item => item.MemberId)
.FirstOrDefault(group => group.Select(item => item.TeamId).Distinct().Count() > 1);
if (conflictingMember != null)
{
throw Oops.Bah("同一名运动员不能代表多个团队报名同一场赛事");
}
var teamIds = normalizedAssignments.Select(assignment => assignment.TeamId).Distinct().ToList();
var teamMembers = await _db.Queryable<TeamMember>()
.Where(member => teamIds.Contains(member.TeamId)
&& allMemberIds.Contains(member.UserId)
&& member.PersonType == PersonType.Athlete
&& !member.DeleteMark)
.ToListAsync();
var teamMemberKeys = teamMembers
.Select(member => (member.TeamId, member.UserId))
.ToHashSet();
var hasInvalidMember = normalizedAssignments.Any(assignment =>
assignment.MemberIds.Any(memberId => !teamMemberKeys.Contains((assignment.TeamId, memberId))));
if (hasInvalidMember)
{
throw Oops.Bah("部分运动员不属于对应团队,请刷新团队成员后重试");
}
}
private static string BuildTeamNameSummary(IReadOnlyCollection<YD_Event.Core.Entities.Team> teams)
{
var teamNames = teams.Select(team => team.Name).Where(name => !string.IsNullOrWhiteSpace(name)).Distinct().ToList();
return teamNames.Count switch
{
0 => "",
1 => teamNames[0],
_ => $"{teamNames[0]}等{teamNames.Count}支队伍"
};
}
private static void ValidateTeamAssignmentStructure(
IEnumerable<GroupTeamAssignment> assignments,
long? fallbackTeamId)
{
var normalizedAssignments = assignments
.Select(assignment => new
{
TeamId = assignment.TeamId ?? fallbackTeamId,
assignment.TeamIndex
})
.ToList();
if (normalizedAssignments.Any(assignment => !assignment.TeamId.HasValue || assignment.TeamId.Value <= 0))
{
throw Oops.Bah("报名队伍信息不完整,请重新选择团队");
}
if (normalizedAssignments.Any(assignment => assignment.TeamIndex <= 0))
{
throw Oops.Bah("队伍序号无效,请刷新后重试");
}
if (normalizedAssignments
.GroupBy(assignment => new { assignment.TeamId, assignment.TeamIndex })
.Any(group => group.Count() > 1))
{
throw Oops.Bah("同一团队的队伍序号不能重复");
}
}
/// <summary>
/// 3.2 分配队员接口 - 提交报名(含保存草稿)
/// </summary>
@ -524,82 +639,71 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient
var eventInfo = await _eventRepository.GetByIdAsync(input.EventId);
if (eventInfo == null) throw Oops.Bah("赛事不存在");
// 获取团队信息
var team = await _db.Queryable<YD_Event.Core.Entities.Team>().FirstAsync(t => t.Id == input.TeamId && !t.DeleteMark);
if (team == null) throw Oops.Bah("团队不存在");
// 收集所有成员ID
var allMemberIds = new List<long>();
if (input.GroupMembers != null)
var groupMembers = input.GroupMembers ?? new List<UserGroupMemberInput>();
var submittedAssignments = new List<(long TeamId, List<long> MemberIds)>();
foreach (var groupMember in groupMembers)
{
foreach (var gm in input.GroupMembers)
if (groupMember.Teams?.Any() == true)
{
// 检查是否有多团队数据
if (gm.Teams != null && gm.Teams.Count > 0)
ValidateTeamAssignmentStructure(groupMember.Teams, input.TeamId);
foreach (var teamAssignment in groupMember.Teams)
{
foreach (var t in gm.Teams)
var teamId = teamAssignment.TeamId ?? input.TeamId;
if (!teamId.HasValue || teamId.Value <= 0)
{
allMemberIds.AddRange(t.MemberIds ?? new List<long>());
throw Oops.Bah("报名队伍信息不完整,请重新选择团队");
}
submittedAssignments.Add((teamId.Value, teamAssignment.MemberIds ?? new List<long>()));
}
else
}
else
{
if (!input.TeamId.HasValue || input.TeamId.Value <= 0)
{
// 向后兼容单团队模式
allMemberIds.AddRange(gm.MemberIds ?? new List<long>());
throw Oops.Bah("报名队伍信息不完整,请重新选择团队");
}
submittedAssignments.Add((input.TeamId.Value, groupMember.MemberIds ?? new List<long>()));
}
}
else if (input.MemberIds != null)
if (!submittedAssignments.Any() && input.TeamId.HasValue && input.TeamId.Value > 0)
{
// 向后兼容旧结构
allMemberIds.AddRange(input.MemberIds);
submittedAssignments.Add((input.TeamId.Value, input.MemberIds ?? new List<long>()));
}
allMemberIds = allMemberIds.Distinct().ToList();
// 验证:一次报名只能选择单个团队内的运动员
if (allMemberIds.Count > 0)
{
var teamMembers = await _db.Queryable<YD_Event.Core.Entities.TeamMember>()
.Where(tm => allMemberIds.Contains(tm.UserId))
.Where(tm => !tm.DeleteMark)
.ToListAsync();
var selectedTeamMap = await GetAccessibleTeamsAsync(userId, submittedAssignments.Select(item => item.TeamId));
await ValidateTeamMemberAssignmentsAsync(submittedAssignments);
if (teamMembers == null || teamMembers.Count == 0)
{
throw Oops.Bah("未找到队员信息");
}
var userTeamMap = teamMembers
.GroupBy(tm => tm.UserId)
.ToDictionary(g => g.Key, g => g.Select(tm => tm.TeamId).ToList());
var missingUsers = allMemberIds.Except(userTeamMap.Keys).ToList();
if (missingUsers.Any())
{
throw Oops.Bah("部分队员未找到团队信息");
}
var allBelongToTeam = userTeamMap.All(ut => ut.Value.Contains(input.TeamId));
if (!allBelongToTeam)
{
throw Oops.Bah("所有选择的运动员必须隶属于同一个团队");
}
}
var allMemberIds = submittedAssignments
.SelectMany(item => item.MemberIds)
.Where(id => id > 0)
.Distinct()
.ToList();
// 获取成员信息
var members = await _userRepository.GetListAsync(x => allMemberIds.Contains(x.Id));
if (members.Count != allMemberIds.Count)
{
throw Oops.Bah("部分运动员不存在或已被删除,请刷新后重试");
}
// 获取所有组别信息
var groupIds = input.GroupMembers?.Select(g => g.GroupId).Distinct().ToList() ?? new List<long>();
var eventProjectGroups = await _eventProjectGroupRepository.GetListAsync(x => groupIds.Contains(x.Id));
var groupIds = groupMembers.Select(group => group.GroupId).Distinct().ToList();
var eventProjectGroups = await _eventProjectGroupRepository.GetListAsync(x =>
groupIds.Contains(x.Id) && x.EventId == input.EventId && !x.DeleteMark);
if (eventProjectGroups.Count != groupIds.Count)
{
throw Oops.Bah("部分报名组别不存在或不属于当前赛事,请刷新后重试");
}
// 构建统一报名输入
var unifiedInput = new UnifiedRegistrationInput
{
EventId = input.EventId,
RegistrationMode = 2, // PC端只做团队报名
TeamName = team.Name,
TeamName = BuildTeamNameSummary(selectedTeamMap.Values.ToList()),
ContactName = userName,
ContactPhone = userMobile,
Items = new List<UnifiedRegistrationItemInput>(),
@ -608,9 +712,9 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient
};
// 填充项目和人员信息
if (input.GroupMembers != null && input.GroupMembers.Any())
if (groupMembers.Any())
{
foreach (var groupMember in input.GroupMembers)
foreach (var groupMember in groupMembers)
{
var eventProjectGroup = eventProjectGroups.FirstOrDefault(g => g.Id == groupMember.GroupId);
if (eventProjectGroup == null) continue;
@ -624,49 +728,45 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient
Persons = new List<UnifiedPersonInput>()
};
// 检查是多团队模式还是单团队模式
var hasMultipleTeams = groupMember.Teams != null && groupMember.Teams.Count > 0;
if (hasMultipleTeams)
{
// ========== 多团队模式 ==========
foreach (var t in groupMember.Teams)
var teamAssignments = groupMember.Teams?.Any() == true
? groupMember.Teams
: new List<GroupTeamAssignment>
{
var teamMembersList = members.Where(m => t.MemberIds != null && t.MemberIds.Contains(m.Id)).ToList();
item.Teams.Add(new UnifiedTeamInput
new()
{
TeamIndex = t.TeamIndex,
Persons = teamMembersList.Select(m => new UnifiedPersonInput
{
UserId = m.Id,
Name = m.Name ?? "",
IdCardNumber = m.IdCardNumber ?? "",
PassportNumber = m.PassportNumber,
AuthType = m.AuthType,
Gender = m.Gender,
Mobile = m.Mobile,
BirthDate = m.BirthDate ?? DateTime.MinValue
}).ToList()
});
}
}
else
TeamId = input.TeamId,
TeamIndex = 1,
MemberIds = groupMember.MemberIds ?? new List<long>()
}
};
foreach (var teamAssignment in teamAssignments)
{
// ========== 单团队模式(向后兼容) ==========
var groupMembersList = members.Where(m => groupMember.MemberIds != null && groupMember.MemberIds.Contains(m.Id)).ToList();
if (!groupMembersList.Any()) continue;
item.Persons = groupMembersList.Select(m => new UnifiedPersonInput
var teamId = teamAssignment.TeamId ?? input.TeamId;
if (!teamId.HasValue || !selectedTeamMap.TryGetValue(teamId.Value, out var selectedTeam))
{
UserId = m.Id,
Name = m.Name ?? "",
IdCardNumber = m.IdCardNumber ?? "",
PassportNumber = m.PassportNumber,
AuthType = m.AuthType,
Gender = m.Gender,
Mobile = m.Mobile,
BirthDate = m.BirthDate ?? DateTime.MinValue
}).ToList();
throw Oops.Bah("报名队伍信息无效,请重新选择团队");
}
var assignmentMemberIds = (teamAssignment.MemberIds ?? new List<long>()).Distinct().ToHashSet();
var teamMembersList = members.Where(member => assignmentMemberIds.Contains(member.Id)).ToList();
item.Teams.Add(new UnifiedTeamInput
{
TeamId = selectedTeam.Id,
TeamName = selectedTeam.Name,
TeamIndex = teamAssignment.TeamIndex > 0 ? teamAssignment.TeamIndex : 1,
Persons = teamMembersList.Select(member => new UnifiedPersonInput
{
UserId = member.Id,
Name = member.Name ?? "",
IdCardNumber = member.IdCardNumber ?? "",
PassportNumber = member.PassportNumber,
AuthType = member.AuthType,
Gender = member.Gender,
Mobile = member.Mobile,
BirthDate = member.BirthDate ?? DateTime.MinValue
}).ToList()
});
}
unifiedInput.Items.Add(item);
@ -711,6 +811,9 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient
return new UserEventProjectListOutput
{
TeamId = i.TeamId,
TeamName = i.TeamName ?? registration.TeamName,
TeamIndex = i.TeamIndex,
Id = i.ProjectId,
Name = i.ProjectName,
GroupName = i.GroupName,
@ -1179,52 +1282,60 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient
string userName = sysUser.Name;
string userMobile = sysUser.Mobile ?? "";
// 获取团队信息 - 优先使用新结构的 TeamId
string teamName = "临时团队";
if (input.TeamId.HasValue)
var groupMembers = input.GroupMembers ?? new List<GroupMemberAssignment>();
var submittedAssignments = new List<(long TeamId, List<long> MemberIds)>();
foreach (var groupMember in groupMembers)
{
var team = await _db.Queryable<YD_Event.Core.Entities.Team>().FirstAsync(t => t.Id == input.TeamId.Value && !t.DeleteMark);
if (team != null)
if (groupMember.Teams?.Any() == true)
{
teamName = team.Name;
}
}
// 获取所有成员信息
var allMemberIds = new List<long>();
if (input.GroupMembers != null)
{
foreach (var gm in input.GroupMembers)
{
// 检查是否有多团队数据
if (gm.Teams != null && gm.Teams.Count > 0)
ValidateTeamAssignmentStructure(groupMember.Teams, input.TeamId);
foreach (var teamAssignment in groupMember.Teams)
{
foreach (var team in gm.Teams)
var teamId = teamAssignment.TeamId ?? input.TeamId;
if (!teamId.HasValue || teamId.Value <= 0)
{
allMemberIds.AddRange(team.MemberIds ?? new List<long>());
throw Oops.Bah("报名队伍信息不完整,请重新选择团队");
}
}
else
{
// 向后兼容单团队模式
allMemberIds.AddRange(gm.MemberIds ?? new List<long>());
submittedAssignments.Add((teamId.Value, teamAssignment.MemberIds ?? new List<long>()));
}
}
else if (input.TeamId.HasValue && input.TeamId.Value > 0)
{
submittedAssignments.Add((input.TeamId.Value, groupMember.MemberIds ?? new List<long>()));
}
}
else if (input.MemberIds != null)
if (!submittedAssignments.Any() && input.TeamId.HasValue && input.TeamId.Value > 0)
{
// 向后兼容旧结构
allMemberIds.AddRange(input.MemberIds);
submittedAssignments.Add((input.TeamId.Value, input.MemberIds ?? new List<long>()));
}
allMemberIds = allMemberIds.Distinct().ToList();
var selectedTeamMap = await GetAccessibleTeamsAsync(userId, submittedAssignments.Select(item => item.TeamId));
await ValidateTeamMemberAssignmentsAsync(submittedAssignments);
var allMemberIds = submittedAssignments
.SelectMany(item => item.MemberIds)
.Where(id => id > 0)
.Distinct()
.ToList();
// 获取成员信息
var members = await _userRepository.GetListAsync(x => allMemberIds.Contains(x.Id));
if (members.Count != allMemberIds.Count)
{
throw Oops.Bah("部分运动员不存在或已被删除,请刷新后重试");
}
// 获取组别信息
var groupIds = input.GroupMembers?.Select(g => g.GroupId).ToList() ?? input.GroupIds ?? new List<long>();
var eventProjectGroups = await _eventProjectGroupRepository.GetListAsync(x => groupIds.Contains(x.Id));
var groupIds = groupMembers.Any()
? groupMembers.Select(group => group.GroupId).Distinct().ToList()
: input.GroupIds?.Distinct().ToList() ?? new List<long>();
var eventProjectGroups = await _eventProjectGroupRepository.GetListAsync(x =>
groupIds.Contains(x.Id) && x.EventId == input.EventId && !x.DeleteMark);
if (eventProjectGroups.Count != groupIds.Count)
{
throw Oops.Bah("部分报名组别不存在或不属于当前赛事,请刷新后重试");
}
// 获取项目信息
var eventProjectIds = eventProjectGroups.Select(epg => epg.EventProjectId).Distinct().ToList();
@ -1235,7 +1346,7 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient
{
EventId = input.EventId,
RegistrationMode = 2, // PC端团队报名
TeamName = teamName,
TeamName = BuildTeamNameSummary(selectedTeamMap.Values.ToList()),
ContactName = userName,
ContactPhone = userMobile,
CreatedUserId = userId,
@ -1244,7 +1355,7 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient
};
// 为每个组别添加项目项
foreach (var groupMember in input.GroupMembers ?? new List<GroupMemberAssignment>())
foreach (var groupMember in groupMembers)
{
var eventProjectGroup = eventProjectGroups.FirstOrDefault(epg => epg.Id == groupMember.GroupId);
if (eventProjectGroup == null) continue;
@ -1261,54 +1372,52 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient
Persons = new List<UnifiedPersonInput>()
};
// 检查是多团队模式还是单团队模式
var hasMultipleTeams = groupMember.Teams != null && groupMember.Teams.Count > 0;
if (hasMultipleTeams)
{
// ========== 多团队模式 ==========
foreach (var team in groupMember.Teams)
var teamAssignments = groupMember.Teams?.Any() == true
? groupMember.Teams
: new List<GroupTeamAssignment>
{
var teamMembers = members.Where(m => team.MemberIds != null && team.MemberIds.Contains(m.Id)).ToList();
item.Teams.Add(new UnifiedTeamInput
new()
{
TeamIndex = team.TeamIndex,
Persons = teamMembers.Select(m => new UnifiedPersonInput
{
UserId = m.Id,
Name = m.Name ?? "",
IdCardNumber = m.IdCardNumber ?? "",
PassportNumber = m.PassportNumber,
AuthType = m.AuthType,
Gender = m.Gender,
Mobile = m.Mobile,
BirthDate = m.BirthDate ?? DateTime.MinValue
}).ToList()
});
}
}
else
TeamId = input.TeamId,
TeamIndex = 1,
MemberIds = groupMember.MemberIds ?? new List<long>()
}
};
foreach (var teamAssignment in teamAssignments)
{
// ========== 单团队模式(向后兼容) ==========
var groupMembersList = members.Where(m => groupMember.MemberIds != null && groupMember.MemberIds.Contains(m.Id)).ToList();
item.Persons = groupMembersList.Select(m => new UnifiedPersonInput
var teamId = teamAssignment.TeamId ?? input.TeamId;
if (!teamId.HasValue || !selectedTeamMap.TryGetValue(teamId.Value, out var selectedTeam))
{
UserId = m.Id,
Name = m.Name ?? "",
IdCardNumber = m.IdCardNumber ?? "",
PassportNumber = m.PassportNumber,
AuthType = m.AuthType,
Gender = m.Gender,
Mobile = m.Mobile,
BirthDate = m.BirthDate ?? DateTime.MinValue
}).ToList();
throw Oops.Bah("报名队伍信息无效,请重新选择团队");
}
var assignmentMemberIds = (teamAssignment.MemberIds ?? new List<long>()).Distinct().ToHashSet();
var teamMembers = members.Where(member => assignmentMemberIds.Contains(member.Id)).ToList();
item.Teams.Add(new UnifiedTeamInput
{
TeamId = selectedTeam.Id,
TeamName = selectedTeam.Name,
TeamIndex = teamAssignment.TeamIndex > 0 ? teamAssignment.TeamIndex : 1,
Persons = teamMembers.Select(member => new UnifiedPersonInput
{
UserId = member.Id,
Name = member.Name ?? "",
IdCardNumber = member.IdCardNumber ?? "",
PassportNumber = member.PassportNumber,
AuthType = member.AuthType,
Gender = member.Gender,
Mobile = member.Mobile,
BirthDate = member.BirthDate ?? DateTime.MinValue
}).ToList()
});
}
unifiedInput.Items.Add(item);
}
// 处理旧结构(向后兼容)
if ((input.GroupMembers == null || !input.GroupMembers.Any()) && input.GroupIds != null && input.MemberIds != null)
if (!groupMembers.Any() && input.GroupIds != null && input.MemberIds != null && input.TeamId.HasValue)
{
foreach (var groupId in input.GroupIds)
{
@ -1325,17 +1434,26 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient
ProjectId = eventProject.Id,
GroupId = groupId,
GroupName = eventProjectGroup.GroupName,
Persons = groupMembersList.Select(m => new UnifiedPersonInput
Teams = new List<UnifiedTeamInput>
{
UserId = m.Id,
Name = m.Name ?? "",
IdCardNumber = m.IdCardNumber ?? "",
PassportNumber = m.PassportNumber,
AuthType = m.AuthType,
Gender = m.Gender,
Mobile = m.Mobile,
BirthDate = m.BirthDate ?? DateTime.MinValue
}).ToList()
new()
{
TeamId = input.TeamId.Value,
TeamName = selectedTeamMap[input.TeamId.Value].Name,
TeamIndex = 1,
Persons = groupMembersList.Select(member => new UnifiedPersonInput
{
UserId = member.Id,
Name = member.Name ?? "",
IdCardNumber = member.IdCardNumber ?? "",
PassportNumber = member.PassportNumber,
AuthType = member.AuthType,
Gender = member.Gender,
Mobile = member.Mobile,
BirthDate = member.BirthDate ?? DateTime.MinValue
}).ToList()
}
}
});
}
}

View File

@ -23,6 +23,24 @@ public class EventRegistrationItem : Entity<long>, ISoftDelete, ICreatedUserId,
/// </summary>
public long? EventProjectGroupId { get; set; }
/// <summary>
/// 报名时选择的来源团队ID
/// </summary>
[SugarColumn(IsNullable = true)]
public long? TeamId { get; set; }
/// <summary>
/// 报名时的来源团队名称快照
/// </summary>
[SugarColumn(Length = 64, IsNullable = true)]
public string? TeamName { get; set; }
/// <summary>
/// 来源团队在当前项目组别下的队伍序号(1队、2队等)
/// </summary>
[SugarColumn(IsNullable = true)]
public int? TeamIndex { get; set; }
/// <summary>
/// 参赛项目名称
/// </summary>
@ -92,4 +110,4 @@ public class EventRegistrationItem : Entity<long>, ISoftDelete, ICreatedUserId,
/// 更新时间
/// </summary>
public DateTime? UpdatedTime { get; set; }
}
}

View File

@ -635,6 +635,41 @@ BEGIN TRY
IF COL_LENGTH('dbo.EventInfo','RefereeRegistrationCode') IS NULL ALTER TABLE dbo.EventInfo ADD RefereeRegistrationCode NVARCHAR(32) NULL; -- 裁判报名邀请码
/* EventRegistrationLimit 报名限额:裁判名额 RefereeLimit,默认0 */
IF COL_LENGTH('dbo.EventRegistrationLimit','RefereeLimit') IS NULL ALTER TABLE dbo.EventRegistrationLimit ADD RefereeLimit INT NOT NULL CONSTRAINT DF_EventRegistrationLimit_RefereeLimit DEFAULT(0) WITH VALUES; -- 裁判名额限制(默认0)
/* EventRegistrationItem 报名项目明细:支持一个单位在同一报名订单中选择多个来源团队。 */
IF COL_LENGTH(N'dbo.EventRegistrationItem', N'TeamId') IS NULL ALTER TABLE dbo.EventRegistrationItem ADD TeamId BIGINT NULL;
IF COL_LENGTH(N'dbo.EventRegistrationItem', N'TeamName') IS NULL ALTER TABLE dbo.EventRegistrationItem ADD TeamName NVARCHAR(64) NULL;
IF COL_LENGTH(N'dbo.EventRegistrationItem', N'TeamIndex') IS NULL ALTER TABLE dbo.EventRegistrationItem ADD TeamIndex INT NULL;
;WITH TeamCandidates AS (
SELECT item.Id AS RegistrationItemId,team.Id AS TeamId,
COUNT_BIG(1) OVER(PARTITION BY item.Id) AS CandidateCount
FROM dbo.EventRegistrationItem item
INNER JOIN dbo.EventRegistration registration ON registration.Id=item.RegistrationId
INNER JOIN dbo.Team team
ON LTRIM(RTRIM(team.Name))=LTRIM(RTRIM(registration.TeamName))
AND ((registration.MerchantCertificationId IS NOT NULL AND team.MerchantCertificationId=registration.MerchantCertificationId)
OR team.CreatedUserId=registration.CreatedUserId)
WHERE item.TeamId IS NULL AND registration.RegistrationMode=2
AND NULLIF(LTRIM(RTRIM(registration.TeamName)),N'') IS NOT NULL
)
UPDATE item SET TeamId=candidate.TeamId
FROM dbo.EventRegistrationItem item
INNER JOIN TeamCandidates candidate ON candidate.RegistrationItemId=item.Id AND candidate.CandidateCount=1;
UPDATE item SET TeamName=LEFT(COALESCE(NULLIF(LTRIM(RTRIM(registration.TeamName)),N''),team.Name),64)
FROM dbo.EventRegistrationItem item
INNER JOIN dbo.EventRegistration registration ON registration.Id=item.RegistrationId
LEFT JOIN dbo.Team team ON team.Id=item.TeamId
WHERE item.TeamName IS NULL
AND (NULLIF(LTRIM(RTRIM(registration.TeamName)),N'') IS NOT NULL OR team.Id IS NOT NULL);
;WITH OrderedTeamItems AS (
SELECT item.Id,ROW_NUMBER() OVER(PARTITION BY item.RegistrationId,item.ProjectId,
ISNULL(item.EventProjectGroupId,0),ISNULL(item.TeamId,0) ORDER BY item.CreatedTime,item.Id) AS RepairedTeamIndex
FROM dbo.EventRegistrationItem item
LEFT JOIN dbo.EventProject eventProject ON eventProject.Id=item.ProjectId
LEFT JOIN dbo.Project project ON project.Id=eventProject.ProjectId
WHERE item.TeamIndex IS NULL AND (project.ProjectType IN(1005,1006) OR item.ProjectTypeName LIKE N'%团队%')
)
UPDATE item SET TeamIndex=ordered.RepairedTeamIndex
FROM dbo.EventRegistrationItem item INNER JOIN OrderedTeamItems ordered ON ordered.Id=item.Id;
/* Project 项目:绑定计分规则 ScoreRuleId -> ScoreRule.Id */
IF COL_LENGTH('dbo.Project','ScoreRuleId') IS NULL ALTER TABLE dbo.Project ADD ScoreRuleId BIGINT NULL; -- 项目绑定的计分规则Id(ScoreRule.Id)
/* SysUser 扩展为“单位账号”:UnitName 单位名称;UnitPlatform 所属平台 0-系统用户 1-报名平台 2-打分系统,默认0 */
@ -673,11 +708,17 @@ BEGIN TRY
IF OBJECT_ID('CK_ScoreRule_Version','C') IS NULL ALTER TABLE dbo.ScoreRule WITH CHECK ADD CONSTRAINT CK_ScoreRule_Version CHECK(RuleVersion>=1);
IF OBJECT_ID('CK_EventJudgeScoreSheet_Version','C') IS NULL ALTER TABLE dbo.EventJudgeScoreSheet WITH CHECK ADD CONSTRAINT CK_EventJudgeScoreSheet_Version CHECK(RuleVersion>=1 AND SheetVersion>=1);
IF OBJECT_ID('CK_EventScoreCorrection_Applicant','C') IS NULL ALTER TABLE dbo.EventScoreCorrection WITH CHECK ADD CONSTRAINT CK_EventScoreCorrection_Applicant CHECK((ApplyUserId IS NOT NULL AND ApplyRefereeId IS NULL) OR (ApplyUserId IS NULL AND ApplyRefereeId IS NOT NULL));
IF OBJECT_ID(N'dbo.CK_EventRegistrationItem_TeamIndex',N'C') IS NULL ALTER TABLE dbo.EventRegistrationItem WITH CHECK ADD CONSTRAINT CK_EventRegistrationItem_TeamIndex CHECK(TeamIndex IS NULL OR TeamIndex>0);
IF EXISTS(SELECT 1 FROM dbo.EventRegistrationItem item LEFT JOIN dbo.Team team ON team.Id=item.TeamId WHERE item.TeamId IS NOT NULL AND team.Id IS NULL) THROW 51024,N'EventRegistrationItem 存在无效 TeamId,停止升级。',1;
/* 3.2 外键:二期表引用一期表主键、以及二期表之间的引用关系 */
IF OBJECT_ID(N'dbo.[FK_RefereeApplication_Event]','F') IS NULL ALTER TABLE dbo.[RefereeApplication] WITH CHECK ADD CONSTRAINT [FK_RefereeApplication_Event] FOREIGN KEY([EventId]) REFERENCES dbo.[EventInfo]([Id]);
IF OBJECT_ID(N'dbo.[FK_RefereeApplication_AppUser]','F') IS NULL ALTER TABLE dbo.[RefereeApplication] WITH CHECK ADD CONSTRAINT [FK_RefereeApplication_AppUser] FOREIGN KEY([RefereeUserId]) REFERENCES dbo.[AppUser]([Id]);
IF OBJECT_ID(N'dbo.[FK_CheckinTask_Event]','F') IS NULL ALTER TABLE dbo.[CheckinTask] WITH CHECK ADD CONSTRAINT [FK_CheckinTask_Event] FOREIGN KEY([EventId]) REFERENCES dbo.[EventInfo]([Id]);
IF OBJECT_ID(N'dbo.[FK_CheckinRecord_Task]','F') IS NULL ALTER TABLE dbo.[CheckinRecord] WITH CHECK ADD CONSTRAINT [FK_CheckinRecord_Task] FOREIGN KEY([TaskId]) REFERENCES dbo.[CheckinTask]([Id]);
IF OBJECT_ID(N'dbo.[FK_EventRegistrationItem_Team]','F') IS NULL ALTER TABLE dbo.[EventRegistrationItem] WITH CHECK ADD CONSTRAINT [FK_EventRegistrationItem_Team] FOREIGN KEY([TeamId]) REFERENCES dbo.[Team]([Id]);
IF NOT EXISTS(SELECT 1 FROM sys.indexes WHERE object_id=OBJECT_ID(N'dbo.[EventRegistrationItem]') AND name=N'IX_EventRegistrationItem_RegistrationId_TeamId') CREATE NONCLUSTERED INDEX [IX_EventRegistrationItem_RegistrationId_TeamId] ON dbo.[EventRegistrationItem]([RegistrationId],[TeamId]) INCLUDE([EventProjectGroupId],[TeamIndex]) WHERE [TeamId] IS NOT NULL;
IF EXISTS(SELECT 1 FROM dbo.EventRegistrationItem WHERE TeamId IS NOT NULL AND TeamIndex IS NOT NULL AND DeleteMark=0 GROUP BY RegistrationId,ProjectId,EventProjectGroupId,TeamId,TeamIndex HAVING COUNT_BIG(1)>1) THROW 51025,N'EventRegistrationItem 存在重复的来源团队队伍序号,停止创建唯一索引。',1;
IF NOT EXISTS(SELECT 1 FROM sys.indexes WHERE object_id=OBJECT_ID(N'dbo.[EventRegistrationItem]') AND name=N'UX_EventRegistrationItem_SourceTeamSquad_Active') CREATE UNIQUE NONCLUSTERED INDEX [UX_EventRegistrationItem_SourceTeamSquad_Active] ON dbo.[EventRegistrationItem]([RegistrationId],[ProjectId],[EventProjectGroupId],[TeamId],[TeamIndex]) WHERE [TeamId] IS NOT NULL AND [TeamIndex] IS NOT NULL AND [DeleteMark]=0;
IF OBJECT_ID(N'dbo.[FK_Referee_AppUser]','F') IS NULL ALTER TABLE dbo.[Referee] WITH CHECK ADD CONSTRAINT [FK_Referee_AppUser] FOREIGN KEY([AppUserId]) REFERENCES dbo.[AppUser]([Id]);
IF OBJECT_ID(N'dbo.[FK_EventReferee_Event]','F') IS NULL ALTER TABLE dbo.[EventReferee] WITH CHECK ADD CONSTRAINT [FK_EventReferee_Event] FOREIGN KEY([EventId]) REFERENCES dbo.[EventInfo]([Id]);
IF OBJECT_ID(N'dbo.[FK_EventReferee_Referee]','F') IS NULL ALTER TABLE dbo.[EventReferee] WITH CHECK ADD CONSTRAINT [FK_EventReferee_Referee] FOREIGN KEY([RefereeId]) REFERENCES dbo.[Referee]([Id]);
@ -1470,6 +1511,7 @@ SELECT r.RuleCode,COUNT(i.Id) AS ItemCount FROM dbo.ScoreRule r LEFT JOIN dbo.Sc
(N'CertificateTemplate',N'UpdatedUserId'),(N'CertificateTemplate',N'UpdatedTime'),
(N'CertificatePlaceholder',N'CreatedUserId'),(N'CertificatePlaceholder',N'CreatedTime'),(N'CertificatePlaceholder',N'UpdatedUserId'),(N'CertificatePlaceholder',N'UpdatedTime'),
(N'EventInfo',N'RefereeRegistrationCode'),(N'EventRegistrationLimit',N'RefereeLimit'),
(N'EventRegistrationItem',N'TeamId'),(N'EventRegistrationItem',N'TeamName'),(N'EventRegistrationItem',N'TeamIndex'),
(N'Project',N'ScoreRuleId'),(N'SysUser',N'UnitName'),(N'SysUser',N'UnitPlatform'),
(N'EventResult',N'SessionId'),(N'EventResult',N'SessionEntryId'),(N'EventResult',N'TotalScore'),(N'EventResult',N'ResultVersion'),(N'EventResult',N'ReviewUserId'),(N'EventResult',N'ReviewTime'),
(N'EventReferee',N'RefereeApplicationId'),(N'EventVenue',N'VenueCode'),(N'EventJudgeMessage',N'ReceiverUserId'),

View File

@ -58,6 +58,68 @@ BEGIN TRY
IF COL_LENGTH('dbo.EventInfo','RefereeRegistrationCode') IS NULL ALTER TABLE dbo.EventInfo ADD RefereeRegistrationCode NVARCHAR(32) NULL; -- 裁判报名邀请码
/* EventRegistrationLimit 报名限额:裁判名额 RefereeLimit,默认0 */
IF COL_LENGTH('dbo.EventRegistrationLimit','RefereeLimit') IS NULL ALTER TABLE dbo.EventRegistrationLimit ADD RefereeLimit INT NOT NULL CONSTRAINT DF_EventRegistrationLimit_RefereeLimit DEFAULT(0) WITH VALUES; -- 裁判名额限制(默认0)
/* EventRegistrationItem 报名项目明细:支持一个单位在同一报名订单中选择多个来源团队。 */
IF COL_LENGTH(N'dbo.EventRegistrationItem', N'TeamId') IS NULL ALTER TABLE dbo.EventRegistrationItem ADD TeamId BIGINT NULL;
IF COL_LENGTH(N'dbo.EventRegistrationItem', N'TeamName') IS NULL ALTER TABLE dbo.EventRegistrationItem ADD TeamName NVARCHAR(64) NULL;
IF COL_LENGTH(N'dbo.EventRegistrationItem', N'TeamIndex') IS NULL ALTER TABLE dbo.EventRegistrationItem ADD TeamIndex INT NULL;
/* 历史 TeamId 仅在单位/创建账号 + 团队名称唯一匹配时回填,歧义数据保持 NULL。 */
;WITH TeamCandidates AS (
SELECT
item.Id AS RegistrationItemId,
team.Id AS TeamId,
COUNT_BIG(1) OVER (PARTITION BY item.Id) AS CandidateCount
FROM dbo.EventRegistrationItem AS item
INNER JOIN dbo.EventRegistration AS registration ON registration.Id = item.RegistrationId
INNER JOIN dbo.Team AS team
ON LTRIM(RTRIM(team.Name)) = LTRIM(RTRIM(registration.TeamName))
AND (
(registration.MerchantCertificationId IS NOT NULL
AND team.MerchantCertificationId = registration.MerchantCertificationId)
OR team.CreatedUserId = registration.CreatedUserId
)
WHERE item.TeamId IS NULL
AND registration.RegistrationMode = 2
AND NULLIF(LTRIM(RTRIM(registration.TeamName)), N'') IS NOT NULL
)
UPDATE item
SET item.TeamId = candidate.TeamId
FROM dbo.EventRegistrationItem AS item
INNER JOIN TeamCandidates AS candidate
ON candidate.RegistrationItemId = item.Id
AND candidate.CandidateCount = 1;
/* 团队名称为报名快照,优先保留报名主表历史名称。 */
UPDATE item
SET item.TeamName = LEFT(COALESCE(
NULLIF(LTRIM(RTRIM(registration.TeamName)), N''),
team.Name
), 64)
FROM dbo.EventRegistrationItem AS item
INNER JOIN dbo.EventRegistration AS registration ON registration.Id = item.RegistrationId
LEFT JOIN dbo.Team AS team ON team.Id = item.TeamId
WHERE item.TeamName IS NULL
AND (NULLIF(LTRIM(RTRIM(registration.TeamName)), N'') IS NOT NULL OR team.Id IS NOT NULL);
/* 1005=团队速度、1006=团队花样;按原明细创建顺序重建一队、二队序号。 */
;WITH OrderedTeamItems AS (
SELECT
item.Id,
ROW_NUMBER() OVER (
PARTITION BY item.RegistrationId, item.ProjectId,
ISNULL(item.EventProjectGroupId, 0), ISNULL(item.TeamId, 0)
ORDER BY item.CreatedTime, item.Id
) AS RepairedTeamIndex
FROM dbo.EventRegistrationItem AS item
LEFT JOIN dbo.EventProject AS eventProject ON eventProject.Id = item.ProjectId
LEFT JOIN dbo.Project AS project ON project.Id = eventProject.ProjectId
WHERE item.TeamIndex IS NULL
AND (project.ProjectType IN (1005, 1006) OR item.ProjectTypeName LIKE N'%团队%')
)
UPDATE item
SET item.TeamIndex = ordered.RepairedTeamIndex
FROM dbo.EventRegistrationItem AS item
INNER JOIN OrderedTeamItems AS ordered ON ordered.Id = item.Id;
/* Project 项目:绑定计分规则 ScoreRuleId -> ScoreRule.Id */
IF COL_LENGTH('dbo.Project','ScoreRuleId') IS NULL ALTER TABLE dbo.Project ADD ScoreRuleId BIGINT NULL; -- 项目绑定的计分规则Id(ScoreRule.Id)
/* SysUser 扩展为“单位账号”:UnitName 单位名称;UnitPlatform 所属平台 0-系统用户 1-报名平台 2-打分系统,默认0 */
@ -93,11 +155,17 @@ BEGIN TRY
IF OBJECT_ID('CK_ScoreRule_Version','C') IS NULL ALTER TABLE dbo.ScoreRule WITH CHECK ADD CONSTRAINT CK_ScoreRule_Version CHECK(RuleVersion>=1);
IF OBJECT_ID('CK_EventJudgeScoreSheet_Version','C') IS NULL ALTER TABLE dbo.EventJudgeScoreSheet WITH CHECK ADD CONSTRAINT CK_EventJudgeScoreSheet_Version CHECK(RuleVersion>=1 AND SheetVersion>=1);
IF OBJECT_ID('CK_EventScoreCorrection_Applicant','C') IS NULL ALTER TABLE dbo.EventScoreCorrection WITH CHECK ADD CONSTRAINT CK_EventScoreCorrection_Applicant CHECK((ApplyUserId IS NOT NULL AND ApplyRefereeId IS NULL) OR (ApplyUserId IS NULL AND ApplyRefereeId IS NOT NULL));
IF OBJECT_ID(N'dbo.CK_EventRegistrationItem_TeamIndex', N'C') IS NULL ALTER TABLE dbo.EventRegistrationItem WITH CHECK ADD CONSTRAINT CK_EventRegistrationItem_TeamIndex CHECK(TeamIndex IS NULL OR TeamIndex>0);
IF EXISTS(SELECT 1 FROM dbo.EventRegistrationItem item LEFT JOIN dbo.Team team ON team.Id=item.TeamId WHERE item.TeamId IS NOT NULL AND team.Id IS NULL) THROW 51024,N'EventRegistrationItem 存在无效 TeamId,停止升级。',1;
/* 外键:二期表引用一期表及二期表。 */
IF OBJECT_ID(N'dbo.[FK_RefereeApplication_Event]','F') IS NULL ALTER TABLE dbo.[RefereeApplication] WITH CHECK ADD CONSTRAINT [FK_RefereeApplication_Event] FOREIGN KEY([EventId]) REFERENCES dbo.[EventInfo]([Id]);
IF OBJECT_ID(N'dbo.[FK_RefereeApplication_AppUser]','F') IS NULL ALTER TABLE dbo.[RefereeApplication] WITH CHECK ADD CONSTRAINT [FK_RefereeApplication_AppUser] FOREIGN KEY([RefereeUserId]) REFERENCES dbo.[AppUser]([Id]);
IF OBJECT_ID(N'dbo.[FK_CheckinTask_Event]','F') IS NULL ALTER TABLE dbo.[CheckinTask] WITH CHECK ADD CONSTRAINT [FK_CheckinTask_Event] FOREIGN KEY([EventId]) REFERENCES dbo.[EventInfo]([Id]);
IF OBJECT_ID(N'dbo.[FK_CheckinRecord_Task]','F') IS NULL ALTER TABLE dbo.[CheckinRecord] WITH CHECK ADD CONSTRAINT [FK_CheckinRecord_Task] FOREIGN KEY([TaskId]) REFERENCES dbo.[CheckinTask]([Id]);
IF OBJECT_ID(N'dbo.[FK_EventRegistrationItem_Team]','F') IS NULL ALTER TABLE dbo.[EventRegistrationItem] WITH CHECK ADD CONSTRAINT [FK_EventRegistrationItem_Team] FOREIGN KEY([TeamId]) REFERENCES dbo.[Team]([Id]);
IF NOT EXISTS(SELECT 1 FROM sys.indexes WHERE object_id=OBJECT_ID(N'dbo.[EventRegistrationItem]') AND name=N'IX_EventRegistrationItem_RegistrationId_TeamId') CREATE NONCLUSTERED INDEX [IX_EventRegistrationItem_RegistrationId_TeamId] ON dbo.[EventRegistrationItem]([RegistrationId],[TeamId]) INCLUDE([EventProjectGroupId],[TeamIndex]) WHERE [TeamId] IS NOT NULL;
IF EXISTS(SELECT 1 FROM dbo.EventRegistrationItem WHERE TeamId IS NOT NULL AND TeamIndex IS NOT NULL AND DeleteMark=0 GROUP BY RegistrationId,ProjectId,EventProjectGroupId,TeamId,TeamIndex HAVING COUNT_BIG(1)>1) THROW 51025,N'EventRegistrationItem 存在重复的来源团队队伍序号,停止创建唯一索引。',1;
IF NOT EXISTS(SELECT 1 FROM sys.indexes WHERE object_id=OBJECT_ID(N'dbo.[EventRegistrationItem]') AND name=N'UX_EventRegistrationItem_SourceTeamSquad_Active') CREATE UNIQUE NONCLUSTERED INDEX [UX_EventRegistrationItem_SourceTeamSquad_Active] ON dbo.[EventRegistrationItem]([RegistrationId],[ProjectId],[EventProjectGroupId],[TeamId],[TeamIndex]) WHERE [TeamId] IS NOT NULL AND [TeamIndex] IS NOT NULL AND [DeleteMark]=0;
IF OBJECT_ID(N'dbo.[FK_Referee_AppUser]','F') IS NULL ALTER TABLE dbo.[Referee] WITH CHECK ADD CONSTRAINT [FK_Referee_AppUser] FOREIGN KEY([AppUserId]) REFERENCES dbo.[AppUser]([Id]);
IF OBJECT_ID(N'dbo.[FK_EventReferee_Event]','F') IS NULL ALTER TABLE dbo.[EventReferee] WITH CHECK ADD CONSTRAINT [FK_EventReferee_Event] FOREIGN KEY([EventId]) REFERENCES dbo.[EventInfo]([Id]);
IF OBJECT_ID(N'dbo.[FK_EventReferee_Referee]','F') IS NULL ALTER TABLE dbo.[EventReferee] WITH CHECK ADD CONSTRAINT [FK_EventReferee_Referee] FOREIGN KEY([RefereeId]) REFERENCES dbo.[Referee]([Id]);
@ -268,6 +336,32 @@ SELECT r.RuleCode,COUNT(i.Id) AS ItemCount FROM dbo.ScoreRule r LEFT JOIN dbo.Sc
COMMIT TRANSACTION;
PRINT N'二期 ALTER 脚本执行成功。';
/* 多团队报名升级结果:未匹配记录允许保留为空,但上线前必须人工核对。 */
SELECT
item.Id AS RegistrationItemId,
item.RegistrationId,
registration.TeamName AS HistoricalTeamName,
registration.MerchantCertificationId,
registration.CreatedUserId,
item.ProjectName,
item.GroupName,
item.TeamIndex
FROM dbo.EventRegistrationItem item
INNER JOIN dbo.EventRegistration registration ON registration.Id=item.RegistrationId
WHERE item.TeamId IS NULL AND registration.RegistrationMode=2 AND item.DeleteMark=0
ORDER BY item.RegistrationId,item.ProjectId,item.Id;
SELECT
(SELECT COUNT_BIG(1) FROM dbo.EventRegistrationItem item LEFT JOIN dbo.Team team ON team.Id=item.TeamId WHERE item.TeamId IS NOT NULL AND team.Id IS NULL) AS InvalidTeamIdCount,
(SELECT COUNT_BIG(1) FROM dbo.EventRegistrationItem WHERE TeamIndex IS NOT NULL AND TeamIndex<=0) AS InvalidTeamIndexCount,
(SELECT COUNT_BIG(1) FROM (
SELECT RegistrationId,ProjectId,EventProjectGroupId,TeamId,TeamIndex
FROM dbo.EventRegistrationItem
WHERE TeamId IS NOT NULL AND TeamIndex IS NOT NULL AND DeleteMark=0
GROUP BY RegistrationId,ProjectId,EventProjectGroupId,TeamId,TeamIndex
HAVING COUNT_BIG(1)>1
) duplicateItems) AS DuplicateSquadCount;
END TRY
BEGIN CATCH
DECLARE @ErrorNumber INT=ERROR_NUMBER(),@ErrorLine INT=ERROR_LINE(),@ErrorMessage NVARCHAR(4000)=ERROR_MESSAGE();

View File

@ -11,7 +11,7 @@
生产 SQL 已拆成三份,必须按以下顺序执行:
1. `二期需求/上线/dbo_add.sql`:创建二期新增表。
2. `二期需求/上线/dbo_alter.sql`:修改一期表,补充约束、外键、索引、视图、存储过程和数据库角色。
2. `二期需求/上线/dbo_alter.sql`:修改一期表,补充约束、外键、索引、视图、存储过程和数据库角色;其中已经包含单位多团队报名的字段升级及历史数据修复。
3. `二期需求/上线/dbo_insert.sql`:写入或更新计分规则、评分项、角色和菜单,并执行最终校验。
三个脚本各自使用事务。任意一个失败都应停止上线,处理完成后从失败的脚本重新执行,不要跳过执行顺序。
@ -20,6 +20,14 @@
原完整脚本 `二期需求/dbo.sql` 继续保留,用于留档和整体核对;实际分步上线使用“上线”目录中的三份脚本。
多团队报名相关脚本说明:
- `20261012_event_registration_multi_team_test_database.sql`:先在测试库执行,增加报名明细的 `TeamId/TeamName/TeamIndex`,自动修复能够唯一匹配的历史报名,并输出未匹配记录和候选团队。
- `20261012_event_registration_multi_team_unmatched_repair_template.sql`:对测试脚本仍未匹配的历史报名,人工确认 `RegistrationId -> TeamId` 后定向修复;脚本会校验团队归属并拒绝覆盖已有不同团队。
- `20261012_event_registration_multi_team.sql`:仅在需要单独发布该功能或单独演练时使用;正式二期完整上线时不要额外重复安排,它的内容已经合并进 `dbo_alter.sql`。
- 历史 `TeamId` 只按“报名时团队名称 + 单位认证/创建账号归属”唯一匹配;候选不唯一时保持为空,由人工依据脚本结果集确认,禁止按名称随意选一条。
- 历史团队项目的 `TeamIndex` 按原报名明细创建顺序恢复;个人项目保持为空。
执行前需要确认最新计分项已经同步进 `dbo_insert.sql`。当前 `scoreruleitem.txt` 后续调整过规定元素配置,不能漏到生产库。
脚本由 DBA 审核后在正式库执行。最后确认 `dbo_insert.sql` 输出的表、字段、种子和角色校验全部通过。
@ -49,6 +57,9 @@ dotnet publish YD_Event.Web.Entry\YD_Event.Web.Entry.csproj -c Release -o <发
## 5. 上线后验证
- 后台服务启动正常,日志无数据库或配置错误
- 测试库脚本最后结果中的 `InvalidTeamIdCount`、`InvalidTeamIndexCount`、`DuplicateSquadCount` 均为 0;未匹配历史团队记录已逐条确认
- 单位账号可同时选择两支团队,同项目分别分配运动员并生成一笔订单
- 多团队订单只允许整单取消,排期及报名详情显示各自来源团队名称
- 裁判报名、赛事编排和检录接口正常
- 裁判端可以获取任务并提交评分
- 成绩计算、成绩修正和投屏接口正常