From c66aa07efa890a0d6448afea7d1cd5ddf17dec95 Mon Sep 17 00:00:00 2001 From: Certificate System Date: Thu, 13 Aug 2026 18:21:24 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A7=BB=E9=99=A4=E5=8F=91=E8=AF=81=E5=8D=95?= =?UTF-8?q?=E4=BD=8D=E5=B9=B6=E4=BF=AE=E6=AD=A3=E8=AF=81=E4=B9=A6=E5=AD=97?= =?UTF-8?q?=E4=BD=93=E5=AD=97=E9=87=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../20260813_0008_remove_issuer_fields.py | 31 +++++++++++++++++ backend/app/api/routes/admin_certificates.py | 3 -- backend/app/api/routes/admin_imports.py | 1 - backend/app/api/routes/admin_projects.py | 5 --- backend/app/api/routes/public.py | 1 - backend/app/db/init_db.py | 1 - backend/app/models/certificate.py | 2 -- backend/app/schemas/certificate.py | 1 - backend/app/schemas/project.py | 3 -- backend/app/services/pdf.py | 33 ++++++++++++------- .../templates/certificate_placeholder.html | 1 - backend/tests/test_certificate_templates.py | 11 ++++++- backend/tests/test_import_rules.py | 2 +- frontend/src/api.ts | 3 -- frontend/src/views/AdminCertificates.vue | 1 - frontend/src/views/AdminProjects.vue | 15 ++------- frontend/src/views/CertificateQuery.vue | 3 -- frontend/src/views/CertificateView.vue | 4 +-- frontend/src/views/VerifyView.vue | 3 +- 19 files changed, 69 insertions(+), 55 deletions(-) create mode 100644 backend/alembic/versions/20260813_0008_remove_issuer_fields.py diff --git a/backend/alembic/versions/20260813_0008_remove_issuer_fields.py b/backend/alembic/versions/20260813_0008_remove_issuer_fields.py new file mode 100644 index 0000000..201da26 --- /dev/null +++ b/backend/alembic/versions/20260813_0008_remove_issuer_fields.py @@ -0,0 +1,31 @@ +"""remove unused issuer fields + +Revision ID: 20260813_0008 +Revises: 20260813_0007 +Create Date: 2026-08-13 +""" + +from alembic import op +import sqlalchemy as sa + + +revision = "20260813_0008" +down_revision = "20260813_0007" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.drop_column("certificates", "issuer_name") + op.drop_column("project_courses", "default_issuer_name") + + +def downgrade() -> None: + op.add_column( + "project_courses", + sa.Column("default_issuer_name", sa.String(length=128), nullable=False, server_default=""), + ) + op.add_column( + "certificates", + sa.Column("issuer_name", sa.String(length=128), nullable=False, server_default=""), + ) diff --git a/backend/app/api/routes/admin_certificates.py b/backend/app/api/routes/admin_certificates.py index ed51a5b..4d68027 100644 --- a/backend/app/api/routes/admin_certificates.py +++ b/backend/app/api/routes/admin_certificates.py @@ -91,7 +91,6 @@ def create_certificate( course_start_date=payload.course_start_date, course_end_date=payload.course_end_date, issue_date=payload.issue_date, - issuer_name=project.default_issuer_name, template_code=template_code, remark=payload.remark, ) @@ -178,7 +177,6 @@ def _certificate_payload(certificate: Certificate, learner: Learner | None = Non "course_start_date": certificate.course_start_date, "course_end_date": certificate.course_end_date, "issue_date": certificate.issue_date, - "issuer_name": certificate.issuer_name, "template_code": certificate.template_code, "status": certificate.status, "pdf_status": certificate.pdf_status, @@ -249,7 +247,6 @@ def preview_certificate( "course_start_date": certificate.course_start_date.isoformat() if certificate.course_start_date else None, "course_end_date": certificate.course_end_date.isoformat() if certificate.course_end_date else None, "issue_date": certificate.issue_date.isoformat(), - "issuer_name": certificate.issuer_name, "template_code": certificate.template_code, "status": certificate.status, "public_url": f"/cert/{public_token.token_value}" if public_token else "", diff --git a/backend/app/api/routes/admin_imports.py b/backend/app/api/routes/admin_imports.py index 422e038..80110b7 100644 --- a/backend/app/api/routes/admin_imports.py +++ b/backend/app/api/routes/admin_imports.py @@ -220,7 +220,6 @@ def confirm_import_batch( course_start_date=course_start_date, course_end_date=course_end_date, issue_date=issue_date, - issuer_name=project.default_issuer_name, template_code=batch.template_code, remark=None, ) diff --git a/backend/app/api/routes/admin_projects.py b/backend/app/api/routes/admin_projects.py index fc49997..4594107 100644 --- a/backend/app/api/routes/admin_projects.py +++ b/backend/app/api/routes/admin_projects.py @@ -37,7 +37,6 @@ def create_project( default_certificate_name=payload.name, default_course_name=payload.default_course_name, default_stage_name=payload.default_stage_name, - default_issuer_name=payload.default_issuer_name, default_template_code=template_code, ) db.add(project) @@ -61,7 +60,6 @@ def update_project( "name": project.name, "default_course_name": project.default_course_name, "default_stage_name": project.default_stage_name, - "default_issuer_name": project.default_issuer_name, "default_template_code": project.default_template_code, "status": project.status, } @@ -72,8 +70,6 @@ def update_project( project.default_course_name = payload.default_course_name if payload.default_stage_name is not None: project.default_stage_name = payload.default_stage_name - if payload.default_issuer_name is not None: - project.default_issuer_name = payload.default_issuer_name if payload.default_template_code is not None: try: template_code = get_certificate_template(payload.default_template_code).code @@ -86,7 +82,6 @@ def update_project( "name": project.name, "default_course_name": project.default_course_name, "default_stage_name": project.default_stage_name, - "default_issuer_name": project.default_issuer_name, "default_template_code": project.default_template_code, "status": project.status, } diff --git a/backend/app/api/routes/public.py b/backend/app/api/routes/public.py index fd189f3..cba23d5 100644 --- a/backend/app/api/routes/public.py +++ b/backend/app/api/routes/public.py @@ -179,7 +179,6 @@ def public_certificate_payload(db: Session, certificate: Certificate, learner: L "course_start_date": certificate.course_start_date.isoformat() if certificate.course_start_date else None, "course_end_date": certificate.course_end_date.isoformat() if certificate.course_end_date else None, "issue_date": certificate.issue_date.isoformat(), - "issuer_name": certificate.issuer_name, "template_code": certificate.template_code, "status": certificate.status, "pdf_status": certificate.pdf_status, diff --git a/backend/app/db/init_db.py b/backend/app/db/init_db.py index 5a60ce0..24b2c47 100644 --- a/backend/app/db/init_db.py +++ b/backend/app/db/init_db.py @@ -36,7 +36,6 @@ def seed_defaults(db: Session, admin_username: str, admin_password: str) -> None default_certificate_name="培训结业证书", default_course_name=name, default_stage_name=None, - default_issuer_name="本公司", default_template_code="classic", ) ) diff --git a/backend/app/models/certificate.py b/backend/app/models/certificate.py index d91255d..584cf08 100644 --- a/backend/app/models/certificate.py +++ b/backend/app/models/certificate.py @@ -15,7 +15,6 @@ class ProjectCourse(Base): default_certificate_name: Mapped[str] = mapped_column(String(128), default="培训结业证书") default_course_name: Mapped[str | None] = mapped_column(String(128), nullable=True) default_stage_name: Mapped[str | None] = mapped_column(String(128), nullable=True) - default_issuer_name: Mapped[str] = mapped_column(String(128), default="本公司") default_template_code: Mapped[str] = mapped_column(String(32), default="classic") status: Mapped[str] = mapped_column(String(32), default="active") created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) @@ -37,7 +36,6 @@ class Certificate(Base): course_start_date: Mapped[date | None] = mapped_column(Date, nullable=True) course_end_date: Mapped[date | None] = mapped_column(Date, nullable=True) issue_date: Mapped[date] = mapped_column(Date) - issuer_name: Mapped[str] = mapped_column(String(128)) template_code: Mapped[str] = mapped_column(String(32), default="classic", index=True) status: Mapped[str] = mapped_column(String(32), default="valid") pdf_status: Mapped[str] = mapped_column(String(32), default="not_generated") diff --git a/backend/app/schemas/certificate.py b/backend/app/schemas/certificate.py index 6b1c66d..c0750ab 100644 --- a/backend/app/schemas/certificate.py +++ b/backend/app/schemas/certificate.py @@ -44,7 +44,6 @@ class CertificateOut(BaseModel): course_start_date: date | None course_end_date: date | None issue_date: date - issuer_name: str template_code: str status: str pdf_status: str diff --git a/backend/app/schemas/project.py b/backend/app/schemas/project.py index 3a9d9c0..d9c5201 100644 --- a/backend/app/schemas/project.py +++ b/backend/app/schemas/project.py @@ -9,7 +9,6 @@ class ProjectCourseCreate(BaseModel): default_certificate_name: str | None = Field(default=None, max_length=128) default_course_name: str | None = Field(default=None, max_length=128) default_stage_name: str | None = Field(default=None, max_length=128) - default_issuer_name: str = Field(default="本公司", min_length=1, max_length=128) default_template_code: str = Field(default="classic", min_length=1, max_length=32) @field_validator("code") @@ -31,7 +30,6 @@ class ProjectCourseUpdate(BaseModel): default_certificate_name: str | None = Field(default=None, max_length=128) default_course_name: str | None = Field(default=None, max_length=128) default_stage_name: str | None = Field(default=None, max_length=128) - default_issuer_name: str | None = Field(default=None, min_length=1, max_length=128) default_template_code: str | None = Field(default=None, min_length=1, max_length=32) status: str | None = Field(default=None, pattern="^(active|disabled)$") @@ -51,7 +49,6 @@ class ProjectCourseOut(BaseModel): default_certificate_name: str default_course_name: str | None default_stage_name: str | None - default_issuer_name: str default_template_code: str status: str created_at: datetime diff --git a/backend/app/services/pdf.py b/backend/app/services/pdf.py index 93ac01b..edd6a9b 100644 --- a/backend/app/services/pdf.py +++ b/backend/app/services/pdf.py @@ -269,15 +269,32 @@ def font(size: int, kind: str = "song", bold: bool = False, scale: float = 1.0) ("/System/Library/Fonts/STHeiti Light.ttc", 1), ], } - paths = choices.get(kind, choices["song"]) - if bold and kind == "song": - paths = [ + bold_choices = { + "kai": [ + r"C:\Windows\Fonts\msyhbd.ttc", + r"C:\Windows\Fonts\simhei.ttf", + "/usr/share/fonts/opentype/noto/NotoSerifCJK-Bold.ttc", + ("/System/Library/Fonts/Supplemental/Songti.ttc", 0), + ("/System/Library/Fonts/STHeiti Medium.ttc", 1), + ], + "hei": [ r"C:\Windows\Fonts\msyhbd.ttc", r"C:\Windows\Fonts\simhei.ttf", "/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc", ("/System/Library/Fonts/STHeiti Medium.ttc", 1), ("/System/Library/Fonts/Hiragino Sans GB.ttc", 2), - ] + paths + ], + "song": [ + r"C:\Windows\Fonts\msyhbd.ttc", + r"C:\Windows\Fonts\simhei.ttf", + "/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc", + ("/System/Library/Fonts/STHeiti Medium.ttc", 1), + ("/System/Library/Fonts/Hiragino Sans GB.ttc", 2), + ], + } + paths = choices.get(kind, choices["song"]) + if bold: + paths = bold_choices.get(kind, bold_choices["song"]) + paths for item in paths: path, index = item if isinstance(item, tuple) else (item, 0) if Path(path).exists(): @@ -344,7 +361,7 @@ def draw_issue_date_numbers( x_scale: float, y_scale: float, ) -> None: - text_font = font(13, "song", True, scale) + text_font = font(13, "song", False, scale) y = 688 * y_scale for value, x in [(issue_year, 500), (issue_month, 535), (issue_day, 566)]: draw.text((x * x_scale, y), value, fill="#111111", font=text_font, anchor="mm") @@ -364,8 +381,6 @@ def draw_inline( ) -> float: text_font = font(size, kind, bold, scale) draw.text((x, y), value, fill="#111111", font=text_font, anchor="ls") - if bold: - draw.text((x + 0.45 * x_scale, y), value, fill="#111111", font=text_font, anchor="ls") return x + draw.textlength(value, font=text_font) + gap * x_scale @@ -374,7 +389,6 @@ def draw_course_name(draw: ImageDraw.ImageDraw, value: str, x: float, y: int) -> text_font = font(27, "kai", True) if draw.textlength(value, font=text_font) <= max_width: draw.text((x, y), value, fill="#111111", font=text_font) - draw.text((x + 0.45, y), value, fill="#111111", font=text_font) return y start_y = y + 34 @@ -382,7 +396,6 @@ def draw_course_name(draw: ImageDraw.ImageDraw, value: str, x: float, y: int) -> for index, line in enumerate(lines): line_y = start_y + index * 34 draw.text((185, line_y), line, fill="#111111", font=text_font) - draw.text((185.45, line_y), line, fill="#111111", font=text_font) return start_y + (len(lines) - 1) * 34 @@ -409,8 +422,6 @@ def draw_inline_flow( x = line_start y += line_height draw.text((x, y), char, fill="#111111", font=text_font, anchor="ls") - if style == "course": - draw.text((x + 0.45 * x_scale, y), char, fill="#111111", font=text_font, anchor="ls") x += width diff --git a/backend/app/templates/certificate_placeholder.html b/backend/app/templates/certificate_placeholder.html index d98d660..0c0791a 100644 --- a/backend/app/templates/certificate_placeholder.html +++ b/backend/app/templates/certificate_placeholder.html @@ -25,7 +25,6 @@
证书编号:{{ certificate_no }}
发证日期:{{ issue_date }}
-
发证单位:{{ issuer_name }}
{% if qr_code_data_url %}二维码{% endif %}
diff --git a/backend/tests/test_certificate_templates.py b/backend/tests/test_certificate_templates.py index 486d942..975bbba 100644 --- a/backend/tests/test_certificate_templates.py +++ b/backend/tests/test_certificate_templates.py @@ -7,7 +7,7 @@ from app.services.certificate_templates import ( get_certificate_template, list_certificate_templates, ) -from app.services.pdf import render_certificate_image +from app.services.pdf import font, render_certificate_image def test_builtin_certificate_templates_have_assets(): @@ -31,3 +31,12 @@ def test_practice_camp_template_renders_four_dynamic_values(): assert rendered.size == (3437, 2551) assert template.dynamic_fields == ("姓名", "课程开始日期", "课程结束日期", "发证日期") + + +def test_certificate_bold_fonts_use_real_bold_faces(): + for kind in ("kai", "song"): + regular = font(32, kind, False) + bold = font(32, kind, True) + + assert regular.getname()[1] != bold.getname()[1] + assert "Bold" in bold.getname()[1] diff --git a/backend/tests/test_import_rules.py b/backend/tests/test_import_rules.py index ba8d297..bec870d 100644 --- a/backend/tests/test_import_rules.py +++ b/backend/tests/test_import_rules.py @@ -87,7 +87,7 @@ def test_certificate_create_rejects_reversed_course_period(): ) -def test_certificate_create_uses_project_issuer_without_manual_input(): +def test_certificate_create_has_no_issuer_field(): certificate = CertificateCreate( learner_id=1, project_code="DBY", diff --git a/frontend/src/api.ts b/frontend/src/api.ts index c181048..7ae9110 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -22,7 +22,6 @@ export interface PublicCertificate { course_start_date: string | null; course_end_date: string | null; issue_date: string; - issuer_name: string; template_code: string; status: string; pdf_status: string; @@ -45,7 +44,6 @@ export interface ProjectCourse { default_certificate_name: string; default_course_name: string | null; default_stage_name: string | null; - default_issuer_name: string; default_template_code: string; status: string; created_at: string; @@ -77,7 +75,6 @@ export interface AdminCertificate { course_start_date: string | null; course_end_date: string | null; issue_date: string; - issuer_name: string; template_code: string; status: string; pdf_status: string; diff --git a/frontend/src/views/AdminCertificates.vue b/frontend/src/views/AdminCertificates.vue index 1ffe238..480dace 100644 --- a/frontend/src/views/AdminCertificates.vue +++ b/frontend/src/views/AdminCertificates.vue @@ -147,7 +147,6 @@ {{ templateName(preview.template_code) }} {{ coursePeriodText(preview) }} {{ preview.issue_date }} - {{ preview.issuer_name }} {{ preview.public_url }} diff --git a/frontend/src/views/AdminProjects.vue b/frontend/src/views/AdminProjects.vue index c484004..6ad5e40 100644 --- a/frontend/src/views/AdminProjects.vue +++ b/frontend/src/views/AdminProjects.vue @@ -3,7 +3,7 @@

项目管理

-

维护项目代码和导入时使用的证书默认信息。

+

维护项目代码、默认证书模板和课程信息。

{{ editingId ? "保存修改" : "新增项目" }}
@@ -16,9 +16,6 @@ - - - @@ -51,7 +48,6 @@ - @@ -90,7 +86,6 @@ const form = reactive({ name: "", default_course_name: "", default_stage_name: "", - default_issuer_name: "本公司", default_template_code: "classic", status: "active", }); @@ -104,7 +99,6 @@ const projects = computed(() => { item.name, item.default_course_name || "", item.default_stage_name || "", - item.default_issuer_name, statusText(item.status), ].join(" ").toLowerCase(); return text.includes(value); @@ -136,7 +130,6 @@ function editProject(row: ProjectCourse) { name: row.name, default_course_name: row.default_course_name || "", default_stage_name: row.default_stage_name || "", - default_issuer_name: row.default_issuer_name, default_template_code: row.default_template_code, status: row.status, }); @@ -149,7 +142,6 @@ function resetForm() { name: "", default_course_name: "", default_stage_name: "", - default_issuer_name: "本公司", default_template_code: "classic", status: "active", }); @@ -161,15 +153,14 @@ function projectPayload() { name: form.name, default_course_name: form.default_course_name || null, default_stage_name: form.default_stage_name || null, - default_issuer_name: form.default_issuer_name, default_template_code: form.default_template_code, status: form.status, }; } async function saveProject() { - if (!form.code || !form.name || !form.default_issuer_name) { - ElMessage.warning("项目代码、项目名称和发证单位必填"); + if (!form.code || !form.name) { + ElMessage.warning("项目代码和项目名称必填"); return; } if (editingId.value) { diff --git a/frontend/src/views/CertificateQuery.vue b/frontend/src/views/CertificateQuery.vue index 47dd2cd..b0cbc1a 100644 --- a/frontend/src/views/CertificateQuery.vue +++ b/frontend/src/views/CertificateQuery.vue @@ -90,8 +90,6 @@
{{ item.issue_date }}
-
发证单位
-
{{ item.issuer_name }}
@@ -132,7 +130,6 @@
证书编号:{{ current.certificate_no }} 发证日期:{{ current.issue_date }} - 发证单位:{{ current.issuer_name }}