fix: 彻底解耦出库/借库权限联动 + 路由去硬编码roles + 补API权限保护

根因: 借库选单页面14处硬编码outbound_selection:operation, 导致两模块权限联动

修复:
- borrow/apply: 14处outbound_selection:operation→op_borrow_apply:operation
- stock.py: 拆分_do_get_stock_list裸逻辑, 出库/借库各绑独立权限码
- transactions: 新增/borrow/stock-list端点(@permission(op_borrow_apply))
- transaction.ts: 新增getBorrowStockList前端API函数
- outbound.py: 出库审批4端点改用独立outbound_approval权限码
- transactions.py: 借库审批3端点补@permission(op_borrow_approval)
- purchase.py: 采购管理7端点补@permission(inbound_buy)
- audit.py: 审计日志补@permission(system_audit)
- router: 清除全部6处硬编码roles, 交由动态权限树控制
This commit is contained in:
yueli
2026-07-15 14:33:11 +08:00
parent 9988d1b7eb
commit 0651eb07fa
6 changed files with 62 additions and 43 deletions

View File

@ -174,6 +174,17 @@ export function approveBorrowRequest(id: number, data: { action: 'approve' | 're
})
}
/**
* 借库选单专用库存列表(独立权限 op_borrow_apply
*/
export function getBorrowStockList(params: { page?: number; pageSize?: number; keyword?: string }) {
return request({
url: '/v1/transactions/borrow/stock-list',
method: 'get',
params
})
}
/**
* 执行借库扣减(审批通过后调用)
* @param data approval_id + 扫码选中的物品 + 借用人信息 + 签名

View File

@ -164,8 +164,7 @@ const routes: Array<RouteRecordRaw> = [
component: () => import('@/views/outbound/approval/index.vue'),
meta: {
title: '出库审批',
icon: 'Stamp',
roles: ['SUPER_ADMIN', 'SUPERVISOR']
icon: 'Stamp'
}
}
]
@ -239,8 +238,7 @@ const routes: Array<RouteRecordRaw> = [
component: () => import('@/views/borrow/approval/index.vue'),
meta: {
title: '借库审批',
icon: 'Stamp',
roles: ['SUPER_ADMIN', 'SUPERVISOR']
icon: 'Stamp'
}
}
]
@ -276,9 +274,7 @@ const routes: Array<RouteRecordRaw> = [
redirect: '/system/user-create',
meta: {
title: '系统管理',
icon: 'Setting',
// [修复] 使用大写角色名,匹配后端常量
roles: ['SUPER_ADMIN', 'SUPERVISOR']
icon: 'Setting'
},
children: [
{
@ -287,8 +283,7 @@ const routes: Array<RouteRecordRaw> = [
component: () => import('@/views/system/UserCreate.vue'),
meta: {
title: '账号开通',
icon: 'User',
roles: ['SUPER_ADMIN', 'SUPERVISOR']
icon: 'User'
}
},
// [新增] 权限分配页面,只有超级管理员可进
@ -298,8 +293,7 @@ const routes: Array<RouteRecordRaw> = [
component: () => import('@/views/system/PermissionConfig.vue'),
meta: {
title: '权限分配',
icon: 'Lock',
roles: ['SUPER_ADMIN', 'SUPERVISOR']
icon: 'Lock'
}
},
{
@ -308,8 +302,7 @@ const routes: Array<RouteRecordRaw> = [
component: () => import('@/views/system/AuditLog.vue'),
meta: {
title: '审计日志',
icon: 'Document',
roles: ['SUPER_ADMIN', 'SUPERVISOR']
icon: 'Document'
}
}
]

View File

@ -19,25 +19,25 @@
</template>
<!-- 普通模式 -->
<template v-else>
<el-button v-if="userStore.hasPermission('outbound_selection:operation')" type="warning" plain :disabled="selectedItems.length === 0" @click="isBulkMode = true">
<el-button v-if="userStore.hasPermission('op_borrow_apply:operation')" type="warning" plain :disabled="selectedItems.length === 0" @click="isBulkMode = true">
批量操作
</el-button>
<el-button v-if="userStore.hasPermission('outbound_selection:operation')" type="danger" :disabled="selectedItems.length === 0" @click="clearAll">
<el-button v-if="userStore.hasPermission('op_borrow_apply:operation')" type="danger" :disabled="selectedItems.length === 0" @click="clearAll">
清空列表
</el-button>
<el-divider direction="vertical" />
<el-button v-if="userStore.hasPermission('outbound_selection:operation')" type="primary" :icon="Plus" @click="openManualSelect">
<el-button v-if="userStore.hasPermission('op_borrow_apply:operation')" type="primary" :icon="Plus" @click="openManualSelect">
手动添加库存
</el-button>
<el-button v-if="userStore.hasPermission('outbound_selection:operation')" type="warning" :icon="List" @click="openBomSelect">
<el-button v-if="userStore.hasPermission('op_borrow_apply:operation')" type="warning" :icon="List" @click="openBomSelect">
BOM 套餐添加
</el-button>
</template>
<el-divider direction="vertical" />
<el-button v-if="userStore.hasPermission('outbound_selection:operation')" type="success" :icon="Printer" :disabled="selectedItems.length === 0" @click="handlePreview">
<el-button v-if="userStore.hasPermission('op_borrow_apply:operation')" type="success" :icon="Printer" :disabled="selectedItems.length === 0" @click="handlePreview">
生成预览 & 打印
</el-button>
<el-button v-if="userStore.hasPermission('outbound_selection:operation')" type="primary" :icon="Plus" :disabled="selectedItems.length === 0" @click="openRequestDialog">
<el-button v-if="userStore.hasPermission('op_borrow_apply:operation')" type="primary" :icon="Plus" :disabled="selectedItems.length === 0" @click="openRequestDialog">
提交借库申请
</el-button>
</div>
@ -98,7 +98,7 @@
size="small"
style="width: 100%"
controls-position="right"
:disabled="!userStore.hasPermission('outbound_selection:operation')"
:disabled="!userStore.hasPermission('op_borrow_apply:operation')"
@change="(val) => handleMainQuantityChange(val, row)"
/>
</template>
@ -106,7 +106,7 @@
<el-table-column label="操作" width="80" align="center" fixed="right">
<template #default="{ $index }">
<el-button v-if="!isBulkMode && userStore.hasPermission('outbound_selection:operation')" type="danger" link @click="removeRow($index)">移除</el-button>
<el-button v-if="!isBulkMode && userStore.hasPermission('op_borrow_apply:operation')" type="danger" link @click="removeRow($index)">移除</el-button>
</template>
</el-table-column>
</el-table>
@ -162,7 +162,7 @@
size="small"
style="width: 100%"
placeholder="0"
:disabled="!userStore.hasPermission('outbound_selection:operation')"
:disabled="!userStore.hasPermission('op_borrow_apply:operation')"
@click.stop
@change="(val) => handleManualQuantityChange(val, row)"
/>
@ -185,7 +185,7 @@
已勾选 {{ tempSelection.length }}
</span>
<el-button @click="manualDialogVisible = false">取消</el-button>
<el-button v-if="userStore.hasPermission('outbound_selection:operation')" type="primary" @click="confirmManualAdd">确认添加</el-button>
<el-button v-if="userStore.hasPermission('op_borrow_apply:operation')" type="primary" @click="confirmManualAdd">确认添加</el-button>
</template>
</el-dialog>
@ -202,7 +202,7 @@
/>
</el-form-item>
<el-form-item label="借库套数">
<el-input-number v-model="bomSets" :min="1" label="套" style="width: 200px;" :disabled="!userStore.hasPermission('outbound_selection:operation')" />
<el-input-number v-model="bomSets" :min="1" label="套" style="width: 200px;" :disabled="!userStore.hasPermission('op_borrow_apply:operation')" />
<el-tag v-if="selectedBomNo && maxBuildableSets >= 0" type="success" style="margin-left: 16px;">
当前库存最多可成套借库: {{ maxBuildableSets }}
</el-tag>
@ -248,7 +248,7 @@
<template #footer>
<el-button @click="bomSelectVisible = false">取消</el-button>
<el-button
v-if="userStore.hasPermission('outbound_selection:operation')"
v-if="userStore.hasPermission('op_borrow_apply:operation')"
type="primary"
@click="confirmBomAdd"
>
@ -288,11 +288,11 @@
<span class="dialog-footer">
<el-button @click="previewVisible = false">取消</el-button>
<el-button v-if="userStore.hasPermission('outbound_selection:operation')" type="warning" :icon="Download" :loading="exportLoading" @click="confirmExport">
<el-button v-if="userStore.hasPermission('op_borrow_apply:operation')" type="warning" :icon="Download" :loading="exportLoading" @click="confirmExport">
导出 Excel
</el-button>
<el-button v-if="userStore.hasPermission('outbound_selection:operation')" type="primary" :icon="Printer" :loading="printLoading" @click="confirmPrint">
<el-button v-if="userStore.hasPermission('op_borrow_apply:operation')" type="primary" :icon="Printer" :loading="printLoading" @click="confirmPrint">
确认打印 (A4)
</el-button>
</span>
@ -440,9 +440,9 @@
import { ref, computed, watch } from 'vue'
import { Printer, Search, Plus, Download, List } from '@element-plus/icons-vue'
import { ElMessage, ElTable, ElMessageBox } from 'element-plus'
import { getStockList, printSelectionList } from '@/api/inbound/stock'
import { printSelectionList } from '@/api/inbound/stock'
import { useUserStore } from '@/stores/user'
import { submitBorrowRequest } from '@/api/transaction'
import { submitBorrowRequest, getBorrowStockList } from '@/api/transaction'
import { getApproversList } from '@/api/auth'
import { getBomList, getBomDetail, getBomWithStock } from '@/api/bom'
@ -583,7 +583,7 @@ const getTypeTag = (type: string) => {
const loadStockList = async () => {
stockLoading.value = true
try {
const res: any = await getStockList({
const res: any = await getBorrowStockList({
page: stockPage.value,
pageSize: stockPageSize.value,
keyword: searchKeyword.value.trim(),
@ -610,7 +610,7 @@ const loadAllStockForBom = async () => {
let page = 1
const pageSize = 200
while (true) {
const res: any = await getStockList({
const res: any = await getBorrowStockList({
page,
pageSize,
is_aggregated: true