Files
KCGL/全局系统体检报告.md

8.4 KiB
Raw Blame History

IRIS 库存管理系统 - 全局系统体检报告

审查日期2026-04-02
审查范围inventory-web (Vue3 + Element Plus) + inventory-backend (Flask + SQLAlchemy)
审查模式:静态代码分析


一、前端状态与渲染漏洞 (Vue3 + Element Plus)

[🚨 高危漏洞]

1. el-table 缺少 reserve-selection 导致分页勾选丢失

  • 模块/文件: inventory-web/src/views/materiel/list.vue (及其他多个页面)
  • 问题描述: 大多数表格使用了 type="selection" 但未设置 :reserve-selection="true"
  • 代码行数: 该问题影响 30+ 个 el-table 组件
  • 根因分析:
    • 分页后切换页面时,之前选中的行会丢失
    • 只有 Selection.vue 的手动选择弹窗表格(第118行)添加了 :reserve-selection="true"
  • 验证方法: 进入物料列表勾选第1页的某几项切换到第2页再返回第1页确认勾选状态

2. el-table 缺少 row-key 导致全选/渲染异常

  • 模块/文件: inventory-web/src/views/stock/stocktake/index.vue
  • 代码行数: 第240行
  • 根因分析: 该表格使用的 row-key="id"但如果存在跨表数据如物料、半成品、成品混排id 可能会冲突

3. 盘点列表一次性加载 10000 条数据

  • 模块/文件: inventory-web/src/views/stock/stocktake/index.vue
  • 代码行数: 第662行、第985行
  • 问题代码:
params: { page: 1, limit: 10000 }  // 获取足够多的数据
limit: 10000,  // 获取全部已盘点记录
  • 风险: 大量盘点记录时会导致前端内存溢出、页面卡死

[⚠️ 交互/逻辑隐患]

4. el-dialog 缺少 destroy-on-close 导致表单残留

  • 模块/文件: inventory-web/src/views/outbound/create.vue
  • 代码行数: 第174行
  • 问题代码: <el-dialog v-model="showDialog" title="新建出库单" width="75%" :close-on-click-modal="false">
  • 根因分析:
    • 弹窗关闭后,数据未重置
    • 再次打开弹窗会看到上一次填写的数据
  • 建议: 添加 destroy-on-close 属性或手动在关闭回调中重置表单

5. formRef.resetFields() 调用不足

  • 模块/文件: 多个视图文件
  • 根因分析: 大多数弹窗表单没有调用 resetFields() 进行彻底重置
  • 对比:
    • material/list.vue: 第1228行 有 resetFields
    • bom/BomManage.vue: 第656行 有 resetFields
    • system/UserCreate.vue: 第374行 有 resetFields
    • 其他大多数 无重置逻辑

6. 响应式解构潜在风险

  • 模块/文件: inventory-web/src/App.vue
  • 代码行数: 第80行
  • 问题代码: const { new_password, confirm_password } = passwordForm.value
  • 说明: 这种写法会失去响应性绑定,属于潜在风险(当前代码未直接修改解构后的变量,风险较低)

二、后端 ORM 与生命周期陷阱 (Flask + SQLAlchemy)

[🚨 高危漏洞]

7. @audit_log 装饰器中的 DetachedInstanceError 风险

  • 模块/文件: inventory-backend/app/utils/decorators.py
  • 代码行数: 第211-260行、第318行
  • 问题代码:
# 第211-217行查询用户
user = SysUser.query.get(user_id)
if user:
    user_info = user.to_dict()  # 可能返回 DetachedInstanceError
    display_name = user_info.get('display_name', username)

# 第248行添加审计日志
log_entry = AuditLog(...)
db.session.add(log_entry)
# 第318行在请求结束后 commit
db.session.commit()
  • 根因分析:
    • 装饰器在 db.session.commit() 后访问已游离的对象属性
    • 特别是在请求返回后session 可能已关闭,再次访问 user 对象会触发 DetachedInstanceError

8. N+1 查询问题 - 库位树/库存列表

  • 模块/文件: inventory-backend/app/api/v1/warehouse.py, inventory-backend/app/api/v1/inbound/stock.py
  • 代码行数:
    • warehouse.py 第118-132行循环查询每条库存记录
    • stock.py 第1113-1138行全量查询后循环处理
  • 问题代码:
# stock.py 第1113行
for item in StockBuy.query.filter(StockBuy.stock_quantity > 0).all():
    # 每次循环访问 item.base 时会触发新的 SQL 查询 (Lazy Load)
    'material_name': item.base.name
  • 根因分析: 没有使用 joinedload 或 eager loading 预加载关联关系

9. 批量操作无最大数量限制

  • 模块/文件: inventory-backend/app/api/v1/inbound/buy.py
  • 根因分析:
    • 批量创建物料/成品/半成品时没有限制最大条目数
    • 前端子啊 stocktake/index.vue 设置了 limit=10000
    • 后端如果接收大量数据会导致内存溢出

[⚠️ 交互/逻辑隐患]

10. 出库扣减逻辑中的可用库存竞态

  • 模块/文件: inventory-backend/app/services/outbound_service.py
  • 代码行数: 第169-173行
  • 问题代码:
stock_record = ModelClass.query.with_for_update().get(stock_id)  # 使用了悲观锁 ✅
if float(stock_record.available_quantity) < quantity:
    raise ValueError(...)
stock_record.available_quantity = float(stock_record.available_quantity) - quantity
  • 分析: 已使用 with_for_update() 悲观锁,但需要确认数据库连接是否支持行级锁
  • 建议: 建议增加版本号字段实现乐观锁,作为双重保险

11. 文件上传无大小限制

  • 模块/文件: inventory-backend/app/api/v1/common/upload.py
  • 根因分析:
    • 没有检查文件大小
    • 没有限制同时上传的文件数量
  • 建议: 添加 MAX_FILE_SIZE 和并发限制

三、业务并发与数据一致性

[🚨 高危漏洞]

12. 库存扣减无乐观锁

  • 模块/文件: 所有库存相关表 (StockBuy, StockSemi, StockProduct)
  • 根因分析:
    • 库存表中没有 version 字段
    • 仅依赖数据库行锁with_for_update()
    • 高并发场景下可能出现库存扣为负数
  • 影响范围: 出库、报废、借用、盘点调整等所有减少库存的操作

13. 盘点实盘数更新竞态

  • 模块/文件: inventory-backend/app/api/v1/stock/adjustment.py
  • 代码行数: 第226-250行
  • 问题代码:
for stock in pagination.items:
    new_qty = float(stock.stock_quantity)
    # 读取和写入之间存在时间窗口,可能被其他请求修改
    stock.stock_quantity = new_qty
    db.session.add(stock)
db.session.commit()
  • 根因分析:
    • 循环中的每条记录没有悲观锁
    • 并发盘点可能导致数据覆盖

[⚠️ 交互/逻辑隐患]

14. 唯一键判断不严谨 - 购物车追加

  • 模块/文件: inventory-web/src/views/outbound/Selection.vue
  • 根因分析:
    • 前端购物车基于 type_id 判断是否重复
    • 如果 type 相同但 id 不同,仍会误判
  • 当前状态: 代码已修复,使用 ${item.type}_${item.id} 格式

15. BOM 批量导入无校验

  • 模块/文件: inventory-backend/app/api/v1/bom.py
  • 代码行数: 第278-340行
  • 根因分析:
    • children 数据直接写入,不检查是否有重复子件
    • 不验证子件是否存在

四、报告总结

漏洞统计

严重程度 数量
🚨 高危漏洞 7
⚠️ 交互/逻辑隐患 8
状态良好 15+

优先修复建议(按优先级排序)

  1. [P0] 前端:为所有分页表格添加 :reserve-selection="true" 和唯一 row-key
  2. [P0] 前端:修复 stocktake <20><> 10000 条限制,改用分页或虚拟滚动
  3. [P0] 后端:修复 @audit_log 的 DetachedInstanceError分两次 commit 或刷新对象)
  4. [P1] 后端:为库存表添加 version 字段实现乐观锁
  5. [P1] 后端:为盘点实盘更新添加 with_for_update()
  6. [P1] 后端:添加批量操作最大限制(如 500 条/请求)
  7. [P2] 前端:为所有表单弹窗添加 destroy-on-close 或手动重置
  8. [P2] 后端:优化 N+1 查询,添加 joinedload

状态良好的核心链路

  • 出库单创建使用了悲观锁 (with_for_update)
  • 物料基础信息管理使用 visibilityLevel 控制
  • 登录 Token 验证与 Redis 单设备登录互踢
  • 文件上传使用 UUID 生成唯一文件名
  • 出库选单唯一键已修复为 ${type}_${id} 格式
  • 库位路径 natural sorting 已实现

本报告由 Qwen Code 全局静态扫描生成,仅供参考。实际修复请结合业务场景进行测试验证。