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
|
{
|
/// <summary>
|
/// 货位信息服务实现类
|
/// </summary>
|
public partial class LocationInfoService : ServiceBase<Dt_LocationInfo, IRepository<Dt_LocationInfo>>, ILocationInfoService
|
{
|
private readonly IMapper _mapper;
|
private readonly IRepository<Dt_Task> _taskRepository;
|
private readonly IRepository<Dt_StockInfo> _stockInfoRepository;
|
private readonly IRecordService _recordService;
|
|
/// <summary>
|
/// 构造函数
|
/// </summary>
|
/// <param name="baseDal">基础数据访问对象</param>
|
/// <param name="taskRepository">任务仓储</param>
|
/// <param name="stockInfoRepository">库存信息仓储</param>
|
public LocationInfoService(
|
IRepository<Dt_LocationInfo> baseDal,
|
IRepository<Dt_Task> taskRepository,
|
IRepository<Dt_StockInfo> stockInfoRepository,
|
IMapper mapper,
|
IRecordService recordService) : base(baseDal)
|
{
|
_taskRepository = taskRepository;
|
_stockInfoRepository = stockInfoRepository;
|
_mapper = mapper;
|
_recordService = recordService;
|
}
|
|
/// <summary>
|
/// 获取货位信息仓储
|
/// </summary>
|
public IRepository<Dt_LocationInfo> Repository => BaseDal;
|
|
/// <summary>
|
/// 批量启用货位
|
/// </summary>
|
/// <param name="keys">货位主键数组</param>
|
/// <returns>操作结果</returns>
|
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();
|
}
|
|
/// <summary>
|
/// 批量禁用货位
|
/// </summary>
|
/// <param name="keys">货位主键数组</param>
|
/// <returns>操作结果</returns>
|
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();
|
}
|
|
/// <summary>
|
/// 单个启用货位
|
/// </summary>
|
/// <param name="key">货位主键</param>
|
/// <returns>操作结果</returns>
|
public WebResponseContent LocationEnableStatus(int key) => LocationEnableStatus(new[] { key });
|
|
/// <summary>
|
/// 单个禁用货位
|
/// </summary>
|
/// <param name="key">货位主键</param>
|
/// <returns>操作结果</returns>
|
public WebResponseContent LocationDisableStatus(int key) => LocationDisableStatus(new[] { key });
|
|
/// <summary>
|
/// 初始化货位
|
/// </summary>
|
/// <param name="dto">初始化货位数据传输对象</param>
|
/// <returns>操作结果</returns>
|
public WebResponseContent InitializationLocation(InitializationLocationDTO dto)
|
{
|
try
|
{
|
var (isValid, errorMsg, _) = ModelValidate.ValidateModelData(dto);
|
if (!isValid) return WebResponseContent.Instance.Error(errorMsg);
|
|
var locationInfos = new List<Dt_LocationInfo>();
|
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);
|
}
|
}
|
|
/// <summary>
|
/// 根据巷道获取空闲货位信息
|
/// </summary>
|
/// <param name="roadwayNo">巷道编号</param>
|
/// <returns>空闲货位信息,如果未找到则返回null</returns>
|
public async Task<Dt_LocationInfo?> GetLocationInfo(string roadwayNo)
|
{
|
var locations = await BaseDal.QueryDataAsync(x =>
|
x.EnableStatus == EnableStatusEnum.Normal.GetHashCode() &&
|
x.RoadwayNo == roadwayNo &&
|
x.LocationStatus == LocationStatusEnum.Free.GetHashCode());
|
|
return locations?
|
.OrderBy(x => x.Layer)
|
.ThenByDescending(x => x.Depth)
|
.ThenBy(x => x.Column)
|
.ThenBy(x => x.Row)
|
.FirstOrDefault();
|
}
|
|
/// <summary>
|
/// 根据巷道和货位编码获取空闲货位信息
|
/// </summary>
|
/// <param name="roadwayNo">巷道编号</param>
|
/// <param name="locationCode">货位编码</param>
|
/// <returns>货位信息,如果未找到则返回null</returns>
|
public async Task<Dt_LocationInfo?> GetLocationInfo(string roadwayNo, string locationCode)
|
{
|
return await BaseDal.QueryFirstAsync(x => x.RoadwayNo == roadwayNo && x.LocationCode == locationCode);
|
}
|
|
/// <summary>
|
/// 根据货位编码获取货位信息
|
/// </summary>
|
/// <param name="locationCode">货位编码</param>
|
/// <returns>货位信息</returns>
|
public async Task<Dt_LocationInfo> GetLocationInfoAsync(string locationCode)
|
{
|
return await BaseDal.QueryFirstAsync(x => x.LocationCode == locationCode);
|
}
|
|
/// <summary>
|
/// 更新货位信息
|
/// </summary>
|
/// <param name="locationInfo">货位信息对象</param>
|
/// <returns>更新是否成功</returns>
|
public async Task<bool> 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<Dt_LocationInfo> 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;
|
}
|
|
/// <summary>
|
/// 检查并生成移库任务或返回出库任务
|
/// </summary>
|
/// <param name="taskNum">任务号</param>
|
/// <returns>任务信息</returns>
|
public async Task<WebResponseContent> 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 私有方法
|
|
/// <summary>
|
/// 计算相对的库位ID
|
/// </summary>
|
/// <param name="locationInfo">货位信息</param>
|
/// <returns>相对的库位ID</returns>
|
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);
|
}
|
|
/// <summary>
|
/// 处理没有任务的库位情况
|
/// </summary>
|
/// <param name="newLocationID">新的库位ID</param>
|
/// <param name="outboundTask">出库任务</param>
|
/// <returns>生成的移库任务或原始出库任务</returns>
|
private async Task<Dt_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<Dt_LocationInfo>();
|
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()
|
};
|
}
|
|
/// <summary>
|
/// 检查货位是否需要移库
|
/// </summary>
|
/// <param name="location">货位信息</param>
|
/// <returns>是否需要移库</returns>
|
private static bool CheckForInternalTransfer(Dt_LocationInfo location)
|
{
|
return location.Depth == 2;
|
}
|
|
/// <summary>
|
/// 根据巷道获取二深位的空库位
|
/// </summary>
|
/// <param name="roadway">巷道编号</param>
|
/// <returns>货位对象</returns>
|
private async Task<Dt_LocationInfo> GetTransferLocationEmptyAsync(string roadway)
|
{
|
return await BaseDal.QueryFirstAsync(x => x.Depth == 2 && x.LocationStatus == LocationStatusEnum.Free.GetHashCode() && x.RoadwayNo == roadway);
|
}
|
|
/// <summary>
|
/// 计算深度
|
/// </summary>
|
/// <param name="row">行号</param>
|
/// <param name="maxRow">最大行数</param>
|
/// <param name="maxDepth">最大深度</param>
|
/// <param name="currentDepth">当前深度</param>
|
/// <returns>计算后的深度</returns>
|
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;
|
}
|
|
/// <summary>
|
/// 创建货位信息
|
/// </summary>
|
/// <param name="roadwayNo">巷道编号</param>
|
/// <param name="row">行号</param>
|
/// <param name="col">列号</param>
|
/// <param name="layer">层数</param>
|
/// <param name="depth">深度</param>
|
/// <returns>货位信息对象</returns>
|
private static Dt_LocationInfo CreateLocationInfo(string roadwayNo, int row, int col, int layer, int depth)
|
{
|
return new Dt_LocationInfo
|
{
|
WarehouseId = 0,
|
Row = row,
|
Column = col,
|
Layer = layer,
|
Depth = depth,
|
RoadwayNo = roadwayNo,
|
EnableStatus = EnableStatusEnum.Normal.GetHashCode(),
|
LocationStatus = LocationStatusEnum.Free.GetHashCode(),
|
LocationType = LocationTypeEnum.Undefined.GetHashCode(),
|
LocationCode = $"{row:D3}-{col:D3}-{layer:D3}",
|
LocationName = $"{roadwayNo}巷道{row:D3}行{col:D3}列{layer:D3}层{depth:D2}深"
|
};
|
}
|
|
#endregion 私有方法
|
}
|
}
|