diff --git a/YD_Event.Application/Client/OAuthController.cs b/YD_Event.Application/Client/OAuthController.cs index 292aa2a..573d39d 100644 --- a/YD_Event.Application/Client/OAuthController.cs +++ b/YD_Event.Application/Client/OAuthController.cs @@ -52,17 +52,20 @@ public class OAuthController : IDynamicApiController } var appUser = await _appUserRepository.GetFirstAsync(x => x.Mobile == input.Mobile); - if (appUser == null) - { - throw Oops.Bah("用户不存在,请先注册"); - } - - if (isPasswordLogin) + + // 统一的未注册账户判断逻辑 + bool isRegistered = appUser != null && !string.IsNullOrEmpty(appUser.Password); + + if (!isRegistered) { if (string.IsNullOrEmpty(appUser.Password)) { throw Oops.Bah("未注册过账户"); } + } + + if (isPasswordLogin) + { var encryptedPassword = MD5Encryption.Encrypt($"{_idGenerator.Encode(appUser.Id)}{input.Password}"); if (appUser.Password != encryptedPassword) { diff --git a/YD_Event.Application/Event/Dtos/UserCompetitionDtos.cs b/YD_Event.Application/Event/Dtos/UserCompetitionDtos.cs index e0c9c3a..d203f21 100644 --- a/YD_Event.Application/Event/Dtos/UserCompetitionDtos.cs +++ b/YD_Event.Application/Event/Dtos/UserCompetitionDtos.cs @@ -752,11 +752,6 @@ public class RegistrationItemDTO /// public string TeamName { get; set; } - /// - /// 支付状态:0-未支付/待审核,1-已支付/审核通过,2-支付失败/审核拒绝,3-已退款,4-已取消 - /// - public int Status { get; set; } - /// /// 创建时间 /// @@ -1177,6 +1172,128 @@ public class GroupTeamLimitOutput public int CurrentTeams { get; set; } } +/// +/// 检查组别选择限制输入 +/// +public class CheckGroupSelectionLimitInput +{ + /// + /// 赛事ID + /// + [Required] + public long EventId { get; set; } + + /// + /// 要检查的组别ID + /// + [Required] + public long GroupId { get; set; } + + /// + /// 当前已选择的组别ID列表 + /// + public List SelectedGroupIds { get; set; } +} + +/// +/// 检查组别选择限制输出 +/// +public class CheckGroupSelectionLimitOutput +{ + /// + /// 是否可以选择该组别 + /// + public bool CanSelect { get; set; } + + /// + /// 限制消息(如果不能选择) + /// + public string? LimitMessage { get; set; } + + /// + /// 组别限制详情 + /// + public GroupLimitDetail? GroupLimit { get; set; } + + /// + /// 项目限制详情 + /// + public ProjectLimitDetail? ProjectLimit { get; set; } + + /// + /// 团队项目限制详情 + /// + public EventLimitDetail? TeamProjectLimit { get; set; } + + /// + /// 个人项目限制详情 + /// + public EventLimitDetail? IndividualProjectLimit { get; set; } +} + +/// +/// 组别限制详情 +/// +public class GroupLimitDetail +{ + /// + /// 组别限报数 + /// + public int? GroupLimitCount { get; set; } + + /// + /// 当前报名数 + /// + public int CurrentRegistrations { get; set; } + + /// + /// 是否达到限制 + /// + public bool IsLimitReached { get; set; } +} + +/// +/// 项目限制详情 +/// +public class ProjectLimitDetail +{ + /// + /// 项目限报数 + /// + public int? ProjectLimitCount { get; set; } + + /// + /// 当前已选择数 + /// + public int CurrentSelectedCount { get; set; } + + /// + /// 是否达到限制 + /// + public bool IsLimitReached { get; set; } +} + +/// +/// 赛事项目限制详情 +/// +public class EventLimitDetail +{ + /// + /// 限报数 + /// + public int? LimitCount { get; set; } + + /// + /// 当前已选择数 + /// + public int CurrentSelectedCount { get; set; } + + /// + /// 是否达到限制 + /// + public bool IsLimitReached { get; set; } +} + /// /// 组别队员分配 /// diff --git a/YD_Event.Application/Event/EventArrangementService.cs b/YD_Event.Application/Event/EventArrangementService.cs index 1d37435..ce86e51 100644 --- a/YD_Event.Application/Event/EventArrangementService.cs +++ b/YD_Event.Application/Event/EventArrangementService.cs @@ -88,7 +88,9 @@ public class EventArrangementService : BaseService x.EventId == eventId); var projectIds = eventProjects.Select(x => x.ProjectId).Distinct().ToList(); var projects = await _projectRepository.GetListAsync(x => projectIds.Contains(x.Id)); - projects = projects.OrderBy(x => x.SortIndex).ToList(); + + // 按照 SortIndex 对 projects 排序 + var sortedProjects = projects.OrderBy(x => x.SortIndex).ToList(); var groups = await _groupRepository.GetListAsync(x => projectIds.Contains(x.ProjectId)); @@ -123,10 +125,11 @@ public class EventArrangementService : BaseService(); - foreach (var eventProj in eventProjects) + // 按照排序后的项目顺序处理 + foreach (var proj in sortedProjects) { - var proj = projects.FirstOrDefault(x => x.Id == eventProj.ProjectId); - if (proj == null) continue; + var eventProj = eventProjects.FirstOrDefault(x => x.ProjectId == proj.Id); + if (eventProj == null) continue; var dto = new ArrangementProjectListOutput { @@ -241,7 +244,9 @@ public class EventArrangementService : BaseService x.EventId == eventId); var projectIds = eventProjects.Select(x => x.ProjectId).Distinct().ToList(); var projects = await _projectRepository.GetListAsync(x => projectIds.Contains(x.Id)); - projects = projects.OrderBy(x => x.SortIndex).ToList(); + + // 按照 SortIndex 对 projects 排序 + var sortedProjects = projects.OrderBy(x => x.SortIndex).ToList(); var groups = await _groupRepository.GetListAsync(x => projectIds.Contains(x.ProjectId)); // 统计各组别报名人数 @@ -274,11 +279,11 @@ public class EventArrangementService : BaseService(); - // 2. 生成排期 - foreach (var eventProj in eventProjects) + // 2. 生成排期 - 按照排序后的项目顺序 + foreach (var proj in sortedProjects) { - var proj = projects.FirstOrDefault(x => x.Id == eventProj.ProjectId); - if (proj == null) continue; + var eventProj = eventProjects.FirstOrDefault(x => x.ProjectId == proj.Id); + if (eventProj == null) continue; int venueCount = proj.VenueCount > 0 ? proj.VenueCount : 1; diff --git a/YD_Event.Application/Event/EventScoreService.cs b/YD_Event.Application/Event/EventScoreService.cs index 225e274..17c2012 100644 --- a/YD_Event.Application/Event/EventScoreService.cs +++ b/YD_Event.Application/Event/EventScoreService.cs @@ -755,21 +755,11 @@ public class EventScoreService : BaseService, ITransient throw Oops.Bah("没有可发放积分的成绩记录"); } - var existingRecords = await _pointsRecordRepository.AsQueryable() - .Where(x => x.EventId == input.EventId) - .Select(x => x.Remark) + var processedResultIdsList = await _pointsRecordRepository.AsQueryable() + .Where(x => x.EventId == input.EventId && x.ResultId.HasValue) + .Select(x => x.ResultId.Value) .ToListAsync(); - - var processedResultIds = existingRecords - .Where(x => !string.IsNullOrEmpty(x) && x.StartsWith("ResultId:")) - .Select(x => - { - var parts = x.Split('|'); - var resultIdPart = parts.FirstOrDefault(p => p.StartsWith("ResultId:")); - return resultIdPart != null && long.TryParse(resultIdPart.Replace("ResultId:", ""), out var id) ? id : 0; - }) - .Where(x => x > 0) - .ToHashSet(); + var processedResultIds = new HashSet(processedResultIdsList); int successCount = 0; int skipCount = 0; @@ -805,8 +795,9 @@ public class EventScoreService : BaseService, ITransient Type = PointsOperationType.Acquire, EventId = input.EventId, ProjectId = result.ProjectIdForRecord, + ResultId = result.ResultId, Points = pointsToAward, - Remark = $"ResultId:{result.ResultId}|Rank:{result.Rank}", + Remark = eventInfo.EventName, Status = AvailabilityStatus.Enable, CreatedTime = DateTime.Now }; diff --git a/YD_Event.Application/Event/UnifiedRegistrationService.cs b/YD_Event.Application/Event/UnifiedRegistrationService.cs index 6a56f0b..1ebff60 100644 --- a/YD_Event.Application/Event/UnifiedRegistrationService.cs +++ b/YD_Event.Application/Event/UnifiedRegistrationService.cs @@ -1,6 +1,8 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using YD_Event.Application.Event.Dtos; +using YD_Event.Application.Order.Dtos; +using YD_Event.Application.Refund; using YD_Event.Core.Domain; using YD_Event.Core.Domain.Pricing; using YD_Event.Core.Domain.Pricing.Models; @@ -46,6 +48,8 @@ public class UnifiedRegistrationService : ITransient private readonly IOptionsMonitor _wechatTenpayClientOptionsMonitor; private readonly ILogger _logger; private readonly OrderNoGeneratorService _orderNoGeneratorService; + private readonly RefundConfigService _refundConfigService; + private readonly ISqlSugarRepository _refundRecordRepository; public UnifiedRegistrationService( ISqlSugarRepository eventRepository, @@ -75,7 +79,9 @@ public class UnifiedRegistrationService : ITransient WeChatPayService weChatPayService, IOptionsMonitor wechatTenpayClientOptionsMonitor, ILogger logger, - OrderNoGeneratorService orderNoGeneratorService) + OrderNoGeneratorService orderNoGeneratorService, + RefundConfigService refundConfigService, + ISqlSugarRepository refundRecordRepository) { _eventRepository = eventRepository; _unitRepository = unitRepository; @@ -105,6 +111,8 @@ public class UnifiedRegistrationService : ITransient _wechatTenpayClientOptionsMonitor = wechatTenpayClientOptionsMonitor; _logger = logger; _orderNoGeneratorService = orderNoGeneratorService; + _refundConfigService = refundConfigService; + _refundRecordRepository = refundRecordRepository; } /// @@ -1285,7 +1293,7 @@ public class UnifiedRegistrationService : ITransient _logger.LogDebug("检查成员报名限制,成员ID: {MemberId}, 姓名: {MemberName}", member.Id, member.Name); var (individualProjectCount, teamProjectCount) = CalculateMemberProjectCounts( - member.Id, eventProjects, projects, registrationPersons, allRegistrationItems); + member.Id, eventProjects, projects, registrationPersons, allRegistrationItems, input); _logger.LogDebug("成员 {MemberName} 个人项目报名数: {IndividualCount}, 团队项目报名数: {TeamCount}", member.Name, individualProjectCount, teamProjectCount); @@ -1296,14 +1304,15 @@ public class UnifiedRegistrationService : ITransient } /// - /// 计算成员项目报名数量 + /// 计算成员项目报名数量(包含本次提交的项目) /// private (int individualCount, int teamCount) CalculateMemberProjectCounts( long memberId, List eventProjects, List projects, List registrationPersons, - List allRegistrationItems) + List allRegistrationItems, + UnifiedRegistrationInput input) { var matchedPersons = registrationPersons.Where(x => x.UserId == memberId).ToList(); var personRegistrationIds = matchedPersons.Select(x => x.RegistrationId).Distinct().ToList(); @@ -1312,6 +1321,7 @@ public class UnifiedRegistrationService : ITransient int individualProjectCount = 0; int teamProjectCount = 0; + // 统计已存在的报名项目 foreach (var item in personRegistrationItems) { var eventProject = eventProjects.FirstOrDefault(ep => ep.Id == item.ProjectId); @@ -1330,6 +1340,43 @@ public class UnifiedRegistrationService : ITransient } } + // 统计本次提交的项目,确保不重复统计 + var existingProjectIds = new HashSet(personRegistrationItems.Select(x => x.ProjectId)); + + foreach (var item in input.Items) + { + // 检查该成员是否在本次提交的项目中 + var allPersonsInItem = GetAllPersonsFromItem(item); + if (!allPersonsInItem.Any(p => p.UserId.HasValue && p.UserId.Value == memberId)) + { + continue; + } + + // 检查是否已经统计过该项目 + if (existingProjectIds.Contains(item.ProjectId)) + { + continue; + } + + var eventProject = eventProjects.FirstOrDefault(ep => ep.Id == item.ProjectId); + if (eventProject == null) continue; + + var baseProject = projects.FirstOrDefault(p => p.Id == eventProject.ProjectId); + if (baseProject == null) continue; + + if (baseProject.IsIndividualProject()) + { + individualProjectCount++; + } + else if (baseProject.IsTeamProject()) + { + teamProjectCount++; + } + + // 标记该项目已统计 + existingProjectIds.Add(item.ProjectId); + } + return (individualProjectCount, teamProjectCount); } @@ -1342,7 +1389,7 @@ public class UnifiedRegistrationService : ITransient YD_Event.Core.Entities.AppUser member, int individualProjectCount) { - if (individualProjectCount < limit.IndividualProjectLimit) + if (individualProjectCount <= limit.IndividualProjectLimit) { return; } @@ -1367,7 +1414,7 @@ public class UnifiedRegistrationService : ITransient YD_Event.Core.Entities.AppUser member, int teamProjectCount) { - if (teamProjectCount < limit.TeamProjectLimit) + if (teamProjectCount <= limit.TeamProjectLimit) { return; } @@ -2108,4 +2155,434 @@ public class UnifiedRegistrationService : ITransient result.Details.Add(CreateValidationDetail("单位认证验证", true, "用户单位认证验证通过")); return result; } + + #region 统一退款相关方法 + + /// + /// 计算退款金额(统一方法) + /// + /// 总金额 + /// 赛事开始时间 + /// 应退金额 + [NonAction] + public async Task CalculateRefundAmountAsync(decimal totalAmount, DateTime eventStartTime) + { + var refundAmount = await _refundConfigService.CalculateRefundAmount(totalAmount, eventStartTime); + _logger.LogInformation("统一退款金额计算:总金额={TotalAmount}, 赛事开始时间={EventStartTime}, 计算后退款金额={RefundAmount}", + totalAmount, eventStartTime, refundAmount); + return refundAmount; + } + + /// + /// 统一取消报名预览方法 + /// + /// 报名ID + /// 操作人用户ID + /// 取消预览输出 + [NonAction] + public async Task GetCancelPreviewAsync(long registrationId, long operatorUserId) + { + var registration = await _registrationRepository.GetFirstAsync(x => x.Id == registrationId && x.CreatedUserId == operatorUserId); + 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 = await CalculateRefundAmountAsync(registration.TotalAmount, eventInfo.MatchStartTime); + + _logger.LogInformation("统一取消报名预览:报名ID={RegistrationId}, 赛事名称={EventName}, 实际支付={ActualAmount}, 可退款={RefundAmount}", + registrationId, eventInfo?.EventName, registration.TotalAmount, refundableAmount); + + 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.ProjectTypeName.Contains("团队")? "团队" : (item.ProjectTypeName.Contains("个人") ? "个人" : string.Empty), + 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; + } + + /// + /// 统一取消报名执行方法 + /// + /// 取消执行输入 + /// 操作人用户ID + /// 取消执行输出 + [UnitOfWork] + [NonAction] + public async Task ExecuteCancelRegistrationAsync(CancelExecuteInput input, long operatorUserId) + { + var registration = await _registrationRepository.GetFirstAsync(x => x.Id == input.RegistrationId && x.CreatedUserId == operatorUserId); + 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 eventInfo = await _eventRepository.GetFirstAsync(e => e.Id == registration.EventId); + + var baseRefundAmount = await CalculateRefundAmountAsync(registration.TotalAmount, eventInfo.MatchStartTime); + _logger.LogInformation("统一退款金额计算:报名ID={RegistrationId}, 总金额={TotalAmount}, 按规则计算后金额={BaseRefundAmount}", + registration.Id, registration.TotalAmount, baseRefundAmount); + + decimal actualRefundAmount = baseRefundAmount; + bool isPartialCancel = input.CancelType == 1 && input.PersonIds != null && input.PersonIds.Any(); + + if (isPartialCancel) + { + var persons = await _registrationPersonRepository.GetListAsync(x => x.RegistrationId == registration.Id); + var totalPersons = persons.Count; + if (totalPersons == 0) + { + throw Oops.Bah("报名记录中没有队员信息"); + } + + var selectedPersons = persons.Where(p => input.PersonIds.Contains(p.Id)).ToList(); + if (selectedPersons.Count != input.PersonIds.Count) + { + var invalidIds = input.PersonIds.Except(selectedPersons.Select(p => p.Id)).ToList(); + throw Oops.Bah($"选择的人员ID无效:{string.Join(", ", invalidIds)}"); + } + + var perPersonRefund = baseRefundAmount / totalPersons; + actualRefundAmount = perPersonRefund * selectedPersons.Count; + + _logger.LogInformation("统一部分退款:报名ID={RegistrationId}, 总人数={TotalPersons}, 选择人数={SelectedCount}, 单人退款金额={PerPersonRefund}, 实际退款金额={ActualRefundAmount}", + registration.Id, totalPersons, selectedPersons.Count, perPersonRefund, actualRefundAmount); + + await _registrationPersonRepository.DeleteAsync(x => input.PersonIds.Contains(x.Id)); + _logger.LogInformation("已删除队员:报名ID={RegistrationId}, 队员ID列表={PersonIds}", + registration.Id, string.Join(", ", input.PersonIds)); + + var remainingPersonsCount = await _registrationPersonRepository.CountAsync(x => x.RegistrationId == registration.Id); + registration.TotalPersonCount = remainingPersonsCount; + + if (remainingPersonsCount > 0) + { + registration.UpdatedUserId = operatorUserId; + registration.UpdatedTime = now; + registration.RegistrationRemark = input.CancelReason; + await _registrationRepository.UpdateAsync(registration); + _logger.LogInformation("统一部分退款完成,仍有剩余人员:报名ID={RegistrationId}, 剩余人数={RemainingCount}", + registration.Id, remainingPersonsCount); + } + else + { + registration.PaymentStatus = 3; + registration.UpdatedUserId = operatorUserId; + registration.UpdatedTime = now; + registration.RegistrationRemark = input.CancelReason; + await _registrationRepository.UpdateAsync(registration); + _logger.LogInformation("统一所有人员已退款,更新报名状态为已退款:报名ID={RegistrationId}", registration.Id); + } + } + else + { + _logger.LogInformation("统一全额退款:报名ID={RegistrationId}, 退款金额={ActualRefundAmount}", + registration.Id, actualRefundAmount); + + registration.PaymentStatus = 3; + registration.UpdatedUserId = operatorUserId; + registration.UpdatedTime = now; + registration.RegistrationRemark = input.CancelReason; + await _registrationRepository.UpdateAsync(registration); + } + + var output = new CancelExecuteOutput + { + Success = true, + Message = isPartialCancel ? "部分取消报名成功" : "取消报名成功", + CancelTime = now, + RefundAmount = actualRefundAmount + }; + + if (registration.TotalAmount > 0 && !string.IsNullOrEmpty(registration.OrderNo) && actualRefundAmount > 0) + { + 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 = await _orderNoGeneratorService.GenerateRefundOrderNoAsync(); + output.RefundNo = outRefundNo; + + var refundRequest = new Core.Infrastructure.WeChat.Dto.CreateRefundRequest + { + OutTradeNo = registration.OrderNo, + OutRefundNo = outRefundNo, + RefundAmount = AmountHelper.YuanToFen(actualRefundAmount), + TotalAmount = AmountHelper.YuanToFen(registration.TotalAmount), + Reason = input.CancelReason + }; + + _logger.LogInformation("开始执行统一退款:报名ID={RegistrationId}, 订单号={OrderNo}, 退款单号={RefundNo}, 退款金额={Amount}, 退款类型={CancelType}", + registration.Id, registration.OrderNo, outRefundNo, actualRefundAmount, isPartialCancel ? "部分退款" : "全额退款"); + + var refundResult = await _weChatPayService.CreateRefundAsync(wechatConfig, refundRequest); + if (!refundResult.Success) + { + _logger.LogError("统一退款失败:报名ID={RegistrationId}, 错误信息={ErrorMessage}", + registration.Id, refundResult.Message); + output.Success = false; + output.Message = refundResult.Message; + output.RefundAmount = 0; + output.RefundNo = null; + return output; + } + + await CreateRefundRecordAsync(registration, eventInfo, wechatConfig, outRefundNo, + refundResult.RefundId, actualRefundAmount, registration.TotalAmount, input.CancelReason, operatorUserId); + + _logger.LogInformation("统一退款成功:报名ID={RegistrationId}, 退款单号={RefundNo}, 退款金额={Amount}", + registration.Id, outRefundNo, actualRefundAmount); + } + } + } + + if (!isPartialCancel || registration.PaymentStatus == 3) + { + await UpdateOrderStatusAsync(registration.OrderNo, OrderStatus.Refunded, operatorUserId); + } + + _logger.LogInformation("统一取消报名完成:报名ID={RegistrationId}, 操作人={UserId}, 退款金额={RefundAmount}, 成功={Success}", + registration.Id, operatorUserId, actualRefundAmount, output.Success); + + return output; + } + + /// + /// 创建退款记录 + /// + private async Task CreateRefundRecordAsync(EventRegistration registration, EventInfo eventInfo, + WeChatPayConfig wechatConfig, string outRefundNo, string? refundId, + decimal refundAmount, decimal totalAmount, string? reason, long operatorUserId) + { + var refundRecord = new RefundRecord + { + Id = _idGenerator.NewLong(), + OutRefundNo = outRefundNo, + RefundId = refundId, + RegistrationId = registration.Id, + OutTradeNo = registration.OrderNo, + TransactionId = registration.PaymentProofUrl, + RefundAmount = refundAmount, + TotalAmount = totalAmount, + RefundReason = reason, + RefundStatus = 1, + ApplyTime = DateTime.Now, + SuccessTime = DateTime.Now, + MerchantId = eventInfo.MerchantId, + WeChatPayConfigId = wechatConfig.Id, + Status = AvailabilityStatus.Enable, + CreatedUserId = operatorUserId, + CreatedTime = DateTime.Now + }; + + await _refundRecordRepository.InsertAsync(refundRecord); + _logger.LogInformation("退款记录创建成功:退款记录ID={RefundRecordId}, 退款单号={OutRefundNo}", + refundRecord.Id, outRefundNo); + } + + /// + /// 更新订单状态 + /// + private async Task UpdateOrderStatusAsync(string orderNo, OrderStatus orderStatus, long operatorUserId) + { + if (!string.IsNullOrEmpty(orderNo)) + { + var order = await _orderRepository.GetFirstAsync(o => o.OrderNo == orderNo); + if (order != null) + { + order.OrderStatus = orderStatus; + order.UpdatedTime = DateTime.Now; + order.UpdatedUserId = operatorUserId; + await _orderRepository.UpdateAsync(order); + _logger.LogInformation("订单状态已同步更新:订单号={OrderNo}, 状态={OrderStatus}", + orderNo, orderStatus); + } + else + { + _logger.LogWarning("未找到对应的订单记录:订单号={OrderNo}", orderNo); + } + } + } + + /// + /// 统一后台管理退款方法 + /// + /// 退款订单输入 + /// 操作人用户ID + [UnitOfWork] + [NonAction] + public async Task ExecuteAdminRefundAsync(RefundOrderInput input, long operatorUserId) + { + var order = await _orderRepository.GetByIdAsync(input.Id); + if (order == null) throw Oops.Bah("订单不存在"); + + if (order.OrderStatus != OrderStatus.Paid) throw Oops.Bah("订单未支付,无法退款"); + + if (!AmountHelper.IsValidPositiveAmount(input.RefundAmount)) + { + throw Oops.Bah("退款金额必须大于0且最多保留两位小数"); + } + + if (input.RefundAmount > order.ActualAmount) + { + _logger.LogWarning("统一后台超额退款尝试:订单ID={OrderId}, 实付金额={ActualAmount}, 申请退款金额={RefundAmount}", + order.Id, order.ActualAmount, input.RefundAmount); + throw Oops.Bah($"退款金额({input.RefundAmount})不能超过实付金额({order.ActualAmount})"); + } + + var currentUserId = operatorUserId; + var now = DateTime.Now; + bool isPartialCancel = input.CancelType == 1 && input.PersonIds != null && input.PersonIds.Any(); + + if (isPartialCancel) + { + var persons = await _registrationPersonRepository.GetListAsync(x => x.RegistrationId == order.RegistrationId); + var totalPersons = persons.Count; + if (totalPersons == 0) + { + throw Oops.Bah("报名记录中没有队员信息"); + } + + var selectedPersons = persons.Where(p => input.PersonIds.Contains(p.Id)).ToList(); + if (selectedPersons.Count != input.PersonIds.Count) + { + var invalidIds = input.PersonIds.Except(selectedPersons.Select(p => p.Id)).ToList(); + throw Oops.Bah($"选择的人员ID无效:{string.Join(", ", invalidIds)}"); + } + + _logger.LogInformation("统一后台部分退款:订单ID={OrderId}, 总人数={TotalPersons}, 选择人数={SelectedCount}, 退款金额={RefundAmount}", + order.Id, totalPersons, selectedPersons.Count, input.RefundAmount); + + await _registrationPersonRepository.DeleteAsync(x => input.PersonIds.Contains(x.Id)); + _logger.LogInformation("已删除队员:订单ID={OrderId}, 队员ID列表={PersonIds}", + order.Id, string.Join(", ", input.PersonIds)); + + var remainingPersonsCount = await _registrationPersonRepository.CountAsync(x => x.RegistrationId == order.RegistrationId); + var reg = await _registrationRepository.GetByIdAsync(order.RegistrationId); + if (reg != null) + { + reg.TotalPersonCount = remainingPersonsCount; + reg.UpdatedTime = now; + reg.UpdatedUserId = currentUserId; + await _registrationRepository.UpdateAsync(reg); + } + + if (remainingPersonsCount > 0) + { + var refundOrder = new YD_Event.Core.Entities.Order + { + Id = _idGenerator.NewLong(), + OrderNo = await _orderNoGeneratorService.GenerateRefundOrderNoAsync(), + OrderType = 2, + MerchantId = order.MerchantId, + MerchantCertificationId = order.MerchantCertificationId, + EventId = order.EventId, + RegistrationId = order.RegistrationId, + RelatedOrderId = order.Id, + OriginalAmount = order.ActualAmount, + ActualAmount = input.RefundAmount, + OrderStatus = OrderStatus.Refunded, + PaymentTime = now, + Remark = input.Reason, + CreatedTime = now, + CreatedUserId = currentUserId + }; + await _orderRepository.InsertAsync(refundOrder); + + _logger.LogInformation("统一后台部分退款成功:订单ID={OrderId}, 退款订单号={RefundOrderNo}, 退款金额={RefundAmount}, 剩余人数={RemainingCount}, 操作人={UserId}", + order.Id, refundOrder.OrderNo, input.RefundAmount, remainingPersonsCount, currentUserId); + return; + } + } + + if (!isPartialCancel) + { + var existingRefund = await _orderRepository.IsAnyAsync(o => o.RelatedOrderId == order.Id && o.OrderType == 2); + if (existingRefund) + { + throw Oops.Bah("该订单已存在退款记录"); + } + } + + var fullRefundOrder = new YD_Event.Core.Entities.Order + { + Id = _idGenerator.NewLong(), + OrderNo = await _orderNoGeneratorService.GenerateRefundOrderNoAsync(), + OrderType = 2, + MerchantId = order.MerchantId, + MerchantCertificationId = order.MerchantCertificationId, + EventId = order.EventId, + RegistrationId = order.RegistrationId, + RelatedOrderId = order.Id, + OriginalAmount = order.ActualAmount, + ActualAmount = input.RefundAmount, + OrderStatus = OrderStatus.Refunded, + PaymentTime = now, + Remark = input.Reason, + CreatedTime = now, + CreatedUserId = currentUserId + }; + await _orderRepository.InsertAsync(fullRefundOrder); + + _logger.LogInformation("统一后台退款成功:订单ID={OrderId}, 退款订单号={RefundOrderNo}, 退款金额={RefundAmount}, 退款类型={RefundType}, 操作人={UserId}", + order.Id, fullRefundOrder.OrderNo, input.RefundAmount, isPartialCancel ? "部分退款" : "全额退款", currentUserId); + + order.OrderStatus = OrderStatus.Refunded; + order.UpdatedTime = now; + order.UpdatedUserId = currentUserId; + await _orderRepository.UpdateAsync(order); + + var registration = await _registrationRepository.GetByIdAsync(order.RegistrationId); + if (registration != null) + { + registration.PaymentStatus = 3; + registration.UpdatedTime = now; + registration.UpdatedUserId = currentUserId; + await _registrationRepository.UpdateAsync(registration); + } + + var payment = await _paymentRepository.GetFirstAsync(p => p.RegistrationId == order.RegistrationId); + if (payment != null) + { + payment.RefundStatus = 1; + payment.UpdatedTime = now; + payment.UpdatedUserId = currentUserId; + await _paymentRepository.UpdateAsync(payment); + } + } + + #endregion } diff --git a/YD_Event.Application/Event/UserEventRecordService.cs b/YD_Event.Application/Event/UserEventRecordService.cs index 4e62ec9..f13cd89 100644 --- a/YD_Event.Application/Event/UserEventRecordService.cs +++ b/YD_Event.Application/Event/UserEventRecordService.cs @@ -35,17 +35,14 @@ public class UserEventRecordService : IDynamicApiController, ITransient private readonly ISqlSugarRepository _projectRepository; private readonly ISqlSugarRepository _userRepository; private readonly ISqlSugarRepository _certificateRepository; - private readonly ISqlSugarRepository _weChatPayConfigRepository; - private readonly ISqlSugarRepository _merchantRepository; - private readonly ISqlSugarRepository _eventInfoRepository; 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 WeChatPayService _weChatPayService; - private readonly OrderNoGeneratorService _orderNoGeneratorService; private readonly ILogger _logger; + private readonly UnifiedRegistrationService _unifiedRegistrationService; public UserEventRecordService( ISqlSugarRepository registrationRepository, @@ -54,17 +51,14 @@ public class UserEventRecordService : IDynamicApiController, ITransient ISqlSugarRepository projectRepository, ISqlSugarRepository userRepository, ISqlSugarRepository certificateRepository, - ISqlSugarRepository weChatPayConfigRepository, - ISqlSugarRepository merchantRepository, - ISqlSugarRepository eventInfoRepository, ISqlSugarRepository orderRepository, + ISqlSugarRepository eventRepository, ISqlSugarClient db, AuthManager authManager, IIdGenerator idGenerator, FileService fileService, - WeChatPayService weChatPayService, - OrderNoGeneratorService orderNoGeneratorService, - ILogger logger) + ILogger logger, + UnifiedRegistrationService unifiedRegistrationService) { _registrationRepository = registrationRepository; _registrationPersonRepository = registrationPersonRepository; @@ -72,17 +66,14 @@ public class UserEventRecordService : IDynamicApiController, ITransient _projectRepository = projectRepository; _userRepository = userRepository; _certificateRepository = certificateRepository; - _weChatPayConfigRepository = weChatPayConfigRepository; - _merchantRepository = merchantRepository; + _orderRepository = orderRepository; + _eventRepository = eventRepository; _db = db; _authManager = authManager; _idGenerator = idGenerator; _fileService = fileService; - _weChatPayService = weChatPayService; - _eventInfoRepository = eventInfoRepository; - _orderRepository = orderRepository; - _orderNoGeneratorService = orderNoGeneratorService; _logger = logger; + _unifiedRegistrationService = unifiedRegistrationService; } /// @@ -98,6 +89,12 @@ public class UserEventRecordService : IDynamicApiController, ITransient .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) @@ -107,7 +104,8 @@ public class UserEventRecordService : IDynamicApiController, ITransient r.CreatedTime, r.UpdatedTime, r.TotalPersonCount, e.MaterialUploadEndTime, e.CoverImage, e.EventType, e.EventStatus, e.RegistrationStartTime, e.RegistrationEndTime, e.MatchStartTime, e.MatchEndTime, - fc.ChargeMode + fc.ChargeMode, + r.PaymentMethod }) .ToPagedListAsync(input); @@ -126,7 +124,6 @@ public class UserEventRecordService : IDynamicApiController, ITransient MatchStartTime = x.MatchStartTime, MatchEndTime = x.MatchEndTime, TeamName = x.TeamName, - Status = x.PaymentStatus, CreateTime = x.CreatedTime, UpdateTime = x.UpdatedTime, PayStatus = x.PaymentStatus, @@ -375,58 +372,8 @@ public class UserEventRecordService : IDynamicApiController, ITransient 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; + _logger.LogInformation("调用统一取消报名预览:报名ID={RegistrationId}, 操作人={UserId}", registrationId, userId); + return await _unifiedRegistrationService.GetCancelPreviewAsync(registrationId, userId); } /// @@ -438,81 +385,8 @@ public class UserEventRecordService : IDynamicApiController, ITransient 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 - }; - - // 如果有支付,先执行退款,成功后再更新状态 - 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 = await _orderNoGeneratorService.GenerateRefundOrderNoAsync(); - 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 - }; - - _logger.LogInformation("开始执行退款:报名ID={RegistrationId}, 订单号={OrderNo}, 退款单号={RefundNo}, 退款金额={Amount}", - registration.Id, registration.OrderNo, outRefundNo, registration.TotalAmount); - - var refundResult = await _weChatPayService.CreateRefundAsync(wechatConfig, refundRequest); - if (!refundResult.Success) - { - _logger.LogError("退款失败:报名ID={RegistrationId}, 错误信息={ErrorMessage}", - registration.Id, refundResult.Message); - output.Success = false; - output.Message = refundResult.Message; - output.RefundAmount = 0; - output.RefundNo = null; - return output; - } - - _logger.LogInformation("退款成功:报名ID={RegistrationId}, 退款单号={RefundNo}", - registration.Id, outRefundNo); - } - } - } - - // 退款成功后才更新报名状态 - registration.PaymentStatus = 3; // 已退款 - registration.UpdatedUserId = userId; - registration.UpdatedTime = now; - registration.RegistrationRemark = input.CancelReason; - await _registrationRepository.UpdateAsync(registration); - - _logger.LogInformation("取消报名成功:报名ID={RegistrationId}, 操作人={UserId}", - registration.Id, userId); - - return output; + _logger.LogInformation("调用统一执行取消报名:报名ID={RegistrationId}, 操作人={UserId}", input.RegistrationId, userId); + return await _unifiedRegistrationService.ExecuteCancelRegistrationAsync(input, userId); } /// @@ -527,7 +401,7 @@ public class UserEventRecordService : IDynamicApiController, ITransient if (registration == null) throw Oops.Bah("报名记录不存在"); // 获取赛事信息 - var eventInfo = await _eventInfoRepository.GetByIdAsync(registration.EventId); + var eventInfo = await _eventRepository.GetByIdAsync(registration.EventId); // 获取报名项目 var items = await _registrationItemRepository.GetListAsync(x => x.RegistrationId == registrationId); diff --git a/YD_Event.Application/Event/UserEventRegistrationService.cs b/YD_Event.Application/Event/UserEventRegistrationService.cs index ba226c6..856447b 100644 --- a/YD_Event.Application/Event/UserEventRegistrationService.cs +++ b/YD_Event.Application/Event/UserEventRegistrationService.cs @@ -1128,6 +1128,10 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient continue; } + // 判断是否为团队项目 + var isTeamProject = project.IsTeamProject(); + + // 计算分配的人数(去重) var totalAssignedMembers = input.GroupMembers .Where(g => assignedProjectGroups.Select(pg => pg.Id).Contains(g.GroupId)) .SelectMany(g => g.MemberIds) @@ -1144,18 +1148,23 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient ProjectId = eventProject.Id, ProjectName = project.Name ?? "未知项目", ErrorType = "Insufficient", - ErrorMessage = $"项目\"{project.Name}\"要求完成人数为{project.CompletionCount}人,当前只分配了{totalAssignedMembers}人" + ErrorMessage = isTeamProject + ? $"项目\"{project.Name}\"要求完成人数为{project.CompletionCount}人,当前只分配了{totalAssignedMembers}人" + : $"项目\"{project.Name}\"要求完成人数为{project.CompletionCount}人,当前只分配了{totalAssignedMembers}人" }); } - else if (totalAssignedMembers > project.CompletionCount) + else if (totalAssignedMembers >= project.CompletionCount) { + // 只要达到或超过完成人数就提示超限(特别是个人项目) result.HasError = true; result.Errors.Add(new ProjectCompletionError { ProjectId = eventProject.Id, ProjectName = project.Name ?? "未知项目", ErrorType = "Excessive", - ErrorMessage = $"项目\"{project.Name}\"要求完成人数为{project.CompletionCount}人,当前分配了{totalAssignedMembers}人,人数超额" + ErrorMessage = isTeamProject + ? $"项目\"{project.Name}\"要求完成人数为{project.CompletionCount}人,当前分配了{totalAssignedMembers}人,人数超额" + : $"项目\"{project.Name}\"要求完成人数为{project.CompletionCount}人,当前分配了{totalAssignedMembers}人,人数超额" }); } } @@ -1258,4 +1267,140 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient }; } + /// + /// 检查组别选择限制 + /// + [DisplayName("检查组别选择限制")] + [HttpPost("check-group-selection-limit")] + public async Task CheckGroupSelectionLimit([FromBody] CheckGroupSelectionLimitInput input) + { + var result = new CheckGroupSelectionLimitOutput + { + CanSelect = true + }; + + // 获取组别信息 + var group = await _eventProjectGroupRepository.GetByIdAsync(input.GroupId); + if (group == null) + { + result.CanSelect = false; + result.LimitMessage = "组别不存在"; + return result; + } + + // 获取项目信息 + var eventProject = await _eventProjectRepository.GetByIdAsync(group.EventProjectId); + if (eventProject == null) + { + result.CanSelect = false; + result.LimitMessage = "项目不存在"; + return result; + } + + // 获取基础项目信息 + var baseProject = await _projectRepository.GetByIdAsync(eventProject.ProjectId); + + // 获取赛事信息获取报名限制 + var eventInfo = await _eventRepository.GetByIdAsync(input.EventId); + var registrationLimit = eventInfo != null + ? await _limitRepository.GetFirstAsync(x => x.EventId == input.EventId && !x.DeleteMark) + : null; + + // 1. 检查组别自身限制(这里暂不实现,因为 EventProjectGroup 实体没有 GroupLimitCount 字段) + // 后续可以根据实际需求添加相关的限制配置表 + + // 2. 检查项目限制 + var selectedGroupIds = input.SelectedGroupIds ?? new List(); + var otherSelectedGroups = await _eventProjectGroupRepository.GetListAsync(x => selectedGroupIds.Contains(x.Id)); + var projectGroups = otherSelectedGroups.Where(g => g.EventProjectId == eventProject.Id).ToList(); + + // 计算该项目已选的组别数量(排除当前组别) + var selectedCountForProject = projectGroups.Count; + + // 获取该项目的限报配置 + var projectLimit = await _projectLimitRepository.GetFirstAsync(x => + x.EventId == input.EventId && x.ProjectId == eventProject.ProjectId && !x.DeleteMark); + + if (projectLimit != null) + { + result.ProjectLimit = new ProjectLimitDetail + { + ProjectLimitCount = projectLimit.LimitCount, + CurrentSelectedCount = selectedCountForProject, + IsLimitReached = selectedCountForProject >= projectLimit.LimitCount + }; + + if (result.ProjectLimit.IsLimitReached && !selectedGroupIds.Contains(input.GroupId)) + { + result.CanSelect = false; + result.LimitMessage = $"该项目最多可报名{projectLimit.LimitCount}个组别"; + return result; + } + } + + // 3. 检查团队/个人项目限制 + if (registrationLimit != null && baseProject != null) + { + var isTeamProject = baseProject.IsTeamProject(); + var isIndividualProject = baseProject.IsIndividualProject(); + + // 计算已选的团队项目和个人项目数量 + var allSelectedEventProjectIds = otherSelectedGroups + .Where(g => g.Id != input.GroupId) + .Select(g => g.EventProjectId) + .Distinct() + .ToList(); + + var allSelectedEventProjects = await _eventProjectRepository.GetListAsync(x => allSelectedEventProjectIds.Contains(x.Id)); + var allSelectedProjectIds = allSelectedEventProjects.Select(ep => ep.ProjectId).Distinct().ToList(); + var allSelectedProjects = await _projectRepository.GetListAsync(x => allSelectedProjectIds.Contains(x.Id)); + + var selectedTeamCount = 0; + var selectedIndividualCount = 0; + + foreach (var selectedProject in allSelectedProjects) + { + if (selectedProject.IsTeamProject()) selectedTeamCount++; + if (selectedProject.IsIndividualProject()) selectedIndividualCount++; + } + + // 团队项目限制 + if (isTeamProject) + { + result.TeamProjectLimit = new EventLimitDetail + { + LimitCount = registrationLimit.TeamProjectLimit, + CurrentSelectedCount = selectedTeamCount, + IsLimitReached = selectedTeamCount >= registrationLimit.TeamProjectLimit + }; + + if (result.TeamProjectLimit.IsLimitReached && !selectedGroupIds.Contains(input.GroupId)) + { + result.CanSelect = false; + result.LimitMessage = $"团队项目最多可报名{registrationLimit.TeamProjectLimit}个"; + return result; + } + } + + // 个人项目限制 + if (isIndividualProject) + { + result.IndividualProjectLimit = new EventLimitDetail + { + LimitCount = registrationLimit.IndividualProjectLimit, + CurrentSelectedCount = selectedIndividualCount, + IsLimitReached = selectedIndividualCount >= registrationLimit.IndividualProjectLimit + }; + + if (result.IndividualProjectLimit.IsLimitReached && !selectedGroupIds.Contains(input.GroupId)) + { + result.CanSelect = false; + result.LimitMessage = $"个人项目最多可报名{registrationLimit.IndividualProjectLimit}个"; + return result; + } + } + } + + return result; + } } diff --git a/YD_Event.Application/Order/Dtos/OrderListOutput.cs b/YD_Event.Application/Order/Dtos/OrderListOutput.cs index bd3cac8..3c41889 100644 --- a/YD_Event.Application/Order/Dtos/OrderListOutput.cs +++ b/YD_Event.Application/Order/Dtos/OrderListOutput.cs @@ -311,4 +311,14 @@ public class RefundOrderInput /// 退款原因 /// public string Reason { get; set; } + + /// + /// 取消类型(0-全部取消,1-部分取消) + /// + public int CancelType { get; set; } + + /// + /// 要取消的队员ID列表(部分取消时使用) + /// + public List? PersonIds { get; set; } } diff --git a/YD_Event.Application/Order/OrderService.cs b/YD_Event.Application/Order/OrderService.cs index 570eb1b..5108507 100644 --- a/YD_Event.Application/Order/OrderService.cs +++ b/YD_Event.Application/Order/OrderService.cs @@ -3,10 +3,10 @@ using OfficeOpenXml; using OfficeOpenXml.Style; using System.Drawing; using YD_Event.Application.Auth; +using YD_Event.Application.Event; using YD_Event.Application.Order.Dtos; using YD_Event.Core.Entities; using YD_Event.Core.Enum; -using YD_Event.Application.Refund; using Microsoft.Extensions.Logging; using YD_Event.Core.Domain; @@ -24,12 +24,12 @@ public class OrderService : BaseService, ITransien private readonly ISqlSugarRepository _paymentRepository; private readonly ISqlSugarRepository _eventRepository; private readonly ISqlSugarRepository _merchantRepository; - private readonly RefundConfigService _refundConfigService; private readonly IIdGenerator _idGenerator; private readonly AuthManager _authManager; private readonly ISqlSugarClient _db; private readonly OrderNoGeneratorService _orderNoGeneratorService; private readonly ILogger _logger; + private readonly UnifiedRegistrationService _unifiedRegistrationService; public OrderService( ISqlSugarRepository orderRepository, @@ -39,12 +39,12 @@ public class OrderService : BaseService, ITransien ISqlSugarRepository paymentRepository, ISqlSugarRepository eventRepository, ISqlSugarRepository merchantRepository, - RefundConfigService refundConfigService, IIdGenerator idGenerator, AuthManager authManager, ISqlSugarClient db, OrderNoGeneratorService orderNoGeneratorService, - ILogger logger) : base(orderRepository) + ILogger logger, + UnifiedRegistrationService unifiedRegistrationService) : base(orderRepository) { _orderRepository = orderRepository; _registrationRepository = registrationRepository; @@ -53,12 +53,12 @@ public class OrderService : BaseService, ITransien _paymentRepository = paymentRepository; _eventRepository = eventRepository; _merchantRepository = merchantRepository; - _refundConfigService = refundConfigService; _idGenerator = idGenerator; _authManager = authManager; _db = db; _orderNoGeneratorService = orderNoGeneratorService; _logger = logger; + _unifiedRegistrationService = unifiedRegistrationService; } /// @@ -432,7 +432,9 @@ public class OrderService : BaseService, ITransien var eventInfo = await _eventRepository.GetByIdAsync(order.EventId); if (eventInfo == null) return order.ActualAmount; - return await _refundConfigService.CalculateRefundAmount(order.ActualAmount, eventInfo.MatchStartTime); + _logger.LogInformation("调用统一退款金额计算:订单ID={OrderId}, 总金额={TotalAmount}, 赛事开始时间={EventStartTime}", + orderId, order.ActualAmount, eventInfo.MatchStartTime); + return await _unifiedRegistrationService.CalculateRefundAmountAsync(order.ActualAmount, eventInfo.MatchStartTime); } /// @@ -443,83 +445,9 @@ public class OrderService : BaseService, ITransien [UnitOfWork] public async Task Refund(RefundOrderInput input) { - var order = await _orderRepository.GetByIdAsync(input.Id); - if (order == null) throw Oops.Bah("订单不存在"); - - if (order.OrderStatus != OrderStatus.Paid) throw Oops.Bah("订单未支付,无法退款"); - - // 验证退款金额 - if (!AmountHelper.IsValidPositiveAmount(input.RefundAmount)) - { - throw Oops.Bah("退款金额必须大于0且最多保留两位小数"); - } - - if (input.RefundAmount > order.ActualAmount) - { - _logger.LogWarning("超额退款尝试:订单ID={OrderId}, 实付金额={ActualAmount}, 申请退款金额={RefundAmount}", - order.Id, order.ActualAmount, input.RefundAmount); - throw Oops.Bah($"退款金额({input.RefundAmount})不能超过实付金额({order.ActualAmount})"); - } - - // 检查该订单是否已有退款记录 - var existingRefund = await _orderRepository.IsAnyAsync(o => o.RelatedOrderId == order.Id && o.OrderType == 2); - if (existingRefund) - { - throw Oops.Bah("该订单已存在退款记录"); - } - var currentUserId = _authManager.UserId; - var now = DateTime.Now; - - // 1. 创建退款订单 - var refundOrder = new YD_Event.Core.Entities.Order - { - Id = _idGenerator.NextId(), - OrderNo = await _orderNoGeneratorService.GenerateRefundOrderNoAsync(), - OrderType = 2, // 退款 - MerchantId = order.MerchantId, - MerchantCertificationId = order.MerchantCertificationId, // 继承原订单的单位认证ID - EventId = order.EventId, - RegistrationId = order.RegistrationId, - RelatedOrderId = order.Id, // 关联原订单 - OriginalAmount = order.ActualAmount, - ActualAmount = input.RefundAmount, // 退款金额 - OrderStatus = OrderStatus.Refunded, // 退款成功 - PaymentTime = now, - Remark = input.Reason, - CreatedTime = now, - CreatedUserId = currentUserId - }; - await _orderRepository.InsertAsync(refundOrder); - - _logger.LogInformation("退款成功:订单ID={OrderId}, 退款订单号={RefundOrderNo}, 退款金额={RefundAmount}, 操作人={UserId}", - order.Id, refundOrder.OrderNo, input.RefundAmount, currentUserId); - - // 2. 更新原订单状态 - order.OrderStatus = OrderStatus.Refunded; - order.UpdatedTime = now; - order.UpdatedUserId = currentUserId; - await _orderRepository.UpdateAsync(order); - - // 3. 更新报名状态 - var reg = await _registrationRepository.GetByIdAsync(order.RegistrationId); - if (reg != null) - { - reg.PaymentStatus = 3; // 已退款 - reg.UpdatedTime = now; - reg.UpdatedUserId = currentUserId; - await _registrationRepository.UpdateAsync(reg); - } - - // 4. 更新支付记录(如果有) - var payment = await _paymentRepository.GetFirstAsync(p => p.RegistrationId == order.RegistrationId); - if (payment != null) - { - payment.RefundStatus = 1; // 已退款 - payment.UpdatedTime = now; - payment.UpdatedUserId = currentUserId; - await _paymentRepository.UpdateAsync(payment); - } + _logger.LogInformation("调用统一后台退款:订单ID={OrderId}, 操作人={UserId}", input.Id, currentUserId); + await _unifiedRegistrationService.ExecuteAdminRefundAsync(input, currentUserId); } /// diff --git a/YD_Event.Core/Entities/PointsRecord.cs b/YD_Event.Core/Entities/PointsRecord.cs index 00a0ff8..1a81529 100644 --- a/YD_Event.Core/Entities/PointsRecord.cs +++ b/YD_Event.Core/Entities/PointsRecord.cs @@ -36,6 +36,12 @@ public class PointsRecord : Entity, ISoftDelete, ICreatedUserId, ICreatedT [SugarColumn(IsNullable = true)] public long? ProjectGroupId { get; set; } + /// + /// 关联成绩记录ID + /// + [SugarColumn(IsNullable = true)] + public long? ResultId { get; set; } + /// /// 单位认证ID(单位用户积分关联) /// diff --git a/YD_Event.Core/YD_Event.Core.xml b/YD_Event.Core/YD_Event.Core.xml index 398e8f4..3700d68 100644 --- a/YD_Event.Core/YD_Event.Core.xml +++ b/YD_Event.Core/YD_Event.Core.xml @@ -2788,6 +2788,11 @@ 关联项目组别ID + + + 关联成绩记录ID + + 单位认证ID(单位用户积分关联)