refresh_selection_request_items.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. #!/usr/bin/env python3
  2. """
  3. Refresh buff_selection_request_item for one request from a JSON file.
  4. Default target:
  5. - user_id = 8
  6. - request_id = 20
  7. - json files = datarefresh/111.json, datarefresh/333.json, datarefresh/2222.json
  8. """
  9. from __future__ import annotations
  10. import argparse
  11. import json
  12. from dataclasses import dataclass
  13. from decimal import Decimal, ROUND_HALF_UP
  14. from pathlib import Path
  15. from typing import Any
  16. import pymysql
  17. USD_RATE = Decimal("5.1")
  18. TWO_DEC = Decimal("0.01")
  19. @dataclass
  20. class DbConfig:
  21. host: str
  22. port: int
  23. user: str
  24. password: str
  25. database: str
  26. def parse_env_file(env_path: Path) -> dict[str, str]:
  27. if not env_path.exists():
  28. raise FileNotFoundError(f".env file not found: {env_path}")
  29. result: dict[str, str] = {}
  30. for raw_line in env_path.read_text(encoding="utf-8").splitlines():
  31. line = raw_line.strip()
  32. if not line or line.startswith("#") or "=" not in line:
  33. continue
  34. key, value = line.split("=", 1)
  35. result[key.strip()] = value.strip().strip('"').strip("'")
  36. return result
  37. def load_db_config(env_path: Path) -> DbConfig:
  38. env_map = parse_env_file(env_path)
  39. required_keys = ["DB_HOST", "DB_PORT", "DB_USER", "DB_PASSWORD", "DB_NAME"]
  40. missing = [k for k in required_keys if k not in env_map or not env_map[k]]
  41. if missing:
  42. raise ValueError(f"Missing DB config in {env_path}: {', '.join(missing)}")
  43. return DbConfig(
  44. host=env_map["DB_HOST"],
  45. port=int(env_map["DB_PORT"]),
  46. user=env_map["DB_USER"],
  47. password=env_map["DB_PASSWORD"],
  48. database=env_map["DB_NAME"],
  49. )
  50. def normalize_price(price: Any) -> Decimal:
  51. cny = Decimal(str(price)).quantize(TWO_DEC, rounding=ROUND_HALF_UP)
  52. return cny
  53. def build_wear_range(min_paintwear: Any, max_paintwear: Any) -> str:
  54. return f"{min_paintwear}-{max_paintwear}"
  55. def placeholders(size: int) -> str:
  56. return ",".join(["%s"] * size)
  57. def load_payload_from_files(json_paths: list[Path]) -> dict[str, Any]:
  58. if not json_paths:
  59. raise ValueError("At least one JSON path is required")
  60. merged_payload: dict[str, Any] = {}
  61. for path in json_paths:
  62. if not path.exists():
  63. raise FileNotFoundError(f"JSON file not found: {path}")
  64. payload = json.loads(path.read_text(encoding="utf-8"))
  65. if not isinstance(payload, dict):
  66. raise ValueError(f"JSON root must be an object in file: {path}")
  67. if not payload:
  68. raise ValueError(f"JSON has no data in file: {path}")
  69. merged_payload.update(payload)
  70. if not merged_payload:
  71. raise ValueError("Merged JSON payload has no data")
  72. return merged_payload
  73. def refresh_items(
  74. *,
  75. db_config: DbConfig,
  76. json_paths: list[Path],
  77. user_id: int,
  78. request_id: int,
  79. ) -> None:
  80. payload = load_payload_from_files(json_paths)
  81. goods_ids = [str(goods_id) for goods_id in payload.keys()]
  82. conn = pymysql.connect(
  83. host=db_config.host,
  84. port=db_config.port,
  85. user=db_config.user,
  86. password=db_config.password,
  87. database=db_config.database,
  88. charset="utf8mb4",
  89. autocommit=False,
  90. cursorclass=pymysql.cursors.DictCursor,
  91. )
  92. try:
  93. with conn.cursor() as cursor:
  94. sql_goods = f"""
  95. SELECT
  96. gi.goods_id,
  97. gi.name AS goods_name,
  98. gi.steam_url AS steam_link,
  99. ga.icon_url
  100. FROM buff_goods_info gi
  101. LEFT JOIN buff_goods_attr ga ON gi.goods_id = ga.goods_id
  102. WHERE gi.goods_id IN ({placeholders(len(goods_ids))})
  103. """
  104. cursor.execute(sql_goods, goods_ids)
  105. goods_rows = cursor.fetchall()
  106. goods_map = {str(row["goods_id"]): row for row in goods_rows}
  107. missing_goods = [gid for gid in goods_ids if gid not in goods_map]
  108. if missing_goods:
  109. raise ValueError(
  110. "These goods_id are missing in buff_goods_info: "
  111. + ", ".join(missing_goods)
  112. )
  113. delete_sql = """
  114. DELETE FROM buff_selection_request_item
  115. WHERE request_id = %s AND user_id = %s
  116. """
  117. cursor.execute(delete_sql, (request_id, user_id))
  118. deleted_count = cursor.rowcount
  119. insert_sql = """
  120. INSERT INTO buff_selection_request_item (
  121. request_id,
  122. user_id,
  123. goods_id,
  124. goods_name,
  125. icon_url,
  126. steam_link,
  127. price_cny,
  128. price_usd,
  129. wear_range
  130. ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
  131. """
  132. rows_to_insert = []
  133. for goods_id, value in payload.items():
  134. if not isinstance(value, dict):
  135. raise ValueError(f"Invalid value for goods_id={goods_id}: must be object")
  136. if "min_paintwear" not in value or "max_paintwear" not in value or "price" not in value:
  137. raise ValueError(
  138. f"goods_id={goods_id} missing one of required keys: "
  139. "min_paintwear, max_paintwear, price"
  140. )
  141. goods_meta = goods_map[str(goods_id)]
  142. price_cny = normalize_price(value["price"])
  143. price_usd = (price_cny / USD_RATE).quantize(TWO_DEC, rounding=ROUND_HALF_UP)
  144. wear_range = build_wear_range(value["min_paintwear"], value["max_paintwear"])
  145. rows_to_insert.append(
  146. (
  147. request_id,
  148. user_id,
  149. str(goods_id),
  150. goods_meta.get("goods_name"),
  151. goods_meta.get("icon_url"),
  152. goods_meta.get("steam_link"),
  153. price_cny,
  154. price_usd,
  155. wear_range,
  156. )
  157. )
  158. cursor.executemany(insert_sql, rows_to_insert)
  159. inserted_count = cursor.rowcount
  160. conn.commit()
  161. print(f"Done. Deleted {deleted_count} rows, inserted {inserted_count} rows.")
  162. files = ", ".join(str(p) for p in json_paths)
  163. print(f"request_id={request_id}, user_id={user_id}, json_files=[{files}]")
  164. except Exception:
  165. conn.rollback()
  166. raise
  167. finally:
  168. conn.close()
  169. def parse_args() -> argparse.Namespace:
  170. parser = argparse.ArgumentParser(
  171. description="Refresh buff_selection_request_item from JSON data"
  172. )
  173. parser.add_argument(
  174. "--json-paths",
  175. nargs="+",
  176. default=[
  177. str(Path(__file__).resolve().parent / "111.json"),
  178. str(Path(__file__).resolve().parent / "333.json"),
  179. str(Path(__file__).resolve().parent / "2222.json"),
  180. ],
  181. help="Paths to source JSON files. Later files override duplicate goods_id",
  182. )
  183. parser.add_argument(
  184. "--env-path",
  185. default=str(Path(__file__).resolve().parents[1] / "backend-admin" / ".env"),
  186. help="Path to .env with DB_HOST/DB_PORT/DB_USER/DB_PASSWORD/DB_NAME",
  187. )
  188. parser.add_argument("--user-id", type=int, default=11, help="Target user_id")
  189. parser.add_argument("--request-id", type=int, default=21, help="Target request_id")
  190. return parser.parse_args()
  191. def main() -> None:
  192. args = parse_args()
  193. env_path = Path(args.env_path).resolve()
  194. json_paths = [Path(path).resolve() for path in args.json_paths]
  195. db_config = load_db_config(env_path)
  196. refresh_items(
  197. db_config=db_config,
  198. json_paths=json_paths,
  199. user_id=args.user_id,
  200. request_id=args.request_id,
  201. )
  202. if __name__ == "__main__":
  203. main()