feat: 物流单绑定包裹/库存(冻结可用库存)
支持我的订单对已创建物流单绑定仓储资源:新增 t_order_stock_bind 与 stock-bind API,库存/包裹增加 FrozenQty、AvailableQty。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
039cbba3d1
commit
dc3071c90d
92
sql/migrate_t_order_stock_bind.sql
Normal file
92
sql/migrate_t_order_stock_bind.sql
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
-- 物流单绑定包裹/库存:t_order_stock_bind + C_inventory/C_parcel_item 冻结与可用字段
|
||||||
|
-- 见 oms系统设计-050801.md §4.2.11 / §4.4.6
|
||||||
|
-- 可重复执行
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `t_order_stock_bind` (
|
||||||
|
`Id` INT NOT NULL AUTO_INCREMENT,
|
||||||
|
`OrderId` INT NOT NULL COMMENT '物流单 t_order.Id',
|
||||||
|
`CustomerId` INT NOT NULL COMMENT '客户隔离键',
|
||||||
|
`WarehouseId` INT NOT NULL COMMENT '仓库',
|
||||||
|
`BindType` VARCHAR(20) NOT NULL COMMENT 'inventory / parcelItem',
|
||||||
|
`SkuId` INT NULL COMMENT 'SKU',
|
||||||
|
`InventoryId` INT NULL COMMENT 'C_inventory.Id,可空=按SKU分摊',
|
||||||
|
`BinLocationId` INT NULL COMMENT '库位快照',
|
||||||
|
`ParcelId` INT NULL COMMENT 'C_parcel.Id',
|
||||||
|
`ParcelItemId` INT NULL COMMENT 'C_parcel_item.Id',
|
||||||
|
`BindQty` INT NOT NULL COMMENT '绑定数量',
|
||||||
|
`Status` VARCHAR(20) NOT NULL DEFAULT 'bound' COMMENT 'bound/deducted/released',
|
||||||
|
`SkuCode` VARCHAR(64) NULL,
|
||||||
|
`SkuName` VARCHAR(128) NULL,
|
||||||
|
`TrackingNo` VARCHAR(64) NULL,
|
||||||
|
`ProductName` VARCHAR(256) NULL,
|
||||||
|
`ImageUrl` VARCHAR(512) NULL,
|
||||||
|
`AttributeText` VARCHAR(256) NULL,
|
||||||
|
`Remark` VARCHAR(512) NULL,
|
||||||
|
`IsDeleted` TINYINT NOT NULL DEFAULT 0,
|
||||||
|
`CreateUserId` INT NOT NULL,
|
||||||
|
`CreateTime` DATETIME NOT NULL,
|
||||||
|
`UpdateUserId` INT NULL,
|
||||||
|
`UpdateTime` DATETIME NULL,
|
||||||
|
PRIMARY KEY (`Id`),
|
||||||
|
KEY `idx_osb_order` (`OrderId`, `IsDeleted`, `Status`),
|
||||||
|
KEY `idx_osb_parcel_item` (`ParcelItemId`, `Status`, `IsDeleted`),
|
||||||
|
KEY `idx_osb_sku` (`CustomerId`, `WarehouseId`, `SkuId`, `Status`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物流单仓储绑定明细';
|
||||||
|
|
||||||
|
-- C_inventory.FrozenQty
|
||||||
|
SET @col_exists := (
|
||||||
|
SELECT COUNT(1) FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'C_inventory' AND COLUMN_NAME = 'FrozenQty'
|
||||||
|
);
|
||||||
|
SET @sql := IF(
|
||||||
|
@col_exists = 0,
|
||||||
|
'ALTER TABLE `C_inventory` ADD COLUMN `FrozenQty` INT NOT NULL DEFAULT 0 COMMENT ''冻结数量'' AFTER `Qty`',
|
||||||
|
'SELECT ''C_inventory.FrozenQty already exists'' AS info'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- C_inventory.AvailableQty
|
||||||
|
SET @col_exists := (
|
||||||
|
SELECT COUNT(1) FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'C_inventory' AND COLUMN_NAME = 'AvailableQty'
|
||||||
|
);
|
||||||
|
SET @sql := IF(
|
||||||
|
@col_exists = 0,
|
||||||
|
'ALTER TABLE `C_inventory` ADD COLUMN `AvailableQty` INT NOT NULL DEFAULT 0 COMMENT ''可用=Qty-FrozenQty'' AFTER `FrozenQty`',
|
||||||
|
'SELECT ''C_inventory.AvailableQty already exists'' AS info'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
UPDATE `C_inventory`
|
||||||
|
SET `AvailableQty` = GREATEST(0, `Qty` - IFNULL(`FrozenQty`, 0))
|
||||||
|
WHERE `AvailableQty` <> GREATEST(0, `Qty` - IFNULL(`FrozenQty`, 0))
|
||||||
|
OR (`AvailableQty` = 0 AND `Qty` > 0 AND IFNULL(`FrozenQty`, 0) = 0);
|
||||||
|
|
||||||
|
-- C_parcel_item.FrozenQty
|
||||||
|
SET @col_exists := (
|
||||||
|
SELECT COUNT(1) FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'C_parcel_item' AND COLUMN_NAME = 'FrozenQty'
|
||||||
|
);
|
||||||
|
SET @sql := IF(
|
||||||
|
@col_exists = 0,
|
||||||
|
'ALTER TABLE `C_parcel_item` ADD COLUMN `FrozenQty` INT NOT NULL DEFAULT 0 COMMENT ''冻结数量'' AFTER `Quantity`',
|
||||||
|
'SELECT ''C_parcel_item.FrozenQty already exists'' AS info'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- C_parcel_item.AvailableQty
|
||||||
|
SET @col_exists := (
|
||||||
|
SELECT COUNT(1) FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'C_parcel_item' AND COLUMN_NAME = 'AvailableQty'
|
||||||
|
);
|
||||||
|
SET @sql := IF(
|
||||||
|
@col_exists = 0,
|
||||||
|
'ALTER TABLE `C_parcel_item` ADD COLUMN `AvailableQty` INT NOT NULL DEFAULT 0 COMMENT ''可用=Quantity-FrozenQty'' AFTER `FrozenQty`',
|
||||||
|
'SELECT ''C_parcel_item.AvailableQty already exists'' AS info'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
UPDATE `C_parcel_item`
|
||||||
|
SET `AvailableQty` = GREATEST(0, `Quantity` - IFNULL(`FrozenQty`, 0))
|
||||||
|
WHERE `AvailableQty` <> GREATEST(0, `Quantity` - IFNULL(`FrozenQty`, 0))
|
||||||
|
OR (`AvailableQty` = 0 AND `Quantity` > 0 AND IFNULL(`FrozenQty`, 0) = 0);
|
||||||
159
src/Oms.Application/Dtos/Orders/OrderStockBindDtos.cs
Normal file
159
src/Oms.Application/Dtos/Orders/OrderStockBindDtos.cs
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Oms.Application.Dtos.Orders;
|
||||||
|
|
||||||
|
public class OrderStockBindDetailOutput
|
||||||
|
{
|
||||||
|
public int OrderId { get; set; }
|
||||||
|
|
||||||
|
public int? WarehouseId { get; set; }
|
||||||
|
|
||||||
|
public string? WarehouseName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>none / bound / deducted</summary>
|
||||||
|
public string BindStatus { get; set; } = "none";
|
||||||
|
|
||||||
|
public List<OrderStockBindItemOutput> Items { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OrderStockBindItemOutput
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
public string BindType { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public int? SkuId { get; set; }
|
||||||
|
|
||||||
|
public string? SkuCode { get; set; }
|
||||||
|
|
||||||
|
public string? SkuName { get; set; }
|
||||||
|
|
||||||
|
public int? InventoryId { get; set; }
|
||||||
|
|
||||||
|
public int? BinLocationId { get; set; }
|
||||||
|
|
||||||
|
public string? BinCode { get; set; }
|
||||||
|
|
||||||
|
public int? ParcelId { get; set; }
|
||||||
|
|
||||||
|
public int? ParcelItemId { get; set; }
|
||||||
|
|
||||||
|
public string? TrackingNo { get; set; }
|
||||||
|
|
||||||
|
public string? ProductName { get; set; }
|
||||||
|
|
||||||
|
public string? AttributeText { get; set; }
|
||||||
|
|
||||||
|
public string? ImageUrl { get; set; }
|
||||||
|
|
||||||
|
public int BindQty { get; set; }
|
||||||
|
|
||||||
|
public int AvailableQty { get; set; }
|
||||||
|
|
||||||
|
public string Status { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OrderStockBindCandidatesInput
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public int OrderId { get; set; }
|
||||||
|
|
||||||
|
public int? WarehouseId { get; set; }
|
||||||
|
|
||||||
|
public string? Keyword { get; set; }
|
||||||
|
|
||||||
|
/// <summary>包裹候选:空/all=全部生命周期。</summary>
|
||||||
|
public string? LifecycleStatus { get; set; }
|
||||||
|
|
||||||
|
public int Page { get; set; } = 1;
|
||||||
|
|
||||||
|
public int PageSize { get; set; } = 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OrderStockBindInventoryCandidateOutput
|
||||||
|
{
|
||||||
|
public int SkuId { get; set; }
|
||||||
|
|
||||||
|
public string SkuCode { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string SkuName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string? SkuImageUrl { get; set; }
|
||||||
|
|
||||||
|
public int WarehouseId { get; set; }
|
||||||
|
|
||||||
|
public int TotalQty { get; set; }
|
||||||
|
|
||||||
|
public int FrozenQty { get; set; }
|
||||||
|
|
||||||
|
public int AvailableQty { get; set; }
|
||||||
|
|
||||||
|
public int BinCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OrderStockBindParcelCandidateOutput
|
||||||
|
{
|
||||||
|
public int ParcelId { get; set; }
|
||||||
|
|
||||||
|
public string TrackingNo { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string? CarrierName { get; set; }
|
||||||
|
|
||||||
|
public string LifecycleStatus { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public int WarehouseId { get; set; }
|
||||||
|
|
||||||
|
public List<OrderStockBindParcelItemCandidateOutput> Items { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OrderStockBindParcelItemCandidateOutput
|
||||||
|
{
|
||||||
|
public int ParcelItemId { get; set; }
|
||||||
|
|
||||||
|
public string? ProductName { get; set; }
|
||||||
|
|
||||||
|
public string? AttributeText { get; set; }
|
||||||
|
|
||||||
|
public string? ImageUrl { get; set; }
|
||||||
|
|
||||||
|
public int? SkuId { get; set; }
|
||||||
|
|
||||||
|
public string? SkuCode { get; set; }
|
||||||
|
|
||||||
|
public int Quantity { get; set; }
|
||||||
|
|
||||||
|
public int FrozenQty { get; set; }
|
||||||
|
|
||||||
|
public int AvailableQty { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OrderStockBindSaveInput
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public int OrderId { get; set; }
|
||||||
|
|
||||||
|
public List<OrderStockBindSaveItemInput> Items { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OrderStockBindSaveItemInput
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public string BindType { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public int? SkuId { get; set; }
|
||||||
|
|
||||||
|
public int? InventoryId { get; set; }
|
||||||
|
|
||||||
|
public int? ParcelId { get; set; }
|
||||||
|
|
||||||
|
public int? ParcelItemId { get; set; }
|
||||||
|
|
||||||
|
[Range(1, int.MaxValue)]
|
||||||
|
public int BindQty { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OrderStockBindOrderInput
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public int OrderId { get; set; }
|
||||||
|
}
|
||||||
@ -354,6 +354,10 @@ public class WarehouseParcelItemOutput
|
|||||||
|
|
||||||
public int Quantity { get; set; }
|
public int Quantity { get; set; }
|
||||||
|
|
||||||
|
public int FrozenQty { get; set; }
|
||||||
|
|
||||||
|
public int AvailableQty { get; set; }
|
||||||
|
|
||||||
public int? SkuId { get; set; }
|
public int? SkuId { get; set; }
|
||||||
|
|
||||||
public string? SkuCode { get; set; }
|
public string? SkuCode { get; set; }
|
||||||
@ -447,6 +451,10 @@ public class WarehouseInventoryOutput
|
|||||||
|
|
||||||
public int TotalQty { get; set; }
|
public int TotalQty { get; set; }
|
||||||
|
|
||||||
|
public int FrozenQty { get; set; }
|
||||||
|
|
||||||
|
public int AvailableQty { get; set; }
|
||||||
|
|
||||||
public int BinCount { get; set; }
|
public int BinCount { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -461,6 +469,10 @@ public class WarehouseInventoryBinDetailOutput
|
|||||||
public string? BinName { get; set; }
|
public string? BinName { get; set; }
|
||||||
|
|
||||||
public int Qty { get; set; }
|
public int Qty { get; set; }
|
||||||
|
|
||||||
|
public int FrozenQty { get; set; }
|
||||||
|
|
||||||
|
public int AvailableQty { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class WarehouseInventoryRelocateInput
|
public class WarehouseInventoryRelocateInput
|
||||||
|
|||||||
757
src/Oms.Application/Services/Orders/OrderService.StockBind.cs
Normal file
757
src/Oms.Application/Services/Orders/OrderService.StockBind.cs
Normal file
@ -0,0 +1,757 @@
|
|||||||
|
using Fake.Application;
|
||||||
|
using Fake.Domain.Exceptions;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Oms.Application.Dtos.Orders;
|
||||||
|
using Oms.Application.Dtos.Shared;
|
||||||
|
using Oms.Application.Shared;
|
||||||
|
using Oms.Domain.OrdersAggregate.Consts;
|
||||||
|
using Oms.Domain.OrdersAggregate.Entities;
|
||||||
|
using Oms.Domain.OrdersAggregate.Repositories;
|
||||||
|
using Oms.Domain.WarehouseAggregate.Entities;
|
||||||
|
using Oms.Domain.WarehouseAggregate.Repositories;
|
||||||
|
|
||||||
|
namespace Oms.Application.Services.Orders;
|
||||||
|
|
||||||
|
public partial class OrderService
|
||||||
|
{
|
||||||
|
private IOrderStockBindRepository OrderStockBindRepository =>
|
||||||
|
LazyServiceProvider.GetRequiredService<IOrderStockBindRepository>();
|
||||||
|
|
||||||
|
private ICInventoryRepository StockBindInventoryRepository =>
|
||||||
|
LazyServiceProvider.GetRequiredService<ICInventoryRepository>();
|
||||||
|
|
||||||
|
private ICParcelRepository StockBindParcelRepository =>
|
||||||
|
LazyServiceProvider.GetRequiredService<ICParcelRepository>();
|
||||||
|
|
||||||
|
private ICParcelItemRepository StockBindParcelItemRepository =>
|
||||||
|
LazyServiceProvider.GetRequiredService<ICParcelItemRepository>();
|
||||||
|
|
||||||
|
private ICSkuRepository StockBindSkuRepository =>
|
||||||
|
LazyServiceProvider.GetRequiredService<ICSkuRepository>();
|
||||||
|
|
||||||
|
private ICBinLocationRepository StockBindBinLocationRepository =>
|
||||||
|
LazyServiceProvider.GetRequiredService<ICBinLocationRepository>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询物流单当前仓储绑定明细。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("/order/stock-bind/detail")]
|
||||||
|
public async Task<OrderStockBindDetailOutput> GetStockBindDetailAsync([FromQuery] int orderId)
|
||||||
|
{
|
||||||
|
var order = await GetOrderOrThrowAsync(orderId);
|
||||||
|
return await BuildStockBindDetailAsync(order);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 库存候选(左侧库存 Tab)。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("/order/stock-bind/inventory-candidates")]
|
||||||
|
public async Task<OmsPageResult<OrderStockBindInventoryCandidateOutput>> GetStockBindInventoryCandidatesAsync(
|
||||||
|
[FromBody] OrderStockBindCandidatesInput input)
|
||||||
|
{
|
||||||
|
var order = await GetOrderOrThrowAsync(input.OrderId);
|
||||||
|
EnsureOrderCanBindStock(order);
|
||||||
|
var warehouseId = ResolveBindWarehouseId(order, input.WarehouseId);
|
||||||
|
var customerId = RequireOrderCustomerId(order);
|
||||||
|
|
||||||
|
var inventories = await StockBindInventoryRepository.GetListAsync(x =>
|
||||||
|
x.CustomerId == customerId
|
||||||
|
&& x.WarehouseId == warehouseId
|
||||||
|
&& x.AvailableQty > 0);
|
||||||
|
|
||||||
|
var skuIds = inventories.Select(x => x.SkuId).Distinct().ToArray();
|
||||||
|
var skus = skuIds.Length == 0
|
||||||
|
? []
|
||||||
|
: await StockBindSkuRepository.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 => x.SkuId)
|
||||||
|
.Select(g =>
|
||||||
|
{
|
||||||
|
skuMap.TryGetValue(g.Key, out var sku);
|
||||||
|
return new OrderStockBindInventoryCandidateOutput
|
||||||
|
{
|
||||||
|
SkuId = g.Key,
|
||||||
|
SkuCode = sku?.SkuCode ?? string.Empty,
|
||||||
|
SkuName = sku?.SkuName ?? string.Empty,
|
||||||
|
SkuImageUrl = sku?.SkuImageUrl,
|
||||||
|
WarehouseId = warehouseId,
|
||||||
|
TotalQty = g.Sum(x => x.Qty),
|
||||||
|
FrozenQty = g.Sum(x => x.FrozenQty),
|
||||||
|
AvailableQty = g.Sum(x => x.AvailableQty),
|
||||||
|
BinCount = g.Count()
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.Where(x =>
|
||||||
|
string.IsNullOrWhiteSpace(keyword)
|
||||||
|
|| x.SkuCode.Contains(keyword!, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| x.SkuName.Contains(keyword!, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.OrderBy(x => x.SkuCode)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var pageIndex = OmsDocHelper.NormalizePage(input.Page);
|
||||||
|
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
|
||||||
|
var skip = (pageIndex - 1) * pageSize;
|
||||||
|
return new OmsPageResult<OrderStockBindInventoryCandidateOutput>
|
||||||
|
{
|
||||||
|
Items = grouped.Skip(skip).Take(pageSize).ToList(),
|
||||||
|
Total = grouped.Count
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 包裹候选(左侧包裹 Tab,允许全部生命周期)。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("/order/stock-bind/parcel-candidates")]
|
||||||
|
public async Task<OmsPageResult<OrderStockBindParcelCandidateOutput>> GetStockBindParcelCandidatesAsync(
|
||||||
|
[FromBody] OrderStockBindCandidatesInput input)
|
||||||
|
{
|
||||||
|
var order = await GetOrderOrThrowAsync(input.OrderId);
|
||||||
|
EnsureOrderCanBindStock(order);
|
||||||
|
var warehouseId = ResolveBindWarehouseId(order, input.WarehouseId);
|
||||||
|
var customerId = RequireOrderCustomerId(order);
|
||||||
|
|
||||||
|
var lifecycle = input.LifecycleStatus?.Trim();
|
||||||
|
var filterLifecycle = !string.IsNullOrWhiteSpace(lifecycle)
|
||||||
|
&& !string.Equals(lifecycle, "all", StringComparison.OrdinalIgnoreCase);
|
||||||
|
var keyword = input.Keyword?.Trim();
|
||||||
|
|
||||||
|
var parcels = await StockBindParcelRepository.GetListAsync(x =>
|
||||||
|
x.CustomerId == customerId
|
||||||
|
&& x.WarehouseId == warehouseId
|
||||||
|
&& (!filterLifecycle || x.LifecycleStatus == lifecycle));
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(keyword))
|
||||||
|
{
|
||||||
|
parcels = parcels
|
||||||
|
.Where(x =>
|
||||||
|
x.TrackingNo.Contains(keyword, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| (x.CarrierName?.Contains(keyword, StringComparison.OrdinalIgnoreCase) ?? false))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
parcels = parcels.OrderByDescending(x => x.Id).ToList();
|
||||||
|
var pageIndex = OmsDocHelper.NormalizePage(input.Page);
|
||||||
|
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
|
||||||
|
var pageParcels = parcels.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList();
|
||||||
|
var parcelIds = pageParcels.Select(x => x.Id).ToArray();
|
||||||
|
var allItems = parcelIds.Length == 0
|
||||||
|
? []
|
||||||
|
: await StockBindParcelItemRepository.GetListAsync(x => parcelIds.Contains(x.ParcelId));
|
||||||
|
var itemsByParcel = allItems.GroupBy(x => x.ParcelId).ToDictionary(g => g.Key, g => g.ToList());
|
||||||
|
|
||||||
|
var outputs = new List<OrderStockBindParcelCandidateOutput>();
|
||||||
|
foreach (var parcel in pageParcels)
|
||||||
|
{
|
||||||
|
itemsByParcel.TryGetValue(parcel.Id, out var items);
|
||||||
|
items ??= [];
|
||||||
|
var availableItems = items
|
||||||
|
.Where(x => GetParcelItemAvailableQty(x) > 0)
|
||||||
|
.Select(x => new OrderStockBindParcelItemCandidateOutput
|
||||||
|
{
|
||||||
|
ParcelItemId = x.Id,
|
||||||
|
ProductName = x.ProductName,
|
||||||
|
AttributeText = x.AttributeText,
|
||||||
|
ImageUrl = x.ImageUrl,
|
||||||
|
SkuId = x.SkuId,
|
||||||
|
SkuCode = x.SkuCode,
|
||||||
|
Quantity = x.Quantity,
|
||||||
|
FrozenQty = x.FrozenQty,
|
||||||
|
AvailableQty = GetParcelItemAvailableQty(x)
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
if (availableItems.Count == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
outputs.Add(new OrderStockBindParcelCandidateOutput
|
||||||
|
{
|
||||||
|
ParcelId = parcel.Id,
|
||||||
|
TrackingNo = parcel.TrackingNo,
|
||||||
|
CarrierName = parcel.CarrierName,
|
||||||
|
LifecycleStatus = parcel.LifecycleStatus,
|
||||||
|
WarehouseId = parcel.WarehouseId,
|
||||||
|
Items = availableItems
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new OmsPageResult<OrderStockBindParcelCandidateOutput>
|
||||||
|
{
|
||||||
|
Items = outputs,
|
||||||
|
Total = parcels.Count
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存绑定(全量覆盖 + 冻结)。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("/order/stock-bind/save")]
|
||||||
|
public async Task<OrderStockBindDetailOutput> SaveStockBindAsync([FromBody] OrderStockBindSaveInput input)
|
||||||
|
{
|
||||||
|
var order = await GetOrderOrThrowAsync(input.OrderId);
|
||||||
|
EnsureOrderCanBindStock(order);
|
||||||
|
await EnsureNoDeductedBindsAsync(order.Id);
|
||||||
|
var customerId = RequireOrderCustomerId(order);
|
||||||
|
var warehouseId = ResolveBindWarehouseId(order, null);
|
||||||
|
var items = input.Items ?? [];
|
||||||
|
|
||||||
|
await ReleaseBoundBindsAsync(order.Id);
|
||||||
|
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
if (item.BindQty <= 0)
|
||||||
|
{
|
||||||
|
throw new BusinessException("绑定数量必须大于 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
var bindType = (item.BindType ?? string.Empty).Trim();
|
||||||
|
if (string.Equals(bindType, OrderStockBindTypeCodes.Inventory, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
await BindInventoryLineAsync(order, customerId, warehouseId, item);
|
||||||
|
}
|
||||||
|
else if (string.Equals(bindType, OrderStockBindTypeCodes.ParcelItem, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
await BindParcelItemLineAsync(order, customerId, warehouseId, item);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new BusinessException($"不支持的绑定类型:{item.BindType}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!order.WarehouseId.HasValue || order.WarehouseId.Value <= 0)
|
||||||
|
{
|
||||||
|
order.WarehouseId = warehouseId;
|
||||||
|
await UpdateOrderAsync(order);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await BuildStockBindDetailAsync(order);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 清空绑定并释放冻结。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("/order/stock-bind/clear")]
|
||||||
|
public async Task<OrderStockBindDetailOutput> ClearStockBindAsync([FromBody] OrderStockBindOrderInput input)
|
||||||
|
{
|
||||||
|
var order = await GetOrderOrThrowAsync(input.OrderId);
|
||||||
|
EnsureOrderCanBindStock(order);
|
||||||
|
await EnsureNoDeductedBindsAsync(order.Id);
|
||||||
|
await ReleaseBoundBindsAsync(order.Id);
|
||||||
|
return await BuildStockBindDetailAsync(order);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 确认扣减:将 bound 明细转为 deducted,扣减实物并释放冻结。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("/order/stock-bind/confirm-deduct")]
|
||||||
|
public async Task<OrderStockBindDetailOutput> ConfirmStockBindDeductAsync([FromBody] OrderStockBindOrderInput input)
|
||||||
|
{
|
||||||
|
var order = await GetOrderOrThrowAsync(input.OrderId);
|
||||||
|
var binds = await OrderStockBindRepository.GetListAsync(x =>
|
||||||
|
x.OrderId == order.Id && x.Status == OrderStockBindStatusCodes.Bound);
|
||||||
|
foreach (var bind in binds)
|
||||||
|
{
|
||||||
|
await DeductBoundLineAsync(bind);
|
||||||
|
bind.Status = OrderStockBindStatusCodes.Deducted;
|
||||||
|
await OrderStockBindRepository.UpdateAsync(bind);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await BuildStockBindDetailAsync(order);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task BindInventoryLineAsync(
|
||||||
|
Order order,
|
||||||
|
int customerId,
|
||||||
|
int warehouseId,
|
||||||
|
OrderStockBindSaveItemInput item)
|
||||||
|
{
|
||||||
|
if (!item.SkuId.HasValue || item.SkuId.Value <= 0)
|
||||||
|
{
|
||||||
|
throw new BusinessException("库存绑定必须指定 skuId");
|
||||||
|
}
|
||||||
|
|
||||||
|
var sku = await StockBindSkuRepository.FirstOrDefaultAsync(x =>
|
||||||
|
x.Id == item.SkuId.Value && x.CustomerId == customerId)
|
||||||
|
?? throw new BusinessException("SKU 不存在");
|
||||||
|
|
||||||
|
var deductions = await FreezeInventoryAsync(
|
||||||
|
customerId,
|
||||||
|
warehouseId,
|
||||||
|
item.SkuId.Value,
|
||||||
|
item.BindQty,
|
||||||
|
item.InventoryId);
|
||||||
|
|
||||||
|
foreach (var d in deductions)
|
||||||
|
{
|
||||||
|
await OrderStockBindRepository.InsertAsync(new OrderStockBind
|
||||||
|
{
|
||||||
|
OrderId = order.Id,
|
||||||
|
CustomerId = customerId,
|
||||||
|
WarehouseId = warehouseId,
|
||||||
|
BindType = OrderStockBindTypeCodes.Inventory,
|
||||||
|
SkuId = sku.Id,
|
||||||
|
InventoryId = d.InventoryId,
|
||||||
|
BinLocationId = d.BinLocationId,
|
||||||
|
BindQty = d.Qty,
|
||||||
|
Status = OrderStockBindStatusCodes.Bound,
|
||||||
|
SkuCode = sku.SkuCode,
|
||||||
|
SkuName = sku.SkuName,
|
||||||
|
ImageUrl = sku.SkuImageUrl
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task BindParcelItemLineAsync(
|
||||||
|
Order order,
|
||||||
|
int customerId,
|
||||||
|
int warehouseId,
|
||||||
|
OrderStockBindSaveItemInput item)
|
||||||
|
{
|
||||||
|
if (!item.ParcelItemId.HasValue || item.ParcelItemId.Value <= 0)
|
||||||
|
{
|
||||||
|
throw new BusinessException("包裹绑定必须指定 parcelItemId");
|
||||||
|
}
|
||||||
|
|
||||||
|
var parcelItem = await StockBindParcelItemRepository.FirstOrDefaultAsync(x => x.Id == item.ParcelItemId.Value)
|
||||||
|
?? throw new BusinessException("包裹明细不存在");
|
||||||
|
var parcel = await StockBindParcelRepository.FirstOrDefaultAsync(x =>
|
||||||
|
x.Id == parcelItem.ParcelId && x.CustomerId == customerId)
|
||||||
|
?? throw new BusinessException("包裹不存在");
|
||||||
|
if (parcel.WarehouseId != warehouseId)
|
||||||
|
{
|
||||||
|
throw new BusinessException("包裹仓库与订单仓库不一致");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.ParcelId.HasValue && item.ParcelId.Value != parcel.Id)
|
||||||
|
{
|
||||||
|
throw new BusinessException("parcelId 与 parcelItemId 不匹配");
|
||||||
|
}
|
||||||
|
|
||||||
|
var available = GetParcelItemAvailableQty(parcelItem);
|
||||||
|
if (item.BindQty > available)
|
||||||
|
{
|
||||||
|
throw new BusinessException($"包裹明细可用不足,需要 {item.BindQty},可用 {available}");
|
||||||
|
}
|
||||||
|
|
||||||
|
parcelItem.FrozenQty += item.BindQty;
|
||||||
|
SyncParcelItemAvailable(parcelItem);
|
||||||
|
await StockBindParcelItemRepository.UpdateAsync(parcelItem);
|
||||||
|
|
||||||
|
// 已上架行同步冻结库存,避免重复售卖
|
||||||
|
if (parcelItem.SkuId is > 0)
|
||||||
|
{
|
||||||
|
await FreezeInventoryAsync(customerId, warehouseId, parcelItem.SkuId.Value, item.BindQty, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
CSku? sku = null;
|
||||||
|
if (parcelItem.SkuId is > 0)
|
||||||
|
{
|
||||||
|
sku = await StockBindSkuRepository.FirstOrDefaultAsync(x => x.Id == parcelItem.SkuId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
await OrderStockBindRepository.InsertAsync(new OrderStockBind
|
||||||
|
{
|
||||||
|
OrderId = order.Id,
|
||||||
|
CustomerId = customerId,
|
||||||
|
WarehouseId = warehouseId,
|
||||||
|
BindType = OrderStockBindTypeCodes.ParcelItem,
|
||||||
|
SkuId = parcelItem.SkuId,
|
||||||
|
ParcelId = parcel.Id,
|
||||||
|
ParcelItemId = parcelItem.Id,
|
||||||
|
BindQty = item.BindQty,
|
||||||
|
Status = OrderStockBindStatusCodes.Bound,
|
||||||
|
SkuCode = sku?.SkuCode ?? parcelItem.SkuCode,
|
||||||
|
SkuName = sku?.SkuName,
|
||||||
|
TrackingNo = parcel.TrackingNo,
|
||||||
|
ProductName = parcelItem.ProductName,
|
||||||
|
ImageUrl = parcelItem.ImageUrl,
|
||||||
|
AttributeText = parcelItem.AttributeText
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ReleaseBoundBindsAsync(int orderId)
|
||||||
|
{
|
||||||
|
var binds = await OrderStockBindRepository.GetListAsync(x =>
|
||||||
|
x.OrderId == orderId && x.Status == OrderStockBindStatusCodes.Bound);
|
||||||
|
foreach (var bind in binds)
|
||||||
|
{
|
||||||
|
await UnfreezeBoundLineAsync(bind);
|
||||||
|
bind.Status = OrderStockBindStatusCodes.Released;
|
||||||
|
bind.IsDeleted = true;
|
||||||
|
await OrderStockBindRepository.UpdateAsync(bind);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UnfreezeBoundLineAsync(OrderStockBind bind)
|
||||||
|
{
|
||||||
|
if (string.Equals(bind.BindType, OrderStockBindTypeCodes.Inventory, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (bind.InventoryId is > 0)
|
||||||
|
{
|
||||||
|
await UnfreezeInventoryByIdAsync(bind.InventoryId.Value, bind.BindQty);
|
||||||
|
}
|
||||||
|
else if (bind.SkuId is > 0)
|
||||||
|
{
|
||||||
|
await UnfreezeInventoryBySkuAsync(bind.CustomerId, bind.WarehouseId, bind.SkuId.Value, bind.BindQty);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(bind.BindType, OrderStockBindTypeCodes.ParcelItem, StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& bind.ParcelItemId is > 0)
|
||||||
|
{
|
||||||
|
var parcelItem = await StockBindParcelItemRepository.FirstOrDefaultAsync(x => x.Id == bind.ParcelItemId.Value);
|
||||||
|
if (parcelItem != null)
|
||||||
|
{
|
||||||
|
parcelItem.FrozenQty = Math.Max(0, parcelItem.FrozenQty - bind.BindQty);
|
||||||
|
SyncParcelItemAvailable(parcelItem);
|
||||||
|
await StockBindParcelItemRepository.UpdateAsync(parcelItem);
|
||||||
|
|
||||||
|
if (parcelItem.SkuId is > 0)
|
||||||
|
{
|
||||||
|
await UnfreezeInventoryBySkuAsync(
|
||||||
|
bind.CustomerId,
|
||||||
|
bind.WarehouseId,
|
||||||
|
parcelItem.SkuId.Value,
|
||||||
|
bind.BindQty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DeductBoundLineAsync(OrderStockBind bind)
|
||||||
|
{
|
||||||
|
if (string.Equals(bind.BindType, OrderStockBindTypeCodes.Inventory, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (bind.InventoryId is > 0)
|
||||||
|
{
|
||||||
|
var inv = await StockBindInventoryRepository.FirstOrDefaultAsync(x => x.Id == bind.InventoryId.Value)
|
||||||
|
?? throw new BusinessException("库存行不存在");
|
||||||
|
if (inv.FrozenQty < bind.BindQty || inv.Qty < bind.BindQty)
|
||||||
|
{
|
||||||
|
throw new BusinessException("库存冻结量不足,无法扣减");
|
||||||
|
}
|
||||||
|
|
||||||
|
inv.Qty -= bind.BindQty;
|
||||||
|
inv.FrozenQty -= bind.BindQty;
|
||||||
|
SyncInventoryAvailable(inv);
|
||||||
|
await StockBindInventoryRepository.UpdateAsync(inv);
|
||||||
|
}
|
||||||
|
else if (bind.SkuId is > 0)
|
||||||
|
{
|
||||||
|
await DeductFrozenInventoryBySkuAsync(bind.CustomerId, bind.WarehouseId, bind.SkuId.Value, bind.BindQty);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(bind.BindType, OrderStockBindTypeCodes.ParcelItem, StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& bind.ParcelItemId is > 0)
|
||||||
|
{
|
||||||
|
var parcelItem = await StockBindParcelItemRepository.FirstOrDefaultAsync(x => x.Id == bind.ParcelItemId.Value)
|
||||||
|
?? throw new BusinessException("包裹明细不存在");
|
||||||
|
if (parcelItem.FrozenQty < bind.BindQty || parcelItem.Quantity < bind.BindQty)
|
||||||
|
{
|
||||||
|
throw new BusinessException("包裹冻结量不足,无法扣减");
|
||||||
|
}
|
||||||
|
|
||||||
|
parcelItem.Quantity -= bind.BindQty;
|
||||||
|
parcelItem.FrozenQty -= bind.BindQty;
|
||||||
|
SyncParcelItemAvailable(parcelItem);
|
||||||
|
await StockBindParcelItemRepository.UpdateAsync(parcelItem);
|
||||||
|
|
||||||
|
if (parcelItem.SkuId is > 0)
|
||||||
|
{
|
||||||
|
await DeductFrozenInventoryBySkuAsync(
|
||||||
|
bind.CustomerId,
|
||||||
|
bind.WarehouseId,
|
||||||
|
parcelItem.SkuId.Value,
|
||||||
|
bind.BindQty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<(int InventoryId, int BinLocationId, int Qty)>> FreezeInventoryAsync(
|
||||||
|
int customerId,
|
||||||
|
int warehouseId,
|
||||||
|
int skuId,
|
||||||
|
int requiredQty,
|
||||||
|
int? inventoryId)
|
||||||
|
{
|
||||||
|
List<CInventory> rows;
|
||||||
|
if (inventoryId is > 0)
|
||||||
|
{
|
||||||
|
var row = await StockBindInventoryRepository.FirstOrDefaultAsync(x =>
|
||||||
|
x.Id == inventoryId.Value
|
||||||
|
&& x.CustomerId == customerId
|
||||||
|
&& x.WarehouseId == warehouseId
|
||||||
|
&& x.SkuId == skuId)
|
||||||
|
?? throw new BusinessException("指定库存行不存在");
|
||||||
|
rows = [row];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
rows = await StockBindInventoryRepository.GetListAsync(x =>
|
||||||
|
x.CustomerId == customerId
|
||||||
|
&& x.WarehouseId == warehouseId
|
||||||
|
&& x.SkuId == skuId
|
||||||
|
&& x.AvailableQty > 0);
|
||||||
|
rows = rows.OrderBy(x => x.BinLocationId).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
var available = rows.Sum(GetInventoryAvailableQty);
|
||||||
|
if (available < requiredQty)
|
||||||
|
{
|
||||||
|
throw new BusinessException($"SKU {skuId} 库存可用不足,需要 {requiredQty},可用 {available}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new List<(int InventoryId, int BinLocationId, int Qty)>();
|
||||||
|
var remaining = requiredQty;
|
||||||
|
foreach (var row in rows)
|
||||||
|
{
|
||||||
|
if (remaining <= 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var take = Math.Min(GetInventoryAvailableQty(row), remaining);
|
||||||
|
if (take <= 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
row.FrozenQty += take;
|
||||||
|
SyncInventoryAvailable(row);
|
||||||
|
await StockBindInventoryRepository.UpdateAsync(row);
|
||||||
|
result.Add((row.Id, row.BinLocationId, take));
|
||||||
|
remaining -= take;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remaining > 0)
|
||||||
|
{
|
||||||
|
throw new BusinessException($"SKU {skuId} 冻结失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UnfreezeInventoryByIdAsync(int inventoryId, int qty)
|
||||||
|
{
|
||||||
|
var inv = await StockBindInventoryRepository.FirstOrDefaultAsync(x => x.Id == inventoryId);
|
||||||
|
if (inv == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
inv.FrozenQty = Math.Max(0, inv.FrozenQty - qty);
|
||||||
|
SyncInventoryAvailable(inv);
|
||||||
|
await StockBindInventoryRepository.UpdateAsync(inv);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UnfreezeInventoryBySkuAsync(int customerId, int warehouseId, int skuId, int qty)
|
||||||
|
{
|
||||||
|
var rows = await StockBindInventoryRepository.GetListAsync(x =>
|
||||||
|
x.CustomerId == customerId
|
||||||
|
&& x.WarehouseId == warehouseId
|
||||||
|
&& x.SkuId == skuId
|
||||||
|
&& x.FrozenQty > 0);
|
||||||
|
rows = rows.OrderByDescending(x => x.FrozenQty).ToList();
|
||||||
|
var remaining = qty;
|
||||||
|
foreach (var row in rows)
|
||||||
|
{
|
||||||
|
if (remaining <= 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var release = Math.Min(row.FrozenQty, remaining);
|
||||||
|
row.FrozenQty -= release;
|
||||||
|
SyncInventoryAvailable(row);
|
||||||
|
await StockBindInventoryRepository.UpdateAsync(row);
|
||||||
|
remaining -= release;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DeductFrozenInventoryBySkuAsync(int customerId, int warehouseId, int skuId, int qty)
|
||||||
|
{
|
||||||
|
var rows = await StockBindInventoryRepository.GetListAsync(x =>
|
||||||
|
x.CustomerId == customerId
|
||||||
|
&& x.WarehouseId == warehouseId
|
||||||
|
&& x.SkuId == skuId
|
||||||
|
&& x.FrozenQty > 0);
|
||||||
|
rows = rows.OrderBy(x => x.BinLocationId).ToList();
|
||||||
|
var remaining = qty;
|
||||||
|
foreach (var row in rows)
|
||||||
|
{
|
||||||
|
if (remaining <= 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var take = Math.Min(row.FrozenQty, remaining);
|
||||||
|
take = Math.Min(take, row.Qty);
|
||||||
|
if (take <= 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
row.Qty -= take;
|
||||||
|
row.FrozenQty -= take;
|
||||||
|
SyncInventoryAvailable(row);
|
||||||
|
await StockBindInventoryRepository.UpdateAsync(row);
|
||||||
|
remaining -= take;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remaining > 0)
|
||||||
|
{
|
||||||
|
throw new BusinessException($"SKU {skuId} 扣减失败,冻结量不足");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EnsureNoDeductedBindsAsync(int orderId)
|
||||||
|
{
|
||||||
|
var hasDeducted = await OrderStockBindRepository.AnyAsync(x =>
|
||||||
|
x.OrderId == orderId && x.Status == OrderStockBindStatusCodes.Deducted);
|
||||||
|
if (hasDeducted)
|
||||||
|
{
|
||||||
|
throw new BusinessException("订单已确认扣减库存,不能改绑;如需调整请联系运营");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EnsureOrderCanBindStock(Order order)
|
||||||
|
{
|
||||||
|
if (string.Equals(order.OrderStatus, OrderStatusCodes.Deleted, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
throw new BusinessException("已删除订单不能绑定");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int RequireOrderCustomerId(Order order)
|
||||||
|
{
|
||||||
|
if (!order.CustomerId.HasValue || order.CustomerId.Value <= 0)
|
||||||
|
{
|
||||||
|
throw new BusinessException("订单未关联客户,无法绑定仓储资源");
|
||||||
|
}
|
||||||
|
|
||||||
|
return order.CustomerId.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ResolveBindWarehouseId(Order order, int? requestedWarehouseId)
|
||||||
|
{
|
||||||
|
if (requestedWarehouseId is > 0)
|
||||||
|
{
|
||||||
|
return requestedWarehouseId.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (order.WarehouseId is > 0)
|
||||||
|
{
|
||||||
|
return order.WarehouseId.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BusinessException("请先为订单指定仓库");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<OrderStockBindDetailOutput> BuildStockBindDetailAsync(Order order)
|
||||||
|
{
|
||||||
|
var binds = await OrderStockBindRepository.GetListAsync(x =>
|
||||||
|
x.OrderId == order.Id
|
||||||
|
&& (x.Status == OrderStockBindStatusCodes.Bound || x.Status == OrderStockBindStatusCodes.Deducted));
|
||||||
|
binds = binds.OrderBy(x => x.Id).ToList();
|
||||||
|
|
||||||
|
string? warehouseName = null;
|
||||||
|
var warehouseId = order.WarehouseId ?? binds.FirstOrDefault()?.WarehouseId;
|
||||||
|
if (warehouseId is > 0)
|
||||||
|
{
|
||||||
|
var warehouse = await WarehouseAddressRepository.FirstOrDefaultAsync(x => x.Id == warehouseId.Value);
|
||||||
|
warehouseName = warehouse?.WarehouseName;
|
||||||
|
}
|
||||||
|
|
||||||
|
var binIds = binds.Where(x => x.BinLocationId.HasValue).Select(x => x.BinLocationId!.Value).Distinct().ToArray();
|
||||||
|
var bins = binIds.Length == 0
|
||||||
|
? []
|
||||||
|
: await StockBindBinLocationRepository.GetListAsync(x => binIds.Contains(x.Id));
|
||||||
|
var binMap = bins.ToDictionary(x => x.Id);
|
||||||
|
|
||||||
|
var items = new List<OrderStockBindItemOutput>();
|
||||||
|
foreach (var bind in binds)
|
||||||
|
{
|
||||||
|
var availableQty = 0;
|
||||||
|
if (string.Equals(bind.BindType, OrderStockBindTypeCodes.Inventory, StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& bind.SkuId is > 0
|
||||||
|
&& warehouseId is > 0)
|
||||||
|
{
|
||||||
|
var invRows = await StockBindInventoryRepository.GetListAsync(x =>
|
||||||
|
x.CustomerId == bind.CustomerId
|
||||||
|
&& x.WarehouseId == warehouseId.Value
|
||||||
|
&& x.SkuId == bind.SkuId.Value);
|
||||||
|
availableQty = invRows.Sum(GetInventoryAvailableQty) + bind.BindQty;
|
||||||
|
}
|
||||||
|
else if (bind.ParcelItemId is > 0)
|
||||||
|
{
|
||||||
|
var parcelItem = await StockBindParcelItemRepository.FirstOrDefaultAsync(x => x.Id == bind.ParcelItemId.Value);
|
||||||
|
if (parcelItem != null)
|
||||||
|
{
|
||||||
|
availableQty = GetParcelItemAvailableQty(parcelItem) + bind.BindQty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binMap.TryGetValue(bind.BinLocationId ?? 0, out var bin);
|
||||||
|
items.Add(new OrderStockBindItemOutput
|
||||||
|
{
|
||||||
|
Id = bind.Id,
|
||||||
|
BindType = bind.BindType,
|
||||||
|
SkuId = bind.SkuId,
|
||||||
|
SkuCode = bind.SkuCode,
|
||||||
|
SkuName = bind.SkuName,
|
||||||
|
InventoryId = bind.InventoryId,
|
||||||
|
BinLocationId = bind.BinLocationId,
|
||||||
|
BinCode = bin?.BinCode,
|
||||||
|
ParcelId = bind.ParcelId,
|
||||||
|
ParcelItemId = bind.ParcelItemId,
|
||||||
|
TrackingNo = bind.TrackingNo,
|
||||||
|
ProductName = bind.ProductName,
|
||||||
|
AttributeText = bind.AttributeText,
|
||||||
|
ImageUrl = bind.ImageUrl,
|
||||||
|
BindQty = bind.BindQty,
|
||||||
|
AvailableQty = availableQty,
|
||||||
|
Status = bind.Status
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var bindStatus = "none";
|
||||||
|
if (binds.Any(x => x.Status == OrderStockBindStatusCodes.Deducted))
|
||||||
|
{
|
||||||
|
bindStatus = OrderStockBindStatusCodes.Deducted;
|
||||||
|
}
|
||||||
|
else if (binds.Any(x => x.Status == OrderStockBindStatusCodes.Bound))
|
||||||
|
{
|
||||||
|
bindStatus = OrderStockBindStatusCodes.Bound;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new OrderStockBindDetailOutput
|
||||||
|
{
|
||||||
|
OrderId = order.Id,
|
||||||
|
WarehouseId = warehouseId,
|
||||||
|
WarehouseName = warehouseName,
|
||||||
|
BindStatus = bindStatus,
|
||||||
|
Items = items
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetInventoryAvailableQty(CInventory inv) =>
|
||||||
|
inv.AvailableQty > 0 ? inv.AvailableQty : Math.Max(0, inv.Qty - inv.FrozenQty);
|
||||||
|
|
||||||
|
private static int GetParcelItemAvailableQty(CParcelItem item) =>
|
||||||
|
Math.Max(0, item.Quantity - item.FrozenQty);
|
||||||
|
|
||||||
|
private static void SyncInventoryAvailable(CInventory inv) =>
|
||||||
|
inv.AvailableQty = Math.Max(0, inv.Qty - inv.FrozenQty);
|
||||||
|
|
||||||
|
private static void SyncParcelItemAvailable(CParcelItem item) =>
|
||||||
|
item.AvailableQty = Math.Max(0, item.Quantity - item.FrozenQty);
|
||||||
|
}
|
||||||
@ -487,8 +487,8 @@ public class PlatformStoreOrderService : ApplicationService
|
|||||||
x.CustomerId == customerId
|
x.CustomerId == customerId
|
||||||
&& x.WarehouseId == warehouseId
|
&& x.WarehouseId == warehouseId
|
||||||
&& x.SkuId == skuId
|
&& x.SkuId == skuId
|
||||||
&& x.Qty > 0);
|
&& (x.AvailableQty > 0 || x.Qty - x.FrozenQty > 0));
|
||||||
var available = total.Sum(x => x.Qty);
|
var available = total.Sum(x => Math.Max(0, x.Qty - x.FrozenQty));
|
||||||
if (available < qty)
|
if (available < qty)
|
||||||
{
|
{
|
||||||
throw new BusinessException($"SKU {skuId} 库存不足,需要 {qty},可用 {available}");
|
throw new BusinessException($"SKU {skuId} 库存不足,需要 {qty},可用 {available}");
|
||||||
@ -526,7 +526,7 @@ public class PlatformStoreOrderService : ApplicationService
|
|||||||
x.CustomerId == customerId
|
x.CustomerId == customerId
|
||||||
&& x.WarehouseId == warehouseId
|
&& x.WarehouseId == warehouseId
|
||||||
&& x.SkuId == skuId
|
&& x.SkuId == skuId
|
||||||
&& x.Qty > 0);
|
&& (x.AvailableQty > 0 || x.Qty > x.FrozenQty));
|
||||||
rows = rows.OrderBy(x => x.BinLocationId).ToList();
|
rows = rows.OrderBy(x => x.BinLocationId).ToList();
|
||||||
|
|
||||||
var deductions = new List<InventoryDeduction>();
|
var deductions = new List<InventoryDeduction>();
|
||||||
@ -538,8 +538,15 @@ public class PlatformStoreOrderService : ApplicationService
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
var deduct = Math.Min(row.Qty, remaining);
|
var available = Math.Max(0, row.Qty - row.FrozenQty);
|
||||||
|
var deduct = Math.Min(available, remaining);
|
||||||
|
if (deduct <= 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
row.Qty -= deduct;
|
row.Qty -= deduct;
|
||||||
|
row.AvailableQty = Math.Max(0, row.Qty - row.FrozenQty);
|
||||||
remaining -= deduct;
|
remaining -= deduct;
|
||||||
await InventoryRepository.UpdateAsync(row);
|
await InventoryRepository.UpdateAsync(row);
|
||||||
deductions.Add(new InventoryDeduction(skuId, row.BinLocationId, deduct));
|
deductions.Add(new InventoryDeduction(skuId, row.BinLocationId, deduct));
|
||||||
@ -570,13 +577,16 @@ public class PlatformStoreOrderService : ApplicationService
|
|||||||
WarehouseId = warehouseId,
|
WarehouseId = warehouseId,
|
||||||
SkuId = item.SkuId,
|
SkuId = item.SkuId,
|
||||||
BinLocationId = item.BinLocationId,
|
BinLocationId = item.BinLocationId,
|
||||||
Qty = item.Qty
|
Qty = item.Qty,
|
||||||
|
FrozenQty = 0,
|
||||||
|
AvailableQty = item.Qty
|
||||||
};
|
};
|
||||||
await InventoryRepository.InsertAsync(inventory);
|
await InventoryRepository.InsertAsync(inventory);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
inventory.Qty += item.Qty;
|
inventory.Qty += item.Qty;
|
||||||
|
inventory.AvailableQty = Math.Max(0, inventory.Qty - inventory.FrozenQty);
|
||||||
await InventoryRepository.UpdateAsync(inventory);
|
await InventoryRepository.UpdateAsync(inventory);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -64,7 +64,9 @@ public partial class WarehouseService
|
|||||||
WarehouseId = warehouseId,
|
WarehouseId = warehouseId,
|
||||||
SkuId = skuId,
|
SkuId = skuId,
|
||||||
BinLocationId = binLocationId,
|
BinLocationId = binLocationId,
|
||||||
Qty = deltaQty
|
Qty = deltaQty,
|
||||||
|
FrozenQty = 0,
|
||||||
|
AvailableQty = deltaQty
|
||||||
};
|
};
|
||||||
await InventoryRepository.InsertAsync(inventory);
|
await InventoryRepository.InsertAsync(inventory);
|
||||||
return;
|
return;
|
||||||
@ -76,7 +78,13 @@ public partial class WarehouseService
|
|||||||
throw new BusinessException("库存不足");
|
throw new BusinessException("库存不足");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (deltaQty < 0 && inventory.AvailableQty < -deltaQty)
|
||||||
|
{
|
||||||
|
throw new BusinessException("库存可用不足");
|
||||||
|
}
|
||||||
|
|
||||||
inventory.Qty = newQty;
|
inventory.Qty = newQty;
|
||||||
|
inventory.AvailableQty = Math.Max(0, inventory.Qty - inventory.FrozenQty);
|
||||||
await InventoryRepository.UpdateAsync(inventory);
|
await InventoryRepository.UpdateAsync(inventory);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,7 +103,7 @@ public partial class WarehouseService
|
|||||||
x.CustomerId == customerId
|
x.CustomerId == customerId
|
||||||
&& x.WarehouseId == warehouseId
|
&& x.WarehouseId == warehouseId
|
||||||
&& x.SkuId == skuId
|
&& x.SkuId == skuId
|
||||||
&& x.Qty > 0);
|
&& x.AvailableQty > 0);
|
||||||
rows = rows.OrderBy(x => x.BinLocationId).ToList();
|
rows = rows.OrderBy(x => x.BinLocationId).ToList();
|
||||||
|
|
||||||
var remaining = requiredQty;
|
var remaining = requiredQty;
|
||||||
@ -106,8 +114,15 @@ public partial class WarehouseService
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
var deduct = Math.Min(row.Qty, remaining);
|
var available = Math.Max(0, row.Qty - row.FrozenQty);
|
||||||
|
var deduct = Math.Min(available, remaining);
|
||||||
|
if (deduct <= 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
row.Qty -= deduct;
|
row.Qty -= deduct;
|
||||||
|
row.AvailableQty = Math.Max(0, row.Qty - row.FrozenQty);
|
||||||
remaining -= deduct;
|
remaining -= deduct;
|
||||||
await InventoryRepository.UpdateAsync(row);
|
await InventoryRepository.UpdateAsync(row);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,7 @@ public partial class WarehouseService
|
|||||||
var warehouseIdValue = input.WarehouseId.GetValueOrDefault();
|
var warehouseIdValue = input.WarehouseId.GetValueOrDefault();
|
||||||
var inventories = await InventoryRepository.GetListAsync(x =>
|
var inventories = await InventoryRepository.GetListAsync(x =>
|
||||||
(!hasCustomerFilter || x.CustomerId == customerIdValue)
|
(!hasCustomerFilter || x.CustomerId == customerIdValue)
|
||||||
&& x.Qty > 0
|
&& x.AvailableQty > 0
|
||||||
&& (!hasWarehouseFilter || x.WarehouseId == warehouseIdValue));
|
&& (!hasWarehouseFilter || x.WarehouseId == warehouseIdValue));
|
||||||
|
|
||||||
var skuIds = inventories.Select(x => x.SkuId).Distinct().ToArray();
|
var skuIds = inventories.Select(x => x.SkuId).Distinct().ToArray();
|
||||||
@ -45,6 +45,8 @@ public partial class WarehouseService
|
|||||||
SkuName = sku?.SkuName ?? string.Empty,
|
SkuName = sku?.SkuName ?? string.Empty,
|
||||||
WarehouseId = g.Key.WarehouseId,
|
WarehouseId = g.Key.WarehouseId,
|
||||||
TotalQty = g.Sum(x => x.Qty),
|
TotalQty = g.Sum(x => x.Qty),
|
||||||
|
FrozenQty = g.Sum(x => x.FrozenQty),
|
||||||
|
AvailableQty = g.Sum(x => x.AvailableQty > 0 ? x.AvailableQty : Math.Max(0, x.Qty - x.FrozenQty)),
|
||||||
BinCount = g.Count()
|
BinCount = g.Count()
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@ -113,7 +115,9 @@ public partial class WarehouseService
|
|||||||
BinLocationId = x.BinLocationId,
|
BinLocationId = x.BinLocationId,
|
||||||
BinCode = bin?.BinCode ?? string.Empty,
|
BinCode = bin?.BinCode ?? string.Empty,
|
||||||
BinName = bin?.BinName,
|
BinName = bin?.BinName,
|
||||||
Qty = x.Qty
|
Qty = x.Qty,
|
||||||
|
FrozenQty = x.FrozenQty,
|
||||||
|
AvailableQty = x.AvailableQty > 0 ? x.AvailableQty : Math.Max(0, x.Qty - x.FrozenQty)
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.ToList()
|
.ToList()
|
||||||
|
|||||||
@ -176,6 +176,8 @@ public partial class WarehouseService
|
|||||||
: itemInput.AttributeText.Trim(),
|
: itemInput.AttributeText.Trim(),
|
||||||
ImageUrl = imageUrl,
|
ImageUrl = imageUrl,
|
||||||
Quantity = itemInput.Quantity,
|
Quantity = itemInput.Quantity,
|
||||||
|
FrozenQty = 0,
|
||||||
|
AvailableQty = itemInput.Quantity,
|
||||||
Remark = string.IsNullOrWhiteSpace(itemInput.Remark) ? null : itemInput.Remark.Trim()
|
Remark = string.IsNullOrWhiteSpace(itemInput.Remark) ? null : itemInput.Remark.Trim()
|
||||||
};
|
};
|
||||||
await ParcelItemRepository.InsertAsync(item);
|
await ParcelItemRepository.InsertAsync(item);
|
||||||
@ -309,6 +311,8 @@ public partial class WarehouseService
|
|||||||
AttributeText = item.AttributeText,
|
AttributeText = item.AttributeText,
|
||||||
ImageUrl = item.ImageUrl,
|
ImageUrl = item.ImageUrl,
|
||||||
Quantity = item.Quantity,
|
Quantity = item.Quantity,
|
||||||
|
FrozenQty = item.FrozenQty,
|
||||||
|
AvailableQty = item.AvailableQty > 0 ? item.AvailableQty : Math.Max(0, item.Quantity - item.FrozenQty),
|
||||||
SkuId = item.SkuId,
|
SkuId = item.SkuId,
|
||||||
SkuCode = item.SkuCode,
|
SkuCode = item.SkuCode,
|
||||||
BinLocationId = item.BinLocationId,
|
BinLocationId = item.BinLocationId,
|
||||||
|
|||||||
14
src/Oms.Domain/OrdersAggregate/Consts/OrderStockBindCodes.cs
Normal file
14
src/Oms.Domain/OrdersAggregate/Consts/OrderStockBindCodes.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
namespace Oms.Domain.OrdersAggregate.Consts;
|
||||||
|
|
||||||
|
public static class OrderStockBindTypeCodes
|
||||||
|
{
|
||||||
|
public const string Inventory = "inventory";
|
||||||
|
public const string ParcelItem = "parcelItem";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class OrderStockBindStatusCodes
|
||||||
|
{
|
||||||
|
public const string Bound = "bound";
|
||||||
|
public const string Deducted = "deducted";
|
||||||
|
public const string Released = "released";
|
||||||
|
}
|
||||||
56
src/Oms.Domain/OrdersAggregate/Entities/OrderStockBind.cs
Normal file
56
src/Oms.Domain/OrdersAggregate/Entities/OrderStockBind.cs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
using Oms.Domain.Shared;
|
||||||
|
using SqlSugar;
|
||||||
|
|
||||||
|
namespace Oms.Domain.OrdersAggregate.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 物流单仓储绑定明细(包裹行 / 库存)。
|
||||||
|
/// </summary>
|
||||||
|
[SugarTable("t_order_stock_bind")]
|
||||||
|
public class OrderStockBind : OmsAuditedAggregateRoot
|
||||||
|
{
|
||||||
|
public int OrderId { get; set; }
|
||||||
|
|
||||||
|
public int CustomerId { get; set; }
|
||||||
|
|
||||||
|
public int WarehouseId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>inventory / parcelItem</summary>
|
||||||
|
public string BindType { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public int? SkuId { get; set; }
|
||||||
|
|
||||||
|
public int? InventoryId { get; set; }
|
||||||
|
|
||||||
|
public int? BinLocationId { get; set; }
|
||||||
|
|
||||||
|
public int? ParcelId { get; set; }
|
||||||
|
|
||||||
|
public int? ParcelItemId { get; set; }
|
||||||
|
|
||||||
|
public int BindQty { get; set; }
|
||||||
|
|
||||||
|
/// <summary>bound / deducted / released</summary>
|
||||||
|
public string Status { get; set; } = "bound";
|
||||||
|
|
||||||
|
[SugarColumn(Length = 64, IsNullable = true)]
|
||||||
|
public string? SkuCode { get; set; }
|
||||||
|
|
||||||
|
[SugarColumn(Length = 128, IsNullable = true)]
|
||||||
|
public string? SkuName { get; set; }
|
||||||
|
|
||||||
|
[SugarColumn(Length = 64, IsNullable = true)]
|
||||||
|
public string? TrackingNo { get; set; }
|
||||||
|
|
||||||
|
[SugarColumn(Length = 256, IsNullable = true)]
|
||||||
|
public string? ProductName { get; set; }
|
||||||
|
|
||||||
|
[SugarColumn(Length = 512, IsNullable = true)]
|
||||||
|
public string? ImageUrl { get; set; }
|
||||||
|
|
||||||
|
[SugarColumn(Length = 256, IsNullable = true)]
|
||||||
|
public string? AttributeText { get; set; }
|
||||||
|
|
||||||
|
[SugarColumn(Length = 512, IsNullable = true)]
|
||||||
|
public string? Remark { get; set; }
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
using Fake.Domain.Repositories;
|
||||||
|
using Oms.Domain.OrdersAggregate.Entities;
|
||||||
|
|
||||||
|
namespace Oms.Domain.OrdersAggregate.Repositories;
|
||||||
|
|
||||||
|
public interface IOrderStockBindRepository : IRepository<OrderStockBind>;
|
||||||
@ -15,4 +15,10 @@ public class CInventory : OmsAuditedAggregateRoot
|
|||||||
public int BinLocationId { get; set; }
|
public int BinLocationId { get; set; }
|
||||||
|
|
||||||
public int Qty { get; set; }
|
public int Qty { get; set; }
|
||||||
|
|
||||||
|
/// <summary>已绑定未出库冻结量。</summary>
|
||||||
|
public int FrozenQty { get; set; }
|
||||||
|
|
||||||
|
/// <summary>可用 = Qty - FrozenQty。</summary>
|
||||||
|
public int AvailableQty { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,12 @@ public class CParcelItem : OmsAuditedAggregateRoot
|
|||||||
|
|
||||||
public int Quantity { get; set; }
|
public int Quantity { get; set; }
|
||||||
|
|
||||||
|
/// <summary>已被物流单占用(冻结)数量。</summary>
|
||||||
|
public int FrozenQty { get; set; }
|
||||||
|
|
||||||
|
/// <summary>可用 = Quantity - FrozenQty。</summary>
|
||||||
|
public int AvailableQty { get; set; }
|
||||||
|
|
||||||
public int? SkuId { get; set; }
|
public int? SkuId { get; set; }
|
||||||
|
|
||||||
public string? SkuCode { get; set; }
|
public string? SkuCode { get; set; }
|
||||||
|
|||||||
8
src/Oms.SqlSugarCore/Orders/OrderStockBindRepository.cs
Normal file
8
src/Oms.SqlSugarCore/Orders/OrderStockBindRepository.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
using Fake.DependencyInjection;
|
||||||
|
using Oms.Domain.OrdersAggregate.Entities;
|
||||||
|
using Oms.Domain.OrdersAggregate.Repositories;
|
||||||
|
using Oms.SqlSugarCore.Repositories;
|
||||||
|
|
||||||
|
namespace Oms.SqlSugarCore.Orders;
|
||||||
|
|
||||||
|
public class OrderStockBindRepository : OmsSqlSugarRepository<OrderStockBind>, IOrderStockBindRepository, IScopedDependency;
|
||||||
Loading…
Reference in New Issue
Block a user