添加ocr识别简历
This commit is contained in:
@@ -140,6 +140,13 @@ mybatis-plus:
|
||||
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节点健康检查
|
||||
management:
|
||||
endpoints:
|
||||
|
||||
@@ -28,6 +28,7 @@ import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.BufferedOutputStream;
|
||||
@@ -251,4 +252,12 @@ public class AppUserController extends BaseController
|
||||
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 com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.domain.entity.AppUserShow;
|
||||
import com.ruoyi.common.core.domain.entity.MyChart;
|
||||
import com.ruoyi.common.core.domain.entity.AppUser;
|
||||
@@ -85,4 +86,6 @@ public interface IAppUserService
|
||||
public AppUser getYtjValidPhone(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;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
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.File;
|
||||
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.SiteSecurityUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.common.utils.bean.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
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 com.ruoyi.cms.service.IAppUserService;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* APP用户Service业务层处理
|
||||
@@ -48,6 +65,16 @@ public class AppUserServiceImpl extends ServiceImpl<AppUserMapper,AppUser> imple
|
||||
@Autowired
|
||||
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用户
|
||||
*
|
||||
@@ -744,4 +771,189 @@ public class AppUserServiceImpl extends ServiceImpl<AppUserMapper,AppUser> imple
|
||||
.eq(AppUser::getDelFlag,"0")
|
||||
.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;
|
||||
|
||||
import org.springframework.beans.BeanWrapper;
|
||||
import org.springframework.beans.BeanWrapperImpl;
|
||||
|
||||
import java.beans.PropertyDescriptor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制属性时忽略空值
|
||||
* @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