Initial certificate system

This commit is contained in:
nelso
2026-06-06 19:32:24 +08:00
commit ce4ed6213b
105 changed files with 9133 additions and 0 deletions

View File

@@ -0,0 +1,84 @@
from fastapi import APIRouter, Depends, Query
from fastapi.responses import StreamingResponse
from openpyxl import Workbook
from sqlalchemy.orm import Session
from app.api.deps import require_roles
from app.core.config import settings
from app.core.paths import data_path
from app.db.session import get_db
from app.models import AdminUser, Certificate, CertificateAccessToken, Learner
from app.services.logs import log_action
router = APIRouter()
@router.get("/certificates")
def export_certificates(
project_code: str | None = Query(default=None),
status_value: str | None = Query(default=None, alias="status"),
db: Session = Depends(get_db),
admin: AdminUser = Depends(require_roles("system_admin", "certificate_admin")),
) -> StreamingResponse:
query = (
db.query(Certificate, Learner, CertificateAccessToken)
.join(Learner, Learner.id == Certificate.learner_id)
.join(CertificateAccessToken, CertificateAccessToken.id == Certificate.public_token_id)
.order_by(Certificate.id.desc())
)
if project_code:
query = query.filter(Certificate.project_code == project_code.strip().upper())
if status_value:
query = query.filter(Certificate.status == status_value)
workbook = Workbook()
sheet = workbook.active
sheet.title = "证书导出"
sheet.append(
[
"姓名",
"手机号",
"证书编号",
"证书对外直达链接",
"项目代码",
"证书名称",
"课程名称",
"阶段名称",
"发证日期",
"证书状态",
"PDF状态",
]
)
for certificate, learner, token in query.limit(50_000).all():
sheet.append(
[
learner.current_name,
_mask_phone(learner.phone),
certificate.certificate_no,
f"{settings.public_base_url}/cert/{token.token_value}",
certificate.project_code,
certificate.certificate_name,
certificate.course_name,
certificate.stage_name,
certificate.issue_date.isoformat(),
certificate.status,
certificate.pdf_status,
]
)
export_path = data_path("exports") / "certificates-export.xlsx"
workbook.save(export_path)
log_action(db, admin, "export_certificates", "certificate", detail={"project_code": project_code, "status": status_value})
db.commit()
file_handle = export_path.open("rb")
return StreamingResponse(
file_handle,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": 'attachment; filename="certificates-export.xlsx"'},
)
def _mask_phone(phone: str) -> str:
if len(phone) < 7:
return phone
return f"{phone[:3]}****{phone[-4:]}"