From 07e44eefc6288bf7ceaa889335ee1a02c8e9d512 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Thu, 1 Oct 2026 23:15:50 +0800 Subject: [PATCH] =?UTF-8?q?feat(gerp):=20=E5=91=98=E5=B7=A5=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E6=94=AF=E6=8C=81=E6=8C=89=E9=9C=80=E7=BB=9F=E8=AE=A1?= =?UTF-8?q?=E5=8E=BB=E9=87=8D=E7=94=A8=E6=88=B7=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 部门详情「部门员工」与公司详情「员工」卡片标题需要显示「角色数:x/用户数:y」。 get_table 新增 WithUserCount 参数:带该参数时额外统计去重用户数(UserRole 直接按 UserUUID 去重;UserRoleDepartment/UserRoleWorkgroup 只存 UserRoleUUID 且非外键, 需回查 UserRole 后再去重)。默认不统计,避免给所有列表加一次查询。 Get/List 响应新增 _FormListResponse:仅在请求了计数时改为 {list, total, user_count} 对象结构 —— 前端 axios 拦截器只透传 data,计数挂在 data 之外读不到;不带该参数时 仍返回原有的数组结构,既有调用方行为不变。 --- gvsds/apps/gerp/form_service.py | 27 ++++++++++++++++++++++++++- gvsds/apps/gerp/views/form_api.py | 26 +++++++++++++++++++++++--- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/gvsds/apps/gerp/form_service.py b/gvsds/apps/gerp/form_service.py index e0110d55..9506687d 100644 --- a/gvsds/apps/gerp/form_service.py +++ b/gvsds/apps/gerp/form_service.py @@ -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(唯一实现) diff --git a/gvsds/apps/gerp/views/form_api.py b/gvsds/apps/gerp/views/form_api.py index 0ec3e348..e6aced8e 100644 --- a/gvsds/apps/gerp/views/form_api.py +++ b/gvsds/apps/gerp/views/form_api.py @@ -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')