Compare commits
16 Commits
cac8a360ec
...
76c7e3d3dc
| Author | SHA1 | Date | |
|---|---|---|---|
| 76c7e3d3dc | |||
| 29de22b575 | |||
| 0e6294890c | |||
| c45e7b5a52 | |||
| 0074d96163 | |||
| 96eb56766e | |||
| c9cf4e0901 | |||
| de2d13fa68 | |||
| 266a0e011f | |||
| efe04c5d42 | |||
| 9c3fcbca4c | |||
| f3c946d584 | |||
| 05832e913b | |||
| 686186d7d6 | |||
| 8b6539b976 | |||
| 1cc7d51e03 |
70
db_migrations/fix_missing_operation_permissions.sql
Normal file
70
db_migrations/fix_missing_operation_permissions.sql
Normal file
@ -0,0 +1,70 @@
|
||||
-- ============================================================
|
||||
-- 补齐缺失的 :operation 操作权限 & 字段权限
|
||||
-- 创建日期: 2026-07-17
|
||||
-- 说明: 前端按钮使用 hasPermission('xxx:operation') 控制显示
|
||||
-- 但 sys_element 中缺少对应的元素权限码,导致:
|
||||
-- 1. 审批按钮可见但 can_approve 拒绝(出库/借库审批)
|
||||
-- 2. 用户管理操作按钮不显示
|
||||
-- 3. 权限分配按钮不显示
|
||||
-- 4. 密码字段被隐藏
|
||||
-- ============================================================
|
||||
BEGIN;
|
||||
|
||||
-- ===== 1. 出库审批操作权限 =====
|
||||
INSERT INTO sys_element (menu_code, name, code, element_type)
|
||||
SELECT 'outbound_approval', '审批操作', 'outbound_approval:operation', 'column'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_element WHERE code = 'outbound_approval:operation');
|
||||
|
||||
INSERT INTO sys_role_permission (role_code, target_code, type)
|
||||
SELECT 'SUPERVISOR', 'outbound_approval:operation', 'element'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_role_permission WHERE role_code = 'SUPERVISOR' AND target_code = 'outbound_approval:operation');
|
||||
|
||||
INSERT INTO sys_role_permission (role_code, target_code, type)
|
||||
SELECT 'SUPER_ADMIN', 'outbound_approval:operation', 'element'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_role_permission WHERE role_code = 'SUPER_ADMIN' AND target_code = 'outbound_approval:operation');
|
||||
|
||||
-- ===== 2. 借库审批操作权限 =====
|
||||
INSERT INTO sys_element (menu_code, name, code, element_type)
|
||||
SELECT 'op_borrow_approval', '审批操作', 'op_borrow_approval:operation', 'column'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_element WHERE code = 'op_borrow_approval:operation');
|
||||
|
||||
INSERT INTO sys_role_permission (role_code, target_code, type)
|
||||
SELECT role_code, 'op_borrow_approval:operation', 'element'
|
||||
FROM (SELECT DISTINCT role_code FROM sys_role_permission WHERE target_code = 'op_borrow_approval') t
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_role_permission WHERE role_code = t.role_code AND target_code = 'op_borrow_approval:operation');
|
||||
|
||||
-- ===== 3. 用户管理操作权限 =====
|
||||
INSERT INTO sys_element (menu_code, name, code, element_type)
|
||||
SELECT 'system_user', '用户操作', 'system_user:operation', 'column'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_element WHERE code = 'system_user:operation');
|
||||
|
||||
INSERT INTO sys_role_permission (role_code, target_code, type)
|
||||
SELECT 'SUPERVISOR', 'system_user:operation', 'element'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_role_permission WHERE role_code = 'SUPERVISOR' AND target_code = 'system_user:operation');
|
||||
|
||||
-- ===== 4. 权限分配操作权限 =====
|
||||
INSERT INTO sys_role_permission (role_code, target_code, type)
|
||||
SELECT 'SUPERVISOR', 'system_permission:operation', 'element'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_role_permission WHERE role_code = 'SUPERVISOR' AND target_code = 'system_permission:operation');
|
||||
|
||||
-- ===== 5. 密码管理字段权限 =====
|
||||
INSERT INTO sys_element (menu_code, name, code, element_type)
|
||||
SELECT 'system_user', '密码管理', 'system_user:password', 'column'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_element WHERE code = 'system_user:password');
|
||||
|
||||
INSERT INTO sys_role_permission (role_code, target_code, type)
|
||||
SELECT 'SUPER_ADMIN', 'system_user:password', 'element'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_role_permission WHERE role_code = 'SUPER_ADMIN' AND target_code = 'system_user:password');
|
||||
|
||||
INSERT INTO sys_role_permission (role_code, target_code, type)
|
||||
SELECT 'SUPERVISOR', 'system_user:password', 'element'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_role_permission WHERE role_code = 'SUPERVISOR' AND target_code = 'system_user:password');
|
||||
|
||||
-- 验证
|
||||
SELECT 'outbound_approval:operation' AS perm, count(*) FROM sys_role_permission WHERE target_code = 'outbound_approval:operation'
|
||||
UNION ALL SELECT 'op_borrow_approval:operation', count(*) FROM sys_role_permission WHERE target_code = 'op_borrow_approval:operation'
|
||||
UNION ALL SELECT 'system_user:operation', count(*) FROM sys_role_permission WHERE target_code = 'system_user:operation'
|
||||
UNION ALL SELECT 'system_permission:operation', count(*) FROM sys_role_permission WHERE target_code = 'system_permission:operation'
|
||||
UNION ALL SELECT 'system_user:password', count(*) FROM sys_role_permission WHERE target_code = 'system_user:password';
|
||||
|
||||
COMMIT;
|
||||
@ -129,10 +129,8 @@ def submit():
|
||||
perm_code = field_to_perm.get(field)
|
||||
if perm_code and perm_code not in user_permissions: data.pop(field, None)
|
||||
new_stock = ProductInboundService.handle_inbound(data)
|
||||
# ★ Fail-Closed: 入库成功响应剥离成本/售价字段
|
||||
resp = new_stock.to_dict()
|
||||
for k in ('raw_material_cost', 'manual_cost', 'unit_total_cost', 'sale_price'):
|
||||
resp.pop(k, None)
|
||||
from app.utils.field_permissions import apply_strict_rbac
|
||||
resp = apply_strict_rbac(new_stock.to_dict(), 'StockProduct', user_permissions)
|
||||
return jsonify({"code": 200, "msg": "入库成功", "data": resp})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
|
||||
@ -123,10 +123,8 @@ def submit():
|
||||
if field in data and perm_code not in user_permissions:
|
||||
data.pop(field, None)
|
||||
new_stock = SemiInboundService.handle_inbound(data)
|
||||
# ★ Fail-Closed: 入库成功响应剥离成本字段
|
||||
resp = new_stock.to_dict()
|
||||
for k in ('raw_material_cost', 'manual_cost', 'unit_total_cost', 'total_price'):
|
||||
resp.pop(k, None)
|
||||
from app.utils.field_permissions import apply_strict_rbac
|
||||
resp = apply_strict_rbac(new_stock.to_dict(), 'StockSemi', user_permissions)
|
||||
return jsonify({"code": 200, "msg": "入库成功", "data": resp})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
|
||||
@ -79,6 +79,14 @@ def assign_perms():
|
||||
role_code = data.get('role_code')
|
||||
permissions = data.get('permissions', [])
|
||||
|
||||
# ★ 防自锁:操作者修改自己角色时,强制保留权限管理的关键权限
|
||||
claims = get_jwt()
|
||||
operator_role = claims.get('role', '')
|
||||
if operator_role and operator_role.upper() == role_code.upper():
|
||||
for required in ('system_permission', 'system_permission:operation'):
|
||||
if required not in permissions:
|
||||
permissions.append(required)
|
||||
|
||||
company_name = _get_operator_company()
|
||||
PermissionService.assign_permissions(role_code, permissions, company_name=company_name)
|
||||
return jsonify({'code': 200, 'msg': '保存成功'}), 200
|
||||
|
||||
@ -296,6 +296,16 @@ class BorrowApprovalService:
|
||||
if user_role and user_role.upper() == 'SUPER_ADMIN':
|
||||
return True
|
||||
|
||||
# 拥有 op_borrow_approval:operation 权限的用户可以审批任意借库单
|
||||
# (前端按钮以此权限控制显示,后端也必须对齐)
|
||||
from app.models.system import SysRolePermission
|
||||
has_op = SysRolePermission.query.filter(
|
||||
SysRolePermission.role_code == user_role,
|
||||
SysRolePermission.target_code.in_(['op_borrow_approval:operation', 'op_borrow_approval:*'])
|
||||
).first() is not None
|
||||
if has_op:
|
||||
return True
|
||||
|
||||
for approver in approvers:
|
||||
approver_type = approver.get('type', '')
|
||||
approver_value = approver.get('value', '')
|
||||
|
||||
@ -846,6 +846,16 @@ class OutboundApprovalService:
|
||||
if user_role and user_role.upper() == 'SUPER_ADMIN':
|
||||
return True
|
||||
|
||||
# 拥有 outbound_approval:operation 权限的用户可以审批任意出库单
|
||||
# (前端按钮以此权限控制显示,后端也必须对齐)
|
||||
from app.models.system import SysRolePermission
|
||||
has_op = SysRolePermission.query.filter(
|
||||
SysRolePermission.role_code == user_role,
|
||||
SysRolePermission.target_code.in_(['outbound_approval:operation', 'outbound_approval:*'])
|
||||
).first() is not None
|
||||
if has_op:
|
||||
return True
|
||||
|
||||
for approver in approvers:
|
||||
approver_type = approver.get('type', '')
|
||||
approver_value = approver.get('value', '')
|
||||
|
||||
@ -130,13 +130,20 @@ class PermissionService:
|
||||
|
||||
session = db.session
|
||||
try:
|
||||
# 1. 删除该角色+该公司的旧权限(只删同公司的,不影响全局模板和其他公司)
|
||||
# 1. 删除该角色的旧权限
|
||||
# 删除逻辑需与 get_user_permissions 的查询逻辑对齐:
|
||||
# - company_name 为空 → 删 company_name IS NULL OR ''(超管/全局模板)
|
||||
# - company_name 有值 → 删 公司定制 + 全局模板(确保能完整替换)
|
||||
if company_name:
|
||||
old_perms = SysRolePermission.query.filter_by(
|
||||
role_code=role_code, company_name=company_name
|
||||
old_perms = SysRolePermission.query.filter(
|
||||
SysRolePermission.role_code == role_code,
|
||||
or_(
|
||||
SysRolePermission.company_name == company_name,
|
||||
SysRolePermission.company_name.is_(None),
|
||||
SysRolePermission.company_name == '',
|
||||
)
|
||||
).all()
|
||||
else:
|
||||
# 公司为空时兼容旧逻辑:删除 company_name 为 NULL 的旧权限
|
||||
old_perms = SysRolePermission.query.filter(
|
||||
SysRolePermission.role_code == role_code,
|
||||
or_(
|
||||
@ -153,16 +160,21 @@ class PermissionService:
|
||||
unique_codes = set(permissions)
|
||||
all_menu_codes = {res[0] for res in session.query(SysMenu.code).all()}
|
||||
|
||||
# 全局权限码 — 不按公司隔离,始终写入 company_name=''
|
||||
GLOBAL_CODES = {'crossDomain', 'global:cross_company_op'}
|
||||
|
||||
new_records = []
|
||||
for code in unique_codes:
|
||||
if not code: continue
|
||||
p_type = 'menu' if code in all_menu_codes else 'element'
|
||||
# 全局权限码强制 company_name='',确保跨公司生效
|
||||
target_company = '' if code in GLOBAL_CODES else company_name
|
||||
|
||||
new_records.append(SysRolePermission(
|
||||
role_code=role_code,
|
||||
target_code=code,
|
||||
type=p_type,
|
||||
company_name=company_name # ★ 存储公司标识
|
||||
company_name=target_company
|
||||
))
|
||||
|
||||
if new_records:
|
||||
|
||||
@ -239,7 +239,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>
|
||||
当前版本:V3.60
|
||||
当前版本:V3.61
|
||||
</span>
|
||||
</footer>
|
||||
|
||||
|
||||
@ -4,7 +4,8 @@ import request from '@/utils/request'
|
||||
export function getAllPermissionTree() {
|
||||
return request({
|
||||
url: '/v1/permissions/tree',
|
||||
method: 'get'
|
||||
method: 'get',
|
||||
params: { _t: Date.now() }
|
||||
})
|
||||
}
|
||||
|
||||
@ -12,7 +13,9 @@ export function getAllPermissionTree() {
|
||||
export function getRolePermissions(roleCode: string) {
|
||||
return request({
|
||||
url: '/v1/permissions/role/' + roleCode,
|
||||
method: 'get'
|
||||
method: 'get',
|
||||
// 加时间戳防浏览器缓存,确保权限修改后立即反映
|
||||
params: { _t: Date.now() }
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -491,7 +491,7 @@
|
||||
<el-autocomplete v-model="form.production_manager" :fetch-suggestions="querySearchManager" placeholder="输入或选择负责人" style="width: 100%" clearable :trigger-on-focus="true" @select="handleManagerSelect"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-col :span="8" v-if="hasFormFieldPermission('sale_price')">
|
||||
<el-form-item label="产品定价">
|
||||
<el-input-number v-model="form.sale_price" :precision="2" :controls="false" style="width:100%" placeholder="请输入">
|
||||
<template #prefix>¥</template>
|
||||
@ -507,18 +507,18 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="8">
|
||||
<el-row :gutter="24" v-if="hasFormFieldPermission('raw_material_cost') || hasFormFieldPermission('unit_total_cost') || hasFormFieldPermission('total_price')">
|
||||
<el-col :span="8" v-if="hasFormFieldPermission('raw_material_cost')">
|
||||
<el-form-item label="原料成本">
|
||||
<el-input-number v-model="form.raw_material_cost" :precision="2" :controls="false" style="width:100%" placeholder="请输入"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-col :span="8" v-if="hasFormFieldPermission('unit_total_cost')">
|
||||
<el-form-item label="单件成本">
|
||||
<el-input-number v-model="form.unit_total_cost" :precision="2" :controls="false" style="width:100%" placeholder="请输入"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-col :span="8" v-if="hasFormFieldPermission('total_price')">
|
||||
<el-form-item label="总成本">
|
||||
<el-input-number v-model="form.total_price" :precision="2" :controls="false" style="width:100%" placeholder="自动计算" disabled/>
|
||||
</el-form-item>
|
||||
@ -1007,6 +1007,8 @@ const hasFormFieldPermission = (fieldName: string) => {
|
||||
production_time_range: 'inbound_product:production_start_time',
|
||||
raw_material_cost: 'inbound_product:raw_material_cost',
|
||||
manual_cost: 'inbound_product:manual_cost',
|
||||
unit_total_cost: 'inbound_product:unit_total_cost',
|
||||
total_price: 'inbound_product:total_price',
|
||||
sale_price: 'inbound_product:sale_price',
|
||||
quality_report_link: 'inbound_product:quality_report_link',
|
||||
inspection_report_link: 'inbound_product:inspection_report_link',
|
||||
@ -1416,6 +1418,14 @@ const submitForm = async () => {
|
||||
production_end_time: form.production_time_range?.[1]
|
||||
}
|
||||
delete payload.production_time_range
|
||||
// Update 模式:无成本/售价权限则剥离敏感字段,防止库管修改已入库的数据
|
||||
// Create 模式:保留成本字段,确保 BOM 自动计算的值能正确提交
|
||||
if (dialogStatus.value === 'update') {
|
||||
if (!hasFormFieldPermission('raw_material_cost')) delete payload.raw_material_cost
|
||||
if (!hasFormFieldPermission('unit_total_cost')) delete payload.unit_total_cost
|
||||
if (!hasFormFieldPermission('total_price')) delete payload.total_price
|
||||
if (!hasFormFieldPermission('sale_price')) delete payload.sale_price
|
||||
}
|
||||
try {
|
||||
if(dialogStatus.value === 'create') {
|
||||
const res: any = await createProductInbound(payload)
|
||||
|
||||
@ -557,19 +557,19 @@
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<div class="divider-text">成本核算</div>
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="8">
|
||||
<div class="divider-text" v-if="hasFormFieldPermission('raw_material_cost') || hasFormFieldPermission('unit_total_cost') || hasFormFieldPermission('total_price')">成本核算</div>
|
||||
<el-row :gutter="24" v-if="hasFormFieldPermission('raw_material_cost') || hasFormFieldPermission('unit_total_cost') || hasFormFieldPermission('total_price')">
|
||||
<el-col :span="8" v-if="hasFormFieldPermission('raw_material_cost')">
|
||||
<el-form-item label="估算成本">
|
||||
<el-input-number v-model="form.raw_material_cost" :precision="2" :controls="false" style="width:100%" placeholder="请输入"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-col :span="8" v-if="hasFormFieldPermission('unit_total_cost')">
|
||||
<el-form-item label="基于BOM成本">
|
||||
<el-input-number v-model="form.unit_total_cost" :precision="2" :controls="false" style="width:100%" placeholder="请输入"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-col :span="8" v-if="hasFormFieldPermission('total_price')">
|
||||
<el-form-item label="总成本">
|
||||
<el-input-number v-model="form.total_price" :precision="2" :controls="false" style="width:100%" placeholder="自动计算" disabled/>
|
||||
</el-form-item>
|
||||
@ -1164,6 +1164,7 @@ const hasFormFieldPermission = (fieldName: string) => {
|
||||
raw_material_cost: 'inbound_semi:raw_material_cost',
|
||||
manual_cost: 'inbound_semi:manual_cost',
|
||||
unit_total_cost: 'inbound_semi:unit_total_cost',
|
||||
total_price: 'inbound_semi:total_price',
|
||||
production_manager: 'inbound_semi:production_manager',
|
||||
production_time_range: 'inbound_semi:production_start_time',
|
||||
arrival_photo: 'inbound_semi:arrival_photo',
|
||||
@ -1506,6 +1507,13 @@ const submitForm = async () => {
|
||||
production_end_time: form.production_time_range?.[1] || null
|
||||
}
|
||||
delete payload.production_time_range
|
||||
// Update 模式:无成本权限则剥离成本字段,防止库管修改已入库的成本数据
|
||||
// Create 模式:保留成本字段,确保 BOM 自动计算的值能正确提交
|
||||
if (dialogStatus.value === 'update') {
|
||||
if (!hasFormFieldPermission('raw_material_cost')) delete payload.raw_material_cost
|
||||
if (!hasFormFieldPermission('unit_total_cost')) delete payload.unit_total_cost
|
||||
if (!hasFormFieldPermission('total_price')) delete payload.total_price
|
||||
}
|
||||
try {
|
||||
if (dialogStatus.value === 'create') {
|
||||
const res: any = await createSemiInbound(payload)
|
||||
|
||||
@ -256,7 +256,18 @@ const fetchTree = async () => {
|
||||
]
|
||||
};
|
||||
// 将虚拟节点放在最前面,然后交给 transformData 处理
|
||||
const rawData = [globalNode, ...(res.data || [])];
|
||||
// ★ 从真实树中移除 crossDomain(已由虚拟节点 global_privileges 统一管理,避免重复)
|
||||
const stripCrossDomain = (nodes: any[]) => {
|
||||
nodes.forEach(node => {
|
||||
if (node.children) {
|
||||
node.children = node.children.filter((c: any) => c.code !== 'crossDomain')
|
||||
stripCrossDomain(node.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
const realData = res.data || []
|
||||
stripCrossDomain(realData)
|
||||
const rawData = [globalNode, ...realData];
|
||||
|
||||
rawTreeData.value = rawData;
|
||||
tableData.value = transformData(rawData);
|
||||
@ -499,6 +510,16 @@ const handleSave = async () => {
|
||||
})
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('权限保存成功!')
|
||||
// 立即重新拉取权限刷新 UI(带 cache-busting,确保拿到最新数据)
|
||||
if (currentRole.value) {
|
||||
try {
|
||||
const refreshRes: any = await getRolePermissions(currentRole.value)
|
||||
if (refreshRes.code === 200) {
|
||||
const perms = new Set([...(refreshRes.data.menus || []), ...(refreshRes.data.elements || [])])
|
||||
setRowStatus(tableData.value, perms)
|
||||
}
|
||||
} catch (e) { /* 静默失败 */ }
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(res.msg || '保存失败')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user