云
16 小时以前 d17024670efca145ff97bc26913bfd9b0fef2cd0
feat(crm): 开放客户跨模块访问权限以支持业务集成

- 在 CrmCustomerApi 接口中添加跨模块访问说明,允许其他业务模块访问关联客户
- 修改 CrmCustomerApiImpl 实现类,移除客户权限过滤逻辑,确保跨模块调用不受CRM权限限制
- 添加 business-get 和 business-page 接口供其他业务模块获取客户信息
- 新增 selectBusinessPage 方法提供无权限限制的客户分页查询
- 移除 ERP 和 MES 模块中对客户权限的校验逻辑
- 更新测试用例以验证跨模块客户访问功能
已修改18个文件
154 ■■■■■ 文件已修改
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/api/customer/CrmCustomerApi.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/api/customer/CrmCustomerApiImpl.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/mps/MesProMpsServiceImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/schedule/MesProBatchScheduleService.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/workorder/MesProWorkOrderServiceImpl.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/imports/MesQcImportHelper.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcServiceImpl.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/batch/MesWmBatchServiceImpl.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/packages/MesWmPackageServiceImpl.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesServiceImpl.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnsales/MesWmReturnSalesServiceImpl.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/salesnotice/MesWmSalesNoticeServiceImpl.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/imports/MesQcOptionalClientTest.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/api/customer/CrmCustomerApi.java
@@ -18,6 +18,8 @@
    /**
     * 获得客户信息
     *
     * 供其它业务模块访问关联客户,拥有跨模块访问权限,不受 CRM 客户负责人权限限制。
     *
     * @param id 客户编号
     * @return 客户信息
     */
@@ -25,6 +27,8 @@
    /**
     * 获得客户信息列表
     *
     * 供其它业务模块访问关联客户,拥有跨模块访问权限,不受 CRM 客户负责人权限限制。
     *
     * @param ids 客户编号列表
     * @return 客户信息列表
@@ -34,6 +38,8 @@
    /**
     * 获得客户信息 Map
     *
     * 供其它业务模块访问关联客户,拥有跨模块访问权限,不受 CRM 客户负责人权限限制。
     *
     * @param ids 客户编号列表
     * @return 客户信息 Map
     */
@@ -41,6 +47,8 @@
    /**
     * 校验客户是否存在
     *
     * 供其它业务模块校验业务单据中的客户,拥有跨模块访问权限。
     *
     * @param id 客户编号
     * @return 客户信息(校验通过)
@@ -65,4 +73,4 @@
     */
    List<CrmCustomerRespDTO> getCustomerSimpleList();
}
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/api/customer/CrmCustomerApiImpl.java
@@ -38,7 +38,8 @@
    @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);
    }
@@ -48,9 +49,10 @@
            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
@@ -59,9 +61,7 @@
            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)));
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java
@@ -114,6 +114,15 @@
        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;
@@ -134,6 +143,18 @@
        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();
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java
@@ -97,6 +97,23 @@
        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);
    }
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerService.java
@@ -103,6 +103,8 @@
     */
    PageResult<CrmCustomerDO> getCustomerPage(CrmCustomerPageReqVO pageReqVO, Long userId);
    PageResult<CrmCustomerDO> getBusinessCustomerPage(CrmCustomerPageReqVO pageReqVO);
    /**
     * 获得放入公海提醒的客户分页
     *
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
@@ -501,6 +501,11 @@
    }
    @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)
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java
@@ -743,8 +743,7 @@
    @Override
    public PageResult<ErpSaleOrderDO> getSaleOrderPage(ErpSaleOrderPageReqVO pageReqVO) {
        List<Long> permittedCustomerIds = customerApi.getPermittedCustomerIds();
        return saleOrderMapper.selectPage(pageReqVO, permittedCustomerIds);
        return saleOrderMapper.selectPage(pageReqVO);
    }
    // ==================== 订单项 ====================
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/mps/MesProMpsServiceImpl.java
@@ -348,9 +348,7 @@
    @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
@@ -618,4 +616,4 @@
        }
    }
}
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/schedule/MesProBatchScheduleService.java
@@ -127,7 +127,6 @@
        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));
@@ -170,9 +169,6 @@
            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());
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/workorder/MesProWorkOrderServiceImpl.java
@@ -174,16 +174,12 @@
    @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
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/imports/MesQcImportHelper.java
@@ -26,11 +26,9 @@
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;
@@ -68,9 +66,6 @@
    private Map<String, Long> supplierNameMap;
    /** 客户名称 -> 编号 */
    private Map<String, Long> customerNameMap;
    /** 当前登录用户可见的客户编号;null 表示不限制 */
    private Set<Long> permittedClientIds;
    private boolean clientPermissionLoaded;
    /**
     * 开始一次导入:清空上一次导入的缓存,保证新建的供应商/客户/方案本次即可命中
@@ -79,8 +74,6 @@
        templateNameMap = null;
        supplierNameMap = null;
        customerNameMap = null;
        permittedClientIds = null;
        clientPermissionLoaded = false;
    }
    /**
@@ -199,26 +192,9 @@
        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 营销管理。
    }
    /**
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcServiceImpl.java
@@ -312,9 +312,7 @@
    @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
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/batch/MesWmBatchServiceImpl.java
@@ -70,9 +70,7 @@
    @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
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/packages/MesWmPackageServiceImpl.java
@@ -96,8 +96,7 @@
    @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
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesServiceImpl.java
@@ -124,8 +124,7 @@
    @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
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnsales/MesWmReturnSalesServiceImpl.java
@@ -106,8 +106,7 @@
    @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
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/salesnotice/MesWmSalesNoticeServiceImpl.java
@@ -103,22 +103,12 @@
    @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
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/imports/MesQcOptionalClientTest.java
@@ -35,13 +35,14 @@
    }
    @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