live-forum/server/webapi/LiveForum/LiveForum.Service/Posts/ViewService.cs
2026-03-24 11:27:37 +08:00

261 lines
8.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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