feat: 支持通过外部API提供简历数据源

This commit is contained in:
weicanie
2025-09-15 21:33:16 +08:00
committed by siyue
parent 40a0a0b754
commit 65e1328fa5
4 changed files with 198 additions and 70 deletions
+2 -1
View File
@@ -1,2 +1,3 @@
FONTCONFIG_PATH=/var/task/fonts
FONTCONFIG_PATH=/var/task/fonts
NEXT_PUBLIC_RESUME_REPO_URL = 简历数据仓库接口URL
+156
View File
@@ -0,0 +1,156 @@
import { ResumeRepositoryManager } from '../types/repository';
import { ResumeData } from '../types/resume';
import { getFileHandle, verifyPermission } from '../utils/fileSystem';
// 同步简历到文件系统
const syncResumeToFile = async (resumeData: ResumeData, prevResume?: ResumeData) => {
try {
const handle = await getFileHandle('syncDirectory');
if (!handle) {
console.warn('No directory handle found');
return;
}
const hasPermission = await verifyPermission(handle);
if (!hasPermission) {
console.warn('No permission to write to directory');
return;
}
const dirHandle = handle as FileSystemDirectoryHandle;
// 避免简历标题发生变化时保留不再使用的文件
if (prevResume && prevResume.id === resumeData.id && prevResume.title !== resumeData.title) {
try {
await dirHandle.removeEntry(`${prevResume.title}.json`);
} catch (error) {
console.warn('Error deleting old file:', error);
}
}
const fileName = `${resumeData.title}.json`;
const fileHandle = await dirHandle.getFileHandle(fileName, {
create: true
});
const writable = await fileHandle.createWritable();
// 如果简历文件已存在,则直接替换其内容
await writable.write(JSON.stringify(resumeData, null, 2));
await writable.close();
} catch (error) {
console.error('Error syncing resume to file:', error);
}
};
// 从文件系统中删除简历
// 不在具体业务逻辑中暴露文件句柄
const deleteResumeFromFile = async (resume: ResumeData) => {
try {
const handle = await getFileHandle('syncDirectory');
if (!handle) return;
const hasPermission = await verifyPermission(handle);
if (!hasPermission) return;
const dirHandle = handle as FileSystemDirectoryHandle;
try {
await dirHandle.removeEntry(`${resume.title}.json`);
} catch (error) {}
} catch (error) {
console.error('Error deleting resume file:', error);
}
};
// 从文件系统中获取简历数据,并同步到store
const getResumesFromFiles = async (updateResumeFromFile: (resume: ResumeData) => void) => {
try {
const handle = await getFileHandle('syncDirectory');
if (!handle) return;
const hasPermission = await verifyPermission(handle);
if (!hasPermission) return;
const dirHandle = handle as FileSystemDirectoryHandle;
// @ts-ignore
for await (const entry of dirHandle.values()) {
if (entry.kind === 'file' && entry.name.endsWith('.json')) {
try {
const file = await entry.getFile();
const content = await file.text();
const resumeData = JSON.parse(content);
// 将简历数据同步到store
updateResumeFromFile(resumeData);
} catch (error) {
console.error('Error reading resume file:', error);
}
}
}
} catch (error) {
console.error('Error syncing resumes from files:', error);
}
};
const syncResuemToAPI = async (resumeData: ResumeData, prevResume?: ResumeData) => {
try {
await fetch(`${process.env.NEXT_PUBLIC_RESUME_REPO_URL}`, {
method: 'POST',
body: JSON.stringify({ actionType: 'sync', payload: { resumeData, prevResume } }),
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${localStorage.getItem('token')}`
}
});
} catch (error) {
console.error('Error syncing resume to database:', error);
}
};
const deleteResumeFromAPI = async (resumeData: ResumeData) => {
try {
await fetch(`${process.env.NEXT_PUBLIC_RESUME_REPO_URL}`, {
method: 'POST',
body: JSON.stringify({ actionType: 'delete', payload: { resumeData } }),
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${localStorage.getItem('token')}`
}
});
} catch (error) {
console.error('Error deleting resume from database:', error);
}
};
const getResumesFromAPI = async (updateResumeFromFile: (resume: ResumeData) => void) => {
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_RESUME_REPO_URL}`, {
method: 'POST',
body: JSON.stringify({ actionType: 'get' }),
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${localStorage.getItem('token')}`
}
});
const data = await response.json();
// data.data: prisma-ai后端统一返回`SDF`
for (const resume of data.data) {
updateResumeFromFile(resume);
}
} catch (error) {
console.error('Error getting resumes from database:', error);
}
};
// 使用浏览器端的 File System Access API
const clientfileManager: ResumeRepositoryManager = {
syncResumeToRepository: syncResumeToFile,
deleteResumeFromRepository: deleteResumeFromFile,
getResumeFromRepository: getResumesFromFiles
};
// 使用外部API(服务器文件系统、数据库、OSS...)
const outerAPIManager: ResumeRepositoryManager = {
syncResumeToRepository: syncResuemToAPI,
deleteResumeFromRepository: deleteResumeFromAPI,
getResumeFromRepository: getResumesFromAPI
};
const resumeRepoManager = clientfileManager;
export { resumeRepoManager };
+15 -69
View File
@@ -3,7 +3,6 @@ import {
initialResumeState,
initialResumeStateEn,
} from "@/config/initialResumeData";
import { getFileHandle, verifyPermission } from "@/utils/fileSystem";
import { generateUUID } from "@/utils/uuid";
import { create } from "zustand";
import {
@@ -16,6 +15,7 @@ import {
Project,
ResumeData,
} from "../types/resume";
import { resumeRepoManager } from "./resumeRepositoryManager";
interface ResumeStore {
resumes: Record<string, ResumeData>;
activeResumeId: string | null;
@@ -23,6 +23,7 @@ interface ResumeStore {
createResume: (templateId: string | null) => string;
deleteResume: (resume: ResumeData) => void;
getResumesFromRepository: () => void;
duplicateResume: (resumeId: string) => string;
updateResume: (resumeId: string, data: Partial<ResumeData>) => void;
setActiveResume: (resumeId: string) => void;
@@ -61,50 +62,6 @@ interface ResumeStore {
addResume: (resume: ResumeData) => string;
}
// 同步简历到文件系统
const syncResumeToFile = async (
resumeData: ResumeData,
prevResume?: ResumeData
) => {
try {
const handle = await getFileHandle("syncDirectory");
if (!handle) {
console.warn("No directory handle found");
return;
}
const hasPermission = await verifyPermission(handle);
if (!hasPermission) {
console.warn("No permission to write to directory");
return;
}
const dirHandle = handle as FileSystemDirectoryHandle;
if (
prevResume &&
prevResume.id === resumeData.id &&
prevResume.title !== resumeData.title
) {
try {
await dirHandle.removeEntry(`${prevResume.title}.json`);
} catch (error) {
console.warn("Error deleting old file:", error);
}
}
const fileName = `${resumeData.title}.json`;
const fileHandle = await dirHandle.getFileHandle(fileName, {
create: true,
});
const writable = await fileHandle.createWritable();
await writable.write(JSON.stringify(resumeData, null, 2));
await writable.close();
} catch (error) {
console.error("Error syncing resume to file:", error);
}
};
export const useResumeStore = create<ResumeStore>(
(
(set, get) => ({
@@ -150,7 +107,7 @@ export const useResumeStore = create<ResumeStore>(
activeResume: newResume,
}));
syncResumeToFile(newResume);
resumeRepoManager.syncResumeToRepository(newResume);
return id;
},
@@ -165,7 +122,7 @@ export const useResumeStore = create<ResumeStore>(
...data,
};
syncResumeToFile(updatedResume, resume);
resumeRepoManager.syncResumeToRepository(updatedResume, resume);
return {
resumes: {
@@ -180,6 +137,10 @@ export const useResumeStore = create<ResumeStore>(
});
},
getResumesFromRepository: () => {
resumeRepoManager.getResumeFromRepository(get().updateResumeFromFile);
},
// 从文件更新,直接更新resumes
updateResumeFromFile: (resume) => {
set((state) => ({
@@ -197,33 +158,18 @@ export const useResumeStore = create<ResumeStore>(
}
},
deleteResume: (resume) => {
deleteResume: resume => {
const resumeId = resume.id;
set((state) => {
set(state => {
const { [resumeId]: _, activeResume, ...rest } = state.resumes;
return {
resumes: rest,
activeResumeId: null,
activeResume: null,
activeResume: null
};
});
(async () => {
try {
const handle = await getFileHandle("syncDirectory");
if (!handle) return;
const hasPermission = await verifyPermission(handle);
if (!hasPermission) return;
const dirHandle = handle as FileSystemDirectoryHandle;
try {
await dirHandle.removeEntry(`${resume.title}.json`);
} catch (error) {}
} catch (error) {
console.error("Error deleting resume file:", error);
}
})();
resumeRepoManager.deleteResumeFromRepository(resume);
},
duplicateResume: (resumeId) => {
@@ -288,7 +234,7 @@ export const useResumeStore = create<ResumeStore>(
activeResume: updatedResume,
};
syncResumeToFile(updatedResume, state.activeResume);
resumeRepoManager.syncResumeToRepository(updatedResume, state.activeResume);
return newState;
});
@@ -611,7 +557,7 @@ export const useResumeStore = create<ResumeStore>(
activeResumeId: resume.id,
}));
syncResumeToFile(resume);
resumeRepoManager.syncResumeToRepository(resume);
return resume.id;
},
})
+25
View File
@@ -0,0 +1,25 @@
import {
ResumeData
} from './resume';
/**
* 简历数据仓库管理器
*/
interface ResumeRepositoryManager {
/**
* 进行简历数据的创建(C)和更新(U)
*/
syncResumeToRepository: (resumeData: ResumeData, prevResume?: ResumeData) => Promise<void>;
/**
* 进行简历数据的删除(D
*/
deleteResumeFromRepository: (resume: ResumeData) => Promise<void>;
/**
* 进行简历数据的获取(R
* @description 从文件系统或数据库中获取简历数据,并同步到store中
*/
getResumeFromRepository: (updateResumeFromFile: (resume: ResumeData) => void) => Promise<void>;
}
export { type ResumeRepositoryManager };