# 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行 - **问题代码**: ```javascript params: { page: 1, limit: 10000 } // 获取足够多的数据 limit: 10000, // 获取全部已盘点记录 ``` - **风险**: 大量盘点记录时会导致前端内存溢出、页面卡死 --- ### [⚠️ 交互/逻辑隐患] #### 4. el-dialog 缺少 destroy-on-close 导致表单残留 - **模块/文件**: `inventory-web/src/views/outbound/create.vue` - **代码行数**: 第174行 - **问题代码**: `` - **根因分析**: - 弹窗关闭后,数据未重置 - 再次打开弹窗会看到上一次填写的数据 - **建议**: 添加 `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行 - **问题代码**: ```python # 第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行:全量查询后循环处理 - **问题代码**: ```python # 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行 - **问题代码**: ```python 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行 - **问题代码**: ```python 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 �� 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 全局静态扫描生成,仅供参考。实际修复请结合业务场景进行测试验证。*