添加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

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

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

View File

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

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