diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index 811b663..49a38c5 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -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: 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 d4dbc7c..0dc75a1 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 @@ -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()); + } } diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/UserInfoDetail.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/UserInfoDetail.java new file mode 100644 index 0000000..c61c6be --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/UserInfoDetail.java @@ -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 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 workExp; + @ApiModelProperty("个人专业技能列表") + private List skillList; + + private String resumeOcrStatus; + private List resumeList; + + @ApiModelProperty("求职者标签列表") + private List indices; + + @ApiModelProperty("是否已认证,0是,1否") + private String isCert; + + private String remark; + +} 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 42297f2..c951725 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 @@ -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); } 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 6642ecb..60f1cac 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 @@ -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 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 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 body = new LinkedMultiValueMap<>(); + body.add("file", fileResource); + + HttpEntity> requestEntity = new HttpEntity<>(body, headers); + + // 发送请求 + ResponseEntity 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 entity = new HttpEntity<>(requestBody, headers); + + try { + // 3. 使用新的 longTimeoutRestTemplate 发送请求 + ResponseEntity 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 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 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()); + } + } + } diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/AppUserFieldCustomCopy.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/AppUserFieldCustomCopy.java new file mode 100644 index 0000000..cedc5a2 --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/AppUserFieldCustomCopy.java @@ -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; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/CustomSensitive.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/CustomSensitive.java new file mode 100644 index 0000000..092aa52 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/CustomSensitive.java @@ -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; +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/config/serializer/SensitiveSerialize.java b/ruoyi-common/src/main/java/com/ruoyi/common/config/serializer/SensitiveSerialize.java new file mode 100644 index 0000000..c752bfb --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/config/serializer/SensitiveSerialize.java @@ -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 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)); + } + } + +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/enums/SensitiveType.java b/ruoyi-common/src/main/java/com/ruoyi/common/enums/SensitiveType.java new file mode 100644 index 0000000..a2f296e --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/enums/SensitiveType.java @@ -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 + +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/bean/BeanUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/bean/BeanUtils.java index 4463662..d097596 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/utils/bean/BeanUtils.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/bean/BeanUtils.java @@ -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 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); + } }