已添加7个文件
已修改25个文件
4465 ■■■■■ 文件已修改
Code/.omc/state/last-tool-error.json 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/.omc/state/subagent-tracking.json 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Client/.omc/project-memory.json 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Client/src/api/http.js 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Client/src/views/taskinfo/robotTask.vue 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_ITaskInfoRepository/IRobotStateRepository.cs 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Model/Models/RobotState/Dt_RobotState.cs 163 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Model/Models/TaskInfo/Dt_RobotTask.cs 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_QuartzJob/DeviceBase/DeviceCommand.cs 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Server/HostedService/ApiRouteCacheWarmupHostedService.cs 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Server/Properties/PublishProfiles/FolderProfile.pubxml 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Server/dotnet-tools.json 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_TaskInfoRepository/RobotStateRepository.cs 174 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_TaskInfoService/RobotTaskService.cs 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/CommonConveyorLineNewJob.cs 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotJob.cs 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotMessageHandler.cs 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotStateManager.cs 140 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotTaskProcessor.cs 69 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotPrefixCommandHandler.cs 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotSimpleCommandHandler.cs 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotWorkflowOrchestrator.cs 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSClient/src/views/Home.vue 409 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSClient/src/views/Index.vue 159 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSClient/src/views/stock/stockChat.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_Core/Extensions/WebSocketSetup.cs 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService_AGV.cs 1347 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Dashboard/DashboardController.cs 645 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Properties/PublishProfiles/FolderProfile.pubxml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/docs/superpowers/plans/2026-04-19-robot-state-redis-to-db-plan.md 804 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/docs/superpowers/specs/2026-04-19-robot-state-redis-to-db-design.md 195 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目资料/设备协议/上位系统对接/陕西顷刻能源科技MES系统对接接口.pdf 补丁 | 查看 | 原始文档 | blame | 历史
Code/.omc/state/last-tool-error.json
@@ -1,7 +1,7 @@
{
  "tool_name": "Read",
  "tool_input_preview": "{\"file_path\":\"D:\\\\Git\\\\ShanMeiXinNengYuan\\\\Code\\\\WMS\\\\WIDESEA_WMSServer\\\\WIDESEA_TaskInfoService\\\\WCS\\\\TaskService.cs\"}",
  "error": "File does not exist. Note: your current working directory is D:\\Git\\ShanMeiXinNengYuan\\Code.",
  "timestamp": "2026-04-18T07:45:29.125Z",
  "tool_name": "Bash",
  "tool_input_preview": "{\"command\":\"cd D:\\\\Git\\\\ShanMeiXinNengYuan\\\\Code\\\\WCS\\\\WIDESEAWCS_Client && npm run build 2>&1 | head -30\",\"timeout\":120000}",
  "error": "Exit code 1\n/usr/bin/bash: line 1: cd: D:GitShanMeiXinNengYuanCodeWCSWIDESEAWCS_Client: No such file or directory",
  "timestamp": "2026-04-19T07:15:17.022Z",
  "retry_count": 1
}
Code/.omc/state/subagent-tracking.json
@@ -772,5 +772,5 @@
  "total_spawned": 83,
  "total_completed": 83,
  "total_failed": 0,
  "last_updated": "2026-04-18T14:04:59.113Z"
  "last_updated": "2026-04-18T14:41:25.052Z"
}
Code/WCS/WIDESEAWCS_Client/.omc/project-memory.json
@@ -177,6 +177,19 @@
      ]
    }
  },
  "hotPaths": [],
  "hotPaths": [
    {
      "path": "src\\views\\system\\Sys_User.vue",
      "accessCount": 1,
      "lastAccessed": 1776583114779,
      "type": "file"
    },
    {
      "path": "src\\views\\taskinfo\\robotTask.vue",
      "accessCount": 1,
      "lastAccessed": 1776583136953,
      "type": "file"
    }
  ],
  "userDirectives": []
}
Code/WCS/WIDESEAWCS_Client/src/api/http.js
@@ -15,11 +15,11 @@
    axios.defaults.baseURL = 'http://127.0.0.1:9292/';
}
else if (process.env.NODE_ENV == 'debug') {
    axios.defaults.baseURL = 'http://127.0.0.1:8098/';
    axios.defaults.baseURL = 'http://127.0.0.1:9292/';
}
else if (process.env.NODE_ENV == 'production') {
    axios.defaults.baseURL = 'http://115.159.85.185:9292/';
    axios.defaults.baseURL = 'http://192.168.60.30:9292/';
}
if (!axios.defaults.baseURL.endsWith('/')) {
    axios.defaults.baseURL+="/";
Code/WCS/WIDESEAWCS_Client/src/views/taskinfo/robotTask.vue
@@ -28,9 +28,67 @@
      sortName: "createDate",       // é»˜è®¤æŽ’序字段
    });
    // ç¼–辑表单字段(初始为空,根据实际编辑需求配置)
    const editFormFields = ref({});
    const editFormOptions = ref([]);
    // ç¼–辑表单字段
    const editFormFields = ref({
      robotTaskNum: "",
      robotRoadway: "",
      robotTaskType: "",
      robotTaskState: "",
      robotTaskTotalNum: "",
      robotSourceAddress: "",
      robotTargetAddress: "",
      robotSourceAddressLineCode: "",
      robotTargetAddressLineCode: "",
      robotSourceAddressPalletCode: "",
      robotTargetAddressPalletCode: "",
      robotGrade: 2,
    });
    // ç¼–辑表单配置
    const editFormOptions = ref([
      [
        { title: "任务编号", field: "robotTaskNum", type: "int", required: true },
        { title: "巷道", field: "robotRoadway", type: "string", required: true },
        { title: "任务总数", field: "robotTaskTotalNum", type: "int", required: true },
        {
          title: "优先级",
          field: "robotGrade",
          type: "select",
          data: [
            { key: 1, value: "低" },
            { key: 2, value: "普通" },
            { key: 3, value: "高" },
            { key: 4, value: "紧急" },
          ],
        },
      ],
      [
        {
          title: "任务类型",
          field: "robotTaskType",
          type: "select",
          dataKey: "taskType",
          data: [],
          required: true,
        },
        {
          title: "任务状态",
          field: "robotTaskState",
          type: "select",
          dataKey: "taskState",
          data: [],
          required: true,
        },
        { title: "来源地址", field: "robotSourceAddress", type: "string", required: true },
        { title: "目标地址", field: "robotTargetAddress", type: "string", required: true },
      ],
      [
        { title: "来源线代码", field: "robotSourceAddressLineCode", type: "string" },
        { title: "目标线代码", field: "robotTargetAddressLineCode", type: "string" },
        { title: "来源托盘代码", field: "robotSourceAddressPalletCode", type: "string" },
        { title: "目标托盘代码", field: "robotTargetAddressPalletCode", type: "string" },
      ],
    ]);
    // æœç´¢è¡¨å•字段
    const searchFormFields = ref({
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_ITaskInfoRepository/IRobotStateRepository.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,53 @@
using WIDESEAWCS_Model.Models;
namespace WIDESEAWCS_ITaskInfoRepository
{
    /// <summary>
    /// æœºæ¢°æ‰‹çŠ¶æ€ä»“å‚¨æŽ¥å£
    /// </summary>
    /// <remarks>
    /// å®šä¹‰æœºæ¢°æ‰‹çŠ¶æ€çš„æ•°æ®åº“è®¿é—®æ“ä½œã€‚
    /// å¤æ‚对象(RobotCrane、CurrentTask、数组等)在调用方使用强类型,
    /// åœ¨æ­¤æŽ¥å£å±‚面以 Dt_RobotState å®žä½“为操作单位。
    /// </remarks>
    public interface IRobotStateRepository
    {
        /// <summary>
        /// æ ¹æ® IP åœ°å€èŽ·å–æœºæ¢°æ‰‹çŠ¶æ€
        /// </summary>
        /// <param name="ipAddress">设备 IP åœ°å€</param>
        /// <returns>状态实体,不存在则返回 null</returns>
        Dt_RobotState? GetByIp(string ipAddress);
        /// <summary>
        /// èŽ·å–æˆ–åˆ›å»ºæœºæ¢°æ‰‹çŠ¶æ€
        /// </summary>
        /// <param name="ipAddress">设备 IP åœ°å€</param>
        /// <param name="robotCrane">机器人设备信息,用于初始化新状态</param>
        /// <returns>状态实体</returns>
        Dt_RobotState GetOrCreate(string ipAddress, RobotCraneDevice robotCrane);
        /// <summary>
        /// å®‰å…¨æ›´æ–°æœºæ¢°æ‰‹çŠ¶æ€ï¼ˆä¹è§‚é”ï¼‰
        /// </summary>
        /// <param name="ipAddress">设备 IP åœ°å€</param>
        /// <param name="newState">新状态实体(RowVersion ä¼šè¢«æ›´æ–°ï¼‰</param>
        /// <param name="expectedRowVersion">期望的行版本号(更新前的版本)</param>
        /// <returns>是否更新成功;false è¡¨ç¤ºç‰ˆæœ¬å†²çªæˆ–记录不存在</returns>
        bool TryUpdate(string ipAddress, Dt_RobotState newState, byte[] expectedRowVersion);
        /// <summary>
        /// å°† Dt_RobotState å®žä½“转换为 RobotSocketState å†…存对象
        /// </summary>
        /// <param name="entity">数据库实体</param>
        /// <returns>内存状态对象</returns>
        RobotSocketState ToSocketState(Dt_RobotState entity);
        /// <summary>
        /// å°† RobotSocketState å†…存对象转换为 Dt_RobotState å®žä½“
        /// </summary>
        /// <param name="state">内存状态对象</param>
        /// <returns>数据库实体</returns>
        Dt_RobotState ToEntity(RobotSocketState state);
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Model/Models/RobotState/Dt_RobotState.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,163 @@
using Newtonsoft.Json;
using SqlSugar;
using WIDESEAWCS_Core.DB.Models;
namespace WIDESEAWCS_Model.Models
{
    /// <summary>
    /// æœºæ¢°æ‰‹çŠ¶æ€æ•°æ®åº“å®žä½“
    /// </summary>
    /// <remarks>
    /// å¯¹åº”数据库表 Dt_RobotState,使用 RowVersion å®žçŽ°ä¹è§‚å¹¶å‘æŽ§åˆ¶ã€‚
    /// å¤æ‚对象(RobotCrane、CurrentTask、数组等)以 JSON å­—符串存储。
    /// </remarks>
    [SugarTable(nameof(Dt_RobotState), "机械手状态表")]
    public class Dt_RobotState : BaseEntity
    {
        /// <summary>
        /// ä¸»é”® ID
        /// </summary>
        [SugarColumn(IsPrimaryKey = true, IsIdentity = true, ColumnDescription = "主键ID")]
        public int Id { get; set; }
        /// <summary>
        /// æœºæ¢°æ‰‹ IP åœ°å€ï¼Œå”¯ä¸€ç´¢å¼•
        /// </summary>
        [SugarColumn(Length = 50, ColumnDescription = "机械手IP地址", IsJsonKey = true)]
        public string IPAddress { get; set; } = string.Empty;
        /// <summary>
        /// è¡Œç‰ˆæœ¬ï¼Œç”¨äºŽä¹è§‚并发控制
        /// </summary>
        /// <remarks>
        /// SqlSugar ä¼šè‡ªåŠ¨ç®¡ç†æ­¤å­—æ®µï¼Œæ¯æ¬¡æ›´æ–°æ—¶æ•°æ®åº“è‡ªåŠ¨é€’å¢žã€‚
        /// æ›´æ–°æ—¶ WHERE RowVersion = @expectedRowVersion,检查影响行数判断是否冲突。
        /// </remarks>
        [SugarColumn(ColumnDescription = "行版本(乐观锁)", IsJsonKey = true)]
        public byte[] RowVersion { get; set; } = Array.Empty<byte>();
        /// <summary>
        /// æ˜¯å¦å·²è®¢é˜…消息事件
        /// </summary>
        [SugarColumn(ColumnDescription = "是否已订阅消息事件", IsJsonKey = true)]
        public bool IsEventSubscribed { get; set; }
        /// <summary>
        /// æœºæ¢°æ‰‹è¿è¡Œæ¨¡å¼
        /// </summary>
        /// <remarks>1: æ‰‹åŠ¨æ¨¡å¼, 2: è‡ªåŠ¨æ¨¡å¼</remarks>
        [SugarColumn(ColumnDescription = "运行模式", IsNullable = true, IsJsonKey = true)]
        public int? RobotRunMode { get; set; }
        /// <summary>
        /// æœºæ¢°æ‰‹æŽ§åˆ¶æ¨¡å¼
        /// </summary>
        /// <remarks>1: å®¢æˆ·ç«¯æŽ§åˆ¶, 2: å…¶ä»–</remarks>
        [SugarColumn(ColumnDescription = "控制模式", IsNullable = true, IsJsonKey = true)]
        public int? RobotControlMode { get; set; }
        /// <summary>
        /// æœºæ¢°æ‰‹æ‰‹è‡‚抓取对象状态
        /// </summary>
        /// <remarks>0: æ— ç‰©æ–™ï¼ˆæ‰‹è‡‚空闲), 1: æœ‰ç‰©æ–™ï¼ˆå·²æŠ“取货物)</remarks>
        [SugarColumn(ColumnDescription = "手臂抓取状态", IsNullable = true, IsJsonKey = true)]
        public int? RobotArmObject { get; set; }
        /// <summary>
        /// æœºæ¢°æ‰‹è®¾å¤‡åŸºç¡€ä¿¡æ¯ï¼ˆJSON åºåˆ—化)
        /// </summary>
        [SugarColumn(Length = 2000, ColumnDescription = "设备信息JSON", IsJsonKey = true)]
        public string RobotCraneJson { get; set; } = string.Empty;
        /// <summary>
        /// æœºæ¢°æ‰‹åˆå§‹åŒ–完成回到待机位状态
        /// </summary>
        /// <remarks>Possible values: "Homed", "Homing"</remarks>
        [SugarColumn(Length = 50, ColumnDescription = "回零状态", IsNullable = true, IsJsonKey = true)]
        public string? Homed { get; set; }
        /// <summary>
        /// æœºæ¢°æ‰‹å½“前正在执行的动作
        /// </summary>
        [SugarColumn(Length = 50, ColumnDescription = "当前动作", IsNullable = true, IsJsonKey = true)]
        public string? CurrentAction { get; set; }
        /// <summary>
        /// æœºæ¢°æ‰‹å½“前运行状态
        /// </summary>
        [SugarColumn(Length = 50, ColumnDescription = "运行状态", IsNullable = true, IsJsonKey = true)]
        public string? OperStatus { get; set; }
        /// <summary>
        /// æœ€è¿‘一次取货完成的位置数组(JSON)
        /// </summary>
        [SugarColumn(Length = 500, ColumnDescription = "取货位置数组JSON", IsNullable = true, IsJsonKey = true)]
        public string? LastPickPositionsJson { get; set; }
        /// <summary>
        /// æœ€è¿‘一次放货完成的位置数组(JSON)
        /// </summary>
        [SugarColumn(Length = 500, ColumnDescription = "放货位置数组JSON", IsNullable = true, IsJsonKey = true)]
        public string? LastPutPositionsJson { get; set; }
        /// <summary>
        /// ç”µæ± /货位条码列表(JSON)
        /// </summary>
        [SugarColumn(Length = 2000, ColumnDescription = "电芯条码列表JSON", IsNullable = true, IsJsonKey = true)]
        public string? CellBarcodeJson { get; set; }
        /// <summary>
        /// æœºæ¢°æ‰‹å½“前正在执行的任务(JSON åºåˆ—化)
        /// </summary>
        [SugarColumn(Length = 2000, ColumnDescription = "当前任务JSON", IsNullable = true, IsJsonKey = true)]
        public string? CurrentTaskJson { get; set; }
        /// <summary>
        /// æ˜¯å¦éœ€è¦æ‰§è¡Œæ‹†ç›˜ä»»åŠ¡
        /// </summary>
        [SugarColumn(ColumnDescription = "是否拆盘任务", IsJsonKey = true)]
        public bool IsSplitPallet { get; set; }
        /// <summary>
        /// æ˜¯å¦éœ€è¦æ‰§è¡Œç»„盘任务
        /// </summary>
        [SugarColumn(ColumnDescription = "是否组盘任务", IsJsonKey = true)]
        public bool IsGroupPallet { get; set; }
        /// <summary>
        /// æœºå™¨äººå·²å¤„理的任务总数
        /// </summary>
        [SugarColumn(ColumnDescription = "已处理任务总数", IsJsonKey = true)]
        public int RobotTaskTotalNum { get; set; }
        /// <summary>
        /// æ˜¯å¦å¤„于假电芯补充模式
        /// </summary>
        [SugarColumn(ColumnDescription = "是否假电芯模式", IsJsonKey = true)]
        public bool IsInFakeBatteryMode { get; set; }
        /// <summary>
        /// å½“前批次起始编号
        /// </summary>
        [SugarColumn(ColumnDescription = "当前批次编号", IsJsonKey = true)]
        public int CurrentBatchIndex { get; set; } = 1;
        /// <summary>
        /// æ¢ç›˜ä»»åŠ¡å½“å‰é˜¶æ®µ
        /// </summary>
        [SugarColumn(ColumnDescription = "换盘阶段", IsJsonKey = true)]
        public int ChangePalletPhase { get; set; }
        /// <summary>
        /// æ˜¯å¦æ‰«ç NG
        /// </summary>
        [SugarColumn(ColumnDescription = "是否扫码NG", IsJsonKey = true)]
        public bool IsScanNG { get; set; }
        /// <summary>
        /// æ˜¯å¦ç”µèŠ¯åˆ°ä½
        /// </summary>
        [SugarColumn(ColumnDescription = "电芯是否到位", IsJsonKey = true)]
        public bool BatteryArrived { get; set; }
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Model/Models/TaskInfo/Dt_RobotTask.cs
@@ -64,27 +64,27 @@
        public string RobotTargetAddress { get; set; }
        /// <summary>
        /// æœºå™¨äººæ¥æºåœ°å€çº¿ä»£ç 
        /// æœºå™¨äººæ¥æºåœ°å€è¾“送线编号
        /// </summary>
        [SugarColumn(Length = 20, ColumnDescription = "机器人来源地址线代码")]
        [SugarColumn(Length = 20, ColumnDescription = "机器人来源地址输送线编号")]
        public string RobotSourceAddressLineCode { get; set; }
        /// <summary>
        /// æœºå™¨äººç›®æ ‡åœ°å€çº¿ä»£ç 
        /// æœºå™¨äººç›®æ ‡åœ°å€è¾“送线编号
        /// </summary>
        [SugarColumn(Length = 20, ColumnDescription = "机器人目标地址线代码")]
        [SugarColumn(Length = 20, ColumnDescription = "机器人目标地址输送线编号")]
        public string RobotTargetAddressLineCode { get; set; }
        /// <summary>
        /// æœºå™¨äººæ¥æºåœ°å€çº¿æ‰˜ç›˜ä»£ç 
        /// æœºå™¨äººæ¥æºåœ°å€è¾“送线托盘号
        /// </summary>
        [SugarColumn(Length = 20, ColumnDescription = "机器人来源地址线托盘代码")]
        [SugarColumn(Length = 20, ColumnDescription = "机器人来源地址输送线托盘号")]
        public string RobotSourceAddressPalletCode { get; set; }
        /// <summary>
        /// æœºå™¨äººç›®æ ‡åœ°å€çº¿æ‰˜ç›˜ä»£ç 
        /// æœºå™¨äººç›®æ ‡åœ°å€çº¿æ‰˜ç›˜å·
        /// </summary>
        [SugarColumn(Length = 20, ColumnDescription = "机器人目标地址线托盘代码")]
        [SugarColumn(Length = 20, ColumnDescription = "机器人目标地址线托盘号")]
        public string RobotTargetAddressPalletCode { get; set; }
        /// <summary>
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_QuartzJob/DeviceBase/DeviceCommand.cs
@@ -166,7 +166,14 @@
                            //throw new Exception($"读取PLC字符串定义长度【{Content[index]}】与对象【{GetType().Name}】属性【{propertyInfo.Name}】特性定义字符串长度【{dataLength}】不一致");
                            //QuartzLogger.Debug($"读取PLC字符串定义长度【{Content[index]}】与对象【{GetType().Name}】属性【{propertyInfo.Name}】特性定义字符串长度【{dataLength}】不一致");
                            propertyInfo.SetValue(this, Encoding.Default.GetString(Content, index, dataLength).Trim().Replace("\0", "").Replace("\\u000","").Trim());
                            try
                            {
                                propertyInfo.SetValue(this, Encoding.Default.GetString(Content, index + 2, Content[index + 1] > 0 ? Content[index + 1] : dataLength - 2).Trim().Replace("\0", "").Replace("\\u000", "").Trim());
                            }
                            catch
                            {
                                propertyInfo.SetValue(this, Encoding.Default.GetString(Content, index, dataLength).Trim().Replace("\0", "").Replace("\\u000", "").Trim());
                            }
                            index += dataLength;
                            break;
                        }
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Server/HostedService/ApiRouteCacheWarmupHostedService.cs
@@ -46,9 +46,9 @@
        public Task StartAsync(CancellationToken cancellationToken)
        {
            const string cacheKey = $"{RedisPrefix.Code}";
            //const string cacheKey = $"{RedisPrefix.Code}";
            _cache.RemoveByPrefix($"{cacheKey}");
            //_cache.RemoveByPrefix($"{cacheKey}");
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Server/Properties/PublishProfiles/FolderProfile.pubxml
@@ -4,14 +4,18 @@
-->
<Project>
  <PropertyGroup>
    <DeleteExistingFiles>false</DeleteExistingFiles>
    <DeleteExistingFiles>true</DeleteExistingFiles>
    <ExcludeApp_Data>false</ExcludeApp_Data>
    <LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
    <LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
    <LastUsedBuildConfiguration>Debug</LastUsedBuildConfiguration>
    <LastUsedPlatform>Any CPU</LastUsedPlatform>
    <PublishProvider>FileSystem</PublishProvider>
    <PublishUrl>bin\Release\net6.0\publish\</PublishUrl>
    <PublishUrl>bin\Debug\net6.0\publish\</PublishUrl>
    <WebPublishMethod>FileSystem</WebPublishMethod>
    <_TargetId>Folder</_TargetId>
    <SiteUrlToLaunchAfterPublish />
    <TargetFramework>net8.0</TargetFramework>
    <ProjectGuid>487fa45b-ea1a-4aca-bb5b-0f6708f462c0</ProjectGuid>
    <SelfContained>false</SelfContained>
  </PropertyGroup>
</Project>
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Server/dotnet-tools.json
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,5 @@
{
  "version": 1,
  "isRoot": true,
  "tools": {}
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_TaskInfoRepository/RobotStateRepository.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,174 @@
using Newtonsoft.Json;
using SqlSugar;
using WIDESEAWCS_Core.BaseRepository;
using WIDESEAWCS_Core.UnitOfWork;
using WIDESEAWCS_ITaskInfoRepository;
using WIDESEAWCS_Model.Models;
namespace WIDESEAWCS_TaskInfoRepository
{
    /// <summary>
    /// æœºæ¢°æ‰‹çŠ¶æ€ SqlSugar ä»“储实现
    /// </summary>
    public class RobotStateRepository : IUnitOfWork, IRobotStateRepository
    {
        private readonly IUnitOfWorkManage _unitOfWork;
        private readonly SqlSugarClient _db;
        public RobotStateRepository(IUnitOfWorkManage unitOfWork)
        {
            _unitOfWork = unitOfWork;
            _db = unitOfWork.GetDbClient();
        }
        public Dt_RobotState? GetByIp(string ipAddress)
        {
            return _db.Queryable<Dt_RobotState>()
                .Where(x => x.IPAddress == ipAddress)
                .First();
        }
        public Dt_RobotState GetOrCreate(string ipAddress, RobotCraneDevice robotCrane)
        {
            var existing = GetByIp(ipAddress);
            if (existing != null)
            {
                return existing;
            }
            var newState = new Dt_RobotState
            {
                IPAddress = ipAddress,
                RobotCraneJson = JsonConvert.SerializeObject(robotCrane),
                CreateTime = DateTime.Now,
                UpdateTime = DateTime.Now
            };
            _db.Insertable(newState).ExecuteCommand();
            return newState;
        }
        public bool TryUpdate(string ipAddress, Dt_RobotState newState, byte[] expectedRowVersion)
        {
            newState.UpdateTime = DateTime.Now;
            var affectedRows = _db.Updateable<Dt_RobotState>(newState)
                .Where(x => x.IPAddress == ipAddress)
                .WhereRowVersion(x => x.RowVersion, expectedRowVersion)
                .ExecuteCommand();
            return affectedRows > 0;
        }
        public RobotSocketState ToSocketState(Dt_RobotState entity)
        {
            var state = new RobotSocketState
            {
                IPAddress = entity.IPAddress,
                Version = BitConverter.ToInt64(entity.RowVersion.Length >= 8 ? entity.RowVersion.Take(8).ToArray() : new byte[8], 0),
                IsEventSubscribed = entity.IsEventSubscribed,
                RobotRunMode = entity.RobotRunMode,
                RobotControlMode = entity.RobotControlMode,
                RobotArmObject = entity.RobotArmObject,
                Homed = entity.Homed,
                CurrentAction = entity.CurrentAction,
                OperStatus = entity.OperStatus,
                IsSplitPallet = entity.IsSplitPallet,
                IsGroupPallet = entity.IsGroupPallet,
                RobotTaskTotalNum = entity.RobotTaskTotalNum,
                IsInFakeBatteryMode = entity.IsInFakeBatteryMode,
                CurrentBatchIndex = entity.CurrentBatchIndex,
                ChangePalletPhase = entity.ChangePalletPhase,
                IsScanNG = entity.IsScanNG,
                BatteryArrived = entity.BatteryArrived
            };
            // ååºåˆ—化复杂 JSON å­—段
            if (!string.IsNullOrEmpty(entity.RobotCraneJson))
            {
                state.RobotCrane = JsonConvert.DeserializeObject<RobotCraneDevice>(entity.RobotCraneJson);
            }
            if (!string.IsNullOrEmpty(entity.CurrentTaskJson))
            {
                state.CurrentTask = JsonConvert.DeserializeObject<Dt_RobotTask>(entity.CurrentTaskJson);
            }
            if (!string.IsNullOrEmpty(entity.LastPickPositionsJson))
            {
                state.LastPickPositions = JsonConvert.DeserializeObject<int[]>(entity.LastPickPositionsJson);
            }
            if (!string.IsNullOrEmpty(entity.LastPutPositionsJson))
            {
                state.LastPutPositions = JsonConvert.DeserializeObject<int[]>(entity.LastPutPositionsJson);
            }
            if (!string.IsNullOrEmpty(entity.CellBarcodeJson))
            {
                state.CellBarcode = JsonConvert.DeserializeObject<List<string>>(entity.CellBarcodeJson) ?? new List<string>();
            }
            return state;
        }
        public Dt_RobotState ToEntity(RobotSocketState state)
        {
            var entity = new Dt_RobotState
            {
                IPAddress = state.IPAddress,
                IsEventSubscribed = state.IsEventSubscribed,
                RobotRunMode = state.RobotRunMode,
                RobotControlMode = state.RobotControlMode,
                RobotArmObject = state.RobotArmObject,
                Homed = state.Homed,
                CurrentAction = state.CurrentAction,
                OperStatus = state.OperStatus,
                IsSplitPallet = state.IsSplitPallet,
                IsGroupPallet = state.IsGroupPallet,
                RobotTaskTotalNum = state.RobotTaskTotalNum,
                IsInFakeBatteryMode = state.IsInFakeBatteryMode,
                CurrentBatchIndex = state.CurrentBatchIndex,
                ChangePalletPhase = state.ChangePalletPhase,
                IsScanNG = state.IsScanNG,
                BatteryArrived = state.BatteryArrived
            };
            // åºåˆ—化复杂对象为 JSON
            if (state.RobotCrane != null)
            {
                entity.RobotCraneJson = JsonConvert.SerializeObject(state.RobotCrane);
            }
            if (state.CurrentTask != null)
            {
                entity.CurrentTaskJson = JsonConvert.SerializeObject(state.CurrentTask);
            }
            if (state.LastPickPositions != null)
            {
                entity.LastPickPositionsJson = JsonConvert.SerializeObject(state.LastPickPositions);
            }
            if (state.LastPutPositions != null)
            {
                entity.LastPutPositionsJson = JsonConvert.SerializeObject(state.LastPutPositions);
            }
            if (state.CellBarcode != null && state.CellBarcode.Count > 0)
            {
                entity.CellBarcodeJson = JsonConvert.SerializeObject(state.CellBarcode);
            }
            return entity;
        }
        public SqlSugarClient GetDbClient() => _db;
        public void BeginTran() => _unitOfWork.BeginTran();
        public void CommitTran() => _unitOfWork.CommitTran();
        public void RollbackTran() => _unitOfWork.RollbackTran();
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_TaskInfoService/RobotTaskService.cs
@@ -28,6 +28,7 @@
using WIDESEAWCS_Core;
using WIDESEAWCS_Core.BaseServices;
using WIDESEAWCS_Core.Helper;
using WIDESEAWCS_Core.Utilities;
using WIDESEAWCS_DTO.Stock;
using WIDESEAWCS_DTO.TaskInfo;
using WIDESEAWCS_ITaskInfoRepository;
@@ -234,6 +235,49 @@
            return stock;
        }
        public override WebResponseContent AddData(SaveModel saveModel)
        {
            try
            {
                if (saveModel == null || saveModel.MainData == null || saveModel.MainData.Count == 0)
                {
                    return WebResponseContent.Instance.Error("传参错误,参数不能为空");
                }
                string validResult = typeof(Dt_RobotTask).ValidateDicInEntity(saveModel.MainData, true, TProperties);
                if (!string.IsNullOrEmpty(validResult))
                {
                    return WebResponseContent.Instance.Error(validResult);
                }
                object? taskNumObj = saveModel.MainData[nameof(Dt_RobotTask.RobotTaskNum)];
                if (taskNumObj != null)
                {
                    int taskNum = Convert.ToInt32(taskNumObj);
                    if (BaseDal.QueryFirst(x => x.RobotTaskNum == taskNum) != null)
                    {
                        return WebResponseContent.Instance.Error($"任务编号 {taskNum} å·²å­˜åœ¨");
                    }
                }
                Dt_RobotTask entity = saveModel.MainData.DicToModel<Dt_RobotTask>();
                entity.Creater = "手动创建";
                entity.CreateDate = DateTime.Now;
                if (saveModel.DetailData == null || saveModel.DetailData.Count == 0)
                {
                    BaseDal.AddData(entity);
                    return WebResponseContent.Instance.OK("新增成功", entity);
                }
                return base.AddData(saveModel);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"新增失败,错误信息:{ex.Message}");
            }
        }
        /// <summary>
        /// æ ¹æ®ç›®æ ‡åœ°å€æŒ‰ã€Œç²¾ç¡® > å›žé€€å€¼ã€è§£æžè§„则值。
        /// </summary>
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/CommonConveyorLineNewJob.cs
@@ -1,4 +1,5 @@
using MapsterMapper;
using Masuit.Tools;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
@@ -40,6 +41,12 @@
        /// ä»»åŠ¡æœåŠ¡
        /// </summary>
        private readonly ITaskService _taskService;
        /// <summary>
        /// æœºå™¨äººä»»åŠ¡æœåŠ¡
        /// </summary>
        private readonly IRobotTaskService _robotTaskService;
        /// <summary>
        /// ä»»åŠ¡æ‰§è¡Œæ˜Žç»†æœåŠ¡
@@ -93,7 +100,7 @@
        /// <param name="mapper">对象映射器</param>
        /// <param name="httpClientHelper">HTTP å®¢æˆ·ç«¯å¸®åŠ©ç±»</param>
        /// <param name="logger">日志记录器</param>
        public CommonConveyorLineNewJob(ITaskService taskService, ITaskExecuteDetailService taskExecuteDetailService, IRouterService routerService, IMapper mapper, HttpClientHelper httpClientHelper, ILogger<CommonConveyorLineNewJob> logger)
        public CommonConveyorLineNewJob(ITaskService taskService, ITaskExecuteDetailService taskExecuteDetailService, IRouterService routerService, IMapper mapper, HttpClientHelper httpClientHelper, ILogger<CommonConveyorLineNewJob> logger, IRobotTaskService robotTaskService)
        {
            _taskService = taskService;
            _taskExecuteDetailService = taskExecuteDetailService;
@@ -101,6 +108,7 @@
            _mapper = mapper;
            _httpClientHelper = httpClientHelper;
            _logger = logger;
            _robotTaskService = robotTaskService;
            // åˆå§‹åŒ–调度处理器
            _conveyorLineDispatch = new ConveyorLineDispatchHandler(_taskService, _taskExecuteDetailService, _routerService, _mapper, _logger);
@@ -247,6 +255,36 @@
                                    ProcessTaskState(conveyorLine, command, task, childDeviceCode);
                                    return Task.CompletedTask;
                                }
                                else if (command.TaskNo == 1 && !command.Barcode.IsNullOrEmpty() && childDeviceCode == "11068")
                                {
                                    if (_robotTaskService.Db.Queryable<Dt_RobotTask>().Any(x => x.RobotTargetAddressPalletCode == command.Barcode))
                                    {
                                        return Task.CompletedTask;
                                    }
                                    Random rnd = new Random();
                                    int num = rnd.StrictNext();//产生真随机数
                                                               // æ²¡æœ‰ä»»åŠ¡å·ä½†æœ‰æ¡ç å¹¶ä¸”åœ¨11068位置,直接添加机械手组盘任务
                                    Dt_RobotTask robotTask = new Dt_RobotTask
                                    {
                                        RobotTargetAddressPalletCode = command.Barcode,
                                        RobotSourceAddress = "1",
                                        RobotTargetAddress = "2", // æœºæ¢°æ‰‹ç›®æ ‡åœ°å€
                                        RobotTaskType = (int)RobotTaskTypeEnum.GroupPallet, // ç»„盘任务
                                        RobotTaskState = (int)TaskRobotStatusEnum.RobotNew, // å¾…执行
                                        RobotTaskTotalNum = 48,
                                        RobotGrade = 1,
                                        RobotRoadway = "注液组盘机械手",
                                        RobotTargetAddressLineCode = childDeviceCode,
                                        RobotTaskNum = num, // ç”Ÿæˆä»»åŠ¡å·
                                        RobotDispatchertime = DateTime.Now,
                                    };
                                    if (_robotTaskService.AddData(robotTask).Status)
                                    {
                                        conveyorLine.SetValue(ConveyorLineDBNameNew.WCS_ACK, (short)1, childDeviceCode);
                                    }
                                }
                            }
                        }
                        catch (Exception innerEx)
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotJob.cs
@@ -7,6 +7,7 @@
using WIDESEAWCS_Core.LogHelper;
using WIDESEAWCS_ITaskInfoService;
using WIDESEAWCS_QuartzJob;
using WIDESEAWCS_RedisService;
using WIDESEAWCS_Tasks.SocketServer;
using WIDESEAWCS_Tasks.Workflow;
using WIDESEAWCS_Tasks.Workflow.Abstractions;
@@ -95,20 +96,20 @@
        /// <param name="tcpSocket">TCP Socket æœåŠ¡å™¨å®žä¾‹</param>
        /// <param name="robotTaskService">机器人任务服务</param>
        /// <param name="taskService">通用任务服务</param>
        /// <param name="cache">缓存服务</param>
        /// <param name="robotStateRepository">机器人状态仓储</param>
        /// <param name="httpClientHelper">HTTP å®¢æˆ·ç«¯å¸®åŠ©ç±»ï¼Œç”¨äºŽè°ƒç”¨ WMS æŽ¥å£</param>
        /// <param name="logger">日志记录器</param>
        public RobotJob(
            TcpSocketServer tcpSocket,
            IRobotTaskService robotTaskService,
            ITaskService taskService,
            ICacheService cache,
            IRobotStateRepository robotStateRepository,
            HttpClientHelper httpClientHelper,
            ILogger<RobotJob> logger,
            IFakeBatteryPositionService fakeBatteryPositionService)
        {
            // åˆå§‹åŒ–状态管理器,传入缓存服务
            _stateManager = new RobotStateManager(cache, _logger);
            // åˆå§‹åŒ–状态管理器,传入仓储服务
            _stateManager = new RobotStateManager(robotStateRepository, _logger);
            _logger = logger;
            // åˆ›å»º Socket ç½‘关,封装 TcpSocketServer çš„访问
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotMessageHandler.cs
@@ -135,6 +135,7 @@
            // å¦‚果缓存中不存在或状态为 null,忽略此消息
            if (!_cache.TryGetValue(cacheKey, out RobotSocketState? cachedState) || cachedState == null)
            {
                _logger.LogInformation($"缓存中不存在或状态为 null,忽略此消息");
                return null;
            }
@@ -150,7 +151,6 @@
            {
                // å¤„理成功后,将原消息回写到客户端(保持原有行为)
                await _socketClientGateway.SendMessageAsync(client, message);
                _logger.LogInformation($"发送消息【{message}】");
                QuartzLogger.Info($"发送消息:【{message}】", state.RobotCrane.DeviceName);
                // å®‰å…¨æ›´æ–°çŠ¶æ€åˆ° Redis
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotStateManager.cs
@@ -1,9 +1,9 @@
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using WIDESEAWCS_Common;
using WIDESEAWCS_Core.Caches;
using WIDESEAWCS_Core.LogHelper;
using WIDESEAWCS_QuartzJob;
using WIDESEAWCS_ITaskInfoRepository;
using WIDESEAWCS_Model.Models;
namespace WIDESEAWCS_Tasks
{
@@ -11,15 +11,15 @@
    /// æœºæ¢°æ‰‹çŠ¶æ€ç®¡ç†å™¨ - è´Ÿè´£ RobotSocketState çš„线程安全更新和克隆
    /// </summary>
    /// <remarks>
    /// æ ¸å¿ƒåŠŸèƒ½æ˜¯é€šè¿‡ç¼“å­˜æœåŠ¡ï¼ˆICacheService)管理 Redis ä¸­çš„æœºæ¢°æ‰‹çŠ¶æ€ã€‚
    /// æä¾›ä¹è§‚并发控制,通过版本号(Version)字段防止并发更新时的数据覆盖问题。
    /// æ ¸å¿ƒåŠŸèƒ½æ˜¯é€šè¿‡ IRobotStateRepository ç®¡ç†æ•°æ®åº“中的机械手状态。
    /// æä¾›ä¹è§‚并发控制,通过 RowVersion é˜²æ­¢å¹¶å‘更新时的数据覆盖问题。
    /// </remarks>
    public class RobotStateManager
    {
        /// <summary>
        /// ç¼“存服务实例,用于读写 Redis ä¸­çš„状态数据
        /// ä»“储服务实例,用于读写数据库中的状态数据
        /// </summary>
        private readonly ICacheService _cache;
        private readonly IRobotStateRepository _repository;
        /// <summary>
        /// æ—¥å¿—记录器
@@ -29,11 +29,11 @@
        /// <summary>
        /// æž„造函数
        /// </summary>
        /// <param name="cache">缓存服务实例(通常为 HybridCacheService)</param>
        /// <param name="repository">仓储服务实例</param>
        /// <param name="logger">日志记录器</param>
        public RobotStateManager(ICacheService cache, ILogger logger)
        public RobotStateManager(IRobotStateRepository repository, ILogger logger)
        {
            _cache = cache;
            _repository = repository;
            _logger = logger;
        }
@@ -41,96 +41,82 @@
        /// å®‰å…¨æ›´æ–° RobotSocketState ç¼“存,防止并发覆盖
        /// </summary>
        /// <remarks>
        /// ä½¿ç”¨ä¹è§‚并发模式:先读取当前版本号,执行更新时检查版本是否一致。
        /// å¦‚果版本不匹配(说明有其他线程已更新),则更新失败返回 false。
        /// ä½¿ç”¨ä¹è§‚并发模式:先读取当前 RowVersion,执行更新时检查版本是否一致。
        /// å¦‚æžœ RowVersion ä¸åŒ¹é…ï¼ˆè¯´æ˜Žæœ‰å…¶ä»–线程已更新),则更新失败返回 false。
        /// </remarks>
        /// <param name="ipAddress">设备 IP åœ°å€ï¼Œç”¨äºŽæž„建缓存键</param>
        /// <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 currentEntity = _repository.GetByIp(ipAddress);
            // ä»Žç¼“存获取当前存储的状态
            var currentState = _cache.Get<RobotSocketState>(cacheKey);
            // å¦‚果缓存中不存在该设备的状态,直接返回 false(应由 GetOrCreateState å…ˆåˆ›å»ºï¼‰
            if (currentState == null)
            if (currentEntity == null)
            {
                return false;
            }
            // è®°å½•当前存储的版本号,作为更新时的期望版本
            var expectedVersion = currentState.Version;
            // è®°å½•当前存储的 RowVersion,作为更新时的期望版本
            var expectedRowVersion = currentEntity.RowVersion;
            // åˆ›å»ºçŠ¶æ€çš„æ·±æ‹·è´å‰¯æœ¬ï¼Œé¿å…ç›´æŽ¥ä¿®æ”¹åŽŸå¯¹è±¡å¼•ç”¨
            // è¿™æ ·å¯ä»¥ç¡®ä¿åœ¨å¤šçº¿ç¨‹çŽ¯å¢ƒä¸‹ï¼Œæ¯ä¸ªçº¿ç¨‹æ“ä½œçš„æ˜¯ç‹¬ç«‹çš„çŠ¶æ€å‰¯æœ¬
            var stateCopy = CloneState(currentState);
            // åˆ›å»ºçŠ¶æ€çš„æ·±æ‹·è´å‰¯æœ¬ï¼ˆä½¿ç”¨ JSON åºåˆ—化实现)
            var stateCopy = CloneState(_repository.ToSocketState(currentEntity));
            // æ‰§è¡Œè°ƒç”¨è€…提供的更新逻辑,传入副本状态,获取新的状态对象
            var newState = updateAction(stateCopy);
            // å°†æ–°çŠ¶æ€çš„ç‰ˆæœ¬å·æ›´æ–°ä¸ºæœ€æ–°çš„æ—¶é—´æˆ³ï¼Œè¡¨ç¤ºæ•°æ®å·²æ›´æ–°
            newState.Version = DateTime.UtcNow.Ticks;
            // å°†æ–°çŠ¶æ€è½¬æ¢ä¸ºæ•°æ®åº“å®žä½“
            var newEntity = _repository.ToEntity(newState);
            newEntity.RowVersion = Array.Empty<byte>(); // SqlSugar ä¼šè‡ªåŠ¨ç®¡ç†
            newEntity.Id = currentEntity.Id;
            // è°ƒç”¨ç¼“存服务的安全更新方法,传入期望版本和版本提取器
            // å¦‚果当前版本与期望版本不一致(已被其他线程更新),则更新失败
            return _cache.TrySafeUpdate(
                cacheKey,
                newState,
                expectedVersion,
                s => s.Version  // æŒ‡å®šå“ªä¸ªå­—段作为版本号
            );
            // è°ƒç”¨ä»“储的安全更新方法,传入期望 RowVersion
            // å¦‚æžœ RowVersion ä¸ä¸€è‡´ï¼ˆå·²è¢«å…¶ä»–线程更新),则更新失败
            return _repository.TryUpdate(ipAddress, newEntity, expectedRowVersion);
        }
        /// <summary>
        /// å®‰å…¨æ›´æ–° RobotSocketState ç¼“存的重载版本(直接传入新状态)
        /// å®‰å…¨æ›´æ–° RobotSocketState çš„重载版本(直接传入新状态)
        /// </summary>
        /// <remarks>
        /// ä¸Žä¸Šä¸€ä¸ªé‡è½½çš„区别:此方法直接接收完整的新状态对象,而不是更新委托。
        /// å¦‚果设备状态不存在于缓存中,则直接添加新状态。
        /// å¦‚果数据库中不存在该设备的状态,则创建新记录。
        /// </remarks>
        /// <param name="ipAddress">设备 IP åœ°å€ï¼Œç”¨äºŽæž„建缓存键</param>
        /// <param name="newState">新状态对象(方法内部会更新其 Version å­—段)</param>
        /// <param name="ipAddress">设备 IP åœ°å€</param>
        /// <param name="newState">新状态对象</param>
        /// <returns>是否更新成功;新建设置为 true</returns>
        public bool TryUpdateStateSafely(string ipAddress, RobotSocketState newState)
        {
            // æž„建 Redis ç¼“存键
            var cacheKey = GetCacheKey(ipAddress);
            // ä»Žæ•°æ®åº“获取当前存储的状态
            var currentEntity = _repository.GetByIp(ipAddress);
            // ä»Žç¼“存获取当前存储的状态
            var currentState = _cache.Get<RobotSocketState>(cacheKey);
            // å¦‚果当前不存在该设备的状态
            if (currentState == null)
            // å¦‚果当前不存在该设备的状态,创建新记录
            if (currentEntity == null)
            {
                // ä¸ºæ–°çŠ¶æ€è®¾ç½®ç‰ˆæœ¬å·ï¼ˆæ—¶é—´æˆ³ï¼‰
                newState.Version = DateTime.UtcNow.Ticks;
                // ç›´æŽ¥æ·»åŠ åˆ°ç¼“å­˜
                _cache.AddObject(cacheKey, newState);
                var entity = _repository.ToEntity(newState);
                entity.CreateTime = DateTime.Now;
                entity.UpdateTime = DateTime.Now;
                _repository.GetOrCreate(newState.IPAddress, newState.RobotCrane ?? new RobotCraneDevice());
                _logger.LogDebug("TryUpdateStateSafely:创建新状态,IP: {IpAddress}", ipAddress);
                QuartzLogger.Debug($"创建新状态,IP: {ipAddress}", ipAddress);
                return true;
            }
            // å½“前存在状态,记录期望版本号用于乐观锁检查
            var expectedVersion = currentState.Version;
            // å½“前存在状态,记录期望 RowVersion ç”¨äºŽä¹è§‚锁检查
            var expectedRowVersion = currentEntity.RowVersion;
            // æ›´æ–°æ–°çŠ¶æ€çš„ç‰ˆæœ¬å·ä¸ºæœ€æ–°æ—¶é—´æˆ³
            newState.Version = DateTime.UtcNow.Ticks;
            // å°†æ–°çŠ¶æ€è½¬æ¢ä¸ºæ•°æ®åº“å®žä½“
            var newEntity = _repository.ToEntity(newState);
            newEntity.Id = currentEntity.Id;
            newEntity.RowVersion = Array.Empty<byte>();
            // å°è¯•安全更新,如果版本冲突则返回 false
            bool success = _cache.TrySafeUpdate(
                cacheKey,
                newState,
                expectedVersion,
                s => s.Version
            );
            bool success = _repository.TryUpdate(ipAddress, newEntity, expectedRowVersion);
            if (!success)
            {
                _logger.LogWarning("TryUpdateStateSafely:版本冲突,更新失败,IP: {IpAddress},期望版本: {ExpectedVersion}", ipAddress, expectedVersion);
                _logger.LogWarning("TryUpdateStateSafely:版本冲突,更新失败,IP: {IpAddress},期望版本字节长度: {ExpectedLength}", ipAddress, expectedRowVersion.Length);
                QuartzLogger.Warn($"版本冲突,更新失败,IP: {ipAddress}", ipAddress);
            }
@@ -148,55 +134,35 @@
        /// <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 ç¼“存键
        /// </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));
            var entity = _repository.GetByIp(ipAddress);
            return entity != null ? _repository.ToSocketState(entity) : null;
        }
        /// <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,  // è®¾ç½® IP åœ°å€ä½œä¸ºæ ‡è¯†
                RobotCrane = robotCrane  // ä¿å­˜è®¾å¤‡ä¿¡æ¯
            });
            var entity = _repository.GetOrCreate(ipAddress, robotCrane);
            return _repository.ToSocketState(entity);
        }
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotTaskProcessor.cs
@@ -185,7 +185,7 @@
                // å°†ä»»åŠ¡å…³è”åˆ°çŠ¶æ€å¯¹è±¡
                state.CurrentTask = task;
                if(isScanNG)
                if (isScanNG)
                {
                    state.IsScanNG = true;
                }
@@ -456,7 +456,7 @@
        /// <param name="state">机器人当前状态</param>
        /// <param name="useSourceAddress">是否使用源地址(true è¡¨ç¤ºæ‹†ç›˜/换盘场景,false è¡¨ç¤ºç»„盘/换盘场景)</param>
        /// <returns>处理是否成功</returns>
        public async Task<bool> HandleInboundTaskAsync(RobotSocketState state, bool useSourceAddress)
        public async Task<bool> HandleInboundTaskAsync(RobotSocketState state, bool useSourceAddress, string isRoadway = null)
        {
            // èŽ·å–å½“å‰å…³è”çš„ä»»åŠ¡
            var currentTask = state.CurrentTask;
@@ -468,18 +468,17 @@
            }
            // èŽ·å–å··é“ä»£ç 
            string roadway = currentTask.RobotSourceAddressLineCode;
            string roadway = string.Empty;
            // æ ¹æ®å··é“名称判断仓库 ID
            // ZYRB1 -> 1, HPRB001 -> 2, å…¶ä»– -> 3
            int warehouseId = currentTask.RobotRoadway == "注液组盘机械手" ? 1 : currentTask.RobotRoadway == "HPRB001" ? 2 : 3;
            // ä»»åŠ¡ç±»åž‹ï¼ˆ0 è¡¨ç¤ºæœªå®šä¹‰ï¼Œç¨åŽæ ¹æ®ä»»åŠ¡ç±»åž‹è®¾ç½®ï¼‰
            int taskType = 0;
            int warehouseId = 0;
            // æºåœ°å€å’Œç›®æ ‡åœ°å€ï¼ˆåˆå§‹åŒ–)
            string SourceAddress = currentTask.RobotTargetAddressLineCode;
            string TargetAddress = currentTask.RobotSourceAddressLineCode;
            // ä»»åŠ¡ç±»åž‹ï¼ˆ0 è¡¨ç¤ºæœªå®šä¹‰ï¼Œç¨åŽæ ¹æ®ä»»åŠ¡ç±»åž‹è®¾ç½®ï¼‰
            int taskType = 0;
            // æ‰˜ç›˜ä»£ç ï¼ˆåˆå§‹åŒ–为空)
            string PalletCode = string.Empty;
@@ -494,16 +493,33 @@
                switch (robotTaskType)
                {
                    case RobotTaskTypeEnum.GroupPallet:
                        // ç»„盘任务不使用源地址,直接返回 false
                        _logger.LogDebug("HandleInboundTaskAsync:组盘任务不使用源地址");
                        QuartzLogger.Debug($"HandleInboundTaskAsync:组盘任务不使用源地址", state.RobotCrane?.DeviceName ?? "Unknown");
                        return false;
                        warehouseId = 1;
                        roadway = "GWSC1";
                        break;
                    case RobotTaskTypeEnum.ChangePallet:
                        // æ¢ç›˜/拆盘场景:托盘需要入库
                        taskType = TaskTypeEnum.InEmpty.GetHashCode();  // ç©ºæ‰˜ç›˜å…¥åº“
                        PalletCode = currentTask.RobotSourceAddressPalletCode;  // ä½¿ç”¨æºåœ°å€çš„æ‰˜ç›˜ç 
                        if (isRoadway == "HWSC1")
                        {
                            warehouseId = 2;
                            roadway = "HWSC1";
                        }
                        else if (isRoadway == "GWSC1")
                        {
                            warehouseId = 1;
                            roadway = "GWSC1";
                        }
                        break;
                    case RobotTaskTypeEnum.SplitPallet:
                        // æ¢ç›˜/拆盘场景:托盘需要入库
                        taskType = TaskTypeEnum.InEmpty.GetHashCode();  // ç©ºæ‰˜ç›˜å…¥åº“
                        PalletCode = currentTask.RobotSourceAddressPalletCode;  // ä½¿ç”¨æºåœ°å€çš„æ‰˜ç›˜ç 
                        warehouseId = 3;
                        roadway = "CWSC1";
                        break;
                }
            }
@@ -513,17 +529,34 @@
                switch (robotTaskType)
                {
                    case RobotTaskTypeEnum.ChangePallet:
                        // æ¢ç›˜/组盘场景:货物需要入库
                        taskType = TaskTypeEnum.Inbound.GetHashCode();  // æˆå“å…¥åº“
                        PalletCode = currentTask.RobotTargetAddressPalletCode;  // ä½¿ç”¨ç›®æ ‡åœ°å€çš„æ‰˜ç›˜ç 
                        if (isRoadway == "HWSC1")
                        {
                            warehouseId = 2;
                            roadway = "HWSC1";
                        }
                        else if (isRoadway == "GWSC1")
                        {
                            warehouseId = 1;
                            roadway = "GWSC1";
                        }
                        break;
                    case RobotTaskTypeEnum.GroupPallet:
                        // æ¢ç›˜/组盘场景:货物需要入库
                        taskType = TaskTypeEnum.Inbound.GetHashCode();  // æˆå“å…¥åº“
                        PalletCode = currentTask.RobotTargetAddressPalletCode;  // ä½¿ç”¨ç›®æ ‡åœ°å€çš„æ‰˜ç›˜ç 
                        warehouseId = 1;
                        roadway = "GWSC1";
                        break;
                    case RobotTaskTypeEnum.SplitPallet:
                        // æ‹†ç›˜ä»»åŠ¡ä¸ä½¿ç”¨ç›®æ ‡åœ°å€
                        _logger.LogDebug("HandleInboundTaskAsync:拆盘任务不使用目标地址");
                        QuartzLogger.Debug($"HandleInboundTaskAsync:拆盘任务不使用目标地址", state.RobotCrane?.DeviceName ?? "Unknown");
                        return true;
                        break;
                }
            }
@@ -532,8 +565,8 @@
            {
                PalletCode = PalletCode,                    // æ‰˜ç›˜æ¡ç 
                SourceAddress = SourceAddress ?? string.Empty,  // æºåœ°å€
                TargetAddress = TargetAddress ?? string.Empty,  // ç›®æ ‡åœ°å€
                Roadway = roadway,                          // å··é“
                TargetAddress = roadway ?? string.Empty,  // ç›®æ ‡åœ°å€
                Roadway = roadway ?? string.Empty,             // å··é“
                WarehouseId = warehouseId,                   // ä»“库 ID
                PalletType = 1,                             // æ‰˜ç›˜ç±»åž‹ï¼ˆé»˜è®¤ä¸º1)
                TaskType = taskType                         // ä»»åŠ¡ç±»åž‹ï¼ˆå…¥åº“/空托盘入库)
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotPrefixCommandHandler.cs
@@ -1,6 +1,7 @@
using System.Net.Sockets;
using WIDESEAWCS_Common.HttpEnum;
using WIDESEAWCS_Common.TaskEnum;
using WIDESEAWCS_Core.Helper;
using WIDESEAWCS_ITaskInfoService;
using WIDESEAWCS_Model.Models;
using WIDESEAWCS_Tasks.Workflow.Abstractions;
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotSimpleCommandHandler.cs
@@ -158,7 +158,7 @@
                            if (state.ChangePalletPhase == 5)
                            {
                                // FlowB æœ€ç»ˆé˜¶æ®µï¼šå‡ç”µèŠ¯å–å®Œï¼Œæºç©ºæ‰˜ç›˜å›žåº“ HCSC1
                                if (!await _taskProcessor.HandleInboundTaskAsync(state, useSourceAddress: true))
                                if (!await _taskProcessor.HandleInboundTaskAsync(state, useSourceAddress: true, isRoadway: "HCSC1"))
                                {
                                    return false;
                                }
@@ -184,15 +184,16 @@
                            {
                                // FlowA ä¸­é—´é˜¶æ®µï¼šæ­£å¸¸ç”µèŠ¯å–å®Œï¼Œæºç©ºæ‰˜ç›˜å›žåº“ GWSC1
                                // ä¸åˆ é™¤ä»»åŠ¡ï¼Œä¸é‡ç½®çŠ¶æ€ï¼Œç»§ç»­ Phase 3-4 å‡ç”µèŠ¯æµç¨‹
                                if (!await _taskProcessor.HandleInboundTaskAsync(state, useSourceAddress: true))
                                if (!await _taskProcessor.HandleInboundTaskAsync(state, useSourceAddress: true, isRoadway: "GWSC1"))
                                {
                                    return false;
                                }
                                return true;
                            }
                            List<string> str = new List<string>() { "11001", "11010" };
                            // Phase == 0: éžæ‰¹æ¬¡æ¨¡å¼ï¼ˆç›®æ ‡æ€»æ•°==48)
                            if (!await _taskProcessor.HandleInboundTaskAsync(state, useSourceAddress: true))
                            if (!await _taskProcessor.HandleInboundTaskAsync(state, useSourceAddress: true, isRoadway: str.Contains(currentTask.RobotSourceAddressLineCode) ? "GWSC1" : "HCSC1"))
                            {
                                return false;
                            }
@@ -274,7 +275,7 @@
                            if (state.ChangePalletPhase == 5)
                            {
                                // FlowA æœ€ç»ˆé˜¶æ®µï¼šå‡ç”µèŠ¯æ”¾å®Œï¼Œç›®æ ‡æ‰˜ç›˜æ»¡48入库 HCSC1
                                if (!await _taskProcessor.HandleInboundTaskAsync(state, useSourceAddress: false))
                                if (!await _taskProcessor.HandleInboundTaskAsync(state, useSourceAddress: false, isRoadway: "HCSC1"))
                                {
                                    return false;
                                }
@@ -301,15 +302,16 @@
                            {
                                // FlowB ä¸­é—´é˜¶æ®µï¼šæ­£å¸¸ç”µèŠ¯æ”¾å®Œï¼Œæœ‰è´§æ‰˜ç›˜ç»„ç›˜å…¥åº“ GWSC1
                                // ä¸åˆ é™¤ä»»åŠ¡ï¼Œä¸é‡ç½®çŠ¶æ€ï¼Œç»§ç»­ Phase 3-4 å‡ç”µèŠ¯æµç¨‹
                                if (!await _taskProcessor.HandleInboundTaskAsync(state, useSourceAddress: false))
                                if (!await _taskProcessor.HandleInboundTaskAsync(state, useSourceAddress: false, isRoadway: "GWSC1"))
                                {
                                    return false;
                                }
                                return true;
                            }
                            List<string> str = new List<string>() { "11001", "11010" };
                            // Phase == 0: éžæ‰¹æ¬¡æ¨¡å¼ï¼ˆç›®æ ‡æ€»æ•°==48)
                            if (!await _taskProcessor.HandleInboundTaskAsync(state, useSourceAddress: false))
                            if (!await _taskProcessor.HandleInboundTaskAsync(state, useSourceAddress: false, str.Contains(currentTask.RobotTargetAddressLineCode) ? "GWSC1" : "HCSC1"))
                            {
                                return false;
                            }
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotWorkflowOrchestrator.cs
@@ -352,7 +352,7 @@
                        // æ¡ç é‡å¤ï¼Œè®°å½•错误日志并停止后续操作(后续放货时会用到这些条码信息,供后续放货时使用,调试后可能会取消此逻辑)
                        // å‘送取货指令 æ ‡è®°æ‰«ç NG,放货时不使用这些条码,并放入NG口
                        await _taskProcessor.SendSocketRobotPickAsync(task, stateForUpdate, true);
                        //await _taskProcessor.SendSocketRobotPickAsync(task, stateForUpdate, true);
                        return;
                    }
                    else
@@ -367,7 +367,6 @@
                        };
                    }
                    // è®°å½•日志:读取托盘条码成功
                    _logger.LogInformation("HandlePutFinishedStateAsync:读取托盘条码成功: {Barcode1}+{Barcode2},任务号: {TaskNum}", trayBarcode1, trayBarcode2, task.RobotTaskNum);
                    QuartzLogger.Info($"读取托盘条码成功: {trayBarcode1}+{trayBarcode2}", stateForUpdate.RobotCrane.DeviceName);
@@ -378,12 +377,12 @@
                else
                {
                    // æ¡ç è¯»å–失败,记录错误日志
                    _logger.LogError("HandlePutFinishedStateAsync:读取托盘条码失败,任务号: {TaskNum}", task.RobotTaskNum);
                    QuartzLogger.Error($"读取托盘条码失败", stateForUpdate.RobotCrane.DeviceName);
                    _logger.LogError("HandlePutFinishedStateAsync:读取托盘条码失败,任务号: {TaskNum},一号位: {trayBarcode1},二号位: {trayBarcode2}", task.RobotTaskNum,trayBarcode1,trayBarcode2);
                    QuartzLogger.Error($"读取托盘条码失败,一号位:{trayBarcode1},二号位:{trayBarcode2}", stateForUpdate.RobotCrane.DeviceName);
                    // å‘送取货指令 æ ‡è®°æ‰«ç NG,放货时不使用这些条码,并放入NG口
                    await _taskProcessor.SendSocketRobotPickAsync(task, stateForUpdate, true);
                    //await _taskProcessor.SendSocketRobotPickAsync(task, stateForUpdate, true);
                    return;
                }
            }
Code/WMS/WIDESEA_WMSClient/src/views/Home.vue
@@ -3,44 +3,21 @@
    <!-- é¡¶éƒ¨ï¼šæœ¬æœˆå‡ºå…¥åº“趋势 (全宽) -->
    <div class="chart-row full-width">
      <div class="chart-card">
        <div class="card-title">本月出入库趋势</div>
        <div class="card-title">每月出入库趋势</div>
        <div id="chart-monthly-trend" class="chart-content"></div>
      </div>
    </div>
    <!-- ç¬¬äºŒè¡Œï¼šä»Šæ—¥/本周出入库对比 -->
    <div class="chart-row">
    <!-- ç¬¬äºŒè¡Œï¼šæ¯æ—¥å‡ºå…¥åº“趋势 (全宽) -->
    <div class="chart-row full-width">
      <div class="chart-card">
        <div class="card-title">今日出入库对比</div>
        <div id="chart-today" class="chart-content"></div>
      </div>
      <div class="chart-card">
        <div class="card-title">本周出入库对比</div>
        <div id="chart-week" class="chart-content"></div>
        <div class="card-title">每日出入库趋势</div>
        <div id="chart-daily" class="chart-content"></div>
      </div>
    </div>
    <!-- ç¬¬ä¸‰è¡Œï¼šæœ¬æœˆå¯¹æ¯”/库存总量 -->
    <!-- ç¬¬å››è¡Œï¼šä»“库分布 -->
    <div class="chart-row">
      <div class="chart-card">
        <div class="card-title">本月出入库对比</div>
        <div id="chart-month" class="chart-content"></div>
      </div>
      <div class="chart-card">
        <div class="card-title">当前库存总量</div>
        <div class="stock-total">
          <div class="total-number">{{ overviewData.TotalStock || 0 }}</div>
          <div class="total-label">托盘</div>
        </div>
      </div>
    </div>
    <!-- ç¬¬å››è¡Œï¼šåº“龄分布/仓库分布 -->
    <div class="chart-row">
      <div class="chart-card">
        <div class="card-title">库存库龄分布</div>
        <div id="chart-stock-age" class="chart-content"></div>
      </div>
      <div class="chart-card">
        <div class="card-title">各仓库库存分布</div>
        <div id="chart-warehouse" class="chart-content"></div>
@@ -57,16 +34,8 @@
  data() {
    return {
      charts: {},
      overviewData: {
        TodayInbound: 0,
        TodayOutbound: 0,
        MonthInbound: 0,
        MonthOutbound: 0,
        TotalStock: 0
      },
      weeklyData: [],
      dailyData: [],
      monthlyData: [],
      stockAgeData: [],
      warehouseData: []
    };
  },
@@ -86,53 +55,22 @@
    initCharts() {
      this.charts.monthlyTrend = echarts.init(document.getElementById("chart-monthly-trend"));
      this.charts.today = echarts.init(document.getElementById("chart-today"));
      this.charts.week = echarts.init(document.getElementById("chart-week"));
      this.charts.month = echarts.init(document.getElementById("chart-month"));
      this.charts.stockAge = echarts.init(document.getElementById("chart-stock-age"));
      this.charts.daily = echarts.init(document.getElementById("chart-daily"));
      this.charts.warehouse = echarts.init(document.getElementById("chart-warehouse"));
    },
    async loadData() {
      await this.loadOverview();
      await this.loadWeeklyStats();
      await this.loadMonthlyStats();
      await this.loadStockAgeDistribution();
      await this.loadDailyStats();
      await this.loadStockByWarehouse();
    },
    async loadOverview() {
      try {
        const res = await this.http.get("/api/Dashboard/Overview");
        console.log("总览数据", res.Data);
        if (res.Status && res.Data) {
          this.overviewData = res.Data;
          this.updateTodayChart();
          this.updateWeekChart();
          this.updateMonthChart();
        }
      } catch (e) {
        console.error("加载总览数据失败", e);
      }
    },
    async loadWeeklyStats() {
      try {
        const res = await this.http.get("/api/Dashboard/WeeklyStats", { weeks: 12 });
        if (res.Status && res.Data) {
          this.weeklyData = res.Data;
          this.updateWeekChart();
        }
      } catch (e) {
        console.error("加载每周统计失败", e);
      }
    },
    async loadMonthlyStats() {
      try {
        const res = await this.http.get("/api/Dashboard/MonthlyStats", { months: 12 });
        if (res.Status && res.Data) {
          this.monthlyData = res.Data;
        if (res.status && res.data) {
          console.log("每月统计数据:", res.data);
          this.monthlyData = res.data;
          this.updateMonthlyTrendChart();
        }
      } catch (e) {
@@ -140,110 +78,30 @@
      }
    },
    async loadStockAgeDistribution() {
    async loadDailyStats() {
      try {
        const res = await this.http.get("/api/Dashboard/StockAgeDistribution");
        if (res.Status && res.Data) {
          this.stockAgeData = res.Data;
          this.updateStockAgeChart();
        const res = await this.http.get("/api/Dashboard/DailyStats", { days: 30 });
        if (res.status && res.data) {
          console.log("每日统计数据:", res.data);
          this.dailyData = res.data;
          this.updateDailyChart();
        }
      } catch (e) {
        console.error("加载库龄分布失败", e);
        console.error("加载每日统计失败", e);
      }
    },
    async loadStockByWarehouse() {
      try {
        const res = await this.http.get("/api/Dashboard/StockByWarehouse");
        if (res.Status && res.Data) {
          this.warehouseData = res.Data;
        if (res.status && res.data) {
          console.log("仓库分布数据:", res.data);
          this.warehouseData = res.data.data || res.data;
          this.updateWarehouseChart();
        }
      } catch (e) {
        console.error("加载仓库分布失败", e);
      }
    },
    updateTodayChart() {
      const option = {
        tooltip: { trigger: "axis" },
        legend: { data: ["入库", "出库"], textStyle: { color: "#fff" } },
        xAxis: {
          type: "category",
          data: ["今日"],
          axisLabel: { color: "#fff" }
        },
        yAxis: {
          type: "value",
          axisLabel: { color: "#fff" }
        },
        series: [
          { name: "入库", type: "bar", data: [this.overviewData.TodayInbound], itemStyle: { color: "#5470c6" } },
          { name: "出库", type: "bar", data: [this.overviewData.TodayOutbound], itemStyle: { color: "#91cc75" } }
        ]
      };
      this.charts.today.setOption(option, true);
    },
    updateWeekChart() {
      const thisWeek = this.getThisWeekData(this.weeklyData);
      const option = {
        tooltip: { trigger: "axis" },
        legend: { data: ["入库", "出库"], textStyle: { color: "#fff" } },
        xAxis: {
          type: "category",
          data: ["本周"],
          axisLabel: { color: "#fff" }
        },
        yAxis: {
          type: "value",
          axisLabel: { color: "#fff" }
        },
        series: [
          { name: "入库", type: "bar", data: [thisWeek.Inbound], itemStyle: { color: "#5470c6" } },
          { name: "出库", type: "bar", data: [thisWeek.Outbound], itemStyle: { color: "#91cc75" } }
        ]
      };
      this.charts.week.setOption(option, true);
    },
    getThisWeekData(weeklyData) {
      if (!weeklyData || weeklyData.length === 0) return { Inbound: 0, Outbound: 0 };
      const thisWeekKey = this.getCurrentWeekKey();
      const thisWeek = weeklyData.find(w => w.Week === thisWeekKey);
      return thisWeek || { Inbound: 0, Outbound: 0 };
    },
    getCurrentWeekKey() {
      const now = new Date();
      const diff = (7 + (now.getDay() - 1)) % 7;
      const monday = new Date(now);
      monday.setDate(now.getDate() - diff);
      const year = monday.getFullYear();
      const jan1 = new Date(year, 0, 1);
      const weekNum = Math.ceil(((monday - jan1) / 86400000 + jan1.getDay() + 1) / 7);
      return `${year}-W${String(weekNum).padStart(2, "0")}`;
    },
    updateMonthChart() {
      const option = {
        tooltip: { trigger: "axis" },
        legend: { data: ["入库", "出库"], textStyle: { color: "#fff" } },
        xAxis: {
          type: "category",
          data: ["本月"],
          axisLabel: { color: "#fff" }
        },
        yAxis: {
          type: "value",
          axisLabel: { color: "#fff" }
        },
        series: [
          { name: "入库", type: "bar", data: [this.overviewData.MonthInbound], itemStyle: { color: "#5470c6" } },
          { name: "出库", type: "bar", data: [this.overviewData.MonthOutbound], itemStyle: { color: "#91cc75" } }
        ]
      };
      this.charts.month.setOption(option, true);
    },
    updateMonthlyTrendChart() {
@@ -252,7 +110,7 @@
        legend: { data: ["入库", "出库"], textStyle: { color: "#fff" } },
        xAxis: {
          type: "category",
          data: this.monthlyData.map(m => m.Month),
          data: this.monthlyData.map(m => m.month),
          axisLabel: { color: "#fff", rotate: 45 }
        },
        yAxis: [
@@ -263,56 +121,144 @@
          }
        ],
        series: [
          { name: "入库", type: "bar", data: this.monthlyData.map(m => m.Inbound), itemStyle: { color: "#5470c6" } },
          { name: "出库", type: "line", data: this.monthlyData.map(m => m.Outbound), itemStyle: { color: "#91cc75" } }
          { name: "入库", type: "bar", data: this.monthlyData.map(m => m.inbound), itemStyle: { color: "#5470c6" } },
          { name: "出库", type: "line", data: this.monthlyData.map(m => m.outbound), itemStyle: { color: "#91cc75" } }
        ]
      };
      this.charts.monthlyTrend.setOption(option, true);
    },
    updateStockAgeChart() {
    updateDailyChart() {
      const option = {
        tooltip: { trigger: "axis" },
        legend: { data: ["入库", "出库"], textStyle: { color: "#fff" } },
        xAxis: {
          type: "category",
          data: this.stockAgeData.map(s => s.Range),
          axisLabel: { color: "#fff" }
          data: this.dailyData.map(d => d.date),
          axisLabel: {
            color: "#fff",
            interval: 0,
            rotate: 45,
            fontSize: 12,
            margin: 10
          },
          axisTick: {
            alignWithLabel: true
          }
        },
        yAxis: {
          type: "value",
          axisLabel: { color: "#fff" }
        },
        grid: {
          left: '3%',
          right: '4%',
          bottom: '15%',
          top: '10%',
          containLabel: true
        },
        series: [
          {
            type: "bar",
            data: this.stockAgeData.map(s => s.Count),
            itemStyle: { color: "#5470c6" }
          }
          { name: "入库", type: "bar", data: this.dailyData.map(d => d.inbound), itemStyle: { color: "#5470c6" } },
          { name: "出库", type: "bar", data: this.dailyData.map(d => d.outbound), itemStyle: { color: "#91cc75" } }
        ]
      };
      this.charts.stockAge.setOption(option, true);
      this.charts.daily.setOption(option, true);
    },
    updateWarehouseChart() {
      const warehouseNames = this.warehouseData.map(w => w.warehouse);
      const totalStocks = this.warehouseData.map(w => w.total);
      const hasStocks = this.warehouseData.map(w => w.hasStock);
      const noStocks = this.warehouseData.map(w => w.noStock);
      const hasStockPercentages = this.warehouseData.map(w => w.hasStockPercentage);
      const noStockPercentages = this.warehouseData.map(w => w.noStockPercentage);
      const option = {
        tooltip: { trigger: "axis" },
        tooltip: {
          trigger: 'axis',
          axisPointer: {
            type: 'shadow'
          },
          formatter: function(params) {
            let tip = params[0].name + '<br/>';
            params.forEach(param => {
              const dataIndex = param.dataIndex;
              const warehouse = window.homeComponent.warehouseData[dataIndex];
              if (param.seriesName === '已用容量') {
                tip += `${param.marker}${param.seriesName}: ${param.value} (${warehouse.hasStockPercentage})<br/>`;
                tip += `有库存: ${warehouse.hasStock}<br/>`;
                tip += `无库存: ${warehouse.noStock}<br/>`;
                tip += `总容量: ${warehouse.total}`;
              } else if (param.seriesName === '剩余容量') {
                tip += `${param.marker}${param.seriesName}: ${param.value} (${warehouse.noStockPercentage})<br/>`;
                tip += `有库存: ${warehouse.hasStock}<br/>`;
                tip += `无库存: ${warehouse.noStock}<br/>`;
                tip += `总容量: ${warehouse.total}`;
              }
            });
            return tip;
          }
        },
        legend: {
          data: ['已用容量', '剩余容量'],
          textStyle: { color: '#fff' }
        },
        xAxis: {
          type: "category",
          data: this.warehouseData.map(w => w.Warehouse),
          axisLabel: { color: "#fff", rotate: 30 }
          type: 'category',
          data: warehouseNames,
          axisLabel: { color: '#fff', rotate: 30 }
        },
        yAxis: {
          type: "value",
          axisLabel: { color: "#fff" }
          type: 'value',
          axisLabel: { color: '#fff' }
        },
        series: [
          {
            type: "bar",
            data: this.warehouseData.map(w => w.Count),
            itemStyle: { color: "#5470c6" }
            name: '已用容量',
            type: 'bar',
            data: hasStocks.map((value, index) => ({
              value: value,
              label: {
                show: true,
                position: 'top',
                formatter: '{c} {a|' + hasStockPercentages[index] + '}',
                rich: {
                  a: {
                    lineHeight: 20,
                    borderColor: '#91cc75',
                    color: '#91cc75'
                  }
                }
              }
            })),
            itemStyle: { color: '#91cc75' }
          },
          {
            name: '剩余容量',
            type: 'bar',
            data: noStocks.map((value, index) => ({
              value: value,
              label: {
                show: true,
                position: 'top',
                formatter: '{c} {a|' + noStockPercentages[index] + '}',
                rich: {
                  a: {
                    lineHeight: 20,
                    borderColor: '#fac858',
                    color: '#fac858'
                  }
                }
              }
            })),
            itemStyle: { color: '#fac858' }
          }
        ]
      };
      window.homeComponent = this;
      this.charts.warehouse.setOption(option, true);
    }
  }
@@ -322,8 +268,10 @@
<style scoped>
.dashboard-container {
  padding: 20px;
  background-color: #0e1a2b;
  color: #e0e0e0;
  min-height: calc(100vh - 60px);
  background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
  background-attachment: fixed;
}
.chart-row {
@@ -338,11 +286,25 @@
.chart-card {
  flex: 1;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(25, 186, 139, 0.17);
  border-radius: 4px;
  background: rgba(10, 16, 35, 0.6);
  backdrop-filter: blur(10px);
  border: 1px solid rgba(64, 224, 208, 0.3);
  border-radius: 12px;
  padding: 15px;
  position: relative;
  box-shadow:
    0 0 15px rgba(0, 255, 255, 0.1),
    inset 0 0 10px rgba(64, 224, 208, 0.1);
  transition: all 0.3s ease;
  overflow: hidden;
}
.chart-card:hover {
  transform: translateY(-5px);
  box-shadow:
    0 0 25px rgba(0, 255, 255, 0.3),
    inset 0 0 15px rgba(64, 224, 208, 0.2);
  border: 1px solid rgba(64, 224, 208, 0.6);
}
.chart-card::before {
@@ -352,8 +314,9 @@
  left: 0;
  width: 10px;
  height: 10px;
  border-top: 2px solid #02a6b5;
  border-left: 2px solid #02a6b5;
  border-top: 2px solid #00ffff;
  border-left: 2px solid #00ffff;
  box-shadow: -2px -2px 10px #00ffff, 0 0 10px rgba(0, 255, 255, 0.7);
}
.chart-card::after {
@@ -363,41 +326,39 @@
  right: 0;
  width: 10px;
  height: 10px;
  border-top: 2px solid #02a6b5;
  border-right: 2px solid #02a6b5;
  border-top: 2px solid #00ffff;
  border-right: 2px solid #00ffff;
  box-shadow: 2px -2px 10px #00ffff, 0 0 10px rgba(0, 255, 255, 0.7);
}
.chart-card::before,
.chart-card::after {
  animation: neon-flicker 2s infinite alternate;
}
@keyframes neon-flicker {
  0%, 100% {
    opacity: 1;
    box-shadow: -2px -2px 10px #00ffff, 0 0 10px rgba(0, 255, 255, 0.7);
  }
  50% {
    opacity: 0.8;
    box-shadow: -2px -2px 5px #00ffff, 0 0 5px rgba(0, 255, 255, 0.5);
  }
}
.card-title {
  color: #fff;
  color: #00ffff;
  font-size: 16px;
  text-align: center;
  margin-bottom: 10px;
  text-shadow: 0 0 10px rgba(0, 255, 255, 0.7);
  font-weight: 500;
}
.chart-content {
  height: 280px;
  width: 100%;
}
.stock-total {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 280px;
}
.total-number {
  font-size: 64px;
  font-weight: bold;
  color: #67caca;
  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif;
}
.total-label {
  font-size: 18px;
  color: #fcf0d8;
  margin-top: 10px;
}
/* å…¨å®½å›¾è¡¨ */
@@ -409,4 +370,20 @@
.full-width .chart-content {
  height: 350px;
}
</style>
/* æ·»åŠ ç½‘æ ¼çº¿æ•ˆæžœ */
.dashboard-container::before {
  content: "";
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-image:
    linear-gradient(rgba(64, 224, 208, 0.05) 1px, transparent 1px),
    linear-gradient(90deg, rgba(64, 224, 208, 0.05) 1px, transparent 1px);
  background-size: 30px 30px;
  pointer-events: none;
  z-index: -1;
}
</style>
Code/WMS/WIDESEA_WMSClient/src/views/Index.vue
@@ -3,22 +3,12 @@
    <div class="vol-aside" :style="{ width: menuWidth + 'px' }">
      <div class="header" :style="{ width: menuWidth - 1 + 'px' }">
        <img v-show="!isCollapse" v-bind:src="logo" />
        <i
          @click="toggleLeft"
          class="collapse-menu"
          :class="isCollapse ? 'el-icon-s-unfold' : 'el-icon-s-fold'"
        />
        <i @click="toggleLeft" class="collapse-menu" :class="isCollapse ? 'el-icon-s-unfold' : 'el-icon-s-fold'" />
      </div>
      <div class="vol-menu">
        <el-scrollbar style="height: 100%">
          <VolMenu
            :currentMenuId="currentMenuId"
            :on-select="onSelect"
            :enable="true"
            :open-select="false"
            :isCollapse="isCollapse"
            :list="menuOptions"
          ></VolMenu>
          <VolMenu :currentMenuId="currentMenuId" :on-select="onSelect" :enable="true" :open-select="false"
            :isCollapse="isCollapse" :list="menuOptions"></VolMenu>
        </el-scrollbar>
      </div>
    </div>
@@ -27,14 +17,9 @@
        <div class="project-name">WMS</div>
        <div class="header-text">
          <div class="h-link">
            <a
              href="javascript:void(0)"
              @click="to(item)"
              v-for="(item, index) in links.filter((c) => {
                return !c.icon;
              })"
              :key="index"
            >
            <a href="javascript:void(0)" @click="to(item)" v-for="(item, index) in links.filter((c) => {
              return !c.icon;
            })" :key="index">
              <span v-if="!item.icon"> {{ item.text }}</span>
              <i v-else :class="item.icon"></i>
            </a>
@@ -42,14 +27,9 @@
        </div>
        <div class="header-info">
          <div class="h-link">
            <a
              href="javascript:void(0)"
              @click="to(item)"
              v-for="(item, index) in links.filter((c) => {
                return c.icon;
              })"
              :key="index"
            >
            <a href="javascript:void(0)" @click="to(item)" v-for="(item, index) in links.filter((c) => {
              return c.icon;
            })" :key="index">
              <span v-if="!item.icon"> {{ item.text }}</span>
              <i v-else :class="item.icon"></i>
            </a>
@@ -57,15 +37,9 @@
          <!--消息管理-->
          <div class="h-link" @click="messageModel = true">
            <a
              ><i class="el-icon-message-solid"
                ><el-badge
                  :value="messageList.length"
                  :type="messageList.length > 0 ? 'danger' : 'success'"
                  class="item"
                  style="width: 10px"
                ></el-badge></i
            ></a>
            <a><i class="el-icon-message-solid"><el-badge :value="messageList.length"
                  :type="messageList.length > 0 ? 'danger' : 'success'" class="item"
                  style="width: 10px"></el-badge></i></a>
          </div>
          <div>
            <img class="user-header" :src="userImg" :onerror="errorImg" />
@@ -75,62 +49,37 @@
            <span id="index-date"></span>
          </div>
          <div class="settings">
            <i
              style="font-size: 20px"
              class="el-icon-s-tools"
              @click="drawer_model = true"
            />
            <i style="font-size: 20px" class="el-icon-s-tools" @click="drawer_model = true" />
          </div>
        </div>
      </div>
      <div class="vol-path">
        <el-tabs
          @tab-click="selectNav"
          @tab-remove="removeNav"
          @contextmenu.prevent="bindRightClickMenu(false)"
          type="border-card"
          class="header-navigation"
          v-model="selectId"
          :strtch="false"
        >
          <el-tab-pane
            v-for="(item, navIndex) in navigation"
            type="card"
            :name="navIndex + ''"
            :closable="navIndex > 0"
            :key="navIndex"
            :label="item.name"
          >
        <el-tabs @tab-click="selectNav" @tab-remove="removeNav" @contextmenu.prevent="bindRightClickMenu(false)"
          type="border-card" class="header-navigation" v-model="selectId" :strtch="false">
          <el-tab-pane v-for="(item, navIndex) in navigation" type="card" :name="navIndex + ''" :closable="navIndex > 0"
            :key="navIndex" :label="item.name">
            <span style="display: none">{{ navIndex }}</span>
          </el-tab-pane>
        </el-tabs>
        <!-- å³é”®èœå• -->
        <div v-show="contextMenuVisible">
          <ul
            :style="{ left: menuLeft + 'px', top: menuTop + 'px' }"
            class="contextMenu"
          >
          <ul :style="{ left: menuLeft + 'px', top: menuTop + 'px' }" class="contextMenu">
            <li v-show="visibleItem.all">
              <el-button link @click="closeTabs()">
                <i class="el-icon-close"></i>
                {{
                  navigation.length == 2 ? "关闭菜单" : "关闭所有"
                }}</el-button
              >
                }}</el-button>
            </li>
            <li v-show="visibleItem.left">
              <el-button link @click="closeTabs('left')"
                ><i class="el-icon-back"></i>关闭左边</el-button
              >
              <el-button link @click="closeTabs('left')"><i class="el-icon-back"></i>关闭左边</el-button>
            </li>
            <li v-show="visibleItem.right">
              <el-button link @click="closeTabs('right')">
                <i class="el-icon-right"></i>关闭右边</el-button
              >
                <i class="el-icon-right"></i>关闭右边</el-button>
            </li>
            <li v-show="visibleItem.other">
              <el-button link @click="closeTabs('other')"
                ><i class="el-icon-right"></i>关闭其他
              <el-button link @click="closeTabs('other')"><i class="el-icon-right"></i>关闭其他
              </el-button>
            </li>
          </ul>
@@ -141,56 +90,29 @@
          <loading v-show="$store.getters.isLoading()"></loading>
          <router-view v-slot="{ Component }">
            <keep-alive>
              <component
                :is="Component"
                :key="$route.name"
                v-if="
                  !$route.meta ||
                  ($route.meta && !$route.meta.hasOwnProperty('keepAlive'))
                "
              />
              <component :is="Component" :key="$route.name" v-if="
                !$route.meta ||
                ($route.meta && !$route.meta.hasOwnProperty('keepAlive'))
              " />
            </keep-alive>
            <component
              :is="Component"
              :key="$route.name"
              v-if="$route.meta && $route.meta.hasOwnProperty('keepAlive')"
            />
            <component :is="Component" :key="$route.name"
              v-if="$route.meta && $route.meta.hasOwnProperty('keepAlive')" />
          </router-view>
        </el-scrollbar>
      </div>
    </div>
    <el-drawer
      title="选择主题"
      v-model="drawer_model"
      direction="rtl"
      destroy-on-close
    >
    <el-drawer title="选择主题" v-model="drawer_model" direction="rtl" destroy-on-close>
      <div class="theme-selector">
        <div
          @click="changeTheme(item.name)"
          class="item"
          v-for="(item, index) in theme_color"
          :key="index"
          :style="{ background: item.color }"
        >
          <div
            v-show="item.leftColor"
            :style="{ background: item.leftColor }"
            style="height: 100%; width: 20px"
            class="t-left"
          ></div>
        <div @click="changeTheme(item.name)" class="item" v-for="(item, index) in theme_color" :key="index"
          :style="{ background: item.color }">
          <div v-show="item.leftColor" :style="{ background: item.leftColor }" style="height: 100%; width: 20px"
            class="t-left"></div>
          <div class="t-right"></div>
        </div>
      </div>
    </el-drawer>
    <el-drawer
      title="消息列表"
      v-model="messageModel"
      direction="rtl"
      destroy-on-close
      size="40%"
    >
    <el-drawer title="消息列表" v-model="messageModel" direction="rtl" destroy-on-close size="40%">
      <Message :list="messageList"></Message>
    </el-drawer>
  </div>
@@ -339,7 +261,7 @@
        setTimeout(createSocket, 10000);
      };
      client.onerror = function () {};
      client.onerror = function () { };
    };
    const changeTheme = (name) => {
@@ -606,7 +528,7 @@
      }
      createSocket("ws://127.0.0.1:9296/" + _userInfo.userName);
      // createSocket("ws://127.0.0.1:9296");
      Object.assign(_config.$tabs, { open: open, close: close });
      http.get("api/Sys_Menu/getTreeMenu", {}, true).then((data) => {
@@ -792,6 +714,7 @@
  font-size: 14px;
  color: #333;
  box-shadow: 2px 2px 3px 0 rgb(182 182 182 / 20%);
  i,
  button {
    font-size: 14px !important;
@@ -814,12 +737,8 @@
  letter-spacing: 1px;
}
.el-tabs.el-tabs--top.el-tabs--border-card.header-navigation
  > .el-tabs__header
  .el-tabs__item:last-child,
.el-tabs--top.el-tabs--border-card.header-navigation
  > .el-tabs__header
  .el-tabs__item:nth-child(2) {
.el-tabs.el-tabs--top.el-tabs--border-card.header-navigation>.el-tabs__header .el-tabs__item:last-child,
.el-tabs--top.el-tabs--border-card.header-navigation>.el-tabs__header .el-tabs__item:nth-child(2) {
  padding: 0;
}
Code/WMS/WIDESEA_WMSClient/src/views/stock/stockChat.vue
@@ -217,6 +217,10 @@
}
function hasCargo(location) {
    // å¦‚æžœLocationStatus=100,即使没有库存信息也显示货位
    if (location.locationStatus === 100) {
        return true
    }
    return Number(location.stockQuantity || 0) > 0 || ((location.details && location.details.length > 0) || false)
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_Core/Extensions/WebSocketSetup.cs
@@ -11,22 +11,37 @@
{
    public static class WebSocketSetup
    {
        public static void AddWebSocketSetup(this IServiceCollection services)
        {
            if (services == null) throw new ArgumentNullException(nameof(services));
        //public static void AddWebSocketSetup(this IServiceCollection services)
        //{
        //    if (services == null) throw new ArgumentNullException(nameof(services));
            int port = AppSettings.Get("WebSocketPort").ObjToInt();
            if (port == 0)
            {
                port = 9296;
            }
            services.AddSingleton(x =>
            {
                WebSocketServer socketServer = new WebSocketServer();
                socketServer.ServerStart(port);
                return socketServer;
            });
        //    int port = AppSettings.Get("WebSocketPort").ObjToInt();
        //    if (port == 0)
        //    {
        //        port = 9296;
        //    }
        //    services.AddSingleton(x =>
        //    {
        //        WebSocketServer socketServer = new WebSocketServer();
        //        socketServer.ServerStart(port);
        //        return socketServer;
        //    });
        //}
        public static void AddWebSocketSetup(this IServiceCollection services)
        {
            if (services == null) throw new ArgumentNullException(nameof(services));
            int port = AppSettings.Get("WebSocketPort").ObjToInt();
            if (port == 0)
            {
                port = 9296;
            }
            // ç›´æŽ¥åˆ›å»ºå¹¶å¯åЍ WebSocket æœåС噍
            WebSocketServer socketServer = new WebSocketServer();
            socketServer.ServerStart(port);
            services.AddSingleton(socketServer);
        }
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService_AGV.cs
@@ -1,681 +1,678 @@
using Mapster;
using System.Text.Json;
using WIDESEA_Common.Constants;
using WIDESEA_Common.LocationEnum;
using WIDESEA_Common.StockEnum;
using WIDESEA_Common.TaskEnum;
using WIDESEA_Common.WareHouseEnum;
using WIDESEA_Core;
using WIDESEA_Core.Enums;
using WIDESEA_Core.Helper;
using WIDESEA_DTO.Task;
using WIDESEA_IStockService;
using WIDESEA_Model.Models;
namespace WIDESEA_TaskInfoService
{
    public partial class TaskService
    {
        public string AGV_OutTaskComplete = WIDESEA_Core.Helper.AppSettings.Configuration["AGV_OutTaskComplete"];
        public string WCS_ReceiveTask = WIDESEA_Core.Helper.AppSettings.Configuration["WCS_ReceiveTask"];
        /// <summary>
        /// æžå·åº“出入库申请
        /// </summary>
        public async Task<AGVResponse> ApplyInOutAsync(ApplyInOutDto applyInOutDto)
        {
            AGVResponse response = new AGVResponse();
            try
            {
                var validationMessage = ValidateApplyInOutRequest(applyInOutDto);
                if (validationMessage != null)
                    return response.Error(validationMessage);
                var existingTask = await BaseDal.QueryFirstAsync(x => x.PalletCode == applyInOutDto.TrayNumber || x.OrderNo == applyInOutDto.TaskId);
                if (existingTask != null)
                    return response.Error($"WMS已有当前任务,不可重复下发,任务号:{applyInOutDto.TaskId}");
                var task = BuildAgvTask(applyInOutDto);
                if (applyInOutDto.InOut == 1)
                {
                    var inboundResult = await CreateAgvInboundTaskAsync(task, applyInOutDto);
                    if (inboundResult != null)
                        return inboundResult;
                }
                else
                {
                    var outboundResult = await CreateAgvOutboundTaskAsync(task, applyInOutDto);
                    if (outboundResult != null)
                        return outboundResult;
                }
                return response.OK(BuildAgvDataDto(task, applyInOutDto));
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error($"WMS任务创建接口错误: {ex.Message}");
            }
        }
        /// <summary>
        /// æ‰‹åŠ¨å‡ºåº“å®Œæˆåé¦ˆç»™AGV
        /// </summary>
        public async Task<WebResponseContent> OutTaskComplete(OutTaskCompleteDto outTaskCompleteDto)
        {
            WebResponseContent response = new WebResponseContent();
            try
            {
                var validationMessage = ValidateOutTaskCompleteRequest(outTaskCompleteDto);
                if (validationMessage != null)
                    return response.Error(validationMessage);
                var task = await BaseDal.QueryFirstAsync(x => x.OrderNo == outTaskCompleteDto.TaskId);
                if (task == null)
                    return response.Error("未找到任务信息");
                outTaskCompleteDto.ReqTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
                var httpResponse = _httpClientHelper.Post<AGVResponse>(AGV_OutTaskComplete, outTaskCompleteDto.ToJson()).Data;
                if (httpResponse == null || httpResponse.Data == null)
                    return response.Error(httpResponse?.Msg ?? "AGV接口调用异常");
                if (!httpResponse.Code)
                        return response.Error(string.IsNullOrWhiteSpace(httpResponse.Msg) ? "AGV接口调用失败" : httpResponse.Msg);
                var syncResult = await CompleteLocalOutboundAfterAgvAckAsync(task);
                return syncResult.Status ? response.OK(httpResponse.Msg) : syncResult;
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error($"WMS任务完成接口错误:{ex.Message}");
            }
        }
        /// <summary>
        /// è¾“送线申请进入
        /// </summary>
        public async Task<AGVResponse> ApplyEnterAsync(ApplyEnterDto applyEnterDto)
        {
            AGVResponse response = new AGVResponse();
            try
            {
                var validationMessage = ValidateApplyEnterRequest(applyEnterDto);
                if (validationMessage != null)
                    return response.Error(validationMessage);
                var task = await BaseDal.QueryFirstAsync(x => x.OrderNo == applyEnterDto.TaskId);
                if (task == null)
                    return response.Error($"未找到任务信息,任务号:{applyEnterDto.TaskId}");
                if (CanApplyEnter(task, applyEnterDto))
                    return response.OK();
                return response.Error($"输送线{applyEnterDto.DevId}当前繁忙,请稍后重试");
            }
            catch (Exception ex)
            {
                return response.Error($"WMS输送线申请接口错误:{ex.Message}");
            }
        }
        /// <summary>
        /// å–放货完成
        /// </summary>
        public async Task<AGVResponse> TaskCompleteAsync(TaskCompleteDto taskCompleteDto)
        {
            AGVResponse response = new AGVResponse();
            try
            {
                var validationMessage = ValidateTaskCompleteRequest(taskCompleteDto);
                if (validationMessage != null)
                    return response.Error(validationMessage);
                var task = await BaseDal.QueryFirstAsync(x => x.OrderNo == taskCompleteDto.TaskId);
                if (task == null)
                    return response.Error($"未找到任务信息,任务号:{taskCompleteDto.TaskId}");
                return taskCompleteDto.InOut == 2
                    ? await CompleteAgvOutboundTaskAsync(task)
                    : await CompleteAgvInboundTaskAsync(task);
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error($"WMS取放货完成接口错误:{ex.Message}");
            }
        }
        /// <summary>
        /// ä»»åŠ¡å–æ¶ˆ
        /// </summary>
        public async Task<AGVResponse> TaskCancelAsync(TaskCancelDto taskCancelDto)
        {
            AGVResponse response = new AGVResponse();
            try
            {
                var validationMessage = ValidateTaskCancelRequest(taskCancelDto);
                if (validationMessage != null)
                    return response.Error(validationMessage);
                var task = await BaseDal.QueryFirstAsync(x => x.OrderNo == taskCancelDto.TaskId);
                if (task == null)
                    return response.OK();
                if (task.TaskStatus == (int)TaskInStatusEnum.InNew)
                    return await CancelAgvInboundTask(task);
                if (task.TaskStatus == (int)TaskOutStatusEnum.OutNew)
                    return await CancelAgvOutboundTaskAsync(task);
                return response.Error("任务已经在执行中,不可取消");
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error($"WMS任务取消接口错误:{ex.Message}");
            }
        }
        #region å‚数验证
        private static string? ValidateApplyInOutRequest(ApplyInOutDto dto)
        {
            if (dto == null) return "请求参数不能为空";
            if (string.IsNullOrWhiteSpace(dto.TrayNumber)) return "托盘号不能为空";
            if (string.IsNullOrWhiteSpace(dto.TaskId)) return "任务号不能为空";
            if (string.IsNullOrWhiteSpace(dto.MaterialType)) return "物料类型不能为空";
            if (string.IsNullOrWhiteSpace(dto.MaterialName)) return "物料描述不能为空";
            if (string.IsNullOrWhiteSpace(dto.ReqTime)) return "请求时间不能为空";
            if (dto.Floor != 1 && dto.Floor != 2) return $"楼层段错误,必须为1(模切段)或2(卷绕段),当前值:{dto.Floor}";
            if (dto.YinYang != 1 && dto.YinYang != 2) return $"阴阳极错误,必须为1(阴极)或2(阳极),当前值:{dto.YinYang}";
            if (dto.InOut != 1 && dto.InOut != 2) return $"出入库类型错误,必须为1(入库)或2(出库),当前值:{dto.InOut}";
            if (dto.InOut == 1 && (dto.Width == null || dto.Width <= 0)) return "入库时宽度不能为空且必须大于0";
            if (dto.InOut == 1 && string.IsNullOrWhiteSpace(dto.Group)) return "入库时整托组别不能为空";
            return null;
        }
        private static string? ValidateOutTaskCompleteRequest(OutTaskCompleteDto dto)
        {
            if (dto == null) return "请求参数不能为空";
            if (string.IsNullOrWhiteSpace(dto.TaskId)) return "任务号不能为空";
            if (string.IsNullOrWhiteSpace(dto.DevId)) return "出库口编号不能为空";
            return null;
        }
        private static string? ValidateApplyEnterRequest(ApplyEnterDto dto)
        {
            if (dto == null) return "请求参数不能为空";
            if (string.IsNullOrWhiteSpace(dto.DevId)) return "设备编号不能为空";
            if (string.IsNullOrWhiteSpace(dto.TaskId)) return "任务号不能为空";
            if (dto.InOut != 1 && dto.InOut != 2) return $"出入库类型错误,必须为1(入库)或2(出库),当前值:{dto.InOut}";
            return null;
        }
        private static string? ValidateTaskCompleteRequest(TaskCompleteDto dto)
        {
            if (dto == null) return "请求参数不能为空";
            if (string.IsNullOrWhiteSpace(dto.TaskId)) return "任务号不能为空";
            if (string.IsNullOrWhiteSpace(dto.DevId)) return "设备编号不能为空";
            if (dto.InOut != 1 && dto.InOut != 2) return $"出入库类型错误,必须为1(入库)或2(出库),当前值:{dto.InOut}";
            return null;
        }
        private static string? ValidateTaskCancelRequest(TaskCancelDto dto)
        {
            if (dto == null) return "请求参数不能为空";
            if (string.IsNullOrWhiteSpace(dto.TaskId)) return "任务号不能为空";
            return null;
        }
        #endregion å‚数验证
        #region å…·ä½“实现
        // å‡ºå…¥åº“共用创建任务
        private Dt_Task BuildAgvTask(ApplyInOutDto dto)
        {
            var task = new Dt_Task
            {
                OrderNo = dto.TaskId,
                PalletCode = dto.TrayNumber,
                PalletType = dto.Floor,
                Grade = 1,
                Creater = "AGV",
                CreateDate = DateTime.Now,
                Remark = $"物料类型:{dto.MaterialType},物料描述:{dto.MaterialName}"
            };
            if (dto.YinYang == 1)
            {
                task.Roadway = WarehouseEnum.FJ1.ToString();
                task.WarehouseId = (int)WarehouseEnum.FJ1;
                task.SourceAddress = dto.InOut == 1 ? "D10010" : "D10020";
                task.NextAddress = "D10080";
                task.TargetAddress = "阴极卷库";
            }
            else
            {
                task.Roadway = WarehouseEnum.ZJ1.ToString();
                task.WarehouseId = (int)WarehouseEnum.ZJ1;
                task.SourceAddress = dto.InOut == 1 ? "D10100" : "D10090";
                task.NextAddress = "D10160";
                task.TargetAddress = "正极卷库";
            }
            return task;
        }
        // æž„建返回AGV出入库请求体
        private AGVDataDto BuildAgvDataDto(Dt_Task task, ApplyInOutDto dto)
        {
            return new AGVDataDto
            {
                DevId = dto.InOut == 1 ? task.SourceAddress : task.TargetAddress,
                TrayNumber = task.PalletCode,
                Group = dto.Group,
                Width = dto.Width ?? 0,
                LabelNumber = dto.LabelNumber,
                ProductNo = dto.ProductNo,
                ProductName = dto.ProductName,
                Quantity = dto.Quantity,
                UomCode = dto.UomCode,
                ProductType = dto.ProductType,
                Equipment = dto.Equipment,
                ProductionDate = dto.ProductionDate,
                LowerLimitTime = dto.LowerLimitTime,
                WarningTime = dto.WarningTime,
                OverdueTime = dto.OverdueTime
            };
        }
        // å…¥åº“创建
        private async Task<AGVResponse?> CreateAgvInboundTaskAsync(Dt_Task task, ApplyInOutDto dto)
        {
            AGVResponse response = new AGVResponse();
            var stockInfo = await _stockInfoService.GetStockInfoAsync(dto.TrayNumber);
            if (stockInfo != null)
                return response.Error($"当前托盘{dto.TrayNumber}已经入库了");
            // åˆ›å»ºåº“存明细
            var details = new Dt_StockInfoDetail
            {
                MaterielCode = dto.ProductNo,
                MaterielName = dto.ProductName,
                StockQuantity = int.TryParse(dto.Quantity, out int quantity) ? quantity : 0,
                Unit = dto.UomCode,
                OrderNo = dto.ProductNo,
                ProductionDate =dto.ProductionDate,
                EffectiveDate = dto.LowerLimitTime,
                SerialNumber = dto.TrayNumber,
                Status = (int)StockStatusEmun.入库确认,
                InboundOrderRowNo = 1,
                Creater = StockConstants.AGV_USER,
                CreateDate = DateTime.Now,
                Remark = $"AGV入库任务创建,任务号:{dto.TaskId}"
            };
            // åˆ›å»ºåº“存主记录
            var stock = new Dt_StockInfo
            {
                PalletCode = dto.TrayNumber,
                PalletType = dto.Floor,
                WarehouseId = dto.YinYang == 1 ? (int)WarehouseEnum.FJ1 : (int)WarehouseEnum.ZJ1,
                StockStatus = (int)StockStatusEmun.入库确认,
                Creater = StockConstants.AGV_USER,
                CreateDate = DateTime.Now,
                Remark = $"AGV入库任务创建,任务号:{dto.TaskId}",
                Details = new List<Dt_StockInfoDetail> { details }
            };
            task.TaskType = (int)TaskInboundTypeEnum.Inbound;
            task.TaskStatus = (int)TaskInStatusEnum.InNew;
            task.CurrentAddress = task.SourceAddress;
            _unitOfWorkManage.BeginTran();
            try
            {
                // å…ˆåˆ›å»ºä»»åŠ¡
                var taskResult = await BaseDal.AddDataAsync(task) > 0;
                if (!taskResult)
                {
                    _unitOfWorkManage.RollbackTran();
                    return response.Error("入库任务创建失败");
using Mapster;
using System.Text.Json;
using WIDESEA_Common.Constants;
using WIDESEA_Common.LocationEnum;
using WIDESEA_Common.StockEnum;
using WIDESEA_Common.TaskEnum;
using WIDESEA_Common.WareHouseEnum;
using WIDESEA_Core;
using WIDESEA_Core.Enums;
using WIDESEA_Core.Helper;
using WIDESEA_DTO.Task;
using WIDESEA_IStockService;
using WIDESEA_Model.Models;
namespace WIDESEA_TaskInfoService
{
    public partial class TaskService
    {
        public string AGV_OutTaskComplete = WIDESEA_Core.Helper.AppSettings.Configuration["AGV_OutTaskComplete"];
        public string WCS_ReceiveTask = WIDESEA_Core.Helper.AppSettings.Configuration["WCS_ReceiveTask"];
        /// <summary>
        /// æžå·åº“出入库申请
        /// </summary>
        public async Task<AGVResponse> ApplyInOutAsync(ApplyInOutDto applyInOutDto)
        {
            AGVResponse response = new AGVResponse();
            try
            {
                var validationMessage = ValidateApplyInOutRequest(applyInOutDto);
                if (validationMessage != null)
                    return response.Error(validationMessage);
                var existingTask = await BaseDal.QueryFirstAsync(x => x.PalletCode == applyInOutDto.TrayNumber || x.OrderNo == applyInOutDto.TaskId);
                if (existingTask != null)
                    return response.Error($"WMS已有当前任务,不可重复下发,任务号:{applyInOutDto.TaskId}");
                var task = BuildAgvTask(applyInOutDto);
                if (applyInOutDto.InOut == 1)
                {
                    var inboundResult = await CreateAgvInboundTaskAsync(task, applyInOutDto);
                    if (inboundResult != null)
                        return inboundResult;
                }
                else
                {
                    var outboundResult = await CreateAgvOutboundTaskAsync(task, applyInOutDto);
                    if (outboundResult != null)
                        return outboundResult;
                }
                return response.OK(BuildAgvDataDto(task, applyInOutDto));
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error($"WMS任务创建接口错误: {ex.Message}");
            }
        }
        /// <summary>
        /// æ‰‹åŠ¨å‡ºåº“å®Œæˆåé¦ˆç»™AGV
        /// </summary>
        public async Task<WebResponseContent> OutTaskComplete(OutTaskCompleteDto outTaskCompleteDto)
        {
            WebResponseContent response = new WebResponseContent();
            try
            {
                var validationMessage = ValidateOutTaskCompleteRequest(outTaskCompleteDto);
                if (validationMessage != null)
                    return response.Error(validationMessage);
                var task = await BaseDal.QueryFirstAsync(x => x.OrderNo == outTaskCompleteDto.TaskId);
                if (task == null)
                    return response.Error("未找到任务信息");
                outTaskCompleteDto.ReqTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
                var httpResponse = _httpClientHelper.Post<AGVResponse>(AGV_OutTaskComplete, outTaskCompleteDto.ToJson()).Data;
                if (httpResponse == null || httpResponse.Data == null)
                    return response.Error(httpResponse?.Msg ?? "AGV接口调用异常");
                if (!httpResponse.Code)
                        return response.Error(string.IsNullOrWhiteSpace(httpResponse.Msg) ? "AGV接口调用失败" : httpResponse.Msg);
                var syncResult = await CompleteLocalOutboundAfterAgvAckAsync(task);
                return syncResult.Status ? response.OK(httpResponse.Msg) : syncResult;
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error($"WMS任务完成接口错误:{ex.Message}");
            }
        }
        /// <summary>
        /// è¾“送线申请进入
        /// </summary>
        public async Task<AGVResponse> ApplyEnterAsync(ApplyEnterDto applyEnterDto)
        {
            AGVResponse response = new AGVResponse();
            try
            {
                var validationMessage = ValidateApplyEnterRequest(applyEnterDto);
                if (validationMessage != null)
                    return response.Error(validationMessage);
                var task = await BaseDal.QueryFirstAsync(x => x.OrderNo == applyEnterDto.TaskId);
                if (task == null)
                    return response.Error($"未找到任务信息,任务号:{applyEnterDto.TaskId}");
                if (CanApplyEnter(task, applyEnterDto))
                    return response.OK();
                return response.Error($"输送线{applyEnterDto.DevId}当前繁忙,请稍后重试");
            }
            catch (Exception ex)
            {
                return response.Error($"WMS输送线申请接口错误:{ex.Message}");
            }
        }
        /// <summary>
        /// å–放货完成
        /// </summary>
        public async Task<AGVResponse> TaskCompleteAsync(TaskCompleteDto taskCompleteDto)
        {
            AGVResponse response = new AGVResponse();
            try
            {
                var validationMessage = ValidateTaskCompleteRequest(taskCompleteDto);
                if (validationMessage != null)
                    return response.Error(validationMessage);
                var task = await BaseDal.QueryFirstAsync(x => x.OrderNo == taskCompleteDto.TaskId);
                if (task == null)
                    return response.Error($"未找到任务信息,任务号:{taskCompleteDto.TaskId}");
                return taskCompleteDto.InOut == 2
                    ? await CompleteAgvOutboundTaskAsync(task)
                    : await CompleteAgvInboundTaskAsync(task);
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error($"WMS取放货完成接口错误:{ex.Message}");
            }
        }
        /// <summary>
        /// ä»»åŠ¡å–æ¶ˆ
        /// </summary>
        public async Task<AGVResponse> TaskCancelAsync(TaskCancelDto taskCancelDto)
        {
            AGVResponse response = new AGVResponse();
            try
            {
                var validationMessage = ValidateTaskCancelRequest(taskCancelDto);
                if (validationMessage != null)
                    return response.Error(validationMessage);
                var task = await BaseDal.QueryFirstAsync(x => x.OrderNo == taskCancelDto.TaskId);
                if (task == null)
                    return response.OK();
                if (task.TaskStatus == (int)TaskInStatusEnum.InNew)
                    return await CancelAgvInboundTask(task);
                if (task.TaskStatus == (int)TaskOutStatusEnum.OutNew)
                    return await CancelAgvOutboundTaskAsync(task);
                return response.Error("任务已经在执行中,不可取消");
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error($"WMS任务取消接口错误:{ex.Message}");
            }
        }
        #region å‚数验证
        private static string? ValidateApplyInOutRequest(ApplyInOutDto dto)
        {
            if (dto == null) return "请求参数不能为空";
            if (string.IsNullOrWhiteSpace(dto.TrayNumber)) return "托盘号不能为空";
            if (string.IsNullOrWhiteSpace(dto.TaskId)) return "任务号不能为空";
            if (string.IsNullOrWhiteSpace(dto.MaterialType)) return "物料类型不能为空";
            if (string.IsNullOrWhiteSpace(dto.MaterialName)) return "物料描述不能为空";
            if (string.IsNullOrWhiteSpace(dto.ReqTime)) return "请求时间不能为空";
            if (dto.Floor != 1 && dto.Floor != 2) return $"楼层段错误,必须为1(模切段)或2(卷绕段),当前值:{dto.Floor}";
            if (dto.YinYang != 1 && dto.YinYang != 2) return $"阴阳极错误,必须为1(阴极)或2(阳极),当前值:{dto.YinYang}";
            if (dto.InOut != 1 && dto.InOut != 2) return $"出入库类型错误,必须为1(入库)或2(出库),当前值:{dto.InOut}";
            if (dto.InOut == 1 && (dto.Width == null || dto.Width <= 0)) return "入库时宽度不能为空且必须大于0";
            if (dto.InOut == 1 && string.IsNullOrWhiteSpace(dto.Group)) return "入库时整托组别不能为空";
            return null;
        }
        private static string? ValidateOutTaskCompleteRequest(OutTaskCompleteDto dto)
        {
            if (dto == null) return "请求参数不能为空";
            if (string.IsNullOrWhiteSpace(dto.TaskId)) return "任务号不能为空";
            if (string.IsNullOrWhiteSpace(dto.DevId)) return "出库口编号不能为空";
            return null;
        }
        private static string? ValidateApplyEnterRequest(ApplyEnterDto dto)
        {
            if (dto == null) return "请求参数不能为空";
            if (string.IsNullOrWhiteSpace(dto.DevId)) return "设备编号不能为空";
            if (string.IsNullOrWhiteSpace(dto.TaskId)) return "任务号不能为空";
            if (dto.InOut != 1 && dto.InOut != 2) return $"出入库类型错误,必须为1(入库)或2(出库),当前值:{dto.InOut}";
            return null;
        }
        private static string? ValidateTaskCompleteRequest(TaskCompleteDto dto)
        {
            if (dto == null) return "请求参数不能为空";
            if (string.IsNullOrWhiteSpace(dto.TaskId)) return "任务号不能为空";
            if (string.IsNullOrWhiteSpace(dto.DevId)) return "设备编号不能为空";
            if (dto.InOut != 1 && dto.InOut != 2) return $"出入库类型错误,必须为1(入库)或2(出库),当前值:{dto.InOut}";
            return null;
        }
        private static string? ValidateTaskCancelRequest(TaskCancelDto dto)
        {
            if (dto == null) return "请求参数不能为空";
            if (string.IsNullOrWhiteSpace(dto.TaskId)) return "任务号不能为空";
            return null;
        }
        #endregion å‚数验证
        #region å…·ä½“实现
        // å‡ºå…¥åº“共用创建任务
        private Dt_Task BuildAgvTask(ApplyInOutDto dto)
        {
            var task = new Dt_Task
            {
                OrderNo = dto.TaskId,
                PalletCode = dto.TrayNumber,
                PalletType = dto.Floor,
                Grade = 1,
                Creater = "AGV",
                CreateDate = DateTime.Now,
                Remark = $"物料类型:{dto.MaterialType},物料描述:{dto.MaterialName}"
            };
            if (dto.YinYang == 1)
            {
                task.Roadway = WarehouseEnum.FJ1.ToString();
                task.WarehouseId = (int)WarehouseEnum.FJ1;
                task.SourceAddress = dto.InOut == 1 ? "D10010" : "D10020";
                task.NextAddress = "D10080";
                task.TargetAddress = "阴极卷库";
            }
            else
            {
                task.Roadway = WarehouseEnum.ZJ1.ToString();
                task.WarehouseId = (int)WarehouseEnum.ZJ1;
                task.SourceAddress = dto.InOut == 1 ? "D10100" : "D10090";
                task.NextAddress = "D10160";
                task.TargetAddress = "正极卷库";
            }
            return task;
        }
        // æž„建返回AGV出入库请求体
        private AGVDataDto BuildAgvDataDto(Dt_Task task, ApplyInOutDto dto)
        {
            return new AGVDataDto
            {
                DevId = dto.InOut == 1 ? task.SourceAddress : task.TargetAddress,
                TrayNumber = task.PalletCode,
                Group = dto.Group,
                Width = dto.Width ?? 0,
                LabelNumber = dto.LabelNumber,
                ProductNo = dto.ProductNo,
                ProductName = dto.ProductName,
                Quantity = dto.Quantity,
                UomCode = dto.UomCode,
                ProductType = dto.ProductType,
                Equipment = dto.Equipment,
                ProductionDate = dto.ProductionDate,
                LowerLimitTime = dto.LowerLimitTime,
                WarningTime = dto.WarningTime,
                OverdueTime = dto.OverdueTime
            };
        }
        // å…¥åº“创建
        private async Task<AGVResponse?> CreateAgvInboundTaskAsync(Dt_Task task, ApplyInOutDto dto)
        {
            AGVResponse response = new AGVResponse();
            var stockInfo = await _stockInfoService.GetStockInfoAsync(dto.TrayNumber);
            if (stockInfo != null)
                return response.Error($"当前托盘{dto.TrayNumber}已经入库了");
            // åˆ›å»ºåº“存明细
            var details = new Dt_StockInfoDetail
            {
                MaterielCode = dto.ProductNo,
                MaterielName = dto.ProductName,
                StockQuantity = int.TryParse(dto.Quantity, out int quantity) ? quantity : 0,
                Unit = dto.UomCode,
                OrderNo = dto.ProductNo,
                ProductionDate =dto.ProductionDate,
                EffectiveDate = dto.LowerLimitTime,
                SerialNumber = dto.TrayNumber,
                Status = (int)StockStatusEmun.入库确认,
                InboundOrderRowNo = 1,
                Creater = StockConstants.AGV_USER,
                CreateDate = DateTime.Now,
                Remark = $"AGV入库任务创建,任务号:{dto.TaskId}"
            };
            // åˆ›å»ºåº“存主记录
            var stock = new Dt_StockInfo
            {
                PalletCode = dto.TrayNumber,
                PalletType = dto.Floor,
                WarehouseId = dto.YinYang == 1 ? (int)WarehouseEnum.FJ1 : (int)WarehouseEnum.ZJ1,
                StockStatus = (int)StockStatusEmun.入库确认,
                Creater = StockConstants.AGV_USER,
                CreateDate = DateTime.Now,
                Remark = $"AGV入库任务创建,任务号:{dto.TaskId}",
                Details = new List<Dt_StockInfoDetail> { details }
            };
            task.TaskType = (int)TaskInboundTypeEnum.Inbound;
            task.TaskStatus = (int)TaskInStatusEnum.InNew;
            task.CurrentAddress = task.SourceAddress;
            _unitOfWorkManage.BeginTran();
            try
            {
                // å…ˆåˆ›å»ºä»»åŠ¡
                var taskResult = await BaseDal.AddDataAsync(task) > 0;
                if (!taskResult)
                {
                    _unitOfWorkManage.RollbackTran();
                    return response.Error("入库任务创建失败");
                }
                var result = _stockInfoService.Repository.AddData(stock, x => x.Details);
                if (result)
                {
                    _unitOfWorkManage.CommitTran();
                    return null;
                {
                    _unitOfWorkManage.CommitTran();
                    return null;
                }
                else
                {
                    _unitOfWorkManage.RollbackTran();
                    return response.Error("库存信息创建失败");
                else
                {
                    _unitOfWorkManage.RollbackTran();
                    return response.Error("库存信息创建失败");
                }
                // ä½¿ç”¨åº“存服务添加库存主记录和明细
                //var stockResult = await _stockInfoService.AddStockWithDetailsUsingTransactionAsync(stock);
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error($"入库任务创建异常:{ex.Message}");
            }
        }
        // å‡ºåº“创建
        private async Task<AGVResponse?> CreateAgvOutboundTaskAsync(Dt_Task task, ApplyInOutDto dto)
        {
            AGVResponse response = new AGVResponse();
            // æ£€æŸ¥åº“存是否存在
            var stockInfo = await _stockInfoService.GetStockInfoAsync(dto.TrayNumber);
            if (stockInfo == null)
                return response.Error($"未找到托盘{dto.TrayNumber}的库存信息");
            // æ£€æŸ¥åº“存是否有明细(即是否真的有库存)
            if (stockInfo.Details == null || !stockInfo.Details.Any())
                return response.Error($"托盘{dto.TrayNumber}没有库存明细,无法出库");
            // æ£€æŸ¥åº“存总数量是否大于0
            var totalQuantity = stockInfo.Details.Sum(d => d.StockQuantity);
            if (totalQuantity <= 0)
                return response.Error($"托盘{dto.TrayNumber}库存数量不足,无法出库");
            // æ ¹æ®dto参数进一步验证库存信息
            if (!string.IsNullOrEmpty(dto.ProductNo))
            {
                // æ£€æŸ¥åº“存明细中是否包含指定的物料编码
                var hasMatchingMaterial = stockInfo.Details.Any(d => d.MaterielCode == dto.ProductNo);
                if (!hasMatchingMaterial)
                    return response.Error($"托盘{dto.TrayNumber}中没有物料编码为{dto.ProductNo}的库存,无法出库");
            }
            // æ£€æŸ¥åº“存状态是否允许出库
            if (stockInfo.StockStatus != (int)StockStatusEmun.入库完成)
                return response.Error($"托盘{dto.TrayNumber}正在移动中,请稍后!");
            // æ£€æŸ¥è´§ä½ä¿¡æ¯
            var locationInfo = await _locationInfoService.GetLocationInfo(stockInfo.LocationCode);
            if (locationInfo == null)
                return response.Error($"未找到托盘{stockInfo.LocationCode}的货位信息");
            // æ£€æŸ¥è´§ä½çŠ¶æ€æ˜¯å¦å…è®¸å‡ºåº“
            if (locationInfo.LocationStatus != (int)LocationStatusEnum.InStock)
                return response.Error($"当前货位{locationInfo.LocationStatus}状态信息错误");
            // éªŒè¯ä»“库ID是否匹配(根据dto的阴阳极参数)
            var expectedWarehouseId = dto.YinYang == 1 ? (int)WarehouseEnum.FJ1 : (int)WarehouseEnum.ZJ1;
            if (stockInfo.WarehouseId != expectedWarehouseId)
                return response.Error($"托盘{dto.TrayNumber}不在预期的仓库中,无法出库");
            task.TaskType = (int)TaskOutboundTypeEnum.Outbound;
            task.TaskStatus = (int)TaskOutStatusEnum.OutNew;
            task.SourceAddress = stockInfo.LocationCode;
            task.CurrentAddress = stockInfo.LocationCode;
            task.TargetAddress = dto.YinYang == 1 ? "D10020" : "D10090";
            var wmsTaskDto = _mapper.Map<WMSTaskDTO>(task);
            var taskList = new List<WMSTaskDTO> { wmsTaskDto };
            var requestBody = JsonSerializer.Serialize(taskList);
            var httpResponse = _httpClientHelper.Post<WebResponseContent>(WCS_ReceiveTask, requestBody);
            if (httpResponse == null || httpResponse.Data == null || !httpResponse.Data.Status)
                return response.Error(httpResponse?.Data?.Message ?? "下发WCS失败");
            stockInfo.StockStatus = (int)StockStatusEmun.出库锁定;
            locationInfo.LocationStatus = (int)LocationStatusEnum.InStockLock;
            _unitOfWorkManage.BeginTran();
            var addTaskResult = await BaseDal.AddDataAsync(task) > 0;
            var updateLocationResult = await _locationInfoService.UpdateLocationInfoAsync(locationInfo);
            var updateStockResult = await _stockInfoService.UpdateStockAsync(stockInfo);
            if (!addTaskResult || !updateLocationResult || !updateStockResult)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error("出库任务创建失败");
            }
            _unitOfWorkManage.CommitTran();
            return null;
        }
        private async Task<WebResponseContent> CompleteLocalOutboundAfterAgvAckAsync(Dt_Task task)
        {
            task.TaskStatus = (int)TaskOutStatusEnum.Line_OutFinish;
            _unitOfWorkManage.BeginTran();
            var updateResult = BaseDal.UpdateData(task);
            if (!updateResult)
            {
                _unitOfWorkManage.RollbackTran();
                return WebResponseContent.Instance.Error("AGV完成回传后,任务更新失败");
            }
            _unitOfWorkManage.CommitTran();
            return WebResponseContent.Instance.OK();
        }
        private bool CanApplyEnter(Dt_Task task, ApplyEnterDto dto)
        {
            if (dto.InOut == 1)
            {
                var hasExecutingOutTask = BaseDal.QueryFirst(x => x.TaskType == (int)TaskOutboundTypeEnum.Outbound
                    && x.TargetAddress == task.SourceAddress
                    && (x.TaskStatus == (int)TaskOutStatusEnum.SC_OutExecuting
                        || x.TaskStatus == (int)TaskOutStatusEnum.Line_OutExecuting));
                // å¦‚果没有正在执行的出库任务,则允许入库
                return hasExecutingOutTask == null;
            }
            else
            {
                return task.TaskType == (int)TaskOutboundTypeEnum.Outbound
                    && task.TaskStatus == (int)TaskStatusEnum.Line_Finish;
            }
        }
        // WCS入库完成
        private async Task<WebResponseContent> CompleteAgvInboundTaskAsync(CreateTaskDto taskDto)
        {
            WebResponseContent response = new WebResponseContent();
            var task = await BaseDal.QueryFirstAsync(x => x.PalletType == taskDto.PalletType);
            if (task == null)
                return response.Error($"没有当前托盘{taskDto.PalletType}入库任务");
            var stockInfo = await _stockInfoService.GetStockInfoAsync(task.PalletCode);
            if (stockInfo == null)
                return response.Error($"未找到托盘{task.PalletCode}的库存信息");
            var locationInfo = await _locationInfoService.GetLocationInfoAsync(task.TargetAddress);
            if (locationInfo == null)
                return response.Error($"未找到货位{task.TargetAddress}的信息");
            if (locationInfo.LocationStatus == (int)LocationStatusEnum.InStock)
                return response.Error($"当前货位{locationInfo.LocationStatus}状态不是空闲状态,无法入库");
            // æ›´æ–°è´§ä½çŠ¶æ€ä¸ºå ç”¨
            locationInfo.LocationStatus = (int)LocationStatusEnum.InStock;
            task.TaskStatus = (int)TaskInStatusEnum.InFinish;
            stockInfo.StockStatus = (int)StockStatusEmun.入库完成;
            _unitOfWorkManage.BeginTran();
            var addStockResult = _stockInfoService.UpdateData(stockInfo);
            var updateLocationResult = _locationInfoService.UpdateData(locationInfo);
            BaseDal.DeleteAndMoveIntoHty(task, App.User.UserId == 0 ? OperateTypeEnum.自动完成 : OperateTypeEnum.人工完成);
            if (!addStockResult.Status || !updateLocationResult.Status)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error("入库完成后,添加库存或货位更新失败");
            }
            _unitOfWorkManage.CommitTran();
            return response.OK();
        }
        // AGV出库完成
        private async Task<AGVResponse> CompleteAgvOutboundTaskAsync(Dt_Task task)
        {
            AGVResponse response = new AGVResponse();
            var stockInfo = await _stockInfoService.GetStockInfoAsync(task.PalletCode);
            if (stockInfo == null)
                return response.Error($"未找到托盘{task.PalletCode}的库存信息");
            var locationInfo = await _locationInfoService.GetLocationInfoAsync(stockInfo.LocationCode);
            if (locationInfo == null)
                return response.Error($"未找到托盘{stockInfo.LocationCode}的货位信息");
            if (stockInfo.StockStatus != (int)StockStatusEmun.出库锁定 || locationInfo.LocationStatus != (int)LocationStatusEnum.InStockLock)
                return response.Error($"当前库存{stockInfo.StockStatus}或者货位{locationInfo.LocationStatus}状态信息错误");
            locationInfo.LocationStatus = (int)LocationStatusEnum.Free;
            task.TaskStatus = (int)TaskOutStatusEnum.OutFinish;
            _unitOfWorkManage.BeginTran();
            //var deleteStockResult = _stockInfoService.DeleteData(stockInfo)
            var deleteStockResult = await _stockInfoService.DeleteStockWithDetailsAsync(stockInfo.Id);
            var updateLocationResult = _locationInfoService.UpdateData(locationInfo);
            BaseDal.DeleteAndMoveIntoHty(task, App.User.UserId == 0 ? OperateTypeEnum.自动完成 : OperateTypeEnum.人工完成);
            if (!deleteStockResult.Status || !updateLocationResult.Status)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error("出库完成后,本地库存或货位更新失败");
            }
            _unitOfWorkManage.CommitTran();
            return response.OK();
        }
        // AGV已放货,准备输送线入库
        private async Task<AGVResponse> CompleteAgvInboundTaskAsync(Dt_Task task)
        {
            AGVResponse response = new AGVResponse();
            var availableLocation = await _locationInfoService.GetLocationInfo(task.Roadway);
            if (availableLocation == null)
                return response.Error("无可用的入库货位");
            task.TargetAddress = availableLocation.LocationCode;
            var wmsTaskDto = _mapper.Map<WMSTaskDTO>(task);
            var taskList = new List<WMSTaskDTO> { wmsTaskDto };
            var requestBody = JsonSerializer.Serialize(taskList);
            var httpResponse = _httpClientHelper.Post<WebResponseContent>(WCS_ReceiveTask, requestBody);
            if (httpResponse == null || httpResponse.Data == null || !httpResponse.Data.Status)
                return response.Error(httpResponse?.Data?.Message ?? "下发WCS失败");
            task.TaskStatus = (int)TaskInStatusEnum.Line_InExecuting;
            task.Dispatchertime = DateTime.Now;
            var locationInfo = await _locationInfoService.GetLocationInfoAsync(task.TargetAddress);
            if (locationInfo == null)
                return response.Error($"未找到托盘{task.TargetAddress}的货位信息");
            if (locationInfo.LocationStatus != (int)LocationStatusEnum.Free)
                return response.Error($"当前货位{locationInfo.LocationStatus}状态信息错误");
            var existingStock = await _stockInfoService.GetStockInfoAsync(task.PalletCode);
            if (existingStock != null)
                return response.Error($"托盘{task.PalletCode}的库存信息已存在,请勿重复入库");
            //Dt_StockInfo stockInfo = new Dt_StockInfo
            //{
            //    PalletCode = task.PalletCode,
            //    StockStatus = (int)StockStatusEmun.入库确认,
            //    LocationCode = locationInfo.LocationCode,
            //    WarehouseId = task.WarehouseId,
            //    Creater = "AGV",
            //    CreateDate = DateTime.Now
            //};
            locationInfo.LocationStatus = (int)LocationStatusEnum.FreeLock;
            _unitOfWorkManage.BeginTran();
            var updateTaskResult = BaseDal.UpdateData(task);
            var updateLocationResult = _locationInfoService.UpdateData(locationInfo);
            //var addStockResult = _stockInfoService.AddData(stockInfo);
            if (!updateTaskResult || !updateLocationResult.Status /*|| !addStockResult.Status*/)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error("入库完成后,本地任务、库存或货位更新失败");
            }
            _unitOfWorkManage.CommitTran();
            return response.OK();
        }
        // AGV入库取消
        private async Task<AGVResponse> CancelAgvInboundTask(Dt_Task task)
        {
            AGVResponse response = new AGVResponse();
            task.TaskStatus = (int)TaskInStatusEnum.InCancel;
            _unitOfWorkManage.BeginTran();
            try
            {
                var stockInfo = await _stockInfoService.GetStockInfoAsync(task.PalletCode);
                if (stockInfo != null)
                {
                    var deleteResult = await _stockInfoService.DeleteStockWithDetailsAsync(stockInfo.Id);
                    if (!deleteResult.Status)
                    {
                        _unitOfWorkManage.RollbackTran();
                        return response.Error($"删除库存失败: {deleteResult.Message}");
                    }
                }
                BaseDal.DeleteAndMoveIntoHty(task, App.User.UserId == 0 ? OperateTypeEnum.自动完成 : OperateTypeEnum.人工完成);
                _unitOfWorkManage.CommitTran();
                return response.OK();
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error($"取消入库任务时发生异常: {ex.Message}");
            }
        }
        // AGV出库取消
        private async Task<AGVResponse> CancelAgvOutboundTaskAsync(Dt_Task task)
        {
            AGVResponse response = new AGVResponse();
            var stockInfo = await _stockInfoService.GetStockInfoAsync(task.PalletCode);
            if (stockInfo == null)
                return response.Error($"未找到托盘{task.PalletCode}的库存信息");
            var locationInfo = await _locationInfoService.GetLocationInfoAsync(stockInfo.LocationCode);
            if (locationInfo == null)
                return response.Error($"未找到托盘{stockInfo.LocationCode}的货位信息");
            if (stockInfo.StockStatus != (int)StockStatusEmun.出库锁定 || locationInfo.LocationStatus != (int)LocationStatusEnum.InStockLock)
                return response.Error($"当前库存{stockInfo.StockStatus}或者货位{locationInfo.LocationStatus}状态信息错误");
            stockInfo.StockStatus = (int)StockStatusEmun.入库完成;
            locationInfo.LocationStatus = (int)LocationStatusEnum.InStock;
            task.TaskStatus = (int)TaskOutStatusEnum.OutCancel;
            _unitOfWorkManage.BeginTran();
            var updateLocationResult = _locationInfoService.UpdateData(locationInfo);
            var updateStockResult = _stockInfoService.UpdateData(stockInfo);
            BaseDal.DeleteAndMoveIntoHty(task, App.User.UserId == 0 ? OperateTypeEnum.自动完成 : OperateTypeEnum.人工完成);
            if (!updateLocationResult.Status || !updateStockResult.Status)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error("出库任务取消失败");
            }
            _unitOfWorkManage.CommitTran();
            return response.OK();
        }
        #endregion å…·ä½“实现
    }
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error($"入库任务创建异常:{ex.Message}");
            }
        }
        // å‡ºåº“创建
        private async Task<AGVResponse?> CreateAgvOutboundTaskAsync(Dt_Task task, ApplyInOutDto dto)
        {
            AGVResponse response = new AGVResponse();
            // æ£€æŸ¥åº“存是否存在
            var stockInfo = await _stockInfoService.GetStockInfoAsync(dto.TrayNumber);
            if (stockInfo == null)
                return response.Error($"未找到托盘{dto.TrayNumber}的库存信息");
            // æ£€æŸ¥åº“存是否有明细(即是否真的有库存)
            if (stockInfo.Details == null || !stockInfo.Details.Any())
                return response.Error($"托盘{dto.TrayNumber}没有库存明细,无法出库");
            // æ£€æŸ¥åº“存总数量是否大于0
            var totalQuantity = stockInfo.Details.Sum(d => d.StockQuantity);
            if (totalQuantity <= 0)
                return response.Error($"托盘{dto.TrayNumber}库存数量不足,无法出库");
            // æ ¹æ®dto参数进一步验证库存信息
            if (!string.IsNullOrEmpty(dto.ProductNo))
            {
                // æ£€æŸ¥åº“存明细中是否包含指定的物料编码
                var hasMatchingMaterial = stockInfo.Details.Any(d => d.MaterielCode == dto.ProductNo);
                if (!hasMatchingMaterial)
                    return response.Error($"托盘{dto.TrayNumber}中没有物料编码为{dto.ProductNo}的库存,无法出库");
            }
            // æ£€æŸ¥åº“存状态是否允许出库
            if (stockInfo.StockStatus != (int)StockStatusEmun.入库完成)
                return response.Error($"托盘{dto.TrayNumber}正在移动中,请稍后!");
            // æ£€æŸ¥è´§ä½ä¿¡æ¯
            var locationInfo = await _locationInfoService.GetLocationInfo(stockInfo.LocationCode);
            if (locationInfo == null)
                return response.Error($"未找到托盘{stockInfo.LocationCode}的货位信息");
            // æ£€æŸ¥è´§ä½çŠ¶æ€æ˜¯å¦å…è®¸å‡ºåº“
            if (locationInfo.LocationStatus != (int)LocationStatusEnum.InStock)
                return response.Error($"当前货位{locationInfo.LocationStatus}状态信息错误");
            // éªŒè¯ä»“库ID是否匹配(根据dto的阴阳极参数)
            var expectedWarehouseId = dto.YinYang == 1 ? (int)WarehouseEnum.FJ1 : (int)WarehouseEnum.ZJ1;
            if (stockInfo.WarehouseId != expectedWarehouseId)
                return response.Error($"托盘{dto.TrayNumber}不在预期的仓库中,无法出库");
            task.TaskType = (int)TaskOutboundTypeEnum.Outbound;
            task.TaskStatus = (int)TaskOutStatusEnum.OutNew;
            task.SourceAddress = stockInfo.LocationCode;
            task.CurrentAddress = stockInfo.LocationCode;
            task.TargetAddress = dto.YinYang == 1 ? "D10020" : "D10090";
            var wmsTaskDto = _mapper.Map<WMSTaskDTO>(task);
            var taskList = new List<WMSTaskDTO> { wmsTaskDto };
            var requestBody = JsonSerializer.Serialize(taskList);
            var httpResponse = _httpClientHelper.Post<WebResponseContent>(WCS_ReceiveTask, requestBody);
            if (httpResponse == null || httpResponse.Data == null || !httpResponse.Data.Status)
                return response.Error(httpResponse?.Data?.Message ?? "下发WCS失败");
            stockInfo.StockStatus = (int)StockStatusEmun.出库锁定;
            locationInfo.LocationStatus = (int)LocationStatusEnum.InStockLock;
            _unitOfWorkManage.BeginTran();
            var addTaskResult = await BaseDal.AddDataAsync(task) > 0;
            var updateLocationResult = await _locationInfoService.UpdateLocationInfoAsync(locationInfo);
            var updateStockResult = await _stockInfoService.UpdateStockAsync(stockInfo);
            if (!addTaskResult || !updateLocationResult || !updateStockResult)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error("出库任务创建失败");
            }
            _unitOfWorkManage.CommitTran();
            return null;
        }
        private async Task<WebResponseContent> CompleteLocalOutboundAfterAgvAckAsync(Dt_Task task)
        {
            task.TaskStatus = (int)TaskOutStatusEnum.Line_OutFinish;
            _unitOfWorkManage.BeginTran();
            var updateResult = BaseDal.UpdateData(task);
            if (!updateResult)
            {
                _unitOfWorkManage.RollbackTran();
                return WebResponseContent.Instance.Error("AGV完成回传后,任务更新失败");
            }
            _unitOfWorkManage.CommitTran();
            return WebResponseContent.Instance.OK();
        }
        private bool CanApplyEnter(Dt_Task task, ApplyEnterDto dto)
        {
            if (dto.InOut == 1)
            {
                var hasExecutingOutTask = BaseDal.QueryFirst(x => x.TaskType == (int)TaskOutboundTypeEnum.Outbound
                    && x.TargetAddress == task.SourceAddress
                    && (x.TaskStatus == (int)TaskOutStatusEnum.SC_OutExecuting
                        || x.TaskStatus == (int)TaskOutStatusEnum.Line_OutExecuting));
                // å¦‚果没有正在执行的出库任务,则允许入库
                return hasExecutingOutTask == null;
            }
            else
            {
                return task.TaskType == (int)TaskOutboundTypeEnum.Outbound
                    && task.TaskStatus == (int)TaskStatusEnum.Line_Finish;
            }
        }
        // WCS入库完成
        private async Task<WebResponseContent> CompleteAgvInboundTaskAsync(CreateTaskDto taskDto)
        {
            WebResponseContent response = new WebResponseContent();
            var task = await BaseDal.QueryFirstAsync(x => x.PalletType == taskDto.PalletType);
            if (task == null)
                return response.Error($"没有当前托盘{taskDto.PalletType}入库任务");
            var stockInfo = await _stockInfoService.GetStockInfoAsync(task.PalletCode);
            if (stockInfo == null)
                return response.Error($"未找到托盘{task.PalletCode}的库存信息");
            var locationInfo = await _locationInfoService.GetLocationInfoAsync(task.TargetAddress);
            if (locationInfo == null)
                return response.Error($"未找到货位{task.TargetAddress}的信息");
            if (locationInfo.LocationStatus == (int)LocationStatusEnum.InStock)
                return response.Error($"当前货位{locationInfo.LocationStatus}状态不是空闲状态,无法入库");
            // æ›´æ–°è´§ä½çŠ¶æ€ä¸ºå ç”¨
            locationInfo.LocationStatus = (int)LocationStatusEnum.InStock;
            task.TaskStatus = (int)TaskInStatusEnum.InFinish;
            stockInfo.StockStatus = (int)StockStatusEmun.入库完成;
            _unitOfWorkManage.BeginTran();
            var addStockResult = _stockInfoService.UpdateData(stockInfo);
            var updateLocationResult = _locationInfoService.UpdateData(locationInfo);
            BaseDal.DeleteAndMoveIntoHty(task, App.User.UserId == 0 ? OperateTypeEnum.自动完成 : OperateTypeEnum.人工完成);
            if (!addStockResult.Status || !updateLocationResult.Status)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error("入库完成后,添加库存或货位更新失败");
            }
            _unitOfWorkManage.CommitTran();
            return response.OK();
        }
        // AGV出库完成
        private async Task<AGVResponse> CompleteAgvOutboundTaskAsync(Dt_Task task)
        {
            AGVResponse response = new AGVResponse();
            var stockInfo = await _stockInfoService.GetStockInfoAsync(task.PalletCode);
            if (stockInfo == null)
                return response.Error($"未找到托盘{task.PalletCode}的库存信息");
            var locationInfo = await _locationInfoService.GetLocationInfoAsync(stockInfo.LocationCode);
            if (locationInfo == null)
                return response.Error($"未找到托盘{stockInfo.LocationCode}的货位信息");
            if (stockInfo.StockStatus != (int)StockStatusEmun.出库锁定 || locationInfo.LocationStatus != (int)LocationStatusEnum.InStockLock)
                return response.Error($"当前库存{stockInfo.StockStatus}或者货位{locationInfo.LocationStatus}状态信息错误");
            locationInfo.LocationStatus = (int)LocationStatusEnum.Free;
            task.TaskStatus = (int)TaskOutStatusEnum.OutFinish;
            _unitOfWorkManage.BeginTran();
            //var deleteStockResult = _stockInfoService.DeleteData(stockInfo)
            var deleteStockResult = await _stockInfoService.DeleteStockWithDetailsAsync(stockInfo.Id);
            var updateLocationResult = _locationInfoService.UpdateData(locationInfo);
            BaseDal.DeleteAndMoveIntoHty(task, App.User.UserId == 0 ? OperateTypeEnum.自动完成 : OperateTypeEnum.人工完成);
            if (!deleteStockResult.Status || !updateLocationResult.Status)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error("出库完成后,本地库存或货位更新失败");
            }
            _unitOfWorkManage.CommitTran();
            return response.OK();
        }
        // AGV已放货,准备输送线入库
        private async Task<AGVResponse> CompleteAgvInboundTaskAsync(Dt_Task task)
        {
            AGVResponse response = new AGVResponse();
            var availableLocation = await _locationInfoService.GetLocationInfo(task.Roadway);
            if (availableLocation == null)
                return response.Error("无可用的入库货位");
            task.TargetAddress = availableLocation.LocationCode;
            var wmsTaskDto = _mapper.Map<WMSTaskDTO>(task);
            var taskList = new List<WMSTaskDTO> { wmsTaskDto };
            var requestBody = JsonSerializer.Serialize(taskList);
            var httpResponse = _httpClientHelper.Post<WebResponseContent>(WCS_ReceiveTask, requestBody);
            if (httpResponse == null || httpResponse.Data == null || !httpResponse.Data.Status)
                return response.Error(httpResponse?.Data?.Message ?? "下发WCS失败");
            task.TaskStatus = (int)TaskInStatusEnum.Line_InExecuting;
            task.Dispatchertime = DateTime.Now;
            var locationInfo = await _locationInfoService.GetLocationInfoAsync(task.TargetAddress);
            if (locationInfo == null)
                return response.Error($"未找到托盘{task.TargetAddress}的货位信息");
            if (locationInfo.LocationStatus != (int)LocationStatusEnum.Free)
                return response.Error($"当前货位{locationInfo.LocationStatus}状态信息错误");
            var existingStock = await _stockInfoService.GetStockInfoAsync(task.PalletCode);
            if (existingStock != null)
                return response.Error($"托盘{task.PalletCode}的库存信息已存在,请勿重复入库");
            //Dt_StockInfo stockInfo = new Dt_StockInfo
            //{
            //    PalletCode = task.PalletCode,
            //    StockStatus = (int)StockStatusEmun.入库确认,
            //    LocationCode = locationInfo.LocationCode,
            //    WarehouseId = task.WarehouseId,
            //    Creater = "AGV",
            //    CreateDate = DateTime.Now
            //};
            locationInfo.LocationStatus = (int)LocationStatusEnum.FreeLock;
            _unitOfWorkManage.BeginTran();
            var updateTaskResult = BaseDal.UpdateData(task);
            var updateLocationResult = _locationInfoService.UpdateData(locationInfo);
            //var addStockResult = _stockInfoService.AddData(stockInfo);
            if (!updateTaskResult || !updateLocationResult.Status /*|| !addStockResult.Status*/)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error("入库完成后,本地任务、库存或货位更新失败");
            }
            _unitOfWorkManage.CommitTran();
            return response.OK();
        }
        // AGV入库取消
        private async Task<AGVResponse> CancelAgvInboundTask(Dt_Task task)
        {
            AGVResponse response = new AGVResponse();
            task.TaskStatus = (int)TaskInStatusEnum.InCancel;
            _unitOfWorkManage.BeginTran();
            try
            {
                var stockInfo = await _stockInfoService.GetStockInfoAsync(task.PalletCode);
                if (stockInfo != null)
                {
                    var deleteResult = await _stockInfoService.DeleteStockWithDetailsAsync(stockInfo.Id);
                    if (!deleteResult.Status)
                    {
                        _unitOfWorkManage.RollbackTran();
                        return response.Error($"删除库存失败: {deleteResult.Message}");
                    }
                }
                BaseDal.DeleteAndMoveIntoHty(task, App.User.UserId == 0 ? OperateTypeEnum.自动完成 : OperateTypeEnum.人工完成);
                _unitOfWorkManage.CommitTran();
                return response.OK();
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error($"取消入库任务时发生异常: {ex.Message}");
            }
        }
        // AGV出库取消
        private async Task<AGVResponse> CancelAgvOutboundTaskAsync(Dt_Task task)
        {
            AGVResponse response = new AGVResponse();
            var stockInfo = await _stockInfoService.GetStockInfoAsync(task.PalletCode);
            if (stockInfo == null)
                return response.Error($"未找到托盘{task.PalletCode}的库存信息");
            var locationInfo = await _locationInfoService.GetLocationInfoAsync(stockInfo.LocationCode);
            if (locationInfo == null)
                return response.Error($"未找到托盘{stockInfo.LocationCode}的货位信息");
            if (stockInfo.StockStatus != (int)StockStatusEmun.出库锁定 || locationInfo.LocationStatus != (int)LocationStatusEnum.InStockLock)
                return response.Error($"当前库存{stockInfo.StockStatus}或者货位{locationInfo.LocationStatus}状态信息错误");
            stockInfo.StockStatus = (int)StockStatusEmun.入库完成;
            locationInfo.LocationStatus = (int)LocationStatusEnum.InStock;
            task.TaskStatus = (int)TaskOutStatusEnum.OutCancel;
            _unitOfWorkManage.BeginTran();
            var updateLocationResult = _locationInfoService.UpdateData(locationInfo);
            var updateStockResult = _stockInfoService.UpdateData(stockInfo);
            BaseDal.DeleteAndMoveIntoHty(task, App.User.UserId == 0 ? OperateTypeEnum.自动完成 : OperateTypeEnum.人工完成);
            if (!updateLocationResult.Status || !updateStockResult.Status)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error("出库任务取消失败");
            }
            _unitOfWorkManage.CommitTran();
            return response.OK();
        }
        #endregion å…·ä½“实现
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Dashboard/DashboardController.cs
@@ -1,272 +1,373 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SqlSugar;
using WIDESEA_Core;
using WIDESEA_Model.Models;
namespace WIDESEA_WMSServer.Controllers.Dashboard
{
    /// <summary>
    /// ä»ªè¡¨ç›˜
    /// </summary>
    [Route("api/Dashboard")]
    [ApiController]
    public class DashboardController : ControllerBase
    {
        private readonly ISqlSugarClient _db;
        public DashboardController(ISqlSugarClient db)
        {
            _db = db;
        }
        /// <summary>
        /// æ€»è§ˆæ•°æ®
        /// </summary>
        [HttpGet("Overview"), AllowAnonymous]
        public async Task<WebResponseContent> Overview()
        {
            try
            {
                var today = DateTime.Today;
                var firstDayOfMonth = new DateTime(today.Year, today.Month, 1);
                // ä»Šæ—¥å…¥åº“æ•°
                var todayInbound = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= today && t.TaskType >= 500 && t.TaskType < 600)
                    .CountAsync();
                // ä»Šæ—¥å‡ºåº“æ•°
                var todayOutbound = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= today && t.TaskType >= 100 && t.TaskType < 200)
                    .CountAsync();
                // æœ¬æœˆå…¥åº“æ•°
                var monthInbound = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= firstDayOfMonth && t.TaskType >= 500 && t.TaskType < 600)
                    .CountAsync();
                // æœ¬æœˆå‡ºåº“æ•°
                var monthOutbound = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= firstDayOfMonth && t.TaskType >= 100 && t.TaskType < 200)
                    .CountAsync();
                // å½“前总库存
                var totalStock = await _db.Queryable<Dt_StockInfo>().CountAsync();
                return WebResponseContent.Instance.OK(null, new
                {
                    TodayInbound = todayInbound,
                    TodayOutbound = todayOutbound,
                    MonthInbound = monthInbound,
                    MonthOutbound = monthOutbound,
                    TotalStock = totalStock
                });
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"总览数据获取失败: {ex.Message}");
            }
        }
        /// <summary>
        /// æ¯æ—¥ç»Ÿè®¡
        /// </summary>
        /// <remarks>
        /// æ³¨æ„ï¼šæ•°æ®åœ¨ SQL å±‚过滤后,在应用层按日期分组。
        /// SqlSugar çš„ GroupBy ä¸æ”¯æŒå¯¹ .Date è¿™æ ·çš„计算列直接生成 SQL GROUP BY,
        /// å› æ­¤é‡‡ç”¨æ­¤æ–¹å¼ä»¥ç¡®ä¿è·¨æ•°æ®åº“兼容性。
        /// </remarks>
        [HttpGet("DailyStats"), AllowAnonymous]
        public async Task<WebResponseContent> DailyStats([FromQuery] int days = 30)
        {
            try
            {
                if (days <= 0) days = 30;
                if (days > 365) days = 365;
                var startDate = DateTime.Today.AddDays(-days + 1);
                var query = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= startDate)
                    .Select(t => new { t.InsertTime, t.TaskType })
                    .ToListAsync();
                var result = query
                    .GroupBy(t => t.InsertTime.Date)
                    .Select(g => new
                    {
                        Date = g.Key.ToString("yyyy-MM-dd"),
                        Inbound = g.Count(t => t.TaskType >= 500 && t.TaskType < 600),
                        Outbound = g.Count(t => t.TaskType >= 100 && t.TaskType < 200)
                    })
                    .OrderBy(x => x.Date)
                    .ToList();
                return WebResponseContent.Instance.OK(null, result);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"每日统计获取失败: {ex.Message}");
            }
        }
        /// <summary>
        /// æ¯å‘¨ç»Ÿè®¡
        /// </summary>
        /// <remarks>
        /// æ³¨æ„ï¼šæ•°æ®åœ¨ SQL å±‚过滤后,在应用层按 ISO 8601 å‘¨é”®åˆ†ç»„。
        /// å‘¨é”®ä¸º "YYYY-Www" æ ¼å¼ï¼Œæ— æ³•直接在 SQL å±‚用 GROUP BY å®žçŽ°ã€‚
        /// </remarks>
        [HttpGet("WeeklyStats"), AllowAnonymous]
        public async Task<WebResponseContent> WeeklyStats([FromQuery] int weeks = 12)
        {
            try
            {
                if (weeks <= 0) weeks = 12;
                var startDate = DateTime.Today.AddDays(-weeks * 7);
                var query = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= startDate)
                    .Select(t => new { t.InsertTime, t.TaskType })
                    .ToListAsync();
                var result = query
                    .GroupBy(t => GetWeekKey(t.InsertTime))
                    .Select(g => new
                    {
                        Week = g.Key,
                        Inbound = g.Count(t => t.TaskType >= 500 && t.TaskType < 600),
                        Outbound = g.Count(t => t.TaskType >= 100 && t.TaskType < 200)
                    })
                    .OrderBy(x => x.Week)
                    .ToList();
                return WebResponseContent.Instance.OK(null, result);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"每周统计获取失败: {ex.Message}");
            }
        }
        private string GetWeekKey(DateTime date)
        {
            // èŽ·å–å‘¨ä¸€å¼€å§‹çš„å‘¨ (ISO 8601)
            var diff = (7 + (date.DayOfWeek - DayOfWeek.Monday)) % 7;
            var monday = date.AddDays(-diff);
            var weekNum = System.Globalization.CultureInfo.InvariantCulture
                .Calendar.GetWeekOfYear(monday, System.Globalization.CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);
            return $"{monday.Year}-W{weekNum:D2}";
        }
        /// <summary>
        /// æ¯æœˆç»Ÿè®¡
        /// </summary>
        /// <remarks>
        /// æ³¨æ„ï¼šæ•°æ®åœ¨ SQL å±‚过滤后,在应用层按年月分组。
        /// SqlSugar çš„ GroupBy ä¸æ”¯æŒåŒ¿åå¯¹è±¡ (Year, Month) ç›´æŽ¥æ˜ å°„到 SQL GROUP BY,
        /// å› æ­¤é‡‡ç”¨æ­¤æ–¹å¼ä»¥ç¡®ä¿è·¨æ•°æ®åº“兼容性。
        /// </remarks>
        [HttpGet("MonthlyStats"), AllowAnonymous]
        public async Task<WebResponseContent> MonthlyStats([FromQuery] int months = 12)
        {
            try
            {
                if (months <= 0) months = 12;
                var startDate = DateTime.Today.AddMonths(-months + 1);
                startDate = new DateTime(startDate.Year, startDate.Month, 1);
                var query = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= startDate)
                    .Select(t => new { t.InsertTime, t.TaskType })
                    .ToListAsync();
                var result = query
                    .GroupBy(t => new { t.InsertTime.Year, t.InsertTime.Month })
                    .Select(g => new
                    {
                        Month = $"{g.Key.Year}-{g.Key.Month:D2}",
                        Inbound = g.Count(t => t.TaskType >= 500 && t.TaskType < 600),
                        Outbound = g.Count(t => t.TaskType >= 100 && t.TaskType < 200)
                    })
                    .OrderBy(x => x.Month)
                    .ToList();
                return WebResponseContent.Instance.OK(null, result);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"每月统计获取失败: {ex.Message}");
            }
        }
        /// <summary>
        /// åº“存库龄分布
        /// </summary>
        [HttpGet("StockAgeDistribution"), AllowAnonymous]
        public async Task<WebResponseContent> StockAgeDistribution()
        {
            try
            {
                var today = DateTime.Today;
                // ä½¿ç”¨ SQL ç›´æŽ¥åˆ†ç»„统计,避免加载所有数据到内存
                var result = new[]
                {
                    new { Range = "7天内", Count = await _db.Queryable<Dt_StockInfo>().Where(s => SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) <= 7).CountAsync() },
                    new { Range = "7-30天", Count = await _db.Queryable<Dt_StockInfo>().Where(s => SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) > 7 && SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) <= 30).CountAsync() },
                    new { Range = "30-90天", Count = await _db.Queryable<Dt_StockInfo>().Where(s => SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) > 30 && SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) <= 90).CountAsync() },
                    new { Range = "90天以上", Count = await _db.Queryable<Dt_StockInfo>().Where(s => SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) > 90).CountAsync() }
                };
                return WebResponseContent.Instance.OK(null, result);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"库存库龄分布获取失败: {ex.Message}");
            }
        }
        /// <summary>
        /// å„仓库库存分布
        /// </summary>
        /// <remarks>
        /// ä½¿ç”¨ SQL GROUP BY åœ¨æ•°æ®åº“层面聚合,避免加载全部库存记录到内存。
        /// </remarks>
        [HttpGet("StockByWarehouse"), AllowAnonymous]
        public async Task<WebResponseContent> StockByWarehouse()
        {
            try
            {
                // æŸ¥è¯¢ä»“库名称
                var warehouses = await _db.Queryable<Dt_Warehouse>()
                    .Select(w => new { w.WarehouseId, w.WarehouseName })
                    .ToListAsync();
                var warehouseDict = warehouses.ToDictionary(w => w.WarehouseId, w => w.WarehouseName);
                // ä½¿ç”¨ SQL GROUP BY åœ¨æ•°æ®åº“层面聚合,仅返回聚合结果
                var stockGroups = await _db.Queryable<Dt_StockInfo>()
                    .GroupBy(s => s.WarehouseId)
                    .Select(s => new { s.WarehouseId, Count = SqlFunc.AggregateCount(s.Id) })
                    .ToListAsync();
                var result = stockGroups
                    .Select(g => new
                    {
                        Warehouse = warehouseDict.TryGetValue(g.WarehouseId, out var name) ? name : $"仓库{g.WarehouseId}",
                        Count = g.Count
                    })
                    .ToList();
                return WebResponseContent.Instance.OK(null, result);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"各仓库库存分布获取失败: {ex.Message}");
            }
        }
    }
}
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SqlSugar;
using WIDESEA_Common.LocationEnum;
using WIDESEA_Core;
using WIDESEA_Model.Models;
namespace WIDESEA_WMSServer.Controllers.Dashboard
{
    /// <summary>
    /// ä»ªè¡¨ç›˜
    /// </summary>
    [Route("api/Dashboard")]
    [ApiController]
    public class DashboardController : ControllerBase
    {
        private readonly ISqlSugarClient _db;
        public DashboardController(ISqlSugarClient db)
        {
            _db = db;
        }
        /// <summary>
        /// æ€»è§ˆæ•°æ®
        /// </summary>
        [HttpGet("Overview"), AllowAnonymous]
        public async Task<WebResponseContent> Overview()
        {
            try
            {
                var today = DateTime.Today;
                var firstDayOfMonth = new DateTime(today.Year, today.Month, 1);
                // ä»Šæ—¥å…¥åº“æ•°
                var todayInbound = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= today && t.TaskType >= 500 && t.TaskType < 600)
                    .CountAsync();
                // ä»Šæ—¥å‡ºåº“æ•°
                var todayOutbound = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= today && t.TaskType >= 100 && t.TaskType < 200)
                    .CountAsync();
                // æœ¬æœˆå…¥åº“æ•°
                var monthInbound = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= firstDayOfMonth && t.TaskType >= 500 && t.TaskType < 600)
                    .CountAsync();
                // æœ¬æœˆå‡ºåº“æ•°
                var monthOutbound = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= firstDayOfMonth && t.TaskType >= 100 && t.TaskType < 200)
                    .CountAsync();
                // å½“前总库存
                var totalStock = await _db.Queryable<Dt_StockInfo>().CountAsync();
                return WebResponseContent.Instance.OK(null, new
                {
                    TodayInbound = todayInbound,
                    TodayOutbound = todayOutbound,
                    MonthInbound = monthInbound,
                    MonthOutbound = monthOutbound,
                    TotalStock = totalStock
                });
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"总览数据获取失败: {ex.Message}");
            }
        }
        /// <summary>
        /// æ¯æ—¥ç»Ÿè®¡
        /// </summary>
        [HttpGet("DailyStats"), AllowAnonymous]
        public async Task<WebResponseContent> DailyStats([FromQuery] int days = 30)
        {
            try
            {
                if (days <= 0) days = 30;
                if (days > 365) days = 365;
                var startDate = DateTime.Today.AddDays(-days + 1);
                var endDate = DateTime.Today; // åŒ…含今天
                var query = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= startDate && t.InsertTime <= endDate)
                    .Select(t => new { t.InsertTime, t.TaskType })
                    .ToListAsync();
                // ç”Ÿæˆæ—¥æœŸèŒƒå›´
                var allDates = new List<DateTime>();
                for (var date = startDate; date <= endDate; date = date.AddDays(1))
                {
                    allDates.Add(date);
                }
                // æŒ‰æ—¥æœŸåˆ†ç»„统计
                var groupedData = query
                    .GroupBy(t => t.InsertTime.Date)
                    .Select(g => new
                    {
                        Date = g.Key,
                        Inbound = g.Count(t => t.TaskType >= 200 && t.TaskType < 300),
                        Outbound = g.Count(t => t.TaskType >= 100 && t.TaskType < 200)
                    })
                    .ToDictionary(x => x.Date, x => x);
                // è¡¥å…¨ç¼ºå¤±æ—¥æœŸ
                var result = allDates.Select(date =>
                {
                    if (groupedData.TryGetValue(date, out var data))
                    {
                        return new
                        {
                            Date = date.ToString("MM-dd"),
                            Inbound = data.Inbound,
                            Outbound = data.Outbound
                        };
                    }
                    else
                    {
                        return new
                        {
                            Date = date.ToString("MM-dd"),
                            Inbound = 0,
                            Outbound = 0
                        };
                    }
                })
                .OrderBy(x => x.Date)
                .ToList();
                return WebResponseContent.Instance.OK(null, result);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"每日统计获取失败: {ex.Message}");
            }
        }
        /// <summary>
        /// æ¯å‘¨ç»Ÿè®¡
        /// </summary>
        /// <remarks>
        /// æ³¨æ„ï¼šæ•°æ®åœ¨ SQL å±‚过滤后,在应用层按 ISO 8601 å‘¨é”®åˆ†ç»„。
        /// å‘¨é”®ä¸º "YYYY-Www" æ ¼å¼ï¼Œæ— æ³•直接在 SQL å±‚用 GROUP BY å®žçŽ°ã€‚
        /// </remarks>
        [HttpGet("WeeklyStats"), AllowAnonymous]
        public async Task<WebResponseContent> WeeklyStats([FromQuery] int weeks = 12)
        {
            try
            {
                if (weeks <= 0) weeks = 12;
                var startDate = DateTime.Today.AddDays(-weeks * 7);
                var query = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= startDate)
                    .Select(t => new { t.InsertTime, t.TaskType })
                    .ToListAsync();
                var result = query
                    .GroupBy(t => GetWeekKey(t.InsertTime))
                    .Select(g => new
                    {
                        Week = g.Key,
                        Inbound = g.Count(t => t.TaskType >= 200 && t.TaskType < 300),
                        Outbound = g.Count(t => t.TaskType >= 100 && t.TaskType < 200)
                    })
                    .OrderBy(x => x.Week)
                    .ToList();
                return WebResponseContent.Instance.OK(null, result);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"每周统计获取失败: {ex.Message}");
            }
        }
        private string GetWeekKey(DateTime date)
        {
            // èŽ·å–å‘¨ä¸€å¼€å§‹çš„å‘¨ (ISO 8601)
            var diff = (7 + (date.DayOfWeek - DayOfWeek.Monday)) % 7;
            var monday = date.AddDays(-diff);
            var weekNum = System.Globalization.CultureInfo.InvariantCulture
                .Calendar.GetWeekOfYear(monday, System.Globalization.CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);
            return $"{monday.Year}-W{weekNum:D2}";
        }
        /// <summary>
        /// æ¯æœˆç»Ÿè®¡
        /// </summary>
        /// <remarks>
        /// æŒ‰å¹´æœˆç»Ÿè®¡å…¥ç«™å’Œå‡ºç«™ä»»åŠ¡æ•°é‡
        /// </remarks>
        [HttpGet("MonthlyStats"), AllowAnonymous]
        public async Task<WebResponseContent> MonthlyStats([FromQuery] int months = 12)
        {
            try
            {
                if (months <= 0) months = 12;
                var startDate = DateTime.Today.AddMonths(-months + 1);
                startDate = new DateTime(startDate.Year, startDate.Month, 1);
                var monthlyStats = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= startDate)
                    .GroupBy(t => new { t.InsertTime.Year, t.InsertTime.Month })
                    .Select(t => new
                    {
                        Year = t.InsertTime.Year,
                        Month = t.InsertTime.Month,
                        Inbound = SqlFunc.AggregateSum(
                            SqlFunc.IIF(t.TaskType >= 200 && t.TaskType < 300, 1, 0)
                        ),
                        Outbound = SqlFunc.AggregateSum(
                            SqlFunc.IIF(t.TaskType >= 100 && t.TaskType < 200, 1, 0)
                        )
                    })
                    .OrderBy(t => t.Year)
                    .OrderBy(t => t.Month)
                    .ToListAsync();
                // ç”Ÿæˆæ‰€æœ‰éœ€è¦ç»Ÿè®¡çš„æœˆä»½åˆ—表
                var allMonths = new List<DateTime>();
                var currentMonth = startDate;
                var endMonth = new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1);
                while (currentMonth <= endMonth)
                {
                    allMonths.Add(currentMonth);
                    currentMonth = currentMonth.AddMonths(1);
                }
                // å°†æŸ¥è¯¢ç»“果转换为字典,方便查找
                var statsDict = monthlyStats.ToDictionary(
                    s => $"{s.Year}-{s.Month:D2}",
                    s => new { s.Inbound, s.Outbound }
                );
                // æž„建完整的结果列表,包含所有月份
                var result = new List<object>();
                foreach (var month in allMonths)
                {
                    var monthKey = $"{month.Year}-{month.Month:D2}";
                    if (statsDict.TryGetValue(monthKey, out var stat))
                    {
                        result.Add(new
                        {
                            Month = monthKey,
                            Inbound = stat.Inbound,
                            Outbound = stat.Outbound
                        });
                    }
                    else
                    {
                        result.Add(new
                        {
                            Month = monthKey,
                            Inbound = 0,
                            Outbound = 0
                        });
                    }
                }
                return WebResponseContent.Instance.OK(null, result);
            }
            catch (Exception ex)
            {
                // è®°å½•异常日志(实际项目中建议使用日志框架)
                // _logger.LogError(ex, "每月统计获取失败");
                return WebResponseContent.Instance.Error($"每月统计获取失败: {ex.Message}");
            }
        }
        /// <summary>
        /// åº“存库龄分布
        /// </summary>
        [HttpGet("StockAgeDistribution"), AllowAnonymous]
        public async Task<WebResponseContent> StockAgeDistribution()
        {
            try
            {
                var today = DateTime.Today;
                // ä½¿ç”¨ SQL ç›´æŽ¥åˆ†ç»„统计,避免加载所有数据到内存
                var result = new[]
                {
                    new { Range = "7天内", Count = await _db.Queryable<Dt_StockInfo>().Where(s => SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) <= 7).CountAsync() },
                    new { Range = "7-30天", Count = await _db.Queryable<Dt_StockInfo>().Where(s => SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) > 7 && SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) <= 30).CountAsync() },
                    new { Range = "30-90天", Count = await _db.Queryable<Dt_StockInfo>().Where(s => SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) > 30 && SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) <= 90).CountAsync() },
                    new { Range = "90天以上", Count = await _db.Queryable<Dt_StockInfo>().Where(s => SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) > 90).CountAsync() }
                };
                return WebResponseContent.Instance.OK(null, result);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"库存库龄分布获取失败: {ex.Message}");
            }
        }
        /// <summary>
        /// å„仓库库存分布
        /// </summary>
        /// <remarks>
        /// ä½¿ç”¨ SQL GROUP BY åœ¨æ•°æ®åº“层面聚合,避免加载全部库存记录到内存。
        /// </remarks>
        [HttpGet("StockByWarehouse"), AllowAnonymous]
        public async Task<WebResponseContent> StockByWarehouse()
        {
            try
            {
                // æŸ¥è¯¢æ‰€æœ‰ä»“库信息
                var warehouses = await _db.Queryable<Dt_Warehouse>()
                    .Select(w => new { w.WarehouseId, w.WarehouseName })
                    .ToListAsync();
                // æŸ¥è¯¢æ‰€æœ‰è´§ä½ä¿¡æ¯ï¼ŒæŒ‰ä»“库分组统计总数
                var locationGroups = await _db.Queryable<Dt_LocationInfo>()
                    .GroupBy(l => l.WarehouseId)
                    .Select(l => new
                    {
                        WarehouseId = l.WarehouseId,
                        TotalLocations = SqlFunc.AggregateCount(l.Id)
                    })
                    .ToListAsync();
                // æŸ¥è¯¢çŠ¶æ€ä¸ä¸ºFree的货位信息(有货货位),按仓库分组统计
                var occupiedLocationGroups = await _db.Queryable<Dt_LocationInfo>()
                    .Where(l => l.LocationStatus != (int)LocationStatusEnum.Free)
                    .GroupBy(l => l.WarehouseId)
                    .Select(l => new
                    {
                        WarehouseId = l.WarehouseId,
                        OccupiedLocations = SqlFunc.AggregateCount(l.Id)
                    })
                    .ToListAsync();
                // å°†ä»“库信息与货位统计信息合并
                var result = warehouses.Select(w =>
                {
                    var totalLocations = locationGroups.FirstOrDefault(lg => lg.WarehouseId == w.WarehouseId)?.TotalLocations ?? 0;
                    var occupiedLocations = occupiedLocationGroups.FirstOrDefault(og => og.WarehouseId == w.WarehouseId)?.OccupiedLocations ?? 0;
                    var emptyLocations = totalLocations - occupiedLocations;
                    var occupiedPercentage = totalLocations > 0 ? Math.Round((double)occupiedLocations / totalLocations * 100, 2) : 0.0;
                    var emptyPercentage = totalLocations > 0 ? Math.Round((double)emptyLocations / totalLocations * 100, 2) : 0.0;
                    return new
                    {
                        Warehouse = w.WarehouseName,
                        Total = totalLocations,
                        HasStock = occupiedLocations,
                        NoStock = emptyLocations,
                        HasStockPercentage = $"{occupiedPercentage}%",
                        NoStockPercentage = $"{emptyPercentage}%"
                    };
                }).ToList();
                return WebResponseContent.Instance.OK(null, result);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"各仓库库存分布获取失败: {ex.Message}");
            }
        }
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Properties/PublishProfiles/FolderProfile.pubxml
@@ -14,7 +14,7 @@
    <WebPublishMethod>FileSystem</WebPublishMethod>
    <_TargetId>Folder</_TargetId>
    <SiteUrlToLaunchAfterPublish />
    <TargetFramework>net6.0</TargetFramework>
    <TargetFramework>net8.0</TargetFramework>
    <ProjectGuid>d81a65b5-47d1-40c1-8fde-7d24ff003f51</ProjectGuid>
    <SelfContained>false</SelfContained>
  </PropertyGroup>
Code/docs/superpowers/plans/2026-04-19-robot-state-redis-to-db-plan.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,804 @@
# RobotState Redis â†’ æ•°æ®åº“迁移实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** å°† `RobotSocketState` çš„存储从 Redis åˆ‡æ¢åˆ° SQL Server æ•°æ®åº“,使用 SqlSugar RowVersion å®žçŽ°ä¹è§‚å¹¶å‘æŽ§åˆ¶ã€‚
**Architecture:** æ–°å¢ž `Dt_RobotState` æ•°æ®åº“实体 + `IRobotStateRepository` ä»“储接口 + `RobotStateRepository` å®žçŽ°ã€‚`RobotStateManager` æ”¹ä¸ºä¾èµ– `IRobotStateRepository`,业务层(`RobotJob` ç­‰ï¼‰é€šè¿‡ DI èŽ·å–ä»“å‚¨å®žä¾‹ã€‚
**Tech Stack:** C# / .NET 6, SqlSugar ORM, SQL Server, Newtonsoft.Json
---
## æ–‡ä»¶å˜æ›´æ€»è§ˆ
| æ“ä½œ | æ–‡ä»¶è·¯å¾„ |
|------|---------|
| æ–°å¢ž | `WIDESEAWCS_Model/Models/RobotState/Dt_RobotState.cs` |
| æ–°å¢ž | `WIDESEAWCS_ITaskInfoRepository/IRobotStateRepository.cs` |
| æ–°å¢ž | `WIDESEAWCS_TaskInfoRepository/RobotStateRepository.cs` |
| æ”¹é€  | `WIDESEAWCS_Tasks/RobotJob/RobotStateManager.cs` |
| æ”¹é€  | `WIDESEAWCS_Tasks/RobotJob/RobotJob.cs` |
| ä¿ç•™ | `WIDESEAWCS_Tasks/RobotJob/RobotSocketState.cs`(作为内存 DTO) |
---
## Task 1: æ–°å»º Dt_RobotState æ•°æ®åº“实体
**文件:**
- æ–°å¢ž: `WCS/WIDESEAWCS_Server/WIDESEAWCS_Model/Models/RobotState/Dt_RobotState.cs`
- [ ] **Step 1: åˆ›å»ºå®žä½“类文件**
```csharp
using Newtonsoft.Json;
using SqlSugar;
using WIDESEAWCS_Core.DB.Models;
namespace WIDESEAWCS_Model.Models
{
    /// <summary>
    /// æœºæ¢°æ‰‹çŠ¶æ€æ•°æ®åº“å®žä½“
    /// </summary>
    /// <remarks>
    /// å¯¹åº”数据库表 Dt_RobotState,使用 RowVersion å®žçŽ°ä¹è§‚å¹¶å‘æŽ§åˆ¶ã€‚
    /// å¤æ‚对象(RobotCrane、CurrentTask、数组等)以 JSON å­—符串存储。
    /// </remarks>
    [SugarTable(nameof(Dt_RobotState), "机械手状态表")]
    public class Dt_RobotState : BaseEntity
    {
        /// <summary>
        /// ä¸»é”® ID
        /// </summary>
        [SugarColumn(IsPrimaryKey = true, IsIdentity = true, ColumnDescription = "主键ID")]
        public int Id { get; set; }
        /// <summary>
        /// æœºæ¢°æ‰‹ IP åœ°å€ï¼Œå”¯ä¸€ç´¢å¼•
        /// </summary>
        [SugarColumn(Length = 50, ColumnDescription = "机械手IP地址", IsJsonKey = true)]
        public string IPAddress { get; set; } = string.Empty;
        /// <summary>
        /// è¡Œç‰ˆæœ¬ï¼Œç”¨äºŽä¹è§‚并发控制
        /// </summary>
        /// <remarks>
        /// SqlSugar ä¼šè‡ªåŠ¨ç®¡ç†æ­¤å­—æ®µï¼Œæ¯æ¬¡æ›´æ–°æ—¶æ•°æ®åº“è‡ªåŠ¨é€’å¢žã€‚
        /// æ›´æ–°æ—¶ WHERE RowVersion = @expectedRowVersion,检查影响行数判断是否冲突。
        /// </remarks>
        [SugarColumn(ColumnDescription = "行版本(乐观锁)", IsJsonKey = true)]
        public byte[] RowVersion { get; set; } = Array.Empty<byte>();
        /// <summary>
        /// æ˜¯å¦å·²è®¢é˜…消息事件
        /// </summary>
        [SugarColumn(ColumnDescription = "是否已订阅消息事件", IsJsonKey = true)]
        public bool IsEventSubscribed { get; set; }
        /// <summary>
        /// æœºæ¢°æ‰‹è¿è¡Œæ¨¡å¼
        /// </summary>
        /// <remarks>1: æ‰‹åŠ¨æ¨¡å¼, 2: è‡ªåŠ¨æ¨¡å¼</remarks>
        [SugarColumn(ColumnDescription = "运行模式", IsNullable = true, IsJsonKey = true)]
        public int? RobotRunMode { get; set; }
        /// <summary>
        /// æœºæ¢°æ‰‹æŽ§åˆ¶æ¨¡å¼
        /// </summary>
        /// <remarks>1: å®¢æˆ·ç«¯æŽ§åˆ¶, 2: å…¶ä»–</remarks>
        [SugarColumn(ColumnDescription = "控制模式", IsNullable = true, IsJsonKey = true)]
        public int? RobotControlMode { get; set; }
        /// <summary>
        /// æœºæ¢°æ‰‹æ‰‹è‡‚抓取对象状态
        /// </summary>
        /// <remarks>0: æ— ç‰©æ–™ï¼ˆæ‰‹è‡‚空闲), 1: æœ‰ç‰©æ–™ï¼ˆå·²æŠ“取货物)</remarks>
        [SugarColumn(ColumnDescription = "手臂抓取状态", IsNullable = true, IsJsonKey = true)]
        public int? RobotArmObject { get; set; }
        /// <summary>
        /// æœºæ¢°æ‰‹è®¾å¤‡åŸºç¡€ä¿¡æ¯ï¼ˆJSON åºåˆ—化)
        /// </summary>
        [SugarColumn(Length = 2000, ColumnDescription = "设备信息JSON", IsJsonKey = true)]
        public string RobotCraneJson { get; set; } = string.Empty;
        /// <summary>
        /// æœºæ¢°æ‰‹åˆå§‹åŒ–完成回到待机位状态
        /// </summary>
        /// <remarks>Possible values: "Homed", "Homing"</remarks>
        [SugarColumn(Length = 50, ColumnDescription = "回零状态", IsNullable = true, IsJsonKey = true)]
        public string? Homed { get; set; }
        /// <summary>
        /// æœºæ¢°æ‰‹å½“前正在执行的动作
        /// </summary>
        [SugarColumn(Length = 50, ColumnDescription = "当前动作", IsNullable = true, IsJsonKey = true)]
        public string? CurrentAction { get; set; }
        /// <summary>
        /// æœºæ¢°æ‰‹å½“前运行状态
        /// </summary>
        [SugarColumn(Length = 50, ColumnDescription = "运行状态", IsNullable = true, IsJsonKey = true)]
        public string? OperStatus { get; set; }
        /// <summary>
        /// æœ€è¿‘一次取货完成的位置数组(JSON)
        /// </summary>
        [SugarColumn(Length = 500, ColumnDescription = "取货位置数组JSON", IsNullable = true, IsJsonKey = true)]
        public string? LastPickPositionsJson { get; set; }
        /// <summary>
        /// æœ€è¿‘一次放货完成的位置数组(JSON)
        /// </summary>
        [SugarColumn(Length = 500, ColumnDescription = "放货位置数组JSON", IsNullable = true, IsJsonKey = true)]
        public string? LastPutPositionsJson { get; set; }
        /// <summary>
        /// ç”µæ± /货位条码列表(JSON)
        /// </summary>
        [SugarColumn(Length = 2000, ColumnDescription = "电芯条码列表JSON", IsNullable = true, IsJsonKey = true)]
        public string? CellBarcodeJson { get; set; }
        /// <summary>
        /// æœºæ¢°æ‰‹å½“前正在执行的任务(JSON åºåˆ—化)
        /// </summary>
        [SugarColumn(Length = 2000, ColumnDescription = "当前任务JSON", IsNullable = true, IsJsonKey = true)]
        public string? CurrentTaskJson { get; set; }
        /// <summary>
        /// æ˜¯å¦éœ€è¦æ‰§è¡Œæ‹†ç›˜ä»»åŠ¡
        /// </summary>
        [SugarColumn(ColumnDescription = "是否拆盘任务", IsJsonKey = true)]
        public bool IsSplitPallet { get; set; }
        /// <summary>
        /// æ˜¯å¦éœ€è¦æ‰§è¡Œç»„盘任务
        /// </summary>
        [SugarColumn(ColumnDescription = "是否组盘任务", IsJsonKey = true)]
        public bool IsGroupPallet { get; set; }
        /// <summary>
        /// æœºå™¨äººå·²å¤„理的任务总数
        /// </summary>
        [SugarColumn(ColumnDescription = "已处理任务总数", IsJsonKey = true)]
        public int RobotTaskTotalNum { get; set; }
        /// <summary>
        /// æ˜¯å¦å¤„于假电芯补充模式
        /// </summary>
        [SugarColumn(ColumnDescription = "是否假电芯模式", IsJsonKey = true)]
        public bool IsInFakeBatteryMode { get; set; }
        /// <summary>
        /// å½“前批次起始编号
        /// </summary>
        [SugarColumn(ColumnDescription = "当前批次编号", IsJsonKey = true)]
        public int CurrentBatchIndex { get; set; } = 1;
        /// <summary>
        /// æ¢ç›˜ä»»åŠ¡å½“å‰é˜¶æ®µ
        /// </summary>
        [SugarColumn(ColumnDescription = "换盘阶段", IsJsonKey = true)]
        public int ChangePalletPhase { get; set; }
        /// <summary>
        /// æ˜¯å¦æ‰«ç NG
        /// </summary>
        [SugarColumn(ColumnDescription = "是否扫码NG", IsJsonKey = true)]
        public bool IsScanNG { get; set; }
        /// <summary>
        /// æ˜¯å¦ç”µèŠ¯åˆ°ä½
        /// </summary>
        [SugarColumn(ColumnDescription = "电芯是否到位", IsJsonKey = true)]
        public bool BatteryArrived { get; set; }
    }
}
```
- [ ] **Step 2: éªŒè¯æ–‡ä»¶åˆ›å»ºæˆåŠŸ**
Run: `dir "D:\Git\ShanMeiXinNengYuan\Code\WCS\WIDESEAWCS_Server\WIDESEAWCS_Model\Models\RobotState\"`
Expected: `Dt_RobotState.cs` æ–‡ä»¶å­˜åœ¨
- [ ] **Step 3: æäº¤**
```bash
git add "WCS/WIDESEAWCS_Server/WIDESEAWCS_Model/Models/RobotState/Dt_RobotState.cs"
git commit -m "feat(RobotState): æ–°å¢ž Dt_RobotState æ•°æ®åº“实体,使用 RowVersion ä¹è§‚锁"
```
---
## Task 2: æ–°å»º IRobotStateRepository æŽ¥å£
**文件:**
- æ–°å¢ž: `WCS/WIDESEAWCS_Server/WIDESEAWCS_ITaskInfoRepository/IRobotStateRepository.cs`
- [ ] **Step 1: åˆ›å»ºæŽ¥å£æ–‡ä»¶**
```csharp
using WIDESEAWCS_Model.Models;
namespace WIDESEAWCS_ITaskInfoRepository
{
    /// <summary>
    /// æœºæ¢°æ‰‹çŠ¶æ€ä»“å‚¨æŽ¥å£
    /// </summary>
    /// <remarks>
    /// å®šä¹‰æœºæ¢°æ‰‹çŠ¶æ€çš„æ•°æ®åº“è®¿é—®æ“ä½œã€‚
    /// å¤æ‚对象(RobotCrane、CurrentTask、数组等)在调用方使用强类型,
    /// åœ¨æ­¤æŽ¥å£å±‚面以 Dt_RobotState å®žä½“为操作单位。
    /// </remarks>
    public interface IRobotStateRepository
    {
        /// <summary>
        /// æ ¹æ® IP åœ°å€èŽ·å–æœºæ¢°æ‰‹çŠ¶æ€
        /// </summary>
        /// <param name="ipAddress">设备 IP åœ°å€</param>
        /// <returns>状态实体,不存在则返回 null</returns>
        Dt_RobotState? GetByIp(string ipAddress);
        /// <summary>
        /// èŽ·å–æˆ–åˆ›å»ºæœºæ¢°æ‰‹çŠ¶æ€
        /// </summary>
        /// <param name="ipAddress">设备 IP åœ°å€</param>
        /// <param name="robotCrane">机器人设备信息,用于初始化新状态</param>
        /// <returns>状态实体</returns>
        Dt_RobotState GetOrCreate(string ipAddress, RobotCraneDevice robotCrane);
        /// <summary>
        /// å®‰å…¨æ›´æ–°æœºæ¢°æ‰‹çŠ¶æ€ï¼ˆä¹è§‚é”ï¼‰
        /// </summary>
        /// <param name="ipAddress">设备 IP åœ°å€</param>
        /// <param name="newState">新状态实体(RowVersion ä¼šè¢«æ›´æ–°ï¼‰</param>
        /// <param name="expectedRowVersion">期望的行版本号(更新前的版本)</param>
        /// <returns>是否更新成功;false è¡¨ç¤ºç‰ˆæœ¬å†²çªæˆ–记录不存在</returns>
        bool TryUpdate(string ipAddress, Dt_RobotState newState, byte[] expectedRowVersion);
        /// <summary>
        /// å°† Dt_RobotState å®žä½“转换为 RobotSocketState å†…存对象
        /// </summary>
        /// <param name="entity">数据库实体</param>
        /// <returns>内存状态对象</returns>
        RobotSocketState ToSocketState(Dt_RobotState entity);
        /// <summary>
        /// å°† RobotSocketState å†…存对象转换为 Dt_RobotState å®žä½“
        /// </summary>
        /// <param name="state">内存状态对象</param>
        /// <returns>数据库实体</returns>
        Dt_RobotState ToEntity(RobotSocketState state);
    }
}
```
- [ ] **Step 2: æäº¤**
```bash
git add "WCS/WIDESEAWCS_Server/WIDESEAWCS_ITaskInfoRepository/IRobotStateRepository.cs"
git commit -m "feat(RobotState): æ–°å¢ž IRobotStateRepository æŽ¥å£"
```
---
## Task 3: æ–°å»º RobotStateRepository å®žçް
**文件:**
- æ–°å¢ž: `WCS/WIDESEAWCS_Server/WIDESEAWCS_TaskInfoRepository/RobotStateRepository.cs`
- [ ] **Step 1: åˆ›å»º RobotStateRepository å®žçŽ°æ–‡ä»¶**
```csharp
using Newtonsoft.Json;
using SqlSugar;
using WIDESEAWCS_Core.BaseRepository;
using WIDESEAWCS_Core.UnitOfWork;
using WIDESEAWCS_ITaskInfoRepository;
using WIDESEAWCS_Model.Models;
namespace WIDESEAWCS_TaskInfoRepository
{
    /// <summary>
    /// æœºæ¢°æ‰‹çŠ¶æ€ SqlSugar ä»“储实现
    /// </summary>
    public class RobotStateRepository : IUnitOfWork, IRobotStateRepository
    {
        private readonly IUnitOfWorkManage _unitOfWork;
        private readonly SqlSugarClient _db;
        public RobotStateRepository(IUnitOfWorkManage unitOfWork)
        {
            _unitOfWork = unitOfWork;
            _db = unitOfWork.GetDbClient();
        }
        public Dt_RobotState? GetByIp(string ipAddress)
        {
            return _db.Queryable<Dt_RobotState>()
                .Where(x => x.IPAddress == ipAddress)
                .First();
        }
        public Dt_RobotState GetOrCreate(string ipAddress, RobotCraneDevice robotCrane)
        {
            var existing = GetByIp(ipAddress);
            if (existing != null)
            {
                return existing;
            }
            var newState = new Dt_RobotState
            {
                IPAddress = ipAddress,
                RobotCraneJson = JsonConvert.SerializeObject(robotCrane),
                CreateTime = DateTime.Now,
                UpdateTime = DateTime.Now
            };
            _db.Insertable(newState).ExecuteCommand();
            return newState;
        }
        public bool TryUpdate(string ipAddress, Dt_RobotState newState, byte[] expectedRowVersion)
        {
            newState.UpdateTime = DateTime.Now;
            var affectedRows = _db.Updateable<Dt_RobotState>(newState)
                .Where(x => x.IPAddress == ipAddress)
                .WhereRowVersion(x => x.RowVersion, expectedRowVersion)
                .ExecuteCommand();
            return affectedRows > 0;
        }
        public RobotSocketState ToSocketState(Dt_RobotState entity)
        {
            var state = new RobotSocketState
            {
                IPAddress = entity.IPAddress,
                Version = BitConverter.ToInt64(entity.RowVersion.Length >= 8 ? entity.RowVersion.Take(8).ToArray() : new byte[8], 0),
                IsEventSubscribed = entity.IsEventSubscribed,
                RobotRunMode = entity.RobotRunMode,
                RobotControlMode = entity.RobotControlMode,
                RobotArmObject = entity.RobotArmObject,
                Homed = entity.Homed,
                CurrentAction = entity.CurrentAction,
                OperStatus = entity.OperStatus,
                IsSplitPallet = entity.IsSplitPallet,
                IsGroupPallet = entity.IsGroupPallet,
                RobotTaskTotalNum = entity.RobotTaskTotalNum,
                IsInFakeBatteryMode = entity.IsInFakeBatteryMode,
                CurrentBatchIndex = entity.CurrentBatchIndex,
                ChangePalletPhase = entity.ChangePalletPhase,
                IsScanNG = entity.IsScanNG,
                BatteryArrived = entity.BatteryArrived
            };
            // ååºåˆ—化复杂 JSON å­—段
            if (!string.IsNullOrEmpty(entity.RobotCraneJson))
            {
                state.RobotCrane = JsonConvert.DeserializeObject<RobotCraneDevice>(entity.RobotCraneJson);
            }
            if (!string.IsNullOrEmpty(entity.CurrentTaskJson))
            {
                state.CurrentTask = JsonConvert.DeserializeObject<Dt_RobotTask>(entity.CurrentTaskJson);
            }
            if (!string.IsNullOrEmpty(entity.LastPickPositionsJson))
            {
                state.LastPickPositions = JsonConvert.DeserializeObject<int[]>(entity.LastPickPositionsJson);
            }
            if (!string.IsNullOrEmpty(entity.LastPutPositionsJson))
            {
                state.LastPutPositions = JsonConvert.DeserializeObject<int[]>(entity.LastPutPositionsJson);
            }
            if (!string.IsNullOrEmpty(entity.CellBarcodeJson))
            {
                state.CellBarcode = JsonConvert.DeserializeObject<List<string>>(entity.CellBarcodeJson) ?? new List<string>();
            }
            return state;
        }
        public Dt_RobotState ToEntity(RobotSocketState state)
        {
            var entity = new Dt_RobotState
            {
                IPAddress = state.IPAddress,
                IsEventSubscribed = state.IsEventSubscribed,
                RobotRunMode = state.RobotRunMode,
                RobotControlMode = state.RobotControlMode,
                RobotArmObject = state.RobotArmObject,
                Homed = state.Homed,
                CurrentAction = state.CurrentAction,
                OperStatus = state.OperStatus,
                IsSplitPallet = state.IsSplitPallet,
                IsGroupPallet = state.IsGroupPallet,
                RobotTaskTotalNum = state.RobotTaskTotalNum,
                IsInFakeBatteryMode = state.IsInFakeBatteryMode,
                CurrentBatchIndex = state.CurrentBatchIndex,
                ChangePalletPhase = state.ChangePalletPhase,
                IsScanNG = state.IsScanNG,
                BatteryArrived = state.BatteryArrived
            };
            // åºåˆ—化复杂对象为 JSON
            if (state.RobotCrane != null)
            {
                entity.RobotCraneJson = JsonConvert.SerializeObject(state.RobotCrane);
            }
            if (state.CurrentTask != null)
            {
                entity.CurrentTaskJson = JsonConvert.SerializeObject(state.CurrentTask);
            }
            if (state.LastPickPositions != null)
            {
                entity.LastPickPositionsJson = JsonConvert.SerializeObject(state.LastPickPositions);
            }
            if (state.LastPutPositions != null)
            {
                entity.LastPutPositionsJson = JsonConvert.SerializeObject(state.LastPutPositions);
            }
            if (state.CellBarcode != null && state.CellBarcode.Count > 0)
            {
                entity.CellBarcodeJson = JsonConvert.SerializeObject(state.CellBarcode);
            }
            return entity;
        }
        public SqlSugarClient GetDbClient() => _db;
        public void BeginTran() => _unitOfWork.BeginTran();
        public void CommitTran() => _unitOfWork.CommitTran();
        public void RollbackTran() => _unitOfWork.RollbackTran();
    }
}
```
- [ ] **Step 2: æäº¤**
```bash
git add "WCS/WIDESEAWCS_Server/WIDESEAWCS_TaskInfoRepository/RobotStateRepository.cs"
git commit -m "feat(RobotState): å®žçް RobotStateRepository,封装 RowVersion ä¹è§‚锁"
```
---
## Task 4: æ”¹é€  RobotStateManager
**文件:**
- æ”¹é€ : `WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotStateManager.cs`
**变更内容:**
- å°† `ICacheService _cache` æ›¿æ¢ä¸º `IRobotStateRepository _repository`
- å°†æ‰€æœ‰ Redis æ“ä½œï¼ˆ`_cache.Get`, `_cache.GetOrAdd`, `_cache.TrySafeUpdate`)替换为数据库操作
- `TryUpdateStateSafely` æ”¹ä¸ºä¸¤æ­¥ï¼šå…ˆé€šè¿‡ `_repository.GetByIp` èŽ·å–å½“å‰å®žä½“çš„ `RowVersion`,再调用 `_repository.TryUpdate`
- `CloneState` ä¿ç•™ï¼ˆç”¨äºŽå†…存中的深拷贝,不涉及数据库)
- ç§»é™¤ `GetCacheKey` æ–¹æ³•(不再需要 Redis Key)
- ä¿ç•™åŽŸæœ‰çš„ `RobotSocketState` å¯¹è±¡ä½œä¸ºå†…å­˜ DTO,Repository å±‚负责与 `Dt_RobotState` äº’转
- [ ] **Step 1: è¯»å–当前 RobotStateManager.cs å…¨æ–‡ï¼ˆå·²åœ¨ä¸Šä¸‹æ–‡ï¼‰**
当前代码中关键变更点:
| åŽŸä»£ç  | æ›¿æ¢ä¸º |
|--------|--------|
| `ICacheService _cache` | `IRobotStateRepository _repository` |
| `RobotStateManager(ICacheService cache, ILogger logger)` | `RobotStateManager(IRobotStateRepository repository, ILogger logger)` |
| `_cache.Get<RobotSocketState>(cacheKey)` | `_repository.GetByIp(ipAddress)` ç„¶åŽ `_repository.ToSocketState(entity)` |
| `_cache.GetOrAdd(cacheKey, _ => new RobotSocketState{...})` | `_repository.GetOrCreate(ipAddress, robotCrane)` ç„¶åŽ `_repository.ToSocketState(entity)` |
| `_cache.AddObject(cacheKey, newState)` | åœ¨ `TryUpdate` çš„"不存在"分支中调用 INSERT |
| `_cache.TrySafeUpdate(cacheKey, newState, expectedVersion, s => s.Version)` | `_repository.TryUpdate(ipAddress, entity, expectedRowVersion)` |
| `GetCacheKey(ipAddress)` | ç§»é™¤ |
- [ ] **Step 2: å†™å…¥æ”¹é€ åŽçš„完整文件**
改造后的完整 `RobotStateManager.cs`:
```csharp
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using WIDESEAWCS_Common;
using WIDESEAWCS_Core.LogHelper;
using WIDESEAWCS_ITaskInfoRepository;
using WIDESEAWCS_Model.Models;
namespace WIDESEAWCS_Tasks
{
    /// <summary>
    /// æœºæ¢°æ‰‹çŠ¶æ€ç®¡ç†å™¨ - è´Ÿè´£ RobotSocketState çš„线程安全更新和克隆
    /// </summary>
    /// <remarks>
    /// æ ¸å¿ƒåŠŸèƒ½æ˜¯é€šè¿‡ IRobotStateRepository ç®¡ç†æ•°æ®åº“中的机械手状态。
    /// æä¾›ä¹è§‚并发控制,通过 RowVersion é˜²æ­¢å¹¶å‘更新时的数据覆盖问题。
    /// </remarks>
    public class RobotStateManager
    {
        /// <summary>
        /// ä»“储服务实例,用于读写数据库中的状态数据
        /// </summary>
        private readonly IRobotStateRepository _repository;
        /// <summary>
        /// æ—¥å¿—记录器
        /// </summary>
        private readonly ILogger _logger;
        /// <summary>
        /// æž„造函数
        /// </summary>
        /// <param name="repository">仓储服务实例</param>
        /// <param name="logger">日志记录器</param>
        public RobotStateManager(IRobotStateRepository repository, ILogger logger)
        {
            _repository = repository;
            _logger = logger;
        }
        /// <summary>
        /// å®‰å…¨æ›´æ–° RobotSocketState ç¼“存,防止并发覆盖
        /// </summary>
        /// <remarks>
        /// ä½¿ç”¨ä¹è§‚并发模式:先读取当前 RowVersion,执行更新时检查版本是否一致。
        /// å¦‚æžœ RowVersion ä¸åŒ¹é…ï¼ˆè¯´æ˜Žæœ‰å…¶ä»–线程已更新),则更新失败返回 false。
        /// </remarks>
        /// <param name="ipAddress">设备 IP åœ°å€</param>
        /// <param name="updateAction">更新状态的委托函数,传入当前状态副本,返回修改后的新状态</param>
        /// <returns>是否更新成功;false è¡¨ç¤ºç‰ˆæœ¬å†²çªæˆ–状态不存在</returns>
        public bool TryUpdateStateSafely(string ipAddress, Func<RobotSocketState, RobotSocketState> updateAction)
        {
            // ä»Žæ•°æ®åº“获取当前存储的状态
            var currentEntity = _repository.GetByIp(ipAddress);
            if (currentEntity == null)
            {
                return false;
            }
            // è®°å½•当前存储的 RowVersion,作为更新时的期望版本
            var expectedRowVersion = currentEntity.RowVersion;
            // åˆ›å»ºçŠ¶æ€çš„æ·±æ‹·è´å‰¯æœ¬ï¼ˆä½¿ç”¨ JSON åºåˆ—化实现)
            var stateCopy = CloneState(_repository.ToSocketState(currentEntity));
            // æ‰§è¡Œè°ƒç”¨è€…提供的更新逻辑,传入副本状态,获取新的状态对象
            var newState = updateAction(stateCopy);
            // å°†æ–°çŠ¶æ€è½¬æ¢ä¸ºæ•°æ®åº“å®žä½“
            var newEntity = _repository.ToEntity(newState);
            newEntity.RowVersion = Array.Empty<byte>(); // SqlSugar ä¼šè‡ªåŠ¨ç®¡ç†
            newEntity.Id = currentEntity.Id;
            // è°ƒç”¨ä»“储的安全更新方法,传入期望 RowVersion
            // å¦‚æžœ RowVersion ä¸ä¸€è‡´ï¼ˆå·²è¢«å…¶ä»–线程更新),则更新失败
            return _repository.TryUpdate(ipAddress, newEntity, expectedRowVersion);
        }
        /// <summary>
        /// å®‰å…¨æ›´æ–° RobotSocketState çš„重载版本(直接传入新状态)
        /// </summary>
        /// <remarks>
        /// ä¸Žä¸Šä¸€ä¸ªé‡è½½çš„区别:此方法直接接收完整的新状态对象,而不是更新委托。
        /// å¦‚果数据库中不存在该设备的状态,则创建新记录。
        /// </remarks>
        /// <param name="ipAddress">设备 IP åœ°å€</param>
        /// <param name="newState">新状态对象</param>
        /// <returns>是否更新成功;新建设置为 true</returns>
        public bool TryUpdateStateSafely(string ipAddress, RobotSocketState newState)
        {
            // ä»Žæ•°æ®åº“获取当前存储的状态
            var currentEntity = _repository.GetByIp(ipAddress);
            // å¦‚果当前不存在该设备的状态,创建新记录
            if (currentEntity == null)
            {
                var entity = _repository.ToEntity(newState);
                entity.CreateTime = DateTime.Now;
                entity.UpdateTime = DateTime.Now;
                _repository.GetOrCreate(newState.IPAddress, newState.RobotCrane ?? new RobotCraneDevice());
                _logger.LogDebug("TryUpdateStateSafely:创建新状态,IP: {IpAddress}", ipAddress);
                QuartzLogger.Debug($"创建新状态,IP: {ipAddress}", ipAddress);
                return true;
            }
            // å½“前存在状态,记录期望 RowVersion ç”¨äºŽä¹è§‚锁检查
            var expectedRowVersion = currentEntity.RowVersion;
            // å°†æ–°çŠ¶æ€è½¬æ¢ä¸ºæ•°æ®åº“å®žä½“
            var newEntity = _repository.ToEntity(newState);
            newEntity.Id = currentEntity.Id;
            newEntity.RowVersion = Array.Empty<byte>();
            // å°è¯•安全更新,如果版本冲突则返回 false
            bool success = _repository.TryUpdate(ipAddress, newEntity, expectedRowVersion);
            if (!success)
            {
                _logger.LogWarning("TryUpdateStateSafely:版本冲突,更新失败,IP: {IpAddress},期望版本字节长度: {ExpectedLength}", ipAddress, expectedRowVersion.Length);
                QuartzLogger.Warn($"版本冲突,更新失败,IP: {ipAddress}", ipAddress);
            }
            return success;
        }
        /// <summary>
        /// å…‹éš† RobotSocketState å¯¹è±¡ï¼ˆæ·±æ‹·è´ï¼‰
        /// </summary>
        /// <remarks>
        /// ä½¿ç”¨ JSON åºåˆ—化/反序列化实现深拷贝。
        /// è¿™æ ·å¯ä»¥ç¡®ä¿æ–°å¯¹è±¡ä¸ŽåŽŸå¯¹è±¡å®Œå…¨ç‹¬ç«‹ï¼Œä¿®æ”¹æ–°å¯¹è±¡ä¸ä¼šå½±å“åŽŸå¯¹è±¡ã€‚
        /// </remarks>
        /// <param name="source">源状态对象</param>
        /// <returns>新的状态对象,是源对象的深拷贝</returns>
        public RobotSocketState CloneState(RobotSocketState source)
        {
            var json = JsonConvert.SerializeObject(source);
            return JsonConvert.DeserializeObject<RobotSocketState>(json) ?? new RobotSocketState { IPAddress = source.IPAddress };
        }
        /// <summary>
        /// ä»Žæ•°æ®åº“获取机械手状态
        /// </summary>
        /// <param name="ipAddress">设备 IP åœ°å€</param>
        /// <returns>如果存在则返回状态对象,否则返回 null</returns>
        public RobotSocketState? GetState(string ipAddress)
        {
            var entity = _repository.GetByIp(ipAddress);
            return entity != null ? _repository.ToSocketState(entity) : null;
        }
        /// <summary>
        /// èŽ·å–æˆ–åˆ›å»ºæœºæ¢°æ‰‹çŠ¶æ€
        /// </summary>
        /// <remarks>
        /// å¦‚果数据库中已存在该设备的状态,直接返回。
        /// å¦‚果不存在,则创建新的状态记录并返回。
        /// </remarks>
        /// <param name="ipAddress">设备 IP åœ°å€</param>
        /// <param name="robotCrane">机器人设备信息,用于初始化新状态</param>
        /// <returns>该设备的状态对象</returns>
        public RobotSocketState GetOrCreateState(string ipAddress, RobotCraneDevice robotCrane)
        {
            var entity = _repository.GetOrCreate(ipAddress, robotCrane);
            return _repository.ToSocketState(entity);
        }
    }
}
```
- [ ] **Step 3: æäº¤**
```bash
git add "WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotStateManager.cs"
git commit -m "refactor(RobotState): å°† RobotStateManager ä»Ž Redis æ”¹ä¸ºä¾èµ–数据库仓储"
```
---
## Task 5: æ”¹é€  RobotJob
**文件:**
- æ”¹é€ : `WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotJob.cs`
**变更内容:**
- æž„造函数增加 `IRobotStateRepository` å‚æ•°
- å°† `new RobotStateManager(cache, _logger)` æ”¹ä¸º `new RobotStateManager(repository, _logger)`
- ç§»é™¤æœªä½¿ç”¨çš„ `ICacheService cache` å‚数(如果原来有)
注意:根据 `JobFactory.cs`,`RobotJob` é€šè¿‡ `IServiceProvider.GetService(bundle.JobDetail.JobType)` è§£æžï¼Œ
所有构造函数参数如果都实现了 `IDependency` æŽ¥å£æˆ–被 Autofac æ‰«æåˆ°ï¼Œå°±ä¼šè‡ªåŠ¨æ³¨å…¥ã€‚
- [ ] **Step 1: æ”¹é€  RobotJob æž„造函数**
在 `RobotJob` æž„造函数中:
**原来的构造:**(参考上文已读取的代码)
```csharp
public RobotJob(
    TcpSocketServer tcpSocket,
    IRobotTaskService robotTaskService,
    ITaskService taskService,
    ICacheService cache,
    HttpClientHelper httpClientHelper,
    ILogger<RobotJob> logger,
    IFakeBatteryPositionService fakeBatteryPositionService)
{
    _stateManager = new RobotStateManager(cache, _logger);
    // ...
}
```
**改造后:**
```csharp
public RobotJob(
    TcpSocketServer tcpSocket,
    IRobotTaskService robotTaskService,
    ITaskService taskService,
    IRobotStateRepository robotStateRepository,
    HttpClientHelper httpClientHelper,
    ILogger<RobotJob> logger,
    IFakeBatteryPositionService fakeBatteryPositionService)
{
    _stateManager = new RobotStateManager(robotStateRepository, _logger);
    // ...
}
```
同时在类成员声明处,确保 `_stateManager` ç±»åž‹æ­£ç¡®ï¼ˆå·²åœ¨ä¸Šä¸€æ­¥çš„ RobotStateManager æ”¹é€ ä¸­å®Œæˆï¼‰ã€‚
- [ ] **Step 2: æäº¤**
```bash
git add "WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotJob.cs"
git commit -m "refactor(RobotJob): æž„造函数从 ICacheService æ”¹ä¸º IRobotStateRepository"
```
---
## Task 6: DI æ³¨å†ŒéªŒè¯
**文件:**
- æ— éœ€æ–°å»ºæ–‡ä»¶ï¼Œä½†éœ€è¦éªŒè¯ AutofacModuleRegister çš„自动扫描能覆盖新增的 `RobotStateRepository`
**说明:**
`AutofacModuleRegister.Load()` ä¸­æœ‰ä»¥ä¸‹è‡ªåŠ¨æ³¨å†Œé€»è¾‘ï¼š
```csharp
builder.RegisterAssemblyTypes(assemblyList.ToArray())
    .Where(x => !x.IsInterface && !x.IsAbstract && baseType.IsAssignableFrom(x))
    .AsImplementedInterfaces()
    .PropertiesAutowired()
    .InstancePerDependency()
    .EnableInterfaceInterceptors()
    .InterceptedBy(cacheType.ToArray());
```
这意味着只要 `RobotStateRepository` å®žçŽ°äº† `IRobotStateRepository` æŽ¥å£å¹¶ä¸”:
- æ‰€åœ¨ç¨‹åºé›†è¢« `assemblyList` åŒ…含(`WIDESEAWCS_TaskInfoRepository` æ˜¯é¡¹ç›®å¼•用)
- å®žçŽ°äº† `IDependency`(`IUnitOfWorkManage` ç»§æ‰¿é“¾ä¸Šæœ‰ `IDependency`)
**不需要额外注册。** å¦‚æžœ Autofac éœ€è¦æ˜¾å¼æ³¨å†Œï¼Œåœ¨ `AutofacModuleRegister.cs` ä¸­æ·»åŠ ï¼š
```csharp
builder.RegisterType<RobotStateRepository>().As<IRobotStateRepository>().InstancePerDependency();
```
- [ ] **Step 1: éªŒè¯æž„建**
Run: `dotnet build WCS/WIDESEAWCS_Server/WIDESEAWCS_Server.sln --no-restore`
Expected: ç¼–译成功,无错误
- [ ] **Step 2: å¦‚有错误,根据错误信息调整 DI æ³¨å†Œ**
---
## Task 7: æ•´ä½“验证
- [ ] **Step 1: å®Œæ•´æž„建**
Run: `dotnet build WCS/WIDESEAWCS_Server/WIDESEAWCS_Server.sln`
Expected: BUILD SUCCEEDED
- [ ] **Step 2: æ£€æŸ¥æ˜¯å¦æœ‰é—漏的 Redis å¼•用**
在 `RobotJob/` æ–‡ä»¶å¤¹å†…搜索是否还有 `_cache` æˆ– `Redis` ç›¸å…³å¼•用:
```bash
grep -r "ICacheService\|_cache\." "WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/" --include="*.cs"
```
Expected: ä»… RobotStateManager.cs ä¸­æœ‰ `_cache` ç›¸å…³å·²æ›¿æ¢ï¼Œæ— é—漏
- [ ] **Step 3: æäº¤å…¨éƒ¨å˜æ›´**
```bash
git add -A
git commit -m "feat(RobotState): å®Œæˆ Redis→数据库迁移,IRobotStateRepository æ›¿æ¢ ICacheService"
```
Code/docs/superpowers/specs/2026-04-19-robot-state-redis-to-db-design.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,195 @@
# RobotState Redis â†’ æ•°æ®åº“迁移设计
**日期**:2026-04-19
**状态**:已批准
## 1. èƒŒæ™¯ä¸Žç›®æ ‡
将 `RobotSocketState` çš„存储从 Redis åˆ‡æ¢åˆ° SQL Server æ•°æ®åº“,利用 SqlSugar çš„ `RowVersion` å®žçŽ°ä¹è§‚å¹¶å‘æŽ§åˆ¶ã€‚
### æ¶‰åŠèŒƒå›´
- `RobotJob/` æ–‡ä»¶å¤¹ä¸‹æ‰€æœ‰ä½¿ç”¨ Redis è¯»å†™ `RobotSocketState` çš„代码
- æ–°å»º `Dt_RobotState` æ•°æ®åº“实体和 `IRobotStateRepository` ä»“储层
### ä¸æ¶‰åŠå˜æ›´
- `RobotWorkflowOrchestrator`、`RobotTaskProcessor`、`RobotSimpleCommandHandler` ç­‰ä¸šåŠ¡é€»è¾‘ä¸å˜
- ä¾èµ– `RobotStateManager` çš„调用方代码不变,只改存储后端
---
## 2. æ•°æ®åº“实体设计
**表名**:`Dt_RobotState`
**实体路径**:`WIDESEAWCS_Model/Models/RobotState/Dt_RobotState.cs`
| å­—段名 | ç±»åž‹ | è¯´æ˜Ž |
|--------|------|------|
| `Id` | int | ä¸»é”®ï¼Œè‡ªå¢ž |
| `IPAddress` | string(50) | è®¾å¤‡IP,唯一索引 |
| `RowVersion` | byte[] | SqlSugar è¡Œç‰ˆæœ¬ï¼Œå¹¶å‘控制 |
| `IsEventSubscribed` | bool | æ˜¯å¦å·²è®¢é˜…消息 |
| `RobotRunMode` | int? | è¿è¡Œæ¨¡å¼ï¼ˆ1手动 2自动) |
| `RobotControlMode` | int? | æŽ§åˆ¶æ¨¡å¼ï¼ˆ1客户端控制 2其他) |
| `RobotArmObject` | int? | æ‰‹è‡‚抓取状态(0无物料 1有物料) |
| `RobotCraneJson` | string(max) | è®¾å¤‡ä¿¡æ¯åºåˆ—化 JSON |
| `Homed` | string(50) | å›žé›¶çŠ¶æ€ï¼ˆHomed/Homing) |
| `CurrentAction` | string(50) | å½“前动作(Picking/Putting/PickFinished ç­‰ï¼‰ |
| `OperStatus` | string(50) | è¿è¡ŒçŠ¶æ€ï¼ˆRunning/Pausing/Emstoping ç­‰ï¼‰ |
| `LastPickPositionsJson` | string(max) | å–货位置数组 JSON |
| `LastPutPositionsJson` | string(max) | æ”¾è´§ä½ç½®æ•°ç»„ JSON |
| `CellBarcodeJson` | string(max) | ç”µèŠ¯æ¡ç åˆ—è¡¨ JSON |
| `CurrentTaskJson` | string(max) | å½“前任务 Dt_RobotTask åºåˆ—化 JSON |
| `IsSplitPallet` | bool | æ˜¯å¦æ‹†ç›˜ä»»åŠ¡ |
| `IsGroupPallet` | bool | æ˜¯å¦ç»„盘/换盘任务 |
| `RobotTaskTotalNum` | int | å·²å¤„理任务总数 |
| `IsInFakeBatteryMode` | bool | æ˜¯å¦å‡ç”µèŠ¯è¡¥å……æ¨¡å¼ |
| `CurrentBatchIndex` | int | å½“前批次起始编号 |
| `ChangePalletPhase` | int | æ¢ç›˜ä»»åŠ¡é˜¶æ®µï¼ˆ0-5) |
| `IsScanNG` | bool | æ˜¯å¦æ‰«ç NG |
| `BatteryArrived` | bool | ç”µèŠ¯æ˜¯å¦åˆ°ä½ |
| `CreateTime` | datetime | åˆ›å»ºæ—¶é—´ |
| `UpdateTime` | datetime | æœ€åŽæ›´æ–°æ—¶é—´ |
**索引**:`IPAddress` å”¯ä¸€ç´¢å¼•,用于快速定位设备状态
**并发控制**:SqlSugar `RowVersion`,数据库自动递增,更新时 `WHERE RowVersion = @expected`
---
## 3. JSON åºåˆ—化字段
以下复杂对象以 JSON å­—符串存储,反序列化时保持与原 `RobotSocketState` å±žæ€§å®Œå…¨å…¼å®¹ï¼š
| JSON å­—段 | å¯¹åº”原属性 | ååºåˆ—化类型 |
|-----------|-----------|-------------|
| `RobotCraneJson` | `RobotCrane` | `RobotCraneDevice` |
| `CurrentTaskJson` | `CurrentTask` | `Dt_RobotTask` |
| `LastPickPositionsJson` | `LastPickPositions` | `int[]` |
| `LastPutPositionsJson` | `LastPutPositions` | `int[]` |
| `CellBarcodeJson` | `CellBarcode` | `List<string>` |
序列化工具:`Newtonsoft.Json`(与项目现有保持一致)
---
## 4. æž¶æž„分层
```
调用方(RobotJob / Workflow / Processor)
        â†“ ä¾èµ–
RobotStateManager
        â†“ ä¾èµ–
IRobotStateRepository(接口)
        â†“ å®žçް
RobotStateRepository(SqlSugar å®žçŽ°ï¼‰
        â†“ æ“ä½œ
SQL Server (Dt_RobotState è¡¨)
```
### 4.1 IRobotStateRepository æŽ¥å£
```csharp
public interface IRobotStateRepository
{
    /// <summary>根据 IP èŽ·å–çŠ¶æ€ï¼Œä¸å­˜åœ¨è¿”å›ž null</summary>
    Dt_RobotState? GetByIp(string ipAddress);
    /// <summary>获取或创建状态(数据库无记录时创建)</summary>
    Dt_RobotState GetOrCreate(string ipAddress, RobotCraneDevice robotCrane);
    /// <summary>安全更新(乐观锁),返回是否成功</summary>
    bool TryUpdate(string ipAddress, Dt_RobotState newState, byte[] expectedRowVersion);
}
```
### 4.2 RobotStateRepository å®žçŽ°è¦ç‚¹
- æ³¨å…¥ `ISqlSugarClient`
- `GetOrCreate`:先查,无记录则 INSERT
- `TryUpdate`:执行 `UPDATE ... WHERE RowVersion = @expected`,检查影响行数
- æ•°ç»„/复杂对象:在 Repository å±‚序列化/反序列化,对外暴露强类型属性
---
## 5. RobotStateManager æ”¹é€ 
**文件**:`WIDESEAWCS_Tasks/RobotJob/RobotStateManager.cs`
### æ”¹é€ å†…容
| åŽŸæ¥ï¼ˆRedis) | æ”¹é€ åŽï¼ˆDB) |
|-------------|-------------|
| `ICacheService _cache` | `IRobotStateRepository _repository` |
| `GetState(ipAddress)` | `_repository.GetByIp(ipAddress)` |
| `GetOrCreateState(ipAddress, robotCrane)` | `_repository.GetOrCreate(ipAddress, robotCrane)` |
| `TryUpdateStateSafely(ipAddress, func)` | å†…部调用 `_repository.TryUpdate`,使用 `RowVersion` ä½œä¸ºæœŸæœ›ç‰ˆæœ¬ |
| `CloneState` | ä¿ç•™ï¼ˆJSON åºåˆ—化深拷贝) |
| `GetCacheKey(ipAddress)` | ç§»é™¤ï¼ˆä¸å†éœ€è¦ Redis Key) |
### æž„造函数变更
```csharp
// åŽŸæ¥
public RobotStateManager(ICacheService cache, ILogger logger)
// æ”¹é€ åŽ
public RobotStateManager(IRobotStateRepository repository, ILogger logger)
```
---
## 6. RobotJob æž„造函数改造
**文件**:`WIDESEAWCS_Tasks/RobotJob/RobotJob.cs`
```csharp
// åŽŸæ¥
_stateManager = new RobotStateManager(cache, _logger);
// æ”¹é€ åŽï¼šéœ€è¦é€šè¿‡ DI æ³¨å…¥ IRobotStateRepository
_stateManager = new RobotStateManager(
    ResolvedInstances.FirstOrDefault(typeof(IRobotStateRepository)) as IRobotStateRepository,
    _logger);
```
**备选方案**:如果 DI å®¹å™¨åœ¨ Job æž„造时不便解析,可通过方法参数注入 `IRobotStateRepository`。
---
## 7. æ–‡ä»¶å˜æ›´æ¸…单
| æ“ä½œ | æ–‡ä»¶è·¯å¾„ |
|------|---------|
| æ–°å¢ž | `WIDESEAWCS_Model/Models/RobotState/Dt_RobotState.cs` |
| æ–°å¢ž | `WIDESEAWCS_ITaskInfoRepository/IRobotStateRepository.cs` |
| æ–°å¢ž | `WIDESEAWCS_ITaskInfoRepository/RobotStateRepository.cs` |
| æ”¹é€  | `WIDESEAWCS_Tasks/RobotJob/RobotStateManager.cs` |
| æ”¹é€  | `WIDESEAWCS_Tasks/RobotJob/RobotJob.cs` |
| æ”¹é€  | `WIDESEAWCS_Tasks/RobotJob/RobotSocketState.cs`(移除或保留为 DTO?) |
**说明**:`RobotSocketState.cs` å»ºè®®ä¿ç•™ä½œä¸ºå†…存中的状态对象(DTO),在 Repository å±‚做实体转换。业务层继续使用 `RobotSocketState`,Repository å±‚负责与 `Dt_RobotState` äº’转。
---
## 8. ä¾èµ–注入注册
在 `AutofacModuleRegister` æˆ–对应 DI é…ç½®ä¸­æ³¨å†Œï¼š
```csharp
builder.RegisterType<RobotStateRepository>().As<IRobotStateRepository>().InstancePerDependency();
```
---
## 9. è¿ç§»æ­¥éª¤ï¼ˆå®žæ–½è®¡åˆ’)
1. **新建 `Dt_RobotState` å®žä½“ç±»**
2. **新建 `IRobotStateRepository` æŽ¥å£å’Œ `RobotStateRepository` å®žçް**
3. **改造 `RobotStateManager`**:依赖 Repository,替换 Redis è°ƒç”¨
4. **改造 `RobotJob`**:注入 Repository åˆ° StateManager
5. **更新 `RobotSocketState`**:作为内存 DTO ä¿ç•™ï¼Œæˆ–与实体合并(待定)
6. **配置 DI æ³¨å†Œ**
7. **测试验证**:确保并发更新、状态流转逻辑与原来一致
ÏîÄ¿×ÊÁÏ/É豸ЭÒé/ÉÏλϵͳ¶Ô½Ó/ÉÂÎ÷Çê¿ÌÄÜÔ´¿Æ¼¼MESϵͳ¶Ô½Ó½Ó¿Ú.pdf
Binary files differ