feat(comment): 新增评论屏蔽关系功能

本次提交完整实现了评论屏蔽关系模块,包括:
- 屏蔽关系实体 KeyboardCommentBlockRelation
- 屏蔽请求 DTO CommentBlockReq
- 屏蔽用户 VO CommentBlockedUserVO
- 控制器、服务层及 MyBatis 映射文件
This commit is contained in:
2026-03-23 11:08:33 +08:00
parent db38fe819c
commit 1fa24f7e34
8 changed files with 446 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
package com.yolo.keyborad.controller;
import cn.dev33.satoken.stp.StpUtil;
import com.yolo.keyborad.common.BaseResponse;
import com.yolo.keyborad.common.ErrorCode;
import com.yolo.keyborad.common.ResultUtils;
import com.yolo.keyborad.exception.BusinessException;
import com.yolo.keyborad.model.dto.comment.CommentBlockReq;
import com.yolo.keyborad.model.vo.CommentBlockedUserVO;
import com.yolo.keyborad.service.KeyboardCommentBlockRelationService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/*
* @author: ziin
* @date: 2026/3/23
*/
@RestController
@RequestMapping("/ai-companion/comment/block")
@Tag(name = "AI陪聊角色评论拉黑", description = "AI陪聊角色评论区拉黑管理接口")
public class AiCompanionCommentBlockController {
@Resource
private KeyboardCommentBlockRelationService commentBlockRelationService;
@PostMapping("/add")
@Operation(summary = "拉黑评论用户", description = "在评论区拉黑指定用户")
public BaseResponse<Boolean> blockUser(@RequestBody CommentBlockReq req) {
Long blockedUserId = validateBlockedUserId(req);
Long blockerUserId = StpUtil.getLoginIdAsLong();
commentBlockRelationService.blockUser(blockerUserId, blockedUserId);
return ResultUtils.success(true);
}
@PostMapping("/cancel")
@Operation(summary = "取消拉黑评论用户", description = "取消在评论区对指定用户的拉黑")
public BaseResponse<Boolean> unblockUser(@RequestBody CommentBlockReq req) {
Long blockedUserId = validateBlockedUserId(req);
Long blockerUserId = StpUtil.getLoginIdAsLong();
commentBlockRelationService.unblockUser(blockerUserId, blockedUserId);
return ResultUtils.success(true);
}
@GetMapping("/list")
@Operation(summary = "查询已拉黑用户列表", description = "查询当前登录用户在评论区已拉黑的用户列表")
public BaseResponse<List<CommentBlockedUserVO>> listBlockedUsers() {
Long blockerUserId = StpUtil.getLoginIdAsLong();
return ResultUtils.success(commentBlockRelationService.listBlockedUsers(blockerUserId));
}
private Long validateBlockedUserId(CommentBlockReq req) {
if (req == null || req.getBlockedUserId() == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "被拉黑用户ID不能为空");
}
return req.getBlockedUserId();
}
}

View File

@@ -0,0 +1,12 @@
package com.yolo.keyborad.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yolo.keyborad.model.entity.KeyboardCommentBlockRelation;
/*
* @author: ziin
* @date: 2026/3/23 10:30
*/
public interface KeyboardCommentBlockRelationMapper extends BaseMapper<KeyboardCommentBlockRelation> {
}

View File

@@ -0,0 +1,16 @@
package com.yolo.keyborad.model.dto.comment;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/*
* @author: ziin
* @date: 2026/3/23
*/
@Data
@Schema(description = "评论区拉黑请求")
public class CommentBlockReq {
@Schema(description = "被拉黑用户ID", requiredMode = Schema.RequiredMode.REQUIRED)
private Long blockedUserId;
}

View File

@@ -0,0 +1,92 @@
package com.yolo.keyborad.model.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.Date;
import lombok.Data;
/*
* @author: ziin
* @date: 2026/3/23 10:30
*/
/**
* 评论区拉黑关系表
*/
@Schema(description="评论区拉黑关系表")
@Data
@TableName(value = "keyboard_comment_block_relation")
public class KeyboardCommentBlockRelation {
/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
@Schema(description="主键")
private Long id;
/**
* 拉黑发起人
*/
@TableField(value = "blocker_user_id")
@Schema(description="拉黑发起人")
private Long blockerUserId;
/**
* 被拉黑用户
*/
@TableField(value = "blocked_user_id")
@Schema(description="被拉黑用户")
private Long blockedUserId;
/**
* 作用域类型: 1评论区全局 2帖子 3圈子 4直播间
*/
@TableField(value = "scope_type")
@Schema(description="作用域类型: 1评论区全局 2帖子 3圈子 4直播间")
private Short scopeType;
/**
* 作用域ID, 全局为0
*/
@TableField(value = "scope_id")
@Schema(description="作用域ID, 全局为0")
private Long scopeId;
/**
* 状态: 1有效 0取消
*/
@TableField(value = "\"status\"")
@Schema(description="状态: 1有效 0取消")
private Short status;
/**
* 来源: 1用户操作 2风控 3管理后台
*/
@TableField(value = "\"source\"")
@Schema(description="来源: 1用户操作 2风控 3管理后台")
private Short source;
/**
* 创建时间
*/
@TableField(value = "created_at")
@Schema(description="创建时间")
private Date createdAt;
/**
* 更新时间
*/
@TableField(value = "updated_at")
@Schema(description="更新时间")
private Date updatedAt;
/**
* 删除时间
*/
@TableField(value = "deleted_at")
@Schema(description="删除时间")
private Date deletedAt;
}

View File

@@ -0,0 +1,30 @@
package com.yolo.keyborad.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.Date;
/*
* @author: ziin
* @date: 2026/3/23
*/
@Data
@Schema(description = "评论区已拉黑用户")
public class CommentBlockedUserVO {
@Schema(description = "用户ID")
private Long userId;
@Schema(description = "用户UID")
private Long userUid;
@Schema(description = "用户昵称")
private String userName;
@Schema(description = "用户头像")
private String userAvatar;
@Schema(description = "拉黑时间")
private Date blockedAt;
}

View File

@@ -0,0 +1,21 @@
package com.yolo.keyborad.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.yolo.keyborad.model.entity.KeyboardCommentBlockRelation;
import com.yolo.keyborad.model.vo.CommentBlockedUserVO;
import java.util.List;
/*
* @author: ziin
* @date: 2026/3/23 10:30
*/
public interface KeyboardCommentBlockRelationService extends IService<KeyboardCommentBlockRelation> {
void blockUser(Long blockerUserId, Long blockedUserId);
void unblockUser(Long blockerUserId, Long blockedUserId);
List<CommentBlockedUserVO> listBlockedUsers(Long blockerUserId);
}

View File

@@ -0,0 +1,187 @@
package com.yolo.keyborad.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yolo.keyborad.common.ErrorCode;
import com.yolo.keyborad.exception.BusinessException;
import com.yolo.keyborad.mapper.KeyboardCommentBlockRelationMapper;
import com.yolo.keyborad.model.entity.KeyboardCommentBlockRelation;
import com.yolo.keyborad.model.entity.KeyboardUser;
import com.yolo.keyborad.model.vo.CommentBlockedUserVO;
import com.yolo.keyborad.service.KeyboardCommentBlockRelationService;
import com.yolo.keyborad.service.UserService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
/*
* @author: ziin
* @date: 2026/3/23 10:30
*/
@Service
public class KeyboardCommentBlockRelationServiceImpl extends ServiceImpl<KeyboardCommentBlockRelationMapper, KeyboardCommentBlockRelation> implements KeyboardCommentBlockRelationService {
private static final short GLOBAL_SCOPE_TYPE = 1;
private static final long GLOBAL_SCOPE_ID = 0L;
private static final short ACTIVE_STATUS = 1;
private static final short CANCELED_STATUS = 0;
private static final short USER_OPERATION_SOURCE = 1;
@Resource
private UserService userService;
@Override
@Transactional(rollbackFor = Exception.class)
public void blockUser(Long blockerUserId, Long blockedUserId) {
validateUserIds(blockerUserId, blockedUserId);
validateBlockedUserExists(blockedUserId);
KeyboardCommentBlockRelation relation = getRelation(blockerUserId, blockedUserId);
if (relation == null) {
saveNewRelation(blockerUserId, blockedUserId);
return;
}
if (isActive(relation)) {
return;
}
reactivateRelation(relation);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void unblockUser(Long blockerUserId, Long blockedUserId) {
validateUserIds(blockerUserId, blockedUserId);
KeyboardCommentBlockRelation relation = getRelation(blockerUserId, blockedUserId);
if (relation == null || !isActive(relation)) {
return;
}
deactivateRelation(relation);
}
@Override
public List<CommentBlockedUserVO> listBlockedUsers(Long blockerUserId) {
if (blockerUserId == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
List<KeyboardCommentBlockRelation> relations = listActiveRelations(blockerUserId);
if (relations.isEmpty()) {
return List.of();
}
Map<Long, KeyboardUser> userMap = getBlockedUserMap(relations);
return relations.stream()
.map(relation -> toBlockedUserVO(relation, userMap.get(relation.getBlockedUserId())))
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
private void validateUserIds(Long blockerUserId, Long blockedUserId) {
if (blockerUserId == null || blockedUserId == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
if (blockerUserId.equals(blockedUserId)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "不能拉黑自己");
}
}
private void validateBlockedUserExists(Long blockedUserId) {
if (userService.getById(blockedUserId) == null) {
throw new BusinessException(ErrorCode.USER_NOT_FOUND);
}
}
private KeyboardCommentBlockRelation getRelation(Long blockerUserId, Long blockedUserId) {
LambdaQueryWrapper<KeyboardCommentBlockRelation> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(KeyboardCommentBlockRelation::getBlockerUserId, blockerUserId)
.eq(KeyboardCommentBlockRelation::getBlockedUserId, blockedUserId)
.eq(KeyboardCommentBlockRelation::getScopeType, GLOBAL_SCOPE_TYPE)
.eq(KeyboardCommentBlockRelation::getScopeId, GLOBAL_SCOPE_ID)
.last("limit 1");
return this.getOne(queryWrapper, false);
}
private void saveNewRelation(Long blockerUserId, Long blockedUserId) {
Date now = new Date();
KeyboardCommentBlockRelation relation = new KeyboardCommentBlockRelation();
relation.setBlockerUserId(blockerUserId);
relation.setBlockedUserId(blockedUserId);
relation.setScopeType(GLOBAL_SCOPE_TYPE);
relation.setScopeId(GLOBAL_SCOPE_ID);
relation.setStatus(ACTIVE_STATUS);
relation.setSource(USER_OPERATION_SOURCE);
relation.setCreatedAt(now);
relation.setUpdatedAt(now);
boolean saved = this.save(relation);
if (!saved) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "拉黑用户失败");
}
}
private void reactivateRelation(KeyboardCommentBlockRelation relation) {
relation.setStatus(ACTIVE_STATUS);
relation.setSource(USER_OPERATION_SOURCE);
relation.setDeletedAt(null);
relation.setUpdatedAt(new Date());
boolean updated = this.updateById(relation);
if (!updated) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "拉黑用户失败");
}
}
private void deactivateRelation(KeyboardCommentBlockRelation relation) {
Date now = new Date();
relation.setStatus(CANCELED_STATUS);
relation.setUpdatedAt(now);
relation.setDeletedAt(now);
boolean updated = this.updateById(relation);
if (!updated) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "取消拉黑失败");
}
}
private List<KeyboardCommentBlockRelation> listActiveRelations(Long blockerUserId) {
LambdaQueryWrapper<KeyboardCommentBlockRelation> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(KeyboardCommentBlockRelation::getBlockerUserId, blockerUserId)
.eq(KeyboardCommentBlockRelation::getScopeType, GLOBAL_SCOPE_TYPE)
.eq(KeyboardCommentBlockRelation::getScopeId, GLOBAL_SCOPE_ID)
.eq(KeyboardCommentBlockRelation::getStatus, ACTIVE_STATUS)
.orderByDesc(KeyboardCommentBlockRelation::getUpdatedAt)
.orderByDesc(KeyboardCommentBlockRelation::getCreatedAt);
return this.list(queryWrapper);
}
private Map<Long, KeyboardUser> getBlockedUserMap(List<KeyboardCommentBlockRelation> relations) {
List<Long> userIds = relations.stream()
.map(KeyboardCommentBlockRelation::getBlockedUserId)
.distinct()
.collect(Collectors.toList());
return userService.listByIds(userIds).stream()
.collect(Collectors.toMap(KeyboardUser::getId, Function.identity()));
}
private CommentBlockedUserVO toBlockedUserVO(KeyboardCommentBlockRelation relation, KeyboardUser user) {
if (user == null) {
return null;
}
CommentBlockedUserVO vo = new CommentBlockedUserVO();
vo.setUserId(user.getId());
vo.setUserUid(user.getUid());
vo.setUserName(user.getNickName());
vo.setUserAvatar(user.getAvatarUrl());
vo.setBlockedAt(relation.getUpdatedAt() != null ? relation.getUpdatedAt() : relation.getCreatedAt());
return vo;
}
private boolean isActive(KeyboardCommentBlockRelation relation) {
return relation.getStatus() != null && relation.getStatus() == ACTIVE_STATUS;
}
}

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yolo.keyborad.mapper.KeyboardCommentBlockRelationMapper">
<resultMap id="BaseResultMap" type="com.yolo.keyborad.model.entity.KeyboardCommentBlockRelation">
<!--@mbg.generated-->
<!--@Table keyboard_comment_block_relation-->
<id column="id" jdbcType="BIGINT" property="id" />
<result column="blocker_user_id" jdbcType="BIGINT" property="blockerUserId" />
<result column="blocked_user_id" jdbcType="BIGINT" property="blockedUserId" />
<result column="scope_type" jdbcType="SMALLINT" property="scopeType" />
<result column="scope_id" jdbcType="BIGINT" property="scopeId" />
<result column="status" jdbcType="SMALLINT" property="status" />
<result column="source" jdbcType="SMALLINT" property="source" />
<result column="created_at" jdbcType="TIMESTAMP" property="createdAt" />
<result column="updated_at" jdbcType="TIMESTAMP" property="updatedAt" />
<result column="deleted_at" jdbcType="TIMESTAMP" property="deletedAt" />
</resultMap>
<sql id="Base_Column_List">
<!--@mbg.generated-->
id, blocker_user_id, blocked_user_id, scope_type, scope_id, "status", "source", created_at,
updated_at, deleted_at
</sql>
</mapper>