flat:AI+
This commit is contained in:
166
utils/jobAnalyzer.js
Normal file
166
utils/jobAnalyzer.js
Normal file
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* 岗位数据分析模块
|
||||
*/
|
||||
const jobAnalyzer = {
|
||||
/**
|
||||
* 清洗无效薪资数据
|
||||
* @param {Array} jobs 原始岗位数据
|
||||
* @returns {Array} 有效岗位数据
|
||||
*/
|
||||
cleanData: (jobs) => {
|
||||
if (!Array.isArray(jobs)) return []
|
||||
return jobs.filter(job =>
|
||||
Number(job.minSalary) > 0 &&
|
||||
Number(job.maxSalary) > 0
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* 执行完整分析流程
|
||||
* @param {Array} jobs 原始岗位数据
|
||||
* @param {Object} options 配置选项
|
||||
* @returns {Object} 分析结果
|
||||
*/
|
||||
analyze: (jobs, options = {
|
||||
verbose: false
|
||||
}) => {
|
||||
// 数据校验
|
||||
if (!Array.isArray(jobs)) {
|
||||
throw new Error('Invalid jobs data format')
|
||||
}
|
||||
|
||||
// 数据清洗
|
||||
const validJobs = jobAnalyzer.cleanData(jobs)
|
||||
if (validJobs.length === 0) {
|
||||
return {
|
||||
warning: 'No valid job data available'
|
||||
}
|
||||
}
|
||||
|
||||
// 执行分析
|
||||
const results = {
|
||||
salary: jobAnalyzer.analyzeSalaries(validJobs),
|
||||
categories: jobAnalyzer.countCategories(validJobs),
|
||||
experience: jobAnalyzer.analyzeExperience(validJobs),
|
||||
areas: jobAnalyzer.analyzeAreas(validJobs)
|
||||
}
|
||||
|
||||
// 按需控制台输出
|
||||
if (options.verbose) {
|
||||
jobAnalyzer.printResults(results)
|
||||
}
|
||||
|
||||
return results
|
||||
},
|
||||
|
||||
/** 薪资分析 */
|
||||
analyzeSalaries: (jobs) => {
|
||||
const stats = jobs.reduce((acc, job) => {
|
||||
acc.totalMin += job.minSalary
|
||||
acc.totalMax += job.maxSalary
|
||||
acc.highPay += (job.maxSalary >= 10000) ? 1 : 0
|
||||
return acc
|
||||
}, {
|
||||
totalMin: 0,
|
||||
totalMax: 0,
|
||||
highPay: 0
|
||||
})
|
||||
|
||||
return {
|
||||
avgMin: Math.round(stats.totalMin / jobs.length),
|
||||
avgMax: Math.round(stats.totalMax / jobs.length),
|
||||
highPayRatio: Math.round((stats.highPay / jobs.length) * 100)
|
||||
}
|
||||
},
|
||||
|
||||
/** 岗位类别统计 */
|
||||
countCategories: (jobs) => {
|
||||
return jobs.reduce((map, job) => {
|
||||
map[job.jobCategory] = (map[job.jobCategory] || 0) + 1
|
||||
return map
|
||||
}, {})
|
||||
},
|
||||
|
||||
/** 经验要求分析 */
|
||||
analyzeExperience: (jobs) => {
|
||||
return jobs.reduce((stats, job) => {
|
||||
const label = job.experIenceLabel || '未知'
|
||||
stats[label] = (stats[label] || 0) + 1
|
||||
return stats
|
||||
}, {})
|
||||
},
|
||||
|
||||
/** 地区分布分析 */
|
||||
analyzeAreas: (jobs) => {
|
||||
return jobs.reduce((map, job) => {
|
||||
const area = job.jobLocationAreaCodeLabel || '未知'
|
||||
map[area] = (map[area] || 0) + 1
|
||||
return map
|
||||
}, {})
|
||||
},
|
||||
|
||||
/** 格式化输出结果 */
|
||||
printResults: (results) => {
|
||||
console.log('【高薪岗位分析】')
|
||||
console.log(`- 平均月薪范围:${results.salary.avgMin}k ~ ${results.salary.avgMax}k`)
|
||||
console.log(`- 月薪≥10k的岗位占比:${results.salary.highPayRatio}%`)
|
||||
|
||||
console.log('\n【热门岗位类别】')
|
||||
console.log(Object.entries(results.categories)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([k, v]) => `- ${k} (${v}个)`)
|
||||
.join('\n'))
|
||||
|
||||
console.log('\n【经验要求分布】')
|
||||
console.log(Object.entries(results.experience)
|
||||
.map(([k, v]) => `- ${k}: ${v}个`)
|
||||
.join('\n'))
|
||||
|
||||
console.log('\n【工作地区分布】')
|
||||
console.log(Object.entries(results.areas)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([k, v]) => `- ${k}: ${v}个`)
|
||||
.join('\n'))
|
||||
},
|
||||
/** 合并所有统计属性并添加类型前缀 */
|
||||
_mergeAllStats: (results) => {
|
||||
const merged = {}
|
||||
|
||||
// 合并岗位类别(添加前缀)
|
||||
Object.entries(results.categories).forEach(([k, v]) => {
|
||||
merged[`岗位:${k}`] = v
|
||||
})
|
||||
|
||||
// 合并工作地区(添加前缀)
|
||||
Object.entries(results.areas).forEach(([k, v]) => {
|
||||
merged[`地区:${k}`] = v
|
||||
})
|
||||
|
||||
// 合并经验要求(添加前缀)
|
||||
Object.entries(results.experience).forEach(([k, v]) => {
|
||||
merged[`经验:${k}`] = v
|
||||
})
|
||||
|
||||
return merged
|
||||
},
|
||||
|
||||
/** 全属性统一排序输出 */
|
||||
printUnifiedResults: (results, options = {
|
||||
log: false
|
||||
}) => {
|
||||
const mergedData = jobAnalyzer._mergeAllStats(results)
|
||||
|
||||
const sortedEntries = Object.entries(mergedData)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
if (options.log) {
|
||||
// 格式化输出
|
||||
console.log('【全维度排序分析】')
|
||||
console.log(sortedEntries
|
||||
.map(([k, v]) => `- ${k}: ${v}个`)
|
||||
.join('\n'))
|
||||
}
|
||||
return sortedEntries
|
||||
}
|
||||
}
|
||||
|
||||
export default jobAnalyzer
|
57
utils/markdownParser.js
Normal file
57
utils/markdownParser.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import MarkdownIt from '@/lib/markdown-it.min.js';
|
||||
// import MarkdownIt from '@/lib/markdown-it.esm.js'
|
||||
import hljs from "@/lib/highlight/highlight-uni.min.js";
|
||||
import parseHtml from '@/lib/html-parser.js';
|
||||
// import DOMPurify from '@/lib/dompurify@3.2.4es.js';
|
||||
|
||||
export let codeDataList = []
|
||||
|
||||
const md = new MarkdownIt({
|
||||
html: true, // 允许 HTML 标签
|
||||
linkify: true, // 自动解析 URL
|
||||
typographer: true, // 美化标点符号
|
||||
tables: true,
|
||||
breaks: true, // 让 \n 自动换行
|
||||
langPrefix: 'language-', // 代码高亮前缀
|
||||
// 如果结果以 <pre ... 开头,内部包装器则会跳过。
|
||||
highlight: function(str, lang) {
|
||||
let preCode = ""
|
||||
try {
|
||||
preCode = hljs.highlightAuto(str).value
|
||||
} catch (err) {
|
||||
preCode = markdownIt.utils.escapeHtml(str);
|
||||
}
|
||||
|
||||
// 以换行进行分割
|
||||
// 按行拆分代码
|
||||
const lines = preCode.split(/\n/).slice(0, -1);
|
||||
const html = lines
|
||||
.map((line, index) =>
|
||||
line ?
|
||||
`<li><span class="line-num" data-line="${index + 1}"></span>${line}</li>` :
|
||||
''
|
||||
)
|
||||
.join('');
|
||||
|
||||
// 代码复制功能
|
||||
const cacheIndex = codeDataList.length;
|
||||
codeDataList.push(str);
|
||||
return `
|
||||
<div class="code-container">
|
||||
<div class="code-header">
|
||||
<span class="lang-label">${lang || 'plaintext'}</span>
|
||||
<a class="copy-btn" data-copy-index="${cacheIndex}">复制代码</a>
|
||||
</div>
|
||||
<pre class="hljs"><code><ol>${html}</ol></code></pre>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
})
|
||||
|
||||
export function parseMarkdown(content) {
|
||||
if (!content) {
|
||||
return //处理特殊情况,比如网络异常导致的响应的 content 的值为空
|
||||
}
|
||||
const unsafeHtml = md.render(content || '')
|
||||
return unsafeHtml
|
||||
}
|
123
utils/request.js
123
utils/request.js
@@ -1,12 +1,12 @@
|
||||
import config from "@/config.js"
|
||||
import useUserStore from '@/stores/useUserStore';
|
||||
const request = ({
|
||||
export function request({
|
||||
url,
|
||||
method = 'GET',
|
||||
data = {},
|
||||
load = false,
|
||||
header = {}
|
||||
} = {}) => {
|
||||
} = {}) {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (load) {
|
||||
@@ -15,7 +15,6 @@ const request = ({
|
||||
mask: true
|
||||
});
|
||||
}
|
||||
console.log(useUserStore())
|
||||
let Authorization = ''
|
||||
if (useUserStore().token) {
|
||||
Authorization = `${useUserStore().userInfo.token}${useUserStore().token}`
|
||||
@@ -26,28 +25,30 @@ const request = ({
|
||||
data: data,
|
||||
header: {
|
||||
'Authorization': Authorization || '',
|
||||
...header
|
||||
},
|
||||
success: resData => {
|
||||
// 响应拦截
|
||||
if (resData.statusCode === 200) {
|
||||
const {
|
||||
code,
|
||||
data,
|
||||
message
|
||||
msg
|
||||
} = resData.data
|
||||
if (code === 200) {
|
||||
resolve(data)
|
||||
resolve(resData.data)
|
||||
return
|
||||
}
|
||||
uni.showToast({
|
||||
title: message,
|
||||
title: msg,
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
if (resData.data?.code === 401 || resData.data?.code === 402) {
|
||||
store.commit('logout')
|
||||
uni.clearStorageSync('userInfo')
|
||||
useUserStore().logOut()
|
||||
uni.showToast({
|
||||
title: '登录过期,请重新登录',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
const err = new Error('请求出现异常,请联系工作人员')
|
||||
err.error = resData
|
||||
@@ -63,4 +64,104 @@ const request = ({
|
||||
})
|
||||
}
|
||||
|
||||
export default request
|
||||
|
||||
/**
|
||||
* @param url String,请求的地址,默认:none
|
||||
* @param data Object,请求的参数,默认:{}
|
||||
* @param method String,请求的方式,默认:GET
|
||||
* @param loading Boolean,是否需要loading ,默认:false
|
||||
* @param header Object,headers,默认:{}
|
||||
* @returns promise
|
||||
**/
|
||||
export function createRequest(url, data = {}, method = 'GET', loading = false, headers = {}) {
|
||||
if (loading) {
|
||||
uni.showLoading({
|
||||
title: '请稍后',
|
||||
mask: true
|
||||
})
|
||||
}
|
||||
let Authorization = ''
|
||||
if (useUserStore().token) {
|
||||
Authorization = `${useUserStore().token}`
|
||||
}
|
||||
|
||||
const header = headers || {};
|
||||
header["Authorization"] = encodeURIComponent(Authorization);
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: config.baseUrl + url,
|
||||
method: method,
|
||||
data: data,
|
||||
header,
|
||||
success: resData => {
|
||||
// 响应拦截
|
||||
if (resData.statusCode === 200) {
|
||||
const {
|
||||
code,
|
||||
msg
|
||||
} = resData.data
|
||||
if (code === 200) {
|
||||
resolve(resData.data)
|
||||
return
|
||||
}
|
||||
uni.showToast({
|
||||
title: msg,
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
if (resData.data?.code === 401 || resData.data?.code === 402) {
|
||||
useUserStore().logOut()
|
||||
}
|
||||
const err = new Error('请求出现异常,请联系工作人员')
|
||||
err.error = resData
|
||||
reject(err)
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err)
|
||||
},
|
||||
complete: () => {
|
||||
if (loading) {
|
||||
uni.hideLoading();
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export function uploadFile(tempFilePaths, loading = false) {
|
||||
if (loading) {
|
||||
uni.showLoading({
|
||||
title: '请稍后',
|
||||
mask: true
|
||||
})
|
||||
}
|
||||
let Authorization = ''
|
||||
if (useUserStore().token) {
|
||||
Authorization = `${useUserStore().token}`
|
||||
}
|
||||
|
||||
const header = {};
|
||||
header["Authorization"] = encodeURIComponent(Authorization);
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.uploadFile({
|
||||
url: config.baseUrl + '/app/file/upload',
|
||||
filePath: tempFilePaths,
|
||||
name: 'file',
|
||||
header,
|
||||
success: (uploadFileRes) => {
|
||||
if (uploadFileRes.statusCode === 200) {
|
||||
return resolve(uploadFileRes.data)
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err)
|
||||
},
|
||||
complete: () => {
|
||||
if (loading) {
|
||||
uni.hideLoading();
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
347
utils/similarity_Job.js
Normal file
347
utils/similarity_Job.js
Normal file
@@ -0,0 +1,347 @@
|
||||
// 使用Intl.Segmenter对中文文本进行分词
|
||||
function segmentText(text) {
|
||||
const segmenter = new Intl.Segmenter('zh-Hans', {
|
||||
granularity: 'word'
|
||||
}); // 使用中文简体语言
|
||||
const segments = [];
|
||||
for (let segment of segmenter.segment(text.toLowerCase())) { // 转为小写后进行分词
|
||||
segments.push(segment.segment);
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
|
||||
// 语气 停用次
|
||||
const stopWords = ['的', '了', '啊', '哦', '/', '、', ' ', '', '-', '(', ')', '(', ')', '+', "=", "~", "!", "<", ">", "?",
|
||||
"[", "]", "{", "}"
|
||||
]
|
||||
|
||||
function cleanKeywords(arr) {
|
||||
return arr.filter(word => word && !stopWords.includes(word))
|
||||
}
|
||||
|
||||
function calculateMatchScore(source, target) {
|
||||
const sourceSet = new Set(cleanKeywords(source))
|
||||
const targetSet = new Set(cleanKeywords(target))
|
||||
let matchCount = 0
|
||||
for (let word of sourceSet) {
|
||||
if (targetSet.has(word)) {
|
||||
matchCount++
|
||||
}
|
||||
}
|
||||
|
||||
// 匹配度 = source中匹配到的词 / source总词数
|
||||
return matchCount / sourceSet.size
|
||||
}
|
||||
|
||||
class CsimilarityJobs {
|
||||
config = {
|
||||
thresholdVal: 0.69,
|
||||
titleSimilarityWeight: 0.4,
|
||||
salaryMatchWeight: 0.2,
|
||||
areaMatchWeight: 0.2,
|
||||
educationMatchWeight: 0.2,
|
||||
experiencenMatchWeight: 0.1
|
||||
}
|
||||
userTitle = ['Java', 'C', '全栈工程师'];
|
||||
userSalaryMin = 10000;
|
||||
userSalaryMax = 15000;
|
||||
userArea = 0; // 用户指定的区域(例如:市南区)
|
||||
userEducation = 4; // 用户学历(假设4为本科)
|
||||
userExperience = 2; // 用户工作经验
|
||||
|
||||
jobTitle = '';
|
||||
jobMinSalary = 10000
|
||||
jobMaxSalary = 15000
|
||||
jobLocationAreaCode = 0
|
||||
jobEducation = 4
|
||||
jobExperience = 2
|
||||
jobCategory = ''
|
||||
// 系统
|
||||
log = false
|
||||
constructor() {}
|
||||
setUserInfo(resume) {
|
||||
this.userTitle = resume.jobTitle
|
||||
this.userSalaryMax = Number(resume.salaryMax)
|
||||
this.userSalaryMin = Number(resume.salaryMin)
|
||||
this.userArea = Number(resume.area)
|
||||
this.userEducation = resume.education
|
||||
this.userExperience = this.getUserExperience(Number(resume.age))
|
||||
}
|
||||
setJobInfo(jobInfo) {
|
||||
this.jobTitle = jobInfo.jobTitle;
|
||||
this.jobMinSalary = jobInfo.minSalary
|
||||
this.jobMaxSalary = jobInfo.maxSalary
|
||||
this.jobLocationAreaCode = jobInfo.jobLocationAreaCode
|
||||
this.jobEducation = jobInfo.education
|
||||
this.jobExperience = jobInfo.experience
|
||||
this.jobCategory = jobInfo.jobCategory
|
||||
}
|
||||
calculationMatchingDegreeJob(resume) {
|
||||
// 计算职位标题相似度
|
||||
// const titleSimilarity = stringSimilarity.compareTwoStrings(this.userTitle, job.jobTitle);
|
||||
let jobT = null
|
||||
if (this.jobCategory) {
|
||||
jobT = this.calculateBestJobCategoryMatch(resume.jobTitle || resume.jobTitleString || [], this
|
||||
.jobCategory);
|
||||
} else {
|
||||
jobT = this.calculateBestJobMatch(resume.jobTitle || resume.jobTitleString || [], this.jobTitle);
|
||||
}
|
||||
const {
|
||||
bestMatchJobTitle,
|
||||
maxSimilarity
|
||||
} = jobT
|
||||
|
||||
// 计算薪资匹配度
|
||||
const salaryMatch = this.calculateSalaryMatch(Number(resume.salaryMin), Number(resume.salaryMax),
|
||||
this
|
||||
.jobMinSalary, this.jobMaxSalary);
|
||||
|
||||
// 计算区域匹配度
|
||||
const areaMatch = this.calculateAreaMatch(Number(resume.area), this.jobLocationAreaCode);
|
||||
|
||||
// 计算学历匹配度
|
||||
const educationMatch = this.calculateEducationMatch(resume.education, this.jobEducation);
|
||||
|
||||
// 计算工作经验匹配度
|
||||
// const experiencenMatch = this.calculateExperienceMatch2(this.userExperience, job.experience);
|
||||
|
||||
// 综合匹配度 = 0.4 * 职位相似度 + 0.2 * 薪资匹配度 + 0.1 * 区域匹配度 + 0.2 * 学历匹配度 + 0.1 * 工作经验匹配度
|
||||
const overallMatch = this.config.titleSimilarityWeight * maxSimilarity +
|
||||
this.config.salaryMatchWeight * salaryMatch + this.config.areaMatchWeight * areaMatch +
|
||||
this.config.educationMatchWeight * educationMatch
|
||||
// console.log(`Job ${job.jobTitle}工作经验匹配度: ${experiencenMatch}`);
|
||||
if (this.log) {
|
||||
console.log(
|
||||
`Job ${job.jobTitle} 标题相似度 ${maxSimilarity} 薪资匹配度: ${salaryMatch}学历匹配度: ${educationMatch} 区域匹配度: ${areaMatch} 综合匹配度: ${overallMatch.toFixed(2)}`
|
||||
);
|
||||
}
|
||||
|
||||
// 设置阈值进行岗位匹配判断
|
||||
const threshold = this.config.thresholdVal;
|
||||
return {
|
||||
overallMatch: (overallMatch.toFixed(2) * 100) + '%',
|
||||
data: resume,
|
||||
maxSimilarity,
|
||||
salaryMatch,
|
||||
educationMatch,
|
||||
areaMatch
|
||||
}
|
||||
}
|
||||
calculationMatchingDegree(job) {
|
||||
|
||||
// 计算职位标题相似度
|
||||
// console.log(this.userTitle, job.jobTitle)
|
||||
// const titleSimilarity = stringSimilarity.compareTwoStrings(this.userTitle, job.jobTitle);
|
||||
let jobT = null
|
||||
if (job.jobCategory) {
|
||||
jobT = this.calculateBestJobCategoryMatch(this.userTitle, job.jobCategory);
|
||||
} else {
|
||||
jobT = this.calculateBestJobMatch(this.userTitle, job.jobTitle);
|
||||
}
|
||||
const {
|
||||
bestMatchJobTitle,
|
||||
maxSimilarity
|
||||
} = jobT
|
||||
// 计算薪资匹配度
|
||||
const salaryMatch = this.calculateSalaryMatch(this.userSalaryMin, this.userSalaryMax, job.minSalary,
|
||||
job
|
||||
.maxSalary);
|
||||
|
||||
// 计算区域匹配度
|
||||
const areaMatch = this.calculateAreaMatch(this.userArea, job.jobLocationAreaCode);
|
||||
|
||||
// 计算学历匹配度
|
||||
const educationMatch = this.calculateEducationMatch(this.userEducation, job.education);
|
||||
|
||||
// 计算工作经验匹配度
|
||||
// const experiencenMatch = this.calculateExperienceMatch2(this.userExperience, job.experience);
|
||||
|
||||
// 综合匹配度 = 0.4 * 职位相似度 + 0.2 * 薪资匹配度 + 0.1 * 区域匹配度 + 0.2 * 学历匹配度 + 0.1 * 工作经验匹配度
|
||||
const overallMatch = this.config.titleSimilarityWeight * maxSimilarity +
|
||||
this.config.salaryMatchWeight * salaryMatch + this.config.areaMatchWeight * areaMatch +
|
||||
this.config.educationMatchWeight * educationMatch
|
||||
// console.log(`Job ${job.jobTitle}工作经验匹配度: ${experiencenMatch}`);
|
||||
if (this.log) {
|
||||
console.log(
|
||||
`Job ${job.jobTitle} 标题相似度 ${maxSimilarity} 薪资匹配度: ${salaryMatch}学历匹配度: ${educationMatch} 区域匹配度: ${areaMatch} 综合匹配度: ${overallMatch.toFixed(2)}`
|
||||
);
|
||||
}
|
||||
|
||||
// 设置阈值进行岗位匹配判断
|
||||
const threshold = this.config.thresholdVal;
|
||||
if (overallMatch > threshold) {
|
||||
return {
|
||||
overallMatch: (overallMatch.toFixed(2) * 100) + '%',
|
||||
data: job,
|
||||
maxSimilarity,
|
||||
salaryMatch,
|
||||
educationMatch,
|
||||
areaMatch
|
||||
}
|
||||
}
|
||||
}
|
||||
// 根据用户年龄推算工作经验年限区间
|
||||
getUserExperience(age) {
|
||||
if (age = 0) { // 30以下
|
||||
return {
|
||||
min: 0,
|
||||
max: 5
|
||||
};
|
||||
} else if (age <= 1) { // 40以下
|
||||
return {
|
||||
min: 5,
|
||||
max: 10
|
||||
};
|
||||
} else if (age <= 2) { // 50以下
|
||||
return {
|
||||
min: 10,
|
||||
max: 20
|
||||
};
|
||||
} else { // 50以上
|
||||
return {
|
||||
min: 20,
|
||||
max: 40
|
||||
};
|
||||
}
|
||||
}
|
||||
// 计算经验匹配度
|
||||
calculateExperienceMatch2(userExperience, jobExperience) {
|
||||
const jobExperienceRange = this.mapJobExperience(jobExperience);
|
||||
|
||||
if (userExperience.min <= jobExperienceRange.max && userExperience.max >= jobExperienceRange.min) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (
|
||||
(userExperience.min <= jobExperienceRange.max && userExperience.max > jobExperienceRange.min) ||
|
||||
(userExperience.max >= jobExperienceRange.min && userExperience.min < jobExperienceRange.max)
|
||||
) {
|
||||
return 0.5; // 部分匹配
|
||||
}
|
||||
return 0; // 不匹配
|
||||
}
|
||||
// 映射岗位经验要求到工作经验年限区间
|
||||
mapJobExperience(jobExperience) {
|
||||
const experienceMapping = {
|
||||
"1": {
|
||||
min: 0,
|
||||
max: 0
|
||||
},
|
||||
"2": {
|
||||
min: 0,
|
||||
max: 1
|
||||
},
|
||||
"3": {
|
||||
min: 0,
|
||||
max: 1
|
||||
},
|
||||
"4": {
|
||||
min: 1,
|
||||
max: 3
|
||||
},
|
||||
"5": {
|
||||
min: 3,
|
||||
max: 5
|
||||
},
|
||||
"6": {
|
||||
min: 5,
|
||||
max: 10
|
||||
},
|
||||
"7": {
|
||||
min: 10,
|
||||
max: 20
|
||||
},
|
||||
"8": {
|
||||
min: 0,
|
||||
max: 40
|
||||
}
|
||||
};
|
||||
return experienceMapping[jobExperience];
|
||||
}
|
||||
// 计算工作经验匹配度
|
||||
calculateExperiencenMatch(userExperience, jobExperience) {
|
||||
if (userExperience === jobExperience) {
|
||||
return 1;
|
||||
} else if (userExperience > jobExperience) {
|
||||
return 0.75;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
calculateSalaryMatch(userMin, userMax, jobMin, jobMax) {
|
||||
const isMinMatch = userMin >= jobMin && userMin <= jobMax;
|
||||
const isMaxMatch = userMax >= jobMin && userMax <= jobMax;
|
||||
|
||||
if (isMinMatch || isMaxMatch) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const minDifference = Math.abs(userMin - jobMin);
|
||||
const maxDifference = Math.abs(userMax - jobMax);
|
||||
|
||||
if (minDifference > 3000 && maxDifference > 3000) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 0.5; // 部分匹配
|
||||
}
|
||||
// 计算区域匹配度
|
||||
calculateAreaMatch(userArea, jobArea) {
|
||||
return userArea === jobArea ? 1 : 0.5;
|
||||
}
|
||||
calculateBestJobCategoryMatch(userJobTitles, jobTitle) {
|
||||
let maxSimilarity = 0;
|
||||
let bestMatchJobTitle = '';
|
||||
for (let i = 0; i < userJobTitles.length; i++) {
|
||||
let userTitle = userJobTitles[i];
|
||||
if (userTitle === jobTitle) {
|
||||
maxSimilarity = 1;
|
||||
bestMatchJobTitle = userTitle;
|
||||
break
|
||||
}
|
||||
}
|
||||
return {
|
||||
bestMatchJobTitle,
|
||||
maxSimilarity
|
||||
};
|
||||
}
|
||||
// 计算职位匹配度
|
||||
calculateBestJobMatch(userJobTitles, jobTitle) {
|
||||
let maxSimilarity = 0;
|
||||
let bestMatchJobTitle = '';
|
||||
|
||||
userJobTitles.forEach((userTitle) => {
|
||||
const userSegments = segmentText(userTitle);
|
||||
const jobSegments = segmentText(jobTitle);
|
||||
// 比较分词的交集,计算匹配度
|
||||
// const intersection = userSegments.filter(segment => jobSegments.includes(segment));
|
||||
// const similarity = intersection.length / userSegments.length; // 计算匹配度
|
||||
// 计算匹配度
|
||||
const similarity = calculateMatchScore(userSegments, jobSegments)
|
||||
// 记录匹配度最高的职位
|
||||
if (similarity > maxSimilarity) {
|
||||
maxSimilarity = similarity;
|
||||
bestMatchJobTitle = userTitle;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
bestMatchJobTitle,
|
||||
maxSimilarity
|
||||
};
|
||||
}
|
||||
// 计算学历匹配度
|
||||
calculateEducationMatch(userEducation, jobEducation) {
|
||||
if (userEducation === jobEducation) {
|
||||
return 1;
|
||||
} else if (userEducation > jobEducation) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const similarityJobs = new CsimilarityJobs()
|
||||
|
||||
export default similarityJobs
|
144
utils/streamRequest.js
Normal file
144
utils/streamRequest.js
Normal file
@@ -0,0 +1,144 @@
|
||||
import config from "@/config.js"
|
||||
import useUserStore from '@/stores/useUserStore';
|
||||
|
||||
/**
|
||||
* @param url String,请求的地址,默认:none
|
||||
* @param data Object,请求的参数,默认:{}
|
||||
* @returns promise
|
||||
**/
|
||||
export default function StreamRequest(url, data = {}, onDataReceived, onError, onComplete) {
|
||||
const userStore = useUserStore();
|
||||
const Authorization = userStore.token ? encodeURIComponent(userStore.token) : '';
|
||||
|
||||
const headers = {
|
||||
"Authorization": Authorization,
|
||||
"Accept": "text/event-stream",
|
||||
"Content-Type": "application/json;charset=UTF-8"
|
||||
};
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const response = await fetch(config.StreamBaseURl + url, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP 错误: ${response.status}`);
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
|
||||
let buffer = "";
|
||||
while (true) {
|
||||
const {
|
||||
done,
|
||||
value
|
||||
} = await reader.read();
|
||||
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, {
|
||||
stream: true
|
||||
});
|
||||
|
||||
let lines = buffer.split("\n");
|
||||
buffer = lines.pop(); // 可能是不完整的 JSON 片段,留待下次解析
|
||||
for (let line of lines) {
|
||||
if (line.startsWith("data: ")) {
|
||||
const jsonData = line.slice(6).trim();
|
||||
if (jsonData === "[DONE]") {
|
||||
onComplete && onComplete();
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedData = JSON.parse(jsonData);
|
||||
const content = parsedData?.choices?.[0]?.delta?.content ??
|
||||
parsedData?.choices?.[0]?.delta?.reasoning_content ??
|
||||
"";
|
||||
if (content) {
|
||||
onDataReceived && onDataReceived(content);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("JSON 解析失败:", e, "原始数据:", jsonData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onComplete && onComplete();
|
||||
resolve();
|
||||
} catch (error) {
|
||||
console.error("Stream 请求失败:", error);
|
||||
onError && onError(error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param url String,请求的地址,默认:none
|
||||
* @param data Object,请求的参数,默认:{}
|
||||
* @param method String,请求的方式,默认:GET
|
||||
* @param loading Boolean,是否需要loading ,默认:false
|
||||
* @param header Object,headers,默认:{}
|
||||
* @returns promise
|
||||
**/
|
||||
export function chatRequest(url, data = {}, method = 'GET', loading = false, headers = {}) {
|
||||
if (loading) {
|
||||
uni.showLoading({
|
||||
title: '请稍后',
|
||||
mask: true
|
||||
})
|
||||
}
|
||||
let Authorization = ''
|
||||
if (useUserStore().token) {
|
||||
Authorization = `${useUserStore().token}`
|
||||
}
|
||||
|
||||
const header = headers || {};
|
||||
header["Authorization"] = encodeURIComponent(Authorization);
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: config.StreamBaseURl + url,
|
||||
method: method,
|
||||
data: data,
|
||||
header,
|
||||
success: resData => {
|
||||
// 响应拦截
|
||||
if (resData.statusCode === 200) {
|
||||
const {
|
||||
code,
|
||||
msg
|
||||
} = resData.data
|
||||
if (code === 200) {
|
||||
resolve(resData.data)
|
||||
return
|
||||
}
|
||||
uni.showToast({
|
||||
title: msg,
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
if (resData.data?.code === 401 || resData.data?.code === 402) {
|
||||
useUserStore().logOut()
|
||||
}
|
||||
const err = new Error('请求出现异常,请联系工作人员')
|
||||
err.error = resData
|
||||
reject(err)
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err)
|
||||
},
|
||||
complete: () => {
|
||||
if (loading) {
|
||||
uni.hideLoading();
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user