# -*- coding: utf-8 -*- """组织导入 / 租户用量 / 文件视图·直传凭证·批量处理 E2E (《03-接口设计》§6.4、§6.5、§6.7、§9.5、§9.7、§9.8) ## 这批接口里三处"看着像细节、其实决定对不对"的地方 ① **组织导入的父子关系用「编码 + 上级编码」表达,不靠行的顺序**。 客户的表是人整理出来的:顺序会被重排、缩进会被吃掉(Excel 里前导空格尤其容易丢)。 所以本脚本特意把**子行排在父行之前** —— 那是客户的表里最常见的形态, 也是"一遍扫描"实现会全部失败的那种。 ② **租户用量的 `ocrCount` / `smsCount` 恒为 0 并带 notes**。 两条通道都还没接,编一个数字会让运营拿着假数据去谈续费。 断言要看的是"**它承认自己没接**",而不是"字段存在"。 ③ **上传凭证的 `existed=true` 时不返回 uploadUrl**。 返回了而前端不用,会让"到底传没传"变成两处判断 —— 迟早不一致。 ## 还有一条不能省的边界 `GET /mp/files/{fileId}` 按 `submitter_id` 授权(学生端);教师看他班学生的材料 走 `/mp/teacher/files/{id}/preview`(按班级范围)。本脚本要证明**前者的越权会被拒** —— 不证明的话,"任何登录用户能看任何人的身份证照片"这种事只在真实越权时才被发现。 跑法:python scripts/e2e-m8-orgfile.py """ import argparse import importlib.util import json import os import sys import urllib.error import urllib.parse sys.stdout.reconfigure(encoding="utf-8") ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PASSED = [] FAILED = [] CREATED_TASK_IDS = [] CREATED_ORG_NAMES = [] TMP_ROSTER = os.path.join(ROOT, "scripts", "_e2e-of-roster.xlsx") TMP_ORG = os.path.join(ROOT, "scripts", "_e2e-of-orgs.xlsx") TINY_JPEG = os.path.join(ROOT, "scripts", "_e2e-of.jpg") def _load(name, fn): spec = importlib.util.spec_from_file_location(name, os.path.join(ROOT, "scripts", fn)) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod R = _load("e2e_m2_roster", "e2e-m2-roster.py") A = _load("e2e-m5-audit", "e2e-m5-audit.py") BASE = "http://localhost:8080" STU = 997201 OTHER = 997202 def ok(name): PASSED.append(name) print(" [OK] " + name) def fail(name, detail): FAILED.append((name, detail)) print(" [FAIL] " + name + "\n " + detail) def check(name, cond, detail=""): if cond: ok(name) else: fail(name, detail or "断言不成立") return bool(cond) def sql(stmt): _, out = R.sql(stmt) return out def body_of(method, path, token=None, payload=None): _, b = R.call(method, BASE + path, token, payload) return b def mp(uid): cfg = R.read_dev_config() return R.mint_mp_token(cfg["jwt_secret"], cfg["issuer"], uid, 1, ["STUDENT"], 4) def upload(path, token, *fields): """用 curl 发 multipart(urllib 手搓 multipart 容易出错)。""" import subprocess args = ["curl", "-s", "-X", "POST", BASE + path, "-H", "Authorization: Bearer " + token] for f in fields: args += ["-F", f] r = subprocess.run(args, capture_output=True, text=True, encoding="utf-8", timeout=120) try: return json.loads(r.stdout) except Exception: return {"code": -1, "msg": (r.stdout or r.stderr or "")[:300]} def main(): ap = argparse.ArgumentParser(description="组织/租户/文件 E2E") ap.parse_args() print("=" * 74) print(" 组织导入 / 租户用量 / 文件视图·直传·批量处理 E2E") print("=" * 74) # ⚠️ 租户相关接口(§6.5 修改 / §6.7 用量)的权限是**运营**(平台级), # 学校管理员调它们会得到 40301 —— 那是权限边界在生效,不是缺陷。 # 所以这里用平台账号。**先清登录失败计数**:`LoginGuard` 连续失败后会要求 # 图形验证码(`xt:login:fail:{account}`),而验证码是图片、脚本给不出来。 # 这不是绕过安全,而是"测试夹具要能清掉自己造成的失败计数"。 for acct in ("platform", "admin"): os.system('docker exec xt-redis redis-cli -a redis_dev_pwd --no-auth-warning ' 'DEL xt:login:fail:%s > NUL 2>&1' % acct) # 平台运营账号的密码在 docs/07-开发实施计划.md 里(Platform@123),不是 Admin@123 _, ops = R.login(BASE, "platform", "Platform@123", None) if ops.get("code") != 0: print(" x 平台运营账号登录失败(先清 Redis 里的登录失败计数):" + str(ops)) return 2 ops_token = ops["data"]["token"] _, adm = R.login(BASE, "admin", "Admin@123", "demo") if adm.get("code") != 0: print(" x 管理员登录失败:" + str(adm)) return 2 at = adm["data"]["token"] # ── 1. §6.4 组织导入 ── print("\n[1] §6.4 组织批量导入") suffix = os.urandom(3).hex() root = "E2E学院" + suffix mid = "E2E系" + suffix leaf = "E2E班" + suffix CREATED_ORG_NAMES.extend([root, mid, leaf]) # ⚠️ 刻意把**子行排在父行之前**:客户的表里最常见, # 也是"一遍扫描"实现会全部失败的那种形态 rows = [ # 层级 / 名称 / 编码 / 上级编码 / 负责人手机号 ("3", leaf, "C" + suffix, "B" + suffix, ""), ("1", root, "A" + suffix, "", ""), ("2", mid, "B" + suffix, "A" + suffix, ""), ] _build_org_xlsx(rows) dry = upload("/api/v1/orgs/import", at, "file=@%s;filename=orgs.xlsx;type=application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" % TMP_ORG, "dryRun=true") check("dryRun 可用", dry.get("code") == 0, json.dumps(dry, ensure_ascii=False)[:250]) dd = dry.get("data") or {} check("⭐ dryRun 报告 created=3(子行在前也能算对层级)", dd.get("created") == 3, json.dumps(dd, ensure_ascii=False)[:250]) check("dryRun **没有**落库", sql("select count(*) from sys_org where org_name='%s';" % leaf).strip() == "0", sql("select count(*) from sys_org where org_name='%s';" % leaf)) imp = upload("/api/v1/orgs/import", at, "file=@%s;filename=orgs.xlsx;type=application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" % TMP_ORG, "dryRun=false") check("正式导入成功", imp.get("code") == 0, json.dumps(imp, ensure_ascii=False)[:250]) d = imp.get("data") or {} check("created=3", d.get("created") == 3, json.dumps(d, ensure_ascii=False)) check("失败 0 / 报错列表为空", d.get("failed") == 0 and not d.get("errors"), json.dumps(d, ensure_ascii=False)[:250]) check("⭐ 库里真的有这三个组织", sql("select count(*) from sys_org where org_name in ('%s','%s','%s');" % (root, mid, leaf)).strip() == "3", sql("select org_name, org_code from sys_org where org_name like 'E2E%%%s';" % suffix) .replace("\n", " | ")) # 父子关系:子行的 parent 是父行的 id check("⭐ 子行的父级接对了(按上级编码,而不是按行序)", sql("""select count(*) from sys_org c join sys_org p on p.id = c.parent_id where c.org_name = '%s' and p.org_name = '%s';""" % (mid, root)).strip() == "1", sql("""select c.org_name, p.org_name from sys_org c left join sys_org p on p.id = c.parent_id where c.org_name in ('%s','%s');""" % (mid, leaf)).replace("\n", " | ")) check("⭐ ancestor_path 被补上了(按层级筛选靠它,漏了会静默少数据)", sql("select coalesce(ancestor_path,'') from sys_org where org_name='%s';" % leaf).strip() not in ("", "None"), "leaf.ancestor_path = " + sql("select coalesce(ancestor_path,'(空)') " "from sys_org where org_name='%s';" % leaf).strip()) # 重复导入:已存在的按编码命中,不重复建 imp2 = upload("/api/v1/orgs/import", at, "file=@%s;filename=orgs.xlsx" % TMP_ORG) check("⭐ 重复导入按编码命中已存在的(不重复建)", (imp2.get("data") or {}).get("updated") == 3 and sql("select count(*) from sys_org where org_name='%s';" % leaf).strip() == "1", json.dumps(imp2.get("data"), ensure_ascii=False)[:250]) # ⚠️ 「认不出表头」这一条必须**放在最后**:它会把坏表头写进同一个临时文件, # 而上面的"重复导入"复用那份文件 —— 顺序反了的话, # 重复导入实际导的是坏表头那份,报错信息与真正原因完全无关 # 认不出表头要明确报错 _build_org_xlsx([("3", "X", "C", "B", "")], header=["甲", "乙", "丙", "丁", "戊"]) bad = upload("/api/v1/orgs/import", at, "file=@%s;filename=bad.xlsx" % TMP_ORG) check("⭐ 认不出表头 → 明确报错并给出应有的列名", bad.get("code") != 0 and "组织名称" in (bad.get("msg") or ""), "code=%s msg=%s" % (bad.get("code"), bad.get("msg"))) # ── 2. §6.5 / §6.7 租户修改与用量 ── print("\n[2] §6.5 / §6.7 租户修改与用量") # 学 校 管 理 员 调 租 户 接 口 应 该 被 拒(权限边界) forbidden = body_of("PUT", "/api/v1/tenants/1", at, {"contactName": "E2E不该改成"}) check("⭐ 学校管理员改租户 → 40301(这是「运营」权限,不是「管理」)", forbidden.get("code") == 40301, "code=%s msg=%s" % (forbidden.get("code"), forbidden.get("msg"))) up = body_of("PUT", "/api/v1/tenants/1", ops_token, {"contactName": "张校长(E2E改)"}) check("修改租户成功", up.get("code") == 0, json.dumps(up, ensure_ascii=False)[:250]) check("联系人改掉了", sql("select contact_name from sys_tenant where id=1;").strip() == "张校长(E2E改)", sql("select contact_name from sys_tenant where id=1;").strip()) check("⭐ tenantCode 没被改动(它出现在域名解析与对接文档里)", sql("select tenant_code from sys_tenant where id=1;").strip() == "demo", sql("select tenant_code from sys_tenant where id=1;").strip()) bad = body_of("PUT", "/api/v1/tenants/1", ops_token, {"tenantType": 9}) check("租户类型越界 → 明确报错", bad.get("code") != 0, "code=%s msg=%s" % (bad.get("code"), bad.get("msg"))) check("不存在的租户 → 明确报错", body_of("PUT", "/api/v1/tenants/999999999", ops_token, {"contactName": "x"}).get("code") != 0) us = body_of("GET", "/api/v1/tenants/1/usage", ops_token) check("用量接口可用", us.get("code") == 0, json.dumps(us, ensure_ascii=False)[:250]) u = us.get("data") or {} check("带学生数与存储", u.get("studentCount") is not None and u.get("storageMb") is not None, json.dumps(u, ensure_ascii=False)[:250]) check("带 quota", isinstance(u.get("quota"), dict) and u["quota"].get("studentLimit"), json.dumps(u.get("quota"), ensure_ascii=False)) check("⭐ ocrCount / smsCount 恒为 0,**并带 notes 说明为什么**" "(编一个数字会让运营拿假数据去谈续费)", u.get("ocrCount") == 0 and u.get("smsCount") == 0 and len(u.get("notes") or []) >= 2, json.dumps({k: u.get(k) for k in ("ocrCount", "smsCount", "notes")}, ensure_ascii=False)[:300]) us2 = body_of("GET", "/api/v1/tenants/1/usage?period=2026Q3", ops_token) check("⭐ period 支持季度写法(2026Q3)", us2.get("code") == 0 and (us2.get("data") or {}).get("period") == "2026Q3", json.dumps((us2.get("data") or {}).get("period"), ensure_ascii=False)) check("period 格式不对 → 明确报错", body_of("GET", "/api/v1/tenants/1/usage?period=" + urllib.parse.quote("去年"), ops_token) .get("code") != 0) b = body_of("GET", "/api/v1/tenants/1/usage", mp(997299)) check("学生看租户用量 → 403", b.get("code") in (40101, 40301, 403), "code=%s" % b.get("code")) # ── 3. 文件夹具 ── print("\n[3] 摆文件夹具(一个任务 + 一份草稿 + 一个材料文件)") _, b = R.call("POST", BASE + "/api/v1/tasks", at, { "taskName": "E2E文件 " + suffix, "batchNo": "202609", "majorName": "数控加工", "collectMode": 1, "certType": 1, "startTime": "2026-01-01", "endTime": "2026-12-31" }) task_id = b["data"]["id"] CREATED_TASK_IDS.append(task_id) _, b = R.call("GET", BASE + "/api/v1/orgs/tree", at) classes = [] def walk(nodes): for n in nodes or []: if n.get("orgType") == 3: classes.append(n) walk(n.get("children")) walk(b.get("data") or []) org = classes[0] R.call("PUT", BASE + "/api/v1/tasks/%d/orgs" % task_id, at, {"orgs": [{"orgId": org["id"], "expectedCount": 1}]}) R.call("POST", BASE + "/api/v1/tasks/%d/materials/apply-template" % task_id, at, {"templateCode": "TPL_SCHOOL_ROLL"}) _build_roster([("文件甲", R.with_checksum("36102920080808091"))]) body = R.upload_roster(BASE, at, task_id, TMP_ROSTER, extra={"dryRun": "false", "mode": "APPEND", "orgId": org["id"]}) check("夹具:名册就绪", body.get("code") == 0, json.dumps(body, ensure_ascii=False)[:180]) R.call("POST", BASE + "/api/v1/tasks/%d/publish" % task_id, at, {"force": True}) os.system('docker exec xt-redis redis-cli -a redis_dev_pwd --no-auth-warning ' 'DEL xt:mp:gate:user:%d > NUL 2>&1' % STU) tok = mp(STU) _, b = R.call("POST", BASE + "/api/v1/mp/roster/match", tok, {"taskId": task_id, "realName": "文件甲", "idCardNo": R.with_checksum("36102920080808091")}) if b.get("code") != 0: print(" x 门禁核验失败:" + json.dumps(b, ensure_ascii=False)[:200]) return 2 _, b = R.call("POST", BASE + "/api/v1/mp/submissions", tok, {"taskId": task_id}) sid = b["data"]["id"] check("夹具:学生建了草稿", b.get("code") == 0, json.dumps(b, ensure_ascii=False)[:150]) _make_jpeg() up_res = upload("/api/v1/mp/files/upload", tok, "submissionId=%d" % sid, "materialCode=IDCARD", "file=@%s;filename=乱名字.jpg;type=image/jpeg" % TINY_JPEG) check("夹具:上传一份材料", up_res.get("code") == 0, json.dumps(up_res, ensure_ascii=False)[:250]) file_id = (up_res.get("data") or {}).get("id") or (up_res.get("data") or {}).get("fileId") check("夹具:拿到 fileId", isinstance(file_id, int), str(file_id)) # ── 4. §9.8 预览 ── print("\n[4] §9.8 预览(签名 URL + 元信息)") v = body_of("GET", "/api/v1/mp/files/%d" % file_id, tok) check("预览可用", v.get("code") == 0, json.dumps(v, ensure_ascii=False)[:250]) vd = v.get("data") or {} check("带 previewUrl / standardName / 尺寸", vd.get("previewUrl") and vd.get("standardName") and vd.get("width"), json.dumps(vd, ensure_ascii=False)[:300]) check("带 expireSeconds(签名 URL 有有效期,不能长期保存)", vd.get("expireSeconds") and vd.get("expireSeconds") > 0, str(vd.get("expireSeconds"))) check("⭐ 带 actualKind(请求 original 时原始件可能不存在,会退到规范件)", vd.get("actualKind") in ("ORIGINAL", "STANDARD"), str(vd.get("actualKind"))) vo = body_of("GET", "/api/v1/mp/files/%d?prefer=original" % file_id, tok) check("prefer=original 也能用(原始件默认不保留时会退到规范件并标注)", vo.get("code") == 0 and (vo.get("data") or {}).get("actualKind") in ("ORIGINAL", "STANDARD"), json.dumps(vo.get("data"), ensure_ascii=False)[:250]) check("⭐ **别人的文件** → 拒绝(不区分「不存在」与「不是你的」,避免枚举)", body_of("GET", "/api/v1/mp/files/%d" % file_id, mp(OTHER)).get("code") != 0, "code=%s" % body_of("GET", "/api/v1/mp/files/%d" % file_id, mp(OTHER)).get("code")) check("不存在的文件 → 明确报错", body_of("GET", "/api/v1/mp/files/999999999", tok).get("code") != 0) check("未登录 → 40101", body_of("GET", "/api/v1/mp/files/%d" % file_id).get("code") == 40101) # ── 5. §9.5 上传凭证 ── print("\n[5] §9.5 上传凭证(预检 + 秒传 + 压缩提示)") tk = body_of("POST", "/api/v1/mp/files/upload-token", tok, { "taskId": task_id, "materialCode": "PHOTO1", "fileName": "相册照片.jpg", "fileSize": 300000, "contentType": "image/jpeg" }) check("凭证接口可用", tk.get("code") == 0, json.dumps(tk, ensure_ascii=False)[:250]) td = tk.get("data") or {} check("ok=true 且没命中秒传", td.get("ok") is True and td.get("existed") is False, json.dumps(td, ensure_ascii=False)[:250]) check("⭐ 带压缩提示(让学生在自己手机上压,省的是他的流量与等待)", (td.get("clientCompressHint") or {}).get("enabled") is True, json.dumps(td.get("clientCompressHint"), ensure_ascii=False)) check("带 objectKey / uploadToken / expireSeconds=900", td.get("objectKey") and td.get("uploadToken") and td.get("expireSeconds") == 900, json.dumps({k: td.get(k) for k in ("objectKey", "expireSeconds")}, ensure_ascii=False)) check("⭐ 一期 direct=false(七牛直传要先把「上传完成回调」打通,见 F-71)", td.get("direct") is False and len(td.get("notes") or []) >= 1, json.dumps({k: td.get(k) for k in ("direct", "notes")}, ensure_ascii=False)[:250]) # 体积超限:**不传字节就能拦** big = body_of("POST", "/api/v1/mp/files/upload-token", tok, { "taskId": task_id, "materialCode": "PHOTO1", "fileName": "巨图.jpg", "fileSize": 8 * 1024 * 1024, "contentType": "image/jpeg" }) bd = big.get("data") or {} check("⭐ 体积超限 → ok=false + **能照着做的原因**(说清上限与当前值)", bd.get("ok") is False and "KB" in (bd.get("rejectReason") or ""), json.dumps(bd, ensure_ascii=False)[:250]) check("超限时压缩提示里 needed=true", (bd.get("clientCompressHint") or {}).get("needed") is True, json.dumps(bd.get("clientCompressHint"), ensure_ascii=False)) # 份数超限(IDCARD 已经传了 1 份,模板 maxCount=2 → 再传 1 份可以,第 3 份不行) for i in range(1): upload("/api/v1/mp/files/upload", tok, "submissionId=%d" % sid, "materialCode=IDCARD", "file=@%s;filename=第二次.jpg;type=image/jpeg" % TINY_JPEG) full = body_of("POST", "/api/v1/mp/files/upload-token", tok, { "taskId": task_id, "materialCode": "IDCARD", "fileName": "第三张.jpg", "fileSize": 100000, "contentType": "image/jpeg" }) fd = full.get("data") or {} check("⭐ 份数超限 → ok=false 并说清「最多几份、你已经传了几份、要换请先删」", fd.get("ok") is False and "最多" in (fd.get("rejectReason") or ""), json.dumps(fd, ensure_ascii=False)[:250]) # 秒传:拿已经传过那份的 fileHash real_hash = sql("select file_hash from collect_file where id=%d;" % file_id).strip() fast = body_of("POST", "/api/v1/mp/files/upload-token", tok, { "taskId": task_id, "materialCode": "IDCARD", "fileName": "同一张.jpg", "fileSize": 100, "contentType": "image/jpeg", "fileHash": real_hash }) fd2 = fast.get("data") or {} check("⭐ 秒传命中(同一个人在同一个槽位传同样的文件)→ existed=true", fd2.get("existed") is True and fd2.get("fileId") == file_id, json.dumps(fd2, ensure_ascii=False)[:250]) check("⭐ 秒传时**不给 uploadUrl**(返回了而前端不用,会让「到底传没传」变成两处判断)", fd2.get("uploadUrl") is None, str(fd2.get("uploadUrl"))) bad = body_of("POST", "/api/v1/mp/files/upload-token", tok, { "taskId": task_id, "materialCode": "NO_SUCH_MATERIAL", "fileName": "x.jpg", "fileSize": 100, "contentType": "image/jpeg" }) check("材料码不存在 → 明确报错", bad.get("code") != 0, "code=%s msg=%s" % (bad.get("code"), bad.get("msg"))) # ── 6. §9.7 批量处理 ── print("\n[6] §9.7 批量处理(多材料一次提交)") bp = body_of("POST", "/api/v1/mp/files/batch-process", tok, { "submissionId": sid, "fileIds": [file_id], "async": True }) check("批量处理可用", bp.get("code") == 0, json.dumps(bp, ensure_ascii=False)[:250]) bpr = bp.get("data") or {} check("带 batchToken / total / results / summary", bpr.get("batchToken") and bpr.get("total") == 1 and bpr.get("summary"), json.dumps(bpr, ensure_ascii=False)[:250]) item = (bpr.get("results") or [{}])[0] check("结果项带 fileId 与 status", item.get("fileId") == file_id and item.get("status") is not None, json.dumps(item, ensure_ascii=False)) check("⭐ 一期 conflict 恒为 false、detectMaterialCode 恒为空,**并说明为什么**" "(不是「识别后没问题」,而是「还没识别」)", item.get("conflict") is False and item.get("detectMaterialCode") is None and len(bpr.get("notes") or []) >= 1, json.dumps({k: item.get(k) for k in ("conflict", "detectMaterialCode")}, ensure_ascii=False) + " notes=" + str(bpr.get("notes"))[:120]) over = body_of("POST", "/api/v1/mp/files/batch-process", tok, { "submissionId": sid, "fileIds": list(range(1, 25)) }) check("⭐ 超过 20 个 → 40011(与批量审批同一个上限口径)", over.get("code") == 40011, "code=%s msg=%s" % (over.get("code"), over.get("msg"))) foreign = body_of("POST", "/api/v1/mp/files/batch-process", tok, { "submissionId": sid, "fileIds": [999999999] }) check("不属于本填报单的文件 → 记成失败项而不是整批失败", (foreign.get("data") or {}).get("summary", {}).get("failed") == 1, json.dumps(foreign.get("data"), ensure_ascii=False)[:250]) noids = body_of("POST", "/api/v1/mp/files/batch-process", tok, {"submissionId": sid}) check("不传 fileIds → 明确报错", noids.get("code") != 0, "code=%s msg=%s" % (noids.get("code"), noids.get("msg"))) cleanup() print("\n" + "=" * 74) print("通过 %d 失败 %d 共 %d" % (len(PASSED), len(FAILED), len(PASSED) + len(FAILED))) print("=" * 74) for n, det in FAILED: print(" x " + n + "\n " + det) return 1 if FAILED else 0 def _build_org_xlsx(rows, header=None): from openpyxl import Workbook wb = Workbook() ws = wb.active ws.title = "组织" head = header or ["层级", "组织名称", "组织编码", "上级编码", "负责人手机号"] for c, t in enumerate(head, start=1): ws.cell(row=1, column=c, value=t) for i, r in enumerate(rows): for c, v in enumerate(r, start=1): ws.cell(row=i + 2, column=c, value=v) wb.save(TMP_ORG) def _build_roster(rows): from openpyxl import Workbook wb = Workbook() ws = wb.active ws.title = "文件班" header = ["序号", "姓名", "身份证号", "学历", "联系电话", "工作单位", "报考职业", "报考工种", "报考等级", "是否补考", "补考理论", "补考实操", "补考综合", "是否在校生", "在读学校", "所学专业", "入学时间"] for c, t in enumerate(header, start=1): ws.cell(row=1, column=c, value=t) for i, (name, idc) in enumerate(rows): vals = [i + 1, name, idc, "中专", "1380013880%d" % i, "无", "车工", "数控车工", "四级", "否", "", "", "", "是", "示范职业学校", "数控加工", "2024-09-01"] for c, v in enumerate(vals, start=1): if v != "": ws.cell(row=i + 2, column=c, value=v) wb.save(TMP_ROSTER) def _make_jpeg(): import base64 b64 = ("/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0a" "HBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/wAALCAABAAEBAREA/8QAFAABAAAAAAAA" "AAAAAAAAAAAACf/EABQQAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQEAAD8AKp//2Q==") with open(TINY_JPEG, "wb") as f: f.write(base64.b64decode(b64)) def cleanup(): for tid in CREATED_TASK_IDS: R.sql("delete from collect_validation_issue where task_id=%d;" % tid) R.sql("delete from collect_audit_record where task_id=%d;" % tid) R.sql("delete from collect_notify_record where task_id=%d;" % tid) R.sql("delete from collect_file where task_id=%d;" % tid) R.sql("delete from collect_submission where task_id=%d;" % tid) R.sql("delete from collect_roster where task_id=%d;" % tid) R.sql("delete from collect_task_org where task_id=%d;" % tid) R.sql("delete from collect_material_def where task_id=%d;" % tid) R.sql("delete from collect_task where id=%d;" % tid) for name in CREATED_ORG_NAMES: R.sql("delete from sys_org where org_name = '%s';" % name) R.sql("update sys_tenant set contact_name='张校长' where id=1;") R.sql("delete from collect_message where user_id in (997201, 997202, 997299);") for p in (TMP_ROSTER, TMP_ORG, TINY_JPEG): if os.path.exists(p): os.remove(p) print(" 已清理:任务 / 材料文件 / 测试组织 / 租户联系人复原 / 临时文件") if __name__ == "__main__": raise SystemExit(main())