Files
012-kaopeilian/backend/app/api/v1/endpoints/certificate.py
yuliang_guo 64f5d567fa
Some checks failed
continuous-integration/drone/push Build is failing
feat: 实现 KPL 系统功能改进计划
1. 课程学习进度追踪
   - 新增 UserCourseProgress 和 UserMaterialProgress 模型
   - 新增 /api/v1/progress/* 进度追踪 API
   - 更新 admin.py 使用真实课程完成率数据

2. 路由权限检查完善
   - 新增前端 permissionChecker.ts 权限检查工具
   - 更新 router/guard.ts 实现团队和课程权限验证
   - 新增后端 permission_service.py

3. AI 陪练音频转文本
   - 新增 speech_recognition.py 语音识别服务
   - 新增 /api/v1/speech/* API
   - 更新 ai-practice-coze.vue 支持语音输入

4. 双人对练报告生成
   - 更新 practice_room_service.py 添加报告生成功能
   - 新增 /rooms/{room_code}/report API
   - 更新 duo-practice-report.vue 调用真实 API

5. 学习提醒推送
   - 新增 notification_service.py 通知服务
   - 新增 scheduler_service.py 定时任务服务
   - 支持钉钉、企微、站内消息推送

6. 智能学习推荐
   - 新增 recommendation_service.py 推荐服务
   - 新增 /api/v1/recommendations/* API
   - 支持错题、能力、进度、热门多维度推荐

7. 安全问题修复
   - DEBUG 默认值改为 False
   - 添加 SECRET_KEY 安全警告
   - 新增 check_security_settings() 检查函数

8. 证书 PDF 生成
   - 更新 certificate_service.py 添加 PDF 生成
   - 添加 weasyprint、Pillow、qrcode 依赖
   - 更新下载 API 支持 PDF 和 PNG 格式
2026-01-30 14:22:35 +08:00

330 lines
8.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
证书管理 API 端点
提供证书相关的 RESTful API
- 获取证书列表
- 获取证书详情
- 下载证书
- 验证证书
"""
from typing import Optional, List
from fastapi import APIRouter, Depends, HTTPException, status, Response, Query
from fastapi.responses import StreamingResponse
from sqlalchemy.ext.asyncio import AsyncSession
import io
from app.core.deps import get_db, get_current_user
from app.models.user import User
from app.services.certificate_service import CertificateService
router = APIRouter()
@router.get("/templates")
async def get_certificate_templates(
cert_type: Optional[str] = Query(None, description="证书类型: course/exam/achievement"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""获取证书模板列表"""
service = CertificateService(db)
templates = await service.get_templates(cert_type)
return {
"code": 200,
"message": "success",
"data": templates
}
@router.get("/me")
async def get_my_certificates(
cert_type: Optional[str] = Query(None, description="证书类型过滤"),
offset: int = Query(0, ge=0),
limit: int = Query(20, ge=1, le=100),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""获取当前用户的证书列表"""
service = CertificateService(db)
result = await service.get_user_certificates(
user_id=current_user.id,
cert_type=cert_type,
offset=offset,
limit=limit
)
return {
"code": 200,
"message": "success",
"data": result
}
@router.get("/user/{user_id}")
async def get_user_certificates(
user_id: int,
cert_type: Optional[str] = Query(None),
offset: int = Query(0, ge=0),
limit: int = Query(20, ge=1, le=100),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""获取指定用户的证书列表(需要管理员权限)"""
# 只允许查看自己的证书或管理员查看
if current_user.id != user_id and current_user.role not in ["admin", "enterprise_admin"]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="无权查看其他用户的证书"
)
service = CertificateService(db)
result = await service.get_user_certificates(
user_id=user_id,
cert_type=cert_type,
offset=offset,
limit=limit
)
return {
"code": 200,
"message": "success",
"data": result
}
@router.get("/{cert_id}")
async def get_certificate_detail(
cert_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""获取证书详情"""
service = CertificateService(db)
cert = await service.get_certificate_by_id(cert_id)
if not cert:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="证书不存在"
)
return {
"code": 200,
"message": "success",
"data": cert
}
@router.get("/{cert_id}/image")
async def get_certificate_image(
cert_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""获取证书分享图片"""
service = CertificateService(db)
try:
# 获取基础URL
base_url = "https://kpl.example.com/certificates" # 可从配置读取
image_bytes = await service.generate_certificate_image(cert_id, base_url)
return StreamingResponse(
io.BytesIO(image_bytes),
media_type="image/png",
headers={
"Content-Disposition": f"inline; filename=certificate_{cert_id}.png"
}
)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=str(e)
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"生成证书图片失败: {str(e)}"
)
@router.get("/{cert_id}/download")
async def download_certificate(
cert_id: int,
format: str = Query("pdf", description="下载格式: pdf 或 png"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
下载证书
支持 PDF 和 PNG 两种格式
- PDF: 高质量打印版本(需要安装 weasyprint
- PNG: 图片版本
"""
service = CertificateService(db)
cert = await service.get_certificate_by_id(cert_id)
if not cert:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="证书不存在"
)
# 如果已有缓存的 PDF/图片 URL 则返回
if format.lower() == "pdf" and cert.get("pdf_url"):
return {
"code": 200,
"message": "success",
"data": {
"download_url": cert["pdf_url"]
}
}
if format.lower() == "png" and cert.get("image_url"):
return {
"code": 200,
"message": "success",
"data": {
"download_url": cert["image_url"]
}
}
# 动态生成证书文件
try:
from app.core.config import settings
base_url = settings.PUBLIC_DOMAIN + "/certificates"
content, filename, mime_type = await service.download_certificate(
cert_id, format, base_url
)
return StreamingResponse(
io.BytesIO(content),
media_type=mime_type,
headers={
"Content-Disposition": f"attachment; filename={filename}"
}
)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"下载失败: {str(e)}"
)
@router.get("/verify/{cert_no}")
async def verify_certificate(
cert_no: str,
db: AsyncSession = Depends(get_db)
):
"""
验证证书真伪
此接口无需登录,可用于公开验证证书
"""
service = CertificateService(db)
cert = await service.get_certificate_by_no(cert_no)
if not cert:
return {
"code": 404,
"message": "证书不存在或编号错误",
"data": {
"valid": False,
"certificate_no": cert_no
}
}
return {
"code": 200,
"message": "证书验证通过",
"data": {
"valid": True,
"certificate_no": cert_no,
"title": cert.get("title"),
"type_name": cert.get("type_name"),
"issued_at": cert.get("issued_at"),
"user": cert.get("user", {}),
}
}
@router.post("/issue/course")
async def issue_course_certificate(
course_id: int,
course_name: str,
completion_rate: float = 100.0,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
颁发课程结业证书
通常由系统在用户完成课程时自动调用
"""
service = CertificateService(db)
try:
cert = await service.issue_course_certificate(
user_id=current_user.id,
course_id=course_id,
course_name=course_name,
completion_rate=completion_rate,
user_name=current_user.full_name or current_user.username
)
await db.commit()
return {
"code": 200,
"message": "证书颁发成功",
"data": cert
}
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
@router.post("/issue/exam")
async def issue_exam_certificate(
exam_id: int,
exam_name: str,
score: float,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
颁发考试合格证书
通常由系统在用户考试通过时自动调用
"""
service = CertificateService(db)
try:
cert = await service.issue_exam_certificate(
user_id=current_user.id,
exam_id=exam_id,
exam_name=exam_name,
score=score,
user_name=current_user.full_name or current_user.username
)
await db.commit()
return {
"code": 200,
"message": "证书颁发成功",
"data": cert
}
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)