文档
API 示例代码
SmartCV API 的完整示例代码和使用场景,包含 JavaScript、React、Node.js 等多种语言示例
更新时间: 2024/12/30
本页面提供 SmartCV API 的完整示例代码,涵盖常见的使用场景和最佳实践。
🔑 基础设置
环境配置
# 安装依赖
npm install axios @types/node dotenv
# 环境变量 (.env)
NEXT_PUBLIC_API_BASE_URL=https://smartcv.cc/api
NEXTAUTH_SECRET=your-secret-key
API 客户端初始化
// utils/api-client.js
import axios from 'axios'
const apiClient = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_BASE_URL || '/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
})
// 请求拦截器 - 添加认证令牌
apiClient.interceptors.request.use((config) => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// 响应拦截器 - 错误处理
apiClient.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// 令牌过期,重定向到登录页
window.location.href = '/login'
}
return Promise.reject(error)
}
)
export default apiClient
🔐 认证示例
用户注册
// services/auth.js
import apiClient from '@/utils/api-client'
export async function registerUser(userData) {
try {
const response = await apiClient.post('/register', {
name: userData.name,
email: userData.email,
password: userData.password
})
return {
success: true,
data: response.data,
message: '注册成功,请查收验证邮件'
}
} catch (error) {
return {
success: false,
error: error.response?.data?.error || '注册失败'
}
}
}
// React 组件使用
function RegisterForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
password: ''
})
const [loading, setLoading] = useState(false)
const handleSubmit = async (e) => {
e.preventDefault()
setLoading(true)
const result = await registerUser(formData)
if (result.success) {
toast.success(result.message)
router.push('/login')
} else {
toast.error(result.error)
}
setLoading(false)
}
return (
<form onSubmit={handleSubmit}>
{/* 表单字段 */}
<button type="submit" disabled={loading}>
{loading ? '注册中...' : '注册'}
</button>
</form>
)
}
NextAuth.js 配置
// pages/api/auth/[...nextauth].js
import NextAuth from 'next-auth'
import GoogleProvider from 'next-auth/providers/google'
import CredentialsProvider from 'next-auth/providers/credentials'
export default NextAuth({
providers: [
CredentialsProvider({
name: 'credentials',
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' }
},
async authorize(credentials) {
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
})
const user = await response.json()
if (response.ok && user) {
return user
}
return null
} catch (error) {
return null
}
}
}),
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET
})
],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.id = user.id
token.plan = user.plan
}
return token
},
async session({ session, token }) {
session.user.id = token.id
session.user.plan = token.plan
return session
}
}
})
📄 简历管理示例
创建简历
// hooks/useResumes.js
import { useState, useCallback } from 'react'
import apiClient from '@/utils/api-client'
export function useResumes() {
const [resumes, setResumes] = useState([])
const [loading, setLoading] = useState(false)
const createResume = useCallback(async (resumeData) => {
setLoading(true)
try {
const response = await apiClient.post('/resumes', {
title: resumeData.title,
content: resumeData.content,
templateId: resumeData.templateId,
personalInfo: resumeData.personalInfo,
sections: resumeData.sections
})
const newResume = response.data.resume
setResumes((prev) => [newResume, ...prev])
return { success: true, resume: newResume }
} catch (error) {
return {
success: false,
error: error.response?.data?.error || '创建简历失败'
}
} finally {
setLoading(false)
}
}, [])
const fetchResumes = useCallback(async () => {
setLoading(true)
try {
const response = await apiClient.get('/resumes')
setResumes(response.data.resumes)
} catch (error) {
console.error('获取简历列表失败:', error)
} finally {
setLoading(false)
}
}, [])
return {
resumes,
loading,
createResume,
fetchResumes
}
}
// React 组件使用
function ResumeBuilder() {
const { createResume, loading } = useResumes()
const [resumeData, setResumeData] = useState({
title: '',
templateId: 'modern-template',
personalInfo: {
name: '',
email: '',
phone: '',
location: ''
},
sections: {
summary: { visible: true, content: '' },
experience: { visible: true, items: [] },
education: { visible: true, items: [] },
skills: { visible: true, items: [] }
}
})
const handleSubmit = async () => {
const result = await createResume(resumeData)
if (result.success) {
toast.success('简历创建成功')
router.push(`/resumes/${result.resume.id}`)
} else {
toast.error(result.error)
}
}
return (
<div className="resume-builder">
{/* 简历编辑表单 */}
<button onClick={handleSubmit} disabled={loading}>
{loading ? '创建中...' : '创建简历'}
</button>
</div>
)
}
简历导出
// services/export.js
export async function exportResumeToPDF(resumeId, options = {}) {
try {
const response = await apiClient.post(
`/resumes/${resumeId}/export/pdf`,
{
format: 'A4',
quality: 'high',
includeWatermark: false,
...options
},
{
responseType: 'blob' // 重要:指定响应类型为 blob
}
)
// 创建下载链接
const blob = new Blob([response.data], { type: 'application/pdf' })
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `resume-${resumeId}.pdf`
// 触发下载
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
// 清理内存
window.URL.revokeObjectURL(url)
return { success: true }
} catch (error) {
return {
success: false,
error: error.response?.data?.error || '导出失败'
}
}
}
// React Hook
export function useResumeExport() {
const [exporting, setExporting] = useState(false)
const exportPDF = useCallback(async (resumeId, options) => {
setExporting(true)
const result = await exportResumeToPDF(resumeId, options)
setExporting(false)
return result
}, [])
return { exportPDF, exporting }
}
🤖 AI 功能示例
ATS 兼容性分析
// services/ai.js
export async function analyzeATSCompatibility(resumeContent, jobDescription) {
try {
const response = await apiClient.post('/ai/analyze', {
type: 'ats_compatibility',
resumeContent,
jobDescription,
options: {
includeKeywords: true,
includeSuggestions: true,
detailLevel: 'comprehensive'
}
})
return {
success: true,
analysis: response.data.analysis
}
} catch (error) {
return {
success: false,
error: error.response?.data?.error || 'ATS 分析失败'
}
}
}
// React 组件
function ATSAnalyzer({ resumeId }) {
const [analysis, setAnalysis] = useState(null)
const [jobDescription, setJobDescription] = useState('')
const [analyzing, setAnalyzing] = useState(false)
const runAnalysis = async () => {
if (!jobDescription.trim()) {
toast.error('请输入职位描述')
return
}
setAnalyzing(true)
try {
// 先获取简历内容
const resumeResponse = await apiClient.get(`/resumes/${resumeId}`)
const resumeContent = resumeResponse.data.resume.content
// 进行 ATS 分析
const result = await analyzeATSCompatibility(resumeContent, jobDescription)
if (result.success) {
setAnalysis(result.analysis)
} else {
toast.error(result.error)
}
} finally {
setAnalyzing(false)
}
}
return (
<div className="ats-analyzer">
<textarea
value={jobDescription}
onChange={(e) => setJobDescription(e.target.value)}
placeholder="粘贴职位描述..."
className="h-32 w-full rounded border p-3"
/>
<button
onClick={runAnalysis}
disabled={analyzing}
className="mt-4 rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
>
{analyzing ? '分析中...' : '开始 ATS 分析'}
</button>
{analysis && (
<div className="mt-6 space-y-4">
<div className="rounded bg-green-50 p-4">
<h3 className="font-semibold text-green-800">ATS 兼容性评分: {analysis.score}/100</h3>
</div>
<div className="rounded bg-blue-50 p-4">
<h4 className="font-semibold text-blue-800">关键词匹配</h4>
<div className="mt-2 flex flex-wrap gap-2">
{analysis.keywords.matched.map((keyword, index) => (
<span key={index} className="rounded bg-green-200 px-2 py-1 text-sm text-green-800">
{keyword}
</span>
))}
</div>
</div>
<div className="rounded bg-yellow-50 p-4">
<h4 className="font-semibold text-yellow-800">优化建议</h4>
<ul className="mt-2 space-y-1">
{analysis.suggestions.map((suggestion, index) => (
<li key={index} className="text-yellow-700">
{suggestion}
</li>
))}
</ul>
</div>
</div>
)}
</div>
)
}
📊 完整应用示例
简历管理仪表板
// components/ResumeDashboard.jsx
import { useState, useEffect } from 'react'
import { useSession } from 'next-auth/react'
function ResumeDashboard() {
const { data: session } = useSession()
const [resumes, setResumes] = useState([])
const [loading, setLoading] = useState(true)
const [stats, setStats] = useState({
totalResumes: 0,
totalViews: 0,
totalDownloads: 0
})
useEffect(() => {
if (session) {
fetchDashboardData()
}
}, [session])
const fetchDashboardData = async () => {
try {
const [resumesRes, statsRes] = await Promise.all([apiClient.get('/resumes'), apiClient.get('/dashboard')])
setResumes(resumesRes.data.resumes)
setStats(statsRes.data.stats)
} catch (error) {
console.error('获取仪表板数据失败:', error)
} finally {
setLoading(false)
}
}
const deleteResume = async (resumeId) => {
if (!confirm('确定要删除这份简历吗?')) return
try {
await apiClient.delete(`/resumes/${resumeId}`)
setResumes((prev) => prev.filter((r) => r.id !== resumeId))
toast.success('简历删除成功')
} catch (error) {
toast.error('删除失败')
}
}
const duplicateResume = async (resumeId) => {
try {
const response = await apiClient.post(`/resumes/${resumeId}/duplicate`)
const newResume = response.data.resume
setResumes((prev) => [newResume, ...prev])
toast.success('简历复制成功')
} catch (error) {
toast.error('复制失败')
}
}
if (loading) {
return <div className="flex justify-center p-8">加载中...</div>
}
return (
<div className="dashboard">
{/* 统计卡片 */}
<div className="mb-8 grid grid-cols-1 gap-6 md:grid-cols-3">
<StatCard title="简历总数" value={stats.totalResumes} icon="📄" />
<StatCard title="总浏览量" value={stats.totalViews} icon="👁️" />
<StatCard title="总下载量" value={stats.totalDownloads} icon="⬇️" />
</div>
{/* 简历列表 */}
<div className="rounded-lg bg-white shadow">
<div className="border-b p-6">
<h2 className="text-xl font-semibold">我的简历</h2>
</div>
<div className="divide-y">
{resumes.map((resume) => (
<ResumeItem key={resume.id} resume={resume} onDelete={deleteResume} onDuplicate={duplicateResume} />
))}
</div>
</div>
</div>
)
}
function StatCard({ title, value, icon }) {
return (
<div className="rounded-lg bg-white p-6 shadow">
<div className="flex items-center">
<div className="mr-3 text-2xl">{icon}</div>
<div>
<p className="text-sm text-gray-600">{title}</p>
<p className="text-2xl font-bold">{value}</p>
</div>
</div>
</div>
)
}
function ResumeItem({ resume, onDelete, onDuplicate }) {
const { exportPDF, exporting } = useResumeExport()
return (
<div className="flex items-center justify-between p-6">
<div className="flex-1">
<h3 className="font-semibold">{resume.title}</h3>
<p className="text-sm text-gray-600">更新于 {new Date(resume.updatedAt).toLocaleDateString()}</p>
</div>
<div className="flex space-x-2">
<button
onClick={() => exportPDF(resume.id)}
disabled={exporting}
className="rounded bg-blue-600 px-3 py-1 text-sm text-white hover:bg-blue-700"
>
{exporting ? '导出中...' : '导出PDF'}
</button>
<button
onClick={() => onDuplicate(resume.id)}
className="rounded bg-gray-600 px-3 py-1 text-sm text-white hover:bg-gray-700"
>
复制
</button>
<button
onClick={() => onDelete(resume.id)}
className="rounded bg-red-600 px-3 py-1 text-sm text-white hover:bg-red-700"
>
删除
</button>
</div>
</div>
)
}
export default ResumeDashboard
🔗 相关文档
- API 概览 - API 基础介绍和认证
- 认证系统 - 详细的认证文档
- 简历管理 - 简历相关 API
- 用户管理 - 用户账户 API
- AI 服务 - AI 功能 API
- Webhooks - Webhook 集成
📝 注意事项
最佳实践
- 错误处理:始终处理 API 错误并提供用户友好的反馈
- 加载状态:为异步操作提供适当的加载指示器
- 数据验证:在客户端和服务端都进行数据验证
- 缓存策略:合理使用缓存减少不必要的 API 调用
- 安全性:永远不要在客户端存储敏感信息
性能优化
- 使用 React.memo 和 useMemo 优化组件渲染
- 实现虚拟滚动处理大量数据
- 使用 SWR 或 React Query 进行数据管理
- 合并多个 API 调用减少网络请求
调试技巧
- 使用浏览器开发者工具监控网络请求
- 设置适当的日志记录
- 使用 React DevTools 检查组件状态
- 实现错误边界处理意外错误