批处理完成后保留固定值设置,新增重试和清空设置按钮

- _on_finished 不再清空产品/客户输入框及缓存选择
- 每行结果保存原始数据 _row 以支持重试
- 新增"重试"按钮:重跑所有非成功记录,原地更新结果表格
- 新增"清空设置"按钮:一键清空所有固定值字段和缓存
This commit is contained in:
2026-05-22 22:52:52 +08:00
parent 017b9bb397
commit 683e5b46de
+184 -12
View File
@@ -208,6 +208,10 @@ class App:
btn_frame, text="⏹ 停止", command=self._stop, width=10, state=DISABLED btn_frame, text="⏹ 停止", command=self._stop, width=10, state=DISABLED
) )
self.stop_btn.pack(side="left", padx=2) 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( Label(
@@ -262,6 +266,11 @@ class App:
Entry(r3, textvariable=self.fv_sender_addr, width=18).pack(side="left", padx=5) Entry(r3, textvariable=self.fv_sender_addr, width=18).pack(side="left", padx=5)
Label(r3, text="(有值即强制覆盖)", fg="gray", font=("", 8)).pack(side="left") 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 = Frame(self.root)
progress_frame.pack(fill=X, padx=10, pady=2) progress_frame.pack(fill=X, padx=10, pady=2)
@@ -876,6 +885,7 @@ class App:
break break
result = await _engine.process_single_row(row) result = await _engine.process_single_row(row)
result["_row"] = row # 保留原始行数据以便重试
self.results.append(result) self.results.append(result)
# 更新结果表格 # 更新结果表格
@@ -914,6 +924,24 @@ class App:
) )
self.status_var.set(f"处理中: {current}/{total}") 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): def _on_finished(self):
global _engine global _engine
self.running = False self.running = False
@@ -921,18 +949,18 @@ class App:
_engine = None _engine = None
self._set_controls_state("stopped") self._set_controls_state("stopped")
self.status_var.set("就绪") self.status_var.set("就绪")
# 清空产品/客户输入框(batch 结束后重置) # 清空产品/客户输入框(batch 结束后重置)—— 已注释,保留设置以便重试
self.fv_product.set("") # self.fv_product.set("")
self.fv_customer.set("") # self.fv_customer.set("")
# 清空缓存的选择 # # 清空缓存的选择
self._product_cached_choice = None # self._product_cached_choice = None
self._product_cached_detail = None # self._product_cached_detail = None
self._customer_cached_choice = None # self._customer_cached_choice = None
self._customer_cached_detail = None # self._customer_cached_detail = None
self._sender_realname_cached = None # self._sender_realname_cached = None
self.fv_transport_type.set("") # self.fv_transport_type.set("")
self.fv_weight.set("") # self.fv_weight.set("")
self._transmode_map = {} # self._transmode_map = {}
def _pause(self): def _pause(self):
global _engine global _engine
@@ -962,15 +990,159 @@ class App:
self.status_var.set("正在停止...") self.status_var.set("正在停止...")
self._log("用户停止") 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): def _set_controls_state(self, state):
if state == "running": if state == "running":
self.start_btn.configure(state=DISABLED) self.start_btn.configure(state=DISABLED)
self.pause_btn.configure(state=NORMAL) self.pause_btn.configure(state=NORMAL)
self.stop_btn.configure(state=NORMAL) self.stop_btn.configure(state=NORMAL)
self.retry_btn.configure(state=DISABLED)
else: else:
self.start_btn.configure(state=NORMAL) self.start_btn.configure(state=NORMAL)
self.pause_btn.configure(state=DISABLED, text="⏸ 暂停") self.pause_btn.configure(state=DISABLED, text="⏸ 暂停")
self.stop_btn.configure(state=DISABLED) 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)
# ─── 日志 ─────────────────────────────────────────────── # ─── 日志 ───────────────────────────────────────────────