test_setting_writes.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. # -*- coding: utf-8 -*-
  2. """设置类写接口端到端测试(配送模板 / 物流公司 / 打印机 / 支付设置 / 支付模板)。
  3. 自带造数与清理:所有测试记录以 TESTJ 命名,跑完物理删除,payment 表恢复为空。
  4. 用法:python test_setting_writes.py
  5. """
  6. import json
  7. import re
  8. import sys
  9. from decimal import Decimal
  10. from pathlib import Path
  11. import pymysql
  12. import requests
  13. JAVA = "http://127.0.0.1:18080"
  14. TOKEN = "testsession123"
  15. STORE_ID = 10001
  16. ROOT = Path(r"C:\Users\Administrator\WorkBuddy\2026-10-17-12-27-10\java-rewrite")
  17. YML = ROOT / "yoshop-java" / "src" / "main" / "resources" / "application.yml"
  18. SESSION = requests.Session()
  19. SESSION.trust_env = False
  20. PASS = FAIL = 0
  21. FAILS = []
  22. def db():
  23. pwd = re.findall(r"password: *(.+)", YML.read_text(encoding="utf-8"))[0].strip().strip('"')
  24. return pymysql.connect(host="192.168.1.177", port=3306, user="root", password=pwd,
  25. database="shop", autocommit=True)
  26. CONN = db()
  27. def query(sql, args=None):
  28. cur = CONN.cursor()
  29. cur.execute(sql, args or ())
  30. cols = [d[0] for d in cur.description]
  31. return [dict(zip(cols, row)) for row in cur.fetchall()]
  32. def execute(sql, args=None):
  33. cur = CONN.cursor()
  34. cur.execute(sql, args or ())
  35. return cur.rowcount
  36. def get(path, params=None):
  37. r = SESSION.get(JAVA + path, headers={"Access-Token": TOKEN}, params=params, timeout=60)
  38. return r.json()
  39. def post(path, body):
  40. r = SESSION.post(JAVA + path, headers={"Access-Token": TOKEN}, json=body, timeout=60)
  41. return r.json()
  42. def post_multipart(path, data, files=None):
  43. r = SESSION.post(JAVA + path, headers={"Access-Token": TOKEN}, data=data, files=files, timeout=60)
  44. return r.json()
  45. def check(name, ok, detail=""):
  46. global PASS, FAIL
  47. if ok:
  48. PASS += 1
  49. print(f" [ok] {name}")
  50. else:
  51. FAIL += 1
  52. FAILS.append(f"{name} :: {detail}")
  53. print(f" [FAIL] {name} :: {detail}")
  54. def cleanup():
  55. execute("delete from yoshop_delivery where name like 'TESTJ%%'")
  56. execute("delete from yoshop_delivery_rule where delivery_id not in (select delivery_id from yoshop_delivery)")
  57. execute("delete from yoshop_express where express_name like 'TESTJ%%'")
  58. execute("delete from yoshop_printer where printer_name like 'TESTJ%%'")
  59. execute("delete from yoshop_payment")
  60. execute("delete from yoshop_payment_template where name like 'TESTJ%%'")
  61. for f in (ROOT / "yoshop-java" / "runtime" / "uploads" / "payment").rglob("*.pem"):
  62. if f.name.startswith("apiclientCert-"):
  63. f.unlink(missing_ok=True)
  64. # ---------------------------------------------------------------- 配送模板
  65. def test_delivery():
  66. print("\n== 配送模板 ==")
  67. rules = [
  68. {"region": [2, 783], "region_text": [{"name": "北京市", "citys": []}, {"name": "上海市", "citys": []}],
  69. "first": 2, "first_fee": "4.50", "additional": 1, "additional_fee": "2.00"},
  70. {"region": [648], "region_text": [{"name": "辽宁省", "citys": []}],
  71. "first": 1, "first_fee": 0, "additional": 0, "additional_fee": 0},
  72. ]
  73. res = post("/store/setting.delivery/add",
  74. {"form": {"name": "TESTJ配送模板", "method": 20, "sort": 99, "rules": rules}})
  75. check("add 返回添加成功", res.get("status") == 200 and res.get("message") == "添加成功", res)
  76. rows = query("select * from yoshop_delivery where name='TESTJ配送模板'")
  77. check("add 落库 1 条", len(rows) == 1, rows)
  78. if not rows:
  79. return
  80. rec = rows[0]
  81. did = rec["delivery_id"]
  82. check("add method/sort/store_id", rec["method"] == 20 and rec["sort"] == 99
  83. and rec["store_id"] == STORE_ID and rec["is_delete"] == 0, rec)
  84. check("add 写入 create_time/update_time", rec["create_time"] > 0 and rec["update_time"] > 0, rec)
  85. rule_rows = query("select * from yoshop_delivery_rule where delivery_id=%s order by rule_id", (did,))
  86. check("add 写入 2 条区域", len(rule_rows) == 2, len(rule_rows))
  87. if len(rule_rows) == 2:
  88. check("add region 存 JSON 数组", rule_rows[0]["region"] == "[2,783]", rule_rows[0]["region"])
  89. check("add region_text 存 JSON", rule_rows[0]["region_text"] == '[{"name":"北京市","citys":[]},{"name":"上海市","citys":[]}]',
  90. rule_rows[0]["region_text"])
  91. check("add first/first_fee", float(rule_rows[0]["first"]) == 2.0
  92. and Decimal(rule_rows[0]["first_fee"]) == Decimal("4.50"), rule_rows[0])
  93. detail = get("/store/setting.delivery/detail", {"deliveryId": did})
  94. check("detail 返回 rule 数组", isinstance(detail["data"]["detail"].get("rule"), list)
  95. and len(detail["data"]["detail"]["rule"]) == 2, detail.get("data"))
  96. check("detail rule.first 为数字", detail["data"]["detail"]["rule"][0]["first"] == 2, detail)
  97. check("detail rule.first_fee 为字符串", detail["data"]["detail"]["rule"][0]["first_fee"] == "4.50", detail)
  98. res = post("/store/setting.delivery/add", {"form": {"name": "TESTJ空规则", "method": 10, "sort": 1}})
  99. check("add 无 rules 被拦截", res.get("status") == 500 and "请选择可配送区域" in (res.get("message") or ""), res)
  100. new_rule = [{"region": [2], "region_text": [{"name": "北京市", "citys": []}],
  101. "first": 3, "first_fee": "9.90", "additional": 2, "additional_fee": "1.00"}]
  102. res = post("/store/setting.delivery/edit",
  103. {"deliveryId": did,
  104. "form": {"name": "TESTJ配送模板-改", "method": 10, "sort": 98, "rules": new_rule}})
  105. check("edit 返回更新成功", res.get("status") == 200 and res.get("message") == "更新成功", res)
  106. rec = query("select * from yoshop_delivery where delivery_id=%s", (did,))[0]
  107. check("edit 主表已更新", rec["name"] == "TESTJ配送模板-改" and rec["method"] == 10 and rec["sort"] == 98, rec)
  108. rule_rows = query("select * from yoshop_delivery_rule where delivery_id=%s", (did,))
  109. check("edit 区域被整体替换为 1 条", len(rule_rows) == 1, len(rule_rows))
  110. if rule_rows:
  111. check("edit 新区域内容正确", rule_rows[0]["region"] == "[2]"
  112. and Decimal(rule_rows[0]["first_fee"]) == Decimal("9.90"), rule_rows[0])
  113. res = post("/store/setting.delivery/edit",
  114. {"deliveryId": did, "form": {"name": "TESTJ配送模板-改", "method": 10, "rules": []}})
  115. check("edit 无 rules 被拦截", res.get("status") == 500 and "请选择可配送区域" in (res.get("message") or ""), res)
  116. res = post("/store/setting.delivery/delete", {"deliveryId": did})
  117. check("delete 返回删除成功", res.get("status") == 200 and res.get("message") == "删除成功", res)
  118. rec = query("select is_delete from yoshop_delivery where delivery_id=%s", (did,))[0]
  119. check("delete 置 is_delete=1", rec["is_delete"] == 1, rec)
  120. detail = get("/store/setting.delivery/detail", {"deliveryId": did})
  121. check("delete 后 detail 为 null", detail["data"]["detail"] is None, detail.get("data"))
  122. res = post("/store/setting.delivery/delete", {"deliveryId": 10001})
  123. check("delete 被商品使用的模板被拦截",
  124. res.get("status") == 500 and "该模板被6个商品使用,不允许删除" in (res.get("message") or ""), res)
  125. # ---------------------------------------------------------------- 物流公司
  126. def test_express():
  127. print("\n== 物流公司 ==")
  128. res = post("/store/setting.express/add",
  129. {"form": {"express_name": "TESTJ快递", "kuaidi100_code": "testj", "kdniao_code": "TESTJ", "sort": 88}})
  130. check("add 返回添加成功", res.get("status") == 200 and res.get("message") == "添加成功", res)
  131. rows = query("select * from yoshop_express where express_name='TESTJ快递'")
  132. check("add 落库 1 条", len(rows) == 1, rows)
  133. if not rows:
  134. return
  135. eid = rows[0]["express_id"]
  136. rec = rows[0]
  137. check("add 字段正确", rec["kuaidi100_code"] == "testj" and rec["kdniao_code"] == "TESTJ"
  138. and rec["sort"] == 88 and rec["store_id"] == STORE_ID and rec["is_delete"] == 0, rec)
  139. res = post("/store/setting.express/edit",
  140. {"expressId": eid,
  141. "form": {"express_name": "TESTJ快递-改", "kuaidi100_code": "testj2", "kdniao_code": "TESTJ2", "sort": 66}})
  142. check("edit 返回更新成功", res.get("status") == 200 and res.get("message") == "更新成功", res)
  143. rec = query("select * from yoshop_express where express_id=%s", (eid,))[0]
  144. check("edit 字段已更新", rec["express_name"] == "TESTJ快递-改" and rec["kuaidi100_code"] == "testj2"
  145. and rec["kdniao_code"] == "TESTJ2" and rec["sort"] == 66, rec)
  146. res = post("/store/setting.express/delete", {"expressId": eid})
  147. check("delete 返回删除成功", res.get("status") == 200 and res.get("message") == "删除成功", res)
  148. rec = query("select is_delete from yoshop_express where express_id=%s", (eid,))[0]
  149. check("delete 置 is_delete=1", rec["is_delete"] == 1, rec)
  150. # ------------------------------------------------------------------ 打印机
  151. def test_printer():
  152. print("\n== 打印机 ==")
  153. config = {"USER": "testuser", "UKEY": "testukey", "SN": "123456789"}
  154. res = post("/store/setting.printer/add",
  155. {"form": {"printer_name": "TESTJ打印机", "printer_type": "FEI_E_YUN",
  156. "printer_config": config, "print_times": 2, "sort": 77}})
  157. check("add 返回添加成功", res.get("status") == 200 and res.get("message") == "添加成功", res)
  158. rows = query("select * from yoshop_printer where printer_name='TESTJ打印机'")
  159. check("add 落库 1 条", len(rows) == 1, rows)
  160. if not rows:
  161. return
  162. pid = rows[0]["printer_id"]
  163. rec = rows[0]
  164. check("add printer_config 存 JSON", json.loads(rec["printer_config"]) == config, rec["printer_config"])
  165. check("add 其他字段", rec["printer_type"] == "FEI_E_YUN" and rec["print_times"] == 2
  166. and rec["sort"] == 77 and rec["store_id"] == STORE_ID, rec)
  167. all_data = get("/store/setting.printer/all")["data"]["list"]
  168. hit = [p for p in all_data if p["printer_id"] == pid]
  169. check("all 输出 printer_config 为对象", len(hit) == 1 and hit[0]["printer_config"] == config, hit)
  170. res = post("/store/setting.printer/edit",
  171. {"printerId": pid,
  172. "form": {"printer_name": "TESTJ打印机-改", "printer_type": "PRINT_CENTER",
  173. "printer_config": {"deviceNo": "D1", "key": "K1"}, "print_times": 3, "sort": 55}})
  174. check("edit 返回更新成功", res.get("status") == 200 and res.get("message") == "更新成功", res)
  175. rec = query("select * from yoshop_printer where printer_id=%s", (pid,))[0]
  176. check("edit 字段已更新", rec["printer_name"] == "TESTJ打印机-改" and rec["printer_type"] == "PRINT_CENTER"
  177. and rec["print_times"] == 3 and rec["sort"] == 55, rec)
  178. check("edit 配置已替换", json.loads(rec["printer_config"]) == {"deviceNo": "D1", "key": "K1"}, rec["printer_config"])
  179. res = post("/store/setting.printer/delete", {"printerId": pid})
  180. check("delete 返回删除成功", res.get("status") == 200 and res.get("message") == "删除成功", res)
  181. rec = query("select is_delete from yoshop_printer where printer_id=%s", (pid,))[0]
  182. check("delete 置 is_delete=1", rec["is_delete"] == 1, rec)
  183. # ---------------------------------------------------------------- 支付设置
  184. def test_payment():
  185. print("\n== 支付设置 ==")
  186. before = get("/store/setting.payment/options")["data"]["options"]
  187. check("options 默认 4 个客户端", list(before.keys()) == ["MP-WEIXIN", "H5", "WXOFFICIAL", "APP"], list(before.keys()))
  188. check("options 方法数 2/3/1/3", [len(before[k]["methods"]) for k in before] == [2, 3, 1, 3],
  189. {k: len(v["methods"]) for k, v in before.items()})
  190. check("options 默认仅余额启用",
  191. [m["method"] for m in before["MP-WEIXIN"]["methods"] if m["is_enable"]] == ["balance"], before["MP-WEIXIN"])
  192. form = json.loads(json.dumps(before))
  193. form["MP-WEIXIN"]["methods"][0]["is_enable"] = True
  194. form["MP-WEIXIN"]["methods"][0]["template_id"] = 0
  195. form["H5"]["methods"][1]["is_enable"] = True
  196. form["H5"]["methods"][1]["template_id"] = 0
  197. res = post("/store/setting.payment/update", {"form": form})
  198. check("update 返回更新成功", res.get("status") == 200 and res.get("message") == "更新成功", res)
  199. rows = query("select * from yoshop_payment where store_id=%s", (STORE_ID,))
  200. check("update 落库 9 条", len(rows) == 9, len(rows))
  201. wechat = [r for r in rows if r["client"] == "MP-WEIXIN" and r["method"] == "wechat"]
  202. check("update wechat 已启用", len(wechat) == 1 and wechat[0]["is_enable"] == 1, wechat)
  203. check("update others 存空数组", all(r["others"] == "[]" for r in rows), [r["others"] for r in rows][:3])
  204. check("update is_must_template 落库", wechat[0]["is_must_template"] == 1 if wechat else False, wechat)
  205. after = get("/store/setting.payment/options")["data"]["options"]
  206. check("options 读回微信已启用", after["MP-WEIXIN"]["methods"][0]["is_enable"] is True, after["MP-WEIXIN"]["methods"][0])
  207. check("options 读回支付宝已启用", after["H5"]["methods"][1]["is_enable"] is True, after["H5"]["methods"][1])
  208. check("options 读回仍保留 key 字段", after["MP-WEIXIN"]["methods"][0].get("key") == 1, after["MP-WEIXIN"]["methods"][0])
  209. # 还原为空(PHP 侧初始即无记录)
  210. execute("delete from yoshop_payment")
  211. restored = get("/store/setting.payment/options")["data"]["options"]
  212. check("清空后恢复默认", restored["MP-WEIXIN"]["methods"][0]["is_enable"] is False, restored["MP-WEIXIN"]["methods"][0])
  213. # ---------------------------------------------------------------- 支付模板
  214. def test_payment_template():
  215. print("\n== 支付模板 ==")
  216. cfg = {"wechat": {
  217. "mchType": "normal", "version": "v2",
  218. "normal": {"appId": "wxtest123", "mchId": "1900000109", "apiKey": "k" * 16,
  219. "signatureMethod": "platformCert", "publicKeyId": "", "publicKey": "",
  220. "apiclientCert": "", "apiclientKey": ""},
  221. "provider": {"spAppId": "", "spMchId": "", "spApiKey": "", "subAppId": "", "subMchId": "",
  222. "spSignatureMethod": "platformCert", "spPublicKeyId": "", "spPublicKey": "",
  223. "spApiclientCert": "", "spApiclientKey": ""},
  224. }}
  225. cert = b"-----BEGIN CERTIFICATE-----\nTESTJ\n-----END CERTIFICATE-----\n"
  226. res = post_multipart("/store/setting.payment.template/add",
  227. data={"name": "TESTJ支付模板", "sort": 90, "method": "wechat",
  228. "remarks": "auto", "config": json.dumps(cfg, ensure_ascii=False)},
  229. files={"apiclientCert": ("apiclientCert.pem", cert, "application/x-pem-file")})
  230. check("add 返回添加成功", res.get("status") == 200 and res.get("message") == "添加成功", res)
  231. rows = query("select * from yoshop_payment_template where name='TESTJ支付模板'")
  232. check("add 落库 1 条", len(rows) == 1, rows)
  233. if not rows:
  234. return
  235. tid = rows[0]["template_id"]
  236. rec = rows[0]
  237. saved = json.loads(rec["config"])
  238. check("add config 仅保留当前支付方式", list(saved.keys()) == ["wechat"], list(saved.keys()))
  239. cert_name = saved["wechat"]["normal"]["apiclientCert"]
  240. check("add 证书文件名规则", bool(re.fullmatch(r"apiclientCert-[0-9a-f]{8}\.pem", cert_name or "")), cert_name)
  241. check("add 保留原配置值", saved["wechat"]["normal"]["appId"] == "wxtest123"
  242. and saved["wechat"]["provider"]["spAppId"] == "", saved["wechat"]["normal"])
  243. cert_path = ROOT / "yoshop-java" / "runtime" / "uploads" / "payment" / "wechat" / str(STORE_ID) / cert_name
  244. check("add 证书文件已落盘", cert_path.exists() and cert_path.read_bytes() == cert, str(cert_path))
  245. check("add wechatpay_serial 为空(v2)", rec["wechatpay_serial"] == "", rec["wechatpay_serial"])
  246. check("add sort/remarks/store_id", rec["sort"] == 90 and rec["remarks"] == "auto" and rec["store_id"] == STORE_ID, rec)
  247. detail = get("/store/setting.payment.template/detail", {"templateId": tid})["data"]["detail"]
  248. check("detail 返回 config 合并默认", list(detail["config"].keys()) == ["wechat", "alipay"], list(detail["config"].keys()))
  249. check("detail 保留证书文件名", detail["config"]["wechat"]["normal"]["apiclientCert"] == cert_name, detail["config"]["wechat"]["normal"])
  250. # 前端编辑页会用 detail 回填 config(含已保存的证书文件名),这里保持一致
  251. res = post_multipart("/store/setting.payment.template/edit",
  252. data={"templateId": tid, "name": "TESTJ支付模板-改", "sort": 91,
  253. "method": "wechat", "remarks": "auto2",
  254. "config": json.dumps(detail["config"], ensure_ascii=False)})
  255. check("edit 返回更新成功", res.get("status") == 200 and res.get("message") == "更新成功", res)
  256. rec = query("select * from yoshop_payment_template where template_id=%s", (tid,))[0]
  257. check("edit 基础字段已更新", rec["name"] == "TESTJ支付模板-改" and rec["sort"] == 91 and rec["remarks"] == "auto2", rec)
  258. saved = json.loads(rec["config"])
  259. check("edit 未传文件时保留原证书名", saved["wechat"]["normal"]["apiclientCert"] == cert_name, saved["wechat"]["normal"])
  260. execute("insert into yoshop_payment (client,method,is_must_template,template_id,is_enable,is_default,others,store_id,create_time,update_time)"
  261. " values ('H5','wechat',1,%s,1,0,'[]',%s,0,0)", (tid, STORE_ID))
  262. res = post_multipart("/store/setting.payment.template/delete", data={"templateId": tid})
  263. check("delete 被引用时被拦截",
  264. res.get("status") == 500 and "当前支付模板已被引用" in (res.get("message") or ""), res)
  265. execute("delete from yoshop_payment where template_id=%s", (tid,))
  266. res = post_multipart("/store/setting.payment.template/delete", data={"templateId": tid})
  267. check("delete 解除引用后成功", res.get("status") == 200 and res.get("message") == "删除成功", res)
  268. rec = query("select is_delete from yoshop_payment_template where template_id=%s", (tid,))[0]
  269. check("delete 置 is_delete=1", rec["is_delete"] == 1, rec)
  270. cert_path.unlink(missing_ok=True)
  271. def main():
  272. print("清理历史测试数据…")
  273. cleanup()
  274. test_delivery()
  275. test_express()
  276. test_printer()
  277. test_payment()
  278. test_payment_template()
  279. print("\n清理测试数据…")
  280. cleanup()
  281. print(f"\n通过 {PASS} / 失败 {FAIL}")
  282. if FAILS:
  283. print("失败明细:")
  284. for f in FAILS:
  285. print(" -", f)
  286. sys.exit(1)
  287. if __name__ == "__main__":
  288. main()