liu
4 天以前 8ad99e5f72085d14fd56e8faa7aa36701823c8b5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
package cn.iocoder.yudao.module.crm.framework.permission.core.aop;
 
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
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;
import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
import cn.iocoder.yudao.module.crm.util.CrmPermissionUtils;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
 
import java.util.*;
 
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CRM_PERMISSION_DENIED;
 
/**
 * Crm 数据权限校验 AOP 切面
 *
 * @author HUIHUI
 */
@Component
@Aspect
@Slf4j
public class CrmPermissionAspect {
 
    @Resource
    private CrmPermissionService crmPermissionService;
 
    @Resource
    private AdminUserApi adminUserApi;
 
    @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) {
        // 1.1 获取相关属性值
        Map<String, Object> expressionValues = parseExpressions(joinPoint, crmPermission);
        Integer bizType = StrUtil.isEmpty(crmPermission.bizTypeValue()) ?
                crmPermission.bizType()[0].getType() : (Integer) expressionValues.get(crmPermission.bizTypeValue()); // 模块类型
        // 1.2 处理兼容多个 bizId 的情况
        Object object = expressionValues.get(crmPermission.bizId()); // 模块数据编号
        if (object == null) {
            throw exception(BAD_REQUEST);
        }
        Set<Long> bizIds = new HashSet<>();
        if (object instanceof Collection<?>) {
            bizIds.addAll(convertSet((Collection<?>) object, item -> Long.parseLong(item.toString())));
        } else {
            bizIds.add(Long.parseLong(object.toString()));
        }
        Integer permissionLevel = crmPermission.level().getLevel(); // 需要的权限级别
 
        // 2. 逐个校验权限
        List<CrmPermissionDO> permissionList = crmPermissionService.getPermissionListByBiz(bizType, bizIds);
        Map<Long, List<CrmPermissionDO>> multiMap = convertMultiMap(permissionList, CrmPermissionDO::getBizId);
        bizIds.forEach(bizId -> validatePermission(bizType, bizId, multiMap.get(bizId), permissionLevel));
    }
 
    private void validatePermission(Integer bizType, Long bizId, List<CrmPermissionDO> bizPermissions, Integer permissionLevel) {
        // 1. 如果是超级管理员则直接通过
        if (CrmPermissionUtils.isCrmAdmin()) {
            return;
        }
        // 2. 数据本身的负责人:直接通过(兼容权限记录缺失的历史数据)
        Long userId = getUserId();
        if (isRecordOwner(bizType, bizId, userId)) {
            return;
        }
        // 3. 联系人/商机/合同/报价/回款/回款计划/开票:继承所属客户的权限
        //    放在「无数据权限」判断之前,避免权限记录缺失的历史数据导致负责人无法写操作
        if (validateCustomerPermission(bizType, bizId, userId, permissionLevel)) {
            return;
        }
 
        // 3. 特殊:没有数据权限的情况,针对 READ 的特殊处理
        if (CollUtil.isEmpty(bizPermissions)) {
            // 3.1 真正的公海数据(父客户也没有团队),大家有 READ 权限;
            //     如果父客户有团队,则走上面继承逻辑,这里不能放行,避免越权读取
            if (CrmPermissionLevelEnum.isRead(permissionLevel) && !hasParentCustomerTeam(bizType, bizId)) {
                return;
            }
            // 没有数据权限的情况下超出了读权限直接报错,避免后面校验空指针
            throw exception(CRM_PERMISSION_DENIED, CrmBizTypeEnum.getNameByType(bizType));
        } else { // 3.2 有数据权限但是没有负责人的情况
            if (!anyMatch(bizPermissions, item -> CrmPermissionLevelEnum.isOwner(item.getLevel()))
                && CrmPermissionLevelEnum.isRead(permissionLevel)) {
                return;
            }
        }
 
        // 4. 只考虑自身的权限
        CrmPermissionDO userPermission = CollUtil.findOne(bizPermissions, permission -> ObjUtil.equal(permission.getUserId(), userId));
        if (userPermission != null) {
            if (isUserPermissionValid(userPermission, permissionLevel)) {
                return;
            }
        }
 
        // 4. 考虑下级的权限
        List<AdminUserRespDTO> subordinateUserIds = adminUserApi.getUserListBySubordinate(userId);
        for (Long subordinateUserId : convertSet(subordinateUserIds, AdminUserRespDTO::getId)) {
            CrmPermissionDO subordinatePermission = CollUtil.findOne(bizPermissions,
                    permission -> ObjUtil.equal(permission.getUserId(), subordinateUserId));
            if (subordinatePermission != null && isUserPermissionValid(subordinatePermission, permissionLevel)) {
                return;
            }
            // 继承所属客户的权限(下属的)
            if (validateCustomerPermission(bizType, bizId, subordinateUserId, permissionLevel)) {
                return;
            }
        }
 
        // 5. 没有权限,抛出异常
        log.info("[doBefore][userId({}) 要求权限({}) 实际权限({}) 数据校验错误]", // 打个 info 日志,方便后续排查问题、审计
                userId, permissionLevel, toJsonString(userPermission));
        throw exception(CRM_PERMISSION_DENIED, CrmBizTypeEnum.getNameByType(bizType));
    }
 
    /**
     * 继承所属客户的权限校验:客户负责人、客户团队成员
     *
     * 适用于:联系人、商机、合同、销售报价、回款、回款计划、开票
     *
     * @param bizType         数据类型
     * @param bizId           数据编号
     * @param userId          用户编号
     * @param permissionLevel 需要的权限级别
     * @return 是否有权限
     */
    private boolean validateCustomerPermission(Integer bizType, Long bizId, Long userId, Integer permissionLevel) {
        // 1. 获取所属客户
        Long customerId = getCustomerId(bizType, bizId);
        if (customerId == null) {
            return false;
        }
        CrmCustomerDO customer = customerMapper.selectById(customerId);
        // 2. 客户负责人:直接通过(兼容权限表中负责人记录缺失/不一致的历史数据)
        if (customer != null && ObjUtil.equal(customer.getOwnerUserId(), userId)) {
            return true;
        }
        // 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)));
    }
 
    /**
     * 校验用户权限是否有效
     *
     * @param userPermission   用户拥有的权限
     * @param permissionLevel  需要的权限级别
     * @return 是否有效
     */
    @SuppressWarnings("RedundantIfStatement")
    private boolean isUserPermissionValid(CrmPermissionDO userPermission, Integer permissionLevel) {
        // 2.1 情况一:如果自己是负责人,则默认有所有权限
        if (CrmPermissionLevelEnum.isOwner(userPermission.getLevel())) {
            return true;
        }
        // 2.2 情况二:校验自己是否有读权限
        if (CrmPermissionLevelEnum.isRead(permissionLevel)) {
            if (CrmPermissionLevelEnum.isRead(userPermission.getLevel()) // 校验当前用户是否有读权限
                    || CrmPermissionLevelEnum.isWrite(userPermission.getLevel())) { // 校验当前用户是否有写权限
                return true;
            }
        }
        // 2.3 情况三:校验自己是否有写权限
        if (CrmPermissionLevelEnum.isWrite(permissionLevel)) {
            if (CrmPermissionLevelEnum.isWrite(userPermission.getLevel())) { // 校验当前用户是否有写权限
                return true;
            }
        }
        return false;
    }
 
    /**
     * 获得用户编号
     *
     * @return 用户编号
     */
    private static Long getUserId() {
        return WebFrameworkUtils.getLoginUserId();
    }
 
    private static Map<String, Object> parseExpressions(JoinPoint joinPoint, CrmPermission crmPermission) {
        // 1. 需要解析的表达式
        List<String> expressionStrings = new ArrayList<>(2);
        expressionStrings.add(crmPermission.bizId());
        if (StrUtil.isNotEmpty(crmPermission.bizTypeValue())) { // 为空则表示 bizType 有值
            expressionStrings.add(crmPermission.bizTypeValue());
        }
        // 2. 执行解析
        return SpringExpressionUtils.parseExpressions(joinPoint, expressionStrings);
    }
 
}