新增PDF生成并发设置
This commit is contained in:
@@ -23,6 +23,7 @@ ACTION_LABELS = {
|
||||
"export_certificates": "\u5bfc\u51fa\u8bc1\u4e66",
|
||||
"create_database_backup": "\u521b\u5efa\u6570\u636e\u5e93\u5907\u4efd",
|
||||
"restore_database_backup": "\u56de\u6eda\u6570\u636e\u5e93\u5907\u4efd",
|
||||
"update_pdf_settings": "\u4fee\u6539PDF\u751f\u6210\u8bbe\u7f6e",
|
||||
"public_search_certificate": "\u516c\u5f00\u67e5\u8be2\u8bc1\u4e66",
|
||||
"public_download_certificate": "\u516c\u5f00\u4e0b\u8f7d\u8bc1\u4e66",
|
||||
}
|
||||
@@ -33,6 +34,7 @@ OBJECT_LABELS = {
|
||||
"certificate": "\u8bc1\u4e66",
|
||||
"import_batch": "\u5bfc\u5165\u6279\u6b21",
|
||||
"database_backup": "\u6570\u636e\u5e93\u5907\u4efd",
|
||||
"system_setting": "\u7cfb\u7edf\u8bbe\u7f6e",
|
||||
"public_access": "\u516c\u5f00\u8bbf\u95ee",
|
||||
}
|
||||
HIGH_RISK_ACTIONS = {
|
||||
@@ -48,6 +50,7 @@ MEDIUM_RISK_ACTIONS = {
|
||||
"confirm_import_batch",
|
||||
"create_certificate",
|
||||
"create_database_backup",
|
||||
"update_pdf_settings",
|
||||
}
|
||||
|
||||
|
||||
@@ -116,6 +119,8 @@ def log_summary(action: str, object_type: str, object_id: object, detail: dict[s
|
||||
filename = detail.get("filename") or object_id
|
||||
size_label = detail.get("size_label")
|
||||
return f"{label}\uff1a{filename}" + (f"\uff08{size_label}\uff09" if size_label else "")
|
||||
if action == "update_pdf_settings":
|
||||
return f"{label}\uff1a\u5e76\u53d1\u6570 {detail.get('before')} -> {detail.get('after')}"
|
||||
if action == "public_search_certificate":
|
||||
mode = "\u8bc1\u4e66\u7f16\u53f7" if detail.get("mode") == "certificate_no" else "\u624b\u673a\u53f7"
|
||||
return f"{label}\uff1a{detail.get('name') or '-'} \u7528{mode}\u67e5\u8be2\uff0c\u5339\u914d {detail.get('matched_count', 0)} \u6761"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import threading
|
||||
from datetime import date, datetime, timedelta
|
||||
from pathlib import Path
|
||||
from time import monotonic
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
@@ -10,6 +12,35 @@ from app.models import Certificate, CertificateAccessToken, Learner, ProjectCour
|
||||
DESIGN_WIDTH = 1024
|
||||
DESIGN_HEIGHT = 759
|
||||
PDF_BASE_RESOLUTION = 150.0
|
||||
PDF_GENERATION_WAIT_TIMEOUT_SECONDS = 120
|
||||
|
||||
|
||||
class PdfGenerationBusy(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class PdfGenerationLimiter:
|
||||
def __init__(self) -> None:
|
||||
self._condition = threading.Condition()
|
||||
self._active = 0
|
||||
|
||||
def acquire(self, limit: int, timeout_seconds: int = PDF_GENERATION_WAIT_TIMEOUT_SECONDS) -> None:
|
||||
deadline = monotonic() + timeout_seconds
|
||||
with self._condition:
|
||||
while self._active >= limit:
|
||||
remaining = deadline - monotonic()
|
||||
if remaining <= 0:
|
||||
raise PdfGenerationBusy("当前PDF生成任务较多,请稍后重试")
|
||||
self._condition.wait(remaining)
|
||||
self._active += 1
|
||||
|
||||
def release(self) -> None:
|
||||
with self._condition:
|
||||
self._active = max(0, self._active - 1)
|
||||
self._condition.notify_all()
|
||||
|
||||
|
||||
pdf_generation_limiter = PdfGenerationLimiter()
|
||||
|
||||
|
||||
def pdf_cache_path(certificate_no: str) -> Path:
|
||||
@@ -51,6 +82,7 @@ def render_certificate_pdf(
|
||||
learner: Learner,
|
||||
project: ProjectCourse | None,
|
||||
token: CertificateAccessToken,
|
||||
concurrency_limit: int = 2,
|
||||
) -> Path:
|
||||
output_path = pdf_cache_path(certificate.certificate_no)
|
||||
template_path = certificate_template_path()
|
||||
@@ -60,8 +92,15 @@ def render_certificate_pdf(
|
||||
update_pdf_access_time(certificate)
|
||||
return output_path
|
||||
|
||||
image = render_certificate_image(certificate, learner, project)
|
||||
image.save(output_path, "PDF", resolution=pdf_resolution(image))
|
||||
pdf_generation_limiter.acquire(max(1, concurrency_limit))
|
||||
try:
|
||||
if cached_pdf_is_fresh(output_path) and output_path.stat().st_mtime >= latest_renderer_mtime:
|
||||
update_pdf_access_time(certificate)
|
||||
return output_path
|
||||
image = render_certificate_image(certificate, learner, project)
|
||||
image.save(output_path, "PDF", resolution=pdf_resolution(image))
|
||||
finally:
|
||||
pdf_generation_limiter.release()
|
||||
|
||||
certificate.pdf_status = "generated"
|
||||
certificate.pdf_file_path = str(output_path)
|
||||
|
||||
64
backend/app/services/system_settings.py
Normal file
64
backend/app/services/system_settings.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models import SystemSetting
|
||||
|
||||
PDF_GENERATION_CONCURRENCY_KEY = "pdf_generation_concurrency_limit"
|
||||
DEFAULT_PDF_GENERATION_CONCURRENCY = 2
|
||||
MIN_PDF_GENERATION_CONCURRENCY = 1
|
||||
MAX_PDF_GENERATION_CONCURRENCY = 10
|
||||
|
||||
|
||||
def ensure_default_settings(db: Session) -> None:
|
||||
if not db.get(SystemSetting, PDF_GENERATION_CONCURRENCY_KEY):
|
||||
db.add(
|
||||
SystemSetting(
|
||||
key=PDF_GENERATION_CONCURRENCY_KEY,
|
||||
value=str(DEFAULT_PDF_GENERATION_CONCURRENCY),
|
||||
description="PDF证书同时生成数量限制",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_pdf_generation_concurrency_limit(db: Session) -> int:
|
||||
setting = db.get(SystemSetting, PDF_GENERATION_CONCURRENCY_KEY)
|
||||
return _coerce_limit(setting.value if setting else None)
|
||||
|
||||
|
||||
def get_pdf_settings(db: Session) -> dict[str, object]:
|
||||
ensure_default_settings(db)
|
||||
setting = db.get(SystemSetting, PDF_GENERATION_CONCURRENCY_KEY)
|
||||
return {
|
||||
"pdf_generation_concurrency_limit": get_pdf_generation_concurrency_limit(db),
|
||||
"updated_at": setting.updated_at if setting else None,
|
||||
}
|
||||
|
||||
|
||||
def update_pdf_generation_concurrency_limit(db: Session, value: int) -> dict[str, object]:
|
||||
limit = _coerce_limit(value)
|
||||
ensure_default_settings(db)
|
||||
setting = db.get(SystemSetting, PDF_GENERATION_CONCURRENCY_KEY)
|
||||
if setting is None:
|
||||
setting = SystemSetting(
|
||||
key=PDF_GENERATION_CONCURRENCY_KEY,
|
||||
value=str(limit),
|
||||
description="PDF证书同时生成数量限制",
|
||||
)
|
||||
db.add(setting)
|
||||
else:
|
||||
setting.value = str(limit)
|
||||
setting.updated_at = datetime.utcnow()
|
||||
db.flush()
|
||||
return {
|
||||
"pdf_generation_concurrency_limit": limit,
|
||||
"updated_at": setting.updated_at,
|
||||
}
|
||||
|
||||
|
||||
def _coerce_limit(value: object) -> int:
|
||||
try:
|
||||
limit = int(value)
|
||||
except (TypeError, ValueError):
|
||||
limit = DEFAULT_PDF_GENERATION_CONCURRENCY
|
||||
return max(MIN_PDF_GENERATION_CONCURRENCY, min(MAX_PDF_GENERATION_CONCURRENCY, limit))
|
||||
Reference in New Issue
Block a user