diff --git a/sql/migrate_t_order_stock_bind.sql b/sql/migrate_t_order_stock_bind.sql new file mode 100644 index 0000000..4b5afd8 --- /dev/null +++ b/sql/migrate_t_order_stock_bind.sql @@ -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); diff --git a/src/Oms.Application/Dtos/Orders/OrderStockBindDtos.cs b/src/Oms.Application/Dtos/Orders/OrderStockBindDtos.cs new file mode 100644 index 0000000..0d02cde --- /dev/null +++ b/src/Oms.Application/Dtos/Orders/OrderStockBindDtos.cs @@ -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; } + + /// none / bound / deducted + public string BindStatus { get; set; } = "none"; + + public List 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; } + + /// 包裹候选:空/all=全部生命周期。 + 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 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 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; } +} diff --git a/src/Oms.Application/Dtos/Warehouse/WarehouseDtos.cs b/src/Oms.Application/Dtos/Warehouse/WarehouseDtos.cs index 490478e..f858314 100644 --- a/src/Oms.Application/Dtos/Warehouse/WarehouseDtos.cs +++ b/src/Oms.Application/Dtos/Warehouse/WarehouseDtos.cs @@ -354,6 +354,10 @@ public class WarehouseParcelItemOutput public int Quantity { get; set; } + public int FrozenQty { get; set; } + + public int AvailableQty { get; set; } + public int? SkuId { get; set; } public string? SkuCode { get; set; } @@ -447,6 +451,10 @@ public class WarehouseInventoryOutput public int TotalQty { get; set; } + public int FrozenQty { get; set; } + + public int AvailableQty { get; set; } + public int BinCount { get; set; } } @@ -461,6 +469,10 @@ public class WarehouseInventoryBinDetailOutput public string? BinName { get; set; } public int Qty { get; set; } + + public int FrozenQty { get; set; } + + public int AvailableQty { get; set; } } public class WarehouseInventoryRelocateInput diff --git a/src/Oms.Application/Services/Orders/OrderService.StockBind.cs b/src/Oms.Application/Services/Orders/OrderService.StockBind.cs new file mode 100644 index 0000000..d4bbf01 --- /dev/null +++ b/src/Oms.Application/Services/Orders/OrderService.StockBind.cs @@ -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(); + + private ICInventoryRepository StockBindInventoryRepository => + LazyServiceProvider.GetRequiredService(); + + private ICParcelRepository StockBindParcelRepository => + LazyServiceProvider.GetRequiredService(); + + private ICParcelItemRepository StockBindParcelItemRepository => + LazyServiceProvider.GetRequiredService(); + + private ICSkuRepository StockBindSkuRepository => + LazyServiceProvider.GetRequiredService(); + + private ICBinLocationRepository StockBindBinLocationRepository => + LazyServiceProvider.GetRequiredService(); + + /// + /// 查询物流单当前仓储绑定明细。 + /// + [HttpGet("/order/stock-bind/detail")] + public async Task GetStockBindDetailAsync([FromQuery] int orderId) + { + var order = await GetOrderOrThrowAsync(orderId); + return await BuildStockBindDetailAsync(order); + } + + /// + /// 库存候选(左侧库存 Tab)。 + /// + [HttpPost("/order/stock-bind/inventory-candidates")] + public async Task> 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 + { + Items = grouped.Skip(skip).Take(pageSize).ToList(), + Total = grouped.Count + }; + } + + /// + /// 包裹候选(左侧包裹 Tab,允许全部生命周期)。 + /// + [HttpPost("/order/stock-bind/parcel-candidates")] + public async Task> 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(); + 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 + { + Items = outputs, + Total = parcels.Count + }; + } + + /// + /// 保存绑定(全量覆盖 + 冻结)。 + /// + [HttpPost("/order/stock-bind/save")] + public async Task 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); + } + + /// + /// 清空绑定并释放冻结。 + /// + [HttpPost("/order/stock-bind/clear")] + public async Task ClearStockBindAsync([FromBody] OrderStockBindOrderInput input) + { + var order = await GetOrderOrThrowAsync(input.OrderId); + EnsureOrderCanBindStock(order); + await EnsureNoDeductedBindsAsync(order.Id); + await ReleaseBoundBindsAsync(order.Id); + return await BuildStockBindDetailAsync(order); + } + + /// + /// 确认扣减:将 bound 明细转为 deducted,扣减实物并释放冻结。 + /// + [HttpPost("/order/stock-bind/confirm-deduct")] + public async Task 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> FreezeInventoryAsync( + int customerId, + int warehouseId, + int skuId, + int requiredQty, + int? inventoryId) + { + List 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 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(); + 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); +} diff --git a/src/Oms.Application/Services/Orders/PlatformStoreOrderService.cs b/src/Oms.Application/Services/Orders/PlatformStoreOrderService.cs index 0433bf3..ae2df79 100644 --- a/src/Oms.Application/Services/Orders/PlatformStoreOrderService.cs +++ b/src/Oms.Application/Services/Orders/PlatformStoreOrderService.cs @@ -487,8 +487,8 @@ public class PlatformStoreOrderService : ApplicationService x.CustomerId == customerId && x.WarehouseId == warehouseId && x.SkuId == skuId - && x.Qty > 0); - var available = total.Sum(x => x.Qty); + && (x.AvailableQty > 0 || x.Qty - x.FrozenQty > 0)); + var available = total.Sum(x => Math.Max(0, x.Qty - x.FrozenQty)); if (available < qty) { throw new BusinessException($"SKU {skuId} 库存不足,需要 {qty},可用 {available}"); @@ -526,7 +526,7 @@ public class PlatformStoreOrderService : ApplicationService x.CustomerId == customerId && x.WarehouseId == warehouseId && x.SkuId == skuId - && x.Qty > 0); + && (x.AvailableQty > 0 || x.Qty > x.FrozenQty)); rows = rows.OrderBy(x => x.BinLocationId).ToList(); var deductions = new List(); @@ -538,8 +538,15 @@ public class PlatformStoreOrderService : ApplicationService 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.AvailableQty = Math.Max(0, row.Qty - row.FrozenQty); remaining -= deduct; await InventoryRepository.UpdateAsync(row); deductions.Add(new InventoryDeduction(skuId, row.BinLocationId, deduct)); @@ -570,13 +577,16 @@ public class PlatformStoreOrderService : ApplicationService WarehouseId = warehouseId, SkuId = item.SkuId, BinLocationId = item.BinLocationId, - Qty = item.Qty + Qty = item.Qty, + FrozenQty = 0, + AvailableQty = item.Qty }; await InventoryRepository.InsertAsync(inventory); continue; } inventory.Qty += item.Qty; + inventory.AvailableQty = Math.Max(0, inventory.Qty - inventory.FrozenQty); await InventoryRepository.UpdateAsync(inventory); } } diff --git a/src/Oms.Application/Services/Warehouse/WarehouseService.Helpers.cs b/src/Oms.Application/Services/Warehouse/WarehouseService.Helpers.cs index fd40d93..7b8869d 100644 --- a/src/Oms.Application/Services/Warehouse/WarehouseService.Helpers.cs +++ b/src/Oms.Application/Services/Warehouse/WarehouseService.Helpers.cs @@ -64,7 +64,9 @@ public partial class WarehouseService WarehouseId = warehouseId, SkuId = skuId, BinLocationId = binLocationId, - Qty = deltaQty + Qty = deltaQty, + FrozenQty = 0, + AvailableQty = deltaQty }; await InventoryRepository.InsertAsync(inventory); return; @@ -76,7 +78,13 @@ public partial class WarehouseService throw new BusinessException("库存不足"); } + if (deltaQty < 0 && inventory.AvailableQty < -deltaQty) + { + throw new BusinessException("库存可用不足"); + } + inventory.Qty = newQty; + inventory.AvailableQty = Math.Max(0, inventory.Qty - inventory.FrozenQty); await InventoryRepository.UpdateAsync(inventory); } @@ -95,7 +103,7 @@ public partial class WarehouseService x.CustomerId == customerId && x.WarehouseId == warehouseId && x.SkuId == skuId - && x.Qty > 0); + && x.AvailableQty > 0); rows = rows.OrderBy(x => x.BinLocationId).ToList(); var remaining = requiredQty; @@ -106,8 +114,15 @@ public partial class WarehouseService 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.AvailableQty = Math.Max(0, row.Qty - row.FrozenQty); remaining -= deduct; await InventoryRepository.UpdateAsync(row); } diff --git a/src/Oms.Application/Services/Warehouse/WarehouseService.Inventory.cs b/src/Oms.Application/Services/Warehouse/WarehouseService.Inventory.cs index b79bf54..152bd54 100644 --- a/src/Oms.Application/Services/Warehouse/WarehouseService.Inventory.cs +++ b/src/Oms.Application/Services/Warehouse/WarehouseService.Inventory.cs @@ -22,7 +22,7 @@ public partial class WarehouseService var warehouseIdValue = input.WarehouseId.GetValueOrDefault(); var inventories = await InventoryRepository.GetListAsync(x => (!hasCustomerFilter || x.CustomerId == customerIdValue) - && x.Qty > 0 + && x.AvailableQty > 0 && (!hasWarehouseFilter || x.WarehouseId == warehouseIdValue)); var skuIds = inventories.Select(x => x.SkuId).Distinct().ToArray(); @@ -45,6 +45,8 @@ public partial class WarehouseService SkuName = sku?.SkuName ?? string.Empty, WarehouseId = g.Key.WarehouseId, 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() }; }) @@ -113,7 +115,9 @@ public partial class WarehouseService BinLocationId = x.BinLocationId, BinCode = bin?.BinCode ?? string.Empty, 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() diff --git a/src/Oms.Application/Services/Warehouse/WarehouseService.Parcel.cs b/src/Oms.Application/Services/Warehouse/WarehouseService.Parcel.cs index 7549fde..fc1a776 100644 --- a/src/Oms.Application/Services/Warehouse/WarehouseService.Parcel.cs +++ b/src/Oms.Application/Services/Warehouse/WarehouseService.Parcel.cs @@ -176,6 +176,8 @@ public partial class WarehouseService : itemInput.AttributeText.Trim(), ImageUrl = imageUrl, Quantity = itemInput.Quantity, + FrozenQty = 0, + AvailableQty = itemInput.Quantity, Remark = string.IsNullOrWhiteSpace(itemInput.Remark) ? null : itemInput.Remark.Trim() }; await ParcelItemRepository.InsertAsync(item); @@ -309,6 +311,8 @@ public partial class WarehouseService AttributeText = item.AttributeText, ImageUrl = item.ImageUrl, Quantity = item.Quantity, + FrozenQty = item.FrozenQty, + AvailableQty = item.AvailableQty > 0 ? item.AvailableQty : Math.Max(0, item.Quantity - item.FrozenQty), SkuId = item.SkuId, SkuCode = item.SkuCode, BinLocationId = item.BinLocationId, diff --git a/src/Oms.Domain/OrdersAggregate/Consts/OrderStockBindCodes.cs b/src/Oms.Domain/OrdersAggregate/Consts/OrderStockBindCodes.cs new file mode 100644 index 0000000..cd4dfce --- /dev/null +++ b/src/Oms.Domain/OrdersAggregate/Consts/OrderStockBindCodes.cs @@ -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"; +} diff --git a/src/Oms.Domain/OrdersAggregate/Entities/OrderStockBind.cs b/src/Oms.Domain/OrdersAggregate/Entities/OrderStockBind.cs new file mode 100644 index 0000000..e086c66 --- /dev/null +++ b/src/Oms.Domain/OrdersAggregate/Entities/OrderStockBind.cs @@ -0,0 +1,56 @@ +using Oms.Domain.Shared; +using SqlSugar; + +namespace Oms.Domain.OrdersAggregate.Entities; + +/// +/// 物流单仓储绑定明细(包裹行 / 库存)。 +/// +[SugarTable("t_order_stock_bind")] +public class OrderStockBind : OmsAuditedAggregateRoot +{ + public int OrderId { get; set; } + + public int CustomerId { get; set; } + + public int WarehouseId { get; set; } + + /// inventory / parcelItem + 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; } + + /// bound / deducted / released + 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; } +} diff --git a/src/Oms.Domain/OrdersAggregate/Repositories/IOrderStockBindRepository.cs b/src/Oms.Domain/OrdersAggregate/Repositories/IOrderStockBindRepository.cs new file mode 100644 index 0000000..55f8303 --- /dev/null +++ b/src/Oms.Domain/OrdersAggregate/Repositories/IOrderStockBindRepository.cs @@ -0,0 +1,6 @@ +using Fake.Domain.Repositories; +using Oms.Domain.OrdersAggregate.Entities; + +namespace Oms.Domain.OrdersAggregate.Repositories; + +public interface IOrderStockBindRepository : IRepository; diff --git a/src/Oms.Domain/WarehouseAggregate/Entities/CInventory.cs b/src/Oms.Domain/WarehouseAggregate/Entities/CInventory.cs index 7bdd3de..91aa6d1 100644 --- a/src/Oms.Domain/WarehouseAggregate/Entities/CInventory.cs +++ b/src/Oms.Domain/WarehouseAggregate/Entities/CInventory.cs @@ -15,4 +15,10 @@ public class CInventory : OmsAuditedAggregateRoot public int BinLocationId { get; set; } public int Qty { get; set; } + + /// 已绑定未出库冻结量。 + public int FrozenQty { get; set; } + + /// 可用 = Qty - FrozenQty。 + public int AvailableQty { get; set; } } diff --git a/src/Oms.Domain/WarehouseAggregate/Entities/CParcelItem.cs b/src/Oms.Domain/WarehouseAggregate/Entities/CParcelItem.cs index 06e870a..0cb75e1 100644 --- a/src/Oms.Domain/WarehouseAggregate/Entities/CParcelItem.cs +++ b/src/Oms.Domain/WarehouseAggregate/Entities/CParcelItem.cs @@ -18,6 +18,12 @@ public class CParcelItem : OmsAuditedAggregateRoot public int Quantity { get; set; } + /// 已被物流单占用(冻结)数量。 + public int FrozenQty { get; set; } + + /// 可用 = Quantity - FrozenQty。 + public int AvailableQty { get; set; } + public int? SkuId { get; set; } public string? SkuCode { get; set; } diff --git a/src/Oms.SqlSugarCore/Orders/OrderStockBindRepository.cs b/src/Oms.SqlSugarCore/Orders/OrderStockBindRepository.cs new file mode 100644 index 0000000..f479c3f --- /dev/null +++ b/src/Oms.SqlSugarCore/Orders/OrderStockBindRepository.cs @@ -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, IOrderStockBindRepository, IScopedDependency;