#!/usr/bin/env python3 """蝶雅采购智能体平台 - 蝶雅数据可视化平台 精简版 app.py:只保留 Flask 初始化、Blueprint 注册、静态文件路由、配置 """ import os from flask import Flask, request, send_file, render_template, redirect from flask_compress import Compress from config import SECRET_KEY, APP_PORT, APP_HOST from routes import register_blueprints from services.db import query from services.audit import init_audit app = Flask(__name__) Compress(app) app.secret_key = SECRET_KEY app.config['JSON_AS_ASCII'] = False app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0 app.jinja_env.auto_reload = True app.jinja_env.cache.clear() import os, time @app.context_processor def inject_build_version(): mtime = int(os.path.getmtime(os.path.join(app.static_folder, 'js', 'app.js'))) return {'app_js_version': mtime} # 注册所有 Blueprint register_blueprints(app) # 注册集中式用户数据变更审计(before_request + after_request 钩子) init_audit(app) # 调试接口:直接查看下单前包材数据(绕过模板缓存) # Gzip 静态文件支持 @app.route('/static/') def static_with_gzip(filename): filepath = os.path.join(app.static_folder, filename) gz_path = filepath + '.gz' accept_encoding = request.headers.get('Accept-Encoding', '') if 'gzip' in accept_encoding and os.path.exists(gz_path): return send_file( gz_path, mimetype='application/javascript' if filename.endswith('.js') else None, as_attachment=False, download_name=filename, conditional=True ), {'Content-Encoding': 'gzip'} return app.send_static_file(filename) @app.route('/feedback') def removed_feedback(): """测试问题板块已下线,旧地址回到数据概览。""" return redirect('/dashboard') # 上下文处理器:注入当前用户权限到所有模板 @app.context_processor def inject_user_permissions(): from flask import session user_permissions = {} user_id = session.get('user_id') role = session.get('role') if user_id and role != 'super_admin': try: role_perms = query( "SELECT page, operation, permission FROM role_operation_permissions WHERE role=%s", ('viewer' if role == '查看者' else role,) ) from services.permission_model import ensure_position_permissions, user_position_code ensure_position_permissions() position = user_position_code(user_id) position_perms = query( "SELECT page, operation, permission FROM position_operation_permissions WHERE position_code=%s", (position,) ) if position else [] user_perms = query( "SELECT page, operation, permission FROM user_permissions WHERE user_id=%s", (user_id,) ) runtime_page_aliases = { 'purchase_pending': 'purchase', 'purchase_backup': 'purchase_ready', 'order_domestic': 'orders_domestic', 'order_overseas': 'orders_overseas', 'order_packaging': 'orders_packaging', 'order_finished': 'orders_finished' } for p in role_perms: runtime_page = runtime_page_aliases.get(p['page'], p['page']) user_permissions.setdefault(runtime_page, {})[p['operation']] = int(p['permission']) # 岗位权限覆盖角色默认;用户级记录再覆盖岗位权限。 for p in position_perms: runtime_page = runtime_page_aliases.get(p['page'], p['page']) user_permissions.setdefault(runtime_page, {})[p['operation']] = int(p['permission']) for p in user_perms: runtime_page = runtime_page_aliases.get(p['page'], p['page']) user_permissions.setdefault(runtime_page, {})[p['operation']] = int(p['permission']) except Exception: pass # 兼容旧版页面权限表:将旧页面权限转换为统一 view 权限。 legacy_aliases = { 'data': ['data_summary', 'data_detail'], 'inventory': ['inventory_finished', 'inventory_packaging'], 'purchase': ['purchase', 'purchase_ready'], 'orders': ['orders_domestic', 'orders_overseas', 'orders_packaging', 'orders_finished'], 'login-records': ['login_records'] } try: legacy = query("SELECT page, permission FROM role_permissions WHERE role=%s AND permission=1", (role,)) for p in legacy: for page_name in legacy_aliases.get(p['page'], [p['page']]): user_permissions.setdefault(page_name, {}).setdefault('view', 1) except Exception: pass # 注入字段级权限显式值(敏感字段默认隐藏 + 角色/用户字段权限) if user_id and role not in ('super_admin',): try: from services.field_perms import effective_field_overrides for runtime_page, field_map in effective_field_overrides(user_id, role).items(): base = user_permissions.setdefault(runtime_page, {}) for field_key, ops in field_map.items(): base['field:' + field_key + ':view'] = ops['view'] base['field:' + field_key + ':edit'] = ops['edit'] except Exception: pass # 用 JSON 序列化传给前端 import json return {'_user_permissions_json': json.dumps(user_permissions, ensure_ascii=False)} # SPA 路由:处理前端路由刷新时的 404 @app.route('/') def spa_fallback(path): from flask import session, redirect, url_for # 仅处理已知的前端 SPA 路径,避免覆盖 API/静态路由 SPA_PATHS = { 'knowledge', 'planning', 'products', 'bom', 'purchase', 'orders', 'inventory', 'warehouse', 'suppliers', 'users', 'organization', 'data', 'config', 'logs', 'permissions', 'login-records', 'contract-templates', 'personnel', 'collaboration', 'spot', 'ai-analysis', 'product-order', 'stagnant-inventory', 'stagnant-inventory/dashboard', 'stagnant-inventory/detail', 'stagnant-inventory/plans', 'stagnant-inventory/lists', 'spot/dashboard', 'spot/application', 'spot/confirmation', 'spot/sales', 'spot/authorization', 'product-new', 'product-category', 'brand-management', 'product-packaging', 'collaboration/inventory-dashboard', 'collaboration/key-sku-forecast', 'collaboration/sku-health-index', 'collaboration/slow-moving-sku', 'collaboration/sales-management' } first = path.split('/')[0] if first not in SPA_PATHS: return redirect(url_for('auth.login')) if 'user_id' not in session: return redirect(url_for('auth.login')) if session.get('must_change_password'): return redirect(url_for('auth.login')) # 用完整路径映射 page key(/collaboration/inventory-dashboard → collaboration_inventory_dashboard), # 不能用第一段,否则刷新子页面会回退到父菜单占位页 return render_template('base.html', page=path.replace('/', '_').replace('-', '_')) @app.after_request def add_header(response): # 静态文件:长期缓存(7天),配合版本号更新 if request.path.startswith('/static/'): response.headers['Cache-Control'] = 'no-cache, must-revalidate' else: response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0' response.headers['Pragma'] = 'no-cache' response.headers['Expires'] = '0' return response # ─── 启动 ─── if __name__ == '__main__': # 启动时自动清洗脏数据 try: import auto_clean auto_clean.clean() except: pass print(f"🚀 蝶雅采购智能体平台 启动中...") print(f" http://{APP_HOST}:{APP_PORT}") print(f" 默认账号: admin / admin123") app.run(host=APP_HOST, port=APP_PORT, debug=True)