feat(crm): 开放客户跨模块访问权限以支持业务集成
- 在 CrmCustomerApi 接口中添加跨模块访问说明,允许其他业务模块访问关联客户
- 修改 CrmCustomerApiImpl 实现类,移除客户权限过滤逻辑,确保跨模块调用不受CRM权限限制
- 添加 business-get 和 business-page 接口供其他业务模块获取客户信息
- 新增 selectBusinessPage 方法提供无权限限制的客户分页查询
- 移除 ERP 和 MES 模块中对客户权限的校验逻辑
- 更新测试用例以验证跨模块客户访问功能
| | |
| | | /** |
| | | * 获得客户信息 |
| | | * |
| | | * 供其它业务模块访问关联客户,拥有跨模块访问权限,不受 CRM 客户负责人权限限制。 |
| | | * |
| | | * @param id 客户编号 |
| | | * @return 客户信息 |
| | | */ |
| | |
| | | |
| | | /** |
| | | * 获得客户信息列表 |
| | | * |
| | | * 供其它业务模块访问关联客户,拥有跨模块访问权限,不受 CRM 客户负责人权限限制。 |
| | | * |
| | | * @param ids 客户编号列表 |
| | | * @return 客户信息列表 |
| | |
| | | /** |
| | | * 获得客户信息 Map |
| | | * |
| | | * 供其它业务模块访问关联客户,拥有跨模块访问权限,不受 CRM 客户负责人权限限制。 |
| | | * |
| | | * @param ids 客户编号列表 |
| | | * @return 客户信息 Map |
| | | */ |
| | |
| | | /** |
| | | * 校验客户是否存在 |
| | | * |
| | | * 供其它业务模块校验业务单据中的客户,拥有跨模块访问权限。 |
| | | * |
| | | * @param id 客户编号 |
| | | * @return 客户信息(校验通过) |
| | | */ |
| | |
| | | |
| | | @Override |
| | | public CrmCustomerRespDTO getCustomer(Long id) { |
| | | CrmCustomerDO customer = customerService.getCustomer(id); |
| | | List<CrmCustomerDO> customers = customerService.getCustomerList(Collections.singleton(id)); |
| | | CrmCustomerDO customer = CollUtil.isEmpty(customers) ? null : customers.get(0); |
| | | return BeanUtils.toBean(customer, CrmCustomerRespDTO.class); |
| | | } |
| | | |
| | |
| | | return Collections.emptyList(); |
| | | } |
| | | List<CrmCustomerDO> customers = customerService.getCustomerList(ids); |
| | | // 过滤掉用户没有权限的客户 |
| | | List<CrmCustomerDO> filteredCustomers = filterCustomersByPermission(customers); |
| | | return BeanUtils.toBean(filteredCustomers, CrmCustomerRespDTO.class); |
| | | // 这是供 MES、ERP、售后等业务模块解析关联客户的跨模块 API。 |
| | | // 营销管理自身的列表权限由 CrmCustomerMapper 分页查询统一控制,不能在这里再次按当前用户过滤, |
| | | // 否则其他模块查看已有业务单据时会丢失非本人客户名称。 |
| | | return BeanUtils.toBean(customers, CrmCustomerRespDTO.class); |
| | | } |
| | | |
| | | @Override |
| | |
| | | return Collections.emptyMap(); |
| | | } |
| | | List<CrmCustomerDO> customers = customerService.getCustomerList(ids); |
| | | // 过滤掉用户没有权限的客户 |
| | | List<CrmCustomerDO> filteredCustomers = filterCustomersByPermission(customers); |
| | | return filteredCustomers.stream() |
| | | return customers.stream() |
| | | .collect(Collectors.toMap( |
| | | CrmCustomerDO::getId, |
| | | customer -> BeanUtils.toBean(customer, CrmCustomerRespDTO.class))); |
| | |
| | | return success(buildCustomerDetail(customer)); |
| | | } |
| | | |
| | | @GetMapping("/business-get") |
| | | @Operation(summary = "获得业务模块关联客户详情") |
| | | @PreAuthorize("isAuthenticated()") |
| | | public CommonResult<CrmCustomerRespVO> getBusinessCustomer(@RequestParam("id") Long id) { |
| | | CrmCustomerDO customer = customerService.getCustomerList(java.util.Collections.singleton(id)) |
| | | .stream().findFirst().orElse(null); |
| | | return success(buildCustomerDetail(customer)); |
| | | } |
| | | |
| | | public CrmCustomerRespVO buildCustomerDetail(CrmCustomerDO customer) { |
| | | if (customer == null) { |
| | | return null; |
| | |
| | | return success(new PageResult<>(buildCustomerDetailList(pageResult.getList()), pageResult.getTotal())); |
| | | } |
| | | |
| | | @GetMapping("/business-page") |
| | | @Operation(summary = "获得业务模块客户分页") |
| | | @PreAuthorize("isAuthenticated()") |
| | | public CommonResult<PageResult<CrmCustomerRespVO>> getBusinessCustomerPage( |
| | | @Valid CrmCustomerPageReqVO pageVO) { |
| | | PageResult<CrmCustomerDO> pageResult = customerService.getBusinessCustomerPage(pageVO); |
| | | if (CollUtil.isEmpty(pageResult.getList())) { |
| | | return success(PageResult.empty(pageResult.getTotal())); |
| | | } |
| | | return success(new PageResult<>(buildCustomerDetailList(pageResult.getList()), pageResult.getTotal())); |
| | | } |
| | | |
| | | public List<CrmCustomerRespVO> buildCustomerDetailList(List<CrmCustomerDO> list) { |
| | | if (CollUtil.isEmpty(list)) { |
| | | return java.util.Collections.emptyList(); |
| | |
| | | return selectJoinPage(pageReqVO, CrmCustomerDO.class, query); |
| | | } |
| | | |
| | | /** |
| | | * 供 MES、ERP、售后等业务模块选择或回显客户。该查询不套用 CRM 负责人权限, |
| | | * 仅按客户筛选条件分页,避免业务单据因当前登录人不是客户负责人而无法显示客户。 |
| | | */ |
| | | default PageResult<CrmCustomerDO> selectBusinessPage(CrmCustomerPageReqVO pageReqVO) { |
| | | MPJLambdaWrapperX<CrmCustomerDO> query = new MPJLambdaWrapperX<>(); |
| | | query.selectAll(CrmCustomerDO.class) |
| | | .likeIfPresent(CrmCustomerDO::getName, pageReqVO.getName()) |
| | | .eqIfPresent(CrmCustomerDO::getMobile, pageReqVO.getMobile()) |
| | | .eqIfPresent(CrmCustomerDO::getIndustryId, pageReqVO.getIndustryId()) |
| | | .eqIfPresent(CrmCustomerDO::getLevel, pageReqVO.getLevel()) |
| | | .eqIfPresent(CrmCustomerDO::getSource, pageReqVO.getSource()) |
| | | .eqIfPresent(CrmCustomerDO::getAutoPoolEnabled, pageReqVO.getAutoPoolEnabled()) |
| | | .orderByAsc(CrmCustomerDO::getLevel); |
| | | return selectJoinPage(pageReqVO, CrmCustomerDO.class, query); |
| | | } |
| | | |
| | | default CrmCustomerDO selectByCustomerName(String name) { |
| | | return selectOne(CrmCustomerDO::getName, name); |
| | | } |
| | |
| | | */ |
| | | PageResult<CrmCustomerDO> getCustomerPage(CrmCustomerPageReqVO pageReqVO, Long userId); |
| | | |
| | | PageResult<CrmCustomerDO> getBusinessCustomerPage(CrmCustomerPageReqVO pageReqVO); |
| | | |
| | | /** |
| | | * 获得放入公海提醒的客户分页 |
| | | * |
| | |
| | | } |
| | | |
| | | @Override |
| | | public PageResult<CrmCustomerDO> getBusinessCustomerPage(CrmCustomerPageReqVO pageReqVO) { |
| | | return customerMapper.selectBusinessPage(pageReqVO); |
| | | } |
| | | |
| | | @Override |
| | | public PageResult<CrmCustomerDO> getPutPoolRemindCustomerPage(CrmCustomerPageReqVO pageVO, Long userId) { |
| | | CrmCustomerPoolConfigDO poolConfig = customerPoolConfigService.getCustomerPoolConfig(); |
| | | if (ObjUtil.isNull(poolConfig) |
| | |
| | | |
| | | @Override |
| | | public PageResult<ErpSaleOrderDO> getSaleOrderPage(ErpSaleOrderPageReqVO pageReqVO) { |
| | | List<Long> permittedCustomerIds = customerApi.getPermittedCustomerIds(); |
| | | return saleOrderMapper.selectPage(pageReqVO, permittedCustomerIds); |
| | | return saleOrderMapper.selectPage(pageReqVO); |
| | | } |
| | | |
| | | // ==================== 订单项 ==================== |
| | |
| | | |
| | | @Override |
| | | public PageResult<MesProMpsDO> getMpsPage(MesProMpsPageReqVO pageReqVO) { |
| | | // 获取用户有权限的客户 ID 列表 |
| | | List<Long> permittedClientIds = customerApi.getPermittedCustomerIds(); |
| | | return mpsMapper.selectPage(pageReqVO, permittedClientIds); |
| | | return mpsMapper.selectPage(pageReqVO, (List<Long>) null); |
| | | } |
| | | |
| | | @Override |
| | |
| | | result.setPreviewTime(now()); |
| | | var orders = workOrderMapper.selectByIds(request.getWorkOrderIds()); |
| | | var orderMap = orders.stream().collect(Collectors.toMap(MesProWorkOrderDO::getId, order -> order)); |
| | | List<Long> permitted = customerApi.getPermittedCustomerIds(); |
| | | var stations = workstationService.getWorkstationListByStatus(0).stream() |
| | | .sorted(Comparator.comparing(MesMdWorkstationDO::getId)).toList(); |
| | | var machineMap = machineryMapper.selectList().stream().collect(Collectors.toMap(MesDvMachineryDO::getId, machine -> machine)); |
| | |
| | | result.getOrders().add(orderPlan); |
| | | try { |
| | | if (order == null) throw new IllegalArgumentException("工单不存在或已删除"); |
| | | if (permitted != null && order.getClientId() != null && !permitted.contains(order.getClientId())) { |
| | | throw new IllegalArgumentException("无权操作该客户的工单"); |
| | | } |
| | | orderPlan.setWorkOrderCode(order.getCode()); |
| | | orderPlan.setWorkOrderName(order.getName()); |
| | | orderPlan.setQuantity(order.getQuantity()); |
| | |
| | | |
| | | @Override |
| | | public PageResult<MesProWorkOrderDO> getWorkOrderPage(MesProWorkOrderPageReqVO pageReqVO) { |
| | | // 获取用户有权限的客户 ID 列表 |
| | | List<Long> permittedClientIds = customerApi.getPermittedCustomerIds(); |
| | | return workOrderMapper.selectPage(pageReqVO, permittedClientIds); |
| | | return workOrderMapper.selectPage(pageReqVO, (List<Long>) null); |
| | | } |
| | | |
| | | @Override |
| | | public PageResult<MesProWorkOrderDO> getWorkOrderSchedulePage(MesProWorkOrderPageReqVO pageReqVO) { |
| | | // 获取用户有权限的客户 ID 列表 |
| | | List<Long> permittedClientIds = customerApi.getPermittedCustomerIds(); |
| | | return workOrderMapper.selectSchedulePage(pageReqVO, permittedClientIds); |
| | | return workOrderMapper.selectSchedulePage(pageReqVO, (List<Long>) null); |
| | | } |
| | | |
| | | @Override |
| | |
| | | import java.time.LocalDateTime; |
| | | import java.time.format.DateTimeFormatter; |
| | | import java.util.HashMap; |
| | | import java.util.HashSet; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Objects; |
| | | import java.util.Set; |
| | | |
| | | import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; |
| | | import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.QC_NO_TEMPLATE; |
| | |
| | | private Map<String, Long> supplierNameMap; |
| | | /** 客户名称 -> 编号 */ |
| | | private Map<String, Long> customerNameMap; |
| | | /** 当前登录用户可见的客户编号;null 表示不限制 */ |
| | | private Set<Long> permittedClientIds; |
| | | private boolean clientPermissionLoaded; |
| | | |
| | | /** |
| | | * 开始一次导入:清空上一次导入的缓存,保证新建的供应商/客户/方案本次即可命中 |
| | |
| | | templateNameMap = null; |
| | | supplierNameMap = null; |
| | | customerNameMap = null; |
| | | permittedClientIds = null; |
| | | clientPermissionLoaded = false; |
| | | } |
| | | |
| | | /** |
| | |
| | | return clientId; |
| | | } |
| | | |
| | | /** |
| | | * 校验客户在当前登录用户的可见范围内 |
| | | * |
| | | * 出货检验单列表按客户数据权限过滤,权限外的客户导入后本人在列表中看不到该单据,因此提前拦截。 |
| | | */ |
| | | /** MES 业务模块对所有 CRM 客户开放访问,不应用营销管理的客户团队权限。 */ |
| | | public void validateClientPermission(Long clientId, String clientName) { |
| | | if (clientId == null) { |
| | | return; |
| | | } |
| | | if (!clientPermissionLoaded) { |
| | | List<Long> permittedIds = crmCustomerApi.getPermittedCustomerIds(); |
| | | permittedClientIds = permittedIds == null ? null : new HashSet<>(permittedIds); |
| | | clientPermissionLoaded = true; |
| | | } |
| | | if (permittedClientIds == null || permittedClientIds.contains(clientId)) { |
| | | return; |
| | | } |
| | | throw new IllegalArgumentException("客户[" + clientName + "]不在您的数据权限范围内:" |
| | | + "导入后您将无法在「出货检验」列表中看到该检验单。请改用您负责的客户(可在「CRM 客户管理-我负责的」中查看)," |
| | | + "或联系管理员为您开通该客户的权限后重新导入"); |
| | | // 客户是否存在仍由 resolveClientId 校验;负责人和团队权限只约束 CRM 营销管理。 |
| | | } |
| | | |
| | | /** |
| | |
| | | |
| | | @Override |
| | | public PageResult<MesQcOqcDO> getOqcPage(MesQcOqcPageReqVO pageReqVO) { |
| | | // 获取用户有权限的客户 ID 列表 |
| | | List<Long> permittedClientIds = customerApi.getPermittedCustomerIds(); |
| | | return oqcMapper.selectPage(pageReqVO, permittedClientIds); |
| | | return oqcMapper.selectPage(pageReqVO, (List<Long>) null); |
| | | } |
| | | |
| | | @Override |
| | |
| | | |
| | | @Override |
| | | public PageResult<MesWmBatchDO> getBatchPage(MesWmBatchPageReqVO pageReqVO) { |
| | | // 获取用户有权限的客户 ID 列表 |
| | | List<Long> permittedClientIds = customerApi.getPermittedCustomerIds(); |
| | | return batchMapper.selectPage(pageReqVO, permittedClientIds); |
| | | return batchMapper.selectPage(pageReqVO, (List<Long>) null); |
| | | } |
| | | |
| | | @Override |
| | |
| | | |
| | | @Override |
| | | public PageResult<MesWmPackageDO> getPackagePage(MesWmPackagePageReqVO pageReqVO) { |
| | | List<Long> permittedClientIds = customerApi.getPermittedCustomerIds(); |
| | | return packageMapper.selectPage(pageReqVO, permittedClientIds); |
| | | return packageMapper.selectPage(pageReqVO, (List<Long>) null); |
| | | } |
| | | |
| | | @Override |
| | |
| | | |
| | | @Override |
| | | public PageResult<MesWmProductSalesDO> getProductSalesPage(MesWmProductSalesPageReqVO pageReqVO) { |
| | | List<Long> permittedClientIds = customerApi.getPermittedCustomerIds(); |
| | | return productSalesMapper.selectPage(pageReqVO, permittedClientIds); |
| | | return productSalesMapper.selectPage(pageReqVO, (List<Long>) null); |
| | | } |
| | | |
| | | @Override |
| | |
| | | |
| | | @Override |
| | | public PageResult<MesWmReturnSalesDO> getReturnSalesPage(MesWmReturnSalesPageReqVO pageReqVO) { |
| | | List<Long> permittedClientIds = customerApi.getPermittedCustomerIds(); |
| | | return returnSalesMapper.selectPage(pageReqVO, permittedClientIds); |
| | | return returnSalesMapper.selectPage(pageReqVO, (List<Long>) null); |
| | | } |
| | | |
| | | @Override |
| | |
| | | |
| | | @Override |
| | | public MesWmSalesNoticeDO getSalesNotice(Long id) { |
| | | MesWmSalesNoticeDO notice = salesNoticeMapper.selectById(id); |
| | | // 校验客户权限 |
| | | if (notice != null && notice.getClientId() != null) { |
| | | List<Long> permittedIds = customerApi.getPermittedCustomerIds(); |
| | | if (permittedIds != null && !permittedIds.contains(notice.getClientId())) { |
| | | return null; // 无权限则返回 null |
| | | } |
| | | } |
| | | return notice; |
| | | return salesNoticeMapper.selectById(id); |
| | | } |
| | | |
| | | @Override |
| | | public PageResult<MesWmSalesNoticeDO> getSalesNoticePage(MesWmSalesNoticePageReqVO pageReqVO) { |
| | | // 获取用户有权限的客户 ID 列表 |
| | | List<Long> permittedClientIds = customerApi.getPermittedCustomerIds(); |
| | | return salesNoticeMapper.selectPage(pageReqVO, permittedClientIds); |
| | | return salesNoticeMapper.selectPage(pageReqVO, (List<Long>) null); |
| | | } |
| | | |
| | | @Override |
| | |
| | | } |
| | | |
| | | @Test |
| | | void customerPermissionsStillApplyWhenACustomerIsSupplied() { |
| | | void customerPermissionsDoNotRestrictMesImports() { |
| | | CrmCustomerApi crm = mock(CrmCustomerApi.class); |
| | | when(crm.getPermittedCustomerIds()).thenReturn(List.of(10L)); |
| | | MesQcImportHelper helper = new MesQcImportHelper(); |
| | | ReflectionTestUtils.setField(helper, "crmCustomerApi", crm); |
| | | helper.validateClientPermission(10L, "可见客户"); |
| | | assertThrows(IllegalArgumentException.class, () -> helper.validateClientPermission(20L, "不可见客户")); |
| | | assertDoesNotThrow(() -> helper.validateClientPermission(20L, "其他客户")); |
| | | verifyNoInteractions(crm); |
| | | } |
| | | |
| | | @Test |