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

@ -1,7 +1,7 @@
# app/services/auth_service.py
from app.models.system import SysUser, SysRolePermission # <== 引入 SysRolePermission
from app.extensions import db, redis_client, revoke_all_tokens_for_user
from sqlalchemy import func
from sqlalchemy import func, or_
from flask_jwt_extended import create_access_token, create_refresh_token, get_jwt_identity
from flask import current_app
from app.utils.constants import UserRole
@ -350,23 +350,43 @@ class AuthService:
return user.to_dict()
@staticmethod
def get_all_users():
"""获取所有系统用户"""
users = SysUser.query.order_by(SysUser.id.desc()).all()
def get_all_users(company_name=None):
"""
获取系统用户列表(支持公司级别隔离)
参数:
company_name — None=不过滤(超管), 具体值=仅返回该公司的用户
"""
query = SysUser.query.order_by(SysUser.id.desc())
if company_name:
query = query.filter(SysUser.department == company_name)
users = query.all()
return [user.to_dict() for user in users]
@staticmethod
def delete_user(user_id, operator_role):
"""删除用户:删除前自动吊销该用户所有 JWT Token"""
# 标准化操作者角色为全大写
def delete_user(user_id, operator_role, operator_company=None):
"""
删除用户:删除前自动吊销该用户所有 JWT Token
权限检查:
- SUPER_ADMIN → 可删除任何用户
- SUPERVISOR → 仅可删除同公司department的用户
"""
operator_role_upper = operator_role.upper() if operator_role else None
if operator_role_upper != UserRole.SUPER_ADMIN:
raise Exception("权限不足:只有超级管理员可以删除用户")
user = SysUser.query.get(user_id)
if not user:
raise Exception("用户不存在")
# 权限校验
if operator_role_upper == UserRole.SUPER_ADMIN:
pass # 超管可删除任何人
elif operator_role_upper == UserRole.SUPERVISOR:
if operator_company and user.department != operator_company:
raise Exception("权限不足:主管只能删除本公司的用户")
else:
raise Exception("权限不足:只有超级管理员或主管可以删除用户")
# 提前获取用户名用于审计日志
username = user.username
@ -386,47 +406,67 @@ class AuthService:
return username
@staticmethod
def get_user_permissions(role_code):
def get_user_permissions(role_code, company_name=None):
"""
获取指定角色的所有权限代码列表
获取指定角色的所有权限代码列表(支持公司级别隔离)
参数:
role_code — 角色代码
company_name — 所属公司None=不过滤/超管,具体值=只查该公司权限+全局模板权限)
返回格式: {
'menus': ['inbound_buy', 'system_user'],
'elements': ['inbound_buy:unit_price', ...]
}
公司过滤逻辑:
- 若无 company_name → 不过滤(兼容旧逻辑 / 超级管理员)
- 若有 company_name → 查「公司定制权限」+「全局模板权限company_name 为空/NULL/System
"""
# 防御性编程role_code 为空时直接返回空权限,避免后续 SQL 崩溃
# 防御性编程role_code 为空时直接返回空权限
if not role_code:
return {'menus': [], 'elements': []}
# 超级管理员返回所有权限(通配符)
from app.utils.constants import UserRole
if role_code.upper() == UserRole.SUPER_ADMIN:
# 返回通配符,表示拥有所有菜单和元素权限
return {
'menus': ['*'],
'elements': ['*']
}
# 构建公司过滤条件:(公司定制 OR 全局模板)
if company_name:
company_filter = or_(
SysRolePermission.company_name == company_name,
SysRolePermission.company_name.is_(None),
SysRolePermission.company_name == '',
SysRolePermission.company_name == 'System'
)
else:
company_filter = True # 不过滤
# 1. 查菜单权限
# 使用 func.upper() 处理数据库字段的大小写
menu_perms = SysRolePermission.query.filter(
func.upper(SysRolePermission.role_code) == role_code.upper(),
SysRolePermission.type == 'menu'
SysRolePermission.type == 'menu',
company_filter
).all()
menu_codes = [p.target_code for p in menu_perms]
# 2. 查元素(列)权限
# 注意:这里我们只返回用户拥有的。前端逻辑是:"如果列配置了Key且用户没这个Key则隐藏"
element_perms = SysRolePermission.query.filter(
func.upper(SysRolePermission.role_code) == role_code.upper(),
SysRolePermission.type == 'element'
SysRolePermission.type == 'element',
company_filter
).all()
element_codes = [p.target_code for p in element_perms]
# 调试日志:输出查询结果便于排查字段权限问题
# 调试日志
from flask import current_app
current_app.logger.info(
f"[权限查询] role={role_code}, 查询到菜单权限={menu_codes}, 元素权限={element_codes}"
f"[权限查询] role={role_code}, company={company_name or '(all)'}, "
f"菜单权限={menu_codes}, 元素权限={element_codes}"
)
return {