feat(order): 增加 IsPaid 字段并修复列表状态筛选

- t_order 新增 IsPaid,支付/出库以该字段判断;含迁移脚本

- 列表接口补充 productName、channelName

- 修复 BuildOrderPredicate 状态条件 SqlSugar 翻译丢失

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-06-27 19:13:15 +08:00
parent e7fd69b644
commit 095ad66717
10 changed files with 131 additions and 13 deletions

1
db.sql
View File

@ -73,6 +73,7 @@ CREATE TABLE `t_order` (
`ReceiveTime` DATETIME NULL COMMENT '收货时间',
`ShipTime` DATETIME NULL COMMENT '发货时间',
`PayTime` DATETIME NULL COMMENT '支付时间',
`IsPaid` TINYINT NOT NULL DEFAULT 0 COMMENT '是否已支付:0-否,1-是',
`SignTime` DATETIME NULL COMMENT '签收时间',
`IsCod` TINYINT NOT NULL DEFAULT 0 COMMENT '是否COD:0-否,1-是',
`CodCurrency` VARCHAR(16) NULL COMMENT 'COD币种',

View File

@ -0,0 +1,7 @@
-- 订单表增加是否已支付字段,并按历史 PayTime 回填
ALTER TABLE `t_order`
ADD COLUMN `IsPaid` TINYINT NOT NULL DEFAULT 0 COMMENT '是否已支付:0-否,1-是' AFTER `PayTime`;
UPDATE `t_order`
SET `IsPaid` = 1
WHERE `PayTime` IS NOT NULL;

View File

@ -30,11 +30,21 @@ public class OrderOutput
/// </summary>
public string? ProductCode { get; set; }
/// <summary>
/// 销售产品名称。
/// </summary>
public string? ProductName { get; set; }
/// <summary>
/// 渠道编码。
/// </summary>
public string? ChannelCode { get; set; }
/// <summary>
/// 服务渠道名称。
/// </summary>
public string? ChannelName { get; set; }
/// <summary>
/// 货物属性。
/// </summary>
@ -95,6 +105,11 @@ public class OrderOutput
/// </summary>
public DateTime? PayTime { get; set; }
/// <summary>
/// 是否已支付。
/// </summary>
public bool IsPaid { get; set; }
/// <summary>
/// 签收时间。
/// </summary>

View File

@ -21,6 +21,7 @@ public class OrderPaginatedInput
/// 状态:all 或 OrderStatusCodes 枚举值。
/// </summary>
[Newtonsoft.Json.JsonProperty("status")]
[global::System.Text.Json.Serialization.JsonPropertyName("status")]
public string? Status { get; set; }
/// <summary>

View File

@ -80,6 +80,11 @@ public class OrderPaginatedOutput
/// </summary>
public DateTime? PayTime { get; set; }
/// <summary>
/// 是否已支付:0-否,1-是。
/// </summary>
public byte IsPaid { get; set; }
/// <summary>
/// 签收时间。
/// </summary>

View File

@ -8,6 +8,7 @@ using Oms.Domain.ChannelsAggregate.Repositories;
using Oms.Domain.OperationsAggregate.Entities;
using Oms.Domain.OperationsAggregate.Repositories;
using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Entities;
using Oms.Domain.OrdersAggregate.Repositories;
using Oms.Domain.SystemAggregate.Repositories;
@ -45,7 +46,7 @@ public class OutboundRecordService : ApplicationService
throw new BusinessException("未找到对应订单,请检查单号");
}
EnsureOutboundAllowed(order.OrderStatus);
EnsureOutboundAllowed(order);
var output = new OutboundOrderLookupOutput
{
@ -114,7 +115,7 @@ public class OutboundRecordService : ApplicationService
throw new BusinessException("未找到对应订单,请检查单号");
}
EnsureOutboundAllowed(order.OrderStatus);
EnsureOutboundAllowed(order);
int? salesProductId = input.SalesProductId;
if (!salesProductId.HasValue && !string.IsNullOrWhiteSpace(order.ProductCode))
@ -226,13 +227,14 @@ public class OutboundRecordService : ApplicationService
return new OmsItemsResult<OutboundRecordOutput> { Items = items };
}
private static void EnsureOutboundAllowed(string orderStatus)
private static void EnsureOutboundAllowed(Order order)
{
if (orderStatus is OrderStatusCodes.PendingPayment or OrderStatusCodes.Received)
if (order.IsPaid == 0)
{
throw new BusinessException("订单未付款,禁止出库");
}
var orderStatus = order.OrderStatus;
if (orderStatus != OrderStatusCodes.PendingShip)
{
if (orderStatus is OrderStatusCodes.PendingOnline or OrderStatusCodes.Shipping

View File

@ -39,6 +39,25 @@ public partial class OrderService
var feeMap = fees.GroupBy(x => x.OrderId).ToDictionary(x => x.Key, x => x.ToList());
var warehouseMap = warehouses.ToDictionary(x => x.Id);
var productCodes = orders
.Where(x => !string.IsNullOrWhiteSpace(x.ProductCode))
.Select(x => x.ProductCode!.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
var channelCodes = orders
.Where(x => !string.IsNullOrWhiteSpace(x.ChannelCode))
.Select(x => x.ChannelCode!.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
var products = productCodes.Length == 0
? []
: await SalesProductRepository.GetListAsync(x => productCodes.Contains(x.ProductCode));
var channels = channelCodes.Length == 0
? []
: await ServiceChannelRepository.GetListAsync(x => channelCodes.Contains(x.ChannelCode));
var productMap = products.ToDictionary(x => x.ProductCode, StringComparer.OrdinalIgnoreCase);
var channelMap = channels.ToDictionary(x => x.ChannelCode, StringComparer.OrdinalIgnoreCase);
return orders.Select(order =>
{
senderMap.TryGetValue(order.Id, out var sender);
@ -58,7 +77,15 @@ public partial class OrderService
TrackNo = order.TrackNo,
ServiceNo = order.ServiceNo,
ProductCode = order.ProductCode,
ProductName = !string.IsNullOrWhiteSpace(order.ProductCode)
&& productMap.TryGetValue(order.ProductCode.Trim(), out var product)
? product.ProductName
: null,
ChannelCode = order.ChannelCode,
ChannelName = !string.IsNullOrWhiteSpace(order.ChannelCode)
&& channelMap.TryGetValue(order.ChannelCode.Trim(), out var channel)
? channel.ChannelName
: null,
CargoAttribute = order.CargoAttribute,
WarehouseId = order.WarehouseId,
Warehouse = order.WarehouseId.HasValue && warehouseMap.TryGetValue(order.WarehouseId.Value, out var warehouse)
@ -73,6 +100,7 @@ public partial class OrderService
ReceiveTime = order.ReceiveTime,
ShipTime = order.ShipTime,
PayTime = order.PayTime,
IsPaid = order.IsPaid > 0,
SignTime = order.SignTime,
IsIntercept = order.IsIntercept > 0,
IsRemote = order.IsRemote > 0,
@ -101,7 +129,9 @@ public partial class OrderService
x.TrackNo,
x.ServiceNo,
x.ProductCode,
x.ProductName,
x.ChannelCode,
x.ChannelName,
x.CargoAttribute,
x.WarehouseId,
x.Warehouse,
@ -111,6 +141,7 @@ public partial class OrderService
x.ReceiveTime,
x.ShipTime,
x.PayTime,
x.IsPaid,
x.SignTime,
x.IsCod,
x.CodCurrency,

View File

@ -1316,7 +1316,7 @@ public partial class OrderService
throw new BusinessException("订单未绑定销售产品,无法出库计费");
}
if (order.OrderStatus is OrderStatusCodes.PendingPayment or OrderStatusCodes.Received)
if (order.IsPaid == 0)
{
throw new BusinessException("订单未付款,禁止出库");
}

View File

@ -114,7 +114,7 @@ public partial class OrderService : ApplicationService
public async Task PayAsync(OrderActionInput input)
{
var order = await GetOrderOrThrowAsync(input.Id);
if (order.OrderStatus == OrderStatusCodes.PendingShip && order.PayTime.HasValue)
if (order.IsPaid > 0)
{
throw new BusinessException("订单已支付");
}
@ -153,6 +153,7 @@ public partial class OrderService : ApplicationService
$"订单支付 {order.ServiceNo}");
order.PayTime = DateTime.Now;
order.IsPaid = 1;
order.OrderStatus = OrderStatusCodes.PendingShip;
await UpdateOrderAsync(order);
}
@ -523,7 +524,8 @@ public partial class OrderService : ApplicationService
/// 1. 先把状态和关键词做统一清洗;
/// 2. 再按时间区间是否存在拆分成不同表达式;
/// 3. 避免把 <see cref="Nullable{T}.HasValue"/> / <see cref="Nullable{T}.Value"/> 放进可翻译 SQL 的表达式中,
/// 从而规避 MySQL 下时间条件被错误翻译的问题。
/// 从而规避 MySQL 下时间条件被错误翻译的问题;
/// 4. 状态筛选同样显式分支,避免 <c>statusValue == null || order.OrderStatus == statusValue</c> 被 SqlSugar 误译。
/// </remarks>
private static Expression<Func<Order, bool>> BuildOrderPredicate(
string? status,
@ -534,21 +536,35 @@ public partial class OrderService : ApplicationService
{
status = status?.Trim();
keyword = keyword?.Trim();
var statusValue = !string.IsNullOrWhiteSpace(status) && !string.Equals(status, "all", StringComparison.OrdinalIgnoreCase)
var orderStatusFilter = !string.IsNullOrWhiteSpace(status) && !string.Equals(status, "all", StringComparison.OrdinalIgnoreCase)
? status
: null;
var hasStatusFilter = orderStatusFilter != null;
var hasKeywordFilter = !string.IsNullOrWhiteSpace(keyword);
var hasCustomerFilter = customerId.HasValue;
var customerIdValue = customerId.GetValueOrDefault();
// 显式拆开时间分支,避免 SqlSugar 在翻译 Nullable<DateTime> 判空时生成异常 SQL。
// 状态与时间均显式分支,避免 SqlSugar 翻译「闭包变量 == null || 字段 == 变量」时丢失条件。
if (createTimeStart.HasValue && createTimeEnd.HasValue)
{
var start = createTimeStart.Value;
var end = createTimeEnd.Value;
if (hasStatusFilter)
{
var statusFilter = orderStatusFilter!;
return order =>
(!hasCustomerFilter || order.CustomerId == customerIdValue) &&
order.OrderStatus == statusFilter &&
(!hasKeywordFilter ||
order.ServiceNo.Contains(keyword!) ||
(order.ClientNo ?? string.Empty).Contains(keyword!) ||
(order.TrackNo ?? string.Empty).Contains(keyword!)) &&
order.CreateTime >= start &&
order.CreateTime <= end;
}
return order =>
(!hasCustomerFilter || order.CustomerId == customerIdValue) &&
(statusValue == null || order.OrderStatus == statusValue) &&
(!hasKeywordFilter ||
order.ServiceNo.Contains(keyword!) ||
(order.ClientNo ?? string.Empty).Contains(keyword!) ||
@ -560,9 +576,21 @@ public partial class OrderService : ApplicationService
if (createTimeStart.HasValue)
{
var start = createTimeStart.Value;
if (hasStatusFilter)
{
var statusFilter = orderStatusFilter!;
return order =>
(!hasCustomerFilter || order.CustomerId == customerIdValue) &&
order.OrderStatus == statusFilter &&
(!hasKeywordFilter ||
order.ServiceNo.Contains(keyword!) ||
(order.ClientNo ?? string.Empty).Contains(keyword!) ||
(order.TrackNo ?? string.Empty).Contains(keyword!)) &&
order.CreateTime >= start;
}
return order =>
(!hasCustomerFilter || order.CustomerId == customerIdValue) &&
(statusValue == null || order.OrderStatus == statusValue) &&
(!hasKeywordFilter ||
order.ServiceNo.Contains(keyword!) ||
(order.ClientNo ?? string.Empty).Contains(keyword!) ||
@ -573,9 +601,12 @@ public partial class OrderService : ApplicationService
if (createTimeEnd.HasValue)
{
var end = createTimeEnd.Value;
if (hasStatusFilter)
{
var statusFilter = orderStatusFilter!;
return order =>
(!hasCustomerFilter || order.CustomerId == customerIdValue) &&
(statusValue == null || order.OrderStatus == statusValue) &&
order.OrderStatus == statusFilter &&
(!hasKeywordFilter ||
order.ServiceNo.Contains(keyword!) ||
(order.ClientNo ?? string.Empty).Contains(keyword!) ||
@ -585,7 +616,27 @@ public partial class OrderService : ApplicationService
return order =>
(!hasCustomerFilter || order.CustomerId == customerIdValue) &&
(statusValue == null || order.OrderStatus == statusValue) &&
(!hasKeywordFilter ||
order.ServiceNo.Contains(keyword!) ||
(order.ClientNo ?? string.Empty).Contains(keyword!) ||
(order.TrackNo ?? string.Empty).Contains(keyword!)) &&
order.CreateTime <= end;
}
if (hasStatusFilter)
{
var statusFilter = orderStatusFilter!;
return order =>
(!hasCustomerFilter || order.CustomerId == customerIdValue) &&
order.OrderStatus == statusFilter &&
(!hasKeywordFilter ||
order.ServiceNo.Contains(keyword!) ||
(order.ClientNo ?? string.Empty).Contains(keyword!) ||
(order.TrackNo ?? string.Empty).Contains(keyword!));
}
return order =>
(!hasCustomerFilter || order.CustomerId == customerIdValue) &&
(!hasKeywordFilter ||
order.ServiceNo.Contains(keyword!) ||
(order.ClientNo ?? string.Empty).Contains(keyword!) ||

View File

@ -48,6 +48,11 @@ public class Order : OmsAuditedAggregateRoot
public DateTime? PayTime { get; set; }
/// <summary>
/// 是否已支付:0-否,1-是。
/// </summary>
public byte IsPaid { get; set; }
public DateTime? SignTime { get; set; }
public byte IsCod { get; set; }