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 Easy.Core; 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 ISqlSugarRepository _eventInfoRepository; 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, ISqlSugarRepository eventInfoRepository, 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; _eventInfoRepository = eventInfoRepository; } /// /// 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.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.PaymentStatus == 4) statusValue = 2; // 已取消 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 == 2) statusValue = 1; // 审核拒绝 else if (x.PaymentStatus == 3) statusValue = 3; // 已退款 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("报名记录不存在"); if (registration.PaymentStatus == 4) throw Oops.Bah("订单已取消"); if (registration.PaymentStatus == 0) throw Oops.Bah("订单未支付"); registration.PaymentStatus = 4; // 已取消 registration.UpdatedUserId = userId; registration.UpdatedTime = DateTime.Now; await _registrationRepository.UpdateAsync(registration); 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) { // 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.PaymentStatus == 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.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 eventInfo = await _eventInfoRepository.GetFirstAsync(e=>e.Id == registration.EventId); // 查找商户配置 var merchant = await _merchantRepository.GetFirstAsync(x => x.Id == eventInfo.MerchantId); 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; } }