diff --git a/app/amazon/__pycache__/approve.cpython-39.pyc b/app/amazon/__pycache__/approve.cpython-39.pyc index c2be601..d43ad6e 100644 Binary files a/app/amazon/__pycache__/approve.cpython-39.pyc and b/app/amazon/__pycache__/approve.cpython-39.pyc differ diff --git a/app/amazon/__pycache__/del_brand.cpython-39.pyc b/app/amazon/__pycache__/del_brand.cpython-39.pyc index 0bfbfd5..34d4212 100644 Binary files a/app/amazon/__pycache__/del_brand.cpython-39.pyc and b/app/amazon/__pycache__/del_brand.cpython-39.pyc differ diff --git a/app/amazon/__pycache__/main.cpython-39.pyc b/app/amazon/__pycache__/main.cpython-39.pyc index fe242db..8f24b08 100644 Binary files a/app/amazon/__pycache__/main.cpython-39.pyc and b/app/amazon/__pycache__/main.cpython-39.pyc differ diff --git a/app/amazon/approve.py b/app/amazon/approve.py index dc9d839..5efc458 100644 --- a/app/amazon/approve.py +++ b/app/amazon/approve.py @@ -7,6 +7,12 @@ from config import runing_task, runing_shop from datetime import datetime from amazon.del_brand import AmamzonBase, kill_process +# 导入 webview 用于前端通知 +try: + import webview +except ImportError: + webview = None + class AmzoneApprove(AmamzonBase): def SwitchPage(self): @@ -190,8 +196,8 @@ class AmzoneApprove(AmamzonBase): num = 0 while 1: - if num > 3: #测试 - return + # if num > 3: #测试 + # return # 等待加载完成 load_ele = self.tab.eles("xpath://div[contains(@class,'Loader-module__loader')]",timeout=5) if len(load_ele) > 0: @@ -202,14 +208,17 @@ class AmzoneApprove(AmamzonBase): page_pamel = self.tab.eles('xpath://kat-pagination',timeout=5) if len(page_pamel) > 0: current_page = page_pamel[0].sr('xpath:.//ul[@class="pages"]//li[@aria-current="true"]').text - print(f"开始处理,当前页码: {current_page}") + print(f"【current_page】开始处理,当前页码: {current_page}") - sku_ls = self.tab.eles("xpath://div[@data-sku]",timeout=3) + sku_ls = self.tab.eles("xpath://div[@data-sku]",timeout=10) print(f"获取到 {len(sku_ls)}") - for sku_ele in sku_ls[0:2]: + # for sku_ele in sku_ls[0:2]: + for sku_ele in sku_ls: # solve_problem = sku_ele.eles('xpath:.//kat-link[@label="解决商品信息问题"]') - solve_problem = sku_ele.eles('xpath:.//kat-link[@label="解决商品信息问题"]|.//kat-link[@label="修复被禁止显示的商品"]|//kat-link[@label="解决商品移除风险"]') + solve_problem = sku_ele.eles('xpath:.//kat-link[@label="解决商品信息问题"]|.//kat-link[@label="修复被禁止显示的商品"]|.//kat-link[@label="解决商品移除风险"]') if len(solve_problem) == 0: + print(f"{asin},没有处理入口,不处理") + yield (asin,"没有处理入口,不处理") continue asin = sku_ele.ele('xpath:.//div[contains(@class,"JanusSplitBox-module__container")]//div[contains(@class,"JanusSplitBox-module__panel--") and contains(string(.),"ASIN")]/..//div[last()]').text print(f"ASIN {asin} 存在问题,正在点击解决...") @@ -235,9 +244,9 @@ class AmzoneApprove(AmamzonBase): # 解决商品信息违规问题 按钮 kat_box_ls = action_panel.eles('xpath:.//kat-box[@variant="white"]',timeout=5) - if len(kat_box_ls) == 0: - print(f"ASIN {asin} 的操作面板中未找到选项,跳过...") - continue + # if len(kat_box_ls) == 0: + # print(f"ASIN {asin} 的操作面板中未找到选项,跳过...") + # continue print(f"ASIN {asin}需要处理的有:{len(kat_box_ls)}个问题.") # 解决商品信息违规问题 按钮,有多少个都要处理 for i in range(len(kat_box_ls)): @@ -284,7 +293,7 @@ class AmzoneApprove(AmamzonBase): kat_box_ls = action_panel.eles('xpath:.//kat-box[@variant="white"]', timeout=5) # 修复商品信息 - repair_product = action_panel.eles('xpath://kat-button[@class="action-button"]',timeout=5) + repair_product = action_panel.eles('xpath:.//kat-button[@class="action-button"]',timeout=5) if len(repair_product) > 0: print(f"ASIN {asin} 存在修复商品信息按钮,正在点击...") self.clear_tab() @@ -316,7 +325,7 @@ class AmzoneApprove(AmamzonBase): break next_page_btn.click() num += 1 - print(f"正在翻页,已翻 {num} 页...") + # print(f"正在翻页,已翻 {num} 页...") class ApproveTask: @@ -330,6 +339,75 @@ class ApproveTask: "UK": "英国" } + @staticmethod + def show_notification(message: str, message_type: str = "error"): + """显示 pywebview 顶层通知(5秒后自动消失) + + Args: + message: 通知消息 + message_type: 消息类型 (success/warning/error/info) + """ + if webview and webview.windows: + try: + # 转义单引号,防止 JavaScript 语法错误 + safe_message = message.replace("'", "\\'").replace('"', '\\"').replace('\n', '\\n') + + # 设置通知样式颜色 + color_map = { + 'success': '#67C23A', + 'warning': '#E6A23C', + 'error': '#F56C6C', + 'info': '#909399' + } + bg_color = color_map.get(message_type, '#F56C6C') + + # 构建前端通知的 JavaScript 代码 - 创建原生 HTML 通知 + js_code = f""" + (function() {{ + // 移除已存在的通知 + var existingNotif = document.getElementById('pywebview-notification'); + if (existingNotif) {{ + existingNotif.remove(); + }} + + // 创建通知容器 + var notif = document.createElement('div'); + notif.id = 'pywebview-notification'; + notif.style.cssText = 'position: fixed; top: 20px; left: 50%; transform: translateX(-50%); ' + + 'background: {bg_color}; color: white; padding: 12px 20px; border-radius: 4px; ' + + 'box-shadow: 0 2px 12px rgba(0,0,0,0.3); z-index: 99999; font-size: 14px; ' + + 'max-width: 600px; word-wrap: break-word; display: flex; align-items: center; gap: 10px;'; + + // 添加消息内容 + var msgSpan = document.createElement('span'); + msgSpan.textContent = '{safe_message}'; + notif.appendChild(msgSpan); + + // 添加关闭按钮 + var closeBtn = document.createElement('span'); + closeBtn.innerHTML = '×'; + closeBtn.style.cssText = 'cursor: pointer; font-size: 18px; font-weight: bold; margin-left: 10px;'; + closeBtn.onclick = function() {{ notif.remove(); }}; + notif.appendChild(closeBtn); + + // 添加到页面 + document.body.appendChild(notif); + + // 5秒后自动消失 + setTimeout(function() {{ + if (notif && notif.parentNode) {{ + notif.style.transition = 'opacity 0.3s'; + notif.style.opacity = '0'; + setTimeout(function() {{ notif.remove(); }}, 300); + }} + }}, 5000); + }})(); + """ + # 在第一个窗口中执行 JavaScript + webview.windows[0].evaluate_js(js_code) + except Exception as e: + print(f"显示通知失败: {str(e)}") + """ 处理流程: 1、解析任务json,提取对应的companyName 组组装user_info实例化 AmzoneApprove @@ -385,6 +463,8 @@ class ApproveTask: """ from datetime import datetime timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + if level == "ERROR": + self.show_notification(message, "error") print(f"[{timestamp}] [ApproveTask] [{level}] {message}") def process_task(self, task_data: dict): @@ -439,6 +519,7 @@ class ApproveTask: shop_name = shop_item.get("shopName", "未知店铺") self.log(f"[{idx}/{len(items)}] 开始处理店铺: {shop_name}") + self.show_notification(f"开始处理店铺: {shop_name}", "info") try: self.process_shop(shop_item, country_codes, task_id,risk_listing_filter) @@ -502,11 +583,11 @@ class ApproveTask: self.log(f"尝试打开店铺 {shop_name} (第 {retry + 1}/{max_retries} 次)") # 如果不是第一次尝试,先杀进程 - if retry > 0: - self.log("重试前先杀掉浏览器进程...") - kill_process("v6") - kill_process("v5") - time.sleep(2) + # if retry > 0: + # self.log("重试前先杀掉浏览器进程...") + # kill_process("v6") + # kill_process("v5") + # time.sleep(2) # 组装用户信息并创建驱动 user_info = { @@ -522,7 +603,8 @@ class ApproveTask: else: self.log(f"打开店铺失败: {browser}", "WARNING") driver = None - + # 判断是否需要登录 + except Exception as e: import traceback self.log(f"打开店铺异常: {str(e)}", "ERROR") @@ -535,7 +617,8 @@ class ApproveTask: # 检查是否成功打开 if not driver or not browser or browser == "店铺不存在": - self.log(f"店铺 {shop_name} 打开失败,已重试 {max_retries} 次,跳过该店铺", "ERROR") + error_msg = f"店铺 {shop_name} 打开失败,已重试 {max_retries} 次,跳过该店铺" + self.log(error_msg, "ERROR") # 从执行列表中移除 if shop_name in runing_shop: del runing_shop[shop_name] @@ -589,7 +672,9 @@ class ApproveTask: # 转换国家代码为中文名称 country_name = self.country_info.get(country_code, country_code) - self.log(f"开始处理国家: {country_name} ({country_code})") + info_mes = f"开始处理国家: {country_name} ({country_code})" + self.log(info_mes) + self.show_notification(info_mes, "info") # 更新当前处理的国家 if task_id in runing_task: @@ -630,7 +715,8 @@ class ApproveTask: # 如果切换失败,直接返回 if not switch_success: - self.log(f"切换到国家 {country_name} 失败,已重试 {max_retries} 次,跳过该国家", "ERROR") + error_message = f"切换到国家 {country_name} 失败,已重试 {max_retries} 次,跳过该国家" + self.log(error_message, "ERROR") if task_id in runing_task: runing_task[task_id]["processed_countries"] += 1 return @@ -819,3 +905,5 @@ if __name__ == '__main__': print(f"ASIN {asin} 的处理结果: {status}") print("已完成操作") + # for i in range(0): + # print(i) \ No newline at end of file diff --git a/app/amazon/backend_api.py b/app/amazon/backend_api.py new file mode 100644 index 0000000..e69de29 diff --git a/app/amazon/del_brand.py b/app/amazon/del_brand.py index 301062c..0ff25f6 100644 --- a/app/amazon/del_brand.py +++ b/app/amazon/del_brand.py @@ -10,6 +10,7 @@ import os from typing import Literal from DrissionPage import Chromium from DrissionPage.common import By +from DrissionPage._pages.chromium_tab import ChromiumTab def kill_process(version: Literal["v5", "v6"]): @@ -33,7 +34,7 @@ class ZiniaoDriver: self.socket_port = socket_port self.client_path = None self.browser = None - self.tab = None + self.tab: ChromiumTab = None self.store_id = None def get_zinaio_exe(self, protocol_name: str = "superbrowser"): @@ -136,11 +137,11 @@ class ZiniaoDriver: print(r) return r.get("browserList") elif str(r.get("statusCode")) == "-10003": - print(f"login Err {json.dumps(r, ensure_ascii=False)}") - exit() + print(f"【get_browser_list】登录失败 {json.dumps(r, ensure_ascii=False)}") + # exit() else: - print(f"Fail {json.dumps(r, ensure_ascii=False)} ") - exit() + print(f"【get_browser_list】失败 {json.dumps(r, ensure_ascii=False)} ") + # exit() def open_store(self, store_info, isWebDriverReadOnlyMode=0, isprivacy=0, isHeadless=0, cookieTypeSave=0, jsInfo=""): @@ -188,11 +189,11 @@ class ZiniaoDriver: if str(r.get("statusCode")) == "0": return r elif str(r.get("statusCode")) == "-10003": - print(f"login Err {json.dumps(r, ensure_ascii=False)}") - exit() + raise RuntimeError(f"【open_store】登录失败 {json.dumps(r, ensure_ascii=False)}") + # exit() else: - print(f"Fail {json.dumps(r, ensure_ascii=False)} ") - exit() + raise RuntimeError(f"【open_store】失败 {json.dumps(r, ensure_ascii=False)} ") + # exit() def get_browser(self, port) -> Chromium: """ @@ -209,6 +210,17 @@ class ZiniaoDriver: def start_client(self): """启动紫鸟客户端""" + + # 检查端口是否已经启动 + try: + url = f'http://127.0.0.1:{self.socket_port}' + response = requests.get(url, timeout=2) + print(f"端口 {self.socket_port} 已经启动,跳过启动客户端操作") + return + except (requests.exceptions.ConnectionError, requests.exceptions.Timeout): + # 端口未启动,继续执行启动操作 + print(f"端口 {self.socket_port} 未启动,开始启动客户端") + self.client_path = self.get_zinaio_exe("superbrowserv6").strip('"') print(self.client_path) cmd = [self.client_path, '--run_type=web_driver', '--ipc_type=http', @@ -261,7 +273,8 @@ class ZiniaoDriver: print("ip检测页地址为空,请升级紫鸟浏览器到最新版") print(f"=====关闭店铺:{shop_name}=====") self.close_store(self.store_id) - exit() + # exit() + raise RuntimeError("没有IP检测地址,为了店铺安全不打开店铺") ip_usable = self.open_ip_check(self.browser, ip_check_url) if ip_usable: print("ip检测通过,打开店铺平台主页") @@ -298,11 +311,11 @@ class ZiniaoDriver: if str(r.get("statusCode")) == "0": return r elif str(r.get("statusCode")) == "-10003": - print(f"login Err {json.dumps(r, ensure_ascii=False)}") - exit() + raise RuntimeError(f"【close_store】登录失败 {json.dumps(r, ensure_ascii=False)}") + # exit() else: - print(f"Fail {json.dumps(r, ensure_ascii=False)} ") - exit() + raise RuntimeError(f"【close_store】失败: {json.dumps(r, ensure_ascii=False)} ") + # exit() def open_launcher_page(self, launcher_page: str, browser: Chromium = None): """ @@ -436,8 +449,38 @@ class AmamzonBase(ZiniaoDriver): print(f"切换国家时发生异常:{traceback.format_exc()}") return False + def need_login(self): + """ + 判断是否需要登录,部分国家可能需要登录后才能切换国家 + 处理流程: + """ + need_login_ele = self.tab.eles('xpath://h1[@class="a-spacing-small"]',timeout=5) + if len(need_login_ele) >0: + print("检测到需要登录元素") + return True + return False + + def login(self,password,username=""): + try: + pwd_input = self.tab.ele('xpath://input[@type="password"]',timeout=10) + pwd_input.input(password) + submit_btn = self.tab.ele('xpath://input[@id="signInSubmit"]',timeout=10) + submit_btn.click() + self.tab.wait.doc_loaded() + opt_code_input = self.tab.ele('xpath://input[@name="otpCode"]',timeout=10) + opt_code_input.wait.displayed(timeout=10,raise_err=False) + for _ in range(30): + if opt_code_input.value is not None and opt_code_input.value.strip() != "": + print("检测到验证码输入完成") + submit_btn = self.tab.ele('xpath://input[@id="auth-signin-button"]',timeout=10) + submit_btn.click() + self.tab.wait.doc_loaded() + return True + except Exception as e: + print("登录过程中发生异常:" + traceback.format_exc()) + return False class AmazoneDriver(AmamzonBase): """亚马逊专用驱动类,继承自ZiniaoDriver""" diff --git a/app/amazon/main.py b/app/amazon/main.py index 5314d3b..af45cf2 100644 --- a/app/amazon/main.py +++ b/app/amazon/main.py @@ -78,7 +78,7 @@ class TaskMonitor: except Exception as e: # 静默处理队列为空的超时,只记录真正的异常 - if "Empty" not in str(e): + if str(e) and "Empty" not in str(e): self.log(f"任务监控异常: {str(e)}", "ERROR") # 队列为空时不需要额外等待,直接继续循环 @@ -210,11 +210,11 @@ class TaskMonitor: self.log(f"尝试打开店铺 {shop_name} (第 {retry + 1}/{max_retries} 次)") # 如果不是第一次尝试,先杀进程 - if retry > 0: - self.log("重试前先杀掉浏览器进程...") - kill_process("v6") - kill_process("v5") - time.sleep(2) + # if retry > 0: + # self.log("重试前先杀掉浏览器进程...") + # kill_process("v6") + # kill_process("v5") + # time.sleep(2) # 创建驱动并打开店铺 user_info = { @@ -230,6 +230,27 @@ class TaskMonitor: else: self.log(f"打开店铺失败: {browser}", "WARNING") driver = None + + # 判断是否需要登录 + # need_login = driver.need_login() + # if need_login: + # self.log(f"店铺 {shop_name} 需要登录,正在登录...") + # password = "" + + # login_success = driver.login(password) + # if login_success: + # self.log(f"店铺 {shop_name} 登录成功,正在重新打开店铺...") + # browser = driver.open_shop(shop_name) + # if browser and browser != "店铺不存在": + # self.log(f"成功打开店铺 {shop_name} 登录后") + # break + # else: + # self.log(f"登录后打开店铺失败: {browser}", "WARNING") + # driver = None + # else: + # self.log(f"店铺 {shop_name} 登录失败", "WARNING") + # driver = None + except Exception as e: self.log(f"打开店铺异常: {str(e)}", "ERROR") diff --git a/app/blueprints/main.py b/app/blueprints/main.py index 82e5ce5..619f126 100644 --- a/app/blueprints/main.py +++ b/app/blueprints/main.py @@ -110,7 +110,10 @@ def proxy(path): if key.lower() in ['host', 'content-length', 'connection']: continue headers[key] = value - ignore_url = [f"{JAVA_API_BASE}/api/delete-brand/tasks/batch",f"{JAVA_API_BASE}/api/delete-brand/history"] + ignore_url = [f"{JAVA_API_BASE}/api/delete-brand/tasks/batch", + f"{JAVA_API_BASE}/api/delete-brand/history", + f"{JAVA_API_BASE}/api/product-risk-resolve/tasks/batch", + ] if target_url not in ignore_url: try: print("=============================") diff --git a/app/main.py b/app/main.py index 3bfc7d0..7a76502 100644 --- a/app/main.py +++ b/app/main.py @@ -11,7 +11,7 @@ from amazon.main import TaskMonitor os.makedirs(cache_path,exist_ok=True) if not debug: today = datetime.datetime.now().strftime("%Y_%m_%d") - sys.stdout = open(os.path.join(cache_path,f'{today}.log'), 'a',buffering=1) + sys.stdout = open(os.path.join(cache_path,f'{today}.log'), 'a',buffering=1,encoding='utf-8') sys.stderr = sys.stdout import webview @@ -20,6 +20,9 @@ import time import requests import subprocess +from amazon.del_brand import kill_process + + with open("version.txt","w",encoding="utf-8") as file: file.write(json.dumps({"version":version})) @@ -54,8 +57,15 @@ class WindowAPI: window.events.restored += lambda _: setattr(self, '_is_maximized', False) def close(self): - sys.exit(1) - self._window.destroy() + try: + kill_process('v6') + except Exception as e: + print(f"【退出前】关闭紫鸟浏览器进程异常: {str(e)}") + try: + self._window.destroy() + except: + pass + os._exit(0) # 强制退出整个进程,包括所有线程 def minimize(self): self._window.minimize() @@ -358,6 +368,23 @@ def start_task_monitor(): print(f"[{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 任务监控线程异常退出: {traceback.format_exc()}") +def on_window_closing(): + """窗口关闭事件处理器,执行与 close() 方法相同的清理逻辑""" + # 先关闭窗口,避免看起来卡着 + try: + if webview.windows: + webview.windows[0].destroy() + except: + pass + + # 然后执行清理逻辑 + try: + kill_process('v6') + except Exception as e: + print(f"【退出前】关闭紫鸟浏览器进程异常: {str(e)}") + os._exit(0) # 强制退出整个进程,包括所有线程 + + def main(): if not os.path.exists(cache_path): os.makedirs(cache_path,exist_ok=True) @@ -384,11 +411,15 @@ def main(): frameless=False, easy_drag=True ) + + # 为窗口关闭事件添加处理器 + window.events.closing += on_window_closing + api = WindowAPI(window) window.expose(api.close, api.minimize, api.maximize, api.toggle_maximize, generate_images, api.save_image, api.save_image_to_folder, api.select_folder, api.select_brand_xlsx_files, api.select_brand_folder, api.save_file_from_url, api.save_template_xlsx,api.save_template_zip,api.upload_file_to_java,api.open_external_url,api.enqueue_json,api.get_detail_del,api.save_file_from_url_new) webview.start( - debug=True, + debug=False, storage_path=cache_path, private_mode=False )