using FreeSql; using LiveForum.Code.Base; using LiveForum.Code.Redis.Contract; using LiveForum.IService.Posts; using LiveForum.Model; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Threading.Tasks; namespace LiveForum.Service.Posts { /// /// 浏览服务实现 /// public class ViewService : IViewService { private readonly IRedisService _redisService; private readonly IBaseRepository _postsRepository; private readonly ILogger _logger; // Redis键命名规则常量 private const string VIEW_LIMIT_KEY_PREFIX = "view:limit:"; private const string VIEW_COUNT_KEY_PREFIX = "view_count:"; private const string VIEW_PENDING_OPERATIONS_KEY = "view:pending:operations"; // 缓存过期时间 private static readonly TimeSpan VIEW_LIMIT_TTL = TimeSpan.FromMinutes(10); // 10分钟限制 private static readonly TimeSpan VIEW_COUNT_CACHE_EXPIRATION = TimeSpan.FromHours(24); // 浏览次数缓存24小时 public ViewService( IRedisService redisService, IBaseRepository postsRepository, ILogger logger) { _redisService = redisService; _postsRepository = postsRepository; _logger = logger; } /// /// 获取10分钟限制键 /// private string GetViewLimitKey(long userId, long postId) { return $"{VIEW_LIMIT_KEY_PREFIX}{userId}:{postId}"; } /// /// 获取浏览次数计数键 /// private string GetViewCountKey(long postId) { return $"{VIEW_COUNT_KEY_PREFIX}{postId}"; } /// /// 获取待处理操作Hash字段名 /// private string GetPendingOperationField(long userId, long postId) { return $"{userId}:{postId}"; } public async Task CanAddViewAsync(long userId, long postId) { try { // 1. 优先从Redis检查10分钟限制 var limitKey = GetViewLimitKey(userId, postId); var exists = await _redisService.ExistsAsync(limitKey); if (exists) { // Redis中存在限制标记,说明10分钟内已浏览过 return false; } // 2. Redis中没有,允许浏览 return true; } catch (Exception ex) { // Redis异常时,允许浏览(降级策略) _logger.LogWarning(ex, "Redis检查浏览限制失败,降级允许浏览。UserId: {UserId}, PostId: {PostId}", userId, postId); return true; } } public async Task GetViewCountAsync(long postId) { try { // 1. 优先从Redis查询 var countKey = GetViewCountKey(postId); var count = await _redisService.StringGetInt64Async(countKey); if (count > 0) { return (int)count; } // 2. Redis中没有或为0,查询数据库 var post = await _postsRepository.Select .Where(x => x.Id == postId) .FirstAsync(); var dbCount = post?.ViewCount ?? 0; // 3. 将结果写入Redis缓存(如果计数大于0) if (dbCount > 0) { await _redisService.StringSetAsync(countKey, dbCount, VIEW_COUNT_CACHE_EXPIRATION); } return dbCount; } catch (Exception ex) { // Redis异常时降级到数据库查询 _logger.LogWarning(ex, "Redis查询浏览次数失败,降级到数据库查询。PostId: {PostId}", postId); try { var post = await _postsRepository.Select .Where(x => x.Id == postId) .FirstAsync(); return post?.ViewCount ?? 0; } catch (Exception dbEx) { _logger.LogError(dbEx, "数据库查询浏览次数失败。PostId: {PostId}", postId); throw; } } } public async Task> BatchGetViewCountAsync(List postIds) { if (postIds == null || !postIds.Any()) { return new Dictionary(); } var result = new Dictionary(); var needQueryFromDb = new List(); try { // 1. 批量从Redis查询 foreach (var postId in postIds) { var countKey = GetViewCountKey(postId); var count = await _redisService.StringGetInt64Async(countKey); result[postId] = (int)count; if (count == 0) { needQueryFromDb.Add(postId); } } // 2. 批量查询数据库中缺失的 if (needQueryFromDb.Any()) { var posts = await _postsRepository.Select .Where(x => needQueryFromDb.Contains(x.Id)) .ToListAsync(); var dbCounts = posts.ToDictionary(x => x.Id, x => x.ViewCount); // 3. 更新结果并写入Redis缓存 foreach (var postId in needQueryFromDb) { var count = dbCounts.ContainsKey(postId) ? dbCounts[postId] : 0; result[postId] = count; if (count > 0) { var countKey = GetViewCountKey(postId); await _redisService.StringSetAsync(countKey, count, VIEW_COUNT_CACHE_EXPIRATION); } } } return result; } catch (Exception ex) { // Redis异常时降级到数据库查询 _logger.LogWarning(ex, "Redis批量查询浏览次数失败,降级到数据库查询。"); try { var posts = await _postsRepository.Select .Where(x => postIds.Contains(x.Id)) .ToListAsync(); var dbCounts = posts.ToDictionary(x => x.Id, x => x.ViewCount); foreach (var postId in postIds) { result[postId] = dbCounts.ContainsKey(postId) ? dbCounts[postId] : 0; } return result; } catch (Exception dbEx) { _logger.LogError(dbEx, "数据库批量查询浏览次数失败。"); throw; } } } public async Task AddViewAsync(long userId, long postId) { try { var now = DateTime.Now; // 1. 设置10分钟限制标记(Redis SETEX,TTL 10分钟) var limitKey = GetViewLimitKey(userId, postId); await _redisService.SetAsync(limitKey, "1", VIEW_LIMIT_TTL); // 2. 更新Redis计数(立即) var countKey = GetViewCountKey(postId); var newCount = await _redisService.IncrementAsync(countKey); // 3. 写入待处理操作Hash(用于定时批量同步到数据库) var pendingField = GetPendingOperationField(userId, postId); var operation = new { OccurredAt = now }; var operationJson = JsonSerializer.Serialize(operation); await _redisService.HashSetAsync(VIEW_PENDING_OPERATIONS_KEY, pendingField, operationJson); _logger.LogInformation( "[ViewService] 增加浏览次数操作成功。UserId: {UserId}, PostId: {PostId}, NewCount: {NewCount}", userId, postId, newCount); return true; } catch (Exception ex) { _logger.LogError(ex, "增加浏览次数操作失败。UserId: {UserId}, PostId: {PostId}", userId, postId); throw; } } } }