test_final_writes.py 16 KB

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