同步 SensitiveWord 敏感词代码,原分支未正确关联仓库

This commit is contained in:
YunaiV 2022-04-09 01:00:20 +08:00
parent bae4502eb9
commit 3f7d7c3bfa
24 changed files with 1066 additions and 0 deletions

View File

@ -0,0 +1,58 @@
package cn.iocoder.yudao.framework.mybatis.core.type;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.apache.ibatis.type.TypeHandler;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
/**
* List<String> 的类型转换器实现类对应数据库的 varchar 类型
*
* @author 永不言败
* @since 2022 3/23 12:50:15
*/
@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes(List.class)
public class StringLiSTTypeHandler implements TypeHandler<List<String>> {
private static final String COMMA = ",";
@Override
public void setParameter(PreparedStatement ps, int i, List<String> strings, JdbcType jdbcType) throws SQLException {
// 设置占位符
ps.setString(i, CollUtil.join(strings, COMMA));
}
@Override
public List<String> getResult(ResultSet rs, String columnName) throws SQLException {
String value = rs.getString(columnName);
return getResult(value);
}
@Override
public List<String> getResult(ResultSet rs, int columnIndex) throws SQLException {
String value = rs.getString(columnIndex);
return getResult(value);
}
@Override
public List<String> getResult(CallableStatement cs, int columnIndex) throws SQLException {
String value = cs.getString(columnIndex);
return getResult(value);
}
private List<String> getResult(String value) {
if (value == null) {
return null;
}
return StrUtil.splitTrim(value, COMMA);
}
}

View File

@ -0,0 +1,16 @@
package cn.iocoder.yudao.module.system.api.sensitiveword;
import java.util.List;
import java.util.Set;
/**
* 大佬别偷懒代码千万行注释第一行
*
* @author: 永不言败 <向国足学习永不言败>
* @since: 2022/3/23 17:00
* @description:
* @modification:
*/
public interface SensitiveWordApi {
}

View File

@ -119,4 +119,8 @@ public interface ErrorCodeConstants {
ErrorCode SOCIAL_USER_UNBIND_NOT_SELF = new ErrorCode(1002018001, "社交解绑失败,非当前用户绑定");
ErrorCode SOCIAL_USER_NOT_FOUND = new ErrorCode(1002018002, "社交授权失败,找不到对应的用户");
// ========== 系统铭感词 1002019000 =========
ErrorCode SENSITIVE_WORD_NOT_EXISTS = new ErrorCode(1002019000, "系统敏感词在所有标签中都不存在");
ErrorCode SENSITIVE_WORD_EXISTS = new ErrorCode(1002019001, "系统敏感词已在标签中存在");
}

View File

@ -0,0 +1,22 @@
GET http://localhost:81/dev-api/admin-api/system/sensitive-word/get-all-tags
Accept: application/json
Authorization: Bearer 385d533b781f44f6bb21ea08afeec47c
###
POST http://localhost:81/dev-api/admin-api/system/sensitive-word/create
Content-Type: application/json
Authorization: Bearer 1649ff1f8b9a4eeeb458fe93a71c78b5
{
"name": "test",
"tags": [
"bbb,aaa"
],
"description": "test",
"status": true
}
###

View File

@ -0,0 +1,104 @@
package cn.iocoder.yudao.module.system.controller.admin.sensitiveword;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
import cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo.*;
import cn.iocoder.yudao.module.system.convert.sensitiveword.SensitiveWordConvert;
import cn.iocoder.yudao.module.system.dal.dataobject.sensitiveword.SensitiveWordDO;
import cn.iocoder.yudao.module.system.service.sensitiveword.SensitiveWordService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.IOException;
import java.util.List;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
@Api(tags = "管理后台 - 敏感词")
@RestController
@RequestMapping("/system/sensitive-word")
@Validated
public class SensitiveWordController {
@Resource
private SensitiveWordService sensitiveWordService;
@PostMapping("/create")
@ApiOperation("创建敏感词")
@PreAuthorize("@ss.hasPermission('system:sensitive-word:create')")
public CommonResult<Long> createSensitiveWord(@Valid @RequestBody SensitiveWordCreateReqVO createReqVO) {
return success(sensitiveWordService.createSensitiveWord(createReqVO));
}
@PutMapping("/update")
@ApiOperation("更新敏感词")
@PreAuthorize("@ss.hasPermission('system:sensitive-word:update')")
public CommonResult<Boolean> updateSensitiveWord(@Valid @RequestBody SensitiveWordUpdateReqVO updateReqVO) {
sensitiveWordService.updateSensitiveWord(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@ApiOperation("删除敏感词")
@ApiImplicitParam(name = "id", value = "编号", required = true, dataTypeClass = Long.class)
@PreAuthorize("@ss.hasPermission('system:sensitive-word:delete')")
public CommonResult<Boolean> deleteSensitiveWord(@RequestParam("id") Long id) {
sensitiveWordService.deleteSensitiveWord(id);
return success(true);
}
@GetMapping("/get")
@ApiOperation("获得敏感词")
@ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
@PreAuthorize("@ss.hasPermission('system:sensitive-word:query')")
public CommonResult<SensitiveWordRespVO> getSensitiveWord(@RequestParam("id") Long id) {
SensitiveWordDO sensitiveWord = sensitiveWordService.getSensitiveWord(id);
return success(SensitiveWordConvert.INSTANCE.convert(sensitiveWord));
}
@GetMapping("/page")
@ApiOperation("获得敏感词分页")
@PreAuthorize("@ss.hasPermission('system:sensitive-word:query')")
public CommonResult<PageResult<SensitiveWordRespVO>> getSensitiveWordPage(@Valid SensitiveWordPageReqVO pageVO) {
PageResult<SensitiveWordDO> pageResult = sensitiveWordService.getSensitiveWordPage(pageVO);
return success(SensitiveWordConvert.INSTANCE.convertPage(pageResult));
}
@GetMapping("/get-tags")
@ApiOperation("获取所有敏感词的标签数组")
@PreAuthorize("@ss.hasPermission('system:sensitive-word:query')")
public CommonResult<Set<String>> getSensitiveWordTags() throws IOException {
return success(sensitiveWordService.getSensitiveWordTags());
}
@GetMapping("/export-excel")
@ApiOperation("导出敏感词 Excel")
@PreAuthorize("@ss.hasPermission('system:sensitive-word:export')")
@OperateLog(type = EXPORT)
public void exportSensitiveWordExcel(@Valid SensitiveWordExportReqVO exportReqVO,
HttpServletResponse response) throws IOException {
List<SensitiveWordDO> list = sensitiveWordService.getSensitiveWordList(exportReqVO);
// 导出 Excel
List<SensitiveWordExcelVO> datas = SensitiveWordConvert.INSTANCE.convertList02(list);
ExcelUtils.write(response, "敏感词.xls", "数据", SensitiveWordExcelVO.class, datas);
}
// @GetMapping("/is-sensitive-word-by-text-and-tag")
// @ApiOperation("通过tag判断传入text是否含有敏感词")
// @PreAuthorize("@ss.hasPermission('system:sensitive-word:checkbytextandtag')")
// public CommonResult<Boolean> isSensitiveWordByTextAndTag(@NotBlank String text, @NotBlank String tag) throws IOException {
// return success(sensitiveWordApi.isSensitiveWordByTextAndTag(text,tag));
// }
}

View File

@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotNull;
import java.util.List;
/**
* 敏感词 Base VO提供给添加修改详细的子 VO 使用
* 如果子 VO 存在差异的字段请不要添加到这里影响 Swagger 文档生成
*/
@Data
public class SensitiveWordBaseVO {
@ApiModelProperty(value = "敏感词", required = true, example = "敏感词")
@NotNull(message = "敏感词不能为空")
private String name;
@ApiModelProperty(value = "标签", required = true, example = "短信,评论")
@NotNull(message = "标签不能为空")
private List<String> tags;
@ApiModelProperty(value = "状态", required = true, example = "1", notes = "参见 CommonStatusEnum 枚举类")
@NotNull(message = "状态不能为空")
private Integer status;
@ApiModelProperty(value = "描述", example = "污言秽语")
private String description;
}

View File

@ -0,0 +1,14 @@
package cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@ApiModel("管理后台 - 敏感词创建 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class SensitiveWordCreateReqVO extends SensitiveWordBaseVO {
}

View File

@ -0,0 +1,35 @@
package cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
import java.util.Date;
import java.util.List;
/**
* 敏感词 Excel VO
*
* @author 永不言败
*/
@Data
public class SensitiveWordExcelVO {
@ExcelProperty("编号")
private Long id;
@ExcelProperty("敏感词")
private String name;
@ExcelProperty("标签")
private List<String> tags;
@ExcelProperty("状态true-启用false-禁用")
private Integer status;
@ExcelProperty("描述")
private String description;
@ExcelProperty("创建时间")
private Date createTime;
}

View File

@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@ApiModel(value = "管理后台 - 敏感词 Excel 导出 Request VO", description = "参数和 SensitiveWordPageReqVO 是一致的")
@Data
public class SensitiveWordExportReqVO {
@ApiModelProperty(value = "敏感词", example = "敏感词")
private String name;
@ApiModelProperty(value = "标签", example = "短信,评论")
private String tag;
@ApiModelProperty(value = "状态", example = "true-启用false-禁用")
private Integer status;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "开始创建时间")
private Date beginCreateTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "结束创建时间")
private Date endCreateTime;
}

View File

@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@ApiModel("管理后台 - 敏感词分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class SensitiveWordPageReqVO extends PageParam {
@ApiModelProperty(value = "敏感词", example = "敏感词")
private String name;
@ApiModelProperty(value = "标签", example = "短信,评论")
private String tag;
@ApiModelProperty(value = "状态", example = "true-启用true-禁用")
private Integer status;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "开始创建时间")
private Date beginCreateTime;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@ApiModelProperty(value = "结束创建时间")
private Date endCreateTime;
}

View File

@ -0,0 +1,23 @@
package cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.util.Date;
@ApiModel("管理后台 - 敏感词 Response VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class SensitiveWordRespVO extends SensitiveWordBaseVO {
@ApiModelProperty(value = "编号", required = true, example = "1")
private Long id;
@ApiModelProperty(value = "创建时间", required = true)
private Date createTime;
}

View File

@ -0,0 +1,21 @@
package cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import javax.validation.constraints.NotNull;
@ApiModel("管理后台 - 敏感词更新 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class SensitiveWordUpdateReqVO extends SensitiveWordBaseVO {
@ApiModelProperty(value = "编号", required = true, example = "1")
@NotNull(message = "编号不能为空")
private Long id;
}

View File

@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.system.convert.sensitiveword;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo.SensitiveWordCreateReqVO;
import cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo.SensitiveWordExcelVO;
import cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo.SensitiveWordRespVO;
import cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo.SensitiveWordUpdateReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.sensitiveword.SensitiveWordDO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
/**
* 敏感词 Convert
*
* @author 永不言败
*/
@Mapper
public interface SensitiveWordConvert {
SensitiveWordConvert INSTANCE = Mappers.getMapper(SensitiveWordConvert.class);
SensitiveWordDO convert(SensitiveWordCreateReqVO bean);
SensitiveWordDO convert(SensitiveWordUpdateReqVO bean);
SensitiveWordRespVO convert(SensitiveWordDO bean);
List<SensitiveWordRespVO> convertList(List<SensitiveWordDO> list);
PageResult<SensitiveWordRespVO> convertPage(PageResult<SensitiveWordDO> page);
List<SensitiveWordExcelVO> convertList02(List<SensitiveWordDO> list);
}

View File

@ -0,0 +1,56 @@
package cn.iocoder.yudao.module.system.dal.dataobject.sensitiveword;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.mybatis.core.type.StringLiSTTypeHandler;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.util.List;
/**
* 敏感词 DO
*
* @author 永不言败
*/
@TableName(value = "system_sensitive_word", autoResultMap = true)
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SensitiveWordDO extends BaseDO {
/**
* 编号
*/
@TableId
private Long id;
/**
* 敏感词
*/
private String name;
/**
* 描述
*/
private String description;
/**
* 标签数组
*
* 用于实现不同的业务场景下需要使用不同标签的敏感词
* 例如说tag 有短信论坛两种敏感词 "推广" 在短信下是敏感词在论坛下不是敏感词
* 此时我们会存储一条敏感词记录它的 name "推广"tag 为短信
*/
@TableField(typeHandler = StringLiSTTypeHandler.class)
private List<String> tags;
/**
* 状态
*
* 枚举 {@link CommonStatusEnum}
*/
private Integer status;
}

View File

@ -0,0 +1,47 @@
package cn.iocoder.yudao.module.system.dal.mysql.sensitiveword;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo.SensitiveWordExportReqVO;
import cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo.SensitiveWordPageReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.sensitiveword.SensitiveWordDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.Date;
import java.util.List;
/**
* 敏感词 Mapper
*
* @author 永不言败
*/
@Mapper
public interface SensitiveWordMapper extends BaseMapperX<SensitiveWordDO> {
default PageResult<SensitiveWordDO> selectPage(SensitiveWordPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<SensitiveWordDO>()
.likeIfPresent(SensitiveWordDO::getName, reqVO.getName())
.likeIfPresent(SensitiveWordDO::getTags, reqVO.getTag())
.eqIfPresent(SensitiveWordDO::getStatus, reqVO.getStatus())
.betweenIfPresent(SensitiveWordDO::getCreateTime, reqVO.getBeginCreateTime(), reqVO.getEndCreateTime())
.orderByDesc(SensitiveWordDO::getId));
}
default List<SensitiveWordDO> selectList(SensitiveWordExportReqVO reqVO) {
return selectList(new LambdaQueryWrapperX<SensitiveWordDO>()
.likeIfPresent(SensitiveWordDO::getName, reqVO.getName())
.likeIfPresent(SensitiveWordDO::getTags, reqVO.getTag())
.eqIfPresent(SensitiveWordDO::getStatus, reqVO.getStatus())
.betweenIfPresent(SensitiveWordDO::getCreateTime, reqVO.getBeginCreateTime(), reqVO.getEndCreateTime())
.orderByDesc(SensitiveWordDO::getId));
}
default SensitiveWordDO selectByName(String name) {
return selectOne(SensitiveWordDO::getName, name);
}
@Select("SELECT id FROM system_sensitive_word WHERE update_time > #{maxUpdateTime} LIMIT 1")
SensitiveWordDO selectExistsByUpdateTimeAfter(Date maxUpdateTime);
}

View File

@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.system.mq.consumer.sensitiveword;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
import cn.iocoder.yudao.module.system.mq.message.sensitiveword.SensitiveWordRefreshMessage;
import cn.iocoder.yudao.module.system.service.sensitiveword.SensitiveWordService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 针对 {@link SensitiveWordRefreshMessage} 的消费者
*
* @author 芋道源码
*/
@Component
@Slf4j
public class SensitiveWordRefreshConsumer extends AbstractChannelMessageListener<SensitiveWordRefreshMessage> {
@Resource
private SensitiveWordService sensitiveWordService;
@Override
public void onMessage(SensitiveWordRefreshMessage message) {
log.info("[onMessage][收到 SensitiveWord 刷新消息]");
sensitiveWordService.initLocalCache();
}
}

View File

@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.system.mq.message.sensitiveword;
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 敏感词的刷新 Message
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class SensitiveWordRefreshMessage extends AbstractChannelMessage {
@Override
public String getChannel() {
return "system.sensitive-word.refresh";
}
}

View File

@ -0,0 +1,26 @@
package cn.iocoder.yudao.module.system.mq.producer.sensitiveword;
import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
import cn.iocoder.yudao.module.system.mq.message.sensitiveword.SensitiveWordRefreshMessage;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 敏感词相关的 Producer
*/
@Component
public class SensitiveWordProducer {
@Resource
private RedisMQTemplate redisMQTemplate;
/**
* 发送 {@link SensitiveWordRefreshMessage} 消息
*/
public void sendSensitiveWordRefreshMessage() {
SensitiveWordRefreshMessage message = new SensitiveWordRefreshMessage();
redisMQTemplate.send(message);
}
}

View File

@ -0,0 +1,86 @@
package cn.iocoder.yudao.module.system.service.sensitiveword;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo.SensitiveWordCreateReqVO;
import cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo.SensitiveWordExportReqVO;
import cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo.SensitiveWordPageReqVO;
import cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo.SensitiveWordUpdateReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.sensitiveword.SensitiveWordDO;
import javax.validation.Valid;
import java.util.List;
import java.util.Set;
/**
* 敏感词 Service 接口
*
* @author 永不言败
*/
public interface SensitiveWordService {
/**
* 初始化本地缓存
*/
void initLocalCache();
/**
* 创建敏感词
*
* @param createReqVO 创建信息
* @return 编号
*/
Long createSensitiveWord(@Valid SensitiveWordCreateReqVO createReqVO);
/**
* 更新敏感词
*
* @param updateReqVO 更新信息
*/
void updateSensitiveWord(@Valid SensitiveWordUpdateReqVO updateReqVO);
/**
* 删除敏感词
*
* @param id 编号
*/
void deleteSensitiveWord(Long id);
/**
* 获得敏感词
*
* @param id 编号
* @return 敏感词
*/
SensitiveWordDO getSensitiveWord(Long id);
/**
* 获得敏感词列表
*
* @return 敏感词列表
*/
List<SensitiveWordDO> getSensitiveWordList();
/**
* 获得敏感词分页
*
* @param pageReqVO 分页查询
* @return 敏感词分页
*/
PageResult<SensitiveWordDO> getSensitiveWordPage(SensitiveWordPageReqVO pageReqVO);
/**
* 获得敏感词列表, 用于 Excel 导出
*
* @param exportReqVO 查询条件
* @return 敏感词列表
*/
List<SensitiveWordDO> getSensitiveWordList(SensitiveWordExportReqVO exportReqVO);
/**
* 获得所有敏感词的标签数组
*
* @return 标签数组
*/
Set<String> getSensitiveWordTags();
}

View File

@ -0,0 +1,206 @@
package cn.iocoder.yudao.module.system.service.sensitiveword;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo.SensitiveWordCreateReqVO;
import cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo.SensitiveWordExportReqVO;
import cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo.SensitiveWordPageReqVO;
import cn.iocoder.yudao.module.system.controller.admin.sensitiveword.vo.SensitiveWordUpdateReqVO;
import cn.iocoder.yudao.module.system.convert.sensitiveword.SensitiveWordConvert;
import cn.iocoder.yudao.module.system.dal.dataobject.sensitiveword.SensitiveWordDO;
import cn.iocoder.yudao.module.system.dal.mysql.sensitiveword.SensitiveWordMapper;
import cn.iocoder.yudao.module.system.mq.producer.sensitiveword.SensitiveWordProducer;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
/**
* 敏感词 Service 实现类
*
* @author 永不言败
*/
@Service
@Slf4j
@Validated
public class SensitiveWordServiceImpl implements SensitiveWordService {
/**
* 定时执行 {@link #schedulePeriodicRefresh()} 的周期
* 因为已经通过 Redis Pub/Sub 机制所以频率不需要高
*/
private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L;
/**
* 敏感词缓存
* key敏感词编号 {@link SensitiveWordDO#getId()}
* <p>
* 这里声明 volatile 修饰的原因是每次刷新时直接修改指向
*/
@Getter
private volatile Map<Long, SensitiveWordDO> sensitiveWordCache = Collections.emptyMap();
/**
* 敏感词标签缓存
* key敏感词编号 {@link SensitiveWordDO#getId()}
* <p>
* 这里声明 volatile 修饰的原因是每次刷新时直接修改指向
*/
@Getter
private volatile Set<String> sensitiveWordTagsCache = Collections.emptySet();
/**
* 缓存敏感词的最大更新时间用于后续的增量轮询判断是否有更新
*/
@Getter
private volatile Date maxUpdateTime;
@Resource
private SensitiveWordMapper sensitiveWordMapper;
@Resource
private SensitiveWordProducer sensitiveWordProducer;
@Resource
@Lazy
private SensitiveWordService self;
/**
* 初始化 {@link #sensitiveWordCache} 缓存
*/
@Override
@PostConstruct
public void initLocalCache() {
// 获取敏感词列表如果有更新
List<SensitiveWordDO> sensitiveWordList = loadSensitiveWordIfUpdate(maxUpdateTime);
if (CollUtil.isEmpty(sensitiveWordList)) {
return;
}
// 写入 sensitiveWordTagsCache 缓存
Set<String> tags = new HashSet<>();
sensitiveWordList.forEach(word -> tags.addAll(word.getTags()));
sensitiveWordTagsCache = tags;
// 写入 sensitiveWordCache 缓存
sensitiveWordCache = CollectionUtils.convertMap(sensitiveWordList, SensitiveWordDO::getId);
maxUpdateTime = CollectionUtils.getMaxValue(sensitiveWordList, SensitiveWordDO::getUpdateTime);
log.info("[initLocalCache][初始化 敏感词 数量为 {}]", sensitiveWordList.size());
}
@Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD)
public void schedulePeriodicRefresh() {
self.initLocalCache();
}
/**
* 如果敏感词发生变化从数据库中获取最新的全量敏感词
* 如果未发生变化则返回空
*
* @param maxUpdateTime 当前敏感词的最大更新时间
* @return 敏感词列表
*/
private List<SensitiveWordDO> loadSensitiveWordIfUpdate(Date maxUpdateTime) {
// 第一步判断是否要更新
// 如果更新时间为空说明 DB 一定有新数据
if (maxUpdateTime == null) {
log.info("[loadSensitiveWordIfUpdate][首次加载全量敏感词]");
} else { // 判断数据库中是否有更新的敏感词
if (sensitiveWordMapper.selectExistsByUpdateTimeAfter(maxUpdateTime) == null) {
return null;
}
log.info("[loadSensitiveWordIfUpdate][增量加载全量敏感词]");
}
// 第二步如果有更新则从数据库加载所有敏感词
return sensitiveWordMapper.selectList();
}
@Override
public Long createSensitiveWord(SensitiveWordCreateReqVO createReqVO) {
// 校验唯一性
checkSensitiveWordNameUnique(null, createReqVO.getName());
// 插入
SensitiveWordDO sensitiveWord = SensitiveWordConvert.INSTANCE.convert(createReqVO);
sensitiveWordMapper.insert(sensitiveWord);
// 发送消息刷新缓存
sensitiveWordProducer.sendSensitiveWordRefreshMessage();
return sensitiveWord.getId();
}
@Override
public void updateSensitiveWord(SensitiveWordUpdateReqVO updateReqVO) {
// 校验唯一性
checkSensitiveWordExists(updateReqVO.getId());
checkSensitiveWordNameUnique(updateReqVO.getId(), updateReqVO.getName());
// 更新
SensitiveWordDO updateObj = SensitiveWordConvert.INSTANCE.convert(updateReqVO);
sensitiveWordMapper.updateById(updateObj);
// 发送消息刷新缓存
sensitiveWordProducer.sendSensitiveWordRefreshMessage();
}
@Override
public void deleteSensitiveWord(Long id) {
// 校验存在
checkSensitiveWordExists(id);
// 删除
sensitiveWordMapper.deleteById(id);
// 发送消息刷新缓存
sensitiveWordProducer.sendSensitiveWordRefreshMessage();
}
private void checkSensitiveWordNameUnique(Long id, String name) {
SensitiveWordDO word = sensitiveWordMapper.selectByName(name);
if (word == null) {
return;
}
// 如果 id 为空说明不用比较是否为相同 id 的敏感词
if (id == null) {
throw exception(SENSITIVE_WORD_EXISTS);
}
if (!word.getId().equals(id)) {
throw exception(SENSITIVE_WORD_EXISTS);
}
}
private void checkSensitiveWordExists(Long id) {
if (sensitiveWordMapper.selectById(id) == null) {
throw exception(SENSITIVE_WORD_NOT_EXISTS);
}
}
@Override
public SensitiveWordDO getSensitiveWord(Long id) {
return sensitiveWordMapper.selectById(id);
}
@Override
public List<SensitiveWordDO> getSensitiveWordList() {
return sensitiveWordMapper.selectList();
}
@Override
public PageResult<SensitiveWordDO> getSensitiveWordPage(SensitiveWordPageReqVO pageReqVO) {
return sensitiveWordMapper.selectPage(pageReqVO);
}
@Override
public List<SensitiveWordDO> getSensitiveWordList(SensitiveWordExportReqVO exportReqVO) {
return sensitiveWordMapper.selectList(exportReqVO);
}
@Override
public Set<String> getSensitiveWordTags() {
return sensitiveWordTagsCache;
}
}

View File

@ -0,0 +1,143 @@
package cn.iocoder.yudao.module.system.util.collection;
import java.util.*;
/**
* 基于前缀树实现敏感词的校验
* <p>
* 相比 Apache Common 提供的 PatriciaTrie 来说性能可能会更加好一些
*
* @author 芋道源码
*/
@SuppressWarnings("unchecked")
public class SimpleTrie {
/**
* 一个敏感词结束后对应的 key
*/
private static final Character CHARACTER_END = '\0';
/**
* 使用敏感词构建的前缀树
*/
private final Map<Character, Object> children;
/**
* 基于字符串构建前缀树
*
* @param strs 字符串数组
*/
public SimpleTrie(List<String> strs) {
children = new HashMap<>();
// 构建树
Collections.sort(strs); // 排序优先使用较短的前缀
for (String str : strs) {
Map<Character, Object> child = children;
// 遍历每个字符
for (Character c : str.toCharArray()) {
// 如果已经到达结束就没必要在添加更长的敏感词
// 例如说有两个敏感词是吃饭啊吃饭输入一句话是 我要吃饭啊则只要匹配到 吃饭 这个敏感词即可
if (child.containsKey(CHARACTER_END)) {
break;
}
if (!child.containsKey(c)) {
child.put(c, new HashMap<>());
}
child = (Map<Character, Object>) child.get(c);
}
// 结束
child.put(CHARACTER_END, null);
}
}
/**
* 验证文本是否合法即不包含敏感词
*
* @param text 文本
* @return 是否 ok
*/
public boolean isValid(String text) {
// 遍历 text使用每一个 [i, n) 段的字符串使用 children 前缀树匹配是否包含敏感词
for (int i = 0; i < text.length() - 1; i++) {
Map<Character, Object> child = (Map<Character, Object>) children.get(text.charAt(i));
if (child == null) {
continue;
}
boolean ok = recursion(text, i + 1, child);
if (!ok) {
return false;
}
}
return true;
}
/**
* 验证文本从指定位置开始是否包含某个敏感词
*
* @param text 文本
* @param index 开始位置
* @param child 节点当前遍历到的
* @return 是否包含
*/
private boolean recursion(String text, int index, Map<Character, Object> child) {
if (index == text.length()) {
return true;
}
child = (Map<Character, Object>) child.get(text.charAt(index));
return child == null || !child.containsKey(CHARACTER_END) && recursion(text, ++index, child);
}
/**
* 获得文本所包含的不合法的敏感词
*
* 注意才当即最短匹配原则例如说当敏感词存在 煞笔煞笔二货 只会返回 煞笔
*
* @param text 文本
* @return 匹配的敏感词
*/
public List<String> validate(String text) {
Set<String> results = new HashSet<>();
for (int i = 0; i < text.length() - 1; i++) {
Character c = text.charAt(i);
Map<Character, Object> child = (Map<Character, Object>) children.get(c);
if (child == null) {
continue;
}
StringBuilder result = new StringBuilder().append(c);
boolean ok = recursionWithResult(text, i + 1, child, result);
if (!ok) {
results.add(result.toString());
}
}
return new ArrayList<>(results);
}
/**
* 返回文本从 index 开始的敏感词并使用 StringBuilder 参数进行返回
*
* 逻辑和 {@link #recursion(String, int, Map)} 是一致只是多了 result 返回结果
*
* @param text 文本
* @param index 开始未知
* @param child 节点当前遍历到的
* @param result 返回敏感词
* @return 是否有敏感词
*/
@SuppressWarnings("unchecked")
private static boolean recursionWithResult(String text, int index, Map<Character, Object> child, StringBuilder result) {
if (index == text.length()) {
return true;
}
Character c = text.charAt(index);
child = (Map<Character, Object>) child.get(c);
if (child == null) {
return true;
}
if (child.containsKey(CHARACTER_END)) {
result.append(c);
return false;
}
return recursionWithResult(text, ++index, child, result.append(c));
}
}

View File

@ -0,0 +1,4 @@
/**
* 每个模块的 util 放专属当前模块的 Utils 工具类
*/
package cn.iocoder.yudao.module.system.util;

View File

@ -17,3 +17,4 @@ DELETE FROM "system_error_code";
DELETE FROM "system_social_user";
DELETE FROM "system_tenant";
DELETE FROM "system_tenant_package";
DELETE FROM "system_sensitive_word";

View File

@ -426,3 +426,17 @@ CREATE TABLE IF NOT EXISTS "system_tenant_package" (
"deleted" bit NOT NULL DEFAULT FALSE,
PRIMARY KEY ("id")
) COMMENT '租户套餐表';
CREATE TABLE IF NOT EXISTS "system_sensitive_word" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"name" varchar(255) NOT NULL,
"tags" varchar(1024) NOT NULL,
"status" bit NOT NULL DEFAULT FALSE,
"description" varchar(512),
"creator" varchar(64) DEFAULT '',
"create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
PRIMARY KEY ("id")
) COMMENT '系统敏感词';