Files
SMANAGE/gvsds/apps/gerp/views/form_api.py
T
TermiNexus 07e44eefc6 feat(gerp): 员工列表支持按需统计去重用户数
部门详情「部门员工」与公司详情「员工」卡片标题需要显示「角色数:x/用户数:y」。

get_table 新增 WithUserCount 参数:带该参数时额外统计去重用户数(UserRole 直接按
UserUUID 去重;UserRoleDepartment/UserRoleWorkgroup 只存 UserRoleUUID 且非外键,
需回查 UserRole 后再去重)。默认不统计,避免给所有列表加一次查询。

Get/List 响应新增 _FormListResponse:仅在请求了计数时改为 {list, total, user_count}
对象结构 —— 前端 axios 拦截器只透传 data,计数挂在 data 之外读不到;不带该参数时
仍返回原有的数组结构,既有调用方行为不变。
2026-10-01 23:15:50 +08:00

720 lines
32 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import logging
import uuid as _uuid
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from gvsdsdk.model_utils import UUIDObj
from . import ELTAuthenticatedAPIView
from gvsds.apps.gerp.form_service import FormService
logger = logging.getLogger(__name__)
def _get_current_context(request):
"""从中间件注入的 request 属性获取当前角色/公司/租户上下文。
返回 (current_role_uuid_bytes, current_company_uuid_bytes, current_tenant_uuid_bytes)。
依赖 CurrentRoleMiddleware 已设置 request.current_role / current_company_uuid / current_tenant_uuid。
"""
current_role_uuid = None
current_role = getattr(request, 'current_role', None)
if current_role:
current_role_uuid = getattr(current_role, 'RoleUUID', None)
current_company_uuid = None
company = getattr(request, 'current_company_uuid', None)
if company is not None:
if hasattr(company, 'pk'):
current_company_uuid = company.pk
elif hasattr(company, 'bytes'):
current_company_uuid = company.bytes
current_tenant_uuid = getattr(request, 'current_tenant_uuid', None)
return current_role_uuid, current_company_uuid, current_tenant_uuid
def _is_admin(request):
"""判断用户是否为管理员(admin:super 或 admin:tenant,委托给 IsAdmin)"""
from gvsdsdk.auth.admin_service import IsAdmin
return IsAdmin(request)
def _is_super_admin(request):
"""判断用户是否为跨租户管理员(仅 admin:super,委托给 IsSuperAdmin)
用于租户隔离判定:仅 admin:super 关闭租户隔离。
"""
from gvsdsdk.auth.admin_service import IsSuperAdmin
return IsSuperAdmin(request)
def _is_tenant_admin(request):
"""判断用户是否为租户内管理员(仅 admin:tenant,委托给 IsTenantAdmin)
用于特权级判定:admin:tenant 关闭公司隔离,但保留租户隔离。
"""
from gvsdsdk.auth.admin_service import IsTenantAdmin
return IsTenantAdmin(request)
def _is_company_admin(request):
"""判断用户是否为公司管理员及以上(admin:company / admin:tenant / admin:super)
委托给 IsCompanyAdmin:租户管理员与跨租户管理员自动继承公司管理员能力。
"""
from gvsdsdk.auth.admin_service import IsCompanyAdmin
return IsCompanyAdmin(request)
def _make_service(request, user):
"""构造 FormService,自动传入当前公司 UUID、角色 UUID 和租户 UUID。
上下文来自 CurrentRoleMiddleware 注入的 request 属性。
"""
admin_verify = request.query_params.get('admin_verify') == '1' if hasattr(request, 'query_params') else False
if not admin_verify and hasattr(request, 'data'):
admin_verify = request.data.get('admin_verify') == '1'
current_role_uuid, current_company_uuid, current_tenant_uuid = _get_current_context(request)
# 获取客户端 IP 地址(用于审计日志)
ip_address = ''
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip_address = x_forwarded_for.split(',')[0].strip()
else:
ip_address = request.META.get('REMOTE_ADDR', '')
# 计算角色作用域(管理员不应用作用域过滤)
role_scope = None
if not _is_admin(request):
from gvsds.apps.gerp.role_scope import GetUserScope
try:
role_scope = GetUserScope(request)
except Exception:
role_scope = None
return FormService(
user,
current_company_uuid=current_company_uuid,
admin_verify=admin_verify,
current_role_uuid=current_role_uuid,
current_tenant_uuid=current_tenant_uuid,
ip_address=ip_address,
role_scope=role_scope,
)
# 动作 → RBAC 权限码后缀映射
_ACTION_PERM_SUFFIX = {
'query': 'read', 'list': 'read', 'get': 'read',
'columns': 'read', 'tables': 'read', 'code_display': 'read',
'add': 'create',
'modify': 'update', 'modify_item': 'update',
'delete': 'delete',
}
def _check_rbac_permission(table_name, action, request):
"""RBAC 权限码校验:构造 {Table}:{suffix} 权限码,检查当前角色是否拥有。
返回 (error_response, perm_defined):
- perm_defined=False: 权限码未定义或当前角色未参与该表的 RBAC 授权,
调用方应回退到黑名单/只读约束
- perm_defined=True, error_response=None: 权限码存在且当前角色已分配 → 放行
- perm_defined=True, error_response=Response: 权限码存在且当前角色参与了该表的
RBAC 授权(分配了该表任一权限码)但未分配当前动作的权限码 → 拒绝
设计意图:RBAC 是"为特定角色授权"的手段,而非"锁定未授权角色"。
只有当当前角色已分配该表的任一权限码时 RBAC 才生效(限定其操作范围);
角色完全未分配该表权限码时回退到原有黑名单/只读约束,避免注册权限码即全局锁定。
"""
suffix = _ACTION_PERM_SUFFIX.get(action)
if not suffix:
return None, False
perm_code = f'{table_name}:{suffix}'
from gvsdsdk.auth.managers import RoleManager
current_role_uuid, current_company_uuid, _ = _get_current_context(request)
granted, decided = RoleManager.CheckTablePermission(
RoleUUID=current_role_uuid,
PermCode=perm_code,
CompanyUUID=current_company_uuid or None,
TableName=table_name,
)
if not decided:
# 权限码未定义或角色未参与该表授权:回退到原有逻辑(黑名单/只读约束兜底)
return None, False
if granted:
return None, True
# 明确拒绝:角色已纳入该表 RBAC 授权,但缺少当前动作权限
return Response({'code': 403, 'msg': f'无权操作:当前角色未分配权限 {perm_code}'}, status=403), True
def _check_table_access(table_name, action, request):
"""检查用户对指定表的访问权限,返回错误响应或 None
校验顺序:
1. 管理员直接放行
2. RBAC 权限码校验:
- 权限码已定义 → RBAC 校验优先(通过则放行,未通过则拒绝,跳过黑名单/只读约束)
- 权限码未定义 → 回退到黑名单/只读约束
3. ADMIN_ONLY_TABLES 黑名单(仅权限码未定义时生效)
4. READONLY_TABLES_FOR_NORMAL 只读约束(仅权限码未定义时生效)
设计意图:当管理员为某表配置了 RBAC 权限码并分配给公司负责人等角色时,
该角色即可在权限范围内(受公司隔离限制)操作该表,不再受 READONLY 硬编码约束。
"""
if _is_admin(request):
return None
# RBAC 权限码校验
rbac_error, perm_defined = _check_rbac_permission(table_name, action, request)
if perm_defined:
# RBAC 驱动模式:权限码已定义,RBAC 校验结果决定是否放行
return rbac_error
# 回退模式:权限码未定义,使用原有黑名单/只读约束
if table_name in FormService.ADMIN_ONLY_TABLES:
return Response({'code': 403, 'msg': f'无权访问表 {table_name},仅管理员可操作'}, status=403)
if table_name in FormService.READONLY_TABLES_FOR_NORMAL:
if action not in ('query', 'list', 'get', 'columns', 'tables', 'code_display'):
return Response({'code': 403, 'msg': f'表 {table_name} 为只读,普通用户不可增删改'}, status=403)
return None
def _FormListResponse(result):
"""Get/List 的统一响应体
默认沿用历史结构(data=记录数组、total 平铺),不动既有调用方的解析;
仅当请求带 WithUserCount(员工卡片「角色数:x/用户数:y」)时改为对象结构 ——
前端 axios 响应拦截器只透传 `data`,计数这类元数据挂在 data 之外是读不到的。
"""
if result.get('user_count') is None:
return {'code': 0, 'data': result['records'], 'total': result['total']}
return {
'code': 0,
'data': {
'list': result['records'],
'total': result['total'],
'user_count': result['user_count'],
},
'total': result['total'],
}
class FormAPICompatView(ELTAuthenticatedAPIView):
def patch(self, request, **kwargs):
return self.post(request, **kwargs)
def put(self, request, **kwargs):
return self.post(request, **kwargs)
def delete(self, request, **kwargs):
return self.post(request, **kwargs)
def get(self, request, **kwargs):
user = request.user if hasattr(request, 'user') and request.user.is_authenticated else None
service = _make_service(request, user)
path = request.path.rstrip('/')
table_name = kwargs.get('TableName', '')
if path.endswith('/Tables'):
result = service.get_tables()
return Response({'code': 0, 'data': result})
elif '/Columns/' in path and table_name:
result = service.get_columns(table_name)
return Response({'code': 0, 'data': result})
elif '/Get/' in path and table_name:
access_error = _check_table_access(table_name, 'get', request)
if access_error:
return access_error
result = service.get_table(table_name, request.query_params)
return Response(_FormListResponse(result))
elif '/CodeDisplay' in path:
result = service.code_display(request.query_params.dict())
return Response({'code': 0, 'data': result})
table_name = request.query_params.get('Table', '')
if not table_name:
return Response({'code': 1, 'msg': '缺少table参数'}, status=400)
access_error = _check_table_access(table_name, 'get', request)
if access_error:
return access_error
result = service.get_table(table_name, request.query_params)
return Response(_FormListResponse(result))
def post(self, request, **kwargs):
user = request.user if hasattr(request, 'user') and request.user.is_authenticated else None
service = _make_service(request, user)
path = request.path.rstrip('/')
table_name = kwargs.get('TableName', '')
item_id = kwargs.get('id', '')
# 批量职务调整:多员工 × 多职务 批量增加/移除(职务统一落 UserRoleDepartmentPosition 子表)
if path.endswith('/BatchPositions'):
table_name = request.data.get('Table', '')
access_error = _check_table_access(table_name, 'modify', request)
if access_error:
return access_error
result = service.batch_positions(request.data)
if isinstance(result, dict) and result.get('error'):
return Response({'code': 1, 'msg': result['error']}, status=400)
return Response({'code': 0, 'msg': '批量职务调整完成', 'data': result})
if '/Add/' in path and table_name:
access_error = _check_table_access(table_name, 'add', request)
if access_error:
return access_error
data = dict(request.data)
data['Table'] = table_name
# 合并 URL query 参数到 data,支持子表关联字段从 URL 传入
# (如 employee-detail 的 UserRole 新增时通过 ?UserUUID=xxx 关联当前用户)
# request.data 优先级高于 query_params,避免覆盖用户显式输入
_RESERVED_QUERY_KEYS = ('server', 'Table', 'table', 'Action', 'action', 'PK', 'pk', 'UUID', 'uuid')
for k, v in request.query_params.items():
if k in _RESERVED_QUERY_KEYS:
continue
if k not in data:
data[k] = v
logger.warning('[FormAPI/Add/%s] query_params=%s, merged_data=%s', table_name, dict(request.query_params), data)
result = service.add_record(data)
if result is None:
return Response({'code': 1, 'msg': '添加失败,表不存在或无权限'}, status=400)
if isinstance(result, dict) and 'error' in result:
return Response({'code': 1, 'msg': result['error'], 'field': result.get('field', '')}, status=400)
return Response({'code': 0, 'msg': '添加成功', 'data': result}, status=201)
elif '/ModifyItem/' in path and table_name:
access_error = _check_table_access(table_name, 'modify', request)
if access_error:
return access_error
data = dict(request.data)
data['Table'] = table_name
if item_id:
data['PK'] = item_id
result = service.modify_item(data)
if result is None:
return Response({'code': 1, 'msg': '修改失败,记录不存在'}, status=404)
if isinstance(result, dict) and 'error' in result:
return Response({'code': 1, 'msg': result['error'], 'field': result.get('field', '')}, status=400)
return Response({'code': 0, 'msg': '修改成功', 'data': result})
elif '/Modify/' in path and table_name:
access_error = _check_table_access(table_name, 'modify', request)
if access_error:
return access_error
data = dict(request.data)
data['Table'] = table_name
if item_id:
data['PK'] = item_id
result = service.modify_record(data)
if result is None:
return Response({'code': 1, 'msg': '修改失败,记录不存在'}, status=404)
if isinstance(result, dict) and 'error' in result:
return Response({'code': 1, 'msg': result['error'], 'field': result.get('field', '')}, status=400)
return Response({'code': 0, 'msg': '修改成功', 'data': result})
elif '/Delete/' in path and table_name:
access_error = _check_table_access(table_name, 'delete', request)
if access_error:
return access_error
data = dict(request.data)
data['Table'] = table_name
if item_id:
data['PK'] = item_id
result = service.delete_record(data)
if isinstance(result, dict) and result.get('error'):
return Response({'code': 1, 'msg': result['error']}, status=400)
if not result:
return Response({'code': 1, 'msg': '删除失败,记录不存在'}, status=404)
return Response({'code': 0, 'msg': '删除成功'})
action = request.data.get('Action', 'add')
table_name = request.data.get('Table', '')
if table_name:
access_error = _check_table_access(table_name, action, request)
if access_error:
return access_error
if action == 'add':
result = service.add_record(request.data)
return Response({'code': 0, 'msg': '添加成功', 'data': result}, status=201)
elif action == 'modify':
result = service.modify_record(request.data)
if result is None:
return Response({'code': 1, 'msg': '修改失败'}, status=404)
return Response({'code': 0, 'msg': '修改成功', 'data': result})
elif action == 'modify_item':
result = service.modify_item(request.data)
if result is None:
return Response({'code': 1, 'msg': '修改失败'}, status=404)
return Response({'code': 0, 'msg': '修改成功', 'data': result})
elif action == 'delete':
result = service.delete_record(request.data)
if isinstance(result, dict) and result.get('error'):
return Response({'code': 1, 'msg': result['error']}, status=400)
if not result:
return Response({'code': 1, 'msg': '删除失败'}, status=404)
return Response({'code': 0, 'msg': '删除成功'})
elif action == 'columns':
table_name = request.data.get('Table', '')
result = service.get_columns(table_name)
return Response({'code': 0, 'data': result})
elif action == 'tables':
result = service.get_tables()
return Response({'code': 0, 'data': result})
elif action == 'code_display':
result = service.code_display(request.data)
return Response({'code': 0, 'data': result})
return Response({'code': 1, 'msg': '未知操作'}, status=400)
class FormAPIView(ELTAuthenticatedAPIView):
def patch(self, request):
return self.post(request)
def put(self, request):
return self.post(request)
def delete(self, request):
return self.post(request)
def get(self, request):
table_name = request.query_params.get('table', '')
if not table_name:
return Response({'code': 1, 'msg': '缺少table参数'}, status=400)
access_error = _check_table_access(table_name, 'get', request)
if access_error:
return access_error
service = _make_service(request, request.user)
result = service.get_table(table_name, request.query_params)
return Response(_FormListResponse(result))
def post(self, request):
action = request.data.get('Action', 'add')
table_name = request.data.get('Table', '')
if table_name:
access_error = _check_table_access(table_name, action, request)
if access_error:
return access_error
service = _make_service(request, request.user)
if action == 'batch_positions':
result = service.batch_positions(request.data)
if isinstance(result, dict) and result.get('error'):
return Response({'code': 1, 'msg': result['error']}, status=400)
return Response({'code': 0, 'msg': '批量职务调整完成', 'data': result})
elif action == 'add':
result = service.add_record(request.data)
return Response({'code': 0, 'msg': '添加成功', 'data': result}, status=201)
elif action == 'modify':
result = service.modify_record(request.data)
return Response({'code': 0, 'msg': '修改成功', 'data': result})
elif action == 'modify_item':
result = service.modify_item(request.data)
return Response({'code': 0, 'msg': '修改成功', 'data': result})
elif action == 'delete':
result = service.delete_record(request.data)
return Response({'code': 0, 'msg': '删除成功'})
elif action == 'columns':
table_name = request.data.get('Table', '')
result = service.get_columns(table_name)
return Response({'code': 0, 'data': result})
elif action == 'tables':
result = service.get_tables()
return Response({'code': 0, 'data': result})
elif action == 'code_display':
result = service.code_display(request.data)
return Response({'code': 0, 'data': result})
return Response({'code': 1, 'msg': '未知操作'}, status=400)
class CheckAdminView(ELTAuthenticatedAPIView):
"""检查当前用户是否具有管理员特权(超级用户或当前角色拥有 admin:super 或 admin:tenant 权限)
额外返回 role_scope 信息,用于前端按角色作用域差异化显示:
- admin: 管理员(看全公司)
- dept_manager: 部门负责人(看本部门成员)
- workgroup_lead: 工作组长(看本工作组成员)
- member: 普通组员(只看自己)
"""
# 角色作用域类型 → 中文标签
SCOPE_TYPE_LABEL = {
'admin': '管理员',
'dept_manager': '部门负责人',
'workgroup_lead': '工作组长',
'member': '普通组员',
}
def get(self, request):
user = request.user if hasattr(request, 'user') and request.user.is_authenticated else None
# 特权级采用角色超级管理员标记(admin:super / admin:tenant),
# 而非用户级 User.IsSuperuser 标记:无角色的超级用户仅获 admin:tenant 级别。
# is_superuser 仅为兼容旧前端回退保留,不再作为特权级判定依据。
is_superuser = bool(user and getattr(user, 'IsSuperuser', False))
is_admin = _is_admin(request) # admin:super 或 admin:tenant
is_super_admin = _is_super_admin(request) # 仅 admin:super(跨租户,最高角色级)
is_tenant_admin = _is_tenant_admin(request) # 仅 admin:tenant(租户内,中角色级)
is_company_admin = _is_company_admin(request) # admin:company / admin:tenant / admin:super
current_role_uuid, current_company_uuid, current_tenant_uuid = _get_current_context(request)
# 获取角色名
role_name = ''
if current_role_uuid:
try:
from gvsdsdk.auth.managers import RoleManager
role = RoleManager.GetRoleByUUID(current_role_uuid)
if role:
role_name = role.RoleName or ''
except Exception:
pass
# 判断隔离状态(与 FormService.__init__ 一致)
# company_scope: admin:super/admin:tenant 关闭,普通用户启用
# tenant_scope: 仅 admin:super 关闭,admin:tenant/普通用户启用
company_scope = not is_admin
tenant_scope = not is_super_admin
# 计算角色作用域(前端差异化显示依据)
scope_type = 'member'
scope_label = self.SCOPE_TYPE_LABEL['member']
dept_uuid_str = None
workgroup_uuid_str = None
try:
from gvsds.apps.gerp.role_scope import GetUserScope
scope = GetUserScope(request)
scope_type = scope.Type
scope_label = self.SCOPE_TYPE_LABEL.get(scope_type, '普通组员')
# 负责人/组长可挂多个部门/工作组:多个 UUID 用逗号拼接
# (前端当前仅消费 Type/IsDeptManager/IsWorkgroupLead,列表值供扩展)
if scope.DeptUUIDs:
dept_uuid_str = ','.join(UUIDObj(d).str for d in scope.DeptUUIDs if d)
if scope.WorkgroupUUIDs:
workgroup_uuid_str = ','.join(UUIDObj(w).str for w in scope.WorkgroupUUIDs if w)
except Exception:
pass
return Response({
'code': 0,
'data': {
'is_admin': is_admin,
'is_superuser': is_superuser,
'is_super_admin': is_super_admin,
'is_tenant_admin': is_tenant_admin,
'is_company_admin': is_company_admin,
'current_role_name': role_name,
'current_role_uuid': UUIDObj(current_role_uuid).str if current_role_uuid else None,
'current_company_uuid': UUIDObj(current_company_uuid).str if current_company_uuid else None,
'current_tenant_uuid': UUIDObj(current_tenant_uuid).str if current_tenant_uuid else None,
'has_current_company': current_company_uuid is not None,
'company_scope': company_scope,
'tenant_scope': tenant_scope,
# 角色作用域(前端差异化显示依据)
'role_scope': {
'Type': scope_type,
'TypeLabel': scope_label,
'DeptUUID': dept_uuid_str,
'WorkgroupUUID': workgroup_uuid_str,
'IsAdmin': scope_type == 'admin',
'IsDeptManager': scope_type == 'dept_manager',
'IsWorkgroupLead': scope_type == 'workgroup_lead',
'IsMember': scope_type == 'member',
},
}
})
class WorkOrderStatsView(ELTAuthenticatedAPIView):
"""工单统计接口——按状态/优先级/分类计数,驱动工单管理页仪表盘
GET /API/FormAPI/WorkOrderStats → { code: 0, data: {
total, byStatus: {0:n,...}, byPriority: {0:n,...}, byCategory: {bug:n,...}
} }
WorkOrder 无 CompanyUUID/TenantUUID 字段,任何登录用户可见全部工单。
"""
def get(self, request):
from gvsdsdk.auth.managers import WorkOrderManager
from gvsds.apps.gerp.form_service import FormService
stats = WorkOrderManager.GetStats()
return Response({
'code': 0,
'data': {
'total': stats['total'],
'byStatus': stats['byStatus'],
'byPriority': stats['byPriority'],
'byCategory': stats['byCategory'],
# 附状态/优先级中文标签,前端可直接渲染
'statusOptions': FormService.WORKORDER_STATUS_OPTIONS['Status'],
'priorityOptions': FormService.WORKORDER_PRIORITY_OPTIONS['Priority'],
'categoryOptions': FormService.WORKORDER_CATEGORY_OPTIONS['Category'],
}
})
def _ToUuidBytes(value):
"""UUID 值归一化为 16 字节(无效/空返回 None)"""
if not value:
return None
try:
return UUIDObj(value).bytes
except Exception:
return None
def _ToUuidStr(value):
"""UUID 值归一化为标准字符串(无效/空返回空串)"""
if not value:
return ''
try:
return UUIDObj(value).str
except Exception:
return str(value)
class DeptEmployeeSearchView(ELTAuthenticatedAPIView):
"""「部门员工关联」搜索——"部门员工关联"选择框的数据源
GET /API/FormAPI/DeptEmployeeSearch?keyword=&CompanyUUID=&DeptUUID=&offset=
设计要点:
- 数据源是 UserRoleDepartment(部门员工关联),而非"用户表 + 部门表"两张独立表:
选择器里一项 = 一条部门员工关联,选中即可同时确定员工与部门(及部门名)。
员工档案(EmployeeProfile)与工作组员工(UserRoleWorkgroup)共用同一叫法。
- 返回字段:UserRoleDepartmentUUID / UserRoleUUID / DeptUUID / DeptName / UserUUID /
UserName / DeptEmployeeLabel("部门名 · 用户名",选择器展示文本)
- 范围:显式传 CompanyUUID 时强制该公司(超管也不豁免);否则取当前角色公司;
无公司上下文返回空列表(不退化成全表)
- keyword 同时匹配用户名与部门名,任一命中即保留(并集)
"""
permission_classes = [IsAuthenticated]
def get(self, request):
from gvsdsdk.auth.managers import AuthManager, DepartmentManager, RoleManager
keyword = (request.query_params.get('keyword') or '').strip()
company_str = (request.query_params.get('CompanyUUID') or '').strip()
if company_str:
company_bytes = _ToUuidBytes(company_str)
else:
# 未显式指定公司:用当前角色公司(与表单服务同一来源)
_, company_bytes, _ = _get_current_context(request)
if not company_bytes:
return Response({'code': 0, 'data': {'list': [], 'has_more': False}})
dept_param = (request.query_params.get('DeptUUID') or '').strip()
qs = RoleManager.QueryUserRoleDepartmentsByCompany(
company_bytes, [dept_param] if dept_param else None,
)
if keyword:
from django.db.models import Q
# 部门名命中:收窄部门
dept_bytes = [
b for b in (
_ToUuidBytes(d.DeptUUID)
for d in DepartmentManager.QueryByCompany(company_bytes).filter(
DeptName__icontains=keyword
)
) if b
]
# 用户名命中:收窄到这些用户在本公司的"用户-角色对"
user_uuids, _role_uuids = RoleManager.MatchUserRoleIds(keyword)
ur_bytes = []
if user_uuids:
ur_bytes = [
b for b in (
_ToUuidBytes(x)
for x in RoleManager.QueryUserRolesByCompany(company_bytes)
.filter(UserUUID__in=list(user_uuids))
.values_list('UserRoleUUID', flat=True)
) if b
]
if dept_bytes and ur_bytes:
qs = qs.filter(Q(DeptUUID__in=dept_bytes) | Q(UserRoleUUID__in=ur_bytes))
elif dept_bytes:
qs = qs.filter(DeptUUID__in=dept_bytes)
elif ur_bytes:
qs = qs.filter(UserRoleUUID__in=ur_bytes)
else:
qs = qs.none()
offset = int(request.query_params.get('offset') or 0)
# Dedup=UserRole:按"用户-角色对"去重(同一员工多部门只保留一条,供工作组等
# 只认 UserRoleUUID 的选择器使用,避免出现同值的重复选项)
dedup_by_user_role = (request.query_params.get('Dedup') or '').lower() == 'userrole'
if dedup_by_user_role:
deduped = []
seen = set()
for r in qs:
key = _ToUuidBytes(r.UserRoleUUID)
if key is None or key in seen:
continue
seen.add(key)
deduped.append(r)
total = len(deduped)
rows = deduped[offset:offset + 20]
else:
try:
total = qs.count()
except Exception:
total = 0
rows = list(qs[offset:offset + 20])
# 批量解析:UserRoleUUID → UserUUID / RoleUUID(UserRoleDepartment 只存"用户-角色对")
ur_objs = RoleManager.GetUserRolesByUUIDs([r.UserRoleUUID for r in rows if r.UserRoleUUID])
ur_user_bytes = {} # UserRoleUUID(bytes) → UserUUID(bytes)
ur_role_bytes = {} # UserRoleUUID(bytes) → RoleUUID(bytes)
for ur in ur_objs:
ur_key = _ToUuidBytes(ur.UserRoleUUID)
if not ur_key:
continue
user_key = _ToUuidBytes(ur.UserUUID)
role_key = _ToUuidBytes(ur.RoleUUID)
if user_key:
ur_user_bytes[ur_key] = user_key
if role_key:
ur_role_bytes[ur_key] = role_key
user_name_map = {}
if ur_user_bytes:
try:
user_name_map = AuthManager.GetUserNames(list(set(ur_user_bytes.values())))
except Exception:
user_name_map = {}
role_name_map = {}
if ur_role_bytes:
try:
role_name_map = RoleManager.GetRoleNameMap(list(set(ur_role_bytes.values())))
except Exception:
role_name_map = {}
dept_name_map = {}
dept_keys = [_ToUuidBytes(r.DeptUUID) for r in rows if r.DeptUUID]
if dept_keys:
try:
dept_name_map = DepartmentManager.GetDepartmentNameMap(dept_keys)
except Exception:
dept_name_map = {}
data = []
for r in rows:
urd_str = _ToUuidStr(r.UserRoleDepartmentUUID)
dept_str = _ToUuidStr(r.DeptUUID)
ur_key = _ToUuidBytes(r.UserRoleUUID)
user_bytes = ur_user_bytes.get(ur_key)
user_str = _ToUuidStr(user_bytes)
user_name = user_name_map.get(user_bytes, '') if user_bytes else ''
role_name = role_name_map.get(ur_role_bytes.get(ur_key), '') if ur_key else ''
dept_name = dept_name_map.get(_ToUuidBytes(r.DeptUUID), '')
label = f'{dept_name} · {user_name}' if (dept_name or user_name) else urd_str
data.append({
'UserRoleDepartmentUUID': urd_str,
'UserRoleUUID': _ToUuidStr(r.UserRoleUUID),
'DeptUUID': dept_str,
'DeptName': dept_name,
'UserUUID': user_str,
'UserName': user_name,
'RoleName': role_name,
'DeptEmployeeLabel': label,
})
result = {'list': data, 'has_more': total > offset + 20}
# tree=1:附带部门树(选择器"按部门树分组展示"面板用,与 UserRoleSearch 口径一致)
if (request.query_params.get('tree') or '') == '1':
tree = []
try:
for d in DepartmentManager.QueryByCompany(company_bytes):
tree.append({
'uuid': _ToUuidStr(d.DeptUUID),
'name': d.DeptName,
'parentUuid': _ToUuidStr(getattr(d, 'ParentDeptUUID', None)),
})
except Exception:
tree = []
result['tree'] = tree
return Response({'code': 0, 'data': result})