feat: JWT多租户数据权限隔离 & 主管系统管理权限 & 含税单价补齐
## 多租户公司数据隔离 - 新增 get_current_company_filter() 工具函数 (decorators.py) SUPER_ADMIN: 可传company_name参数过滤或传ALL看全量 其他角色: 强制隔离到JWT中的company_name - 重构 base_service.py / buy_service.py: 用集中式函数替换内联公司过滤 - SysRolePermission 表新增 company_name 字段,支持同角色不同公司权限 - get_user_permissions() 新增 company_name 参数,查公司定制+全局模板权限 - permission.py API 新增 @permission_required 拦截 + 公司过滤 - 19个API/service文件传递 company_name 到权限查询 ## 主管系统管理权限 - delete_user() 允许SUPERVISOR删除同公司用户 (原仅SUPER_ADMIN) - get_all_users() 新增 company_name 参数过滤 - 用户列表/权限分配 API 应用 get_current_company_filter() - 前端 UserCreate.vue: 超管可见公司下拉框,主管隐藏部门字段 ## 前端多租户适配 - material/list.vue / buy.vue: 公司下拉框仅超管可见,默认ALL - UserCreate.vue: 新增搜索栏公司筛选,部门字段按角色显隐 - auth.ts: getUserList() 支持 params 参数 ## Bug修复: 含税单价字段补齐 - buy.vue: 表格列/高级筛选/排序/权限映射新增 post_tax_unit_price - buy_service.py: allowed_fields/sort_field_map 新增 post_tax_unit_price
This commit is contained in:
@ -35,11 +35,12 @@ export function getUserInfo() {
|
||||
})
|
||||
}
|
||||
|
||||
// 获取所有用户列表
|
||||
export function getUserList() {
|
||||
// 获取所有用户列表(支持公司过滤参数)
|
||||
export function getUserList(params?: any) {
|
||||
return request({
|
||||
url: '/v1/auth/users',
|
||||
method: 'get'
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -21,6 +21,7 @@
|
||||
</el-input>
|
||||
|
||||
<el-select
|
||||
v-if="isSuperAdmin"
|
||||
v-model="queryParams.company"
|
||||
placeholder="所属公司"
|
||||
clearable
|
||||
@ -29,6 +30,7 @@
|
||||
style="width: 120px; margin-right: 10px;"
|
||||
@change="handleQuery"
|
||||
>
|
||||
<el-option label="全部 (跨域)" value="ALL" />
|
||||
<el-option v-for="item in companyOptions" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
|
||||
@ -699,6 +701,7 @@ import ImageSearchDialog from '@/components/ImageSearchDialog.vue';
|
||||
import { imageSearch as imageSearchApi, type ImageSearchItem } from '@/api/common/upload';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const isSuperAdmin = computed(() => userStore.role === 'SUPER_ADMIN');
|
||||
|
||||
// --- 类型定义 ---
|
||||
interface MaterialBaseVO {
|
||||
@ -1071,7 +1074,7 @@ const queryParams = reactive<QueryParams>({
|
||||
searchField: 'all',
|
||||
category: '',
|
||||
type: '',
|
||||
company: '',
|
||||
company: 'ALL',
|
||||
isEnabled: undefined,
|
||||
orderByColumn: '',
|
||||
isAsc: undefined,
|
||||
@ -1296,7 +1299,7 @@ const resetQuery = () => {
|
||||
queryParams.searchField = 'all';
|
||||
queryParams.category = '';
|
||||
queryParams.type = '';
|
||||
queryParams.company = '';
|
||||
queryParams.company = isSuperAdmin.value ? 'ALL' : '';
|
||||
queryParams.isEnabled = undefined;
|
||||
queryParams.orderByColumn = '';
|
||||
queryParams.isAsc = undefined;
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
<div class="search-form-area" style="flex-wrap: wrap;">
|
||||
|
||||
<el-select
|
||||
v-if="isSuperAdmin"
|
||||
v-model="queryParams.company"
|
||||
placeholder="所属公司"
|
||||
class="filter-item-select"
|
||||
@ -12,6 +13,7 @@
|
||||
@change="fetchData"
|
||||
style="width: 160px;"
|
||||
>
|
||||
<el-option label="全部 (跨域)" value="ALL" />
|
||||
<el-option v-for="item in companyOptions" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
|
||||
@ -229,7 +231,7 @@
|
||||
</el-link>
|
||||
</template>
|
||||
|
||||
<template #default="scope" v-else-if="['unit_price', 'total_price'].includes(col.prop)">
|
||||
<template #default="scope" v-else-if="['unit_price', 'post_tax_unit_price', 'total_price'].includes(col.prop)">
|
||||
<span class="money-text">{{ formatMoney(scope.row[col.prop], scope.row.currency) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@ -765,6 +767,7 @@ const hasFormFieldPermission = (fieldName: string) => {
|
||||
available_quantity: 'inbound_buy:available_quantity',
|
||||
warehouse_location: 'inbound_buy:warehouse_location',
|
||||
unit_price: 'inbound_buy:unit_price',
|
||||
post_tax_unit_price: 'inbound_buy:post_tax_unit_price',
|
||||
tax_rate: 'inbound_buy:tax_rate',
|
||||
total_price: 'inbound_buy:total_price',
|
||||
currency: 'inbound_buy:currency',
|
||||
@ -790,6 +793,7 @@ const hasFormFieldPermission = (fieldName: string) => {
|
||||
// 状态与变量
|
||||
// ------------------------------------
|
||||
const userStore = useUserStore()
|
||||
const isSuperAdmin = computed(() => userStore.role === 'SUPER_ADMIN')
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const visible = ref(false)
|
||||
@ -826,7 +830,7 @@ const queryParams = reactive({
|
||||
sku: '',
|
||||
category: '',
|
||||
material_type: '',
|
||||
company: '',
|
||||
company: 'ALL',
|
||||
statuses: ['在库', '借库'],
|
||||
orderByColumn: '',
|
||||
isAsc: undefined as string | undefined,
|
||||
@ -885,6 +889,7 @@ const fieldOptions = computed(() => {
|
||||
{ value: 'qty_stock', label: '库存数', perm: 'inbound_buy:qty_stock' },
|
||||
{ value: 'qty_available', label: '可用数', perm: 'inbound_buy:qty_available' },
|
||||
{ value: 'unit_price', label: '不含税单价', perm: 'inbound_buy:unit_price' },
|
||||
{ value: 'post_tax_unit_price', label: '含税单价', perm: 'inbound_buy:post_tax_unit_price' },
|
||||
{ value: 'total_price', label: '不含税总价', perm: 'inbound_buy:total_price' },
|
||||
{ value: 'tax_rate', label: '税率', perm: 'inbound_buy:tax_rate' },
|
||||
{ value: 'currency', label: '币种', perm: 'inbound_buy:currency' },
|
||||
@ -936,6 +941,7 @@ const stockColumns = [
|
||||
|
||||
{prop: 'tax_rate', label: '税率', minWidth: '80'},
|
||||
{prop: 'unit_price', label: '不含税单价', minWidth: '120'},
|
||||
{prop: 'post_tax_unit_price', label: '含税单价', minWidth: '120'},
|
||||
|
||||
{prop: 'total_price', label: '不含税总价', minWidth: '120'},
|
||||
{prop: 'currency', label: '币种', minWidth: '80'},
|
||||
@ -973,6 +979,7 @@ const permissionMap: Record<string, string> = {
|
||||
warehouse_loc: 'inbound_buy:warehouse_loc',
|
||||
tax_rate: 'inbound_buy:tax_rate',
|
||||
unit_price: 'inbound_buy:unit_price',
|
||||
post_tax_unit_price: 'inbound_buy:post_tax_unit_price',
|
||||
total_price: 'inbound_buy:total_price',
|
||||
currency: 'inbound_buy:currency',
|
||||
exchange_rate: 'inbound_buy:exchange_rate',
|
||||
@ -1409,7 +1416,7 @@ const resetQuery = () => {
|
||||
queryParams.sku = ''
|
||||
queryParams.category = ''
|
||||
queryParams.material_type = ''
|
||||
queryParams.company = ''
|
||||
queryParams.company = isSuperAdmin.value ? 'ALL' : ''
|
||||
queryParams.page = 1
|
||||
fetchData()
|
||||
}
|
||||
@ -1677,7 +1684,7 @@ const resetAdvancedFilter = () => {
|
||||
fetchData()
|
||||
}
|
||||
const isColumnSortable = (prop: string) => {
|
||||
const sortableColumns = ['company_name', 'material_name', 'material_type', 'category', 'spec_model', 'unit', 'sku', 'barcode', 'inbound_date', 'serial_number', 'batch_number', 'status', 'inspection_status', 'qty_inbound', 'qty_stock', 'qty_available', 'warehouse_loc', 'unit_price', 'total_price', 'tax_rate', 'currency', 'exchange_rate', 'supplier_name', 'purchaser', 'purchaser_email', 'source_link', 'detail_link']
|
||||
const sortableColumns = ['company_name', 'material_name', 'material_type', 'category', 'spec_model', 'unit', 'sku', 'barcode', 'inbound_date', 'serial_number', 'batch_number', 'status', 'inspection_status', 'qty_inbound', 'qty_stock', 'qty_available', 'warehouse_loc', 'unit_price', 'post_tax_unit_price', 'total_price', 'tax_rate', 'currency', 'exchange_rate', 'supplier_name', 'purchaser', 'purchaser_email', 'source_link', 'detail_link']
|
||||
return sortableColumns.includes(prop)
|
||||
}
|
||||
const handleSortChange = ({ column, prop, order }: any) => {
|
||||
|
||||
@ -15,6 +15,23 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="filter-container" style="margin-bottom: 16px; display: flex; align-items: center; gap: 10px;">
|
||||
<el-select
|
||||
v-if="isSuperAdmin"
|
||||
v-model="queryParams.company"
|
||||
placeholder="所属公司"
|
||||
clearable
|
||||
filterable
|
||||
style="width: 180px;"
|
||||
@change="getList"
|
||||
>
|
||||
<el-option label="全部 (跨域)" value="ALL" />
|
||||
<el-option v-for="item in companyOptions" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
<el-button type="primary" plain @click="getList">搜索</el-button>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-loading="tableLoading"
|
||||
:data="tableData"
|
||||
@ -104,7 +121,7 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="所属部门" prop="department" v-if="hasFormFieldPermission('department')">
|
||||
<el-form-item label="所属部门" prop="department" v-if="hasFormFieldPermission('department') && isSuperAdmin">
|
||||
<el-select
|
||||
v-model="form.department"
|
||||
placeholder="请输入或选择部门"
|
||||
@ -147,7 +164,7 @@
|
||||
<!-- 批量新增弹窗 -->
|
||||
<el-dialog v-model="batchDialogVisible" title="批量新增员工" width="600px" destroy-on-close @close="batchForm.namesText = ''">
|
||||
<el-form :model="batchForm" label-width="100px">
|
||||
<el-form-item label="所属部门" required>
|
||||
<el-form-item label="所属部门" required v-if="isSuperAdmin">
|
||||
<el-select v-model="batchForm.department" style="width: 100%" placeholder="请选择部门">
|
||||
<el-option v-for="d in departmentOptions" :key="d" :label="d" :value="d" />
|
||||
</el-select>
|
||||
@ -194,6 +211,11 @@ import { ElMessage } from 'element-plus'
|
||||
import { pinyin } from 'pinyin-pro' // ★ 务必安装: npm install pinyin-pro
|
||||
|
||||
const userStore = useUserStore()
|
||||
const isSuperAdmin = computed(() => userStore.role === 'SUPER_ADMIN')
|
||||
|
||||
// 查询参数 & 公司列表
|
||||
const queryParams = reactive({ company: 'ALL' })
|
||||
const companyOptions = ref<string[]>([])
|
||||
|
||||
// 列与权限Code的映射关系(数据库中的code)
|
||||
const permissionMap: Record<string, string> = {
|
||||
@ -327,7 +349,9 @@ const rules = computed(() => {
|
||||
{ validator: validateNameStrict, trigger: 'blur' }
|
||||
],
|
||||
role: [{ required: true, message: '请选择角色', trigger: 'change' }],
|
||||
department: [{ required: true, message: '请输入或选择部门', trigger: ['blur', 'change'] }],
|
||||
department: isSuperAdmin.value
|
||||
? [{ required: true, message: '请输入或选择部门', trigger: ['blur', 'change'] }]
|
||||
: [],
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
||||
{ type: 'email', message: '请输入正确的邮箱格式', trigger: ['blur', 'change'] }
|
||||
@ -357,11 +381,18 @@ const rules = computed(() => {
|
||||
const getList = async () => {
|
||||
tableLoading.value = true
|
||||
try {
|
||||
const res = await getUserList()
|
||||
const params: any = {}
|
||||
if (queryParams.company && queryParams.company !== 'ALL') {
|
||||
params.company_name = queryParams.company
|
||||
}
|
||||
const res = await getUserList(params)
|
||||
tableData.value = res.data || []
|
||||
extractDepartments(tableData.value)
|
||||
// 提取公司列表(从部门字段)
|
||||
const deptSet = new Set<string>()
|
||||
tableData.value.forEach(u => { if (u.department) deptSet.add(u.department) })
|
||||
companyOptions.value = Array.from(deptSet)
|
||||
} catch (error) {
|
||||
// 错误已由全局拦截器统一处理
|
||||
console.error('Fetch users failed:', error)
|
||||
} finally {
|
||||
tableLoading.value = false
|
||||
@ -448,8 +479,11 @@ const onSubmit = async () => {
|
||||
|
||||
// 批量提交逻辑
|
||||
const handleBatchSubmit = async () => {
|
||||
if (!batchForm.department || !batchForm.role || !batchForm.namesText.trim()) {
|
||||
return ElMessage.warning('请填写完整部门、角色及员工名单')
|
||||
if (!batchForm.role || !batchForm.namesText.trim()) {
|
||||
return ElMessage.warning('请填写完整角色及员工名单')
|
||||
}
|
||||
if (isSuperAdmin.value && !batchForm.department) {
|
||||
return ElMessage.warning('请填写所属部门')
|
||||
}
|
||||
|
||||
const names = batchForm.namesText.split('\n').map(n => n.trim()).filter(n => n)
|
||||
|
||||
Reference in New Issue
Block a user