飞书提交数据.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. import tkinter as tk
  2. from tkinter import ttk, filedialog, messagebox, scrolledtext
  3. import pandas as pd
  4. import requests
  5. import json
  6. import threading
  7. import time
  8. import random
  9. import re
  10. from datetime import datetime
  11. class FeishuSubmitApp:
  12. def __init__(self, root):
  13. self.root = root
  14. self.root.title("飞书表格数据提交工具")
  15. self.root.geometry("1100x800")
  16. # 状态变量
  17. self.is_running = False
  18. self.stop_flag = False
  19. self.excel_file = ""
  20. self.cookie_string = ""
  21. # 从curl提取的默认配置
  22. self.default_headers = {
  23. "accept": "application/json, text/plain, */*",
  24. "accept-language": "zh-CN,zh;q=0.9",
  25. "content-type": "application/json",
  26. "f-version": "docs-6-9-1780996889854",
  27. "origin": "https://xhd.feishu.cn",
  28. "priority": "u=1, i",
  29. "referer": "https://xhd.feishu.cn/share/base/form/shrcnJqFUFF0sqlmrfsWpDNf8df",
  30. "request-id": "HgNmkEtK9iNf-7646228072081099729",
  31. "sec-ch-ua": '"Chromium";v="148", "Google Chrome";v="148", "Not/A)Brand";v="99"',
  32. "sec-ch-ua-mobile": "?0",
  33. "sec-ch-ua-platform": '"Windows"',
  34. "sec-fetch-dest": "empty",
  35. "sec-fetch-mode": "cors",
  36. "sec-fetch-site": "same-origin",
  37. "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36",
  38. "x-csrftoken": "83aae12269bced10680d3ccf817f82262305e7d0-1781579108",
  39. "x-request-id": "HgNmkEtK9iNf-7646228072081099729",
  40. "x-tt-logid": "02178175254676300000000000000000000ffffe33263555f5be8",
  41. "x-tt-trace": "1",
  42. "x-tt-trace-id": "HgNmkEtK9iNf-7646228072081099729"
  43. }
  44. self.url = "https://xhd.feishu.cn/space/api/bitable/share/content"
  45. self.share_token = "shrcnJqFUFF0sqlmrfsWpDNf8df"
  46. self.setup_ui()
  47. self.load_config()
  48. def setup_ui(self):
  49. """构建界面"""
  50. # 主容器
  51. main_frame = ttk.Frame(self.root, padding="10")
  52. main_frame.pack(fill=tk.BOTH, expand=True)
  53. # ========== 第一行:文件选择 ==========
  54. file_frame = ttk.LabelFrame(main_frame, text="📁 数据文件", padding="10")
  55. file_frame.pack(fill=tk.X, pady=(0, 10))
  56. ttk.Label(file_frame, text="Excel文件:").grid(row=0, column=0, sticky=tk.W)
  57. self.file_path_var = tk.StringVar()
  58. ttk.Entry(file_frame, textvariable=self.file_path_var, width=60).grid(row=0, column=1, padx=(10, 10))
  59. ttk.Button(file_frame, text="浏览...", command=self.select_file).grid(row=0, column=2)
  60. ttk.Label(file_frame, text="工作表:").grid(row=1, column=0, sticky=tk.W, pady=(10, 0))
  61. self.sheet_var = tk.StringVar(value="Sheet1")
  62. ttk.Entry(file_frame, textvariable=self.sheet_var, width=20).grid(row=1, column=1, sticky=tk.W, padx=(10, 10), pady=(10, 0))
  63. ttk.Label(file_frame, text="(默认: Sheet1)").grid(row=1, column=2, sticky=tk.W, pady=(10, 0))
  64. # ========== 第二行:Cookie配置 ==========
  65. cookie_frame = ttk.LabelFrame(main_frame, text="🍪 Cookie 配置", padding="10")
  66. cookie_frame.pack(fill=tk.X, pady=(0, 10))
  67. ttk.Label(cookie_frame, text="Cookie:").grid(row=0, column=0, sticky=tk.W)
  68. self.cookie_text = tk.Text(cookie_frame, height=4, width=80, wrap=tk.WORD)
  69. self.cookie_text.grid(row=0, column=1, padx=(10, 10), pady=(0, 5), sticky=tk.W)
  70. cookie_btn_frame = ttk.Frame(cookie_frame)
  71. cookie_btn_frame.grid(row=1, column=1, sticky=tk.W, padx=(10, 0))
  72. ttk.Button(cookie_btn_frame, text="从剪贴板粘贴", command=self.paste_cookie).pack(side=tk.LEFT, padx=(0, 10))
  73. ttk.Button(cookie_btn_frame, text="保存配置", command=self.save_config).pack(side=tk.LEFT, padx=(0, 10))
  74. ttk.Button(cookie_btn_frame, text="加载默认", command=self.load_default_cookie).pack(side=tk.LEFT)
  75. # ========== 第三行:发送控制 ==========
  76. control_frame = ttk.LabelFrame(main_frame, text="⚙️ 发送控制", padding="10")
  77. control_frame.pack(fill=tk.X, pady=(0, 10))
  78. # 间隔配置
  79. ttk.Label(control_frame, text="最小间隔(秒):").grid(row=0, column=0, sticky=tk.W)
  80. self.min_interval_var = tk.StringVar(value="2")
  81. ttk.Entry(control_frame, textvariable=self.min_interval_var, width=10).grid(row=0, column=1, padx=(5, 15), sticky=tk.W)
  82. ttk.Label(control_frame, text="最大间隔(秒):").grid(row=0, column=2, sticky=tk.W)
  83. self.max_interval_var = tk.StringVar(value="5")
  84. ttk.Entry(control_frame, textvariable=self.max_interval_var, width=10).grid(row=0, column=3, padx=(5, 15), sticky=tk.W)
  85. ttk.Label(control_frame, text="起始行:").grid(row=0, column=4, sticky=tk.W)
  86. self.start_row_var = tk.StringVar(value="2")
  87. ttk.Entry(control_frame, textvariable=self.start_row_var, width=8).grid(row=0, column=5, padx=(5, 15), sticky=tk.W)
  88. ttk.Label(control_frame, text="(表头在第1行)").grid(row=0, column=6, sticky=tk.W)
  89. # 操作按钮
  90. btn_frame = ttk.Frame(control_frame)
  91. btn_frame.grid(row=1, column=0, columnspan=7, pady=(15, 0), sticky=tk.W)
  92. self.start_btn = ttk.Button(btn_frame, text="🚀 开始发送", command=self.start_send)
  93. self.start_btn.pack(side=tk.LEFT, padx=(0, 10))
  94. self.stop_btn = ttk.Button(btn_frame, text="⏹ 停止", command=self.stop_send, state=tk.DISABLED)
  95. self.stop_btn.pack(side=tk.LEFT, padx=(0, 10))
  96. ttk.Button(btn_frame, text="清空日志", command=self.clear_log).pack(side=tk.LEFT)
  97. # 状态显示
  98. self.status_var = tk.StringVar(value="就绪")
  99. ttk.Label(control_frame, textvariable=self.status_var, foreground="blue").grid(row=2, column=0, columnspan=7, pady=(10, 0), sticky=tk.W)
  100. # ========== 日志显示 ==========
  101. log_frame = ttk.LabelFrame(main_frame, text="📋 发送日志", padding="10")
  102. log_frame.pack(fill=tk.BOTH, expand=True)
  103. self.log_text = scrolledtext.ScrolledText(log_frame, height=20, wrap=tk.WORD, font=("Consolas", 9))
  104. self.log_text.pack(fill=tk.BOTH, expand=True)
  105. # 日志颜色标签
  106. self.log_text.tag_config("success", foreground="green")
  107. self.log_text.tag_config("error", foreground="red")
  108. self.log_text.tag_config("info", foreground="blue")
  109. self.log_text.tag_config("warning", foreground="orange")
  110. # ========== 底部状态栏 ==========
  111. status_bar = ttk.Frame(self.root)
  112. status_bar.pack(fill=tk.X, side=tk.BOTTOM)
  113. ttk.Label(status_bar, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W).pack(fill=tk.X, padx=5, pady=2)
  114. def select_file(self):
  115. """选择Excel文件"""
  116. file_path = filedialog.askopenfilename(
  117. title="选择Excel文件",
  118. filetypes=[("Excel files", "*.xlsx *.xls"), ("All files", "*.*")]
  119. )
  120. if file_path:
  121. self.file_path_var.set(file_path)
  122. self.excel_file = file_path
  123. self.log(f"已选择文件: {file_path}", "info")
  124. # 尝试自动识别工作表
  125. try:
  126. xl = pd.ExcelFile(file_path)
  127. sheets = xl.sheet_names
  128. if sheets:
  129. self.sheet_var.set(sheets[0])
  130. self.log(f"自动选择工作表: {sheets[0]}", "info")
  131. except Exception as e:
  132. self.log(f"读取工作表失败: {str(e)}", "warning")
  133. def paste_cookie(self):
  134. """从剪贴板粘贴Cookie"""
  135. try:
  136. clipboard_text = self.root.clipboard_get()
  137. # 尝试从curl命令中提取cookie
  138. if "cookie:" in clipboard_text.lower() or "-H" in clipboard_text:
  139. # 尝试从curl -b 或 -H "cookie: ..." 提取
  140. cookie_match = re.search(r'-b\s+"([^"]+)"', clipboard_text)
  141. if not cookie_match:
  142. cookie_match = re.search(r'-H\s+"cookie:\s*([^"]+)"', clipboard_text, re.IGNORECASE)
  143. if cookie_match:
  144. self.cookie_text.delete(1.0, tk.END)
  145. self.cookie_text.insert(1.0, cookie_match.group(1))
  146. self.log("已从剪贴板提取Cookie", "success")
  147. return
  148. # 直接粘贴
  149. self.cookie_text.delete(1.0, tk.END)
  150. self.cookie_text.insert(1.0, clipboard_text)
  151. self.log("已粘贴Cookie", "info")
  152. except:
  153. messagebox.showwarning("提示", "无法读取剪贴板内容")
  154. def load_default_cookie(self):
  155. """加载默认Cookie"""
  156. default_cookie = "_uuid_hera_ab_path_1=7651649498397052109; __tea__ug__uid=4144561781538482924; _gcl_au=1.1.775810441.1781538483; lgw_csrf_token=9dcbe9c422a1a2df5727bebebf4136760e1ad6dc-1781538485; passport_web_did=7651823325329411260; passport_trace_id=7651823325353757894; QXV0aHpDb250ZXh0=ba539bcfe4a042c8bc1e99d4d03add38; s_v_web_id=verify_mqg251ox_Tp4ofGaR_NCf4_4zII_8Ad3_c3HxGb4PXIuN; session=XN0YXJ0-5b9t9966-1122-4520-890c-e9313b1f818a-WVuZA; session_list=XN0YXJ0-5b9t9966-1122-4520-890c-e9313b1f818a-WVuZA; login_recently=1; is_anonymous_session=; _csrf_token=83aae12269bced10680d3ccf817f82262305e7d0-1781579108; lang=zh; et=c0c63f4480fdf14d7e6e9a21bab2b7e1; ccm_cdn_host=//lf-package-cn.feishucdn.com/obj/feishu-static; locale=zh-CN; landing_url=https://www.feishu.cn/; Hm_lvt_a79616d9322d81f12a92402ac6ae32ea=1781538483,1781579751; Hm_lpvt_a79616d9322d81f12a92402ac6ae32ea=1781579751; HMACCOUNT=5CA5EB76287246C7; _ga_VPYRHN104D=GS2.1.s1781579004$o2$g1$t1781579751$j60$l0$h0; _uetvid=9877555068d111f1992d03b61d301033; _ga=GA1.2.692261332.1781538484; i18n_locale=zh-CN; i18n_locale=zh-CN; _gid=GA1.2.1918132955.1781749497; site_env=pre=0; sl_session=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3ODE3OTMyNTgsInVuaXQiOiJldV9uYyIsInJhdyI6eyJtZXRhIjoiQVdvYzIrdU1BNHZSYUhkQjl4N2lRQUpxTUx6RjlrN012R293dk1YMlRzeThhakM5WXdRQURMSUNLZ0VBUVVGQlFVRkJRVUZCUVVKeFRVd3hha2h6UkUxNFVUMDkiLCJzdW0iOiI1MWU3ZWM3YzU0NTAzNTljMzRhMjE3ODdlM2U1OGNmZjQzNzZmM2IzYjJjNzU4NWVjMGE3MjQyZDZjNTBiNmNkIiwibG9jIjoiemhfY24iLCJhcGMiOiJSZWxlYXNlIiwiaWF0IjoxNzgxNzUwMDU4LCJzYWMiOnsiVXNlclN0YWZmU3RhdHVzIjoiMSIsIlVzZXJUeXBlIjoiNDIifSwibG9kIjpudWxsLCJjbmYiOnsiamt0Ijoic3VWaTdnNDNJX0lUb0w2ZGo5QXhEWnBacUhkYldzNWJaaVhvV0s4akZrdyJ9LCJucyI6ImxhcmsiLCJuc191aWQiOiI3NjQ2MjI4MDcyMDgxMDk5NzI5IiwibnNfdGlkIjoiNzUyNzU1NzgzMTgwMzk0NDk2MiIsIm90IjoxLCJjdCI6MTc4MTU3OTEwNywicnQiOjE3ODE2Njg4OTMsInRjcyI6MiwidGJ0IjowfX0.6MrIkLX5grSs5PUBr3U-TC5keonxZZDADzbJ0c00y9HsalJhhQ0gbmfk2u2w5RWhMkXt46a50yMc9E7Po6kymg; msToken=Oyg-XUAwfnJlSSSvRBdj1W_LxhRCXLlWMx9mk0Ryxvku6FZLH0ysyeYnmGrIG_yvUClyZT2cLYe5rhC1PtVygRRXOhYTvrDcL8Oidp-ZHHI-LNFKPJk-She-3kUxNTnwb3fYj5FfI36vX-1eFZ_neFVmoTtgD1GhK3M3oNjh5H-Y; swp_csrf_token=6fe8dbf3-4ab2-4f46-b4f3-5c9cba7364cd; t_beda37=a427edf4f48d9fa049a454a58600fa2f188ffacaf053a85e3782d6615e737b03; passport_app_access_token=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3ODE3OTU2NDcsInVuaXQiOiJldV9uYyIsInJhdyI6eyJtX2FjY2Vzc19pbmZvIjp7IjE0MyI6eyJpYXQiOjE3ODE3NDk1MTksImFjY2VzcyI6dHJ1ZX0sIjIiOnsiaWF0IjoxNzgxNzUyMTY1LCJhY2Nlc3MiOnRydWV9LCIxMDAiOnsiaWF0IjoxNzgxNzUyMjk1LCJhY2Nlc3MiOnRydWV9LCI0Ijp7ImlhdCI6MTc4MTc1MjQ0NywiYWNjZXNzIjp0cnVlfX0sInN1bSI6IjUxZTdlYzdjNTQ1MDM1OWMzNGEyMTc4N2UzZTU4Y2ZmNDM3NmYzYjNiMmM3NTg1ZWMwYTcyNDJkNmM1MGI2Y2QifX0.6o43emSe8iOBsXTPUbx4cShf-DdIpGuTTYDR5NDAAvA6ZByV_biiboF32sk5SjLAuTfEEy1xWpZ_35Fb-nxvJg"
  157. self.cookie_text.delete(1.0, tk.END)
  158. self.cookie_text.insert(1.0, default_cookie)
  159. self.log("已加载默认Cookie", "info")
  160. def save_config(self):
  161. """保存Cookie配置到文件"""
  162. cookie = self.cookie_text.get(1.0, tk.END).strip()
  163. if not cookie:
  164. messagebox.showwarning("提示", "Cookie不能为空")
  165. return
  166. try:
  167. with open("cookie_config.txt", "w", encoding="utf-8") as f:
  168. f.write(cookie)
  169. self.log("Cookie配置已保存", "success")
  170. messagebox.showinfo("成功", "Cookie配置已保存到 cookie_config.txt")
  171. except Exception as e:
  172. self.log(f"保存失败: {str(e)}", "error")
  173. def load_config(self):
  174. """加载保存的Cookie配置"""
  175. try:
  176. with open("cookie_config.txt", "r", encoding="utf-8") as f:
  177. cookie = f.read().strip()
  178. if cookie:
  179. self.cookie_text.delete(1.0, tk.END)
  180. self.cookie_text.insert(1.0, cookie)
  181. self.log("已加载保存的Cookie配置", "info")
  182. except FileNotFoundError:
  183. pass
  184. except Exception as e:
  185. self.log(f"加载配置失败: {str(e)}", "warning")
  186. def log(self, message, tag="info"):
  187. """添加日志"""
  188. timestamp = datetime.now().strftime("%H:%M:%S")
  189. self.log_text.insert(tk.END, f"[{timestamp}] {message}\n", tag)
  190. self.log_text.see(tk.END)
  191. self.root.update_idletasks()
  192. def clear_log(self):
  193. """清空日志"""
  194. self.log_text.delete(1.0, tk.END)
  195. def parse_cookie(self, cookie_string):
  196. """解析Cookie字符串为字典"""
  197. cookies = {}
  198. for item in cookie_string.split(';'):
  199. item = item.strip()
  200. if '=' in item:
  201. key, value = item.split('=', 1)
  202. cookies[key.strip()] = value.strip()
  203. return cookies
  204. def build_request_body(self, row):
  205. """根据Excel行数据构建请求体"""
  206. # 这个函数需要根据你的实际列名和字段映射来调整
  207. # 这里是一个示例,你需要根据实际情况修改
  208. try:
  209. # 获取各列数据
  210. name = str(row.get("姓名", "")) if pd.notna(row.get("姓名")) else ""
  211. gender = str(row.get("性别", "")) if pd.notna(row.get("性别")) else ""
  212. phone = str(row.get("手机号码", "")) if pd.notna(row.get("手机号码")) else ""
  213. remark = str(row.get("备注", "")) if pd.notna(row.get("备注")) else ""
  214. institution = str(row.get("机构", "")) if pd.notna(row.get("机构")) else ""
  215. target_school = str(row.get("意向学校", "")) if pd.notna(row.get("意向学校")) else ""
  216. consulting = str(row.get("咨询项目", "")) if pd.notna(row.get("咨询项目")) else ""
  217. product = str(row.get("产品", "")) if pd.notna(row.get("产品")) else ""
  218. source = str(row.get("线索来源", "")) if pd.notna(row.get("线索来源")) else ""
  219. market_person = str(row.get("市场人员", "")) if pd.notna(row.get("市场人员")) else ""
  220. # 构建备注文本(包含所有信息)
  221. remark_text = f"姓名:{name},性别:{gender},手机号码:{phone}"
  222. if remark:
  223. remark_text += f",{remark}"
  224. # 构建data JSON
  225. data_obj = {
  226. "fld43Khnuw": {
  227. "type": 1,
  228. "value": [{"type": "text", "text": remark_text}]
  229. },
  230. "fldexDPwcE": {"type": 3, "value": "optmlZt00z"} if institution else {"type": 3},
  231. "fldWt6JrgK": {"type": 3, "value": "optTD4UQsM"} if target_school else {"type": 3},
  232. "fldirDDoLV": {"type": 4, "value": ["opt96MRele"]} if consulting else {"type": 4},
  233. "fldudfcNVO": {"type": 4, "value": ["optgwn1azI"]} if product else {"type": 4},
  234. "fldS90QCXo": {"type": 1, "value": [{"type": "text", "text": name}]} if name else {"type": 1},
  235. "fldsA4YdS6": {"type": 3},
  236. "fldSSElgkA": {"type": 13, "value": {"fullPhoneNum": phone}} if phone else {"type": 13},
  237. "fldZPL52gS": {"type": 1},
  238. "fldPIaD4UH": {"type": 13},
  239. "fldUGeDVDF": {"type": 13},
  240. "fldKZ5WKyN": {"type": 1},
  241. "fld0MHPEaW": {"type": 3},
  242. "fldOLvk3zZ": {"type": 3},
  243. "fldkSVVnuu": {"type": 1},
  244. "fldCfRL3qM": {"type": 3, "value": "optTWIvAFb"} if source else {"type": 3},
  245. "fldodpd0fR": {
  246. "type": 11,
  247. "value": {
  248. "users": [{
  249. "userId": "7646228072081099729",
  250. "name": "俞玥戎",
  251. "avatarUrl": "https://s1-imfile.feishucdn.com/static-resource/v1/v3_00128_e0f4db42-191a-4878-8546-67a4dcb9005g~?image_size=72x72&cut_type=default-face&quality=&format=jpeg&sticker_format=.webp",
  252. "enName": "俞玥戎",
  253. "notify": False
  254. }]
  255. }
  256. } if market_person else {"type": 11},
  257. "flde7thxT7": {"type": 3, "value": "optKdksJ5a"},
  258. "fldaxavu52": {"type": 3, "value": "optHsn4zKU"},
  259. "fldMpwPaI1": {"type": 17}
  260. }
  261. # 构建完整的请求体
  262. body = {
  263. "shareToken": self.share_token,
  264. "data": json.dumps(data_obj, ensure_ascii=False),
  265. "preUploadEnable": True
  266. }
  267. return body
  268. except Exception as e:
  269. self.log(f"构建请求体失败: {str(e)}", "error")
  270. return None
  271. def send_request(self, row_data, row_index):
  272. """发送单条请求"""
  273. try:
  274. cookie_string = self.cookie_text.get(1.0, tk.END).strip()
  275. if not cookie_string:
  276. self.log("Cookie为空,请先配置Cookie", "error")
  277. return False, "Cookie为空"
  278. # 构建请求体
  279. body = self.build_request_body(row_data)
  280. if body is None:
  281. return False, "构建请求体失败"
  282. # 准备请求
  283. headers = self.default_headers.copy()
  284. # 更新Cookie
  285. cookies = self.parse_cookie(cookie_string)
  286. # 发送请求
  287. response = requests.post(
  288. self.url,
  289. headers=headers,
  290. cookies=cookies,
  291. json=body,
  292. timeout=30
  293. )
  294. # 解析响应
  295. try:
  296. resp_data = response.json()
  297. resp_text = json.dumps(resp_data, ensure_ascii=False, indent=2)
  298. except:
  299. resp_text = response.text
  300. if response.status_code == 200:
  301. self.log(f"✅ 第{row_index}行 发送成功 | {resp_text[:200]}...", "success")
  302. return True, resp_text
  303. else:
  304. self.log(f"❌ 第{row_index}行 发送失败 (HTTP {response.status_code}) | {resp_text[:200]}...", "error")
  305. return False, resp_text
  306. except requests.exceptions.Timeout:
  307. self.log(f"⏱️ 第{row_index}行 请求超时", "error")
  308. return False, "请求超时"
  309. except Exception as e:
  310. self.log(f"💥 第{row_index}行 异常: {str(e)}", "error")
  311. return False, str(e)
  312. def send_all(self):
  313. """发送所有数据的主循环"""
  314. try:
  315. # 获取配置
  316. excel_file = self.file_path_var.get()
  317. sheet_name = self.sheet_var.get()
  318. min_interval = float(self.min_interval_var.get())
  319. max_interval = float(self.max_interval_var.get())
  320. start_row = int(self.start_row_var.get()) - 1 # 转为0索引
  321. if not excel_file:
  322. self.log("请先选择Excel文件", "error")
  323. self.status_var.set("❌ 请先选择Excel文件")
  324. return
  325. # 读取Excel
  326. try:
  327. df = pd.read_excel(excel_file, sheet_name=sheet_name)
  328. self.log(f"成功读取Excel: {len(df)} 行数据", "info")
  329. except Exception as e:
  330. self.log(f"读取Excel失败: {str(e)}", "error")
  331. self.status_var.set(f"❌ 读取失败: {str(e)}")
  332. return
  333. if len(df) == 0:
  334. self.log("Excel中没有数据", "warning")
  335. self.status_var.set("⚠️ 没有数据")
  336. return
  337. # 从指定行开始
  338. df = df.iloc[start_row:]
  339. total = len(df)
  340. success_count = 0
  341. fail_count = 0
  342. self.log(f"开始发送,共 {total} 条数据", "info")
  343. self.status_var.set(f"🔄 发送中... 0/{total}")
  344. for idx, (_, row) in enumerate(df.iterrows()):
  345. if self.stop_flag:
  346. self.log("⏹️ 已停止发送", "warning")
  347. self.status_var.set("⏹️ 已停止")
  348. break
  349. row_index = start_row + idx + 1 # 实际行号
  350. # 发送请求
  351. success, resp_text = self.send_request(row.to_dict(), row_index)
  352. if success:
  353. success_count += 1
  354. else:
  355. fail_count += 1
  356. # 更新状态
  357. self.status_var.set(f"🔄 发送中... {idx+1}/{total} (成功:{success_count} 失败:{fail_count})")
  358. # 随机间隔(最后一条不等待)
  359. if idx < total - 1 and not self.stop_flag:
  360. interval = random.uniform(min_interval, max_interval)
  361. self.log(f"⏳ 等待 {interval:.1f} 秒...", "info")
  362. time.sleep(interval)
  363. # 完成统计
  364. self.log(f"🏁 发送完成!成功: {success_count}, 失败: {fail_count}", "success" if fail_count == 0 else "warning")
  365. self.status_var.set(f"✅ 完成 成功:{success_count} 失败:{fail_count}")
  366. except Exception as e:
  367. self.log(f"程序异常: {str(e)}", "error")
  368. self.status_var.set(f"❌ 异常: {str(e)}")
  369. finally:
  370. self.is_running = False
  371. self.start_btn.config(state=tk.NORMAL)
  372. self.stop_btn.config(state=tk.DISABLED)
  373. def start_send(self):
  374. """启动发送线程"""
  375. if self.is_running:
  376. return
  377. # 验证配置
  378. if not self.file_path_var.get():
  379. messagebox.showwarning("提示", "请先选择Excel文件")
  380. return
  381. cookie = self.cookie_text.get(1.0, tk.END).strip()
  382. if not cookie:
  383. messagebox.showwarning("提示", "请先配置Cookie")
  384. return
  385. try:
  386. float(self.min_interval_var.get())
  387. float(self.max_interval_var.get())
  388. int(self.start_row_var.get())
  389. except ValueError:
  390. messagebox.showerror("错误", "间隔或行号请输入有效数字")
  391. return
  392. # 重置状态
  393. self.is_running = True
  394. self.stop_flag = False
  395. self.start_btn.config(state=tk.DISABLED)
  396. self.stop_btn.config(state=tk.NORMAL)
  397. self.status_var.set("🔄 正在发送...")
  398. # 启动线程
  399. thread = threading.Thread(target=self.send_all, daemon=True)
  400. thread.start()
  401. def stop_send(self):
  402. """停止发送"""
  403. self.stop_flag = True
  404. self.log("正在停止...", "warning")
  405. self.stop_btn.config(state=tk.DISABLED)
  406. if __name__ == "__main__":
  407. root = tk.Tk()
  408. app = FeishuSubmitApp(root)
  409. root.mainloop()