From 5b197900370b1820a60bb835805a779c3bd6a1bd Mon Sep 17 00:00:00 2001 From: hjq <770690987@qq.com> Date: Thu, 11 Jun 2026 15:58:56 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8A=93=E5=8F=96=E7=94=9F=E4=BA=A7=E5=B7=A5?= =?UTF-8?q?=E5=8D=95=EF=BC=8C=E8=B5=9A=E5=8F=96=E5=8F=91=E6=96=99=E5=BC=82?= =?UTF-8?q?=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/.gitignore | 5 + .idea/Datie.iml | 9 + .idea/misc.xml | 6 + .idea/modules.xml | 8 + .idea/vcs.xml | 7 + browser_login/analyze_duplicates.py | 42 + browser_login/auto_fetch_abnormal_report.py | 249 +++ browser_login/auto_navigate_report.py | 56 + browser_login/check_page.py | 18 + browser_login/check_tab.py | 9 + browser_login/click_report.py | 26 + browser_login/click_report_tab.py | 17 + browser_login/do_search.py | 45 + browser_login/do_search2.py | 52 + browser_login/enter_report.py | 36 + browser_login/enter_report2.py | 25 + browser_login/fetch_abnormal_report.py | 81 + .../fetch_basis_quality_incremental.py | 147 ++ browser_login/fetch_basis_quality_sample.py | 109 ++ browser_login/fetch_issue_receipt_details.py | 297 +++ .../fetch_issue_receipt_incremental.py | 297 +++ browser_login/fetch_work_orders.py | 107 ++ .../fetch_work_orders_incremental.py | 241 +++ browser_login/find_buttons.py | 13 + browser_login/find_report.py | 12 + browser_login/import_abnormal.py | 17 + browser_login/import_to_sqlite.py | 375 +++- browser_login/inspect_current.py | 18 + browser_login/inspect_menus.py | 17 + browser_login/inspect_report.py | 41 + browser_login/navigate_report.py | 43 + browser_login/search_report.py | 19 + browser_login/test_date.py | 16 + browser_login/test_fill_date.py | 91 + browser_login/test_vue_injection.py | 87 + page.html | 1637 +++++++++++++++++ web_ui/app.py | 210 ++- web_ui/templates/abnormal_report.html | 287 +++ web_ui/templates/home.html | 53 + web_ui/templates/work_orders.html | 171 ++ 40 files changed, 4942 insertions(+), 54 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/Datie.iml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 browser_login/analyze_duplicates.py create mode 100644 browser_login/auto_fetch_abnormal_report.py create mode 100644 browser_login/auto_navigate_report.py create mode 100644 browser_login/check_page.py create mode 100644 browser_login/check_tab.py create mode 100644 browser_login/click_report.py create mode 100644 browser_login/click_report_tab.py create mode 100644 browser_login/do_search.py create mode 100644 browser_login/do_search2.py create mode 100644 browser_login/enter_report.py create mode 100644 browser_login/enter_report2.py create mode 100644 browser_login/fetch_abnormal_report.py create mode 100644 browser_login/fetch_basis_quality_incremental.py create mode 100644 browser_login/fetch_basis_quality_sample.py create mode 100644 browser_login/fetch_issue_receipt_details.py create mode 100644 browser_login/fetch_issue_receipt_incremental.py create mode 100644 browser_login/fetch_work_orders.py create mode 100644 browser_login/fetch_work_orders_incremental.py create mode 100644 browser_login/find_buttons.py create mode 100644 browser_login/find_report.py create mode 100644 browser_login/import_abnormal.py create mode 100644 browser_login/inspect_current.py create mode 100644 browser_login/inspect_menus.py create mode 100644 browser_login/inspect_report.py create mode 100644 browser_login/navigate_report.py create mode 100644 browser_login/search_report.py create mode 100644 browser_login/test_date.py create mode 100644 browser_login/test_fill_date.py create mode 100644 browser_login/test_vue_injection.py create mode 100644 page.html create mode 100644 web_ui/templates/abnormal_report.html create mode 100644 web_ui/templates/work_orders.html diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..10b731c --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ diff --git a/.idea/Datie.iml b/.idea/Datie.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/.idea/Datie.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..89ee753 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..c67be3e --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..8306744 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/browser_login/analyze_duplicates.py b/browser_login/analyze_duplicates.py new file mode 100644 index 0000000..67ce04b --- /dev/null +++ b/browser_login/analyze_duplicates.py @@ -0,0 +1,42 @@ +import json +from collections import defaultdict +from config import OUTPUT_DIR + +filepath = OUTPUT_DIR / "issue_receipt_details_full.json" + +with open(filepath, 'r', encoding='utf-8') as f: + data = json.load(f) + +# 用来记录每个组合出现的次数和对应的列表索引 +seen = defaultdict(list) +null_keys = 0 + +for idx, item in enumerate(data): + wo = item.get("发料单号") + line = item.get("行号") + mat = item.get("物料代码") + + if not wo or not line or not mat: + null_keys += 1 + continue + + key = f"{wo}_{line}_{mat}" + seen[key].append(idx) + +duplicates = {k: v for k, v in seen.items() if len(v) > 1} + +print(f"总数据条数: {len(data)}") +print(f"缺失关键字段的数据条数: {null_keys}") +print(f"发现重复的组合数: {len(duplicates)}") +redundant_count = sum(len(v)-1 for v in duplicates.values()) +print(f"因重复而多出的冗余条数: {redundant_count}") + +# 打印前 5 个重复的例子 +count = 0 +for k, indices in duplicates.items(): + if count >= 5: + break + print(f"\n重复键 (发料单号_行号_物料代码): {k}") + print(f" 第一次出现在第 {indices[0] + 1} 条,最新状态: {data[indices[0]].get('状态')}") + print(f" 第二次出现在第 {indices[1] + 1} 条,最新状态: {data[indices[1]].get('状态')}") + count += 1 diff --git a/browser_login/auto_fetch_abnormal_report.py b/browser_login/auto_fetch_abnormal_report.py new file mode 100644 index 0000000..be64451 --- /dev/null +++ b/browser_login/auto_fetch_abnormal_report.py @@ -0,0 +1,249 @@ +import sys +import time +from pathlib import Path +import datetime +import calendar +import json +sys.path.insert(0, str(Path(__file__).parent)) +from login import get_page +from config import OUTPUT_DIR + +def navigate_to_report(page): + print("正在打开主页...") + page.get("https://yunmes.tftykj.cn/") + page.wait.load_start() + time.sleep(2) + + print("正在打开 生产工单发料异常检查报表...") + try: + m1 = page.ele('text=自定义报表管理') + if m1: + print("点击第一级...") + m1.click() + time.sleep(1) + + # 找到展开后的第二级 + for m in page.eles('text:自定义报表管理'): + try: + m.click() + except: + pass + time.sleep(1) + + for m in page.eles('text:自定义报表'): + if m.text == '自定义报表': + try: + m.click() + print("点击第三级...") + except: + pass + time.sleep(2) + + ele = page.ele('text:生产工单发料异常检查报表', timeout=5) + if ele: + print("找到报表行,选中...") + ele.parent('tag:tr').click() + time.sleep(0.5) + btn = page.ele('text=进入自定义报表') + if btn: + print("点击进入自定义报表...") + btn.click() + time.sleep(3) + print("成功进入报表!") + return True + else: + print("未找到进入按钮。") + return False + else: + print("未能找到 '生产工单发料异常检查报表'") + return False + except Exception as e: + print(f"执行导航过程中发生异常: {e}") + return False + +def fetch_report_data(page): + # Wait for the new tab to be ready + time.sleep(3) + target_tab = page.get_tab(page.latest_tab) + + # Wait for the label to appear + target_tab.ele('text:下单日期(开始)', timeout=10) + + now = datetime.datetime.now() + first_day = datetime.date(now.year, now.month, 1).strftime('%Y-%m-%d') + last_day = datetime.date(now.year, now.month, calendar.monthrange(now.year, now.month)[1]).strftime('%Y-%m-%d') + + print(f"设置下单日期为当月: {first_day} 至 {last_day},并清理发料情况过滤条件...") + + # 使用注入到全部 iframe 的 JS 强制执行 EasyUI 方法 + target_tab.run_js(f""" + var iframes = document.querySelectorAll('iframe'); + for(var j=0; j 0) {{ + win.$(startInputs[0]).datebox('setValue', '{first_day}'); + }} + + // 2. 设置结束日期 + var endInputs = doc.querySelectorAll('.input_EndValue.datebox-f'); + if (endInputs.length > 0) {{ + win.$(endInputs[0]).datebox('setValue', '{last_day}'); + }} + + // 3. 清理所有下拉框(包括发料情况) + var combos = doc.querySelectorAll('.combobox-f, .textbox-f'); + for(var i=0; i 0 else 1 + + # Import and save to database + try: + import import_to_sqlite + if items: + inserted = import_to_sqlite.import_abnormal_report_data(items) + total_inserted += inserted + print(f"✅ 成功将本页 {inserted} 条异常报表数据存入数据库") + except Exception as db_err: + print(f"❌ 保存异常报表数据到数据库失败: {db_err}") + + found_data = True + else: + print(f"[{datetime.datetime.now().strftime('%H:%M:%S')}] 数据结构不匹配。") + except Exception as e: + print(f"[{datetime.datetime.now().strftime('%H:%M:%S')}] 解析数据包出错: {e}") + pass + + if not found_data: + print(f"[{datetime.datetime.now().strftime('%H:%M:%S')}] 第 {current_page} 页等待了3秒,没有拦截到匹配的报表数据...") + + # 再给一次机会等3秒 + print("再等待3秒重试...") + time.sleep(3) + retry_packets = target_tab.listen.steps() + print(f"重试收集到 {len(retry_packets)} 个数据包。") + if not retry_packets: + print(f"彻底没有数据,停止抓取。") + break + else: + packets.extend(retry_packets) + # 重新让上面解析 + continue + + if current_page >= total_pages: + print(f"已到达最后一页 (共 {total_pages} 页),抓取完成!") + break + + print(f"准备抓取下一页 (第 {current_page + 1} 页)...") + time.sleep(1) + + # 尝试点击下一页 (同样需要穿透 iframe) + target_tab.run_js(""" + var iframes = document.querySelectorAll('iframe'); + for(var j=0; j {end_date_str}") + + page = get_page(port=9222) + all_clean_items = [] + + try: + log("INFO", f"正在回到主页起点: {HOME_URL}") + page.get(HOME_URL) + page.wait.load_start() + time.sleep(2) + + menus = [ + ("进入质量报表", 'xpath://*[@id="el-collapse-content-21"]/div/div/div/div[1]/div/div/div[6]/div') + ] + + log("INFO", "开始模拟人工点击左侧导航菜单...") + for name, xpath in menus: + ele = page.ele(xpath, timeout=5) + if ele: + try: ele.click() + except: page.run_js("arguments[0].click();", ele) + else: + log("ERR", f"找不到菜单元素: {name}") + return + + log("OK", "✅ 成功点开质量报表界面!") + time.sleep(2) + + # 开启普通的数据监听 + log("INFO", f"开启底层拦截网: {API_TARGET}") + page.listen.start(API_TARGET) + + # ========================================================= + # 循环翻页抓取逻辑 (测试模式:仅抓取前 3 页) + # ========================================================= + current_page = 1 + query_btn_xpath = 'xpath://*[@id="customTable-search-area"]/div[1]/div/div[1]/a[2]/span/span' + + while current_page <= 3: # 限制只抓取前 3 页用于测试 + # 1. 因为我们无法用 DrissionPage 的 listen 修改发送出去的 POST Data + # 我们直接在 Python 层发送一个 JS Fetch 请求,完全模拟原有的请求,但带上我们自己构造的 Payload! + log("INFO", f"正在通过底层 JS Fetch 强行注入带时间窗口的请求... (页码: {current_page})") + + # 注意:这里的 new_payload 必须转义所有的单双引号以适配 JS 字符串拼接 + base_payload = f"page={current_page}&rows=50&id=80&sqlFilter%5BfieldList%5D%5B0%5D%5Bid%5D=17647&sqlFilter%5BfieldList%5D%5B0%5D%5Bfield%5D=%E4%B8%8B%E5%8D%95%E6%97%A5%E6%9C%9F(%E7%BB%93%E6%9D%9F)&sqlFilter%5BfieldList%5D%5B0%5D%5BfieldTranslate%5D=%5B%E4%B8%8B%E5%8D%95%E6%97%A5%E6%9C%9F(%E7%BB%93%E6%9D%9F)%5D&sqlFilter%5BfieldList%5D%5B0%5D%5BstartValue%5D={encoded_end}&sqlFilter%5BfieldList%5D%5B0%5D%5BendValue%5D=&sqlFilter%5BfieldList%5D%5B0%5D%5BcompareEnum%5D=0&sqlFilter%5BfieldList%5D%5B0%5D%5BfieldDataType%5D=2&sqlFilter%5BfieldList%5D%5B0%5D%5BorderNumber%5D=&sqlFilter%5BfieldList%5D%5B0%5D%5BorderType%5D=0&sqlFilter%5BfieldList%5D%5B0%5D%5BisTimeLimit%5D=false&sqlFilter%5BfieldList%5D%5B0%5D%5BlimitLength%5D=0&sqlFilter%5BfieldList%5D%5B0%5D%5BdateType%5D=1&sqlFilter%5BfieldList%5D%5B0%5D%5BdateDefaultType%5D=0&sqlFilter%5BfieldList%5D%5B0%5D%5BisSqlField%5D=false&sqlFilter%5BfieldList%5D%5B0%5D%5Bcondition%5D=0&sqlFilter%5BfieldList%5D%5B0%5D%5BgetValue%5D=&sqlFilter%5BfieldList%5D%5B0%5D%5BbackgroundColor%5D=&sqlFilter%5BfieldList%5D%5B0%5D%5BfontColor%5D=&sqlFilter%5BfieldList%5D%5B0%5D%5BisSeachParam%5D=true&sqlFilter%5BfieldList%5D%5B0%5D%5BdefaultValue%5D=&sqlFilter%5BfieldList%5D%5B0%5D%5Bwidth%5D=&sqlFilter%5BfieldList%5D%5B0%5D%5BdefaultTime%5D=&sqlFilter%5BfieldList%5D%5B0%5D%5BsearchParamEnableVal%5D=0&sqlFilter%5BfieldList%5D%5B0%5D%5BoptionMode%5D=0&sqlFilter%5BfieldList%5D%5B1%5D%5Bid%5D=17646&sqlFilter%5BfieldList%5D%5B1%5D%5Bfield%5D=%E4%B8%8B%E5%8D%95%E6%97%A5%E6%9C%9F(%E5%BC%80%E5%A7%8B)&sqlFilter%5BfieldList%5D%5B1%5D%5BfieldTranslate%5D=%5B%E4%B8%8B%E5%8D%95%E6%97%A5%E6%9C%9F(%E5%BC%80%E5%A7%8B)%5D&sqlFilter%5BfieldList%5D%5B1%5D%5BstartValue%5D={encoded_start}&sqlFilter%5BfieldList%5D%5B1%5D%5BendValue%5D=&sqlFilter%5BfieldList%5D%5B1%5D%5BcompareEnum%5D=0&sqlFilter%5BfieldList%5D%5B1%5D%5BfieldDataType%5D=2&sqlFilter%5BfieldList%5D%5B1%5D%5BorderNumber%5D=&sqlFilter%5BfieldList%5D%5B1%5D%5BorderType%5D=0&sqlFilter%5BfieldList%5D%5B1%5D%5BisTimeLimit%5D=false&sqlFilter%5BfieldList%5D%5B1%5D%5BlimitLength%5D=0&sqlFilter%5BfieldList%5D%5B1%5D%5BdateType%5D=1&sqlFilter%5BfieldList%5D%5B1%5D%5BdateDefaultType%5D=0&sqlFilter%5BfieldList%5D%5B1%5D%5BisSqlField%5D=false&sqlFilter%5BfieldList%5D%5B1%5D%5Bcondition%5D=0&sqlFilter%5BfieldList%5D%5B1%5D%5BgetValue%5D=&sqlFilter%5BfieldList%5D%5B1%5D%5BbackgroundColor%5D=&sqlFilter%5BfieldList%5D%5B1%5D%5BfontColor%5D=&sqlFilter%5BfieldList%5D%5B1%5D%5BisSeachParam%5D=true&sqlFilter%5BfieldList%5D%5B1%5D%5BdefaultValue%5D=&sqlFilter%5BfieldList%5D%5B1%5D%5Bwidth%5D=&sqlFilter%5BfieldList%5D%5B1%5D%5BdefaultTime%5D=&sqlFilter%5BfieldList%5D%5B1%5D%5BsearchParamEnableVal%5D=1&sqlFilter%5BfieldList%5D%5B1%5D%5BoptionMode%5D=0&isAll=false" + + # 强行在页面中注入一个 Fetch 请求。由于在页面上下文中运行,它会自动带上所有的 Cookies 和 Auth Token! + fetch_js = f""" + fetch('/api/services/TfTechApi/SQLSolution/SearchCustomReportBySQL_Proxy', {{ + method: 'POST', + headers: {{ + 'accept': 'application/json, text/javascript, */*; q=0.01', + 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8', + 'x-requested-with': 'XMLHttpRequest' + }}, + body: '{base_payload}' + }}); + """ + page.run_js(fetch_js) + + # 2. 等待我们注入的请求响应 + packet = page.listen.wait(timeout=15) + if not packet: + log("ERR", f"第 {current_page} 页注入请求超时或未触发,中止抓取。") + break + + # 3. 解析数据 + body = packet.response.body + data = body if isinstance(body, (dict, list)) else json.loads(body) + + if isinstance(data, dict) and "result" in data: + # 检查 result 是否是字典,如果直接是列表则直接取用 + if isinstance(data["result"], dict): + items = data["result"].get("items", []) + elif isinstance(data["result"], list): + items = data["result"] + else: + items = [] + + if not items: + log("WARN", f"第 {current_page} 页返回了空列表,可能该时间段内无数据。") + break + + for item in items: + all_clean_items.append(item) + + log("OK", f"第 {current_page} 页清洗完成,累计提取 {len(all_clean_items)} 条数据。") + + if current_page % 10 == 0: + with open(SAVE_PATH, "w", encoding="utf-8") as f: + json.dump(all_clean_items, f, ensure_ascii=False, indent=2) + else: + log("ERR", f"第 {current_page} 页数据结构异常,中止。") + break + + current_page += 1 + + # 最终保存 + if all_clean_items: + with open(SAVE_PATH, "w", encoding="utf-8") as f: + json.dump(all_clean_items, f, ensure_ascii=False, indent=2) + log("OK", f"🎉 抓取完成!总计成功提取 {len(all_clean_items)} 条数据。") + log("OK", f"数据已保存至: {SAVE_PATH}") + + except Exception as e: + log("ERR", f"发生全局异常: {e}") + finally: + try: + page.listen.stop() + log("INFO", "🛑 已释放浏览器监听资源。") + except: + pass + +if __name__ == "__main__": + fetch_basis_quality_incremental() diff --git a/browser_login/fetch_basis_quality_sample.py b/browser_login/fetch_basis_quality_sample.py new file mode 100644 index 0000000..a724da3 --- /dev/null +++ b/browser_login/fetch_basis_quality_sample.py @@ -0,0 +1,109 @@ +""" +质量报表 (Basis Quality Report) - 样本抓取脚本 +目标: 模拟点击菜单进入页面,拦截 BasisQualityReport_GetValueFieldListNew_Proxy 接口,提取前 5 条数据进行结构分析。 +""" +import sys +import json +import time +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent)) +from login import get_page, log +from config import OUTPUT_DIR + +HOME_URL = "https://yunmes.tftykj.cn/" +API_TARGET = "SearchCustomReportBySQL_Proxy" +SAVE_PATH = OUTPUT_DIR / "basis_quality_sample.json" + +def fetch_basis_quality_sample(): + log("INFO", "=== 🧪 启动质量报表样本抓取 (前5条) ===") + page = get_page(port=9222) + + try: + log("INFO", f"正在回到主页起点: {HOME_URL}") + page.get(HOME_URL) + page.wait.load_start() + time.sleep(2) + + menus = [ + ("进入质量报表", 'xpath://*[@id="el-collapse-content-21"]/div/div/div/div[1]/div/div/div[6]/div') + ] + + # 核心修改:因为数据是一进页面就加载,所以必须在点击菜单【之前】就开始监听! + log("INFO", f"开启底层数据拦截网: {API_TARGET} (提前开启,以防错过初始加载)") + page.listen.start(API_TARGET) + + log("INFO", "开始模拟人工点击左侧导航菜单...") + for name, xpath in menus: + ele = page.ele(xpath, timeout=5) + if ele: + try: ele.click() + except: page.run_js("arguments[0].click();", ele) + else: + log("ERR", f"找不到菜单元素: {name}") + return + + log("OK", "✅ 成功点开质量报表界面!") + + # 尝试点击空白处隐藏可能遮挡的菜单 (根据实际情况可能需要调整或注释掉) + blank_xpath = 'xpath://*[@id="app"]/div/div[1]/div[2]/div[1]/div[2]/div[2]/div/div[1]/div' + blank_ele = page.ele(blank_xpath, timeout=3) + if blank_ele: + try: blank_ele.click() + except: pass + + log("INFO", "等待拦截初始加载的数据包...") + packet = page.listen.wait(timeout=15) + + if not packet: + log("ERR", "未能拦截到数据请求,可能网络超时。") + page.listen.stop() + return + + # ========================================================= + # 数据处理 + # ========================================================= + log("OK", f"🎉 成功拦截到数据!HTTP 状态码: {packet.response.status}") + body = packet.response.body + data = body if isinstance(body, (dict, list)) else json.loads(body) + + sample_items = [] + + if isinstance(data, dict) and "result" in data: + # 根据提供的 curl 参数 {"wageCalculationPlanId":80,"basisQualityReportId":23} + # 这个接口的返回结构可能与之前的 SearchList 不同,这里做个宽泛的判断 + items = data["result"] + + # 如果 result 直接是个列表,或者里面包着 items 列表 + if isinstance(items, dict) and "items" in items: + items = items["items"] + elif not isinstance(items, list): + # 如果既不是列表也不是包含 items 的字典,就把整个 result 放进去看看 + items = [items] + + log("INFO", f"本页包含 {len(items)} 条数据,准备提取前 5 条。") + + for item in items[:5]: + sample_items.append(item) + + with open(SAVE_PATH, "w", encoding="utf-8") as f: + json.dump(sample_items, f, ensure_ascii=False, indent=2) + log("OK", f"💾 样本提取完成!已保存 {len(sample_items)} 条记录至: {SAVE_PATH}") + else: + log("ERR", "返回的数据结构中找不到 'result' 节点。") + # 把原始结构存下来方便分析 + with open(SAVE_PATH, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + log("INFO", f"已将原始返回数据保存至: {SAVE_PATH} 以供分析。") + + except Exception as e: + log("ERR", f"发生全局异常: {e}") + finally: + try: + page.listen.stop() + log("INFO", "🛑 已释放浏览器监听资源。") + except: + pass + +if __name__ == "__main__": + fetch_basis_quality_sample() diff --git a/browser_login/fetch_issue_receipt_details.py b/browser_login/fetch_issue_receipt_details.py new file mode 100644 index 0000000..7b57940 --- /dev/null +++ b/browser_login/fetch_issue_receipt_details.py @@ -0,0 +1,297 @@ +""" +发料单报表 - 导航测试脚本 +目标: 模拟点击菜单,进入“发料单报表”页面。 +""" +import sys +import json +import time +import random +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent)) +from login import get_page, log +from config import OUTPUT_DIR + +HOME_URL = "https://yunmes.tftykj.cn/" +API_TARGET = "WorkOrdersDetailed_SearchListAll_Proxy" +SAVE_PATH = OUTPUT_DIR / "issue_receipt_details_full.json" + +def fetch_issue_receipt_details(): + log("INFO", "=== 🚀 启动发料单报表全量数据抓取 ===") + # 强制复用 9222 端口,不关闭浏览器 + page = get_page(port=9222) + + all_clean_items = [] + if SAVE_PATH.exists(): + try: + with open(SAVE_PATH, "r", encoding="utf-8") as f: + all_clean_items = json.load(f) + log("INFO", f"📦 已加载本地历史存档,包含 {len(all_clean_items)} 条数据。") + except Exception as e: + log("WARN", f"加载本地存档失败: {e},将从空列表开始。") + all_clean_items = [] + + try: + log("INFO", f"正在回到主页起点: {HOME_URL}") + page.get(HOME_URL) + page.wait.load_start() + time.sleep(2) + + menus = [ + ("第一层: 业务统计报表", 'xpath://*[@id="app"]/div/div[1]/div[1]/div[2]/div/div[1]/div/div[10]/div/p'), + ("第二层: 生产业务报表(推测)", 'xpath:/html/body/div[7]/div/div[1]/div/div[9]/div/p'), + ("第三层: 发料单报表", 'xpath:/html/body/div[8]/div/div[1]/div/div[6]/div/p') + ] + + log("INFO", "开始模拟人工点击左侧导航菜单...") + for name, xpath in menus: + ele = page.ele(xpath, timeout=5) + if ele: + try: + ele.click() + except: + page.run_js("arguments[0].click();", ele) + time.sleep(1.5) + else: + log("ERR", f"找不到菜单元素: {name}") + return + + log("OK", "✅ 成功点开发料单报表界面!") + + # 点击空白处隐藏菜单 + blank_xpath = 'xpath://*[@id="app"]/div/div[1]/div[2]/div[1]/div[2]/div[2]/div/div[1]/div' + blank_ele = page.ele(blank_xpath, timeout=3) + if blank_ele: + try: + blank_ele.click() + except: + page.run_js("arguments[0].click();", blank_ele) + time.sleep(0.5) + + log("INFO", f"开启底层数据拦截网: {API_TARGET}") + page.listen.start(API_TARGET) + + # 等待页面自动发起的请求 + packet = page.listen.wait(timeout=10) + + if not packet: + log("INFO", "尝试寻找并点击页面上的【查询】按钮...") + query_btn_xpath = 'xpath://*[@id="app"]/div/div[1]/div[2]/div[2]/div[1]/div[1]/div/button[1]/span' + query_btn = page.ele(query_btn_xpath, timeout=3) + + if query_btn: + try: query_btn.click() + except: page.run_js("arguments[0].click();", query_btn) + packet = page.listen.wait(timeout=15) + + if not packet: + log("ERR", "未能拦截到数据请求,可能网络超时或查询未触发。") + return + + # 设定开始抓取的页码,如果因为中断需要断点续传,请修改此变量 + # 刚才抓到了 95 页,我们需要从 96 页开始继续 + target_resume_page = 1 + + + # ========================================================= + # 第一页数据处理 + # ========================================================= + log("OK", f"🎉 成功拦截到第一页数据!HTTP 状态码: {packet.response.status}") + body = packet.response.body + data = body if isinstance(body, (dict, list)) else json.loads(body) + + total_count = 0 + if isinstance(data, dict) and "result" in data: + total_count = data["result"].get("totalCount", 0) + items = data["result"].get("items", []) + log("OK", f"后端报告总条数: {total_count}") + + # 只有当不是断点续传(即从第1页开始)时,才把第一页的数据加入列表 + if target_resume_page <= 1: + # 由于可能触发断点,如果是重新抓取,这里直接覆盖 + if not all_clean_items: + for item in items: + all_clean_items.append(_extract_fields(item)) + log("OK", f"第一页清洗完成,提取了 {len(items)} 条数据。") + else: + log("INFO", f"本地已有数据,跳过第一页保存,走翻页逻辑(注意:发料单可能需要您清空旧存档才能从头抓,这里先保留累加)") + else: + log("INFO", f"触发断点续传,跳过第一页的数据保存。后端报告总条数: {total_count}") + else: + log("ERR", "第一页返回的数据结构异常。") + return + + page_num = 1 + + # ========================================================= + # 断点续传逻辑跳转 + # ========================================================= + if target_resume_page > 1: + log("INFO", f"🚀 触发断点续传机制!准备直接跳转到第 {target_resume_page} 页...") + # 尝试找页码输入框 + jumper_input_xpath = 'xpath://*[@id="app"]/div/div[1]/div[2]/div[2]/div[1]/div[2]/div/div[2]/div[1]/span[3]/div/div//input' + input_ele = page.ele(jumper_input_xpath, timeout=5) + + if not input_ele: + jumper_input_xpath = 'xpath://input[@type="number" and @aria-label="页"]' + input_ele = page.ele(jumper_input_xpath, timeout=5) + + if input_ele: + input_ele.clear() + input_ele.input(str(target_resume_page)) + time.sleep(0.5) + input_ele.input('\n') + + packet = page.listen.wait(timeout=15) + if not packet: + log("ERR", "断点跳转失败,未拦截到目标页的数据请求。") + return + + log("OK", f"✅ 成功跳转至第 {target_resume_page} 页并截获数据!") + page_num = target_resume_page + + # 读取并解析断点页的数据 + body = packet.response.body + data = body if isinstance(body, (dict, list)) else json.loads(body) + if isinstance(data, dict) and "result" in data: + items = data["result"].get("items", []) + for item in items: + all_clean_items.append(_extract_fields(item)) + log("OK", f"第 {page_num} 页清洗完成,累计提取 {len(all_clean_items)} 条数据。") + else: + log("ERR", "找不到页码输入框,断点跳转失败,将从第 1 页继续!") + + # ========================================================= + # 循环翻页抓取 + # ========================================================= + while True: + # 引入“类人”随机延迟 + delay = random.uniform(2.5, 5.5) + log("INFO", f"⏳ 模拟真人停顿 {delay:.2f} 秒后,准备点击下一页...") + time.sleep(delay) + + if page_num > 1 and page_num % 50 == 0: + long_delay = random.uniform(10.0, 20.0) + log("INFO", f"☕️ 已经连续高强度翻了 {page_num} 页,触发风控规避机制,假装喝水休息 {long_delay:.2f} 秒...") + time.sleep(long_delay) + + # 用户指定的下一页按钮 xpath + next_btn_xpath = 'xpath://*[@id="app"]/div/div[1]/div[2]/div[2]/div[1]/div[2]/div/div[2]/div[1]/button[2]' + next_btn = page.ele(next_btn_xpath, timeout=3) + + if not next_btn: + log("ERR", "找不到下一页按钮,尝试强制刷新页面或终止。") + break + + # 检查按钮是否被禁用 + class_str = str(next_btn.attr("class")) + aria_disabled = next_btn.attr("aria-disabled") + is_disabled_attr = next_btn.attr("disabled") is not None + + if "disabled" in class_str or is_disabled_attr or aria_disabled == "true": + log("OK", "🏁 下一页按钮已被禁用,说明已经到达最后一页!") + break + + page_num += 1 + log("INFO", f"正在点击【下一页】抓取第 {page_num} 页...") + + try: + next_btn.click() + except Exception as e: + log("ERR", f"普通点击失败: {e},尝试 JS 点击...") + page.run_js("arguments[0].click();", next_btn) + + # 等待新一页的 API 响应 + packet = page.listen.wait(timeout=15) + if not packet: + log("ERR", f"第 {page_num} 页请求超时或未触发,中止抓取。") + break + + body = packet.response.body + data = body if isinstance(body, (dict, list)) else json.loads(body) + + if isinstance(data, dict) and "result" in data: + items = data["result"].get("items", []) + if not items: + log("WARN", f"第 {page_num} 页返回了空列表,可能已无数据。") + break + + for item in items: + all_clean_items.append(_extract_fields(item)) + log("OK", f"第 {page_num} 页清洗完成,累计提取 {len(all_clean_items)} 条数据。") + + # 每 10 页自动保存一次 + if page_num % 10 == 0: + with open(SAVE_PATH, "w", encoding="utf-8") as f: + json.dump(all_clean_items, f, ensure_ascii=False, indent=2) + log("INFO", f"💾 自动存档: 已保存 {len(all_clean_items)} 条记录至本地。") + else: + log("ERR", f"第 {page_num} 页数据结构异常,中止。") + break + + page.listen.stop() + + # 最终保存 + if all_clean_items: + with open(SAVE_PATH, "w", encoding="utf-8") as f: + json.dump(all_clean_items, f, ensure_ascii=False, indent=2) + log("OK", f"🎉 全部抓取完成!总计成功提取 {len(all_clean_items)} 条数据。") + log("OK", f"数据已保存至: {SAVE_PATH}") + + except Exception as e: + log("ERR", f"发生全局异常: {e}") + if all_clean_items: + rescue_path = OUTPUT_DIR / "issue_receipt_details_RESCUE.json" + with open(rescue_path, "w", encoding="utf-8") as f: + json.dump(all_clean_items, f, ensure_ascii=False, indent=2) + log("INFO", f"🆘 触发异常保存,抢救了 {len(all_clean_items)} 条数据。") + finally: + try: + page.listen.stop() + log("INFO", "🛑 已释放浏览器监听资源,保持浏览器开启。") + except: + pass + +def _extract_fields(item): + """提取所需的字段""" + return { + "生产任务单号": item.get("productionOrderNo"), + "生产物料代码": item.get("productMaterialCode"), + "生产物料名称": item.get("productMaterialName"), + "生产物料规格": item.get("productMaterialSpecification"), + "发料单号": item.get("workOrdersNumber"), + "状态": item.get("status"), + "物料规格": item.get("materialSpecification"), + "物料名称": item.get("materialName"), + "物料代码": item.get("materialCode"), + "发料数量": item.get("issueNumber"), + "已发料数量": item.get("hasIssueNumber"), + "金额": item.get("amount"), + "成本价": item.get("costPrice"), + "发料金额": item.get("issueAmount"), + "生产订单备注": item.get("productionOrderRemark"), + "明细备注": item.get("detailedRemark"), + "单位名称": item.get("unitName"), + "仓库名称": item.get("warehouseName"), + "行号": item.get("lineNumber"), + "发料单备注": item.get("workOrdersRemark"), + "执行人名称": item.get("executorUserName"), + "物料型号": item.get("materialModel"), + "执行时间": item.get("executionTime"), + "领料人": item.get("materialsUserName"), + "生产物料型号": item.get("productMaterialModel"), + "自定义字段": item.get("customField"), + "部门代码": item.get("departmentInformationCode"), + "部门名称": item.get("departmentInformationName"), + "图片文件": item.get("imageFile"), + "汇总金额": item.get("issueAmountTotal"), + "物料组代码": item.get("materialGroupCode"), + "物料组名称": item.get("materialGroupName"), + "单价小数位数": item.get("numnberOfReservedDigits"), + "单价进位策略": item.get("placeMentStrategy"), + "单价": item.get("price"), + "销售订单号": item.get("salesOrderCode") + } + +if __name__ == "__main__": + fetch_issue_receipt_details() diff --git a/browser_login/fetch_issue_receipt_incremental.py b/browser_login/fetch_issue_receipt_incremental.py new file mode 100644 index 0000000..962598c --- /dev/null +++ b/browser_login/fetch_issue_receipt_incremental.py @@ -0,0 +1,297 @@ +""" +发料单报表 - 智能增量同步脚本 (从第一页开始抓,遇到旧数据即停) +目标: +1. 自动连接本地 SQLite 数据库查询是否存在某条记录。 +2. 进入 ERP 系统截获发料单数据,由于新数据都在第一页,我们从第 1 页开始抓。 +3. 逐条对比,如果发现某页的数据在本地已经存在,则认为增量部分已经抓取完毕,提前终止。 +4. 将新增数据存入 SQLite。 +""" +import sys +import json +import time +import math +import random +import sqlite3 +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent)) +from login import get_page, log +from config import DB_PATH + +HOME_URL = "https://yunmes.tftykj.cn/" +API_TARGET = "WorkOrdersDetailed_SearchListAll_Proxy" + +def get_local_count(conn): + """获取本地数据库已有的总记录数""" + cursor = conn.cursor() + cursor.execute("SELECT COUNT(*) FROM issue_receipt_details") + return cursor.fetchone()[0] + +def item_exists(cursor, item): + """判断某条发料明细是否已在数据库中存在(基于发料单号+行号+物料代码组合判断)""" + wo_number = item.get("workOrdersNumber") + line_no = item.get("lineNumber") + mat_code = item.get("materialCode") + + # 增加一个容错判断,如果其中有 None 就不当作重复 + if not wo_number or not line_no or not mat_code: + return False + + cursor.execute(''' + SELECT 1 FROM issue_receipt_details + WHERE work_orders_number = ? AND line_number = ? AND material_code = ? + ''', (wo_number, line_no, mat_code)) + return cursor.fetchone() is not None + +def _extract_fields(item): + """提取所需的字段""" + return { + "生产任务单号": item.get("productionOrderNo"), + "生产物料代码": item.get("productMaterialCode"), + "生产物料名称": item.get("productMaterialName"), + "生产物料规格": item.get("productMaterialSpecification"), + "发料单号": item.get("workOrdersNumber"), + "状态": item.get("status"), + "物料规格": item.get("materialSpecification"), + "物料名称": item.get("materialName"), + "物料代码": item.get("materialCode"), + "发料数量": item.get("issueNumber"), + "已发料数量": item.get("hasIssueNumber"), + "金额": item.get("amount"), + "成本价": item.get("costPrice"), + "发料金额": item.get("issueAmount"), + "生产订单备注": item.get("productionOrderRemark"), + "明细备注": item.get("detailedRemark"), + "单位名称": item.get("unitName"), + "仓库名称": item.get("warehouseName"), + "行号": item.get("lineNumber"), + "发料单备注": item.get("workOrdersRemark"), + "执行人名称": item.get("executorUserName"), + "物料型号": item.get("materialModel"), + "执行时间": item.get("executionTime"), + "领料人": item.get("materialsUserName"), + "生产物料型号": item.get("productMaterialModel"), + "自定义字段": item.get("customField"), + "部门代码": item.get("departmentInformationCode"), + "部门名称": item.get("departmentInformationName"), + "图片文件": item.get("imageFile"), + "汇总金额": item.get("issueAmountTotal"), + "物料组代码": item.get("materialGroupCode"), + "物料组名称": item.get("materialGroupName"), + "单价小数位数": item.get("numnberOfReservedDigits"), + "单价进位策略": item.get("placeMentStrategy"), + "单价": item.get("price"), + "销售订单号": item.get("salesOrderCode") + } + +def fetch_issue_receipt_incremental(): + log("INFO", "=== 🚀 启动发料单报表 - 智能增量同步 (首屏更新模式) ===") + + if not DB_PATH.exists(): + log("ERR", f"找不到数据库文件: {DB_PATH},请先执行全量导入!") + return + + conn = sqlite3.connect(DB_PATH) + local_count = get_local_count(conn) + log("INFO", f"📦 本地数据库当前总计: {local_count} 条数据") + + page = get_page(port=9222) + + try: + log("INFO", f"正在回到主页起点: {HOME_URL}") + page.get(HOME_URL) + page.wait.load_start() + time.sleep(2) + + menus = [ + ("第一层: 业务统计报表", 'xpath://*[@id="app"]/div/div[1]/div[1]/div[2]/div/div[1]/div/div[10]/div/p'), + ("第二层: 生产业务报表(推测)", 'xpath:/html/body/div[7]/div/div[1]/div/div[9]/div/p'), + ("第三层: 发料单报表", 'xpath:/html/body/div[8]/div/div[1]/div/div[6]/div/p') + ] + + log("INFO", "模拟点击左侧导航菜单...") + for name, xpath in menus: + ele = page.ele(xpath, timeout=5) + if ele: + try: ele.click() + except: page.run_js("arguments[0].click();", ele) + time.sleep(1.5) + else: + log("ERR", f"找不到菜单元素: {name}") + return + + log("OK", "✅ 成功点开发料单报表界面!") + + # 隐藏菜单 + blank_xpath = 'xpath://*[@id="app"]/div/div[1]/div[2]/div[1]/div[2]/div[2]/div/div[1]/div' + blank_ele = page.ele(blank_xpath, timeout=3) + if blank_ele: + try: blank_ele.click() + except: page.run_js("arguments[0].click();", blank_ele) + time.sleep(0.5) + + log("INFO", f"开启底层数据拦截网: {API_TARGET}") + page.listen.start(API_TARGET) + + packet = page.listen.wait(timeout=10) + if not packet: + query_btn_xpath = 'xpath://*[@id="app"]/div/div[1]/div[2]/div[2]/div[1]/div[1]/div/button[1]/span' + query_btn = page.ele(query_btn_xpath, timeout=3) + if query_btn: + try: query_btn.click() + except: page.run_js("arguments[0].click();", query_btn) + packet = page.listen.wait(timeout=15) + + if not packet: + log("ERR", "未能拦截到第一页数据,无法获取线上总条数。") + return + + body = packet.response.body + data = body if isinstance(body, (dict, list)) else json.loads(body) + + remote_count = 0 + if isinstance(data, dict) and "result" in data: + remote_count = data["result"].get("totalCount", 0) + + log("INFO", f"🌐 线上 ERP 系统当前总条数: {remote_count} 条") + + if remote_count == local_count: + log("OK", "🎉 线上条数与本地一致,数据已是最新状态,无需抓取!") + return + + new_items_count = remote_count - local_count + if new_items_count > 0: + log("INFO", f"🔥 发现大致 {new_items_count} 条新增数据!准备从第 1 页开始扫描录入...") + else: + log("INFO", f"⚠️ 线上条数 ({remote_count}) 少于本地条数 ({local_count}),可能存在数据删除。仍将扫描第一页验证更新。") + + # ========================================================= + # 开始处理第一页,并循环往后翻,直到遇到重复数据 + # ========================================================= + current_page = 1 + cursor = conn.cursor() + total_inserted = 0 + + # 第一次的数据已经在上面的 packet 里了,直接处理 + first_page_data = data + + while True: + should_stop = False + inserted_this_page = 0 + + if isinstance(first_page_data, dict) and "result" in first_page_data: + items = first_page_data["result"].get("items", []) + if not items: + log("WARN", f"第 {current_page} 页返回了空列表,已无数据。") + break + + # 打印第一条数据的信息,用于调试 + if items: + first_item = items[0] + log("INFO", f"🔍 正在检查本页第一条数据: 发料单 {first_item.get('workOrdersNumber')} 行号 {first_item.get('lineNumber')} 物料 {first_item.get('materialCode')}") + + for raw_item in items: + # 1. 检查是否存在 + if item_exists(cursor, raw_item): + # 发料单的新数据都在最前面。当我们遇到一条已经在数据库里的数据时, + # 说明这之前的数据都是新的,这之后的数据肯定都抓过了,直接停止。 + log("INFO", f"🛑 在第 {current_page} 页发现本地已存在的记录 (发料单: {raw_item.get('workOrdersNumber')} 行号: {raw_item.get('lineNumber')} 物料: {raw_item.get('materialCode')}),增量扫描结束!") + should_stop = True + break + + # 2. 如果不存在,提取并插入 + item = _extract_fields(raw_item) + + cursor.execute(''' + INSERT INTO issue_receipt_details ( + production_order_no, product_material_code, product_material_name, product_material_specification, + work_orders_number, status, material_specification, material_name, material_code, + issue_number, has_issue_number, amount, cost_price, issue_amount, + production_order_remark, detailed_remark, unit_name, warehouse_name, line_number, + work_orders_remark, executor_user_name, material_model, execution_time, materials_user_name, + product_material_model, custom_field, department_information_code, department_information_name, + image_file, issue_amount_total, material_group_code, material_group_name, + numnber_of_reserved_digits, place_ment_strategy, price, sales_order_code + ) VALUES ( + ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, + ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? + ) + ''', ( + item.get("生产任务单号"), item.get("生产物料代码"), item.get("生产物料名称"), item.get("生产物料规格"), + item.get("发料单号"), item.get("状态"), item.get("物料规格"), item.get("物料名称"), item.get("物料代码"), + item.get("发料数量"), item.get("已发料数量"), item.get("金额"), item.get("成本价"), item.get("发料金额"), + item.get("生产订单备注"), item.get("明细备注"), item.get("单位名称"), item.get("仓库名称"), item.get("行号"), + item.get("发料单备注"), item.get("执行人名称"), item.get("物料型号"), item.get("执行时间"), item.get("领料人"), + item.get("生产物料型号"), item.get("自定义字段"), item.get("部门代码"), item.get("部门名称"), + item.get("图片文件"), item.get("汇总金额"), item.get("物料组代码"), item.get("物料组名称"), + item.get("单价小数位数"), item.get("单价进位策略"), item.get("单价"), item.get("销售订单号") + )) + inserted_this_page += 1 + total_inserted += 1 + + conn.commit() + log("OK", f"第 {current_page} 页处理完毕,成功插入 {inserted_this_page} 条新数据。") + + if should_stop: + break + else: + log("ERR", f"第 {current_page} 页数据结构异常,中止。") + break + + # 如果没遇到旧数据,继续点击下一页 + delay = random.uniform(1.5, 3.5) + log("INFO", f"⏳ 停顿 {delay:.2f} 秒后点击下一页...") + time.sleep(delay) + + next_btn = None + for _ in range(3): + next_btn = page.ele('xpath://button[contains(@class, "btn-next")]', timeout=3) + if next_btn: + break + time.sleep(1) + + if not next_btn: + next_btn = page.ele('xpath://i[contains(@class, "el-icon-arrow-right")]/parent::button', timeout=3) + + if next_btn: + # 检查按钮是否被禁用 + class_str = str(next_btn.attr("class")) + aria_disabled = next_btn.attr("aria-disabled") + is_disabled_attr = next_btn.attr("disabled") is not None + + if "disabled" in class_str or is_disabled_attr or aria_disabled == "true": + log("OK", "🏁 下一页按钮已被禁用,已经翻到最后一页。") + break + + try: next_btn.click() + except: page.run_js("arguments[0].click();", next_btn) + + packet = page.listen.wait(timeout=15) + if not packet: + log("ERR", f"第 {current_page + 1} 页请求超时!") + break + + # 为下一轮循环准备数据 + body = packet.response.body + first_page_data = body if isinstance(body, (dict, list)) else json.loads(body) + else: + log("ERR", "重试 3 次后仍然找不到下一页按钮!") + break + + current_page += 1 + + log("OK", f"🎉 发料单增量同步大功告成!总计新增了 {total_inserted} 条记录入库!") + + except Exception as e: + log("ERR", f"发生全局异常: {e}") + finally: + if 'conn' in locals() and conn: + conn.close() + if 'page' in locals() and page: + try: + page.listen.stop() + except Exception: + pass + +if __name__ == "__main__": + fetch_issue_receipt_incremental() \ No newline at end of file diff --git a/browser_login/fetch_work_orders.py b/browser_login/fetch_work_orders.py new file mode 100644 index 0000000..9e97a34 --- /dev/null +++ b/browser_login/fetch_work_orders.py @@ -0,0 +1,107 @@ +""" +生产工单查询 - 数据提取 +""" + +import os +import sys +import json +import time +import datetime +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent)) +from login import get_page, login, login_manual, log, dump_page_state +from config import OUTPUT_DIR + +PAGE_URL = "https://yunmes.tftykj.cn/WorkOrdersQuery" +API_PATH = "WorkOrdersDetailed_SearchListAll_Proxy" # 精确匹配生产工单列表 API + +# ── 导航到目标页面 ─────────────────────────────────────────────────────────── +def navigate_to_page(page): + log("INFO", f"导航到生产工单查询页面...") + page.get(PAGE_URL) + # 等待数据表格区域出现 + table = page.ele("xpath://table | .el-table__body", timeout=15) + if table: + log("OK", "页面已加载") + else: + log("WARN", "表格元素未找到,继续执行") + +# ── 拦截并获取第 1 页数据 ─────────────────────────────────────────────── +def fetch_page1(page) -> dict: + log("INFO", "开启网络监听...") + page.listen.start(API_PATH) + + # 刷新页面触发自动加载请求 + page.refresh() + + log("INFO", "等待 API 响应...") + packet = page.listen.wait(timeout=30) + page.listen.stop() + + if not packet: + log("ERR", "超时未收到响应") + dump_page_state(page, "监听超时") + return None + + log("OK", f"拦截成功 → HTTP {packet.response.status} (URL: {packet.request.url})") + + try: + body = packet.response.body + data = body if isinstance(body, (dict, list)) else json.loads(body) + + if isinstance(data, dict): + result = data.get("result", {}) + if isinstance(result, dict) and "totalCount" in result: + items = result.get("items", []) + total = result.get("totalCount", "?") + log("INFO", f"本页记录: {len(items)} 条 | 总计: {total} 条") + return data + except Exception as e: + log("WARN", f"解析 {packet.request.url} 失败: {e}") + + log("ERR", "未解析到有效的数据结构") + return None + +# ── 保存为 JSON ─────────────────────────────────────────────────────────────── +def save_json(data, prefix: str = "work_orders") -> Path: + ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + path = OUTPUT_DIR / f"{prefix}_{ts}.json" + with open(path, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + log("OK", f"已保存: {path}") + return path + +# ── 主流程 ──────────────────────────────────────────────────────────────────── +def run(manual: bool = False): + mode = "复用保活浏览器" + log("INFO", f"生产工单查询提取启动 [{mode}]") + + # 使用已经登录的保活浏览器端口 9222 + page = get_page(port=9222) + try: + # Step 1: 进入页面 + navigate_to_page(page) + + # Step 2: 获取第 1 页数据 + data = fetch_page1(page) + if data is None: + log("ERR", "数据获取失败") + return + + # Step 3: 保存 JSON + save_json(data, prefix="work_orders_page1") + log("OK", "完成!文件保存在 output/") + + except KeyboardInterrupt: + log("INFO", "用户中断 (Ctrl+C)") + except Exception as e: + log("ERR", f"异常: {e}") + import traceback + traceback.print_exc() + finally: + # 复用浏览器的情况下不关闭浏览器 + pass + +if __name__ == "__main__": + run(manual="--manual" in sys.argv) diff --git a/browser_login/fetch_work_orders_incremental.py b/browser_login/fetch_work_orders_incremental.py new file mode 100644 index 0000000..c448df3 --- /dev/null +++ b/browser_login/fetch_work_orders_incremental.py @@ -0,0 +1,241 @@ +""" +生产工单报表 - 智能增量同步脚本 +""" +import sys +import json +import time +import math +import random +import sqlite3 +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent)) +from login import get_page, log +from config import DB_PATH +from import_to_sqlite import init_db + +HOME_URL = "https://yunmes.tftykj.cn/" +PAGE_URL = "https://yunmes.tftykj.cn/WorkOrdersQuery" +API_TARGET = "WorkOrdersDetailed_SearchListAll_Proxy" + +def get_local_count(conn): + """获取本地数据库已有的总记录数""" + cursor = conn.cursor() + cursor.execute("SELECT COUNT(*) FROM work_orders") + return cursor.fetchone()[0] + +def fetch_work_orders_incremental(): + log("INFO", "=== 🚀 启动生产工单查询 - 智能增量同步 ===") + + # 确保表结构已初始化 + conn = init_db() + local_count = get_local_count(conn) + log("INFO", f"📦 本地数据库当前总计: {local_count} 条数据") + + page = get_page(port=9222) + + try: + log("INFO", f"正在进入工单页面: {PAGE_URL}") + page.get(PAGE_URL) + table = page.ele("xpath://table | .el-table__body", timeout=10) + if not table: + log("WARN", "未加载出工单页面表格元素,继续尝试监听...") + + # 添加一小段硬延时,确保页面 JS 完全执行完毕 + time.sleep(2) + + log("INFO", f"开启底层数据拦截网: {API_TARGET}") + page.listen.start(API_TARGET) + + # 点击查询按钮触发第一页请求 + query_btn = page.ele('#Search', timeout=3) + if not query_btn: + # 兼容 ElementUI 的按钮 + query_btn_xpath = 'xpath://*[@id="app"]/div/div[1]/div[2]/div[2]/div[1]/div[1]/div/button[1]/span' + query_btn = page.ele(query_btn_xpath, timeout=3) + + if query_btn: + try: query_btn.click() + except: page.run_js("arguments[0].click();", query_btn) + + packet = page.listen.wait(timeout=15) + if not packet: + # 备用方案:刷新页面 + page.refresh() + packet = page.listen.wait(timeout=15) + + if not packet: + log("ERR", "未能拦截到第一页数据,无法获取线上总条数。") + return + + body = packet.response.body + data = body if isinstance(body, (dict, list)) else json.loads(body) + + remote_count = 0 + if isinstance(data, dict) and "result" in data: + result = data["result"] + if isinstance(result, dict): + remote_count = result.get("totalCount", 0) + + log("INFO", f"🌐 线上 ERP 系统当前总条数: {remote_count} 条") + + if remote_count <= local_count: + log("INFO", f"本地已有 {local_count} 条数据,但根据策略,我们将强制进行一轮全量更新检查...") + + log("INFO", f"🔥 准备进行全量跳页抓取...") + + # --- 【增量抓取策略优化】:不再根据总量做分页跳转 --- + # 始终从第 1 页(即最新发生变化/新增的工单页)开始抓取, + # 并往后翻页,直到发现连续 N 页的数据在本地数据库中都已经存在,即认为“增量部分”已抓取完毕。 + start_page = 1 + end_page = math.ceil(remote_count / 50) + + log("INFO", f"🎯 增量抓取策略启动:从第 {start_page} 页向后抓取,直至遇到全为已存旧数据的页面。") + + current_page = start_page + cursor = conn.cursor() + total_inserted = 0 + total_updated = 0 + consecutive_old_pages = 0 # 连续多少页都是老数据 + + while current_page <= end_page: + body = packet.response.body + data = body if isinstance(body, (dict, list)) else json.loads(body) + + inserted_this_page = 0 + if isinstance(data, dict) and "result" in data: + result = data.get("result", {}) + if isinstance(result, dict): + items = result.get("items", []) + + page_inserted = 0 + page_updated = 0 + + for item in items: + wo_number = item.get("workOrdersNumber") + line_no = item.get("lineNumber") + mat_code = item.get("materialCode") + + if not wo_number or not mat_code: + continue + + # 检查此记录在本地是否已存在,以及关键状态是否发生变化 + cursor.execute(""" + SELECT status, total_issue_number FROM work_orders + WHERE work_orders_number = ? AND line_number = ? AND material_code = ? + """, (wo_number, line_no, mat_code)) + existing_record = cursor.fetchone() + + new_status = item.get("status") + new_total_issue_number = item.get("hasIssueNumber") + + if not existing_record: + # 本地不存在,执行插入 + cursor.execute(''' + INSERT INTO work_orders ( + work_orders_number, line_number, material_code, material_name, material_specification, + status, unit_name, cost_price, issue_number, total_issue_number, + issue_amount, issue_amount_total, executor_user_name, execution_time, + production_order_no, warehouse_name, materials_user_name, work_orders_remark + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', ( + wo_number, line_no, mat_code, item.get("materialName"), item.get("materialSpecification"), + new_status, item.get("unitName"), item.get("costPrice"), item.get("issueNumber"), + new_total_issue_number, item.get("issueAmount"), item.get("issueAmountTotal"), + item.get("executorUserName"), item.get("executionTime"), item.get("productionOrderNo"), + item.get("warehouseName"), item.get("materialsUserName"), item.get("workOrdersRemark") + )) + page_inserted += 1 + total_inserted += 1 + else: + # 本地已存在,检查关键状态或数量是否有更新 + old_status = existing_record[0] + old_total_issue_number = existing_record[1] + + if str(old_status) != str(new_status) or str(old_total_issue_number) != str(new_total_issue_number): + cursor.execute(''' + UPDATE work_orders SET + status = ?, + cost_price = ?, + issue_number = ?, + total_issue_number = ?, + issue_amount = ?, + issue_amount_total = ?, + executor_user_name = ?, + execution_time = ?, + warehouse_name = ?, + materials_user_name = ? + WHERE work_orders_number = ? AND line_number = ? AND material_code = ? + ''', ( + new_status, item.get("costPrice"), item.get("issueNumber"), new_total_issue_number, + item.get("issueAmount"), item.get("issueAmountTotal"), item.get("executorUserName"), + item.get("executionTime"), item.get("warehouseName"), item.get("materialsUserName"), + wo_number, line_no, mat_code + )) + page_updated += 1 + total_updated += 1 + + conn.commit() + log("OK", f"第 {current_page} 页处理完毕: 新增 {page_inserted} 条, 更新 {page_updated} 条。") + + # 增量判定逻辑:如果当前页全部都在本地存在,且没有任何一条发生了状态/数量的更新 + # 则说明我们已经追溯到了历史旧数据,不需要再继续往后翻页抓取了! + if page_inserted == 0 and page_updated == 0: + consecutive_old_pages += 1 + log("INFO", f"⚡️ 第 {current_page} 页全为无变动的旧数据 (累计 {consecutive_old_pages} 页)") + + if consecutive_old_pages >= 2: + log("OK", "🎉 连续 2 页未发现新数据或变动数据,增量抓取完成,提前结束!") + break + else: + # 只要有任何一条插入或更新,重置计数器 + consecutive_old_pages = 0 + + if current_page < end_page: + delay = random.uniform(1.5, 3.5) + log("INFO", f"⏳ 停顿 {delay:.2f} 秒后点击下一页...") + time.sleep(delay) + + next_btn = None + for _ in range(3): + # 尝试 EasyUI 的下一页按钮 + next_btn = page.ele('xpath://*[contains(@class, "pagination-next")]/ancestor::a', timeout=3) + if not next_btn: + # 兼容 ElementUI 的下一页按钮 + next_btn = page.ele('xpath://button[contains(@class, "btn-next")]', timeout=3) + + if next_btn: + break + time.sleep(1) + + if next_btn: + try: next_btn.click() + except: page.run_js("arguments[0].click();", next_btn) + + packet = page.listen.wait(timeout=15) + if not packet: + log("ERR", f"第 {current_page + 1} 页请求超时!") + break + else: + log("ERR", "重试 3 次后仍然找不到下一页按钮!") + break + + current_page += 1 + + log("OK", f"🎉 增量同步大功告成!总计向数据库执行了 {total_inserted} 次插入/更新操作!") + + except Exception as e: + log("ERR", f"发生全局异常: {e}") + import traceback + traceback.print_exc() + finally: + if 'conn' in locals() and conn: + conn.close() + if 'page' in locals() and page: + try: + page.listen.stop() + except Exception: + pass + +if __name__ == "__main__": + fetch_work_orders_incremental() diff --git a/browser_login/find_buttons.py b/browser_login/find_buttons.py new file mode 100644 index 0000000..ccc6502 --- /dev/null +++ b/browser_login/find_buttons.py @@ -0,0 +1,13 @@ +import sys +from pathlib import Path +sys.path.insert(0, str(Path(__file__).parent)) +from login import get_page + +page = get_page(port=9222) +print("URL:", page.url) + +links = page.eles('tag:a') +for a in links: + if a.text and len(a.text) < 10: + print("Link:", a.text, a.html) + diff --git a/browser_login/find_report.py b/browser_login/find_report.py new file mode 100644 index 0000000..0077403 --- /dev/null +++ b/browser_login/find_report.py @@ -0,0 +1,12 @@ +import sys +from pathlib import Path +sys.path.insert(0, str(Path(__file__).parent)) +from login import get_page + +page = get_page(port=9222) +print("当前 URL:", page.url) + +menus = page.eles('text:自定义报表') +for m in menus: + print("Found menu:", m.html) + diff --git a/browser_login/import_abnormal.py b/browser_login/import_abnormal.py new file mode 100644 index 0000000..02c0936 --- /dev/null +++ b/browser_login/import_abnormal.py @@ -0,0 +1,17 @@ +import sys +import json +from pathlib import Path +sys.path.insert(0, str(Path(__file__).parent)) +from import_to_sqlite import import_abnormal_report_data +from config import OUTPUT_DIR + +latest_file = OUTPUT_DIR / "report_abnormal_20260611_120355.json" +print(f"Importing from {latest_file}") +with open(latest_file, 'r', encoding='utf-8') as f: + data = json.load(f) + items = data.get('result', {}).get('items', []) + if items: + count = import_abnormal_report_data(items) + print(f"Imported {count} items.") + else: + print("No items in json.") diff --git a/browser_login/import_to_sqlite.py b/browser_login/import_to_sqlite.py index cabd43e..988c4b1 100644 --- a/browser_login/import_to_sqlite.py +++ b/browser_login/import_to_sqlite.py @@ -6,6 +6,7 @@ from config import OUTPUT_DIR, DB_PATH RECEIPT_JSON = OUTPUT_DIR / "receipt_details_full_clean.json" BOM_JSON = OUTPUT_DIR / "bom_cost_full_tree_final.json" +ISSUE_JSON = OUTPUT_DIR / "issue_receipt_details_full.json" def init_db(): """初始化数据库并创建表""" @@ -40,6 +41,68 @@ def init_db(): cursor.execute('CREATE INDEX IF NOT EXISTS idx_receipt_supplier_name ON receipt_details(supplier_name)') cursor.execute('CREATE INDEX IF NOT EXISTS idx_receipt_time ON receipt_details(receipt_time)') + # 创建发料明细表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS issue_receipt_details ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + production_order_no TEXT, + product_material_code TEXT, + product_material_name TEXT, + product_material_specification TEXT, + work_orders_number TEXT, + status INTEGER, + material_specification TEXT, + material_name TEXT, + material_code TEXT, + issue_number REAL, + has_issue_number REAL, + amount REAL, + cost_price REAL, + issue_amount REAL, + production_order_remark TEXT, + detailed_remark TEXT, + unit_name TEXT, + warehouse_name TEXT, + line_number INTEGER, + work_orders_remark TEXT, + executor_user_name TEXT, + material_model TEXT, + execution_time TEXT, + materials_user_name TEXT, + product_material_model TEXT, + custom_field TEXT, + department_information_code TEXT, + department_information_name TEXT, + image_file TEXT, + issue_amount_total REAL, + material_group_code TEXT, + material_group_name TEXT, + numnber_of_reserved_digits INTEGER, + place_ment_strategy INTEGER, + price REAL, + sales_order_code TEXT + ) + ''') + cursor.execute('CREATE INDEX IF NOT EXISTS idx_issue_material_code ON issue_receipt_details(material_code)') + cursor.execute('CREATE INDEX IF NOT EXISTS idx_issue_execution_time ON issue_receipt_details(execution_time)') + cursor.execute('CREATE INDEX IF NOT EXISTS idx_issue_work_orders_number ON issue_receipt_details(work_orders_number)') + + # 删除历史可能存在的重复数据,确保唯一索引能够成功创建 + cursor.execute(''' + DELETE FROM issue_receipt_details + WHERE id NOT IN ( + SELECT MIN(id) + FROM issue_receipt_details + GROUP BY work_orders_number, line_number, material_code + ) + ''') + + # 为发料明细表创建唯一索引,用于 UPSERT 冲突检测 + cursor.execute(''' + CREATE UNIQUE INDEX IF NOT EXISTS idx_issue_unique + ON issue_receipt_details(work_orders_number, line_number, material_code) + ''') + # 注意:为了在打包部署时不丢失用户已抓取的数据,改为 IF NOT EXISTS cursor.execute(''' CREATE TABLE IF NOT EXISTS bom_parent ( @@ -66,6 +129,81 @@ def init_db(): cursor.execute('CREATE INDEX IF NOT EXISTS idx_bom_child_parent_code ON bom_child(parent_material_code)') cursor.execute('CREATE INDEX IF NOT EXISTS idx_bom_child_node_code ON bom_child(node_material_code)') + # 创建生产工单表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS work_orders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + work_orders_number TEXT, + line_number INTEGER, + material_code TEXT, + material_name TEXT, + material_specification TEXT, + status INTEGER, + unit_name TEXT, + cost_price REAL, + issue_number REAL, + total_issue_number REAL, + issue_amount REAL, + issue_amount_total REAL, + executor_user_name TEXT, + execution_time TEXT, + production_order_no TEXT, + warehouse_name TEXT, + materials_user_name TEXT, + work_orders_remark TEXT + ) + ''') + + cursor.execute('CREATE INDEX IF NOT EXISTS idx_work_orders_number ON work_orders(work_orders_number)') + cursor.execute('CREATE INDEX IF NOT EXISTS idx_work_orders_material_code ON work_orders(material_code)') + + # 删除可能存在的重复数据 + cursor.execute(''' + DELETE FROM work_orders + WHERE id NOT IN ( + SELECT MIN(id) + FROM work_orders + GROUP BY work_orders_number, line_number, material_code + ) + ''') + + # 唯一索引 + cursor.execute(''' + CREATE UNIQUE INDEX IF NOT EXISTS idx_work_orders_unique + ON work_orders(work_orders_number, line_number, material_code) + ''') + + # 创建发料异常报表表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS abnormal_report ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + work_orders_number TEXT, + product_code TEXT, + product_name TEXT, + status TEXT, + completed_qty REAL, + order_date TEXT, + workshop TEXT, + material_code TEXT, + material_name TEXT, + material_specification TEXT, + unit_qty REAL, + total_demand_qty REAL, + warehouse_issue_qty REAL, + theoretical_issue_qty REAL, + issue_method TEXT, + issue_status TEXT + ) + ''') + + cursor.execute('CREATE INDEX IF NOT EXISTS idx_abnormal_work_orders_number ON abnormal_report(work_orders_number)') + cursor.execute('CREATE INDEX IF NOT EXISTS idx_abnormal_material_code ON abnormal_report(material_code)') + + cursor.execute(''' + CREATE UNIQUE INDEX IF NOT EXISTS idx_abnormal_unique + ON abnormal_report(work_orders_number, material_code) + ''') + conn.commit() return conn @@ -80,68 +218,74 @@ def import_receipt_details(conn): data = json.load(f) cursor = conn.cursor() - # 清空旧数据(如果需要重复运行),并且我们现在要更新表结构 - cursor.execute('DROP TABLE IF EXISTS receipt_details') + + # 删除历史可能存在的重复数据,确保唯一索引能够成功创建 cursor.execute(''' - CREATE TABLE receipt_details ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - purchase_order_code TEXT, - row_no INTEGER, - material_code TEXT, - material_name TEXT, - material_specification TEXT, - warehouse_code TEXT, - warehouse_name TEXT, - supplier_code TEXT, - supplier_name TEXT, - unit_name TEXT, - conversion_unit TEXT, - receive_price REAL, - receipt_time TEXT, - purchase_qty REAL, - receive_qty REAL, - total_amount REAL - ) + DELETE FROM receipt_details + WHERE id NOT IN ( + SELECT MIN(id) + FROM receipt_details + GROUP BY purchase_order_code, row_no, material_code + ) + ''') + + # 为了避免没有唯一索引导致 UPSERT 报错,这里显式创建一次 + cursor.execute(''' + CREATE UNIQUE INDEX IF NOT EXISTS idx_receipt_unique + ON receipt_details(purchase_order_code, row_no, material_code) ''') - cursor.execute('CREATE INDEX IF NOT EXISTS idx_receipt_material_code ON receipt_details(material_code)') - cursor.execute('CREATE INDEX IF NOT EXISTS idx_receipt_supplier_name ON receipt_details(supplier_name)') - cursor.execute('CREATE INDEX IF NOT EXISTS idx_receipt_time ON receipt_details(receipt_time)') - count = 0 + count_inserted = 0 + for item in data: p_qty = item.get("进货数量") r_qty = item.get("收货数量") + po_code = item.get("采购订单号") + row_no = item.get("行号") + mat_code = item.get("物料代码") - cursor.execute(''' - INSERT INTO receipt_details ( - purchase_order_code, row_no, material_code, material_name, - material_specification, warehouse_code, warehouse_name, - supplier_code, supplier_name, unit_name, conversion_unit, - receive_price, receipt_time, - purchase_qty, receive_qty, total_amount - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ''', ( - item.get("采购订单号"), - item.get("行号"), - item.get("物料代码"), - item.get("物料名称"), - item.get("物料规格"), - item.get("仓库代码"), - item.get("仓库名称"), - item.get("供应商代码"), - item.get("供应商名称"), - item.get("单位名称"), - item.get("转换单位"), - item.get("收货单价"), - item.get("收货时间"), - p_qty, - r_qty, - item.get("收货总金额") - )) - count += 1 + # 容错:如果关键字段为空则跳过 + if not po_code or not row_no or not mat_code: + continue + + try: + cursor.execute(''' + INSERT INTO receipt_details ( + purchase_order_code, row_no, material_code, material_name, + material_specification, warehouse_code, warehouse_name, + supplier_code, supplier_name, unit_name, conversion_unit, + receive_price, receipt_time, + purchase_qty, receive_qty, total_amount + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(purchase_order_code, row_no, material_code) DO UPDATE SET + receive_price=excluded.receive_price, + receive_qty=excluded.receive_qty, + total_amount=excluded.total_amount, + receipt_time=excluded.receipt_time + ''', ( + po_code, + row_no, + mat_code, + item.get("物料名称"), + item.get("物料规格"), + item.get("仓库代码"), + item.get("仓库名称"), + item.get("供应商代码"), + item.get("供应商名称"), + item.get("单位名称"), + item.get("转换单位"), + item.get("收货单价"), + item.get("收货时间"), + p_qty, + r_qty, + item.get("收货总金额") + )) + count_inserted += 1 + except sqlite3.Error as e: + print(f"入库报错 收货单:{po_code} 行号:{row_no} 错误:{e}") conn.commit() - print(f"成功导入 {count} 条收货明细数据!") + print(f"成功处理(新增或更新) {count_inserted} 条收货明细数据!") def _insert_bom_tree(cursor, parent_material_code, tree_nodes, parent_node_id=None): """递归插入 BOM 树节点""" @@ -213,6 +357,128 @@ def import_bom_data(conn): child_count = cursor.fetchone()[0] print(f"成功导入 {parent_count} 个 BOM 父件,包含 {child_count} 个子件节点!") +def import_issue_receipt_details(conn): + """导入发料明细数据""" + if not ISSUE_JSON.exists(): + print(f"找不到发料明细文件: {ISSUE_JSON}") + return + + print("开始导入发料明细数据...") + with open(ISSUE_JSON, 'r', encoding='utf-8') as f: + data = json.load(f) + + cursor = conn.cursor() + + # 为了避免没有唯一索引导致 UPSERT 报错,这里显式创建一次 + cursor.execute(''' + CREATE UNIQUE INDEX IF NOT EXISTS idx_issue_unique + ON issue_receipt_details(work_orders_number, line_number, material_code) + ''') + + count_inserted = 0 + count_updated = 0 + + for item in data: + # 提取关键字段 + wo_number = item.get("发料单号") + line_no = item.get("行号") + mat_code = item.get("物料代码") + + # 容错:如果关键字段为空则跳过 + if not wo_number or not line_no or not mat_code: + continue + + try: + cursor.execute(''' + INSERT INTO issue_receipt_details ( + production_order_no, product_material_code, product_material_name, product_material_specification, + work_orders_number, status, material_specification, material_name, material_code, + issue_number, has_issue_number, amount, cost_price, issue_amount, + production_order_remark, detailed_remark, unit_name, warehouse_name, line_number, + work_orders_remark, executor_user_name, material_model, execution_time, materials_user_name, + product_material_model, custom_field, department_information_code, department_information_name, + image_file, issue_amount_total, material_group_code, material_group_name, + numnber_of_reserved_digits, place_ment_strategy, price, sales_order_code + ) VALUES ( + ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, + ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? + ) + ON CONFLICT(work_orders_number, line_number, material_code) DO UPDATE SET + status=excluded.status, + issue_number=excluded.issue_number, + has_issue_number=excluded.has_issue_number, + amount=excluded.amount, + cost_price=excluded.cost_price, + issue_amount=excluded.issue_amount, + warehouse_name=excluded.warehouse_name, + executor_user_name=excluded.executor_user_name, + execution_time=excluded.execution_time, + materials_user_name=excluded.materials_user_name, + issue_amount_total=excluded.issue_amount_total + ''', ( + item.get("生产任务单号"), item.get("生产物料代码"), item.get("生产物料名称"), item.get("生产物料规格"), + wo_number, item.get("状态"), item.get("物料规格"), item.get("物料名称"), mat_code, + item.get("发料数量"), item.get("已发料数量"), item.get("金额"), item.get("成本价"), item.get("发料金额"), + item.get("生产订单备注"), item.get("明细备注"), item.get("单位名称"), item.get("仓库名称"), line_no, + item.get("发料单备注"), item.get("执行人名称"), item.get("物料型号"), item.get("执行时间"), item.get("领料人"), + item.get("生产物料型号"), item.get("自定义字段"), item.get("部门代码"), item.get("部门名称"), + item.get("图片文件"), item.get("汇总金额"), item.get("物料组代码"), item.get("物料组名称"), + item.get("单价小数位数"), item.get("单价进位策略"), item.get("单价"), item.get("销售订单号") + )) + + # 由于 sqlite3 在 UPSERT 时,如果发生了 UPDATE,rowcount 也是 1 + # 但我们可以通过比较变化前后的总数来粗略估计,或者统一提示处理成功 + count_inserted += 1 + + except sqlite3.Error as e: + print(f"入库报错 发料单:{wo_number} 行号:{line_no} 错误:{e}") + + conn.commit() + print(f"成功处理(新增或更新) {count_inserted} 条发料明细数据!") + +def import_abnormal_report_data(items): + """直接将 API 获取到的异常报表 items 数组存入数据库""" + conn = init_db() + cursor = conn.cursor() + + count_inserted = 0 + + for item in items: + wo_number = item.get("生产工单号") + mat_code = item.get("需求物料代码") + + if not wo_number or not mat_code: + continue + + try: + cursor.execute(''' + INSERT INTO abnormal_report ( + work_orders_number, product_code, product_name, status, completed_qty, + order_date, workshop, material_code, material_name, material_specification, + unit_qty, total_demand_qty, warehouse_issue_qty, theoretical_issue_qty, + issue_method, issue_status + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(work_orders_number, material_code) DO UPDATE SET + status=excluded.status, + completed_qty=excluded.completed_qty, + warehouse_issue_qty=excluded.warehouse_issue_qty, + theoretical_issue_qty=excluded.theoretical_issue_qty, + issue_status=excluded.issue_status + ''', ( + wo_number, item.get("产品代码"), item.get("产品名称"), item.get("工单状态"), + item.get("已完工数量"), item.get("下单日期"), item.get("生产车间"), + mat_code, item.get("需求物料名称"), item.get("需求物料规格"), + item.get("单机用量"), item.get("需求总量"), item.get("仓库发放数量"), + item.get("理论仓库出料数量"), item.get("发料方式"), item.get("发料情况") + )) + count_inserted += 1 + except sqlite3.Error as e: + print(f"异常报表入库报错 工单:{wo_number} 物料:{mat_code} 错误:{e}") + + conn.commit() + conn.close() + return count_inserted + if __name__ == "__main__": import sys print(f"数据库文件将保存在: {DB_PATH}") @@ -225,10 +491,13 @@ if __name__ == "__main__": import_bom_data(conn) elif "--receipt-only" in args: import_receipt_details(conn) + elif "--issue-only" in args: + import_issue_receipt_details(conn) else: # 默认全量导入 import_receipt_details(conn) import_bom_data(conn) + import_issue_receipt_details(conn) conn.close() print("全部导入完成!你可以使用 SQLite 客户端连接 erp_data.db 查看数据。") \ No newline at end of file diff --git a/browser_login/inspect_current.py b/browser_login/inspect_current.py new file mode 100644 index 0000000..92b1b0c --- /dev/null +++ b/browser_login/inspect_current.py @@ -0,0 +1,18 @@ +import sys +from pathlib import Path +sys.path.insert(0, str(Path(__file__).parent)) +from login import get_page + +page = get_page(port=9222) +tabs = page.get_tabs() +print("当前打开的标签页:") +for t in tabs: + print(t.url) + +target_tab = page.get_tab(page.latest_tab) +print("\n当前活动标签页 URL:", target_tab.url) + +# 打印一下当前的弹窗或按钮 +print("包含'查询'的元素:") +for el in target_tab.eles('text:查询'): + print(el.html) diff --git a/browser_login/inspect_menus.py b/browser_login/inspect_menus.py new file mode 100644 index 0000000..2b60620 --- /dev/null +++ b/browser_login/inspect_menus.py @@ -0,0 +1,17 @@ +import sys +from pathlib import Path +sys.path.insert(0, str(Path(__file__).parent)) +from login import get_page + +page = get_page(port=9222) +page.get("https://yunmes.tftykj.cn/") + +print("等待页面加载...") +page.wait.load_start() + +# 尝试寻找菜单 +menus = page.eles('tag:div@@text():自定义报表管理') +print(f"找到 {len(menus)} 个 自定义报表管理") +for m in menus: + print(m.html) + diff --git a/browser_login/inspect_report.py b/browser_login/inspect_report.py new file mode 100644 index 0000000..6043cc1 --- /dev/null +++ b/browser_login/inspect_report.py @@ -0,0 +1,41 @@ +import sys +import time +from pathlib import Path +import json +sys.path.insert(0, str(Path(__file__).parent)) +from login import get_page + +page = get_page(port=9222) +target_tab = page.get_tab(page.latest_tab) + +target_tab.listen.start() + +btn = target_tab.ele('#onSearch') or target_tab.ele('text=查询') +if btn: + btn.run_js('this.click()') + print("Clicked") + +time.sleep(5) +for p in target_tab.listen.steps(): + if p.method == 'POST': + print("API:", p.url) + body = p.response.body + try: + data = body if isinstance(body, (dict, list)) else json.loads(body) + # dump each to a file for inspection + ts = time.time() + with open(f"browser_login/output/inspect_{ts}.json", "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + if isinstance(data, dict): + print("Keys:", list(data.keys())) + if 'result' in data: + res = data['result'] + if isinstance(res, dict): + print("Result Keys:", list(res.keys())) + else: + print("Result is list/other") + elif 'rows' in data: + print("Rows length:", len(data['rows'])) + except Exception as e: + print("Error parsing:", e) diff --git a/browser_login/navigate_report.py b/browser_login/navigate_report.py new file mode 100644 index 0000000..1a7da6a --- /dev/null +++ b/browser_login/navigate_report.py @@ -0,0 +1,43 @@ +import sys +import time +from pathlib import Path +sys.path.insert(0, str(Path(__file__).parent)) +from login import get_page + +page = get_page(port=9222) +page.get("https://yunmes.tftykj.cn/") +page.wait.load_start() +time.sleep(2) + +try: + print("点击一级菜单: 自定义报表管理") + # 这里可能有多个匹配,找可见的那个 + menu1 = page.ele('text=自定义报表管理', timeout=5) + if menu1: + menu1.click() + time.sleep(1) + + print("点击二级菜单: 自定义报表管理") + menu2 = page.eles('text=自定义报表管理')[1] # 或者是下一个 + if menu2: + menu2.click() + time.sleep(1) + + print("点击三级菜单: 自定义报表") + menu3 = page.ele('text=自定义报表', timeout=5) + if menu3: + menu3.click() + time.sleep(2) + print("成功进?mport sys +import time +from pathlib import Path +sys.path.insert(0, stint("找不到三级菜?ys.path.insert(0, str( from login import get_page + +page = get_page(p??page = get_pag表管理") +epage.get("https://yunmes. ppage.wait.load_start() +time.sleep(2)ontime.sleep(2) + +try: + at +try: + y \ No newline at end of file diff --git a/browser_login/search_report.py b/browser_login/search_report.py new file mode 100644 index 0000000..2e20b26 --- /dev/null +++ b/browser_login/search_report.py @@ -0,0 +1,19 @@ +import sys +import time +from pathlib import Path +sys.path.insert(0, str(Path(__file__).parent)) +from login import get_page + +page = get_page(port=9222) +print("URL:", page.url) + +ele = page.ele('text:生产工单发料异常检查报表') +if ele: + print("找到了报表:", ele.html) +else: + print("未找到报表,尝试在搜索框输入") + # 查找输入框,可能是 placeholder 为 "请输入报表名称" 等 + inputs = page.eles('tag:input') + for i in inputs: + print(i.attr('placeholder')) + diff --git a/browser_login/test_date.py b/browser_login/test_date.py new file mode 100644 index 0000000..2388f8a --- /dev/null +++ b/browser_login/test_date.py @@ -0,0 +1,16 @@ +import sys, time +from pathlib import Path +sys.path.insert(0, str(Path(__file__).parent)) +from login import get_page + +page = get_page(port=9222) +target_tab = page.get_tab(page.latest_tab) + +btn = target_tab.ele('#onSearch') or target_tab.ele('text=查询') +if btn: + print("Found btn:", btn.html) + try: + btn.click(by_js=True) + print("Click by_js success") + except Exception as e: + print("Error click:", e) diff --git a/browser_login/test_fill_date.py b/browser_login/test_fill_date.py new file mode 100644 index 0000000..5a05e9d --- /dev/null +++ b/browser_login/test_fill_date.py @@ -0,0 +1,91 @@ +""" +测试脚本:尝试在 ERP 质量报表页面填写“下单日期(开始)” +目标: 验证 DrissionPage 是否能成功清除并输入 ElementUI 的日期选择器。 +""" +import sys +import time +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent)) +from login import get_page, log + +HOME_URL = "https://yunmes.tftykj.cn/" + +def test_fill_date(): + log("INFO", "=== 🧪 启动日期输入框填写测试 ===") + page = get_page(port=9222) + + try: + log("INFO", f"正在回到主页起点: {HOME_URL}") + page.get(HOME_URL) + page.wait.load_start() + time.sleep(2) + + menus = [ + ("进入质量报表", 'xpath://*[@id="el-collapse-content-21"]/div/div/div/div[1]/div/div/div[6]/div') + ] + + log("INFO", "开始模拟人工点击左侧导航菜单...") + for name, xpath in menus: + ele = page.ele(xpath, timeout=5) + if ele: + try: ele.click() + except: page.run_js("arguments[0].click();", ele) + else: + log("ERR", f"找不到菜单元素: {name}") + return + + log("OK", "✅ 成功点开质量报表界面!") + + # 等待页面稍微加载一下 + time.sleep(2) + + # 尝试寻找并填写下单日期(开始) + date_xpath = 'xpath://*[@id="customTable-search-area"]/div[1]/div/div[2]/div[11]/div[2]/span/input[1]' + log("INFO", f"正在寻找日期输入框: {date_xpath}") + + date_input = page.ele(date_xpath, timeout=5) + + if date_input: + log("INFO", "✅ 找到输入框!尝试清除并输入 '2026-05-01'...") + + # ElementUI 的日期输入框比较难搞,通常需要组合拳 + # 放弃在 UI 层面折腾这个顽固的日期选择器 + # 我们采用“黑客”做法:直接在浏览器底层拦截并篡改即将发出的网络请求数据包! + API_TARGET = "SearchCustomReportBySQL_Proxy" + + # 1. 设置请求拦截器 + log("INFO", f"正在开启全局请求拦截器,目标: {API_TARGET}") + page.listen.start(API_TARGET) + + # 2. 我们不需要在输入框里填东西了,直接去点击查询按钮 + query_btn_xpath = 'xpath://*[@id="customTable-search-area"]/div[1]/div/div[1]/a[2]/span/span' + query_btn = page.ele(query_btn_xpath, timeout=3) + + if query_btn: + log("INFO", "准备点击【查询】按钮触发网络请求...") + try: + query_btn.click() + except: + page.run_js("arguments[0].click();", query_btn) + + # 3. 拦截请求 + packet = page.listen.wait(timeout=10) + if packet: + log("OK", "✅ 成功拦截到了查询请求!") + + # 打印一下它原来发送的数据体,看看结构 + raw_post_data = packet.request.postData + log("INFO", "原始发送的数据截断前100字符: " + str(raw_post_data)[:100]) + + log("OK", "如果这种拦截思路可行,我们下一步就可以在发送前篡改它里面的日期参数!") + else: + log("ERR", "未能拦截到查询请求,可能超时。") + else: + log("ERR", "未找到【查询】按钮。") + + except Exception as e: + log("ERR", f"发生异常: {e}") + +if __name__ == "__main__": + test_fill_date() diff --git a/browser_login/test_vue_injection.py b/browser_login/test_vue_injection.py new file mode 100644 index 0000000..acfacd0 --- /dev/null +++ b/browser_login/test_vue_injection.py @@ -0,0 +1,87 @@ +""" +测试强行注入 Vue 实例修改日期控件的值 +""" +import sys +import time +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent)) +from login import get_page, log + +HOME_URL = "https://yunmes.tftykj.cn/" + +def test_vue_injection(): + log("INFO", "=== 🧪 启动 Vue 实例强行注入测试 ===") + page = get_page(port=9222) + + try: + log("INFO", "⚠️ 假设您已经在保活浏览器中手动打开了【质量报表】页面!") + + # 尝试刷新一下页面,确保处于初始状态 (可选,这里先不刷新,直接找元素) + # page.refresh() + # page.wait.load_start() + # time.sleep(2) + + date_xpath = 'xpath://*[@id="customTable-search-area"]/div[1]/div/div[2]/div[11]/div[2]/span/input[1]' + log("INFO", f"正在当前页面寻找日期输入框: {date_xpath}") + date_input = page.ele(date_xpath, timeout=5) + + if date_input: + log("INFO", "✅ 找到输入框!准备执行 Vue 实例劫持注入...") + + # 黑客核心:拿到 DOM 元素,获取其上的 __vue__ 实例,然后修改其父组件的 value 或 model 值 + # ElementUI 的 el-date-picker 绑定的值通常在自身或其父组件实例上 + # 这里尝试了多种可能的 Vue 内部变量名以确保万无一失 + vue_hack_js = """ + var el = arguments[0]; + if (el.__vue__) { + // 尝试修改自身绑定的值 + el.__vue__.value = '2026-05-01 00:00:00'; + el.__vue__.$emit('input', '2026-05-01 00:00:00'); + el.__vue__.$emit('change', '2026-05-01 00:00:00'); + } + + // 如果外层有包裹的 el-date-picker 父组件 + var parent = el.closest('.el-date-editor'); + if (parent && parent.__vue__) { + parent.__vue__.value = '2026-05-01 00:00:00'; + if (parent.__vue__.userInput) { + parent.__vue__.userInput = '2026-05-01 00:00:00'; + } + parent.__vue__.$emit('input', '2026-05-01 00:00:00'); + parent.__vue__.$emit('change', '2026-05-01 00:00:00'); + } + + // 物理备用手段,防止 Vue 版本差异 + el.value = '2026-05-01 00:00:00'; + el.dispatchEvent(new Event('input', { bubbles: true })); + el.dispatchEvent(new Event('change', { bubbles: true })); + """ + + try: + page.run_js(vue_hack_js, date_input) + log("OK", "✅ Vue 注入指令已发送!请肉眼观察输入框是否有变化。") + except Exception as e: + log("WARN", f"Vue 注入执行时发生警告: {e}") + + time.sleep(2) + + log("INFO", "准备点击【查询】按钮触发网络请求...") + query_btn_xpath = 'xpath://*[@id="customTable-search-area"]/div[1]/div/div[1]/a[2]/span/span' + query_btn = page.ele(query_btn_xpath, timeout=3) + + if query_btn: + try: query_btn.click() + except: page.run_js("arguments[0].click();", query_btn) + log("OK", "✅ 已点击【查询】按钮!请在浏览器中观察页面是否开始刷新 2026-05-01 之后的数据。") + else: + log("ERR", "找不到查询按钮。") + + else: + log("ERR", "找不到日期输入框,请检查 XPath 是否正确!") + + except Exception as e: + log("ERR", f"发生异常: {e}") + +if __name__ == "__main__": + test_vue_injection() \ No newline at end of file diff --git a/page.html b/page.html new file mode 100644 index 0000000..baa915f --- /dev/null +++ b/page.html @@ -0,0 +1,1637 @@ + + + + + + + 腾一飞龙MOM平台 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
-
计划
已执行
完结
-
+ +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
行号
状态
物料代码
物料名称
物料规格
物料型号
仓库名称
领料人
单位名称
发料数量
执行人名称
执行时间
生产物料代码
生产物料名称
生产物料规格
生产物料型号
已发料数量
10
执行
1Z00000004
热缩管 100米1卷
黑色-10mm
五金配件仓
邝居凤 接线装配
PCS
1
莫春兰 仓储部
2026-06-10 17:20:28
1
10
执行
1BBJKC0023
K550-油泵安装板金-版本A3
K550
钣金仓
PCS
1
潘相德 仓储部
2026-06-10 16:48:02
1
10
执行
1BGBDH0003
吊环螺钉 GB/T 825-1988
M20(非标)
五金配件仓
陈佳乐 总装组
PCS
20
莫春兰 仓储部
2026-06-10 14:07:37
20
20
执行
1Z00000002
热缩管 100米1卷
红色-12mm
五金配件仓
邝居凤 接线装配
PCS
1
莫春兰 仓储部
2026-06-10 17:20:36
1
10
执行
1BBJKX0044
K系列操控台平面2025-8-5
K35-45通用
钣金仓
陈月成 总装组
PCS
1
潘相德 仓储部
2026-06-10 10:03:22
1
20
执行
1BBJKX0044
K系列操控台平面2025-8-5
K35-45通用
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 10:47:22
2
30
执行
1BBJKX0005
K350-运丝拖板护罩-版本A6
K35-45通用
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 10:47:37
2
40
执行
1BBJKX0042
AQ435-545操控台支架2025-5-24
K35-45通用
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 10:47:45
2
50
执行
1BBJKX0002
K350-运丝底座护罩2025-8-7
K35-45通用
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 10:47:54
2
60
执行
1BBJKB0024
K450大围水框底框-2023-8-18-A01
K450
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 10:48:56
2
70
执行
1BBJKB0019
K450床身水槽2023-2-1
K450
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 10:58:15
2
80
执行
1BBJKX0045
K450电压表面板2022-10-2091695P
K35-45通用
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 10:58:22
2
90
执行
1BBJKB0015
ALS545中拖板水槽2025-5-25.JPG
K450
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 10:58:32
2
100
执行
1BBJKB0029
K450-左转门(装配体)2023-8-18-A01
K450
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 10:58:39
2
110
执行
1BBJKB0027
AK450K350备份-前门B-A1
K450
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 10:58:48
2
120
执行
1BBJKB0026
AK450K350备份-前门A-A1
K450
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 10:58:58
2
130
执行
1BBJKB0028
K450-右插门-2023-8-18-A01
K450
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 10:59:06
2
140
执行
1BBJKX0032
升降护罩-上-版本A6
K35-45通用
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 10:59:20
2
150
执行
1BBJKX0024
立柱背部钣金-A9-前盖板-量产阶段3
K35-45通用
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 10:59:31
2
160
执行
1BBJKX0027
立柱后钣金-A9-附件2-(量产阶段2)
K35-45通用
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 10:59:40
2
170
执行
1BBJKX0065
AQ锥度头护罩顶部封板2025-8-7
AQ系列通用
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 10:59:50
2
180
执行
1BBJKX0066
AQ435锥度头护罩封板2025-8-7
AQ系列通用
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 11:00:09
2
190
执行
1BBJKX0033
升降护罩-下-版本5
K35-45通用
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 11:00:18
2
200
执行
1BBJKX0062
AQ435锥度头护罩2025-8-7
AQ系列通用
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 11:00:29
2
210
执行
1BBJKB0004
AQ545上丝臂护罩2025-8-16
K450
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 11:00:41
2
220
执行
1BBJKH0035
二次升降工作台前装饰板-A2
ALS545
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 11:00:48
2
230
执行
1BBJKH0031
k45-二次升降中拖板前护罩-后-附件-改
ALS545
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 11:00:59
2
240
执行
1BBJKH0034
k45-二次升降工作后装饰板
ALS545
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 11:01:07
2
250
执行
1BBJKX0064
AQ435锥度头下拖板右封板2025-8-7
AQ系列通用
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 11:01:16
2
260
执行
1BBJKX0063
AQ435锥度头下拖板左挡板2025-8-7
AQ系列通用
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 11:01:25
2
270
执行
1BBJKB0005
AQ545上丝臂护罩后封板2025-8-16
K450
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 11:01:32
2
280
执行
1BBJKH0038
ALS545运丝压线钣金
ALS545
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 11:02:33
2
290
执行
1BBJKX0008
油泵安装板金-版本A4-量产阶段3
K35-45通用
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 11:09:01
2
300
执行
1BBJKX0006
丝筒移门挡水罩-版本A3
K35-45通用
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 11:09:25
2
310
执行
1BBJKX0009
K350-运丝接水-A6-量产阶段3
K35-45通用
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 11:10:04
2
320
执行
1BBJKB0003
K450-下丝臂护罩-版本A16-附件1-量产阶段3
K450
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 11:10:13
2
330
执行
1BBJKB0002
K450-下丝臂护罩-版本A7-附件1
K450
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 11:10:20
2
340
执行
1BBJKX0007
行程支架-版本A5-(量产阶段3)
K35-55通用
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 11:10:28
2
350
执行
1BBJKX0003
K350-运丝底座护罩-版本A8-(量产阶段3)-附件1
K35-45通用
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 11:10:36
2
360
执行
1BBJKX0010
运丝接水盘2025-4-5
AQ ALS AG通用
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 11:10:46
2
370
执行
1BBJKB0025
K450新水框移门框
K450
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 11:11:00
2
380
执行
1BBJKH0028
k45-二次升降中拖板左侧钣金
ALS545
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 11:11:26
2
390
执行
1BBJKX0073
运丝行程支架右拨叉
AQ、ALS、C系列通用
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 11:11:34
2
400
执行
1BBJKX0072
运丝行程支架左拨叉
AQ、ALS、C系列通用
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 11:11:40
2
410
执行
1BBJKX0011
AQ系列运丝拖板加压式油排2025-12-23
K35-45通用
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 11:11:50
2
420
执行
1BBJKX0039
K350新显示器支架2023-5-11
K35-45通用
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 11:11:59
2
430
执行
1BBJKB0023
K450床身侧封板
K450
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 11:12:10
2
440
执行
1BBJKB0023
K450床身侧封板
K450
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 11:12:39
2
450
执行
1BBJKB0020
K450工作台铁板2023-1-11
K450
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 11:12:48
2
460
执行
1BBJKX0047
K350床身后封板2023-7-6
K35-45通用
钣金仓
陈月成 总装组
PCS
2
潘相德 仓储部
2026-06-10 11:12:56
2
+ + +
+
+ 销售订单号: + + 客户代码: + 客户名称: +
+
+ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web_ui/app.py b/web_ui/app.py index e3fa00c..1434666 100644 --- a/web_ui/app.py +++ b/web_ui/app.py @@ -44,13 +44,31 @@ BROWSER_LOGIN_DIR = BASE_DIR / "browser_login" browser_lock = threading.Lock() is_browser_busy = False current_task_name = "" +task_logs = [] + +class WebLogger: + def __init__(self, orig): + self.orig = orig + def write(self, text): + self.orig.write(text) + msg = text.strip() + if msg and is_browser_busy: + task_logs.append(msg) + if len(task_logs) > 500: + task_logs.pop(0) + def flush(self): + self.orig.flush() + +sys.stdout = WebLogger(sys.stdout) +sys.stderr = WebLogger(sys.stderr) def set_browser_busy(task_name): """设置浏览器为忙碌状态""" - global is_browser_busy, current_task_name + global is_browser_busy, current_task_name, task_logs with browser_lock: is_browser_busy = True current_task_name = task_name + task_logs.clear() def release_browser(): """释放浏览器控制权""" @@ -127,6 +145,16 @@ def receipts_page(): """渲染收货明细数据看板""" return render_template('index.html') +@app.route('/work_orders') +def work_orders_page(): + """渲染生产工单数据看板""" + return render_template('work_orders.html') + +@app.route('/abnormal_report') +def abnormal_report_page(): + """渲染发料异常检查数据看板""" + return render_template('abnormal_report.html') + @app.route('/api/receipts') def get_receipts(): """获取收货明细数据(支持分页和多条件搜索)""" @@ -175,6 +203,102 @@ def get_receipts(): "rows": [dict(ix) for ix in receipts] }) +@app.route('/api/work_orders') +def get_work_orders(): + """获取生产工单数据(支持分页和多条件搜索)""" + page = int(request.args.get('page', 1)) + limit = int(request.args.get('limit', 50)) + offset = (page - 1) * limit + + # 获取搜索参数 + wo_number = request.args.get('work_orders_number', '').strip() + material_name = request.args.get('material_name', '').strip() + material_code = request.args.get('material_code', '').strip() + + conn = get_db_connection() + + # 构建动态 SQL 查询 + query_conditions = [] + params = [] + + if wo_number: + query_conditions.append("work_orders_number LIKE ?") + params.append(f"%{wo_number}%") + if material_name: + query_conditions.append("material_name LIKE ?") + params.append(f"%{material_name}%") + if material_code: + query_conditions.append("material_code LIKE ?") + params.append(f"%{material_code}%") + + where_clause = "" + if query_conditions: + where_clause = " WHERE " + " AND ".join(query_conditions) + + # 获取总数 + count_query = f"SELECT COUNT(*) FROM work_orders{where_clause}" + total = conn.execute(count_query, params).fetchone()[0] + + # 获取分页数据 + data_query = f"SELECT * FROM work_orders{where_clause} ORDER BY execution_time DESC LIMIT ? OFFSET ?" + params.extend([limit, offset]) + orders = conn.execute(data_query, params).fetchall() + + conn.close() + + return jsonify({ + "total": total, + "rows": [dict(ix) for ix in orders] + }) + +@app.route('/api/abnormal_report') +def get_abnormal_report(): + """获取发料异常报表数据(支持分页和多条件搜索)""" + page = int(request.args.get('page', 1)) + limit = int(request.args.get('limit', 50)) + offset = (page - 1) * limit + + # 获取搜索参数 + wo_number = request.args.get('work_orders_number', '').strip() + material_code = request.args.get('material_code', '').strip() + issue_status = request.args.get('issue_status', '').strip() + + conn = get_db_connection() + + # 构建动态 SQL 查询 + query_conditions = [] + params = [] + + if wo_number: + query_conditions.append("work_orders_number LIKE ?") + params.append(f"%{wo_number}%") + if material_code: + query_conditions.append("material_code LIKE ?") + params.append(f"%{material_code}%") + if issue_status: + query_conditions.append("issue_status = ?") + params.append(issue_status) + + where_clause = "" + if query_conditions: + where_clause = " WHERE " + " AND ".join(query_conditions) + + # 获取总数 + count_query = f"SELECT COUNT(*) FROM abnormal_report{where_clause}" + total = conn.execute(count_query, params).fetchone()[0] + + # 获取分页数据 (默认按工单号倒序排列) + data_query = f"SELECT * FROM abnormal_report{where_clause} ORDER BY work_orders_number DESC LIMIT ? OFFSET ?" + params.extend([limit, offset]) + orders = conn.execute(data_query, params).fetchall() + + conn.close() + + return jsonify({ + "total": total, + "rows": [dict(ix) for ix in orders] + }) + @app.route('/api/task_status') def get_task_status(): """获取当前浏览器控制任务的状态""" @@ -183,6 +307,11 @@ def get_task_status(): "task_name": current_task_name }) +@app.route('/api/task_logs') +def get_task_logs(): + """获取实时日志""" + return jsonify({"logs": task_logs}) + @app.route('/api/sync_receipts', methods=['POST']) def sync_receipts(): """触发后台运行收货明细增量抓取脚本(修复跨机器路径问题)""" @@ -222,6 +351,85 @@ def sync_receipts(): except Exception as e: return jsonify({"success": False, "message": f"系统错误: {str(e)}"}), 500 +@app.route('/api/sync_work_orders', methods=['POST']) +def sync_work_orders(): + """触发后台运行生产工单增量抓取脚本""" + global is_browser_busy + import sys + + if is_browser_busy: + return jsonify({ + "success": False, + "message": f"系统忙碌:当前正在执行 '{current_task_name}',请稍后再试。" + }), 409 + + if str(BROWSER_LOGIN_DIR) not in sys.path: + sys.path.insert(0, str(BROWSER_LOGIN_DIR)) + + def run_work_orders_sync(): + set_browser_busy("手动生产工单增量同步") + try: + from fetch_work_orders_incremental import fetch_work_orders_incremental + fetch_work_orders_incremental() + except Exception as e: + print(f"手动生产工单同步失败: {e}") + finally: + release_browser() + + try: + threading.Thread(target=run_work_orders_sync, daemon=True).start() + + return jsonify({ + "success": True, + "message": "工单增量同步任务已在后台启动!请观察黑框控制台的运行日志。", + "logs": "任务已在后台运行..." + }) + except ImportError: + return jsonify({"success": False, "message": "找不到增量抓取脚本或导入失败"}), 404 + except Exception as e: + return jsonify({"success": False, "message": f"系统错误: {str(e)}"}), 500 + +@app.route('/api/sync_abnormal_report', methods=['POST']) +def sync_abnormal_report(): + """触发后台运行异常报表抓取脚本""" + global is_browser_busy + import sys + + if is_browser_busy: + return jsonify({ + "success": False, + "message": f"系统忙碌:当前正在执行 '{current_task_name}',请稍后再试。" + }), 409 + + if str(BROWSER_LOGIN_DIR) not in sys.path: + sys.path.insert(0, str(BROWSER_LOGIN_DIR)) + + def run_abnormal_report_sync(): + set_browser_busy("手动发料异常报表抓取") + try: + import auto_fetch_abnormal_report + page = auto_fetch_abnormal_report.get_page(port=9222) + success = auto_fetch_abnormal_report.navigate_to_report(page) + if success: + auto_fetch_abnormal_report.fetch_report_data(page) + except Exception as e: + print(f"手动发料异常报表抓取失败: {e}") + finally: + release_browser() + + try: + threading.Thread(target=run_abnormal_report_sync, daemon=True).start() + + return jsonify({ + "success": True, + "message": "发料异常报表抓取任务已在后台启动!请观察黑框控制台的运行日志。", + "logs": "任务已在后台运行..." + }) + except ImportError: + return jsonify({"success": False, "message": "找不到异常报表抓取脚本或导入失败"}), 404 + except Exception as e: + return jsonify({"success": False, "message": f"系统错误: {str(e)}"}), 500 + @app.route('/api/sync_bom', methods=['POST']) def sync_bom(): """触发后台运行 BOM 成本全量抓取脚本(修复跨机器路径问题)""" diff --git a/web_ui/templates/abnormal_report.html b/web_ui/templates/abnormal_report.html new file mode 100644 index 0000000..101fbe1 --- /dev/null +++ b/web_ui/templates/abnormal_report.html @@ -0,0 +1,287 @@ + + + + + + 发料异常检查报表 + + + + + + + + + + + +
+ + + + +
+ + + + + + + + + + + + 搜索 + 重置 + + + +
+ + + + + + + + +
+
+
正在启动任务,等待输出...
+
+ + 关闭窗口 (后台会继续执行) + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+
+ + + + \ No newline at end of file diff --git a/web_ui/templates/home.html b/web_ui/templates/home.html index c494d53..2f4f65f 100644 --- a/web_ui/templates/home.html +++ b/web_ui/templates/home.html @@ -104,6 +104,12 @@ .card-bom-compare:hover { border-color: #E6A23C; background-color: #fdf6ec; } .card-bom-compare i { color: #E6A23C; } + .card-work-order { border-top: 4px solid #E6A23C; } + .card-work-order i { color: #E6A23C; } + + .card-abnormal { border-top: 4px solid #F56C6C; } + .card-abnormal i { color: #F56C6C; } + .action-group { margin-top: 40px; padding-top: 30px; @@ -142,6 +148,20 @@

期间成本对比分析表

跨时间段核算 BOM 最新价差异,支持虚拟件过滤与历史价回溯。

+ + + + + +
@@ -166,6 +186,15 @@ round> + + + +
@@ -176,6 +205,7 @@ return { syncing: false, syncingBom: false, + syncingWorkOrders: false, isSystemBusy: false, globalTaskName: "", statusTimer: null @@ -249,6 +279,29 @@ .finally(() => { this.syncingBom = false; }); + }, + syncWorkOrders() { + this.syncingWorkOrders = true; + + axios.post('/api/sync_work_orders') + .then(res => { + if (res.data.success) { + this.$message.success('已触发!' + res.data.message); + setTimeout(this.checkTaskStatus, 500); + } else { + this.$message.error('触发失败:' + res.data.message); + } + }) + .catch(err => { + if (err.response && err.response.status === 409) { + this.$message.warning(err.response.data.message); + } else { + this.$message.error('请求发生异常,请检查后端日志。'); + } + }) + .finally(() => { + this.syncingWorkOrders = false; + }); } } }); diff --git a/web_ui/templates/work_orders.html b/web_ui/templates/work_orders.html new file mode 100644 index 0000000..5d12666 --- /dev/null +++ b/web_ui/templates/work_orders.html @@ -0,0 +1,171 @@ + + + + + + 生产工单明细看板 + + + + + + + + + + + +
+ + + + +
+ + + + + + + + + + 搜索 + 重置 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+
+ + + + \ No newline at end of file