feat(gerp): 员工列表支持按需统计去重用户数

部门详情「部门员工」与公司详情「员工」卡片标题需要显示「角色数:x/用户数:y」。

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

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

No files matched your search

+26 -1
View File
@@ -1349,6 +1349,25 @@ class FormService(FormServiceBase):
return 'select'
return mapping.get(t, 'text')
def CountDistinctUsers(self, table_name, qs):
"""统计 qs 命中的**去重用户数**(角色数 = qs.count(),由调用方给出)
为什么要走不同路径:
- UserRole 自身带 UserUUID 列,直接去重计数即可;
- UserRoleDepartment / UserRoleWorkgroup 只存 UserRoleUUID,而且该字段是普通
UUIDField 而非 ForeignKey,不能用 `__` 跨表,必须拿 UUID 列表回查 UserRole
再按 UserUUID 去重(与 get_table 里为这两张表补 UserUUID 的做法一致)。
其余表没有"用户"语义,返回 None(调用方据此退回默认响应结构)。
"""
if table_name == 'UserRole':
return qs.values('UserUUID').distinct().count()
if table_name in ('UserRoleDepartment', 'UserRoleWorkgroup'):
from gvsdsdk.models import UserRole
return UserRole.objects.filter(
UserRoleUUID__in=qs.values_list('UserRoleUUID', flat=True)
).values('UserUUID').distinct().count()
return None
def get_table(self, table_name, params):
model = self.TABLE_MAP.get(table_name)
if not model:
@@ -1437,8 +1456,14 @@ class FormService(FormServiceBase):
qs = self.ApplyExactFilters(qs, model, params, SkipKeys=_SKIP_FILTER_PARAMS)
qs = self.ApplyKeyword(qs, model, params.get('keyword', ''))
page, page_size = self.ParsePagination(params)
# 员工卡片标题「角色数/用户数」:角色数就是关系条数(total),用户数是去重用户数。
# 默认不算(多一次统计查询),调用方显式带 WithUserCount=1 时才计算。
# 先置 None 是为了下面的 except 分支也能正常收尾(该分支会重建 total/records)
user_count = None
try:
total = qs.count()
if params.get('WithUserCount'):
user_count = self.CountDistinctUsers(table_name, qs)
start = (page - 1) * page_size
# 识别 entityPicker 字段,使用 select_related 预加载关联实体
# 注意:select_related 只能用于 ForeignKey,UUIDField 类型的 entityPicker 不能预加载
@@ -1773,7 +1798,7 @@ class FormService(FormServiceBase):
logging.error('[FormService] get_table error: table=%s, error=%s', table_name, e, exc_info=True)
total = 0
records = []
return {'records': records, 'total': total, 'page': page, 'PageSize': page_size}
return {'records': records, 'total': total, 'page': page, 'PageSize': page_size, 'user_count': user_count}
def _ResolveDeptEmployee(self, data):
"""解析「部门员工关联」选择器提交的 UserRoleDepartmentUUID(唯一实现)
+23 -3
View File
@@ -171,6 +171,26 @@ def _check_table_access(table_name, action, request):
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)
@@ -198,7 +218,7 @@ class FormAPICompatView(ELTAuthenticatedAPIView):
if access_error:
return access_error
result = service.get_table(table_name, request.query_params)
return Response({'code': 0, 'data': result['records'], 'total': result['total']})
return Response(_FormListResponse(result))
elif '/CodeDisplay' in path:
result = service.code_display(request.query_params.dict())
return Response({'code': 0, 'data': result})
@@ -210,7 +230,7 @@ class FormAPICompatView(ELTAuthenticatedAPIView):
if access_error:
return access_error
result = service.get_table(table_name, request.query_params)
return Response({'code': 0, 'data': result['records'], 'total': result['total']})
return Response(_FormListResponse(result))
def post(self, request, **kwargs):
user = request.user if hasattr(request, 'user') and request.user.is_authenticated else None
@@ -354,7 +374,7 @@ class FormAPIView(ELTAuthenticatedAPIView):
return access_error
service = _make_service(request, request.user)
result = service.get_table(table_name, request.query_params)
return Response({'code': 0, 'data': result['records'], 'total': result['total']})
return Response(_FormListResponse(result))
def post(self, request):
action = request.data.get('Action', 'add')