backfill_food_spec_relation.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. # -*- coding: utf-8 -*-
  2. """
  3. 2026-10-19 008 规格数据源统一(去 food_sku 回退)——存量数据回填脚本。
  4. 背景:pos_food.food_sku JSON 是旧规格存储;系统已统一以 food_spec_relation
  5. 为唯一规格源(菜单/报价/订单同源)。部署"去回退"版本前,必须先用本脚本把
  6. 仅在 food_sku 中的存量规格迁入关联表,否则老商品规格会从 App 消失。
  7. 做法(幂等,可重复执行):
  8. 对每个 food_sku 非空且尚无任何关联的商品:
  9. - JSON 内每个规格组:缺 food_specs 行则按 JSON 建(保留原 id);
  10. - 组内每个规格值:缺 food_specs_value 行则按 JSON 建(价格取 JSON 内嵌价);
  11. - 建 food_spec_relation(food_id, specs_id)。
  12. 已有关联的商品整行跳过,不动数据。
  13. 用法:
  14. python backfill_food_spec_relation.py # 默认连测试库(application-druid.yml 测试站点配置)
  15. python backfill_food_spec_relation.py --host H --port P --user U --password P --db foodie # 指定环境(生产执行用)
  16. 执行顺序要求:先在各环境执行本脚本,再部署去回退的后端版本。
  17. """
  18. import argparse
  19. import json
  20. import pymysql
  21. DEFAULT_HOST = "47.237.129.98"
  22. DEFAULT_PORT = 3306
  23. DEFAULT_USER = "foodie"
  24. DEFAULT_PASSWORD = "pdjpwMmr3tscf2Re"
  25. DEFAULT_DB = "foodie"
  26. def columns(cursor, table):
  27. cursor.execute("SHOW COLUMNS FROM `%s`" % table)
  28. return [row["Field"] for row in cursor.fetchall()]
  29. def insert_if_missing(cursor, table, key_column, row, allowed):
  30. cursor.execute("SELECT `%s` FROM `%s` WHERE `%s` = %%s" % (key_column, table, key_column), (row.get(key_column),))
  31. if cursor.fetchone():
  32. return False
  33. fields = [c for c in allowed if c in row and row[c] is not None]
  34. sql = "INSERT INTO `%s` (%s) VALUES (%s)" % (table, ",".join("`%s`" % f for f in fields),
  35. ",".join(["%s"] * len(fields)))
  36. cursor.execute(sql, [row[f] for f in fields])
  37. return True
  38. def main():
  39. parser = argparse.ArgumentParser()
  40. parser.add_argument("--host", default=DEFAULT_HOST)
  41. parser.add_argument("--port", type=int, default=DEFAULT_PORT)
  42. parser.add_argument("--user", default=DEFAULT_USER)
  43. parser.add_argument("--password", default=DEFAULT_PASSWORD)
  44. parser.add_argument("--db", default=DEFAULT_DB)
  45. args = parser.parse_args()
  46. conn = pymysql.connect(host=args.host, port=args.port, user=args.user, password=args.password,
  47. database=args.db, charset="utf8mb4")
  48. cur = conn.cursor(pymysql.cursors.DictCursor)
  49. spec_cols = columns(cur, "food_specs")
  50. value_cols = columns(cur, "food_specs_value")
  51. cur.execute("SELECT id, mdid, food_sku FROM pos_food "
  52. "WHERE food_sku IS NOT NULL AND food_sku <> '' AND food_sku <> '[]'")
  53. foods = cur.fetchall()
  54. skipped = linked = groups_created = values_created = relations_created = malformed = 0
  55. for food in foods:
  56. cur.execute("SELECT specs_id FROM food_spec_relation WHERE food_id = %s", (food["id"],))
  57. if cur.fetchall():
  58. linked += 1
  59. continue
  60. try:
  61. sku_groups = json.loads(food["food_sku"])
  62. except Exception:
  63. malformed += 1
  64. continue
  65. if not isinstance(sku_groups, list):
  66. malformed += 1
  67. continue
  68. touched = False
  69. for group in sku_groups:
  70. gid = group.get("id")
  71. if gid is None:
  72. continue
  73. spec_row = {"id": gid, "title": group.get("title") or "", "type": str(group.get("type") or "1"),
  74. "state": str(group.get("state") or "1"), "language": group.get("language"),
  75. "md_id": food["mdid"], "sort": group.get("sort") or 0,
  76. "is_open": 1 if group.get("isOpen") is not False else 0,
  77. "is_delete": 1 if group.get("isDelete") is True else 0}
  78. if insert_if_missing(cur, "food_specs", "id", spec_row, spec_cols):
  79. groups_created += 1
  80. for value in group.get("objects") or []:
  81. vid = value.get("id")
  82. if vid is None:
  83. continue
  84. value_row = {"id": vid, "parent_id": value.get("parentId") or gid,
  85. "name": value.get("name") or "", "price": value.get("price") or 0,
  86. "state": str(value.get("state") or "0"),
  87. "is_open": 1 if value.get("isOpen") is not False else 0}
  88. if insert_if_missing(cur, "food_specs_value", "id", value_row, value_cols):
  89. values_created += 1
  90. cur.execute("SELECT id FROM food_spec_relation WHERE food_id = %s AND specs_id = %s", (food["id"], gid))
  91. if not cur.fetchone():
  92. cur.execute("INSERT INTO food_spec_relation (food_id, specs_id) VALUES (%s, %s)", (food["id"], gid))
  93. relations_created += 1
  94. touched = True
  95. if not touched:
  96. skipped += 1
  97. conn.commit()
  98. print("回填完成:商品总数 %d,已有关联跳过 %d,无有效组跳过 %d,JSON异常 %d;"
  99. "新建规格组 %d,新建规格值 %d,新建关联 %d"
  100. % (len(foods), linked, skipped, malformed, groups_created, values_created, relations_created))
  101. conn.close()
  102. if __name__ == "__main__":
  103. main()