170 lines
5.6 KiB
Python
170 lines
5.6 KiB
Python
# 订单中心接口服务。本期开放接口 1、3 收件;接口 2 只在 outbound 组装报文。
|
||
#
|
||
# 路径:
|
||
# - POST /api/oms/v1/sales-goods
|
||
# - POST /api/oms/v1/purchase-contracts
|
||
# 鉴权:X-Oms-Token,内网共享密钥,不上 OAuth。
|
||
# 请求里不跑选品、不录合同。
|
||
# 对外 401/503 用固定文案;ORA 原文只进审计表。
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
import sys
|
||
import threading
|
||
from pathlib import Path
|
||
from typing import Annotated, Any
|
||
|
||
from fastapi import Depends, FastAPI, Header, Request
|
||
from fastapi.responses import JSONResponse
|
||
|
||
# 把仓库根目录放进 sys.path,才能 import rpa_oracle。
|
||
def _put_repo_on_path() -> None:
|
||
root = Path(__file__).resolve().parents[2]
|
||
if str(root) not in sys.path:
|
||
sys.path.insert(0, str(root))
|
||
|
||
|
||
_put_repo_on_path()
|
||
from rpa_oracle.env import load_env
|
||
from rpa_oracle.errors import RepoUnavailable
|
||
|
||
from .auth import token_ok
|
||
from .inbound.audit import cause_text, write_audit
|
||
from .inbound.envelope import EnvelopeError, parse_envelope
|
||
from .inbound.messages import AUTH_FAILED, DB_UNAVAILABLE
|
||
from .inbound.oracle_contract_repo import OraclePurchaseContractRepo
|
||
from .inbound.oracle_repo import OracleSalesGoodsRepo
|
||
from .inbound.purchase_contract import receive_purchase_contracts
|
||
from .inbound.sales_goods import receive_sales_goods
|
||
|
||
load_env(Path(__file__).resolve().parents[1] / ".env")
|
||
|
||
logger = logging.getLogger("rpa_oms")
|
||
app = FastAPI(title="洁达 RPA × 订单中心", version="0.1")
|
||
|
||
_pool = None
|
||
_pool_lock = threading.Lock()
|
||
|
||
|
||
# 进程内共用 OMS 连接池。缺配置抛「数据库不可用」。
|
||
#
|
||
# 懒加载加锁:并发的首个请求只建一个池,输家的池不会被丢掉漏连接。
|
||
def get_pool():
|
||
global _pool
|
||
if _pool is not None:
|
||
return _pool
|
||
with _pool_lock:
|
||
if _pool is None:
|
||
from rpa_oracle.pool import create_pool
|
||
|
||
_pool = create_pool(role="oms")
|
||
return _pool
|
||
|
||
|
||
# 依赖注入阶段库不可用时,仍回固定 503,不把异常原文带出去。
|
||
@app.exception_handler(RepoUnavailable)
|
||
def handle_repo_unavailable(_request: Request, _exc: RepoUnavailable):
|
||
return JSONResponse(
|
||
status_code=503,
|
||
content={"code": 503, "msg": DB_UNAVAILABLE, "results": []},
|
||
)
|
||
|
||
|
||
@app.get("/health", description="进程存活探活,不连库。")
|
||
def health():
|
||
return {"ok": True}
|
||
|
||
|
||
@app.post("/api/oms/v1/sales-goods", description="接收接口 1 销售商品,落表 1。请求里不跑选品。")
|
||
def post_sales_goods(
|
||
payload: dict[str, Any] | None = None,
|
||
x_oms_token: Annotated[str | None, Header()] = None,
|
||
pool=Depends(get_pool),
|
||
):
|
||
return _post_inbound(
|
||
payload,
|
||
x_oms_token,
|
||
pool,
|
||
api_no=1,
|
||
receive=receive_sales_goods,
|
||
repo_cls=OracleSalesGoodsRepo,
|
||
)
|
||
|
||
|
||
@app.post("/api/oms/v1/purchase-contracts", description="接收接口 3 采购合同,落表 3。请求里不录合同。")
|
||
def post_purchase_contracts(
|
||
payload: dict[str, Any] | None = None,
|
||
x_oms_token: Annotated[str | None, Header()] = None,
|
||
pool=Depends(get_pool),
|
||
):
|
||
return _post_inbound(
|
||
payload,
|
||
x_oms_token,
|
||
pool,
|
||
api_no=3,
|
||
receive=receive_purchase_contracts,
|
||
repo_cls=OraclePurchaseContractRepo,
|
||
)
|
||
|
||
|
||
# 接口 1/3 共用:鉴权、拆信封、整批收件、写审计。混批 HTTP 仍 200。
|
||
def _post_inbound(payload, x_oms_token, pool, api_no: int, receive, repo_cls):
|
||
if not token_ok(x_oms_token):
|
||
body = {"code": 401, "msg": AUTH_FAILED, "results": []}
|
||
_audit_best_effort(pool, api_no, 401, payload, body, None, x_oms_token)
|
||
return JSONResponse(status_code=401, content=body)
|
||
try:
|
||
rows = parse_envelope(payload if payload is not None else {})
|
||
except EnvelopeError as exc:
|
||
body = {"code": 422, "msg": exc.message, "results": []}
|
||
_audit_best_effort(pool, api_no, 422, payload, body, None, x_oms_token)
|
||
return JSONResponse(status_code=422, content=body)
|
||
try:
|
||
with pool.acquire() as conn:
|
||
results = receive(rows, repo_cls(conn))
|
||
body = {"code": 200, "msg": _summary(results), "results": results}
|
||
write_audit(
|
||
conn,
|
||
api_no=api_no,
|
||
http_code=200,
|
||
payload=payload,
|
||
response=body,
|
||
error_text=None,
|
||
token=x_oms_token,
|
||
)
|
||
return body
|
||
except RepoUnavailable as exc:
|
||
body = {"code": 503, "msg": DB_UNAVAILABLE, "results": []}
|
||
_audit_best_effort(
|
||
pool, api_no, 503, payload, body, cause_text(exc), x_oms_token
|
||
)
|
||
return JSONResponse(status_code=503, content=body)
|
||
|
||
|
||
# 401/422/503 路径再借一条连接写审计。失败只记日志。
|
||
def _audit_best_effort(pool, api_no, http_code, payload, body, error_text, token) -> None:
|
||
try:
|
||
with pool.acquire() as conn:
|
||
write_audit(
|
||
conn,
|
||
api_no=api_no,
|
||
http_code=http_code,
|
||
payload=payload,
|
||
response=body,
|
||
error_text=error_text,
|
||
token=token,
|
||
)
|
||
except Exception:
|
||
logger.exception("inbound audit write failed")
|
||
|
||
|
||
# 按本批 resultCode 拼整包 msg:全成功 / 含重复 / 其它混批。
|
||
def _summary(results: list[dict]) -> str:
|
||
codes = {item["resultCode"] for item in results}
|
||
if codes <= {"ACCEPTED"}:
|
||
return "批量接收完成"
|
||
if codes <= {"ACCEPTED", "DUPLICATE"} and "DUPLICATE" in codes:
|
||
return "批量接收完成,存在重复数据"
|
||
return "批量接收完成"
|