Revert "✨ 应收账单接入角色数据权限并同步在线缓存"

This reverts commit 4659c63ef9.
This commit is contained in:
caojia
2026-10-27 15:28:52 +08:00
parent 4d4065b9e9
commit fe30345045
13 changed files with 24 additions and 649 deletions
@@ -143,13 +143,7 @@ public class SysRoleController extends BaseController
{
roleService.checkRoleAllowed(role);
roleService.checkRoleDataScope(role.getRoleId());
int rows = roleService.authDataScope(role);
if (rows > 0)
{
SysRole savedRole = roleService.selectRoleById(role.getRoleId());
tokenService.refreshPermissionByRoleId(role.getRoleId(), permissionService, savedRole.getDataScope());
}
return toAjax(rows);
return toAjax(roleService.authDataScope(role));
}
/**
@@ -1,378 +0,0 @@
package com.bms.ar;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.session.Configuration;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.springframework.aop.aspectj.annotation.AspectJProxyFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.test.util.ReflectionTestUtils;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.bms.ar.domain.ArBill;
import com.bms.ar.domain.ArBillExport;
import com.bms.ar.domain.ArBillGenerateTask;
import com.bms.ar.domain.ArBillLog;
import com.bms.ar.domain.vo.ArBillGenerateQuery;
import com.bms.ar.domain.vo.ArBillGenerateRequest;
import com.bms.ar.domain.vo.ArBillQuery;
import com.bms.ar.mapper.ArBillDetailFeeMapper;
import com.bms.ar.mapper.ArBillDetailMapper;
import com.bms.ar.mapper.ArBillExportMapper;
import com.bms.ar.mapper.ArBillGenerateTaskMapper;
import com.bms.ar.mapper.ArBillLogMapper;
import com.bms.ar.mapper.ArBillMapper;
import com.bms.ar.service.ArBillDataScopeService;
import com.bms.ar.service.impl.ArBillServiceImpl;
import com.bms.common.core.domain.entity.SysRole;
import com.bms.common.core.domain.entity.SysUser;
import com.bms.common.core.domain.model.LoginUser;
import com.bms.common.exception.ServiceException;
import com.bms.common.utils.file.OssStorage;
import com.bms.framework.aspectj.DataScopeAspect;
import com.bms.price.mapper.PrcOrderBillMapper;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/** 不启动应用或连接业务数据库,验证真实切面、Mapper SQL 和业务入口的越权阻断。 */
class ArBillDataScopeTest {
private ArBillMapper billMapper;
private ArBillDetailMapper detailMapper;
private ArBillDetailFeeMapper feeMapper;
private ArBillExportMapper exportMapper;
private ArBillGenerateTaskMapper taskMapper;
private ArBillLogMapper logMapper;
private PrcOrderBillMapper sourceMapper;
private ArBillDataScopeService scopeService;
private ArBillServiceImpl service;
@BeforeEach
void setUp() {
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(new MockHttpServletRequest()));
billMapper = mock(ArBillMapper.class);
detailMapper = mock(ArBillDetailMapper.class);
feeMapper = mock(ArBillDetailFeeMapper.class);
exportMapper = mock(ArBillExportMapper.class);
taskMapper = mock(ArBillGenerateTaskMapper.class);
logMapper = mock(ArBillLogMapper.class);
sourceMapper = mock(PrcOrderBillMapper.class);
ArBillDataScopeService scopeTarget = new ArBillDataScopeService();
ReflectionTestUtils.setField(scopeTarget, "billMapper", billMapper);
ReflectionTestUtils.setField(scopeTarget, "logMapper", logMapper);
ReflectionTestUtils.setField(scopeTarget, "generateTaskMapper", taskMapper);
scopeService = proxy(scopeTarget);
ArBillServiceImpl target = new ArBillServiceImpl();
ReflectionTestUtils.setField(target, "billMapper", billMapper);
ReflectionTestUtils.setField(target, "detailMapper", detailMapper);
ReflectionTestUtils.setField(target, "detailFeeMapper", feeMapper);
ReflectionTestUtils.setField(target, "exportMapper", exportMapper);
ReflectionTestUtils.setField(target, "generateTaskMapper", taskMapper);
ReflectionTestUtils.setField(target, "logMapper", logMapper);
ReflectionTestUtils.setField(target, "sourceBillMapper", sourceMapper);
ReflectionTestUtils.setField(target, "dataScopeService", scopeService);
service = proxy(target);
login(42L, role(10L, "5", "ar:bill:list"));
}
@AfterEach
void cleanUp() {
SecurityContextHolder.clearContext();
RequestContextHolder.resetRequestAttributes();
PageHelper.clearPage();
}
@ParameterizedTest
@CsvSource({"1, ALL", "2, role_id = 10", "3, ar_scope_user.dept_id = 200",
"4, ancestors", "5, ar_scope_user.user_id = 42"})
void appliesAllFiveRoleScopesAndClearsClientSuppliedSql(String scope, String expected) {
login(42L, role(10L, scope, "ar:bill:list"));
ArBillQuery query = new ArBillQuery();
query.getParams().put("dataScope", " OR 1=1 /* client supplied */");
service.selectArBillList(query);
String sql = scope(query);
if ("ALL".equals(expected)) {
assertEquals("", sql);
} else {
assertTrue(sql.contains(expected), sql);
}
assertFalse(sql.contains("client supplied"));
verify(billMapper).selectArBillList(same(query));
}
@Test
void mergesOnlyRolesWithBillListPermissionAndIgnoresDisabledRoles() {
SysRole disabled = role(13L, "1", "ar:bill:list");
disabled.setStatus("1");
login(42L, role(10L, "5", "ar:bill:list"), role(11L, "3", "ar:bill:list"),
role(12L, "1", "other:list"), disabled);
ArBillQuery query = new ArBillQuery();
service.selectArBillList(query);
assertTrue(scope(query).contains("ar_scope_user.user_id = 42"));
assertTrue(scope(query).contains("ar_scope_user.dept_id = 200"));
assertTrue(scope(query).contains(" OR "));
login(42L, role(10L, "5", "ar:bill:list"), role(11L, "1", "ar:bill:list"));
service.selectArBillList(query);
assertEquals("", scope(query));
}
@Test
void superAdminIsUnrestrictedButNoMatchingRoleGetsNoRows() {
ArBillQuery query = new ArBillQuery();
login(1L);
service.selectArBillList(query);
assertEquals("", scope(query));
login(42L, role(10L, "1", "other:list"));
service.selectArBillList(query);
assertTrue(scope(query).contains("ar_scope_user.dept_id = 0"));
}
@Test
void combinesCustomDepartmentRoles() {
login(42L, role(10L, "2", "ar:bill:list"), role(11L, "2", "ar:bill:list"));
ArBillQuery query = new ArBillQuery();
service.selectArBillList(query);
assertTrue(scope(query).contains("role_id in (10,11)"));
}
@Test
void rejectsBillIdsBeforeReadingDetailsOrRunningActions() {
forbidden(() -> service.selectArBillById(80L));
forbidden(() -> service.selectArBillDetails(80L));
forbidden(() -> service.selectArBillDetailFees(80L, 1L));
forbidden(() -> service.selectExports(80L));
forbidden(() -> service.confirmBill(80L, null, "tester", "confirm"));
forbidden(() -> service.voidBill(80L, null, "tester", "void"));
forbidden(() -> service.exportBill(80L, "tester", "export"));
verifyNoInteractions(detailMapper, feeMapper, exportMapper, logMapper);
verify(billMapper, never()).selectArBillByIdForUpdate(anyLong());
}
@Test
void checksExportAndGenerateTaskOwnershipThroughTheirBill() throws Exception {
ArBillExport export = new ArBillExport();
export.setArBillId(80L);
export.setExportStatus("SUCCESS");
export.setStorageKey("oss://test/bill.xlsx");
when(exportMapper.selectById(9L)).thenReturn(export);
MockHttpServletResponse response = new MockHttpServletResponse();
forbidden(() -> service.selectExport(9L));
forbidden(() -> service.downloadExport(9L, response));
assertEquals(0, response.getContentAsByteArray().length);
assertNull(response.getHeader("Content-Disposition"));
ArBillGenerateTask task = new ArBillGenerateTask();
task.setArBillId(80L);
when(taskMapper.selectById(3L)).thenReturn(task);
forbidden(() -> service.selectGenerateTask(3L));
forbidden(() -> service.cancelGenerateTask(3L, "tester", "cancel"));
forbidden(() -> service.retryGenerateTask(3L, "tester", "retry"));
verify(taskMapper, never()).markCancelRequested(anyLong());
}
@Test
void authorizedBillStillReturnsMaskedAccountAndDownloadsItsExport() throws Exception {
ArBill bill = new ArBill();
bill.setArBillId(80L);
bill.setBankAccountNo("123456780504");
when(billMapper.selectArBillByDataScope(any())).thenReturn(bill);
assertEquals("****0504", service.selectArBillById(80L).getBankAccountNo());
ArBillExport export = new ArBillExport();
export.setArBillId(80L);
export.setExportStatus("SUCCESS");
export.setStorageKey("oss://test/bill.xlsx");
export.setFileName("bill.xlsx");
when(exportMapper.selectById(9L)).thenReturn(export);
OssStorage storage = mock(OssStorage.class);
ReflectionTestUtils.setField(service, "ossStorage", storage);
doAnswer(invocation -> {
((OutputStream) invocation.getArgument(1)).write(new byte[] {1, 2, 3});
return null;
}).when(storage).download(eq("oss://test/bill.xlsx"), any(OutputStream.class));
MockHttpServletResponse response = new MockHttpServletResponse();
service.downloadExport(9L, response);
assertArrayEquals(new byte[] {1, 2, 3}, response.getContentAsByteArray());
assertTrue(response.getHeader("Content-Disposition").contains("bill.xlsx"));
}
@Test
void permissionLookupPreservesDetailPaginationAndClearsItOnDenial() {
Page<Object> page = PageHelper.startPage(3, 20);
page.setOrderBy("detail_no desc");
when(billMapper.selectArBillByDataScope(any())).thenAnswer(invocation -> {
assertNull(PageHelper.getLocalPage(), "权限校验不能消耗明细分页");
assertTrue(scope(invocation.getArgument(0)).contains("ar_scope_user.user_id = 42"));
return new ArBill();
});
when(detailMapper.selectByBillId(80L)).thenAnswer(invocation -> {
assertSame(page, PageHelper.getLocalPage());
assertEquals("detail_no desc", page.getOrderBy());
PageHelper.clearPage();
return Collections.emptyList();
});
service.selectArBillDetails(80L);
assertNull(PageHelper.getLocalPage());
doReturn(null).when(billMapper).selectArBillByDataScope(any());
PageHelper.startPage(3, 20);
forbidden(() -> service.selectArBillDetails(81L));
assertNull(PageHelper.getLocalPage(), "拒绝请求后不能向下一请求遗留分页");
}
@Test
void rejectsCustomerPreviewAndGenerationBeforeReadingSourceOrders() {
ArBillGenerateQuery preview = new ArBillGenerateQuery();
preview.setCustomerId(100L);
preview.setBillPeriodEnd(new Date());
forbidden(() -> service.previewBill(preview));
forbidden(() -> service.sumPreviewBill(preview));
ArBillGenerateRequest request = new ArBillGenerateRequest();
request.setCustomerId(100L);
request.setBillName("test");
request.setBillPeriodEnd(new Date());
forbidden(() -> service.generateBill(request, "tester", "generate"));
verifyNoInteractions(sourceMapper, taskMapper);
}
@Test
void authorizedCustomerPreviewRetainsItsOriginalPagination() {
Page<Object> page = PageHelper.startPage(2, 10);
when(billMapper.selectCustomerIdByDataScope(any())).thenAnswer(invocation -> {
assertNull(PageHelper.getLocalPage());
return 100L;
});
doAnswer(invocation -> {
assertSame(page, PageHelper.getLocalPage());
PageHelper.clearPage();
return Collections.emptyList();
}).when(sourceMapper).selectPrcOrderBillByCustomerPeriod(eq(100L), any(), anyList(), anyList(),
any(), any(), anyList(), any(), any());
ArBillGenerateQuery preview = new ArBillGenerateQuery();
preview.setCustomerId(100L);
preview.setBillPeriodEnd(new Date());
service.previewBill(preview);
assertNull(PageHelper.getLocalPage());
}
@Test
void exportIdempotencyCannotReturnAnExportFromAnotherBill() {
when(billMapper.selectArBillByDataScope(any())).thenReturn(new ArBill());
ArBillLog previous = new ArBillLog();
previous.setObjectId("9");
when(logMapper.selectSuccessByRequestId(anyString(), anyString(), anyString())).thenReturn(previous);
ArBillExport export = new ArBillExport();
export.setArBillId(81L);
when(exportMapper.selectById(9L)).thenReturn(export);
ServiceException denied = assertThrows(ServiceException.class,
() -> service.exportBill(80L, "tester", "reused"));
assertTrue(denied.getMessage().contains("其他账单"));
verify(exportMapper, never()).insert(any());
}
@Test
void logAndActiveTaskQueriesUseTheSameRoleScope() {
scopeService.selectLogs(new ArBillQuery(), new ArBillLog());
scopeService.selectActiveGenerateTasks(new ArBillQuery(), "tester");
verify(logMapper).selectList(any(), argThat(params ->
params.get("dataScope").toString().contains("ar_scope_user.user_id = 42")));
verify(taskMapper).selectActiveByCreateBy(eq("tester"), argThat(params ->
params.get("dataScope").toString().contains("ar_scope_user.user_id = 42")));
}
@Test
void allReadMappersRenderScopedSqlAndKeepNormalFilters() throws Exception {
Configuration configuration = new Configuration();
configuration.getTypeAliasRegistry().registerAlias("ArBill", ArBill.class);
configuration.getTypeAliasRegistry().registerAlias("ArBillLog", ArBillLog.class);
configuration.getTypeAliasRegistry().registerAlias("ArBillGenerateTask", ArBillGenerateTask.class);
for (String name : List.of("ArBillMapper", "ArBillLogMapper", "ArBillGenerateTaskMapper")) {
String resource = "mapper/ar/" + name + ".xml";
try (InputStream input = new ClassPathResource(resource).getInputStream()) {
new XMLMapperBuilder(input, configuration, resource, configuration.getSqlFragments()).parse();
}
}
ArBillQuery query = new ArBillQuery();
query.setArBillId(80L);
query.setCustomerId(100L);
query.setStatus("CONFIRMED");
service.selectArBillList(query);
String listSql = sql(configuration, "ArBillMapper.selectArBillList", query);
assertTrue(listSql.contains("status = ?"));
assertTrue(listSql.contains("customer_id = ?"));
assertTrue(listSql.contains("ar_scope_customer.customer_id = ar_bill.customer_id"));
assertTrue(listSql.contains("ar_scope_id_user.user_id is null"), "历史工号仅在ID不存在时兜底");
assertTrue(listSql.contains("exists ("), "EXISTS 避免业务员关联放大列表和分页总数");
assertFalse(listSql.contains("${"));
for (String statement : List.of("ArBillMapper.selectArBillByDataScope", "ArBillMapper.selectCustomerIdByDataScope")) {
assertTrue(sql(configuration, statement, query).contains("ar_scope_user.user_id = 42"));
}
Map<String, Object> logs = Map.of("query", new ArBillLog(), "params", query.getParams());
assertTrue(sql(configuration, "ArBillLogMapper.selectList", logs).contains("ar_scope_customer.customer_id = b.customer_id"));
Map<String, Object> tasks = Map.of("createBy", "tester", "params", query.getParams());
assertTrue(sql(configuration, "ArBillGenerateTaskMapper.selectActiveByCreateBy", tasks)
.contains("b.ar_bill_id = ar_bill_generate_task.ar_bill_id"));
login(42L, role(10L, "1", "ar:bill:list"));
service.selectArBillList(query);
assertFalse(sql(configuration, "ArBillMapper.selectArBillList", query).contains("ar_scope_customer"));
}
private String sql(Configuration configuration, String statement, Object parameters) {
return configuration.getMappedStatement("com.bms.ar.mapper." + statement).getBoundSql(parameters)
.getSql().replaceAll("\\s+", " ");
}
private String scope(ArBillQuery query) {
return String.valueOf(query.getParams().get("dataScope"));
}
private void forbidden(Executable action) {
assertEquals(403, assertThrows(ServiceException.class, action).getCode());
}
private SysRole role(Long id, String scope, String permission) {
SysRole role = new SysRole(id);
role.setDataScope(scope);
role.setStatus("0");
role.setPermissions(Set.of(permission));
return role;
}
private void login(Long userId, SysRole... roles) {
SysUser user = new SysUser(userId);
user.setDeptId(200L);
user.setUserName("tester");
user.setRoles(List.of(roles));
LoginUser login = new LoginUser(userId, 200L, user, Set.of("ar:bill:list"));
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(login, null, Collections.emptyList()));
}
@SuppressWarnings("unchecked")
private <T> T proxy(T target) {
AspectJProxyFactory factory = new AspectJProxyFactory(target);
factory.setProxyTargetClass(true);
factory.addAspect(new DataScopeAspect());
return (T) factory.getProxy();
}
}
@@ -1,68 +0,0 @@
package com.bms.system;
import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import com.bms.common.constant.CacheConstants;
import com.bms.common.core.domain.entity.SysRole;
import com.bms.common.core.domain.entity.SysUser;
import com.bms.common.core.domain.model.LoginUser;
import com.bms.common.core.redis.RedisCache;
import com.bms.framework.web.service.SysPermissionService;
import com.bms.framework.web.service.TokenService;
import com.bms.system.service.ISysRoleService;
import com.bms.web.controller.system.SysRoleController;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
class SysRoleDataScopeRefreshTest {
@Test
void savingDataScopeRefreshesOnlineUsersWithPersistedScope() {
ISysRoleService roles = mock(ISysRoleService.class);
TokenService tokens = mock(TokenService.class);
SysPermissionService permissions = mock(SysPermissionService.class);
SysRoleController controller = new SysRoleController();
ReflectionTestUtils.setField(controller, "roleService", roles);
ReflectionTestUtils.setField(controller, "tokenService", tokens);
ReflectionTestUtils.setField(controller, "permissionService", permissions);
SysRole submitted = new SysRole(10L);
submitted.setDataScope("5");
SysRole persisted = new SysRole(10L);
persisted.setDataScope("5");
when(roles.authDataScope(submitted)).thenReturn(1);
when(roles.selectRoleById(10L)).thenReturn(persisted);
controller.dataScope(submitted);
verify(tokens).refreshPermissionByRoleId(10L, permissions, "5");
}
@Test
void replacesCachedDataScopeBeforeSavingSessionAndPreservesOtherRoles() {
RedisCache redis = mock(RedisCache.class);
SysPermissionService permissions = mock(SysPermissionService.class);
TokenService tokens = spy(new TokenService());
ReflectionTestUtils.setField(tokens, "redisCache", redis);
doNothing().when(tokens).refreshToken(any());
SysRole target = new SysRole(10L);
target.setDataScope("1");
SysRole other = new SysRole(11L);
other.setDataScope("3");
SysUser user = new SysUser(42L);
user.setRoles(List.of(target, other));
LoginUser login = new LoginUser(42L, 200L, user, Set.of("ar:bill:list"));
String key = CacheConstants.LOGIN_TOKEN_KEY + "test";
when(redis.keys(CacheConstants.LOGIN_TOKEN_KEY + "*")).thenReturn(List.of(key));
when(redis.getCacheObject(key)).thenReturn(login);
when(permissions.getMenuPermission(user)).thenAnswer(invocation -> {
assertEquals("5", target.getDataScope());
return Set.of("ar:bill:list");
});
tokens.refreshPermissionByRoleId(10L, permissions, "5");
assertEquals("5", login.getUser().getRoles().get(0).getDataScope());
assertEquals("3", other.getDataScope());
verify(tokens).refreshToken(login);
}
}