新增多证书模板管理流程
This commit is contained in:
25
backend/app/api/routes/admin_certificate_templates.py
Normal file
25
backend/app/api/routes/admin_certificate_templates.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from app.api.deps import require_roles
|
||||
from app.models import AdminUser
|
||||
from app.schemas.certificate_template import CertificateTemplateOut
|
||||
from app.services.certificate_templates import get_certificate_template, list_certificate_templates
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("", response_model=list[CertificateTemplateOut])
|
||||
def list_templates(
|
||||
_: AdminUser = Depends(require_roles("system_admin", "certificate_admin", "readonly")),
|
||||
) -> list[dict[str, object]]:
|
||||
return [template.to_dict() for template in list_certificate_templates()]
|
||||
|
||||
|
||||
@router.get("/{template_code}/preview")
|
||||
def preview_template(template_code: str) -> FileResponse:
|
||||
try:
|
||||
template = get_certificate_template(template_code)
|
||||
except (ValueError, FileNotFoundError) as exc:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||
return FileResponse(template.asset_path, media_type="image/png")
|
||||
@@ -13,6 +13,7 @@ from app.services.logs import log_action
|
||||
from app.services.pdf import PdfGenerationBusy, render_certificate_pdf
|
||||
from app.services.pdf_pregeneration import pdf_pregeneration_manager
|
||||
from app.services.system_settings import get_pdf_generation_concurrency_limit
|
||||
from app.services.certificate_templates import get_certificate_template
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -73,6 +74,11 @@ def create_certificate(
|
||||
project = db.query(ProjectCourse).filter(ProjectCourse.code == payload.project_code, ProjectCourse.status == "active").first()
|
||||
if not project:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Project code is inactive or missing")
|
||||
template_code = payload.template_code or project.default_template_code
|
||||
try:
|
||||
template_code = get_certificate_template(template_code).code
|
||||
except (ValueError, FileNotFoundError) as exc:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
||||
|
||||
certificate = Certificate(
|
||||
learner_id=payload.learner_id,
|
||||
@@ -86,6 +92,7 @@ def create_certificate(
|
||||
course_end_date=payload.course_end_date,
|
||||
issue_date=payload.issue_date,
|
||||
issuer_name=project.default_issuer_name,
|
||||
template_code=template_code,
|
||||
remark=payload.remark,
|
||||
)
|
||||
db.add(certificate)
|
||||
@@ -172,6 +179,7 @@ def _certificate_payload(certificate: Certificate, learner: Learner | None = Non
|
||||
"course_end_date": certificate.course_end_date,
|
||||
"issue_date": certificate.issue_date,
|
||||
"issuer_name": certificate.issuer_name,
|
||||
"template_code": certificate.template_code,
|
||||
"status": certificate.status,
|
||||
"pdf_status": certificate.pdf_status,
|
||||
"created_at": certificate.created_at,
|
||||
@@ -242,6 +250,7 @@ def preview_certificate(
|
||||
"course_end_date": certificate.course_end_date.isoformat() if certificate.course_end_date else None,
|
||||
"issue_date": certificate.issue_date.isoformat(),
|
||||
"issuer_name": certificate.issuer_name,
|
||||
"template_code": certificate.template_code,
|
||||
"status": certificate.status,
|
||||
"public_url": f"/cert/{public_token.token_value}" if public_token else "",
|
||||
"verify_url": f"/verify/{qr_token.token_value}" if qr_token else "",
|
||||
|
||||
@@ -41,6 +41,7 @@ def export_certificates(
|
||||
"证书编号",
|
||||
"证书对外直达链接",
|
||||
"项目代码",
|
||||
"证书模板",
|
||||
"证书名称",
|
||||
"课程名称",
|
||||
"阶段名称",
|
||||
@@ -59,6 +60,7 @@ def export_certificates(
|
||||
certificate.certificate_no,
|
||||
f"{settings.public_base_url}/cert/{token.token_value}",
|
||||
certificate.project_code,
|
||||
certificate.template_code,
|
||||
certificate.certificate_name,
|
||||
certificate.course_name,
|
||||
certificate.stage_name,
|
||||
|
||||
@@ -3,7 +3,7 @@ import shutil
|
||||
from datetime import date, datetime
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
|
||||
from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile, status
|
||||
from fastapi.responses import FileResponse, StreamingResponse
|
||||
from openpyxl import Workbook, load_workbook
|
||||
from openpyxl.comments import Comment
|
||||
@@ -28,6 +28,7 @@ from app.models import (
|
||||
from app.schemas.import_batch import ImportBatchOut
|
||||
from app.services.certificate_number import build_certificate_no
|
||||
from app.services.logs import log_action
|
||||
from app.services.certificate_templates import get_certificate_template
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -74,18 +75,23 @@ def build_import_template_workbook() -> Workbook:
|
||||
|
||||
@router.post("", response_model=ImportBatchOut, status_code=status.HTTP_201_CREATED)
|
||||
def upload_import_file(
|
||||
template_code: str = Form(...),
|
||||
file: UploadFile = File(...),
|
||||
db: Session = Depends(get_db),
|
||||
admin: AdminUser = Depends(require_roles("system_admin", "certificate_admin")),
|
||||
) -> ImportBatch:
|
||||
if not file.filename or not file.filename.lower().endswith(".xlsx"):
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Only .xlsx files are supported")
|
||||
try:
|
||||
template_code = get_certificate_template(template_code).code
|
||||
except (ValueError, FileNotFoundError) as exc:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
||||
|
||||
upload_path = data_path("uploads") / file.filename
|
||||
with upload_path.open("wb") as target:
|
||||
shutil.copyfileobj(file.file, target)
|
||||
|
||||
batch = ImportBatch(filename=file.filename, file_path=str(upload_path), created_by=admin.id)
|
||||
batch = ImportBatch(filename=file.filename, file_path=str(upload_path), template_code=template_code, created_by=admin.id)
|
||||
db.add(batch)
|
||||
db.flush()
|
||||
validate_batch(db, batch, upload_path)
|
||||
@@ -188,7 +194,15 @@ def confirm_import_batch(
|
||||
row.error_message = f"Project code is inactive or missing: {project_code}"
|
||||
failed_rows += 1
|
||||
continue
|
||||
duplicate = find_duplicate_certificate(db, learner.id, project, course_start_date, course_end_date, issue_date)
|
||||
duplicate = find_duplicate_certificate(
|
||||
db,
|
||||
learner.id,
|
||||
project,
|
||||
course_start_date,
|
||||
course_end_date,
|
||||
issue_date,
|
||||
batch.template_code,
|
||||
)
|
||||
if duplicate:
|
||||
row.status = "skipped"
|
||||
row.error_message = "\u5df2\u5b58\u5728\uff0c\u65e0\u9700\u5904\u7406"
|
||||
@@ -207,6 +221,7 @@ def confirm_import_batch(
|
||||
course_end_date=course_end_date,
|
||||
issue_date=issue_date,
|
||||
issuer_name=project.default_issuer_name,
|
||||
template_code=batch.template_code,
|
||||
remark=None,
|
||||
)
|
||||
db.add(certificate)
|
||||
@@ -326,6 +341,7 @@ def find_duplicate_certificate(
|
||||
course_start_date: date,
|
||||
course_end_date: date,
|
||||
issue_date: date,
|
||||
template_code: str,
|
||||
) -> Certificate | None:
|
||||
return (
|
||||
db.query(Certificate)
|
||||
@@ -337,6 +353,7 @@ def find_duplicate_certificate(
|
||||
.filter(Certificate.course_start_date == course_start_date)
|
||||
.filter(Certificate.course_end_date == course_end_date)
|
||||
.filter(Certificate.issue_date == issue_date)
|
||||
.filter(Certificate.template_code == template_code)
|
||||
.first()
|
||||
)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from app.db.session import get_db
|
||||
from app.models import AdminUser, ProjectCourse
|
||||
from app.schemas.project import ProjectCourseCreate, ProjectCourseOut, ProjectCourseUpdate
|
||||
from app.services.logs import diff_values, log_action
|
||||
from app.services.certificate_templates import get_certificate_template
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -24,6 +25,10 @@ def create_project(
|
||||
db: Session = Depends(get_db),
|
||||
admin: AdminUser = Depends(require_roles("system_admin", "certificate_admin")),
|
||||
) -> ProjectCourse:
|
||||
try:
|
||||
template_code = get_certificate_template(payload.default_template_code).code
|
||||
except (ValueError, FileNotFoundError) as exc:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
||||
if db.query(ProjectCourse).filter(ProjectCourse.code == payload.code).first():
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="项目代码已存在")
|
||||
project = ProjectCourse(
|
||||
@@ -33,6 +38,7 @@ def create_project(
|
||||
default_course_name=payload.default_course_name,
|
||||
default_stage_name=payload.default_stage_name,
|
||||
default_issuer_name=payload.default_issuer_name,
|
||||
default_template_code=template_code,
|
||||
)
|
||||
db.add(project)
|
||||
log_action(db, admin, "create_project", "project_course", detail={"code": payload.code, "name": payload.name, "status": project.status})
|
||||
@@ -56,6 +62,7 @@ def update_project(
|
||||
"default_course_name": project.default_course_name,
|
||||
"default_stage_name": project.default_stage_name,
|
||||
"default_issuer_name": project.default_issuer_name,
|
||||
"default_template_code": project.default_template_code,
|
||||
"status": project.status,
|
||||
}
|
||||
if payload.name is not None:
|
||||
@@ -67,6 +74,12 @@ def update_project(
|
||||
project.default_stage_name = payload.default_stage_name
|
||||
if payload.default_issuer_name is not None:
|
||||
project.default_issuer_name = payload.default_issuer_name
|
||||
if payload.default_template_code is not None:
|
||||
try:
|
||||
template_code = get_certificate_template(payload.default_template_code).code
|
||||
except (ValueError, FileNotFoundError) as exc:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
||||
project.default_template_code = template_code
|
||||
if payload.status is not None:
|
||||
project.status = payload.status
|
||||
after = {
|
||||
@@ -74,6 +87,7 @@ def update_project(
|
||||
"default_course_name": project.default_course_name,
|
||||
"default_stage_name": project.default_stage_name,
|
||||
"default_issuer_name": project.default_issuer_name,
|
||||
"default_template_code": project.default_template_code,
|
||||
"status": project.status,
|
||||
}
|
||||
log_action(db, admin, "update_project", "project_course", project.id, {"code": project.code, "name": project.name, "changes": diff_values(before, after, list(after.keys()))})
|
||||
|
||||
@@ -180,6 +180,7 @@ def public_certificate_payload(db: Session, certificate: Certificate, learner: L
|
||||
"course_end_date": certificate.course_end_date.isoformat() if certificate.course_end_date else None,
|
||||
"issue_date": certificate.issue_date.isoformat(),
|
||||
"issuer_name": certificate.issuer_name,
|
||||
"template_code": certificate.template_code,
|
||||
"status": certificate.status,
|
||||
"pdf_status": certificate.pdf_status,
|
||||
"public_token": token_value,
|
||||
|
||||
Reference in New Issue
Block a user