Showing
6 changed files
with
156 additions
and
20 deletions
| 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"] |
api/data_router.py
0 → 100644
| 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, |
-
Please register or login to post a comment