# -*- 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)