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

@ -31,16 +31,17 @@ auth_bp = Blueprint('auth', __name__)
def get_current_user_permissions(): def get_current_user_permissions():
""" """
返回当前用户拥有的所有权限码列表(包括菜单和元素) 返回当前用户拥有的所有权限码列表(包括菜单和元素)
此函数根据角色查询数据库得到权限。 此函数根据角色+公司查询数据库得到权限。
""" """
claims = get_jwt() claims = get_jwt()
user_role = claims.get('role') user_role = claims.get('role')
user_company = claims.get('company_name', '')
if not user_role: if not user_role:
return [] return []
# 超级管理员返回所有字段权限 (忽略大小写) # 超级管理员返回所有字段权限
if user_role.upper() == 'SUPER_ADMIN': if user_role.upper() == 'SUPER_ADMIN':
return ['system_user:*'] return ['system_user:*']
perm_dict = AuthService.get_user_permissions(user_role) perm_dict = AuthService.get_user_permissions(user_role, company_name=user_company)
# 合并菜单和元素权限 # 合并菜单和元素权限
perms = perm_dict.get('menus', []) + perm_dict.get('elements', []) perms = perm_dict.get('menus', []) + perm_dict.get('elements', [])
return perms return perms
@ -269,7 +270,9 @@ def update_user(user_id):
@permission_required('system_user') @permission_required('system_user')
def get_users(): def get_users():
try: try:
users = AuthService.get_all_users() from app.utils.decorators import get_current_company_filter
company_filter = get_current_company_filter()
users = AuthService.get_all_users(company_name=company_filter)
user_permissions = get_current_user_permissions() user_permissions = get_current_user_permissions()
filtered_users = [filter_item_by_permissions(user, user_permissions) for user in users] filtered_users = [filter_item_by_permissions(user, user_permissions) for user in users]
return jsonify({'msg': '获取成功', 'data': filtered_users}), 200 return jsonify({'msg': '获取成功', 'data': filtered_users}), 200
@ -293,8 +296,9 @@ def delete_user(user_id):
try: try:
claims = get_jwt() claims = get_jwt()
operator_role = claims.get('role') operator_role = claims.get('role')
operator_company = claims.get('company_name', '')
username = AuthService.delete_user(user_id, operator_role) username = AuthService.delete_user(user_id, operator_role, operator_company)
return jsonify({'msg': '删除成功', 'username': username}), 200 return jsonify({'msg': '删除成功', 'username': username}), 200
except Exception as e: except Exception as e:
current_app.logger.error(f"Delete User Failed: {str(e)}") current_app.logger.error(f"Delete User Failed: {str(e)}")
@ -311,7 +315,8 @@ def get_my_permissions():
try: try:
claims = get_jwt() claims = get_jwt()
role = claims.get('role') role = claims.get('role')
permissions = AuthService.get_user_permissions(role) company_name = claims.get('company_name', '')
permissions = AuthService.get_user_permissions(role, company_name=company_name)
return jsonify({'msg': '获取成功', 'data': permissions}), 200 return jsonify({'msg': '获取成功', 'data': permissions}), 200
except Exception as e: except Exception as e:
current_app.logger.error(f"Get Permissions Failed: {str(e)}") current_app.logger.error(f"Get Permissions Failed: {str(e)}")

View File

@ -22,12 +22,13 @@ def get_current_user_permissions():
""" """
claims = get_jwt() claims = get_jwt()
user_role = claims.get('role') user_role = claims.get('role')
user_company = claims.get('company_name', '')
if not user_role: if not user_role:
return [] return []
# 超级管理员返回所有字段权限 (忽略大小写) # 超级管理员返回所有字段权限 (忽略大小写)
if user_role.upper() == 'SUPER_ADMIN': if user_role.upper() == 'SUPER_ADMIN':
return ['bom_manage:*'] return ['bom_manage:*']
perm_dict = AuthService.get_user_permissions(user_role) perm_dict = AuthService.get_user_permissions(user_role, company_name=user_company)
# 合并菜单和元素权限 # 合并菜单和元素权限
perms = perm_dict.get('menus', []) + perm_dict.get('elements', []) perms = perm_dict.get('menus', []) + perm_dict.get('elements', [])
return perms return perms

View File

@ -18,17 +18,17 @@ inbound_base_bp = Blueprint('stock_base', __name__)
def get_current_user_permissions(): def get_current_user_permissions():
""" """
返回当前用户拥有的所有权限码列表(包括菜单和元素) 返回当前用户拥有的所有权限码列表(包括菜单和元素)
此函数根据角色查询数据库得到权限。 此函数根据角色+公司查询数据库得到权限。
""" """
from flask_jwt_extended import get_jwt from flask_jwt_extended import get_jwt
from app.services.auth_service import AuthService from app.services.auth_service import AuthService
claims = get_jwt() claims = get_jwt()
user_role = claims.get('role') user_role = claims.get('role')
user_company = claims.get('company_name', '')
if not user_role: if not user_role:
return [] return []
# 超级管理员返回所有字段权限 (忽略大小写) # 超级管理员返回所有字段权限
if user_role.upper() == 'SUPER_ADMIN': if user_role.upper() == 'SUPER_ADMIN':
# 返回通配符权限(供列表脱敏使用)以及所有具体权限(供导出脱敏使用)
return [ return [
'material_list:*', 'material_list:*',
'material_list:id', 'material_list:id',
@ -45,7 +45,7 @@ def get_current_user_permissions():
'material_list:isEnabled', 'material_list:isEnabled',
'material_list:operation' 'material_list:operation'
] ]
perm_dict = AuthService.get_user_permissions(user_role) perm_dict = AuthService.get_user_permissions(user_role, company_name=user_company)
# 合并菜单和元素权限 # 合并菜单和元素权限
perms = perm_dict.get('menus', []) + perm_dict.get('elements', []) perms = perm_dict.get('menus', []) + perm_dict.get('elements', [])
return perms return perms

View File

@ -18,6 +18,7 @@ def get_current_user_permissions():
from app.services.auth_service import AuthService from app.services.auth_service import AuthService
claims = get_jwt() claims = get_jwt()
user_role = claims.get('role') user_role = claims.get('role')
user_company = claims.get('company_name', '')
if not user_role: if not user_role:
return [] return []
# 超级管理员返回所有字段权限 (忽略大小写) # 超级管理员返回所有字段权限 (忽略大小写)
@ -25,7 +26,7 @@ def get_current_user_permissions():
# 返回所有以 inbound_buy: 开头的权限码(这里我们返回一个特殊标记,表示全部) # 返回所有以 inbound_buy: 开头的权限码(这里我们返回一个特殊标记,表示全部)
# 为了简单,我们返回 ['inbound_buy:*'],在过滤函数中特殊处理 # 为了简单,我们返回 ['inbound_buy:*'],在过滤函数中特殊处理
return ['inbound_buy:*'] return ['inbound_buy:*']
perm_dict = AuthService.get_user_permissions(user_role) perm_dict = AuthService.get_user_permissions(user_role, company_name=user_company)
# 合并菜单和元素权限 # 合并菜单和元素权限
perms = perm_dict.get('menus', []) + perm_dict.get('elements', []) perms = perm_dict.get('menus', []) + perm_dict.get('elements', [])
return perms return perms

View File

@ -12,9 +12,10 @@ def get_current_user_permissions():
from app.services.auth_service import AuthService from app.services.auth_service import AuthService
claims = get_jwt() claims = get_jwt()
user_role = claims.get('role') user_role = claims.get('role')
user_company = claims.get('company_name', '')
if not user_role: return [] if not user_role: return []
if user_role.upper() == 'SUPER_ADMIN': return ['inbound_product:*'] if user_role.upper() == 'SUPER_ADMIN': return ['inbound_product:*']
perm_dict = AuthService.get_user_permissions(user_role) perm_dict = AuthService.get_user_permissions(user_role, company_name=user_company)
return perm_dict.get('menus', []) + perm_dict.get('elements', []) return perm_dict.get('menus', []) + perm_dict.get('elements', [])
def filter_item_by_permissions(item_dict, user_permissions): def filter_item_by_permissions(item_dict, user_permissions):

View File

@ -12,9 +12,10 @@ def get_current_user_permissions():
from app.services.auth_service import AuthService from app.services.auth_service import AuthService
claims = get_jwt() claims = get_jwt()
user_role = claims.get('role') user_role = claims.get('role')
user_company = claims.get('company_name', '')
if not user_role: return [] if not user_role: return []
if user_role.upper() == 'SUPER_ADMIN': return ['inbound_semi:*'] if user_role.upper() == 'SUPER_ADMIN': return ['inbound_semi:*']
perm_dict = AuthService.get_user_permissions(user_role) perm_dict = AuthService.get_user_permissions(user_role, company_name=user_company)
return perm_dict.get('menus', []) + perm_dict.get('elements', []) return perm_dict.get('menus', []) + perm_dict.get('elements', [])
def filter_item_by_permissions(item_dict, user_permissions): def filter_item_by_permissions(item_dict, user_permissions):

View File

@ -19,12 +19,13 @@ def get_current_user_permissions():
from app.services.auth_service import AuthService from app.services.auth_service import AuthService
claims = get_jwt() claims = get_jwt()
user_role = claims.get('role') user_role = claims.get('role')
user_company = claims.get('company_name', '')
if not user_role: if not user_role:
return [] return []
# 超级管理员返回所有字段权限 (忽略大小写) # 超级管理员返回所有字段权限 (忽略大小写)
if user_role.upper() == 'SUPER_ADMIN': if user_role.upper() == 'SUPER_ADMIN':
return ['inbound_service:*'] return ['inbound_service:*']
perm_dict = AuthService.get_user_permissions(user_role) perm_dict = AuthService.get_user_permissions(user_role, company_name=user_company)
# 合并菜单和元素权限 # 合并菜单和元素权限
perms = perm_dict.get('menus', []) + perm_dict.get('elements', []) perms = perm_dict.get('menus', []) + perm_dict.get('elements', [])
return perms return perms

View File

@ -20,12 +20,13 @@ def get_current_user_permissions():
from app.services.auth_service import AuthService from app.services.auth_service import AuthService
claims = get_jwt() claims = get_jwt()
user_role = claims.get('role') user_role = claims.get('role')
user_company = claims.get('company_name', '')
if not user_role: if not user_role:
return [] return []
# 超级管理员返回所有字段权限 (忽略大小写) # 超级管理员返回所有字段权限 (忽略大小写)
if user_role.upper() == 'SUPER_ADMIN': if user_role.upper() == 'SUPER_ADMIN':
return ['outbound_list:*'] return ['outbound_list:*']
perm_dict = AuthService.get_user_permissions(user_role) perm_dict = AuthService.get_user_permissions(user_role, company_name=user_company)
# 合并菜单和元素权限 # 合并菜单和元素权限
perms = perm_dict.get('menus', []) + perm_dict.get('elements', []) perms = perm_dict.get('menus', []) + perm_dict.get('elements', [])
return perms return perms
@ -116,12 +117,13 @@ def create_outbound():
# 权限检查:需要 outbound_create:operation 或 outbound_selection:operation 之一 # 权限检查:需要 outbound_create:operation 或 outbound_selection:operation 之一
claims = get_jwt() claims = get_jwt()
user_role = claims.get('role') user_role = claims.get('role')
user_company = claims.get('company_name', '')
if not user_role: if not user_role:
return jsonify({'code': 403, 'msg': '未授权'}), 403 return jsonify({'code': 403, 'msg': '未授权'}), 403
# 超级管理员直接放行 # 超级管理员直接放行
if user_role.upper() != 'SUPER_ADMIN': if user_role.upper() != 'SUPER_ADMIN':
perm_dict = AuthService.get_user_permissions(user_role) perm_dict = AuthService.get_user_permissions(user_role, company_name=user_company)
perms = perm_dict.get('menus', []) + perm_dict.get('elements', []) perms = perm_dict.get('menus', []) + perm_dict.get('elements', [])
if ('outbound_create:operation' not in perms) and ('outbound_selection:operation' not in perms): if ('outbound_create:operation' not in perms) and ('outbound_selection:operation' not in perms):
return jsonify({'code': 403, 'msg': '权限不足'}), 403 return jsonify({'code': 403, 'msg': '权限不足'}), 403

View File

@ -1,32 +1,42 @@
# inventory-backend/app/api/v1/permission.py # inventory-backend/app/api/v1/permission.py
from flask import Blueprint, request, jsonify, current_app from flask import Blueprint, request, jsonify, current_app
from flask_jwt_extended import jwt_required from flask_jwt_extended import jwt_required, get_jwt
from app.services.permission_service import PermissionService from app.services.permission_service import PermissionService
from app.utils.decorators import audit_log from app.utils.decorators import permission_required, audit_log
permission_bp = Blueprint('permission', __name__) permission_bp = Blueprint('permission', __name__)
def _get_operator_company():
"""从 JWT 获取当前操作者公司(None=超管)"""
claims = get_jwt()
role = claims.get('role', '')
if role and role.upper() == 'SUPER_ADMIN':
return None # 超管不限制公司
return claims.get('company_name', '')
@permission_bp.route('/tree', methods=['GET']) @permission_bp.route('/tree', methods=['GET'])
@jwt_required() @jwt_required()
@permission_required('system_permission')
def get_tree(): def get_tree():
"""获取权限树""" """获取权限树"""
try: try:
data = PermissionService.get_permission_tree() data = PermissionService.get_permission_tree()
return jsonify({'code': 200, 'msg': '获取成功', 'data': data}), 200 return jsonify({'code': 200, 'msg': '获取成功', 'data': data}), 200
except Exception as e: except Exception as e:
# 打印详细错误到控制台,方便调试
current_app.logger.error(f"Get Tree Failed: {str(e)}") current_app.logger.error(f"Get Tree Failed: {str(e)}")
# 返回 500 时带上错误信息
return jsonify({'code': 500, 'msg': f'服务器内部错误: {str(e)}'}), 500 return jsonify({'code': 500, 'msg': f'服务器内部错误: {str(e)}'}), 500
@permission_bp.route('/role/<string:role_code>', methods=['GET']) @permission_bp.route('/role/<string:role_code>', methods=['GET'])
@jwt_required() @jwt_required()
@permission_required('system_permission')
def get_role_perms(role_code): def get_role_perms(role_code):
"""获取某个角色的权限列表""" """获取某个角色的权限列表(已选中的)"""
try: try:
data = PermissionService.get_role_permissions(role_code) company_name = _get_operator_company()
data = PermissionService.get_role_permissions(role_code, company_name=company_name)
return jsonify({'code': 200, 'msg': '获取成功', 'data': data}), 200 return jsonify({'code': 200, 'msg': '获取成功', 'data': data}), 200
except Exception as e: except Exception as e:
current_app.logger.error(f"Get Role Perms Failed: {str(e)}") current_app.logger.error(f"Get Role Perms Failed: {str(e)}")
@ -35,19 +45,21 @@ def get_role_perms(role_code):
@permission_bp.route('/assign', methods=['POST']) @permission_bp.route('/assign', methods=['POST'])
@jwt_required() @jwt_required()
@permission_required('system_permission:operation')
@audit_log( @audit_log(
module='权限管理', module='权限管理',
action='分配', action='分配',
get_target_name_fn=lambda: request.get_json().get('role_code') if request.get_json() else None get_target_name_fn=lambda: request.get_json().get('role_code') if request.get_json() else None
) )
def assign_perms(): def assign_perms():
"""保存权限分配""" """保存权限分配(自动带上当前操作者的公司标识)"""
try: try:
data = request.get_json() data = request.get_json()
role_code = data.get('role_code') role_code = data.get('role_code')
permissions = data.get('permissions', []) # list of codes permissions = data.get('permissions', [])
PermissionService.assign_permissions(role_code, permissions) company_name = _get_operator_company()
PermissionService.assign_permissions(role_code, permissions, company_name=company_name)
return jsonify({'code': 200, 'msg': '保存成功'}), 200 return jsonify({'code': 200, 'msg': '保存成功'}), 200
except Exception as e: except Exception as e:
current_app.logger.error(f"Assign Perms Failed: {str(e)}") current_app.logger.error(f"Assign Perms Failed: {str(e)}")

View File

@ -24,11 +24,12 @@ def get_current_user_permissions():
from app.services.auth_service import AuthService from app.services.auth_service import AuthService
claims = get_jwt() claims = get_jwt()
user_role = claims.get('role') user_role = claims.get('role')
user_company = claims.get('company_name', '')
if not user_role: if not user_role:
return [] return []
if user_role.upper() == 'SUPER_ADMIN': if user_role.upper() == 'SUPER_ADMIN':
return ['scrap_list:*'] return ['scrap_list:*']
perm_dict = AuthService.get_user_permissions(user_role) perm_dict = AuthService.get_user_permissions(user_role, company_name=user_company)
perms = perm_dict.get('menus', []) + perm_dict.get('elements', []) perms = perm_dict.get('menus', []) + perm_dict.get('elements', [])
return perms return perms
@ -71,12 +72,13 @@ def scan_barcode():
def create_scrap(): def create_scrap():
claims = get_jwt() claims = get_jwt()
user_role = claims.get('role') user_role = claims.get('role')
user_company = claims.get('company_name', '')
if not user_role: if not user_role:
return jsonify({'code': 403, 'msg': '未授权'}), 403 return jsonify({'code': 403, 'msg': '未授权'}), 403
# 超级管理员直接放行 # 超级管理员直接放行
if user_role.upper() != 'SUPER_ADMIN': if user_role.upper() != 'SUPER_ADMIN':
perm_dict = AuthService.get_user_permissions(user_role) perm_dict = AuthService.get_user_permissions(user_role, company_name=user_company)
perms = perm_dict.get('menus', []) + perm_dict.get('elements', []) perms = perm_dict.get('menus', []) + perm_dict.get('elements', [])
if 'scrap_create:operation' not in perms: if 'scrap_create:operation' not in perms:
return jsonify({'code': 403, 'msg': '权限不足'}), 403 return jsonify({'code': 403, 'msg': '权限不足'}), 403

View File

@ -19,12 +19,13 @@ def get_current_user_permissions():
""" """
claims = get_jwt() claims = get_jwt()
user_role = claims.get('role') user_role = claims.get('role')
user_company = claims.get('company_name', '')
if not user_role: if not user_role:
return [] return []
# 超级管理员返回所有字段权限 (忽略大小写) # 超级管理员返回所有字段权限 (忽略大小写)
if user_role.upper() == 'SUPER_ADMIN': if user_role.upper() == 'SUPER_ADMIN':
return ['*'] return ['*']
perm_dict = AuthService.get_user_permissions(user_role) perm_dict = AuthService.get_user_permissions(user_role, company_name=user_company)
# 合并菜单和元素权限 # 合并菜单和元素权限
perms = perm_dict.get('menus', []) + perm_dict.get('elements', []) perms = perm_dict.get('menus', []) + perm_dict.get('elements', [])
return perms return perms

View File

@ -147,12 +147,13 @@ class SysElement(db.Model):
class SysRolePermission(db.Model): class SysRolePermission(db.Model):
"""角色权限关联表""" """角色权限关联表(支持公司级别隔离)"""
__tablename__ = 'sys_role_permission' __tablename__ = 'sys_role_permission'
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
role_code = db.Column(db.String(50), nullable=False) role_code = db.Column(db.String(50), nullable=False)
target_code = db.Column(db.String(100), nullable=False) # menu_code 或 element_code target_code = db.Column(db.String(100), nullable=False) # menu_code 或 element_code
type = db.Column(db.String(20), nullable=False) # 'menu' 或 'element' type = db.Column(db.String(20), nullable=False) # 'menu' 或 'element'
company_name = db.Column(db.String(255), comment='所属公司(NULL/空/System=全局模板,具体值=公司定制)')
# ========================================== # ==========================================

View File

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

View File

@ -133,7 +133,8 @@ class DifyPermissionService:
""" """
if not role: if not role:
role = DifyPermissionService.get_user_role() role = DifyPermissionService.get_user_role()
return AuthService.get_user_permissions(role) # Dify AI 场景不传 company_name,使用全局模板权限
return AuthService.get_user_permissions(role, company_name=None)
@staticmethod @staticmethod
def get_target_codes(role: str = None) -> list: def get_target_codes(role: str = None) -> list:

View File

@ -216,39 +216,13 @@ class MaterialBaseService:
)) ))
# ============================================================ # ============================================================
# 【行级数据隔离】基于 JWT 中的 company_name 进行过滤 # 【行级数据隔离】基于 JWT 多租户公司过滤
# ============================================================ # ============================================================
from flask_jwt_extended import get_jwt from app.utils.decorators import get_current_company_filter
claims = get_jwt()
user_role = claims.get('role', '').upper() if claims.get('role') else ''
user_company = claims.get('company_name', '')
# 获取用户权限列表(用于检查 global:cross_company_op 特权) company_limit = get_current_company_filter()
from app.api.v1.inbound.base import get_current_user_permissions if company_limit is not None:
user_perms = get_current_user_permissions() or [] query = query.filter(MaterialBase.company_name == company_limit)
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)
# 没选公司则不加过滤,看到全量
category = filters.get('category') category = filters.get('category')
if category is not None and category != '': if category is not None and category != '':
@ -750,32 +724,13 @@ class MaterialBaseService:
MaterialBase.company_name.ilike(kw) MaterialBase.company_name.ilike(kw)
)) ))
# ============================================================ # ============================================================
# 【行级数据隔离】基于 JWT 中的 company_name 进行过滤(高级筛选) # 【行级数据隔离】基于 JWT 多租户公司过滤
# ============================================================ # ============================================================
from flask_jwt_extended import get_jwt from app.utils.decorators import get_current_company_filter
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
req_company = filters.get('company') if filters else None company_limit = get_current_company_filter()
if company_limit is not None:
if user_role != 'SUPER_ADMIN' and not has_cross_company: filter_conditions.append(MaterialBase.company_name == company_limit)
# 普通用户:强制隔离
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)
category = filters.get('category') category = filters.get('category')
if category is not None and category != '': if category is not None and category != '':

View File

@ -396,33 +396,13 @@ class BuyInboundService:
query = query.filter(MaterialBase.material_type == material_type.strip()) query = query.filter(MaterialBase.material_type == material_type.strip())
# ============================================================ # ============================================================
# 【行级数据隔离】基于 JWT 中的 company_name 进行过滤 # 【行级数据隔离】基于 JWT 多租户公司过滤
# ============================================================ # ============================================================
from flask_jwt_extended import get_jwt from app.utils.decorators import get_current_company_filter
claims = get_jwt()
user_role = claims.get('role', '').upper() if claims.get('role') else ''
user_company = claims.get('company_name', '')
# 获取用户权限列表(用于检查 global:cross_company_op 特权) company_limit = get_current_company_filter()
from app.api.v1.inbound.base import get_current_user_permissions if company_limit is not None:
user_perms = get_current_user_permissions() or [] query = query.filter(MaterialBase.company_name == company_limit)
normalized_perms = set(p.lower().replace('_', '').replace(':', '') for p in user_perms)
# 检查是否拥有全局特权或超管角色
has_cross_company = 'globalcrosscompanyop' in normalized_perms
if user_role != 'SUPER_ADMIN' and not has_cross_company:
# 无特权:严禁查其他公司,强制绑定本公司
if company and company.strip() and company.strip() != user_company:
from flask import abort
abort(403, description=f'越权访问:您无权查询 {company} 的数据')
if user_company:
query = query.filter(MaterialBase.company_name == user_company)
elif user_role == 'SUPER_ADMIN' or has_cross_company:
# 有特权:允许下拉框传过来的 company 参数生效
if company and company.strip():
query = query.filter(MaterialBase.company_name == company.strip())
# 4. 状态筛选 # 4. 状态筛选
if not statuses: statuses = ['在库', '借库'] if not statuses: statuses = ['在库', '借库']
@ -451,6 +431,7 @@ class BuyInboundService:
'qty_stock': StockBuy.stock_quantity, 'qty_stock': StockBuy.stock_quantity,
'qty_available': StockBuy.available_quantity, 'qty_available': StockBuy.available_quantity,
'unit_price': StockBuy.pre_tax_unit_price, 'unit_price': StockBuy.pre_tax_unit_price,
'post_tax_unit_price': StockBuy.post_tax_unit_price,
'total_price': StockBuy.total_price, 'total_price': StockBuy.total_price,
'tax_rate': StockBuy.tax_rate, 'tax_rate': StockBuy.tax_rate,
'currency': StockBuy.currency, 'currency': StockBuy.currency,
@ -515,6 +496,7 @@ class BuyInboundService:
'qty_available': StockBuy.available_quantity, 'qty_available': StockBuy.available_quantity,
'warehouse_loc': StockBuy.warehouse_location, 'warehouse_loc': StockBuy.warehouse_location,
'unit_price': StockBuy.pre_tax_unit_price, 'unit_price': StockBuy.pre_tax_unit_price,
'post_tax_unit_price': StockBuy.post_tax_unit_price,
'total_price': StockBuy.total_price, 'total_price': StockBuy.total_price,
'tax_rate': StockBuy.tax_rate, 'tax_rate': StockBuy.tax_rate,
'currency': StockBuy.currency, 'currency': StockBuy.currency,

View File

@ -1,7 +1,7 @@
from app.models.system import SysMenu, SysElement, SysRolePermission from app.models.system import SysMenu, SysElement, SysRolePermission
from app.extensions import db from app.extensions import db
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy import func from sqlalchemy import func, or_
class PermissionService: class PermissionService:
@ -60,83 +60,106 @@ class PermissionService:
return tree_data return tree_data
@staticmethod @staticmethod
def get_role_permissions(role_code): def get_role_permissions(role_code, company_name=None):
"""获取指定角色拥有的所有权限Code""" """
获取指定角色拥有的所有权限Code(支持公司级别隔离)
参数:
company_name — None=不过滤, 具体值=查公司定制+全局模板权限
"""
try: try:
# === 新增逻辑:超级管理员上帝模式 === # === 超级管理员上帝模式 ===
if role_code == 'SUPER_ADMIN': if role_code == 'SUPER_ADMIN':
# 直接获取所有可见菜单和元素,无视配置表
all_menus = [m.code for m in SysMenu.query.filter(SysMenu.is_visible == True).all()] all_menus = [m.code for m in SysMenu.query.filter(SysMenu.is_visible == True).all()]
all_elements = [e.code for e in SysElement.query.all()] all_elements = [e.code for e in SysElement.query.all()]
return { return {
'menus': all_menus, 'menus': all_menus,
'elements': all_elements 'elements': all_elements
} }
# =================================
perms = SysRolePermission.query.filter_by(role_code=role_code).all() # 构建公司过滤条件
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
perms = SysRolePermission.query.filter(
SysRolePermission.role_code == role_code,
company_filter
).all()
menu_codes = [] menu_codes = []
element_codes = [] element_codes = []
for p in perms: for p in perms:
# 这里假设你的数据库存的是 target_code
if p.type == 'menu': if p.type == 'menu':
menu_codes.append(p.target_code) menu_codes.append(p.target_code)
else: else:
element_codes.append(p.target_code) element_codes.append(p.target_code)
# 前端 handleRoleSelect 会合并这两个数组,所以分开返回没问题
return { return {
'menus': menu_codes, 'menus': menu_codes,
'elements': element_codes 'elements': element_codes
} }
except Exception as e: except Exception as e:
# 记录日志或处理错误
print(f"Error fetching role permissions: {e}") print(f"Error fetching role permissions: {e}")
return {'menus': [], 'elements': []} return {'menus': [], 'elements': []}
@staticmethod @staticmethod
def assign_permissions(role_code, permissions): def assign_permissions(role_code, permissions, company_name=None):
""" """
保存角色的权限 保存角色的权限(支持公司级别隔离)
permissions: 前端传来的 list,混合了 menu_code 和 element_code
参数:
role_code — 角色代码
permissions — 前端传来的 list,混合了 menu_code 和 element_code
company_name — 所属公司(None=全局模板,具体值=公司定制权限)
""" """
if not role_code: if not role_code:
raise ValueError("角色代码不能为空") raise ValueError("角色代码不能为空")
session = db.session session = db.session
try: try:
# 1. 开启事务 (Flask-SQLAlchemy 自动管理,但明确逻辑更好) # 1. 删除该角色+该公司的旧权限(只删同公司的,不影响全局模板和其他公司)
if company_name:
old_perms = SysRolePermission.query.filter_by(
role_code=role_code, company_name=company_name
).all()
else:
# 公司为空时兼容旧逻辑:删除 company_name 为 NULL 的旧权限
old_perms = SysRolePermission.query.filter(
SysRolePermission.role_code == role_code,
or_(
SysRolePermission.company_name.is_(None),
SysRolePermission.company_name == ''
)
).all()
# 2. 删除该角色旧的所有权限(改为对象级删除以触发审计事件)
old_perms = SysRolePermission.query.filter_by(role_code=role_code).all()
for p in old_perms: for p in old_perms:
db.session.delete(p) db.session.delete(p)
# 3. 准备新数据 # 2. 准备新数据
if permissions: if permissions:
# 3.1 去重
unique_codes = set(permissions) unique_codes = set(permissions)
# 3.2 预加载所有 Menu Code,用于区分是 Menu 还是 Element
# 这一步很重要,因为 SysRolePermission 表需要 type 字段
all_menu_codes = {res[0] for res in session.query(SysMenu.code).all()} all_menu_codes = {res[0] for res in session.query(SysMenu.code).all()}
new_records = [] new_records = []
for code in unique_codes: for code in unique_codes:
if not code: continue if not code: continue
# 判断类型:如果 code 存在于菜单表中,就是 menu,否则就是 element
p_type = 'menu' if code in all_menu_codes else 'element' p_type = 'menu' if code in all_menu_codes else 'element'
new_records.append(SysRolePermission( new_records.append(SysRolePermission(
role_code=role_code, role_code=role_code,
target_code=code, target_code=code,
type=p_type type=p_type,
company_name=company_name # ★ 存储公司标识
)) ))
# 3.3 批量插入
if new_records: if new_records:
session.add_all(new_records) session.add_all(new_records)

View File

@ -149,9 +149,12 @@ def permission_required(permission_code):
if user_role and user_role.upper() == 'SUPER_ADMIN': if user_role and user_role.upper() == 'SUPER_ADMIN':
return fn(*args, **kwargs) return fn(*args, **kwargs)
# 获取当前用户公司,用于权限隔离
user_company = claims.get('company_name', '')
try: try:
from app.services.auth_service import AuthService from app.services.auth_service import AuthService
perm_dict = AuthService.get_user_permissions(user_role) perm_dict = AuthService.get_user_permissions(user_role, company_name=user_company)
except Exception as e: except Exception as e:
logging.warning(f"Failed to fetch permissions for role {user_role}: {e}") logging.warning(f"Failed to fetch permissions for role {user_role}: {e}")
return jsonify(msg='权限查询失败'), 403 return jsonify(msg='权限查询失败'), 403
@ -164,6 +167,46 @@ def permission_required(permission_code):
return decorator return decorator
return wrapper return wrapper
def get_current_company_filter():
"""
多租户数据权限隔离工具函数。
根据当前 JWT 中的 role 和 company_name 决定 SQL 查询应过滤到哪个公司。
返回值:
None → 不限制公司(超级管理员全局视角,或显式传了 'ALL')
str → 仅查询该公司数据
使用示例:
company_limit = get_current_company_filter()
if company_limit is not None:
query = query.filter(MaterialBase.company_name == company_limit)
"""
from flask import request
claims = get_jwt()
user_role = claims.get('role', '')
# 规范化角色为大写
if user_role:
user_role = user_role.upper()
# 从请求参数获取前端指定的公司(兼容 company_name 和 company 两个参数名)
req_company = request.args.get('company_name', '') or request.args.get('company', '')
if user_role == 'SUPER_ADMIN':
# 超级管理员:允许通过参数指定公司过滤
if req_company and req_company.strip().upper() != 'ALL':
return req_company.strip()
# 未指定或指定了 ALL → 返回 None(全量,不限制)
return None
else:
# 其他角色(SUPERVISOR / FINANCE / WAREHOUSE_MGR 等):
# 强制隔离到 JWT 中记录的公司,无视前端参数
user_company = claims.get('company_name', '')
return user_company if user_company else None
def audit_log(module: str = None, action: str = None, get_target_id_fn=None, get_target_name_fn=None, get_details_fn=None): def audit_log(module: str = None, action: str = None, get_target_id_fn=None, get_target_name_fn=None, get_details_fn=None):
""" """
已废弃! 已废弃!

View File

@ -35,11 +35,12 @@ export function getUserInfo() {
}) })
} }
// 获取所有用户列表 // 获取所有用户列表(支持公司过滤参数)
export function getUserList() { export function getUserList(params?: any) {
return request({ return request({
url: '/v1/auth/users', url: '/v1/auth/users',
method: 'get' method: 'get',
params
}) })
} }

View File

@ -21,6 +21,7 @@
</el-input> </el-input>
<el-select <el-select
v-if="isSuperAdmin"
v-model="queryParams.company" v-model="queryParams.company"
placeholder="所属公司" placeholder="所属公司"
clearable clearable
@ -29,6 +30,7 @@
style="width: 120px; margin-right: 10px;" style="width: 120px; margin-right: 10px;"
@change="handleQuery" @change="handleQuery"
> >
<el-option label="全部 (跨域)" value="ALL" />
<el-option v-for="item in companyOptions" :key="item" :label="item" :value="item" /> <el-option v-for="item in companyOptions" :key="item" :label="item" :value="item" />
</el-select> </el-select>
@ -699,6 +701,7 @@ import ImageSearchDialog from '@/components/ImageSearchDialog.vue';
import { imageSearch as imageSearchApi, type ImageSearchItem } from '@/api/common/upload'; import { imageSearch as imageSearchApi, type ImageSearchItem } from '@/api/common/upload';
const userStore = useUserStore(); const userStore = useUserStore();
const isSuperAdmin = computed(() => userStore.role === 'SUPER_ADMIN');
// --- 类型定义 --- // --- 类型定义 ---
interface MaterialBaseVO { interface MaterialBaseVO {
@ -1071,7 +1074,7 @@ const queryParams = reactive<QueryParams>({
searchField: 'all', searchField: 'all',
category: '', category: '',
type: '', type: '',
company: '', company: 'ALL',
isEnabled: undefined, isEnabled: undefined,
orderByColumn: '', orderByColumn: '',
isAsc: undefined, isAsc: undefined,
@ -1296,7 +1299,7 @@ const resetQuery = () => {
queryParams.searchField = 'all'; queryParams.searchField = 'all';
queryParams.category = ''; queryParams.category = '';
queryParams.type = ''; queryParams.type = '';
queryParams.company = ''; queryParams.company = isSuperAdmin.value ? 'ALL' : '';
queryParams.isEnabled = undefined; queryParams.isEnabled = undefined;
queryParams.orderByColumn = ''; queryParams.orderByColumn = '';
queryParams.isAsc = undefined; queryParams.isAsc = undefined;

View File

@ -4,6 +4,7 @@
<div class="search-form-area" style="flex-wrap: wrap;"> <div class="search-form-area" style="flex-wrap: wrap;">
<el-select <el-select
v-if="isSuperAdmin"
v-model="queryParams.company" v-model="queryParams.company"
placeholder="所属公司" placeholder="所属公司"
class="filter-item-select" class="filter-item-select"
@ -12,6 +13,7 @@
@change="fetchData" @change="fetchData"
style="width: 160px;" style="width: 160px;"
> >
<el-option label="全部 (跨域)" value="ALL" />
<el-option v-for="item in companyOptions" :key="item" :label="item" :value="item" /> <el-option v-for="item in companyOptions" :key="item" :label="item" :value="item" />
</el-select> </el-select>
@ -229,7 +231,7 @@
</el-link> </el-link>
</template> </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> <span class="money-text">{{ formatMoney(scope.row[col.prop], scope.row.currency) }}</span>
</template> </template>
</el-table-column> </el-table-column>
@ -765,6 +767,7 @@ const hasFormFieldPermission = (fieldName: string) => {
available_quantity: 'inbound_buy:available_quantity', available_quantity: 'inbound_buy:available_quantity',
warehouse_location: 'inbound_buy:warehouse_location', warehouse_location: 'inbound_buy:warehouse_location',
unit_price: 'inbound_buy:unit_price', unit_price: 'inbound_buy:unit_price',
post_tax_unit_price: 'inbound_buy:post_tax_unit_price',
tax_rate: 'inbound_buy:tax_rate', tax_rate: 'inbound_buy:tax_rate',
total_price: 'inbound_buy:total_price', total_price: 'inbound_buy:total_price',
currency: 'inbound_buy:currency', currency: 'inbound_buy:currency',
@ -790,6 +793,7 @@ const hasFormFieldPermission = (fieldName: string) => {
// 状态与变量 // 状态与变量
// ------------------------------------ // ------------------------------------
const userStore = useUserStore() const userStore = useUserStore()
const isSuperAdmin = computed(() => userStore.role === 'SUPER_ADMIN')
const loading = ref(false) const loading = ref(false)
const submitting = ref(false) const submitting = ref(false)
const visible = ref(false) const visible = ref(false)
@ -826,7 +830,7 @@ const queryParams = reactive({
sku: '', sku: '',
category: '', category: '',
material_type: '', material_type: '',
company: '', company: 'ALL',
statuses: ['在库', '借库'], statuses: ['在库', '借库'],
orderByColumn: '', orderByColumn: '',
isAsc: undefined as string | undefined, isAsc: undefined as string | undefined,
@ -885,6 +889,7 @@ const fieldOptions = computed(() => {
{ value: 'qty_stock', label: '库存数', perm: 'inbound_buy:qty_stock' }, { value: 'qty_stock', label: '库存数', perm: 'inbound_buy:qty_stock' },
{ value: 'qty_available', label: '可用数', perm: 'inbound_buy:qty_available' }, { value: 'qty_available', label: '可用数', perm: 'inbound_buy:qty_available' },
{ value: 'unit_price', label: '不含税单价', perm: 'inbound_buy:unit_price' }, { 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: 'total_price', label: '不含税总价', perm: 'inbound_buy:total_price' },
{ value: 'tax_rate', label: '税率', perm: 'inbound_buy:tax_rate' }, { value: 'tax_rate', label: '税率', perm: 'inbound_buy:tax_rate' },
{ value: 'currency', label: '币种', perm: 'inbound_buy:currency' }, { value: 'currency', label: '币种', perm: 'inbound_buy:currency' },
@ -936,6 +941,7 @@ const stockColumns = [
{prop: 'tax_rate', label: '税率', minWidth: '80'}, {prop: 'tax_rate', label: '税率', minWidth: '80'},
{prop: 'unit_price', label: '不含税单价', minWidth: '120'}, {prop: 'unit_price', label: '不含税单价', minWidth: '120'},
{prop: 'post_tax_unit_price', label: '含税单价', minWidth: '120'},
{prop: 'total_price', label: '不含税总价', minWidth: '120'}, {prop: 'total_price', label: '不含税总价', minWidth: '120'},
{prop: 'currency', label: '币种', minWidth: '80'}, {prop: 'currency', label: '币种', minWidth: '80'},
@ -973,6 +979,7 @@ const permissionMap: Record<string, string> = {
warehouse_loc: 'inbound_buy:warehouse_loc', warehouse_loc: 'inbound_buy:warehouse_loc',
tax_rate: 'inbound_buy:tax_rate', tax_rate: 'inbound_buy:tax_rate',
unit_price: 'inbound_buy:unit_price', unit_price: 'inbound_buy:unit_price',
post_tax_unit_price: 'inbound_buy:post_tax_unit_price',
total_price: 'inbound_buy:total_price', total_price: 'inbound_buy:total_price',
currency: 'inbound_buy:currency', currency: 'inbound_buy:currency',
exchange_rate: 'inbound_buy:exchange_rate', exchange_rate: 'inbound_buy:exchange_rate',
@ -1409,7 +1416,7 @@ const resetQuery = () => {
queryParams.sku = '' queryParams.sku = ''
queryParams.category = '' queryParams.category = ''
queryParams.material_type = '' queryParams.material_type = ''
queryParams.company = '' queryParams.company = isSuperAdmin.value ? 'ALL' : ''
queryParams.page = 1 queryParams.page = 1
fetchData() fetchData()
} }
@ -1677,7 +1684,7 @@ const resetAdvancedFilter = () => {
fetchData() fetchData()
} }
const isColumnSortable = (prop: string) => { 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) return sortableColumns.includes(prop)
} }
const handleSortChange = ({ column, prop, order }: any) => { const handleSortChange = ({ column, prop, order }: any) => {

View File

@ -15,6 +15,23 @@
</div> </div>
</template> </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 <el-table
v-loading="tableLoading" v-loading="tableLoading"
:data="tableData" :data="tableData"
@ -104,7 +121,7 @@
/> />
</el-form-item> </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 <el-select
v-model="form.department" v-model="form.department"
placeholder="请输入或选择部门" placeholder="请输入或选择部门"
@ -147,7 +164,7 @@
<!-- 批量新增弹窗 --> <!-- 批量新增弹窗 -->
<el-dialog v-model="batchDialogVisible" title="批量新增员工" width="600px" destroy-on-close @close="batchForm.namesText = ''"> <el-dialog v-model="batchDialogVisible" title="批量新增员工" width="600px" destroy-on-close @close="batchForm.namesText = ''">
<el-form :model="batchForm" label-width="100px"> <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-select v-model="batchForm.department" style="width: 100%" placeholder="请选择部门">
<el-option v-for="d in departmentOptions" :key="d" :label="d" :value="d" /> <el-option v-for="d in departmentOptions" :key="d" :label="d" :value="d" />
</el-select> </el-select>
@ -194,6 +211,11 @@ import { ElMessage } from 'element-plus'
import { pinyin } from 'pinyin-pro' // ★ 务必安装: npm install pinyin-pro import { pinyin } from 'pinyin-pro' // ★ 务必安装: npm install pinyin-pro
const userStore = useUserStore() const userStore = useUserStore()
const isSuperAdmin = computed(() => userStore.role === 'SUPER_ADMIN')
// 查询参数 & 公司列表
const queryParams = reactive({ company: 'ALL' })
const companyOptions = ref<string[]>([])
// 列与权限Code的映射关系(数据库中的code) // 列与权限Code的映射关系(数据库中的code)
const permissionMap: Record<string, string> = { const permissionMap: Record<string, string> = {
@ -327,7 +349,9 @@ const rules = computed(() => {
{ validator: validateNameStrict, trigger: 'blur' } { validator: validateNameStrict, trigger: 'blur' }
], ],
role: [{ required: true, message: '请选择角色', trigger: 'change' }], role: [{ required: true, message: '请选择角色', trigger: 'change' }],
department: [{ required: true, message: '请输入或选择部门', trigger: ['blur', 'change'] }], department: isSuperAdmin.value
? [{ required: true, message: '请输入或选择部门', trigger: ['blur', 'change'] }]
: [],
email: [ email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' }, { required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱格式', trigger: ['blur', 'change'] } { type: 'email', message: '请输入正确的邮箱格式', trigger: ['blur', 'change'] }
@ -357,11 +381,18 @@ const rules = computed(() => {
const getList = async () => { const getList = async () => {
tableLoading.value = true tableLoading.value = true
try { 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 || [] tableData.value = res.data || []
extractDepartments(tableData.value) 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) { } catch (error) {
// 错误已由全局拦截器统一处理
console.error('Fetch users failed:', error) console.error('Fetch users failed:', error)
} finally { } finally {
tableLoading.value = false tableLoading.value = false
@ -448,8 +479,11 @@ const onSubmit = async () => {
// 批量提交逻辑 // 批量提交逻辑
const handleBatchSubmit = async () => { const handleBatchSubmit = async () => {
if (!batchForm.department || !batchForm.role || !batchForm.namesText.trim()) { if (!batchForm.role || !batchForm.namesText.trim()) {
return ElMessage.warning('请填写完整部门、角色及员工名单') return ElMessage.warning('请填写完整角色及员工名单')
}
if (isSuperAdmin.value && !batchForm.department) {
return ElMessage.warning('请填写所属部门')
} }
const names = batchForm.namesText.split('\n').map(n => n.trim()).filter(n => n) const names = batchForm.namesText.split('\n').map(n => n.trim()).filter(n => n)