324 lines
12 KiB
C#
324 lines
12 KiB
C#
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 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;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|