新增多证书模板管理流程

This commit is contained in:
Certificate System
2026-08-13 18:06:05 +08:00
parent eac19ea593
commit abbefc4b48
27 changed files with 774 additions and 61 deletions

View File

@@ -8,6 +8,11 @@ from PIL import Image, ImageDraw, ImageFont
from app.core.config import settings
from app.core.paths import data_path
from app.models import Certificate, CertificateAccessToken, Learner, ProjectCourse
from app.services.certificate_templates import (
CLASSIC_TEMPLATE_CODE,
PRACTICE_CAMP_TEMPLATE_CODE,
get_certificate_template,
)
DESIGN_WIDTH = 1024
DESIGN_HEIGHT = 759
@@ -70,13 +75,8 @@ def mark_pdf_generated(certificate: Certificate, output_path: Path) -> None:
update_pdf_access_time(certificate)
def certificate_template_path() -> Path:
assets_dir = Path(__file__).resolve().parents[1] / "assets"
for name in ["certificate-template.png", "certificate-template.jpg"]:
path = assets_dir / name
if path.exists():
return path
raise FileNotFoundError("Certificate template image not found")
def certificate_template_path(template_code: str = CLASSIC_TEMPLATE_CODE) -> Path:
return get_certificate_template(template_code).asset_path
def pdf_resolution(image: Image.Image) -> float:
@@ -91,7 +91,8 @@ def render_certificate_pdf(
concurrency_limit: int = 2,
) -> Path:
output_path = pdf_cache_path(certificate.certificate_no)
template_path = certificate_template_path()
template_code = getattr(certificate, "template_code", CLASSIC_TEMPLATE_CODE)
template_path = certificate_template_path(template_code)
latest_renderer_mtime = max(template_path.stat().st_mtime, Path(__file__).stat().st_mtime)
if cached_pdf_is_fresh(output_path) and output_path.stat().st_mtime >= latest_renderer_mtime:
mark_pdf_generated(certificate, output_path)
@@ -112,7 +113,13 @@ def render_certificate_pdf(
def render_certificate_image(certificate: Certificate, learner: Learner, project: ProjectCourse | None) -> Image.Image:
template = certificate_template_path()
if getattr(certificate, "template_code", CLASSIC_TEMPLATE_CODE) == PRACTICE_CAMP_TEMPLATE_CODE:
return render_practice_camp_certificate_image(certificate, learner)
return render_classic_certificate_image(certificate, learner, project)
def render_classic_certificate_image(certificate: Certificate, learner: Learner, project: ProjectCourse | None) -> Image.Image:
template = certificate_template_path(CLASSIC_TEMPLATE_CODE)
image = Image.open(template).convert("RGB")
base_image = image.copy()
draw = ImageDraw.Draw(image)
@@ -174,6 +181,62 @@ def render_certificate_image(certificate: Certificate, learner: Learner, project
return image
def render_practice_camp_certificate_image(certificate: Certificate, learner: Learner) -> Image.Image:
design_width = 1854
design_height = 1359
image = Image.open(certificate_template_path(PRACTICE_CAMP_TEMPLATE_CODE)).convert("RGB")
draw = ImageDraw.Draw(image)
x_scale = image.width / design_width
y_scale = image.height / design_height
text_scale = (x_scale + y_scale) / 2
start_year, start_month, _ = date_parts(certificate.course_start_date or certificate.issue_date)
end_year, end_month, _ = date_parts(certificate.course_end_date or certificate.issue_date)
issue_year, issue_month, issue_day = date_parts(certificate.issue_date)
draw.text(
(568 * x_scale, 542 * y_scale),
learner.current_name,
fill="#202020",
font=font(42, "kai", True, text_scale),
anchor="ms",
)
for value, x in [
(start_year, 474),
(start_month, 630),
(end_year, 888),
(end_month, 1058),
]:
draw.text(
(x * x_scale, 668 * y_scale),
value,
fill="#202020",
font=font(41, "song", False, text_scale),
anchor="ms",
)
issue_date_text = f"{issue_year}{issue_month}{issue_day}"
issue_date_font = font(36, "song", False, text_scale)
issue_date_position = (568 * x_scale, 1158 * y_scale)
draw.rectangle(
(
442 * x_scale,
1118 * y_scale,
696 * x_scale,
1168 * y_scale,
),
fill="#ffffff",
)
draw.text(
issue_date_position,
issue_date_text,
fill="#202020",
font=issue_date_font,
anchor="ms",
)
return image
def font(size: int, kind: str = "song", bold: bool = False, scale: float = 1.0) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
choices = {
"kai": [