Compare commits
11 Commits
c86e67b793
...
a96597da33
| Author | SHA1 | Date | |
|---|---|---|---|
| a96597da33 | |||
| 4c1c61065e | |||
| 25487dbede | |||
| a547d6b164 | |||
| 661ce4e5a0 | |||
| d6d9621bf3 | |||
| f178b9cd00 | |||
| 11fafde5e3 | |||
| 1f9a363545 | |||
| b3e1ac6245 | |||
| 73ee163352 |
@ -1,22 +1,77 @@
|
||||
# 文件路径: app/api/v1/inbound/base.py
|
||||
|
||||
from flask import Blueprint, request, jsonify, send_file
|
||||
from flask import Blueprint, request, jsonify, send_file, g
|
||||
from app.services.inbound.base_service import MaterialBaseService
|
||||
from app.utils.decorators import login_required, permission_required
|
||||
import traceback
|
||||
import datetime
|
||||
|
||||
inbound_base_bp = Blueprint('stock_base', __name__)
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 辅助函数:获取当前用户的完整权限列表(基于角色查询)
|
||||
# ==============================================================================
|
||||
def get_current_user_permissions():
|
||||
"""
|
||||
返回当前用户拥有的所有权限码列表(包括菜单和元素)
|
||||
此函数根据角色查询数据库得到权限。
|
||||
"""
|
||||
from flask_jwt_extended import get_jwt
|
||||
from app.services.auth_service import AuthService
|
||||
claims = get_jwt()
|
||||
user_role = claims.get('role')
|
||||
if not user_role:
|
||||
return []
|
||||
# 超级管理员返回所有字段权限
|
||||
if user_role == 'super_admin':
|
||||
return ['material_list:id', 'material_list:companyName', 'material_list:name', 'material_list:commonName', 'material_list:category', 'material_list:type',
|
||||
'material_list:spec', 'material_list:unit', 'material_list:inventoryCount', 'material_list:availableCount', 'material_list:files', 'material_list:isEnabled']
|
||||
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):
|
||||
"""
|
||||
根据用户权限过滤 item 字典,无权限的字段值置为 None
|
||||
"""
|
||||
# 字段名到权限码的映射(与前端 permissionMap 保持一致)
|
||||
field_to_perm = {
|
||||
'id': 'material_list:id',
|
||||
'companyName': 'material_list:companyName',
|
||||
'name': 'material_list:name',
|
||||
'commonName': 'material_list:commonName',
|
||||
'category': 'material_list:category',
|
||||
'type': 'material_list:type',
|
||||
'spec': 'material_list:spec',
|
||||
'unit': 'material_list:unit',
|
||||
'inventoryCount': 'material_list:inventoryCount',
|
||||
'availableCount': 'material_list:availableCount',
|
||||
'generalManual': 'material_list:files',
|
||||
'generalImage': 'material_list:files',
|
||||
'isEnabled': 'material_list:isEnabled'
|
||||
}
|
||||
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
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 1. 搜索接口 (GET /api/v1/inbound/base/search)
|
||||
# ==============================================================================
|
||||
@inbound_base_bp.route('/search', methods=['GET'])
|
||||
@permission_required('material_list')
|
||||
def search_base():
|
||||
try:
|
||||
keyword = request.args.get('keyword', '')
|
||||
data = MaterialBaseService.search_material(keyword)
|
||||
return jsonify({"code": 200, "msg": "success", "data": data})
|
||||
# 字段级脱敏
|
||||
user_permissions = get_current_user_permissions()
|
||||
filtered_data = [filter_item_by_permissions(item, user_permissions) for item in data]
|
||||
return jsonify({"code": 200, "msg": "success", "data": filtered_data})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
@ -26,6 +81,7 @@ def search_base():
|
||||
# 2. 列表接口 (GET /api/v1/inbound/base/list)
|
||||
# ==============================================================================
|
||||
@inbound_base_bp.route('/list', methods=['GET'])
|
||||
@permission_required('material_list')
|
||||
def get_list():
|
||||
try:
|
||||
page = request.args.get('pageNum', 1, type=int)
|
||||
@ -41,6 +97,10 @@ def get_list():
|
||||
}
|
||||
|
||||
result = MaterialBaseService.get_list(page, limit, filters)
|
||||
# 字段级脱敏
|
||||
user_permissions = get_current_user_permissions()
|
||||
if result.get('items'):
|
||||
result['items'] = [filter_item_by_permissions(item, user_permissions) for item in result['items']]
|
||||
return jsonify({"code": 200, "msg": "success", "data": result})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
@ -51,6 +111,7 @@ def get_list():
|
||||
# 2.1 选项接口 (GET /api/v1/inbound/base/options)
|
||||
# ==============================================================================
|
||||
@inbound_base_bp.route('/options', methods=['GET'])
|
||||
@permission_required('material_list')
|
||||
def get_options():
|
||||
try:
|
||||
data = MaterialBaseService.get_distinct_options()
|
||||
@ -64,6 +125,7 @@ def get_options():
|
||||
# 2.2 导出接口 (GET /api/v1/inbound/base/export)
|
||||
# ==============================================================================
|
||||
@inbound_base_bp.route('/export', methods=['GET'])
|
||||
@permission_required('material_list')
|
||||
def export_data():
|
||||
try:
|
||||
# 获取筛选条件
|
||||
@ -101,6 +163,7 @@ def export_data():
|
||||
# 3. 新增接口 (POST /api/v1/inbound/base/)
|
||||
# ==============================================================================
|
||||
@inbound_base_bp.route('/', methods=['POST'])
|
||||
@permission_required('material_list:operation')
|
||||
def create():
|
||||
try:
|
||||
data = request.get_json()
|
||||
@ -122,6 +185,7 @@ def create():
|
||||
# 4. 修改接口 (PUT /api/v1/inbound/base/<id>)
|
||||
# ==============================================================================
|
||||
@inbound_base_bp.route('/<int:id>', methods=['PUT'])
|
||||
@permission_required('material_list:operation')
|
||||
def update(id):
|
||||
try:
|
||||
data = request.get_json()
|
||||
@ -136,10 +200,11 @@ def update(id):
|
||||
# 5. 删除接口 (DELETE /api/v1/inbound/base/<id>)
|
||||
# ==============================================================================
|
||||
@inbound_base_bp.route('/<int:id>', methods=['DELETE'])
|
||||
@permission_required('material_list:operation')
|
||||
def delete(id):
|
||||
try:
|
||||
MaterialBaseService.delete_material(id)
|
||||
return jsonify({"code": 200, "msg": "删除成功"})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
# app/utils/decorators.py
|
||||
from functools import wraps
|
||||
from flask_jwt_extended import get_jwt
|
||||
from flask import jsonify
|
||||
from flask_jwt_extended import get_jwt, verify_jwt_in_request
|
||||
from flask import jsonify, g
|
||||
import logging
|
||||
|
||||
|
||||
def role_required(*roles):
|
||||
@ -27,4 +28,61 @@ def role_required(*roles):
|
||||
|
||||
return decorator
|
||||
|
||||
return wrapper
|
||||
return wrapper
|
||||
|
||||
|
||||
def login_required(fn):
|
||||
"""
|
||||
验证 JWT 令牌是否存在且有效
|
||||
"""
|
||||
@wraps(fn)
|
||||
def decorator(*args, **kwargs):
|
||||
try:
|
||||
verify_jwt_in_request()
|
||||
except Exception as e:
|
||||
logging.warning(f"JWT verification failed: {e}")
|
||||
return jsonify(msg='登录已过期,请重新登录'), 401
|
||||
return fn(*args, **kwargs)
|
||||
return decorator
|
||||
|
||||
|
||||
def permission_required(permission_code):
|
||||
"""
|
||||
检查当前用户是否拥有指定权限码
|
||||
使用方法: @permission_required('material:base:read')
|
||||
"""
|
||||
def wrapper(fn):
|
||||
@wraps(fn)
|
||||
def decorator(*args, **kwargs):
|
||||
# 首先验证 JWT
|
||||
try:
|
||||
verify_jwt_in_request()
|
||||
except Exception as e:
|
||||
logging.warning(f"JWT verification failed: {e}")
|
||||
return jsonify(msg='登录已过期,请重新登录'), 401
|
||||
|
||||
claims = get_jwt()
|
||||
user_role = claims.get('role')
|
||||
# 超级管理员放行
|
||||
if user_role == 'super_admin':
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
# 根据角色查询数据库中的权限
|
||||
try:
|
||||
from app.services.auth_service import AuthService
|
||||
perm_dict = AuthService.get_user_permissions(user_role)
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to fetch permissions for role {user_role}: {e}")
|
||||
return jsonify(msg='权限查询失败'), 403
|
||||
|
||||
# 合并菜单和元素权限
|
||||
all_perms = perm_dict.get('menus', []) + perm_dict.get('elements', [])
|
||||
if permission_code not in all_perms:
|
||||
# 详细的调试日志
|
||||
print(f"🔴 [权限拦截] 角色 '{user_role}' 访问被拒!需要权限码: '{permission_code}', 但该角色实际拥有: {all_perms}")
|
||||
logging.warning(
|
||||
f"权限检查失败: 角色={user_role}, 所需权限={permission_code}, 实际权限列表={all_perms}")
|
||||
return jsonify(msg='权限不足:您没有访问此资源的权限'), 403
|
||||
return fn(*args, **kwargs)
|
||||
return decorator
|
||||
return wrapper
|
||||
|
||||
@ -89,7 +89,7 @@ const handleLogout = () => {
|
||||
<footer v-if="!isLoginPage" class="app-footer">
|
||||
<span class="version-tag">
|
||||
<el-icon style="vertical-align: middle; margin-right: 4px"><InfoFilled /></el-icon>
|
||||
当前版本: 1.3 Beta (2.25权限管理版)
|
||||
当前版本: 1.4 Beta (2.27权限管理版)
|
||||
</span>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@ -71,7 +71,7 @@
|
||||
<el-icon style="margin-right: 5px"><Download /></el-icon>导出库存统计
|
||||
</el-button>
|
||||
|
||||
<el-button type="primary" @click="handleAdd" style="margin-right: 10px">
|
||||
<el-button v-if="userStore.hasPermission('material_list:operation')" type="primary" @click="handleAdd" style="margin-right: 10px">
|
||||
<el-icon style="margin-right: 5px"><Plus /></el-icon>新增
|
||||
</el-button>
|
||||
|
||||
@ -210,14 +210,15 @@
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
:loading="scope.row.statusLoading"
|
||||
:disabled="!userStore.hasPermission('material_list:operation')"
|
||||
@change="handleStatusChange(scope.row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="userStore.hasPermission('operation')" label="操作" min-width="150" fixed="right" align="center">
|
||||
<el-table-column v-if="userStore.hasPermission('material_list:operation')" label="操作" min-width="150" fixed="right" align="center">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
|
||||
<el-button link type="danger" size="small" @click="handleDelete(scope.row)">删除</el-button>
|
||||
<el-button v-if="userStore.hasPermission('material_list:operation')" link type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
|
||||
<el-button v-if="userStore.hasPermission('material_list:operation')" link type="danger" size="small" @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@ -506,18 +507,18 @@ const columns = reactive({
|
||||
|
||||
// 列与权限Code的映射关系(数据库中的code)
|
||||
const permissionMap: Record<string, string> = {
|
||||
id: 'id',
|
||||
companyName: 'companyName',
|
||||
name: 'name',
|
||||
commonName: 'commonName',
|
||||
category: 'category',
|
||||
type: 'type',
|
||||
spec: 'spec',
|
||||
unit: 'unit',
|
||||
inventory: 'inventoryCount', // 前端变量是 inventory,数据库Code是 inventoryCount
|
||||
available: 'availableCount', // 前端变量是 available,数据库Code是 availableCount
|
||||
files: 'files',
|
||||
isEnabled: 'isEnabled'
|
||||
id: 'material_list:id',
|
||||
companyName: 'material_list:companyName',
|
||||
name: 'material_list:name',
|
||||
commonName: 'material_list:commonName',
|
||||
category: 'material_list:category',
|
||||
type: 'material_list:type',
|
||||
spec: 'material_list:spec',
|
||||
unit: 'material_list:unit',
|
||||
inventory: 'material_list:inventoryCount',
|
||||
available: 'material_list:availableCount',
|
||||
files: 'material_list:files',
|
||||
isEnabled: 'material_list:isEnabled'
|
||||
};
|
||||
|
||||
// 根据用户权限初始化列显示状态
|
||||
@ -527,11 +528,12 @@ const initColumnPermissions = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// 普通用户:严格执行列级权限控制,没有权限的列必须隐藏
|
||||
Object.keys(columns).forEach(key => {
|
||||
const code = permissionMap[key];
|
||||
if (code) {
|
||||
// 如果用户有该权限,则显示列(默认true);否则隐藏
|
||||
columns[key].visible = userStore.hasPermission(code);
|
||||
// 如果不具备该权限,必须设为 false
|
||||
columns[key].visible = !!userStore.hasPermission(code);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@ -220,11 +220,11 @@ const transformData = (nodes: any[]): PermissionNode[] => {
|
||||
const allChildren = node.children || []
|
||||
|
||||
// 1. 找操作列 (作为 Write 权限)
|
||||
const opNode = allChildren.find((c: any) => c.type === 'element' && (c.code === 'operation' || c.code.endsWith('_op')))
|
||||
const opNode = allChildren.find((c: any) => c.type === 'element' && (c.code === 'operation' || c.code.endsWith(':operation') || c.code.endsWith('_op')))
|
||||
const operationCode = opNode ? opNode.code : null
|
||||
|
||||
// 2. 找普通列 (作为字段权限)
|
||||
const columns = allChildren.filter((c: any) => c.type === 'element' && c.code !== 'operation' && !c.code.endsWith('_op'))
|
||||
const columns = allChildren.filter((c: any) => c.type === 'element' && c.code !== 'operation' && !c.code.endsWith('_op') && !c.code.endsWith(':operation'))
|
||||
|
||||
// 3. 找子菜单 (如果有)
|
||||
const subMenus = allChildren.filter((c: any) => c.type === 'menu')
|
||||
@ -624,4 +624,4 @@ onMounted(() => {
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user