Files
KCGL/inventory-backend/app/__init__.py
yueli 69c38a1bf7 feat(return): 逆向物流数据模型与迁移
新增原单退回与不良品在管的持久化结构。

- TransOutbound 增 returned_quantity(numeric(19,4),非 float):该值参与
  「return_qty <= quantity - returned_quantity」判等,浮点误差会让反复部分
  退回后出现「已退满却判定未退满」的错判
- 新增 TransReturn:退回流水,每次退回写一条而非覆盖式更新。刻意与
  trans_borrow 划清界限——后者部分归还时会覆盖 return_time/operator,
  导致归还历史永久丢失
- 新增 TransDefectiveGoods:不良品在管台账。坏件全程不入库存表,因为
  status 是行级属性而质量是件级属性,把坏件加回原行只能整行打不良
  (实测 stock_buy 单行最大 4789 件、中位 8 件,整行打不良会凭空损失良品)
- 状态机:待处理 → 处理中 → {已回库|已报废|已闭环}。终态由累计去向推导
  而非「最后一次动作」——一批坏件可能既回库过又报废过,按最后动作定状态
  会产生误导
- restocked_qty/scrapped_qty 两列:二期用 quantity-remaining_qty 反推回库量,
  三期加入报废出口后该反推失效
- 审计白名单与模型预加载同步登记(监听器绑定 18 → 20 个模型)

迁移脚本均为纯追加式 DDL,含预检、回滚段与执行后核对。首个脚本用
COALESCE 包裹数量列——库存表允许数量为 NULL,而「NULL 大于 0」求值为
NULL 而非真,裸写会让脏行在预览与诊断两次查询里凭空消失。
2026-09-16 15:45:22 +08:00

386 lines
18 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.8 我的申请单(跨模块聚合,申请人视角)
# 仅需登录态,applicant_id 硬编码为当前用户;
# 与各模块的审批端点(管理视角)严格分离。
# -----------------------------------------------------
try:
from app.api.v1.my_requests import my_requests_bp
app.register_blueprint(my_requests_bp, url_prefix='/api/v1/my-requests')
app.register_blueprint(my_requests_bp, url_prefix='/api/my-requests', name='my_requests_legacy')
print("✅ MyRequests 模块注册成功")
except ImportError as e:
print(f"❌ 错误: MyRequests 模块导入失败: {e}")
# -----------------------------------------------------
# 2.85 扫码草稿(出库/借库作业中途暂停用)
# 按 (user_id, biz_type, request_id) 隔离,仅需登录态。
# 模型在此显式导入,确保审计监听器注册时已进入 db.metadata。
# -----------------------------------------------------
try:
from app.models.scan_draft import ScanDraft # noqa: F401
from app.api.v1.scan_draft import scan_draft_bp
app.register_blueprint(scan_draft_bp, url_prefix='/api/v1/scan-draft')
app.register_blueprint(scan_draft_bp, url_prefix='/api/scan-draft', name='scan_draft_legacy')
print("✅ ScanDraft 模块注册成功")
except ImportError as e:
print(f"❌ 错误: ScanDraft 模块导入失败: {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
# 确保借还模型被加载
# ★ TransReturn / TransDefectiveGoods 为二期逆向物流新增,必须在此
# 预加载:审计监听器按表名从 db.metadata 取模型,未预加载的表在
# create_app() 完成时尚未映射,会漏绑审计(虽然后续惰性补绑能兜底,
# 但预加载更可靠)。
from app.models.transaction import (
TransBorrow, TransRepair, TransScrap,
TransReturn, TransDefectiveGoods,
)
# ★ 审批单模型(原仅在函数体内延迟导入,会导致审计监听器漏绑)
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