Files
certificate-system/backend/app/services/certificate_issuance.py
2026-08-14 11:37:09 +08:00

136 lines
5.2 KiB
Python

from dataclasses import dataclass
from datetime import date
from sqlalchemy.orm import Session
from app.core.security import generate_public_token, hash_token
from app.models import Certificate, CertificateAccessToken, Learner, ProjectCourse
from app.services.certificate_number import build_certificate_no
from app.services.certificate_templates import certificate_field_keys, get_certificate_template
from app.services.learner_identity import resolve_learner
class DuplicateCertificate(ValueError):
def __init__(self, certificate: Certificate):
self.certificate = certificate
super().__init__(f"已存在相同证书:{certificate.certificate_no}")
@dataclass(frozen=True)
class CertificateIssueData:
learner_name: str
learner_phone: str
project_code: str
template_code: str
issue_date: date
course_start_date: date | None = None
course_end_date: date | None = None
course_name: str | None = None
stage_name: str | None = None
class_name: str | None = None
remark: str | None = None
import_batch_id: int | None = None
def issue_certificate(db: Session, data: CertificateIssueData, *, source: str) -> tuple[Certificate, Learner, bool]:
template = get_certificate_template(data.template_code)
project_code = data.project_code.strip().upper()
project = db.query(ProjectCourse).filter(ProjectCourse.code == project_code, ProjectCourse.status == "active").first()
if not project:
raise ValueError(f"项目代码不存在或已停用:{project_code}")
values = _template_values(data, project)
_validate_template_values(template, values)
learner, learner_created = resolve_learner(
db,
data.learner_name,
data.learner_phone,
source=source,
create_if_missing=True,
)
assert learner is not None
duplicate = find_duplicate_certificate(db, learner.id, project_code, template.code, values)
if duplicate:
raise DuplicateCertificate(duplicate)
certificate = Certificate(
learner_id=learner.id,
import_batch_id=data.import_batch_id,
project_code=project_code,
certificate_no="PENDING",
certificate_name=project.default_certificate_name or project.name,
class_name=data.class_name,
course_name=values["course_name"],
stage_name=values["stage_name"],
course_start_date=values["course_start_date"],
course_end_date=values["course_end_date"],
issue_date=data.issue_date,
template_code=template.code,
remark=data.remark,
)
db.add(certificate)
db.flush()
certificate.certificate_no = build_certificate_no(certificate.id, certificate.project_code, certificate.issue_date)
certificate.public_token_id = _create_access_token(db, certificate.id, "public_link")
certificate.qr_token_id = _create_access_token(db, certificate.id, "qr_verify")
return certificate, learner, learner_created
def find_duplicate_certificate(
db: Session,
learner_id: int,
project_code: str,
template_code: str,
values: dict[str, object],
) -> Certificate | None:
return (
db.query(Certificate)
.filter(Certificate.learner_id == learner_id)
.filter(Certificate.project_code == project_code)
.filter(Certificate.template_code == template_code)
.filter(Certificate.course_name == values["course_name"])
.filter(Certificate.stage_name == values["stage_name"])
.filter(Certificate.course_start_date == values["course_start_date"])
.filter(Certificate.course_end_date == values["course_end_date"])
.filter(Certificate.issue_date == values["issue_date"])
.filter(Certificate.status == "valid")
.first()
)
def _template_values(data: CertificateIssueData, project: ProjectCourse) -> dict[str, object]:
template = get_certificate_template(data.template_code)
keys = certificate_field_keys(template)
return {
"course_name": (data.course_name or project.default_course_name) if "course_name" in keys else None,
"stage_name": (data.stage_name or project.default_stage_name) if "stage_name" in keys else None,
"course_start_date": data.course_start_date if "course_start_date" in keys else None,
"course_end_date": data.course_end_date if "course_end_date" in keys else None,
"issue_date": data.issue_date,
}
def _validate_template_values(template, values: dict[str, object]) -> None:
for field in template.fields:
if field.key == "learner_name":
continue
if field.required and not values.get(field.key):
raise ValueError(f"{template.name}需要填写{field.label}")
start_date = values.get("course_start_date")
end_date = values.get("course_end_date")
if start_date and end_date and end_date < start_date:
raise ValueError("课程结束日期不能早于课程开始日期")
def _create_access_token(db: Session, certificate_id: int, token_type: str) -> int:
raw_token = generate_public_token()
token = CertificateAccessToken(
certificate_id=certificate_id,
token_hash=hash_token(raw_token),
token_value=raw_token,
token_type=token_type,
)
db.add(token)
db.flush()
return token.id