test_goods_writes.py 13 KB

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