1. 奖章条件优化 - 修复统计查询 SQL 语法 - 添加按类别检查奖章方法 2. 移动端适配 - 登录页、课程中心、课程详情 - 考试页面、成长路径、排行榜 3. 证书系统 - 数据库模型和迁移脚本 - 证书颁发/列表/下载/验证 API - 前端证书列表页面 4. 数据大屏 - 企业级/团队级数据 API - ECharts 可视化大屏页面
This commit is contained in:
@@ -110,5 +110,11 @@ api_router.include_router(system_settings_router, prefix="/settings", tags=["sys
|
||||
# level_router 等级与奖章路由
|
||||
from .endpoints.level import router as level_router
|
||||
api_router.include_router(level_router, prefix="/level", tags=["level"])
|
||||
# certificate_router 证书管理路由
|
||||
from .endpoints.certificate import router as certificate_router
|
||||
api_router.include_router(certificate_router, prefix="/certificates", tags=["certificates"])
|
||||
# dashboard_router 数据大屏路由
|
||||
from .endpoints.dashboard import router as dashboard_router
|
||||
api_router.include_router(dashboard_router, prefix="/dashboard", tags=["dashboard"])
|
||||
|
||||
__all__ = ["api_router"]
|
||||
|
||||
305
backend/app/api/v1/endpoints/certificate.py
Normal file
305
backend/app/api/v1/endpoints/certificate.py
Normal file
@@ -0,0 +1,305 @@
|
||||
"""
|
||||
证书管理 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.database import get_db
|
||||
from app.core.security import 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_pdf(
|
||||
cert_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""下载证书PDF"""
|
||||
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 cert.get("pdf_url"):
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"download_url": cert["pdf_url"]
|
||||
}
|
||||
}
|
||||
|
||||
# 否则返回图片作为替代
|
||||
try:
|
||||
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"attachment; filename=certificate_{cert['certificate_no']}.png"
|
||||
}
|
||||
)
|
||||
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)
|
||||
)
|
||||
231
backend/app/api/v1/endpoints/dashboard.py
Normal file
231
backend/app/api/v1/endpoints/dashboard.py
Normal file
@@ -0,0 +1,231 @@
|
||||
"""
|
||||
数据大屏 API 端点
|
||||
|
||||
提供企业级和团队级数据大屏接口
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.security import get_current_user
|
||||
from app.models.user import User
|
||||
from app.services.dashboard_service import DashboardService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/enterprise/overview")
|
||||
async def get_enterprise_overview(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
获取企业级数据概览
|
||||
|
||||
需要管理员或企业管理员权限
|
||||
"""
|
||||
if current_user.role not in ["admin", "enterprise_admin", "manager"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="需要管理员权限"
|
||||
)
|
||||
|
||||
service = DashboardService(db)
|
||||
data = await service.get_enterprise_overview()
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": data
|
||||
}
|
||||
|
||||
|
||||
@router.get("/enterprise/departments")
|
||||
async def get_department_comparison(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
获取部门/团队学习对比数据
|
||||
"""
|
||||
if current_user.role not in ["admin", "enterprise_admin", "manager"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="需要管理员权限"
|
||||
)
|
||||
|
||||
service = DashboardService(db)
|
||||
data = await service.get_department_comparison()
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": data
|
||||
}
|
||||
|
||||
|
||||
@router.get("/enterprise/trend")
|
||||
async def get_learning_trend(
|
||||
days: int = Query(7, ge=1, le=30),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
获取学习趋势数据
|
||||
"""
|
||||
if current_user.role not in ["admin", "enterprise_admin", "manager"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="需要管理员权限"
|
||||
)
|
||||
|
||||
service = DashboardService(db)
|
||||
data = await service.get_learning_trend(days)
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": data
|
||||
}
|
||||
|
||||
|
||||
@router.get("/enterprise/level-distribution")
|
||||
async def get_level_distribution(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
获取等级分布数据
|
||||
"""
|
||||
if current_user.role not in ["admin", "enterprise_admin", "manager"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="需要管理员权限"
|
||||
)
|
||||
|
||||
service = DashboardService(db)
|
||||
data = await service.get_level_distribution()
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": data
|
||||
}
|
||||
|
||||
|
||||
@router.get("/enterprise/activities")
|
||||
async def get_realtime_activities(
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
获取实时动态
|
||||
"""
|
||||
if current_user.role not in ["admin", "enterprise_admin", "manager"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="需要管理员权限"
|
||||
)
|
||||
|
||||
service = DashboardService(db)
|
||||
data = await service.get_realtime_activities(limit)
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": data
|
||||
}
|
||||
|
||||
|
||||
@router.get("/enterprise/course-ranking")
|
||||
async def get_course_ranking(
|
||||
limit: int = Query(10, ge=1, le=50),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
获取课程热度排行
|
||||
"""
|
||||
if current_user.role not in ["admin", "enterprise_admin", "manager"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="需要管理员权限"
|
||||
)
|
||||
|
||||
service = DashboardService(db)
|
||||
data = await service.get_course_ranking(limit)
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": data
|
||||
}
|
||||
|
||||
|
||||
@router.get("/team")
|
||||
async def get_team_dashboard(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
获取团队级数据大屏
|
||||
|
||||
面向团队负责人,显示其管理团队的数据
|
||||
"""
|
||||
if current_user.role not in ["admin", "enterprise_admin", "manager"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="需要团队负责人权限"
|
||||
)
|
||||
|
||||
service = DashboardService(db)
|
||||
data = await service.get_team_dashboard(current_user.id)
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": data
|
||||
}
|
||||
|
||||
|
||||
@router.get("/all")
|
||||
async def get_all_dashboard_data(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
获取完整的大屏数据(一次性获取所有数据)
|
||||
|
||||
用于大屏初始化加载
|
||||
"""
|
||||
if current_user.role not in ["admin", "enterprise_admin", "manager"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="需要管理员权限"
|
||||
)
|
||||
|
||||
service = DashboardService(db)
|
||||
|
||||
# 并行获取所有数据
|
||||
overview = await service.get_enterprise_overview()
|
||||
departments = await service.get_department_comparison()
|
||||
trend = await service.get_learning_trend(7)
|
||||
level_dist = await service.get_level_distribution()
|
||||
activities = await service.get_realtime_activities(20)
|
||||
course_ranking = await service.get_course_ranking(10)
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"overview": overview,
|
||||
"departments": departments,
|
||||
"trend": trend,
|
||||
"level_distribution": level_dist,
|
||||
"activities": activities,
|
||||
"course_ranking": course_ranking,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user