profit_calculator.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. # -*- coding: utf-8 -*-
  2. """
  3. 扫货利润计算工具
  4. 用于计算扫货成功数据的利润和利润率
  5. """
  6. import tkinter as tk
  7. from tkinter import ttk, filedialog, messagebox
  8. import pandas as pd
  9. import pymysql
  10. import os
  11. from datetime import datetime
  12. import sys
  13. # 判断是否为打包环境
  14. if getattr(sys, 'frozen', False):
  15. APPLICATION_PATH = sys._MEIPASS
  16. else:
  17. APPLICATION_PATH = os.path.dirname(os.path.abspath(__file__))
  18. class ProfitCalculatorApp:
  19. def __init__(self, root):
  20. self.root = root
  21. self.root.title("扫货利润计算工具 v1.0")
  22. self.root.geometry("900x650")
  23. self.root.resizable(True, True)
  24. # 程序图标 (打包后从资源目录读取)
  25. try:
  26. if getattr(sys, 'frozen', False):
  27. icon_path = os.path.join(APPLICATION_PATH, 'icon.ico')
  28. if os.path.exists(icon_path):
  29. root.iconbitmap(icon_path)
  30. except Exception:
  31. pass
  32. # 数据库配置 - 从环境变量读取
  33. self.db_config = {
  34. 'host': os.getenv('DB_HOST', '125.122.32.114'),
  35. 'port': int(os.getenv('DB_PORT', 3306)),
  36. 'user': os.getenv('DB_USER', 'root'),
  37. 'password': os.getenv('DB_PASSWORD', 'Buff123456'),
  38. 'database': os.getenv('DB_NAME', 'buff'),
  39. 'charset': 'utf8mb4'
  40. }
  41. # 价格缓存 - 启动时加载
  42. self.price_cache = {}
  43. # 当前处理的结果数据
  44. self.current_results = []
  45. # 上次输出路径
  46. self.last_output_path = None
  47. self.setup_ui()
  48. self.load_price_data()
  49. def setup_ui(self):
  50. """设置UI界面"""
  51. # 主框架
  52. main_frame = ttk.Frame(self.root, padding="15")
  53. main_frame.pack(fill=tk.BOTH, expand=True)
  54. # 顶部区域 - 标题和状态
  55. top_frame = ttk.Frame(main_frame)
  56. top_frame.pack(fill=tk.X, pady=(0, 10))
  57. # 标题
  58. title_label = ttk.Label(top_frame, text="扫货利润计算工具",
  59. font=("微软雅黑", 20, "bold"))
  60. title_label.pack(side=tk.LEFT, pady=10)
  61. # 状态区域
  62. status_frame = ttk.Frame(top_frame)
  63. status_frame.pack(side=tk.RIGHT)
  64. # 数据库状态
  65. self.db_status_label = ttk.Label(status_frame, text="数据库: 连接中...",
  66. font=("微软雅黑", 9))
  67. self.db_status_label.pack(anchor=tk.E)
  68. # 价格数据加载状态
  69. self.cache_status_label = ttk.Label(status_frame, text="价格缓存: 加载中...",
  70. font=("微软雅黑", 9))
  71. self.cache_status_label.pack(anchor=tk.E)
  72. # 按钮区域
  73. btn_frame = ttk.Frame(main_frame)
  74. btn_frame.pack(fill=tk.X, pady=15)
  75. # 左侧按钮
  76. left_btn_frame = ttk.Frame(btn_frame)
  77. left_btn_frame.pack(side=tk.LEFT)
  78. self.upload_btn = ttk.Button(left_btn_frame, text="📂 选择Excel文件",
  79. command=self.upload_excel, width=18)
  80. self.upload_btn.pack(side=tk.LEFT, padx=5)
  81. self.reload_btn = ttk.Button(left_btn_frame, text="🔄 重新加载数据",
  82. command=self.reload_data, width=18)
  83. self.reload_btn.pack(side=tk.LEFT, padx=5)
  84. # 右侧导出按钮
  85. right_btn_frame = ttk.Frame(btn_frame)
  86. right_btn_frame.pack(side=tk.RIGHT)
  87. self.export_btn = ttk.Button(right_btn_frame, text="💾 导出Excel",
  88. command=self.export_excel, width=18)
  89. self.export_btn.pack(side=tk.LEFT, padx=5)
  90. self.export_btn.config(state=tk.DISABLED)
  91. self.export_as_btn = ttk.Button(right_btn_frame, text="📤 另存为...",
  92. command=self.export_as_excel, width=18)
  93. self.export_as_btn.pack(side=tk.LEFT, padx=5)
  94. self.export_as_btn.config(state=tk.DISABLED)
  95. # 文件路径显示
  96. self.file_path_var = tk.StringVar(value="未选择文件")
  97. self.file_path_label = ttk.Label(main_frame, textvariable=self.file_path_var,
  98. font=("微软雅黑", 9), foreground="gray")
  99. self.file_path_label.pack(pady=5)
  100. # 处理结果区域
  101. result_frame = ttk.LabelFrame(main_frame, text="处理结果", padding="10")
  102. result_frame.pack(fill=tk.BOTH, expand=True, pady=10)
  103. # 结果文本框
  104. self.result_text = tk.Text(result_frame, height=18, width=100,
  105. font=("微软雅黑", 10))
  106. self.result_text.pack(fill=tk.BOTH, expand=True, side=tk.LEFT)
  107. # 滚动条
  108. scrollbar = ttk.Scrollbar(result_frame, command=self.result_text.yview)
  109. scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
  110. self.result_text.config(yscrollcommand=scrollbar.set)
  111. # 底部说明
  112. info_frame = ttk.Frame(main_frame)
  113. info_frame.pack(fill=tk.X, pady=5)
  114. info_label = ttk.Label(info_frame,
  115. text="Excel模版:序号 | 饰品ID | 饰品名称 | 已购磨损值 | 目标价格(美元) | 成交价格(美元) | 成交价格(人民币) | 购买账号 | 扫货时间",
  116. font=("微软雅黑", 8), foreground="gray")
  117. info_label.pack(side=tk.LEFT)
  118. # 统计标签
  119. self.stats_label = ttk.Label(info_frame, text="", font=("微软雅黑", 9, "bold"))
  120. self.stats_label.pack(side=tk.RIGHT)
  121. def reload_data(self):
  122. """重新加载数据"""
  123. self.result_text.delete(1.0, tk.END)
  124. self.result_text.insert(tk.END, "正在重新加载数据...\n")
  125. self.root.update()
  126. self.load_price_data()
  127. self.result_text.insert(tk.END, "数据重新加载完成\n")
  128. def load_price_data(self):
  129. """从数据库加载价格数据到缓存"""
  130. try:
  131. conn = pymysql.connect(**self.db_config)
  132. cursor = conn.cursor(pymysql.cursors.DictCursor)
  133. sql = """
  134. SELECT goods_id, min_float, max_float, qj_min_price
  135. FROM buff_goods_float_price
  136. """
  137. cursor.execute(sql)
  138. rows = cursor.fetchall()
  139. self.price_cache = {}
  140. for row in rows:
  141. goods_id = str(row['goods_id'])
  142. min_float = float(row['min_float'])
  143. max_float = float(row['max_float'])
  144. qj_min_price = float(row['qj_min_price'])
  145. if goods_id not in self.price_cache:
  146. self.price_cache[goods_id] = []
  147. self.price_cache[goods_id].append({
  148. 'min_float': min_float,
  149. 'max_float': max_float,
  150. 'qj_min_price': qj_min_price
  151. })
  152. cursor.close()
  153. conn.close()
  154. self.db_status_label.config(text="数据库: ✅ 已连接", foreground="green")
  155. self.cache_status_label.config(
  156. text=f"价格缓存: ✅ {len(self.price_cache)} 个饰品",
  157. foreground="green"
  158. )
  159. except Exception as e:
  160. self.db_status_label.config(text="数据库: ❌ 连接失败", foreground="red")
  161. self.cache_status_label.config(text=f"价格缓存: ❌ - {str(e)[:30]}",
  162. foreground="red")
  163. def find_price_in_range(self, goods_id, wear_value):
  164. """根据饰品ID和磨损值查找对应区间的底价"""
  165. goods_id_str = str(goods_id)
  166. if goods_id_str not in self.price_cache:
  167. return None
  168. ranges = self.price_cache[goods_id_str]
  169. for r in ranges:
  170. if r['min_float'] <= wear_value <= r['max_float']:
  171. return r['qj_min_price']
  172. return None
  173. def upload_excel(self):
  174. """处理上传的Excel文件"""
  175. file_path = filedialog.askopenfilename(
  176. title="选择Excel文件",
  177. filetypes=[("Excel文件", "*.xlsx *.xls"), ("所有文件", "*.*")]
  178. )
  179. if not file_path:
  180. return
  181. self.file_path_var.set(file_path)
  182. self.result_text.delete(1.0, tk.END)
  183. self.result_text.insert(tk.END, f"正在处理: {file_path}\n")
  184. self.result_text.insert(tk.END, "=" * 70 + "\n\n")
  185. self.root.update()
  186. try:
  187. df = pd.read_excel(file_path)
  188. if len(df.columns) < 7:
  189. messagebox.showerror("错误", "Excel文件列数不足,需要至少7列数据")
  190. return
  191. self.current_results = []
  192. found_count = 0
  193. not_found_count = 0
  194. empty_price_count = 0
  195. total_profit = 0
  196. for idx, row in df.iterrows():
  197. try:
  198. goods_id = str(row.iloc[1])
  199. wear_value = float(row.iloc[3])
  200. deal_price_cny_raw = row.iloc[6]
  201. # 检查成交价格是否为空
  202. if pd.isna(deal_price_cny_raw):
  203. name_display = str(row.iloc[2])[:25]
  204. if len(str(row.iloc[2])) > 25:
  205. name_display += "..."
  206. self.result_text.insert(tk.END,
  207. f"⚠️ {row.iloc[0]:>3} | {name_display:<28} | 成交价为空\n")
  208. empty_price_count += 1
  209. continue
  210. deal_price_cny = float(deal_price_cny_raw)
  211. range_min_price = self.find_price_in_range(goods_id, wear_value)
  212. if range_min_price is not None:
  213. profit = range_min_price - deal_price_cny
  214. profit_rate = (profit / deal_price_cny * 100) if deal_price_cny > 0 else 0
  215. total_profit += profit
  216. self.current_results.append({
  217. '序号': row.iloc[0],
  218. '饰品ID': goods_id,
  219. '饰品名称': row.iloc[2],
  220. '已购磨损值': wear_value,
  221. '目标价格美元': row.iloc[4],
  222. '成交价格美元': row.iloc[5],
  223. '成交价格人民币': deal_price_cny,
  224. '购买账号': row.iloc[7] if len(df.columns) > 7 else '',
  225. '扫货时间': row.iloc[8] if len(df.columns) > 8 else '',
  226. '区间底价': range_min_price,
  227. '利润': round(profit, 2),
  228. '利润率(%)': round(profit_rate, 2)
  229. })
  230. found_count += 1
  231. name_display = str(row.iloc[2])[:25] + "..." if len(str(row.iloc[2])) > 25 else row.iloc[2]
  232. self.result_text.insert(tk.END,
  233. f"✅ {row.iloc[0]:>3} | {name_display:<28} | 利润: ¥{round(profit, 2):>8} ({round(profit_rate, 1):>5}%)\n")
  234. else:
  235. not_found_count += 1
  236. name_display = str(row.iloc[2])[:25] + "..." if len(str(row.iloc[2])) > 25 else row.iloc[2]
  237. self.result_text.insert(tk.END,
  238. f"⚠️ {row.iloc[0]:>3} | {name_display:<28} | 未匹配到区间\n")
  239. except Exception as e:
  240. self.result_text.insert(tk.END,
  241. f"❌ 序号 {row.iloc[0]}: {str(e)[:50]}\n")
  242. if self.current_results:
  243. # 生成输出文件名
  244. timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
  245. base_name = os.path.splitext(os.path.basename(file_path))[0]
  246. self.last_output_path = os.path.join(
  247. os.path.dirname(file_path),
  248. f"{base_name}_利润分析_{timestamp}.xlsx"
  249. )
  250. result_df = pd.DataFrame(self.current_results)
  251. result_df.to_excel(self.last_output_path, index=False)
  252. self.result_text.insert(tk.END, "\n" + "=" * 70 + "\n")
  253. self.result_text.insert(tk.END, f"✅ 处理完成!\n")
  254. self.result_text.insert(tk.END, f" 成功匹配: {found_count} 条\n")
  255. self.result_text.insert(tk.END, f" 成交价为空: {empty_price_count} 条\n")
  256. self.result_text.insert(tk.END, f" 未匹配区间: {not_found_count} 条\n")
  257. self.result_text.insert(tk.END, f" 总利润: ¥{round(total_profit, 2)}\n")
  258. self.result_text.insert(tk.END, f" 结果已保存: {self.last_output_path}\n")
  259. # 启用导出按钮
  260. self.export_btn.config(state=tk.NORMAL)
  261. self.export_as_btn.config(state=tk.NORMAL)
  262. # 更新统计
  263. stats_text = f"成功: {found_count} | 成交价空: {empty_price_count} | 未匹配: {not_found_count} | 利润: ¥{round(total_profit, 2)}"
  264. self.stats_label.config(text=stats_text, foreground="green")
  265. messagebox.showinfo("处理完成",
  266. f"成功匹配: {found_count} 条\n成交价为空: {empty_price_count} 条\n未匹配区间: {not_found_count} 条\n总利润: ¥{round(total_profit, 2)}\n\n结果已保存")
  267. else:
  268. self.stats_label.config(text="无匹配数据", foreground="orange")
  269. messagebox.showwarning("无匹配数据", "没有找到任何匹配的饰品数据")
  270. except Exception as e:
  271. messagebox.showerror("处理错误", f"处理文件时出错:\n{str(e)}")
  272. self.result_text.insert(tk.END, f"\n❌ 错误: {str(e)}")
  273. def export_excel(self):
  274. """导出上次的结果到Excel"""
  275. if not self.current_results:
  276. messagebox.showwarning("无数据", "没有可导出的数据,请先选择Excel文件处理")
  277. return
  278. if self.last_output_path and os.path.exists(self.last_output_path):
  279. result_df = pd.DataFrame(self.current_results)
  280. result_df.to_excel(self.last_output_path, index=False)
  281. messagebox.showinfo("导出成功", f"已导出到:\n{self.last_output_path}")
  282. else:
  283. self.export_as_excel()
  284. def export_as_excel(self):
  285. """另存为Excel"""
  286. if not self.current_results:
  287. messagebox.showwarning("无数据", "没有可导出的数据,请先选择Excel文件处理")
  288. return
  289. file_path = filedialog.asksaveasfilename(
  290. title="另存为",
  291. defaultextension=".xlsx",
  292. filetypes=[("Excel文件", "*.xlsx"), ("所有文件", "*.*")],
  293. initialfile=f"利润分析_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
  294. )
  295. if not file_path:
  296. return
  297. try:
  298. result_df = pd.DataFrame(self.current_results)
  299. result_df.to_excel(file_path, index=False)
  300. self.last_output_path = file_path
  301. messagebox.showinfo("导出成功", f"已导出到:\n{file_path}")
  302. except Exception as e:
  303. messagebox.showerror("导出失败", f"导出时出错:\n{str(e)}")
  304. def main():
  305. root = tk.Tk()
  306. app = ProfitCalculatorApp(root)
  307. root.mainloop()
  308. if __name__ == "__main__":
  309. main()