# 订单中心接口服务。本期开放接口 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 "批量接收完成"