258 lines
9.2 KiB
Python
258 lines
9.2 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
全链路Mock验证脚本
|
||
不依赖真实医药城/合同系统,用mock数据跑通:建单→ORDER_FETCH→对码→CONTRACT_CREATE
|
||
验证状态机推进、合同编号回填、前端能观测到next_node
|
||
|
||
用法:
|
||
export STATE_CENTER_URL="http://127.0.0.1:8000"
|
||
export AGENT_TOKEN="dev-agent-token"
|
||
python run_mock_chain.py
|
||
"""
|
||
import json
|
||
import sys
|
||
import uuid
|
||
from datetime import datetime, timedelta
|
||
from urllib.request import Request, urlopen
|
||
from urllib.error import HTTPError
|
||
from urllib.parse import quote
|
||
|
||
|
||
def encode_job_id(job_id: str) -> str:
|
||
"""job_id含中文,URL路径段需编码"""
|
||
return quote(job_id, safe="")
|
||
|
||
STATE_CENTER_URL = "http://127.0.0.1:8000"
|
||
AGENT_TOKEN = "dev-agent-token"
|
||
|
||
|
||
def http_post(path: str, payload: dict, token: str | None = None):
|
||
"""POST请求辅助函数"""
|
||
url = f"{STATE_CENTER_URL}{path}"
|
||
headers = {"Content-Type": "application/json"}
|
||
if token:
|
||
headers["X-Agent-Token"] = token
|
||
|
||
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||
request = Request(url, data=data, headers=headers, method="POST")
|
||
|
||
try:
|
||
with urlopen(request, timeout=15) as response:
|
||
raw = response.read().decode("utf-8")
|
||
return json.loads(raw) if raw else {}
|
||
except HTTPError as exc:
|
||
detail = exc.read().decode("utf-8", errors="replace")
|
||
print(f"❌ HTTP {exc.code}: {detail}")
|
||
raise
|
||
|
||
|
||
def http_get(path: str):
|
||
"""GET请求辅助函数"""
|
||
url = f"{STATE_CENTER_URL}{path}"
|
||
request = Request(url, method="GET")
|
||
|
||
try:
|
||
with urlopen(request, timeout=15) as response:
|
||
raw = response.read().decode("utf-8")
|
||
return json.loads(raw) if raw else {}
|
||
except HTTPError as exc:
|
||
detail = exc.read().decode("utf-8", errors="replace")
|
||
print(f"❌ HTTP {exc.code}: {detail}")
|
||
raise
|
||
|
||
|
||
def step(num: int, title: str):
|
||
print(f"\n{'='*60}")
|
||
print(f"【第{num}步】{title}")
|
||
print('='*60)
|
||
|
||
|
||
def run_chain():
|
||
order_date = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
|
||
# supplier加唯一后缀,保证每次全新job(避免幂等复用旧状态)
|
||
supplier = f"重庆亿昊药业-{uuid.uuid4().hex[:4]}"
|
||
source_type = "YYC" # 医药城线上单
|
||
|
||
# ---- 第1步:建单 ----
|
||
step(1, "建单 POST /api/jobs")
|
||
resp = http_post("/api/jobs", {
|
||
"source_type": source_type,
|
||
"order_date": order_date,
|
||
"supplier_name": supplier,
|
||
"created_by": "mock-chain",
|
||
})
|
||
job_id = resp["job_id"]
|
||
print(f"✓ job_id: {job_id}")
|
||
print(f"✓ first_node: {resp.get('first_node')}")
|
||
print(f"✓ reused: {resp.get('reused')}")
|
||
|
||
# ---- 第2步:加载fixture货品(不依赖Oracle)----
|
||
step(2, "加载测试货品 POST /api/dev/match/load-fixture-goods")
|
||
resp = http_post("/api/dev/match/load-fixture-goods", {}, token=AGENT_TOKEN)
|
||
print(f"✓ 已加载货品数: {resp.get('loaded')}")
|
||
|
||
# ---- 第3步:ORDER_FETCH 开始回调 ----
|
||
step(3, "ORDER_FETCH start 回调")
|
||
resp = http_post(
|
||
f"/api/jobs/{encode_job_id(job_id)}/nodes/ORDER_FETCH/start",
|
||
{"attemptNo": 1, "executorPlatform": "mock-rpa", "operator": "mock-chain"},
|
||
token=AGENT_TOKEN,
|
||
)
|
||
print(f"✓ start响应: {resp}")
|
||
|
||
# ---- 第4步:写订单明细(模拟ORDER_FETCH导出的数据)----
|
||
step(4, "写订单明细 POST /api/jobs/{job_id}/items")
|
||
order_no = f"DD-{order_date.replace('-', '')}-001"
|
||
resp = http_post(
|
||
f"/api/jobs/{encode_job_id(job_id)}/items",
|
||
{
|
||
"replace": True,
|
||
"rows": [
|
||
{
|
||
"orderNo": order_no,
|
||
"rawProductName": "阿卡波糖片",
|
||
"platformProductCode": "YP-001",
|
||
"spec": "50mg*30片",
|
||
"manufacturer": "示例制药",
|
||
"approvalNo": "国药准字H00000001",
|
||
"qty": 100,
|
||
"unitPrice": 25.5,
|
||
"sourceRowNo": 1,
|
||
},
|
||
{
|
||
"orderNo": order_no,
|
||
"rawProductName": "阿莫西林胶囊",
|
||
"platformProductCode": "YP-002",
|
||
"spec": "0.25g*24粒",
|
||
"manufacturer": "示例医药",
|
||
"approvalNo": "国药准字H00000002",
|
||
"qty": 50,
|
||
"unitPrice": 18.0,
|
||
"sourceRowNo": 2,
|
||
},
|
||
],
|
||
},
|
||
token=AGENT_TOKEN,
|
||
)
|
||
print(f"✓ 写入明细: {resp.get('inserted')} 行")
|
||
print(f"✓ 订单号: {resp.get('order_nos')}")
|
||
|
||
# ---- 第5步:ORDER_FETCH 完成回调 ----
|
||
step(5, "ORDER_FETCH complete 回调")
|
||
resp = http_post(
|
||
f"/api/jobs/{encode_job_id(job_id)}/nodes/ORDER_FETCH/complete",
|
||
{
|
||
"attemptNo": 1,
|
||
"executorRunId": f"MOCK-{uuid.uuid4().hex[:8]}",
|
||
"summary": {
|
||
"orderCount": 1,
|
||
"supplierList": [supplier],
|
||
"orderNoList": [order_no],
|
||
},
|
||
"artifacts": [
|
||
{
|
||
"artifactType": "SOURCE_EXCEL",
|
||
"storagePath": f"/tmp/mock_rpa_data/orders_{supplier}_{order_date}.xlsx",
|
||
"fileHash": "sha256:mock-order-excel",
|
||
}
|
||
],
|
||
},
|
||
token=AGENT_TOKEN,
|
||
)
|
||
print(f"✓ next_node: {resp.get('next_node')}")
|
||
|
||
# ---- 第6步:跑对码 ----
|
||
step(6, "对码 POST /api/jobs/{job_id}/match/run")
|
||
resp = http_post(f"/api/jobs/{encode_job_id(job_id)}/match/run", {}, token=AGENT_TOKEN)
|
||
print(f"✓ 对码结果: {json.dumps(resp, ensure_ascii=False)}")
|
||
|
||
# ---- 第7步:查当前状态,判断是否需要人工对码 ----
|
||
step(7, "查单据状态 GET /api/jobs/{job_id}")
|
||
detail = http_get(f"/api/jobs/{encode_job_id(job_id)}")
|
||
job = detail.get("job", {})
|
||
print(f"✓ 当前节点: {job.get('current_node_code')}")
|
||
print(f"✓ 当前状态: {job.get('status')}")
|
||
|
||
tasks = detail.get("tasks", [])
|
||
if tasks:
|
||
print(f"⚠ 有 {len(tasks)} 条待办需要人工处理")
|
||
for t in tasks:
|
||
print(f" - {t.get('title')} [{t.get('reason_type')}]")
|
||
|
||
# ---- 第7.5步:如卡在SOURCE_ADD,走待办提交,不打 RPA complete ----
|
||
if job.get("current_node_code") == "SOURCE_ADD":
|
||
step("7.5", "SOURCE_ADD 待办提交 CONFIRM_AND_CONTINUE")
|
||
source_task = next(
|
||
(t for t in tasks if t.get("node_code") == "SOURCE_ADD" and t.get("status") == "OPEN"),
|
||
None,
|
||
)
|
||
if not source_task:
|
||
raise RuntimeError("SOURCE_ADD 没有 OPEN 待办,无法推进")
|
||
resp = http_post(
|
||
f"/api/tasks/{source_task['task_id']}/submit",
|
||
{
|
||
"action": "CONFIRM_AND_CONTINUE",
|
||
"payload": {"sourceAdded": True},
|
||
"submitToken": f"mock-source-{uuid.uuid4().hex}",
|
||
"operator": "mock-chain",
|
||
},
|
||
token=AGENT_TOKEN,
|
||
)
|
||
print(f"✓ next_node: {resp.get('next_node')}")
|
||
|
||
# ---- 第8步:CONTRACT_CREATE mock(直接传假合同编号)----
|
||
step(8, "CONTRACT_CREATE mock(传假合同编号,跳过真实页面)")
|
||
mock_contract_no = f"HT{order_date.replace('-', '')}{uuid.uuid4().hex[:5].upper()}"
|
||
print(f" 使用mock合同编号: {mock_contract_no}")
|
||
|
||
# 先start
|
||
http_post(
|
||
f"/api/jobs/{encode_job_id(job_id)}/nodes/CONTRACT_CREATE/start",
|
||
{"attemptNo": 1, "executorPlatform": "mock-rpa", "operator": "mock-chain"},
|
||
token=AGENT_TOKEN,
|
||
)
|
||
# 再complete,带businessRefs.contractSystemNo
|
||
resp = http_post(
|
||
f"/api/jobs/{encode_job_id(job_id)}/nodes/CONTRACT_CREATE/complete",
|
||
{
|
||
"attemptNo": 1,
|
||
"executorRunId": f"MOCK-CONTRACT-{uuid.uuid4().hex[:8]}",
|
||
"businessRefs": {"contractSystemNo": mock_contract_no},
|
||
"summary": {
|
||
"supplierName": supplier,
|
||
"contractDate": order_date,
|
||
"sourceType": source_type,
|
||
"uploadRule": "merge_pdf_before_upload",
|
||
},
|
||
"artifacts": [],
|
||
},
|
||
token=AGENT_TOKEN,
|
||
)
|
||
print(f"✓ next_node: {resp.get('next_node')}")
|
||
|
||
# ---- 第9步:验证合同编号回填 ----
|
||
step(9, "验证合同编号回填 GET /api/jobs/{job_id}")
|
||
detail = http_get(f"/api/jobs/{encode_job_id(job_id)}")
|
||
job = detail.get("job", {})
|
||
print(f"✓ 当前节点: {job.get('current_node_code')}")
|
||
print(f"✓ 当前状态: {job.get('status')}")
|
||
contract_no = job.get("contract_system_no")
|
||
if contract_no == mock_contract_no:
|
||
print(f"✅ 合同编号回填成功: {contract_no}")
|
||
else:
|
||
print(f"⚠ 合同编号: {contract_no}(期望 {mock_contract_no})")
|
||
|
||
print(f"\n{'='*60}")
|
||
print(f"🎉 Mock链跑完!job_id = {job_id}")
|
||
print(f" 前端观测: http://localhost:5173")
|
||
print('='*60)
|
||
return job_id
|
||
|
||
|
||
if __name__ == "__main__":
|
||
try:
|
||
run_chain()
|
||
except Exception as e:
|
||
print(f"\n❌ 链路中断: {e}")
|
||
sys.exit(1)
|