wanshenmean
2026-03-04 17e5dbd7bd0364e27a33f1a7dab91cf33d5dcabc
增强Redis缓存服务与设备通信优化

- 新增缓存删除扩展方法(RemoveByPrefix/RemoveAndGet)
- 支持L1/L2缓存独立开关配置
- 新增BaseAPI枚举与HTTP常量定义
- 优化Omron PLC与串口通信器
- 重构机器人、输送线、穿梭车任务逻辑
- 完善WMS库存与任务控制器
已添加9个文件
已修改34个文件
2594 ■■■■■ 文件已修改
Code/WCS/WIDESEAWCS_Server/CLAUDE.md 172 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Common/HttpEnum/BaseAPI.cs 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Common/HttpEnum/ConfigKey.cs 46 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Common/Redis/RedisName.cs 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Common/Redis/RedisPrefix.cs 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Communicator/Omron/OmronEtherNetCommunicator.cs 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Communicator/SerialPortComm/SerialPortCommunicator.cs 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Core/BaseRepository/UnitOfWorks/UnitOfWorkManage.cs 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Core/Caches/ICacheService.cs 153 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Core/Caches/MemoryCacheService.cs 202 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Core/Const/CommunicationConst.cs 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Core/Helper/AppSettings.cs 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Core/Helper/HttpHelper.cs 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Core/Http/HTTP/HttpClientHelper.cs 32 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Core/LogHelper/Logger.cs 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Core/WIDESEAWCS_Core.csproj 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_QuartzJob/QuartzNet/QuartzNetExtension.cs 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_QuartzJob/QuartzNet/SchedulerCenterServer.cs 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_QuartzJob/Service/RouterService.cs 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_QuartzJob/StackerCrane/Spec/SpeTemperatureStackerCrane.cs 36 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_QuartzJob/WIDESEAWCS_QuartzJob.csproj 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_RedisService/Cache/CacheSyncBackgroundService.cs 211 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_RedisService/Cache/HybridCacheService.cs 550 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_RedisService/Cache/RedisCacheService.cs 226 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_RedisService/Connection/RedisConnectionManager.cs 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_RedisService/Extensions/RedisServiceSetup.cs 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_RedisService/Options/RedisOptions.cs 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Server.sln 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Server/Redis使用案例.md 270 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Server/appsettings.json 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_TaskInfoService/TaskExecuteDetailService.cs 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_TaskInfoService/TaskService.cs 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/CommonConveyorLineNewJob.cs 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotJob.cs 139 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ShuttleCarJob/ShuttleCarJob.cs 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/SocketServer/TcpSocketServer.Server.cs 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tests/CommunicationConstTests.cs 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tests/UnitTest1.cs 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tests/WIDESEAWCS_Tests.csproj 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_IStockService/IStockService.cs 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_StockService/StockSerivce.cs 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Stock/StockController.cs 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/TaskInfo/TaskController.cs 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/CLAUDE.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,172 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Build Commands
```bash
# Build entire solution
dotnet build WIDESEAWCS_Server.sln
# Build and run server
cd WIDESEAWCS_Server
dotnet run
# Run tests
cd WIDESEAWCS_Tests
dotnet test
```
## Architecture Overview
This is a **WCS (Warehouse Control System)** built with ASP.NET Core 6.0, using:
- **Autofac** for DI with automatic service discovery via `IDependency` marker interface
- **Quartz.NET** for scheduled job execution (device communication loops)
- **SqlSugar ORM** for database access
- **Redis** (via `WIDESEAWCS_RedisService`) for distributed caching with L1+L2 hybrid pattern
- **StackExchange.Redis** for Redis operations
- **TCP Socket Server** for real-time device communication
- **HslCommunication** library for PLC/hardware communication
## Project Structure
```
WIDESEAWCS_Server/          # Main ASP.NET Core API server
WIDESEAWCS_Core/            # Core infrastructure: base classes, DI, extensions, middleware
WIDESEAWCS_Model/           # Data models and DTOs
WIDESEAWCS_Communicator/    # Hardware communication drivers (Siemens, Omron, Modbus, etc.)
WIDESEAWCS_QuartzJob/       # Job scheduling infrastructure and device abstractions
WIDESEAWCS_Tasks/           # Quartz job implementations (device communication loops)
WIDESEAWCS_RedisService/    # Redis services: Cache, Lock, Counter, PubSub, etc.
WIDESEAWCS_*Repository/     # Data access layer implementations
WIDESEAWCS_*Service/        # Business service layer
WIDESEAWCS_Tests/           # Unit tests
```
## Dependency Injection - IDependency Pattern
Services are **automatically registered** with Autofac by implementing the empty `IDependency` marker interface:
```csharp
// In WIDESEAWCS_Core/IDependency.cs
public interface IDependency { }
// Your service gets auto-registered
public class MyService : IDependency  // Automatically registered as scoped
{
    // ...
}
```
Registration happens in `AutofacModuleRegister` which scans all project assemblies for `IDependency` implementations.
**Important**: When adding services to `IServiceCollection` (e.g., in `Program.cs`), they can be overridden by Autofac's registrations. Use `Remove()` to replace existing registrations:
```csharp
// In RedisServiceSetup.cs - removes MemoryCacheService before adding HybridCacheService
var existing = services.FirstOrDefault(d => d.ServiceType == typeof(ICacheService));
if (existing != null) services.Remove(existing);
```
## Caching - ICacheService
The system uses a **hybrid L1 (Memory) + L2 (Redis)** cache pattern via `ICacheService`. Three implementations exist:
- `MemoryCacheService` - Memory only
- `RedisCacheService` - Redis only
- `HybridCacheService` - L1+L2 with fallback (default when Redis enabled)
**Common methods**:
- `Add/AddObject` - Add cache
- `Get/Get<T>` - Retrieve cached values
- `Remove` - Delete single key
- `RemoveByPrefix/RemoveByPattern` - Bulk delete by pattern
- `GetOrAdd<T>` - Retrieve or add with factory
- `TryAdd/TryUpdate/TryUpdateIfChanged` - ConcurrentDictionary-style operations
**Configuration** in `appsettings.json`:
```json
"RedisConfig": {
  "Enabled": true,
  "ConnectionString": "127.0.0.1:6379,password=P@ssw0rd,...",
  "KeyPrefix": "wcs:"
}
```
## Quartz Jobs - Device Communication
Jobs inherit from `JobBase` and implement Quartz's `IJob`:
```csharp
public class MyDeviceJob : JobBase, IJob
{
    public async Task Execute(IJobExecutionContext context)
    {
        ExecuteJob(context, async () => {
            // Job logic here
            WriteDebug("MyDevice", "Debug message");
            WriteInfo("MyDevice", "Info message");
        });
    }
}
```
Jobs are registered dynamically via `SchedulerCenterServer` using device info from `Dt_DeviceInfo` table.
**Device types**:
- `IStackerCrane` - Stacker cranes
- `IConveyorLine` - Conveyor lines
- `IShuttleCar` - Shuttle cars
- `IRobot` - Robot cranes
## Hardware Communication
Communicator classes wrap the `HslCommunication` library:
- `SiemensS7Communicator` / `SiemensS7200SmartCommunicator` - Siemens PLCs
- `OmronEtherNetCommunicator` - Omron PLCs
- `ModbusTcpCommunicator` - Modbus TCP
- `SerialPortCommunicator` - Serial port devices
## TCP Socket Server
The `TcpSocketServer` (port 2000) handles real-time device communication:
- Managed as a Singleton with `SocketServerHostedService`
- Client connections stored in `ConcurrentDictionary<string, TcpClient>`
- Messages handled via `OnDataReceived` event
## Configuration Settings
Key settings in `appsettings.json`:
- `"urls": "http://*:9292"` - Server port
- `"QuartzJobAutoStart": true` - Auto-start scheduled jobs
- `"SocketServer:Enabled": true` - Enable TCP server
- `"RedisConfig:Enabled": true` - Enable Redis caching
- `"LogAOPEnable": false` - Enable AOP logging
- `"DBType": "SqlServer"` - Database type
## Service Layer Pattern
Services follow a layered pattern:
- **Interface** in `WIDESEAWCS_IService/` (e.g., `ITaskInfoService`)
- **Implementation** in `WIDESEAWCS_Service/` (e.g., `TaskInfoService`)
- Both implement `IDependency` for auto-registration
## Base Classes
- `ServiceBase<T, TKey>` - Base service with CRUD operations
- `RepositoryBase<TEntity>` - Base repository with SqlSugar ORM
- `ApiBaseController` - Base API controller with common functionality
- `JobBase` - Base Quartz job with logging helpers
## Adding New Features
1. **New Service**: Create interface in `I*Service/` and class in `*Service/`, implement `IDependency`
2. **New Job**: Inherit from `JobBase` and `IJob` in `WIDESEAWCS_Tasks/`
3. **New Device Type**: Add interface in `WIDESEAWCS_QuartzJob/Device/` and implement
## Important Notes
- The application uses **CamelCase** JSON serialization
- All services use **scoped** lifetime by default via Autofac
- Redis connection uses **Lazy initialization** - first access triggers connection
- Use `ConsoleHelper.WriteSuccessLine()` / `WriteErrorLine()` for console output in jobs
- TCP Socket server runs independently of the HTTP API
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Common/HttpEnum/BaseAPI.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WIDESEAWCS_Common
{
    public class BaseAPI
    {
        /// <summary>
        /// WMS接口基础URL
        /// </summary>
        public const string WMSBaseUrl = "http://localhost:9291/api/";
        /// <summary>
        /// WCS接口基础URL
        /// </summary>
        public const string WCSBaseUrl = "http://localhost:9292/api/";
        /// <summary>
        /// MES接口基础URL
        /// </summary>
        public const string MESBaseUrl = "http://localhost:9293/api/";
        /// <summary>
        /// ERP接口基础URL
        /// </summary>
        public const string ERPBaseUrl = "http://localhost:9294/api/";
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Common/HttpEnum/ConfigKey.cs
@@ -1,27 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WIDESEAWCS_Common.HttpEnum
namespace WIDESEAWCS_Common.HttpEnum
{
    public enum ConfigKey
    {
        ERP_API_Base,
        ERP_API_Url,
        ERP_API_Timeout,
        WMS_API_Base,
        WMS_API_Url,
        WMS_API_Timeout,
        MES_API_Base,
        MES_API_Url,
        MES_API_Timeout,
        #region WMS接口
        /// <summary>
        /// ç»„盘
        /// </summary>
@@ -45,8 +27,28 @@
        /// <summary>
        /// èŽ·å–ä»»åŠ¡å¯å…¥è´§ä½
        /// </summary>
        GetTasksLocation
        GetTasksLocation,
        #endregion
        /// <summary>
        /// å‡ºåº“任务完成
        /// </summary>
        OutboundFinishTaskAsync,
        /// <summary>
        /// å…¥åº“任务完成
        /// </summary>
        InboundFinishTaskAsync,
        /// <summary>
        /// åˆ›å»ºç©ºæ‰˜ç›˜å‡ºåº“任务
        /// </summary>
        GetOutBoundTrayTaskAsync,
        /// <summary>
        /// åˆ›å»ºç©ºæ‰˜ç›˜å…¥åº“任务
        /// </summary>
        CreateTaskInboundTrayAsync,
        #endregion WMS接口
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Common/Redis/RedisName.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WIDESEAWCS_Common
{
    public class RedisName
    {
        /// <summary>
        /// Socket设备列表,清除缓存时会用到,清除以SocketDevices开头的缓存
        /// </summary>
        public const string SocketDevices = "SocketDevices";
        /// <summary>
        /// è®¾å¤‡åˆ—表,清除缓存时会用到,清除以IDevice开头的缓存
        /// </summary>
        public const string IDevice = "IDevice";
        /// <summary>
        /// è®¾å¤‡äº¤äº’位置列表,清除缓存时会用到,清除以DevicePositions开头的缓存
        /// </summary>
        public const string DevicePositions = "DevicePositions";
        /// <summary>
        /// API列表,清除缓存时会用到,清除以DevicePositions开头的缓存
        /// </summary>
        public const string API = "API";
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Common/Redis/RedisPrefix.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WIDESEAWCS_Common
{
    /// <summary>
    /// Redis前缀类,定义了系统、用户、代码等前缀,清除缓存时会用到,清除以这些前缀开头的缓存
    /// </summary>
    public class RedisPrefix
    {
        /// <summary>
        /// ç³»ç»Ÿå‰ç¼€ï¼Œæ¸…除缓存时会用到,清除以System开头的缓存
        /// </summary>
        public const string System = "System";
        /// <summary>
        /// ç”¨æˆ·å‰ç¼€ï¼Œæ¸…除缓存时会用到,清除以User开头的缓存
        /// </summary>
        public const string User = "User";
        /// <summary>
        /// ä»£ç å‰ç¼€ï¼Œæ¸…除缓存时会用到,清除以Code开头的缓存
        /// </summary>
        public const string Code = "Code";
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Communicator/Omron/OmronEtherNetCommunicator.cs
@@ -24,6 +24,13 @@
    [Description("欧姆龙EtherNet/IP(CIP)")]
    public class OmronEtherNetCommunicator : BaseCommunicator
    {
        #region Constants
        /// <summary>
        /// Ping æ£€æµ‹é—´é𔿗¶é—´ï¼ˆæ¯«ç§’)
        /// </summary>
        private const int PingIntervalMs = 100;
        #endregion Constants
        #region Private Member
        /// <summary>
        /// HSLCommunication的西门子的S7协议的通讯类
@@ -328,7 +335,7 @@
        private void Ping()
        {
            Task.Run(() =>
            Task.Run(async () =>
            {
                while (_isPing)
                {
@@ -342,7 +349,7 @@
                    }
                    finally
                    {
                        Thread.Sleep(100);
                        await Task.Delay(PingIntervalMs);
                    }
                }
            });
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Communicator/SerialPortComm/SerialPortCommunicator.cs
@@ -120,7 +120,7 @@
            }
            catch (Exception ex)
            {
                LogNet.WriteException(Name, $"串口数据接收异常,串口号:{_serialPortName}", ex);
            }
        }
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Core/BaseRepository/UnitOfWorks/UnitOfWorkManage.cs
@@ -6,6 +6,7 @@
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using WIDESEAWCS_Core.Helper;
@@ -135,9 +136,10 @@
            lock (this)
            {
                string result = "";
                var spinner = new SpinWait();
                while (!TranStack.IsEmpty && !TranStack.TryPeek(out result))
                {
                    Thread.Sleep(1);
                    spinner.SpinOnce();
                }
@@ -158,9 +160,10 @@
                    }
                    finally
                    {
                        var spinner2 = new SpinWait();
                        while (!TranStack.TryPop(out _))
                        {
                            Thread.Sleep(1);
                            spinner2.SpinOnce();
                        }
                        _tranCount = TranStack.Count;
@@ -185,9 +188,10 @@
            lock (this)
            {
                string result = "";
                var spinner = new SpinWait();
                while (!TranStack.IsEmpty && !TranStack.TryPeek(out result))
                {
                    Thread.Sleep(1);
                    spinner.SpinOnce();
                }
                if (result == method.GetFullName())
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Core/Caches/ICacheService.cs
@@ -45,6 +45,52 @@
        /// <returns></returns>
        void Remove(IEnumerable<string> keys);
        #region åˆ é™¤æ‰©å±•方法
        /// <summary>
        /// åˆ é™¤å¹¶èŽ·å–å€¼ï¼šåˆ é™¤æŒ‡å®šKey并返回其值
        /// </summary>
        /// <param name="key">缓存Key</param>
        /// <returns>被删除的值,不存在返回null</returns>
        string? RemoveAndGet(string key);
        /// <summary>
        /// åˆ é™¤å¹¶èŽ·å–å¯¹è±¡ï¼šåˆ é™¤æŒ‡å®šKey并返回其对象值
        /// </summary>
        /// <param name="key">缓存Key</param>
        /// <returns>被删除的对象,不存在返回null</returns>
        T? RemoveAndGet<T>(string key) where T : class;
        /// <summary>
        /// æŒ‰å‰ç¼€åˆ é™¤ï¼šåˆ é™¤æ‰€æœ‰ä»¥æŒ‡å®šå‰ç¼€å¼€å¤´çš„Key
        /// </summary>
        /// <param name="prefix">Key前缀</param>
        /// <returns>删除的数量</returns>
        int RemoveByPrefix(string prefix);
        /// <summary>
        /// æŒ‰æ¨¡å¼åˆ é™¤ï¼šåˆ é™¤åŒ¹é…æŒ‡å®šæ¨¡å¼çš„æ‰€æœ‰Key(支持*通配符)
        /// </summary>
        /// <param name="pattern">匹配模式,如 "user:*", "session:123:*"</param>
        /// <returns>删除的数量</returns>
        int RemoveByPattern(string pattern);
        /// <summary>
        /// æ‰¹é‡åˆ é™¤å¹¶è¿”回成功数量
        /// </summary>
        /// <param name="keys">缓存Key集合</param>
        /// <returns>成功删除的数量</returns>
        int RemoveAll(IEnumerable<string> keys);
        /// <summary>
        /// æ¡ä»¶åˆ é™¤ï¼šåˆ é™¤æ»¡è¶³æŒ‡å®šæ¡ä»¶çš„æ‰€æœ‰Key
        /// </summary>
        /// <param name="predicate">条件谓词</param>
        /// <returns>删除的数量</returns>
        int RemoveWhere(Func<string, bool> predicate);
        #endregion
        /// <summary>
        /// èŽ·å–ç¼“å­˜
        /// </summary>
@@ -60,6 +106,95 @@
        /// <param name="key">缓存Key</param>
        /// <returns></returns>
        string? Get(string key);
        #region æ·»åŠ å’Œä¿®æ”¹æ‰©å±•æ–¹æ³•
        /// <summary>
        /// æ‰¹é‡æ·»åŠ ç¼“å­˜
        /// </summary>
        void AddAll(IDictionary<string, string> items, int expireSeconds = -1);
        /// <summary>
        /// æ‰¹é‡æ·»åŠ å¯¹è±¡ç¼“å­˜
        /// </summary>
        void AddAllObjects(IDictionary<string, object> items, int expireSeconds = -1);
        /// <summary>
        /// æ›¿æ¢ï¼šä»…当Key存在时替换其值
        /// </summary>
        bool Replace(string key, string newValue, int expireSeconds = -1);
        /// <summary>
        /// æ›¿æ¢å¯¹è±¡ï¼šä»…当Key存在时替换其值
        /// </summary>
        bool Replace<T>(string key, T newValue, int expireSeconds = -1) where T : class;
        /// <summary>
        /// èŽ·å–å¹¶åˆ·æ–°ï¼šèŽ·å–å€¼å¹¶åˆ·æ–°å…¶è¿‡æœŸæ—¶é—´
        /// </summary>
        string? GetAndRefresh(string key, int expireSeconds);
        /// <summary>
        /// èŽ·å–å¹¶åˆ·æ–°å¯¹è±¡ï¼šèŽ·å–å¯¹è±¡å¹¶åˆ·æ–°å…¶è¿‡æœŸæ—¶é—´
        /// </summary>
        T? GetAndRefresh<T>(string key, int expireSeconds) where T : class;
        /// <summary>
        /// åˆ·æ–°è¿‡æœŸæ—¶é—´ï¼šæ›´æ–°æŒ‡å®šKey的过期时间
        /// </summary>
        bool RefreshExpire(string key, int expireSeconds);
        /// <summary>
        /// è®¾ç½®è¿‡æœŸæ—¶é—´ï¼šåœ¨æŒ‡å®šç§’数后过期
        /// </summary>
        bool ExpireIn(string key, int seconds);
        /// <summary>
        /// è®¾ç½®è¿‡æœŸæ—¶é—´ï¼šåœ¨æŒ‡å®šæ—¶é—´ç‚¹è¿‡æœŸ
        /// </summary>
        bool ExpireAt(string key, DateTime expireTime);
        /// <summary>
        /// èŽ·å–å‰©ä½™è¿‡æœŸæ—¶é—´ï¼ˆç§’ï¼‰
        /// </summary>
        long? GetExpire(string key);
        /// <summary>
        /// åŽŸå­æ“ä½œï¼šä»…å½“Key不存在时添加(原子操作)
        /// </summary>
        bool AddIfNotExists(string key, string value, int expireSeconds = -1);
        /// <summary>
        /// åŽŸå­æ“ä½œï¼šä»…å½“Key不存在时添加对象(原子操作)
        /// </summary>
        bool AddIfNotExists<T>(string key, T value, int expireSeconds = -1) where T : class;
        /// <summary>
        /// åŽŸå­æ“ä½œï¼šèŽ·å–æ—§å€¼å¹¶è®¾ç½®æ–°å€¼
        /// </summary>
        string? GetAndSet(string key, string newValue, int expireSeconds = -1);
        /// <summary>
        /// åŽŸå­æ“ä½œï¼šèŽ·å–æ—§å¯¹è±¡å¹¶è®¾ç½®æ–°å¯¹è±¡
        /// </summary>
        T? GetAndSet<T>(string key, T newValue, int expireSeconds = -1) where T : class;
        /// <summary>
        /// è‡ªå¢žï¼šå°†Key中的数值自增,返回自增后的值
        /// </summary>
        long Increment(string key, long value = 1);
        /// <summary>
        /// è‡ªå‡ï¼šå°†Key中的数值自减,返回自减后的值
        /// </summary>
        long Decrement(string key, long value = 1);
        /// <summary>
        /// è¿½åŠ ï¼šå‘çŽ°æœ‰å€¼è¿½åŠ å†…å®¹
        /// </summary>
        long Append(string key, string value);
        #endregion
        #region ConcurrentDictionary风格方法
@@ -94,6 +229,24 @@
        bool TryUpdate(string key, string newValue, int expireSeconds = -1);
        /// <summary>
        /// å€¼å‘生改变时更新:仅当Key存在且新值与旧值不同时才更新
        /// </summary>
        /// <param name="key">缓存Key</param>
        /// <param name="newValue">新值</param>
        /// <param name="expireSeconds">过期时间(秒)</param>
        /// <returns>值是否发生了改变并更新成功</returns>
        bool TryUpdateIfChanged(string key, string newValue, int expireSeconds = -1);
        /// <summary>
        /// å€¼å‘生改变时更新对象:仅当Key存在且新值与旧值不同时才更新
        /// </summary>
        /// <param name="key">缓存Key</param>
        /// <param name="newValue">新值</param>
        /// <param name="expireSeconds">过期时间(秒)</param>
        /// <returns>值是否发生了改变并更新成功</returns>
        bool TryUpdateIfChanged<T>(string key, T newValue, int expireSeconds = -1) where T : class;
        /// <summary>
        /// èŽ·å–æˆ–æ·»åŠ ï¼šKey存在则返回现有值,不存在则添加并返回新值
        /// </summary>
        string GetOrAdd(string key, string value, int expireSeconds = -1);
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Core/Caches/MemoryCacheService.cs
@@ -145,6 +145,183 @@
            keys.ToList().ForEach(item => _cache.Remove(item));
        }
        #region åˆ é™¤æ‰©å±•方法
        public string? RemoveAndGet(string key)
        {
            var value = Get(key);
            if (value != null) _cache.Remove(key);
            return value;
        }
        public T? RemoveAndGet<T>(string key) where T : class
        {
            var value = Get<T>(key);
            if (value != null) _cache.Remove(key);
            return value;
        }
        public int RemoveByPrefix(string prefix)
        {
            if (string.IsNullOrEmpty(prefix)) return 0;
            // MemoryCache不支持枚举,返回0
            return 0;
        }
        public int RemoveByPattern(string pattern)
        {
            // MemoryCache不支持模式匹配
            return 0;
        }
        public int RemoveAll(IEnumerable<string> keys)
        {
            if (keys == null) return 0;
            int count = 0;
            foreach (var key in keys)
            {
                if (Remove(key)) count++;
            }
            return count;
        }
        public int RemoveWhere(Func<string, bool> predicate)
        {
            // MemoryCache不支持枚举所有Key
            return 0;
        }
        #endregion
        #region æ·»åŠ å’Œä¿®æ”¹æ‰©å±•æ–¹æ³•
        public void AddAll(IDictionary<string, string> items, int expireSeconds = -1)
        {
            if (items == null) return;
            foreach (var item in items)
            {
                Add(item.Key, item.Value, expireSeconds);
            }
        }
        public void AddAllObjects(IDictionary<string, object> items, int expireSeconds = -1)
        {
            if (items == null) return;
            foreach (var item in items)
            {
                AddObject(item.Key, item.Value, expireSeconds);
            }
        }
        public bool Replace(string key, string newValue, int expireSeconds = -1)
        {
            return TryUpdate(key, newValue, expireSeconds);
        }
        public bool Replace<T>(string key, T newValue, int expireSeconds = -1) where T : class
        {
            if (!Exists(key)) return false;
            AddObject(key, newValue, expireSeconds);
            return true;
        }
        public string? GetAndRefresh(string key, int expireSeconds)
        {
            var value = Get(key);
            if (value != null)
            {
                Add(key, value, expireSeconds);
                return value;
            }
            return null;
        }
        public T? GetAndRefresh<T>(string key, int expireSeconds) where T : class
        {
            var value = Get<T>(key);
            if (value != null)
            {
                AddObject(key, value, expireSeconds);
                return value;
            }
            return default;
        }
        public bool RefreshExpire(string key, int expireSeconds)
        {
            var value = Get(key);
            if (value != null)
            {
                Add(key, value, expireSeconds);
                return true;
            }
            return false;
        }
        public bool ExpireIn(string key, int seconds)
        {
            return RefreshExpire(key, seconds);
        }
        public bool ExpireAt(string key, DateTime expireTime)
        {
            var seconds = (int)(expireTime - DateTime.Now).TotalSeconds;
            if (seconds <= 0) return Remove(key);
            return RefreshExpire(key, seconds);
        }
        public long? GetExpire(string key)
        {
            return null; // MemoryCache不支持TTL查询
        }
        public bool AddIfNotExists(string key, string value, int expireSeconds = -1)
        {
            return TryAdd(key, value, expireSeconds);
        }
        public bool AddIfNotExists<T>(string key, T value, int expireSeconds = -1) where T : class
        {
            return TryAdd(key, value, expireSeconds);
        }
        public string? GetAndSet(string key, string newValue, int expireSeconds = -1)
        {
            var oldValue = Get(key);
            Add(key, newValue, expireSeconds);
            return oldValue;
        }
        public T? GetAndSet<T>(string key, T newValue, int expireSeconds = -1) where T : class
        {
            var oldValue = Get<T>(key);
            AddObject(key, newValue, expireSeconds);
            return oldValue;
        }
        public long Increment(string key, long value = 1)
        {
            var current = long.TryParse(Get(key), out var v) ? v : 0;
            var newValue = current + value;
            Add(key, newValue.ToString());
            return newValue;
        }
        public long Decrement(string key, long value = 1)
        {
            return Increment(key, -value);
        }
        public long Append(string key, string value)
        {
            var current = Get(key) ?? "";
            var newValue = current + value;
            Add(key, newValue);
            return newValue.Length;
        }
        #endregion
        public bool TryAdd(string key, string value, int expireSeconds = -1)
        {
            if (Exists(key)) return false;
@@ -190,6 +367,31 @@
            return true;
        }
        public bool TryUpdateIfChanged(string key, string newValue, int expireSeconds = -1)
        {
            var existing = Get(key);
            if (existing == null) return false;
            if (existing == newValue) return false; // å€¼ç›¸åŒï¼Œä¸æ›´æ–°
            Remove(key);
            Add(key, newValue, expireSeconds);
            return true;
        }
        public bool TryUpdateIfChanged<T>(string key, T newValue, int expireSeconds = -1) where T : class
        {
            var existing = Get<T>(key);
            if (existing == null) return false;
            // ä½¿ç”¨JSON序列化比较内容,而不是引用比较
            var existingJson = JsonConvert.SerializeObject(existing);
            var newJson = JsonConvert.SerializeObject(newValue);
            if (existingJson == newJson) return false; // JSON字符串相同,不更新
            Remove(key);
            AddObject(key, newValue, expireSeconds);
            return true;
        }
        public string GetOrAdd(string key, string value, int expireSeconds = -1)
        {
            var existing = _cache.Get(key)?.ToString();
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Core/Const/CommunicationConst.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,55 @@
using System;
namespace WIDESEAWCS_Core.Const
{
    /// <summary>
    /// è®¾å¤‡é€šè®¯ç›¸å…³å¸¸é‡
    /// </summary>
    public static class CommunicationConst
    {
        /// <summary>
        /// è®¾å¤‡ç­‰å¾…间隔时间(毫秒)
        /// </summary>
        public const int WaitIntervalMs = 500;
        /// <summary>
        /// è®¾å¤‡ç­‰å¾…超时时间基数(毫秒)
        /// </summary>
        public const int WaitTimeoutBaseMs = 6000;
        /// <summary>
        /// è®¾å¤‡ç­‰å¾…超时时间倍数
        /// </summary>
        public const int WaitTimeoutMultiplier = 10;
        /// <summary>
        /// è®¾å¤‡ç­‰å¾…总超时时间(毫秒)= WaitTimeoutMultiplier * WaitTimeoutBaseMs
        /// </summary>
        public static readonly int WaitTotalTimeoutMs = WaitTimeoutMultiplier * WaitTimeoutBaseMs;
        /// <summary>
        /// Ping æ£€æµ‹é—´é𔿗¶é—´ï¼ˆæ¯«ç§’)
        /// </summary>
        public const int PingIntervalMs = 100;
        /// <summary>
        /// æ—¥å¿—写入间隔时间(毫秒)
        /// </summary>
        public const int LogWriteIntervalMs = 5000;
        /// <summary>
        /// HTTP é»˜è®¤è¶…时时间(秒)
        /// </summary>
        public const int HttpDefaultTimeoutSeconds = 60;
        /// <summary>
        /// HTTP è¿žæŽ¥è¶…时时间(秒)
        /// </summary>
        public const int HttpConnectTimeoutSeconds = 30;
        /// <summary>
        /// æ•°æ®åº“连接超时时间(秒)
        /// </summary>
        public const int DbConnectTimeoutSeconds = 30;
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Core/Helper/AppSettings.cs
@@ -52,7 +52,10 @@
                    return Configuration[string.Join(":", sections)];
                }
            }
            catch (Exception) { }
            catch (Exception ex)
            {
                Console.WriteLine($"AppSettings读取配置失败: {ex.Message}");
            }
            return "";
        }
@@ -83,7 +86,10 @@
            {
                return Configuration[sectionsPath];
            }
            catch (Exception) { }
            catch (Exception ex)
            {
                Console.WriteLine($"AppSettings读取配置失败: {ex.Message}");
            }
            return "";
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Core/Helper/HttpHelper.cs
@@ -6,6 +6,7 @@
using System.Security.Policy;
using System.Text;
using System.Threading.Tasks;
using WIDESEAWCS_Core.Const;
using WIDESEAWCS_Core.LogHelper;
namespace WIDESEAWCS_Core.Helper
@@ -24,7 +25,7 @@
        /// æ–¹æ³•会自动记录请求日志,包含请求地址、响应内容和耗时等信息
        /// é»˜è®¤è¯·æ±‚超时时间为60秒
        /// </remarks>
        public static async Task<string> GetAsync(string serviceAddress, string contentType = "application/json", int timeOut = 60, Dictionary<string, string>? headers = null)
        public static async Task<string> GetAsync(string serviceAddress, string contentType = "application/json", int timeOut = CommunicationConst.HttpDefaultTimeoutSeconds, Dictionary<string, string>? headers = null)
        {
            string result = string.Empty;
            DateTime beginDate = DateTime.Now;
@@ -39,7 +40,8 @@
                        httpClient.DefaultRequestHeaders.Add(header.Key, header.Value);
                }
                result = await httpClient.GetAsync(serviceAddress).Result.Content.ReadAsStringAsync();
                HttpResponseMessage response = await httpClient.GetAsync(serviceAddress);
                result = await response.Content.ReadAsStringAsync();
                return result;
            }
            catch (Exception e)
@@ -63,7 +65,7 @@
        /// <remarks>
        /// è‡ªåŠ¨è®°å½•è¯·æ±‚æ—¥å¿—ï¼ŒåŒ…å«è¯·æ±‚åœ°å€ã€è¯·æ±‚æ•°æ®ã€å“åº”æ•°æ®å’Œè€—æ—¶
        /// </remarks>
        public static async Task<string?> PostAsync(string serviceAddress, string requestJson = null, string contentType = "application/json", int timeOut = 60, Dictionary<string, string>? headers = null)
        public static async Task<string?> PostAsync(string serviceAddress, string requestJson = null, string contentType = "application/json", int timeOut = CommunicationConst.HttpDefaultTimeoutSeconds, Dictionary<string, string>? headers = null)
        {
            string result = string.Empty;
            DateTime beginDate = DateTime.Now;
@@ -86,7 +88,8 @@
                            httpClient.DefaultRequestHeaders.Add(header.Key, header.Value);
                    }
                    result = await httpClient.PostAsync(serviceAddress, httpContent).Result.Content.ReadAsStringAsync();
                    HttpResponseMessage response = await httpClient.PostAsync(serviceAddress, httpContent);
                    result = await response.Content.ReadAsStringAsync();
                }
                return result;
            }
@@ -111,7 +114,7 @@
        /// /// <remarks>
        /// è¯¥æ–¹æ³•会自动记录请求日志,包含请求地址、请求数据、响应数据和耗时
        /// </remarks>
        public static string Get(string serviceAddress, string contentType = "application/json", int timeOut = 60, Dictionary<string, string>? headers = null)
        public static string Get(string serviceAddress, string contentType = "application/json", int timeOut = CommunicationConst.HttpDefaultTimeoutSeconds, Dictionary<string, string>? headers = null)
        {
            string result = string.Empty;
            DateTime beginDate = DateTime.Now;
@@ -126,7 +129,8 @@
                        httpClient.DefaultRequestHeaders.Add(header.Key, header.Value);
                }
                result = httpClient.GetStringAsync(serviceAddress).Result;
                Task<string> task = httpClient.GetStringAsync(serviceAddress);
                result = task.GetAwaiter().GetResult();
                return result;
            }
            catch (Exception e)
@@ -151,7 +155,7 @@
        /// <remarks>
        /// è¯¥æ–¹æ³•会自动记录请求日志,包含请求地址、请求数据、响应数据和耗时
        /// </remarks>
        public static string Post(string serviceAddress, string requestJson = null, string contentType = "application/json", int timeOut = 60, Dictionary<string, string>? headers = null)
        public static string Post(string serviceAddress, string requestJson = null, string contentType = "application/json", int timeOut = CommunicationConst.HttpDefaultTimeoutSeconds, Dictionary<string, string>? headers = null)
        {
            string result = string.Empty;
            DateTime beginDate = DateTime.Now;
@@ -174,10 +178,12 @@
                            httpClient.DefaultRequestHeaders.Add(header.Key, header.Value);
                    }
                    HttpResponseMessage message = httpClient.PostAsync(serviceAddress, httpContent).Result;
                    Task<HttpResponseMessage> postTask = httpClient.PostAsync(serviceAddress, httpContent);
                    HttpResponseMessage message = postTask.GetAwaiter().GetResult();
                    if (message.StatusCode == HttpStatusCode.OK)
                    {
                        result = message.Content.ReadAsStringAsync().Result;
                        Task<string> readTask = message.Content.ReadAsStringAsync();
                        result = readTask.GetAwaiter().GetResult();
                    }
                    else
                    {
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Core/Http/HTTP/HttpClientHelper.cs
@@ -10,6 +10,8 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using WIDESEAWCS_Common;
using WIDESEAWCS_Common.HttpEnum;
using WIDESEAWCS_Core.Caches;
namespace WIDESEA_Core
@@ -23,6 +25,19 @@
        {
            _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
            _cache = cache ?? throw new ArgumentNullException(nameof(cache));
            _cache.TryAdd($"{RedisPrefix.Code}:{RedisName.API}:{nameof(ConfigKey.CreateTaskInboundAsync)}", $"Task/CreateTaskInbound");
            _cache.TryAdd($"{RedisPrefix.Code}:{RedisName.API}:{nameof(ConfigKey.GetTasksLocation)}", $"Task/GetTasksLocation");
            _cache.TryAdd($"{RedisPrefix.Code}:{RedisName.API}:{nameof(ConfigKey.OutboundFinishTaskAsync)}", $"Task/OutboundFinishTask");
            _cache.TryAdd($"{RedisPrefix.Code}:{RedisName.API}:{nameof(ConfigKey.InboundFinishTaskAsync)}", $"Task/InboundFinishTask");
            _cache.TryAdd($"{RedisPrefix.Code}:{RedisName.API}:{nameof(ConfigKey.GetOutBoundTrayTaskAsync)}", $"Task/GetOutBoundTrayTask");
            _cache.TryAdd($"{RedisPrefix.Code}:{RedisName.API}:{nameof(ConfigKey.CreateTaskInboundTrayAsync)}", $"Task/CreateTaskInboundTray");
            _cache.TryAdd($"{RedisPrefix.Code}:{RedisName.API}:{nameof(ConfigKey.GroupPalletAsync)}", $"Stock/GroupPalletAsync");
            _cache.TryAdd($"{RedisPrefix.Code}:{RedisName.API}:{nameof(ConfigKey.ChangePalletAsync)}", $"Stock/ChangePalletAsync");
            _cache.TryAdd($"{RedisPrefix.Code}:{RedisName.API}:{nameof(ConfigKey.SplitPalletAsync)}", $"Stock/SplitPalletAsync");
        }
        /// <summary>
@@ -35,26 +50,31 @@
        /// <returns></returns>
        public HttpResponseResult Post(string url, string content, string contentType = "application/json", HttpRequestConfig? config = null)
        {
            url = _cache.Get(url);
            HttpResponseResult httpResponseResult = ExecuteAsync(async (client) =>
            HttpResponseResult httpResponseResult = Task.Run(async () =>
            {
                return await ExecuteAsync(async (client) =>
            {
                var request = new HttpRequestMessage(HttpMethod.Post, url);
                request.Content = new StringContent(content ?? string.Empty, Encoding.UTF8, contentType);
                SetRequestHeaders(request, config?.Headers);
                return await client.SendAsync(request);
            }, config, $"POST {url}").Result;
                }, config, $"POST {url}");
            }).GetAwaiter().GetResult();
            httpResponseResult.ApiUrl = url;
            return httpResponseResult;
        }
        public HttpResponseResult Get(string url, HttpRequestConfig? config = null)
        {
            HttpResponseResult httpResponseResult = ExecuteAsync(async (client) =>
            HttpResponseResult httpResponseResult = Task.Run(async () =>
            {
                return await ExecuteAsync(async (client) =>
            {
                var request = new HttpRequestMessage(HttpMethod.Get, url);
                SetRequestHeaders(request, config?.Headers);
                return await client.SendAsync(request);
            }, config, $"GET {url}").Result;
                }, config, $"GET {url}");
            }).GetAwaiter().GetResult();
            httpResponseResult.ApiUrl = url;
            return httpResponseResult;
@@ -72,6 +92,7 @@
        public HttpResponseResult<TResponse> Post<TResponse>(string url, string content, string contentType = "application/json", HttpRequestConfig? config = null)
        {
            url = BaseAPI.WMSBaseUrl + _cache.Get($"{RedisPrefix.Code}:{RedisName.API}:{url}");
            HttpResponseResult httpResponseResult = Post(url, content, contentType, config);
            HttpResponseResult<TResponse> result = new HttpResponseResult<TResponse>
@@ -113,6 +134,7 @@
        public HttpResponseResult<TResponse> Get<TResponse>(string url, HttpRequestConfig? config = null)
        {
            url = BaseAPI.WMSBaseUrl + _cache.Get(url);
            HttpResponseResult httpResponseResult = Get(url, config);
            HttpResponseResult<TResponse> result = new HttpResponseResult<TResponse>
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Core/LogHelper/Logger.cs
@@ -7,6 +7,7 @@
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WIDESEAWCS_Core.Const;
using WIDESEAWCS_Core.DB;
using WIDESEAWCS_Core.Helper;
using WIDESEAWCS_Core.HttpContextUser;
@@ -35,7 +36,7 @@
        /// 2. å½“缓存表有数据时,使用SqlSugar进行批量插入
        /// 3. å‘生异常时会输出错误信息但不会终止线程
        /// </remarks>
        static void StartWriteLog()
        static async void StartWriteLog()
        {
            DataTable queueTable = CreateEmptyTable();
            while (true)
@@ -48,7 +49,7 @@
                        DequeueToTable(queueTable); continue;
                    }
                    //每5秒写一次数据
                    Thread.Sleep(5000);
                    await Task.Delay(CommunicationConst.LogWriteIntervalMs);
                    //如果队列表中的行数为0,则跳过本次循环
                    if (queueTable.Rows.Count == 0) { continue; }
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Core/WIDESEAWCS_Core.csproj
@@ -76,4 +76,8 @@
        <Folder Include="ServiceExtensions\" />
    </ItemGroup>
    <ItemGroup>
      <ProjectReference Include="..\WIDESEAWCS_Common\WIDESEAWCS_Common.csproj" />
    </ItemGroup>
</Project>
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_QuartzJob/QuartzNet/QuartzNetExtension.cs
@@ -1,5 +1,7 @@
using System.Reflection;
using WIDESEAWCS_Common;
using WIDESEAWCS_Core;
using WIDESEAWCS_Core.Caches;
using WIDESEAWCS_Core.Helper;
using WIDESEAWCS_QuartzJob.DTO;
using WIDESEAWCS_QuartzJob.Service;
@@ -15,19 +17,21 @@
        private readonly IDeviceInfoService _deviceInfoService;
        private readonly IDispatchInfoService _dispatchInfoService;
        private readonly IDeviceProtocolDetailService _deviceProtocolDetailService;
        private readonly ICacheService _cacheService;
        private readonly Storage _storage;
        /// <summary>
        /// å¯åŠ¨ç¨‹åºè‡ªåŠ¨å¼€å¯è°ƒåº¦æœåŠ¡
        /// </summary>
        /// <returns></returns>
        public QuartzNetExtension(IDeviceInfoService deviceInfoService, IDispatchInfoService dispatchInfoService, ISchedulerCenter schedulerCenter, IDeviceProtocolDetailService deviceProtocolDetailService, Storage storage)
        public QuartzNetExtension(IDeviceInfoService deviceInfoService, IDispatchInfoService dispatchInfoService, ISchedulerCenter schedulerCenter, IDeviceProtocolDetailService deviceProtocolDetailService, Storage storage, ICacheService cacheService)
        {
            _deviceInfoService = deviceInfoService;
            _dispatchInfoService = dispatchInfoService;
            _schedulerCenter = schedulerCenter;
            _deviceProtocolDetailService = deviceProtocolDetailService;
            _storage = storage;
            _cacheService = cacheService;
        }
        /// <summary>
@@ -40,6 +44,7 @@
            {
                List<DispatchInfoDTO> dispatches = _dispatchInfoService.QueryDispatchInfos();
                List<DeviceInfoDTO> deviceInfos = await _deviceInfoService.QueryDeviceProInfos();
                _cacheService.RemoveByPrefix($"{RedisPrefix.System}");
                deviceInfos.ForEach(x =>
                {
@@ -107,6 +112,8 @@
                                x.Device = (IDevice)deviceInstance;
                                Storage.Devices.Add((IDevice)deviceInstance);
                                _cacheService.AddObject($"{RedisPrefix.System}:{RedisName.IDevice}:{x.DeviceName}", (IDevice)deviceInstance);
                            }
                        }
                        catch (Exception ex)
@@ -125,6 +132,7 @@
                    if (targetDevice is null) continue;
                    // ä½¿ç”¨æ¨¡å¼åŒ¹é…
                    dispatches[i].JobParams = targetDevice switch
                    {
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_QuartzJob/QuartzNet/SchedulerCenterServer.cs
@@ -49,9 +49,10 @@
        public SchedulerCenterServer(IJobFactory jobFactory)
        {
            _iocjobFactory = jobFactory;
            _scheduler = GetSchedulerAsync();
            // ä½¿ç”¨åŒæ­¥ä¸Šä¸‹æ–‡è¿è¡Œå¼‚步方法
            _scheduler = Task.Run(async () => await GetSchedulerAsync()).GetAwaiter().GetResult();
        }
        private IScheduler GetSchedulerAsync()
        private async Task<IScheduler> GetSchedulerAsync()
        {
            if (_scheduler != null)
                return this._scheduler;
@@ -66,7 +67,7 @@
                    };
                    //StdSchedulerFactory factory = new StdSchedulerFactory(collection);
                    StdSchedulerFactory factory = new StdSchedulerFactory();
                    return _scheduler = factory.GetScheduler().Result;
                    return _scheduler = await factory.GetScheduler();
                }
                catch (Exception ex)
                {
@@ -93,7 +94,7 @@
                    };
                    //StdSchedulerFactory factory = new StdSchedulerFactory(collection);
                    StdSchedulerFactory factory = new StdSchedulerFactory();
                    _scheduler = factory.GetScheduler().Result;
                    _scheduler = await factory.GetScheduler();
                }
                this._scheduler.JobFactory = this._iocjobFactory;
@@ -176,7 +177,7 @@
                        };
                        //StdSchedulerFactory factory = new StdSchedulerFactory(collection);
                        StdSchedulerFactory factory = new StdSchedulerFactory();
                        _scheduler = factory.GetScheduler().Result;
                        _scheduler = await factory.GetScheduler();
                    }
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_QuartzJob/Service/RouterService.cs
@@ -11,6 +11,7 @@
using WIDESEAWCS_DTO.BasicInfo;
using WIDESEAWCS_QuartzJob.Models;
using WIDESEAWCS_QuartzJob.Repository;
using WIDESEAWCS_Common;
using ICacheService = WIDESEAWCS_Core.Caches.ICacheService;
namespace WIDESEAWCS_QuartzJob.Service
@@ -330,10 +331,9 @@
        {
            // åˆ›å»ºä¸€ä¸ªå­—符串列表,用于存储所有位置
            List<string> positions = new List<string>();
            var device = _cacheService.Get<List<string>>($"DevicePositions:{deviceCode}");
            var device = _cacheService.Get<List<string>>($"{RedisPrefix.System}:{RedisName.DevicePositions}:{deviceCode}");
            if (device.IsNullOrEmpty())
            {
                try
                {
                    // æŸ¥è¯¢æ‰€æœ‰è¿›å…¥è·¯ç”±å™¨çš„位置
@@ -352,6 +352,10 @@
                {
                }
                finally
                {
                    _cacheService.TryAdd($"{RedisPrefix.System}:{RedisName.DevicePositions}:{deviceCode}", positions);
                }
            }
            else 
                positions = device;
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_QuartzJob/StackerCrane/Spec/SpeTemperatureStackerCrane.cs
@@ -16,6 +16,30 @@
    [Description("高常温堆垛机")]
    public class SpeTemperatureStackerCrane : StackerCraneBase, IStackerCrane
    {
        #region Constants
        /// <summary>
        /// è®¾å¤‡ç­‰å¾…间隔时间(毫秒)
        /// </summary>
        private const int WaitIntervalMs = 500;
        /// <summary>
        /// è®¾å¤‡ç­‰å¾…超时时间基数(毫秒)
        /// </summary>
        private const int WaitTimeoutBaseMs = 6000;
        /// <summary>
        /// è®¾å¤‡ç­‰å¾…超时时间倍数
        /// </summary>
        private const int WaitTimeoutMultiplier = 10;
        /// <summary>
        /// è®¾å¤‡ç­‰å¾…总超时时间(毫秒)
        /// </summary>
        private static readonly int WaitTotalTimeoutMs = WaitTimeoutMultiplier * WaitTimeoutBaseMs;
        #endregion Constants
        #region Private Member
        /// <summary>
@@ -410,22 +434,22 @@
            return typeCode switch
            {
                TypeCode.Boolean => Communicator.Wait(devicePro.DeviceProAddress, 500, 10 * 6000,
                TypeCode.Boolean => Communicator.Wait(devicePro.DeviceProAddress, WaitIntervalMs, WaitTotalTimeoutMs,
                    Convert.ToBoolean(deviceProtocolDetail.ProtocalDetailValue)),
                TypeCode.Byte => Communicator.Wait(devicePro.DeviceProAddress, 500, 10 * 6000,
                TypeCode.Byte => Communicator.Wait(devicePro.DeviceProAddress, WaitIntervalMs, WaitTotalTimeoutMs,
                    Convert.ToByte(deviceProtocolDetail.ProtocalDetailValue)),
                TypeCode.Int16 => Communicator.Wait(devicePro.DeviceProAddress, 500, 10 * 6000,
                TypeCode.Int16 => Communicator.Wait(devicePro.DeviceProAddress, WaitIntervalMs, WaitTotalTimeoutMs,
                    Convert.ToInt16(deviceProtocolDetail.ProtocalDetailValue)),
                TypeCode.Int32 => Communicator.Wait(devicePro.DeviceProAddress, 500, 10 * 6000,
                TypeCode.Int32 => Communicator.Wait(devicePro.DeviceProAddress, WaitIntervalMs, WaitTotalTimeoutMs,
                    Convert.ToInt32(deviceProtocolDetail.ProtocalDetailValue)),
                TypeCode.UInt16 => Communicator.Wait(devicePro.DeviceProAddress, 500, 10 * 6000,
                TypeCode.UInt16 => Communicator.Wait(devicePro.DeviceProAddress, WaitIntervalMs, WaitTotalTimeoutMs,
                    Convert.ToUInt16(deviceProtocolDetail.ProtocalDetailValue)),
                TypeCode.UInt32 => Communicator.Wait(devicePro.DeviceProAddress, 500, 10 * 6000,
                TypeCode.UInt32 => Communicator.Wait(devicePro.DeviceProAddress, WaitIntervalMs, WaitTotalTimeoutMs,
                    Convert.ToUInt32(deviceProtocolDetail.ProtocalDetailValue)),
                _ => new OperateResult<TimeSpan>()
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_QuartzJob/WIDESEAWCS_QuartzJob.csproj
@@ -40,6 +40,7 @@
    </ItemGroup>
    <ItemGroup>
        <ProjectReference Include="..\WIDESEAWCS_Common\WIDESEAWCS_Common.csproj" />
        <ProjectReference Include="..\WIDESEAWCS_Communicator\WIDESEAWCS_Communicator.csproj" />
        <ProjectReference Include="..\WIDESEAWCS_Core\WIDESEAWCS_Core.csproj" />
    </ItemGroup>
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_RedisService/Cache/CacheSyncBackgroundService.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,211 @@
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StackExchange.Redis;
using System.Collections.Concurrent;
using WIDESEAWCS_RedisService.Connection;
using WIDESEAWCS_RedisService.Options;
using WIDESEAWCS_RedisService.Serialization;
namespace WIDESEAWCS_RedisService.Cache
{
    /// <summary>
    /// Redis到内存缓存的自动同步后台服务
    /// å®šæœŸä»ŽRedis提取数据并覆盖内存缓存,确保数据一致性
    /// </summary>
    public class CacheSyncBackgroundService : BackgroundService
    {
        private readonly IRedisConnectionManager _connectionManager;
        private readonly IMemoryCache _memoryCache;
        private readonly IRedisSerializer _serializer;
        private readonly RedisOptions _options;
        private readonly ILogger<CacheSyncBackgroundService> _logger;
        private readonly ConcurrentDictionary<string, bool> _trackedKeys;
        public CacheSyncBackgroundService(
            IRedisConnectionManager connectionManager,
            IMemoryCache memoryCache,
            IRedisSerializer serializer,
            IOptions<RedisOptions> options,
            ILogger<CacheSyncBackgroundService> logger)
        {
            _connectionManager = connectionManager;
            _memoryCache = memoryCache;
            _serializer = serializer;
            _options = options.Value;
            _logger = logger;
            _trackedKeys = new ConcurrentDictionary<string, bool>();
        }
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            // å¦‚果未启用L1缓存,则无需同步
            if (!_options.EnableL1Cache)
            {
                _logger.LogInformation("L1缓存未启用,后台同步服务不会运行");
                return;
            }
            // å¦‚果未启用自动同步,则不运行
            if (!_options.EnableAutoSync)
            {
                _logger.LogInformation("Redis自动同步未启用,后台同步服务不会运行");
                return;
            }
            _logger.LogInformation("Redis缓存同步服务已启动,同步间隔: {Interval}秒", _options.SyncIntervalSeconds);
            // ç­‰å¾…Redis连接就绪
            while (!_connectionManager.IsConnected && !stoppingToken.IsCancellationRequested)
            {
                _logger.LogWarning("等待Redis连接就绪...");
                await Task.Delay(5000, stoppingToken);
            }
            if (stoppingToken.IsCancellationRequested)
                return;
            // å¯åŠ¨æ—¶å…ˆè¿›è¡Œä¸€æ¬¡å…¨é‡åŒæ­¥
            await SyncCacheAsync(stoppingToken);
            // å®šæœŸåŒæ­¥
            while (!stoppingToken.IsCancellationRequested)
            {
                try
                {
                    await Task.Delay(TimeSpan.FromSeconds(_options.SyncIntervalSeconds), stoppingToken);
                    if (_connectionManager.IsConnected)
                    {
                        await SyncCacheAsync(stoppingToken);
                    }
                    else
                    {
                        _logger.LogWarning("Redis未连接,跳过本次同步");
                    }
                }
                catch (OperationCanceledException)
                {
                    // æ­£å¸¸é€€å‡º
                    break;
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "缓存同步过程中发生错误");
                }
            }
            _logger.LogInformation("Redis缓存同步服务已停止");
        }
        /// <summary>
        /// æ‰§è¡Œç¼“存同步
        /// </summary>
        private async Task SyncCacheAsync(CancellationToken stoppingToken)
        {
            try
            {
                var db = _connectionManager.GetDatabase();
                var server = _connectionManager.GetServer();
                var keyPrefix = _options.KeyPrefix;
                // èŽ·å–æ‰€æœ‰åŒ¹é…å‰ç¼€çš„Redis key
                var redisKeys = server.Keys(pattern: $"{keyPrefix}*", pageSize: _options.SyncBatchSize).ToList();
                if (redisKeys.Count == 0)
                {
                    _logger.LogDebug("Redis中没有找到匹配前缀 {Prefix} çš„key", keyPrefix);
                    return;
                }
                int syncedCount = 0;
                int skippedCount = 0;
                int removedCount = 0;
                // 1. åŒæ­¥Redis中的数据到内存缓存
                foreach (var redisKey in redisKeys)
                {
                    if (stoppingToken.IsCancellationRequested)
                        break;
                    try
                    {
                        var keyStr = redisKey.ToString();
                        _trackedKeys.AddOrUpdate(keyStr, true, (_, _) => true);
                        // èŽ·å–Redis中的值
                        var value = db.StringGet(redisKey);
                        if (!value.IsNullOrEmpty)
                        {
                            var valueStr = value.ToString();
                            var ttl = db.KeyTimeToLive(redisKey);
                            // èŽ·å–è¿‡æœŸæ—¶é—´ï¼Œå¦‚æžœæ²¡æœ‰è®¾ç½®TTL则使用默认5分钟
                            var expireSeconds = ttl.HasValue && ttl.Value.TotalSeconds > 0
                                ? (int)ttl.Value.TotalSeconds
                                : 300;
                            // æ›´æ–°å†…存缓存
                            var entryOptions = new MemoryCacheEntryOptions
                            {
                                AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(expireSeconds)
                            };
                            _memoryCache.Set(keyStr, valueStr, entryOptions);
                            syncedCount++;
                        }
                    }
                    catch (Exception ex)
                    {
                        _logger.LogWarning(ex, "同步key {Key} æ—¶å‘生错误", redisKey);
                    }
                }
                // 2. æ¸…理内存缓存中不存在于Redis的key
                // æ³¨æ„ï¼šIMemoryCache不支持枚举,这里只能清理已跟踪的key
                var keysToRemove = _trackedKeys.Where(k => !redisKeys.Contains(k.Key)).Select(k => k.Key).ToList();
                foreach (var keyToRemove in keysToRemove)
                {
                    _memoryCache.Remove(keyToRemove);
                    _trackedKeys.TryRemove(keyToRemove, out _);
                    removedCount++;
                }
                _logger.LogInformation(
                    "缓存同步完成: åŒæ­¥ {SyncedCount} ä¸ª, è·³è¿‡ {SkippedCount} ä¸ª, æ¸…理 {RemovedCount} ä¸ª, æ€»è®¡ {TotalCount} ä¸ªkey",
                    syncedCount, skippedCount, removedCount, redisKeys.Count);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "缓存同步时发生错误");
            }
        }
        /// <summary>
        /// èŽ·å–åŒæ­¥ç»Ÿè®¡ä¿¡æ¯
        /// </summary>
        public CacheSyncStatistics GetStatistics()
        {
            return new CacheSyncStatistics
            {
                IsEnabled = _options.EnableAutoSync && _options.EnableL1Cache,
                SyncIntervalSeconds = _options.SyncIntervalSeconds,
                TrackedKeysCount = _trackedKeys.Count,
                IsRedisConnected = _connectionManager.IsConnected
            };
        }
    }
    /// <summary>
    /// ç¼“存同步统计信息
    /// </summary>
    public class CacheSyncStatistics
    {
        public bool IsEnabled { get; set; }
        public int SyncIntervalSeconds { get; set; }
        public int TrackedKeysCount { get; set; }
        public bool IsRedisConnected { get; set; }
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_RedisService/Cache/HybridCacheService.cs
@@ -53,9 +53,20 @@
        public bool Add(string key, string value, int expireSeconds = -1, bool isSliding = false)
        {
            var fullKey = BuildKey(key);
            // åªæœ‰å¯ç”¨L1缓存时才写入内存缓存
            if (_options.EnableL1Cache)
            {
            SetMemoryCache(fullKey, value, expireSeconds, isSliding);
            }
            if (!RedisAvailable)
            {
                if (!_options.EnableL1Cache)
                {
                    _logger.LogWarning("Redis不可用且L1缓存已禁用, key={Key}", key);
                    return false;
                }
                _logger.LogWarning("Redis不可用,仅使用内存缓存, key={Key}", key);
                return true;
            }
@@ -69,7 +80,7 @@
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "Redis Add失败, key={Key}", key);
                return _options.FallbackToMemory;
                return _options.FallbackToMemory && _options.EnableL1Cache;
            }
        }
@@ -91,8 +102,14 @@
        public bool Remove(string key)
        {
            var fullKey = BuildKey(key);
            // åªæœ‰å¯ç”¨L1缓存时才从内存缓存中移除
            if (_options.EnableL1Cache)
            {
            _memoryCache.Remove(fullKey);
            if (!RedisAvailable) return true;
            }
            if (!RedisAvailable) return _options.EnableL1Cache;
            try
            {
                return _connectionManager.GetDatabase().KeyDelete(fullKey);
@@ -109,9 +126,434 @@
            foreach (var key in keys) Remove(key);
        }
        #region åˆ é™¤æ‰©å±•方法
        public string? RemoveAndGet(string key)
        {
            var value = Get(key);
            if (value != null) Remove(key);
            return value;
        }
        public T? RemoveAndGet<T>(string key) where T : class
        {
            var value = Get<T>(key);
            if (value != null) Remove(key);
            return value;
        }
        public int RemoveByPrefix(string prefix)
        {
            if (string.IsNullOrEmpty(prefix)) return 0;
            var fullPrefix = BuildKey(prefix);
            int count = 0;
            // åˆ é™¤å†…存缓存中的匹配项
            // MemoryCache无法枚举,跳过
            if (RedisAvailable)
            {
                try
                {
                    var server = _connectionManager.GetServer();
                    var keys = server.Keys(pattern: $"{fullPrefix}*").ToArray();
                    if (keys.Length > 0)
                    {
                        count = (int)_connectionManager.GetDatabase().KeyDelete(keys);
                    }
                }
                catch (Exception ex)
                {
                    _logger.LogWarning(ex, "Redis RemoveByPrefix失败, prefix={Prefix}", prefix);
                }
            }
            return count;
        }
        public int RemoveByPattern(string pattern)
        {
            if (string.IsNullOrEmpty(pattern)) return 0;
            int count = 0;
            if (RedisAvailable)
            {
                try
                {
                    var server = _connectionManager.GetServer();
                    var keys = server.Keys(pattern: $"{_options.KeyPrefix}{pattern}").ToArray();
                    if (keys.Length > 0)
                    {
                        count = (int)_connectionManager.GetDatabase().KeyDelete(keys);
                    }
                }
                catch (Exception ex)
                {
                    _logger.LogWarning(ex, "Redis RemoveByPattern失败, pattern={Pattern}", pattern);
                }
            }
            return count;
        }
        public int RemoveAll(IEnumerable<string> keys)
        {
            if (keys == null) return 0;
            int count = 0;
            foreach (var key in keys)
            {
                if (Remove(key)) count++;
            }
            return count;
        }
        public int RemoveWhere(Func<string, bool> predicate)
        {
            if (predicate == null) return 0;
            int count = 0;
            if (RedisAvailable)
            {
                try
                {
                    var server = _connectionManager.GetServer();
                    var keys = server.Keys(pattern: $"{_options.KeyPrefix}*").ToArray();
                    var keysToDelete = keys.Where(k =>
                    {
                        var originalKey = k.ToString().Replace(_options.KeyPrefix, "");
                        return predicate(originalKey);
                    }).ToArray();
                    if (keysToDelete.Length > 0)
                    {
                        count = (int)_connectionManager.GetDatabase().KeyDelete(keysToDelete);
                    }
                }
                catch (Exception ex)
                {
                    _logger.LogWarning(ex, "Redis RemoveWhere失败");
                }
            }
            return count;
        }
        #endregion
        #region æ·»åŠ å’Œä¿®æ”¹æ‰©å±•æ–¹æ³•
        public void AddAll(IDictionary<string, string> items, int expireSeconds = -1)
        {
            if (items == null) return;
            foreach (var item in items)
            {
                Add(item.Key, item.Value, expireSeconds);
            }
        }
        public void AddAllObjects(IDictionary<string, object> items, int expireSeconds = -1)
        {
            if (items == null) return;
            foreach (var item in items)
            {
                AddObject(item.Key, item.Value, expireSeconds);
            }
        }
        public bool Replace(string key, string newValue, int expireSeconds = -1)
        {
            return TryUpdate(key, newValue, expireSeconds);
        }
        public bool Replace<T>(string key, T newValue, int expireSeconds = -1) where T : class
        {
            if (!Exists(key)) return false;
            AddObject(key, newValue, expireSeconds);
            return true;
        }
        public string? GetAndRefresh(string key, int expireSeconds)
        {
            var fullKey = BuildKey(key);
            string? value = null;
            // ä»ŽRedis获取值
            if (RedisAvailable)
            {
                try
                {
                    var redisValue = _connectionManager.GetDatabase().StringGet(fullKey);
                    if (!redisValue.IsNullOrEmpty)
                    {
                        value = redisValue.ToString();
                        // åˆ·æ–°Redis过期时间
                        _connectionManager.GetDatabase().KeyExpire(fullKey, TimeSpan.FromSeconds(expireSeconds));
                    }
                }
                catch (Exception ex)
                {
                    _logger.LogWarning(ex, "Redis GetAndRefresh失败, key={Key}", key);
                }
            }
            // å¦‚æžœRedis不可用,尝试从内存缓存获取
            if (value == null && _options.EnableL1Cache)
            {
                if (_memoryCache.TryGetValue(fullKey, out string? cached))
                    value = cached;
            }
            // æ›´æ–°å†…存缓存(如果有值)
            if (value != null && _options.EnableL1Cache)
            {
                SetMemoryCache(fullKey, value, expireSeconds, false);
            }
            return value;
        }
        public T? GetAndRefresh<T>(string key, int expireSeconds) where T : class
        {
            var fullKey = BuildKey(key);
            T? value = default;
            // ä»ŽRedis获取值
            if (RedisAvailable)
            {
                try
                {
                    var redisValue = _connectionManager.GetDatabase().StringGet(fullKey);
                    if (!redisValue.IsNullOrEmpty)
                    {
                        var json = redisValue.ToString();
                        value = _serializer.Deserialize<T>(json);
                        // åˆ·æ–°Redis过期时间
                        _connectionManager.GetDatabase().KeyExpire(fullKey, TimeSpan.FromSeconds(expireSeconds));
                    }
                }
                catch (Exception ex)
                {
                    _logger.LogWarning(ex, "Redis GetAndRefresh<T>失败, key={Key}", key);
                }
            }
            // å¦‚æžœRedis不可用,尝试从内存缓存获取
            if (value == null && _options.EnableL1Cache)
            {
                if (_memoryCache.TryGetValue(fullKey, out string? cached) && cached != null)
                    value = _serializer.Deserialize<T>(cached);
            }
            // æ›´æ–°å†…存缓存(如果有值)
            if (value != null && _options.EnableL1Cache)
            {
                SetMemoryCache(fullKey, _serializer.Serialize(value), expireSeconds, false);
            }
            return value;
        }
        public bool RefreshExpire(string key, int expireSeconds)
        {
            var fullKey = BuildKey(key);
            bool result = false;
            // åˆ·æ–°Redis过期时间
            if (RedisAvailable)
            {
                try
                {
                    result = _connectionManager.GetDatabase().KeyExpire(fullKey, TimeSpan.FromSeconds(expireSeconds));
                }
                catch { }
            }
            // æ›´æ–°å†…存缓存过期时间(需要重新设置值来刷新过期时间)
            if (_options.EnableL1Cache && _memoryCache.TryGetValue(fullKey, out string? cached) && cached != null)
            {
                SetMemoryCache(fullKey, cached, expireSeconds, false);
                return true;
            }
            return result;
        }
        public bool ExpireIn(string key, int seconds)
        {
            return RefreshExpire(key, seconds);
        }
        public bool ExpireAt(string key, DateTime expireTime)
        {
            var seconds = (long)(expireTime - DateTime.Now).TotalSeconds;
            if (seconds <= 0) return Remove(key);
            return RefreshExpire(key, (int)seconds);
        }
        public long? GetExpire(string key)
        {
            if (RedisAvailable)
            {
                try
                {
                    var ttl = _connectionManager.GetDatabase().KeyTimeToLive(BuildKey(key));
                    return ttl.HasValue ? (long)ttl.Value.TotalSeconds : null;
                }
                catch { }
            }
            return null; // MemoryCache不支持TTL查询
        }
        public bool AddIfNotExists(string key, string value, int expireSeconds = -1)
        {
            return TryAdd(key, value, expireSeconds);
        }
        public bool AddIfNotExists<T>(string key, T value, int expireSeconds = -1) where T : class
        {
            return TryAdd(key, value, expireSeconds);
        }
        public string? GetAndSet(string key, string newValue, int expireSeconds = -1)
        {
            var fullKey = BuildKey(key);
            string? oldValue = null;
            // ä»ŽRedis获取旧值
            if (RedisAvailable)
            {
                try
                {
                    var value = _connectionManager.GetDatabase().StringGet(fullKey);
                    oldValue = value.IsNullOrEmpty ? null : value.ToString();
                }
                catch { }
            }
            // å¦‚æžœRedis不可用,从内存缓存获取
            if (oldValue == null && _options.EnableL1Cache)
            {
                _memoryCache.TryGetValue(fullKey, out oldValue);
            }
            // å†™å…¥Redis
            if (RedisAvailable)
            {
                try
                {
                    var expiry = expireSeconds > 0 ? TimeSpan.FromSeconds(expireSeconds) : (TimeSpan?)null;
                    _connectionManager.GetDatabase().StringSet(fullKey, newValue, expiry);
                }
                catch { }
            }
            // æ›´æ–°å†…存缓存
            if (_options.EnableL1Cache)
            {
                SetMemoryCache(fullKey, newValue, expireSeconds, false);
            }
            return oldValue;
        }
        public T? GetAndSet<T>(string key, T newValue, int expireSeconds = -1) where T : class
        {
            var fullKey = BuildKey(key);
            T? oldValue = default;
            string? oldJson = null;
            // ä»ŽRedis获取旧值
            if (RedisAvailable)
            {
                try
                {
                    var value = _connectionManager.GetDatabase().StringGet(fullKey);
                    if (!value.IsNullOrEmpty)
                    {
                        oldJson = value.ToString();
                        oldValue = _serializer.Deserialize<T>(oldJson);
                    }
                }
                catch { }
            }
            var newJson = _serializer.Serialize(newValue);
            // å†™å…¥Redis
            if (RedisAvailable)
            {
                try
                {
                    var expiry = expireSeconds > 0 ? TimeSpan.FromSeconds(expireSeconds) : (TimeSpan?)null;
                    _connectionManager.GetDatabase().StringSet(fullKey, newJson, expiry);
                }
                catch { }
            }
            // æ›´æ–°å†…存缓存
            if (_options.EnableL1Cache)
            {
                SetMemoryCache(fullKey, newJson, expireSeconds, false);
            }
            return oldValue;
        }
        public long Increment(string key, long value = 1)
        {
            if (RedisAvailable)
            {
                try
                {
                    return _connectionManager.GetDatabase().StringIncrement(BuildKey(key), value);
                }
                catch { }
            }
            // Fallback to memory
            var current = long.TryParse(Get(key), out var v) ? v : 0;
            var newValue = current + value;
            Add(key, newValue.ToString());
            return newValue;
        }
        public long Decrement(string key, long value = 1)
        {
            return Increment(key, -value);
        }
        public long Append(string key, string value)
        {
            var current = Get(key) ?? "";
            var newValue = current + value;
            Add(key, newValue);
            return newValue.Length;
        }
        #endregion
        public T? Get<T>(string key) where T : class
        {
            var fullKey = BuildKey(key);
            // å¦‚果禁用了L1缓存,直接查Redis
            if (!_options.EnableL1Cache)
            {
                if (!RedisAvailable) return default;
                try
                {
                    var value = _connectionManager.GetDatabase().StringGet(fullKey);
                    if (value.IsNullOrEmpty) return default;
                    return _serializer.Deserialize<T>(value!);
                }
                catch (Exception ex)
                {
                    _logger.LogWarning(ex, "Redis Get<T>失败, key={Key}", key);
                    return default;
                }
            }
            // æ­£å¸¸çš„L1+L2逻辑
            if (_memoryCache.TryGetValue(fullKey, out string? cached) && cached != null)
                return _serializer.Deserialize<T>(cached);
@@ -134,6 +576,25 @@
        public object? Get(Type type, string key)
        {
            var fullKey = BuildKey(key);
            // å¦‚果禁用了L1缓存,直接查Redis
            if (!_options.EnableL1Cache)
            {
                if (!RedisAvailable) return null;
                try
                {
                    var value = _connectionManager.GetDatabase().StringGet(fullKey);
                    if (value.IsNullOrEmpty) return null;
                    return _serializer.Deserialize(value!, type);
                }
                catch (Exception ex)
                {
                    _logger.LogWarning(ex, "Redis Get(Type)失败, key={Key}", key);
                    return null;
                }
            }
            // æ­£å¸¸çš„L1+L2逻辑
            if (_memoryCache.TryGetValue(fullKey, out string? cached) && cached != null)
                return _serializer.Deserialize(cached, type);
@@ -156,6 +617,24 @@
        public string? Get(string key)
        {
            var fullKey = BuildKey(key);
            // å¦‚果禁用了L1缓存,直接查Redis
            if (!_options.EnableL1Cache)
            {
                if (!RedisAvailable) return null;
                try
                {
                    var value = _connectionManager.GetDatabase().StringGet(fullKey);
                    return value.IsNullOrEmpty ? null : value.ToString();
                }
                catch (Exception ex)
                {
                    _logger.LogWarning(ex, "Redis Get失败, key={Key}", key);
                    return null;
                }
            }
            // æ­£å¸¸çš„L1+L2逻辑
            if (_memoryCache.TryGetValue(fullKey, out string? cached))
                return cached;
@@ -229,6 +708,73 @@
            return true;
        }
        public bool TryUpdateIfChanged(string key, string newValue, int expireSeconds = -1)
        {
            var existing = Get(key);
            if (existing == null) return false;
            if (existing == newValue) return false; // å€¼ç›¸åŒï¼Œä¸æ›´æ–°
            Add(key, newValue, expireSeconds);
            return true;
        }
        public bool TryUpdateIfChanged<T>(string key, T newValue, int expireSeconds = -1) where T : class
        {
            var fullKey = BuildKey(key);
            // æ€»æ˜¯ä»ŽRedis获取当前实际值进行比较,确保数据一致性
            string? existingJson = null;
            if (RedisAvailable)
            {
                try
                {
                    var value = _connectionManager.GetDatabase().StringGet(fullKey);
                    if (!value.IsNullOrEmpty)
                        existingJson = value.ToString();
                }
                catch { }
            }
            if (existingJson == null)
            {
                // Redis不可用,检查内存缓存
                if (_options.EnableL1Cache && _memoryCache.TryGetValue(fullKey, out string? cached) && cached != null)
                    existingJson = cached;
                else
                    return false;
            }
            var newJson = _serializer.Serialize(newValue);
            if (existingJson == newJson) return false; // JSON字符串相同,不更新
            // å…ˆå†™å…¥Redis,成功后再更新内存缓存
            if (RedisAvailable)
            {
                try
                {
                    var expiry = expireSeconds > 0 ? TimeSpan.FromSeconds(expireSeconds) : (TimeSpan?)null;
                    if (!_connectionManager.GetDatabase().StringSet(fullKey, newJson, expiry))
                    {
                        // Redis写入失败
                        _logger.LogWarning("Redis TryUpdateIfChanged写入失败, key={Key}", key);
                        return _options.FallbackToMemory && _options.EnableL1Cache;
                    }
                }
                catch (Exception ex)
                {
                    _logger.LogWarning(ex, "Redis TryUpdateIfChanged写入失败, key={Key}", key);
                    return _options.FallbackToMemory && _options.EnableL1Cache;
                }
            }
            // Redis写入成功(或Redis不可用时),更新内存缓存
            if (_options.EnableL1Cache)
            {
                SetMemoryCache(fullKey, newJson, expireSeconds, false);
            }
            return true;
        }
        public string GetOrAdd(string key, string value, int expireSeconds = -1)
        {
            var existing = Get(key);
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_RedisService/Cache/RedisCacheService.cs
@@ -70,6 +70,209 @@
            Db.KeyDelete(redisKeys);
        }
        #region åˆ é™¤æ‰©å±•方法
        public string? RemoveAndGet(string key)
        {
            var fullKey = BuildKey(key);
            var value = Db.StringGet(fullKey);
            if (!value.IsNullOrEmpty)
            {
                Db.KeyDelete(fullKey);
                return value.ToString();
            }
            return null;
        }
        public T? RemoveAndGet<T>(string key) where T : class
        {
            var fullKey = BuildKey(key);
            var value = Db.StringGet(fullKey);
            if (!value.IsNullOrEmpty)
            {
                Db.KeyDelete(fullKey);
                return _serializer.Deserialize<T>(value!);
            }
            return default;
        }
        public int RemoveByPrefix(string prefix)
        {
            var fullPrefix = BuildKey(prefix);
            var server = Db.Multiplexer.GetServer(Db.Multiplexer.GetEndPoints().First());
            var keys = server.Keys(pattern: $"{fullPrefix}*").ToArray();
            if (keys.Length == 0) return 0;
            return (int)Db.KeyDelete(keys);
        }
        public int RemoveByPattern(string pattern)
        {
            var fullPattern = BuildKey(pattern).Replace("*", ""); // ä¿ç•™ç”¨æˆ·ä¼ å…¥çš„通配符
            var server = Db.Multiplexer.GetServer(Db.Multiplexer.GetEndPoints().First());
            var keys = server.Keys(pattern: $"{_options.KeyPrefix}{pattern}").ToArray();
            if (keys.Length == 0) return 0;
            return (int)Db.KeyDelete(keys);
        }
        public int RemoveAll(IEnumerable<string> keys)
        {
            if (keys == null) return 0;
            var redisKeys = keys.Select(k => (RedisKey)BuildKey(k)).ToArray();
            return (int)Db.KeyDelete(redisKeys);
        }
        public int RemoveWhere(Func<string, bool> predicate)
        {
            if (predicate == null) return 0;
            var server = Db.Multiplexer.GetServer(Db.Multiplexer.GetEndPoints().First());
            var keys = server.Keys(pattern: $"{_options.KeyPrefix}*")
                .Where(k => predicate(k.ToString().Replace(_options.KeyPrefix, "")))
                .ToArray();
            if (keys.Length == 0) return 0;
            return (int)Db.KeyDelete(keys);
        }
        #endregion
        #region æ·»åŠ å’Œä¿®æ”¹æ‰©å±•æ–¹æ³•
        public void AddAll(IDictionary<string, string> items, int expireSeconds = -1)
        {
            if (items == null) return;
            var batch = Db.CreateBatch();
            var expiry = expireSeconds > 0 ? TimeSpan.FromSeconds(expireSeconds) : (TimeSpan?)null;
            foreach (var item in items)
            {
                batch.StringSetAsync(BuildKey(item.Key), item.Value, expiry);
            }
            batch.Execute();
        }
        public void AddAllObjects(IDictionary<string, object> items, int expireSeconds = -1)
        {
            if (items == null) return;
            var batch = Db.CreateBatch();
            var expiry = expireSeconds > 0 ? TimeSpan.FromSeconds(expireSeconds) : (TimeSpan?)null;
            foreach (var item in items)
            {
                batch.StringSetAsync(BuildKey(item.Key), _serializer.Serialize(item.Value), expiry);
            }
            batch.Execute();
        }
        public bool Replace(string key, string newValue, int expireSeconds = -1)
        {
            return TryUpdate(key, newValue, expireSeconds);
        }
        public bool Replace<T>(string key, T newValue, int expireSeconds = -1) where T : class
        {
            var fullKey = BuildKey(key);
            if (!Db.KeyExists(fullKey)) return false;
            var expiry = expireSeconds > 0 ? TimeSpan.FromSeconds(expireSeconds) : (TimeSpan?)null;
            return Db.StringSet(fullKey, _serializer.Serialize(newValue), expiry, When.Exists);
        }
        public string? GetAndRefresh(string key, int expireSeconds)
        {
            var fullKey = BuildKey(key);
            var value = Db.StringGet(fullKey);
            if (!value.IsNullOrEmpty)
            {
                Db.KeyExpire(fullKey, TimeSpan.FromSeconds(expireSeconds));
                return value.ToString();
            }
            return null;
        }
        public T? GetAndRefresh<T>(string key, int expireSeconds) where T : class
        {
            var fullKey = BuildKey(key);
            var value = Db.StringGet(fullKey);
            if (!value.IsNullOrEmpty)
            {
                Db.KeyExpire(fullKey, TimeSpan.FromSeconds(expireSeconds));
                return _serializer.Deserialize<T>(value!);
            }
            return default;
        }
        public bool RefreshExpire(string key, int expireSeconds)
        {
            return Db.KeyExpire(BuildKey(key), TimeSpan.FromSeconds(expireSeconds));
        }
        public bool ExpireIn(string key, int seconds)
        {
            return Db.KeyExpire(BuildKey(key), TimeSpan.FromSeconds(seconds));
        }
        public bool ExpireAt(string key, DateTime expireTime)
        {
            return Db.KeyExpire(BuildKey(key), expireTime);
        }
        public long? GetExpire(string key)
        {
            var ttl = Db.KeyTimeToLive(BuildKey(key));
            return ttl.HasValue ? (long)ttl.Value.TotalSeconds : null;
        }
        public bool AddIfNotExists(string key, string value, int expireSeconds = -1)
        {
            var fullKey = BuildKey(key);
            var expiry = expireSeconds > 0 ? TimeSpan.FromSeconds(expireSeconds) : (TimeSpan?)null;
            return Db.StringSet(fullKey, value, expiry, When.NotExists);
        }
        public bool AddIfNotExists<T>(string key, T value, int expireSeconds = -1) where T : class
        {
            var fullKey = BuildKey(key);
            var expiry = expireSeconds > 0 ? TimeSpan.FromSeconds(expireSeconds) : (TimeSpan?)null;
            return Db.StringSet(fullKey, _serializer.Serialize(value), expiry, When.NotExists);
        }
        public string? GetAndSet(string key, string newValue, int expireSeconds = -1)
        {
            var fullKey = BuildKey(key);
            var expiry = expireSeconds > 0 ? TimeSpan.FromSeconds(expireSeconds) : (TimeSpan?)null;
            var oldValue = Db.StringGetSet(fullKey, newValue);
            if (expireSeconds > 0)
            {
                Db.KeyExpire(fullKey, expiry);
            }
            return oldValue.IsNullOrEmpty ? null : oldValue.ToString();
        }
        public T? GetAndSet<T>(string key, T newValue, int expireSeconds = -1) where T : class
        {
            var fullKey = BuildKey(key);
            var serialized = _serializer.Serialize(newValue);
            var oldValue = Db.StringGetSet(fullKey, serialized);
            if (expireSeconds > 0)
            {
                Db.KeyExpire(fullKey, TimeSpan.FromSeconds(expireSeconds));
            }
            return oldValue.IsNullOrEmpty ? default : _serializer.Deserialize<T>(oldValue!);
        }
        public long Increment(string key, long value = 1)
        {
            return Db.StringIncrement(BuildKey(key), value);
        }
        public long Decrement(string key, long value = 1)
        {
            return Db.StringDecrement(BuildKey(key), value);
        }
        public long Append(string key, string value)
        {
            return Db.StringAppend(BuildKey(key), value);
        }
        #endregion
        public T? Get<T>(string key) where T : class
        {
            var value = Db.StringGet(BuildKey(key));
@@ -139,6 +342,29 @@
            return Db.StringSet(fullKey, newValue, expiry, When.Exists);
        }
        public bool TryUpdateIfChanged(string key, string newValue, int expireSeconds = -1)
        {
            var fullKey = BuildKey(key);
            var existing = Db.StringGet(fullKey);
            if (existing.IsNullOrEmpty) return false;
            if (existing.ToString() == newValue) return false; // å€¼ç›¸åŒï¼Œä¸æ›´æ–°
            var expiry = expireSeconds > 0 ? TimeSpan.FromSeconds(expireSeconds) : (TimeSpan?)null;
            return Db.StringSet(fullKey, newValue, expiry, When.Exists);
        }
        public bool TryUpdateIfChanged<T>(string key, T newValue, int expireSeconds = -1) where T : class
        {
            var fullKey = BuildKey(key);
            var existing = Db.StringGet(fullKey);
            if (existing.IsNullOrEmpty) return false;
            var newJson = _serializer.Serialize(newValue);
            if (existing.ToString() == newJson) return false; // JSON字符串相同,不更新
            var expiry = expireSeconds > 0 ? TimeSpan.FromSeconds(expireSeconds) : (TimeSpan?)null;
            return Db.StringSet(fullKey, newJson, expiry, When.Exists);
        }
        public string GetOrAdd(string key, string value, int expireSeconds = -1)
        {
            var fullKey = BuildKey(key);
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_RedisService/Connection/RedisConnectionManager.cs
@@ -2,6 +2,7 @@
using Microsoft.Extensions.Options;
using StackExchange.Redis;
using System.Linq;
using WIDESEAWCS_Core.Helper;
using WIDESEAWCS_RedisService.Options;
namespace WIDESEAWCS_RedisService.Connection
@@ -21,8 +22,6 @@
                {
                    // å¼ºåˆ¶è®¿é—®Value来触发连接创建
                    var connected = _connection.Value.IsConnected;
                    _logger.LogDebug("IsConnected检查: IsValueCreated={IsValueCreated}, IsConnected={Connected}",
                        _connection.IsValueCreated, connected);
                    return connected;
                }
                catch (Exception ex)
@@ -37,22 +36,17 @@
        {
            _options = options.Value;
            _logger = logger;
            _logger.LogInformation("RedisConnectionManager构造开始, ConnectionString={ConnectionString}, Enabled={Enabled}",
                _options.ConnectionString, _options.Enabled);
            _connection = new Lazy<ConnectionMultiplexer>(CreateConnection);
            _logger.LogInformation("RedisConnectionManager构造完成, Lazy已创建");
        }
        private ConnectionMultiplexer CreateConnection()
        {
            try
            {
                _logger.LogInformation("开始创建Redis连接, ConnectionString={ConnectionString}", _options.ConnectionString);
                var configOptions = ConfigurationOptions.Parse(_options.ConnectionString);
                configOptions.AbortOnConnectFail = false;
                configOptions.ConnectRetry = _options.ConnectRetry;
                configOptions.DefaultDatabase = _options.DefaultDatabase;
                _logger.LogInformation("ConfigurationOptions解析完成, EndPoints={EndPoints}", string.Join(",", configOptions.EndPoints.Select(e => e.ToString())));
                if (_options.EnableSentinel && _options.SentinelEndpoints.Count > 0)
                {
@@ -65,16 +59,16 @@
                var connection = ConnectionMultiplexer.Connect(configOptions);
                connection.ConnectionFailed += (_, e) =>
                    _logger.LogError("Redis连接失败: {FailureType}", e.FailureType);
                    ConsoleHelper.WriteErrorLine($"Redis连接失败: {e.FailureType}");
                connection.ConnectionRestored += (_, e) =>
                    _logger.LogInformation("Redis连接恢复: {EndPoint}", e.EndPoint);
                    ConsoleHelper.WriteSuccessLine($"Redis连接恢复: {e.EndPoint}");
                _logger.LogInformation("Redis连接成功: {EndPoints}", string.Join(",", configOptions.EndPoints));
                ConsoleHelper.WriteSuccessLine($"Redis连接成功: {string.Join(",", configOptions.EndPoints)}");
                return connection;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Redis连接创建失败");
                ConsoleHelper.WriteErrorLine($"Redis连接创建失败:{ex.Message}");
                throw;
            }
        }
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_RedisService/Extensions/RedisServiceSetup.cs
@@ -71,6 +71,12 @@
            services.AddSingleton<ISessionStorage, RedisSessionStorage>();
            services.AddSingleton<IRedisMonitorService, RedisMonitorService>();
            // åŽå°æœåŠ¡ï¼ˆä»…åœ¨å¯ç”¨L1缓存时注册)
            if (options.EnableL1Cache)
            {
                services.AddHostedService<CacheSyncBackgroundService>();
            }
            return services;
        }
    }
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_RedisService/Options/RedisOptions.cs
@@ -26,6 +26,26 @@
        public string KeyPrefix { get; set; } = "wcs:";
        /// <summary>
        /// æ˜¯å¦å¯ç”¨L1内存缓存层。禁用后只使用Redis,适用于需要外部修改Redis数据的场景
        /// </summary>
        public bool EnableL1Cache { get; set; } = true;
        /// <summary>
        /// æ˜¯å¦å¯ç”¨Redis到内存缓存的自动同步
        /// </summary>
        public bool EnableAutoSync { get; set; } = true;
        /// <summary>
        /// è‡ªåŠ¨åŒæ­¥é—´éš”æ—¶é—´ï¼ˆç§’ï¼‰ï¼Œé»˜è®¤30秒
        /// </summary>
        public int SyncIntervalSeconds { get; set; } = 30;
        /// <summary>
        /// åŒæ­¥æ—¶å•次批量获取的Redis key数量上限,防止一次扫描过多key
        /// </summary>
        public int SyncBatchSize { get; set; } = 1000;
        public MonitoringOptions Monitoring { get; set; } = new();
        public EvictionOptions Eviction { get; set; } = new();
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Server.sln
@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.9.34728.123
# Visual Studio Version 18
VisualStudioVersion = 18.2.11415.280 d18.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WIDESEAWCS_Server", "WIDESEAWCS_Server\WIDESEAWCS_Server.csproj", "{487FA45B-EA1A-4ACA-BB5B-0F6708F462C0}"
EndProject
@@ -65,6 +65,8 @@
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WIDESEAWCS_BasicInfoService", "WIDESEAWCS_BasicInfoService\WIDESEAWCS_BasicInfoService.csproj", "{FFAB2C76-1C9E-4006-95C8-A0B2AA53139D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WIDESEAWCS_RedisService", "WIDESEAWCS_RedisService\WIDESEAWCS_RedisService.csproj", "{F9886971-C3B2-4334-B014-D5109F2041DE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WIDESEAWCS_Tests", "WIDESEAWCS_Tests\WIDESEAWCS_Tests.csproj", "{D4D17AAD-CB14-AF78-5BD1-F16380EBE911}"
EndProject
Global
    GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -328,6 +330,18 @@
        {F9886971-C3B2-4334-B014-D5109F2041DE}.Release|x64.Build.0 = Release|Any CPU
        {F9886971-C3B2-4334-B014-D5109F2041DE}.Release|x86.ActiveCfg = Release|Any CPU
        {F9886971-C3B2-4334-B014-D5109F2041DE}.Release|x86.Build.0 = Release|Any CPU
        {D4D17AAD-CB14-AF78-5BD1-F16380EBE911}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
        {D4D17AAD-CB14-AF78-5BD1-F16380EBE911}.Debug|Any CPU.Build.0 = Debug|Any CPU
        {D4D17AAD-CB14-AF78-5BD1-F16380EBE911}.Debug|x64.ActiveCfg = Debug|Any CPU
        {D4D17AAD-CB14-AF78-5BD1-F16380EBE911}.Debug|x64.Build.0 = Debug|Any CPU
        {D4D17AAD-CB14-AF78-5BD1-F16380EBE911}.Debug|x86.ActiveCfg = Debug|Any CPU
        {D4D17AAD-CB14-AF78-5BD1-F16380EBE911}.Debug|x86.Build.0 = Debug|Any CPU
        {D4D17AAD-CB14-AF78-5BD1-F16380EBE911}.Release|Any CPU.ActiveCfg = Release|Any CPU
        {D4D17AAD-CB14-AF78-5BD1-F16380EBE911}.Release|Any CPU.Build.0 = Release|Any CPU
        {D4D17AAD-CB14-AF78-5BD1-F16380EBE911}.Release|x64.ActiveCfg = Release|Any CPU
        {D4D17AAD-CB14-AF78-5BD1-F16380EBE911}.Release|x64.Build.0 = Release|Any CPU
        {D4D17AAD-CB14-AF78-5BD1-F16380EBE911}.Release|x86.ActiveCfg = Release|Any CPU
        {D4D17AAD-CB14-AF78-5BD1-F16380EBE911}.Release|x86.Build.0 = Release|Any CPU
    EndGlobalSection
    GlobalSection(SolutionProperties) = preSolution
        HideSolutionNode = FALSE
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Server/RedisʹÓð¸Àý.md
@@ -1,6 +1,6 @@
# Redis æœåŠ¡ä½¿ç”¨æ¡ˆä¾‹
## 1. ç¼“存(ICacheService)
## 1. ç¼“存(ICacheService)- åŸºç¡€æ“ä½œ
通过构造函数注入 `ICacheService`,HybridCacheService è‡ªåŠ¨å®žçŽ° L1(内存) + L2(Redis) åŒå±‚缓存。
@@ -38,7 +38,211 @@
}
```
## 2. åˆ†å¸ƒå¼é”ï¼ˆIDistributedLockService)
## 2. ç¼“存(ICacheService)- æ‰©å±•删除方法
```csharp
public class CacheDeleteDemo
{
    private readonly ICacheService _cache;
    // åˆ é™¤å¹¶èŽ·å–å€¼
    public string? RemoveAndGet(string key)
    {
        return _cache.RemoveAndGet(key); // è¿”回被删除的值
    }
    // æŒ‰å‰ç¼€åˆ é™¤æ‰€æœ‰åŒ¹é…çš„key
    public int ClearUserCache()
    {
        return _cache.RemoveByPrefix("user:"); // åˆ é™¤æ‰€æœ‰ user: å¼€å¤´çš„key
    }
    // æŒ‰æ¨¡å¼åˆ é™¤ï¼ˆæ”¯æŒé€šé…ç¬¦ï¼‰
    public int ClearSessionCache()
    {
        return _cache.RemoveByPattern("session:123:*"); // åˆ é™¤ session:123: å¼€å¤´çš„æ‰€æœ‰key
    }
    // æ‰¹é‡åˆ é™¤å¹¶è¿”回成功数量
    public int RemoveMultiple()
    {
        var keys = new[] { "key1", "key2", "key3" };
        return _cache.RemoveAll(keys); // è¿”回实际删除的数量
    }
    // æ¡ä»¶åˆ é™¤
    public int RemoveTempCache()
    {
        return _cache.RemoveWhere(key => key.Contains("temp")); // åˆ é™¤åŒ…含"temp"的key
    }
}
```
## 3. ç¼“存(ICacheService)- æ·»åŠ å’Œä¿®æ”¹æ‰©å±•æ–¹æ³•
```csharp
public class CacheAdvancedDemo
{
    private readonly ICacheService _cache;
    // æ‰¹é‡æ·»åŠ 
    public void AddMultiple()
    {
        var items = new Dictionary<string, string>
        {
            { "user:1", "张三" },
            { "user:2", "李四" },
            { "user:3", "王五" }
        };
        _cache.AddAll(items, 300); // æ‰¹é‡æ·»åŠ ï¼Œ300秒过期
    }
    // æ‰¹é‡æ·»åŠ å¯¹è±¡
    public void AddMultipleObjects()
    {
        var items = new Dictionary<string, object>
        {
            { "order:1", new { Id = 1, Amount = 100 } },
            { "order:2", new { Id = 2, Amount = 200 } }
        };
        _cache.AddAllObjects(items, 600);
    }
    // æ›¿æ¢ï¼ˆä»…存在时替换)
    public bool ReplaceExisting()
    {
        return _cache.Replace("user:1", "新用户名"); // key不存在返回false
    }
    // èŽ·å–å¹¶åˆ·æ–°è¿‡æœŸæ—¶é—´
    public string? GetAndRefresh(string key)
    {
        return _cache.GetAndRefresh(key, 1800); // èŽ·å–å€¼å¹¶å»¶é•¿30分钟
    }
    // åˆ·æ–°è¿‡æœŸæ—¶é—´
    public bool RefreshExpire(string key)
    {
        return _cache.RefreshExpire(key, 3600); // åˆ·æ–°ä¸º1小时后过期
    }
    // è®¾ç½®N秒后过期
    public bool SetExpireIn(string key, int seconds)
    {
        return _cache.ExpireIn(key, seconds);
    }
    // è®¾ç½®åœ¨æŒ‡å®šæ—¶é—´ç‚¹è¿‡æœŸ
    public bool SetExpireAt(string key, DateTime expireTime)
    {
        return _cache.ExpireAt(key, expireTime);
    }
    // èŽ·å–å‰©ä½™è¿‡æœŸæ—¶é—´
    public long? GetTTL(string key)
    {
        return _cache.GetExpire(key); // è¿”回剩余秒数,null表示永不过期或key不存在
    }
}
```
## 4. ç¼“存(ICacheService)- åŽŸå­æ“ä½œæ–¹æ³•
```csharp
public class AtomicOperationDemo
{
    private readonly ICacheService _cache;
    // åŽŸå­æ·»åŠ ï¼ˆä»…ä¸å­˜åœ¨æ—¶æ·»åŠ ï¼‰- åˆ†å¸ƒå¼é”åœºæ™¯
    public bool AcquireLock(string lockKey, string lockValue)
    {
        return _cache.AddIfNotExists(lockKey, lockValue, 30); // 30秒自动过期
    }
    // èŽ·å–æ—§å€¼å¹¶è®¾ç½®æ–°å€¼
    public string? GetAndSet(string key, string newValue)
    {
        return _cache.GetAndSet(key, newValue); // è¿”回旧值,设置新值
    }
    // è‡ªå¢žè®¡æ•°å™¨
    public long IncrementCounter(string key)
    {
        return _cache.Increment(key); // è‡ªå¢ž1,返回新值
    }
    // è‡ªå¢žæŒ‡å®šå€¼
    public long IncrementBy(string key, long value)
    {
        return _cache.Increment(key, value); // è‡ªå¢žvalue
    }
    // è‡ªå‡è®¡æ•°å™¨
    public long DecrementCounter(string key)
    {
        return _cache.Decrement(key); // è‡ªå‡1
    }
    // è¿½åР内容
    public long AppendContent(string key, string content)
    {
        return _cache.Append(key, content); // è¿”回追加后的字符串长度
    }
}
```
## 5. ç¼“存(ICacheService)- ConcurrentDictionary风格方法
```csharp
public class ConcurrentStyleDemo
{
    private readonly ICacheService _cache;
    // å°è¯•添加(仅不存在时添加)
    public bool TryAdd(string key, string value)
    {
        return _cache.TryAdd(key, value, 60); // key存在返回false
    }
    // å°è¯•获取
    public bool TryGet(string key, out string? value)
    {
        return _cache.TryGetValue(key, out value);
    }
    // å°è¯•移除并返回值
    public bool TryRemove(string key, out string? value)
    {
        return _cache.TryRemove(key, out value);
    }
    // å°è¯•更新(仅存在时更新)
    public bool TryUpdate(string key, string newValue)
    {
        return _cache.TryUpdate(key, newValue, 60);
    }
    // å€¼æ”¹å˜æ—¶æ›´æ–°ï¼ˆé¿å…æ— æ•ˆå†™å…¥ï¼‰
    public bool TryUpdateIfChanged(string key, string newValue)
    {
        return _cache.TryUpdateIfChanged(key, newValue, 60); // å€¼ç›¸åŒè¿”回false
    }
    // èŽ·å–æˆ–æ·»åŠ 
    public string GetOrAdd(string key)
    {
        return _cache.GetOrAdd(key, "默认值", 60);
    }
    // èŽ·å–æˆ–æ·»åŠ ï¼ˆå·¥åŽ‚æ–¹æ³•ï¼‰
    public T GetOrAdd<T>(string key, Func<string, T> factory) where T : class
    {
        return _cache.GetOrAdd(key, factory, 60);
    }
}
```
## 6. åˆ†å¸ƒå¼é”ï¼ˆIDistributedLockService)
```csharp
public class OrderService
@@ -80,7 +284,7 @@
}
```
## 3. è®¡æ•°å™¨ï¼ˆICounterService)
## 7. è®¡æ•°å™¨ï¼ˆICounterService)
```csharp
public class StatisticsService
@@ -107,7 +311,7 @@
}
```
## 4. å‘布/订阅(IMessageQueueService)
## 8. å‘布/订阅(IMessageQueueService)
```csharp
public class NotificationService
@@ -143,7 +347,7 @@
}
```
## 5. é™æµï¼ˆIRateLimitingService)
## 9. é™æµï¼ˆIRateLimitingService)
```csharp
public class ApiController
@@ -175,7 +379,7 @@
}
```
## 6. åˆ†å¸ƒå¼ID生成器(IDistributedIdGenerator)
## 10. åˆ†å¸ƒå¼ID生成器(IDistributedIdGenerator)
```csharp
public class TaskService
@@ -203,7 +407,7 @@
}
```
## 7. æŽ’行榜(ILeaderboardService)
## 11. æŽ’行榜(ILeaderboardService)
```csharp
public class LeaderboardDemo
@@ -231,7 +435,7 @@
}
```
## 8. å¯¹è±¡å­˜å‚¨ï¼ˆIObjectStorageService)
## 12. å¯¹è±¡å­˜å‚¨ï¼ˆIObjectStorageService)
```csharp
public class DeviceService
@@ -259,7 +463,7 @@
}
```
## 9. é…ç½®ä¸­å¿ƒï¼ˆIConfigurationCenterService)
## 13. é…ç½®ä¸­å¿ƒï¼ˆIConfigurationCenterService)
```csharp
public class ConfigDemo
@@ -290,7 +494,7 @@
}
```
## 10. ç›‘控(IRedisMonitorService)
## 14. ç›‘控(IRedisMonitorService)
```csharp
public class MonitorDemo
@@ -318,7 +522,7 @@
}
```
## 11. Session存储(ISessionStorage)
## 15. Session存储(ISessionStorage)
```csharp
public class SessionDemo
@@ -350,7 +554,7 @@
}
```
## 12. å¸ƒéš†è¿‡æ»¤å™¨ï¼ˆIBloomFilterService)
## 16. å¸ƒéš†è¿‡æ»¤å™¨ï¼ˆIBloomFilterService)
```csharp
public class BloomFilterDemo
@@ -378,3 +582,45 @@
    }
}
```
## 17. ç¼“存自动同步(CacheSyncBackgroundService)
Redis到内存缓存的自动同步后台服务,解决L1+L2混合缓存中外部修改Redis数据导致内存缓存不一致的问题。
### é…ç½®è¯´æ˜Ž
在 `appsettings.json` ä¸­é…ç½®ï¼š
```json
{
  "RedisConfig": {
    "EnableL1Cache": true,           // å¯ç”¨L1内存缓存
    "EnableAutoSync": true,          // å¯ç”¨è‡ªåŠ¨åŒæ­¥
    "SyncIntervalSeconds": 30,       // åŒæ­¥é—´éš”:30秒
    "SyncBatchSize": 1000            // å•次批量获取key数量上限
  }
}
```
### å·¥ä½œåŽŸç†
1. **启动时全量同步**:服务启动后立即执行一次全量同步
2. **定期增量同步**:按照配置的间隔(默认30秒)定期执行同步
3. **双向同步**:
   - å°†Redis中的数据更新到内存缓存
   - æ¸…理内存缓存中不存在于Redis的key
4. **智能TTL同步**:同步时会保留Redis中设置的过期时间
### ä½¿ç”¨åœºæ™¯
- **多系统共享Redis**:当多个WCS实例共享同一个Redis,一个实例修改数据后,其他实例能自动同步
- **外部修改Redis**:通过Redis CLI或其他工具修改数据后,应用能自动获取最新值
- **缓存一致性保障**:避免启用L1缓存后,内存缓存和Redis数据不一致的问题
### æ³¨æ„äº‹é¡¹
- ä»…在 `EnableL1Cache = true` ä¸” `EnableAutoSync = true` æ—¶è¿è¡Œ
- IMemoryCache不支持枚举,因此只能清理已跟踪的key
- åŒæ­¥é—´éš”建议设置为30-60秒,过短会影响性能
- å¯¹äºŽè¦æ±‚强一致性的场景,建议直接禁用L1缓存(`EnableL1Cache: false`)
```
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Server/appsettings.json
@@ -75,6 +75,10 @@
    "SerializerType": "Newtonsoft", //序列化方式:Newtonsoft
    "FallbackToMemory": true, //Redis不可用时是否降级到内存缓存
    "KeyPrefix": "wcs:", //全局Key前缀,用于隔离不同系统的数据
    "EnableL1Cache": true, //是否启用L1内存缓存层。禁用后只使用Redis,适用于需要外部修改Redis数据的场景
    "EnableAutoSync": true, //是否启用Redis到内存缓存的自动同步
    "SyncIntervalSeconds": 30, //自动同步间隔时间(秒),建议30-60秒
    "SyncBatchSize": 1000, //同步时单次批量获取的Redis key数量上限
    "Monitoring": {
      "Enabled": false, //是否启用监控
      "SlowLogThresholdMs": 100, //慢查询阈值(毫秒)
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_TaskInfoService/TaskExecuteDetailService.cs
@@ -99,7 +99,7 @@
            }
            catch (Exception ex)
            {
                Console.WriteLine($"AddTaskExecuteDetail æ·»åŠ ä»»åŠ¡æ‰§è¡Œè¯¦æƒ…å¤±è´¥: {ex.Message}");
            }
        }
@@ -220,7 +220,7 @@
            }
            catch (Exception ex)
            {
                content = WebResponseContent.Instance.Error($"获取任务详情失败: {ex.Message}");
            }
            return content;
        }
@@ -244,7 +244,7 @@
            }
            catch (Exception ex)
            {
                content = WebResponseContent.Instance.Error($"获取任务详情失败: {ex.Message}");
            }
            return content;
        }
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_TaskInfoService/TaskService.cs
@@ -179,6 +179,9 @@
        /// <returns></returns>
        public Dt_Task QueryExecutingConveyorLineTask(int taskNum, string nextAddress)
        {
            if (string.IsNullOrEmpty(nextAddress))
                throw new ArgumentNullException(nameof(nextAddress), "下一地址不能为空");
            return BaseDal.QueryFirst(x => x.TaskNum == taskNum && x.NextAddress == nextAddress && (x.TaskState == (int)TaskInStatusEnum.Line_InExecuting || x.TaskState == (int)TaskOutStatusEnum.Line_OutExecuting), TaskOrderBy);
        }
@@ -190,6 +193,9 @@
        /// <returns></returns>
        public Dt_Task QueryCompletedConveyorLineTask(int taskNum, string currentAddress)
        {
            if (string.IsNullOrEmpty(currentAddress))
                throw new ArgumentNullException(nameof(currentAddress), "当前地址不能为空");
            return BaseDal.QueryFirst(x => x.TaskNum == taskNum && x.CurrentAddress == currentAddress && (x.TaskState == (int)TaskInStatusEnum.Line_InFinish || x.TaskState == (int)TaskOutStatusEnum.Line_OutFinish), TaskOrderBy);
        }
@@ -273,6 +279,9 @@
            WebResponseContent content = new WebResponseContent();
            try
            {
                if (string.IsNullOrEmpty(message))
                    throw new ArgumentNullException(nameof(message), "异常信息不能为空");
                Dt_Task task = BaseDal.QueryFirst(x => x.TaskNum == taskNum);
                if (task == null) return WebResponseContent.Instance.Error($"未找到该任务信息,任务号:【{taskNum}】");
                if (task.TaskType.GetTaskTypeGroup() == TaskTypeGroup.OutbondGroup)
@@ -371,7 +380,8 @@
                        {
                            PalletCode = task.PalletCode,
                        };
                        var result = _httpClientHelper.Post<WebResponseContent>("WMS", taskDto.ToJson(), nameof(ConfigKey.GetTasksLocation));
                        var result = _httpClientHelper.Post<WebResponseContent>(nameof(ConfigKey.GetTasksLocation), taskDto.ToJson(), nameof(ConfigKey.GetTasksLocation));
                        if (!result.IsSuccess && !result.Data.Status)
                        {
                            return WebResponseContent.Instance.Error($"调用WMS接口获取任务目标地址失败,任务号:【{task.TaskNum}】,错误信息:【{content.Message}】");
@@ -434,6 +444,9 @@
        {
            try
            {
                if (string.IsNullOrEmpty(currentAddress))
                    throw new ArgumentNullException(nameof(currentAddress), "当前地址不能为空");
                Dt_Task task = BaseDal.QueryFirst(x => x.TaskNum == taskNum && x.CurrentAddress == currentAddress);
                if (task == null) throw new Exception($"未找到该任务信息,任务号:【{taskNum}】");
@@ -455,6 +468,7 @@
            }
            catch (Exception ex)
            {
                Console.WriteLine($"UpdatePosition æ›´æ–°ä»»åŠ¡ä½ç½®å¤±è´¥,任务号:【{taskNum}】,错误信息:【{ex.Message}】");
            }
            return null;
        }
@@ -487,7 +501,7 @@
                    _taskExecuteDetailService.AddTaskExecuteDetail(task.TaskId, $"堆垛机出库完成");
                    var result = _httpClientHelper.Post<WebResponseContent>("WMS", (new StockInfoDTO() { PalletCode = task.PalletCode, TaskNum = task.TaskNum }).ToJson());
                    var result = _httpClientHelper.Post<WebResponseContent>(nameof(ConfigKey.OutboundFinishTaskAsync), (new StockInfoDTO() { PalletCode = task.PalletCode, TaskNum = task.TaskNum }).ToJson());
                    if (result.IsSuccess && result.Data.Status)
                    {
                        return content.Error($"通知WMS系统堆垛机出库完成成功,任务号:【{task.TaskNum}】,托盘号:【{task.PalletCode}】");
@@ -508,7 +522,7 @@
                    _taskExecuteDetailService.AddTaskExecuteDetail(task.TaskId, $"堆垛机入库完成");
                    var result = _httpClientHelper.Post<WebResponseContent>("WMS", (new CreateTaskDto()
                    var result = _httpClientHelper.Post<WebResponseContent>(nameof(ConfigKey.InboundFinishTaskAsync), (new CreateTaskDto()
                    {
                        PalletCode = task.PalletCode,
                    }).ToJson());
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/CommonConveyorLineNewJob.cs
@@ -81,6 +81,12 @@
                        {
                            ConveyorLineTaskCommandNew command = conveyorLine.ReadCustomer<ConveyorLineTaskCommandNew>(childDeviceCode);
                            if (command == null)
                            {
                                return;
                            }
                            #region æ£€æŸ¥ç‰¹å®šä½ç½®æ˜¯å¦æœ‰æ‰˜ç›˜
                            var checkPalletPositions = App.Configuration.GetSection("CheckPalletPositions")
@@ -112,10 +118,7 @@
                            #endregion
                            if (command == null || command.PLC_STB != 1)
                            {
                                return;
                            }
                            if (command.PLC_STB != 1) return;//PLC_STB=1时才处理任务
                            if (command.Barcode.IsNullOrEmpty())
                            {
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotJob.cs
@@ -1,22 +1,22 @@
using HslCommunication;
using Newtonsoft.Json;
using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup;
using Quartz;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Sockets;
using System.Text.Json;
using System.Threading.Tasks;
using WIDESEA_Core;
using WIDESEAWCS_Common;
using WIDESEAWCS_Common.HttpEnum;
using WIDESEAWCS_Common.TaskEnum;
using WIDESEAWCS_Core;
using WIDESEAWCS_Core.Caches;
using WIDESEAWCS_Core.Helper;
using WIDESEAWCS_Core.Http;
using WIDESEAWCS_DTO.Stock;
using WIDESEAWCS_DTO.TaskInfo;
using WIDESEAWCS_ITaskInfoRepository;
using WIDESEAWCS_ITaskInfoService;
using WIDESEAWCS_Model.Models;
using WIDESEAWCS_QuartzJob;
using WIDESEAWCS_QuartzJob.Service;
using WIDESEAWCS_Tasks.SocketServer;
namespace WIDESEAWCS_Tasks
@@ -27,22 +27,25 @@
        private const int MaxTaskTotalNum = 48;
        private readonly TcpSocketServer _TcpSocket;
        //private static readonly ConcurrentDictionary<string, RobotSocketState> _socketStates = new();
        private static int _eventSubscribedFlag;
        private readonly ITaskService _taskService;
        private readonly IRobotTaskService _robotTaskService;
        private readonly ICacheService _cache;
        private readonly HttpClientHelper _httpClientHelper;
        private static IRobotTaskService _latestRobotTaskService = null!;
        private static ITaskService _latestTaskService = null!;
        public RobotJob(TcpSocketServer TcpSocket, IRobotTaskService RobottaskService, ITaskService TaskService, ICacheService cache)
        public RobotJob(TcpSocketServer TcpSocket, IRobotTaskService RobottaskService, ITaskService TaskService, ICacheService cache, HttpClientHelper httpClientHelper)
        {
            _TcpSocket = TcpSocket;
            _robotTaskService = RobottaskService;
            _taskService = TaskService;
            _cache = cache;
            _httpClientHelper = httpClientHelper;
            _latestRobotTaskService = RobottaskService;
            _latestTaskService = TaskService;
@@ -60,7 +63,7 @@
            string ipAddress = robotCrane.IPAddress;
            // èŽ·å–æˆ–åˆ›å»ºçŠ¶æ€
            RobotSocketState state = _cache.GetOrAdd(ipAddress, _ => new RobotSocketState
            RobotSocketState state = _cache.GetOrAdd($"{RedisPrefix.Code}:{RedisName.SocketDevices}:{ipAddress}", _ => new RobotSocketState
            {
                IPAddress = ipAddress,
                RobotCrane = robotCrane
@@ -71,7 +74,6 @@
            try
            {
                // æ£€æŸ¥æ˜¯å¦æœ‰è¯¥å®¢æˆ·ç«¯è¿žæŽ¥
                var clientIds = _TcpSocket.GetClientIds();
                if (!clientIds.Contains(ipAddress))
@@ -97,6 +99,9 @@
                                    Console.WriteLine($"HandleClientAsync error: {t.Exception?.GetBaseException().Message}");
                            }, TaskContinuationOptions.OnlyOnFaulted);
                        state.IsEventSubscribed = true;
                        // æ›´æ–°ç¼“存中的状态
                        _cache.TryUpdateIfChanged($"{RedisPrefix.Code}:{RedisName.SocketDevices}:{ipAddress}", state);
                    }
                }
@@ -104,16 +109,12 @@
                Dt_RobotTask? task = GetTask(robotCrane);
                if (task != null)
                {
                    state.IsSplitPallet = task.RobotTaskType == RobotTaskTypeEnum.SplitPallet.GetHashCode();
                    state.IsGroupPallet = task.RobotTaskType == RobotTaskTypeEnum.GroupPallet.GetHashCode() || task.RobotTaskType == RobotTaskTypeEnum.ChangePallet.GetHashCode();
                    state.CurrentTask = task;
                    if (task.RobotTaskTotalNum <= MaxTaskTotalNum)
                    {
                        // å¤„理正在执行的任务
                        if (state.RobotRunMode == 2 && state.RobotControlMode == 1 && state.OperStatus != "Running")
                        {
                            await Task.Delay(1000);
                            if (state.CurrentAction == "PickFinished" && state.RobotArmObject == 1 && task.RobotTaskState != TaskRobotStatusEnum.RobotExecuting.GetHashCode())
                            if (state.CurrentAction == "PickFinished" && state.RobotArmObject == 1 && task.RobotTaskState == TaskRobotStatusEnum.RobotPickFinish.GetHashCode())
                            {
                                string taskString = $"Putbattery,{task.RobotTargetAddress}";
                                bool result = await _TcpSocket.SendToClientAsync(ipAddress, taskString);
@@ -123,16 +124,18 @@
                                    await _robotTaskService.UpdateRobotTaskAsync(task);
                                }
                            }
                            else if (state.CurrentAction == "PutFinished" && state.RobotArmObject == 0 && task.RobotTaskState != TaskRobotStatusEnum.RobotExecuting.GetHashCode())
                            else if (state.CurrentAction == "PutFinished" && state.RobotArmObject == 0 && task.RobotTaskState == TaskRobotStatusEnum.RobotPutFinish.GetHashCode())
                            {
                                task.RobotTaskState = TaskRobotStatusEnum.RobotExecuting.GetHashCode();
                                await _robotTaskService.UpdateRobotTaskAsync(task);
                            }
                            else if (state.OperStatus == "Homed" && state.RobotArmObject == 0 && task.RobotTaskState != TaskRobotStatusEnum.RobotExecuting.GetHashCode())
                            {
                                // TODO è¯»å–线体电池条码,发送取电池指令
                                // éšæœºç”Ÿæˆä¸¤å¤©æ‰˜ç›˜æ¡ç å­˜æ”¾åˆ°ä¸¤ä¸ªå˜é‡é‡Œé¢
                                // å®šä¹‰å‰ç¼€ï¼ˆä¾‹å¦‚:TRAY代表托盘)
                                // ç»„盘读取线体条码
                                if (task.RobotTaskType == RobotTaskTypeEnum.GroupPallet.GetHashCode())
                                {
                                string prefix = "TRAY";
                                // ç”Ÿæˆä¸¤ä¸ªæ‰˜ç›˜æ¡ç 
@@ -140,16 +143,23 @@
                                string trayBarcode2 = GenerateTrayBarcode(state, prefix);
                                if (!trayBarcode1.IsNullOrEmpty() && !trayBarcode2.IsNullOrEmpty())
                                {
                                    string taskString = $"Pickbattery,{task.RobotSourceAddress}";
                                    // å‘送任务指令
                                    bool result = await _TcpSocket.SendToClientAsync(ipAddress, taskString);
                                    if (result)
                                        await SendSocketRobotPickAsync(task, state);
                                    }
                                }
                                else // æ¢ç›˜ç›´æŽ¥å‘送取货地址
                                    {
                                        // TODO å¤„理成功发送任务指令后的逻辑
                                        task.RobotTaskState = TaskRobotStatusEnum.RobotExecuting.GetHashCode();
                                        result = await _robotTaskService.UpdateRobotTaskAsync(task);
                                    await SendSocketRobotPickAsync(task, state);
                                    }
                                }
                            if (state.CurrentTask.IsNullOrEmpty() && state.ToJson() != task.ToJson())
                            {
                                state.IsSplitPallet = task.RobotTaskType == RobotTaskTypeEnum.SplitPallet.GetHashCode();
                                state.IsGroupPallet = task.RobotTaskType == RobotTaskTypeEnum.GroupPallet.GetHashCode() || task.RobotTaskType == RobotTaskTypeEnum.ChangePallet.GetHashCode();
                                state.CurrentTask = task;
                                // æ›´æ–°ç¼“存中的状态
                                _cache.TryUpdateIfChanged($"{RedisPrefix.Code}:{RedisName.SocketDevices}:{ipAddress}", state);
                            }
                        }
                    }
@@ -157,13 +167,10 @@
            }
            catch (Exception)
            {
            }
            finally
            {
                // å¯é€‰ï¼šåœ¨è¿™é‡Œå¤„理任何需要在任务完成后执行的清理工作
                // æ›´æ–°ç¼“存中的状态
                _cache.AddOrUpdate(ipAddress, state);
            }
        }
@@ -181,7 +188,7 @@
            // ç»„合:前缀 + æ—¥æœŸ + æ—¶é—´ + éšæœºæ•°
            var barCode = prefix + datePart + timePart + randomPart;
            state.CellBarcode.Add(randomPart);
            state.CellBarcode.Add(barCode);
            return barCode;
        }
@@ -193,7 +200,14 @@
        /// <returns></returns>
        private Task<string?> _TcpSocket_RobotReceived(string clientId)
        {
            _cache.TryRemove(clientId, out _);
            var robotSocketState = _cache.Get<RobotSocketState>($"{RedisPrefix.Code}:{RedisName.SocketDevices}:{clientId}");
            robotSocketState.IsEventSubscribed = false;
            robotSocketState.CurrentAction = "";
            robotSocketState.OperStatus = "";
            robotSocketState.RobotArmObject = 0;
            robotSocketState.RobotControlMode = 0;
            robotSocketState.RobotRunMode = 0;
            _cache.TryUpdateIfChanged($"{RedisPrefix.Code}:{RedisName.SocketDevices}:{clientId}", robotSocketState);
            return Task.FromResult<string?>(null);
        }
@@ -207,15 +221,16 @@
        /// <returns></returns>
        private async Task<string?> _TcpSocket_MessageReceived(string message, bool isJson, TcpClient client, RobotSocketState state)
        {
            if (!(bool)(_cache?.TryGetValue($"{RedisPrefix.Code}:{RedisName.SocketDevices}:{client.Client.RemoteEndPoint}", out state)))
                return null;
            string messageLower = message.ToLowerInvariant();
            if (await IsSimpleCommandAsync(messageLower, state))
            {
                await _TcpSocket.SendMessageAsync(client, message);
                return null;
            }
            if (IsPrefixCommand(messageLower))
            else if (IsPrefixCommand(messageLower))
            {
                try
                {
@@ -238,9 +253,9 @@
                                var stockDTO = BuildStockDTO(state, positions);
                                state.LastPickPositions = positions;
                                var result = await HttpRequestHelper.HTTPPostAsync(nameof(Category.WMS), stockDTO.ToJsonString(), nameof(ConfigKey.SplitPalletAsync));
                                var result = _httpClientHelper.Post<WebResponseContent>(nameof(ConfigKey.SplitPalletAsync), stockDTO.ToJson());
                                if (result.Status)
                                if (result.Data.Status && result.IsSuccess)
                                {
                                    state.CurrentAction = "PickFinished";
                                }
@@ -250,6 +265,7 @@
                                state.CurrentAction = "PickFinished";
                            }
                            state.LastPickPositions = positions;
                            task.RobotTaskState = TaskRobotStatusEnum.RobotPickFinish.GetHashCode();
                            await _latestRobotTaskService.Repository.UpdateDataAsync(task);
                        }
@@ -262,8 +278,9 @@
                                var stockDTO = BuildStockDTO(state, positions);
                                var configKey = state.CurrentTask?.RobotTaskType == RobotTaskTypeEnum.ChangePallet.GetHashCode()
                                    ? nameof(ConfigKey.ChangePalletAsync) : nameof(ConfigKey.GroupPalletAsync);
                                var result = await HttpRequestHelper.HTTPPostAsync(nameof(Category.WMS), stockDTO.ToJsonString(), configKey);
                                putSuccess = result.Status;
                                var result = _httpClientHelper.Post<WebResponseContent>(configKey, stockDTO.ToJson());
                                putSuccess = result.Data.Status && result.IsSuccess;
                            }
                            if (putSuccess)
@@ -276,17 +293,18 @@
                            task.RobotTaskState = TaskRobotStatusEnum.RobotPutFinish.GetHashCode();
                            await _latestRobotTaskService.Repository.UpdateDataAsync(task);
                        }
                        await _TcpSocket.SendMessageAsync(client, message);
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"RobotJob MessageReceived Error: {ex.Message}");
                }
                await _TcpSocket.SendMessageAsync(client, message);
                return null;
            }
            // æ›´æ–°ç¼“存中的状态
            _cache.TryUpdateIfChanged($"{RedisPrefix.Code}:{RedisName.SocketDevices}:{state.IPAddress}", state);
            return null;
        }
@@ -297,7 +315,7 @@
        /// <param name="message"></param>
        /// <param name="state"></param>
        /// <returns></returns>
        private static async Task<bool> IsSimpleCommandAsync(string message, RobotSocketState state)
        private async Task<bool> IsSimpleCommandAsync(string message, RobotSocketState state)
        {
            switch (message)
            {
@@ -378,7 +396,7 @@
            }
        }
        private static async Task HandleInboundTaskAsync(RobotSocketState state, bool useSourceAddress)
        private async Task HandleInboundTaskAsync(RobotSocketState state, bool useSourceAddress)
        {
            var currentTask = state.CurrentTask;
            if (currentTask == null)
@@ -399,9 +417,8 @@
                PalletType = 1,
                TaskType = 4
            };
            var result = await HttpRequestHelper.HTTPPostAsync(nameof(Category.WMS), taskDto.ToJsonString(), nameof(ConfigKey.CreateTaskInboundAsync));
            if (!result.Status)
            var result = _httpClientHelper.Post<WebResponseContent>(nameof(ConfigKey.CreateTaskInboundAsync), taskDto.ToJson());
            if (!result.Data.Status && result.IsSuccess)
            {
                return;
            }
@@ -433,20 +450,21 @@
        private static StockDTO BuildStockDTO(RobotSocketState state, int[] positions)
        {
            string sss = state.ToJson();
            return new StockDTO
            {
                SourceLineNo = state.CurrentTask?.RobotSourceAddressLineCode,
                SourcePalletNo = state.CurrentTask?.RobotSourceAddressPalletCode,
                TargetPalletNo = state.CurrentTask?.RobotTargetAddressPalletCode,
                TargetLineNo = state.CurrentTask?.RobotTargetAddressLineCode,
                SourceLineNo = state.CurrentTask.RobotSourceAddressLineCode,
                SourcePalletNo = state.CurrentTask.RobotSourceAddressPalletCode,
                TargetPalletNo = state.CurrentTask.RobotTargetAddressPalletCode,
                TargetLineNo = state.CurrentTask.RobotTargetAddressLineCode,
                Details = positions
                    .Where(x => x > 0)
                    .OrderBy(x => x)
                    .Select((x, idx) => new StockDetailDTO
                    {
                        Quantity = state.CurrentTask?.RobotTaskTotalNum ?? 1,
                        Quantity = state.RobotTaskTotalNum > 0 ? state.RobotTaskTotalNum + positions.Length : positions.Length,
                        Channel = x,
                        CellBarcode = !state.CellBarcode.IsNullOrEmpty() ? state.CellBarcode[idx] : ""
                        CellBarcode = state.CellBarcode?.Count > 0 ? state.CellBarcode[x - 1] : ""
                    })
                    .ToList()
            };
@@ -455,6 +473,27 @@
        private Dt_RobotTask? GetTask(RobotCraneDevice robotCrane)
        {
            return _robotTaskService.QueryRobotCraneTask(robotCrane.DeviceCode);
        }
        /// <summary>
        /// å‘送机械手取货命令
        /// </summary>
        /// <param name="task"></param>
        /// <param name="state"></param>
        /// <returns></returns>
        private async Task SendSocketRobotPickAsync(Dt_RobotTask task, RobotSocketState state)
        {
            string taskString = $"Pickbattery,{task.RobotSourceAddress}";
            // å‘送任务指令
            bool result = await _TcpSocket.SendToClientAsync(state.IPAddress, taskString);
            if (result)
            {
                // TODO å¤„理成功发送任务指令后的逻辑
                task.RobotTaskState = TaskRobotStatusEnum.RobotExecuting.GetHashCode();
                result = await _robotTaskService.UpdateRobotTaskAsync(task);
                // æ›´æ–°ç¼“存中的状态
                _cache.TryUpdateIfChanged($"{RedisPrefix.Code}:{RedisName.SocketDevices}:{state.IPAddress}", state);
            }
        }
    }
@@ -510,7 +549,7 @@
        /// <summary>
        /// æŠ“取位置条码
        /// </summary>
        public List<string> CellBarcode { get; set; } = new();
        public List<string> CellBarcode { get; set; }
        /// <summary>
        /// å½“前抓取任务
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ShuttleCarJob/ShuttleCarJob.cs
@@ -8,6 +8,7 @@
using WIDESEAWCS_ITaskInfoService;
using WIDESEAWCS_QuartzJob;
using WIDESEAWCS_QuartzJob.Service;
using Microsoft.Extensions.Logging;
namespace WIDESEAWCS_Tasks
{
@@ -18,13 +19,15 @@
        private readonly ITaskExecuteDetailService _taskExecuteDetailService;
        private readonly IRouterService _routerService;
        private readonly IMapper _mapper;
        private readonly ILogger<ShuttleCarJob> _logger;
        public ShuttleCarJob(ITaskService taskService, ITaskExecuteDetailService taskExecuteDetailService, IRouterService routerService, IMapper mapper)
        public ShuttleCarJob(ITaskService taskService, ITaskExecuteDetailService taskExecuteDetailService, IRouterService routerService, IMapper mapper, ILogger<ShuttleCarJob> logger)
        {
            _taskService = taskService;
            _taskExecuteDetailService = taskExecuteDetailService;
            _routerService = routerService;
            _mapper = mapper;
            _logger = logger;
        }
        public Task Execute(IJobExecutionContext context)
@@ -35,7 +38,7 @@
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "ShuttleCarJob æ‰§è¡Œå¤±è´¥");
            }
            finally
            {
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/SocketServer/TcpSocketServer.Server.cs
@@ -3,8 +3,10 @@
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using WIDESEAWCS_Core.Helper;
namespace WIDESEAWCS_Tasks.SocketServer
{
@@ -87,6 +89,7 @@
                try
                {
                    client = await _listener!.AcceptTcpClientAsync().WaitAsync(cancellationToken);
                    ConsoleHelper.WriteSuccessLine($"客户端上线:{client.Client.RemoteEndPoint.ToString()}");
                }
                catch (OperationCanceledException)
                {
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tests/CommunicationConstTests.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,84 @@
using Xunit;
using WIDESEAWCS_Core.Const;
namespace WIDESEAWCS_Tests
{
    /// <summary>
    /// é€šè®¯å¸¸é‡ç±»å•元测试
    /// </summary>
    public class CommunicationConstTests
    {
        [Fact]
        public void WaitIntervalMs_ShouldBe500()
        {
            // Arrange & Act & Assert
            Assert.Equal(500, CommunicationConst.WaitIntervalMs);
        }
        [Fact]
        public void WaitTimeoutBaseMs_ShouldBe6000()
        {
            // Arrange & Act & Assert
            Assert.Equal(6000, CommunicationConst.WaitTimeoutBaseMs);
        }
        [Fact]
        public void WaitTimeoutMultiplier_ShouldBe10()
        {
            // Arrange & Act & Assert
            Assert.Equal(10, CommunicationConst.WaitTimeoutMultiplier);
        }
        [Fact]
        public void WaitTotalTimeoutMs_ShouldBe60000()
        {
            // Arrange & Act & Assert
            Assert.Equal(60000, CommunicationConst.WaitTotalTimeoutMs);
        }
        [Fact]
        public void WaitTotalTimeoutMs_ShouldEqualMultiplierTimesBase()
        {
            // Arrange & Act & Assert
            Assert.Equal(
                CommunicationConst.WaitTimeoutMultiplier * CommunicationConst.WaitTimeoutBaseMs,
                CommunicationConst.WaitTotalTimeoutMs
            );
        }
        [Fact]
        public void PingIntervalMs_ShouldBe100()
        {
            // Arrange & Act & Assert
            Assert.Equal(100, CommunicationConst.PingIntervalMs);
        }
        [Fact]
        public void LogWriteIntervalMs_ShouldBe5000()
        {
            // Arrange & Act & Assert
            Assert.Equal(5000, CommunicationConst.LogWriteIntervalMs);
        }
        [Fact]
        public void HttpDefaultTimeoutSeconds_ShouldBe60()
        {
            // Arrange & Act & Assert
            Assert.Equal(60, CommunicationConst.HttpDefaultTimeoutSeconds);
        }
        [Fact]
        public void HttpConnectTimeoutSeconds_ShouldBe30()
        {
            // Arrange & Act & Assert
            Assert.Equal(30, CommunicationConst.HttpConnectTimeoutSeconds);
        }
        [Fact]
        public void DbConnectTimeoutSeconds_ShouldBe30()
        {
            // Arrange & Act & Assert
            Assert.Equal(30, CommunicationConst.DbConnectTimeoutSeconds);
        }
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tests/UnitTest1.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,10 @@
namespace WIDESEAWCS_Tests;
public class UnitTest1
{
    [Fact]
    public void Test1()
    {
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tests/WIDESEAWCS_Tests.csproj
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <IsPackable>false</IsPackable>
    <NoWarn>$(NoWarn);NU1605</NoWarn>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="coverlet.collector" Version="6.0.4" />
    <PackageReference Include="FluentAssertions" Version="8.8.0" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
    <PackageReference Include="Moq" Version="4.20.72" />
    <PackageReference Include="xunit" Version="2.9.3" />
    <PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
  </ItemGroup>
  <ItemGroup>
    <Using Include="Xunit" />
  </ItemGroup>
  <ItemGroup>
    <ProjectReference Include="..\WIDESEAWCS_Core\WIDESEAWCS_Core.csproj" />
    <ProjectReference Include="..\WIDESEAWCS_TaskInfoService\WIDESEAWCS_TaskInfoService.csproj" />
  </ItemGroup>
</Project>
Code/WMS/WIDESEA_WMSServer/WIDESEA_IStockService/IStockService.cs
@@ -19,11 +19,11 @@
        IStockInfo_HtyService StockInfo_HtyService { get; }
        Task<bool> GroupPalletAsync(StockDTO stock);
        Task<WebResponseContent> GroupPalletAsync(StockDTO stock);
        Task<bool> ChangePalletAsync(StockDTO stock);
        Task<WebResponseContent> ChangePalletAsync(StockDTO stock);
        Task<bool> SplitPalletAsync(StockDTO stock);
        Task<WebResponseContent> SplitPalletAsync(StockDTO stock);
        /// <summary>
        /// å †åž›æœºæ¢ç›˜åŽæ›´æ–°åº“存信息(清空库位信息)
Code/WMS/WIDESEA_WMSServer/WIDESEA_StockService/StockSerivce.cs
@@ -1,7 +1,4 @@
using Autofac.Core;
using System.Net;
using System.Threading.Channels;
using WIDESEA_Common.StockEnum;
using WIDESEA_Common.StockEnum;
using WIDESEA_Core;
using WIDESEA_DTO.Stock;
using WIDESEA_IStockService;
@@ -19,8 +16,6 @@
        public IStockInfoDetail_HtyService StockInfoDetail_HtyService { get; }
        public IStockInfo_HtyService StockInfo_HtyService { get; }
        public StockSerivce(
            IStockInfoDetailService stockInfoDetailService,
            IStockInfoService stockInfoService,
@@ -36,8 +31,9 @@
        /// <summary>
        /// ç»„盘
        /// </summary>
        public async Task<bool> GroupPalletAsync(StockDTO stock)
        public async Task<WebResponseContent> GroupPalletAsync(StockDTO stock)
        {
            WebResponseContent content = new WebResponseContent();
            var now = DateTime.Now;
            var details = stock.Details.Select(item => new Dt_StockInfoDetail
            {
@@ -55,10 +51,13 @@
            }).ToList();
            var existingStock = StockInfoService.Repository.QueryFirst(s => s.PalletCode == stock.TargetPalletNo);
            var result = false;
            if (existingStock != null)
            {
                details.ForEach(d => d.StockId = existingStock.Id);
                return await StockInfoDetailService.Repository.AddDataAsync(details) > 0;
                result = await StockInfoDetailService.Repository.AddDataAsync(details) > 0;
                if (result) return content.OK("组盘成功");
                return content.Error("组盘失败");
            }
            var entity = new Dt_StockInfo
@@ -70,24 +69,27 @@
                Details = details
            };
            return StockInfoService.Repository.AddData(entity, x => x.Details);
            result = StockInfoService.Repository.AddData(entity, x => x.Details);
            if (result) return content.OK("组盘成功");
            return content.Error("组盘失败");
        }
        /// <summary>
        /// æ¢ç›˜
        /// </summary>
        public async Task<bool> ChangePalletAsync(StockDTO stock)
        public async Task<WebResponseContent> ChangePalletAsync(StockDTO stock)
        {
            WebResponseContent content = new WebResponseContent();
            if (stock == null ||
                string.IsNullOrWhiteSpace(stock.TargetPalletNo) ||
                string.IsNullOrWhiteSpace(stock.SourcePalletNo) ||
                string.Equals(stock.SourcePalletNo, stock.TargetPalletNo, StringComparison.OrdinalIgnoreCase))
            {
                return false;
                return content.Error("源托盘号与目标托盘号相同");
            }
            var sourceStock = StockInfoService.Repository.QueryFirst(s => s.PalletCode == stock.SourcePalletNo);
            if (sourceStock == null) return false;
            if (sourceStock == null) return content.Error("源托盘不存在");
            var targetStock = StockInfoService.Repository.QueryFirst(s => s.PalletCode == stock.TargetPalletNo);
            if (targetStock == null)
@@ -101,39 +103,42 @@
                };
                var newId = StockInfoService.Repository.AddData(newStock);
                if (newId <= 0) return false;
                if (newId <= 0) return content.Error("换盘失败");
                targetStock = newStock;
                targetStock.Id = newId;
            }
            var serialNumbers = stock.Details.Select(d => d.CellBarcode).Distinct().ToList();
            if (!serialNumbers.Any()) return false;
            if (!serialNumbers.Any()) return content.Error("未找到有效的序列号");
            var detailEntities = StockInfoDetailService.Repository.QueryData(
                d => d.StockId == sourceStock.Id && serialNumbers.Contains(d.SerialNumber));
            if (!detailEntities.Any()) return false;
            if (!detailEntities.Any()) return content.Error("未找到有效的库存明细");
            if (await StockInfoDetail_HtyService.Repository.AddDataAsync(CreateDetailHistory(detailEntities, "换盘")) <= 0)
                return false;
                return content.Error("换盘历史记录保存失败");
            if (await StockInfo_HtyService.Repository.AddDataAsync(CreateStockHistory(new[] { sourceStock, targetStock }, "换盘")) <= 0)
                return false;
                return content.Error("换盘历史记录保存失败");
            detailEntities.ForEach(d => d.StockId = targetStock.Id);
            return await StockInfoDetailService.Repository.UpdateDataAsync(detailEntities);
            var result = await StockInfoDetailService.Repository.UpdateDataAsync(detailEntities);
            if (!result) return content.Error("换盘失败");
            return content.OK("换盘成功");
        }
        /// <summary>
        /// æ‹†ç›˜
        /// </summary>
        public async Task<bool> SplitPalletAsync(StockDTO stock)
        public async Task<WebResponseContent> SplitPalletAsync(StockDTO stock)
        {
            WebResponseContent content = new WebResponseContent();
            if (stock == null || string.IsNullOrWhiteSpace(stock.SourcePalletNo))
                return false;
                return content.Error("源托盘号不能为空");
            var sourceStock = StockInfoService.Repository.QueryFirst(s => s.PalletCode == stock.SourcePalletNo);
            if (sourceStock == null) return false;
            if (sourceStock == null) return content.Error("源托盘不存在");
            var serialNumbers = stock.Details.Select(d => d.CellBarcode).Distinct().ToList();
            if (!serialNumbers.Any())
@@ -146,15 +151,17 @@
            var detailEntities = StockInfoDetailService.Repository.QueryData(
                d => d.StockId == sourceStock.Id && serialNumbers.Contains(d.SerialNumber));
            if (!detailEntities.Any()) return false;
            if (!detailEntities.Any()) return content.Error("未找到有效的库存明细");
            if (await StockInfoDetail_HtyService.Repository.AddDataAsync(CreateDetailHistory(detailEntities, "拆盘")) <= 0)
                return false;
                return content.Error("拆盘历史记录保存失败");
            if (await StockInfo_HtyService.Repository.AddDataAsync(CreateStockHistory(new[] { sourceStock }, "拆盘")) <= 0)
                return false;
                return content.Error("拆盘历史记录保存失败");
            return await StockInfoDetailService.Repository.DeleteDataAsync(detailEntities);
            var result = await StockInfoDetailService.Repository.DeleteDataAsync(detailEntities);
            if (!result) return content.Error("拆盘失败");
            return content.OK("拆盘成功");
        }
        /// <summary>
@@ -227,6 +234,5 @@
                ModifyDate = s.ModifyDate
            }).ToList();
        }
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Stock/StockController.cs
@@ -26,7 +26,7 @@
        /// <param name="stock"></param>
        /// <returns></returns>
        [HttpGet,HttpPost,Route("GroupPalletAsync"), AllowAnonymous]
        public async Task<bool> GroupPallet([FromBody]StockDTO stock)
        public async Task<WebResponseContent> GroupPallet([FromBody]StockDTO stock)
        {
            return await Service.GroupPalletAsync(stock);
        }
@@ -37,7 +37,7 @@
        /// <param name="stock"></param>
        /// <returns></returns>
        [HttpGet, HttpPost, Route("ChangePalletAsync"),AllowAnonymous]
        public async Task<bool> ChangePalletAsync([FromBody] StockDTO stock)
        public async Task<WebResponseContent> ChangePalletAsync([FromBody] StockDTO stock)
        {
            return await Service.ChangePalletAsync(stock);
        }
@@ -48,7 +48,7 @@
        /// <param name="stock"></param>
        /// <returns></returns>
        [HttpGet, HttpPost, Route("SplitPalletAsync"), AllowAnonymous]
        public async Task<bool> SplitPalletAsync([FromBody] StockDTO stock)
        public async Task<WebResponseContent> SplitPalletAsync([FromBody] StockDTO stock)
        {
            return await Service.SplitPalletAsync(stock);
        }
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/TaskInfo/TaskController.cs
@@ -90,6 +90,17 @@
        }
        /// <summary>
        /// åˆ›å»ºç©ºæ‰˜ç›˜å‡ºåº“任务
        /// </summary>
        /// <param name="taskDto"></param>
        /// <returns></returns>
        [HttpGet, HttpPost, Route("CreateTaskInboundTray"), AllowAnonymous]
        public async Task<WebResponseContent?> CreateTaskInboundTrayAsync([FromBody] CreateTaskDto taskDto)
        {
            return await Service.CreateTaskInboundTrayAsync(taskDto);
        }
        /// <summary>
        /// å †åž›æœºå–放货完成后物流通知化成分容柜完成信号
        /// </summary>
        /// <param name="input"></param>