From afcf90a8598a8784476aae43940ddcd82ed8e093 Mon Sep 17 00:00:00 2001 From: dxc Date: Fri, 27 Feb 2026 15:03:44 +0800 Subject: [PATCH] feat: enforce field-level permissions for buy and service modules Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) --- inventory-backend/app/api/v1/inbound/buy.py | 89 +++++++++++++++++++ .../app/api/v1/inbound/service.py | 50 +++++++++++ inventory-web/src/views/stock/inbound/buy.vue | 61 +++++++++++-- .../src/views/stock/inbound/service.vue | 42 +++++++-- 4 files changed, 228 insertions(+), 14 deletions(-) diff --git a/inventory-backend/app/api/v1/inbound/buy.py b/inventory-backend/app/api/v1/inbound/buy.py index 59915bb..4b1f8ac 100644 --- a/inventory-backend/app/api/v1/inbound/buy.py +++ b/inventory-backend/app/api/v1/inbound/buy.py @@ -145,6 +145,51 @@ def submit(): if not data: return jsonify({"code": 400, "msg": "No data"}), 400 + # 数据清洗:移除用户没有权限的字段 + user_permissions = get_current_user_permissions() + # 超级管理员不过滤 + if 'inbound_buy:*' not in user_permissions: + # 字段名到权限码的映射(与前端 permissionMap 保持一致) + field_to_perm = { + 'id': 'inbound_buy:id', + 'base_id': 'inbound_buy:base_id', + 'global_print_id': 'inbound_buy:global_print_id', + 'sku': 'inbound_buy:sku', + 'barcode': 'inbound_buy:barcode', + 'in_date': 'inbound_buy:in_date', + 'serial_number': 'inbound_buy:serial_number', + 'batch_number': 'inbound_buy:batch_number', + 'status': 'inbound_buy:status', + 'in_quantity': 'inbound_buy:in_quantity', + 'stock_quantity': 'inbound_buy:stock_quantity', + 'available_quantity': 'inbound_buy:available_quantity', + 'inspection_status': 'inbound_buy:inspection_status', + 'warehouse_location': 'inbound_buy:warehouse_location', + 'unit_price': 'inbound_buy:unit_price', + 'tax_rate': 'inbound_buy:tax_rate', + 'total_price': 'inbound_buy:total_price', + 'currency': 'inbound_buy:currency', + 'exchange_rate': 'inbound_buy:exchange_rate', + 'supplier_name': 'inbound_buy:supplier_name', + 'buyer_name': 'inbound_buy:buyer_name', + 'buyer_email': 'inbound_buy:buyer_email', + 'original_link': 'inbound_buy:original_link', + 'detail_link': 'inbound_buy:detail_link', + 'arrival_photo': 'inbound_buy:arrival_photo', + 'inspection_report': 'inbound_buy:inspection_report', + 'material_name': 'inbound_buy:material_name', + 'spec_model': 'inbound_buy:spec_model', + 'category': 'inbound_buy:category', + 'unit': 'inbound_buy:unit', + 'material_type': 'inbound_buy:material_type', + 'company_name': 'inbound_buy:company_name', + } + # 复制一份,避免遍历时修改字典 + for field in list(data.keys()): + perm_code = field_to_perm.get(field) + if perm_code and perm_code not in user_permissions: + data.pop(field, None) + new_stock = BuyInboundService.handle_inbound(data) return jsonify({ @@ -165,6 +210,50 @@ def submit(): def update_buy(id): try: data = request.get_json() + # 数据清洗:移除用户没有权限的字段 + user_permissions = get_current_user_permissions() + # 超级管理员不过滤 + if 'inbound_buy:*' not in user_permissions: + field_to_perm = { + 'id': 'inbound_buy:id', + 'base_id': 'inbound_buy:base_id', + 'global_print_id': 'inbound_buy:global_print_id', + 'sku': 'inbound_buy:sku', + 'barcode': 'inbound_buy:barcode', + 'in_date': 'inbound_buy:in_date', + 'serial_number': 'inbound_buy:serial_number', + 'batch_number': 'inbound_buy:batch_number', + 'status': 'inbound_buy:status', + 'in_quantity': 'inbound_buy:in_quantity', + 'stock_quantity': 'inbound_buy:stock_quantity', + 'available_quantity': 'inbound_buy:available_quantity', + 'inspection_status': 'inbound_buy:inspection_status', + 'warehouse_location': 'inbound_buy:warehouse_location', + 'unit_price': 'inbound_buy:unit_price', + 'tax_rate': 'inbound_buy:tax_rate', + 'total_price': 'inbound_buy:total_price', + 'currency': 'inbound_buy:currency', + 'exchange_rate': 'inbound_buy:exchange_rate', + 'supplier_name': 'inbound_buy:supplier_name', + 'buyer_name': 'inbound_buy:buyer_name', + 'buyer_email': 'inbound_buy:buyer_email', + 'original_link': 'inbound_buy:original_link', + 'detail_link': 'inbound_buy:detail_link', + 'arrival_photo': 'inbound_buy:arrival_photo', + 'inspection_report': 'inbound_buy:inspection_report', + 'material_name': 'inbound_buy:material_name', + 'spec_model': 'inbound_buy:spec_model', + 'category': 'inbound_buy:category', + 'unit': 'inbound_buy:unit', + 'material_type': 'inbound_buy:material_type', + 'company_name': 'inbound_buy:company_name', + } + # 复制一份,避免遍历时修改字典 + for field in list(data.keys()): + perm_code = field_to_perm.get(field) + if perm_code and perm_code not in user_permissions: + data.pop(field, None) + BuyInboundService.update_inbound(id, data) return jsonify({"code": 200, "msg": "更新成功"}) except Exception as e: diff --git a/inventory-backend/app/api/v1/inbound/service.py b/inventory-backend/app/api/v1/inbound/service.py index 91f9680..6d2f9fc 100644 --- a/inventory-backend/app/api/v1/inbound/service.py +++ b/inventory-backend/app/api/v1/inbound/service.py @@ -118,6 +118,31 @@ def create_service(): if not data: return jsonify({'code': 400, 'msg': '请求数据为空'}), 400 + # 数据清洗:移除用户没有权限的字段 + user_permissions = get_current_user_permissions() + # 超级管理员不过滤 + if 'inbound_service:*' not in user_permissions: + # 字段名到权限码的映射(与前端 permissionMap 保持一致) + field_to_perm = { + 'id': 'inbound_service:id', + 'base_id': 'inbound_service:base_id', + 'sku': 'inbound_service:sku', + 'material_name': 'inbound_service:material_name', + 'provider_name': 'inbound_service:provider_name', + 'sale_price': 'inbound_service:sale_price', + 'description': 'inbound_service:description', + 'created_at': 'inbound_service:created_at', + 'material_type': 'inbound_service:material_type', + 'category': 'inbound_service:category', + 'spec_model': 'inbound_service:spec_model', + 'unit': 'inbound_service:unit', + } + # 复制一份,避免遍历时修改字典 + for field in list(data.keys()): + perm_code = field_to_perm.get(field) + if perm_code and perm_code not in user_permissions: + data.pop(field, None) + # 基础校验 if not data.get('base_id'): return jsonify({'code': 400, 'msg': '请选择基础物料'}), 400 @@ -169,6 +194,31 @@ def update_service(service_id): if not data: return jsonify({'code': 400, 'msg': '请求数据为空'}), 400 + # 数据清洗:移除用户没有权限的字段 + user_permissions = get_current_user_permissions() + # 超级管理员不过滤 + if 'inbound_service:*' not in user_permissions: + # 字段名到权限码的映射(与前端 permissionMap 保持一致) + field_to_perm = { + 'id': 'inbound_service:id', + 'base_id': 'inbound_service:base_id', + 'sku': 'inbound_service:sku', + 'material_name': 'inbound_service:material_name', + 'provider_name': 'inbound_service:provider_name', + 'sale_price': 'inbound_service:sale_price', + 'description': 'inbound_service:description', + 'created_at': 'inbound_service:created_at', + 'material_type': 'inbound_service:material_type', + 'category': 'inbound_service:category', + 'spec_model': 'inbound_service:spec_model', + 'unit': 'inbound_service:unit', + } + # 复制一份,避免遍历时修改字典 + for field in list(data.keys()): + perm_code = field_to_perm.get(field) + if perm_code and perm_code not in user_permissions: + data.pop(field, None) + # 允许更新的字段 allowed_fields = { 'sale_price', 'provider_name', 'description', diff --git a/inventory-web/src/views/stock/inbound/buy.vue b/inventory-web/src/views/stock/inbound/buy.vue index 96bc6d2..4111a7e 100644 --- a/inventory-web/src/views/stock/inbound/buy.vue +++ b/inventory-web/src/views/stock/inbound/buy.vue @@ -272,12 +272,12 @@
- - - - - - + + + + + +
@@ -611,6 +611,55 @@ const vLoadmore = { } } +// ------------------------------------ +// 表单字段权限检查 +// ------------------------------------ +const hasFormFieldPermission = (fieldName: string) => { + // 超级管理员直接返回true + if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') { + return true + } + // 根据字段名映射到权限码 + const map: Record = { + company_name: 'inbound_buy:company_name', + material_name: 'inbound_buy:material_name', + spec_model: 'inbound_buy:spec_model', + category: 'inbound_buy:category', + material_type: 'inbound_buy:material_type', + unit: 'inbound_buy:unit', + sku: 'inbound_buy:sku', + barcode: 'inbound_buy:barcode', + in_date: 'inbound_buy:in_date', + serial_number: 'inbound_buy:serial_number', + batch_number: 'inbound_buy:batch_number', + status: 'inbound_buy:status', + inspection_status: 'inbound_buy:inspection_status', + in_quantity: 'inbound_buy:in_quantity', + stock_quantity: 'inbound_buy:stock_quantity', + available_quantity: 'inbound_buy:available_quantity', + warehouse_location: 'inbound_buy:warehouse_location', + unit_price: 'inbound_buy:unit_price', + tax_rate: 'inbound_buy:tax_rate', + total_price: 'inbound_buy:total_price', + currency: 'inbound_buy:currency', + exchange_rate: 'inbound_buy:exchange_rate', + supplier_name: 'inbound_buy:supplier_name', + purchaser: 'inbound_buy:purchaser', + purchaser_email: 'inbound_buy:purchaser_email', + source_link: 'inbound_buy:original_link', + detail_link: 'inbound_buy:detail_link', + arrival_photo: 'inbound_buy:arrival_photo', + inspection_report: 'inbound_buy:inspection_report', + print_copies: 'inbound_buy:print_copies', + } + const code = map[fieldName] + if (!code) { + // 没有映射的字段默认显示 + return true + } + return userStore.hasPermission(code) +} + // ------------------------------------ // 状态与变量 // ------------------------------------ diff --git a/inventory-web/src/views/stock/inbound/service.vue b/inventory-web/src/views/stock/inbound/service.vue index 5ceec2d..05d9112 100644 --- a/inventory-web/src/views/stock/inbound/service.vue +++ b/inventory-web/src/views/stock/inbound/service.vue @@ -134,11 +134,11 @@
- - - - - + + + + +
@@ -150,7 +150,7 @@ 2. 服务详情
- + - + - + { return code ? userStore.hasPermission(code) : false } +// 表单字段权限检查 +const hasFormFieldPermission = (fieldName: string) => { + // 超级管理员直接返回true + if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') { + return true + } + // 根据字段名映射到权限码 + const map: Record = { + base_id: 'inbound_service:base_id', + material_name: 'inbound_service:material_name', + spec_model: 'inbound_service:spec_model', + category: 'inbound_service:category', + material_type: 'inbound_service:material_type', + unit: 'inbound_service:unit', + sale_price: 'inbound_service:sale_price', + provider_name: 'inbound_service:provider_name', + description: 'inbound_service:description', + } + const code = map[fieldName] + if (!code) { + // 没有映射的字段默认显示 + return true + } + return userStore.hasPermission(code) +} + // 表格数据 const tableData = ref([]) const loading = ref(false)