订单管理

This commit is contained in:
马梓彭 2026-04-09 20:49:36 +08:00
parent a9a0ecf0d5
commit daa5f7b973
10 changed files with 305 additions and 73 deletions

View File

@ -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<SysDistrict> _districtRepository;
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Order> _orderRepository;
private readonly ISqlSugarRepository<MerchantCertification> _merchantCertificationRepository;
private readonly ISqlSugarRepository<EventRegistrationPayment> _paymentRepository;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IIdGenerator _idGenerator;
private readonly WeChatPayService _weChatPayService;
@ -79,6 +81,7 @@ public class EventClientController : IDynamicApiController
ISqlSugarRepository<SysDistrict> districtRepository,
ISqlSugarRepository<YD_Event.Core.Entities.Order> orderRepository,
ISqlSugarRepository<MerchantCertification> merchantCertificationRepository,
ISqlSugarRepository<EventRegistrationPayment> 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)

View File

@ -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<EventInfo>, 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<EventInfo>, 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<EventInfo>, 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<EventInfo>, 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<EventInfo>, 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<EventInfo>, 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);
}

View File

@ -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
};

View File

@ -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)
};

View File

@ -0,0 +1,18 @@
using System.ComponentModel.DataAnnotations;
namespace YD_Event.Application.Order.Dtos;
public class UpdatePaymentStatusInput
{
/// <summary>
/// 订单ID列表(批量更新)
/// </summary>
[Required]
public List<long> Ids { get; set; }
/// <summary>
/// 支付状态:0-未支付,1-已支付,3-已退款
/// </summary>
[Required]
public int PaymentStatus { get; set; }
}

View File

@ -136,7 +136,7 @@ public class OrderService : BaseService<YD_Event.Core.Entities.Order>, 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<YD_Event.Core.Entities.Order>, 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<YD_Event.Core.Entities.Order>, ITransien
}
/// <summary>
/// 更新订单状态
/// 批量更新订单支付状态
/// </summary>
[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<YD_Event.Core.Entities.Order>, 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<YD_Event.Core.Entities.Order>, 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);
}
}
/// <summary>
/// 删除订单
/// 删除订单(软删除)
/// </summary>
[DisplayName("删除订单")]
[HttpPost("delete")]
public async Task Delete([FromBody] List<long> 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);
}
}
/// <summary>
@ -415,7 +484,7 @@ public class OrderService : BaseService<YD_Event.Core.Entities.Order>, 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,

View File

@ -70,19 +70,6 @@ public class ProjectService : BaseService<YD_Event.Core.Entities.Project>, 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<YD_Event.Core.Entities.Project>, ITran
var groups = await _groupRepository.GetListAsync(x => x.ProjectId == id);
dto.Groups = groups.Adapt<List<ProjectGroupDto>>();
dto.CompletionCount = await GetProjectStatisticAsync(id, ProjectStatisticType.CompletionCount);
return dto;
}

View File

@ -8,10 +8,13 @@
<ItemGroup>
<Compile Remove="Blog\**" />
<Compile Remove="Config\**" />
<Compile Remove="File\**" />
<EmbeddedResource Remove="Blog\**" />
<EmbeddedResource Remove="Config\**" />
<EmbeddedResource Remove="File\**" />
<None Remove="Blog\**" />
<None Remove="Config\**" />
<None Remove="File\**" />
</ItemGroup>
<ItemGroup>
<None Remove="applicationsettings.json" />
@ -25,7 +28,6 @@
<ItemGroup>
<Folder Include="Auth\Dtos\" />
<Folder Include="Certificate\Dtos\" />
<Folder Include="File\Dtos\" />
<Folder Include="Logging\Dtos\" />
<Folder Include="Menu\Dtos\" />
<Folder Include="Organization\Dtos\" />

View File

@ -0,0 +1,79 @@
namespace YD_Event.Core.Utils;
/// <summary>
/// 金额处理工具类
/// 统一处理货币单位转换、精度控制和四舍五入规则
/// </summary>
public static class AmountHelper
{
/// <summary>
/// 金额精度小数位数
/// </summary>
private const int DecimalPlaces = 2;
/// <summary>
/// 将元转换为分
/// </summary>
/// <param name="amount">金额(元)</param>
/// <returns>金额(分)</returns>
public static int YuanToFen(decimal amount)
{
// 使用四舍五入到最近的整数,使用银行家舍入法(四舍六入五成双)
// 这里先确保精度,然后乘以100,再四舍五入
var roundedAmount = Math.Round(amount, DecimalPlaces, MidpointRounding.AwayFromZero);
return (int)Math.Round(roundedAmount * 100, MidpointRounding.AwayFromZero);
}
/// <summary>
/// 将分转换为元
/// </summary>
/// <param name="amount">金额(分)</param>
/// <returns>金额(元)</returns>
public static decimal FenToYuan(int amount)
{
return amount / 100m;
}
/// <summary>
/// 将分转换为元(可为空)
/// </summary>
/// <param name="amount">金额(分)</param>
/// <returns>金额(元)</returns>
public static decimal? FenToYuan(int? amount)
{
if (!amount.HasValue) return null;
return amount.Value / 100m;
}
/// <summary>
/// 四舍五入金额到指定小数位
/// </summary>
/// <param name="amount">金额</param>
/// <param name="decimalPlaces">小数位数,默认2位</param>
/// <returns>四舍五入后的金额</returns>
public static decimal Round(decimal amount, int decimalPlaces = DecimalPlaces)
{
return Math.Round(amount, decimalPlaces, MidpointRounding.AwayFromZero);
}
/// <summary>
/// 验证金额格式是否正确(最多保留2位小数)
/// </summary>
/// <param name="amount">金额</param>
/// <returns>是否有效</returns>
public static bool IsValidAmount(decimal amount)
{
var rounded = Math.Round(amount, DecimalPlaces, MidpointRounding.AwayFromZero);
return rounded == amount;
}
/// <summary>
/// 验证金额是否大于0且格式正确
/// </summary>
/// <param name="amount">金额</param>
/// <returns>是否有效</returns>
public static bool IsValidPositiveAmount(decimal amount)
{
return amount > 0 && IsValidAmount(amount);
}
}

View File

@ -5811,6 +5811,60 @@
子选项
</summary>
</member>
<member name="T:YD_Event.Core.Utils.AmountHelper">
<summary>
金额处理工具类
统一处理货币单位转换、精度控制和四舍五入规则
</summary>
</member>
<member name="F:YD_Event.Core.Utils.AmountHelper.DecimalPlaces">
<summary>
金额精度小数位数
</summary>
</member>
<member name="M:YD_Event.Core.Utils.AmountHelper.YuanToFen(System.Decimal)">
<summary>
将元转换为分
</summary>
<param name="amount">金额(元)</param>
<returns>金额(分)</returns>
</member>
<member name="M:YD_Event.Core.Utils.AmountHelper.FenToYuan(System.Int32)">
<summary>
将分转换为元
</summary>
<param name="amount">金额(分)</param>
<returns>金额(元)</returns>
</member>
<member name="M:YD_Event.Core.Utils.AmountHelper.FenToYuan(System.Nullable{System.Int32})">
<summary>
将分转换为元(可为空)
</summary>
<param name="amount">金额(分)</param>
<returns>金额(元)</returns>
</member>
<member name="M:YD_Event.Core.Utils.AmountHelper.Round(System.Decimal,System.Int32)">
<summary>
四舍五入金额到指定小数位
</summary>
<param name="amount">金额</param>
<param name="decimalPlaces">小数位数,默认2位</param>
<returns>四舍五入后的金额</returns>
</member>
<member name="M:YD_Event.Core.Utils.AmountHelper.IsValidAmount(System.Decimal)">
<summary>
验证金额格式是否正确(最多保留2位小数)
</summary>
<param name="amount">金额</param>
<returns>是否有效</returns>
</member>
<member name="M:YD_Event.Core.Utils.AmountHelper.IsValidPositiveAmount(System.Decimal)">
<summary>
验证金额是否大于0且格式正确
</summary>
<param name="amount">金额</param>
<returns>是否有效</returns>
</member>
<member name="F:Easy.Core.EnumExtension.EnumDic">
<summary>
缓存