From aefdecd0aa3226b7d00d1dc764241b82658b3be8 Mon Sep 17 00:00:00 2001
From: wanshenmean <cathay_xy@163.com>
Date: 星期五, 06 三月 2026 10:41:02 +0800
Subject: [PATCH] 添加机器人客户端;更新 WCS 缓存及任务
---
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_RedisService/Cache/HybridCacheService.cs | 652 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 648 insertions(+), 4 deletions(-)
diff --git a/Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_RedisService/Cache/HybridCacheService.cs b/Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_RedisService/Cache/HybridCacheService.cs
index 55c3c22..36f0375 100644
--- a/Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_RedisService/Cache/HybridCacheService.cs
+++ b/Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_RedisService/Cache/HybridCacheService.cs
@@ -53,9 +53,20 @@
public bool Add(string key, string value, int expireSeconds = -1, bool isSliding = false)
{
var fullKey = BuildKey(key);
- 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;
}
@@ -69,7 +80,7 @@
catch (Exception ex)
{
_logger.LogWarning(ex, "Redis Add澶辫触, key={Key}", key);
- return _options.FallbackToMemory;
+ return _options.FallbackToMemory && _options.EnableL1Cache;
}
}
@@ -91,8 +102,14 @@
public bool Remove(string key)
{
var fullKey = BuildKey(key);
- _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);
@@ -109,9 +126,434 @@
foreach (var key in keys) Remove(key);
}
+ #region 鍒犻櫎鎵╁睍鏂规硶
+
+ public string? RemoveAndGet(string key)
+ {
+ var value = Get(key);
+ if (value != null) Remove(key);
+ return value;
+ }
+
+ public T? RemoveAndGet<T>(string key) where T : class
+ {
+ var value = Get<T>(key);
+ if (value != null) Remove(key);
+ return value;
+ }
+
+ public int RemoveByPrefix(string prefix)
+ {
+ if (string.IsNullOrEmpty(prefix)) return 0;
+ var fullPrefix = BuildKey(prefix);
+ int count = 0;
+
+ // 鍒犻櫎鍐呭瓨缂撳瓨涓殑鍖归厤椤�
+ // MemoryCache鏃犳硶鏋氫妇锛岃烦杩�
+
+ if (RedisAvailable)
+ {
+ try
+ {
+ var server = _connectionManager.GetServer();
+ var keys = server.Keys(pattern: $"{fullPrefix}*").ToArray();
+ if (keys.Length > 0)
+ {
+ count = (int)_connectionManager.GetDatabase().KeyDelete(keys);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Redis RemoveByPrefix澶辫触, prefix={Prefix}", prefix);
+ }
+ }
+
+ return count;
+ }
+
+ public int RemoveByPattern(string pattern)
+ {
+ if (string.IsNullOrEmpty(pattern)) return 0;
+ int count = 0;
+
+ if (RedisAvailable)
+ {
+ try
+ {
+ var server = _connectionManager.GetServer();
+ var keys = server.Keys(pattern: $"{_options.KeyPrefix}{pattern}").ToArray();
+ if (keys.Length > 0)
+ {
+ count = (int)_connectionManager.GetDatabase().KeyDelete(keys);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Redis RemoveByPattern澶辫触, pattern={Pattern}", pattern);
+ }
+ }
+
+ return count;
+ }
+
+ public int RemoveAll(IEnumerable<string> keys)
+ {
+ if (keys == null) return 0;
+ int count = 0;
+ foreach (var key in keys)
+ {
+ if (Remove(key)) count++;
+ }
+ return count;
+ }
+
+ public int RemoveWhere(Func<string, bool> predicate)
+ {
+ if (predicate == null) return 0;
+ int count = 0;
+
+ if (RedisAvailable)
+ {
+ try
+ {
+ var server = _connectionManager.GetServer();
+ var keys = server.Keys(pattern: $"{_options.KeyPrefix}*").ToArray();
+ var keysToDelete = keys.Where(k =>
+ {
+ var originalKey = k.ToString().Replace(_options.KeyPrefix, "");
+ return predicate(originalKey);
+ }).ToArray();
+
+ if (keysToDelete.Length > 0)
+ {
+ count = (int)_connectionManager.GetDatabase().KeyDelete(keysToDelete);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Redis RemoveWhere澶辫触");
+ }
+ }
+
+ return count;
+ }
+
+ #endregion
+
+ #region 娣诲姞鍜屼慨鏀规墿灞曟柟娉�
+
+ public void AddAll(IDictionary<string, string> items, int expireSeconds = -1)
+ {
+ if (items == null) return;
+ foreach (var item in items)
+ {
+ Add(item.Key, item.Value, expireSeconds);
+ }
+ }
+
+ public void AddAllObjects(IDictionary<string, object> items, int expireSeconds = -1)
+ {
+ if (items == null) return;
+ foreach (var item in items)
+ {
+ AddObject(item.Key, item.Value, expireSeconds);
+ }
+ }
+
+ public bool Replace(string key, string newValue, int expireSeconds = -1)
+ {
+ return TryUpdate(key, newValue, expireSeconds);
+ }
+
+ public bool Replace<T>(string key, T newValue, int expireSeconds = -1) where T : class
+ {
+ if (!Exists(key)) return false;
+ AddObject(key, newValue, expireSeconds);
+ return true;
+ }
+
+ public string? GetAndRefresh(string key, int expireSeconds)
+ {
+ var fullKey = BuildKey(key);
+ string? value = null;
+
+ // 浠嶳edis鑾峰彇鍊�
+ 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;
+
+ // 浠嶳edis鑾峰彇鍊�
+ 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涓嶆敮鎸乀TL鏌ヨ
+ }
+
+ 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;
+
+ // 浠嶳edis鑾峰彇鏃у��
+ 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;
+
+ // 浠嶳edis鑾峰彇鏃у��
+ 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);
+
+ // 濡傛灉绂佺敤浜哃1缂撳瓨锛岀洿鎺ユ煡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;
+ }
+ }
+
+ // 姝e父鐨凩1+L2閫昏緫
if (_memoryCache.TryGetValue(fullKey, out string? cached) && cached != null)
return _serializer.Deserialize<T>(cached);
@@ -131,9 +573,126 @@
}
}
+ /// <summary>
+ /// 瀹夊叏鏇存柊锛氫粎褰撳唴瀛樼紦瀛樹腑鐨勫�间笌expectedVersion鍖归厤鏃舵墠鏇存柊
+ /// 闃叉骞跺彂鍐欏叆鏃舵棫鍊艰鐩栨柊鍊�
+ /// </summary>
+ /// <typeparam name="T">鍊肩被鍨�</typeparam>
+ /// <param name="key">缂撳瓨閿�</param>
+ /// <param name="newValue">鏂板��</param>
+ /// <param name="expectedVersion">鏈熸湜鐨勭増鏈紙閫氬父鏄棫瀵硅薄鐨勫搱甯屽�兼垨鏃堕棿鎴筹級</param>
+ /// <param name="versionExtractor">浠庡璞℃彁鍙栫増鏈彿鐨勫嚱鏁�</param>
+ /// <param name="expireSeconds">杩囨湡鏃堕棿</param>
+ /// <returns>鏄惁鏇存柊鎴愬姛</returns>
+ public bool TrySafeUpdate<T>(
+ string key,
+ T newValue,
+ object? expectedVersion,
+ Func<T, object?> versionExtractor,
+ int expireSeconds = -1) where T : class
+ {
+ var fullKey = BuildKey(key);
+
+ // 浠嶳edis鑾峰彇褰撳墠鍊�
+ string? existingJson = null;
+ T? existingValue = default;
+ if (RedisAvailable)
+ {
+ try
+ {
+ var value = _connectionManager.GetDatabase().StringGet(fullKey);
+ if (!value.IsNullOrEmpty)
+ {
+ existingJson = value.ToString();
+ existingValue = _serializer.Deserialize<T>(existingJson);
+ }
+ }
+ catch { }
+ }
+
+ // 濡傛灉Redis涓嶅彲鐢紝浠庡唴瀛樼紦瀛樿幏鍙�
+ if (existingValue == null && _options.EnableL1Cache)
+ {
+ if (_memoryCache.TryGetValue(fullKey, out string? cached) && cached != null)
+ {
+ existingValue = _serializer.Deserialize<T>(cached);
+ existingJson = cached;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ // 妫�鏌ョ増鏈槸鍚﹀尮閰�
+ if (existingValue != null)
+ {
+ var currentVersion = versionExtractor(existingValue);
+ if (!Equals(currentVersion, expectedVersion))
+ {
+ _logger.LogWarning("TrySafeUpdate鐗堟湰涓嶅尮閰�, key={Key}, expected={Expected}, current={Current}",
+ key, expectedVersion, currentVersion);
+ return false; // 鐗堟湰涓嶅尮閰嶏紝鎷掔粷鏇存柊
+ }
+ }
+
+ // 鐗堟湰鍖归厤锛屾墽琛屾洿鏂�
+ var newJson = _serializer.Serialize(newValue);
+
+ // 鍏堝啓鍏edis
+ if (RedisAvailable)
+ {
+ try
+ {
+ var expiry = expireSeconds > 0 ? TimeSpan.FromSeconds(expireSeconds) : (TimeSpan?)null;
+ if (!_connectionManager.GetDatabase().StringSet(fullKey, newJson, expiry))
+ {
+ _logger.LogWarning("Redis TrySafeUpdate鍐欏叆澶辫触, key={Key}", key);
+ return _options.FallbackToMemory && _options.EnableL1Cache;
+ }
+ else
+ {
+ _logger.LogInformation("Redis TrySafeUpdate鍐欏叆鎴愬姛, key={Key}", key);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Redis TrySafeUpdate鍐欏叆澶辫触, key={Key}", key);
+ return _options.FallbackToMemory && _options.EnableL1Cache;
+ }
+ }
+
+ // 鏇存柊鍐呭瓨缂撳瓨
+ if (_options.EnableL1Cache)
+ {
+ SetMemoryCache(fullKey, newJson, expireSeconds, false);
+ }
+
+ return true;
+ }
+
public object? Get(Type type, string key)
{
var fullKey = BuildKey(key);
+
+ // 濡傛灉绂佺敤浜哃1缂撳瓨锛岀洿鎺ユ煡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;
+ }
+ }
+
+ // 姝e父鐨凩1+L2閫昏緫
if (_memoryCache.TryGetValue(fullKey, out string? cached) && cached != null)
return _serializer.Deserialize(cached, type);
@@ -156,6 +715,24 @@
public string? Get(string key)
{
var fullKey = BuildKey(key);
+
+ // 濡傛灉绂佺敤浜哃1缂撳瓨锛岀洿鎺ユ煡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;
+ }
+ }
+
+ // 姝e父鐨凩1+L2閫昏緫
if (_memoryCache.TryGetValue(fullKey, out string? cached))
return cached;
@@ -229,6 +806,73 @@
return true;
}
+ public bool TryUpdateIfChanged(string key, string newValue, int expireSeconds = -1)
+ {
+ var existing = Get(key);
+ if (existing == null) return false;
+ if (existing == newValue) return false; // 鍊肩浉鍚岋紝涓嶆洿鏂�
+ Add(key, newValue, expireSeconds);
+ return true;
+ }
+
+ public bool TryUpdateIfChanged<T>(string key, T newValue, int expireSeconds = -1) where T : class
+ {
+ var fullKey = BuildKey(key);
+
+ // 鎬绘槸浠嶳edis鑾峰彇褰撳墠瀹為檯鍊艰繘琛屾瘮杈冿紝纭繚鏁版嵁涓�鑷存��
+ 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瀛楃涓茬浉鍚岋紝涓嶆洿鏂�
+
+ // 鍏堝啓鍏edis锛屾垚鍔熷悗鍐嶆洿鏂板唴瀛樼紦瀛�
+ 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);
--
Gitblit v1.9.3