1. 新增装机绑定实体与相关CRUD逻辑,支持工件工序物料绑定/解绑 2. 为BOM物料新增装配工序字段,支持按工序校验绑定 3. 扩展权限表,新增父权限码字段支持菜单按钮关联 4. 统一各页面帮助弹窗参数,新增角色管理帮助文档 5. 新增公共格式化工具函数,优化侧边栏菜单展开逻辑 6. 新增工位终端绑定代理接口与MES内部绑定API 7. 修复主入口数据库连接与schema初始化逻辑,新增一键迁移工具
194 lines
9.7 KiB
Python
194 lines
9.7 KiB
Python
# -*- 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)
|