html逻辑优化
This commit is contained in:
151
web_ui/app.py
151
web_ui/app.py
@@ -40,6 +40,25 @@ DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
# 抓取脚本目录 (属于代码文件,从 BASE_DIR 加载)
|
||||
BROWSER_LOGIN_DIR = BASE_DIR / "browser_login"
|
||||
|
||||
# 全局互斥锁机制:保证同一时间只有一个任务在控制浏览器
|
||||
browser_lock = threading.Lock()
|
||||
is_browser_busy = False
|
||||
current_task_name = ""
|
||||
|
||||
def set_browser_busy(task_name):
|
||||
"""设置浏览器为忙碌状态"""
|
||||
global is_browser_busy, current_task_name
|
||||
with browser_lock:
|
||||
is_browser_busy = True
|
||||
current_task_name = task_name
|
||||
|
||||
def release_browser():
|
||||
"""释放浏览器控制权"""
|
||||
global is_browser_busy, current_task_name
|
||||
with browser_lock:
|
||||
is_browser_busy = False
|
||||
current_task_name = ""
|
||||
|
||||
def auto_init_db():
|
||||
"""如果是新环境首次运行,自动初始化数据库表结构"""
|
||||
if str(BROWSER_LOGIN_DIR) not in sys.path:
|
||||
@@ -56,7 +75,16 @@ auto_init_db()
|
||||
|
||||
def background_sync_job():
|
||||
"""APScheduler 后台定时任务执行增量抓取"""
|
||||
global is_browser_busy
|
||||
print("[定时任务] 正在执行后台增量数据同步...")
|
||||
|
||||
# 检查浏览器是否被其他任务占用
|
||||
if is_browser_busy:
|
||||
print(f"[定时任务] ⚠️ 浏览器当前正被 '{current_task_name}' 占用,本次增量同步跳过。")
|
||||
return
|
||||
|
||||
set_browser_busy("定时后台增量同步")
|
||||
|
||||
if str(BROWSER_LOGIN_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(BROWSER_LOGIN_DIR))
|
||||
|
||||
@@ -69,6 +97,8 @@ def background_sync_job():
|
||||
print(f"[定时任务] 同步完成。")
|
||||
except Exception as e:
|
||||
print(f"[定时任务] 同步失败: {str(e)}")
|
||||
finally:
|
||||
release_browser()
|
||||
|
||||
# 初始化定时调度器
|
||||
scheduler = BackgroundScheduler()
|
||||
@@ -145,19 +175,42 @@ def get_receipts():
|
||||
"rows": [dict(ix) for ix in receipts]
|
||||
})
|
||||
|
||||
@app.route('/api/task_status')
|
||||
def get_task_status():
|
||||
"""获取当前浏览器控制任务的状态"""
|
||||
return jsonify({
|
||||
"is_busy": is_browser_busy,
|
||||
"task_name": current_task_name
|
||||
})
|
||||
|
||||
@app.route('/api/sync_receipts', methods=['POST'])
|
||||
def sync_receipts():
|
||||
"""触发后台运行收货明细增量抓取脚本(修复跨机器路径问题)"""
|
||||
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_receipt_sync():
|
||||
set_browser_busy("手动收货明细增量同步")
|
||||
try:
|
||||
from fetch_receipt_details_incremental import fetch_receipt_details_incremental
|
||||
fetch_receipt_details_incremental()
|
||||
except Exception as e:
|
||||
print(f"手动增量同步失败: {e}")
|
||||
finally:
|
||||
release_browser()
|
||||
|
||||
try:
|
||||
from fetch_receipt_details_incremental import fetch_receipt_details_incremental
|
||||
|
||||
# 将长时间运行的抓取任务放入后台线程,防止前端请求超时
|
||||
threading.Thread(target=fetch_receipt_details_incremental, daemon=True).start()
|
||||
threading.Thread(target=run_receipt_sync, daemon=True).start()
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
@@ -172,16 +225,31 @@ def sync_receipts():
|
||||
@app.route('/api/sync_bom', methods=['POST'])
|
||||
def sync_bom():
|
||||
"""触发后台运行 BOM 成本全量抓取脚本(修复跨机器路径问题)"""
|
||||
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_bom_sync():
|
||||
set_browser_busy("手动 BOM 全量树抓取")
|
||||
try:
|
||||
from fetch_bom_cost_full_tree import fetch_bom_cost_tree
|
||||
fetch_bom_cost_tree()
|
||||
except Exception as e:
|
||||
print(f"BOM 抓取失败: {e}")
|
||||
finally:
|
||||
release_browser()
|
||||
|
||||
try:
|
||||
from fetch_bom_cost_full_tree import fetch_bom_cost_tree
|
||||
|
||||
# 将长时间运行的抓取任务放入后台线程,防止前端请求超时
|
||||
threading.Thread(target=fetch_bom_cost_tree, daemon=True).start()
|
||||
threading.Thread(target=run_bom_sync, daemon=True).start()
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
@@ -359,13 +427,14 @@ def get_bom_tree(parent_code):
|
||||
if parent_id in nodes_map:
|
||||
nodes_map[parent_id]['children'].append(node_data)
|
||||
|
||||
# 5. 递归处理:计算树的成本,并且根据是否有子件修改圆圈的实心/空心样式
|
||||
# 5. 递归处理:计算树的成本,并且根据是否有子件修改圆圈的实心/空心样式
|
||||
def process_tree_nodes(node, prices_dict):
|
||||
node_cost = 0.0
|
||||
price_record = prices_dict.get(node['materialCode'])
|
||||
own_price = price_record['receive_price'] if price_record and isinstance(price_record['receive_price'], (int, float)) else 0.0
|
||||
|
||||
has_children = bool(node.get('children'))
|
||||
code = node.get('materialCode', '')
|
||||
|
||||
# 【核心视觉区分:还能不能点开?】
|
||||
# 如果它有子件(还能点开展开),就让它变成实心的(填充颜色与边框一致)
|
||||
@@ -379,18 +448,29 @@ def get_bom_tree(parent_code):
|
||||
node['itemStyle']['color'] = '#FFFFFF' # 白色填充(空心)
|
||||
node['itemStyle']['borderWidth'] = 2 # 加粗彩色边框
|
||||
|
||||
# 乘以该节点的耗用量,得到该节点的总成本贡献
|
||||
usage_qty = float(node.get('usageQty', 1.0))
|
||||
|
||||
if not has_children:
|
||||
node_cost = own_price
|
||||
node_cost = node_cost * usage_qty
|
||||
else:
|
||||
for child in node['children']:
|
||||
node_cost += process_tree_nodes(child, prices_dict)
|
||||
|
||||
if node_cost == 0.0 and own_price > 0:
|
||||
node_cost = own_price
|
||||
|
||||
# 乘以该节点的耗用量,得到该节点的总成本贡献
|
||||
usage_qty = float(node.get('usageQty', 1.0))
|
||||
node_cost = node_cost * usage_qty
|
||||
|
||||
# 【工序件特殊处理逻辑】
|
||||
# 如果当前节点是 2 或 3 开头的加工工序件,由于物理上是同一个东西的流转,
|
||||
# BOM 表中的用量只是为了匹配底层的原材料用量,不能再重复相乘。
|
||||
# 所以强行忽略 2 或 3 开头工序件的自身 BOM 用量,直接 1:1 继承其子件汇总的成本。
|
||||
if code.startswith('2') or code.startswith('3'):
|
||||
# 保持 node_cost 不变,相当于乘以 1
|
||||
pass
|
||||
else:
|
||||
# 正常的装配组件,需要将子件汇总成本乘以自身用量
|
||||
node_cost = node_cost * usage_qty
|
||||
|
||||
node['totalCost'] = round(node_cost, 2)
|
||||
node['ownPrice'] = round(own_price, 2)
|
||||
@@ -652,9 +732,16 @@ def build_bom_compare_tree(parent_code, start_a, end_a, start_b, end_b):
|
||||
total_a += child['totalPeriodA']
|
||||
total_b += child['totalPeriodB']
|
||||
|
||||
node['totalLatest'] = (total_latest if total_latest > 0 else node['latestUnitPrice']) * usage_qty
|
||||
node['totalPeriodA'] = (total_a if total_a > 0 else node['periodAUnitPrice']) * usage_qty
|
||||
node['totalPeriodB'] = (total_b if total_b > 0 else node['periodBUnitPrice']) * usage_qty
|
||||
# 【工序件特殊处理逻辑】
|
||||
# 同样在成本对比页面,2或3开头的工序件不能重复乘以用量,直接继承子件成本
|
||||
if code.startswith('2') or code.startswith('3'):
|
||||
node['totalLatest'] = total_latest if total_latest > 0 else node['latestUnitPrice']
|
||||
node['totalPeriodA'] = total_a if total_a > 0 else node['periodAUnitPrice']
|
||||
node['totalPeriodB'] = total_b if total_b > 0 else node['periodBUnitPrice']
|
||||
else:
|
||||
node['totalLatest'] = (total_latest if total_latest > 0 else node['latestUnitPrice']) * usage_qty
|
||||
node['totalPeriodA'] = (total_a if total_a > 0 else node['periodAUnitPrice']) * usage_qty
|
||||
node['totalPeriodB'] = (total_b if total_b > 0 else node['periodBUnitPrice']) * usage_qty
|
||||
|
||||
node['showAStatus'] = 'normal'
|
||||
node['showBStatus'] = 'normal'
|
||||
@@ -811,27 +898,33 @@ def find_free_port(start_port=5050, max_port=5100):
|
||||
return s.getsockname()[1]
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 动态获取一个可用端口
|
||||
try:
|
||||
port = find_free_port()
|
||||
except Exception as e:
|
||||
print(f"⚠️ 无法自动获取端口,回退到默认端口: {e}")
|
||||
port = 5050
|
||||
|
||||
# 启动前开启一个线程去拉起浏览器
|
||||
threading.Thread(target=open_browser, args=(port,), daemon=True).start()
|
||||
|
||||
# 启动后端服务
|
||||
print(f"🚀 前端展示后端服务已启动!请在浏览器访问: http://127.0.0.1:{port}")
|
||||
|
||||
# 智能判断:如果是通过 PyInstaller 打包运行的,则关闭热加载以避免多进程冲突和双开浏览器
|
||||
# 智能判断:如果是通过 PyInstaller 打包运行的,或者是 Werkzeug 重载进程,则控制浏览器打开行为
|
||||
is_frozen = getattr(sys, 'frozen', False)
|
||||
# 当 Werkzeug (Flask内置服务器) 使用热加载时,它会启动一个监控主进程和一个运行子进程。
|
||||
# 子进程会有 WERKZEUG_RUN_MAIN 这个环境变量。
|
||||
is_werkzeug_reloader = os.environ.get('WERKZEUG_RUN_MAIN') == 'true'
|
||||
|
||||
# 只有在不需要热加载,或者是热加载的真正工作子进程中,才去寻找端口并打开浏览器
|
||||
if is_frozen or not app.debug or is_werkzeug_reloader:
|
||||
# 动态获取一个可用端口
|
||||
try:
|
||||
port = find_free_port()
|
||||
except Exception as e:
|
||||
print(f"⚠️ 无法自动获取端口,回退到默认端口: {e}")
|
||||
port = 5050
|
||||
|
||||
# 启动前开启一个线程去拉起浏览器
|
||||
threading.Thread(target=open_browser, args=(port,), daemon=True).start()
|
||||
print(f"🚀 前端展示后端服务已启动!请在浏览器访问: http://127.0.0.1:{port}")
|
||||
else:
|
||||
# 如果是热加载的主控进程,随便给个默认端口(反正它不干活),并且不打开浏览器
|
||||
port = 5050
|
||||
|
||||
# 更改为动态端口,避开被占用的端口。修改 host 为 127.0.0.1 避免 Windows 权限拦截
|
||||
app.run(
|
||||
debug=not is_frozen,
|
||||
host='127.0.0.1',
|
||||
port=port,
|
||||
threaded=True,
|
||||
use_reloader=not is_frozen
|
||||
use_reloader=False # 彻底关闭 Flask 内置的热加载,避免双进程互相影响
|
||||
)
|
||||
Reference in New Issue
Block a user