更新
This commit is contained in:
@@ -0,0 +1,346 @@
|
||||
from pydantic import BaseModel, EmailStr, Field
|
||||
|
||||
from dataclasses import asdict
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from models.database import get_db
|
||||
from models.db_config import (
|
||||
PASSWORD_PLACEHOLDER as DB_PASSWORD_PLACEHOLDER,
|
||||
database_config_to_response,
|
||||
persist_database_config,
|
||||
test_database_connection,
|
||||
)
|
||||
from models.db_transfer import inspect_sqlite_source, migrate_sqlite_to_target
|
||||
from models.models import User
|
||||
from .dependencies import require_admin
|
||||
from .email_service import send_test_email
|
||||
from .system_settings import (
|
||||
PASSWORD_PLACEHOLDER,
|
||||
SystemSettingsData,
|
||||
load_settings,
|
||||
save_settings,
|
||||
settings_to_admin_response,
|
||||
settings_to_payment_response,
|
||||
settings_to_public,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/api/settings", tags=["settings"])
|
||||
|
||||
|
||||
class PublicSettingsResponse(BaseModel):
|
||||
registration_enabled: bool
|
||||
email_verification_required: bool
|
||||
email_binding_required: bool
|
||||
|
||||
|
||||
class SystemSettingsResponse(BaseModel):
|
||||
registration_enabled: bool
|
||||
email_verification_required: bool
|
||||
email_binding_required: bool = False
|
||||
email_verify_token_hours: int
|
||||
auto_reply_delay_seconds: int = 0
|
||||
auto_reply_cooldown_seconds: int = 60
|
||||
app_url: str
|
||||
default_register_max_accounts: int = 3
|
||||
smtp_host: str
|
||||
smtp_port: int
|
||||
smtp_user: str
|
||||
smtp_password: str = ""
|
||||
smtp_password_configured: bool = False
|
||||
smtp_from: str
|
||||
smtp_use_tls: bool
|
||||
smtp_use_ssl: bool = False
|
||||
debug_show_verify_link: bool
|
||||
email_verify_subject: str
|
||||
email_verify_body: str
|
||||
email_verify_html: str
|
||||
email_test_subject: str
|
||||
email_test_body: str
|
||||
email_test_html: str
|
||||
|
||||
|
||||
class SystemSettingsUpdate(BaseModel):
|
||||
registration_enabled: bool | None = None
|
||||
email_verification_required: bool | None = None
|
||||
email_binding_required: bool | None = None
|
||||
email_verify_token_hours: int | None = Field(default=None, ge=1, le=168)
|
||||
auto_reply_delay_seconds: int | None = Field(default=None, ge=0, le=86400)
|
||||
auto_reply_cooldown_seconds: int | None = Field(default=None, ge=0, le=86400)
|
||||
app_url: str | None = None
|
||||
default_register_max_accounts: int | None = Field(default=None, ge=0, le=999)
|
||||
smtp_host: str | None = None
|
||||
smtp_port: int | None = Field(default=None, ge=1, le=65535)
|
||||
smtp_user: str | None = None
|
||||
smtp_password: str | None = None
|
||||
smtp_from: str | None = None
|
||||
smtp_use_tls: bool | None = None
|
||||
smtp_use_ssl: bool | None = None
|
||||
debug_show_verify_link: bool | None = None
|
||||
email_verify_subject: str | None = None
|
||||
email_verify_body: str | None = None
|
||||
email_verify_html: str | None = None
|
||||
email_test_subject: str | None = None
|
||||
email_test_body: str | None = None
|
||||
email_test_html: str | None = None
|
||||
|
||||
|
||||
class TestEmailRequest(BaseModel):
|
||||
to_email: EmailStr
|
||||
smtp_host: str | None = None
|
||||
smtp_port: int | None = Field(default=None, ge=1, le=65535)
|
||||
smtp_user: str | None = None
|
||||
smtp_password: str | None = None
|
||||
smtp_from: str | None = None
|
||||
smtp_use_tls: bool | None = None
|
||||
smtp_use_ssl: bool | None = None
|
||||
|
||||
|
||||
class MessageResponse(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
class DatabaseSettingsResponse(BaseModel):
|
||||
db_type: str
|
||||
db_host: str = ""
|
||||
db_port: int = 0
|
||||
db_user: str = ""
|
||||
db_password: str = ""
|
||||
db_password_configured: bool = False
|
||||
db_name: str = ""
|
||||
db_path: str = ""
|
||||
database_url_display: str = ""
|
||||
supported_types: list[str] = []
|
||||
|
||||
|
||||
class DatabaseSettingsUpdate(BaseModel):
|
||||
db_type: str
|
||||
db_host: str | None = None
|
||||
db_port: int | None = Field(default=None, ge=1, le=65535)
|
||||
db_user: str | None = None
|
||||
db_password: str | None = None
|
||||
db_name: str | None = None
|
||||
db_path: str | None = None
|
||||
|
||||
|
||||
class DatabaseTestRequest(BaseModel):
|
||||
db_type: str
|
||||
db_host: str | None = None
|
||||
db_port: int | None = Field(default=None, ge=1, le=65535)
|
||||
db_user: str | None = None
|
||||
db_password: str | None = None
|
||||
db_name: str | None = None
|
||||
db_path: str | None = None
|
||||
|
||||
|
||||
class DatabaseMigratePreviewResponse(BaseModel):
|
||||
source_path: str
|
||||
exists: bool
|
||||
tables: dict[str, int]
|
||||
total_rows: int
|
||||
|
||||
|
||||
class DatabaseMigrateRequest(DatabaseTestRequest):
|
||||
source_db_path: str | None = None
|
||||
clear_target: bool = False
|
||||
|
||||
|
||||
class DatabaseMigrateResponse(BaseModel):
|
||||
message: str
|
||||
source_path: str
|
||||
target_type: str
|
||||
target_url: str
|
||||
tables: dict[str, int]
|
||||
total_rows: int
|
||||
|
||||
|
||||
class PaymentSettingsResponse(BaseModel):
|
||||
app_url: str
|
||||
payment_enabled: bool = False
|
||||
payment_demo_mode: bool = True
|
||||
wechat_pay_enabled: bool = False
|
||||
alipay_pay_enabled: bool = False
|
||||
account_slot_unit_price: float = 9.9
|
||||
account_slot_purchase_min: int = 1
|
||||
account_slot_purchase_max: int = 20
|
||||
wechat_app_id: str = ""
|
||||
wechat_mch_id: str = ""
|
||||
wechat_api_v3_key: str = ""
|
||||
wechat_api_v3_key_configured: bool = False
|
||||
wechat_cert_serial: str = ""
|
||||
wechat_private_key: str = ""
|
||||
wechat_private_key_configured: bool = False
|
||||
wechat_pay_configured: bool = False
|
||||
alipay_app_id: str = ""
|
||||
alipay_private_key: str = ""
|
||||
alipay_private_key_configured: bool = False
|
||||
alipay_public_key: str = ""
|
||||
alipay_sandbox: bool = False
|
||||
alipay_configured: bool = False
|
||||
|
||||
|
||||
class PaymentSettingsUpdate(BaseModel):
|
||||
payment_enabled: bool | None = None
|
||||
payment_demo_mode: bool | None = None
|
||||
wechat_pay_enabled: bool | None = None
|
||||
alipay_pay_enabled: bool | None = None
|
||||
account_slot_unit_price: float | None = Field(default=None, ge=0.01, le=99999)
|
||||
account_slot_purchase_min: int | None = Field(default=None, ge=1, le=100)
|
||||
account_slot_purchase_max: int | None = Field(default=None, ge=1, le=100)
|
||||
wechat_app_id: str | None = None
|
||||
wechat_mch_id: str | None = None
|
||||
wechat_api_v3_key: str | None = None
|
||||
wechat_cert_serial: str | None = None
|
||||
wechat_private_key: str | None = None
|
||||
alipay_app_id: str | None = None
|
||||
alipay_private_key: str | None = None
|
||||
alipay_public_key: str | None = None
|
||||
alipay_sandbox: bool | None = None
|
||||
|
||||
|
||||
@router.get("/public", response_model=PublicSettingsResponse)
|
||||
async def get_public_settings(db: AsyncSession = Depends(get_db)):
|
||||
data = await load_settings(db)
|
||||
return PublicSettingsResponse(**settings_to_public(data))
|
||||
|
||||
|
||||
@router.get("", response_model=SystemSettingsResponse)
|
||||
async def get_system_settings(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(require_admin),
|
||||
):
|
||||
data = await load_settings(db)
|
||||
return SystemSettingsResponse(**settings_to_admin_response(data))
|
||||
|
||||
|
||||
@router.put("", response_model=SystemSettingsResponse)
|
||||
async def update_system_settings(
|
||||
body: SystemSettingsUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(require_admin),
|
||||
):
|
||||
updates = body.model_dump(exclude_unset=True)
|
||||
if "app_url" in updates and updates["app_url"]:
|
||||
updates["app_url"] = updates["app_url"].strip().rstrip("/")
|
||||
data = await save_settings(db, updates)
|
||||
return SystemSettingsResponse(**settings_to_admin_response(data))
|
||||
|
||||
|
||||
@router.get("/payment", response_model=PaymentSettingsResponse)
|
||||
async def get_payment_settings(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(require_admin),
|
||||
):
|
||||
data = await load_settings(db)
|
||||
return PaymentSettingsResponse(**settings_to_payment_response(data))
|
||||
|
||||
|
||||
@router.put("/payment", response_model=PaymentSettingsResponse)
|
||||
async def update_payment_settings(
|
||||
body: PaymentSettingsUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(require_admin),
|
||||
):
|
||||
updates = body.model_dump(exclude_unset=True)
|
||||
data = await save_settings(db, updates)
|
||||
return PaymentSettingsResponse(**settings_to_payment_response(data))
|
||||
|
||||
|
||||
@router.post("/test-email", response_model=MessageResponse)
|
||||
async def test_smtp_email(
|
||||
body: TestEmailRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(require_admin),
|
||||
):
|
||||
data = await load_settings(db)
|
||||
overrides = body.model_dump(exclude_unset=True, exclude={"to_email"})
|
||||
if overrides:
|
||||
merged = asdict(data)
|
||||
for key, value in overrides.items():
|
||||
if value is None:
|
||||
continue
|
||||
if key == "smtp_password":
|
||||
pwd = str(value).strip()
|
||||
if not pwd or pwd == PASSWORD_PLACEHOLDER:
|
||||
continue
|
||||
merged[key] = value
|
||||
data = SystemSettingsData(**merged)
|
||||
if not data.smtp_configured():
|
||||
raise HTTPException(status_code=400, detail="请先完整配置 SMTP 服务器与发件人")
|
||||
try:
|
||||
await send_test_email(str(body.to_email), data)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
return MessageResponse(message=f"测试邮件已发送至 {body.to_email}")
|
||||
|
||||
|
||||
@router.get("/database", response_model=DatabaseSettingsResponse)
|
||||
async def get_database_settings(_: User = Depends(require_admin)):
|
||||
return DatabaseSettingsResponse(**database_config_to_response())
|
||||
|
||||
|
||||
@router.put("/database", response_model=MessageResponse)
|
||||
async def update_database_settings(
|
||||
body: DatabaseSettingsUpdate,
|
||||
_: User = Depends(require_admin),
|
||||
):
|
||||
payload = body.model_dump(exclude_unset=True)
|
||||
if payload.get("db_password") in (None, "", DB_PASSWORD_PLACEHOLDER):
|
||||
payload.pop("db_password", None)
|
||||
try:
|
||||
await test_database_connection(payload)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=400, detail=f"连接测试失败: {exc}") from exc
|
||||
try:
|
||||
persist_database_config(payload)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail=f"写入配置失败: {exc}") from exc
|
||||
return MessageResponse(message="数据库配置已保存至 .env,请重启后端服务后生效")
|
||||
|
||||
|
||||
@router.post("/database/test", response_model=MessageResponse)
|
||||
async def test_database_settings(
|
||||
body: DatabaseTestRequest,
|
||||
_: User = Depends(require_admin),
|
||||
):
|
||||
payload = body.model_dump(exclude_unset=True)
|
||||
if payload.get("db_password") in (None, "", DB_PASSWORD_PLACEHOLDER):
|
||||
payload.pop("db_password", None)
|
||||
try:
|
||||
await test_database_connection(payload)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
return MessageResponse(message="数据库连接测试成功")
|
||||
|
||||
|
||||
@router.get("/database/migrate/preview", response_model=DatabaseMigratePreviewResponse)
|
||||
async def preview_database_migration(
|
||||
source_db_path: str | None = None,
|
||||
_: User = Depends(require_admin),
|
||||
):
|
||||
return DatabaseMigratePreviewResponse(**await inspect_sqlite_source(source_db_path))
|
||||
|
||||
|
||||
@router.post("/database/migrate", response_model=DatabaseMigrateResponse)
|
||||
async def migrate_database_data(
|
||||
body: DatabaseMigrateRequest,
|
||||
_: User = Depends(require_admin),
|
||||
):
|
||||
payload = body.model_dump(exclude_unset=True)
|
||||
clear_target = bool(payload.pop("clear_target", False))
|
||||
if payload.get("db_password") in (None, "", DB_PASSWORD_PLACEHOLDER):
|
||||
payload.pop("db_password", None)
|
||||
try:
|
||||
await test_database_connection(payload)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=400, detail=f"目标库连接失败: {exc}") from exc
|
||||
try:
|
||||
result = await migrate_sqlite_to_target(payload, clear_target=clear_target)
|
||||
except FileNotFoundError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail=f"迁移失败: {exc}") from exc
|
||||
return DatabaseMigrateResponse(**result)
|
||||
Reference in New Issue
Block a user