添加石河子本地数据导入接口

This commit is contained in:
chenshaohua
2026-07-07 16:06:10 +08:00
parent 734c91eea1
commit dc414dba34
19 changed files with 1206 additions and 5 deletions

View File

@@ -1,14 +1,21 @@
package com.ruoyi.cms.controller.cms;
import cn.hutool.core.bean.BeanUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.enums.CellExtraTypeEnum;
import com.alibaba.fastjson.JSON;
import com.ruoyi.cms.domain.*;
import com.ruoyi.cms.domain.query.CompanySearch;
import com.ruoyi.cms.domain.query.ESJobSearch;
import com.ruoyi.cms.domain.query.JobSearch;
import com.ruoyi.cms.domain.vo.CandidateVO;
import com.ruoyi.cms.domain.vo.CompanyVo;
import com.ruoyi.cms.domain.vo.ContactParseResult;
import com.ruoyi.cms.domain.vo.JobLocalExcelVo;
import com.ruoyi.cms.service.*;
import com.ruoyi.cms.util.RoleUtils;
import com.ruoyi.cms.util.StringUtil;
import com.ruoyi.cms.util.excel.JobExcelListener;
import com.ruoyi.cms.util.sensitiveWord.SensitiveWordChecker;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.annotation.Log;
@@ -26,16 +33,17 @@ import com.ruoyi.common.utils.poi.ExcelUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.io.InputStream;
import java.util.*;
import java.util.stream.Collectors;
/**
* 岗位Controller
@@ -381,5 +389,87 @@ public class CmsJobController extends BaseController
return success(jobService.updateReviewStatus(jobId, reviewStatus));
}
/**
* 通用上传请求(单个)
*/
@PostMapping("/shzLocalUploadFile")
@PreAuthorize("@ss.hasPermi('cms:job:shzLocalUploadFile')")
@ApiOperation("石河子本地岗位上传")
public AjaxResult shzLocalUploadFile(@RequestParam("file") MultipartFile file) throws Exception {
if (file.isEmpty()) {
return AjaxResult.error("上传文件不能为空");
}
String fileName = file.getOriginalFilename();
if (fileName == null || !fileName.endsWith(".xlsx") && !fileName.endsWith(".xls")) {
return AjaxResult.error("请上传Excel格式的文件");
}
try (InputStream inputStream = file.getInputStream()){
JobExcelListener listener = new JobExcelListener();
EasyExcel.read(inputStream, JobLocalExcelVo.class, listener).extraRead(CellExtraTypeEnum.MERGE).sheet(1).headRowNumber(2).doRead();
if (listener.hasColumnError()) {
return AjaxResult.error(listener.getColumnErrorMsg());
}
List<JobLocalExcelVo> validDataList = listener.getValidDataList();
if (CollectionUtils.isEmpty(validDataList)) {
return AjaxResult.error("Excel文件中无有效数据");
}
//获取企业id
List<Company> allCompany = companyService.selectCompanyList(new CompanySearch());
String string = UUID.randomUUID().toString();
//获取jobs
List<Job> jobs = validDataList.stream().map(excelVo -> {
Job job = new Job();
// 基础字段赋值
BeanUtil.copyProperties(excelVo, job);
job.setIsPublish(1);//发布
job.setView(0L);
job.setApplyNum(0);
job.setCreateBy(string);
job.setJobLocation(job.getJobAddress());
job.setCompanyId(getCompanyId(allCompany, job.getCompanyName()));
job.setPostingDate(DateUtils.getTime());
String companyName=StringUtils.isNotEmpty(job.getCompanyName())?job.getCompanyName():null;
// 联系方式转换
List<JobContact> contactList = new ArrayList<>();
ContactParseResult parseResult = excelVo.getContactParseResult();
if (parseResult != null && parseResult.getContactItemList() != null) {
parseResult.getContactItemList().stream()
.filter(item -> StringUtils.isNotBlank(item.getPhone().trim()))
.map(item -> {
JobContact c = new JobContact();
c.setContactPerson(companyName);
c.setContactPersonPhone(item.getPhone().trim());
c.setRemark(item.getRemark());
return c;
}).forEach(contactList::add);
}
if (!contactList.isEmpty()) {
job.setJobContactList(contactList);
}
return job;
}).collect(Collectors.toList());
jobService.shzLocalUploadFile(jobs);
return AjaxResult.success("已上传!");
} catch (Exception e) {
e.printStackTrace();
return AjaxResult.error(e.getMessage());
}
}
/**
* @param allCompany
* @param aab004
* @return
*/
private Long getCompanyId(List<Company> allCompany, String aab004) {
for (Company company:allCompany){
if(company.getName().equals(aab004)){
return company.getCompanyId();
}
}
return null;
}
}

View File

@@ -128,7 +128,7 @@ public class Job extends BaseEntity
@ApiModelProperty("是否发布 0未发布 1发布")
private Integer isPublish;
@ApiModelProperty("数据来源")
@ApiModelProperty("数据来源 5本地石河子模板导入")
private String dataSource;
@ApiModelProperty("岗位链接")

View File

@@ -0,0 +1,14 @@
package com.ruoyi.cms.domain.vo;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class ContactParseResult {
// 单元格原始完整文本(用于拼接进描述)
private String rawText;
// 拆分后的多条联系人
private List<ContactUploadItem> contactItemList= new ArrayList<>();
}

View File

@@ -0,0 +1,13 @@
package com.ruoyi.cms.domain.vo;
import lombok.Data;
@Data
public class ContactUploadItem {
// 联系人姓名:马经理、王女士、刘老师
private String name;
// 号码:手机号/固话
private String phone;
// 剩余备注:(微信同步)、部分岗位有五险一金
private String remark;
}

View File

@@ -0,0 +1,11 @@
package com.ruoyi.cms.domain.vo;
import lombok.Data;
@Data
public class EducationParseResult {
// 学历编码 -1=不限
private Integer educationCode;
// 原始学历文本
private String rawText;
}

View File

@@ -0,0 +1,127 @@
package com.ruoyi.cms.domain.vo;
import com.alibaba.excel.annotation.ExcelProperty;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.cms.domain.JobContact;
import com.ruoyi.cms.util.excel.ContactParseConverter;
import com.ruoyi.cms.util.excel.EducationConverter;
import com.ruoyi.cms.util.excel.SalaryParseConverter;
import com.ruoyi.cms.util.excel.VacancyLongConverter;
import com.ruoyi.common.annotation.Excel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
/**
* 岗位对象 job
*/
@Data
public class JobLocalExcelVo
{
@ExcelProperty(value = "序号", index = 0)
@Excel(name = "序号")
@ApiModelProperty("序号")
private String xh;
@ExcelProperty(value = "所属行业", index = 1)
@Excel(name = "所属行业")
@ApiModelProperty("所属行业")
private String jobCategory;
@ExcelProperty(value = "单位名称", index = 2)
@Excel(name = "单位名称")
@ApiModelProperty("单位名称")
private String companyName;
@ExcelProperty(value = "工作地点", index = 3)
@ApiModelProperty("工作地点")
private String jobAddress;
@ExcelProperty(value = "岗位工种名称", index = 4)
@Excel(name = "岗位工种名称")
@ApiModelProperty("岗位工种名称")
private String jobTitle;
@ExcelProperty(value = "招聘人数", index = 5,converter = VacancyLongConverter.class)
@Excel(name = "招聘人数")
@ApiModelProperty("招聘人数")
private Long vacancies;
@ExcelProperty(value = "任职要求", index = 6)
@ApiModelProperty("任职要求")
private String description;
@ExcelProperty(value = "学历要求", index = 7,converter = EducationConverter.class)
private EducationParseResult educationResult;
@ExcelProperty(value = "工资待遇元/月", index = 8, converter = SalaryParseConverter.class)
@Excel(name = "工资待遇元/月")
@ApiModelProperty("工资待遇元/月")
private SalaryParseDTO salaryParseDto;
@ExcelProperty(value = "联系人对象", index = 9, converter = ContactParseConverter.class)
private ContactParseResult contactParseResult;
@ExcelProperty(value = "岗位收集人", index = 10)
@ApiModelProperty("岗位收集人")
private String gwsjr;
@ExcelProperty(value = "联系人")
@ApiModelProperty("联系人")
private String contactPerson;
@ExcelProperty(value = "联系人电话")
@ApiModelProperty("联系人电话")
private String contactPersonPhone;
@ExcelProperty(value = "最小薪资")
@Excel(name = "最小薪资", readConverterExp = "元=")
@ApiModelProperty("最小薪资(元)")
private Long minSalary;
@ExcelProperty(value = "最大薪资")
@Excel(name = "最大薪资", readConverterExp = "元=")
@ApiModelProperty("最大薪资(元)")
private Long maxSalary;
@ExcelProperty(value = "岗位区划")
@Excel(name = "岗位区划")
@ApiModelProperty("岗位区划")
private String jobLocation;
@ExcelProperty(value = "工作经验")
@Excel(name = "工作经验要求 对应字典experience")
@ApiModelProperty("工作经验要求 对应字典experience")
private String experience;
@ExcelProperty(value = "岗位类型")
@ApiModelProperty("岗位类型 0疆内 1疆外")
private String jobType;
@ExcelProperty(value = "类型")
@ApiModelProperty("类型 0常规岗位 1就业见习岗位 2实习实训岗位 3社区实践岗位 4零工 对应字段字典position_type")
private String type;
@ApiModelProperty("是否发布 0未发布 1发布")
private Integer isPublish;
@ApiModelProperty("工作地点区县字典代码")
private Integer jobLocationAreaCode;
@JsonFormat(pattern = "yyyy-MM-dd")
@Excel(name = "发布时间", width = 30, dateFormat = "yyyy-MM-dd")
@ApiModelProperty("发布时间")
private String postingDate;
@ApiModelProperty("数据来源")
private String dataSource;
@Excel(name = "学历要求 对应字典education")
@ApiModelProperty("学历要求 对应字典education")
private String education;
@ApiModelProperty("联系人列表")
private List<JobContact> jobContacts;
}

View File

@@ -0,0 +1,19 @@
package com.ruoyi.cms.domain.vo;
import com.ruoyi.cms.util.excel.SalaryType;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class SalaryParseDTO {
// 薪资类型
private SalaryType salaryType;
// 最低薪资(按月统一换算,单位:元)
private Long minMoney;
// 最高薪资
private Long maxMoney;
// 原始文本
private String originalText;
// 福利/备注剩余文字
private String remark;
}

View File

@@ -116,4 +116,6 @@ public interface IJobService
Long updateReviewStatus(Long jobId, String reviewStatus);
List<AppUser> getRecommendByJobId(Long jobId);
void shzLocalUploadFile(List<Job> jobs);
}

View File

@@ -1286,4 +1286,23 @@ public class JobServiceImpl extends ServiceImpl<JobMapper,Job> implements IJobSe
}
return list;
}
@Override
public void shzLocalUploadFile(List<Job> jobs) {
List<JobContact> allContactList = new ArrayList<>();
for (Job job : jobs) {
jobMapper.insert(job);
List<JobContact> contactList = job.getJobContactList();
if (CollectionUtils.isEmpty(contactList)) {
continue;
}
contactList.forEach(c -> c.setJobId(job.getJobId()));
allContactList.addAll(contactList);
}
if (!allContactList.isEmpty()) {
jobContactMapper.batchInsert(allContactList);
}
//刷新es
iesJobSearchService.resetTextCache();
}
}

View File

@@ -11,6 +11,8 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class StringUtil {
@@ -97,6 +99,16 @@ public class StringUtil {
private static final String SEPARATOR = "-";
private static final String DEFAULT_VALUE = "0";
/**
* 本地石河子模板导入
*/
public static final String DATA_SOURCE_LOCAL = "5";
/**
* 截取数字
*/
private static final Pattern DIGIT_PATTERN = Pattern.compile("\\d+");
/**
* 身份证规则
*/
@@ -349,4 +361,31 @@ public class StringUtil {
// 两端不同,正常拼接
return min + SEPARATOR + max;
}
/**
* 处理上传中人数问题
* @param source
* @return
*/
public static Long extractVacancyNum(String source) {
if (StringUtils.isBlank(source)) {
return 1L;
}
String text = source.trim();
if (text.contains("若干")) {
return 20L;
}
Matcher matcher = DIGIT_PATTERN.matcher(text);
Long maxNum = null;
while (matcher.find()) {
String numStr = matcher.group();
try {
long num = Long.parseLong(numStr);
if (maxNum == null || num > maxNum) {
maxNum = num;
}
} catch (NumberFormatException ignored) {}
}
return maxNum == null ? 1L : maxNum;
}
}

View File

@@ -0,0 +1,48 @@
package com.ruoyi.cms.util.excel;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
import com.ruoyi.cms.domain.vo.ContactParseResult;
import java.math.BigDecimal;
public class ContactParseConverter implements Converter<ContactParseResult> {
@Override
public ContactParseResult convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
ContactParseResult res = new ContactParseResult();
String raw = "";
if (cellData != null) {
// 正确枚举CellDataTypeEnum包路径enums下
CellDataTypeEnum cellType = cellData.getType();
if (CellDataTypeEnum.STRING.equals(cellType)) {
raw = cellData.getStringValue() == null ? "" : cellData.getStringValue();
} else if (CellDataTypeEnum.NUMBER.equals(cellType)) {
// 数字单元格转完整字符串,避免科学计数
BigDecimal numVal = cellData.getNumberValue();
raw = numVal.toPlainString();
} else if (CellDataTypeEnum.BOOLEAN.equals(cellType)) {
raw = String.valueOf(cellData.getBooleanValue());
} else if (CellDataTypeEnum.EMPTY.equals(cellType)) {
raw = "";
}
}
res.setRawText(raw);
// 拆分多条联系人
res.setContactItemList(ContactParseUtil.parse(raw));
return res;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
@Override
public Class<?> supportJavaTypeKey() {
return ContactParseResult.class;
}
}

View File

@@ -0,0 +1,227 @@
package com.ruoyi.cms.util.excel;
import com.ruoyi.cms.domain.vo.ContactUploadItem;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 分层联系人解析工具
* 方案B同时支持手机号、座机、邮箱解决1750993xxxx 带出0993xxxx问题
* 实现逻辑:先提取并屏蔽手机号,再解析座机,避免手机号内部数字误匹配座机
* name固定空remark存储Excel完整原始单元格文本
* \n / <br/> 自动拆分多行
*/
public class ContactParseUtil {
// 11位纯数字手机号
private static final Pattern MOBILE_11 = Pattern.compile("1[3-9]\\d{9}");
// 11位掩码手机号
private static final Pattern MOBILE_MASK_11 = Pattern.compile("1[3-9][0-9xX]{10}");
// 带横杠固定电话 0993-xxxxxx / 0993—xxxxxx
private static final Pattern TEL_FULL = Pattern.compile("(0\\d{2,4}[—-])(\\d{4,8})");
// 无横杠纯数字座机 0开头6~12位
private static final Pattern TEL_PURE_NUM = Pattern.compile("0\\d{6,12}");
// 提取区号前缀
private static final Pattern AREA_PREFIX = Pattern.compile("0\\d{2,4}[—-]");
// 兼容掩码邮箱正则
private static final Pattern EMAIL_PATTERN = Pattern.compile("[a-zA-Z0-9xX._-]+@[a-zA-Z0-9_-]+\\.[a-zA-Z0-9_.+-]+");
// 对外统一入口
public static List<ContactUploadItem> parse(String source) {
List<ContactUploadItem> result = new ArrayList<>();
if (StringUtils.isBlank(source)) {
return result;
}
String fullOriginalText = source;
List<String> lineList = splitTopMultiLine(source);
for (String line : lineList) {
String trimLine = line.trim();
if (StringUtils.isBlank(trimLine)) {
continue;
}
if (trimLine.contains("")) {
handleColonTypeLine(trimLine, result, fullOriginalText);
} else {
handleNormalLine(trimLine, result, fullOriginalText);
}
}
return result;
}
/**
* \n \r <br/> <br> 全部作为独立分隔符,拆分为多行联系人
*/
private static List<String> splitTopMultiLine(String source) {
List<String> lineResult = new ArrayList<>();
String temp = source.replace("\n", "|")
.replace("\r", "|")
.replace("<br/>", "|")
.replace("<br>", "|");
String[] arr = temp.split("\\|");
for (String s : arr) {
String trimStr = s.trim();
if (StringUtils.isNotBlank(trimStr)) {
lineResult.add(trimStr);
}
}
return lineResult;
}
/**
* 处理 文字:联系方式 冒号格式name固定空
*/
private static void handleColonTypeLine(String line, List<ContactUploadItem> result, String fullSource) {
String[] colonArr = line.split("", 2);
String contactArea = colonArr.length > 1 ? colonArr[1].trim() : "";
String contactVal = extractAllContact(contactArea);
if (StringUtils.isBlank(contactVal)) {
return;
}
ContactUploadItem item = new ContactUploadItem();
item.setName("");
item.setPhone(contactVal);
item.setRemark(fullSource);
result.add(item);
}
/**
* 无冒号普通行不提取任何汉字姓名name固定空
*/
private static void handleNormalLine(String line, List<ContactUploadItem> result, String fullSource) {
boolean hasContact = existContact(line);
if (!hasContact) {
return;
}
String areaPrefix = extractAreaPrefix(line);
List<String> segList = splitContactSegment(line);
for (String seg : segList) {
String segTrim = seg.trim();
if (StringUtils.isBlank(segTrim)) {
continue;
}
// 判断当前片段是否是完整手机号,是则不拼接区号
boolean isMobile = checkTextHasMobile(segTrim);
String segComplete;
if (isMobile) {
segComplete = segTrim;
} else {
segComplete = autoFillAreaCode(segTrim, areaPrefix);
}
String contactVal = extractAllContact(segComplete);
if (StringUtils.isBlank(contactVal)) {
continue;
}
ContactUploadItem item = new ContactUploadItem();
item.setName("");
item.setPhone(contactVal);
item.setRemark(fullSource);
result.add(item);
}
}
/**
* 判断文本是否包含手机号(纯数字/掩码)
*/
private static boolean checkTextHasMobile(String text) {
return MOBILE_MASK_11.matcher(text).find()
|| MOBILE_11.matcher(text).find();
}
/**
* 判断文本是否包含任意联系方式
*/
private static boolean existContact(String text) {
return MOBILE_MASK_11.matcher(text).find()
|| MOBILE_11.matcher(text).find()
|| TEL_FULL.matcher(text).find()
|| TEL_PURE_NUM.matcher(text).find()
|| EMAIL_PATTERN.matcher(text).find();
}
/**
* 提取全部有效联系方式:手机号、带横杠固话、纯数字座机、邮箱
* 核心修复逻辑:先缓存手机号、屏蔽手机号文本,再匹配座机,避免手机号内部数字误匹配座机
*/
private static String extractAllContact(String text) {
List<String> mobileCache = new ArrayList<>();
// 1. 缓存所有手机号,并用占位符替换,屏蔽内部数字
Matcher maskMobileMatcher = MOBILE_MASK_11.matcher(text);
while (maskMobileMatcher.find()) {
mobileCache.add(maskMobileMatcher.group());
}
String tempText = text.replaceAll(MOBILE_MASK_11.pattern(), "#MOBILE#");
Matcher mobileMatcher = MOBILE_11.matcher(tempText);
while (mobileMatcher.find()) {
mobileCache.add(mobileMatcher.group());
}
tempText = tempText.replaceAll(MOBILE_11.pattern(), "#MOBILE#");
StringBuilder sb = new StringBuilder();
// 2. 先回填所有手机号
for (String mobile : mobileCache) {
sb.append(mobile).append(" ");
}
// 3. 在屏蔽手机号后的文本里匹配座机,不会捕获手机号内部数字
// 带横杠固话
Matcher telMatcher = TEL_FULL.matcher(tempText);
while (telMatcher.find()) {
sb.append(telMatcher.group(1)).append(telMatcher.group(2)).append(" ");
}
// 无横杠纯数字座机
Matcher telPureMatcher = TEL_PURE_NUM.matcher(tempText);
while (telPureMatcher.find()) {
sb.append(telPureMatcher.group()).append(" ");
}
// 4. 匹配邮箱不受影响
Matcher emailMatcher = EMAIL_PATTERN.matcher(text);
while (emailMatcher.find()) {
sb.append(emailMatcher.group()).append(" ");
}
return sb.toString().trim();
}
/**
* 提取区号前缀
*/
private static String extractAreaPrefix(String text) {
Matcher matcher = AREA_PREFIX.matcher(text);
if (matcher.find()) {
return matcher.group();
}
return "";
}
/**
* 仅4~8位纯数字短号 且 不是手机号才自动拼接区号
*/
private static String autoFillAreaCode(String seg, String areaCode) {
if (StringUtils.isBlank(areaCode)) {
return seg;
}
// 只有纯数字4-8位且不包含手机号才补区号
if (seg.matches("\\d{4,8}") && !checkTextHasMobile(seg)) {
return areaCode + seg;
}
return seg;
}
/**
* 按 / , ,分割多个联系方式片段
*/
private static List<String> splitContactSegment(String line) {
List<String> segResult = new ArrayList<>();
String[] arr = line.split("[/,]");
for (String s : arr) {
String trimStr = s.trim();
if (StringUtils.isNotBlank(trimStr)) {
segResult.add(trimStr);
}
}
return segResult;
}
}

View File

@@ -0,0 +1,40 @@
package com.ruoyi.cms.util.excel;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
import com.ruoyi.cms.domain.vo.EducationParseResult;
/**
* 学历文本转字典编码转换器
*/
public class EducationConverter implements Converter<EducationParseResult> {
@Override
public EducationParseResult convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
String text = "";
if (cellData != null && cellData.getStringValue() != null) {
text = cellData.getStringValue();
}
try {
return EducationParseUtil.parseDetail(text);
} catch (Exception e) {
EducationParseResult err = new EducationParseResult();
err.setRawText(text);
err.setEducationCode(-1);
return err;
}
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
@Override
public Class<?> supportJavaTypeKey() {
return Integer.class;
}
}

View File

@@ -0,0 +1,93 @@
package com.ruoyi.cms.util.excel;
import com.ruoyi.cms.domain.vo.EducationParseResult;
import org.apache.commons.lang3.StringUtils;
import java.util.HashMap;
import java.util.Map;
public class EducationParseUtil {
private static final Map<String, Integer> EDUCATION_MAP = new HashMap<>();
static {
EDUCATION_MAP.put("初中及以下", 0);
EDUCATION_MAP.put("中专/中技", 1);
EDUCATION_MAP.put("高中", 2);
EDUCATION_MAP.put("大专", 3);
EDUCATION_MAP.put("本科", 4);
EDUCATION_MAP.put("硕士", 5);
EDUCATION_MAP.put("博士", 6);
EDUCATION_MAP.put("mba/emba", 7);
EDUCATION_MAP.put("留学-学士", 8);
EDUCATION_MAP.put("留学-硕士", 9);
EDUCATION_MAP.put("留学-博士", 10);
EDUCATION_MAP.put("不限", -1);
}
/**
* 只返回编码(原有方法保留兼容)
*/
public static Integer parse(String source) {
EducationParseResult result = parseDetail(source);
return result.getEducationCode();
}
/**
* 完整解析,返回编码+原文+识别标记(核心)
*/
public static EducationParseResult parseDetail(String source) {
EducationParseResult result = new EducationParseResult();
String raw = source == null ? "" : source.trim();
result.setRawText(raw);
result.setEducationCode(-1);
if (StringUtils.isBlank(raw)) {
// 空白:不限,识别成功
return result;
}
String text = raw.toLowerCase();
// 1. 不限
if (text.contains("不限") || text.contains("无要求") || text.equals("")) {
result.setEducationCode(-1);
return result;
}
// 2. MBA/EMBA
if (text.contains("mba") || text.contains("emba")) {
result.setEducationCode(7);
return result;
}
// 3. 留学
if (text.contains("留学") || text.contains("海外") || text.contains("国外")) {
if (text.contains("学士") || text.contains("本科")) {
result.setEducationCode(8);
return result;
} else if (text.contains("硕士")) {
result.setEducationCode(9);
return result;
} else if (text.contains("博士")) {
result.setEducationCode(10);
return result;
}
}
// 4. 标准学历短句(正常识别)
if (text.contains("初中")) {
result.setEducationCode(0);
} else if (text.contains("中专") || text.contains("中技")) {
result.setEducationCode(1);
} else if (text.contains("高中")) {
result.setEducationCode(2);
} else if (text.contains("大专") || text.contains("专科")) {
result.setEducationCode(3);
} else if (text.contains("本科") || text.contains("大学")) {
result.setEducationCode(4);
} else if (text.contains("硕士") || text.contains("研究生")) {
result.setEducationCode(5);
} else if (text.contains("博士")) {
result.setEducationCode(6);
}
return result;
}
}

View File

@@ -0,0 +1,186 @@
package com.ruoyi.cms.util.excel;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.enums.CellExtraTypeEnum;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.metadata.CellExtra;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.ruoyi.cms.domain.JobContact;
import com.ruoyi.cms.domain.vo.*;
import com.ruoyi.cms.util.StringUtil;
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.lang.reflect.Field;
/**
* 企业岗位excel监听
*/
@Getter
public class JobExcelListener extends AnalysisEventListener<JobLocalExcelVo> {
private final List<JobLocalExcelVo> validDataList = new ArrayList<>();
private final List<CellExtra> mergeCellList = new ArrayList<>();
private final Map<Integer, Field> columnFieldMap = new HashMap<>();
private String columnErrorMsg;
public JobExcelListener() {
initColumnFieldMap();
}
@Override
public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {
}
@Override
public void extra(CellExtra extra, AnalysisContext context) {
if (extra.getType() == CellExtraTypeEnum.MERGE) {
mergeCellList.add(extra);
}
}
@Override
public void invoke(JobLocalExcelVo jobExcelVo, AnalysisContext analysisContext) {
//薪资要求
SalaryParseDTO dto = jobExcelVo.getSalaryParseDto();
if (dto == null) {
dto = new SalaryParseDTO();
dto.setSalaryType(SalaryType.FACE_TO_FACE);
dto.setOriginalText("");
dto.setRemark("未读取到薪资列数据");
}
jobExcelVo.setMinSalary(dto.getMinMoney());
jobExcelVo.setMaxSalary(dto.getMaxMoney());
String rawSalary = dto.getOriginalText();
if (StringUtils.isNotBlank(rawSalary)) {
String desc = jobExcelVo.getDescription() == null ? "" : jobExcelVo.getDescription();
String tag = "薪资待遇:" + rawSalary;
if (!desc.contains(tag)) {
jobExcelVo.setDescription(desc + "\n" + tag);
}
}
////学历要求
EducationParseResult eduResult = jobExcelVo.getEducationResult();
if (eduResult == null) {
eduResult = new EducationParseResult();
eduResult.setEducationCode(-1);
eduResult.setRawText("");
}
jobExcelVo.setEducation(String.valueOf(eduResult.getEducationCode()));
String rawEduText = eduResult.getRawText();
if (StringUtils.isNotBlank(rawEduText)) {
String desc = jobExcelVo.getDescription() == null ? "" : jobExcelVo.getDescription();
String tag = "学历要求:" + rawEduText;
if (!desc.contains(tag)) {
jobExcelVo.setDescription(desc + "\n" + tag);
}
}
//处理联系方式
ContactParseResult contactResult = jobExcelVo.getContactParseResult();
if (contactResult == null) {
contactResult = new ContactParseResult();
contactResult.setRawText("");
}
// 遍历拆分好的【联系人+号码】,可单独入库
List<JobContact> jobContacts=new ArrayList<>();
for (ContactUploadItem item : contactResult.getContactItemList()) {
JobContact jobContact=new JobContact();
if(StringUtils.isNotBlank(item.getPhone().trim())){
String name = item.getName();
String phone = item.getPhone();
String remark = item.getRemark();
jobContact.setContactPerson(name);
jobContact.setContactPersonPhone(phone);
jobContact.setRemark(remark);
jobContacts.add(jobContact);
}
}
if(!jobContacts.isEmpty()){
jobExcelVo.setJobContacts(jobContacts);
}
//数据来源
jobExcelVo.setDataSource(StringUtil.DATA_SOURCE_LOCAL);
validDataList.add(jobExcelVo);
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
fillMergedCellData(analysisContext);
}
/**
* 填充合并单元格空值,实现拆分效果
*/
private void fillMergedCellData(AnalysisContext context) {
int headRowNum = context.readSheetHolder().getHeadRowNumber();
if (columnFieldMap.isEmpty()) {
return;
}
for (CellExtra merge : mergeCellList) {
int firstRow = merge.getFirstRowIndex();
int lastRow = merge.getLastRowIndex();
int firstCol = merge.getFirstColumnIndex();
// 跳过表头
if (firstRow < headRowNum) {
continue;
}
Field field = columnFieldMap.get(firstCol);
if (field == null) {
continue;
}
try {
int firstDataIndex = firstRow - headRowNum;
if (firstDataIndex >= validDataList.size()) {
continue;
}
// 获取合并首行的值
Object mergeValue = field.get(validDataList.get(firstDataIndex));
// 给合并区域所有行赋值
for (int row = firstRow + 1; row <= lastRow; row++) {
int dataIndex = row - headRowNum;
if (dataIndex >= validDataList.size()) {
continue;
}
field.set(validDataList.get(dataIndex), mergeValue);
}
} catch (Exception e) {
columnErrorMsg = "合并单元格填充失败:" + e.getMessage();
}
}
}
/**
* 扫描VO实体 @ExcelProperty(index) 构建列-字段映射
*/
private void initColumnFieldMap() {
Class<JobLocalExcelVo> clazz = JobLocalExcelVo.class;
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
ExcelProperty excelProp = field.getAnnotation(ExcelProperty.class);
if (excelProp == null || excelProp.index() < 0) {
continue;
}
field.setAccessible(true);
columnFieldMap.put(excelProp.index(), field);
}
}
public boolean hasColumnError() {
return StringUtils.isNotBlank(columnErrorMsg);
}
}

View File

@@ -0,0 +1,40 @@
package com.ruoyi.cms.util.excel;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
import com.ruoyi.cms.domain.vo.SalaryParseDTO;
public class SalaryParseConverter implements Converter<SalaryParseDTO> {
@Override
public SalaryParseDTO convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
String text = "";
if (cellData != null && cellData.getStringValue() != null) {
text = cellData.getStringValue();
}
try {
return SalaryParseUtil.parse(text);
} catch (Exception e) {
SalaryParseDTO dto = new SalaryParseDTO();
dto.setSalaryType(SalaryType.FACE_TO_FACE);
dto.setOriginalText("");
dto.setRemark("薪资单元格为空/解析失败");
dto.setMinMoney(null);
dto.setMaxMoney(null);
return dto;
}
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
@Override
public Class<?> supportJavaTypeKey() {
return SalaryParseDTO.class;
}
}

View File

@@ -0,0 +1,159 @@
package com.ruoyi.cms.util.excel;
import com.ruoyi.cms.domain.vo.SalaryParseDTO;
import org.apache.commons.lang3.StringUtils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class SalaryParseUtil {
// 匹配面议关键词
private static final Pattern FACE_PATTERN = Pattern.compile("(面议|薪资面议|工资面议)");
// 年薪匹配 年薪14万、年薪12万元
private static final Pattern YEAR_PATTERN = Pattern.compile("年薪(\\d+(\\.\\d+)?)[万w]");
// 日薪 200-300/天、300元/天、200/天
private static final Pattern DAY_PATTERN = Pattern.compile("(\\d+(\\.\\d+)?)-(\\d+(\\.\\d+)?)[/元]*天|(\\d+(\\.\\d+)?)[/元]*天");
// 时薪 100元/小时、50/小时
private static final Pattern HOUR_PATTERN = Pattern.compile("(\\d+(\\.\\d+)?)[/元]*小时");
// 月薪区间 3000-5000、3000--5000、3500-6000元/月
private static final Pattern MONTH_RANGE_PATTERN = Pattern.compile("(\\d+)[-–—]{1,2}(\\d+)");
// 固定月薪 4000元、到手3100、应发6800、3800
private static final Pattern MONTH_FIX_PATTERN = Pattern.compile("(到手|应发)?(\\d+)");
// 模糊薪资 3500左右、5000+、3000以上
private static final Pattern FUZZY_PATTERN = Pattern.compile("(\\d+)(左右|\\+|以上)");
// 每月折算工作日22天每日8小时
private static final BigDecimal DAY_TO_MONTH = new BigDecimal("22");
private static final BigDecimal HOUR_TO_DAY = new BigDecimal("8");
public static SalaryParseDTO parse(String text) {
SalaryParseDTO dto = new SalaryParseDTO();
if (StringUtils.isBlank(text)) {
dto.setSalaryType(SalaryType.FACE_TO_FACE);
dto.setOriginalText("");
return dto;
}
// 清洗文本:去除引号、换行、多余空格、单位干扰词
String origin = text.trim();
String cleanText = origin.replace("\"", "")
.replace("\n", "")
.replace("\r", "")
.replaceAll("\\s+", " ")
.replace("元/月", "")
.replace("", "")
.replace("/月", "");
dto.setOriginalText(origin);
// 1. 判断是否面议
if (FACE_PATTERN.matcher(cleanText).find()) {
dto.setSalaryType(SalaryType.FACE_TO_FACE);
dto.setRemark(cleanText);
return dto;
}
// 2. 年薪解析
Matcher yearMatcher = YEAR_PATTERN.matcher(cleanText);
if (yearMatcher.find()) {
dto.setSalaryType(SalaryType.YEAR_SALARY);
BigDecimal yearWan = new BigDecimal(yearMatcher.group(1));
BigDecimal yearTotal = yearWan.multiply(new BigDecimal("10000"));
BigDecimal month = yearTotal.divide(new BigDecimal("12"), 2, RoundingMode.HALF_UP);
dto.setMinMoney(month.longValue());
dto.setMaxMoney(month.longValue());
dto.setRemark(cleanText.replace(yearMatcher.group(), "").trim());
return dto;
}
// 3. 日薪解析,统一折算月薪
Matcher dayMatcher = DAY_PATTERN.matcher(cleanText);
if (dayMatcher.find()) {
dto.setSalaryType(SalaryType.DAY_SALARY);
String minDayStr = dayMatcher.group(1);
String maxDayStr = dayMatcher.group(3);
BigDecimal minDay = new BigDecimal(StringUtils.isNotBlank(minDayStr) ? minDayStr : dayMatcher.group(5));
BigDecimal maxDay = StringUtils.isNotBlank(maxDayStr) ? new BigDecimal(maxDayStr) : minDay;
// 折算月
BigDecimal minMonth = minDay.multiply(DAY_TO_MONTH);
BigDecimal maxMonth = maxDay.multiply(DAY_TO_MONTH);
dto.setMinMoney(minMonth.longValue());
dto.setMaxMoney(maxMonth.longValue());
dto.setRemark(cleanText.replace(dayMatcher.group(), "").trim());
return dto;
}
// 4. 时薪解析
Matcher hourMatcher = HOUR_PATTERN.matcher(cleanText);
if (hourMatcher.find()) {
dto.setSalaryType(SalaryType.HOUR_SALARY);
BigDecimal hour = new BigDecimal(hourMatcher.group(1));
BigDecimal day = hour.multiply(HOUR_TO_DAY);
BigDecimal month = day.multiply(DAY_TO_MONTH);
dto.setMinMoney(month.longValue());
dto.setMaxMoney(month.longValue());
dto.setRemark(cleanText.replace(hourMatcher.group(), "").trim());
return dto;
}
// 5. 模糊薪资 3500左右、5000+
Matcher fuzzyMatcher = FUZZY_PATTERN.matcher(cleanText);
if (fuzzyMatcher.find()) {
dto.setSalaryType(SalaryType.FUZZY_MONTH);
BigDecimal num = new BigDecimal(fuzzyMatcher.group(1));
dto.setMinMoney(num.longValue());
dto.setMaxMoney(null);
dto.setRemark(cleanText.replace(fuzzyMatcher.group(), "").trim());
return dto;
}
// 6. 月薪区间 3000-5000
Matcher rangeMatcher = MONTH_RANGE_PATTERN.matcher(cleanText);
if (rangeMatcher.find()) {
dto.setSalaryType(SalaryType.MONTH_RANGE);
BigDecimal min = new BigDecimal(rangeMatcher.group(1));
BigDecimal max = new BigDecimal(rangeMatcher.group(2));
dto.setMinMoney(min.longValue());
dto.setMaxMoney(max.longValue());
dto.setRemark(cleanText.replace(rangeMatcher.group(), "").trim());
return dto;
}
// 7. 固定月薪
Matcher fixMatcher = MONTH_FIX_PATTERN.matcher(cleanText);
if (fixMatcher.find()) {
dto.setSalaryType(SalaryType.MONTH_FIX);
BigDecimal val = new BigDecimal(fixMatcher.group(2));
dto.setMinMoney(val.longValue());
dto.setMaxMoney(val.longValue());
dto.setRemark(cleanText.replace(fixMatcher.group(), "").trim());
return dto;
}
// 兜底:无法识别归为面议
dto.setSalaryType(SalaryType.FACE_TO_FACE);
dto.setRemark(cleanText);
return dto;
}
// 测试方法
// public static void main(String[] args) {
// String test1 = "5000-6000元\n五险";
// String test2 = "工资面议,包吃包住";
// String test3 = "200-300/天";
// String test4 = "年薪14万元包食宿";
// String test5 = "3500左右";
// String test6 = "到手3100";
// String test7 = "100元/小时";
// String test8 = "底薪3000+提成";
//
// System.out.println(parse(test1));
// System.out.println(parse(test2));
// System.out.println(parse(test3));
// System.out.println(parse(test4));
// System.out.println(parse(test5));
// System.out.println(parse(test6));
// System.out.println(parse(test7));
// System.out.println(parse(test8));
// }
}

View File

@@ -0,0 +1,11 @@
package com.ruoyi.cms.util.excel;
public enum SalaryType {
FACE_TO_FACE, // 面议
MONTH_RANGE, // 月薪区间
MONTH_FIX, // 固定月薪
DAY_SALARY, // 日薪
HOUR_SALARY, // 时薪
YEAR_SALARY, // 年薪
FUZZY_MONTH // 模糊3500左右、5000+
}

View File

@@ -0,0 +1,63 @@
package com.ruoyi.cms.util.excel;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
import org.apache.commons.lang3.StringUtils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 招聘人数文本转Long转换器
*/
public class VacancyLongConverter implements Converter<Long> {
private static final Pattern DIGIT_PATTERN = Pattern.compile("\\d+");
/**
* 读取Excel时文本转Long
*/
@Override
public Long convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
String source = cellData.getStringValue();
return extractVacancyNum(source);
}
/**
* 你的原有提取逻辑完整放入转换器内部
*/
private static Long extractVacancyNum(String source) {
if (StringUtils.isBlank(source)) {
return 1L;
}
String text = source.trim();
if (text.contains("若干")) {
return 20L;
}
Matcher matcher = DIGIT_PATTERN.matcher(text);
Long maxNum = null;
while (matcher.find()) {
String numStr = matcher.group();
try {
long num = Long.parseLong(numStr);
if (maxNum == null || num > maxNum) {
maxNum = num;
}
} catch (NumberFormatException ignored) {
}
}
return maxNum == null ? 1L : maxNum;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
@Override
public Class<?> supportJavaTypeKey() {
return Long.class;
}
}