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

1078 lines
43 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.Base;
using LiveForum.Code.JwtInfrastructure;
using LiveForum.Code.Redis.Contract;
using LiveForum.Code.SensitiveWord.Interfaces;
using LiveForum.IService.Posts;
using LiveForum.IService.Messages;
using LiveForum.IService.Others;
using LiveForum.IService.Users;
using LiveForum.IService.Flowers;
using LiveForum.IService.Permission;
using LiveForum.Model;
using LiveForum.Model.Dto.Base;
using LiveForum.Model.Dto.PostComments;
using LiveForum.Model.Dto.Posts;
using LiveForum.Model.Dto.Messages;
using LiveForum.Model.Dto.Users;
using LiveForum.Model.Enum.Posts;
using LiveForum.Model.Enum.Users;
using Mapster;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LiveForum.Service.Posts
{
public class PostsService : IPostsService
{
private readonly JwtUserInfoModel _userInfoModel;
private readonly IBaseRepository<T_Posts> _postsRepository;
private readonly IBaseRepository<T_PostImages> _postImagesRepository;
private readonly IBaseRepository<T_PostCategories> _postCategoriesRepository;
private readonly IBaseRepository<T_Users> _usersRepository;
private readonly IBaseRepository<T_UserLevels> _userLevelsRepository;
private readonly IBaseRepository<T_Likes> _likesRepository;
private readonly IBaseRepository<T_Follows> _followsRepository;
private readonly ISensitiveWordService _sensitiveWordService;
private readonly IUserInfoService _userInfoService;
private readonly IMessagePublisher _messagePublisher;
private readonly IRedisService _redisService;
private readonly ILikeService _likeService;
private readonly IViewService _viewService;
private readonly IFlowerService _flowerService;
private readonly IBaseRepository<T_CertificationTypes> _certificationTypesRepository;
private readonly IPostReplyIntervalService _postReplyIntervalService;
private readonly IAntiAddictionService _antiAddictionService;
private readonly IPermissionService _permissionService;
// Redis缓存键前缀和过期时间
private const string POST_IMAGE_CACHE_KEY_PREFIX = "post:image:";
private static readonly TimeSpan POST_IMAGE_CACHE_EXPIRATION = TimeSpan.FromHours(24);
/// <summary>
/// 构造函数
/// </summary>
/// <param name="userInfoModel">JWT用户信息模型</param>
/// <param name="postsRepository">帖子仓储</param>
/// <param name="postImagesRepository">帖子图片仓储</param>
/// <param name="postCategoriesRepository">帖子分类仓储</param>
/// <param name="usersRepository">用户仓储</param>
/// <param name="userLevelsRepository">用户等级仓储</param>
/// <param name="likesRepository">点赞仓储</param>
/// <param name="followsRepository">关注仓储</param>
/// <param name="sensitiveWordService">敏感词服务</param>
/// <param name="userInfoService">用户信息转换服务</param>
/// <param name="messagePublisher">消息发布器</param>
/// <param name="redisService">Redis服务</param>
/// <param name="likeService">点赞服务</param>
/// <param name="viewService">浏览服务</param>
/// <param name="flowerService">送花服务</param>
/// <param name="certificationTypesRepository">认证类型仓储</param>
/// <param name="antiAddictionService">防沉迷校验服务</param>
public PostsService(
JwtUserInfoModel userInfoModel,
IBaseRepository<T_Posts> postsRepository,
IBaseRepository<T_PostImages> postImagesRepository,
IBaseRepository<T_PostCategories> postCategoriesRepository,
IBaseRepository<T_Users> usersRepository,
IBaseRepository<T_UserLevels> userLevelsRepository,
IBaseRepository<T_Likes> likesRepository,
IBaseRepository<T_Follows> followsRepository,
ISensitiveWordService sensitiveWordService,
IUserInfoService userInfoService,
IMessagePublisher messagePublisher,
IRedisService redisService,
ILikeService likeService,
IViewService viewService,
IFlowerService flowerService,
IBaseRepository<T_CertificationTypes> certificationTypesRepository,
IPostReplyIntervalService postReplyIntervalService,
IAntiAddictionService antiAddictionService,
IPermissionService permissionService)
{
_userInfoModel = userInfoModel;
_postsRepository = postsRepository;
_postImagesRepository = postImagesRepository;
_postCategoriesRepository = postCategoriesRepository;
_usersRepository = usersRepository;
_userLevelsRepository = userLevelsRepository;
_likesRepository = likesRepository;
_followsRepository = followsRepository;
_sensitiveWordService = sensitiveWordService;
_userInfoService = userInfoService;
_messagePublisher = messagePublisher;
_redisService = redisService;
_likeService = likeService;
_viewService = viewService;
_flowerService = flowerService;
_certificationTypesRepository = certificationTypesRepository;
_postReplyIntervalService = postReplyIntervalService;
_antiAddictionService = antiAddictionService;
_permissionService = permissionService;
}
/// <summary>
/// 获取帖子列表(瀑布流)
/// </summary>
/// <param name="request">请求参数</param>
/// <returns></returns>
public async Task<BaseResponse<GetPostsRespDto>> GetPosts(GetPostsReq request)
{
var currentUserId = (long)_userInfoModel.UserId;
// 1. 构建查询条件
var query = _postsRepository.Select
.Where(x => !x.IsDeleted && x.Status == PostsStatusEnum.Publish);
// 2. 分类筛选
if (request.CategoryId.HasValue)
{
query = query.Where(x => x.CategoryId == request.CategoryId.Value);
}
// 3. 排序
switch (request.SortType)
{
case 1: // 最新
query = query.OrderByDescending(x => x.IsTop).OrderByDescending(x => x.PublishTime);
break;
case 2: // 最热
query = query.OrderByDescending(x => x.LikeCount)
.OrderByDescending(x => x.ViewCount);
break;
case 3: // 推荐
query = query.OrderByDescending(x => x.IsTop)
.OrderByDescending(x => x.IsEssence)
.OrderByDescending(x => x.PublishTime);
break;
default:
query = query.OrderByDescending(x => x.PublishTime);
break;
}
// 4. 分页
var total = await query.CountAsync();
var posts = await query
.Skip((request.PageIndex - 1) * request.PageSize)
.Take(request.PageSize)
.ToListAsync();
// 5. 构建返回数据
var postIds = posts.Select(x => x.Id).ToList();
// 5.1 从Redis缓存批量获取帖子图片
var postImages = await GetPostImagesFromCacheAsync(postIds);
// 5.2 找出缓存未命中的帖子ID从数据库查询并写入缓存
var cachedPostIds = postImages.Select(x => x.PostId).Distinct().ToList();
var uncachedPostIds = postIds.Except(cachedPostIds).ToList();
if (uncachedPostIds.Any())
{
var dbPostImages = await _postImagesRepository.Select
.Where(x => uncachedPostIds.Contains(x.PostId))
.OrderBy(x => x.PostId)
.OrderBy(x => x.SortOrder)
.ToListAsync();
// 将数据库查询结果添加到结果集
postImages.AddRange(dbPostImages);
// 按帖子ID分组并批量写入缓存
await SetPostImagesToCacheAsync(dbPostImages);
}
// 按PostId和SortOrder排序
postImages = postImages
.OrderBy(x => x.PostId)
.ThenBy(x => x.SortOrder)
.ToList();
//var categoryIds = posts.Where(x => x.CategoryId.HasValue).Select(x => x.CategoryId!.Value).Distinct().ToList();
//暂时不需要分类,先留个接口
var categories = new List<T_PostCategories>();
//await _postCategoriesRepository.Select
//.Where(x => categoryIds.Contains(x.Id))
//.ToListAsync();
var userIds = posts.Select(x => x.UserId).Distinct().ToList();
var users = await _usersRepository.Select
.Where(x => userIds.Contains(x.Id))
.ToListAsync();
// 6. 批量查询点赞和关注状态
var likedStatusDict = await _likeService.BatchIsLikedAsync(currentUserId, 1, postIds);
var likeCountDict = await _likeService.BatchGetLikeCountAsync(1, postIds);
// 6.1 批量查询送花数量(从缓存获取)
var flowerCountDict = await _flowerService.BatchGetFlowerCountAsync("Post", postIds);
//暂时不需要关注,先留个接口
var followedUserIds = new List<T_Follows>();
//await _followsRepository.Select
//.Where(x => x.FollowerId == currentUserId && userIds.Contains(x.FollowedUserId))
//.ToListAsync();
// 7. 批量转换用户信息(自动获取认证类型数据)
var usersDict = await _userInfoService.ToUserInfoDtoDictionaryAsync(users);
// 8. 构建帖子列表
var items = posts.Select(post =>
{
var user = users.FirstOrDefault(x => x.Id == post.UserId);
var category = categories.FirstOrDefault(x => x.Id == post.CategoryId);
var images = postImages.Where(x => x.PostId == post.Id).ToList();
return new PostListItemDto
{
PostId = post.Id,
Title = post.Title,
Content = post.Content.Length > 100 ? post.Content.Substring(0, 100) + "..." : post.Content,
CoverImage = post.CoverImage,
//送花次数(从缓存获取)
FlowerCount = flowerCountDict.ContainsKey(post.Id) ? flowerCountDict[post.Id] : 0,
Images = images.Select(img => new PostImageDto
{
ImageId = (int)img.Id,
ImageUrl = img.ImageUrl,
ThumbnailUrl = img.ThumbnailUrl,
ImageWidth = img.ImageWidth ?? 0,
ImageHeight = img.ImageHeight ?? 0,
SortOrder = img.SortOrder
}).ToList(),
CategoryId = post.CategoryId,
CategoryName = category?.CategoryName ?? "",
ViewCount = post.ViewCount,
LikeCount = likeCountDict.ContainsKey(post.Id) ? likeCountDict[post.Id] : 0,
CommentCount = post.CommentCount,
ShareCount = post.ShareCount,
IsTop = post.IsTop,
IsHot = post.IsHot,
IsEssence = post.IsEssence,
PublishTime = post.PublishTime,
User = user != null && usersDict.ContainsKey(user.Id) ? usersDict[user.Id] : new UserInfoDto(),
IsLiked = likedStatusDict.ContainsKey(post.Id) && likedStatusDict[post.Id],
IsFollowed = followedUserIds.Any(x => x.FollowedUserId == post.UserId)
};
}).ToList();
var result = new GetPostsRespDto
{
PageIndex = request.PageIndex,
PageSize = request.PageSize,
Total = (int)total,
TotalPages = (int)Math.Ceiling((double)total / request.PageSize),
Items = items
};
return new BaseResponse<GetPostsRespDto>(result);
}
/// <summary>
/// 获取帖子详情
/// </summary>
/// <param name="request">请求参数</param>
/// <returns></returns>
public async Task<BaseResponse<PostDetailDto>> GetPostDetail(GetPostDetailReq request)
{
var currentUserId = (long)_userInfoModel.UserId;
// 1. 获取帖子信息(不过滤已删除状态)
var post = await _postsRepository.Select
.Where(x => x.Id == request.PostId)
.FirstAsync();
if (post == null)
{
return new BaseResponse<PostDetailDto>(ResponseCode.Error, "帖子不存在");
}
// 如果帖子已删除,返回特定错误码
if (post.IsDeleted)
{
return new BaseResponse<PostDetailDto>(ResponseCode.PostDeleted, "帖子已删除");
}
// 2. 检查10分钟限制并增加浏览次数
var canAddView = await _viewService.CanAddViewAsync(currentUserId, post.Id);
if (canAddView)
{
await _viewService.AddViewAsync(currentUserId, post.Id);
}
// 3. 获取相关数据(优先从缓存获取)
var postImages = await GetPostImagesFromCacheAsync(new List<long> { post.Id });
// 如果缓存未命中,从数据库查询并写入缓存
if (!postImages.Any())
{
postImages = await _postImagesRepository.Select
.Where(x => x.PostId == post.Id)
.OrderBy(x => x.SortOrder)
.ToListAsync();
// 写入缓存
await SetPostImagesToCacheAsync(postImages);
}
var category = post.CategoryId.HasValue ? await _postCategoriesRepository.Select
.Where(x => x.Id == post.CategoryId.Value)
.FirstAsync() : null;
var user = await _usersRepository.Select
.Where(x => x.Id == post.UserId)
.FirstAsync();
var userLevel = user != null ? await _userLevelsRepository.Select
.Where(x => x.Id == user.LevelId)
.FirstAsync() : null;
// 4. 检查点赞和关注状态
var isLiked = await _likeService.IsLikedAsync(currentUserId, 1, post.Id);
var likeCount = await _likeService.GetLikeCountAsync(1, post.Id);
// 4.1 获取送花数量(从缓存获取)
var flowerCount = await _flowerService.GetFlowerCountAsync("Post", post.Id);
var isFollowed = await _followsRepository.Select
.Where(x => x.FollowerId == currentUserId && x.FollowedUserId == post.UserId)
.AnyAsync();
// 5. 使用服务转换用户信息(自动获取认证类型数据)
var userInfo = await _userInfoService.ToUserInfoDtoAsync(user);
// 6. 查询当前用户是否为管理员
var currentUser = await _usersRepository.Select
.Where(x => x.Id == currentUserId)
.FirstAsync();
var isAdmin = currentUser?.CertifiedType != null
&& currentUser.CertifiedType > 0
&& await IsAdminCertificationType(currentUser.CertifiedType.Value);
// 6.1 获取当前用户的有效权限(身份组+认证等级)
var effectivePermission = await _permissionService.GetEffectivePermissionAsync(currentUserId);
// 7. 构建返回数据
var result = new PostDetailDto
{
PostId = post.Id,
Title = post.Title,
Content = post.Content,
CoverImage = post.CoverImage,
//送花次数(从缓存获取)
FlowerCount = flowerCount,
Images = postImages.Select(img => new PostImageDto
{
ImageId = (int)img.Id,
ImageUrl = img.ImageUrl,
ThumbnailUrl = img.ThumbnailUrl,
ImageWidth = img.ImageWidth ?? 0,
ImageHeight = img.ImageHeight ?? 0,
SortOrder = img.SortOrder
}).ToList(),
CategoryId = post.CategoryId,
CategoryName = category?.CategoryName ?? "",
ViewCount = await _viewService.GetViewCountAsync(post.Id),
LikeCount = likeCount,
CommentCount = post.CommentCount,
ShareCount = post.ShareCount,
IsTop = post.IsTop,
IsHot = post.IsHot,
IsEssence = post.IsEssence,
Status = (int)post.Status,
PublishTime = post.PublishTime,
User = userInfo,
IsLiked = isLiked,
IsFollowed = isFollowed,
IsMine = post.UserId == currentUserId,
AllowReply = post.AllowReply,
IsAdmin = isAdmin,
CanDeleteOtherPost = isAdmin || effectivePermission.CanDeleteOtherPost
};
return new BaseResponse<PostDetailDto>(result);
}
/// <summary>
/// 发布帖子
/// </summary>
/// <param name="request">请求参数</param>
/// <returns></returns>
public async Task<BaseResponse<PublishPostsRespDto>> PublishPosts(PublishPostsReq request)
{
var currentUserId = (long)_userInfoModel.UserId;
var now = DateTime.Now;
// 0. 防抖检查使用Redis防止重复提交
var lockKey = $"posting:{currentUserId}";
var db = await _redisService.GetDatabaseAsync();
var lockExpiration = TimeSpan.FromSeconds(30); // 锁定30秒防止重复提交
// 尝试设置锁如果键已存在则返回false原子操作
var lockAcquired = await db.StringSetAsync(lockKey, "1", lockExpiration, When.NotExists);
if (!lockAcquired)
{
// 获取剩余过期时间,给用户更友好的提示
var remainingTime = await db.KeyTimeToLiveAsync(lockKey);
var seconds = remainingTime?.TotalSeconds ?? 0;
return new BaseResponse<PublishPostsRespDto>(
ResponseCode.Error,
$"您正在提交帖子,请稍候再试(剩余 {Math.Ceiling(seconds)} 秒)");
}
try
{
// 1. 验证分类是否存在
var category = await _postCategoriesRepository.Select
.Where(x => x.Id == request.CategoryId && x.IsActive)
.FirstAsync();
if (category == null)
{
category = new T_PostCategories() { Id = 0 };
//return new BaseResponse<PublishPostsRespDto>(ResponseCode.Error, "分类不存在");
}
var userInfo = await _userInfoService.GetUserInfo(currentUserId);
if (userInfo.IsCertified == false)
{
return new BaseResponse<PublishPostsRespDto>(ResponseCode.Error, "用户未认证");
}
if (userInfo.CertifiedStatus == null || userInfo.CertifiedStatus == CertifiedStatusEnum.)
{
return new BaseResponse<PublishPostsRespDto>(ResponseCode.UserNotCertified, "用户未实名认证,请先完成认证");
}
if (userInfo.CertifiedStatus == CertifiedStatusEnum.)
{
return new BaseResponse<PublishPostsRespDto>(ResponseCode.UserCertificationPending, "认证审核中,请等待审核完成");
}
// 发帖间隔校验
var remainingSeconds = await _postReplyIntervalService.CheckPostIntervalAsync(currentUserId);
if (remainingSeconds.HasValue)
{
return new BaseResponse<PublishPostsRespDto>(
ResponseCode.Error,
$"发帖过于频繁,请等待 {remainingSeconds.Value} 秒后再试");
}
// 防沉迷校验
if (await _antiAddictionService.IsRestrictedAsync("Post"))
{
return new BaseResponse<PublishPostsRespDto>(ResponseCode.Error, "防沉迷时间内无法操作");
}
// 2. 过滤敏感词
request.Title = _sensitiveWordService.Filter(request.Title);
request.Content = _sensitiveWordService.Filter(request.Content);
// 3. 创建帖子
var post = new T_Posts
{
UserId = currentUserId,
CategoryId = request.CategoryId,
Title = request.Title,
Content = request.Content,
Status = (PostsStatusEnum)request.Status,
PublishTime = request.Status == 1 ? now : null,
AllowReply = request.AllowReply,
CreatedAt = now,
UpdatedAt = now
};
// 4. 设置封面图片
if (request.Images.Any())
{
var firstImage = request.Images.OrderBy(x => x.SortOrder).First();
post.CoverImage = firstImage.ImageUrl;
}
await _postsRepository.InsertAsync(post);
// 5. 保存帖子图片
if (request.Images.Any())
{
var postImages = request.Images.Select((img, index) => new T_PostImages
{
PostId = post.Id,
ImageUrl = img.ImageUrl,
ThumbnailUrl = img.ThumbnailUrl,
ImageWidth = img.ImageWidth,
ImageHeight = img.ImageHeight,
SortOrder = img.SortOrder,
CreatedAt = now
}).ToList();
await _postImagesRepository.InsertAsync(postImages);
// 清除该帖子的图片缓存,下次查询时会自动更新
await ClearPostImageCacheAsync(post.Id);
}
// 6. 构建返回数据
var postInterval = await _postReplyIntervalService.GetPostIntervalAsync(currentUserId);
var result = new PublishPostsRespDto
{
PostId = post.Id,
Title = post.Title,
Status = (byte)post.Status,
PublishTime = post.PublishTime,
PostInterval = postInterval
};
return new BaseResponse<PublishPostsRespDto>(result);
}
finally
{
// 释放防抖锁
await _redisService.RemoveAsync(lockKey);
}
}
/// <summary>
/// 编辑帖子
/// </summary>
/// <param name="request">请求参数</param>
/// <returns></returns>
public async Task<BaseResponse<UpdatePostsRespDto>> UpdatePosts(UpdatePostsReq request)
{
var currentUserId = (long)_userInfoModel.UserId;
// 1. 获取帖子信息
var post = await _postsRepository.Select
.Where(x => x.Id == request.PostId && x.UserId == currentUserId && !x.IsDeleted)
.FirstAsync();
if (post == null)
{
return new BaseResponse<UpdatePostsRespDto>(ResponseCode.Error, "帖子不存在或无权限编辑");
}
// 2. 过滤敏感词
request.Title = _sensitiveWordService.Filter(request.Title);
request.Content = _sensitiveWordService.Filter(request.Content);
// 3. 更新帖子信息
post.Title = request.Title;
post.Content = request.Content;
post.CategoryId = request.CategoryId;
post.UpdatedAt = DateTime.Now;
await _postsRepository.UpdateAsync(post);
// 4. 更新图片(删除旧图片,添加新图片)
await _postImagesRepository.DeleteAsync(x => x.PostId == post.Id);
// 清除该帖子的图片缓存
await ClearPostImageCacheAsync(post.Id);
if (request.Images.Any())
{
var postImages = request.Images.Select(img => new T_PostImages
{
PostId = post.Id,
ImageUrl = img.ImageUrl,
ThumbnailUrl = img.ThumbnailUrl,
ImageWidth = img.ImageWidth,
ImageHeight = img.ImageHeight,
SortOrder = img.SortOrder,
CreatedAt = DateTime.Now
}).ToList();
await _postImagesRepository.InsertAsync(postImages);
// 更新封面图片
var firstImage = request.Images.OrderBy(x => x.SortOrder).First();
post.CoverImage = firstImage.ImageUrl;
await _postsRepository.UpdateAsync(post);
}
// 5. 构建返回数据
var result = new UpdatePostsRespDto
{
PostId = post.Id,
UpdatedAt = post.UpdatedAt
};
return new BaseResponse<UpdatePostsRespDto>(result);
}
/// <summary>
/// 删除帖子
/// </summary>
/// <param name="request">请求参数</param>
/// <returns></returns>
public async Task<BaseResponseBool> DeletePosts(DeletePostsReq request)
{
var currentUserId = (long)_userInfoModel.UserId;
// 1. 获取帖子信息(不限制 UserId
var post = await _postsRepository.Select
.Where(x => x.Id == request.PostId && !x.IsDeleted)
.FirstAsync();
if (post == null)
{
return new BaseResponseBool { Code = ResponseCode.Error, Message = "帖子不存在" };
}
// 2. 权限校验:帖子作者 或 管理员 或 有删除其他用户帖子权限
if (post.UserId != currentUserId)
{
// 非作者,检查是否为管理员或有删除权限
var currentUser = await _usersRepository.Select
.Where(x => x.Id == currentUserId)
.FirstAsync();
var isAdmin = currentUser?.CertifiedType != null
&& currentUser.CertifiedType > 0
&& await IsAdminCertificationType(currentUser.CertifiedType.Value);
if (!isAdmin)
{
// 再检查身份组/认证等级的有效权限
var effectivePermission = await _permissionService.GetEffectivePermissionAsync(currentUserId);
if (!effectivePermission.CanDeleteOtherPost)
{
return new BaseResponseBool { Code = ResponseCode.Error, Message = "权限不足,无法删除该帖子" };
}
}
}
// 3. 软删除
post.IsDeleted = true;
post.DeletedAt = DateTime.Now;
await _postsRepository.UpdateAsync(post);
// 清除该帖子的图片缓存
await ClearPostImageCacheAsync(post.Id);
return new BaseResponseBool { Code = ResponseCode.Success, Data = true };
}
/// <summary>
/// 获取我的帖子列表
/// </summary>
/// <param name="request">请求参数</param>
/// <returns></returns>
public async Task<BaseResponse<GetMyPostsRespDto>> GetMyPosts(GetMyPostsReq request)
{
var currentUserId = (long)_userInfoModel.UserId;
// 1. 构建查询条件
var query = _postsRepository.Select
.Where(x => x.UserId == currentUserId && !x.IsDeleted);
// 2. 状态筛选
if (request.Status.HasValue)
{
query = query.Where(x => x.Status == (PostsStatusEnum)request.Status.Value);
}
// 3. 排序
query = query.OrderByDescending(x => x.CreatedAt);
// 4. 分页
var total = await query.CountAsync();
var posts = await query
.Skip((request.PageIndex - 1) * request.PageSize)
.Take(request.PageSize)
.ToListAsync();
// 5. 构建返回数据
var items = posts.Select(post => new MyPostListItemDto
{
PostId = post.Id,
Title = post.Title,
CoverImage = post.CoverImage,
ViewCount = post.ViewCount,
LikeCount = post.LikeCount,
CommentCount = post.CommentCount,
Status = (byte)post.Status,
PublishTime = post.PublishTime
}).ToList();
var result = new GetMyPostsRespDto
{
PageIndex = request.PageIndex,
PageSize = request.PageSize,
Total = (int)total,
TotalPages = (int)Math.Ceiling((double)total / request.PageSize),
Items = items
};
return new BaseResponse<GetMyPostsRespDto>(result);
}
/// <summary>
/// 点赞/取消点赞帖子
/// </summary>
/// <param name="request">请求参数</param>
/// <returns></returns>
public async Task<BaseResponse<LikePostRespDto>> LikePost(LikePostReq request)
{
var currentUserId = (long)_userInfoModel.UserId;
// 1. 检查帖子是否存在
var post = await _postsRepository.Select
.Where(x => x.Id == request.PostId && !x.IsDeleted)
.FirstAsync();
if (post == null)
{
return new BaseResponse<LikePostRespDto>(ResponseCode.Error, "帖子不存在");
}
// 2. 查询当前点赞状态
var isCurrentlyLiked = await _likeService.IsLikedAsync(currentUserId, 1, request.PostId);
bool isLiked;
if (isCurrentlyLiked)
{
// 取消点赞
await _likeService.UnlikeAsync(currentUserId, 1, request.PostId);
isLiked = false;
}
else
{
// 添加点赞
await _likeService.LikeAsync(currentUserId, 1, request.PostId, post.UserId);
isLiked = true;
}
// 3. 获取最新点赞数
var likeCount = await _likeService.GetLikeCountAsync(1, request.PostId);
// 4. 发送点赞消息(如果是点赞且不是自己的帖子)
if (isLiked && post.UserId != currentUserId)
{
try
{
await _messagePublisher.SendLikeMessageAsync(
triggerId: currentUserId,
receiverId: post.UserId,
contentType: 1, // 帖子类型
contentId: post.Id,
postTitle: post.Title,
commentContent: null
);
}
catch (Exception ex)
{
// 记录日志但不影响点赞操作
Console.WriteLine($"发送帖子点赞消息失败: {ex.Message}");
}
}
// 5. 构建返回数据
var result = new LikePostRespDto
{
PostId = post.Id,
LikeCount = likeCount,
IsLiked = isLiked
};
return new BaseResponse<LikePostRespDto>(isLiked ? "点赞成功!" : "取消点赞成功!", result);
}
/// <summary>
/// 获取我点赞的帖子列表
/// </summary>
/// <param name="request">请求参数</param>
/// <returns></returns>
public async Task<BaseResponse<GetLikedPostsRespDto>> GetLikedPosts(GetLikedPostsReq request)
{
var currentUserId = (long)_userInfoModel.UserId;
// 1. 获取点赞的帖子ID列表
var likedLikes = await _likesRepository.Select
.Where(x => x.UserId == currentUserId && x.TargetType == 1)
.OrderByDescending(x => x.CreatedAt)
.Skip((request.PageIndex - 1) * request.PageSize)
.Take(request.PageSize)
.ToListAsync();
var likedPostIds = likedLikes.Select(x => x.TargetId).ToList();
var total = await _likesRepository.Select
.Where(x => x.UserId == currentUserId && x.TargetType == 1)
.CountAsync();
if (!likedPostIds.Any())
{
var emptyResult = new GetLikedPostsRespDto
{
PageIndex = request.PageIndex,
PageSize = request.PageSize,
Total = (int)total,
TotalPages = (int)Math.Ceiling((double)total / request.PageSize),
Items = new List<LikedPostListItemDto>()
};
return new BaseResponse<GetLikedPostsRespDto>(emptyResult);
}
// 2. 获取帖子信息
var posts = await _postsRepository.Select
.Where(x => likedPostIds.Contains(x.Id) && !x.IsDeleted)
.ToListAsync();
// 3. 构建返回数据复用GetPosts的逻辑优先从缓存获取
var postImages = await GetPostImagesFromCacheAsync(likedPostIds);
// 找出缓存未命中的帖子ID从数据库查询并写入缓存
var cachedPostIds = postImages.Select(x => x.PostId).Distinct().ToList();
var uncachedPostIds = likedPostIds.Except(cachedPostIds).ToList();
if (uncachedPostIds.Any())
{
var dbPostImages = await _postImagesRepository.Select
.Where(x => uncachedPostIds.Contains(x.PostId))
.OrderBy(x => x.PostId)
.OrderBy(x => x.SortOrder)
.ToListAsync();
// 将数据库查询结果添加到结果集
postImages.AddRange(dbPostImages);
// 批量写入缓存
await SetPostImagesToCacheAsync(dbPostImages);
}
// 按PostId和SortOrder排序
postImages = postImages
.OrderBy(x => x.PostId)
.ThenBy(x => x.SortOrder)
.ToList();
var categoryIds = posts.Where(x => x.CategoryId.HasValue).Select(x => x.CategoryId!.Value).Distinct().ToList();
var categories = await _postCategoriesRepository.Select
.Where(x => categoryIds.Contains(x.Id))
.ToListAsync();
var userIds = posts.Select(x => x.UserId).Distinct().ToList();
var users = await _usersRepository.Select
.Where(x => userIds.Contains(x.Id))
.ToListAsync();
var userLevelIds = users.Select(x => x.LevelId).Distinct().ToList();
var userLevels = await _userLevelsRepository.Select
.Where(x => userLevelIds.Contains(x.Id))
.ToListAsync();
var followedUserIds = await _followsRepository.Select
.Where(x => x.FollowerId == currentUserId && userIds.Contains(x.FollowedUserId))
.ToListAsync();
// 4. 构建帖子列表
var items = posts.Select(post =>
{
var user = users.FirstOrDefault(x => x.Id == post.UserId);
var likedAt = likedLikes.FirstOrDefault(x => x.TargetId == post.Id)?.CreatedAt ?? DateTime.Now;
return new LikedPostListItemDto
{
PostId = post.Id,
Title = post.Title,
CoverImage = post.CoverImage,
LikeCount = post.LikeCount,
CommentCount = post.CommentCount,
LikedAt = likedAt,
User = new LikedPostAuthorDto
{
UserId = user?.Id ?? 0,
NickName = user?.NickName ?? "",
Avatar = user?.Avatar ?? ""
}
};
}).ToList();
var result = new GetLikedPostsRespDto
{
PageIndex = request.PageIndex,
PageSize = request.PageSize,
Total = (int)total,
TotalPages = (int)Math.Ceiling((double)total / request.PageSize),
Items = items
};
return new BaseResponse<GetLikedPostsRespDto>(result);
}
/// <summary>
/// 修改帖子回复权限
/// </summary>
public async Task<BaseResponse<UpdateReplyPermissionRespDto>> UpdateReplyPermission(UpdateReplyPermissionReq request)
{
var currentUserId = (long)_userInfoModel.UserId;
// 1. 根据 PostId 查询帖子
var post = await _postsRepository.Select
.Where(x => x.Id == request.PostId && !x.IsDeleted)
.FirstAsync();
if (post == null)
{
return new BaseResponse<UpdateReplyPermissionRespDto>(ResponseCode.NotFound, "帖子不存在");
}
// 2. 校验当前用户是否为帖子作者
if (post.UserId != currentUserId)
{
return new BaseResponse<UpdateReplyPermissionRespDto>(ResponseCode.Forbidden, "权限不足,仅帖子作者可修改回复设置");
}
// 3. 更新 AllowReply 字段
post.AllowReply = request.AllowReply;
await _postsRepository.UpdateAsync(post);
// 4. 返回更新后的状态
var result = new UpdateReplyPermissionRespDto
{
PostId = post.Id,
AllowReply = post.AllowReply
};
return new BaseResponse<UpdateReplyPermissionRespDto>(result);
}
/// <summary>
/// 判断指定认证类型是否为管理员认证
/// </summary>
/// <param name="certificationTypeId">认证类型ID</param>
/// <returns>是否为管理员认证</returns>
private async Task<bool> IsAdminCertificationType(int certificationTypeId)
{
var certType = await _certificationTypesRepository.Select
.Where(x => x.Id == certificationTypeId && x.IsActive)
.FirstAsync();
return certType?.Name == "管理员认证";
}
#region
/// <summary>
/// 获取缓存键
/// </summary>
private string GetPostImageCacheKey(long postId)
{
return $"{POST_IMAGE_CACHE_KEY_PREFIX}{postId}";
}
/// <summary>
/// 批量从Redis缓存获取帖子图片
/// </summary>
private async Task<List<T_PostImages>> GetPostImagesFromCacheAsync(List<long> postIds)
{
var result = new List<T_PostImages>();
if (postIds == null || !postIds.Any())
{
return result;
}
try
{
var db = await _redisService.GetDatabaseAsync();
var tasks = postIds.Select(postId =>
{
var cacheKey = GetPostImageCacheKey(postId);
return db.StringGetAsync(cacheKey);
}).ToArray();
var values = await Task.WhenAll(tasks);
for (int i = 0; i < postIds.Count; i++)
{
if (values[i].HasValue)
{
try
{
var images = System.Text.Json.JsonSerializer.Deserialize<List<T_PostImages>>(values[i].ToString());
if (images != null && images.Any())
{
result.AddRange(images);
}
}
catch (Exception ex)
{
// 反序列化失败,忽略该缓存,后续会从数据库查询
// 可以记录日志
System.Diagnostics.Debug.WriteLine($"反序列化帖子图片缓存失败PostId: {postIds[i]}, Error: {ex.Message}");
}
}
}
}
catch (Exception ex)
{
// Redis异常时返回空列表后续会从数据库查询
System.Diagnostics.Debug.WriteLine($"从Redis获取帖子图片缓存失败: {ex.Message}");
}
return result;
}
/// <summary>
/// 批量将帖子图片写入Redis缓存
/// </summary>
private async Task SetPostImagesToCacheAsync(List<T_PostImages> postImages)
{
if (postImages == null || !postImages.Any())
{
return;
}
try
{
// 按帖子ID分组
var groupedImages = postImages.GroupBy(x => x.PostId).ToList();
var db = await _redisService.GetDatabaseAsync();
var tasks = groupedImages.Select(group =>
{
var cacheKey = GetPostImageCacheKey(group.Key);
var imagesList = group.OrderBy(x => x.SortOrder).ToList();
var jsonValue = System.Text.Json.JsonSerializer.Serialize(imagesList);
return db.StringSetAsync(cacheKey, jsonValue, POST_IMAGE_CACHE_EXPIRATION);
}).ToArray();
await Task.WhenAll(tasks);
}
catch (Exception ex)
{
// Redis异常时记录日志但不影响主流程
System.Diagnostics.Debug.WriteLine($"写入帖子图片缓存失败: {ex.Message}");
}
}
/// <summary>
/// 清除指定帖子的图片缓存
/// </summary>
private async Task ClearPostImageCacheAsync(long postId)
{
try
{
var cacheKey = GetPostImageCacheKey(postId);
await _redisService.RemoveAsync(cacheKey);
}
catch (Exception ex)
{
// Redis异常时记录日志但不影响主流程
System.Diagnostics.Debug.WriteLine($"清除帖子图片缓存失败PostId: {postId}, Error: {ex.Message}");
}
}
#endregion
}
}