diff --git a/YD_Event.Application/Client/CheckinController.cs b/YD_Event.Application/Client/CheckinController.cs
index aae8068..1d9d579 100644
--- a/YD_Event.Application/Client/CheckinController.cs
+++ b/YD_Event.Application/Client/CheckinController.cs
@@ -408,8 +408,7 @@ public partial class CheckinController : IDynamicApiController
///
/// 对报名了所选项目的人员发送检录通知;单人失败不影响整体流程。
- /// 个人项目:通知运动员本人(从 EventRegistrationPerson 取)。
- /// 团队项目:通知教练和领队(从 TeamMember 取,因为报名表中不存储教练/领队的 RegistrationItemId)。
+ /// 个人报名:通知运动员本人;单位报名:通知教练和领队,不受项目类型影响。
/// 短信内容:您报名的{赛事名称}正在检录,当前检录至{项目名称}第{x}场次,您所在第{y}场次,请提前前往检录区做好准备。
///
private async Task<(int Total, int Success, int Failed)> NotifyUnitsAsync(
@@ -423,24 +422,24 @@ public partial class CheckinController : IDynamicApiController
if (!isIndividualProject && !isTeamProject)
throw Oops.Bah("当前项目类型不支持检录短信通知");
- // 收集通知接收人:{ Phone, SessionOrder }
- List<(string? Phone, int SessionOrder)> recipients;
+ // 已知报名方式优先按个人/单位报名区分;缺少报名方式时按项目类型兜底。
+ var individualUnits = units.Where(x => x.RegistrationMode == 1 ||
+ (x.RegistrationMode == null && isIndividualProject)).ToList();
+ var unitUnits = units.Where(x => x.RegistrationMode == 2 ||
+ (x.RegistrationMode == null && isTeamProject)).ToList();
+ var recipients = new List<(string? Phone, List SessionOrders)>();
+ if (individualUnits.Count > 0)
+ recipients.AddRange(await CollectIndividualRecipientsAsync(individualUnits));
+ if (unitUnits.Count > 0)
+ recipients.AddRange(await CollectTeamCoachLeaderRecipientsAsync(unitUnits));
- if (isIndividualProject)
- {
- recipients = await CollectIndividualRecipientsAsync(units);
- }
- else
- {
- recipients = await CollectTeamCoachLeaderRecipientsAsync(units);
- }
-
- // 按手机号去重(同一教练带多个队取最早场次)
+ // 同一手机号在本项目的多个报名单元/队伍只收到一条,场次号合并展示。
var missingPhoneCount = recipients.Count(x => string.IsNullOrWhiteSpace(x.Phone));
var validRecipients = recipients
.Where(x => !string.IsNullOrWhiteSpace(x.Phone))
.GroupBy(x => x.Phone!.Trim())
- .Select(g => g.OrderBy(x => x.SessionOrder).First())
+ .Select(g => (Phone: g.Key, SessionOrders: g.SelectMany(x => x.SessionOrders)
+ .Where(order => order > 0).Distinct().OrderBy(order => order).ToList()))
.ToList();
// 当前检录进度:获取该项目在秩序册中的起始场次
@@ -455,7 +454,8 @@ public partial class CheckinController : IDynamicApiController
try
{
var sent = await _smsService.SendCheckinNotice(
- recipient.Phone!.Trim(), eventName, currentSession, recipient.SessionOrder);
+ recipient.Phone, eventName, currentSession,
+ recipient.SessionOrders.Count == 0 ? "1" : string.Join("、", recipient.SessionOrders));
if (sent) success++;
else failed++;
}
@@ -470,22 +470,27 @@ public partial class CheckinController : IDynamicApiController
return (validRecipients.Count + missingPhoneCount, success, failed);
}
- /// 个人项目:取 RegistrationItemId 下的运动员及其场次号。
- private async Task> CollectIndividualRecipientsAsync(
+ /// 个人报名:取 RegistrationItemId 下的运动员及其场次号。
+ private async Task SessionOrders)>> CollectIndividualRecipientsAsync(
List units)
{
// 构建 personId/userId → 场次号 的映射(优先使用秩序册编排的 SessionIndex,缺失时回退到序号)
- var userIdToOrder = new Dictionary();
- var personIdToOrder = new Dictionary();
+ var userIdToOrders = new Dictionary>();
+ var personIdToOrders = new Dictionary>();
for (var i = 0; i < units.Count; i++)
{
var sessionOrder = units[i].SessionIndex > 0 ? units[i].SessionIndex : (i + 1);
foreach (var athlete in units[i].Athletes)
{
- if (athlete.UserId > 0 && !userIdToOrder.ContainsKey(athlete.UserId))
- userIdToOrder[athlete.UserId] = sessionOrder;
- if (!personIdToOrder.ContainsKey(athlete.Id))
- personIdToOrder[athlete.Id] = sessionOrder;
+ if (athlete.UserId > 0)
+ {
+ if (!userIdToOrders.TryGetValue(athlete.UserId, out var userOrders))
+ userIdToOrders[athlete.UserId] = userOrders = new HashSet();
+ userOrders.Add(sessionOrder);
+ }
+ if (!personIdToOrders.TryGetValue(athlete.Id, out var personOrders))
+ personIdToOrders[athlete.Id] = personOrders = new HashSet();
+ personOrders.Add(sessionOrder);
}
}
@@ -510,54 +515,57 @@ public partial class CheckinController : IDynamicApiController
userMap.TryGetValue(p.UserId, out var user);
var phone = !string.IsNullOrWhiteSpace(p.Phone) ? p.Phone : user?.Mobile;
- // 取该人的场次号
- var order = 0;
- if (p.UserId > 0) userIdToOrder.TryGetValue(p.UserId, out order);
- if (order == 0) personIdToOrder.TryGetValue(p.Id, out order);
- if (order == 0) order = 1; // 兜底
+ var orders = p.UserId > 0 && userIdToOrders.TryGetValue(p.UserId, out var userOrders)
+ ? userOrders
+ : personIdToOrders.GetValueOrDefault(p.Id);
+ var sessionOrders = orders?.OrderBy(order => order).ToList() ?? new List { 1 };
- return (phone, order);
+ return (phone, sessionOrders);
}).ToList();
}
///
- /// 团队项目:运动员 → TeamMember → 同团队教练/领队 → AppUser。
- /// 每位教练/领队的场次号取其所带队伍中最早的场次。
+ /// 单位报名:运动员 → TeamMember → 同团队教练/领队 → AppUser,汇总其队伍涉及的场次。
///
- private async Task> CollectTeamCoachLeaderRecipientsAsync(
+ private async Task SessionOrders)>> CollectTeamCoachLeaderRecipientsAsync(
List units)
{
- // 构建 athleteUserId → 最早场次号(优先使用 SessionIndex)
- var athleteToOrder = new Dictionary();
+ // 构建 athleteUserId → 场次号集合(优先使用 SessionIndex)
+ var athleteToOrders = new Dictionary>();
for (var i = 0; i < units.Count; i++)
{
var sessionOrder = units[i].SessionIndex > 0 ? units[i].SessionIndex : (i + 1);
foreach (var a in units[i].Athletes.Where(a => a.UserId > 0))
{
- if (!athleteToOrder.ContainsKey(a.UserId))
- athleteToOrder[a.UserId] = sessionOrder;
+ if (!athleteToOrders.TryGetValue(a.UserId, out var orders))
+ athleteToOrders[a.UserId] = orders = new HashSet();
+ orders.Add(sessionOrder);
}
}
- var athleteUserIds = athleteToOrder.Keys.ToList();
- if (athleteUserIds.Count == 0) return new();
+ var athleteUserIds = athleteToOrders.Keys.ToList();
+ if (athleteUserIds.Count == 0)
+ return await FallbackCollectCoachLeaderAsync(units);
// 步骤1:运动员 → TeamMember(运动员身份)
var athleteMembers = await _teamMemberRepository.AsQueryable()
- .Where(x => athleteUserIds.Contains(x.UserId) && !x.DeleteMark)
+ .Where(x => athleteUserIds.Contains(x.UserId)
+ && x.PersonType == PersonType.Athlete && !x.DeleteMark)
.ToListAsync();
var teamIds = athleteMembers.Select(x => x.TeamId).Distinct().ToList();
- if (teamIds.Count == 0) return new();
+ if (teamIds.Count == 0)
+ return await FallbackCollectCoachLeaderAsync(units);
- // 构建 teamId → 最早场次号(取该队运动员中最早的场次)
- var teamToOrder = new Dictionary();
+ // 构建 teamId → 场次号集合
+ var teamToOrders = new Dictionary>();
foreach (var am in athleteMembers)
{
- if (athleteToOrder.TryGetValue(am.UserId, out var order))
+ if (athleteToOrders.TryGetValue(am.UserId, out var orders))
{
- if (!teamToOrder.ContainsKey(am.TeamId) || teamToOrder[am.TeamId] > order)
- teamToOrder[am.TeamId] = order;
+ if (!teamToOrders.TryGetValue(am.TeamId, out var teamOrders))
+ teamToOrders[am.TeamId] = teamOrders = new HashSet();
+ teamOrders.UnionWith(orders);
}
}
@@ -574,38 +582,41 @@ public partial class CheckinController : IDynamicApiController
return await FallbackCollectCoachLeaderAsync(units);
}
- // 教练/领队 UserId → 最早场次号
- var coachOrderMap = new Dictionary();
+ // 教练/领队 UserId → 场次号集合
+ var coachOrderMap = new Dictionary>();
foreach (var cl in coachLeaders)
{
- if (teamToOrder.TryGetValue(cl.TeamId, out var order))
+ if (teamToOrders.TryGetValue(cl.TeamId, out var orders))
{
- if (!coachOrderMap.ContainsKey(cl.UserId) || coachOrderMap[cl.UserId] > order)
- coachOrderMap[cl.UserId] = order;
+ if (!coachOrderMap.TryGetValue(cl.UserId, out var coachOrders))
+ coachOrderMap[cl.UserId] = coachOrders = new HashSet();
+ coachOrders.UnionWith(orders);
}
}
+ if (coachOrderMap.Count == 0)
+ return await FallbackCollectCoachLeaderAsync(units);
+
var userMap = await LoadUserMapAsync(coachOrderMap.Keys.ToList());
return coachOrderMap.Select(kv =>
{
userMap.TryGetValue(kv.Key, out var user);
- return (user?.Mobile, kv.Value);
+ return (user?.Mobile, kv.Value.OrderBy(order => order).ToList());
}).ToList();
}
- /// 降级路径:从 EventRegistrationPerson 查教练/领队(默认场次号 1)。
- private async Task> FallbackCollectCoachLeaderAsync(
+ /// 降级路径:从 EventRegistrationPerson 查同报名下教练/领队。
+ private async Task SessionOrders)>> FallbackCollectCoachLeaderAsync(
List units)
{
var registrationItemIds = units.Select(x => x.RegistrationItemId).Distinct().ToList();
if (registrationItemIds.Count == 0) return new();
- var registrationIds = await _personRepository.AsQueryable()
+ var athletePersons = await _personRepository.AsQueryable()
.Where(x => registrationItemIds.Contains(x.RegistrationItemId) && !x.DeleteMark)
- .Select(x => x.RegistrationId)
- .Distinct()
.ToListAsync();
+ var registrationIds = athletePersons.Select(x => x.RegistrationId).Distinct().ToList();
if (registrationIds.Count == 0) return new();
@@ -615,18 +626,25 @@ public partial class CheckinController : IDynamicApiController
&& !x.DeleteMark)
.ToListAsync();
- var coachUserIds = fallbackPersons.Where(p => p.UserId > 0).Select(p => p.UserId).Distinct().ToList();
+ var itemOrders = units.GroupBy(x => x.RegistrationItemId)
+ .ToDictionary(g => g.Key, g => g.Select(x => x.SessionIndex > 0 ? x.SessionIndex : x.Order)
+ .Where(order => order > 0).Distinct().ToList());
+ var registrationOrders = athletePersons.GroupBy(x => x.RegistrationId)
+ .ToDictionary(g => g.Key, g => g.SelectMany(person => itemOrders.GetValueOrDefault(person.RegistrationItemId) ?? new List())
+ .Distinct().OrderBy(order => order).ToList());
+ var coachOrders = fallbackPersons.Where(p => p.UserId > 0)
+ .GroupBy(p => p.UserId)
+ .ToDictionary(g => g.Key, g => g.SelectMany(p => registrationOrders.GetValueOrDefault(p.RegistrationId) ?? new List())
+ .Distinct().OrderBy(order => order).ToList());
+ var coachUserIds = coachOrders.Keys.ToList();
if (coachUserIds.Count == 0) return new();
var userMap = await LoadUserMapAsync(coachUserIds);
- var defaultOrder = units.Select(x => x.SessionIndex).FirstOrDefault(x => x > 0);
- if (defaultOrder <= 0) defaultOrder = 1;
-
return coachUserIds.Select(uid =>
{
userMap.TryGetValue(uid, out var user);
- return (user?.Mobile, defaultOrder);
+ return (user?.Mobile, coachOrders[uid]);
}).ToList();
}
diff --git a/YD_Event.Application/Client/CheckinScheduleService.cs b/YD_Event.Application/Client/CheckinScheduleService.cs
index 1f2254e..1de81da 100644
--- a/YD_Event.Application/Client/CheckinScheduleService.cs
+++ b/YD_Event.Application/Client/CheckinScheduleService.cs
@@ -8,6 +8,7 @@ public class CheckinScheduleService : ITransient
{
private readonly ISqlSugarRepository _eventRepository;
private readonly ISqlSugarRepository _personRepository;
+ private readonly ISqlSugarRepository _registrationRepository;
private readonly ISqlSugarRepository _sessionRepository;
private readonly ISqlSugarRepository _entryRepository;
private readonly ISqlSugarRepository _eventProjectRepository;
@@ -18,6 +19,7 @@ public class CheckinScheduleService : ITransient
public CheckinScheduleService(
ISqlSugarRepository eventRepository,
ISqlSugarRepository personRepository,
+ ISqlSugarRepository registrationRepository,
ISqlSugarRepository sessionRepository,
ISqlSugarRepository entryRepository,
ISqlSugarRepository eventProjectRepository,
@@ -27,6 +29,7 @@ public class CheckinScheduleService : ITransient
{
_eventRepository = eventRepository;
_personRepository = personRepository;
+ _registrationRepository = registrationRepository;
_sessionRepository = sessionRepository;
_entryRepository = entryRepository;
_eventProjectRepository = eventProjectRepository;
@@ -57,6 +60,13 @@ public class CheckinScheduleService : ITransient
.Where(x => personIds.Contains(x.Id) && !x.DeleteMark && x.PersonType == PersonType.Athlete)
.ToListAsync();
var personMap = persons.ToDictionary(x => x.Id);
+ var registrationIds = persons.Select(x => x.RegistrationId).Distinct().ToList();
+ var registrationModes = registrationIds.Count == 0
+ ? new Dictionary()
+ : (await _registrationRepository.AsQueryable()
+ .Where(x => registrationIds.Contains(x.Id) && !x.DeleteMark)
+ .ToListAsync())
+ .ToDictionary(x => x.Id, x => x.RegistrationMode);
// 团队项目沿用报名项目作为检录单位;个人项目追加报名人员 ID,
// 避免单位账号在同一报名项目下填报多人时共用同一个检录键。
@@ -75,6 +85,10 @@ public class CheckinScheduleService : ITransient
GroupId = row.EventProjectGroupId,
GroupName = row.GroupName,
RegistrationItemId = row.RegistrationItemId,
+ RegistrationMode = row.RegistrationPersonIds
+ .Where(personMap.ContainsKey)
+ .Select(personId => registrationModes.GetValueOrDefault(personMap[personId].RegistrationId))
+ .FirstOrDefault(mode => mode > 0) is var mode && mode > 0 ? mode : null,
TeamName = row.TeamName ?? string.Empty,
UnitName = row.UnitName ?? string.Empty,
RegistrationPersonIds = row.RegistrationPersonIds.Distinct().ToList(),
diff --git a/YD_Event.Application/Client/Dtos/CheckinAdminDtos.cs b/YD_Event.Application/Client/Dtos/CheckinAdminDtos.cs
index 818e735..0246dce 100644
--- a/YD_Event.Application/Client/Dtos/CheckinAdminDtos.cs
+++ b/YD_Event.Application/Client/Dtos/CheckinAdminDtos.cs
@@ -48,6 +48,8 @@ public class CheckinOrderBookUnitDto
public long? GroupId { get; set; }
public string GroupName { get; set; } = string.Empty;
public long RegistrationItemId { get; set; }
+ /// 报名方式:1-个人报名,2-单位报名;无法关联报名记录时为空。
+ public int? RegistrationMode { get; set; }
public string TeamName { get; set; } = string.Empty;
public string UnitName { get; set; } = string.Empty;
public List RegistrationPersonIds { get; set; } = new();
diff --git a/YD_Event.Application/Sms/AliyunSmsService.cs b/YD_Event.Application/Sms/AliyunSmsService.cs
index 8999ac6..7999f18 100644
--- a/YD_Event.Application/Sms/AliyunSmsService.cs
+++ b/YD_Event.Application/Sms/AliyunSmsService.cs
@@ -78,10 +78,10 @@ public class AliyunSmsService : ITransient
///
/// 发送检录通知短信;未配置检录模板时返回 false 由调用方降级处理。
- /// 模板示例:您报名的${event}正在检录,当前检录至${current},您所在第${order}场次,请提前前往检录区做好准备。
+ /// 模板示例:您报名的${event}正在检录,当前检录至${current},您所在第${order}场次,请提前前往检录区做好准备。单位报名多个场次时 order 可为“1、3、4、5”。
/// current 参数格式:{项目名称}第{x}场次。
///
- public async Task SendCheckinNotice(string phone, string eventName, string currentSession, int sessionOrder)
+ public async Task SendCheckinNotice(string phone, string eventName, string currentSession, string sessionOrders)
{
if (string.IsNullOrWhiteSpace(_options.CheckinNoticeTemplateCode))
{
@@ -93,7 +93,7 @@ public class AliyunSmsService : ITransient
{
@event = eventName,
current = currentSession,
- order = sessionOrder.ToString()
+ order = sessionOrders
});
await SendSms(phone, _options.CheckinNoticeTemplateCode, param);
return true;
diff --git a/YD_Event.Core/Options/SmsOptions.cs b/YD_Event.Core/Options/SmsOptions.cs
index 7c04dbb..7a66e22 100644
--- a/YD_Event.Core/Options/SmsOptions.cs
+++ b/YD_Event.Core/Options/SmsOptions.cs
@@ -32,7 +32,7 @@ public class SmsOptions : IConfigurableOptions
public string VerificationCodeTemplateCode { get; set; }
///
- /// 检录通知模板Code;模板格式:您报名的${event}正在检录,当前检录至${current},您所在第${order}场次,请提前前往检录区做好准备。
+ /// 检录通知模板Code;order 支持单个场次或“1、3、4、5”场次列表。
/// current 参数格式为“{项目名称}第{x}场次”;未配置时不发送检录短信。
///
[ConfigurationKeyName("CheckinNoticeTemplate")]