153 lines
5.0 KiB
Python
153 lines
5.0 KiB
Python
"""龙虾运行时配置:读取(DB 优先 → .env 兜底)+ 更新。"""
|
||
from __future__ import annotations
|
||
|
||
from dataclasses import dataclass
|
||
|
||
from sqlalchemy import select
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from app.core.config import settings
|
||
from app.core.crypto import decrypt, encrypt, mask
|
||
from app.models import LobsterConfig
|
||
from app.schemas.lobster_config import LobsterConfigRead, LobsterConfigUpdate
|
||
|
||
|
||
@dataclass
|
||
class EffectiveLobsterConfig:
|
||
"""运行时真正生效的值(已合并 DB + .env)。"""
|
||
|
||
api_base: str
|
||
api_key: str
|
||
callback_secret: str
|
||
timeout_seconds: int
|
||
poll_interval_seconds: int
|
||
|
||
# 各字段的来源:db / env / unset,仅用于展示
|
||
api_base_source: str = "env"
|
||
api_key_source: str = "env"
|
||
callback_secret_source: str = "env"
|
||
timeout_source: str = "env"
|
||
poll_interval_source: str = "env"
|
||
|
||
@property
|
||
def is_mock(self) -> bool:
|
||
return not self.api_base or not self.api_key
|
||
|
||
|
||
async def _get_or_create_row(db: AsyncSession) -> LobsterConfig:
|
||
row = (await db.execute(select(LobsterConfig).where(LobsterConfig.id == 1))).scalar_one_or_none()
|
||
if row is None:
|
||
row = LobsterConfig(id=1)
|
||
db.add(row)
|
||
await db.flush()
|
||
return row
|
||
|
||
|
||
async def load_effective(db: AsyncSession) -> EffectiveLobsterConfig:
|
||
row = (await db.execute(select(LobsterConfig).where(LobsterConfig.id == 1))).scalar_one_or_none()
|
||
|
||
# DB 字段(可能 None)
|
||
db_api_base = (row.api_base if row else None) or None
|
||
db_api_key = decrypt(row.api_key_cipher) if row and row.api_key_cipher else None
|
||
db_callback_secret = (
|
||
decrypt(row.callback_secret_cipher) if row and row.callback_secret_cipher else None
|
||
)
|
||
db_timeout = row.timeout_seconds if row else None
|
||
db_poll = row.poll_interval_seconds if row else None
|
||
|
||
# env 兜底
|
||
env_api_base = settings.LOBSTER_API_BASE or ""
|
||
env_api_key = settings.LOBSTER_API_KEY or ""
|
||
env_callback_secret = settings.LOBSTER_CALLBACK_SECRET or ""
|
||
env_timeout = settings.LOBSTER_TIMEOUT_SECONDS or 30
|
||
env_poll = settings.LOBSTER_POLL_INTERVAL_SECONDS or 5
|
||
|
||
api_base = db_api_base or env_api_base
|
||
api_base_source = "db" if db_api_base else ("env" if env_api_base else "unset")
|
||
|
||
api_key = db_api_key or env_api_key
|
||
api_key_source = "db" if db_api_key else ("env" if env_api_key else "unset")
|
||
|
||
callback = db_callback_secret or env_callback_secret
|
||
callback_source = (
|
||
"db" if db_callback_secret else ("env" if env_callback_secret else "unset")
|
||
)
|
||
|
||
timeout = db_timeout or env_timeout
|
||
timeout_source = "db" if db_timeout else "env"
|
||
|
||
poll = db_poll or env_poll
|
||
poll_source = "db" if db_poll else "env"
|
||
|
||
return EffectiveLobsterConfig(
|
||
api_base=api_base,
|
||
api_key=api_key,
|
||
callback_secret=callback,
|
||
timeout_seconds=timeout,
|
||
poll_interval_seconds=poll,
|
||
api_base_source=api_base_source,
|
||
api_key_source=api_key_source,
|
||
callback_secret_source=callback_source,
|
||
timeout_source=timeout_source,
|
||
poll_interval_source=poll_source,
|
||
)
|
||
|
||
|
||
async def read(db: AsyncSession) -> LobsterConfigRead:
|
||
eff = await load_effective(db)
|
||
note = None
|
||
row = (await db.execute(select(LobsterConfig).where(LobsterConfig.id == 1))).scalar_one_or_none()
|
||
if row:
|
||
note = row.note
|
||
return LobsterConfigRead(
|
||
api_base=eff.api_base or "",
|
||
api_base_source=eff.api_base_source,
|
||
api_key_mask=mask(eff.api_key) if eff.api_key else "",
|
||
api_key_source=eff.api_key_source,
|
||
has_api_key=bool(eff.api_key),
|
||
callback_secret_mask=mask(eff.callback_secret) if eff.callback_secret else "",
|
||
callback_secret_source=eff.callback_secret_source,
|
||
has_callback_secret=bool(eff.callback_secret),
|
||
timeout_seconds=eff.timeout_seconds,
|
||
timeout_source=eff.timeout_source,
|
||
poll_interval_seconds=eff.poll_interval_seconds,
|
||
poll_interval_source=eff.poll_interval_source,
|
||
mode="real" if not eff.is_mock else "mock",
|
||
note=note,
|
||
)
|
||
|
||
|
||
async def update(db: AsyncSession, data: LobsterConfigUpdate) -> LobsterConfigRead:
|
||
row = await _get_or_create_row(db)
|
||
changes = data.model_dump(exclude_unset=True)
|
||
|
||
# api_key / callback_secret 空串表示清空 → fallback env
|
||
if "api_key" in changes:
|
||
v = changes.pop("api_key")
|
||
if v is None:
|
||
pass # 不改
|
||
elif v == "":
|
||
row.api_key_cipher = None
|
||
else:
|
||
row.api_key_cipher = encrypt(v)
|
||
|
||
if "callback_secret" in changes:
|
||
v = changes.pop("callback_secret")
|
||
if v is None:
|
||
pass
|
||
elif v == "":
|
||
row.callback_secret_cipher = None
|
||
else:
|
||
row.callback_secret_cipher = encrypt(v)
|
||
|
||
# api_base 空串 = 清空回落 env
|
||
if "api_base" in changes:
|
||
v = changes.pop("api_base")
|
||
row.api_base = v if v else None
|
||
|
||
for k, v in changes.items():
|
||
setattr(row, k, v)
|
||
|
||
await db.commit()
|
||
return await read(db)
|