抓取生产工单,抓取发料异常

This commit is contained in:
hjq
2026-06-11 19:38:16 +08:00
parent a160d5d48f
commit 94c81cdc4f
10 changed files with 160 additions and 28 deletions

View File

@@ -73,6 +73,22 @@ def fetch_report_data(page):
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')
# ==== 断点续传逻辑 ====
state_file = OUTPUT_DIR / 'abnormal_sync_state.json'
start_page = 1
if state_file.exists():
try:
with open(state_file, 'r', encoding='utf-8') as f:
state = json.load(f)
if state.get('month') == f"{now.year}-{now.month}":
saved_page = state.get('current_page', 1)
if saved_page > 1:
start_page = saved_page
print(f"发现上次中断记录,准备从第 {start_page} 页恢复抓取...")
except Exception as e:
print(f"读取状态文件失败: {e}")
# ====================
print(f"设置下单日期为当月: {first_day}{last_day},并清理发料情况过滤条件...")
# 使用注入到全部 iframe 的 JS 强制执行 EasyUI 方法
@@ -121,7 +137,6 @@ def fetch_report_data(page):
}}
// 4. [提速黑科技]:强行把每页请求的数量从 50 条改为 500 条
// 找到底部的分页组件并修改它的 pageSize这样点击查询时就会一次请求 500 条
var paginations = doc.querySelectorAll('.pagination');
for(var i=0; i<paginations.length; i++) {{
try {{ win.$(paginations[i]).pagination({{pageSize: 500}}); }} catch(e) {{}}
@@ -168,20 +183,25 @@ def fetch_report_data(page):
current_page = 1
total_inserted = 0
total_pages = 1
print("开始监听网络请求,寻找 API 数据包...")
while True:
print(f"[{datetime.datetime.now().strftime('%H:%M:%S')}] 正在收集并解析网络数据包...")
packets = target_tab.listen.steps(timeout=5)
print(f"[{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 正在收集并解析网络数据包...")
try:
packets = target_tab.listen.steps(timeout=5)
except Exception as e:
print(f"❌ 监听数据包时页面发生异常 (可能是会话超时跳转): {e}")
print("♻️ 准备触发断点续传机制,重新进入菜单...")
return False
found_data = False
total_pages = 1
for p in packets:
if 'SearchCustomReportBySQL_Proxy' in p.url or 'CustomTableViewData' in p.url or 'SeachList' in p.url:
print(f"[{datetime.datetime.now().strftime('%H:%M:%S')}] 命中目标 URL: {p.url[:100]}...")
print(f"[{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 命中目标 URL: {p.url[:100]}...")
if p.method == 'POST' and p.response and p.response.body:
print(f"[{datetime.datetime.now().strftime('%H:%M:%S')}] 这是一个 POST 请求,且包含 response body")
print(f"[{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 这是一个 POST 请求,且包含 response body")
try:
body = p.response.body
data = body if isinstance(body, (dict, list)) else json.loads(body)
@@ -211,14 +231,34 @@ def fetch_report_data(page):
print(f"❌ 保存异常报表数据到数据库失败: {db_err}")
found_data = True
# 只有当我们不是处于准备跳页的初始阶段时,才将进度记录到文件
if not (current_page == 1 and start_page > 1):
try:
with open(state_file, 'w', encoding='utf-8') as f:
json.dump({
'month': f"{now.year}-{now.month}",
'current_page': current_page,
'total_pages': total_pages
}, f)
except Exception as e:
print(f"保存进度失败: {e}")
pass
else:
print(f"[{datetime.datetime.now().strftime('%H:%M:%S')}] 数据结构不匹配。")
print(f"[{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 数据结构不匹配。")
except Exception as e:
print(f"[{datetime.datetime.now().strftime('%H:%M:%S')}] 解析数据包出错: {e}")
print(f"[{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 解析数据包出错: {e}")
pass
if not found_data:
print(f"[{datetime.datetime.now().strftime('%H:%M:%S')}] 第 {current_page} 页等待了超时,没有拦截到匹配的报表数据...")
# 检查是否由于会话超时被系统强制跳转回首页
if "Home/Index" in target_tab.url or target_tab.url == "https://yunmes.tftykj.cn/":
print("❌ 警告:页面已跳转回首页,可能是会话超时或被强制登出。")
print(f"进度已保存 (停留在第 {current_page} 页),下次启动抓取任务将自动从中断处继续!")
return False
print(f"[{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 第 {current_page} 页等待了超时,没有拦截到匹配的报表数据...")
# 再给一次机会等3秒
print("再等待3秒重试...")
@@ -226,8 +266,36 @@ def fetch_report_data(page):
# 重新让上面解析
continue
# ====== 触发断点续传跳页 ======
if current_page == 1 and start_page > 1:
print(f"===================================")
print(f"⏭️ 触发断点续传,跳过第 1 页,直接跳转到第 {start_page} 页...")
print(f"===================================")
current_page = start_page
target_tab.run_js(f"""
var iframes = document.querySelectorAll('iframe');
for(var j=0; j<iframes.length; j++) {{
try {{
var doc = iframes[j].contentDocument || iframes[j].contentWindow.document;
var win = iframes[j].contentWindow;
var paginations = doc.querySelectorAll('.pagination');
for(var i=0; i<paginations.length; i++) {{
try {{ win.$(paginations[i]).pagination('select', {start_page}); }} catch(e) {{}}
}}
}} catch(e) {{}}
}}
""")
time.sleep(2)
continue
# ==============================
if current_page >= total_pages:
print(f"已到达最后一页 (共 {total_pages} 页),抓取完成!")
try:
if state_file.exists():
state_file.unlink() # 抓取完毕后清除记录
except:
pass
break
print(f"准备抓取下一页 (第 {current_page + 1} 页)...")

View File

@@ -40,7 +40,7 @@ def fetch_receipt_details_full():
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[4]/div/p'),
("第三层: 收货明细报表", 'xpath:/html/body/div[8]/div/div[1]/div/div[4]/div/p')
("第三层: 财务收货明细报表", 'xpath:/html/body/div[8]/div/div[1]/div/div[4]/div/p')
]
log("INFO", "开始模拟人工点击左侧导航菜单...")

View File

@@ -489,11 +489,34 @@ def sync_abnormal_report():
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)
# 自动重试机制
while True:
try:
success = auto_fetch_abnormal_report.navigate_to_report(page)
if success:
result = auto_fetch_abnormal_report.fetch_report_data(page)
# 如果返回 False说明被踢回首页或发生中断需要重试
if result is False:
print("♻️ 检测到页面被系统踢回首页或中断,正在为您自动重新导航并恢复抓取...")
import time
time.sleep(3)
continue
else:
break
else:
print("❌ 导航进入报表失败,尝试重新导航...")
import time
time.sleep(3)
continue
except Exception as inner_e:
print(f"⚠️ 抓取循环中发生异常: {inner_e},尝试重新恢复...")
import time
time.sleep(3)
continue
except Exception as e:
print(f"手动发料异常报表抓取失败: {e}")
print(f"手动发料异常报表抓取严重失败: {e}")
finally:
release_browser()
@@ -1207,15 +1230,17 @@ if __name__ == '__main__':
# 启动前开启一个线程去拉起浏览器
threading.Thread(target=open_browser, args=(port,), daemon=True).start()
print(f"🚀 前端展示后端服务已启动!请在浏览器访问: http://127.0.0.1:{port}")
print("🚀 前端展示后端服务已启动!")
print(f"👉 本机访问地址: http://127.0.0.1:{port}")
print(f"👉 局域网访问地址: http://192.168.7.198:{port} (允许手机/其他电脑访问)")
else:
# 如果是热加载的主控进程,随便给个默认端口(反正它不干活),并且不打开浏览器
port = 5050
# 更改为动态端口,避开被占用的端口。修改 host 为 127.0.0.1 避免 Windows 权限拦截
# 更改为动态端口,避开被占用的端口。修改 host 为 0.0.0.0 允许局域网访问
app.run(
debug=not is_frozen,
host='127.0.0.1',
host='0.0.0.0',
port=port,
threaded=True,
use_reloader=False # 彻底关闭 Flask 内置的热加载,避免双进程互相影响

View File

@@ -42,6 +42,7 @@
<el-select v-model="searchParams.issue_status" placeholder="发料情况" style="width: 150px" clearable @change="handleSearch">
<el-option label="全部" value=""></el-option>
<el-option label="发料正常" value="发料正常"></el-option>
<el-option label="发料异常" value="发料异常"></el-option>
<el-option label="未发料" value="未发料"></el-option>
</el-select>
<el-button type="primary" icon="el-icon-search" @click="handleSearch">搜索</el-button>
@@ -123,7 +124,10 @@
<el-table-column prop="issue_status" label="发料情况" width="100" align="center">
<template slot-scope="scope">
<el-tag :type="scope.row.issue_status === '超领发料' ? 'danger' : (scope.row.issue_status === '未发料' ? 'info' : 'warning')" size="small" v-text="scope.row.issue_status">
<el-tag
:type="scope.row.issue_status === '发料异常' ? 'danger' : (scope.row.issue_status === '未发料' ? 'info' : (scope.row.issue_status === '发料正常' ? 'success' : 'warning'))"
size="small"
v-text="scope.row.issue_status">
</el-tag>
</template>
</el-table-column>

View File

@@ -171,6 +171,7 @@
<div class="header">
<h2>📊 BOM 成本期间对比分析表</h2>
<div>
<el-button type="info" plain icon="el-icon-back" @click="goBack" size="small">返回主控台</el-button>
<el-button type="text" @click="goToBomTree">返回雷达图</el-button>
<el-button type="text" @click="goToReceipts">返回收货明细</el-button>
</div>
@@ -435,7 +436,8 @@
this.searchParents();
},
methods: {
goToReceipts() { window.location.href = '/'; },
goBack() { window.location.href = '/'; },
goToReceipts() { window.location.href = '/receipts'; },
goToBomTree() { window.location.href = '/bom'; },
getStatusText(status) {

View File

@@ -122,7 +122,8 @@
<div class="header">
<h2>🕸️ ERP 动态 BOM 成本核算雷达图</h2>
<div>
<el-button type="primary" @click="goToCompare" plain>切换至 BOM 期间成本对比</el-button>
<el-button type="info" plain icon="el-icon-back" @click="goBack" size="small">返回主控台</el-button>
<el-button type="primary" @click="goToCompare" plain size="small">切换至 BOM 期间成本对比</el-button>
<el-button type="text" @click="goToReceipts">返回收货明细</el-button>
</div>
</div>
@@ -233,9 +234,12 @@
});
},
methods: {
goToReceipts() {
goBack() {
window.location.href = '/';
},
goToReceipts() {
window.location.href = '/receipts';
},
goToCompare() {
window.location.href = '/compare';
},

View File

@@ -15,8 +15,7 @@
<div v-if="logs.length === 0" style="color: #666; text-align: center; margin-top: 150px;">
暂无后台任务输出...
</div>
<div v-for="(log, index) in logs" :key="index" style="margin-bottom: 2px;">
{{ log }}
<div v-for="(log, index) in logs" :key="index" style="margin-bottom: 2px;" :style="getLogStyle(log)" v-text="log">
</div>
</div>
<span slot="footer" class="dialog-footer">
@@ -62,6 +61,17 @@
}
},
methods: {
getLogStyle(log) {
if (!log) return '';
if (log.includes('✅') || log.includes('大功告成')) return 'color: #67C23A; font-weight: bold;'; // 绿色
if (log.includes('❌') || log.includes('ERR') || log.includes('失败') || log.includes('异常')) return 'color: #F56C6C; font-weight: bold;'; // 红色
if (log.includes('⏭️') || log.includes('断点续传') || log.includes('中断记录')) return 'color: #E040FB; font-weight: bold;'; // 紫色
if (log.includes('正在收集并解析') || log.includes('准备抓取下一页') || log.includes('等待重试') || log.includes('警告')) return 'color: #E6A23C;'; // 橙黄色
if (log.includes('===================================')) return 'color: #409EFF; font-weight: bold;'; // 蓝色
if (log.includes('命中目标 URL') || log.includes('POST 请求')) return 'color: #909399; font-style: italic;'; // 灰色斜体
if (log.includes('开始') || log.includes('完成')) return 'color: #FFF; font-weight: bold;'; // 白色高亮
return 'color: #a9b7c6;'; // 默认灰白色
},
fetchLogs() {
// 如果弹窗没打开,也可以不刷新日志以节省性能,或者一直拉取保持最新
if (!this.logDialogVisible) return;

View File

@@ -171,8 +171,8 @@
<!-- 卡片 1: 收货明细表 -->
<div class="nav-card card-receipt" onclick="window.location.href='/receipts'">
<i class="el-icon-document"></i>
<h3>收货明细报表</h3>
<p>查看、搜索所有历史收货记录及详细价格数据。</p>
<h3>财务收货明细报表</h3>
<p>查看、搜索所有历史财务收货记录及详细价格数据。</p>
</div>
<!-- 卡片 2: BOM 雷达图 -->

View File

@@ -23,6 +23,9 @@
background-color: #f5f7fa;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #ebeef5;
@@ -46,8 +49,11 @@
<body>
<div id="app">
<div class="header">
<h2>📦 ERP 数据展示看板 - 收货明细报表</h2>
<el-button type="primary" @click="goToBomTree" icon="el-icon-data-analysis">切换至 BOM 成本雷达图</el-button>
<h2>📦 ERP 数据展示看板 - 财务收货明细报表</h2>
<div>
<el-button type="info" plain icon="el-icon-back" @click="goBack" size="small">返回主控台</el-button>
<el-button type="primary" size="small" @click="goToBomTree" icon="el-icon-data-analysis">切换至 BOM 成本雷达图</el-button>
</div>
</div>
<div class="card">
@@ -132,6 +138,9 @@
this.fetchData();
},
methods: {
goBack() {
window.location.href = '/';
},
goToBomTree() {
window.location.href = '/bom';
},

View File

@@ -244,6 +244,16 @@
}
},
mounted() {
// 默认初始化为当月的第一天和最后一天
const now = new Date();
const y = now.getFullYear();
const m = now.getMonth() + 1;
const pad = (n) => n.toString().padStart(2, '0');
const lastDayDate = new Date(y, m, 0);
const firstDay = `${y}-${pad(m)}-01`;
const lastDay = `${y}-${pad(m)}-${pad(lastDayDate.getDate())}`;
this.dateRangeSelect = [firstDay, lastDay];
// 初始化加载数据
this.loadSummary();
this.loadUnmatchedData();