259 lines
8.3 KiB
Python
259 lines
8.3 KiB
Python
"""食谱系统信号源 API。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from typing import Optional
|
||
|
||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||
from sqlalchemy import select
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from ..core.logging import get_logger
|
||
from ..database import get_db
|
||
from ..dependencies.tenant import get_tenant_id
|
||
from ..models import ProjectSignalLink, Signal
|
||
from .signal_sources_models import RecipeBatchRequest, RecipeRecognitionRequest, RecipeRecognitionResponse
|
||
from .signal_sources_utils import resolve_target_project_id
|
||
|
||
router = APIRouter(prefix="/signal-sources", tags=["Signal Sources"])
|
||
logger = get_logger(__name__)
|
||
|
||
|
||
@router.post("/recipe/webhook", response_model=RecipeRecognitionResponse, summary="食谱系统 Webhook")
|
||
async def recipe_webhook(
|
||
request: RecipeRecognitionRequest,
|
||
project_id: Optional[int] = Query(None, description="项目ID,不传则自动查找食谱系统项目"),
|
||
db: AsyncSession = Depends(get_db),
|
||
tenant_id: int = Depends(get_tenant_id),
|
||
):
|
||
"""接收食谱系统的识别请求。"""
|
||
try:
|
||
target_project_id = await resolve_target_project_id(
|
||
db=db,
|
||
tenant_id=tenant_id,
|
||
project_id=project_id,
|
||
)
|
||
|
||
content_parts = [f"食物:{request.food_name}"]
|
||
if request.food_description:
|
||
content_parts.append(f"描述:{request.food_description}")
|
||
|
||
if request.calories or request.protein or request.fat or request.carbs or request.fiber:
|
||
nutrition = []
|
||
if request.calories:
|
||
nutrition.append(f"热量 {request.calories} kcal")
|
||
if request.protein:
|
||
nutrition.append(f"蛋白 {request.protein}g")
|
||
if request.fat:
|
||
nutrition.append(f"脂肪 {request.fat}g")
|
||
if request.carbs:
|
||
nutrition.append(f"碳水 {request.carbs}g")
|
||
if request.fiber:
|
||
nutrition.append(f"纤维 {request.fiber}g")
|
||
content_parts.append("营养:" + ", ".join(nutrition))
|
||
|
||
if request.category:
|
||
content_parts.append(f"分类:{request.category}")
|
||
if request.cuisine:
|
||
content_parts.append(f"菜系:{request.cuisine}")
|
||
content_parts.append(f"意图:{request.intent}")
|
||
|
||
content = "\n".join(content_parts)
|
||
|
||
intent_text = {
|
||
"analyze": "热量分析",
|
||
"save": "保存食谱",
|
||
"recommend": "推荐菜谱",
|
||
}.get(request.intent, request.intent)
|
||
|
||
title = f"🍽️ 食谱{intent_text}:{request.food_name}"
|
||
|
||
summary = f"食物:{request.food_name}"
|
||
if request.calories:
|
||
summary += f"\n热量:{request.calories} kcal"
|
||
if request.category:
|
||
summary += f"\n分类:{request.category}"
|
||
summary += f"\n意图:{intent_text}"
|
||
|
||
signal_metadata = {
|
||
"source": "recipe_system",
|
||
"intent": request.intent,
|
||
"category": request.category,
|
||
"cuisine": request.cuisine,
|
||
"nutrition": {
|
||
"calories": request.calories,
|
||
"protein": request.protein,
|
||
"fat": request.fat,
|
||
"carbs": request.carbs,
|
||
"fiber": request.fiber,
|
||
},
|
||
"image_url": request.image_url,
|
||
"user_name": request.user_name,
|
||
}
|
||
|
||
signal = Signal(
|
||
tenant_id=tenant_id,
|
||
source="recipe_system",
|
||
title=title,
|
||
content=content,
|
||
summary=summary,
|
||
signal_type="recipe_recognition",
|
||
priority="medium",
|
||
impact_score=0.5,
|
||
status="待处理",
|
||
processed=False,
|
||
strategic_analysis=json.dumps(signal_metadata, ensure_ascii=False),
|
||
)
|
||
|
||
db.add(signal)
|
||
await db.flush()
|
||
|
||
link = ProjectSignalLink(
|
||
tenant_id=tenant_id,
|
||
project_id=target_project_id,
|
||
signal_id=signal.id,
|
||
)
|
||
db.add(link)
|
||
|
||
await db.commit()
|
||
await db.refresh(signal)
|
||
|
||
return RecipeRecognitionResponse(
|
||
success=True,
|
||
signal_id=signal.id,
|
||
project_id=target_project_id,
|
||
message="信号创建成功并已关联到食谱系统项目",
|
||
details={
|
||
"signal_id": signal.id,
|
||
"project_id": target_project_id,
|
||
"title": title,
|
||
"status": signal.status,
|
||
},
|
||
)
|
||
|
||
except HTTPException:
|
||
await db.rollback()
|
||
raise
|
||
except Exception as exc:
|
||
await db.rollback()
|
||
logger.exception(
|
||
"recipe_webhook_create_signal_failed",
|
||
tenant_id=tenant_id,
|
||
project_id=project_id,
|
||
food_name=request.food_name,
|
||
error=str(exc),
|
||
)
|
||
raise HTTPException(status_code=500, detail=f"创建信号失败: {str(exc)}")
|
||
|
||
|
||
@router.get("/recipe/status/{signal_id}", summary="查询食谱信号状态")
|
||
async def get_recipe_signal_status(
|
||
signal_id: int,
|
||
db: AsyncSession = Depends(get_db),
|
||
tenant_id: int = Depends(get_tenant_id),
|
||
):
|
||
"""查询食谱识别信号的处理状态"""
|
||
result = await db.execute(
|
||
select(Signal).where(
|
||
Signal.id == signal_id,
|
||
Signal.tenant_id == tenant_id,
|
||
Signal.source == "recipe_system",
|
||
)
|
||
)
|
||
signal = result.scalar_one_or_none()
|
||
|
||
if not signal:
|
||
raise HTTPException(status_code=404, detail="信号不存在")
|
||
|
||
return {
|
||
"signal_id": signal.id,
|
||
"title": signal.title,
|
||
"status": signal.status,
|
||
"processed": signal.processed,
|
||
"summary": signal.summary,
|
||
}
|
||
|
||
|
||
@router.post("/recipe/batch", response_model=RecipeRecognitionResponse, summary="批量导入食谱")
|
||
async def recipe_batch_import(
|
||
request: RecipeBatchRequest,
|
||
project_id: Optional[int] = Query(None, description="项目ID"),
|
||
db: AsyncSession = Depends(get_db),
|
||
tenant_id: int = Depends(get_tenant_id),
|
||
):
|
||
"""批量导入食谱记录,每个都会创建信号"""
|
||
try:
|
||
target_project_id = await resolve_target_project_id(
|
||
db=db,
|
||
tenant_id=tenant_id,
|
||
project_id=project_id,
|
||
)
|
||
|
||
signal_ids = []
|
||
for recipe in request.recipes:
|
||
content = f"食物:{recipe.food_name}"
|
||
if recipe.calories:
|
||
content += f"\n热量:{recipe.calories} kcal"
|
||
|
||
title = f"🍽️ 食谱记录:{recipe.food_name}"
|
||
|
||
metadata = {
|
||
"source": "recipe_system",
|
||
"intent": recipe.intent,
|
||
"nutrition": {
|
||
"calories": recipe.calories,
|
||
"protein": recipe.protein,
|
||
"fat": recipe.fat,
|
||
"carbs": recipe.carbs,
|
||
},
|
||
}
|
||
|
||
signal = Signal(
|
||
tenant_id=tenant_id,
|
||
source="recipe_system",
|
||
title=title,
|
||
content=content,
|
||
summary=f"食物:{recipe.food_name}",
|
||
signal_type="recipe_recognition",
|
||
priority="low",
|
||
impact_score=0.3,
|
||
status="待处理",
|
||
processed=True,
|
||
strategic_analysis=json.dumps(metadata),
|
||
)
|
||
db.add(signal)
|
||
await db.flush()
|
||
|
||
link = ProjectSignalLink(
|
||
tenant_id=tenant_id,
|
||
project_id=target_project_id,
|
||
signal_id=signal.id,
|
||
)
|
||
db.add(link)
|
||
signal_ids.append(signal.id)
|
||
|
||
await db.commit()
|
||
|
||
return RecipeRecognitionResponse(
|
||
success=True,
|
||
signal_id=len(signal_ids),
|
||
project_id=target_project_id,
|
||
message=f"成功导入 {len(signal_ids)} 条食谱记录",
|
||
details={"signal_count": len(signal_ids)},
|
||
)
|
||
|
||
except HTTPException:
|
||
await db.rollback()
|
||
raise
|
||
except Exception as exc:
|
||
await db.rollback()
|
||
logger.exception(
|
||
"recipe_batch_import_failed",
|
||
tenant_id=tenant_id,
|
||
project_id=project_id,
|
||
recipe_count=len(request.recipes),
|
||
error=str(exc),
|
||
)
|
||
raise HTTPException(status_code=500, detail=f"批量导入失败: {str(exc)}")
|