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,