feat(upload): 添加文件上传功能接口和静态文件支持

refactor(third_party_api): 重构第三方API模块结构和逻辑
feat(third_party_api): 新增OCR图片识别接口
style(third_party_api): 优化API请求参数和响应模型

build: 添加静态文件目录挂载配置
This commit is contained in:
邹方成 2025-10-09 15:19:29 +08:00
parent 258404fa45
commit 11542275f3
11 changed files with 271 additions and 128 deletions

View File

@ -1,6 +1,7 @@
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from tortoise import Tortoise from tortoise import Tortoise
from app.core.exceptions import SettingNotFound from app.core.exceptions import SettingNotFound
@ -33,6 +34,8 @@ def create_app() -> FastAPI:
middleware=make_middlewares(), middleware=make_middlewares(),
lifespan=lifespan, lifespan=lifespan,
) )
# 注册静态文件目录
app.mount("/static", StaticFiles(directory="app/static"), name="static")
register_exceptions(app) register_exceptions(app)
register_routers(app, prefix="/api") register_routers(app, prefix="/api")
return app return app

View File

@ -16,6 +16,7 @@ from .menus import menus_router
from .policy.policy import router as policy_router from .policy.policy import router as policy_router
from .roles import roles_router from .roles import roles_router
from .third_party_api import third_party_api_router from .third_party_api import third_party_api_router
from .upload import router as upload_router
from .users import users_router from .users import users_router
from .valuations import router as valuations_router from .valuations import router as valuations_router
@ -34,9 +35,10 @@ v1_router.include_router(esg_router, prefix="/esg")
v1_router.include_router(index_router, prefix="/index") v1_router.include_router(index_router, prefix="/index")
v1_router.include_router(industry_router, prefix="/industry") v1_router.include_router(industry_router, prefix="/industry")
v1_router.include_router(policy_router, prefix="/policy") v1_router.include_router(policy_router, prefix="/policy")
v1_router.include_router(upload_router, prefix="/upload") # 文件上传路由
v1_router.include_router( v1_router.include_router(
third_party_api_router, third_party_api_router,
prefix="/third_party_api", prefix="/third_party_api",
dependencies=[DependAuth, DependPermission], dependencies=[DependAuth, DependPermission],
) )
v1_router.include_router(valuations_router, prefix="/valuations", dependencies=[DependAuth, DependPermission]) v1_router.include_router(valuations_router, prefix="/valuations", dependencies=[DependAuth, DependPermission])

View File

@ -8,6 +8,7 @@ from app.schemas.base import Success, Fail
from app.schemas.third_party_api import ( from app.schemas.third_party_api import (
BaseAPIRequest, BaseAPIRequest,
ChinazAPIRequest, ChinazAPIRequest,
OCRRequest,
XiaohongshuNoteRequest, XiaohongshuNoteRequest,
JizhiliaoSearchRequest, JizhiliaoSearchRequest,
APIResponse, APIResponse,
@ -39,8 +40,7 @@ async def query_copyright_software(request: ChinazAPIRequest):
logger.error(f"查询企业软件著作权失败: {e}") logger.error(f"查询企业软件著作权失败: {e}")
return Fail(message=f"查询企业软件著作权失败: {str(e)}") return Fail(message=f"查询企业软件著作权失败: {str(e)}")
@router.post("/chinaz/patent", summary="企业专利信息查询")
@router.post("/chinaz/patent_info", summary="企业专利信息查询")
async def query_patent_info(request: ChinazAPIRequest): async def query_patent_info(request: ChinazAPIRequest):
"""查询企业专利信息""" """查询企业专利信息"""
try: try:
@ -57,13 +57,12 @@ async def query_patent_info(request: ChinazAPIRequest):
logger.error(f"查询企业专利信息失败: {e}") logger.error(f"查询企业专利信息失败: {e}")
return Fail(message=f"查询企业专利信息失败: {str(e)}") return Fail(message=f"查询企业专利信息失败: {str(e)}")
@router.post("/chinaz/ocr", summary="OCR图片识别")
@router.post("/chinaz/judicial_data", summary="司法综合数据查询") async def recognize_ocr(request: OCRRequest):
async def query_judicial_data(request: ChinazAPIRequest): """OCR图片识别接口"""
"""查询司法综合数据"""
try: try:
result = await third_party_api_controller.query_judicial_data( result = await third_party_api_controller.recognize_ocr(
company_name=request.company_name, image_url=request.url,
chinaz_ver=request.chinaz_ver chinaz_ver=request.chinaz_ver
) )
@ -72,11 +71,11 @@ async def query_judicial_data(request: ChinazAPIRequest):
else: else:
return Fail(message=result.message) return Fail(message=result.message)
except Exception as e: except Exception as e:
logger.error(f"查询司法综合数据失败: {e}") logger.error(f"OCR识别失败: {e}")
return Fail(message=f"查询司法综合数据失败: {str(e)}") return Fail(message=f"OCR识别失败: {str(e)}")
# 小红书API端点 # 小红书API端点
@router.post("/xiaohongshu/note", summary="获取小红书笔记详情") @router.post("/xiaohongshu/note", summary="获取小红书笔记详情")
async def get_xiaohongshu_note(request: XiaohongshuNoteRequest): async def get_xiaohongshu_note(request: XiaohongshuNoteRequest):
"""获取小红书笔记详情""" """获取小红书笔记详情"""
@ -90,20 +89,20 @@ async def get_xiaohongshu_note(request: XiaohongshuNoteRequest):
else: else:
return Fail(message=result.message) return Fail(message=result.message)
except Exception as e: except Exception as e:
logger.error(f"获取小红书笔记失败: {e}") logger.error(f"获取小红书笔记详情失败: {e}")
return Fail(message=f"获取小红书笔记失败: {str(e)}") return Fail(message=f"获取小红书笔记详情失败: {str(e)}")
@router.post("/jizhiliao/search", summary="极致聊指数搜索")
@router.post("/jizhiliao/index_search", summary="极致聊指数搜索") async def search_jizhiliao_index(request: JizhiliaoSearchRequest):
async def jizhiliao_index_search(request: JizhiliaoSearchRequest): """执行极致聊指数搜索"""
"""调用极致聊指数搜索接口"""
try: try:
params = request.model_dump(by_alias=True, exclude_none=True, exclude={"timeout"}) params = {
timeout = request.timeout if request.timeout is not None else 30 "keyword": request.keyword,
result = await third_party_api_controller.search_jizhiliao_index( "page": request.page,
params=params, "size": request.size
timeout=timeout }
)
result = await third_party_api_controller.search_jizhiliao_index(params)
if result.success: if result.success:
return Success(data=result.data, message=result.message) return Success(data=result.data, message=result.message)

View File

@ -0,0 +1,5 @@
from fastapi import APIRouter
from .upload import router as upload_router
router = APIRouter()
router.include_router(upload_router, prefix="/upload", tags=["文件上传"])

View File

@ -0,0 +1,14 @@
from fastapi import APIRouter, UploadFile, File
from app.controllers.upload import UploadController
from app.schemas.upload import ImageUploadResponse
router = APIRouter()
@router.post("/image", response_model=ImageUploadResponse, summary="上传图片")
async def upload_image(file: UploadFile = File(...)) -> ImageUploadResponse:
"""
上传图片接口
:param file: 图片文件
:return: 图片URL和文件名
"""
return await UploadController.upload_image(file)

View File

@ -1,53 +1,60 @@
import logging import logging
from typing import Dict, Any, List, Optional from typing import Dict, Any, Optional
from app.utils.universal_api_manager import UniversalAPIManager
from app.utils.universal_api_manager import universal_api from app.schemas.third_party_api import APIResponse
from app.schemas.third_party_api import (
APIProviderInfo,
APIEndpointInfo,
APIResponse
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class ThirdPartyAPIController: class ThirdPartyAPIController:
"""第三方API控制器""" """第三方API控制器"""
def __init__(self): def __init__(self):
self.api_manager = universal_api """初始化控制器"""
self.api_manager = UniversalAPIManager()
async def make_api_request( async def make_api_request(
self, self,
provider: str, provider: str,
endpoint: str, endpoint: str,
params: Dict[str, Any] = None, params: Dict[str, Any],
timeout: int = 30 timeout: Optional[int] = None
) -> APIResponse: ) -> APIResponse:
"""通用API请求方法""" """
try: 发送API请求
result = self.api_manager.make_request(
provider=provider, Args:
endpoint=endpoint, provider: API提供商
params=params or {}, endpoint: API端点
timeout=timeout params: 请求参数
) timeout: 超时时间()
Returns:
APIResponse: API响应
"""
try:
# 发送请求
result = self.api_manager.make_request(provider, endpoint, params)
# 检查响应
if isinstance(result, dict) and result.get('code') == '200':
return APIResponse(
success=True,
message="请求成功",
data=result
)
else:
return APIResponse(
success=False,
message=f"请求失败: {result.get('msg', '未知错误')}",
data=result
)
return APIResponse(
success=True,
data=result,
message="请求成功",
provider=provider,
endpoint=endpoint
)
except Exception as e: except Exception as e:
logger.error(f"API请求失败: {e}") logger.error(f"API请求失败: {str(e)}")
return APIResponse( return APIResponse(
success=False, success=False,
data={}, message=f"API请求失败: {str(e)}",
message=f"请求失败: {str(e)}", data=None
provider=provider,
endpoint=endpoint
) )
# 站长之家API便捷方法 # 站长之家API便捷方法
@ -73,19 +80,27 @@ class ThirdPartyAPIController:
"searchKey": company_name, "searchKey": company_name,
"pageNo": 1, "pageNo": 1,
"range": 100, "range": 100,
"searchType": "0", "searchType": 0,
"ChinazVer": chinaz_ver "ChinazVer": chinaz_ver
} }
) )
async def query_judicial_data(self, company_name: str, chinaz_ver: str = "1") -> APIResponse: async def recognize_ocr(self, image_url: str, chinaz_ver: str = "1.0") -> APIResponse:
"""查询司法综合数据""" """
OCR图片识别
Args:
image_url: 图片URL地址(支持jpgpngjpeg1M以内)
chinaz_ver: API版本号
Returns:
APIResponse: OCR识别结果
"""
return await self.make_api_request( return await self.make_api_request(
provider="chinaz", provider="chinaz",
endpoint="judgement", endpoint="recognition_ocr",
params={ params={
"q": company_name, "url": image_url,
"pageNo": 1,
"ChinazVer": chinaz_ver "ChinazVer": chinaz_ver
} }
) )

51
app/controllers/upload.py Normal file
View File

@ -0,0 +1,51 @@
import os
from pathlib import Path
from typing import List
from fastapi import UploadFile
from app.schemas.upload import ImageUploadResponse
class UploadController:
"""文件上传控制器"""
@staticmethod
async def upload_image(file: UploadFile) -> ImageUploadResponse:
"""
上传图片
:param file: 上传的图片文件
:return: 图片URL和文件名
"""
# 检查文件类型
if not file.content_type.startswith('image/'):
raise ValueError("只支持上传图片文件")
# 获取项目根目录
base_dir = Path(__file__).resolve().parent.parent
# 图片保存目录
upload_dir = base_dir / "static" / "images"
# 确保目录存在
if not upload_dir.exists():
upload_dir.mkdir(parents=True, exist_ok=True)
# 生成文件名
filename = file.filename
file_path = upload_dir / filename
# 如果文件已存在,重命名
counter = 1
while file_path.exists():
name, ext = os.path.splitext(filename)
filename = f"{name}_{counter}{ext}"
file_path = upload_dir / filename
counter += 1
# 保存文件
content = await file.read()
with open(file_path, "wb") as f:
f.write(content)
# 返回文件URL
return ImageUploadResponse(
url=f"/static/images/{filename}",
filename=filename
)

View File

@ -1,72 +1,53 @@
from typing import Dict, Any, Optional, List import logging
from pydantic import BaseModel, Field, ConfigDict from typing import List, Optional
from pydantic import BaseModel, Field
logger = logging.getLogger(__name__)
class BaseAPIRequest(BaseModel): class BaseAPIRequest(BaseModel):
"""基础API请求模型""" """基础API请求模型"""
provider: str = Field(..., description="API提供商", example="chinaz") pass
endpoint: str = Field(..., description="端点名称", example="icp_info")
params: Dict[str, Any] = Field(default_factory=dict, description="请求参数")
timeout: Optional[int] = Field(30, description="超时时间(秒)")
class ChinazAPIRequest(BaseAPIRequest):
class ChinazAPIRequest(BaseModel):
"""站长之家API请求模型""" """站长之家API请求模型"""
company_name: str = Field(..., description="公司名称", example="百度在线网络技术(北京)有限公司") company_name: Optional[str] = Field(None, description="公司名称")
chinaz_ver: Optional[str] = Field("1", description="API版本", example="1") chinaz_ver: str = Field("1.0", description="API版本号")
timeout: Optional[int] = Field(30, description="超时时间(秒)")
class OCRRequest(BaseAPIRequest):
"""OCR识别请求模型"""
url: str = Field(..., description="图片URL地址(支持jpgpngjpeg1M以内)")
chinaz_ver: str = Field("1.0", description="API版本号")
class XiaohongshuNoteRequest(BaseModel): class XiaohongshuNoteRequest(BaseAPIRequest):
"""小红书笔记详情请求""" """小红书笔记请求模型"""
note_id: str = Field(..., description="笔记ID", example="68d2c71d000000000e00e9ea") note_id: str = Field(..., description="笔记ID")
class JizhiliaoSearchRequest(BaseModel):
"""极致聊指数搜索请求"""
model_config = ConfigDict(populate_by_name=True)
keyword: str = Field(..., description="搜索关键词", example="人民日报")
mode: int = Field(2, description="搜索模式", example=2)
business_type: int = Field(
8192,
alias="BusinessType",
description="业务类型标识",
example=8192,
)
sub_search_type: int = Field(0, description="子搜索类型", example=0)
verifycode: Optional[str] = Field("", description="验证码", example="")
timeout: Optional[int] = Field(30, description="超时时间(秒)")
class JizhiliaoSearchRequest(BaseAPIRequest):
"""极致聊搜索请求模型"""
keyword: str = Field(..., description="搜索关键词")
page: int = Field(1, description="页码")
size: int = Field(10, description="每页数量")
class APIResponse(BaseModel): class APIResponse(BaseModel):
"""API响应模型""" """API响应模型"""
success: bool = Field(..., description="请求是否成功") success: bool
data: Dict[str, Any] = Field(..., description="响应数据") message: str
message: Optional[str] = Field(None, description="响应消息") data: Optional[dict] = None
provider: Optional[str] = Field(None, description="API提供商")
endpoint: Optional[str] = Field(None, description="端点名称")
class APIProviderInfo(BaseModel):
"""API提供商信息"""
name: str = Field(..., description="提供商名称")
base_url: str = Field(..., description="基础URL")
description: Optional[str] = Field(None, description="描述")
endpoints: List[str] = Field(default_factory=list, description="可用端点")
class APIEndpointInfo(BaseModel): class APIEndpointInfo(BaseModel):
"""API端点信息""" """API端点信息"""
name: str = Field(..., description="端点名称") path: str
path: str = Field(..., description="请求路径") method: str
method: str = Field(..., description="HTTP方法") description: str
description: Optional[str] = Field(None, description="描述") required_params: List[str]
required_params: List[str] = Field(default_factory=list, description="必需参数") optional_params: Optional[List[str]] = None
optional_params: List[str] = Field(default_factory=list, description="可选参数")
class APIProviderInfo(BaseModel):
"""API提供商信息"""
name: str
base_url: str
endpoints: dict[str, APIEndpointInfo]
class APIListResponse(BaseModel): class APIListResponse(BaseModel):
"""API列表响应""" """API列表响应"""
providers: List[APIProviderInfo] = Field(..., description="API提供商列表") providers: List[APIProviderInfo]
total_providers: int = Field(..., description="提供商总数")

6
app/schemas/upload.py Normal file
View File

@ -0,0 +1,6 @@
from pydantic import BaseModel
class ImageUploadResponse(BaseModel):
"""图片上传响应模型"""
url: str
filename: str

View File

@ -29,10 +29,10 @@ class APIConfig:
# 默认配置 # 默认配置
default_config = { default_config = {
"chinaz": { "chinaz": {
"api_key": os.getenv("CHINAZ_API_KEY", "YOUR_API_KEY"),
"base_url": "https://openapi.chinaz.net", "base_url": "https://openapi.chinaz.net",
"endpoints": { "endpoints": {
"copyright_software": { "copyright_software": {
"api_key": os.getenv("CHINAZ_COPYRIGHT_API_KEY", "YOUR_API_KEY"),
"path": "/v1/1036/copyrightsoftware", "path": "/v1/1036/copyrightsoftware",
"method": "POST", "method": "POST",
"description": "企业软件著作权查询", "description": "企业软件著作权查询",
@ -40,6 +40,7 @@ class APIConfig:
"optional_params": ["sign"] "optional_params": ["sign"]
}, },
"patent": { "patent": {
"api_key": os.getenv("CHINAZ_PATENT_API_KEY", "YOUR_API_KEY"),
"path": "/v1/1036/patent", "path": "/v1/1036/patent",
"method": "POST", "method": "POST",
"description": "企业专利信息查询", "description": "企业专利信息查询",
@ -47,12 +48,21 @@ class APIConfig:
"optional_params": ["sign", "searchType"] "optional_params": ["sign", "searchType"]
}, },
"judgement": { "judgement": {
"api_key": os.getenv("CHINAZ_JUDGEMENT_API_KEY", "YOUR_API_KEY"),
"path": "/v1/1036/judgementdetailv4", "path": "/v1/1036/judgementdetailv4",
"method": "POST", "method": "POST",
"description": "司法综合数据查询", "description": "司法综合数据查询",
"required_params": ["APIKey", "ChinazVer"], "required_params": ["APIKey", "ChinazVer"],
"optional_params": ["sign", "q", "idCardNo", "datatype", "id", "pageNo"] "optional_params": ["sign", "q", "idCardNo", "datatype", "id", "pageNo"]
} },
"recognition_ocr": {
"api_key": os.getenv("CHINAZ_OCR_API_KEY", "YOUR_API_KEY"),
"path": "/v1/1024/recognition_ocr",
"method": "POST",
"description": "图片OCR识别",
"required_params": ["url", "APIKey", "ChinazVer"],
"optional_params": ["sign"]
},
} }
}, },

View File

@ -41,12 +41,69 @@ class UniversalAPIManager:
raise ValueError(f"未找到API提供商配置: {provider}") raise ValueError(f"未找到API提供商配置: {provider}")
return config return config
def _get_endpoint_config(self, provider: str, endpoint: str) -> Dict[str, Any]: def _get_endpoint_config(self, provider: str, endpoint: str) -> Optional[Dict[str, Any]]:
"""获取端点配置""" """获取端点配置"""
endpoint_config = self.config.get_endpoint_config(provider, endpoint) provider_config = self.config.get_api_config(provider)
if not provider_config or 'endpoints' not in provider_config:
return None
return provider_config['endpoints'].get(endpoint)
def _get_api_key(self, provider: str, endpoint: str) -> Optional[str]:
"""获取API密钥"""
endpoint_config = self._get_endpoint_config(provider, endpoint)
if not endpoint_config: if not endpoint_config:
raise ValueError(f"未找到端点配置: {provider}.{endpoint}") return None
return endpoint_config return endpoint_config.get('api_key')
def make_request(self, provider: str, endpoint: str, params: Dict[str, Any], timeout: Optional[int] = None) -> Dict[str, Any]:
"""
发送API请求
Args:
provider: API提供商
endpoint: API端点
params: 请求参数
timeout: 超时时间()
Returns:
Dict[str, Any]: API响应
"""
# 获取API配置
endpoint_config = self._get_endpoint_config(provider, endpoint)
if not endpoint_config:
raise ValueError(f"未找到API配置: {provider}/{endpoint}")
# 获取API密钥
api_key = self._get_api_key(provider, endpoint)
if not api_key:
raise ValueError(f"未找到API密钥: {provider}/{endpoint}")
# 获取基础URL
provider_config = self.config.get_api_config(provider)
base_url = provider_config.get('base_url')
if not base_url:
raise ValueError(f"未找到基础URL: {provider}")
# 构建完整URL
url = f"{base_url.rstrip('/')}{endpoint_config['path']}"
# 添加API密钥到参数中
params['APIKey'] = api_key
# 发送请求
try:
response = self.session.request(
method=endpoint_config['method'],
url=url,
json=params if endpoint_config['method'] == 'POST' else None,
params=params if endpoint_config['method'] == 'GET' else None,
timeout=timeout or provider_config.get('timeout', 30)
)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
logger.error(f"API请求失败: {str(e)}")
raise
def _generate_chinaz_sign(self, date: datetime = None) -> str: def _generate_chinaz_sign(self, date: datetime = None) -> str:
""" """