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;
}
}
}
}