# -*- coding: utf-8 -*- """ MES 8 项优化端到端验证(2026-09-04) 覆盖: ① 工单状态机(流转白名单/原因/日志/终态联动) ② 日排产与工单状态联动 ③ BOM 需求量自动计算(不手填) ④ 备料生成前 WMS 物料存在性校验(缺料阻断 + 降级) ⑤ 下发 SN 归属/进线校验 + 未完成信号握手 ⑧ 流程卡打印预检 + 产品名称联查修复 + 拧紧数据渲染 ⑦ 流程多工位绑定(一流程多工位 / 一工位一启用流程 / 停用可再绑) 依赖:本机 8888(MES) 与 8890(WMS) 已用新二进制启动。 """ import json import os import subprocess import sys import time import urllib.request import urllib.error from datetime import date, timedelta MES = "http://127.0.0.1:8888" WMS = "http://127.0.0.1:8890" PSQL = [r"C:\Program Files\PostgreSQL\17\bin\psql.exe", "-U", "postgres", "-h", "127.0.0.1"] PSQL_ENV = dict(os.environ, PGPASSWORD="postgres") 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, base=MES, raw=False, xapi=None): url = base + path data = None headers = {"Content-Type": "application/json"} if body is not None: data = json.dumps(body, ensure_ascii=False).encode() if token: headers["Authorization"] = "Bearer " + token if xapi: headers["X-API-TOKEN"] = xapi req = urllib.request.Request(url, data=data, headers=headers, method=method) try: with urllib.request.urlopen(req, timeout=15) as r: if raw: return r.status, r.read().decode("utf-8", "replace") 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, db="bj_power_mes"): """psql 只读/写查询,返回行文本""" p = subprocess.run(PSQL + ["-d", db, "-t", "-A", "-c", sql], capture_output=True, text=True, env=PSQL_ENV) return p.stdout.strip() def esc(s): return s.replace("'", "''") def today(offset=0): return (date.today() + timedelta(days=offset)).isoformat() # 预清理(脚本可重入):清掉上次运行残留,避免工单号唯一约束冲突 / 旧SENT干扰握手 CLEAN_SQL = """ DELETE FROM material_request WHERE order_no LIKE 'E2E-%'; DELETE FROM daily_plan WHERE order_no LIKE 'E2E-%'; DELETE FROM scan_record WHERE sn LIKE 'E2E-SN-%' OR sn LIKE 'SN-DBG-%'; DELETE FROM plc_send_log WHERE order_no LIKE 'E2E-%' OR status='SENT'; DELETE FROM torque_record WHERE sn LIKE 'E2E-SN-%' OR sn LIKE 'SN-DBG-%' OR work_order_no LIKE 'E2E-%'; DELETE FROM step_data WHERE sn LIKE 'E2E-SN-%' OR sn LIKE 'SN-DBG-%'; DELETE FROM workpiece_process WHERE sn LIKE 'E2E-SN-%' OR sn LIKE 'SN-DBG-%'; DELETE FROM association_trace WHERE finished_sn LIKE 'E2E-SN-%' OR finished_sn LIKE 'SN-DBG-%'; DELETE FROM workpiece WHERE sn LIKE 'E2E-SN-%' OR sn LIKE 'SN-DBG-%' OR order_no LIKE 'E2E-%'; DELETE FROM event_log WHERE work_order_no LIKE 'E2E-%'; DELETE FROM work_order WHERE work_order_no LIKE 'E2E-%'; DELETE FROM work_order_bom WHERE product_code LIKE 'E2E-PROD-%'; """ q(CLEAN_SQL) # ---------- 登录 ---------- 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: print(json.dumps(login, ensure_ascii=False)[:300]) sys.exit(1) st, pt = api("GET", "/api/v1/product-types", token=token) ptypes = (pt.get("data") or []) report("产品类型可查", pt.get("code") == 0 and len(ptypes) > 0) ptype_id = ptypes[0]["id"] if ptypes else 0 def create_wo(no, qty=10, product_code="E2E-PROD-001", product_name="测试产品E2E", status="CREATED", seq=""): body = {"workOrderNo": no, "productTypeId": ptype_id, "productCode": product_code, "productName": product_name, "quantity": qty, "status": status, "processSeq": seq} return api("POST", "/api/v1/work-orders", body, token) def wo_by_no(no): st, r = api("GET", "/api/v1/work-orders?orderNo=" + no, token=token) rows = r.get("data") or [] return (rows[0] if rows else None) def set_status(wo_id, status, reason=""): return api("POST", "/api/v1/work-orders/status", {"id": wo_id, "status": status, "reason": reason}, token) def get_wo(wo_id): st, r = api("GET", "/api/v1/work-orders/%d" % wo_id, token=token) return r.get("data") D1 = today(1); D2 = today(2); D3 = today(3); TDY = today(0) # ================= A. 工单状态机 ================= print("\n==== A. 工单状态机 ====") noA = "E2E-WO-A-" + today().replace("-", "") create_wo(noA, qty=10) wo = wo_by_no(noA) report("A1 创建工单(恒CREATED)", wo is not None and wo.get("status") == "CREATED", "status=" + (wo or {}).get("status", "?")) # 非法跳转 CREATED→DONE 应拒绝 code, r = set_status(wo["id"], "DONE") report("A2 非法跳转 CREATED→DONE 被拒", code == 200 and r.get("code") != 0, r.get("message", "")) # 合法 RELEASED code, r = set_status(wo["id"], "RELEASED") report("A3 CREATED→RELEASED 允许", r.get("code") == 0, r.get("message", "")) # RELEASED→DONE 拒绝 code, r = set_status(wo["id"], "DONE") report("A4 RELEASED→DONE 被拒(需先开工)", r.get("code") != 0, r.get("message", "")) # RELEASED→IN_PROGRESS code, r = set_status(wo["id"], "IN_PROGRESS") report("A5 RELEASED→IN_PROGRESS 允许", r.get("code") == 0, r.get("message", "")) # 暂停带原因 code, r = set_status(wo["id"], "PAUSED", "设备检修") report("A6 IN_PROGRESS→PAUSED(带原因) 允许", r.get("code") == 0, r.get("message", "")) # 暂停期间再完工应拒绝 code, r = set_status(wo["id"], "DONE") report("A7 PAUSED→DONE 被拒", r.get("code") != 0, r.get("message", "")) # 恢复 code, r = set_status(wo["id"], "IN_PROGRESS") report("A8 PAUSED→IN_PROGRESS 允许", r.get("code") == 0, r.get("message", "")) # 完工 code, r = set_status(wo["id"], "DONE") report("A9 IN_PROGRESS→DONE 允许", r.get("code") == 0, r.get("message", "")) woA = get_wo(wo["id"]) report("A10 DONE 记录 completedAt", (woA or {}).get("completedAt") is not None) # 终态不可再流转 code, r = set_status(wo["id"], "CANCELLED") report("A11 DONE→CANCELLED 被拒", r.get("code") != 0, r.get("message", "")) # 取消带原因 noB = "E2E-WO-B-" + today().replace("-", "") create_wo(noB, qty=5) woB = wo_by_no(noB) set_status(woB["id"], "CANCELLED", "计划取消") # 取消后不可流转 code, r = set_status(woB["id"], "IN_PROGRESS") report("A12 CANCELLED 不可再开工", r.get("code") != 0, r.get("message", "")) # 状态日志 + 原因入库 logrows = q("SELECT description || '|' || COALESCE(payload->>'reason','') FROM event_log WHERE work_order_no='%s' AND event_type='work.order.status' ORDER BY id DESC" % esc(noA)) report("A13 状态流转写日志且带原因", "变更工单状态" in logrows and "设备检修" in logrows, logrows.replace("\n", " ")) logB = q("SELECT payload->>'reason' FROM event_log WHERE work_order_no='%s' AND event_type='work.order.status' ORDER BY id DESC LIMIT 1" % esc(noB)) report("A14 取消原因入库", logB == "计划取消", logB) # 编辑/删除约束 noC1 = "E2E-WO-C1-" + today().replace("-", "") create_wo(noC1, qty=6) woC1 = wo_by_no(noC1) code, r = api("PUT", "/api/v1/work-orders", {"id": woC1["id"], "workOrderNo": noC1, "productTypeId": ptype_id, "quantity": 7}, token) report("A15 CREATED 可编辑", r.get("code") == 0, r.get("message", "")) set_status(woC1["id"], "RELEASED") code, r = api("PUT", "/api/v1/work-orders", {"id": woC1["id"], "workOrderNo": noC1, "productTypeId": ptype_id, "quantity": 8}, token) report("A16 已下发不可编辑", r.get("code") != 0, r.get("message", "")) code, r = api("DELETE", "/api/v1/work-orders/%d" % woC1["id"], token=token) report("A17 已下发不可删除", r.get("code") != 0, r.get("message", "")) woC1 = wo_by_no(noC1) # ================= B. 日排产联动 ================= print("\n==== B. 日排产与工单状态联动 ====") noC = "E2E-WO-C-" + today().replace("-", "") create_wo(noC, qty=20) woC = wo_by_no(noC) def save_plan(order_no, d, qty, status=""): return api("POST", "/api/v1/daily-plans", {"orderNo": order_no, "planDate": d, "planQty": qty, "status": status}, token) code, r = save_plan(noC, D1, 8) report("B1 保存日排产(PENDING)", r.get("code") == 0, r.get("message", "")) code, r = save_plan(noC, D2, 15) report("B2 排产合计超工单量被拒", r.get("code") != 0, r.get("message", "")) code, r = save_plan(noC, D2, 10) report("B3 合计未超可保存", r.get("code") == 0, r.get("message", "")) # 工单开工 → 排产置 PROCESSING set_status(woC["id"], "RELEASED") set_status(woC["id"], "IN_PROGRESS") p1 = q("SELECT status FROM daily_plan WHERE order_no='%s' AND plan_date='%s'" % (esc(noC), D1)) report("B4 工单执行中→排产置PROCESSING", p1 == "PROCESSING", p1) # 暂停期间不可新增排产 code, r = set_status(woC["id"], "PAUSED", "待料") code, r = save_plan(noC, D3, 2) report("B5 工单暂停→新增排产被拒", r.get("code") != 0 and "暂停" in r.get("message", ""), r.get("message", "")) code, r = set_status(woC["id"], "IN_PROGRESS") code, r = save_plan(noC, D3, 2) report("B6 恢复后可排产", r.get("code") == 0, r.get("message", "")) # 工单完工 → 未足额排产置 CANCELLED code, r = set_status(woC["id"], "DONE") plans = q("SELECT plan_date||':'||status FROM daily_plan WHERE order_no='%s' ORDER BY plan_date" % esc(noC)).replace("\n", " ") report("B7 工单DONE→未足额排产置CANCELLED", all(s in plans for s in [D1 + ":CANCELLED", D2 + ":CANCELLED", D3 + ":CANCELLED"]), plans) # 已结束工单不可排产 code, r = save_plan(noC, D1, 1) report("B8 DONE 工单不可排产", r.get("code") != 0, r.get("message", "")) # 取消工单联动排产 noCC = "E2E-WO-CC-" + today().replace("-", "") create_wo(noCC, qty=5) woCC = wo_by_no(noCC) save_plan(noCC, D1, 2) set_status(woCC["id"], "CANCELLED", "紧急取消") pcc = q("SELECT status FROM daily_plan WHERE order_no='%s'" % esc(noCC)) report("B9 工单取消→排产置CANCELLED", pcc == "CANCELLED", pcc) # ================= C. 完工联动当日排产 completedQty ================= print("\n==== C. 完工联动当日排产 ====") noF = "E2E-WO-F-" + today().replace("-", "") create_wo(noF, qty=5) woF = wo_by_no(noF) save_plan(noF, TDY, 2) def online(sn, order_no): return api("POST", "/api/v1/workpiece/online", {"sn": sn, "orderNo": order_no}, token) def report_proc(sn, code, station): return api("POST", "/api/v1/workpiece/process/report", {"sn": sn, "processCode": code, "stationNo": station, "steps": []}, token) def done(sn): return api("POST", "/api/v1/workpiece/done", {"sn": sn, "batchItems": [], "serialItems": []}, token) code, r = online("E2E-SN-F1", noF) report("C1 工件进线", r.get("code") == 0, r.get("message", "")) code, r = report_proc("E2E-SN-F1", 1, 1) report("C2 工序报工", r.get("code") == 0, r.get("message", "")) code, r = done("E2E-SN-F1") report("C3 完工OK", r.get("code") == 0, r.get("message", "")) pc = q("SELECT completed_qty||':'||status FROM daily_plan WHERE order_no='%s' AND plan_date='%s'" % (esc(noF), TDY)) report("C4 完工后排产 completedQty=1/置PROCESSING", pc == "1:PROCESSING", pc) code, r = online("E2E-SN-F2", noF) code2, r2 = done("E2E-SN-F2") report("C5 第二件进线+完工", r.get("code") == 0 and r2.get("code") == 0, r.get("message", "") + " / " + r2.get("message", "")) pc = q("SELECT completed_qty||':'||status FROM daily_plan WHERE order_no='%s' AND plan_date='%s'" % (esc(noF), TDY)) report("C6 足额后排产置DONE", pc == "2:DONE", pc) # ================= D. BOM 算料 + WMS 校验 ================= print("\n==== D. BOM 自动算料 + WMS 存在性校验 ====") noG = "E2E-WO-G-" + today().replace("-", "") PROD = "E2E-PROD-001" create_wo(noG, qty=5, product_code=PROD) woG = wo_by_no(noG) save_plan(noG, D3, 3) def save_bom(items): return api("PUT", "/api/v1/bom", {"productCode": PROD, "items": items}, token) good_items = [ {"materialCode": "GJ-BAN-001", "materialName": "10mm钢板(结构件)", "spec": "", "unit": "件", "manageMode": "1", "unitQty": 2, "lossRate": 5}, {"materialCode": "JM-ZHOUCHENG-6020", "materialName": "精密轴承6020", "spec": "", "unit": "个", "manageMode": "2", "unitQty": 1, "lossRate": 0}, ] bom_with_bad = good_items + [{"materialCode": "NO-SUCH-CODE-9", "materialName": "幽灵料", "spec": "", "unit": "个", "manageMode": "2", "unitQty": 1, "lossRate": 0}] code, r = save_bom(bom_with_bad) report("D1 保存BOM(含缺失料) OK", r.get("code") == 0, r.get("message", "")) code, r = api("POST", "/api/v1/material-requests/generate", {"planDate": D3}, token) data = r.get("data") or {} report("D2 生成备料单:缺料阻断不生成", r.get("code") == 0 and data.get("blocked") is True and "NO-SUCH-CODE-9" in data.get("missing", []), json.dumps(data, ensure_ascii=False)) cnt = q("SELECT count(*) FROM material_request WHERE order_no='%s'" % esc(noG)) report("D3 阻断时零写入", cnt == "0", cnt) # 修正 BOM(SaveBom 为 upsert 不删行,先移除缺料行)→ 生成成功,数量=3*2*1.05=6.3 与 3 q("DELETE FROM work_order_bom WHERE product_code='%s' AND material_code='NO-SUCH-CODE-9'" % esc(PROD)) code, r = save_bom(good_items) code, r = api("POST", "/api/v1/material-requests/generate", {"planDate": D3}, token) data = r.get("data") or {} report("D4 修正BOM后生成成功", r.get("code") == 0 and data.get("count") == 2, json.dumps(data, ensure_ascii=False)) st, rows = api("GET", "/api/v1/material-requests?orderNo=" + noG, token=token) mrs = rows.get("data") or [] rn = sorted(x.get("requestNo") for x in mrs) report("D5 生成两条且单号唯一", len(mrs) == 2 and len(set(rn)) == 2, ";".join(rn)) qty_ok = sorted(round(x.get("reqQty"), 2) for x in mrs) == [3.0, 6.3] report("D6 reqQty=planQty×unitQty×(1+loss) 自动计算", qty_ok, str([(x.get("materialCode"), x.get("reqQty")) for x in mrs])) # WMS 直连接口(可选旁证) st, r = api("POST", "/api/internal/material/exists", {"codes": ["GJ-BAN-001", "NO-SUCH-CODE-9"]}, base=WMS, xapi="Hardman_2026") wdata = r.get("data") or {} report("D7 WMS内部接口 missing 直查", st == 200 and r.get("code") == 0 and wdata.get("missing") == ["NO-SUCH-CODE-9"], json.dumps(wdata, ensure_ascii=False)) # ================= E. 下发 SN 校验 + 握手 ================= print("\n==== E. PLC 下发 SN 校验 ====") def plc_send(body): return api("POST", "/api/v1/plc/send-process", body, token) code, r = plc_send({"orderNo": noG, "sn": "E2E-SN-NOPE", "stationNo": 1, "processCombination": "1"}) report("E1 未进线SN被拒", r.get("code") != 0 and ("不属于" in r.get("message", "") or "未进线" in r.get("message", "")), r.get("message", "")) code, r = online("E2E-SN-001", noG) report("E2 进线E2E-SN-001", r.get("code") == 0, r.get("message", "")) code, r = plc_send({"orderNo": noG, "sn": "E2E-SN-001", "stationNo": 1, "processCombination": "1"}) report("E3 合法SN下发成功", r.get("code") == 0, r.get("message", "")) # 立即再发第二条 → 上一条未收到完成信号 code, r = plc_send({"orderNo": noG, "sn": "E2E-SN-001", "stationNo": 1, "processCombination": "1"}) report("E4 未收到完成信号被拒(握手)", r.get("code") != 0 and "完成信号" in r.get("message", ""), r.get("message", "")) time.sleep(7) code, r = plc_send({"orderNo": noG, "sn": "E2E-SN-001", "stationNo": 1, "processCombination": "1"}) report("E5 完成信号后再次下发成功", r.get("code") == 0, r.get("message", "")) # DONE 工单不可下发:SN 属于该工单才先命中 DONE 校验 noH = "E2E-WO-H-" + today().replace("-", "") create_wo(noH, qty=3) woH = wo_by_no(noH) set_status(woH["id"], "RELEASED"); set_status(woH["id"], "IN_PROGRESS"); set_status(woH["id"], "DONE") online("E2E-SN-H1", noH) code, r = plc_send({"orderNo": noH, "sn": "E2E-SN-H1", "stationNo": 1, "processCombination": "1"}) report("E6 DONE工单禁止下发", r.get("code") != 0 and "已完成" in r.get("message", ""), r.get("message", "")) # ================= F. 流程多工位绑定 ================= print("\n==== F. 流程多工位绑定 ====") def flows(station_no=None): u = "/api/v1/process-flows" + (("?stationNo=" + str(station_no)) if station_no else "") st, r = api("GET", u, token=token) return r.get("data") or [] def save_flow(body): return api("POST", "/api/v1/process-flows", body, token) def flow_status(fid, status): return api("POST", "/api/v1/process-flows/status", {"id": fid, "status": status}, token) def stations(): st, r = api("GET", "/api/v1/stations", token=token) return r.get("data") or [] STEP11 = [{"seq": 1, "name": "装配完成", "collectType": "NONE", "isTorque": False, "remark": "", "criteria": []}] base_st = {x["stationNo"]: (x.get("flowId") or 0) for x in stations()} base_flows = {x["id"]: x for x in flows()} report("F1 列表含 stations 绑定字段", all(("stations" in x) for x in base_flows.values()), str([(x["id"], x.get("stations")) for x in list(base_flows.values())[:3]])) # 负向:启用流程占用冲突 code, r = save_flow({"name": "E2E-冲突流程", "stationNo": 5, "stations": [5], "steps": []}) report("F2 工位已被启用流程占用被拒", r.get("code") != 0 and "已被启用流程" in r.get("message", ""), r.get("message", "")) # 停用 11/12 → 新建临时流程绑 12 → 扩到 11+12 flow_status(11, "INACTIVE"); flow_status(12, "INACTIVE") code, r = save_flow({"name": "E2E-流程-双工位", "stationNo": 12, "stations": [12], "steps": STEP11}) fid = None if r.get("code") == 0: for f in flows(): if f["name"] == "E2E-流程-双工位": fid = f["id"] report("F3 停用后可新建流程绑定工位12", r.get("code") == 0 and fid, r.get("message", "")) code, r = save_flow({"id": fid, "name": "E2E-流程-双工位", "stationNo": 12, "stations": [11, 12], "steps": STEP11}) report("F4 一流程绑多工位(11+12)", r.get("code") == 0, r.get("message", "")) st_map = {x["stationNo"]: (x.get("flowId") or 0) for x in stations()} report("F5 工位11/12均指向临时流程", st_map.get(11) == fid and st_map.get(12) == fid, str(st_map)) fl11 = flows(11) report("F6 按工位号过滤命中临时流程", any(x.get("id") == fid for x in fl11), str([x.get("id") for x in fl11])) # 恢复 code, r = api("DELETE", "/api/v1/process-flows/%d" % fid, token=token) report("F7 删除流程自动解绑工位", r.get("code") == 0, r.get("message", "")) st_map = {x["stationNo"]: (x.get("flowId") or 0) for x in stations()} report("F8 删除后工位解绑", st_map.get(11) == 0 and st_map.get(12) == 0, str(st_map)) flow_status(11, "ACTIVE"); flow_status(12, "ACTIVE") code, r = save_flow({"id": 11, "name": base_flows[11]["name"], "stationNo": 11, "stations": [11], "steps": STEP11}) code2, r2 = save_flow({"id": 12, "name": base_flows[12]["name"], "stationNo": 12, "stations": [12], "steps": STEP11}) report("F9 恢复工位11/12原绑定", r.get("code") == 0 and r2.get("code") == 0, r.get("message", "") + " / " + r2.get("message", "")) st_map = {x["stationNo"]: (x.get("flowId") or 0) for x in stations()} final_ok = st_map.get(11) == 11 and st_map.get(12) == 12 and base_st.get(11) == st_map.get(11) and base_st.get(12) == st_map.get(12) report("F10 恢复后绑定与基线一致", final_ok, str({11: st_map.get(11), 12: st_map.get(12)})) fl11_now = {x["id"]: x for x in flows()} ok11 = len(fl11_now.get(11, {}).get("steps", [])) == len(base_flows.get(11, {}).get("steps", [])) ok12 = len(fl11_now.get(12, {}).get("steps", [])) == len(base_flows.get(12, {}).get("steps", [])) report("F11 恢复后11/12号流程步骤数一致", ok11 and ok12, "11:" + str(len(fl11_now.get(11, {}).get("steps", []))) + "/" + str(len(base_flows.get(11, {}).get("steps", []))) + " 12:" + str(len(fl11_now.get(12, {}).get("steps", []))) + "/" + str(len(base_flows.get(12, {}).get("steps", [])))) # ================= G. 流程卡预检与渲染 ================= print("\n==== G. 流程卡打印预检 ====") st, r = api("GET", "/api/v1/process-card?sn=E2E-SN-001&check=1", token=token) miss = (r.get("data") or {}).get("missing", []) if r.get("code") == 0 else None report("G1 无任何工序记录时预检报缺失", miss is not None and any("工序" in m for m in miss), str(miss)) # 补 1、7 号工序 + 拧紧数据 report_proc("E2E-SN-001", 1, 1) report_proc("E2E-SN-001", 7, 7) code, r = api("POST", "/api/v1/torque/report", {"sn": "E2E-SN-001", "workOrder": noG, "stationNo": "7", "screwNo": "S-001", "torque": 50.0, "angle": 90.0, "result": "OK"}, token) report("G2 拧紧上报OK", r.get("code") == 0, r.get("message", "")) st, r = api("GET", "/api/v1/process-card?sn=E2E-SN-001&check=1", token=token) miss = (r.get("data") or {}).get("missing", []) report("G3 补齐后预检缺失为空", r.get("code") == 0 and miss == [], str(miss)) st, html = api("GET", "/api/v1/process-card?sn=E2E-SN-001", token=token, raw=True) report("G4 打印HTML含产品名/工序/拧紧数据", st == 200 and "测试产品E2E" in html and "装配七" in html and "拧紧数据" in html and "S-001" in html, ("len=%d" % len(html)) if html else "") # ================= 清理 ================= print("\n==== 清理测试数据 ====") q(CLEAN_SQL) left = q("SELECT count(*) FROM work_order WHERE work_order_no LIKE 'E2E-%'") report("清理完成", left == "0", "剩余=" + left) print("\n========== 汇总 ==========") print("PASS %d / FAIL %d" % (len(PASS), len(FAIL))) if FAIL: print("FAILED: " + "; ".join(FAIL)) sys.exit(1) print("全部通过")