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:
@ -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'])
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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 + 扫码选中的物品 + 借用人信息 + 签名
|
||||||
|
|||||||
@ -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']
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user