sync_float_price.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. BUFF饰品磨损区间价格数据同步脚本(修正数据逻辑)
  5. 功能:
  6. 1. 将有tag_id、tag_name、min_float、max_float的详细价格数据存储到float_price表
  7. 2. 将没有这些属性的基本价格数据存储到float_price_attr表
  8. 3. 每10分钟执行一次
  9. 使用方法:
  10. python sync_float_price.py # 手动执行一次
  11. python sync_float_price.py --daemon # 守护进程模式(每10分钟执行一次)
  12. python sync_float_price.py --install-cron # 安装到系统定时任务(每10分钟执行)
  13. 作者: 游铛铛技术数字员工
  14. 创建时间: 2026-01-28
  15. """
  16. import os
  17. import sys
  18. import time
  19. import argparse
  20. import logging
  21. import json
  22. import requests
  23. import pymysql
  24. from datetime import datetime, timedelta
  25. from typing import List, Dict, Any, Optional, Tuple
  26. import schedule
  27. import urllib.parse
  28. # ============================================================
  29. # 配置信息
  30. # ============================================================
  31. # BUFF API配置
  32. BUFF_FLOAT_API_URL = "https://buff.163.com/api/market/enterprise/float/items"
  33. BUFF_API_TOKEN = "2ce4f8fd95194fe090d53b785056de0b"
  34. # 数据库配置(从环境变量读取)
  35. DB_HOST = os.getenv("DB_HOST", "125.122.32.114")
  36. DB_PORT = int(os.getenv("DB_PORT", "3306"))
  37. DB_USER = os.getenv("DB_USER", "root")
  38. DB_PASSWORD = os.getenv("DB_PASSWORD", "Buff123456")
  39. DB_NAME = os.getenv("DB_NAME", "buff")
  40. # 请求配置
  41. REQUEST_TIMEOUT = 60
  42. BATCH_SIZE = 5000 # 批量处理大小
  43. MAX_RETRIES = 3
  44. RETRY_DELAY = 5
  45. # 执行频率配置(分钟)
  46. SYNC_INTERVAL_MINUTES = 10
  47. # ============================================================
  48. # 配置日志
  49. # ============================================================
  50. def setup_logging():
  51. log_dir = os.path.dirname(os.path.abspath(__file__))
  52. log_file = os.path.join(log_dir, "logs", f"sync_float_price_{datetime.now().strftime('%Y%m%d')}.log")
  53. os.makedirs(os.path.dirname(log_file), exist_ok=True)
  54. logging.basicConfig(
  55. level=logging.INFO,
  56. format='%(asctime)s - %(levelname)s - %(message)s',
  57. handlers=[
  58. logging.StreamHandler(sys.stdout),
  59. logging.FileHandler(log_file, encoding='utf-8')
  60. ]
  61. )
  62. return logging.getLogger(__name__)
  63. logger = setup_logging()
  64. # ============================================================
  65. # 数据库操作类
  66. # ============================================================
  67. class FloatPriceDatabase:
  68. """磨损区间价格数据库操作类"""
  69. def __init__(self):
  70. self.connection = None
  71. self._connect()
  72. def _connect(self):
  73. """建立数据库连接"""
  74. try:
  75. self.connection = pymysql.connect(
  76. host=DB_HOST,
  77. port=DB_PORT,
  78. user=DB_USER,
  79. password=DB_PASSWORD,
  80. database=DB_NAME,
  81. charset='utf8mb4',
  82. cursorclass=pymysql.cursors.DictCursor,
  83. autocommit=False,
  84. connect_timeout=10,
  85. read_timeout=30,
  86. write_timeout=30
  87. )
  88. logger.info(f"数据库连接成功: {DB_HOST}:{DB_PORT}/{DB_NAME}")
  89. except Exception as e:
  90. logger.error(f"数据库连接失败: {e}")
  91. raise
  92. def close(self):
  93. if self.connection and self.connection.open:
  94. self.connection.close()
  95. logger.info("数据库连接已关闭")
  96. def clear_table(self, table_name: str):
  97. """清空表"""
  98. try:
  99. with self.connection.cursor() as cursor:
  100. cursor.execute(f"TRUNCATE TABLE {table_name}")
  101. self.connection.commit()
  102. logger.info(f"已清空 {table_name} 表")
  103. except Exception as e:
  104. logger.warning(f"TRUNCATE失败,使用DELETE: {e}")
  105. try:
  106. with self.connection.cursor() as cursor:
  107. cursor.execute(f"DELETE FROM {table_name}")
  108. self.connection.commit()
  109. logger.info(f"已清空 {table_name} 表")
  110. except Exception as e2:
  111. logger.error(f"清空表失败: {e2}")
  112. raise
  113. def bulk_insert(self, table_name: str, data_list: List[Dict[str, Any]],
  114. columns: List[str]) -> int:
  115. """高性能批量插入"""
  116. if not data_list:
  117. return 0
  118. # 构建INSERT SQL
  119. column_list = ', '.join([f'`{col}`' for col in columns])
  120. placeholders = ', '.join(['%s'] * len(columns))
  121. sql = f"INSERT INTO `{table_name}` ({column_list}) VALUES ({placeholders})"
  122. total_inserted = 0
  123. with self.connection.cursor() as cursor:
  124. # 临时优化插入性能
  125. cursor.execute("SET SESSION autocommit = 0")
  126. cursor.execute("SET SESSION unique_checks = 0")
  127. cursor.execute("SET SESSION foreign_key_checks = 0")
  128. try:
  129. for i in range(0, len(data_list), BATCH_SIZE):
  130. batch = data_list[i:i + BATCH_SIZE]
  131. values = []
  132. for item in batch:
  133. row_values = []
  134. for col in columns:
  135. value = item.get(col)
  136. if value is None:
  137. row_values.append(None)
  138. elif isinstance(value, datetime):
  139. row_values.append(value.strftime('%Y-%m-%d %H:%M:%S'))
  140. elif isinstance(value, (int, float)):
  141. row_values.append(value)
  142. else:
  143. row_values.append(str(value))
  144. values.append(tuple(row_values))
  145. # 使用executemany批量插入
  146. cursor.executemany(sql, values)
  147. total_inserted += len(batch)
  148. # 每插入3批提交一次
  149. if i > 0 and i % (BATCH_SIZE * 3) == 0:
  150. self.connection.commit()
  151. logger.info(f"批量插入进度: {total_inserted}/{len(data_list)}")
  152. # 最终提交
  153. self.connection.commit()
  154. finally:
  155. # 恢复会话设置
  156. cursor.execute("SET SESSION autocommit = 1")
  157. cursor.execute("SET SESSION unique_checks = 1")
  158. cursor.execute("SET SESSION foreign_key_checks = 1")
  159. return total_inserted
  160. def insert_float_price_middle(self, data_list: List[Dict[str, Any]]) -> int:
  161. """批量插入数据到buff_goods_float_price_middle表(存储有tag和磨损区间的详细数据)"""
  162. if not data_list:
  163. return 0
  164. # 定义列和对应的数据字段
  165. columns = [
  166. 'goods_id', 'name', 'qj_min_price', 'qj_buy_max_price',
  167. 'qj_avg_min_price', 'qj_sell_num', 'qj_buy_order_num',
  168. 'min_float', 'max_float', 'tag_id', 'tag_name',
  169. 'stat_time', 'create_time'
  170. ]
  171. # 准备数据
  172. now = datetime.now()
  173. processed_data = []
  174. logger.info(f"开始准备 {len(data_list)} 条详细价格数据(有磨损区间和标签)...")
  175. start_time = time.time()
  176. for item in data_list:
  177. processed_data.append({
  178. 'goods_id': str(item.get('goods_id', '')),
  179. 'name': item.get('name', ''),
  180. 'qj_min_price': float(item.get('min_price', 0)) if item.get('min_price') else 0.00,
  181. 'qj_buy_max_price': float(item.get('buy_max_price', 0)) if item.get('buy_max_price') else 0.00,
  182. 'qj_avg_min_price': float(item.get('avg_min_price', 0)) if item.get('avg_min_price') else 0.00,
  183. 'qj_sell_num': int(item.get('sell_num', 0)) if item.get('sell_num') else 0,
  184. 'qj_buy_order_num': int(item.get('buy_order_num', 0)) if item.get('buy_order_num') else 0,
  185. 'min_float': float(item.get('min_float', 0)) if item.get('min_float') is not None else 0.000000,
  186. 'max_float': float(item.get('max_float', 0)) if item.get('max_float') is not None else 0.000000,
  187. 'tag_id': item.get('tag_id'),
  188. 'tag_name': item.get('tag_name'),
  189. 'stat_time': datetime.fromtimestamp(item.get('stat_time', 0)),
  190. 'create_time': now
  191. })
  192. prep_time = time.time() - start_time
  193. logger.info(f"数据准备完成,耗时: {prep_time:.2f}秒")
  194. logger.info(f"开始插入 {len(processed_data)} 条数据到buff_goods_float_price_middle...")
  195. insert_start = time.time()
  196. result = self.bulk_insert('buff_goods_float_price_middle', processed_data, columns)
  197. insert_time = time.time() - insert_start
  198. logger.info(f"插入完成: {result} 条,耗时: {insert_time:.2f}秒")
  199. return result
  200. def copy_from_middle_to_float_price(self) -> int:
  201. """将中间表数据插入到buff_goods_float_price表(先清空再插入)"""
  202. try:
  203. # 先清空目标表
  204. self.clear_table("buff_goods_float_price")
  205. with self.connection.cursor() as cursor:
  206. sql = """
  207. INSERT INTO buff_goods_float_price
  208. (goods_id, name, qj_min_price, qj_buy_max_price, qj_avg_min_price,
  209. qj_sell_num, qj_buy_order_num, min_float, max_float, tag_id, tag_name,
  210. stat_time, create_time, update_time)
  211. SELECT goods_id, name, qj_min_price, qj_buy_max_price, qj_avg_min_price,
  212. qj_sell_num, qj_buy_order_num, min_float, max_float, tag_id, tag_name,
  213. stat_time, NOW(), NOW()
  214. FROM buff_goods_float_price_middle
  215. """
  216. cursor.execute(sql)
  217. affected_rows = cursor.connection.affected_rows()
  218. self.connection.commit()
  219. return affected_rows
  220. except Exception as e:
  221. logger.error(f"复制详细价格数据失败: {e}")
  222. self.connection.rollback()
  223. return 0
  224. def insert_float_price_attr_middle(self, data_list: List[Dict[str, Any]]) -> int:
  225. """批量插入数据到buff_goods_float_price_attr_middle表(存储没有tag和磨损区间的基本数据)"""
  226. if not data_list:
  227. return 0
  228. # 定义列和对应的数据字段
  229. columns = [
  230. 'goods_id', 'name', 'qj_min_price', 'qj_buy_max_price',
  231. 'qj_avg_min_price', 'qj_sell_num', 'qj_buy_order_num',
  232. 'stat_time', 'create_time'
  233. ]
  234. # 准备数据
  235. now = datetime.now()
  236. processed_data = []
  237. logger.info(f"开始准备 {len(data_list)} 条基本价格数据(无磨损区间无标签)...")
  238. start_time = time.time()
  239. for item in data_list:
  240. processed_data.append({
  241. 'goods_id': str(item.get('goods_id', '')),
  242. 'name': item.get('name', ''),
  243. 'qj_min_price': float(item.get('min_price', 0)) if item.get('min_price') else 0.00,
  244. 'qj_buy_max_price': float(item.get('buy_max_price', 0)) if item.get('buy_max_price') else 0.00,
  245. 'qj_avg_min_price': float(item.get('avg_min_price', 0)) if item.get('avg_min_price') else 0.00,
  246. 'qj_sell_num': int(item.get('sell_num', 0)) if item.get('sell_num') else 0,
  247. 'qj_buy_order_num': int(item.get('buy_order_num', 0)) if item.get('buy_order_num') else 0,
  248. 'stat_time': datetime.fromtimestamp(item.get('stat_time', 0)),
  249. 'create_time': now
  250. })
  251. prep_time = time.time() - start_time
  252. logger.info(f"数据准备完成,耗时: {prep_time:.2f}秒")
  253. logger.info(f"开始插入 {len(processed_data)} 条数据到buff_goods_float_price_attr_middle...")
  254. insert_start = time.time()
  255. result = self.bulk_insert('buff_goods_float_price_attr_middle', processed_data, columns)
  256. insert_time = time.time() - insert_start
  257. logger.info(f"插入完成: {result} 条,耗时: {insert_time:.2f}秒")
  258. return result
  259. def copy_from_middle_to_float_price_attr(self) -> int:
  260. """将中间表数据插入到buff_goods_float_price_attr表(先清空再插入)"""
  261. try:
  262. # 先清空目标表
  263. self.clear_table("buff_goods_float_price_attr")
  264. with self.connection.cursor() as cursor:
  265. sql = """
  266. INSERT INTO buff_goods_float_price_attr
  267. (goods_id, name, qj_min_price, qj_buy_max_price, qj_avg_min_price,
  268. qj_sell_num, qj_buy_order_num, stat_time, create_time, update_time)
  269. SELECT goods_id, name, qj_min_price, qj_buy_max_price, qj_avg_min_price,
  270. qj_sell_num, qj_buy_order_num, stat_time, NOW(), NOW()
  271. FROM buff_goods_float_price_attr_middle
  272. """
  273. cursor.execute(sql)
  274. affected_rows = cursor.connection.affected_rows()
  275. self.connection.commit()
  276. return affected_rows
  277. except Exception as e:
  278. logger.error(f"复制基本价格数据失败: {e}")
  279. self.connection.rollback()
  280. return 0
  281. # ============================================================
  282. # BUFF API 操作类
  283. # ============================================================
  284. class BuffFloatPriceAPI:
  285. """BUFF磨损区间价格API操作类"""
  286. def __init__(self):
  287. self.session = requests.Session()
  288. self.session.headers.update({
  289. "Authorization": f"Bearer {BUFF_API_TOKEN}",
  290. "Accept": "application/json",
  291. "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
  292. })
  293. def fetch_float_price_data(self, game: str = "cs2", include_sticker: int = 1) -> Dict[str, Any]:
  294. """获取磨损区间价格数据"""
  295. params = {"game": game, "include_sticker": include_sticker}
  296. for retry in range(MAX_RETRIES):
  297. try:
  298. logger.info(f"请求BUFF磨损区间API (第{retry + 1}次尝试)...")
  299. response = self.session.get(BUFF_FLOAT_API_URL, params=params, timeout=REQUEST_TIMEOUT)
  300. response.raise_for_status()
  301. data = response.json()
  302. logger.info("=" * 60)
  303. logger.info("🔍 磨损区间API返回数据结构:")
  304. logger.info(f" 顶层keys: {list(data.keys())}")
  305. # 获取数据统计信息
  306. stat_time = data.get("stat_time", 0)
  307. remaining = data.get("req_remaining", 0)
  308. info_data = data.get("info", [])
  309. logger.info(f" 统计时间: {datetime.fromtimestamp(stat_time)}")
  310. logger.info(f" 剩余调用次数: {remaining}")
  311. logger.info(f" 数据条数: {len(info_data)}")
  312. # 检查数据格式
  313. if info_data and len(info_data) > 0:
  314. first_item = info_data[0]
  315. logger.info(f" 第一条数据检查:")
  316. logger.info(f" goods_id: {first_item.get('goods_id')}")
  317. logger.info(f" name: {first_item.get('name')}")
  318. sales = first_item.get('sales', [])
  319. logger.info(f" sales数量: {len(sales)}")
  320. if sales:
  321. for i, sale in enumerate(sales[:2]): # 显示前2个sales
  322. logger.info(f" 第{i+1}个sales:")
  323. logger.info(f" 字段: {list(sale.keys())}")
  324. logger.info(f" min_float: {sale.get('min_float')}")
  325. logger.info(f" max_float: {sale.get('max_float')}")
  326. logger.info(f" tag_id: {sale.get('tag_id')}")
  327. logger.info(f" tag_name: {sale.get('tag_name')}")
  328. logger.info("=" * 60)
  329. # 验证数据格式
  330. if "info" not in data:
  331. logger.error(f"API返回格式错误: 无info字段, 完整响应: {data}")
  332. raise Exception(f"API错误: {data.get('msg', '未知错误')}")
  333. logger.info(f"API请求成功,获取到 {len(info_data)} 条饰品记录")
  334. logger.info(f"API剩余调用次数: {remaining}")
  335. return data
  336. except requests.exceptions.RequestException as e:
  337. logger.error(f"API请求失败 (尝试 {retry + 1}/{MAX_RETRIES}): {e}")
  338. if 'response' in locals():
  339. logger.error(f"响应状态码: {response.status_code}")
  340. logger.error(f"响应内容: {response.text[:500]}")
  341. if retry < MAX_RETRIES - 1:
  342. time.sleep(RETRY_DELAY)
  343. else:
  344. raise
  345. def parse_float_price_data(self, api_response: Dict[str, Any]) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
  346. """解析磨损区间价格数据,返回两种类型的数据列表
  347. Returns:
  348. tuple: (detailed_price_list, basic_price_list)
  349. detailed_price_list: 包含有tag和磨损区间的详细价格数据(用于buff_goods_float_price表)
  350. basic_price_list: 不包含tag和磨损区间的基本价格数据(用于buff_goods_float_price_attr表)
  351. """
  352. logger.info("🔍 开始解析磨损区间数据...")
  353. stat_time = api_response.get("stat_time", 0)
  354. info_data = api_response.get("info", [])
  355. logger.info(f" stat_time: {datetime.fromtimestamp(stat_time)}")
  356. logger.info(f" info列表长度: {len(info_data)}")
  357. detailed_price_list = [] # 包含有tag和磨损区间的详细数据
  358. basic_price_list = [] # 不包含tag和磨损区间的基本数据
  359. start_time = time.time()
  360. total_detailed_count = 0
  361. total_basic_count = 0
  362. for item in info_data:
  363. goods_id = item.get("goods_id")
  364. name = item.get("name")
  365. sales = item.get("sales", [])
  366. # 标记是否已找到基本价格数据
  367. found_basic_price = False
  368. # 处理每个sales条目
  369. for sale in sales:
  370. # 检查是否有tag和磨损区间
  371. has_tag = sale.get("tag_id") is not None or sale.get("tag_name") is not None
  372. has_float = sale.get("min_float") is not None or sale.get("max_float") is not None
  373. # 构建销售数据
  374. sale_data = {
  375. "goods_id": goods_id,
  376. "name": name,
  377. "min_price": sale.get("min_price"),
  378. "avg_min_price": sale.get("avg_min_price"),
  379. "buy_max_price": sale.get("buy_max_price"),
  380. "sell_num": sale.get("sell_num"),
  381. "buy_order_num": sale.get("buy_order_num"),
  382. "stat_time": stat_time,
  383. "min_float": sale.get("min_float"),
  384. "max_float": sale.get("max_float"),
  385. "tag_id": sale.get("tag_id"),
  386. "tag_name": sale.get("tag_name")
  387. }
  388. # 如果有tag或磨损区间,则添加到详细价格列表
  389. if has_tag or has_float:
  390. total_detailed_count += 1
  391. detailed_price_list.append(sale_data)
  392. # 否则添加到基本价格列表(每个饰品只取第一个没有tag和磨损区间的数据)
  393. elif not found_basic_price:
  394. total_basic_count += 1
  395. found_basic_price = True
  396. basic_price_list.append(sale_data)
  397. elapsed = time.time() - start_time
  398. logger.info(f" 解析完成,耗时: {elapsed:.2f}秒")
  399. logger.info(f" 总饰品记录: {len(info_data)} 条")
  400. logger.info(f" 详细价格数据(有tag或有磨损): {total_detailed_count} 条")
  401. logger.info(f" 基本价格数据(无tag无磨损): {total_basic_count} 条")
  402. # 统计不同类型的数据
  403. if detailed_price_list:
  404. logger.info(f" 详细价格数据样本:")
  405. for i in range(min(2, len(detailed_price_list))):
  406. sample = detailed_price_list[i]
  407. logger.info(f" [{i}] goods_id: {sample.get('goods_id')}, "
  408. f"min_float: {sample.get('min_float')}, tag_id: {sample.get('tag_id')}")
  409. if basic_price_list:
  410. logger.info(f" 基本价格数据样本:")
  411. for i in range(min(2, len(basic_price_list))):
  412. sample = basic_price_list[i]
  413. logger.info(f" [{i}] goods_id: {sample.get('goods_id')}, "
  414. f"min_price: {sample.get('min_price')}")
  415. if total_basic_count != len(info_data):
  416. logger.warning(f"警告: 基本价格数据数量({total_basic_count})与饰品记录数量({len(info_data)})不一致")
  417. return detailed_price_list, basic_price_list
  418. # ============================================================
  419. # 主逻辑
  420. # ============================================================
  421. class FloatPriceSyncTask:
  422. """磨损区间价格数据同步任务"""
  423. def __init__(self):
  424. self.db = None
  425. self.api = BuffFloatPriceAPI()
  426. def _init_db(self):
  427. """初始化数据库连接"""
  428. if self.db is None:
  429. self.db = FloatPriceDatabase()
  430. def run(self) -> bool:
  431. """执行同步任务"""
  432. start_time = datetime.now()
  433. logger.info("=" * 60)
  434. logger.info(f"开始执行磨损区间价格数据同步任务: {start_time}")
  435. logger.info("=" * 60)
  436. try:
  437. # 初始化数据库连接
  438. self._init_db()
  439. # Step 1: 清空磨损区间中间表
  440. logger.info("[Step 1/8] 清空buff_goods_float_price_middle表...")
  441. clear_start = time.time()
  442. self.db.clear_table("buff_goods_float_price_middle")
  443. logger.info(f"清空中间表耗时: {time.time() - clear_start:.2f}秒")
  444. # Step 2: 调用BUFF API获取磨损区间数据
  445. logger.info("[Step 2/8] 调用BUFF API获取磨损区间价格数据...")
  446. api_start = time.time()
  447. api_response = self.api.fetch_float_price_data(game="cs2", include_sticker=1)
  448. logger.info(f"API调用耗时: {time.time() - api_start:.2f}秒")
  449. # Step 3: 解析数据
  450. logger.info("[Step 3/8] 解析磨损区间数据...")
  451. parse_start = time.time()
  452. detailed_price_list, basic_price_list = self.api.parse_float_price_data(api_response)
  453. logger.info(f"数据解析耗时: {time.time() - parse_start:.2f}秒")
  454. if not detailed_price_list and not basic_price_list:
  455. logger.warning("未获取到任何价格数据,任务取消")
  456. return False
  457. # Step 4: 插入详细价格数据到中间表(有tag和磨损区间的数据)
  458. logger.info("[Step 4/8] 插入详细价格数据到中间表...")
  459. inserted_detailed_count = self.db.insert_float_price_middle(detailed_price_list)
  460. # Step 5: 清空目标表并更新主表
  461. logger.info("[Step 5/8] 清空buff_goods_float_price表并更新数据...")
  462. copy_start = time.time()
  463. float_inserted = self.db.copy_from_middle_to_float_price()
  464. logger.info(f"详细价格主表插入完成: {float_inserted} 条,耗时: {time.time() - copy_start:.2f}秒")
  465. # Step 6: 清空属性中间表
  466. logger.info("[Step 6/8] 清空buff_goods_float_price_attr_middle表...")
  467. clear_attr_start = time.time()
  468. self.db.clear_table("buff_goods_float_price_attr_middle")
  469. logger.info(f"清空属性中间表耗时: {time.time() - clear_attr_start:.2f}秒")
  470. # Step 7: 插入基本价格数据到中间表
  471. logger.info("[Step 7/8] 插入基本价格数据到中间表...")
  472. inserted_basic_count = self.db.insert_float_price_attr_middle(basic_price_list)
  473. # Step 8: 清空目标表并更新属性主表
  474. logger.info("[Step 8/8] 清空buff_goods_float_price_attr表并更新数据...")
  475. copy_attr_start = time.time()
  476. attr_inserted = self.db.copy_from_middle_to_float_price_attr()
  477. logger.info(f"基本价格主表插入完成: {attr_inserted} 条,耗时: {time.time() - copy_attr_start:.2f}秒")
  478. # 清理中间表
  479. logger.info("[清理] 清空中间表数据...")
  480. cleanup_mid_start = time.time()
  481. self.db.clear_table("buff_goods_float_price_middle")
  482. self.db.clear_table("buff_goods_float_price_attr_middle")
  483. logger.info(f"清理中间表耗时: {time.time() - cleanup_mid_start:.2f}秒")
  484. # 任务完成
  485. end_time = datetime.now()
  486. duration = (end_time - start_time).total_seconds()
  487. logger.info("=" * 60)
  488. logger.info(f"磨损区间价格数据同步任务完成! 总耗时: {duration:.2f}秒")
  489. logger.info(f"同步数据量:")
  490. logger.info(f" 详细价格数据: {inserted_detailed_count} 条(有tag或有磨损)")
  491. logger.info(f" 基本价格数据: {inserted_basic_count} 条(无tag无磨损)")
  492. logger.info(f" 总处理饰品记录: {len(api_response.get('info', []))} 条")
  493. logger.info("=" * 60)
  494. return True
  495. except Exception as e:
  496. logger.exception(f"同步任务失败: {e}")
  497. return False
  498. finally:
  499. if self.db:
  500. self.db.close()
  501. self.db = None
  502. def run_once() -> bool:
  503. """执行一次同步任务"""
  504. task = FloatPriceSyncTask()
  505. return task.run()
  506. def run_daemon():
  507. """守护进程模式,持续运行并定时执行任务"""
  508. logger.info(f"启动守护进程模式... 定时任务: 每{SYNC_INTERVAL_MINUTES}分钟执行一次")
  509. # 立即执行一次
  510. run_once()
  511. # 设置定时任务:每10分钟执行一次
  512. schedule.every(SYNC_INTERVAL_MINUTES).minutes.do(run_once)
  513. logger.info(f"定时任务已设置: 每{SYNC_INTERVAL_MINUTES}分钟执行一次")
  514. logger.info(f"下次执行时间: {schedule.next_run()}")
  515. while True:
  516. try:
  517. schedule.run_pending()
  518. time.sleep(1)
  519. except KeyboardInterrupt:
  520. logger.info("收到中断信号,停止守护进程")
  521. break
  522. except Exception as e:
  523. logger.error(f"定时任务执行异常: {e}")
  524. time.sleep(60)
  525. def install_cron_job():
  526. """安装到系统定时任务(每10分钟执行)"""
  527. script_path = os.path.abspath(__file__)
  528. cron_expression = f"*/{SYNC_INTERVAL_MINUTES} * * * * /usr/bin/python3 {script_path}\n"
  529. try:
  530. import subprocess
  531. result = subprocess.run(["crontab", "-l"], capture_output=True, text=True)
  532. current_crontab = result.stdout
  533. if script_path in current_crontab:
  534. logger.info("定时任务已存在,更新为每10分钟执行")
  535. lines = current_crontab.split('\n')
  536. new_lines = [line for line in lines if script_path not in line]
  537. new_crontab = '\n'.join(new_lines) + '\n' + cron_expression
  538. else:
  539. new_crontab = current_crontab + cron_expression
  540. subprocess.run(["crontab", "-"], input=new_crontab, text=True)
  541. logger.info(f"定时任务安装成功: {cron_expression.strip()}")
  542. except subprocess.CalledProcessError as e:
  543. if "no crontab" in str(e.stderr).lower():
  544. new_crontab = cron_expression
  545. subprocess.run(["crontab", "-"], input=new_crontab, text=True)
  546. logger.info(f"定时任务安装成功: {cron_expression.strip()}")
  547. else:
  548. logger.error(f"安装定时任务失败: {e}")
  549. sys.exit(1)
  550. except Exception as e:
  551. logger.error(f"安装定时任务失败: {e}")
  552. sys.exit(1)
  553. def main():
  554. parser = argparse.ArgumentParser(description="BUFF饰品磨损区间价格数据同步脚本")
  555. parser.add_argument("--daemon", action="store_true", help="守护进程模式(每10分钟执行一次)")
  556. parser.add_argument("--install-cron", action="store_true", help="安装到系统定时任务(每10分钟执行)")
  557. args = parser.parse_args()
  558. if args.install_cron:
  559. install_cron_job()
  560. elif args.daemon:
  561. run_daemon()
  562. else:
  563. success = run_once()
  564. sys.exit(0 if success else 1)
  565. if __name__ == "__main__":
  566. main()