414 lines
16 KiB
C#
414 lines
16 KiB
C#
using FreeSql;
|
||
|
||
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 LikeService : ILikeService
|
||
{
|
||
private readonly IRedisService _redisService;
|
||
private readonly IBaseRepository<T_Likes> _likesRepository;
|
||
private readonly ILogger<LikeService> _logger;
|
||
|
||
// Redis键命名规则常量
|
||
private const string LIKE_STATUS_KEY_PREFIX = "like:status:";
|
||
private const string LIKE_COUNT_KEY_PREFIX = "like_count:";
|
||
private const string LIKE_PENDING_OPERATIONS_KEY = "like:pending:operations";
|
||
|
||
// 缓存过期时间
|
||
private static readonly TimeSpan LIKE_STATUS_CACHE_EXPIRATION = TimeSpan.FromHours(1);
|
||
|
||
public LikeService(
|
||
IRedisService redisService,
|
||
IBaseRepository<T_Likes> likesRepository,
|
||
ILogger<LikeService> logger)
|
||
{
|
||
_redisService = redisService;
|
||
_likesRepository = likesRepository;
|
||
_logger = logger;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取点赞状态键
|
||
/// </summary>
|
||
private string GetLikeStatusKey(long userId, int targetType, long targetId)
|
||
{
|
||
return $"{LIKE_STATUS_KEY_PREFIX}{userId}:{targetType}:{targetId}";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取点赞计数键
|
||
/// </summary>
|
||
private string GetLikeCountKey(int targetType, long targetId)
|
||
{
|
||
return $"{LIKE_COUNT_KEY_PREFIX}{targetType}:{targetId}";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取待处理操作Hash字段名
|
||
/// </summary>
|
||
private string GetPendingOperationField(long userId, int targetType, long targetId)
|
||
{
|
||
return $"{userId}:{targetType}:{targetId}";
|
||
}
|
||
|
||
public async Task<bool> IsLikedAsync(long userId, int targetType, long targetId)
|
||
{
|
||
try
|
||
{
|
||
// 1. 优先从Redis查询
|
||
var statusKey = GetLikeStatusKey(userId, targetType, targetId);
|
||
var exists = await _redisService.SetContainsAsync(statusKey, "1");
|
||
|
||
if (exists)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
// 2. Redis中没有,查询数据库
|
||
var like = await _likesRepository.Select
|
||
.Where(x => x.UserId == userId && x.TargetType == targetType && x.TargetId == targetId)
|
||
.FirstAsync();
|
||
|
||
var isLiked = like != null;
|
||
|
||
// 3. 将结果写入Redis缓存
|
||
if (isLiked)
|
||
{
|
||
await _redisService.SetAddAsync(statusKey, "1");
|
||
await _redisService.SetExpirationAsync(statusKey, LIKE_STATUS_CACHE_EXPIRATION);
|
||
}
|
||
|
||
return isLiked;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// Redis异常时降级到数据库查询
|
||
_logger.LogWarning(ex, "Redis查询点赞状态失败,降级到数据库查询。UserId: {UserId}, TargetType: {TargetType}, TargetId: {TargetId}",
|
||
userId, targetType, targetId);
|
||
|
||
try
|
||
{
|
||
var like = await _likesRepository.Select
|
||
.Where(x => x.UserId == userId && x.TargetType == targetType && x.TargetId == targetId)
|
||
.FirstAsync();
|
||
|
||
return like != null;
|
||
}
|
||
catch (Exception dbEx)
|
||
{
|
||
_logger.LogError(dbEx, "数据库查询点赞状态失败。UserId: {UserId}, TargetType: {TargetType}, TargetId: {TargetId}",
|
||
userId, targetType, targetId);
|
||
throw;
|
||
}
|
||
}
|
||
}
|
||
|
||
public async Task<int> GetLikeCountAsync(int targetType, long targetId)
|
||
{
|
||
try
|
||
{
|
||
// 1. 优先从Redis查询
|
||
var countKey = GetLikeCountKey(targetType, targetId);
|
||
var count = await _redisService.StringGetInt64Async(countKey);
|
||
|
||
if (count > 0)
|
||
{
|
||
return (int)count;
|
||
}
|
||
|
||
// 2. Redis中没有或为0,查询数据库
|
||
var dbCount = await _likesRepository.Select
|
||
.Where(x => x.TargetType == targetType && x.TargetId == targetId)
|
||
.CountAsync();
|
||
|
||
// 3. 将结果写入Redis缓存(如果计数大于0)
|
||
if (dbCount > 0)
|
||
{
|
||
await _redisService.StringSetAsync(countKey, dbCount);
|
||
}
|
||
|
||
return (int)dbCount;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// Redis异常时降级到数据库查询
|
||
_logger.LogWarning(ex, "Redis查询点赞数失败,降级到数据库查询。TargetType: {TargetType}, TargetId: {TargetId}",
|
||
targetType, targetId);
|
||
|
||
try
|
||
{
|
||
return (int)await _likesRepository.Select
|
||
.Where(x => x.TargetType == targetType && x.TargetId == targetId)
|
||
.CountAsync();
|
||
}
|
||
catch (Exception dbEx)
|
||
{
|
||
_logger.LogError(dbEx, "数据库查询点赞数失败。TargetType: {TargetType}, TargetId: {TargetId}",
|
||
targetType, targetId);
|
||
throw;
|
||
}
|
||
}
|
||
}
|
||
|
||
public async Task<Dictionary<long, bool>> BatchIsLikedAsync(long userId, int targetType, List<long> targetIds)
|
||
{
|
||
if (targetIds == null || !targetIds.Any())
|
||
{
|
||
return new Dictionary<long, bool>();
|
||
}
|
||
|
||
var result = new Dictionary<long, bool>();
|
||
var needQueryFromDb = new List<long>();
|
||
|
||
try
|
||
{
|
||
// 1. 批量从Redis查询
|
||
foreach (var targetId in targetIds)
|
||
{
|
||
var statusKey = GetLikeStatusKey(userId, targetType, targetId);
|
||
var exists = await _redisService.SetContainsAsync(statusKey, "1");
|
||
result[targetId] = exists;
|
||
|
||
if (!exists)
|
||
{
|
||
needQueryFromDb.Add(targetId);
|
||
}
|
||
}
|
||
|
||
// 2. 批量查询数据库中缺失的
|
||
if (needQueryFromDb.Any())
|
||
{
|
||
var dbLikes = await _likesRepository.Select
|
||
.Where(x => x.UserId == userId && x.TargetType == targetType && needQueryFromDb.Contains(x.TargetId))
|
||
.ToListAsync();
|
||
|
||
var likedIds = dbLikes.Select(x => x.TargetId).ToList();
|
||
|
||
// 3. 更新结果并写入Redis缓存
|
||
foreach (var targetId in needQueryFromDb)
|
||
{
|
||
var isLiked = likedIds.Contains(targetId);
|
||
result[targetId] = isLiked;
|
||
|
||
if (isLiked)
|
||
{
|
||
var statusKey = GetLikeStatusKey(userId, targetType, targetId);
|
||
await _redisService.SetAddAsync(statusKey, "1");
|
||
await _redisService.SetExpirationAsync(statusKey, LIKE_STATUS_CACHE_EXPIRATION);
|
||
}
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// Redis异常时降级到数据库查询
|
||
_logger.LogWarning(ex, "Redis批量查询点赞状态失败,降级到数据库查询。UserId: {UserId}, TargetType: {TargetType}",
|
||
userId, targetType);
|
||
|
||
try
|
||
{
|
||
var dbLikes = await _likesRepository.Select
|
||
.Where(x => x.UserId == userId && x.TargetType == targetType && targetIds.Contains(x.TargetId))
|
||
.ToListAsync();
|
||
|
||
var likedIds = dbLikes.Select(x => x.TargetId).ToHashSet();
|
||
|
||
foreach (var targetId in targetIds)
|
||
{
|
||
result[targetId] = likedIds.Contains(targetId);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
catch (Exception dbEx)
|
||
{
|
||
_logger.LogError(dbEx, "数据库批量查询点赞状态失败。UserId: {UserId}, TargetType: {TargetType}",
|
||
userId, targetType);
|
||
throw;
|
||
}
|
||
}
|
||
}
|
||
|
||
public async Task<Dictionary<long, int>> BatchGetLikeCountAsync(int targetType, List<long> targetIds)
|
||
{
|
||
if (targetIds == null || !targetIds.Any())
|
||
{
|
||
return new Dictionary<long, int>();
|
||
}
|
||
|
||
var result = new Dictionary<long, int>();
|
||
var needQueryFromDb = new List<long>();
|
||
|
||
try
|
||
{
|
||
// 1. 批量从Redis查询
|
||
foreach (var targetId in targetIds)
|
||
{
|
||
var countKey = GetLikeCountKey(targetType, targetId);
|
||
var count = await _redisService.StringGetInt64Async(countKey);
|
||
result[targetId] = (int)count;
|
||
|
||
if (count == 0)
|
||
{
|
||
needQueryFromDb.Add(targetId);
|
||
}
|
||
}
|
||
|
||
// 2. 批量查询数据库中缺失的
|
||
if (needQueryFromDb.Any())
|
||
{
|
||
var dbCounts = _likesRepository.Select
|
||
.Where(x => x.TargetType == targetType && needQueryFromDb.Contains(x.TargetId))
|
||
.GroupBy(x => x.TargetId)
|
||
.Select(g => new { TargetId = g.Key, Count = g.Count() })
|
||
.ToList();
|
||
|
||
var countsDict = dbCounts.ToDictionary(x => x.TargetId, x => x.Count);
|
||
|
||
// 3. 更新结果并写入Redis缓存
|
||
foreach (var targetId in needQueryFromDb)
|
||
{
|
||
var count = countsDict.ContainsKey(targetId) ? countsDict[targetId] : 0;
|
||
result[targetId] = count;
|
||
|
||
if (count > 0)
|
||
{
|
||
var countKey = GetLikeCountKey(targetType, targetId);
|
||
await _redisService.StringSetAsync(countKey, count);
|
||
}
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// Redis异常时降级到数据库查询
|
||
_logger.LogWarning(ex, "Redis批量查询点赞数失败,降级到数据库查询。TargetType: {TargetType}",
|
||
targetType);
|
||
|
||
try
|
||
{
|
||
var dbCounts = _likesRepository.Select
|
||
.Where(x => x.TargetType == targetType && targetIds.Contains(x.TargetId))
|
||
.GroupBy(x => x.TargetId)
|
||
.Select(g => new { TargetId = g.Key, Count = g.Count() })
|
||
.ToList();
|
||
|
||
var countsDict = dbCounts.ToDictionary(x => x.TargetId, x => x.Count);
|
||
|
||
foreach (var targetId in targetIds)
|
||
{
|
||
result[targetId] = countsDict.ContainsKey(targetId) ? countsDict[targetId] : 0;
|
||
}
|
||
|
||
return result;
|
||
}
|
||
catch (Exception dbEx)
|
||
{
|
||
_logger.LogError(dbEx, "数据库批量查询点赞数失败。TargetType: {TargetType}",
|
||
targetType);
|
||
throw;
|
||
}
|
||
}
|
||
}
|
||
|
||
public async Task<bool> LikeAsync(long userId, int targetType, long targetId, long receiverId)
|
||
{
|
||
try
|
||
{
|
||
// 1. 更新Redis状态(立即)
|
||
var statusKey = GetLikeStatusKey(userId, targetType, targetId);
|
||
await _redisService.SetAddAsync(statusKey, "1");
|
||
await _redisService.SetExpirationAsync(statusKey, LIKE_STATUS_CACHE_EXPIRATION);
|
||
|
||
// 2. 更新Redis计数(立即)
|
||
var countKey = GetLikeCountKey(targetType, targetId);
|
||
var newCount = await _redisService.IncrementAsync(countKey);
|
||
|
||
// 3. 写入待处理操作Hash(用于定时批量同步到数据库)
|
||
var pendingField = GetPendingOperationField(userId, targetType, targetId);
|
||
var operation = new
|
||
{
|
||
Action = "INSERT",
|
||
OccurredAt = DateTime.Now,
|
||
ReceiverId = receiverId
|
||
};
|
||
var operationJson = JsonSerializer.Serialize(operation);
|
||
await _redisService.HashSetAsync(LIKE_PENDING_OPERATIONS_KEY, pendingField, operationJson);
|
||
|
||
_logger.LogInformation(
|
||
"[LikeService] 点赞操作成功。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;
|
||
}
|
||
}
|
||
|
||
public async Task<bool> UnlikeAsync(long userId, int targetType, long targetId)
|
||
{
|
||
try
|
||
{
|
||
// 1. 更新Redis状态(立即)
|
||
var statusKey = GetLikeStatusKey(userId, targetType, targetId);
|
||
await _redisService.SetRemoveAsync(statusKey, "1");
|
||
|
||
// 2. 更新Redis计数(立即,但不能小于0)
|
||
var countKey = GetLikeCountKey(targetType, targetId);
|
||
var newCount = await _redisService.DecrementAsync(countKey);
|
||
if (newCount < 0)
|
||
{
|
||
// 如果计数小于0,重置为0(可能Redis和数据库不一致)
|
||
await _redisService.StringSetAsync(countKey, 0);
|
||
newCount = 0;
|
||
}
|
||
|
||
// 3. 写入待处理操作Hash(用于定时批量同步到数据库)
|
||
var pendingField = GetPendingOperationField(userId, targetType, targetId);
|
||
var operation = new
|
||
{
|
||
Action = "DELETE",
|
||
OccurredAt = DateTime.Now
|
||
};
|
||
var operationJson = JsonSerializer.Serialize(operation);
|
||
await _redisService.HashSetAsync(LIKE_PENDING_OPERATIONS_KEY, pendingField, operationJson);
|
||
|
||
_logger.LogInformation(
|
||
"[LikeService] 取消点赞操作成功。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;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|