bug修复

This commit is contained in:
2026-07-21 11:02:26 +08:00
parent 14899055a4
commit 1d7e7d999c
5 changed files with 51 additions and 27 deletions
+2 -2
View File
@@ -12,7 +12,7 @@ import logging
import subprocess import subprocess
import tempfile import tempfile
from pathlib import Path from pathlib import Path
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
import requests import requests
from cryptography import x509 from cryptography import x509
@@ -326,7 +326,7 @@ class AcmeService:
try: try:
cert = x509.load_pem_x509_certificate(cert_path.read_bytes()) cert = x509.load_pem_x509_certificate(cert_path.read_bytes())
not_after = cert.not_valid_after_utc 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 return days_left <= self.config.renew_days, days_left
except Exception: except Exception:
return True, 0 return True, 0
+29
View File
@@ -22,6 +22,35 @@ async def get_db():
raise 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 def init_db():
async with engine.begin() as conn: async with engine.begin() as conn:
# 迁移旧表:acme_config → acme_configs # 迁移旧表:acme_config → acme_configs
+15 -23
View File
@@ -14,7 +14,7 @@ import time
logger = logging.getLogger(__name__) 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.models import Server, Domain, DeployLog, AcmeConfig, AcmeLog
from backend.config import get_settings 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) @router.post("/servers", status_code=201)
async def create_server(data: ServerCreate, db: AsyncSession = Depends(get_db), _: bool = Depends(require_auth)): async def create_server(data: ServerCreate, db: AsyncSession = Depends(get_db), _: bool = Depends(require_auth)):
server = Server(**data.model_dump()) server = Server(**data.model_dump())
db.add(server) await db_add(db, server, refresh=True)
await db.flush()
await db.refresh(server)
return {"id": server.id, "name": server.name} 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() server = result.scalar_one_or_none()
if not server: if not server:
raise HTTPException(404, "Server not found") raise HTTPException(404, "Server not found")
for key, value in data.model_dump(exclude_unset=True).items(): await db_update(db, server, data.model_dump(exclude_unset=True))
setattr(server, key, value)
return {"id": server.id} 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() server = result.scalar_one_or_none()
if not server: if not server:
raise HTTPException(404, "Server not found") raise HTTPException(404, "Server not found")
await db.delete(server) await db_delete(db, server)
return {"ok": True} return {"ok": True}
@@ -311,9 +308,7 @@ async def create_domain(data: DomainCreate, db: AsyncSession = Depends(get_db),
raise HTTPException(409, "该域名在此服务器下已存在相同 ACME 配置的记录") raise HTTPException(409, "该域名在此服务器下已存在相同 ACME 配置的记录")
domain = Domain(**dump) domain = Domain(**dump)
db.add(domain) await db_add(db, domain, refresh=True)
await db.flush()
await db.refresh(domain)
return {"id": domain.id, "domain": domain.domain} 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: if dup:
raise HTTPException(409, "该域名在此服务器下已存在相同 ACME 配置的记录") raise HTTPException(409, "该域名在此服务器下已存在相同 ACME 配置的记录")
for key, value in update_fields.items(): await db_update(db, domain, update_fields)
setattr(domain, key, value)
return {"id": domain.id} 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() domain = result.scalar_one_or_none()
if not domain: if not domain:
raise HTTPException(404, "Domain not found") raise HTTPException(404, "Domain not found")
await db.delete(domain) await db_delete(db, domain)
return {"ok": True} 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) @router.post("/acme/configs", status_code=201)
async def create_acme_config(data: AcmeConfigCreate, db: AsyncSession = Depends(get_db), _: bool = Depends(require_auth)): async def create_acme_config(data: AcmeConfigCreate, db: AsyncSession = Depends(get_db), _: bool = Depends(require_auth)):
config = AcmeConfig(**data.model_dump()) config = AcmeConfig(**data.model_dump())
db.add(config) await db_add(db, config, refresh=True)
await db.flush()
await db.refresh(config)
return {"id": config.id, "name": config.name} 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() config = result.scalar_one_or_none()
if not config: if not config:
raise HTTPException(404, "ACME config not found") raise HTTPException(404, "ACME config not found")
for key, value in data.model_dump(exclude_unset=True).items(): await db_update(db, config, data.model_dump(exclude_unset=True))
setattr(config, key, value)
return {"id": config.id} 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)) result = await db.execute(select(Domain).where(Domain.acme_config_id == config_id))
for d in result.scalars().all(): for d in result.scalars().all():
d.acme_config_id = None d.acme_config_id = None
await db.delete(config) await db_delete(db, config)
return {"ok": True} 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}") log = AcmeLog(domain_id=domain_id, action="issue", status="pending", message=f"Issuing certificate for {domain.domain}")
db.add(log) db.add(log)
await db.flush() await db_flush(db)
try: try:
success, msg, cert_info, raw_output = service.issue_certificate(domain.domain) 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) domain.version = str(int(domain.version or "0") + 1)
logger.warning(f"无法获取证书信息: {domain.domain}") logger.warning(f"无法获取证书信息: {domain.domain}")
await db.flush() await db.commit()
return {"success": success, "message": msg} return {"success": success, "message": msg}
except Exception as e: except Exception as e:
log.status = "failed" log.status = "failed"
log.message = str(e) log.message = str(e)
await db.flush() await db.commit()
raise HTTPException(500, str(e)) 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: if need_renew:
log = AcmeLog(domain_id=d.id, action="renew", status="pending", message=f"Auto-renewing {d.domain} (config: {config.name})") log = AcmeLog(domain_id=d.id, action="renew", status="pending", message=f"Auto-renewing {d.domain} (config: {config.name})")
db.add(log) db.add(log)
await db.flush() await db_flush(db)
success, msg, cert_info, raw_output = service.renew_certificate(d.domain) success, msg, cert_info, raw_output = service.renew_certificate(d.domain)
log.status = "success" if success else "failed" 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: else:
results.append({"domain": d.domain, "success": True, "message": f"Skipped, {days_left} days left", "days_left": days_left}) results.append({"domain": d.domain, "success": True, "message": f"Skipped, {days_left} days left", "days_left": days_left})
await db.commit()
return {"results": results} return {"results": results}
+2 -2
View File
@@ -6,7 +6,7 @@ from jinja2 import Environment, FileSystemLoader
from pathlib import Path from pathlib import Path
import os 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.models import Server, Domain, DeployLog
from backend.config import get_settings 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) 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) 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} return {"ok": True}
+3
View File
@@ -1,6 +1,9 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
# 补全 PATHcron 等非交互式环境默认 PATH 不含 /usr/sbin 等目录)
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH}"
# === 由 CertCenter 生成 === # === 由 CertCenter 生成 ===
DOMAIN="{{ domain }}" DOMAIN="{{ domain }}"
BASE_URL="{{ base_url }}" BASE_URL="{{ base_url }}"