using Fengling.RiskControl.Domain.Aggregates.RiskRules; using Fengling.RiskControl.Configuration; using Microsoft.Extensions.Logging; using StackExchange.Redis; namespace Fengling.RiskControl.Counter; public interface IRiskCounterService { Task IncrementAsync(string memberId, string metric, int value = 1); Task GetValueAsync(string memberId, string metric); Task> GetAllValuesAsync(string memberId); Task SetValueAsync(string memberId, string metric, int value); Task RefreshTtlAsync(string memberId); Task ExistsAsync(string memberId); } public class RedisCounterService : IRiskCounterService { private readonly IConnectionMultiplexer _redis; private readonly RiskControlClientOptions _options; private readonly ILogger _logger; private string MemberKey(string memberId) => $"{_options.Redis.KeyPrefix}{memberId}"; public RedisCounterService( IConnectionMultiplexer redis, RiskControlClientOptions options, ILogger logger) { _redis = redis; _options = options; _logger = logger; } public async Task IncrementAsync(string memberId, string metric, int value = 1) { var db = _redis.GetDatabase(); var key = MemberKey(memberId); try { var result = await db.HashIncrementAsync(key, metric, value); await db.KeyExpireAsync(key, TimeSpan.FromSeconds(_options.Redis.DefaultTtlSeconds)); return result; } catch (RedisException ex) { _logger.LogError(ex, "Redis increment failed for member {MemberId}, metric {Metric}", memberId, metric); throw; } } public async Task GetValueAsync(string memberId, string metric) { var db = _redis.GetDatabase(); var key = MemberKey(memberId); try { var value = await db.HashGetAsync(key, metric); return value.HasValue ? int.Parse(value) : 0; } catch (RedisException ex) { _logger.LogError(ex, "Redis get failed for member {MemberId}, metric {Metric}", memberId, metric); throw; } } public async Task> GetAllValuesAsync(string memberId) { var db = _redis.GetDatabase(); var key = MemberKey(memberId); try { var entries = await db.HashGetAllAsync(key); return entries .Where(e => e.Name != "_ttl") .ToDictionary( e => e.Name.ToString(), e => int.Parse(e.Value) ); } catch (RedisException ex) { _logger.LogError(ex, "Redis get all failed for member {MemberId}", memberId); throw; } } public async Task SetValueAsync(string memberId, string metric, int value) { var db = _redis.GetDatabase(); var key = MemberKey(memberId); try { await db.HashSetAsync(key, metric, value); await db.KeyExpireAsync(key, TimeSpan.FromSeconds(_options.Redis.DefaultTtlSeconds)); return true; } catch (RedisException ex) { _logger.LogError(ex, "Redis set failed for member {MemberId}, metric {Metric}", memberId, metric); throw; } } public async Task RefreshTtlAsync(string memberId) { var db = _redis.GetDatabase(); var key = MemberKey(memberId); try { await db.KeyExpireAsync(key, TimeSpan.FromSeconds(_options.Redis.DefaultTtlSeconds)); } catch (RedisException ex) { _logger.LogError(ex, "Redis TTL refresh failed for member {MemberId}", memberId); throw; } } public async Task ExistsAsync(string memberId) { var db = _redis.GetDatabase(); var key = MemberKey(memberId); try { return await db.KeyExistsAsync(key); } catch (RedisException ex) { _logger.LogError(ex, "Redis exists check failed for member {MemberId}", memberId); throw; } } }