1078 lines
43 KiB
C#
1078 lines
43 KiB
C#
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
|
||
}
|
||
}
|