Commit Graph

36 Commits

Author SHA1 Message Date
077fd2f2cf feat(borrow,scrap): 补齐申请人撤回端点,与出库对齐
背景
----
出库已有独立的申请人撤回端点(仅 @jwt_required + 服务层归属断言),
但借库/报废没有:

  · 借库撤回复用 close 端点,权限是 op_borrow_approval(管理路径),
    普通员工调用返回 403;
  · 报废撤回带 @permission_required('scrap_apply'),同样挡住普通申请人
    (scrap_apply 只授予 INBOUND/OUTBOUND/SUPERVISOR/SUPER_ADMIN)。

结果是「我的申请单」页面里,借库/报废的撤回按钮对普通员工点了报错。

借库
----
服务层拆成与管理路径并列的两条入口(与出库同构):

  mark_completed     —— 管理路径,需 op_borrow_approval,仅 status==1
  withdraw_request   —— 申请人路径,仅校验单据归属,status 0 或 1

两者共用 _release_and_close(),释放逻辑只有一份实现。
新增 POST /transactions/borrow/request/<id>/withdraw(仅 @jwt_required)。

报废
----
withdraw() 增加 require_owner 参数区分两条路径:
  require_owner=True (默认,申请人路径)→ 断言 applicant_id == operator_id
  require_owner=False(管理路径)        → 由调用方权限装饰器鉴权
WITHDRAWABLE_STATUS 从 (1,) 放宽到 (0, 1),与出库/借库对齐。
移除端点上的 @permission_required('scrap_apply'),改走归属校验。

安全实测
--------
  借库 B 撤 A 的单 → 403,库存仍是 27(未被释放)
  借库 A 撤自己的单 → 200,30 完全释放
  报废 B 撤 A 的单 → 403
  报废 A 撤自己的单 → 200
  主管代撤员工的单(特权路径)→ 200
2026-09-10 15:46:55 +08:00
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
fd0bfd3d9c feat(records): 借还记录接入高级筛选
复用 app/utils/advanced_filter.py 的解析与谓词逻辑:
  · 父级字段(单号 borrow_no、借用人 borrower_name)走标准 SQL 谓词
  · 子级字段(SKU、物料名称)经单号子查询过滤,否定操作符走 NOT IN 整单排除
  · 物料名经三表联查(buy/semi/product JOIN material_base)

借还的单号维度查询基于 order_subq 子查询分页,故此处把过滤条件施加在
order_subq.c.borrow_no 上,与既有的状态/日期/公司隔离过滤保持同一层次。

前端 records.vue 新增「高级筛选」el-popover(字段/操作符/值 + 添加条件/
应用筛选/重置),序列化为 advancedFilters JSON 字符串随查询下发。

验证:sku ne 0000000002 → 52 单(库中无单含该 SKU,正确不减);
      sku contains 0000 → 52 单(52 单的 SKU 全部含 0000,数据巧合)。
2026-09-10 13:06:03 +08:00
c35ec9a659 feat(records): 出库/借还记录统一高级搜索版式
三个记录页(出库/借还/报废)此前搜索区形态各异:出库用裸 div + 分散样式,
借还缺日期范围,报废只有 SKU 输入框。现统一为 el-form :inline="true" +
.filter-form 版式,并补齐缺失的过滤维度。

借还记录(新增日期范围能力):
  · 后端 get_records 增加 start_date/end_date 参数,按「借出时间」过滤;
  · 边界补全时分秒(YYYY-MM-DD → 当日 00:00:00 / 23:59:59),
    与出库记录同口径,解决零点截断导致当天记录漏查的问题;
  · API 层透传两个新参数。

出库记录:
  · 版式统一为 inline 表单,日期选择器加 label;
  · 新增「重置」按钮;
  · 搜索类型切换由 500ms 防抖改为立即查询。

借还记录:
  · 新增「重置」按钮;搜索类型/状态切换改为立即查询(取消防抖)。

实测(SUPER_ADMIN):
  报废 全部/单号/SKU/操作人/物料名/日期 六类过滤均 200 且命中正确;
  借还 无过滤 52 单 / 本月 18 单 / 空区间 0 单(日期过滤生效);
  出库 无过滤 395 单 / SKU 277 / 姓名 7 / 物料名 16 / 单号 OUT-2026 命中 395。
2026-09-10 12:06:08 +08:00
1dbf74b7bc feat(borrow): 借库审批补「完结」能力(对齐出库 status=4)
- mark_completed 改为 1→4 已完结(真正扫码借出完成仍为 status=3)
- 前端审批页新增「已完结」筛选项与状态映射;审批信息列对 3/4 显示审批人
2026-09-10 09:47:07 +08:00
044e6dbd98 feat(borrow,outbound): 库管(WAREHOUSE_MGR)代建申请强制审批
- submit/create 新增 force_approval:库管建单无视物料是否需审批,一律走审批
- 路由按当前角色为 WAREHOUSE_MGR 传 true;未选审批人返回明确业务错误
2026-09-09 16:19:38 +08:00
eff9b62224 fix(borrow,outbound): 普通用户记录只看本人——按领用人/借用人姓名(不含账号前缀)匹配
- 出库记录/借还记录:非管理者视角时按 领用人(consumer_name)/借用人(borrower_name) 过滤
- 匹配取登录名 username.split(/)[0] 的姓名;兼容库里存“姓名/xiaolongxia”全名(姓名+/前缀)
- 修复:管理员替员工创建、领用人=员工 的单,员工登录可见
2026-09-09 11:19:15 +08:00
6bdc8a82f2 feat(borrow,outbound): 申请预检按需显示审批人——默认不显示,命中需审批才出现并必选
- 后端新增 /borrow/request/check-approval 与 /outbound/request/check-approval 预检接口
- 申请弹窗默认隐藏审批人;提交前预检:命中需审批→显示审批人并红字列物料、未选拦截;未命中→隐藏并直接提交(approver=null)
- 申请 items 补传 base_id 供后端 ID 精确判定
2026-09-09 10:47:29 +08:00
9622baf76e fix(borrow,outbound): 记录查看按角色收窄——普通只看自己,库管/主管/超管/跨域看全部
- decorators 新增 is_privileged_viewer(SUPER_ADMIN/SUPERVISOR/WAREHOUSE_MGR 或 crossDomain)
- 借库记录列表:非管理者强制 applicant_id=当前用户
- 出库记录列表+详情:非管理者强制只看自己/无权访问他人单(403)
2026-09-09 09:31:01 +08:00
02f36b383c fix: 借库归还/报废/借库执行操作人由 JWT 数字 ID 改为用户名(归还人不再显示数字) 2026-09-04 16:10:00 +08:00
3bd19c1ab5 feat: 借库审批支持手动完结——已通过(status=1)单可强制完结,库管/主管/超管可见 2026-09-04 14:52:31 +08:00
36e9f500aa feat(borrow): 借库未归还直接报废功能(关联报废单流程)
后端:
- trans_service 新增 scrap_borrow 方法:未归还借用标记为 scrapped,
  扣减总库存 stock_quantity(可用已在借出时冻结),生成 TransScrap 报废记录
- transactions.py 新增 POST /borrow/scrap 接口(复用 op_return:operation 权限,库管/主管可操作)
- scrap.py query_records 支持 trans_borrow 来源的物料名解析 + 行级隔离

前端:
- records.vue 未归还记录新增「报废」按钮(库管/主管可见)
- 报废弹窗:勾选明细 + 填报废原因 + 二次确认
- 状态显示新增「已报废」标签;整单所有明细报废时标记为 scrapped
2026-08-31 10:21:28 +08:00
63c36d1466 fix: 借库记录签名改回 null(始终可见),暂等 sys_element 补齐权限码 2026-07-21 09:59:14 +08:00
ca21294580 fix: transactions filter_item_by_permissions 只保留签名过滤,基础字段不再剥离 2026-07-21 09:56:34 +08:00
3c0954598c perf: 综合安全加固 — RBAC严格映射+异步邮件+字段权限白名单+前端对齐+导入模板
本次提交包含本会话所有修改的最终统一提交

## 权限系统重构
- permission_service.py: 添加入库/采购操作元素 + ensure_default_permissions
- field_permissions.py: 严格1-to-1 Default Deny 字段映射(StockBuy/Semi/Product/MaterialBase)
- decorators.py: _expand_operation_perms 双向粒度桥接 + prevent_double_submit
- deploy_production.sql: 修复 sys_element 别名码(qty_inbound→in_quantity)

## 采购模块
- purchase.py: 权限驱动可见性 + inbound_purchase独立权限 + 价格字段过滤
- purchase_service.py: 异步邮件 + 三阶段批量模糊匹配防N+1
- purchase/index.vue: canApprove严格操作权限 + upload重复修复

## 导出/入
- base_service.py: export_excel 流式写入防OOM + get_latest_specs 优化
- import_service.py + import_api.py: Excel批量导入(模板+预览+执行)
- ImportDialog.vue: 三步骤导入弹窗

## 异步邮件
- email_service.py: send_email_async (守护线程)
- inventory_task.py: send_email→send_email_async

## 前端对齐
- product/semi/buy.vue: 列对齐in_quantity/stock_quantity/available_quantity + localStorage缓存V2
- buyOdoo.vue: 排序修复 + 导入按钮 + 移除点击展开加载
- BomManage.vue: 懒加载分组 + 导入按钮
- list.vue: 导入按钮
- Selection.vue + borrow/apply: BOM匹配修复 + 导入按钮
- outbound/create.vue: 出库类型必选
- AppMain.vue: 移除transition白屏修复
- material_base.ts, outbound.ts, bom.ts, stock.ts: 新增API函数
2026-07-17 13:07:12 +08:00
347f20497c feat: Redis幂等锁 + 关键端点防重复提交
## decorators.py
- 新增 @prevent_double_submit(lock_timeout=5) 装饰器
  Redis key: idem:{user_id}:{path}:{MD5(body)}
  锁存在→409, 不存在→setex→执行业务→delete
  Redis不可用时fail-open降级放行

## purchase.py
- POST /purchase (创建): +@prevent_double_submit
- PATCH /purchase/<id>/approve (审批): +@prevent_double_submit

## outbound.py
- POST /outbound (出库): +@prevent_double_submit

## transactions.py
- POST /borrow/dispatch (借库扣减): +@prevent_double_submit
2026-07-16 13:08:37 +08:00
c07f25b646 fix: Fail-Closed 字段级安全加固 — 堵住15+端点价格泄露
## stock.py
- _make_price_stripper(): 工厂函数,选单前缀自动剥离所有价格/成本字段
- _do_get_stock_list(permission_prefix): 每个item.to_dict()后剥离价格
- /all 端点: 非AI模式自动剥离价格字段
- /list 端点: permission_prefix='outbound_selection' 传递

## outbound.py
- /bom-match-stock: 返回前按stock_type剥离全部价格/成本字段
- /scan: result.pop('price', None)

## scrap.py
- /scan: result.pop('price', None)
- /records: 每条记录剥离 cost_at_scrap, total_loss

## transactions.py
- filter_item_by_permissions: 从空字典恢复完整20字段映射
- /borrow/stock-list: permission_prefix='op_borrow_apply' 传递

## buy.py / semi.py / product.py
- submit 成功响应剥离所有价格/成本字段(不泄露给前端)

## bom.py
- /base/list: 剥离 referencePrice
2026-07-16 11:26:05 +08:00
0651eb07fa fix: 彻底解耦出库/借库权限联动 + 路由去硬编码roles + 补API权限保护
根因: 借库选单页面14处硬编码outbound_selection:operation, 导致两模块权限联动

修复:
- borrow/apply: 14处outbound_selection:operation→op_borrow_apply:operation
- stock.py: 拆分_do_get_stock_list裸逻辑, 出库/借库各绑独立权限码
- transactions: 新增/borrow/stock-list端点(@permission(op_borrow_apply))
- transaction.ts: 新增getBorrowStockList前端API函数
- outbound.py: 出库审批4端点改用独立outbound_approval权限码
- transactions.py: 借库审批3端点补@permission(op_borrow_approval)
- purchase.py: 采购管理7端点补@permission(inbound_buy)
- audit.py: 审计日志补@permission(system_audit)
- router: 清除全部6处硬编码roles, 交由动态权限树控制
2026-07-15 14:33:11 +08:00
4f5965db02 feat: JWT多租户数据权限隔离 & 主管系统管理权限 & 含税单价补齐
## 多租户公司数据隔离
- 新增 get_current_company_filter() 工具函数 (decorators.py)
  SUPER_ADMIN: 可传company_name参数过滤或传ALL看全量
  其他角色: 强制隔离到JWT中的company_name
- 重构 base_service.py / buy_service.py: 用集中式函数替换内联公司过滤
- SysRolePermission 表新增 company_name 字段,支持同角色不同公司权限
- get_user_permissions() 新增 company_name 参数,查公司定制+全局模板权限
- permission.py API 新增 @permission_required 拦截 + 公司过滤
- 19个API/service文件传递 company_name 到权限查询

## 主管系统管理权限
- delete_user() 允许SUPERVISOR删除同公司用户 (原仅SUPER_ADMIN)
- get_all_users() 新增 company_name 参数过滤
- 用户列表/权限分配 API 应用 get_current_company_filter()
- 前端 UserCreate.vue: 超管可见公司下拉框,主管隐藏部门字段

## 前端多租户适配
- material/list.vue / buy.vue: 公司下拉框仅超管可见,默认ALL
- UserCreate.vue: 新增搜索栏公司筛选,部门字段按角色显隐
- auth.ts: getUserList() 支持 params 参数

## Bug修复: 含税单价字段补齐
- buy.vue: 表格列/高级筛选/排序/权限映射新增 post_tax_unit_price
- buy_service.py: allowed_fields/sort_field_map 新增 post_tax_unit_price
2026-07-13 15:12:22 +08:00
DXC
1450e6c1de fix(借还记录列表): 按 borrow_no 单号维度分页 + 修 SQLAlchemy Row 适配错误
- 分页基准从明细行改为单号:21 项单号不再被拆到 3 页

- 步骤 1a 构造 GROUP BY borrow_no 的 subquery(sort_key + status 聚合)

- 步骤 2 主查询 SELECT order_subq.c.borrow_no 一列,避免触发 PG GROUP BY 严格模式 (f405)

- 步骤 3 用 page_borrow_nos 拉明细,保留前端 groupMap 期望的 items 形态

- pagination.items 用 isinstance + hasattr(_mapping) 兜底提取纯字符串(修 psycopg2 can't adapt type 'Row')

- service 加 try-except,路由层识别 500 透传 traceback

- status 过滤改为单号聚合(borrowed=至少一条未还,returned=全部归还)
2026-06-16 14:53:42 +08:00
DXC
83b3db693a fix(借库扫码出库): 校验 key 从 (source_table, sku) 改为 (name, spec_model) + N+1 修复
- 借库申请按 (name, spec_model) 发起,审批明细无 sku 字段;
  旧代码用 sku 做 key 会导致所有条目坍塌到同一桶,校验形同虚设
- 改为在扫码循环内即时累加、即时拦截:
  防线4 锁定 stock 行后从 material_base 取真实 (name, spec_model),
  与审批单按 strip 后的 (name, spec_model) 聚合比对
- 新增 joinedload(ModelClass.base) 一次 JOIN 加载 base,
  避免循环内 stock.base 触发 N+1
- 修正 dispatch_borrow docstring 中"sku 用于超额交叉校验"的错误描述
2026-06-16 13:50:49 +08:00
DXC
bfeb397c4a fix(借库扫码出库): items 字段名 stock_id → id 修复 400 + dispatch_borrow docstring 补全 sku 字段说明 2026-06-16 13:38:51 +08:00
DXC
941bd20fbd fix(借库审批): borrow_service pytz时区修复 + transactions except块traceback增强 2026-06-12 14:06:17 +08:00
DXC
d736d5d4a9 fix: 彻底移除 op_records 字段级权限过滤,所有字段对普通角色可见 2026-05-15 10:40:53 +08:00
DXC
27683d2a8b fix: 借用记录列表移除 sku 字段的权限过滤,所有角色均可正常查看 2026-05-15 09:57:55 +08:00
DXC
62c0e3738e fix(outbound+trans): 修复POST接口错误数据清洗导致的sku/quantity字段被清除Bug,并新增出库审批工作流全链路 2026-04-28 16:02:34 +08:00
DXC
71e5f075d2 feat: implement composite debounced search with prepended select and wipe out duplicate root permission nodes 2026-03-20 10:26:45 +08:00
dxc
de0a5c8db2 全局审计日志 2026-03-10 17:27:54 +08:00
dxc
a0993767fe fix: make SUPER_ADMIN role checks case-insensitive across app
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-27 17:04:22 +08:00
dxc
1fe00a8ba3 feat: Add field permission checks to outbound and transaction APIs
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-27 15:11:10 +08:00
dxc
a2b1a62132 feat: add RBAC and field masking for borrow/return/records pages
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-27 14:05:52 +08:00
dxc
b98f89bfe4 chore: add .material->.base refactor check comments
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-10 11:34:50 +08:00
dxc
04ee938cd1 借库逻辑实现 2026-02-06 17:11:47 +08:00
dxc
fd5600b65b 修改登录退出逻辑 2026-02-04 14:29:59 +08:00
dxc
13590b1fac 超级管理员登录设置 2026-02-04 13:30:07 +08:00
dxc
ee9f4aed3e 修正git管理关系 2026-01-26 13:47:53 +08:00