diff --git a/YD_Event.Application/Event/Dtos/EventRecordDtos.cs b/YD_Event.Application/Event/Dtos/EventRecordDtos.cs index 84b140d..ee4a915 100644 --- a/YD_Event.Application/Event/Dtos/EventRecordDtos.cs +++ b/YD_Event.Application/Event/Dtos/EventRecordDtos.cs @@ -452,6 +452,11 @@ public class CancelPreviewOutput /// 是否需要填写收款人信息(线下支付且已支付时需要) /// public bool NeedPayeeInfo { get; set; } + + /// + /// 是否允许部分取消(多团队合并报名仅允许整单取消) + /// + public bool AllowPartialCancel { get; set; } = true; } /// @@ -827,6 +832,21 @@ public class CancelExecuteOutput /// public class RegistrationItemWithMembersDto { + /// + /// 来源团队ID + /// + public long? TeamId { get; set; } + + /// + /// 来源团队名称 + /// + public string? TeamName { get; set; } + + /// + /// 队伍序号 + /// + public int? TeamIndex { get; set; } + /// /// 报名项目ID /// diff --git a/YD_Event.Application/Event/Dtos/UserCompetitionDtos.cs b/YD_Event.Application/Event/Dtos/UserCompetitionDtos.cs index c36303d..1d17b77 100644 --- a/YD_Event.Application/Event/Dtos/UserCompetitionDtos.cs +++ b/YD_Event.Application/Event/Dtos/UserCompetitionDtos.cs @@ -352,6 +352,21 @@ public class UserEventProjectGroupOutput /// public class UserEventProjectListOutput { + /// + /// 来源团队ID(报名预览时返回) + /// + public long? TeamId { get; set; } + + /// + /// 来源团队名称(报名预览时返回) + /// + public string? TeamName { get; set; } + + /// + /// 队伍序号(报名预览时返回) + /// + public int? TeamIndex { get; set; } + /// /// 项目ID /// @@ -743,10 +758,9 @@ public class UserSubmitRegistrationInput public long? CouponId { get; set; } /// - /// 团队ID (必填) + /// 团队ID(旧版单团队结构,保留兼容) /// - [Required] - public long TeamId { get; set; } + public long? TeamId { get; set; } } #region 参赛记录相关 DTOs @@ -1389,6 +1403,11 @@ public class EventLimitDetail /// public class GroupTeamAssignment { + /// + /// 来源团队ID + /// + public long? TeamId { get; set; } + /// /// 团队序号(例如:1, 2, 3) /// diff --git a/YD_Event.Application/Event/EventArrangementService.cs b/YD_Event.Application/Event/EventArrangementService.cs index 08d6954..7895dc5 100644 --- a/YD_Event.Application/Event/EventArrangementService.cs +++ b/YD_Event.Application/Event/EventArrangementService.cs @@ -137,18 +137,32 @@ namespace YD_Event.Application.Event; // 获取所有相关的人员和报名记录(用于统计团队数) var personWithTeam = await _personRepository.AsQueryable() .InnerJoin((p, r) => p.RegistrationId == r.Id) - .Where((p, r) => r.EventId == eventId + .InnerJoin((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((p, r) => p.RegistrationId == r.Id) - .Where((p, r) => r.EventId == eventId + .InnerJoin((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((p, r) => p.RegistrationId == r.Id) - .Where((p, r) => p.ProjectId == eventProj.Id + .InnerJoin((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, diff --git a/YD_Event.Application/Event/EventRegistrationService.cs b/YD_Event.Application/Event/EventRegistrationService.cs index 5564870..1e651ca 100644 --- a/YD_Event.Application/Event/EventRegistrationService.cs +++ b/YD_Event.Application/Event/EventRegistrationService.cs @@ -1019,18 +1019,19 @@ public class EventRegistrationService : BaseService, ITransie // 统计符合条件的所有运动员(与赛事编排页面保持一致的筛选规则) var athletes = await _personRepository.AsQueryable() .InnerJoin((p, r) => p.RegistrationId == r.Id) - .Where((p, r) => r.EventId == eventId + .InnerJoin((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, diff --git a/YD_Event.Application/Event/EventScoreService.cs b/YD_Event.Application/Event/EventScoreService.cs index 11e34a8..abb37b7 100644 --- a/YD_Event.Application/Event/EventScoreService.cs +++ b/YD_Event.Application/Event/EventScoreService.cs @@ -133,7 +133,8 @@ public class EventScoreService : BaseService, ITransient .LeftJoin((p, r) => p.RegistrationId == r.Id) .LeftJoin((p, r, res) => p.Id == res.RegistrationPersonId) .LeftJoin((p, r, res, c) => res.Id == c.ResultId) - .Where((p, r, res, c) => r.EventId == input.EventId + .LeftJoin((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, 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, 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, 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, ITransient // 步骤2:批量预加载该赛事的所有人员数据(包含团队信息) var allPersonsWithTeam = await _personRepository.AsQueryable() .InnerJoin((p, r) => p.RegistrationId == r.Id) - .Where((p, r) => r.EventId == eventId + .InnerJoin((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, ITransient p.GroupName, p.RegistrationId, p.ProjectId, - r.TeamName + TeamName = item.TeamName ?? r.TeamName }) .ToListAsync(); @@ -833,8 +836,9 @@ public class EventScoreService : BaseService, ITransient // 步骤3:批量预加载该赛事的所有人员数据(包含团队信息) var allPersonsWithTeam = await _personRepository.AsQueryable() .InnerJoin((p, r) => p.RegistrationId == r.Id) - .Where((p, r) => r.EventId == input.EventId && !p.DeleteMark) - .Select((p, r) => new { + .InnerJoin((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, ITransient p.RegistrationItemId, p.EventProjectGroupId, p.DeleteMark, - r.TeamName, + TeamName = item.TeamName ?? r.TeamName, r.MerchantCertificationId, Person = p }) diff --git a/YD_Event.Application/Event/UnifiedRegistrationService.cs b/YD_Event.Application/Event/UnifiedRegistrationService.cs index 306f28c..f37cae7 100644 --- a/YD_Event.Application/Event/UnifiedRegistrationService.cs +++ b/YD_Event.Application/Event/UnifiedRegistrationService.cs @@ -654,6 +654,50 @@ public class UnifiedRegistrationService : ITransient return persons; } + /// + /// 根据已保存的报名明细重建统一报名结构,用于支付确认前再次校验报名限制。 + /// 同一项目组别的多个报名明细会还原为多个参赛队伍,避免少算队伍数量。 + /// + private static List BuildUnifiedItemsFromStoredDetails( + IEnumerable items, + IEnumerable 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(); + } + /// /// 获取人员证件号码(根据认证类型返回身份证号或护照号) /// @@ -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; } + /// + /// 判断报名单是否包含多个来源团队。 + /// 来源团队与同一项目下的一队、二队不是同一概念,以报名项的 TeamId 为准。 + /// + private static bool IsMultiSourceTeamRegistration(IEnumerable items) + { + return items + .Where(x => !x.DeleteMark && x.TeamId.HasValue) + .Select(x => x.TeamId!.Value) + .Distinct() + .Take(2) + .Count() > 1; + } + public async Task 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().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) { diff --git a/YD_Event.Application/Event/UserEventRecordService.cs b/YD_Event.Application/Event/UserEventRecordService.cs index 8c27dde..4b66e03 100644 --- a/YD_Event.Application/Event/UserEventRecordService.cs +++ b/YD_Event.Application/Event/UserEventRecordService.cs @@ -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)); diff --git a/YD_Event.Application/Event/UserEventRegistrationService.cs b/YD_Event.Application/Event/UserEventRegistrationService.cs index 254eb88..7405e92 100644 --- a/YD_Event.Application/Event/UserEventRegistrationService.cs +++ b/YD_Event.Application/Event/UserEventRegistrationService.cs @@ -507,6 +507,121 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient return idCard.Substring(0, 6) + "********" + idCard.Substring(idCard.Length - 4); } + private async Task> GetAccessibleTeamsAsync( + long userId, + IEnumerable teamIds) + { + var distinctTeamIds = teamIds.Where(id => id > 0).Distinct().ToList(); + if (!distinctTeamIds.Any()) + { + throw Oops.Bah("请至少选择一个团队"); + } + + var teams = await _db.Queryable() + .Where(team => distinctTeamIds.Contains(team.Id) && !team.DeleteMark) + .ToListAsync(); + if (teams.Count != distinctTeamIds.Count) + { + throw Oops.Bah("部分团队不存在或已被删除,请刷新后重试"); + } + + var certification = await _db.Queryable() + .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 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() + .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 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 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("同一团队的队伍序号不能重复"); + } + } + /// /// 3.2 分配队员接口 - 提交报名(含保存草稿) /// @@ -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().FirstAsync(t => t.Id == input.TeamId && !t.DeleteMark); - if (team == null) throw Oops.Bah("团队不存在"); - - // 收集所有成员ID - var allMemberIds = new List(); - if (input.GroupMembers != null) + var groupMembers = input.GroupMembers ?? new List(); + var submittedAssignments = new List<(long TeamId, List 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()); + throw Oops.Bah("报名队伍信息不完整,请重新选择团队"); } + + submittedAssignments.Add((teamId.Value, teamAssignment.MemberIds ?? new List())); } - else + } + else + { + if (!input.TeamId.HasValue || input.TeamId.Value <= 0) { - // 向后兼容单团队模式 - allMemberIds.AddRange(gm.MemberIds ?? new List()); + throw Oops.Bah("报名队伍信息不完整,请重新选择团队"); } + + submittedAssignments.Add((input.TeamId.Value, groupMember.MemberIds ?? new List())); } } - 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())); } - - allMemberIds = allMemberIds.Distinct().ToList(); - // 验证:一次报名只能选择单个团队内的运动员 - if (allMemberIds.Count > 0) - { - var teamMembers = await _db.Queryable() - .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(); - 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(), @@ -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() }; - // 检查是多团队模式还是单团队模式 - 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 { - 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() + } + }; + + 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()).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(); + var submittedAssignments = new List<(long TeamId, List MemberIds)>(); + foreach (var groupMember in groupMembers) { - var team = await _db.Queryable().FirstAsync(t => t.Id == input.TeamId.Value && !t.DeleteMark); - if (team != null) + if (groupMember.Teams?.Any() == true) { - teamName = team.Name; - } - } - - // 获取所有成员信息 - var allMemberIds = new List(); - 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()); + throw Oops.Bah("报名队伍信息不完整,请重新选择团队"); } - } - else - { - // 向后兼容单团队模式 - allMemberIds.AddRange(gm.MemberIds ?? new List()); + + submittedAssignments.Add((teamId.Value, teamAssignment.MemberIds ?? new List())); } } + else if (input.TeamId.HasValue && input.TeamId.Value > 0) + { + submittedAssignments.Add((input.TeamId.Value, groupMember.MemberIds ?? new List())); + } } - 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())); } - - 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(); - 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(); + 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()) + 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() }; - // 检查是多团队模式还是单团队模式 - 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 { - 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() + } + }; + + 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()).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 { - 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() + } + } }); } } diff --git a/YD_Event.Core/Entities/EventRegistrationItem.cs b/YD_Event.Core/Entities/EventRegistrationItem.cs index 7e0bd64..16b7e37 100644 --- a/YD_Event.Core/Entities/EventRegistrationItem.cs +++ b/YD_Event.Core/Entities/EventRegistrationItem.cs @@ -23,6 +23,24 @@ public class EventRegistrationItem : Entity, ISoftDelete, ICreatedUserId, /// public long? EventProjectGroupId { get; set; } + /// + /// 报名时选择的来源团队ID + /// + [SugarColumn(IsNullable = true)] + public long? TeamId { get; set; } + + /// + /// 报名时的来源团队名称快照 + /// + [SugarColumn(Length = 64, IsNullable = true)] + public string? TeamName { get; set; } + + /// + /// 来源团队在当前项目组别下的队伍序号(1队、2队等) + /// + [SugarColumn(IsNullable = true)] + public int? TeamIndex { get; set; } + /// /// 参赛项目名称 /// @@ -92,4 +110,4 @@ public class EventRegistrationItem : Entity, ISoftDelete, ICreatedUserId, /// 更新时间 /// public DateTime? UpdatedTime { get; set; } -} \ No newline at end of file +} diff --git a/二期需求/dbo.sql b/二期需求/dbo.sql index 8effb2d..2df1aab 100644 --- a/二期需求/dbo.sql +++ b/二期需求/dbo.sql @@ -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'), diff --git a/二期需求/上线/dbo_alter.sql b/二期需求/上线/dbo_alter.sql index b0f26eb..949be01 100644 --- a/二期需求/上线/dbo_alter.sql +++ b/二期需求/上线/dbo_alter.sql @@ -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(); diff --git a/二期需求/上线/二期上线准备与执行清单.md b/二期需求/上线/二期上线准备与执行清单.md index 3ff8bd7..1417ae0 100644 --- a/二期需求/上线/二期上线准备与执行清单.md +++ b/二期需求/上线/二期上线准备与执行清单.md @@ -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;未匹配历史团队记录已逐条确认 +- 单位账号可同时选择两支团队,同项目分别分配运动员并生成一笔订单 +- 多团队订单只允许整单取消,排期及报名详情显示各自来源团队名称 - 裁判报名、赛事编排和检录接口正常 - 裁判端可以获取任务并提交评分 - 成绩计算、成绩修正和投屏接口正常