简历优化和岗位简历
This commit is contained in:
+20
-1
@@ -32,7 +32,18 @@ export interface AiResult<T = any> {
|
||||
|
||||
/** 上传简历返回数据 */
|
||||
export interface UploadResumeData {
|
||||
resumeId: number
|
||||
/** 简历 ID(字符串,避免大整数精度丢失) */
|
||||
resumeId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义 JSON 解析:将超出安全整数范围的数字转为字符串,防止精度丢失
|
||||
* 匹配 JSON 中独立的纯数字(不在引号内),超过 15 位的整数加上引号
|
||||
*/
|
||||
function safeParseLargeNumbers(data: string) {
|
||||
// 将超过 15 位的整数值加上引号,变成字符串
|
||||
const processed = data.replace(/:\s*(\d{16,})/g, ':"$1"')
|
||||
return JSON.parse(processed)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,6 +59,14 @@ export function uploadResume(file: File) {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
// 自定义响应解析,防止大整数 ID 精度丢失
|
||||
transformResponse: [(data: string) => {
|
||||
try {
|
||||
return safeParseLargeNumbers(data)
|
||||
} catch {
|
||||
return data
|
||||
}
|
||||
}],
|
||||
}).then(res => res.data)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,15 @@ import axios from 'axios'
|
||||
import type { AxiosResponse } from 'axios'
|
||||
import store from '@/stores'
|
||||
|
||||
/**
|
||||
* 自定义 JSON 解析:将超出安全整数范围的数字转为字符串,防止精度丢失
|
||||
* 匹配 JSON 中独立的纯数字(超过 15 位的整数),加上引号变成字符串
|
||||
*/
|
||||
function safeParseLargeNumbers(data: string) {
|
||||
const processed = data.replace(/:\s*(\d{16,})/g, ':"$1"')
|
||||
return JSON.parse(processed)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 axios 实例
|
||||
* withCredentials: true — 浏览器自动携带 Cookie(包括 HttpOnly 的 Token)
|
||||
@@ -10,6 +19,14 @@ const service = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL,
|
||||
timeout: 15000,
|
||||
withCredentials: true,
|
||||
// 自定义响应解析,防止大整数 ID 精度丢失
|
||||
transformResponse: [(data: string) => {
|
||||
try {
|
||||
return safeParseLargeNumbers(data)
|
||||
} catch {
|
||||
return data
|
||||
}
|
||||
}],
|
||||
})
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* 文本差异对比工具 — 用于简历原文与 AI 改进版本的逐字高亮对比
|
||||
*
|
||||
* 采用 Myers diff 算法的简化实现,能精确找出两段文本之间的
|
||||
* 插入、删除和保留片段,比逐字顺序扫描更准确。
|
||||
*/
|
||||
|
||||
/** 差异片段类型 */
|
||||
export interface DiffSegment {
|
||||
/** 文本内容 */
|
||||
text: string
|
||||
/** 是否为差异(需要高亮) */
|
||||
highlight: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两段文本的差异,返回新文本中每个片段的高亮标记
|
||||
* 使用最长公共子序列(LCS)来识别相同部分,非 LCS 部分标记为差异
|
||||
*
|
||||
* @param original - 原文字符串
|
||||
* @param modified - AI 改进后的字符串
|
||||
* @returns 差异片段数组,每个片段包含文本和是否高亮
|
||||
*/
|
||||
export function computeDiff(original: string, modified: string): DiffSegment[] {
|
||||
if (!original || !modified) {
|
||||
return modified ? [{ text: modified, highlight: true }] : []
|
||||
}
|
||||
|
||||
// 计算 LCS 表
|
||||
const m = original.length
|
||||
const n = modified.length
|
||||
|
||||
// 优化:如果文本完全相同,直接返回
|
||||
if (original === modified) {
|
||||
return [{ text: modified, highlight: false }]
|
||||
}
|
||||
|
||||
// 使用滚动数组优化空间,只保留两行
|
||||
let prev = new Uint16Array(n + 1)
|
||||
let curr = new Uint16Array(n + 1)
|
||||
|
||||
for (let i = 1; i <= m; i++) {
|
||||
for (let j = 1; j <= n; j++) {
|
||||
if (original[i - 1] === modified[j - 1]) {
|
||||
curr[j] = prev[j - 1] + 1
|
||||
} else {
|
||||
curr[j] = Math.max(prev[j], curr[j - 1])
|
||||
}
|
||||
}
|
||||
// 交换行
|
||||
;[prev, curr] = [curr, prev]
|
||||
curr.fill(0)
|
||||
}
|
||||
|
||||
// 回溯找出 LCS 中每个字符在 modified 中的位置
|
||||
// 需要完整的 DP 表来回溯,重新计算(用二维数组但限制大小)
|
||||
const lcsSet = new Set<number>() // modified 中属于 LCS 的索引
|
||||
|
||||
// 对于较长文本,使用空间优化的回溯
|
||||
if (m * n > 1000000) {
|
||||
// 文本过长时使用简化的贪心匹配
|
||||
let oi = 0
|
||||
for (let mi = 0; mi < n && oi < m; mi++) {
|
||||
if (modified[mi] === original[oi]) {
|
||||
lcsSet.add(mi)
|
||||
oi++
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 标准 LCS 回溯
|
||||
const dp: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0))
|
||||
for (let i = 1; i <= m; i++) {
|
||||
for (let j = 1; j <= n; j++) {
|
||||
if (original[i - 1] === modified[j - 1]) {
|
||||
dp[i][j] = dp[i - 1][j - 1] + 1
|
||||
} else {
|
||||
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 回溯
|
||||
let i = m, j = n
|
||||
while (i > 0 && j > 0) {
|
||||
if (original[i - 1] === modified[j - 1]) {
|
||||
lcsSet.add(j - 1)
|
||||
i--
|
||||
j--
|
||||
} else if (dp[i - 1][j] >= dp[i][j - 1]) {
|
||||
i--
|
||||
} else {
|
||||
j--
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 根据 LCS 标记生成差异片段
|
||||
const segments: DiffSegment[] = []
|
||||
let currentText = ''
|
||||
let currentHighlight = false
|
||||
|
||||
for (let mi = 0; mi < n; mi++) {
|
||||
const isMatch = lcsSet.has(mi)
|
||||
const highlight = !isMatch
|
||||
|
||||
if (mi === 0) {
|
||||
currentHighlight = highlight
|
||||
currentText = modified[mi]
|
||||
} else if (highlight === currentHighlight) {
|
||||
currentText += modified[mi]
|
||||
} else {
|
||||
segments.push({ text: currentText, highlight: currentHighlight })
|
||||
currentText = modified[mi]
|
||||
currentHighlight = highlight
|
||||
}
|
||||
}
|
||||
|
||||
// 推入最后一段
|
||||
if (currentText) {
|
||||
segments.push({ text: currentText, highlight: currentHighlight })
|
||||
}
|
||||
|
||||
return segments
|
||||
}
|
||||
Reference in New Issue
Block a user