feat: apply RBAC read/write separation to outbound_create module
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
This commit is contained in:
@ -1,7 +1,8 @@
|
|||||||
from flask import Blueprint, request, jsonify
|
from flask import Blueprint, request, jsonify
|
||||||
from app.services.outbound_service import OutboundService
|
from app.services.outbound_service import OutboundService
|
||||||
from flask_jwt_extended import jwt_required, get_jwt_identity
|
from flask_jwt_extended import jwt_required, get_jwt_identity, get_jwt
|
||||||
from app.utils.decorators import permission_required
|
from app.utils.decorators import permission_required
|
||||||
|
from app.services.auth_service import AuthService
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
outbound_bp = Blueprint('outbound', __name__, url_prefix='/outbound')
|
outbound_bp = Blueprint('outbound', __name__, url_prefix='/outbound')
|
||||||
@ -46,8 +47,20 @@ def scan_barcode():
|
|||||||
# --------------------------------------------------------
|
# --------------------------------------------------------
|
||||||
@outbound_bp.route('', methods=['POST'])
|
@outbound_bp.route('', methods=['POST'])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
@permission_required('outbound_selection:operation')
|
|
||||||
def create_outbound():
|
def create_outbound():
|
||||||
|
# 权限检查:需要 outbound_create:operation 或 outbound_selection:operation 之一
|
||||||
|
claims = get_jwt()
|
||||||
|
user_role = claims.get('role')
|
||||||
|
if not user_role:
|
||||||
|
return jsonify({'code': 403, 'msg': '未授权'}), 403
|
||||||
|
|
||||||
|
# 超级管理员直接放行
|
||||||
|
if user_role != 'super_admin':
|
||||||
|
perm_dict = AuthService.get_user_permissions(user_role)
|
||||||
|
perms = perm_dict.get('menus', []) + perm_dict.get('elements', [])
|
||||||
|
if ('outbound_create:operation' not in perms) and ('outbound_selection:operation' not in perms):
|
||||||
|
return jsonify({'code': 403, 'msg': '权限不足'}), 403
|
||||||
|
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
if not data:
|
if not data:
|
||||||
return jsonify({'code': 400, 'msg': '无有效数据'}), 400
|
return jsonify({'code': 400, 'msg': '无有效数据'}), 400
|
||||||
|
|||||||
@ -17,10 +17,14 @@
|
|||||||
|
|
||||||
<div class="scan-section">
|
<div class="scan-section">
|
||||||
|
|
||||||
<div class="camera-placeholder" @click="showCamera = true">
|
<div v-if="userStore.hasPermission('outbound_create:operation')" class="camera-placeholder" @click="showCamera = true">
|
||||||
<el-icon :size="40" color="#409EFF"><CameraFilled /></el-icon>
|
<el-icon :size="40" color="#409EFF"><CameraFilled /></el-icon>
|
||||||
<span class="text">点击开启全屏扫码</span>
|
<span class="text">点击开启全屏扫码</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="camera-placeholder" style="background-color: #f5f5f5; cursor: not-allowed;">
|
||||||
|
<el-icon :size="40" color="#909399"><CameraFilled /></el-icon>
|
||||||
|
<span class="text">无扫码权限</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="input-box">
|
<div class="input-box">
|
||||||
<el-input
|
<el-input
|
||||||
@ -30,12 +34,13 @@
|
|||||||
clearable
|
clearable
|
||||||
ref="barcodeRef"
|
ref="barcodeRef"
|
||||||
size="large"
|
size="large"
|
||||||
|
:disabled="!userStore.hasPermission('outbound_create:operation')"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<el-icon><Scissor /></el-icon>
|
<el-icon><Scissor /></el-icon>
|
||||||
</template>
|
</template>
|
||||||
<template #append>
|
<template #append>
|
||||||
<el-button @click="handleManualInput">添加</el-button>
|
<el-button @click="handleManualInput" :disabled="!userStore.hasPermission('outbound_create:operation')">添加</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
</div>
|
</div>
|
||||||
@ -64,13 +69,14 @@
|
|||||||
:max="parseFloat(row.available_quantity)"
|
:max="parseFloat(row.available_quantity)"
|
||||||
size="small"
|
size="small"
|
||||||
style="width: 100px"
|
style="width: 100px"
|
||||||
|
:disabled="!userStore.hasPermission('outbound_create:operation')"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
<el-table-column label="操作" width="60" align="center" fixed="right">
|
<el-table-column label="操作" width="60" align="center" fixed="right">
|
||||||
<template #default="{$index}">
|
<template #default="{$index}">
|
||||||
<el-button type="danger" icon="Delete" circle size="small" @click="removeFromCart($index)" />
|
<el-button v-if="userStore.hasPermission('outbound_create:operation')" type="danger" icon="Delete" circle size="small" @click="removeFromCart($index)" />
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@ -120,7 +126,7 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="电子签名确认" required>
|
<el-form-item label="电子签名确认" required>
|
||||||
<div class="signature-box" @click="openSignatureDialog">
|
<div class="signature-box" @click="openSignatureDialog" v-if="userStore.hasPermission('outbound_create:operation')">
|
||||||
<div v-if="signaturePreviewUrl" class="signed-img">
|
<div v-if="signaturePreviewUrl" class="signed-img">
|
||||||
<img :src="signaturePreviewUrl" alt="签名" />
|
<img :src="signaturePreviewUrl" alt="签名" />
|
||||||
<span class="re-sign-tip">点击重签</span>
|
<span class="re-sign-tip">点击重签</span>
|
||||||
@ -130,11 +136,17 @@
|
|||||||
<span>点击此处进行全屏签名</span>
|
<span>点击此处进行全屏签名</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="signature-box" style="background-color: #f5f5f5; cursor: not-allowed;">
|
||||||
|
<div class="unsigned-placeholder">
|
||||||
|
<el-icon :size="24"><EditPen /></el-icon>
|
||||||
|
<span>无签名权限</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<div class="bottom-actions">
|
<div class="bottom-actions">
|
||||||
<el-button @click="clearAll" icon="Refresh">清空</el-button>
|
<el-button v-if="userStore.hasPermission('outbound_create:operation')" @click="clearAll" icon="Refresh">清空</el-button>
|
||||||
<el-button type="primary" size="large" :loading="loading" @click="submitForm" icon="Select">
|
<el-button v-if="userStore.hasPermission('outbound_create:operation')" type="primary" size="large" :loading="loading" @click="submitForm" icon="Select">
|
||||||
确认出库
|
确认出库
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
@ -205,6 +217,8 @@ import { getStockByBarcode, submitOutbound, getOutboundList } from '@/api/outbou
|
|||||||
import { uploadFile } from '@/api/common/upload'
|
import { uploadFile } from '@/api/common/upload'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
// --- 状态定义 ---
|
// --- 状态定义 ---
|
||||||
const barcodeInput = ref('')
|
const barcodeInput = ref('')
|
||||||
const cartItems = ref<any[]>([])
|
const cartItems = ref<any[]>([])
|
||||||
@ -212,7 +226,6 @@ const loading = ref(false)
|
|||||||
const showCamera = ref(false)
|
const showCamera = ref(false)
|
||||||
const barcodeRef = ref()
|
const barcodeRef = ref()
|
||||||
const formRef = ref()
|
const formRef = ref()
|
||||||
const userStore = useUserStore()
|
|
||||||
|
|
||||||
// 签名相关
|
// 签名相关
|
||||||
const showSignatureDialog = ref(false)
|
const showSignatureDialog = ref(false)
|
||||||
@ -292,6 +305,10 @@ const onScanSuccess = (code: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleManualInput = async () => {
|
const handleManualInput = async () => {
|
||||||
|
if (!userStore.hasPermission('outbound_create:operation')) {
|
||||||
|
ElMessage.warning('无操作权限')
|
||||||
|
return
|
||||||
|
}
|
||||||
const code = barcodeInput.value.trim()
|
const code = barcodeInput.value.trim()
|
||||||
if (!code) return
|
if (!code) return
|
||||||
|
|
||||||
@ -355,10 +372,18 @@ const handleManualInput = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const removeFromCart = (index: number) => {
|
const removeFromCart = (index: number) => {
|
||||||
|
if (!userStore.hasPermission('outbound_create:operation')) {
|
||||||
|
ElMessage.warning('无操作权限')
|
||||||
|
return
|
||||||
|
}
|
||||||
cartItems.value.splice(index, 1)
|
cartItems.value.splice(index, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearAll = () => {
|
const clearAll = () => {
|
||||||
|
if (!userStore.hasPermission('outbound_create:operation')) {
|
||||||
|
ElMessage.warning('无操作权限')
|
||||||
|
return
|
||||||
|
}
|
||||||
ElMessageBox.confirm('确定清空所有已选商品吗?', '提示', { type: 'warning' })
|
ElMessageBox.confirm('确定清空所有已选商品吗?', '提示', { type: 'warning' })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
cartItems.value = []
|
cartItems.value = []
|
||||||
@ -372,6 +397,10 @@ const clearAll = () => {
|
|||||||
|
|
||||||
// --- 提交逻辑 ---
|
// --- 提交逻辑 ---
|
||||||
const submitForm = async () => {
|
const submitForm = async () => {
|
||||||
|
if (!userStore.hasPermission('outbound_create:operation')) {
|
||||||
|
ElMessage.warning('无操作权限')
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!formRef.value) return
|
if (!formRef.value) return
|
||||||
if (cartItems.value.length === 0) return ElMessage.warning('请先添加商品')
|
if (cartItems.value.length === 0) return ElMessage.warning('请先添加商品')
|
||||||
|
|
||||||
@ -427,7 +456,13 @@ const submitForm = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- 签名逻辑 (Canvas) ---
|
// --- 签名逻辑 (Canvas) ---
|
||||||
const openSignatureDialog = () => { showSignatureDialog.value = true }
|
const openSignatureDialog = () => {
|
||||||
|
if (!userStore.hasPermission('outbound_create:operation')) {
|
||||||
|
ElMessage.warning('无签名权限')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
showSignatureDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
const initCanvas = async () => {
|
const initCanvas = async () => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
|||||||
Reference in New Issue
Block a user