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

324 lines
12 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.Redis.Contract;
using LiveForum.IService.Flowers;
using LiveForum.Model;
using Microsoft.Extensions.Logging;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
namespace LiveForum.Service.Flowers
{
/// <summary>
/// 送花服务实现
/// </summary>
public class FlowerService : IFlowerService
{
private readonly IRedisService _redisService;
private readonly IBaseRepository<T_Posts> _postsRepository;
private readonly IBaseRepository<T_Streamers> _streamersRepository;
private readonly ILogger<FlowerService> _logger;
// Redis键命名规则常量
private const string FLOWER_LIMIT_KEY_PREFIX = "flower:limit:";
private const string FLOWER_COUNT_KEY_PREFIX = "flower_count:";
private const string FLOWER_PENDING_OPERATIONS_KEY = "flower:pending:operations";
// 缓存过期时间
private static readonly TimeSpan FLOWER_LIMIT_TTL = TimeSpan.FromHours(1); // 1小时限制
private static readonly TimeSpan FLOWER_COUNT_CACHE_EXPIRATION = TimeSpan.FromHours(24); // 花数缓存24小时
public FlowerService(
IRedisService redisService,
IBaseRepository<T_Posts> postsRepository,
IBaseRepository<T_Streamers> streamersRepository,
ILogger<FlowerService> logger)
{
_redisService = redisService;
_postsRepository = postsRepository;
_streamersRepository = streamersRepository;
_logger = logger;
}
/// <summary>
/// 获取1小时限制键
/// </summary>
private string GetFlowerLimitKey(long userId, string targetType, long targetId)
{
return $"{FLOWER_LIMIT_KEY_PREFIX}{userId}:{targetType}:{targetId}";
}
/// <summary>
/// 获取花数计数键
/// </summary>
private string GetFlowerCountKey(string targetType, long targetId)
{
return $"{FLOWER_COUNT_KEY_PREFIX}{targetType}:{targetId}";
}
/// <summary>
/// 获取待处理操作Hash字段名
/// </summary>
private string GetPendingOperationField(long userId, string targetType, long targetId)
{
return $"{userId}:{targetType}:{targetId}";
}
public async Task<bool> CanSendFlowerAsync(long userId, string targetType, long targetId)
{
try
{
// 1. 优先从Redis检查1小时限制
var limitKey = GetFlowerLimitKey(userId, targetType, targetId);
var exists = await _redisService.ExistsAsync(limitKey);
if (exists)
{
// Redis中存在限制标记说明1小时内已送过花
return false;
}
// 2. Redis中没有查询数据库降级策略
// 注意由于送花频率较低这里可以只查Redis如果Redis没有就认为可以送花
// 数据库检查会在批量同步时处理重复数据
return true;
}
catch (Exception ex)
{
// Redis异常时允许送花降级策略数据库层面会处理重复
_logger.LogWarning(ex, "Redis检查送花限制失败降级允许送花。UserId: {UserId}, TargetType: {TargetType}, TargetId: {TargetId}",
userId, targetType, targetId);
return true;
}
}
public async Task<int> GetFlowerCountAsync(string targetType, long targetId)
{
try
{
// 1. 优先从Redis查询
var countKey = GetFlowerCountKey(targetType, targetId);
var count = await _redisService.StringGetInt64Async(countKey);
if (count > 0)
{
return (int)count;
}
// 2. Redis中没有或为0查询数据库
int dbCount = 0;
if (targetType == "Post")
{
var post = await _postsRepository.Select
.Where(x => x.Id == targetId)
.FirstAsync();
if (post != null)
{
dbCount = post.FlowerCount;
}
}
else if (targetType == "Streamer")
{
var streamer = await _streamersRepository.Select
.Where(x => x.Id == targetId)
.FirstAsync();
if (streamer != null)
{
dbCount = streamer.FlowerCount;
}
}
// 3. 将结果写入Redis缓存如果计数大于0
if (dbCount > 0)
{
await _redisService.StringSetAsync(countKey, dbCount, FLOWER_COUNT_CACHE_EXPIRATION);
}
return dbCount;
}
catch (Exception ex)
{
// Redis异常时降级到数据库查询
_logger.LogWarning(ex, "Redis查询花数失败降级到数据库查询。TargetType: {TargetType}, TargetId: {TargetId}",
targetType, targetId);
try
{
if (targetType == "Post")
{
var post = await _postsRepository.Select
.Where(x => x.Id == targetId)
.FirstAsync();
return post?.FlowerCount ?? 0;
}
else if (targetType == "Streamer")
{
var streamer = await _streamersRepository.Select
.Where(x => x.Id == targetId)
.FirstAsync();
return streamer?.FlowerCount ?? 0;
}
return 0;
}
catch (Exception dbEx)
{
_logger.LogError(dbEx, "数据库查询花数失败。TargetType: {TargetType}, TargetId: {TargetId}",
targetType, targetId);
throw;
}
}
}
public async Task<Dictionary<long, int>> BatchGetFlowerCountAsync(string 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 = GetFlowerCountKey(targetType, targetId);
var count = await _redisService.StringGetInt64Async(countKey);
result[targetId] = (int)count;
if (count == 0)
{
needQueryFromDb.Add(targetId);
}
}
// 2. 批量查询数据库中缺失的
if (needQueryFromDb.Any())
{
Dictionary<long, int> dbCounts = new Dictionary<long, int>();
if (targetType == "Post")
{
var posts = await _postsRepository.Select
.Where(x => needQueryFromDb.Contains(x.Id))
.ToListAsync();
dbCounts = posts.ToDictionary(x => x.Id, x => x.FlowerCount);
}
else if (targetType == "Streamer")
{
var streamers = await _streamersRepository.Select
.Where(x => needQueryFromDb.Contains(x.Id))
.ToListAsync();
dbCounts = streamers.ToDictionary(x => (long)x.Id, x => x.FlowerCount);
}
// 3. 更新结果并写入Redis缓存
foreach (var targetId in needQueryFromDb)
{
var count = dbCounts.ContainsKey(targetId) ? dbCounts[targetId] : 0;
result[targetId] = count;
if (count > 0)
{
var countKey = GetFlowerCountKey(targetType, targetId);
await _redisService.StringSetAsync(countKey, count, FLOWER_COUNT_CACHE_EXPIRATION);
}
}
}
return result;
}
catch (Exception ex)
{
// Redis异常时降级到数据库查询
_logger.LogWarning(ex, "Redis批量查询花数失败降级到数据库查询。TargetType: {TargetType}",
targetType);
try
{
Dictionary<long, int> dbCounts = new Dictionary<long, int>();
if (targetType == "Post")
{
var posts = await _postsRepository.Select
.Where(x => targetIds.Contains(x.Id))
.ToListAsync();
dbCounts = posts.ToDictionary(x => x.Id, x => x.FlowerCount);
}
else if (targetType == "Streamer")
{
var streamers = await _streamersRepository.Select
.Where(x => targetIds.Contains(x.Id))
.ToListAsync();
dbCounts = streamers.ToDictionary(x => (long)x.Id, x => x.FlowerCount);
}
foreach (var targetId in targetIds)
{
result[targetId] = dbCounts.ContainsKey(targetId) ? dbCounts[targetId] : 0;
}
return result;
}
catch (Exception dbEx)
{
_logger.LogError(dbEx, "数据库批量查询花数失败。TargetType: {TargetType}",
targetType);
throw;
}
}
}
public async Task<bool> SendFlowerAsync(long userId, string targetType, long targetId, long receiverId)
{
try
{
var now = DateTime.Now;
var yearMonth = now.ToString("yyyy-MM");
// 1. 设置1小时限制标记Redis SETEXTTL 1小时
var limitKey = GetFlowerLimitKey(userId, targetType, targetId);
await _redisService.SetAsync(limitKey, "1", FLOWER_LIMIT_TTL);
// 2. 更新Redis计数立即
var countKey = GetFlowerCountKey(targetType, targetId);
var newCount = await _redisService.IncrementAsync(countKey);
// 3. 写入待处理操作Hash用于定时批量同步到数据库
var pendingField = GetPendingOperationField(userId, targetType, targetId);
var operation = new
{
SendMonth = yearMonth,
OccurredAt = now,
ReceiverId = receiverId
};
var operationJson = JsonSerializer.Serialize(operation);
await _redisService.HashSetAsync(FLOWER_PENDING_OPERATIONS_KEY, pendingField, operationJson);
_logger.LogInformation(
"[FlowerService] 送花操作成功。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;
}
}
}
}