From 25fb6ba9ce3c062823d1ab3a48c09757ff1aea0a Mon Sep 17 00:00:00 2001 From: mamamiyear Date: Tue, 18 Nov 2025 00:14:07 +0800 Subject: [PATCH] feat: support upload image api - support upload and delete image of people - support uploads any image and get link after login --- src/utils/error.py | 3 ++ src/utils/obs.py | 91 ++++++++++++++++++++++++++++------ src/web/api.py | 119 ++++++++++++++++++++++++++++++++++++--------- 3 files changed, 174 insertions(+), 39 deletions(-) diff --git a/src/utils/error.py b/src/utils/error.py index 022bb64..c7e5c7f 100644 --- a/src/utils/error.py +++ b/src/utils/error.py @@ -7,6 +7,9 @@ class ErrorCode(Enum): SUCCESS = 0 MODEL_ERROR = 1000 RLDB_ERROR = 2100 + OBS_ERROR = 3100 + OBS_INPUT_ERROR = 3102 + OBS_SERVICE_ERROR = 3103 class error(Protocol): _error_code: int = 0 diff --git a/src/utils/obs.py b/src/utils/obs.py index 75c6ee0..d49fd7e 100644 --- a/src/utils/obs.py +++ b/src/utils/obs.py @@ -4,11 +4,13 @@ import logging from typing import Protocol import qiniu import requests + +from .error import ErrorCode, error from .config import get_instance as get_config class OBS(Protocol): - def Put(self, obs_path: str, content: bytes) -> str: + def put(self, obs_path: str, content: bytes) -> str: """ 上传文件到OBS @@ -21,7 +23,7 @@ class OBS(Protocol): """ ... - def Get(self, obs_path: str) -> bytes: + def get(self, obs_path: str) -> bytes: """ 从OBS下载文件 @@ -33,7 +35,7 @@ class OBS(Protocol): """ ... - def List(self, obs_path: str) -> list: + def list(self, obs_path: str) -> list: """ 列出OBS目录下的所有文件 @@ -45,7 +47,7 @@ class OBS(Protocol): """ ... - def Del(self, obs_path: str) -> bool: + def delete(self, obs_path: str) -> error: """ 删除OBS文件 @@ -57,7 +59,7 @@ class OBS(Protocol): """ ... - def Link(self, obs_path: str) -> str: + def get_link(self, obs_path: str) -> str: """ 获取OBS文件链接 @@ -68,6 +70,31 @@ class OBS(Protocol): str: OBS文件链接 """ ... + + def delete_by_link(self, obs_link: str) -> error: + """ + 根据OBS文件链接删除文件 + + Args: + obs_link (str): OBS文件链接 + + Returns: + bool: 是否删除成功 + """ + ... + + def get_obs_path_by_link(self, obs_link: str) -> (str, error): + """ + 从OBS文件链接获取OBS路径 + + Args: + obs_link (str): OBS文件链接 + + Returns: + str: OBS文件路径 + error: 错误信息 + """ + ... class Koodo: @@ -82,7 +109,7 @@ class Koodo: self.bucket = qiniu.BucketManager(self.auth) pass - def Put(self, obs_path: str, content: bytes) -> str: + def put(self, obs_path: str, content: bytes) -> str: """ 上传文件到OBS @@ -103,7 +130,7 @@ class Koodo: logging.info(f"文件 {obs_path} 上传成功, OBS路径: {full_path}") return f"{self.outer_domain}/{full_path}" - def Get(self, obs_path: str) -> bytes: + def get(self, obs_path: str) -> bytes: """ 从OBS下载文件 @@ -121,7 +148,7 @@ class Koodo: return None return resp.content - def List(self, prefix: str = "") -> list[str]: + def list(self, prefix: str = "") -> list[str]: """ 列出OBS目录下的所有文件 @@ -143,7 +170,7 @@ class Koodo: # logging.debug(f"info: {info}") return keys - def Del(self, obs_path: str) -> bool: + def delete(self, obs_path: str) -> error: """ 删除OBS文件 @@ -151,17 +178,17 @@ class Koodo: obs_path (str): OBS文件路径 Returns: - bool: 是否删除成功 + error: 删除结果 """ ret, info = self.bucket.delete(self.bucket_name, f"{self.prefix_path}{obs_path}") - logging.debug(f"文件 {obs_path} 删除 OBS, 结果: {ret}, 状态码: {info.status_code}, 错误信息: {info.text_body}") + logging.debug(f"文件 {self.prefix_path}{obs_path} 删除 OBS, 结果: {ret}, 状态码: {info.status_code}, 错误信息: {info.text_body}") if ret is None or info.status_code != 200: logging.error(f"文件 {obs_path} 删除 OBS 失败, 错误信息: {info.text_body}") - return False + return error(error_code=ErrorCode.OBS_INPUT_ERROR, error_info=f"文件 {self.prefix_path}{obs_path} 删除 OBS 失败, 错误信息: {info.text_body}") logging.info(f"文件 {obs_path} 删除 OBS 成功") - return True + return error(error_code=ErrorCode.SUCCESS, error_info="success") - def Link(self, obs_path: str) -> str: + def get_link(self, obs_path: str) -> str: """ 获取OBS文件链接 @@ -173,6 +200,38 @@ class Koodo: """ return f"{self.outer_domain}/{self.prefix_path}{obs_path}" + def delete_by_link(self, obs_link: str) -> error: + """ + 根据OBS文件链接删除文件 + + Args: + obs_link (str): OBS文件链接 + + Returns: + error: 删除结果 + """ + obs_path, err = self.get_obs_path_by_link(obs_link) + if not err.success: + return err + return self.delete(obs_path) + + def get_obs_path_by_link(self, obs_link: str) -> (str, error): + """ + 从OBS文件链接获取OBS路径 + + Args: + obs_link (str): OBS文件链接 + + Returns: + str: OBS文件路径 + error: 错误信息 + """ + if not obs_link.startswith(f"{self.outer_domain}/{self.prefix_path}"): + logging.error(f"文件 {obs_link} 不是 OBS 文件链接") + return "", error(error_code=ErrorCode.OBS_INPUT_ERROR, error_info=f"文件 {obs_link} 不是 OBS 文件链接") + obs_path = obs_link[len(self.outer_domain) + len(self.prefix_path) + 1:] + return obs_path, error(error_code=ErrorCode.SUCCESS, error_info="success") + _obs_instance: OBS = None @@ -213,8 +272,8 @@ if __name__ == "__main__": # print(f"文件 {obs_path} 链接: {link}") # 列出OBS目录下的所有文件 - keys = obs.List("") + keys = obs.list("") print(f"OBS 目录下的所有文件: {keys}") for key in keys: - link = obs.Del(key) + link = obs.delete(key) print(f"文件 {key} 删除 OBS 成功: {link}") diff --git a/src/web/api.py b/src/web/api.py index 3242637..95fadff 100644 --- a/src/web/api.py +++ b/src/web/api.py @@ -38,38 +38,52 @@ async def ping(): class PostInputRequest(BaseModel): text: str -@api.post("/api/recognition/input") -async def post_input(request: PostInputRequest): - people = extract_people(request.text) - resp = BaseResponse(error_code=0, error_info="success") - resp.data = people.to_dict() - return resp - -@api.post("/api/recognition/image") -async def post_input_image(image: UploadFile = File(...)): +@authorized_router.post("/api/upload/image") +async def post_upload_image(image: UploadFile = File(...)): # 实现上传图片的处理 # 保存上传的图片文件 # 生成唯一的文件名 file_extension = os.path.splitext(image.filename)[1] unique_filename = f"{uuid.uuid4()}{file_extension}" - - # 确保uploads目录存在 - os.makedirs("uploads", exist_ok=True) - + # 保存文件到对象存储 file_path = f"uploads/{unique_filename}" obs_util = obs.get_instance() - obs_util.Put(file_path, await image.read()) - + obs_util.put(file_path, await image.read()) + # 获取对象存储外链 - obs_url = obs_util.Link(file_path) + obs_url = obs_util.get_link(file_path) + return BaseResponse(error_code=0, error_info="success", data=obs_url) + +@authorized_router.post("/api/recognition/input") +async def post_recognition_input(request: PostInputRequest): + people = extract_people(request.text) + resp = BaseResponse(error_code=0, error_info="success") + resp.data = people.to_dict() + return resp + +@authorized_router.post("/api/recognition/image") +async def post_recognition_image(image: UploadFile = File(...)): + # 实现上传图片的处理 + # 保存上传的图片文件 + # 生成唯一的文件名 + file_extension = os.path.splitext(image.filename)[1] + unique_filename = f"{uuid.uuid4()}{file_extension}" + + # 保存文件到对象存储 + file_path = f"uploads/{unique_filename}" + obs_util = obs.get_instance() + obs_util.put(file_path, await image.read()) + + # 获取对象存储外链 + obs_url = obs_util.get_link(file_path) logging.info(f"obs_url: {obs_url}") - + # 调用OCR处理图片 ocr_util = ocr.get_instance() ocr_result = ocr_util.recognize_image_text(obs_url) logging.info(f"ocr_result: {ocr_result}") - + people = extract_people(ocr_result, obs_url) resp = BaseResponse(error_code=0, error_info="success") resp.data = people.to_dict() @@ -130,7 +144,7 @@ class GetPeopleRequest(BaseModel): query: Optional[str] = None conds: Optional[dict] = None top_k: int = 5 - + @authorized_router.get("/api/peoples") async def get_peoples( request: Request, @@ -142,7 +156,7 @@ async def get_peoples( limit: int = Query(10, description="分页大小"), offset: int = Query(0, description="分页偏移量"), ): - + # 解析查询参数为字典 conds = {} conds["user_id"] = getattr(request.state, 'user_id', '') @@ -156,7 +170,7 @@ async def get_peoples( conds["height"] = height if marital_status: conds["marital_status"] = marital_status - + logging.info(f"conds: , limit: {limit}, offset: {offset}") results = [] @@ -174,7 +188,7 @@ class RemarkRequest(BaseModel): @authorized_router.post("/api/people/{people_id}/remark") -async def post_remark(request: Request, people_id: str, body: RemarkRequest): +async def post_people_remark(request: Request, people_id: str, body: RemarkRequest): service = get_people_service() res, err = service.get(people_id) if not err.success or not res: @@ -188,7 +202,7 @@ async def post_remark(request: Request, people_id: str, body: RemarkRequest): @authorized_router.delete("/api/people/{people_id}/remark") -async def delete_remark(request: Request, people_id: str): +async def delete_people_remark(request: Request, people_id: str): service = get_people_service() res, err = service.get(people_id) if not err.success or not res: @@ -201,6 +215,64 @@ async def delete_remark(request: Request, people_id: str): return BaseResponse(error_code=0, error_info="success") +@authorized_router.post("/api/people/{people_id}/image") +async def post_people_image(request: Request, people_id: str, image: UploadFile = File(...)): + + # 检查 people id 是否存在 + service = get_people_service() + people, err = service.get(people_id) + if not err.success: + return BaseResponse(error_code=err.code, error_info=err.info) + + if people.user_id != getattr(request.state, 'user_id', ''): + return BaseResponse(error_code=ErrorCode.MODEL_ERROR.value, error_info="permission denied") + + # 实现上传图片的处理 + # 保存上传的图片文件 + # 生成唯一的文件名 + file_extension = os.path.splitext(image.filename)[1] + unique_filename = f"{uuid.uuid4()}{file_extension}" + + # 保存文件到对象存储 + file_path = f"peoples/{people_id}/images/{unique_filename}" + obs_util = obs.get_instance() + obs_util.put(file_path, await image.read()) + + # 获取对象存储外链 + obs_url = obs_util.get_link(file_path) + logging.info(f"obs_url: {obs_url}") + + return BaseResponse(error_code=0, error_info="success", data=obs_url) + + +@authorized_router.delete("/api/people/{people_id}/image") +async def delete_people_image(request: Request, people_id: str, image_url: str): + # 检查 people id 是否存在 + service = get_people_service() + people, err = service.get(people_id) + if not err.success: + return BaseResponse(error_code=err.code, error_info=err.info) + + if people.user_id != getattr(request.state, 'user_id', ''): + return BaseResponse(error_code=ErrorCode.MODEL_ERROR.value, error_info="permission denied") + + # 检查 image_url 是否是该 people 名下的图片链接 + obs_util = obs.get_instance() + obs_path, err = obs_util.get_obs_path_by_link(image_url) + if not err.success: + return BaseResponse(error_code=err.code, error_info=err.info) + if not obs_path.startswith(f"peoples/{people_id}/images/"): + return BaseResponse(error_code=ErrorCode.OBS_INPUT_ERROR, error_info=f"文件 {image_url} 不是 {people_id} 名下的图片链接") + + # 实现删除图片的处理 + # 删除对象存储中的文件 + err = obs_util.delete_by_link(image_url) + if not err.success: + return BaseResponse(error_code=err.code, error_info=err.info) + + return BaseResponse(error_code=0, error_info="success") + + class SendCodeRequest(BaseModel): target_type: str target: str @@ -411,4 +483,5 @@ async def update_user_email(request: Request, body: UpdateEmailRequest): } return BaseResponse(error_code=0, error_info="success", data=data) + api.include_router(authorized_router)