feat: add RBAC read-write separation and field masking for bom_manage
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
This commit is contained in:
@ -3,15 +3,61 @@ from app.services.bom_service import BomService
|
||||
from app.models.base import MaterialBase
|
||||
from app.models.bom import BomTable
|
||||
from app.extensions import db
|
||||
from flask_jwt_extended import jwt_required
|
||||
from flask_jwt_extended import jwt_required, get_jwt
|
||||
from app.utils.decorators import permission_required
|
||||
from app.services.auth_service import AuthService
|
||||
|
||||
bom_bp = Blueprint('bom', __name__)
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 辅助函数:获取当前用户的完整权限列表(基于角色查询)
|
||||
# ==============================================================================
|
||||
def get_current_user_permissions():
|
||||
"""
|
||||
返回当前用户拥有的所有权限码列表(包括菜单和元素)
|
||||
此函数根据角色查询数据库得到权限。
|
||||
"""
|
||||
claims = get_jwt()
|
||||
user_role = claims.get('role')
|
||||
if not user_role:
|
||||
return []
|
||||
# 超级管理员返回所有字段权限
|
||||
if user_role == 'super_admin':
|
||||
return ['bom_manage:*']
|
||||
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 = {
|
||||
'bom_no': 'bom_manage:bom_no',
|
||||
'parent_name': 'bom_manage:parent_name',
|
||||
'parent_spec': 'bom_manage:parent_spec',
|
||||
'version': 'bom_manage:version',
|
||||
'is_enabled': 'bom_manage:status',
|
||||
'child_count': 'bom_manage:child_count',
|
||||
}
|
||||
# 如果用户是超级管理员且有 'bom_manage:*',则不过滤
|
||||
if 'bom_manage:*' 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
|
||||
|
||||
|
||||
# ==================== 新版 BOM 接口(基于 bom_no) ====================
|
||||
|
||||
@bom_bp.route('/list', methods=['GET'])
|
||||
@jwt_required()
|
||||
@permission_required('bom_manage')
|
||||
def get_bom_list():
|
||||
"""获取所有 BOM 配方列表,支持 keyword 搜索和 active_only 过滤"""
|
||||
try:
|
||||
@ -20,6 +66,10 @@ def get_bom_list():
|
||||
active_only = request.args.get('active_only', 'false').lower() == 'true'
|
||||
|
||||
data = BomService.get_bom_list(keyword=keyword, active_only=active_only)
|
||||
# 字段级脱敏
|
||||
user_permissions = get_current_user_permissions()
|
||||
if isinstance(data, list):
|
||||
data = [filter_item_by_permissions(item, user_permissions) for item in data]
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
'msg': 'success',
|
||||
@ -32,6 +82,7 @@ def get_bom_list():
|
||||
|
||||
@bom_bp.route('/detail/<bom_no>', methods=['GET'])
|
||||
@jwt_required()
|
||||
@permission_required('bom_manage')
|
||||
def get_bom_detail(bom_no):
|
||||
"""
|
||||
根据 BOM 编号获取配方详情
|
||||
@ -42,6 +93,9 @@ def get_bom_detail(bom_no):
|
||||
data = BomService.get_bom_detail(bom_no, version=version)
|
||||
if not data:
|
||||
return jsonify({'code': 404, 'msg': 'BOM 不存在'}), 404
|
||||
# 字段级脱敏
|
||||
user_permissions = get_current_user_permissions()
|
||||
data = filter_item_by_permissions(data, user_permissions)
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
'msg': 'success',
|
||||
@ -54,6 +108,7 @@ def get_bom_detail(bom_no):
|
||||
|
||||
@bom_bp.route('/save', methods=['POST'])
|
||||
@jwt_required()
|
||||
@permission_required('bom_manage:operation')
|
||||
def save_bom():
|
||||
"""保存或更新 BOM 配方(支持自定义 bom_no 和 多版本)"""
|
||||
try:
|
||||
@ -81,12 +136,16 @@ def save_bom():
|
||||
|
||||
@bom_bp.route('/stock/<bom_no>', methods=['GET'])
|
||||
@jwt_required()
|
||||
@permission_required('bom_manage')
|
||||
def get_bom_with_stock_by_no(bom_no):
|
||||
"""根据 BOM 编号获取配方详情及库存信息"""
|
||||
try:
|
||||
data = BomService.get_bom_with_stock_by_bom_no(bom_no)
|
||||
if not data:
|
||||
return jsonify({'code': 404, 'msg': 'BOM 不存在'}), 404
|
||||
# 字段级脱敏
|
||||
user_permissions = get_current_user_permissions()
|
||||
data = filter_item_by_permissions(data, user_permissions)
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
'msg': 'success',
|
||||
@ -101,6 +160,7 @@ def get_bom_with_stock_by_no(bom_no):
|
||||
|
||||
@bom_bp.route('/<bom_no>', methods=['DELETE'])
|
||||
@jwt_required()
|
||||
@permission_required('bom_manage:operation')
|
||||
def delete_bom(bom_no):
|
||||
"""
|
||||
根据 BOM 编号删除
|
||||
@ -133,9 +193,13 @@ def delete_bom(bom_no):
|
||||
|
||||
@bom_bp.route('/<int:parent_id>', methods=['GET'])
|
||||
@jwt_required()
|
||||
@permission_required('bom_manage')
|
||||
def get_bom(parent_id):
|
||||
try:
|
||||
data = BomService.get_bom_with_stock(parent_id)
|
||||
# 字段级脱敏
|
||||
user_permissions = get_current_user_permissions()
|
||||
data = filter_item_by_permissions(data, user_permissions)
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
'msg': 'success',
|
||||
@ -148,6 +212,7 @@ def get_bom(parent_id):
|
||||
|
||||
@bom_bp.route('', methods=['POST'])
|
||||
@jwt_required()
|
||||
@permission_required('bom_manage:operation')
|
||||
def save_bom_legacy():
|
||||
try:
|
||||
req_data = request.get_json()
|
||||
@ -169,11 +234,14 @@ def save_bom_legacy():
|
||||
|
||||
@bom_bp.route('/base/list', methods=['GET'])
|
||||
@jwt_required()
|
||||
@permission_required('bom_manage')
|
||||
def get_material_base_list():
|
||||
"""获取所有基础物料列表,用于前端下拉框"""
|
||||
try:
|
||||
materials = MaterialBase.query.filter_by(is_enabled=True).order_by(MaterialBase.id.desc()).all()
|
||||
data = [item.to_dict() for item in materials]
|
||||
# 字段级脱敏 (如果需要,但此接口通常用于下拉选择,可能不需要脱敏)
|
||||
# 保持原样
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
'msg': 'success',
|
||||
@ -186,12 +254,16 @@ def get_material_base_list():
|
||||
|
||||
@bom_bp.route('/parents', methods=['GET'])
|
||||
@jwt_required()
|
||||
@permission_required('bom_manage')
|
||||
def get_bom_parents():
|
||||
"""获取所有已定义BOM的父件物料列表(兼容旧版)"""
|
||||
try:
|
||||
subq = db.session.query(BomTable.parent_id).distinct().subquery()
|
||||
parents = MaterialBase.query.join(subq, MaterialBase.id == subq.c.parent_id).all()
|
||||
data = [item.to_dict() for item in parents]
|
||||
# 字段级脱敏 (如果需要)
|
||||
user_permissions = get_current_user_permissions()
|
||||
data = [filter_item_by_permissions(item, user_permissions) for item in data]
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
'msg': 'success',
|
||||
|
||||
@ -17,29 +17,29 @@
|
||||
<el-button :icon="Search" @click="fetchBomList" />
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button type="primary" :icon="Plus" @click="handleCreate">新建 BOM</el-button>
|
||||
<el-button v-if="userStore.hasPermission('bom_manage:operation')" type="primary" :icon="Plus" @click="handleCreate">新建 BOM</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table v-loading="loading" :data="bomList" border style="width: 100%">
|
||||
<el-table-column prop="bom_no" label="BOM编号" min-width="180" sortable />
|
||||
<el-table-column prop="parent_name" label="父件名称" min-width="150" />
|
||||
<el-table-column prop="parent_spec" label="父件规格" min-width="150" />
|
||||
<el-table-column prop="version" label="版本" width="100" align="center">
|
||||
<el-table-column v-if="hasColumnPermission('bom_no')" prop="bom_no" label="BOM编号" min-width="180" sortable />
|
||||
<el-table-column v-if="hasColumnPermission('parent_name')" prop="parent_name" label="父件名称" min-width="150" />
|
||||
<el-table-column v-if="hasColumnPermission('parent_spec')" prop="parent_spec" label="父件规格" min-width="150" />
|
||||
<el-table-column v-if="hasColumnPermission('version')" prop="version" label="版本" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag>{{ row.version }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<el-table-column v-if="hasColumnPermission('status')" label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.is_enabled ? 'success' : 'danger'">
|
||||
{{ row.is_enabled ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="child_count" label="子件数" width="80" align="center" />
|
||||
<el-table-column label="操作" width="250" align="center" fixed="right">
|
||||
<el-table-column v-if="hasColumnPermission('child_count')" prop="child_count" label="子件数" width="80" align="center" />
|
||||
<el-table-column v-if="userStore.hasPermission('bom_manage:operation')" label="操作" width="250" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button type="success" link @click="handleSaveAs(row)">另存为</el-button>
|
||||
@ -80,7 +80,7 @@
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="是否启用" prop="is_enabled">
|
||||
<el-switch v-model="form.is_enabled" active-text="启用" inactive-text="禁用" />
|
||||
<el-switch v-model="form.is_enabled" active-text="启用" inactive-text="禁用" :disabled="!userStore.hasPermission('bom_manage:operation')" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@ -169,6 +169,7 @@ import { ElMessage, ElMessageBox, FormInstance, FormRules } from 'element-plus'
|
||||
import { Plus, Search } from '@element-plus/icons-vue'
|
||||
import { getBomList, getBomDetail, saveBom, deleteBom } from '@/api/bom'
|
||||
import { getMaterialBaseList } from '@/api/inbound/stock'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
// 类型定义
|
||||
interface BomItem {
|
||||
@ -190,6 +191,7 @@ interface ChildRow {
|
||||
remark: string
|
||||
}
|
||||
|
||||
const userStore = useUserStore()
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const saving = ref(false)
|
||||
@ -199,6 +201,25 @@ const bomList = ref<BomItem[]>([])
|
||||
const materialOptions = ref<MaterialBase[]>([])
|
||||
const searchKeyword = ref('')
|
||||
|
||||
// 列与权限Code的映射关系(数据库中的code)
|
||||
const permissionMap: Record<string, string> = {
|
||||
bom_no: 'bom_manage:bom_no',
|
||||
parent_name: 'bom_manage:parent_name',
|
||||
parent_spec: 'bom_manage:parent_spec',
|
||||
version: 'bom_manage:version',
|
||||
status: 'bom_manage:status',
|
||||
child_count: 'bom_manage:child_count',
|
||||
}
|
||||
|
||||
// 检查列权限
|
||||
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 formRef = ref<FormInstance>()
|
||||
const form = reactive({
|
||||
bom_prefix: '', // 自动生成的父件规格前缀
|
||||
|
||||
Reference in New Issue
Block a user