From 26a1857acbed60cf86b54ee23283b05d5edfc7d9 Mon Sep 17 00:00:00 2001 From: yueli Date: Fri, 18 Sep 2026 14:36:02 +0800 Subject: [PATCH] =?UTF-8?q?fix(purchase):=20=E5=BE=85=E9=87=87=E8=B4=AD?= =?UTF-8?q?=E6=B8=85=E5=8D=95=E7=9A=84=E5=8F=82=E8=80=83=E4=BB=B7=E6=A0=BC?= =?UTF-8?q?=E6=94=B9=E6=8C=89=20material=5Flist:referencePrice=20=E7=AE=A1?= =?UTF-8?q?=E6=8E=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复一个权限旁路:待采购清单的「参考单价」原本后端无条件返回、前端列也没有 任何门控,而 material_list:referencePrice 只授予 SUPER_ADMIN 与 SUPERVISOR。 结果是 WAREHOUSE_MGR(库管) / INBOUND(入库员) / SALES(销售) 虽然都持有 inbound_purchase:pending_pool、能打开待采购清单,就会看到参考价格 —— 而这个 数字他们在物料列表里是被挡住的。等于本页面成了绕过该权限的后门。 参考价格来自 material_base.reference_price,与物料列表同源,因此复用同一个 权限码管控,不另造新码(同源数据用同码,口径才不会漂)。 改动: - 后端 get_pending_purchase_pool 新增 include_reference_price 参数, fail-closed 默认 False;无权限时**整个字段不返回**而不是给 None - 新增 _has_material_reference_price_perm(),判定方式与既有的 _filter_purchase_prices 保持一致(超管/主管放行 + 逐个权限码比对) - 前端「参考单价」列加 hasPermission 门控,TS 类型改为可选 ★ 只挡前端等于没挡(接口仍然裸奔),前后端必须同时改。 --- inventory-backend/app/api/v1/purchase.py | 21 +++++++++++++++++++ .../app/services/purchase_service.py | 15 +++++++++++-- inventory-web/src/api/purchase.ts | 3 ++- .../src/views/purchase/PendingPool.vue | 10 ++++++++- 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/inventory-backend/app/api/v1/purchase.py b/inventory-backend/app/api/v1/purchase.py index b49e94f..7bbd0ca 100644 --- a/inventory-backend/app/api/v1/purchase.py +++ b/inventory-backend/app/api/v1/purchase.py @@ -53,6 +53,25 @@ def _filter_purchase_prices(item_dict): item_dict.pop('tax_rate', None) +def _has_material_reference_price_perm(): + """ + 能否看物料的参考价格。 + + ★ 参考价格(material_base.reference_price)是受 material_list:referencePrice + 管控的价格数据,与物料列表同源。任何暴露它的新接口都必须用**同一个码**, + 否则就成了绕过该权限的后门 —— 物料列表里挡住的数字,换个页面就看到了。 + 判定方式与 _filter_purchase_prices 保持一致(超管/主管放行 + 逐个权限码比对)。 + """ + from app.services.auth_service import AuthService + claims = get_jwt() + role = claims.get('role', '') + if role.upper() in ('SUPER_ADMIN', 'SUPERVISOR'): + return True + perm_dict = AuthService.get_user_permissions(role, company_name=claims.get('company_name', '')) + all_perms = perm_dict.get('menus', []) + perm_dict.get('elements', []) + return 'material_list:referencePrice' in all_perms + + # -------------------------------------------------------- # 1. 采购申请列表 # GET /api/v1/purchase @@ -452,6 +471,8 @@ def get_pending_purchase_pool(): page=page, per_page=limit, keyword=keyword, category=category, material_type=material_type, warning_status=warning_status, + # 参考价格按 material_list:referencePrice 管控,无权限时不返回该字段 + include_reference_price=_has_material_reference_price_perm(), ) return jsonify({'code': 200, 'msg': '获取成功', 'data': result}), 200 except Exception as e: diff --git a/inventory-backend/app/services/purchase_service.py b/inventory-backend/app/services/purchase_service.py index 471a49e..971f1e4 100644 --- a/inventory-backend/app/services/purchase_service.py +++ b/inventory-backend/app/services/purchase_service.py @@ -536,7 +536,8 @@ class PurchaseService: @staticmethod def get_pending_purchase_pool(page=1, per_page=20, keyword=None, category=None, material_type=None, - warning_status=None): + warning_status=None, + include_reference_price=False): """ 待采购池:找出「**有效供给**仍不足、需要再买」的物料。 @@ -561,6 +562,12 @@ class PurchaseService: - 阈值可能为 None,红黄两个阈值**各自独立**判断,不能 coalesce 成一个 - 用 `<=` 而非 `<` —— 恰好等于阈值即算不足 + ★ 参考价格(reference_price)默认**不返回**(fail-closed)。 + 它是 material_base.reference_price,属于受 material_list:referencePrice + 管控的价格数据 —— 与物料列表同源,就必须同码管控,否则这里会变成 + 绕过该权限的后门(物料列表看不到、待采购清单却看得到)。 + 调用方(API 层)按权限显式传 include_reference_price=True。 + ★ 关于 `<=`(2026-09-18 与业务方确认,**刻意维持,勿擅自改成 `<`**): 阈值是「警戒下限」,到线即需关注;且物料列表预警、预警邮件用的是同一套 `<=` 口径(那是系统原有约定,不是本接口另立的)。若只把本接口改成 `<`, @@ -738,7 +745,11 @@ class PurchaseService: 'company_name': material.company_name or '', 'image': PurchaseService._first_image(material.product_image), 'purchase_link': material.purchase_link or '', - 'reference_price': float(material.reference_price) if material.reference_price is not None else None, + # fail-closed:无权限时整个字段不出现,而不是给 None —— + # 前端据字段是否存在决定列是否渲染,语义更干净 + **({'reference_price': float(material.reference_price) + if material.reference_price is not None else None} + if include_reference_price else {}), 'inventory_count': inv, 'available_count': avail, # 在途量与有效供给一并返回,前端才能向采购员解释「为什么只建议买这么多」 diff --git a/inventory-web/src/api/purchase.ts b/inventory-web/src/api/purchase.ts index 7e854e3..5ec760d 100644 --- a/inventory-web/src/api/purchase.ts +++ b/inventory-web/src/api/purchase.ts @@ -175,7 +175,8 @@ export interface PendingPoolItem { company_name: string image: string purchase_link: string - reference_price: number | null + // 仅供参考展示,受 material_list:referencePrice 管控 —— 无权限时后端**不返回该字段** + reference_price?: number | null inventory_count: number available_count: number // 在途量 = 该物料所有活跃采购单的剩余待入库量之和 diff --git a/inventory-web/src/views/purchase/PendingPool.vue b/inventory-web/src/views/purchase/PendingPool.vue index 97d9c53..0417d76 100644 --- a/inventory-web/src/views/purchase/PendingPool.vue +++ b/inventory-web/src/views/purchase/PendingPool.vue @@ -184,7 +184,15 @@ - + +