d01295c254063b3349a86a4474e04a62b284bd19..8e42d0c1b7ae36cff2e7c69999117911a4b6f300
2026-03-26 wanshenmean
feat(WCS): 完善 WIDESEAWCS_Tasks 模块代码注释
8e42d0 对比 | 目录
2026-03-26 wanshenmean
1
0831fc 对比 | 目录
2026-03-26 wanshenmean
1
263dd6 对比 | 目录
2026-03-26 wanshenmean
feat(WMS): 增强日志配置与添加事务处理支持
e25dc0 对比 | 目录
已添加1个文件
已修改57个文件
4984 ■■■■ 文件已修改
.gitignore 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.omc/project-memory.json 146 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/.vs/WIDESEAWCS_Server/v18/DocumentLayout.json 99 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Server/Program.cs 15 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Server/WIDESEAWCS_Server.csproj 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Server/appsettings.json 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/CommonConveyorLineNewJob.cs 170 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/ConstraintMachine/ConstraintMachineDBName.cs 44 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/ConveyorLine/CheckPalletPosition.cs 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/ConveyorLine/ConveyorLineDBNameNew.cs 145 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/ConveyorLine/ConveyorLineTaskCommandNew.cs 126 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/ConveyorLineDispatchHandler.cs 178 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/ConveyorLineTargetAddressSelector.cs 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/ConveyorLineTaskFilter.cs 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/PinMachine/PinMachineCommand.cs 42 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/PinMachine/PinMachineDBName.cs 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Abstractions/IRobotMessageRouter.cs 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Abstractions/IRobotPrefixCommandHandler.cs 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Abstractions/IRobotSimpleCommandHandler.cs 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Abstractions/IRobotWorkflowOrchestrator.cs 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Abstractions/ISocketClientGateway.cs 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotBarcodeGenerator.cs 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotClientManager.cs 137 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotJob.cs 160 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotMessageHandler.cs 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotSocketState.cs 108 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotStateManager.cs 102 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotTaskProcessor.cs 225 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotPrefixCommandHandler.cs 160 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotSimpleCommandHandler.cs 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotWorkflowOrchestrator.cs 148 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/SocketServer/SocketClientGateway.cs 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/SocketServer/SocketServerHostedService.cs 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/SocketServer/SocketServerOptions.cs 67 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/SocketServer/TcpSocketServer.Clients.cs 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/SocketServer/TcpSocketServer.Dispose.cs 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/SocketServer/TcpSocketServer.Messaging.cs 139 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/SocketServer/TcpSocketServer.Server.cs 133 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/SocketServer/TcpSocketServer.cs 146 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/StackerCraneJob/CommonStackerCraneJob.cs 149 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/StackerCraneJob/StackerCraneCommandBuilder.cs 191 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/StackerCraneJob/StackerCraneCommandConfig.cs 26 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/StackerCraneJob/StackerCraneDBName.cs 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/StackerCraneJob/StackerCraneTaskCommand.cs 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/StackerCraneJob/StackerCraneTaskSelector.cs 129 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/.claude/settings.local.json 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSClient/.omc/project-memory.json 44 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/.vs/WIDESEA_WMSServer/CopilotIndices/18.0.988.22099/CodeChunks.db 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/.vs/WIDESEA_WMSServer/CopilotIndices/18.0.988.22099/SemanticSymbols.db 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/.vs/WIDESEA_WMSServer/v18/DocumentLayout.backup.json 239 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/.vs/WIDESEA_WMSServer/v18/DocumentLayout.json 246 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_Core/BaseServices/ServiceBase.cs 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_StockService/StockSerivce.cs 193 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_SystemService/Sys_MenuService.cs 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService.cs 239 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Program.cs 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/WIDESEA_WMSServer.csproj 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/appsettings.json 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.gitignore
@@ -427,3 +427,10 @@
/Code/WCS/WIDESEAWCS_S7Simulator/.vs/WIDESEAWCS_S7Simulator.slnx/v18/DocumentLayout.backup.json
/Code/WCS/WIDESEAWCS_S7Simulator/.vs/WIDESEAWCS_S7Simulator.slnx/v18/DocumentLayout.json
/.omc
/Code/WMS/WIDESEA_WMSServer/.omc
/Code/WMS/WIDESEA_WMSServer/.vs
/.omc
/Code/WCS/WIDESEAWCS_Server/.vs
Code/WCS/WIDESEAWCS_Server/.vs/WIDESEAWCS_Server/v18/DocumentLayout.json
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Server/appsettings.json
/Code/WMS/WIDESEA_WMSClient/.omc
.omc/project-memory.json
@@ -1,6 +1,6 @@
{
  "version": "1.0.0",
  "lastScanned": 1774319550302,
  "lastScanned": 1774488038530,
  "projectRoot": "D:\\Git\\ShanMeiXinNengYuan",
  "techStack": {
    "languages": [],
@@ -36,172 +36,70 @@
      "path": "Code",
      "purpose": null,
      "fileCount": 0,
      "lastAccessed": 1774319550279,
      "lastAccessed": 1774488038511,
      "keyFiles": []
    },
    "项目资料": {
      "path": "项目资料",
      "purpose": null,
      "fileCount": 0,
      "lastAccessed": 1774319550280,
      "lastAccessed": 1774488038512,
      "keyFiles": []
    }
  },
  "hotPaths": [
    {
      "path": "Code\\WCS\\WIDESEAWCS_Client\\src\\views\\Index.vue",
      "accessCount": 8,
      "lastAccessed": 1774325288485,
      "path": "Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_StockService\\StockSerivce.cs",
      "accessCount": 14,
      "lastAccessed": 1774492937086,
      "type": "file"
    },
    {
      "path": "Code\\WCS\\CLAUDE.md",
      "accessCount": 4,
      "lastAccessed": 1774319832508,
      "type": "file"
    },
    {
      "path": "Code\\WMS\\CLAUDE.md",
      "accessCount": 4,
      "lastAccessed": 1774320014175,
      "type": "file"
    },
    {
      "path": "Code\\WMS",
      "accessCount": 2,
      "lastAccessed": 1774319685208,
      "type": "directory"
    },
    {
      "path": "Code\\WCS\\WIDESEAWCS_Client\\package.json",
      "accessCount": 1,
      "lastAccessed": 1774319589861,
      "type": "file"
    },
    {
      "path": "Code\\WCS\\WIDESEAWCS_Server\\CLAUDE.md",
      "accessCount": 1,
      "lastAccessed": 1774319589915,
      "type": "file"
    },
    {
      "path": "Code\\WCS\\WIDESEAWCS_Client\\README.md",
      "accessCount": 1,
      "lastAccessed": 1774319589977,
      "type": "file"
    },
    {
      "path": "Code\\WCS\\WIDESEAWCS_Client\\src\\main.js",
      "accessCount": 1,
      "lastAccessed": 1774319604017,
      "type": "file"
    },
    {
      "path": "Code\\WCS\\WIDESEAWCS_Client\\src\\router\\index.js",
      "accessCount": 1,
      "lastAccessed": 1774319604071,
      "type": "file"
    },
    {
      "path": "Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_Server\\appsettings.json",
      "accessCount": 1,
      "lastAccessed": 1774319604091,
      "type": "file"
    },
    {
      "path": "Code\\WCS\\WIDESEAWCS_Client\\src\\store\\index.js",
      "accessCount": 1,
      "lastAccessed": 1774319611661,
      "type": "file"
    },
    {
      "path": "Code\\WCS\\WIDESEAWCS_Client\\src\\api\\http.js",
      "accessCount": 1,
      "lastAccessed": 1774319611675,
      "type": "file"
    },
    {
      "path": "Code\\WMS\\WIDESEA_WMSClient\\package.json",
      "accessCount": 1,
      "lastAccessed": 1774319652164,
      "type": "file"
    },
    {
      "path": "Code\\WMS\\WIDESEA_WMSClient\\vite.config.js",
      "accessCount": 1,
      "lastAccessed": 1774319656371,
      "type": "file"
    },
    {
      "path": "Code\\WMS\\WIDESEA_WMSClient\\src\\main.js",
      "accessCount": 1,
      "lastAccessed": 1774319656381,
      "path": "Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_WMSServer\\appsettings.json",
      "accessCount": 5,
      "lastAccessed": 1774490361799,
      "type": "file"
    },
    {
      "path": "Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_WMSServer\\Program.cs",
      "accessCount": 1,
      "lastAccessed": 1774319656394,
      "accessCount": 3,
      "lastAccessed": 1774488093580,
      "type": "file"
    },
    {
      "path": "Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_WMSServer\\WIDESEA_WMSServer.csproj",
      "accessCount": 1,
      "lastAccessed": 1774319656456,
      "lastAccessed": 1774488065335,
      "type": "file"
    },
    {
      "path": "Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_Core\\WIDESEA_Core.csproj",
      "path": "Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_StockService\\StockViewService.cs",
      "accessCount": 1,
      "lastAccessed": 1774319664837,
      "lastAccessed": 1774492577060,
      "type": "file"
    },
    {
      "path": "Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_Core\\Extensions\\AutofacModuleRegister.cs",
      "path": "Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_StockService\\StockInfoService.cs",
      "accessCount": 1,
      "lastAccessed": 1774319668135,
      "lastAccessed": 1774492577097,
      "type": "file"
    },
    {
      "path": "Code\\WMS\\WIDESEA_WMSClient\\src\\router\\index.js",
      "path": "Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_StockService\\StockInfoDetailService.cs",
      "accessCount": 1,
      "lastAccessed": 1774319668167,
      "lastAccessed": 1774492577122,
      "type": "file"
    },
    {
      "path": "Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_Core\\BaseServices\\IService.cs",
      "path": "Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_Core\\BaseServices\\ServiceBase.cs",
      "accessCount": 1,
      "lastAccessed": 1774319668191,
      "lastAccessed": 1774492577619,
      "type": "file"
    },
    {
      "path": "Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_WMSServer\\Controllers\\Basic\\MaterielInfoController.cs",
      "path": "Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_IStockService\\IStockInfoService.cs",
      "accessCount": 1,
      "lastAccessed": 1774319671555,
      "type": "file"
    },
    {
      "path": "Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_IBasicService\\IMaterielInfoService.cs",
      "accessCount": 1,
      "lastAccessed": 1774319671580,
      "type": "file"
    },
    {
      "path": "Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_Model\\WIDESEA_Model.csproj",
      "accessCount": 1,
      "lastAccessed": 1774319671631,
      "type": "file"
    },
    {
      "path": "Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_WMSServer\\appsettings.json",
      "accessCount": 1,
      "lastAccessed": 1774319685049,
      "type": "file"
    },
    {
      "path": "Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_Core\\BaseController\\ApiBaseController.cs",
      "accessCount": 1,
      "lastAccessed": 1774319685114,
      "lastAccessed": 1774492782997,
      "type": "file"
    }
  ],
Code/WCS/WIDESEAWCS_Server/.vs/WIDESEAWCS_Server/v18/DocumentLayout.json
@@ -3,6 +3,14 @@
  "WorkspaceRootPath": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\",
  "Documents": [
    {
      "AbsoluteMoniker": "D:0:0:{487FA45B-EA1A-4ACA-BB5B-0F6708F462C0}|WIDESEAWCS_Server\\WIDESEAWCS_Server.csproj|d:\\git\\shanmeixinnengyuan\\code\\wcs\\wideseawcs_server\\wideseawcs_server\\program.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
      "RelativeMoniker": "D:0:0:{487FA45B-EA1A-4ACA-BB5B-0F6708F462C0}|WIDESEAWCS_Server\\WIDESEAWCS_Server.csproj|solutionrelative:wideseawcs_server\\program.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
    },
    {
      "AbsoluteMoniker": "D:0:0:{487FA45B-EA1A-4ACA-BB5B-0F6708F462C0}|WIDESEAWCS_Server\\WIDESEAWCS_Server.csproj|d:\\git\\shanmeixinnengyuan\\code\\wcs\\wideseawcs_server\\wideseawcs_server\\appsettings.json||{90A6B3A7-C1A3-4009-A288-E2FF89E96FA0}",
      "RelativeMoniker": "D:0:0:{487FA45B-EA1A-4ACA-BB5B-0F6708F462C0}|WIDESEAWCS_Server\\WIDESEAWCS_Server.csproj|solutionrelative:wideseawcs_server\\appsettings.json||{90A6B3A7-C1A3-4009-A288-E2FF89E96FA0}"
    },
    {
      "AbsoluteMoniker": "D:0:0:{83F18A31-5983-4587-A0B2-414BF70E50B5}|WIDESEAWCS_TaskInfoService\\WIDESEAWCS_TaskInfoService.csproj|d:\\git\\shanmeixinnengyuan\\code\\wcs\\wideseawcs_server\\wideseawcs_taskinfoservice\\flows\\relocationtaskflowservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
      "RelativeMoniker": "D:0:0:{83F18A31-5983-4587-A0B2-414BF70E50B5}|WIDESEAWCS_TaskInfoService\\WIDESEAWCS_TaskInfoService.csproj|solutionrelative:wideseawcs_taskinfoservice\\flows\\relocationtaskflowservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
    },
@@ -58,7 +66,7 @@
      "DocumentGroups": [
        {
          "DockedWidth": 200,
          "SelectedChildIndex": 4,
          "SelectedChildIndex": 3,
          "Children": [
            {
              "$type": "Bookmark",
@@ -73,25 +81,47 @@
              "Name": "ST:0:0:{aa2115a1-9712-457b-9047-dbb71ca2cdd2}"
            },
            {
              "$type": "Bookmark",
              "Name": "ST:0:0:{1c4feeaa-4718-4aa9-859d-94ce25d182ba}"
              "$type": "Document",
              "DocumentIndex": 0,
              "Title": "Program.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_Server\\Program.cs",
              "RelativeDocumentMoniker": "WIDESEAWCS_Server\\Program.cs",
              "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_Server\\Program.cs",
              "RelativeToolTip": "WIDESEAWCS_Server\\Program.cs",
              "ViewState": "AgIAAB8AAAAAAAAAAAAAAD4AAAAuAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-03-25T08:54:07.752Z",
              "EditorCaption": ""
            },
            {
              "$type": "Document",
              "DocumentIndex": 0,
              "DocumentIndex": 1,
              "Title": "appsettings.json",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_Server\\appsettings.json",
              "RelativeDocumentMoniker": "WIDESEAWCS_Server\\appsettings.json",
              "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_Server\\appsettings.json",
              "RelativeToolTip": "WIDESEAWCS_Server\\appsettings.json",
              "ViewState": "AgIAAAAAAAAAAAAAAAAAABAAAAAEAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.001642|",
              "WhenOpened": "2026-03-25T08:50:00.052Z",
              "EditorCaption": ""
            },
            {
              "$type": "Document",
              "DocumentIndex": 2,
              "Title": "RelocationTaskFlowService.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_TaskInfoService\\Flows\\RelocationTaskFlowService.cs",
              "RelativeDocumentMoniker": "WIDESEAWCS_TaskInfoService\\Flows\\RelocationTaskFlowService.cs",
              "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_TaskInfoService\\Flows\\RelocationTaskFlowService.cs",
              "RelativeToolTip": "WIDESEAWCS_TaskInfoService\\Flows\\RelocationTaskFlowService.cs",
              "ViewState": "AgIAAAAAAAAAAAAAAAAAABEAAABHAAAAAAAAAA==",
              "ViewState": "AgIAAGkAAAAAAAAAAAAUwHAAAAAVAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-03-19T09:35:02.468Z",
              "EditorCaption": ""
            },
            {
              "$type": "Document",
              "DocumentIndex": 2,
              "DocumentIndex": 4,
              "Title": "OutboundTaskFlowService.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_TaskInfoService\\Flows\\OutboundTaskFlowService.cs",
              "RelativeDocumentMoniker": "WIDESEAWCS_TaskInfoService\\Flows\\OutboundTaskFlowService.cs",
@@ -99,11 +129,12 @@
              "RelativeToolTip": "WIDESEAWCS_TaskInfoService\\Flows\\OutboundTaskFlowService.cs",
              "ViewState": "AgIAAEMAAAAAAAAAAAAewEEAAAAiAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-03-19T09:34:19.406Z"
              "WhenOpened": "2026-03-19T09:34:19.406Z",
              "EditorCaption": ""
            },
            {
              "$type": "Document",
              "DocumentIndex": 4,
              "DocumentIndex": 6,
              "Title": "InboundTaskFlowService.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_TaskInfoService\\Flows\\InboundTaskFlowService.cs",
              "RelativeDocumentMoniker": "WIDESEAWCS_TaskInfoService\\Flows\\InboundTaskFlowService.cs",
@@ -111,11 +142,12 @@
              "RelativeToolTip": "WIDESEAWCS_TaskInfoService\\Flows\\InboundTaskFlowService.cs",
              "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-03-19T09:34:14.627Z"
              "WhenOpened": "2026-03-19T09:34:14.627Z",
              "EditorCaption": ""
            },
            {
              "$type": "Document",
              "DocumentIndex": 9,
              "DocumentIndex": 11,
              "Title": "ApiRouteCacheWarmupHostedService.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_Server\\HostedService\\ApiRouteCacheWarmupHostedService.cs",
              "RelativeDocumentMoniker": "WIDESEAWCS_Server\\HostedService\\ApiRouteCacheWarmupHostedService.cs",
@@ -123,11 +155,12 @@
              "RelativeToolTip": "WIDESEAWCS_Server\\HostedService\\ApiRouteCacheWarmupHostedService.cs",
              "ViewState": "AgIAABoAAAAAAAAAAADwvzIAAAAdAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-03-19T08:01:46.997Z"
              "WhenOpened": "2026-03-19T08:01:46.997Z",
              "EditorCaption": ""
            },
            {
              "$type": "Document",
              "DocumentIndex": 10,
              "DocumentIndex": 12,
              "Title": "RedisServiceSetup.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_RedisService\\Extensions\\RedisServiceSetup.cs",
              "RelativeDocumentMoniker": "WIDESEAWCS_RedisService\\Extensions\\RedisServiceSetup.cs",
@@ -135,11 +168,12 @@
              "RelativeToolTip": "WIDESEAWCS_RedisService\\Extensions\\RedisServiceSetup.cs",
              "ViewState": "AgIAADQAAAAAAAAAAAAqwCgAAAAAAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-03-19T07:52:38.709Z"
              "WhenOpened": "2026-03-19T07:52:38.709Z",
              "EditorCaption": ""
            },
            {
              "$type": "Document",
              "DocumentIndex": 11,
              "DocumentIndex": 13,
              "Title": "HttpClientHelper.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_Core\\Http\\HTTP\\HttpClientHelper.cs",
              "RelativeDocumentMoniker": "WIDESEAWCS_Core\\Http\\HTTP\\HttpClientHelper.cs",
@@ -147,11 +181,12 @@
              "RelativeToolTip": "WIDESEAWCS_Core\\Http\\HTTP\\HttpClientHelper.cs",
              "ViewState": "AgIAAA4AAAAAAAAAAAAowBoAAABNAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-03-19T07:48:09.389Z"
              "WhenOpened": "2026-03-19T07:48:09.389Z",
              "EditorCaption": ""
            },
            {
              "$type": "Document",
              "DocumentIndex": 3,
              "DocumentIndex": 5,
              "Title": "TaskService.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_TaskInfoService\\TaskService.cs",
              "RelativeDocumentMoniker": "WIDESEAWCS_TaskInfoService\\TaskService.cs",
@@ -159,11 +194,12 @@
              "RelativeToolTip": "WIDESEAWCS_TaskInfoService\\TaskService.cs",
              "ViewState": "AgIAAGQBAAAAAAAAAADwv3oBAABOAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-03-19T01:42:07.337Z"
              "WhenOpened": "2026-03-19T01:42:07.337Z",
              "EditorCaption": ""
            },
            {
              "$type": "Document",
              "DocumentIndex": 1,
              "DocumentIndex": 3,
              "Title": "CommonStackerCraneJob.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_Tasks\\StackerCraneJob\\CommonStackerCraneJob.cs",
              "RelativeDocumentMoniker": "WIDESEAWCS_Tasks\\StackerCraneJob\\CommonStackerCraneJob.cs",
@@ -171,11 +207,12 @@
              "RelativeToolTip": "WIDESEAWCS_Tasks\\StackerCraneJob\\CommonStackerCraneJob.cs",
              "ViewState": "AgIAAEoAAAAAAAAAAAAQwF0AAAAzAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-03-19T01:42:07.364Z"
              "WhenOpened": "2026-03-19T01:42:07.364Z",
              "EditorCaption": ""
            },
            {
              "$type": "Document",
              "DocumentIndex": 7,
              "DocumentIndex": 9,
              "Title": "Sys_DictionaryService.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_SystemServices\\Sys_DictionaryService.cs",
              "RelativeDocumentMoniker": "WIDESEAWCS_SystemServices\\Sys_DictionaryService.cs",
@@ -183,35 +220,38 @@
              "RelativeToolTip": "WIDESEAWCS_SystemServices\\Sys_DictionaryService.cs",
              "ViewState": "AgIAAAsAAAAAAAAAAAAIwFcAAAAQAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-03-19T07:47:06.312Z"
              "WhenOpened": "2026-03-19T07:47:06.312Z",
              "EditorCaption": ""
            },
            {
              "$type": "Document",
              "DocumentIndex": 6,
              "DocumentIndex": 8,
              "Title": "RobotWorkflowOrchestrator.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_Tasks\\RobotJob\\Workflow\\RobotWorkflowOrchestrator.cs",
              "RelativeDocumentMoniker": "WIDESEAWCS_Tasks\\RobotJob\\Workflow\\RobotWorkflowOrchestrator.cs",
              "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_Tasks\\RobotJob\\Workflow\\RobotWorkflowOrchestrator.cs",
              "RelativeToolTip": "WIDESEAWCS_Tasks\\RobotJob\\Workflow\\RobotWorkflowOrchestrator.cs",
              "ViewState": "AgIAADQAAAAAAAAAAAAkwC8AAAA1AAAAAAAAAA==",
              "ViewState": "AgIAADQAAAAAAAAAAAA5wC8AAAA1AAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-03-18T09:21:38.852Z"
              "WhenOpened": "2026-03-18T09:21:38.852Z",
              "EditorCaption": ""
            },
            {
              "$type": "Document",
              "DocumentIndex": 5,
              "DocumentIndex": 7,
              "Title": "RobotMessageHandler.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_Tasks\\RobotJob\\RobotMessageHandler.cs",
              "RelativeDocumentMoniker": "WIDESEAWCS_Tasks\\RobotJob\\RobotMessageHandler.cs",
              "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_Tasks\\RobotJob\\RobotMessageHandler.cs",
              "RelativeToolTip": "WIDESEAWCS_Tasks\\RobotJob\\RobotMessageHandler.cs",
              "ViewState": "AgIAABQAAAAAAAAAAADwvzwAAAAwAAAAAAAAAA==",
              "ViewState": "AgIAABQAAAAAAAAAAAAwwDwAAAAwAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-03-18T09:10:02.533Z"
              "WhenOpened": "2026-03-18T09:10:02.533Z",
              "EditorCaption": ""
            },
            {
              "$type": "Document",
              "DocumentIndex": 8,
              "DocumentIndex": 10,
              "Title": "RobotJob.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WCS\\WIDESEAWCS_Server\\WIDESEAWCS_Tasks\\RobotJob\\RobotJob.cs",
              "RelativeDocumentMoniker": "WIDESEAWCS_Tasks\\RobotJob\\RobotJob.cs",
@@ -219,7 +259,8 @@
              "RelativeToolTip": "WIDESEAWCS_Tasks\\RobotJob\\RobotJob.cs",
              "ViewState": "AgIAAEcAAAAAAAAAAAAmwDUAAAAPAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-03-18T08:56:41.452Z"
              "WhenOpened": "2026-03-18T08:56:41.452Z",
              "EditorCaption": ""
            }
          ]
        }
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Server/Program.cs
@@ -1,6 +1,7 @@
using Autofac;
using Autofac.Core;
using Autofac.Extensions.DependencyInjection;
using Magicodes.ExporterAndImporter.Excel.Utility.TemplateExport;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
@@ -27,10 +28,10 @@
using WIDESEAWCS_QuartzJob;
using WIDESEAWCS_QuartzJob.QuartzExtensions;
using WIDESEAWCS_QuartzJob.Seed;
using WIDESEAWCS_RedisService.Extensions;
using WIDESEAWCS_Server.Filter;
using WIDESEAWCS_Server.HostedService;
using WIDESEAWCS_Tasks.SocketServer;
using WIDESEAWCS_RedisService.Extensions;
using WIDESEAWCS_WCSServer.Filter;
var builder = WebApplication.CreateBuilder(args);
@@ -50,13 +51,19 @@
        // è¿™æ ·å¯ä»¥å‡å°‘Microsoft框架本身的详细日志,避免过多的Debug日志
        .MinimumLevel.Override("Microsoft", LogEventLevel.Information)
        .WriteTo.Console()  // æ·»åŠ æŽ§åˆ¶å°è¾“å‡ºæŽ¥æ”¶å™¨ï¼Œæ—¥å¿—å°†æ˜¾ç¤ºåœ¨æŽ§åˆ¶å°çª—å£ä¸­
        // æ·»åŠ æ–‡ä»¶è¾“å‡ºæŽ¥æ”¶å™¨ï¼Œå°†æ—¥å¿—å†™å…¥æ–‡ä»¶ç³»ç»Ÿ
                            // æ·»åŠ æ–‡ä»¶è¾“å‡ºæŽ¥æ”¶å™¨ï¼Œå°†æ—¥å¿—å†™å…¥æ–‡ä»¶ç³»ç»Ÿ
        .WriteTo.File(
            /*Path.Combine(AppContext.BaseDirectory, "Logs", "serilog-.log"),*/  // æŒ‡å®šæ—¥å¿—文件的完整路径:应用程序目录 + "Log"文件夹 + "serilog-日期.log"
            "logs/serilog-.log",
            rollingInterval: RollingInterval.Day,  // è®¾ç½®æ—¥å¿—文件按天滚动,每天生成一个新的日志文件
            rollingInterval: RollingInterval.Day,  // è®¾ç½®æ—¥å¿—文件按天滚动,每天生成一个新的日志文件  U1od4UGVsIKZG39S5Yak
            retainedFileCountLimit: 30,  // æœ€å¤šä¿ç•™æœ€è¿‘30天的日志文件,超过30天的文件会自动删除
            shared: true);  // å…è®¸å¤šä¸ªè¿›ç¨‹åŒæ—¶å†™å…¥åŒä¸€ä¸ªæ—¥å¿—文件,适用于多实例部署场景
            shared: true)  // å…è®¸å¤šä¸ªè¿›ç¨‹åŒæ—¶å†™å…¥åŒä¸€ä¸ªæ—¥å¿—文件,适用于多实例部署场景
        .WriteTo.Seq(
                 serverUrl: "http://localhost:5341",
                 apiKey: "U1od4UGVsIKZG39S5Yak", // å¦‚Seq需要ApiKey则配置真实密钥
                 batchPostingLimit: 1000, // æ‰¹é‡å‘送数量
                 period: TimeSpan.FromSeconds(2) // å‘送间隔
             );
});
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory()).ConfigureContainer<ContainerBuilder>(builder =>
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Server/WIDESEAWCS_Server.csproj
@@ -68,7 +68,8 @@
        <PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
        <PackageReference Include="Serilog.Settings.Configuration" Version="8.0.4" />
        <PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
        <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
        <PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
        <PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0" />
        <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
    </ItemGroup>
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Server/appsettings.json
@@ -1,5 +1,20 @@
{
  "urls": "http://*:9292", //web服务端口,如果用IIS部署,把这个去掉
  "Serilog": {
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Information",
        "Microsoft.AspNetCore": "Warning",
        "Microsoft.AspNetCore.Routing": "Warning",
        "Microsoft.AspNetCore.Mvc": "Warning",
        "Microsoft.AspNetCore.Mvc.Infrastructure": "Warning",
        "Microsoft.AspNetCore.Mvc.Filters": "Warning",
        "Microsoft.AspNetCore.Mvc.ModelBinding": "Warning",
        "Microsoft.EntityFrameworkCore": "Warning"
      }
    }
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/CommonConveyorLineNewJob.cs
@@ -1,22 +1,3 @@
#region << ç‰ˆ æœ¬ æ³¨ é‡Š >>
/*----------------------------------------------------------------
 * å‘½åç©ºé—´ï¼šWIDESEAWCS_Tasks.ConveyorLineJob
 * åˆ›å»ºè€…:胡童庆
 * åˆ›å»ºæ—¶é—´ï¼š2024/8/2 16:13:36
 * ç‰ˆæœ¬ï¼šV1.0.0
 * æè¿°ï¼š
 *
 * ----------------------------------------------------------------
 * ä¿®æ”¹äººï¼š
 * ä¿®æ”¹æ—¶é—´ï¼š
 * ç‰ˆæœ¬ï¼šV1.0.1
 * ä¿®æ”¹è¯´æ˜Žï¼š
 *
 *----------------------------------------------------------------*/
#endregion << ç‰ˆ æœ¬ æ³¨ é‡Š >>
using MapsterMapper;
using Microsoft.Extensions.Configuration;
using Quartz;
@@ -34,74 +15,154 @@
namespace WIDESEAWCS_Tasks
{
    /// <summary>
    /// è¾“送线任务作业(Quartz Job)- æ ¸å¿ƒæ‰§è¡Œé€»è¾‘
    /// </summary>
    /// <remarks>
    /// Quartz å®šæ—¶ä»»åŠ¡ï¼Œè´Ÿè´£å¤„ç†è¾“é€çº¿çš„ä»»åŠ¡è°ƒåº¦ã€‚
    /// ä½¿ç”¨ [DisallowConcurrentExecution] ç¦æ­¢å¹¶å‘执行,确保同一设备的任务串行处理。
    ///
    /// æ ¸å¿ƒèŒè´£ï¼š
    /// 1. èŽ·å–è¾“é€çº¿çš„æ‰€æœ‰å­è®¾å¤‡ä½ç½®
    /// 2. å¹¶è¡Œå¤„理每个子设备的消息
    /// 3. æ ¹æ®ä»»åŠ¡çŠ¶æ€è°ƒç”¨ç›¸åº”çš„è°ƒåº¦æ–¹æ³•
    /// 4. å¤„理入库和出库两大类任务
    ///
    /// è¯¥ Job é€šè¿‡ Parallel.For å¹¶è¡Œå¤„理多个子设备,提高处理效率。
    /// </remarks>
    [DisallowConcurrentExecution]
    public class CommonConveyorLineNewJob : IJob
    {
        /// <summary>
        /// ä»»åŠ¡æœåŠ¡
        /// </summary>
        private readonly ITaskService _taskService;
        /// <summary>
        /// ä»»åŠ¡æ‰§è¡Œæ˜Žç»†æœåŠ¡
        /// </summary>
        private readonly ITaskExecuteDetailService _taskExecuteDetailService;
        /// <summary>
        /// è·¯ç”±æœåŠ¡
        /// </summary>
        private readonly IRouterService _routerService;
        /// <summary>
        /// å¯¹è±¡æ˜ å°„器
        /// </summary>
        private readonly IMapper _mapper;
        /// <summary>
        /// è¾“送线调度处理器
        /// </summary>
        /// <remarks>
        /// å°è£…了输送线业务逻辑的处理方法。
        /// </remarks>
        private ConveyorLineDispatchHandler _conveyorLineDispatch;
        /// <summary>
        /// HTTP å®¢æˆ·ç«¯å¸®åŠ©ç±»
        /// </summary>
        /// <remarks>
        /// ç”¨äºŽè°ƒç”¨ WMS ç³»ç»Ÿçš„ HTTP æŽ¥å£ã€‚
        /// </remarks>
        private readonly HttpClientHelper _httpClientHelper;
        /// <summary>
        /// æž„造函数
        /// </summary>
        /// <param name="taskService">任务服务</param>
        /// <param name="taskExecuteDetailService">任务执行明细服务</param>
        /// <param name="routerService">路由服务</param>
        /// <param name="mapper">对象映射器</param>
        /// <param name="httpClientHelper">HTTP å®¢æˆ·ç«¯å¸®åŠ©ç±»</param>
        public CommonConveyorLineNewJob(ITaskService taskService, ITaskExecuteDetailService taskExecuteDetailService, IRouterService routerService, IMapper mapper, HttpClientHelper httpClientHelper)
        {
            _taskService = taskService;
            _taskExecuteDetailService = taskExecuteDetailService;
            _routerService = routerService;
            _mapper = mapper;
            _conveyorLineDispatch = new ConveyorLineDispatchHandler(_taskService, _taskExecuteDetailService, _routerService, _mapper);
            _httpClientHelper = httpClientHelper;
            // åˆå§‹åŒ–调度处理器
            _conveyorLineDispatch = new ConveyorLineDispatchHandler(_taskService, _taskExecuteDetailService, _routerService, _mapper);
        }
        /// <summary>
        /// Quartz Job çš„æ‰§è¡Œå…¥å£
        /// </summary>
        /// <remarks>
        /// æ‰§è¡Œæµç¨‹ï¼š
        /// 1. ä»Ž JobDataMap èŽ·å–è¾“é€çº¿è®¾å¤‡ä¿¡æ¯
        /// 2. èŽ·å–è¯¥è¾“é€çº¿çš„æ‰€æœ‰å­è®¾å¤‡ä½ç½®åˆ—è¡¨
        /// 3. å¹¶è¡Œå¤„理每个子设备的消息
        /// 4. æ£€æŸ¥æ‰˜ç›˜ä½ç½®ï¼ˆç‰¹å®šé…ç½®çš„位置)
        /// 5. æ ¹æ®ä»»åŠ¡çŠ¶æ€åˆ†å‘åˆ°ç›¸åº”çš„å¤„ç†æ–¹æ³•
        ///
        /// å¹¶è¡Œå¤„理提高了对多站台输送线的处理效率。
        /// </remarks>
        /// <param name="context">Quartz ä½œä¸šæ‰§è¡Œä¸Šä¸‹æ–‡</param>
        public Task Execute(IJobExecutionContext context)
        {
            try
            {
                // ä»Ž JobDataMap èŽ·å–è¾“é€çº¿è®¾å¤‡å‚æ•°
                CommonConveyorLine conveyorLine = (CommonConveyorLine)context.JobDetail.JobDataMap.Get("JobParams");
                if (conveyorLine != null)
                {
                    // èŽ·å–è¯¥è¾“é€çº¿ä¸‹çš„æ‰€æœ‰å­è®¾å¤‡ä½ç½®ç¼–ç 
                    List<string> childDeviceCodes = _routerService.QueryAllPositions(conveyorLine.DeviceCode);
                    if (childDeviceCodes == null || childDeviceCodes.Count == 0)
                    {
                        // æ²¡æœ‰å­è®¾å¤‡ï¼Œç›´æŽ¥è¿”回
                        Console.WriteLine($"输送线 {conveyorLine.DeviceCode} æ²¡æœ‰å­è®¾å¤‡");
                        return Task.CompletedTask;
                    }
                    // åˆ›å»ºå¹¶è¡Œé€‰é¡¹
                    // åˆ›å»ºå¹¶è¡Œé€‰é¡¹ï¼Œé™åˆ¶æœ€å¤§å¹¶å‘æ•°
                    var parallelOptions = new ParallelOptions
                    {
                        MaxDegreeOfParallelism = Math.Min(childDeviceCodes.Count, Environment.ProcessorCount * 2), // åˆç†é™åˆ¶å¹¶å‘æ•°
                        // é™åˆ¶å¹¶å‘数:子设备数量和 CPU æ ¸å¿ƒæ•°*2 çš„较小值
                        MaxDegreeOfParallelism = Math.Min(childDeviceCodes.Count, Environment.ProcessorCount * 2),
                    };
                    // å¹¶è¡Œå¤„理每个子设备
                    Parallel.For(0, childDeviceCodes.Count, parallelOptions, i =>
                    {
                        string childDeviceCode = childDeviceCodes[i];
                        var correlationId = Guid.NewGuid().ToString("N");
                        try
                        {
                            // è¯»å–该位置的 PLC å‘½ä»¤æ•°æ®
                            ConveyorLineTaskCommandNew command = conveyorLine.ReadCustomer<ConveyorLineTaskCommandNew>(childDeviceCode);
                            // å¦‚果命令为空,跳过
                            if (command == null)
                            {
                                return;
                            }
                            if(command.WCS_ACK == 1)
                            // å¦‚æžœ WCS_ACK ä¸º 1,先清除(表示处理过上一次请求)
                            if (command.WCS_ACK == 1)
                                conveyorLine.SetValue(ConveyorLineDBNameNew.WCS_ACK, 0, childDeviceCode);
                            #region æ£€æŸ¥ç‰¹å®šä½ç½®æ˜¯å¦æœ‰æ‰˜ç›˜
                            // ========== æ£€æŸ¥ç‰¹å®šä½ç½®æ˜¯å¦æœ‰æ‰˜ç›˜ ==========
                            // ä»Žé…ç½®ä¸­è¯»å–需要检查托盘的位置列表
                            var checkPalletPositions = App.Configuration.GetSection("CheckPalletPositions")
                                .Get<List<CheckPalletPosition>>() ?? new List<CheckPalletPosition>();
                            // å¦‚果当前设备在检查列表中
                            if (checkPalletPositions.Any(x => x.Code == childDeviceCode))
                            {
                                // æ£€æŸ¥è¾“送线状态(是否有托盘)
                                if (command.CV_State.ObjToBool())
                                {
                                    // æ£€æŸ¥è¯¥ä½ç½®æ˜¯å¦å·²æœ‰ä»»åŠ¡
                                    var existingTask = _taskService.Repository.QueryFirst(x => x.TargetAddress == childDeviceCode);
                                    if (existingTask.IsNullOrEmpty())
                                    {
                                        // æ²¡æœ‰ä»»åŠ¡ï¼Œå‘ WMS è¯·æ±‚出库托盘任务
                                        var position = checkPalletPositions.FirstOrDefault(x => x.Code == childDeviceCode);
                                        var responseResult = _httpClientHelper.Post<WebResponseContent>("GetOutBoundTrayTaskAsync", new CreateTaskDto()
                                        {
@@ -109,6 +170,7 @@
                                            TargetAddress = childDeviceCode
                                        }.Serialize());
                                        // å¦‚果请求成功,接收 WMS è¿”回的任务
                                        if (responseResult.IsSuccess && responseResult.Data.Status)
                                        {
                                            var wmsTask = JsonSerializer.Deserialize<List<WMSTaskDTO>>(responseResult.Data.Data.Serialize());
@@ -119,32 +181,34 @@
                                }
                            }
                            #endregion
                            // ========== æ£€æŸ¥ PLC_STB æ ‡å¿— ==========
                            // åªæœ‰å½“ PLC_STB ä¸º 1 æ—¶æ‰å¤„理任务
                            if (command.PLC_STB != 1) return;
                            if (command.PLC_STB != 1) return;//PLC_STB=1时才处理任务
                            // ========== å¤„理无托盘条码的情况 ==========
                            // æ— æ‰˜ç›˜æ¡ç æ—¶ï¼Œè¯·æ±‚出库任务
                            if (command.Barcode.IsNullOrEmpty() || command.Barcode.Replace("\0", "") == "")
                            {
                                //无托盘号时
                                _conveyorLineDispatch.RequestOutbound(conveyorLine, command, childDeviceCode);
                                return;
                            }
                            // ========== å¤„理已有任务号的情况 ==========
                            if (command.TaskNo > 0)
                            {
                                // æŸ¥è¯¢æ­£åœ¨æ‰§è¡Œçš„任务
                                Dt_Task task = _taskService.QueryExecutingConveyorLineTask(command.TaskNo, childDeviceCode);
                                if (!task.IsNullOrEmpty())
                                {
                                    // å¤„理任务状态
                                    // å¤„理任务状态(根据状态分发到不同方法)
                                    ProcessTaskState(conveyorLine, command, task, childDeviceCode);
                                    //_conveyorLineDispatch.RequestInbound(conveyorLine, command, childDeviceCode);
                                    return;
                                }
                            }
                        }
                        catch (Exception innerEx)
                        {
                            // è®°å½•异常,但不影响其他子设备的处理
                            Console.Error.WriteLine($"{DateTime.UtcNow:O} [{childDeviceCode}] CorrelationId={correlationId} {innerEx}");
                        }
                    });
@@ -152,6 +216,7 @@
            }
            catch (Exception ex)
            {
                // è®°å½•整体异常
                Console.Error.WriteLine(ex);
            }
            return Task.CompletedTask;
@@ -160,43 +225,56 @@
        /// <summary>
        /// å¤„理任务状态
        /// </summary>
        /// <param name="conveyorLine">输送线实例对象</param>
        /// <param name="command">读取的请求信息</param>
        /// <param name="task">子设备编号</param>
        /// <param name="childDeviceCode"></param>
        /// <remarks>
        /// æ ¹æ®ä»»åŠ¡çš„å½“å‰çŠ¶æ€ï¼Œè°ƒç”¨ç›¸åº”çš„è°ƒåº¦æ–¹æ³•ï¼š
        /// - InExecuting: å…¥åº“执行中 -> è°ƒç”¨ RequestInNextAddress
        /// - OutExecuting: å‡ºåº“执行中 -> æ ¹æ®æ˜¯å¦åˆ°è¾¾ç›®æ ‡åœ°å€è°ƒç”¨å¯¹åº”方法
        /// - InFinish: å…¥åº“完成 -> è°ƒç”¨ ConveyorLineInFinish
        /// - OutFinish: å‡ºåº“完成 -> è°ƒç”¨ ConveyorLineOutFinish
        /// </remarks>
        /// <param name="conveyorLine">输送线设备对象</param>
        /// <param name="command">PLC å‘½ä»¤æ•°æ®</param>
        /// <param name="task">任务对象</param>
        /// <param name="childDeviceCode">子设备编码</param>
        private void ProcessTaskState(CommonConveyorLine conveyorLine, ConveyorLineTaskCommandNew command, Dt_Task task, string childDeviceCode)
        {
            // å®šä¹‰çŠ¶æ€å¸¸é‡ï¼ˆå¦‚æžœç±»ä¸­å·²å®šä¹‰åˆ™å¯ç§»é™¤ï¼‰
            const int InExecuting = (int)TaskInStatusEnum.Line_InExecuting;
            const int OutExecuting = (int)TaskOutStatusEnum.Line_OutExecuting;
            const int InFinish = (int)TaskInStatusEnum.InFinish;
            const int OutFinish = (int)TaskOutStatusEnum.OutFinish;
            // å®šä¹‰ä»»åŠ¡çŠ¶æ€å¸¸é‡
            const int InExecuting = (int)TaskInStatusEnum.Line_InExecuting;     // å…¥åº“执行中
            const int OutExecuting = (int)TaskOutStatusEnum.Line_OutExecuting; // å‡ºåº“执行中
            const int InFinish = (int)TaskInStatusEnum.InFinish;                 // å…¥åº“完成
            const int OutFinish = (int)TaskOutStatusEnum.OutFinish;             // å‡ºåº“完成
            // èŽ·å–å½“å‰ä»»åŠ¡çŠ¶æ€
            int state = task.TaskStatus;
            // åˆ¤æ–­å½“前子设备是否为目标地址
            bool isTargetAddress = task.TargetAddress == childDeviceCode;
            // å¤„理状态逻辑
            // æ ¹æ®çŠ¶æ€åˆ†å‘å¤„ç†
            switch (state)
            {
                case InExecuting:
                    //if (isTargetAddress)
                    //    _conveyorLineDispatch.ConveyorLineInFinish(conveyorLine, command, childDeviceCode);
                    //else
                    // å…¥åº“执行中,调用下一地址处理
                    _conveyorLineDispatch.RequestInNextAddress(conveyorLine, command, childDeviceCode);
                    break;
                case OutExecuting:
                    // å‡ºåº“执行中
                    if (isTargetAddress)
                        // åˆ°è¾¾ç›®æ ‡åœ°å€ï¼Œè°ƒç”¨å‡ºåº“完成
                        _conveyorLineDispatch.ConveyorLineOutFinish(conveyorLine, command, childDeviceCode);
                    else
                        // æœªåˆ°è¾¾ç›®æ ‡åœ°å€ï¼Œè°ƒç”¨å‡ºåº“下一地址处理
                        _conveyorLineDispatch.RequestOutNextAddress(conveyorLine, command, childDeviceCode);
                    break;
                case InFinish:
                    // å…¥åº“完成
                    _conveyorLineDispatch.ConveyorLineInFinish(conveyorLine, command, childDeviceCode);
                    break;
                case OutFinish:
                    // å‡ºåº“完成
                    _conveyorLineDispatch.ConveyorLineOutFinish(conveyorLine, command, childDeviceCode);
                    break;
            }
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/ConstraintMachine/ConstraintMachineDBName.cs
@@ -1,51 +1,81 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WIDESEAWCS_Tasks
{
    /// <summary>
    /// æ‹˜æŸæœº PLC å¯„存器名称枚举
    /// </summary>
    /// <remarks>
    /// å®šä¹‰æ‹˜æŸæœºä¸Ž WCS é€šä¿¡æ—¶ä½¿ç”¨çš„ PLC å¯„存器地址名称。
    /// æ‹˜æŸæœºæ˜¯ä¸€ç§ç”¨äºŽç”µæ± ç”Ÿäº§çš„设备,负责固定/约束电池托盘。
    /// è®¾å¤‡æœ‰ä¸Šä¸‹ä¸¤å±‚结构,每层都有独立的物料请求和出料信号。
    /// </remarks>
    public enum ConstraintMachineDBName
    {
        /// <summary>
        /// ç‰©æµçº¿è¿è¡Œä¿¡å·
        /// </summary>
        /// <remarks>
        /// è¡¨ç¤ºç‰©æµçº¿ï¼ˆè¿žæŽ¥æ‹˜æŸæœºçš„输送线)是否在运行。
        /// </remarks>
        LogisticsLineRunningSignal,
        /// <summary>
        /// æ‹˜æŸæœºè¿è¡Œä¿¡å·
        /// </summary>
        /// <remarks>
        /// è¡¨ç¤ºæ‹˜æŸæœºæœ¬èº«çš„运行状态。
        /// </remarks>
        ConstraintMachineRunningSignal,
        /// <summary>
        /// è¦æ–™è¯·æ±‚-上层
        /// </summary>
        /// <remarks>
        /// ä¸Šå±‚工位向上级设备发出的原料请求。
        /// éžé›¶å€¼è¡¨ç¤ºä¸Šå±‚需要补充物料。
        /// </remarks>
        MaterialRequestUpper,
        /// <summary>
        /// æ‹˜æŸç›˜å¯å‡ºæ–™-上层
        /// </summary>
        /// <remarks>
        /// WCS å†™å…¥çš„信号,告知上层托盘可以出料。
        /// </remarks>
        ConstraintTrayOutputReadyUpper,
        /// <summary>
        /// å‡ºæ–™è¯·æ±‚-上层
        /// </summary>
        /// <remarks>
        /// ä¸Šå±‚工位完成加工后发出的出料请求。
        /// éžé›¶å€¼è¡¨ç¤ºæœ‰æ–™éœ€è¦è¾“出。
        /// </remarks>
        OutputRequestUpper,
        /// <summary>
        /// è¦æ–™è¯·æ±‚-下层
        /// </summary>
        /// <remarks>
        /// ä¸‹å±‚工位向上级设备发出的原料请求。
        /// éžé›¶å€¼è¡¨ç¤ºä¸‹å±‚需要补充物料。
        /// </remarks>
        MaterialRequestLower,
        /// <summary>
        /// æ‹˜æŸç›˜å¯å‡ºæ–™-下层
        /// </summary>
        /// <remarks>
        /// WCS å†™å…¥çš„信号,告知下层托盘可以出料。
        /// </remarks>
        ConstraintTrayOutputReadyLower,
        /// <summary>
        /// å‡ºæ–™è¯·æ±‚-下层
        /// </summary>
        /// <remarks>
        /// ä¸‹å±‚工位完成加工后发出的出料请求。
        /// éžé›¶å€¼è¡¨ç¤ºæœ‰æ–™éœ€è¦è¾“出。
        /// </remarks>
        OutputRequestLower
    }
}
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/ConveyorLine/CheckPalletPosition.cs
@@ -1,9 +1,32 @@
namespace WIDESEAWCS_Tasks
{
    /// <summary>
    /// æ‰˜ç›˜ä½ç½®æ£€æŸ¥é…ç½®å®žä½“
    /// </summary>
    /// <remarks>
    /// ç”¨äºŽé…ç½®éœ€è¦æ£€æŸ¥æ‰˜ç›˜ä½ç½®çš„站点信息。
    /// å½“系统需要检查特定位置是否有托盘时使用此配置。
    /// é…ç½®æ¥æºäºŽ appsettings.json ä¸­çš„ CheckPalletPositions èŠ‚ç‚¹ã€‚
    /// </remarks>
    public class CheckPalletPosition
    {
        /// <summary>
        /// å­è®¾å¤‡ç¼–码/位置编码
        /// </summary>
        /// <remarks>
        /// ç”¨äºŽæ ‡è¯†éœ€è¦æ£€æŸ¥æ‰˜ç›˜çš„位置点。
        /// </remarks>
        public string Code { get; set; } = string.Empty;
        /// <summary>
        /// ä»“库 ID
        /// </summary>
        /// <remarks>
        /// æ ‡è¯†æ‰˜ç›˜æ‰€åœ¨çš„仓库编号。
        /// 1: ZYRB1 å¯¹åº”的仓库
        /// 2: HPRB001 å¯¹åº”的仓库
        /// 3: å…¶ä»–仓库
        /// </remarks>
        public int WarehouseId { get; set; } = 0;
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/ConveyorLine/ConveyorLineDBNameNew.cs
@@ -1,93 +1,164 @@
#region << ç‰ˆ æœ¬ æ³¨ é‡Š >>
/*----------------------------------------------------------------
 * å‘½åç©ºé—´ï¼šWIDESEAWCS_Tasks.ConveyorLineJob
 * åˆ›å»ºè€…:胡童庆
 * åˆ›å»ºæ—¶é—´ï¼š2024/8/2 16:13:36
 * ç‰ˆæœ¬ï¼šV1.0.0
 * æè¿°ï¼š
 *
 * ----------------------------------------------------------------
 * ä¿®æ”¹äººï¼š
 * ä¿®æ”¹æ—¶é—´ï¼š
 * ç‰ˆæœ¬ï¼šV1.0.1
 * ä¿®æ”¹è¯´æ˜Žï¼š
 *
 *----------------------------------------------------------------*/
#endregion << ç‰ˆ æœ¬ æ³¨ é‡Š >>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WIDESEAWCS_Tasks
{
    /// <summary>
    /// è¾“送线 PLC å¯„存器名称枚举(新版)
    /// </summary>
    /// <remarks>
    /// å®šä¹‰è¾“送线与 WCS é€šä¿¡æ—¶ä½¿ç”¨çš„ PLC å¯„存器地址名称。
    /// åŒ…含任务号、地址、状态标志、条码等字段。
    /// WCS é€šè¿‡è¿™äº›å¯„存器与 PLC äº¤äº’,实现任务的下发、状态同步和完成确认。
    /// </remarks>
    public enum ConveyorLineDBNameNew
    {
        /// <summary>
        /// ä»»åŠ¡å·
        /// </summary>
        /// <remarks>
        /// PLC å’Œ WCS ä¹‹é—´å…±äº«çš„任务标识号。
        /// WCS ä¸‹å‘任务时写入,PLC å®Œæˆä»»åŠ¡åŽä¿æŒã€‚
        /// </remarks>
        TaskNo,
        /// <summary>
        /// å¼€å§‹åœ°å€
        /// å¼€å§‹åœ°å€/源地址
        /// </summary>
        /// <remarks>
        /// ä»»åŠ¡çš„èµ·å§‹ä½ç½®åœ°å€ã€‚
        /// ç”¨äºŽå…¥åº“任务时表示货物来源,出库任务时表示货物当前位置。
        /// </remarks>
        Source,
        /// <summary>
        /// ç›®çš„地址
        /// ç›®æ ‡åœ°å€
        /// </summary>
        /// <remarks>
        /// ä»»åŠ¡çš„ç›®æ ‡ä½ç½®åœ°å€ã€‚
        /// ç”¨äºŽå…¥åº“任务时表示货物存放位置,出库任务时表示货物送达位置。
        /// </remarks>
        Target,
        /// <summary>
        /// æ‰˜ç›˜ç±»åž‹
        /// </summary>
        /// <remarks>
        /// æ ‡è¯†æ‰˜ç›˜çš„规格类型。
        /// </remarks>
        BoxType,
        /// <summary>
        /// è®¾å¤‡ç©ºé—²çŠ¶æ€è¾“é€å¸¦çŠ¶æ€
        /// è¾“送线空闲状态
        /// </summary>
        /// <remarks>
        /// è¾“送线当前是否空闲。
        /// é€šå¸¸ç”¨äºŽåˆ¤æ–­è¾“送线上是否有货物正在移动。
        /// </remarks>
        CV_State,
        /// <summary>
        /// è¾“送故障代码
        /// è¾“送线故障代码
        /// </summary>
        /// <remarks>
        /// PLC æŠ¥å‘Šçš„设备故障代码。
        /// 0 è¡¨ç¤ºæ— æ•…障,非零值表示具体故障类型。
        /// </remarks>
        CV_ERRCode,
        /// <summary>
        /// WCS下发完成时,触发为1
        /// WCS ä¸‹å‘完成标志
        /// </summary>
        /// <remarks>
        /// WCS ä¸‹å‘任务完成时置 1。
        /// é€šçŸ¥ PLC å¯ä»¥å¼€å§‹å¤„理该任务。
        /// PLC è¯»å–后应立即清除此标志。
        /// </remarks>
        WCS_STB,
        /// <summary>
        /// WCS收到完成时,触发为1
        /// WCS åº”答标志
        /// </summary>
        /// <remarks>
        /// WCS æ”¶åˆ° PLC è¯·æ±‚后回复的确认标志。
        /// PLC å‘出请求后等待 WCS æ­¤æ ‡å¿—ç½® 1。
        /// </remarks>
        WCS_ACK,
        /// <summary>
        /// å®Œæˆä»»åŠ¡æ—¶ï¼Œè§¦å‘ä¸º1
        /// PLC ä»»åŠ¡å®Œæˆæ ‡å¿—
        /// </summary>
        /// <remarks>
        /// PLC å®Œæˆä»»åŠ¡æ—¶ç½® 1。
        /// é€šçŸ¥ WCS ä»»åŠ¡å·²å®Œæˆï¼Œå¯ä»¥è¿›è¡ŒåŽç»­å¤„ç†ã€‚
        /// WCS è¯»å–后应立即清除此标志。
        /// </remarks>
        PLC_STB,
        /// <summary>
        /// æ”¶åˆ°ä»»åŠ¡æ—¶ï¼Œè§¦å‘ä¸º1
        /// PLC åº”答标志
        /// </summary>
        /// <remarks>
        /// PLC æ”¶åˆ° WCS å‘½ä»¤åŽå›žå¤çš„确认标志。
        /// WCS ä¸‹å‘命令后等待 PLC æ­¤æ ‡å¿—ç½® 1。
        /// </remarks>
        PLC_ACK,
        /// <summary>
        /// å…¥åº“站台,到位写1
        /// PLC è¯·æ±‚标志
        /// </summary>
        /// <remarks>
        /// PLC ä¸»åŠ¨è¯·æ±‚æœåŠ¡æ—¶ç½® 1。
        /// é€šå¸¸ç”¨äºŽå…¥åº“站台,表示货物已到位,请求 WCS ä¸‹å‘任务。
        /// </remarks>
        PLC_REQ,
        /// <summary>
        /// WCS故障代码
        /// WCS é”™è¯¯ä»£ç 
        /// </summary>
        /// <remarks>
        /// WCS æŠ¥å‘Šçš„业务错误代码。
        /// ç”¨äºŽæ ‡è¯†ä»»åŠ¡æ‰§è¡Œè¿‡ç¨‹ä¸­çš„ä¸šåŠ¡é€»è¾‘é”™è¯¯ã€‚
        /// </remarks>
        WCS_ERRCode,
        /// <summary>
        /// WCS特殊处理标识(旋转标识、强制放行、循环、特殊申请、是否叠盘、是否堵塞)
        /// WCS ç‰¹æ®Šå¤„理标识
        /// </summary>
        /// <remarks>
        /// ç”¨äºŽæ ‡è¯†ç‰¹æ®Šå¤„理需求,包含以下位标志(从低位到高位):
        /// - ä½0: æ—‹è½¬æ ‡è¯†
        /// - ä½1: å¼ºåˆ¶æ”¾è¡Œ
        /// - ä½2: å¾ªçޝ
        /// - ä½3: ç‰¹æ®Šç”³è¯·
        /// - ä½4: æ˜¯å¦å ç›˜
        /// - ä½5: æ˜¯å¦å µå¡ž
        /// </remarks>
        WCS_Special,
        /// <summary>
        /// æ‰‹åЍ1,自动2
        /// è®¾å¤‡è‡ªåŠ¨æ¨¡å¼
        /// </summary>
        /// <remarks>
        /// æ ‡è¯†è®¾å¤‡çš„运行模式:
        /// - 1: æ‰‹åŠ¨æ¨¡å¼
        /// - 2: è‡ªåŠ¨æ¨¡å¼
        /// </remarks>
        Equ_Auto,
        /// <summary>
        /// å°¾ç›˜æ ‡è¯†
        /// å°¾ç›˜/尾板标识
        /// </summary>
        /// <remarks>
        /// æ ‡è¯†å½“前托盘是否为最后一个(尾盘)。
        /// ç”¨äºŽç”µæ± ç”Ÿäº§çº¿çš„æœ€åŽä¸€é“工序。
        /// </remarks>
        Last_pallet,
        /// <summary>
        /// å®¹å™¨æ¡ç 1,字符数组
        /// æ‰˜ç›˜æ¡ç 
        /// </summary>
        /// <remarks>
        /// å­˜å‚¨æ‰˜ç›˜çš„æ¡ç ä¿¡æ¯ï¼ˆ22个字符)。
        /// ç”¨äºŽè´§ç‰©è¿½è¸ªå’Œåº“位管理。
        /// </remarks>
        Barcode
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/ConveyorLine/ConveyorLineTaskCommandNew.cs
@@ -1,110 +1,164 @@
#region << ç‰ˆ æœ¬ æ³¨ é‡Š >>
/*----------------------------------------------------------------
 * å‘½åç©ºé—´ï¼šWIDESEAWCS_Tasks.ConveyorLineJob
 * åˆ›å»ºè€…:胡童庆
 * åˆ›å»ºæ—¶é—´ï¼š2024/8/2 16:13:36
 * ç‰ˆæœ¬ï¼šV1.0.0
 * æè¿°ï¼š
 *
 * ----------------------------------------------------------------
 * ä¿®æ”¹äººï¼š
 * ä¿®æ”¹æ—¶é—´ï¼š
 * ç‰ˆæœ¬ï¼šV1.0.1
 * ä¿®æ”¹è¯´æ˜Žï¼š
 *
 *----------------------------------------------------------------*/
#endregion << ç‰ˆ æœ¬ æ³¨ é‡Š >>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using WIDESEAWCS_QuartzJob.DeviceBase;
namespace WIDESEAWCS_Tasks
{
    /// <summary>
    /// è¾“送线 PLC é€šä¿¡å‘½ä»¤æ•°æ®ç±»ï¼ˆæ–°ç‰ˆï¼‰
    /// </summary>
    /// <remarks>
    /// ç»§æ‰¿è‡ª DeviceCommand,用于与输送线 PLC è¿›è¡Œé€šä¿¡ã€‚
    /// åŒ…含任务号、源/目标地址、托盘条码、WCS/PLC åº”答标志等字段。
    /// WCS é€šè¿‡è¿™äº›å­—段与 PLC äº¤äº’,实现任务的下发、状态同步和完成确认。
    /// </remarks>
    public class ConveyorLineTaskCommandNew : DeviceCommand
    {
        /// <summary>
        /// ä»»åŠ¡å·
        /// </summary>
        /// <remarks>
        /// WCS åˆ†é…çš„任务唯一标识号。
        /// ç”¨äºŽåœ¨ WCS å’Œ PLC ä¹‹é—´å»ºç«‹ä»»åŠ¡å¯¹åº”çš„å…³è”ã€‚
        /// </remarks>
        public short TaskNo { get; set; }
        /// <summary>
        /// æºä½ç½® å¼€å§‹åœ°å€
        /// æºä½ç½®/起始地址
        /// </summary>
        public short Source {  get; set; }
        /// <remarks>
        /// ä»»åŠ¡çš„èµ·å§‹ä½ç½®åœ°å€ç¼–ç ã€‚
        /// å…¥åº“任务时表示货物来自哪个站台,出库任务时表示货物当前所在位置。
        /// </remarks>
        public short Source { get; set; }
        /// <summary>
        /// ç›®æ ‡ä½ç½®
        /// </summary>
        /// <remarks>
        /// ä»»åŠ¡çš„ç›®æ ‡ä½ç½®åœ°å€ç¼–ç ã€‚
        /// å…¥åº“任务时表示货物存入哪个库位,出库任务时表示货物送达哪个站台。
        /// </remarks>
        public short Target { get; set; }
        /// <summary>
        /// ç®±åž‹
        /// ç®±åž‹/托盘类型
        /// </summary>
        /// <remarks>
        /// æ ‡è¯†æ‰˜ç›˜çš„规格类型,用于区分不同的货物载体。
        /// </remarks>
        public byte BoxType { get; set; }
        /// <summary>
        /// è¾“送线状态 è®¾å¤‡ç©ºé—²çŠ¶æ€
        /// è¾“送线状态
        /// </summary>
        /// <remarks>
        /// è¾“送线的空闲/占用状态。
        /// ç”¨äºŽåˆ¤æ–­è¾“送线是否可以接受新任务。
        /// </remarks>
        public byte CV_State { get; set; }
        /// <summary>
        /// è¾“送线错误代码
        /// </summary>
        /// <remarks>
        /// PLC æŠ¥å‘Šçš„设备故障代码。
        /// 0 è¡¨ç¤ºæ­£å¸¸è¿è¡Œï¼Œéžé›¶å€¼è¡¨ç¤ºå…·ä½“的故障类型。
        /// </remarks>
        public byte CV_ERRCode { get; set; }
        /// <summary>
        /// WCS就绪标志 WCS下发完成时,触发为1
        /// WCS ä¸‹å‘完成标志
        /// </summary>
        /// <remarks>
        /// WCS ä¸‹å‘任务时置 1,通知 PLC å¼€å§‹æ‰§è¡Œä»»åŠ¡ã€‚
        /// PLC è¯»å–后应立即清除此标志。
        /// </remarks>
        public byte WCS_STB { get; set; }
        /// <summary>
        /// WCS应答标志 WCS收到完成时,触发为1
        /// WCS åº”答标志
        /// </summary>
        /// <remarks>
        /// WCS æ”¶åˆ° PLC è¯·æ±‚后的回复标志。
        /// å½“值为 1 æ—¶è¡¨ç¤º WCS å·²æ”¶åˆ°è¯·æ±‚并处理。
        /// </remarks>
        public byte WCS_ACK { get; set; }
        /// <summary>
        /// PLC就绪标志 å®Œæˆä»»åŠ¡æ—¶ï¼Œè§¦å‘ä¸º1
        /// PLC ä»»åŠ¡å®Œæˆæ ‡å¿—
        /// </summary>
        /// <remarks>
        /// PLC å®Œæˆä»»åŠ¡æ—¶ç½® 1,通知 WCS ä»»åŠ¡å·²å®Œæˆã€‚
        /// WCS è¯»å–后应清除此标志。
        /// </remarks>
        public byte PLC_STB { get; set; }
        /// <summary>
        /// PLC应答标志 æ”¶åˆ°ä»»åŠ¡æ—¶ï¼Œè§¦å‘ä¸º1
        /// PLC åº”答标志
        /// </summary>
        /// <remarks>
        /// PLC æ”¶åˆ° WCS å‘½ä»¤åŽçš„回复标志。
        /// å½“值为 1 æ—¶è¡¨ç¤º PLC å·²æ”¶åˆ°å‘½ä»¤ã€‚
        /// </remarks>
        public byte PLC_ACK { get; set; }
        /// <summary>
        /// PLC请求标志 å…¥åº“站台,到位写1
        /// PLC è¯·æ±‚标志
        /// </summary>
        /// <remarks>
        /// PLC ä¸»åŠ¨è¯·æ±‚æœåŠ¡æ—¶ç½® 1。
        /// é€šå¸¸ç”¨äºŽå…¥åº“站台,表示货物已到位可以开始处理。
        /// </remarks>
        public byte PLC_REQ { get; set; }
        /// <summary>
        /// WCS错误代码
        /// WCS é”™è¯¯ä»£ç 
        /// </summary>
        /// <remarks>
        /// WCS æŠ¥å‘Šçš„业务错误代码。
        /// ç”¨äºŽæ ‡è¯†ä»»åŠ¡æ‰§è¡Œè¿‡ç¨‹ä¸­çš„ä¸šåŠ¡é€»è¾‘é”™è¯¯ã€‚
        /// </remarks>
        public byte WCS_ERRCode { get; set; }
        /// <summary>
        /// WCS特殊标志 (旋转标识、强制放行、循环、特殊申请、是否叠盘、是否堵塞)
        /// WCS ç‰¹æ®Šæ ‡å¿—
        /// </summary>
        /// <remarks>
        /// ç”¨äºŽæ ‡è¯†ç‰¹æ®Šå¤„理需求,包含多个位标志:
        /// - ä½0: æ—‹è½¬æ ‡è¯† - æ˜¯å¦éœ€è¦æ—‹è½¬æ‰˜ç›˜
        /// - ä½1: å¼ºåˆ¶æ”¾è¡Œ - å¿½ç•¥å¸¸è§„检查直接放行
        /// - ä½2: å¾ªçޝ - æ˜¯å¦å¾ªçŽ¯æ‰§è¡Œ
        /// - ä½3: ç‰¹æ®Šç”³è¯· - æ˜¯å¦æœ‰ç‰¹æ®Šè¯·æ±‚需要处理
        /// - ä½4: æ˜¯å¦å ç›˜ - æ˜¯å¦éœ€è¦å ç›˜å¤„理
        /// - ä½5: æ˜¯å¦å µå¡ž - æ˜¯å¦å¤„于堵塞状态
        /// </remarks>
        public byte WCS_Special { get; set; }
        /// <summary>
        /// è®¾å¤‡è‡ªåŠ¨æ¨¡å¼ æ‰‹åЍ1,自动2
        /// è®¾å¤‡è‡ªåŠ¨æ¨¡å¼
        /// </summary>
        /// <remarks>
        /// æ ‡è¯†è®¾å¤‡çš„运行模式:
        /// - 1: æ‰‹åŠ¨æ¨¡å¼
        /// - 2: è‡ªåŠ¨æ¨¡å¼
        /// </remarks>
        public byte Equ_Auto { get; set; }
        /// <summary>
        /// å°¾æ¿æ ‡å¿—
        /// å°¾æ¿/尾盘标志
        /// </summary>
        /// <remarks>
        /// æ ‡è¯†å½“前托盘是否为最后一个(尾盘)。
        /// ç”¨äºŽç”µæ± ç”Ÿäº§çº¿çš„æœ€åŽä¸€é“工序,标记整批任务的结束。
        /// </remarks>
        public byte Last_pallet { get; set; }
        /// <summary>
        /// æ¡ç ï¼ˆ22个字符)
        /// æ‰˜ç›˜æ¡ç ï¼ˆ22个字符)
        /// </summary>
        /// <remarks>
        /// å­˜å‚¨æ‰˜ç›˜çš„æ¡ç ä¿¡æ¯ï¼Œç”¨äºŽè´§ç‰©è¿½è¸ªå’Œåº“位管理。
        /// </remarks>
        [DataLength(22)]
        public string Barcode { get; set; }
    }
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/ConveyorLineDispatchHandler.cs
@@ -1,22 +1,3 @@
#region << ç‰ˆ æœ¬ æ³¨ é‡Š >>
/*----------------------------------------------------------------
 * å‘½åç©ºé—´ï¼šWIDESEAWCS_Tasks.ConveyorLineJob
 * åˆ›å»ºè€…:胡童庆
 * åˆ›å»ºæ—¶é—´ï¼š2024/8/2 16:13:36
 * ç‰ˆæœ¬ï¼šV1.0.0
 * æè¿°ï¼š
 *
 * ----------------------------------------------------------------
 * ä¿®æ”¹äººï¼š
 * ä¿®æ”¹æ—¶é—´ï¼š
 * ç‰ˆæœ¬ï¼šV1.0.1
 * ä¿®æ”¹è¯´æ˜Žï¼š
 *
 *----------------------------------------------------------------*/
#endregion << ç‰ˆ æœ¬ æ³¨ é‡Š >>
using MapsterMapper;
using WIDESEAWCS_Common.TaskEnum;
using WIDESEAWCS_Core;
@@ -28,16 +9,66 @@
namespace WIDESEAWCS_Tasks
{
    /// <summary>
    /// è¾“送线调度处理器 - å¤„理输送线的各种业务请求
    /// </summary>
    /// <remarks>
    /// æ ¸å¿ƒèŒè´£ï¼š
    /// 1. å¤„理输送线的心跳(保持连接)
    /// 2. å¤„理入库请求(PLC è¯·æ±‚入库任务)
    /// 3. å¤„理入库下一地址(任务执行中的地址更新)
    /// 4. å¤„理入库完成
    /// 5. å¤„理出库请求
    /// 6. å¤„理出库下一地址
    /// 7. å¤„理出库完成
    ///
    /// è¯¥ç±»æ˜¯è¾“送线业务逻辑的核心,根据 PLC çš„请求类型调用相应的处理方法。
    /// </remarks>
    public class ConveyorLineDispatchHandler
    {
        /// <summary>
        /// ä»»åŠ¡æœåŠ¡
        /// </summary>
        private readonly ITaskService _taskService;
        /// <summary>
        /// ä»»åŠ¡æ‰§è¡Œæ˜Žç»†æœåŠ¡
        /// </summary>
        private readonly ITaskExecuteDetailService _taskExecuteDetailService;
        /// <summary>
        /// è·¯ç”±æœåŠ¡
        /// </summary>
        private readonly IRouterService _routerService;
        /// <summary>
        /// å¯¹è±¡æ˜ å°„器
        /// </summary>
        private readonly IMapper _mapper;
        /// <summary>
        /// è¾“送线任务过滤器
        /// </summary>
        /// <remarks>
        /// ç”¨äºŽæŸ¥è¯¢å¾…处理和执行中的任务。
        /// </remarks>
        private readonly ConveyorLineTaskFilter _taskFilter;
        /// <summary>
        /// ç›®æ ‡åœ°å€é€‰æ‹©å™¨
        /// </summary>
        /// <remarks>
        /// ç”¨äºŽå¤„理拘束机/插拔钉机等设备的上下层请求。
        /// </remarks>
        private readonly ConveyorLineTargetAddressSelector _targetAddressSelector;
        /// <summary>
        /// æž„造函数
        /// </summary>
        /// <param name="taskService">任务服务</param>
        /// <param name="taskExecuteDetailService">任务执行明细服务</param>
        /// <param name="routerService">路由服务</param>
        /// <param name="mapper">对象映射器</param>
        public ConveyorLineDispatchHandler(ITaskService taskService, ITaskExecuteDetailService taskExecuteDetailService, IRouterService routerService, IMapper mapper)
        {
            _taskService = taskService;
@@ -45,123 +76,214 @@
            _routerService = routerService;
            _mapper = mapper;
            // åˆå§‹åŒ–任务过滤器和目标地址选择器
            _taskFilter = new ConveyorLineTaskFilter(taskService);
            _targetAddressSelector = new ConveyorLineTargetAddressSelector();
        }
        /// <summary>
        /// å¿ƒè·³å¤„理
        /// å¤„理输送线心跳
        /// </summary>
        /// <remarks>
        /// å½“收到 PLC çš„心跳信号时调用。
        /// æ¸…除任务号,表示当前没有执行任务。
        /// è¿™æ˜¯ä¸ºäº†ä¿æŒä¸Ž PLC çš„连接活跃。
        /// </remarks>
        /// <param name="conveyorLine">输送线设备对象</param>
        /// <param name="command">PLC å‘½ä»¤æ•°æ®</param>
        /// <param name="childDeviceCode">子设备编码</param>
        public void HeartBeat(CommonConveyorLine conveyorLine, ConveyorLineTaskCommandNew command, string childDeviceCode)
        {
            // æ¸…除任务号,表示当前空闲
            conveyorLine.SetValue(ConveyorLineDBNameNew.TaskNo, 0, childDeviceCode);
        }
        /// <summary>
        /// è¾“送线请求入库
        /// å¤„理输送线入库请求
        /// </summary>
        /// <remarks>
        /// å½“ PLC è¯·æ±‚入库任务时调用。
        /// æµç¨‹ï¼š
        /// 1. å‘ WMS è¯·æ±‚新任务
        /// 2. æŸ¥è¯¢å¾…处理任务
        /// 3. ä¸‹å‘任务到 PLC
        /// 4. æ›´æ–°ä»»åŠ¡çŠ¶æ€
        /// </remarks>
        /// <param name="conveyorLine">输送线设备对象</param>
        /// <param name="command">PLC å‘½ä»¤æ•°æ®</param>
        /// <param name="childDeviceCode">子设备编码</param>
        public void RequestInbound(CommonConveyorLine conveyorLine, ConveyorLineTaskCommandNew command, string childDeviceCode)
        {
            // å‘ WMS è¯·æ±‚新任务(基于条码)
            if (_taskFilter.RequestWmsTask(command.Barcode, childDeviceCode))
            {
                // WMS è¿”回成功,查询待处理任务
                Dt_Task? task = _taskFilter.QueryPendingTask(conveyorLine.DeviceCode, childDeviceCode);
                if (task != null)
                {
                    // å°†ä»»åŠ¡æ˜ å°„ä¸º PLC å‘½ä»¤
                    ConveyorLineTaskCommandNew taskCommand = _mapper.Map<ConveyorLineTaskCommandNew>(task);
                    // ç»§æ‰¿ WCS_ACK æ ‡å¿—
                    taskCommand.WCS_ACK = command.WCS_ACK;
                    // å‘送命令到 PLC
                    conveyorLine.SendCommand(taskCommand, childDeviceCode);
                    // æ›´æ–°ä»»åŠ¡çŠ¶æ€åˆ°ä¸‹ä¸€é˜¶æ®µ
                    _taskService.UpdateTaskStatusToNext(task);
                }
            }
        }
        /// <summary>
        /// è¾“送线请求入库下一地址
        /// å¤„理输送线入库下一地址请求
        /// </summary>
        /// <remarks>
        /// å½“入库任务执行到某个中间站点时调用。
        /// æ ¹æ®ä¸‹ä¸€åœ°å€åˆ¤æ–­æ˜¯å¦éœ€è¦ä¸Žæ‹˜æŸæœº/插拔钉机等设备交互。
        /// </remarks>
        /// <param name="conveyorLine">输送线设备对象</param>
        /// <param name="command">PLC å‘½ä»¤æ•°æ®</param>
        /// <param name="childDeviceCode">子设备编码</param>
        public void RequestInNextAddress(CommonConveyorLine conveyorLine, ConveyorLineTaskCommandNew command, string childDeviceCode)
        {
            // æŸ¥è¯¢æ­£åœ¨æ‰§è¡Œçš„任务
            Dt_Task? task = _taskFilter.QueryExecutingTask(command.TaskNo, childDeviceCode);
            if (task == null)
            {
                return;
            }
            // å¦‚果不是空托盘任务,处理目标地址(与拘束机/插拔钉机交互)
            if (task.TaskType != (int)TaskOutboundTypeEnum.OutEmpty)
            {
                _targetAddressSelector.HandleInboundNextAddress(conveyorLine, task.NextAddress, childDeviceCode);
            }
            // æ›´æ–°ä»»åŠ¡å½“å‰ä½ç½®
            _ = _taskService.UpdatePosition(task.TaskNum, task.CurrentAddress);
            // è®¾ç½® WCS_STB æ ‡å¿—,表示 WCS å·²å¤„理
            conveyorLine.SetValue(ConveyorLineDBNameNew.WCS_STB, 1, childDeviceCode);
        }
        /// <summary>
        /// è¾“送线入库完成
        /// å¤„理输送线入库完成
        /// </summary>
        /// <remarks>
        /// å½“入库任务完成时调用。
        /// æ›´æ–°ä»»åŠ¡çŠ¶æ€å¹¶å›žå¤ PLC。
        /// </remarks>
        /// <param name="conveyorLine">输送线设备对象</param>
        /// <param name="command">PLC å‘½ä»¤æ•°æ®</param>
        /// <param name="childDeviceCode">子设备编码</param>
        public void ConveyorLineInFinish(CommonConveyorLine conveyorLine, ConveyorLineTaskCommandNew command, string childDeviceCode)
        {
            // æŸ¥è¯¢æ­£åœ¨æ‰§è¡Œçš„任务
            Dt_Task? task = _taskFilter.QueryExecutingTask(command.TaskNo, childDeviceCode);
            if (task != null)
            {
                // å›žå¤ ACK ç¡®è®¤
                conveyorLine.SetValue(ConveyorLineDBNameNew.WCS_ACK, 1, childDeviceCode);
                // æ›´æ–°ä»»åŠ¡çŠ¶æ€åˆ°ä¸‹ä¸€é˜¶æ®µï¼ˆé€šå¸¸æ˜¯å®Œæˆï¼‰
                WebResponseContent content = _taskService.UpdateTaskStatusToNext(task);
                Console.Out.WriteLine(content.Serialize());
            }
        }
        /// <summary>
        /// è¾“送线请求出信息
        /// å¤„理输送线出库请求
        /// </summary>
        /// <remarks>
        /// å½“ PLC è¯·æ±‚出库任务时调用。
        /// æµç¨‹ï¼š
        /// 1. æŸ¥è¯¢å¾…处理任务
        /// 2. ä¸‹å‘任务到 PLC
        /// 3. æ›´æ–°ä»»åŠ¡çŠ¶æ€
        /// </remarks>
        /// <param name="conveyorLine">输送线设备对象</param>
        /// <param name="command">PLC å‘½ä»¤æ•°æ®</param>
        /// <param name="childDeviceCode">子设备编码</param>
        public void RequestOutbound(CommonConveyorLine conveyorLine, ConveyorLineTaskCommandNew command, string childDeviceCode)
        {
            // æŸ¥è¯¢å¾…处理任务
            Dt_Task? task = _taskFilter.QueryPendingTask(conveyorLine.DeviceCode, childDeviceCode);
            if (task != null)
            {
                // è®¾ç½®ä»»åŠ¡å·
                conveyorLine.SetValue(ConveyorLineDBNameNew.TaskNo, task.TaskNum, childDeviceCode);
                // è®¾ç½®æ‰˜ç›˜æ¡ç 
                conveyorLine.SetValue(ConveyorLineDBNameNew.Barcode, task.PalletCode, childDeviceCode);
                // è®¾ç½®ç›®æ ‡åœ°å€
                conveyorLine.SetValue(ConveyorLineDBNameNew.Target, task.NextAddress, childDeviceCode);
                // å›žå¤ ACK ç¡®è®¤
                conveyorLine.SetValue(ConveyorLineDBNameNew.WCS_ACK, 1, childDeviceCode);
                // æ›´æ–°ä»»åŠ¡çŠ¶æ€
                _taskService.UpdateTaskStatusToNext(task);
            }
        }
        /// <summary>
        /// è¾“送线请求出库下一地址
        /// å¤„理输送线出库下一地址请求
        /// </summary>
        /// <remarks>
        /// å½“出库任务执行到某个中间站点时调用。
        /// æ ¹æ®ä¸‹ä¸€åœ°å€åˆ¤æ–­æ˜¯å¦éœ€è¦ä¸Žæ‹˜æŸæœº/插拔钉机等设备交互。
        /// </remarks>
        /// <param name="conveyorLine">输送线设备对象</param>
        /// <param name="command">PLC å‘½ä»¤æ•°æ®</param>
        /// <param name="childDeviceCode">子设备编码</param>
        public void RequestOutNextAddress(CommonConveyorLine conveyorLine, ConveyorLineTaskCommandNew command, string childDeviceCode)
        {
            // æŸ¥è¯¢æ­£åœ¨æ‰§è¡Œçš„任务
            Dt_Task? task = _taskFilter.QueryExecutingTask(command.TaskNo, childDeviceCode);
            if (task == null)
            {
                return;
            }
            // å¦‚果不是空托盘任务,处理目标地址
            if (task.TaskType != (int)TaskOutboundTypeEnum.OutEmpty)
            {
                _targetAddressSelector.HandleOutboundNextAddress(conveyorLine, task.NextAddress, childDeviceCode);
            }
            // æ›´æ–°ä»»åŠ¡å½“å‰ä½ç½®
            _ = _taskService.UpdatePosition(task.TaskNum, task.CurrentAddress);
            // å›žå¤ ACK ç¡®è®¤
            conveyorLine.SetValue(ConveyorLineDBNameNew.WCS_ACK, 1, childDeviceCode);
        }
        /// <summary>
        /// è¾“送线出库完成
        /// å¤„理输送线出库完成
        /// </summary>
        /// <remarks>
        /// å½“出库任务完成时调用。
        /// æ›´æ–°ä»»åŠ¡çŠ¶æ€å¹¶å›žå¤ PLC。
        /// </remarks>
        /// <param name="conveyorLine">输送线设备对象</param>
        /// <param name="command">PLC å‘½ä»¤æ•°æ®</param>
        /// <param name="childDeviceCode">子设备编码</param>
        public void ConveyorLineOutFinish(CommonConveyorLine conveyorLine, ConveyorLineTaskCommandNew command, string childDeviceCode)
        {
            // æŸ¥è¯¢æ­£åœ¨æ‰§è¡Œçš„任务
            Dt_Task? task = _taskFilter.QueryExecutingTask(command.TaskNo, childDeviceCode);
            if (task != null)
            {
                // å›žå¤ ACK ç¡®è®¤
                conveyorLine.SetValue(ConveyorLineDBNameNew.WCS_ACK, 1, childDeviceCode);
                // æ›´æ–°ä»»åŠ¡çŠ¶æ€åˆ°ä¸‹ä¸€é˜¶æ®µï¼ˆé€šå¸¸æ˜¯å®Œæˆï¼‰
                WebResponseContent content = _taskService.UpdateTaskStatusToNext(task);
                Console.Out.WriteLine(content.Serialize());
            }
        }
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/ConveyorLineTargetAddressSelector.cs
@@ -1,50 +1,121 @@
using WIDESEAWCS_QuartzJob;
using WIDESEAWCS_QuartzJob;
namespace WIDESEAWCS_Tasks
{
    /// <summary>
    /// è¾“送线设备请求处理器:处理拘束机/插拔钉机上下层请求。
    /// è¾“送线目标地址选择器 - å¤„理拘束机/插拔钉机的上下层请求
    /// </summary>
    /// <remarks>
    /// æ ¸å¿ƒèŒè´£ï¼š
    /// 1. å¤„理入库场景的目标地址选择
    /// 2. å¤„理出库场景的目标地址选择
    /// 3. åˆ¤æ–­æ‹˜æŸæœºå’Œæ’拔钉机的物料请求状态
    /// 4. åè°ƒè¾“送线与上下层设备之间的物料流转
    ///
    /// æ‹˜æŸæœºå’Œæ’拔钉机都有上下两层结构,
    /// æ¯å±‚都有独立的物料请求和出料信号,需要分别处理。
    /// </remarks>
    public class ConveyorLineTargetAddressSelector
    {
        /// <summary>
        /// æ‹˜æŸæœºåç§°å¸¸é‡
        /// </summary>
        private const string ConstraintMachineName = "拘束机";
        /// <summary>
        /// æ’拔钉机名称常量
        /// </summary>
        private const string PinMachineName = "插拔钉机";
        // æ‹˜æŸæœºç‚¹ä½
        /// <summary>
        /// æ‹˜æŸæœºå¯¹åº”的点位编码列表
        /// </summary>
        /// <remarks>
        /// å½“目标地址在这些编码中时,表示需要与拘束机交互。
        /// </remarks>
        private static readonly List<string> ConstraintMachineCodes = new List<string> { "10180", "20090" };
        // æ’拔钉机点位
        /// <summary>
        /// æ’拔钉机对应的点位编码列表
        /// </summary>
        /// <remarks>
        /// å½“目标地址在这些编码中时,表示需要与插拔钉机交互。
        /// </remarks>
        private static readonly List<string> PinMachineCodes = new List<string> { "10190", "20100" };
        /// <summary>
        /// å¤„理入库场景的下一地址请求
        /// </summary>
        /// <remarks>
        /// å½“入库任务执行到某个位置时调用此方法。
        /// åˆ¤æ–­ç›®æ ‡è®¾å¤‡æ˜¯å¦éœ€è¦ç‰©æ–™æˆ–可以出料。
        /// </remarks>
        /// <param name="conveyorLine">输送线设备对象</param>
        /// <param name="nextAddress">下一地址/目标设备编码</param>
        /// <param name="childDeviceCode">当前子设备编码</param>
        public void HandleInboundNextAddress(CommonConveyorLine conveyorLine, string nextAddress, string childDeviceCode)
        {
            // è°ƒç”¨é€šç”¨å¤„理方法,isUpper = true è¡¨ç¤ºå¤„理上层
            HandleDeviceRequest(conveyorLine, nextAddress, childDeviceCode, isUpper: true);
        }
        /// <summary>
        /// å¤„理出库场景的下一地址请求
        /// </summary>
        /// <remarks>
        /// å½“出库任务执行到某个位置时调用此方法。
        /// åˆ¤æ–­ç›®æ ‡è®¾å¤‡æ˜¯å¦éœ€è¦ç‰©æ–™æˆ–可以出料。
        /// </remarks>
        /// <param name="conveyorLine">输送线设备对象</param>
        /// <param name="nextAddress">下一地址/目标设备编码</param>
        /// <param name="childDeviceCode">当前子设备编码</param>
        public void HandleOutboundNextAddress(CommonConveyorLine conveyorLine, string nextAddress, string childDeviceCode)
        {
            // è°ƒç”¨é€šç”¨å¤„理方法,isUpper = false è¡¨ç¤ºå¤„理下层
            HandleDeviceRequest(conveyorLine, nextAddress, childDeviceCode, isUpper: false);
        }
        /// <summary>
        /// é€šç”¨è®¾å¤‡è¯·æ±‚处理方法
        /// </summary>
        /// <remarks>
        /// æ ¹æ®ç›®æ ‡åœ°å€ç±»åž‹ï¼ˆæ‹˜æŸæœº/插拔钉机)调用相应的处理逻辑。
        /// å¤„理上下层设备的物料请求和出料协调。
        /// </remarks>
        /// <param name="conveyorLine">输送线设备对象</param>
        /// <param name="nextAddress">下一地址/目标设备编码</param>
        /// <param name="childDeviceCode">当前子设备编码</param>
        /// <param name="isUpper">是否处理上层(true=上层,false=下层)</param>
        private void HandleDeviceRequest(CommonConveyorLine conveyorLine, string nextAddress, string childDeviceCode, bool isUpper)
        {
            // èŽ·å–å…¨å±€è®¾å¤‡åˆ—è¡¨
            var devices = Storage.Devices;
            // åˆ¤æ–­ç›®æ ‡è®¾å¤‡ç±»åž‹
            if (ConstraintMachineCodes.Contains(nextAddress))
            {
                // æ‹˜æŸæœºå¤„理分支
                // æŸ¥æ‰¾æ‹˜æŸæœºè®¾å¤‡
                ConstraintMachine? constraint = devices.OfType<ConstraintMachine>().FirstOrDefault(d => d.DeviceName == ConstraintMachineName);
                if (constraint == null)
                {
                    // æœªæ‰¾åˆ°æ‹˜æŸæœºè®¾å¤‡ï¼Œç›´æŽ¥è¿”回
                    return;
                }
                // å¤„理拘束机的请求
                ProcessDeviceRequest(
                    conveyorLine,
                    childDeviceCode,
                    // èŽ·å–ç‰©æ–™è¯·æ±‚æ ‡å¿—ï¼ˆä¸Šå±‚æˆ–ä¸‹å±‚ï¼‰
                    getMaterialRequest: () => isUpper
                        ? constraint.GetValue<ConstraintMachineDBName, short>(ConstraintMachineDBName.MaterialRequestUpper) != 0
                        : constraint.GetValue<ConstraintMachineDBName, short>(ConstraintMachineDBName.MaterialRequestLower) != 0,
                    // èŽ·å–å‡ºæ–™è¯·æ±‚æ ‡å¿—ï¼ˆä¸Šå±‚æˆ–ä¸‹å±‚ï¼‰
                    getOutputRequest: () => isUpper
                        ? constraint.GetValue<ConstraintMachineDBName, short>(ConstraintMachineDBName.OutputRequestUpper) != 0
                        : constraint.GetValue<ConstraintMachineDBName, short>(ConstraintMachineDBName.OutputRequestLower) != 0,
                    // è®¾ç½®è¾“出就绪标志(上层或下层)
                    setOutputReady: outputReq =>
                    {
                        if (isUpper)
@@ -59,21 +130,27 @@
            }
            else if (PinMachineCodes.Contains(nextAddress))
            {
                // æ’拔钉机处理分支
                // æŸ¥æ‰¾æ’拔钉机设备
                PinMachine? pinMachine = devices.OfType<PinMachine>().FirstOrDefault(d => d.DeviceName == PinMachineName);
                if (pinMachine == null)
                {
                    return;
                }
                // å¤„理插拔钉机的请求
                ProcessDeviceRequest(
                    conveyorLine,
                    childDeviceCode,
                    // èŽ·å–ç‰©æ–™è¯·æ±‚æ ‡å¿—ï¼ˆä¸Šå±‚æˆ–ä¸‹å±‚ï¼‰
                    getMaterialRequest: () => isUpper
                        ? pinMachine.GetValue<PinMachineDBName, short>(PinMachineDBName.MaterialRequestUpper) != 0
                        : pinMachine.GetValue<PinMachineDBName, short>(PinMachineDBName.MaterialRequestLower) != 0,
                    // èŽ·å–å‡ºæ–™è¯·æ±‚æ ‡å¿—ï¼ˆä¸Šå±‚æˆ–ä¸‹å±‚ï¼‰
                    getOutputRequest: () => isUpper
                        ? pinMachine.GetValue<PinMachineDBName, short>(PinMachineDBName.OutputRequestUpper) != 0
                        : pinMachine.GetValue<PinMachineDBName, short>(PinMachineDBName.OutputRequestLower) != 0,
                    // è®¾ç½®è¾“出就绪标志(上层或下层)
                    setOutputReady: outputReq =>
                    {
                        if (isUpper)
@@ -88,6 +165,19 @@
            }
        }
        /// <summary>
        /// å¤„理设备请求的核心逻辑
        /// </summary>
        /// <remarks>
        /// æ ¹æ®ç‰©æ–™è¯·æ±‚和出料请求的状态:
        /// - å¦‚果有物料请求,设置目标地址并发送 ACK
        /// - å¦‚果有出料请求,设置设备的输出就绪标志
        /// </remarks>
        /// <param name="conveyorLine">输送线设备对象</param>
        /// <param name="childDeviceCode">当前子设备编码</param>
        /// <param name="getMaterialRequest">获取物料请求状态的委托</param>
        /// <param name="getOutputRequest">获取出料请求状态的委托</param>
        /// <param name="setOutputReady">设置输出就绪标志的委托</param>
        private static void ProcessDeviceRequest(
            CommonConveyorLine conveyorLine,
            string childDeviceCode,
@@ -95,19 +185,27 @@
            Func<bool> getOutputRequest,
            Action<bool> setOutputReady)
        {
            // èŽ·å–ç‰©æ–™è¯·æ±‚çŠ¶æ€
            bool materialReq = getMaterialRequest();
            // èŽ·å–å‡ºæ–™è¯·æ±‚çŠ¶æ€
            bool outputReq = getOutputRequest();
            // å¦‚果设备需要物料
            if (materialReq)
            {
                // è®¾ç½®ç›®æ ‡åœ°å€ä¸º 1(表示有料进来)
                conveyorLine.SetValue(ConveyorLineDBNameNew.Target, 1, childDeviceCode);
                // å›žå¤ ACK ç¡®è®¤ä¿¡å·
                conveyorLine.SetValue(ConveyorLineDBNameNew.WCS_ACK, 1, childDeviceCode);
            }
            else
            {
                // è®¾å¤‡ä¸éœ€è¦ç‰©æ–™ï¼Œè®¾ç½®è¾“出就绪标志
                // é€šçŸ¥è®¾å¤‡å¯ä»¥ç»§ç»­å‡ºæ–™
                setOutputReady(outputReq);
            }
        }
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/ConveyorLineTaskFilter.cs
@@ -1,30 +1,79 @@
using WIDESEAWCS_ITaskInfoService;
using WIDESEAWCS_ITaskInfoService;
using WIDESEAWCS_Model.Models;
namespace WIDESEAWCS_Tasks
{
    /// <summary>
    /// è¾“送线任务访问器:统一封装任务查询与 WMS è¯·æ±‚。
    /// è¾“送线任务过滤器 - ç»Ÿä¸€å°è£…任务查询与 WMS è¯·æ±‚
    /// </summary>
    /// <remarks>
    /// æ ¸å¿ƒèŒè´£ï¼š
    /// 1. æŸ¥è¯¢è¾“送线的待处理任务
    /// 2. æŸ¥è¯¢æ­£åœ¨æ‰§è¡Œçš„任务
    /// 3. å‘ WMS è¯·æ±‚新任务
    ///
    /// è¯¥ç±»ä½œä¸ºä¸šåŠ¡å±‚ä¸Žä»»åŠ¡æœåŠ¡ä¹‹é—´çš„ä¸­é—´å±‚ï¼Œ
    /// å°è£…了常用的任务查询操作,提供简洁的接口给调用方。
    /// </remarks>
    public class ConveyorLineTaskFilter
    {
        /// <summary>
        /// ä»»åŠ¡æœåŠ¡å®žä¾‹
        /// </summary>
        /// <remarks>
        /// ç”¨äºŽè®¿é—®æ•°æ®åº“中的任务数据,以及与 WMS ç³»ç»Ÿäº¤äº’。
        /// </remarks>
        private readonly ITaskService _taskService;
        /// <summary>
        /// æž„造函数
        /// </summary>
        /// <param name="taskService">任务服务实例</param>
        public ConveyorLineTaskFilter(ITaskService taskService)
        {
            _taskService = taskService;
        }
        /// <summary>
        /// æŸ¥è¯¢å¾…处理的输送线任务
        /// </summary>
        /// <remarks>
        /// æ ¹æ®è®¾å¤‡ç¼–码和子设备编码查询状态为"待处理"的任务。
        /// ç”¨äºŽå½“ PLC è¯·æ±‚任务时,WCS å‘数据库查询可下发的任务。
        /// </remarks>
        /// <param name="deviceCode">设备编码(主设备)</param>
        /// <param name="childDeviceCode">子设备编码(位置编码)</param>
        /// <returns>待处理的任务对象,如果没有则返回 null</returns>
        public Dt_Task? QueryPendingTask(string deviceCode, string childDeviceCode)
        {
            return _taskService.QueryConveyorLineTask(deviceCode, childDeviceCode);
        }
        /// <summary>
        /// æŸ¥è¯¢æ­£åœ¨æ‰§è¡Œçš„输送线任务
        /// </summary>
        /// <remarks>
        /// æ ¹æ®ä»»åŠ¡å·å’Œå­è®¾å¤‡ç¼–ç æŸ¥è¯¢çŠ¶æ€ä¸º"执行中"的任务。
        /// ç”¨äºŽè·Ÿè¸ªä»»åŠ¡çš„æ‰§è¡Œè¿›åº¦ã€‚
        /// </remarks>
        /// <param name="taskNo">任务号</param>
        /// <param name="childDeviceCode">子设备编码</param>
        /// <returns>执行中的任务对象,如果没有则返回 null</returns>
        public Dt_Task? QueryExecutingTask(int taskNo, string childDeviceCode)
        {
            return _taskService.QueryExecutingConveyorLineTask(taskNo, childDeviceCode);
        }
        /// <summary>
        /// å‘ WMS è¯·æ±‚新任务
        /// </summary>
        /// <remarks>
        /// å½“输送线有货物到达时,向 WMS è¯·æ±‚新的任务。
        /// WMS ä¼šæ ¹æ®ä»“库情况和调度策略返回合适的任务。
        /// </remarks>
        /// <param name="barcode">货物/托盘条码</param>
        /// <param name="childDeviceCode">请求的子设备编码</param>
        /// <returns>请求是否成功</returns>
        public bool RequestWmsTask(string barcode, string childDeviceCode)
        {
            return _taskService.RequestWMSTask(barcode, childDeviceCode).Status;
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/PinMachine/PinMachineCommand.cs
@@ -1,52 +1,84 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WIDESEAWCS_QuartzJob.DeviceBase;
namespace WIDESEAWCS_Tasks
{
    /// <summary>
    /// æ’拔钉机 PLC é€šä¿¡å‘½ä»¤æ•°æ®ç±»
    /// </summary>
    /// <remarks>
    /// ç»§æ‰¿è‡ª DeviceCommand,用于与插拔钉机进行 PLC é€šä¿¡ã€‚
    /// åŒ…含设备的运行状态、物料请求、出料请求等信号。
    /// </remarks>
    public class PinMachineCommand : DeviceCommand
    {
        /// <summary>
        /// ç‰©æµçº¿è¿è¡Œä¿¡å·
        /// </summary>
        /// <remarks>
        /// è¡¨ç¤ºç‰©æµçº¿ï¼ˆè¿žæŽ¥æ’拔钉机的输送线)是否在运行。
        /// </remarks>
        public short LogisticsLineRunningSignal { get; set; }
        /// <summary>
        /// æ’拔钉机运行信号
        /// </summary>
        /// <remarks>
        /// è¡¨ç¤ºæ’拔钉机本身的运行状态。
        /// </remarks>
        public short PlugPinMachineRunningSignal { get; set; }
        /// <summary>
        /// è¦æ–™è¯·æ±‚-上层
        /// </summary>
        /// <remarks>
        /// ä¸Šå±‚工位向上级设备(如输送线)发出的原料请求。
        /// éžé›¶å€¼è¡¨ç¤ºéœ€è¦è¡¥å……物料。
        /// </remarks>
        public short MaterialRequestUpper { get; set; }
        /// <summary>
        /// å‡ºæ–™è¯·æ±‚-上层
        /// </summary>
        /// <remarks>
        /// ä¸Šå±‚工位完成加工后,向下级设备发出的出料请求。
        /// éžé›¶å€¼è¡¨ç¤ºæœ‰æ–™éœ€è¦è¾“出。
        /// </remarks>
        public short OutputRequestUpper { get; set; }
        /// <summary>
        /// æ’拔钉盘可出料-上层
        /// </summary>
        /// <remarks>
        /// WCS å›žå¤ç»™ä¸Šå±‚工位的确认信号。
        /// å‘ŠçŸ¥ä¸Šå±‚托盘已被接收,可以继续出料。
        /// </remarks>
        public short PlugPinTrayOutputReadyUpper { get; set; }
        /// <summary>
        /// è¦æ–™è¯·æ±‚-下层
        /// </summary>
        /// <remarks>
        /// ä¸‹å±‚工位向上级设备(如输送线)发出的原料请求。
        /// éžé›¶å€¼è¡¨ç¤ºéœ€è¦è¡¥å……物料。
        /// </remarks>
        public short MaterialRequestLower { get; set; }
        /// <summary>
        /// å‡ºæ–™è¯·æ±‚-下层
        /// </summary>
        /// <remarks>
        /// ä¸‹å±‚工位完成加工后,向下级设备发出的出料请求。
        /// éžé›¶å€¼è¡¨ç¤ºæœ‰æ–™éœ€è¦è¾“出。
        /// </remarks>
        public short OutputRequestLower { get; set; }
        /// <summary>
        /// æ’拔钉盘可出料-下层
        /// </summary>
        /// <remarks>
        /// WCS å›žå¤ç»™ä¸‹å±‚工位的确认信号。
        /// å‘ŠçŸ¥ä¸‹å±‚托盘已被接收,可以继续出料。
        /// </remarks>
        public short PlugPinTrayOutputReadyLower { get; set; }
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/PinMachine/PinMachineDBName.cs
@@ -1,45 +1,83 @@
namespace WIDESEAWCS_Tasks
namespace WIDESEAWCS_Tasks
{
    /// <summary>
    /// æ’拔钉机 PLC å¯„存器名称枚举
    /// </summary>
    /// <remarks>
    /// å®šä¹‰æ’拔钉机与 WCS é€šä¿¡æ—¶ä½¿ç”¨çš„ PLC å¯„存器地址名称。
    /// æ’拔钉机是一种用于电池生产的设备,负责插针和拔针操作。
    /// è®¾å¤‡æœ‰ä¸Šä¸‹ä¸¤å±‚结构,每层都有独立的物料请求和出料信号。
    /// </remarks>
    public enum PinMachineDBName
    {
        /// <summary>
        /// ç‰©æµçº¿è¿è¡Œä¿¡å·
        /// </summary>
        /// <remarks>
        /// è¡¨ç¤ºç‰©æµçº¿å½“前是否在运行。
        /// </remarks>
        LogisticsLineRunningSignal,
        /// <summary>
        /// æ’拔钉机运行信号
        /// </summary>
        /// <remarks>
        /// è¡¨ç¤ºæ’拔钉机本身是否在运行。
        /// </remarks>
        PlugPinMachineRunningSignal,
        /// <summary>
        /// è¦æ–™è¯·æ±‚-上层
        /// </summary>
        /// <remarks>
        /// ä¸Šå±‚工位需要原料的请求信号。
        /// å½“值为非零时,表示上层需要补充物料。
        /// </remarks>
        MaterialRequestUpper,
        /// <summary>
        /// å‡ºæ–™è¯·æ±‚-上层
        /// </summary>
        /// <remarks>
        /// ä¸Šå±‚工位完成加工后的出料请求信号。
        /// å½“值为非零时,表示上层有料需要输出。
        /// </remarks>
        OutputRequestUpper,
        /// <summary>
        /// æ’拔钉盘可出料-上层
        /// </summary>
        /// <remarks>
        /// WCS å†™å…¥çš„信号,告知上层托盘可以出料。
        /// å½“ WCS è®¾ç½®ä¸º 1 æ—¶ï¼Œè¡¨ç¤ºå…è®¸ä¸Šå±‚出料。
        /// </remarks>
        PlugPinTrayOutputReadyUpper,
        /// <summary>
        /// è¦æ–™è¯·æ±‚-下层
        /// </summary>
        /// <remarks>
        /// ä¸‹å±‚工位需要原料的请求信号。
        /// å½“值为非零时,表示下层需要补充物料。
        /// </remarks>
        MaterialRequestLower,
        /// <summary>
        /// å‡ºæ–™è¯·æ±‚-下层
        /// </summary>
        /// <remarks>
        /// ä¸‹å±‚工位完成加工后的出料请求信号。
        /// å½“值为非零时,表示下层有料需要输出。
        /// </remarks>
        OutputRequestLower,
        /// <summary>
        /// æ’拔钉盘可出料-下层
        /// </summary>
        /// <remarks>
        /// WCS å†™å…¥çš„信号,告知下层托盘可以出料。
        /// å½“ WCS è®¾ç½®ä¸º 1 æ—¶ï¼Œè¡¨ç¤ºå…è®¸ä¸‹å±‚出料。
        /// </remarks>
        PlugPinTrayOutputReadyLower
    }
}
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Abstractions/IRobotMessageRouter.cs
@@ -3,11 +3,18 @@
namespace WIDESEAWCS_Tasks.Workflow.Abstractions
{
    /// <summary>
    /// æœºå™¨äººæ¶ˆæ¯è·¯ç”±å…¥å£ã€‚用于承接 TcpSocketServer çš„æ¶ˆæ¯äº‹ä»¶ã€‚
    /// æœºå™¨äººæ¶ˆæ¯è·¯ç”±æŽ¥å£ - è´Ÿè´£æŽ¥æ”¶æ¥è‡ª TcpSocketServer çš„æ¶ˆæ¯å¹¶åˆ†å‘给合适的处理器
    /// </summary>
    public interface IRobotMessageRouter
    {
        /// <summary>
        /// å¤„理接收到的消息
        /// </summary>
        /// <param name="message">原始消息字符串</param>
        /// <param name="isJson">消息是否为 JSON æ ¼å¼</param>
        /// <param name="client">TCP å®¢æˆ·ç«¯è¿žæŽ¥</param>
        /// <param name="state">机器人当前状态</param>
        /// <returns>响应消息,如果无需回复则返回 null</returns>
        Task<string?> HandleMessageReceivedAsync(string message, bool isJson, TcpClient client, RobotSocketState state);
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Abstractions/IRobotPrefixCommandHandler.cs
@@ -1,14 +1,29 @@
using System.Net.Sockets;
using System.Net.Sockets;
namespace WIDESEAWCS_Tasks.Workflow.Abstractions
{
    /// <summary>
    /// æœºå™¨äººå‰ç¼€å‘½ä»¤å¤„理器(pickfinished / putfinished)。
    /// æœºå™¨äººå‰ç¼€å‘½ä»¤å¤„理器接口
    /// </summary>
    /// <remarks>
    /// å‰ç¼€å‘½ä»¤æ˜¯æŒ‡ä»¥ç‰¹å®šå‰ç¼€å¼€å¤´çš„命令,后面跟随逗号分隔的参数。
    /// å½“前支持:pickfinished(取货完成)、putfinished(放货完成)
    /// </remarks>
    public interface IRobotPrefixCommandHandler
    {
        /// <summary>
        /// æ£€æŸ¥æ¶ˆæ¯æ˜¯å¦ä¸ºå‰ç¼€å‘½ä»¤
        /// </summary>
        /// <param name="message">消息内容(通常是小写形式)</param>
        /// <returns>如果是前缀命令返回 true</returns>
        bool IsPrefixCommand(string message);
        /// <summary>
        /// å¤„理前缀命令
        /// </summary>
        /// <param name="message">原始消息内容</param>
        /// <param name="state">机器人当前状态</param>
        /// <param name="client">TCP å®¢æˆ·ç«¯è¿žæŽ¥ï¼Œç”¨äºŽå‘送响应</param>
        Task HandleAsync(string message, RobotSocketState state, TcpClient client);
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Abstractions/IRobotSimpleCommandHandler.cs
@@ -1,10 +1,20 @@
namespace WIDESEAWCS_Tasks.Workflow.Abstractions
namespace WIDESEAWCS_Tasks.Workflow.Abstractions
{
    /// <summary>
    /// æœºå™¨äººç®€å•命令处理器(如运行状态、模式切换、全流程完成命令)。
    /// æœºå™¨äººç®€å•命令处理器接口
    /// </summary>
    /// <remarks>
    /// ç®€å•命令是指不需要额外参数的状态更新命令,如运行状态、模式切换等。
    /// ä¸Žå‰ç¼€å‘½ä»¤ï¼ˆéœ€è¦è§£æžä½ç½®å‚数)相对。
    /// </remarks>
    public interface IRobotSimpleCommandHandler
    {
        /// <summary>
        /// å¤„理简单命令
        /// </summary>
        /// <param name="message">消息内容(小写形式)</param>
        /// <param name="state">机器人当前状态(会被修改)</param>
        /// <returns>是否成功处理;无法识别的命令返回 false</returns>
        Task<bool> HandleAsync(string message, RobotSocketState state);
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Abstractions/IRobotWorkflowOrchestrator.cs
@@ -3,11 +3,16 @@
namespace WIDESEAWCS_Tasks.Workflow.Abstractions
{
    /// <summary>
    /// æœºå™¨äººæµç¨‹ç¼–排器。负责 RobotJob å†…的状态机分支执行。
    /// æœºå™¨äººä»»åŠ¡ç¼–æŽ’å™¨æŽ¥å£ - è´Ÿè´£ RobotJob ä¸­çš„状态机流转和执行步骤编排
    /// </summary>
    public interface IRobotWorkflowOrchestrator
    {
        /// <summary>
        /// æ‰§è¡Œä»»åŠ¡ç¼–æŽ’æµç¨‹
        /// </summary>
        /// <param name="latestState">机器人最新状态</param>
        /// <param name="task">待执行的机器人任务</param>
        /// <param name="ipAddress">机器人 IP åœ°å€</param>
        Task ExecuteAsync(RobotSocketState latestState, Dt_RobotTask task, string ipAddress);
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Abstractions/ISocketClientGateway.cs
@@ -4,17 +4,51 @@
namespace WIDESEAWCS_Tasks.Workflow.Abstractions
{
    /// <summary>
    /// Socket å®¢æˆ·ç«¯ç½‘关。用于隔离 Robot ä¸šåС坹 TcpSocketServer çš„直接依赖。
    /// Socket å®¢æˆ·ç«¯ç½‘关接口 - å°è£… TcpSocketServer çš„访问,使业务层不直接依赖底层通信实现
    /// </summary>
    /// <remarks>
    /// è¯¥æŽ¥å£æ˜¯ä¸šåŠ¡å±‚ä¸Žåº•å±‚ TCP é€šä¿¡ä¹‹é—´çš„æŠ½è±¡å±‚。
    /// é€šè¿‡ä¾èµ–注入和使用接口,使上层代码不直接依赖 TcpSocketServer,
    /// ä¾¿äºŽåŽç»­æ›¿æ¢é€šä¿¡å®žçŽ°æˆ–è¿›è¡Œå•å…ƒæµ‹è¯•ã€‚
    /// </remarks>
    public interface ISocketClientGateway
    {
        /// <summary>
        /// å¼‚步发送消息到指定客户端
        /// </summary>
        /// <param name="clientId">目标客户端的 IP åœ°å€</param>
        /// <param name="message">要发送的消息内容</param>
        /// <returns>发送是否成功</returns>
        Task<bool> SendToClientAsync(string clientId, string message);
        /// <summary>
        /// é€šè¿‡ TcpClient å¯¹è±¡å‘送消息
        /// </summary>
        /// <remarks>
        /// ä¸Ž SendToClientAsync çš„区别:此方法直接使用 TcpClient å¯¹è±¡ï¼Œ
        /// é€‚用于需要回写响应给发送方的场景。
        /// </remarks>
        /// <param name="client">TCP å®¢æˆ·ç«¯è¿žæŽ¥å¯¹è±¡</param>
        /// <param name="message">要发送的消息内容</param>
        Task SendMessageAsync(TcpClient client, string message);
        /// <summary>
        /// èŽ·å–æ‰€æœ‰å·²è¿žæŽ¥å®¢æˆ·ç«¯çš„ ID åˆ—表
        /// </summary>
        /// <returns>客户端 IP åœ°å€åˆ—表</returns>
        IReadOnlyList<string> GetClientIds();
        /// <summary>
        /// å¼‚步处理客户端连接的消息循环
        /// </summary>
        /// <remarks>
        /// å¯åŠ¨åŽä¼šæŒç»­æŽ¥æ”¶å®¢æˆ·ç«¯æ¶ˆæ¯ï¼Œç›´åˆ°è¿žæŽ¥æ–­å¼€æˆ–å–æ¶ˆä»¤ç‰Œè¢«è§¦å‘ã€‚
        /// ç”¨äºŽç®¡ç†å•个客户端的生命周期。
        /// </remarks>
        /// <param name="client">TCP å®¢æˆ·ç«¯è¿žæŽ¥</param>
        /// <param name="clientId">客户端标识(通常是 IP åœ°å€ï¼‰</param>
        /// <param name="cancellationToken">取消令牌</param>
        /// <param name="robotCrane">机器人状态对象</param>
        Task HandleClientAsync(TcpClient client, string clientId, CancellationToken cancellationToken, RobotSocketState robotCrane);
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotBarcodeGenerator.cs
@@ -3,25 +3,33 @@
    /// <summary>
    /// æœºæ¢°æ‰‹æ¡ç ç”Ÿæˆå™¨ - è´Ÿè´£ç”Ÿæˆæ‰˜ç›˜æ¡ç 
    /// </summary>
    /// <remarks>
    /// æ¡ç æ ¼å¼ï¼šå‰ç¼€ï¼ˆå¯é€‰ï¼‰+ æ—¥æœŸï¼ˆyyyyMMdd)+ æ—¶é—´ï¼ˆHHmmss)+ éšæœºæ•°ï¼ˆ100-999)
    /// ä¾‹å¦‚:TRAY20260326093045123
    /// </remarks>
    public static class RobotBarcodeGenerator
    {
        /// <summary>
        /// ç”Ÿæˆæ‰˜ç›˜æ¡ç 
        /// </summary>
        /// <param name="prefix">条码前缀,默认为空</param>
        /// <returns>生成的条码字符串</returns>
        /// <param name="prefix">条码前缀,默认为空字符串,例如 "TRAY"</param>
        /// <returns>生成的条码字符串,格式:前缀+日期+时间+随机数</returns>
        public static string GenerateTrayBarcode(string prefix = "")
        {
            // å½“前日期
            // èŽ·å–å½“å‰æ—¥æœŸï¼Œæ ¼å¼åŒ–ä¸º yyyyMMdd(8位数字)
            // ä¾‹å¦‚:20260326
            string datePart = DateTime.Now.ToString("yyyyMMdd");
            // æ—¶é—´æˆ³ï¼ˆæ—¶åˆ†ç§’)
            // èŽ·å–å½“å‰æ—¶é—´ï¼ˆæ—¶åˆ†ç§’ï¼‰ï¼Œæ ¼å¼åŒ–ä¸º HHmmss(6位数字)
            // ä¾‹å¦‚:093045 è¡¨ç¤º 09:30:45
            string timePart = DateTime.Now.ToString("HHmmss");
            // éšæœºæ•°
            // ç”Ÿæˆ3位随机数,范围 100-999,确保条码唯一性
            // ä½¿ç”¨ Random.Shared èŽ·å–çº¿ç¨‹å®‰å…¨çš„éšæœºæ•°ç”Ÿæˆå™¨
            string randomPart = Random.Shared.Next(100, 1000).ToString();
            // ç»„合:前缀 + æ—¥æœŸ + æ—¶é—´ + éšæœºæ•°
            // ç»„合所有部分:前缀 + æ—¥æœŸ + æ—¶é—´ + éšæœºæ•°
            // å¦‚果前缀为空,则直接返回日期+时间+随机数的组合
            return prefix + datePart + timePart + randomPart;
        }
    }
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotClientManager.cs
@@ -1,24 +1,64 @@
using System.Collections.Concurrent;
using System.Net.Sockets;
using WIDESEAWCS_Core.LogHelper;
using WIDESEAWCS_QuartzJob;
using WIDESEAWCS_Tasks.SocketServer;
namespace WIDESEAWCS_Tasks
{
    /// <summary>
    /// æœºæ¢°æ‰‹å®¢æˆ·ç«¯è¿žæŽ¥ç®¡ç†å™¨ - è´Ÿè´£TCP客户端连接管理和事件订阅
    /// æœºæ¢°æ‰‹å®¢æˆ·ç«¯è¿žæŽ¥ç®¡ç†å™¨ - è´Ÿè´£ TCP å®¢æˆ·ç«¯è¿žæŽ¥ç®¡ç†å’Œäº‹ä»¶è®¢é˜…
    /// </summary>
    /// <remarks>
    /// æ ¸å¿ƒèŒè´£ï¼š
    /// 1. ç»´æŠ¤ä¸Žæœºæ¢°æ‰‹è®¾å¤‡çš„ TCP è¿žæŽ¥çŠ¶æ€
    /// 2. ç¡®ä¿æ¯ä¸ªå®¢æˆ·ç«¯åªå¯åŠ¨ä¸€æ¬¡æ¶ˆæ¯å¤„ç†å¾ªçŽ¯
    /// 3. ç®¡ç†å®¢æˆ·ç«¯è¿žæŽ¥/断开的生命周期事件
    /// 4. æä¾›å‘送消息到客户端的接口
    /// </remarks>
    public class RobotClientManager
    {
        /// <summary>
        /// TCP Socket æœåŠ¡å™¨å®žä¾‹ï¼Œç”¨äºŽç®¡ç†æ‰€æœ‰å®¢æˆ·ç«¯è¿žæŽ¥
        /// </summary>
        private readonly TcpSocketServer _tcpSocket;
        /// <summary>
        /// æœºæ¢°æ‰‹çŠ¶æ€ç®¡ç†å™¨ï¼Œç”¨äºŽè¯»å†™è®¾å¤‡çŠ¶æ€
        /// </summary>
        private readonly RobotStateManager _stateManager;
        // è·Ÿè¸ªå·²ç»å¯åЍ HandleClientAsync çš„客户端
        /// <summary>
        /// è·Ÿè¸ªå·²å¯åŠ¨æ¶ˆæ¯å¤„ç†çš„å®¢æˆ·ç«¯ï¼Œé¿å…é‡å¤å¯åŠ¨
        /// </summary>
        /// <remarks>
        /// Key: å®¢æˆ·ç«¯ IP åœ°å€
        /// Value: æ˜¯å¦å·²å¯åŠ¨ï¼ˆtrue è¡¨ç¤ºå·²å¯åŠ¨ï¼‰
        /// ä½¿ç”¨ ConcurrentDictionary ä¿è¯çº¿ç¨‹å®‰å…¨
        /// </remarks>
        private static readonly ConcurrentDictionary<string, bool> _handleClientStarted = new();
        /// <summary>
        /// äº‹ä»¶è®¢é˜…标志,确保 RobotReceived äº‹ä»¶åªè®¢é˜…一次
        /// </summary>
        /// <remarks>
        /// ä½¿ç”¨åŽŸå­æ“ä½œ Interlocked.CompareExchange ä¿è¯å…¨å±€åªè®¢é˜…一次
        /// </remarks>
        private static int _eventSubscribedFlag;
        /// <summary>
        /// å®¢æˆ·ç«¯æ–­å¼€è¿žæŽ¥æ—¶è§¦å‘的事件
        /// </summary>
        /// <remarks>
        /// äº‹ä»¶å‚数包含断开连接的机械手状态信息
        /// </remarks>
        public event EventHandler<RobotSocketState>? OnClientDisconnected;
        /// <summary>
        /// æž„造函数
        /// </summary>
        /// <param name="tcpSocket">TCP Socket æœåŠ¡å™¨å®žä¾‹</param>
        /// <param name="stateManager">状态管理器实例</param>
        public RobotClientManager(TcpSocketServer tcpSocket, RobotStateManager stateManager)
        {
            _tcpSocket = tcpSocket;
@@ -28,66 +68,88 @@
        /// <summary>
        /// ç¡®ä¿å®¢æˆ·ç«¯å·²è¿žæŽ¥å¹¶è®¢é˜…消息事件
        /// </summary>
        /// <param name="ipAddress">设备IP地址</param>
        /// <remarks>
        /// è¿™æ˜¯ RobotJob Execute æ–¹æ³•中的核心检查逻辑:
        /// 1. éªŒè¯å®¢æˆ·ç«¯æ˜¯å¦åœ¨çº¿
        /// 2. è®¢é˜…断开事件(全局只执行一次)
        /// 3. ç¡®ä¿æ¶ˆæ¯å¤„理循环已启动
        /// 4. é˜²æ­¢é‡å¤å¯åЍ HandleClientAsync
        /// </remarks>
        /// <param name="ipAddress">设备 IP åœ°å€</param>
        /// <param name="robotCrane">机器人设备信息</param>
        /// <returns>客户端是否可用(已连接且消息处理已启动)</returns>
        public bool EnsureClientSubscribed(string ipAddress, RobotCraneDevice robotCrane)
        {
            // æ£€æŸ¥æ˜¯å¦æœ‰è¯¥å®¢æˆ·ç«¯è¿žæŽ¥
            // ä»Ž TCP æœåŠ¡å™¨èŽ·å–æ‰€æœ‰å·²è¿žæŽ¥å®¢æˆ·ç«¯çš„ ID åˆ—表
            var clientIds = _tcpSocket.GetClientIds();
            // æ£€æŸ¥è¯¥ IP åœ°å€çš„客户端是否已连接
            bool isClientConnected = clientIds.Contains(ipAddress);
            // å¦‚果客户端未连接
            if (!isClientConnected)
            {
                // å®¢æˆ·ç«¯æœªè¿žæŽ¥ï¼Œæ¸…理 HandleClientAsync çŠ¶æ€
                // æ¸…理该客户端的 HandleClientAsync å¯åŠ¨æ ‡å¿—
                // ä»¥ä¾¿ä¸‹æ¬¡é‡è¿žæ—¶å¯ä»¥é‡æ–°å¯åŠ¨å¤„ç†
                _handleClientStarted.TryRemove(ipAddress, out _);
                return false;
            }
            // è®¢é˜…一次 robot äº‹ä»¶ï¼ˆå…¨å±€ä¸€æ¬¡ï¼‰- message事件由RobotJob订阅
            // è®¢é˜…一次 RobotReceived äº‹ä»¶ï¼ˆå…¨å±€åªè®¢é˜…一次)
            // ä½¿ç”¨ Interlocked.CompareExchange å®žçŽ°åŽŸå­æ“ä½œï¼Œç¡®ä¿çº¿ç¨‹å®‰å…¨
            if (System.Threading.Interlocked.CompareExchange(ref _eventSubscribedFlag, 1, 0) == 0)
            {
                // ç»‘定客户端断开连接的事件处理
                _tcpSocket.RobotReceived += OnRobotReceived;
                Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] æœºå™¨äººTCP断开事件已订阅");
                // è®°å½•日志(注意:日志内容为"客户端已断开连接",可能是遗留的占位文本)
                QuartzLogger.Error($"客户端已断开连接", robotCrane.DeviceName);
            }
            // èŽ·å–TcpClient
            // ä»Ž TCP æœåŠ¡å™¨çš„å®¢æˆ·ç«¯å­—å…¸ä¸­èŽ·å– TcpClient å¯¹è±¡
            TcpClient? tcpClient = null;
            _tcpSocket._clients.TryGetValue(ipAddress, out tcpClient);
            // å¦‚果获取失败(虽然 isClientConnected ä¸º true,但可能存在字典不同步的情况)
            if (tcpClient == null)
            {
                // isClientConnected为true但无法获取tcpClient,列表可能不同步
                // ç§»é™¤å¯åŠ¨æ ‡å¿—ï¼Œè¿”å›ž false è¡¨ç¤ºå®¢æˆ·ç«¯ä¸å¯ç”¨
                _handleClientStarted.TryRemove(ipAddress, out _);
                return false;
            }
            // æ£€æŸ¥æ˜¯å¦å·²ç»ä¸ºè¿™ä¸ªå®¢æˆ·ç«¯å¯åŠ¨è¿‡ HandleClientAsync
            // æ£€æŸ¥æ˜¯å¦å·²ç»ä¸ºè¿™ä¸ªå®¢æˆ·ç«¯å¯åŠ¨è¿‡æ¶ˆæ¯å¤„ç†å¾ªçŽ¯
            bool alreadyStarted = _handleClientStarted.TryGetValue(ipAddress, out _);
            // å¦‚果尚未启动,则启动消息处理循环
            if (!alreadyStarted)
            {
                Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] å¯åŠ¨å®¢æˆ·ç«¯æ¶ˆæ¯å¤„ç†: {ipAddress}");
                // è®°å½•日志
                QuartzLogger.Error($"启动客户端消息处理", robotCrane.DeviceName);
                // é‡æ–°èŽ·å–æœ€æ–°çš„ state å¯¹è±¡
                // èŽ·å–æœ€æ–°çš„çŠ¶æ€å¯¹è±¡
                var latestStateForSubscribe = _stateManager.GetState(ipAddress);
                if (latestStateForSubscribe != null)
                {
                    // æ ‡è®°ä¸ºå·²å¯åЍ
                    // æ ‡è®°ä¸ºå·²å¯åŠ¨ï¼Œé˜²æ­¢é‡å¤å¯åŠ¨
                    _handleClientStarted[ipAddress] = true;
                    // å¼‚步启动客户端消息处理循环
                    // ä½¿ç”¨ TaskContinuationOptions.OnlyOnFaulted æ•获异常情况
                    _ = _tcpSocket.HandleClientAsync(tcpClient, robotCrane.IPAddress, _tcpSocket._cts.Token, latestStateForSubscribe)
                        .ContinueWith(t =>
                        {
                            // å¦‚果处理出现异常
                            if (t.IsFaulted)
                            {
                                // è®°å½•错误日志
                                QuartzLogger.Error($"监听客户端消息事件异常", robotCrane.DeviceName);
                                Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] HandleClientAsync error: {t.Exception?.GetBaseException().Message}");
                                // å‘生错误时,移除启动标志,允许下次重试
                                _handleClientStarted.TryRemove(ipAddress, out _);
                            }
                        }, TaskContinuationOptions.OnlyOnFaulted);
                    // æ›´æ–° IsEventSubscribed çŠ¶æ€
                    // å®‰å…¨æ›´æ–°çŠ¶æ€ï¼Œæ ‡è®°ä¸ºå·²è®¢é˜…æ¶ˆæ¯äº‹ä»¶
                    _stateManager.TryUpdateStateSafely(ipAddress, s =>
                    {
                        s.IsEventSubscribed = true;
@@ -96,54 +158,81 @@
                }
            }
            // è¿”回 true è¡¨ç¤ºå®¢æˆ·ç«¯å¯ç”¨
            return true;
        }
        /// <summary>
        /// äº‹ä»¶ï¼šå®¢æˆ·ç«¯æ–­å¼€è¿žæŽ¥æ—¶è§¦å‘
        /// äº‹ä»¶å¤„理:客户端断开连接时调用
        /// </summary>
        /// <remarks>
        /// è§¦å‘时机:当 TCP æœåŠ¡å™¨æ£€æµ‹åˆ°å®¢æˆ·ç«¯æ–­å¼€è¿žæŽ¥æ—¶
        /// å¤„理逻辑:
        /// 1. æ¸…理 HandleClientAsync å¯åŠ¨æ ‡å¿—
        /// 2. é‡ç½®è®¾å¤‡çŠ¶æ€ï¼ˆå–æ¶ˆè®¢é˜…ã€æ¸…é™¤åŠ¨ä½œå’ŒçŠ¶æ€ï¼‰
        /// 3. è§¦å‘ OnClientDisconnected äº‹ä»¶é€šçŸ¥ä¸Šå±‚
        /// </remarks>
        /// <param name="clientId">断开连接的客户端 IP åœ°å€</param>
        /// <returns>固定返回 null,因为是事件处理器而非真正的消息处理器</returns>
        private Task<string?> OnRobotReceived(string clientId)
        {
            // å®¢æˆ·ç«¯æ–­å¼€è¿žæŽ¥ï¼Œæ¸…理 HandleClientAsync å¯åŠ¨æ ‡å¿—
            // ç§»é™¤è¯¥å®¢æˆ·ç«¯çš„ HandleClientAsync å¯åŠ¨æ ‡å¿—
            _handleClientStarted.TryRemove(clientId, out _);
            // é‡ç½®è¯¥å®¢æˆ·ç«¯çš„状态信息
            _stateManager.TryUpdateStateSafely(clientId, state =>
            {
                state.IsEventSubscribed = false;
                state.CurrentAction = "";
                state.OperStatus = "";
                state.RobotArmObject = 0;
                state.RobotControlMode = 0;
                state.RobotRunMode = 0;
                state.IsEventSubscribed = false;  // å–消订阅标志
                state.CurrentAction = "";         // æ¸…除当前动作
                state.OperStatus = "";             // æ¸…除运行状态
                state.RobotArmObject = 0;         // é‡ç½®æ‰‹è‡‚对象状态
                state.RobotControlMode = 0;       // é‡ç½®æŽ§åˆ¶æ¨¡å¼
                state.RobotRunMode = 0;           // é‡ç½®è¿è¡Œæ¨¡å¼
                return state;
            });
            // è§¦å‘断开连接事件
            // è§¦å‘客户端断开连接事件,通知上层(如 RobotJob)
            // ä½¿ç”¨ç©ºçš„状态对象作为后备(如果获取不到)
            OnClientDisconnected?.Invoke(this, _stateManager.GetState(clientId) ?? new RobotSocketState { IPAddress = clientId });
            // è¿”回 null,因为这是事件处理而非真正的消息路由
            return Task.FromResult<string?>(null);
        }
        /// <summary>
        /// æ£€æŸ¥å®¢æˆ·ç«¯æ˜¯å¦å·²è¿žæŽ¥
        /// </summary>
        /// <param name="ipAddress">设备 IP åœ°å€</param>
        /// <returns>如果已连接则返回 true</returns>
        public bool IsClientConnected(string ipAddress)
        {
            // èŽ·å–æ‰€æœ‰å·²è¿žæŽ¥å®¢æˆ·ç«¯çš„ ID åˆ—表
            var clientIds = _tcpSocket.GetClientIds();
            // æ£€æŸ¥åˆ—表中是否包含指定的 IP åœ°å€
            return clientIds.Contains(ipAddress);
        }
        /// <summary>
        /// å‘送消息到客户端
        /// </summary>
        /// <remarks>
        /// å°è£… TcpSocketServer çš„发送方法,提供更简洁的接口给业务层
        /// </remarks>
        /// <param name="ipAddress">目标客户端 IP åœ°å€</param>
        /// <param name="message">要发送的消息内容</param>
        /// <returns>发送是否成功</returns>
        public async Task<bool> SendToClientAsync(string ipAddress, string message)
        {
            return await _tcpSocket.SendToClientAsync(ipAddress, message);
        }
        /// <summary>
        /// èŽ·å–TcpSocketServer引用(用于RobotJob直接访问)
        /// èŽ·å– TcpSocketServer å¼•用
        /// </summary>
        /// <remarks>
        /// RobotJob å¯èƒ½éœ€è¦ç›´æŽ¥è®¿é—® TcpSocketServer è¿›è¡Œé…ç½®
        /// æ­¤å±žæ€§æä¾›åªè¯»è®¿é—®
        /// </remarks>
        public TcpSocketServer TcpSocket => _tcpSocket;
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotJob.cs
@@ -1,33 +1,112 @@
using Quartz;
using Microsoft.Extensions.Logging;
using Quartz;
using System.Net;
using WIDESEA_Core;
using WIDESEAWCS_Core.Caches;
using WIDESEAWCS_Core.Helper;
using WIDESEAWCS_Core.LogHelper;
using WIDESEAWCS_ITaskInfoService;
using WIDESEAWCS_QuartzJob;
using WIDESEAWCS_Tasks.Workflow.Abstractions;
using WIDESEAWCS_Tasks.Workflow;
using WIDESEAWCS_Tasks.SocketServer;
using Microsoft.Extensions.Logging;
using WIDESEAWCS_Tasks.Workflow;
using WIDESEAWCS_Tasks.Workflow.Abstractions;
namespace WIDESEAWCS_Tasks
{
    /// <summary>
    /// æœºå™¨äººä»»åŠ¡ä½œä¸šï¼šè´Ÿè´£è°ƒåº¦ä¸Žç”Ÿå‘½å‘¨æœŸç®¡ç†ï¼Œå…·ä½“çŠ¶æ€æœºæµç¨‹äº¤ç»™ç¼–æŽ’å™¨ã€‚
    /// æœºå™¨äººä»»åŠ¡ä½œä¸šï¼ˆQuartz Job)- è´Ÿè´£è°ƒåº¦ä¸Žç”Ÿå‘½å‘¨æœŸç®¡ç†
    /// </summary>
    /// <remarks>
    /// Quartz å®šæ—¶ä»»åŠ¡ï¼Œæ¯ç§’æ‰§è¡Œä¸€æ¬¡ï¼ˆé»˜è®¤ï¼‰ï¼Œä¸»è¦èŒè´£ï¼š
    /// 1. ä»Ž JobDataMap èŽ·å–è®¾å¤‡ä¿¡æ¯
    /// 2. ç¡®ä¿ TCP å®¢æˆ·ç«¯å·²è¿žæŽ¥å¹¶è®¢é˜…消息
    /// 3. è½®è¯¢å¾…处理的机器人任务
    /// 4. è°ƒç”¨å·¥ä½œæµç¼–排器执行任务状态机
    ///
    /// ä½¿ç”¨ [DisallowConcurrentExecution] ç¦æ­¢å¹¶å‘执行,确保同一设备的任务串行处理。
    /// å…·ä½“的状态机流程逻辑委托给 <see cref="RobotWorkflowOrchestrator"/> å¤„理。
    /// </remarks>
    [DisallowConcurrentExecution]
    public class RobotJob : IJob
    {
        /// <summary>
        /// ä»»åŠ¡æ€»æ•°ä¸Šé™
        /// </summary>
        /// <remarks>
        /// å½“机器人处理的货物数量达到此上限时,不再下发新任务。
        /// é˜²æ­¢æœºå™¨äººè¿‡åº¦åŠ³ç´¯æˆ–ç³»ç»Ÿè¿‡è½½ã€‚
        /// </remarks>
        private const int MaxTaskTotalNum = 48;
        /// <summary>
        /// æ¶ˆæ¯äº‹ä»¶è®¢é˜…标志
        /// </summary>
        /// <remarks>
        /// ä½¿ç”¨åŽŸå­æ“ä½œç¡®ä¿å…¨å±€åªè®¢é˜…ä¸€æ¬¡ TCP æ¶ˆæ¯äº‹ä»¶ã€‚
        /// é˜²æ­¢å¤šä¸ª Job å®žä¾‹é‡å¤è®¢é˜…导致消息被多次处理。
        /// </remarks>
        private static int _messageSubscribedFlag;
        /// <summary>
        /// æœºæ¢°æ‰‹å®¢æˆ·ç«¯è¿žæŽ¥ç®¡ç†å™¨
        /// </summary>
        /// <remarks>
        /// è´Ÿè´£ç®¡ç† TCP è¿žæŽ¥çš„生命周期,包括连接、断开、消息发送等。
        /// </remarks>
        private readonly RobotClientManager _clientManager;
        /// <summary>
        /// æœºæ¢°æ‰‹çŠ¶æ€ç®¡ç†å™¨
        /// </summary>
        /// <remarks>
        /// è´Ÿè´£ç®¡ç† Redis ç¼“存中的机械手状态,包括读写和并发控制。
        /// </remarks>
        private readonly RobotStateManager _stateManager;
        /// <summary>
        /// æ¶ˆæ¯è·¯ç”±å™¨
        /// </summary>
        /// <remarks>
        /// è´Ÿè´£å¤„理从 TCP æœåŠ¡å™¨æŽ¥æ”¶åˆ°çš„æ¶ˆæ¯ï¼Œå¹¶åˆ†å‘ç»™åˆé€‚çš„å¤„ç†å™¨ã€‚
        /// æ˜¯æ¶ˆæ¯å¤„理管道的入口。
        /// </remarks>
        private readonly IRobotMessageRouter _messageRouter;
        /// <summary>
        /// æœºå™¨äººä»»åŠ¡å¤„ç†å™¨
        /// </summary>
        /// <remarks>
        /// è´Ÿè´£ä»»åŠ¡çš„ä¸‹å‘ã€çŠ¶æ€æ›´æ–°ã€ä¸Ž WMS çš„交互等。
        /// </remarks>
        private readonly RobotTaskProcessor _taskProcessor;
        /// <summary>
        /// æœºå™¨äººå·¥ä½œæµç¼–排器
        /// </summary>
        /// <remarks>
        /// è´Ÿè´£æ‰§è¡Œä»»åŠ¡çš„çŠ¶æ€æœºæµè½¬ï¼Œæ ¹æ®å½“å‰çŠ¶æ€å†³å®šä¸‹ä¸€æ­¥åŠ¨ä½œã€‚
        /// è¿™æ˜¯æ ¸å¿ƒçš„业务逻辑编排组件。
        /// </remarks>
        private readonly IRobotWorkflowOrchestrator _workflowOrchestrator;
        /// <summary>
        /// æ—¥å¿—记录器
        /// </summary>
        private readonly ILogger<RobotJob> _logger;
        /// <summary>
        /// æž„造函数
        /// </summary>
        /// <remarks>
        /// é‡‡ç”¨ä¾èµ–注入方式获取所需服务,并完成组件的初始化和组装。
        /// è¿™é‡Œä½“现了"控制反转"和"依赖注入"的设计原则。
        /// </remarks>
        /// <param name="tcpSocket">TCP Socket æœåŠ¡å™¨å®žä¾‹</param>
        /// <param name="robotTaskService">机器人任务服务</param>
        /// <param name="taskService">通用任务服务</param>
        /// <param name="cache">缓存服务</param>
        /// <param name="httpClientHelper">HTTP å®¢æˆ·ç«¯å¸®åŠ©ç±»ï¼Œç”¨äºŽè°ƒç”¨ WMS æŽ¥å£</param>
        /// <param name="logger">日志记录器</param>
        public RobotJob(
            TcpSocketServer tcpSocket,
            IRobotTaskService robotTaskService,
@@ -36,75 +115,134 @@
            HttpClientHelper httpClientHelper,
            ILogger<RobotJob> logger)
        {
            // åˆå§‹åŒ–状态管理器,传入缓存服务
            _stateManager = new RobotStateManager(cache);
            _logger = logger;
            // æ”¶å£ Socket è®¿é—®ï¼ŒåŽç»­è‹¥æ›¿æ¢é€šä¿¡å®žçŽ°åªéœ€æ›¿æ¢ç½‘å…³å±‚ã€‚
            // åˆ›å»º Socket ç½‘关,封装 TcpSocketServer çš„访问
            // åŽç»­æ›¿æ¢é€šä¿¡å®žçŽ°æ—¶åªéœ€æ›¿æ¢ç½‘å…³å±‚
            ISocketClientGateway socketGateway = new SocketClientGateway(tcpSocket);
            // åˆå§‹åŒ–任务处理器
            _taskProcessor = new RobotTaskProcessor(socketGateway, _stateManager, robotTaskService, taskService, httpClientHelper);
            // åˆå§‹åŒ–客户端管理器
            _clientManager = new RobotClientManager(tcpSocket, _stateManager);
            // åˆå§‹åŒ–命令处理器
            // ç®€å•命令处理器:处理状态更新等简单命令
            var simpleCommandHandler = new RobotSimpleCommandHandler(_taskProcessor);
            // å‰ç¼€å‘½ä»¤å¤„理器:处理 pickfinished、putfinished ç­‰å¸¦å‚数的命令
            var prefixCommandHandler = new RobotPrefixCommandHandler(robotTaskService, _taskProcessor, _stateManager, socketGateway);
            // åˆå§‹åŒ–消息路由器
            _messageRouter = new RobotMessageHandler(socketGateway, _stateManager, cache, simpleCommandHandler, prefixCommandHandler, logger);
            // åˆå§‹åŒ–工作流编排器
            _workflowOrchestrator = new RobotWorkflowOrchestrator(_stateManager, _clientManager, _taskProcessor, robotTaskService);
            // è®¢é˜…客户端断开连接事件
            _clientManager.OnClientDisconnected += OnClientDisconnected;
            // å…¨å±€åªè®¢é˜…一次消息事件,保持原有行为。
            // å…¨å±€åªè®¢é˜…一次 TCP æ¶ˆæ¯äº‹ä»¶ï¼ˆä¿æŒåŽŸæœ‰è¡Œä¸ºï¼‰
            // ä½¿ç”¨ Interlocked.CompareExchange å®žçŽ°åŽŸå­æ“ä½œ
            if (System.Threading.Interlocked.CompareExchange(ref _messageSubscribedFlag, 1, 0) == 0)
            {
                // å°†æ¶ˆæ¯è·¯ç”±å™¨çš„处理方法绑定到 TCP æœåŠ¡å™¨çš„æ¶ˆæ¯æŽ¥æ”¶äº‹ä»¶
                tcpSocket.MessageReceived += _messageRouter.HandleMessageReceivedAsync;
                Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] æœºå™¨æ‰‹TCP消息事件已订阅");
                _logger.LogError("机器手TCP消息事件已订阅");
                QuartzLogger.Error($"机器手TCP消息事件已订阅");
            }
        }
        /// <summary>
        /// å®¢æˆ·ç«¯æ–­å¼€è¿žæŽ¥çš„事件处理
        /// </summary>
        /// <remarks>
        /// å½“客户端断开连接时记录日志,便于排查问题。
        /// </remarks>
        /// <param name="sender">事件发送者</param>
        /// <param name="state">断开连接的机械手状态</param>
        private void OnClientDisconnected(object? sender, RobotSocketState state)
        {
            Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] å®¢æˆ·ç«¯å·²æ–­å¼€è¿žæŽ¥: {state.IPAddress}");
            _logger.LogError("客户端已断开连接");
            QuartzLogger.Error($"客户端已断开连接", state.RobotCrane.DeviceName);
        }
        /// <summary>
        /// Quartz Job çš„æ‰§è¡Œå…¥å£
        /// </summary>
        /// <remarks>
        /// æ‰§è¡Œæµç¨‹ï¼š
        /// 1. ä»Ž JobDataMap èŽ·å–è®¾å¤‡ä¿¡æ¯
        /// 2. ç¡®ä¿å®¢æˆ·ç«¯å·²è¿žæŽ¥å¹¶è®¢é˜…消息
        /// 3. è½®è¯¢å¾…处理任务
        /// 4. è°ƒç”¨å·¥ä½œæµç¼–排器执行任务
        ///
        /// æ³¨æ„ï¼šæ­¤æ–¹æ³•可能频繁调用(每秒一次),需注意性能。
        /// </remarks>
        /// <param name="context">Quartz ä½œä¸šæ‰§è¡Œä¸Šä¸‹æ–‡ï¼ŒåŒ…含作业详情和数据</param>
        public async Task Execute(IJobExecutionContext context)
        {
            // ä»Ž JobDataMap ä¸­èŽ·å–ä½œä¸šå‚æ•°
            bool flag = context.JobDetail.JobDataMap.TryGetValue("JobParams", out object? value);
            // å°†å‚数转换为机器人设备信息
            RobotCraneDevice robotCrane = (RobotCraneDevice?)value ?? new RobotCraneDevice();
            // å¦‚果没有获取到有效的设备信息,直接返回
            if (!flag || robotCrane.IsNullOrEmpty())
            {
                return;
            }
            // èŽ·å–è®¾å¤‡ IP åœ°å€ï¼Œä½œä¸ºçŠ¶æ€ç¼“å­˜çš„é”®
            string ipAddress = robotCrane.IPAddress;
            // èŽ·å–æˆ–åˆ›å»ºè®¾å¤‡çŠ¶æ€å¯¹è±¡
            RobotSocketState state = _stateManager.GetOrCreateState(ipAddress, robotCrane);
            // æ›´æ–°è®¾å¤‡åŸºç¡€ä¿¡æ¯ï¼ˆä»¥é˜²è®¾å¤‡ä¿¡æ¯åœ¨è¿è¡ŒæœŸé—´å‘生变化)
            state.RobotCrane = robotCrane;
            try
            {
                // ç¡®ä¿å®¢æˆ·ç«¯å·²è¿žæŽ¥å¹¶è®¢é˜…消息事件
                // å¦‚果客户端未连接或订阅失败,直接返回等待下次调度
                if (!_clientManager.EnsureClientSubscribed(ipAddress, robotCrane))
                {
                    return;
                }
                // è½®è¯¢èŽ·å–è¯¥è®¾å¤‡çš„å¾…å¤„ç†ä»»åŠ¡
                var task = _taskProcessor.GetTask(robotCrane);
                // å¦‚果有待处理任务
                if (task != null)
                {
                    // èŽ·å–æœ€æ–°çš„è®¾å¤‡çŠ¶æ€
                    var latestState = _stateManager.GetState(ipAddress);
                    if (latestState == null)
                    {
                        // çŠ¶æ€ä¸å­˜åœ¨ï¼Œå¯èƒ½è®¾å¤‡æœªåˆå§‹åŒ–
                        return;
                    }
                    // æ£€æŸ¥ä»»åŠ¡æ€»æ•°æ˜¯å¦æœªè¾¾åˆ°ä¸Šé™
                    if (latestState.RobotTaskTotalNum < MaxTaskTotalNum)
                    {
                        // è°ƒç”¨å·¥ä½œæµç¼–排器执行任务
                        // ç¼–排器会根据当前状态决定下一步动作
                        await _workflowOrchestrator.ExecuteAsync(latestState, task, ipAddress);
                    }
                }
            }
            catch (Exception)
            catch (Exception ex)
            {
                // å¼‚常处理已在组件内部进行,Job å±‚保持兜底吞吐语义。
                // å¼‚常处理已在组件内部进行,Job å±‚保持兜底语义
                // è®°å½•异常而不是静默吞掉,便于排查问题
                _logger?.LogError(ex, "RobotJob执行异常,IP: {IpAddress}", ipAddress);
                QuartzLogger.Error($"RobotJob执行异常,IP: {ipAddress}", state.RobotCrane.DeviceName, ex);
            }
        }
    }
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotMessageHandler.cs
@@ -1,23 +1,82 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using System.Net;
using System.Net.Sockets;
using WIDESEAWCS_Common;
using WIDESEAWCS_Core.Caches;
using WIDESEAWCS_Core.LogHelper;
using WIDESEAWCS_Tasks.Workflow.Abstractions;
namespace WIDESEAWCS_Tasks
{
    /// <summary>
    /// æœºå™¨äººæ¶ˆæ¯è·¯ç”±å…¥å£ï¼šè´Ÿè´£ç¼“存状态读取、命令分发和回包触发。
    /// æœºå™¨äººæ¶ˆæ¯å¤„理器 - æ¶ˆæ¯è·¯ç”±å…¥å£
    /// </summary>
    /// <remarks>
    /// æ ¸å¿ƒèŒè´£ï¼š
    /// 1. ç¼“存状态读取:从 Redis ä¸­èŽ·å–æœºå™¨äººæœ€æ–°çš„çŠ¶æ€
    /// 2. å‘½ä»¤åˆ†å‘:根据消息类型分发给不同的处理器
    ///    - ç®€å•命令(如 homing、running):由 <see cref="IRobotSimpleCommandHandler"/> å¤„理
    ///    - å‰ç¼€å‘½ä»¤ï¼ˆå¦‚ pickfinished、putfinished):由 <see cref="IRobotPrefixCommandHandler"/> å¤„理
    /// 3. å›žåŒ…触发:将原始消息回写到客户端
    ///
    /// è¿™æ˜¯æ¶ˆæ¯å¤„理管道的入口点,由 TcpSocketServer çš„ MessageReceived äº‹ä»¶è§¦å‘。
    /// </remarks>
    public class RobotMessageHandler : IRobotMessageRouter
    {
        /// <summary>
        /// Socket å®¢æˆ·ç«¯ç½‘关接口
        /// </summary>
        /// <remarks>
        /// ç”¨äºŽå‘客户端发送响应消息。
        /// </remarks>
        private readonly ISocketClientGateway _socketClientGateway;
        /// <summary>
        /// æœºæ¢°æ‰‹çŠ¶æ€ç®¡ç†å™¨
        /// </summary>
        /// <remarks>
        /// ç”¨äºŽè¯»å–和更新机器人的状态。
        /// </remarks>
        private readonly RobotStateManager _stateManager;
        /// <summary>
        /// ç¼“存服务
        /// </summary>
        /// <remarks>
        /// ç›´æŽ¥ä½¿ç”¨ç¼“存服务检查状态是否存在。
        /// </remarks>
        private readonly ICacheService _cache;
        /// <summary>
        /// ç®€å•命令处理器
        /// </summary>
        /// <remarks>
        /// å¤„理简单的状态更新命令,如运行状态、模式切换等。
        /// </remarks>
        private readonly IRobotSimpleCommandHandler _simpleCommandHandler;
        /// <summary>
        /// å‰ç¼€å‘½ä»¤å¤„理器
        /// </summary>
        /// <remarks>
        /// å¤„理带参数的前缀命令,如 pickfinished(取货完成)、putfinished(放货完成)。
        /// </remarks>
        private readonly IRobotPrefixCommandHandler _prefixCommandHandler;
        /// <summary>
        /// æ—¥å¿—记录器
        /// </summary>
        private readonly ILogger<RobotJob> _logger;
        /// <summary>
        /// æž„造函数
        /// </summary>
        /// <param name="socketClientGateway">Socket ç½‘å…³</param>
        /// <param name="stateManager">状态管理器</param>
        /// <param name="cache">缓存服务</param>
        /// <param name="simpleCommandHandler">简单命令处理器</param>
        /// <param name="prefixCommandHandler">前缀命令处理器</param>
        /// <param name="logger">日志记录器</param>
        public RobotMessageHandler(
            ISocketClientGateway socketClientGateway,
            RobotStateManager stateManager,
@@ -35,32 +94,70 @@
        }
        /// <summary>
        /// å¤„理接收到的消息。保持原有行为:简单命令和前缀命令都回写原消息。
        /// å¤„理接收到的消息
        /// </summary>
        /// <remarks>
        /// å¤„理流程:
        /// 1. è®°å½•日志(记录原始消息内容)
        /// 2. éªŒè¯ç¼“存中是否存在该设备的状态
        /// 3. å°è¯•用简单命令处理器处理(状态更新类命令)
        ///    - å¦‚果处理成功,回写原消息并更新状态
        /// 4. å¦‚果不是简单命令,检查是否是前缀命令(pickfinished/putfinished)
        ///    - å¦‚果是,调用前缀命令处理器处理
        /// 5. ä¿æŒåŽŸæœ‰è¡Œä¸ºï¼šç®€å•å‘½ä»¤å’Œå‰ç¼€å‘½ä»¤éƒ½å›žå†™åŽŸæ¶ˆæ¯
        ///
        /// æ³¨æ„ï¼šæ­¤æ–¹æ³•可能在 TCP æ¶ˆæ¯æŽ¥æ”¶çš„上下文中被频繁调用,需注意性能。
        /// </remarks>
        /// <param name="message">原始消息字符串</param>
        /// <param name="isJson">消息是否为 JSON æ ¼å¼ï¼ˆå½“前未使用)</param>
        /// <param name="client">TCP å®¢æˆ·ç«¯è¿žæŽ¥</param>
        /// <param name="state">机器人当前状态</param>
        /// <returns>响应消息,如果无需回复则返回 null</returns>
        public async Task<string?> HandleMessageReceivedAsync(string message, bool isJson, TcpClient client, RobotSocketState state)
        {
            // è®°å½•接收到的消息日志
            _logger.LogInformation($"接收到客户端【{state.RobotCrane.DeviceName}】发送消息【{message}】");
            QuartzLogger.Error($"接收到客户端消息【{message}】", state.RobotCrane.DeviceName);
            // æž„建缓存键,检查 Redis ä¸­æ˜¯å¦å­˜åœ¨è¯¥è®¾å¤‡çš„状态
            var cacheKey = $"{RedisPrefix.Code}:{RedisName.SocketDevices}:{client.Client.RemoteEndPoint}";
            // å¦‚果缓存中不存在或状态为 null,忽略此消息
            if (!_cache.TryGetValue(cacheKey, out RobotSocketState? cachedState) || cachedState == null)
            {
                return null;
            }
            // ä½¿ç”¨ç¼“存中获取的状态
            var activeState = cachedState;
            // å°†æ¶ˆæ¯è½¬æ¢ä¸ºå°å†™ï¼ˆç”¨äºŽç®€å•命令匹配)
            string messageLower = message.ToLowerInvariant();
            // å°è¯•用简单命令处理器处理
            // ç®€å•命令包括:homing、homed、running、pausing、runmode、controlmode ç­‰
            if (await _simpleCommandHandler.HandleAsync(messageLower, activeState))
            {
                // å¤„理成功后,将原消息回写到客户端(保持原有行为)
                await _socketClientGateway.SendMessageAsync(client, message);
                _logger.LogInformation($"发送消息【{message}】");
                QuartzLogger.Error($"发送消息:【{message}】", state.RobotCrane.DeviceName);
                // å®‰å…¨æ›´æ–°çŠ¶æ€åˆ° Redis
                _stateManager.TryUpdateStateSafely(activeState.IPAddress, activeState);
                return null;
            }
            // å¦‚果不是简单命令,检查是否是前缀命令
            // å‰ç¼€å‘½ä»¤åŒ…括:pickfinished、putfinished(后面跟逗号分隔的位置参数)
            if (_prefixCommandHandler.IsPrefixCommand(messageLower))
            {
                // è°ƒç”¨å‰ç¼€å‘½ä»¤å¤„理器
                // å‰ç¼€å‘½ä»¤å¤„理器会解析位置参数并更新状态
                await _prefixCommandHandler.HandleAsync(message, activeState, client);
            }
            // é»˜è®¤è¿”回 null,不产生响应消息
            return null;
        }
    }
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotSocketState.cs
@@ -4,86 +4,164 @@
namespace WIDESEAWCS_Tasks
{
    /// <summary>
    /// æœºæ¢°æ‰‹Socket通信状态
    /// æœºæ¢°æ‰‹ Socket é€šä¿¡çŠ¶æ€æ•°æ®ç±»
    /// </summary>
    /// <remarks>
    /// è¯¥ç±»ç”¨äºŽåœ¨ Redis ç¼“存中存储机械手的实时状态,包括位置、任务、手臂对象等信息。
    /// æ‰€æœ‰å±žæ€§å‡è®¾è®¡ä¸ºçº¿ç¨‹å®‰å…¨æ›´æ–°ï¼Œé€šè¿‡ <see cref="RobotStateManager"/> çš„版本控制机制来防止并发覆盖。
    /// </remarks>
    public class RobotSocketState
    {
        /// <summary>
        /// æœºæ¢°æ‰‹çš„ IP åœ°å€ï¼Œä½œä¸ºç¼“存键的唯一标识
        /// </summary>
        public string IPAddress { get; set; } = string.Empty;
        /// <summary>
        /// ç‰ˆæœ¬å·ï¼Œç”¨äºŽé˜²æ­¢å¹¶å‘更新时旧值覆盖新值
        /// æ¯æ¬¡ä¿®æ”¹çŠ¶æ€æ—¶éƒ½åº”è¯¥æ›´æ–°æ­¤å€¼
        /// ç‰ˆæœ¬å·ï¼Œç”¨äºŽä¹è§‚并发控制
        /// </summary>
        /// <remarks>
        /// æ¯æ¬¡ä¿®æ”¹çŠ¶æ€æ—¶æ›´æ–°ä¸º DateTime.UtcNow.Ticks。
        /// <see cref="RobotStateManager"/> ä½¿ç”¨æ­¤å­—段实现乐观锁,防止并发更新时旧值覆盖新值。
        /// </remarks>
        public long Version { get; set; } = DateTime.UtcNow.Ticks;
        /// <summary>
        /// æ˜¯å¦å·²è®¢é˜…消息事件
        /// æ˜¯å¦å·²è®¢é˜…消息事件标志
        /// </summary>
        /// <remarks>
        /// ç¡®ä¿æ¯ä¸ªå®¢æˆ·ç«¯åªå¯åŠ¨ä¸€æ¬¡æ¶ˆæ¯å¤„ç†å¾ªçŽ¯ï¼Œé¿å…é‡å¤è®¢é˜…å¯¼è‡´çš„æ¶ˆæ¯é‡å¤å¤„ç†ã€‚
        /// </remarks>
        public bool IsEventSubscribed { get; set; }
        /// <summary>
        /// æœºæ¢°æ‰‹è¿è¡Œæ¨¡å¼
        /// </summary>
        /// <remarks>
        /// 1: æ‰‹åŠ¨æ¨¡å¼
        /// 2: è‡ªåŠ¨æ¨¡å¼
        /// å½“ RobotRunMode == 2 ä¸” RobotControlMode == 1 æ—¶ï¼Œç³»ç»Ÿè¿›å…¥è‡ªåŠ¨æŽ§åˆ¶çŠ¶æ€ã€‚
        /// </remarks>
        public int? RobotRunMode { get; set; }
        /// <summary>
        /// æœºæ¢°æ‰‹æŽ§åˆ¶æ¨¡å¼
        /// </summary>
        /// <remarks>
        /// 1: å®¢æˆ·ç«¯æŽ§åˆ¶
        /// 2: æœªçŸ¥/其他
        /// ä¸Ž RobotRunMode é…åˆåˆ¤æ–­æœºå™¨äººçš„当前控制状态。
        /// </remarks>
        public int? RobotControlMode { get; set; }
        /// <summary>
        /// æœºæ¢°æ‰‹æ˜¯å¦æŠ“取物料,0-无物料,1-有物料
        /// æœºæ¢°æ‰‹æ‰‹è‡‚抓取对象状态
        /// </summary>
        /// <remarks>
        /// 0: æ— ç‰©æ–™ï¼ˆæ‰‹è‡‚空闲)
        /// 1: æœ‰ç‰©æ–™ï¼ˆå·²æŠ“取货物)
        /// ç”¨äºŽåˆ¤æ–­æœºå™¨äººæ˜¯å¦å¯ä»¥æ‰§è¡Œä¸‹ä¸€æ­¥åŠ¨ä½œã€‚
        /// </remarks>
        public int? RobotArmObject { get; set; }
        /// <summary>
        /// æœºæ¢°æ‰‹è®¾å¤‡ä¿¡æ¯
        /// æœºæ¢°æ‰‹è®¾å¤‡åŸºç¡€ä¿¡æ¯
        /// </summary>
        /// <remarks>
        /// åŒ…含设备的 DeviceCode、DeviceName、IPAddress ç­‰åŸºç¡€ä¿¡æ¯ã€‚
        /// åœ¨çŠ¶æ€åˆå§‹åŒ–æ—¶ä»Ž JobDetail.JobDataMap èŽ·å–å¹¶ç¼“å­˜ã€‚
        /// </remarks>
        public RobotCraneDevice? RobotCrane { get; set; }
        /// <summary>
        /// å½“前动作
        /// æœºæ¢°æ‰‹å½“前正在执行的动作
        /// </summary>
        /// <remarks>
        /// å¯èƒ½çš„值:
        /// - "Picking": å–货中
        /// - "Putting": æ”¾è´§ä¸­
        /// - "PickFinished": å–货完成
        /// - "PutFinished": æ”¾è´§å®Œæˆ
        /// - "AllPickFinished": å…¨éƒ¨å–货完成
        /// - "AllPutFinished": å…¨éƒ¨æ”¾è´§å®Œæˆ
        /// </remarks>
        public string? CurrentAction { get; set; }
        /// <summary>
        /// å½“前状态
        /// æœºæ¢°æ‰‹å½“前运行状态
        /// </summary>
        /// <remarks>
        /// å¯èƒ½çš„值:
        /// - "Homing": å›žé›¶ä¸­
        /// - "Homed": å·²å›žé›¶
        /// - "Running": è¿è¡Œä¸­
        /// - "Pausing": æš‚停中
        /// - "Warming": é¢„热中
        /// - "Emstoping": æ€¥åœä¸­
        /// </remarks>
        public string? OperStatus { get; set; }
        /// <summary>
        /// å–货完成位置
        /// æœ€è¿‘一次取货完成的位置数组
        /// </summary>
        /// <remarks>
        /// æ•°ç»„中的每个元素代表一个电池位置编号。
        /// ç”¨äºŽè®°å½•取货动作涉及的货位,供后续组盘/拆盘操作使用。
        /// </remarks>
        public int[]? LastPickPositions { get; set; }
        /// <summary>
        /// æ”¾è´§å®Œæˆä½ç½®
        /// æœ€è¿‘一次放货完成的位置数组
        /// </summary>
        /// <remarks>
        /// æ•°ç»„中的每个元素代表一个电池位置编号。
        /// ç”¨äºŽè®°å½•放货动作涉及的货位。
        /// </remarks>
        public int[]? LastPutPositions { get; set; }
        /// <summary>
        /// æŠ“取位置条码
        /// ç”µæ± /货位条码列表
        /// </summary>
        /// <remarks>
        /// åœ¨ç»„盘操作时用于记录生成的托盘条码。
        /// æ¯ä¸ªæ¡ç æ ¼å¼ä¸º "TRAY" + æ—¥æœŸ + æ—¶é—´ + éšæœºæ•°ã€‚
        /// </remarks>
        public List<string> CellBarcode { get; set; } = new List<string>();
        /// <summary>
        /// å½“前抓取任务
        /// æœºæ¢°æ‰‹å½“前正在执行的任务
        /// </summary>
        /// <remarks>
        /// å½“任务下发到机器人后,该字段保存任务详情。
        /// ä»»åŠ¡ç±»åž‹åŒ…æ‹¬ï¼šç»„ç›˜(500)、换盘(510)、拆盘(520)。
        /// </remarks>
        public Dt_RobotTask? CurrentTask { get; set; }
        /// <summary>
        /// æ˜¯å¦éœ€è¦æ‹†ç›˜
        /// æ˜¯å¦éœ€è¦æ‰§è¡Œæ‹†ç›˜ä»»åŠ¡
        /// </summary>
        /// <remarks>
        /// å½“任务类型为 SplitPallet (520) æ—¶è®¾ä¸º true。
        /// æ‹†ç›˜ä»»åŠ¡å°†ç”µæ± ä»Žæ‰˜ç›˜ä¸Šå–ä¸‹å¹¶é€ä¸ªæ”¾ç½®åˆ°ç›®æ ‡ä½ç½®ã€‚
        /// </remarks>
        public bool IsSplitPallet { get; set; }
        /// <summary>
        /// æ˜¯å¦éœ€è¦ç»„盘
        /// æ˜¯å¦éœ€è¦æ‰§è¡Œç»„盘任务
        /// </summary>
        /// <remarks>
        /// å½“任务类型为 GroupPallet (500) æˆ– ChangePallet (510) æ—¶è®¾ä¸º true。
        /// ç»„盘任务将多个电池组合到同一个托盘上。
        /// </remarks>
        public bool IsGroupPallet { get; set; }
        /// <summary>
        /// ä»»åŠ¡æ€»æ•°
        /// æœºå™¨äººå·²å¤„理的任务总数
        /// </summary>
        /// <remarks>
        /// ç´¯è®¡è®°å½•机器人已完成处理的电池/货物数量。
        /// å½“达到 MaxTaskTotalNum (48) æ—¶ï¼Œä¸å†ä¸‹å‘新任务。
        /// </remarks>
        public int RobotTaskTotalNum { get; set; }
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotStateManager.cs
@@ -6,74 +6,110 @@
namespace WIDESEAWCS_Tasks
{
    /// <summary>
    /// æœºæ¢°æ‰‹çŠ¶æ€ç®¡ç†å™¨ - è´Ÿè´£RobotSocketState的安全更新和克隆
    /// æœºæ¢°æ‰‹çŠ¶æ€ç®¡ç†å™¨ - è´Ÿè´£ RobotSocketState çš„线程安全更新和克隆
    /// </summary>
    /// <remarks>
    /// æ ¸å¿ƒåŠŸèƒ½æ˜¯é€šè¿‡ç¼“å­˜æœåŠ¡ï¼ˆICacheService)管理 Redis ä¸­çš„æœºæ¢°æ‰‹çŠ¶æ€ã€‚
    /// æä¾›ä¹è§‚并发控制,通过版本号(Version)字段防止并发更新时的数据覆盖问题。
    /// </remarks>
    public class RobotStateManager
    {
        /// <summary>
        /// ç¼“存服务实例,用于读写 Redis ä¸­çš„状态数据
        /// </summary>
        private readonly ICacheService _cache;
        /// <summary>
        /// æž„造函数
        /// </summary>
        /// <param name="cache">缓存服务实例(通常为 HybridCacheService)</param>
        public RobotStateManager(ICacheService cache)
        {
            _cache = cache;
        }
        /// <summary>
        /// å®‰å…¨æ›´æ–°RobotSocketState缓存,防止并发覆盖
        /// å®‰å…¨æ›´æ–° RobotSocketState ç¼“存,防止并发覆盖
        /// </summary>
        /// <param name="ipAddress">设备IP地址</param>
        /// <param name="updateAction">更新状态的委托(传入当前状态,返回修改后的新状态)</param>
        /// <returns>是否更新成功</returns>
        /// <remarks>
        /// ä½¿ç”¨ä¹è§‚并发模式:先读取当前版本号,执行更新时检查版本是否一致。
        /// å¦‚果版本不匹配(说明有其他线程已更新),则更新失败返回 false。
        /// </remarks>
        /// <param name="ipAddress">设备 IP åœ°å€ï¼Œç”¨äºŽæž„建缓存键</param>
        /// <param name="updateAction">更新状态的委托函数,传入当前状态副本,返回修改后的新状态</param>
        /// <returns>是否更新成功;false è¡¨ç¤ºç‰ˆæœ¬å†²çªæˆ–状态不存在</returns>
        public bool TryUpdateStateSafely(string ipAddress, Func<RobotSocketState, RobotSocketState> updateAction)
        {
            // æž„建 Redis ç¼“存键,格式:{RedisPrefix.Code}:{RedisName.SocketDevices}:{ipAddress}
            var cacheKey = GetCacheKey(ipAddress);
            // ä»Žç¼“存获取当前存储的状态
            var currentState = _cache.Get<RobotSocketState>(cacheKey);
            // å¦‚果缓存中不存在该设备的状态,直接返回 false(应由 GetOrCreateState å…ˆåˆ›å»ºï¼‰
            if (currentState == null)
            {
                return false;
            }
            // ä½¿ç”¨å½“前存储的版本号作为期望版本
            // è®°å½•当前存储的版本号,作为更新时的期望版本
            var expectedVersion = currentState.Version;
            // åˆ›å»ºçŠ¶æ€å‰¯æœ¬è¿›è¡Œä¿®æ”¹ï¼ˆé¿å…ä¿®æ”¹åŽŸå¯¹è±¡å¼•ç”¨ï¼‰
            // åˆ›å»ºçŠ¶æ€çš„æ·±æ‹·è´å‰¯æœ¬ï¼Œé¿å…ç›´æŽ¥ä¿®æ”¹åŽŸå¯¹è±¡å¼•ç”¨
            // è¿™æ ·å¯ä»¥ç¡®ä¿åœ¨å¤šçº¿ç¨‹çŽ¯å¢ƒä¸‹ï¼Œæ¯ä¸ªçº¿ç¨‹æ“ä½œçš„æ˜¯ç‹¬ç«‹çš„çŠ¶æ€å‰¯æœ¬
            var stateCopy = CloneState(currentState);
            // æ‰§è¡Œè°ƒç”¨è€…提供的更新逻辑,传入副本状态,获取新的状态对象
            var newState = updateAction(stateCopy);
            // å°†æ–°çŠ¶æ€çš„ç‰ˆæœ¬å·æ›´æ–°ä¸ºæœ€æ–°çš„æ—¶é—´æˆ³ï¼Œè¡¨ç¤ºæ•°æ®å·²æ›´æ–°
            newState.Version = DateTime.UtcNow.Ticks;
            // è°ƒç”¨ç¼“存服务的安全更新方法,传入期望版本和版本提取器
            // å¦‚果当前版本与期望版本不一致(已被其他线程更新),则更新失败
            return _cache.TrySafeUpdate(
                cacheKey,
                newState,
                expectedVersion,
                s => s.Version
                s => s.Version  // æŒ‡å®šå“ªä¸ªå­—段作为版本号
            );
        }
        /// <summary>
        /// å®‰å…¨æ›´æ–°RobotSocketState缓存(简单版本)
        /// å®‰å…¨æ›´æ–° RobotSocketState ç¼“存的重载版本(直接传入新状态)
        /// </summary>
        /// <param name="ipAddress">设备IP地址</param>
        /// <param name="newState">新状态(会被更新Version字段)</param>
        /// <returns>是否更新成功</returns>
        /// <remarks>
        /// ä¸Žä¸Šä¸€ä¸ªé‡è½½çš„区别:此方法直接接收完整的新状态对象,而不是更新委托。
        /// å¦‚果设备状态不存在于缓存中,则直接添加新状态。
        /// </remarks>
        /// <param name="ipAddress">设备 IP åœ°å€ï¼Œç”¨äºŽæž„建缓存键</param>
        /// <param name="newState">新状态对象(方法内部会更新其 Version å­—段)</param>
        /// <returns>是否更新成功;新建设置为 true</returns>
        public bool TryUpdateStateSafely(string ipAddress, RobotSocketState newState)
        {
            // æž„建 Redis ç¼“存键
            var cacheKey = GetCacheKey(ipAddress);
            // ä»Žç¼“存获取当前存储的状态
            var currentState = _cache.Get<RobotSocketState>(cacheKey);
            // å¦‚果当前不存在该设备的状态
            if (currentState == null)
            {
                // å½“前不存在,直接添加
                // ä¸ºæ–°çŠ¶æ€è®¾ç½®ç‰ˆæœ¬å·ï¼ˆæ—¶é—´æˆ³ï¼‰
                newState.Version = DateTime.UtcNow.Ticks;
                // ç›´æŽ¥æ·»åŠ åˆ°ç¼“å­˜
                _cache.AddObject(cacheKey, newState);
                return true;
            }
            // ä½¿ç”¨å½“前存储的版本号作为期望版本
            // å½“前存在状态,记录期望版本号用于乐观锁检查
            var expectedVersion = currentState.Version;
            // æ›´æ–°æ–°çŠ¶æ€çš„ç‰ˆæœ¬å·ä¸ºæœ€æ–°æ—¶é—´æˆ³
            newState.Version = DateTime.UtcNow.Ticks;
            // å°è¯•安全更新,如果版本冲突则返回 false
            return _cache.TrySafeUpdate(
                cacheKey,
                newState,
@@ -83,40 +119,64 @@
        }
        /// <summary>
        /// å…‹éš†RobotSocketState对象(创建深拷贝)
        /// å…‹éš† RobotSocketState å¯¹è±¡ï¼ˆæ·±æ‹·è´ï¼‰
        /// </summary>
        /// <remarks>
        /// ä½¿ç”¨ JSON åºåˆ—化/反序列化实现深拷贝。
        /// è¿™æ ·å¯ä»¥ç¡®ä¿æ–°å¯¹è±¡ä¸ŽåŽŸå¯¹è±¡å®Œå…¨ç‹¬ç«‹ï¼Œä¿®æ”¹æ–°å¯¹è±¡ä¸ä¼šå½±å“åŽŸå¯¹è±¡ã€‚
        /// </remarks>
        /// <param name="source">源状态对象</param>
        /// <returns>新的状态对象,是源对象的深拷贝</returns>
        public RobotSocketState CloneState(RobotSocketState source)
        {
            // ä½¿ç”¨åºåˆ—化/反序列化进行深拷贝
            // å°†æºå¯¹è±¡åºåˆ—化为 JSON å­—符串
            var json = JsonConvert.SerializeObject(source);
            // ååºåˆ—化为新的 RobotSocketState å¯¹è±¡
            // å¦‚果反序列化失败(返回 null),创建一个新对象并复制 IPAddress
            return JsonConvert.DeserializeObject<RobotSocketState>(json) ?? new RobotSocketState { IPAddress = source.IPAddress };
        }
        /// <summary>
        /// èŽ·å–Redis缓存键
        /// èŽ·å– Redis ç¼“存键
        /// </summary>
        /// <remarks>
        /// ç¼“存键格式:{RedisPrefix.Code}:{RedisName.SocketDevices}:{ipAddress}
        /// ä¾‹å¦‚:Code:SocketDevices:192.168.1.100
        /// </remarks>
        /// <param name="ipAddress">设备 IP åœ°å€</param>
        /// <returns>完整的 Redis ç¼“存键</returns>
        public static string GetCacheKey(string ipAddress)
        {
            return $"{RedisPrefix.Code}:{RedisName.SocketDevices}:{ipAddress}";
        }
        /// <summary>
        /// ä»Žç¼“存获取状态
        /// ä»Žç¼“存获取机械手状态
        /// </summary>
        /// <param name="ipAddress">设备 IP åœ°å€</param>
        /// <returns>如果存在则返回状态对象,否则返回 null</returns>
        public RobotSocketState? GetState(string ipAddress)
        {
            return _cache.Get<RobotSocketState>(GetCacheKey(ipAddress));
        }
        /// <summary>
        /// èŽ·å–æˆ–åˆ›å»ºçŠ¶æ€
        /// èŽ·å–æˆ–åˆ›å»ºæœºæ¢°æ‰‹çŠ¶æ€
        /// </summary>
        /// <remarks>
        /// å¦‚果缓存中已存在该设备的状态,直接返回。
        /// å¦‚果不存在,则创建新的状态对象并存入缓存,然后返回。
        /// </remarks>
        /// <param name="ipAddress">设备 IP åœ°å€</param>
        /// <param name="robotCrane">机器人设备信息,用于初始化新状态</param>
        /// <returns>该设备的状态对象</returns>
        public RobotSocketState GetOrCreateState(string ipAddress, RobotCraneDevice robotCrane)
        {
            // ä½¿ç”¨ç¼“存服务的 GetOrAdd æ–¹æ³•,工厂函数在缓存未命中时创建新状态
            return _cache.GetOrAdd(GetCacheKey(ipAddress), _ => new RobotSocketState
            {
                IPAddress = ipAddress,
                RobotCrane = robotCrane
                IPAddress = ipAddress,  // è®¾ç½® IP åœ°å€ä½œä¸ºæ ‡è¯†
                RobotCrane = robotCrane  // ä¿å­˜è®¾å¤‡ä¿¡æ¯
            });
        }
    }
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotTaskProcessor.cs
@@ -1,10 +1,11 @@
using Newtonsoft.Json;
using Newtonsoft.Json;
using WIDESEA_Core;
using WIDESEAWCS_Common;
using WIDESEAWCS_Common.HttpEnum;
using WIDESEAWCS_Common.TaskEnum;
using WIDESEAWCS_Core;
using WIDESEAWCS_Core.Helper;
using WIDESEAWCS_Core.LogHelper;
using WIDESEAWCS_DTO.Stock;
using WIDESEAWCS_DTO.TaskInfo;
using WIDESEAWCS_ITaskInfoService;
@@ -15,17 +16,65 @@
namespace WIDESEAWCS_Tasks
{
    /// <summary>
    /// æœºå™¨äººä»»åŠ¡å¤„ç†å™¨ï¼šè´Ÿè´£ä»»åŠ¡èŽ·å–ã€ä¸‹å‘ã€å…¥åº“ä»»åŠ¡å›žä¼ åŠåº“å­˜ DTO æž„建。
    /// æœºå™¨äººä»»åŠ¡å¤„ç†å™¨ - è´Ÿè´£ä»»åŠ¡èŽ·å–ã€ä¸‹å‘ã€å…¥åº“ä»»åŠ¡å›žä¼ åŠåº“å­˜ DTO æž„建
    /// </summary>
    /// <remarks>
    /// æ ¸å¿ƒèŒè´£ï¼š
    /// 1. ä»Žæ•°æ®åº“轮询待处理的机器人任务
    /// 2. å‘机器人客户端下发取货指令(Pickbattery)
    /// 3. å¤„理入库任务的回传(拆盘/组盘/换盘场景)
    /// 4. æž„建库存回传 DTO å¹¶è°ƒç”¨ WMS æŽ¥å£
    ///
    /// é€šè¿‡ç½‘关访问 Socket,避免业务层直接依赖 TcpSocketServer。
    /// </remarks>
    public class RobotTaskProcessor
    {
        // é€šè¿‡ç½‘关访问 Socket,避免业务层直接依赖 TcpSocketServer。
        /// <summary>
        /// Socket å®¢æˆ·ç«¯ç½‘关接口
        /// </summary>
        /// <remarks>
        /// é€šè¿‡ç½‘关访问 Socket,避免业务层直接依赖 TcpSocketServer。
        /// æä¾›ç»Ÿä¸€çš„客户端通信接口。
        /// </remarks>
        private readonly ISocketClientGateway _socketClientGateway;
        /// <summary>
        /// æœºæ¢°æ‰‹çŠ¶æ€ç®¡ç†å™¨
        /// </summary>
        private readonly RobotStateManager _stateManager;
        /// <summary>
        /// æœºå™¨äººä»»åŠ¡æœåŠ¡
        /// </summary>
        /// <remarks>
        /// ç”¨äºŽæŸ¥è¯¢ã€æ›´æ–°ã€åˆ é™¤æœºå™¨äººä»»åŠ¡è®°å½•ã€‚
        /// </remarks>
        private readonly IRobotTaskService _robotTaskService;
        /// <summary>
        /// é€šç”¨ä»»åŠ¡æœåŠ¡
        /// </summary>
        /// <remarks>
        /// ç”¨äºŽä¸Ž WMS ç³»ç»Ÿäº¤äº’,接收任务、处理任务状态等。
        /// </remarks>
        private readonly ITaskService _taskService;
        /// <summary>
        /// HTTP å®¢æˆ·ç«¯å¸®åŠ©ç±»
        /// </summary>
        /// <remarks>
        /// ç”¨äºŽè°ƒç”¨ WMS ç³»ç»Ÿçš„ HTTP æŽ¥å£ã€‚
        /// </remarks>
        private readonly HttpClientHelper _httpClientHelper;
        /// <summary>
        /// æž„造函数
        /// </summary>
        /// <param name="socketClientGateway">Socket ç½‘å…³</param>
        /// <param name="stateManager">状态管理器</param>
        /// <param name="robotTaskService">机器人任务服务</param>
        /// <param name="taskService">通用任务服务</param>
        /// <param name="httpClientHelper">HTTP å®¢æˆ·ç«¯å¸®åŠ©ç±»</param>
        public RobotTaskProcessor(
            ISocketClientGateway socketClientGateway,
            RobotStateManager stateManager,
@@ -41,34 +90,68 @@
        }
        /// <summary>
        /// æŒ‰è®¾å¤‡ç¼–码获取当前机器人任务。
        /// æŒ‰è®¾å¤‡ç¼–码获取当前机器人的待处理任务
        /// </summary>
        /// <remarks>
        /// ä»Žæ•°æ®åº“中查询指定设备编码的待处理机器人任务。
        /// åªè¿”回状态为"待处理"的任务。
        /// </remarks>
        /// <param name="robotCrane">机器人设备信息,包含设备编码</param>
        /// <returns>待处理的任务对象,如果没有则返回 null</returns>
        public Dt_RobotTask? GetTask(RobotCraneDevice robotCrane)
        {
            return _robotTaskService.QueryRobotCraneTask(robotCrane.DeviceCode);
        }
        /// <summary>
        /// åˆ é™¤æœºå™¨äººä»»åŠ¡ã€‚
        /// åˆ é™¤æœºå™¨äººä»»åŠ¡
        /// </summary>
        /// <remarks>
        /// å½“任务完成(无论是成功还是失败)时调用,删除数据库中的任务记录。
        /// </remarks>
        /// <param name="ID">要删除的任务 ID</param>
        /// <returns>删除是否成功</returns>
        public bool? DeleteTask(int ID)
        {
            return _robotTaskService.Repository.DeleteDataById(ID);
        }
        /// <summary>
        /// ä¸‹å‘取货指令(Pickbattery)到机器人客户端。
        /// ä¸‹å‘取货指令(Pickbattery)到机器人客户端
        /// </summary>
        /// <remarks>
        /// å‘送格式:Pickbattery,{源地址}
        /// ä¾‹å¦‚:Pickbattery,A01 è¡¨ç¤ºä»Ž A01 ä½ç½®å–è´§
        ///
        /// ä¸‹å‘成功后:
        /// 1. æ›´æ–°ä»»åŠ¡çŠ¶æ€ä¸º"机器人执行中"
        /// 2. å°†ä»»åŠ¡å…³è”åˆ°çŠ¶æ€å¯¹è±¡
        /// 3. å®‰å…¨æ›´æ–°çŠ¶æ€åˆ° Redis
        /// 4. æ›´æ–°ä»»åŠ¡è®°å½•åˆ°æ•°æ®åº“
        /// </remarks>
        /// <param name="task">要下发的任务对象</param>
        /// <param name="state">机器人当前状态</param>
        public async Task SendSocketRobotPickAsync(Dt_RobotTask task, RobotSocketState state)
        {
            // æž„建取货指令,格式:Pickbattery,{源地址}
            string taskString = $"Pickbattery,{task.RobotSourceAddress}";
            // é€šè¿‡ Socket ç½‘关发送指令到机器人客户端
            bool result = await _socketClientGateway.SendToClientAsync(state.IPAddress, taskString);
            if (result)
            {
                // å‘送成功,记录日志
                QuartzLogger.Error($"下发取货指令,指令: {taskString}", state.RobotCrane.DeviceName);
                // æ›´æ–°ä»»åŠ¡çŠ¶æ€ä¸º"机器人执行中"
                task.RobotTaskState = TaskRobotStatusEnum.RobotExecuting.GetHashCode();
                // å°†ä»»åŠ¡å…³è”åˆ°çŠ¶æ€å¯¹è±¡
                state.CurrentTask = task;
                // ä¿æŒåŽŸè¯­ä¹‰ï¼šä»…åœ¨çŠ¶æ€å®‰å…¨å†™å…¥æˆåŠŸåŽå†æ›´æ–°ä»»åŠ¡çŠ¶æ€ã€‚
                // ä¿æŒåŽŸè¯­ä¹‰ï¼šä»…åœ¨çŠ¶æ€å®‰å…¨å†™å…¥æˆåŠŸåŽå†æ›´æ–°ä»»åŠ¡çŠ¶æ€
                // è¿™æ ·å¯ä»¥ç¡®ä¿çŠ¶æ€å’Œä»»åŠ¡è®°å½•çš„ä¸€è‡´æ€§
                if (_stateManager.TryUpdateStateSafely(state.IPAddress, state))
                {
                    await _robotTaskService.UpdateRobotTaskAsync(task);
@@ -77,89 +160,142 @@
        }
        /// <summary>
        /// å¤„理入库任务回传(拆盘/组盘/换盘场景)。
        /// å¤„理入库任务回传(拆盘/组盘/换盘场景)
        /// </summary>
        /// <remarks>
        /// å½“取货完成(AllPickFinished)或放货完成(AllPutFinished)时调用此方法。
        /// æ ¹æ®ä»»åŠ¡ç±»åž‹å’Œåœ°å€æ¥æºå†³å®šå¦‚ä½•å›žä¼ ç»™ WMS。
        ///
        /// å¤„理逻辑:
        /// 1. æ ¹æ® useSourceAddress å†³å®šä½¿ç”¨æºåœ°å€è¿˜æ˜¯ç›®æ ‡åœ°å€
        /// 2. æ ¹æ®ä»»åŠ¡ç±»åž‹ï¼ˆç»„ç›˜/换盘/拆盘)决定任务类型(入库/空托盘入库)
        /// 3. æž„建 CreateTaskDto å¹¶è°ƒç”¨ WMS æŽ¥å£åˆ›å»ºä»»åŠ¡
        /// 4. æŽ¥æ”¶ WMS è¿”回的任务信息
        /// 5. æ›´æ–°è¾“送线的目标地址、任务号等
        /// </remarks>
        /// <param name="state">机器人当前状态</param>
        /// <param name="useSourceAddress">是否使用源地址(true è¡¨ç¤ºæ‹†ç›˜/换盘场景,false è¡¨ç¤ºç»„盘/换盘场景)</param>
        /// <returns>处理是否成功</returns>
        public async Task<bool> HandleInboundTaskAsync(RobotSocketState state, bool useSourceAddress)
        {
            // èŽ·å–å½“å‰å…³è”çš„ä»»åŠ¡
            var currentTask = state.CurrentTask;
            if (currentTask == null)
            {
                return false;
            }
            // èŽ·å–å··é“ä»£ç 
            string roadway = currentTask.RobotSourceAddressLineCode;
            // æ ¹æ®å··é“名称判断仓库 ID
            // ZYRB1 -> 1, HPRB001 -> 2, å…¶ä»– -> 3
            int warehouseId = currentTask.RobotRoadway == "ZYRB1" ? 1 : currentTask.RobotRoadway == "HPRB001" ? 2 : 3;
            // ä»»åŠ¡ç±»åž‹ï¼ˆ0 è¡¨ç¤ºæœªå®šä¹‰ï¼Œç¨åŽæ ¹æ®ä»»åŠ¡ç±»åž‹è®¾ç½®ï¼‰
            int taskType = 0;
            // æºåœ°å€å’Œç›®æ ‡åœ°å€ï¼ˆåˆå§‹åŒ–)
            string SourceAddress = currentTask.RobotTargetAddressLineCode;
            string TargetAddress = currentTask.RobotSourceAddressLineCode;
            // æ‰˜ç›˜ä»£ç ï¼ˆåˆå§‹åŒ–为空)
            string PalletCode = string.Empty;
            // èŽ·å–ä»»åŠ¡ç±»åž‹çš„æžšä¸¾å€¼
            var robotTaskType = (RobotTaskTypeEnum)currentTask.RobotTaskType;
            // æ ¹æ® useSourceAddress å†³å®šå¤„理逻辑
            if (useSourceAddress)
            {
                // ä½¿ç”¨æºåœ°å€çš„场景:拆盘、换盘(放空托盘)
                switch (robotTaskType)
                {
                    case RobotTaskTypeEnum.GroupPallet:
                        // ç»„盘任务不使用源地址,直接返回 false
                        return false;
                    case RobotTaskTypeEnum.ChangePallet:
                    case RobotTaskTypeEnum.SplitPallet:
                        taskType = TaskTypeEnum.InEmpty.GetHashCode();
                        PalletCode = currentTask.RobotSourceAddressPalletCode;
                        // æ¢ç›˜/拆盘场景:托盘需要入库
                        taskType = TaskTypeEnum.InEmpty.GetHashCode();  // ç©ºæ‰˜ç›˜å…¥åº“
                        PalletCode = currentTask.RobotSourceAddressPalletCode;  // ä½¿ç”¨æºåœ°å€çš„æ‰˜ç›˜ç 
                        break;
                }
            }
            else
            {
                // ä½¿ç”¨ç›®æ ‡åœ°å€çš„场景:组盘、换盘(成品入库)
                switch (robotTaskType)
                {
                    case RobotTaskTypeEnum.ChangePallet:
                    case RobotTaskTypeEnum.GroupPallet:
                        taskType = TaskTypeEnum.Inbound.GetHashCode();
                        PalletCode = currentTask.RobotTargetAddressPalletCode;
                        // æ¢ç›˜/组盘场景:货物需要入库
                        taskType = TaskTypeEnum.Inbound.GetHashCode();  // æˆå“å…¥åº“
                        PalletCode = currentTask.RobotTargetAddressPalletCode;  // ä½¿ç”¨ç›®æ ‡åœ°å€çš„æ‰˜ç›˜ç 
                        break;
                    case RobotTaskTypeEnum.SplitPallet:
                        // æ‹†ç›˜ä»»åŠ¡ä¸ä½¿ç”¨ç›®æ ‡åœ°å€
                        return true;
                }
            }
            // æž„建创建任务的 DTO
            CreateTaskDto taskDto = new CreateTaskDto
            {
                PalletCode = PalletCode,
                SourceAddress = SourceAddress ?? string.Empty,
                TargetAddress = TargetAddress ?? string.Empty,
                Roadway = roadway,
                WarehouseId = warehouseId,
                PalletType = 1,
                TaskType = taskType
                PalletCode = PalletCode,                    // æ‰˜ç›˜æ¡ç 
                SourceAddress = SourceAddress ?? string.Empty,  // æºåœ°å€
                TargetAddress = TargetAddress ?? string.Empty,  // ç›®æ ‡åœ°å€
                Roadway = roadway,                          // å··é“
                WarehouseId = warehouseId,                   // ä»“库 ID
                PalletType = 1,                             // æ‰˜ç›˜ç±»åž‹ï¼ˆé»˜è®¤ä¸º1)
                TaskType = taskType                         // ä»»åŠ¡ç±»åž‹ï¼ˆå…¥åº“/空托盘入库)
            };
            // è°ƒç”¨ WMS æŽ¥å£åˆ›å»ºå…¥åº“任务
            var result = _httpClientHelper.Post<WebResponseContent>(nameof(ConfigKey.CreateTaskInboundAsync), taskDto.ToJson());
            // å¦‚果调用失败或返回错误状态
            if (!result.Data.Status && result.IsSuccess)
            {
                return false;
            }
            // è§£æž WMS è¿”回的任务信息
            WMSTaskDTO taskDTO = JsonConvert.DeserializeObject<WMSTaskDTO>(result.Data.Data.ToJson() ?? string.Empty) ?? new WMSTaskDTO();
            // è°ƒç”¨ä»»åŠ¡æœåŠ¡æŽ¥æ”¶ WMS ä»»åŠ¡
            var content = _taskService.ReceiveWMSTask(new List<WMSTaskDTO> { taskDTO });
            if (!content.Status)
            {
                return false;
            }
            // è§£æžè¿”回的任务信息
            var taskInfo = JsonConvert.DeserializeObject<Dt_Task>(content.Data.ToJson() ?? string.Empty) ?? new Dt_Task();
            // èŽ·å–æºåœ°å€
            string sourceAddress = taskDTO.SourceAddress;
            // æŸ¥æ‰¾æºåœ°å€å¯¹åº”的输送线设备
            IDevice? device = Storage.Devices.FirstOrDefault(x => x.DeviceProDTOs.Any(d => d.DeviceChildCode == sourceAddress));
            if (device != null)
            {
                // å°†è®¾å¤‡è½¬æ¢ä¸ºè¾“送线类型
                CommonConveyorLine conveyorLine = (CommonConveyorLine)device;
                // è®¾ç½®è¾“送线的目标地址
                conveyorLine.SetValue(ConveyorLineDBNameNew.Target, taskInfo.NextAddress, sourceAddress);
                // è®¾ç½®è¾“送线的任务号
                conveyorLine.SetValue(ConveyorLineDBNameNew.TaskNo, taskInfo.TaskNum, sourceAddress);
                // è§¦å‘输送线开始执行(写入 WCS_STB = 1)
                conveyorLine.SetValue(ConveyorLineDBNameNew.WCS_STB, 1, sourceAddress);
                // æ›´æ–°ä»»åŠ¡çŠ¶æ€åˆ°ä¸‹ä¸€é˜¶æ®µ
                if (_taskService.UpdateTaskStatusToNext(taskInfo).Status)
                {
                    return true;
@@ -170,23 +306,47 @@
        }
        /// <summary>
        /// æž„建库存回传 DTO。
        /// æž„建库存回传 DTO
        /// </summary>
        /// <remarks>
        /// ç”¨äºŽæ‹†ç›˜å’Œç»„盘操作时,向 WMS å›žä¼ åº“存信息。
        /// DTO åŒ…含源货位、目标货位、托盘码以及每个位置的电池条码。
        /// </remarks>
        /// <param name="state">机器人当前状态</param>
        /// <param name="positions">电池位置数组</param>
        /// <returns>构建好的库存 DTO</returns>
        public static StockDTO BuildStockDTO(RobotSocketState state, int[] positions)
        {
            return new StockDTO
            {
                // æºè¾“送线编号
                SourceLineNo = state.CurrentTask.RobotSourceAddressLineCode,
                // æºæ‰˜ç›˜å·
                SourcePalletNo = state.CurrentTask.RobotSourceAddressPalletCode,
                // ç›®æ ‡æ‰˜ç›˜å·
                TargetPalletNo = state.CurrentTask.RobotTargetAddressPalletCode,
                // ç›®æ ‡è¾“送线编号
                TargetLineNo = state.CurrentTask.RobotTargetAddressLineCode,
                // ç”µæ± ä½ç½®è¯¦æƒ…列表
                // è¿‡æ»¤æŽ‰ä½ç½®ä¸º 0 æˆ–负数的无效数据
                // æŒ‰ä½ç½®ç¼–号排序
                // ä¸ºæ¯ä¸ªä½ç½®ç”Ÿæˆå¯¹åº”的库存详情
                Details = positions
                    .Where(x => x > 0)
                    .OrderBy(x => x)
                    .Where(x => x > 0)  // è¿‡æ»¤æ— æ•ˆä½ç½®
                    .OrderBy(x => x)   // æŒ‰ä½ç½®æŽ’序
                    .Select((x, idx) => new StockDetailDTO
                    {
                        // æ•°é‡ï¼šå¦‚果已有任务总数,使用任务总数+当前位置数;否则只使用当前位置数
                        Quantity = state.RobotTaskTotalNum > 0 ? state.RobotTaskTotalNum + positions.Length : positions.Length,
                        // é€šé“/位置编号
                        Channel = x,
                        // ç”µæ± æ¡ç ï¼šå¦‚果状态中有条码列表,取对应位置的条码;否则为空
                        CellBarcode = state.CellBarcode?.Count > 0 ? state.CellBarcode[x - 1] : ""
                    })
                    .ToList()
@@ -194,16 +354,33 @@
        }
        /// <summary>
        /// è°ƒç”¨æ‹†ç›˜ API。
        /// è°ƒç”¨æ‹†ç›˜ API
        /// </summary>
        /// <remarks>
        /// å½“取货完成且需要拆盘时调用。
        /// å°†ç”µæ± ä»Žæ‰˜ç›˜ä¸Šå–下,逐个放置到目标位置。
        /// </remarks>
        /// <param name="stockDTO">库存 DTO,包含要拆盘的电芯信息</param>
        /// <returns>HTTP å“åº”结果</returns>
        public HttpResponseResult<WebResponseContent> PostSplitPalletAsync(StockDTO stockDTO)
        {
            return _httpClientHelper.Post<WebResponseContent>(nameof(ConfigKey.SplitPalletAsync), stockDTO.ToJson());
        }
        /// <summary>
        /// è°ƒç”¨ç»„盘/换盘 API。
        /// è°ƒç”¨ç»„盘/换盘 API
        /// </summary>
        /// <remarks>
        /// å½“放货完成且需要组盘或换盘时调用。
        /// å°†å¤šä¸ªç”µæ± ç»„合到同一个托盘上。
        ///
        /// configKey å‚数决定调用哪个 API:
        /// - GroupPalletAsync: ç»„盘接口
        /// - ChangePalletAsync: æ¢ç›˜æŽ¥å£
        /// </remarks>
        /// <param name="configKey">配置键名,决定调用哪个 API</param>
        /// <param name="stockDTO">库存 DTO,包含要组盘的电芯信息</param>
        /// <returns>HTTP å“åº”结果</returns>
        public HttpResponseResult<WebResponseContent> PostGroupPalletAsync(string configKey, StockDTO stockDTO)
        {
            return _httpClientHelper.Post<WebResponseContent>(configKey, stockDTO.ToJson());
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotPrefixCommandHandler.cs
@@ -1,4 +1,4 @@
using System.Net.Sockets;
using System.Net.Sockets;
using WIDESEAWCS_Common.HttpEnum;
using WIDESEAWCS_Common.TaskEnum;
using WIDESEAWCS_DTO.TaskInfo;
@@ -9,15 +9,61 @@
namespace WIDESEAWCS_Tasks.Workflow
{
    /// <summary>
    /// å‰ç¼€å‘½ä»¤å¤„理:迁移原 RobotMessageHandler çš„ pickfinished/putfinished åˆ†æ”¯ã€‚
    /// å‰ç¼€å‘½ä»¤å¤„理器
    /// </summary>
    /// <remarks>
    /// è¿ç§»åŽŸ RobotMessageHandler çš„ pickfinished/putfinished åˆ†æ”¯ã€‚
    ///
    /// å‰ç¼€å‘½ä»¤æ˜¯æŒ‡ä»¥ç‰¹å®šå‰ç¼€å¼€å¤´çš„命令,后面跟随逗号分隔的参数。
    /// æ ¼å¼ï¼š{前缀},{参数1},{参数2},...
    ///
    /// å½“前支持的前缀命令:
    /// - pickfinished: å–货完成,后面跟随完成的位置编号列表
    /// - putfinished: æ”¾è´§å®Œæˆï¼ŒåŽé¢è·Ÿéšå®Œæˆçš„位置编号列表
    ///
    /// è¿™äº›å‘½ä»¤é€šå¸¸åŒ…含取货或放货的位置信息,需要解析并更新状态。
    /// </remarks>
    public class RobotPrefixCommandHandler : IRobotPrefixCommandHandler
    {
        /// <summary>
        /// æœºå™¨äººä»»åŠ¡æœåŠ¡
        /// </summary>
        /// <remarks>
        /// ç”¨äºŽæŸ¥è¯¢å’Œæ›´æ–°ä»»åŠ¡è®°å½•ã€‚
        /// </remarks>
        private readonly IRobotTaskService _robotTaskService;
        /// <summary>
        /// ä»»åŠ¡å¤„ç†å™¨
        /// </summary>
        /// <remarks>
        /// ç”¨äºŽå¤„理取货/放货完成时的业务逻辑,如调用拆盘/组盘 API。
        /// </remarks>
        private readonly RobotTaskProcessor _taskProcessor;
        /// <summary>
        /// çŠ¶æ€ç®¡ç†å™¨
        /// </summary>
        /// <remarks>
        /// ç”¨äºŽå®‰å…¨æ›´æ–°æœºå™¨äººçš„状态。
        /// </remarks>
        private readonly RobotStateManager _stateManager;
        /// <summary>
        /// Socket ç½‘å…³
        /// </summary>
        /// <remarks>
        /// ç”¨äºŽå‘客户端发送响应消息。
        /// </remarks>
        private readonly ISocketClientGateway _socketClientGateway;
        /// <summary>
        /// æž„造函数
        /// </summary>
        /// <param name="robotTaskService">任务服务</param>
        /// <param name="taskProcessor">任务处理器</param>
        /// <param name="stateManager">状态管理器</param>
        /// <param name="socketClientGateway">Socket ç½‘å…³</param>
        public RobotPrefixCommandHandler(
            IRobotTaskService robotTaskService,
            RobotTaskProcessor taskProcessor,
@@ -30,69 +76,137 @@
            _socketClientGateway = socketClientGateway;
        }
        /// <summary>
        /// æ£€æŸ¥æ¶ˆæ¯æ˜¯å¦ä¸ºå‰ç¼€å‘½ä»¤
        /// </summary>
        /// <remarks>
        /// å‰ç¼€å‘½ä»¤å¿…须以 "pickfinished" æˆ– "putfinished" å¼€å¤´ï¼ˆä¸åŒºåˆ†å¤§å°å†™ï¼‰ã€‚
        /// </remarks>
        /// <param name="message">消息内容(小写形式)</param>
        /// <returns>如果是指缀命令返回 true</returns>
        public bool IsPrefixCommand(string message)
        {
            // æ£€æŸ¥æ¶ˆæ¯æ˜¯å¦ä»¥ pickfinished æˆ– putfinished å¼€å¤´
            return message.StartsWith("pickfinished") || message.StartsWith("putfinished");
        }
        /// <summary>
        /// å¤„理前缀命令
        /// </summary>
        /// <remarks>
        /// å¤„理流程:
        /// 1. è§£æžæ¶ˆæ¯ï¼Œæå–位置参数
        /// 2. æŸ¥è¯¢å½“前任务
        /// 3. æ ¹æ®å‘½ä»¤ç±»åž‹è°ƒç”¨ç›¸åº”的处理方法
        /// 4. å›žå†™åŽŸæ¶ˆæ¯åˆ°å®¢æˆ·ç«¯
        ///
        /// æ¶ˆæ¯æ ¼å¼ï¼š{命令前缀},{位置1},{位置2},...
        /// ç¤ºä¾‹ï¼špickfinished,1,2,3 è¡¨ç¤ºå–货完成,位置 1、2、3 çš„货物已取走
        /// </remarks>
        /// <param name="message">原始消息内容</param>
        /// <param name="state">机器人当前状态</param>
        /// <param name="client">TCP å®¢æˆ·ç«¯è¿žæŽ¥ï¼Œç”¨äºŽå‘送响应</param>
        public async Task HandleAsync(string message, RobotSocketState state, TcpClient client)
        {
            try
            {
                // æŒ‰é€—号分隔消息,提取命令和参数
                // ä¾‹å¦‚:pickfinished,1,2,3 -> ["pickfinished", "1", "2", "3"]
                var parts = message.Split(',');
                // æ£€æŸ¥æ¶ˆæ¯æ ¼å¼æ˜¯å¦æœ‰æ•ˆï¼šè‡³å°‘要有命令前缀,且状态中有当前任务
                if (parts.Length < 1 || state.CurrentTask == null)
                {
                    return;
                }
                // æå–命令前缀并转换为小写
                var cmd = parts[0].ToLowerInvariant();
                // è§£æžä½ç½®å‚数(跳过命令前缀,处理后面的数字)
                // è¿‡æ»¤æŽ‰æ— æ³•解析为数字或值为 0 çš„位置
                int[] positions = parts.Skip(1)
                    .Select(p => int.TryParse(p, out int value) ? value : (int?)null)
                    .Where(v => v.HasValue && v.Value != 0)
                    .Select(v => v!.Value)
                    .Select(p => int.TryParse(p, out int value) ? value : (int?)null)  // å°è¯•解析为整数
                    .Where(v => v.HasValue && v.Value != 0)  // è¿‡æ»¤æŽ‰ null å’Œ 0
                    .Select(v => v!.Value)  // æå–值(已知非 null)
                    .ToArray();
                // ä»Žæ•°æ®åº“重新查询当前任务(确保获取最新状态)
                var task = await _robotTaskService.Repository.QueryFirstAsync(x => x.RobotTaskId == state.CurrentTask.RobotTaskId);
                // æ ¹æ®å‘½ä»¤å‰ç¼€åˆ†å‘处理
                if (cmd.StartsWith("pickfinished"))
                {
                    // å¤„理取货完成
                    await HandlePickFinishedAsync(state, positions, task);
                }
                else if (cmd.StartsWith("putfinished"))
                {
                    // å¤„理放货完成
                    await HandlePutFinishedAsync(state, positions, task);
                }
                // å›žå†™åŽŸæ¶ˆæ¯åˆ°å®¢æˆ·ç«¯ï¼ˆä¿æŒåŽŸæœ‰è¡Œä¸ºï¼‰
                await _socketClientGateway.SendMessageAsync(client, message);
            }
            catch (Exception ex)
            {
                // æ•获并记录异常,防止异常向上传播导致消息处理中断
                Console.WriteLine($"RobotJob MessageReceived Error: {ex.Message}");
            }
        }
        /// <summary>
        /// å¤„理取货完成(pickfinished)命令
        /// </summary>
        /// <remarks>
        /// å¤„理逻辑:
        /// 1. å¦‚果是拆盘任务,构建库存 DTO å¹¶è°ƒç”¨æ‹†ç›˜ API
        /// 2. æ›´æ–°å½“前动作为"取货完成"
        /// 3. è®°å½•取货完成的位置
        /// 4. æ›´æ–°ä»»åŠ¡çŠ¶æ€ä¸º"机器人取货完成"
        /// 5. å®‰å…¨æ›´æ–°çŠ¶æ€åˆ° Redis
        /// </remarks>
        /// <param name="state">机器人当前状态</param>
        /// <param name="positions">取货完成的位置编号数组</param>
        /// <param name="task">机器人任务记录</param>
        private async Task HandlePickFinishedAsync(RobotSocketState state, int[] positions, Dt_RobotTask? task)
        {
            // å¦‚果是拆盘任务
            if (state.IsSplitPallet)
            {
                // æž„建库存 DTO,包含位置信息和托盘条码
                var stockDTO = RobotTaskProcessor.BuildStockDTO(state, positions);
                // è®°å½•取货完成的位置
                state.LastPickPositions = positions;
                // è°ƒç”¨æ‹†ç›˜ API
                var result = _taskProcessor.PostSplitPalletAsync(stockDTO);
                // å¦‚æžœ API è°ƒç”¨æˆåŠŸ
                if (result.Data.Status && result.IsSuccess)
                {
                    // æ›´æ–°å½“前动作为"取货完成"
                    state.CurrentAction = "PickFinished";
                }
            }
            else
            {
                // éžæ‹†ç›˜ä»»åŠ¡ï¼Œç›´æŽ¥æ›´æ–°åŠ¨ä½œ
                state.CurrentAction = "PickFinished";
            }
            // è®°å½•取货完成的位置(无论是否拆盘都记录)
            state.LastPickPositions = positions;
            // å¦‚果任务存在
            if (task != null)
            {
                // æ›´æ–°ä»»åŠ¡çŠ¶æ€ä¸º"机器人取货完成"
                task.RobotTaskState = TaskRobotStatusEnum.RobotPickFinish.GetHashCode();
                // å®‰å…¨æ›´æ–°çŠ¶æ€åˆ° Redis,确保更新成功后再更新数据库
                if (_stateManager.TryUpdateStateSafely(state.IPAddress, state))
                {
                    await _robotTaskService.Repository.UpdateDataAsync(task);
@@ -100,34 +214,70 @@
            }
        }
        /// <summary>
        /// å¤„理放货完成(putfinished)命令
        /// </summary>
        /// <remarks>
        /// å¤„理逻辑:
        /// 1. å¦‚果是组盘任务,构建库存 DTO å¹¶è°ƒç”¨ç»„盘/换盘 API
        /// 2. å¦‚果组盘成功,增加任务计数
        /// 3. æ›´æ–°å½“前动作为"放货完成"
        /// 4. æ›´æ–°ä»»åŠ¡çŠ¶æ€ä¸º"机器人放货完成"
        /// 5. å®‰å…¨æ›´æ–°çŠ¶æ€åˆ° Redis
        /// </remarks>
        /// <param name="state">机器人当前状态</param>
        /// <param name="positions">放货完成的位置编号数组</param>
        /// <param name="task">机器人任务记录</param>
        private async Task HandlePutFinishedAsync(RobotSocketState state, int[] positions, Dt_RobotTask? task)
        {
            // å‡è®¾æ”¾è´§æˆåŠŸï¼ˆå¦‚æžœåŽç»­ API è°ƒç”¨å¤±è´¥ä¹Ÿä¸å›žé€€è®¡æ•°ï¼‰
            bool putSuccess = true;
            // å¦‚果是组盘任务(包含换盘)
            if (state.IsGroupPallet)
            {
                // è®°å½•放货完成的位置
                state.LastPutPositions = positions;
                // æž„建库存 DTO
                var stockDTO = RobotTaskProcessor.BuildStockDTO(state, positions);
                // æ ¹æ®ä»»åŠ¡ç±»åž‹å†³å®šè°ƒç”¨å“ªä¸ª API
                // æ¢ç›˜ä»»åŠ¡è°ƒç”¨ ChangePalletAsync,组盘任务调用 GroupPalletAsync
                var configKey = state.CurrentTask?.RobotTaskType == RobotTaskTypeEnum.ChangePallet.GetHashCode()
                    ? nameof(ConfigKey.ChangePalletAsync)
                    : nameof(ConfigKey.GroupPalletAsync);
                // è°ƒç”¨ç»„盘/换盘 API
                var result = _taskProcessor.PostGroupPalletAsync(configKey, stockDTO);
                // æ£€æŸ¥ API è¿”回状态
                putSuccess = result.Data.Status && result.IsSuccess;
            }
            // å¦‚果放货成功
            if (putSuccess)
            {
                // æ›´æ–°å½“前动作为"放货完成"
                state.CurrentAction = "PutFinished";
                // å¢žåŠ ä»»åŠ¡è®¡æ•°ï¼ˆç´¯åŠ æœ¬æ¬¡å®Œæˆçš„æ•°é‡ï¼‰
                state.RobotTaskTotalNum += positions.Length;
                // å¦‚果任务存在,同步更新任务的计数
                if (task != null)
                {
                    task.RobotTaskTotalNum += positions.Length;
                }
            }
            // å¦‚果任务存在
            if (task != null)
            {
                // æ›´æ–°ä»»åŠ¡çŠ¶æ€ä¸º"机器人放货完成"
                task.RobotTaskState = TaskRobotStatusEnum.RobotPutFinish.GetHashCode();
                // å®‰å…¨æ›´æ–°çŠ¶æ€åˆ° Redis
                if (_stateManager.TryUpdateStateSafely(state.IPAddress, state))
                {
                    await _robotTaskService.Repository.UpdateDataAsync(task);
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotSimpleCommandHandler.cs
@@ -1,54 +1,121 @@
using WIDESEAWCS_Common.TaskEnum;
using WIDESEAWCS_Common.TaskEnum;
using WIDESEAWCS_Tasks.Workflow.Abstractions;
namespace WIDESEAWCS_Tasks.Workflow
{
    /// <summary>
    /// ç®€å•命令处理:仅迁移原 RobotMessageHandler ä¸­çš„命令分支,不改变业务语义。
    /// ç®€å•命令处理器
    /// </summary>
    /// <remarks>
    /// è¿ç§»åŽŸ RobotMessageHandler ä¸­çš„简单命令分支,不改变业务语义。
    ///
    /// ç®€å•命令是指不需要额外参数的状态更新命令,如运行状态、模式切换等。
    /// ä¸Žå‰ç¼€å‘½ä»¤ï¼ˆéœ€è¦è§£æžä½ç½®å‚数)相对。
    ///
    /// å¤„理完成后返回 true;无法识别的命令返回 false。
    /// </remarks>
    public class RobotSimpleCommandHandler : IRobotSimpleCommandHandler
    {
        /// <summary>
        /// æœºå™¨äººä»»åŠ¡å¤„ç†å™¨
        /// </summary>
        /// <remarks>
        /// ç”¨äºŽå¤„理 allpickfinished å’Œ allputfinished å‘½ä»¤æ—¶ï¼Œ
        /// è°ƒç”¨ä»»åŠ¡å…¥åº“å’Œåˆ é™¤é€»è¾‘ã€‚
        /// </remarks>
        private readonly RobotTaskProcessor _taskProcessor;
        /// <summary>
        /// æž„造函数
        /// </summary>
        /// <param name="taskProcessor">任务处理器实例</param>
        public RobotSimpleCommandHandler(RobotTaskProcessor taskProcessor)
        {
            _taskProcessor = taskProcessor;
        }
        /// <summary>
        /// å¤„理简单命令
        /// </summary>
        /// <remarks>
        /// æ ¹æ®æ¶ˆæ¯å†…容更新机器人的运行状态、模式、手臂对象等属性。
        /// æŸäº›å‘½ä»¤ï¼ˆå¦‚ allpickfinished、allputfinished)会触发实际的业务逻辑。
        ///
        /// å‘½ä»¤åˆ—表:
        /// - homing: å›žé›¶ä¸­
        /// - homed: å·²å›žé›¶
        /// - running: è¿è¡Œä¸­
        /// - pausing: æš‚停中
        /// - warming: é¢„热中
        /// - emstoping: æ€¥åœä¸­
        /// - picking: å–货中
        /// - puting: æ”¾è´§ä¸­
        /// - runmode,1: è¿è¡Œæ¨¡å¼åˆ‡æ¢åˆ°æ‰‹åЍ
        /// - runmode,2: è¿è¡Œæ¨¡å¼åˆ‡æ¢åˆ°è‡ªåЍ
        /// - controlmode,1: æŽ§åˆ¶æ¨¡å¼åˆ‡æ¢åˆ°å®¢æˆ·ç«¯æŽ§åˆ¶
        /// - controlmode,2: æŽ§åˆ¶æ¨¡å¼åˆ‡æ¢åˆ°å…¶ä»–
        /// - armobject,1: æ‰‹è‡‚有物料
        /// - armobject,0: æ‰‹è‡‚无物料
        /// - allpickfinished: å…¨éƒ¨å–货完成
        /// - allputfinished: å…¨éƒ¨æ”¾è´§å®Œæˆ
        /// </remarks>
        /// <param name="message">消息内容(小写形式)</param>
        /// <param name="state">机器人当前状态(会被修改)</param>
        /// <returns>是否成功处理;无法识别的命令返回 false</returns>
        public async Task<bool> HandleAsync(string message, RobotSocketState state)
        {
            // ä½¿ç”¨ switch è¡¨è¾¾å¼è¿›è¡Œæ¨¡å¼åŒ¹é…ï¼Œæé«˜å¯è¯»æ€§å’Œæ€§èƒ½
            switch (message)
            {
                // ==================== è¿è¡ŒçŠ¶æ€å‘½ä»¤ ====================
                // æœºå™¨äººæ­£åœ¨å›žé›¶
                case "homing":
                    state.OperStatus = "Homing";
                    return true;
                // æœºå™¨äººå·²å®Œæˆå›žé›¶
                case "homed":
                    state.OperStatus = "Homed";
                    return true;
                // æœºå™¨äººæ­£åœ¨å–è´§
                case "picking":
                    state.CurrentAction = "Picking";
                    return true;
                // æœºå™¨äººæ­£åœ¨æ”¾è´§
                case "puting":
                    state.CurrentAction = "Putting";
                    return true;
                // ==================== å…¨éƒ¨å®Œæˆå‘½ä»¤ ====================
                // å…¨éƒ¨å–货完成
                case "allpickfinished":
                {
                    // æ›´æ–°å½“前动作为"全部取货完成"
                    state.CurrentAction = "AllPickFinished";
                    // èŽ·å–å½“å‰å…³è”çš„ä»»åŠ¡
                    var currentTask = state.CurrentTask;
                    if (currentTask == null)
                    {
                        // æ²¡æœ‰ä»»åŠ¡å…³è”ï¼Œè¿”å›ž false
                        return false;
                    }
                    // åˆ¤æ–­ä»»åŠ¡ç±»åž‹
                    var robotTaskType = (RobotTaskTypeEnum)currentTask.RobotTaskType;
                    // åªæœ‰æ‹†ç›˜æˆ–换盘任务需要处理入库
                    if (robotTaskType == RobotTaskTypeEnum.SplitPallet || robotTaskType == RobotTaskTypeEnum.ChangePallet)
                    {
                        // å¤„理入库任务回传
                        // useSourceAddress: true è¡¨ç¤ºä½¿ç”¨æºåœ°å€ï¼ˆæ‹†ç›˜/换盘场景)
                        if (await _taskProcessor.HandleInboundTaskAsync(state, useSourceAddress: true))
                        {
                            // å…¥åº“成功,删除任务记录
                            _taskProcessor.DeleteTask(currentTask.RobotTaskId);
                            return true;
                        }
@@ -56,70 +123,101 @@
                    return false;
                }
                // å…¨éƒ¨æ”¾è´§å®Œæˆ
                case "allputfinished":
                {
                    // æ›´æ–°å½“前动作为"全部放货完成"
                    state.CurrentAction = "AllPutFinished";
                    // èŽ·å–å½“å‰å…³è”çš„ä»»åŠ¡
                    var currentTask = state.CurrentTask;
                    if (currentTask == null)
                    {
                        return false;
                    }
                    // åˆ¤æ–­ä»»åŠ¡ç±»åž‹
                    var robotTaskType = (RobotTaskTypeEnum)currentTask.RobotTaskType;
                    // åªæœ‰ç»„盘或换盘任务需要处理入库
                    if (robotTaskType == RobotTaskTypeEnum.GroupPallet || robotTaskType == RobotTaskTypeEnum.ChangePallet)
                    {
                        // å¤„理入库任务回传
                        // useSourceAddress: false è¡¨ç¤ºä½¿ç”¨ç›®æ ‡åœ°å€ï¼ˆç»„盘/换盘场景)
                        if (await _taskProcessor.HandleInboundTaskAsync(state, useSourceAddress: false))
                        {
                            // å…¥åº“成功,删除任务记录
                            _taskProcessor.DeleteTask(currentTask.RobotTaskId);
                            state.CurrentTask = null;
                            state.RobotTaskTotalNum = 0;
                            state.CellBarcode = new List<string>();
                            // æ¸…理状态,为下一个任务做准备
                            state.CurrentTask = null;           // æ¸…除当前任务
                            state.RobotTaskTotalNum = 0;        // é‡ç½®ä»»åŠ¡è®¡æ•°
                            state.CellBarcode = new List<string>();  // æ¸…空条码列表
                            return true;
                        }
                    }
                    return false;
                }
                // ==================== è¿è¡ŒçŠ¶æ€å‘½ä»¤ï¼ˆç»­ï¼‰ ====================
                // æœºå™¨äººæ­£åœ¨è¿è¡Œ
                case "running":
                    state.OperStatus = "Running";
                    return true;
                // æœºå™¨äººæ­£åœ¨æš‚停
                case "pausing":
                    state.OperStatus = "Pausing";
                    return true;
                // æœºå™¨äººæ­£åœ¨é¢„热
                case "warming":
                    state.OperStatus = "Warming";
                    return true;
                // æœºå™¨äººæ­£åœ¨æ€¥åœ
                case "emstoping":
                    state.OperStatus = "Emstoping";
                    return true;
                // ==================== æ¨¡å¼åˆ‡æ¢å‘½ä»¤ ====================
                // è¿è¡Œæ¨¡å¼åˆ‡æ¢ä¸ºæ‰‹åŠ¨ï¼ˆæ¨¡å¼1)
                case "runmode,1":
                    state.RobotRunMode = 1;
                    return true;
                // è¿è¡Œæ¨¡å¼åˆ‡æ¢ä¸ºè‡ªåŠ¨ï¼ˆæ¨¡å¼2)
                case "runmode,2":
                    state.RobotRunMode = 2;
                    return true;
                // æŽ§åˆ¶æ¨¡å¼åˆ‡æ¢ä¸ºå®¢æˆ·ç«¯æŽ§åˆ¶ï¼ˆæ¨¡å¼1)
                case "controlmode,1":
                    state.RobotControlMode = 1;
                    return true;
                // æŽ§åˆ¶æ¨¡å¼åˆ‡æ¢ä¸ºå…¶ä»–(模式2)
                case "controlmode,2":
                    state.RobotControlMode = 2;
                    return true;
                // ==================== æ‰‹è‡‚对象命令 ====================
                // æ‰‹è‡‚有物料(抓取到货物)
                case "armobject,1":
                    state.RobotArmObject = 1;
                    return true;
                // æ‰‹è‡‚无物料(手臂空闲)
                case "armobject,0":
                    state.RobotArmObject = 0;
                    return true;
                // ==================== é»˜è®¤æƒ…况 ====================
                // æ— æ³•识别的命令,返回 false
                default:
                    return false;
            }
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotWorkflowOrchestrator.cs
@@ -1,5 +1,6 @@
using WIDESEA_Core;
using WIDESEAWCS_Common.TaskEnum;
using WIDESEAWCS_Core.LogHelper;
using WIDESEAWCS_ITaskInfoService;
using WIDESEAWCS_Model.Models;
using WIDESEAWCS_Tasks.Workflow.Abstractions;
@@ -7,15 +8,62 @@
namespace WIDESEAWCS_Tasks.Workflow
{
    /// <summary>
    /// RobotJob æµç¨‹ç¼–排器:迁移原 RobotJob çŠ¶æ€æœºåˆ†æ”¯ï¼Œé™ä½Ž Job ç±»å¤æ‚度。
    /// æœºå™¨äººä»»åŠ¡ç¼–æŽ’å™¨ - è´Ÿè´£ RobotJob ä¸­çš„状态机流转和执行步骤编排
    /// </summary>
    /// <remarks>
    /// è¿ç§»åŽŸ RobotJob çŠ¶æ€æœºæµè½¬æ”¯æŒï¼Œé™ä½Ž Job å±‚的复杂度。
    ///
    /// æ ¸å¿ƒèŒè´£ï¼š
    /// 1. æ ¹æ®æœºå™¨äººå½“前状态和任务状态,决定下一步动作
    /// 2. å¤„理取货完成后的放货指令下发
    /// 3. å¤„理放货完成后的取货指令下发(组盘场景)
    ///
    /// çŠ¶æ€æœºæµè½¬è§„åˆ™ï¼š
    /// - æ¡ä»¶ï¼šRobotRunMode == 2(自动模式)且 RobotControlMode == 1(客户端控制)且 OperStatus != "Running"
    /// - å–货完成 -> æ”¾è´§ï¼šPickFinished + RobotArmObject == 1 + RobotPickFinish -> å‘送 Putbattery
    /// - æ”¾è´§å®Œæˆ -> å–货:PutFinished + Homed + RobotArmObject == 0 -> å‘送 Pickbattery(组盘/换盘场景)
    /// </remarks>
    public class RobotWorkflowOrchestrator : IRobotWorkflowOrchestrator
    {
        /// <summary>
        /// æœºæ¢°æ‰‹çŠ¶æ€ç®¡ç†å™¨
        /// </summary>
        /// <remarks>
        /// ç”¨äºŽè¯»å–和更新机器人的状态。
        /// </remarks>
        private readonly RobotStateManager _stateManager;
        /// <summary>
        /// æœºæ¢°æ‰‹å®¢æˆ·ç«¯ç®¡ç†å™¨
        /// </summary>
        /// <remarks>
        /// ç”¨äºŽå‘机器人客户端发送指令。
        /// </remarks>
        private readonly RobotClientManager _clientManager;
        /// <summary>
        /// ä»»åŠ¡å¤„ç†å™¨
        /// </summary>
        /// <remarks>
        /// ç”¨äºŽæ‰§è¡Œä»»åŠ¡ç›¸å…³çš„ä¸šåŠ¡é€»è¾‘ï¼Œå¦‚å‘é€å–è´§æŒ‡ä»¤ã€‚
        /// </remarks>
        private readonly RobotTaskProcessor _taskProcessor;
        /// <summary>
        /// æœºå™¨äººä»»åŠ¡æœåŠ¡
        /// </summary>
        /// <remarks>
        /// ç”¨äºŽæ›´æ–°ä»»åŠ¡çŠ¶æ€ã€‚
        /// </remarks>
        private readonly IRobotTaskService _robotTaskService;
        /// <summary>
        /// æž„造函数
        /// </summary>
        /// <param name="stateManager">状态管理器</param>
        /// <param name="clientManager">客户端管理器</param>
        /// <param name="taskProcessor">任务处理器</param>
        /// <param name="robotTaskService">任务服务</param>
        public RobotWorkflowOrchestrator(
            RobotStateManager stateManager,
            RobotClientManager clientManager,
@@ -28,81 +76,173 @@
            _robotTaskService = robotTaskService;
        }
        /// <summary>
        /// æ‰§è¡Œä»»åŠ¡ç¼–æŽ’æµç¨‹
        /// </summary>
        /// <remarks>
        /// æ ¹æ®æœºå™¨äººå½“前状态和任务状态,决定是否下发指令。
        ///
        /// æ‰§è¡Œæ¡ä»¶ï¼š
        /// 1. æœºå™¨äººå¤„于自动模式(RobotRunMode == 2)
        /// 2. æœºå™¨äººå¤„于客户端控制模式(RobotControlMode == 1)
        /// 3. æœºå™¨äººä¸åœ¨è¿è¡Œä¸­ï¼ˆOperStatus != "Running")
        ///
        /// æµè½¬é€»è¾‘:
        /// - å–货完成且手臂有货 -> å‘送放货指令(Putbattery)
        /// - æ”¾è´§å®Œæˆä¸”手臂无货 -> å‘送取货指令(Pickbattery)
        /// </remarks>
        /// <param name="latestState">机器人最新状态</param>
        /// <param name="task">待执行的机器人任务</param>
        /// <param name="ipAddress">机器人 IP åœ°å€</param>
        public async Task ExecuteAsync(RobotSocketState latestState, Dt_RobotTask task, string ipAddress)
        {
            // ä¿æŒåŽŸæœ‰åˆ†æ”¯åˆ¤å®šæ¡ä»¶ä¸å˜ï¼Œç¡®ä¿è¡Œä¸ºä¸€è‡´ã€‚
            // æŒ‰åŽŸæ–¹æ¡ˆåˆ†æ”¯åˆ¤æ–­ï¼Œç¡®ä¿é€»è¾‘ä¸€è‡´
            // æ£€æŸ¥æ˜¯å¦æ»¡è¶³è‡ªåŠ¨æŽ§åˆ¶æ¡ä»¶ï¼š
            // 1. è¿è¡Œæ¨¡å¼ä¸ºè‡ªåŠ¨ï¼ˆ2)
            // 2. æŽ§åˆ¶æ¨¡å¼ä¸ºå®¢æˆ·ç«¯æŽ§åˆ¶ï¼ˆ1)
            // 3. è¿è¡ŒçŠ¶æ€ä¸æ˜¯ Running(说明已完成当前动作)
            if (latestState.RobotRunMode == 2 && latestState.RobotControlMode == 1 && latestState.OperStatus != "Running")
            {
                // ========== å–货完成后的放货处理 ==========
                // æ¡ä»¶ï¼š
                // - å½“前动作是 PickFinished æˆ– AllPickFinished(取货完成)
                // - æ‰‹è‡‚上有物料(RobotArmObject == 1)
                // - ä»»åŠ¡çŠ¶æ€ä¸º RobotPickFinish(已记录取货完成)
                if ((latestState.CurrentAction == "PickFinished" || latestState.CurrentAction == "AllPickFinished")
                    && latestState.RobotArmObject == 1
                    && task.RobotTaskState == TaskRobotStatusEnum.RobotPickFinish.GetHashCode())
                {
                    // å‘送放货指令
                    await HandlePickFinishedStateAsync(task, ipAddress);
                }
                // ========== æ”¾è´§å®ŒæˆåŽçš„取货处理 ==========
                // æ¡ä»¶ï¼š
                // - å½“前动作是 PutFinished、AllPutFinished æˆ– null(放货完成)
                // - è¿è¡ŒçŠ¶æ€ä¸º Homed(已归位)
                // - æ‰‹è‡‚上无物料(RobotArmObject == 0)
                // - ä»»åŠ¡çŠ¶æ€ä¸º RobotPutFinish æˆ–不是 RobotExecuting
                else if ((latestState.CurrentAction == "PutFinished" || latestState.CurrentAction == "AllPutFinished" || latestState.CurrentAction == null)
                    && latestState.OperStatus == "Homed"
                    && latestState.RobotArmObject == 0
                    && (task.RobotTaskState == TaskRobotStatusEnum.RobotPutFinish.GetHashCode()
                    || task.RobotTaskState != TaskRobotStatusEnum.RobotExecuting.GetHashCode()))
                {
                    // å‘送取货指令
                    await HandlePutFinishedStateAsync(task, ipAddress);
                }
            }
        }
        /// <summary>
        /// å¤„理取货完成后的放货指令
        /// </summary>
        /// <remarks>
        /// å½“取货完成后,向机器人发送放货指令(Putbattery)。
        /// æœºå™¨äººæ”¶åˆ°æŒ‡ä»¤åŽä¼šå°†è´§ç‰©æ”¾ç½®åˆ°ç›®æ ‡åœ°å€ã€‚
        ///
        /// æŒ‡ä»¤æ ¼å¼ï¼šPutbattery,{目标地址}
        /// ä¾‹å¦‚:Putbattery,B01 è¡¨ç¤ºå°†è´§ç‰©æ”¾ç½®åˆ° B01 ä½ç½®
        /// </remarks>
        /// <param name="task">当前任务</param>
        /// <param name="ipAddress">机器人 IP åœ°å€</param>
        private async Task HandlePickFinishedStateAsync(Dt_RobotTask task, string ipAddress)
        {
            // æž„建放货指令,格式:Putbattery,{目标地址}
            string taskString = $"Putbattery,{task.RobotTargetAddress}";
            // é€šè¿‡å®¢æˆ·ç«¯ç®¡ç†å™¨å‘送指令到机器人
            bool result = await _clientManager.SendToClientAsync(ipAddress, taskString);
            if (result)
            {
                // å‘送成功,记录日志
                QuartzLogger.Error($"下发放货指令,指�?: {taskString}", task.RobotRoadway);
                // æ›´æ–°ä»»åŠ¡çŠ¶æ€ä¸º"机器人执行中"
                task.RobotTaskState = TaskRobotStatusEnum.RobotExecuting.GetHashCode();
                // èŽ·å–æœ€æ–°çŠ¶æ€å¹¶æ›´æ–°ä»»åŠ¡å…³è”
                var stateToUpdate = _stateManager.GetState(ipAddress);
                if (stateToUpdate != null)
                {
                    stateToUpdate.CurrentTask = task;
                    // å®‰å…¨æ›´æ–°çŠ¶æ€åˆ° Redis
                    if (_stateManager.TryUpdateStateSafely(ipAddress, stateToUpdate))
                    {
                        // çŠ¶æ€æ›´æ–°æˆåŠŸåŽï¼Œæ›´æ–°ä»»åŠ¡è®°å½•
                        await _robotTaskService.UpdateRobotTaskAsync(task);
                    }
                }
            }
        }
        /// <summary>
        /// å¤„理放货完成后的取货指令
        /// </summary>
        /// <remarks>
        /// å½“放货完成后,根据任务类型决定下一步:
        ///
        /// 1. å¦‚果不是拆盘也不是组盘(普通任务):
        ///    - ç›´æŽ¥å‘送取货指令
        ///
        /// 2. å¦‚果是组盘或换盘任务:
        ///    - ç”Ÿæˆæ–°çš„æ‰˜ç›˜æ¡ç 
        ///    - å°†æ¡ç æ·»åŠ åˆ°çŠ¶æ€ä¸­
        ///    - å‘送取货指令
        ///
        /// ç»„盘任务的条码用于标识新生成的托盘,
        /// åŽç»­æ”¾è´§æ—¶ä¼šç”¨åˆ°è¿™äº›æ¡ç ä¿¡æ¯ã€‚
        /// </remarks>
        /// <param name="task">当前任务</param>
        /// <param name="ipAddress">机器人 IP åœ°å€</param>
        private async Task HandlePutFinishedStateAsync(Dt_RobotTask task, string ipAddress)
        {
            // èŽ·å–æœ€æ–°çŠ¶æ€
            var stateForUpdate = _stateManager.GetState(ipAddress);
            if (stateForUpdate == null)
            {
                return;
            }
            // å¦‚果状态中还没有设置任务类型标志,根据任务类型设置
            if (!stateForUpdate.IsSplitPallet && !stateForUpdate.IsGroupPallet)
            {
                // åˆ¤æ–­ä»»åŠ¡ç±»åž‹å¹¶è®¾ç½®ç›¸åº”çš„æ ‡å¿—
                stateForUpdate.IsSplitPallet = task.RobotTaskType == RobotTaskTypeEnum.SplitPallet.GetHashCode();
                stateForUpdate.IsGroupPallet = task.RobotTaskType == RobotTaskTypeEnum.GroupPallet.GetHashCode()
                    || task.RobotTaskType == RobotTaskTypeEnum.ChangePallet.GetHashCode();
            }
            // å¦‚果是组盘任务(包括换盘)
            if (task.RobotTaskType == RobotTaskTypeEnum.GroupPallet.GetHashCode())
            {
                // ç”Ÿæˆæ‰˜ç›˜æ¡ç å‰ç¼€
                const string prefix = "TRAY";
                // ç”Ÿæˆä¸¤ä¸ªæ‰˜ç›˜æ¡ç ï¼ˆç”¨äºŽç»„盘操作)
                string trayBarcode1 = RobotBarcodeGenerator.GenerateTrayBarcode(prefix);
                string trayBarcode2 = RobotBarcodeGenerator.GenerateTrayBarcode(prefix);
                // å¦‚果条码生成成功
                if (!string.IsNullOrEmpty(trayBarcode1) && !string.IsNullOrEmpty(trayBarcode2))
                {
                    // å°†æ¡ç æ·»åŠ åˆ°çŠ¶æ€ä¸­ï¼Œä¾›åŽç»­æ”¾è´§æ—¶ä½¿ç”¨
                    stateForUpdate.CellBarcode.Add(trayBarcode1);
                    stateForUpdate.CellBarcode.Add(trayBarcode2);
                    // è®°å½•日志
                    QuartzLogger.Error($"ȡ�������о�ţ���о: {trayBarcode1}+{trayBarcode2}", stateForUpdate.RobotCrane.DeviceName);
                    // å‘送取货指令
                    await _taskProcessor.SendSocketRobotPickAsync(task, stateForUpdate);
                }
            }
            else
            {
                // éžç»„盘任务,直接发送取货指令
                await _taskProcessor.SendSocketRobotPickAsync(task, stateForUpdate);
            }
        }
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/SocketServer/SocketClientGateway.cs
@@ -5,36 +5,69 @@
namespace WIDESEAWCS_Tasks.SocketServer
{
    /// <summary>
    /// TcpSocketServer çš„适配器实现,保持底层行为不变,仅做访问收口。
    /// TcpSocketServer çš„网关实现
    /// </summary>
    /// <remarks>
    /// å®žçް ISocketClientGateway æŽ¥å£ï¼Œå°†åº•层 TCP é€šä¿¡ç»†èŠ‚å°è£…ã€‚
    /// ä½¿ä¸šåŠ¡å±‚ä¸ç›´æŽ¥ä¾èµ– TcpSocketServer,便于单元测试和替换实现。
    /// </remarks>
    public class SocketClientGateway : ISocketClientGateway
    {
        /// <summary>
        /// TCP Socket æœåŠ¡å™¨å®žä¾‹
        /// </summary>
        private readonly TcpSocketServer _tcpSocket;
        /// <summary>
        /// æž„造函数
        /// </summary>
        /// <param name="tcpSocket">TCP Socket æœåŠ¡å™¨å®žä¾‹</param>
        public SocketClientGateway(TcpSocketServer tcpSocket)
        {
            _tcpSocket = tcpSocket;
        }
        /// <summary>
        /// å¼‚步发送消息到指定客户端
        /// </summary>
        /// <param name="clientId">目标客户端 ID</param>
        /// <param name="message">消息内容</param>
        /// <returns>发送是否成功</returns>
        public Task<bool> SendToClientAsync(string clientId, string message)
        {
            return _tcpSocket.SendToClientAsync(clientId, message);
        }
        /// <summary>
        /// é€šè¿‡ TcpClient å‘送消息
        /// </summary>
        /// <param name="client">TCP å®¢æˆ·ç«¯è¿žæŽ¥</param>
        /// <param name="message">消息内容</param>
        public Task SendMessageAsync(TcpClient client, string message)
        {
            return _tcpSocket.SendMessageAsync(client, message);
        }
        /// <summary>
        /// èŽ·å–æ‰€æœ‰å·²è¿žæŽ¥å®¢æˆ·ç«¯ ID
        /// </summary>
        /// <returns>客户端 ID åˆ—表</returns>
        public IReadOnlyList<string> GetClientIds()
        {
            return _tcpSocket.GetClientIds();
        }
        /// <summary>
        /// å¤„理客户端连接的消息循环
        /// </summary>
        /// <param name="client">TCP å®¢æˆ·ç«¯è¿žæŽ¥</param>
        /// <param name="clientId">客户端 ID</param>
        /// <param name="cancellationToken">取消令牌</param>
        /// <param name="robotCrane">机器人状态</param>
        /// <returns>任务</returns>
        public Task HandleClientAsync(TcpClient client, string clientId, CancellationToken cancellationToken, RobotSocketState robotCrane)
        {
            return _tcpSocket.HandleClientAsync(client, clientId, cancellationToken, robotCrane);
        }
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/SocketServer/SocketServerHostedService.cs
@@ -4,29 +4,60 @@
namespace WIDESEAWCS_Tasks.SocketServer
{
    /// <summary>
    /// Socket服务端托管服务
    /// Socket æœåŠ¡å™¨åŽå°ä¸»æœºæœåŠ¡
    /// </summary>
    /// <remarks>
    /// å®žçް IHostedService æŽ¥å£ï¼Œä½œä¸º ASP.NET Core çš„后台服务运行。
    /// è´Ÿè´£åœ¨åº”用启动时启动 Socket æœåŠ¡å™¨ï¼Œåœæ­¢æ—¶å…³é—­æœåŠ¡å™¨ã€‚
    /// </remarks>
    public class SocketServerHostedService : IHostedService
    {
        /// <summary>
        /// TCP Socket æœåŠ¡å™¨å®žä¾‹
        /// </summary>
        private readonly TcpSocketServer _server;
        /// <summary>
        /// Socket æœåŠ¡å™¨é…ç½®é€‰é¡¹
        /// </summary>
        private readonly SocketServerOptions _options;
        /// <summary>
        /// æž„造函数
        /// </summary>
        /// <param name="server">TCP Socket æœåŠ¡å™¨å®žä¾‹</param>
        /// <param name="options">配置选项</param>
        public SocketServerHostedService(TcpSocketServer server, IOptions<SocketServerOptions> options)
        {
            _server = server;
            _options = options.Value;
        }
        /// <summary>
        /// å¯åЍ Socket æœåС噍
        /// </summary>
        /// <remarks>
        /// å¦‚果配置中服务器被禁用(Enabled=false),则不启动。
        /// </remarks>
        /// <param name="cancellationToken">取消令牌</param>
        /// <returns>启动任务</returns>
        public Task StartAsync(CancellationToken cancellationToken)
        {
            // æ£€æŸ¥æœåŠ¡å™¨æ˜¯å¦å¯ç”¨
            if (!_options.Enabled)
            {
                return Task.CompletedTask;
            }
            // å¯åŠ¨æœåŠ¡å™¨
            return _server.StartAsync(cancellationToken);
        }
        /// <summary>
        /// åœæ­¢ Socket æœåС噍
        /// </summary>
        /// <param name="cancellationToken">取消令牌</param>
        /// <returns>停止任务</returns>
        public Task StopAsync(CancellationToken cancellationToken)
        {
            return _server.StopAsync(cancellationToken);
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/SocketServer/SocketServerOptions.cs
@@ -3,63 +3,106 @@
namespace WIDESEAWCS_Tasks.SocketServer
{
    /// <summary>
    /// Socket服务端配置
    /// Socket æœåŠ¡å™¨é…ç½®é€‰é¡¹
    /// </summary>
    /// <remarks>
    /// ç”¨äºŽé…ç½® TCP Socket æœåŠ¡å™¨çš„è¿è¡Œå‚æ•°ã€‚
    /// é…ç½®é€šè¿‡ appsettings.json çš„ SocketServer èŠ‚ç‚¹åŠ è½½ã€‚
    /// </remarks>
    public class SocketServerOptions : IConfigurableOptions
    {
        /// <summary>
        /// æ˜¯å¦å¯ç”¨
        /// æ˜¯å¦å¯ç”¨ Socket æœåС噍
        /// </summary>
        /// <remarks>
        /// è®¾ç½®ä¸º false æ—¶ï¼ŒæœåŠ¡å™¨ä¸ä¼šå¯åŠ¨ã€‚
        /// </remarks>
        public bool Enabled { get; set; } = true;
        /// <summary>
        /// ç›‘听端口
        /// æœåŠ¡å™¨ç›‘å¬ç«¯å£
        /// </summary>
        /// <remarks>
        /// TCP å®¢æˆ·ç«¯è¿žæŽ¥åˆ°æ­¤ç«¯å£ã€‚
        /// é»˜è®¤ä¸º 2000。
        /// </remarks>
        public int Port { get; set; } = 2000;
        /// <summary>
        /// ç›‘听地址
        /// ç›‘听地址
        /// </summary>
        /// <remarks>
        /// æœåŠ¡å™¨ç»‘å®šåˆ°æ­¤åœ°å€ã€‚
        /// 0.0.0.0 è¡¨ç¤ºç›‘听所有网络接口。
        /// </remarks>
        public string IpAddress { get; set; } = "0.0.0.0";
        /// <summary>
        /// è¿žæŽ¥é˜Ÿåˆ—长度
        /// è¿žæŽ¥é˜Ÿåˆ—长度
        /// </summary>
        /// <remarks>
        /// ç­‰å¾…接受的连接队列最大长度。
        /// </remarks>
        public int Backlog { get; set; } = 1000;
        /// <summary>
        /// æ–‡æœ¬ç¼–码名称(例如: utf-8, gbk)
        /// å­—符编码名称
        /// </summary>
        /// <remarks>
        /// ç”¨äºŽæ¶ˆæ¯çš„字符编码,如 utf-8、gbk。
        /// </remarks>
        public string EncodingName { get; set; } = "utf-8";
        /// <summary>
        /// æ˜¯å¦è‡ªåŠ¨æ£€æµ‹ç¼–ç ï¼ˆå°è¯• UTF-8 åŽå›žé€€åˆ° GBK)
        /// æ˜¯å¦è‡ªåŠ¨æ£€æµ‹ç¼–ç ï¼ˆé’ˆå¯¹ GBK å®¢æˆ·ç«¯ï¼‰
        /// </summary>
        /// <remarks>
        /// å½“设置为 true æ—¶ï¼Œä¼šè‡ªåŠ¨æ£€æµ‹å®¢æˆ·ç«¯æ¶ˆæ¯çš„ç¼–ç ã€‚
        /// å¦‚果消息是 UTF-8 æ ¼å¼åˆ™ç”¨ UTF-8 è§£ç ï¼Œå¦åˆ™å°è¯• GBK è§£ç ã€‚
        /// </remarks>
        public bool AutoDetectEncoding { get; set; } = true;
        /// <summary>
        /// å®¢æˆ·ç«¯ç©ºé—²è¶…时时间(秒),超过则断开
        /// å®¢æˆ·ç«¯ç©ºé—²è¶…时时间(秒)
        /// </summary>
        /// <remarks>
        /// å¦‚果客户端在此时间内没有活动,断开连接。
        /// è®¾ç½®ä¸º 0 è¡¨ç¤ºä¸å¯ç”¨è¶…时。
        /// </remarks>
        public int IdleTimeoutSeconds { get; set; } = 300;
        /// <summary>
        /// æ˜¯å¦å¯ç”¨å¿ƒè·³æ£€æŸ¥
        /// æ˜¯å¦å¯ç”¨å¿ƒè·³æ£€æµ‹
        /// </summary>
        /// <remarks>
        /// å¯ç”¨åŽï¼Œä¼šåœ¨è¿žæŽ¥ç©ºé—²æ—¶å‘送心跳探测。
        /// </remarks>
        public bool EnableHeartbeat { get; set; } = true;
        /// <summary>
        /// æ—¥å¿—文件路径(相对于程序运行目录)
        /// æ—¥å¿—文件路径
        /// </summary>
        /// <remarks>
        /// æ—¥å¿—文件的相对路径,相对于应用程序目录。
        /// </remarks>
        public string LogFilePath { get; set; } = "socketserver.log";
        /// <summary>
        /// æ¶ˆæ¯å¤´æ ‡è®°
        /// æ¶ˆæ¯å¤´æ ‡è¯†
        /// </summary>
        /// <remarks>
        /// ç”¨äºŽå¸§è§£æžçš„æ¶ˆæ¯å¤´ã€‚
        /// æŽ¥æ”¶æ¶ˆæ¯æ—¶æŸ¥æ‰¾æ­¤å¤´æ ‡è¯†ã€‚
        /// </remarks>
        public string MessageHeader { get; set; } = "<START>";
        /// <summary>
        /// æ¶ˆæ¯å°¾æ ‡è®°
        /// æ¶ˆæ¯å°¾æ ‡è¯†
        /// </summary>
        /// <remarks>
        /// ç”¨äºŽå¸§è§£æžçš„æ¶ˆæ¯å°¾ã€‚
        /// æŽ¥æ”¶æ¶ˆæ¯æ—¶æŸ¥æ‰¾æ­¤å°¾æ ‡è¯†ã€‚
        /// </remarks>
        public string MessageFooter { get; set; } = "<END>";
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/SocketServer/TcpSocketServer.Clients.cs
@@ -10,10 +10,13 @@
    public partial class TcpSocketServer
    {
        /// <summary>
        /// æ£€ç´¢å½“前在服务中注册的所有客户端标识符的只读列表。
        /// èŽ·å–æ‰€æœ‰å·²è¿žæŽ¥å®¢æˆ·ç«¯ ID åˆ—表
        /// </summary>
        /// <remarks>返回的列表表示调用时刻客户端ID的快照。后续对客户端集合的更改不会影响返回的列表。此方法是线程安全的。</remarks>
        /// <returns>包含客户端ID的<see cref="IReadOnlyList{String}"/>。如果没有客户端注册,列表将为空。</returns>
        /// <remarks>
        /// è¿”回当前在服务器注册的客户端标识列表。
        /// è¿™æ˜¯ä¸€ä¸ªåªè¯»åˆ—表的快照,线程安全。
        /// </remarks>
        /// <returns>客户端 ID åˆ—表</returns>
        public IReadOnlyList<string> GetClientIds()
        {
            lock (_syncRoot)
@@ -23,11 +26,13 @@
        }
        /// <summary>
        /// æ£€ç´¢ä¸ŽæŒ‡å®šè®¾å¤‡æ ‡è¯†ç¬¦å…³è”的客户端标识符。
        /// æ ¹æ®è®¾å¤‡ ID èŽ·å–å®¢æˆ·ç«¯ ID
        /// </summary>
        /// <remarks>此方法是线程安全的。如果未找到设备标识符,方法将返回null而不是抛出异常。</remarks>
        /// <param name="deviceId">要检索客户端标识符的设备的唯一标识符。不能为null。</param>
        /// <returns>与指定设备标识符关联的客户端标识符,如果不存在关联则返回null。</returns>
        /// <remarks>
        /// åœ¨è®¾å¤‡ç»‘定表中查找对应的客户端 ID。
        /// </remarks>
        /// <param name="deviceId">设备唯一标识</param>
        /// <returns>客户端 ID,如果未找到则返回 null</returns>
        public string? GetClientIdByDevice(string deviceId)
        {
            lock (_syncRoot)
@@ -37,13 +42,15 @@
        }
        /// <summary>
        /// å¼‚步向指定设备发送消息。
        /// å¼‚步向指定设备发送消息
        /// </summary>
        /// <remarks>如果指定设备未注册或无法找到,则返回 <see langword="false"/>。</remarks>
        /// <param name="deviceId">目标设备的唯一标识符。不能为null或空。</param>
        /// <param name="message">要发送给设备的消息。不能为null。</param>
        /// <returns>表示异步操作的任务。如果消息成功发送,任务结果为 <see langword="true"/>;
        /// å¦åˆ™ä¸º <see langword="false"/>。</returns>
        /// <remarks>
        /// é€šè¿‡è®¾å¤‡ ID æŸ¥æ‰¾å¯¹åº”的客户端连接,然后发送消息。
        /// å¦‚果设备未注册或连接不存在,返回 false。
        /// </remarks>
        /// <param name="deviceId">目标设备唯一标识</param>
        /// <param name="message">要发送的消息</param>
        /// <returns>发送是否成功</returns>
        public Task<bool> SendToDeviceAsync(string deviceId, string message)
        {
            var clientId = GetClientIdByDevice(deviceId);
@@ -52,15 +59,16 @@
        }
        /// <summary>
        /// é€šè¿‡TCP连接异步向指定客户端发送带帧的文本消息。
        /// å¼‚步向指定客户端发送消息
        /// </summary>
        /// <remarks>如果客户端未连接或不存在,此方法将返回 <see langword="false"/> ä¸”不发送消息。
        /// æ¶ˆæ¯å°†ä¼˜å…ˆä½¿ç”¨å®¢æˆ·ç«¯é¦–选的文本编码进行编码;否则使用默认编码。
        /// æ­¤æ–¹æ³•对于向不同客户端的并发调用是线程安全的。</remarks>
        /// <param name="clientId">要发送消息到的客户端的唯一标识符。必须对应已连接的客户端。</param>
        /// <param name="message">要发送给客户端的文本消息。不能为null。</param>
        /// <returns>表示异步操作的任务。如果消息成功发送,任务结果为 <see langword="true"/>;
        /// å¦åˆ™ï¼Œå¦‚果客户端未连接或不存在,结果为 <see langword="false"/>。</returns>
        /// <remarks>
        /// ä½¿ç”¨å¸§æ ¼å¼å‘送消息(添加头尾标识)。
        /// æ¯ä¸ªå®¢æˆ·ç«¯çš„发送操作是互斥的(通过信号量实现)。
        /// å¦‚果客户端未连接或不存在,发送失败返回 false。
        /// </remarks>
        /// <param name="clientId">目标客户端 ID</param>
        /// <param name="message">要发送的消息</param>
        /// <returns>发送是否成功</returns>
        public async Task<bool> SendToClientAsync(string clientId, string message)
        {
            TcpClient? client;
@@ -80,9 +88,11 @@
            enc ??= _textEncoding;
            // èŽ·å–å®¢æˆ·ç«¯å‘é€é”
            if (sem != null) await sem.WaitAsync();
            try
            {
                // å‘送消息
                var ns = client.GetStream();
                var framedMessage = BuildFramedMessage(message);
                var data = enc.GetBytes(framedMessage);
@@ -96,12 +106,13 @@
        }
        /// <summary>
        /// å¼‚步向所有已连接的客户端发送指定的消息。
        /// å¼‚步广播消息到所有客户端
        /// </summary>
        /// <remarks>如果向某个客户端发送消息时发生错误,异常将被抑制并继续向其他客户端广播。
        /// å½“所有发送操作完成后,此方法结束。</remarks>
        /// <param name="message">要广播给所有客户端的消息。不能为null。</param>
        /// <returns>表示异步广播操作的任务。</returns>
        /// <remarks>
        /// å°†æ¶ˆæ¯å‘送给所有已连接的客户端。
        /// å¦‚果某个客户端发送失败,不影响其他客户端的发送。
        /// </remarks>
        /// <param name="message">要广播的消息</param>
        public async Task BroadcastAsync(string message)
        {
            List<TcpClient> clients;
@@ -110,6 +121,7 @@
                clients = _clients.Values.ToList();
            }
            // å¹¶è¡Œå‘送消息到所有客户端
            await Task.WhenAll(clients.Select(c => Task.Run(async () =>
            {
                try { await SendMessageAsync(c, message); } catch { }
@@ -117,13 +129,14 @@
        }
        /// <summary>
        /// é€šè¿‡ç½‘络流异步向指定的TCP客户端发送带帧的文本消息。
        /// é€šè¿‡ NetworkStream å‘送消息
        /// </summary>
        /// <remarks>如果客户端为null或未连接,此方法将立即返回而不发送消息。
        /// æ¶ˆæ¯å°†ä½¿ç”¨é…ç½®çš„æ–‡æœ¬ç¼–码进行编码并添加帧头后通过网络流发送。</remarks>
        /// <param name="client">要发送消息到的TCP客户端。必须处于连接状态;否则方法不执行任何操作。</param>
        /// <param name="message">要发送给客户端的文本消息。消息在传输前将被编码并添加帧头。</param>
        /// <returns>表示异步发送操作的任务。</returns>
        /// <remarks>
        /// ç›´æŽ¥ä½¿ç”¨ TcpClient çš„ NetworkStream å‘送消息。
        /// æ¶ˆæ¯ä¼šæ·»åŠ å¸§å¤´å¸§å°¾ã€‚
        /// </remarks>
        /// <param name="client">TCP å®¢æˆ·ç«¯</param>
        /// <param name="message">消息内容</param>
        public async Task SendMessageAsync(TcpClient client, string message)
        {
            if (client == null || !client.Connected)
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/SocketServer/TcpSocketServer.Dispose.cs
@@ -6,17 +6,28 @@
    public partial class TcpSocketServer
    {
        /// <summary>
        /// é‡Šæ”¾æœåŠ¡å™¨ä½¿ç”¨çš„æ‰€æœ‰èµ„æºå¹¶åœæ­¢ç›‘å¬ä¼ å…¥è¿žæŽ¥ã€‚
        /// é‡Šæ”¾æœåŠ¡å™¨èµ„æº
        /// </summary>
        /// <remarks>当不再需要服务器时调用此方法,以确保所有相关资源(如网络监听器和同步原语)被正确释放。
        /// è°ƒç”¨ <see cref="Dispose"/> åŽï¼ŒæœåŠ¡å™¨æ— æ³•é‡æ–°å¯åŠ¨æˆ–å†æ¬¡ä½¿ç”¨ã€‚</remarks>
        /// <remarks>
        /// åœæ­¢ç›‘听、取消所有客户端任务、关闭监听器、释放信号量。
        /// è°ƒç”¨æ­¤æ–¹æ³•后,服务器无法再次使用。
        /// </remarks>
        public void Dispose()
        {
            // å–消所有操作
            _cts?.Cancel();
            // åœæ­¢ç›‘听器
            _listener?.Stop();
            // é‡Šæ”¾å–消令牌源
            _cts?.Dispose();
            // é‡Šæ”¾æ‰€æœ‰å®¢æˆ·ç«¯ä¿¡å·é‡
            foreach (var sem in _clientLocks.Values) { try { sem.Dispose(); } catch { } }
            _clientLocks.Clear();
            // è®°å½•停止日志
            Log($"[{DateTime.Now}] TcpSocketServer stopped");
        }
    }
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/SocketServer/TcpSocketServer.Messaging.cs
@@ -8,16 +8,21 @@
    public partial class TcpSocketServer
    {
        /// <summary>
        /// å¼‚步处理与已连接的TCP客户端的通信,处理机器人起重机会话中的传入消息和客户端状态更新。
        /// å¤„理客户端连接的消息循环
        /// </summary>
        /// <remarks>此方法管理客户端连接的生命周期,包括读取消息、更新客户端状态和调用相关事件。
        /// å½“处理结束时,客户端和相关的网络资源将被释放。如果启用心跳或空闲超时选项,
        /// å°†åº”用额外的取消逻辑。事件调用期间的异常将被捕获并抑制,以确保会话处理的鲁棒性。</remarks>
        /// <param name="client">表示要处理的远程连接的TCP客户端。方法完成后将释放此对象。</param>
        /// <param name="clientId">已连接客户端的唯一标识符。用于在整个会话中跟踪和更新客户端状态。</param>
        /// <param name="cancellationToken">可用于取消客户端处理操作的取消令牌。如果请求取消,方法将立即终止处理。</param>
        /// <param name="robotCrane">表示与客户端关联的机器人起重机的当前状态对象。用于为消息处理和事件调用提供上下文。</param>
        /// <returns>表示处理客户端连接的异步操作的任务。当客户端断开连接或请求取消时任务完成。</returns>
        /// <remarks>
        /// æŒç»­æŽ¥æ”¶å®¢æˆ·ç«¯æ¶ˆæ¯ï¼Œç›´åˆ°è¿žæŽ¥æ–­å¼€æˆ–取消。
        /// å¤„理流程:
        /// 1. æŽ¥æ”¶æ¶ˆæ¯ï¼ˆå¸§è§£æžï¼‰
        /// 2. æ›´æ–°å®¢æˆ·ç«¯çŠ¶æ€ï¼ˆæ´»è·ƒæ—¶é—´ã€ç¼–ç ï¼‰
        /// 3. å¤„理设备注册
        /// 4. è§¦å‘ MessageReceived äº‹ä»¶
        /// è¿žæŽ¥æ–­å¼€æ—¶æ¸…理资源并触发 RobotReceived äº‹ä»¶ã€‚
        /// </remarks>
        /// <param name="client">TCP å®¢æˆ·ç«¯è¿žæŽ¥</param>
        /// <param name="clientId">客户端唯一标识</param>
        /// <param name="cancellationToken">取消令牌</param>
        /// <param name="robotCrane">机器人状态</param>
        public async Task HandleClientAsync(TcpClient client, string clientId, CancellationToken cancellationToken, RobotSocketState robotCrane)
        {
            using (client)
@@ -28,19 +33,21 @@
                CancellationTokenSource? localCts = null;
                if (_options.EnableHeartbeat || _options.IdleTimeoutSeconds > 0)
                {
                    // åˆ›å»ºé“¾æŽ¥çš„取消令牌源
                    localCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
                }
                try
                {
                    // æ¶ˆæ¯æŽ¥æ”¶å¾ªçޝ
                    while (!cancellationToken.IsCancellationRequested && client.Connected)
                    {
                        string? message;
                        try
                        {
                            var ct = localCts?.Token ?? cancellationToken;
                            // æŽ¥æ”¶å®Œæ•´æ¶ˆæ¯ï¼ˆå¸§è§£æžï¼‰
                            message = await ReceiveFullMessageAsync(networkStream, _textEncoding, ct);
                            //message = await reader.ReadLineAsync().WaitAsync(ct);
                        }
                        catch (OperationCanceledException)
                        {
@@ -52,20 +59,23 @@
                            break;
                        }
                        // æ›´æ–°å®¢æˆ·ç«¯çŠ¶æ€
                        UpdateClientStatus(clientId, message);
                        string messageLower = message.ToLowerInvariant();
                        // å¤„理注册消息
                        if (TryHandleRegister(messageLower, message, clientId, networkStream, cancellationToken))
                        {
                            continue;
                        }
                        // è§¦å‘消息接收事件
                        if (MessageReceived != null)
                        {
                            try
                            {
                                // åˆ¤æ–­æ˜¯å¦ä¸º JSON æ ¼å¼
                                // åˆ¤æ–­æ˜¯å¦ä¸º JSON æ ¼å¼
                                bool isJsonFormat = TryParseJsonSilent(message);
                                _ = MessageReceived.Invoke(message, isJsonFormat, client, robotCrane);
                            }
@@ -75,6 +85,7 @@
                }
                finally
                {
                    // æ¸…理资源
                    try { localCts?.Cancel(); localCts?.Dispose(); } catch { }
                    RemoveClient(clientId);
                    try { _ = RobotReceived.Invoke(clientId); } catch { }
@@ -83,17 +94,18 @@
        }
        /// <summary>
        /// å°è¯•处理来自客户端的设备注册请求。返回一个值指示该消息是否被作为注册请求处理。
        /// å¤„理设备注册消息
        /// </summary>
        /// <remarks>如果消息是有效的注册请求且包含非空的设备标识符,
        /// åˆ™å°†è®¾å¤‡ç»‘定到客户端并发送确认信息。此方法不会因无效消息而抛出异常;
        /// å®ƒä»…返回 false。</remarks>
        /// <param name="messageLower">客户端消息的小写版本,用于判断消息是否为注册请求。</param>
        /// <param name="message">包含注册命令和设备标识符的原始客户端消息。</param>
        /// <param name="clientId">发送注册请求的客户端的唯一标识符。</param>
        /// <param name="client">与客户端通信的TCP客户端连接。</param>
        /// <param name="cancellationToken">可用于取消注册操作的取消令牌。</param>
        /// <returns>如果消息被识别并作为注册请求处理,则返回 true;否则返回 false。</returns>
        /// <remarks>
        /// æ³¨å†Œæ¶ˆæ¯æ ¼å¼ï¼šregister,{deviceId}
        /// å°†è®¾å¤‡ ID ç»‘定到当前客户端 ID。
        /// </remarks>
        /// <param name="messageLower">消息小写版本</param>
        /// <param name="message">原始消息</param>
        /// <param name="clientId">客户端 ID</param>
        /// <param name="networkStream">网络流</param>
        /// <param name="cancellationToken">取消令牌</param>
        /// <returns>是否处理了注册消息</returns>
        private bool TryHandleRegister(string messageLower, string message, string clientId, NetworkStream networkStream, CancellationToken cancellationToken)
        {
            if (!messageLower.StartsWith("register,"))
@@ -101,14 +113,17 @@
                return false;
            }
            // æå–设备 ID
            string deviceId = message.Substring("register,".Length).Trim();
            if (!string.IsNullOrEmpty(deviceId))
            {
                lock (_syncRoot)
                {
                    // ç»‘定设备到客户端
                    _deviceBindings[deviceId] = clientId;
                }
                // å›žå¤æ³¨å†ŒæˆåŠŸ
                _ = WriteToClientAsync(clientId, networkStream, $"Registered,{deviceId}", cancellationToken);
            }
@@ -116,20 +131,27 @@
        }
        /// <summary>
        /// æ›´æ–°å®¢æˆ·ç«¯çŠ¶æ€
        /// æ›´æ–°å®¢æˆ·ç«¯çŠ¶æ€
        /// </summary>
        /// <param name="clientId"></param>
        /// <param name="message"></param>
        /// <remarks>
        /// æ›´æ–°æœ€åŽæ´»è·ƒæ—¶é—´å’Œå­—符编码。
        /// å¦‚果开启了自动编码检测,根据消息内容判断是 UTF-8 è¿˜æ˜¯ GBK。
        /// </remarks>
        /// <param name="clientId">客户端 ID</param>
        /// <param name="message">最新接收的消息</param>
        private void UpdateClientStatus(string clientId, string message)
        {
            lock (_syncRoot)
            {
                // æ›´æ–°æœ€åŽæ´»è·ƒæ—¶é—´
                _clientLastActive[clientId] = DateTime.Now;
                // å¦‚果还没有记录编码
                if (!_clientEncodings.ContainsKey(clientId))
                {
                    if (_options.AutoDetectEncoding && _autoDetectedGb2312 != null)
                    {
                        // è‡ªåŠ¨æ£€æµ‹ç¼–ç ï¼šJSON æˆ– UTF-8 å­—节特征则用 UTF-8,否则用 GBK
                        bool isUtf8 = TryParseJsonSilent(message) || IsLikelyUtf8(_textEncoding.GetBytes(message));
                        _clientEncodings[clientId] = isUtf8 ? _textEncoding : _autoDetectedGb2312;
                    }
@@ -142,13 +164,11 @@
        }
        /// <summary>
        /// å†™å…¥æ¶ˆæ¯åˆ°å®¢æˆ·ç«¯
        /// å¼‚步发送消息到客户端
        /// </summary>
        /// <param name="clientId"></param>
        /// <param name="networkStream"></param>
        /// <param name="message"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        /// <remarks>
        /// å†…部方法,不使用帧格式,直接发送原始消息。
        /// </remarks>
        private async Task WriteToClientAsync(string clientId, NetworkStream networkStream, string message, CancellationToken cancellationToken)
        {
            SemaphoreSlim? sem = null;
@@ -175,10 +195,13 @@
        }
        /// <summary>
        /// æ·»åŠ æ¶ˆæ¯å¸§å¤´å°¾
        /// æž„建帧消息
        /// </summary>
        /// <param name="message"></param>
        /// <returns></returns>
        /// <remarks>
        /// åœ¨æ¶ˆæ¯å‰åŽæ·»åŠ å¤´å°¾æ ‡è¯†ã€‚
        /// </remarks>
        /// <param name="message">原始消息</param>
        /// <returns>带帧标识的消息</returns>
        private string BuildFramedMessage(string message)
        {
            var header = _options.MessageHeader ?? string.Empty;
@@ -187,10 +210,14 @@
        }
        /// <summary>
        /// JSON格式尝试解析(静默失败)
        /// é™é»˜å°è¯•解析 JSON
        /// </summary>
        /// <param name="message"></param>
        /// <returns></returns>
        /// <remarks>
        /// åˆ¤æ–­æ¶ˆæ¯æ˜¯å¦ä»¥ { æˆ– [ å¼€å¤´ï¼Œå¦‚果是则尝试解析。
        /// è§£æžå¤±è´¥ä¸æŠ›å¼‚常。
        /// </remarks>
        /// <param name="message">消息内容</param>
        /// <returns>是否是有效的 JSON æ ¼å¼</returns>
        private static bool TryParseJsonSilent(string message)
        {
            if (string.IsNullOrWhiteSpace(message)) return false;
@@ -200,30 +227,35 @@
        }
        /// <summary>
        /// utf-8 å¯èƒ½æ€§æ£€æµ‹
        /// åˆ¤æ–­å­—节数组是否为 UTF-8 ç¼–码
        /// </summary>
        /// <param name="data"></param>
        /// <returns></returns>
        /// <remarks>
        /// é€šè¿‡æ£€æŸ¥å­—节序列是否符合 UTF-8 å¤šå­—节字符的编码规则。
        /// </remarks>
        /// <param name="data">字节数组</param>
        /// <returns>是否可能是 UTF-8 ç¼–码</returns>
        private static bool IsLikelyUtf8(byte[] data)
        {
            int i = 0;
            while (i < data.Length)
            {
                byte b = data[i];
                if (b <= 0x7F) { i++; continue; }
                if (b >= 0xC2 && b <= 0xDF)
                if (b <= 0x7F) { i++; continue; }  // ASCII å­—符
                // æ£€æŸ¥å¤šå­—节字符
                if (b >= 0xC2 && b <= 0xDF)  // 2字节字符
                {
                    if (i + 1 >= data.Length) return false;
                    if ((data[i + 1] & 0xC0) != 0x80) return false;
                    i += 2; continue;
                }
                if (b >= 0xE0 && b <= 0xEF)
                if (b >= 0xE0 && b <= 0xEF)  // 3字节字符
                {
                    if (i + 2 >= data.Length) return false;
                    if ((data[i + 1] & 0xC0) != 0x80 || (data[i + 2] & 0xC0) != 0x80) return false;
                    i += 3; continue;
                }
                if (b >= 0xF0 && b <= 0xF4)
                if (b >= 0xF0 && b <= 0xF4)  // 4字节字符
                {
                    if (i + 3 >= data.Length) return false;
                    if ((data[i + 1] & 0xC0) != 0x80 || (data[i + 2] & 0xC0) != 0x80 || (data[i + 3] & 0xC0) != 0x80) return false;
@@ -235,12 +267,16 @@
        }
        /// <summary>
        /// è¯»å–完整消息
        /// æŽ¥æ”¶å®Œæ•´æ¶ˆæ¯ï¼ˆå¸§è§£æžï¼‰
        /// </summary>
        /// <param name="networkStream">字节流</param>
        /// <param name="encoding">编码格式</param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        /// <remarks>
        /// æ ¹æ®é…ç½®çš„头尾标识解析消息。
        /// å¦‚果未配置头尾,则一直读到数据不可用。
        /// </remarks>
        /// <param name="networkStream">网络流</param>
        /// <param name="encoding">字符编码</param>
        /// <param name="cancellationToken">取消令牌</param>
        /// <returns>接收到的消息</returns>
        private async Task<string?> ReceiveFullMessageAsync(NetworkStream networkStream, Encoding encoding, CancellationToken cancellationToken)
        {
            var header = _options.MessageHeader ?? string.Empty;
@@ -251,15 +287,18 @@
            while (true)
            {
                // è¯»å–数据
                int bytesRead = await networkStream.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken);
                if (bytesRead <= 0)
                {
                    if (builder.Length == 0) return null;
                    // æ— å¤´å°¾é…ç½®æ—¶ï¼Œè¿”回已有数据
                    return string.IsNullOrEmpty(header) && string.IsNullOrEmpty(footer) ? builder.ToString() : null;
                }
                builder.Append(encoding.GetString(buffer, 0, bytesRead));
                // å¦‚果没有配置头尾,且数据不可用,返回已有数据
                if (string.IsNullOrEmpty(header) && string.IsNullOrEmpty(footer))
                {
                    if (!networkStream.DataAvailable)
@@ -269,6 +308,7 @@
                    continue;
                }
                // æŸ¥æ‰¾å¸§å¤´
                var data = builder.ToString();
                var headerIndex = string.IsNullOrEmpty(header) ? 0 : data.IndexOf(header, StringComparison.Ordinal);
                if (headerIndex < 0)
@@ -276,6 +316,7 @@
                    continue;
                }
                // æå–帧内容
                var startIndex = headerIndex + header.Length;
                var footerIndex = string.IsNullOrEmpty(footer) ? data.Length : data.IndexOf(footer, startIndex, StringComparison.Ordinal);
                if (footerIndex >= 0)
@@ -287,4 +328,4 @@
            return builder.ToString();
        }
    }
}
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/SocketServer/TcpSocketServer.Server.cs
@@ -1,24 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using WIDESEAWCS_Core.Helper;
using System.IO;
using WIDESEAWCS_Core.LogHelper;
namespace WIDESEAWCS_Tasks.SocketServer
{
    public partial class TcpSocketServer
    {
        /// <summary>
        /// å¼‚步启动TCP服务器,使其开始接受传入的客户端连接。
        /// å¼‚步启动 TCP Socket æœåС噍
        /// </summary>
        /// <remarks>如果服务器已在运行或通过配置禁用,此方法将立即返回而不启动服务器。
        /// åŽç»­çš„客户端监控和接受操作在后台任务中运行。此方法不会阻塞调用线程。</remarks>
        /// <param name="cancellationToken">可用于请求取消服务器启动及后续后台操作的取消令牌。</param>
        /// <returns>表示异步启动操作的任务。当服务器开始监听连接时任务完成。</returns>
        /// <remarks>
        /// åˆ›å»º TCP ç›‘听器并开始接受客户端连接。
        /// å¦‚果服务器已在运行或被禁用,直接返回。
        /// å¯åŠ¨åŽå¯åŠ¨æŽ¥å—å¾ªçŽ¯å’Œå®¢æˆ·ç«¯ç›‘æŽ§ä»»åŠ¡ã€‚
        /// </remarks>
        /// <param name="cancellationToken">取消令牌</param>
        /// <returns>启动任务</returns>
        public Task StartAsync(CancellationToken cancellationToken)
        {
            if (IsRunning || !_options.Enabled)
@@ -26,30 +26,36 @@
                return Task.CompletedTask;
            }
            // è§£æžç›‘听地址
            IPAddress ipAddress = IPAddress.Any;
            if (IPAddress.TryParse(_options.IpAddress, out IPAddress? parsedAddress))
            {
                ipAddress = parsedAddress;
            }
            // åˆ›å»ºç›‘听器
            _listener = new TcpListener(ipAddress, _options.Port);
            _listener.Start(_options.Backlog);
            _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
            IsRunning = true;
            // å¯åŠ¨æŽ¥å—å®¢æˆ·ç«¯è¿žæŽ¥å¾ªçŽ¯
            _ = AcceptLoopAsync(_cts.Token);
            // å¯åŠ¨å®¢æˆ·ç«¯ç›‘æŽ§ä»»åŠ¡ï¼ˆæ£€æŸ¥ç©ºé—²è¶…æ—¶ï¼‰
            _monitorTask = Task.Run(() => MonitorClientsAsync(_cts.Token));
            return Task.CompletedTask;
        }
        //// <summary>
        /// å¼‚步停止服务器并等待所有活动客户端连接完成。
        /// <summary>
        /// å¼‚步停止 TCP Socket æœåС噍
        /// </summary>
        /// <remarks>如果服务器未运行,此方法将立即返回而不执行任何操作。
        /// æ­¤æ–¹æ³•确保所有客户端任务完成后才将服务器标记为已停止。</remarks>
        /// <param name="cancellationToken">可用于在完成前取消停止操作的取消令牌。</param>
        /// <returns>表示异步停止操作的任务。</returns>
        /// <remarks>
        /// åœæ­¢æŽ¥å—新连接,等待所有客户端任务完成。
        /// </remarks>
        /// <param name="cancellationToken">取消令牌</param>
        /// <returns>停止任务</returns>
        public async Task StopAsync(CancellationToken cancellationToken)
        {
            if (!IsRunning)
@@ -57,9 +63,13 @@
                return;
            }
            // å‘送取消信号
            _cts?.Cancel();
            // åœæ­¢ç›‘听
            _listener?.Stop();
            // ç­‰å¾…所有客户端任务完成
            Task[] tasks;
            lock (_syncRoot)
            {
@@ -75,12 +85,16 @@
        }
        /// <summary>
        /// æŒç»­æŽ¥å—传入的TCP客户端连接,直到请求取消。
        /// å¼‚步接受客户端连接的主循环
        /// </summary>
        /// <remarks>此方法旨在后台运行以处理新的客户端连接。
        /// å¦‚果监听器被释放或通过提供的令牌请求取消,循环将退出。</remarks>
        /// <param name="cancellationToken">可用于请求取消接受循环的令牌。当请求取消时,循环将立即终止。</param>
        /// <returns>表示异步接受循环操作的任务。</returns>
        /// <summary>
        /// å¼‚步接受客户端连接的主循环
        /// </summary>
        /// <remarks>
        /// åœ¨åŽå°çº¿ç¨‹ä¸­æŒç»­æŽ¥å—新的客户端连接。
        /// å½“有新连接时,将其添加到客户端字典并启动消息处理任务。
        /// </remarks>
        /// <param name="cancellationToken">取消令牌</param>
        private async Task AcceptLoopAsync(CancellationToken cancellationToken)
        {
            while (!cancellationToken.IsCancellationRequested)
@@ -88,31 +102,23 @@
                TcpClient? client = null;
                try
                {
                    // ç­‰å¾…客户端连接
                    client = await _listener!.AcceptTcpClientAsync().WaitAsync(cancellationToken);
                    ConsoleHelper.WriteSuccessLine($"客户端上线:{client.Client.RemoteEndPoint.ToString()}");
                    QuartzLogger.Info($"客户端连接:{client.Client.RemoteEndPoint.ToString()}");
                }
                catch (OperationCanceledException)
                {
                    break;
                }
                catch (ObjectDisposedException)
                {
                    break;
                }
                catch (OperationCanceledException) { break; }
                catch (ObjectDisposedException) { break; }
                catch
                {
                    if (cancellationToken.IsCancellationRequested)
                    {
                        break;
                    }
                    if (cancellationToken.IsCancellationRequested) break;
                }
                if (client == null)
                {
                    continue;
                }
                if (client == null) continue;
                // ç”Ÿæˆå®¢æˆ·ç«¯ ID(使用远程端点地址)
                string clientId = GetClientId(client);
                // æ·»åŠ åˆ°å®¢æˆ·ç«¯å­—å…¸
                lock (_syncRoot)
                {
                    _clients[clientId] = client;
@@ -122,30 +128,41 @@
        }
        /// <summary>
        /// ä»Žå†…部集合中移除指定标识符的客户端,并释放相关资源。
        /// ç§»é™¤å®¢æˆ·ç«¯è¿žæŽ¥
        /// </summary>
        /// <remarks>此方法关闭客户端连接,释放任何关联的锁,并移除对客户端的所有引用,
        /// åŒ…括设备绑定和编码信息。通过对内部同步对象加锁确保线程安全。</remarks>
        /// <param name="clientId">要移除的客户端的唯一标识符。不能为null或空。</param>
        /// <remarks>
        /// å…³é—­å®¢æˆ·ç«¯è¿žæŽ¥å¹¶æ¸…理相关资源:
        /// - å…³é—­ TcpClient
        /// - é‡Šæ”¾ä¿¡å·é‡
        /// - ç§»é™¤æ´»è·ƒæ—¶é—´å’Œç¼–码记录
        /// - ç§»é™¤è®¾å¤‡ç»‘定
        /// </remarks>
        /// <param name="clientId">要移除的客户端唯一标识</param>
        private void RemoveClient(string clientId)
        {
            lock (_syncRoot)
            {
                // å…³é—­å¹¶ç§»é™¤å®¢æˆ·ç«¯è¿žæŽ¥
                if (_clients.TryGetValue(clientId, out var client))
                {
                    try { client.Close(); } catch { }
                    _clients.Remove(clientId);
                }
                // é‡Šæ”¾ä¿¡å·é‡
                if (_clientLocks.TryGetValue(clientId, out var sem))
                {
                    _clientLocks.Remove(clientId);
                    sem.Dispose();
                }
                // ç§»é™¤æ´»è·ƒæ—¶é—´è®°å½•
                _clientLastActive.Remove(clientId);
                // ç§»é™¤ç¼–码记录
                _clientEncodings.Remove(clientId);
                // ç§»é™¤è®¾å¤‡ç»‘定
                var deviceIds = _deviceBindings.Where(kv => kv.Value == clientId).Select(kv => kv.Key).ToList();
                foreach (var deviceId in deviceIds)
                {
@@ -155,12 +172,13 @@
        }
        /// <summary>
        /// å¼‚步监控已连接的客户端,并断开超过配置超时时间闲置的客户端连接。
        /// å¼‚步监控客户端空闲超时
        /// </summary>
        /// <remarks>此方法持续检查闲置客户端,如果其不活动时间超过指定的空闲超时,则断开连接。
        /// ç›‘控循环将持续运行,直到通过提供的令牌请求取消。</remarks>
        /// <param name="cancellationToken">可用于请求终止监控循环的取消令牌。</param>
        /// <returns>表示异步监控操作的任务。</returns>
        /// <remarks>
        /// å®šæœŸæ£€æŸ¥æ‰€æœ‰å®¢æˆ·ç«¯çš„æœ€åŽæ´»è·ƒæ—¶é—´ï¼Œ
        /// å¦‚果超过空闲超时时间,断开该客户端连接。
        /// </remarks>
        /// <param name="cancellationToken">取消令牌</param>
        private async Task MonitorClientsAsync(CancellationToken cancellationToken)
        {
            while (!cancellationToken.IsCancellationRequested)
@@ -172,6 +190,7 @@
                    {
                        foreach (var kv in _clientLastActive)
                        {
                            // æ£€æŸ¥æ˜¯å¦è¶…过空闲超时
                            if (_options.IdleTimeoutSeconds > 0 && DateTime.Now - kv.Value > TimeSpan.FromSeconds(_options.IdleTimeoutSeconds))
                            {
                                toRemove.Add(kv.Key);
@@ -179,27 +198,29 @@
                        }
                    }
                    // æ–­å¼€è¶…时的客户端
                    foreach (var cid in toRemove)
                    {
                        RemoveClient(cid);
                        Log($"[{DateTime.Now}] TcpSocketServer disconnect idle client {cid}");
                    }
                }
                catch
                {
                }
                catch { }
                // æ¯ç§’检查一次
                try { await Task.Delay(1000, cancellationToken); } catch { }
            }
        }
        /// <summary>
        /// åŸºäºŽè¿œç¨‹ç»ˆç«¯ç‚¹èŽ·å–æŒ‡å®šTCP客户端的唯一标识符字符串。
        /// èŽ·å–å®¢æˆ·ç«¯å”¯ä¸€æ ‡è¯†
        /// </summary>
        /// <remarks>返回的标识符适用于在日志记录或跟踪场景中区分客户端。
        /// å¦‚果客户端的远程终端点不可用,将生成GUID以确保唯一性。</remarks>
        /// <param name="client">要获取标识符的TCP客户端。不能为null。</param>
        /// <returns>表示客户端远程终端点的字符串(如果可用);否则为生成的新GUID字符串。</returns>
        /// <remarks>
        /// ä½¿ç”¨å®¢æˆ·ç«¯çš„远程端点地址作为标识。
        /// å¦‚果远程端点不可用,生成随机 GUID。
        /// </remarks>
        /// <param name="client">TCP å®¢æˆ·ç«¯</param>
        /// <returns>客户端标识字符串</returns>
        public static string GetClientId(TcpClient client)
        {
            return client.Client.RemoteEndPoint?.ToString() ?? Guid.NewGuid().ToString();
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/SocketServer/TcpSocketServer.cs
@@ -11,115 +11,191 @@
namespace WIDESEAWCS_Tasks.SocketServer
{
    /// <summary>
    /// TCP Socket服务端(基于行协议,按换行符分割消息)
    /// TCP Socket æœåС噍 - æ ¸å¿ƒç±»
    /// </summary>
    /// <remarks>
    /// æ ¸å¿ƒèŒè´£ï¼š
    /// 1. æŽ¥å—客户端 TCP è¿žæŽ¥
    /// 2. ç®¡ç†å®¢æˆ·ç«¯è¿žæŽ¥çŠ¶æ€
    /// 3. æŽ¥æ”¶å’Œå‘送消息
    /// 4. å¤„理设备注册
    /// 5. æ¶ˆæ¯å¸§è§£æžï¼ˆæ”¯æŒå¤´å°¾æ ‡è¯†ï¼‰
    ///
    /// æœåŠ¡å™¨ä½¿ç”¨ä»¥ä¸‹æ•°æ®ç»“æž„ç®¡ç†å®¢æˆ·ç«¯ï¼š
    /// - _clients: å®¢æˆ·ç«¯ ID åˆ° TcpClient çš„æ˜ å°„
    /// - _clientLocks: å®¢æˆ·ç«¯ ID åˆ°ä¿¡å·é‡çš„æ˜ å°„(保证每个客户端的发送互斥)
    /// - _deviceBindings: è®¾å¤‡ ID åˆ°å®¢æˆ·ç«¯ ID çš„æ˜ å°„
    /// - _clientEncodings: å®¢æˆ·ç«¯ ID åˆ°ç¼–码的映射(支持自动编码检测)
    /// - _clientLastActive: å®¢æˆ·ç«¯ ID åˆ°æœ€åŽæ´»è·ƒæ—¶é—´çš„æ˜ å°„
    /// </remarks>
    public partial class TcpSocketServer : IDisposable
    {
        /// <summary>
        /// æœåŠ¡å™¨é…ç½®é€‰é¡¹
        /// </summary>
        private readonly SocketServerOptions _options;
        /// <summary>
        /// æä¾›ä¸€ä¸ªå¯ç”¨äºŽåŒæ­¥å¯¹åŒ…含实例的访问的对象。
        /// åŒæ­¥æ ¹å¯¹è±¡ï¼Œç”¨äºŽçº¿ç¨‹åŒæ­¥
        /// </summary>
        /// <remarks>在对实例实现线程安全操作时,可将此对象用作锁定目标。此模式通常用于避免死锁并确保一致的同步。</remarks>
        public readonly object _syncRoot = new();
        /// <remarks>
        /// åœ¨å¤šçº¿ç¨‹è®¿é—®å…±äº«æ•°æ®ç»“构时使用此对象进行同步。
        /// é‡‡ç”¨ä¿å®ˆç­–略,确保线程安全。
        /// </remarks>
        public readonly object _syncRoot = new object();
        /// <summary>
        /// TCP ç›‘听器
        /// </summary>
        private TcpListener? _listener;
        /// <summary>
        /// è¡¨ç¤ºç”¨äºŽå‘出进行中操作的取消请求的取消令牌源。
        /// å–消令牌源
        /// </summary>
        /// <remarks>如果当前没有活动的取消机制,此字段可能为null。使用此令牌源取消支持取消的任务或操作。</remarks>
        /// <remarks>
        /// ç”¨äºŽè¯·æ±‚停止服务器的运行。
        /// </remarks>
        public CancellationTokenSource? _cts;
        /// <summary>
        /// æä¾›è¡¨ç¤ºæ´»åŠ¨å®¢æˆ·ç«¯æ“ä½œçš„ä»»åŠ¡åˆ—è¡¨ã€‚
        /// å®¢æˆ·ç«¯ä»»åŠ¡åˆ—è¡¨
        /// </summary>
        /// <remarks>此字段用于内部跟踪异步客户端活动。它是只读的,不应在包含类外部直接修改。</remarks>
        /// <remarks>
        /// è®°å½•所有活跃客户端的处理任务。
        /// </remarks>
        public readonly List<Task> _clientTasks = new();
        /// <summary>
        /// æä¾›ä»Žå®¢æˆ·ç«¯æ ‡è¯†ç¬¦åˆ°å…¶å…³è”çš„TCP客户端连接的映射。
        /// å®¢æˆ·ç«¯è¿žæŽ¥å­—å…¸
        /// </summary>
        /// <remarks>此字典允许通过唯一字符串标识符访问活动的TCP客户端。在多线程场景中,对集合的修改应小心进行以避免并发问题。</remarks>
        /// <remarks>
        /// Key: å®¢æˆ·ç«¯ ID(通常是 IP:Port)
        /// Value: TcpClient è¿žæŽ¥å¯¹è±¡
        /// </remarks>
        public readonly Dictionary<string, TcpClient> _clients = new();
        /// <summary>
        /// æä¾›ä»Žè®¾å¤‡æ ‡è¯†ç¬¦åˆ°å…¶å¯¹åº”绑定值的映射。
        /// è®¾å¤‡ç»‘定字典
        /// </summary>
        /// <remarks>此字段是只读的,用于包含类内部使用。应通过指定的方法或属性对字典进行修改以确保一致性。</remarks>
        /// <remarks>
        /// Key: è®¾å¤‡ ID
        /// Value: å®¢æˆ·ç«¯ ID
        /// ç”¨äºŽé€šè¿‡è®¾å¤‡ ID æ‰¾åˆ°å¯¹åº”的客户端连接。
        /// </remarks>
        public readonly Dictionary<string, string> _deviceBindings = new();
        /// <summary>
        /// æä¾›ä»Žå®¢æˆ·ç«¯æ ‡è¯†ç¬¦åˆ°å…¶å…³è”锁的映射,用于同步对客户端特定资源的访问。
        /// å®¢æˆ·ç«¯é”å­—å…¸
        /// </summary>
        /// <remarks>字典中的每个条目将一个唯一的客户端ID与一个<see cref="SemaphoreSlim"/>实例关联,实现每个客户端的线程安全操作。此集合用于内部协调并发访问,不应直接修改。</remarks>
        /// <remarks>
        /// æ¯ä¸ªå®¢æˆ·ç«¯ä¸€ä¸ª SemaphoreSlim,确保同一客户端的发送操作互斥。
        /// </remarks>
        public readonly Dictionary<string, SemaphoreSlim> _clientLocks = new();
        /// <summary>
        /// æä¾›ä»Žå®¢æˆ·ç«¯æ ‡è¯†ç¬¦åˆ°å…¶å…³è”文本编码的映射。
        /// å®¢æˆ·ç«¯ç¼–码字典
        /// </summary>
        /// <remarks>此字典用于内部跟踪已连接客户端的编码偏好。键表示客户端标识符,值指定用于文本操作的对应<see cref="System.Text.Encoding"/>。</remarks>
        /// <remarks>
        /// è®°å½•每个客户端使用的字符编码。
        /// æ”¯æŒè‡ªåŠ¨æ£€æµ‹ï¼šUTF-8 æˆ– GBK。
        /// </remarks>
        public readonly Dictionary<string, Encoding> _clientEncodings = new();
        /// <summary>
        /// å­˜å‚¨æ¯ä¸ªå®¢æˆ·ç«¯æœ€åŽæ´»åŠ¨çš„æ—¶é—´æˆ³ï¼Œä»¥å®¢æˆ·ç«¯æ ‡è¯†ç¬¦ä¸ºé”®ã€‚
        /// å®¢æˆ·ç«¯æœ€åŽæ´»è·ƒæ—¶é—´å­—å…¸
        /// </summary>
        /// <remarks>此字段用于内部跟踪客户端活动。字典将客户端标识符映射到对应的最后活动时间(UTC)。直接修改此集合可能影响客户端会话管理逻辑。</remarks>
        /// <remarks>
        /// è®°å½•每个客户端最后一次活动的时间。
        /// ç”¨äºŽç©ºé—²è¶…时检测。
        /// </remarks>
        public readonly Dictionary<string, DateTime> _clientLastActive = new();
        /// <summary>
        /// æŒ‡å®šåŒ…含类型中字符数据使用的文本编码。
        /// é»˜è®¤æ–‡æœ¬ç¼–码
        /// </summary>
        /// <remarks>使用此字段确定处理字符数据时如何编码或解码文本。编码影响字节如何被解释为字符,反之亦然。常见的编码包括UTF8、ASCII和Unicode。</remarks>
        public readonly Encoding _textEncoding;
        /// <summary>
        /// è¡¨ç¤ºè‡ªåŠ¨æ£€æµ‹åˆ°çš„GB2312编码(如果可用)。
        /// è‡ªåŠ¨æ£€æµ‹çš„ GBK ç¼–码
        /// </summary>
        /// <remarks>通常从输入数据确定编码时设置此字段。如果检测失败或未执行检测,值可能为null。</remarks>
        public readonly Encoding? _autoDetectedGb2312;
        /// <summary>
        /// æ—¥å¿—文件路径
        /// </summary>
        private readonly string _logFile;
        /// <summary>
        /// å®¢æˆ·ç«¯ç›‘控任务
        /// </summary>
        private Task? _monitorTask;
        /// <summary>
        /// ä½¿ç”¨æŒ‡å®šçš„æœåŠ¡å™¨é€‰é¡¹åˆå§‹åŒ– TcpSocketServer ç±»çš„æ–°å®žä¾‹ã€‚
        /// æœåŠ¡å™¨æ˜¯å¦æ­£åœ¨è¿è¡Œ
        /// </summary>
        /// <remarks>如果启用了 AutoDetectEncoding é€‰é¡¹ï¼ŒæœåŠ¡å™¨å°†é»˜è®¤ä½¿ç”¨ UTF-8 ç¼–码,
        /// å¹¶å°è¯•支持 GBK ç¼–码进行自动检测。如果编码检测失败或提供了无效的编码名称,
        /// å°†å›žé€€ä½¿ç”¨ UTF-8 ç¼–码。日志文件路径由 LogFilePath é€‰é¡¹å†³å®šï¼Œ
        /// å¦‚果未指定,则默认为应用程序基目录下的 'socketserver.log' æ–‡ä»¶ã€‚</remarks>
        /// <param name="options">套接字服务器的配置选项。不能为 null。提供编码设置、日志文件路径和自动检测行为等配置。</param>
        public bool IsRunning { get; private set; }
        /// <summary>
        /// æ¶ˆæ¯æŽ¥æ”¶äº‹ä»¶
        /// </summary>
        /// <remarks>
        /// å½“服务器接收到消息时触发。
        /// å‚数:消息内容、是否 JSON æ ¼å¼ã€TCP å®¢æˆ·ç«¯ã€æœºå™¨äººçŠ¶æ€
        /// </remarks>
        public event Func<string, bool, TcpClient, RobotSocketState, Task<string?>>? MessageReceived;
        /// <summary>
        /// æœºå™¨äººè¿žæŽ¥æ–­å¼€äº‹ä»¶
        /// </summary>
        /// <remarks>
        /// å½“机器人客户端断开连接时触发。
        /// å‚数:客户端 ID
        /// </remarks>
        public event Func<string, Task<string?>>? RobotReceived;
        /// <summary>
        /// æž„造函数
        /// </summary>
        /// <remarks>
        /// ä½¿ç”¨æŒ‡å®šçš„配置选项初始化 TcpSocketServer å®žä¾‹ã€‚
        /// é…ç½®é¡¹åŒ…括:端口、字符编码、自动编码检测、日志文件路径等。
        /// </remarks>
        /// <param name="options">Socket æœåŠ¡å™¨é…ç½®é€‰é¡¹</param>
        public TcpSocketServer(IOptions<SocketServerOptions> options)
        {
            _options = options.Value;
            // é…ç½®å­—符编码
            if (_options.AutoDetectEncoding)
            {
                // è‡ªåŠ¨æ£€æµ‹ç¼–ç æ¨¡å¼ï¼šé»˜è®¤ UTF-8,也支持 GBK
                _textEncoding = Encoding.UTF8;
                try { _autoDetectedGb2312 = Encoding.GetEncoding("GBK"); } catch { _autoDetectedGb2312 = null; }
            }
            else
            {
                // æŒ‡å®šç¼–码模式
                try { _textEncoding = Encoding.GetEncoding(_options.EncodingName ?? "utf-8"); }
                catch { _textEncoding = Encoding.UTF8; }
                _autoDetectedGb2312 = null;
            }
            // é…ç½®æ—¥å¿—文件路径
            _logFile = Path.Combine(AppContext.BaseDirectory ?? ".", _options.LogFilePath ?? "socketserver.log");
            Log($"[{DateTime.Now}] TcpSocketServer starting");
        }
        public bool IsRunning { get; private set; }
        public event Func<string, bool, TcpClient, RobotSocketState, Task<string?>>? MessageReceived;
        public event Func<string, Task<string?>>? RobotReceived;
        /// <summary>
        /// è®°å½•日志
        /// </summary>
        /// <remarks>
        /// å°†æ¶ˆæ¯è¾“出到控制台并写入日志文件。
        /// </remarks>
        /// <param name="message">日志消息</param>
        private void Log(string message)
        {
            Console.WriteLine(message);
            try { File.AppendAllText(_logFile, message + Environment.NewLine); } catch { }
        }
    }
}
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/StackerCraneJob/CommonStackerCraneJob.cs
@@ -1,4 +1,4 @@
using Quartz;
using Quartz;
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
@@ -15,17 +15,77 @@
namespace WIDESEAWCS_Tasks
{
    /// <summary>
    /// å †åž›æœºä»»åŠ¡ä½œä¸šï¼ˆQuartz Job)- æ ¸å¿ƒè°ƒåº¦é€»è¾‘
    /// </summary>
    /// <remarks>
    /// Quartz å®šæ—¶ä»»åŠ¡ï¼Œè´Ÿè´£å †åž›æœºçš„ä»»åŠ¡è°ƒåº¦ã€‚
    /// ä½¿ç”¨ [DisallowConcurrentExecution] ç¦æ­¢å¹¶å‘执行,确保同一堆垛机的任务串行处理。
    ///
    /// æ ¸å¿ƒèŒè´£ï¼š
    /// 1. ä»Žé…ç½®æ–‡ä»¶åŠ è½½å‘½ä»¤ç±»åž‹æ˜ å°„
    /// 2. æ£€æŸ¥å †åž›æœºä»»åŠ¡å®ŒæˆçŠ¶æ€
    /// 3. é€‰æ‹©åˆé€‚的任务(委托给 StackerCraneTaskSelector)
    /// 4. æž„建命令对象(委托给 StackerCraneCommandBuilder)
    /// 5. å‘送命令到堆垛机
    /// 6. å¤„理任务完成事件
    ///
    /// æž¶æž„设计:
    /// - StackerCraneTaskSelector:负责选择合适的任务
    /// - StackerCraneCommandBuilder:负责将任务转换为命令对象
    /// - CommonStackerCraneJob:负责调度流程控制
    /// </remarks>
    [DisallowConcurrentExecution]
    public class CommonStackerCraneJob : IJob
    {
        /// <summary>
        /// ä»»åŠ¡æœåŠ¡
        /// </summary>
        private readonly ITaskService _taskService;
        /// <summary>
        /// ä»»åŠ¡æ‰§è¡Œæ˜Žç»†æœåŠ¡
        /// </summary>
        private readonly ITaskExecuteDetailService _taskExecuteDetailService;
        /// <summary>
        /// ä»»åС仓傍
        /// </summary>
        private readonly ITaskRepository _taskRepository;
        /// <summary>
        /// å †åž›æœºå‘½ä»¤é…ç½®
        /// </summary>
        /// <remarks>
        /// åŒ…含巷道与命令类型的映射关系。
        /// ä»Ž JSON æ–‡ä»¶åŠ è½½ã€‚
        /// </remarks>
        private readonly StackerCraneCommandConfig _config;
        /// <summary>
        /// å †åž›æœºä»»åŠ¡é€‰æ‹©å™¨
        /// </summary>
        /// <remarks>
        /// è´Ÿè´£é€‰æ‹©åˆé€‚的任务进行执行。
        /// </remarks>
        private readonly StackerCraneTaskSelector _taskSelector;
        /// <summary>
        /// å †åž›æœºå‘½ä»¤æž„建器
        /// </summary>
        /// <remarks>
        /// è´Ÿè´£å°†ä»»åŠ¡è½¬æ¢ä¸ºå †åž›æœºå¯æ‰§è¡Œçš„å‘½ä»¤å¯¹è±¡ã€‚
        /// </remarks>
        private readonly StackerCraneCommandBuilder _commandBuilder;
        /// <summary>
        /// æž„造函数
        /// </summary>
        /// <param name="taskService">任务服务</param>
        /// <param name="taskExecuteDetailService">任务执行明细服务</param>
        /// <param name="taskRepository">任务仓储</param>
        /// <param name="routerService">路由服务</param>
        /// <param name="httpClientHelper">HTTP å®¢æˆ·ç«¯å¸®åŠ©ç±»</param>
        public CommonStackerCraneJob(
            ITaskService taskService,
            ITaskExecuteDetailService taskExecuteDetailService,
@@ -37,82 +97,120 @@
            _taskExecuteDetailService = taskExecuteDetailService;
            _taskRepository = taskRepository;
            // åŠ è½½é…ç½®æ–‡ä»¶
            _config = LoadConfig();
            // åˆå§‹åŒ–任务选择器
            _taskSelector = new StackerCraneTaskSelector(taskService, routerService, httpClientHelper);
            // åˆå§‹åŒ–命令构建器
            _commandBuilder = new StackerCraneCommandBuilder(taskService, routerService, _config);
        }
        /// <summary>
        /// åŠ è½½é…ç½®ï¼ˆä¼˜å…ˆçº§ï¼šé…ç½®æ–‡ä»¶ > é»˜è®¤é…ç½®ï¼‰
        /// åŠ è½½é…ç½®æ–‡ä»¶ï¼ˆä¼˜å…ˆçº§ï¼šé…ç½®æ–‡ä»¶ > é»˜è®¤é…ç½®ï¼‰
        /// </summary>
        /// <remarks>
        /// ä»Žåº”用程序目录下的 StackerCraneJob/stackercrane-command-config.json è¯»å–配置。
        /// å¦‚果文件不存在或解析失败,使用默认配置。
        /// </remarks>
        /// <returns>堆垛机命令配置</returns>
        private static StackerCraneCommandConfig LoadConfig()
        {
            try
            {
                // æž„造配置文件路径
                string configPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "StackerCraneJob", "stackercrane-command-config.json");
                if (File.Exists(configPath))
                {
                    // è¯»å–并解析 JSON é…ç½®
                    string json = File.ReadAllText(configPath);
                    return System.Text.Json.JsonSerializer.Deserialize<StackerCraneCommandConfig>(json) ?? new StackerCraneCommandConfig();
                }
            }
            catch (Exception ex)
            {
                // é…ç½®åŠ è½½å¤±è´¥ï¼Œä½¿ç”¨é»˜è®¤é…ç½®
                Console.WriteLine($"配置加载失败: {ex.Message},使用默认配置");
            }
            return new StackerCraneCommandConfig();
        }
        /// <summary>
        /// Quartz Job çš„æ‰§è¡Œå…¥å£
        /// </summary>
        /// <remarks>
        /// æ‰§è¡Œæµç¨‹ï¼š
        /// 1. ä»Ž JobDataMap èŽ·å–å †åž›æœºè®¾å¤‡ä¿¡æ¯
        /// 2. è®¢é˜…任务完成事件(仅第一次执行时)
        /// 3. æ£€æŸ¥å †åž›æœºä»»åŠ¡å®ŒæˆçŠ¶æ€
        /// 4. æ£€æŸ¥æ˜¯å¦å¯ä»¥å‘送任务
        /// 5. é€‰æ‹©ä»»åŠ¡
        /// 6. æž„建命令
        /// 7. å‘送命令
        /// 8. æ›´æ–°ä»»åŠ¡çŠ¶æ€å’Œå †åž›æœºçŠ¶æ€
        /// </remarks>
        /// <param name="context">Quartz ä½œä¸šæ‰§è¡Œä¸Šä¸‹æ–‡</param>
        public Task Execute(IJobExecutionContext context)
        {
            try
            {
                //Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " CommonStackerCraneJob Start");
                // ä»Ž JobDataMap èŽ·å–å †åž›æœºè®¾å¤‡å‚æ•°
                bool flag = context.JobDetail.JobDataMap.TryGetValue("JobParams", out object? value);
                if (!flag || value is not IStackerCrane commonStackerCrane)
                {
                    // å‚数无效,直接返回
                    return Task.CompletedTask;
                }
                // è®¢é˜…一次任务完成事件。
                // ========== è®¢é˜…任务完成事件(全局只订阅一次) ==========
                if (!commonStackerCrane.IsEventSubscribed)
                {
                    // ç»‘定任务完成事件处理方法
                    commonStackerCrane.StackerCraneTaskCompletedEventHandler += CommonStackerCrane_StackerCraneTaskCompletedEventHandler;
                }
                // ========== æ£€æŸ¥å †åž›æœºä»»åŠ¡å®ŒæˆçŠ¶æ€ ==========
                commonStackerCrane.CheckStackerCraneTaskCompleted();
                // ========== æ£€æŸ¥æ˜¯å¦å¯ä»¥å‘送新任务 ==========
                if (!commonStackerCrane.IsCanSendTask(commonStackerCrane.Communicator, commonStackerCrane.DeviceProDTOs, commonStackerCrane.DeviceProtocolDetailDTOs))
                {
                    // å †åž›æœºä¸å¯ç”¨ï¼ˆå¦‚正在执行上一任务),直接返回
                    return Task.CompletedTask;
                }
                // ä»»åŠ¡é€‰æ‹©ä¸‹æ²‰åˆ°ä¸“ç”¨é€‰æ‹©å™¨ã€‚
                // ========== é€‰æ‹©ä»»åŠ¡ ==========
                // ä»»åŠ¡é€‰æ‹©ä¸‹æ²‰åˆ°ä¸“ç”¨é€‰æ‹©å™¨
                Dt_Task? task = _taskSelector.SelectTask(commonStackerCrane);
                if (task == null)
                {
                    // æ²¡æœ‰å¯ç”¨ä»»åŠ¡
                    return Task.CompletedTask;
                }
                // å‘½ä»¤æž„建下沉到专用构建器。
                // ========== æž„建命令 ==========
                // å‘½ä»¤æž„建下沉到专用构建器
                object? stackerCraneTaskCommand = _commandBuilder.ConvertToStackerCraneTaskCommand(task);
                if (stackerCraneTaskCommand == null)
                {
                    // å‘½ä»¤æž„建失败
                    return Task.CompletedTask;
                }
                // ========== å‘送命令 ==========
                bool sendFlag = SendStackerCraneCommand(commonStackerCrane, stackerCraneTaskCommand);
                if (sendFlag)
                {
                    // å‘送成功,更新状态
                    commonStackerCrane.LastTaskType = task.TaskType;
                    _taskService.UpdateTaskStatusToNext(task.TaskNum);
                }
            }
            catch (Exception ex)
            {
                // è®°å½•异常
                Console.WriteLine($"CommonStackerCraneJob Error: {ex.Message}");
            }
@@ -120,25 +218,52 @@
        }
        /// <summary>
        /// ä»»åŠ¡å®Œæˆäº‹ä»¶è®¢é˜…çš„æ–¹æ³•
        /// å †åž›æœºä»»åŠ¡å®Œæˆäº‹ä»¶å¤„ç†
        /// </summary>
        /// <remarks>
        /// å½“堆垛机报告任务完成时调用此方法。
        /// å¤„理:
        /// 1. è®°å½•日志
        /// 2. æ›´æ–°ä»»åŠ¡çŠ¶æ€ä¸ºå®Œæˆ
        /// 3. æ¸…除堆垛机的作业指令
        /// </remarks>
        /// <param name="sender">事件发送者(堆垛机设备)</param>
        /// <param name="e">事件参数,包含完成的任务号</param>
        private void CommonStackerCrane_StackerCraneTaskCompletedEventHandler(object? sender, StackerCraneTaskCompletedEventArgs e)
        {
            CommonStackerCrane? commonStackerCrane = sender as CommonStackerCrane;
            if (commonStackerCrane != null)
            IStackerCrane? stackerCrane = sender as IStackerCrane;
            if (stackerCrane != null)
            {
                // è®°å½•日志
                Console.Out.WriteLine("TaskCompleted" + e.TaskNum);
                // æ›´æ–°ä»»åŠ¡çŠ¶æ€ä¸ºå®Œæˆ
                _taskService.StackCraneTaskCompleted(e.TaskNum);
                commonStackerCrane.SetValue(StackerCraneDBName.WorkAction, 2);
                // æ¸…除堆垛机的作业指令(设置为 2,表示空闲)
                stackerCrane.SetValue(StackerCraneDBName.WorkAction, 2);
            }
        }
        /// <summary>
        /// å‘送堆垛机命令
        /// </summary>
        /// <remarks>
        /// æ ¹æ®å‘½ä»¤ç±»åž‹è°ƒç”¨ç›¸åº”的发送方法。
        /// æ”¯æŒæ ‡å‡†å‘½ä»¤å’Œæˆåž‹å‘½ä»¤ä¸¤ç§æ ¼å¼ã€‚
        /// </remarks>
        /// <param name="commonStackerCrane">堆垛机设备对象</param>
        /// <param name="command">命令对象</param>
        /// <returns>发送是否成功</returns>
        private static bool SendStackerCraneCommand(IStackerCrane commonStackerCrane, object command)
        {
            return command switch
            {
                // æˆåž‹å‘½ä»¤ï¼ˆå¦‚ HC å··é“)
                FormationStackerCraneTaskCommand formationCommand => commonStackerCrane.SendCommand(formationCommand),
                // æ ‡å‡†å‘½ä»¤ï¼ˆå¦‚ GW、CW å··é“)
                StackerCraneTaskCommand standardCommand => commonStackerCrane.SendCommand(standardCommand),
                // æœªçŸ¥å‘½ä»¤ç±»åž‹
                _ => false
            };
        }
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/StackerCraneJob/StackerCraneCommandBuilder.cs
@@ -1,4 +1,4 @@
using System;
using System;
using System.Diagnostics.CodeAnalysis;
using WIDESEAWCS_Common.TaskEnum;
using WIDESEAWCS_ITaskInfoService;
@@ -10,14 +10,40 @@
namespace WIDESEAWCS_Tasks
{
    /// <summary>
    /// å †åž›æœºå‘½ä»¤æž„建器:封装任务到命令对象的转换与地址解析。
    /// å †åž›æœºå‘½ä»¤æž„建器 - å°è£…任务到命令对象的转换与地址解析
    /// </summary>
    /// <remarks>
    /// æ ¸å¿ƒèŒè´£ï¼š
    /// 1. æ ¹æ®å··é“类型选择命令格式(标准命令/成型命令)
    /// 2. æž„建入库、出库、移库任务的命令对象
    /// 3. è§£æžä»»åŠ¡åœ°å€ï¼ˆè¡Œ-列-层格式)到命令坐标
    /// 4. æŸ¥è¯¢è·¯ç”±ä¿¡æ¯ï¼Œç¡®å®šå †åž›æœºçš„取货/放货站台
    ///
    /// åœ°å€æ ¼å¼çº¦å®šï¼šåœ°å€å­—符串格式为 "行-列-层",例如 "1-2-3" è¡¨ç¤ºç¬¬1行、第2列、第3层。
    /// </remarks>
    public class StackerCraneCommandBuilder
    {
        /// <summary>
        /// ä»»åŠ¡æœåŠ¡
        /// </summary>
        private readonly ITaskService _taskService;
        /// <summary>
        /// è·¯ç”±æœåŠ¡
        /// </summary>
        private readonly IRouterService _routerService;
        /// <summary>
        /// å †åž›æœºå‘½ä»¤é…ç½®
        /// </summary>
        private readonly StackerCraneCommandConfig _config;
        /// <summary>
        /// æž„造函数
        /// </summary>
        /// <param name="taskService">任务服务</param>
        /// <param name="routerService">路由服务</param>
        /// <param name="config">命令配置</param>
        public StackerCraneCommandBuilder(
            ITaskService taskService,
            IRouterService routerService,
@@ -28,16 +54,36 @@
            _config = config;
        }
        /// <summary>
        /// å°†ä»»åŠ¡è½¬æ¢ä¸ºå †åž›æœºå‘½ä»¤
        /// </summary>
        /// <remarks>
        /// æ ¹æ®å··é“类型选择不同的命令构建策略。
        /// </remarks>
        /// <param name="task">任务对象</param>
        /// <returns>堆垛机命令对象,转换失败返回 null</returns>
        public object? ConvertToStackerCraneTaskCommand([NotNull] Dt_Task task)
        {
            // æ ¹æ®å··é“获取命令类型
            string commandType = GetCommandType(task.Roadway);
            // æ ¹æ®å‘½ä»¤ç±»åž‹è°ƒç”¨ç›¸åº”的构建方法
            return commandType switch
            {
                "Formation" => BuildCommand(task, CreateFormationCommand(task)),
                _ => BuildCommand(task, CreateStandardCommand(task))
                "Formation" => BuildCommand(task, CreateFormationCommand(task)),  // æˆåž‹å‘½ä»¤
                _ => BuildCommand(task, CreateStandardCommand(task))              // æ ‡å‡†å‘½ä»¤
            };
        }
        /// <summary>
        /// æ ¹æ®å··é“获取命令类型
        /// </summary>
        /// <remarks>
        /// éåŽ†é…ç½®ä¸­çš„æ˜ å°„å…³ç³»ï¼Œæ‰¾åˆ°åŒ¹é…çš„å‘½ä»¤ç±»åž‹ã€‚
        /// å¦‚果不匹配任何映射,返回默认命令类型。
        /// </remarks>
        /// <param name="roadway">巷道编码</param>
        /// <returns>命令类型(Standard æˆ– Formation)</returns>
        private string GetCommandType(string roadway)
        {
            foreach (var mapping in _config.RoadwayCommandMapping)
@@ -51,45 +97,88 @@
            return _config.DefaultCommandType;
        }
        /// <summary>
        /// åˆ›å»ºæ ‡å‡†å‘½ä»¤
        /// </summary>
        /// <remarks>
        /// ç”¨äºŽæ ‡å‡†å †åž›æœºï¼ˆGW、CW å¼€å¤´å··é“)。
        /// </remarks>
        /// <param name="task">任务对象</param>
        /// <returns>标准命令对象</returns>
        private static StackerCraneTaskCommand CreateStandardCommand(Dt_Task task)
        {
            return new StackerCraneTaskCommand
            {
                TaskNum = task.TaskNum,
                WorkType = 1,
                WorkAction = 1
                TaskNum = task.TaskNum,   // ä»»åŠ¡å·
                WorkType = 1,             // ä½œä¸šç±»åž‹
                WorkAction = 1            // ä½œä¸šæŒ‡ä»¤ï¼šå¼€å§‹æ‰§è¡Œ
            };
        }
        /// <summary>
        /// åˆ›å»ºæˆåž‹å‘½ä»¤
        /// </summary>
        /// <remarks>
        /// ç”¨äºŽæˆåž‹å †åž›æœºï¼ˆHC å¼€å¤´å··é“)。
        /// åŒ…含条码字段,用于电池追溯。
        /// </remarks>
        /// <param name="task">任务对象</param>
        /// <returns>成型命令对象</returns>
        private static FormationStackerCraneTaskCommand CreateFormationCommand(Dt_Task task)
        {
            return new FormationStackerCraneTaskCommand
            {
                Barcode = task.PalletCode,
                TaskNum = task.TaskNum,
                WorkType = 1,
                WorkAction = 1,
                FireAlarm = 0,
                HeartBeat = 0,
                FieldName = string.Empty
                Barcode = task.PalletCode,   // æ‰˜ç›˜æ¡ç 
                TaskNum = task.TaskNum,      // ä»»åŠ¡å·
                WorkType = 1,               // ä½œä¸šç±»åž‹
                WorkAction = 1,             // ä½œä¸šæŒ‡ä»¤ï¼šå¼€å§‹æ‰§è¡Œ
                FireAlarm = 0,              // ç«è­¦ï¼šæ­£å¸¸
                HeartBeat = 0,              // å¿ƒè·³
                FieldName = string.Empty     // ä¿ç•™å­—段
            };
        }
        /// <summary>
        /// æž„建命令(通用)
        /// </summary>
        /// <remarks>
        /// æ ¹æ®ä»»åŠ¡ç±»åž‹ï¼ˆå…¥åº“/出库/移库)调用相应的构建方法。
        /// </remarks>
        /// <typeparam name="T">命令类型</typeparam>
        /// <param name="task">任务对象</param>
        /// <param name="command">初始命令对象</param>
        /// <returns>填充好的命令对象</returns>
        private T? BuildCommand<T>(Dt_Task task, T command) where T : class
        {
            // èŽ·å–ä»»åŠ¡ç±»åž‹åˆ†ç»„
            TaskTypeGroup taskTypeGroup = task.TaskType.GetTaskTypeGroup();
            // æ ¹æ®ä»»åŠ¡ç±»åž‹åˆ†å‘æž„å»º
            return taskTypeGroup switch
            {
                TaskTypeGroup.InboundGroup => BuildInboundCommand(task, command),
                TaskTypeGroup.OutbondGroup => BuildOutboundCommand(task, command),
                TaskTypeGroup.RelocationGroup => BuildRelocationCommand(task, command),
                _ => command
                TaskTypeGroup.InboundGroup => BuildInboundCommand(task, command),    // å…¥åº“
                TaskTypeGroup.OutbondGroup => BuildOutboundCommand(task, command),  // å‡ºåº“
                TaskTypeGroup.RelocationGroup => BuildRelocationCommand(task, command),  // ç§»åº“
                _ => command  // æœªçŸ¥ç±»åž‹ï¼Œè¿”回原命令
            };
        }
        /// <summary>
        /// æž„建入库命令
        /// </summary>
        /// <remarks>
        /// å…¥åº“任务需要:
        /// 1. æŸ¥è¯¢å †åž›æœºå–货站台(根据当前地址和任务类型)
        /// 2. è®¾ç½®èµ·å§‹åæ ‡ï¼ˆæ¥è‡ªç«™å°ï¼‰
        /// 3. è§£æžç›®æ ‡åœ°å€ï¼Œè®¾ç½®ç»ˆç‚¹åæ ‡
        /// </remarks>
        /// <typeparam name="T">命令类型</typeparam>
        /// <param name="task">任务对象</param>
        /// <param name="command">命令对象</param>
        /// <returns>填充好的命令对象</returns>
        private T? BuildInboundCommand<T>(Dt_Task task, T command) where T : class
        {
            // ç¡®å®šä»»åŠ¡ç±»åž‹ï¼ˆç©ºæ‰˜ç›˜ç”¨ç‰¹æ®Šç±»åž‹ 100)
            int taskType = 0;
            if (task.TaskType == (int)TaskOutboundTypeEnum.OutEmpty)
            {
@@ -97,23 +186,29 @@
            }
            else
                taskType = task.TaskType;
            // æŸ¥è¯¢å †åž›æœºå–货站台路由
            Dt_Router? router = _routerService.QueryNextRoute(task.CurrentAddress, task.Roadway, taskType);
            if (router == null)
            {
                // æœªæ‰¾åˆ°ç«™å°ï¼Œæ›´æ–°å¼‚常信息
                _taskService.UpdateTaskExceptionMessage(task.TaskNum, $"未找到站台【{task.CurrentAddress}】信息,无法获取对应的堆垛机取货站台信息");
                return null;
            }
            // è®¾ç½®èµ·å§‹åæ ‡ï¼ˆæ¥è‡ªè·¯ç”±é…ç½®ï¼‰
            SetCommandProperty(command, "StartRow", Convert.ToInt16(router.SrmRow));
            SetCommandProperty(command, "StartColumn", Convert.ToInt16(router.SrmColumn));
            SetCommandProperty(command, "StartLayer", Convert.ToInt16(router.SrmLayer));
            // è§£æžç›®æ ‡åœ°å€ï¼ˆåº“位地址)
            if (!TryParseAddress(task.NextAddress, out short endRow, out short endColumn, out short endLayer))
            {
                _taskService.UpdateTaskExceptionMessage(task.TaskNum, $"入库任务终点错误,终点:【{task.NextAddress}】");
                return null;
            }
            // è®¾ç½®ç»ˆç‚¹åæ ‡
            SetCommandProperty(command, "EndRow", endRow);
            SetCommandProperty(command, "EndColumn", endColumn);
            SetCommandProperty(command, "EndLayer", endLayer);
@@ -121,8 +216,22 @@
            return command;
        }
        /// <summary>
        /// æž„建出库命令
        /// </summary>
        /// <remarks>
        /// å‡ºåº“任务需要:
        /// 1. æŸ¥è¯¢å †åž›æœºæ”¾è´§ç«™å°ï¼ˆæ ¹æ®ç›®æ ‡åœ°å€å’Œä»»åŠ¡ç±»åž‹ï¼‰
        /// 2. è®¾ç½®ç»ˆç‚¹åæ ‡ï¼ˆæ¥è‡ªç«™å°ï¼‰
        /// 3. è§£æžèµ·å§‹åœ°å€ï¼Œè®¾ç½®èµ·ç‚¹åæ ‡
        /// </remarks>
        /// <typeparam name="T">命令类型</typeparam>
        /// <param name="task">任务对象</param>
        /// <param name="command">命令对象</param>
        /// <returns>填充好的命令对象</returns>
        private T? BuildOutboundCommand<T>(Dt_Task task, T command) where T : class
        {
            // ç¡®å®šä»»åŠ¡ç±»åž‹
            int taskType = 0;
            if (task.TaskType == (int)TaskOutboundTypeEnum.OutEmpty)
            {
@@ -131,6 +240,7 @@
            else
                taskType = task.TaskType;
            // æŸ¥è¯¢å †åž›æœºæ”¾è´§ç«™å°è·¯ç”±
            Dt_Router? router = _routerService.QueryNextRoute(task.Roadway, task.TargetAddress, taskType);
            if (router == null)
            {
@@ -138,16 +248,19 @@
                return null;
            }
            // è®¾ç½®ç»ˆç‚¹åæ ‡ï¼ˆæ¥è‡ªè·¯ç”±é…ç½®ï¼‰
            SetCommandProperty(command, "EndRow", Convert.ToInt16(router.SrmRow));
            SetCommandProperty(command, "EndColumn", Convert.ToInt16(router.SrmColumn));
            SetCommandProperty(command, "EndLayer", Convert.ToInt16(router.SrmLayer));
            // è§£æžèµ·å§‹åœ°å€ï¼ˆåº“位地址)
            if (!TryParseAddress(task.CurrentAddress, out short startRow, out short startColumn, out short startLayer))
            {
                _taskService.UpdateTaskExceptionMessage(task.TaskNum, $"出库任务起点错误,起点:【{task.CurrentAddress}】");
                return null;
            }
            // è®¾ç½®èµ·ç‚¹åæ ‡
            SetCommandProperty(command, "StartRow", startRow);
            SetCommandProperty(command, "StartColumn", startColumn);
            SetCommandProperty(command, "StartLayer", startLayer);
@@ -155,24 +268,41 @@
            return command;
        }
        /// <summary>
        /// æž„建移库命令
        /// </summary>
        /// <remarks>
        /// ç§»åº“任务需要:
        /// 1. è§£æžç›®æ ‡åœ°å€ï¼ˆæ–°çš„库位)
        /// 2. è§£æžèµ·å§‹åœ°å€ï¼ˆåŽŸæ¥çš„åº“ä½ï¼‰
        /// 3. è®¾ç½®èµ·æ­¢åæ ‡
        /// </remarks>
        /// <typeparam name="T">命令类型</typeparam>
        /// <param name="task">任务对象</param>
        /// <param name="command">命令对象</param>
        /// <returns>填充好的命令对象</returns>
        private T? BuildRelocationCommand<T>(Dt_Task task, T command) where T : class
        {
            // è§£æžç›®æ ‡åœ°å€
            if (!TryParseAddress(task.NextAddress, out short endRow, out short endColumn, out short endLayer))
            {
                _taskService.UpdateTaskExceptionMessage(task.TaskNum, $"移库任务终点错误,终点:【{task.NextAddress}】");
                return null;
            }
            // è®¾ç½®ç»ˆç‚¹åæ ‡
            SetCommandProperty(command, "EndRow", endRow);
            SetCommandProperty(command, "EndColumn", endColumn);
            SetCommandProperty(command, "EndLayer", endLayer);
            // è§£æžèµ·å§‹åœ°å€
            if (!TryParseAddress(task.CurrentAddress, out short startRow, out short startColumn, out short startLayer))
            {
                _taskService.UpdateTaskExceptionMessage(task.TaskNum, $"移库任务起点错误,起点:【{task.CurrentAddress}】");
                return null;
            }
            // è®¾ç½®èµ·ç‚¹åæ ‡
            SetCommandProperty(command, "StartRow", startRow);
            SetCommandProperty(command, "StartColumn", startColumn);
            SetCommandProperty(command, "StartLayer", startLayer);
@@ -180,22 +310,45 @@
            return command;
        }
        /// <summary>
        /// è®¾ç½®å‘½ä»¤å±žæ€§å€¼ï¼ˆé€šè¿‡åå°„)
        /// </summary>
        /// <remarks>
        /// ä½¿ç”¨åå°„动态设置命令对象的属性值。
        /// </remarks>
        /// <typeparam name="T">命令类型</typeparam>
        /// <param name="command">命令对象</param>
        /// <param name="propertyName">属性名称</param>
        /// <param name="value">属性值</param>
        private static void SetCommandProperty<T>(T command, string propertyName, object value) where T : class
        {
            var property = typeof(T).GetProperty(propertyName);
            property?.SetValue(command, value);
        }
        /// <summary>
        /// è§£æžåœ°å€å­—符串
        /// </summary>
        /// <remarks>
        /// åœ°å€æ ¼å¼ï¼šè¡Œ-列-层,例如 "1-2-3" è¡¨ç¤ºç¬¬1行、第2列、第3层。
        /// </remarks>
        /// <param name="address">地址字符串</param>
        /// <param name="row">解析出的行坐标</param>
        /// <param name="column">解析出的列坐标</param>
        /// <param name="layer">解析出的层坐标</param>
        /// <returns>解析成功返回 true</returns>
        private static bool TryParseAddress(string address, out short row, out short column, out short layer)
        {
            row = column = layer = 0;
            string[] parts = address.Split("-");
            // æŒ‰ "-" åˆ†éš”地址
            string[] parts = address.Split('-');
            if (parts.Length != 3)
            {
                return false;
            }
            // è§£æžå„部分为 short ç±»åž‹
            return short.TryParse(parts[0], out row)
                && short.TryParse(parts[1], out column)
                && short.TryParse(parts[2], out layer);
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/StackerCraneJob/StackerCraneCommandConfig.cs
@@ -5,21 +5,39 @@
    /// <summary>
    /// å †åž›æœºå‘½ä»¤é…ç½®
    /// </summary>
    /// <remarks>
    /// å®šä¹‰å †åž›æœºå‘½ä»¤ç±»åž‹ä¸Žå··é“的映射关系。
    /// æ ¹æ®å··é“(Roadway)的不同,堆垛机可能使用不同的命令格式。
    /// é…ç½®å¯ä»¥é€šè¿‡ JSON æ–‡ä»¶åŠ¨æ€åŠ è½½ã€‚
    /// </remarks>
    public class StackerCraneCommandConfig
    {
        /// <summary>
        /// Roadway å…³é”®å­—到命令类型的映射
        /// å··é“关键字到命令类型的映射字典
        /// </summary>
        /// <remarks>
        /// Key: å··é“编码的关键字(如 HC、GW、CW)
        /// Value: å‘½ä»¤ç±»åž‹ï¼ˆå¦‚ Formation、Standard)
        ///
        /// æ˜ å°„规则:
        /// - HC å¼€å¤´ -> Formation(成型堆垛机命令)
        /// - GW å¼€å¤´ -> Standard(标准堆垛机命令)
        /// - CW å¼€å¤´ -> Standard(标准堆垛机命令)
        /// </remarks>
        public Dictionary<string, string> RoadwayCommandMapping { get; set; } = new()
        {
            { "HC", "Formation" },
            { "GW", "Standard" },
            { "CW", "Standard" }
            { "HC", "Formation" },  // æˆåž‹å †åž›æœº
            { "GW", "Standard" },  // æ ‡å‡†å †åž›æœº
            { "CW", "Standard" }   // æ ‡å‡†å †åž›æœº
        };
        /// <summary>
        /// é»˜è®¤å‘½ä»¤ç±»åž‹
        /// </summary>
        /// <remarks>
        /// å½“巷道编码不匹配任何映射规则时使用的默认命令类型。
        /// é»˜è®¤ä¸º Standard(标准命令格式)。
        /// </remarks>
        public string DefaultCommandType { get; set; } = "Standard";
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/StackerCraneJob/StackerCraneDBName.cs
@@ -1,85 +1,109 @@
#region << ç‰ˆ æœ¬ æ³¨ é‡Š >>
/*----------------------------------------------------------------
 * å‘½åç©ºé—´ï¼šWIDESEAWCS_Tasks.StackerCraneJob
 * åˆ›å»ºè€…:胡童庆
 * åˆ›å»ºæ—¶é—´ï¼š2024/8/2 16:13:36
 * ç‰ˆæœ¬ï¼šV1.0.0
 * æè¿°ï¼š
 *
 * ----------------------------------------------------------------
 * ä¿®æ”¹äººï¼š
 * ä¿®æ”¹æ—¶é—´ï¼š
 * ç‰ˆæœ¬ï¼šV1.0.1
 * ä¿®æ”¹è¯´æ˜Žï¼š
 *
 *----------------------------------------------------------------*/
#endregion << ç‰ˆ æœ¬ æ³¨ é‡Š >>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WIDESEAWCS_Tasks.StackerCraneJob
{
    /// <summary>
    /// å †åž›æœº PLC å¯„存器名称枚举
    /// </summary>
    /// <remarks>
    /// å®šä¹‰å †åž›æœºä¸Ž WCS é€šä¿¡æ—¶ä½¿ç”¨çš„ PLC å¯„存器地址名称。
    /// åŒ…含任务号、作业类型、起止位置等信息。
    /// </remarks>
    public enum StackerCraneDBName
    {
        /// <summary>
        /// ä»»åŠ¡å·
        /// </summary>
        /// <remarks>
        /// WCS åˆ†é…çš„任务唯一标识号。
        /// ç”¨äºŽ WCS å’Œå †åž›æœºä¹‹é—´å»ºç«‹ä»»åŠ¡å¯¹åº”çš„å…³è”ã€‚
        /// </remarks>
        TaskNum,
        /// <summary>
        /// ä½œä¸šç±»åž‹
        /// </summary>
        /// <remarks>
        /// æ ‡è¯†ä»»åŠ¡çš„ç±»åž‹ã€‚
        /// </remarks>
        WorkType,
        /// <summary>
        /// æ‰˜ç›˜ç±»åž‹
        /// </summary>
        /// <remarks>
        /// æ ‡è¯†æ‰˜ç›˜çš„规格类型。
        /// </remarks>
        TrayType,
        /// <summary>
        /// èµ·å§‹è¡Œ
        /// </summary>
        /// <remarks>
        /// ä»»åŠ¡çš„èµ·å§‹ä½ç½®-行坐标。
        /// ç”¨äºŽç¡®å®šåº“位在货架中的行位置。
        /// </remarks>
        StartRow,
        /// <summary>
        /// èµ·å§‹åˆ—
        /// </summary>
        /// <remarks>
        /// ä»»åŠ¡çš„èµ·å§‹ä½ç½®-列坐标。
        /// ç”¨äºŽç¡®å®šåº“位在货架中的列位置。
        /// </remarks>
        StartColumn,
        /// <summary>
        /// èµ·å§‹å±‚
        /// </summary>
        /// <remarks>
        /// ä»»åŠ¡çš„èµ·å§‹ä½ç½®-层坐标。
        /// ç”¨äºŽç¡®å®šåº“位在货架中的层位置。
        /// </remarks>
        StartLayer,
        /// <summary>
        /// ç›®æ ‡è¡Œ
        /// </summary>
        /// <remarks>
        /// ä»»åŠ¡çš„ç›®æ ‡ä½ç½®-行坐标。
        /// å…¥åº“时表示货物存放的行位置,出库时表示货物来源的行位置。
        /// </remarks>
        EndRow,
        /// <summary>
        /// ç›®æ ‡åˆ—
        /// </summary>
        /// <remarks>
        /// ä»»åŠ¡çš„ç›®æ ‡ä½ç½®-列坐标。
        /// å…¥åº“时表示货物存放的列位置,出库时表示货物来源的列位置。
        /// </remarks>
        EndColumn,
        /// <summary>
        /// ç›®æ ‡å±‚
        /// </summary>
        /// <remarks>
        /// ä»»åŠ¡çš„ç›®æ ‡ä½ç½®-层坐标。
        /// å…¥åº“时表示货物存放的层位置,出库时表示货物来源的层位置。
        /// </remarks>
        EndLayer,
        /// <summary>
        /// ä½œä¸šæŒ‡ä»¤
        /// </summary>
        /// <remarks>
        /// æŽ§åˆ¶å †åž›æœºçš„动作:
        /// - 1: å¼€å§‹æ‰§è¡Œä»»åŠ¡
        /// - 2: ä»»åŠ¡å®Œæˆ/停止
        /// </remarks>
        WorkAction,
        ///// <summary>
        ///// æ‰˜ç›˜å·
        ///// </summary>
        //Barcode,
        /// <summary>
        /// å½“前任务号
        /// </summary>
        /// <remarks>
        /// å †åž›æœºå½“前正在执行的任务号。
        /// </remarks>
        CurrentTaskNum
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/StackerCraneJob/StackerCraneTaskCommand.cs
@@ -1,83 +1,108 @@
#region << ç‰ˆ æœ¬ æ³¨ é‡Š >>
/*----------------------------------------------------------------
 * å‘½åç©ºé—´ï¼šWIDESEAWCS_Tasks.StackerCraneJob
 * åˆ›å»ºè€…:胡童庆
 * åˆ›å»ºæ—¶é—´ï¼š2024/8/2 16:13:36
 * ç‰ˆæœ¬ï¼šV1.0.0
 * æè¿°ï¼š
 *
 * ----------------------------------------------------------------
 * ä¿®æ”¹äººï¼š
 * ä¿®æ”¹æ—¶é—´ï¼š
 * ç‰ˆæœ¬ï¼šV1.0.1
 * ä¿®æ”¹è¯´æ˜Žï¼š
 *
 *----------------------------------------------------------------*/
#endregion << ç‰ˆ æœ¬ æ³¨ é‡Š >>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WIDESEAWCS_Model.Models;
using WIDESEAWCS_QuartzJob.DeviceBase;
namespace WIDESEAWCS_Tasks.StackerCraneJob
{
    /// <summary>
    /// å †åž›æœºä»»åŠ¡å‘½ä»¤æ•°æ®ç±»
    /// </summary>
    /// <remarks>
    /// ç»§æ‰¿è‡ª DeviceCommand,用于与堆垛机进行 PLC é€šä¿¡ã€‚
    /// åŒ…含任务号、作业类型、起止行列层坐标等字段。
    /// æ ‡å‡†å‘½ä»¤æ ¼å¼ï¼Œç”¨äºŽå¤§å¤šæ•°å··é“的堆垛机。
    /// </remarks>
    public class StackerCraneTaskCommand : DeviceCommand
    {
        #region <Public Menber>
        /// <summary>
        /// ä½œä¸šå‘½ä»¤
        /// </summary>
        /// <remarks>
        /// æŽ§åˆ¶å †åž›æœºçš„动作:
        /// - 1: å¼€å§‹æ‰§è¡Œä»»åŠ¡
        /// - 2: ä»»åŠ¡å®Œæˆ/停止
        /// </remarks>
        public short WorkAction { get; set; }
        /// <summary>
        /// ä»»åŠ¡å·
        /// </summary>
        /// <remarks>
        /// WCS åˆ†é…çš„任务唯一标识号。
        /// </remarks>
        public int TaskNum { get; set; }
        /// <summary>
        /// ä½œä¸šç±»åž‹
        /// </summary>
        /// <remarks>
        /// æ ‡è¯†ä»»åŠ¡çš„ç±»åž‹ã€‚
        /// </remarks>
        public short WorkType { get; set; }
        /// <summary>
        /// æ— æ•ˆå­—段
        /// æ— æ•ˆå­—段(保留字段)
        /// </summary>
        /// <remarks>
        /// åŽ†å²é—ç•™å­—æ®µï¼Œç›®å‰ä¸å†ä½¿ç”¨ã€‚
        /// </remarks>
        [DataLength(6)]
        public string FieldName { get; set; } = "";
        /// <summary>
        /// èµ·å§‹è¡Œ
        /// </summary>
        /// <remarks>
        /// ä»»åŠ¡çš„èµ·å§‹ä½ç½®-行坐标(货架行号)。
        /// å…¥åº“时表示货物来自哪个位置。
        /// </remarks>
        public short StartRow { get; set; }
        /// <summary>
        /// èµ·å§‹åˆ—
        /// </summary>
        /// <remarks>
        /// ä»»åŠ¡çš„èµ·å§‹ä½ç½®-列坐标(货架列号)。
        /// å…¥åº“时表示货物来自哪个位置。
        /// </remarks>
        public short StartColumn { get; set; }
        /// <summary>
        /// èµ·å§‹å±‚
        /// </summary>
        /// <remarks>
        /// ä»»åŠ¡çš„èµ·å§‹ä½ç½®-层坐标(货架层号)。
        /// å…¥åº“时表示货物来自哪个位置。
        /// </remarks>
        public short StartLayer { get; set; }
        /// <summary>
        /// ç›®æ ‡è¡Œ
        /// </summary>
        /// <remarks>
        /// ä»»åŠ¡çš„ç›®æ ‡ä½ç½®-行坐标(货架行号)。
        /// å…¥åº“时表示货物存放到哪个位置。
        /// å‡ºåº“时表示货物从哪个位置取出。
        /// </remarks>
        public short EndRow { get; set; }
        /// <summary>
        /// ç›®æ ‡åˆ—
        /// </summary>
        /// <remarks>
        /// ä»»åŠ¡çš„ç›®æ ‡ä½ç½®-列坐标(货架列号)。
        /// å…¥åº“时表示货物存放到哪个位置。
        /// å‡ºåº“时表示货物从哪个位置取出。
        /// </remarks>
        public short EndColumn { get; set; }
        /// <summary>
        /// ç›®æ ‡å±‚
        /// </summary>
        /// <remarks>
        /// ä»»åŠ¡çš„ç›®æ ‡ä½ç½®-层坐标(货架层号)。
        /// å…¥åº“时表示货物存放到哪个位置。
        /// å‡ºåº“时表示货物从哪个位置取出。
        /// </remarks>
        public short EndLayer { get; set; }
        #endregion <Public Menber>
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/StackerCraneJob/StackerCraneTaskSelector.cs
@@ -1,4 +1,4 @@
using Newtonsoft.Json;
using Newtonsoft.Json;
using System.Diagnostics.CodeAnalysis;
using WIDESEA_Core;
using WIDESEAWCS_Common.HttpEnum;
@@ -13,19 +13,57 @@
namespace WIDESEAWCS_Tasks
{
    /// <summary>
    /// å †åž›æœºä»»åŠ¡é€‰æ‹©å™¨ï¼šå°è£…ä»»åŠ¡æŒ‘é€‰ä¸Žç«™å°å¯ç”¨æ€§åˆ¤æ–­ã€‚
    /// å †åž›æœºä»»åŠ¡é€‰æ‹©å™¨ - å°è£…任务挑选与站台可用性判断
    /// </summary>
    /// <remarks>
    /// æ ¸å¿ƒèŒè´£ï¼š
    /// 1. æ ¹æ®å †åž›æœºä¸Šä¸€ä»»åŠ¡ç±»åž‹é€‰æ‹©ä¸‹ä¸€ä¸ªåˆé€‚çš„ä»»åŠ¡
    /// 2. å¯¹å‡ºåº“任务进行移库检查(WMS åˆ¤æ–­ï¼‰
    /// 3. åˆ¤æ–­å‡ºåº“站台是否可用(是否被占用)
    /// 4. å°è¯•选择备选出库站台
    ///
    /// ä»»åŠ¡é€‰æ‹©ç­–ç•¥ï¼š
    /// - å¦‚果上一任务是出库,优先选择入库任务
    /// - å¦‚果上一任务是入库,优先选择出库任务
    /// - å¯¹äºŽå‡ºåº“任务,先检查是否需要移库
    /// </remarks>
    public class StackerCraneTaskSelector
    {
        /// <summary>
        /// ä»»åŠ¡æœåŠ¡
        /// </summary>
        private readonly ITaskService _taskService;
        /// <summary>
        /// è·¯ç”±æœåŠ¡
        /// </summary>
        private readonly IRouterService _routerService;
        /// <summary>
        /// ç§»åº“检查委托函数
        /// </summary>
        /// <remarks>
        /// ç”¨äºŽè°ƒç”¨ WMS åˆ¤æ–­å‡ºåº“任务是否需要先执行移库。
        /// </remarks>
        private readonly Func<int, Dt_Task?> _transferCheck;
        /// <summary>
        /// æž„造函数(使用 HTTP å®¢æˆ·ç«¯å¸®åŠ©ç±»ï¼‰
        /// </summary>
        /// <param name="taskService">任务服务</param>
        /// <param name="routerService">路由服务</param>
        /// <param name="httpClientHelper">HTTP å®¢æˆ·ç«¯å¸®åŠ©ç±»</param>
        public StackerCraneTaskSelector(ITaskService taskService, IRouterService routerService, HttpClientHelper httpClientHelper)
            : this(taskService, routerService, taskNum => QueryTransferTask(httpClientHelper, taskNum))
        {
        }
        /// <summary>
        /// æž„造函数(使用委托函数)
        /// </summary>
        /// <param name="taskService">任务服务</param>
        /// <param name="routerService">路由服务</param>
        /// <param name="transferCheck">移库检查函数</param>
        public StackerCraneTaskSelector(ITaskService taskService, IRouterService routerService, Func<int, Dt_Task?> transferCheck)
        {
            _taskService = taskService;
@@ -33,44 +71,69 @@
            _transferCheck = transferCheck;
        }
        /// <summary>
        /// é€‰æ‹©åˆé€‚的任务
        /// </summary>
        /// <remarks>
        /// æ ¹æ®å †åž›æœºçš„上一任务类型和当前状态,选择下一个应该执行的任务。
        ///
        /// é€‰æ‹©ç­–略:
        /// 1. å¦‚果没有上一任务类型,查询普通任务
        /// 2. å¦‚果上一任务是出库,优先查入库任务,再查出库任务
        /// 3. å¦‚果上一任务是入库,优先查出库任务
        /// 4. å¯¹äºŽå‡ºåº“任务,需要判断站台是否可用
        /// </remarks>
        /// <param name="commonStackerCrane">堆垛机设备对象</param>
        /// <returns>选中的任务,如果没有可用任务返回 null</returns>
        public Dt_Task? SelectTask(IStackerCrane commonStackerCrane)
        {
            Dt_Task? candidateTask;
            // æ ¹æ®ä¸Šä¸€ä»»åŠ¡ç±»åž‹å†³å®šæŸ¥è¯¢ç­–ç•¥
            if (commonStackerCrane.LastTaskType == null)
            {
                // æ²¡æœ‰ä¸Šä¸€ä»»åŠ¡ç±»åž‹ï¼ŒæŸ¥è¯¢æ™®é€šä»»åŠ¡
                candidateTask = _taskService.QueryStackerCraneTask(commonStackerCrane.DeviceCode);
            }
            else if (commonStackerCrane.LastTaskType.GetValueOrDefault().GetTaskTypeGroup() == TaskTypeGroup.OutbondGroup)
            {
                // ä¸Šä¸€ä»»åŠ¡æ˜¯å‡ºåº“ï¼Œä¼˜å…ˆæŸ¥å…¥åº“ä»»åŠ¡
                candidateTask = _taskService.QueryStackerCraneInTask(commonStackerCrane.DeviceCode);
                // å¦‚果没有入库任务,再查一下出库任务
                candidateTask ??= _taskService.QueryStackerCraneOutTask(commonStackerCrane.DeviceCode);
            }
            else
            {
                // ä¸Šä¸€ä»»åŠ¡æ˜¯å…¥åº“ï¼ˆéžå‡ºåº“ï¼‰ï¼Œä¼˜å…ˆæŸ¥å‡ºåº“ä»»åŠ¡
                candidateTask = _taskService.QueryStackerCraneOutTask(commonStackerCrane.DeviceCode);
            }
            // å¦‚果没有候选任务,返回 null
            if (candidateTask == null)
            {
                return null;
            }
            // å¦‚果不是出库任务,直接返回
            if (candidateTask.TaskType.GetTaskTypeGroup() != TaskTypeGroup.OutbondGroup)
            {
                return candidateTask;
            }
            // å°è¯•选择出库任务(可能需要移库检查和站台可用性判断)
            Dt_Task? selectedTask = TrySelectOutboundTask(candidateTask);
            if (selectedTask != null)
            {
                return selectedTask;
            }
            // æŸ¥æ‰¾å…¶ä»–可用的出库站台
            var otherOutStationCodes = _routerService
                .QueryNextRoutes(commonStackerCrane.DeviceCode, candidateTask.NextAddress, candidateTask.TaskType)
                .Select(x => x.ChildPosi)
                .ToList();
            // æŸ¥è¯¢å…¶ä»–站台的出库任务
            var tasks = _taskService.QueryStackerCraneOutTasks(commonStackerCrane.DeviceCode, otherOutStationCodes);
            foreach (var alternativeTask in tasks)
            {
@@ -81,89 +144,147 @@
                }
            }
            // æ²¡æœ‰å¯ç”¨å‡ºåº“任务,尝试返回入库任务
            return _taskService.QueryStackerCraneInTask(commonStackerCrane.DeviceCode);
        }
        /// <summary>
        /// å°è¯•选择出库任务
        /// </summary>
        /// <remarks>
        /// å¯¹å€™é€‰å‡ºåº“任务进行:
        /// 1. ç§»åº“检查(调用 WMS åˆ¤æ–­æ˜¯å¦éœ€è¦ç§»åº“)
        /// 2. ç«™å°å¯ç”¨æ€§åˆ¤æ–­
        ///
        /// å¦‚果任务被判定为需要移库,则返回移库后的任务。
        /// </remarks>
        /// <param name="outboundTask">候选出库任务</param>
        /// <returns>可选中的任务,或 null(站台不可用)</returns>
        private Dt_Task? TrySelectOutboundTask(Dt_Task outboundTask)
        {
            // åªè¦æ˜¯å‡ºåº“任务,必须先调用WMS判断是否需要移库。
            // å¯¹äºŽæ‰€æœ‰å‡ºåº“任务,必须先调用 WMS åˆ¤æ–­æ˜¯å¦éœ€è¦ç§»åº“
            var taskAfterTransferCheck = _transferCheck(outboundTask.TaskNum) ?? outboundTask;
            var taskGroup = taskAfterTransferCheck.TaskType.GetTaskTypeGroup();
            // å¦‚果是移库任务或出库任务,尝试从 WMS æ·»åŠ ä»»åŠ¡
            if (taskGroup == TaskTypeGroup.RelocationGroup || taskGroup == TaskTypeGroup.OutbondGroup)
            {
                TryAddTaskFromWms(taskAfterTransferCheck);
            }
            // å¦‚果是移库任务,直接返回
            if (taskGroup == TaskTypeGroup.RelocationGroup)
            {
                return taskAfterTransferCheck;
            }
            // å¦‚果不是出库任务,返回原任务
            if (taskGroup != TaskTypeGroup.OutbondGroup)
            {
                return taskAfterTransferCheck;
            }
            // åˆ¤æ–­å‡ºåº“站台是否可用
            return IsOutTaskStationAvailable(taskAfterTransferCheck) ? taskAfterTransferCheck : null;
        }
        /// <summary>
        /// è°ƒç”¨ WMS æ£€æŸ¥ç§»åº“
        /// </summary>
        /// <remarks>
        /// é€šè¿‡ HTTP è¯·æ±‚调用 WMS çš„移库检查接口。
        /// </remarks>
        /// <param name="httpClientHelper">HTTP å®¢æˆ·ç«¯å¸®åŠ©ç±»</param>
        /// <param name="taskNum">任务号</param>
        /// <returns>如果需要移库返回移库任务,否则返回 null</returns>
        private static Dt_Task? QueryTransferTask(HttpClientHelper httpClientHelper, int taskNum)
        {
            // è°ƒç”¨ WMS çš„移库检查接口
            var response = httpClientHelper.Post<WebResponseContent>(
                nameof(ConfigKey.TransferCheck),
                taskNum.ToString());
            // æ£€æŸ¥å“åº”是否成功
            if (response == null || !response.IsSuccess || response.Data == null || !response.Data.Status || response.Data.Data == null)
            {
                return null;
            }
            // è§£æžè¿”回的任务数据
            var taskJson = response.Data.Data.ToString();
            return string.IsNullOrWhiteSpace(taskJson) ? null : JsonConvert.DeserializeObject<Dt_Task>(taskJson);
        }
        /// <summary>
        /// å°è¯•从 WMS æ·»åŠ ä»»åŠ¡
        /// </summary>
        /// <remarks>
        /// å¦‚果任务不存在于本地数据库,从 WMS è¿”回的数据添加到本地。
        /// </remarks>
        /// <param name="task">任务对象</param>
        private void TryAddTaskFromWms(Dt_Task task)
        {
            // æ£€æŸ¥ä»»åŠ¡å·æ˜¯å¦æœ‰æ•ˆ
            if (task.TaskNum <= 0)
            {
                return;
            }
            // æ£€æŸ¥ä»»åŠ¡æ˜¯å¦å·²å­˜åœ¨
            var existingTask = _taskService.QueryByTaskNum(task.TaskNum);
            if (existingTask != null)
            {
                return;
            }
            // æ·»åŠ åˆ°æœ¬åœ°æ•°æ®åº“
            _taskService.AddData(task);
        }
        /// <summary>
        /// åˆ¤æ–­å‡ºåº“站台是否可用
        /// </summary>
        /// <remarks>
        /// æ£€æŸ¥ç›®æ ‡ç«™å°å¯¹åº”的输送线是否被占用。
        /// å¦‚果站台上有货物,则该站台不可用。
        /// </remarks>
        /// <param name="task">出库任务</param>
        /// <returns>站台可用返回 true</returns>
        private bool IsOutTaskStationAvailable([NotNull] Dt_Task task)
        {
            // ç¡®å®šä»»åŠ¡ç±»åž‹
            int taskType = 0;
            if (task.TaskType == (int)TaskOutboundTypeEnum.OutEmpty)
            {
                // ç©ºæ‰˜ç›˜å‡ºåº“
                taskType = 100;
            }
            else
                taskType = task.TaskType;
            // æŸ¥è¯¢ç«™å°è·¯ç”±ä¿¡æ¯
            Dt_Router? router = _routerService.QueryNextRoute(task.Roadway, task.NextAddress, taskType);
            if (router == null)
            {
                // æœªæ‰¾åˆ°ç«™å°è·¯ç”±ä¿¡æ¯
                _taskService.UpdateTaskExceptionMessage(task.TaskNum, $"未找到站台【{task.NextAddress}】信息,无法校验站台");
                return false;
            }
            // æŸ¥æ‰¾ç«™å°å¯¹åº”的设备
            IDevice? device = Storage.Devices.FirstOrDefault(x => x.DeviceCode == router.ChildPosiDeviceCode);
            if (device == null)
            {
                // æœªæ‰¾åˆ°è®¾å¤‡
                _taskService.UpdateTaskExceptionMessage(task.TaskNum, $"未找到出库站台【{router.ChildPosiDeviceCode}】对应的通讯对象,无法判断出库站台是否被占用");
                return false;
            }
            // è½¬æ¢ä¸ºè¾“送线设备
            CommonConveyorLine conveyorLine = (CommonConveyorLine)device;
            // æ£€æŸ¥ç«™å°æ˜¯å¦è¢«å ç”¨
            return conveyorLine.IsOccupied(router.ChildPosi);
        }
    }
}
}
Code/WMS/.claude/settings.local.json
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,8 @@
{
  "permissions": {
    "allow": [
      "Bash(git:*)",
      "Bash(dotnet build:*)"
    ]
  }
}
Code/WMS/WIDESEA_WMSClient/.omc/project-memory.json
@@ -1,6 +1,6 @@
{
  "version": "1.0.0",
  "lastScanned": 1774324348368,
  "lastScanned": 1774489415859,
  "projectRoot": "d:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSClient",
  "techStack": {
    "languages": [
@@ -25,16 +25,16 @@
        "category": "build"
      }
    ],
    "packageManager": "yarn",
    "packageManager": "pnpm",
    "runtime": null
  },
  "build": {
    "buildCommand": "yarn build",
    "buildCommand": "pnpm build",
    "testCommand": null,
    "lintCommand": "yarn lint",
    "devCommand": "yarn dev",
    "lintCommand": "pnpm lint",
    "devCommand": null,
    "scripts": {
      "dev": "vite",
      "serve": "vite",
      "build": "vite build",
      "preview": "vite preview --port 8080",
      "test:unit": "vue-cli-service test:unit",
@@ -66,7 +66,7 @@
      "path": "config",
      "purpose": "Configuration files",
      "fileCount": 1,
      "lastAccessed": 1774324348233,
      "lastAccessed": 1774489415781,
      "keyFiles": [
        "buttons.js"
      ]
@@ -75,7 +75,7 @@
      "path": "dist",
      "purpose": "Distribution/build output",
      "fileCount": 4,
      "lastAccessed": 1774324348234,
      "lastAccessed": 1774489415782,
      "keyFiles": [
        "index.html",
        "wcslogo.png",
@@ -87,7 +87,7 @@
      "path": "public",
      "purpose": "Public files",
      "fileCount": 4,
      "lastAccessed": 1774324348294,
      "lastAccessed": 1774489415810,
      "keyFiles": [
        "index.html",
        "wcslogo.png",
@@ -99,7 +99,7 @@
      "path": "src",
      "purpose": "Source code",
      "fileCount": 2,
      "lastAccessed": 1774324348294,
      "lastAccessed": 1774489415810,
      "keyFiles": [
        "App.vue",
        "main.js"
@@ -109,25 +109,25 @@
      "path": "tests",
      "purpose": "Test files",
      "fileCount": 0,
      "lastAccessed": 1774324348294,
      "lastAccessed": 1774489415811,
      "keyFiles": []
    },
    "dist\\assets": {
      "path": "dist\\assets",
      "purpose": "Static assets",
      "fileCount": 106,
      "lastAccessed": 1774324348296,
      "fileCount": 107,
      "lastAccessed": 1774489415814,
      "keyFiles": [
        "401-CX5beHYt.js",
        "404-mamt5IUf.js",
        "Audit-BmPdFI9f.js"
        "401-cNgsEGiV.js",
        "404-CZEj1mZh.js",
        "Audit-C52MvHzW.css"
      ]
    },
    "dist\\static": {
      "path": "dist\\static",
      "purpose": "Static files",
      "fileCount": 1,
      "lastAccessed": 1774324348296,
      "lastAccessed": 1774489415815,
      "keyFiles": [
        "login_bg.png"
      ]
@@ -136,7 +136,7 @@
      "path": "public\\static",
      "purpose": "Static files",
      "fileCount": 1,
      "lastAccessed": 1774324348297,
      "lastAccessed": 1774489415815,
      "keyFiles": [
        "login_bg.png"
      ]
@@ -145,7 +145,7 @@
      "path": "src\\api",
      "purpose": "API routes",
      "fileCount": 3,
      "lastAccessed": 1774324348297,
      "lastAccessed": 1774489415816,
      "keyFiles": [
        "http.js",
        "permission.js",
@@ -156,7 +156,7 @@
      "path": "src\\assets",
      "purpose": "Static assets",
      "fileCount": 1,
      "lastAccessed": 1774324348298,
      "lastAccessed": 1774489415817,
      "keyFiles": [
        "logo.png"
      ]
@@ -165,14 +165,14 @@
      "path": "src\\components",
      "purpose": "UI components",
      "fileCount": 0,
      "lastAccessed": 1774324348299,
      "lastAccessed": 1774489415817,
      "keyFiles": []
    },
    "src\\views": {
      "path": "src\\views",
      "purpose": "View templates",
      "fileCount": 3,
      "lastAccessed": 1774324348299,
      "lastAccessed": 1774489415818,
      "keyFiles": [
        "Home.vue",
        "Index.vue",
Code/WMS/WIDESEA_WMSServer/.vs/WIDESEA_WMSServer/CopilotIndices/18.0.988.22099/CodeChunks.db
Binary files differ
Code/WMS/WIDESEA_WMSServer/.vs/WIDESEA_WMSServer/CopilotIndices/18.0.988.22099/SemanticSymbols.db
Binary files differ
Code/WMS/WIDESEA_WMSServer/.vs/WIDESEA_WMSServer/v18/DocumentLayout.backup.json
@@ -3,54 +3,6 @@
  "WorkspaceRootPath": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\",
  "Documents": [
    {
      "AbsoluteMoniker": "D:0:0:{D81A65B5-47D1-40C1-8FDE-7D24FF003F51}|WIDESEA_WMSServer\\WIDESEA_WMSServer.csproj|d:\\git\\shanmeixinnengyuan\\code\\wms\\widesea_wmsserver\\widesea_wmsserver\\backgroundservices\\autooutboundtaskbackgroundservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
      "RelativeMoniker": "D:0:0:{D81A65B5-47D1-40C1-8FDE-7D24FF003F51}|WIDESEA_WMSServer\\WIDESEA_WMSServer.csproj|solutionrelative:widesea_wmsserver\\backgroundservices\\autooutboundtaskbackgroundservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
    },
    {
      "AbsoluteMoniker": "D:0:0:{7DC26D42-D8EE-46F0-BA66-A13457086885}|WIDESEA_StockService\\WIDESEA_StockService.csproj|d:\\git\\shanmeixinnengyuan\\code\\wms\\widesea_wmsserver\\widesea_stockservice\\stockserivce.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
      "RelativeMoniker": "D:0:0:{7DC26D42-D8EE-46F0-BA66-A13457086885}|WIDESEA_StockService\\WIDESEA_StockService.csproj|solutionrelative:widesea_stockservice\\stockserivce.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
    },
    {
      "AbsoluteMoniker": "D:0:0:{7DC26D42-D8EE-46F0-BA66-A13457086885}|WIDESEA_StockService\\WIDESEA_StockService.csproj|d:\\git\\shanmeixinnengyuan\\code\\wms\\widesea_wmsserver\\widesea_stockservice\\stockinfoservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
      "RelativeMoniker": "D:0:0:{7DC26D42-D8EE-46F0-BA66-A13457086885}|WIDESEA_StockService\\WIDESEA_StockService.csproj|solutionrelative:widesea_stockservice\\stockinfoservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
    },
    {
      "AbsoluteMoniker": "D:0:0:{7D7534D4-51D9-46DC-A6B7-6430042F4E12}|WIDESEA_TaskInfoService\\WIDESEA_TaskInfoService.csproj|d:\\git\\shanmeixinnengyuan\\code\\wms\\widesea_wmsserver\\widesea_taskinfoservice\\taskservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
      "RelativeMoniker": "D:0:0:{7D7534D4-51D9-46DC-A6B7-6430042F4E12}|WIDESEA_TaskInfoService\\WIDESEA_TaskInfoService.csproj|solutionrelative:widesea_taskinfoservice\\taskservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
    },
    {
      "AbsoluteMoniker": "D:0:0:{929DF936-042C-4EEC-8722-A831FC2F0AEA}|WIDESEA_DTO\\WIDESEA_DTO.csproj|d:\\git\\shanmeixinnengyuan\\code\\wms\\widesea_wmsserver\\widesea_dto\\task\\wmstaskdto.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
      "RelativeMoniker": "D:0:0:{929DF936-042C-4EEC-8722-A831FC2F0AEA}|WIDESEA_DTO\\WIDESEA_DTO.csproj|solutionrelative:widesea_dto\\task\\wmstaskdto.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
    },
    {
      "AbsoluteMoniker": "D:0:0:{D81A65B5-47D1-40C1-8FDE-7D24FF003F51}|WIDESEA_WMSServer\\WIDESEA_WMSServer.csproj|d:\\git\\shanmeixinnengyuan\\code\\wms\\widesea_wmsserver\\widesea_wmsserver\\controllers\\taskinfo\\taskcontroller.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
      "RelativeMoniker": "D:0:0:{D81A65B5-47D1-40C1-8FDE-7D24FF003F51}|WIDESEA_WMSServer\\WIDESEA_WMSServer.csproj|solutionrelative:widesea_wmsserver\\controllers\\taskinfo\\taskcontroller.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
    },
    {
      "AbsoluteMoniker": "D:0:0:{AF8F3D65-1D75-4B8F-AFD9-4150E591C44D}|WIDESEA_Common\\WIDESEA_Common.csproj|d:\\git\\shanmeixinnengyuan\\code\\wms\\widesea_wmsserver\\widesea_common\\taskenum\\tasktypeenum.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
      "RelativeMoniker": "D:0:0:{AF8F3D65-1D75-4B8F-AFD9-4150E591C44D}|WIDESEA_Common\\WIDESEA_Common.csproj|solutionrelative:widesea_common\\taskenum\\tasktypeenum.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
    },
    {
      "AbsoluteMoniker": "D:0:0:{AF8F3D65-1D75-4B8F-AFD9-4150E591C44D}|WIDESEA_Common\\WIDESEA_Common.csproj|d:\\git\\shanmeixinnengyuan\\code\\wms\\widesea_wmsserver\\widesea_common\\stockenum\\stockstatusemun.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
      "RelativeMoniker": "D:0:0:{AF8F3D65-1D75-4B8F-AFD9-4150E591C44D}|WIDESEA_Common\\WIDESEA_Common.csproj|solutionrelative:widesea_common\\stockenum\\stockstatusemun.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
    },
    {
      "AbsoluteMoniker": "D:0:0:{D11C804C-2FF4-4C18-A3EE-2F0574427BB3}|WIDESEA_BasicService\\WIDESEA_BasicService.csproj|d:\\git\\shanmeixinnengyuan\\code\\wms\\widesea_wmsserver\\widesea_basicservice\\locationinfoservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
      "RelativeMoniker": "D:0:0:{D11C804C-2FF4-4C18-A3EE-2F0574427BB3}|WIDESEA_BasicService\\WIDESEA_BasicService.csproj|solutionrelative:widesea_basicservice\\locationinfoservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
    },
    {
      "AbsoluteMoniker": "D:0:0:{929DF936-042C-4EEC-8722-A831FC2F0AEA}|WIDESEA_DTO\\WIDESEA_DTO.csproj|d:\\git\\shanmeixinnengyuan\\code\\wms\\widesea_wmsserver\\widesea_dto\\stock\\stockdto.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
      "RelativeMoniker": "D:0:0:{929DF936-042C-4EEC-8722-A831FC2F0AEA}|WIDESEA_DTO\\WIDESEA_DTO.csproj|solutionrelative:widesea_dto\\stock\\stockdto.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
    },
    {
      "AbsoluteMoniker": "D:0:0:{CE0DB91F-5A68-448E-A419-4C26B5039F51}|WIDESEA_ITaskInfoService\\WIDESEA_ITaskInfoService.csproj|d:\\git\\shanmeixinnengyuan\\code\\wms\\widesea_wmsserver\\widesea_itaskinfoservice\\itaskservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
      "RelativeMoniker": "D:0:0:{CE0DB91F-5A68-448E-A419-4C26B5039F51}|WIDESEA_ITaskInfoService\\WIDESEA_ITaskInfoService.csproj|solutionrelative:widesea_itaskinfoservice\\itaskservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
    },
    {
      "AbsoluteMoniker": "D:0:0:{AF8F3D65-1D75-4B8F-AFD9-4150E591C44D}|WIDESEA_Common\\WIDESEA_Common.csproj|d:\\git\\shanmeixinnengyuan\\code\\wms\\widesea_wmsserver\\widesea_common\\taskenum\\taskstatusenum.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
      "RelativeMoniker": "D:0:0:{AF8F3D65-1D75-4B8F-AFD9-4150E591C44D}|WIDESEA_Common\\WIDESEA_Common.csproj|solutionrelative:widesea_common\\taskenum\\taskstatusenum.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
    },
    {
      "AbsoluteMoniker": "D:0:0:{D81A65B5-47D1-40C1-8FDE-7D24FF003F51}|WIDESEA_WMSServer\\WIDESEA_WMSServer.csproj|d:\\git\\shanmeixinnengyuan\\code\\wms\\widesea_wmsserver\\widesea_wmsserver\\appsettings.json||{90A6B3A7-C1A3-4009-A288-E2FF89E96FA0}",
      "RelativeMoniker": "D:0:0:{D81A65B5-47D1-40C1-8FDE-7D24FF003F51}|WIDESEA_WMSServer\\WIDESEA_WMSServer.csproj|solutionrelative:widesea_wmsserver\\appsettings.json||{90A6B3A7-C1A3-4009-A288-E2FF89E96FA0}"
    },
@@ -59,8 +11,8 @@
      "RelativeMoniker": "D:0:0:{D81A65B5-47D1-40C1-8FDE-7D24FF003F51}|WIDESEA_WMSServer\\WIDESEA_WMSServer.csproj|solutionrelative:widesea_wmsserver\\program.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
    },
    {
      "AbsoluteMoniker": "D:0:0:{D81A65B5-47D1-40C1-8FDE-7D24FF003F51}|WIDESEA_WMSServer\\WIDESEA_WMSServer.csproj|d:\\git\\shanmeixinnengyuan\\code\\wms\\widesea_wmsserver\\widesea_wmsserver\\controllers\\basic\\locationinfocontroller.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
      "RelativeMoniker": "D:0:0:{D81A65B5-47D1-40C1-8FDE-7D24FF003F51}|WIDESEA_WMSServer\\WIDESEA_WMSServer.csproj|solutionrelative:widesea_wmsserver\\controllers\\basic\\locationinfocontroller.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
      "AbsoluteMoniker": "D:0:0:{111BD7AA-9749-4506-9772-79F9EF14754C}|WIDESEA_Core\\WIDESEA_Core.csproj|d:\\git\\shanmeixinnengyuan\\code\\wms\\widesea_wmsserver\\widesea_core\\baseservices\\servicebase.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
      "RelativeMoniker": "D:0:0:{111BD7AA-9749-4506-9772-79F9EF14754C}|WIDESEA_Core\\WIDESEA_Core.csproj|solutionrelative:widesea_core\\baseservices\\servicebase.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
    }
  ],
  "DocumentGroupContainers": [
@@ -70,7 +22,7 @@
      "DocumentGroups": [
        {
          "DockedWidth": 200,
          "SelectedChildIndex": 8,
          "SelectedChildIndex": 4,
          "Children": [
            {
              "$type": "Bookmark",
@@ -86,186 +38,41 @@
            },
            {
              "$type": "Document",
              "DocumentIndex": 4,
              "Title": "WMSTaskDTO.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_DTO\\Task\\WMSTaskDTO.cs",
              "RelativeDocumentMoniker": "WIDESEA_DTO\\Task\\WMSTaskDTO.cs",
              "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_DTO\\Task\\WMSTaskDTO.cs",
              "RelativeToolTip": "WIDESEA_DTO\\Task\\WMSTaskDTO.cs",
              "ViewState": "AgIAABgAAAAAAAAAAAAIwCkAAAAdAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-03-17T07:42:09.48Z"
            },
            {
              "$type": "Document",
              "DocumentIndex": 9,
              "Title": "StockDTO.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_DTO\\Stock\\StockDTO.cs",
              "RelativeDocumentMoniker": "WIDESEA_DTO\\Stock\\StockDTO.cs",
              "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_DTO\\Stock\\StockDTO.cs",
              "RelativeToolTip": "WIDESEA_DTO\\Stock\\StockDTO.cs",
              "ViewState": "AgIAAA8AAAAAAAAAAAAIwAoAAAA+AAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-03-17T03:01:04.659Z"
            },
            {
              "$type": "Document",
              "DocumentIndex": 10,
              "Title": "ITaskService.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_ITaskInfoService\\ITaskService.cs",
              "RelativeDocumentMoniker": "WIDESEA_ITaskInfoService\\ITaskService.cs",
              "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_ITaskInfoService\\ITaskService.cs",
              "RelativeToolTip": "WIDESEA_ITaskInfoService\\ITaskService.cs",
              "ViewState": "AgIAAIIAAAAAAAAAAAAMwIkAAAAVAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-03-17T03:12:58.977Z"
            },
            {
              "$type": "Document",
              "DocumentIndex": 6,
              "Title": "TaskTypeEnum.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_Common\\TaskEnum\\TaskTypeEnum.cs",
              "RelativeDocumentMoniker": "WIDESEA_Common\\TaskEnum\\TaskTypeEnum.cs",
              "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_Common\\TaskEnum\\TaskTypeEnum.cs",
              "RelativeToolTip": "WIDESEA_Common\\TaskEnum\\TaskTypeEnum.cs",
              "ViewState": "AgIAAIsAAAAAAAAAAAAewKAAAAAIAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-03-17T02:28:48.033Z"
            },
            {
              "$type": "Document",
              "DocumentIndex": 5,
              "Title": "TaskController.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_WMSServer\\Controllers\\TaskInfo\\TaskController.cs",
              "RelativeDocumentMoniker": "WIDESEA_WMSServer\\Controllers\\TaskInfo\\TaskController.cs",
              "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_WMSServer\\Controllers\\TaskInfo\\TaskController.cs",
              "RelativeToolTip": "WIDESEA_WMSServer\\Controllers\\TaskInfo\\TaskController.cs",
              "ViewState": "AgIAAIoAAAAAAAAAAAAhwJsAAAAIAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-03-13T02:00:31.089Z"
            },
            {
              "$type": "Document",
              "DocumentIndex": 0,
              "Title": "AutoOutboundTaskBackgroundService.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_WMSServer\\BackgroundServices\\AutoOutboundTaskBackgroundService.cs",
              "RelativeDocumentMoniker": "WIDESEA_WMSServer\\BackgroundServices\\AutoOutboundTaskBackgroundService.cs",
              "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_WMSServer\\BackgroundServices\\AutoOutboundTaskBackgroundService.cs",
              "RelativeToolTip": "WIDESEA_WMSServer\\BackgroundServices\\AutoOutboundTaskBackgroundService.cs",
              "ViewState": "AgIAABQAAAAAAAAAAADwvykAAABdAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-03-12T10:18:13.91Z",
              "EditorCaption": ""
            },
            {
              "$type": "Document",
              "DocumentIndex": 12,
              "Title": "appsettings.json",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_WMSServer\\appsettings.json",
              "RelativeDocumentMoniker": "WIDESEA_WMSServer\\appsettings.json",
              "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_WMSServer\\appsettings.json",
              "RelativeToolTip": "WIDESEA_WMSServer\\appsettings.json",
              "ViewState": "AgIAAAAAAAAAAAAAAAAAABgAAAA9AAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.001642|",
              "WhenOpened": "2026-03-12T10:06:27.509Z"
            },
            {
              "$type": "Document",
              "DocumentIndex": 13,
              "DocumentIndex": 1,
              "Title": "Program.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_WMSServer\\Program.cs",
              "RelativeDocumentMoniker": "WIDESEA_WMSServer\\Program.cs",
              "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_WMSServer\\Program.cs",
              "RelativeToolTip": "WIDESEA_WMSServer\\Program.cs",
              "ViewState": "AgIAABcAAAAAAAAAAAAswCYAAAAAAAAAAAAAAA==",
              "ViewState": "AgIAACIAAAAAAAAAAAAiwDcAAAAYAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-03-12T09:52:09.124Z"
            },
            {
              "$type": "Document",
              "DocumentIndex": 7,
              "Title": "StockStatusEmun.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_Common\\StockEnum\\StockStatusEmun.cs",
              "RelativeDocumentMoniker": "WIDESEA_Common\\StockEnum\\StockStatusEmun.cs",
              "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_Common\\StockEnum\\StockStatusEmun.cs",
              "RelativeToolTip": "WIDESEA_Common\\StockEnum\\StockStatusEmun.cs",
              "ViewState": "AgIAACMAAAAAAAAAAAAewDsAAAAIAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-03-12T09:08:34.784Z"
            },
            {
              "$type": "Document",
              "DocumentIndex": 11,
              "Title": "TaskStatusEnum.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_Common\\TaskEnum\\TaskStatusEnum.cs",
              "RelativeDocumentMoniker": "WIDESEA_Common\\TaskEnum\\TaskStatusEnum.cs",
              "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_Common\\TaskEnum\\TaskStatusEnum.cs",
              "RelativeToolTip": "WIDESEA_Common\\TaskEnum\\TaskStatusEnum.cs",
              "ViewState": "AgIAAJsAAAAAAAAAAAAAwKkAAAASAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-03-12T07:20:25.969Z"
            },
            {
              "$type": "Document",
              "DocumentIndex": 8,
              "Title": "LocationInfoService.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_BasicService\\LocationInfoService.cs",
              "RelativeDocumentMoniker": "WIDESEA_BasicService\\LocationInfoService.cs",
              "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_BasicService\\LocationInfoService.cs",
              "RelativeToolTip": "WIDESEA_BasicService\\LocationInfoService.cs",
              "ViewState": "AgIAAFcAAAAAAAAAAAAAwGgAAAARAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-03-12T02:05:14.224Z",
              "WhenOpened": "2026-03-25T08:48:13.949Z",
              "EditorCaption": ""
            },
            {
              "$type": "Document",
              "DocumentIndex": 14,
              "Title": "LocationInfoController.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_WMSServer\\Controllers\\Basic\\LocationInfoController.cs",
              "RelativeDocumentMoniker": "WIDESEA_WMSServer\\Controllers\\Basic\\LocationInfoController.cs",
              "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_WMSServer\\Controllers\\Basic\\LocationInfoController.cs",
              "RelativeToolTip": "WIDESEA_WMSServer\\Controllers\\Basic\\LocationInfoController.cs",
              "ViewState": "AgIAAEkAAAAAAAAAAAAIwF8AAAAsAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-03-12T01:53:01.837Z"
              "DocumentIndex": 0,
              "Title": "appsettings.json",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_WMSServer\\appsettings.json",
              "RelativeDocumentMoniker": "WIDESEA_WMSServer\\appsettings.json",
              "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_WMSServer\\appsettings.json",
              "RelativeToolTip": "WIDESEA_WMSServer\\appsettings.json",
              "ViewState": "AgIAAAAAAAAAAAAAAAAAAAcAAAAoAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.001642|",
              "WhenOpened": "2026-03-25T08:40:34.998Z",
              "EditorCaption": ""
            },
            {
              "$type": "Document",
              "DocumentIndex": 2,
              "Title": "StockInfoService.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_StockService\\StockInfoService.cs",
              "RelativeDocumentMoniker": "WIDESEA_StockService\\StockInfoService.cs",
              "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_StockService\\StockInfoService.cs",
              "RelativeToolTip": "WIDESEA_StockService\\StockInfoService.cs",
              "ViewState": "AgIAADQAAAAAAAAAAAAIwDkAAAAuAAAAAAAAAA==",
              "Title": "ServiceBase.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_Core\\BaseServices\\ServiceBase.cs",
              "RelativeDocumentMoniker": "WIDESEA_Core\\BaseServices\\ServiceBase.cs",
              "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_Core\\BaseServices\\ServiceBase.cs",
              "RelativeToolTip": "WIDESEA_Core\\BaseServices\\ServiceBase.cs",
              "ViewState": "AgIAACsCAAAAAAAAAAASwEACAAAIAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-03-11T09:16:37.34Z"
            },
            {
              "$type": "Document",
              "DocumentIndex": 1,
              "Title": "StockSerivce.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_StockService\\StockSerivce.cs",
              "RelativeDocumentMoniker": "WIDESEA_StockService\\StockSerivce.cs",
              "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_StockService\\StockSerivce.cs",
              "RelativeToolTip": "WIDESEA_StockService\\StockSerivce.cs",
              "ViewState": "AgIAAAAAAAAAAAAAAAAAABkAAAAWAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-03-17T02:09:23.282Z",
              "EditorCaption": ""
            },
            {
              "$type": "Document",
              "DocumentIndex": 3,
              "Title": "TaskService.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_TaskInfoService\\TaskService.cs",
              "RelativeDocumentMoniker": "WIDESEA_TaskInfoService\\TaskService.cs",
              "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_TaskInfoService\\TaskService.cs",
              "RelativeToolTip": "WIDESEA_TaskInfoService\\TaskService.cs",
              "ViewState": "AgIAACkAAAAAAAAAAAAywBAAAAAcAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-02-06T06:34:59.734Z",
              "WhenOpened": "2026-03-25T06:03:58.013Z",
              "EditorCaption": ""
            }
          ]
Code/WMS/WIDESEA_WMSServer/.vs/WIDESEA_WMSServer/v18/DocumentLayout.json
@@ -3,64 +3,16 @@
  "WorkspaceRootPath": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\",
  "Documents": [
    {
      "AbsoluteMoniker": "D:0:0:{D81A65B5-47D1-40C1-8FDE-7D24FF003F51}|WIDESEA_WMSServer\\WIDESEA_WMSServer.csproj|d:\\git\\shanmeixinnengyuan\\code\\wms\\widesea_wmsserver\\widesea_wmsserver\\backgroundservices\\autooutboundtaskbackgroundservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
      "RelativeMoniker": "D:0:0:{D81A65B5-47D1-40C1-8FDE-7D24FF003F51}|WIDESEA_WMSServer\\WIDESEA_WMSServer.csproj|solutionrelative:widesea_wmsserver\\backgroundservices\\autooutboundtaskbackgroundservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
    },
    {
      "AbsoluteMoniker": "D:0:0:{7DC26D42-D8EE-46F0-BA66-A13457086885}|WIDESEA_StockService\\WIDESEA_StockService.csproj|d:\\git\\shanmeixinnengyuan\\code\\wms\\widesea_wmsserver\\widesea_stockservice\\stockserivce.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
      "RelativeMoniker": "D:0:0:{7DC26D42-D8EE-46F0-BA66-A13457086885}|WIDESEA_StockService\\WIDESEA_StockService.csproj|solutionrelative:widesea_stockservice\\stockserivce.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
    },
    {
      "AbsoluteMoniker": "D:0:0:{7DC26D42-D8EE-46F0-BA66-A13457086885}|WIDESEA_StockService\\WIDESEA_StockService.csproj|d:\\git\\shanmeixinnengyuan\\code\\wms\\widesea_wmsserver\\widesea_stockservice\\stockinfoservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
      "RelativeMoniker": "D:0:0:{7DC26D42-D8EE-46F0-BA66-A13457086885}|WIDESEA_StockService\\WIDESEA_StockService.csproj|solutionrelative:widesea_stockservice\\stockinfoservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
    },
    {
      "AbsoluteMoniker": "D:0:0:{7D7534D4-51D9-46DC-A6B7-6430042F4E12}|WIDESEA_TaskInfoService\\WIDESEA_TaskInfoService.csproj|d:\\git\\shanmeixinnengyuan\\code\\wms\\widesea_wmsserver\\widesea_taskinfoservice\\taskservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
      "RelativeMoniker": "D:0:0:{7D7534D4-51D9-46DC-A6B7-6430042F4E12}|WIDESEA_TaskInfoService\\WIDESEA_TaskInfoService.csproj|solutionrelative:widesea_taskinfoservice\\taskservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
    },
    {
      "AbsoluteMoniker": "D:0:0:{929DF936-042C-4EEC-8722-A831FC2F0AEA}|WIDESEA_DTO\\WIDESEA_DTO.csproj|d:\\git\\shanmeixinnengyuan\\code\\wms\\widesea_wmsserver\\widesea_dto\\task\\wmstaskdto.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
      "RelativeMoniker": "D:0:0:{929DF936-042C-4EEC-8722-A831FC2F0AEA}|WIDESEA_DTO\\WIDESEA_DTO.csproj|solutionrelative:widesea_dto\\task\\wmstaskdto.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
    },
    {
      "AbsoluteMoniker": "D:0:0:{D81A65B5-47D1-40C1-8FDE-7D24FF003F51}|WIDESEA_WMSServer\\WIDESEA_WMSServer.csproj|d:\\git\\shanmeixinnengyuan\\code\\wms\\widesea_wmsserver\\widesea_wmsserver\\controllers\\taskinfo\\taskcontroller.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
      "RelativeMoniker": "D:0:0:{D81A65B5-47D1-40C1-8FDE-7D24FF003F51}|WIDESEA_WMSServer\\WIDESEA_WMSServer.csproj|solutionrelative:widesea_wmsserver\\controllers\\taskinfo\\taskcontroller.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
    },
    {
      "AbsoluteMoniker": "D:0:0:{AF8F3D65-1D75-4B8F-AFD9-4150E591C44D}|WIDESEA_Common\\WIDESEA_Common.csproj|d:\\git\\shanmeixinnengyuan\\code\\wms\\widesea_wmsserver\\widesea_common\\taskenum\\tasktypeenum.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
      "RelativeMoniker": "D:0:0:{AF8F3D65-1D75-4B8F-AFD9-4150E591C44D}|WIDESEA_Common\\WIDESEA_Common.csproj|solutionrelative:widesea_common\\taskenum\\tasktypeenum.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
    },
    {
      "AbsoluteMoniker": "D:0:0:{AF8F3D65-1D75-4B8F-AFD9-4150E591C44D}|WIDESEA_Common\\WIDESEA_Common.csproj|d:\\git\\shanmeixinnengyuan\\code\\wms\\widesea_wmsserver\\widesea_common\\stockenum\\stockstatusemun.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
      "RelativeMoniker": "D:0:0:{AF8F3D65-1D75-4B8F-AFD9-4150E591C44D}|WIDESEA_Common\\WIDESEA_Common.csproj|solutionrelative:widesea_common\\stockenum\\stockstatusemun.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
    },
    {
      "AbsoluteMoniker": "D:0:0:{D11C804C-2FF4-4C18-A3EE-2F0574427BB3}|WIDESEA_BasicService\\WIDESEA_BasicService.csproj|d:\\git\\shanmeixinnengyuan\\code\\wms\\widesea_wmsserver\\widesea_basicservice\\locationinfoservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
      "RelativeMoniker": "D:0:0:{D11C804C-2FF4-4C18-A3EE-2F0574427BB3}|WIDESEA_BasicService\\WIDESEA_BasicService.csproj|solutionrelative:widesea_basicservice\\locationinfoservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
    },
    {
      "AbsoluteMoniker": "D:0:0:{929DF936-042C-4EEC-8722-A831FC2F0AEA}|WIDESEA_DTO\\WIDESEA_DTO.csproj|d:\\git\\shanmeixinnengyuan\\code\\wms\\widesea_wmsserver\\widesea_dto\\stock\\stockdto.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
      "RelativeMoniker": "D:0:0:{929DF936-042C-4EEC-8722-A831FC2F0AEA}|WIDESEA_DTO\\WIDESEA_DTO.csproj|solutionrelative:widesea_dto\\stock\\stockdto.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
    },
    {
      "AbsoluteMoniker": "D:0:0:{CE0DB91F-5A68-448E-A419-4C26B5039F51}|WIDESEA_ITaskInfoService\\WIDESEA_ITaskInfoService.csproj|d:\\git\\shanmeixinnengyuan\\code\\wms\\widesea_wmsserver\\widesea_itaskinfoservice\\itaskservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
      "RelativeMoniker": "D:0:0:{CE0DB91F-5A68-448E-A419-4C26B5039F51}|WIDESEA_ITaskInfoService\\WIDESEA_ITaskInfoService.csproj|solutionrelative:widesea_itaskinfoservice\\itaskservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
    },
    {
      "AbsoluteMoniker": "D:0:0:{AF8F3D65-1D75-4B8F-AFD9-4150E591C44D}|WIDESEA_Common\\WIDESEA_Common.csproj|d:\\git\\shanmeixinnengyuan\\code\\wms\\widesea_wmsserver\\widesea_common\\taskenum\\taskstatusenum.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
      "RelativeMoniker": "D:0:0:{AF8F3D65-1D75-4B8F-AFD9-4150E591C44D}|WIDESEA_Common\\WIDESEA_Common.csproj|solutionrelative:widesea_common\\taskenum\\taskstatusenum.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
      "AbsoluteMoniker": "D:0:0:{111BD7AA-9749-4506-9772-79F9EF14754C}|WIDESEA_Core\\WIDESEA_Core.csproj|d:\\git\\shanmeixinnengyuan\\code\\wms\\widesea_wmsserver\\widesea_core\\baseservices\\servicebase.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
      "RelativeMoniker": "D:0:0:{111BD7AA-9749-4506-9772-79F9EF14754C}|WIDESEA_Core\\WIDESEA_Core.csproj|solutionrelative:widesea_core\\baseservices\\servicebase.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
    },
    {
      "AbsoluteMoniker": "D:0:0:{D81A65B5-47D1-40C1-8FDE-7D24FF003F51}|WIDESEA_WMSServer\\WIDESEA_WMSServer.csproj|d:\\git\\shanmeixinnengyuan\\code\\wms\\widesea_wmsserver\\widesea_wmsserver\\appsettings.json||{90A6B3A7-C1A3-4009-A288-E2FF89E96FA0}",
      "RelativeMoniker": "D:0:0:{D81A65B5-47D1-40C1-8FDE-7D24FF003F51}|WIDESEA_WMSServer\\WIDESEA_WMSServer.csproj|solutionrelative:widesea_wmsserver\\appsettings.json||{90A6B3A7-C1A3-4009-A288-E2FF89E96FA0}"
    },
    {
      "AbsoluteMoniker": "D:0:0:{D81A65B5-47D1-40C1-8FDE-7D24FF003F51}|WIDESEA_WMSServer\\WIDESEA_WMSServer.csproj|d:\\git\\shanmeixinnengyuan\\code\\wms\\widesea_wmsserver\\widesea_wmsserver\\program.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
      "RelativeMoniker": "D:0:0:{D81A65B5-47D1-40C1-8FDE-7D24FF003F51}|WIDESEA_WMSServer\\WIDESEA_WMSServer.csproj|solutionrelative:widesea_wmsserver\\program.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
    },
    {
      "AbsoluteMoniker": "D:0:0:{D81A65B5-47D1-40C1-8FDE-7D24FF003F51}|WIDESEA_WMSServer\\WIDESEA_WMSServer.csproj|d:\\git\\shanmeixinnengyuan\\code\\wms\\widesea_wmsserver\\widesea_wmsserver\\controllers\\basic\\locationinfocontroller.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
      "RelativeMoniker": "D:0:0:{D81A65B5-47D1-40C1-8FDE-7D24FF003F51}|WIDESEA_WMSServer\\WIDESEA_WMSServer.csproj|solutionrelative:widesea_wmsserver\\controllers\\basic\\locationinfocontroller.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
    }
  ],
  "DocumentGroupContainers": [
@@ -70,7 +22,7 @@
      "DocumentGroups": [
        {
          "DockedWidth": 200,
          "SelectedChildIndex": 8,
          "SelectedChildIndex": 3,
          "Children": [
            {
              "$type": "Bookmark",
@@ -86,184 +38,42 @@
            },
            {
              "$type": "Document",
              "DocumentIndex": 4,
              "Title": "WMSTaskDTO.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_DTO\\Task\\WMSTaskDTO.cs",
              "RelativeDocumentMoniker": "WIDESEA_DTO\\Task\\WMSTaskDTO.cs",
              "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_DTO\\Task\\WMSTaskDTO.cs",
              "RelativeToolTip": "WIDESEA_DTO\\Task\\WMSTaskDTO.cs",
              "ViewState": "AgIAABgAAAAAAAAAAAAIwCkAAAAdAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-03-17T07:42:09.48Z"
            },
            {
              "$type": "Document",
              "DocumentIndex": 9,
              "Title": "StockDTO.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_DTO\\Stock\\StockDTO.cs",
              "RelativeDocumentMoniker": "WIDESEA_DTO\\Stock\\StockDTO.cs",
              "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_DTO\\Stock\\StockDTO.cs",
              "RelativeToolTip": "WIDESEA_DTO\\Stock\\StockDTO.cs",
              "ViewState": "AgIAAA8AAAAAAAAAAAAIwAoAAAA+AAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-03-17T03:01:04.659Z"
            },
            {
              "$type": "Document",
              "DocumentIndex": 10,
              "Title": "ITaskService.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_ITaskInfoService\\ITaskService.cs",
              "RelativeDocumentMoniker": "WIDESEA_ITaskInfoService\\ITaskService.cs",
              "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_ITaskInfoService\\ITaskService.cs",
              "RelativeToolTip": "WIDESEA_ITaskInfoService\\ITaskService.cs",
              "ViewState": "AgIAAIIAAAAAAAAAAAAMwIkAAAAVAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-03-17T03:12:58.977Z"
            },
            {
              "$type": "Document",
              "DocumentIndex": 6,
              "Title": "TaskTypeEnum.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_Common\\TaskEnum\\TaskTypeEnum.cs",
              "RelativeDocumentMoniker": "WIDESEA_Common\\TaskEnum\\TaskTypeEnum.cs",
              "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_Common\\TaskEnum\\TaskTypeEnum.cs",
              "RelativeToolTip": "WIDESEA_Common\\TaskEnum\\TaskTypeEnum.cs",
              "ViewState": "AgIAAIsAAAAAAAAAAAAewKAAAAAIAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-03-17T02:28:48.033Z"
            },
            {
              "$type": "Document",
              "DocumentIndex": 5,
              "Title": "TaskController.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_WMSServer\\Controllers\\TaskInfo\\TaskController.cs",
              "RelativeDocumentMoniker": "WIDESEA_WMSServer\\Controllers\\TaskInfo\\TaskController.cs",
              "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_WMSServer\\Controllers\\TaskInfo\\TaskController.cs",
              "RelativeToolTip": "WIDESEA_WMSServer\\Controllers\\TaskInfo\\TaskController.cs",
              "ViewState": "AgIAAIoAAAAAAAAAAAAhwJsAAAAIAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-03-13T02:00:31.089Z"
            },
            {
              "$type": "Document",
              "DocumentIndex": 0,
              "Title": "AutoOutboundTaskBackgroundService.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_WMSServer\\BackgroundServices\\AutoOutboundTaskBackgroundService.cs",
              "RelativeDocumentMoniker": "WIDESEA_WMSServer\\BackgroundServices\\AutoOutboundTaskBackgroundService.cs",
              "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_WMSServer\\BackgroundServices\\AutoOutboundTaskBackgroundService.cs",
              "RelativeToolTip": "WIDESEA_WMSServer\\BackgroundServices\\AutoOutboundTaskBackgroundService.cs",
              "ViewState": "AgIAABQAAAAAAAAAAADwvykAAABdAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-03-12T10:18:13.91Z",
              "EditorCaption": ""
            },
            {
              "$type": "Document",
              "DocumentIndex": 12,
              "Title": "appsettings.json",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_WMSServer\\appsettings.json",
              "RelativeDocumentMoniker": "WIDESEA_WMSServer\\appsettings.json",
              "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_WMSServer\\appsettings.json",
              "RelativeToolTip": "WIDESEA_WMSServer\\appsettings.json",
              "ViewState": "AgIAAAAAAAAAAAAAAAAAABgAAAA9AAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.001642|",
              "WhenOpened": "2026-03-12T10:06:27.509Z"
            },
            {
              "$type": "Document",
              "DocumentIndex": 13,
              "Title": "Program.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_WMSServer\\Program.cs",
              "RelativeDocumentMoniker": "WIDESEA_WMSServer\\Program.cs",
              "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_WMSServer\\Program.cs",
              "RelativeToolTip": "WIDESEA_WMSServer\\Program.cs",
              "ViewState": "AgIAABcAAAAAAAAAAAAswCYAAAAAAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-03-12T09:52:09.124Z"
            },
            {
              "$type": "Document",
              "DocumentIndex": 7,
              "Title": "StockStatusEmun.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_Common\\StockEnum\\StockStatusEmun.cs",
              "RelativeDocumentMoniker": "WIDESEA_Common\\StockEnum\\StockStatusEmun.cs",
              "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_Common\\StockEnum\\StockStatusEmun.cs",
              "RelativeToolTip": "WIDESEA_Common\\StockEnum\\StockStatusEmun.cs",
              "ViewState": "AgIAACMAAAAAAAAAAAAewDsAAAAIAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-03-12T09:08:34.784Z"
            },
            {
              "$type": "Document",
              "DocumentIndex": 11,
              "Title": "TaskStatusEnum.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_Common\\TaskEnum\\TaskStatusEnum.cs",
              "RelativeDocumentMoniker": "WIDESEA_Common\\TaskEnum\\TaskStatusEnum.cs",
              "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_Common\\TaskEnum\\TaskStatusEnum.cs",
              "RelativeToolTip": "WIDESEA_Common\\TaskEnum\\TaskStatusEnum.cs",
              "ViewState": "AgIAAJsAAAAAAAAAAAAAwKkAAAASAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-03-12T07:20:25.969Z"
            },
            {
              "$type": "Document",
              "DocumentIndex": 8,
              "Title": "LocationInfoService.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_BasicService\\LocationInfoService.cs",
              "RelativeDocumentMoniker": "WIDESEA_BasicService\\LocationInfoService.cs",
              "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_BasicService\\LocationInfoService.cs",
              "RelativeToolTip": "WIDESEA_BasicService\\LocationInfoService.cs",
              "ViewState": "AgIAAFcAAAAAAAAAAAAAwGgAAAARAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-03-12T02:05:14.224Z"
            },
            {
              "$type": "Document",
              "DocumentIndex": 14,
              "Title": "LocationInfoController.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_WMSServer\\Controllers\\Basic\\LocationInfoController.cs",
              "RelativeDocumentMoniker": "WIDESEA_WMSServer\\Controllers\\Basic\\LocationInfoController.cs",
              "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_WMSServer\\Controllers\\Basic\\LocationInfoController.cs",
              "RelativeToolTip": "WIDESEA_WMSServer\\Controllers\\Basic\\LocationInfoController.cs",
              "ViewState": "AgIAAEkAAAAAAAAAAAAIwF8AAAAsAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-03-12T01:53:01.837Z"
            },
            {
              "$type": "Document",
              "DocumentIndex": 2,
              "Title": "StockInfoService.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_StockService\\StockInfoService.cs",
              "RelativeDocumentMoniker": "WIDESEA_StockService\\StockInfoService.cs",
              "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_StockService\\StockInfoService.cs",
              "RelativeToolTip": "WIDESEA_StockService\\StockInfoService.cs",
              "ViewState": "AgIAADQAAAAAAAAAAAAIwDkAAAAuAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-03-11T09:16:37.34Z"
            },
            {
              "$type": "Document",
              "DocumentIndex": 1,
              "Title": "StockSerivce.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_StockService\\StockSerivce.cs",
              "RelativeDocumentMoniker": "WIDESEA_StockService\\StockSerivce.cs",
              "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_StockService\\StockSerivce.cs",
              "RelativeToolTip": "WIDESEA_StockService\\StockSerivce.cs",
              "ViewState": "AgIAAAAAAAAAAAAAAAAAABkAAAAWAAAAAAAAAA==",
              "ViewState": "AgIAAHcAAAAAAAAAAAAIwIoAAAASAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-03-17T02:09:23.282Z"
              "WhenOpened": "2026-03-26T01:10:20.079Z",
              "EditorCaption": ""
            },
            {
              "$type": "Document",
              "DocumentIndex": 3,
              "Title": "TaskService.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_TaskInfoService\\TaskService.cs",
              "RelativeDocumentMoniker": "WIDESEA_TaskInfoService\\TaskService.cs",
              "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_TaskInfoService\\TaskService.cs",
              "RelativeToolTip": "WIDESEA_TaskInfoService\\TaskService.cs",
              "ViewState": "AgIAACkAAAAAAAAAAAAywBAAAAAcAAAAAAAAAA==",
              "DocumentIndex": 2,
              "Title": "appsettings.json",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_WMSServer\\appsettings.json",
              "RelativeDocumentMoniker": "WIDESEA_WMSServer\\appsettings.json",
              "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_WMSServer\\appsettings.json",
              "RelativeToolTip": "WIDESEA_WMSServer\\appsettings.json",
              "ViewState": "AgIAAAAAAAAAAAAAAAAAABMAAAAfAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.001642|",
              "WhenOpened": "2026-03-25T08:40:34.998Z",
              "EditorCaption": ""
            },
            {
              "$type": "Document",
              "DocumentIndex": 1,
              "Title": "ServiceBase.cs",
              "DocumentMoniker": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_Core\\BaseServices\\ServiceBase.cs",
              "RelativeDocumentMoniker": "WIDESEA_Core\\BaseServices\\ServiceBase.cs",
              "ToolTip": "D:\\Git\\ShanMeiXinNengYuan\\Code\\WMS\\WIDESEA_WMSServer\\WIDESEA_Core\\BaseServices\\ServiceBase.cs",
              "RelativeToolTip": "WIDESEA_Core\\BaseServices\\ServiceBase.cs",
              "ViewState": "AgIAABoAAAAAAAAAAAAuwD8AAAARAAAAAAAAAA==",
              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
              "WhenOpened": "2026-02-06T06:34:59.734Z"
              "WhenOpened": "2026-03-25T06:03:58.013Z",
              "EditorCaption": ""
            }
          ]
        }
Code/WMS/WIDESEA_WMSServer/WIDESEA_Core/BaseServices/ServiceBase.cs
@@ -36,6 +36,51 @@
        public ISqlSugarClient Db => BaseDal.Db;
        protected async Task<WebResponseContent> ExecuteWithinTransactionAsync(Func<Task<WebResponseContent>> operation)
        {
            var db = Db as SqlSugarClient;
            if (db == null)
            {
                return WebResponseContent.Instance.Error("Database context does not support transactions");
            }
            var ownsTransaction = db.Ado.Transaction == null;
            try
            {
                if (ownsTransaction)
                {
                    db.BeginTran();
                }
                var result = await operation();
                if (result?.Status == true)
                {
                    if (ownsTransaction)
                    {
                        db.CommitTran();
                    }
                    return result;
                }
                if (ownsTransaction)
                {
                    db.RollbackTran();
                }
                return result ?? WebResponseContent.Instance.Error("Transaction failed");
            }
            catch
            {
                if (ownsTransaction)
                {
                    db.RollbackTran();
                }
                throw;
            }
        }
        private PropertyInfo[] _propertyInfo { get; set; } = null;
        public PropertyInfo[] TProperties
        {
Code/WMS/WIDESEA_WMSServer/WIDESEA_StockService/StockSerivce.cs
@@ -1,4 +1,5 @@
using WIDESEA_Common.StockEnum;
using SqlSugar;
using WIDESEA_Common.StockEnum;
using WIDESEA_Core;
using WIDESEA_DTO.Stock;
using WIDESEA_IStockService;
@@ -51,6 +52,51 @@
        }
        /// <summary>
        /// åœ¨äº‹åŠ¡ä¸­æ‰§è¡Œæ“ä½œ
        /// </summary>
        private async Task<WebResponseContent> ExecuteWithinTransactionAsync(Func<Task<WebResponseContent>> operation)
        {
            var db = StockInfoService.Repository.Db as SqlSugarClient;
            if (db == null)
            {
                return WebResponseContent.Instance.Error("Database context does not support transactions");
            }
            var ownsTransaction = db.Ado.Transaction == null;
            try
            {
                if (ownsTransaction)
                {
                    db.BeginTran();
                }
                var result = await operation();
                if (result?.Status == true)
                {
                    if (ownsTransaction)
                    {
                        db.CommitTran();
                    }
                    return result;
                }
                if (ownsTransaction)
                {
                    db.RollbackTran();
                }
                return result ?? WebResponseContent.Instance.Error("Transaction failed");
            }
            catch
            {
                if (ownsTransaction)
                {
                    db.RollbackTran();
                }
                throw;
            }
        }
        /// <summary>
        /// ç»„盘
        /// </summary>
        public async Task<WebResponseContent> GroupPalletAsync(StockDTO stock)
@@ -72,28 +118,30 @@
                Status = StockStatusEmun.组盘暂存.GetHashCode(),
            }).ToList();
            var existingStock = StockInfoService.Repository.QueryFirst(s => s.PalletCode == stock.TargetPalletNo);
            var result = false;
            if (existingStock != null)
            return await ExecuteWithinTransactionAsync(async () =>
            {
                details.ForEach(d => d.StockId = existingStock.Id);
                result = await StockInfoDetailService.Repository.AddDataAsync(details) > 0;
                if (result) return content.OK("组盘成功");
                return content.Error("组盘失败");
            }
                var existingStock = StockInfoService.Repository.QueryFirst(s => s.PalletCode == stock.TargetPalletNo);
                var result = false;
                if (existingStock != null)
                {
                    details.ForEach(d => d.StockId = existingStock.Id);
                    result = await StockInfoDetailService.Repository.AddDataAsync(details) > 0;
                    return result ? content.OK("组盘成功") : content.Error("组盘失败");
                }
            var entity = new Dt_StockInfo
            {
                PalletCode = stock.TargetPalletNo,
                WarehouseId = 1,
                StockStatus = 1,
                Creater = "system",
                Details = details
            };
                var entity = new Dt_StockInfo
                {
                    PalletCode = stock.TargetPalletNo,
                    //WarehouseId = stock.WarehouseId > 0 ? stock.WarehouseId : 1,
                    WarehouseId = 1,
                    StockStatus = StockStatusEmun.组盘暂存.GetHashCode(),
                    Creater = "system",
                    Details = details
                };
            result = StockInfoService.Repository.AddData(entity, x => x.Details);
            if (result) return content.OK("组盘成功");
            return content.Error("组盘失败");
                result = StockInfoService.Repository.AddData(entity, x => x.Details);
                return result ? content.OK("组盘成功") : content.Error("组盘失败");
            });
        }
        /// <summary>
@@ -101,7 +149,6 @@
        /// </summary>
        public async Task<WebResponseContent> ChangePalletAsync(StockDTO stock)
        {
            WebResponseContent content = new WebResponseContent();
            if (stock == null ||
                string.IsNullOrWhiteSpace(stock.TargetPalletNo) ||
@@ -111,44 +158,47 @@
                return content.Error("源托盘号与目标托盘号相同");
            }
            var sourceStock = await StockInfoService.Repository.QueryDataNavFirstAsync(s => s.PalletCode == stock.SourcePalletNo);
            if (sourceStock == null) return content.Error("源托盘不存在");
            var targetStock = StockInfoService.Repository.QueryFirst(s => s.PalletCode == stock.TargetPalletNo);
            if (targetStock == null)
            return await ExecuteWithinTransactionAsync(async () =>
            {
                var newStock = new Dt_StockInfo
                var sourceStock = await StockInfoService.Repository.QueryDataNavFirstAsync(s => s.PalletCode == stock.SourcePalletNo);
                if (sourceStock == null) return content.Error("源托盘不存在");
                var targetStock = StockInfoService.Repository.QueryFirst(s => s.PalletCode == stock.TargetPalletNo);
                if (targetStock == null)
                {
                    PalletCode = stock.TargetPalletNo,
                    WarehouseId = sourceStock.WarehouseId,
                    StockStatus = StockStatusEmun.组盘暂存.GetHashCode(),
                    Creater = "system",
                };
                    var newStock = new Dt_StockInfo
                    {
                        PalletCode = stock.TargetPalletNo,
                        WarehouseId = sourceStock.WarehouseId,
                        StockStatus = StockStatusEmun.组盘暂存.GetHashCode(),
                        Creater = "system",
                    };
                var newId = StockInfoService.Repository.AddData(newStock);
                if (newId <= 0) return content.Error("换盘失败");
                    var newId = StockInfoService.Repository.AddData(newStock);
                    if (newId <= 0) return content.Error("换盘失败");
                targetStock = newStock;
                targetStock.Id = newId;
            }
                    targetStock = newStock;
                    targetStock.Id = newId;
                }
            var serialNumbers = stock.Details.Select(d => d.Channel).Distinct().ToList();
            if (!serialNumbers.Any()) return content.Error("未找到有效的序列号");
                var serialNumbers = stock.Details.Select(d => d.Channel).Distinct().ToList();
                if (!serialNumbers.Any()) return content.Error("未找到有效的序列号");
            var detailEntities = StockInfoDetailService.Repository.QueryData(
                d => d.StockId == sourceStock.Id && serialNumbers.Contains(d.InboundOrderRowNo));
            if (!detailEntities.Any()) return content.Error("未找到有效的库存明细");
                var detailEntities = StockInfoDetailService.Repository.QueryData(
                    d => d.StockId == sourceStock.Id && serialNumbers.Contains(d.InboundOrderRowNo));
                if (!detailEntities.Any()) return content.Error("未找到有效的库存明细");
            if (await StockInfoDetail_HtyService.Repository.AddDataAsync(CreateDetailHistory(detailEntities, "换盘")) <= 0)
                return content.Error("换盘历史记录保存失败");
                if (await StockInfoDetail_HtyService.Repository.AddDataAsync(CreateDetailHistory(detailEntities, "换盘")) <= 0)
                    return content.Error("换盘历史记录保存失败");
            if (await StockInfo_HtyService.Repository.AddDataAsync(CreateStockHistory(new[] { sourceStock, targetStock }, "换盘")) <= 0)
                return content.Error("换盘历史记录保存失败");
                if (await StockInfo_HtyService.Repository.AddDataAsync(CreateStockHistory(new[] { sourceStock, targetStock }, "换盘")) <= 0)
                    return content.Error("换盘历史记录保存失败");
            detailEntities.ForEach(d => d.StockId = targetStock.Id);
            var result = await StockInfoDetailService.Repository.UpdateDataAsync(detailEntities);
            if (!result) return content.Error("换盘失败");
            return content.OK("换盘成功");
                detailEntities.ForEach(d => d.StockId = targetStock.Id);
                var result = await StockInfoDetailService.Repository.UpdateDataAsync(detailEntities);
                if (!result) return content.Error("换盘失败");
                return content.OK("换盘成功");
            });
        }
        /// <summary>
@@ -160,31 +210,34 @@
            if (stock == null || string.IsNullOrWhiteSpace(stock.SourcePalletNo))
                return content.Error("源托盘号不能为空");
            var sourceStock = StockInfoService.Repository.QueryFirst(s => s.PalletCode == stock.SourcePalletNo);
            if (sourceStock == null) return content.Error("源托盘不存在");
            var serialNumbers = stock.Details.Select(d => d.CellBarcode).Distinct().ToList();
            if (!serialNumbers.Any())
            return await ExecuteWithinTransactionAsync(async () =>
            {
                serialNumbers = sourceStock.Details
                                            .Where(x => stock.Details.Any(d => d.Channel == x.InboundOrderRowNo))
                                            .Select(x => x.SerialNumber)
                                            .ToList();
            }
                var sourceStock = StockInfoService.Repository.QueryFirst(s => s.PalletCode == stock.SourcePalletNo);
                if (sourceStock == null) return content.Error("源托盘不存在");
            var detailEntities = StockInfoDetailService.Repository.QueryData(
                d => d.StockId == sourceStock.Id && serialNumbers.Contains(d.SerialNumber));
            if (!detailEntities.Any()) return content.Error("未找到有效的库存明细");
                var serialNumbers = stock.Details.Select(d => d.CellBarcode).Distinct().ToList();
                if (!serialNumbers.Any())
                {
                    serialNumbers = sourceStock.Details
                                                .Where(x => stock.Details.Any(d => d.Channel == x.InboundOrderRowNo))
                                                .Select(x => x.SerialNumber)
                                                .ToList();
                }
            if (await StockInfoDetail_HtyService.Repository.AddDataAsync(CreateDetailHistory(detailEntities, "拆盘")) <= 0)
                return content.Error("拆盘历史记录保存失败");
                var detailEntities = StockInfoDetailService.Repository.QueryData(
                    d => d.StockId == sourceStock.Id && serialNumbers.Contains(d.SerialNumber));
                if (!detailEntities.Any()) return content.Error("未找到有效的库存明细");
            if (await StockInfo_HtyService.Repository.AddDataAsync(CreateStockHistory(new[] { sourceStock }, "拆盘")) <= 0)
                return content.Error("拆盘历史记录保存失败");
                if (await StockInfoDetail_HtyService.Repository.AddDataAsync(CreateDetailHistory(detailEntities, "拆盘")) <= 0)
                    return content.Error("拆盘历史记录保存失败");
            var result = await StockInfoDetailService.Repository.DeleteDataAsync(detailEntities);
            if (!result) return content.Error("拆盘失败");
            return content.OK("拆盘成功");
                if (await StockInfo_HtyService.Repository.AddDataAsync(CreateStockHistory(new[] { sourceStock }, "拆盘")) <= 0)
                    return content.Error("拆盘历史记录保存失败");
                var result = await StockInfoDetailService.Repository.DeleteDataAsync(detailEntities);
                if (!result) return content.Error("拆盘失败");
                return content.OK("拆盘成功");
            });
        }
        /// <summary>
Code/WMS/WIDESEA_WMSServer/WIDESEA_SystemService/Sys_MenuService.cs
@@ -309,6 +309,30 @@
            }
        }
        public object x_GetTreeItem(int menuId)
        {
            var sysMenu = BaseDal.QueryData(x => x.MenuId == menuId)
                .Select(
                p => new
                {
                    p.MenuId,
                    p.ParentId,
                    p.MenuName,
                    p.Url,
                    p.Auth,
                    p.OrderNo,
                    p.Icon,
                    p.Enable,
                    // 2022.03.26增移动端加菜单类型
                    MenuType = p.MenuType,
                    p.CreateDate,
                    p.Creater,
                    p.TableName,
                    p.ModifyDate
                }).FirstOrDefault();
            return sysMenu;
        }
        /// <summary>
        /// ç¼–辑菜单时,获取菜单信息
        /// </summary>
@@ -316,7 +340,7 @@
        /// <returns></returns>
        public object GetTreeItem(int menuId)
        {
            return GetTreeItem(menuId);
            return x_GetTreeItem(menuId);
        }
        /// <summary>
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService.cs
@@ -67,7 +67,7 @@
        {
            try
            {
                WebResponseContent content = await GetTasksByPalletCodeAsync(taskDto.PalletCode);
                WebResponseContent content = await GetTaskByPalletCodeAsync(taskDto.PalletCode);
                if (content.Status)
                {
                    return content;
@@ -172,18 +172,23 @@
                var locationInfo = await _locationInfoService.GetLocationInfo(task.Roadway);
                if (locationInfo == null) return WebResponseContent.Instance.Error("未找到对应的货位");
                locationInfo.LocationStatus = LocationStatusEnum.FreeLock.GetHashCode();
                task.CurrentAddress = task.SourceAddress;
                task.NextAddress = locationInfo.LocationCode;
                task.TargetAddress = locationInfo.LocationCode;
                task.TaskStatus = TaskInStatusEnum.Line_InFinish.GetHashCode();
                return await ExecuteWithinTransactionAsync(async () =>
                {
                    locationInfo.LocationStatus = LocationStatusEnum.FreeLock.GetHashCode();
                    task.CurrentAddress = task.SourceAddress;
                    task.NextAddress = locationInfo.LocationCode;
                    task.TargetAddress = locationInfo.LocationCode;
                    task.TaskStatus = TaskInStatusEnum.Line_InFinish.GetHashCode();
                var updateResult = await BaseDal.UpdateDataAsync(task);
                var locationResult = await _locationInfoService.UpdateLocationInfoAsync(locationInfo);
                    var updateTaskResult = await BaseDal.UpdateDataAsync(task);
                    var updateLocationResult = await _locationInfoService.UpdateLocationInfoAsync(locationInfo);
                    if (!updateTaskResult || !updateLocationResult)
                    {
                        return WebResponseContent.Instance.Error("任务更新失败");
                    }
                return WebResponseContent.Instance.OK(
                    updateResult && locationResult ? "任务更新成功" : "任务更新失败",
                    locationInfo.LocationCode);
                    return WebResponseContent.Instance.OK("任务更新成功", locationInfo.LocationCode);
                });
            }
            catch (Exception ex)
            {
@@ -205,23 +210,28 @@
                if (location == null) return WebResponseContent.Instance.Error("未找到对应的货位");
                var stockInfo = await _stockInfoService.GetStockInfoAsync(taskDto.PalletCode);
                stockInfo.LocationCode = location.LocationCode;
                stockInfo.LocationId = location.Id;
                stockInfo.OutboundDate = task.Roadway switch
                if (stockInfo == null) return WebResponseContent.Instance.Error("未找到对应库存信息");
                return await ExecuteWithinTransactionAsync(async () =>
                {
                    var r when r.Contains("GW") => DateTime.Now.AddHours(2),
                    var r when r.Contains("CW") => DateTime.Now.AddHours(1),
                    _ => DateTime.Now
                };
                stockInfo.StockStatus = StockStatusEmun.入库完成.GetHashCode();
                    stockInfo.LocationCode = location.LocationCode;
                    stockInfo.LocationId = location.Id;
                    stockInfo.OutboundDate = task.Roadway switch
                    {
                        var r when r.Contains("GW") => DateTime.Now.AddHours(2),
                        var r when r.Contains("CW") => DateTime.Now.AddHours(1),
                        _ => DateTime.Now
                    };
                    stockInfo.StockStatus = StockStatusEmun.入库完成.GetHashCode();
                location.LocationStatus = LocationStatusEnum.InStock.GetHashCode();
                    location.LocationStatus = LocationStatusEnum.InStock.GetHashCode();
                var updateLocationResult = await _locationInfoService.UpdateLocationInfoAsync(location);
                var updateStockResult = await _stockInfoService.UpdateStockAsync(stockInfo);
                if (!updateLocationResult || !updateStockResult)
                    return WebResponseContent.Instance.Error("任务完成失败");
                return await CompleteTaskAsync(task);
                    var updateLocationResult = await _locationInfoService.UpdateLocationInfoAsync(location);
                    var updateStockResult = await _stockInfoService.UpdateStockAsync(stockInfo);
                    if (!updateLocationResult || !updateStockResult)
                        return WebResponseContent.Instance.Error("任务完成失败");
                    return await CompleteTaskAsync(task);
                });
            }
            catch (Exception ex)
            {
@@ -242,18 +252,23 @@
                var location = await _locationInfoService.GetLocationInfo(task.Roadway, task.SourceAddress);
                if (location == null) return WebResponseContent.Instance.Error("未找到对应的货位");
                var stockInfo = await _stockInfoService.GetStockInfoAsync(taskDto.PalletCode);
                stockInfo.LocationId = 0;
                stockInfo.LocationCode = null;
                stockInfo.OutboundDate = DateTime.Now;
                var stockInfo = await _stockInfoService.GetStockInfoAsync(taskDto.PalletCode);
                if (stockInfo == null) return WebResponseContent.Instance.Error("未找到对应库存信息");
                location.LocationStatus = LocationStatusEnum.Free.GetHashCode();
                return await ExecuteWithinTransactionAsync(async () =>
                {
                    stockInfo.LocationId = 0;
                    stockInfo.LocationCode = null;
                    stockInfo.OutboundDate = DateTime.Now;
                var updateLocationResult = await _locationInfoService.UpdateLocationInfoAsync(location);
                var updateStockResult = await _stockInfoService.UpdateStockAsync(stockInfo);
                if (!updateLocationResult || !updateStockResult)
                    return WebResponseContent.Instance.Error("任务完成失败");
                return await CompleteTaskAsync(task);
                    location.LocationStatus = LocationStatusEnum.Free.GetHashCode();
                    var updateLocationResult = await _locationInfoService.UpdateLocationInfoAsync(location);
                    var updateStockResult = await _stockInfoService.UpdateStockAsync(stockInfo);
                    if (!updateLocationResult || !updateStockResult)
                        return WebResponseContent.Instance.Error("任务完成失败");
                    return await CompleteTaskAsync(task);
                });
            }
            catch (Exception ex)
            {
@@ -282,21 +297,24 @@
                var stockInfo = await _stockInfoService.GetStockInfoAsync(taskDto.PalletCode);
                if (stockInfo == null) return WebResponseContent.Instance.Error("未找到对应库存信息");
                stockInfo.LocationCode = targetLocation.LocationCode;
                stockInfo.LocationId = targetLocation.Id;
                stockInfo.StockStatus = StockStatusEmun.入库完成.GetHashCode();
                return await ExecuteWithinTransactionAsync(async () =>
                {
                    stockInfo.LocationCode = targetLocation.LocationCode;
                    stockInfo.LocationId = targetLocation.Id;
                    stockInfo.StockStatus = StockStatusEmun.入库完成.GetHashCode();
                sourceLocation.LocationStatus = LocationStatusEnum.Free.GetHashCode();
                targetLocation.LocationStatus = LocationStatusEnum.InStock.GetHashCode();
                    sourceLocation.LocationStatus = LocationStatusEnum.Free.GetHashCode();
                    targetLocation.LocationStatus = LocationStatusEnum.InStock.GetHashCode();
                var updateSourceResult = await _locationInfoService.UpdateLocationInfoAsync(sourceLocation);
                var updateTargetResult = await _locationInfoService.UpdateLocationInfoAsync(targetLocation);
                var updateStockResult = await _stockInfoService.UpdateStockAsync(stockInfo);
                    var updateSourceResult = await _locationInfoService.UpdateLocationInfoAsync(sourceLocation);
                    var updateTargetResult = await _locationInfoService.UpdateLocationInfoAsync(targetLocation);
                    var updateStockResult = await _stockInfoService.UpdateStockAsync(stockInfo);
                if (!updateSourceResult || !updateTargetResult || !updateStockResult)
                    return WebResponseContent.Instance.Error("移库任务完成失败");
                    if (!updateSourceResult || !updateTargetResult || !updateStockResult)
                        return WebResponseContent.Instance.Error("移库任务完成失败");
                return await CompleteTaskAsync(task);
                    return await CompleteTaskAsync(task);
                });
            }
            catch (Exception ex)
            {
@@ -313,7 +331,7 @@
        {
            try
            {
                WebResponseContent content = await GetTasksByPalletCodeAsync(taskDto.PalletCode);
                WebResponseContent content = await GetTaskByPalletCodeAsync(taskDto.PalletCode);
                if (content.Status)
                {
                    return content;
@@ -363,8 +381,10 @@
                };
                var taskDtos = task.Adapt<WMSTaskDTO>();
                BaseDal.AddData(task);
                return WebResponseContent.Instance.OK("查询成功", taskDtos);
                var addResult = await BaseDal.AddDataAsync(task);
                if (!addResult)
                    return WebResponseContent.Instance.Error("任务创建失败");
                return WebResponseContent.Instance.OK("任务创建成功", taskDtos);
            }
            catch (Exception ex)
            {
@@ -403,15 +423,15 @@
        /// </summary>
        /// <param name="palletCode"></param>
        /// <returns></returns>
        private async Task<WebResponseContent> GetTasksByPalletCodeAsync(string palletCode)
        private async Task<WebResponseContent> GetTaskByPalletCodeAsync(string palletCode)
        {
            try
            {
                var tasks = await BaseDal.QueryFirstAsync(s => s.PalletCode == palletCode);
                if (tasks == null)
                var task = await BaseDal.QueryFirstAsync(s => s.PalletCode == palletCode);
                if (task == null)
                    return WebResponseContent.Instance.Error("未找到对应的任务");
                var taskDtos = _mapper.Map<List<WMSTaskDTO>>(tasks);
                return WebResponseContent.Instance.OK("查询成功", taskDtos);
                var taskDto = _mapper.Map<WMSTaskDTO>(task);
                return WebResponseContent.Instance.OK("查询成功", taskDto);
            }
            catch (Exception ex)
            {
@@ -549,45 +569,54 @@
                    taskList.Add(task);
                }
                var addResult = await BaseDal.AddDataAsync(taskList) > 0;
                if (!addResult)
                var transactionResult = await ExecuteWithinTransactionAsync(async () =>
                {
                    return WebResponseContent.Instance.Error($"批量创建任务失败,共 {taskList.Count} ä¸ªä»»åŠ¡");
                }
                // ä»»åŠ¡åˆ›å»ºæˆåŠŸåŽï¼ŒåŒæ­¥é”å®šåº“å­˜å’Œè´§ä½çŠ¶æ€ï¼Œé¿å…é‡å¤åˆ†é…
                var stocksToUpdate = stocksToProcess
                    .Select(s =>
                    var addResult = await BaseDal.AddDataAsync(taskList) > 0;
                    if (!addResult)
                    {
                        s.StockStatus = StockStatusEmun.出库锁定.GetHashCode();
                        return s;
                    })
                    .ToList();
                var updateStockResult = await _stockInfoService.Repository.UpdateDataAsync(stocksToUpdate);
                if (!updateStockResult)
                {
                    return WebResponseContent.Instance.Error($"任务创建成功,但库存状态更新失败,共 {stocksToUpdate.Count} æ¡");
                }
                var locationsToUpdate = stocksToProcess
                    .Where(s => s.LocationDetails != null)
                    .GroupBy(s => s.LocationDetails.Id)
                    .Select(g =>
                    {
                        var location = g.First().LocationDetails;
                        location.LocationStatus = LocationStatusEnum.InStockLock.GetHashCode();
                        return location;
                    })
                    .ToList();
                if (locationsToUpdate.Any())
                {
                    var updateLocationResult = await _locationInfoService.Repository.UpdateDataAsync(locationsToUpdate);
                    if (!updateLocationResult)
                    {
                        return WebResponseContent.Instance.Error($"任务创建成功,但货位状态更新失败,共 {locationsToUpdate.Count} æ¡");
                        return WebResponseContent.Instance.Error($"批量创建任务失败,共 {taskList.Count} ä¸ªä»»åŠ¡");
                    }
                    // ä»»åŠ¡åˆ›å»ºæˆåŠŸåŽï¼ŒåŒæ­¥é”å®šåº“å­˜å’Œè´§ä½çŠ¶æ€ï¼Œé¿å…é‡å¤åˆ†é…
                    var stocksToUpdate = stocksToProcess
                        .Select(s =>
                        {
                            s.StockStatus = StockStatusEmun.出库锁定.GetHashCode();
                            return s;
                        })
                        .ToList();
                    var updateStockResult = await _stockInfoService.Repository.UpdateDataAsync(stocksToUpdate);
                    if (!updateStockResult)
                    {
                        return WebResponseContent.Instance.Error($"任务创建成功,但库存状态更新失败,共 {stocksToUpdate.Count} æ¡");
                    }
                    var locationsToUpdate = stocksToProcess
                        .Where(s => s.LocationDetails != null)
                        .GroupBy(s => s.LocationDetails.Id)
                        .Select(g =>
                        {
                            var location = g.First().LocationDetails;
                            location.LocationStatus = LocationStatusEnum.InStockLock.GetHashCode();
                            return location;
                        })
                        .ToList();
                    if (locationsToUpdate.Any())
                    {
                        var updateLocationResult = await _locationInfoService.Repository.UpdateDataAsync(locationsToUpdate);
                        if (!updateLocationResult)
                        {
                            return WebResponseContent.Instance.Error($"任务创建成功,但货位状态更新失败,共 {locationsToUpdate.Count} æ¡");
                        }
                    }
                    return WebResponseContent.Instance.OK();
                });
                if (!transactionResult.Status)
                {
                    return transactionResult;
                }
                // 6. é€šçŸ¥ WCS(异步,不影响主流程)
@@ -752,30 +781,26 @@
            try
            {
                var stockInfo = await _stockInfoService.GetStockInfoAsync(input.PalletCode, input.LocationCode);
                int locationStatus;
                if (stockInfo == null)
                {
                    var location = await _locationInfoService.GetLocationInfoAsync(input.LocationCode);
                    OutputDto outPutDto = new OutputDto()
                    {
                        LocationCode = input.LocationCode,
                        PalletCode = input.PalletCode,
                        IsNormalProcedure = 1,
                        LocationStatus = location.LocationStatus
                    };
                    content.OK(data: outPutDto);
                    locationStatus = location?.LocationStatus ?? 0;
                }
                else
                {
                    OutputDto outPutDto = new OutputDto()
                    {
                        LocationCode = input.LocationCode,
                        PalletCode = input.PalletCode,
                        IsNormalProcedure = 1,
                        LocationStatus = stockInfo.LocationDetails.LocationStatus
                    };
                    content.OK(data: outPutDto);
                    locationStatus = stockInfo.LocationDetails?.LocationStatus ?? 0;
                }
                OutputDto outPutDto = new OutputDto()
                {
                    LocationCode = input.LocationCode,
                    PalletCode = input.PalletCode,
                    IsNormalProcedure = 1,
                    LocationStatus = locationStatus
                };
                return content.OK(data: outPutDto);
            }
            catch (Exception ex)
            {
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Program.cs
@@ -1,5 +1,3 @@
using System.Reflection;
using System.Text;
using Autofac;
using Autofac.Core;
using Autofac.Extensions.DependencyInjection;
@@ -12,6 +10,9 @@
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
using Serilog;
using Serilog.Formatting.Json;
using System.Reflection;
using System.Text;
using WIDESEA_Core;
using WIDESEA_Core.Authorization;
using WIDESEA_Core.BaseServices;
@@ -42,13 +43,27 @@
    loggerConfiguration
        .ReadFrom.Configuration(context.Configuration)
        .ReadFrom.Services(services)
        .Enrich.FromLogContext()
        .WriteTo.Console()
        //.Enrich.FromLogContext()
        .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
        .WriteTo.File(
            "logs/serilog-.log.txt",
            //new JsonFormatter(renderMessage: true),
            "logs/serilog-.log",
            outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}",
            rollingInterval: RollingInterval.Day,
            retainedFileCountLimit: 30,
            shared: true);
            // æ¯ä¸ªæ—¥å¿—文件最大大小(字节),此处设置为10MB
            fileSizeLimitBytes: 10 * 1024 * 1024,
            shared: true
            )
         // 6. å¯é€‰ï¼šè¾“出到Seq日志服务器(结构化日志服务器)
         // éœ€è¦å®‰è£… Serilog.Sinks.Seq NuGet包,并确保Seq服务在 http://localhost:5341 è¿è¡Œ
         // å¦‚不需要Seq日志,注释掉下方代码即可
         .WriteTo.Seq(
             serverUrl: "http://localhost:5341",
             apiKey: "CWVa8UWQ9CdUp9GWXCPL", // å¦‚Seq需要ApiKey则配置真实密钥
             batchPostingLimit: 1000, // æ‰¹é‡å‘送数量
             period: TimeSpan.FromSeconds(2) // å‘送间隔
         );
});
builder.ConfigureApplication();
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/WIDESEA_WMSServer.csproj
@@ -50,7 +50,8 @@
      <PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
      <PackageReference Include="Serilog.Settings.Configuration" Version="8.0.0" />
      <PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
      <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
      <PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
      <PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0" />
      <PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.7.3" />
    </ItemGroup>
    
@@ -88,4 +89,8 @@
        <CopyToOutputDirectory>Always</CopyToOutputDirectory>
      </Content>
    </ItemGroup>
    <ItemGroup>
      <Folder Include="logs\" />
    </ItemGroup>
</Project>
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/appsettings.json
@@ -1,10 +1,31 @@
{
  "urls": "http://*:9291", //web服务端口,如果用IIS部署,把这个去掉
  "Serilog": {
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Information",
        "Microsoft.AspNetCore": "Warning",
        "Microsoft.AspNetCore.Routing": "Warning",
        "Microsoft.AspNetCore.Mvc": "Warning",
        "Microsoft.AspNetCore.Mvc.Infrastructure": "Warning",
        "Microsoft.AspNetCore.Mvc.Filters": "Warning",
        "Microsoft.AspNetCore.Mvc.ModelBinding": "Warning",
        "Microsoft.EntityFrameworkCore": "Warning"
      }
    }
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.AspNetCore.DataProtection": "Warning"
      "Microsoft.AspNetCore.DataProtection": "Warning",
      "Microsoft.AspNetCore.Routing": "Warning",
      "Microsoft.AspNetCore.Mvc": "Warning",
      "Microsoft.AspNetCore.Mvc.Infrastructure": "Warning",
      "Microsoft.AspNetCore.Mvc.Filters": "Warning",
      "Microsoft.AspNetCore.Mvc.ModelBinding": "Warning"
    }
  },
  "dics": "inOrderType,outOrderType,inboundState,createType,enableEnum,enableStatusEnum,locationStatusEnum,locationTypeEnum,taskTypeEnum,taskStatusEnum,outboundStatusEnum,orderDetailStatusEnum,stockStatusEmun,stockChangeType,outStockStatus,receiveOrderTypeEnum,authorityScope,authorityScopes,locationChangeType,warehouses,suppliers,taskType,receiveStatus,purchaseType,purchaseOrderStatus,printStatus",
@@ -25,6 +46,14 @@
    // æ³¨æ„ï¼Œhttp://127.0.0.1:1818 å’Œ http://localhost:1818 æ˜¯ä¸ä¸€æ ·çš„
    "IPs": "http://127.0.0.1:8080,http://localhost:8080,http://127.0.0.1:8081,http://localhost:8081"
  },
  "LocalLogConfig": {
    "LogLevel": "DEBUG", //日志级别 DEBUG,INFO,WARN,ERROR,FATAL
    "LogFileSize": 10, //单个日志文件大小,单位MB
    "LogFileCount": 300, //日志文件数量
    "EnableConsoleOutput": false, //是否输出到控制台
    "EnableFloderByLevel": true //是否按日志级别生成不同的文件夹
  },
  "LogAopEnable": false,
  "PrintSql": false, //打印SQL语句
  "ApiName": "WIDESEA",