136 lines
5.2 KiB
Python
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
|