diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java index 312ddd7..e6cb963 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java @@ -2,6 +2,9 @@ package com.ruoyi.web.controller.system; import java.util.List; import java.util.Set; + +import com.ruoyi.common.core.domain.model.LoginSiteUser; +import com.ruoyi.common.utils.SiteSecurityUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import com.ruoyi.common.constant.Constants; @@ -56,6 +59,21 @@ public class SysLoginController ajax.put(Constants.TOKEN, token); return ajax; } + + @PostMapping("/app/appLogin") + public AjaxResult appLogin(@RequestBody LoginBody loginBody) + { + AjaxResult ajax = AjaxResult.success(); + // 若携带令牌且有效,直接返回 + if (SiteSecurityUtils.isLogin()) { + LoginSiteUser loginSiteUser = SiteSecurityUtils.getLoginUser(); + ajax.put(Constants.TOKEN, loginSiteUser.getToken()); + }else{ + ajax=loginService.appLogin(loginBody); + } + return ajax; + } + /** * 获取用户信息 * diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/app/AppUserController.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/app/AppUserController.java index 827d5dd..b9f650c 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/app/AppUserController.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/app/AppUserController.java @@ -116,4 +116,12 @@ public class AppUserController extends BaseController HashMap result = jobApplyService.statistics(); return AjaxResult.success(result); } + + @ApiOperation("根据条件查询用户信息") + @GetMapping("/list") + public AjaxResult getUserList(AppUser appUser) + { + List list = appUserService.selectAppUserList(appUser); + return AjaxResult.success(list); + } } diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/CmsJobController.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/CmsJobController.java index 7f1127a..97e7593 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/CmsJobController.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/CmsJobController.java @@ -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 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 sensitiveWords = sensitiveWordChecker.checkSensitiveWords(job.getDescription()); + if (!sensitiveWords.isEmpty()) { + String errorMsg = "描述中包含敏感词:" + String.join("、", sensitiveWords); + return AjaxResult.error(errorMsg); + } return toAjax(jobService.updateJob(job)); } diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/SensitiveWordDataController.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/SensitiveWordDataController.java index 35f637f..1b9b35f 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/SensitiveWordDataController.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/SensitiveWordDataController.java @@ -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(); + } } diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/Company.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/Company.java index 868fd52..bd9a88a 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/Company.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/Company.java @@ -105,4 +105,10 @@ public class Company extends BaseEntity @TableField(exist = false) @ApiModelProperty("驳回时间") private String rejectTime; + + @ApiModelProperty("是否本地重点企业") + private String isImpCompany; + + @ApiModelProperty("本地重点发展产业") + private String impCompanyType; } diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/SensitiveWordData.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/SensitiveWordData.java index 49ac2b8..7293329 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/SensitiveWordData.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/SensitiveWordData.java @@ -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; diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/SensitiveWordDataMapper.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/SensitiveWordDataMapper.java index 3fc6c6f..e7eafd3 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/SensitiveWordDataMapper.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/SensitiveWordDataMapper.java @@ -16,4 +16,6 @@ import java.util.List; public interface SensitiveWordDataMapper extends BaseMapper { List selectSensitiveworddataList(SensitiveWordData sensitiveWordData); + + int batchInsert(List list); } diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/IAppUserService.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/IAppUserService.java index d226ece..a6b0018 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/IAppUserService.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/IAppUserService.java @@ -51,4 +51,5 @@ public interface IAppUserService */ public int deleteAppUserByUserIds(Long[] userIds); + public AppUser getPhone(String phone); } diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/SensitiveWordDataService.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/SensitiveWordDataService.java index 1a51022..7a39a4c 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/SensitiveWordDataService.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/SensitiveWordDataService.java @@ -22,5 +22,7 @@ public interface SensitiveWordDataService { int updateSensitiveworddata(SensitiveWordData sensitiveWordData); int deleteSensitiveworddataIds(Long[] ids); + + int batchInsert(List list); } diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/AppUserServiceImpl.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/AppUserServiceImpl.java index 0df2df7..d8257c8 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/AppUserServiceImpl.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/AppUserServiceImpl.java @@ -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 imple return appUserMapper.deleteBatchIds(Arrays.asList(userIds)); } + @Override + public AppUser getPhone(String phone) { + return appUserMapper.selectOne(new LambdaQueryWrapper() + .eq(AppUser::getPhone, phone)); + } } diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/SensitiveWordDataServiceImpl.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/SensitiveWordDataServiceImpl.java index bb3aea1..6cd4c8e 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/SensitiveWordDataServiceImpl.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/SensitiveWordDataServiceImpl.java @@ -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 list) { + return sensitiveWordDataMapper.batchInsert(list); + } } diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/EasyExcelUtils.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/EasyExcelUtils.java new file mode 100644 index 0000000..39f45fe --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/EasyExcelUtils.java @@ -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 实体类泛型 + * @return 解析后的数据集 + */ + public static List readExcel(File file, Class head) { + return EasyExcel.read(file) + .head(head) + .sheet() + .doReadSync(); + } + + /** + * 读取 Excel 输入流(一次性读取所有数据) + * + * @param inputStream Excel 输入流(如 MultipartFile 的 getInputStream()) + * @param head 实体类字节码 + * @param 实体类泛型 + * @return 解析后的数据集 + */ + public static List readExcel(InputStream inputStream, Class head) { + return EasyExcel.read(inputStream) + .head(head) + .sheet() + .doReadSync(); + } + + /** + * 分批读取 Excel(适用于大数据量,避免内存溢出) + * + * @param inputStream Excel 输入流 + * @param head 实体类字节码 + * @param batchSize 每批处理的数据量 + * @param consumer 数据处理函数(如批量保存到数据库) + * @param 实体类泛型 + */ + public static void readExcelByBatch(InputStream inputStream, Class head, int batchSize, Consumer> consumer) { + EasyExcel.read(inputStream) + .head(head) + .sheet() + .registerReadListener(new AnalysisEventListener() { + private List 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 实体类泛型 + */ + public static void writeExcel(OutputStream outputStream, List data, Class 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 实体类泛型 + * @throws IOException IO异常 + */ + public static void downloadExcel(HttpServletResponse response, List data, Class 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 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(); + } + } + } + } + } +} \ No newline at end of file diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/WechatUtil.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/WechatUtil.java index d01a0d4..d55a80e 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/WechatUtil.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/WechatUtil.java @@ -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(); } diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/sensitiveWord/SensitiveWordChecker.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/sensitiveWord/SensitiveWordChecker.java new file mode 100644 index 0000000..85dab98 --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/sensitiveWord/SensitiveWordChecker.java @@ -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 sensitiveWords = getSensitiveWordsFromCache(); + if (sensitiveWords.isEmpty()) { + // 缓存未命中,从数据库加载 + sensitiveWords = loadSensitiveWordsFromDbAndCache(); + } + // 初始化前缀树 + trie.batchAddWords(sensitiveWords); + } + + /** + * 检测文本中的敏感词 + * @param text 待检测文本(如job.getDescription()) + * @return 敏感词列表(空列表表示无敏感词) + */ + public List checkSensitiveWords(String text) { + return trie.findAllSensitiveWords(text); + } + + /** + * 从缓存获取敏感词列表 + */ + @Cacheable(value = SENSITIVE_WORD_CACHE_KEY) + public List 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 loadSensitiveWordsFromDbAndCache() { + List wordList = sensitiveWordDataMapper.selectSensitiveworddataList(new SensitiveWordData()); + List 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 latestWords = loadSensitiveWordsFromDbAndCache(); + // 3. 重建前缀树 + trie.clear(); + trie.batchAddWords(latestWords); + } +} \ No newline at end of file diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/sensitiveWord/SensitiveWordTrie.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/sensitiveWord/SensitiveWordTrie.java new file mode 100644 index 0000000..3a0bd59 --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/sensitiveWord/SensitiveWordTrie.java @@ -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 findAllSensitiveWords(String text) { + if (text == null || text.isEmpty()) { + return new ArrayList<>(); + } + + Set 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 words) { + if (words == null || words.isEmpty()) { + return; + } + // 批量处理比循环单次添加更高效,减少重复判断 + for (String word : words) { + addWord(word); + } + } + + /** + * 清空前缀树所有节点 + */ + public void clear() { + // 清空根节点的子节点映射,达到清空整个前缀树的效果 + root.getChildren().clear(); + } + +} \ No newline at end of file diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/sensitiveWord/TrieNode.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/sensitiveWord/TrieNode.java new file mode 100644 index 0000000..fb429fa --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/sensitiveWord/TrieNode.java @@ -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 children = new HashMap<>(); + // 标记当前节点是否为敏感词的结尾 + private boolean isEnd = false; + + public Map getChildren() { + return children; + } + + public boolean isEnd() { + return isEnd; + } + + public void setEnd(boolean end) { + isEnd = end; + } +} \ No newline at end of file diff --git a/ruoyi-bussiness/src/main/resources/mapper/app/SensitiveWordDataMapper.xml b/ruoyi-bussiness/src/main/resources/mapper/app/SensitiveWordDataMapper.xml index 0144446..b8af9bb 100644 --- a/ruoyi-bussiness/src/main/resources/mapper/app/SensitiveWordDataMapper.xml +++ b/ruoyi-bussiness/src/main/resources/mapper/app/SensitiveWordDataMapper.xml @@ -29,4 +29,12 @@ + + INSERT INTO sensitive_word_data (sensitive_word, type, create_time,create_by,del_flag,remark) + VALUES + + (#{item.sensitiveWord}, #{item.type}, #{item.createTime},#{item.createBy},#{item.delFlag},#{item.remark}) + + + diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/AppUser.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/AppUser.java index 2252586..f5f3792 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/AppUser.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/AppUser.java @@ -113,4 +113,15 @@ public class AppUser extends BaseEntity @TableField(exist = false) @ApiModelProperty("密码") private String password; + + /** + * 微信小程序用户唯一标识 + */ + @ApiModelProperty("微信小程序用户唯一标识") + private String openid; + /** + * 微信开放平台全局标识 + */ + @ApiModelProperty("微信开放平台全局标识") + private String unionid; } diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginBody.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginBody.java index b5bc8c8..6913a49 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginBody.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginBody.java @@ -26,6 +26,14 @@ public class LoginBody * 唯一标识 */ private String uuid; + /** + * 微信加密的手机号数据 + */ + private String encryptedData; + /** + * 加密向量 + */ + private String iv; public String getUsername() { @@ -66,4 +74,20 @@ public class LoginBody { this.uuid = uuid; } + + public String getEncryptedData() { + return encryptedData; + } + + public void setEncryptedData(String encryptedData) { + this.encryptedData = encryptedData; + } + + public String getIv() { + return iv; + } + + public void setIv(String iv) { + this.iv = iv; + } } diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java index de54348..998a3a7 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java @@ -2,7 +2,12 @@ package com.ruoyi.framework.web.service; import javax.annotation.Resource; +import com.alibaba.fastjson.JSONObject; +import com.ruoyi.cms.service.IAppUserService; +import com.ruoyi.cms.util.WechatUtil; +import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.domain.entity.AppUser; +import com.ruoyi.common.core.domain.model.LoginBody; import com.ruoyi.common.core.domain.model.LoginSiteUser; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationManager; @@ -57,6 +62,10 @@ public class SysLoginService @Autowired private TokenSiteService tokenSiteService; + @Autowired + WechatUtil wechatUtil; + @Autowired + private IAppUserService appUserService; /** * 登录验证 * @@ -118,6 +127,24 @@ public class SysLoginService // 生成token return tokenSiteService.createToken(loginSiteUser); } + + /** + * 根据微信生成的 + * @param appUser + * @return + */ + public String loginUserIdApp(AppUser appUser) + { + LoginSiteUser loginSiteUser = new LoginSiteUser(); + loginSiteUser.setUserId(appUser.getUserId()); + loginSiteUser.setUser(appUser); + recordLoginInfo(appUser); + AsyncManager.me().execute(AsyncFactory.recordLogininfor(appUser.getName(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"))); + recordLoginInfo(appUser); + // 生成token + return tokenSiteService.createToken(loginSiteUser); + } + //单点登录 public String loginOss(String ticket) { @@ -209,4 +236,44 @@ public class SysLoginService sysUser.setLoginDate(DateUtils.getNowDate()); userService.updateUserProfile(sysUser); } + + /** + * 小程序微信授权登录 + * @param dto + * @return + */ + public AjaxResult appLogin(LoginBody dto){ + JSONObject sessionInfo = wechatUtil.code2Session("appid", "secret", dto.getCode()); + String openid = sessionInfo.getString("openid"); + String unionid = sessionInfo.getString("unionid"); + String sessionKey = sessionInfo.getString("session_key"); + if (openid == null) { + return AjaxResult.error("微信授权失败"); + } + + JSONObject phoneInfo = wechatUtil.decryptPhoneNumber(dto.getEncryptedData(), sessionKey, dto.getIv()); + String phone = phoneInfo.getString("phoneNumber"); + if (phone == null) { + return AjaxResult.error("获取手机号失败"); + } + + // 3. 检查手机号是否已被绑定 + AppUser existUser = appUserService.getPhone(phone); + if (existUser != null) { + return AjaxResult.error("该手机号已注册"); + } + + // 4. 创建用户并存储所有信息 + AppUser appUser = new AppUser(); + appUser.setOpenid(openid); + appUser.setUnionid(unionid); + appUser.setPhone(phone); + appUserService.insertAppUser(appUser); + + // 5. 生成系统令牌 + AjaxResult ajax = AjaxResult.success(); + String token = loginUserIdApp(appUser); + ajax.put(Constants.TOKEN, token); + return ajax; + } }