feat: 新增 approval_staff 员工角色并收紧派单候选池为审批参与人(#45)
This commit is contained in:
@@ -68,9 +68,11 @@ public final class ApprovalConstants {
|
||||
// 角色编码
|
||||
public static final String ROLE_ARCHIVIST = "approval_archivist";
|
||||
public static final String ROLE_SUPER_ADMIN = "superAdmin";
|
||||
/** 普通消费用户角色:无部门且仅有这些角色时不算派单候选。 */
|
||||
public static final String ROLE_USER = "user";
|
||||
public static final String ROLE_GUEST = "guest";
|
||||
/** 内部员工基础角色:持有它才能提交/审批/确认,也才进入派单候选池(#45)。 */
|
||||
public static final String ROLE_STAFF = "approval_staff";
|
||||
/** 部门岗位角色前缀(dept_leader_<orgCode> / dept_deputy_<orgCode>)。 */
|
||||
public static final String ROLE_DEPT_LEADER_PREFIX = "dept_leader_";
|
||||
public static final String ROLE_DEPT_DEPUTY_PREFIX = "dept_deputy_";
|
||||
|
||||
// 权限串(与 spec 约定一致;菜单/按钮共用)
|
||||
public static final String PERMISSION_LIST = "approval:instance:list";
|
||||
|
||||
@@ -121,19 +121,25 @@ public class ApprovalDirectoryService {
|
||||
return jdbcTemplate.query(sql.toString(), USER_MAPPER, args.toArray());
|
||||
}
|
||||
|
||||
/** 部门启用成员。 */
|
||||
/** 部门启用成员中的审批参与人;无审批角色的成员不进入候选池(#45:派给无权限的人会在存档确认时 403)。 */
|
||||
public List<ApprovalUserVO> listOrganizationMembers(Integer organizationId) {
|
||||
if (organizationId == null || organizationId <= 0) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Integer tenantId = ApprovalTenantContext.currentTenantId();
|
||||
List<Object> args = new java.util.ArrayList<>();
|
||||
args.add(tenantId);
|
||||
args.add(organizationId);
|
||||
args.add(tenantId);
|
||||
args.addAll(participantArgs(tenantId));
|
||||
return jdbcTemplate.query(
|
||||
"SELECT u.user_id, u.username, u.nickname, u.real_name, u.organization_id, o.organization_name "
|
||||
+ "FROM sys_user u "
|
||||
+ "LEFT JOIN sys_organization o ON o.organization_id = u.organization_id AND o.tenant_id = ? "
|
||||
+ "WHERE u.organization_id = ? AND u.tenant_id = ? AND u.status = 0 AND u.deleted = 0 "
|
||||
+ "ORDER BY u.user_id",
|
||||
USER_MAPPER, tenantId, organizationId, tenantId);
|
||||
+ "AND " + PARTICIPANT_EXISTS
|
||||
+ " ORDER BY u.user_id",
|
||||
USER_MAPPER, args.toArray());
|
||||
}
|
||||
|
||||
/** 按id批量取启用用户。 */
|
||||
@@ -173,10 +179,10 @@ public class ApprovalDirectoryService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 派单候选池:优先受理部门启用成员;部门无成员时回退为当前租户的非普通消费用户。
|
||||
* 派单候选池:优先受理部门启用成员中的审批参与人;部门无参与人时回退为当前租户的审批参与人。
|
||||
*
|
||||
* <p>回退排除「未归属组织且角色仅为 user/guest 或无角色」的账号;
|
||||
* 部门有成员时不混入回退候选。</p>
|
||||
* <p>审批参与人 = 持有员工或任一审批岗位角色的用户(#45 授予矩阵,见 sql/04-approval_role_grants.sql);
|
||||
* 两个分支同一判定,避免把单据派给无权限、存档确认必然 403 的人。</p>
|
||||
*/
|
||||
public List<ApprovalUserVO> listDispatchCandidates(Integer acceptOrganizationId) {
|
||||
List<ApprovalUserVO> members = listOrganizationMembers(acceptOrganizationId);
|
||||
@@ -184,19 +190,37 @@ public class ApprovalDirectoryService {
|
||||
return members;
|
||||
}
|
||||
Integer tenantId = ApprovalTenantContext.currentTenantId();
|
||||
List<Object> args = new java.util.ArrayList<>();
|
||||
args.add(tenantId);
|
||||
args.add(tenantId);
|
||||
args.addAll(participantArgs(tenantId));
|
||||
return jdbcTemplate.query(
|
||||
"SELECT u.user_id, u.username, u.nickname, u.real_name, u.organization_id, o.organization_name "
|
||||
+ "FROM sys_user u "
|
||||
+ "LEFT JOIN sys_organization o ON o.organization_id = u.organization_id AND o.tenant_id = ? "
|
||||
+ "WHERE u.tenant_id = ? AND u.status = 0 AND u.deleted = 0 "
|
||||
+ "AND NOT ((u.organization_id IS NULL OR u.organization_id = 0) AND NOT EXISTS ("
|
||||
+ "SELECT 1 FROM sys_user_role ur "
|
||||
+ "JOIN sys_role r ON r.role_id = ur.role_id AND r.tenant_id = ? "
|
||||
+ "WHERE ur.user_id = u.user_id AND ur.tenant_id = ? AND r.deleted = 0 "
|
||||
+ "AND r.role_code NOT IN (?, ?))) "
|
||||
+ "ORDER BY u.user_id",
|
||||
USER_MAPPER, tenantId, tenantId, tenantId, tenantId,
|
||||
ApprovalConstants.ROLE_USER, ApprovalConstants.ROLE_GUEST);
|
||||
+ "AND " + PARTICIPANT_EXISTS
|
||||
+ " ORDER BY u.user_id",
|
||||
USER_MAPPER, args.toArray());
|
||||
}
|
||||
|
||||
/** 审批参与人判定:持有员工、superAdmin 或任一审批岗位角色。 */
|
||||
private static final String PARTICIPANT_EXISTS =
|
||||
"EXISTS (SELECT 1 FROM sys_user_role pur "
|
||||
+ "JOIN sys_role pr ON pr.role_id = pur.role_id AND pr.tenant_id = ? AND pr.deleted = 0 "
|
||||
+ "WHERE pur.user_id = u.user_id AND pur.tenant_id = ? AND ("
|
||||
+ "pr.role_code = ? OR LEFT(pr.role_code, 12) = ? OR LEFT(pr.role_code, 12) = ? "
|
||||
+ "OR pr.role_code IN (?, ?, ?, ?, ?, ?)))";
|
||||
|
||||
private static List<Object> participantArgs(Integer tenantId) {
|
||||
// 与 sql/04-approval_role_grants.sql 的授予矩阵保持一致(superAdmin 在 02 已获全部审批权限)
|
||||
return List.of(tenantId, tenantId,
|
||||
ApprovalConstants.ROLE_STAFF,
|
||||
ApprovalConstants.ROLE_DEPT_LEADER_PREFIX,
|
||||
ApprovalConstants.ROLE_DEPT_DEPUTY_PREFIX,
|
||||
ApprovalConstants.ROLE_ARCHIVIST,
|
||||
ApprovalConstants.ROLE_SUPER_ADMIN,
|
||||
"approval_cashier", "approval_typist", "approval_issuer", "approval_general_manager");
|
||||
}
|
||||
|
||||
/** 是否持有任一角色(用于 allVisible 判断)。 */
|
||||
|
||||
@@ -148,25 +148,6 @@ class ApprovalConfirmArchiveIT {
|
||||
+ "AND action = 'CONFIRM' AND opinion IS NOT NULL", instanceId)).isZero();
|
||||
}
|
||||
|
||||
/**
|
||||
* 回归 #45:按 #11/#14,存档确认是报送人本人的动作,不应要求 approval:instance:approve。
|
||||
* 当前实现 403,掩盖于测试给 reporter 免费发放 APPROVE;待决策(权限授予或改 @PreAuthorize)后
|
||||
* 按结论启用或改写本用例。
|
||||
*/
|
||||
@Test
|
||||
@org.junit.jupiter.api.Disabled("待 #45 决策:仅持 submit 的报送人当前无法确认自己的存档任务")
|
||||
void reporterWithoutApproveAuthorityConfirmsOwnArchiveTask() throws Exception {
|
||||
long instanceId = createAndSubmit("最小权限存档");
|
||||
approveByLeader(instanceId);
|
||||
long reporterTask = pendingTaskId(instanceId, reporter.userId);
|
||||
|
||||
mockMvc.perform(authed(post("/api/approval/task/" + reporterTask + "/confirm"),
|
||||
reporter, organizationId, SUBMIT))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.status").value("APPROVED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void archivesOnceAndCancelsOtherArchiveTasks() throws Exception {
|
||||
long instanceId = createAndSubmit("归档或签");
|
||||
|
||||
@@ -89,14 +89,18 @@ class ApprovalDispatchIT {
|
||||
data.bindTenant(acceptLeader.userId, acceptLeaderRole.roleId, TENANT);
|
||||
acceptMember = data.createUser("派单受理成员" + data.uniqueSuffix(), acceptOrgId, TENANT);
|
||||
|
||||
// 候选池只含审批参与人(#45):报送人/受理成员持 approval_staff
|
||||
ApprovalTestData.TestRole staffRole = data.createRole("approval_staff", "员工", TENANT);
|
||||
data.bindTenant(reporter.userId, staffRole.roleId, TENANT);
|
||||
data.bindTenant(acceptMember.userId, staffRole.roleId, TENANT);
|
||||
|
||||
ApprovalTestData.TestRole archivistRole = data.createRole("approval_archivist", "收文员", TENANT);
|
||||
archivist = data.createUser("派单收文员" + data.uniqueSuffix(), reportOrgId, TENANT);
|
||||
data.bindTenant(archivist.userId, archivistRole.roleId, TENANT);
|
||||
|
||||
outsider = data.createUser("派单路人" + data.uniqueSuffix(), reportOrgId, TENANT);
|
||||
|
||||
// 回退候选:无部门但持有业务角色应保留,仅有 user/guest 角色或无角色的普通消费用户被排除
|
||||
ApprovalTestData.TestRole staffRole = data.createRole("backoffice_staff", "业务管理员", TENANT);
|
||||
// 回退候选:无部门但持有审批参与角色的用户保留;user/guest 或无角色一律排除
|
||||
staffNoOrg = data.createUser("派单无部门业务员" + data.uniqueSuffix(), null, TENANT);
|
||||
data.bindTenant(staffNoOrg.userId, staffRole.roleId, TENANT);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user