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:
dxc
2026-02-27 13:54:06 +08:00
parent af41eb1803
commit 3714dd180b
2 changed files with 59 additions and 11 deletions

View File

@ -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

View File

@ -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()