diff --git a/db.sql b/db.sql
index 5cabbb9..cf0b316 100644
--- a/db.sql
+++ b/db.sql
@@ -490,6 +490,7 @@ CREATE TABLE `t_order_exception_event` (
`ClaimReason` VARCHAR(512) NULL COMMENT '理赔原因',
`Remark` VARCHAR(512) NULL COMMENT '操作备注',
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是',
+ `ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()) COMMENT '并发戳',
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '操作人',
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
`UpdateUserId` INT NULL COMMENT '更新人',
diff --git a/sql/migrate_t_order_exception_event_handler.sql b/sql/migrate_t_order_exception_event_handler.sql
new file mode 100644
index 0000000..96e6284
--- /dev/null
+++ b/sql/migrate_t_order_exception_event_handler.sql
@@ -0,0 +1,118 @@
+-- 订单异常事件表:补齐 Handler* / ConcurrencyStamp(幂等;列已存在则自动跳过)
+-- 修复:Unknown column 'HandlerNames' / 'ConcurrencyStamp' in 'field list'
+-- MySQL 5.7+ / 8.0
+
+SET @db := DATABASE();
+
+-- HandlerNames
+SET @col := (
+ SELECT COUNT(*)
+ FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = @db
+ AND TABLE_NAME = 't_order_exception_event'
+ AND COLUMN_NAME = 'HandlerNames'
+);
+SET @sql := IF(
+ @col = 0,
+ 'ALTER TABLE `t_order_exception_event` ADD COLUMN `HandlerNames` VARCHAR(256) NULL COMMENT ''处理人快照''',
+ 'SELECT ''skip HandlerNames'' AS msg'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- HandlerType
+SET @col := (
+ SELECT COUNT(*)
+ FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = @db
+ AND TABLE_NAME = 't_order_exception_event'
+ AND COLUMN_NAME = 'HandlerType'
+);
+SET @sql := IF(
+ @col = 0,
+ 'ALTER TABLE `t_order_exception_event` ADD COLUMN `HandlerType` VARCHAR(16) NULL COMMENT ''处理人类型''',
+ 'SELECT ''skip HandlerType'' AS msg'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- HandlerUserId
+SET @col := (
+ SELECT COUNT(*)
+ FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = @db
+ AND TABLE_NAME = 't_order_exception_event'
+ AND COLUMN_NAME = 'HandlerUserId'
+);
+SET @sql := IF(
+ @col = 0,
+ 'ALTER TABLE `t_order_exception_event` ADD COLUMN `HandlerUserId` INT NULL COMMENT ''处理人用户Id(customer 时为订单客户账号)''',
+ 'SELECT ''skip HandlerUserId'' AS msg'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- ConcurrencyStamp(AggregateRoot 并发戳;避免 DEFAULT(UUID()) 触发 1674 复制不安全)
+SET @col := (
+ SELECT COUNT(*)
+ FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = @db
+ AND TABLE_NAME = 't_order_exception_event'
+ AND COLUMN_NAME = 'ConcurrencyStamp'
+);
+SET @sql := IF(
+ @col = 0,
+ 'ALTER TABLE `t_order_exception_event` ADD COLUMN `ConcurrencyStamp` CHAR(36) NULL COMMENT ''并发戳''',
+ 'SELECT ''skip ConcurrencyStamp add'' AS msg'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+SET @col := (
+ SELECT COUNT(*)
+ FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = @db
+ AND TABLE_NAME = 't_order_exception_event'
+ AND COLUMN_NAME = 'ConcurrencyStamp'
+);
+SET @sql := IF(
+ @col > 0,
+ 'UPDATE `t_order_exception_event`
+ SET `ConcurrencyStamp` = LOWER(CONCAT(
+ LPAD(HEX(`Id`), 8, ''0''),
+ ''-0000-4000-8000-'',
+ LPAD(HEX(`Id` * 9973), 12, ''0'')
+ ))
+ WHERE `ConcurrencyStamp` IS NULL OR `ConcurrencyStamp` = ''''',
+ 'SELECT ''skip ConcurrencyStamp backfill'' AS msg'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+SET @col := (
+ SELECT COUNT(*)
+ FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = @db
+ AND TABLE_NAME = 't_order_exception_event'
+ AND COLUMN_NAME = 'ConcurrencyStamp'
+);
+SET @isNullable := (
+ SELECT IFNULL(MAX(CASE WHEN IS_NULLABLE = 'YES' THEN 1 ELSE 0 END), 0)
+ FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = @db
+ AND TABLE_NAME = 't_order_exception_event'
+ AND COLUMN_NAME = 'ConcurrencyStamp'
+);
+SET @sql := IF(
+ @col > 0 AND @isNullable = 1,
+ 'ALTER TABLE `t_order_exception_event` MODIFY COLUMN `ConcurrencyStamp` CHAR(36) NOT NULL COMMENT ''并发戳''',
+ 'SELECT ''skip ConcurrencyStamp not null'' AS msg'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
diff --git a/sql/migrate_t_order_fulfillment_type.sql b/sql/migrate_t_order_fulfillment_type.sql
index 60d3fc3..ff3cd2c 100644
--- a/sql/migrate_t_order_fulfillment_type.sql
+++ b/sql/migrate_t_order_fulfillment_type.sql
@@ -1,14 +1,58 @@
--- 代发订单:t_order 履约类型字段(逐条执行;若列已存在请跳过对应 ALTER)
+-- 代发订单:t_order 履约类型字段(幂等;列/索引已存在则自动跳过)
-- MySQL 5.7+ / 8.0
-ALTER TABLE `t_order`
- ADD COLUMN `FulfillmentType` VARCHAR(16) NULL DEFAULT 'direct' COMMENT '履约类型 direct/warehouse' AFTER `AdditionalServices`;
+SET @db := DATABASE();
-ALTER TABLE `t_order`
- ADD COLUMN `FulfillmentSource` VARCHAR(32) NULL COMMENT '代发来源 storeFulfill/postBind' AFTER `FulfillmentType`;
+-- FulfillmentType
+SET @col := (
+ SELECT COUNT(*)
+ FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = @db
+ AND TABLE_NAME = 't_order'
+ AND COLUMN_NAME = 'FulfillmentType'
+);
+SET @sql := IF(
+ @col = 0,
+ 'ALTER TABLE `t_order` ADD COLUMN `FulfillmentType` VARCHAR(16) NULL DEFAULT ''direct'' COMMENT ''履约类型 direct/warehouse'' AFTER `AdditionalServices`',
+ 'SELECT ''skip FulfillmentType'' AS msg'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
-ALTER TABLE `t_order`
- ADD KEY `idx_ord_fulfillment_customer` (`FulfillmentType`, `CustomerId`, `OrderStatus`);
+-- FulfillmentSource
+SET @col := (
+ SELECT COUNT(*)
+ FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = @db
+ AND TABLE_NAME = 't_order'
+ AND COLUMN_NAME = 'FulfillmentSource'
+);
+SET @sql := IF(
+ @col = 0,
+ 'ALTER TABLE `t_order` ADD COLUMN `FulfillmentSource` VARCHAR(32) NULL COMMENT ''代发来源 storeFulfill/postBind'' AFTER `FulfillmentType`',
+ 'SELECT ''skip FulfillmentSource'' AS msg'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- idx_ord_fulfillment_customer
+SET @idx := (
+ SELECT COUNT(*)
+ FROM information_schema.STATISTICS
+ WHERE TABLE_SCHEMA = @db
+ AND TABLE_NAME = 't_order'
+ AND INDEX_NAME = 'idx_ord_fulfillment_customer'
+);
+SET @sql := IF(
+ @idx = 0,
+ 'ALTER TABLE `t_order` ADD KEY `idx_ord_fulfillment_customer` (`FulfillmentType`, `CustomerId`, `OrderStatus`)',
+ 'SELECT ''skip idx_ord_fulfillment_customer'' AS msg'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
-- 历史:已有绑定行的订单标记为 warehouse
UPDATE `t_order` o
diff --git a/src/Oms.Application/Dtos/Orders/OrderExportInput.cs b/src/Oms.Application/Dtos/Orders/OrderExportInput.cs
index afcfd84..cfb9101 100644
--- a/src/Oms.Application/Dtos/Orders/OrderExportInput.cs
+++ b/src/Oms.Application/Dtos/Orders/OrderExportInput.cs
@@ -23,6 +23,13 @@ public class OrderExportInput
[Newtonsoft.Json.JsonProperty("fulfillmentType")]
public string? FulfillmentType { get; set; }
+ ///
+ /// 代发进度筛选(pendingBind / domesticInTransit / arrivedPendingPick / outboundDone)。
+ ///
+ [Newtonsoft.Json.JsonProperty("fulfillProgress")]
+ [global::System.Text.Json.Serialization.JsonPropertyName("fulfillProgress")]
+ public string? FulfillProgress { get; set; }
+
///
/// 关键词。
///
diff --git a/src/Oms.Application/Dtos/Orders/OrderItemOutput.cs b/src/Oms.Application/Dtos/Orders/OrderItemOutput.cs
index 6b4c374..c7dba00 100644
--- a/src/Oms.Application/Dtos/Orders/OrderItemOutput.cs
+++ b/src/Oms.Application/Dtos/Orders/OrderItemOutput.cs
@@ -51,10 +51,20 @@ public class OrderItemOutput
public decimal? Weight { get; set; }
///
- /// HS 编码。
+ /// HS 编码(兼容旧字段,优先出口编码)。
///
public string? HsCode { get; set; }
+ ///
+ /// 进口 HS 编码。
+ ///
+ public string? ImHsCode { get; set; }
+
+ ///
+ /// 出口 HS 编码。
+ ///
+ public string? ExHsCode { get; set; }
+
///
/// SKU 图片。
///
diff --git a/src/Oms.Application/Dtos/Orders/OrderOutput.cs b/src/Oms.Application/Dtos/Orders/OrderOutput.cs
index 39d6479..47b72f4 100644
--- a/src/Oms.Application/Dtos/Orders/OrderOutput.cs
+++ b/src/Oms.Application/Dtos/Orders/OrderOutput.cs
@@ -175,6 +175,11 @@ public class OrderOutput
///
public string? PrimaryBinCode { get; set; }
+ ///
+ /// 绑定 SKU 编码汇总(逗号分隔,列表展示/拣货筛选用)。
+ ///
+ public string? PrimarySkuCodes { get; set; }
+
///
/// 关联揽收单 Id。
///
diff --git a/src/Oms.Application/Dtos/Orders/OrderStatusCountsOutput.cs b/src/Oms.Application/Dtos/Orders/OrderStatusCountsOutput.cs
index 9568dd2..60dfc49 100644
--- a/src/Oms.Application/Dtos/Orders/OrderStatusCountsOutput.cs
+++ b/src/Oms.Application/Dtos/Orders/OrderStatusCountsOutput.cs
@@ -20,4 +20,10 @@ public class OrderStatusCountsOutput
///
public Dictionary HandleCounts { get; set; } =
new(StringComparer.OrdinalIgnoreCase);
+
+ ///
+ /// 代发进度分组数量(fulfillmentType=warehouse 时返回)。
+ ///
+ public Dictionary FulfillProgressCounts { get; set; } =
+ new(StringComparer.OrdinalIgnoreCase);
}
diff --git a/src/Oms.Application/Services/Orders/OrderService.Helpers.cs b/src/Oms.Application/Services/Orders/OrderService.Helpers.cs
index d59fe8f..8b5ea91 100644
--- a/src/Oms.Application/Services/Orders/OrderService.Helpers.cs
+++ b/src/Oms.Application/Services/Orders/OrderService.Helpers.cs
@@ -187,6 +187,7 @@ public partial class OrderService
bindSummary?.LineCount ?? 0,
bindSummary?.HasInTransitParcel ?? false),
PrimaryBinCode = bindSummary?.PrimaryBinCode,
+ PrimarySkuCodes = bindSummary?.PrimarySkuCodes,
PickupOrderId = order.PickupOrderId,
PickupNo = order.PickupOrderId is > 0
&& pickupNoMap.TryGetValue(order.PickupOrderId.Value, out var pickupNo)
@@ -280,6 +281,8 @@ public partial class OrderService
public bool HasInTransitParcel { get; init; }
public string? PrimaryBinCode { get; init; }
+
+ public string? PrimarySkuCodes { get; init; }
}
private async Task> LoadStockBindListSummariesAsync(int[] orderIds)
@@ -342,6 +345,12 @@ public partial class OrderService
.Where(code => !string.IsNullOrWhiteSpace(code))
.OrderBy(code => code, StringComparer.OrdinalIgnoreCase)
.FirstOrDefault();
+ var skuCodes = rows
+ .Select(row => row.SkuCode?.Trim())
+ .Where(code => !string.IsNullOrWhiteSpace(code))
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .OrderBy(code => code, StringComparer.OrdinalIgnoreCase)
+ .ToArray();
return new StockBindListSummary
{
BindStatus = bindStatus,
@@ -349,7 +358,8 @@ public partial class OrderService
QtyTotal = rows.Sum(x => x.BindQty),
TrackingNosText = trackingNos.Length == 0 ? null : string.Join(", ", trackingNos),
HasInTransitParcel = hasInTransitParcel,
- PrimaryBinCode = primaryBinCode
+ PrimaryBinCode = primaryBinCode,
+ PrimarySkuCodes = skuCodes.Length == 0 ? null : string.Join(", ", skuCodes),
};
});
}
@@ -732,6 +742,8 @@ public partial class OrderService
Amount = x.Amount,
Weight = x.Weight,
HsCode = x.ExHsCode ?? x.ImHsCode ?? x.HsCode,
+ ImHsCode = x.ImHsCode ?? x.HsCode,
+ ExHsCode = x.ExHsCode ?? x.HsCode,
SkuImage = x.SkuImage,
Category = x.Category,
Brand = x.Brand,
diff --git a/src/Oms.Application/Services/Orders/OrderService.cs b/src/Oms.Application/Services/Orders/OrderService.cs
index 99c9574..2929197 100644
--- a/src/Oms.Application/Services/Orders/OrderService.cs
+++ b/src/Oms.Application/Services/Orders/OrderService.cs
@@ -93,12 +93,18 @@ public partial class OrderService : ApplicationService
Processed = x.Value.Processed
},
StringComparer.OrdinalIgnoreCase);
- return new OrderStatusCountsOutput
+ var output = new OrderStatusCountsOutput
{
Total = rows.Sum(x => x.Count),
Counts = counts,
HandleCounts = handleCounts
};
+ if (string.Equals(filter.FulfillmentType, FulfillmentTypeCodes.Warehouse, StringComparison.OrdinalIgnoreCase))
+ {
+ output.FulfillProgressCounts = await OrderRepository.GetFulfillProgressCountsByFilterAsync(filter);
+ }
+
+ return output;
}
///
@@ -626,6 +632,7 @@ public partial class OrderService : ApplicationService
Status = input.Status,
OrderSource = input.OrderSource,
FulfillmentType = NormalizeFulfillmentTypeFilter(input.FulfillmentType),
+ FulfillProgress = NormalizeFulfillProgressFilter(input.FulfillProgress),
Keyword = NormalizeOrderKeyword(input.Keyword),
CreateTimeStart = input.CreateTimeStart,
CreateTimeEnd = input.CreateTimeEnd,
diff --git a/src/Oms.Domain/OrdersAggregate/Repositories/IOrderRepository.cs b/src/Oms.Domain/OrdersAggregate/Repositories/IOrderRepository.cs
index ceb1c11..e06bec6 100644
--- a/src/Oms.Domain/OrdersAggregate/Repositories/IOrderRepository.cs
+++ b/src/Oms.Domain/OrdersAggregate/Repositories/IOrderRepository.cs
@@ -86,6 +86,10 @@ public interface IOrderRepository : IRepository
OrderListFilter filter,
CancellationToken cancellationToken = default);
+ Task> GetFulfillProgressCountsByFilterAsync(
+ OrderListFilter filter,
+ CancellationToken cancellationToken = default);
+
Task CountByFilterAndCreateTimeRangeAsync(
OrderListFilter filter,
DateTime startInclusive,
diff --git a/src/Oms.SqlSugarCore/Orders/OrderRepository.cs b/src/Oms.SqlSugarCore/Orders/OrderRepository.cs
index d3f0139..b957f31 100644
--- a/src/Oms.SqlSugarCore/Orders/OrderRepository.cs
+++ b/src/Oms.SqlSugarCore/Orders/OrderRepository.cs
@@ -1212,6 +1212,56 @@ public class OrderRepository : OmsSqlSugarRepository, IOrderRepository, I
return result;
}
+ public async Task> GetFulfillProgressCountsByFilterAsync(
+ OrderListFilter filter,
+ CancellationToken cancellationToken = default)
+ {
+ var db = await GetDbContextAsync(cancellationToken);
+ var baseFilter = new OrderListFilter
+ {
+ Keyword = filter.Keyword,
+ CreateTimeStart = filter.CreateTimeStart,
+ CreateTimeEnd = filter.CreateTimeEnd,
+ CustomerId = filter.CustomerId,
+ WarehouseId = filter.WarehouseId,
+ FulfillmentType = filter.FulfillmentType,
+ StockBindStatus = filter.StockBindStatus,
+ OrderSource = filter.OrderSource,
+ FieldFilters = filter.FieldFilters,
+ SortField = filter.SortField,
+ SortOrder = filter.SortOrder,
+ ScopeOrganizationIds = filter.ScopeOrganizationIds,
+ ScopeSecretLevels = filter.ScopeSecretLevels,
+ ScopeDataScope = filter.ScopeDataScope,
+ ScopePersonalOwnerMapId = filter.ScopePersonalOwnerMapId,
+ ScopePersonalOwnerAccount = filter.ScopePersonalOwnerAccount,
+ ScopeOwnerMapIds = filter.ScopeOwnerMapIds,
+ ScopeBizFilters = filter.ScopeBizFilters,
+ ScopeCustomFilters = filter.ScopeCustomFilters,
+ };
+
+ async Task CountAsync(string? fulfillProgress)
+ {
+ var query = ApplyListFilter(db.Queryable(), baseFilter);
+ if (!string.IsNullOrWhiteSpace(fulfillProgress))
+ {
+ query = ApplyFulfillProgressFilter(query, fulfillProgress);
+ }
+
+ return await query.CountAsync(cancellationToken);
+ }
+
+ var result = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["all"] = await CountAsync(null),
+ [FulfillmentProgressCodes.PendingBind] = await CountAsync(FulfillmentProgressCodes.PendingBind),
+ [FulfillmentProgressCodes.DomesticInTransit] = await CountAsync(FulfillmentProgressCodes.DomesticInTransit),
+ [FulfillmentProgressCodes.ArrivedPendingPick] = await CountAsync(FulfillmentProgressCodes.ArrivedPendingPick),
+ [FulfillmentProgressCodes.OutboundDone] = await CountAsync(FulfillmentProgressCodes.OutboundDone),
+ };
+ return result;
+ }
+
public async Task CountByFilterAndCreateTimeRangeAsync(
OrderListFilter filter,
DateTime startInclusive,