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:
yueli
2026-07-13 15:12:22 +08:00
parent 4ce42309db
commit 4f5965db02
22 changed files with 289 additions and 172 deletions

View File

@ -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) => {