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

605 lines
25 KiB
C#
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.SensitiveWord.Interfaces;
using LiveForum.IService.Messages;
using LiveForum.IService.Posts;
using LiveForum.IService.Others;
using LiveForum.IService.Users;
using LiveForum.Model;
using LiveForum.Model.Dto.Base;
using LiveForum.Model.Dto.Messages;
using LiveForum.Model.Dto.PostComments;
using LiveForum.Model.Dto.Users;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LiveForum.Service.Posts
{
public class PostCommentsService : IPostCommentsService
{
private readonly JwtUserInfoModel _userInfoModel;
private readonly IBaseRepository<T_Comments> _commentsRepository;
private readonly IBaseRepository<T_Posts> _postsRepository;
private readonly IBaseRepository<T_Users> _usersRepository;
private readonly IBaseRepository<T_Likes> _likesRepository;
private readonly ISensitiveWordService _sensitiveWordService;
private readonly IUserInfoService _userInfoService;
private readonly IMessagePublisher _messagePublisher;
private readonly ILikeService _likeService;
private readonly IPostReplyIntervalService _postReplyIntervalService;
private readonly IAntiAddictionService _antiAddictionService;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="userInfoModel">JWT用户信息模型</param>
/// <param name="commentsRepository">评论仓储</param>
/// <param name="postsRepository">帖子仓储</param>
/// <param name="usersRepository">用户仓储</param>
/// <param name="likesRepository">点赞仓储</param>
/// <param name="sensitiveWordService">敏感词服务</param>
/// <param name="userInfoService">用户信息转换服务</param>
/// <param name="messagePublisher">消息发布器</param>
/// <param name="likeService">点赞服务</param>
/// <param name="postReplyIntervalService">发帖/回复间隔校验服务</param>
/// <param name="antiAddictionService">防沉迷校验服务</param>
public PostCommentsService(
JwtUserInfoModel userInfoModel,
IBaseRepository<T_Comments> commentsRepository,
IBaseRepository<T_Posts> postsRepository,
IBaseRepository<T_Users> usersRepository,
IBaseRepository<T_Likes> likesRepository,
ISensitiveWordService sensitiveWordService,
IUserInfoService userInfoService,
IMessagePublisher messagePublisher,
ILikeService likeService,
IPostReplyIntervalService postReplyIntervalService,
IAntiAddictionService antiAddictionService)
{
_userInfoModel = userInfoModel;
_commentsRepository = commentsRepository;
_postsRepository = postsRepository;
_usersRepository = usersRepository;
_likesRepository = likesRepository;
_sensitiveWordService = sensitiveWordService;
_userInfoService = userInfoService;
_messagePublisher = messagePublisher;
_likeService = likeService;
_postReplyIntervalService = postReplyIntervalService;
_antiAddictionService = antiAddictionService;
}
/// <summary>
/// 获取评论列表
/// </summary>
/// <param name="request">请求参数</param>
/// <returns></returns>
public async Task<BaseResponse<GetPostCommentsRespDto>> GetPostComments(GetPostCommentsReq 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<GetPostCommentsRespDto>(ResponseCode.Error, "帖子不存在");
}
// 2. 构建查询条件 - 只获取顶级评论ParentCommentId为null
var query = _commentsRepository.Select
.Where(x => x.PostId == request.PostId && x.ParentCommentId == null && !x.IsDeleted);
// 3. 排序
if (request.SortType == 1) // 正序
{
query = query.OrderBy(x => x.CreatedAt);
}
else // 倒序
{
query = query.OrderByDescending(x => x.CreatedAt);
}
// 4. 分页
var total = await query.CountAsync();
var comments = await query
.Skip((request.PageIndex - 1) * request.PageSize)
.Take(request.PageSize)
.ToListAsync();
if (!comments.Any())
{
var res = new GetPostCommentsRespDto
{
PageIndex = request.PageIndex,
PageSize = request.PageSize,
Total = (int)total,
TotalPages = (int)Math.Ceiling((double)total / request.PageSize),
Items = new List<CommentListItemDto>()
};
return new BaseResponse<GetPostCommentsRespDto>(res);
}
// 5. 获取相关数据
var commentIds = comments.Select(x => x.Id).ToList();
var userIds = comments.Select(x => x.UserId).Distinct().ToList();
// 获取用户信息
//var users = await _usersRepository.Select
// .Where(x => userIds.Contains(x.Id))
// .ToListAsync();
// 获取回复数量
var replyCounts = await _commentsRepository.Select
.Where(x => commentIds.Contains(x.ParentCommentId!.Value) && !x.IsDeleted)
.GroupBy(x => x.ParentCommentId)
.ToListAsync(g => new { ParentCommentId = g.Key, Count = g.Count() });
var replyCountDict = replyCounts.ToDictionary(x => x.ParentCommentId!.Value, x => x.Count);
// 获取每个顶级评论的前5条回复
var replies = await _commentsRepository.Select
.Where(x => commentIds.Contains(x.ParentCommentId!.Value) && !x.IsDeleted)
.OrderBy(x => x.CreatedAt)
.ToListAsync();
// 按父评论ID分组每组取前5条
var replyDict = replies
.GroupBy(x => x.ParentCommentId!.Value)
.ToDictionary(
g => g.Key,
g => g.Take(5).ToList()
);
// 收集所有相关的用户ID包括回复者和被回复用户
var replyUserIds = replies.Select(x => x.UserId).Distinct().ToList();
var replyToUserIds = replies.Where(x => x.ReplyToUserId.HasValue)
.Select(x => x.ReplyToUserId!.Value).Distinct().ToList();
var allUserIds = userIds.Union(replyUserIds).Union(replyToUserIds).Distinct().ToList();
// 重新获取所有用户信息
var users = await _usersRepository.Select
.Where(x => allUserIds.Contains(x.Id))
.ToListAsync();
// 获取所有评论和回复的ID用于查询点赞状态
var allCommentIds = commentIds.Union(replies.Select(x => x.Id)).ToList();
// 批量查询点赞状态和点赞数(包括顶级评论和回复)
var likedStatusDict = await _likeService.BatchIsLikedAsync(currentUserId, 2, allCommentIds);
var likeCountDict = await _likeService.BatchGetLikeCountAsync(2, allCommentIds);
// 6. 批量转换用户信息(自动获取认证类型数据)
var usersDict = await _userInfoService.ToUserInfoDtoDictionaryAsync(users);
// 7. 构建评论列表
var items = comments.Select(comment =>
{
var user = users.FirstOrDefault(x => x.Id == comment.UserId);
var replyCount = replyCountDict.GetValueOrDefault(comment.Id, 0);
// 获取该评论的前5条回复
var commentReplies = replyDict.GetValueOrDefault(comment.Id, new List<T_Comments>());
var replyDtos = commentReplies.Select(reply =>
{
var replyUser = users.FirstOrDefault(x => x.Id == reply.UserId);
var replyToUser = reply.ReplyToUserId.HasValue
? users.FirstOrDefault(x => x.Id == reply.ReplyToUserId.Value)
: null;
return new CommentReplyDto
{
CommentId = reply.Id,
ParentCommentId = reply.ParentCommentId!.Value,
Content = reply.Content,
LikeCount = likeCountDict.ContainsKey(reply.Id) ? likeCountDict[reply.Id] : 0,
CreatedAt = reply.CreatedAt,
ReplyToUserId = reply.ReplyToUserId,
ReplyToUserName = replyToUser?.NickName ?? "",
User = replyUser != null && usersDict.ContainsKey(replyUser.Id) ? usersDict[replyUser.Id] : new UserInfoDto(),
IsLiked = likedStatusDict.ContainsKey(reply.Id) && likedStatusDict[reply.Id]
};
}).ToList();
return new CommentListItemDto
{
CommentId = comment.Id,
PostId = comment.PostId,
Content = comment.Content,
LikeCount = likeCountDict.ContainsKey(comment.Id) ? likeCountDict[comment.Id] : 0,
ReplyCount = replyCount,
CreatedAt = comment.CreatedAt,
User = user != null && usersDict.ContainsKey(user.Id) ? usersDict[user.Id] : new UserInfoDto(),
IsLiked = likedStatusDict.ContainsKey(comment.Id) && likedStatusDict[comment.Id],
Replies = replyDtos // 显示前5条回复
};
}).ToList();
var result = new GetPostCommentsRespDto
{
PageIndex = request.PageIndex,
PageSize = request.PageSize,
Total = (int)total,
TotalPages = (int)Math.Ceiling((double)total / request.PageSize),
Items = items
};
return new BaseResponse<GetPostCommentsRespDto>(result);
}
/// <summary>
/// 获取评论回复列表
/// </summary>
/// <param name="request">请求参数</param>
/// <returns></returns>
public async Task<BaseResponse<GetCommentsRepliesRespDto>> GetCommentsReplies(GetCommentsRepliesReq request)
{
var currentUserId = (long)_userInfoModel.UserId;
// 1. 验证父评论是否存在
var parentComment = await _commentsRepository.Select
.Where(x => x.Id == request.CommentId && !x.IsDeleted)
.FirstAsync();
if (parentComment == null)
{
return new BaseResponse<GetCommentsRepliesRespDto>(ResponseCode.Error, "评论不存在");
}
// 2. 构建查询条件 - 获取该评论的所有回复
var query = _commentsRepository.Select
.Where(x => x.ParentCommentId == request.CommentId && !x.IsDeleted)
.OrderBy(x => x.CreatedAt);
// 3. 分页
var total = await query.CountAsync();
var replies = await query
.Skip((request.PageIndex - 1) * request.PageSize)
.Take(request.PageSize)
.ToListAsync();
if (!replies.Any())
{
var res = new GetCommentsRepliesRespDto
{
PageIndex = request.PageIndex,
PageSize = request.PageSize,
Total = (int)total,
TotalPages = (int)Math.Ceiling((double)total / request.PageSize),
Items = new List<CommentReplyDto>()
};
return new BaseResponse<GetCommentsRepliesRespDto>(res);
}
// 4. 获取相关数据
var replyIds = replies.Select(x => x.Id).ToList();
var userIds = replies.Select(x => x.UserId).Distinct().ToList();
// 获取用户信息
var users = await _usersRepository.Select
.Where(x => userIds.Contains(x.Id))
.ToListAsync();
// 批量查询点赞状态和点赞数
var likedStatusDict = await _likeService.BatchIsLikedAsync(currentUserId, 2, replyIds);
var likeCountDict = await _likeService.BatchGetLikeCountAsync(2, replyIds);
// 5. 批量转换用户信息(自动获取认证类型数据)
var usersDict = await _userInfoService.ToUserInfoDtoDictionaryAsync(users);
// 6. 构建回复列表
var items = replies.Select(reply =>
{
var user = users.FirstOrDefault(x => x.Id == reply.UserId);
var replyToUser = reply.ReplyToUserId.HasValue ?
users.FirstOrDefault(x => x.Id == reply.ReplyToUserId.Value) : null;
return new CommentReplyDto
{
CommentId = reply.Id,
ParentCommentId = reply.ParentCommentId,
ReplyToUserId = reply.ReplyToUserId,
ReplyToUserName = replyToUser?.NickName ?? "",
Content = reply.Content,
LikeCount = likeCountDict.ContainsKey(reply.Id) ? likeCountDict[reply.Id] : 0,
CreatedAt = reply.CreatedAt,
User = user != null && usersDict.ContainsKey(user.Id) ? usersDict[user.Id] : new UserInfoDto(),
IsLiked = likedStatusDict.ContainsKey(reply.Id) && likedStatusDict[reply.Id]
};
}).ToList();
var result = new GetCommentsRepliesRespDto
{
PageIndex = request.PageIndex,
PageSize = request.PageSize,
Total = (int)total,
TotalPages = (int)Math.Ceiling((double)total / request.PageSize),
Items = items
};
return new BaseResponse<GetCommentsRepliesRespDto>(result);
}
/// <summary>
/// 发表评论
/// </summary>
/// <param name="request">请求参数</param>
/// <returns></returns>
public async Task<BaseResponse<PublishPostCommentsRespDto>> PublishPostComments(PublishPostCommentsReq request)
{
var currentUserId = (long)_userInfoModel.UserId;
var now = DateTime.Now;
if (string.IsNullOrEmpty(request.Content))
{
return new BaseResponse<PublishPostCommentsRespDto>(ResponseCode.Error, "评论内容不能为空");
}
// 0. 过滤敏感词
request.Content = _sensitiveWordService.Filter(request.Content);
if (string.IsNullOrEmpty(request.Content.Replace("*", "")))
{
return new BaseResponse<PublishPostCommentsRespDto>(ResponseCode.Error, "评论内容包含敏感词");
}
// 1. 验证帖子是否存在
var post = await _postsRepository.Select
.Where(x => x.Id == request.PostId)
.FirstAsync();
if (post == null)
{
return new BaseResponse<PublishPostCommentsRespDto>(ResponseCode.Error, "帖子不存在");
}
if (post.IsDeleted)
{
return new BaseResponse<PublishPostCommentsRespDto>(ResponseCode.PostDeleted, "帖子已删除");
}
// 1.1 检查帖子是否允许回复
if (!post.AllowReply)
{
return new BaseResponse<PublishPostCommentsRespDto>(ResponseCode.Error, "该帖子不允许回复");
}
// 1.2 回复间隔校验
var remainingSeconds = await _postReplyIntervalService.CheckReplyIntervalAsync(currentUserId);
if (remainingSeconds.HasValue)
{
return new BaseResponse<PublishPostCommentsRespDto>(
ResponseCode.Error,
$"回复过于频繁,请等待 {remainingSeconds.Value} 秒后再试");
}
// 1.3 防沉迷校验
if (await _antiAddictionService.IsRestrictedAsync("Reply"))
{
return new BaseResponse<PublishPostCommentsRespDto>(ResponseCode.Error, "防沉迷时间内无法操作");
}
// 2. 如果是回复评论,验证父评论是否存在
if (request.ParentCommentId.HasValue && request.ParentCommentId > 0)
{
var parentComment = await _commentsRepository.Select
.Where(x => x.Id == request.ParentCommentId.Value && !x.IsDeleted)
.FirstAsync();
if (parentComment == null)
{
return new BaseResponse<PublishPostCommentsRespDto>(ResponseCode.Error, "父评论不存在");
}
// 验证父评论是否属于同一个帖子
if (parentComment.PostId != request.PostId)
{
return new BaseResponse<PublishPostCommentsRespDto>(ResponseCode.Error, "评论帖子不匹配");
}
if (request.ReplyToUserId == null || request.ReplyToUserId == 0)
{
request.ReplyToUserId = parentComment.UserId;
}
}
// 4. 创建评论
var comment = new T_Comments
{
PostId = request.PostId,
UserId = currentUserId,
ParentCommentId = request.ParentCommentId,
ReplyToUserId = request.ReplyToUserId,
Content = request.Content,
LikeCount = 0,
CreatedAt = now,
UpdatedAt = now
};
await _commentsRepository.InsertAsync(comment);
var commentCount = await _commentsRepository.Where(it => !it.IsDeleted && it.PostId == request.PostId).CountAsync();
// 5. 更新帖子的评论数
post.CommentCount = (int)commentCount;
await _postsRepository.UpdateAsync(post);
// 6. 发送回复消息
try
{
if (request.ParentCommentId.HasValue && request.ParentCommentId.Value > 0)
{
// 回复评论场景
var parentComment = await _commentsRepository.Select
.Where(x => x.Id == request.ParentCommentId.Value)
.FirstAsync();
if (parentComment != null && parentComment.UserId != currentUserId)
{
await _messagePublisher.SendReplyMessageAsync(
triggerId: currentUserId,
receiverId: parentComment.UserId,
commentId: comment.Id,
commentContent: comment.Content,
postId: post.Id,
postTitle: post.Title,
parentCommentId: parentComment.Id,
myCommentContent: parentComment.Content // 传入被回复的评论内容
);
}
}
else if (post.UserId != currentUserId)
{
// 直接评论帖子场景
await _messagePublisher.SendReplyMessageAsync(
triggerId: currentUserId,
receiverId: post.UserId,
commentId: comment.Id,
commentContent: comment.Content,
postId: post.Id,
postTitle: post.Title,
parentCommentId: null,
myCommentContent: null
);
}
}
catch (Exception ex)
{
// 记录日志但不影响评论发布
Console.WriteLine($"发送回复消息失败: {ex.Message}");
}
// 7. 构建返回数据
var replyInterval = await _postReplyIntervalService.GetReplyIntervalAsync(currentUserId);
var result = new PublishPostCommentsRespDto
{
CommentId = comment.Id,
PostId = comment.PostId,
Content = comment.Content,
CreatedAt = comment.CreatedAt,
ReplyInterval = replyInterval
};
return new BaseResponse<PublishPostCommentsRespDto>("发送评论成功!", result);
}
/// <summary>
/// 删除评论
/// </summary>
/// <param name="request">请求参数</param>
/// <returns></returns>
public async Task<BaseResponseBool> DeletePostComments(DeletePostCommentsReq request)
{
var currentUserId = (long)_userInfoModel.UserId;
// 1. 获取评论信息
var comment = await _commentsRepository.Select
.Where(x => x.Id == request.CommentId && x.UserId == currentUserId && !x.IsDeleted)
.FirstAsync();
if (comment == null)
{
return new BaseResponseBool { Code = ResponseCode.Error, Message = "评论不存在或无权限删除" };
}
// 2. 软删除评论
comment.IsDeleted = true;
comment.DeletedAt = DateTime.Now;
await _commentsRepository.UpdateAsync(comment);
// 3. 更新帖子的评论数
var post = await _postsRepository.Select
.Where(x => x.Id == comment.PostId)
.FirstAsync();
if (post != null)
{
post.CommentCount = Math.Max(0, post.CommentCount - 1);
await _postsRepository.UpdateAsync(post);
}
return new BaseResponseBool { Code = ResponseCode.Success, Data = true, Message = "删除帖子成功!" };
}
/// <summary>
/// 点赞/取消点赞评论
/// </summary>
/// <param name="request">请求参数</param>
/// <returns></returns>
public async Task<BaseResponse<LikeCommentRespDto>> LikeComment(LikeCommentReq request)
{
var currentUserId = (long)_userInfoModel.UserId;
// 1. 检查评论是否存在
var comment = await _commentsRepository.Select
.Where(x => x.Id == request.CommentId && !x.IsDeleted)
.FirstAsync();
if (comment == null)
{
return new BaseResponse<LikeCommentRespDto>(ResponseCode.Error, "评论不存在");
}
// 2. 查询当前点赞状态
var isCurrentlyLiked = await _likeService.IsLikedAsync(currentUserId, 2, request.CommentId);
bool isLiked;
if (isCurrentlyLiked)
{
// 取消点赞
await _likeService.UnlikeAsync(currentUserId, 2, request.CommentId);
isLiked = false;
}
else
{
// 添加点赞
await _likeService.LikeAsync(currentUserId, 2, request.CommentId, comment.UserId);
isLiked = true;
}
// 3. 获取最新点赞数
var likeCount = await _likeService.GetLikeCountAsync(2, request.CommentId);
// 4. 发送点赞消息(如果是点赞且不是自己的评论)
if (isLiked && comment.UserId != currentUserId)
{
try
{
// 获取帖子信息
var post = await _postsRepository.Select
.Where(x => x.Id == comment.PostId)
.FirstAsync();
if (post != null)
{
await _messagePublisher.SendLikeMessageAsync(
triggerId: currentUserId,
receiverId: comment.UserId,
contentType: 2, // 评论类型
contentId: comment.Id,
postTitle: post.Title,
commentContent: comment.Content
);
}
}
catch (Exception ex)
{
// 记录日志但不影响点赞操作
Console.WriteLine($"发送评论点赞消息失败: {ex.Message}");
}
}
// 5. 构建返回数据
var result = new LikeCommentRespDto
{
CommentId = comment.Id,
IsLiked = isLiked,
LikeCount = likeCount
};
return new BaseResponse<LikeCommentRespDto>(isLiked ? "点赞成功!" : "取消点赞成功!", result);
}
}
}