using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using YD_Event.Application.Event.Dtos; using YD_Event.Application.Common; using YD_Event.Core.Entities; using YD_Event.Core.Enum; using Easy.Core; using SqlSugar; using Mapster; using YD_Event.Application.Auth; using Furion.DynamicApiController; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Authorization; using System.ComponentModel; using Furion.FriendlyException; using Furion.DatabaseAccessor; using Furion.DependencyInjection; using YD_Event.Core.Infrastructure.WeChat; using YD_Event.Core.Domain; using Microsoft.Extensions.Logging; namespace YD_Event.Application.Event; /// /// 用户端参赛记录管理 /// public class UserEventRecordService : IDynamicApiController, ITransient { private readonly ISqlSugarRepository _registrationRepository; private readonly ISqlSugarRepository _registrationPersonRepository; private readonly ISqlSugarRepository _registrationItemRepository; private readonly ISqlSugarRepository _projectRepository; private readonly ISqlSugarRepository _userRepository; private readonly ISqlSugarRepository _certificateRepository; private readonly ISqlSugarRepository _orderRepository; private readonly ISqlSugarRepository _eventRepository; private readonly ISqlSugarClient _db; private readonly AuthManager _authManager; private readonly IIdGenerator _idGenerator; private readonly FileService _fileService; private readonly ILogger _logger; private readonly UnifiedRegistrationService _unifiedRegistrationService; public UserEventRecordService( ISqlSugarRepository registrationRepository, ISqlSugarRepository registrationPersonRepository, ISqlSugarRepository registrationItemRepository, ISqlSugarRepository projectRepository, ISqlSugarRepository userRepository, ISqlSugarRepository certificateRepository, ISqlSugarRepository orderRepository, ISqlSugarRepository eventRepository, ISqlSugarClient db, AuthManager authManager, IIdGenerator idGenerator, FileService fileService, ILogger logger, UnifiedRegistrationService unifiedRegistrationService) { _registrationRepository = registrationRepository; _registrationPersonRepository = registrationPersonRepository; _registrationItemRepository = registrationItemRepository; _projectRepository = projectRepository; _userRepository = userRepository; _certificateRepository = certificateRepository; _orderRepository = orderRepository; _eventRepository = eventRepository; _db = db; _authManager = authManager; _idGenerator = idGenerator; _fileService = fileService; _logger = logger; _unifiedRegistrationService = unifiedRegistrationService; } /// /// 1. 列表查询 /// [DisplayName("参赛记录列表查询")] [HttpGet("list")] public async Task> GetRegistrationList([FromQuery] RegistrationListQueryInput input) { var userId = _authManager.UserId; var list = await _registrationRepository.AsQueryable() .InnerJoin((r, e) => r.EventId == e.Id) .LeftJoin((r, e, fc) => e.Id == fc.EventId) .Where((r, e, fc) => r.CreatedUserId == userId && !r.DeleteMark) // 支付状态筛选: // - 线下支付时:显示未支付(0)和已支付(1)状态的数据 // - 线上支付时:仅显示已支付(1)状态的数据 .Where((r, e, fc) => (r.PaymentMethod == 1 && (r.PaymentStatus == 0 || r.PaymentStatus == 1)) || (r.PaymentMethod == 2 && r.PaymentStatus == 1)) .WhereIF(input.EventId.HasValue, (r, e, fc) => r.EventId == input.EventId) .WhereIF(!string.IsNullOrWhiteSpace(input.Keyword), (r, e, fc) => r.TeamName.Contains(input.Keyword) || e.EventName.Contains(input.Keyword)) .OrderByDescending((r, e, fc) => r.CreatedTime) .Select((r, e, fc) => new { r.Id, r.EventId, e.EventName, r.TeamName, r.PaymentStatus, r.MaterialUrl, r.PaymentProofUrl, r.CreatedTime, r.UpdatedTime, r.TotalPersonCount, e.MaterialUploadEndTime, e.CoverImage, e.EventType, e.EventStatus, e.RegistrationStartTime, e.RegistrationEndTime, e.MatchStartTime, e.MatchEndTime, fc.ChargeMode, r.PaymentMethod }) .ToPagedListAsync(input); var resultItems = list.Rows.Select(x => { return new RegistrationItemDTO { RegistrationId = x.Id, EventName = x.EventName, CoverImage = x.CoverImage, EventType = (int?)x.EventType, ChargeMode = (int?)x.ChargeMode, EventStatus = (int?)x.EventStatus, RegistrationStartTime = x.RegistrationStartTime, RegistrationEndTime = x.RegistrationEndTime, MaterialDeadline = x.MaterialUploadEndTime, MatchStartTime = x.MatchStartTime, MatchEndTime = x.MatchEndTime, TeamName = x.TeamName, CreateTime = x.CreatedTime, UpdateTime = x.UpdatedTime, PayStatus = x.PaymentStatus, CertStatus = 0, // 未颁发 HasMaterial = !string.IsNullOrEmpty(x.MaterialUrl), HasVoucher = !string.IsNullOrEmpty(x.PaymentProofUrl), MemberCount = x.TotalPersonCount, MaxMember = 10, ExpireTime = x.MaterialUploadEndTime }; }).ToList(); return new PageResult { PageNo = list.PageNo, PageSize = list.PageSize, Total = list.Total, Pages = list.Pages, Rows = resultItems }; } /// /// 2. 取消报名 /// [DisplayName("取消报名")] [HttpPost("cancel/{registrationId}")] [UnitOfWork] public async Task> CancelRegistration([FromRoute] long registrationId) { var userId = _authManager.UserId; var registration = await _registrationRepository.GetFirstAsync(x => x.Id == registrationId && x.CreatedUserId == userId); if (registration == null) throw Oops.Bah("报名记录不存在"); if (registration.PaymentStatus == 4) throw Oops.Bah("订单已取消"); // 校验状态:只有未支付状态可直接取消,已支付需走退款流程 if (registration.PaymentStatus == 1) throw Oops.Bah("已支付订单请联系客服退款"); _logger.LogInformation("开始取消报名:报名ID={RegistrationId}, 操作人={UserId}, 当前状态={PaymentStatus}", registrationId, userId, registration.PaymentStatus); // 更新报名状态 registration.PaymentStatus = 4; // 已取消 registration.UpdatedUserId = userId; registration.UpdatedTime = DateTime.Now; await _registrationRepository.UpdateAsync(registration); // 同步更新订单状态 if (!string.IsNullOrEmpty(registration.OrderNo)) { var order = await _orderRepository.GetFirstAsync(o => o.OrderNo == registration.OrderNo); if (order != null) { order.OrderStatus = OrderStatus.Cancelled; order.UpdatedTime = DateTime.Now; order.UpdatedUserId = userId; await _orderRepository.UpdateAsync(order); _logger.LogInformation("订单状态已同步更新:订单号={OrderNo}, 状态={OrderStatus}", registration.OrderNo, OrderStatus.Cancelled); } else { _logger.LogWarning("未找到对应的订单记录:报名ID={RegistrationId}, 订单号={OrderNo}", registrationId, registration.OrderNo); } } _logger.LogInformation("取消报名成功:报名ID={RegistrationId}", registrationId); return new ResponseDTO { Code = 200, Success = true, Data = true, Message = "取消成功" }; } /// /// 3. 上传素材 /// [DisplayName("上传素材")] [HttpPost("material/{registrationId}")] [UnitOfWork] public async Task>> UploadMaterial([FromRoute] long registrationId, [FromForm] UploadMaterialInput input) { var userId = _authManager.UserId; var registration = await _registrationRepository.GetFirstAsync(x => x.Id == registrationId && x.CreatedUserId == userId); if (registration == null) throw Oops.Bah("报名记录不存在"); // 仅 status = 待上传素材 或 已上传素材 允许再次上传 // 即 PayStatus == 1 (已支付) if (registration.PaymentStatus != 1) throw Oops.Bah("未支付或状态不正确,无法上传素材"); // 总素材大小累计不超过 100 MB (简化校验,假设单次) long totalSize = input.Files.Sum(f => f.Length); if (totalSize > 100 * 1024 * 1024) throw Oops.Bah("素材总大小超过100MB"); var result = new List(); foreach (var file in input.Files) { var uploadResult = await _fileService.Upload(file); var item = uploadResult.FirstOrDefault(); if (item != null) { result.Add(new MaterialDTO { MaterialId = _idGenerator.NewLong().ToString(), OssUrl = item.Url, FileName = item.Name, FileSize = file.Length, UploadTime = DateTime.Now }); } } if (result.Any()) { registration.MaterialUrl = result.First().OssUrl; // 更新主表 await _registrationRepository.UpdateAsync(registration); } return new ResponseDTO> { Code = 200, Success = true, Data = result, Message = "上传成功" }; } /// /// 4. 上传付款凭证 /// [DisplayName("上传付款凭证")] [HttpPost("voucher/{registrationId}")] [UnitOfWork] public async Task> UploadVoucher([FromRoute] long registrationId, [FromForm] UploadVoucherInput input) { var userId = _authManager.UserId; var registration = await _registrationRepository.GetFirstAsync(x => x.Id == registrationId && x.CreatedUserId == userId); if (registration == null) throw Oops.Bah("报名记录不存在"); // 仅 status = 待付款 允许上传 if (registration.PaymentStatus != 0) throw Oops.Bah("当前状态不允许上传凭证"); var uploadResult = await _fileService.Upload(input.File); var fileInfo = uploadResult.First(); registration.PaymentProofUrl = fileInfo.Url; registration.PaymentStatus = 0; registration.UpdatedTime = DateTime.Now; await _registrationRepository.UpdateAsync(registration); return new ResponseDTO { Code = 200, Success = true, Message = "上传成功", Data = new VoucherDTO { VoucherId = _idGenerator.NewLong().ToString(), OssUrl = fileInfo.Url, AuditStatus = "待审核" } }; } /// /// 5.1 查看队员列表 /// [DisplayName("查看队员列表")] [HttpGet("members/{registrationId}")] public async Task> GetTeamMembers([FromRoute] long registrationId) { var userId = _authManager.UserId; var registration = await _registrationRepository.GetFirstAsync(x => x.Id == registrationId && x.CreatedUserId == userId); if (registration == null) throw Oops.Bah("报名记录不存在"); var persons = await _registrationPersonRepository.GetListAsync(x => x.RegistrationId == registrationId); // 需要关联 AppUser 获取更多信息 (Email 等) // 这里简化处理 return persons.Select(p => new TeamMemberDTO { UserId = 0, // 需关联 AppUser RealName = p.Name, Phone = p.Phone, Email = "", // 暂无 Role = p.PersonType == PersonType.Leader ? "队长" : "队员", // 假设 InviteStatus = "已加入", JoinTime = p.CreatedTime }).ToList(); } /// /// 5.2 更换队员 /// [DisplayName("更换队员")] [HttpPost("members/replace/{registrationId}/{userId}")] [UnitOfWork] public async Task> ReplaceMember([FromRoute] long registrationId, [FromRoute] long userId, [FromBody] ReplaceMemberInput input) { var currentUserId = _authManager.UserId; // 校验队长权限: 仅创建者(队长)可操作 var registration = await _registrationRepository.GetFirstAsync(x => x.Id == registrationId && x.CreatedUserId == currentUserId); if (registration == null) throw Oops.Bah("无权操作或记录不存在"); var person = await _registrationPersonRepository.GetFirstAsync(x => x.RegistrationId == registrationId && x.Id == userId); if (person == null) throw Oops.Bah("队员不存在"); // 被替换人 role ≠ 队长 if (person.PersonType == PersonType.Leader) throw Oops.Bah("不能替换队长"); // 检查新队员 var newUser = await _userRepository.GetByIdAsync(input.NewUserId); if (newUser == null) throw Oops.Bah("新队员不存在"); long oldUserId = person.Id; // 更新信息 person.Name = newUser.Name; person.IdCard = newUser.IdCardNumber; person.Phone = newUser.Mobile; person.UpdatedTime = DateTime.Now; await _registrationPersonRepository.UpdateAsync(person); return new ResponseDTO { Code = 200, Success = true, Data = true, Message = "更换成功" }; } /// /// 6. 查看证书 /// [DisplayName("查看证书")] [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, Data = new CertificateDTO { CertUrl = cert.CertificateUrl, CertNumber = cert.CertificateNo, IssueTime = cert.IssuedTime ?? DateTime.MinValue, ExpireTime = null } }; } /// /// 检查证书查看权限 /// 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. 取消报名预览 /// [DisplayName("取消报名预览")] [HttpGet("cancel/preview/{registrationId}")] public async Task CancelPreview([FromRoute] long registrationId) { var userId = _authManager.UserId; _logger.LogInformation("调用统一取消报名预览:报名ID={RegistrationId}, 操作人={UserId}", registrationId, userId); return await _unifiedRegistrationService.GetCancelPreviewAsync(registrationId, userId); } /// /// 8. 执行取消报名 /// [DisplayName("执行取消报名")] [HttpPost("cancel/execute")] [UnitOfWork] public async Task CancelExecute([FromBody] CancelExecuteInput input) { var userId = _authManager.UserId; _logger.LogInformation("调用统一执行取消报名:报名ID={RegistrationId}, 操作人={UserId}", input.RegistrationId, userId); return await _unifiedRegistrationService.ExecuteCancelRegistrationAsync(input, userId); } /// /// 9. 获取参赛记录详情(包含项目和队员) /// [DisplayName("获取参赛记录详情(包含项目和队员)")] [HttpGet("detail-with-items/{registrationId}")] public async Task GetRegistrationDetailWithItems([FromRoute] long registrationId) { var userId = _authManager.UserId; var registration = await _registrationRepository.GetFirstAsync(x => x.Id == registrationId && x.CreatedUserId == userId); if (registration == null) throw Oops.Bah("报名记录不存在"); // 获取赛事信息 var eventInfo = await _eventRepository.GetByIdAsync(registration.EventId); // 获取报名项目 var items = await _registrationItemRepository.GetListAsync(x => x.RegistrationId == registrationId); // 获取所有队员 var persons = await _registrationPersonRepository.GetListAsync(x => x.RegistrationId == registrationId); // 获取组别信息(年龄和性别限制) var groupIds = items.Where(x => x.EventProjectGroupId.HasValue).Select(x => x.EventProjectGroupId.Value).Distinct().ToList(); var groups = new Dictionary(); if (groupIds.Any()) { var groupList = await _db.Queryable() .Where(x => groupIds.Contains(x.Id)) .ToListAsync(); groups = groupList.ToDictionary(x => x.Id); } // 获取项目信息(从Project表查询项目名称和完成人数) var projectIds = items.Select(x => x.ProjectId).Distinct().ToList(); var projects = new Dictionary(); if (projectIds.Any()) { var projectList = await _projectRepository.GetListAsync(x => projectIds.Contains(x.Id)); projects = projectList.ToDictionary(x => x.Id); } var result = new RegistrationDetailWithItemsDto { RegistrationId = registration.Id, EventName = eventInfo?.EventName ?? "", TeamName = registration.TeamName ?? "" }; foreach (var item in items) { var itemMembers = persons.Where(p => p.RegistrationItemId == item.Id).ToList(); // 获取组别信息 EventProjectGroup? group = null; if (item.EventProjectGroupId.HasValue && groups.TryGetValue(item.EventProjectGroupId.Value, out var foundGroup)) { group = foundGroup; } // 获取项目信息 YD_Event.Core.Entities.Project? project = null; if (projects.TryGetValue(item.ProjectId, out var foundProject)) { project = foundProject; } result.Items.Add(new RegistrationItemWithMembersDto { RegistrationItemId = item.Id, ProjectId = item.ProjectId, // 优先从Project表获取项目名称,如果没有则使用报名时保存的名称 ProjectName = project?.Name ?? item.ProjectName ?? "", GroupName = item.GroupName ?? "", ProjectTypeName = item.ProjectTypeName ?? "", PersonCount = item.PersonCount, // 从Project表获取完成人数 ProjectCompletionCount = project?.CompletionCount ?? 0, BirthDateStart = group?.BirthDateStart, BirthDateEnd = group?.BirthDateEnd, GenderRequirement = group?.Gender, Members = itemMembers.Select(p => new RegistrationPersonDto { Id = p.Id, Name = p.Name, IdCardNumber = p.IdCard, Gender = p.Gender, Mobile = p.Phone, BirthDate = p.BirthDate }).ToList() }); } return result; } /// /// 10. 更新参赛队员 /// [DisplayName("更新参赛队员")] [HttpPost("update-members")] [UnitOfWork] public async Task> UpdateRegistrationMembers([FromBody] UpdateRegistrationMembersInput input) { var userId = _authManager.UserId; var registration = await _registrationRepository.GetFirstAsync(x => x.Id == input.RegistrationId && x.CreatedUserId == userId); if (registration == null) throw Oops.Bah("报名记录不存在或无权操作"); _logger.LogInformation("开始更新参赛队员:报名ID={RegistrationId}, 操作人={UserId}", input.RegistrationId, userId); // 获取所有需要查询的用户ID var userIds = input.Items.SelectMany(x => x.Members).Where(x => x.UserId.HasValue).Select(x => x.UserId.Value).Distinct().ToList(); var users = new Dictionary(); if (userIds.Any()) { var userList = await _userRepository.GetListAsync(x => userIds.Contains(x.Id)); users = userList.ToDictionary(x => x.Id); } foreach (var item in input.Items) { // 验证项目是否属于该报名 var regItem = await _registrationItemRepository.GetFirstAsync(x => x.Id == item.RegistrationItemId && x.RegistrationId == input.RegistrationId); if (regItem == null) throw Oops.Bah($"项目记录不存在:{item.RegistrationItemId}"); // 删除旧的队员 await _registrationPersonRepository.DeleteAsync(x => x.RegistrationItemId == item.RegistrationItemId); // 添加新的队员 var newPersons = new List(); foreach (var member in item.Members) { // 如果有UserId,从用户表获取完整信息 YD_Event.Core.Entities.AppUser? user = null; if (member.UserId.HasValue && users.TryGetValue(member.UserId.Value, out var foundUser)) { user = foundUser; } var name = member.Name ?? user?.Name; var gender = member.Gender ?? user?.Gender ?? Gender.Male; var idCardNumber = member.IdCardNumber ?? user?.IdCardNumber; var birthDate = member.BirthDate ?? user?.BirthDate ?? DateTime.Now.AddYears(-20); var mobile = member.Mobile ?? user?.Mobile; // 验证必填字段 if (string.IsNullOrWhiteSpace(name)) throw Oops.Bah("队员姓名不能为空"); if (string.IsNullOrWhiteSpace(idCardNumber)) throw Oops.Bah("队员身份证号不能为空"); if (string.IsNullOrWhiteSpace(mobile)) throw Oops.Bah("队员手机号不能为空"); newPersons.Add(new EventRegistrationPerson { Id = _idGenerator.NewLong(), RegistrationId = input.RegistrationId, RegistrationItemId = item.RegistrationItemId, ProjectId = regItem.ProjectId, EventProjectGroupId = regItem.EventProjectGroupId, GroupName = regItem.GroupName, ProjectName = regItem.ProjectName, Name = name, Gender = gender, IdCard = idCardNumber, BirthDate = birthDate, Phone = mobile, Age = CalculateAge(birthDate), PersonType = PersonType.Athlete, UserId = member.UserId ?? 0, CreatedUserId = userId, CreatedTime = DateTime.Now }); } await _registrationPersonRepository.InsertRangeAsync(newPersons); // 更新项目的人数 regItem.PersonCount = item.Members.Count; regItem.UpdatedTime = DateTime.Now; regItem.UpdatedUserId = userId; await _registrationItemRepository.UpdateAsync(regItem); } // 更新报名总人数 var allPersonsCount = await _registrationPersonRepository.CountAsync(x => x.RegistrationId == input.RegistrationId); registration.TotalPersonCount = allPersonsCount; registration.UpdatedTime = DateTime.Now; registration.UpdatedUserId = userId; await _registrationRepository.UpdateAsync(registration); _logger.LogInformation("更新参赛队员成功:报名ID={RegistrationId}", input.RegistrationId); return new ResponseDTO { Code = 200, Success = true, Data = true, Message = "更新成功" }; } /// /// 计算年龄 /// private int CalculateAge(DateTime birthDate) { var today = DateTime.Today; var age = today.Year - birthDate.Year; if (birthDate.Date > today.AddYears(-age)) age--; return age; } }