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',
`ImHsCode` 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 '类别',
`Brand` 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 class WarehouseParcelSummaryOutput
{
public int InTransit { get; set; }
public int Arrived { get; set; }
public int Stored { get; set; }
}
// --- Parcel ---
public class WarehouseParcelListInput : WarehousePageInput

View File

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

View File

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

View File

@ -97,7 +97,8 @@ public partial class OrderService
bundle.Recipient,
bundle.Declarations,
bundle.Weights,
CountryRepository);
CountryRepository,
FileStorage);
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
{
@ -759,7 +759,7 @@ public partial class OrderService
HsCode = x.ExHsCode ?? x.ImHsCode ?? x.HsCode,
ImHsCode = x.ImHsCode ?? x.HsCode,
ExHsCode = x.ExHsCode ?? x.HsCode,
SkuImage = x.SkuImage,
SkuImage = FileStorage.GetPublicUrl(x.SkuImage),
Category = x.Category,
Brand = x.Brand,
BrandType = x.BrandType,

View File

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

View File

@ -1965,7 +1965,7 @@ public partial class OrderService
HsCode = x.ExHsCode ?? x.ImHsCode,
ImHsCode = x.ImHsCode,
ExHsCode = x.ExHsCode,
SkuImage = x.SkuImage,
SkuImage = FileStorage.ToStorageKey(x.SkuImage),
Category = x.Category,
Brand = x.Brand,
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>
/// 规范化列表状态筛选:空 / all 表示不按状态过滤;已知编码统一为规范值。
/// </summary>

View File

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

View File

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