test_final_writes.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. # -*- coding: utf-8 -*-
  2. """第 8 批写接口双端对照(评论编辑/删除、客户端设置更新、订单导出、小票打印)。
  3. 策略:同一初始数据下,先由 PHP 执行写操作并采样「响应 + 库内状态」,
  4. 恢复数据后由 Java 执行同一操作并再次采样,最后逐字段对照。
  5. 用法:python test_final_writes.py
  6. 前置:
  7. 1. F:/shop/shop 的 PHP 8.2 本地站点运行在 8899(与 Java 共用 192.168.1.177/shop 库);
  8. 2. Java 服务运行在 18080;
  9. 3. 评论接口依赖 tools/seed_comment_golden.py 的种子数据(脚本会自动重置)。
  10. """
  11. import json
  12. import re
  13. import sys
  14. from pathlib import Path
  15. import openpyxl
  16. import pymysql
  17. import requests
  18. from _root import ROOT
  19. PHP_BASE = "http://127.0.0.1:8899/index.php?s="
  20. JAVA_BASE = "http://127.0.0.1:18080"
  21. TOKEN = "testsession123"
  22. STORE_ID = 10001
  23. YML = Path(str(ROOT / "yoshop-java/src/main/resources/application.yml"))
  24. JAVA_EXPORT_DIR = Path(str(ROOT / "yoshop-java/runtime/downloads"))
  25. PHP_EXPORT_DIR = Path(r"F:\shop\shop\public\downloads")
  26. SESSION = requests.Session()
  27. SESSION.trust_env = False
  28. passed = failed = 0
  29. def check(name, cond, extra=""):
  30. global passed, failed
  31. if cond:
  32. passed += 1
  33. print(f" [ok] {name}")
  34. else:
  35. failed += 1
  36. print(f" [FAIL] {name} {extra}")
  37. def db():
  38. pwd = re.findall(r"password:\s*(.+)", YML.read_text(encoding="utf-8"))[0].strip().strip('"')
  39. return pymysql.connect(host="192.168.1.177", port=3306, user="root", password=pwd,
  40. database="shop", charset="utf8mb4", autocommit=True,
  41. cursorclass=pymysql.cursors.DictCursor)
  42. def call(base, path, method="post", body=None):
  43. # PHP 用 ?s=/store/x/y&a=1 追加参数;Java 的路径是 /store/x/y?a=1
  44. java_path = path if "?" in path else path.replace("&", "?", 1)
  45. url = base + (java_path if base == JAVA_BASE else path)
  46. headers = {"Access-Token": TOKEN}
  47. if method == "post":
  48. r = SESSION.post(url, json=body, headers=headers, timeout=120)
  49. else:
  50. r = SESSION.get(url, headers=headers, timeout=120)
  51. try:
  52. return r.json()
  53. except Exception:
  54. return {"raw": r.text}
  55. def reset_comments():
  56. import subprocess
  57. subprocess.run([sys.executable, str(Path(__file__).with_name("seed_comment_golden.py"))],
  58. check=True, capture_output=True)
  59. def comment_state(cur):
  60. cur.execute("select comment_id,score,content,is_picture,status,sort from yoshop_comment"
  61. " where comment_id in (10001,10002,10003) order by comment_id")
  62. comments = cur.fetchall()
  63. cur.execute("select comment_id,image_id from yoshop_comment_image"
  64. " where comment_id in (10001,10002,10003) order by comment_id,image_id")
  65. images = cur.fetchall()
  66. return {"comments": comments, "images": images}
  67. def compare_pair(label, path, body, state_fn, method="post"):
  68. """PHP 先跑 → 采样 → 复位 → Java 跑 → 采样 → 对照"""
  69. reset_comments()
  70. php_resp = call(PHP_BASE, path, method, body)
  71. conn = db()
  72. cur = conn.cursor()
  73. php_state = state_fn(cur)
  74. conn.close()
  75. reset_comments()
  76. java_resp = call(JAVA_BASE, path, method, body)
  77. conn = db()
  78. cur = conn.cursor()
  79. java_state = state_fn(cur)
  80. conn.close()
  81. check(f"{label} status/message 一致",
  82. php_resp.get("status") == java_resp.get("status")
  83. and php_resp.get("message") == java_resp.get("message"),
  84. f"php={php_resp.get('status')}/{php_resp.get('message')!r} "
  85. f"java={java_resp.get('status')}/{java_resp.get('message')!r}")
  86. check(f"{label} 库内状态一致", php_state == java_state,
  87. f"\n php={php_state}\n java={java_state}")
  88. # ------------------------------------------------------------------ 用例
  89. def case_comment_edit():
  90. print("\n[case] 评价编辑:改分/内容/状态/排序 + 替换图片")
  91. compare_pair("comment.edit", "/store/goods.comment/edit&commentId=10002",
  92. {"commentId": 10002,
  93. "form": {"score": 30, "content": "改过的内容", "status": 0, "sort": 50,
  94. "imageIds": [10003, 10004]}},
  95. comment_state)
  96. def case_comment_edit_clear_images():
  97. print("\n[case] 评价编辑:清空图片(is_picture 应变 0)")
  98. compare_pair("comment.edit.clear", "/store/goods.comment/edit&commentId=10001",
  99. {"commentId": 10001,
  100. "form": {"score": 10, "content": "很好", "status": 1, "sort": 0, "imageIds": []}},
  101. comment_state)
  102. def case_comment_delete():
  103. print("\n[case] 评价删除(软删除)")
  104. def state(cur):
  105. cur.execute("select comment_id,is_delete from yoshop_comment where comment_id=10003")
  106. return cur.fetchall()
  107. compare_pair("comment.delete", "/store/goods.comment/delete&commentId=10003",
  108. {"commentId": 10003}, state)
  109. def case_comment_missing():
  110. print("\n[case] 评价不存在:edit / delete")
  111. reset_comments()
  112. php_edit = call(PHP_BASE, "/store/goods.comment/edit&commentId=99999",
  113. body={"commentId": 99999, "form": {"content": "x"}})
  114. java_edit = call(JAVA_BASE, "/store/goods.comment/edit&commentId=99999",
  115. body={"commentId": 99999, "form": {"content": "x"}})
  116. check("comment.edit.missing 响应一致",
  117. php_edit.get("status") == java_edit.get("status")
  118. and php_edit.get("message") == java_edit.get("message"),
  119. f"php={php_edit} java={java_edit}")
  120. php_del = call(PHP_BASE, "/store/goods.comment/delete&commentId=99999", body={"commentId": 99999})
  121. java_del = call(JAVA_BASE, "/store/goods.comment/delete&commentId=99999", body={"commentId": 99999})
  122. check("comment.delete.missing 响应一致",
  123. php_del.get("status") == java_del.get("status")
  124. and php_del.get("message") == java_del.get("message"),
  125. f"php={php_del} java={java_del}")
  126. def case_h5_setting():
  127. print("\n[case] H5 设置更新(新增 → 再更新)")
  128. def state(cur):
  129. cur.execute("select `key`,`describe`,`values`,store_id from yoshop_h5_setting order by `key`")
  130. return cur.fetchall()
  131. def clear():
  132. conn = db()
  133. conn.cursor().execute("delete from yoshop_h5_setting")
  134. conn.close()
  135. clear()
  136. php1 = call(PHP_BASE, "/store/client.h5.setting/update&key=basic",
  137. body={"key": "basic", "form": {"enabled": False, "baseUrl": "https://h5.example.com"}})
  138. conn = db()
  139. php_state = state(conn.cursor())
  140. conn.close()
  141. clear()
  142. java1 = call(JAVA_BASE, "/store/client.h5.setting/update?key=basic",
  143. body={"key": "basic", "form": {"enabled": False, "baseUrl": "https://h5.example.com"}})
  144. conn = db()
  145. java_state = state(conn.cursor())
  146. conn.close()
  147. check("h5.update 首次响应一致",
  148. php1.get("status") == java1.get("status") and php1.get("message") == java1.get("message"),
  149. f"php={php1} java={java1}")
  150. check("h5.update 落库一致", php_state == java_state, f"\n php={php_state}\n java={java_state}")
  151. # 二次更新(覆盖同一条记录)
  152. php2 = call(PHP_BASE, "/store/client.h5.setting/update&key=basic",
  153. body={"key": "basic", "form": {"enabled": True, "baseUrl": "https://h5b.example.com"}})
  154. conn = db()
  155. php_state2 = state(conn.cursor())
  156. conn.close()
  157. clear()
  158. java2 = call(JAVA_BASE, "/store/client.h5.setting/update?key=basic",
  159. body={"key": "basic", "form": {"enabled": True, "baseUrl": "https://h5b.example.com"}})
  160. conn = db()
  161. java_state2 = state(conn.cursor())
  162. conn.close()
  163. check("h5.update 二次覆盖一致",
  164. php2.get("status") == java2.get("status") and php_state2 == java_state2,
  165. f"\n php={php2}\n java={java2}\n php_state={php_state2}\n java_state={java_state2}")
  166. clear()
  167. def case_wxapp_setting():
  168. print("\n[case] 小程序设置更新(enableShipping=false)")
  169. def state(cur):
  170. cur.execute("select `key`,`describe`,`values`,store_id from yoshop_wxapp_setting order by `key`")
  171. return cur.fetchall()
  172. def clear():
  173. conn = db()
  174. conn.cursor().execute("delete from yoshop_wxapp_setting")
  175. conn.close()
  176. form = {"enabled": True, "app_id": "wxtest123", "app_secret": "secret456", "enableShipping": False}
  177. clear()
  178. php1 = call(PHP_BASE, "/store/client.wxapp.setting/update&key=basic",
  179. body={"key": "basic", "form": form})
  180. conn = db()
  181. php_state = state(conn.cursor())
  182. conn.close()
  183. clear()
  184. java1 = call(JAVA_BASE, "/store/client.wxapp.setting/update?key=basic",
  185. body={"key": "basic", "form": form})
  186. conn = db()
  187. java_state = state(conn.cursor())
  188. conn.close()
  189. check("wxapp.update 响应一致",
  190. php1.get("status") == java1.get("status") and php1.get("message") == java1.get("message"),
  191. f"php={php1} java={java1}")
  192. check("wxapp.update 落库一致", php_state == java_state, f"\n php={php_state}\n java={java_state}")
  193. clear()
  194. def export_case_rows(base, export_dir, label):
  195. """执行一次导出,返回 (响应, 记录行, 单元格矩阵)"""
  196. resp = call(base, "/store/order.export/exportOrder&dataType=all"
  197. "&columns%5B%5D=order_no&columns%5B%5D=goods_detail&columns%5B%5D=pay_price"
  198. "&columns%5B%5D=create_time&columns%5B%5D=order_status&columns%5B%5D=pay_method"
  199. "&columns%5B%5D=receipt_address&columns%5B%5D=update_price&columns%5B%5D=is_comment",
  200. method="get")
  201. conn = db()
  202. cur = conn.cursor()
  203. cur.execute("select id,start_time,end_time,file_path,status,store_id from yoshop_order_export"
  204. " order by id desc limit 1")
  205. record = cur.fetchone()
  206. conn.close()
  207. rows = []
  208. if record:
  209. path = export_dir / record["file_path"].replace("downloads/", "")
  210. if path.exists():
  211. wb = openpyxl.load_workbook(path)
  212. ws = wb.active
  213. rows = [[("" if c is None else str(c)) for c in row]
  214. for row in ws.iter_rows(values_only=True)]
  215. # 导出文件保留(文件名带时间戳,互不覆盖;沙箱禁止删除,交由人工清理)
  216. # 记录里带时间戳,清理掉便于重复执行
  217. conn = db()
  218. conn.cursor().execute("delete from yoshop_order_export where id=%s", (record["id"],))
  219. conn.close()
  220. print(f" {label} 导出响应={resp.get('status')}/{resp.get('message')} 记录={record}")
  221. return resp, record, rows
  222. def case_export():
  223. print("\n[case] 订单导出:Excel 内容 + 导出记录")
  224. php_resp, php_rec, php_rows = export_case_rows(PHP_BASE, PHP_EXPORT_DIR, "PHP")
  225. java_resp, java_rec, java_rows = export_case_rows(JAVA_BASE, JAVA_EXPORT_DIR, "Java")
  226. check("exportOrder 响应一致",
  227. php_resp.get("status") == java_resp.get("status")
  228. and php_resp.get("message") == java_resp.get("message"),
  229. f"php={php_resp} java={java_resp}")
  230. check("导出记录字段一致(file_path 归一化时间戳)",
  231. php_rec is not None and java_rec is not None
  232. and re.sub(r"order-\d+", "order-TS", php_rec["file_path"])
  233. == re.sub(r"order-\d+", "order-TS", java_rec["file_path"])
  234. and php_rec["status"] == java_rec["status"]
  235. and php_rec["store_id"] == java_rec["store_id"]
  236. and php_rec["start_time"] == java_rec["start_time"]
  237. and php_rec["end_time"] == java_rec["end_time"],
  238. f"\n php={php_rec}\n java={java_rec}")
  239. check("Excel 单元格完全一致", php_rows == java_rows,
  240. f"\n php={php_rows}\n java={java_rows}")
  241. # 无数据分支:筛选一个不存在的会员
  242. php_empty = call(PHP_BASE, "/store/order.export/exportOrder&dataType=all&userId=99999"
  243. "&columns%5B%5D=order_no", method="get")
  244. java_empty = call(JAVA_BASE, "/store/order.export/exportOrder?dataType=all&userId=99999"
  245. "&columns%5B%5D=order_no", method="get")
  246. check("exportOrder 无数据提示一致",
  247. php_empty.get("status") == java_empty.get("status")
  248. and php_empty.get("message") == java_empty.get("message"),
  249. f"php={php_empty} java={java_empty}")
  250. def case_printer():
  251. print("\n[case] 小票打印:打印机不存在 / 驱动返回错误")
  252. php = call(PHP_BASE, "/store/order.event/printer&orderId=10001", body={"orderId": 10001, "form": {"printerId": 99999}})
  253. java = call(JAVA_BASE, "/store/order.event/printer?orderId=10001", body={"orderId": 10001, "form": {"printerId": 99999}})
  254. check("printer 不存在响应一致",
  255. php.get("status") == java.get("status") and php.get("message") == java.get("message"),
  256. f"php={php} java={java}")
  257. # 真实调用飞鹅云接口(测试账号必然被拒),验证两端驱动错误透传一致
  258. conn = db()
  259. cur = conn.cursor()
  260. cur.execute("select count(*) c from yoshop_printer where printer_id=10001")
  261. has_printer = cur.fetchone()["c"] > 0
  262. conn.close()
  263. if not has_printer:
  264. print(" [skip] 未找到 10001 测试打印机,跳过驱动错误对照")
  265. return
  266. php2 = call(PHP_BASE, "/store/order.event/printer&orderId=10001", body={"orderId": 10001, "form": {"printerId": 10001}})
  267. java2 = call(JAVA_BASE, "/store/order.event/printer?orderId=10001", body={"orderId": 10001, "form": {"printerId": 10001}})
  268. check("printer 驱动失败响应一致(含 data 结构)",
  269. php2.get("status") == java2.get("status")
  270. and php2.get("message") == java2.get("message")
  271. and php2.get("data") == java2.get("data"),
  272. f"php={php2} java={java2}")
  273. def case_export_between_time():
  274. print("\n[case] 订单导出:带 betweenTime 时落库起止日期")
  275. def run(base, export_dir, label):
  276. resp = call(base, "/store/order.export/exportOrder&dataType=all"
  277. "&betweenTime%5B%5D=2026-10-01&betweenTime%5B%5D=2026-10-17"
  278. "&columns%5B%5D=order_no&columns%5B%5D=create_time", method="get")
  279. conn = db()
  280. cur = conn.cursor()
  281. cur.execute("select id,start_time,end_time,status,store_id,file_path from yoshop_order_export"
  282. " order by id desc limit 1")
  283. rec = cur.fetchone()
  284. cur.execute("delete from yoshop_order_export where id=%s", (rec["id"],))
  285. conn.close()
  286. print(f" {label} 记录={rec}")
  287. return resp, rec
  288. php_resp, php_rec = run(PHP_BASE, PHP_EXPORT_DIR, "PHP")
  289. java_resp, java_rec = run(JAVA_BASE, JAVA_EXPORT_DIR, "Java")
  290. check("betweenTime 导出响应一致",
  291. php_resp.get("status") == java_resp.get("status")
  292. and php_resp.get("message") == java_resp.get("message"),
  293. f"php={php_resp} java={java_resp}")
  294. check("betweenTime 起止时间落库一致",
  295. php_rec and java_rec
  296. and php_rec["start_time"] == java_rec["start_time"]
  297. and php_rec["end_time"] == java_rec["end_time"]
  298. and php_rec["status"] == java_rec["status"],
  299. f"\n php={php_rec}\n java={java_rec}")
  300. def main():
  301. case_comment_edit()
  302. case_comment_edit_clear_images()
  303. case_comment_delete()
  304. case_comment_missing()
  305. case_h5_setting()
  306. case_wxapp_setting()
  307. case_export()
  308. case_export_between_time()
  309. case_printer()
  310. reset_comments()
  311. print(f"\n通过 {passed} / 失败 {failed}")
  312. if __name__ == "__main__":
  313. main()