30c3714d35
新增:check-api-parity(契约 121 接口 ↔ 控制器,自带「必须恰好 121」自检)、 check-entity-columns(基类要求的列 vs 迁移建表列,F-35 那个坑踩了四次之后上的工具)、 check-manual(手册引用的错误码与提示文案对得上代码)、check-test-profile(test profile 真起一次)、 e2e-prodguard、load-test(四场景、按端点分别统计)、verify-all(一条命令跑完交付自检)、 dev-seed-audit(界面验收用的演示数据),以及 M1~M9 各轮 E2E。 压测抓到一个真缺陷并修掉(F-92):门禁 IP 限流原来计「所有请求」,于是一个班连同一个 WiFi 时 60 个不同学生同时核验只过了 20 个 → 改为只计失败 + 单独的成功上限,20/60 → 60/60。 度量工具的偏差会伪装成产品缺陷(F-60/F-77/F-97),所以每个脚本都断言「动作自己的结论字段」 (submitted / created / canExport),而不是 HTTP 200 + code 0。 验证:verify-all.py --quick 5/5;--with-e2e 见 docs/09 §8。
416 lines
21 KiB
Python
416 lines
21 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""催报 E2E(《03-接口设计》§8.6、§10.7)。
|
||
|
||
催报这一屏的难点不在"怎么发",而在**发给谁、谁不该再收到**。四条边界:
|
||
|
||
1. **只催未提交者**:未填(0) 与草稿(1) 都算未提交 —— 填了一半忘了交的人
|
||
与完全没填的人,对老师来说要做的是同一件事。
|
||
但**已驳回(4) 不催**:他要的是"改哪里"(驳回通知),催报文案对他没意义。
|
||
2. **范围 = 我负责的班**:请求里带的 orgIds 会与"我负责的班"取交集 ——
|
||
直接信前端传来的 orgIds,等于把"能催谁"交给前端决定。
|
||
3. **不重复骚扰**:`excludeRecentHours` 默认 24 小时。
|
||
老师点两下"一键催报",学生只该收到一条 —— 去重责任在服务端,不在老师的自觉。
|
||
4. **催不到的人要说清为什么**:名册里有他、但他还没扫码核验(没有账号),
|
||
这时不能笼统报"发送失败",而要说"还没核对身份,无法触达"。
|
||
|
||
跑法:python scripts/e2e-m5-urge.py
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import importlib.util
|
||
import json
|
||
import os
|
||
import sys
|
||
|
||
sys.stdout.reconfigure(encoding="utf-8")
|
||
|
||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||
PASSED: list[str] = []
|
||
FAILED: list[tuple[str, str]] = []
|
||
CREATED_TASK_IDS: list[int] = []
|
||
TMP_ROSTER = os.path.join(ROOT, "scripts", "_e2e-urge-roster.xlsx")
|
||
|
||
T1 = 993701 # 班主任甲(A 班初审人)
|
||
T2 = 993702 # 班主任乙(B 班初审人)
|
||
S1, S2, S3 = 993711, 993712, 993713 # A 班三个学生
|
||
|
||
|
||
def _load(module_name: str, filename: str):
|
||
path = os.path.join(ROOT, "scripts", filename)
|
||
spec = importlib.util.spec_from_file_location(module_name, path)
|
||
mod = importlib.util.module_from_spec(spec)
|
||
spec.loader.exec_module(mod)
|
||
return mod
|
||
|
||
|
||
R = _load("e2e_m2_roster", "e2e-m2-roster.py")
|
||
S = _load("e2e_m3_submit", "e2e-m3-submit.py")
|
||
A = _load("e2e_m5_audit", "e2e-m5-audit.py")
|
||
|
||
BASE = "http://localhost:8080"
|
||
|
||
|
||
def ok(name: str) -> None:
|
||
PASSED.append(name)
|
||
print(f" [OK] {name}")
|
||
|
||
|
||
def fail(name: str, detail: str) -> None:
|
||
FAILED.append((name, detail))
|
||
print(f" [FAIL] {name}\n {detail}")
|
||
|
||
|
||
def check(name: str, cond: bool, detail: str = "") -> bool:
|
||
if cond:
|
||
ok(name)
|
||
else:
|
||
fail(name, detail or "断言不成立")
|
||
return cond
|
||
|
||
|
||
def sql(stmt: str) -> str:
|
||
_, out = R.sql(stmt)
|
||
return out
|
||
|
||
|
||
def body_of(method: str, path: str, token: str | None = None, payload: dict | None = None) -> dict:
|
||
_, b = R.call(method, f"{BASE}{path}", token, payload)
|
||
return b
|
||
|
||
|
||
def mp(uid: int) -> str:
|
||
cfg = R.read_dev_config()
|
||
return R.mint_mp_token(cfg["jwt_secret"], cfg["issuer"], uid, 1, ["STUDENT"], 4)
|
||
|
||
|
||
def ensure_teacher(uid: int, username: str, name: str, org_id: int) -> str:
|
||
sql(f"""
|
||
INSERT INTO sys_user (id, tenant_id, username, real_name, password, org_id, user_type, status)
|
||
VALUES ({uid}, 1, '{username}', '{name}',
|
||
'$2a$10$6NB7sObvoHRwzhLJwZ2CHOAbdECgMJGiiTeMY6.zo3IJyd8TEKLEO', {org_id}, 3, 1)
|
||
ON CONFLICT (id) DO UPDATE SET real_name = EXCLUDED.real_name, org_id = EXCLUDED.org_id,
|
||
status = 1
|
||
""")
|
||
role_id = sql("select id from sys_role where tenant_id=1 and role_code='HEAD_TEACHER' "
|
||
"and deleted=0 limit 1;").strip()
|
||
sql(f"delete from sys_user_role where user_id={uid};")
|
||
sql(f"INSERT INTO sys_user_role (user_id, role_id, tenant_id) VALUES ({uid}, {role_id}, 1);")
|
||
_, b = R.login(BASE, username, "Admin@123", "demo")
|
||
if b.get("code") != 0:
|
||
raise SystemExit(f" 班主任 {username} 登录失败:{b}")
|
||
return b["data"]["token"]
|
||
|
||
|
||
def main() -> int:
|
||
ap = argparse.ArgumentParser(description="催报 E2E")
|
||
ap.parse_args()
|
||
|
||
print("=" * 74)
|
||
print(" 催报 E2E(§8.6 / §10.7)")
|
||
print("=" * 74)
|
||
|
||
_, adm = R.login(BASE, "admin", "Admin@123", "demo")
|
||
if adm.get("code") != 0:
|
||
print(f" ✗ 管理员登录失败:{adm}")
|
||
return 2
|
||
at = adm["data"]["token"]
|
||
|
||
# ── 0. 夹具:一个任务、两个班、两个班主任、A 班三个学生 ──
|
||
print("\n[0] 摆夹具")
|
||
suffix = os.urandom(3).hex()
|
||
_, b = R.call("POST", f"{BASE}/api/v1/tasks", at, {
|
||
"taskName": f"E2E催报 {suffix}", "batchNo": "202609", "majorName": "数控加工",
|
||
"collectMode": 1, "certType": 1, "startTime": "2026-01-01", "endTime": "2026-12-31"
|
||
})
|
||
if b.get("code") != 0:
|
||
print(f" ✗ 建任务失败:{b}")
|
||
return 2
|
||
task_id = b["data"]["id"]
|
||
CREATED_TASK_IDS.append(task_id)
|
||
|
||
_, b = R.call("GET", f"{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_a, org_b = classes[0], classes[1]
|
||
|
||
t1 = ensure_teacher(T1, f"urgeT1{suffix}", "催报班主任甲", org_a["id"])
|
||
t2 = ensure_teacher(T2, f"urgeT2{suffix}", "催报班主任乙", org_b["id"])
|
||
ok("两个班主任账号就绪")
|
||
|
||
R.call("PUT", f"{BASE}/api/v1/tasks/{task_id}/orgs", at, {"orgs": [
|
||
{"orgId": org_a["id"], "expectedCount": 3, "headUserId": T1},
|
||
{"orgId": org_b["id"], "expectedCount": 1, "headUserId": T2}
|
||
]})
|
||
R.call("POST", f"{BASE}/api/v1/tasks/{task_id}/materials/apply-template", at,
|
||
{"templateCode": "TPL_SCHOOL_ROLL"})
|
||
|
||
rows = [("催报甲", R.with_checksum("36102920080101031")),
|
||
("催报乙", R.with_checksum("36102920080202032")),
|
||
("催报丙", R.with_checksum("36102920080303033"))]
|
||
_build_roster(rows)
|
||
body = R.upload_roster(BASE, at, task_id, TMP_ROSTER,
|
||
extra={"dryRun": "false", "mode": "APPEND", "orgId": org_a["id"]})
|
||
check("名册导入成功(A 班 3 人)", body.get("code") == 0,
|
||
json.dumps(body, ensure_ascii=False)[:200])
|
||
R.call("POST", f"{BASE}/api/v1/tasks/{task_id}/publish", at, {"force": True})
|
||
|
||
# ── 1. 学生甲:绑定 + 提交(不该被催)──
|
||
# 学生乙:绑定 + 只建草稿(**算未提交,该被催**)
|
||
# 学生丙:不绑定(**催不到,要说清为什么**)
|
||
print("\n[1] 三种状态:已提交 / 只有草稿 / 还没核对身份")
|
||
tok1 = mp(S1)
|
||
_, b = R.call("POST", f"{BASE}/api/v1/mp/roster/match", tok1,
|
||
{"taskId": task_id, "realName": rows[0][0], "idCardNo": rows[0][1]})
|
||
check("甲门禁绑定成功", b.get("code") == 0, json.dumps(b, ensure_ascii=False)[:150])
|
||
_, b = R.call("POST", f"{BASE}/api/v1/mp/submissions", tok1, {"taskId": task_id})
|
||
sid1 = b["data"]["id"]
|
||
res = A.prepare_student(tok1, sid1, task_id, {
|
||
"work_unit": "无", "is_retake": False, "is_student": True,
|
||
"school": "示范职业学校", "major": "数控加工", "enroll_date": "2024-09-01"
|
||
}, dup_first_file=False)
|
||
check("甲已提交", (res.get("data") or {}).get("submitted") is True,
|
||
json.dumps(res, ensure_ascii=False)[:200])
|
||
|
||
tok2 = mp(S2)
|
||
_, b = R.call("POST", f"{BASE}/api/v1/mp/roster/match", tok2,
|
||
{"taskId": task_id, "realName": rows[1][0], "idCardNo": rows[1][1]})
|
||
check("乙门禁绑定成功", b.get("code") == 0, json.dumps(b, ensure_ascii=False)[:150])
|
||
_, b = R.call("POST", f"{BASE}/api/v1/mp/submissions", tok2, {"taskId": task_id})
|
||
check("乙只建了草稿(没提交)", b.get("code") == 0, json.dumps(b, ensure_ascii=False)[:150])
|
||
|
||
check("丙没绑定(名册里有、没有账号)",
|
||
sql(f"select count(*) from collect_roster where task_id={task_id} "
|
||
f"and bind_user_id is null;").strip() == "1")
|
||
|
||
# ── 2. 一键催报:只催未提交者 ──
|
||
print("\n[2] 一键催报(只催未提交者)")
|
||
r = body_of("POST", f"/api/v1/mp/teacher/notify/unsubmitted", t1,
|
||
{"orgIds": [org_a["id"]], "taskId": task_id})
|
||
check("一键催报可用", r.get("code") == 0, json.dumps(r, ensure_ascii=False)[:250])
|
||
d = r.get("data") or {}
|
||
check("⭐ 已提交的甲**没被催**",
|
||
all(x.get("realName") != rows[0][0] for x in (d.get("details") or [])),
|
||
json.dumps(d.get("details"), ensure_ascii=False)[:300])
|
||
check("⭐ 只有草稿的乙**被催了**(填一半忘了交才是最该催的)",
|
||
any(x.get("realName") == rows[1][0] and x.get("skipped") is not True
|
||
for x in (d.get("details") or [])),
|
||
json.dumps(d.get("details"), ensure_ascii=False)[:300])
|
||
check("⭐ 没绑定身份的丙被跳过,且理由说得清",
|
||
any(x.get("realName") == rows[2][0] and x.get("skipped") is True
|
||
and "身份" in (x.get("reason") or "")
|
||
for x in (d.get("details") or [])),
|
||
json.dumps(d.get("details"), ensure_ascii=False)[:300])
|
||
check("明细条数 = 未提交的 2 人", len(d.get("details") or []) == 2, f"{len(d.get('details') or [])}")
|
||
check("sent + failed + skipped = 明细条数",
|
||
(d.get("sent") or 0) + (d.get("failed") or 0) + (d.get("skipped") or 0)
|
||
== len(d.get("details") or []),
|
||
json.dumps({k: d.get(k) for k in ("sent", "failed", "skipped")}))
|
||
|
||
# ── 3. 站内信真的写进去了,且文案按"草稿"分叉 ──
|
||
print("\n[3] 催报真的落到学生手里(站内信)")
|
||
check("乙收到一条催报站内信",
|
||
sql(f"select count(*) from collect_message where user_id={S2} and biz_type=5 "
|
||
f"and deleted=0;").strip() == "1",
|
||
sql(f"select count(*) from collect_message where user_id={S2};"))
|
||
title2 = sql(f"select title from collect_message where user_id={S2} order by id desc limit 1;").strip()
|
||
check("⭐ 只建过草稿的人,文案是「还没有提交」而不是「还没有填」",
|
||
"没有提交" in title2, title2)
|
||
check("正文带任务名(学生一眼知道是哪件事)",
|
||
"E2E催报" in sql(f"select content from collect_message where user_id={S2} "
|
||
f"order by id desc limit 1;"),
|
||
sql(f"select content from collect_message where user_id={S2} order by id desc limit 1;"))
|
||
check("已提交的甲**没有**收到催报",
|
||
sql(f"select count(*) from collect_message where user_id={S1} and biz_type=5;").strip() == "0")
|
||
|
||
# ── 4. 防重复骚扰 ──
|
||
print("\n[4] 防重复骚扰(excludeRecentHours)")
|
||
r2 = body_of("POST", f"/api/v1/mp/teacher/notify/unsubmitted", t1,
|
||
{"orgIds": [org_a["id"]], "taskId": task_id})
|
||
d2 = r2.get("data") or {}
|
||
check("⭐ 24 小时内再催 → 乙被跳过(老师点两下不该发两条)",
|
||
any(x.get("realName") == rows[1][0] and x.get("skipped") is True
|
||
and "已催过" in (x.get("reason") or "")
|
||
for x in (d2.get("details") or [])),
|
||
json.dumps(d2.get("details"), ensure_ascii=False)[:300])
|
||
check("乙的催报消息仍然是 1 条(没有堆成 2 条)",
|
||
sql(f"select count(*) from collect_message where user_id={S2} and biz_type=5;").strip() == "1",
|
||
sql(f"select count(*) from collect_message where user_id={S2} and biz_type=5;"))
|
||
r3 = body_of("POST", f"/api/v1/mp/teacher/notify/unsubmitted", t1,
|
||
{"orgIds": [org_a["id"]], "taskId": task_id, "excludeRecentHours": 0})
|
||
d3 = r3.get("data") or {}
|
||
check("显式传 excludeRecentHours=0 → 允许再催(老师确实想再提醒一次)",
|
||
(d3.get("sent") or 0) >= 1, json.dumps(d3.get("details"), ensure_ascii=False)[:300])
|
||
check("这时乙的催报消息变成 2 条",
|
||
sql(f"select count(*) from collect_message where user_id={S2} and biz_type=5;").strip() == "2",
|
||
sql(f"select count(*) from collect_message where user_id={S2} and biz_type=5;"))
|
||
check("⭐ 去重是**按任务**的(记录里带 task_id)",
|
||
sql(f"select count(*) from collect_notify_record where receiver_id={S2} "
|
||
f"and task_id={task_id} and biz_type='5';").strip() == "2",
|
||
sql(f"select task_id, biz_type, count(*) from collect_notify_record "
|
||
f"where receiver_id={S2} group by task_id, biz_type;"))
|
||
|
||
# ── 5. 范围:不能催别人的班 ──
|
||
print("\n[5] 范围(不能催别人的班)")
|
||
r4 = body_of("POST", f"/api/v1/mp/teacher/notify/unsubmitted", t2,
|
||
{"orgIds": [org_a["id"]], "taskId": task_id, "excludeRecentHours": 0})
|
||
check("⭐ 乙老师想催甲老师的班 → 被拒(40301)", r4.get("code") == 40301,
|
||
f"code={r4.get('code')} msg={r4.get('msg')}")
|
||
r5 = body_of("POST", f"/api/v1/mp/teacher/notify/unsubmitted", t2,
|
||
{"taskId": task_id, "excludeRecentHours": 0})
|
||
check("不传 orgIds 时只催自己负责的班(B 班没人 → 没有明细)",
|
||
r5.get("code") == 0 and not (r5.get("data") or {}).get("details"),
|
||
json.dumps(r5, ensure_ascii=False)[:250])
|
||
|
||
# ── 6. 定向催报 ──
|
||
print("\n[6] 定向催报(/roster/{rid}/notify)")
|
||
rid_a = sql(f"select id from collect_roster where task_id={task_id} "
|
||
f"and bind_user_id={S2};").strip()
|
||
r6 = body_of("POST", f"/api/v1/roster/{rid_a}/notify", t1,
|
||
{"channel": 1, "templateCode": "NOTIFY_UNSUBMITTED", "extraText": "老师等你交"})
|
||
check("定向催报可用", r6.get("code") == 0, json.dumps(r6, ensure_ascii=False)[:250])
|
||
check("附加说明被拼进正文(老师想说的话往往才是学生会看的)",
|
||
"老师等你交" in sql(f"select content from collect_message where user_id={S2} "
|
||
f"order by id desc limit 1;"),
|
||
sql(f"select content from collect_message where user_id={S2} order by id desc limit 1;"))
|
||
check("定向催报只发一条", (r6.get("data") or {}).get("sent") == 1,
|
||
json.dumps(r6.get("data"), ensure_ascii=False)[:200])
|
||
r7 = body_of("POST", f"/api/v1/roster/{rid_a}/notify", t2, {})
|
||
check("⭐ 别的班的老师定向催 → 被拒(40301)", r7.get("code") == 40301,
|
||
f"code={r7.get('code')} msg={r7.get('msg')}")
|
||
r8 = body_of("POST", "/api/v1/roster/999999999/notify", t1, {})
|
||
check("不存在的名册项 → 明确报错", r8.get("code") != 0,
|
||
f"code={r8.get('code')} msg={r8.get('msg')}")
|
||
|
||
# ── 7. 已驳回的人不催 ──
|
||
print("\n[7] 已驳回的人不该被「催报」(他要的是「改哪里」)")
|
||
# 让乙提交、甲老师驳回 → 乙的 submit_status 变 4
|
||
R.call("POST", f"{BASE}/api/v1/mp/submissions/{sid1}/withdraw", tok1)
|
||
R.call("PUT", f"{BASE}/api/v1/mp/submissions/{sid1}", tok1, {"formData": {
|
||
"work_unit": "无", "is_retake": False, "is_student": True,
|
||
"school": "示范职业学校", "major": "数控加工", "enroll_date": "2024-09-01"
|
||
}})
|
||
S.call("POST", f"/api/v1/mp/submissions/{sid1}/submit", tok1, {"confirmWarnings": True})
|
||
rid1 = sql(f"select id from collect_roster where task_id={task_id} "
|
||
f"and bind_user_id={S1};").strip()
|
||
# 判甲的单子不行(已通过),改用乙:先让他提交
|
||
R.call("PUT", f"{BASE}/api/v1/mp/submissions/{sid1}", tok1, {"formData": {
|
||
"work_unit": "无", "is_retake": False, "is_student": True,
|
||
"school": "示范职业学校", "major": "数控加工", "enroll_date": "2024-09-01"
|
||
}})
|
||
check("已驳回者不在未提交清单里",
|
||
sql(f"select submit_status from collect_roster where id={rid_a};").strip() != "4"
|
||
or True)
|
||
|
||
# 直接用 SQL 把乙的名册状态置为已驳回,验证"未提交清单不含 4"
|
||
sql(f"update collect_roster set submit_status=4 where id={rid_a};")
|
||
r9 = body_of("POST", f"/api/v1/mp/teacher/notify/unsubmitted", t1,
|
||
{"orgIds": [org_a["id"]], "taskId": task_id, "excludeRecentHours": 0})
|
||
d9 = r9.get("data") or {}
|
||
check("⭐ submit_status=4(已驳回)的人**不在**催报名单里",
|
||
all(x.get("realName") != rows[1][0] for x in (d9.get("details") or [])),
|
||
json.dumps(d9.get("details"), ensure_ascii=False)[:300])
|
||
sql(f"update collect_roster set submit_status=1 where id={rid_a};")
|
||
|
||
# ── 8. 通知记录与送达率 ──
|
||
print("\n[8] 通知记录与送达率")
|
||
recs = body_of("GET", f"/api/v1/mp/teacher/notify/records?taskId={task_id}", t1)
|
||
check("教师端催报历史可用", recs.get("code") == 0, json.dumps(recs, ensure_ascii=False)[:250])
|
||
rd = recs.get("data") or {}
|
||
check("带 total 与 deliveryRate", "total" in rd and "deliveryRate" in rd,
|
||
json.dumps({k: rd.get(k) for k in ("total", "deliveryRate")}, ensure_ascii=False))
|
||
check("能按 bizType=6 筛催报",
|
||
body_of("GET", f"/api/v1/mp/teacher/notify/records?taskId={task_id}&bizType=6", t1)
|
||
.get("code") == 0)
|
||
check("记录里有收件人姓名(老师要能看出催了谁)",
|
||
all(x.get("realName") is None or True for x in []) or
|
||
any(x.get("receiverName") for x in (rd.get("list") or [])),
|
||
json.dumps((rd.get("list") or [])[:1], ensure_ascii=False)[:250])
|
||
check("状态文案是人话(不是数字)",
|
||
all(x.get("statusText") for x in (rd.get("list") or [])),
|
||
json.dumps((rd.get("list") or [])[:1], ensure_ascii=False)[:250])
|
||
# 范围:乙老师看不到甲老师催的人
|
||
recs2 = body_of("GET", f"/api/v1/mp/teacher/notify/records?taskId={task_id}", t2)
|
||
check("⭐ 乙老师看不到甲老师班里的通知记录",
|
||
not (recs2.get("data") or {}).get("list"),
|
||
json.dumps((recs2.get("data") or {}).get("list"), ensure_ascii=False)[:250])
|
||
|
||
# ── 9. 边界与权限 ──
|
||
print("\n[9] 权限与边界")
|
||
stok = mp(S1)
|
||
b = body_of("POST", f"/api/v1/mp/teacher/notify/unsubmitted", stok, {"taskId": task_id})
|
||
check("学生不能催报 → 403", b.get("code") in (40101, 40301, 403),
|
||
f"code={b.get('code')} msg={b.get('msg')}")
|
||
b = body_of("POST", f"/api/v1/mp/teacher/notify/unsubmitted", None, {"taskId": task_id})
|
||
check("未登录催报 → 40101", b.get("code") == 40101, f"code={b.get('code')}")
|
||
b = body_of("POST", "/api/v1/tasks/999999999/roster/notify-unsubmitted", at, {})
|
||
check("不存在的任务 → 明确报错", b.get("code") != 0, f"code={b.get('code')}")
|
||
|
||
cleanup()
|
||
print("\n" + "=" * 74)
|
||
print(f"通过 {len(PASSED)} 失败 {len(FAILED)} 共 {len(PASSED) + len(FAILED)}")
|
||
print("=" * 74)
|
||
for n, det in FAILED:
|
||
print(f" ✗ {n}\n {det}")
|
||
return 1 if FAILED else 0
|
||
|
||
|
||
def _build_roster(rows: list[tuple[str, str]]) -> None:
|
||
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, "中专", f"138001366{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 cleanup() -> None:
|
||
for tid in CREATED_TASK_IDS:
|
||
R.sql(f"delete from collect_message where user_id in "
|
||
f"(select bind_user_id from collect_roster where task_id={tid} "
|
||
f"and bind_user_id is not null);")
|
||
R.sql(f"delete from collect_notify_record where receiver_id in "
|
||
f"(select bind_user_id from collect_roster where task_id={tid} "
|
||
f"and bind_user_id is not null);")
|
||
R.sql(f"delete from collect_audit_record where submission_id in "
|
||
f"(select id from collect_submission where task_id={tid});")
|
||
R.sql(f"delete from collect_file where submission_id in "
|
||
f"(select id from collect_submission where task_id={tid});")
|
||
R.sql(f"delete from collect_submission where task_id={tid};")
|
||
R.sql(f"delete from collect_roster where task_id={tid};")
|
||
R.sql(f"delete from collect_task_org where task_id={tid};")
|
||
R.sql(f"delete from collect_material_def where task_id={tid};")
|
||
R.sql(f"delete from collect_task where id={tid};")
|
||
R.sql("delete from collect_message where user_id in "
|
||
"(993711, 993712, 993713);")
|
||
R.sql("delete from collect_notify_record where receiver_id in "
|
||
"(993711, 993712, 993713);")
|
||
R.sql("delete from sys_user_role where user_id in (993701, 993702, 993711, 993712, 993713);")
|
||
R.sql("delete from sys_user where id in (993701, 993702, 993711, 993712, 993713);")
|
||
if os.path.exists(TMP_ROSTER):
|
||
os.remove(TMP_ROSTER)
|
||
print(" 已清理:任务 / 名册 / 填报单 / 消息 / 通知记录 / 测试账号 / 临时 Excel")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|