sync_goods_attr copy.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. BUFF饰品属性信息获取脚本(使用连接池版本)
  5. 特点:
  6. 1. 使用DBUtils连接池管理数据库连接
  7. 2. 自动处理连接失效和重连
  8. 3. 更稳定的长连接管理
  9. 4. 避免频繁创建和关闭连接的开销
  10. 5. 直接请求接口,无代理
  11. 快速补齐缺失属性:可设置环境变量调大每轮处理量,例如:
  12. BATCH_SIZE=200 python sync_goods_attr.py
  13. (当前为串行模式:每次 1 个请求,每 3 秒一次)
  14. 安装依赖:
  15. pip install DBUtils==3.0.2
  16. """
  17. import os
  18. import sys
  19. import time
  20. import logging
  21. import threading
  22. import json
  23. import requests
  24. import pymysql
  25. from dbutils.pooled_db import PooledDB
  26. import concurrent.futures
  27. from datetime import datetime
  28. from typing import List, Dict, Any, Optional, Tuple
  29. # ============================================================
  30. # 配置信息
  31. # ============================================================
  32. # BUFF API配置
  33. BUFF_API_BASE_URL = "https://buff.163.com/api/market/goods/sell_order"
  34. GAME = "csgo"
  35. # 数据库配置
  36. DB_HOST = os.getenv("DB_HOST", "47.99.164.66")
  37. DB_PORT = int(os.getenv("DB_PORT", "3306"))
  38. DB_USER = os.getenv("DB_USER", "root")
  39. DB_PASSWORD = os.getenv("DB_PASSWORD", "hn02le.34lkdLKD")
  40. DB_NAME = os.getenv("DB_NAME", "buff")
  41. # 连接池配置
  42. DB_POOL_CONFIG = {
  43. 'creator': pymysql,
  44. 'host': DB_HOST,
  45. 'port': DB_PORT,
  46. 'user': DB_USER,
  47. 'password': DB_PASSWORD,
  48. 'database': DB_NAME,
  49. 'charset': 'utf8mb4',
  50. 'cursorclass': pymysql.cursors.DictCursor,
  51. 'autocommit': True, # 自动提交事务
  52. 'maxconnections': 10, # 最大连接数
  53. 'mincached': 3, # 初始空闲连接数
  54. 'maxcached': 5, # 最大空闲连接数
  55. 'maxusage': 1000, # 单个连接最大使用次数
  56. 'ping': 7, # 使用ping检查连接:0=从不, 1=创建时, 2=获取时, 4=归还时, 7=总是
  57. 'blocking': True, # 连接池满时是否等待
  58. 'setsession': [], # 可选的会话命令列表
  59. }
  60. # 请求配置(可通过环境变量 BATCH_SIZE 调大每轮处理量)
  61. REQUEST_TIMEOUT = 30
  62. REQUEST_INTERVAL = 3 # 每次请求间隔秒数(每3秒请求一次)
  63. CONCURRENT_WORKERS = int(os.getenv("CONCURRENT_WORKERS", "1")) # 并发请求数(1=串行,每3秒一次)
  64. MAX_RETRIES = 3
  65. RETRY_DELAY = 2
  66. BATCH_SIZE = int(os.getenv("BATCH_SIZE", "100")) # 每轮拉取的饰品数量,调大可加快整体补齐
  67. # ============================================================
  68. # 配置日志
  69. # ============================================================
  70. def setup_logging():
  71. """配置日志系统"""
  72. log_dir = os.path.dirname(os.path.abspath(__file__))
  73. log_file = os.path.join(log_dir, "logs", f"sync_goods_attr_{datetime.now().strftime('%Y%m%d')}.log")
  74. os.makedirs(os.path.dirname(log_file), exist_ok=True)
  75. logging.basicConfig(
  76. level=logging.INFO,
  77. format='%(asctime)s - %(levelname)s - [%(threadName)s] - %(message)s',
  78. handlers=[
  79. logging.StreamHandler(sys.stdout),
  80. logging.FileHandler(log_file, encoding='utf-8')
  81. ]
  82. )
  83. return logging.getLogger(__name__)
  84. logger = setup_logging()
  85. # 请求频率限制:每 REQUEST_INTERVAL 秒最多一次请求(多线程安全)
  86. _request_lock = threading.Lock()
  87. _last_request_time = [0.0] # 使用列表以便在闭包中修改
  88. # ============================================================
  89. # 数据库连接池操作类
  90. # ============================================================
  91. class DatabaseConnectionPool:
  92. """数据库连接池管理类"""
  93. _instance = None
  94. _pool = None
  95. def __new__(cls):
  96. if cls._instance is None:
  97. cls._instance = super().__new__(cls)
  98. cls._instance._init_pool()
  99. return cls._instance
  100. def _init_pool(self):
  101. """初始化连接池"""
  102. try:
  103. self._pool = PooledDB(**DB_POOL_CONFIG)
  104. logger.info(f"数据库连接池初始化成功: {DB_HOST}:{DB_PORT}/{DB_NAME}")
  105. logger.info(f"连接池配置: maxconnections={DB_POOL_CONFIG['maxconnections']}, mincached={DB_POOL_CONFIG['mincached']}")
  106. except Exception as e:
  107. logger.error(f"数据库连接池初始化失败: {e}")
  108. raise
  109. def get_connection(self):
  110. """从连接池获取连接"""
  111. try:
  112. conn = self._pool.connection()
  113. return conn
  114. except Exception as e:
  115. logger.error(f"从连接池获取连接失败: {e}")
  116. # 尝试重新初始化连接池
  117. time.sleep(2)
  118. self._init_pool()
  119. return self._pool.connection()
  120. def close_pool(self):
  121. """关闭连接池"""
  122. if self._pool:
  123. self._pool.close()
  124. logger.info("数据库连接池已关闭")
  125. class GoodsAttrDatabase:
  126. """饰品属性数据库操作类(使用连接池)"""
  127. def __init__(self):
  128. self.pool = DatabaseConnectionPool()
  129. def execute_query(self, sql: str, params=None, fetch_all=True):
  130. """执行查询"""
  131. conn = None
  132. cursor = None
  133. try:
  134. conn = self.pool.get_connection()
  135. cursor = conn.cursor()
  136. if params:
  137. cursor.execute(sql, params)
  138. else:
  139. cursor.execute(sql)
  140. if fetch_all:
  141. return cursor.fetchall()
  142. else:
  143. return cursor.fetchone()
  144. except pymysql.Error as e:
  145. logger.error(f"数据库查询失败: {e}, SQL: {sql}")
  146. raise
  147. finally:
  148. if cursor:
  149. cursor.close()
  150. # 注意:这里不需要关闭conn,连接池会自动管理
  151. def execute_update(self, sql: str, params=None, many=False):
  152. """执行更新操作"""
  153. conn = None
  154. cursor = None
  155. try:
  156. conn = self.pool.get_connection()
  157. cursor = conn.cursor()
  158. if many and params:
  159. cursor.executemany(sql, params)
  160. elif params:
  161. cursor.execute(sql, params)
  162. else:
  163. cursor.execute(sql)
  164. affected_rows = cursor.rowcount
  165. return affected_rows
  166. except pymysql.Error as e:
  167. logger.error(f"数据库更新失败: {e}, SQL: {sql}")
  168. raise
  169. finally:
  170. if cursor:
  171. cursor.close()
  172. def execute_transaction(self, operations: List[Tuple[str, tuple]]):
  173. """执行事务(一组操作要么全部成功,要么全部回滚)"""
  174. conn = None
  175. cursor = None
  176. try:
  177. conn = self.pool.get_connection()
  178. cursor = conn.cursor()
  179. # 开始事务
  180. conn.begin()
  181. for sql, params in operations:
  182. if params:
  183. cursor.execute(sql, params)
  184. else:
  185. cursor.execute(sql)
  186. # 提交事务
  187. conn.commit()
  188. return True
  189. except Exception as e:
  190. logger.error(f"事务执行失败: {e}")
  191. if conn:
  192. try:
  193. conn.rollback()
  194. except:
  195. pass
  196. return False
  197. finally:
  198. if cursor:
  199. cursor.close()
  200. def get_goods_without_attr(self, limit: int = BATCH_SIZE) -> List[Dict[str, Any]]:
  201. """
  202. 获取没有属性信息的饰品列表
  203. 从buff_goods_info表中找出在buff_goods_attr表中不存在的饰品
  204. """
  205. try:
  206. sql = """
  207. SELECT
  208. g.goods_id,
  209. g.name,
  210. g.buff_url,
  211. g.stat_time
  212. FROM buff_goods_info g
  213. LEFT JOIN buff_goods_attr a ON g.goods_id = a.goods_id
  214. WHERE a.goods_id IS NULL
  215. ORDER BY g.create_time ASC
  216. LIMIT %s
  217. """
  218. results = self.execute_query(sql, (limit,))
  219. goods_list = []
  220. for row in results:
  221. goods_list.append({
  222. 'goods_id': row['goods_id'],
  223. 'name': row['name'],
  224. 'buff_url': row['buff_url'],
  225. 'stat_time': row['stat_time']
  226. })
  227. logger.info(f"找到 {len(goods_list)} 个没有属性信息的饰品")
  228. return goods_list
  229. except Exception as e:
  230. logger.error(f"查询没有属性信息的饰品失败: {e}")
  231. return []
  232. def batch_insert_goods_attrs(self, attr_data_list: List[Dict[str, Any]]) -> int:
  233. """批量插入饰品属性信息"""
  234. if not attr_data_list:
  235. return 0
  236. # 准备批量插入操作
  237. operations = []
  238. for attr_data in attr_data_list:
  239. # 删除已存在的记录
  240. delete_sql = "DELETE FROM buff_goods_attr WHERE goods_id = %s"
  241. operations.append((delete_sql, (attr_data['goods_id'],)))
  242. # 插入新记录
  243. insert_sql = """
  244. INSERT INTO buff_goods_attr
  245. (goods_id, name, icon_url, type, type_name, category, category_name,
  246. category_group, category_group_name, rarity, rarity_name,
  247. quality, quality_name, exterior, exterior_name, weapon, weapon_name,
  248. series, series_name, custom, custom_name, itemset, itemset_name,
  249. extra_tag_ids, extra_tag_ids_name, custom_patch, custom_patch_name,
  250. custom_charm, custom_charm_name, stat_time, create_time, update_time)
  251. VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
  252. %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())
  253. """
  254. values = (
  255. attr_data.get('goods_id'),
  256. attr_data.get('name', ''),
  257. attr_data.get('icon_url'),
  258. attr_data.get('type'),
  259. attr_data.get('type_name'),
  260. attr_data.get('category'),
  261. attr_data.get('category_name'),
  262. attr_data.get('category_group'),
  263. attr_data.get('category_group_name'),
  264. attr_data.get('rarity'),
  265. attr_data.get('rarity_name'),
  266. attr_data.get('quality'),
  267. attr_data.get('quality_name'),
  268. attr_data.get('exterior'),
  269. attr_data.get('exterior_name'),
  270. attr_data.get('weapon'),
  271. attr_data.get('weapon_name'),
  272. attr_data.get('series'),
  273. attr_data.get('series_name'),
  274. attr_data.get('custom'),
  275. attr_data.get('custom_name'),
  276. attr_data.get('itemset'),
  277. attr_data.get('itemset_name'),
  278. attr_data.get('extra_tag_ids'),
  279. attr_data.get('extra_tag_ids_name'),
  280. attr_data.get('custom_patch'),
  281. attr_data.get('custom_patch_name'),
  282. attr_data.get('custom_charm'),
  283. attr_data.get('custom_charm_name'),
  284. attr_data.get('stat_time', datetime.now())
  285. )
  286. operations.append((insert_sql, values))
  287. try:
  288. # 执行事务
  289. if self.execute_transaction(operations):
  290. logger.info(f"成功批量插入 {len(attr_data_list)} 个饰品的属性信息")
  291. return len(attr_data_list)
  292. else:
  293. logger.error("批量插入饰品属性信息失败")
  294. return 0
  295. except Exception as e:
  296. logger.error(f"批量插入饰品属性信息异常: {e}")
  297. return 0
  298. def insert_goods_attr(self, attr_data: Dict[str, Any]) -> bool:
  299. """插入单个饰品属性信息"""
  300. try:
  301. # 使用事务确保删除和插入的原子性
  302. delete_sql = "DELETE FROM buff_goods_attr WHERE goods_id = %s"
  303. insert_sql = """
  304. INSERT INTO buff_goods_attr
  305. (goods_id, name, icon_url, type, type_name, category, category_name,
  306. category_group, category_group_name, rarity, rarity_name,
  307. quality, quality_name, exterior, exterior_name, weapon, weapon_name,
  308. series, series_name, custom, custom_name, itemset, itemset_name,
  309. extra_tag_ids, extra_tag_ids_name, custom_patch, custom_patch_name,
  310. custom_charm, custom_charm_name, stat_time, create_time, update_time)
  311. VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
  312. %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())
  313. """
  314. values = (
  315. attr_data.get('goods_id'),
  316. attr_data.get('name', ''),
  317. attr_data.get('icon_url'),
  318. attr_data.get('type'),
  319. attr_data.get('type_name'),
  320. attr_data.get('category'),
  321. attr_data.get('category_name'),
  322. attr_data.get('category_group'),
  323. attr_data.get('category_group_name'),
  324. attr_data.get('rarity'),
  325. attr_data.get('rarity_name'),
  326. attr_data.get('quality'),
  327. attr_data.get('quality_name'),
  328. attr_data.get('exterior'),
  329. attr_data.get('exterior_name'),
  330. attr_data.get('weapon'),
  331. attr_data.get('weapon_name'),
  332. attr_data.get('series'),
  333. attr_data.get('series_name'),
  334. attr_data.get('custom'),
  335. attr_data.get('custom_name'),
  336. attr_data.get('itemset'),
  337. attr_data.get('itemset_name'),
  338. attr_data.get('extra_tag_ids'),
  339. attr_data.get('extra_tag_ids_name'),
  340. attr_data.get('custom_patch'),
  341. attr_data.get('custom_patch_name'),
  342. attr_data.get('custom_charm'),
  343. attr_data.get('custom_charm_name'),
  344. attr_data.get('stat_time', datetime.now())
  345. )
  346. # 执行事务
  347. operations = [
  348. (delete_sql, (attr_data['goods_id'],)),
  349. (insert_sql, values)
  350. ]
  351. success = self.execute_transaction(operations)
  352. if success:
  353. logger.debug(f"成功插入饰品 {attr_data['goods_id']} 的属性信息")
  354. else:
  355. logger.error(f"插入饰品 {attr_data['goods_id']} 属性信息失败")
  356. return success
  357. except Exception as e:
  358. logger.error(f"插入饰品 {attr_data.get('goods_id', '未知')} 属性信息异常: {e}")
  359. return False
  360. def check_connection(self):
  361. """检查连接是否正常"""
  362. try:
  363. self.execute_query("SELECT 1")
  364. return True
  365. except Exception as e:
  366. logger.error(f"数据库连接检查失败: {e}")
  367. return False
  368. # ============================================================
  369. # BUFF API 操作类
  370. # ============================================================
  371. class BuffGoodsAPI:
  372. """BUFF饰品API操作类(直连请求)"""
  373. def __init__(self):
  374. self.session = requests.Session()
  375. self.session.headers.update({
  376. "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
  377. "Accept": "application/json, text/plain, */*",
  378. "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
  379. "Accept-Encoding": "gzip, deflate, br",
  380. "Connection": "keep-alive",
  381. "Referer": "https://buff.163.com/",
  382. "Sec-Fetch-Dest": "empty",
  383. "Sec-Fetch-Mode": "cors",
  384. "Sec-Fetch-Site": "same-origin"
  385. })
  386. def fetch_goods_info(self, goods_id: str) -> Optional[Dict[str, Any]]:
  387. """获取单个饰品的详细信息"""
  388. url = f"{BUFF_API_BASE_URL}?game={GAME}&goods_id={goods_id}"
  389. for retry in range(MAX_RETRIES):
  390. try:
  391. logger.debug(f"请求饰品 {goods_id} (第{retry + 1}次尝试)...")
  392. # 每 REQUEST_INTERVAL 秒请求一次,多线程下串行控制
  393. with _request_lock:
  394. elapsed = time.time() - _last_request_time[0]
  395. if elapsed < REQUEST_INTERVAL:
  396. time.sleep(REQUEST_INTERVAL - elapsed)
  397. _last_request_time[0] = time.time()
  398. # 使用 requests.get 避免多线程共享 Session,保证并发安全,直连请求
  399. response = requests.get(
  400. url,
  401. headers=self.session.headers,
  402. timeout=REQUEST_TIMEOUT,
  403. )
  404. response.raise_for_status()
  405. data = response.json()
  406. # 检查API响应状态
  407. if data.get("code") != "OK":
  408. logger.warning(f"饰品 {goods_id} API返回错误: {data.get('msg', '未知错误')}")
  409. return None
  410. # 提取饰品信息
  411. goods_info = self._extract_goods_info(data, goods_id)
  412. if goods_info:
  413. logger.info(f"成功获取饰品 {goods_id} 的属性信息")
  414. return goods_info
  415. else:
  416. logger.warning(f"饰品 {goods_id} 未找到属性信息")
  417. return None
  418. except requests.exceptions.RequestException as e:
  419. logger.error(f"请求饰品 {goods_id} 失败 (尝试 {retry + 1}/{MAX_RETRIES}): {e}")
  420. if retry < MAX_RETRIES - 1:
  421. time.sleep(RETRY_DELAY * (retry + 1))
  422. else:
  423. logger.error(f"饰品 {goods_id} 请求失败,已重试{MAX_RETRIES}次")
  424. return None
  425. except json.JSONDecodeError as e:
  426. logger.error(f"饰品 {goods_id} 响应JSON解析失败: {e}")
  427. return None
  428. except Exception as e:
  429. logger.error(f"处理饰品 {goods_id} 时发生未知错误: {e}")
  430. return None
  431. return None
  432. def _extract_goods_info(self, api_data: Dict[str, Any], goods_id: str) -> Optional[Dict[str, Any]]:
  433. """从API响应中提取饰品属性信息"""
  434. try:
  435. data = api_data.get("data", {})
  436. goods_infos = data.get("goods_infos", {})
  437. # 获取指定goods_id的饰品信息
  438. goods_data = goods_infos.get(str(goods_id))
  439. if not goods_data:
  440. logger.warning(f"饰品 {goods_id} 未在响应中找到")
  441. return None
  442. # 提取tags信息
  443. tags = goods_data.get("tags", {})
  444. # 构建属性字典
  445. attr_data = {
  446. 'goods_id': goods_id,
  447. 'name': goods_data.get("name", ""),
  448. 'icon_url': goods_data.get("icon_url"),
  449. 'stat_time': datetime.now(),
  450. 'create_time': datetime.now()
  451. }
  452. # 映射tags到对应字段
  453. tag_mapping = {
  454. 'type': ('type', 'type_name'),
  455. 'rarity': ('rarity', 'rarity_name'),
  456. 'category': ('category', 'category_name'),
  457. 'category_group': ('category_group', 'category_group_name'),
  458. 'quality': ('quality', 'quality_name'),
  459. 'exterior': ('exterior', 'exterior_name'),
  460. 'weapon': ('weapon', 'weapon_name'),
  461. 'series': ('series', 'series_name'),
  462. 'custom': ('custom', 'custom_name'),
  463. 'itemset': ('itemset', 'itemset_name')
  464. }
  465. # 处理每个tag
  466. for tag_key, (field_key, field_name_key) in tag_mapping.items():
  467. if tag_key in tags:
  468. tag_data = tags[tag_key]
  469. attr_data[field_key] = tag_data.get("internal_name")
  470. attr_data[field_name_key] = tag_data.get("localized_name")
  471. logger.debug(f"饰品 {goods_id} 属性提取成功")
  472. return attr_data
  473. except Exception as e:
  474. logger.error(f"提取饰品 {goods_id} 属性时出错: {e}")
  475. return None
  476. # ============================================================
  477. # 主逻辑
  478. # ============================================================
  479. class GoodsAttrSyncTask:
  480. """饰品属性同步任务"""
  481. def __init__(self):
  482. self.db = GoodsAttrDatabase()
  483. self.api = BuffGoodsAPI()
  484. def fetch_and_save_goods_attrs(self, goods_ids: List[str]) -> Dict[str, int]:
  485. """获取并保存饰品属性信息(每次最多 CONCURRENT_WORKERS 个并发请求)"""
  486. results = {'success': 0, 'failed': 0, 'total': len(goods_ids)}
  487. batch_attr_data: List[Dict[str, Any]] = []
  488. def fetch_one(goods_id: str) -> Optional[Dict[str, Any]]:
  489. try:
  490. return self.api.fetch_goods_info(goods_id)
  491. except Exception as e:
  492. logger.error(f"获取饰品 {goods_id} 属性异常: {e}")
  493. return None
  494. with concurrent.futures.ThreadPoolExecutor(max_workers=CONCURRENT_WORKERS) as executor:
  495. future_to_id = {executor.submit(fetch_one, gid): gid for gid in goods_ids}
  496. for future in concurrent.futures.as_completed(future_to_id):
  497. goods_id = future_to_id[future]
  498. try:
  499. attr_data = future.result()
  500. if attr_data:
  501. batch_attr_data.append(attr_data)
  502. logger.info(f"获取饰品 {goods_id} 属性成功")
  503. else:
  504. results['failed'] += 1
  505. logger.warning(f"获取饰品 {goods_id} 属性失败")
  506. except Exception as e:
  507. results['failed'] += 1
  508. logger.error(f"获取饰品 {goods_id} 异常: {e}")
  509. if batch_attr_data:
  510. saved_count = self.db.batch_insert_goods_attrs(batch_attr_data)
  511. results['success'] += saved_count
  512. if saved_count < len(batch_attr_data):
  513. results['failed'] += len(batch_attr_data) - saved_count
  514. return results
  515. def run_single_batch(self) -> bool:
  516. """执行单次批次同步任务"""
  517. start_time = datetime.now()
  518. logger.info("=" * 60)
  519. logger.info(f"开始执行饰品属性同步任务: {start_time}")
  520. logger.info("=" * 60)
  521. try:
  522. # 检查数据库连接
  523. if not self.db.check_connection():
  524. logger.error("数据库连接不可用,任务中止")
  525. return False
  526. # Step 1: 获取需要同步的饰品列表
  527. logger.info("[Step 1/3] 查询需要同步的饰品...")
  528. goods_list = self.db.get_goods_without_attr(BATCH_SIZE)
  529. if not goods_list:
  530. logger.info("没有需要同步的饰品,任务结束")
  531. return True
  532. logger.info(f"找到 {len(goods_list)} 个需要同步的饰品")
  533. # 提取goods_id列表
  534. goods_ids = [goods['goods_id'] for goods in goods_list]
  535. # Step 2: 获取并保存饰品属性信息
  536. logger.info("[Step 2/3] 获取并保存饰品属性信息...")
  537. results = self.fetch_and_save_goods_attrs(goods_ids)
  538. # Step 3: 任务完成统计
  539. end_time = datetime.now()
  540. duration = (end_time - start_time).total_seconds()
  541. logger.info("=" * 60)
  542. logger.info(f"饰品属性同步任务完成!")
  543. logger.info(f"处理饰品总数: {results['total']}")
  544. logger.info(f"成功获取并保存: {results['success']}")
  545. logger.info(f"失败: {results['failed']}")
  546. logger.info(f"耗时: {duration:.2f}秒")
  547. logger.info("=" * 60)
  548. return results['success'] > 0
  549. except Exception as e:
  550. logger.exception(f"同步任务失败: {e}")
  551. return False
  552. def run_continuous(self):
  553. """连续运行模式,每次执行后随机暂停"""
  554. execution_count = 0
  555. while True:
  556. execution_count += 1
  557. logger.info(f"\n{'='*60}")
  558. logger.info(f"开始第 {execution_count} 次执行")
  559. logger.info(f"{'='*60}")
  560. # 执行同步任务
  561. success = self.run_single_batch()
  562. if success:
  563. logger.info(f"第 {execution_count} 次执行成功")
  564. else:
  565. logger.warning(f"第 {execution_count} 次执行失败")
  566. # 直接进入下一轮,不再等待
  567. def cleanup(self):
  568. """清理资源"""
  569. try:
  570. # 关闭连接池
  571. DatabaseConnectionPool().close_pool()
  572. logger.info("资源清理完成")
  573. except:
  574. pass
  575. # ============================================================
  576. # 主函数
  577. # ============================================================
  578. def main():
  579. """主函数"""
  580. task = None
  581. try:
  582. logger.info("启动饰品属性同步脚本(连接池版本)...")
  583. logger.info(f"配置信息: 每次处理 {BATCH_SIZE} 个饰品, 串行请求每 {REQUEST_INTERVAL} 秒一次, 直连请求")
  584. # 创建任务实例
  585. task = GoodsAttrSyncTask()
  586. # 运行连续模式
  587. task.run_continuous()
  588. except KeyboardInterrupt:
  589. logger.info("\n收到中断信号,正在退出...")
  590. if task:
  591. task.cleanup()
  592. sys.exit(0)
  593. except Exception as e:
  594. logger.exception(f"程序运行异常: {e}")
  595. if task:
  596. task.cleanup()
  597. sys.exit(1)
  598. if __name__ == "__main__":
  599. # 安装依赖提示
  600. try:
  601. import dbutils
  602. except ImportError:
  603. print("请先安装依赖: pip install DBUtils==3.0.2")
  604. sys.exit(1)
  605. main()