diff --git a/YD_Event.Application/Event/UnifiedRegistrationService.cs b/YD_Event.Application/Event/UnifiedRegistrationService.cs index 73d64c2..f4050a5 100644 --- a/YD_Event.Application/Event/UnifiedRegistrationService.cs +++ b/YD_Event.Application/Event/UnifiedRegistrationService.cs @@ -2809,52 +2809,16 @@ public class UnifiedRegistrationService : ITransient // 全部取消 if (input.CancelType == 0) { - // 对于定价赛事,需要判断是否有团队项目 - if (eventInfo.ChargeMode == ChargeMode.FixedPrice) - { - // 检查是否有团队项目(项目类型名称包含"团队") - bool hasTeamProject = items.Any(item => item.ProjectTypeName.Contains("团队")); - - _logger.LogInformation("定价赛事,hasTeamProject={HasTeamProject}", hasTeamProject); - - // 如果没有团队项目,个人取消不退款 - if (!hasTeamProject) - { - _logger.LogInformation("定价赛事且无团队项目,个人取消不退款"); - return 0; - } - } - var fullRefundAmount = await CalculateRefundAmountAsync(remainingOriginalAmount, eventInfo.MatchStartTime); _logger.LogInformation("全部取消,退款金额={Amount}", fullRefundAmount); return fullRefundAmount; } // 部分取消 - // 【重要】退款基本原则 - // - 付款时已对报名人员进行去重处理,即同一运动员报名多个项目仅收取一份费用 - // - 退款时需遵循相同原则:仅当某运动员的所有已报名项目均被取消时,方可进行退款 - // - 若运动员报名多个项目但仅取消部分项目,则不进行退款 - - _logger.LogInformation("开始执行新退款规则:仅当运动员所有项目均被取消时方可退款"); - + // 收集所有取消的队员ID var cancelMemberIds = input.PersonIds ?? new List(); var cancelTeams = input.CancelTeams ?? new List(); - // 第一步:构建每个运动员的报名项目映射 - var athleteProjects = new Dictionary>(); - foreach (var person in persons) - { - if (!athleteProjects.ContainsKey(person.Id)) - { - athleteProjects[person.Id] = new List(); - } - athleteProjects[person.Id].Add(person.RegistrationItemId); - } - - _logger.LogInformation("运动员报名项目统计:共{Count}名运动员", athleteProjects.Count); - - // 第二步:收集所有要取消的队员 var allCancelIds = new HashSet(cancelMemberIds); foreach (var teamKey in cancelTeams) { @@ -2871,184 +2835,106 @@ public class UnifiedRegistrationService : ITransient _logger.LogInformation("本次取消的队员数量:{Count}", allCancelIds.Count); - // 第三步:识别符合退款条件的运动员(所有项目均被取消的运动员) - var eligibleAthletes = new List(); - - // 首先,构建团队取消映射 - var canceledItemIds = new HashSet(); - foreach (var teamKey in cancelTeams) - { - var parts = teamKey.Split('_'); - if (parts.Length == 2 && long.TryParse(parts[0], out var registrationItemId)) - { - canceledItemIds.Add(registrationItemId); - } - } - - foreach (var kvp in athleteProjects) - { - var athleteId = kvp.Key; - var athleteProjectIds = kvp.Value; - - _logger.LogInformation("检查运动员ID={AthleteId}, 报名项目数={Count}", athleteId, athleteProjectIds.Count); - - // 检查该运动员的所有项目是否都被取消了 - bool allProjectsCanceled = true; - - foreach (var projectId in athleteProjectIds) - { - // 检查该项目是否被取消: - // 1. 该项目的团队是否被取消 - // 2. 或者该运动员在该项目中的个人报名是否被取消 - bool projectCanceled = false; - - // 检查团队是否被取消 - if (canceledItemIds.Contains(projectId)) - { - projectCanceled = true; - _logger.LogInformation("运动员ID={AthleteId}, 项目ID={ProjectId}:团队被取消", athleteId, projectId); - } - else - { - // 检查该运动员在该项目中的个人报名是否被取消 - // 获取该运动员在该项目中的队员记录 - var memberInProject = persons.FirstOrDefault(p => p.Id == athleteId && p.RegistrationItemId == projectId); - if (memberInProject != null && cancelMemberIds.Contains(memberInProject.Id)) - { - projectCanceled = true; - _logger.LogInformation("运动员ID={AthleteId}, 项目ID={ProjectId}:个人报名被取消", athleteId, projectId); - } - } - - if (!projectCanceled) - { - allProjectsCanceled = false; - _logger.LogInformation("运动员ID={AthleteId}, 项目ID={ProjectId}:未被取消", athleteId, projectId); - break; - } - } - - if (allProjectsCanceled && athleteProjectIds.Count > 0) - { - eligibleAthletes.Add(athleteId); - _logger.LogInformation("运动员ID={AthleteId}:所有项目均被取消,符合退款条件", athleteId); - } - else - { - _logger.LogInformation("运动员ID={AthleteId}:部分项目未取消,不符合退款条件", athleteId); - } - } - - _logger.LogInformation("符合退款条件的运动员数量:{Count}", eligibleAthletes.Count); - - // 第四步:根据不同收费模式计算退款金额 - var baseRefundAmount = await CalculateRefundAmountAsync(remainingOriginalAmount, eventInfo.MatchStartTime); - var refundRatio = remainingOriginalAmount > 0 ? baseRefundAmount / remainingOriginalAmount : 1; - switch (eventInfo.ChargeMode) { case ChargeMode.FixedPrice: // 【定价(赛事)退款规则】 - // - 全队取消场景:严格按照既定退款规则执行退款操作 - // - 个人取消场景:不进行退款(仅当所有项目均取消时才退款) + // - 全队取消:严格按照预设退款规则执行退款 + // - 个人取消:不退款 + // - 仅当所有项目取消时才启动退款,单个项目取消不退款 _logger.LogInformation("定价(赛事)退款:开始计算"); - // 首先检查是否有团队被取消 - bool hasTeamCanceled = cancelTeams.Any(); + // 检查是否取消了所有队员(所有项目取消) + var allPersonsCanceled = allCancelIds.Count == persons.Count; - if (!hasTeamCanceled) + if (!allPersonsCanceled) { - // 没有取消任何团队,可能只是取消了个人 - _logger.LogInformation("定价赛事部分取消,未取消团队,检查是否有符合条件的运动员"); - - // 检查是否有团队项目 - bool hasTeamProject = items.Any(item => item.ProjectTypeName.Contains("团队")); - if (!hasTeamProject) - { - // 没有团队项目,个人取消不退款 - _logger.LogInformation("定价赛事没有团队项目,不退款"); - return 0; - } - } - - // 如果有符合退款条件的运动员,按照退款规则计算退款金额 - if (eligibleAthletes.Count > 0) - { - // 定价赛事中,符合条件的运动员退款按照退款规则计算 - var fixedPriceRefundAmount = await CalculateRefundAmountAsync(remainingOriginalAmount, eventInfo.MatchStartTime); - _logger.LogInformation("定价(赛事)退款:符合条件的运动员数={Count}, 退款金额={Amount}", - eligibleAthletes.Count, fixedPriceRefundAmount); - return fixedPriceRefundAmount; - } - - _logger.LogInformation("定价(赛事)退款:没有符合条件的运动员,不退款"); - return 0; - - case ChargeMode.UnitPriceEvent: - // 【赛事单价退款计算规则】 - // - 全队取消:严格按照既定退款规则执行退款操作 - // - 个人取消:计算公式为【赛事单价 × 符合退款条件的取消人数】,再根据退款规则计算最终退款金额 - - _logger.LogInformation("赛事单价退款:开始计算"); - - if (eligibleAthletes.Count == 0) - { - _logger.LogInformation("赛事单价退款:没有符合条件的运动员,不退款"); + // 没有取消所有队员,个人取消不退款 + _logger.LogInformation("定价赛事部分取消(未取消所有人员),个人取消不退款"); return 0; } - var unitPriceEvent = persons.Count > 0 ? remainingOriginalAmount / persons.Count : 0; - var unitPriceEventAmount = unitPriceEvent * eligibleAthletes.Count * refundRatio; + // 取消了所有队员,按退款规则退款 + var fixedPriceRefundAmount = await CalculateRefundAmountAsync(remainingOriginalAmount, eventInfo.MatchStartTime); + _logger.LogInformation("定价赛事全部取消,按退款规则退款,金额={Amount}", fixedPriceRefundAmount); + return fixedPriceRefundAmount; + + case ChargeMode.UnitPriceEvent: + // 【单价(赛事)退款规则】 + // - 全队取消:严格按照退款规则执行退款 + // - 个人取消:计算公式为【单价(赛事)× 取消人数】,并正确应用退款规则 + // - 全额退款:用户取消全部报名项目时按退款规则全额退还费用 + // - 部分退款:用户取消部分项目时按取消人数对应费用进行退款 - _logger.LogInformation("赛事单价退款:符合条件的运动员数={Count}, 人均单价={UnitPrice}, 退款比例={Ratio}, 计算金额={Amount}", - eligibleAthletes.Count, unitPriceEvent, refundRatio, unitPriceEventAmount); + _logger.LogInformation("单价(赛事)退款:开始计算"); + + if (allCancelIds.Count == 0) + { + _logger.LogInformation("单价赛事退款:没有取消任何人,不退款"); + return 0; + } + + // 获取赛事单价(从费用配置中获取) + var feeConfig = await _feeConfigRepository.GetFirstAsync(x => x.EventId == eventInfo.Id && !x.DeleteMark); + var eventUnitPrice = feeConfig?.Price ?? 0; + + _logger.LogInformation("单价(赛事)信息:赛事单价={EventUnitPrice}", eventUnitPrice); + + // 计算本次取消的原始金额 + var unitPriceEventCancelAmount = eventUnitPrice * allCancelIds.Count; + // 直接对本次取消的原始金额应用退款规则 + var unitPriceEventFinalAmount = await CalculateRefundAmountAsync(unitPriceEventCancelAmount, eventInfo.MatchStartTime); // 确保不超过剩余可退款金额 - var finalUnitPriceEventAmount = Math.Min(unitPriceEventAmount, baseRefundAmount); - _logger.LogInformation("赛事单价退款:最终金额={FinalAmount}", finalUnitPriceEventAmount); + var maxUnitPriceEventRefund = await CalculateRefundAmountAsync(remainingOriginalAmount, eventInfo.MatchStartTime); + var finalUnitPriceEventAmount = Math.Min(unitPriceEventFinalAmount, maxUnitPriceEventRefund); + + _logger.LogInformation("单价(赛事)退款:取消人数={CancelCount}, 赛事单价={UnitPrice}, 本次取消原始金额={CancelAmount}, 按规则退款金额={RefundAmount}, 最终金额={FinalAmount}", + allCancelIds.Count, eventUnitPrice, unitPriceEventCancelAmount, unitPriceEventFinalAmount, finalUnitPriceEventAmount); + return finalUnitPriceEventAmount; case ChargeMode.UnitPriceProject: - // 【项目单价退款计算规则】 - // - 全队取消:严格按照既定退款规则执行退款操作 - // - 个人取消:计算公式为【项目A单价 × 项目A符合退款条件的取消人数 + 项目B单价 × 项目B符合退款条件的取消人数】,再根据退款规则计算最终退款金额 + // 【单价(项目)退款规则】 + // - 全队取消:严格按照退款规则执行退款 + // - 个人取消:计算公式为【单价(项目A)× 项目A取消人数 + 单价(项目B)× 项目B取消人数】,并正确应用退款规则 - _logger.LogInformation("项目单价退款:开始计算"); + _logger.LogInformation("单价(项目)退款:开始计算"); - if (eligibleAthletes.Count == 0) + if (allCancelIds.Count == 0) { - _logger.LogInformation("项目单价退款:没有符合条件的运动员,不退款"); + _logger.LogInformation("单价项目退款:没有取消任何人,不退款"); return 0; } decimal totalProjectCancelAmount = 0; - // 按项目分组计算符合条件的运动员 + // 按项目分组计算取消的人数和金额 foreach (var item in items) { var itemPersons = persons.Where(p => p.RegistrationItemId == item.Id).ToList(); + var canceledInItem = itemPersons.Count(p => allCancelIds.Contains(p.Id)); - // 统计该项目中符合退款条件的运动员数量 - var eligibleInProject = itemPersons.Count(p => eligibleAthletes.Contains(p.Id)); - - if (eligibleInProject > 0) + if (canceledInItem > 0) { - var projectEligibleAmount = item.UnitPrice * eligibleInProject; - totalProjectCancelAmount += projectEligibleAmount; - _logger.LogInformation("项目单价退款:项目名称={ProjectName}, 项目单价={UnitPrice}, 符合条件人数={Count}, 本项金额={Amount}", - item.ProjectName, item.UnitPrice, eligibleInProject, projectEligibleAmount); + var projectCancelAmount = item.UnitPrice * canceledInItem; + totalProjectCancelAmount += projectCancelAmount; + _logger.LogInformation("单价(项目)退款:项目={ProjectName}, 单价={UnitPrice}, 取消人数={CancelCount}, 本项原始金额={Amount}", + item.ProjectName, item.UnitPrice, canceledInItem, projectCancelAmount); } } - var projectRefundAmount = totalProjectCancelAmount * refundRatio; - - _logger.LogInformation("项目单价退款:总符合条件金额={TotalAmount}, 退款比例={Ratio}, 计算金额={Amount}", - totalProjectCancelAmount, refundRatio, projectRefundAmount); + // 直接对本次取消的原始总金额应用退款规则 + var projectRefundAmount = await CalculateRefundAmountAsync(totalProjectCancelAmount, eventInfo.MatchStartTime); // 确保不超过剩余可退款金额 - var finalProjectRefundAmount = Math.Min(projectRefundAmount, baseRefundAmount); - _logger.LogInformation("项目单价退款:最终金额={FinalAmount}", finalProjectRefundAmount); + var maxProjectRefund = await CalculateRefundAmountAsync(remainingOriginalAmount, eventInfo.MatchStartTime); + var finalProjectRefundAmount = Math.Min(projectRefundAmount, maxProjectRefund); + + _logger.LogInformation("单价(项目)退款:本次取消总原始金额={TotalCancelAmount}, 按规则退款金额={RefundAmount}, 最终金额={FinalAmount}", + totalProjectCancelAmount, projectRefundAmount, finalProjectRefundAmount); + return finalProjectRefundAmount; default: @@ -3149,18 +3035,19 @@ public class UnifiedRegistrationService : ITransient _logger.LogInformation("线下支付部分取消:报名ID={RegistrationId}, 选择人数={SelectedCount}, 退款金额={RefundAmount}", registration.Id, selectedPersons.Count, actualRefundAmount); - await _registrationPersonRepository.DeleteAsync(x => allCancelMemberIds.Contains(x.Id)); - _logger.LogInformation("已删除队员:报名ID={RegistrationId}, 队员ID列表={PersonIds}", + // 软删除报名人员 + await SoftDeletePersonsAsync(allCancelMemberIds.ToList(), operatorUserId, now); + _logger.LogInformation("已软删除队员:报名ID={RegistrationId}, 队员ID列表={PersonIds}", registration.Id, string.Join(", ", allCancelMemberIds)); - // 检查并删除空的 RegistrationItem(所有成员都被删除的项) - var remainingPersons = await _registrationPersonRepository.GetListAsync(x => x.RegistrationId == registration.Id); + // 检查并软删除空的 RegistrationItem(所有成员都被软删除的项) + var remainingPersons = await _registrationPersonRepository.GetListAsync(x => x.RegistrationId == registration.Id && !x.DeleteMark); var remainingRegistrationItemIds = new HashSet(remainingPersons.Select(p => p.RegistrationItemId)); var itemsToDelete = items.Where(item => !remainingRegistrationItemIds.Contains(item.Id)).ToList(); if (itemsToDelete.Count > 0) { - await _registrationItemRepository.DeleteAsync(itemsToDelete); - _logger.LogInformation("已删除空团队:报名ID={RegistrationId}, 团队ID列表={ItemIds}", + await SoftDeleteItemsAsync(itemsToDelete.Select(i => i.Id).ToList(), operatorUserId, now); + _logger.LogInformation("已软删除空团队:报名ID={RegistrationId}, 团队ID列表={ItemIds}", registration.Id, string.Join(", ", itemsToDelete.Select(i => i.Id))); } @@ -3191,6 +3078,14 @@ public class UnifiedRegistrationService : ITransient _logger.LogInformation("线下支付全额取消:报名ID={RegistrationId}, 退款金额={RefundAmount}", registration.Id, actualRefundAmount); + // 软删除所有报名人员 + await SoftDeletePersonsAsync(persons.Select(p => p.Id).ToList(), operatorUserId, now); + _logger.LogInformation("已软删除所有队员:报名ID={RegistrationId}", registration.Id); + + // 软删除所有报名项目 + await SoftDeleteItemsAsync(items.Select(i => i.Id).ToList(), operatorUserId, now); + _logger.LogInformation("已软删除所有项目:报名ID={RegistrationId}", registration.Id); + registration.PaymentStatus = 4; // 已取消 registration.UpdatedUserId = operatorUserId; registration.UpdatedTime = now; @@ -3218,18 +3113,19 @@ public class UnifiedRegistrationService : ITransient _logger.LogInformation("统一部分退款:报名ID={RegistrationId}, 总人数={TotalPersons}, 选择人数={SelectedCount}, 实际退款金额={ActualRefundAmount}", registration.Id, totalPersons, selectedPersons.Count, actualRefundAmount); - await _registrationPersonRepository.DeleteAsync(x => allCancelMemberIds.Contains(x.Id)); - _logger.LogInformation("已删除队员:报名ID={RegistrationId}, 队员ID列表={PersonIds}", + // 软删除报名人员 + await SoftDeletePersonsAsync(allCancelMemberIds.ToList(), operatorUserId, now); + _logger.LogInformation("已软删除队员:报名ID={RegistrationId}, 队员ID列表={PersonIds}", registration.Id, string.Join(", ", allCancelMemberIds)); - // 检查并删除空的 RegistrationItem(所有成员都被删除的项) - var remainingPersons = await _registrationPersonRepository.GetListAsync(x => x.RegistrationId == registration.Id); + // 检查并软删除空的 RegistrationItem(所有成员都被软删除的项) + var remainingPersons = await _registrationPersonRepository.GetListAsync(x => x.RegistrationId == registration.Id && !x.DeleteMark); var remainingRegistrationItemIds = new HashSet(remainingPersons.Select(p => p.RegistrationItemId)); var itemsToDelete = items.Where(item => !remainingRegistrationItemIds.Contains(item.Id)).ToList(); if (itemsToDelete.Count > 0) { - await _registrationItemRepository.DeleteAsync(itemsToDelete); - _logger.LogInformation("已删除空团队:报名ID={RegistrationId}, 团队ID列表={ItemIds}", + await SoftDeleteItemsAsync(itemsToDelete.Select(i => i.Id).ToList(), operatorUserId, now); + _logger.LogInformation("已软删除空团队:报名ID={RegistrationId}, 团队ID列表={ItemIds}", registration.Id, string.Join(", ", itemsToDelete.Select(i => i.Id))); } @@ -3260,6 +3156,14 @@ public class UnifiedRegistrationService : ITransient _logger.LogInformation("统一全额退款:报名ID={RegistrationId}, 退款金额={ActualRefundAmount}", registration.Id, actualRefundAmount); + // 软删除所有报名人员 + await SoftDeletePersonsAsync(persons.Select(p => p.Id).ToList(), operatorUserId, now); + _logger.LogInformation("已软删除所有队员:报名ID={RegistrationId}", registration.Id); + + // 软删除所有报名项目 + await SoftDeleteItemsAsync(items.Select(i => i.Id).ToList(), operatorUserId, now); + _logger.LogInformation("已软删除所有项目:报名ID={RegistrationId}", registration.Id); + registration.PaymentStatus = 3; registration.UpdatedUserId = operatorUserId; registration.UpdatedTime = now; @@ -3457,6 +3361,36 @@ public class UnifiedRegistrationService : ITransient refundRecord.Id, outRefundNo); } + /// + /// 软删除报名人员 + /// + private async Task SoftDeletePersonsAsync(List personIds, long operatorUserId, DateTime now) + { + var personsToDelete = await _registrationPersonRepository.GetListAsync(x => personIds.Contains(x.Id)); + foreach (var person in personsToDelete) + { + person.DeleteMark = true; + person.UpdatedUserId = operatorUserId; + person.UpdatedTime = now; + } + await _registrationPersonRepository.UpdateRangeAsync(personsToDelete); + } + + /// + /// 软删除报名项目 + /// + private async Task SoftDeleteItemsAsync(List itemIds, long operatorUserId, DateTime now) + { + var itemsToDelete = await _registrationItemRepository.GetListAsync(x => itemIds.Contains(x.Id)); + foreach (var item in itemsToDelete) + { + item.DeleteMark = true; + item.UpdatedUserId = operatorUserId; + item.UpdatedTime = now; + } + await _registrationItemRepository.UpdateRangeAsync(itemsToDelete); + } + /// /// 更新订单状态 /// @@ -3514,6 +3448,7 @@ public class UnifiedRegistrationService : ITransient if (isPartialCancel) { var persons = await _registrationPersonRepository.GetListAsync(x => x.RegistrationId == order.RegistrationId); + var items = await _registrationItemRepository.GetListAsync(x => x.RegistrationId == order.RegistrationId); var totalPersons = persons.Count; if (totalPersons == 0) { @@ -3530,11 +3465,23 @@ public class UnifiedRegistrationService : ITransient _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}", + // 软删除报名人员 + await SoftDeletePersonsAsync(input.PersonIds, currentUserId, now); + _logger.LogInformation("已软删除队员:订单ID={OrderId}, 队员ID列表={PersonIds}", order.Id, string.Join(", ", input.PersonIds)); + + // 检查并软删除空的 RegistrationItem(所有成员都被软删除的项) + var remainingPersons = await _registrationPersonRepository.GetListAsync(x => x.RegistrationId == order.RegistrationId && !x.DeleteMark); + var remainingRegistrationItemIds = new HashSet(remainingPersons.Select(p => p.RegistrationItemId)); + var itemsToDelete = items.Where(item => !remainingRegistrationItemIds.Contains(item.Id)).ToList(); + if (itemsToDelete.Count > 0) + { + await SoftDeleteItemsAsync(itemsToDelete.Select(i => i.Id).ToList(), currentUserId, now); + _logger.LogInformation("已软删除空团队:订单ID={OrderId}, 团队ID列表={ItemIds}", + order.Id, string.Join(", ", itemsToDelete.Select(i => i.Id))); + } - var remainingPersonsCount = await _registrationPersonRepository.CountAsync(x => x.RegistrationId == order.RegistrationId); + var remainingPersonsCount = remainingPersons.Count; var reg = await _registrationRepository.GetByIdAsync(order.RegistrationId); if (reg != null) { @@ -3579,6 +3526,22 @@ public class UnifiedRegistrationService : ITransient { throw Oops.Bah("该订单已存在退款记录"); } + + // 软删除所有报名人员 + var persons = await _registrationPersonRepository.GetListAsync(x => x.RegistrationId == order.RegistrationId); + if (persons.Count > 0) + { + await SoftDeletePersonsAsync(persons.Select(p => p.Id).ToList(), currentUserId, now); + _logger.LogInformation("已软删除所有队员:订单ID={OrderId}", order.Id); + } + + // 软删除所有报名项目 + var items = await _registrationItemRepository.GetListAsync(x => x.RegistrationId == order.RegistrationId); + if (items.Count > 0) + { + await SoftDeleteItemsAsync(items.Select(i => i.Id).ToList(), currentUserId, now); + _logger.LogInformation("已软删除所有项目:订单ID={OrderId}", order.Id); + } } var fullRefundOrder = new YD_Event.Core.Entities.Order diff --git a/YD_Event.Application/YD_Event.Application.xml b/YD_Event.Application/YD_Event.Application.xml index da0238b..beeaaf1 100644 --- a/YD_Event.Application/YD_Event.Application.xml +++ b/YD_Event.Application/YD_Event.Application.xml @@ -8913,6 +8913,16 @@ 创建退款记录 + + + 软删除报名人员 + + + + + 软删除报名项目 + + 更新订单状态