Add Luxsin curve API client and validation functionality

- Introduced a new module `curve_client.py` for interacting with the Luxsin curve API, including functions for custom Base64 decoding and parametric EQ validation.
- Implemented error handling for API responses and validation checks for curve data.
- Removed outdated test files to streamline the codebase.
- Updated brand creation and update logic in `brands.py` to ensure brand names are validated and checked for duplicates.
- Enhanced model handling in `models.py` with new validation and push-to-search functionality, integrating curve validation before pushing to Meilisearch.
- Updated frontend components to support new validation and push processes, including user feedback for validation results and progress tracking.
- Changed favicon to SVG format for better scalability and appearance.
This commit is contained in:
yangy
2026-05-20 14:13:42 +08:00
parent 1564c8766d
commit fb1aad45af
12 changed files with 653 additions and 256 deletions
+125 -63
View File
@@ -1,4 +1,5 @@
from fastapi import APIRouter, Depends, HTTPException, Query, Body, UploadFile, File, Form
from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy.orm import Session
from sqlalchemy import asc, desc
from typing import List, Optional
@@ -6,6 +7,7 @@ import logging
from database import get_db
from models.model import Model
from response import ApiResponse, PageData
from curve_client import fetch_and_validate_curve
import requests
import os
import shutil
@@ -16,6 +18,15 @@ logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/models", tags=["models"])
class PushToSearchBody(BaseModel):
"""推送 Meilisearch 请求体;字段名 model_ids 需关闭 protected_namespaces 避免 Pydantic 警告。"""
model_config = ConfigDict(protected_namespaces=())
model_ids: List[int] = Field(..., min_length=1, description="型号 ID 列表")
# Meilisearch 配置(从环境变量读取)
MEILISEARCH_URL = os.getenv("MEILISEARCH_URL", "http://localhost:7700")
MEILISEARCH_API_KEY = os.getenv("MEILISEARCH_API_KEY", "")
@@ -260,69 +271,120 @@ def delete_model(model_id: int, db: Session = Depends(get_db)):
return ApiResponse(code=0, msg="error", data=None)
@router.post("/push-to-search", response_model=ApiResponse)
def push_to_search(
model_ids: List[int] = Body(..., embed=True, description="型号 ID 列表"),
db: Session = Depends(get_db)
):
"""推送型号数据到 Meilisearch"""
try:
if not model_ids:
return ApiResponse(code=0, msg="请选择要推送的型号", data=None)
# 查询选中的型号
models = db.query(Model).filter(Model.id.in_(model_ids)).all()
if not models:
return ApiResponse(code=0, msg="未找到选中的型号数据", data=None)
# 准备推送数据
push_data = []
for model in models:
model_dict = {
'id': model.id,
'brand_name': model.brand_name,
'name': model.name,
'rig': model.rig,
'form': model.form,
'source': model.source
}
# 只包含有值的字段
push_data.append({k: v for k, v in model_dict.items() if v is not None})
# 推送到 Meilisearch
headers = {
"Authorization": f"Bearer {MEILISEARCH_API_KEY}",
"Content-Type": "application/json"
}
# 如果索引不存在,先创建索引
try:
# 更新或添加文档
response = requests.post(
f"{MEILISEARCH_URL}/indexes/{MEILISEARCH_INDEX}/documents",
json=push_data,
headers=headers,
timeout=30
)
if response.status_code not in [200, 202]:
return ApiResponse(code=0, msg=f"推送到 Meilisearch 失败:{response.text}", data=None)
task_info = response.json()
return ApiResponse(
code=1,
msg="success",
data={
"pushed_count": len(push_data),
"task_uid": task_info.get("taskUid"),
"models": push_data
def _get_models_by_ids(model_ids: List[int], db: Session):
if not model_ids:
return None, ApiResponse(code=0, msg="请选择要推送的型号", data=None)
models = db.query(Model).filter(Model.id.in_(model_ids)).all()
if not models:
return None, ApiResponse(code=0, msg="未找到选中的型号数据", data=None)
return models, None
def _validate_models_curve(models) -> List[dict]:
validation_errors = []
for model in models:
ok, reason = fetch_and_validate_curve(
model.brand_name,
model.name,
model.form or "",
)
if not ok:
validation_errors.append(
{
"id": model.id,
"brand_name": model.brand_name,
"name": model.name,
"form": model.form,
"reason": reason,
}
)
except requests.exceptions.RequestException as e:
return ApiResponse(code=0, msg=f"连接 Meilisearch 失败:{str(e)}", data=None)
return validation_errors
def _build_meilisearch_documents(models) -> List[dict]:
push_data = []
for model in models:
model_dict = {
"id": model.id,
"brand_name": model.brand_name,
"name": model.name,
"rig": model.rig,
"form": model.form,
"source": model.source,
}
push_data.append({k: v for k, v in model_dict.items() if v is not None})
return push_data
def _push_documents_to_meilisearch(push_data: List[dict]) -> ApiResponse:
headers = {
"Authorization": f"Bearer {MEILISEARCH_API_KEY}",
"Content-Type": "application/json",
}
try:
response = requests.post(
f"{MEILISEARCH_URL}/indexes/{MEILISEARCH_INDEX}/documents",
json=push_data,
headers=headers,
timeout=30,
)
if response.status_code not in [200, 202]:
return ApiResponse(code=0, msg=f"推送到 Meilisearch 失败:{response.text}", data=None)
task_info = response.json()
return ApiResponse(
code=1,
msg="success",
data={
"pushed_count": len(push_data),
"task_uid": task_info.get("taskUid"),
"models": push_data,
},
)
except requests.exceptions.RequestException as e:
return ApiResponse(code=0, msg=f"连接 Meilisearch 失败:{str(e)}", data=None)
@router.post("/push-to-search/validate", response_model=ApiResponse)
def validate_push_to_search(body: PushToSearchBody, db: Session = Depends(get_db)):
"""推送前校验:拉取并验证曲线 parametric_eq 数据"""
try:
models, err = _get_models_by_ids(body.model_ids, db)
if err:
return err
validation_errors = _validate_models_curve(models)
if validation_errors:
names = "".join(
f"{e['brand_name']} {e['name']}" for e in validation_errors[:5]
)
suffix = "" if len(validation_errors) > 5 else ""
return ApiResponse(
code=0,
msg=f"曲线数据校验未通过:{names}{suffix}",
data={"errors": validation_errors, "validated_count": 0},
)
return ApiResponse(
code=1,
msg="success",
data={"validated_count": len(models)},
)
except Exception as e:
return ApiResponse(code=0, msg="error", data=None)
logger.error("validate_push_to_search failed: %s", e, exc_info=True)
return ApiResponse(code=0, msg=f"校验失败:{str(e)}", data=None)
@router.post("/push-to-search", response_model=ApiResponse)
def push_to_search(body: PushToSearchBody, db: Session = Depends(get_db)):
"""推送型号数据到 Meilisearch(需先通过 validate 接口)"""
try:
models, err = _get_models_by_ids(body.model_ids, db)
if err:
return err
push_data = _build_meilisearch_documents(models)
return _push_documents_to_meilisearch(push_data)
except Exception as e:
logger.error("push_to_search failed: %s", e, exc_info=True)
return ApiResponse(code=0, msg=f"推送失败:{str(e)}", data=None)