# -*- coding: utf-8 -*- """联调:备注改字段后的业务流程回归。""" import json import sys import time import urllib.error import urllib.parse import urllib.request sys.stdout.reconfigure(encoding="utf-8") BASE = "http://127.0.0.1:8088/sjnmtybt" DMS = "http://121.43.55.7:2101" PASS = 0 FAIL = 0 def ok(name, cond, extra=""): global PASS, FAIL if cond: PASS += 1 print(f" [OK] {name}" + (f" {extra}" if extra else "")) else: FAIL += 1 print(f" [FAIL] {name}" + (f" {extra}" if extra else "")) def req(method, path, body=None, query=None, timeout=90): url = BASE + path if query: url += "?" + urllib.parse.urlencode(query) data = None headers = {} if body is not None: data = json.dumps(body, ensure_ascii=False).encode("utf-8") headers["Content-Type"] = "application/json; charset=utf-8" r = urllib.request.Request(url, data=data, method=method, headers=headers) try: with urllib.request.urlopen(r, timeout=timeout) as resp: raw = resp.read().decode("utf-8") return json.loads(raw) if raw else {} except urllib.error.HTTPError as e: raw = e.read().decode("utf-8") try: return json.loads(raw) except Exception: return {"code": e.code, "message": raw} except Exception as e: return {"code": 599, "message": str(e)} def gb18(body17): w = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2] m = "10X98765432" s = sum(int(body17[i]) * w[i] for i in range(17)) return body17 + m[s % 11] def dms_login(): body = urllib.parse.urlencode( {"userName": "user_liu", "password": "WE176852439@lmx", "clientId": "1"} ).encode("utf-8") r = urllib.request.Request( DMS + "/proxy_oauth/user/login", data=body, method="POST", headers={"Content-Type": "application/x-www-form-urlencoded"}, ) with urllib.request.urlopen(r, timeout=40) as resp: return json.loads(resp.read().decode("utf-8"))["message"] def dms_list(token, column_id, model_id, page_size=100): body = urllib.parse.urlencode( { "columnId": str(column_id), "modelId": str(model_id), "states": "0", "page": "0", "pageSize": str(page_size), } ).encode("utf-8") r = urllib.request.Request( DMS + "/proxy_dms/content/selectContentList", data=body, method="POST", headers={"Content-Type": "application/x-www-form-urlencoded", "Token": token}, ) with urllib.request.urlopen(r, timeout=40) as resp: data = json.loads(resp.read().decode("utf-8")) return (data.get("content") or {}).get("data") or [] print("======== 1) 人员:查询回填 / 新增 / 改号 / 备注纯说明 ========") page = req("POST", "/api/personnel/page", {"name": "陈桂新", "pageNum": 1, "pageSize": 5}) chen = ((page.get("data") or {}).get("list") or [None])[0] ok("陈桂新能查到", chen is not None) if chen: ok("业务号仍是 P197", chen.get("id") == "P197", str(chen.get("id"))) ok("手机号在字段里", chen.get("phoneNumber") == "13800000001", str(chen.get("phoneNumber"))) remarks = str(chen.get("remarks") or "") ok("备注不含 MOCK_SEED", "MOCK_SEED" not in remarks, remarks) ok("备注不含业务号协议", "P197" not in remarks, remarks) ok("备注是用户说明", remarks in ("村级录入", "Excel导入") or "录入" in remarks, remarks) detail = req("GET", "/api/personnel/" + chen["id"]) d = detail.get("data") or {} ok("按业务号取详情", d.get("id") == "P197" and d.get("name") == "陈桂新") seq = str(int(time.time()) % 900 + 100) idno = gb18("31011719800813" + seq) name = "字段回归测试" + seq save = req( "POST", "/api/personnel", { "name": name, "idNumber": idno, "phoneNumber": "13987654321", "townId": "T_YEXIE", "villageId": "V_YX_001", "payeeName": name, "bankCardNumber": "6222021234567890123", "bankName": "农业银行", "enjoyStartMonth": "202601", "source": "联调验证", "remarks": "这是用户备注不要当协议", "heirName": "测试继承人", "heirRelation": "子女", }, ) ok("新增人员成功", save.get("code") == 200, str(save.get("message"))) p = save.get("data") or {} pid = p.get("id") ok("新人员有业务号", bool(pid) and str(pid).startswith("P"), str(pid)) ok("新人员手机号回读", p.get("phoneNumber") == "13987654321", str(p.get("phoneNumber"))) ok( "新人员备注无 MOCK_SEED/业务号", "MOCK_SEED" not in str(p.get("remarks") or "") and str(pid) not in str(p.get("remarks") or ""), str(p.get("remarks")), ) ok("继承人关系在字段", p.get("heirRelation") == "子女", str(p.get("heirRelation"))) village_edit = req( "POST", "/api/personnel", { "id": pid, "name": name, "idNumber": idno, "phoneNumber": "13800138000", "townId": "T_YEXIE", "villageId": "V_YX_001", "remarks": "改过的用户备注", "operatorRoleKey": "village", }, ) vp = village_edit.get("data") or {} ok("村级改手机号/备注成功", village_edit.get("code") == 200, str(village_edit.get("message"))) ok("村级编辑后手机号更新", vp.get("phoneNumber") == "13800138000", str(vp.get("phoneNumber"))) ok("村级编辑后备注是用户说明", vp.get("remarks") == "改过的用户备注", str(vp.get("remarks"))) ok("村级未改继承人时关系仍是子女", vp.get("heirRelation") == "子女", str(vp.get("heirRelation"))) heir_deny = req( "POST", "/api/personnel", { "id": pid, "name": name, "idNumber": idno, "phoneNumber": "13800138000", "townId": "T_YEXIE", "villageId": "V_YX_001", "remarks": "改过的用户备注", "heirRelation": "配偶", "operatorRoleKey": "village", }, ) ok( "村级改继承人被拒", heir_deny.get("code") not in (None, 200), str(heir_deny.get("code")) + " " + str(heir_deny.get("message")), ) ok("拒绝文案指向镇社区事务", "继承人" in str(heir_deny.get("message") or ""), str(heir_deny.get("message"))) heir_ok = req( "POST", "/api/personnel", { "id": pid, "name": name, "idNumber": idno, "phoneNumber": "13800138000", "townId": "T_YEXIE", "villageId": "V_YX_001", "remarks": "改过的用户备注", "heirRelation": "配偶", "operatorRoleKey": "social", }, ) ep = heir_ok.get("data") or {} ok("镇级改继承人成功", heir_ok.get("code") == 200, str(heir_ok.get("message"))) ok("编辑后手机号仍是新号", ep.get("phoneNumber") == "13800138000", str(ep.get("phoneNumber"))) ok("编辑后备注是用户说明", ep.get("remarks") == "改过的用户备注", str(ep.get("remarks"))) ok("编辑后继承人关系更新", ep.get("heirRelation") == "配偶", str(ep.get("heirRelation"))) print("======== 2) 人员状态:无 allowDirect 拒绝,有则放行 ========") deny = req( "PUT", "/api/personnel/status", {"personnelId": pid, "bizStatus": "PAUSED", "payThisMonth": False, "remarks": "想直接暂停"}, ) ok( "无 allowDirect 暂停被拒", deny.get("code") not in (None, 200), str(deny.get("code")) + " " + str(deny.get("message")), ) ok("拒绝文案不再提备注 ALLOW_DIRECT", "ALLOW_DIRECT" not in str(deny.get("message") or ""), str(deny.get("message"))) allow = req( "PUT", "/api/personnel/status", { "personnelId": pid, "bizStatus": "PAUSED", "payThisMonth": False, "remarks": "管理员直改暂停", "allowDirect": True, "operator": "联调", }, ) ok("allowDirect=true 可暂停", allow.get("code") == 200, str(allow.get("message"))) ok( "暂停后备注不含 ALLOW_DIRECT", "ALLOW_DIRECT" not in str((allow.get("data") or {}).get("remarks") or ""), str((allow.get("data") or {}).get("remarks")), ) ok("状态已是 PAUSED", (allow.get("data") or {}).get("bizStatus") == "PAUSED", str((allow.get("data") or {}).get("bizStatus"))) restore = req( "PUT", "/api/personnel/status", {"personnelId": pid, "bizStatus": "DRAFT", "payThisMonth": True, "allowDirect": True, "remarks": "测完恢复草稿"}, ) ok("恢复草稿", restore.get("code") == 200, str(restore.get("message"))) print("======== 3) 区划:启用/银行/分担比例走字段 ========") regions = req("GET", "/api/regions", query={"level": "TOWN"}) towns = regions.get("data") or [] yexie = next((t for t in towns if t.get("code") == "T_YEXIE" or t.get("id") == "T_YEXIE" or "叶榭" in str(t.get("name"))), None) ok("能查到叶榭镇", yexie is not None) if yexie: ok("enabled 是布尔字段", isinstance(yexie.get("enabled"), bool), str(yexie.get("enabled"))) ok("区级分担比例是数字", isinstance(yexie.get("districtSharePercent"), int), str(yexie.get("districtSharePercent"))) ok("镇级分担比例是数字", isinstance(yexie.get("townSharePercent"), int), str(yexie.get("townSharePercent"))) rem = str(yexie.get("remarks") or "") ok("区划备注不含 level=/code=", "level=" not in rem and "code=" not in rem, rem) old_bank = yexie.get("defaultBank") old_d = yexie.get("districtSharePercent") old_t = yexie.get("townSharePercent") save_r = req( "POST", "/api/regions", { "id": yexie.get("id"), "code": yexie.get("code") or "T_YEXIE", "name": yexie.get("name") or "叶榭镇", "level": "TOWN", "parentId": yexie.get("parentId") or "D_SJ", "entrustPlanCode": yexie.get("entrustPlanCode"), "defaultBank": "农业银行-回归测", "enabled": True, "districtSharePercent": 90, "townSharePercent": 10, "remarks": "区划用户备注", }, ) ok("更新街镇成功", save_r.get("code") == 200, str(save_r.get("message"))) rr = save_r.get("data") or {} ok("默认银行写入字段", rr.get("defaultBank") == "农业银行-回归测", str(rr.get("defaultBank"))) ok( "分担比例写入字段", rr.get("districtSharePercent") == 90 and rr.get("townSharePercent") == 10, str(rr.get("districtSharePercent")) + "/" + str(rr.get("townSharePercent")), ) ok("区划备注是用户说明", rr.get("remarks") == "区划用户备注", str(rr.get("remarks"))) req( "POST", "/api/regions", { "id": yexie.get("id"), "code": yexie.get("code") or "T_YEXIE", "name": yexie.get("name") or "叶榭镇", "level": "TOWN", "parentId": yexie.get("parentId") or "D_SJ", "entrustPlanCode": yexie.get("entrustPlanCode"), "defaultBank": old_bank, "enabled": True, "districtSharePercent": old_d, "townSharePercent": old_t, "remarks": "", }, ) print("======== 4) 批次:提交时间 / 测算锁定 / 轮次字段 ========") bp = req("POST", "/api/batches/page", {"pageNum": 1, "pageSize": 50}) batches = (bp.get("data") or {}).get("list") or [] ok("批次列表能查到", len(batches) > 0, "n=" + str(len(batches))) batch = next((b for b in batches if b.get("status") not in ("ARCHIVED", "EXPORTED", "RETURNED")), None) if batch is None and batches: batch = next((b for b in batches if b.get("status") not in ("ARCHIVED",)), batches[0]) if batch: bid = batch.get("id") ok("批次有 submitTime 字段", batch.get("submitTime") is None or isinstance(batch.get("submitTime"), (int, float)), str(batch.get("submitTime"))) ok("批次有 calcLocked 字段", isinstance(batch.get("calcLocked"), bool), str(batch.get("calcLocked"))) ok("批次有 yearlyAdjust 字段", isinstance(batch.get("yearlyAdjust"), bool), str(batch.get("yearlyAdjust"))) snap = req("POST", f"/api/payments/batches/{urllib.parse.quote(bid)}/calc-rounds", query={"operator": "联调"}) ok("测算快照成功", snap.get("code") == 200, str(snap.get("message"))) sd = snap.get("data") or {} ok("快照含 version/personCount/payableTotal", "version" in sd and "personCount" in sd, str(sd)) rounds = req("GET", f"/api/payments/batches/{urllib.parse.quote(bid)}/calc-rounds") lst = rounds.get("data") or [] ok("测算轮次列表可读", rounds.get("code") == 200 and len(lst) >= 1, "n=" + str(len(lst))) if lst: ok("轮次 version 来自字段", lst[0].get("version") is not None, str(lst[0])) lock = req("POST", f"/api/payments/batches/{urllib.parse.quote(bid)}/calc-lock", query={"locked": "true", "operator": "联调"}) ok("锁定测算成功", lock.get("code") == 200, str(lock.get("message"))) ok("锁定后 calcLocked=true", (lock.get("data") or {}).get("calcLocked") is True, str((lock.get("data") or {}).get("calcLocked"))) rem = str((lock.get("data") or {}).get("remarks") or "") ok("锁定不写 CALC_LOCKED 到备注", "CALC_LOCKED" not in rem, rem) unlock = req("POST", f"/api/payments/batches/{urllib.parse.quote(bid)}/calc-lock", query={"locked": "false", "operator": "联调"}) ok("解锁测算成功", unlock.get("code") == 200 and (unlock.get("data") or {}).get("calcLocked") is False, str((unlock.get("data") or {}).get("calcLocked"))) detail = req("GET", "/api/batches/" + urllib.parse.quote(bid)) dd = detail.get("data") or {} ok("批次详情带回 calcLocked", "calcLocked" in dd, str(dd.get("calcLocked"))) print("======== 5) 发放明细:区镇金额走字段 ========") if batch: gen = req("POST", f"/api/payments/batches/{urllib.parse.quote(batch.get('id'))}/generate") ok("生成/对齐发放明细成功", gen.get("code") == 200, str(gen.get("message"))) pays = (gen.get("data") or {}).get("list") or [] if not pays: pay = req("POST", "/api/payments/details/page", {"batchId": batch.get("id"), "pageNum": 1, "pageSize": 10}) pays = (pay.get("data") or {}).get("list") or [] ok("发放明细能查到", pay.get("code") == 200, "n=" + str(len(pays))) else: ok("发放明细能查到", len(pays) > 0, "n=" + str(len(pays))) if pays: one = pays[0] ok( "区/镇承担金额有值或可空但字段存在", "districtAmount" in one and "townAmount" in one, "dist=" + str(one.get("districtAmount")) + " town=" + str(one.get("townAmount")), ) prem = str(one.get("remarks") or "") ok("明细备注不含 DIST_AMT 协议", "DIST_AMT" not in prem and "TOWN_AMT" not in prem, prem) ok("明细备注不含 API生成 协议", "API生成" not in prem, prem) ok("调标字段存在", "oldMonthlyAmount" in one and "newMonthlyAmount" in one and "adjustMonths" in one) adj = next((b for b in batches if b.get("yearlyAdjust") is True), None) if adj: ap = req("POST", "/api/payments/details/page", {"batchId": adj.get("id"), "pageNum": 1, "pageSize": 5}) al = (ap.get("data") or {}).get("list") or [] ok("年调标批次能查明细", ap.get("code") == 200, "n=" + str(len(al))) if al: ok("年调标明细有新旧月标字段", "oldMonthlyAmount" in al[0] and "newMonthlyAmount" in al[0], str(al[0].get("oldMonthlyAmount"))) else: ok("当前没有年调标批次(跳过明细断言)", True) print("======== 6) 特殊业务:金额/姓名/继承人走字段 ========") sp = req( "POST", "/api/special-biz", { "personnelId": pid, "bizType": "PAUSE", "amount": 0, "reason": "字段回归-暂停申请", "operator": "联调", "operatorRoleKey": "social", "heirName": "继承人甲", "heirIdNumber": gb18("31011719900101001"), "heirPhone": "13700001111", }, ) ok("创建暂停特殊业务", sp.get("code") == 200, str(sp.get("message")) + " " + str(sp.get("code"))) svo = sp.get("data") or {} sid = svo.get("id") ok("特殊单姓名来自字段", svo.get("name") == name or bool(svo.get("name")), str(svo.get("name"))) ok("特殊单继承人姓名", svo.get("heirName") == "继承人甲", str(svo.get("heirName"))) ok("特殊单继承人电话", svo.get("heirPhone") == "13700001111", str(svo.get("heirPhone"))) page_sp = req("GET", "/api/special-biz/page", query={"pageNum": "1", "pageSize": "20", "bizType": "PAUSE"}) splist = (page_sp.get("data") or {}).get("list") or [] hit = next((x for x in splist if x.get("id") == sid), None) ok("特殊业务分页能查到刚建的单", hit is not None) if hit: ok("列表姓名不是靠备注协议", bool(hit.get("name")), str(hit.get("name"))) if sid: rej = req( "POST", "/api/special-biz/leader-approve", {"bizId": sid, "action": "REJECT", "opinion": "联调驳回,不落地"}, ) ok("驳回特殊业务(清理)", rej.get("code") == 200, str(rej.get("message"))) rf = req( "POST", "/api/special-biz", { "personnelId": pid, "bizType": "REFUND", "amount": 100, "reason": "字段回归-退款", "refundMethod": "DEDUCT_NEXT", "operator": "联调", }, ) ok("创建退款特殊业务", rf.get("code") == 200, str(rf.get("message"))) rvo = rf.get("data") or {} rid = rvo.get("id") ok("退款金额在字段", rvo.get("amount") == 100 or str(rvo.get("amount")) in ("100", "100.0", "100.00"), str(rvo.get("amount"))) ok("退款进度字段有值", bool(rvo.get("refundProgress")), str(rvo.get("refundProgress"))) ok( "退款方式字段", str(rvo.get("refundMethod")) in ("DEDUCT_NEXT", "下月扣减") or rvo.get("refundMethod") is not None, str(rvo.get("refundMethod")), ) if rid: adv = req("POST", f"/api/special-biz/{rid}/refund-progress", {"stage": "VERIFYING"}) ok("推进退款进度", adv.get("code") == 200, str(adv.get("message"))) ok("进度更新到 VERIFYING", (adv.get("data") or {}).get("refundProgress") == "VERIFYING", str((adv.get("data") or {}).get("refundProgress"))) req("POST", "/api/special-biz/leader-approve", {"bizId": rid, "action": "REJECT", "opinion": "联调驳回"}) print("======== 7) 金额标准:已删除不进列表 / 无 allowDirect 走审批 ========") std = req("GET", "/api/amount-standards", query={"townId": "T_YEXIE", "pageNum": "1", "pageSize": "50"}) ok("金额标准列表成功", std.get("code") == 200, str(std.get("message"))) slist = (std.get("data") or {}).get("list") or [] ok("当前镇有标准", len(slist) > 0, "n=" + str(len(slist))) ok("列表项无 deleted=true", all(not s.get("deleted") for s in slist)) if slist: cur = next((s for s in slist if s.get("current") is True), slist[0]) ok("标准金额在字段", cur.get("amount") is not None, str(cur.get("amount"))) pending = req( "POST", "/api/amount-standards", { "townId": "T_YEXIE", "standardType": "YEARLY_ADJUST", "amount": 1, "funeralAmount": 1, "effectiveTime": int(time.time() * 1000) + 86400 * 1000 * 365 * 20, "adjustReason": "字段回归-不应直接生效", "remarks": "用户说明", }, ) ok("无 allowDirect 不直接生效", pending.get("code") == 200, str(pending.get("message"))) pstd = pending.get("data") or {} ok("返回待审批说明", "审批" in str(pstd.get("remarks") or pending.get("message") or ""), str(pstd.get("remarks"))) ok("待审批单 current=false", pstd.get("current") is False, str(pstd.get("current"))) if pstd.get("id"): rej_std = req( "POST", "/api/special-biz/leader-approve", {"bizId": pstd.get("id"), "action": "REJECT", "opinion": "联调驳回标准变更"}, ) ok("驳回标准变更待审单", rej_std.get("code") == 200, str(rej_std.get("message"))) print("======== 8) 待补发台账分页 ========") ar = req("POST", "/api/return-arrears/page", {"pageNum": 1, "pageSize": 10}) ok("待补发台账接口通", ar.get("code") == 200, str(ar.get("message"))) arlist = (ar.get("data") or {}).get("list") or [] if arlist: one_ar = arlist[0] ok("待补发有人员字段", bool(one_ar.get("personnelId") or one_ar.get("name")), str(one_ar.get("personnelId"))) print("======== 9) DMS 原表核对新建人员 ========") token = dms_login() rows = dms_list(token, 1847, 1970) raw = next((r for r in rows if str(r.get("c_biz_id")) == str(pid) or str(r.get("c_name")) == name), None) ok("DMS 能按 c_biz_id 找到人", raw is not None) if raw: ok("DMS c_biz_id", str(raw.get("c_biz_id")) == str(pid), str(raw.get("c_biz_id"))) ok("DMS c_phone_number 是字符串 11 位", str(raw.get("c_phone_number")) == "13800138000", str(raw.get("c_phone_number"))) ok("DMS c_heir_relation=配偶", str(raw.get("c_heir_relation")) == "配偶", str(raw.get("c_heir_relation"))) rmk = str(raw.get("c_remarks") or "") ok("DMS 备注无 MOCK_SEED/PHONE=/P号", "MOCK_SEED" not in rmk and "PHONE=" not in rmk and str(pid) not in rmk, rmk) print("======== 10) 清理测试人员 ========") if pid: dl = req("DELETE", "/api/personnel/" + urllib.parse.quote(pid)) ok("删除测试人员", dl.get("code") == 200, str(dl.get("message"))) gone = req("GET", "/api/personnel/" + urllib.parse.quote(pid)) st = (gone.get("data") or {}).get("bizStatus") ok( "删除后归档或不存在", gone.get("code") == 404 or st in ("ARCHIVED", "已归档") or gone.get("data") is None, str(gone.get("code")) + " " + str(st), ) print() print(f"合计 PASS={PASS} FAIL={FAIL}") sys.exit(1 if FAIL else 0)