count_selection.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 导出选品数据到Excel的脚本
  5. 条件:在售数量>=100,利润率10%,智能选品助手:低
  6. """
  7. import os
  8. import sys
  9. import configparser
  10. from pathlib import Path
  11. from decimal import Decimal
  12. from typing import Dict, List, Any, Optional, Tuple
  13. import pymysql
  14. from pymysql.cursors import DictCursor
  15. import openpyxl
  16. from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
  17. def load_config():
  18. """从 config.ini 加载配置"""
  19. conf_path = Path(__file__).resolve().parent / "config.ini"
  20. if not conf_path.exists():
  21. print(f"错误: 未找到配置文件 {conf_path}", file=sys.stderr)
  22. sys.exit(1)
  23. config = configparser.ConfigParser()
  24. config.read(conf_path, encoding="utf-8")
  25. return config
  26. def get_db_connection(config):
  27. """创建数据库连接"""
  28. return pymysql.connect(
  29. host=config.get("database", "host"),
  30. port=config.getint("database", "port"),
  31. user=config.get("database", "user"),
  32. password=config.get("database", "password"),
  33. database=config.get("database", "database"),
  34. charset=config.get("database", "charset"),
  35. cursorclass=DictCursor,
  36. )
  37. def get_candidate_ids(conn, config: dict) -> list:
  38. """获取候选饰品ID列表"""
  39. conditions = ["1=1"]
  40. params = []
  41. pmin = config.get("buffPriceMin")
  42. pmax = config.get("buffPriceMax")
  43. if pmin is not None and pmax is not None:
  44. conditions.append("`当前在售最低价格` >= %s AND `当前在售最低价格` <= %s")
  45. params.extend([float(pmin), float(pmax)])
  46. elif pmin is not None:
  47. conditions.append("`当前在售最低价格` >= %s")
  48. params.append(float(pmin))
  49. elif pmax is not None:
  50. conditions.append("`当前在售最低价格` <= %s")
  51. params.append(float(pmax))
  52. smin = config.get("buffSalesMin")
  53. smax = config.get("buffSalesMax")
  54. if smin is not None and smax is not None:
  55. conditions.append("`在售数量` >= %s AND `在售数量` <= %s")
  56. params.extend([int(smin), int(smax)])
  57. elif smin is not None:
  58. conditions.append("`在售数量` >= %s")
  59. params.append(int(smin))
  60. elif smax is not None:
  61. conditions.append("`在售数量` <= %s")
  62. params.append(int(smax))
  63. exclude_ids = config.get("excludeIds", [])
  64. if exclude_ids:
  65. placeholders = ", ".join(["%s"] * len(exclude_ids))
  66. conditions.append(f"`BUFF饰品ID` NOT IN ({placeholders})")
  67. params.extend([str(x) for x in exclude_ids])
  68. where_sql = " AND ".join(conditions)
  69. sql = f"SELECT DISTINCT `BUFF饰品ID` FROM buff_goods_filtered_view WHERE {where_sql}"
  70. with conn.cursor() as cursor:
  71. cursor.execute(sql, params)
  72. rows = cursor.fetchall()
  73. return [str(r["BUFF饰品ID"]) for r in rows if r["BUFF饰品ID"]]
  74. def get_goods_data(conn, goods_id: str) -> Optional[Dict[str, Any]]:
  75. """获取饰品完整数据"""
  76. sql = """
  77. SELECT `当前在售最低价格`, `7天在售最低价格平均价`, `30天在售最低价格平均价`
  78. FROM buff_goods_filtered_view WHERE `BUFF饰品ID` = %s LIMIT 1
  79. """
  80. with conn.cursor() as cursor:
  81. cursor.execute(sql, (goods_id,))
  82. row = cursor.fetchone()
  83. if not row or row["当前在售最低价格"] is None:
  84. return None
  85. return {
  86. "lowest": Decimal(str(row["当前在售最低价格"])),
  87. "avg7": Decimal(str(row["7天在售最低价格平均价"])) if row["7天在售最低价格平均价"] is not None else None,
  88. "avg30": Decimal(str(row["30天在售最低价格平均价"])) if row["30天在售最低价格平均价"] is not None else None,
  89. }
  90. def get_wear_ranges(conn, goods_id: str) -> List[Tuple[Decimal, Decimal, Decimal]]:
  91. """获取磨损区间列表:(最小磨损值, 最大磨损值, 区间底价)"""
  92. sql = """
  93. SELECT `最小磨损值`, `最大磨损值`, `区间底价`
  94. FROM buff_goods_filtered_view
  95. WHERE `BUFF饰品ID` = %s
  96. ORDER BY `最小磨损值` ASC
  97. """
  98. with conn.cursor() as cursor:
  99. cursor.execute(sql, (goods_id,))
  100. rows = cursor.fetchall()
  101. out = []
  102. for r in rows:
  103. if r["最小磨损值"] is not None and r["最大磨损值"] is not None and r["区间底价"] is not None:
  104. out.append((
  105. Decimal(str(r["最小磨损值"])),
  106. Decimal(str(r["最大磨损值"])),
  107. Decimal(str(r["区间底价"]))
  108. ))
  109. return out
  110. def normal_ranges(ranges: List[Tuple]) -> List[Tuple]:
  111. """正常磨损区间:区间底价随最小磨损值递增而单调不升"""
  112. if not ranges:
  113. return []
  114. out = [ranges[0]]
  115. for i in range(1, len(ranges)):
  116. if ranges[i][2] <= out[-1][2]:
  117. out.append(ranges[i])
  118. else:
  119. break
  120. return out
  121. def qualified_ranges(normal: List[Tuple], compare_price: Decimal) -> List[Tuple]:
  122. """满足利润的区间:区间底价 > 比较价,一旦不满足则截断"""
  123. out = []
  124. for r in normal:
  125. if r[2] > compare_price:
  126. out.append(r)
  127. else:
  128. break
  129. return out
  130. def check_profit(conn, goods_id: str, profit_rate: float) -> Tuple[bool, Optional[Dict]]:
  131. """
  132. 检查饰品是否符合利润条件
  133. 返回: (是否通过, 利润信息)
  134. """
  135. goods_data = get_goods_data(conn, goods_id)
  136. if not goods_data:
  137. return False, None
  138. lowest = goods_data["lowest"]
  139. if lowest <= 0:
  140. return False, None
  141. compare_price = lowest * (Decimal("1") + Decimal(str(profit_rate)))
  142. ranges = get_wear_ranges(conn, goods_id)
  143. normal = normal_ranges(ranges)
  144. qualified = qualified_ranges(normal, compare_price)
  145. if not qualified:
  146. return False, None
  147. # 获取第一个符合条件区间的信息
  148. first_range = qualified[0]
  149. min_wear = float(first_range[0])
  150. max_wear = float(first_range[1])
  151. range_base_price = float(first_range[2])
  152. return True, {
  153. "lowest_price": float(lowest),
  154. "compare_price": float(compare_price),
  155. "profit_margin": float(compare_price) - float(lowest),
  156. "min_wear": min_wear,
  157. "max_wear": max_wear,
  158. "range_base_price": range_base_price,
  159. }
  160. def get_comprehensive_data(conn, goods_id: str) -> Optional[Dict]:
  161. """从 buff_goods_comprehensive_view 获取完整数据"""
  162. sql = f"SELECT * FROM buff_goods_comprehensive_view WHERE `BUFF饰品ID` = %s LIMIT 1"
  163. with conn.cursor() as cursor:
  164. cursor.execute(sql, (goods_id,))
  165. return cursor.fetchone()
  166. def export_to_excel(data: List[Dict], output_path: str):
  167. """将数据导出到Excel"""
  168. wb = openpyxl.Workbook()
  169. ws = wb.active
  170. ws.title = "选品数据"
  171. # 样式定义
  172. header_font = Font(bold=True, size=11)
  173. header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
  174. header_font_white = Font(bold=True, size=11, color="FFFFFF")
  175. thin_border = Border(
  176. left=Side(style='thin'),
  177. right=Side(style='thin'),
  178. top=Side(style='thin'),
  179. bottom=Side(style='thin')
  180. )
  181. # 设置列宽(根据字段名)
  182. column_widths = {
  183. "BUFF饰品ID": 15,
  184. "饰品名称": 30,
  185. "武器类型": 12,
  186. "品质": 10,
  187. "外观": 20,
  188. "当前在售最低价格": 18,
  189. "在售数量": 12,
  190. "7天在售最低价格平均价": 22,
  191. "30天在售最低价格平均价": 22,
  192. "磨损区间底价": 15,
  193. "参考利润": 15,
  194. "利润率": 12,
  195. }
  196. # 写入表头
  197. headers = [
  198. "BUFF饰品ID", "饰品名称", "武器类型", "品质", "外观",
  199. "当前在售最低价格", "在售数量", "7天在售最低价格平均价", "30天在售最低价格平均价",
  200. "磨损区间底价", "参考利润", "利润率"
  201. ]
  202. for col, header in enumerate(headers, start=1):
  203. cell = ws.cell(row=1, column=col, value=header)
  204. cell.font = header_font_white
  205. cell.fill = header_fill
  206. cell.alignment = Alignment(horizontal='center', vertical='center')
  207. cell.border = thin_border
  208. # 设置列宽
  209. if header in column_widths:
  210. ws.column_dimensions[openpyxl.utils.get_column_letter(col)].width = column_widths[header]
  211. # 写入数据
  212. for row_idx, item in enumerate(data, start=2):
  213. ws.cell(row=row_idx, column=1, value=item.get("BUFF饰品ID")).border = thin_border
  214. ws.cell(row=row_idx, column=2, value=item.get("饰品名称")).border = thin_border
  215. ws.cell(row=row_idx, column=3, value=item.get("武器类型")).border = thin_border
  216. ws.cell(row=row_idx, column=4, value=item.get("品质")).border = thin_border
  217. ws.cell(row=row_idx, column=5, value=item.get("外观")).border = thin_border
  218. ws.cell(row=row_idx, column=6, value=item.get("当前在售最低价格")).border = thin_border
  219. ws.cell(row=row_idx, column=7, value=item.get("在售数量")).border = thin_border
  220. ws.cell(row=row_idx, column=8, value=item.get("7天在售最低价格平均价")).border = thin_border
  221. ws.cell(row=row_idx, column=9, value=item.get("30天在售最低价格平均价")).border = thin_border
  222. ws.cell(row=row_idx, column=10, value=item.get("磨损区间底价")).border = thin_border
  223. ws.cell(row=row_idx, column=11, value=item.get("参考利润")).border = thin_border
  224. ws.cell(row=row_idx, column=12, value=item.get("利润率")).border = thin_border
  225. wb.save(output_path)
  226. def main():
  227. # 筛选条件
  228. config = {
  229. "buffPriceMin": 5,
  230. "buffPriceMax": 200,
  231. "buffSalesMin": 100,
  232. "buffSalesMax": None,
  233. "profitRate": 10,
  234. "excludeIds": [],
  235. }
  236. print("开始导出选品数据...")
  237. print(f"筛选条件:")
  238. print(f" - BUFF价格区间: {config['buffPriceMin']} - {config['buffPriceMax']} 元")
  239. print(f" - 在售数量: >={config['buffSalesMin']}")
  240. print(f" - 利润率: {config['profitRate']}%")
  241. print(f" - 智能选品: 低")
  242. print("=" * 60)
  243. db_config = load_config()
  244. conn = get_db_connection(db_config)
  245. try:
  246. # 获取候选饰品
  247. print("\n[1/4] 获取候选饰品...")
  248. candidate_ids = get_candidate_ids(conn, config)
  249. print(f"候选饰品数量: {len(candidate_ids)}")
  250. # 筛选符合利润条件的饰品
  251. print("\n[2/4] 筛选符合利润条件的饰品...")
  252. profit_rate = config["profitRate"] / 100.0
  253. qualified_ids = []
  254. profit_info_map = {}
  255. for i, goods_id in enumerate(candidate_ids, start=1):
  256. if i % 200 == 0:
  257. print(f" 已处理 {i}/{len(candidate_ids)}...")
  258. passed, info = check_profit(conn, goods_id, profit_rate)
  259. if passed:
  260. qualified_ids.append(goods_id)
  261. profit_info_map[goods_id] = info
  262. print(f"符合利润条件的饰品: {len(qualified_ids)}")
  263. # 获取完整数据
  264. print("\n[3/4] 获取完整数据...")
  265. export_data = []
  266. for i, goods_id in enumerate(qualified_ids, start=1):
  267. if i % 200 == 0:
  268. print(f" 已处理 {i}/{len(qualified_ids)}...")
  269. row = get_comprehensive_data(conn, goods_id)
  270. if row:
  271. profit_info = profit_info_map.get(goods_id, {})
  272. row_data = {
  273. "BUFF饰品ID": row.get("BUFF饰品ID"),
  274. "饰品名称": row.get("饰品名称"),
  275. "武器类型": row.get("武器类型"),
  276. "品质": row.get("品质"),
  277. "外观": row.get("外观"),
  278. "当前在售最低价格": row.get("当前在售最低价格"),
  279. "在售数量": row.get("在售数量"),
  280. "7天在售最低价格平均价": row.get("7天在售最低价格平均价"),
  281. "30天在售最低价格平均价": row.get("30天在售最低价格平均价"),
  282. "磨损区间底价": profit_info.get("range_base_price"),
  283. "参考利润": profit_info.get("profit_margin"),
  284. "利润率": config["profitRate"],
  285. }
  286. export_data.append(row_data)
  287. # 导出到Excel
  288. print("\n[4/4] 导出到Excel...")
  289. script_dir = Path(__file__).resolve().parent
  290. output_path = script_dir / f"选品数据_{config['buffSalesMin']}_{config['profitRate']}pct.xlsx"
  291. export_to_excel(export_data, str(output_path))
  292. print(f"\n导出完成!")
  293. print(f"文件路径: {output_path}")
  294. print(f"导出数据: {len(export_data)} 条")
  295. finally:
  296. conn.close()
  297. if __name__ == "__main__":
  298. main()