|
|
@@ -21,14 +21,21 @@ class FeishuSubmitApp:
|
|
|
def __init__(self, root):
|
|
|
self.root = root
|
|
|
self.root.title("飞书表格数据提交工具 - 自动提取图片")
|
|
|
- self.root.geometry("1100x800")
|
|
|
+ self.root.geometry("1200x900")
|
|
|
|
|
|
- # 状态变量
|
|
|
+ # ========== 状态变量 ==========
|
|
|
self.is_running = False
|
|
|
self.stop_flag = False
|
|
|
self.excel_file = ""
|
|
|
self.cookie_string = ""
|
|
|
|
|
|
+ # 新增:暂停与阈值状态
|
|
|
+ self.is_paused = False
|
|
|
+ self.sent_count = 0 # 本次运行已成功发送计数
|
|
|
+ self.pause_threshold = 0 # 0表示不限制
|
|
|
+ self.schedule_running = False
|
|
|
+ self.schedule_thread = None
|
|
|
+
|
|
|
# 图片缓存
|
|
|
self.image_cache = {} # {行号: [图片数据列表]}
|
|
|
self.image_extract_folder = None
|
|
|
@@ -60,6 +67,7 @@ class FeishuSubmitApp:
|
|
|
|
|
|
self.setup_ui()
|
|
|
self.load_config()
|
|
|
+ self.start_schedule() # 启动定时调度器
|
|
|
|
|
|
def setup_ui(self):
|
|
|
"""构建界面"""
|
|
|
@@ -95,27 +103,52 @@ class FeishuSubmitApp:
|
|
|
ttk.Button(cookie_btn_frame, text="保存配置", command=self.save_config).pack(side=tk.LEFT, padx=(0, 10))
|
|
|
ttk.Button(cookie_btn_frame, text="加载默认", command=self.load_default_cookie).pack(side=tk.LEFT)
|
|
|
|
|
|
- # ========== 第三行:发送控制 ==========
|
|
|
- control_frame = ttk.LabelFrame(main_frame, text="⚙️ 发送控制", padding="10")
|
|
|
+ # ========== 第三行:发送控制 + 定时 + 阈值 ==========
|
|
|
+ control_frame = ttk.LabelFrame(main_frame, text="⚙️ 发送控制 / ⏰ 定时调度 / 🚦 阈值限制", padding="10")
|
|
|
control_frame.pack(fill=tk.X, pady=(0, 10))
|
|
|
|
|
|
- # 间隔配置
|
|
|
+ # --- 3.1 基础间隔配置 ---
|
|
|
ttk.Label(control_frame, text="最小间隔(秒):").grid(row=0, column=0, sticky=tk.W)
|
|
|
self.min_interval_var = tk.StringVar(value="20")
|
|
|
- ttk.Entry(control_frame, textvariable=self.min_interval_var, width=10).grid(row=0, column=1, padx=(5, 15), sticky=tk.W)
|
|
|
+ ttk.Entry(control_frame, textvariable=self.min_interval_var, width=8).grid(row=0, column=1, padx=(5, 15), sticky=tk.W)
|
|
|
|
|
|
ttk.Label(control_frame, text="最大间隔(秒):").grid(row=0, column=2, sticky=tk.W)
|
|
|
self.max_interval_var = tk.StringVar(value="50")
|
|
|
- ttk.Entry(control_frame, textvariable=self.max_interval_var, width=10).grid(row=0, column=3, padx=(5, 15), sticky=tk.W)
|
|
|
+ ttk.Entry(control_frame, textvariable=self.max_interval_var, width=8).grid(row=0, column=3, padx=(5, 15), sticky=tk.W)
|
|
|
|
|
|
ttk.Label(control_frame, text="起始行:").grid(row=0, column=4, sticky=tk.W)
|
|
|
self.start_row_var = tk.StringVar(value="2")
|
|
|
ttk.Entry(control_frame, textvariable=self.start_row_var, width=8).grid(row=0, column=5, padx=(5, 15), sticky=tk.W)
|
|
|
ttk.Label(control_frame, text="(表头在第1行)").grid(row=0, column=6, sticky=tk.W)
|
|
|
|
|
|
- # 操作按钮
|
|
|
+ # --- 3.2 定时自动启停 ---
|
|
|
+ ttk.Separator(control_frame, orient=tk.HORIZONTAL).grid(row=1, column=0, columnspan=7, sticky=tk.EW, pady=(12, 8))
|
|
|
+
|
|
|
+ ttk.Label(control_frame, text="⏰ 自动启动时间:").grid(row=2, column=0, sticky=tk.W)
|
|
|
+ self.start_time_var = tk.StringVar(value="")
|
|
|
+ ttk.Entry(control_frame, textvariable=self.start_time_var, width=10).grid(row=2, column=1, padx=(5, 5), sticky=tk.W)
|
|
|
+ ttk.Label(control_frame, text="(HH:MM, 留空则不自动启动)").grid(row=2, column=2, sticky=tk.W, columnspan=2)
|
|
|
+
|
|
|
+ ttk.Label(control_frame, text="自动关闭时间:").grid(row=2, column=4, sticky=tk.W)
|
|
|
+ self.stop_time_var = tk.StringVar(value="")
|
|
|
+ ttk.Entry(control_frame, textvariable=self.stop_time_var, width=10).grid(row=2, column=5, padx=(5, 5), sticky=tk.W)
|
|
|
+ ttk.Label(control_frame, text="(HH:MM, 留空则不自动关闭)").grid(row=2, column=6, sticky=tk.W)
|
|
|
+
|
|
|
+ # --- 3.3 阈值限制 ---
|
|
|
+ ttk.Separator(control_frame, orient=tk.HORIZONTAL).grid(row=3, column=0, columnspan=7, sticky=tk.EW, pady=(12, 8))
|
|
|
+
|
|
|
+ ttk.Label(control_frame, text="🚦 录入阈值(条):").grid(row=4, column=0, sticky=tk.W)
|
|
|
+ self.threshold_var = tk.StringVar(value="0")
|
|
|
+ ttk.Entry(control_frame, textvariable=self.threshold_var, width=10).grid(row=4, column=1, padx=(5, 5), sticky=tk.W)
|
|
|
+ ttk.Label(control_frame, text="(0=不限制, 达到后自动暂停)").grid(row=4, column=2, sticky=tk.W, columnspan=2)
|
|
|
+
|
|
|
+ # 已发送计数显示
|
|
|
+ self.count_display_var = tk.StringVar(value="本次已发送: 0 条")
|
|
|
+ ttk.Label(control_frame, textvariable=self.count_display_var, foreground="green", font=("Microsoft YaHei", 9, "bold")).grid(row=4, column=4, columnspan=3, sticky=tk.W)
|
|
|
+
|
|
|
+ # --- 3.4 操作按钮 ---
|
|
|
btn_frame = ttk.Frame(control_frame)
|
|
|
- btn_frame.grid(row=1, column=0, columnspan=7, pady=(15, 0), sticky=tk.W)
|
|
|
+ btn_frame.grid(row=5, column=0, columnspan=7, pady=(15, 0), sticky=tk.W)
|
|
|
|
|
|
self.start_btn = ttk.Button(btn_frame, text="🚀 开始发送", command=self.start_send)
|
|
|
self.start_btn.pack(side=tk.LEFT, padx=(0, 10))
|
|
|
@@ -123,12 +156,16 @@ class FeishuSubmitApp:
|
|
|
self.stop_btn = ttk.Button(btn_frame, text="⏹ 停止", command=self.stop_send, state=tk.DISABLED)
|
|
|
self.stop_btn.pack(side=tk.LEFT, padx=(0, 10))
|
|
|
|
|
|
+ # 新增:暂停/继续按钮
|
|
|
+ self.pause_btn = ttk.Button(btn_frame, text="⏸ 暂停", command=self.toggle_pause, state=tk.DISABLED)
|
|
|
+ self.pause_btn.pack(side=tk.LEFT, padx=(0, 10))
|
|
|
+
|
|
|
ttk.Button(btn_frame, text="清空日志", command=self.clear_log).pack(side=tk.LEFT, padx=(0, 10))
|
|
|
ttk.Button(btn_frame, text="📸 提取图片", command=self.extract_images).pack(side=tk.LEFT)
|
|
|
|
|
|
# 状态显示
|
|
|
- self.status_var = tk.StringVar(value="就绪")
|
|
|
- ttk.Label(control_frame, textvariable=self.status_var, foreground="blue").grid(row=2, column=0, columnspan=7, pady=(10, 0), sticky=tk.W)
|
|
|
+ self.status_var = tk.StringVar(value="就绪 | 定时调度: 运行中")
|
|
|
+ ttk.Label(control_frame, textvariable=self.status_var, foreground="blue").grid(row=6, column=0, columnspan=7, pady=(10, 0), sticky=tk.W)
|
|
|
|
|
|
# ========== 日志显示 ==========
|
|
|
log_frame = ttk.LabelFrame(main_frame, text="📋 发送日志", padding="10")
|
|
|
@@ -143,12 +180,116 @@ class FeishuSubmitApp:
|
|
|
self.log_text.tag_config("info", foreground="blue")
|
|
|
self.log_text.tag_config("warning", foreground="orange")
|
|
|
self.log_text.tag_config("debug", foreground="purple")
|
|
|
+ self.log_text.tag_config("pause", foreground="#FF6600")
|
|
|
+ self.log_text.tag_config("schedule", foreground="#0066CC")
|
|
|
|
|
|
# ========== 底部状态栏 ==========
|
|
|
status_bar = ttk.Frame(self.root)
|
|
|
status_bar.pack(fill=tk.X, side=tk.BOTTOM)
|
|
|
ttk.Label(status_bar, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W).pack(fill=tk.X, padx=5, pady=2)
|
|
|
|
|
|
+ # ==================== 定时调度相关 ====================
|
|
|
+
|
|
|
+ def start_schedule(self):
|
|
|
+ """启动后台定时调度线程"""
|
|
|
+ self.schedule_running = True
|
|
|
+ self.schedule_thread = threading.Thread(target=self._schedule_worker, daemon=True)
|
|
|
+ self.schedule_thread.start()
|
|
|
+ self.log("⏰ 定时调度器已启动,将按设定时间自动启停", "schedule")
|
|
|
+
|
|
|
+ def stop_schedule(self):
|
|
|
+ """停止定时调度线程"""
|
|
|
+ self.schedule_running = False
|
|
|
+ if self.schedule_thread and self.schedule_thread.is_alive():
|
|
|
+ # 由于线程在sleep,无法直接中断,设置标志即可
|
|
|
+ pass
|
|
|
+
|
|
|
+ def _schedule_worker(self):
|
|
|
+ """后台线程:每秒检查一次时间,触发自动启停"""
|
|
|
+ while self.schedule_running:
|
|
|
+ try:
|
|
|
+ now = datetime.now()
|
|
|
+ current_time_str = now.strftime("%H:%M")
|
|
|
+
|
|
|
+ start_time = self.start_time_var.get().strip()
|
|
|
+ stop_time = self.stop_time_var.get().strip()
|
|
|
+
|
|
|
+ # 自动启动检查
|
|
|
+ if start_time and not self.is_running and not self.is_paused:
|
|
|
+ if current_time_str == start_time and now.second < 2: # 在目标时间的0-1秒内触发
|
|
|
+ self.log(f"⏰ 到达自动启动时间 {start_time},即将开始发送...", "schedule")
|
|
|
+ # 在主线程中调用启动(通过after确保线程安全)
|
|
|
+ self.root.after(0, self._safe_auto_start)
|
|
|
+
|
|
|
+ # 自动关闭检查
|
|
|
+ if stop_time and self.is_running:
|
|
|
+ if current_time_str == stop_time and now.second < 2:
|
|
|
+ self.log(f"⏰ 到达自动关闭时间 {stop_time},即将停止发送...", "schedule")
|
|
|
+ self.root.after(0, self._safe_auto_stop)
|
|
|
+
|
|
|
+ time.sleep(1)
|
|
|
+ except Exception as e:
|
|
|
+ time.sleep(1)
|
|
|
+
|
|
|
+ def _safe_auto_start(self):
|
|
|
+ """线程安全的自动启动(在主线程执行)"""
|
|
|
+ if not self.is_running:
|
|
|
+ self.start_send()
|
|
|
+
|
|
|
+ def _safe_auto_stop(self):
|
|
|
+ """线程安全的自动停止(在主线程执行)"""
|
|
|
+ if self.is_running:
|
|
|
+ self.stop_send()
|
|
|
+
|
|
|
+ # ==================== 暂停/继续 ====================
|
|
|
+
|
|
|
+ def toggle_pause(self):
|
|
|
+ """暂停/继续切换"""
|
|
|
+ if not self.is_running:
|
|
|
+ return
|
|
|
+
|
|
|
+ if self.is_paused:
|
|
|
+ # 继续
|
|
|
+ self.is_paused = False
|
|
|
+ self.pause_btn.config(text="⏸ 暂停")
|
|
|
+ self.log("▶️ 已继续发送", "pause")
|
|
|
+ self.status_var.set("🔄 发送中... (已继续)")
|
|
|
+ else:
|
|
|
+ # 暂停
|
|
|
+ self.is_paused = True
|
|
|
+ self.pause_btn.config(text="▶️ 继续")
|
|
|
+ self.log("⏸ 已暂停发送(当前请求完成后暂停)", "pause")
|
|
|
+ self.status_var.set("⏸ 已暂停")
|
|
|
+
|
|
|
+ def _check_threshold(self):
|
|
|
+ """检查是否达到录入阈值"""
|
|
|
+ threshold_str = self.threshold_var.get().strip()
|
|
|
+ if not threshold_str or threshold_str == "0":
|
|
|
+ return False
|
|
|
+
|
|
|
+ try:
|
|
|
+ threshold = int(threshold_str)
|
|
|
+ if threshold <= 0:
|
|
|
+ return False
|
|
|
+
|
|
|
+ if self.sent_count >= threshold:
|
|
|
+ self.is_paused = True
|
|
|
+ self.pause_btn.config(text="▶️ 继续")
|
|
|
+ self.log(f"🚦 已达到录入阈值 {threshold} 条,自动暂停!", "pause")
|
|
|
+ self.status_var.set(f"⏸ 已达阈值 {threshold} 条,已自动暂停")
|
|
|
+ return True
|
|
|
+ except ValueError:
|
|
|
+ pass
|
|
|
+ return False
|
|
|
+
|
|
|
+ def reset_count(self):
|
|
|
+ """重置已发送计数"""
|
|
|
+ self.sent_count = 0
|
|
|
+ self.count_display_var.set("本次已发送: 0 条")
|
|
|
+ self.log("🔄 已重置发送计数", "info")
|
|
|
+
|
|
|
+ # ==================== 原有方法(修改部分标注) ====================
|
|
|
+
|
|
|
def select_file(self):
|
|
|
"""选择Excel文件"""
|
|
|
file_path = filedialog.askopenfilename(
|
|
|
@@ -160,7 +301,6 @@ class FeishuSubmitApp:
|
|
|
self.excel_file = file_path
|
|
|
self.log(f"已选择文件: {file_path}", "info")
|
|
|
|
|
|
- # 尝试自动识别工作表
|
|
|
try:
|
|
|
xl = pd.ExcelFile(file_path)
|
|
|
sheets = xl.sheet_names
|
|
|
@@ -249,15 +389,12 @@ class FeishuSubmitApp:
|
|
|
def _get_csrf_token(self):
|
|
|
"""从cookie中提取csrf_token"""
|
|
|
cookie_text = self.cookie_text.get(1.0, tk.END).strip()
|
|
|
- # 匹配 _csrf_token=xxx 或 csrf_token=xxx
|
|
|
match = re.search(r'_csrf_token=([^;]+)', cookie_text)
|
|
|
if match:
|
|
|
return match.group(1)
|
|
|
- # 也尝试匹配 x-csrftoken
|
|
|
match = re.search(r'x-csrftoken[=:]\s*([^;\s]+)', cookie_text, re.IGNORECASE)
|
|
|
if match:
|
|
|
return match.group(1)
|
|
|
- # 返回默认值
|
|
|
return "83aae12269bced10680d3ccf817f82262305e7d0-1781579108"
|
|
|
|
|
|
def _get_mime_type(self, filename):
|
|
|
@@ -274,9 +411,7 @@ class FeishuSubmitApp:
|
|
|
return mime_map.get(ext, 'image/jpeg')
|
|
|
|
|
|
def upload_image(self, image_data, filename="image.jpg"):
|
|
|
- """
|
|
|
- 完整上传图片到飞书,返回file_token信息
|
|
|
- """
|
|
|
+ """完整上传图片到飞书,返回file_token信息"""
|
|
|
try:
|
|
|
cookie_string = self.cookie_text.get(1.0, tk.END).strip()
|
|
|
if not cookie_string:
|
|
|
@@ -289,7 +424,6 @@ class FeishuSubmitApp:
|
|
|
self.log(f" 🔍 使用的CSRF token: {csrf_token[:30] if csrf_token else 'None'}...", "debug")
|
|
|
self.log(f" 🔍 图片大小: {len(image_data)} bytes", "debug")
|
|
|
|
|
|
- # 构建通用请求头
|
|
|
base_headers = {
|
|
|
"accept": "application/json, text/plain, */*",
|
|
|
"accept-language": "zh-CN,zh;q=0.9",
|
|
|
@@ -300,7 +434,7 @@ class FeishuSubmitApp:
|
|
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
|
|
}
|
|
|
|
|
|
- # ========== 步骤1: 获取uploadCode ==========
|
|
|
+ # 步骤1: 获取uploadCode
|
|
|
self.log(f" 📤 [1/4] 获取上传code...", "info")
|
|
|
code_url = "https://xhd.feishu.cn/space/api/bitable/external/share/uploadCode"
|
|
|
code_params = {
|
|
|
@@ -310,16 +444,10 @@ class FeishuSubmitApp:
|
|
|
"mountPoint": "bitable_tmp_point"
|
|
|
}
|
|
|
|
|
|
- self.log(f" 📋 请求URL: {code_url}", "debug")
|
|
|
- self.log(f" 📋 请求参数: {json.dumps(code_params, ensure_ascii=False)}", "debug")
|
|
|
-
|
|
|
code_response = requests.get(code_url, params=code_params, headers=base_headers, cookies=cookies, timeout=30)
|
|
|
|
|
|
- self.log(f" 📡 响应状态码: {code_response.status_code}", "debug")
|
|
|
-
|
|
|
if code_response.status_code != 200:
|
|
|
self.log(f" ❌ 获取code失败: HTTP {code_response.status_code}", "error")
|
|
|
- self.log(f" ❌ 响应内容: {code_response.text}", "error")
|
|
|
return None
|
|
|
|
|
|
try:
|
|
|
@@ -331,12 +459,11 @@ class FeishuSubmitApp:
|
|
|
upload_code = code_data.get('data', {}).get('uploadCode')
|
|
|
if not upload_code:
|
|
|
self.log(f" ❌ 响应中没有uploadCode字段", "error")
|
|
|
- self.log(f" ❌ data内容: {code_data.get('data', {})}", "error")
|
|
|
return None
|
|
|
|
|
|
- self.log(f" ✅ 获取uploadCode成功: {upload_code[:30]}...", "success")
|
|
|
+ self.log(f" ✅ 获取uploadCode成功", "success")
|
|
|
|
|
|
- # ========== 步骤2: 准备上传 ==========
|
|
|
+ # 步骤2: 准备上传
|
|
|
self.log(f" 📤 [2/4] 准备上传,获取upload_id...", "info")
|
|
|
prepare_url = "https://xhd.feishu.cn/space/api/box/upload/prepare/authcode/"
|
|
|
prepare_headers = base_headers.copy()
|
|
|
@@ -349,15 +476,10 @@ class FeishuSubmitApp:
|
|
|
"mount_node_token": self.share_token
|
|
|
}
|
|
|
|
|
|
- self.log(f" 📋 请求体: {json.dumps(prepare_body, ensure_ascii=False)}", "debug")
|
|
|
-
|
|
|
prepare_response = requests.post(prepare_url, headers=prepare_headers, cookies=cookies, json=prepare_body, timeout=30)
|
|
|
|
|
|
- self.log(f" 📡 响应状态码: {prepare_response.status_code}", "debug")
|
|
|
-
|
|
|
if prepare_response.status_code != 200:
|
|
|
self.log(f" ❌ 准备上传失败: HTTP {prepare_response.status_code}", "error")
|
|
|
- self.log(f" ❌ 响应内容: {prepare_response.text}", "error")
|
|
|
return None
|
|
|
|
|
|
try:
|
|
|
@@ -367,55 +489,45 @@ class FeishuSubmitApp:
|
|
|
return None
|
|
|
|
|
|
if prepare_data.get('code') != 0:
|
|
|
- self.log(f" ❌ 准备上传失败: code={prepare_data.get('code')}, msg={prepare_data.get('msg', '未知错误')}", "error")
|
|
|
+ self.log(f" ❌ 准备上传失败: code={prepare_data.get('code')}", "error")
|
|
|
return None
|
|
|
|
|
|
upload_id = prepare_data.get('data', {}).get('upload_id')
|
|
|
if not upload_id:
|
|
|
self.log(f" ❌ 响应中没有upload_id字段", "error")
|
|
|
- self.log(f" ❌ data内容: {prepare_data.get('data', {})}", "error")
|
|
|
return None
|
|
|
|
|
|
self.log(f" ✅ 获取upload_id成功: {upload_id}", "success")
|
|
|
|
|
|
+ # 步骤3: 上传文件内容
|
|
|
self.log(f" 📤 [3/4] 上传文件内容 ({len(image_data)} bytes)...", "info")
|
|
|
-
|
|
|
- # 使用 Adler-32 计算 checksum(飞书使用的算法)
|
|
|
- import zlib
|
|
|
checksum = zlib.adler32(image_data) & 0xffffffff
|
|
|
-
|
|
|
+
|
|
|
upload_url = f"https://internal-api-drive-stream.feishu.cn/space/api/box/stream/upload/merge_block/?upload_id={upload_id}"
|
|
|
upload_headers = base_headers.copy()
|
|
|
upload_headers["content-type"] = "application/octet-stream"
|
|
|
upload_headers["x-command"] = "space.api.box.stream.upload.merge_block"
|
|
|
upload_headers["x-seq-list"] = "0"
|
|
|
- upload_headers["x-block-list-checksum"] = str(checksum) # 注意:是数字字符串,不是十六进制
|
|
|
+ upload_headers["x-block-list-checksum"] = str(checksum)
|
|
|
upload_headers["x-block-origin-size"] = str(len(image_data))
|
|
|
-
|
|
|
- self.log(f" 📋 文件大小: {len(image_data)} bytes, checksum(Adler-32): {checksum}", "debug")
|
|
|
|
|
|
upload_response = requests.post(upload_url, headers=upload_headers, cookies=cookies, data=image_data, timeout=60)
|
|
|
|
|
|
- self.log(f" 📡 响应状态码: {upload_response.status_code}", "debug")
|
|
|
- self.log(f" 📡 响应内容: {upload_response.text[:500]}", "debug")
|
|
|
-
|
|
|
if upload_response.status_code != 200:
|
|
|
self.log(f" ❌ 上传文件失败: HTTP {upload_response.status_code}", "error")
|
|
|
- self.log(f" ❌ 响应内容: {upload_response.text}", "error")
|
|
|
return None
|
|
|
|
|
|
- # 检查上传是否真正成功
|
|
|
try:
|
|
|
upload_result = upload_response.json()
|
|
|
if upload_result.get('code') != 0:
|
|
|
- self.log(f" ❌ 上传文件失败: code={upload_result.get('code')}, msg={upload_result.get('message', '未知错误')}", "error")
|
|
|
+ self.log(f" ❌ 上传文件失败: code={upload_result.get('code')}", "error")
|
|
|
return None
|
|
|
except:
|
|
|
pass
|
|
|
|
|
|
self.log(f" ✅ 文件上传成功", "success")
|
|
|
|
|
|
- # ========== 步骤4: 完成上传 ==========
|
|
|
+ # 步骤4: 完成上传
|
|
|
self.log(f" 📤 [4/4] 完成上传,获取file_token...", "info")
|
|
|
finish_url = "https://xhd.feishu.cn/space/api/box/upload/finish/"
|
|
|
finish_headers = base_headers.copy()
|
|
|
@@ -428,16 +540,10 @@ class FeishuSubmitApp:
|
|
|
"push_open_history_record": 0
|
|
|
}
|
|
|
|
|
|
- self.log(f" 📋 请求体: {json.dumps(finish_body, ensure_ascii=False)}", "debug")
|
|
|
-
|
|
|
finish_response = requests.post(finish_url, headers=finish_headers, cookies=cookies, json=finish_body, timeout=30)
|
|
|
|
|
|
- self.log(f" 📡 响应状态码: {finish_response.status_code}", "debug")
|
|
|
- self.log(f" 📡 响应内容: {finish_response.text[:500]}", "debug")
|
|
|
-
|
|
|
if finish_response.status_code != 200:
|
|
|
self.log(f" ❌ 完成上传失败: HTTP {finish_response.status_code}", "error")
|
|
|
- self.log(f" ❌ 响应内容: {finish_response.text}", "error")
|
|
|
return None
|
|
|
|
|
|
try:
|
|
|
@@ -447,21 +553,16 @@ class FeishuSubmitApp:
|
|
|
return None
|
|
|
|
|
|
if finish_data.get('code') != 0:
|
|
|
- self.log(f" ❌ 完成上传失败: code={finish_data.get('code')}, msg={finish_data.get('message', '未知错误')}", "error")
|
|
|
- self.log(f" ❌ 完整响应: {json.dumps(finish_data, ensure_ascii=False, indent=2)}", "error")
|
|
|
+ self.log(f" ❌ 完成上传失败: code={finish_data.get('code')}", "error")
|
|
|
return None
|
|
|
|
|
|
file_token = finish_data.get('data', {}).get('file_token')
|
|
|
if not file_token:
|
|
|
self.log(f" ❌ 响应中没有file_token字段", "error")
|
|
|
- self.log(f" ❌ data内容: {finish_data.get('data', {})}", "error")
|
|
|
return None
|
|
|
|
|
|
- # 获取图片信息
|
|
|
width, height = 0, 0
|
|
|
try:
|
|
|
- from PIL import Image
|
|
|
- import io
|
|
|
pil_img = Image.open(io.BytesIO(image_data))
|
|
|
width, height = pil_img.size
|
|
|
except:
|
|
|
@@ -491,10 +592,7 @@ class FeishuSubmitApp:
|
|
|
return None
|
|
|
|
|
|
def extract_images_to_cache(self, file_path, sheet_name):
|
|
|
- """
|
|
|
- 提取Excel中的所有图片到缓存
|
|
|
- 返回: dict {行号: [图片数据列表]}
|
|
|
- """
|
|
|
+ """提取Excel中的所有图片到缓存"""
|
|
|
image_cache = {}
|
|
|
try:
|
|
|
wb = load_workbook(file_path, data_only=True)
|
|
|
@@ -504,7 +602,6 @@ class FeishuSubmitApp:
|
|
|
self.log(f" 📸 开始提取 {len(ws._images)} 张图片到缓存...", "info")
|
|
|
for idx, img in enumerate(ws._images):
|
|
|
try:
|
|
|
- # 获取位置信息
|
|
|
if hasattr(img, 'anchor') and hasattr(img.anchor, '_from'):
|
|
|
anchor_from = img.anchor._from
|
|
|
row = anchor_from.row + 1
|
|
|
@@ -513,11 +610,7 @@ class FeishuSubmitApp:
|
|
|
row = idx + 1
|
|
|
col = 0
|
|
|
|
|
|
- # 获取图片数据
|
|
|
image_data = None
|
|
|
- img_format = 'jpg'
|
|
|
- filename = f"image_{row}_{idx}.jpg"
|
|
|
-
|
|
|
if hasattr(img, '_data') and callable(img._data):
|
|
|
image_data = img._data()
|
|
|
elif hasattr(img, '_data') and not callable(img._data):
|
|
|
@@ -531,10 +624,9 @@ class FeishuSubmitApp:
|
|
|
if image_data is None:
|
|
|
continue
|
|
|
|
|
|
- # 检测图片格式
|
|
|
+ img_format = 'jpg'
|
|
|
+ filename = f"image_{row}_{idx}.jpg"
|
|
|
try:
|
|
|
- from PIL import Image
|
|
|
- import io
|
|
|
pil_img = Image.open(io.BytesIO(image_data))
|
|
|
img_format = pil_img.format if pil_img.format else 'jpg'
|
|
|
filename = f"image_{row}_{idx}.{img_format.lower()}"
|
|
|
@@ -566,7 +658,6 @@ class FeishuSubmitApp:
|
|
|
def build_request_body(self, row_dict, row_index):
|
|
|
"""根据Excel行数据构建请求体"""
|
|
|
try:
|
|
|
- # 获取各列数据
|
|
|
name = str(row_dict.get("姓名", "")) if pd.notna(row_dict.get("姓名")) else ""
|
|
|
gender = str(row_dict.get("性别", "")) if pd.notna(row_dict.get("性别")) else ""
|
|
|
phone = str(row_dict.get("手机号码", "")) if pd.notna(row_dict.get("手机号码")) else ""
|
|
|
@@ -580,130 +671,58 @@ class FeishuSubmitApp:
|
|
|
userId = str(row_dict.get("用户ID", "")) if pd.notna(row_dict.get("用户ID")) else ""
|
|
|
channel_code = str(row_dict.get("渠道代码", "")) if pd.notna(row_dict.get("渠道代码")) else ""
|
|
|
userImg = str(row_dict.get("人员头像", "")) if pd.notna(row_dict.get("人员头像")) else ""
|
|
|
- # 去掉数字的小数点
|
|
|
phone = phone.split('.')[0]
|
|
|
self.log(f" 📝 提取数据: 姓名={name}, 手机={phone}, 渠道代码={channel_code}, 意向学校={target_school_text}, 咨询项目={consulting_text}, 产品={product_text}", "debug")
|
|
|
|
|
|
- # 意向校区映射字典
|
|
|
school_mapping = {
|
|
|
- "上海": "optqw2jaJ5",
|
|
|
- "徐汇": "optTD4UQsM",
|
|
|
- "人广": "opt7hQjSZQ",
|
|
|
- "长宁": "opt7A8WBtE",
|
|
|
- "浦东": "optnyoxnTK",
|
|
|
- "杨浦": "optgArj17r",
|
|
|
- "闵行": "optCIwo1J1",
|
|
|
- "松江": "optuZzXsYm",
|
|
|
- "锦秋个人学校": "optLQYDW3S",
|
|
|
- "封闭学院": "optitJpd5G",
|
|
|
- "大学生-市区": "optHlvKsKy",
|
|
|
- "大学生-曹崇杨": "optdcoQXBt",
|
|
|
- "大学生-临港": "opt3i4fOKJ",
|
|
|
- "大学生-松江": "optG7eGFUg",
|
|
|
- "大学生-奉贤": "optJJqnLgY",
|
|
|
+ "上海": "optqw2jaJ5", "徐汇": "optTD4UQsM", "人广": "opt7hQjSZQ",
|
|
|
+ "长宁": "opt7A8WBtE", "浦东": "optnyoxnTK", "杨浦": "optgArj17r",
|
|
|
+ "闵行": "optCIwo1J1", "松江": "optuZzXsYm", "锦秋个人学校": "optLQYDW3S",
|
|
|
+ "封闭学院": "optitJpd5G", "大学生-市区": "optHlvKsKy", "大学生-曹崇杨": "optdcoQXBt",
|
|
|
+ "大学生-临港": "opt3i4fOKJ", "大学生-松江": "optG7eGFUg", "大学生-奉贤": "optJJqnLgY",
|
|
|
"常州": "opt5wuiYjW"
|
|
|
}
|
|
|
|
|
|
- # 咨询项目映射字典
|
|
|
consulting_mapping = {
|
|
|
- "语培": "opt96MRele",
|
|
|
- "留学": "optQFQsXIe",
|
|
|
- "考研": "opti3wbRF7",
|
|
|
- "AF艺术": "optaZC2VCL",
|
|
|
- "美行音乐": "optzSqwgLp",
|
|
|
- "锦秋个人学校": "opt9N4AGbJ",
|
|
|
- "研学考团": "optdYBLknN",
|
|
|
- "尚思": "optf8lTDcV"
|
|
|
+ "语培": "opt96MRele", "留学": "optQFQsXIe", "考研": "opti3wbRF7",
|
|
|
+ "AF艺术": "optaZC2VCL", "美行音乐": "optzSqwgLp", "锦秋个人学校": "opt9N4AGbJ",
|
|
|
+ "研学考团": "optdYBLknN", "尚思": "optf8lTDcV"
|
|
|
}
|
|
|
|
|
|
- # 语培产品映射字典(完整版)
|
|
|
product_mapping = {
|
|
|
- "ACT": "optlGkVm5n",
|
|
|
- "AEAS": "optqX3b1rJ",
|
|
|
- "ALEVEL": "optgMNcV8D",
|
|
|
- "AP": "optWKWYQgo",
|
|
|
- "BC": "opt8huX4Nv",
|
|
|
- "DSE": "optxFBYmUs",
|
|
|
- "ESL": "optaYj9RZM",
|
|
|
- "GMAT": "optgwu03Nf",
|
|
|
- "GRE": "optEOyf5TR",
|
|
|
- "IB": "optBhWIw7x",
|
|
|
- "IGCSE": "optIuoUFpo",
|
|
|
- "KET": "optjcLdrsB",
|
|
|
- "PET": "optY42DNFo",
|
|
|
- "PTE": "optOrHJvTX",
|
|
|
- "SAT": "optTxT8NEu",
|
|
|
- "SSAT": "optJ2a0tkB",
|
|
|
- "Toefl Junior": "optEGInE0d",
|
|
|
- "国际备考": "optgwn1azI",
|
|
|
- "国际研学及考团": "optAmxtqGN",
|
|
|
- "国家地理": "opttXZvoj3",
|
|
|
- "剑桥英语": "optQkkhzD5",
|
|
|
- "锦秋竞赛": "optUgXXmpF",
|
|
|
- "考研": "optgQrGHTS",
|
|
|
- "留学预备": "optWxtOU8b",
|
|
|
- "美高": "opt36E5u0t",
|
|
|
- "硕果": "optNKeSnho",
|
|
|
- "硕果竞赛": "optMzk3TpL",
|
|
|
- "腾飞计划": "optYTD9h04",
|
|
|
- "团企培": "optW5UDF6G",
|
|
|
- "托福": "optKuZ2zCq",
|
|
|
- "雅思": "optWsal1MB",
|
|
|
- "用英语讲中国故事": "optinda27s",
|
|
|
- "在线": "opt2X07PY4",
|
|
|
- "浙大国际本科": "optZFmVTTC",
|
|
|
- "语培待定/未知": "optTNitLA5",
|
|
|
- "大学四级": "optAqYPFmN",
|
|
|
- "大学六级": "optpdfKgdl",
|
|
|
- "菁英树": "optBFhBHFX",
|
|
|
- "其它": "opt698ww1H"
|
|
|
+ "ACT": "optlGkVm5n", "AEAS": "optqX3b1rJ", "ALEVEL": "optgMNcV8D",
|
|
|
+ "AP": "optWKWYQgo", "BC": "opt8huX4Nv", "DSE": "optxFBYmUs",
|
|
|
+ "ESL": "optaYj9RZM", "GMAT": "optgwu03Nf", "GRE": "optEOyf5TR",
|
|
|
+ "IB": "optBhWIw7x", "IGCSE": "optIuoUFpo", "KET": "optjcLdrsB",
|
|
|
+ "PET": "optY42DNFo", "PTE": "optOrHJvTX", "SAT": "optTxT8NEu",
|
|
|
+ "SSAT": "optJ2a0tkB", "Toefl Junior": "optEGInE0d", "国际备考": "optgwn1azI",
|
|
|
+ "国际研学及考团": "optAmxtqGN", "国家地理": "opttXZvoj3", "剑桥英语": "optQkkhzD5",
|
|
|
+ "锦秋竞赛": "optUgXXmpF", "考研": "optgQrGHTS", "留学预备": "optWxtOU8b",
|
|
|
+ "美高": "opt36E5u0t", "硕果": "optNKeSnho", "硕果竞赛": "optMzk3TpL",
|
|
|
+ "腾飞计划": "optYTD9h04", "团企培": "optW5UDF6G", "托福": "optKuZ2zCq",
|
|
|
+ "雅思": "optWsal1MB", "用英语讲中国故事": "optinda27s", "在线": "opt2X07PY4",
|
|
|
+ "浙大国际本科": "optZFmVTTC", "语培待定/未知": "optTNitLA5", "大学四级": "optAqYPFmN",
|
|
|
+ "大学六级": "optpdfKgdl", "菁英树": "optBFhBHFX", "其它": "opt698ww1H"
|
|
|
}
|
|
|
|
|
|
-
|
|
|
- # 处理意向学校:根据文本查找对应的ID
|
|
|
target_school_id = None
|
|
|
if target_school_text:
|
|
|
- # 精确匹配
|
|
|
if target_school_text in school_mapping:
|
|
|
target_school_id = school_mapping[target_school_text]
|
|
|
- self.log(f" 🏫 意向学校: {target_school_text} -> {target_school_id}", "debug")
|
|
|
else:
|
|
|
- # 尝试模糊匹配(包含关系)
|
|
|
- matched = False
|
|
|
for school_name, school_id in school_mapping.items():
|
|
|
if school_name in target_school_text or target_school_text in school_name:
|
|
|
target_school_id = school_id
|
|
|
- matched = True
|
|
|
- self.log(f" 🏫 意向学校模糊匹配: {target_school_text} -> {school_name} ({target_school_id})", "debug")
|
|
|
break
|
|
|
-
|
|
|
- if not matched:
|
|
|
- self.log(f" ⚠️ 未找到匹配的意向学校: {target_school_text}", "warning")
|
|
|
|
|
|
- # 处理咨询项目:根据文本查找对应的ID(支持多选)
|
|
|
consulting_ids = []
|
|
|
if consulting_text:
|
|
|
- # 如果是多个项目,用分隔符分割(如逗号、顿号等)
|
|
|
consulting_items = re.split(r'[,,、/ ]+', consulting_text)
|
|
|
for item in consulting_items:
|
|
|
item = item.strip()
|
|
|
- if item:
|
|
|
- if item in consulting_mapping:
|
|
|
- consulting_ids.append(consulting_mapping[item])
|
|
|
- self.log(f" 📚 咨询项目: {item} -> {consulting_mapping[item]}", "debug")
|
|
|
- else:
|
|
|
- # 尝试模糊匹配
|
|
|
- matched = False
|
|
|
- for consulting_name, consulting_id in consulting_mapping.items():
|
|
|
- if consulting_name in item or item in consulting_name:
|
|
|
- consulting_ids.append(consulting_id)
|
|
|
- matched = True
|
|
|
- self.log(f" 📚 咨询项目模糊匹配: {item} -> {consulting_name} ({consulting_id})", "debug")
|
|
|
- break
|
|
|
- if not matched:
|
|
|
- self.log(f" ⚠️ 未找到匹配的咨询项目: {item}", "warning")
|
|
|
+ if item and item in consulting_mapping:
|
|
|
+ consulting_ids.append(consulting_mapping[item])
|
|
|
|
|
|
- # 处理产品:根据文本查找对应的ID(支持多选)
|
|
|
product_ids = []
|
|
|
if product_text:
|
|
|
product_items = re.split(r'[,,、/ ]+', product_text)
|
|
|
@@ -712,38 +731,25 @@ class FeishuSubmitApp:
|
|
|
if item:
|
|
|
if item in product_mapping:
|
|
|
product_ids.append(product_mapping[item])
|
|
|
- self.log(f" 📦 产品: {item} -> {product_mapping[item]}", "debug")
|
|
|
else:
|
|
|
- # 尝试模糊匹配(支持大小写不敏感)
|
|
|
- matched = False
|
|
|
item_lower = item.lower()
|
|
|
for product_name, product_id in product_mapping.items():
|
|
|
- # 支持大小写不敏感的匹配
|
|
|
if product_name.lower() == item_lower or product_name in item or item in product_name:
|
|
|
product_ids.append(product_id)
|
|
|
- matched = True
|
|
|
- self.log(f" 📦 产品模糊匹配: {item} -> {product_name} ({product_id})", "debug")
|
|
|
break
|
|
|
- if not matched:
|
|
|
- self.log(f" ⚠️ 未找到匹配的产品: {item}", "warning")
|
|
|
|
|
|
- # 构建备注文本
|
|
|
remark_text = f"姓名:{name},性别:{gender},手机号码:{phone}"
|
|
|
if remark:
|
|
|
remark_text += f",{remark}"
|
|
|
|
|
|
- # 构建data JSON
|
|
|
data_obj = {
|
|
|
"fld43Khnuw": {
|
|
|
"type": 1,
|
|
|
"value": [{"type": "text", "text": remark_text}]
|
|
|
},
|
|
|
"fldexDPwcE": {"type": 3, "value": "optmlZt00z"} if institution else {"type": 3},
|
|
|
- # 意向学校 - 使用映射后的ID
|
|
|
"fldWt6JrgK": {"type": 3, "value": target_school_id} if target_school_id else {"type": 3},
|
|
|
- # 咨询项目 - 使用映射后的ID列表(多选)
|
|
|
"fldirDDoLV": {"type": 4, "value": consulting_ids} if consulting_ids else {"type": 4},
|
|
|
- # 语培产品 - 使用映射后的ID列表(多选)
|
|
|
"fldudfcNVO": {"type": 4, "value": product_ids} if product_ids else {"type": 4},
|
|
|
"fldS90QCXo": {"type": 1, "value": [{"type": "text", "text": name}]} if name else {"type": 1},
|
|
|
"fldsA4YdS6": {"type": 3},
|
|
|
@@ -760,57 +766,39 @@ class FeishuSubmitApp:
|
|
|
"type": 11,
|
|
|
"value": {
|
|
|
"users": [{
|
|
|
- "userId":userId,
|
|
|
- "name":market_person,
|
|
|
- "avatarUrl":userImg,
|
|
|
- "enName":market_person,
|
|
|
- "notify":False,
|
|
|
+ "userId": userId,
|
|
|
+ "name": market_person,
|
|
|
+ "avatarUrl": userImg,
|
|
|
+ "enName": market_person,
|
|
|
+ "notify": False,
|
|
|
}]
|
|
|
}
|
|
|
} if market_person else {"type": 11},
|
|
|
- # ====== 渠道代码 - 使用映射后的ID ======
|
|
|
"flde7thxT7": {"type": 3, "value": channel_code},
|
|
|
"fldaxavu52": {"type": 3, "value": "optHsn4zKU"},
|
|
|
- "fldMpwPaI1": {"type": 17} # 图片字段,默认空
|
|
|
+ "fldMpwPaI1": {"type": 17}
|
|
|
}
|
|
|
|
|
|
- # ========== 检查并处理图片 ==========
|
|
|
if self.image_cache and row_index in self.image_cache:
|
|
|
images = self.image_cache[row_index]
|
|
|
- self.log(f" 🖼️ 检测到 {len(images)} 个嵌入图片在第 {row_index} 行", "info")
|
|
|
-
|
|
|
- # 上传第一张图片
|
|
|
if images:
|
|
|
img_info = images[0]
|
|
|
-
|
|
|
- # 检查是否已经上传过
|
|
|
if row_index in self.uploaded_cache:
|
|
|
img_value = self.uploaded_cache[row_index]
|
|
|
- self.log(f" ♻️ 使用已上传的图片: {img_value.get('attachmentToken')}", "debug")
|
|
|
else:
|
|
|
- # 上传图片
|
|
|
- self.log(f" 📤 开始上传图片...", "info")
|
|
|
uploaded = self.upload_image(img_info['data'], img_info['filename'])
|
|
|
if uploaded:
|
|
|
self.uploaded_cache[row_index] = uploaded
|
|
|
img_value = uploaded
|
|
|
- self.log(f" ✅ 图片上传成功,token: {uploaded.get('attachmentToken')}", "success")
|
|
|
else:
|
|
|
- self.log(f" ⚠️ 图片上传失败,跳过该图片", "warning")
|
|
|
img_value = None
|
|
|
|
|
|
- # 如果上传成功,添加到data_obj
|
|
|
if img_value:
|
|
|
data_obj["fldMpwPaI1"] = {
|
|
|
"type": 17,
|
|
|
"value": [img_value]
|
|
|
}
|
|
|
- self.log(f" 📎 图片已添加到请求中", "debug")
|
|
|
- else:
|
|
|
- # 没有图片,确保图片字段为空
|
|
|
- data_obj["fldMpwPaI1"] = {"type": 17}
|
|
|
|
|
|
- # 构建完整的请求体
|
|
|
body = {
|
|
|
"shareToken": self.share_token,
|
|
|
"data": json.dumps(data_obj, ensure_ascii=False),
|
|
|
@@ -835,27 +823,19 @@ class FeishuSubmitApp:
|
|
|
self.log("❌ Cookie为空,请先配置Cookie", "error")
|
|
|
return False, "Cookie为空"
|
|
|
|
|
|
- # 构建请求体
|
|
|
body = self.build_request_body(row_data, row_index)
|
|
|
if body is None:
|
|
|
self.log("❌ 构建请求体失败", "error")
|
|
|
return False, "构建请求体失败"
|
|
|
|
|
|
- # 记录请求体大小
|
|
|
body_str = json.dumps(body, ensure_ascii=False)
|
|
|
self.log(f"📦 请求体大小: {len(body_str)} 字节", "debug")
|
|
|
|
|
|
- # 准备请求
|
|
|
headers = self.default_headers.copy()
|
|
|
cookies = self.parse_cookie(cookie_string)
|
|
|
-
|
|
|
- # 更新动态头
|
|
|
headers["x-csrftoken"] = self._get_csrf_token()
|
|
|
headers["f-version"] = "docs-6-17-1781628518399"
|
|
|
|
|
|
- self.log(f"🌐 发送请求到: {self.url}", "debug")
|
|
|
-
|
|
|
- # 发送请求
|
|
|
response = requests.post(
|
|
|
self.url,
|
|
|
headers=headers,
|
|
|
@@ -864,47 +844,33 @@ class FeishuSubmitApp:
|
|
|
timeout=30
|
|
|
)
|
|
|
|
|
|
- self.log(f"📡 响应状态码: {response.status_code}", "debug")
|
|
|
-
|
|
|
- # 解析响应
|
|
|
try:
|
|
|
resp_data = response.json()
|
|
|
resp_text = json.dumps(resp_data, ensure_ascii=False, indent=2)
|
|
|
-
|
|
|
- if isinstance(resp_data, dict):
|
|
|
- if resp_data.get('code') != 0 and resp_data.get('code') is not None:
|
|
|
- self.log(f"⚠️ 服务器返回错误码: {resp_data.get('code')}", "warning")
|
|
|
- self.log(f" 错误信息: {resp_data.get('msg', '未知错误')}", "error")
|
|
|
- if 'data' in resp_data:
|
|
|
- self.log(f" 📊 响应数据: {json.dumps(resp_data['data'], ensure_ascii=False)[:200]}...", "debug")
|
|
|
except:
|
|
|
resp_text = response.text
|
|
|
- self.log("⚠️ 响应不是有效的JSON格式", "warning")
|
|
|
|
|
|
if response.status_code == 200:
|
|
|
self.log(f"✅ 第{row_index}行 发送成功", "success")
|
|
|
return True, resp_text
|
|
|
else:
|
|
|
self.log(f"❌ 第{row_index}行 发送失败 (HTTP {response.status_code})", "error")
|
|
|
- self.log(f" 响应内容: {resp_text[:500]}...", "error")
|
|
|
return False, resp_text
|
|
|
|
|
|
except requests.exceptions.Timeout:
|
|
|
self.log(f"⏱️ 第{row_index}行 请求超时", "error")
|
|
|
return False, "请求超时"
|
|
|
except requests.exceptions.ConnectionError:
|
|
|
- self.log(f"🔌 第{row_index}行 连接失败,请检查网络", "error")
|
|
|
+ self.log(f"🔌 第{row_index}行 连接失败", "error")
|
|
|
return False, "连接失败"
|
|
|
except Exception as e:
|
|
|
self.log(f"💥 第{row_index}行 异常: {str(e)}", "error")
|
|
|
- import traceback
|
|
|
- self.log(f"详细错误: {traceback.format_exc()}", "debug")
|
|
|
return False, str(e)
|
|
|
finally:
|
|
|
self.log(f"{'='*50}\n", "info")
|
|
|
|
|
|
def send_all(self):
|
|
|
- """发送所有数据的主循环"""
|
|
|
+ """发送所有数据的主循环(已增加暂停和阈值检查)"""
|
|
|
try:
|
|
|
excel_file = self.file_path_var.get()
|
|
|
sheet_name = self.sheet_var.get()
|
|
|
@@ -917,29 +883,23 @@ class FeishuSubmitApp:
|
|
|
self.status_var.set("❌ 请先选择Excel文件")
|
|
|
return
|
|
|
|
|
|
- # 清空上次上传缓存
|
|
|
+ # 初始化计数
|
|
|
+ self.sent_count = 0
|
|
|
+ self.count_display_var.set("本次已发送: 0 条")
|
|
|
self.uploaded_cache = {}
|
|
|
|
|
|
- # ========== 自动提取所有图片到缓存 ==========
|
|
|
self.log(f"🔍 正在自动提取Excel中的嵌入图片...", "info")
|
|
|
self.image_cache = self.extract_images_to_cache(excel_file, sheet_name)
|
|
|
|
|
|
if self.image_cache:
|
|
|
total_images = sum(len(imgs) for imgs in self.image_cache.values())
|
|
|
self.log(f"📊 共提取 {total_images} 张图片,分布在 {len(self.image_cache)} 行", "info")
|
|
|
- rows_with_images = sorted(self.image_cache.keys())
|
|
|
- if len(rows_with_images) <= 20:
|
|
|
- self.log(f"📋 有图片的行: {rows_with_images}", "debug")
|
|
|
- else:
|
|
|
- self.log(f"📋 有图片的行: {rows_with_images[:20]} ... (共{len(rows_with_images)}行)", "debug")
|
|
|
else:
|
|
|
self.log(f"📷 未检测到任何嵌入图片", "info")
|
|
|
|
|
|
- # ========== 读取Excel数据 ==========
|
|
|
try:
|
|
|
df = pd.read_excel(excel_file, sheet_name=sheet_name)
|
|
|
self.log(f"📊 成功读取Excel: {len(df)} 行数据", "info")
|
|
|
- self.log(f"📋 列名: {', '.join(df.columns.tolist())}", "info")
|
|
|
except Exception as e:
|
|
|
self.log(f"❌ 读取Excel失败: {str(e)}", "error")
|
|
|
self.status_var.set(f"❌ 读取失败: {str(e)}")
|
|
|
@@ -947,7 +907,6 @@ class FeishuSubmitApp:
|
|
|
|
|
|
if len(df) == 0:
|
|
|
self.log("⚠️ Excel中没有数据", "warning")
|
|
|
- self.status_var.set("⚠️ 没有数据")
|
|
|
return
|
|
|
|
|
|
df = df.iloc[start_row:]
|
|
|
@@ -959,26 +918,42 @@ class FeishuSubmitApp:
|
|
|
self.status_var.set(f"🔄 发送中... 0/{total}")
|
|
|
|
|
|
for idx, (_, row) in enumerate(df.iterrows()):
|
|
|
+ # ========== 暂停检查(核心新增)==========
|
|
|
+ while self.is_paused and not self.stop_flag:
|
|
|
+ self.status_var.set(f"⏸ 已暂停 | 成功:{success_count} 失败:{fail_count}")
|
|
|
+ time.sleep(0.5)
|
|
|
+
|
|
|
if self.stop_flag:
|
|
|
self.log("⏹️ 已停止发送", "warning")
|
|
|
self.status_var.set("⏹️ 已停止")
|
|
|
break
|
|
|
|
|
|
- # ===== 关键修复:计算Excel实际行号 =====
|
|
|
- # DataFrame的索引 + 2(因为第1行是表头,DataFrame从0开始)
|
|
|
- # 再加上start_row(起始行的偏移)
|
|
|
excel_row_number = idx + 2 + start_row
|
|
|
|
|
|
- self.log(f" 📍 DataFrame索引: {idx}, Excel行号: {excel_row_number}", "debug")
|
|
|
-
|
|
|
- # 检查该行是否有图片(使用Excel实际行号)
|
|
|
if self.image_cache and excel_row_number in self.image_cache:
|
|
|
img_count = len(self.image_cache[excel_row_number])
|
|
|
- self.log(f" 🖼️ 第{excel_row_number}行有 {img_count} 个嵌入图片,将自动上传并添加", "info")
|
|
|
+ self.log(f" 🖼️ 第{excel_row_number}行有 {img_count} 个嵌入图片", "info")
|
|
|
|
|
|
success, resp_text = self.send_request(row.to_dict(), excel_row_number)
|
|
|
if success:
|
|
|
success_count += 1
|
|
|
+ self.sent_count += 1
|
|
|
+ self.count_display_var.set(f"本次已发送: {self.sent_count} 条")
|
|
|
+
|
|
|
+ # ========== 阈值检查(核心新增)==========
|
|
|
+ if self._check_threshold():
|
|
|
+ self.log(f"⏸ 因达到阈值暂停,等待手动继续...", "pause")
|
|
|
+ # 进入暂停等待循环
|
|
|
+ while self.is_paused and not self.stop_flag:
|
|
|
+ self.status_var.set(f"⏸ 已达阈值暂停 | 成功:{success_count} 失败:{fail_count}")
|
|
|
+ time.sleep(0.5)
|
|
|
+
|
|
|
+ if self.stop_flag:
|
|
|
+ break
|
|
|
+ # 继续后重置计数(可选:如果想累计不清零,注释掉下面这行)
|
|
|
+ # self.sent_count = 0
|
|
|
+ # self.count_display_var.set(f"本次已发送: {self.sent_count} 条")
|
|
|
+
|
|
|
else:
|
|
|
fail_count += 1
|
|
|
|
|
|
@@ -999,6 +974,8 @@ class FeishuSubmitApp:
|
|
|
self.status_var.set(f"❌ 异常: {str(e)}")
|
|
|
finally:
|
|
|
self.is_running = False
|
|
|
+ self.is_paused = False
|
|
|
+ self.pause_btn.config(text="⏸ 暂停", state=tk.DISABLED)
|
|
|
self.start_btn.config(state=tk.NORMAL)
|
|
|
self.stop_btn.config(state=tk.DISABLED)
|
|
|
|
|
|
@@ -1020,14 +997,27 @@ class FeishuSubmitApp:
|
|
|
float(self.min_interval_var.get())
|
|
|
float(self.max_interval_var.get())
|
|
|
int(self.start_row_var.get())
|
|
|
+ threshold_str = self.threshold_var.get().strip()
|
|
|
+ if threshold_str and threshold_str != "0":
|
|
|
+ int(threshold_str)
|
|
|
except ValueError:
|
|
|
- messagebox.showerror("错误", "间隔或行号请输入有效数字")
|
|
|
+ messagebox.showerror("错误", "间隔、行号或阈值请输入有效数字")
|
|
|
return
|
|
|
|
|
|
+ # 验证时间格式
|
|
|
+ for time_var, name in [(self.start_time_var, "启动时间"), (self.stop_time_var, "关闭时间")]:
|
|
|
+ t = time_var.get().strip()
|
|
|
+ if t:
|
|
|
+ if not re.match(r'^([01]\d|2[0-3]):([0-5]\d)$', t):
|
|
|
+ messagebox.showerror("错误", f"{name} 格式错误,请使用 HH:MM 格式")
|
|
|
+ return
|
|
|
+
|
|
|
self.is_running = True
|
|
|
self.stop_flag = False
|
|
|
+ self.is_paused = False
|
|
|
self.start_btn.config(state=tk.DISABLED)
|
|
|
self.stop_btn.config(state=tk.NORMAL)
|
|
|
+ self.pause_btn.config(text="⏸ 暂停", state=tk.NORMAL)
|
|
|
self.status_var.set("🔄 正在发送...")
|
|
|
|
|
|
thread = threading.Thread(target=self.send_all, daemon=True)
|
|
|
@@ -1036,10 +1026,11 @@ class FeishuSubmitApp:
|
|
|
def stop_send(self):
|
|
|
"""停止发送"""
|
|
|
self.stop_flag = True
|
|
|
+ self.is_paused = False
|
|
|
self.log("⏹️ 正在停止...", "warning")
|
|
|
self.stop_btn.config(state=tk.DISABLED)
|
|
|
+ self.pause_btn.config(state=tk.DISABLED)
|
|
|
|
|
|
- # ========== 提取图片功能(手动) ==========
|
|
|
def extract_images_to_folder(self, file_path, sheet_name, output_folder=None):
|
|
|
"""提取Excel中的所有图片并保存到文件夹"""
|
|
|
try:
|
|
|
@@ -1065,11 +1056,8 @@ class FeishuSubmitApp:
|
|
|
f.write(f"工作表: {sheet_name}\n")
|
|
|
f.write(f"提取时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
|
|
f.write("="*60 + "\n\n")
|
|
|
- f.write("行号\t列号\t列字母\t文件名\t尺寸\t格式\n")
|
|
|
- f.write("-"*60 + "\n")
|
|
|
|
|
|
if hasattr(ws, '_images') and ws._images:
|
|
|
- self.log(f" 📸 开始提取 {len(ws._images)} 张图片...", "debug")
|
|
|
for idx, img in enumerate(ws._images):
|
|
|
try:
|
|
|
if hasattr(img, 'anchor') and hasattr(img.anchor, '_from'):
|
|
|
@@ -1097,12 +1085,9 @@ class FeishuSubmitApp:
|
|
|
error_count += 1
|
|
|
continue
|
|
|
|
|
|
- # 检测格式并保存
|
|
|
img_format = 'jpg'
|
|
|
width, height = 0, 0
|
|
|
try:
|
|
|
- from PIL import Image
|
|
|
- import io
|
|
|
pil_img = Image.open(io.BytesIO(image_data))
|
|
|
img_format = pil_img.format if pil_img.format else 'jpg'
|
|
|
width, height = pil_img.size
|
|
|
@@ -1115,27 +1100,12 @@ class FeishuSubmitApp:
|
|
|
with open(filepath, 'wb') as f_out:
|
|
|
f_out.write(image_data)
|
|
|
|
|
|
- f.write(f"{row}\t{col}\t{col_letter}\t{filename}\t{width}x{height}\t{img_format}\n")
|
|
|
image_count += 1
|
|
|
-
|
|
|
- if image_count % 50 == 0:
|
|
|
- self.log(f" 📸 已提取 {image_count} 张图片...", "debug")
|
|
|
except Exception as e:
|
|
|
error_count += 1
|
|
|
- self.log(f" ⚠️ 提取图片失败: {str(e)}", "warning")
|
|
|
|
|
|
wb.close()
|
|
|
-
|
|
|
- with open(mapping_file, 'a', encoding='utf-8') as f:
|
|
|
- f.write("\n" + "="*60 + "\n")
|
|
|
- f.write(f"提取完成: 成功 {image_count} 张, 失败 {error_count} 张\n")
|
|
|
- f.write("="*60 + "\n")
|
|
|
-
|
|
|
self.log(f"✅ 成功提取 {image_count} 张图片到: {output_folder}", "success")
|
|
|
- if error_count > 0:
|
|
|
- self.log(f"⚠️ 有 {error_count} 张图片提取失败", "warning")
|
|
|
- self.log(f"📋 映射文件: {mapping_file}", "info")
|
|
|
-
|
|
|
return output_folder
|
|
|
|
|
|
except Exception as e:
|