using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using YD_Event.Application.Event.Dtos; using YD_Event.Core.Entities; using YD_Event.Core.Enum; using YD_Event.Core.Utils; using SqlSugar; using Mapster; using YD_Event.Application.Auth; using Furion.DynamicApiController; using Microsoft.AspNetCore.Mvc; using YD_Event.Application.Common; 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; namespace YD_Event.Application.Event; /// /// 用户端参赛记录管理 /// public class UserEventRecordService : IDynamicApiController, ITransient { private readonly ISqlSugarRepository _registrationRepository; private readonly ISqlSugarRepository _registrationPersonRepository; private readonly ISqlSugarRepository _userRepository; private readonly ISqlSugarRepository _certificateRepository; private readonly ISqlSugarRepository _weChatPayConfigRepository; private readonly ISqlSugarRepository _merchantRepository; private readonly ISqlSugarClient _db; private readonly AuthManager _authManager; private readonly IIdGenerator _idGenerator; private readonly FileService _fileService; private readonly WeChatPayService _weChatPayService; public UserEventRecordService( ISqlSugarRepository registrationRepository, ISqlSugarRepository registrationPersonRepository, ISqlSugarRepository userRepository, ISqlSugarRepository certificateRepository, ISqlSugarRepository weChatPayConfigRepository, ISqlSugarRepository merchantRepository, ISqlSugarClient db, AuthManager authManager, IIdGenerator idGenerator, FileService fileService, WeChatPayService weChatPayService) { _registrationRepository = registrationRepository; _registrationPersonRepository = registrationPersonRepository; _userRepository = userRepository; _certificateRepository = certificateRepository; _weChatPayConfigRepository = weChatPayConfigRepository; _merchantRepository = merchantRepository; _db = db; _authManager = authManager; _idGenerator = idGenerator; _fileService = fileService; _weChatPayService = weChatPayService; } /// /// 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) .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.RegistrationStatus, 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 }) .ToPagedListAsync(input); var resultItems = list.Rows.Select(x => { // 计算综合状态 int statusValue = 0; if (x.RegistrationStatus == 4) statusValue = 2; // 已取消 else if (x.RegistrationStatus == 1) statusValue = 1; // 已支付/待审核 else if (x.PaymentStatus == 0) statusValue = 0; // 待付款 else if (x.PaymentStatus == 1 && string.IsNullOrEmpty(x.MaterialUrl)) statusValue = 1; // 已支付 else if (x.PaymentStatus == 1 && !string.IsNullOrEmpty(x.MaterialUrl)) statusValue = 4; // 已完成/已报名 else if (x.PaymentStatus == 3) statusValue = 3; // 已退款 else if (x.PaymentStatus == 4) statusValue = 1; // 待审核 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, Status = statusValue, 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("报名记录不存在"); // 仅允许 status ∈ (待付款, 待上传素材) 可取消 // 待付款: PayStatus == 0 // 待上传素材: PayStatus == 1 && !HasMaterial // 已取消: RegStatus == 4 if (registration.RegistrationStatus == 4) throw Oops.Bah("订单已取消"); bool canCancel = false; if (registration.PaymentStatus == 0) canCancel = true; else if (registration.PaymentStatus == 1 && string.IsNullOrEmpty(registration.MaterialUrl)) canCancel = true; if (!canCancel) { // 已付款且已上传素材的报名需走退赛流程 throw Oops.Bah("当前状态无法直接取消,请联系管理员退赛"); } registration.RegistrationStatus = 4; // 已取消 registration.UpdatedUserId = userId; registration.UpdatedTime = DateTime.Now; // registration.CancelReason = "用户主动取消"; // 需扩展字段 await _registrationRepository.UpdateAsync(registration); // 记录日志 (暂不需要) /* await _eventLogRepository.InsertAsync(new EventLog { RegistrationId = registration.Id, LogType = EventLogType.Cancel, Description = "用户取消报名", OperatorId = userId, CreatedTime = DateTime.Now }); */ 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); // 假设 FileService 支持 IFormFile var item = uploadResult.FirstOrDefault(); // 假设返回 List 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); // 记录日志 (暂不需要) /* await _eventLogRepository.InsertAsync(new EventLog { RegistrationId = registration.Id, LogType = EventLogType.UploadMaterial, Description = $"用户上传素材: {result.Count}个文件", OperatorId = userId, CreatedTime = DateTime.Now }); */ } 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 = 4; // 假设 4 = 待审核 registration.UpdatedTime = DateTime.Now; // registration.VoucherUploadTime = DateTime.Now; await _registrationRepository.UpdateAsync(registration); // 记录日志 (暂不需要) /* await _eventLogRepository.InsertAsync(new EventLog { RegistrationId = registration.Id, LogType = EventLogType.UploadVoucher, Description = "用户上传付款凭证", OperatorId = userId, CreatedTime = DateTime.Now }); */ 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 ≠ 队长 (假设 PersonType=Leader 是队长) if (person.PersonType == PersonType.Leader) throw Oops.Bah("不能替换队长"); // 检查新队员 var newUser = await _userRepository.GetByIdAsync(input.NewUserId); if (newUser == null) throw Oops.Bah("新队员不存在"); // 检查新队员是否已报名该赛事 (简化检查: 是否在 Person 表中存在该 EventId 的记录) // var exists = await _registrationPersonRepository.AnyAsync(x => x.EventId == registration.EventId && x.IdCard == newUser.IdCardNumber); // if (exists) throw Oops.Bah("新队员已报名该赛事"); long oldUserId = person.Id; // 这里应该是 AppUserId,但 Person 表没存 AppUserId,只存了 Name/Phone // 更新信息 person.Name = newUser.Name; person.IdCard = newUser.IdCardNumber; person.Phone = newUser.Mobile; person.UpdatedTime = DateTime.Now; await _registrationPersonRepository.UpdateAsync(person); // 记录日志 (暂不需要) /* await _eventLogRepository.InsertAsync(new EventLog { RegistrationId = registration.Id, LogType = EventLogType.ReplaceMember, Description = $"更换队员: {oldUserId} -> {input.NewUserId}", OperatorId = currentUserId, CreatedTime = DateTime.Now }); */ return new ResponseDTO { Code = 200, Success = true, Data = true, Message = "更换成功" }; } /// /// 6. 查看证书 /// [DisplayName("查看证书")] [HttpGet("certificate/{registrationId}")] public async Task> GetCertificate([FromRoute] long registrationId) { // 2-已发放 var cert = await _certificateRepository.GetFirstAsync(x => x.RegistrationId == registrationId && x.CertificateStatus == 2); if (cert == null) return new ResponseDTO { Code = 404, Message = "证书未颁发", Success = false }; return new ResponseDTO { Code = 200, Success = true, Data = new CertificateDTO { CertUrl = cert.CertificateUrl + "?sas_token=mock_10min_token", // 模拟带签名 URL CertNumber = cert.CertificateNo, IssueTime = cert.IssuedTime ?? DateTime.MinValue, ExpireTime = null } }; } /// /// 7. 取消报名预览 /// [DisplayName("取消报名预览")] [HttpGet("cancel/preview/{registrationId}")] public async Task CancelPreview([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 _db.Queryable().InSingleAsync(registration.EventId); // 获取项目列表 var items = await _db.Queryable() .Where(x => x.RegistrationId == registrationId) .ToListAsync(); // 获取队员列表 var persons = await _registrationPersonRepository.GetListAsync(x => x.RegistrationId == registrationId); // 计算可退款金额(简化逻辑,实际需根据退款规则计算) var refundableAmount = registration.TotalAmount; var output = new CancelPreviewOutput { RegistrationId = registration.Id, EventName = eventInfo?.EventName ?? "", TeamName = registration.TeamName ?? "", ChargeMode = registration.TotalAmount > 0 ? 1 : 0, ActualPaidAmount = registration.TotalAmount, RefundableAmount = refundableAmount }; // 添加项目 foreach (var item in items) { output.Projects.Add(new CancelPreviewProjectDto { ProjectId = item.ProjectId, ProjectName = item.ProjectName ?? "", ProjectType = item.PersonCount > 1 ? "团队" : "个人", PersonCount = item.PersonCount, UnitPrice = item.UnitPrice }); } // 添加队员 foreach (var person in persons) { output.Members.Add(new CancelPreviewMemberDto { MemberId = person.Id, Name = person.Name ?? "", IsLeader = person.PersonType == PersonType.Leader }); } return output; } /// /// 8. 执行取消报名 /// [DisplayName("执行取消报名")] [HttpPost("cancel/execute")] [UnitOfWork] public async Task CancelExecute([FromBody] CancelExecuteInput input) { var userId = _authManager.UserId; var registration = await _registrationRepository.GetFirstAsync(x => x.Id == input.RegistrationId && x.CreatedUserId == userId); if (registration == null) throw Oops.Bah("报名记录不存在"); // 检查状态 if (registration.RegistrationStatus == 4) throw Oops.Bah("订单已取消"); if (registration.PaymentStatus != 1) throw Oops.Bah("未支付的订单无法退款"); var now = DateTime.Now; var output = new CancelExecuteOutput { Success = true, Message = "取消报名成功", CancelTime = now, RefundAmount = registration.TotalAmount }; // 更新报名状态 registration.RegistrationStatus = 4; // 已取消 registration.PaymentStatus = 3; // 已退款 registration.UpdatedUserId = userId; registration.UpdatedTime = now; registration.RegistrationRemark = input.CancelReason; await _registrationRepository.UpdateAsync(registration); // 如果有支付,执行退款 if (registration.TotalAmount > 0 && !string.IsNullOrEmpty(registration.OrderNo)) { // 查找商户配置 var merchant = await _merchantRepository.GetFirstAsync(x => x.CreatedUserId == userId); if (merchant != null) { var wechatConfig = await _weChatPayConfigRepository.GetFirstAsync(x => x.MerchantId == merchant.Id); if (wechatConfig != null) { // 生成退款单号 var outRefundNo = $"REF{DateTime.Now:yyyyMMddHHmmss}{new Random().Next(1000, 9999)}"; output.RefundNo = outRefundNo; // 调用微信退款服务 var refundRequest = new Core.Infrastructure.WeChat.Dto.CreateRefundRequest { OutTradeNo = registration.OrderNo, OutRefundNo = outRefundNo, RefundAmount = AmountHelper.YuanToFen(registration.TotalAmount), // 转分 TotalAmount = AmountHelper.YuanToFen(registration.TotalAmount), Reason = input.CancelReason }; var refundResult = await _weChatPayService.CreateRefundAsync(wechatConfig, refundRequest); if (!refundResult.Success) { output.Success = false; output.Message = refundResult.Message; output.RefundAmount = 0; output.RefundNo = null; } } } } return output; } }