增强Redis缓存服务与设备通信优化
- 新增缓存删除扩展方法(RemoveByPrefix/RemoveAndGet)
- 支持L1/L2缓存独立开关配置
- 新增BaseAPI枚举与HTTP常量定义
- 优化Omron PLC与串口通信器
- 重构机器人、输送线、穿梭车任务逻辑
- 完善WMS库存与任务控制器
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | # 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 |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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/"; |
| | | } |
| | | } |
| | |
| | | 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> |
| | |
| | | /// <summary> |
| | | /// è·åä»»å¡å¯å
¥è´§ä½ |
| | | /// </summary> |
| | | GetTasksLocation |
| | | GetTasksLocation, |
| | | |
| | | #endregion |
| | | /// <summary> |
| | | /// åºåºä»»å¡å®æ |
| | | /// </summary> |
| | | OutboundFinishTaskAsync, |
| | | |
| | | /// <summary> |
| | | /// å
¥åºä»»å¡å®æ |
| | | /// </summary> |
| | | InboundFinishTaskAsync, |
| | | |
| | | /// <summary> |
| | | /// å建空æçåºåºä»»å¡ |
| | | /// </summary> |
| | | GetOutBoundTrayTaskAsync, |
| | | |
| | | /// <summary> |
| | | /// å建空æçå
¥åºä»»å¡ |
| | | /// </summary> |
| | | CreateTaskInboundTrayAsync, |
| | | |
| | | #endregion WMSæ¥å£ |
| | | } |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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"; |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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"; |
| | | } |
| | | } |
| | |
| | | [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åè®®çé讯类 |
| | |
| | | |
| | | private void Ping() |
| | | { |
| | | Task.Run(() => |
| | | Task.Run(async () => |
| | | { |
| | | while (_isPing) |
| | | { |
| | |
| | | } |
| | | finally |
| | | { |
| | | Thread.Sleep(100); |
| | | await Task.Delay(PingIntervalMs); |
| | | } |
| | | } |
| | | }); |
| | |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | |
| | | LogNet.WriteException(Name, $"䏲壿°æ®æ¥æ¶å¼å¸¸ï¼ä¸²å£å·:{_serialPortName}", ex); |
| | | } |
| | | } |
| | | |
| | |
| | | using System.Linq; |
| | | using System.Reflection; |
| | | using System.Text; |
| | | using System.Threading; |
| | | using System.Threading.Tasks; |
| | | using WIDESEAWCS_Core.Helper; |
| | | |
| | |
| | | lock (this) |
| | | { |
| | | string result = ""; |
| | | var spinner = new SpinWait(); |
| | | while (!TranStack.IsEmpty && !TranStack.TryPeek(out result)) |
| | | { |
| | | Thread.Sleep(1); |
| | | spinner.SpinOnce(); |
| | | } |
| | | |
| | | |
| | |
| | | } |
| | | finally |
| | | { |
| | | var spinner2 = new SpinWait(); |
| | | while (!TranStack.TryPop(out _)) |
| | | { |
| | | Thread.Sleep(1); |
| | | spinner2.SpinOnce(); |
| | | } |
| | | |
| | | _tranCount = TranStack.Count; |
| | |
| | | lock (this) |
| | | { |
| | | string result = ""; |
| | | var spinner = new SpinWait(); |
| | | while (!TranStack.IsEmpty && !TranStack.TryPeek(out result)) |
| | | { |
| | | Thread.Sleep(1); |
| | | spinner.SpinOnce(); |
| | | } |
| | | |
| | | if (result == method.GetFullName()) |
| | |
| | | /// <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> |
| | |
| | | /// <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飿 ¼æ¹æ³ |
| | | |
| | |
| | | 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); |
| | |
| | | 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; |
| | |
| | | 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(); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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; |
| | | } |
| | | } |
| | |
| | | return Configuration[string.Join(":", sections)]; |
| | | } |
| | | } |
| | | catch (Exception) { } |
| | | catch (Exception ex) |
| | | { |
| | | Console.WriteLine($"AppSettings读åé
置失败: {ex.Message}"); |
| | | } |
| | | |
| | | return ""; |
| | | } |
| | |
| | | { |
| | | return Configuration[sectionsPath]; |
| | | } |
| | | catch (Exception) { } |
| | | catch (Exception ex) |
| | | { |
| | | Console.WriteLine($"AppSettings读åé
置失败: {ex.Message}"); |
| | | } |
| | | |
| | | return ""; |
| | | |
| | |
| | | using System.Security.Policy; |
| | | using System.Text; |
| | | using System.Threading.Tasks; |
| | | using WIDESEAWCS_Core.Const; |
| | | using WIDESEAWCS_Core.LogHelper; |
| | | |
| | | namespace WIDESEAWCS_Core.Helper |
| | |
| | | /// æ¹æ³ä¼èªå¨è®°å½è¯·æ±æ¥å¿ï¼å
å«è¯·æ±å°åãååºå
容åèæ¶çä¿¡æ¯ |
| | | /// é»è®¤è¯·æ±è¶
æ¶æ¶é´ä¸º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; |
| | |
| | | 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) |
| | |
| | | /// <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; |
| | |
| | | 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; |
| | | } |
| | |
| | | /// /// <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; |
| | |
| | | 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) |
| | |
| | | /// <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; |
| | |
| | | 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 |
| | | { |
| | |
| | | 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 |
| | |
| | | { |
| | | _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> |
| | |
| | | /// <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 () => |
| | | { |
| | | 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; |
| | | 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}"); |
| | | }).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 () => |
| | | { |
| | | var request = new HttpRequestMessage(HttpMethod.Get, url); |
| | | SetRequestHeaders(request, config?.Headers); |
| | | return await client.SendAsync(request); |
| | | }, config, $"GET {url}").Result; |
| | | return await ExecuteAsync(async (client) => |
| | | { |
| | | var request = new HttpRequestMessage(HttpMethod.Get, url); |
| | | SetRequestHeaders(request, config?.Headers); |
| | | return await client.SendAsync(request); |
| | | }, config, $"GET {url}"); |
| | | }).GetAwaiter().GetResult(); |
| | | |
| | | httpResponseResult.ApiUrl = url; |
| | | return httpResponseResult; |
| | |
| | | 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> |
| | |
| | | |
| | | 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> |
| | |
| | | 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; |
| | |
| | | /// 2. å½ç¼åè¡¨ææ°æ®æ¶ï¼ä½¿ç¨SqlSugarè¿è¡æ¹éæå
¥ |
| | | /// 3. åçå¼å¸¸æ¶ä¼è¾åºé误信æ¯ä½ä¸ä¼ç»æ¢çº¿ç¨ |
| | | /// </remarks> |
| | | static void StartWriteLog() |
| | | static async void StartWriteLog() |
| | | { |
| | | DataTable queueTable = CreateEmptyTable(); |
| | | while (true) |
| | |
| | | DequeueToTable(queueTable); continue; |
| | | } |
| | | //æ¯5ç§å䏿¬¡æ°æ® |
| | | Thread.Sleep(5000); |
| | | await Task.Delay(CommunicationConst.LogWriteIntervalMs); |
| | | |
| | | //妿éå表ä¸çè¡æ°ä¸º0ï¼åè·³è¿æ¬æ¬¡å¾ªç¯ |
| | | if (queueTable.Rows.Count == 0) { continue; } |
| | |
| | | <Folder Include="ServiceExtensions\" /> |
| | | </ItemGroup> |
| | | |
| | | <ItemGroup> |
| | | <ProjectReference Include="..\WIDESEAWCS_Common\WIDESEAWCS_Common.csproj" /> |
| | | </ItemGroup> |
| | | |
| | | </Project> |
| | |
| | | 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; |
| | |
| | | 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> |
| | |
| | | { |
| | | List<DispatchInfoDTO> dispatches = _dispatchInfoService.QueryDispatchInfos(); |
| | | List<DeviceInfoDTO> deviceInfos = await _deviceInfoService.QueryDeviceProInfos(); |
| | | _cacheService.RemoveByPrefix($"{RedisPrefix.System}"); |
| | | |
| | | deviceInfos.ForEach(x => |
| | | { |
| | |
| | | x.Device = (IDevice)deviceInstance; |
| | | |
| | | Storage.Devices.Add((IDevice)deviceInstance); |
| | | |
| | | _cacheService.AddObject($"{RedisPrefix.System}:{RedisName.IDevice}:{x.DeviceName}", (IDevice)deviceInstance); |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | |
| | | |
| | | if (targetDevice is null) continue; |
| | | |
| | | |
| | | // ä½¿ç¨æ¨¡å¼å¹é
|
| | | dispatches[i].JobParams = targetDevice switch |
| | | { |
| | |
| | | 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; |
| | |
| | | }; |
| | | //StdSchedulerFactory factory = new StdSchedulerFactory(collection); |
| | | StdSchedulerFactory factory = new StdSchedulerFactory(); |
| | | return _scheduler = factory.GetScheduler().Result; |
| | | return _scheduler = await factory.GetScheduler(); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | |
| | | }; |
| | | //StdSchedulerFactory factory = new StdSchedulerFactory(collection); |
| | | StdSchedulerFactory factory = new StdSchedulerFactory(); |
| | | _scheduler = factory.GetScheduler().Result; |
| | | _scheduler = await factory.GetScheduler(); |
| | | } |
| | | |
| | | this._scheduler.JobFactory = this._iocjobFactory; |
| | |
| | | }; |
| | | //StdSchedulerFactory factory = new StdSchedulerFactory(collection); |
| | | StdSchedulerFactory factory = new StdSchedulerFactory(); |
| | | _scheduler = factory.GetScheduler().Result; |
| | | _scheduler = await factory.GetScheduler(); |
| | | } |
| | | |
| | | |
| | |
| | | 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 |
| | |
| | | { |
| | | // å建ä¸ä¸ªå符串å表ï¼ç¨äºå卿æä½ç½® |
| | | 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 |
| | | { |
| | | // æ¥è¯¢ææè¿å
¥è·¯ç±å¨çä½ç½® |
| | |
| | | { |
| | | |
| | | } |
| | | finally |
| | | { |
| | | _cacheService.TryAdd($"{RedisPrefix.System}:{RedisName.DevicePositions}:{deviceCode}", positions); |
| | | } |
| | | } |
| | | else |
| | | positions = device; |
| | |
| | | [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> |
| | |
| | | |
| | | 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>() |
| | |
| | | </ItemGroup> |
| | | |
| | | <ItemGroup> |
| | | <ProjectReference Include="..\WIDESEAWCS_Common\WIDESEAWCS_Common.csproj" /> |
| | | <ProjectReference Include="..\WIDESEAWCS_Communicator\WIDESEAWCS_Communicator.csproj" /> |
| | | <ProjectReference Include="..\WIDESEAWCS_Core\WIDESEAWCS_Core.csproj" /> |
| | | </ItemGroup> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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; } |
| | | } |
| | | } |
| | |
| | | public bool Add(string key, string value, int expireSeconds = -1, bool isSliding = false) |
| | | { |
| | | var fullKey = BuildKey(key); |
| | | SetMemoryCache(fullKey, value, expireSeconds, isSliding); |
| | | |
| | | // åªæå¯ç¨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; |
| | | } |
| | |
| | | catch (Exception ex) |
| | | { |
| | | _logger.LogWarning(ex, "Redis Add失败, key={Key}", key); |
| | | return _options.FallbackToMemory; |
| | | return _options.FallbackToMemory && _options.EnableL1Cache; |
| | | } |
| | | } |
| | | |
| | |
| | | public bool Remove(string key) |
| | | { |
| | | var fullKey = BuildKey(key); |
| | | _memoryCache.Remove(fullKey); |
| | | if (!RedisAvailable) return true; |
| | | |
| | | // åªæå¯ç¨L1ç¼åæ¶æä»å
åç¼åä¸ç§»é¤ |
| | | if (_options.EnableL1Cache) |
| | | { |
| | | _memoryCache.Remove(fullKey); |
| | | } |
| | | |
| | | if (!RedisAvailable) return _options.EnableL1Cache; |
| | | try |
| | | { |
| | | return _connectionManager.GetDatabase().KeyDelete(fullKey); |
| | |
| | | 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); |
| | | |
| | |
| | | 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); |
| | | |
| | |
| | | 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; |
| | | |
| | |
| | | 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); |
| | |
| | | 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)); |
| | |
| | | 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); |
| | |
| | | using Microsoft.Extensions.Options; |
| | | using StackExchange.Redis; |
| | | using System.Linq; |
| | | using WIDESEAWCS_Core.Helper; |
| | | using WIDESEAWCS_RedisService.Options; |
| | | |
| | | namespace WIDESEAWCS_RedisService.Connection |
| | |
| | | { |
| | | // 强å¶è®¿é®Valueæ¥è§¦åè¿æ¥å建 |
| | | var connected = _connection.Value.IsConnected; |
| | | _logger.LogDebug("IsConnectedæ£æ¥: IsValueCreated={IsValueCreated}, IsConnected={Connected}", |
| | | _connection.IsValueCreated, connected); |
| | | return connected; |
| | | } |
| | | catch (Exception ex) |
| | |
| | | { |
| | | _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) |
| | | { |
| | |
| | | |
| | | 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; |
| | | } |
| | | } |
| | |
| | | services.AddSingleton<ISessionStorage, RedisSessionStorage>(); |
| | | services.AddSingleton<IRedisMonitorService, RedisMonitorService>(); |
| | | |
| | | // åå°æå¡ï¼ä»
å¨å¯ç¨L1ç¼åæ¶æ³¨åï¼ |
| | | if (options.EnableL1Cache) |
| | | { |
| | | services.AddHostedService<CacheSyncBackgroundService>(); |
| | | } |
| | | |
| | | return services; |
| | | } |
| | | } |
| | |
| | | |
| | | 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(); |
| | |
| | |  |
| | | 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 |
| | |
| | | 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 |
| | |
| | | {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 |
| | |
| | | # Redis æå¡ä½¿ç¨æ¡ä¾ |
| | | |
| | | ## 1. ç¼åï¼ICacheServiceï¼ |
| | | ## 1. ç¼åï¼ICacheServiceï¼- åºç¡æä½ |
| | | |
| | | éè¿æé 彿°æ³¨å
¥ `ICacheService`ï¼HybridCacheService èªå¨å®ç° L1(å
å) + L2(Redis) åå±ç¼åã |
| | | |
| | |
| | | } |
| | | ``` |
| | | |
| | | ## 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 |
| | |
| | | } |
| | | ``` |
| | | |
| | | ## 3. 计æ°å¨ï¼ICounterServiceï¼ |
| | | ## 7. 计æ°å¨ï¼ICounterServiceï¼ |
| | | |
| | | ```csharp |
| | | public class StatisticsService |
| | |
| | | } |
| | | ``` |
| | | |
| | | ## 4. åå¸/订é
ï¼IMessageQueueServiceï¼ |
| | | ## 8. åå¸/订é
ï¼IMessageQueueServiceï¼ |
| | | |
| | | ```csharp |
| | | public class NotificationService |
| | |
| | | } |
| | | ``` |
| | | |
| | | ## 5. éæµï¼IRateLimitingServiceï¼ |
| | | ## 9. éæµï¼IRateLimitingServiceï¼ |
| | | |
| | | ```csharp |
| | | public class ApiController |
| | |
| | | } |
| | | ``` |
| | | |
| | | ## 6. åå¸å¼IDçæå¨ï¼IDistributedIdGeneratorï¼ |
| | | ## 10. åå¸å¼IDçæå¨ï¼IDistributedIdGeneratorï¼ |
| | | |
| | | ```csharp |
| | | public class TaskService |
| | |
| | | } |
| | | ``` |
| | | |
| | | ## 7. æè¡æ¦ï¼ILeaderboardServiceï¼ |
| | | ## 11. æè¡æ¦ï¼ILeaderboardServiceï¼ |
| | | |
| | | ```csharp |
| | | public class LeaderboardDemo |
| | |
| | | } |
| | | ``` |
| | | |
| | | ## 8. 对象åå¨ï¼IObjectStorageServiceï¼ |
| | | ## 12. 对象åå¨ï¼IObjectStorageServiceï¼ |
| | | |
| | | ```csharp |
| | | public class DeviceService |
| | |
| | | } |
| | | ``` |
| | | |
| | | ## 9. é
ç½®ä¸å¿ï¼IConfigurationCenterServiceï¼ |
| | | ## 13. é
ç½®ä¸å¿ï¼IConfigurationCenterServiceï¼ |
| | | |
| | | ```csharp |
| | | public class ConfigDemo |
| | |
| | | } |
| | | ``` |
| | | |
| | | ## 10. çæ§ï¼IRedisMonitorServiceï¼ |
| | | ## 14. çæ§ï¼IRedisMonitorServiceï¼ |
| | | |
| | | ```csharp |
| | | public class MonitorDemo |
| | |
| | | } |
| | | ``` |
| | | |
| | | ## 11. Sessionåå¨ï¼ISessionStorageï¼ |
| | | ## 15. Sessionåå¨ï¼ISessionStorageï¼ |
| | | |
| | | ```csharp |
| | | public class SessionDemo |
| | |
| | | } |
| | | ``` |
| | | |
| | | ## 12. å¸éè¿æ»¤å¨ï¼IBloomFilterServiceï¼ |
| | | ## 16. å¸éè¿æ»¤å¨ï¼IBloomFilterServiceï¼ |
| | | |
| | | ```csharp |
| | | public class BloomFilterDemo |
| | |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | ## 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`ï¼ |
| | | ``` |
| | |
| | | "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, //æ
¢æ¥è¯¢éå¼ï¼æ¯«ç§ï¼ |
| | |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | |
| | | Console.WriteLine($"AddTaskExecuteDetail æ·»å 任塿§è¡è¯¦æ
失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | |
| | | content = WebResponseContent.Instance.Error($"è·åä»»å¡è¯¦æ
失败: {ex.Message}"); |
| | | } |
| | | return content; |
| | | } |
| | |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | |
| | | content = WebResponseContent.Instance.Error($"è·åä»»å¡è¯¦æ
失败: {ex.Message}"); |
| | | } |
| | | return content; |
| | | } |
| | |
| | | /// <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); |
| | | } |
| | | |
| | |
| | | /// <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); |
| | | } |
| | | |
| | |
| | | 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) |
| | |
| | | { |
| | | 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}ã"); |
| | |
| | | { |
| | | 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}ã"); |
| | | |
| | |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | Console.WriteLine($"UpdatePosition æ´æ°ä»»å¡ä½ç½®å¤±è´¥,ä»»å¡å·:ã{taskNum}ã,é误信æ¯:ã{ex.Message}ã"); |
| | | } |
| | | return null; |
| | | } |
| | |
| | | |
| | | _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}ã"); |
| | |
| | | _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()); |
| | |
| | | { |
| | | ConveyorLineTaskCommandNew command = conveyorLine.ReadCustomer<ConveyorLineTaskCommandNew>(childDeviceCode); |
| | | |
| | | |
| | | if (command == null) |
| | | { |
| | | return; |
| | | } |
| | | |
| | | #region æ£æ¥ç¹å®ä½ç½®æ¯å¦ææç |
| | | |
| | | var checkPalletPositions = App.Configuration.GetSection("CheckPalletPositions") |
| | |
| | | |
| | | #endregion |
| | | |
| | | if (command == null || command.PLC_STB != 1) |
| | | { |
| | | return; |
| | | } |
| | | if (command.PLC_STB != 1) return;//PLC_STB=1æ¶æå¤çä»»å¡ |
| | | |
| | | if (command.Barcode.IsNullOrEmpty()) |
| | | { |
| | |
| | | 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 |
| | |
| | | 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; |
| | |
| | | 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 |
| | |
| | | |
| | | try |
| | | { |
| | | |
| | | // æ£æ¥æ¯å¦æè¯¥å®¢æ·ç«¯è¿æ¥ |
| | | var clientIds = _TcpSocket.GetClientIds(); |
| | | if (!clientIds.Contains(ipAddress)) |
| | |
| | | Console.WriteLine($"HandleClientAsync error: {t.Exception?.GetBaseException().Message}"); |
| | | }, TaskContinuationOptions.OnlyOnFaulted); |
| | | state.IsEventSubscribed = true; |
| | | |
| | | // æ´æ°ç¼åä¸çç¶æ |
| | | _cache.TryUpdateIfChanged($"{RedisPrefix.Code}:{RedisName.SocketDevices}:{ipAddress}", state); |
| | | } |
| | | } |
| | | |
| | |
| | | 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); |
| | |
| | | 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代表æçï¼ |
| | | string prefix = "TRAY"; |
| | | |
| | | // çæä¸¤ä¸ªæçæ¡ç |
| | | string trayBarcode1 = GenerateTrayBarcode(state, prefix); |
| | | string trayBarcode2 = GenerateTrayBarcode(state, prefix); |
| | | if (!trayBarcode1.IsNullOrEmpty() && !trayBarcode2.IsNullOrEmpty()) |
| | | // ç»ç读åçº¿ä½æ¡ç |
| | | if (task.RobotTaskType == RobotTaskTypeEnum.GroupPallet.GetHashCode()) |
| | | { |
| | | string taskString = $"Pickbattery,{task.RobotSourceAddress}"; |
| | | // åé任塿令 |
| | | bool result = await _TcpSocket.SendToClientAsync(ipAddress, taskString); |
| | | if (result) |
| | | string prefix = "TRAY"; |
| | | |
| | | // çæä¸¤ä¸ªæçæ¡ç |
| | | string trayBarcode1 = GenerateTrayBarcode(state, prefix); |
| | | string trayBarcode2 = GenerateTrayBarcode(state, prefix); |
| | | if (!trayBarcode1.IsNullOrEmpty() && !trayBarcode2.IsNullOrEmpty()) |
| | | { |
| | | // TODO å¤çæååé任塿令åçé»è¾ |
| | | task.RobotTaskState = TaskRobotStatusEnum.RobotExecuting.GetHashCode(); |
| | | result = await _robotTaskService.UpdateRobotTaskAsync(task); |
| | | |
| | | await SendSocketRobotPickAsync(task, state); |
| | | } |
| | | } |
| | | else // æ¢çç´æ¥åéåè´§å°å |
| | | { |
| | | 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); |
| | | } |
| | | } |
| | | } |
| | |
| | | } |
| | | catch (Exception) |
| | | { |
| | | |
| | | } |
| | | finally |
| | | { |
| | | // å¯éï¼å¨è¿éå¤çä»»ä½éè¦å¨ä»»å¡å®æåæ§è¡çæ¸
çå·¥ä½ |
| | | // æ´æ°ç¼åä¸çç¶æ |
| | | _cache.AddOrUpdate(ipAddress, state); |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | // ç»åï¼åç¼ + æ¥æ + æ¶é´ + éæºæ° |
| | | var barCode = prefix + datePart + timePart + randomPart; |
| | | state.CellBarcode.Add(randomPart); |
| | | state.CellBarcode.Add(barCode); |
| | | |
| | | return barCode; |
| | | } |
| | |
| | | /// <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); |
| | | } |
| | | |
| | |
| | | /// <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 |
| | | { |
| | |
| | | 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"; |
| | | } |
| | |
| | | state.CurrentAction = "PickFinished"; |
| | | } |
| | | |
| | | state.LastPickPositions = positions; |
| | | task.RobotTaskState = TaskRobotStatusEnum.RobotPickFinish.GetHashCode(); |
| | | await _latestRobotTaskService.Repository.UpdateDataAsync(task); |
| | | } |
| | |
| | | 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) |
| | |
| | | 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; |
| | | } |
| | |
| | | /// <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) |
| | | { |
| | |
| | | } |
| | | } |
| | | |
| | | private static async Task HandleInboundTaskAsync(RobotSocketState state, bool useSourceAddress) |
| | | private async Task HandleInboundTaskAsync(RobotSocketState state, bool useSourceAddress) |
| | | { |
| | | var currentTask = state.CurrentTask; |
| | | if (currentTask == null) |
| | |
| | | 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; |
| | | } |
| | |
| | | |
| | | 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() |
| | | }; |
| | |
| | | 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); |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | /// <summary> |
| | | /// æåä½ç½®æ¡ç |
| | | /// </summary> |
| | | public List<string> CellBarcode { get; set; } = new(); |
| | | public List<string> CellBarcode { get; set; } |
| | | |
| | | /// <summary> |
| | | /// å½åæåä»»å¡ |
| | |
| | | using WIDESEAWCS_ITaskInfoService; |
| | | using WIDESEAWCS_QuartzJob; |
| | | using WIDESEAWCS_QuartzJob.Service; |
| | | using Microsoft.Extensions.Logging; |
| | | |
| | | namespace WIDESEAWCS_Tasks |
| | | { |
| | |
| | | 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) |
| | | { |
| | | try |
| | | { |
| | | |
| | | |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | |
| | | _logger.LogError(ex, "ShuttleCarJob æ§è¡å¤±è´¥"); |
| | | } |
| | | finally |
| | | { |
| | |
| | | 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 |
| | | { |
| | |
| | | try |
| | | { |
| | | client = await _listener!.AcceptTcpClientAsync().WaitAsync(cancellationToken); |
| | | ConsoleHelper.WriteSuccessLine($"客æ·ç«¯ä¸çº¿:{client.Client.RemoteEndPoint.ToString()}"); |
| | | } |
| | | catch (OperationCanceledException) |
| | | { |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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); |
| | | } |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | namespace WIDESEAWCS_Tests; |
| | | |
| | | public class UnitTest1 |
| | | { |
| | | [Fact] |
| | | public void Test1() |
| | | { |
| | | |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| | |
| | | |
| | | 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> |
| | | /// å åæºæ¢çåæ´æ°åºåä¿¡æ¯ï¼æ¸
空åºä½ä¿¡æ¯ï¼ |
| | |
| | | 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; |
| | |
| | | public IStockInfoDetail_HtyService StockInfoDetail_HtyService { get; } |
| | | public IStockInfo_HtyService StockInfo_HtyService { get; } |
| | | |
| | | |
| | | |
| | | public StockSerivce( |
| | | IStockInfoDetailService stockInfoDetailService, |
| | | IStockInfoService stockInfoService, |
| | |
| | | /// <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 |
| | | { |
| | |
| | | }).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 |
| | |
| | | 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) |
| | |
| | | }; |
| | | |
| | | 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()) |
| | |
| | | |
| | | 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> |
| | |
| | | ModifyDate = s.ModifyDate |
| | | }).ToList(); |
| | | } |
| | | |
| | | } |
| | | } |
| | |
| | | /// <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); |
| | | } |
| | |
| | | /// <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); |
| | | } |
| | |
| | | /// <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); |
| | | } |
| | |
| | | } |
| | | |
| | | /// <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> |