liu
3 天以前 8ad99e5f72085d14fd56e8faa7aa36701823c8b5
feat(crm): 统一客户团队数据权限并放开开票金额上限

- 联系人/商机/合同/报价/回款/回款计划/开票统一继承客户团队权限
- super_admin 视为 CRM 管理员,可查看全部数据
- 合同/报价列表移除额外的客户权限过滤
- 取消开票金额不得超过合同金额的限制
- 新增 CRM 权限修复与诊断 SQL
已添加2个文件
已修改10个文件
902 ■■■■■ 文件已修改
sql/crm_permission_diagnose.sql 118 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/crm_permission_repair.sql 271 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/framework/permission/core/aop/CrmPermissionAspect.java 232 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contact/CrmContactServiceImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/invoice/CrmInvoiceServiceImpl.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/quotation/CrmSaleQuotationServiceImpl.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/util/CrmPermissionUtils.java 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/crm_permission_diagnose.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,118 @@
-- ============================================================
-- CRM æƒé™è¯Šæ–­è„šæœ¬ï¼šæŸ¥æŸä¸ªè´¦å·çš„æƒé™åˆ°åº•æ¥è‡ªå“ªé‡Œ
--
-- ä½¿ç”¨æ–¹æ³•:
--   1. æŠŠ @uid æ”¹æˆè¦æŸ¥è¯¢çš„用户编号
--   2. æ•´æ®µæ‰§è¡Œï¼ŒæŒ‰ç»“果逐段看
--   3. æƒ³æŸ¥æŸæ¡åˆåŒä¸ºä»€ä¹ˆèƒ½/不能看,改 @cid åŽæ‰§è¡Œæœ€åŽä¸€æ®µ
-- ============================================================
SET @uid = 235;
-- ------------------------------------------------------------
-- 1. è´¦å·è§’色(决定是不是管理员,管理员不受数据权限限制)
-- ------------------------------------------------------------
SELECT '1.账号角色' AS section, u.id, u.username, u.nickname,
       r.code AS role_code, r.name AS role_name
FROM system_users u
         LEFT JOIN system_user_role ur ON ur.user_id = u.id AND ur.deleted = 0
         LEFT JOIN system_role r ON r.id = ur.role_id AND r.deleted = 0
WHERE u.id = @uid AND u.deleted = 0;
-- è¯´æ˜Žï¼šrole_code é‡Œæœ‰ super_admin æˆ– crm_admin â†’ å…¨éƒ¨æ•°æ®å¯è§
-- ------------------------------------------------------------
-- 2. ä»–作为「客户负责人」的客户
-- ------------------------------------------------------------
SELECT '2.客户负责人' AS section, c.id AS customer_id, c.name AS customer_name,
       c.owner_user_id
FROM crm_customer c
WHERE c.deleted = 0 AND c.owner_user_id = @uid;
-- ------------------------------------------------------------
-- 3. ä»–在「客户团队」里的记录(联系人/商机/合同 é è¿™ä¸ªç»§æ‰¿ï¼‰
-- ------------------------------------------------------------
SELECT '3.客户团队' AS section, c.id AS customer_id, c.name AS customer_name,
       p.level, ELT(p.level, '负责人', '只读', '读写') AS level_name
FROM crm_permission p
         JOIN crm_customer c ON c.id = p.biz_id AND c.deleted = 0
WHERE p.biz_type = 2 AND p.user_id = @uid AND p.deleted = 0
ORDER BY c.id;
-- ------------------------------------------------------------
-- 4. ä»–在各业务模块「自己的团队」里的身份(非客户)
--    æ³¨æ„ï¼šçº¿ç´¢/报价/回款/回款计划/开票 ä»ç„¶åªè®¤è¿™é‡Œ
-- ------------------------------------------------------------
SELECT '4.业务团队' AS section,
       CASE p.biz_type
           WHEN 1 THEN '线索'
           WHEN 3 THEN '联系人'
           WHEN 4 THEN '商机'
           WHEN 5 THEN '合同'
           WHEN 7 THEN '回款'
           WHEN 8 THEN '回款计划'
           WHEN 9 THEN '销售报价'
           WHEN 10 THEN '开票'
           ELSE CONCAT('类型', p.biz_type) END AS module,
       p.biz_id, p.level, ELT(p.level, '负责人', '只读', '读写') AS level_name
FROM crm_permission p
WHERE p.user_id = @uid
  AND p.deleted = 0
  AND p.biz_type <> 2
ORDER BY p.biz_type, p.biz_id;
-- ------------------------------------------------------------
-- 5. ä»–作为各模块「负责人」的数据
-- ------------------------------------------------------------
SELECT '5.1客户负责人' AS section, c.id, c.name AS data_name FROM crm_customer c
WHERE c.deleted = 0 AND c.owner_user_id = @uid;
SELECT '5.2联系人负责人' AS section, ct.id, ct.name AS data_name FROM crm_contact ct
WHERE ct.deleted = 0 AND ct.owner_user_id = @uid;
SELECT '5.3商机负责人' AS section, b.id, b.name AS data_name FROM crm_business b
WHERE b.deleted = 0 AND b.owner_user_id = @uid;
SELECT '5.4合同负责人' AS section, c.id, c.name AS data_name FROM crm_contract c
WHERE c.deleted = 0 AND c.owner_user_id = @uid;
SELECT '5.5报价负责人' AS section, q.id, CONCAT(q.no, ' ', q.name) AS data_name FROM crm_sale_quotation q
WHERE q.deleted = 0 AND q.owner_user_id = @uid;
SELECT '5.6回款负责人' AS section, r.id, r.no AS data_name FROM crm_receivable r
WHERE r.deleted = 0 AND r.owner_user_id = @uid;
SELECT '5.7开票负责人' AS section, i.id, i.no AS data_name FROM crm_invoice i
WHERE i.deleted = 0 AND i.owner_user_id = @uid;
-- ------------------------------------------------------------
-- 6. å•条数据溯源:为什么这个账号能/不能看这条合同
--    æŠŠ @cid æ”¹æˆåˆåŒç¼–号后执行
-- ------------------------------------------------------------
SET @cid = 3;
SELECT '6.合同权限溯源' AS section,
       ct.id AS contract_id,
       ct.name AS contract_name,
       ct.customer_id,
       ct.owner_user_id                       AS contract_owner,
       cu.owner_user_id                       AS customer_owner,
       EXISTS(SELECT 1 FROM crm_permission p
              WHERE p.biz_type = 5 AND p.biz_id = ct.id
                AND p.user_id = @uid AND p.deleted = 0) AS in_contract_team,
       EXISTS(SELECT 1 FROM crm_permission p
              WHERE p.biz_type = 2 AND p.biz_id = ct.customer_id
                AND p.user_id = @uid AND p.deleted = 0) AS in_customer_team,
       (cu.owner_user_id = @uid)              AS is_customer_owner,
       (ct.owner_user_id = @uid)              AS is_contract_owner
FROM crm_contract ct
         LEFT JOIN crm_customer cu ON cu.id = ct.customer_id AND cu.deleted = 0
WHERE ct.id = @cid AND ct.deleted = 0;
-- åˆ¤æ–­è§„则:
--   role_code = super_admin / crm_admin           â†’ èƒ½çœ‹
--   is_contract_owner = 1                          â†’ èƒ½çœ‹
--   in_contract_team = 1                           â†’ èƒ½çœ‹
--   is_customer_owner = 1                           â†’ èƒ½çœ‹ï¼ˆåˆåŒç»§æ‰¿å®¢æˆ·è´Ÿè´£äººï¼‰
--   in_customer_team = 1                            â†’ èƒ½çœ‹ï¼ˆåˆåŒç»§æ‰¿å®¢æˆ·å›¢é˜Ÿï¼‰
--   ä»¥ä¸Šéƒ½ä¸æ»¡è¶³                                     â†’ çœ‹ä¸åˆ°
sql/crm_permission_repair.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,271 @@
-- ============================================================
-- CRM æ•°æ®æƒé™ä¿®å¤è„šæœ¬ï¼ˆç»Ÿä¸€ä»¥ã€Œå®¢æˆ·å›¢é˜Ÿã€ä¸ºå‡†ï¼‰
--
-- èƒŒæ™¯ï¼ˆå®žæµ‹å‘现):
--   1. crm_customer.owner_user_id ä¸Ž crm_permission çš„负责人行不一致(6 ä¸ªå®¢æˆ·ï¼‰
--   2. éƒ¨åˆ†åˆåŒ/商机/联系人没有自身的权限记录(历史数据直接入库)
--
-- è¯´æ˜Žï¼š
--   - ä»£ç å·²å…¼å®¹ä¸Šè¿°å¼‚常,本脚本不是必须执行,但执行后数据更规范
--   - è„šæœ¬å¹‚ç­‰ï¼Œå¯é‡å¤æ‰§è¡Œ
--   - æ‰§è¡Œå‰è¯·å¤‡ä»½ crm_permission、crm_customer
--   - ä¸æ¶‰åŠè¡¨ç»“构变更,无需停机
--
-- æžšä¸¾å€¼ï¼š
--   biz_type:2=客户 3=联系人 4=商机 5=合同
--   level   ï¼š1=负责人 2=只读 3=读写
-- ============================================================
-- ============================================================
-- ä¸€ã€å®¢æˆ·è´Ÿè´£äººæƒé™è¡Œå¯¹é½ crm_customer.owner_user_id
-- ============================================================
-- 1.1 ç›®æ ‡è´Ÿè´£äººè¿˜æ²¡æœ‰æƒé™è¡Œï¼šæŠŠæ—§è´Ÿè´£äººè¡Œæ”¹åˆ°ç›®æ ‡è´Ÿè´£äºº
DROP TEMPORARY TABLE IF EXISTS tmp_perm_customer;
CREATE TEMPORARY TABLE tmp_perm_customer AS
SELECT biz_id, user_id
FROM crm_permission
WHERE biz_type = 2 AND deleted = 0;
ALTER TABLE tmp_perm_customer ADD INDEX idx_bu (biz_id, user_id);
UPDATE crm_permission p
    JOIN crm_customer c ON c.id = p.biz_id
    LEFT JOIN tmp_perm_customer t ON t.biz_id = c.id AND t.user_id = c.owner_user_id
SET p.user_id     = c.owner_user_id,
    p.update_time = NOW()
WHERE p.biz_type = 2
  AND p.deleted = 0
  AND c.deleted = 0
  AND p.level = 1
  AND p.user_id <> c.owner_user_id
  AND t.user_id IS NULL;
-- 1.2 ç›®æ ‡è´Ÿè´£äººå·²æœ‰æƒé™è¡Œï¼šå‡çº§ä¸ºè´Ÿè´£äºº
UPDATE crm_permission p
    JOIN crm_customer c ON c.id = p.biz_id
SET p.level       = 1,
    p.update_time = NOW()
WHERE p.biz_type = 2
  AND p.deleted = 0
  AND c.deleted = 0
  AND p.user_id = c.owner_user_id
  AND p.level <> 1;
-- 1.3 åˆ é™¤é‡å¤çš„æ—§è´Ÿè´£äººè¡Œï¼ˆç›®æ ‡è´Ÿè´£äººè¡Œå·²å­˜åœ¨æ—¶ï¼‰
DROP TEMPORARY TABLE IF EXISTS tmp_perm_customer;
CREATE TEMPORARY TABLE tmp_perm_customer AS
SELECT id, biz_id, user_id, level
FROM crm_permission
WHERE biz_type = 2 AND deleted = 0;
ALTER TABLE tmp_perm_customer ADD INDEX idx_bu (biz_id, user_id);
DELETE p
FROM crm_permission p
         JOIN crm_customer c ON c.id = p.biz_id
         JOIN tmp_perm_customer t
              ON t.biz_id = c.id AND t.user_id = c.owner_user_id AND t.level = 1
WHERE p.biz_type = 2
  AND p.deleted = 0
  AND c.deleted = 0
  AND p.level = 1
  AND p.user_id <> c.owner_user_id;
DROP TEMPORARY TABLE IF EXISTS tmp_perm_customer;
-- ============================================================
-- äºŒã€è¡¥é½å­æ•°æ®è‡ªèº«çš„负责人权限行
-- ============================================================
-- 2.1 åˆåŒï¼ˆbiz_type = 5)
INSERT INTO crm_permission (biz_type, biz_id, user_id, level, creator, create_time, updater, update_time, deleted)
SELECT 5, ct.id, ct.owner_user_id, 1, '1', NOW(), '1', NOW(), 0
FROM crm_contract ct
WHERE ct.deleted = 0
  AND ct.owner_user_id IS NOT NULL
  AND NOT EXISTS (SELECT 1
                  FROM crm_permission p
                  WHERE p.biz_type = 5
                    AND p.biz_id = ct.id
                    AND p.user_id = ct.owner_user_id
                    AND p.deleted = 0);
UPDATE crm_permission p
    JOIN crm_contract ct ON ct.id = p.biz_id AND ct.deleted = 0
SET p.level       = 1,
    p.update_time = NOW()
WHERE p.biz_type = 5
  AND p.deleted = 0
  AND p.user_id = ct.owner_user_id
  AND p.level <> 1;
-- 2.2 å•†æœºï¼ˆbiz_type = 4)
INSERT INTO crm_permission (biz_type, biz_id, user_id, level, creator, create_time, updater, update_time, deleted)
SELECT 4, b.id, b.owner_user_id, 1, '1', NOW(), '1', NOW(), 0
FROM crm_business b
WHERE b.deleted = 0
  AND b.owner_user_id IS NOT NULL
  AND NOT EXISTS (SELECT 1
                  FROM crm_permission p
                  WHERE p.biz_type = 4
                    AND p.biz_id = b.id
                    AND p.user_id = b.owner_user_id
                    AND p.deleted = 0);
UPDATE crm_permission p
    JOIN crm_business b ON b.id = p.biz_id AND b.deleted = 0
SET p.level       = 1,
    p.update_time = NOW()
WHERE p.biz_type = 4
  AND p.deleted = 0
  AND p.user_id = b.owner_user_id
  AND p.level <> 1;
-- 2.3 è”系人(biz_type = 3)
INSERT INTO crm_permission (biz_type, biz_id, user_id, level, creator, create_time, updater, update_time, deleted)
SELECT 3, ct.id, ct.owner_user_id, 1, '1', NOW(), '1', NOW(), 0
FROM crm_contact ct
WHERE ct.deleted = 0
  AND ct.owner_user_id IS NOT NULL
  AND NOT EXISTS (SELECT 1
                  FROM crm_permission p
                  WHERE p.biz_type = 3
                    AND p.biz_id = ct.id
                    AND p.user_id = ct.owner_user_id
                    AND p.deleted = 0);
UPDATE crm_permission p
    JOIN crm_contact ct ON ct.id = p.biz_id AND ct.deleted = 0
SET p.level       = 1,
    p.update_time = NOW()
WHERE p.biz_type = 3
  AND p.deleted = 0
  AND p.user_id = ct.owner_user_id
  AND p.level <> 1;
-- 2.4 é”€å”®æŠ¥ä»·ï¼ˆbiz_type = 9)
INSERT INTO crm_permission (biz_type, biz_id, user_id, level, creator, create_time, updater, update_time, deleted)
SELECT 9, q.id, q.owner_user_id, 1, '1', NOW(), '1', NOW(), 0
FROM crm_sale_quotation q
WHERE q.deleted = 0
  AND q.owner_user_id IS NOT NULL
  AND NOT EXISTS (SELECT 1
                  FROM crm_permission p
                  WHERE p.biz_type = 9
                    AND p.biz_id = q.id
                    AND p.user_id = q.owner_user_id
                    AND p.deleted = 0);
UPDATE crm_permission p
    JOIN crm_sale_quotation q ON q.id = p.biz_id AND q.deleted = 0
SET p.level       = 1,
    p.update_time = NOW()
WHERE p.biz_type = 9
  AND p.deleted = 0
  AND p.user_id = q.owner_user_id
  AND p.level <> 1;
-- 2.5 å›žæ¬¾ï¼ˆbiz_type = 7)
INSERT INTO crm_permission (biz_type, biz_id, user_id, level, creator, create_time, updater, update_time, deleted)
SELECT 7, r.id, r.owner_user_id, 1, '1', NOW(), '1', NOW(), 0
FROM crm_receivable r
WHERE r.deleted = 0
  AND r.owner_user_id IS NOT NULL
  AND NOT EXISTS (SELECT 1
                  FROM crm_permission p
                  WHERE p.biz_type = 7
                    AND p.biz_id = r.id
                    AND p.user_id = r.owner_user_id
                    AND p.deleted = 0);
UPDATE crm_permission p
    JOIN crm_receivable r ON r.id = p.biz_id AND r.deleted = 0
SET p.level       = 1,
    p.update_time = NOW()
WHERE p.biz_type = 7
  AND p.deleted = 0
  AND p.user_id = r.owner_user_id
  AND p.level <> 1;
-- 2.6 å›žæ¬¾è®¡åˆ’(biz_type = 8)
INSERT INTO crm_permission (biz_type, biz_id, user_id, level, creator, create_time, updater, update_time, deleted)
SELECT 8, rp.id, rp.owner_user_id, 1, '1', NOW(), '1', NOW(), 0
FROM crm_receivable_plan rp
WHERE rp.deleted = 0
  AND rp.owner_user_id IS NOT NULL
  AND NOT EXISTS (SELECT 1
                  FROM crm_permission p
                  WHERE p.biz_type = 8
                    AND p.biz_id = rp.id
                    AND p.user_id = rp.owner_user_id
                    AND p.deleted = 0);
UPDATE crm_permission p
    JOIN crm_receivable_plan rp ON rp.id = p.biz_id AND rp.deleted = 0
SET p.level       = 1,
    p.update_time = NOW()
WHERE p.biz_type = 8
  AND p.deleted = 0
  AND p.user_id = rp.owner_user_id
  AND p.level <> 1;
-- 2.7 å¼€ç¥¨ï¼ˆbiz_type = 10)
INSERT INTO crm_permission (biz_type, biz_id, user_id, level, creator, create_time, updater, update_time, deleted)
SELECT 10, i.id, i.owner_user_id, 1, '1', NOW(), '1', NOW(), 0
FROM crm_invoice i
WHERE i.deleted = 0
  AND i.owner_user_id IS NOT NULL
  AND NOT EXISTS (SELECT 1
                  FROM crm_permission p
                  WHERE p.biz_type = 10
                    AND p.biz_id = i.id
                    AND p.user_id = i.owner_user_id
                    AND p.deleted = 0);
UPDATE crm_permission p
    JOIN crm_invoice i ON i.id = p.biz_id AND i.deleted = 0
SET p.level       = 1,
    p.update_time = NOW()
WHERE p.biz_type = 10
  AND p.deleted = 0
  AND p.user_id = i.owner_user_id
  AND p.level <> 1;
-- ============================================================
-- ä¸‰ã€æ ¡éªŒï¼ˆå¯é€‰ï¼Œæ‰§è¡ŒåŽè·‘一下看结果)
-- ============================================================
-- 3.1 å®¢æˆ·è´Ÿè´£äººæ˜¯å¦å·²ä¸€è‡´ï¼ˆåº”为 0 è¡Œï¼‰
-- SELECT c.id, c.name, c.owner_user_id, p.user_id AS perm_owner
-- FROM crm_customer c
-- LEFT JOIN crm_permission p
--        ON p.biz_type = 2 AND p.biz_id = c.id AND p.level = 1 AND p.deleted = 0
-- WHERE c.deleted = 0 AND (p.user_id IS NULL OR p.user_id <> c.owner_user_id);
-- 3.2 å­æ•°æ®æ˜¯å¦éƒ½æœ‰è´Ÿè´£äººæƒé™è¡Œï¼ˆåº”为 0)
-- SELECT '合同' AS biz, COUNT(*) AS missing FROM crm_contract ct
--  WHERE ct.deleted = 0 AND NOT EXISTS (
--    SELECT 1 FROM crm_permission p WHERE p.biz_type = 5 AND p.biz_id = ct.id AND p.level = 1 AND p.deleted = 0)
-- UNION ALL
-- SELECT '商机', COUNT(*) FROM crm_business b
--  WHERE b.deleted = 0 AND NOT EXISTS (
--    SELECT 1 FROM crm_permission p WHERE p.biz_type = 4 AND p.biz_id = b.id AND p.level = 1 AND p.deleted = 0)
-- UNION ALL
-- SELECT '联系人', COUNT(*) FROM crm_contact ct
--  WHERE ct.deleted = 0 AND NOT EXISTS (
--    SELECT 1 FROM crm_permission p WHERE p.biz_type = 3 AND p.biz_id = ct.id AND p.level = 1 AND p.deleted = 0)
-- UNION ALL
-- SELECT '销售报价', COUNT(*) FROM crm_sale_quotation q
--  WHERE q.deleted = 0 AND NOT EXISTS (
--    SELECT 1 FROM crm_permission p WHERE p.biz_type = 9 AND p.biz_id = q.id AND p.level = 1 AND p.deleted = 0)
-- UNION ALL
-- SELECT '回款', COUNT(*) FROM crm_receivable r
--  WHERE r.deleted = 0 AND NOT EXISTS (
--    SELECT 1 FROM crm_permission p WHERE p.biz_type = 7 AND p.biz_id = r.id AND p.level = 1 AND p.deleted = 0)
-- UNION ALL
-- SELECT '回款计划', COUNT(*) FROM crm_receivable_plan rp
--  WHERE rp.deleted = 0 AND NOT EXISTS (
--    SELECT 1 FROM crm_permission p WHERE p.biz_type = 8 AND p.biz_id = rp.id AND p.level = 1 AND p.deleted = 0)
-- UNION ALL
-- SELECT '开票', COUNT(*) FROM crm_invoice i
--  WHERE i.deleted = 0 AND NOT EXISTS (
--    SELECT 1 FROM crm_permission p WHERE p.biz_type = 10 AND p.biz_id = i.id AND p.level = 1 AND p.deleted = 0);
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/framework/permission/core/aop/CrmPermissionAspect.java
@@ -6,8 +6,22 @@
import cn.iocoder.yudao.framework.common.util.spring.SpringExpressionUtils;
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.contact.CrmContactDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.invoice.CrmInvoiceDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.quotation.CrmSaleQuotationDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivableDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivablePlanDO;
import cn.iocoder.yudao.module.crm.dal.mysql.business.CrmBusinessMapper;
import cn.iocoder.yudao.module.crm.dal.mysql.contact.CrmContactMapper;
import cn.iocoder.yudao.module.crm.dal.mysql.contract.CrmContractMapper;
import cn.iocoder.yudao.module.crm.dal.mysql.customer.CrmCustomerMapper;
import cn.iocoder.yudao.module.crm.dal.mysql.invoice.CrmInvoiceMapper;
import cn.iocoder.yudao.module.crm.dal.mysql.quotation.CrmSaleQuotationMapper;
import cn.iocoder.yudao.module.crm.dal.mysql.receivable.CrmReceivableMapper;
import cn.iocoder.yudao.module.crm.dal.mysql.receivable.CrmReceivablePlanMapper;
import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum;
import cn.iocoder.yudao.module.crm.framework.permission.core.annotations.CrmPermission;
@@ -48,6 +62,20 @@
    @Resource
    private CrmBusinessMapper businessMapper;
    @Resource
    private CrmContractMapper contractMapper;
    @Resource
    private CrmContactMapper contactMapper;
    @Resource
    private CrmCustomerMapper customerMapper;
    @Resource
    private CrmSaleQuotationMapper quotationMapper;
    @Resource
    private CrmReceivableMapper receivableMapper;
    @Resource
    private CrmReceivablePlanMapper receivablePlanMapper;
    @Resource
    private CrmInvoiceMapper invoiceMapper;
    @Before("@annotation(crmPermission)")
    public void doBefore(JoinPoint joinPoint, CrmPermission crmPermission) {
@@ -79,33 +107,37 @@
        if (CrmPermissionUtils.isCrmAdmin()) {
            return;
        }
        // ç‰¹æ®Šï¼šæ²¡æœ‰æ•°æ®æƒé™çš„æƒ…况,针对 READ çš„特殊处理
        // 2. æ•°æ®æœ¬èº«çš„负责人:直接通过(兼容权限记录缺失的历史数据)
        Long userId = getUserId();
        if (isRecordOwner(bizType, bizId, userId)) {
            return;
        }
        // 3. è”系人/商机/合同/报价/回款/回款计划/开票:继承所属客户的权限
        //    æ”¾åœ¨ã€Œæ— æ•°æ®æƒé™ã€åˆ¤æ–­ä¹‹å‰ï¼Œé¿å…æƒé™è®°å½•缺失的历史数据导致负责人无法写操作
        if (validateCustomerPermission(bizType, bizId, userId, permissionLevel)) {
            return;
        }
        // 3. ç‰¹æ®Šï¼šæ²¡æœ‰æ•°æ®æƒé™çš„æƒ…况,针对 READ çš„特殊处理
        if (CollUtil.isEmpty(bizPermissions)) {
            // 1.1 å…¬æµ·æ•°æ®ï¼Œå¦‚果没有团队成员,大家也应该有 READ æƒé™æ‰å¯¹
            if (CrmPermissionLevelEnum.isRead(permissionLevel)) {
            // 3.1 çœŸæ­£çš„公海数据(父客户也没有团队),大家有 READ æƒé™ï¼›
            //     å¦‚果父客户有团队,则走上面继承逻辑,这里不能放行,避免越权读取
            if (CrmPermissionLevelEnum.isRead(permissionLevel) && !hasParentCustomerTeam(bizType, bizId)) {
                return;
            }
            // æ²¡æœ‰æ•°æ®æƒé™çš„æƒ…况下超出了读权限直接报错,避免后面校验空指针
            throw exception(CRM_PERMISSION_DENIED, CrmBizTypeEnum.getNameByType(bizType));
        } else { // 1.2 æœ‰æ•°æ®æƒé™ä½†æ˜¯æ²¡æœ‰è´Ÿè´£äººçš„æƒ…况
        } else { // 3.2 æœ‰æ•°æ®æƒé™ä½†æ˜¯æ²¡æœ‰è´Ÿè´£äººçš„æƒ…况
            if (!anyMatch(bizPermissions, item -> CrmPermissionLevelEnum.isOwner(item.getLevel()))
                && CrmPermissionLevelEnum.isRead(permissionLevel)) {
                return;
            }
        }
        // 2. åªè€ƒè™‘自身的权限
        Long userId = getUserId();
        // 4. åªè€ƒè™‘自身的权限
        CrmPermissionDO userPermission = CollUtil.findOne(bizPermissions, permission -> ObjUtil.equal(permission.getUserId(), userId));
        if (userPermission != null) {
            if (isUserPermissionValid(userPermission, permissionLevel)) {
                return;
            }
        }
        // 3. å•†æœºç‰¹æ®Šå¤„理:检查客户团队权限
        if (CrmBizTypeEnum.CRM_BUSINESS.getType().equals(bizType)) {
            if (validateBusinessCustomerPermission(bizId, userId, permissionLevel)) {
                return;
            }
        }
@@ -118,11 +150,9 @@
            if (subordinatePermission != null && isUserPermissionValid(subordinatePermission, permissionLevel)) {
                return;
            }
            // å•†æœºç‰¹æ®Šå¤„理:检查客户团队权限(下属的)
            if (CrmBizTypeEnum.CRM_BUSINESS.getType().equals(bizType)) {
                if (validateBusinessCustomerPermission(bizId, subordinateUserId, permissionLevel)) {
                    return;
                }
            // ç»§æ‰¿æ‰€å±žå®¢æˆ·çš„æƒé™ï¼ˆä¸‹å±žçš„)
            if (validateCustomerPermission(bizType, bizId, subordinateUserId, permissionLevel)) {
                return;
            }
        }
@@ -133,33 +163,161 @@
    }
    /**
     * å•†æœºç‰¹æ®Šæƒé™æ ¡éªŒï¼šæ£€æŸ¥ç”¨æˆ·æ˜¯å¦æœ‰è¯¥å•†æœºæ‰€å±žå®¢æˆ·çš„团队权限
     * ç»§æ‰¿æ‰€å±žå®¢æˆ·çš„æƒé™æ ¡éªŒï¼šå®¢æˆ·è´Ÿè´£äººã€å®¢æˆ·å›¢é˜Ÿæˆå‘˜
     *
     * @param businessId     å•†æœºç¼–号
     * @param userId         ç”¨æˆ·ç¼–号
     * é€‚用于:联系人、商机、合同、销售报价、回款、回款计划、开票
     *
     * @param bizType         æ•°æ®ç±»åž‹
     * @param bizId           æ•°æ®ç¼–号
     * @param userId          ç”¨æˆ·ç¼–号
     * @param permissionLevel éœ€è¦çš„æƒé™çº§åˆ«
     * @return æ˜¯å¦æœ‰æƒé™
     */
    private boolean validateBusinessCustomerPermission(Long businessId, Long userId, Integer permissionLevel) {
        // èŽ·å–å•†æœºçš„å®¢æˆ·ID
        CrmBusinessDO business = businessMapper.selectById(businessId);
        if (business == null || business.getCustomerId() == null) {
    private boolean validateCustomerPermission(Integer bizType, Long bizId, Long userId, Integer permissionLevel) {
        // 1. èŽ·å–æ‰€å±žå®¢æˆ·
        Long customerId = getCustomerId(bizType, bizId);
        if (customerId == null) {
            return false;
        }
        // æ£€æŸ¥å®¢æˆ·å›¢é˜Ÿæƒé™
        List<CrmPermissionDO> customerPermissions = crmPermissionService.getPermissionListByBiz(
                CrmBizTypeEnum.CRM_CUSTOMER.getType(), Collections.singleton(business.getCustomerId()));
        if (CollUtil.isEmpty(customerPermissions)) {
            // å®¢æˆ·å…¬æµ·æ•°æ®ï¼ŒREAD æƒé™æ”¾è¡Œ
            return CrmPermissionLevelEnum.isRead(permissionLevel);
        }
        // æ£€æŸ¥ç”¨æˆ·æ˜¯å¦æœ‰å®¢æˆ·å›¢é˜Ÿçš„æƒé™
        CrmPermissionDO userCustomerPermission = CollUtil.findOne(customerPermissions,
                permission -> ObjUtil.equal(permission.getUserId(), userId));
        if (userCustomerPermission != null && isUserPermissionValid(userCustomerPermission, permissionLevel)) {
        CrmCustomerDO customer = customerMapper.selectById(customerId);
        // 2. å®¢æˆ·è´Ÿè´£äººï¼šç›´æŽ¥é€šè¿‡ï¼ˆå…¼å®¹æƒé™è¡¨ä¸­è´Ÿè´£äººè®°å½•缺失/不一致的历史数据)
        if (customer != null && ObjUtil.equal(customer.getOwnerUserId(), userId)) {
            return true;
        }
        return false;
        // 3. æ£€æŸ¥å®¢æˆ·å›¢é˜Ÿæƒé™
        List<CrmPermissionDO> customerPermissions = crmPermissionService.getPermissionListByBiz(
                CrmBizTypeEnum.CRM_CUSTOMER.getType(), Collections.singleton(customerId));
        if (CollUtil.isEmpty(customerPermissions)) {
            // å®¢æˆ·æœ‰è´Ÿè´£äººä½†ä¸æ˜¯å½“前用户:不是公海,不能放行
            if (customer != null && customer.getOwnerUserId() != null) {
                return false;
            }
            // çœŸæ­£çš„公海数据,READ æƒé™æ”¾è¡Œ
            return CrmPermissionLevelEnum.isRead(permissionLevel);
        }
        // 4. æ£€æŸ¥ç”¨æˆ·æ˜¯å¦æœ‰å®¢æˆ·å›¢é˜Ÿçš„æƒé™
        CrmPermissionDO userCustomerPermission = CollUtil.findOne(customerPermissions,
                permission -> ObjUtil.equal(permission.getUserId(), userId));
        return userCustomerPermission != null && isUserPermissionValid(userCustomerPermission, permissionLevel);
    }
    /**
     * èŽ·å–æ•°æ®æ‰€å±žçš„å®¢æˆ·ç¼–å·
     *
     * @param bizType æ•°æ®ç±»åž‹
     * @param bizId   æ•°æ®ç¼–号
     * @return å®¢æˆ·ç¼–号,不存在时返回 null
     */
    private Long getCustomerId(Integer bizType, Long bizId) {
        if (CrmBizTypeEnum.CRM_CONTACT.getType().equals(bizType)) {
            CrmContactDO contact = contactMapper.selectById(bizId);
            return contact == null ? null : contact.getCustomerId();
        }
        if (CrmBizTypeEnum.CRM_BUSINESS.getType().equals(bizType)) {
            CrmBusinessDO business = businessMapper.selectById(bizId);
            return business == null ? null : business.getCustomerId();
        }
        if (CrmBizTypeEnum.CRM_CONTRACT.getType().equals(bizType)) {
            CrmContractDO contract = contractMapper.selectById(bizId);
            return contract == null ? null : contract.getCustomerId();
        }
        if (CrmBizTypeEnum.CRM_SALE_QUOTATION.getType().equals(bizType)) {
            CrmSaleQuotationDO quotation = quotationMapper.selectById(bizId);
            return quotation == null ? null : quotation.getCustomerId();
        }
        if (CrmBizTypeEnum.CRM_RECEIVABLE.getType().equals(bizType)) {
            CrmReceivableDO receivable = receivableMapper.selectById(bizId);
            return receivable == null ? null : receivable.getCustomerId();
        }
        if (CrmBizTypeEnum.CRM_RECEIVABLE_PLAN.getType().equals(bizType)) {
            CrmReceivablePlanDO plan = receivablePlanMapper.selectById(bizId);
            return plan == null ? null : plan.getCustomerId();
        }
        if (CrmBizTypeEnum.CRM_INVOICE.getType().equals(bizType)) {
            CrmInvoiceDO invoice = invoiceMapper.selectById(bizId);
            return invoice == null ? null : invoice.getCustomerId();
        }
        return null;
    }
    /**
     * èŽ·å–æ•°æ®æœ¬èº«çš„è´Ÿè´£äººç¼–å·
     *
     * @param bizType æ•°æ®ç±»åž‹
     * @param bizId   æ•°æ®ç¼–号
     * @return è´Ÿè´£äººç¼–号,不存在时返回 null
     */
    private Long getOwnerUserId(Integer bizType, Long bizId) {
        if (CrmBizTypeEnum.CRM_CONTACT.getType().equals(bizType)) {
            CrmContactDO contact = contactMapper.selectById(bizId);
            return contact == null ? null : contact.getOwnerUserId();
        }
        if (CrmBizTypeEnum.CRM_BUSINESS.getType().equals(bizType)) {
            CrmBusinessDO business = businessMapper.selectById(bizId);
            return business == null ? null : business.getOwnerUserId();
        }
        if (CrmBizTypeEnum.CRM_CONTRACT.getType().equals(bizType)) {
            CrmContractDO contract = contractMapper.selectById(bizId);
            return contract == null ? null : contract.getOwnerUserId();
        }
        if (CrmBizTypeEnum.CRM_SALE_QUOTATION.getType().equals(bizType)) {
            CrmSaleQuotationDO quotation = quotationMapper.selectById(bizId);
            return quotation == null ? null : quotation.getOwnerUserId();
        }
        if (CrmBizTypeEnum.CRM_RECEIVABLE.getType().equals(bizType)) {
            CrmReceivableDO receivable = receivableMapper.selectById(bizId);
            return receivable == null ? null : receivable.getOwnerUserId();
        }
        if (CrmBizTypeEnum.CRM_RECEIVABLE_PLAN.getType().equals(bizType)) {
            CrmReceivablePlanDO plan = receivablePlanMapper.selectById(bizId);
            return plan == null ? null : plan.getOwnerUserId();
        }
        if (CrmBizTypeEnum.CRM_INVOICE.getType().equals(bizType)) {
            CrmInvoiceDO invoice = invoiceMapper.selectById(bizId);
            return invoice == null ? null : invoice.getOwnerUserId();
        }
        return null;
    }
    /**
     * åˆ¤æ–­ç”¨æˆ·æ˜¯å¦æ˜¯æ•°æ®æœ¬èº«çš„负责人
     *
     * ä¸ä¾èµ– crm_permission è®°å½•,兼容历史数据中权限记录缺失的情况
     *
     * @param bizType æ•°æ®ç±»åž‹
     * @param bizId   æ•°æ®ç¼–号
     * @param userId  ç”¨æˆ·ç¼–号
     * @return æ˜¯å¦æ˜¯è´Ÿè´£äºº
     */
    private boolean isRecordOwner(Integer bizType, Long bizId, Long userId) {
        return userId != null && ObjUtil.equal(getOwnerUserId(bizType, bizId), userId);
    }
    /**
     * åˆ¤æ–­å­æ•°æ®æ‰€å±žå®¢æˆ·æ˜¯å¦å­˜åœ¨å›¢é˜Ÿï¼ˆè´Ÿè´£äººæˆ–团队成员)
     *
     * ç”¨äºŽåŒºåˆ†ã€ŒçœŸæ­£çš„公海数据」和「父客户有团队、但子数据本身没有权限记录」的历史数据
     *
     * @param bizType æ•°æ®ç±»åž‹
     * @param bizId   æ•°æ®ç¼–号
     * @return æ˜¯å¦å­˜åœ¨å®¢æˆ·å›¢é˜Ÿ
     */
    private boolean hasParentCustomerTeam(Integer bizType, Long bizId) {
        Long customerId = getCustomerId(bizType, bizId);
        if (customerId == null) {
            return false;
        }
        // å®¢æˆ·æœ‰è´Ÿè´£äººï¼Œè¯´æ˜Žä¸æ˜¯å…¬æµ·
        CrmCustomerDO customer = customerMapper.selectById(customerId);
        if (customer == null) {
            return false;
        }
        if (customer.getOwnerUserId() != null) {
            return true;
        }
        // å®¢æˆ·æ²¡æœ‰è´Ÿè´£äººæ—¶ï¼Œæ£€æŸ¥æ˜¯å¦æœ‰å›¢é˜Ÿæˆå‘˜
        return CollUtil.isNotEmpty(crmPermissionService.getPermissionListByBiz(
                CrmBizTypeEnum.CRM_CUSTOMER.getType(), Collections.singleton(customerId)));
    }
    /**
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java
@@ -126,6 +126,9 @@
        permissionService.createPermission(new CrmPermissionCreateReqBO().setUserId(business.getOwnerUserId())
                .setBizType(CrmBizTypeEnum.CRM_BUSINESS.getType()).setBizId(business.getId())
                .setLevel(CrmPermissionLevelEnum.OWNER.getLevel()));
        // 3.1 å®¢æˆ·è´Ÿè´£äººè‡ªåŠ¨æ‹¥æœ‰è¯¥å•†æœºçš„è¯»å†™æƒé™
        permissionService.createPermissionForCustomerOwner(CrmBizTypeEnum.CRM_BUSINESS.getType(),
                business.getId(), business.getCustomerId(), business.getOwnerUserId());
        // 4. åœ¨è”系人的详情页,如果直接【新建商机】,则需要关联下
        if (createReqVO.getContactId() != null) {
@@ -524,6 +527,9 @@
                    permissionService.createPermission(new CrmPermissionCreateReqBO()
                            .setBizType(CrmBizTypeEnum.CRM_BUSINESS.getType()).setBizId(business.getId())
                            .setUserId(ownerUserId).setLevel(CrmPermissionLevelEnum.OWNER.getLevel()));
                    // å®¢æˆ·è´Ÿè´£äººè‡ªåŠ¨æ‹¥æœ‰è¯¥å•†æœºçš„è¯»å†™æƒé™
                    permissionService.createPermissionForCustomerOwner(CrmBizTypeEnum.CRM_BUSINESS.getType(),
                            business.getId(), customer.getId(), ownerUserId);
                    respVO.getCreateBusinessNames().add(importBusiness.getName());
                    // è®°å½•操作日志
                    getSelf().importBusinessLog(business, false);
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contact/CrmContactServiceImpl.java
@@ -100,6 +100,9 @@
        permissionService.createPermission(new CrmPermissionCreateReqBO().setUserId(userId)
                .setBizType(CrmBizTypeEnum.CRM_CONTACT.getType()).setBizId(contact.getId())
                .setLevel(CrmPermissionLevelEnum.OWNER.getLevel()));
        // 3.1 å®¢æˆ·è´Ÿè´£äººè‡ªåŠ¨æ‹¥æœ‰è¯¥è”ç³»äººçš„è¯»å†™æƒé™
        permissionService.createPermissionForCustomerOwner(CrmBizTypeEnum.CRM_CONTACT.getType(),
                contact.getId(), contact.getCustomerId(), userId);
        // 4. å¦‚果有关联商机,则需要创建关联
        if (createReqVO.getBusinessId() != null) {
@@ -363,6 +366,9 @@
                    permissionService.createPermission(new CrmPermissionCreateReqBO()
                            .setBizType(CrmBizTypeEnum.CRM_CONTACT.getType()).setBizId(contact.getId())
                            .setUserId(ownerUserId).setLevel(CrmPermissionLevelEnum.OWNER.getLevel()));
                    // å®¢æˆ·è´Ÿè´£äººè‡ªåŠ¨æ‹¥æœ‰è¯¥è”ç³»äººçš„è¯»å†™æƒé™
                    permissionService.createPermissionForCustomerOwner(CrmBizTypeEnum.CRM_CONTACT.getType(),
                            contact.getId(), customer.getId(), ownerUserId);
                    respVO.getCreateContactNames().add(importContact.getName());
                    // è®°å½•操作日志
                    getSelf().importContactLog(contact, false);
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java
@@ -33,7 +33,6 @@
import cn.iocoder.yudao.module.crm.framework.permission.core.annotations.CrmPermission;
import cn.iocoder.yudao.module.crm.service.business.CrmBusinessService;
import cn.iocoder.yudao.module.crm.service.contact.CrmContactService;
import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
@@ -115,8 +114,6 @@
    @Resource
    private CrmCustomerService customerService;
    @Resource
    private CrmCustomerApi customerApi;
    @Resource
    private CrmBusinessService businessService;
    @Resource
    private CrmContactService contactService;
@@ -170,6 +167,9 @@
        crmPermissionService.createPermission(new CrmPermissionCreateReqBO().setUserId(contract.getOwnerUserId())
                .setBizType(CrmBizTypeEnum.CRM_CONTRACT.getType()).setBizId(contract.getId())
                .setLevel(CrmPermissionLevelEnum.OWNER.getLevel()));
        // 3.1 å®¢æˆ·è´Ÿè´£äººè‡ªåŠ¨æ‹¥æœ‰è¯¥åˆåŒçš„è¯»å†™æƒé™
        crmPermissionService.createPermissionForCustomerOwner(CrmBizTypeEnum.CRM_CONTRACT.getType(),
                contract.getId(), contract.getCustomerId(), contract.getOwnerUserId());
        // 4. è®°å½•操作日志上下文
        LogRecordContext.putVariable("contract", contract);
@@ -481,10 +481,10 @@
                return PageResult.empty();
            }
        }
        // 2. èŽ·å–ç”¨æˆ·æœ‰æƒé™çš„å®¢æˆ· ID åˆ—表
        List<Long> permittedCustomerIds = customerApi.getPermittedCustomerIds();
        // 3. æŸ¥è¯¢åˆ†é¡µ
        return contractMapper.selectPage(pageReqVO, userId, config, permittedCustomerIds);
        // 2. æŸ¥è¯¢åˆ†é¡µ
        // åˆåŒå¯è§æ€§å®Œå…¨ç”± appendPermissionCondition æŽ§åˆ¶ï¼ˆåˆåŒå›¢é˜Ÿ âˆª å®¢æˆ·å›¢é˜Ÿï¼‰ï¼Œ
        // ä¸å†é¢å¤–按「有权限的客户」过滤,否则不在客户团队、但属于合同负责人/合同团队的用户会被错误过滤掉
        return contractMapper.selectPage(pageReqVO, userId, config);
    }
    @Override
@@ -718,6 +718,9 @@
            if (!products.isEmpty() || contract.getTotalPrice() == null) calculateTotalPrice(contract, validateContractProducts(products));
            contractMapper.insert(contract);
            crmPermissionService.createPermission(new CrmPermissionCreateReqBO().setBizType(CrmBizTypeEnum.CRM_CONTRACT.getType()).setBizId(contract.getId()).setUserId(ownerId).setLevel(CrmPermissionLevelEnum.OWNER.getLevel()));
            // å®¢æˆ·è´Ÿè´£äººè‡ªåŠ¨æ‹¥æœ‰è¯¥åˆåŒçš„è¯»å†™æƒé™
            crmPermissionService.createPermissionForCustomerOwner(CrmBizTypeEnum.CRM_CONTRACT.getType(),
                    contract.getId(), customer.getId(), ownerId);
        } else {
            contract.setId(existing.getId());
            List<CrmContractProductDO> amountDetails = products.isEmpty()
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java
@@ -219,6 +219,8 @@
        if (CollUtil.isNotEmpty(reqVO.getToBizTypes())) {
            transfer(reqVO, userId);
        }
        // 2.4 æ–°è´Ÿè´£äººè‡ªåŠ¨æ‹¥æœ‰è¯¥å®¢æˆ·ä¸‹è”ç³»äººã€å•†æœºã€åˆåŒçš„è¯»å†™æƒé™
        permissionService.syncCustomerOwnerPermission(reqVO.getId(), reqVO.getNewOwnerUserId());
        // 3. è®°å½•转移日志
        LogRecordContext.putVariable("customer", customer);
@@ -419,7 +421,8 @@
        customerMapper.updateBatch(updateCustomers);
        // 2.3 åˆ›å»ºè´Ÿè´£äººæ•°æ®æƒé™
        permissionService.createPermissionBatch(createPermissions);
        // TODO @芋艿:要不要处理关联的联系人???
        // 2.4 è´Ÿè´£äººè‡ªåŠ¨æ‹¥æœ‰è¿™äº›å®¢æˆ·ä¸‹è”ç³»äººã€å•†æœºã€åˆåŒçš„è¯»å†™æƒé™
        customers.forEach(customer -> permissionService.syncCustomerOwnerPermission(customer.getId(), ownerUserId));
        // 3. è®°å½•操作日志
        AdminUserRespDTO user = null;
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/invoice/CrmInvoiceServiceImpl.java
@@ -102,9 +102,7 @@
    @LogRecord(type = CRM_INVOICE_TYPE, subType = CRM_INVOICE_CREATE_SUB_TYPE, bizNo = "{{#invoice.id}}",
            success = CRM_INVOICE_CREATE_SUCCESS)
    public Long createInvoice(CrmInvoiceSaveReqVO createReqVO) {
        // 1.1 æ ¡éªŒå¯å¼€ç¥¨é‡‘额超过上限
        validateInvoicePriceExceedsLimit(createReqVO);
        // 1.2 æ ¡éªŒå…³è”数据存在
        // 1.1 æ ¡éªŒå…³è”数据存在(开票金额不再受合同金额限制,仅前端提示可开票金额)
        validateRelationDataExists(createReqVO);
        // 1.3 ç”Ÿæˆå¼€ç¥¨ç¼–号
        String no = noRedisDAO.generate(CrmNoRedisDAO.INVOICE_PREFIX);
@@ -132,22 +130,6 @@
        return invoice.getId();
    }
    private void validateInvoicePriceExceedsLimit(CrmInvoiceSaveReqVO reqVO) {
        // 1. è®¡ç®—剩余可开票金额,不包括 reqVO è‡ªèº«ï¼ˆå®¡æ‰¹ä¸­ä¹Ÿå ç”¨é¢åº¦ï¼Œé˜²æ­¢ç´¯è®¡å¼€ç¥¨è¶…过合同金额)
        CrmContractDO contract = contractService.validateContract(reqVO.getContractId());
        List<CrmInvoiceDO> invoices = invoiceMapper.selectListByContractIdAndStatus(reqVO.getContractId(),
                List.of(CrmAuditStatusEnum.APPROVE.getStatus(), CrmAuditStatusEnum.PROCESS.getStatus()));
        if (reqVO.getId() != null) {
            invoices.removeIf(invoice -> ObjectUtil.equal(invoice.getId(), reqVO.getId()));
        }
        BigDecimal notInvoicePrice = contract.getTotalPrice().subtract(
                CollectionUtils.getSumValue(invoices, CrmInvoiceDO::getPrice, BigDecimal::add, BigDecimal.ZERO));
        // 2. æ ¡éªŒé‡‘额是否超过
        if (reqVO.getPrice().compareTo(notInvoicePrice) > 0) {
            throw exception(INVOICE_CREATE_FAIL_PRICE_EXCEEDS_LIMIT, notInvoicePrice);
        }
    }
    private void validateRelationDataExists(CrmInvoiceSaveReqVO reqVO) {
        if (reqVO.getOwnerUserId() != null) {
            adminUserApi.validateUser(reqVO.getOwnerUserId()); // æ ¡éªŒè´Ÿè´£äººå­˜åœ¨
@@ -173,10 +155,7 @@
        CrmInvoiceDO oldInvoice = validateInvoiceExists(updateReqVO.getId());
        updateReqVO.setOwnerUserId(oldInvoice.getOwnerUserId()).setCustomerId(oldInvoice.getCustomerId())
                .setContractId(oldInvoice.getContractId()); // è®¾ç½®å·²å­˜åœ¨çš„值
        // 1.2 æ ¡éªŒå¯å¼€ç¥¨é‡‘额超过上限
        validateInvoicePriceExceedsLimit(updateReqVO);
        // 1.3 åªæœ‰è‰ç¨¿ã€å®¡æ ¸ä¸é€šè¿‡ï¼Œå¯ä»¥ç¼–辑;
        // 1.2 åªæœ‰è‰ç¨¿ã€å®¡æ ¸ä¸é€šè¿‡ï¼Œå¯ä»¥ç¼–辑;
        if (!ObjectUtils.equalsAny(oldInvoice.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(),
                CrmAuditStatusEnum.REJECT.getStatus())) {
            throw exception(INVOICE_UPDATE_FAIL_EDITING_PROHIBITED);
@@ -232,11 +211,7 @@
            throw exception(INVOICE_SUBMIT_FAIL_NOT_EDITABLE);
        }
        // 2. é‡æ–°æ ¡éªŒå¯å¼€ç¥¨é‡‘额,防止累计开票超过合同金额(草稿期后可能有其他单进入审批中)
        validateInvoicePriceExceedsLimit(new CrmInvoiceSaveReqVO()
                .setId(invoice.getId()).setContractId(invoice.getContractId()).setPrice(invoice.getPrice()));
        // 3. æ ¡éªŒå®¡æ‰¹é…ç½®å·²å¯ç”¨ä¸”配置了有效审批人(未配置时抛出带明确提示的业务异常)
        // 2. æ ¡éªŒå®¡æ‰¹é…ç½®å·²å¯ç”¨ä¸”配置了有效审批人(未配置时抛出带明确提示的业务异常)
        approvalConfigApi.validateApprovalEnabledAndGetApprovers(INVOICE_APPROVE_BIZ_TYPE);
        // 4. çŠ¶æ€ç½®ä¸ºå®¡æ‰¹ä¸­ï¼Œå¹¶æ¸…ç©ºä¸Šä¸€è½®å®¡æ ¸ç»“æžœï¼Œé¿å…æ®‹ç•™ã€Œå®¡æ ¸ä¸é€šè¿‡ã€çš„åŽŸå› 
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionService.java
@@ -44,6 +44,28 @@
    void createPermissionBatch(@Valid List<CrmPermissionCreateReqBO> createReqBOs);
    /**
     * åŒæ­¥å®¢æˆ·è´Ÿè´£äººçš„子业务【读写】权限
     *
     * å®¢æˆ·è´Ÿè´£äººå˜æ›´ï¼ˆè½¬ç§»ã€é¢†å–公海客户)后,自动获得该客户下联系人、商机、合同的读写权限
     *
     * @param customerId  å®¢æˆ·ç¼–号
     * @param ownerUserId å®¢æˆ·è´Ÿè´£äººç¼–号
     */
    void syncCustomerOwnerPermission(Long customerId, Long ownerUserId);
    /**
     * æ–°å¢žå­ä¸šåŠ¡æ•°æ®æ—¶ï¼Œä¸ºå®¢æˆ·è´Ÿè´£äººè¡¥å……ã€è¯»å†™ã€‘æƒé™
     *
     * å®¢æˆ·è´Ÿè´£äººä¸Žè¯¥æ•°æ®è´Ÿè´£äººä¸æ˜¯åŒä¸€äººæ—¶ï¼Œè‡ªåŠ¨ä¸ºå®¢æˆ·è´Ÿè´£äººåˆ›å»ºè¯»å†™æƒé™
     *
     * @param bizType     æ•°æ®ç±»åž‹ï¼Œå…³è” {@link CrmBizTypeEnum}
     * @param bizId       æ•°æ®ç¼–号
     * @param customerId  å®¢æˆ·ç¼–号
     * @param ownerUserId æ•°æ®è´Ÿè´£äººç¼–号
     */
    void createPermissionForCustomerOwner(Integer bizType, Long bizId, Long customerId, Long ownerUserId);
    /**
     * æ›´æ–°æ•°æ®æƒé™
     *
     * @param updateReqVO æ›´æ–°ä¿¡æ¯
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
@@ -9,7 +9,12 @@
import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.contact.CrmContactDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
import cn.iocoder.yudao.module.crm.dal.mysql.business.CrmBusinessMapper;
import cn.iocoder.yudao.module.crm.dal.mysql.contact.CrmContactMapper;
import cn.iocoder.yudao.module.crm.dal.mysql.contract.CrmContractMapper;
import cn.iocoder.yudao.module.crm.dal.mysql.customer.CrmCustomerMapper;
import cn.iocoder.yudao.module.crm.dal.mysql.permission.CrmPermissionMapper;
import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum;
@@ -46,6 +51,14 @@
    @Resource
    private CrmPermissionMapper permissionMapper;
    @Resource
    private CrmCustomerMapper customerMapper;
    @Resource
    private CrmContactMapper contactMapper;
    @Resource
    private CrmBusinessMapper businessMapper;
    @Resource
    private CrmContractMapper contractMapper;
    @Resource
    @Lazy // è§£å†³ä¾èµ–循环
    private CrmContactService contactService;
@@ -177,6 +190,90 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void syncCustomerOwnerPermission(Long customerId, Long ownerUserId) {
        if (customerId == null || ownerUserId == null) {
            return;
        }
        // 1. è”系人
        syncCustomerOwnerPermission(CrmBizTypeEnum.CRM_CONTACT.getType(), ownerUserId,
                convertSet(contactMapper.selectListByCustomerId(customerId), CrmContactDO::getId));
        // 2. å•†æœº
        syncCustomerOwnerPermission(CrmBizTypeEnum.CRM_BUSINESS.getType(), ownerUserId,
                convertSet(businessMapper.selectListByCustomerId(customerId), CrmBusinessDO::getId));
        // 3. åˆåŒ
        syncCustomerOwnerPermission(CrmBizTypeEnum.CRM_CONTRACT.getType(), ownerUserId,
                convertSet(contractMapper.selectListByCustomerId(customerId), CrmContractDO::getId));
    }
    /**
     * åŒæ­¥æŸä¸ªä¸šåŠ¡ç±»åž‹ä¸‹ï¼Œå®¢æˆ·è´Ÿè´£äººçš„ã€è¯»å†™ã€‘æƒé™
     *
     * @param bizType     æ•°æ®ç±»åž‹
     * @param ownerUserId å®¢æˆ·è´Ÿè´£äººç¼–号
     * @param bizIds      æ•°æ®ç¼–号集合
     */
    private void syncCustomerOwnerPermission(Integer bizType, Long ownerUserId, Set<Long> bizIds) {
        if (CollUtil.isEmpty(bizIds)) {
            return;
        }
        // 1. æŸ¥è¯¢å·²æœ‰çš„æƒé™ï¼Œé¿å…é‡å¤æ’å…¥
        Map<Long, CrmPermissionDO> permissionMap = new HashMap<>();
        for (CrmPermissionDO permission : permissionMapper.selectByBizTypeAndBizIds(bizType, bizIds)) {
            if (ObjUtil.equal(permission.getUserId(), ownerUserId)) {
                permissionMap.putIfAbsent(permission.getBizId(), permission);
            }
        }
        // 2. è®¡ç®—需要新增、升级的权限
        List<CrmPermissionDO> createPermissions = new ArrayList<>();
        List<CrmPermissionDO> updatePermissions = new ArrayList<>();
        for (Long bizId : bizIds) {
            CrmPermissionDO permission = permissionMap.get(bizId);
            if (permission == null) {
                createPermissions.add(new CrmPermissionDO().setBizType(bizType).setBizId(bizId)
                        .setUserId(ownerUserId).setLevel(CrmPermissionLevelEnum.WRITE.getLevel()));
            } else if (CrmPermissionLevelEnum.isRead(permission.getLevel())) {
                // åªè¯»å‡çº§ä¸ºè¯»å†™ï¼›è´Ÿè´£äººçº§åˆ«ä¸é™çº§
                updatePermissions.add(new CrmPermissionDO().setId(permission.getId())
                        .setLevel(CrmPermissionLevelEnum.WRITE.getLevel()));
            }
        }
        // 3. æ‰¹é‡å†™å…¥
        if (CollUtil.isNotEmpty(createPermissions)) {
            permissionMapper.insertBatch(createPermissions);
        }
        if (CollUtil.isNotEmpty(updatePermissions)) {
            permissionMapper.updateBatch(updatePermissions);
        }
    }
    @Override
    public void createPermissionForCustomerOwner(Integer bizType, Long bizId, Long customerId, Long ownerUserId) {
        if (bizId == null || customerId == null) {
            return;
        }
        // 1. æŸ¥è¯¢å®¢æˆ·è´Ÿè´£äºº
        CrmCustomerDO customer = customerMapper.selectById(customerId);
        if (customer == null || customer.getOwnerUserId() == null) {
            return;
        }
        Long customerOwnerUserId = customer.getOwnerUserId();
        // 2. æ•°æ®è´Ÿè´£äººæœ¬èº«å°±æ˜¯å®¢æˆ·è´Ÿè´£äººæ—¶ï¼Œå·²ç»æ‹¥æœ‰æƒé™ï¼Œæ— éœ€é‡å¤æŽˆæƒ
        if (ObjUtil.equal(customerOwnerUserId, ownerUserId)) {
            return;
        }
        // 3. ä¸å­˜åœ¨åˆ™æ–°å¢žè¯»å†™æƒé™ï¼Œåªè¯»åˆ™å‡çº§ä¸ºè¯»å†™
        CrmPermissionDO permission = permissionMapper.selectByBizTypeAndBizIdByUserId(bizType, bizId, customerOwnerUserId);
        if (permission == null) {
            permissionMapper.insert(new CrmPermissionDO().setBizType(bizType).setBizId(bizId)
                    .setUserId(customerOwnerUserId).setLevel(CrmPermissionLevelEnum.WRITE.getLevel()));
        } else if (CrmPermissionLevelEnum.isRead(permission.getLevel())) {
            permissionMapper.updateById(new CrmPermissionDO().setId(permission.getId())
                    .setLevel(CrmPermissionLevelEnum.WRITE.getLevel()));
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updatePermission(CrmPermissionUpdateReqVO updateReqVO) {
        // 1. æ ¡éªŒå­˜åœ¨
        validatePermissionExists(updateReqVO.getIds());
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/quotation/CrmSaleQuotationServiceImpl.java
@@ -20,7 +20,6 @@
import cn.iocoder.yudao.module.crm.service.contact.CrmContactService;
import cn.iocoder.yudao.module.crm.service.contract.CrmContractService;
import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
@@ -63,8 +62,6 @@
    @Resource
    private CrmCustomerService customerService;
    @Resource
    private CrmCustomerApi customerApi;
    @Resource
    private CrmBusinessService businessService;
    @Resource
@@ -241,9 +238,9 @@
    @Override
    public PageResult<CrmSaleQuotationDO> getSaleQuotationPage(CrmSaleQuotationPageReqVO pageReqVO, Long userId) {
        // èŽ·å–ç”¨æˆ·æœ‰æƒé™çš„å®¢æˆ· ID åˆ—表
        List<Long> permittedCustomerIds = customerApi.getPermittedCustomerIds();
        return saleQuotationMapper.selectPage(pageReqVO, userId, permittedCustomerIds);
        // å¯è§æ€§å®Œå…¨ç”± appendPermissionCondition æŽ§åˆ¶ï¼ˆæŠ¥ä»·å›¢é˜Ÿ âˆª å®¢æˆ·å›¢é˜Ÿï¼‰ï¼Œ
        // ä¸å†é¢å¤–按「有权限的客户」过滤,避免不在客户团队、但属于报价负责人/报价团队的用户被错误过滤
        return saleQuotationMapper.selectPage(pageReqVO, userId, null);
    }
    @Override
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/util/CrmPermissionUtils.java
@@ -27,13 +27,16 @@
public class CrmPermissionUtils {
    /**
     * æ ¡éªŒç”¨æˆ·æ˜¯å¦æ˜¯ CRM ç®¡ç†å‘˜
     * æ ¡éªŒç”¨æˆ·æ˜¯å¦æ˜¯ CRM ç®¡ç†å‘˜ï¼ˆCRM ç®¡ç†å‘˜æˆ–系统超级管理员)
     *
     * è¶…级管理员(super_admin)不受 CRM æ•°æ®æƒé™é™åˆ¶ï¼Œå¯ä»¥æŸ¥çœ‹å…¨éƒ¨æ•°æ®
     *
     * @return æ˜¯/否
     */
    public static boolean isCrmAdmin() {
        PermissionCommonApi permissionApi = SpringUtil.getBean(PermissionCommonApi.class);
        return permissionApi.hasAnyRoles(getLoginUserId(), RoleCodeEnum.CRM_ADMIN.getCode());
        return permissionApi.hasAnyRoles(getLoginUserId(),
                RoleCodeEnum.CRM_ADMIN.getCode(), RoleCodeEnum.SUPER_ADMIN.getCode());
    }
    /**
@@ -57,6 +60,10 @@
     */
    public static <T extends MPJLambdaWrapper<?>, S> void appendPermissionCondition(T query, Integer bizType, SFunction<S, ?> bizId,
                                                                                    Long userId, Integer sceneType) {
        // ç‰¹æ®Šé€»è¾‘:CRM ç®¡ç†å‘˜ / è¶…级管理员直接查询所有,不过滤数据权限
        if (isCrmAdmin()) {
            return;
        }
        MybatisPlusJoinProperties mybatisPlusJoinProperties = SpringUtil.getBean(MybatisPlusJoinProperties.class);
        final String ownerUserIdField = mybatisPlusJoinProperties.getTableAlias() + ".owner_user_id";
        // åœºæ™¯ä¸€ï¼šæˆ‘负责的数据
@@ -65,12 +72,10 @@
        }
        // åœºæ™¯äºŒï¼šæˆ‘参与的数据(我有读或写权限,并且不是负责人)
        if (CrmSceneTypeEnum.isInvolved(sceneType)) {
            if (CrmPermissionUtils.isCrmAdmin()) { // ç‰¹æ®Šé€»è¾‘:如果是超管,直接查询所有,不过滤数据权限
                return;
            }
            // å•†æœºç‰¹æ®Šå¤„理:同时关联商机团队和客户团队的权限
            if (CrmBizTypeEnum.CRM_BUSINESS.getType().equals(bizType)) {
                appendBusinessPermissionCondition(query, bizId, userId, ownerUserIdField);
            // éœ€è¦ç»§æ‰¿å®¢æˆ·å›¢é˜Ÿçš„æ¨¡å—:数据自身团队 âˆª å®¢æˆ·è´Ÿè´£äºº/客户团队
            String tableName = getInheritCustomerTeamTable(bizType);
            if (tableName != null) {
                appendCustomerInheritedPermissionCondition(query, bizType, tableName, userId, ownerUserIdField);
            } else {
                query.innerJoin(CrmPermissionDO.class, on -> on.eq(CrmPermissionDO::getBizType, bizType)
                        .eq(CrmPermissionDO::getBizId, bizId)
@@ -100,23 +105,60 @@
    }
    /**
     * å•†æœºç‰¹æ®Šæƒé™æ¡ä»¶ï¼šåŒæ—¶å…³è”商机团队和客户团队的权限
     * å•†æœºç»§æ‰¿å®¢æˆ·å›¢é˜Ÿçš„æƒé™ï¼Œå³å®¢æˆ·å›¢é˜Ÿæˆå‘˜ä¹Ÿèƒ½çœ‹åˆ°è¯¥å®¢æˆ·ä¸‹çš„商机
     * èŽ·å¾—ã€Œç»§æ‰¿å®¢æˆ·å›¢é˜Ÿã€çš„ä¸šåŠ¡è¡¨åï¼›ä¸éœ€è¦ç»§æ‰¿çš„è¿”å›ž null
     *
     * ç›®å‰ç»Ÿä¸€ä¸ºï¼šè”系人、商机、合同、销售报价、回款、回款计划、开票
     */
    private static <T extends MPJLambdaWrapper<?>, S> void appendBusinessPermissionCondition(T query, SFunction<S, ?> bizId,
                                                                                              Long userId, String ownerUserIdField) {
        // ä½¿ç”¨å­æŸ¥è¯¢æ–¹å¼ï¼šå•†æœºå›¢é˜Ÿæˆå‘˜ OR å®¢æˆ·å›¢é˜Ÿæˆå‘˜
        // å­æŸ¥è¯¢1:商机团队成员权限
    private static String getInheritCustomerTeamTable(Integer bizType) {
        if (CrmBizTypeEnum.CRM_CONTACT.getType().equals(bizType)) {
            return "crm_contact";
        }
        if (CrmBizTypeEnum.CRM_BUSINESS.getType().equals(bizType)) {
            return "crm_business";
        }
        if (CrmBizTypeEnum.CRM_CONTRACT.getType().equals(bizType)) {
            return "crm_contract";
        }
        if (CrmBizTypeEnum.CRM_SALE_QUOTATION.getType().equals(bizType)) {
            return "crm_sale_quotation";
        }
        if (CrmBizTypeEnum.CRM_RECEIVABLE.getType().equals(bizType)) {
            return "crm_receivable";
        }
        if (CrmBizTypeEnum.CRM_RECEIVABLE_PLAN.getType().equals(bizType)) {
            return "crm_receivable_plan";
        }
        if (CrmBizTypeEnum.CRM_INVOICE.getType().equals(bizType)) {
            return "crm_invoice";
        }
        return null;
    }
    /**
     * ç»§æ‰¿å®¢æˆ·å›¢é˜Ÿçš„æ•°æ®æƒé™ï¼šæ•°æ®è‡ªèº«å›¢é˜Ÿ âˆª å®¢æˆ·è´Ÿè´£äºº/客户团队
     *
     * @param query            è¿žè¡¨æŸ¥è¯¢å¯¹è±¡
     * @param bizType          æ•°æ®ç±»åž‹
     * @param tableName        ä¸šåŠ¡è¡¨å
     * @param userId           ç”¨æˆ·ç¼–号
     * @param ownerUserIdField è´Ÿè´£äººå­—段
     */
    private static <T extends MPJLambdaWrapper<?>> void appendCustomerInheritedPermissionCondition(
            T query, Integer bizType, String tableName, Long userId, String ownerUserIdField) {
        // å­æŸ¥è¯¢1:数据自身的团队成员权限
        String subQuery1 = "(SELECT p1.biz_id FROM crm_permission p1 " +
                "WHERE p1.biz_type = " + CrmBizTypeEnum.CRM_BUSINESS.getType() + " " +
                "AND p1.level IN (" + CrmPermissionLevelEnum.READ.getLevel() + "," + CrmPermissionLevelEnum.WRITE.getLevel() + ") " +
                "AND p1.user_id = " + userId + ")";
        // å­æŸ¥è¯¢2:客户团队成员权限(通过商机的 customer_id å…³è”)
        String subQuery2 = "(SELECT b.id FROM crm_business b " +
                "INNER JOIN crm_permission p2 ON p2.biz_type = " + CrmBizTypeEnum.CRM_CUSTOMER.getType() + " " +
                "AND p2.biz_id = b.customer_id " +
                "AND p2.level IN (" + CrmPermissionLevelEnum.READ.getLevel() + "," + CrmPermissionLevelEnum.WRITE.getLevel() + ") " +
                "AND p2.user_id = " + userId + ")";
                "WHERE p1.biz_type = " + bizType + " " +
                "AND p1.level IN (" + CrmPermissionLevelEnum.OWNER.getLevel() + "," +
                CrmPermissionLevelEnum.READ.getLevel() + "," + CrmPermissionLevelEnum.WRITE.getLevel() + ") " +
                "AND p1.user_id = " + userId + " AND p1.deleted = 0)";
        // å­æŸ¥è¯¢2:所属客户的负责人 + å›¢é˜Ÿæˆå‘˜
        // æ³¨æ„ï¼šä»¥ crm_customer.owner_user_id ä¸ºå‡†ï¼Œå…¼å®¹æƒé™è¡¨ä¸­è´Ÿè´£äººè®°å½•缺失/不一致的历史数据
        String subQuery2 = "(SELECT x.id FROM " + tableName + " x " +
                "INNER JOIN crm_customer cu ON cu.id = x.customer_id AND cu.deleted = 0 " +
                "WHERE x.deleted = 0 AND (cu.owner_user_id = " + userId +
                " OR EXISTS (SELECT 1 FROM crm_permission p2 " +
                "WHERE p2.biz_type = " + CrmBizTypeEnum.CRM_CUSTOMER.getType() + " " +
                "AND p2.biz_id = x.customer_id AND p2.user_id = " + userId + " AND p2.deleted = 0)))";
        // ä½¿ç”¨ OR è¿žæŽ¥ä¸¤ä¸ªå­æŸ¥è¯¢ï¼Œå¹¶æŽ’除自己是负责人的情况
        query.apply("t.id IN (" + subQuery1 + " UNION " + subQuery2 + ")")
             .ne(ownerUserIdField, userId);