编辑 | blame | 历史 | 原始文档

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: 创建实体类文件
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: 提交
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: 创建接口文件
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: 提交
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 实现文件
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: 提交
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

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: 提交
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.csRobotJob 通过 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: 提交
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 是项目引用)
- 实现了 IDependencyIUnitOfWorkManage 继承链上有 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/ 文件夹内搜索是否还有 _cacheRedis 相关引用:
bash grep -r "ICacheService\|_cache\." "WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/" --include="*.cs"
Expected: 仅 RobotStateManager.cs 中有 _cache 相关已替换,无遗漏

  • [ ] Step 3: 提交全部变更
git add -A
git commit -m "feat(RobotState): 完成 Redis→数据库迁移,IRobotStateRepository 替换 ICacheService"