添加ocr识别简历

This commit is contained in:
sh
2026-02-04 10:55:14 +08:00
parent 9d0ff4cbf7
commit f7f31ae0fa
10 changed files with 707 additions and 0 deletions

View File

@@ -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:

View File

@@ -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());
}
} }

View File

@@ -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;
}

View File

@@ -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);
} }

View File

@@ -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());
}
}
} }

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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));
}
}
}

View File

@@ -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
}

View File

@@ -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);
}
} }