diff --git a/YD_Event.Application/Client/MyController.cs b/YD_Event.Application/Client/MyController.cs index 8f9c6dd..9f721de 100644 --- a/YD_Event.Application/Client/MyController.cs +++ b/YD_Event.Application/Client/MyController.cs @@ -82,7 +82,7 @@ public class MyController : IDynamicApiController if (user == null) throw Oops.Bah("用户信息不存在"); // 统计证书数量 - var certCount = await _certificateRepository.CountAsync(c => c.CreatedUserId == userId && c.CertificateStatus == 2); + var certCount = await _certificateRepository.CountAsync(c => c.UserId == userId && c.CertificateStatus == 2); // 积分 (需计算积分总和,PointsRecord 表有 Points 字段,Type 1-加 2-减) // 简单计算:Sum(Type=1) - Sum(Type=2) @@ -351,7 +351,7 @@ public class MyController : IDynamicApiController public async Task GetCertificatesOverview() { var userId = GetCurrentUserId(); - var count = await _certificateRepository.CountAsync(c => c.CreatedUserId == userId && c.CertificateStatus == 2 && !c.DeleteMark); + var count = await _certificateRepository.CountAsync(c => c.UserId == userId && c.CertificateStatus == 2 && !c.DeleteMark); return new MyCertificateOverviewDto { @@ -372,7 +372,7 @@ public class MyController : IDynamicApiController var query = _certificateRepository.AsQueryable() .LeftJoin((c, e) => c.EventId == e.Id) .LeftJoin((c, e, p) => c.ProjectId == p.Id) - .Where((c, e, p) => c.CreatedUserId == userId && c.CertificateStatus == 2 && !c.DeleteMark) + .Where((c, e, p) => c.UserId == userId && c.CertificateStatus == 2 && !c.DeleteMark) .Select((c, e, p) => new { Id = c.Id, diff --git a/YD_Event.Application/Event/Dtos/ScoreDtos.cs b/YD_Event.Application/Event/Dtos/ScoreDtos.cs index faec0ea..5c42a7e 100644 --- a/YD_Event.Application/Event/Dtos/ScoreDtos.cs +++ b/YD_Event.Application/Event/Dtos/ScoreDtos.cs @@ -193,6 +193,12 @@ public class IssueCertificateInput /// [Required(ErrorMessage = "请选择证书")] public long CertificateId { get; set; } + + /// + /// 页面类型:1-运动员页面,2-教练页面,3-领队页面 + /// 用于明确区分不同角色页面的证书生成逻辑 + /// + public CertificatePageType PageType { get; set; } } /// diff --git a/YD_Event.Application/Event/EventScoreService.cs b/YD_Event.Application/Event/EventScoreService.cs index 128eb7f..b796a0f 100644 --- a/YD_Event.Application/Event/EventScoreService.cs +++ b/YD_Event.Application/Event/EventScoreService.cs @@ -418,16 +418,66 @@ public class EventScoreService : BaseService, ITransient throw Oops.Bah("证书关联的模板不存在或不可用"); } - // 批量预加载所有需要的数据 - var persons = await _personRepository.GetListAsync(x => input.PersonIds.Contains(x.Id)); - var personDict = persons.ToDictionary(p => p.Id); + // 根据页面类型确定允许的人员类型 + List allowedPersonTypes = GetAllowedPersonTypes(input.PageType); + _logger.LogInformation("发放证书 - 页面类型: {PageType}, 允许的人员类型: {AllowedTypes}", + input.PageType, string.Join(", ", allowedPersonTypes)); - var personIds = persons.Select(p => p.Id).ToList(); - var existingResults = await _resultRepository.GetListAsync(x => personIds.Contains(x.RegistrationPersonId)); + // 批量预加载所有需要的数据 + // 先尝试直接从 EventRegistrationPerson 表获取所有人员 + var directPersons = await _personRepository.GetListAsync(x => input.PersonIds.Contains(x.Id)); + var directPersonDict = directPersons.ToDictionary(p => p.Id); + + // 找出在 directPersonDict 中找不到的人员ID,这些可能是教练/领队的 AppUser ID + var missingPersonIds = input.PersonIds.Where(id => !directPersonDict.ContainsKey(id)).ToList(); + + // 构建最终的人员字典 + var personDict = new Dictionary(); + foreach (var kvp in directPersonDict) + { + personDict[kvp.Key] = kvp.Value; + } + + // 对于找不到的人员ID,尝试通过团队关系查找对应的 EventRegistrationPerson 记录 + if (missingPersonIds.Any()) + { + _logger.LogInformation("查找 {Count} 个缺失的人员ID,尝试通过团队关系获取", missingPersonIds.Count); + + // 获取该赛事的所有 EventRegistrationPerson 记录 + var allEventPersons = await _personRepository.AsQueryable() + .InnerJoin((p, r) => p.RegistrationId == r.Id) + .Where((p, r) => r.EventId == input.EventId && !p.DeleteMark) + .Select((p, r) => p) + .ToListAsync(); + + // 建立 UserId 到 EventRegistrationPerson 的映射 + var userToPersonMap = allEventPersons + .Where(p => p.UserId > 0) + .GroupBy(p => p.UserId) + .ToDictionary(g => g.Key, g => g.First()); + + // 尝试通过 UserId 匹配 + foreach (var missingId in missingPersonIds) + { + if (userToPersonMap.TryGetValue(missingId, out var person)) + { + personDict[missingId] = person; + _logger.LogInformation("通过 UserId {UserId} 找到对应的 EventRegistrationPerson: {PersonId}", missingId, person.Id); + } + else + { + _logger.LogWarning("无法找到人员ID: {PersonId}", missingId); + } + } + } + + // 使用最终找到的人员ID列表 + var foundPersonIds = personDict.Values.Select(p => p.Id).ToList(); + var existingResults = await _resultRepository.GetListAsync(x => foundPersonIds.Contains(x.RegistrationPersonId)); var resultDict = existingResults.ToDictionary(r => r.RegistrationPersonId); var existingCerts = await _certificateRepository.GetListAsync(x => - personIds.Contains(x.ParticipationRecordId)); + foundPersonIds.Contains(x.ParticipationRecordId)); var certDict = existingCerts.ToDictionary(c => c.ParticipationRecordId); var certsToInsert = new List(); @@ -442,19 +492,30 @@ public class EventScoreService : BaseService, ITransient continue; } + // 验证人员类型是否与当前页面类型匹配 + if (!allowedPersonTypes.Contains(person.PersonType)) + { + result.FailCount++; + result.FailDetails.Add($"人员 {person.Name} 的类型 ({person.PersonType}) 与当前页面类型不匹配"); + continue; + } + + // 获取实际的 EventRegistrationPerson ID(如果通过 UserId 映射过来的,需要使用实际的 Id) + var actualPersonId = person.Id; + EventCertificate? cert = null; bool isNewCert = false; if (person.PersonType == PersonType.Athlete || person.PersonType == PersonType.User) { - if (!resultDict.TryGetValue(person.Id, out var resultRecord)) + if (!resultDict.TryGetValue(actualPersonId, out var resultRecord)) { result.FailCount++; result.FailDetails.Add($"人员 {person.Name} 没有成绩记录"); continue; } - if (certDict.TryGetValue(person.Id, out var existingCert)) + if (certDict.TryGetValue(actualPersonId, out var existingCert)) { cert = existingCert; } @@ -466,12 +527,14 @@ public class EventScoreService : BaseService, ITransient EventId = input.EventId, ResultId = resultRecord.Id, RegistrationId = person.RegistrationId, - ParticipationRecordId = person.Id, + ParticipationRecordId = actualPersonId, + ProjectId = person.ProjectId, + UserId = person.UserId, ParticipantName = person.Name, GroupName = person.GroupName, CertificateStatus = 1, GeneratedTime = now, - CertificateNo = $"C{now:yyyyMMdd}{person.Id}", + CertificateNo = $"C{now:yyyyMMdd}{actualPersonId}", CreatedTime = now, TemplateId = templateId, CertificateUrl = string.Empty @@ -515,7 +578,7 @@ public class EventScoreService : BaseService, ITransient } else { - if (certDict.TryGetValue(person.Id, out var existingCert)) + if (certDict.TryGetValue(actualPersonId, out var existingCert)) { cert = existingCert; } @@ -526,12 +589,14 @@ public class EventScoreService : BaseService, ITransient Id = _idGenerator.NewLong(), EventId = input.EventId, RegistrationId = person.RegistrationId, - ParticipationRecordId = person.Id, + ParticipationRecordId = actualPersonId, + ProjectId = person.ProjectId, + UserId = person.UserId, ParticipantName = person.Name, GroupName = person.GroupName, CertificateStatus = 1, GeneratedTime = now, - CertificateNo = $"CC{now:yyyyMMdd}{person.Id}", + CertificateNo = $"CC{now:yyyyMMdd}{actualPersonId}", CreatedTime = now, TemplateId = templateId, CertificateUrl = string.Empty @@ -609,6 +674,22 @@ public class EventScoreService : BaseService, ITransient return result; } + /// + /// 根据页面类型获取允许的人员类型列表 + /// + /// 页面类型 + /// 允许的人员类型列表 + private List GetAllowedPersonTypes(CertificatePageType pageType) + { + return pageType switch + { + CertificatePageType.Athlete => new List { PersonType.Athlete, PersonType.User }, + CertificatePageType.Coach => new List { PersonType.Coach }, + CertificatePageType.Leader => new List { PersonType.Leader }, + _ => new List { PersonType.Athlete, PersonType.User, PersonType.Coach, PersonType.Leader } + }; + } + /// /// 构建运动员占位符数据 /// @@ -708,7 +789,8 @@ public class EventScoreService : BaseService, ITransient /// 输入参数 /// 证书详情 [DisplayName("查看证书")] - public async Task GetCertificateDetail(YD_Event.Application.Certificate.Dtos.IdInput input) + [HttpPost("getcertificatedetail")] + public async Task GetCertificateDetail([FromBody] YD_Event.Application.Certificate.Dtos.IdInput input) { if (input.Id <= 0) { @@ -727,7 +809,16 @@ public class EventScoreService : BaseService, ITransient throw Oops.Bah("未找到对应的证书信息"); } - return new { Url = cert.CertificateUrl }; + return new + { + Id = cert.Id, + CertificateNo = cert.CertificateNo, + CertificateUrl = cert.CertificateUrl, + CertificateStatus = cert.CertificateStatus, + IssuedTime = cert.IssuedTime, + ParticipantName = cert.ParticipantName, + GroupName = cert.GroupName + }; } /// @@ -817,7 +908,8 @@ public class EventScoreService : BaseService, ITransient /// 导出证书 /// [DisplayName("导出证书")] - public async Task ExportCertificate(ScoreQueryInput input) + [HttpGet] + public async Task ExportCertificate([FromQuery] ScoreQueryInput input) { var list = await _certificateRepository.AsQueryable() .LeftJoin((c, r) => c.ResultId == r.Id) @@ -845,8 +937,13 @@ public class EventScoreService : BaseService, ITransient .ToListAsync(); using var stream = new MemoryStream(); - stream.SaveAs(list); - return Convert.ToBase64String(stream.ToArray()); + await stream.SaveAsAsync(list); + var bytes = stream.ToArray(); + + return new FileContentResult(bytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + { + FileDownloadName = $"证书列表_{DateTime.Now:yyyyMMddHHmmss}.xlsx" + }; } /// diff --git a/YD_Event.Application/Event/UnifiedRegistrationService.cs b/YD_Event.Application/Event/UnifiedRegistrationService.cs index 8cab486..2fc9ecd 100644 --- a/YD_Event.Application/Event/UnifiedRegistrationService.cs +++ b/YD_Event.Application/Event/UnifiedRegistrationService.cs @@ -1266,7 +1266,7 @@ public class UnifiedRegistrationService : ITransient 0, eventProjectGroup.GroupName ?? "未知组别", "ProjectGroupCapacity", - $"项目组别限报设置:项目\"{project.Name}\"的组别\"{eventProjectGroup.GroupName}\"限报{projectLimit.LimitCount}队,本次提交{submittedTeamCount}队,超出限报配置")); + $"{project.Name}项目已达到报名限制")); } } else @@ -1286,7 +1286,7 @@ public class UnifiedRegistrationService : ITransient 0, eventProjectGroup.GroupName ?? "未知组别", "ProjectGroupCapacity", - $"项目组别限报设置:项目\"{project.Name}\"的组别\"{eventProjectGroup.GroupName}\"限报{projectLimit.LimitCount}人,本次提交{submittedPersonCount}人,超出限报配置")); + $"{project.Name}项目已达到报名限制")); } } } @@ -1434,7 +1434,7 @@ public class UnifiedRegistrationService : ITransient member.Id, member.Name ?? member.NickName ?? "未知", "IndividualProjectLimit", - $"成员项目限报设置:队员\"{member.Name}\"已报名{individualProjectCount}个个人项目,个人项目限报{limit.IndividualProjectLimit}个,请减少个人项目报名数量")); + $"{member.Name}队员已达到报名限制")); } /// @@ -1459,7 +1459,7 @@ public class UnifiedRegistrationService : ITransient member.Id, member.Name ?? member.NickName ?? "未知", "TeamProjectLimit", - $"成员项目限报设置:队员\"{member.Name}\"已参与{teamProjectCount}个团队项目,团队项目限报{limit.TeamProjectLimit}个,请减少团队项目报名数量")); + $"{member.Name}队员已达到报名限制")); } /// @@ -1632,22 +1632,33 @@ public class UnifiedRegistrationService : ITransient .Distinct() .Count(); + // ==================== 项目组别条件判断逻辑 ==================== + // 当且仅当某个项目组别中已选择至少一名人员时,才对该项目组别执行后续条件判断流程 + if (totalAssignedMembers == 0) + { + // 若项目组别未选择任何人员,直接忽略该项目组别,不进行任何条件判断 + continue; + } + + // ==================== 报名限制验证规则 ==================== + // 个人项目不进行完成人数限制,仅适用项目报名限制规则 + if (!isTeamProject) + { + continue; + } + // ==================== 项目完成人数验证 ==================== - // 验证用户选择的参与人数必须与项目的"完成人数"完全一致 + // 只对团队项目进行完成人数验证 if (project.CompletionCount > 0 && totalAssignedMembers != project.CompletionCount) { string errorMessage; if (totalAssignedMembers < project.CompletionCount) { - errorMessage = isTeamProject - ? $"项目完成人数限制:项目\"{project.Name}\"的组别\"{eventProjectGroup.GroupName}\"要求完成人数为{project.CompletionCount}人,当前只分配了{totalAssignedMembers}人,请补充队员" - : $"项目完成人数限制:项目\"{project.Name}\"的组别\"{eventProjectGroup.GroupName}\"要求完成人数为{project.CompletionCount}人,当前只分配了{totalAssignedMembers}人,请补充队员"; + errorMessage = $"{project.Name}参赛人数错误(少{project.CompletionCount - totalAssignedMembers}人)"; } else { - errorMessage = isTeamProject - ? $"项目完成人数限制:项目\"{project.Name}\"的组别\"{eventProjectGroup.GroupName}\"要求完成人数为{project.CompletionCount}人,当前分配了{totalAssignedMembers}人,人数超额,请移除多余队员" - : $"项目完成人数限制:项目\"{project.Name}\"的组别\"{eventProjectGroup.GroupName}\"要求完成人数为{project.CompletionCount}人,当前分配了{totalAssignedMembers}人,人数超额,请移除多余队员"; + errorMessage = $"{project.Name}参赛人数错误(多{totalAssignedMembers - project.CompletionCount}人)"; } result.HasLimit = true; diff --git a/YD_Event.Application/Event/UserEventRecordService.cs b/YD_Event.Application/Event/UserEventRecordService.cs index f13cd89..724bb4f 100644 --- a/YD_Event.Application/Event/UserEventRecordService.cs +++ b/YD_Event.Application/Event/UserEventRecordService.cs @@ -347,10 +347,19 @@ public class UserEventRecordService : IDynamicApiController, ITransient [HttpGet("certificate/{registrationId}")] public async Task> GetCertificate([FromRoute] long registrationId) { + var userId = _authManager.UserId; + // 2-已发放 var cert = await _certificateRepository.GetFirstAsync(x => x.RegistrationId == registrationId && x.CertificateStatus == 2); if (cert == null) return new ResponseDTO { Code = 404, Message = "证书未颁发", Success = false }; + // 权限验证 + bool hasPermission = await CheckCertificatePermissionAsync(userId, cert); + if (!hasPermission) + { + return new ResponseDTO { Code = 403, Message = "无权查看该证书", Success = false }; + } + return new ResponseDTO { Code = 200, Success = true, @@ -364,6 +373,44 @@ public class UserEventRecordService : IDynamicApiController, ITransient }; } + /// + /// 检查证书查看权限 + /// + private async Task CheckCertificatePermissionAsync(long currentUserId, EventCertificate cert) + { + // 如果是自己的证书,直接允许 + if (cert.UserId == currentUserId) + { + return true; + } + + // 检查当前用户是否是单位用户,且该证书属于其管理的团队成员 + var currentUser = await _userRepository.GetByIdAsync(currentUserId); + if (currentUser == null) + { + return false; + } + + // 如果当前用户有单位认证 + if (currentUser.MerchantCertificationId.HasValue) + { + // 查询当前用户管理的团队成员 + var teamMemberUserIds = await _db.Queryable() + .InnerJoin((t, tm) => t.Id == tm.TeamId) + .Where((t, tm) => t.CreatedUserId == currentUserId && !t.DeleteMark && !tm.DeleteMark) + .Select((t, tm) => tm.UserId) + .ToListAsync(); + + // 检查证书用户是否在团队成员列表中 + if (teamMemberUserIds.Contains(cert.UserId)) + { + return true; + } + } + + return false; + } + /// /// 7. 取消报名预览 /// diff --git a/YD_Event.Application/Event/UserEventRegistrationService.cs b/YD_Event.Application/Event/UserEventRegistrationService.cs index 33a9ecb..cf4569e 100644 --- a/YD_Event.Application/Event/UserEventRegistrationService.cs +++ b/YD_Event.Application/Event/UserEventRegistrationService.cs @@ -1141,35 +1141,43 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient // 计算该组别的分配人数(不跨组别,也不跨项目去重) var assignedCount = groupMemberAssignment.MemberIds?.Distinct().Count() ?? 0; - if (project.CompletionCount > 0) + // ==================== 项目组别条件判断逻辑 ==================== + // 当且仅当某个项目组别中已选择至少一名人员时,才对该项目组别执行后续条件判断流程 + if (assignedCount == 0) { + // 若项目组别未选择任何人员,直接忽略该项目组别,不进行任何条件判断 + continue; + } + + // ==================== 报名限制验证规则 ==================== + // 个人项目不进行完成人数限制,仅适用项目报名限制规则 + if (!isTeamProject) + { + continue; + } + + // ==================== 项目完成人数验证 ==================== + // 只对团队项目进行完成人数验证 + if (project.CompletionCount > 0 && assignedCount != project.CompletionCount) + { + string errorMessage; if (assignedCount < project.CompletionCount) { - result.HasError = true; - result.Errors.Add(new ProjectCompletionError - { - ProjectId = eventProject.Id, - ProjectName = project.Name ?? "未知项目", - ErrorType = "Insufficient", - ErrorMessage = isTeamProject - ? $"项目\"{project.Name}\"的组别\"{assignedGroup.GroupName}\"要求完成人数为{project.CompletionCount}人,当前只分配了{assignedCount}人,请补充队员" - : $"项目\"{project.Name}\"的组别\"{assignedGroup.GroupName}\"要求完成人数为{project.CompletionCount}人,当前只分配了{assignedCount}人,请补充队员" - }); + errorMessage = $"{project.Name}参赛人数错误(少{project.CompletionCount - assignedCount}人)"; } - else if (assignedCount > project.CompletionCount) + else { - // 只要达到或超过完成人数就提示超限(特别是个人项目) - result.HasError = true; - result.Errors.Add(new ProjectCompletionError - { - ProjectId = eventProject.Id, - ProjectName = project.Name ?? "未知项目", - ErrorType = "Excessive", - ErrorMessage = isTeamProject - ? $"项目\"{project.Name}\"的组别\"{assignedGroup.GroupName}\"要求完成人数为{project.CompletionCount}人,当前分配了{assignedCount}人,人数超额,请移除多余队员" - : $"项目\"{project.Name}\"的组别\"{assignedGroup.GroupName}\"要求完成人数为{project.CompletionCount}人,当前分配了{assignedCount}人,人数超额,请移除多余队员" - }); + errorMessage = $"{project.Name}参赛人数错误(多{assignedCount - project.CompletionCount}人)"; } + + result.HasError = true; + result.Errors.Add(new ProjectCompletionError + { + ProjectId = eventProject.Id, + ProjectName = project.Name ?? "未知项目", + ErrorType = assignedCount < project.CompletionCount ? "Insufficient" : "Excessive", + ErrorMessage = errorMessage + }); } } } diff --git a/YD_Event.Application/YD_Event.Application.xml b/YD_Event.Application/YD_Event.Application.xml index d73ddd1..bb5cf3a 100644 --- a/YD_Event.Application/YD_Event.Application.xml +++ b/YD_Event.Application/YD_Event.Application.xml @@ -5895,6 +5895,12 @@ 证书ID(必选) + + + 页面类型:1-运动员页面,2-教练页面,3-领队页面 + 用于明确区分不同角色页面的证书生成逻辑 + + 发放证书结果 @@ -7749,6 +7755,13 @@ 发放证书(支持模板选择和批量发放) + + + 根据页面类型获取允许的人员类型列表 + + 页面类型 + 允许的人员类型列表 + 构建运动员占位符数据 @@ -8289,6 +8302,11 @@ 6. 查看证书 + + + 检查证书查看权限 + + 7. 取消报名预览 diff --git a/YD_Event.Core/Entities/EventCertificate.cs b/YD_Event.Core/Entities/EventCertificate.cs index ec9d4bf..80aa1fb 100644 --- a/YD_Event.Core/Entities/EventCertificate.cs +++ b/YD_Event.Core/Entities/EventCertificate.cs @@ -33,6 +33,11 @@ public class EventCertificate : Entity, ISoftDelete, ICreatedUserId, ICrea /// public long ProjectId { get; set; } + /// + /// 关联的用户ID + /// + public long UserId { get; set; } + /// /// 参赛选手姓名 /// diff --git a/YD_Event.Core/Enum/CertificatePageType.cs b/YD_Event.Core/Enum/CertificatePageType.cs new file mode 100644 index 0000000..4242b3f --- /dev/null +++ b/YD_Event.Core/Enum/CertificatePageType.cs @@ -0,0 +1,33 @@ +using System.ComponentModel; + +namespace YD_Event.Core.Enum; + +/// +/// 证书页面类型 +/// +public enum CertificatePageType +{ + /// + /// 未指定(向后兼容) + /// + [Description("未指定")] + Unspecified = 0, + + /// + /// 运动员页面 + /// + [Description("运动员页面")] + Athlete = 1, + + /// + /// 教练页面 + /// + [Description("教练页面")] + Coach = 2, + + /// + /// 领队页面 + /// + [Description("领队页面")] + Leader = 3 +} diff --git a/YD_Event.Core/YD_Event.Core.xml b/YD_Event.Core/YD_Event.Core.xml index aafd277..494f3b0 100644 --- a/YD_Event.Core/YD_Event.Core.xml +++ b/YD_Event.Core/YD_Event.Core.xml @@ -1142,6 +1142,11 @@ 赛事项目ID + + + 关联的用户ID + + 参赛选手姓名 @@ -4290,6 +4295,31 @@ 禁用 + + + 证书页面类型 + + + + + 未指定(向后兼容) + + + + + 运动员页面 + + + + + 教练页面 + + + + + 领队页面 + + 收费模式枚举