Files
bj_power/e2e_bind_verify.py
T
SunYF 6ee131a4bd feat: 新增装机绑定功能与权限、BOM工序配置
1.  新增装机绑定实体与相关CRUD逻辑,支持工件工序物料绑定/解绑
2.  为BOM物料新增装配工序字段,支持按工序校验绑定
3.  扩展权限表,新增父权限码字段支持菜单按钮关联
4.  统一各页面帮助弹窗参数,新增角色管理帮助文档
5.  新增公共格式化工具函数,优化侧边栏菜单展开逻辑
6.  新增工位终端绑定代理接口与MES内部绑定API
7.  修复主入口数据库连接与schema初始化逻辑,新增一键迁移工具
2026-09-07 11:58:19 +08:00

194 lines
9.7 KiB
Python
Raw 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.
# -*- coding: utf-8 -*-
"""
装机绑定 + 工艺流程强校验 端到端验证(2026-09-07
覆盖:
A 绑定面板(应绑清单/齐套状态)
B 非BOM料拒绝 C 工序不匹配拒绝 D 正常绑定 E 精密件SN全局防重
F 缺料拒报工 G 结构件批次绑定 H 齐套后报工成功
I 完工兜底校验(后续工序缺料拒完工) J 多SN齐套(单台用量=2)
K 完工成功 L 追溯聚合 bindTrace M 撤销绑定(报工前可/报工后不可)
依赖:MES 8888 已启动;psql 可用(清理测试数据)。
"""
import json
import sys
import time
import urllib.request
import urllib.error
import subprocess
import os
MES = "http://127.0.0.1:8888"
PSQL = [r"C:\Program Files\PostgreSQL\17\bin\psql.exe", "-U", "postgres", "-h", "127.0.0.1"]
PSQL_ENV = dict(os.environ, PGPASSWORD="postgres")
TS = str(int(time.time()))[-6:]
PROD = "E2EB-PROD-" + TS
WO1 = "E2EB-WO-" + TS
SN1, SN2 = "E2EB-SN1-" + TS, "E2EB-SN2-" + TS
CHIP_A, CHIP_B = "E2EB-CHIP-A-" + TS, "E2EB-CHIP-B-" + TS
SYNC1, SYNC2 = "E2EB-SYNC-1-" + TS, "E2EB-SYNC-2-" + TS
BATCH1 = "E2EB-BATCH-1-" + TS
PASS, FAIL = [], []
def report(name, ok, extra=""):
(PASS if ok else FAIL).append(name)
print(("PASS " if ok else "FAIL ") + name + ((" | " + extra) if extra else ""))
def api(method, path, body=None, token=None):
data = json.dumps(body, ensure_ascii=False).encode() if body is not None else None
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = "Bearer " + token
req = urllib.request.Request(MES + path, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=15) as r:
return r.status, json.loads(r.read().decode("utf-8", "replace"))
except urllib.error.HTTPError as e:
try:
return e.code, json.loads(e.read().decode("utf-8", "replace"))
except Exception:
return e.code, {}
except Exception as e:
return 0, {"code": -1, "message": str(e)}
def q(sql):
p = subprocess.run(PSQL + ["-d", "bj_power_mes", "-t", "-A", "-c", sql],
env=PSQL_ENV, capture_output=True)
return p.stdout.decode("utf-8", "replace").strip()
# ---------- 登录 ----------
st, login = api("POST", "/api/v1/login", {"username": "admin", "password": "123456"})
token = (login.get("data") or {}).get("accessToken", "")
report("登录 admin", st == 200 and token and login.get("code") == 0, login.get("message", ""))
if not token:
sys.exit(1)
# ---------- 准备数据 ----------
st, pt = api("GET", "/api/v1/product-types", token=token)
ptype_id = ((pt.get("data") or [{}])[0]).get("id", 0)
# BOM:工序1 = 结构件1个 + 精密件1个;工序2 = 精密件2个
bom_items = [
{"materialCode": "M-BOLT", "materialName": "结构件-螺栓", "unit": "个", "manageMode": "1", "processCode": 1, "unitQty": 1, "lossRate": 0},
{"materialCode": "M-CHIP", "materialName": "精密件-模块", "unit": "个", "manageMode": "2", "processCode": 1, "unitQty": 1, "lossRate": 0},
{"materialCode": "M-SYNC", "materialName": "精密件-同步器", "unit": "个", "manageMode": "2", "processCode": 2, "unitQty": 2, "lossRate": 0},
]
st, r = api("PUT", "/api/v1/bom", {"productCode": PROD, "items": bom_items}, token)
report("BOM 保存(带装配工序)", r.get("code") == 0, r.get("message", ""))
st, r = api("POST", "/api/v1/work-orders", {"workOrderNo": WO1, "productTypeId": ptype_id,
"productCode": PROD, "productName": "E2E绑定测试品", "quantity": 2, "status": "CREATED", "processSeq": ""}, token)
report("建工单", r.get("code") == 0, r.get("message", ""))
for sn in (SN1, SN2):
st, r = api("POST", "/api/v1/workpiece/online", {"sn": sn, "orderNo": WO1}, token)
report("工件进线 " + sn[:18], r.get("code") == 0, r.get("message", ""))
def bind(sn, process_code, items, station=None):
return api("POST", "/api/v1/binds", {"sn": sn, "orderNo": WO1, "processCode": process_code,
"stationNo": station or process_code, "items": items}, token)
def unbind(sn, process_code, mc, bv):
return api("POST", "/api/v1/binds/remove", {"sn": sn, "processCode": process_code,
"materialCode": mc, "bindValue": bv}, token)
def panel(sn, pc):
st, r = api("GET", "/api/v1/bind-panel?sn=%s&processCode=%d" % (sn, pc), token=token)
return r.get("data") or {}
def report_proc(sn, pc):
return api("POST", "/api/v1/workpiece/process/report", {"sn": sn, "processCode": pc, "stationNo": pc, "steps": []}, token)
# ================= A. 绑定面板 =================
print("\n==== A. 绑定面板 ====")
p = panel(SN1, 1)
report("A1 面板 enabled 且 2 项应绑", p.get("enabled") is True and len(p.get("required") or []) == 2,
"enabled=%s n=%s" % (p.get("enabled"), len(p.get("required") or [])))
report("A2 面板初始未齐套", p.get("complete") is False)
p2 = panel(SN1, 2)
report("A3 工序2 面板 M-SYNC need=2", any(i.get("materialCode") == "M-SYNC" and i.get("need") == 2 for i in (p2.get("required") or [])))
# ================= B/C. 非法绑定 =================
print("\n==== B/C. 非法绑定 ====")
st, r = bind(SN1, 1, [{"materialCode": "M-NOPE", "bindValue": "X1"}])
report("B 非BOM料拒绝", r.get("code") != 0 and "BOM" in r.get("message", ""), r.get("message", "")[:60])
st, r = bind(SN1, 1, [{"materialCode": "M-SYNC", "bindValue": SYNC1}])
report("C 工序不匹配拒绝", r.get("code") != 0 and "不可绑定" in r.get("message", ""), r.get("message", "")[:60])
# ================= D/E. 正常绑定 + SN全局防重 =================
print("\n==== D/E. 绑定与防重 ====")
st, r = bind(SN1, 1, [{"materialCode": "M-CHIP", "bindValue": CHIP_A}])
report("D1 精密件SN绑定成功", r.get("code") == 0, r.get("message", "")[:60])
p = panel(SN1, 1)
chip = next((i for i in (p.get("required") or []) if i.get("materialCode") == "M-CHIP"), {})
report("D2 面板 M-CHIP 齐套", chip.get("complete") is True and chip.get("boundCount") == 1)
st, r = bind(SN2, 1, [{"materialCode": "M-CHIP", "bindValue": CHIP_A}])
report("E SN已绑他机拒绝", r.get("code") != 0 and "已绑定到工件" in r.get("message", ""), r.get("message", "")[:60])
st, r = bind(SN1, 1, [{"materialCode": "M-CHIP", "bindValue": CHIP_A}])
report("E2 同工件重复扫码幂等", r.get("code") == 0)
# ================= F/G/H. 齐套强校验报工 =================
print("\n==== F/G/H. 报工强校验 ====")
st, r = report_proc(SN1, 1)
report("F 缺料拒报工", r.get("code") != 0 and "未绑齐" in r.get("message", ""), r.get("message", "")[:80])
st, r = bind(SN1, 1, [{"materialCode": "M-BOLT", "bindValue": BATCH1}])
report("G 结构件批次绑定成功", r.get("code") == 0, r.get("message", "")[:60])
p = panel(SN1, 1)
report("H1 面板齐套 complete=true", p.get("complete") is True)
st, r = report_proc(SN1, 1)
report("H2 齐套后报工成功", r.get("code") == 0, r.get("message", "")[:60])
# ================= M. 撤销规则 =================
print("\n==== M. 撤销绑定 ====")
st, r = bind(SN2, 1, [{"materialCode": "M-CHIP", "bindValue": CHIP_B}])
report("M1 sn2绑定成功", r.get("code") == 0, r.get("message", "")[:50])
st, r = unbind(SN2, 1, "M-CHIP", CHIP_B)
report("M2 报工前撤销成功", r.get("code") == 0, r.get("message", "")[:50])
st, r = unbind(SN1, 1, "M-BOLT", BATCH1)
report("M3 报工后撤销被拒", r.get("code") != 0 and "已报工" in r.get("message", ""), r.get("message", "")[:60])
# ================= I/J/K. 完工兜底 =================
print("\n==== I/J/K. 完工兜底 ====")
st, r = api("POST", "/api/v1/workpiece/done", {"sn": SN1, "batchItems": [], "serialItems": []}, token)
report("I 后续工序缺料拒完工", r.get("code") != 0 and "绑齐" in r.get("message", ""), r.get("message", "")[:80])
st, r = bind(SN1, 2, [{"materialCode": "M-SYNC", "bindValue": SYNC1}])
report("J1 工序2绑第1颗SN", r.get("code") == 0, r.get("message", "")[:50])
p = panel(SN1, 2)
report("J2 面板 1/2 未齐套", p.get("complete") is False)
st, r = report_proc(SN1, 2)
report("J3 1颗不足拒报工", r.get("code") != 0 and "未绑齐" in r.get("message", ""), r.get("message", "")[:60])
st, r = bind(SN1, 2, [{"materialCode": "M-SYNC", "bindValue": SYNC2}])
report("J4 工序2绑第2颗SN", r.get("code") == 0, r.get("message", "")[:50])
st, r = report_proc(SN1, 2)
report("J5 2颗齐套报工成功", r.get("code") == 0, r.get("message", "")[:50])
st, r = api("POST", "/api/v1/workpiece/done", {"sn": SN1, "batchItems": [BATCH1], "serialItems": [CHIP_A, SYNC1, SYNC2]}, token)
report("K 完工成功", r.get("code") == 0, r.get("message", "")[:60])
# ================= L. 追溯聚合 =================
print("\n==== L. 追溯聚合 ====")
st, r = api("GET", "/api/v1/trace?sn=" + SN1, token=token)
bt = ((r.get("data") or {}).get("bindTrace")) or {}
report("L1 bindTrace.complete", bt.get("complete") is True, str(bt.get("missing", ""))[:60])
report("L2 bindTrace.binds=4条", len(bt.get("binds") or []) == 4, "n=%d" % len(bt.get("binds") or []))
report("L3 完工后绑定再被拒", bind(SN1, 1, [{"materialCode": "M-BOLT", "bindValue": "X9"}])[1].get("code") != 0)
# ================= 清理 =================
CLEAN_SQL = f"""
DELETE FROM workpiece_bind WHERE order_no = '{WO1}' OR sn IN ('{SN1}','{SN2}');
DELETE FROM step_data WHERE sn IN ('{SN1}','{SN2}');
DELETE FROM workpiece_process WHERE sn IN ('{SN1}','{SN2}');
DELETE FROM association_trace WHERE finished_sn = '{SN1}';
DELETE FROM torque_record WHERE sn IN ('{SN1}','{SN2}');
DELETE FROM workpiece WHERE sn IN ('{SN1}','{SN2}');
DELETE FROM event_log WHERE order_no = '{WO1}';
DELETE FROM work_order WHERE work_order_no = '{WO1}';
DELETE FROM work_order_bom WHERE product_code = '{PROD}';
"""
q(CLEAN_SQL)
print("\n测试数据已清理")
print("\n==== 结果: PASS=%d FAIL=%d ====" % (len(PASS), len(FAIL)))
if FAIL:
print("失败项:", "; ".join(FAIL))
sys.exit(1)