添加ocr识别简历
This commit is contained in:
@@ -140,6 +140,13 @@ mybatis-plus:
|
|||||||
file:
|
file:
|
||||||
upload-dir: /data/file
|
upload-dir: /data/file
|
||||||
|
|
||||||
|
ocr:
|
||||||
|
ocr_url: http://127.0.0.1:9001/ocr
|
||||||
|
ocr_mutipart: https://qd.zhaopinzao8dian.com/ocr-api/ocr
|
||||||
|
# ocr_mutipart: http://10.98.80.141:9000/ocr
|
||||||
|
ocr_llm_url: http://39.98.44.136:6016/inner-ai/aicoapi/gateway/v2/chatbot/api_run/1763386387_d4c07131-a047-4c0d-9623-7e3c3a45bd7e
|
||||||
|
ocr_llm_apiKey: NfzPnFRtogHlYCAh2hHIB7ra5EsrSQEM
|
||||||
|
|
||||||
#nginx节点健康检查
|
#nginx节点健康检查
|
||||||
management:
|
management:
|
||||||
endpoints:
|
endpoints:
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import io.swagger.annotations.Api;
|
|||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import java.io.BufferedOutputStream;
|
import java.io.BufferedOutputStream;
|
||||||
@@ -251,4 +252,12 @@ public class AppUserController extends BaseController
|
|||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiOperation("简历识别")
|
||||||
|
@PostMapping("/resume/recognition")
|
||||||
|
@ResponseBody
|
||||||
|
@BussinessLog(title = "简历识别")
|
||||||
|
public AjaxResult recognition(@RequestParam("file") MultipartFile file) throws Exception{
|
||||||
|
return appUserService.recognition(file.getBytes(),file.getOriginalFilename(),SiteSecurityUtils.getUserId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package com.ruoyi.cms.domain;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import com.ruoyi.common.annotation.CustomSensitive;
|
||||||
|
import com.ruoyi.common.core.domain.entity.AppSkill;
|
||||||
|
import com.ruoyi.common.core.domain.entity.UserWorkExperiences;
|
||||||
|
import com.ruoyi.common.enums.SensitiveType;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APP用户对象 app_user
|
||||||
|
* @author lishundong
|
||||||
|
* @date 2024-09-03
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class UserInfoDetail
|
||||||
|
{
|
||||||
|
|
||||||
|
@ApiModelProperty("用户ID")
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
@ApiModelProperty("用户名称")
|
||||||
|
@CustomSensitive(type = SensitiveType.CHINESE_NAME)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@ApiModelProperty("年龄段 对应字典age")
|
||||||
|
private String age;
|
||||||
|
|
||||||
|
@ApiModelProperty("用户性别(0男 1女)对应字典sex")
|
||||||
|
private String sex;
|
||||||
|
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||||
|
@ApiModelProperty("生日")
|
||||||
|
private Date birthDate;
|
||||||
|
|
||||||
|
@ApiModelProperty("学历 对应字典education")
|
||||||
|
private String education;
|
||||||
|
|
||||||
|
@ApiModelProperty("政治面貌")
|
||||||
|
private String politicalAffiliation;
|
||||||
|
|
||||||
|
@ApiModelProperty("手机号码")
|
||||||
|
@CustomSensitive(type = SensitiveType.PHONE)
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
@ApiModelProperty("头像地址")
|
||||||
|
private String avatar;
|
||||||
|
|
||||||
|
@ApiModelProperty("最低工资")
|
||||||
|
private String salaryMin;
|
||||||
|
|
||||||
|
@ApiModelProperty("最高工资")
|
||||||
|
private String salaryMax;
|
||||||
|
|
||||||
|
@ApiModelProperty("期望工作地 对应字典area")
|
||||||
|
private String area;
|
||||||
|
|
||||||
|
@ApiModelProperty("期望岗位,逗号分隔")
|
||||||
|
private String jobTitleId;
|
||||||
|
|
||||||
|
@ApiModelProperty("期望薪资")
|
||||||
|
private String experience;
|
||||||
|
|
||||||
|
@ApiModelProperty("期望岗位列表")
|
||||||
|
private List<String> jobTitle;
|
||||||
|
|
||||||
|
@ApiModelProperty("身份证号码")
|
||||||
|
@CustomSensitive(type = SensitiveType.ID_CARD)
|
||||||
|
private String idNumber;
|
||||||
|
|
||||||
|
@ApiModelProperty("个人简要介绍")
|
||||||
|
private String introduction;
|
||||||
|
|
||||||
|
@ApiModelProperty("个人自我评价")
|
||||||
|
private String selfEvaluation;
|
||||||
|
|
||||||
|
@ApiModelProperty("联系邮箱")
|
||||||
|
@CustomSensitive(type = SensitiveType.EMAIL)
|
||||||
|
private String contactEmail;
|
||||||
|
|
||||||
|
@ApiModelProperty("求职意向岗位")
|
||||||
|
private String jobIntention;
|
||||||
|
|
||||||
|
@ApiModelProperty("毕业院校")
|
||||||
|
private String graduationSchool;
|
||||||
|
|
||||||
|
@ApiModelProperty("工作年限")
|
||||||
|
private Integer workYears;
|
||||||
|
|
||||||
|
@ApiModelProperty("居住地址")
|
||||||
|
@CustomSensitive(type = SensitiveType.LIVE_ADDRESS)
|
||||||
|
private String residenceAddress;
|
||||||
|
|
||||||
|
@ApiModelProperty("就读专业")
|
||||||
|
private String major;
|
||||||
|
|
||||||
|
@ApiModelProperty("工作经历数组")
|
||||||
|
private List<UserWorkExperiences> workExp;
|
||||||
|
@ApiModelProperty("个人专业技能列表")
|
||||||
|
private List<AppSkill> skillList;
|
||||||
|
|
||||||
|
private String resumeOcrStatus;
|
||||||
|
private List<String> resumeList;
|
||||||
|
|
||||||
|
@ApiModelProperty("求职者标签列表")
|
||||||
|
private List<String> indices;
|
||||||
|
|
||||||
|
@ApiModelProperty("是否已认证,0是,1否")
|
||||||
|
private String isCert;
|
||||||
|
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package com.ruoyi.cms.service;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.ruoyi.common.core.domain.AjaxResult;
|
||||||
import com.ruoyi.common.core.domain.entity.AppUserShow;
|
import com.ruoyi.common.core.domain.entity.AppUserShow;
|
||||||
import com.ruoyi.common.core.domain.entity.MyChart;
|
import com.ruoyi.common.core.domain.entity.MyChart;
|
||||||
import com.ruoyi.common.core.domain.entity.AppUser;
|
import com.ruoyi.common.core.domain.entity.AppUser;
|
||||||
@@ -85,4 +86,6 @@ public interface IAppUserService
|
|||||||
public AppUser getYtjValidPhone(String phone);
|
public AppUser getYtjValidPhone(String phone);
|
||||||
|
|
||||||
public AppUser getYtjValidIdcard(String phone);
|
public AppUser getYtjValidIdcard(String phone);
|
||||||
|
|
||||||
|
AjaxResult recognition(byte[] bytes, String fileName, Long userId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
package com.ruoyi.cms.service.impl;
|
package com.ruoyi.cms.service.impl;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollectionUtil;
|
||||||
import com.alibaba.fastjson.JSON;
|
import com.alibaba.fastjson.JSON;
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
|
import com.ruoyi.cms.domain.UserInfoDetail;
|
||||||
|
import com.ruoyi.cms.util.AppUserFieldCustomCopy;
|
||||||
|
import com.ruoyi.common.core.domain.AjaxResult;
|
||||||
import com.ruoyi.common.core.domain.entity.MyChart;
|
import com.ruoyi.common.core.domain.entity.MyChart;
|
||||||
import com.ruoyi.common.core.domain.entity.File;
|
import com.ruoyi.common.core.domain.entity.File;
|
||||||
import com.ruoyi.cms.domain.vo.AppSkillVo;
|
import com.ruoyi.cms.domain.vo.AppSkillVo;
|
||||||
@@ -18,11 +24,22 @@ import com.ruoyi.common.core.domain.model.RegisterBody;
|
|||||||
import com.ruoyi.common.utils.SecurityUtils;
|
import com.ruoyi.common.utils.SecurityUtils;
|
||||||
import com.ruoyi.common.utils.SiteSecurityUtils;
|
import com.ruoyi.common.utils.SiteSecurityUtils;
|
||||||
import com.ruoyi.common.utils.StringUtils;
|
import com.ruoyi.common.utils.StringUtils;
|
||||||
|
import com.ruoyi.common.utils.bean.BeanUtils;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.core.io.ByteArrayResource;
|
||||||
|
import org.springframework.http.HttpEntity;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import com.ruoyi.cms.service.IAppUserService;
|
import com.ruoyi.cms.service.IAppUserService;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
|
import org.springframework.util.MultiValueMap;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* APP用户Service业务层处理
|
* APP用户Service业务层处理
|
||||||
@@ -48,6 +65,16 @@ public class AppUserServiceImpl extends ServiceImpl<AppUserMapper,AppUser> imple
|
|||||||
@Autowired
|
@Autowired
|
||||||
private FileMapper fileMapper;
|
private FileMapper fileMapper;
|
||||||
|
|
||||||
|
@Value("${ocr.ocr_mutipart}")
|
||||||
|
private String ocrMutipartUrl;
|
||||||
|
// 大模型配置
|
||||||
|
@Value("${ocr.ocr_llm_url}")
|
||||||
|
private String llmApiUrl;
|
||||||
|
@Value("${ocr.ocr_llm_apiKey}")
|
||||||
|
private String llmApiKey;
|
||||||
|
|
||||||
|
private final RestTemplate restTemplate = new RestTemplate();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询APP用户
|
* 查询APP用户
|
||||||
*
|
*
|
||||||
@@ -744,4 +771,189 @@ public class AppUserServiceImpl extends ServiceImpl<AppUserMapper,AppUser> imple
|
|||||||
.eq(AppUser::getDelFlag,"0")
|
.eq(AppUser::getDelFlag,"0")
|
||||||
.orderByDesc(AppUser::getUpdateTime).last("LIMIT 1");
|
.orderByDesc(AppUser::getUpdateTime).last("LIMIT 1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AjaxResult recognition(byte[] bytes, String fileName, Long userId){
|
||||||
|
// --- 第一步:调用 OCR 服务 ---
|
||||||
|
String ocrText = callOcrService(bytes,fileName);
|
||||||
|
|
||||||
|
if (ocrText == null || ocrText.isEmpty()) {
|
||||||
|
return AjaxResult.error("OCR识别内容为空或失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 第二步:调用大模型进行简历分析 ---
|
||||||
|
JSONObject llmResult = callLlmService(ocrText,userId);
|
||||||
|
if(llmResult.containsKey("error")){
|
||||||
|
return AjaxResult.error(llmResult.getString("error"));
|
||||||
|
}
|
||||||
|
return AjaxResult.success(llmResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用 OCR 接口
|
||||||
|
*/
|
||||||
|
private String callOcrService(byte[] bytes,String fileName) {
|
||||||
|
try {
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||||
|
|
||||||
|
// 构建文件资源,重写 getFilename 确保服务端能识别文件名
|
||||||
|
ByteArrayResource fileResource = new ByteArrayResource(bytes) {
|
||||||
|
@Override
|
||||||
|
public String getFilename() {
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||||
|
body.add("file", fileResource);
|
||||||
|
|
||||||
|
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
ResponseEntity<JSONObject> response = restTemplate.postForEntity(ocrMutipartUrl, requestEntity, JSONObject.class);
|
||||||
|
JSONObject resultBody = response.getBody();
|
||||||
|
|
||||||
|
if (resultBody != null && resultBody.getInteger("code") == 200) {
|
||||||
|
return resultBody.getString("data"); // 获取识别出的文本
|
||||||
|
} else {
|
||||||
|
// 记录日志:OCR调用失败 msg: resultBody.getString("msg")
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用大模型接口
|
||||||
|
*/
|
||||||
|
private JSONObject callLlmService(String contextText,Long userId) {
|
||||||
|
// 1. 创建一个具有长超时时间的 Factory
|
||||||
|
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
|
||||||
|
// 设置连接超时(握手时间),10秒通常够了
|
||||||
|
factory.setConnectTimeout(10000);
|
||||||
|
// 【关键】设置读取超时。大模型分析简历可能需要较长时间,建议设为 60秒 或 120秒
|
||||||
|
factory.setReadTimeout(120000);
|
||||||
|
|
||||||
|
// 2. 使用这个 Factory 创建一个新的 RestTemplate
|
||||||
|
RestTemplate longTimeoutRestTemplate = new RestTemplate(factory);
|
||||||
|
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
|
headers.set("Authorization", "Bearer " + llmApiKey);
|
||||||
|
|
||||||
|
JSONObject requestBody = new JSONObject();
|
||||||
|
requestBody.put("doc_list", new Object[]{});
|
||||||
|
requestBody.put("image_url", "");
|
||||||
|
requestBody.put("query", contextText);
|
||||||
|
requestBody.put("session_id", "");
|
||||||
|
// 保持 false,让后端等待所有结果生成完再一次性拿回
|
||||||
|
requestBody.put("stream", false);
|
||||||
|
|
||||||
|
HttpEntity<JSONObject> entity = new HttpEntity<>(requestBody, headers);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 3. 使用新的 longTimeoutRestTemplate 发送请求
|
||||||
|
ResponseEntity<JSONObject> response = longTimeoutRestTemplate.postForEntity(llmApiUrl, entity, JSONObject.class);
|
||||||
|
JSONObject o = response.getBody();
|
||||||
|
if(Objects.nonNull(userId)&&userId > 0){
|
||||||
|
String msg = buildUserDetailInfo(o,userId);
|
||||||
|
o = new JSONObject();
|
||||||
|
if(msg.equals("error")){
|
||||||
|
o.put("error",msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return o;
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
JSONObject error = new JSONObject();
|
||||||
|
// 打印具体错误以便调试
|
||||||
|
error.put("error", "LLM调用失败: " + e.getMessage());
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildUserDetailInfo(JSONObject object,Long userId){
|
||||||
|
String msg = "";
|
||||||
|
try {
|
||||||
|
if("200".equals(object.getString("code"))){
|
||||||
|
object = object.getJSONObject("data");
|
||||||
|
object = object.getJSONObject("data");
|
||||||
|
object = object.getJSONObject("output");
|
||||||
|
UserInfoDetail detail = JSONObject.parseObject(object.toJSONString(),UserInfoDetail.class);
|
||||||
|
if(StringUtils.isNotEmpty(detail.getArea())){
|
||||||
|
String area = detail.getArea();
|
||||||
|
area = area.replaceAll("\\[","");
|
||||||
|
area = area.replaceAll("\\]","");
|
||||||
|
area = area.trim();
|
||||||
|
detail.setArea(area);
|
||||||
|
}
|
||||||
|
detail.setUserId(userId);
|
||||||
|
editUserOtherInfo(detail,true);
|
||||||
|
}
|
||||||
|
}catch (Exception e){
|
||||||
|
e.printStackTrace();
|
||||||
|
msg = e.getMessage();
|
||||||
|
}
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void editUserOtherInfo(UserInfoDetail detail, boolean flag) throws Exception {
|
||||||
|
try {
|
||||||
|
Long userId = detail.getUserId();
|
||||||
|
AppUser appUser = appUserMapper.selectById(userId);
|
||||||
|
//先将appUser没有的或不同的数据拷给appUser
|
||||||
|
if(flag){
|
||||||
|
//条件拷贝,当detail数据来源于第三方或简历解析时,已现有的appUser数据为主
|
||||||
|
appUser = new AppUserFieldCustomCopy().conditionalCopy(appUser,detail);
|
||||||
|
}else{
|
||||||
|
BeanUtils.copyPropertiesIgnoreNull(appUser, detail);
|
||||||
|
}
|
||||||
|
//再将detail没有的数据拷给它
|
||||||
|
BeanUtils.copyPropertiesIgnoreNull(detail, appUser);
|
||||||
|
|
||||||
|
String regex = "[@#$%&*-_ ]"; // 黑名单字符,空格也写在这里面
|
||||||
|
|
||||||
|
if(StringUtils.isNotEmpty(appUser.getName())&& Pattern.compile(regex).matcher(appUser.getName()).find()){
|
||||||
|
appUser.setName(null);
|
||||||
|
}
|
||||||
|
if(StringUtils.isNotEmpty(appUser.getPhone())&&Pattern.compile(regex).matcher(appUser.getPhone()).find()){
|
||||||
|
appUser.setPhone(null);
|
||||||
|
}
|
||||||
|
if(StringUtils.isNotEmpty(appUser.getIdCard())&&Pattern.compile(regex).matcher(appUser.getIdCard()).find()){
|
||||||
|
appUser.setIdCard(null);
|
||||||
|
}
|
||||||
|
if(StringUtils.isNotEmpty(appUser.getAddress())&&Pattern.compile(regex).matcher(appUser.getAddress()).find()){
|
||||||
|
appUser.setAddress(null);
|
||||||
|
}
|
||||||
|
appUserMapper.updateById(appUser);
|
||||||
|
|
||||||
|
if(CollectionUtil.isNotEmpty(detail.getWorkExp())){
|
||||||
|
List<UserWorkExperiences> list = userWorkExperiencesMapper.selectList(Wrappers.lambdaQuery(UserWorkExperiences.class).eq(UserWorkExperiences::getUserId,userId));
|
||||||
|
if(CollectionUtil.isEmpty(list)){
|
||||||
|
for(UserWorkExperiences workExperience : detail.getWorkExp()){
|
||||||
|
workExperience.setUserId(userId);
|
||||||
|
userWorkExperiencesMapper.insert(workExperience);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(CollectionUtil.isNotEmpty(detail.getSkillList())){
|
||||||
|
List<AppSkill> list = appSkillMapper.selectList(Wrappers.lambdaQuery(AppSkill.class).eq(AppSkill::getUserId,userId));
|
||||||
|
if(CollectionUtil.isNotEmpty(list)){
|
||||||
|
appSkillMapper.deleteBatchIds(list.stream().map(AppSkill::getId).collect(Collectors.toList()));
|
||||||
|
}
|
||||||
|
for(AppSkill skill : detail.getSkillList()){
|
||||||
|
skill.setUserId(userId);
|
||||||
|
appSkillMapper.insert(skill);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
throw new Exception(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.ruoyi.cms.util;
|
||||||
|
|
||||||
|
import com.ruoyi.cms.domain.UserInfoDetail;
|
||||||
|
import com.ruoyi.common.core.domain.entity.AppUser;
|
||||||
|
import com.ruoyi.common.utils.StringUtils;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public class AppUserFieldCustomCopy {
|
||||||
|
|
||||||
|
private boolean isNull(Object o){
|
||||||
|
if(Objects.isNull(o)|| StringUtils.isEmpty(o.toString())){
|
||||||
|
return true;
|
||||||
|
}else{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public AppUser conditionalCopy(AppUser appUser, UserInfoDetail detail) throws IllegalAccessException {
|
||||||
|
Field[] fields = appUser.getClass().getDeclaredFields();
|
||||||
|
Field sourceField = null;
|
||||||
|
Object sourceFieldValue = null;
|
||||||
|
for (Field field : fields) {
|
||||||
|
field.setAccessible(true);
|
||||||
|
if (!isNull(field.get(appUser))){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
sourceField = detail.getClass().getDeclaredField(field.getName());
|
||||||
|
}catch (Exception e){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
sourceField.setAccessible(true);
|
||||||
|
sourceFieldValue = sourceField.get(detail);
|
||||||
|
field.set(appUser, sourceFieldValue);
|
||||||
|
}
|
||||||
|
return appUser;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.ruoyi.common.annotation;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
|
import com.ruoyi.common.config.serializer.SensitiveSerialize;
|
||||||
|
import com.ruoyi.common.enums.SensitiveType;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义数据脱敏注解
|
||||||
|
*/
|
||||||
|
@Target(ElementType.FIELD)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@JacksonAnnotationsInside
|
||||||
|
@JsonSerialize(using = SensitiveSerialize.class)
|
||||||
|
public @interface CustomSensitive {
|
||||||
|
|
||||||
|
// 脱敏类型
|
||||||
|
SensitiveType type() default SensitiveType.DEFAULT;
|
||||||
|
|
||||||
|
// 自定义前缀保留长度
|
||||||
|
int prefix() default 0;
|
||||||
|
|
||||||
|
// 自定义后缀保留长度
|
||||||
|
int suffix() default 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
package com.ruoyi.common.config.serializer;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonGenerator;
|
||||||
|
import com.fasterxml.jackson.databind.BeanProperty;
|
||||||
|
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||||
|
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||||
|
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||||
|
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
|
||||||
|
import com.ruoyi.common.annotation.CustomSensitive;
|
||||||
|
import com.ruoyi.common.enums.SensitiveType;
|
||||||
|
import com.ruoyi.common.utils.StringUtils;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据回显前端前的数据脱敏序列化实现类
|
||||||
|
*/
|
||||||
|
public class SensitiveSerialize extends JsonSerializer<String> implements ContextualSerializer {
|
||||||
|
|
||||||
|
private SensitiveType type;
|
||||||
|
private int prefix;
|
||||||
|
private int suffix;
|
||||||
|
|
||||||
|
public SensitiveSerialize() {}
|
||||||
|
|
||||||
|
public SensitiveSerialize(SensitiveType type, int prefix, int suffix) {
|
||||||
|
this.type = type;
|
||||||
|
this.prefix = prefix;
|
||||||
|
this.suffix = suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers)
|
||||||
|
throws IOException {
|
||||||
|
if (StringUtils.isBlank(value)) {
|
||||||
|
gen.writeString(value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String result;
|
||||||
|
switch (type) {
|
||||||
|
case CHINESE_NAME:
|
||||||
|
result = desensitizeChineseName(value);
|
||||||
|
break;
|
||||||
|
case ENGLISH_NAME:
|
||||||
|
result = desensitizeEnglishName(value);
|
||||||
|
break;
|
||||||
|
case ID_CARD:
|
||||||
|
result = desensitizeIdCard(value);
|
||||||
|
break;
|
||||||
|
case PHONE:
|
||||||
|
result = desensitizePhone(value);
|
||||||
|
break;
|
||||||
|
case EMAIL:
|
||||||
|
result = desensitizeEmail(value);
|
||||||
|
break;
|
||||||
|
case BANK_CARD:
|
||||||
|
result = desensitizeBankCard(value);
|
||||||
|
break;
|
||||||
|
case LIVE_ADDRESS:
|
||||||
|
result = desensitizeLiveAddress(value);
|
||||||
|
break;
|
||||||
|
case CUSTOM:
|
||||||
|
result = desensitizeCustom(value, prefix, suffix);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
result = desensitizeDefault(value);
|
||||||
|
}
|
||||||
|
gen.writeString(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property)
|
||||||
|
throws JsonMappingException
|
||||||
|
{
|
||||||
|
CustomSensitive annotation = property.getAnnotation(CustomSensitive.class);
|
||||||
|
if (Objects.nonNull(annotation) && Objects.equals(String.class, property.getType().getRawClass()))
|
||||||
|
{
|
||||||
|
this.type = annotation.type();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return prov.findValueSerializer(property.getType(), property);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String desensitizeChineseName(String name) {
|
||||||
|
if (name.length() <= 1) return name;
|
||||||
|
String str = "";
|
||||||
|
for(int i = 0; i < name.length()-1; i++) {
|
||||||
|
str = str+"*";
|
||||||
|
}
|
||||||
|
return str+name.charAt(name.length()-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String desensitizeEnglishName(String fullName) {
|
||||||
|
// 先 trim 再按一个或多个空格分段
|
||||||
|
String[] parts = fullName.trim().split("\\s+");
|
||||||
|
if (parts.length < 2) {
|
||||||
|
return fullName.trim(); // 只有一段,保持原样
|
||||||
|
}
|
||||||
|
// 脱敏第一段
|
||||||
|
String first = parts[0];
|
||||||
|
if (first.length() == 1) {
|
||||||
|
parts[0] = "*"; // 单字符直接变 *
|
||||||
|
} else {
|
||||||
|
char last = first.charAt(first.length() - 1);
|
||||||
|
StringBuilder sb = new StringBuilder(first.length());
|
||||||
|
for (int i = 0; i < first.length() - 1; i++) {
|
||||||
|
sb.append('*');
|
||||||
|
}
|
||||||
|
sb.append(last);
|
||||||
|
parts[0] = sb.toString();
|
||||||
|
}
|
||||||
|
// 用单个空格重新拼接
|
||||||
|
return String.join(" ", parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String desensitizeIdCard(String idCard) {
|
||||||
|
String str = "";
|
||||||
|
for (int i = 0; i < idCard.length(); i++) {
|
||||||
|
if(i < 1 || i > idCard.length()-2) {
|
||||||
|
str = str+idCard.charAt(i);
|
||||||
|
}else{
|
||||||
|
str = str+"*";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String desensitizePhone(String phone) {
|
||||||
|
String str = "";
|
||||||
|
for (int i = 0; i < phone.length(); i++) {
|
||||||
|
if(i < 3 || i > phone.length()-3) {
|
||||||
|
str = str+phone.charAt(i);
|
||||||
|
}else{
|
||||||
|
str = str+"*";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String desensitizeEmail(String email) {
|
||||||
|
int atIndex = email.indexOf("@");
|
||||||
|
if(atIndex > -1){
|
||||||
|
String[] strs = email.split("@");
|
||||||
|
String str = "";
|
||||||
|
for (int i = 0; i < strs[0].length(); i++) {
|
||||||
|
str = str+"*";
|
||||||
|
}
|
||||||
|
str = str+"@"+strs[1];
|
||||||
|
return str;
|
||||||
|
}else{
|
||||||
|
return email;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String desensitizeBankCard(String bankCard) {
|
||||||
|
String str = "";
|
||||||
|
for (int i = 0; i < bankCard.length(); i++) {
|
||||||
|
if(i > bankCard.length()-5) {
|
||||||
|
str = str+bankCard.charAt(i);
|
||||||
|
}else{
|
||||||
|
str = str+"*";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String desensitizeCustom(String value, int prefix, int suffix) {
|
||||||
|
if (value.length() <= prefix + suffix) return value;
|
||||||
|
String prefixStr = value.substring(0, prefix);
|
||||||
|
String suffixStr = suffix > 0 ? value.substring(value.length() - suffix) : "";
|
||||||
|
int maskLength = value.length() - prefix - suffix;
|
||||||
|
String mask = StringUtils.repeat('*', maskLength);
|
||||||
|
return prefixStr + mask + suffixStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String desensitizeLiveAddress(String value) {
|
||||||
|
int length = value.length();
|
||||||
|
int num = 0;
|
||||||
|
if(length >= 12){
|
||||||
|
num = 6;
|
||||||
|
}else{
|
||||||
|
num = (int) Math.floor(length/2f);
|
||||||
|
}
|
||||||
|
value = value.substring(0,num)+"********";
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String desensitizeDefault(String value) {
|
||||||
|
if (value.length() <= 2){
|
||||||
|
return value.substring(0, 1) + "*";
|
||||||
|
};
|
||||||
|
if(value.length() > 2 && value.length() < 9){
|
||||||
|
return value.substring(0, 1) + "*" + value.substring(value.length() - 1);
|
||||||
|
}
|
||||||
|
if(value.length()%3 == 0){
|
||||||
|
int num = value.length()/3;
|
||||||
|
return value.substring(0, num) + "*" + value.substring(value.length() - num);
|
||||||
|
}else{
|
||||||
|
double num = Math.floor(value.length()/3f);
|
||||||
|
String number = String.format("%.0f",num);
|
||||||
|
return value.substring(0, Integer.parseInt(number)+1) + "*" + value.substring(value.length()-Integer.parseInt(number));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package com.ruoyi.common.enums;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据脱敏类型枚举
|
||||||
|
*/
|
||||||
|
public enum SensitiveType {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认(缺省值脱敏)
|
||||||
|
*/
|
||||||
|
DEFAULT,
|
||||||
|
/**
|
||||||
|
* 中文名
|
||||||
|
*/
|
||||||
|
CHINESE_NAME,
|
||||||
|
/**
|
||||||
|
* 英文名
|
||||||
|
*/
|
||||||
|
ENGLISH_NAME,
|
||||||
|
/**
|
||||||
|
* 身份证号
|
||||||
|
*/
|
||||||
|
ID_CARD,
|
||||||
|
/**
|
||||||
|
* 手机号
|
||||||
|
*/
|
||||||
|
PHONE,
|
||||||
|
/**
|
||||||
|
* 邮箱
|
||||||
|
*/
|
||||||
|
EMAIL,
|
||||||
|
/**
|
||||||
|
* 银行卡
|
||||||
|
*/
|
||||||
|
BANK_CARD,
|
||||||
|
/**
|
||||||
|
* 户籍所在地活现居住地
|
||||||
|
*/
|
||||||
|
LIVE_ADDRESS,
|
||||||
|
/**
|
||||||
|
* 自定义
|
||||||
|
*/
|
||||||
|
CUSTOM
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,8 +1,14 @@
|
|||||||
package com.ruoyi.common.utils.bean;
|
package com.ruoyi.common.utils.bean;
|
||||||
|
|
||||||
|
import org.springframework.beans.BeanWrapper;
|
||||||
|
import org.springframework.beans.BeanWrapperImpl;
|
||||||
|
|
||||||
|
import java.beans.PropertyDescriptor;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
@@ -107,4 +113,33 @@ public class BeanUtils extends org.springframework.beans.BeanUtils
|
|||||||
{
|
{
|
||||||
return m1.substring(BEAN_METHOD_PROP_INDEX).equals(m2.substring(BEAN_METHOD_PROP_INDEX));
|
return m1.substring(BEAN_METHOD_PROP_INDEX).equals(m2.substring(BEAN_METHOD_PROP_INDEX));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 复制属性时忽略空值
|
||||||
|
* @param dest 目标对象
|
||||||
|
* @param src 源对象
|
||||||
|
*/
|
||||||
|
public static void copyPropertiesIgnoreNull(Object dest,Object src) {
|
||||||
|
BeanUtils.copyProperties(src, dest, getNullPropertyNames(src));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取源对象中值为null的属性名数组
|
||||||
|
*/
|
||||||
|
private static String[] getNullPropertyNames(Object source) {
|
||||||
|
final BeanWrapper src = new BeanWrapperImpl(source);
|
||||||
|
PropertyDescriptor[] pds = src.getPropertyDescriptors();
|
||||||
|
|
||||||
|
Set<String> emptyNames = new HashSet<>();
|
||||||
|
for (PropertyDescriptor pd : pds) {
|
||||||
|
// 获取属性值
|
||||||
|
Object srcValue = src.getPropertyValue(pd.getName());
|
||||||
|
if (srcValue == null) {
|
||||||
|
emptyNames.add(pd.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] result = new String[emptyNames.size()];
|
||||||
|
return emptyNames.toArray(result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user