feat(gerp): 部门新增成员改用部门员工关联选择器

UserRoleDepartment 表单人员输入统一为 DEPT_EMPLOYEE_PICKER(与 eHRM/工作组同源单一配置):新增虚拟列置顶,用户/角色列改为纯列表展示,DeptUUID 表单隐藏(上下文优先,否则取选择器解析的部门),删除就地写的 UserRoleSearch 选择器;add_record/modify_record 支持 UserRoleDepartmentUUID 解析 UserRoleUUID 并拒绝选到别的员工。
This commit is contained in:
TermiNexus committed 2026-10-01 14:16:18 +08:00
1 parent 4669a7c53c
commit fd56a9e4ff
2 files changed
+79 -59

No files matched your search

+12 -4
View File
@@ -109,16 +109,24 @@
| 常量 | valueField | 附加行为 | 使用点 |
|------|-----------|---------|--------|
| `DEPT_TREE_GROUP` | —(公共 treeGroup) | 部门树分组 + 未分配部门兜底 | 下列两处 + `UserRoleDepartment.UserRoleUUID` |
| `DEPT_EMPLOYEE_PICKER` | `UserRoleDepartmentUUID` | 单选 | `EmployeeProfile.UserRoleDepartmentUUID`(eHRM 员工档案) |
| `DEPT_TREE_GROUP` | —(公共 treeGroup) | 部门树分组 + 未分配部门兜底 | 被下面两个常量引用(勿单独就地使用) |
| `DEPT_EMPLOYEE_PICKER` | `UserRoleDepartmentUUID` | 单选 | `EmployeeProfile.UserRoleDepartmentUUID`(eHRM 员工档案)、`UserRoleDepartment.UserRoleDepartmentUUID`(部门新增成员,虚拟列) |
| `DEPT_EMPLOYEE_PICKER_MULTI` | `UserRoleDepartmentUUID` | 多选暂存 + 成员/负责人类型 | `UserRoleWorkgroup.UserRoleDepartmentUUID`(工作组新增成员,虚拟列) |
**两个使用点取值一致**(都是「部门员工关联」= 部门 + 该用户在本公司的用户-角色对),
区别只在是否多选暂存;落库时由 `FormService._ResolveDeptEmployee()` **统一解析**(唯一实现):
**三个使用点取值一致**(都是「部门员工关联」= 部门 + 该用户在本公司的用户-角色对),
区别只在单/多选;落库时由 `FormService._ResolveDeptEmployee()` **统一解析**(唯一实现):
- `EmployeeProfile`:取 `UserUUID` / `DeptUUID` / `DeptName` 三列落库(常量 `_PROFILE_DEPT_EMPLOYEE_KEYS` 声明)
- `UserRoleWorkgroup`:取 `UserRoleUUID` 落库到关系列(关系表只存用户-角色对,**部门仅用于选择/分组展示**);
表单里该关系列 `hideInForm`,由虚拟列 `UserRoleDepartmentUUID` 作为唯一人员选择入口
- `UserRoleDepartment`:取 `UserRoleUUID` 落库;`DeptUUID` **表单上下文优先**(部门详情页 = 当前部门),
无上下文时取选择器解析出的部门(员工详情页新增"部门职务"即此场景);
员工详情页上下文里 `UserUUID` 由 query 注入,选择器选到别的员工会被后端拒绝
(`所选「部门员工关联」不属于当前员工`)。两张关联表的 `UserUUID`/`RoleUUID` 列一律
`editable=False`(纯列表展示),人员输入只有选择器一个入口
> 已知取舍:选择器数据源是 `UserRoleDepartment`,因此**尚未挂靠任何部门的"用户-角色对"
> 选不到**。公司员工列表(`UserRole`)新增/编辑时若填了部门会同步建主关联,正常路径不受影响。
**唯一性约束**:员工档案一人一份 —— 新增与改绑均由 `form_service.add_record/modify_record` 拦截
(前端在已有档案时隐藏新增入口)。
+67 -55
View File
@@ -124,7 +124,8 @@ def create_tenant_admin_role(tenant):
# (落库时再解析出 UserUUID / DeptUUID / DeptName,一人一份档案)
# 2) UserRoleWorkgroup.UserRoleDepartmentUUID(工作组新增成员):多选暂存批量新增,
# 落库时解析出 UserRoleUUID(关系表只存"用户-角色对",部门仅用于选择/分组展示)
# 3) UserRoleDepartment.UserRoleUUID(部门挂靠,表单隐藏):仅复用 DEPT_TREE_GROUP
# 3) UserRoleDepartment.UserRoleDepartmentUUID(部门新增成员):单选,
# 落库时解析出 UserRoleUUID(部门取表单上下文注入的 DeptUUID,选择器的部门只用于分组)
#: 「按部门树分组」浏览面板的公共 treeGroup 配置(EntityPicker):
#: 下拉里先展示部门树、点部门看该部门成员,未挂部门的归入 ungroupedTitle
@@ -704,14 +705,16 @@ class FormService(FormServiceBase):
if table_name == 'UserRole' and pascal_name == 'CompanyUUID':
col['visible'] = False
# UserRoleDepartment 关联表冗余字段:仅用于隔离过滤与上下文注入。
# DeptUUID 列表展示部门(回显部门名),表单可编辑(无 query 注入时(如员工详情页
# 新增成员)用户自选部门;详情页场景由 query 注入自动从表单排除);TenantUUID/CompanyUUID
# 始终内部注入,表单隐藏
# DeptUUID 列表展示部门(回显部门名),表单隐藏 —— 人员输入统一走
# "部门员工关联"选择器:部门详情页由 query 注入当前部门,员工详情页则取
# 选择器解析出的部门(add_record 里 DeptUUID 表单上下文优先);
# TenantUUID/CompanyUUID 始终内部注入,表单隐藏
if table_name == 'UserRoleDepartment' and pascal_name == 'DeptUUID':
col['title'] = '部门'
col['type'] = 'entityPicker'
col['entityPicker'] = {**self.ENTITY_PICKER_MAP['Department'], 'placeholder': '选择部门'}
col['editable'] = True
col['editable'] = False
col['hideInForm'] = True
col['visible'] = True
if table_name == 'UserRoleDepartment' and pascal_name in ('TenantUUID', 'CompanyUUID'):
col['editable'] = False
@@ -728,29 +731,14 @@ class FormService(FormServiceBase):
col['type'] = 'entityPicker'
col['entityPicker'] = {**self.ENTITY_PICKER_MAP['PositionCategory'], 'placeholder': '选择绑定的通用部门'}
col['editable'] = True
# UserRoleDepartment 的 UserRoleUUID:仅列表展示"角色名(用户名)",
# 表单隐藏——改用下方两级级联"用户→角色"选择
# (UserRoleWorkgroup 的 UserRoleUUID 同为其关系列,表单改由虚拟列
# UserRoleDepartmentUUID 的"部门员工关联"选择器承担,见该表的虚拟列配置)
if table_name == 'UserRoleDepartment' and pascal_name == 'UserRoleUUID':
col['type'] = 'entityPicker'
# 复用公共部门树分组配置(取值仍是 UserRoleUUID,数据源为 UserRoleSearch)
col['entityPicker'] = {
'api': '/API/FormAPI/UserRoleSearch',
'labelField': 'UserName',
'secondaryField': 'RoleName',
'valueField': 'UserRoleUUID',
'placeholder': '选择部门员工关联(按部门分组展示)',
'treeGroup': dict(DEPT_TREE_GROUP),
}
col['editable'] = True
col['hideInForm'] = True
col['entityPicker'] = {**col['entityPicker'], 'dependCompany': True}
# UserRoleDepartment 列表可读性:UserRoleUUID 关联列列表隐藏(表单仍隐藏),
# 用户/角色由下方虚拟列(UserUUID/RoleUUID)分开展示(值经 get_table 反查 UserRole 注入);
# UserRoleDepartment 列表可读性:UserRoleUUID 是"用户-角色对"外键列,列表与表单
# 都隐藏 —— 用户/角色由下方虚拟列(UserUUID/RoleUUID)分开展示(值经 get_table
# 反查 UserRole 注入),人员输入由虚拟列 UserRoleDepartmentUUID 的"部门员工关联"
# 选择器承担(原先就地写的 UserRoleSearch 选择器已删除,避免同一语义两套配置);
# PositionUUID 列标题改"职务"(值为职务名,由 _EnrichEntityLabels 回显)
if table_name == 'UserRoleDepartment' and pascal_name == 'UserRoleUUID':
col['visible'] = False
col['hideInForm'] = True
if table_name == 'UserRoleDepartment' and pascal_name == 'PositionUUID':
col['title'] = '职务'
# UserRoleWorkgroup 列表可读性:与 UserRoleDepartment 对齐——
@@ -1042,13 +1030,12 @@ class FormService(FormServiceBase):
columns = columns[:_idx + 1] + [_dept_col, _pos_col] + columns[_idx + 1:]
else:
columns = columns + [_dept_col, _pos_col]
# 虚拟列"用户/角色":UserRoleDepartment 承担新增表单两级级联(用户选好后,
# 角色只显示该用户已绑定且属于目标公司的角色,RoleSearch 级联过滤;提交时后端
# 从 (用户, 角色) 解析出 UserRole 关联写入关联表,不重复新增 UserRole);
# UserRoleWorkgroup 列表同样拆分展示用户/角色(值经 get_table 反查 UserRole 注入),
# 其新增表单用 UserRoleDepartmentUUID"部门员工关联"选择器(见下方虚拟列)
# 虚拟列"用户/角色":两张关联表都要把"用户-角色对"拆成两列展示
# (值经 get_table 反查 UserRole 注入,关联表本身只存 UserRoleUUID)。
# 这两列只用于**列表展示**(editable=False → 任何表单都不出现),
# 人员输入统一走 UserRoleDepartmentUUID"部门员工关联"选择器(见下方虚拟列):
# 部门新增成员与工作组新增成员同口径
if table_name in ('UserRoleDepartment', 'UserRoleWorkgroup'):
is_ur_dept = table_name == 'UserRoleDepartment'
virtual_cols = [
{
'field': 'UserUUID',
@@ -1056,12 +1043,11 @@ class FormService(FormServiceBase):
'titleEn': 'UserUUID',
'type': 'entityPicker',
'entityPicker': {**self.ENTITY_PICKER_MAP['User'], 'placeholder': '用户'},
'editable': is_ur_dept,
'editable': False,
'sortable': False,
'filterable': False,
'required': is_ur_dept,
'required': False,
'visible': True, # 列表展示"用户"(值经 get_table 反查注入)
**({'hideInEditForm': True} if is_ur_dept else {}), # 级联列仅新增表单显示
},
{
'field': 'RoleUUID',
@@ -1074,12 +1060,11 @@ class FormService(FormServiceBase):
'dependCompany': True,
'placeholder': '该用户绑定的角色',
},
'editable': is_ur_dept,
'editable': False,
'sortable': False,
'filterable': False,
'required': is_ur_dept,
'required': False,
'visible': True, # 列表展示"角色"(值经 get_table 反查注入)
**({'hideInEditForm': True} if is_ur_dept else {}), # 级联列仅新增表单显示
},
{
# 角色覆盖名(UserRole.RoleName):与"角色"分列展示,仅列表可见
@@ -1111,16 +1096,21 @@ class FormService(FormServiceBase):
'visible': True,
'multiValue': True, # 前端多选:值/回显为逗号分隔的多个职务
})
# UserRoleWorkgroup 的"部门员工关联"虚拟列(表单里唯一的人员选择入口):
# 选一条"部门 + 用户-角色对"的关联,落库时后端解析出 UserRoleUUID
# (关系表只存用户-角色对;与 eHRM 员工档案共用 DEPT_EMPLOYEE_PICKER* 单一来源)
if table_name == 'UserRoleWorkgroup':
# "部门员工关联"虚拟列(表单里唯一的人员选择入口):选一条"部门 + 用户-角色对"
# 的关联,落库时后端解析出 UserRoleUUID(关系表只存用户-角色对;部门仅用于分组
# 展示,或由表单上下文注入的 DeptUUID 决定)。与 eHRM 员工档案共用
# DEPT_EMPLOYEE_PICKER* 单一来源,两张关联表只在"是否多选暂存"上不同:
# UserRoleWorkgroup —— 多选暂存,一次批量加入工作组
# UserRoleDepartment —— 单选,一次新增一名部门成员
virtual_cols.append({
'field': 'UserRoleDepartmentUUID',
'title': '部门员工关联',
'titleEn': 'UserRoleDepartmentUUID',
'type': 'entityPicker',
'entityPicker': dict(DEPT_EMPLOYEE_PICKER_MULTI),
'entityPicker': dict(
DEPT_EMPLOYEE_PICKER_MULTI if table_name == 'UserRoleWorkgroup'
else DEPT_EMPLOYEE_PICKER
),
'editable': True,
'sortable': False,
'filterable': False,
@@ -1348,6 +1338,19 @@ class FormService(FormServiceBase):
r['UserRoleUUID_label'] = urw_info[_uid]['RoleName']
r['UserRoleUUID_UserName'] = urw_info[_uid]['UserName']
r['RoleNameCover'] = urw_info[_uid]['RoleNameCover']
# UserRoleDepartment 编辑表单"部门员工关联"选择器回显:本行的
# UserRoleDepartmentUUID 就是取值本身,标签与选择器选项格式一致
# (DeptEmployeeSearch 的 DeptEmployeeLabel = "部门名 · 用户名")
if table_name == 'UserRoleDepartment':
for r in records:
_urd_key = r.get('UserRoleDepartmentUUID')
if not _urd_key:
continue
_dn = r.get('DeptUUID_label') or ''
_un = r.get('UserUUID_label') or ''
_rd_label = f'{_dn} · {_un}' if (_dn and _un) else (_dn or _un)
if _rd_label:
r['UserRoleDepartmentUUID_label'] = _rd_label
# UserRoleDepartment 职务多值聚合(身兼数职):PositionUUID 从子表取,
# 列表值/回显为逗号分隔(前端 multiValue 拆多标签展示)
if table_name == 'UserRoleDepartment':
@@ -1795,20 +1798,27 @@ class FormService(FormServiceBase):
create_data['RoleName'] = None
# UserRoleWorkgroup / UserRoleDepartment 规则拦截:新增"用户-角色对 → 工作组/部门"关系时,
# 支持三种提交方式:
# 1) 直接提交 UserRoleUUID(旧路径,兼容)
# 2) 前端"用户→角色"两级级联:提交 UserUUID + RoleUUID,后端解析出 UserRole 关联
# 3) 前端"部门员工关联"选择器:提交 UserRoleDepartmentUUID,后端解析出 UserRole 关联
# (工作组新增成员;部门仅用于选择,不落库)
# 1) 前端"部门员工关联"选择器:提交 UserRoleDepartmentUUID,后端解析出 UserRole 关联
# (两张表的表单唯一入口:工作组多选批量新增、部门单选新增成员;
# UserRoleDepartment 的部门优先取表单上下文注入的 DeptUUID,否则取选择器解析结果)
# 2) 前端"用户→角色"两级级联:提交 UserUUID + RoleUUID,后端解析出 UserRole 关联(兼容旧调用)
# 3) 直接提交 UserRoleUUID(旧路径,兼容)
# 校验 UserRole 关联存在,且避免同一用户-角色对重复挂同一工作组/部门
if table_name in ('UserRoleWorkgroup', 'UserRoleDepartment'):
if (table_name == 'UserRoleWorkgroup'
and (data.get('UserRoleDepartmentUUID') or data.get('UserRoleDepartmentUUID_id'))):
if data.get('UserRoleDepartmentUUID') or data.get('UserRoleDepartmentUUID_id'):
# 选择器提交的"部门员工关联"优先(其值就是本次要绑定的人员)
_urw_de, _urw_de_err = self._ResolveDeptEmployee(data)
if _urw_de_err:
return {'error': _urw_de_err, 'field': 'UserRoleDepartmentUUID'}
if _urw_de and _urw_de.get('UserRoleUUID'):
_ctx_user = _to_uuid_bytes(data.get('UserUUID'))
if (table_name == 'UserRoleDepartment' and _ctx_user
and _urw_de.get('UserUUID') and _urw_de['UserUUID'] != _ctx_user):
# 员工详情页新增"部门职务":员工由上下文固定,选择器不得选到别人
return {'error': '所选「部门员工关联」不属于当前员工', 'field': 'UserRoleDepartmentUUID'}
create_data['UserRoleUUID'] = _urw_de['UserRoleUUID']
# 部门:表单上下文注入的 DeptUUID 优先(部门详情页新增成员),否则用选择器解析出的部门
if table_name == 'UserRoleDepartment' and not create_data.get('DeptUUID'):
create_data['DeptUUID'] = _urw_de['DeptUUID']
urw_ur = create_data.get('UserRoleUUID')
target_label = '工作组' if table_name == 'UserRoleWorkgroup' else '部门'
urw_target = (create_data.get('WorkgroupUUID') or create_data.get('WorkgroupUUID_id')
@@ -1819,10 +1829,8 @@ class FormService(FormServiceBase):
if urw_ur is None:
# 走"用户→角色"级联:从 (用户, 角色) 解析 UserRole 关联(同公司约束)
if not urw_user or not urw_role:
if table_name == 'UserRoleWorkgroup':
return {'error': '请选择「部门员工关联」(或直接指定用户与角色)',
return {'error': f'请选择「部门员工关联」({target_label}成员须绑定一条部门员工关联)',
'field': 'UserRoleDepartmentUUID'}
return {'error': f'必须指定用户、角色和{target_label}', 'field': 'UserRoleUUID'}
try:
ur_obj = RoleManager.GetUserRoleRelation(urw_user, urw_role, urw_company)
except Exception:
@@ -2115,15 +2123,19 @@ class FormService(FormServiceBase):
# 工单状态流转:保存修改前的 Status,用于流转判定(时间戳 + 通知)
_wo_old_status = getattr(obj, 'Status', None) if table_name == 'WorkOrder' else None
pascal_map = self._BuildFieldMap(model)
# UserRoleWorkgroup:表单用「部门员工关联」(UserRoleDepartmentUUID)选择器,
# 编辑时改绑人员 → 解析出 UserRoleUUID 覆盖提交值(关系表只存用户-角色对,部门不落库)
if table_name == 'UserRoleWorkgroup' and (
# UserRoleWorkgroup / UserRoleDepartment:表单用「部门员工关联」(UserRoleDepartmentUUID)
# 选择器,编辑时改绑人员 → 解析出 UserRoleUUID 覆盖提交值(关系表只存用户-角色对,
# 部门不落库;UserRoleDepartment 的部门保持原值/表单上下文值,不随改绑人员漂移)
if table_name in ('UserRoleWorkgroup', 'UserRoleDepartment') and (
data.get('UserRoleDepartmentUUID') or data.get('UserRoleDepartmentUUID_id')):
_urw_de, _urw_de_err = self._ResolveDeptEmployee(data)
if _urw_de_err:
return {'error': _urw_de_err, 'field': 'UserRoleDepartmentUUID'}
if _urw_de and _urw_de.get('UserRoleUUID'):
data = {**data, 'UserRoleUUID': _urw_de['UserRoleUUID']}
if table_name == 'UserRoleDepartment' and not (
data.get('DeptUUID') or data.get('DeptUUID_id')):
data = {**data, 'DeptUUID': _urw_de['DeptUUID']}
# EmployeeProfile(员工档案)直接绑「部门-员工」关系:解析选择器提交的
# UserRoleDepartmentUUID → UserUUID/DeptUUID/DeptName(未提交/解析不出则保持原值)
if table_name == 'EmployeeProfile':