一个面向招聘场景的全栈应用,支持 PDF 简历上传、AI 结构化信息提取、岗位匹配评分、候选人管理与多 JD 对比分析。
当前仓库已完成:
- PDF 批量上传与进度展示
- PDF 首页缩略图预览
- AI 结构化提取与 SSE 渐进渲染
- 岗位管理与候选人管理
- 候选人详情、状态流转、手动修正
- 岗位匹配分析与多 JD 对比
- 暗黑 / 亮色主题切换
本项目采用 pnpm workspace + turborepo 的 monorepo 结构,前后端分离,公共类型共享。
.
├── apps
│ ├── web # Next.js 前端
│ └── api # Express 后端
├── packages
│ └── shared-types # 前后端共享类型与 zod schema
├── DATABASE_SCHEMA.sql # Supabase / Postgres 建表脚本
├── DESIGN.md # 设计文档
├── API_CONTRACT.md # API 约定
├── JOTAI_DOMAIN_GUIDE.md # 前端领域化状态规范
└── README.md
前端位于 apps/web,基于 Next.js App Router 构建,主要分层如下:
src/app- 路由入口与页面装配
src/components/ui- shadcn 风格基础组件
src/components/common- 通用布局与公共业务组件
src/features- 页面级业务编排
src/domains- 领域状态、selectors、actions、hooks
src/instance- 统一业务调用入口,约束为
useInstance()/instance.domain.action()
- 统一业务调用入口,约束为
前端状态管理遵循“领域化建模”,不允许散落大量业务 atom。当前已拆分为:
upload-domaincandidate-domainjob-domainmatching-domainui-domainsettings-domain
后端位于 apps/api,基于 Express,职责是提供 REST API、SSE、文件上传、PDF 解析、AI 编排与数据持久化。
主要目录:
src/routes- HTTP 路由
src/services- 业务编排、AI 提取、上传处理、匹配分析
src/repositories- Supabase Postgres / Storage 访问
src/schemas- 入参校验
src/middleware- 错误处理与请求校验
src/clients- Supabase client
- 结构化数据:
Supabase Postgres - 文件存储:
Supabase Storage - AI 调用:兼容 OpenAI 风格接口
核心数据实体包括:
resume_uploadscandidatescandidate_skillscandidate_educationscandidate_work_experiencescandidate_projectsjobscandidate_matchings
Next.js- 适合 Vercel 部署
- 路由、构建、静态资源与页面组织成熟
React- 组件化能力强,适合中后台复杂交互
Tailwind CSS- 便于快速建立语义化、统一化设计系统
shadcn 风格组件- 组件基础质量高,可控性强,适合黑白极简风格
Jotai- 轻量,适合按领域拆分状态,避免 Redux 级别样板代码
Express- 足够轻,便于落地文件上传、SSE、RESTful API
- 与 Vercel Serverless / Functions 兼容成本低
pdf-parse + pdfjs-dist- 支持 PDF 文本提取与前端首页缩略图能力
Supabase Postgres- 关系型模型适合候选人、教育、工作、项目、岗位、评分记录
- 与 Vercel 配合顺滑
Supabase Storage- 适合存储原始简历 PDF
- 支持签名 URL,便于详情页预览
TypeScript- 统一前后端类型边界
zod- 请求数据与共享 schema 校验清晰
- Node.js
>= 20 - pnpm
>= 10 - 一个可用的 Supabase 项目
在仓库根目录执行:
pnpm install根目录已有模板文件:
建议按当前仓库约定分别创建:
- 根目录
.env.local apps/web/.env.localapps/api/.env.local
前端至少需要:
NEXT_PUBLIC_API_BASE_URL=http://localhost:3001
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=your_publishable_key后端至少需要:
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key
SUPABASE_STORAGE_BUCKET=resumes-private
APP_BASE_URL=http://localhost:3001
WEB_BASE_URL=http://localhost:3000
OPENAI_API_KEY=说明:
publishable key给前端使用service role key只能给后端使用- 当前项目支持前端通过“AI 设置”弹窗手动配置 AI
baseUrl与apiKey,因此OPENAI_API_KEY可暂时留空
在 Supabase SQL Editor 中执行:
然后在 Supabase Storage 中创建 bucket:
resumes-private
在仓库根目录执行:
pnpm dev默认端口:
- Web:
http://localhost:3000 - API:
http://localhost:3001
如果只想单独启动:
pnpm --filter @ai-resume/web dev
pnpm --filter @ai-resume/api devpnpm --filter @ai-resume/web build
pnpm --filter @ai-resume/api build推荐部署到 Vercel,采用“同仓库双项目”模式。
resume-web- Root Directory:
apps/web
- Root Directory:
resume-api- Root Directory:
apps/api
- Root Directory:
在 Vercel 导入同一个仓库,创建后端项目:
- Root Directory:
apps/api - Framework Preset:
Other
仓库中已包含:
它会将 Express 服务作为 Vercel Node Function 部署。
后端环境变量:
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key
SUPABASE_STORAGE_BUCKET=resumes-private
APP_BASE_URL=https://your-api-domain.vercel.app
WEB_BASE_URL=https://your-web-domain.vercel.app
OPENAI_API_KEY=部署完成后先验证:
https://your-api-domain.vercel.app/health
再创建前端项目:
- Root Directory:
apps/web - Framework Preset:
Next.js
前端环境变量:
NEXT_PUBLIC_API_BASE_URL=https://your-api-domain.vercel.app
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=your_publishable_key建议顺序如下:
- 先部署 API
- 拿到 API 正式域名
- 将前端的
NEXT_PUBLIC_API_BASE_URL指向该域名 - 再部署 Web
部署完成后建议至少检查:
/health是否返回正常- 前端上传 PDF 是否成功
- SSE 提取过程是否正常流式返回
- 候选人详情页 PDF 预览是否可用
- 岗位匹配分析是否能成功生成结果
原因是前后端共享:
- 候选人 / 岗位 / 匹配结果 DTO
- zod schema
- API 返回结构
这样可以显著减少接口漂移与重复定义。
项目页面交互多,如果用“一个输入框一个 atom”的方式,状态很快失控。
因此统一改成:
domains/*/atoms.tsdomains/*/selectors.tsdomains/*/actions.tsdomains/*/hooks.ts
并要求业务操作经 useInstance() 统一收口。
没有把“基本信息 / 教育 / 工作 / 技能 / 项目”拆成多次 AI 请求,而是:
- 后端单次调用 AI 获取完整结构化结果
- 再按领域分段通过 SSE 推送给前端
这样做的原因是:
- 降低模型调用成本
- 降低多次调用带来的时延和不一致
- 前端仍然能保留“逐步渲染”的体验
缩略图使用 pdfjs-dist 在浏览器端提取首页并生成预览图,而不是在服务端做渲染。
这样做的收益是:
- 不增加后端渲染依赖
- 不增加 Vercel Function 负担
- 用户上传时可以立即看到预览
AI 提取不可能完全准确,特别是:
- 日期格式
- 公司 / 项目字段切分
- 技能归类
因此详情页保留手动修正入口,确保系统不是只读演示,而是具备真实使用闭环。
这样可以让用户保持在选择上下文中,快速切换:
- 主岗位
- 对比 JD
- 候选人选择
- 结果查看
交互效率更高,也更适合桌面端分析台场景。
当前仓库的代码能力与题目要求对照说明,见:
当前已验证通过:
pnpm --filter @ai-resume/api build
pnpm --filter @ai-resume/web build