feat: reload_config 返回错误详情 + 远程配置增加 schema 校验
6. reload_config() 吞掉异常详情: - 返回类型从 bool 改为 tuple[bool, str],包含具体错误信息 - API 层将错误详情返回给前端,便于排查 - 异常统一记录到日志 8. 远程配置无 schema 校验: - 后端新增 MaterialsConfig Pydantic Model 校验 materials.json 结构 - load_config() 远程/本地配置均先校验再应用,格式错误时抛出明确异常 - 前端 CoverDesign 新增 isValidBgConfig() 运行时校验 bg-config.json - 校验失败时优雅降级到本地配置,避免页面白屏
This commit is contained in:
@@ -40,7 +40,7 @@ async def reload_materials_config():
|
||||
从远程 CDN 拉取最新配置,失败则 fallback 到本地文件。
|
||||
新增素材后调用此接口即可生效,无需重启服务。
|
||||
"""
|
||||
ok = reload_config()
|
||||
ok, error_msg = reload_config()
|
||||
if ok:
|
||||
return success_response(data=True, message="素材配置已重新加载")
|
||||
return success_response(data=False, message="素材配置重载失败")
|
||||
return success_response(data=False, message=f"素材配置重载失败: {error_msg}")
|
||||
|
||||
@@ -6,6 +6,20 @@
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class MaterialEntry(BaseModel):
|
||||
"""素材配置条目"""
|
||||
|
||||
url: str = Field(description="素材 URL(远程或本地路径)")
|
||||
|
||||
|
||||
class MaterialsConfig(BaseModel):
|
||||
"""素材库配置 Schema(用于校验远程/本地 JSON)"""
|
||||
|
||||
version: str = Field(default="1.0", description="配置版本")
|
||||
keywords: dict[str, str] = Field(description="关键词到分类 slug 的映射")
|
||||
materials: dict[str, list[MaterialEntry]] = Field(description="分类 slug 到素材列表的映射")
|
||||
|
||||
|
||||
class MaterialInfo(BaseModel):
|
||||
"""素材条目"""
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@ import re
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
from pydantic import ValidationError
|
||||
|
||||
from app.schemas.materials import MaterialsConfig
|
||||
|
||||
# 正则:从文件名中提取时长,如 plumbing_10s_a23f8fcb.mp4 → 10
|
||||
_DURATION_RE = re.compile(r"_(\d+)s_")
|
||||
@@ -59,6 +62,16 @@ def _apply_config(data: dict) -> None:
|
||||
_materials[slug] = parsed
|
||||
|
||||
|
||||
def _validate_config(data: dict) -> dict:
|
||||
"""使用 Pydantic Schema 校验配置结构,失败时抛出 ValidationError"""
|
||||
try:
|
||||
validated = MaterialsConfig.model_validate(data)
|
||||
return validated.model_dump()
|
||||
except ValidationError as e:
|
||||
logger.error(f"素材配置格式校验失败: {e}")
|
||||
raise ValueError(f"素材配置格式错误: {e}") from e
|
||||
|
||||
|
||||
def load_config() -> None:
|
||||
"""加载素材配置:优先远程 CDN,fallback 本地文件"""
|
||||
global _keywords, _materials
|
||||
@@ -69,6 +82,7 @@ def load_config() -> None:
|
||||
response = client.get(REMOTE_CONFIG_URL)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
_validate_config(data)
|
||||
_apply_config(data)
|
||||
return
|
||||
except Exception as e:
|
||||
@@ -83,16 +97,22 @@ def load_config() -> None:
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
_validate_config(data)
|
||||
_apply_config(data)
|
||||
|
||||
|
||||
def reload_config() -> bool:
|
||||
"""手动重新加载配置(用于更新素材后即时生效)"""
|
||||
def reload_config() -> tuple[bool, str]:
|
||||
"""手动重新加载配置(用于更新素材后即时生效)
|
||||
|
||||
Returns:
|
||||
(是否成功, 错误信息)。成功时错误信息为空字符串。
|
||||
"""
|
||||
try:
|
||||
load_config()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
return True, ""
|
||||
except Exception as e:
|
||||
logger.warning(f"素材配置重载失败: {e}")
|
||||
return False, str(e)
|
||||
|
||||
|
||||
def match_material(
|
||||
|
||||
@@ -120,10 +120,26 @@ export default function CoverDesign() {
|
||||
const REMOTE_BG_CONFIG_URL = 'https://img.liche.cn/meijiaka-zy/bg-config.json';
|
||||
const LOCAL_BG_CONFIG_URL = '/bg-config.json';
|
||||
|
||||
/** 校验背景图配置结构 */
|
||||
function isValidBgConfig(data: unknown): data is { backgrounds: BgImage[] } {
|
||||
if (typeof data !== 'object' || data === null) return false;
|
||||
const raw = data as Record<string, unknown>;
|
||||
const backgrounds = raw.backgrounds;
|
||||
if (!Array.isArray(backgrounds)) return false;
|
||||
return backgrounds.every(
|
||||
(b) =>
|
||||
typeof b === 'object' &&
|
||||
b !== null &&
|
||||
typeof (b as Record<string, unknown>).id === 'string' &&
|
||||
typeof (b as Record<string, unknown>).src === 'string' &&
|
||||
typeof (b as Record<string, unknown>).name === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
// 加载背景图配置(优先远程,fallback 本地)
|
||||
useEffect(() => {
|
||||
const loadConfig = async () => {
|
||||
let data: { backgrounds?: BgImage[] } | null = null;
|
||||
let data: unknown = null;
|
||||
// 1. 尝试远程配置(5秒超时)
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
@@ -135,25 +151,35 @@ export default function CoverDesign() {
|
||||
clearTimeout(timeoutId);
|
||||
if (res.ok) {
|
||||
data = await res.json();
|
||||
console.log('[CoverDesign] 使用远程背景图配置');
|
||||
if (isValidBgConfig(data)) {
|
||||
console.log('[CoverDesign] 使用远程背景图配置');
|
||||
} else {
|
||||
console.warn('[CoverDesign] 远程配置格式校验失败,尝试本地 fallback');
|
||||
data = null;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[CoverDesign] 远程配置加载失败:', err);
|
||||
}
|
||||
// 2. Fallback 本地配置
|
||||
if (!data?.backgrounds) {
|
||||
if (!data) {
|
||||
try {
|
||||
const res = await fetch(LOCAL_BG_CONFIG_URL);
|
||||
if (res.ok) {
|
||||
data = await res.json();
|
||||
console.log('[CoverDesign] 使用本地背景图配置');
|
||||
if (isValidBgConfig(data)) {
|
||||
console.log('[CoverDesign] 使用本地背景图配置');
|
||||
} else {
|
||||
console.error('[CoverDesign] 本地配置格式校验失败');
|
||||
data = null;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[CoverDesign] 本地配置加载失败:', err);
|
||||
}
|
||||
}
|
||||
// 3. 应用配置
|
||||
if (data?.backgrounds) {
|
||||
if (isValidBgConfig(data)) {
|
||||
const list: BgImage[] = data.backgrounds;
|
||||
setBgList(list);
|
||||
const picked = pickThree(list, new Set());
|
||||
|
||||
Reference in New Issue
Block a user