新增功能: 1. Task实体新增字段: - subTaskTotal: 子任务总数 - subTaskCompleted: 已完成子任务数 - subTaskInProgress: 进行中子任务数 - subTaskPending: 待接单子任务数 - subTasks: 子任务列表(包含处理人信息) 2. TaskService新增方法: - fillSubTaskStats(): 填充流水线任务的子任务统计信息 - 自动在getMyPublishedTasks和getMyReceivedTasks中调用 - 统计各状态子任务数量 - 自动填充处理人信息 3. 同时修复流水线任务主任务状态同步问题: - 设计师接单第一个子任务时,主任务状态更新为in_progress - 确保需求方能正确看到任务进度 影响范围: - 需求方查看"我发起的"任务时,可以看到子任务进度 - 设计师查看接收的任务时,可以看到子任务详情 - 为后续前端显示提供数据支持
490 lines
19 KiB
Java
490 lines
19 KiB
Java
package com.designhub.service;
|
||
|
||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||
import com.designhub.dto.DepartmentCreateRequest;
|
||
import com.designhub.dto.DepartmentLimitSettingsRequest;
|
||
import com.designhub.dto.DepartmentScoreAdjustRequest;
|
||
import com.designhub.dto.DepartmentUpdateRequest;
|
||
import com.designhub.entity.Department;
|
||
import com.designhub.entity.User;
|
||
import com.designhub.exception.BusinessException;
|
||
import com.designhub.mapper.DepartmentMapper;
|
||
import com.designhub.mapper.UserMapper;
|
||
import com.designhub.vo.DepartmentOverviewVO;
|
||
import lombok.RequiredArgsConstructor;
|
||
import org.springframework.stereotype.Service;
|
||
import org.springframework.transaction.annotation.Transactional;
|
||
import org.springframework.util.CollectionUtils;
|
||
import org.springframework.util.StringUtils;
|
||
|
||
import java.util.ArrayList;
|
||
import java.util.Arrays;
|
||
import java.util.Collections;
|
||
import java.util.List;
|
||
import java.util.Map;
|
||
import java.util.Objects;
|
||
import java.util.stream.Collectors;
|
||
|
||
/**
|
||
* 部门管理Service
|
||
*/
|
||
@Service
|
||
@RequiredArgsConstructor
|
||
public class DepartmentService {
|
||
|
||
private final DepartmentMapper departmentMapper;
|
||
private final TeamScorePoolService teamScorePoolService;
|
||
private final UserMapper userMapper;
|
||
|
||
/**
|
||
* 获取所有部门列表
|
||
*/
|
||
public List<Department> getAllDepartments() {
|
||
return departmentMapper.selectList(
|
||
new LambdaQueryWrapper<Department>()
|
||
.eq(Department::getStatus, 1)
|
||
.orderByAsc(Department::getSortOrder)
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 获取部门树形结构
|
||
*/
|
||
public List<Department> getDepartmentTree() {
|
||
List<Department> allDepts = departmentMapper.selectDeptTree();
|
||
return buildTree(allDepts, 0L);
|
||
}
|
||
|
||
/**
|
||
* 根据类型获取部门列表
|
||
*/
|
||
public List<Department> getDepartmentsByType(String deptType) {
|
||
return departmentMapper.selectList(
|
||
new LambdaQueryWrapper<Department>()
|
||
.eq(Department::getDeptType, deptType)
|
||
.eq(Department::getStatus, 1)
|
||
.orderByAsc(Department::getSortOrder)
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 根据ID获取部门
|
||
*/
|
||
public Department getDepartmentById(Long id) {
|
||
return departmentMapper.selectById(id);
|
||
}
|
||
|
||
/**
|
||
* 部门概览列表
|
||
*/
|
||
public List<DepartmentOverviewVO> getDepartmentOverviewList() {
|
||
List<Department> departments = getAllDepartments();
|
||
Map<Long, Long> memberCountMap = loadMemberCountMap();
|
||
Map<Long, String> leaderNameMap = loadLeaderNames(departments);
|
||
return departments.stream()
|
||
.map(dept -> buildOverviewVO(dept, memberCountMap, leaderNameMap))
|
||
.collect(Collectors.toList());
|
||
}
|
||
|
||
/**
|
||
* 获取单个部门概览
|
||
*/
|
||
public DepartmentOverviewVO getDepartmentOverview(Long deptId) {
|
||
Department department = getDepartmentById(deptId);
|
||
if (department == null) {
|
||
throw new BusinessException("部门不存在");
|
||
}
|
||
Map<Long, Long> memberCountMap = loadMemberCountMap();
|
||
Map<Long, String> leaderNameMap = loadLeaderNames(Collections.singletonList(department));
|
||
return buildOverviewVO(department, memberCountMap, leaderNameMap);
|
||
}
|
||
|
||
/**
|
||
* 创建部门
|
||
*/
|
||
@Transactional(rollbackFor = Exception.class)
|
||
public DepartmentOverviewVO createDepartment(DepartmentCreateRequest request) {
|
||
validateDeptType(request.getDeptType());
|
||
|
||
// 验证负责人ID是否有效
|
||
if (request.getLeaderId() != null && request.getLeaderId() > 0) {
|
||
User leader = userMapper.selectById(request.getLeaderId());
|
||
if (leader == null) {
|
||
throw new BusinessException("指定的负责人不存在");
|
||
}
|
||
}
|
||
|
||
Department department = new Department();
|
||
department.setDeptName(request.getDeptName());
|
||
department.setDeptCode(request.getDeptCode());
|
||
department.setDeptType(request.getDeptType());
|
||
department.setParentId(request.getParentId() != null ? request.getParentId() : 0L);
|
||
department.setLeaderId(request.getLeaderId());
|
||
department.setSortOrder(request.getSortOrder());
|
||
department.setStatus(request.getStatus());
|
||
department.setAllowedDemandTypes(joinAllowedTypes(request.getAllowedDemandTypes()));
|
||
|
||
departmentMapper.insert(department);
|
||
|
||
// 初始化积分池
|
||
teamScorePoolService.upsertPool(department.getId(), department.getDeptName());
|
||
if ("demand".equals(request.getDeptType())) {
|
||
if (request.getInitialScoreBalance() != null) {
|
||
teamScorePoolService.setBalance(department.getId(), request.getInitialScoreBalance());
|
||
department.setScoreBalance(request.getInitialScoreBalance());
|
||
}
|
||
teamScorePoolService.updateLimitConfig(
|
||
department.getId(),
|
||
defaultLimitEnabled(request.getLimitEnabled()),
|
||
defaultLimitPeriod(request.getLimitPeriod()),
|
||
request.getPeriodLimit(),
|
||
request.getAlertThreshold()
|
||
);
|
||
department.setLimitEnabled(defaultLimitEnabled(request.getLimitEnabled()));
|
||
department.setLimitPeriod(defaultLimitPeriod(request.getLimitPeriod()));
|
||
department.setPeriodLimit(request.getPeriodLimit());
|
||
department.setAlertThreshold(request.getAlertThreshold());
|
||
department.setOverLimitAction(
|
||
StringUtils.hasText(request.getOverLimitAction()) ? request.getOverLimitAction() : "alert"
|
||
);
|
||
}
|
||
|
||
return getDepartmentOverview(department.getId());
|
||
}
|
||
|
||
/**
|
||
* 更新部门
|
||
*/
|
||
@Transactional(rollbackFor = Exception.class)
|
||
public boolean updateDepartment(Long id, DepartmentUpdateRequest request) {
|
||
Department existDept = getDepartmentById(id);
|
||
if (existDept == null) {
|
||
throw new BusinessException("部门不存在");
|
||
}
|
||
|
||
if (StringUtils.hasText(request.getDeptName())) {
|
||
existDept.setDeptName(request.getDeptName());
|
||
}
|
||
if (StringUtils.hasText(request.getDeptCode())) {
|
||
existDept.setDeptCode(request.getDeptCode());
|
||
}
|
||
if (StringUtils.hasText(request.getDeptType())) {
|
||
validateDeptType(request.getDeptType());
|
||
existDept.setDeptType(request.getDeptType());
|
||
}
|
||
// 处理parentId:允许设置为0来清除(设置为null)
|
||
if (request.getParentId() != null) {
|
||
Long parentId = request.getParentId();
|
||
existDept.setParentId(parentId == 0 ? null : parentId);
|
||
}
|
||
// 处理leaderId:前端总是发送leaderId字段
|
||
// null表示清除负责人,非null数字表示设置负责人
|
||
// 由于无法区分字段不存在和字段为null,前端必须总是发送此字段
|
||
// 如果前端不发送字段,这里getLeaderId()也会返回null,但不会更新(保持原值)
|
||
// 为了支持清除,我们检查:如果leaderId字段在请求中存在(通过检查其他必填字段),则处理
|
||
// 简化方案:总是检查leaderId,如果为null且当前有值,则清除;如果为数字,则设置
|
||
// 但由于无法判断字段是否存在,我们采用:如果leaderId不为null,则更新
|
||
if (request.getLeaderId() != null) {
|
||
Long leaderId = request.getLeaderId();
|
||
// 验证负责人ID是否有效(0表示清除,大于0需要验证)
|
||
if (leaderId > 0) {
|
||
User leader = userMapper.selectById(leaderId);
|
||
if (leader == null) {
|
||
throw new BusinessException("指定的负责人不存在");
|
||
}
|
||
}
|
||
existDept.setLeaderId(leaderId == 0 ? null : leaderId);
|
||
} else if (request.getDeptName() != null) {
|
||
// 如果其他字段被更新(说明这是完整的更新请求),且leaderId为null,则清除负责人
|
||
// 这是一个启发式判断:如果请求包含deptName(通常是必填字段),则认为leaderId=null是明确的清除操作
|
||
existDept.setLeaderId(null);
|
||
}
|
||
// 否则(leaderId为null且没有其他字段更新),不处理leaderId,保持原值
|
||
if (request.getSortOrder() != null) {
|
||
existDept.setSortOrder(request.getSortOrder());
|
||
}
|
||
if (request.getStatus() != null) {
|
||
existDept.setStatus(request.getStatus());
|
||
}
|
||
if (request.getAllowedDemandTypes() != null) {
|
||
existDept.setAllowedDemandTypes(joinAllowedTypes(request.getAllowedDemandTypes()));
|
||
}
|
||
if (request.getLimitEnabled() != null) {
|
||
existDept.setLimitEnabled(request.getLimitEnabled());
|
||
}
|
||
if (request.getLimitPeriod() != null) {
|
||
existDept.setLimitPeriod(request.getLimitPeriod());
|
||
}
|
||
if (request.getPeriodLimit() != null) {
|
||
existDept.setPeriodLimit(request.getPeriodLimit());
|
||
}
|
||
if (request.getAlertThreshold() != null) {
|
||
existDept.setAlertThreshold(request.getAlertThreshold());
|
||
}
|
||
if (request.getOverLimitAction() != null) {
|
||
existDept.setOverLimitAction(request.getOverLimitAction());
|
||
}
|
||
|
||
departmentMapper.updateById(existDept);
|
||
|
||
// 同步积分池
|
||
teamScorePoolService.upsertPool(existDept.getId(), existDept.getDeptName());
|
||
if ("demand".equals(existDept.getDeptType()) &&
|
||
(request.getLimitEnabled() != null || request.getLimitPeriod() != null ||
|
||
request.getPeriodLimit() != null || request.getAlertThreshold() != null)) {
|
||
teamScorePoolService.updateLimitConfig(
|
||
existDept.getId(),
|
||
request.getLimitEnabled(),
|
||
request.getLimitPeriod(),
|
||
request.getPeriodLimit(),
|
||
request.getAlertThreshold()
|
||
);
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 更新部门限制配置
|
||
*/
|
||
@Transactional(rollbackFor = Exception.class)
|
||
public boolean updateLimitSettings(Long deptId, DepartmentLimitSettingsRequest request) {
|
||
Department department = getDepartmentById(deptId);
|
||
if (department == null) {
|
||
throw new BusinessException("部门不存在");
|
||
}
|
||
if (!"demand".equals(department.getDeptType())) {
|
||
throw new BusinessException("只有需求部门需要配置积分限制");
|
||
}
|
||
|
||
department.setLimitEnabled(request.getLimitEnabled());
|
||
department.setLimitPeriod(request.getLimitPeriod());
|
||
department.setPeriodLimit(request.getPeriodLimit());
|
||
department.setAlertThreshold(request.getAlertThreshold());
|
||
department.setOverLimitAction(request.getOverLimitAction());
|
||
departmentMapper.updateById(department);
|
||
|
||
teamScorePoolService.updateLimitConfig(
|
||
deptId,
|
||
request.getLimitEnabled(),
|
||
request.getLimitPeriod(),
|
||
request.getPeriodLimit(),
|
||
request.getAlertThreshold()
|
||
);
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 删除部门(逻辑删除)
|
||
*/
|
||
@Transactional(rollbackFor = Exception.class)
|
||
public boolean deleteDepartment(Long id) {
|
||
long childCount = departmentMapper.selectCount(
|
||
new LambdaQueryWrapper<Department>()
|
||
.eq(Department::getParentId, id)
|
||
);
|
||
if (childCount > 0) {
|
||
throw new BusinessException("该部门下还有子部门,无法删除");
|
||
}
|
||
|
||
return departmentMapper.deleteById(id) > 0;
|
||
}
|
||
|
||
/**
|
||
* 设置部门负责人/主管
|
||
*/
|
||
@Transactional(rollbackFor = Exception.class)
|
||
public boolean setDepartmentHead(Long deptId, Long userId, String type) {
|
||
Department department = getDepartmentById(deptId);
|
||
if (department == null) {
|
||
throw new BusinessException("部门不存在");
|
||
}
|
||
|
||
if ("head".equals(type)) {
|
||
if (!"demand".equals(department.getDeptType())) {
|
||
throw new BusinessException("只有需求部门可以设置负责人");
|
||
}
|
||
department.setLeaderId(userId);
|
||
} else if ("supervisor".equals(type)) {
|
||
if (!"receiving".equals(department.getDeptType())) {
|
||
throw new BusinessException("只有接单部门可以设置主管");
|
||
}
|
||
department.setSupervisorId(userId);
|
||
} else {
|
||
throw new BusinessException("类型参数错误");
|
||
}
|
||
|
||
return departmentMapper.updateById(department) > 0;
|
||
}
|
||
|
||
/**
|
||
* 调整部门积分
|
||
*/
|
||
@Transactional(rollbackFor = Exception.class)
|
||
public void adjustScore(Long deptId, DepartmentScoreAdjustRequest request) {
|
||
Department department = getDepartmentById(deptId);
|
||
if (department == null) {
|
||
throw new BusinessException("部门不存在");
|
||
}
|
||
if (!"demand".equals(department.getDeptType())) {
|
||
throw new BusinessException("只有需求部门可以调整积分");
|
||
}
|
||
if (request.getAmount() == null || request.getAmount() <= 0) {
|
||
throw new BusinessException("调整积分必须大于0");
|
||
}
|
||
|
||
String action = request.getAction().toLowerCase();
|
||
switch (action) {
|
||
case "recharge" -> teamScorePoolService.rechargeScore(deptId, request.getAmount());
|
||
case "deduct" -> teamScorePoolService.deductScore(deptId, request.getAmount());
|
||
default -> throw new BusinessException("不支持的积分操作类型");
|
||
}
|
||
}
|
||
|
||
private void validateDeptType(String deptType) {
|
||
if (!"demand".equals(deptType) && !"receiving".equals(deptType)) {
|
||
throw new BusinessException("部门类型必须是 demand 或 receiving");
|
||
}
|
||
}
|
||
|
||
private Integer defaultLimitEnabled(Integer limitEnabled) {
|
||
return limitEnabled != null ? limitEnabled : 0;
|
||
}
|
||
|
||
private String defaultLimitPeriod(String limitPeriod) {
|
||
return StringUtils.hasText(limitPeriod) ? limitPeriod : "MONTHLY";
|
||
}
|
||
|
||
private String joinAllowedTypes(List<String> allowedTypes) {
|
||
if (CollectionUtils.isEmpty(allowedTypes)) {
|
||
return null;
|
||
}
|
||
return allowedTypes.stream()
|
||
.filter(StringUtils::hasText)
|
||
.collect(Collectors.joining(","));
|
||
}
|
||
|
||
private List<String> splitAllowedTypes(String allowedTypes) {
|
||
if (allowedTypes == null || allowedTypes.trim().isEmpty()) {
|
||
return Collections.emptyList();
|
||
}
|
||
return Arrays.stream(allowedTypes.split(","))
|
||
.map(String::trim)
|
||
.filter(StringUtils::hasText)
|
||
.collect(Collectors.toList());
|
||
}
|
||
|
||
private Map<Long, Long> loadMemberCountMap() {
|
||
return userMapper.selectList(null).stream()
|
||
.filter(user -> user.getDeptId() != null)
|
||
.collect(Collectors.groupingBy(
|
||
User::getDeptId,
|
||
Collectors.counting()
|
||
));
|
||
}
|
||
|
||
private Map<Long, String> loadLeaderNames(List<Department> departments) {
|
||
List<Long> leaderIds = departments.stream()
|
||
.map(Department::getLeaderId)
|
||
.filter(Objects::nonNull)
|
||
.distinct()
|
||
.collect(Collectors.toList());
|
||
if (leaderIds.isEmpty()) {
|
||
return Collections.emptyMap();
|
||
}
|
||
return userMapper.selectBatchIds(leaderIds).stream()
|
||
.collect(Collectors.toMap(
|
||
User::getId,
|
||
user -> StringUtils.hasText(user.getRealName()) ? user.getRealName() : user.getUsername()
|
||
));
|
||
}
|
||
|
||
private DepartmentOverviewVO buildOverviewVO(Department department,
|
||
Map<Long, Long> memberCountMap,
|
||
Map<Long, String> leaderNameMap) {
|
||
DepartmentOverviewVO vo = new DepartmentOverviewVO();
|
||
vo.setId(department.getId());
|
||
vo.setDeptName(department.getDeptName());
|
||
vo.setDeptCode(department.getDeptCode());
|
||
vo.setDeptType(department.getDeptType());
|
||
vo.setParentId(department.getParentId());
|
||
vo.setLeaderId(department.getLeaderId());
|
||
if (department.getLeaderId() != null && leaderNameMap != null) {
|
||
vo.setLeaderName(leaderNameMap.get(department.getLeaderId()));
|
||
}
|
||
vo.setDescription(department.getDescription());
|
||
vo.setSortOrder(department.getSortOrder());
|
||
vo.setStatus(department.getStatus());
|
||
vo.setAllowedDemandTypes(splitAllowedTypes(department.getAllowedDemandTypes()));
|
||
|
||
var pool = teamScorePoolService.getByDeptId(department.getId());
|
||
if (pool != null) {
|
||
vo.setScoreBalance(pool.getBalance());
|
||
vo.setTotalRecharge(pool.getTotalRecharge());
|
||
vo.setTotalConsume(pool.getTotalConsume());
|
||
vo.setTotalEarn(pool.getTotalEarn());
|
||
vo.setLimitEnabled(pool.getLimitEnabled());
|
||
vo.setLimitPeriod(pool.getLimitPeriod());
|
||
vo.setPeriodLimit(pool.getPeriodLimit());
|
||
vo.setPeriodUsed(pool.getPeriodUsed());
|
||
vo.setAlertThreshold(pool.getAlertThreshold());
|
||
vo.setLimitAlert(teamScorePoolService.isAlert(department.getId()));
|
||
} else {
|
||
vo.setScoreBalance(department.getScoreBalance());
|
||
vo.setLimitEnabled(department.getLimitEnabled());
|
||
vo.setLimitPeriod(department.getLimitPeriod());
|
||
vo.setPeriodLimit(department.getPeriodLimit());
|
||
vo.setAlertThreshold(department.getAlertThreshold());
|
||
vo.setLimitAlert(false);
|
||
}
|
||
vo.setOverLimitAction(department.getOverLimitAction());
|
||
|
||
if (memberCountMap != null && memberCountMap.containsKey(department.getId())) {
|
||
vo.setMemberCount(memberCountMap.get(department.getId()).intValue());
|
||
} else {
|
||
vo.setMemberCount(0);
|
||
}
|
||
return vo;
|
||
}
|
||
|
||
/**
|
||
* 构建树形结构
|
||
*/
|
||
private List<Department> buildTree(List<Department> allDepts, Long parentId) {
|
||
List<Department> children = allDepts.stream()
|
||
.filter(dept -> {
|
||
Long deptParentId = dept.getParentId();
|
||
if (deptParentId == null) {
|
||
return parentId == null || parentId == 0L;
|
||
}
|
||
return deptParentId.equals(parentId);
|
||
})
|
||
.collect(Collectors.toList());
|
||
|
||
for (Department child : children) {
|
||
buildTree(allDepts, child.getId());
|
||
}
|
||
|
||
return children;
|
||
}
|
||
|
||
/**
|
||
* 检查用户是否为任何部门的负责人
|
||
*/
|
||
public boolean isUserDepartmentLeader(Long userId) {
|
||
if (userId == null) {
|
||
return false;
|
||
}
|
||
|
||
// 查询是否有部门的 leader_id 等于该用户ID
|
||
Long count = departmentMapper.selectCount(
|
||
new LambdaQueryWrapper<Department>()
|
||
.eq(Department::getLeaderId, userId)
|
||
.eq(Department::getStatus, 1)
|
||
);
|
||
|
||
return count != null && count > 0;
|
||
}
|
||
}
|
||
|