From 4659c63ef94799227852cdc069bed22d7e19e0ca Mon Sep 17 00:00:00 2001 From: caojia <1301869380@qq.com> Date: Tue, 27 Oct 2026 11:29:24 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E5=BA=94=E6=94=B6=E8=B4=A6?= =?UTF-8?q?=E5=8D=95=E6=8E=A5=E5=85=A5=E8=A7=92=E8=89=B2=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E6=9D=83=E9=99=90=E5=B9=B6=E5=90=8C=E6=AD=A5=E5=9C=A8=E7=BA=BF?= =?UTF-8?q?=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/system/SysRoleController.java | 8 +- .../java/com/bms/ar/ArBillDataScopeTest.java | 378 ++++++++++++++++++ .../system/SysRoleDataScopeRefreshTest.java | 68 ++++ .../framework/web/service/TokenService.java | 12 + .../ar/mapper/ArBillGenerateTaskMapper.java | 4 +- .../com/bms/ar/mapper/ArBillLogMapper.java | 3 +- .../java/com/bms/ar/mapper/ArBillMapper.java | 4 + .../ar/service/ArBillDataScopeService.java | 82 ++++ .../ar/service/impl/ArBillLogServiceImpl.java | 6 +- .../ar/service/impl/ArBillServiceImpl.java | 51 ++- .../mapper/ar/ArBillGenerateTaskMapper.xml | 6 + .../resources/mapper/ar/ArBillLogMapper.xml | 17 +- .../main/resources/mapper/ar/ArBillMapper.xml | 34 ++ 13 files changed, 649 insertions(+), 24 deletions(-) create mode 100644 bms-back/bms-admin/src/test/java/com/bms/ar/ArBillDataScopeTest.java create mode 100644 bms-back/bms-admin/src/test/java/com/bms/system/SysRoleDataScopeRefreshTest.java create mode 100644 bms-back/bms-system/src/main/java/com/bms/ar/service/ArBillDataScopeService.java diff --git a/bms-back/bms-admin/src/main/java/com/bms/web/controller/system/SysRoleController.java b/bms-back/bms-admin/src/main/java/com/bms/web/controller/system/SysRoleController.java index b06236d5..9c34b0e9 100644 --- a/bms-back/bms-admin/src/main/java/com/bms/web/controller/system/SysRoleController.java +++ b/bms-back/bms-admin/src/main/java/com/bms/web/controller/system/SysRoleController.java @@ -143,7 +143,13 @@ public class SysRoleController extends BaseController { roleService.checkRoleAllowed(role); roleService.checkRoleDataScope(role.getRoleId()); - return toAjax(roleService.authDataScope(role)); + int rows = roleService.authDataScope(role); + if (rows > 0) + { + SysRole savedRole = roleService.selectRoleById(role.getRoleId()); + tokenService.refreshPermissionByRoleId(role.getRoleId(), permissionService, savedRole.getDataScope()); + } + return toAjax(rows); } /** diff --git a/bms-back/bms-admin/src/test/java/com/bms/ar/ArBillDataScopeTest.java b/bms-back/bms-admin/src/test/java/com/bms/ar/ArBillDataScopeTest.java new file mode 100644 index 00000000..93ef3a2c --- /dev/null +++ b/bms-back/bms-admin/src/test/java/com/bms/ar/ArBillDataScopeTest.java @@ -0,0 +1,378 @@ +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 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 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 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 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 proxy(T target) { + AspectJProxyFactory factory = new AspectJProxyFactory(target); + factory.setProxyTargetClass(true); + factory.addAspect(new DataScopeAspect()); + return (T) factory.getProxy(); + } +} diff --git a/bms-back/bms-admin/src/test/java/com/bms/system/SysRoleDataScopeRefreshTest.java b/bms-back/bms-admin/src/test/java/com/bms/system/SysRoleDataScopeRefreshTest.java new file mode 100644 index 00000000..ed094eab --- /dev/null +++ b/bms-back/bms-admin/src/test/java/com/bms/system/SysRoleDataScopeRefreshTest.java @@ -0,0 +1,68 @@ +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); + } +} diff --git a/bms-back/bms-framework/src/main/java/com/bms/framework/web/service/TokenService.java b/bms-back/bms-framework/src/main/java/com/bms/framework/web/service/TokenService.java index 0c3cbc39..0394280d 100644 --- a/bms-back/bms-framework/src/main/java/com/bms/framework/web/service/TokenService.java +++ b/bms-back/bms-framework/src/main/java/com/bms/framework/web/service/TokenService.java @@ -238,6 +238,12 @@ public class TokenService * @param permissionService 权限服务 */ public void refreshPermissionByRoleId(Long roleId, SysPermissionService permissionService) + { + refreshPermissionByRoleId(roleId, permissionService, null); + } + + /** 数据范围变更时同步更新在线登录信息中的角色快照。 */ + public void refreshPermissionByRoleId(Long roleId, SysPermissionService permissionService, String dataScope) { // 扫描所有在线 token String pattern = CacheConstants.LOGIN_TOKEN_KEY + "*"; @@ -261,6 +267,12 @@ public class TokenService { continue; } + if (dataScope != null) + { + loginUser.getUser().getRoles().stream() + .filter(r -> roleId.equals(r.getRoleId())) + .forEach(r -> r.setDataScope(dataScope)); + } // 刷新权限缓存 loginUser.setPermissions(permissionService.getMenuPermission(loginUser.getUser())); refreshToken(loginUser); diff --git a/bms-back/bms-system/src/main/java/com/bms/ar/mapper/ArBillGenerateTaskMapper.java b/bms-back/bms-system/src/main/java/com/bms/ar/mapper/ArBillGenerateTaskMapper.java index 5e0d9af4..fe76c088 100644 --- a/bms-back/bms-system/src/main/java/com/bms/ar/mapper/ArBillGenerateTaskMapper.java +++ b/bms-back/bms-system/src/main/java/com/bms/ar/mapper/ArBillGenerateTaskMapper.java @@ -2,6 +2,7 @@ package com.bms.ar.mapper; import java.util.Date; import java.util.List; +import java.util.Map; import org.apache.ibatis.annotations.Param; import com.bms.ar.domain.ArBillGenerateTask; @@ -14,7 +15,8 @@ public interface ArBillGenerateTaskMapper { ArBillGenerateTask selectByRequestToken(String requestToken); - List selectActiveByCreateBy(@Param("createBy") String createBy); + List selectActiveByCreateBy(@Param("createBy") String createBy, + @Param("params") Map params); List selectWaiting(@Param("limit") int limit); diff --git a/bms-back/bms-system/src/main/java/com/bms/ar/mapper/ArBillLogMapper.java b/bms-back/bms-system/src/main/java/com/bms/ar/mapper/ArBillLogMapper.java index d98f5087..973d668b 100644 --- a/bms-back/bms-system/src/main/java/com/bms/ar/mapper/ArBillLogMapper.java +++ b/bms-back/bms-system/src/main/java/com/bms/ar/mapper/ArBillLogMapper.java @@ -1,12 +1,13 @@ package com.bms.ar.mapper; import java.util.List; +import java.util.Map; import org.apache.ibatis.annotations.Param; import com.bms.ar.domain.ArBillLog; public interface ArBillLogMapper { - List selectList(ArBillLog query); + List selectList(@Param("query") ArBillLog query, @Param("params") Map params); ArBillLog selectSuccessByRequestId(@Param("requestId") String requestId, @Param("objectType") String objectType, diff --git a/bms-back/bms-system/src/main/java/com/bms/ar/mapper/ArBillMapper.java b/bms-back/bms-system/src/main/java/com/bms/ar/mapper/ArBillMapper.java index be476768..7b2514be 100644 --- a/bms-back/bms-system/src/main/java/com/bms/ar/mapper/ArBillMapper.java +++ b/bms-back/bms-system/src/main/java/com/bms/ar/mapper/ArBillMapper.java @@ -11,6 +11,10 @@ import com.bms.ar.domain.vo.ArBillQuery; public interface ArBillMapper { List selectArBillList(ArBillQuery query); + ArBill selectArBillByDataScope(ArBillQuery query); + + Long selectCustomerIdByDataScope(ArBillQuery query); + ArBill selectArBillById(Long arBillId); ArBill selectArBillByBillNo(String billNo); diff --git a/bms-back/bms-system/src/main/java/com/bms/ar/service/ArBillDataScopeService.java b/bms-back/bms-system/src/main/java/com/bms/ar/service/ArBillDataScopeService.java new file mode 100644 index 00000000..dad5a6ee --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/ar/service/ArBillDataScopeService.java @@ -0,0 +1,82 @@ +package com.bms.ar.service; + +import java.util.List; +import java.util.function.Supplier; + +import com.github.pagehelper.Page; +import com.github.pagehelper.PageHelper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.bms.ar.domain.ArBill; +import com.bms.ar.domain.ArBillGenerateTask; +import com.bms.ar.domain.ArBillLog; +import com.bms.ar.domain.vo.ArBillQuery; +import com.bms.ar.mapper.ArBillGenerateTaskMapper; +import com.bms.ar.mapper.ArBillLogMapper; +import com.bms.ar.mapper.ArBillMapper; +import com.bms.common.annotation.DataScope; +import com.bms.common.constant.HttpStatus; +import com.bms.common.exception.ServiceException; + +/** + * 按客户归属业务员和其部门校验账单可见范围。 + * 独立 Bean 保证内部业务调用也经过 DataScope 切面;范围始终取具有账单列表权限的角色。 + */ +@Service +public class ArBillDataScopeService { + @Autowired + private ArBillMapper billMapper; + @Autowired + private ArBillLogMapper logMapper; + @Autowired + private ArBillGenerateTaskMapper generateTaskMapper; + + @DataScope(deptAlias = "ar_scope_user", userAlias = "ar_scope_user", permission = "ar:bill:list") + public ArBill requireBill(ArBillQuery query) { + return withoutPagination(() -> { + ArBill bill = query.getArBillId() == null ? null : billMapper.selectArBillByDataScope(query); + if (bill == null) { + throw new ServiceException("应收账单不存在或无权访问", HttpStatus.FORBIDDEN); + } + return bill; + }); + } + + @DataScope(deptAlias = "ar_scope_user", userAlias = "ar_scope_user", permission = "ar:bill:list") + public void requireCustomer(ArBillQuery query) { + withoutPagination(() -> { + Long customerId = query.getCustomerId() == null || query.getCustomerId() <= 0 + ? null : billMapper.selectCustomerIdByDataScope(query); + if (customerId == null || customerId <= 0) { + throw new ServiceException("客户不存在或无权访问该客户的应收账单", HttpStatus.FORBIDDEN); + } + return customerId; + }); + } + + @DataScope(deptAlias = "ar_scope_user", userAlias = "ar_scope_user", permission = "ar:bill:list") + public List selectLogs(ArBillQuery scope, ArBillLog query) { + return logMapper.selectList(query, scope.getParams()); + } + + @DataScope(deptAlias = "ar_scope_user", userAlias = "ar_scope_user", permission = "ar:bill:list") + public List selectActiveGenerateTasks(ArBillQuery scope, String operator) { + return generateTaskMapper.selectActiveByCreateBy(operator, scope.getParams()); + } + + /** 权限校验 SQL 不消耗调用方为明细/订单预览设置的分页,拒绝访问时清理分页。 */ + private T withoutPagination(Supplier lookup) { + Page pendingPage = PageHelper.getLocalPage(); + PageHelper.clearPage(); + try { + T result = lookup.get(); + if (pendingPage != null) { + PageHelper.setLocalPage(pendingPage); + } + return result; + } catch (RuntimeException | Error e) { + PageHelper.clearPage(); + throw e; + } + } +} diff --git a/bms-back/bms-system/src/main/java/com/bms/ar/service/impl/ArBillLogServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/ar/service/impl/ArBillLogServiceImpl.java index b98e5b38..53fe7363 100644 --- a/bms-back/bms-system/src/main/java/com/bms/ar/service/impl/ArBillLogServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/ar/service/impl/ArBillLogServiceImpl.java @@ -13,16 +13,20 @@ import com.bms.ar.enums.ArLogOperationType; import com.bms.ar.enums.ArLogResult; import com.bms.ar.mapper.ArBillLogMapper; import com.bms.ar.service.IArBillLogService; +import com.bms.ar.service.ArBillDataScopeService; +import com.bms.ar.domain.vo.ArBillQuery; import com.bms.common.utils.uuid.IdUtils; @Service public class ArBillLogServiceImpl implements IArBillLogService { @Autowired private ArBillLogMapper logMapper; + @Autowired + private ArBillDataScopeService dataScopeService; @Override public List selectLogList(ArBillLog query) { - return logMapper.selectList(query); + return dataScopeService.selectLogs(new ArBillQuery(), query); } @Override diff --git a/bms-back/bms-system/src/main/java/com/bms/ar/service/impl/ArBillServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/ar/service/impl/ArBillServiceImpl.java index 67c74020..fcde0ec2 100644 --- a/bms-back/bms-system/src/main/java/com/bms/ar/service/impl/ArBillServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/ar/service/impl/ArBillServiceImpl.java @@ -75,11 +75,13 @@ import com.bms.ar.mapper.ArBankAccountMapper; import com.bms.ar.mapper.ArExportTemplateMapper; import com.bms.ar.service.IArBillLogService; import com.bms.ar.service.IArBillService; +import com.bms.ar.service.ArBillDataScopeService; import com.bms.ar.domain.vo.ArBillActionRequest; import com.bms.ar.domain.vo.ArBillGenerateQuery; import com.bms.ar.domain.vo.ArBillGenerateRequest; import com.bms.ar.domain.vo.ArBillQuery; import com.bms.common.config.BmsConfig; +import com.bms.common.annotation.DataScope; import com.bms.common.exception.ServiceException; import com.bms.common.utils.DateUtils; import com.bms.common.utils.SecurityUtils; @@ -197,6 +199,8 @@ public class ArBillServiceImpl implements IArBillService { @Autowired private IArBillLogService logService; @Autowired + private ArBillDataScopeService dataScopeService; + @Autowired private ApplicationEventPublisher eventPublisher; @Autowired private PrcOrderBillMapper sourceBillMapper; @@ -211,6 +215,7 @@ public class ArBillServiceImpl implements IArBillService { * 查询账单列表,并对返回给前端的收款账号做脱敏。 */ @Override + @DataScope(deptAlias = "ar_scope_user", userAlias = "ar_scope_user", permission = "ar:bill:list") public List selectArBillList(ArBillQuery query) { long startedAt = System.nanoTime(); List bills = billMapper.selectArBillList(query); @@ -229,10 +234,7 @@ public class ArBillServiceImpl implements IArBillService { @Override public ArBill selectArBillById(Long arBillId) { long startedAt = System.nanoTime(); - ArBill bill = billMapper.selectArBillById(arBillId); - if (bill == null) { - throw new ServiceException("应收账单不存在"); - } + ArBill bill = requireBillDataScope(arBillId); bill.setBankAccountNo(maskAccount(bill.getBankAccountNo())); bill.setExports(exportMapper.selectByBillId(arBillId)); log.debug("应收账单详情查询完成,arBillId={}, status={}, exportCount={}, costMs={}", @@ -247,6 +249,7 @@ public class ArBillServiceImpl implements IArBillService { @Override public List selectArBillDetails(Long arBillId) { long startedAt = System.nanoTime(); + requireBillDataScope(arBillId); List details = detailMapper.selectByBillId(arBillId); log.debug("应收账单明细查询完成,arBillId={}, resultCount={}, costMs={}", arBillId, details.size(), elapsedMillis(startedAt)); @@ -259,6 +262,7 @@ public class ArBillServiceImpl implements IArBillService { @Override public List selectArBillDetailFees(Long arBillId, Long detailId) { long startedAt = System.nanoTime(); + requireBillDataScope(arBillId); List fees = detailFeeMapper.selectByBillIdAndDetailId(arBillId, detailId); log.debug("应收账单费用明细查询完成,arBillId={}, detailId={}, resultCount={}, costMs={}", arBillId, detailId, fees.size(), elapsedMillis(startedAt)); @@ -299,6 +303,7 @@ public class ArBillServiceImpl implements IArBillService { public List previewBill(ArBillGenerateQuery query) { long startedAt = System.nanoTime(); validatePreviewQuery(query); + requireCustomerDataScope(query.getCustomerId()); List orderIdentifiers = normalizeOrderIdentifiers(query.getOrderOrTrackingNo()); List excludeOrderIdentifiers = normalizeOrderIdentifiers(query.getExcludeOrderOrTrackingNo()); List result = sourceBillMapper.selectPrcOrderBillByCustomerPeriod(query.getCustomerId(), @@ -327,6 +332,7 @@ public class ArBillServiceImpl implements IArBillService { public BigDecimal sumPreviewBill(ArBillGenerateQuery query) { long startedAt = System.nanoTime(); validatePreviewQuery(query); + requireCustomerDataScope(query.getCustomerId()); List orderIdentifiers = normalizeOrderIdentifiers(query.getOrderOrTrackingNo()); List excludeOrderIdentifiers = normalizeOrderIdentifiers(query.getExcludeOrderOrTrackingNo()); BigDecimal total = nvl(sourceBillMapper.sumPrcOrderBillIncomeByCustomerPeriod(query.getCustomerId(), @@ -350,8 +356,13 @@ public class ArBillServiceImpl implements IArBillService { long startedAt = System.nanoTime(); Long createUserId = SecurityUtils.getUserId(); validatePeriod(request); + requireCustomerDataScope(request.getCustomerId()); ArBillGenerateTask existed = generateTaskMapper.selectByRequestToken(requestId); if (existed != null) { + ArBill existedBill = requireBillDataScope(existed.getArBillId()); + if (!request.getCustomerId().equals(existedBill.getCustomerId())) { + throw new ServiceException("请求标识已用于其他客户,请重新发起"); + } log.info("应收账单生成请求命中幂等记录,operator={}, requestId={}, taskId={}, status={}, costMs={}", safeLogValue(operator), safeLogValue(requestId), existed.getTaskId(), existed.getStatus(), elapsedMillis(startedAt)); @@ -471,6 +482,7 @@ public class ArBillServiceImpl implements IArBillService { if (task == null) { throw new ServiceException("账单生成任务不存在"); } + requireBillDataScope(task.getArBillId()); log.debug("应收账单生成任务状态查询,taskId={}, status={}, processedCount={}, totalCount={}, costMs={}", taskId, task.getStatus(), task.getProcessedCount(), task.getTotalCount(), elapsedMillis(startedAt)); return task; @@ -482,7 +494,7 @@ public class ArBillServiceImpl implements IArBillService { @Override public List selectActiveGenerateTasks(String operator) { long startedAt = System.nanoTime(); - List tasks = generateTaskMapper.selectActiveByCreateBy(operator); + List tasks = dataScopeService.selectActiveGenerateTasks(new ArBillQuery(), operator); log.debug("当前用户应收账单生成任务查询,operator={}, taskCount={}, costMs={}", safeLogValue(operator), tasks.size(), elapsedMillis(startedAt)); return tasks; @@ -913,6 +925,7 @@ public class ArBillServiceImpl implements IArBillService { @Transactional(rollbackFor = Exception.class) public ArBill confirmBill(Long arBillId, ArBillActionRequest request, String operator, String requestId) { long startedAt = System.nanoTime(); + requireBillDataScope(arBillId); ArBill bill = billMapper.selectArBillByIdForUpdate(arBillId); if (bill == null) { throw failure(arBillId, ArLogObjectType.BILL, String.valueOf(arBillId), ArLogOperationType.CONFIRM, @@ -957,6 +970,7 @@ public class ArBillServiceImpl implements IArBillService { @Transactional(rollbackFor = Exception.class) public ArBill voidBill(Long arBillId, ArBillActionRequest request, String operator, String requestId) { long startedAt = System.nanoTime(); + requireBillDataScope(arBillId); String remark = request == null ? null : request.getRemark(); if (StringUtils.isEmpty(remark)) { throw failure(arBillId, ArLogObjectType.BILL, String.valueOf(arBillId), ArLogOperationType.VOID, @@ -1001,23 +1015,22 @@ public class ArBillServiceImpl implements IArBillService { @Transactional(rollbackFor = Exception.class) public ArBillExport exportBill(Long arBillId, String operator, String requestId) { long startedAt = System.nanoTime(); + ArBill bill = requireBillDataScope(arBillId); Long exportUserId = SecurityUtils.getUserId(); ArBillLog previous = logMapper.selectSuccessByRequestId(requestId, ArLogObjectType.EXPORT.getCode(), ArLogResult.SUCCESS.getCode()); if (previous != null && isNumber(previous.getObjectId())) { ArBillExport previousExport = exportMapper.selectById(Long.valueOf(previous.getObjectId())); if (previousExport != null) { + if (!arBillId.equals(previousExport.getArBillId())) { + throw new ServiceException("请求标识已用于其他账单,请重新发起"); + } log.info("应收账单导出请求命中幂等记录,operator={}, requestId={}, arBillId={}, exportId={}, status={}, costMs={}", safeLogValue(operator), safeLogValue(requestId), arBillId, previousExport.getExportId(), previousExport.getExportStatus(), elapsedMillis(startedAt)); return previousExport; } } - ArBill bill = billMapper.selectArBillById(arBillId); - if (bill == null) { - throw failure(arBillId, ArLogObjectType.EXPORT, String.valueOf(arBillId), ArLogOperationType.EXPORT, - null, "应收账单不存在", operator, requestId); - } if (!ArBillGenerationStatus.READY.equals(bill.getGenerationStatus())) { throw failure(arBillId, ArLogObjectType.EXPORT, String.valueOf(arBillId), ArLogOperationType.EXPORT, bill.getStatus(), "账单尚未生成完成,暂不能导出", operator, requestId); @@ -1078,6 +1091,7 @@ public class ArBillServiceImpl implements IArBillService { if (export == null) { throw new ServiceException("导出任务不存在"); } + requireBillDataScope(export.getArBillId()); log.debug("应收账单导出任务状态查询,exportId={}, status={}, costMs={}", exportId, export.getExportStatus(), elapsedMillis(startedAt)); return export; @@ -1158,6 +1172,7 @@ public class ArBillServiceImpl implements IArBillService { @Override public List selectExports(Long arBillId) { long startedAt = System.nanoTime(); + requireBillDataScope(arBillId); List exports = exportMapper.selectByBillId(arBillId); log.debug("应收账单导出记录查询,arBillId={}, exportCount={}, costMs={}", arBillId, exports.size(), elapsedMillis(startedAt)); @@ -1167,8 +1182,8 @@ public class ArBillServiceImpl implements IArBillService { @Override public void downloadExport(Long exportId, HttpServletResponse response) throws Exception { long startedAt = System.nanoTime(); - ArBillExport export = exportMapper.selectById(exportId); - if (export == null || !ArExportStatus.SUCCESS.getCode().equals(export.getExportStatus()) || StringUtils.isEmpty(export.getStorageKey())) { + ArBillExport export = selectExport(exportId); + if (!ArExportStatus.SUCCESS.getCode().equals(export.getExportStatus()) || StringUtils.isEmpty(export.getStorageKey())) { throw new ServiceException("导出文件不存在或尚未生成成功"); } String resource = export.getStorageKey(); @@ -1197,6 +1212,18 @@ public class ArBillServiceImpl implements IArBillService { return logs; } + private ArBill requireBillDataScope(Long arBillId) { + ArBillQuery query = new ArBillQuery(); + query.setArBillId(arBillId); + return dataScopeService.requireBill(query); + } + + private void requireCustomerDataScope(Long customerId) { + ArBillQuery query = new ArBillQuery(); + query.setCustomerId(customerId); + dataScopeService.requireCustomer(query); + } + private String exportFailureMessage(Exception e) { String message = StringUtils.isEmpty(e.getMessage()) ? "账单导出失败" : e.getMessage(); return message.length() > 500 ? message.substring(0, 500) : message; diff --git a/bms-back/bms-system/src/main/resources/mapper/ar/ArBillGenerateTaskMapper.xml b/bms-back/bms-system/src/main/resources/mapper/ar/ArBillGenerateTaskMapper.xml index 27d3b0be..76548d68 100644 --- a/bms-back/bms-system/src/main/resources/mapper/ar/ArBillGenerateTaskMapper.xml +++ b/bms-back/bms-system/src/main/resources/mapper/ar/ArBillGenerateTaskMapper.xml @@ -66,6 +66,12 @@ where create_by = #{createBy} and status in ('WAITING', 'RUNNING', 'CANCEL_REQUESTED') + + and exists ( + select 1 from ar_bill b where b.ar_bill_id = ar_bill_generate_task.ar_bill_id + + ) + order by create_time desc, task_id desc diff --git a/bms-back/bms-system/src/main/resources/mapper/ar/ArBillLogMapper.xml b/bms-back/bms-system/src/main/resources/mapper/ar/ArBillLogMapper.xml index 4254aa23..786c5eac 100644 --- a/bms-back/bms-system/src/main/resources/mapper/ar/ArBillLogMapper.xml +++ b/bms-back/bms-system/src/main/resources/mapper/ar/ArBillLogMapper.xml @@ -26,7 +26,7 @@ - select l.log_id, l.ar_bill_id, l.object_type, l.object_id, l.operation_type, l.operation_desc, case l.object_type when 'BILL' then b.bill_no @@ -43,14 +43,15 @@ left join ar_balance_writeoff bw on bw.writeoff_id = case when l.object_type = 'BALANCE_WRITEOFF' then cast(l.object_id as unsigned) end - and l.ar_bill_id = #{arBillId} - and l.object_type = #{objectType} - and l.object_id = #{objectId} - and l.operation_type = #{operationType} - and l.operation_source = #{operationSource} + and l.ar_bill_id = #{query.arBillId} + and l.object_type = #{query.objectType} + and l.object_id = #{query.objectId} + and l.operation_type = #{query.operationType} + and l.operation_source = #{query.operationSource} - and l.request_id = #{requestId} - and l.result = #{result} + and l.request_id = #{query.requestId} + and l.result = #{query.result} + order by l.create_time desc, l.log_id desc diff --git a/bms-back/bms-system/src/main/resources/mapper/ar/ArBillMapper.xml b/bms-back/bms-system/src/main/resources/mapper/ar/ArBillMapper.xml index aa835a73..139533e5 100644 --- a/bms-back/bms-system/src/main/resources/mapper/ar/ArBillMapper.xml +++ b/bms-back/bms-system/src/main/resources/mapper/ar/ArBillMapper.xml @@ -67,6 +67,27 @@ from ar_bill + + + + and exists ( + select 1 from ctr_customer ar_scope_customer + left join sys_user ar_scope_id_user + on ar_scope_customer.sales_user_id regexp '^[0-9]+$' + and ar_scope_id_user.user_id = ar_scope_customer.sales_user_id + 0 + left join sys_user ar_scope_user + on ar_scope_user.del_flag = '0' + and (ar_scope_user.user_id = ar_scope_id_user.user_id + or (ar_scope_id_user.user_id is null + and ar_scope_user.user_name = ar_scope_customer.sales_user_id)) + where ar_scope_customer.customer_id = ${customerIdColumn} + ${params.dataScope} + ) + + + + + + + +