产品/客户搜索提前到启动前:输入框防抖搜索+下拉选择+缓存,登录时初始化搜索API,batch结束自动清空
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
# 中国邮政揽收系统 - 自动填单工具
|
# 中国邮政揽收系统 - 自动填单工具 v2.0
|
||||||
|
|
||||||
通过 HTTP API + Node.js token bridge 实现,**不依赖浏览器自动化**。读取 Excel 运单数据,逐条自动填写产品、客户、运单信息并提交。
|
通过 HTTP API + Node.js token bridge 实现,**不依赖浏览器自动化**。读取 Excel 运单数据,逐条自动填写产品、客户、运单信息并提交。
|
||||||
|
|
||||||
@@ -53,11 +53,11 @@ bash build.sh # Linux
|
|||||||
|
|
||||||
```
|
```
|
||||||
Excel 行数据
|
Excel 行数据
|
||||||
→ 产品搜索 (autocomplete API) → 用户选择
|
→ 产品选择 (启动前搜索 + 缓存) → 用户确认
|
||||||
→ 客户搜索 (autocomplete API) → 用户选择
|
→ 客户选择 (启动前搜索 + 缓存) → 用户确认
|
||||||
→ 运单查询 → 获取收寄件人/地址/电话
|
→ 运单查询 → 获取收寄件人/地址/电话
|
||||||
→ 地址匹配 → 获取区划代码
|
→ 地址匹配 → 获取区划代码
|
||||||
→ 组装表单 (5层优先级) → 确认弹窗 → 提交
|
→ 组装表单 (5层优先级) → 确认弹窗(首行) → 提交
|
||||||
```
|
```
|
||||||
|
|
||||||
### 表单字段优先级
|
### 表单字段优先级
|
||||||
@@ -80,19 +80,32 @@ Excel 行数据
|
|||||||
|
|
||||||
点击 `🔑 登录` → 弹出对话框 → 用浏览器 cookies 导出工具导出 `10.4.188.1` 的所有 cookies 为 JSON 数组 → 粘贴到对话框 → 保存。
|
点击 `🔑 登录` → 弹出对话框 → 用浏览器 cookies 导出工具导出 `10.4.188.1` 的所有 cookies 为 JSON 数组 → 粘贴到对话框 → 保存。
|
||||||
|
|
||||||
cookies 保存到 `session.json`(与 exe 同目录)。
|
cookies 保存到 `session.json`(与 exe 同目录)。登录成功后自动初始化 token bridge 和搜索 API,供后续产品/客户搜索使用。
|
||||||
|
|
||||||
### 2. 选择 Excel
|
### 2. 选择产品/客户
|
||||||
|
|
||||||
|
登录后,在"固定值设置"区域的产品和客户输入框中输入关键词,自动触发搜索并弹出下拉选择框。选中后自动获取产品详情和客户详情并缓存,缓存有效期为一个 batch(处理完一批 Excel 数据后自动清空)。
|
||||||
|
|
||||||
|
### 3. 选择 Excel
|
||||||
|
|
||||||
点击 `选择文件`,选择要处理的 Excel 文件。
|
点击 `选择文件`,选择要处理的 Excel 文件。
|
||||||
|
|
||||||
### 3. 配置固定值(可选)
|
### 4. 配置固定值(可选)
|
||||||
|
|
||||||
在 GUI 固定值表格中设置常用字段,勾选 "强制覆盖" 可覆盖 API 获取的值。
|
在固定值区域填写:
|
||||||
|
|
||||||
### 4. 开始处理
|
| 字段 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 产品 | 用于 autocomplete 搜索,从下拉框选择 |
|
||||||
|
| 客户 | 用于 autocomplete 搜索,从下拉框选择 |
|
||||||
|
| 寄达地 | 强制覆盖运单返回的寄达地(勾选强制覆盖后生效) |
|
||||||
|
| 寄达地区划代码 | 直接决定寄达地,勾选强制覆盖后优先级最高 |
|
||||||
|
| 寄件人姓名 | 强制覆盖客户资料中的寄件人 |
|
||||||
|
| 寄件地址 | 强制覆盖客户资料中的寄件地址 |
|
||||||
|
|
||||||
点击 `▶ 开始处理` → 弹出产品/客户选择框 → 确认提交信息 → 自动批量处理后续行。
|
### 5. 开始处理
|
||||||
|
|
||||||
|
点击 `▶ 开始处理` → 首行弹出确认弹窗 → 确认后自动批量处理后续行。提交结果实时显示在下方表格中。
|
||||||
|
|
||||||
## Excel 格式
|
## Excel 格式
|
||||||
|
|
||||||
@@ -116,13 +129,13 @@ cookies 保存到 `session.json`(与 exe 同目录)。
|
|||||||
- `url`: 揽收系统地址
|
- `url`: 揽收系统地址
|
||||||
|
|
||||||
### timing
|
### timing
|
||||||
- `delay_between_rows_ms`: 行间延时(毫秒)
|
- `delay_between_rows_ms`: 行间延时(毫秒),GUI 中间隔输入框单位也是毫秒
|
||||||
|
|
||||||
### fixed_values
|
### fixed_values
|
||||||
```
|
```
|
||||||
"字段名": {"value": "值", "force": false}
|
"字段名": {"value": "值", "force": false}
|
||||||
```
|
```
|
||||||
`force: true` 时覆盖 API 获取值。
|
`force: true` 时覆盖 API 获取值。GUI 中通过复选框控制,点击"开始处理"时自动保存到配置文件。
|
||||||
|
|
||||||
### defaults
|
### defaults
|
||||||
字段的默认值(中文名,自动翻译为系统编码)。
|
字段的默认值(中文名,自动翻译为系统编码)。
|
||||||
@@ -130,6 +143,18 @@ cookies 保存到 `session.json`(与 exe 同目录)。
|
|||||||
### field_mapping
|
### field_mapping
|
||||||
Excel 列名 → 表单参数名的自定义映射。内置映射覆盖常见字段,仅需在此配置自定义列名。
|
Excel 列名 → 表单参数名的自定义映射。内置映射覆盖常见字段,仅需在此配置自定义列名。
|
||||||
|
|
||||||
|
## 开发
|
||||||
|
|
||||||
|
### 日志等级
|
||||||
|
|
||||||
|
`log.py` 中修改 `LEVEL`:
|
||||||
|
- `-1` = 关闭所有日志(默认)
|
||||||
|
- `0` = 仅用户级消息
|
||||||
|
- `1` = 调试级
|
||||||
|
- `2` = 追踪级(API 请求/响应)
|
||||||
|
|
||||||
|
修改后重新打包生效。
|
||||||
|
|
||||||
## 文档
|
## 文档
|
||||||
|
|
||||||
项目级文档见 `../docs/`:
|
项目级文档见 `../docs/`:
|
||||||
@@ -142,14 +167,3 @@ Excel 列名 → 表单参数名的自定义映射。内置映射覆盖常见字
|
|||||||
| `portal-to-pickup-full-flow.har` | 门户→揽收页面完整网络流 |
|
| `portal-to-pickup-full-flow.har` | 门户→揽收页面完整网络流 |
|
||||||
| `product-customer-selection-flow.har` | 产品/客户选择 API 流 |
|
| `product-customer-selection-flow.har` | 产品/客户选择 API 流 |
|
||||||
| `successful-submit-with-address-change.har` | 改寄达地后成功提交 |
|
| `successful-submit-with-address-change.har` | 改寄达地后成功提交 |
|
||||||
|
|
||||||
## 开发
|
|
||||||
|
|
||||||
### 日志等级
|
|
||||||
|
|
||||||
`log.py` 中修改 `LEVEL`:
|
|
||||||
- `0` = 仅用户级消息
|
|
||||||
- `1` = 调试级
|
|
||||||
- `2` = 追踪级(API 请求/响应)
|
|
||||||
|
|
||||||
修改后重新打包生效。
|
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ if exist build rmdir /s /q build
|
|||||||
echo [4/4] Building exe with PyInstaller...
|
echo [4/4] Building exe with PyInstaller...
|
||||||
uv run pyinstaller ^
|
uv run pyinstaller ^
|
||||||
--onefile ^
|
--onefile ^
|
||||||
--windowed ^
|
--console ^
|
||||||
--name "PickupAuto" ^
|
--name "PickupAuto" ^
|
||||||
--icon="pickup.ico" ^
|
--icon="pickup.ico" ^
|
||||||
--add-data "config.json;." ^
|
--add-data "config.json;." ^
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ rm -rf dist build
|
|||||||
echo "[4/4] Building with PyInstaller..."
|
echo "[4/4] Building with PyInstaller..."
|
||||||
uv run pyinstaller \
|
uv run pyinstaller \
|
||||||
--onefile \
|
--onefile \
|
||||||
--windowed \
|
--console \
|
||||||
--name "PickupAuto" \
|
--name "PickupAuto" \
|
||||||
--add-data "config.json:." \
|
--add-data "config.json:." \
|
||||||
--add-data "pickup.ico:." \
|
--add-data "pickup.ico:." \
|
||||||
|
|||||||
@@ -306,6 +306,7 @@ class PickupEngine:
|
|||||||
|
|
||||||
self._stop_requested = False
|
self._stop_requested = False
|
||||||
self._pause_requested = False
|
self._pause_requested = False
|
||||||
|
self._infra_injected = False # 基础设施从外部注入时跳过 start/begin_processing
|
||||||
|
|
||||||
# 跨行复用的产品/客户详情(从 API 获取)
|
# 跨行复用的产品/客户详情(从 API 获取)
|
||||||
self._product_detail: dict = {}
|
self._product_detail: dict = {}
|
||||||
@@ -333,10 +334,24 @@ class PickupEngine:
|
|||||||
return translate_value(excel_col, value)
|
return translate_value(excel_col, value)
|
||||||
return translate_value(excel_col, self.defaults.get(excel_col, ""))
|
return translate_value(excel_col, self.defaults.get(excel_col, ""))
|
||||||
|
|
||||||
|
# ─── 基础设施注入(由外部预先初始化)──────────────────────
|
||||||
|
|
||||||
|
def inject_infrastructure(self, sess: PickupSession, bridge: TokenBridge, api: PickupAPI):
|
||||||
|
"""注入已初始化的 session / bridge / api,跳过 start/begin_processing。"""
|
||||||
|
self._sess = sess
|
||||||
|
self._bridge = bridge
|
||||||
|
self._api = api
|
||||||
|
self._infra_injected = True
|
||||||
|
self._dlog("基础设施已从外部注入")
|
||||||
|
|
||||||
# ─── 生命周期 ────────────────────────────────────────────
|
# ─── 生命周期 ────────────────────────────────────────────
|
||||||
|
|
||||||
async def start(self) -> None:
|
async def start(self) -> None:
|
||||||
"""启动 token bridge 并初始化 HTTP 会话。"""
|
"""启动 token bridge 并初始化 HTTP 会话。"""
|
||||||
|
if self._infra_injected:
|
||||||
|
self._dlog("start 跳过(基础设施已注入)")
|
||||||
|
return
|
||||||
|
|
||||||
self._ulog("启动 HTTP API 引擎...")
|
self._ulog("启动 HTTP API 引擎...")
|
||||||
|
|
||||||
# 启动 token bridge
|
# 启动 token bridge
|
||||||
@@ -364,6 +379,11 @@ class PickupEngine:
|
|||||||
if not self._sess:
|
if not self._sess:
|
||||||
raise RuntimeError("未启动")
|
raise RuntimeError("未启动")
|
||||||
|
|
||||||
|
if self._infra_injected:
|
||||||
|
self._dlog("begin_processing 跳过(基础设施已注入)")
|
||||||
|
self._ulog("后台自动化已就绪")
|
||||||
|
return
|
||||||
|
|
||||||
self._ulog("加载页面资源...")
|
self._ulog("加载页面资源...")
|
||||||
resources = self._sess.fetch_page_resources()
|
resources = self._sess.fetch_page_resources()
|
||||||
|
|
||||||
@@ -414,6 +434,12 @@ class PickupEngine:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# 外部注入的基础设施不销毁,由外部管理生命周期
|
||||||
|
if self._infra_injected:
|
||||||
|
self._dlog("跳过 bridge 销毁(基础设施由外部管理)")
|
||||||
|
self._clear_batch_cache()
|
||||||
|
return
|
||||||
|
|
||||||
# 停止 token bridge
|
# 停止 token bridge
|
||||||
if self._bridge:
|
if self._bridge:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
import sys
|
import sys
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
LEVEL = -1 # 编译时修改此值
|
LEVEL = 2 # 编译时修改此值
|
||||||
|
|
||||||
_gui_callback = None # GUI 注册的回调: (msg: str) -> None
|
_gui_callback = None # GUI 注册的回调: (msg: str) -> None
|
||||||
|
|
||||||
|
|||||||
@@ -128,8 +128,26 @@ class App:
|
|||||||
self.fv_sender_addr = StringVar()
|
self.fv_sender_addr = StringVar()
|
||||||
self.fv_force_sender_addr = BooleanVar()
|
self.fv_force_sender_addr = BooleanVar()
|
||||||
|
|
||||||
|
# 搜索基础设施(懒加载)
|
||||||
|
self._search_bridge = None
|
||||||
|
self._search_sess = None
|
||||||
|
self._search_api = None
|
||||||
|
self._loading_config = False
|
||||||
|
self._search_debounce = None
|
||||||
|
|
||||||
|
# 产品/客户预缓存(batch 期间复用)
|
||||||
|
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._build_ui()
|
self._build_ui()
|
||||||
|
|
||||||
|
# 监听固定值输入框变化,触发产品/客户搜索
|
||||||
|
self.fv_product.trace_add("write", self._on_product_change)
|
||||||
|
self.fv_customer.trace_add("write", self._on_customer_change)
|
||||||
|
|
||||||
# 关闭窗口时清理
|
# 关闭窗口时清理
|
||||||
self.root.protocol("WM_DELETE_WINDOW", self._on_close)
|
self.root.protocol("WM_DELETE_WINDOW", self._on_close)
|
||||||
|
|
||||||
@@ -268,6 +286,7 @@ class App:
|
|||||||
def _load_config(self):
|
def _load_config(self):
|
||||||
"""加载配置文件。首次运行时从 _MEIPASS 复制到 exe 目录。"""
|
"""加载配置文件。首次运行时从 _MEIPASS 复制到 exe 目录。"""
|
||||||
try:
|
try:
|
||||||
|
self._loading_config = True
|
||||||
config_path = _get_config_path()
|
config_path = _get_config_path()
|
||||||
# 打包模式首次运行:从 _MEIPASS 复制 config.json 到可写目录
|
# 打包模式首次运行:从 _MEIPASS 复制 config.json 到可写目录
|
||||||
if getattr(sys, 'frozen', False) and not os.path.exists(config_path):
|
if getattr(sys, 'frozen', False) and not os.path.exists(config_path):
|
||||||
@@ -292,6 +311,7 @@ class App:
|
|||||||
self.fv_force_sender_name.set(fv.get("寄件人", {}).get("force", False))
|
self.fv_force_sender_name.set(fv.get("寄件人", {}).get("force", False))
|
||||||
self.fv_sender_addr.set(fv.get("寄件地址", {}).get("value", ""))
|
self.fv_sender_addr.set(fv.get("寄件地址", {}).get("value", ""))
|
||||||
self.fv_force_sender_addr.set(fv.get("寄件地址", {}).get("force", False))
|
self.fv_force_sender_addr.set(fv.get("寄件地址", {}).get("force", False))
|
||||||
|
self._loading_config = False
|
||||||
|
|
||||||
# 检查已保存的登录会话
|
# 检查已保存的登录会话
|
||||||
self._check_session_status()
|
self._check_session_status()
|
||||||
@@ -349,6 +369,189 @@ class App:
|
|||||||
)
|
)
|
||||||
self.result_tree.insert("", "end", values=values)
|
self.result_tree.insert("", "end", values=values)
|
||||||
|
|
||||||
|
# ─── 产品/客户搜索 ──────────────────────────────────────
|
||||||
|
|
||||||
|
def _init_search_infra(self):
|
||||||
|
"""在登录后(或重新登录后)初始化搜索基础设施(token bridge + session + api)。"""
|
||||||
|
# 销毁旧的基础设施(可能是过期 cookies 的 session)
|
||||||
|
if self._search_bridge:
|
||||||
|
self._search_bridge.stop()
|
||||||
|
self._search_bridge = None
|
||||||
|
self._search_sess = None
|
||||||
|
self._search_api = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
from pickup_api import TokenBridge, PickupSession, PickupAPI
|
||||||
|
config = load_config(_get_config_path())
|
||||||
|
url = config.get("system", {}).get("url", "https://10.4.188.1/pickup-web/a/postwaybill/main")
|
||||||
|
|
||||||
|
self._search_bridge = TokenBridge()
|
||||||
|
self._search_bridge.start()
|
||||||
|
self._search_sess = PickupSession(url)
|
||||||
|
session_path = _get_session_path()
|
||||||
|
if not self._search_sess.load_session(session_path):
|
||||||
|
self._search_bridge.stop()
|
||||||
|
self._search_bridge = None
|
||||||
|
self._search_sess = None
|
||||||
|
self._log("[error] 搜索初始化失败:未找到登录会话")
|
||||||
|
return
|
||||||
|
resources = self._search_sess.fetch_page_resources()
|
||||||
|
minimal_html = (
|
||||||
|
f"<html><head><script>"
|
||||||
|
f"$_ts=window['$_ts']||{{}};"
|
||||||
|
f"$_ts.nsd={resources['ts_nsd']};"
|
||||||
|
f'$_ts.cd="{resources["ts_cd"]}";'
|
||||||
|
f"</script></head><body></body></html>"
|
||||||
|
)
|
||||||
|
self._search_bridge.initialize(minimal_html, resources["scripts"], resources["base_url"])
|
||||||
|
self._search_api = PickupAPI(self._search_sess, self._search_bridge)
|
||||||
|
try:
|
||||||
|
self._search_api.query_ecommerce("B")
|
||||||
|
self._search_api.query_deliver_date()
|
||||||
|
self._search_api.check_sheet_warning()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"[error] 搜索初始化失败: {e}")
|
||||||
|
if self._search_bridge:
|
||||||
|
self._search_bridge.stop()
|
||||||
|
self._search_bridge = None
|
||||||
|
self._search_sess = None
|
||||||
|
|
||||||
|
def _on_product_change(self, *args):
|
||||||
|
if self._loading_config:
|
||||||
|
return
|
||||||
|
if self._search_debounce:
|
||||||
|
self.root.after_cancel(self._search_debounce)
|
||||||
|
keyword = self.fv_product.get().strip()
|
||||||
|
if not keyword or len(keyword) < 1:
|
||||||
|
return
|
||||||
|
self._search_debounce = self.root.after(300, lambda: self._do_product_search(keyword))
|
||||||
|
|
||||||
|
def _on_customer_change(self, *args):
|
||||||
|
if self._loading_config:
|
||||||
|
return
|
||||||
|
if self._search_debounce:
|
||||||
|
self.root.after_cancel(self._search_debounce)
|
||||||
|
keyword = self.fv_customer.get().strip()
|
||||||
|
if not keyword or len(keyword) < 1:
|
||||||
|
return
|
||||||
|
self._search_debounce = self.root.after(300, lambda: self._do_customer_search(keyword))
|
||||||
|
|
||||||
|
def _do_product_search(self, keyword: str):
|
||||||
|
if self._search_api is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
candidates = self._search_api.query_product(keyword)
|
||||||
|
if not candidates:
|
||||||
|
return
|
||||||
|
names = [c.get("productName", "") for c in candidates]
|
||||||
|
self._show_search_dropdown(
|
||||||
|
"选择产品", names,
|
||||||
|
lambda idx: self._on_product_selected(candidates[idx]),
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"[error] 产品搜索失败: {e}")
|
||||||
|
|
||||||
|
def _do_customer_search(self, keyword: str):
|
||||||
|
if self._search_api is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
candidates = self._search_api.query_customer(keyword)
|
||||||
|
if not candidates:
|
||||||
|
return
|
||||||
|
names = [
|
||||||
|
c.get("customerFullName", c.get("customerName", ""))
|
||||||
|
for c in candidates
|
||||||
|
]
|
||||||
|
self._show_search_dropdown(
|
||||||
|
"选择客户", names,
|
||||||
|
lambda idx: self._on_customer_selected(candidates[idx]),
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"[error] 客户搜索失败: {e}")
|
||||||
|
|
||||||
|
def _show_search_dropdown(self, title: str, options: list[str], on_select):
|
||||||
|
"""在屏幕中央弹出下拉选择窗口。"""
|
||||||
|
popup = Toplevel(self.root)
|
||||||
|
popup.title(title)
|
||||||
|
popup.geometry("360x260")
|
||||||
|
popup.transient(self.root)
|
||||||
|
popup.grab_set()
|
||||||
|
popup.attributes("-topmost", True)
|
||||||
|
|
||||||
|
Label(popup, text=title, font=("", 10, "bold"), pady=6).pack()
|
||||||
|
|
||||||
|
frame = Frame(popup)
|
||||||
|
frame.pack(fill="both", expand=True, padx=10, pady=5)
|
||||||
|
listbox = Listbox(frame, font=("", 10))
|
||||||
|
scroll = Scrollbar(frame, orient=VERTICAL, command=listbox.yview)
|
||||||
|
listbox.configure(yscrollcommand=scroll.set)
|
||||||
|
scroll.pack(side=RIGHT, fill=Y)
|
||||||
|
listbox.pack(side=LEFT, fill=BOTH, expand=True)
|
||||||
|
for opt in options:
|
||||||
|
listbox.insert(END, opt)
|
||||||
|
|
||||||
|
chosen = [False]
|
||||||
|
|
||||||
|
def select():
|
||||||
|
sel = listbox.curselection()
|
||||||
|
if sel:
|
||||||
|
chosen[0] = True
|
||||||
|
popup.destroy()
|
||||||
|
on_select(sel[0])
|
||||||
|
|
||||||
|
def cancel():
|
||||||
|
popup.destroy()
|
||||||
|
|
||||||
|
listbox.bind("<Double-1>", lambda e: select())
|
||||||
|
listbox.bind("<Return>", lambda e: select())
|
||||||
|
listbox.bind("<Escape>", lambda e: cancel())
|
||||||
|
listbox.focus_set()
|
||||||
|
if listbox.size() > 0:
|
||||||
|
listbox.selection_set(0)
|
||||||
|
|
||||||
|
btn_f = Frame(popup)
|
||||||
|
btn_f.pack(pady=8)
|
||||||
|
Button(btn_f, text="选择", command=select, bg="#4CAF50", fg="white", width=8).pack(side=LEFT, padx=5)
|
||||||
|
Button(btn_f, text="取消", command=cancel, width=8).pack(side=LEFT, padx=5)
|
||||||
|
|
||||||
|
popup.protocol("WM_DELETE_WINDOW", cancel)
|
||||||
|
popup.wait_window()
|
||||||
|
|
||||||
|
def _on_product_selected(self, choice: dict):
|
||||||
|
"""产品选中:缓存选择并获取详情。"""
|
||||||
|
self._product_cached_choice = choice
|
||||||
|
self.fv_product.set(choice.get("productName", ""))
|
||||||
|
# 获取产品详情
|
||||||
|
try:
|
||||||
|
biz_id = choice.get("productId", "")
|
||||||
|
biz_no = choice.get("bizProductNo", "")
|
||||||
|
if biz_id and biz_no and self._search_api:
|
||||||
|
self._product_cached_detail = self._search_api.select_product(biz_id, biz_no)
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"[error] 获取产品详情失败: {e}")
|
||||||
|
self._product_cached_detail = {}
|
||||||
|
|
||||||
|
def _on_customer_selected(self, choice: dict):
|
||||||
|
"""客户选中:缓存选择并获取详情和寄件人实名。"""
|
||||||
|
self._customer_cached_choice = choice
|
||||||
|
self.fv_customer.set(choice.get("customerFullName", choice.get("customerName", "")))
|
||||||
|
# 获取客户详情
|
||||||
|
try:
|
||||||
|
cust_id = choice.get("customerId", "")
|
||||||
|
if cust_id and self._search_api:
|
||||||
|
detail = self._search_api.select_customer(str(cust_id))
|
||||||
|
if isinstance(detail, dict) and "_text" not in detail:
|
||||||
|
self._customer_cached_detail = detail
|
||||||
|
base = detail.get("baseCustome", {}) if isinstance(detail.get("baseCustome"), dict) else {}
|
||||||
|
sender_no = base.get("businessCode") or base.get("customerSubCode") or ""
|
||||||
|
if sender_no:
|
||||||
|
self._sender_realname_cached = self._search_api.find_sender_history(str(sender_no))
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"[error] 获取客户详情失败: {e}")
|
||||||
|
self._customer_cached_detail = {}
|
||||||
|
|
||||||
# ─── 文件选择 ───────────────────────────────────────────
|
# ─── 文件选择 ───────────────────────────────────────────
|
||||||
|
|
||||||
def _pick_file(self):
|
def _pick_file(self):
|
||||||
@@ -365,6 +568,8 @@ class App:
|
|||||||
global _engine
|
global _engine
|
||||||
if _engine:
|
if _engine:
|
||||||
_engine.request_stop()
|
_engine.request_stop()
|
||||||
|
if self._search_bridge:
|
||||||
|
self._search_bridge.stop()
|
||||||
self.root.destroy()
|
self.root.destroy()
|
||||||
|
|
||||||
# ─── 控制按钮 ───────────────────────────────────────────
|
# ─── 控制按钮 ───────────────────────────────────────────
|
||||||
@@ -389,10 +594,10 @@ class App:
|
|||||||
def do_save():
|
def do_save():
|
||||||
try:
|
try:
|
||||||
n = parse_json_and_save(json_text.get("1.0", "end-1c"))
|
n = parse_json_and_save(json_text.get("1.0", "end-1c"))
|
||||||
self._log(f"已保存 {n} 个 cookies 到 session.json")
|
|
||||||
self.engine_status_var.set("已登录")
|
self.engine_status_var.set("已登录")
|
||||||
self.status_label.configure(fg="green")
|
self.status_label.configure(fg="green")
|
||||||
self._check_session_status()
|
self._check_session_status()
|
||||||
|
self._init_search_infra()
|
||||||
win.destroy()
|
win.destroy()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
messagebox.showerror("解析失败", f"JSON 解析失败:\n{e}", parent=win)
|
messagebox.showerror("解析失败", f"JSON 解析失败:\n{e}", parent=win)
|
||||||
@@ -489,6 +694,20 @@ class App:
|
|||||||
confirm_callback=self._confirm_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
|
||||||
|
|
||||||
self._update_status("启动中...")
|
self._update_status("启动中...")
|
||||||
try:
|
try:
|
||||||
await _engine.start()
|
await _engine.start()
|
||||||
@@ -498,7 +717,7 @@ class App:
|
|||||||
await _engine.stop()
|
await _engine.stop()
|
||||||
return
|
return
|
||||||
|
|
||||||
# 验证 engine.start() 中已加载了 session,且包含关键 cookie
|
# 验证已加载了 session,且包含关键 cookie
|
||||||
if not _engine._sess or not _engine._sess._http.cookies:
|
if not _engine._sess or not _engine._sess._http.cookies:
|
||||||
self._log_async("[提示] 未找到登录会话,请点击'登录'按钮完成登录后再开始处理")
|
self._log_async("[提示] 未找到登录会话,请点击'登录'按钮完成登录后再开始处理")
|
||||||
self._update_status("未登录")
|
self._update_status("未登录")
|
||||||
@@ -564,7 +783,15 @@ class App:
|
|||||||
_engine = None
|
_engine = None
|
||||||
self._set_controls_state("stopped")
|
self._set_controls_state("stopped")
|
||||||
self.status_var.set("就绪")
|
self.status_var.set("就绪")
|
||||||
self._log("处理结束")
|
# 清空产品/客户输入框(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
|
||||||
|
|
||||||
def _pause(self):
|
def _pause(self):
|
||||||
global _engine
|
global _engine
|
||||||
|
|||||||
Reference in New Issue
Block a user