Files
KCGL/inventory-backend/app/__init__.py
yueli 4808a48594 refactor(audit): 审计架构清理——复活白名单监听器、停用噪声监听器、清除僵尸装饰器
一、统一为单一监听器实现
  原先两套 SQLAlchemy 事件监听器并存:
    · app/utils/audit_events.py   —— 全局监听 db.Model、无白名单、无请求上下文守卫(实际在跑)
    · app/core/audit_listener.py  —— 白名单制、有守卫、有模型级开关(从未生效)
  后者失效的根因:注册代码写在 extensions.py 的 init_extensions() 内,
  而该函数全仓库只有定义、没有任何调用(create_app 直接内联调用 db.init_app 等)。

  现统一由 app/core/audit_listener.py 承担,并在 create_app() 中显式注册。
  extensions.py 的死函数 init_extensions 整体删除,避免后人误以为它是有效入口。

二、修复监听器三处致命缺陷(此前注册了也写不进数据)
  1. 事件回调第二个参数是 Connection,原代码却调用 Connection.add()(不存在),
     每次写日志都抛 AttributeError 并被 except 吞掉 → 改为 connection.execute()
  2. register_audit_listeners 从 app.models 批量 import 多个未导出的模型,
     ImportError 被上层 try/except 吞掉 → 改为按表名从 db.metadata 取模型
  3. 本项目有 31 处函数体内延迟导入模型(如 scrap.py 内部才 import ScrapApproval),
     一次性注册会静默漏表 → 增加 ensure_audit_listeners() 惰性补绑,
     并在模型预加载段补全审批单/BOM/采购等模型

三、强约束
  · WHITELIST_TABLES:仅 18 张核心业务表,系统表/草稿表/向量表不再自审
  · has_request_context() 守卫:系统初始化与后台定时任务不再产生 username=system 噪声
  · IGNORE_FIELDS 增加 password/password_hash/salt/token/secret/api_key(安全红线)
  · created_at 显式写 beijing_time(),与全系统时间口径一致

四、清除僵尸装饰器
  @audit_log 早已退化为直接透传的空壳(module/action 参数全被忽略,
  数据库中零星的中文 action 即其历史遗留产物),却仍挂在 38 处路由上。
  连同 13 个文件的 import 一并移除;audit_events.register_audit_events 改为空操作。

验证:应用上下文中的写操作不产生日志;HTTP 请求产生 5 条日志,
对象为业务单号(APR-SCRAP-... / SKU),模块中文,操作人真实,时间为北京时间。
2026-09-10 14:16:27 +08:00

352 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 文件路径: inventory-backend/app/__init__.py
from flask import Flask, jsonify
from config import Config
from app.extensions import db, migrate, cors, jwt
from app.api.v1.scan import scan_bp
import os
def create_app():
app = Flask(__name__)
app.config.from_object(Config)
# =========================================================
# 1. 初始化插件
# =========================================================
db.init_app(app)
migrate.init_app(app, db)
jwt.init_app(app) # 初始化 JWT
# 允许所有 /api/ 开头的请求跨域,支持 credentials
cors.init_app(app, resources={r"/*": {"origins": "*"}}, supports_credentials=True)
# =========================================================
# 1.1 [已迁移] 审计日志监听器注册
#
# 原注册的是 app/utils/audit_events.py 的全局监听器(监听 db.Model、
# 无白名单、无请求上下文守卫),已停用;审计统一由
# app/core/audit_listener.py 接管。
#
# ★ 注册动作已移至本函数末尾见「5. 注册审计监听器」),
# 因为此处模型尚未全部映射,会导致 scrap_approval 等表被静默漏掉。
# =========================================================
# 2. 注册蓝图 (Blueprints)
# ---------------------------------------------------------
# 注意:为了解决前端请求不带 /v1 导致的 404 错误,
# 下面的模块都采用了 "双重注册" 策略:
# 1. 标准地址: /api/v1/...
# 2. 兼容地址: /api/... (name 参数必须不同)
# =========================================================
# -----------------------------------------------------
# 2.0 注册权限与认证模块 (Auth)
# -----------------------------------------------------
try:
from app.api.v1.auth import auth_bp
# 标准
app.register_blueprint(auth_bp, url_prefix='/api/v1/auth')
# 兼容 (防止前端忘记写 v1)
app.register_blueprint(auth_bp, url_prefix='/api/auth', name='auth_legacy')
print("✅ Auth 模块注册成功")
except ImportError as e:
print(f"❌ 错误: Auth 模块导入失败: {e}")
# -----------------------------------------------------
# 2.1 注册入库聚合模块 (Inbound)
# -----------------------------------------------------
try:
from app.api.v1.inbound import inbound_bp
# 标准: /api/v1/inbound/base/list
app.register_blueprint(inbound_bp, url_prefix='/api/v1/inbound')
# 兼容: /api/inbound/base/list (修复前端 404)
app.register_blueprint(inbound_bp, url_prefix='/api/inbound', name='inbound_legacy')
print("✅ Inbound 模块注册成功 (已启用兼容模式: /api/inbound)")
except ImportError as e:
print(f"❌ 错误: Inbound 模块导入失败: {e}")
# -----------------------------------------------------
# 2.2 注册通用打印模块 (Common Print)
# -----------------------------------------------------
try:
from app.api.v1.common.print import print_bp
app.register_blueprint(print_bp, url_prefix='/api/v1/common/print')
app.register_blueprint(print_bp, url_prefix='/api/common/print', name='print_legacy')
print("✅ Print 模块注册成功")
except ImportError as e:
print(f"❌ 错误: Print 模块导入失败: {e}")
# -----------------------------------------------------
# 2.3 注册通用上传模块 (Common Upload)
# -----------------------------------------------------
try:
from app.api.v1.common.upload import upload_bp
app.register_blueprint(upload_bp, url_prefix='/api/v1/common')
app.register_blueprint(upload_bp, url_prefix='/api/common', name='upload_legacy')
print("✅ Upload 模块注册成功")
except ImportError as e:
print(f"❌ 错误: Upload 模块导入失败: {e}")
# -----------------------------------------------------
# 2.4 注册以图搜图模块 (Image Search)
# -----------------------------------------------------
try:
from app.api.v1.common.image_search import image_search_bp
app.register_blueprint(image_search_bp, url_prefix='/api/v1/common')
app.register_blueprint(image_search_bp, url_prefix='/api/common', name='image_search_legacy')
print("✅ Image Search 模块注册成功")
except ImportError as e:
print(f"❌ 错误: Image Search 模块导入失败: {e}")
# -----------------------------------------------------
# 2.4 注册业务操作模块 (Transactions - 借还/维修/报废)
# -----------------------------------------------------
try:
from app.api.v1.transactions import trans_bp
# 标准: /api/v1/transactions/borrow
app.register_blueprint(trans_bp, url_prefix='/api/v1/transactions')
# 兼容: /api/transactions/borrow
app.register_blueprint(trans_bp, url_prefix='/api/transactions', name='trans_legacy')
print("✅ Transactions 模块注册成功")
except ImportError as e:
print(f"⚠️ 提示: Transaction 模块导入失败: {e}")
# -----------------------------------------------------
# 2.5 注册出库模块 (Outbound)
# -----------------------------------------------------
try:
from app.api.v1.outbound import outbound_bp
# 标准: /api/v1/outbound
app.register_blueprint(outbound_bp, url_prefix='/api/v1/outbound')
# 兼容: /api/outbound
app.register_blueprint(outbound_bp, url_prefix='/api/outbound', name='outbound_legacy')
print("✅ Outbound 模块注册成功")
except ImportError as e:
print(f"❌ 错误: Outbound 模块导入失败: {e}")
# -----------------------------------------------------
# 2.6 注册报废模块
# -----------------------------------------------------
try:
from app.api.v1.scrap import scrap_bp
app.register_blueprint(scrap_bp, url_prefix='/api/v1/scrap')
print("✅ Scrap 模块注册成功")
except ImportError as e:
print(f"❌ 错误: Scrap 模块导入失败: {e}")
# -----------------------------------------------------
# 2.8 注册采购管理模块
# -----------------------------------------------------
try:
from app.api.v1.purchase import purchase_bp
app.register_blueprint(purchase_bp, url_prefix='/api/v1/purchase')
app.register_blueprint(purchase_bp, url_prefix='/api/purchase', name='purchase_legacy')
print("✅ Purchase 模块注册成功")
except ImportError as e:
print(f"❌ 错误: Purchase 模块导入失败: {e}")
# -----------------------------------------------------
# 2.7 注册 BOM 模块
# -----------------------------------------------------
try:
from app.api.v1.bom import bom_bp
# 标准: /api/v1/bom
app.register_blueprint(bom_bp, url_prefix='/api/v1/bom')
# 兼容: /api/bom
app.register_blueprint(bom_bp, url_prefix='/api/bom', name='bom_legacy')
print("✅ BOM 模块注册成功")
except ImportError as e:
print(f"❌ 错误: BOM 模块导入失败: {e}")
# -----------------------------------------------------
# 2.7 注册权限管理模块 (Permission) - [新增]
# -----------------------------------------------------
try:
from app.api.v1.permission import permission_bp
# 标准: /api/v1/permissions/tree
app.register_blueprint(permission_bp, url_prefix='/api/v1/permissions')
# 兼容: /api/permissions/tree
app.register_blueprint(permission_bp, url_prefix='/api/permissions', name='permission_legacy')
print("✅ Permission 模块注册成功")
except ImportError as e:
print(f"❌ 错误: Permission 模块导入失败 (请检查 app/api/v1/permission.py 是否存在): {e}")
# 2.8 注册审计日志模块 (Audit)
# -----------------------------------------------------
try:
from app.api.v1.audit import audit_bp
# 标准: /api/v1/audit/logs
app.register_blueprint(audit_bp, url_prefix='/api/v1/audit')
# 兼容: /api/audit/logs
app.register_blueprint(audit_bp, url_prefix='/api/audit', name='audit_legacy')
print("✅ Audit 模块注册成功")
except ImportError as e:
print(f"❌ 错误: Audit 模块导入失败: {e}")
# 2.9 初始化审计日志菜单和权限(防重复)
# -----------------------------------------------------
try:
from app.services.permission_service import PermissionService
with app.app_context():
# 先清理旧版菜单,再初始化新版
PermissionService.cleanup_legacy_stocktake_menus()
PermissionService.init_audit_menu()
PermissionService.init_stocktake_menus()
# 初始化所有菜单的层级结构
PermissionService.init_all_menus()
# ★ 启动时自动为所有已知角色补充默认权限(仅补充空角色,不覆盖已有)
PermissionService.ensure_default_permissions()
except Exception as e:
print(f"⚠️ 菜单初始化跳过: {e}")
# -----------------------------------------------------
# 2.10 注册盘盈盘亏管理模块 (Stock Adjustment)
# -----------------------------------------------------
try:
from app.api.v1.stock.adjustment import adjustment_bp
app.register_blueprint(adjustment_bp, url_prefix='/api/v1/stock/adjustment')
print("✅ Stock Adjustment 模块注册成功")
except ImportError as e:
print(f"❌ 错误: Stock Adjustment 模块导入失败: {e}")
# -----------------------------------------------------
# 2.11 注册库位管理模块 (Warehouse)
# -----------------------------------------------------
try:
from app.api.v1.warehouse import warehouse_bp
# 标准: /api/v1/warehouse/tree
app.register_blueprint(warehouse_bp, url_prefix='/api/v1/warehouse')
# 兼容: /api/warehouse/tree
app.register_blueprint(warehouse_bp, url_prefix='/api/warehouse', name='warehouse_legacy')
print("✅ Warehouse 模块注册成功")
except ImportError as e:
print(f"❌ 错误: Warehouse 模块导入失败: {e}")
# -----------------------------------------------------
# 2.12 注册通用聚合搜索模块 (Common - Global Search)
# -----------------------------------------------------
try:
from app.api.v1.common import common_bp
# 标准: /api/v1/common/global-search
app.register_blueprint(common_bp, url_prefix='/api/v1/common')
# 兼容: /api/common/global-search
app.register_blueprint(common_bp, url_prefix='/api/common', name='common_legacy')
print("✅ Common 模块注册成功")
except ImportError as e:
print(f"❌ 错误: Common 模块导入失败: {e}")
# -----------------------------------------------------
# 2.13 注册扫码查库存模块 (Scan)
# -----------------------------------------------------
try:
app.register_blueprint(scan_bp, url_prefix='/api/v1/scan')
print("✅ Scan 模块注册成功")
except Exception as e:
print(f"❌ 错误: Scan 模块注册失败: {e}")
# -----------------------------------------------------
# 2.x 注册异步导出模块 (Export)
# -----------------------------------------------------
try:
from app.api.v1.export import export_bp
app.register_blueprint(export_bp, url_prefix='/api/v1/export')
app.register_blueprint(export_bp, url_prefix='/api/export', name='export_legacy')
print("✅ Export 模块注册成功")
except Exception as e:
print(f"❌ 错误: Export 模块注册失败: {e}")
# -----------------------------------------------------
# 2.14 注册批量导入模块 (Import)
# -----------------------------------------------------
try:
from app.api.v1.import_api import import_bp
app.register_blueprint(import_bp, url_prefix='/api/v1/import')
print("✅ Import 模块注册成功")
except ImportError as e:
print(f"❌ 错误: Import 模块导入失败: {e}")
# =========================================================
# 3. 预加载数据模型
# =========================================================
with app.app_context():
try:
# 基础与库存模型
from app.models.base import MaterialBase
from app.models.inbound.buy import StockBuy
from app.models.inbound.semi import StockSemi
from app.models.inbound.product import StockProduct
# 出库模型
from app.models.outbound import TransOutbound
# 系统与业务模型 (SysRolePermission 等在 models.system 中)
from app.models.system import SysUser, SysLog, SysMenu, SysElement, SysRolePermission, SysWarehouseLocation
# 确保借还模型被加载
from app.models.transaction import TransBorrow, TransRepair, TransScrap
# ★ 审批单模型(原仅在函数体内延迟导入,会导致审计监听器漏绑)
from app.models.outbound import OutboundApproval
from app.models.borrow import BorrowApproval
from app.models.scrap_approval import ScrapApproval
# ★ 其余延迟导入的业务模型,一并预加载以便审计覆盖
from app.models.bom import BomTable
from app.models.inbound.service import StockService
from app.models.stock.adjustment import StockAdjustment
from app.models.purchase import PurchaseRequest
# 首次运行时可取消注释自动建表 (但在生产环境建议使用 flask db upgrade)
# db.create_all()
except ImportError as e:
print(f"⚠️ 模型预加载部分失败 (检查是否缺少文件): {e}")
except Exception as e:
print(f"⚠️ 模型预加载发生未知错误: {e}")
# =========================================================
# 5. 注册审计监听器(必须放在最后,等所有模型完成映射)
#
# ★ 为什么不能放在 create_app 开头(紧邻 db.init_app
# 本函数早期只有 17/18 张白名单表完成映射 —— scrap_approval 等模型是在
# 蓝图注册阶段才被导入的,注册过早会导致这些表被静默漏掉
# register_audit_listeners 遍历 db.metadata.tables未映射即不可见
# 放在模型预加载之后可确保白名单全部就位。
# =========================================================
with app.app_context():
try:
from app.core.audit_listener import register_audit_listeners
count = register_audit_listeners(db)
print(f"✅ 审计监听器注册成功,共绑定 {count} 个模型")
except Exception as e:
print(f"⚠️ 审计监听器注册失败: {e}")
# =========================================================
# 6. 全局异常处理(面向生产:不泄露堆栈)
# =========================================================
import logging
import traceback
@app.errorhandler(ValueError)
def handle_value_error(e):
"""业务校验异常 → 400 + 自定义消息"""
return jsonify({'code': 400, 'msg': str(e)}), 400
@app.errorhandler(404)
def handle_not_found(e):
return jsonify({'code': 404, 'msg': '资源不存在'}), 404
@app.errorhandler(405)
def handle_method_not_allowed(e):
return jsonify({'code': 405, 'msg': '请求方法不允许'}), 405
@app.errorhandler(Exception)
def handle_unhandled_exception(e):
"""兜底:任何未捕获异常 → 500 + 通用消息,堆栈仅记录日志"""
app.logger.error(f"Unhandled exception: {type(e).__name__}: {e}\n{traceback.format_exc()}")
return jsonify({
'code': 500,
'msg': '系统繁忙,请稍后再试',
'error': 'Internal Server Error'
}), 500
return app