diff --git a/main.py b/main.py index 26c58a3..881f629 100644 --- a/main.py +++ b/main.py @@ -208,6 +208,10 @@ class App: btn_frame, text="⏹ 停止", command=self._stop, width=10, state=DISABLED ) self.stop_btn.pack(side="left", padx=2) + self.retry_btn = Button( + btn_frame, text="重试", command=self._retry, width=10, state=DISABLED + ) + self.retry_btn.pack(side="left", padx=2) # 状态 Label( @@ -262,6 +266,11 @@ class App: Entry(r3, textvariable=self.fv_sender_addr, width=18).pack(side="left", padx=5) Label(r3, text="(有值即强制覆盖)", fg="gray", font=("", 8)).pack(side="left") + # 清空设置按钮 + r4 = Frame(fixed_frame) + r4.pack(fill=X, pady=2) + Button(r4, text="清空设置", command=self._clear_fixed_values, width=10).pack(side="right", padx=5) + # ── 进度条 ── progress_frame = Frame(self.root) progress_frame.pack(fill=X, padx=10, pady=2) @@ -876,6 +885,7 @@ class App: break result = await _engine.process_single_row(row) + result["_row"] = row # 保留原始行数据以便重试 self.results.append(result) # 更新结果表格 @@ -914,6 +924,24 @@ class App: ) self.status_var.set(f"处理中: {current}/{total}") + def _clear_fixed_values(self): + """清空所有固定值设置和缓存的选择。""" + self.fv_product.set("") + self.fv_customer.set("") + self.fv_destination.set("") + self.fv_district_code.set("") + self.fv_sender_name.set("") + self.fv_sender_addr.set("") + self.fv_transport_type.set("") + self.fv_weight.set("") + self._product_cached_choice = None + self._product_cached_detail = None + self._customer_cached_choice = None + self._customer_cached_detail = None + self._sender_realname_cached = None + self._transmode_map = {} + self.fv_transport_combo["values"] = [] + def _on_finished(self): global _engine self.running = False @@ -921,18 +949,18 @@ class App: _engine = None self._set_controls_state("stopped") self.status_var.set("就绪") - # 清空产品/客户输入框(batch 结束后重置) - self.fv_product.set("") - self.fv_customer.set("") - # 清空缓存的选择 - self._product_cached_choice = None - self._product_cached_detail = None - self._customer_cached_choice = None - self._customer_cached_detail = None - self._sender_realname_cached = None - self.fv_transport_type.set("") - self.fv_weight.set("") - self._transmode_map = {} + # 清空产品/客户输入框(batch 结束后重置)—— 已注释,保留设置以便重试 + # self.fv_product.set("") + # self.fv_customer.set("") + # # 清空缓存的选择 + # self._product_cached_choice = None + # self._product_cached_detail = None + # self._customer_cached_choice = None + # self._customer_cached_detail = None + # self._sender_realname_cached = None + # self.fv_transport_type.set("") + # self.fv_weight.set("") + # self._transmode_map = {} def _pause(self): global _engine @@ -962,15 +990,159 @@ class App: self.status_var.set("正在停止...") self._log("用户停止") + def _retry(self): + """重试所有非成功的记录。""" + if self.running: + return + failed_indices = [] + failed_rows = [] + for i, r in enumerate(self.results): + if r.get("status") != "成功" and "_row" in r: + failed_indices.append(i) + failed_rows.append(r["_row"]) + if not failed_rows: + messagebox.showinfo("提示", "没有需要重试的记录") + return + self._save_fixed_from_entries() + self.running = True + self._set_controls_state("running") + self.status_var.set(f"重试 {len(failed_rows)} 条...") + self._log(f"开始重试 {len(failed_rows)} 条失败记录...") + self.progress["maximum"] = len(failed_rows) + self.progress["value"] = 0 + self.progress_label.configure(text=f"成功 0 | 失败 0 | 剩余 {len(failed_rows)}") + _run_in_thread(self._run_retry(failed_indices, failed_rows)) + + async def _run_retry(self, indices, rows): + """后台重试指定的行。""" + global _engine + try: + from engine import PickupEngine + + field_mapping = get_field_mapping(self.config) + defaults = get_defaults(self.config) + timing = get_timing_config(self.config) + fixed_values = get_fixed_values(self.config) + + _engine = PickupEngine( + url="https://10.4.188.1/pickup-web/a/postwaybill/main", + field_mapping=field_mapping, + defaults=defaults, + timing=timing, + log_callback=self._log_async, + fixed_values=fixed_values, + choose_callback=self._choose_callback, + confirm_callback=self._confirm_callback, + ) + + if self._search_api is not None: + _engine.inject_infrastructure(self._search_sess, self._search_bridge, self._search_api) + if self._product_cached_choice is not None: + _engine._product_choice = self._product_cached_choice + if self._product_cached_detail is not None: + _engine._product_detail = self._product_cached_detail + if self._customer_cached_choice is not None: + _engine._customer_choice = self._customer_cached_choice + if self._customer_cached_detail is not None: + _engine._customer_detail = self._customer_cached_detail + if self._sender_realname_cached is not None: + _engine._sender_realname = self._sender_realname_cached + transport_name = self.fv_transport_type.get() + if transport_name and self._transmode_map: + _engine._transport_type = self._transmode_map.get(transport_name, "") + + self._update_status("启动中...") + try: + await _engine.start() + except Exception as e: + self._log_async(f"[error] 启动失败: {e}") + self._update_status("启动失败") + await _engine.stop() + return + + if not _engine._sess or not _engine._sess._http.cookies: + self._log_async("[提示] 未找到登录会话,请点击'登录'按钮完成登录后再开始处理") + self._update_status("未登录") + await _engine.stop() + return + cookie_names = {c.name for c in _engine._sess._http.cookies} + missing = [n for n in ("JSESSIONID", "jdpt.session.id") if n not in cookie_names] + if missing: + self._log_async(f"[提示] 缺少关键 cookie: {', '.join(missing)},请重新登录并手动填写") + self._update_status("未登录") + await _engine.stop() + return + + try: + await _engine.begin_processing() + self._update_status("后台运行中") + self._log_async("HTTP API 引擎已就绪") + except Exception as e: + self._log_async(f"[error] 初始化失败: {e}") + self._update_status("启动失败") + await _engine.stop() + return + + children = self.result_tree.get_children() + success = 0 + fail = 0 + for j, (idx, row) in enumerate(zip(indices, rows)): + if _engine._stop_requested: + break + + result = await _engine.process_single_row(row) + result["_row"] = row + self.results[idx] = result + + values = ( + result.get("单号", ""), + result.get("status", ""), + result.get("重量", ""), + result.get("产品", ""), + result.get("客户", ""), + result.get("运输类型", ""), + result.get("寄达地区划代码", ""), + result.get("寄件人姓名", ""), + result.get("寄件地址", ""), + ) + # 更新对应 treeview 行 + if idx < len(children): + self.root.after(0, lambda v=values, iid=children[idx]: self.result_tree.item(iid, values=v)) + + if result.get("status") == "成功": + success += 1 + else: + fail += 1 + + self.root.after(0, lambda cur=j+1, total=len(rows), s=success, f=fail: self._update_progress(cur, total, s, f)) + + delay = timing.get("delay_between_rows_ms", 3000) / 1000 + if j < len(rows) - 1: + await asyncio.sleep(delay) + + self._log_async(f"\n重试完成!成功 {success},失败 {fail}") + + except Exception as e: + self._log_async(f"[error] 重试异常: {e}") + import traceback + traceback.print_exc() + finally: + if _engine: + await _engine.stop() + self.root.after(0, self._on_finished) + def _set_controls_state(self, state): if state == "running": self.start_btn.configure(state=DISABLED) self.pause_btn.configure(state=NORMAL) self.stop_btn.configure(state=NORMAL) + self.retry_btn.configure(state=DISABLED) else: self.start_btn.configure(state=NORMAL) self.pause_btn.configure(state=DISABLED, text="⏸ 暂停") self.stop_btn.configure(state=DISABLED) + has_failed = any(r.get("status") != "成功" for r in self.results) + self.retry_btn.configure(state=NORMAL if has_failed else DISABLED) # ─── 日志 ───────────────────────────────────────────────