部门详情「部门员工」与公司详情「员工」卡片标题需要显示「角色数:x/用户数:y」。
get_table 新增 WithUserCount 参数:带该参数时额外统计去重用户数(UserRole 直接按
UserUUID 去重;UserRoleDepartment/UserRoleWorkgroup 只存 UserRoleUUID 且非外键,
需回查 UserRole 后再去重)。默认不统计,避免给所有列表加一次查询。
Get/List 响应新增 _FormListResponse:仅在请求了计数时改为 {list, total, user_count}
对象结构 —— 前端 axios 拦截器只透传 data,计数挂在 data 之外读不到;不带该参数时
仍返回原有的数组结构,既有调用方行为不变。
720 lines
32 KiB
Python
720 lines
32 KiB
Python
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})
|