diff --git a/backend/src/main/java/com/designhub/controller/DepartmentController.java b/backend/src/main/java/com/designhub/controller/DepartmentController.java index d1e5cef..fd4527e 100644 --- a/backend/src/main/java/com/designhub/controller/DepartmentController.java +++ b/backend/src/main/java/com/designhub/controller/DepartmentController.java @@ -8,6 +8,7 @@ import com.designhub.dto.DepartmentUpdateRequest; import com.designhub.entity.Brand; import com.designhub.entity.Department; import com.designhub.entity.DeptBrandRelation; +import com.designhub.entity.ReceivingDeptBrandRelation; import com.designhub.entity.ScoreGrantRecord; import com.designhub.entity.TeamScorePool; import com.designhub.service.DepartmentService; @@ -163,6 +164,31 @@ public class DepartmentController { public Result> getAllBrandDeptRelations() { return Result.success(departmentService.getAllBrandDeptRelations()); } + + @Operation(summary = "设置接单部门负责的品牌") + @PostMapping("/{deptId}/receiving-brands") + public Result setReceivingResponsibleBrands(@PathVariable Long deptId, @RequestBody List brandIds) { + departmentService.setReceivingDeptResponsibleBrands(deptId, brandIds); + return Result.success(); + } + + @Operation(summary = "获取接单部门负责的品牌列表") + @GetMapping("/{deptId}/receiving-brands") + public Result> getReceivingResponsibleBrands(@PathVariable Long deptId) { + return Result.success(departmentService.getReceivingDeptResponsibleBrands(deptId)); + } + + @Operation(summary = "获取接单部门负责的品牌ID列表") + @GetMapping("/{deptId}/receiving-brand-ids") + public Result> getReceivingResponsibleBrandIds(@PathVariable Long deptId) { + return Result.success(departmentService.getReceivingDeptResponsibleBrandIds(deptId)); + } + + @Operation(summary = "获取所有接单部门-品牌关联关系") + @GetMapping("/receiving-brand-relations") + public Result> getAllReceivingBrandDeptRelations() { + return Result.success(departmentService.getAllReceivingBrandDeptRelations()); + } @Operation(summary = "根据品牌ID获取负责的需求部门") @GetMapping("/by-brand/{brandId}") @@ -193,4 +219,3 @@ public class DepartmentController { return Result.success(); } } - diff --git a/backend/src/main/java/com/designhub/controller/UserController.java b/backend/src/main/java/com/designhub/controller/UserController.java index a96fcfd..fb39c23 100644 --- a/backend/src/main/java/com/designhub/controller/UserController.java +++ b/backend/src/main/java/com/designhub/controller/UserController.java @@ -150,12 +150,13 @@ public class UserController { public Result>> getDesignersWithWorkload( @RequestParam(required = false) Long deptId, @RequestParam(required = false) Long taskTypeId, + @RequestParam(required = false) Long brandId, @RequestHeader("Authorization") String authHeader ) { // 如果传入了 taskTypeId,则根据任务类型配置的接单部门来获取设计师 if (taskTypeId != null) { - log.info("根据任务类型获取设计师列表: taskTypeId={}", taskTypeId); - return Result.success(userService.getDesignersByTaskType(taskTypeId)); + log.info("根据任务类型获取设计师列表: taskTypeId={}, brandId={}", taskTypeId, brandId); + return Result.success(userService.getDesignersByTaskType(taskTypeId, brandId)); } // 如果没有传入deptId,则默认为当前用户的部门 diff --git a/backend/src/main/java/com/designhub/entity/ReceivingDeptBrandRelation.java b/backend/src/main/java/com/designhub/entity/ReceivingDeptBrandRelation.java new file mode 100644 index 0000000..3bef176 --- /dev/null +++ b/backend/src/main/java/com/designhub/entity/ReceivingDeptBrandRelation.java @@ -0,0 +1,60 @@ +package com.designhub.entity; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 接单部门品牌负责关系实体类 + * 用于关联接单部门与其负责的品牌过滤范围 + */ +@TableName("receiving_dept_brand_relation") +public class ReceivingDeptBrandRelation implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 接单部门ID + */ + private Long deptId; + + /** + * 品牌ID + */ + private Long brandId; + + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + @TableField(exist = false) + private String deptName; + + @TableField(exist = false) + private String brandName; + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public Long getDeptId() { return deptId; } + public void setDeptId(Long deptId) { this.deptId = deptId; } + + public Long getBrandId() { return brandId; } + public void setBrandId(Long brandId) { this.brandId = brandId; } + + public LocalDateTime getCreateTime() { return createTime; } + public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; } + + public String getDeptName() { return deptName; } + public void setDeptName(String deptName) { this.deptName = deptName; } + + public String getBrandName() { return brandName; } + public void setBrandName(String brandName) { this.brandName = brandName; } +} diff --git a/backend/src/main/java/com/designhub/mapper/ReceivingDeptBrandRelationMapper.java b/backend/src/main/java/com/designhub/mapper/ReceivingDeptBrandRelationMapper.java new file mode 100644 index 0000000..a945aeb --- /dev/null +++ b/backend/src/main/java/com/designhub/mapper/ReceivingDeptBrandRelationMapper.java @@ -0,0 +1,41 @@ +package com.designhub.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.designhub.entity.ReceivingDeptBrandRelation; +import org.apache.ibatis.annotations.Delete; +import org.apache.ibatis.annotations.Insert; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +/** + * 接单部门品牌关系Mapper + */ +@Mapper +public interface ReceivingDeptBrandRelationMapper extends BaseMapper { + + @Select("SELECT dept_id FROM receiving_dept_brand_relation WHERE brand_id = #{brandId}") + List getDeptIdsByBrandId(@Param("brandId") Long brandId); + + @Select("SELECT brand_id FROM receiving_dept_brand_relation WHERE dept_id = #{deptId}") + List getBrandIdsByDeptId(@Param("deptId") Long deptId); + + @Delete("DELETE FROM receiving_dept_brand_relation WHERE dept_id = #{deptId}") + int deleteByDeptId(@Param("deptId") Long deptId); + + @Insert("") + int batchInsert(@Param("deptId") Long deptId, @Param("brandIds") List brandIds); + + @Select("SELECT r.*, d.dept_name as dept_name, b.brand_name as brand_name " + + "FROM receiving_dept_brand_relation r " + + "LEFT JOIN sys_dept d ON r.dept_id = d.id " + + "LEFT JOIN brand b ON r.brand_id = b.id") + List getAllRelationsWithNames(); +} diff --git a/backend/src/main/java/com/designhub/service/DepartmentService.java b/backend/src/main/java/com/designhub/service/DepartmentService.java index 7b34959..5eef5e1 100644 --- a/backend/src/main/java/com/designhub/service/DepartmentService.java +++ b/backend/src/main/java/com/designhub/service/DepartmentService.java @@ -8,11 +8,13 @@ import com.designhub.dto.DepartmentUpdateRequest; import com.designhub.entity.Brand; import com.designhub.entity.Department; import com.designhub.entity.DeptBrandRelation; +import com.designhub.entity.ReceivingDeptBrandRelation; import com.designhub.entity.User; import com.designhub.exception.BusinessException; import com.designhub.mapper.BrandMapper; import com.designhub.mapper.DepartmentMapper; import com.designhub.mapper.DeptBrandRelationMapper; +import com.designhub.mapper.ReceivingDeptBrandRelationMapper; import com.designhub.mapper.UserMapper; import com.designhub.vo.DepartmentOverviewVO; import lombok.RequiredArgsConstructor; @@ -42,6 +44,7 @@ public class DepartmentService { private final TeamScorePoolService teamScorePoolService; private final UserMapper userMapper; private final DeptBrandRelationMapper deptBrandRelationMapper; + private final ReceivingDeptBrandRelationMapper receivingDeptBrandRelationMapper; private final BrandMapper brandMapper; /** @@ -432,6 +435,122 @@ public class DepartmentService { public List getAllBrandDeptRelations() { return deptBrandRelationMapper.getAllRelationsWithNames(); } + + /** + * 设置接单部门负责的品牌(用于创建任务时过滤接单部门) + */ + @Transactional(rollbackFor = Exception.class) + public void setReceivingDeptResponsibleBrands(Long deptId, List brandIds) { + Department department = getDepartmentById(deptId); + if (department == null) { + throw new BusinessException("部门不存在"); + } + if (!"receiving".equals(department.getDeptType())) { + throw new BusinessException("只有接单部门可以设置负责品牌"); + } + + receivingDeptBrandRelationMapper.deleteByDeptId(deptId); + if (!CollectionUtils.isEmpty(brandIds)) { + receivingDeptBrandRelationMapper.batchInsert(deptId, brandIds); + } + + log.info("设置接单部门 {} 负责品牌: {}", deptId, brandIds); + } + + public List getReceivingDeptResponsibleBrands(Long deptId) { + List brandIds = receivingDeptBrandRelationMapper.getBrandIdsByDeptId(deptId); + if (CollectionUtils.isEmpty(brandIds)) { + return Collections.emptyList(); + } + return brandMapper.selectBatchIds(brandIds); + } + + public List getReceivingDeptResponsibleBrandIds(Long deptId) { + return receivingDeptBrandRelationMapper.getBrandIdsByDeptId(deptId); + } + + public List getAllReceivingBrandDeptRelations() { + return receivingDeptBrandRelationMapper.getAllRelationsWithNames(); + } + + /** + * 获取品牌可用的接单部门(自动展开子部门) + */ + public List getReceivingDeptIdsByBrandId(Long brandId) { + if (brandId == null) { + return Collections.emptyList(); + } + List deptIds = receivingDeptBrandRelationMapper.getDeptIdsByBrandId(brandId); + if (CollectionUtils.isEmpty(deptIds)) { + return Collections.emptyList(); + } + return expandDeptIdsWithChildren(deptIds); + } + + /** + * 是否配置了品牌维度的接单部门限制 + */ + public boolean hasReceivingBrandConfig(Long brandId) { + if (brandId == null) { + return false; + } + List deptIds = receivingDeptBrandRelationMapper.getDeptIdsByBrandId(brandId); + return !CollectionUtils.isEmpty(deptIds); + } + + /** + * 计算任务类型和品牌维度共同作用下的可接单部门列表 + * - 品牌未配置接单部门时:回退现有任务类型逻辑 + * - 品牌已配置接单部门时:取任务类型与品牌配置的交集 + */ + public List filterReceivingDeptIdsByBrand(Long brandId, String allowedDeptIdsText) { + List brandDeptIds = getReceivingDeptIdsByBrandId(brandId); + if (CollectionUtils.isEmpty(brandDeptIds)) { + return StringUtils.hasText(allowedDeptIdsText) + ? expandDeptIdsWithChildren(allowedDeptIdsText) + : getAllReceivingDeptIds(); + } + + if (!StringUtils.hasText(allowedDeptIdsText)) { + return brandDeptIds; + } + + java.util.LinkedHashSet allowedDeptIds = new java.util.LinkedHashSet<>(expandDeptIdsWithChildren(allowedDeptIdsText)); + allowedDeptIds.retainAll(new java.util.LinkedHashSet<>(brandDeptIds)); + return new ArrayList<>(allowedDeptIds); + } + + public String filterReceivingDeptIdsTextByBrand(Long brandId, String allowedDeptIdsText) { + List deptIds = filterReceivingDeptIdsByBrand(brandId, allowedDeptIdsText); + if (CollectionUtils.isEmpty(deptIds)) { + return ""; + } + return deptIds.stream().map(String::valueOf).collect(Collectors.joining(",")); + } + + public boolean isReceivingDeptAllowedForBrand(Long brandId, Long deptId) { + if (brandId == null || deptId == null) { + return true; + } + List brandDeptIds = getReceivingDeptIdsByBrandId(brandId); + if (CollectionUtils.isEmpty(brandDeptIds)) { + return true; + } + return brandDeptIds.contains(deptId); + } + + private List getAllReceivingDeptIds() { + List receivingDepts = departmentMapper.selectList( + new LambdaQueryWrapper() + .eq(Department::getDeptType, "receiving") + .eq(Department::getStatus, 1) + .eq(Department::getDeleted, 0) + ); + return receivingDepts.stream() + .map(Department::getId) + .distinct() + .collect(Collectors.toList()); + } /** * 检查部门是否为一级需求部门 diff --git a/backend/src/main/java/com/designhub/service/TaskApprovalService.java b/backend/src/main/java/com/designhub/service/TaskApprovalService.java index 6df575b..076caf2 100644 --- a/backend/src/main/java/com/designhub/service/TaskApprovalService.java +++ b/backend/src/main/java/com/designhub/service/TaskApprovalService.java @@ -1016,9 +1016,9 @@ public class TaskApprovalService { } } - if (allowedDeptIds != null && !allowedDeptIds.isEmpty()) { - List expandedDeptIds = departmentService.expandDeptIdsWithChildren(allowedDeptIds); - for (Long deptId : expandedDeptIds) { + List effectiveDeptIds = departmentService.filterReceivingDeptIdsByBrand(task.getBrandId(), allowedDeptIds); + if (effectiveDeptIds != null && !effectiveDeptIds.isEmpty()) { + for (Long deptId : effectiveDeptIds) { Department dept = departmentMapper.selectById(deptId); if (dept != null && "receiving".equals(dept.getDeptType()) && Integer.valueOf(1).equals(dept.getStatus()) diff --git a/backend/src/main/java/com/designhub/service/TaskService.java b/backend/src/main/java/com/designhub/service/TaskService.java index f0ecf68..6656abc 100644 --- a/backend/src/main/java/com/designhub/service/TaskService.java +++ b/backend/src/main/java/com/designhub/service/TaskService.java @@ -443,26 +443,8 @@ public class TaskService { com.designhub.dto.PipelineTaskDTO.TaskItemDTO itemDto = dto.getItems().get(i); var demandType = configService.getDemandTypeById(itemDto.getTaskTypeId()); - // 获取当前任务类型的接单部门配置 - String currentDeptKey; - - // 🔥 优先使用用户选择的接单部门 - if (itemDto.getSelectedDeptId() != null) { - currentDeptKey = String.valueOf(itemDto.getSelectedDeptId()); - log.info("任务类型 {} 使用用户选择的接单部门: {}", demandType.getName(), currentDeptKey); - } else { - // 否则使用任务类型配置的 allowedDeptIds(支持三级分类继承二级分类的配置) - currentDeptKey = demandType.getAllowedDeptIds(); - if (currentDeptKey == null && demandType.getParentId() != null) { - // 如果当前类型没有配置,尝试获取父级的配置 - var parentType = configService.getDemandTypeById(demandType.getParentId()); - if (parentType != null) { - currentDeptKey = parentType.getAllowedDeptIds(); - log.info("三级分类 {} 继承父级 {} 的接单部门配置: {}", - demandType.getName(), parentType.getName(), currentDeptKey); - } - } - } + // 获取当前任务类型在当前品牌下的接单部门配置 + String currentDeptKey = resolveEffectiveDeptKey(demandType, itemDto.getSelectedDeptId(), dto.getBrandId()); // 🔥 构建分组键:部门 + 设计师(如果指定了设计师) String currentGroupKey; @@ -518,19 +500,8 @@ public class TaskService { int itemGroupNo = itemDto.getSortOrder() / 1000; // 获取部门键 - String deptKey; - if (itemDto.getSelectedDeptId() != null) { - deptKey = String.valueOf(itemDto.getSelectedDeptId()); - } else { - var demandType = configService.getDemandTypeById(itemDto.getTaskTypeId()); - deptKey = demandType.getAllowedDeptIds(); - if (deptKey == null && demandType.getParentId() != null) { - var parentType = configService.getDemandTypeById(demandType.getParentId()); - if (parentType != null) { - deptKey = parentType.getAllowedDeptIds(); - } - } - } + var demandType = configService.getDemandTypeById(itemDto.getTaskTypeId()); + String deptKey = resolveEffectiveDeptKey(demandType, itemDto.getSelectedDeptId(), dto.getBrandId()); if (deptKey == null) deptKey = "null"; // 记录分组到部门的映射 @@ -773,6 +744,70 @@ public class TaskService { return "THIRD-" + datetime + "-" + random; } + private String resolveAllowedDeptIds(DemandType demandType) { + if (demandType == null) { + return null; + } + + String allowedDeptIds = demandType.getAllowedDeptIds(); + if (!StringUtils.hasText(allowedDeptIds) && demandType.getParentId() != null) { + DemandType parentType = configService.getDemandTypeById(demandType.getParentId()); + if (parentType != null) { + allowedDeptIds = parentType.getAllowedDeptIds(); + log.info("三级分类 {} 继承父级 {} 的接单部门配置: {}", + demandType.getName(), parentType.getName(), allowedDeptIds); + } + } + return allowedDeptIds; + } + + private String resolveEffectiveDeptKey(DemandType demandType, Long selectedDeptId, Long brandId) { + String allowedDeptIds = resolveAllowedDeptIds(demandType); + boolean hasBrandConfig = departmentService.hasReceivingBrandConfig(brandId); + + if (selectedDeptId != null) { + Department selectedDept = departmentService.getDepartmentById(selectedDeptId); + if (selectedDept == null || !"receiving".equals(selectedDept.getDeptType())) { + throw new BusinessException(ResultCode.PARAM_ERROR, "所选接单部门不存在或不是接单部门"); + } + + if (StringUtils.hasText(allowedDeptIds) || hasBrandConfig) { + List effectiveDeptIds = departmentService.filterReceivingDeptIdsByBrand(brandId, allowedDeptIds); + if (effectiveDeptIds.isEmpty() || !effectiveDeptIds.contains(selectedDeptId)) { + throw new BusinessException(ResultCode.PARAM_ERROR, + "任务类型 [" + demandType.getName() + "] 在当前品牌下不可选择接单部门 [" + selectedDept.getDeptName() + "]"); + } + } + + return String.valueOf(selectedDeptId); + } + + if (!hasBrandConfig) { + return StringUtils.hasText(allowedDeptIds) ? allowedDeptIds : null; + } + + List effectiveDeptIds = departmentService.filterReceivingDeptIdsByBrand(brandId, allowedDeptIds); + if (effectiveDeptIds.isEmpty()) { + throw new BusinessException(ResultCode.PARAM_ERROR, + "任务类型 [" + demandType.getName() + "] 在品牌 [" + getBrandName(brandId) + "] 下没有可用的接单部门"); + } + + if (effectiveDeptIds.size() > 1) { + throw new BusinessException(ResultCode.PARAM_ERROR, + "任务类型 [" + demandType.getName() + "] 在当前品牌下存在多个可用接单部门,请明确选择接单部门"); + } + + return effectiveDeptIds.stream().map(String::valueOf).collect(Collectors.joining(",")); + } + + private String getBrandName(Long brandId) { + if (brandId == null) { + return "未选择品牌"; + } + Brand brand = brandMapper.selectById(brandId); + return brand != null ? brand.getName() : String.valueOf(brandId); + } + /** * 获取任务列表 */ @@ -2061,19 +2096,8 @@ public class TaskService { com.designhub.dto.PipelineTaskDTO.TaskItemDTO itemDto = dto.getItems().get(i); var demandType = configService.getDemandTypeById(itemDto.getTaskTypeId()); - // 获取当前任务类型的接单部门配置 - String currentDeptKey; - if (itemDto.getSelectedDeptId() != null) { - currentDeptKey = String.valueOf(itemDto.getSelectedDeptId()); - } else { - currentDeptKey = demandType.getAllowedDeptIds(); - if (currentDeptKey == null && demandType.getParentId() != null) { - var parentType = configService.getDemandTypeById(demandType.getParentId()); - if (parentType != null) { - currentDeptKey = parentType.getAllowedDeptIds(); - } - } - } + // 获取当前任务类型在当前品牌下的接单部门配置 + String currentDeptKey = resolveEffectiveDeptKey(demandType, itemDto.getSelectedDeptId(), dto.getBrandId()); // 🔥 构建分组键:部门 + 设计师(如果指定了设计师) String currentGroupKey; @@ -2119,19 +2143,8 @@ public class TaskService { com.designhub.dto.PipelineTaskDTO.TaskItemDTO itemDto = dto.getItems().get(i); int itemGroupNo = itemDto.getSortOrder() / 1000; - String deptKey; - if (itemDto.getSelectedDeptId() != null) { - deptKey = String.valueOf(itemDto.getSelectedDeptId()); - } else { - var demandType = configService.getDemandTypeById(itemDto.getTaskTypeId()); - deptKey = demandType.getAllowedDeptIds(); - if (deptKey == null && demandType.getParentId() != null) { - var parentType = configService.getDemandTypeById(demandType.getParentId()); - if (parentType != null) { - deptKey = parentType.getAllowedDeptIds(); - } - } - } + var demandType = configService.getDemandTypeById(itemDto.getTaskTypeId()); + String deptKey = resolveEffectiveDeptKey(demandType, itemDto.getSelectedDeptId(), dto.getBrandId()); if (deptKey == null) deptKey = "null"; if (!groupToDeptKey.containsKey(itemGroupNo)) { @@ -4441,26 +4454,8 @@ public class TaskService { throw new BusinessException(ResultCode.PARAM_ERROR, "任务类型不存在: " + itemDto.getTaskTypeId()); } - // 获取当前任务类型的接单部门配置 - String currentDeptKey; - - // 🔥 优先使用用户选择的接单部门 - if (itemDto.getSelectedDeptId() != null) { - currentDeptKey = String.valueOf(itemDto.getSelectedDeptId()); - log.info("任务类型 {} 使用用户选择的接单部门: {}", demandType.getName(), currentDeptKey); - } else { - // 否则使用任务类型配置的 allowedDeptIds(支持三级分类继承二级分类的配置) - currentDeptKey = demandType.getAllowedDeptIds(); - if (currentDeptKey == null && demandType.getParentId() != null) { - // 如果当前类型没有配置,尝试获取父级的配置 - var parentType = configService.getDemandTypeById(demandType.getParentId()); - if (parentType != null) { - currentDeptKey = parentType.getAllowedDeptIds(); - log.info("三级分类 {} 继承父级 {} 的接单部门配置: {}", - demandType.getName(), parentType.getName(), currentDeptKey); - } - } - } + // 获取当前任务类型在当前品牌下的接单部门配置 + String currentDeptKey = resolveEffectiveDeptKey(demandType, itemDto.getSelectedDeptId(), mainTask.getBrandId()); // 🔥 构建分组键:部门 + 设计师(如果指定了设计师) String currentGroupKey; @@ -4511,19 +4506,8 @@ public class TaskService { int itemGroupNo = itemDto.getSortOrder() / 1000; // 获取部门键 - String deptKey; - if (itemDto.getSelectedDeptId() != null) { - deptKey = String.valueOf(itemDto.getSelectedDeptId()); - } else { - var demandType = configService.getDemandTypeById(itemDto.getTaskTypeId()); - deptKey = demandType.getAllowedDeptIds(); - if (deptKey == null && demandType.getParentId() != null) { - var parentType = configService.getDemandTypeById(demandType.getParentId()); - if (parentType != null) { - deptKey = parentType.getAllowedDeptIds(); - } - } - } + var demandType = configService.getDemandTypeById(itemDto.getTaskTypeId()); + String deptKey = resolveEffectiveDeptKey(demandType, itemDto.getSelectedDeptId(), mainTask.getBrandId()); if (deptKey == null) deptKey = "null"; // 记录分组到部门的映射 diff --git a/backend/src/main/java/com/designhub/service/UserService.java b/backend/src/main/java/com/designhub/service/UserService.java index 13247fb..692aa77 100644 --- a/backend/src/main/java/com/designhub/service/UserService.java +++ b/backend/src/main/java/com/designhub/service/UserService.java @@ -1690,7 +1690,11 @@ public class UserService { * @return 设计师列表,包含每个设计师的进行中任务数量 */ public List> getDesignersByTaskType(Long taskTypeId) { - log.info("根据任务类型获取设计师列表: taskTypeId={}", taskTypeId); + return getDesignersByTaskType(taskTypeId, null); + } + + public List> getDesignersByTaskType(Long taskTypeId, Long brandId) { + log.info("根据任务类型获取设计师列表: taskTypeId={}, brandId={}", taskTypeId, brandId); if (demandTypeMapper == null) { log.warn("demandTypeMapper 未注入,返回空列表"); @@ -1744,22 +1748,14 @@ public class UserService { log.info("找到接单部门配置: taskTypeId={}, allowedDeptIds={}", currentTypeId, allowedDeptIds); // 解析接单部门ID列表 - List targetDeptIds = new ArrayList<>(); - for (String deptIdStr : allowedDeptIds.split(",")) { - String trimmed = deptIdStr.trim(); - if (!trimmed.isEmpty()) { - try { - Long deptId = Long.parseLong(trimmed); - // 递归获取该部门及其所有子部门 - targetDeptIds.addAll(getAllSubDepartmentIds(deptId)); - } catch (NumberFormatException e) { - log.warn("无效的部门ID: {}", trimmed); - } - } - } + List targetDeptIds = departmentService.filterReceivingDeptIdsByBrand(brandId, allowedDeptIds); if (targetDeptIds.isEmpty()) { - log.warn("解析后的部门列表为空"); + if (departmentService.hasReceivingBrandConfig(brandId)) { + log.warn("品牌 {} 下没有可承接任务类型 {} 的接单部门", brandId, taskTypeId); + } else { + log.warn("解析后的部门列表为空"); + } return new ArrayList<>(); } diff --git a/backend/src/main/resources/db/migration/V14__add_receiving_dept_brand_relation.sql b/backend/src/main/resources/db/migration/V14__add_receiving_dept_brand_relation.sql new file mode 100644 index 0000000..082abef --- /dev/null +++ b/backend/src/main/resources/db/migration/V14__add_receiving_dept_brand_relation.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS receiving_dept_brand_relation ( + id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + dept_id BIGINT NOT NULL COMMENT '接单部门ID', + brand_id BIGINT NOT NULL COMMENT '品牌ID', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + UNIQUE KEY uk_receiving_dept_brand (dept_id, brand_id), + KEY idx_receiving_brand_id (brand_id), + KEY idx_receiving_dept_id (dept_id) +) COMMENT='接单部门品牌过滤关系表'; diff --git a/frontend/src/api/department.ts b/frontend/src/api/department.ts index 226c0f9..d7751c9 100644 --- a/frontend/src/api/department.ts +++ b/frontend/src/api/department.ts @@ -133,6 +133,26 @@ export const getAllBrandDeptRelations = () => { return request.get('/departments/brand-relations') } +// 设置接单部门负责的品牌列表(用于任务发布时过滤接单部门) +export const setReceivingDeptResponsibleBrands = (deptId: number, brandIds: number[]) => { + return request.post(`/departments/${deptId}/receiving-brands`, brandIds) +} + +// 获取接单部门负责的品牌列表 +export const getReceivingDeptResponsibleBrands = (deptId: number) => { + return request.get(`/departments/${deptId}/receiving-brands`) +} + +// 获取接单部门负责的品牌ID列表 +export const getReceivingDeptResponsibleBrandIds = (deptId: number) => { + return request.get(`/departments/${deptId}/receiving-brand-ids`) +} + +// 获取所有接单部门-品牌关联关系 +export const getAllReceivingBrandDeptRelations = () => { + return request.get('/departments/receiving-brand-relations') +} + // 根据品牌ID获取负责的需求部门 export const getDemandDeptByBrandId = (brandId: number) => { return request.get(`/departments/by-brand/${brandId}`) diff --git a/frontend/src/api/user.ts b/frontend/src/api/user.ts index ffad892..6901d75 100644 --- a/frontend/src/api/user.ts +++ b/frontend/src/api/user.ts @@ -167,11 +167,13 @@ export interface DesignerWithWorkload { * 获取设计师列表及工作量 * @param deptId 部门ID(可选) * @param taskTypeId 任务类型ID(可选,优先根据任务类型配置的接单部门获取设计师) + * @param brandId 品牌ID(可选,用于按品牌过滤接单部门) */ -export const getDesignersWithWorkload = async (deptId?: number, taskTypeId?: number): Promise => { +export const getDesignersWithWorkload = async (deptId?: number, taskTypeId?: number, brandId?: number): Promise => { const params: any = {}; if (taskTypeId) params.taskTypeId = taskTypeId; // 优先使用 taskTypeId else if (deptId) params.deptId = deptId; + if (brandId) params.brandId = brandId; const response = await request.get('/users/designers-with-workload', { params }); return response.data || []; }; diff --git a/frontend/src/views/CreateTask.vue b/frontend/src/views/CreateTask.vue index 511044b..cf05e2f 100644 --- a/frontend/src/views/CreateTask.vue +++ b/frontend/src/views/CreateTask.vue @@ -982,6 +982,7 @@ const hierarchicalTaskTypes = ref([]) // 部门列表数据 const departmentList = ref([]) +const receivingBrandDeptRelations = ref([]) // 加载层级任务类型数据 const loadHierarchicalTaskTypes = async () => { @@ -1007,6 +1008,16 @@ const loadDepartmentList = async () => { } } +const loadReceivingBrandDeptRelations = async () => { + try { + const response = await departmentApi.getAllReceivingBrandDeptRelations() + receivingBrandDeptRelations.value = response?.data || [] + } catch (error) { + console.error('加载接单部门品牌关联失败:', error) + receivingBrandDeptRelations.value = [] + } +} + const getDepartmentById = (deptId?: number | null) => { if (!deptId) return undefined return departmentList.value.find(d => d.id === deptId) @@ -1029,6 +1040,111 @@ const getDepartmentDisplayName = ( return `${parentDept.deptName} / ${dept.deptName}` } +const getDepartmentAndChildIds = (deptId?: number | null): number[] => { + if (!deptId) return [] + + const result = new Set() + const visit = (currentDeptId: number) => { + if (result.has(currentDeptId)) return + result.add(currentDeptId) + departmentList.value + .filter(dept => dept.parentId === currentDeptId) + .forEach(child => { + if (child.id) { + visit(child.id) + } + }) + } + + visit(deptId) + return Array.from(result) +} + +const getAllReceivingDeptIds = () => { + return departmentList.value + .filter(dept => dept.deptType === 'receiving' && dept.status !== 0 && dept.id) + .map(dept => dept.id!) +} + +const parseDeptIds = (deptIdsText?: string) => { + if (!deptIdsText) return [] + return deptIdsText + .split(',') + .map(id => parseInt(id.trim())) + .filter(id => !isNaN(id)) +} + +const getBrandRestrictedReceivingDeptIds = (brandId?: number) => { + if (!brandId) return [] + + const directDeptIds = receivingBrandDeptRelations.value + .filter(relation => relation.brandId === brandId && relation.deptId) + .map(relation => relation.deptId) + + if (!directDeptIds.length) return [] + + const expandedDeptIds = new Set() + directDeptIds.forEach(deptId => { + getDepartmentAndChildIds(deptId).forEach(id => expandedDeptIds.add(id)) + }) + + return Array.from(expandedDeptIds) +} + +const getEffectiveTaskTypeDeptIds = (typeInfo: any): number[] => { + const typeDeptIds = parseDeptIds(getTypeAllowedDeptIds(typeInfo)) + const expandedTypeDeptIds = typeDeptIds.length + ? Array.from(new Set(typeDeptIds.flatMap(id => getDepartmentAndChildIds(id)))) + : [] + const brandDeptIds = getBrandRestrictedReceivingDeptIds(formData.brandId) + + if (!brandDeptIds.length) { + return expandedTypeDeptIds + } + + if (!expandedTypeDeptIds.length) { + return brandDeptIds + } + + const brandDeptIdSet = new Set(brandDeptIds) + return expandedTypeDeptIds.filter(id => brandDeptIdSet.has(id)) +} + +const hasTaskTypeDeptRestriction = (typeInfo: any) => { + return parseDeptIds(getTypeAllowedDeptIds(typeInfo)).length > 0 || getBrandRestrictedReceivingDeptIds(formData.brandId).length > 0 +} + +const syncTaskTypeDeptSelection = (item: TaskTypeItem) => { + if (!item.taskTypeId) { + item.selectedDeptId = undefined + return + } + + const typeInfo = getTaskTypeInfo(item.taskTypeId) + if (!typeInfo) { + item.selectedDeptId = undefined + return + } + + if (!hasTaskTypeDeptRestriction(typeInfo)) { + return + } + + const effectiveDeptIds = getEffectiveTaskTypeDeptIds(typeInfo) + if (!effectiveDeptIds.length) { + item.selectedDeptId = undefined + item.receiverId = undefined + return + } + + if (item.selectedDeptId && effectiveDeptIds.includes(item.selectedDeptId)) { + return + } + + item.selectedDeptId = effectiveDeptIds.length === 1 ? effectiveDeptIds[0] : undefined + item.receiverId = undefined +} + // 构建级联选择器选项(三级结构) const taskTypeCascaderOptions = computed(() => { const buildOptions = (items: any[]): CascaderOption[] => { @@ -1079,6 +1195,7 @@ const onTaskTypeChange = (value: (string | number)[] | undefined, item: TaskType item.taskTypeId = undefined item.taskTypePath = undefined } + syncTaskTypeDeptSelection(item) calculateScore() } @@ -1122,12 +1239,12 @@ const findTaskTypePath = (targetId: number, items: any[] = hierarchicalTaskTypes const loadDesignersForItem = async (item: TaskTypeItem) => { if (!item.taskTypeId) return - const cacheKey = `taskType_${item.taskTypeId}` + const cacheKey = `taskType_${item.taskTypeId}_brand_${formData.brandId || 0}` if (!designersCache[cacheKey]) { try { - // 使用 taskTypeId 调用后端 API,后端会处理接单部门配置 - const designers = await userApi.getDesignersWithWorkload(undefined, item.taskTypeId) + // 使用 taskTypeId + brandId 调用后端 API,按任务类型与品牌共同过滤可选设计师 + const designers = await userApi.getDesignersWithWorkload(undefined, item.taskTypeId, formData.brandId) designersCache[cacheKey] = designers } catch (error) { console.error('加载设计师失败:', error) @@ -1139,7 +1256,7 @@ const loadDesignersForItem = async (item: TaskTypeItem) => { const getDesignersForItem = (item: TaskTypeItem) => { if (!item.taskTypeId) return [] - const cacheKey = `taskType_${item.taskTypeId}` + const cacheKey = `taskType_${item.taskTypeId}_brand_${formData.brandId || 0}` return designersCache[cacheKey] || [] } @@ -1415,30 +1532,31 @@ const pipelineGroups = computed(() => { const typeInfo = getTaskTypeInfo(taskType.taskTypeId) if (!typeInfo) continue - // 获取接单部门配置(三级分类从二级父类继承) - let deptIds = getTypeAllowedDeptIds(typeInfo) + // 获取接单部门配置(任务类型可接单部门与品牌负责接单部门共同作用) + let deptIds = getEffectiveTaskTypeDeptIds(typeInfo) let deptName = '' // 如果用户选择了具体部门,使用用户选择的部门 if (taskType.selectedDeptId) { - deptIds = String(taskType.selectedDeptId) + deptIds = [taskType.selectedDeptId] const selectedDept = getDepartmentById(taskType.selectedDeptId) deptName = getDepartmentDisplayName(selectedDept) } else { - deptName = getDeptNameFromIds(deptIds) + deptName = getDeptNameFromIdList(deptIds) } // 判断是否需要创建新分组 - if (!currentGroup || currentGroup.deptIds !== deptIds) { + const deptKey = deptIds.join(',') + if (!currentGroup || currentGroup.deptIds !== deptKey) { // 保存之前的分组 if (currentGroup) { groups.push(currentGroup) } - + // 创建新分组 currentGroup = { deptName, - deptIds, + deptIds: deptKey, items: [], totalScore: 0, totalHours: 0 @@ -1476,13 +1594,12 @@ const getTaskTypeDeptOptions = (item: TaskTypeItem): Array<{ id: number, deptNam const typeInfo = getTaskTypeInfo(item.taskTypeId) if (!typeInfo) return [] - const deptIds = getTypeAllowedDeptIds(typeInfo) - if (!deptIds) return [] + const deptIds = getEffectiveTaskTypeDeptIds(typeInfo) + if (!deptIds.length) return [] - const ids = deptIds.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id)) - if (ids.length <= 1) return [] // 只有一个部门时不需要选择 + if (deptIds.length <= 1) return [] // 只有一个部门时不需要选择 - const depts = ids + const depts = deptIds .map(id => getDepartmentById(id)) .filter((dept): dept is departmentApi.DepartmentOverview => !!dept) @@ -1500,15 +1617,46 @@ const getTaskTypeDeptOptions = (item: TaskTypeItem): Array<{ id: number, deptNam })) } +const validateTaskTypeDeptSelections = (items: TaskTypeItem[]) => { + for (const item of items) { + if (!item.taskTypeId) continue + + const typeInfo = getTaskTypeInfo(item.taskTypeId) + if (!typeInfo) continue + + if (!hasTaskTypeDeptRestriction(typeInfo)) { + continue + } + + const effectiveDeptIds = getEffectiveTaskTypeDeptIds(typeInfo) + if (!effectiveDeptIds.length) { + message.error(`任务类型「${typeInfo.name}」在当前品牌下没有可用的接单部门,请先检查品牌与接单部门配置`) + return false + } + + if (effectiveDeptIds.length > 1 && !item.selectedDeptId) { + message.error(`任务类型「${typeInfo.name}」存在多个可用接单部门,请先选择具体部门`) + return false + } + + if (item.selectedDeptId && !effectiveDeptIds.includes(item.selectedDeptId)) { + message.error(`任务类型「${typeInfo.name}」已选择的接单部门不在当前品牌可用范围内,请重新选择`) + return false + } + } + + return true +} + // 根据部门ID获取部门名称 const getDeptNameFromIds = (deptIds: string): string => { - if (!deptIds) return '所有部门' - - const ids = deptIds.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id)) - if (ids.length === 0) return '所有部门' - - // 从部门列表中查找部门信息 - const depts = ids.map(id => getDepartmentById(id)).filter((dept): dept is departmentApi.DepartmentOverview => !!dept) + return getDeptNameFromIdList(parseDeptIds(deptIds)) +} + +const getDeptNameFromIdList = (deptIds: number[]): string => { + if (!deptIds.length) return '所有部门' + + const depts = deptIds.map(id => getDepartmentById(id)).filter((dept): dept is departmentApi.DepartmentOverview => !!dept) if (depts.length === 0) return '所有部门' // 如果只有一个部门,显示部门名称 @@ -1712,6 +1860,9 @@ const handleSubmit = async () => { message.error('请至少添加一个任务类型') return } + if (!validateTaskTypeDeptSelections(validTaskTypes)) { + return + } } // 检查库存(如果需要) @@ -1935,7 +2086,12 @@ const loadTaskSubTypes = async () => { // 监听配置更新事件 const handleConfigUpdate = async (event: CustomEvent) => { console.log('配置已更新,刷新配置数据:', event.detail) - await loadConfigData() + await Promise.all([ + loadConfigData(), + loadDepartmentList(), + loadReceivingBrandDeptRelations() + ]) + formData.taskTypes.forEach(item => syncTaskTypeDeptSelection(item)) } // 加载团队积分余额 @@ -2691,7 +2847,8 @@ onMounted(async () => { loadTaskSubTypes(), loadReleaseTypeConfig(), loadHierarchicalTaskTypes(), - loadDepartmentList() + loadDepartmentList(), + loadReceivingBrandDeptRelations() ]) // 检查是否是编辑模式 @@ -2727,6 +2884,14 @@ watch(selectedPublishingDeptId, async (deptId, oldDeptId) => { await loadTeamScore() }) +watch(() => formData.brandId, () => { + Object.keys(designersCache).forEach(key => { + delete designersCache[key] + }) + formData.taskTypes.forEach(item => syncTaskTypeDeptSelection(item)) + calculateScore() +}) + // 组件卸载 onUnmounted(() => { window.removeEventListener('configUpdated', handleConfigUpdate as EventListener) diff --git a/frontend/src/views/Settings.vue b/frontend/src/views/Settings.vue index ef69d45..7d1a114 100644 --- a/frontend/src/views/Settings.vue +++ b/frontend/src/views/Settings.vue @@ -2678,19 +2678,26 @@ 接单部门(只能接任务) - - + +
- 一个品牌只能由一个需求部门负责,一级需求部门发起任务时会根据选择的品牌自动路由到对应的需求部门 + {{ formData.department.deptType === 'demand' + ? '一个品牌只能由一个需求部门负责,一级需求部门发起任务时会根据选择的品牌自动路由到对应的需求部门' + : '品牌已配置接单部门时,发布任务选择任务类型后,可选接单部门将按“任务类型可接单部门 ∩ 品牌负责接单部门”过滤' }}
@@ -3254,6 +3261,8 @@ import { getDepartmentScorePool, setDeptResponsibleBrands, getDeptResponsibleBrandIds, + setReceivingDeptResponsibleBrands, + getReceivingDeptResponsibleBrandIds, updateAutoGrantConfig, getGrantRecords, manualGrant, @@ -3333,7 +3342,7 @@ interface DepartmentForm { alertThreshold?: number overLimitAction: 'alert' | 'block' initialScoreBalance?: number - responsibleBrandIds?: number[] // 负责的品牌ID列表(仅需求部门) + responsibleBrandIds?: number[] // 负责的品牌ID列表(需求路由 / 接单过滤) // 积分自动发放配置 autoGrantEnabled?: boolean autoGrantPeriod?: 'WEEKLY' | 'MONTHLY' @@ -5101,9 +5110,11 @@ const showDepartmentModal = async (item?: DepartmentOverview) => { if (item) { // 加载已有的品牌关联 let responsibleBrandIds: number[] = [] - if (item.deptType === 'demand' && item.id) { + if ((item.deptType === 'demand' || item.deptType === 'receiving') && item.id) { try { - const brandIdsResult = await getDeptResponsibleBrandIds(item.id) + const brandIdsResult = item.deptType === 'demand' + ? await getDeptResponsibleBrandIds(item.id) + : await getReceivingDeptResponsibleBrandIds(item.id) responsibleBrandIds = brandIdsResult?.data || brandIdsResult || [] } catch (e) { console.warn('加载品牌关联失败:', e) @@ -5390,11 +5401,15 @@ const saveDepartment = async () => { message.success('创建成功') } - // 如果是需求部门,保存负责的品牌 - if (formData.department.deptType === 'demand' && deptId) { + // 保存品牌关联:需求部门用于品牌路由,接单部门用于接单过滤 + if ((formData.department.deptType === 'demand' || formData.department.deptType === 'receiving') && deptId) { const brandIds = formData.department.responsibleBrandIds || [] try { - await setDeptResponsibleBrands(deptId, brandIds) + if (formData.department.deptType === 'demand') { + await setDeptResponsibleBrands(deptId, brandIds) + } else { + await setReceivingDeptResponsibleBrands(deptId, brandIds) + } } catch (brandError: any) { console.error('保存品牌关联失败:', brandError) message.warning('部门保存成功,但品牌关联保存失败:' + (brandError?.response?.data?.message || brandError?.message))