jw_detail.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. # -*- coding: utf-8 -*-
  2. # Author : Charley
  3. # Python : 3.12.10
  4. # Date : 2026/08/19
  5. """集物星球商品详情补全:拉 merchantGoodsId(免登录) 取详情页字段,供在售/已售两表补全。
  6. 详情字段在列表接口(index/top、corp/history)里没有,只在商品详情里,需逐商品补拉一次。
  7. 用 detail_fetched 状态位控制每商品只补一次(在售的列表 upsert 不动这些详情列,避免被覆盖)。
  8. 图片:取 resList 第一张图(商品主图)的相对路径 resAddr,拼 FILE_DOMAIN(逆向 EnvironmentManager 得) 成完整 URL。
  9. """
  10. import time
  11. import jiwu_core as core
  12. IMG_BASE_URL = "https://files.jiwustar.com/" # 文件/图片域名(EnvironmentManager RELEASE 环境 FILE_DOMAIN)
  13. # 详情补全的列(两表同名同序),值来源见 parse_detail_fields
  14. DETAIL_COLS = ["img", "goods_type", "price", "original_price", "plan_up_time", "off_shelf_time",
  15. "stock", "surplus_stock", "status", "collection_card_name", "gift_way", "random_way",
  16. "gift_series"]
  17. def fetch_detail(log, goods_id: int) -> dict | None:
  18. """拉商品详情 merchantGoodsId(免登录),返回 data 对象。
  19. Args:
  20. log: 日志对象。
  21. goods_id (int): 商品 goodsId。
  22. Returns:
  23. dict | None: 详情 data 字典;失败返回 None。
  24. """
  25. j = core.do_request(log, "/search/app/merchantGoodsId",
  26. {"goodsId": str(goods_id), "systemBusinessType": 5}) # 免登录
  27. if not j:
  28. return None
  29. return j.get("data") or None
  30. def parse_detail_fields(data: dict) -> dict:
  31. """从详情 data 抽取补全列(含图片拼完整域名;collection/gift/random 在 cardGoods 里)。
  32. Args:
  33. data (dict | None): fetch_detail 返回的详情 data;None 时返回全 None 的列。
  34. Returns:
  35. dict: 键为 DETAIL_COLS 的字典。
  36. """
  37. if not data:
  38. return {c: None for c in DETAIL_COLS}
  39. cg = data.get("cardGoods") or {}
  40. res_list = data.get("resList") or []
  41. addr = res_list[0].get("resAddr") if res_list else None # resList 第一张=商品主图
  42. return {
  43. "img": (IMG_BASE_URL + addr) if addr else None,
  44. "goods_type": data.get("goodsType"),
  45. "price": core.to_yuan(data.get("price")),
  46. "original_price": core.to_yuan(data.get("originalPrice")),
  47. "plan_up_time": data.get("planUpTime"),
  48. "off_shelf_time": data.get("offShelfTime"),
  49. "stock": data.get("stock"),
  50. "surplus_stock": data.get("surplusStock"),
  51. "status": data.get("status"),
  52. "collection_card_name": cg.get("collectionCardName"),
  53. "gift_way": cg.get("giftWay"),
  54. "random_way": cg.get("randomWay"),
  55. "gift_series": cg.get("giftSeries") or data.get("specifications"), # 赠品系列(App"系列"),兜底取顶层 specifications
  56. }
  57. def enrich_detail(log, pool, table: str, goods_ids: list) -> None:
  58. """对表内 detail_fetched=0 的商品补全详情列,成功后置 detail_fetched=1(每商品补一次)。
  59. Args:
  60. log: 日志对象。
  61. pool: 数据库连接池。
  62. table (str): 目标表名(jw_onsale_product_record / jw_sold_product_record,受控常量)。
  63. goods_ids (list): 本轮相关的商品 goods_id 列表。
  64. """
  65. ids = list({g for g in goods_ids if g})
  66. if not ids:
  67. return
  68. ph = ",".join(["%s"] * len(ids))
  69. pending = [r[0] for r in pool.select_all(
  70. f"SELECT goods_id FROM {table} WHERE goods_id IN ({ph}) AND detail_fetched=0", tuple(ids))]
  71. if not pending:
  72. return
  73. set_cols = ", ".join(f"`{c}`=%s" for c in DETAIL_COLS)
  74. done = 0
  75. for gid in pending:
  76. data = fetch_detail(log, gid)
  77. if not data: # 取不到详情,不置位、下轮再补
  78. continue
  79. f = parse_detail_fields(data)
  80. pool.update_one(
  81. f"UPDATE {table} SET {set_cols}, detail_fetched=1 WHERE goods_id=%s",
  82. tuple(f[c] for c in DETAIL_COLS) + (gid,))
  83. done += 1
  84. time.sleep(0.2)
  85. if done:
  86. log.info(f"{table} 详情补全 {done} 个商品")