feat: support upload image api

- support upload and delete image of people
- support uploads any image and get link after login
This commit is contained in:
2025-11-18 00:14:07 +08:00
parent 3840080074
commit 25fb6ba9ce
3 changed files with 174 additions and 39 deletions

View File

@@ -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

View File

@@ -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}")

View File

@@ -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)