Your Name 371cfd5b2a feat: 为任务卡片添加子任务进度统计功能
新增功能:
1. Task实体新增字段:
   - subTaskTotal: 子任务总数
   - subTaskCompleted: 已完成子任务数
   - subTaskInProgress: 进行中子任务数
   - subTaskPending: 待接单子任务数
   - subTasks: 子任务列表(包含处理人信息)

2. TaskService新增方法:
   - fillSubTaskStats(): 填充流水线任务的子任务统计信息
   - 自动在getMyPublishedTasks和getMyReceivedTasks中调用
   - 统计各状态子任务数量
   - 自动填充处理人信息

3. 同时修复流水线任务主任务状态同步问题:
   - 设计师接单第一个子任务时,主任务状态更新为in_progress
   - 确保需求方能正确看到任务进度

影响范围:
- 需求方查看"我发起的"任务时,可以看到子任务进度
- 设计师查看接收的任务时,可以看到子任务详情
- 为后续前端显示提供数据支持
2026-01-03 11:32:16 +08:00

490 lines
19 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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