feat: support brand-based receiving departments

This commit is contained in:
Your Name 2026-03-30 14:35:26 +08:00
parent 4c911854f4
commit ef15e00889
13 changed files with 586 additions and 149 deletions

View File

@ -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<List<DeptBrandRelation>> getAllBrandDeptRelations() {
return Result.success(departmentService.getAllBrandDeptRelations());
}
@Operation(summary = "设置接单部门负责的品牌")
@PostMapping("/{deptId}/receiving-brands")
public Result<Void> setReceivingResponsibleBrands(@PathVariable Long deptId, @RequestBody List<Long> brandIds) {
departmentService.setReceivingDeptResponsibleBrands(deptId, brandIds);
return Result.success();
}
@Operation(summary = "获取接单部门负责的品牌列表")
@GetMapping("/{deptId}/receiving-brands")
public Result<List<Brand>> getReceivingResponsibleBrands(@PathVariable Long deptId) {
return Result.success(departmentService.getReceivingDeptResponsibleBrands(deptId));
}
@Operation(summary = "获取接单部门负责的品牌ID列表")
@GetMapping("/{deptId}/receiving-brand-ids")
public Result<List<Long>> getReceivingResponsibleBrandIds(@PathVariable Long deptId) {
return Result.success(departmentService.getReceivingDeptResponsibleBrandIds(deptId));
}
@Operation(summary = "获取所有接单部门-品牌关联关系")
@GetMapping("/receiving-brand-relations")
public Result<List<ReceivingDeptBrandRelation>> getAllReceivingBrandDeptRelations() {
return Result.success(departmentService.getAllReceivingBrandDeptRelations());
}
@Operation(summary = "根据品牌ID获取负责的需求部门")
@GetMapping("/by-brand/{brandId}")
@ -193,4 +219,3 @@ public class DepartmentController {
return Result.success();
}
}

View File

@ -150,12 +150,13 @@ public class UserController {
public Result<List<Map<String, Object>>> 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,则默认为当前用户的部门

View File

@ -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; }
}

View File

@ -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<ReceivingDeptBrandRelation> {
@Select("SELECT dept_id FROM receiving_dept_brand_relation WHERE brand_id = #{brandId}")
List<Long> getDeptIdsByBrandId(@Param("brandId") Long brandId);
@Select("SELECT brand_id FROM receiving_dept_brand_relation WHERE dept_id = #{deptId}")
List<Long> getBrandIdsByDeptId(@Param("deptId") Long deptId);
@Delete("DELETE FROM receiving_dept_brand_relation WHERE dept_id = #{deptId}")
int deleteByDeptId(@Param("deptId") Long deptId);
@Insert("<script>" +
"INSERT INTO receiving_dept_brand_relation (dept_id, brand_id, create_time) VALUES " +
"<foreach collection='brandIds' item='brandId' separator=','>" +
"(#{deptId}, #{brandId}, NOW())" +
"</foreach>" +
"</script>")
int batchInsert(@Param("deptId") Long deptId, @Param("brandIds") List<Long> 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<ReceivingDeptBrandRelation> getAllRelationsWithNames();
}

View File

@ -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<DeptBrandRelation> getAllBrandDeptRelations() {
return deptBrandRelationMapper.getAllRelationsWithNames();
}
/**
* 设置接单部门负责的品牌(用于创建任务时过滤接单部门)
*/
@Transactional(rollbackFor = Exception.class)
public void setReceivingDeptResponsibleBrands(Long deptId, List<Long> 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<Brand> getReceivingDeptResponsibleBrands(Long deptId) {
List<Long> brandIds = receivingDeptBrandRelationMapper.getBrandIdsByDeptId(deptId);
if (CollectionUtils.isEmpty(brandIds)) {
return Collections.emptyList();
}
return brandMapper.selectBatchIds(brandIds);
}
public List<Long> getReceivingDeptResponsibleBrandIds(Long deptId) {
return receivingDeptBrandRelationMapper.getBrandIdsByDeptId(deptId);
}
public List<ReceivingDeptBrandRelation> getAllReceivingBrandDeptRelations() {
return receivingDeptBrandRelationMapper.getAllRelationsWithNames();
}
/**
* 获取品牌可用的接单部门(自动展开子部门)
*/
public List<Long> getReceivingDeptIdsByBrandId(Long brandId) {
if (brandId == null) {
return Collections.emptyList();
}
List<Long> 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<Long> deptIds = receivingDeptBrandRelationMapper.getDeptIdsByBrandId(brandId);
return !CollectionUtils.isEmpty(deptIds);
}
/**
* 计算任务类型和品牌维度共同作用下的可接单部门列表
* - 品牌未配置接单部门时:回退现有任务类型逻辑
* - 品牌已配置接单部门时:取任务类型与品牌配置的交集
*/
public List<Long> filterReceivingDeptIdsByBrand(Long brandId, String allowedDeptIdsText) {
List<Long> brandDeptIds = getReceivingDeptIdsByBrandId(brandId);
if (CollectionUtils.isEmpty(brandDeptIds)) {
return StringUtils.hasText(allowedDeptIdsText)
? expandDeptIdsWithChildren(allowedDeptIdsText)
: getAllReceivingDeptIds();
}
if (!StringUtils.hasText(allowedDeptIdsText)) {
return brandDeptIds;
}
java.util.LinkedHashSet<Long> 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<Long> 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<Long> brandDeptIds = getReceivingDeptIdsByBrandId(brandId);
if (CollectionUtils.isEmpty(brandDeptIds)) {
return true;
}
return brandDeptIds.contains(deptId);
}
private List<Long> getAllReceivingDeptIds() {
List<Department> receivingDepts = departmentMapper.selectList(
new LambdaQueryWrapper<Department>()
.eq(Department::getDeptType, "receiving")
.eq(Department::getStatus, 1)
.eq(Department::getDeleted, 0)
);
return receivingDepts.stream()
.map(Department::getId)
.distinct()
.collect(Collectors.toList());
}
/**
* 检查部门是否为一级需求部门

View File

@ -1016,9 +1016,9 @@ public class TaskApprovalService {
}
}
if (allowedDeptIds != null && !allowedDeptIds.isEmpty()) {
List<Long> expandedDeptIds = departmentService.expandDeptIdsWithChildren(allowedDeptIds);
for (Long deptId : expandedDeptIds) {
List<Long> 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())

View File

@ -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<Long> 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<Long> 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";
// 记录分组到部门的映射

View File

@ -1690,7 +1690,11 @@ public class UserService {
* @return 设计师列表,包含每个设计师的进行中任务数量
*/
public List<Map<String, Object>> getDesignersByTaskType(Long taskTypeId) {
log.info("根据任务类型获取设计师列表: taskTypeId={}", taskTypeId);
return getDesignersByTaskType(taskTypeId, null);
}
public List<Map<String, Object>> 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<Long> 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<Long> targetDeptIds = departmentService.filterReceivingDeptIdsByBrand(brandId, allowedDeptIds);
if (targetDeptIds.isEmpty()) {
log.warn("解析后的部门列表为空");
if (departmentService.hasReceivingBrandConfig(brandId)) {
log.warn("品牌 {} 下没有可承接任务类型 {} 的接单部门", brandId, taskTypeId);
} else {
log.warn("解析后的部门列表为空");
}
return new ArrayList<>();
}

View File

@ -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='接单部门品牌过滤关系表';

View File

@ -133,6 +133,26 @@ export const getAllBrandDeptRelations = () => {
return request.get<DeptBrandRelation[]>('/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<Brand[]>(`/departments/${deptId}/receiving-brands`)
}
// 获取接单部门负责的品牌ID列表
export const getReceivingDeptResponsibleBrandIds = (deptId: number) => {
return request.get<number[]>(`/departments/${deptId}/receiving-brand-ids`)
}
// 获取所有接单部门-品牌关联关系
export const getAllReceivingBrandDeptRelations = () => {
return request.get<DeptBrandRelation[]>('/departments/receiving-brand-relations')
}
// 根据品牌ID获取负责的需求部门
export const getDemandDeptByBrandId = (brandId: number) => {
return request.get<DepartmentOverview>(`/departments/by-brand/${brandId}`)

View File

@ -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<DesignerWithWorkload[]> => {
export const getDesignersWithWorkload = async (deptId?: number, taskTypeId?: number, brandId?: number): Promise<DesignerWithWorkload[]> => {
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 || [];
};

View File

@ -982,6 +982,7 @@ const hierarchicalTaskTypes = ref<any[]>([])
// 部门列表数据
const departmentList = ref<departmentApi.DepartmentOverview[]>([])
const receivingBrandDeptRelations = ref<departmentApi.DeptBrandRelation[]>([])
// 加载层级任务类型数据
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<number>()
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<number>()
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<CascaderOption[]>(() => {
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)

View File

@ -2678,19 +2678,26 @@
<a-radio value="receiving">接单部门(只能接任务)</a-radio>
</a-radio-group>
</a-form-item>
<!-- 负责品牌配置(仅普通需求部门显示) -->
<a-form-item v-if="formData.department.deptType === 'demand'" label="负责品牌">
<!-- 品牌配置:需求部门用于品牌路由,接单部门用于接单过滤 -->
<a-form-item
v-if="formData.department.deptType === 'demand' || formData.department.deptType === 'receiving'"
:label="formData.department.deptType === 'demand' ? '负责品牌' : '负责品牌(接单过滤)'"
>
<a-select
v-model:value="formData.department.responsibleBrandIds"
mode="multiple"
placeholder="选择该部门负责的品牌(一级需求部门发起的任务将根据品牌路由到此部门)"
:placeholder="formData.department.deptType === 'demand'
? '选择该部门负责的品牌(一级需求部门发起的任务将根据品牌路由到此部门)'
: '选择该接单部门负责的品牌(发布任务时将按品牌过滤可选接单部门)'"
allow-clear
:options="brandOptions"
:filter-option="filterBrandOption"
show-search
/>
<div class="form-tip" style="margin-top: 4px; color: #888; font-size: 12px;">
一个品牌只能由一个需求部门负责,一级需求部门发起任务时会根据选择的品牌自动路由到对应的需求部门
{{ formData.department.deptType === 'demand'
? '一个品牌只能由一个需求部门负责,一级需求部门发起任务时会根据选择的品牌自动路由到对应的需求部门'
: '品牌已配置接单部门时,发布任务选择任务类型后,可选接单部门将按“任务类型可接单部门 ∩ 品牌负责接单部门”过滤' }}
</div>
</a-form-item>
<a-form-item label="部门负责人">
@ -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))