feat: 实现仓储模块与平台店铺订单后端能力
新增仓储领域实体、仓储库、WarehouseService 全套接口及数据库迁移脚本; 实现店铺授权、店铺订单服务与库存匹配创建小包单;补充仓储模块 RBAC 权限种子数据。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
18b53de20c
commit
a383ee6f32
263
sql/migrate_warehouse_module.sql
Normal file
263
sql/migrate_warehouse_module.sql
Normal 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='销售店铺订单明细';
|
||||
42
sql/seed_warehouse_module_permissions.sql
Normal file
42
sql/seed_warehouse_module_permissions.sql
Normal 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`
|
||||
);
|
||||
293
src/Oms.Application/Dtos/PlatformStore/PlatformStoreDtos.cs
Normal file
293
src/Oms.Application/Dtos/PlatformStore/PlatformStoreDtos.cs
Normal 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;
|
||||
}
|
||||
468
src/Oms.Application/Dtos/Warehouse/WarehouseDtos.cs
Normal file
468
src/Oms.Application/Dtos/Warehouse/WarehouseDtos.cs
Normal 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; }
|
||||
}
|
||||
@ -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("收件人信息不完整,请同步平台订单或在匹配时补充收件人");
|
||||
}
|
||||
}
|
||||
}
|
||||
612
src/Oms.Application/Services/Orders/PlatformStoreOrderService.cs
Normal file
612
src/Oms.Application/Services/Orders/PlatformStoreOrderService.cs
Normal 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);
|
||||
}
|
||||
192
src/Oms.Application/Services/Orders/PlatformStoreService.cs
Normal file
192
src/Oms.Application/Services/Orders/PlatformStoreService.cs
Normal 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
|
||||
};
|
||||
}
|
||||
@ -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
|
||||
};
|
||||
}
|
||||
@ -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} 件");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
};
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
};
|
||||
}
|
||||
179
src/Oms.Application/Services/Warehouse/WarehouseService.Sku.cs
Normal file
179
src/Oms.Application/Services/Warehouse/WarehouseService.Sku.cs
Normal 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)
|
||||
};
|
||||
}
|
||||
24
src/Oms.Application/Services/Warehouse/WarehouseService.cs
Normal file
24
src/Oms.Application/Services/Warehouse/WarehouseService.cs
Normal 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>();
|
||||
}
|
||||
31
src/Oms.Application/Shared/WarehouseCustomerHelper.cs
Normal file
31
src/Oms.Application/Shared/WarehouseCustomerHelper.cs
Normal 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)}";
|
||||
}
|
||||
41
src/Oms.Domain/OrdersAggregate/Entities/SalesStore.cs
Normal file
41
src/Oms.Domain/OrdersAggregate/Entities/SalesStore.cs
Normal 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; }
|
||||
}
|
||||
51
src/Oms.Domain/OrdersAggregate/Entities/SalesStoreOrder.cs
Normal file
51
src/Oms.Domain/OrdersAggregate/Entities/SalesStoreOrder.cs
Normal 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; }
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
@ -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>;
|
||||
38
src/Oms.Domain/WarehouseAggregate/Consts/WarehouseCodes.cs
Normal file
38
src/Oms.Domain/WarehouseAggregate/Consts/WarehouseCodes.cs
Normal 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";
|
||||
}
|
||||
20
src/Oms.Domain/WarehouseAggregate/Entities/CBinLocation.cs
Normal file
20
src/Oms.Domain/WarehouseAggregate/Entities/CBinLocation.cs
Normal 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; }
|
||||
}
|
||||
24
src/Oms.Domain/WarehouseAggregate/Entities/CInboundBox.cs
Normal file
24
src/Oms.Domain/WarehouseAggregate/Entities/CInboundBox.cs
Normal 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; }
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
26
src/Oms.Domain/WarehouseAggregate/Entities/CInboundNotice.cs
Normal file
26
src/Oms.Domain/WarehouseAggregate/Entities/CInboundNotice.cs
Normal 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; }
|
||||
}
|
||||
18
src/Oms.Domain/WarehouseAggregate/Entities/CInventory.cs
Normal file
18
src/Oms.Domain/WarehouseAggregate/Entities/CInventory.cs
Normal 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; }
|
||||
}
|
||||
22
src/Oms.Domain/WarehouseAggregate/Entities/CParcel.cs
Normal file
22
src/Oms.Domain/WarehouseAggregate/Entities/CParcel.cs
Normal 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; }
|
||||
}
|
||||
22
src/Oms.Domain/WarehouseAggregate/Entities/CParcelItem.cs
Normal file
22
src/Oms.Domain/WarehouseAggregate/Entities/CParcelItem.cs
Normal 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; }
|
||||
}
|
||||
26
src/Oms.Domain/WarehouseAggregate/Entities/CSku.cs
Normal file
26
src/Oms.Domain/WarehouseAggregate/Entities/CSku.cs
Normal 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; }
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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>;
|
||||
12
src/Oms.SqlSugarCore/Orders/SalesStoreRepositories.cs
Normal file
12
src/Oms.SqlSugarCore/Orders/SalesStoreRepositories.cs
Normal 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;
|
||||
24
src/Oms.SqlSugarCore/Warehouse/WarehouseRepositories.cs
Normal file
24
src/Oms.SqlSugarCore/Warehouse/WarehouseRepositories.cs
Normal 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;
|
||||
Loading…
Reference in New Issue
Block a user