云
20 小时以前 8e704c6474b8a9df9652191112f6dc280992ba52
fix(crm): 修复客户公海数据显示问题

- 修改客户公海查询逻辑,允许显示已有负责人的客户
- 移除公海查询中对 owner_user_id 为空的限制条件
- 更新 CrmCustomerPageReqVO 中 pool 字段的描述说明
- 在权限切面中增加公海可见性检查逻辑
- 添加单元测试验证公海客户数据的正确显示
- 修复测试断言逻辑确保查询条件正确性
已添加4个文件
已修改4个文件
136 ■■■■■ 文件已修改
scripts/operate-log-time-20261013/fix_misc_receipt_schema.py 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
scripts/operate-log-time-20261013/read_customer_pool.py 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
scripts/operate-log-time-20261013/read_misc_receipt_schema.py 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/mes_wm_misc_receipt_source_fields.sql 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/customer/CrmCustomerPageReqVO.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/framework/permission/core/aop/CrmPermissionAspect.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/test/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerAutoPoolTest.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
scripts/operate-log-time-20261013/fix_misc_receipt_schema.py
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,50 @@
from read_log_times import connect
from pathlib import Path
import json
import re
HERE = Path(__file__).resolve().parent
ROOT = HERE.parents[1]
ADDITIONS = {
    'type': "int DEFAULT 1 COMMENT '杂项类型:mes_wm_misc_receipt_type' AFTER `name`",
    'source_doc_type': "varchar(64) DEFAULT NULL COMMENT '来源单据类型' AFTER `type`",
    'source_doc_id': "bigint DEFAULT NULL COMMENT '来源单据ID' AFTER `source_doc_type`",
    'source_doc_code': "varchar(100) DEFAULT NULL COMMENT '来源单据编码' AFTER `source_doc_id`",
}
with connect() as c:
    with c.cursor() as q:
        q.execute('SHOW CREATE TABLE mes_wm_misc_receipt')
        old_schema = q.fetchall()
        q.execute('SHOW COLUMNS FROM mes_wm_misc_receipt')
        columns = {r['Field'] for r in q.fetchall()}
        q.execute('SELECT * FROM mes_wm_misc_receipt ORDER BY id')
        before = q.fetchall()
        missing = [name for name in ADDITIONS if name not in columns]
        backup = HERE/'misc-receipt-schema-before.json'
        if missing:
            with backup.open('x', encoding='utf-8') as f:
                json.dump(dict(target='36.213.150.237:10000/mom-hm', schema=old_schema,
                               rows=before, missing=missing), f, ensure_ascii=False, indent=2, default=str)
            sql = 'ALTER TABLE mes_wm_misc_receipt '+', '.join(
                'ADD COLUMN `'+name+'` '+ADDITIONS[name] for name in missing)
            q.execute(sql)
        # éªŒè¯ä¸‰ä¸ªæ•°æ®å¯¹è±¡çš„全部映射字段都能查询。
        counts = {}
        for suffix, class_name in [('', 'MesWmMiscReceiptDO'), ('_line', 'MesWmMiscReceiptLineDO'),
                                   ('_detail', 'MesWmMiscReceiptDetailDO')]:
            source = (ROOT / ('yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/miscreceipt/'+class_name+'.java')).read_text(encoding='utf-8')
            fields = re.findall(r'private\s+\w+\s+(\w+);', source)
            mapped = [re.sub(r'(?<!^)([A-Z])', r'_\1', name).lower() for name in fields]
            mapped += ['create_time', 'update_time', 'creator', 'updater', 'deleted']
            table = 'mes_wm_misc_receipt'+suffix
            q.execute('SELECT '+','.join('`'+field+'`' for field in mapped)+
                      ' FROM '+table+' WHERE deleted=0 ORDER BY id DESC LIMIT 10')
            counts[table] = len(q.fetchall())
        q.execute('SELECT * FROM mes_wm_misc_receipt ORDER BY id')
        after = q.fetchall()
        assert [{k: row[k] for k in columns} for row in after] == before, 'Existing data changed'
        result = dict(target='36.213.150.237:10000/mom-hm', added_columns=missing,
                      original_data_unchanged=True, query_verified=counts, backup=str(backup))
        (HERE/'misc-receipt-schema-result.json').write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding='utf-8')
        print(json.dumps(result, ensure_ascii=False))
scripts/operate-log-time-20261013/read_customer_pool.py
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
from read_log_times import connect
import json
with connect() as c:
    with c.cursor() as q:
        queries = [
            "SHOW COLUMNS FROM crm_customer LIKE 'auto_pool_enabled'",
            "SELECT COUNT(*) total, SUM(owner_user_id IS NULL) no_owner, SUM(owner_user_id=0) zero_owner, SUM(auto_pool_enabled=1) enabled, SUM(auto_pool_enabled=0) disabled, SUM(auto_pool_enabled IS NULL) unset FROM crm_customer WHERE deleted=0",
            "SELECT owner_user_id IS NULL no_owner, auto_pool_enabled+0 enabled, COUNT(*) count FROM crm_customer WHERE deleted=0 GROUP BY owner_user_id IS NULL, auto_pool_enabled",
            "SELECT id,code,status FROM system_role WHERE code IN ('super_admin','crm_admin') AND deleted=0",
        ]
        for sql in queries:
            q.execute(sql)
            print(json.dumps({'sql': sql, 'rows': q.fetchall()}, ensure_ascii=False, default=str))
scripts/operate-log-time-20261013/read_misc_receipt_schema.py
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
from read_log_times import connect
import json
with connect() as c:
    with c.cursor() as q:
        for sql in [
            "SHOW CREATE TABLE mes_wm_misc_receipt_line",
            "SHOW CREATE TABLE mes_wm_misc_receipt_detail",
            "SHOW CREATE TABLE mes_wm_misc_receipt",
            "SHOW TABLES LIKE 'mes_wm_misc_receipt%'",
            "SELECT COUNT(*) count FROM mes_wm_misc_receipt",
            "SELECT id,code,name,status FROM mes_wm_misc_receipt ORDER BY id DESC LIMIT 3",
            "SELECT label,value,status FROM system_dict_data WHERE dict_type='mes_wm_misc_receipt_type' AND deleted=0",
        ]:
            q.execute(sql)
            print(json.dumps({'sql': sql, 'rows': q.fetchall()}, ensure_ascii=False, default=str))
sql/mysql/mes_wm_misc_receipt_source_fields.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,7 @@
-- ä¿®å¤æ‚项入库主表与 MesWmMiscReceiptDO çš„字段差异。
-- å­˜é‡åº“存补充单对应类型字典:1 = åº“存调整;来源单据字段保持可空。
ALTER TABLE `mes_wm_misc_receipt`
    ADD COLUMN `type` int DEFAULT 1 COMMENT '杂项类型:mes_wm_misc_receipt_type' AFTER `name`,
    ADD COLUMN `source_doc_type` varchar(64) DEFAULT NULL COMMENT '来源单据类型' AFTER `type`,
    ADD COLUMN `source_doc_id` bigint DEFAULT NULL COMMENT '来源单据ID' AFTER `source_doc_type`,
    ADD COLUMN `source_doc_code` varchar(100) DEFAULT NULL COMMENT '来源单据编码' AFTER `source_doc_id`;
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/customer/CrmCustomerPageReqVO.java
@@ -46,8 +46,8 @@
    @InEnum(CrmSceneTypeEnum.class)
    private Integer sceneType; // åœºæ™¯ç±»åž‹ï¼Œä¸º null æ—¶åˆ™è¡¨ç¤ºå…¨éƒ¨
    @Schema(description = "是否为公海数据", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
    private Boolean pool; // null åˆ™è¡¨ç¤ºä¸ºä¸æ˜¯å…¬æµ·æ•°æ®
    @Schema(description = "是否查询公海可见客户(包含已有负责人的客户)", example = "false")
    private Boolean pool;
    @Schema(description = "联系状态", example = "1")
    private Integer contactStatus; // backlog æŸ¥è¯¢æ¡ä»¶
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java
@@ -56,10 +56,9 @@
    default PageResult<CrmCustomerDO> selectPage(CrmCustomerPageReqVO pageReqVO, Long ownerUserId,
                                               boolean superAdmin) {
        MPJLambdaWrapperX<CrmCustomerDO> query = new MPJLambdaWrapperX<>();
        // å…¬æµ·æŸ¥è¯¢å¿…须优先过滤,不能被“全部数据”场景绕过。
        // å…¬æµ·å±•示允许共享的客户(包含已有负责人),与客户管理使用同一份数据。
        if (Boolean.TRUE.equals(pageReqVO.getPool())) {
            query.isNull(CrmCustomerDO::getOwnerUserId)
                    .eq(!superAdmin, CrmCustomerDO::getAutoPoolEnabled, true);
            query.eq(!superAdmin, CrmCustomerDO::getAutoPoolEnabled, true);
        } else if (CrmSceneTypeEnum.isAll(pageReqVO.getSceneType())) {
            // åœºæ™¯å››ï¼šå…¨éƒ¨æ•°æ®ï¼Œä¸åŒºåˆ†å…¬æµ·/非公海,由 appendPermissionCondition ç»Ÿä¸€å¤„理
            CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_CUSTOMER.getType(),
@@ -213,7 +212,6 @@
    default Long selectPoolCount() {
        return selectCount(new LambdaQueryWrapperX<CrmCustomerDO>()
                .isNull(CrmCustomerDO::getOwnerUserId)
                .eq(!CrmPermissionUtils.isSuperAdmin(), CrmCustomerDO::getAutoPoolEnabled, true));
    }
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/framework/permission/core/aop/CrmPermissionAspect.java
@@ -83,6 +83,11 @@
        if (CrmPermissionUtils.isCrmAdmin()) {
            return;
        }
        // å…¬æµ·å…è®¸æŸ¥çœ‹æœ‰è´Ÿè´£äººçš„客户;只授予读取权限,不影响写入、转移或领取权限。
        if (CrmBizTypeEnum.CRM_CUSTOMER.getType().equals(bizType)
                && CrmPermissionLevelEnum.isRead(permissionLevel) && isPublicPoolVisible(bizType, bizId)) {
            return;
        }
        // ç‰¹æ®Šï¼šæ²¡æœ‰æ•°æ®æƒé™çš„æƒ…况,针对 READ çš„特殊处理
        if (CollUtil.isEmpty(bizPermissions)) {
            // 1.1 å…¬æµ·æ•°æ®ï¼Œå¦‚果没有团队成员,大家也应该有 READ æƒé™æ‰å¯¹
yudao-module-crm/src/test/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerAutoPoolTest.java
@@ -10,6 +10,11 @@
import cn.iocoder.yudao.module.crm.framework.permission.core.aop.CrmPermissionAspect;
import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
import cn.iocoder.yudao.module.crm.enums.common.CrmSceneTypeEnum;
import cn.iocoder.yudao.module.crm.controller.admin.customer.CrmCustomerController;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import cn.idev.excel.metadata.data.ReadCellData;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@@ -21,6 +26,7 @@
import java.lang.reflect.Proxy;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.jupiter.api.Assertions.*;
@@ -70,6 +76,31 @@
        assertPoolListFilter(true);
    }
    @Test
    void poolCustomersUseSameOwnerAndDepartmentAsCustomerManagement() {
        var controller = new CrmCustomerController();
        var users = AdminUserApi.class.cast(Proxy.newProxyInstance(getClass().getClassLoader(),
                new Class<?>[]{AdminUserApi.class}, (proxy, method, args) -> {
                    assertEquals("getUserMap", method.getName());
                    return Map.of(10L, new AdminUserRespDTO().setId(10L).setNickname("销售负责人").setDeptId(20L));
                }));
        var departments = DeptApi.class.cast(Proxy.newProxyInstance(getClass().getClassLoader(),
                new Class<?>[]{DeptApi.class}, (proxy, method, args) -> {
                    assertEquals("getDeptMap", method.getName());
                    return Map.of(20L, new DeptRespDTO().setId(20L).setName("销售部"));
                }));
        var config = CrmCustomerPoolConfigService.class.cast(Proxy.newProxyInstance(getClass().getClassLoader(),
                new Class<?>[]{CrmCustomerPoolConfigService.class}, (proxy, method, args) -> null));
        ReflectionTestUtils.setField(controller, "adminUserApi", users);
        ReflectionTestUtils.setField(controller, "deptApi", departments);
        ReflectionTestUtils.setField(controller, "customerPoolConfigService", config);
        var customer = controller.buildCustomerDetailList(List.of(new CrmCustomerDO().setId(1L)
                .setOwnerUserId(10L).setAutoPoolEnabled(true))).getFirst();
        assertEquals(10L, customer.getOwnerUserId());
        assertEquals("销售负责人", customer.getOwnerUserName());
        assertEquals("销售部", customer.getOwnerUserDeptName());
    }
    private void assertPoolListFilter(boolean superAdmin) {
        TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), "test"), CrmCustomerDO.class);
        var mapper = CrmCustomerMapper.class.cast(Proxy.newProxyInstance(getClass().getClassLoader(),
@@ -77,7 +108,7 @@
                    if (method.getName().equals("selectJoinPage")) {
                        var query = (com.github.yulichang.wrapper.MPJLambdaWrapper<?>) args[2];
                        String sql = query.getSqlSegment();
                        assertTrue(sql.contains("owner_user_id IS NULL"), sql);
                        assertFalse(sql.contains("owner_user_id IS NULL"), sql);
                        assertEquals(!superAdmin, sql.contains("auto_pool_enabled ="), sql);
                        return new cn.iocoder.yudao.framework.common.pojo.PageResult<>(List.of(), 0L);
                    }