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

@ -216,39 +216,13 @@ class MaterialBaseService:
))
# ============================================================
# 【行级数据隔离】基于 JWT 中的 company_name 进行过滤
# 【行级数据隔离】基于 JWT 多租户公司过滤
# ============================================================
from flask_jwt_extended import get_jwt
claims = get_jwt()
user_role = claims.get('role', '').upper() if claims.get('role') else ''
user_company = claims.get('company_name', '')
from app.utils.decorators import get_current_company_filter
# 获取用户权限列表(用于检查 global:cross_company_op 特权)
from app.api.v1.inbound.base import get_current_user_permissions
user_perms = get_current_user_permissions() or []
normalized_perms = set(p.lower().replace('_', '').replace(':', '') for p in user_perms)
# 检查是否拥有全局特权或超管角色
has_cross_company = 'globalcrosscompanyop' in normalized_perms
# 获取前端传的查询参数
req_company = filters.get('company') if filters else None
if user_role != 'SUPER_ADMIN' and not has_cross_company:
# 【显式拒绝越权】如果前端传了公司参数且不是当前用户的公司返回403
if req_company and req_company != user_company:
from flask import abort
abort(403, description=f'越权访问:您无权查询 {req_company} 的数据')
# 正常查询本公司数据
if user_company:
query = query.filter(MaterialBase.company_name == user_company)
# 如果用户没有所属公司字段,则只显示公司为空的记录(或不允许查看)
elif user_role == 'SUPER_ADMIN' or has_cross_company:
# 超级管理员或有跨域特权:允许跨公司视角
if req_company:
query = query.filter(MaterialBase.company_name == req_company)
# 没选公司则不加过滤,看到全量
company_limit = get_current_company_filter()
if company_limit is not None:
query = query.filter(MaterialBase.company_name == company_limit)
category = filters.get('category')
if category is not None and category != '':
@ -750,32 +724,13 @@ class MaterialBaseService:
MaterialBase.company_name.ilike(kw)
))
# ============================================================
# 【行级数据隔离】基于 JWT 中的 company_name 进行过滤(高级筛选)
# 【行级数据隔离】基于 JWT 多租户公司过滤
# ============================================================
from flask_jwt_extended import get_jwt
claims = get_jwt()
user_role = claims.get('role', '').upper() if claims.get('role') else ''
user_company = claims.get('company_name', '')
# 获取用户权限列表(用于检查 global:cross_company_op 特权)
from app.api.v1.inbound.base import get_current_user_permissions
user_perms = get_current_user_permissions() or []
normalized_perms = set(p.lower().replace('_', '').replace(':', '') for p in user_perms)
# 检查是否拥有全局特权或超管角色
has_cross_company = 'globalcrosscompanyop' in normalized_perms
from app.utils.decorators import get_current_company_filter
req_company = filters.get('company') if filters else None
if user_role != 'SUPER_ADMIN' and not has_cross_company:
# 普通用户:强制隔离
if user_company:
filter_conditions.append(MaterialBase.company_name == user_company)
elif user_role == 'SUPER_ADMIN' or has_cross_company:
# 超级管理员或有跨域特权:允许跨公司视角
if req_company:
filter_conditions.append(MaterialBase.company_name == req_company)
company_limit = get_current_company_filter()
if company_limit is not None:
filter_conditions.append(MaterialBase.company_name == company_limit)
category = filters.get('category')
if category is not None and category != '':