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:
小鱼开发
2026-04-30 16:20:32 +08:00
parent 75b1e20633
commit 8cb9e2da12
4 changed files with 72 additions and 12 deletions
+2 -2
View File
@@ -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}")
+14
View File
@@ -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):
"""素材条目"""
+25 -5
View File
@@ -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());