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
{
///
/// 送花服务实现
///
public class FlowerService : IFlowerService
{
private readonly IRedisService _redisService;
private readonly IBaseRepository _postsRepository;
private readonly IBaseRepository _streamersRepository;
private readonly ILogger _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 postsRepository,
IBaseRepository streamersRepository,
ILogger logger)
{
_redisService = redisService;
_postsRepository = postsRepository;
_streamersRepository = streamersRepository;
_logger = logger;
}
///
/// 获取1小时限制键
///
private string GetFlowerLimitKey(long userId, string targetType, long targetId)
{
return $"{FLOWER_LIMIT_KEY_PREFIX}{userId}:{targetType}:{targetId}";
}
///
/// 获取花数计数键
///
private string GetFlowerCountKey(string targetType, long targetId)
{
return $"{FLOWER_COUNT_KEY_PREFIX}{targetType}:{targetId}";
}
///
/// 获取待处理操作Hash字段名
///
private string GetPendingOperationField(long userId, string targetType, long targetId)
{
return $"{userId}:{targetType}:{targetId}";
}
public async Task 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 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> BatchGetFlowerCountAsync(string targetType, List targetIds)
{
if (targetIds == null || !targetIds.Any())
{
return new Dictionary();
}
var result = new Dictionary();
var needQueryFromDb = new List();
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 dbCounts = new Dictionary();
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 dbCounts = new Dictionary();
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 SendFlowerAsync(long userId, string targetType, long targetId, long receiverId)
{
try
{
var now = DateTime.Now;
var yearMonth = now.ToString("yyyy-MM");
// 1. 设置1小时限制标记(Redis SETEX,TTL 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;
}
}
}
}