添加ocr识别简历
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user