diff --git a/sql/04-approval_role_grants.sql b/sql/04-approval_role_grants.sql new file mode 100644 index 0000000..8f8c463 --- /dev/null +++ b/sql/04-approval_role_grants.sql @@ -0,0 +1,59 @@ +-- 审批参与权限授予种子(可重复执行) +-- 目标库:gxtyzx_core(server) +-- 执行顺序:01-approval.sql -> 02-approval_menu.sql -> 03-approval_orgs_roles.sql -> 04-approval_role_grants.sql +-- +-- 背景(#45):审批动作的 @PreAuthorize 按 #14 动作矩阵要求 approval:instance:approve, +-- 而报送人/被派单人是一般员工——参与审批的权限由角色授予承载,接口鉴权不放宽。 +-- +-- 授予矩阵: +-- approval_staff(员工,本文件新建)与既有岗位角色 +-- dept_leader_*、dept_deputy_*、approval_archivist、approval_cashier、 +-- approval_typist、approval_issuer、approval_general_manager +-- -> 页面 /approval、/approval/todo、/approval/instance +-- -> approval:instance:list、submit、approve +-- approval_archivist 额外 -> approval:instance:archive(收文员归档) +-- assign、export 仍仅 superAdmin(02-approval_menu.sql 已授) +-- +-- 成员分配(把 approval_staff 发给员工)由客户在后台完成,同 03 的约定。 +-- 按 tenant_id=10150 执行;换租户时改下面的 @tenant_id。 + +SET @tenant_id := 10150; + +-- 1. 员工基础角色 +INSERT INTO sys_role (role_name, role_code, comments, sort_number, tenant_id, deleted) +SELECT '员工', 'approval_staff', '审批种子:内部员工基础角色,提交/审批/确认自己的单据(#45)', 900, @tenant_id, 0 +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM sys_role WHERE tenant_id = @tenant_id AND deleted = 0 AND role_code = 'approval_staff' +); + +-- 2. 参与人角色:页面 + list/submit/approve +INSERT INTO sys_role_menu (role_id, menu_id, tenant_id) +SELECT r.role_id, m.menu_id, @tenant_id +FROM sys_role r +JOIN sys_menu m + ON m.tenant_id = @tenant_id AND m.deleted = 0 + AND (m.path IN ('/approval', '/approval/todo', '/approval/instance') + OR m.authority IN ('approval:instance:list', 'approval:instance:submit', 'approval:instance:approve')) +WHERE r.tenant_id = @tenant_id AND r.deleted = 0 + AND (r.role_code = 'approval_staff' + OR LEFT(r.role_code, 12) = 'dept_leader_' + OR LEFT(r.role_code, 12) = 'dept_deputy_' + OR r.role_code IN ('approval_archivist', 'approval_cashier', 'approval_typist', + 'approval_issuer', 'approval_general_manager')) + AND NOT EXISTS ( + SELECT 1 FROM sys_role_menu rm + WHERE rm.role_id = r.role_id AND rm.menu_id = m.menu_id AND rm.tenant_id = @tenant_id + ); + +-- 3. 收文员追加归档权限 +INSERT INTO sys_role_menu (role_id, menu_id, tenant_id) +SELECT r.role_id, m.menu_id, @tenant_id +FROM sys_role r +JOIN sys_menu m + ON m.tenant_id = @tenant_id AND m.deleted = 0 AND m.authority = 'approval:instance:archive' +WHERE r.tenant_id = @tenant_id AND r.deleted = 0 AND r.role_code = 'approval_archivist' + AND NOT EXISTS ( + SELECT 1 FROM sys_role_menu rm + WHERE rm.role_id = r.role_id AND rm.menu_id = m.menu_id AND rm.tenant_id = @tenant_id + ); diff --git a/src/main/java/com/gxwebsoft/approval/constant/ApprovalConstants.java b/src/main/java/com/gxwebsoft/approval/constant/ApprovalConstants.java index e922c47..1ce1734 100644 --- a/src/main/java/com/gxwebsoft/approval/constant/ApprovalConstants.java +++ b/src/main/java/com/gxwebsoft/approval/constant/ApprovalConstants.java @@ -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_ / dept_deputy_)。 */ + 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"; diff --git a/src/main/java/com/gxwebsoft/approval/service/ApprovalDirectoryService.java b/src/main/java/com/gxwebsoft/approval/service/ApprovalDirectoryService.java index 1f77412..25acce8 100644 --- a/src/main/java/com/gxwebsoft/approval/service/ApprovalDirectoryService.java +++ b/src/main/java/com/gxwebsoft/approval/service/ApprovalDirectoryService.java @@ -121,19 +121,25 @@ public class ApprovalDirectoryService { return jdbcTemplate.query(sql.toString(), USER_MAPPER, args.toArray()); } - /** 部门启用成员。 */ + /** 部门启用成员中的审批参与人;无审批角色的成员不进入候选池(#45:派给无权限的人会在存档确认时 403)。 */ public List listOrganizationMembers(Integer organizationId) { if (organizationId == null || organizationId <= 0) { return Collections.emptyList(); } Integer tenantId = ApprovalTenantContext.currentTenantId(); + List 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 { } /** - * 派单候选池:优先受理部门启用成员;部门无成员时回退为当前租户的非普通消费用户。 + * 派单候选池:优先受理部门启用成员中的审批参与人;部门无参与人时回退为当前租户的审批参与人。 * - *

回退排除「未归属组织且角色仅为 user/guest 或无角色」的账号; - * 部门有成员时不混入回退候选。

+ *

审批参与人 = 持有员工或任一审批岗位角色的用户(#45 授予矩阵,见 sql/04-approval_role_grants.sql); + * 两个分支同一判定,避免把单据派给无权限、存档确认必然 403 的人。

*/ public List listDispatchCandidates(Integer acceptOrganizationId) { List members = listOrganizationMembers(acceptOrganizationId); @@ -184,19 +190,37 @@ public class ApprovalDirectoryService { return members; } Integer tenantId = ApprovalTenantContext.currentTenantId(); + List 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 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 判断)。 */ diff --git a/src/test/java/com/gxwebsoft/approval/ApprovalConfirmArchiveIT.java b/src/test/java/com/gxwebsoft/approval/ApprovalConfirmArchiveIT.java index 2c02e97..1348d78 100644 --- a/src/test/java/com/gxwebsoft/approval/ApprovalConfirmArchiveIT.java +++ b/src/test/java/com/gxwebsoft/approval/ApprovalConfirmArchiveIT.java @@ -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("归档或签"); diff --git a/src/test/java/com/gxwebsoft/approval/ApprovalDispatchIT.java b/src/test/java/com/gxwebsoft/approval/ApprovalDispatchIT.java index 9851d5f..9f335ef 100644 --- a/src/test/java/com/gxwebsoft/approval/ApprovalDispatchIT.java +++ b/src/test/java/com/gxwebsoft/approval/ApprovalDispatchIT.java @@ -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);