流程列表添加角色数据权限
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
# 流程列表角色数据权限实施记录
|
||||
|
||||
## 修改日期
|
||||
|
||||
2026-05-12
|
||||
|
||||
## 需求范围
|
||||
|
||||
- 仅控制 `GET /loanPricing/workflow/list` 流程列表接口。
|
||||
- 超级管理员 `user_id=1`、启用角色名为“管理员”或角色标识为 `headAdmin` 的用户可查看全部流程。
|
||||
- 非管理员用户只能查看 `loan_pricing_workflow.create_by` 精确等于当前登录人 `昵称-柜员号` 的流程。
|
||||
- 列表页“创建者”查询参数继续保留,但只按 `create_by` 中的柜员号部分进行模糊匹配。
|
||||
|
||||
## 修改内容
|
||||
|
||||
- `LoanPricingWorkflow` 增加非表字段 `dataScopeCreateBy`,专用于后端内部数据权限精确过滤。
|
||||
- `LoanPricingWorkflowServiceImpl.selectLoanPricingPage` 增加流程列表数据权限裁剪:
|
||||
- 管理员不写入 `dataScopeCreateBy`。
|
||||
- 非管理员写入 `dataScopeCreateBy = nickName + "-" + username`。
|
||||
- 前端传入 `createBy` 时仍保留原查询参数,但不能扩大非管理员可见范围。
|
||||
- `LoanPricingWorkflowMapper.xml` 增加 `lpw.create_by = #{query.dataScopeCreateBy}` 精确权限条件。
|
||||
- `LoanPricingWorkflowMapper.xml` 将 `createBy` 查询调整为 `SUBSTRING_INDEX(lpw.create_by, '-', -1) LIKE ...`,即只按柜员号模糊匹配。
|
||||
- 补充 `LoanPricingWorkflowServiceImplTest` 和 `LoanPricingWorkflowMapperXmlTest`,覆盖管理员、业务管理员、客户经理、越权创建者查询参数和 XML 条件。
|
||||
|
||||
## 验证记录
|
||||
|
||||
- 单元测试通过:
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest,LoanPricingWorkflowMapperXmlTest -Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
- 后端打包通过:
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-admin -am clean package -DskipTests
|
||||
```
|
||||
|
||||
- API 验证通过:
|
||||
- `admin/admin123` 查询流程列表返回全量数据,包含测试行和 `若依-admin` 历史行。
|
||||
- `8929999/123456` 业务管理员查询流程列表返回全量数据。
|
||||
- `8920001/123456` 客户经理查询流程列表只返回本人创建的测试行。
|
||||
- 客户经理按 `createBy=8920001` 查询可返回本人测试行。
|
||||
- 客户经理按昵称 `createBy=测试客户经理` 查询返回 0 条。
|
||||
- 客户经理按其他柜员号 `createBy=admin` 查询返回 0 条。
|
||||
|
||||
- browser-use 真实页面验证通过:
|
||||
- 管理员登录真实流程列表页,页面显示 `共 40 条`,可见本人测试行和 `若依-admin` 历史行。
|
||||
- 客户经理登录真实流程列表页,页面只显示本人创建的测试行。
|
||||
- 客户经理在“创建者”输入 `admin` 后页面显示暂无数据。
|
||||
- 客户经理在“创建者”输入 `8920001` 后页面重新显示本人测试行。
|
||||
|
||||
## 验证数据与清理
|
||||
|
||||
- 因当前开发库创建流程接口依赖的模型输出表缺少 `coupon_rate` 字段,无法通过页面新增生成验证流程。
|
||||
- 本次验证使用一条临时 SQL 测试数据:`serial_num = ROLE_SCOPE_20260512_001`,`create_by = 测试客户经理-8920001`。
|
||||
- 验证结束后已删除该临时数据,回查剩余数量为 0。
|
||||
@@ -150,6 +150,10 @@ public class LoanPricingWorkflow implements Serializable
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private String createBy;
|
||||
|
||||
/** 列表数据权限创建者过滤条件 */
|
||||
@TableField(exist = false)
|
||||
private String dataScopeCreateBy;
|
||||
|
||||
/** 创建时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
|
||||
@@ -3,7 +3,12 @@ package com.ruoyi.loanpricing.service.impl;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ruoyi.common.constant.UserConstants;
|
||||
import com.ruoyi.common.core.domain.entity.SysRole;
|
||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
import com.ruoyi.common.core.domain.model.LoginUser;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.loanpricing.domain.dto.CorporateLoanPricingCreateDTO;
|
||||
import com.ruoyi.loanpricing.domain.dto.PersonalLoanPricingCreateDTO;
|
||||
import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow;
|
||||
@@ -38,6 +43,10 @@ import java.util.Objects;
|
||||
@Service
|
||||
public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowService
|
||||
{
|
||||
private static final String WORKFLOW_ADMIN_ROLE_NAME = "管理员";
|
||||
|
||||
private static final String WORKFLOW_ADMIN_ROLE_KEY = "headAdmin";
|
||||
|
||||
@Resource
|
||||
private LoanPricingWorkflowMapper loanPricingWorkflowMapper;
|
||||
|
||||
@@ -209,7 +218,8 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi
|
||||
@Override
|
||||
public IPage<LoanPricingWorkflowListVO> selectLoanPricingPage(Page<LoanPricingWorkflowListVO> page, LoanPricingWorkflow loanPricingWorkflow)
|
||||
{
|
||||
IPage<LoanPricingWorkflowListVO> pageResult = loanPricingWorkflowMapper.selectWorkflowPageWithRates(page, loanPricingWorkflow);
|
||||
LoanPricingWorkflow scopedQuery = applyWorkflowListDataScope(loanPricingWorkflow);
|
||||
IPage<LoanPricingWorkflowListVO> pageResult = loanPricingWorkflowMapper.selectWorkflowPageWithRates(page, scopedQuery);
|
||||
pageResult.getRecords().forEach(row -> row.setCustName(
|
||||
loanPricingSensitiveDisplayService.maskCustName(
|
||||
sensitiveFieldCryptoService.decrypt(row.getCustName()))));
|
||||
@@ -292,6 +302,41 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
private LoanPricingWorkflow applyWorkflowListDataScope(LoanPricingWorkflow query)
|
||||
{
|
||||
LoanPricingWorkflow scopedQuery = query == null ? new LoanPricingWorkflow() : query;
|
||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||
if (!canViewAllWorkflows(loginUser))
|
||||
{
|
||||
scopedQuery.setDataScopeCreateBy(buildCurrentCreateBy(loginUser));
|
||||
}
|
||||
return scopedQuery;
|
||||
}
|
||||
|
||||
private boolean canViewAllWorkflows(LoginUser loginUser)
|
||||
{
|
||||
SysUser user = loginUser.getUser();
|
||||
if (user.isAdmin())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
List<SysRole> roles = user.getRoles();
|
||||
if (roles == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return roles.stream().anyMatch(role -> role != null
|
||||
&& UserConstants.ROLE_NORMAL.equals(role.getStatus())
|
||||
&& (WORKFLOW_ADMIN_ROLE_NAME.equals(role.getRoleName())
|
||||
|| WORKFLOW_ADMIN_ROLE_KEY.equals(role.getRoleKey())));
|
||||
}
|
||||
|
||||
private String buildCurrentCreateBy(LoginUser loginUser)
|
||||
{
|
||||
SysUser user = loginUser.getUser();
|
||||
return user.getNickName() + "-" + loginUser.getUsername();
|
||||
}
|
||||
|
||||
private void maskModelRetailOutputBasicInfo(ModelRetailOutputFields modelRetailOutputFields)
|
||||
{
|
||||
modelRetailOutputFields.setCustName(
|
||||
|
||||
@@ -23,8 +23,11 @@
|
||||
LEFT JOIN model_retail_output_fields mr ON lpw.model_output_id = mr.id
|
||||
LEFT JOIN model_corp_output_fields mc ON lpw.model_output_id = mc.id
|
||||
<where>
|
||||
<if test="query != null and query.dataScopeCreateBy != null and query.dataScopeCreateBy != ''">
|
||||
AND lpw.create_by = #{query.dataScopeCreateBy}
|
||||
</if>
|
||||
<if test="query != null and query.createBy != null and query.createBy != ''">
|
||||
AND lpw.create_by LIKE CONCAT('%', #{query.createBy}, '%')
|
||||
AND SUBSTRING_INDEX(lpw.create_by, '-', -1) LIKE CONCAT('%', #{query.createBy}, '%')
|
||||
</if>
|
||||
<if test="query != null and query.custIsn != null and query.custIsn != ''">
|
||||
AND lpw.cust_isn LIKE CONCAT('%', #{query.custIsn}, '%')
|
||||
|
||||
@@ -18,5 +18,7 @@ class LoanPricingWorkflowMapperXmlTest
|
||||
|
||||
assertTrue(xml.contains("WHEN lpw.cust_type = '个人' THEN mr.final_calculate_rate"));
|
||||
assertTrue(xml.contains("WHEN lpw.cust_type = '企业' THEN mc.calculate_rate"));
|
||||
assertTrue(xml.contains("lpw.create_by = #{query.dataScopeCreateBy}"));
|
||||
assertTrue(xml.contains("SUBSTRING_INDEX(lpw.create_by, '-', -1) LIKE CONCAT('%', #{query.createBy}, '%')"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.ruoyi.loanpricing.service.impl;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
@@ -13,6 +14,10 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ruoyi.common.constant.UserConstants;
|
||||
import com.ruoyi.common.core.domain.entity.SysRole;
|
||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
import com.ruoyi.common.core.domain.model.LoginUser;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.loanpricing.domain.dto.CorporateLoanPricingCreateDTO;
|
||||
import com.ruoyi.loanpricing.domain.dto.PersonalLoanPricingCreateDTO;
|
||||
@@ -28,12 +33,16 @@ import com.ruoyi.loanpricing.service.LoanPricingSensitiveDisplayService;
|
||||
import com.ruoyi.loanpricing.service.LoanPricingModelService;
|
||||
import com.ruoyi.loanpricing.service.SensitiveFieldCryptoService;
|
||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Objects;
|
||||
@@ -62,6 +71,18 @@ class LoanPricingWorkflowServiceImplTest
|
||||
@InjectMocks
|
||||
private LoanPricingWorkflowServiceImpl loanPricingWorkflowService;
|
||||
|
||||
@BeforeEach
|
||||
void setUpLoginUser()
|
||||
{
|
||||
setLoginUser(1L, "admin", "若依", role(1L, "超级管理员", "admin"));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void clearLoginUser()
|
||||
{
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldEncryptCustNameAndIdNumBeforeInsert()
|
||||
{
|
||||
@@ -116,6 +137,64 @@ class LoanPricingWorkflowServiceImplTest
|
||||
assertEquals("张*", result.getRecords().get(0).getCustName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotSetDataScopeCreateByForSuperAdminWhenReturningPagedWorkflowList()
|
||||
{
|
||||
when(loanPricingWorkflowMapper.selectWorkflowPageWithRates(any(), any())).thenReturn(emptyPageResult());
|
||||
|
||||
loanPricingWorkflowService.selectLoanPricingPage(new Page<>(1, 10), new LoanPricingWorkflow());
|
||||
|
||||
ArgumentCaptor<LoanPricingWorkflow> queryCaptor = ArgumentCaptor.forClass(LoanPricingWorkflow.class);
|
||||
verify(loanPricingWorkflowMapper).selectWorkflowPageWithRates(any(), queryCaptor.capture());
|
||||
|
||||
assertNull(queryCaptor.getValue().getDataScopeCreateBy());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotSetDataScopeCreateByForBusinessAdminWhenReturningPagedWorkflowList()
|
||||
{
|
||||
setLoginUser(100L, "8929999", "测试管理员", role(100L, "管理员", "headAdmin"));
|
||||
when(loanPricingWorkflowMapper.selectWorkflowPageWithRates(any(), any())).thenReturn(emptyPageResult());
|
||||
|
||||
loanPricingWorkflowService.selectLoanPricingPage(new Page<>(1, 10), new LoanPricingWorkflow());
|
||||
|
||||
ArgumentCaptor<LoanPricingWorkflow> queryCaptor = ArgumentCaptor.forClass(LoanPricingWorkflow.class);
|
||||
verify(loanPricingWorkflowMapper).selectWorkflowPageWithRates(any(), queryCaptor.capture());
|
||||
|
||||
assertNull(queryCaptor.getValue().getDataScopeCreateBy());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSetCurrentCreateByForCustomerManagerWhenReturningPagedWorkflowList()
|
||||
{
|
||||
setLoginUser(101L, "8920001", "测试客户经理", role(101L, "客户经理", "common"));
|
||||
when(loanPricingWorkflowMapper.selectWorkflowPageWithRates(any(), any())).thenReturn(emptyPageResult());
|
||||
|
||||
loanPricingWorkflowService.selectLoanPricingPage(new Page<>(1, 10), new LoanPricingWorkflow());
|
||||
|
||||
ArgumentCaptor<LoanPricingWorkflow> queryCaptor = ArgumentCaptor.forClass(LoanPricingWorkflow.class);
|
||||
verify(loanPricingWorkflowMapper).selectWorkflowPageWithRates(any(), queryCaptor.capture());
|
||||
|
||||
assertEquals("测试客户经理-8920001", queryCaptor.getValue().getDataScopeCreateBy());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldKeepCustomerManagerWithinOwnDataScopeWhenCreateByQueryIsProvided()
|
||||
{
|
||||
setLoginUser(101L, "8920001", "测试客户经理", role(101L, "客户经理", "common"));
|
||||
LoanPricingWorkflow query = new LoanPricingWorkflow();
|
||||
query.setCreateBy("若依-admin");
|
||||
when(loanPricingWorkflowMapper.selectWorkflowPageWithRates(any(), any())).thenReturn(emptyPageResult());
|
||||
|
||||
loanPricingWorkflowService.selectLoanPricingPage(new Page<>(1, 10), query);
|
||||
|
||||
ArgumentCaptor<LoanPricingWorkflow> queryCaptor = ArgumentCaptor.forClass(LoanPricingWorkflow.class);
|
||||
verify(loanPricingWorkflowMapper).selectWorkflowPageWithRates(any(), queryCaptor.capture());
|
||||
|
||||
assertEquals("若依-admin", queryCaptor.getValue().getCreateBy());
|
||||
assertEquals("测试客户经理-8920001", queryCaptor.getValue().getDataScopeCreateBy());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseCustIsnInsteadOfCustNameAsQueryCondition()
|
||||
{
|
||||
@@ -399,4 +478,33 @@ class LoanPricingWorkflowServiceImplTest
|
||||
workflow.setBusinessType("新增");
|
||||
return workflow;
|
||||
}
|
||||
|
||||
private Page<LoanPricingWorkflowListVO> emptyPageResult()
|
||||
{
|
||||
Page<LoanPricingWorkflowListVO> pageResult = new Page<>(1, 10);
|
||||
pageResult.setRecords(Collections.emptyList());
|
||||
return pageResult;
|
||||
}
|
||||
|
||||
private void setLoginUser(Long userId, String username, String nickName, SysRole... roles)
|
||||
{
|
||||
SysUser user = new SysUser();
|
||||
user.setUserId(userId);
|
||||
user.setUserName(username);
|
||||
user.setNickName(nickName);
|
||||
user.setRoles(java.util.Arrays.asList(roles));
|
||||
LoginUser loginUser = new LoginUser(userId, 100L, user, Collections.emptySet());
|
||||
UsernamePasswordAuthenticationToken authentication =
|
||||
new UsernamePasswordAuthenticationToken(loginUser, null, Collections.emptyList());
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
}
|
||||
|
||||
private SysRole role(Long roleId, String roleName, String roleKey)
|
||||
{
|
||||
SysRole role = new SysRole(roleId);
|
||||
role.setRoleName(roleName);
|
||||
role.setRoleKey(roleKey);
|
||||
role.setStatus(UserConstants.ROLE_NORMAL);
|
||||
return role;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user