feat: SkuImage 存 storage key,包裹 summary,修复批次测试
All checks were successful
Build and Deploy / deploy (push) Successful in 1m48s

避免签名 URL 超长写库;新增包裹状态汇总;OrderBatchJobCoordinator 注入 IFileStorage 后补齐单测。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-09-12 12:30:35 +08:00
parent 40997f7d71
commit 43fcb4136a
12 changed files with 95 additions and 10 deletions

2
db.sql
View File

@ -403,7 +403,7 @@ CREATE TABLE `t_order_declaration` (
`HsCode` VARCHAR(50) NULL COMMENT 'HsCode', `HsCode` VARCHAR(50) NULL COMMENT 'HsCode',
`ImHsCode` VARCHAR(50) NULL COMMENT '进口HsCode', `ImHsCode` VARCHAR(50) NULL COMMENT '进口HsCode',
`ExHsCode` VARCHAR(50) NULL COMMENT '出口HsCode', `ExHsCode` VARCHAR(50) NULL COMMENT '出口HsCode',
`SkuImage` VARCHAR(255) NULL COMMENT '图片', `SkuImage` VARCHAR(512) NULL COMMENT '图片(存 storage key 或短 URL)',
`Category` VARCHAR(20) NULL COMMENT '类别', `Category` VARCHAR(20) NULL COMMENT '类别',
`Brand` VARCHAR(50) NULL COMMENT '品牌', `Brand` VARCHAR(50) NULL COMMENT '品牌',
`BrandType` VARCHAR(50) NULL COMMENT '品牌类型', `BrandType` VARCHAR(50) NULL COMMENT '品牌类型',

View File

@ -0,0 +1,27 @@
-- t_order_declaration.SkuImage:库内 SKU 选图后写入签名 URL 易超 VARCHAR(255)
-- 后端已改为存 storage key;列宽对齐仓库 SkuImageUrl,兼容外链
SET @db := DATABASE();
SET @need := (
SELECT COUNT(*)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db
AND TABLE_NAME = 't_order_declaration'
AND COLUMN_NAME = 'SkuImage'
AND (
DATA_TYPE <> 'varchar'
OR CHARACTER_MAXIMUM_LENGTH IS NULL
OR CHARACTER_MAXIMUM_LENGTH < 512
)
);
SET @sql := IF(
@need > 0,
'ALTER TABLE `t_order_declaration` MODIFY COLUMN `SkuImage` VARCHAR(512) NULL COMMENT ''图片(存 storage key 或短 URL)''',
'SELECT ''skip t_order_declaration.SkuImage'' AS msg'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

View File

@ -335,6 +335,15 @@ public class WarehouseInboundSummaryOutput
public int Completed { get; set; } public int Completed { get; set; }
} }
public class WarehouseParcelSummaryOutput
{
public int InTransit { get; set; }
public int Arrived { get; set; }
public int Stored { get; set; }
}
// --- Parcel --- // --- Parcel ---
public class WarehouseParcelListInput : WarehousePageInput public class WarehouseParcelListInput : WarehousePageInput

View File

@ -6,6 +6,7 @@ using Oms.Application.Integrations.Itfx;
using Oms.Application.Integrations.SmallBagTrack; using Oms.Application.Integrations.SmallBagTrack;
using Oms.Application.Messaging; using Oms.Application.Messaging;
using Oms.Application.Shared; using Oms.Application.Shared;
using Oms.Application.Storage;
using Oms.Domain.OrdersAggregate.Consts; using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Entities; using Oms.Domain.OrdersAggregate.Entities;
using Oms.Domain.OrdersAggregate.Repositories; using Oms.Domain.OrdersAggregate.Repositories;
@ -16,6 +17,7 @@ namespace Oms.Application.Services.Orders.Batch;
public sealed class OrderBatchActionExecutor( public sealed class OrderBatchActionExecutor(
IOrderRepository orderRepository, IOrderRepository orderRepository,
ICountryRepository countryRepository, ICountryRepository countryRepository,
IFileStorage fileStorage,
ISmallBagTrackOrderClient smallBagTrackOrderClient, ISmallBagTrackOrderClient smallBagTrackOrderClient,
ISmallBagTrackUpsertRequestBuilder smallBagTrackUpsertRequestBuilder, ISmallBagTrackUpsertRequestBuilder smallBagTrackUpsertRequestBuilder,
OrderDownstreamCredentialResolver downstreamCredentialResolver, OrderDownstreamCredentialResolver downstreamCredentialResolver,
@ -176,6 +178,7 @@ public sealed class OrderBatchActionExecutor(
bundle.Declarations, bundle.Declarations,
bundle.Weights, bundle.Weights,
countryRepository, countryRepository,
fileStorage,
cancellationToken); cancellationToken);
} }

View File

@ -8,6 +8,7 @@ using Oms.Application.Dtos.Shared;
using Oms.Application.Integrations.SmallBagTrack; using Oms.Application.Integrations.SmallBagTrack;
using Oms.Application.Messaging; using Oms.Application.Messaging;
using Oms.Application.Shared; using Oms.Application.Shared;
using Oms.Application.Storage;
using Oms.Domain.OrdersAggregate.Consts; using Oms.Domain.OrdersAggregate.Consts;
using System.Linq.Expressions; using System.Linq.Expressions;
using Oms.Domain.OrdersAggregate.Entities; using Oms.Domain.OrdersAggregate.Entities;
@ -23,6 +24,7 @@ public sealed class OrderBatchJobCoordinator(
IOrderBatchJobRepository batchJobRepository, IOrderBatchJobRepository batchJobRepository,
IOrderRepository orderRepository, IOrderRepository orderRepository,
ICountryRepository countryRepository, ICountryRepository countryRepository,
IFileStorage fileStorage,
IOrderBatchJobPublisher publisher, IOrderBatchJobPublisher publisher,
OrderBatchJobProcessor batchJobProcessor, OrderBatchJobProcessor batchJobProcessor,
IOptions<OrderBatchRabbitMqOptions> batchOptions, IOptions<OrderBatchRabbitMqOptions> batchOptions,
@ -649,6 +651,7 @@ public sealed class OrderBatchJobCoordinator(
bundle.Declarations, bundle.Declarations,
bundle.Weights, bundle.Weights,
countryRepository, countryRepository,
fileStorage,
cancellationToken); cancellationToken);
result[orderId] = JsonConvert.SerializeObject(body); result[orderId] = JsonConvert.SerializeObject(body);
} }

View File

@ -97,7 +97,8 @@ public partial class OrderService
bundle.Recipient, bundle.Recipient,
bundle.Declarations, bundle.Declarations,
bundle.Weights, bundle.Weights,
CountryRepository); CountryRepository,
FileStorage);
return (bundle.Order, body); return (bundle.Order, body);
} }

View File

@ -743,7 +743,7 @@ public partial class OrderService
}; };
} }
private static OrderItemOutput MapDeclaration(OrderDeclaration x) private OrderItemOutput MapDeclaration(OrderDeclaration x)
{ {
return new OrderItemOutput return new OrderItemOutput
{ {
@ -759,7 +759,7 @@ public partial class OrderService
HsCode = x.ExHsCode ?? x.ImHsCode ?? x.HsCode, HsCode = x.ExHsCode ?? x.ImHsCode ?? x.HsCode,
ImHsCode = x.ImHsCode ?? x.HsCode, ImHsCode = x.ImHsCode ?? x.HsCode,
ExHsCode = x.ExHsCode ?? x.HsCode, ExHsCode = x.ExHsCode ?? x.HsCode,
SkuImage = x.SkuImage, SkuImage = FileStorage.GetPublicUrl(x.SkuImage),
Category = x.Category, Category = x.Category,
Brand = x.Brand, Brand = x.Brand,
BrandType = x.BrandType, BrandType = x.BrandType,

View File

@ -894,7 +894,7 @@ public partial class OrderService
_ => (OrderWeightTypeCodes.Forecast, OrderFeeChargingStageCodes.Forecast) _ => (OrderWeightTypeCodes.Forecast, OrderFeeChargingStageCodes.Forecast)
}; };
private static OrderDeclaration MapDeclarationInput(int orderId, OrderParcelItemInput x) private OrderDeclaration MapDeclarationInput(int orderId, OrderParcelItemInput x)
{ {
return new OrderDeclaration return new OrderDeclaration
{ {
@ -911,7 +911,7 @@ public partial class OrderService
HsCode = x.ExHsCode ?? x.ImHsCode, HsCode = x.ExHsCode ?? x.ImHsCode,
ImHsCode = x.ImHsCode, ImHsCode = x.ImHsCode,
ExHsCode = x.ExHsCode ?? x.ImHsCode, ExHsCode = x.ExHsCode ?? x.ImHsCode,
SkuImage = x.SkuImage, SkuImage = FileStorage.ToStorageKey(x.SkuImage),
Category = x.Category, Category = x.Category,
Brand = x.Brand, Brand = x.Brand,
BrandType = x.BrandType, BrandType = x.BrandType,

View File

@ -1965,7 +1965,7 @@ public partial class OrderService
HsCode = x.ExHsCode ?? x.ImHsCode, HsCode = x.ExHsCode ?? x.ImHsCode,
ImHsCode = x.ImHsCode, ImHsCode = x.ImHsCode,
ExHsCode = x.ExHsCode, ExHsCode = x.ExHsCode,
SkuImage = x.SkuImage, SkuImage = FileStorage.ToStorageKey(x.SkuImage),
Category = x.Category, Category = x.Category,
Brand = x.Brand, Brand = x.Brand,
BrandType = x.BrandType, BrandType = x.BrandType,

View File

@ -79,6 +79,43 @@ public partial class WarehouseService
}; };
} }
/// <summary>
/// 查询包裹生命周期状态汇总。
/// </summary>
/// <param name="customerId">客户 ID(可选)。</param>
/// <param name="warehouseId">仓库 ID(可选)。</param>
/// <returns>各状态数量汇总。</returns>
[HttpGet("/warehouse/parcel/summary")]
public async Task<WarehouseParcelSummaryOutput> GetParcelSummaryAsync(
[FromQuery] int? customerId = null,
[FromQuery] int? warehouseId = null)
{
var resolvedCustomerId = await ResolveListCustomerIdAsync(customerId);
var (hasCustomerFilter, customerIdValue) = CustomerFilter(resolvedCustomerId);
var hasWarehouseFilter = warehouseId.HasValue;
var warehouseIdValue = warehouseId.GetValueOrDefault();
var inTransit = await ParcelRepository.CountAsync(x =>
(!hasCustomerFilter || x.CustomerId == customerIdValue)
&& (!hasWarehouseFilter || x.WarehouseId == warehouseIdValue)
&& x.LifecycleStatus == WarehouseParcelLifecycleCodes.InTransit);
var arrived = await ParcelRepository.CountAsync(x =>
(!hasCustomerFilter || x.CustomerId == customerIdValue)
&& (!hasWarehouseFilter || x.WarehouseId == warehouseIdValue)
&& x.LifecycleStatus == WarehouseParcelLifecycleCodes.Arrived);
var stored = await ParcelRepository.CountAsync(x =>
(!hasCustomerFilter || x.CustomerId == customerIdValue)
&& (!hasWarehouseFilter || x.WarehouseId == warehouseIdValue)
&& x.LifecycleStatus == WarehouseParcelLifecycleCodes.Stored);
return new WarehouseParcelSummaryOutput
{
InTransit = inTransit,
Arrived = arrived,
Stored = stored
};
}
/// <summary> /// <summary>
/// 规范化列表状态筛选:空 / all 表示不按状态过滤;已知编码统一为规范值。 /// 规范化列表状态筛选:空 / all 表示不按状态过滤;已知编码统一为规范值。
/// </summary> /// </summary>

View File

@ -1,4 +1,5 @@
using Oms.Application.Dtos.Orders; using Oms.Application.Dtos.Orders;
using Oms.Application.Storage;
using Oms.Domain.OrdersAggregate.Consts; using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Entities; using Oms.Domain.OrdersAggregate.Entities;
using Oms.Domain.SystemAggregate.Repositories; using Oms.Domain.SystemAggregate.Repositories;
@ -17,6 +18,7 @@ public static class OrderDownstreamCreateOneBodyBuilder
IReadOnlyList<OrderDeclaration> declarations, IReadOnlyList<OrderDeclaration> declarations,
IReadOnlyList<OrderWeight> weights, IReadOnlyList<OrderWeight> weights,
ICountryRepository countryRepository, ICountryRepository countryRepository,
IFileStorage? fileStorage = null,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
var countryIds = new List<int>(); var countryIds = new List<int>();
@ -66,7 +68,7 @@ public static class OrderDownstreamCreateOneBodyBuilder
Quantity = order.Quantity, Quantity = order.Quantity,
Receiver = MapReceiver(recipient, order.ConsigneeCountry, idToCode), Receiver = MapReceiver(recipient, order.ConsigneeCountry, idToCode),
Sender = MapSender(sender, idToCode), Sender = MapSender(sender, idToCode),
Items = declarations.Select(MapItem).ToList() Items = declarations.Select(x => MapItem(x, fileStorage)).ToList()
}; };
} }
@ -150,7 +152,7 @@ public static class OrderDownstreamCreateOneBodyBuilder
}; };
} }
private static OrderParcelItemInput MapItem(OrderDeclaration x) => private static OrderParcelItemInput MapItem(OrderDeclaration x, IFileStorage? fileStorage) =>
new() new()
{ {
CnName = x.CnName, CnName = x.CnName,
@ -164,7 +166,7 @@ public static class OrderDownstreamCreateOneBodyBuilder
Weight = x.Weight, Weight = x.Weight,
ImHsCode = x.ImHsCode, ImHsCode = x.ImHsCode,
ExHsCode = x.ExHsCode ?? x.HsCode, ExHsCode = x.ExHsCode ?? x.HsCode,
SkuImage = x.SkuImage, SkuImage = fileStorage?.GetPublicUrl(x.SkuImage) ?? x.SkuImage,
Category = x.Category, Category = x.Category,
Brand = x.Brand, Brand = x.Brand,
BrandType = x.BrandType, BrandType = x.BrandType,

View File

@ -6,6 +6,7 @@ using Moq;
using Oms.Application.Dtos.Orders; using Oms.Application.Dtos.Orders;
using Oms.Application.Messaging; using Oms.Application.Messaging;
using Oms.Application.Services.Orders.Batch; using Oms.Application.Services.Orders.Batch;
using Oms.Application.Storage;
using Oms.Domain.OrdersAggregate.Consts; using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Entities; using Oms.Domain.OrdersAggregate.Entities;
using Oms.Domain.OrdersAggregate.Repositories; using Oms.Domain.OrdersAggregate.Repositories;
@ -18,6 +19,7 @@ public class OrderBatchJobCoordinatorGovernanceTests
private readonly Mock<IOrderBatchJobRepository> _batchJobRepository = new(MockBehavior.Strict); private readonly Mock<IOrderBatchJobRepository> _batchJobRepository = new(MockBehavior.Strict);
private readonly Mock<IOrderRepository> _orderRepository = new(MockBehavior.Loose); private readonly Mock<IOrderRepository> _orderRepository = new(MockBehavior.Loose);
private readonly Mock<ICountryRepository> _countryRepository = new(MockBehavior.Loose); private readonly Mock<ICountryRepository> _countryRepository = new(MockBehavior.Loose);
private readonly Mock<IFileStorage> _fileStorage = new(MockBehavior.Loose);
private readonly Mock<IOrderBatchJobPublisher> _publisher = new(MockBehavior.Strict); private readonly Mock<IOrderBatchJobPublisher> _publisher = new(MockBehavior.Strict);
private readonly OrderBatchJobCoordinator _coordinator; private readonly OrderBatchJobCoordinator _coordinator;
@ -36,6 +38,7 @@ public class OrderBatchJobCoordinatorGovernanceTests
_batchJobRepository.Object, _batchJobRepository.Object,
_orderRepository.Object, _orderRepository.Object,
_countryRepository.Object, _countryRepository.Object,
_fileStorage.Object,
_publisher.Object, _publisher.Object,
processor, processor,
batchOptions, batchOptions,