Files
000-platform/backend/app/routers/ruimeiyun.py
Admin afcf30b519
All checks were successful
continuous-integration/drone/push Build is passing
feat: 新增睿美云对接模块
- 扩展 ToolConfig 配置类型,新增 external_api 类型
- 实现接口注册表,包含 90+ 睿美云开放接口定义
- 实现 TPOS SHA256WithRSA 签名鉴权
- 实现睿美云 API 客户端,支持多租户配置
- 新增代理路由 /api/ruimeiyun/call/{api_name}
- 支持接口权限控制和健康检查
2026-01-30 17:27:58 +08:00

288 lines
7.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
睿美云代理路由
提供统一的睿美云接口代理能力,支持:
- 多租户配置隔离
- 接口权限控制
- 统一日志记录
- 错误处理
"""
import logging
from typing import Optional, Dict, Any, List
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel
from sqlalchemy.orm import Session
from ..database import get_db
from ..services.ruimeiyun import RuimeiyunClient, RUIMEIYUN_APIS, get_api_definition
from ..services.ruimeiyun.client import RuimeiyunError
from ..services.ruimeiyun.registry import get_all_modules, get_api_list_by_module, get_api_summary
from .auth import get_current_user
from ..models.user import User
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/ruimeiyun", tags=["睿美云代理"])
# ========================================
# Schemas
# ========================================
class RuimeiyunCallRequest(BaseModel):
"""睿美云接口调用请求"""
params: Optional[Dict[str, Any]] = None # URL 参数
body: Optional[Dict[str, Any]] = None # 请求体
class RuimeiyunRawCallRequest(BaseModel):
"""睿美云原始接口调用请求"""
method: str # HTTP 方法
path: str # API 路径
params: Optional[Dict[str, Any]] = None
body: Optional[Dict[str, Any]] = None
# ========================================
# API Endpoints
# ========================================
@router.post("/call/{api_name}")
async def call_ruimeiyun_api(
api_name: str,
request: RuimeiyunCallRequest,
tenant_id: str = Query(..., description="租户ID"),
user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""
调用睿美云接口(通过接口名称)
Args:
api_name: 接口名称,如 customer.search, order.list
request: 请求参数
tenant_id: 租户ID
Returns:
睿美云接口返回的数据
示例:
POST /api/ruimeiyun/call/customer.search?tenant_id=xxx
Body: {"params": {"keyword": "13800138000", "page": 1, "size": 20}}
"""
try:
client = RuimeiyunClient(tenant_id, db)
result = await client.call(
api_name=api_name,
params=request.params,
body=request.body
)
if result.success:
return {
"success": True,
"data": result.data
}
else:
return {
"success": False,
"error": result.error,
"raw": result.raw_response
}
except RuimeiyunError as e:
logger.error(f"睿美云调用失败: {api_name}, {e}")
raise HTTPException(status_code=e.status_code, detail=str(e))
except Exception as e:
logger.exception(f"睿美云调用异常: {api_name}")
raise HTTPException(status_code=500, detail=f"调用失败: {str(e)}")
@router.post("/call-raw")
async def call_ruimeiyun_raw(
request: RuimeiyunRawCallRequest,
tenant_id: str = Query(..., description="租户ID"),
user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""
直接调用睿美云接口(通过路径)
用于调用未在注册表中定义的接口
Args:
request: 请求参数(包含 method, path, params, body
tenant_id: 租户ID
Returns:
睿美云接口返回的数据
示例:
POST /api/ruimeiyun/call-raw?tenant_id=xxx
Body: {
"method": "GET",
"path": "/api/v1/tpos/customer/customer-search",
"params": {"keyword": "13800138000"}
}
"""
try:
client = RuimeiyunClient(tenant_id, db)
result = await client.call_raw(
method=request.method,
path=request.path,
params=request.params,
body=request.body
)
if result.success:
return {
"success": True,
"data": result.data
}
else:
return {
"success": False,
"error": result.error,
"raw": result.raw_response
}
except RuimeiyunError as e:
logger.error(f"睿美云调用失败: {request.path}, {e}")
raise HTTPException(status_code=e.status_code, detail=str(e))
except Exception as e:
logger.exception(f"睿美云调用异常: {request.path}")
raise HTTPException(status_code=500, detail=f"调用失败: {str(e)}")
# ========================================
# 接口元数据
# ========================================
@router.get("/apis")
async def list_apis(
module: Optional[str] = None,
user: User = Depends(get_current_user)
):
"""
获取可用的接口列表
Args:
module: 模块名称(可选),如 customer, order
Returns:
接口列表
"""
if module:
apis = get_api_list_by_module(module)
return {
"module": module,
"count": len(apis),
"apis": apis
}
else:
return {
"count": len(RUIMEIYUN_APIS),
"summary": get_api_summary(),
"apis": RUIMEIYUN_APIS
}
@router.get("/apis/{api_name}")
async def get_api_info(
api_name: str,
user: User = Depends(get_current_user)
):
"""
获取接口详情
Args:
api_name: 接口名称,如 customer.search
Returns:
接口定义
"""
api_def = get_api_definition(api_name)
if not api_def:
raise HTTPException(status_code=404, detail=f"接口不存在: {api_name}")
return {
"name": api_name,
**api_def
}
@router.get("/modules")
async def list_modules(
user: User = Depends(get_current_user)
):
"""
获取所有模块列表
Returns:
模块名称列表和每个模块的接口数量
"""
modules = get_all_modules()
summary = get_api_summary()
return {
"modules": [
{"name": m, "count": summary.get(m, 0)}
for m in modules
]
}
# ========================================
# 健康检查
# ========================================
@router.get("/health/{tenant_id}")
async def check_ruimeiyun_health(
tenant_id: str,
user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""
检查租户的睿美云连接状态
Args:
tenant_id: 租户ID
Returns:
连接状态信息
"""
try:
client = RuimeiyunClient(tenant_id, db)
# 调用门店列表接口测试连接
result = await client.call("tenant.list")
if result.success:
return {
"status": "connected",
"tenant_id": tenant_id,
"base_url": client.config.base_url,
"account": client.config.account,
"message": "连接正常"
}
else:
return {
"status": "error",
"tenant_id": tenant_id,
"message": result.error
}
except RuimeiyunError as e:
return {
"status": "error",
"tenant_id": tenant_id,
"message": str(e)
}
except Exception as e:
return {
"status": "error",
"tenant_id": tenant_id,
"message": f"检查失败: {str(e)}"
}