diff --git a/YD_Event.Application/Client/EventClientController.cs b/YD_Event.Application/Client/EventClientController.cs index 595846f..6e8d555 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.Utils; using YD_Event.Core.Infrastructure.WeChat; using YD_Event.Core.Infrastructure.WeChat.Dto; using YD_Event.Core.Options; @@ -48,6 +49,7 @@ public class EventClientController : IDynamicApiController private readonly ISqlSugarRepository _districtRepository; private readonly ISqlSugarRepository _orderRepository; private readonly ISqlSugarRepository _merchantCertificationRepository; + private readonly ISqlSugarRepository _paymentRepository; private readonly IHttpContextAccessor _httpContextAccessor; private readonly IIdGenerator _idGenerator; private readonly WeChatPayService _weChatPayService; @@ -79,6 +81,7 @@ public class EventClientController : IDynamicApiController ISqlSugarRepository districtRepository, ISqlSugarRepository orderRepository, ISqlSugarRepository merchantCertificationRepository, + ISqlSugarRepository paymentRepository, IHttpContextAccessor httpContextAccessor, IIdGenerator idGenerator, WeChatPayService weChatPayService, @@ -109,6 +112,7 @@ public class EventClientController : IDynamicApiController _districtRepository = districtRepository; _orderRepository = orderRepository; _merchantCertificationRepository = merchantCertificationRepository; + _paymentRepository = paymentRepository; _httpContextAccessor = httpContextAccessor; _idGenerator = idGenerator; _weChatPayService = weChatPayService; @@ -929,7 +933,7 @@ public class EventClientController : IDynamicApiController AppId = configOptions.AppId, Description = $"{evt.EventName} - 报名费", NotifyUrl = configOptions.NotifyUrl, - TotalAmount = (int)(reg.TotalAmount * 100), + TotalAmount = AmountHelper.YuanToFen(reg.TotalAmount), ExpireTime = DateTimeOffset.Now.AddMinutes(30), OpenId = input.OpenId }; @@ -1279,6 +1283,24 @@ public class EventClientController : IDynamicApiController await _registrationRepository.UpdateAsync(reg); _logger.LogInformation($"订单 {reg.OrderNo} 支付状态同步成功,微信交易号:{queryResult.TransactionId}"); + // 创建支付记录 + var payment = new EventRegistrationPayment + { + Id = _idGenerator.NewLong(), + RegistrationId = reg.Id, + TransactionNo = queryResult.TransactionId ?? "", + PaymentChannel = 1, // 微信支付 + PaymentAmount = reg.TotalAmount, + PaymentTime = queryResult.SuccessTime?.DateTime ?? DateTime.Now, + PaymentStatus = 1, // 已支付 + RefundStatus = 0, // 未退款 + PayeeAccount = "", // 收款账户 + CreatedUserId = userId, + CreatedTime = DateTime.Now + }; + await _paymentRepository.InsertAsync(payment); + _logger.LogInformation($"支付记录创建成功,报名ID={reg.Id},交易号={queryResult.TransactionId}"); + // 更新 Order 记录状态 var order = await _orderRepository.GetFirstAsync(o => o.OrderNo == reg.OrderNo); if (order != null) @@ -1412,6 +1434,24 @@ public class EventClientController : IDynamicApiController await _registrationRepository.UpdateAsync(reg); _logger.LogInformation($"订单 {reg.OrderNo} 支付成功,微信交易号:{notifyData.TransactionId}"); + // 创建支付记录 + var payment = new EventRegistrationPayment + { + Id = _idGenerator.NewLong(), + RegistrationId = reg.Id, + TransactionNo = notifyData.TransactionId ?? "", + PaymentChannel = 1, // 微信支付 + PaymentAmount = reg.TotalAmount, + PaymentTime = notifyData.SuccessTime?.DateTime ?? DateTime.Now, + PaymentStatus = 1, // 已支付 + RefundStatus = 0, // 未退款 + PayeeAccount = "", // 收款账户 + CreatedUserId = reg.CreatedUserId, + CreatedTime = DateTime.Now + }; + await _paymentRepository.InsertAsync(payment); + _logger.LogInformation($"支付记录创建成功,报名ID={reg.Id},交易号={notifyData.TransactionId}"); + // 更新 Order 记录状态 var order = await _orderRepository.GetFirstAsync(o => o.OrderNo == reg.OrderNo); if (order != null) diff --git a/YD_Event.Application/Event/EventService.cs b/YD_Event.Application/Event/EventService.cs index 97391d9..60c2905 100644 --- a/YD_Event.Application/Event/EventService.cs +++ b/YD_Event.Application/Event/EventService.cs @@ -2,6 +2,7 @@ using YD_Event.Application.Event.Dtos; using YD_Event.Application.Project.Dtos; using YD_Event.Core.Entities; using YD_Event.Core.Enum; +using YD_Event.Core.Utils; using MiniExcelLibs; using SqlSugar; using Mapster; @@ -402,7 +403,7 @@ public class EventService : BaseService, ITransient EventId = eventInfo.Id, ProjectId = projInput.ProjectId, GenderType = projInput.GenderType, - Price = (int)(projInput.Fee * 100), + Price = AmountHelper.YuanToFen(projInput.Fee), CreatedTime = DateTime.Now }; await _eventProjectRepository.InsertAsync(eventProject); @@ -576,7 +577,7 @@ public class EventService : BaseService, ITransient EventId = eventInfo.Id, ProjectId = projInput.ProjectId, GenderType = projInput.GenderType, - Price = (int)(projInput.Fee * 100), + Price = AmountHelper.YuanToFen(projInput.Fee), CreatedTime = DateTime.Now }; await _eventProjectRepository.InsertAsync(eventProject); @@ -938,7 +939,7 @@ public class EventService : BaseService, ITransient EventId = input.EventId, ProjectId = projInput.ProjectId, GenderType = projInput.GenderType, - Price = (int)(projInput.Fee * 100), + Price = AmountHelper.YuanToFen(projInput.Fee), CreatedTime = DateTime.Now }; await _eventProjectRepository.InsertAsync(eventProject); @@ -1033,7 +1034,7 @@ public class EventService : BaseService, ITransient } // 格式验证:最多保留两位小数 - if (Math.Round(input.FeeConfig.Price, 2) != input.FeeConfig.Price) + if (!AmountHelper.IsValidAmount(input.FeeConfig.Price)) { throw new InvalidOperationException("价格格式不正确,最多保留两位小数"); } @@ -1059,7 +1060,7 @@ public class EventService : BaseService, ITransient } // 格式验证:最多保留两位小数 - if (Math.Round(feeInput.Fee, 2) != feeInput.Fee) + if (!AmountHelper.IsValidAmount(feeInput.Fee)) { throw new InvalidOperationException("项目单价格式不正确,最多保留两位小数"); } @@ -1067,7 +1068,7 @@ public class EventService : BaseService, ITransient var proj = eventProjects.FirstOrDefault(p => p.Id == feeInput.ProjectId); if (proj != null) { - proj.Price = (int)(feeInput.Fee * 100); + proj.Price = AmountHelper.YuanToFen(feeInput.Fee); proj.UpdatedTime = DateTime.Now; await _eventProjectRepository.UpdateAsync(proj); } diff --git a/YD_Event.Application/Event/UserEventRecordService.cs b/YD_Event.Application/Event/UserEventRecordService.cs index 160048f..6787fec 100644 --- a/YD_Event.Application/Event/UserEventRecordService.cs +++ b/YD_Event.Application/Event/UserEventRecordService.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using YD_Event.Application.Event.Dtos; using YD_Event.Core.Entities; using YD_Event.Core.Enum; +using YD_Event.Core.Utils; using SqlSugar; using Mapster; using YD_Event.Application.Auth; @@ -519,8 +520,8 @@ public class UserEventRecordService : IDynamicApiController, ITransient { OutTradeNo = registration.OrderNo, OutRefundNo = outRefundNo, - RefundAmount = (int)(registration.TotalAmount * 100), // 转分 - TotalAmount = (int)(registration.TotalAmount * 100), + RefundAmount = AmountHelper.YuanToFen(registration.TotalAmount), // 转分 + TotalAmount = AmountHelper.YuanToFen(registration.TotalAmount), Reason = input.CancelReason }; diff --git a/YD_Event.Application/Event/UserEventRegistrationService.cs b/YD_Event.Application/Event/UserEventRegistrationService.cs index 79bed66..c3f157d 100644 --- a/YD_Event.Application/Event/UserEventRegistrationService.cs +++ b/YD_Event.Application/Event/UserEventRegistrationService.cs @@ -7,6 +7,7 @@ using YD_Event.Application.Project; using YD_Event.Core.Entities; using YD_Event.Core.Enum; using YD_Event.Core.Domain.Pricing; +using YD_Event.Core.Utils; using SqlSugar; using Mapster; using YD_Event.Application.Auth; @@ -234,25 +235,16 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient .Where(t => !t.DeleteMark && t.Status == AvailabilityStatus.Enable) .ToListAsync(); - var completionStats = await _projectService.GetProjectStatisticsBatchAsync( - projectIds, - ProjectService.ProjectStatisticType.CompletionCount); - output.Projects = eventProjects.Select(ep => { var p = baseProjects.FirstOrDefault(b => b.Id == ep.ProjectId); - var completionCount = 0; - if (p != null && completionStats.TryGetValue(p.Id, out var count)) - { - completionCount = count; - } var dto = new UserEventProjectListOutput { Id = ep.Id, Name = p?.Name ?? "", Fee = ep.Price / 100m, - CompletionCount = completionCount + CompletionCount = p?.CompletionCount ?? 0 }; if (p != null) @@ -330,18 +322,9 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient .Where(t => !t.DeleteMark && t.Status == AvailabilityStatus.Enable) .ToListAsync(); - var completionStats = await _projectService.GetProjectStatisticsBatchAsync( - projectIds, - ProjectService.ProjectStatisticType.CompletionCount); - return eventProjects.Select(ep => { var p = baseProjects.FirstOrDefault(b => b.Id == ep.ProjectId); - var completionCount = 0; - if (p != null && completionStats.TryGetValue(p.Id, out var count)) - { - completionCount = count; - } var dto = new UserEventProjectListOutput { @@ -349,7 +332,7 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient Name = p?.Name ?? "", Fee = ep.Price / 100m, TeamSize = 1, - CompletionCount = completionCount + CompletionCount = p?.CompletionCount ?? 0 }; if (p != null) @@ -939,7 +922,7 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient AppId = configOptions.AppId, Description = $"{eventInfo.EventName} - 报名费", NotifyUrl = configOptions.NotifyUrl, - TotalAmount = (int)(finalAmount * 100), // 单位为分,使用优惠后的金额 + TotalAmount = AmountHelper.YuanToFen(finalAmount), // 单位为分,使用优惠后的金额 ExpireTime = DateTimeOffset.Now.AddMinutes(5) }; diff --git a/YD_Event.Application/Order/Dtos/UpdatePaymentStatusInput.cs b/YD_Event.Application/Order/Dtos/UpdatePaymentStatusInput.cs new file mode 100644 index 0000000..f51662a --- /dev/null +++ b/YD_Event.Application/Order/Dtos/UpdatePaymentStatusInput.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; + +namespace YD_Event.Application.Order.Dtos; + +public class UpdatePaymentStatusInput +{ + /// + /// 订单ID列表(批量更新) + /// + [Required] + public List Ids { get; set; } + + /// + /// 支付状态:0-未支付,1-已支付,3-已退款 + /// + [Required] + public int PaymentStatus { get; set; } +} diff --git a/YD_Event.Application/Order/OrderService.cs b/YD_Event.Application/Order/OrderService.cs index e23aa66..08298d6 100644 --- a/YD_Event.Application/Order/OrderService.cs +++ b/YD_Event.Application/Order/OrderService.cs @@ -136,7 +136,7 @@ public class OrderService : BaseService, ITransien Id = _idGenerator.NextId(), OrderNo = entity.OrderNo, OrderType = 1, // 收入 - PaymentMethod = 1, // 默认线上支付 + PaymentMethod = 2, // 默认线上支付 (1=线下, 2=线上) RegistrationMode = 2, // 后台管理页面只做团队报名 MerchantId = eventInfo?.MerchantId, // 设置商户ID(支付商户) MerchantCertificationId = certification?.Id, // 设置单位认证ID @@ -202,7 +202,7 @@ public class OrderService : BaseService, ITransien EventName = e.EventName, OrderType = o.OrderType == 1 ? "收入" : "退款", RelatedOrderNo = ro.OrderNo, - PaymentMethod = o.PaymentMethod == 1 ? "线上" : "线下", + PaymentMethod = o.PaymentMethod == 2 ? "线上" : "线下", PaymentProofUrl = o.PaymentProofUrl, RegistrationMode = o.RegistrationMode == 1 ? "个人" : "团队", ContactName = r.ContactName, @@ -282,30 +282,60 @@ public class OrderService : BaseService, ITransien } /// - /// 更新订单状态 + /// 批量更新订单支付状态 /// - [DisplayName("更新订单状态")] - [HttpPut("status")] - public async Task UpdateStatus(UpdateOrderStatusInput input) + [DisplayName("批量更新订单支付状态")] + [HttpPut("payment-status")] + public async Task UpdatePaymentStatus(UpdatePaymentStatusInput input) { - var order = await _orderRepository.GetByIdAsync(input.Id); - if (order == null) throw Oops.Bah("订单不存在"); - - // 更新订单 - order.OrderStatus = input.PaymentStatus == 1 ? OrderStatus.Paid : OrderStatus.Pending; // 假设1表示支付成功 - if (input.PaymentStatus == 1) + if (input.Ids == null || !input.Ids.Any()) { - order.PaymentTime = DateTime.Now; + throw Oops.Bah("请选择要更新的订单"); } - await _orderRepository.UpdateAsync(order); - // 同步更新报名记录 - var reg = await _registrationRepository.GetByIdAsync(order.RegistrationId); - if (reg != null) + var orders = await _orderRepository.AsQueryable() + .Where(o => input.Ids.Contains(o.Id)) + .ToListAsync(); + + if (!orders.Any()) { - reg.PaymentStatus = input.PaymentStatus; - reg.PaymentTime = order.PaymentTime; - await _registrationRepository.UpdateAsync(reg); + throw Oops.Bah("未找到订单"); + } + + var currentUserId = _authManager.UserId; + var now = DateTime.Now; + + foreach (var order in orders) + { + // 更新订单的 OrderStatus(以 EventRegistration 的支付状态为准) + if (input.PaymentStatus == 1) + { + order.OrderStatus = OrderStatus.Paid; + order.PaymentTime = now; + } + else if (input.PaymentStatus == 3) + { + order.OrderStatus = OrderStatus.Refunded; + order.PaymentTime = now; + } + else + { + order.OrderStatus = OrderStatus.Pending; + } + order.UpdatedTime = now; + order.UpdatedUserId = currentUserId; + await _orderRepository.UpdateAsync(order); + + // 同步更新报名记录的支付状态 + var reg = await _registrationRepository.GetByIdAsync(order.RegistrationId); + if (reg != null) + { + reg.PaymentStatus = input.PaymentStatus; + reg.PaymentTime = order.PaymentTime; + reg.UpdatedTime = now; + reg.UpdatedUserId = currentUserId; + await _registrationRepository.UpdateAsync(reg); + } } } @@ -338,6 +368,9 @@ public class OrderService : BaseService, ITransien if (order.OrderStatus != OrderStatus.Paid) throw Oops.Bah("订单未支付,无法退款"); + var currentUserId = _authManager.UserId; + var now = DateTime.Now; + // 1. 创建退款订单 var refundOrder = new YD_Event.Core.Entities.Order { @@ -354,30 +387,66 @@ public class OrderService : BaseService, ITransien OriginalAmount = order.ActualAmount, ActualAmount = input.RefundAmount, // 退款金额 OrderStatus = OrderStatus.Refunded, // 退款成功 - PaymentTime = DateTime.Now, + PaymentTime = now, Remark = input.Reason, - CreatedTime = DateTime.Now, - CreatedUserId = _idGenerator.NewLong() // 系统或当前用户 + CreatedTime = now, + CreatedUserId = currentUserId }; await _orderRepository.InsertAsync(refundOrder); - // 2. 更新报名状态 + // 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); + } } /// - /// 删除订单 + /// 删除订单(软删除) /// [DisplayName("删除订单")] [HttpPost("delete")] public async Task Delete([FromBody] List ids) { - await _orderRepository.DeleteAsync(x => ids.Contains(x.Id)); + if (ids == null || !ids.Any()) + { + throw Oops.Bah("请选择要删除的订单"); + } + + var currentUserId = _authManager.UserId; + var now = DateTime.Now; + + var orders = await _orderRepository.AsQueryable() + .Where(o => ids.Contains(o.Id)) + .ToListAsync(); + + foreach (var order in orders) + { + order.DeleteMark = true; + order.UpdatedTime = now; + order.UpdatedUserId = currentUserId; + await _orderRepository.UpdateAsync(order); + } } /// @@ -415,7 +484,7 @@ public class OrderService : BaseService, ITransien EventName = e.EventName, OrderType = o.OrderType == 1 ? "收入" : "退款", RelatedOrderNo = ro.OrderNo, - PaymentMethod = o.PaymentMethod == 1 ? "线上" : "线下", + PaymentMethod = o.PaymentMethod == 2 ? "线上" : "线下", RegistrationMode = o.RegistrationMode == 1 ? "个人" : "团队", ContactName = r.ContactName, ContactPhone = r.ContactPhone, diff --git a/YD_Event.Application/Project/ProjectService.cs b/YD_Event.Application/Project/ProjectService.cs index f89dca5..614d0de 100644 --- a/YD_Event.Application/Project/ProjectService.cs +++ b/YD_Event.Application/Project/ProjectService.cs @@ -70,19 +70,6 @@ public class ProjectService : BaseService, ITran }) .ToPagedListAsync(input); - var projectIds = result.Rows.Select(x => x.Id).ToList(); - if (projectIds.Any()) - { - var stats = await GetProjectStatisticsBatchAsync(projectIds, ProjectStatisticType.CompletionCount); - foreach (var item in result.Rows) - { - if (stats.TryGetValue(item.Id, out var count)) - { - item.CompletionCount = count; - } - } - } - return result; } @@ -181,8 +168,6 @@ public class ProjectService : BaseService, ITran var groups = await _groupRepository.GetListAsync(x => x.ProjectId == id); dto.Groups = groups.Adapt>(); - - dto.CompletionCount = await GetProjectStatisticAsync(id, ProjectStatisticType.CompletionCount); return dto; } diff --git a/YD_Event.Application/YD_Event.Application.csproj b/YD_Event.Application/YD_Event.Application.csproj index 5e22c1c..7c79515 100644 --- a/YD_Event.Application/YD_Event.Application.csproj +++ b/YD_Event.Application/YD_Event.Application.csproj @@ -8,10 +8,13 @@ + + + @@ -25,7 +28,6 @@ - diff --git a/YD_Event.Core/Utils/AmountHelper.cs b/YD_Event.Core/Utils/AmountHelper.cs new file mode 100644 index 0000000..24bc8d4 --- /dev/null +++ b/YD_Event.Core/Utils/AmountHelper.cs @@ -0,0 +1,79 @@ +namespace YD_Event.Core.Utils; + +/// +/// 金额处理工具类 +/// 统一处理货币单位转换、精度控制和四舍五入规则 +/// +public static class AmountHelper +{ + /// + /// 金额精度小数位数 + /// + private const int DecimalPlaces = 2; + + /// + /// 将元转换为分 + /// + /// 金额(元) + /// 金额(分) + public static int YuanToFen(decimal amount) + { + // 使用四舍五入到最近的整数,使用银行家舍入法(四舍六入五成双) + // 这里先确保精度,然后乘以100,再四舍五入 + var roundedAmount = Math.Round(amount, DecimalPlaces, MidpointRounding.AwayFromZero); + return (int)Math.Round(roundedAmount * 100, MidpointRounding.AwayFromZero); + } + + /// + /// 将分转换为元 + /// + /// 金额(分) + /// 金额(元) + public static decimal FenToYuan(int amount) + { + return amount / 100m; + } + + /// + /// 将分转换为元(可为空) + /// + /// 金额(分) + /// 金额(元) + public static decimal? FenToYuan(int? amount) + { + if (!amount.HasValue) return null; + return amount.Value / 100m; + } + + /// + /// 四舍五入金额到指定小数位 + /// + /// 金额 + /// 小数位数,默认2位 + /// 四舍五入后的金额 + public static decimal Round(decimal amount, int decimalPlaces = DecimalPlaces) + { + return Math.Round(amount, decimalPlaces, MidpointRounding.AwayFromZero); + } + + /// + /// 验证金额格式是否正确(最多保留2位小数) + /// + /// 金额 + /// 是否有效 + public static bool IsValidAmount(decimal amount) + { + var rounded = Math.Round(amount, DecimalPlaces, MidpointRounding.AwayFromZero); + return rounded == amount; + } + + /// + /// 验证金额是否大于0且格式正确 + /// + /// 金额 + /// 是否有效 + public static bool IsValidPositiveAmount(decimal amount) + { + return amount > 0 && IsValidAmount(amount); + } +} diff --git a/YD_Event.Core/YD_Event.Core.xml b/YD_Event.Core/YD_Event.Core.xml index 2961e16..1845ec7 100644 --- a/YD_Event.Core/YD_Event.Core.xml +++ b/YD_Event.Core/YD_Event.Core.xml @@ -5811,6 +5811,60 @@ 子选项 + + + 金额处理工具类 + 统一处理货币单位转换、精度控制和四舍五入规则 + + + + + 金额精度小数位数 + + + + + 将元转换为分 + + 金额(元) + 金额(分) + + + + 将分转换为元 + + 金额(分) + 金额(元) + + + + 将分转换为元(可为空) + + 金额(分) + 金额(元) + + + + 四舍五入金额到指定小数位 + + 金额 + 小数位数,默认2位 + 四舍五入后的金额 + + + + 验证金额格式是否正确(最多保留2位小数) + + 金额 + 是否有效 + + + + 验证金额是否大于0且格式正确 + + 金额 + 是否有效 + 缓存