From a2b1a62132ffb64d9302d38aa665ed32fa631586 Mon Sep 17 00:00:00 2001 From: dxc Date: Fri, 27 Feb 2026 14:05:52 +0800 Subject: [PATCH] feat: add RBAC and field masking for borrow/return/records pages Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) --- inventory-backend/app/api/v1/transactions.py | 59 ++++++++++++++++++- .../src/views/transaction/borrow.vue | 55 +++++++++++++---- .../src/views/transaction/records.vue | 28 ++++++++- .../src/views/transaction/return.vue | 51 ++++++++++++---- 4 files changed, 170 insertions(+), 23 deletions(-) diff --git a/inventory-backend/app/api/v1/transactions.py b/inventory-backend/app/api/v1/transactions.py index 1228c93..3654d3e 100644 --- a/inventory-backend/app/api/v1/transactions.py +++ b/inventory-backend/app/api/v1/transactions.py @@ -1,14 +1,64 @@ from flask import Blueprint, jsonify, request # .material -> .base refactor checked -from flask_jwt_extended import jwt_required, get_jwt_identity +from flask_jwt_extended import jwt_required, get_jwt_identity, get_jwt +from app.utils.decorators import permission_required +from app.services.auth_service import AuthService from app.services.trans_service import TransService import traceback trans_bp = Blueprint('transactions', __name__, url_prefix='/transactions') +# ============================================================================== +# 辅助函数:获取当前用户的完整权限列表(基于角色查询) +# ============================================================================== +def get_current_user_permissions(): + """ + 返回当前用户拥有的所有权限码列表(包括菜单和元素) + 此函数根据角色查询数据库得到权限。 + """ + claims = get_jwt() + user_role = claims.get('role') + if not user_role: + return [] + # 超级管理员返回所有字段权限 + if user_role == 'super_admin': + return ['*'] + perm_dict = AuthService.get_user_permissions(user_role) + # 合并菜单和元素权限 + perms = perm_dict.get('menus', []) + perm_dict.get('elements', []) + return perms + + +def filter_item_by_permissions(item_dict, user_permissions, prefix='op_records'): + """ + 根据用户权限过滤 item 字典,无权限的字段值置为 None + """ + # 字段名到权限码的映射(与前端 permissionMap 保持一致) + field_to_perm = { + 'borrow_no': f'{prefix}:borrow_no', + 'borrower_name': f'{prefix}:borrower_name', + 'sku': f'{prefix}:sku', + 'borrow_time': f'{prefix}:borrow_time', + 'return_time': f'{prefix}:return_time', + 'status': f'{prefix}:status', + 'expected_return_time': f'{prefix}:expected_return_time', + 'return_location': f'{prefix}:return_location', + 'borrow_signature': f'{prefix}:borrow_signature', + 'return_signature': f'{prefix}:return_signature', + } + # 如果用户是超级管理员且有 '*',则不过滤 + if '*' in user_permissions: + return item_dict + for field, perm_code in field_to_perm.items(): + if field in item_dict and perm_code not in user_permissions: + item_dict[field] = None + return item_dict + + # --- 借库接口 --- @trans_bp.route('/borrow', methods=['POST']) @jwt_required() +@permission_required('op_borrow:operation') def create_borrow(): data = request.get_json() try: @@ -21,6 +71,7 @@ def create_borrow(): # --- 还库辅助:扫码查找借出记录 --- @trans_bp.route('/return/scan', methods=['GET']) @jwt_required() +@permission_required('op_return') def scan_borrowed_item(): barcode = request.args.get('barcode') if not barcode: @@ -36,6 +87,7 @@ def scan_borrowed_item(): # --- 还库提交 --- @trans_bp.route('/return', methods=['POST']) @jwt_required() +@permission_required('op_return:operation') def submit_return(): data = request.get_json() user = get_jwt_identity() # 库管 @@ -49,10 +101,15 @@ def submit_return(): # --- 记录列表 --- @trans_bp.route('/records', methods=['GET']) @jwt_required() +@permission_required('op_records') def get_records(): status = request.args.get('status', 'all') page = int(request.args.get('page', 1)) keyword = request.args.get('keyword', '') res = TransService.get_records(page=page, limit=10, status=status, keyword=keyword) + # 字段级脱敏 + user_permissions = get_current_user_permissions() + if res.get('items'): + res['items'] = [filter_item_by_permissions(item, user_permissions, 'op_records') for item in res['items']] return jsonify({'code': 200, 'data': res}) diff --git a/inventory-web/src/views/transaction/borrow.vue b/inventory-web/src/views/transaction/borrow.vue index 0c9e5b8..f8b5606 100644 --- a/inventory-web/src/views/transaction/borrow.vue +++ b/inventory-web/src/views/transaction/borrow.vue @@ -13,10 +13,14 @@
-
+
点击开启全屏扫码
+
+ + 无扫码权限 +
@@ -40,16 +45,16 @@
- - + + - + - + - + @@ -102,7 +108,7 @@ -
+
签名 点击重签 @@ -112,11 +118,17 @@ 点击此处进行全屏签名
+
+
+ + 无签名权限 +
+
- 清空 - + 清空 + 确认借出
@@ -187,6 +199,27 @@ import QrScanner from '@/components/QrScanner/index.vue' import { getStockByBarcode } from '@/api/outbound' import request from '@/utils/request' import { uploadFile } from '@/api/common/upload' +import { useUserStore } from '@/stores/user' + +const userStore = useUserStore() + +// 列与权限Code的映射关系(数据库中的code) +const permissionMap: Record = { + borrower_name: 'op_borrow:borrower_name', + sku: 'op_borrow:sku', + available_quantity: 'op_borrow:available_quantity', + out_quantity: 'op_borrow:out_quantity', + // 其他字段可根据需要添加 +} + +// 检查列权限 +const hasColumnPermission = (prop: string) => { + if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') { + return true + } + const code = permissionMap[prop] + return code ? userStore.hasPermission(code) : false +} // --- 状态定义 --- const barcodeInput = ref('') @@ -564,4 +597,4 @@ onUnmounted(() => { .sidebar-actions { flex-direction: row; width: 100%; gap: 10px; } .sidebar-actions .el-button { flex: 1; height: 40px; } } - \ No newline at end of file + diff --git a/inventory-web/src/views/transaction/records.vue b/inventory-web/src/views/transaction/records.vue index fd622a6..28f73fe 100644 --- a/inventory-web/src/views/transaction/records.vue +++ b/inventory-web/src/views/transaction/records.vue @@ -88,6 +88,32 @@ import request from '@/utils/request' import dayjs from 'dayjs' // 建议使用 dayjs 处理日期,如果没有安装,可以用原生 Date import 'dayjs/locale/zh-cn' // 导入中文包 dayjs.locale('zh-cn') +import { useUserStore } from '@/stores/user' + +const userStore = useUserStore() + +// 列与权限Code的映射关系(数据库中的code) +const permissionMap: Record = { + borrow_no: 'op_records:borrow_no', + borrower_name: 'op_records:borrower_name', + sku: 'op_records:sku', + borrow_time: 'op_records:borrow_time', + return_time: 'op_records:return_time', + status: 'op_records:status', + expected_return_time: 'op_records:expected_return_time', + return_location: 'op_records:return_location', + borrow_signature: 'op_records:borrow_signature', + return_signature: 'op_records:return_signature', +} + +// 检查列权限 +const hasColumnPermission = (prop: string) => { + if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') { + return true + } + const code = permissionMap[prop] + return code ? userStore.hasPermission(code) : false +} const list = ref([]) const total = ref(0) @@ -195,4 +221,4 @@ onMounted(fetchData) .text-normal { color: #909399; } - \ No newline at end of file + diff --git a/inventory-web/src/views/transaction/return.vue b/inventory-web/src/views/transaction/return.vue index a175be1..c98519d 100644 --- a/inventory-web/src/views/transaction/return.vue +++ b/inventory-web/src/views/transaction/return.vue @@ -13,10 +13,14 @@
-
+
点击开启全屏扫码
+
+ + 无扫码权限 +
@@ -40,16 +45,17 @@
- - + + - +