抓取生产工单,抓取发料异常
This commit is contained in:
@@ -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} 页)...")
|
||||
|
||||
@@ -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", "开始模拟人工点击左侧导航菜单...")
|
||||
|
||||
@@ -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 内置的热加载,避免双进程互相影响
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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';
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 雷达图 -->
|
||||
|
||||
@@ -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';
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user