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

414 lines
16 KiB
C#
Raw Permalink 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.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;
}
}
}
}