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

@ -209,15 +209,9 @@ def get_all_stock():
# ============================================================================== # ==============================================================================
# 分页库存查询接口(服务端分页,出库/盘点/借用模块共用) # 分页库存查询接口(服务端分页,出库/盘点/借用模块共用)
# ============================================================================== # ==============================================================================
@bp.route('/list', methods=['GET']) def _do_get_stock_list():
@jwt_required()
def get_stock_list():
""" """
分页获取库存列表(stock_quantity > 0) 分页获取库存列表(stock_quantity > 0) — 裸逻辑,供各模块复用
参数:
page - 页码(默认 1)
pageSize - 每页条数(默认 20)
keyword - 搜索关键字(模糊匹配名称/规格/SKU)
""" """
try: try:
page = request.args.get('page', 1, type=int) page = request.args.get('page', 1, type=int)
@ -352,6 +346,14 @@ def get_stock_list():
return jsonify({'msg': f'获取库存列表失败: {str(e)}'}), 500 return jsonify({'msg': f'获取库存列表失败: {str(e)}'}), 500
@bp.route('/list', methods=['GET'])
@jwt_required()
@permission_required('outbound_selection')
def get_stock_list():
"""出库选单专用库存列表"""
return _do_get_stock_list()
# --- 草稿箱接口 --- # --- 草稿箱接口 ---
@bp.route('/draft/list', methods=['GET']) @bp.route('/draft/list', methods=['GET'])

View File

@ -234,7 +234,7 @@ def get_current_user_info():
# -------------------------------------------------------- # --------------------------------------------------------
@outbound_bp.route('/request', methods=['POST']) @outbound_bp.route('/request', methods=['POST'])
@jwt_required() @jwt_required()
@permission_required('outbound_list') @permission_required('outbound_approval')
def create_outbound_request(): def create_outbound_request():
""" """
创建出库审批单(申请阶段,用户只需提交宏观物料信息,无需关联具体库存记录) 创建出库审批单(申请阶段,用户只需提交宏观物料信息,无需关联具体库存记录)
@ -323,7 +323,7 @@ def create_outbound_request():
# -------------------------------------------------------- # --------------------------------------------------------
@outbound_bp.route('/request/<int:request_id>/approve', methods=['PATCH']) @outbound_bp.route('/request/<int:request_id>/approve', methods=['PATCH'])
@jwt_required() @jwt_required()
@permission_required('outbound_list') @permission_required('outbound_approval')
def approve_outbound_request(request_id): def approve_outbound_request(request_id):
""" """
审批出库申请 审批出库申请
@ -377,7 +377,7 @@ def approve_outbound_request(request_id):
# -------------------------------------------------------- # --------------------------------------------------------
@outbound_bp.route('/request', methods=['GET']) @outbound_bp.route('/request', methods=['GET'])
@jwt_required() @jwt_required()
@permission_required('outbound_list') @permission_required('outbound_approval')
def get_outbound_request_list(): def get_outbound_request_list():
""" """
获取出库审批单列表 获取出库审批单列表
@ -424,7 +424,7 @@ def get_outbound_request_list():
# -------------------------------------------------------- # --------------------------------------------------------
@outbound_bp.route('/request/<int:request_id>', methods=['GET']) @outbound_bp.route('/request/<int:request_id>', methods=['GET'])
@jwt_required() @jwt_required()
@permission_required('outbound_list') @permission_required('outbound_approval')
def get_outbound_request_detail(request_id): def get_outbound_request_detail(request_id):
"""获取出库审批单详情""" """获取出库审批单详情"""
try: try:

View File

@ -155,6 +155,7 @@ def get_records():
# --- 提交借库申请 --- # --- 提交借库申请 ---
@trans_bp.route('/borrow/request', methods=['POST']) @trans_bp.route('/borrow/request', methods=['POST'])
@jwt_required() @jwt_required()
@permission_required('op_borrow_approval')
def submit_borrow_request(): def submit_borrow_request():
""" """
提交借库申请(仅存储意向,不扣库存) 提交借库申请(仅存储意向,不扣库存)
@ -216,6 +217,7 @@ def submit_borrow_request():
# --- 审批借库申请 --- # --- 审批借库申请 ---
@trans_bp.route('/borrow/request/<int:request_id>/approve', methods=['PATCH']) @trans_bp.route('/borrow/request/<int:request_id>/approve', methods=['PATCH'])
@jwt_required() @jwt_required()
@permission_required('op_borrow_approval')
def approve_borrow_request(request_id): def approve_borrow_request(request_id):
""" """
审批借库申请 审批借库申请
@ -257,6 +259,7 @@ def approve_borrow_request(request_id):
# --- 获取借库审批单列表 --- # --- 获取借库审批单列表 ---
@trans_bp.route('/borrow/request', methods=['GET']) @trans_bp.route('/borrow/request', methods=['GET'])
@jwt_required() @jwt_required()
@permission_required('op_borrow_approval')
def get_borrow_request_list(): def get_borrow_request_list():
""" """
获取借库审批单列表 获取借库审批单列表
@ -282,6 +285,16 @@ def get_borrow_request_list():
return jsonify({'code': 500, 'msg': str(e)}), 500 return jsonify({'code': 500, 'msg': str(e)}), 500
# --- 借库选单:库存查询(独立权限)---
@trans_bp.route('/borrow/stock-list', methods=['GET'])
@jwt_required()
@permission_required('op_borrow_apply')
def get_borrow_stock_list():
"""借库选单专用库存列表,与出库选单共享底层逻辑"""
from app.api.v1.inbound.stock import _do_get_stock_list
return _do_get_stock_list()
# --- 执行借库扣减(审批通过后调用)--- # --- 执行借库扣减(审批通过后调用)---
@trans_bp.route('/borrow/dispatch', methods=['POST']) @trans_bp.route('/borrow/dispatch', methods=['POST'])
@jwt_required() @jwt_required()

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 + 扫码选中的物品 + 借用人信息 + 签名 * @param data approval_id + 扫码选中的物品 + 借用人信息 + 签名

View File

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

View File

@ -19,25 +19,25 @@
</template> </template>
<!-- 普通模式 --> <!-- 普通模式 -->
<template v-else> <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>
<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-button>
<el-divider direction="vertical" /> <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>
<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 套餐添加 按 BOM 套餐添加
</el-button> </el-button>
</template> </template>
<el-divider direction="vertical" /> <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>
<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> </el-button>
</div> </div>
@ -98,7 +98,7 @@
size="small" size="small"
style="width: 100%" style="width: 100%"
controls-position="right" controls-position="right"
:disabled="!userStore.hasPermission('outbound_selection:operation')" :disabled="!userStore.hasPermission('op_borrow_apply:operation')"
@change="(val) => handleMainQuantityChange(val, row)" @change="(val) => handleMainQuantityChange(val, row)"
/> />
</template> </template>
@ -106,7 +106,7 @@
<el-table-column label="操作" width="80" align="center" fixed="right"> <el-table-column label="操作" width="80" align="center" fixed="right">
<template #default="{ $index }"> <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> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@ -162,7 +162,7 @@
size="small" size="small"
style="width: 100%" style="width: 100%"
placeholder="0" placeholder="0"
:disabled="!userStore.hasPermission('outbound_selection:operation')" :disabled="!userStore.hasPermission('op_borrow_apply:operation')"
@click.stop @click.stop
@change="(val) => handleManualQuantityChange(val, row)" @change="(val) => handleManualQuantityChange(val, row)"
/> />
@ -185,7 +185,7 @@
已勾选 {{ tempSelection.length }} 项 已勾选 {{ tempSelection.length }} 项
</span> </span>
<el-button @click="manualDialogVisible = false">取消</el-button> <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> </template>
</el-dialog> </el-dialog>
@ -202,7 +202,7 @@
/> />
</el-form-item> </el-form-item>
<el-form-item label="借库套数"> <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;"> <el-tag v-if="selectedBomNo && maxBuildableSets >= 0" type="success" style="margin-left: 16px;">
当前库存最多可成套借库: {{ maxBuildableSets }} 套 当前库存最多可成套借库: {{ maxBuildableSets }} 套
</el-tag> </el-tag>
@ -248,7 +248,7 @@
<template #footer> <template #footer>
<el-button @click="bomSelectVisible = false">取消</el-button> <el-button @click="bomSelectVisible = false">取消</el-button>
<el-button <el-button
v-if="userStore.hasPermission('outbound_selection:operation')" v-if="userStore.hasPermission('op_borrow_apply:operation')"
type="primary" type="primary"
@click="confirmBomAdd" @click="confirmBomAdd"
> >
@ -288,11 +288,11 @@
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="previewVisible = false">取消</el-button> <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 导出 Excel
</el-button> </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) 确认打印 (A4)
</el-button> </el-button>
</span> </span>
@ -440,9 +440,9 @@
import { ref, computed, watch } from 'vue' import { ref, computed, watch } from 'vue'
import { Printer, Search, Plus, Download, List } from '@element-plus/icons-vue' import { Printer, Search, Plus, Download, List } from '@element-plus/icons-vue'
import { ElMessage, ElTable, ElMessageBox } from 'element-plus' 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 { useUserStore } from '@/stores/user'
import { submitBorrowRequest } from '@/api/transaction' import { submitBorrowRequest, getBorrowStockList } from '@/api/transaction'
import { getApproversList } from '@/api/auth' import { getApproversList } from '@/api/auth'
import { getBomList, getBomDetail, getBomWithStock } from '@/api/bom' import { getBomList, getBomDetail, getBomWithStock } from '@/api/bom'
@ -583,7 +583,7 @@ const getTypeTag = (type: string) => {
const loadStockList = async () => { const loadStockList = async () => {
stockLoading.value = true stockLoading.value = true
try { try {
const res: any = await getStockList({ const res: any = await getBorrowStockList({
page: stockPage.value, page: stockPage.value,
pageSize: stockPageSize.value, pageSize: stockPageSize.value,
keyword: searchKeyword.value.trim(), keyword: searchKeyword.value.trim(),
@ -610,7 +610,7 @@ const loadAllStockForBom = async () => {
let page = 1 let page = 1
const pageSize = 200 const pageSize = 200
while (true) { while (true) {
const res: any = await getStockList({ const res: any = await getBorrowStockList({
page, page,
pageSize, pageSize,
is_aggregated: true is_aggregated: true