using Mapster; using MapsterMapper; using WIDESEA_Common.CommonEnum; using WIDESEA_Common.LocationEnum; using WIDESEA_Common.StockEnum; using WIDESEA_Common.TaskEnum; using WIDESEA_Core; using WIDESEA_Core.BaseRepository; using WIDESEA_Core.BaseServices; using WIDESEA_Core.Utilities; using WIDESEA_DTO.Basic; using WIDESEA_DTO.Task; using WIDESEA_IBasicService; using WIDESEA_IRecordService; using WIDESEA_Model.Models; namespace WIDESEA_BasicService { /// /// 货位信息服务实现类 /// public partial class LocationInfoService : ServiceBase>, ILocationInfoService { private readonly IMapper _mapper; private readonly IRepository _taskRepository; private readonly IRepository _stockInfoRepository; private readonly IRecordService _recordService; private readonly IRepository _warehouseRepository; /// /// 构造函数 /// /// 基础数据访问对象 /// 任务仓储 /// 库存信息仓储 public LocationInfoService( IRepository baseDal, IRepository taskRepository, IRepository stockInfoRepository, IRepository warehouseRepository, IMapper mapper, IRecordService recordService) : base(baseDal) { _taskRepository = taskRepository; _stockInfoRepository = stockInfoRepository; _mapper = mapper; _recordService = recordService; _warehouseRepository = warehouseRepository; } /// /// 获取货位信息仓储 /// public IRepository Repository => BaseDal; /// /// 批量启用货位 /// /// 货位主键数组 /// 操作结果 public WebResponseContent LocationEnableStatus(int[] keys) { var locationInfos = Repository.QueryData(x => keys.Contains(x.Id)); locationInfos.ForEach(x => x.EnableStatus = EnableStatusEnum.Normal.GetHashCode()); Repository.UpdateData(locationInfos); return WebResponseContent.Instance.OK(); } /// /// 批量禁用货位 /// /// 货位主键数组 /// 操作结果 public WebResponseContent LocationDisableStatus(int[] keys) { var locationInfos = Repository.QueryData(x => keys.Contains(x.Id)); locationInfos.ForEach(x => x.EnableStatus = EnableStatusEnum.Disable.GetHashCode()); Repository.UpdateData(locationInfos); return WebResponseContent.Instance.OK(); } /// /// 单个启用货位 /// /// 货位主键 /// 操作结果 public WebResponseContent LocationEnableStatus(int key) => LocationEnableStatus(new[] { key }); /// /// 单个禁用货位 /// /// 货位主键 /// 操作结果 public WebResponseContent LocationDisableStatus(int key) => LocationDisableStatus(new[] { key }); /// /// 初始化货位 /// /// 初始化货位数据传输对象 /// 操作结果 public WebResponseContent InitializationLocation(InitializationLocationDTO dto) { try { var (isValid, errorMsg, _) = ModelValidate.ValidateModelData(dto); if (!isValid) return WebResponseContent.Instance.Error(errorMsg); var locationInfos = new List(); int depth = dto.Depth; for (int row = 1; row <= dto.MaxRow; row++) { depth = CalculateDepth(row, dto.MaxRow, dto.Depth, depth); for (int col = 1; col <= dto.MaxColumn; col++) { for (int layer = 1; layer <= dto.MaxLayer; layer++) { var location = CreateLocationInfo(dto.Roadway, row, col, layer, depth); locationInfos.Add(location); } } } BaseDal.AddData(locationInfos); return WebResponseContent.Instance.OK(); } catch (Exception ex) { return WebResponseContent.Instance.Error(ex.Message); } } /// /// 根据巷道获取空闲货位信息 /// /// 巷道编号 /// 空闲货位信息,如果未找到则返回null public async Task GetLocationInfo(string roadwayNo) { var locations = await BaseDal.QueryDataAsync(x => x.EnableStatus == EnableStatusEnum.Normal.GetHashCode() && x.RoadwayNo == roadwayNo && x.LocationStatus == LocationStatusEnum.Free.GetHashCode()); return locations? .OrderByDescending(x => x.Depth) // 1. 深度优先(从大到小) .ThenBy(x => x.Layer) // 2. 层数 .ThenBy(x => x.Column) // 3. 列 .ThenBy(x => x.Row) // 4. 行 .FirstOrDefault(); } /// /// 根据巷道和货位编码获取空闲货位信息 /// /// 巷道编号 /// 货位编码 /// 货位信息,如果未找到则返回null public async Task GetLocationInfo(string roadwayNo, string locationCode) { return await BaseDal.QueryFirstAsync(x => x.RoadwayNo == roadwayNo && x.LocationCode == locationCode); } /// /// 根据货位编码获取货位信息 /// /// 货位编码 /// 货位信息 public async Task GetLocationInfoAsync(string locationCode) { return await BaseDal.QueryFirstAsync(x => x.LocationCode == locationCode); } /// /// 根据货位ID获取货位信息 /// /// 货位id /// 货位信息 public async Task GetLocationInfoAsync(int id) { return await BaseDal.QueryFirstAsync(x => x.Id == id); } /// /// 更新货位信息 /// /// 货位信息对象 /// 更新是否成功 public async Task UpdateLocationInfoAsync(Dt_LocationInfo locationInfo) { var beforeLocation = await BaseDal.QueryFirstAsync(x => x.Id == locationInfo.Id); var result = await BaseDal.UpdateDataAsync(locationInfo); if (!result) return false; return beforeLocation == null || await _recordService.AddLocationChangeRecordAsync(beforeLocation, locationInfo, LocationChangeType.HandUpdate, remark: "货位更新"); } public override WebResponseContent UpdateData(Dt_LocationInfo entity) { var beforeLocation = BaseDal.QueryFirst(x => x.Id == entity.Id); var result = base.UpdateData(entity); if (!result.Status || beforeLocation == null) return result; var saveRecordResult = _recordService.AddLocationChangeRecordAsync(beforeLocation, entity, LocationChangeType.HandUpdate, remark: "货位更新").GetAwaiter().GetResult(); return saveRecordResult ? result : WebResponseContent.Instance.Error("货位状态变更记录保存失败"); } public override WebResponseContent UpdateData(List entities) { var beforeLocations = entities .Select(entity => BaseDal.QueryFirst(x => x.Id == entity.Id)) .Where(location => location != null) .ToDictionary(location => location!.Id, location => location!); var result = base.UpdateData(entities); if (!result.Status) return result; foreach (var entity in entities) { if (!beforeLocations.TryGetValue(entity.Id, out var beforeLocation)) continue; var saveRecordResult = _recordService.AddLocationChangeRecordAsync(beforeLocation, entity, LocationChangeType.HandUpdate, remark: "批量货位更新").GetAwaiter().GetResult(); if (!saveRecordResult) return WebResponseContent.Instance.Error("货位状态变更记录保存失败"); } return result; } /// /// 检查并生成移库任务或返回出库任务 /// /// 任务号 /// 任务信息 public async Task TransferCheckAsync(int taskNum) { var content = new WebResponseContent(); try { // 根据任务号获取任务 var outboundTask = await _taskRepository.QueryFirstAsync(x => x.TaskNum == taskNum); if (outboundTask == null) return content.Error("任务不存在"); var location = await BaseDal.QueryFirstAsync(x => x.LocationCode == outboundTask.SourceAddress && x.RoadwayNo == outboundTask.Roadway); // 检查是否需要进行移库 if (CheckForInternalTransfer(location)) { // 计算对应位置的相对库位(奇数行的下一行或者偶数行的上一行) var newLocationID = GetRelativeLocationID(location); // 获取新的库位的任务 var internalTransferTask = await _taskRepository.QueryFirstAsync(x => x.SourceAddress == newLocationID && x.Roadway == outboundTask.Roadway); // 如果新的库位没有找到对应的任务 if (internalTransferTask == null) { return content.OK("获取到移库任务", await HandleNoTaskAtLocation(newLocationID, outboundTask)); } // 直接返回一深位出库任务 return content.OK("获取到一深位出库任务", internalTransferTask); } // 返回当前库位的出库任务 return content.OK("当前出库任务", outboundTask); } catch (Exception ex) { return content.Error($"发生错误:{ex.Message}"); } } #region 私有方法 /// /// 计算相对的库位ID /// /// 货位信息 /// 相对的库位ID private static string GetRelativeLocationID(Dt_LocationInfo locationInfo) { int line = locationInfo.Row; // 计算相对的货位行值,奇数行的下一行或者偶数行的上一行 int relativeLine = line % 2 == 1 ? line + 1 : line - 1; // 构建新的库位ID string[] newLocationParts = new string[] { relativeLine.ToString().PadLeft(3, '0'), locationInfo.Column.ToString().PadLeft(3, '0'), locationInfo.Layer.ToString().PadLeft(3, '0') }; return string.Join("-", newLocationParts); } /// /// 处理没有任务的库位情况 /// /// 新的库位ID /// 出库任务 /// 生成的移库任务或原始出库任务 private async Task HandleNoTaskAtLocation(string newLocationID, Dt_Task outboundTask) { // 判断该位置是否有库存 var stockInfo = await _stockInfoRepository.QueryDataNavFirstAsync(x => x.LocationCode == newLocationID && x.StockStatus == StockStatusEmun.入库完成.GetHashCode() && x.LocationDetails.LocationStatus == LocationStatusEnum.InStock.GetHashCode()); if (stockInfo == null) { // 如果没有库存,直接返回当前出库任务 return outboundTask; } // 如果有库存,生成移库任务 var emptyLocation = await GetTransferLocationEmptyAsync(outboundTask.Roadway); var taskNo = await _taskRepository.GetTaskNo(); var newTransferTask = new Dt_Task { CreateDate = DateTime.Now, Creater = App.User.UserName ?? "system", CurrentAddress = newLocationID, Grade = 99, NextAddress = emptyLocation.LocationCode, PalletCode = stockInfo.PalletCode, Remark = "移库", Roadway = stockInfo.LocationDetails.RoadwayNo, SourceAddress = newLocationID, TaskNum = taskNo, TargetAddress = emptyLocation.LocationCode, TaskType = TaskRelocationTypeEnum.Relocation.GetHashCode(), TaskStatus = TaskRelocationStatusEnum.RelocationNew.GetHashCode(), WarehouseId = stockInfo.WarehouseId }; var createdTask = await _taskRepository.Db.Insertable(newTransferTask).ExecuteReturnEntityAsync(); var beforeStock = CloneStockSnapshot(stockInfo); var beforeSourceLocation = stockInfo.LocationDetails == null ? null : CloneLocationSnapshot(stockInfo.LocationDetails); var beforeTargetLocation = CloneLocationSnapshot(emptyLocation); // 创建移库任务后,立即锁定库存和相关货位,避免并发重复分配 stockInfo.StockStatus = StockStatusEmun.移库锁定.GetHashCode(); var updateStockResult = await _stockInfoRepository.UpdateDataAsync(stockInfo); var locationsToUpdate = new List(); if (stockInfo.LocationDetails != null) { stockInfo.LocationDetails.LocationStatus = LocationStatusEnum.InStockLock.GetHashCode(); locationsToUpdate.Add(stockInfo.LocationDetails); } emptyLocation.LocationStatus = LocationStatusEnum.FreeLock.GetHashCode(); locationsToUpdate.Add(emptyLocation); var updateLocationResult = await BaseDal.UpdateDataAsync(locationsToUpdate); if (!updateStockResult || !updateLocationResult) { throw new Exception("创建移库任务后更新库存状态或货位状态失败"); } var saveStockRecordResult = await _recordService.AddStockChangeRecordAsync( beforeStock, stockInfo, StockChangeTypeEnum.Relocation, createdTask.TaskNum, createdTask.OrderNo, "移库任务预占库存"); if (!saveStockRecordResult) { throw new Exception("创建移库任务后记录库存变更失败"); } if (beforeSourceLocation != null && stockInfo.LocationDetails != null) { var saveSourceLocationRecordResult = await _recordService.AddLocationChangeRecordAsync( beforeSourceLocation, stockInfo.LocationDetails, LocationChangeType.RelocationAssignLocation, createdTask.TaskNum, createdTask.OrderNo, null, "移库任务锁定源货位"); if (!saveSourceLocationRecordResult) { throw new Exception("创建移库任务后记录源货位变更失败"); } } var saveTargetLocationRecordResult = await _recordService.AddLocationChangeRecordAsync( beforeTargetLocation, emptyLocation, LocationChangeType.RelocationAssignLocation, createdTask.TaskNum, createdTask.OrderNo, null, "移库任务锁定目标货位"); if (!saveTargetLocationRecordResult) { throw new Exception("创建移库任务后记录目标货位变更失败"); } return createdTask; } private static Dt_LocationInfo CloneLocationSnapshot(Dt_LocationInfo location) { return new Dt_LocationInfo { Id = location.Id, WarehouseId = location.WarehouseId, LocationCode = location.LocationCode, LocationName = location.LocationName, RoadwayNo = location.RoadwayNo, Row = location.Row, Column = location.Column, Layer = location.Layer, Depth = location.Depth, LocationType = location.LocationType, LocationStatus = location.LocationStatus, EnableStatus = location.EnableStatus, Remark = location.Remark }; } private static Dt_StockInfo CloneStockSnapshot(Dt_StockInfo stockInfo) { return new Dt_StockInfo { Id = stockInfo.Id, PalletCode = stockInfo.PalletCode, PalletType = stockInfo.PalletType, LocationId = stockInfo.LocationId, LocationCode = stockInfo.LocationCode, WarehouseId = stockInfo.WarehouseId, StockStatus = stockInfo.StockStatus, Remark = stockInfo.Remark, OutboundDate = stockInfo.OutboundDate, Details = stockInfo.Details?.Select(detail => new Dt_StockInfoDetail { Id = detail.Id, StockId = detail.StockId, MaterielCode = detail.MaterielCode, MaterielName = detail.MaterielName, OrderNo = detail.OrderNo, BatchNo = detail.BatchNo, ProductionDate = detail.ProductionDate, EffectiveDate = detail.EffectiveDate, SerialNumber = detail.SerialNumber, StockQuantity = detail.StockQuantity, OutboundQuantity = detail.OutboundQuantity, Status = detail.Status, Unit = detail.Unit, InboundOrderRowNo = detail.InboundOrderRowNo, Remark = detail.Remark }).ToList() }; } /// /// 检查货位是否需要移库 /// /// 货位信息 /// 是否需要移库 private static bool CheckForInternalTransfer(Dt_LocationInfo location) { return location.Depth == 2; } /// /// 根据巷道获取二深位的空库位 /// /// 巷道编号 /// 货位对象 private async Task GetTransferLocationEmptyAsync(string roadway) { return await BaseDal.QueryFirstAsync(x => x.Depth == 2 && x.LocationStatus == LocationStatusEnum.Free.GetHashCode() && x.RoadwayNo == roadway); } /// /// 计算深度 /// /// 行号 /// 最大行数 /// 最大深度 /// 当前深度 /// 计算后的深度 private static int CalculateDepth(int row, int maxRow, int maxDepth, int currentDepth) { int mod = row % maxRow; if (mod == 1) return maxDepth; if (mod == maxDepth + 1) return 1; if (mod > 1 && mod <= maxDepth) return currentDepth - 1; return currentDepth + 1; } /// /// 创建货位信息 /// /// 巷道编号 /// 行号 /// 列号 /// 层数 /// 深度 /// 货位信息对象 private Dt_LocationInfo CreateLocationInfo(string roadwayNo, int row, int col, int layer, int depth) { var warehouse = _warehouseRepository.QueryData(x => x.WarehouseCode == roadwayNo).FirstOrDefault(); if (warehouse == null) { throw new InvalidOperationException($"未找到巷道编号为 {roadwayNo} 的仓库信息"); } return new Dt_LocationInfo { WarehouseId = warehouse.WarehouseId, Row = row, Column = col, Layer = layer, Depth = depth, RoadwayNo = roadwayNo, EnableStatus = EnableStatusEnum.Normal.GetHashCode(), LocationStatus = LocationStatusEnum.Free.GetHashCode(), LocationType = LocationTypeEnum.Undefined.GetHashCode(), //LocationCode = $"{roadwayNo}-{row:D3}-{col:D3}-{layer:D3}", LocationCode = $"{row:D3}-{col:D3}-{layer:D3}", LocationName = $"{roadwayNo}巷道{row:D3}行{col:D3}列{layer:D3}层{depth:D2}深" }; } #endregion 私有方法 } }