feat: 新增 approval_staff 员工角色并收紧派单候选池为审批参与人(#45)
This commit is contained in:
@@ -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
|
||||
);
|
||||
@@ -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