feat(crm): 增加商机客户团队权限校验功能
- 在 CrmPermissionAspect 中新增 CrmBusinessDO 和 CrmBusinessMapper 依赖
- 修改 validatePermission 方法增加 bizId 参数用于权限校验
- 修复注释中的错别字"自的身"为"自身的"
- 新增商机特殊处理逻辑:检查客户团队权限
- 在 validateBusinessCustomerPermission 方法中实现商机客户团队权限验证
- 在 CrmPermissionUtils 中增加商机权限条件的特殊处理
- 通过子查询方式关联商机团队和客户团队权限
feat(mdm): 增加物料批量删除功能
- 在 MdmItemController 中新增 deleteItemList 接口支持批量删除
- 在 MdmItemService 中定义 deleteItemList 方法接口
- 在 MdmItemServiceImpl 中实现批量删除逻辑,逐个调用单条删除保持校验
feat(mes): 优化生产报工完成流程
- 调整报工完成步骤顺序,先更新报工状态为已完成
- 非关键工序也需检查 MPS 完成状态
- 在更新工单生产数量后同步更新 MPS 已生产数量
- 新增 checkAndFinishMps 方法检查并自动完成 MPS
- 增加日志记录 MPS 完成检查过程
| | |
| | | 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.permission.CrmPermissionDO; |
| | | import cn.iocoder.yudao.module.crm.dal.mysql.business.CrmBusinessMapper; |
| | | 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; |
| | |
| | | @Resource |
| | | private AdminUserApi adminUserApi; |
| | | |
| | | @Resource |
| | | private CrmBusinessMapper businessMapper; |
| | | |
| | | @Before("@annotation(crmPermission)") |
| | | public void doBefore(JoinPoint joinPoint, CrmPermission crmPermission) { |
| | | // 1.1 获取相关属性值 |
| | |
| | | // 2. 逐个校验权限 |
| | | List<CrmPermissionDO> permissionList = crmPermissionService.getPermissionListByBiz(bizType, bizIds); |
| | | Map<Long, List<CrmPermissionDO>> multiMap = convertMultiMap(permissionList, CrmPermissionDO::getBizId); |
| | | bizIds.forEach(bizId -> validatePermission(bizType, multiMap.get(bizId), permissionLevel)); |
| | | bizIds.forEach(bizId -> validatePermission(bizType, bizId, multiMap.get(bizId), permissionLevel)); |
| | | } |
| | | |
| | | private void validatePermission(Integer bizType, List<CrmPermissionDO> bizPermissions, Integer permissionLevel) { |
| | | private void validatePermission(Integer bizType, Long bizId, List<CrmPermissionDO> bizPermissions, Integer permissionLevel) { |
| | | // 1. 如果是超级管理员则直接通过 |
| | | if (CrmPermissionUtils.isCrmAdmin()) { |
| | | return; |
| | |
| | | } |
| | | } |
| | | |
| | | // 2. 只考虑自的身权限 |
| | | // 2. 只考虑自身的权限 |
| | | Long userId = getUserId(); |
| | | CrmPermissionDO userPermission = CollUtil.findOne(bizPermissions, permission -> ObjUtil.equal(permission.getUserId(), userId)); |
| | | if (userPermission != null) { |
| | |
| | | } |
| | | } |
| | | |
| | | // 3. 考虑下级的权限 |
| | | // 3. 商机特殊处理:检查客户团队权限 |
| | | if (CrmBizTypeEnum.CRM_BUSINESS.getType().equals(bizType)) { |
| | | if (validateBusinessCustomerPermission(bizId, userId, permissionLevel)) { |
| | | return; |
| | | } |
| | | } |
| | | |
| | | // 4. 考虑下级的权限 |
| | | List<AdminUserRespDTO> subordinateUserIds = adminUserApi.getUserListBySubordinate(userId); |
| | | for (Long subordinateUserId : convertSet(subordinateUserIds, AdminUserRespDTO::getId)) { |
| | | CrmPermissionDO subordinatePermission = CollUtil.findOne(bizPermissions, |
| | |
| | | if (subordinatePermission != null && isUserPermissionValid(subordinatePermission, permissionLevel)) { |
| | | return; |
| | | } |
| | | // 商机特殊处理:检查客户团队权限(下属的) |
| | | if (CrmBizTypeEnum.CRM_BUSINESS.getType().equals(bizType)) { |
| | | if (validateBusinessCustomerPermission(bizId, subordinateUserId, permissionLevel)) { |
| | | return; |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 4. 没有权限,抛出异常 |
| | | // 5. 没有权限,抛出异常 |
| | | log.info("[doBefore][userId({}) 要求权限({}) 实际权限({}) 数据校验错误]", // 打个 info 日志,方便后续排查问题、审计 |
| | | userId, permissionLevel, toJsonString(userPermission)); |
| | | throw exception(CRM_PERMISSION_DENIED, CrmBizTypeEnum.getNameByType(bizType)); |
| | | } |
| | | |
| | | /** |
| | | * 商机特殊权限校验:检查用户是否有该商机所属客户的团队权限 |
| | | * |
| | | * @param businessId 商机编号 |
| | | * @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) { |
| | | 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)) { |
| | | return true; |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * 校验用户权限是否有效 |
| | | * |
| | | * @param userPermission 用户拥有的权限 |
| | |
| | | if (CrmPermissionUtils.isCrmAdmin()) { // 特殊逻辑:如果是超管,直接查询所有,不过滤数据权限 |
| | | return; |
| | | } |
| | | query.innerJoin(CrmPermissionDO.class, on -> on.eq(CrmPermissionDO::getBizType, bizType) |
| | | .eq(CrmPermissionDO::getBizId, bizId) |
| | | .in(CrmPermissionDO::getLevel, CrmPermissionLevelEnum.READ.getLevel(), CrmPermissionLevelEnum.WRITE.getLevel()) |
| | | .eq(CrmPermissionDO::getUserId,userId)); |
| | | query.ne(ownerUserIdField, userId); |
| | | // 商机特殊处理:同时关联商机团队和客户团队的权限 |
| | | if (CrmBizTypeEnum.CRM_BUSINESS.getType().equals(bizType)) { |
| | | appendBusinessPermissionCondition(query, bizId, userId, ownerUserIdField); |
| | | } else { |
| | | query.innerJoin(CrmPermissionDO.class, on -> on.eq(CrmPermissionDO::getBizType, bizType) |
| | | .eq(CrmPermissionDO::getBizId, bizId) |
| | | .in(CrmPermissionDO::getLevel, CrmPermissionLevelEnum.READ.getLevel(), CrmPermissionLevelEnum.WRITE.getLevel()) |
| | | .eq(CrmPermissionDO::getUserId, userId)); |
| | | query.ne(ownerUserIdField, userId); |
| | | } |
| | | } |
| | | // 场景三:下属负责的数据(下属是负责人) |
| | | if (CrmSceneTypeEnum.isSubordinate(sceneType)) { |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 商机特殊权限条件:同时关联商机团队和客户团队的权限 |
| | | * 商机继承客户团队的权限,即客户团队成员也能看到该客户下的商机 |
| | | */ |
| | | private static <T extends MPJLambdaWrapper<?>, S> void appendBusinessPermissionCondition(T query, SFunction<S, ?> bizId, |
| | | Long userId, String ownerUserIdField) { |
| | | // 使用子查询方式:商机团队成员 OR 客户团队成员 |
| | | // 子查询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 + ")"; |
| | | // 使用 OR 连接两个子查询,并排除自己是负责人的情况 |
| | | query.apply("t.id IN (" + subQuery1 + " UNION " + subQuery2 + ")") |
| | | .ne(ownerUserIdField, userId); |
| | | } |
| | | |
| | | } |
| | |
| | | return success(true); |
| | | } |
| | | |
| | | @DeleteMapping("/delete-list") |
| | | @Operation(summary = "批量删除物料") |
| | | @Parameter(name = "ids", description = "编号列表", required = true, example = "1,2,3") |
| | | @PreAuthorize("@ss.hasPermission('mdm:item:delete')") |
| | | public CommonResult<Boolean> deleteItemList(@RequestParam("ids") List<Long> ids) { |
| | | itemService.deleteItemList(ids); |
| | | return success(true); |
| | | } |
| | | |
| | | @GetMapping("/get") |
| | | @Operation(summary = "获得物料") |
| | | @Parameter(name = "id", description = "编号", required = true, example = "1") |
| | |
| | | void deleteItem(Long id); |
| | | |
| | | /** |
| | | * 批量删除物料 |
| | | * |
| | | * @param ids 编号列表 |
| | | */ |
| | | void deleteItemList(Collection<Long> ids); |
| | | |
| | | /** |
| | | * 获得物料 |
| | | * |
| | | * @param id 编号 |
| | |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void deleteItemList(Collection<Long> ids) { |
| | | if (CollUtil.isEmpty(ids)) { |
| | | return; |
| | | } |
| | | // 逐个删除(复用单条删除逻辑,保持校验) |
| | | ids.forEach(this::deleteItem); |
| | | } |
| | | |
| | | @Override |
| | | public MdmItemDO validateItemExists(Long id) { |
| | | MdmItemDO item = itemMapper.selectById(id); |
| | | if (item == null) { |
| | |
| | | import cn.iocoder.yudao.module.mes.api.mps.dto.MesProMpsCreateReqDTO; |
| | | import cn.iocoder.yudao.module.mes.api.mps.dto.MesProMpsRespDTO; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.util.List; |
| | | |
| | | /** |
| | |
| | | */ |
| | | MesProMpsRespDTO getMpsByWorkOrderId(Long workOrderId); |
| | | |
| | | /** |
| | | * 更新 MPS 的已生产数量(累加) |
| | | * |
| | | * @param mpsId MPS 编号 |
| | | * @param quantity 增加的数量 |
| | | */ |
| | | void updateMpsProducedQuantity(Long mpsId, BigDecimal quantity); |
| | | |
| | | /** |
| | | * 检查并自动完成 MPS |
| | | * |
| | | * 如果 MPS 满足完成条件(已生产数量 >= 计划数量 且 所有报工已完成), |
| | | * 则自动将 MPS 状态更新为已完成 |
| | | * |
| | | * @param mpsId MPS 编号 |
| | | * @return true=已完成或本次自动完成,false=未完成 |
| | | */ |
| | | Boolean checkAndFinishMps(Long mpsId); |
| | | |
| | | } |
| | |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.validation.annotation.Validated; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.util.Collections; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | |
| | | return BeanUtils.toBean(mps, MesProMpsRespDTO.class); |
| | | } |
| | | |
| | | @Override |
| | | public void updateMpsProducedQuantity(Long mpsId, BigDecimal quantity) { |
| | | mpsService.updateProducedQuantity(mpsId, quantity); |
| | | } |
| | | |
| | | @Override |
| | | public Boolean checkAndFinishMps(Long mpsId) { |
| | | return mpsService.checkAndFinishMps(mpsId); |
| | | } |
| | | |
| | | } |
| | |
| | | } |
| | | } |
| | | |
| | | // 4. 关键工序:生成产出单,并根据是否需要检验决定入库方式 |
| | | // 5. 先更新报工状态为已完成(确保 checkAndFinishMps 检查时当前报工已标记完成) |
| | | feedbackMapper.updateById(new MesProFeedbackDO().setId(id) |
| | | .setStatus(MesProFeedbackStatusEnum.FINISHED.getStatus()) |
| | | .setUncheckQuantity(BigDecimal.ZERO)); |
| | | |
| | | // 6. 关键工序:生成产出单,并根据是否需要检验决定入库方式 |
| | | if (keyFlag) { |
| | | // 4.1 需要检验:生成产出单(质量状态=待检验),更新报工状态为待检验,等质检完成回调后再入库 |
| | | // 6.1 需要检验:生成产出单(质量状态=待检验),更新报工状态为待检验,等质检完成回调后再入库 |
| | | if (checkFlag) { |
| | | // 完成时回调见:MesQcIpqcServiceImpl#finishIpqc → splitPendingAndFinishProduce + completeFeedbackFromIpqc |
| | | productProduceService.generateProductProduce(feedback, true); |
| | |
| | | .setStatus(MesProFeedbackStatusEnum.UNCHECK.getStatus())); |
| | | return false; |
| | | } |
| | | // 4.2 无需检验:生成产出单(按合格/不合格拆行),直接完成入库,并更新任务/工单的已生产数量 |
| | | // 6.2 无需检验:生成产出单(按合格/不合格拆行),直接完成入库,并更新任务/工单的已生产数量 |
| | | MesWmProductProduceDO produce = productProduceService.generateProductProduce(feedback, false); |
| | | productProduceService.finishProductProduce(produce.getId()); |
| | | updateTaskAndWorkOrderByFeedback(feedback); |
| | | } else { |
| | | // 4.3 非关键工序:只更新任务的已生产数量(工单不更新,工单只看关键工序产出) |
| | | // 6.3 非关键工序:只更新任务的已生产数量(工单不更新,工单只看关键工序产出) |
| | | taskService.updateProducedQuantity(feedback.getTaskId(), |
| | | feedback.getFeedbackQuantity(), |
| | | ObjectUtil.defaultIfNull(feedback.getQualifiedQuantity(), BigDecimal.ZERO), |
| | | ObjectUtil.defaultIfNull(feedback.getUnqualifiedQuantity(), BigDecimal.ZERO)); |
| | | // 6.4 非关键工序也需要检查 MPS 完成状态(所有工序报工完成才算完成) |
| | | checkAndPublishProductionFinished(feedback.getWorkOrderId()); |
| | | } |
| | | |
| | | // 5. 非关键工序 / 关键非质检工序:直接完成(清零 uncheckQuantity 防止 !key+check 留脏数据) |
| | | feedbackMapper.updateById(new MesProFeedbackDO().setId(id) |
| | | .setStatus(MesProFeedbackStatusEnum.FINISHED.getStatus()) |
| | | .setUncheckQuantity(BigDecimal.ZERO)); |
| | | return true; // 已完成 |
| | | } |
| | | |
| | |
| | | workOrderService.updateProducedQuantity(feedback.getWorkOrderId(), |
| | | feedback.getFeedbackQuantity()); |
| | | |
| | | // 4. 检查并发布生产完成事件 |
| | | // 4. 更新 MPS 的已生产数量(如果是销售订单来源) |
| | | MesProMpsRespDTO mps = mpsApi.getMpsByWorkOrderId(feedback.getWorkOrderId()); |
| | | if (mps != null) { |
| | | mpsApi.updateMpsProducedQuantity(mps.getId(), feedback.getFeedbackQuantity()); |
| | | } |
| | | |
| | | // 5. 检查并发布生产完成事件 |
| | | checkAndPublishProductionFinished(feedback.getWorkOrderId()); |
| | | } |
| | | |
| | |
| | | return; // 非销售订单来源的工单,跳过 |
| | | } |
| | | |
| | | // 2. 检查销售订单所有生产是否完成 |
| | | // 2. 检查并自动完成 MPS(如果满足条件) |
| | | mpsApi.checkAndFinishMps(mps.getId()); |
| | | |
| | | // 3. 检查销售订单所有生产是否完成 |
| | | if (mpsApi.checkSaleOrderProductionFinished(mps.getSaleOrderId())) { |
| | | eventPublisher.publishEvent(new ProductionFinishedEvent(mps.getSaleOrderId(), java.time.LocalDateTime.now())); |
| | | } |
| | |
| | | */ |
| | | MesProMpsDO getMpsByWorkOrderId(Long workOrderId); |
| | | |
| | | /** |
| | | * 检查并自动完成 MPS |
| | | * |
| | | * 如果 MPS 满足完成条件(已生产数量 >= 计划数量 且 所有报工已完成), |
| | | * 则自动将 MPS 状态更新为已完成 |
| | | * |
| | | * @param mpsId MPS 编号 |
| | | * @return true=已完成或本次自动完成,false=未完成 |
| | | */ |
| | | Boolean checkAndFinishMps(Long mpsId); |
| | | |
| | | } |
| | |
| | | return mpsMapper.selectOne(MesProMpsDO::getWorkOrderId, workOrderId); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public Boolean checkAndFinishMps(Long mpsId) { |
| | | if (mpsId == null) { |
| | | log.warn("[checkAndFinishMps] mpsId is null"); |
| | | return false; |
| | | } |
| | | MesProMpsDO mps = mpsMapper.selectById(mpsId); |
| | | if (mps == null) { |
| | | log.warn("[checkAndFinishMps] MPS not found, mpsId={}", mpsId); |
| | | return false; |
| | | } |
| | | // 已完成或已取消,直接返回 true |
| | | if (MesProMpsStatusEnum.FINISHED.getStatus().equals(mps.getStatus()) |
| | | || MesProMpsStatusEnum.CANCELED.getStatus().equals(mps.getStatus())) { |
| | | log.info("[checkAndFinishMps] MPS {} already finished or canceled, status={}", mpsId, mps.getStatus()); |
| | | return true; |
| | | } |
| | | // 检查是否满足完成条件 |
| | | boolean finished = isMpsFinished(mps); |
| | | log.info("[checkAndFinishMps] MPS {} check result: {}, quantity={}, produced={}, workOrderId={}", |
| | | mpsId, finished, mps.getQuantity(), mps.getQuantityProduced(), mps.getWorkOrderId()); |
| | | if (finished) { |
| | | // 自动完成 MPS |
| | | mpsMapper.updateById(new MesProMpsDO() |
| | | .setId(mpsId) |
| | | .setStatus(MesProMpsStatusEnum.FINISHED.getStatus()) |
| | | .setFinishDate(LocalDateTime.now())); |
| | | log.info("[checkAndFinishMps] MPS {} auto finished", mpsId); |
| | | return true; |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * 判断单个 MPS 是否完成 |
| | | */ |
| | |
| | | // 1. 已生产数量 >= 计划数量 |
| | | BigDecimal quantityProduced = ObjUtil.defaultIfNull(mps.getQuantityProduced(), BigDecimal.ZERO); |
| | | if (quantityProduced.compareTo(mps.getQuantity()) < 0) { |
| | | log.debug("[isMpsFinished] MPS {} quantity not enough: produced={}, required={}", |
| | | mps.getId(), quantityProduced, mps.getQuantity()); |
| | | return false; |
| | | } |
| | | // 2. 检查工单下所有报工是否完成(确保质检完成) |
| | |
| | | new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<MesProFeedbackDO>() |
| | | .eq(MesProFeedbackDO::getWorkOrderId, mps.getWorkOrderId()) |
| | | .ne(MesProFeedbackDO::getStatus, MesProFeedbackStatusEnum.FINISHED.getStatus())); |
| | | log.debug("[isMpsFinished] MPS {} unfinished feedback count: {}", mps.getId(), unfinishedCount); |
| | | if (unfinishedCount > 0) { |
| | | return false; |
| | | } |