wangdongxu

jira:NYJ-1460 desc:接口代码

1 -# 应用镜像 - 基于基础镜像部署代码  
2 FROM aigc-embedding-service-base:latest 1 FROM aigc-embedding-service-base:latest
  2 +ENV PROJECT_ROOT=/app ENV=docker
3 3
4 -# 设置工作目录  
5 WORKDIR /app 4 WORKDIR /app
6 -  
7 -# 复制应用代码(包含配置文件)  
8 COPY app/ ./app/ 5 COPY app/ ./app/
9 6
10 -# 暴露端口  
11 EXPOSE 8000 7 EXPOSE 8000
12 -  
13 -# 启动命令  
14 -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] 8 +CMD ["python", "main.py"]
  1 +from typing import List, Optional, Literal, Union, Dict, Any, Annotated
  2 +from pydantic import BaseModel, Field
  3 +from fastapi import APIRouter
  4 +from api.resp_bean import RespBean, success
  5 +
  6 +router = APIRouter()
  7 +
  8 +
  9 +# ============== 请求/响应模型定义 ==============
  10 +
  11 +class FaceKwargs(BaseModel):
  12 + """人脸类型扩展字段"""
  13 + person_id: Optional[str] = Field(None, description="人物id")
  14 + person_name: Optional[str] = Field(None, description="人物姓名")
  15 +
  16 +
  17 +class SportShotKwargs(BaseModel):
  18 + """体育镜头类型扩展字段"""
  19 + match_name: Optional[str] = Field(None, description="比赛名称")
  20 + person_name: Optional[str] = Field(None, description="人名")
  21 + video_desc: Optional[str] = Field(None, description="视频描述")
  22 + shot_cls: Optional[str] = Field(None, description="镜头分类")
  23 + match_id: Optional[str] = Field(None, description="比赛id")
  24 + program_id: Optional[str] = Field(None, description="节目id")
  25 +
  26 +
  27 +# 定义类型映射,用于根据type自动转换kwargs
  28 +KwargsType = Union[FaceKwargs, SportShotKwargs]
  29 +
  30 +
  31 +class PutDataRequest(BaseModel):
  32 + """写入数据请求体"""
  33 + id: str = Field(..., description="唯一键id")
  34 + embedding: List[float] = Field(..., description="向量")
  35 + embedding_version: str = Field(..., description="向量版本 小写数字下划线组成")
  36 + kwargs: Optional[Dict[str, Any]] = Field(None, description="扩展字段,根据type不同字段不同")
  37 +
  38 + def get_typed_kwargs(self, type: str) -> Optional[KwargsType]:
  39 + """根据type自动转换kwargs为具体类型"""
  40 + if self.kwargs is None:
  41 + return None
  42 + type_map = {
  43 + 'face': FaceKwargs,
  44 + 'sport_shot': SportShotKwargs
  45 + }
  46 + kwargs_class = type_map.get(type)
  47 + if kwargs_class:
  48 + return kwargs_class(**self.kwargs)
  49 + return None
  50 +
  51 +
  52 +class DelByIdRequest(BaseModel):
  53 + """删除数据请求体"""
  54 + ids: List[str] = Field(..., description="id 集合")
  55 +
  56 +
  57 +class FilterItem(BaseModel):
  58 + """过滤条件项"""
  59 + name: str = Field(..., description="字段名")
  60 + value: List = Field(..., description="字段值")
  61 + opt: Optional[Literal["eq", "neq", "lt", "gt", "lte", "gte", "like", "in"]] = Field(
  62 + None, description="操作类型: eq相等, neq不等, lt小于, gt大于, lte小于等于, gte大于等于, like模糊匹配, in内"
  63 + )
  64 +
  65 +
  66 +class SearchRequest(BaseModel):
  67 + """检索数据请求体"""
  68 + embedding: List[float] = Field(..., description="向量")
  69 + embedding_version: str = Field(..., description="向量版本 小写数字下划线组成")
  70 + topk: int = Field(..., description="top k")
  71 + filters: Optional[List[FilterItem]] = Field(None, description="过滤字段")
  72 +
  73 +
  74 +class SearchResultItem(BaseModel):
  75 + """检索结果单项"""
  76 + id: str = Field(..., description="id")
  77 + kwargs: dict = Field(..., description="扩展字段")
  78 + score: float = Field(..., description="分值")
  79 +
  80 +
  81 +class SearchResponseData(BaseModel):
  82 + """检索响应数据"""
  83 + type: str = Field(..., description="type")
  84 + data_list: List[SearchResultItem] = Field(..., description="topk结果列表")
  85 +
  86 +
  87 +# ============== 接口定义 ==============
  88 +
  89 +@router.put("/{type}/put", response_model=RespBean, tags=["数据服务"], summary="写入数据")
  90 +@router.post("/{type}/put", response_model=RespBean, tags=["数据服务"], summary="写入数据")
  91 +async def put_data(type: Literal["face", "sport_shot"], request: PutDataRequest):
  92 + """
  93 + 写入数据
  94 + """
  95 +
  96 + pass
  97 + return success()
  98 +
  99 +
  100 +@router.delete("/{type}/del_by_id", response_model=RespBean, tags=["数据服务"], summary="删除数据")
  101 +@router.post("/{type}/del_by_id", response_model=RespBean, tags=["数据服务"], summary="删除数据")
  102 +async def del_by_id(type: Literal["face", "sport_shot"], request: DelByIdRequest):
  103 + """
  104 + 删除数据
  105 +
  106 + - **type**: 数据类型 (face-人脸, sport_shot-体育镜头)
  107 + - **ids**: id集合
  108 + """
  109 + # TODO: 实现数据删除逻辑
  110 + pass
  111 + return success()
  112 +
  113 +
  114 +
  115 +@router.post("/{type}/search", response_model=RespBean[SearchResponseData], tags=["数据服务"], summary="检索数据")
  116 +async def search_data(type: Literal["face", "sport_shot"], request: SearchRequest):
  117 + """
  118 + 检索数据
  119 +
  120 + - **type**: 数据类型 (face-人脸, sport_shot-体育镜头)
  121 + - **embedding**: 查询向量
  122 + - **embedding_version**: 向量版本
  123 + - **topk**: 返回top k条结果
  124 + - **filters**: 过滤条件列表
  125 + """
  126 + # TODO: 实现数据检索逻辑
  127 + pass
  128 + return success()
@@ -11,7 +11,6 @@ from config import settings @@ -11,7 +11,6 @@ from config import settings
11 11
12 class LoggingMiddleware(BaseHTTPMiddleware): 12 class LoggingMiddleware(BaseHTTPMiddleware):
13 13
14 - # 日志记录路径前缀  
15 async def dispatch(self, request: Request, call_next): 14 async def dispatch(self, request: Request, call_next):
16 # 获取请求信息 15 # 获取请求信息
17 method = request.method 16 method = request.method
@@ -40,11 +39,6 @@ class LoggingMiddleware(BaseHTTPMiddleware): @@ -40,11 +39,6 @@ class LoggingMiddleware(BaseHTTPMiddleware):
40 except Exception: 39 except Exception:
41 pass 40 pass
42 41
43 - # 记录请求日志  
44 - logger.info(f"→ HTTP Request | {method} {url} | Client: {client_host}")  
45 - if request_body:  
46 - logger.info(f" Request Body: {request_body}")  
47 -  
48 # 处理请求 42 # 处理请求
49 response = await call_next(request) 43 response = await call_next(request)
50 44
@@ -77,11 +71,22 @@ class LoggingMiddleware(BaseHTTPMiddleware): @@ -77,11 +71,22 @@ class LoggingMiddleware(BaseHTTPMiddleware):
77 except Exception: 71 except Exception:
78 pass 72 pass
79 73
80 - # 记录响应日志 74 + # 构建单条日志记录
81 status_code = response.status_code 75 status_code = response.status_code
82 - logger.info(f"← HTTP Response | {method} {url} | Status: {status_code} | Time: {process_time:.3f}s") 76 + log_data = {
  77 + "method": method,
  78 + "url": url,
  79 + "client": client_host,
  80 + "status": status_code,
  81 + "time": f"{process_time:.3f}s"
  82 + }
  83 +
  84 + if request_body:
  85 + log_data["request_body"] = request_body
83 if response_body: 86 if response_body:
84 - logger.info(f" Response Body: {response_body}") 87 + log_data["response_body"] = response_body
  88 +
  89 + logger.info(json.dumps(log_data, ensure_ascii=False))
85 90
86 return response 91 return response
87 else: 92 else:
1 from fastapi import APIRouter 1 from fastapi import APIRouter
2 from config import settings 2 from config import settings
  3 +from api.data_router import router as data_router
3 4
4 api_router = APIRouter(prefix=settings.url_prefix) 5 api_router = APIRouter(prefix=settings.url_prefix)
5 6
6 7
7 def register_router(router: APIRouter, prefix: str): 8 def register_router(router: APIRouter, prefix: str):
8 api_router.include_router(router, prefix=prefix) 9 api_router.include_router(router, prefix=prefix)
  10 +
  11 +
  12 +# 注册数据路由
  13 +register_router(data_router, prefix="/data")
1 from aabd.base.cfg_loader import load_yaml_by_file_with_env 1 from aabd.base.cfg_loader import load_yaml_by_file_with_env
2 from aabd.base.enhance_dict import EnhanceDict, read_prefixed_env_vars 2 from aabd.base.enhance_dict import EnhanceDict, read_prefixed_env_vars
3 from pathlib import Path 3 from pathlib import Path
4 - 4 +from logging import getLogger
  5 +logger = getLogger(__name__)
5 cfg_dir = Path(__file__).parent.absolute() / 'cfg' 6 cfg_dir = Path(__file__).parent.absolute() / 'cfg'
6 settings = EnhanceDict(load_yaml_by_file_with_env((cfg_dir / 'config.yaml').as_posix())) 7 settings = EnhanceDict(load_yaml_by_file_with_env((cfg_dir / 'config.yaml').as_posix()))
7 -settings.update(read_prefixed_env_vars('APP_')) 8 +settings.update(read_prefixed_env_vars('EMB_'))
  9 +
  10 +logger.info(f'Settings: {settings}')
@@ -50,8 +50,9 @@ config_fastapi(app) @@ -50,8 +50,9 @@ config_fastapi(app)
50 if __name__ == "__main__": 50 if __name__ == "__main__":
51 import uvicorn 51 import uvicorn
52 52
  53 + logger.info(f'docs_url: http://127.0.0.1:{settings.port}/docs')
53 uvicorn.run( 54 uvicorn.run(
54 - "main:app", 55 + app,
55 host='0.0.0.0', 56 host='0.0.0.0',
56 port=settings.port, 57 port=settings.port,
57 reload=settings.debug, 58 reload=settings.debug,