1.添加微信小程序验证登录
2.添加敏感词上传 3.保存工作描述时,验证敏感词
This commit is contained in:
		| @@ -116,4 +116,12 @@ public class AppUserController extends BaseController | ||||
|         HashMap<String,Integer> result = jobApplyService.statistics(); | ||||
|         return AjaxResult.success(result); | ||||
|     } | ||||
|  | ||||
|     @ApiOperation("根据条件查询用户信息") | ||||
|     @GetMapping("/list") | ||||
|     public AjaxResult getUserList(AppUser appUser) | ||||
|     { | ||||
|         List<AppUser> list = appUserService.selectAppUserList(appUser); | ||||
|         return AjaxResult.success(list); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -3,6 +3,7 @@ package com.ruoyi.cms.controller.cms; | ||||
| import com.ruoyi.cms.domain.Job; | ||||
| import com.ruoyi.cms.domain.vo.CandidateVO; | ||||
| import com.ruoyi.cms.service.IJobService; | ||||
| import com.ruoyi.cms.util.sensitiveWord.SensitiveWordChecker; | ||||
| import com.ruoyi.common.annotation.Anonymous; | ||||
| import com.ruoyi.common.annotation.Log; | ||||
| import com.ruoyi.common.core.controller.BaseController; | ||||
| @@ -33,6 +34,8 @@ public class CmsJobController extends BaseController | ||||
| { | ||||
|     @Autowired | ||||
|     private IJobService jobService; | ||||
|     @Autowired | ||||
|     SensitiveWordChecker sensitiveWordChecker; | ||||
|     /** | ||||
|      * 查询岗位列表 | ||||
|      */ | ||||
| @@ -79,6 +82,13 @@ public class CmsJobController extends BaseController | ||||
|     @PostMapping | ||||
|     public AjaxResult add(@RequestBody Job job) | ||||
|     { | ||||
|         // 校验描述中的敏感词 | ||||
|         List<String> sensitiveWords = sensitiveWordChecker.checkSensitiveWords(job.getDescription()); | ||||
|         if (!sensitiveWords.isEmpty()) { | ||||
|             String errorMsg = "描述中包含敏感词:" + String.join("、", sensitiveWords); | ||||
|             return AjaxResult.error(errorMsg); | ||||
|         } | ||||
|         // 无敏感词,执行插入 | ||||
|         return toAjax(jobService.insertJob(job)); | ||||
|     } | ||||
|  | ||||
| @@ -91,6 +101,12 @@ public class CmsJobController extends BaseController | ||||
|     @PutMapping | ||||
|     public AjaxResult edit(@RequestBody Job job) | ||||
|     { | ||||
|         // 校验描述中的敏感词 | ||||
|         List<String> sensitiveWords = sensitiveWordChecker.checkSensitiveWords(job.getDescription()); | ||||
|         if (!sensitiveWords.isEmpty()) { | ||||
|             String errorMsg = "描述中包含敏感词:" + String.join("、", sensitiveWords); | ||||
|             return AjaxResult.error(errorMsg); | ||||
|         } | ||||
|         return toAjax(jobService.updateJob(job)); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -2,6 +2,7 @@ package com.ruoyi.cms.controller.cms; | ||||
|  | ||||
| import com.ruoyi.cms.domain.SensitiveWordData; | ||||
| import com.ruoyi.cms.service.SensitiveWordDataService; | ||||
| import com.ruoyi.cms.util.EasyExcelUtils; | ||||
| import com.ruoyi.common.annotation.Log; | ||||
| import com.ruoyi.common.core.controller.BaseController; | ||||
| import com.ruoyi.common.core.domain.AjaxResult; | ||||
| @@ -12,7 +13,9 @@ import io.swagger.annotations.ApiOperation; | ||||
| import org.springframework.beans.factory.annotation.Autowired; | ||||
| import org.springframework.security.access.prepost.PreAuthorize; | ||||
| import org.springframework.web.bind.annotation.*; | ||||
| import org.springframework.web.multipart.MultipartFile; | ||||
|  | ||||
| import java.io.InputStream; | ||||
| import java.util.List; | ||||
|  | ||||
|  | ||||
| @@ -88,4 +91,24 @@ public class SensitiveWordDataController extends BaseController { | ||||
|         return toAjax(sensitiveWordDataService.deleteSensitiveworddataIds(ids)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 通用上传请求(单个) | ||||
|      */ | ||||
|     @PostMapping("/exoprt") | ||||
|     public AjaxResult uploadFile(@RequestParam("file") MultipartFile file) throws Exception | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             InputStream inputStream = file.getInputStream(); | ||||
|             EasyExcelUtils.readExcelByBatch(inputStream, SensitiveWordData.class, 100, list -> { | ||||
|                 // 处理逻辑:如批量保存到数据库 | ||||
|                 sensitiveWordDataService.batchInsert(list); | ||||
|             }); | ||||
|         } | ||||
|         catch (Exception e) | ||||
|         { | ||||
|             return AjaxResult.error(e.getMessage()); | ||||
|         } | ||||
|         return AjaxResult.success(); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -105,4 +105,10 @@ public class Company extends BaseEntity | ||||
|     @TableField(exist = false) | ||||
|     @ApiModelProperty("驳回时间") | ||||
|     private String rejectTime; | ||||
|  | ||||
|     @ApiModelProperty("是否本地重点企业") | ||||
|     private String isImpCompany; | ||||
|  | ||||
|     @ApiModelProperty("本地重点发展产业") | ||||
|     private String impCompanyType; | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| package com.ruoyi.cms.domain; | ||||
|  | ||||
| import com.alibaba.excel.annotation.ExcelProperty; | ||||
| import com.baomidou.mybatisplus.annotation.IdType; | ||||
| import com.baomidou.mybatisplus.annotation.TableField; | ||||
| import com.baomidou.mybatisplus.annotation.TableId; | ||||
| @@ -31,11 +32,13 @@ public class SensitiveWordData extends BaseEntity { | ||||
| 	/** | ||||
| 	 * 敏感词 | ||||
| 	 */ | ||||
| 	@ExcelProperty(value = "敏感词", index = 0) | ||||
| 	@ApiModelProperty("敏感词") | ||||
| 	private String sensitiveWord; | ||||
| 	/** | ||||
| 	 * 类型 | ||||
| 	 */ | ||||
| 	@ExcelProperty(value = "类型", index = 1) | ||||
| 	@ApiModelProperty("类型") | ||||
| 	private String type; | ||||
|  | ||||
|   | ||||
| @@ -16,4 +16,6 @@ import java.util.List; | ||||
| public interface SensitiveWordDataMapper extends BaseMapper<SensitiveWordData> { | ||||
|  | ||||
|     List<SensitiveWordData> selectSensitiveworddataList(SensitiveWordData sensitiveWordData); | ||||
|  | ||||
|     int batchInsert(List<SensitiveWordData> list); | ||||
| } | ||||
|   | ||||
| @@ -51,4 +51,5 @@ public interface IAppUserService | ||||
|      */ | ||||
|     public int deleteAppUserByUserIds(Long[] userIds); | ||||
|  | ||||
|     public AppUser getPhone(String phone); | ||||
| } | ||||
|   | ||||
| @@ -22,5 +22,7 @@ public interface SensitiveWordDataService { | ||||
|     int updateSensitiveworddata(SensitiveWordData sensitiveWordData); | ||||
|  | ||||
|     int deleteSensitiveworddataIds(Long[] ids); | ||||
|  | ||||
|     int batchInsert(List<SensitiveWordData> list); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import java.util.List; | ||||
| import java.util.Arrays; | ||||
| import java.util.stream.Collectors; | ||||
|  | ||||
| import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; | ||||
| import com.ruoyi.cms.mapper.JobTitleMapper; | ||||
| import com.ruoyi.cms.util.StringUtil; | ||||
| import com.ruoyi.common.core.domain.entity.JobTitle; | ||||
| @@ -102,4 +103,9 @@ public class AppUserServiceImpl extends ServiceImpl<AppUserMapper,AppUser> imple | ||||
|         return appUserMapper.deleteBatchIds(Arrays.asList(userIds)); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public AppUser getPhone(String phone) { | ||||
|         return appUserMapper.selectOne(new LambdaQueryWrapper<AppUser>() | ||||
|                 .eq(AppUser::getPhone, phone)); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -40,4 +40,9 @@ public class SensitiveWordDataServiceImpl implements SensitiveWordDataService { | ||||
|     public int deleteSensitiveworddataIds(Long[] ids) { | ||||
|         return sensitiveWordDataMapper.deleteBatchIds(Arrays.asList(ids)); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public int batchInsert(List<SensitiveWordData> list) { | ||||
|         return sensitiveWordDataMapper.batchInsert(list); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,198 @@ | ||||
| package com.ruoyi.cms.util; | ||||
|  | ||||
| import com.alibaba.excel.EasyExcel; | ||||
| import com.alibaba.excel.context.AnalysisContext; | ||||
| import com.alibaba.excel.event.AnalysisEventListener; | ||||
| import com.alibaba.excel.write.builder.ExcelWriterSheetBuilder; | ||||
| import com.alibaba.excel.write.metadata.style.WriteCellStyle; | ||||
| import com.alibaba.excel.write.metadata.style.WriteFont; | ||||
| import com.alibaba.excel.write.style.HorizontalCellStyleStrategy; | ||||
| import org.apache.poi.ss.usermodel.HorizontalAlignment; | ||||
| import org.apache.poi.ss.usermodel.VerticalAlignment; | ||||
|  | ||||
| import javax.servlet.http.HttpServletResponse; | ||||
| import java.io.*; | ||||
| import java.net.URLEncoder; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| import java.util.Objects; | ||||
| import java.util.function.Consumer; | ||||
|  | ||||
| /** | ||||
|  * EasyExcel 工具类(基于 alibaba easyexcel 3.x+) | ||||
|  */ | ||||
| public class EasyExcelUtils { | ||||
|  | ||||
|     /** | ||||
|      * 读取 Excel 文件(一次性读取所有数据) | ||||
|      * | ||||
|      * @param file      上传的 Excel 文件 | ||||
|      * @param head      实体类字节码(需使用 @ExcelProperty 注解映射表头) | ||||
|      * @param <T>       实体类泛型 | ||||
|      * @return 解析后的数据集 | ||||
|      */ | ||||
|     public static <T> List<T> readExcel(File file, Class<T> head) { | ||||
|         return EasyExcel.read(file) | ||||
|                 .head(head) | ||||
|                 .sheet() | ||||
|                 .doReadSync(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 读取 Excel 输入流(一次性读取所有数据) | ||||
|      * | ||||
|      * @param inputStream Excel 输入流(如 MultipartFile 的 getInputStream()) | ||||
|      * @param head        实体类字节码 | ||||
|      * @param <T>         实体类泛型 | ||||
|      * @return 解析后的数据集 | ||||
|      */ | ||||
|     public static <T> List<T> readExcel(InputStream inputStream, Class<T> head) { | ||||
|         return EasyExcel.read(inputStream) | ||||
|                 .head(head) | ||||
|                 .sheet() | ||||
|                 .doReadSync(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 分批读取 Excel(适用于大数据量,避免内存溢出) | ||||
|      * | ||||
|      * @param inputStream Excel 输入流 | ||||
|      * @param head        实体类字节码 | ||||
|      * @param batchSize   每批处理的数据量 | ||||
|      * @param consumer    数据处理函数(如批量保存到数据库) | ||||
|      * @param <T>         实体类泛型 | ||||
|      */ | ||||
|     public static <T> void readExcelByBatch(InputStream inputStream, Class<T> head, int batchSize, Consumer<List<T>> consumer) { | ||||
|         EasyExcel.read(inputStream) | ||||
|                 .head(head) | ||||
|                 .sheet() | ||||
|                 .registerReadListener(new AnalysisEventListener<T>() { | ||||
|                     private List<T> batchList; // 临时存储批数据 | ||||
|  | ||||
|                     @Override | ||||
|                     public void invoke(T data, AnalysisContext context) { | ||||
|                         if (batchList == null) { | ||||
|                             batchList = new java.util.ArrayList<>(batchSize); | ||||
|                         } | ||||
|                         batchList.add(data); | ||||
|                         // 达到批处理量时执行消费逻辑 | ||||
|                         if (batchList.size() >= batchSize) { | ||||
|                             consumer.accept(batchList); | ||||
|                             batchList.clear(); // 清空集合,释放内存 | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     @Override | ||||
|                     public void doAfterAllAnalysed(AnalysisContext context) { | ||||
|                         // 处理剩余不足一批的数据 | ||||
|                         if (batchList != null && !batchList.isEmpty()) { | ||||
|                             consumer.accept(batchList); | ||||
|                         } | ||||
|                     } | ||||
|                 }) | ||||
|                 .doRead(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 生成 Excel 并写入到输出流(通用样式) | ||||
|      * | ||||
|      * @param outputStream 输出流(如 HttpServletResponse 的 getOutputStream()) | ||||
|      * @param data         数据集 | ||||
|      * @param head         实体类字节码 | ||||
|      * @param sheetName    工作表名称 | ||||
|      * @param <T>          实体类泛型 | ||||
|      */ | ||||
|     public static <T> void writeExcel(OutputStream outputStream, List<T> data, Class<T> head, String sheetName) { | ||||
|         // 构建通用样式策略(表头居中加粗,内容居中) | ||||
|         HorizontalCellStyleStrategy styleStrategy = getDefaultStyleStrategy(); | ||||
|  | ||||
|         ExcelWriterSheetBuilder writerBuilder = EasyExcel.write(outputStream, head) | ||||
|                 .sheet(sheetName) | ||||
|                 .registerWriteHandler(styleStrategy); | ||||
|  | ||||
|         writerBuilder.doWrite(data); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 生成 Excel 并通过 HttpServletResponse 下载(前端直接触发下载) | ||||
|      * | ||||
|      * @param response  HttpServletResponse | ||||
|      * @param data      数据集 | ||||
|      * @param head      实体类字节码 | ||||
|      * @param sheetName 工作表名称 | ||||
|      * @param fileName  下载的文件名(不带后缀) | ||||
|      * @param <T>       实体类泛型 | ||||
|      * @throws IOException IO异常 | ||||
|      */ | ||||
|     public static <T> void downloadExcel(HttpServletResponse response, List<T> data, Class<T> head, | ||||
|                                          String sheetName, String fileName) throws IOException { | ||||
|         // 设置响应头,触发前端下载 | ||||
|         response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); | ||||
|         response.setCharacterEncoding("UTF-8"); | ||||
|         // 文件名编码,避免中文乱码 | ||||
|         String encodedFileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20"); | ||||
|         response.setHeader("Content-disposition", "attachment;filename*=UTF-8''" + encodedFileName + ".xlsx"); | ||||
|  | ||||
|         // 写入数据到响应流 | ||||
|         writeExcel(response.getOutputStream(), data, head, sheetName); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 填充 Excel 模板(适用于带固定格式的模板文件) | ||||
|      * | ||||
|      * @param templateInputStream 模板文件输入流 | ||||
|      * @param outputStream        输出流(如响应流或文件流) | ||||
|      * @param dataMap             填充数据(key为模板中的占位符,value为填充值) | ||||
|      * @param sheetName           工作表名称 | ||||
|      */ | ||||
|     public static void fillTemplate(InputStream templateInputStream, OutputStream outputStream, | ||||
|                                     Map<String, Object> dataMap, String sheetName) { | ||||
|         EasyExcel.write(outputStream) | ||||
|                 .withTemplate(templateInputStream) | ||||
|                 .sheet(sheetName) | ||||
|                 .doFill(dataMap); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取默认单元格样式策略(表头加粗居中,内容居中) | ||||
|      */ | ||||
|     private static HorizontalCellStyleStrategy getDefaultStyleStrategy() { | ||||
|         // 表头样式 | ||||
|         WriteCellStyle headStyle = new WriteCellStyle(); | ||||
|         WriteFont headFont = new WriteFont(); | ||||
|         headFont.setBold(true); // 加粗 | ||||
|         headFont.setFontHeightInPoints((short) 11); | ||||
|         headStyle.setWriteFont(headFont); | ||||
|         headStyle.setHorizontalAlignment(HorizontalAlignment.CENTER); // 水平居中 | ||||
|         headStyle.setVerticalAlignment(VerticalAlignment.CENTER); // 垂直居中 | ||||
|  | ||||
|         // 内容样式 | ||||
|         WriteCellStyle contentStyle = new WriteCellStyle(); | ||||
|         WriteFont contentFont = new WriteFont(); | ||||
|         contentFont.setFontHeightInPoints((short) 11); | ||||
|         contentStyle.setWriteFont(contentFont); | ||||
|         contentStyle.setHorizontalAlignment(HorizontalAlignment.CENTER); | ||||
|         contentStyle.setVerticalAlignment(VerticalAlignment.CENTER); | ||||
|  | ||||
|         // 返回样式策略 | ||||
|         return new HorizontalCellStyleStrategy(headStyle, contentStyle); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 关闭流(工具类内部使用) | ||||
|      */ | ||||
|     private static void closeStream(Closeable... closeables) { | ||||
|         if (closeables != null) { | ||||
|             for (Closeable closeable : closeables) { | ||||
|                 if (Objects.nonNull(closeable)) { | ||||
|                     try { | ||||
|                         closeable.close(); | ||||
|                     } catch (IOException e) { | ||||
|                         // 日志记录(建议替换为实际项目的日志框架) | ||||
|                         e.printStackTrace(); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -11,10 +11,14 @@ import lombok.extern.slf4j.Slf4j; | ||||
| import org.apache.commons.io.FileUtils; | ||||
| import org.springframework.stereotype.Component; | ||||
|  | ||||
| import javax.crypto.Cipher; | ||||
| import javax.crypto.spec.IvParameterSpec; | ||||
| import javax.crypto.spec.SecretKeySpec; | ||||
| import java.io.File; | ||||
| import java.io.IOException; | ||||
| import java.nio.charset.StandardCharsets; | ||||
| import java.security.MessageDigest; | ||||
| import java.util.Base64; | ||||
| import java.util.Formatter; | ||||
| import java.util.HashMap; | ||||
| import java.util.Map; | ||||
| @@ -210,6 +214,60 @@ public class WechatUtil { | ||||
|         return result; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 通过code获取微信用户的openid和session_key | ||||
|      * | ||||
|      * @param appid     小程序appid | ||||
|      * @param secret 小程序secret | ||||
|      * @param code      登录凭证code | ||||
|      * @return 包含openid、session_key、unionid的JSON对象 | ||||
|      */ | ||||
|     public JSONObject code2Session(String appid, String secret, String code) { | ||||
|         try { | ||||
|             String response = getAccessTokenData("https://api.weixin.qq.com/sns/jscode2session?appid="+appid+"&secret="+secret+"&js_code="+code+"&grant_type=authorization_code"); | ||||
|             JSONObject result = JSONObject.parseObject(response); | ||||
|             // 微信返回错误码处理 | ||||
|             if (result.containsKey("errcode") && result.getInteger("errcode") != 0) { | ||||
|                 throw new RuntimeException("微信授权失败:" + result.getString("errmsg")); | ||||
|             } | ||||
|             return result; | ||||
|         } catch (Exception e) { | ||||
|             throw new RuntimeException("调用微信接口失败:" + e.getMessage()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 解密微信用户手机号(用户通过 getPhoneNumber 组件授权后返回的加密数据) | ||||
|      * @param encryptedData 微信返回的加密手机号数据 | ||||
|      * @param sessionKey 从 code2Session 接口获取的会话密钥 | ||||
|      * @param iv 微信返回的加密向量(与 encryptedData 配套) | ||||
|      * @return 解密后的 JSON 对象(包含 phoneNumber、purePhoneNumber 等字段) | ||||
|      * @throws RuntimeException 解密失败时抛出 | ||||
|      */ | ||||
|     public JSONObject decryptPhoneNumber(String encryptedData, String sessionKey, String iv) { | ||||
|         try { | ||||
|             // 1. Base64 解码(encryptedData、sessionKey、iv 均为 Base64 编码) | ||||
|             byte[] encryptedDataBytes = Base64.getDecoder().decode(encryptedData); | ||||
|             byte[] sessionKeyBytes = Base64.getDecoder().decode(sessionKey); | ||||
|             byte[] ivBytes = Base64.getDecoder().decode(iv); | ||||
|  | ||||
|             // 2. 初始化 AES-128-CBC 解密器(微信固定加密算法) | ||||
|             Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding"); | ||||
|             SecretKeySpec keySpec = new SecretKeySpec(sessionKeyBytes, "AES"); | ||||
|             IvParameterSpec ivSpec = new IvParameterSpec(ivBytes); | ||||
|             cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); | ||||
|  | ||||
|             // 3. 执行解密并转换为字符串 | ||||
|             byte[] decryptedBytes = cipher.doFinal(encryptedDataBytes); | ||||
|             String decryptedStr = new String(decryptedBytes, StandardCharsets.UTF_8); | ||||
|  | ||||
|             // 4. 解析为 JSON 并返回(包含手机号等信息) | ||||
|             return JSONObject.parseObject(decryptedStr); | ||||
|         } catch (Exception e) { | ||||
|             throw new RuntimeException("解密用户手机号失败:" + e.getMessage(), e); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private String create_nonce_str() { | ||||
|         return IdUtil.simpleUUID(); | ||||
|     } | ||||
|   | ||||
| @@ -0,0 +1,105 @@ | ||||
| package com.ruoyi.cms.util.sensitiveWord; | ||||
|  | ||||
| import com.ruoyi.cms.domain.SensitiveWordData; | ||||
| import com.ruoyi.cms.mapper.SensitiveWordDataMapper; | ||||
| import com.ruoyi.common.core.redis.RedisCache; | ||||
| import org.springframework.beans.factory.annotation.Autowired; | ||||
| import org.springframework.cache.annotation.CacheEvict; | ||||
| import org.springframework.cache.annotation.Cacheable; | ||||
| import org.springframework.stereotype.Component; | ||||
| import javax.annotation.PostConstruct; | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
| import java.util.concurrent.TimeUnit; | ||||
| import java.util.stream.Collectors; | ||||
|  | ||||
| @Component | ||||
| public class SensitiveWordChecker { | ||||
|  | ||||
|     @Autowired | ||||
|     private SensitiveWordDataMapper sensitiveWordDataMapper; | ||||
|  | ||||
|     @Autowired | ||||
|     private SensitiveWordTrie trie; // 注入前缀树 | ||||
|     // 缓存键常量 | ||||
|     private static final String SENSITIVE_WORD_CACHE_KEY = "sensitive:words"; | ||||
|     @Autowired | ||||
|     private RedisCache redisCache; | ||||
|  | ||||
|     /** | ||||
|      * 项目启动时初始化敏感词库到前缀树 | ||||
|      */ | ||||
|     @PostConstruct | ||||
|     public void init() { | ||||
|         List<String> sensitiveWords = getSensitiveWordsFromCache(); | ||||
|         if (sensitiveWords.isEmpty()) { | ||||
|             // 缓存未命中,从数据库加载 | ||||
|             sensitiveWords = loadSensitiveWordsFromDbAndCache(); | ||||
|         } | ||||
|         // 初始化前缀树 | ||||
|         trie.batchAddWords(sensitiveWords); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 检测文本中的敏感词 | ||||
|      * @param text 待检测文本(如job.getDescription()) | ||||
|      * @return 敏感词列表(空列表表示无敏感词) | ||||
|      */ | ||||
|     public List<String> checkSensitiveWords(String text) { | ||||
|         return trie.findAllSensitiveWords(text); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 从缓存获取敏感词列表 | ||||
|      */ | ||||
|     @Cacheable(value = SENSITIVE_WORD_CACHE_KEY) | ||||
|     public List<String> getSensitiveWordsFromCache() { | ||||
|         // 从Redis获取 | ||||
|         Object cachedWords = redisCache.getCacheObject(SENSITIVE_WORD_CACHE_KEY); | ||||
|         if (cachedWords instanceof List<?>) { | ||||
|             return ((List<?>) cachedWords).stream() | ||||
|                     .filter(obj -> obj instanceof String) | ||||
|                     .map(obj -> (String) obj) | ||||
|                     .collect(Collectors.toList()); | ||||
|         } | ||||
|         return Collections.emptyList(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 从数据库加载敏感词并更新缓存 | ||||
|      */ | ||||
|     public List<String> loadSensitiveWordsFromDbAndCache() { | ||||
|         List<SensitiveWordData> wordList = sensitiveWordDataMapper.selectSensitiveworddataList(new SensitiveWordData()); | ||||
|         List<String> sensitiveWords = wordList.stream() | ||||
|                 .map(SensitiveWordData::getSensitiveWord) | ||||
|                 .collect(Collectors.toList()); | ||||
|         // 缓存有效期设置为24小时,可根据实际需求调整 | ||||
|         redisCache.setCacheObject(SENSITIVE_WORD_CACHE_KEY, sensitiveWords, 24, TimeUnit.HOURS); | ||||
|         return sensitiveWords; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 敏感词更新时清除缓存(需在敏感词管理服务中调用) | ||||
|      */ | ||||
|     @CacheEvict(value = SENSITIVE_WORD_CACHE_KEY, allEntries = true) | ||||
|     public void clearSensitiveWordCache() { | ||||
|         // 清除缓存后可重新加载 | ||||
|         loadSensitiveWordsFromDbAndCache(); | ||||
|         // 重新初始化前缀树 | ||||
|         trie.clear(); | ||||
|         init(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 新增/修改敏感词后更新缓存和前缀树 | ||||
|      */ | ||||
|     public void updateCacheAfterModify() { | ||||
|         // 1. 清除旧缓存 | ||||
|         redisCache.deleteObject(SENSITIVE_WORD_CACHE_KEY); | ||||
|         // 2. 从数据库重新加载最新数据并更新缓存 | ||||
|         List<String> latestWords = loadSensitiveWordsFromDbAndCache(); | ||||
|         // 3. 重建前缀树 | ||||
|         trie.clear(); | ||||
|         trie.batchAddWords(latestWords); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,97 @@ | ||||
| package com.ruoyi.cms.util.sensitiveWord; | ||||
|  | ||||
| import org.springframework.stereotype.Component; | ||||
|  | ||||
| import java.util.*; | ||||
|  | ||||
| /** | ||||
|  * 前缀树(Trie树)节点 | ||||
|  */ | ||||
|  | ||||
|  | ||||
| /** | ||||
|  * 前缀树(Trie树)工具类 | ||||
|  */ | ||||
| @Component | ||||
| public class SensitiveWordTrie { | ||||
|     // 根节点(无实际字符) | ||||
|     private final TrieNode root = new TrieNode(); | ||||
|  | ||||
|     /** | ||||
|      * 向前缀树中添加敏感词 | ||||
|      */ | ||||
|     public void addWord(String word) { | ||||
|         if (word == null || word.isEmpty()) { | ||||
|             return; | ||||
|         } | ||||
|         TrieNode current = root; | ||||
|         for (char c : word.toCharArray()) { | ||||
|             // 若子节点中不存在该字符,则创建新节点 | ||||
|             current.getChildren().putIfAbsent(c, new TrieNode()); | ||||
|             // 移动到子节点 | ||||
|             current = current.getChildren().get(c); | ||||
|         } | ||||
|         // 标记当前节点为敏感词结尾 | ||||
|         current.setEnd(true); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 检测文本中所有敏感词(去重) | ||||
|      * @param text 待检测文本 | ||||
|      * @return 敏感词列表 | ||||
|      */ | ||||
|     public List<String> findAllSensitiveWords(String text) { | ||||
|         if (text == null || text.isEmpty()) { | ||||
|             return new ArrayList<>(); | ||||
|         } | ||||
|  | ||||
|         Set<String> sensitiveWords = new HashSet<>(); | ||||
|         TrieNode current = root; | ||||
|         StringBuilder currentWord = new StringBuilder(); // 记录当前匹配的字符 | ||||
|  | ||||
|         for (int i = 0; i < text.length(); i++) { | ||||
|             char c = text.charAt(i); | ||||
|  | ||||
|             // 若当前字符不在子节点中,重置匹配状态 | ||||
|             if (!current.getChildren().containsKey(c)) { | ||||
|                 current = root; // 回到根节点 | ||||
|                 currentWord.setLength(0); // 清空当前匹配的字符 | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             // 匹配到字符,移动到子节点 | ||||
|             current = current.getChildren().get(c); | ||||
|             currentWord.append(c); | ||||
|  | ||||
|             // 若当前节点是敏感词结尾,记录该敏感词 | ||||
|             if (current.isEnd()) { | ||||
|                 sensitiveWords.add(currentWord.toString()); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return new ArrayList<>(sensitiveWords); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 批量添加敏感词到前缀树 | ||||
|      * @param words 敏感词列表 | ||||
|      */ | ||||
|     public void batchAddWords(List<String> words) { | ||||
|         if (words == null || words.isEmpty()) { | ||||
|             return; | ||||
|         } | ||||
|         // 批量处理比循环单次添加更高效,减少重复判断 | ||||
|         for (String word : words) { | ||||
|             addWord(word); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 清空前缀树所有节点 | ||||
|      */ | ||||
|     public void clear() { | ||||
|         // 清空根节点的子节点映射,达到清空整个前缀树的效果 | ||||
|         root.getChildren().clear(); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,23 @@ | ||||
| package com.ruoyi.cms.util.sensitiveWord; | ||||
|  | ||||
| import java.util.HashMap; | ||||
| import java.util.Map; | ||||
|  | ||||
| class TrieNode { | ||||
|     // 子节点:key=字符,value=子节点 | ||||
|     private final Map<Character, TrieNode> children = new HashMap<>(); | ||||
|     // 标记当前节点是否为敏感词的结尾 | ||||
|     private boolean isEnd = false; | ||||
|  | ||||
|     public Map<Character, TrieNode> getChildren() { | ||||
|         return children; | ||||
|     } | ||||
|  | ||||
|     public boolean isEnd() { | ||||
|         return isEnd; | ||||
|     } | ||||
|  | ||||
|     public void setEnd(boolean end) { | ||||
|         isEnd = end; | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user