Files
cmroubao/erp-connector/src/shunyunbaoerp/api.py
T

168 lines
5.1 KiB
Python

"""可选 FastAPI 包装层,供其他项目通过 HTTP 调用。"""
from __future__ import annotations
import os
import secrets
import threading
from typing import Annotated
from fastapi import Depends, FastAPI, Header, HTTPException, Response, status
from pydantic import BaseModel, Field
from .client import ERPClient
from .errors import (
ERPAPIError,
ERPAuthenticationError,
ERPNotFoundError,
ERPProtocolError,
ERPTransportError,
)
from .normalizer import normalize_freight_result
app = FastAPI(
title="顺运宝 ERP 货运查询服务",
version="0.1.0",
description="单租户、受控会话的 ERP 查询适配层",
)
_client: ERPClient | None = None
_client_lock = threading.Lock()
class LoginRequest(BaseModel):
captcha_code: str = Field(min_length=1, max_length=16)
class FreightQueryRequest(BaseModel):
mode: str = Field(default="ORDER_NUMBER", max_length=32)
order_number: str | None = Field(default=None, min_length=1, max_length=128)
created_from: str | None = Field(default=None, min_length=10, max_length=10)
created_to: str | None = Field(default=None, min_length=10, max_length=10)
def get_client() -> ERPClient:
global _client
with _client_lock:
if _client is None:
_client = ERPClient(
os.getenv(
"SHUNYUNBAO_BASE_URL",
"https://www.shunyunbaoerp.com",
)
)
return _client
def require_api_key(
x_api_key: Annotated[str | None, Header(alias="X-API-Key")] = None,
) -> None:
expected = os.getenv("SHUNYUNBAO_SERVICE_API_KEY")
if not expected or len(expected.encode("utf-8")) < 32:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="CONNECTOR_API_KEY_NOT_CONFIGURED",
)
if not x_api_key or not secrets.compare_digest(x_api_key, expected):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="CONNECTOR_API_KEY_INVALID",
)
APIKeyDependency = Annotated[None, Depends(require_api_key)]
@app.get("/health")
def health() -> dict[str, object]:
client = get_client()
expires = client.token_expires_at
return {
"status": "ok",
"loggedIn": client.is_logged_in,
"tokenExpiresAt": expires.isoformat() if expires else None,
}
@app.get("/v1/session/captcha")
def captcha(_: APIKeyDependency) -> Response:
try:
client = get_client()
content = client.fetch_captcha()
return Response(
content=content,
media_type=client.last_captcha_content_type,
headers={"Cache-Control": "no-store"},
)
except (ERPTransportError, ERPProtocolError) as exc:
raise HTTPException(
status_code=502,
detail="ERP_CAPTCHA_UNAVAILABLE",
) from exc
@app.post("/v1/session/login")
def login(body: LoginRequest, _: APIKeyDependency) -> dict[str, object]:
username = os.getenv("SHUNYUNBAO_USERNAME")
password = os.getenv("SHUNYUNBAO_PASSWORD")
if not username or not password:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="ERP_CREDENTIALS_NOT_CONFIGURED",
)
try:
get_client().login(username, password, body.captcha_code)
return {"status": "ok"}
except ERPAuthenticationError as exc:
raise HTTPException(status_code=401, detail="ERP_LOGIN_FAILED") from exc
except (ERPTransportError, ERPProtocolError) as exc:
raise HTTPException(
status_code=502,
detail="ERP_UPSTREAM_UNAVAILABLE",
) from exc
@app.post("/v1/freight/query")
def query_freight(
body: FreightQueryRequest,
_: APIKeyDependency,
) -> dict[str, object]:
try:
if body.mode == "ORDER_NUMBER" and body.order_number:
if body.created_from is not None or body.created_to is not None:
raise ValueError("ORDER_NUMBER 不能包含日期范围")
result = get_client().get_freight_details(body.order_number)
elif (
body.mode == "CREATED_RANGE"
and body.order_number is None
and body.created_from
and body.created_to
):
result = get_client().get_freight_details_by_created_range(
body.created_from,
body.created_to,
)
else:
raise ValueError("查询模式与参数不匹配")
return normalize_freight_result(result)
except ERPAuthenticationError as exc:
raise HTTPException(
status_code=401,
detail="ERP_SESSION_REQUIRED",
) from exc
except ERPNotFoundError as exc:
raise HTTPException(
status_code=404,
detail="ERP_FREIGHT_NOT_FOUND",
) from exc
except ValueError as exc:
raise HTTPException(
status_code=422,
detail="ERP_QUERY_INVALID",
) from exc
except (ERPTransportError, ERPProtocolError, ERPAPIError) as exc:
raise HTTPException(
status_code=502,
detail="ERP_UPSTREAM_UNAVAILABLE",
) from exc