定制简历浏览器记录缓存加IndexDB存储工具封装,修复同一个页面的不同协议切换问题

This commit is contained in:
2026-05-29 15:45:16 +08:00
parent 0d1d080cc1
commit 59ac8ab783
3 changed files with 342 additions and 21 deletions
+264
View File
@@ -283,3 +283,267 @@ export function generateResumeWordFile(element: HTMLElement, fileName: string):
const blob = new Blob([fullHtml], { type: 'application/msword' })
return new File([blob], `${fileName}.doc`, { type: 'application/msword' })
}
// ==================== 定制简历本地缓存(IndexedDB ====================
/**
* 【定制简历缓存机制说明】
*
* 由于服务器资源有限,投递流程中生成的岗位专属简历PDF不直接上传到OSS,
* 而是以 Blob 形式缓存到浏览器 IndexedDB 中(空间可达几百MB,远大于 localStorage 的 5~10MB)。
*
* 存储结构:
* - 索引记录数组:localStorage key = "CUSTOM_RESUME_CACHE_INDEX"
* 存储 CachedResumeRecord[] 数组的 JSON 字符串(几百字节,不占空间),每条记录包含:
* userId(用户ID)、jobId(岗位ID)、fileName(雪花ID文件名)、
* storageKey(简历文件在 IndexedDB 中的 key)、localDateTime(存储时间)
*
* - 简历文件数据:IndexedDB 数据库名 = "ResumeFileCache",对象仓库名 = "files"
* 以 storageKey 为主键,直接存储 PDF 的 Blob 对象(无需 Base64 编码,不膨胀体积)
*
* 使用方式:
* 1. 存储:调用 cacheResumePdfToLocal(element, userId, jobId) 生成PDF并缓存到IndexedDB
* 2. 查询:调用 getCachedResumeRecord(userId, jobId) 通过用户ID+岗位ID获取缓存记录
* 3. 取文件:调用 getCachedResumeFile(storageKey) 通过记录中的 storageKey 从IndexedDB获取 File 对象
* 4. 清理:调用 clearExpiredResumeCache(maxAgeDays) 清理过期缓存
*
* 优势:
* - IndexedDB 直接存 Blob,不需要 Base64 编码,文件体积不膨胀
* - 空间充足(几百MB~GB级),不会像 localStorage 那样容易满
* - 取出来就是 Blob,直接 new File([blob], name) 即可当文件使用
*/
/** 定制简历缓存索引记录 */
export interface CachedResumeRecord {
/** 系统用户ID */
userId: string
/** 对应岗位ID */
jobId: string
/** 缓存的简历文件名(雪花ID格式,不含扩展名) */
fileName: string
/** 简历文件在 IndexedDB 中的存储 key */
storageKey: string
/** 存储时间(ISO 8601 格式) */
localDateTime: string
}
/** 缓存索引在 localStorage 中的 key(仅存索引JSON,体积极小) */
const CACHE_INDEX_KEY = 'CUSTOM_RESUME_CACHE_INDEX'
/** IndexedDB 数据库名 */
const IDB_NAME = 'ResumeFileCache'
/** IndexedDB 对象仓库名 */
const IDB_STORE = 'files'
/** IndexedDB 版本号 */
const IDB_VERSION = 1
/**
* 打开 IndexedDB 数据库连接
* @returns IDBDatabase 实例
*/
function openResumeDB(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(IDB_NAME, IDB_VERSION)
request.onupgradeneeded = () => {
const db = request.result
// 创建对象仓库(如果不存在)
if (!db.objectStoreNames.contains(IDB_STORE)) {
db.createObjectStore(IDB_STORE)
}
}
request.onsuccess = () => resolve(request.result)
request.onerror = () => reject(request.error)
})
}
/**
* 向 IndexedDB 写入 Blob 数据
* @param key 存储键
* @param blob 文件 Blob
*/
async function idbPut(key: string, blob: Blob): Promise<void> {
const db = await openResumeDB()
return new Promise((resolve, reject) => {
const tx = db.transaction(IDB_STORE, 'readwrite')
const store = tx.objectStore(IDB_STORE)
const request = store.put(blob, key)
request.onsuccess = () => resolve()
request.onerror = () => reject(request.error)
tx.oncomplete = () => db.close()
})
}
/**
* 从 IndexedDB 读取 Blob 数据
* @param key 存储键
* @returns Blob 对象,未找到返回 null
*/
async function idbGet(key: string): Promise<Blob | null> {
const db = await openResumeDB()
return new Promise((resolve, reject) => {
const tx = db.transaction(IDB_STORE, 'readonly')
const store = tx.objectStore(IDB_STORE)
const request = store.get(key)
request.onsuccess = () => resolve(request.result || null)
request.onerror = () => reject(request.error)
tx.oncomplete = () => db.close()
})
}
/**
* 从 IndexedDB 删除指定 key 的数据
* @param key 存储键
*/
async function idbDelete(key: string): Promise<void> {
const db = await openResumeDB()
return new Promise((resolve, reject) => {
const tx = db.transaction(IDB_STORE, 'readwrite')
const store = tx.objectStore(IDB_STORE)
const request = store.delete(key)
request.onsuccess = () => resolve()
request.onerror = () => reject(request.error)
tx.oncomplete = () => db.close()
})
}
/**
* 生成简单的雪花ID(基于时间戳 + 随机数,保证不重复)
* @returns 雪花ID字符串
*/
function generateSnowflakeId(): string {
const timestamp = Date.now().toString(36)
const random = Math.random().toString(36).substring(2, 10)
return `${timestamp}${random}`
}
/**
* 获取缓存索引记录数组(从 localStorage 读取)
* @returns 缓存记录数组
*/
function getCacheIndex(): CachedResumeRecord[] {
try {
const raw = localStorage.getItem(CACHE_INDEX_KEY)
return raw ? JSON.parse(raw) : []
} catch {
return []
}
}
/**
* 保存缓存索引记录数组(写入 localStorage
* @param records 缓存记录数组
*/
function saveCacheIndex(records: CachedResumeRecord[]) {
localStorage.setItem(CACHE_INDEX_KEY, JSON.stringify(records))
}
/**
* 将简历PDF生成并缓存到浏览器 IndexedDB
* @param element 简历DOM元素
* @param userId 当前用户ID
* @param jobId 对应岗位ID
* @returns 缓存记录(包含 storageKey 等信息),存储失败返回 null
*/
export async function cacheResumePdfToLocal(
element: HTMLElement,
userId: string,
jobId: string,
): Promise<CachedResumeRecord | null> {
try {
// 生成雪花ID作为文件名
const snowflakeId = generateSnowflakeId()
const storageKey = `resume_${snowflakeId}`
// 生成PDF Blob
const options = {
margin: [10, 10, 10, 10] as [number, number, number, number],
filename: `${snowflakeId}.pdf`,
image: { type: 'jpeg' as const, quality: 0.98 },
html2canvas: { scale: 2, useCORS: true, logging: false },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' as const },
pagebreak: { mode: ['css', 'legacy'] },
}
const blob: Blob = await html2pdf().set(options).from(element).outputPdf('blob')
// 将 Blob 直接存入 IndexedDB(无需 Base64 编码)
await idbPut(storageKey, blob)
// 构建缓存记录
const record: CachedResumeRecord = {
userId,
jobId,
fileName: snowflakeId,
storageKey,
localDateTime: new Date().toISOString(),
}
// 更新索引(如果同一用户+岗位已有记录,先移除旧的)
const index = getCacheIndex()
const existIdx = index.findIndex(r => r.userId === userId && r.jobId === jobId)
if (existIdx !== -1) {
// 删除旧的 IndexedDB 文件缓存
await idbDelete(index[existIdx].storageKey)
index.splice(existIdx, 1)
}
index.push(record)
saveCacheIndex(index)
return record
} catch (e) {
console.error('[resumeExport] 缓存简历到IndexedDB失败', e)
return null
}
}
/**
* 通过用户ID和岗位ID查询缓存记录
* @param userId 用户ID
* @param jobId 岗位ID
* @returns 缓存记录,未找到返回 null
*/
export function getCachedResumeRecord(userId: string, jobId: string): CachedResumeRecord | null {
const index = getCacheIndex()
return index.find(r => r.userId === userId && r.jobId === jobId) || null
}
/**
* 通过 storageKey 从 IndexedDB 获取缓存的简历 File 对象
* @param storageKey 缓存记录中的 storageKey
* @param fileName 可选,指定输出文件名(不含扩展名),默认用 storageKey 中的雪花ID
* @returns PDF格式的 File 对象,未找到返回 null
*/
export async function getCachedResumeFile(storageKey: string, fileName?: string): Promise<File | null> {
try {
const blob = await idbGet(storageKey)
if (!blob) return null
const name = fileName || storageKey.replace('resume_', '')
return new File([blob], `${name}.pdf`, { type: 'application/pdf' })
} catch {
return null
}
}
/**
* 清理过期的简历缓存
* @param maxAgeDays 最大保留天数,默认7天
*/
export async function clearExpiredResumeCache(maxAgeDays = 7) {
const index = getCacheIndex()
const now = Date.now()
const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000
const validRecords: CachedResumeRecord[] = []
for (const record of index) {
const recordTime = new Date(record.localDateTime).getTime()
if (now - recordTime > maxAgeMs) {
// 过期,删除 IndexedDB 中的文件缓存
await idbDelete(record.storageKey)
} else {
validRecords.push(record)
}
}
saveCacheIndex(validRecords)
}