From 1d7e7d999cb18b4bcb93f4b5e1998767ddd5d51f Mon Sep 17 00:00:00 2001 From: sans Date: Tue, 21 Jul 2026 11:02:26 +0800 Subject: [PATCH] =?UTF-8?q?bug=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/acme_service.py | 4 +-- backend/database.py | 29 ++++++++++++++++++ backend/routers/admin.py | 38 ++++++++++-------------- backend/routers/api.py | 4 +-- backend/templates_cert/deploy-cert.sh.j2 | 3 ++ 5 files changed, 51 insertions(+), 27 deletions(-) diff --git a/backend/acme_service.py b/backend/acme_service.py index 09aa519..c656c73 100644 --- a/backend/acme_service.py +++ b/backend/acme_service.py @@ -12,7 +12,7 @@ import logging import subprocess import tempfile from pathlib import Path -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import requests from cryptography import x509 @@ -326,7 +326,7 @@ class AcmeService: try: cert = x509.load_pem_x509_certificate(cert_path.read_bytes()) not_after = cert.not_valid_after_utc - days_left = (not_after - datetime.utcnow()).days + days_left = (not_after - datetime.now(timezone.utc)).days return days_left <= self.config.renew_days, days_left except Exception: return True, 0 diff --git a/backend/database.py b/backend/database.py index d6b4ed0..85affb1 100644 --- a/backend/database.py +++ b/backend/database.py @@ -22,6 +22,35 @@ async def get_db(): raise +# ────────────── CRUD 工具函数 ────────────── + +async def db_add(db: AsyncSession, obj, *, refresh: bool = False): + """创建记录并提交。refresh=True 会在提交后刷新对象(获取生成的 id 等)。""" + db.add(obj) + await db.flush() + if refresh: + await db.refresh(obj) + await db.commit() + + +async def db_update(db: AsyncSession, obj, data: dict): + """批量更新字段并提交。data 一般来自 model_dump(exclude_unset=True)。""" + for key, value in data.items(): + setattr(obj, key, value) + await db.commit() + + +async def db_delete(db: AsyncSession, obj): + """删除记录并提交。""" + await db.delete(obj) + await db.commit() + + +async def db_flush(db: AsyncSession): + """将待写操作刷到数据库(不提交事务),用于需要获取生成 ID 等中间状态的场景。""" + await db.flush() + + async def init_db(): async with engine.begin() as conn: # 迁移旧表:acme_config → acme_configs diff --git a/backend/routers/admin.py b/backend/routers/admin.py index 204b13c..6950963 100644 --- a/backend/routers/admin.py +++ b/backend/routers/admin.py @@ -14,7 +14,7 @@ import time logger = logging.getLogger(__name__) -from backend.database import get_db +from backend.database import get_db, db_add, db_update, db_delete, db_flush from backend.models import Server, Domain, DeployLog, AcmeConfig, AcmeLog from backend.config import get_settings @@ -212,9 +212,7 @@ async def get_server(server_id: int, db: AsyncSession = Depends(get_db), _: bool @router.post("/servers", status_code=201) async def create_server(data: ServerCreate, db: AsyncSession = Depends(get_db), _: bool = Depends(require_auth)): server = Server(**data.model_dump()) - db.add(server) - await db.flush() - await db.refresh(server) + await db_add(db, server, refresh=True) return {"id": server.id, "name": server.name} @@ -224,8 +222,7 @@ async def update_server(server_id: int, data: ServerUpdate, db: AsyncSession = D server = result.scalar_one_or_none() if not server: raise HTTPException(404, "Server not found") - for key, value in data.model_dump(exclude_unset=True).items(): - setattr(server, key, value) + await db_update(db, server, data.model_dump(exclude_unset=True)) return {"id": server.id} @@ -235,7 +232,7 @@ async def delete_server(server_id: int, db: AsyncSession = Depends(get_db), _: b server = result.scalar_one_or_none() if not server: raise HTTPException(404, "Server not found") - await db.delete(server) + await db_delete(db, server) return {"ok": True} @@ -311,9 +308,7 @@ async def create_domain(data: DomainCreate, db: AsyncSession = Depends(get_db), raise HTTPException(409, "该域名在此服务器下已存在相同 ACME 配置的记录") domain = Domain(**dump) - db.add(domain) - await db.flush() - await db.refresh(domain) + await db_add(db, domain, refresh=True) return {"id": domain.id, "domain": domain.domain} @@ -343,8 +338,7 @@ async def update_domain(domain_id: int, data: DomainUpdate, db: AsyncSession = D if dup: raise HTTPException(409, "该域名在此服务器下已存在相同 ACME 配置的记录") - for key, value in update_fields.items(): - setattr(domain, key, value) + await db_update(db, domain, update_fields) return {"id": domain.id} @@ -354,7 +348,7 @@ async def delete_domain(domain_id: int, db: AsyncSession = Depends(get_db), _: b domain = result.scalar_one_or_none() if not domain: raise HTTPException(404, "Domain not found") - await db.delete(domain) + await db_delete(db, domain) return {"ok": True} @@ -448,9 +442,7 @@ async def get_acme_config(config_id: int, db: AsyncSession = Depends(get_db), _: @router.post("/acme/configs", status_code=201) async def create_acme_config(data: AcmeConfigCreate, db: AsyncSession = Depends(get_db), _: bool = Depends(require_auth)): config = AcmeConfig(**data.model_dump()) - db.add(config) - await db.flush() - await db.refresh(config) + await db_add(db, config, refresh=True) return {"id": config.id, "name": config.name} @@ -460,8 +452,7 @@ async def update_acme_config(config_id: int, data: AcmeConfigUpdate, db: AsyncSe config = result.scalar_one_or_none() if not config: raise HTTPException(404, "ACME config not found") - for key, value in data.model_dump(exclude_unset=True).items(): - setattr(config, key, value) + await db_update(db, config, data.model_dump(exclude_unset=True)) return {"id": config.id} @@ -475,7 +466,7 @@ async def delete_acme_config(config_id: int, db: AsyncSession = Depends(get_db), result = await db.execute(select(Domain).where(Domain.acme_config_id == config_id)) for d in result.scalars().all(): d.acme_config_id = None - await db.delete(config) + await db_delete(db, config) return {"ok": True} @@ -517,7 +508,7 @@ async def issue_cert(domain_id: int, db: AsyncSession = Depends(get_db), _: bool # 记录开始 log = AcmeLog(domain_id=domain_id, action="issue", status="pending", message=f"Issuing certificate for {domain.domain}") db.add(log) - await db.flush() + await db_flush(db) try: success, msg, cert_info, raw_output = service.issue_certificate(domain.domain) @@ -537,12 +528,12 @@ async def issue_cert(domain_id: int, db: AsyncSession = Depends(get_db), _: bool domain.version = str(int(domain.version or "0") + 1) logger.warning(f"无法获取证书信息: {domain.domain}") - await db.flush() + await db.commit() return {"success": success, "message": msg} except Exception as e: log.status = "failed" log.message = str(e) - await db.flush() + await db.commit() raise HTTPException(500, str(e)) @@ -583,7 +574,7 @@ async def auto_renew_all(db: AsyncSession = Depends(get_db), _: bool = Depends(r if need_renew: log = AcmeLog(domain_id=d.id, action="renew", status="pending", message=f"Auto-renewing {d.domain} (config: {config.name})") db.add(log) - await db.flush() + await db_flush(db) success, msg, cert_info, raw_output = service.renew_certificate(d.domain) log.status = "success" if success else "failed" @@ -601,6 +592,7 @@ async def auto_renew_all(db: AsyncSession = Depends(get_db), _: bool = Depends(r else: results.append({"domain": d.domain, "success": True, "message": f"Skipped, {days_left} days left", "days_left": days_left}) + await db.commit() return {"results": results} diff --git a/backend/routers/api.py b/backend/routers/api.py index f42a5d4..7a56bb3 100644 --- a/backend/routers/api.py +++ b/backend/routers/api.py @@ -6,7 +6,7 @@ from jinja2 import Environment, FileSystemLoader from pathlib import Path import os -from backend.database import get_db +from backend.database import get_db, db_add from backend.models import Server, Domain, DeployLog from backend.config import get_settings @@ -142,5 +142,5 @@ async def report_deploy( d = await _find_domain(db, domain, server_id=server.id, acme_config_id=acme_config_id) log = DeployLog(domain_id=d.id, server_id=server.id, hostname=hostname, status=status, message=message) - db.add(log) + await db_add(db, log) return {"ok": True} diff --git a/backend/templates_cert/deploy-cert.sh.j2 b/backend/templates_cert/deploy-cert.sh.j2 index 1504d7b..d61029b 100644 --- a/backend/templates_cert/deploy-cert.sh.j2 +++ b/backend/templates_cert/deploy-cert.sh.j2 @@ -1,6 +1,9 @@ #!/usr/bin/env bash set -euo pipefail +# 补全 PATH(cron 等非交互式环境默认 PATH 不含 /usr/sbin 等目录) +export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH}" + # === 由 CertCenter 生成 === DOMAIN="{{ domain }}" BASE_URL="{{ base_url }}"