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