简历优化和岗位简历

This commit is contained in:
2026-04-13 21:34:40 +08:00
parent a39835be01
commit 1c91818494
32 changed files with 3850 additions and 320 deletions
+20 -1
View File
@@ -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)
}
+17
View File
@@ -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
}
}],
})
/**
+124
View File
@@ -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
}