You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

228 lines
8.8 KiB
C#

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
}