using FreeSql; using LiveForum.Code.Redis.Contract; using LiveForum.IService.Flowers; using LiveForum.Model; using Microsoft.Extensions.Logging; using StackExchange.Redis; using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Threading.Tasks; namespace LiveForum.Service.Flowers { /// /// 送花服务实现 /// public class FlowerService : IFlowerService { private readonly IRedisService _redisService; private readonly IBaseRepository _postsRepository; private readonly IBaseRepository _streamersRepository; private readonly ILogger _logger; // Redis键命名规则常量 private const string FLOWER_LIMIT_KEY_PREFIX = "flower:limit:"; private const string FLOWER_COUNT_KEY_PREFIX = "flower_count:"; private const string FLOWER_PENDING_OPERATIONS_KEY = "flower:pending:operations"; // 缓存过期时间 private static readonly TimeSpan FLOWER_LIMIT_TTL = TimeSpan.FromHours(1); // 1小时限制 private static readonly TimeSpan FLOWER_COUNT_CACHE_EXPIRATION = TimeSpan.FromHours(24); // 花数缓存24小时 public FlowerService( IRedisService redisService, IBaseRepository postsRepository, IBaseRepository streamersRepository, ILogger logger) { _redisService = redisService; _postsRepository = postsRepository; _streamersRepository = streamersRepository; _logger = logger; } /// /// 获取1小时限制键 /// private string GetFlowerLimitKey(long userId, string targetType, long targetId) { return $"{FLOWER_LIMIT_KEY_PREFIX}{userId}:{targetType}:{targetId}"; } /// /// 获取花数计数键 /// private string GetFlowerCountKey(string targetType, long targetId) { return $"{FLOWER_COUNT_KEY_PREFIX}{targetType}:{targetId}"; } /// /// 获取待处理操作Hash字段名 /// private string GetPendingOperationField(long userId, string targetType, long targetId) { return $"{userId}:{targetType}:{targetId}"; } public async Task CanSendFlowerAsync(long userId, string targetType, long targetId) { try { // 1. 优先从Redis检查1小时限制 var limitKey = GetFlowerLimitKey(userId, targetType, targetId); var exists = await _redisService.ExistsAsync(limitKey); if (exists) { // Redis中存在限制标记,说明1小时内已送过花 return false; } // 2. Redis中没有,查询数据库(降级策略) // 注意:由于送花频率较低,这里可以只查Redis,如果Redis没有就认为可以送花 // 数据库检查会在批量同步时处理重复数据 return true; } catch (Exception ex) { // Redis异常时,允许送花(降级策略,数据库层面会处理重复) _logger.LogWarning(ex, "Redis检查送花限制失败,降级允许送花。UserId: {UserId}, TargetType: {TargetType}, TargetId: {TargetId}", userId, targetType, targetId); return true; } } public async Task GetFlowerCountAsync(string targetType, long targetId) { try { // 1. 优先从Redis查询 var countKey = GetFlowerCountKey(targetType, targetId); var count = await _redisService.StringGetInt64Async(countKey); if (count > 0) { return (int)count; } // 2. Redis中没有或为0,查询数据库 int dbCount = 0; if (targetType == "Post") { var post = await _postsRepository.Select .Where(x => x.Id == targetId) .FirstAsync(); if (post != null) { dbCount = post.FlowerCount; } } else if (targetType == "Streamer") { var streamer = await _streamersRepository.Select .Where(x => x.Id == targetId) .FirstAsync(); if (streamer != null) { dbCount = streamer.FlowerCount; } } // 3. 将结果写入Redis缓存(如果计数大于0) if (dbCount > 0) { await _redisService.StringSetAsync(countKey, dbCount, FLOWER_COUNT_CACHE_EXPIRATION); } return dbCount; } catch (Exception ex) { // Redis异常时降级到数据库查询 _logger.LogWarning(ex, "Redis查询花数失败,降级到数据库查询。TargetType: {TargetType}, TargetId: {TargetId}", targetType, targetId); try { if (targetType == "Post") { var post = await _postsRepository.Select .Where(x => x.Id == targetId) .FirstAsync(); return post?.FlowerCount ?? 0; } else if (targetType == "Streamer") { var streamer = await _streamersRepository.Select .Where(x => x.Id == targetId) .FirstAsync(); return streamer?.FlowerCount ?? 0; } return 0; } catch (Exception dbEx) { _logger.LogError(dbEx, "数据库查询花数失败。TargetType: {TargetType}, TargetId: {TargetId}", targetType, targetId); throw; } } } public async Task> BatchGetFlowerCountAsync(string targetType, List targetIds) { if (targetIds == null || !targetIds.Any()) { return new Dictionary(); } var result = new Dictionary(); var needQueryFromDb = new List(); try { // 1. 批量从Redis查询 foreach (var targetId in targetIds) { var countKey = GetFlowerCountKey(targetType, targetId); var count = await _redisService.StringGetInt64Async(countKey); result[targetId] = (int)count; if (count == 0) { needQueryFromDb.Add(targetId); } } // 2. 批量查询数据库中缺失的 if (needQueryFromDb.Any()) { Dictionary dbCounts = new Dictionary(); if (targetType == "Post") { var posts = await _postsRepository.Select .Where(x => needQueryFromDb.Contains(x.Id)) .ToListAsync(); dbCounts = posts.ToDictionary(x => x.Id, x => x.FlowerCount); } else if (targetType == "Streamer") { var streamers = await _streamersRepository.Select .Where(x => needQueryFromDb.Contains(x.Id)) .ToListAsync(); dbCounts = streamers.ToDictionary(x => (long)x.Id, x => x.FlowerCount); } // 3. 更新结果并写入Redis缓存 foreach (var targetId in needQueryFromDb) { var count = dbCounts.ContainsKey(targetId) ? dbCounts[targetId] : 0; result[targetId] = count; if (count > 0) { var countKey = GetFlowerCountKey(targetType, targetId); await _redisService.StringSetAsync(countKey, count, FLOWER_COUNT_CACHE_EXPIRATION); } } } return result; } catch (Exception ex) { // Redis异常时降级到数据库查询 _logger.LogWarning(ex, "Redis批量查询花数失败,降级到数据库查询。TargetType: {TargetType}", targetType); try { Dictionary dbCounts = new Dictionary(); if (targetType == "Post") { var posts = await _postsRepository.Select .Where(x => targetIds.Contains(x.Id)) .ToListAsync(); dbCounts = posts.ToDictionary(x => x.Id, x => x.FlowerCount); } else if (targetType == "Streamer") { var streamers = await _streamersRepository.Select .Where(x => targetIds.Contains(x.Id)) .ToListAsync(); dbCounts = streamers.ToDictionary(x => (long)x.Id, x => x.FlowerCount); } foreach (var targetId in targetIds) { result[targetId] = dbCounts.ContainsKey(targetId) ? dbCounts[targetId] : 0; } return result; } catch (Exception dbEx) { _logger.LogError(dbEx, "数据库批量查询花数失败。TargetType: {TargetType}", targetType); throw; } } } public async Task SendFlowerAsync(long userId, string targetType, long targetId, long receiverId) { try { var now = DateTime.Now; var yearMonth = now.ToString("yyyy-MM"); // 1. 设置1小时限制标记(Redis SETEX,TTL 1小时) var limitKey = GetFlowerLimitKey(userId, targetType, targetId); await _redisService.SetAsync(limitKey, "1", FLOWER_LIMIT_TTL); // 2. 更新Redis计数(立即) var countKey = GetFlowerCountKey(targetType, targetId); var newCount = await _redisService.IncrementAsync(countKey); // 3. 写入待处理操作Hash(用于定时批量同步到数据库) var pendingField = GetPendingOperationField(userId, targetType, targetId); var operation = new { SendMonth = yearMonth, OccurredAt = now, ReceiverId = receiverId }; var operationJson = JsonSerializer.Serialize(operation); await _redisService.HashSetAsync(FLOWER_PENDING_OPERATIONS_KEY, pendingField, operationJson); _logger.LogInformation( "[FlowerService] 送花操作成功。UserId: {UserId}, TargetType: {TargetType}, TargetId: {TargetId}, NewCount: {NewCount}", userId, targetType, targetId, newCount); return true; } catch (Exception ex) { _logger.LogError(ex, "送花操作失败。UserId: {UserId}, TargetType: {TargetType}, TargetId: {TargetId}", userId, targetType, targetId); throw; } } } }