流程列表添加角色数据权限

This commit is contained in:
wkc
2026-05-12 16:35:32 +08:00
parent 9ed6fc2d38
commit a22c83ba7e
6 changed files with 221 additions and 2 deletions

View File

@@ -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。

View File

@@ -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)

View File

@@ -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(

View File

@@ -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}, '%')

View File

@@ -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}, '%')"));
}
}

View File

@@ -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;
}
}