|
|
|
|
|
using Sln.Wcs.Business.Domain.Enum;
|
|
|
|
|
|
using Sln.Wcs.Model.Domain;
|
|
|
|
|
|
using Sln.Wcs.Serilog;
|
|
|
|
|
|
|
|
|
|
|
|
namespace Sln.Wcs.Business;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 接驳位拓扑驱动的任务创建服务
|
|
|
|
|
|
/// 根据起止楼栋/楼层/位置,自动生成 LiveTaskQueue 及其子任务明细序列
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public class TaskCreateService
|
|
|
|
|
|
{
|
|
|
|
|
|
private readonly SerilogHelper _logger;
|
|
|
|
|
|
|
|
|
|
|
|
public TaskCreateService(SerilogHelper logger) => _logger = logger;
|
|
|
|
|
|
|
|
|
|
|
|
#region 拓扑配置
|
|
|
|
|
|
|
|
|
|
|
|
private record TopoNode(int Building, int Floor);
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 拓扑邻接表:(栋,层) → 可达的相邻节点
|
|
|
|
|
|
/// 所有楼栋 1~5F,本栋提升机可直达任意楼层(全连接)
|
|
|
|
|
|
/// 跨栋仅 2F 连通: (13,2)↔(14,2) 廊桥交接, (14,2)↔(15,2) AGV直连
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private static readonly Dictionary<TopoNode, List<TopoNode>> Topology = BuildTopology();
|
|
|
|
|
|
|
|
|
|
|
|
private static Dictionary<TopoNode, List<TopoNode>> BuildTopology()
|
|
|
|
|
|
{
|
|
|
|
|
|
var dict = new Dictionary<TopoNode, List<TopoNode>>();
|
|
|
|
|
|
|
|
|
|
|
|
// 每个楼栋内全连接(提升机直达任意楼层)
|
|
|
|
|
|
foreach (var building in new[] { 13, 14, 15 })
|
|
|
|
|
|
{
|
|
|
|
|
|
for (int f1 = 1; f1 <= 5; f1++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var neighbors = new List<TopoNode>();
|
|
|
|
|
|
for (int f2 = 1; f2 <= 5; f2++)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (f1 != f2)
|
|
|
|
|
|
neighbors.Add(new(building, f2));
|
|
|
|
|
|
}
|
|
|
|
|
|
dict[new(building, f1)] = neighbors;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 跨栋: 仅 2F 连通
|
|
|
|
|
|
dict[new(13, 2)].Add(new(14, 2)); // 13↔14 廊桥交接
|
|
|
|
|
|
dict[new(14, 2)].Add(new(13, 2));
|
|
|
|
|
|
dict[new(14, 2)].Add(new(15, 2)); // 14↔15 AGV直连
|
|
|
|
|
|
dict[new(15, 2)].Add(new(14, 2));
|
|
|
|
|
|
|
|
|
|
|
|
return dict;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 创建任务:输入楼栋/楼层/位置号,自动生成明细序列
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="startBuilding">起始楼栋 (13/14/15)</param>
|
|
|
|
|
|
/// <param name="startFloor">起始楼层</param>
|
|
|
|
|
|
/// <param name="startLocation">起始位置号,如 "01"</param>
|
|
|
|
|
|
/// <param name="endBuilding">目标楼栋 (13/14/15)</param>
|
|
|
|
|
|
/// <param name="endFloor">目标楼层</param>
|
|
|
|
|
|
/// <param name="endLocation">目标位置号,如 "02"</param>
|
|
|
|
|
|
/// <param name="taskType">任务类型: 1-入库, 2-出库</param>
|
|
|
|
|
|
/// <param name="taskCategory">任务类别: 1-包材, 2-成品, 3-托盘</param>
|
|
|
|
|
|
/// <param name="palletBarcode">托盘条码</param>
|
|
|
|
|
|
/// <param name="materialCode">物料编码</param>
|
|
|
|
|
|
/// <returns>完整 LiveTaskQueue(含明细列表)</returns>
|
|
|
|
|
|
public LiveTaskQueue CreateTask(
|
|
|
|
|
|
int startBuilding, int startFloor, string startLocation,
|
|
|
|
|
|
int endBuilding, int endFloor, string endLocation,
|
|
|
|
|
|
int taskType, int taskCategory,
|
|
|
|
|
|
string palletBarcode, string materialCode)
|
|
|
|
|
|
{
|
|
|
|
|
|
var startPoint = PosCode(startBuilding, startFloor, startLocation);
|
|
|
|
|
|
var endPoint = PosCode(endBuilding, endFloor, endLocation);
|
|
|
|
|
|
|
|
|
|
|
|
_logger.Info($"创建任务 - {startPoint} ({startBuilding}#_{startFloor}F) → {endPoint} ({endBuilding}#_{endFloor}F)");
|
|
|
|
|
|
|
|
|
|
|
|
// Step 1: BFS 找最短路径(节点序列)
|
|
|
|
|
|
var nodes = FindPath(startBuilding, startFloor, endBuilding, endFloor);
|
|
|
|
|
|
_logger.Info($"路由节点: {string.Join(" → ", nodes.Select(n => $"({n.Building},{n.Floor})"))}");
|
|
|
|
|
|
|
|
|
|
|
|
// Step 2: 沿节点路径逐段生成明细
|
|
|
|
|
|
// 规则: 同层移动=AGV, 跨层=提升机, 跨栋(13↔14)=廊桥交接两段AGV
|
|
|
|
|
|
var details = new List<LiveTaskDetail>();
|
|
|
|
|
|
int seq = 1;
|
|
|
|
|
|
var taskCode = GenerateTaskCode();
|
|
|
|
|
|
string currentPos = startPoint; // 当前货物所在接驳位
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < nodes.Count - 1; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var (b1, f1) = nodes[i];
|
|
|
|
|
|
var (b2, f2) = nodes[i + 1];
|
|
|
|
|
|
|
|
|
|
|
|
if (b1 != b2) // 跨栋
|
|
|
|
|
|
{
|
|
|
|
|
|
if (IsBridgeRequired(b1, b2))
|
|
|
|
|
|
{
|
|
|
|
|
|
// 13↔14: 廊桥交接 — 13# AGV 送到廊桥,等待 14# AGV
|
|
|
|
|
|
details.Add(NewDetail(taskCode, palletBarcode, materialCode,
|
|
|
|
|
|
taskType, taskCategory, seq++, currentPos, "BRIDGE_13_14", deviceType: 1));
|
|
|
|
|
|
currentPos = "BRIDGE_13_14";
|
|
|
|
|
|
}
|
|
|
|
|
|
// 14↔15: 14# AGV 可直接驶入 15#,不生成过渡段,由下一步直达
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (f1 != f2) // 同栋跨层 → AGV到提升机入口 + 提升机
|
|
|
|
|
|
{
|
|
|
|
|
|
var hoistEntry = HoistPoint(b1, f1);
|
|
|
|
|
|
var hoistExit = HoistPoint(b2, f2);
|
|
|
|
|
|
|
|
|
|
|
|
details.Add(NewDetail(taskCode, palletBarcode, materialCode,
|
|
|
|
|
|
taskType, taskCategory, seq++, currentPos, hoistEntry, deviceType: 1));
|
|
|
|
|
|
details.Add(NewDetail(taskCode, palletBarcode, materialCode,
|
|
|
|
|
|
taskType, taskCategory, seq++, hoistEntry, hoistExit, deviceType: 2));
|
|
|
|
|
|
|
|
|
|
|
|
currentPos = hoistExit;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Step 3: 最终 AGV 送达终点(同栋同层时直接一条 AGV)
|
|
|
|
|
|
if (details.Count == 0 || currentPos != endPoint)
|
|
|
|
|
|
{
|
|
|
|
|
|
details.Add(NewDetail(taskCode, palletBarcode, materialCode,
|
|
|
|
|
|
taskType, taskCategory, seq, currentPos, endPoint, deviceType: 1));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_logger.Info($"生成 {details.Count} 条明细");
|
|
|
|
|
|
|
|
|
|
|
|
return new LiveTaskQueue
|
|
|
|
|
|
{
|
|
|
|
|
|
taskCode = taskCode,
|
|
|
|
|
|
taskType = taskType,
|
|
|
|
|
|
taskCategory = taskCategory,
|
|
|
|
|
|
taskStatus = 1,
|
|
|
|
|
|
startPoint = startPoint,
|
|
|
|
|
|
endPoint = endPoint,
|
|
|
|
|
|
palletBarcode = palletBarcode,
|
|
|
|
|
|
materialCode = materialCode,
|
|
|
|
|
|
taskSteps = details.Count,
|
|
|
|
|
|
taskDetails = details,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#region 内部方法
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>BFS 最短路径</summary>
|
|
|
|
|
|
private static List<TopoNode> FindPath(int b_s, int f_s, int b_e, int f_e)
|
|
|
|
|
|
{
|
|
|
|
|
|
var start = new TopoNode(b_s, f_s);
|
|
|
|
|
|
var end = new TopoNode(b_e, f_e);
|
|
|
|
|
|
if (start == end) return new List<TopoNode> { start };
|
|
|
|
|
|
|
|
|
|
|
|
// 校验起止节点是否在拓扑中
|
|
|
|
|
|
if (!Topology.ContainsKey(start))
|
|
|
|
|
|
throw new InvalidOperationException($"起始位置不在拓扑中: {b_s}#_{f_s}F,可用节点: {string.Join(", ", Topology.Keys.Select(n => $"{n.Building}#_{n.Floor}F"))}");
|
|
|
|
|
|
if (!Topology.ContainsKey(end))
|
|
|
|
|
|
throw new InvalidOperationException($"目标位置不在拓扑中: {b_e}#_{f_e}F,可用节点: {string.Join(", ", Topology.Keys.Select(n => $"{n.Building}#_{n.Floor}F"))}");
|
|
|
|
|
|
|
|
|
|
|
|
var visited = new HashSet<TopoNode> { start };
|
|
|
|
|
|
var queue = new Queue<List<TopoNode>>();
|
|
|
|
|
|
queue.Enqueue(new List<TopoNode> { start });
|
|
|
|
|
|
|
|
|
|
|
|
while (queue.Count > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
var path = queue.Dequeue();
|
|
|
|
|
|
var current = path[^1];
|
|
|
|
|
|
|
|
|
|
|
|
if (!Topology.TryGetValue(current, out var neighbors)) continue;
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var next in neighbors)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (visited.Contains(next)) continue;
|
|
|
|
|
|
var newPath = new List<TopoNode>(path) { next };
|
|
|
|
|
|
if (next == end) return newPath;
|
|
|
|
|
|
visited.Add(next);
|
|
|
|
|
|
queue.Enqueue(newPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
throw new InvalidOperationException($"无路径: ({b_s},{f_s}) → ({b_e},{f_e})");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>是否需要廊桥交接</summary>
|
|
|
|
|
|
private static bool IsBridgeRequired(int b1, int b2) =>
|
|
|
|
|
|
(b1 == 13 && b2 == 14) || (b1 == 14 && b2 == 13);
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>拼接位置编码: 楼栋#_L楼层_位置号</summary>
|
|
|
|
|
|
private static string PosCode(int building, int floor, string location) =>
|
|
|
|
|
|
$"{building}#_L{floor}_{location}";
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>提升机接驳位</summary>
|
|
|
|
|
|
private static string HoistPoint(int building, int floor) =>
|
|
|
|
|
|
$"{building}#_L{floor}_HOIST";
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>普通接驳位(非提升机、非廊桥)</summary>
|
|
|
|
|
|
private static string DockingPoint(int building, int floor) =>
|
|
|
|
|
|
$"{building}#_L{floor}_01";
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>生成任务编号</summary>
|
|
|
|
|
|
private static string GenerateTaskCode() =>
|
|
|
|
|
|
DateTime.Now.ToString("yyyyMMddHHmmss") + new Random().Next(10, 99);
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>创建一条 LiveTaskDetail</summary>
|
|
|
|
|
|
private static LiveTaskDetail NewDetail(
|
|
|
|
|
|
string taskCode, string palletBarcode, string materialCode,
|
|
|
|
|
|
int taskType, int taskCategory, int seq,
|
|
|
|
|
|
string startPoint, string endPoint, int deviceType) =>
|
|
|
|
|
|
new()
|
|
|
|
|
|
{
|
|
|
|
|
|
taskCode = taskCode,
|
|
|
|
|
|
objId = seq,
|
|
|
|
|
|
palletBarcode = palletBarcode,
|
|
|
|
|
|
materialCode = materialCode,
|
|
|
|
|
|
startPoint = startPoint,
|
|
|
|
|
|
endPoint = endPoint,
|
|
|
|
|
|
deviceType = deviceType,
|
|
|
|
|
|
taskType = taskType,
|
|
|
|
|
|
taskCategory = taskCategory,
|
|
|
|
|
|
taskStatus = 1,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
}
|