feat: enforce field-level permissions for buy and service modules

Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
This commit is contained in:
dxc
2026-02-27 15:03:44 +08:00
parent 5bc3dab31c
commit afcf90a859
4 changed files with 228 additions and 14 deletions

View File

@ -145,6 +145,51 @@ def submit():
if not data: if not data:
return jsonify({"code": 400, "msg": "No data"}), 400 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) new_stock = BuyInboundService.handle_inbound(data)
return jsonify({ return jsonify({
@ -165,6 +210,50 @@ def submit():
def update_buy(id): def update_buy(id):
try: try:
data = request.get_json() 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) BuyInboundService.update_inbound(id, data)
return jsonify({"code": 200, "msg": "更新成功"}) return jsonify({"code": 200, "msg": "更新成功"})
except Exception as e: except Exception as e:

View File

@ -118,6 +118,31 @@ def create_service():
if not data: if not data:
return jsonify({'code': 400, 'msg': '请求数据为空'}), 400 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'): if not data.get('base_id'):
return jsonify({'code': 400, 'msg': '请选择基础物料'}), 400 return jsonify({'code': 400, 'msg': '请选择基础物料'}), 400
@ -169,6 +194,31 @@ def update_service(service_id):
if not data: if not data:
return jsonify({'code': 400, 'msg': '请求数据为空'}), 400 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 = { allowed_fields = {
'sale_price', 'provider_name', 'description', 'sale_price', 'provider_name', 'description',

View File

@ -272,12 +272,12 @@
<div class="read-only-grid"> <div class="read-only-grid">
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="8"><el-form-item label="所属公司"><el-input v-model="form.company_name" readonly class="is-text-view"/></el-form-item></el-col> <el-col :span="8"><el-form-item label="所属公司" v-if="hasFormFieldPermission('company_name')"><el-input v-model="form.company_name" readonly class="is-text-view"/></el-form-item></el-col>
<el-col :span="8"><el-form-item label="名称"><el-input v-model="form.material_name" readonly class="is-text-view"/></el-form-item></el-col> <el-col :span="8"><el-form-item label="名称" v-if="hasFormFieldPermission('material_name')"><el-input v-model="form.material_name" readonly class="is-text-view"/></el-form-item></el-col>
<el-col :span="8"><el-form-item label="类型"><el-input v-model="form.material_type" readonly class="is-text-view"/></el-form-item></el-col> <el-col :span="8"><el-form-item label="类型" v-if="hasFormFieldPermission('material_type')"><el-input v-model="form.material_type" readonly class="is-text-view"/></el-form-item></el-col>
<el-col :span="8"><el-form-item label="类别"><el-input v-model="form.category" readonly class="is-text-view"/></el-form-item></el-col> <el-col :span="8"><el-form-item label="类别" v-if="hasFormFieldPermission('category')"><el-input v-model="form.category" readonly class="is-text-view"/></el-form-item></el-col>
<el-col :span="8"><el-form-item label="规格型号"><el-input v-model="form.spec_model" readonly class="is-text-view"/></el-form-item></el-col> <el-col :span="8"><el-form-item label="规格型号" v-if="hasFormFieldPermission('spec_model')"><el-input v-model="form.spec_model" readonly class="is-text-view"/></el-form-item></el-col>
<el-col :span="8"><el-form-item label="单位"><el-input v-model="form.unit" readonly class="is-text-view"/></el-form-item></el-col> <el-col :span="8"><el-form-item label="单位" v-if="hasFormFieldPermission('unit')"><el-input v-model="form.unit" readonly class="is-text-view"/></el-form-item></el-col>
</el-row> </el-row>
</div> </div>
</div> </div>
@ -611,6 +611,55 @@ const vLoadmore = {
} }
} }
// ------------------------------------
// 表单字段权限检查
// ------------------------------------
const hasFormFieldPermission = (fieldName: string) => {
// 超级管理员直接返回true
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
return true
}
// 根据字段名映射到权限码
const map: Record<string, string> = {
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)
}
// ------------------------------------ // ------------------------------------
// 状态与变量 // 状态与变量
// ------------------------------------ // ------------------------------------

View File

@ -134,11 +134,11 @@
<div class="read-only-grid" v-if="form.base_id"> <div class="read-only-grid" v-if="form.base_id">
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="8"><el-form-item label="名称"><el-input v-model="form.material_name" disabled class="is-text-view"/></el-form-item></el-col> <el-col :span="8"><el-form-item label="名称" v-if="hasFormFieldPermission('material_name')"><el-input v-model="form.material_name" disabled class="is-text-view"/></el-form-item></el-col>
<el-col :span="8"><el-form-item label="类型"><el-input v-model="form.material_type" disabled class="is-text-view"/></el-form-item></el-col> <el-col :span="8"><el-form-item label="类型" v-if="hasFormFieldPermission('material_type')"><el-input v-model="form.material_type" disabled class="is-text-view"/></el-form-item></el-col>
<el-col :span="8"><el-form-item label="类别"><el-input v-model="form.category" disabled class="is-text-view"/></el-form-item></el-col> <el-col :span="8"><el-form-item label="类别" v-if="hasFormFieldPermission('category')"><el-input v-model="form.category" disabled class="is-text-view"/></el-form-item></el-col>
<el-col :span="8"><el-form-item label="规格型号"><el-input v-model="form.spec_model" disabled class="is-text-view"/></el-form-item></el-col> <el-col :span="8"><el-form-item label="规格型号" v-if="hasFormFieldPermission('spec_model')"><el-input v-model="form.spec_model" disabled class="is-text-view"/></el-form-item></el-col>
<el-col :span="8"><el-form-item label="单位"><el-input v-model="form.unit" disabled class="is-text-view"/></el-form-item></el-col> <el-col :span="8"><el-form-item label="单位" v-if="hasFormFieldPermission('unit')"><el-input v-model="form.unit" disabled class="is-text-view"/></el-form-item></el-col>
</el-row> </el-row>
</div> </div>
</div> </div>
@ -150,7 +150,7 @@
<span>2. 服务详情</span> <span>2. 服务详情</span>
</div> </div>
<div class="card-content"> <div class="card-content">
<el-form-item label="售价" prop="sale_price"> <el-form-item label="售价" prop="sale_price" v-if="hasFormFieldPermission('sale_price')">
<el-input-number <el-input-number
v-model="form.sale_price" v-model="form.sale_price"
placeholder="请输入售价" placeholder="请输入售价"
@ -160,7 +160,7 @@
style="width: 100%;" style="width: 100%;"
/> />
</el-form-item> </el-form-item>
<el-form-item label="服务商" prop="provider_name"> <el-form-item label="服务商" prop="provider_name" v-if="hasFormFieldPermission('provider_name')">
<el-autocomplete <el-autocomplete
v-model="form.provider_name" v-model="form.provider_name"
:fetch-suggestions="querySearchProvider" :fetch-suggestions="querySearchProvider"
@ -171,7 +171,7 @@
@select="handleProviderSelect" @select="handleProviderSelect"
/> />
</el-form-item> </el-form-item>
<el-form-item label="简介" prop="description"> <el-form-item label="简介" prop="description" v-if="hasFormFieldPermission('description')">
<el-input <el-input
v-model="form.description" v-model="form.description"
type="textarea" type="textarea"
@ -234,6 +234,32 @@ const hasColumnPermission = (prop: string) => {
return code ? userStore.hasPermission(code) : false return code ? userStore.hasPermission(code) : false
} }
// 表单字段权限检查
const hasFormFieldPermission = (fieldName: string) => {
// 超级管理员直接返回true
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
return true
}
// 根据字段名映射到权限码
const map: Record<string, string> = {
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<ServiceItem[]>([]) const tableData = ref<ServiceItem[]>([])
const loading = ref(false) const loading = ref(false)