feat: 实现仓储模块与平台店铺订单后端能力

新增仓储领域实体、仓储库、WarehouseService 全套接口及数据库迁移脚本;
实现店铺授权、店铺订单服务与库存匹配创建小包单;补充仓储模块 RBAC 权限种子数据。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-07-11 15:19:35 +08:00
parent 18b53de20c
commit a383ee6f32
32 changed files with 3721 additions and 0 deletions

View File

@ -0,0 +1,263 @@
-- 仓储模块 + 店铺订单表结构(v0.3)
-- 设计文档:E:\oms0414\docs\仓储模块设计方案.md
-- 仓储表前缀:C_ ;店铺表前缀:t_sales_
-- 幂等:CREATE TABLE IF NOT EXISTS
-- ---------------------------------------------------------------------------
-- 仓储域(C_*)
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `C_sku` (
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
`CustomerId` INT NOT NULL COMMENT '客户ID → t_customer.Id',
`SkuCode` VARCHAR(64) NOT NULL COMMENT '库内SKU编码',
`SkuName` VARCHAR(256) NOT NULL COMMENT 'SKU名称',
`SkuImageUrl` VARCHAR(512) NULL COMMENT '图片URL',
`WeightG` DECIMAL(10,2) NULL COMMENT '重量(g)',
`LengthCm` DECIMAL(10,2) NULL COMMENT '长(cm)',
`WidthCm` DECIMAL(10,2) NULL COMMENT '宽(cm)',
`HeightCm` DECIMAL(10,2) NULL COMMENT '高(cm)',
`Remark` VARCHAR(512) NULL COMMENT '备注',
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是',
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人',
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`UpdateUserId` INT NULL COMMENT '更新人',
`UpdateTime` DATETIME NULL COMMENT '更新时间',
PRIMARY KEY (`Id`),
UNIQUE KEY `uk_c_sku_code` (`CustomerId`, `SkuCode`),
KEY `idx_c_sku_customer` (`CustomerId`, `IsDeleted`)
) COMMENT='仓储-SKU主数据';
CREATE TABLE IF NOT EXISTS `C_bin_location` (
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
`WarehouseId` INT NOT NULL COMMENT '仓库ID → t_warehouse_address.Id',
`BinCode` VARCHAR(32) NOT NULL COMMENT '库位编码',
`BinName` VARCHAR(64) NULL COMMENT '库位展示名',
`ZoneCode` VARCHAR(32) NULL COMMENT '库区编码',
`IsEnabled` TINYINT NOT NULL DEFAULT 1 COMMENT '是否启用:0-否,1-是',
`Remark` VARCHAR(512) NULL COMMENT '备注',
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是',
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人',
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`UpdateUserId` INT NULL COMMENT '更新人',
`UpdateTime` DATETIME NULL COMMENT '更新时间',
PRIMARY KEY (`Id`),
UNIQUE KEY `uk_c_bin_wh_code` (`WarehouseId`, `BinCode`),
KEY `idx_c_bin_warehouse` (`WarehouseId`, `IsEnabled`)
) COMMENT='仓储-库位主数据';
CREATE TABLE IF NOT EXISTS `C_inventory` (
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
`CustomerId` INT NOT NULL COMMENT '客户ID',
`WarehouseId` INT NOT NULL COMMENT '仓库ID',
`SkuId` INT NOT NULL COMMENT 'SKU ID → C_sku.Id',
`BinLocationId` INT NOT NULL COMMENT '库位ID → C_bin_location.Id',
`Qty` INT NOT NULL DEFAULT 0 COMMENT '可用库存数量',
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是',
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人',
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`UpdateUserId` INT NULL COMMENT '更新人',
`UpdateTime` DATETIME NULL COMMENT '更新时间',
PRIMARY KEY (`Id`),
UNIQUE KEY `uk_c_inventory` (`CustomerId`, `WarehouseId`, `SkuId`, `BinLocationId`),
KEY `idx_c_inv_sku_wh` (`CustomerId`, `SkuId`, `WarehouseId`)
) COMMENT='仓储-库存余额(按库位)';
CREATE TABLE IF NOT EXISTS `C_inbound_notice` (
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
`CustomerId` INT NOT NULL COMMENT '客户ID',
`WarehouseId` INT NOT NULL COMMENT '仓库ID',
`NoticeNo` VARCHAR(32) NOT NULL COMMENT '入库单号 IN+雪花',
`MarkReference` VARCHAR(64) NULL COMMENT '唛头参考号',
`InboundType` VARCHAR(16) NOT NULL DEFAULT 'normal' COMMENT 'normal/return',
`ReceiptStatus` VARCHAR(16) NOT NULL DEFAULT 'forecasted' COMMENT 'forecasted/received/completed',
`ExpectedArrivalTime` DATETIME NULL COMMENT '预计到货时间',
`ActualArrivalTime` DATETIME NULL COMMENT '实际到货时间',
`Remark` VARCHAR(512) NULL COMMENT '备注',
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是',
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人',
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`UpdateUserId` INT NULL COMMENT '更新人',
`UpdateTime` DATETIME NULL COMMENT '更新时间',
PRIMARY KEY (`Id`),
UNIQUE KEY `uk_c_inbound_notice_no` (`NoticeNo`),
KEY `idx_c_inbound_customer_status` (`CustomerId`, `ReceiptStatus`, `CreateTime`)
) COMMENT='仓储-入库预报主表';
CREATE TABLE IF NOT EXISTS `C_inbound_box` (
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
`InboundNoticeId` INT NOT NULL COMMENT '入库预报ID → C_inbound_notice.Id',
`BoxSeqNo` VARCHAR(16) NOT NULL COMMENT '箱序号',
`MarkBoxNo` VARCHAR(64) NULL COMMENT '箱唛号/箱号',
`WeightG` DECIMAL(10,2) NULL COMMENT '箱重(g)',
`LengthCm` DECIMAL(10,2) NULL COMMENT '长(cm)',
`WidthCm` DECIMAL(10,2) NULL COMMENT '宽(cm)',
`HeightCm` DECIMAL(10,2) NULL COMMENT '高(cm)',
`IsReceived` TINYINT NOT NULL DEFAULT 0 COMMENT '是否已收货:0-否,1-是',
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是',
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人',
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`UpdateUserId` INT NULL COMMENT '更新人',
`UpdateTime` DATETIME NULL COMMENT '更新时间',
PRIMARY KEY (`Id`),
KEY `idx_c_inbound_box_notice` (`InboundNoticeId`, `IsDeleted`)
) COMMENT='仓储-入库预报箱';
CREATE TABLE IF NOT EXISTS `C_inbound_box_line` (
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
`InboundBoxId` INT NOT NULL COMMENT '入库箱ID → C_inbound_box.Id',
`SkuId` INT NOT NULL COMMENT 'SKU ID → C_sku.Id',
`SkuCode` VARCHAR(64) NOT NULL COMMENT '冗余SKU编码',
`PlannedQty` INT NOT NULL DEFAULT 0 COMMENT '计划入库数量',
`ReceivedQty` INT NULL COMMENT '实收数量',
`BinLocationId` INT NULL COMMENT '上架库位ID → C_bin_location.Id',
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是',
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人',
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`UpdateUserId` INT NULL COMMENT '更新人',
`UpdateTime` DATETIME NULL COMMENT '更新时间',
PRIMARY KEY (`Id`),
KEY `idx_c_inbound_box_line_box` (`InboundBoxId`, `IsDeleted`),
KEY `idx_c_inbound_box_line_sku` (`SkuId`)
) COMMENT='仓储-入库箱内SKU行';
CREATE TABLE IF NOT EXISTS `C_parcel` (
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
`CustomerId` INT NOT NULL COMMENT '客户ID',
`WarehouseId` INT NOT NULL COMMENT '仓库ID',
`TrackingNo` VARCHAR(64) NOT NULL COMMENT '快递单号',
`CarrierName` VARCHAR(64) NULL COMMENT '快递公司',
`LifecycleStatus` VARCHAR(16) NOT NULL DEFAULT 'inTransit' COMMENT 'inTransit/arrived/stored',
`ArrivalTime` DATETIME NULL COMMENT '到仓时间',
`Remark` VARCHAR(512) NULL COMMENT '备注',
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是',
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人',
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`UpdateUserId` INT NULL COMMENT '更新人',
`UpdateTime` DATETIME NULL COMMENT '更新时间',
PRIMARY KEY (`Id`),
UNIQUE KEY `uk_c_parcel_tracking` (`CustomerId`, `TrackingNo`, `WarehouseId`),
KEY `idx_c_parcel_status` (`CustomerId`, `LifecycleStatus`)
) COMMENT='仓储-客户包裹';
CREATE TABLE IF NOT EXISTS `C_parcel_item` (
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
`ParcelId` INT NOT NULL COMMENT '包裹ID → C_parcel.Id',
`ProductName` VARCHAR(512) NULL COMMENT '商品名称',
`Quantity` INT NOT NULL DEFAULT 0 COMMENT '数量',
`SkuId` INT NULL COMMENT '库内SKU ID → C_sku.Id',
`SkuCode` VARCHAR(64) NULL COMMENT '冗余SKU编码',
`BinLocationId` INT NULL COMMENT '上架库位ID',
`Remark` VARCHAR(512) NULL COMMENT '备注',
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是',
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人',
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`UpdateUserId` INT NULL COMMENT '更新人',
`UpdateTime` DATETIME NULL COMMENT '更新时间',
PRIMARY KEY (`Id`),
KEY `idx_c_parcel_item_parcel` (`ParcelId`, `IsDeleted`)
) COMMENT='仓储-包裹明细';
CREATE TABLE IF NOT EXISTS `C_sku_platform_map` (
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
`CustomerId` INT NOT NULL COMMENT '客户ID',
`StoreId` INT NOT NULL COMMENT '店铺ID → t_sales_store.Id',
`PlatformSku` VARCHAR(128) NOT NULL COMMENT '平台listing SKU',
`SkuId` INT NOT NULL COMMENT '库内SKU ID → C_sku.Id',
`SkuCode` VARCHAR(64) NOT NULL COMMENT '冗余SKU编码',
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是',
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人',
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`UpdateUserId` INT NULL COMMENT '更新人',
`UpdateTime` DATETIME NULL COMMENT '更新时间',
PRIMARY KEY (`Id`),
UNIQUE KEY `uk_c_sku_map` (`StoreId`, `PlatformSku`),
KEY `idx_c_sku_map_customer` (`CustomerId`, `SkuId`)
) COMMENT='仓储-平台SKU映射';
-- ---------------------------------------------------------------------------
-- 订单域 — 店铺(t_sales_*,接口阶段使用)
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `t_sales_store` (
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
`CustomerId` INT NOT NULL COMMENT '客户ID',
`PlatformCode` VARCHAR(32) NOT NULL COMMENT 'lazada/shopee/tiktok/temu/manual',
`StoreNickname` VARCHAR(128) NOT NULL COMMENT '店铺昵称',
`PlatformStoreId` VARCHAR(64) NULL COMMENT '平台侧店铺ID',
`PlatformAccount` VARCHAR(128) NULL COMMENT '平台账号',
`SiteCountry` VARCHAR(8) NULL COMMENT '开户站点国家码',
`SellerId` VARCHAR(64) NULL COMMENT '平台卖家ID',
`AuthStatus` VARCHAR(16) NOT NULL DEFAULT 'active' COMMENT 'active/expired/revoked',
`AuthTime` DATETIME NULL COMMENT '授权时间',
`TokenExpireTime` DATETIME NULL COMMENT 'AccessToken过期时间',
`RefreshExpireTime` DATETIME NULL COMMENT 'RefreshToken过期时间',
`AccessToken` VARCHAR(2048) NULL COMMENT 'AccessToken(加密存储)',
`RefreshToken` VARCHAR(2048) NULL COMMENT 'RefreshToken(加密存储)',
`TokenExtraJson` JSON NULL COMMENT '平台扩展Token信息',
`IsAutoFulfill` TINYINT NOT NULL DEFAULT 0 COMMENT '是否自动发货:0-否,1-是',
`Remark` VARCHAR(512) NULL COMMENT '备注',
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是',
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人',
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`UpdateUserId` INT NULL COMMENT '更新人',
`UpdateTime` DATETIME NULL COMMENT '更新时间',
PRIMARY KEY (`Id`),
KEY `idx_sales_store_customer` (`CustomerId`, `IsDeleted`),
KEY `idx_sales_store_platform` (`CustomerId`, `PlatformCode`, `AuthStatus`)
) COMMENT='销售店铺授权';
CREATE TABLE IF NOT EXISTS `t_sales_store_order` (
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
`CustomerId` INT NOT NULL COMMENT '客户ID',
`StoreId` INT NOT NULL COMMENT '店铺ID → t_sales_store.Id',
`PlatformCode` VARCHAR(32) NOT NULL COMMENT '平台编码',
`PlatformOrderNo` VARCHAR(64) NOT NULL COMMENT '平台订单号',
`OrderCountry` VARCHAR(8) NULL COMMENT '订单国家',
`ItemCount` INT NULL COMMENT '商品行数',
`TotalAmount` DECIMAL(18,4) NULL COMMENT '订单总额',
`Currency` VARCHAR(8) NULL COMMENT '币种',
`PlatformOrderTime` DATETIME NULL COMMENT '平台下单时间',
`FulfillStatus` VARCHAR(32) NOT NULL DEFAULT 'pending' COMMENT '履约状态',
`PlatformStatus` VARCHAR(64) NULL COMMENT '平台原始状态码',
`PlatformStatusLabel` VARCHAR(128) NULL COMMENT '平台状态展示文案',
`ShippingLabelNo` VARCHAR(64) NULL COMMENT '面单号',
`ShippingLabelUrl` VARCHAR(512) NULL COMMENT '面单URL',
`IsMultiLabel` TINYINT NOT NULL DEFAULT 0 COMMENT '是否多面单',
`CustomTags` VARCHAR(512) NULL COMMENT '自定义标签',
`IsSuspended` TINYINT NOT NULL DEFAULT 0 COMMENT '是否搁置',
`WarehouseId` INT NULL COMMENT '发货仓库ID',
`LinkedOrderId` INT NULL COMMENT '关联小包订单 t_order.Id',
`LastSyncTime` DATETIME NULL COMMENT '最近同步时间',
`PlatformExtraJson` JSON NULL COMMENT '平台扩展字段',
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是',
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人',
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`UpdateUserId` INT NULL COMMENT '更新人',
`UpdateTime` DATETIME NULL COMMENT '更新时间',
PRIMARY KEY (`Id`),
UNIQUE KEY `uk_store_order_no` (`StoreId`, `PlatformOrderNo`),
KEY `idx_store_order_customer_status` (`CustomerId`, `FulfillStatus`, `PlatformOrderTime`)
) COMMENT='销售店铺订单';
CREATE TABLE IF NOT EXISTS `t_sales_store_order_line` (
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
`StoreOrderId` INT NOT NULL COMMENT '店铺订单ID → t_sales_store_order.Id',
`PlatformLineId` VARCHAR(64) NULL COMMENT '平台行ID',
`PlatformSku` VARCHAR(128) NULL COMMENT '平台SKU',
`ProductName` VARCHAR(512) NULL COMMENT '商品名称',
`ProductImageUrl` VARCHAR(512) NULL COMMENT '商品图片',
`VariantText` VARCHAR(256) NULL COMMENT '规格属性',
`UnitPrice` DECIMAL(18,4) NULL COMMENT '单价',
`Quantity` INT NOT NULL DEFAULT 0 COMMENT '数量',
`Currency` VARCHAR(8) NULL COMMENT '币种',
`IsShipExcluded` TINYINT NOT NULL DEFAULT 0 COMMENT '是否拒绝发货',
`MappedSkuId` INT NULL COMMENT '映射库内SKU → C_sku.Id',
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是',
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人',
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`UpdateUserId` INT NULL COMMENT '更新人',
`UpdateTime` DATETIME NULL COMMENT '更新时间',
PRIMARY KEY (`Id`),
KEY `idx_store_order_line_order` (`StoreOrderId`, `IsDeleted`)
) COMMENT='销售店铺订单明细';

View File

@ -0,0 +1,42 @@
-- 仓储模块 + 店铺授权/订单 RBAC 权限(幂等)
-- RBAC 库执行;执行后用户需重新登录或刷新权限缓存
SET @ADMIN_USER_ID = (
SELECT `Id` FROM `user`
WHERE `Account` = 'admin' AND (`IsDeleted` = 0 OR `IsDeleted` IS NULL)
LIMIT 1
);
SET @ADMIN_USER_ID = IFNULL(
@ADMIN_USER_ID,
'08de4226-3fd4-4a5c-83e5-70e164b15f2f'
);
INSERT INTO `role_permission` (`Id`, `RoleId`, `PermissionCode`, `CreateUserId`, `CreateTime`)
SELECT UUID(), r.`Id`, p.`Code`, @ADMIN_USER_ID, NOW()
FROM `role` r
CROSS JOIN (
SELECT 'Warehouse.Sku.Query' AS `Code` UNION ALL
SELECT 'Warehouse.Sku.Save' UNION ALL
SELECT 'Warehouse.Sku.Delete' UNION ALL
SELECT 'Warehouse.Inventory.Query' UNION ALL
SELECT 'Warehouse.Inventory.Relocate' UNION ALL
SELECT 'Warehouse.Parcel.Query' UNION ALL
SELECT 'Warehouse.Parcel.Save' UNION ALL
SELECT 'Warehouse.Parcel.Delete' UNION ALL
SELECT 'Warehouse.InboundNotice.Query' UNION ALL
SELECT 'Warehouse.InboundNotice.Save' UNION ALL
SELECT 'Warehouse.InboundNotice.Delete' UNION ALL
SELECT 'Order.PlatformStore.Query' UNION ALL
SELECT 'Order.PlatformStore.Save' UNION ALL
SELECT 'Order.PlatformStore.Delete' UNION ALL
SELECT 'Order.PlatformStore.Auth' UNION ALL
SELECT 'Order.PlatformStoreOrder.Query' UNION ALL
SELECT 'Order.PlatformStoreOrder.MatchInventory' UNION ALL
SELECT 'Order.PlatformStoreOrder.ToggleSuspend' UNION ALL
SELECT 'Order.PlatformStoreOrder.Delete'
) p
WHERE r.`Code` IN ('Admin', 'Customer')
AND NOT EXISTS (
SELECT 1 FROM `role_permission` rp
WHERE rp.`RoleId` = r.`Id` AND rp.`PermissionCode` = p.`Code`
);

View File

@ -0,0 +1,293 @@
using System.ComponentModel.DataAnnotations;
using Oms.Application.Dtos.Orders;
namespace Oms.Application.Dtos.PlatformStore;
public class PlatformStorePageInput
{
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 30;
public int? CustomerId { get; set; }
public string? PlatformCode { get; set; }
public string? StoreNickname { get; set; }
public string? ExpirationFilter { get; set; }
}
public class PlatformStoreOutput
{
public int Id { get; set; }
public int CustomerId { get; set; }
public string PlatformCode { get; set; } = string.Empty;
public string PlatformName { get; set; } = string.Empty;
public string StoreNickname { get; set; } = string.Empty;
public string? PlatformAccount { get; set; }
public string? SiteCountry { get; set; }
public string AuthStatus { get; set; } = string.Empty;
public DateTime? AuthTime { get; set; }
public DateTime? TokenExpireTime { get; set; }
public bool IsAutoFulfill { get; set; }
public string? Remark { get; set; }
public DateTime CreateTime { get; set; }
}
public class PlatformStoreSaveInput
{
public int Id { get; set; }
public int? CustomerId { get; set; }
public string PlatformCode { get; set; } = "manual";
[Required]
public string StoreNickname { get; set; } = string.Empty;
public string? PlatformAccount { get; set; }
public string? SiteCountry { get; set; }
public bool IsAutoFulfill { get; set; }
public string? Remark { get; set; }
}
public class PlatformStoreAuthUrlInput
{
[Required]
public string PlatformCode { get; set; } = string.Empty;
public int? CustomerId { get; set; }
public string? RedirectUri { get; set; }
}
public class PlatformStoreAuthUrlOutput
{
public string AuthUrl { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
}
public class PlatformStoreIdsInput
{
[Required]
public List<int> Ids { get; set; } = [];
}
// --- Store Order ---
public class PlatformStoreOrderListInput : PlatformStorePageInput
{
public int? StoreId { get; set; }
public string? FulfillStatus { get; set; }
public string? PlatformOrderNo { get; set; }
public bool? IsSuspended { get; set; }
}
public class PlatformStoreOrderLineOutput
{
public int Id { get; set; }
public string? PlatformLineId { get; set; }
public string? PlatformSku { get; set; }
public string? ProductName { get; set; }
public string? ProductImageUrl { get; set; }
public string? VariantText { get; set; }
public decimal? UnitPrice { get; set; }
public int Quantity { get; set; }
public string? Currency { get; set; }
public bool IsShipExcluded { get; set; }
public int? MappedSkuId { get; set; }
public string? MappedSkuCode { get; set; }
}
public class PlatformStoreOrderOutput
{
public int Id { get; set; }
public int CustomerId { get; set; }
public int StoreId { get; set; }
public string StoreNickname { get; set; } = string.Empty;
public string PlatformCode { get; set; } = string.Empty;
public string PlatformOrderNo { get; set; } = string.Empty;
public string? OrderCountry { get; set; }
public int? ItemCount { get; set; }
public decimal? TotalAmount { get; set; }
public string? Currency { get; set; }
public DateTime? PlatformOrderTime { get; set; }
public string FulfillStatus { get; set; } = string.Empty;
public string? PlatformStatusLabel { get; set; }
public string? ShippingLabelNo { get; set; }
public string? ShippingLabelUrl { get; set; }
public bool IsMultiLabel { get; set; }
public string? CustomTags { get; set; }
public bool IsSuspended { get; set; }
public int? WarehouseId { get; set; }
public int? LinkedOrderId { get; set; }
public string? LinkedServiceNo { get; set; }
public DateTime? LastSyncTime { get; set; }
public List<PlatformStoreOrderLineOutput> Lines { get; set; } = [];
}
public class PlatformStoreOrderSummaryOutput
{
public int Pending { get; set; }
public int ToShip { get; set; }
public int Shipped { get; set; }
public int Canceled { get; set; }
public int Suspended { get; set; }
public int Completed { get; set; }
}
public class PlatformStoreOrderIdInput
{
[Required]
public int Id { get; set; }
public int? CustomerId { get; set; }
}
public class PlatformStoreOrderIdsInput
{
[Required]
public List<int> Ids { get; set; } = [];
public int? CustomerId { get; set; }
}
public class PlatformStoreOrderMatchInventoryInput
{
[Required]
public int Id { get; set; }
public int? CustomerId { get; set; }
public int? WarehouseId { get; set; }
public string? ProductCode { get; set; }
public string? TrackNo { get; set; }
public OrderParcelReceiverInput? Receiver { get; set; }
}
public class PlatformStoreOrderBatchMatchInventoryInput
{
[Required]
public List<int> Ids { get; set; } = [];
public int? CustomerId { get; set; }
public int? WarehouseId { get; set; }
public string? ProductCode { get; set; }
}
public class PlatformStoreOrderBatchMatchItemOutput
{
public int Id { get; set; }
public string PlatformOrderNo { get; set; } = string.Empty;
public bool Success { get; set; }
public string Message { get; set; } = string.Empty;
public int? LinkedOrderId { get; set; }
public string? ServiceNo { get; set; }
}
public class PlatformStoreOrderBatchMatchInventoryOutput
{
public int SuccessCount { get; set; }
public int FailCount { get; set; }
public List<PlatformStoreOrderBatchMatchItemOutput> Items { get; set; } = [];
}
public class PlatformStoreOrderMatchInventoryOutput
{
public bool Success { get; set; }
public string Message { get; set; } = string.Empty;
public int? LinkedOrderId { get; set; }
public string? ServiceNo { get; set; }
public string? ClientNo { get; set; }
}
public class PlatformStoreOrderToggleSuspendInput
{
[Required]
public int Id { get; set; }
public int? CustomerId { get; set; }
public bool IsSuspended { get; set; }
}
public class PlatformStoreOrderActionOutput
{
public bool Success { get; set; } = true;
public string Message { get; set; } = string.Empty;
}

View File

@ -0,0 +1,468 @@
using System.ComponentModel.DataAnnotations;
namespace Oms.Application.Dtos.Warehouse;
public class WarehousePageInput
{
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 30;
public int? CustomerId { get; set; }
}
// --- SKU ---
public class WarehouseSkuListInput : WarehousePageInput
{
public string? Keyword { get; set; }
}
public class WarehouseSkuOutput
{
public int Id { get; set; }
public int CustomerId { get; set; }
public string SkuCode { get; set; } = string.Empty;
public string SkuName { get; set; } = string.Empty;
public string? SkuImageUrl { get; set; }
public decimal? WeightG { get; set; }
public decimal? LengthCm { get; set; }
public decimal? WidthCm { get; set; }
public decimal? HeightCm { get; set; }
public string? Remark { get; set; }
public int TotalQty { get; set; }
public DateTime CreateTime { get; set; }
public DateTime? UpdateTime { get; set; }
}
public class WarehouseSkuSaveInput
{
public int Id { get; set; }
public int? CustomerId { get; set; }
[Required]
public string SkuCode { get; set; } = string.Empty;
[Required]
public string SkuName { get; set; } = string.Empty;
public string? SkuImageUrl { get; set; }
public decimal? WeightG { get; set; }
public decimal? LengthCm { get; set; }
public decimal? WidthCm { get; set; }
public decimal? HeightCm { get; set; }
public string? Remark { get; set; }
}
public class WarehouseSkuBindMappingInput
{
public int? CustomerId { get; set; }
[Required]
public int StoreId { get; set; }
[Required]
public string PlatformSku { get; set; } = string.Empty;
[Required]
public int SkuId { get; set; }
}
public class WarehouseSkuMappingOutput
{
public int Id { get; set; }
public int StoreId { get; set; }
public string PlatformSku { get; set; } = string.Empty;
public int SkuId { get; set; }
public string SkuCode { get; set; } = string.Empty;
}
// --- Bin Location ---
public class WarehouseBinLocationListInput
{
public int WarehouseId { get; set; }
public string? Keyword { get; set; }
public bool? IsEnabled { get; set; }
}
public class WarehouseBinLocationOutput
{
public int Id { get; set; }
public int WarehouseId { get; set; }
public string BinCode { get; set; } = string.Empty;
public string? BinName { get; set; }
public string? ZoneCode { get; set; }
public bool IsEnabled { get; set; }
public string? Remark { get; set; }
}
public class WarehouseBinLocationSaveInput
{
public int Id { get; set; }
[Required]
public int WarehouseId { get; set; }
[Required]
public string BinCode { get; set; } = string.Empty;
public string? BinName { get; set; }
public string? ZoneCode { get; set; }
public bool IsEnabled { get; set; } = true;
public string? Remark { get; set; }
}
// --- Inbound ---
public class WarehouseInboundListInput : WarehousePageInput
{
public int? WarehouseId { get; set; }
public string? ReceiptStatus { get; set; }
public string? Keyword { get; set; }
}
public class WarehouseInboundOutput
{
public int Id { get; set; }
public int CustomerId { get; set; }
public int WarehouseId { get; set; }
public string NoticeNo { get; set; } = string.Empty;
public string? MarkReference { get; set; }
public string InboundType { get; set; } = string.Empty;
public string ReceiptStatus { get; set; } = string.Empty;
public DateTime? ExpectedArrivalTime { get; set; }
public DateTime? ActualArrivalTime { get; set; }
public string? Remark { get; set; }
public int BoxCount { get; set; }
public int PlannedQty { get; set; }
public DateTime CreateTime { get; set; }
}
public class WarehouseInboundLineSaveInput
{
public int SkuId { get; set; }
public string SkuCode { get; set; } = string.Empty;
public int PlannedQty { get; set; }
}
public class WarehouseInboundBoxSaveInput
{
public string BoxSeqNo { get; set; } = string.Empty;
public string? MarkBoxNo { get; set; }
public decimal? WeightG { get; set; }
public decimal? LengthCm { get; set; }
public decimal? WidthCm { get; set; }
public decimal? HeightCm { get; set; }
public List<WarehouseInboundLineSaveInput> Lines { get; set; } = [];
}
public class WarehouseInboundSaveInput
{
public int Id { get; set; }
public int? CustomerId { get; set; }
[Required]
public int WarehouseId { get; set; }
public string? MarkReference { get; set; }
public string InboundType { get; set; } = "normal";
public DateTime? ExpectedArrivalTime { get; set; }
public string? Remark { get; set; }
public List<WarehouseInboundBoxSaveInput> Boxes { get; set; } = [];
}
public class WarehouseInboundLineOutput
{
public int Id { get; set; }
public int SkuId { get; set; }
public string SkuCode { get; set; } = string.Empty;
public int PlannedQty { get; set; }
public int? ReceivedQty { get; set; }
public int? BinLocationId { get; set; }
}
public class WarehouseInboundBoxOutput
{
public int Id { get; set; }
public string BoxSeqNo { get; set; } = string.Empty;
public string? MarkBoxNo { get; set; }
public decimal? WeightG { get; set; }
public decimal? LengthCm { get; set; }
public decimal? WidthCm { get; set; }
public decimal? HeightCm { get; set; }
public bool IsReceived { get; set; }
public List<WarehouseInboundLineOutput> Lines { get; set; } = [];
}
public class WarehouseInboundDetailOutput : WarehouseInboundOutput
{
public List<WarehouseInboundBoxOutput> Boxes { get; set; } = [];
}
public class WarehouseInboundConfirmLineInput
{
public int LineId { get; set; }
public int ReceivedQty { get; set; }
public int BinLocationId { get; set; }
}
public class WarehouseInboundConfirmBoxInput
{
public int BoxId { get; set; }
public List<WarehouseInboundConfirmLineInput> Lines { get; set; } = [];
}
public class WarehouseInboundConfirmReceiveInput
{
[Required]
public int InboundNoticeId { get; set; }
public int? CustomerId { get; set; }
public List<WarehouseInboundConfirmBoxInput> Boxes { get; set; } = [];
}
public class WarehouseInboundSummaryOutput
{
public int Forecasted { get; set; }
public int Received { get; set; }
public int Completed { get; set; }
}
// --- Parcel ---
public class WarehouseParcelListInput : WarehousePageInput
{
public int? WarehouseId { get; set; }
public string? LifecycleStatus { get; set; }
public string? Keyword { get; set; }
}
public class WarehouseParcelItemSaveInput
{
public int Id { get; set; }
public string? ProductName { get; set; }
public int Quantity { get; set; }
public string? Remark { get; set; }
}
public class WarehouseParcelItemOutput
{
public int Id { get; set; }
public string? ProductName { get; set; }
public int Quantity { get; set; }
public int? SkuId { get; set; }
public string? SkuCode { get; set; }
public int? BinLocationId { get; set; }
public string? Remark { get; set; }
}
public class WarehouseParcelOutput
{
public int Id { get; set; }
public int CustomerId { get; set; }
public int WarehouseId { get; set; }
public string TrackingNo { get; set; } = string.Empty;
public string? CarrierName { get; set; }
public string LifecycleStatus { get; set; } = string.Empty;
public DateTime? ArrivalTime { get; set; }
public string? Remark { get; set; }
public List<WarehouseParcelItemOutput> Items { get; set; } = [];
public DateTime CreateTime { get; set; }
}
public class WarehouseParcelSaveInput
{
public int Id { get; set; }
public int? CustomerId { get; set; }
[Required]
public int WarehouseId { get; set; }
[Required]
public string TrackingNo { get; set; } = string.Empty;
public string? CarrierName { get; set; }
public string LifecycleStatus { get; set; } = "inTransit";
public string? Remark { get; set; }
public List<WarehouseParcelItemSaveInput> Items { get; set; } = [];
}
public class WarehouseParcelPutawayItemInput
{
public int ItemId { get; set; }
public int SkuId { get; set; }
public int BinLocationId { get; set; }
}
public class WarehouseParcelPutawayInput
{
[Required]
public int ParcelId { get; set; }
public int? CustomerId { get; set; }
public List<WarehouseParcelPutawayItemInput> Items { get; set; } = [];
}
// --- Inventory ---
public class WarehouseInventoryListInput : WarehousePageInput
{
public int? WarehouseId { get; set; }
public string? Keyword { get; set; }
}
public class WarehouseInventoryOutput
{
public int SkuId { get; set; }
public string SkuCode { get; set; } = string.Empty;
public string SkuName { get; set; } = string.Empty;
public int WarehouseId { get; set; }
public int TotalQty { get; set; }
public int BinCount { get; set; }
}
public class WarehouseInventoryBinDetailOutput
{
public int Id { get; set; }
public int BinLocationId { get; set; }
public string BinCode { get; set; } = string.Empty;
public string? BinName { get; set; }
public int Qty { get; set; }
}
public class WarehouseInventoryRelocateInput
{
public int? CustomerId { get; set; }
[Required]
public int WarehouseId { get; set; }
[Required]
public int SkuId { get; set; }
[Required]
public int SourceBinLocationId { get; set; }
[Required]
public int TargetBinLocationId { get; set; }
[Range(1, int.MaxValue)]
public int Qty { get; set; }
}

View File

@ -0,0 +1,263 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using Fake.Application;
using Oms.Application.Dtos.Orders;
using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Entities;
using Oms.Domain.SystemAggregate.Entities;
using Oms.Domain.WarehouseAggregate.Entities;
namespace Oms.Application.Services.Orders;
/// <summary>
/// 将店铺订单转换为小包创建入参。
/// </summary>
public static class PlatformStoreOrderParcelBuilder
{
private const decimal DefaultLengthCm = 10m;
private const decimal DefaultWidthCm = 10m;
private const decimal DefaultHeightCm = 10m;
private const decimal MinActualWeightKg = 0.01m;
public static OrderParcelCreateInput Build(
SalesStoreOrder storeOrder,
IReadOnlyCollection<SalesStoreOrderLine> lines,
IReadOnlyDictionary<int, CSku> skuMap,
WarehouseAddress warehouse,
string productCode,
int warehouseId,
int customerId,
OrderParcelReceiverInput? receiverOverride = null,
string? trackNoOverride = null)
{
var receiver = receiverOverride ?? ParseReceiver(storeOrder);
ValidateReceiver(receiver);
var declarationLines = BuildDeclarationLines(lines, skuMap);
var (length, width, height, actualWeightKg) = ResolveParcelMetrics(declarationLines, lines, skuMap);
return new OrderParcelCreateInput
{
RequestId = $"platform-store-order:{storeOrder.Id}",
ClientNo = storeOrder.PlatformOrderNo,
TrackNo = string.IsNullOrWhiteSpace(trackNoOverride) ? storeOrder.ShippingLabelNo : trackNoOverride.Trim(),
ProductCode = productCode,
WarehouseId = warehouseId,
CustomerId = customerId,
Length = length,
Width = width,
Height = height,
ActualWeight = actualWeightKg,
ChargeableWeight = actualWeightKg,
CargoAttribute = CargoAttributeCodes.General,
DeclaredCurrency = string.IsNullOrWhiteSpace(storeOrder.Currency) ? "USD" : storeOrder.Currency.Trim(),
DeclaredSum = storeOrder.TotalAmount,
Quantity = 1,
Receiver = receiver,
Sender = BuildSender(warehouse),
Items = declarationLines
};
}
private static List<OrderParcelItemInput> BuildDeclarationLines(
IReadOnlyCollection<SalesStoreOrderLine> lines,
IReadOnlyDictionary<int, CSku> skuMap)
{
var items = new List<OrderParcelItemInput>();
foreach (var line in lines.Where(x => !x.IsShipExcluded))
{
if (!line.MappedSkuId.HasValue || !skuMap.TryGetValue(line.MappedSkuId.Value, out var sku))
{
throw new BusinessException($"申报行「{line.PlatformSku}」缺少库内 SKU 映射");
}
var unitWeightKg = sku.WeightG is > 0 ? Math.Max(sku.WeightG.Value / 1000m, MinActualWeightKg) : MinActualWeightKg;
var unitPrice = line.UnitPrice is > 0 ? line.UnitPrice.Value : 1m;
var cnName = string.IsNullOrWhiteSpace(line.ProductName) ? sku.SkuName : line.ProductName.Trim();
var enName = string.IsNullOrWhiteSpace(sku.SkuName) ? cnName : sku.SkuName;
items.Add(new OrderParcelItemInput
{
CnName = cnName,
EnName = enName,
Sku = sku.SkuCode,
Unit = "PCS",
UnitPrice = unitPrice,
UnitWeight = unitWeightKg,
Quantity = line.Quantity,
Amount = unitPrice * line.Quantity,
Weight = unitWeightKg * line.Quantity
});
}
if (items.Count == 0)
{
throw new BusinessException("没有可申报的商品行");
}
return items;
}
private static (decimal Length, decimal Width, decimal Height, decimal ActualWeightKg) ResolveParcelMetrics(
IReadOnlyCollection<OrderParcelItemInput> items,
IReadOnlyCollection<SalesStoreOrderLine> lines,
IReadOnlyDictionary<int, CSku> skuMap)
{
decimal maxLength = DefaultLengthCm;
decimal maxWidth = DefaultWidthCm;
decimal maxHeight = DefaultHeightCm;
decimal totalWeightKg = 0m;
foreach (var line in lines.Where(x => !x.IsShipExcluded && x.MappedSkuId.HasValue))
{
if (!skuMap.TryGetValue(line.MappedSkuId!.Value, out var sku))
{
continue;
}
if (sku.LengthCm is > 0)
{
maxLength = Math.Max(maxLength, sku.LengthCm.Value);
}
if (sku.WidthCm is > 0)
{
maxWidth = Math.Max(maxWidth, sku.WidthCm.Value);
}
if (sku.HeightCm is > 0)
{
maxHeight = Math.Max(maxHeight, sku.HeightCm.Value);
}
var unitWeightKg = sku.WeightG is > 0 ? sku.WeightG.Value / 1000m : MinActualWeightKg;
totalWeightKg += unitWeightKg * line.Quantity;
}
if (totalWeightKg <= 0)
{
totalWeightKg = Math.Max(items.Sum(x => x.Weight ?? 0m), MinActualWeightKg);
}
return (maxLength, maxWidth, maxHeight, Math.Max(totalWeightKg, MinActualWeightKg));
}
private static OrderParcelSenderInput BuildSender(WarehouseAddress warehouse)
{
var senderName = string.IsNullOrWhiteSpace(warehouse.Recipient) ? warehouse.WarehouseName : warehouse.Recipient.Trim();
return new OrderParcelSenderInput
{
SenderName = senderName,
Phone = warehouse.ContactPhone,
StreetAddress1 = string.IsNullOrWhiteSpace(warehouse.ShippingAddress) ? warehouse.WarehouseName : warehouse.ShippingAddress.Trim()
};
}
private static OrderParcelReceiverInput? ParseReceiver(SalesStoreOrder storeOrder)
{
if (!string.IsNullOrWhiteSpace(storeOrder.PlatformExtraJson))
{
try
{
var node = JsonNode.Parse(storeOrder.PlatformExtraJson);
if (node != null)
{
var parsed = ParseReceiverNode(node);
if (parsed != null)
{
return parsed;
}
}
}
catch (JsonException)
{
// 忽略 JSON 解析错误,走兜底
}
}
if (string.IsNullOrWhiteSpace(storeOrder.OrderCountry))
{
return null;
}
return new OrderParcelReceiverInput
{
RecipientsName = "Platform Recipient",
CountryCode = storeOrder.OrderCountry.Trim(),
ZipCode = "00000",
StreetAddress1 = "TBD"
};
}
private static OrderParcelReceiverInput? ParseReceiverNode(JsonNode node)
{
var candidates = new List<JsonNode?> { node };
foreach (var key in new[] { "recipient", "receiver", "shippingAddress", "address" })
{
if (node[key] != null)
{
candidates.Add(node[key]);
}
}
foreach (var candidate in candidates)
{
if (candidate == null)
{
continue;
}
var name = ReadText(candidate, "recipientsName", "recipientName", "receiverName", "name", "fullName");
var country = ReadText(candidate, "countryCode", "country", "region", "orderCountry") ?? ReadText(node, "orderCountry");
var zip = ReadText(candidate, "zipCode", "zip", "postalCode", "postcode");
var address = ReadText(candidate, "streetAddress1", "address", "address1", "fullAddress", "detailAddress");
if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(country) || string.IsNullOrWhiteSpace(zip) || string.IsNullOrWhiteSpace(address))
{
continue;
}
return new OrderParcelReceiverInput
{
RecipientsName = name,
CompanyName = ReadText(candidate, "companyName", "company"),
Phone = ReadText(candidate, "phone", "mobile", "phoneNumber"),
TelePhone = ReadText(candidate, "telePhone", "tel"),
Email = ReadText(candidate, "email"),
CountryCode = country,
Province = ReadText(candidate, "province", "state", "regionState"),
City = ReadText(candidate, "city"),
StreetAddress1 = address,
StreetAddress2 = ReadText(candidate, "streetAddress2", "address2"),
ZipCode = zip
};
}
return null;
}
private static string? ReadText(JsonNode node, params string[] keys)
{
foreach (var key in keys)
{
var value = node[key]?.ToString()?.Trim();
if (!string.IsNullOrWhiteSpace(value))
{
return value;
}
}
return null;
}
private static void ValidateReceiver(OrderParcelReceiverInput? receiver)
{
if (receiver == null
|| string.IsNullOrWhiteSpace(receiver.RecipientsName)
|| string.IsNullOrWhiteSpace(receiver.CountryCode)
|| string.IsNullOrWhiteSpace(receiver.ZipCode)
|| string.IsNullOrWhiteSpace(receiver.StreetAddress1))
{
throw new BusinessException("收件人信息不完整,请同步平台订单或在匹配时补充收件人");
}
}
}

View File

@ -0,0 +1,612 @@
using System.Linq.Expressions;
using Fake.Application;
using Microsoft.AspNetCore.Mvc;
using Oms.Application.Dtos.Orders;
using Oms.Application.Dtos.PlatformStore;
using Oms.Application.Dtos.Shared;
using Oms.Application.Shared;
using Oms.Domain.ChannelsAggregate.Repositories;
using Oms.Domain.CustomersAggregate.Repositories;
using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Entities;
using Oms.Domain.OrdersAggregate.Repositories;
using Oms.Domain.Shared.Enums;
using Oms.Domain.SystemAggregate.Repositories;
using Oms.Domain.WarehouseAggregate.Consts;
using Oms.Domain.WarehouseAggregate.Repositories;
namespace Oms.Application.Services.Orders;
/// <summary>
/// 店铺订单 API 服务。
/// </summary>
[ApiExplorerSettings(GroupName = "OMS-Order")]
public class PlatformStoreOrderService : ApplicationService
{
private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService<ICustomerRepository>();
private ISalesStoreRepository StoreRepository => LazyServiceProvider.GetRequiredService<ISalesStoreRepository>();
private ISalesStoreOrderRepository StoreOrderRepository => LazyServiceProvider.GetRequiredService<ISalesStoreOrderRepository>();
private ISalesStoreOrderLineRepository StoreOrderLineRepository => LazyServiceProvider.GetRequiredService<ISalesStoreOrderLineRepository>();
private ICSkuRepository SkuRepository => LazyServiceProvider.GetRequiredService<ICSkuRepository>();
private ICSkuPlatformMapRepository SkuPlatformMapRepository => LazyServiceProvider.GetRequiredService<ICSkuPlatformMapRepository>();
private ICInventoryRepository InventoryRepository => LazyServiceProvider.GetRequiredService<ICInventoryRepository>();
private IOrderRepository OrderRepository => LazyServiceProvider.GetRequiredService<IOrderRepository>();
private IWarehouseAddressRepository WarehouseAddressRepository => LazyServiceProvider.GetRequiredService<IWarehouseAddressRepository>();
private ISalesProductRepository SalesProductRepository => LazyServiceProvider.GetRequiredService<ISalesProductRepository>();
private OrderService OrderService => LazyServiceProvider.GetRequiredService<OrderService>();
[HttpPost("/order/platform-store-order/list")]
public async Task<OmsPageResult<PlatformStoreOrderOutput>> GetListAsync(PlatformStoreOrderListInput input)
{
var customerId = await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync(
CustomerRepository,
CurrentUser,
input.CustomerId);
var predicate = BuildListPredicate(customerId, input);
var pageIndex = OmsDocHelper.NormalizePage(input.Page);
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
var entities = await StoreOrderRepository.GetPagedListAsync(
predicate,
pageIndex,
pageSize,
new Dictionary<string, bool> { [nameof(SalesStoreOrder.UpdateTime)] = false, [nameof(SalesStoreOrder.Id)] = false });
var total = await StoreOrderRepository.CountAsync(predicate);
var storeIds = entities.Select(x => x.StoreId).Distinct().ToArray();
var stores = storeIds.Length == 0
? []
: await StoreRepository.GetListAsync(x => storeIds.Contains(x.Id));
var storeMap = stores.ToDictionary(x => x.Id);
var linkedOrderIds = entities.Where(x => x.LinkedOrderId is > 0).Select(x => x.LinkedOrderId!.Value).Distinct().ToArray();
var linkedOrders = linkedOrderIds.Length == 0
? []
: await OrderRepository.GetListAsync(x => linkedOrderIds.Contains(x.Id));
var linkedOrderMap = linkedOrders.ToDictionary(x => x.Id);
return new OmsPageResult<PlatformStoreOrderOutput>
{
Items = entities.Select(x =>
{
storeMap.TryGetValue(x.StoreId, out var store);
string? linkedServiceNo = null;
if (x.LinkedOrderId is > 0 && linkedOrderMap.TryGetValue(x.LinkedOrderId.Value, out var linkedOrder))
{
linkedServiceNo = linkedOrder.ServiceNo;
}
return MapOrderOutput(x, store?.StoreNickname ?? string.Empty, [], linkedServiceNo: linkedServiceNo);
}).ToList(),
Total = total
};
}
[HttpGet("/order/platform-store-order/detail")]
public async Task<PlatformStoreOrderOutput> GetDetailAsync([FromQuery] int id, [FromQuery] int? customerId = null)
{
var resolvedCustomerId = await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync(
CustomerRepository,
CurrentUser,
customerId);
var order = await GetOrderOrThrowAsync(resolvedCustomerId, id);
var store = await StoreRepository.FirstOrDefaultAsync(x => x.Id == order.StoreId);
var lines = await StoreOrderLineRepository.GetListAsync(x => x.StoreOrderId == order.Id);
var skuIds = lines.Where(x => x.MappedSkuId.HasValue).Select(x => x.MappedSkuId!.Value).Distinct().ToArray();
var skus = skuIds.Length == 0
? []
: await SkuRepository.GetListAsync(x => skuIds.Contains(x.Id));
var skuMap = skus.ToDictionary(x => x.Id);
var linkedServiceNo = await ResolveLinkedServiceNoAsync(order.LinkedOrderId);
return MapOrderOutput(order, store?.StoreNickname ?? string.Empty, lines, skuMap, linkedServiceNo);
}
[HttpGet("/order/platform-store-order/summary")]
public async Task<PlatformStoreOrderSummaryOutput> GetSummaryAsync([FromQuery] int? customerId = null, [FromQuery] int? storeId = null)
{
var resolvedCustomerId = await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync(
CustomerRepository,
CurrentUser,
customerId);
var orders = await StoreOrderRepository.GetListAsync(x =>
x.CustomerId == resolvedCustomerId
&& (!storeId.HasValue || x.StoreId == storeId.Value));
return new PlatformStoreOrderSummaryOutput
{
Pending = orders.Count(x => x.FulfillStatus == SalesStoreFulfillStatusCodes.Pending),
ToShip = orders.Count(x => x.FulfillStatus == SalesStoreFulfillStatusCodes.ToShip),
Shipped = orders.Count(x => x.FulfillStatus == SalesStoreFulfillStatusCodes.Shipped),
Canceled = orders.Count(x => x.FulfillStatus == SalesStoreFulfillStatusCodes.Canceled),
Suspended = orders.Count(x => x.IsSuspended),
Completed = orders.Count(x => x.FulfillStatus == SalesStoreFulfillStatusCodes.Completed)
};
}
[HttpPost("/order/platform-store-order/sync-status")]
public async Task<PlatformStoreOrderActionOutput> SyncStatusAsync(PlatformStoreOrderIdsInput input)
{
await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync(
CustomerRepository,
CurrentUser,
input.CustomerId);
return new PlatformStoreOrderActionOutput
{
Message = "平台订单同步待 SDK 接入,当前为占位实现"
};
}
[HttpPost("/order/platform-store-order/ready-to-ship")]
public async Task<PlatformStoreOrderActionOutput> ReadyToShipAsync(PlatformStoreOrderIdsInput input)
{
await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync(
CustomerRepository,
CurrentUser,
input.CustomerId);
return new PlatformStoreOrderActionOutput
{
Message = "RTS 待平台 SDK 接入,当前为占位实现"
};
}
[HttpPost("/order/platform-store-order/fetch-label")]
public async Task<PlatformStoreOrderActionOutput> FetchLabelAsync(PlatformStoreOrderIdInput input)
{
await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync(
CustomerRepository,
CurrentUser,
input.CustomerId);
return new PlatformStoreOrderActionOutput
{
Message = "面单拉取待平台 SDK 接入,当前为占位实现"
};
}
[HttpPost("/order/platform-store-order/toggle-suspend")]
public async Task ToggleSuspendAsync(PlatformStoreOrderToggleSuspendInput input)
{
var customerId = await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync(
CustomerRepository,
CurrentUser,
input.CustomerId);
var order = await GetOrderOrThrowAsync(customerId, input.Id);
order.IsSuspended = input.IsSuspended;
order.FulfillStatus = input.IsSuspended
? SalesStoreFulfillStatusCodes.Suspended
: SalesStoreFulfillStatusCodes.Pending;
await StoreOrderRepository.UpdateAsync(order);
}
[HttpPost("/order/platform-store-order/delete")]
public async Task DeleteAsync(PlatformStoreOrderIdsInput input)
{
if (input.Ids.Count == 0)
{
throw new BusinessException("请选择要删除的订单");
}
var customerId = await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync(
CustomerRepository,
CurrentUser,
input.CustomerId);
var orders = await StoreOrderRepository.GetListAsync(x => x.CustomerId == customerId && input.Ids.Contains(x.Id));
foreach (var order in orders)
{
var lines = await StoreOrderLineRepository.GetListAsync(x => x.StoreOrderId == order.Id);
foreach (var line in lines)
{
await StoreOrderLineRepository.DeleteAsync(line);
}
await StoreOrderRepository.DeleteAsync(order);
}
}
[HttpPost("/order/platform-store-order/match-inventory")]
public async Task<PlatformStoreOrderMatchInventoryOutput> MatchInventoryAsync(PlatformStoreOrderMatchInventoryInput input)
{
var customerId = await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync(
CustomerRepository,
CurrentUser,
input.CustomerId);
var result = await MatchInventoryCoreAsync(
customerId,
input.Id,
input.WarehouseId,
input.ProductCode,
input.TrackNo,
input.Receiver);
return new PlatformStoreOrderMatchInventoryOutput
{
Success = true,
Message = $"库存匹配成功,已创建小包订单 {result.ServiceNo}",
LinkedOrderId = result.OrderId,
ServiceNo = result.ServiceNo,
ClientNo = result.ClientNo
};
}
[HttpPost("/order/platform-store-order/batch-match-inventory")]
public async Task<PlatformStoreOrderBatchMatchInventoryOutput> BatchMatchInventoryAsync(
PlatformStoreOrderBatchMatchInventoryInput input)
{
if (input.Ids.Count == 0)
{
throw new BusinessException("请选择要匹配的订单");
}
var customerId = await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync(
CustomerRepository,
CurrentUser,
input.CustomerId);
var output = new PlatformStoreOrderBatchMatchInventoryOutput();
foreach (var id in input.Ids.Distinct())
{
var storeOrder = await StoreOrderRepository.FirstOrDefaultAsync(x => x.Id == id && x.CustomerId == customerId);
var item = new PlatformStoreOrderBatchMatchItemOutput
{
Id = id,
PlatformOrderNo = storeOrder?.PlatformOrderNo ?? id.ToString()
};
try
{
var result = await MatchInventoryCoreAsync(
customerId,
id,
input.WarehouseId,
input.ProductCode,
null,
null);
item.Success = true;
item.Message = $"已创建小包订单 {result.ServiceNo}";
item.LinkedOrderId = result.OrderId;
item.ServiceNo = result.ServiceNo;
output.SuccessCount++;
}
catch (BusinessException ex)
{
item.Success = false;
item.Message = ex.Message;
output.FailCount++;
}
catch (Exception ex)
{
item.Success = false;
item.Message = ex.Message;
output.FailCount++;
}
output.Items.Add(item);
}
return output;
}
private async Task<OrderParcelCreateOutput> MatchInventoryCoreAsync(
int customerId,
int storeOrderId,
int? warehouseId,
string? productCode,
string? trackNo,
OrderParcelReceiverInput? receiver)
{
var order = await GetOrderOrThrowAsync(customerId, storeOrderId);
await EnsureMatchableAsync(order);
var resolvedWarehouseId = warehouseId ?? order.WarehouseId;
if (!resolvedWarehouseId.HasValue || resolvedWarehouseId.Value <= 0)
{
throw new BusinessException("请指定发货仓库");
}
var warehouse = await WarehouseAddressRepository.FirstOrDefaultAsync(x => x.Id == resolvedWarehouseId.Value)
?? throw new BusinessException("仓库不存在");
var lines = await StoreOrderLineRepository.GetListAsync(x => x.StoreOrderId == order.Id && !x.IsShipExcluded);
if (lines.Count == 0)
{
throw new BusinessException("没有可发货的商品行");
}
await ResolveMappedSkuLinesAsync(customerId, order, lines);
var requiredQty = lines
.Where(x => x.MappedSkuId.HasValue)
.GroupBy(x => x.MappedSkuId!.Value)
.ToDictionary(g => g.Key, g => g.Sum(x => x.Quantity));
await EnsureInventoryAvailableAsync(customerId, resolvedWarehouseId.Value, requiredQty);
var skuIds = requiredQty.Keys.ToArray();
var skus = await SkuRepository.GetListAsync(x => x.CustomerId == customerId && skuIds.Contains(x.Id));
var skuMap = skus.ToDictionary(x => x.Id);
if (skuMap.Count != skuIds.Length)
{
throw new BusinessException("部分库内 SKU 不存在");
}
var resolvedProductCode = await ResolveProductCodeAsync(productCode);
var parcelInput = PlatformStoreOrderParcelBuilder.Build(
order,
lines,
skuMap,
warehouse,
resolvedProductCode,
resolvedWarehouseId.Value,
customerId,
receiver,
trackNo);
List<InventoryDeduction> deductions = [];
try
{
foreach (var (skuId, qty) in requiredQty)
{
deductions.AddRange(await DeductInventoryAsync(customerId, resolvedWarehouseId.Value, skuId, qty));
}
var parcelOutput = await OrderService.CreateParcelForBatchAsync(parcelInput, CurrentUser.GetId<int>());
order.WarehouseId = resolvedWarehouseId;
order.LinkedOrderId = parcelOutput.OrderId;
order.FulfillStatus = SalesStoreFulfillStatusCodes.ToShip;
order.LastSyncTime = DateTime.Now;
if (!string.IsNullOrWhiteSpace(parcelOutput.TrackNo))
{
order.ShippingLabelNo = parcelOutput.TrackNo;
}
await StoreOrderRepository.UpdateAsync(order);
return parcelOutput;
}
catch
{
await RestoreInventoryAsync(customerId, resolvedWarehouseId.Value, deductions);
throw;
}
}
private async Task EnsureMatchableAsync(SalesStoreOrder order)
{
if (order.IsSuspended)
{
throw new BusinessException("订单已搁置,不能匹配库存");
}
if (order.FulfillStatus is SalesStoreFulfillStatusCodes.Shipped or SalesStoreFulfillStatusCodes.Completed)
{
throw new BusinessException("订单已发货或完成,不能重复匹配");
}
if (order.LinkedOrderId is > 0)
{
var linked = await OrderRepository.FirstOrDefaultAsync(x => x.Id == order.LinkedOrderId.Value);
if (linked != null && linked.OrderStatus != OrderStatusCodes.Deleted)
{
throw new BusinessException($"已关联小包订单 {linked.ServiceNo},不能重复匹配");
}
}
}
private async Task ResolveMappedSkuLinesAsync(int customerId, SalesStoreOrder order, List<SalesStoreOrderLine> lines)
{
var mappings = await SkuPlatformMapRepository.GetListAsync(x => x.CustomerId == customerId && x.StoreId == order.StoreId);
var mappingDict = mappings.ToDictionary(x => x.PlatformSku, x => x, StringComparer.OrdinalIgnoreCase);
foreach (var line in lines)
{
if (line.MappedSkuId.HasValue)
{
continue;
}
if (!string.IsNullOrWhiteSpace(line.PlatformSku) && mappingDict.TryGetValue(line.PlatformSku, out var map))
{
line.MappedSkuId = map.SkuId;
await StoreOrderLineRepository.UpdateAsync(line);
continue;
}
throw new BusinessException($"平台 SKU「{line.PlatformSku}」未映射库内 SKU");
}
}
private async Task EnsureInventoryAvailableAsync(int customerId, int warehouseId, Dictionary<int, int> requiredQty)
{
foreach (var (skuId, qty) in requiredQty)
{
var total = await InventoryRepository.GetListAsync(x =>
x.CustomerId == customerId
&& x.WarehouseId == warehouseId
&& x.SkuId == skuId
&& x.Qty > 0);
var available = total.Sum(x => x.Qty);
if (available < qty)
{
throw new BusinessException($"SKU {skuId} 库存不足,需要 {qty},可用 {available}");
}
}
}
private async Task<string> ResolveProductCodeAsync(string? productCode)
{
var normalized = productCode?.Trim();
if (!string.IsNullOrWhiteSpace(normalized))
{
var product = await SalesProductRepository.GetByProductCodeAsync(normalized);
if (product == null || product.IsActive != EnableStatus.Enabled)
{
throw new BusinessException("销售产品不存在或未启用");
}
return normalized;
}
var products = await SalesProductRepository.GetListAsync(x => x.IsActive == EnableStatus.Enabled);
var first = products.OrderBy(x => x.ProductCode).FirstOrDefault();
if (first == null || string.IsNullOrWhiteSpace(first.ProductCode))
{
throw new BusinessException("请指定销售产品编码");
}
return first.ProductCode;
}
private async Task<List<InventoryDeduction>> DeductInventoryAsync(int customerId, int warehouseId, int skuId, int requiredQty)
{
var rows = await InventoryRepository.GetListAsync(x =>
x.CustomerId == customerId
&& x.WarehouseId == warehouseId
&& x.SkuId == skuId
&& x.Qty > 0);
rows = rows.OrderBy(x => x.BinLocationId).ToList();
var deductions = new List<InventoryDeduction>();
var remaining = requiredQty;
foreach (var row in rows)
{
if (remaining <= 0)
{
break;
}
var deduct = Math.Min(row.Qty, remaining);
row.Qty -= deduct;
remaining -= deduct;
await InventoryRepository.UpdateAsync(row);
deductions.Add(new InventoryDeduction(skuId, row.BinLocationId, deduct));
}
if (remaining > 0)
{
throw new BusinessException($"SKU {skuId} 库存扣减失败");
}
return deductions;
}
private async Task RestoreInventoryAsync(int customerId, int warehouseId, IReadOnlyCollection<InventoryDeduction> deductions)
{
foreach (var item in deductions)
{
var inventory = await InventoryRepository.FirstOrDefaultAsync(x =>
x.CustomerId == customerId
&& x.WarehouseId == warehouseId
&& x.SkuId == item.SkuId
&& x.BinLocationId == item.BinLocationId);
if (inventory == null)
{
inventory = new Domain.WarehouseAggregate.Entities.CInventory
{
CustomerId = customerId,
WarehouseId = warehouseId,
SkuId = item.SkuId,
BinLocationId = item.BinLocationId,
Qty = item.Qty
};
await InventoryRepository.InsertAsync(inventory);
continue;
}
inventory.Qty += item.Qty;
await InventoryRepository.UpdateAsync(inventory);
}
}
private async Task<string?> ResolveLinkedServiceNoAsync(int? linkedOrderId)
{
if (linkedOrderId is not > 0)
{
return null;
}
var linked = await OrderRepository.FirstOrDefaultAsync(x => x.Id == linkedOrderId.Value);
return linked?.ServiceNo;
}
private async Task<SalesStoreOrder> GetOrderOrThrowAsync(int customerId, int id)
{
var entity = await StoreOrderRepository.FirstOrDefaultAsync(x => x.Id == id && x.CustomerId == customerId);
if (entity == null)
{
throw new BusinessException("店铺订单不存在");
}
return entity;
}
private static Expression<Func<SalesStoreOrder, bool>> BuildListPredicate(int customerId, PlatformStoreOrderListInput input)
{
var fulfillStatus = input.FulfillStatus?.Trim();
var platformOrderNo = input.PlatformOrderNo?.Trim();
return x =>
x.CustomerId == customerId
&& (!input.StoreId.HasValue || x.StoreId == input.StoreId.Value)
&& (string.IsNullOrWhiteSpace(fulfillStatus) || x.FulfillStatus == fulfillStatus)
&& (!input.IsSuspended.HasValue || x.IsSuspended == input.IsSuspended.Value)
&& (string.IsNullOrWhiteSpace(platformOrderNo) || x.PlatformOrderNo.Contains(platformOrderNo));
}
private static PlatformStoreOrderOutput MapOrderOutput(
SalesStoreOrder order,
string storeNickname,
IReadOnlyCollection<SalesStoreOrderLine> lines,
IReadOnlyDictionary<int, Domain.WarehouseAggregate.Entities.CSku>? skuMap = null,
string? linkedServiceNo = null) =>
new()
{
Id = order.Id,
CustomerId = order.CustomerId,
StoreId = order.StoreId,
StoreNickname = storeNickname,
PlatformCode = order.PlatformCode,
PlatformOrderNo = order.PlatformOrderNo,
OrderCountry = order.OrderCountry,
ItemCount = order.ItemCount,
TotalAmount = order.TotalAmount,
Currency = order.Currency,
PlatformOrderTime = order.PlatformOrderTime,
FulfillStatus = order.FulfillStatus,
PlatformStatusLabel = order.PlatformStatusLabel,
ShippingLabelNo = order.ShippingLabelNo,
ShippingLabelUrl = order.ShippingLabelUrl,
IsMultiLabel = order.IsMultiLabel,
CustomTags = order.CustomTags,
IsSuspended = order.IsSuspended,
WarehouseId = order.WarehouseId,
LinkedOrderId = order.LinkedOrderId,
LinkedServiceNo = linkedServiceNo,
LastSyncTime = order.LastSyncTime,
Lines = lines.Select(line =>
{
string? mappedSkuCode = null;
if (line.MappedSkuId.HasValue && skuMap != null && skuMap.TryGetValue(line.MappedSkuId.Value, out var sku))
{
mappedSkuCode = sku.SkuCode;
}
return new PlatformStoreOrderLineOutput
{
Id = line.Id,
PlatformLineId = line.PlatformLineId,
PlatformSku = line.PlatformSku,
ProductName = line.ProductName,
ProductImageUrl = line.ProductImageUrl,
VariantText = line.VariantText,
UnitPrice = line.UnitPrice,
Quantity = line.Quantity,
Currency = line.Currency,
IsShipExcluded = line.IsShipExcluded,
MappedSkuId = line.MappedSkuId,
MappedSkuCode = mappedSkuCode
};
}).ToList()
};
private sealed record InventoryDeduction(int SkuId, int BinLocationId, int Qty);
}

View File

@ -0,0 +1,192 @@
using System.Linq.Expressions;
using Fake.Application;
using Microsoft.AspNetCore.Mvc;
using Oms.Application.Dtos.PlatformStore;
using Oms.Application.Dtos.Shared;
using Oms.Application.Shared;
using Oms.Domain.CustomersAggregate.Repositories;
using Oms.Domain.OrdersAggregate.Entities;
using Oms.Domain.OrdersAggregate.Repositories;
using Oms.Domain.WarehouseAggregate.Consts;
namespace Oms.Application.Services.Orders;
/// <summary>
/// 店铺授权 API 服务。
/// </summary>
[ApiExplorerSettings(GroupName = "OMS-Order")]
public class PlatformStoreService : ApplicationService
{
private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService<ICustomerRepository>();
private ISalesStoreRepository StoreRepository => LazyServiceProvider.GetRequiredService<ISalesStoreRepository>();
[HttpPost("/order/platform-store/list")]
public async Task<OmsPageResult<PlatformStoreOutput>> GetListAsync(PlatformStorePageInput input)
{
var customerId = await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync(
CustomerRepository,
CurrentUser,
input.CustomerId);
var predicate = BuildListPredicate(customerId, input);
var pageIndex = OmsDocHelper.NormalizePage(input.Page);
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
var entities = await StoreRepository.GetPagedListAsync(
predicate,
pageIndex,
pageSize,
new Dictionary<string, bool> { [nameof(SalesStore.UpdateTime)] = false, [nameof(SalesStore.Id)] = false });
var total = await StoreRepository.CountAsync(predicate);
return new OmsPageResult<PlatformStoreOutput>
{
Items = entities.Select(MapOutput).ToList(),
Total = total
};
}
[HttpPost("/order/platform-store/auth-url")]
public async Task<PlatformStoreAuthUrlOutput> GetAuthUrlAsync(PlatformStoreAuthUrlInput input)
{
await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync(
CustomerRepository,
CurrentUser,
input.CustomerId);
var platform = input.PlatformCode.Trim().ToLowerInvariant();
var redirect = string.IsNullOrWhiteSpace(input.RedirectUri)
? "https://localhost/oauth/callback"
: input.RedirectUri.Trim();
return new PlatformStoreAuthUrlOutput
{
AuthUrl = $"https://oauth.example.com/{platform}?redirect={Uri.EscapeDataString(redirect)}",
Message = "OAuth 对接待平台 SDK 接入,当前返回占位地址"
};
}
[HttpPost("/order/platform-store/save")]
public async Task<PlatformStoreOutput> SaveAsync(PlatformStoreSaveInput input)
{
var customerId = await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync(
CustomerRepository,
CurrentUser,
input.CustomerId);
var nickname = input.StoreNickname.Trim();
if (string.IsNullOrWhiteSpace(nickname))
{
throw new BusinessException("店铺昵称不能为空");
}
SalesStore entity;
if (input.Id > 0)
{
entity = await GetStoreOrThrowAsync(customerId, input.Id);
}
else
{
entity = new SalesStore
{
CustomerId = customerId,
PlatformCode = string.IsNullOrWhiteSpace(input.PlatformCode) ? "manual" : input.PlatformCode.Trim(),
AuthStatus = SalesStoreAuthStatusCodes.Active,
AuthTime = DateTime.Now
};
}
entity.StoreNickname = nickname;
entity.PlatformAccount = string.IsNullOrWhiteSpace(input.PlatformAccount) ? null : input.PlatformAccount.Trim();
entity.SiteCountry = string.IsNullOrWhiteSpace(input.SiteCountry) ? null : input.SiteCountry.Trim();
entity.IsAutoFulfill = input.IsAutoFulfill;
entity.Remark = string.IsNullOrWhiteSpace(input.Remark) ? null : input.Remark.Trim();
if (input.Id > 0)
{
await StoreRepository.UpdateAsync(entity);
}
else
{
await StoreRepository.InsertAsync(entity);
}
return MapOutput(entity);
}
[HttpPost("/order/platform-store/delete")]
public async Task DeleteAsync(PlatformStoreIdsInput input)
{
if (input.Ids.Count == 0)
{
throw new BusinessException("请选择要删除的店铺");
}
var customerId = await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync(
CustomerRepository,
CurrentUser,
null);
var entities = await StoreRepository.GetListAsync(x => x.CustomerId == customerId && input.Ids.Contains(x.Id));
foreach (var entity in entities)
{
await StoreRepository.DeleteAsync(entity);
}
}
private async Task<SalesStore> GetStoreOrThrowAsync(int customerId, int id)
{
var entity = await StoreRepository.FirstOrDefaultAsync(x => x.Id == id && x.CustomerId == customerId);
if (entity == null)
{
throw new BusinessException("店铺不存在");
}
return entity;
}
private static Expression<Func<SalesStore, bool>> BuildListPredicate(int customerId, PlatformStorePageInput input)
{
var platformCode = input.PlatformCode?.Trim();
var nickname = input.StoreNickname?.Trim();
var filter = input.ExpirationFilter?.Trim().ToLowerInvariant();
var now = DateTime.Now;
var within7Days = now.AddDays(7);
return x =>
x.CustomerId == customerId
&& (string.IsNullOrWhiteSpace(platformCode) || x.PlatformCode == platformCode)
&& (string.IsNullOrWhiteSpace(nickname) || x.StoreNickname.Contains(nickname))
&& (string.IsNullOrWhiteSpace(filter) || filter == "normal"
? string.IsNullOrWhiteSpace(filter) || !x.TokenExpireTime.HasValue || x.TokenExpireTime > within7Days
: filter == "expired"
? x.TokenExpireTime.HasValue && x.TokenExpireTime < now
: filter == "within7days"
? x.TokenExpireTime.HasValue && x.TokenExpireTime >= now && x.TokenExpireTime <= within7Days
: true);
}
private static PlatformStoreOutput MapOutput(SalesStore entity) =>
new()
{
Id = entity.Id,
CustomerId = entity.CustomerId,
PlatformCode = entity.PlatformCode,
PlatformName = ResolvePlatformName(entity.PlatformCode),
StoreNickname = entity.StoreNickname,
PlatformAccount = entity.PlatformAccount,
SiteCountry = entity.SiteCountry,
AuthStatus = entity.AuthStatus,
AuthTime = entity.AuthTime,
TokenExpireTime = entity.TokenExpireTime,
IsAutoFulfill = entity.IsAutoFulfill,
Remark = entity.Remark,
CreateTime = entity.CreateTime
};
private static string ResolvePlatformName(string platformCode) => platformCode.ToLowerInvariant() switch
{
"lazada" => "Lazada",
"shopee" => "Shopee",
"tiktok" => "TikTok",
"temu" => "Temu",
"manual" => "手工店铺",
_ => platformCode
};
}

View File

@ -0,0 +1,93 @@
using Fake.Application;
using Microsoft.AspNetCore.Mvc;
using Oms.Application.Dtos.Shared;
using Oms.Application.Dtos.Warehouse;
using Oms.Domain.WarehouseAggregate.Entities;
namespace Oms.Application.Services.Warehouse;
public partial class WarehouseService
{
[HttpPost("/warehouse/bin-location/list")]
public async Task<OmsItemsResult<WarehouseBinLocationOutput>> GetBinLocationListAsync(WarehouseBinLocationListInput input)
{
if (input.WarehouseId <= 0)
{
throw new BusinessException("仓库ID不能为空");
}
var keyword = input.Keyword?.Trim();
var hasKeyword = !string.IsNullOrWhiteSpace(keyword);
var items = await BinLocationRepository.GetListAsync(x =>
x.WarehouseId == input.WarehouseId
&& (!input.IsEnabled.HasValue || x.IsEnabled == input.IsEnabled.Value)
&& (!hasKeyword || x.BinCode.Contains(keyword!) || (x.BinName != null && x.BinName.Contains(keyword!))));
items = items.OrderBy(x => x.BinCode).ThenBy(x => x.Id).ToList();
return new OmsItemsResult<WarehouseBinLocationOutput>
{
Items = items.Select(MapBinLocation).ToList()
};
}
[HttpPost("/warehouse/bin-location/save")]
public async Task<WarehouseBinLocationOutput> SaveBinLocationAsync(WarehouseBinLocationSaveInput input)
{
var binCode = input.BinCode.Trim();
if (string.IsNullOrWhiteSpace(binCode))
{
throw new BusinessException("库位编码不能为空");
}
CBinLocation entity;
if (input.Id > 0)
{
entity = await BinLocationRepository.FirstOrDefaultAsync(x => x.Id == input.Id)
?? throw new BusinessException("库位不存在");
if (entity.WarehouseId != input.WarehouseId)
{
throw new BusinessException("库位仓库不匹配");
}
}
else
{
entity = new CBinLocation { WarehouseId = input.WarehouseId };
}
var duplicate = await BinLocationRepository.FirstOrDefaultAsync(x =>
x.WarehouseId == input.WarehouseId && x.BinCode == binCode && x.Id != entity.Id);
if (duplicate != null)
{
throw new BusinessException("库位编码已存在");
}
entity.BinCode = binCode;
entity.BinName = string.IsNullOrWhiteSpace(input.BinName) ? null : input.BinName.Trim();
entity.ZoneCode = string.IsNullOrWhiteSpace(input.ZoneCode) ? null : input.ZoneCode.Trim();
entity.IsEnabled = input.IsEnabled;
entity.Remark = string.IsNullOrWhiteSpace(input.Remark) ? null : input.Remark.Trim();
if (input.Id > 0)
{
await BinLocationRepository.UpdateAsync(entity);
}
else
{
await BinLocationRepository.InsertAsync(entity);
}
return MapBinLocation(entity);
}
private static WarehouseBinLocationOutput MapBinLocation(CBinLocation entity) =>
new()
{
Id = entity.Id,
WarehouseId = entity.WarehouseId,
BinCode = entity.BinCode,
BinName = entity.BinName,
ZoneCode = entity.ZoneCode,
IsEnabled = entity.IsEnabled,
Remark = entity.Remark
};
}

View File

@ -0,0 +1,109 @@
using Fake.Application;
using Oms.Application.Shared;
using Oms.Domain.WarehouseAggregate.Entities;
namespace Oms.Application.Services.Warehouse;
public partial class WarehouseService
{
private async Task<int> ResolveCustomerIdAsync(int? requestedCustomerId) =>
await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync(
CustomerRepository,
CurrentUser,
requestedCustomerId);
private async Task<CSku> GetSkuOrThrowAsync(int customerId, int skuId)
{
var entity = await SkuRepository.FirstOrDefaultAsync(x => x.Id == skuId && x.CustomerId == customerId);
if (entity == null)
{
throw new BusinessException("SKU 不存在");
}
return entity;
}
private async Task UpsertInventoryQtyAsync(
int customerId,
int warehouseId,
int skuId,
int binLocationId,
int deltaQty)
{
if (deltaQty == 0)
{
return;
}
var inventory = await InventoryRepository.FirstOrDefaultAsync(x =>
x.CustomerId == customerId
&& x.WarehouseId == warehouseId
&& x.SkuId == skuId
&& x.BinLocationId == binLocationId);
if (inventory == null)
{
if (deltaQty < 0)
{
throw new BusinessException("库存不足");
}
inventory = new CInventory
{
CustomerId = customerId,
WarehouseId = warehouseId,
SkuId = skuId,
BinLocationId = binLocationId,
Qty = deltaQty
};
await InventoryRepository.InsertAsync(inventory);
return;
}
var newQty = inventory.Qty + deltaQty;
if (newQty < 0)
{
throw new BusinessException("库存不足");
}
inventory.Qty = newQty;
await InventoryRepository.UpdateAsync(inventory);
}
private async Task DeductInventoryAsync(
int customerId,
int warehouseId,
int skuId,
int requiredQty)
{
if (requiredQty <= 0)
{
return;
}
var rows = await InventoryRepository.GetListAsync(x =>
x.CustomerId == customerId
&& x.WarehouseId == warehouseId
&& x.SkuId == skuId
&& x.Qty > 0);
rows = rows.OrderBy(x => x.BinLocationId).ToList();
var remaining = requiredQty;
foreach (var row in rows)
{
if (remaining <= 0)
{
break;
}
var deduct = Math.Min(row.Qty, remaining);
row.Qty -= deduct;
remaining -= deduct;
await InventoryRepository.UpdateAsync(row);
}
if (remaining > 0)
{
throw new BusinessException($"SKU {skuId} 库存不足,缺少 {remaining} 件");
}
}
}

View File

@ -0,0 +1,365 @@
using System.Linq.Expressions;
using Fake.Application;
using Microsoft.AspNetCore.Mvc;
using Oms.Application.Dtos.Shared;
using Oms.Application.Dtos.Warehouse;
using Oms.Application.Shared;
using Oms.Domain.WarehouseAggregate.Consts;
using Oms.Domain.WarehouseAggregate.Entities;
namespace Oms.Application.Services.Warehouse;
public partial class WarehouseService
{
[HttpPost("/warehouse/inbound/list")]
public async Task<OmsPageResult<WarehouseInboundOutput>> GetInboundListAsync(WarehouseInboundListInput input)
{
var customerId = await ResolveCustomerIdAsync(input.CustomerId);
var keyword = input.Keyword?.Trim();
var hasKeyword = !string.IsNullOrWhiteSpace(keyword);
var receiptStatus = input.ReceiptStatus?.Trim();
Expression<Func<CInboundNotice, bool>> predicate = x =>
x.CustomerId == customerId
&& (!input.WarehouseId.HasValue || x.WarehouseId == input.WarehouseId.Value)
&& (string.IsNullOrWhiteSpace(receiptStatus) || x.ReceiptStatus == receiptStatus)
&& (!hasKeyword || x.NoticeNo.Contains(keyword!) || (x.MarkReference != null && x.MarkReference.Contains(keyword!)));
var pageIndex = OmsDocHelper.NormalizePage(input.Page);
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
var entities = await InboundNoticeRepository.GetPagedListAsync(
predicate,
pageIndex,
pageSize,
new Dictionary<string, bool> { [nameof(CInboundNotice.UpdateTime)] = false, [nameof(CInboundNotice.Id)] = false });
var total = await InboundNoticeRepository.CountAsync(predicate);
var noticeIds = entities.Select(x => x.Id).ToArray();
var boxes = noticeIds.Length == 0
? []
: await InboundBoxRepository.GetListAsync(x => noticeIds.Contains(x.InboundNoticeId));
var boxIds = boxes.Select(x => x.Id).ToArray();
var lines = boxIds.Length == 0
? []
: await InboundBoxLineRepository.GetListAsync(x => boxIds.Contains(x.InboundBoxId));
return new OmsPageResult<WarehouseInboundOutput>
{
Items = entities.Select(x => MapInboundOutput(x, boxes, lines)).ToList(),
Total = total
};
}
[HttpGet("/warehouse/inbound/detail")]
public async Task<WarehouseInboundDetailOutput> GetInboundDetailAsync(
[FromQuery] int id,
[FromQuery] int? customerId = null)
{
var resolvedCustomerId = await ResolveCustomerIdAsync(customerId);
var notice = await GetInboundNoticeOrThrowAsync(resolvedCustomerId, id);
var boxes = await InboundBoxRepository.GetListAsync(x => x.InboundNoticeId == notice.Id);
boxes = boxes.OrderBy(x => x.BoxSeqNo).ThenBy(x => x.Id).ToList();
var boxIds = boxes.Select(x => x.Id).ToArray();
var lines = boxIds.Length == 0
? []
: await InboundBoxLineRepository.GetListAsync(x => boxIds.Contains(x.InboundBoxId));
var output = MapInboundOutput(notice, boxes, lines);
return new WarehouseInboundDetailOutput
{
Id = output.Id,
CustomerId = output.CustomerId,
WarehouseId = output.WarehouseId,
NoticeNo = output.NoticeNo,
MarkReference = output.MarkReference,
InboundType = output.InboundType,
ReceiptStatus = output.ReceiptStatus,
ExpectedArrivalTime = output.ExpectedArrivalTime,
ActualArrivalTime = output.ActualArrivalTime,
Remark = output.Remark,
BoxCount = output.BoxCount,
PlannedQty = output.PlannedQty,
CreateTime = output.CreateTime,
Boxes = boxes.Select(box => MapInboundBox(box, lines.Where(l => l.InboundBoxId == box.Id))).ToList()
};
}
[HttpPost("/warehouse/inbound/save")]
public async Task<WarehouseInboundDetailOutput> SaveInboundAsync(WarehouseInboundSaveInput input)
{
var customerId = await ResolveCustomerIdAsync(input.CustomerId);
if (input.WarehouseId <= 0)
{
throw new BusinessException("仓库ID不能为空");
}
if (input.Boxes.Count == 0)
{
throw new BusinessException("请至少添加一个箱子");
}
CInboundNotice notice;
if (input.Id > 0)
{
notice = await GetInboundNoticeOrThrowAsync(customerId, input.Id);
if (notice.ReceiptStatus != WarehouseReceiptStatusCodes.Forecasted)
{
throw new BusinessException("仅预报状态可编辑");
}
}
else
{
notice = new CInboundNotice
{
CustomerId = customerId,
NoticeNo = WarehouseCustomerHelper.GenerateInboundNoticeNo(),
ReceiptStatus = WarehouseReceiptStatusCodes.Forecasted
};
}
notice.WarehouseId = input.WarehouseId;
notice.MarkReference = string.IsNullOrWhiteSpace(input.MarkReference) ? null : input.MarkReference.Trim();
notice.InboundType = string.IsNullOrWhiteSpace(input.InboundType)
? WarehouseInboundTypeCodes.Normal
: input.InboundType.Trim();
notice.ExpectedArrivalTime = input.ExpectedArrivalTime;
notice.Remark = string.IsNullOrWhiteSpace(input.Remark) ? null : input.Remark.Trim();
if (input.Id > 0)
{
await InboundNoticeRepository.UpdateAsync(notice);
await ClearInboundBoxesAsync(notice.Id);
}
else
{
await InboundNoticeRepository.InsertAsync(notice);
}
foreach (var boxInput in input.Boxes)
{
var box = new CInboundBox
{
InboundNoticeId = notice.Id,
BoxSeqNo = string.IsNullOrWhiteSpace(boxInput.BoxSeqNo) ? "1" : boxInput.BoxSeqNo.Trim(),
MarkBoxNo = string.IsNullOrWhiteSpace(boxInput.MarkBoxNo) ? null : boxInput.MarkBoxNo.Trim(),
WeightG = boxInput.WeightG,
LengthCm = boxInput.LengthCm,
WidthCm = boxInput.WidthCm,
HeightCm = boxInput.HeightCm
};
await InboundBoxRepository.InsertAsync(box);
foreach (var lineInput in boxInput.Lines)
{
if (lineInput.SkuId <= 0 || lineInput.PlannedQty <= 0)
{
throw new BusinessException("箱内 SKU 行数据不完整");
}
var sku = await GetSkuOrThrowAsync(customerId, lineInput.SkuId);
var line = new CInboundBoxLine
{
InboundBoxId = box.Id,
SkuId = sku.Id,
SkuCode = string.IsNullOrWhiteSpace(lineInput.SkuCode) ? sku.SkuCode : lineInput.SkuCode.Trim(),
PlannedQty = lineInput.PlannedQty
};
await InboundBoxLineRepository.InsertAsync(line);
}
}
return await GetInboundDetailAsync(notice.Id, customerId);
}
[HttpPost("/warehouse/inbound/delete")]
public async Task DeleteInboundAsync(IdInput input)
{
var customerId = await ResolveCustomerIdAsync(null);
var notice = await GetInboundNoticeOrThrowAsync(customerId, input.Id);
if (notice.ReceiptStatus != WarehouseReceiptStatusCodes.Forecasted)
{
throw new BusinessException("仅预报状态可删除");
}
await ClearInboundBoxesAsync(notice.Id);
await InboundNoticeRepository.DeleteAsync(notice);
}
[HttpPost("/warehouse/inbound/confirm-receive")]
public async Task ConfirmInboundReceiveAsync(WarehouseInboundConfirmReceiveInput input)
{
var customerId = await ResolveCustomerIdAsync(input.CustomerId);
var notice = await GetInboundNoticeOrThrowAsync(customerId, input.InboundNoticeId);
if (notice.ReceiptStatus == WarehouseReceiptStatusCodes.Completed)
{
throw new BusinessException("入库单已完成收货");
}
if (input.Boxes.Count == 0)
{
throw new BusinessException("请提交收货箱数据");
}
var boxes = await InboundBoxRepository.GetListAsync(x => x.InboundNoticeId == notice.Id);
var boxMap = boxes.ToDictionary(x => x.Id);
var boxIds = boxes.Select(x => x.Id).ToArray();
var lines = boxIds.Length == 0
? []
: await InboundBoxLineRepository.GetListAsync(x => boxIds.Contains(x.InboundBoxId));
var lineMap = lines.ToDictionary(x => x.Id);
foreach (var boxInput in input.Boxes)
{
if (!boxMap.TryGetValue(boxInput.BoxId, out var box))
{
throw new BusinessException($"箱 {boxInput.BoxId} 不存在");
}
foreach (var lineInput in boxInput.Lines)
{
if (!lineMap.TryGetValue(lineInput.LineId, out var line) || line.InboundBoxId != box.Id)
{
throw new BusinessException($"箱内行 {lineInput.LineId} 不存在");
}
if (lineInput.ReceivedQty < 0)
{
throw new BusinessException("实收数量不能为负");
}
if (lineInput.ReceivedQty > 0 && lineInput.BinLocationId <= 0)
{
throw new BusinessException("上架库位不能为空");
}
var previousQty = line.ReceivedQty ?? 0;
var deltaQty = lineInput.ReceivedQty - previousQty;
line.ReceivedQty = lineInput.ReceivedQty;
line.BinLocationId = lineInput.ReceivedQty > 0 ? lineInput.BinLocationId : null;
await InboundBoxLineRepository.UpdateAsync(line);
if (deltaQty > 0)
{
await UpsertInventoryQtyAsync(
customerId,
notice.WarehouseId,
line.SkuId,
lineInput.BinLocationId,
deltaQty);
}
}
box.IsReceived = boxInput.Lines.All(x =>
{
if (!lineMap.TryGetValue(x.LineId, out var line))
{
return false;
}
return x.ReceivedQty >= line.PlannedQty;
});
await InboundBoxRepository.UpdateAsync(box);
}
notice.ReceiptStatus = WarehouseReceiptStatusCodes.Completed;
notice.ActualArrivalTime = DateTime.Now;
await InboundNoticeRepository.UpdateAsync(notice);
}
[HttpGet("/warehouse/inbound/summary")]
public async Task<WarehouseInboundSummaryOutput> GetInboundSummaryAsync([FromQuery] int? customerId = null, [FromQuery] int? warehouseId = null)
{
var resolvedCustomerId = await ResolveCustomerIdAsync(customerId);
var notices = await InboundNoticeRepository.GetListAsync(x =>
x.CustomerId == resolvedCustomerId
&& (!warehouseId.HasValue || x.WarehouseId == warehouseId.Value));
return new WarehouseInboundSummaryOutput
{
Forecasted = notices.Count(x => x.ReceiptStatus == WarehouseReceiptStatusCodes.Forecasted),
Received = notices.Count(x => x.ReceiptStatus == WarehouseReceiptStatusCodes.Received),
Completed = notices.Count(x => x.ReceiptStatus == WarehouseReceiptStatusCodes.Completed)
};
}
private async Task<CInboundNotice> GetInboundNoticeOrThrowAsync(int customerId, int id)
{
var entity = await InboundNoticeRepository.FirstOrDefaultAsync(x => x.Id == id && x.CustomerId == customerId);
if (entity == null)
{
throw new BusinessException("入库预报不存在");
}
return entity;
}
private async Task ClearInboundBoxesAsync(int noticeId)
{
var boxes = await InboundBoxRepository.GetListAsync(x => x.InboundNoticeId == noticeId);
if (boxes.Count == 0)
{
return;
}
var boxIds = boxes.Select(x => x.Id).ToArray();
var lines = await InboundBoxLineRepository.GetListAsync(x => boxIds.Contains(x.InboundBoxId));
foreach (var line in lines)
{
await InboundBoxLineRepository.DeleteAsync(line);
}
foreach (var box in boxes)
{
await InboundBoxRepository.DeleteAsync(box);
}
}
private static WarehouseInboundOutput MapInboundOutput(
CInboundNotice notice,
IReadOnlyCollection<CInboundBox> boxes,
IReadOnlyCollection<CInboundBoxLine> lines)
{
var noticeBoxes = boxes.Where(x => x.InboundNoticeId == notice.Id).ToList();
var noticeBoxIds = noticeBoxes.Select(x => x.Id).ToHashSet();
var noticeLines = lines.Where(x => noticeBoxIds.Contains(x.InboundBoxId)).ToList();
return new WarehouseInboundOutput
{
Id = notice.Id,
CustomerId = notice.CustomerId,
WarehouseId = notice.WarehouseId,
NoticeNo = notice.NoticeNo,
MarkReference = notice.MarkReference,
InboundType = notice.InboundType,
ReceiptStatus = notice.ReceiptStatus,
ExpectedArrivalTime = notice.ExpectedArrivalTime,
ActualArrivalTime = notice.ActualArrivalTime,
Remark = notice.Remark,
BoxCount = noticeBoxes.Count,
PlannedQty = noticeLines.Sum(x => x.PlannedQty),
CreateTime = notice.CreateTime
};
}
private static WarehouseInboundBoxOutput MapInboundBox(CInboundBox box, IEnumerable<CInboundBoxLine> lines) =>
new()
{
Id = box.Id,
BoxSeqNo = box.BoxSeqNo,
MarkBoxNo = box.MarkBoxNo,
WeightG = box.WeightG,
LengthCm = box.LengthCm,
WidthCm = box.WidthCm,
HeightCm = box.HeightCm,
IsReceived = box.IsReceived,
Lines = lines.Select(line => new WarehouseInboundLineOutput
{
Id = line.Id,
SkuId = line.SkuId,
SkuCode = line.SkuCode,
PlannedQty = line.PlannedQty,
ReceivedQty = line.ReceivedQty,
BinLocationId = line.BinLocationId
}).ToList()
};
}

View File

@ -0,0 +1,130 @@
using Fake.Application;
using Microsoft.AspNetCore.Mvc;
using Oms.Application.Dtos.Shared;
using Oms.Application.Dtos.Warehouse;
using Oms.Application.Shared;
namespace Oms.Application.Services.Warehouse;
public partial class WarehouseService
{
[HttpPost("/warehouse/inventory/list")]
public async Task<OmsPageResult<WarehouseInventoryOutput>> GetInventoryListAsync(WarehouseInventoryListInput input)
{
var customerId = await ResolveCustomerIdAsync(input.CustomerId);
var inventories = await InventoryRepository.GetListAsync(x =>
x.CustomerId == customerId
&& x.Qty > 0
&& (!input.WarehouseId.HasValue || x.WarehouseId == input.WarehouseId.Value));
var skuIds = inventories.Select(x => x.SkuId).Distinct().ToArray();
var skus = skuIds.Length == 0
? []
: await SkuRepository.GetListAsync(x => x.CustomerId == customerId && skuIds.Contains(x.Id));
var skuMap = skus.ToDictionary(x => x.Id);
var keyword = input.Keyword?.Trim();
var grouped = inventories
.GroupBy(x => new { x.SkuId, x.WarehouseId })
.Select(g =>
{
skuMap.TryGetValue(g.Key.SkuId, out var sku);
return new WarehouseInventoryOutput
{
SkuId = g.Key.SkuId,
SkuCode = sku?.SkuCode ?? string.Empty,
SkuName = sku?.SkuName ?? string.Empty,
WarehouseId = g.Key.WarehouseId,
TotalQty = g.Sum(x => x.Qty),
BinCount = g.Count()
};
})
.Where(x =>
string.IsNullOrWhiteSpace(keyword)
|| x.SkuCode.Contains(keyword!, StringComparison.OrdinalIgnoreCase)
|| x.SkuName.Contains(keyword!, StringComparison.OrdinalIgnoreCase))
.OrderBy(x => x.SkuCode)
.ThenBy(x => x.WarehouseId)
.ToList();
var pageIndex = OmsDocHelper.NormalizePage(input.Page);
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
var skip = (pageIndex - 1) * pageSize;
return new OmsPageResult<WarehouseInventoryOutput>
{
Items = grouped.Skip(skip).Take(pageSize).ToList(),
Total = grouped.Count
};
}
[HttpGet("/warehouse/inventory/bin-detail")]
public async Task<OmsItemsResult<WarehouseInventoryBinDetailOutput>> GetInventoryBinDetailAsync(
[FromQuery] int skuId,
[FromQuery] int warehouseId,
[FromQuery] int? customerId = null)
{
if (skuId <= 0 || warehouseId <= 0)
{
throw new BusinessException("SKU 和仓库不能为空");
}
var resolvedCustomerId = await ResolveCustomerIdAsync(customerId);
await GetSkuOrThrowAsync(resolvedCustomerId, skuId);
var inventories = await InventoryRepository.GetListAsync(x =>
x.CustomerId == resolvedCustomerId
&& x.WarehouseId == warehouseId
&& x.SkuId == skuId
&& x.Qty > 0);
var binIds = inventories.Select(x => x.BinLocationId).Distinct().ToArray();
var bins = binIds.Length == 0
? []
: await BinLocationRepository.GetListAsync(x => binIds.Contains(x.Id));
var binMap = bins.ToDictionary(x => x.Id);
return new OmsItemsResult<WarehouseInventoryBinDetailOutput>
{
Items = inventories
.OrderBy(x => x.BinLocationId)
.Select(x =>
{
binMap.TryGetValue(x.BinLocationId, out var bin);
return new WarehouseInventoryBinDetailOutput
{
Id = x.Id,
BinLocationId = x.BinLocationId,
BinCode = bin?.BinCode ?? string.Empty,
BinName = bin?.BinName,
Qty = x.Qty
};
})
.ToList()
};
}
[HttpPost("/warehouse/inventory/relocate")]
public async Task RelocateInventoryAsync(WarehouseInventoryRelocateInput input)
{
var customerId = await ResolveCustomerIdAsync(input.CustomerId);
if (input.SourceBinLocationId == input.TargetBinLocationId)
{
throw new BusinessException("源库位与目标库位不能相同");
}
await GetSkuOrThrowAsync(customerId, input.SkuId);
var source = await InventoryRepository.FirstOrDefaultAsync(x =>
x.CustomerId == customerId
&& x.WarehouseId == input.WarehouseId
&& x.SkuId == input.SkuId
&& x.BinLocationId == input.SourceBinLocationId);
if (source == null || source.Qty < input.Qty)
{
throw new BusinessException("源库位库存不足");
}
await UpsertInventoryQtyAsync(customerId, input.WarehouseId, input.SkuId, input.SourceBinLocationId, -input.Qty);
await UpsertInventoryQtyAsync(customerId, input.WarehouseId, input.SkuId, input.TargetBinLocationId, input.Qty);
}
}

View File

@ -0,0 +1,233 @@
using System.Linq.Expressions;
using Fake.Application;
using Microsoft.AspNetCore.Mvc;
using Oms.Application.Dtos.Shared;
using Oms.Application.Dtos.Warehouse;
using Oms.Application.Shared;
using Oms.Domain.WarehouseAggregate.Consts;
using Oms.Domain.WarehouseAggregate.Entities;
namespace Oms.Application.Services.Warehouse;
public partial class WarehouseService
{
[HttpPost("/warehouse/parcel/list")]
public async Task<OmsPageResult<WarehouseParcelOutput>> GetParcelListAsync(WarehouseParcelListInput input)
{
var customerId = await ResolveCustomerIdAsync(input.CustomerId);
var keyword = input.Keyword?.Trim();
var hasKeyword = !string.IsNullOrWhiteSpace(keyword);
var lifecycleStatus = input.LifecycleStatus?.Trim();
Expression<Func<CParcel, bool>> predicate = x =>
x.CustomerId == customerId
&& (!input.WarehouseId.HasValue || x.WarehouseId == input.WarehouseId.Value)
&& (string.IsNullOrWhiteSpace(lifecycleStatus) || x.LifecycleStatus == lifecycleStatus)
&& (!hasKeyword || x.TrackingNo.Contains(keyword!) || (x.CarrierName != null && x.CarrierName.Contains(keyword!)));
var pageIndex = OmsDocHelper.NormalizePage(input.Page);
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
var entities = await ParcelRepository.GetPagedListAsync(
predicate,
pageIndex,
pageSize,
new Dictionary<string, bool> { [nameof(CParcel.UpdateTime)] = false, [nameof(CParcel.Id)] = false });
var total = await ParcelRepository.CountAsync(predicate);
var parcelIds = entities.Select(x => x.Id).ToArray();
var items = parcelIds.Length == 0
? []
: await ParcelItemRepository.GetListAsync(x => parcelIds.Contains(x.ParcelId));
return new OmsPageResult<WarehouseParcelOutput>
{
Items = entities.Select(x => MapParcelOutput(x, items.Where(i => i.ParcelId == x.Id))).ToList(),
Total = total
};
}
[HttpPost("/warehouse/parcel/save")]
public async Task<WarehouseParcelOutput> SaveParcelAsync(WarehouseParcelSaveInput input)
{
var customerId = await ResolveCustomerIdAsync(input.CustomerId);
if (input.WarehouseId <= 0)
{
throw new BusinessException("仓库ID不能为空");
}
var trackingNo = input.TrackingNo.Trim();
if (string.IsNullOrWhiteSpace(trackingNo))
{
throw new BusinessException("快递单号不能为空");
}
CParcel parcel;
if (input.Id > 0)
{
parcel = await GetParcelOrThrowAsync(customerId, input.Id);
if (parcel.LifecycleStatus == WarehouseParcelLifecycleCodes.Stored)
{
throw new BusinessException("已上架包裹不可编辑");
}
}
else
{
parcel = new CParcel { CustomerId = customerId };
}
var duplicate = await ParcelRepository.FirstOrDefaultAsync(x =>
x.CustomerId == customerId
&& x.WarehouseId == input.WarehouseId
&& x.TrackingNo == trackingNo
&& x.Id != parcel.Id);
if (duplicate != null)
{
throw new BusinessException("快递单号已存在");
}
parcel.WarehouseId = input.WarehouseId;
parcel.TrackingNo = trackingNo;
parcel.CarrierName = string.IsNullOrWhiteSpace(input.CarrierName) ? null : input.CarrierName.Trim();
parcel.LifecycleStatus = string.IsNullOrWhiteSpace(input.LifecycleStatus)
? WarehouseParcelLifecycleCodes.InTransit
: input.LifecycleStatus.Trim();
parcel.Remark = string.IsNullOrWhiteSpace(input.Remark) ? null : input.Remark.Trim();
if (input.Id > 0)
{
await ParcelRepository.UpdateAsync(parcel);
await ClearParcelItemsAsync(parcel.Id);
}
else
{
await ParcelRepository.InsertAsync(parcel);
}
foreach (var itemInput in input.Items)
{
if (itemInput.Quantity <= 0)
{
throw new BusinessException("商品数量必须大于 0");
}
var item = new CParcelItem
{
ParcelId = parcel.Id,
ProductName = string.IsNullOrWhiteSpace(itemInput.ProductName) ? null : itemInput.ProductName.Trim(),
Quantity = itemInput.Quantity,
Remark = string.IsNullOrWhiteSpace(itemInput.Remark) ? null : itemInput.Remark.Trim()
};
await ParcelItemRepository.InsertAsync(item);
}
var savedItems = await ParcelItemRepository.GetListAsync(x => x.ParcelId == parcel.Id);
return MapParcelOutput(parcel, savedItems);
}
[HttpPost("/warehouse/parcel/delete")]
public async Task DeleteParcelAsync(IdInput input)
{
var customerId = await ResolveCustomerIdAsync(null);
var parcel = await GetParcelOrThrowAsync(customerId, input.Id);
if (parcel.LifecycleStatus == WarehouseParcelLifecycleCodes.Stored)
{
throw new BusinessException("已上架包裹不可删除");
}
await ClearParcelItemsAsync(parcel.Id);
await ParcelRepository.DeleteAsync(parcel);
}
[HttpPost("/warehouse/parcel/putaway")]
public async Task PutawayParcelAsync(WarehouseParcelPutawayInput input)
{
var customerId = await ResolveCustomerIdAsync(input.CustomerId);
var parcel = await GetParcelOrThrowAsync(customerId, input.ParcelId);
if (parcel.LifecycleStatus == WarehouseParcelLifecycleCodes.Stored)
{
throw new BusinessException("包裹已上架");
}
if (input.Items.Count == 0)
{
throw new BusinessException("请指定上架明细");
}
var items = await ParcelItemRepository.GetListAsync(x => x.ParcelId == parcel.Id);
var itemMap = items.ToDictionary(x => x.Id);
foreach (var putawayItem in input.Items)
{
if (!itemMap.TryGetValue(putawayItem.ItemId, out var item))
{
throw new BusinessException($"包裹明细 {putawayItem.ItemId} 不存在");
}
if (putawayItem.SkuId <= 0 || putawayItem.BinLocationId <= 0)
{
throw new BusinessException("SKU 和库位不能为空");
}
var sku = await GetSkuOrThrowAsync(customerId, putawayItem.SkuId);
item.SkuId = sku.Id;
item.SkuCode = sku.SkuCode;
item.BinLocationId = putawayItem.BinLocationId;
await ParcelItemRepository.UpdateAsync(item);
await UpsertInventoryQtyAsync(
customerId,
parcel.WarehouseId,
sku.Id,
putawayItem.BinLocationId,
item.Quantity);
}
parcel.LifecycleStatus = WarehouseParcelLifecycleCodes.Stored;
parcel.ArrivalTime = DateTime.Now;
await ParcelRepository.UpdateAsync(parcel);
}
private async Task<CParcel> GetParcelOrThrowAsync(int customerId, int id)
{
var entity = await ParcelRepository.FirstOrDefaultAsync(x => x.Id == id && x.CustomerId == customerId);
if (entity == null)
{
throw new BusinessException("包裹不存在");
}
return entity;
}
private async Task ClearParcelItemsAsync(int parcelId)
{
var items = await ParcelItemRepository.GetListAsync(x => x.ParcelId == parcelId);
foreach (var item in items)
{
await ParcelItemRepository.DeleteAsync(item);
}
}
private static WarehouseParcelOutput MapParcelOutput(CParcel parcel, IEnumerable<CParcelItem> items) =>
new()
{
Id = parcel.Id,
CustomerId = parcel.CustomerId,
WarehouseId = parcel.WarehouseId,
TrackingNo = parcel.TrackingNo,
CarrierName = parcel.CarrierName,
LifecycleStatus = parcel.LifecycleStatus,
ArrivalTime = parcel.ArrivalTime,
Remark = parcel.Remark,
CreateTime = parcel.CreateTime,
Items = items.Select(item => new WarehouseParcelItemOutput
{
Id = item.Id,
ProductName = item.ProductName,
Quantity = item.Quantity,
SkuId = item.SkuId,
SkuCode = item.SkuCode,
BinLocationId = item.BinLocationId,
Remark = item.Remark
}).ToList()
};
}

View File

@ -0,0 +1,179 @@
using System.Linq.Expressions;
using Fake.Application;
using Microsoft.AspNetCore.Mvc;
using Oms.Application.Dtos.Shared;
using Oms.Application.Dtos.Warehouse;
using Oms.Application.Shared;
namespace Oms.Application.Services.Warehouse;
public partial class WarehouseService
{
[HttpPost("/warehouse/sku/list")]
public async Task<OmsPageResult<WarehouseSkuOutput>> GetSkuListAsync(WarehouseSkuListInput input)
{
var customerId = await ResolveCustomerIdAsync(input.CustomerId);
var keyword = input.Keyword?.Trim();
var hasKeyword = !string.IsNullOrWhiteSpace(keyword);
Expression<Func<Domain.WarehouseAggregate.Entities.CSku, bool>> predicate = x =>
x.CustomerId == customerId
&& (!hasKeyword || x.SkuCode.Contains(keyword!) || x.SkuName.Contains(keyword!));
var pageIndex = OmsDocHelper.NormalizePage(input.Page);
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
var entities = await SkuRepository.GetPagedListAsync(
predicate,
pageIndex,
pageSize,
new Dictionary<string, bool> { [nameof(Domain.WarehouseAggregate.Entities.CSku.UpdateTime)] = false, [nameof(Domain.WarehouseAggregate.Entities.CSku.Id)] = false });
var total = await SkuRepository.CountAsync(predicate);
var skuIds = entities.Select(x => x.Id).ToArray();
var inventories = skuIds.Length == 0
? []
: await InventoryRepository.GetListAsync(x => x.CustomerId == customerId && skuIds.Contains(x.SkuId));
var qtyMap = inventories
.GroupBy(x => x.SkuId)
.ToDictionary(g => g.Key, g => g.Sum(x => x.Qty));
return new OmsPageResult<WarehouseSkuOutput>
{
Items = entities.Select(x => MapSkuOutput(x, qtyMap.GetValueOrDefault(x.Id))).ToList(),
Total = total
};
}
[HttpGet("/warehouse/sku/detail")]
public async Task<WarehouseSkuOutput> GetSkuDetailAsync([FromQuery] int id, [FromQuery] int? customerId = null)
{
var resolvedCustomerId = await ResolveCustomerIdAsync(customerId);
var entity = await GetSkuOrThrowAsync(resolvedCustomerId, id);
var inventories = await InventoryRepository.GetListAsync(x => x.CustomerId == resolvedCustomerId && x.SkuId == id);
return MapSkuOutput(entity, inventories.Sum(x => x.Qty));
}
[HttpPost("/warehouse/sku/save")]
public async Task<WarehouseSkuOutput> SaveSkuAsync(WarehouseSkuSaveInput input)
{
var customerId = await ResolveCustomerIdAsync(input.CustomerId);
var skuCode = input.SkuCode.Trim();
var skuName = input.SkuName.Trim();
if (string.IsNullOrWhiteSpace(skuCode) || string.IsNullOrWhiteSpace(skuName))
{
throw new BusinessException("SKU 编码和名称不能为空");
}
Domain.WarehouseAggregate.Entities.CSku entity;
if (input.Id > 0)
{
entity = await GetSkuOrThrowAsync(customerId, input.Id);
}
else
{
entity = new Domain.WarehouseAggregate.Entities.CSku { CustomerId = customerId };
}
var duplicate = await SkuRepository.FirstOrDefaultAsync(x =>
x.CustomerId == customerId && x.SkuCode == skuCode && x.Id != entity.Id);
if (duplicate != null)
{
throw new BusinessException("SKU 编码已存在");
}
entity.SkuCode = skuCode;
entity.SkuName = skuName;
entity.SkuImageUrl = string.IsNullOrWhiteSpace(input.SkuImageUrl) ? null : input.SkuImageUrl.Trim();
entity.WeightG = input.WeightG;
entity.LengthCm = input.LengthCm;
entity.WidthCm = input.WidthCm;
entity.HeightCm = input.HeightCm;
entity.Remark = string.IsNullOrWhiteSpace(input.Remark) ? null : input.Remark.Trim();
if (input.Id > 0)
{
await SkuRepository.UpdateAsync(entity);
}
else
{
await SkuRepository.InsertAsync(entity);
}
return MapSkuOutput(entity, 0);
}
[HttpPost("/warehouse/sku/delete")]
public async Task DeleteSkuAsync(IdInput input)
{
var customerId = await ResolveCustomerIdAsync(null);
var entity = await GetSkuOrThrowAsync(customerId, input.Id);
var hasInventory = await InventoryRepository.CountAsync(x =>
x.CustomerId == customerId && x.SkuId == entity.Id && x.Qty > 0);
if (hasInventory > 0)
{
throw new BusinessException("SKU 仍有库存,不能删除");
}
await SkuRepository.DeleteAsync(entity);
}
[HttpPost("/warehouse/sku/bind-mapping")]
public async Task<WarehouseSkuMappingOutput> BindSkuMappingAsync(WarehouseSkuBindMappingInput input)
{
var customerId = await ResolveCustomerIdAsync(input.CustomerId);
var platformSku = input.PlatformSku.Trim();
if (string.IsNullOrWhiteSpace(platformSku))
{
throw new BusinessException("平台 SKU 不能为空");
}
var sku = await GetSkuOrThrowAsync(customerId, input.SkuId);
var existing = await SkuPlatformMapRepository.FirstOrDefaultAsync(x =>
x.CustomerId == customerId && x.StoreId == input.StoreId && x.PlatformSku == platformSku);
if (existing == null)
{
existing = new Domain.WarehouseAggregate.Entities.CSkuPlatformMap
{
CustomerId = customerId,
StoreId = input.StoreId,
PlatformSku = platformSku,
SkuId = sku.Id,
SkuCode = sku.SkuCode
};
await SkuPlatformMapRepository.InsertAsync(existing);
}
else
{
existing.SkuId = sku.Id;
existing.SkuCode = sku.SkuCode;
await SkuPlatformMapRepository.UpdateAsync(existing);
}
return new WarehouseSkuMappingOutput
{
Id = existing.Id,
StoreId = existing.StoreId,
PlatformSku = existing.PlatformSku,
SkuId = existing.SkuId,
SkuCode = existing.SkuCode
};
}
private static WarehouseSkuOutput MapSkuOutput(Domain.WarehouseAggregate.Entities.CSku entity, int totalQty) =>
new()
{
Id = entity.Id,
CustomerId = entity.CustomerId,
SkuCode = entity.SkuCode,
SkuName = entity.SkuName,
SkuImageUrl = entity.SkuImageUrl,
WeightG = entity.WeightG,
LengthCm = entity.LengthCm,
WidthCm = entity.WidthCm,
HeightCm = entity.HeightCm,
Remark = entity.Remark,
TotalQty = totalQty,
CreateTime = entity.CreateTime,
UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime)
};
}

View File

@ -0,0 +1,24 @@
using Fake.Application;
using Microsoft.AspNetCore.Mvc;
using Oms.Domain.CustomersAggregate.Repositories;
using Oms.Domain.WarehouseAggregate.Repositories;
namespace Oms.Application.Services.Warehouse;
/// <summary>
/// 仓储模块 API 服务(我的包裹 / 库存 / SKU / 入库预报)。
/// </summary>
[ApiExplorerSettings(GroupName = "OMS-Warehouse")]
public partial class WarehouseService : ApplicationService
{
private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService<ICustomerRepository>();
private ICSkuRepository SkuRepository => LazyServiceProvider.GetRequiredService<ICSkuRepository>();
private ICBinLocationRepository BinLocationRepository => LazyServiceProvider.GetRequiredService<ICBinLocationRepository>();
private ICInventoryRepository InventoryRepository => LazyServiceProvider.GetRequiredService<ICInventoryRepository>();
private ICInboundNoticeRepository InboundNoticeRepository => LazyServiceProvider.GetRequiredService<ICInboundNoticeRepository>();
private ICInboundBoxRepository InboundBoxRepository => LazyServiceProvider.GetRequiredService<ICInboundBoxRepository>();
private ICInboundBoxLineRepository InboundBoxLineRepository => LazyServiceProvider.GetRequiredService<ICInboundBoxLineRepository>();
private ICParcelRepository ParcelRepository => LazyServiceProvider.GetRequiredService<ICParcelRepository>();
private ICParcelItemRepository ParcelItemRepository => LazyServiceProvider.GetRequiredService<ICParcelItemRepository>();
private ICSkuPlatformMapRepository SkuPlatformMapRepository => LazyServiceProvider.GetRequiredService<ICSkuPlatformMapRepository>();
}

View File

@ -0,0 +1,31 @@
using Fake.Application;
using Fake.Users;
using Oms.Domain.CustomersAggregate.Repositories;
namespace Oms.Application.Shared;
/// <summary>
/// 仓储模块客户上下文解析。
/// </summary>
public static class WarehouseCustomerHelper
{
public static async Task<int> ResolveRequiredCustomerIdAsync(
ICustomerRepository customerRepository,
ICurrentUser currentUser,
int? requestedCustomerId = null)
{
var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync(
customerRepository,
currentUser,
requestedCustomerId);
if (!customerId.HasValue)
{
throw new BusinessException("客户ID不能为空");
}
return customerId.Value;
}
public static string GenerateInboundNoticeNo() =>
$"IN{DateTime.Now:yyyyMMddHHmmss}{Random.Shared.Next(1000, 9999)}";
}

View File

@ -0,0 +1,41 @@
using Oms.Domain.Shared;
using SqlSugar;
namespace Oms.Domain.OrdersAggregate.Entities;
[SugarTable("t_sales_store")]
public class SalesStore : OmsAuditedAggregateRoot
{
public int CustomerId { get; set; }
public string PlatformCode { get; set; } = string.Empty;
public string StoreNickname { get; set; } = string.Empty;
public string? PlatformStoreId { get; set; }
public string? PlatformAccount { get; set; }
public string? SiteCountry { get; set; }
public string? SellerId { get; set; }
public string AuthStatus { get; set; } = "active";
public DateTime? AuthTime { get; set; }
public DateTime? TokenExpireTime { get; set; }
public DateTime? RefreshExpireTime { get; set; }
public string? AccessToken { get; set; }
public string? RefreshToken { get; set; }
[SugarColumn(IsJson = true, ColumnDataType = "json", IsNullable = true)]
public string? TokenExtraJson { get; set; }
public bool IsAutoFulfill { get; set; }
public string? Remark { get; set; }
}

View File

@ -0,0 +1,51 @@
using Oms.Domain.Shared;
using SqlSugar;
namespace Oms.Domain.OrdersAggregate.Entities;
[SugarTable("t_sales_store_order")]
public class SalesStoreOrder : OmsAuditedAggregateRoot
{
public int CustomerId { get; set; }
public int StoreId { get; set; }
public string PlatformCode { get; set; } = string.Empty;
public string PlatformOrderNo { get; set; } = string.Empty;
public string? OrderCountry { get; set; }
public int? ItemCount { get; set; }
public decimal? TotalAmount { get; set; }
public string? Currency { get; set; }
public DateTime? PlatformOrderTime { get; set; }
public string FulfillStatus { get; set; } = "pending";
public string? PlatformStatus { get; set; }
public string? PlatformStatusLabel { get; set; }
public string? ShippingLabelNo { get; set; }
public string? ShippingLabelUrl { get; set; }
public bool IsMultiLabel { get; set; }
public string? CustomTags { get; set; }
public bool IsSuspended { get; set; }
public int? WarehouseId { get; set; }
public int? LinkedOrderId { get; set; }
public DateTime? LastSyncTime { get; set; }
[SugarColumn(IsJson = true, ColumnDataType = "json", IsNullable = true)]
public string? PlatformExtraJson { get; set; }
}

View File

@ -0,0 +1,30 @@
using Oms.Domain.Shared;
using SqlSugar;
namespace Oms.Domain.OrdersAggregate.Entities;
[SugarTable("t_sales_store_order_line")]
public class SalesStoreOrderLine : OmsAuditedAggregateRoot
{
public int StoreOrderId { get; set; }
public string? PlatformLineId { get; set; }
public string? PlatformSku { get; set; }
public string? ProductName { get; set; }
public string? ProductImageUrl { get; set; }
public string? VariantText { get; set; }
public decimal? UnitPrice { get; set; }
public int Quantity { get; set; }
public string? Currency { get; set; }
public bool IsShipExcluded { get; set; }
public int? MappedSkuId { get; set; }
}

View File

@ -0,0 +1,10 @@
using Fake.Domain.Repositories;
using Oms.Domain.OrdersAggregate.Entities;
namespace Oms.Domain.OrdersAggregate.Repositories;
public interface ISalesStoreRepository : IRepository<SalesStore>;
public interface ISalesStoreOrderRepository : IRepository<SalesStoreOrder>;
public interface ISalesStoreOrderLineRepository : IRepository<SalesStoreOrderLine>;

View File

@ -0,0 +1,38 @@
namespace Oms.Domain.WarehouseAggregate.Consts;
public static class WarehouseInboundTypeCodes
{
public const string Normal = "normal";
public const string Return = "return";
}
public static class WarehouseReceiptStatusCodes
{
public const string Forecasted = "forecasted";
public const string Received = "received";
public const string Completed = "completed";
}
public static class WarehouseParcelLifecycleCodes
{
public const string InTransit = "inTransit";
public const string Arrived = "arrived";
public const string Stored = "stored";
}
public static class SalesStoreAuthStatusCodes
{
public const string Active = "active";
public const string Expired = "expired";
public const string Revoked = "revoked";
}
public static class SalesStoreFulfillStatusCodes
{
public const string Pending = "pending";
public const string ToShip = "toShip";
public const string Canceled = "canceled";
public const string Suspended = "suspended";
public const string Shipped = "shipped";
public const string Completed = "completed";
}

View File

@ -0,0 +1,20 @@
using Oms.Domain.Shared;
using SqlSugar;
namespace Oms.Domain.WarehouseAggregate.Entities;
[SugarTable("C_bin_location")]
public class CBinLocation : OmsAuditedAggregateRoot
{
public int WarehouseId { get; set; }
public string BinCode { get; set; } = string.Empty;
public string? BinName { get; set; }
public string? ZoneCode { get; set; }
public bool IsEnabled { get; set; } = true;
public string? Remark { get; set; }
}

View File

@ -0,0 +1,24 @@
using Oms.Domain.Shared;
using SqlSugar;
namespace Oms.Domain.WarehouseAggregate.Entities;
[SugarTable("C_inbound_box")]
public class CInboundBox : OmsAuditedAggregateRoot
{
public int InboundNoticeId { get; set; }
public string BoxSeqNo { get; set; } = string.Empty;
public string? MarkBoxNo { get; set; }
public decimal? WeightG { get; set; }
public decimal? LengthCm { get; set; }
public decimal? WidthCm { get; set; }
public decimal? HeightCm { get; set; }
public bool IsReceived { get; set; }
}

View File

@ -0,0 +1,20 @@
using Oms.Domain.Shared;
using SqlSugar;
namespace Oms.Domain.WarehouseAggregate.Entities;
[SugarTable("C_inbound_box_line")]
public class CInboundBoxLine : OmsAuditedAggregateRoot
{
public int InboundBoxId { get; set; }
public int SkuId { get; set; }
public string SkuCode { get; set; } = string.Empty;
public int PlannedQty { get; set; }
public int? ReceivedQty { get; set; }
public int? BinLocationId { get; set; }
}

View File

@ -0,0 +1,26 @@
using Oms.Domain.Shared;
using SqlSugar;
namespace Oms.Domain.WarehouseAggregate.Entities;
[SugarTable("C_inbound_notice")]
public class CInboundNotice : OmsAuditedAggregateRoot
{
public int CustomerId { get; set; }
public int WarehouseId { get; set; }
public string NoticeNo { get; set; } = string.Empty;
public string? MarkReference { get; set; }
public string InboundType { get; set; } = "normal";
public string ReceiptStatus { get; set; } = "forecasted";
public DateTime? ExpectedArrivalTime { get; set; }
public DateTime? ActualArrivalTime { get; set; }
public string? Remark { get; set; }
}

View File

@ -0,0 +1,18 @@
using Oms.Domain.Shared;
using SqlSugar;
namespace Oms.Domain.WarehouseAggregate.Entities;
[SugarTable("C_inventory")]
public class CInventory : OmsAuditedAggregateRoot
{
public int CustomerId { get; set; }
public int WarehouseId { get; set; }
public int SkuId { get; set; }
public int BinLocationId { get; set; }
public int Qty { get; set; }
}

View File

@ -0,0 +1,22 @@
using Oms.Domain.Shared;
using SqlSugar;
namespace Oms.Domain.WarehouseAggregate.Entities;
[SugarTable("C_parcel")]
public class CParcel : OmsAuditedAggregateRoot
{
public int CustomerId { get; set; }
public int WarehouseId { get; set; }
public string TrackingNo { get; set; } = string.Empty;
public string? CarrierName { get; set; }
public string LifecycleStatus { get; set; } = "inTransit";
public DateTime? ArrivalTime { get; set; }
public string? Remark { get; set; }
}

View File

@ -0,0 +1,22 @@
using Oms.Domain.Shared;
using SqlSugar;
namespace Oms.Domain.WarehouseAggregate.Entities;
[SugarTable("C_parcel_item")]
public class CParcelItem : OmsAuditedAggregateRoot
{
public int ParcelId { get; set; }
public string? ProductName { get; set; }
public int Quantity { get; set; }
public int? SkuId { get; set; }
public string? SkuCode { get; set; }
public int? BinLocationId { get; set; }
public string? Remark { get; set; }
}

View File

@ -0,0 +1,26 @@
using Oms.Domain.Shared;
using SqlSugar;
namespace Oms.Domain.WarehouseAggregate.Entities;
[SugarTable("C_sku")]
public class CSku : OmsAuditedAggregateRoot
{
public int CustomerId { get; set; }
public string SkuCode { get; set; } = string.Empty;
public string SkuName { get; set; } = string.Empty;
public string? SkuImageUrl { get; set; }
public decimal? WeightG { get; set; }
public decimal? LengthCm { get; set; }
public decimal? WidthCm { get; set; }
public decimal? HeightCm { get; set; }
public string? Remark { get; set; }
}

View File

@ -0,0 +1,18 @@
using Oms.Domain.Shared;
using SqlSugar;
namespace Oms.Domain.WarehouseAggregate.Entities;
[SugarTable("C_sku_platform_map")]
public class CSkuPlatformMap : OmsAuditedAggregateRoot
{
public int CustomerId { get; set; }
public int StoreId { get; set; }
public string PlatformSku { get; set; } = string.Empty;
public int SkuId { get; set; }
public string SkuCode { get; set; } = string.Empty;
}

View File

@ -0,0 +1,22 @@
using Fake.Domain.Repositories;
using Oms.Domain.WarehouseAggregate.Entities;
namespace Oms.Domain.WarehouseAggregate.Repositories;
public interface ICSkuRepository : IRepository<CSku>;
public interface ICBinLocationRepository : IRepository<CBinLocation>;
public interface ICInventoryRepository : IRepository<CInventory>;
public interface ICInboundNoticeRepository : IRepository<CInboundNotice>;
public interface ICInboundBoxRepository : IRepository<CInboundBox>;
public interface ICInboundBoxLineRepository : IRepository<CInboundBoxLine>;
public interface ICParcelRepository : IRepository<CParcel>;
public interface ICParcelItemRepository : IRepository<CParcelItem>;
public interface ICSkuPlatformMapRepository : IRepository<CSkuPlatformMap>;

View File

@ -0,0 +1,12 @@
using Fake.DependencyInjection;
using Oms.Domain.OrdersAggregate.Entities;
using Oms.Domain.OrdersAggregate.Repositories;
using Oms.SqlSugarCore.Repositories;
namespace Oms.SqlSugarCore.Orders;
public class SalesStoreRepository : OmsSqlSugarRepository<SalesStore>, ISalesStoreRepository, IScopedDependency;
public class SalesStoreOrderRepository : OmsSqlSugarRepository<SalesStoreOrder>, ISalesStoreOrderRepository, IScopedDependency;
public class SalesStoreOrderLineRepository : OmsSqlSugarRepository<SalesStoreOrderLine>, ISalesStoreOrderLineRepository, IScopedDependency;

View File

@ -0,0 +1,24 @@
using Fake.DependencyInjection;
using Oms.Domain.WarehouseAggregate.Entities;
using Oms.Domain.WarehouseAggregate.Repositories;
using Oms.SqlSugarCore.Repositories;
namespace Oms.SqlSugarCore.Warehouse;
public class CSkuRepository : OmsSqlSugarRepository<CSku>, ICSkuRepository, IScopedDependency;
public class CBinLocationRepository : OmsSqlSugarRepository<CBinLocation>, ICBinLocationRepository, IScopedDependency;
public class CInventoryRepository : OmsSqlSugarRepository<CInventory>, ICInventoryRepository, IScopedDependency;
public class CInboundNoticeRepository : OmsSqlSugarRepository<CInboundNotice>, ICInboundNoticeRepository, IScopedDependency;
public class CInboundBoxRepository : OmsSqlSugarRepository<CInboundBox>, ICInboundBoxRepository, IScopedDependency;
public class CInboundBoxLineRepository : OmsSqlSugarRepository<CInboundBoxLine>, ICInboundBoxLineRepository, IScopedDependency;
public class CParcelRepository : OmsSqlSugarRepository<CParcel>, ICParcelRepository, IScopedDependency;
public class CParcelItemRepository : OmsSqlSugarRepository<CParcelItem>, ICParcelItemRepository, IScopedDependency;
public class CSkuPlatformMapRepository : OmsSqlSugarRepository<CSkuPlatformMap>, ICSkuPlatformMapRepository, IScopedDependency;