文档

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

🔗 相关文档

📝 注意事项

最佳实践

  1. 错误处理:始终处理 API 错误并提供用户友好的反馈
  2. 加载状态:为异步操作提供适当的加载指示器
  3. 数据验证:在客户端和服务端都进行数据验证
  4. 缓存策略:合理使用缓存减少不必要的 API 调用
  5. 安全性:永远不要在客户端存储敏感信息

性能优化

  • 使用 React.memo 和 useMemo 优化组件渲染
  • 实现虚拟滚动处理大量数据
  • 使用 SWR 或 React Query 进行数据管理
  • 合并多个 API 调用减少网络请求

调试技巧

  • 使用浏览器开发者工具监控网络请求
  • 设置适当的日志记录
  • 使用 React DevTools 检查组件状态
  • 实现错误边界处理意外错误