test_goods_writes.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. # -*- coding: utf-8 -*-
  2. """商品写接口端到端自测:Java 写入 → PHP 读取 → 逐字段比对。
  3. 因为本地 Java 与线上 PHP 共用同一个库(192.168.1.177/shop),
  4. 所以「Java 新增/编辑的商品,PHP 端 detail 能读、且与 Java detail 完全一致」
  5. 是比单侧断言更强的正确性证明。
  6. 用法:python test_goods_writes.py
  7. """
  8. import re
  9. import json
  10. import requests
  11. import pymysql
  12. from pathlib import Path
  13. from compare_live import diff
  14. JAVA_BASE = "http://127.0.0.1:18080"
  15. PHP_BASE = "https://shop.qinys.cn/index.php?s="
  16. TOKEN = "testsession123"
  17. STORE_ID = 10001
  18. SESSION = requests.Session()
  19. SESSION.trust_env = False
  20. YML = Path(r"C:\Users\Administrator\WorkBuddy\2026-10-17-12-27-10\java-rewrite\yoshop-java\src\main\resources\application.yml")
  21. passed = failed = 0
  22. def check(name, cond, extra=""):
  23. global passed, failed
  24. if cond:
  25. passed += 1
  26. print(f" [ok] {name}")
  27. else:
  28. failed += 1
  29. print(f" [FAIL] {name} {extra}")
  30. def db():
  31. text = YML.read_text(encoding="utf-8")
  32. pwd = re.findall(r"password:\s*(.+)", text)[0].strip().strip('"')
  33. # autocommit=True:Java 侧写入要立刻可见,避免 REPEATABLE READ 快照挡住新数据
  34. return pymysql.connect(host="192.168.1.177", port=3306, user="root",
  35. password=pwd, database="shop", charset="utf8mb4",
  36. autocommit=True, cursorclass=pymysql.cursors.DictCursor)
  37. def php_token():
  38. src = Path(r"F:\import goods\upload_goods.py").read_text(encoding="utf-8")
  39. return re.search(r'TOKEN\s*=\s*"([^"]+)"', src).group(1)
  40. def java(path, method="get", params=None, body=None):
  41. if method == "get":
  42. r = SESSION.get(JAVA_BASE + path, headers={"Access-Token": TOKEN}, params=params or {}, timeout=60)
  43. else:
  44. r = SESSION.post(JAVA_BASE + path, headers={"Access-Token": TOKEN}, json=body, timeout=60)
  45. return r.json()
  46. def php(path, params=None, token=""):
  47. r = SESSION.get(PHP_BASE + path, headers={"Access-Token": token}, params=params or {}, timeout=60)
  48. return r.json()
  49. def single_form(name, category_id, image_id):
  50. return {
  51. "goods_type": 10,
  52. "goods_name": name,
  53. "goods_no": "TEST-SINGLE",
  54. "selling_point": "自测卖点",
  55. "categoryIds": [category_id],
  56. "imagesIds": [image_id],
  57. "serviceIds": [],
  58. "video_id": 0,
  59. "video_cover_id": 0,
  60. "delivery_id": 10001,
  61. "is_ind_delivery_type": 0,
  62. "delivery_type": [10],
  63. "status": 10,
  64. "sort": 99,
  65. "spec_type": 10,
  66. "goods_price": 12.34,
  67. "line_price": 20,
  68. "stock_num": 50,
  69. "goods_weight": 0.5,
  70. "deduct_stock_type": 10,
  71. "is_restrict": 0,
  72. "content": "<p>自测商品详情</p>",
  73. "sales_initial": 3,
  74. "is_points_gift": 1,
  75. "is_points_discount": 1,
  76. "is_alone_points_discount": 0,
  77. "is_enable_grade": 1,
  78. "is_alone_grade": 0,
  79. }
  80. def multi_form(name, category_id, image_id, price1=8.8, price2=18.8):
  81. return {
  82. **single_form(name, category_id, image_id),
  83. "spec_type": 20,
  84. "goods_price": None,
  85. "line_price": None,
  86. "stock_num": None,
  87. "specData": {
  88. "specList": [
  89. {"key": 0, "spec_name": "自测颜色", "valueList": [
  90. {"key": 0, "groupKey": 0, "spec_value": "自测红"},
  91. {"key": 1, "groupKey": 0, "spec_value": "自测蓝"},
  92. ]}
  93. ],
  94. "skuList": [
  95. {"image_id": 0, "goods_price": price1, "line_price": 0, "stock_num": 7,
  96. "goods_weight": 0, "goods_sku_no": "T-R", "skuKeys": [{"groupKey": 0, "valueKey": 0}]},
  97. {"image_id": 0, "goods_price": price2, "line_price": 0, "stock_num": 9,
  98. "goods_weight": 0, "goods_sku_no": "T-B", "skuKeys": [{"groupKey": 0, "valueKey": 1}]},
  99. ],
  100. },
  101. }
  102. def main():
  103. conn = db()
  104. cur = conn.cursor()
  105. token = php_token()
  106. # 依赖数据:一个分类 + 一张已有图片
  107. cur.execute("SELECT category_id FROM yoshop_category ORDER BY category_id LIMIT 1")
  108. category_id = cur.fetchone()["category_id"]
  109. cur.execute("SELECT file_id FROM yoshop_upload_file WHERE file_type=10 AND is_delete=0 ORDER BY file_id DESC LIMIT 1")
  110. image_id = cur.fetchone()["file_id"]
  111. print(f"依赖:categoryId={category_id} imageId={image_id}")
  112. # 清理历史残留(上次异常中断留下的同前缀商品)
  113. cur.execute("SELECT goods_id FROM yoshop_goods WHERE goods_name LIKE '接口自测商品-%'")
  114. for old in cur.fetchall():
  115. oid = old["goods_id"]
  116. for t in ("yoshop_goods_sku", "yoshop_goods_spec_rel", "yoshop_goods_category_rel", "yoshop_goods_image"):
  117. cur.execute(f"DELETE FROM {t} WHERE goods_id=%s", (oid,))
  118. cur.execute("DELETE FROM yoshop_goods WHERE goods_id=%s", (oid,))
  119. cur.execute("DELETE FROM yoshop_spec_value WHERE spec_value IN ('自测红','自测蓝')")
  120. cur.execute("DELETE FROM yoshop_spec WHERE spec_name='自测颜色'")
  121. name = f"接口自测商品-{int(__import__('time').time())}"
  122. # ---------------------------------------------------------------- 1. 新增(单规格)
  123. print("\n[1] 新增单规格商品")
  124. res = java("/store/goods/add", "post", body={"form": single_form(name, category_id, image_id)})
  125. check("add 返回 200/添加成功", res.get("status") == 200 and res.get("message") == "添加成功", res)
  126. cur.execute("SELECT * FROM yoshop_goods WHERE goods_name=%s ORDER BY goods_id DESC LIMIT 1", (name,))
  127. row = cur.fetchone()
  128. check("商品已落库", row is not None)
  129. goods_id = row["goods_id"]
  130. check("spec_type=10", row["spec_type"] == 10, row["spec_type"])
  131. check("goods_price_min/max=12.34", str(row["goods_price_min"]) == "12.34" and str(row["goods_price_max"]) == "12.34",
  132. (row["goods_price_min"], row["goods_price_max"]))
  133. check("stock_total=50", row["stock_total"] == 50, row["stock_total"])
  134. check("delivery_type 存 JSON", row["delivery_type"] == "[10]", row["delivery_type"])
  135. check("alone_grade_equity=[]", row["alone_grade_equity"] == "[]", row["alone_grade_equity"])
  136. cur.execute("SELECT * FROM yoshop_goods_sku WHERE goods_id=%s", (goods_id,))
  137. skus = cur.fetchall()
  138. check("生成 1 条 sku", len(skus) == 1, len(skus))
  139. check("sku goods_sku_id='0'", str(skus[0]["goods_sku_id"]) == "0", skus[0]["goods_sku_id"])
  140. check("sku 价格=12.34", str(skus[0]["goods_price"]) == "12.34", skus[0]["goods_price"])
  141. cur.execute("SELECT COUNT(*) c FROM yoshop_goods_category_rel WHERE goods_id=%s", (goods_id,))
  142. check("分类关联 1 条", cur.fetchone()["c"] == 1)
  143. cur.execute("SELECT COUNT(*) c FROM yoshop_goods_image WHERE goods_id=%s", (goods_id,))
  144. check("图片关联 1 条", cur.fetchone()["c"] == 1)
  145. # ---------------------------------------------------------------- 2. 读回一致性
  146. # 注意:本机 shop 库与线上 PHP 库并非强一致(PHP→本地单向下发),
  147. # 所以写操作用「Java 读回 == 库内实际值」自洽校验,不跨端比对。
  148. print("\n[2] Java 读回与库内数据一致(单规格)")
  149. j = java("/store/goods/detail", "get", {"goodsId": goods_id})
  150. info = j.get("data", {}).get("goodsInfo", {})
  151. check("detail 返回成功", j.get("status") == 200, j.get("message"))
  152. check("detail 名称一致", info.get("goods_name") == name, info.get("goods_name"))
  153. check("detail stock_total=50", info.get("stock_total") == 50, info.get("stock_total"))
  154. check("detail categoryIds", info.get("categoryIds") == [category_id], info.get("categoryIds"))
  155. check("detail skuList 1 条", len(info.get("skuList") or []) == 1)
  156. check("detail goods_image 非空", bool(info.get("goods_image")))
  157. check("detail isSpecLocked=false", info.get("isSpecLocked") is False)
  158. # ---------------------------------------------------------------- 3. 编辑为多规格
  159. print("\n[3] 编辑为多规格(sku 重建 + 规格入库)")
  160. res = java("/store/goods/edit", "post", body={"goodsId": goods_id,
  161. "form": multi_form(name + "-多规格", category_id, image_id)})
  162. check("edit 返回 200/更新成功", res.get("status") == 200 and res.get("message") == "更新成功", res)
  163. cur.execute("SELECT * FROM yoshop_goods WHERE goods_id=%s", (goods_id,))
  164. row = cur.fetchone()
  165. check("spec_type=20", row["spec_type"] == 20, row["spec_type"])
  166. check("价格区间 8.80~18.80", (str(row["goods_price_min"]), str(row["goods_price_max"])) == ("8.80", "18.80"),
  167. (row["goods_price_min"], row["goods_price_max"]))
  168. check("库存合计 16", row["stock_total"] == 16, row["stock_total"])
  169. cur.execute("SELECT * FROM yoshop_goods_sku WHERE goods_id=%s ORDER BY id", (goods_id,))
  170. skus = cur.fetchall()
  171. check("sku 数量 2", len(skus) == 2, len(skus))
  172. # 只有 1 个规格组时组合就是单个规格值 ID(PHP implode 行为一致)
  173. check("goods_sku_id = 规格值 ID", str(skus[0]["goods_sku_id"]).isdigit(), skus[0]["goods_sku_id"])
  174. props = json.loads(skus[0]["goods_props"] or "[]")
  175. check("goods_props 结构正确", props and props[0]["group"]["name"] == "自测颜色", skus[0]["goods_props"])
  176. cur.execute("SELECT COUNT(*) c FROM yoshop_goods_spec_rel WHERE goods_id=%s", (goods_id,))
  177. check("规格关联 2 条", cur.fetchone()["c"] == 2)
  178. cur.execute("SELECT COUNT(*) c FROM yoshop_spec WHERE spec_name='自测颜色'")
  179. check("spec 组已创建", cur.fetchone()["c"] == 1)
  180. print("\n[4] Java 读回与库内数据一致(多规格)")
  181. j = java("/store/goods/detail", "get", {"goodsId": goods_id})
  182. info = j.get("data", {}).get("goodsInfo", {})
  183. check("detail 价格区间 8.80~18.80",
  184. (info.get("goods_price_min"), info.get("goods_price_max")) == ("8.80", "18.80"),
  185. (info.get("goods_price_min"), info.get("goods_price_max")))
  186. check("detail skuList 2 条", len(info.get("skuList") or []) == 2)
  187. check("detail specList 1 组", len(info.get("specList") or []) == 1)
  188. check("specList 结构正确",
  189. (info.get("specList") or [{}])[0].get("spec_name") == "自测颜色",
  190. info.get("specList"))
  191. check("sku 的 goods_props 为数组",
  192. isinstance((info.get("skuList") or [{}])[0].get("goods_props"), list),
  193. type((info.get("skuList") or [{}])[0].get("goods_props")).__name__)
  194. check("sku 的 spec_value_ids 为数组",
  195. isinstance((info.get("skuList") or [{}])[0].get("spec_value_ids"), list))
  196. # ---------------------------------------------------------------- 5. 上下架
  197. print("\n[5] 批量下架 / 上架")
  198. res = java("/store/goods/state", "post", body={"goodsIds": [goods_id], "state": False})
  199. check("state 返回 操作成功", res.get("status") == 200 and res.get("message") == "操作成功", res)
  200. cur.execute("SELECT status FROM yoshop_goods WHERE goods_id=%s", (goods_id,))
  201. check("下架后 status=20", cur.fetchone()["status"] == 20)
  202. java("/store/goods/state", "post", body={"goodsIds": [goods_id], "state": True})
  203. cur.execute("SELECT status FROM yoshop_goods WHERE goods_id=%s", (goods_id,))
  204. check("上架后 status=10", cur.fetchone()["status"] == 10)
  205. # ---------------------------------------------------------------- 6. 删除
  206. print("\n[6] 软删除")
  207. res = java("/store/goods/delete", "post", body={"goodsIds": [goods_id]})
  208. check("delete 返回 删除成功", res.get("status") == 200 and res.get("message") == "删除成功", res)
  209. cur.execute("SELECT is_delete FROM yoshop_goods WHERE goods_id=%s", (goods_id,))
  210. check("is_delete=1", cur.fetchone()["is_delete"] == 1)
  211. j = java("/store/goods/detail", "get", {"goodsId": goods_id})
  212. check("删除后 detail 报不存在", j.get("status") == 500 and "不存在" in str(j.get("message")), j.get("message"))
  213. jl = java("/store/goods/list", "get", {"page": 1})
  214. names = [g["goods_name"] for g in jl["data"]["list"]["data"]]
  215. check("删除后列表不可见", (name + "-多规格") not in names)
  216. # ---------------------------------------------------------------- 7. 清理
  217. print("\n[7] 清理测试数据")
  218. cur.execute("DELETE FROM yoshop_goods_sku WHERE goods_id=%s", (goods_id,))
  219. cur.execute("DELETE FROM yoshop_goods_spec_rel WHERE goods_id=%s", (goods_id,))
  220. cur.execute("DELETE FROM yoshop_goods_category_rel WHERE goods_id=%s", (goods_id,))
  221. cur.execute("DELETE FROM yoshop_goods_image WHERE goods_id=%s", (goods_id,))
  222. cur.execute("DELETE FROM yoshop_goods WHERE goods_id=%s", (goods_id,))
  223. # 测试规格组/规格值(仅本次创建的)
  224. cur.execute("DELETE FROM yoshop_spec_value WHERE spec_value IN ('自测红','自测蓝')")
  225. cur.execute("DELETE FROM yoshop_spec WHERE spec_name='自测颜色'")
  226. conn.commit()
  227. cur.execute("SELECT COUNT(*) c FROM yoshop_goods WHERE goods_id=%s", (goods_id,))
  228. check("测试商品已清理", cur.fetchone()["c"] == 0)
  229. conn.close()
  230. print(f"\n通过 {passed} / 失败 {failed}")
  231. if __name__ == "__main__":
  232. main()