261 lines
8.8 KiB
C#
261 lines
8.8 KiB
C#
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
|
||
{
|
||
/// <summary>
|
||
/// 浏览服务实现
|
||
/// </summary>
|
||
public class ViewService : IViewService
|
||
{
|
||
private readonly IRedisService _redisService;
|
||
private readonly IBaseRepository<T_Posts> _postsRepository;
|
||
private readonly ILogger<ViewService> _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<T_Posts> postsRepository,
|
||
ILogger<ViewService> logger)
|
||
{
|
||
_redisService = redisService;
|
||
_postsRepository = postsRepository;
|
||
_logger = logger;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取10分钟限制键
|
||
/// </summary>
|
||
private string GetViewLimitKey(long userId, long postId)
|
||
{
|
||
return $"{VIEW_LIMIT_KEY_PREFIX}{userId}:{postId}";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取浏览次数计数键
|
||
/// </summary>
|
||
private string GetViewCountKey(long postId)
|
||
{
|
||
return $"{VIEW_COUNT_KEY_PREFIX}{postId}";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取待处理操作Hash字段名
|
||
/// </summary>
|
||
private string GetPendingOperationField(long userId, long postId)
|
||
{
|
||
return $"{userId}:{postId}";
|
||
}
|
||
|
||
public async Task<bool> 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<int> 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<Dictionary<long, int>> BatchGetViewCountAsync(List<long> postIds)
|
||
{
|
||
if (postIds == null || !postIds.Any())
|
||
{
|
||
return new Dictionary<long, int>();
|
||
}
|
||
|
||
var result = new Dictionary<long, int>();
|
||
var needQueryFromDb = new List<long>();
|
||
|
||
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<bool> 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;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|