From cf39aba63e3376a7b02c9a00bf8ef0eeeec3d772 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E6=A2=93=E5=BD=AD?= Date: Mon, 6 Apr 2026 10:50:57 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B5=9B=E4=BA=8B=E6=8A=A5=E5=90=8D=E8=B4=B9?= =?UTF-8?q?=E7=94=A8=E5=BD=92=E6=80=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Client/EventClientController.cs | 1 + .../Event/Dtos/UserCompetitionDtos.cs | 6 + .../Event/UserEventRegistrationService.cs | 205 +++++--------- YD_Event.Application/YD_Event.Application.xml | 30 +-- .../Domain/Pricing/EventPricingService.cs | 255 ++++++++++++++++++ YD_Event.Core/YD_Event.Core.xml | 143 ++++++++++ 6 files changed, 471 insertions(+), 169 deletions(-) create mode 100644 YD_Event.Core/Domain/Pricing/EventPricingService.cs diff --git a/YD_Event.Application/Client/EventClientController.cs b/YD_Event.Application/Client/EventClientController.cs index 587037e..5bc7f92 100644 --- a/YD_Event.Application/Client/EventClientController.cs +++ b/YD_Event.Application/Client/EventClientController.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging; using YD_Event.Application.Client.Dtos; using YD_Event.Core.Entities; using YD_Event.Core.Enum; +using YD_Event.Core.Domain.Pricing; using YD_Event.Core.Infrastructure.WeChat; using YD_Event.Core.Infrastructure.WeChat.Dto; using YD_Event.Core.Options; diff --git a/YD_Event.Application/Event/Dtos/UserCompetitionDtos.cs b/YD_Event.Application/Event/Dtos/UserCompetitionDtos.cs index 0f45fa7..7b4032b 100644 --- a/YD_Event.Application/Event/Dtos/UserCompetitionDtos.cs +++ b/YD_Event.Application/Event/Dtos/UserCompetitionDtos.cs @@ -656,6 +656,12 @@ public class UserSubmitRegistrationInput /// 优惠券ID (可选) /// public long? CouponId { get; set; } + + /// + /// 团队ID (必填) + /// + [Required] + public long TeamId { get; set; } } #region 参赛记录相关 DTOs diff --git a/YD_Event.Application/Event/UserEventRegistrationService.cs b/YD_Event.Application/Event/UserEventRegistrationService.cs index 0a484cc..9ab3ab4 100644 --- a/YD_Event.Application/Event/UserEventRegistrationService.cs +++ b/YD_Event.Application/Event/UserEventRegistrationService.cs @@ -6,6 +6,7 @@ using YD_Event.Application.Event.Dtos; using YD_Event.Application.Project; using YD_Event.Core.Entities; using YD_Event.Core.Enum; +using YD_Event.Core.Domain.Pricing; using SqlSugar; using Mapster; using YD_Event.Application.Auth; @@ -446,7 +447,7 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient // 验证2:一次报名只能选择单个团队内的运动员 if (input.MemberIds != null && input.MemberIds.Count > 0) { - // 获取所有队员的团队ID + // 获取所有队员的团队关系 var teamMembers = await _db.Queryable() .Where(tm => input.MemberIds.Contains(tm.UserId)) .Where(tm => !tm.DeleteMark) @@ -457,11 +458,23 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient throw Oops.Bah("未找到队员信息"); } - // 检查所有队员是否属于同一个团队 - var teamIds = teamMembers.Select(tm => tm.TeamId).Distinct().ToList(); - if (teamIds.Count > 1) + // 按用户ID分组,获取每个用户所属的所有团队 + var userTeamMap = teamMembers + .GroupBy(tm => tm.UserId) + .ToDictionary(g => g.Key, g => g.Select(tm => tm.TeamId).ToList()); + + // 检查是否所有用户都有团队信息 + var missingUsers = input.MemberIds.Except(userTeamMap.Keys).ToList(); + if (missingUsers.Any()) { - throw Oops.Bah("一次报名只能选择单个团队内的运动员"); + throw Oops.Bah("部分队员未找到团队信息"); + } + + // 验证所有队员是否都属于指定的 TeamId + var allBelongToTeam = userTeamMap.All(ut => ut.Value.Contains(input.TeamId)); + if (!allBelongToTeam) + { + throw Oops.Bah("所有选择的运动员必须隶属于同一个团队"); } } @@ -516,7 +529,6 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient var baseProjectGroupIds = eventProjectGroups.Select(x => x.ProjectGroupId).Distinct().ToList(); var baseProjectGroups = await _projectGroupRepository.GetListAsync(x => baseProjectGroupIds.Contains(x.Id)); - decimal totalAmount = 0; int totalPersonCount = 0; FeeCalculationResult? feeCalculationResult = null; @@ -554,7 +566,6 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient }; await _registrationItemRepository.InsertAsync(item); - totalAmount += item.TotalPrice; totalPersonCount += item.PersonCount; foreach (var member in groupMembers) @@ -603,7 +614,6 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient }; await _registrationItemRepository.InsertAsync(item); - totalAmount += item.TotalPrice; totalPersonCount += item.PersonCount; foreach (var member in members) @@ -640,10 +650,9 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient ParticipantCount = totalPersonCount }; feeCalculationResult = await CalculateFeeAsync(feeInput); - totalAmount = feeCalculationResult.TotalAmount; + registration.TotalAmount = feeCalculationResult.TotalAmount; } - registration.TotalAmount = totalAmount; registration.TotalPersonCount = totalPersonCount; await _registrationRepository.UpdateAsync(registration); @@ -1354,154 +1363,71 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient _logger.LogInformation("开始计算费用,赛事ID: {EventId}, 项目数: {ProjectCount}, 人数: {ParticipantCount}", input.EventId, input.EventProjectIds.Count, input.ParticipantCount); - var result = new FeeCalculationResult - { - TotalPersonCount = input.ParticipantCount - }; - // 获取赛事费用配置 var feeConfig = await _feeConfigRepository.GetFirstAsync(x => x.EventId == input.EventId); if (feeConfig == null) { _logger.LogWarning("未找到赛事费用配置,赛事ID: {EventId}", input.EventId); - result.ChargeMode = "免费"; - result.TotalAmount = 0; - result.CalculationLogs.Add($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 未找到费用配置,默认免费"); - return result; + var freeResult = new FeeCalculationResult + { + ChargeMode = "免费", + TotalAmount = 0, + TotalPersonCount = input.ParticipantCount + }; + freeResult.CalculationLogs.Add($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 未找到费用配置,默认免费"); + return freeResult; } - result.ChargeMode = GetChargeModeDescription(feeConfig.ChargeMode); - result.CalculationLogs.Add($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 收费模式: {result.ChargeMode}"); - - // 获取项目信息 + // 获取项目信息(用于单价项目模式) var eventProjects = await _eventProjectRepository.GetListAsync(x => input.EventProjectIds.Contains(x.Id)); var projectIds = eventProjects.Select(x => x.ProjectId).Distinct().ToList(); var baseProjects = await _projectRepository.GetListAsync(x => projectIds.Contains(x.Id)); - switch (feeConfig.ChargeMode) + // 构建统一计算服务的输入参数 + var pricingInput = new EventPricingService.PricingInput { - case ChargeMode.Free: - result = CalculateFreeMode(result, feeConfig); - break; - case ChargeMode.FixedPrice: - result = CalculateFixedPriceMode(result, feeConfig, input.ParticipantCount); - break; - case ChargeMode.UnitPriceEvent: - result = CalculateUnitPriceEventMode(result, feeConfig, input.ParticipantCount); - break; - case ChargeMode.UnitPriceProject: - result = await CalculateUnitPriceProjectMode(result, feeConfig, eventProjects, baseProjects, input.ParticipantCount); - break; - } - - // 确保金额精确到分 - result.TotalAmount = Math.Round(result.TotalAmount, 2, MidpointRounding.AwayFromZero); - result.CalculationLogs.Add($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 最终总金额: {result.TotalAmount:F2} 元"); - - _logger.LogInformation("费用计算完成,总金额: {TotalAmount:F2} 元", result.TotalAmount); - return result; - } - - /// - /// 免费模式 - /// - private FeeCalculationResult CalculateFreeMode(FeeCalculationResult result, EventFeeConfig feeConfig) - { - result.TotalAmount = 0; - result.FeeDetails.Add(new FeeDetailItem - { - ItemName = "报名费", - UnitPrice = 0, - Quantity = 1, - Subtotal = 0, - Remark = "免费" - }); - result.CalculationLogs.Add($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 免费模式,总金额: 0 元"); - return result; - } - - /// - /// 定价(赛事)模式:无论个人或单位报名,无论参加多少项目,统一按照固定价格计算 - /// - private FeeCalculationResult CalculateFixedPriceMode(FeeCalculationResult result, EventFeeConfig feeConfig, int participantCount) - { - var price = feeConfig.Price / 100m; - result.TotalAmount = price; - result.FeeDetails.Add(new FeeDetailItem - { - ItemName = "赛事报名费", - UnitPrice = price, - Quantity = 1, - Subtotal = price, - Remark = $"固定价格,参赛人数: {participantCount} 人" - }); - result.CalculationLogs.Add($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 定价模式: 固定价格 {price:F2} 元"); - return result; - } - - /// - /// 单价(赛事)模式:按照单价乘以实际参赛人数计算 - /// - private FeeCalculationResult CalculateUnitPriceEventMode(FeeCalculationResult result, EventFeeConfig feeConfig, int participantCount) - { - var unitPrice = feeConfig.Price / 100m; - var totalAmount = unitPrice * participantCount; - result.TotalAmount = totalAmount; - result.FeeDetails.Add(new FeeDetailItem - { - ItemName = "赛事报名费", - UnitPrice = unitPrice, - Quantity = participantCount, - Subtotal = totalAmount, - Remark = $"单价 × 人数 = {unitPrice:F2} × {participantCount}" - }); - result.CalculationLogs.Add($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 单价(赛事)模式: {unitPrice:F2} 元/人 × {participantCount} 人 = {totalAmount:F2} 元"); - return result; - } - - /// - /// 单价(项目)模式:按照各项目单价总和乘以实际参赛人数计算 - /// - private async Task CalculateUnitPriceProjectMode( - FeeCalculationResult result, - EventFeeConfig feeConfig, - List eventProjects, - List baseProjects, - int participantCount) - { - decimal totalProjectPrice = 0; + ChargeMode = feeConfig.ChargeMode, + ParticipantCount = input.ParticipantCount + }; + // 添加项目信息 foreach (var eventProject in eventProjects) { var baseProject = baseProjects.FirstOrDefault(p => p.Id == eventProject.ProjectId); - var projectPrice = eventProject.Price / 100m; - totalProjectPrice += projectPrice; - - result.FeeDetails.Add(new FeeDetailItem + pricingInput.Projects.Add(new EventPricingService.ProjectPricingInfo { - ItemName = baseProject?.Name ?? "未知项目", - UnitPrice = projectPrice, - Quantity = 1, - Subtotal = projectPrice, - Remark = "项目单价" + ProjectId = eventProject.ProjectId, + ProjectName = baseProject?.Name ?? "未知项目", + UnitPrice = eventProject.Price / 100m }); - result.CalculationLogs.Add($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 项目: {baseProject?.Name ?? "未知"}, 单价: {projectPrice:F2} 元"); } - var totalAmount = totalProjectPrice * participantCount; - result.TotalAmount = totalAmount; + // 使用统一计算服务计算费用 + var pricingResult = EventPricingService.Calculate(pricingInput); - // 添加汇总项 - result.FeeDetails.Add(new FeeDetailItem + // 转换为现有格式的结果 + var result = new FeeCalculationResult { - ItemName = "项目总价 × 人数", - UnitPrice = totalProjectPrice, - Quantity = participantCount, - Subtotal = totalAmount, - Remark = $"({totalProjectPrice:F2} 元/套) × {participantCount} 人" - }); - result.CalculationLogs.Add($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 单价(项目)模式: 项目总价 {totalProjectPrice:F2} 元 × {participantCount} 人 = {totalAmount:F2} 元"); + ChargeMode = GetChargeModeDescription(feeConfig.ChargeMode), + TotalAmount = pricingResult.TotalAmount, + TotalPersonCount = pricingResult.TotalPersonCount, + CalculationLogs = pricingResult.CalculationLogs + }; + // 转换费用明细 + foreach (var detail in pricingResult.Details) + { + result.FeeDetails.Add(new FeeDetailItem + { + ItemName = detail.ItemName, + UnitPrice = detail.UnitPrice, + Quantity = detail.Quantity, + Subtotal = detail.Subtotal, + Remark = detail.Remark + }); + } + + _logger.LogInformation("费用计算完成,总金额: {TotalAmount:F2} 元", result.TotalAmount); return result; } @@ -1520,13 +1446,4 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient }; } - /// - /// 费用计算预览API - /// - [DisplayName("费用计算预览")] - [HttpPost("fee/preview")] - public async Task PreviewFee([FromBody] FeeCalculationInput input) - { - return await CalculateFeeAsync(input); - } } diff --git a/YD_Event.Application/YD_Event.Application.xml b/YD_Event.Application/YD_Event.Application.xml index a1ac271..08b4003 100644 --- a/YD_Event.Application/YD_Event.Application.xml +++ b/YD_Event.Application/YD_Event.Application.xml @@ -5596,6 +5596,11 @@ 优惠券ID (可选) + + + 团队ID (必填) + + 参赛记录查询输入 @@ -6460,36 +6465,11 @@ 费用计算输入 费用计算结果 - - - 免费模式 - - - - - 定价(赛事)模式:无论个人或单位报名,无论参加多少项目,统一按照固定价格计算 - - - - - 单价(赛事)模式:按照单价乘以实际参赛人数计算 - - - - - 单价(项目)模式:按照各项目单价总和乘以实际参赛人数计算 - - 获取收费模式描述 - - - 费用计算预览API - - 文件上传输出参数 diff --git a/YD_Event.Core/Domain/Pricing/EventPricingService.cs b/YD_Event.Core/Domain/Pricing/EventPricingService.cs new file mode 100644 index 0000000..61d6d22 --- /dev/null +++ b/YD_Event.Core/Domain/Pricing/EventPricingService.cs @@ -0,0 +1,255 @@ +using YD_Event.Core.Enum; + +namespace YD_Event.Core.Domain.Pricing; + +/// +/// 赛事定价计算服务 +/// 所有金额计算均以此服务为准,确保计算结果的一致性 +/// +public static class EventPricingService +{ + /// + /// 赛事基础定价(元) + /// + public const decimal EventBasePrice = 1000m; + + /// + /// 项目A定价(元) + /// + public const decimal ProjectAPrice = 500m; + + /// + /// 项目B定价(元) + /// + public const decimal ProjectBPrice = 1000m; + + /// + /// 定价计算输入参数 + /// + public class PricingInput + { + /// + /// 收费模式 + /// + public ChargeMode ChargeMode { get; set; } + + /// + /// 参赛人数 + /// + public int ParticipantCount { get; set; } + + /// + /// 项目列表(单价项目模式使用) + /// + public List Projects { get; set; } = new List(); + } + + /// + /// 项目定价信息 + /// + public class ProjectPricingInfo + { + /// + /// 项目ID + /// + public long ProjectId { get; set; } + + /// + /// 项目名称 + /// + public string ProjectName { get; set; } = string.Empty; + + /// + /// 项目单价(元) + /// + public decimal UnitPrice { get; set; } + } + + /// + /// 定价计算结果 + /// + public class PricingResult + { + /// + /// 总金额(精确到分) + /// + public decimal TotalAmount { get; set; } + + /// + /// 总人数 + /// + public int TotalPersonCount { get; set; } + + /// + /// 费用明细 + /// + public List Details { get; set; } = new List(); + + /// + /// 计算日志(用于审计) + /// + public List CalculationLogs { get; set; } = new List(); + } + + /// + /// 费用明细项 + /// + public class PricingDetailItem + { + /// + /// 项目名称 + /// + public string ItemName { get; set; } = string.Empty; + + /// + /// 单价 + /// + public decimal UnitPrice { get; set; } + + /// + /// 数量 + /// + public int Quantity { get; set; } + + /// + /// 小计 + /// + public decimal Subtotal { get; set; } + + /// + /// 备注 + /// + public string? Remark { get; set; } + } + + /// + /// 计算赛事报名费用 + /// + /// 定价计算输入 + /// 定价计算结果 + public static PricingResult Calculate(PricingInput input) + { + var result = new PricingResult + { + TotalPersonCount = input.ParticipantCount + }; + + result.CalculationLogs.Add($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 开始计算费用,收费模式: {input.ChargeMode}, 人数: {input.ParticipantCount}"); + + switch (input.ChargeMode) + { + case ChargeMode.Free: + CalculateFreeMode(result); + break; + case ChargeMode.FixedPrice: + CalculateFixedPriceMode(result, input.ParticipantCount); + break; + case ChargeMode.UnitPriceEvent: + CalculateUnitPriceEventMode(result, input.ParticipantCount); + break; + case ChargeMode.UnitPriceProject: + CalculateUnitPriceProjectMode(result, input.Projects, input.ParticipantCount); + break; + } + + // 确保金额精确到分 + result.TotalAmount = Math.Round(result.TotalAmount, 2, MidpointRounding.AwayFromZero); + result.CalculationLogs.Add($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 最终总金额: {result.TotalAmount:F2} 元"); + + return result; + } + + /// + /// 免费模式 + /// + private static void CalculateFreeMode(PricingResult result) + { + result.TotalAmount = 0; + result.Details.Add(new PricingDetailItem + { + ItemName = "报名费", + UnitPrice = 0, + Quantity = 1, + Subtotal = 0, + Remark = "免费" + }); + result.CalculationLogs.Add($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 免费模式,总金额: 0 元"); + } + + /// + /// 定价(赛事)模式:固定为1000元/场,无论参赛主体是个人还是单位,也无论参赛项目数量多少,均统一按1000元计算 + /// + private static void CalculateFixedPriceMode(PricingResult result, int participantCount) + { + var price = EventBasePrice; + result.TotalAmount = price; + result.Details.Add(new PricingDetailItem + { + ItemName = "赛事报名费", + UnitPrice = price, + Quantity = 1, + Subtotal = price, + Remark = $"固定价格 {EventBasePrice:F2} 元/场,参赛人数: {participantCount} 人" + }); + result.CalculationLogs.Add($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 定价(赛事)模式: 固定价格 {EventBasePrice:F2} 元/场"); + } + + /// + /// 单价(赛事)模式:赛事基础定价为1000元,个人参赛按1000元计算;团队参赛按人数乘以1000元计算 + /// + private static void CalculateUnitPriceEventMode(PricingResult result, int participantCount) + { + var unitPrice = EventBasePrice; + var totalAmount = unitPrice * participantCount; + result.TotalAmount = totalAmount; + result.Details.Add(new PricingDetailItem + { + ItemName = "赛事报名费", + UnitPrice = unitPrice, + Quantity = participantCount, + Subtotal = totalAmount, + Remark = participantCount == 1 + ? $"个人参赛 {unitPrice:F2} 元" + : $"团队参赛 {unitPrice:F2} 元/人 × {participantCount} 人" + }); + result.CalculationLogs.Add($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 单价(赛事)模式: {unitPrice:F2} 元/人 × {participantCount} 人 = {totalAmount:F2} 元"); + } + + /// + /// 单价(项目)模式:按具体项目定价计算,个人参加多项时,费用为各项单价之和;团队参加多项时,费用为(各项单价之和)×团队人数 + /// + private static void CalculateUnitPriceProjectMode(PricingResult result, List projects, int participantCount) + { + decimal totalProjectPrice = 0; + + foreach (var project in projects) + { + totalProjectPrice += project.UnitPrice; + result.Details.Add(new PricingDetailItem + { + ItemName = project.ProjectName, + UnitPrice = project.UnitPrice, + Quantity = 1, + Subtotal = project.UnitPrice, + Remark = "项目单价" + }); + result.CalculationLogs.Add($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 项目: {project.ProjectName}, 单价: {project.UnitPrice:F2} 元"); + } + + var totalAmount = totalProjectPrice * participantCount; + result.TotalAmount = totalAmount; + + // 添加汇总项 + result.Details.Add(new PricingDetailItem + { + ItemName = participantCount == 1 ? "项目总价" : "项目总价 × 人数", + UnitPrice = totalProjectPrice, + Quantity = participantCount, + Subtotal = totalAmount, + Remark = participantCount == 1 + ? $"个人参赛,项目总价 {totalProjectPrice:F2} 元" + : $"团队参赛,({totalProjectPrice:F2} 元) × {participantCount} 人" + }); + result.CalculationLogs.Add($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 单价(项目)模式: 项目总价 {totalProjectPrice:F2} 元 × {participantCount} 人 = {totalAmount:F2} 元"); + } +} diff --git a/YD_Event.Core/YD_Event.Core.xml b/YD_Event.Core/YD_Event.Core.xml index beae919..0f8ddd2 100644 --- a/YD_Event.Core/YD_Event.Core.xml +++ b/YD_Event.Core/YD_Event.Core.xml @@ -488,6 +488,149 @@ 执行导出 + + + 赛事定价计算服务 + 所有金额计算均以此服务为准,确保计算结果的一致性 + + + + + 赛事基础定价(元) + + + + + 项目A定价(元) + + + + + 项目B定价(元) + + + + + 定价计算输入参数 + + + + + 收费模式 + + + + + 参赛人数 + + + + + 项目列表(单价项目模式使用) + + + + + 项目定价信息 + + + + + 项目ID + + + + + 项目名称 + + + + + 项目单价(元) + + + + + 定价计算结果 + + + + + 总金额(精确到分) + + + + + 总人数 + + + + + 费用明细 + + + + + 计算日志(用于审计) + + + + + 费用明细项 + + + + + 项目名称 + + + + + 单价 + + + + + 数量 + + + + + 小计 + + + + + 备注 + + + + + 计算赛事报名费用 + + 定价计算输入 + 定价计算结果 + + + + 免费模式 + + + + + 定价(赛事)模式:固定为1000元/场,无论参赛主体是个人还是单位,也无论参赛项目数量多少,均统一按1000元计算 + + + + + 单价(赛事)模式:赛事基础定价为1000元,个人参赛按1000元计算;团队参赛按人数乘以1000元计算 + + + + + 单价(项目)模式:按具体项目定价计算,个人参加多项时,费用为各项单价之和;团队参加多项时,费用为(各项单价之和)×团队人数 + + 表实体继承