项目甘特图后端接口设计

This commit is contained in:
wangna0328 2025-08-13 14:39:15 +08:00
parent f7b877aef8
commit 9585f6dd39
6 changed files with 740 additions and 0 deletions

View File

@ -0,0 +1,45 @@
package com.dite.znpt.domain.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.time.LocalDate;
/**
* 甘特图查询请求类
*/
@Data
@ApiModel(value = "GanttChartReq", description = "甘特图查询请求")
public class GanttChartReq {
@ApiModelProperty("项目ID")
private String projectId;
@ApiModelProperty("任务组ID")
private String taskGroupId;
@ApiModelProperty("任务状态")
private Integer status;
@ApiModelProperty("负责人ID")
private String mainUserId;
@ApiModelProperty("开始时间范围-开始")
private LocalDate startDateFrom;
@ApiModelProperty("开始时间范围-结束")
private LocalDate startDateTo;
@ApiModelProperty("结束时间范围-开始")
private LocalDate endDateFrom;
@ApiModelProperty("结束时间范围-结束")
private LocalDate endDateTo;
@ApiModelProperty("是否包含已完成任务")
private Boolean includeCompleted = true;
@ApiModelProperty("是否只显示逾期任务")
private Boolean overdueOnly = false;
}

View File

@ -0,0 +1,91 @@
package com.dite.znpt.domain.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.time.LocalDate;
import java.util.List;
/**
* 甘特图数据响应类
*/
@Data
@ApiModel(value = "GanttChartResp", description = "甘特图数据响应")
public class GanttChartResp {
@ApiModelProperty("任务ID")
private String taskId;
@ApiModelProperty("任务名称")
private String taskName;
@ApiModelProperty("任务编号")
private String taskCode;
@ApiModelProperty("上级任务ID")
private String parentTaskId;
@ApiModelProperty("任务组ID")
private String taskGroupId;
@ApiModelProperty("任务组名称")
private String taskGroupName;
@ApiModelProperty("计划开始时间")
private LocalDate planStartDate;
@ApiModelProperty("计划结束时间")
private LocalDate planEndDate;
@ApiModelProperty("实际开始时间")
private LocalDate actualStartDate;
@ApiModelProperty("实际结束时间")
private LocalDate actualEndDate;
@ApiModelProperty("任务状态0未开始1进行中2已结束")
private Integer status;
@ApiModelProperty("任务状态描述")
private String statusDesc;
@ApiModelProperty("是否逾期0未逾期1已逾期")
private Integer overdueStatus;
@ApiModelProperty("任务负责人ID")
private String mainUserId;
@ApiModelProperty("任务负责人姓名")
private String mainUserName;
@ApiModelProperty("任务参与人")
private String userIds;
@ApiModelProperty("任务参与人姓名列表")
private List<String> participantNames;
@ApiModelProperty("进度百分比")
private Integer progress;
@ApiModelProperty("任务层级")
private Integer level;
@ApiModelProperty("任务在时间轴上的位置(天数)")
private Integer position;
@ApiModelProperty("任务持续时间(天数)")
private Integer duration;
@ApiModelProperty("子任务列表")
private List<GanttChartResp> children;
@ApiModelProperty("任务备注")
private String remark;
@ApiModelProperty("项目ID")
private String projectId;
@ApiModelProperty("项目名称")
private String projectName;
}

View File

@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.dite.znpt.domain.entity.ProjectTaskEntity;
import com.dite.znpt.domain.vo.ProjectTaskListReq;
import com.dite.znpt.domain.vo.ProjectTaskResp;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@ -13,6 +14,7 @@ import java.util.List;
* @date 2025/06/24 16:44
* @Description: 项目任务信息表数据库访问层
*/
@Mapper
public interface ProjectTaskMapper extends BaseMapper<ProjectTaskEntity> {
List<ProjectTaskResp> queryBySelective(ProjectTaskListReq projectTaskReq);

View File

@ -0,0 +1,55 @@
package com.dite.znpt.service;
import com.dite.znpt.domain.vo.GanttChartReq;
import com.dite.znpt.domain.vo.GanttChartResp;
import java.util.List;
/**
* 甘特图服务接口
*/
public interface GanttChartService {
/**
* 获取项目甘特图数据
*
* @param req 查询条件
* @return 甘特图数据列表
*/
List<GanttChartResp> getGanttChartData(GanttChartReq req);
/**
* 获取项目甘特图统计信息
*
* @param projectId 项目ID
* @return 统计信息
*/
Object getGanttChartStatistics(String projectId);
/**
* 获取项目甘特图时间轴信息
*
* @param projectId 项目ID
* @return 时间轴信息
*/
Object getGanttChartTimeline(String projectId);
/**
* 更新任务进度
*
* @param taskId 任务ID
* @param progress 进度百分比
* @return 是否成功
*/
boolean updateTaskProgress(String taskId, Integer progress);
/**
* 拖拽更新任务时间
*
* @param taskId 任务ID
* @param startDate 开始时间
* @param endDate 结束时间
* @return 是否成功
*/
boolean updateTaskTime(String taskId, String startDate, String endDate);
}

View File

@ -0,0 +1,486 @@
package com.dite.znpt.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.dite.znpt.domain.entity.ProjectTaskEntity;
import com.dite.znpt.domain.entity.UserEntity;
import com.dite.znpt.domain.vo.GanttChartReq;
import com.dite.znpt.domain.vo.GanttChartResp;
import com.dite.znpt.mapper.ProjectTaskMapper;
import com.dite.znpt.service.GanttChartService;
import com.dite.znpt.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.stream.Collectors;
/**
* 甘特图服务实现类
*/
@Service
@RequiredArgsConstructor
public class GanttChartServiceImpl implements GanttChartService {
private final ProjectTaskMapper projectTaskMapper;
private final UserService userService;
@Override
public List<GanttChartResp> getGanttChartData(GanttChartReq req) {
// 查询所有任务
QueryWrapper<ProjectTaskEntity> queryWrapper = new QueryWrapper<>();
if (StrUtil.isNotBlank(req.getProjectId())) {
queryWrapper.eq("project_id", req.getProjectId());
}
if (StrUtil.isNotBlank(req.getTaskGroupId())) {
queryWrapper.eq("task_group_id", req.getTaskGroupId());
}
if (req.getStatus() != null) {
queryWrapper.eq("status", req.getStatus());
}
if (StrUtil.isNotBlank(req.getMainUserId())) {
queryWrapper.eq("main_user_id", req.getMainUserId());
}
if (req.getStartDateFrom() != null) {
queryWrapper.ge("plan_start_date", req.getStartDateFrom());
}
if (req.getStartDateTo() != null) {
queryWrapper.le("plan_start_date", req.getStartDateTo());
}
if (req.getEndDateFrom() != null) {
queryWrapper.ge("plan_end_date", req.getEndDateFrom());
}
if (req.getEndDateTo() != null) {
queryWrapper.le("plan_end_date", req.getEndDateTo());
}
if (req.getOverdueOnly() != null && req.getOverdueOnly()) {
queryWrapper.eq("overdue_status", 1);
}
if (req.getIncludeCompleted() != null && !req.getIncludeCompleted()) {
queryWrapper.ne("status", 2);
}
queryWrapper.orderByAsc("plan_start_date");
List<ProjectTaskEntity> taskList = projectTaskMapper.selectList(queryWrapper);
// 获取用户信息
final Set<String> userIds = new HashSet<>();
taskList.forEach(task -> {
if (StrUtil.isNotBlank(task.getMainUserId())) {
userIds.add(task.getMainUserId());
}
if (StrUtil.isNotBlank(task.getUserIds())) {
userIds.addAll(Arrays.asList(task.getUserIds().split(",")));
}
});
final Map<String, UserEntity> userMap = new HashMap<>();
if (CollUtil.isNotEmpty(userIds)) {
List<UserEntity> users = userService.listByIds(userIds);
userMap.putAll(users.stream().collect(Collectors.toMap(UserEntity::getUserId, user -> user)));
}
// 批量获取项目时间范围避免重复查询
final Map<String, LocalDate> projectStartDates = new HashMap<>();
if (StrUtil.isNotBlank(req.getProjectId())) {
// 如果指定了项目ID直接获取该项目的时间范围
LocalDate projectStart = getProjectStartDate(req.getProjectId());
projectStartDates.put(req.getProjectId(), projectStart);
} else {
// 如果没有指定项目ID获取所有相关项目的时间范围
Set<String> projectIds = taskList.stream()
.map(ProjectTaskEntity::getProjectId)
.filter(StrUtil::isNotBlank)
.collect(Collectors.toSet());
for (String projectId : projectIds) {
LocalDate projectStart = getProjectStartDate(projectId);
projectStartDates.put(projectId, projectStart);
}
}
// 转换为甘特图数据
List<GanttChartResp> ganttData = taskList.stream().map(task -> {
GanttChartResp resp = BeanUtil.copyProperties(task, GanttChartResp.class);
// 设置状态描述
switch (task.getStatus()) {
case 0:
resp.setStatusDesc("未开始");
break;
case 1:
resp.setStatusDesc("进行中");
break;
case 2:
resp.setStatusDesc("已结束");
break;
default:
resp.setStatusDesc("未知");
}
// 设置负责人姓名
if (StrUtil.isNotBlank(task.getMainUserId()) && userMap.containsKey(task.getMainUserId())) {
resp.setMainUserName(userMap.get(task.getMainUserId()).getName());
}
// 设置参与人姓名列表
if (StrUtil.isNotBlank(task.getUserIds())) {
List<String> participantNames = Arrays.stream(task.getUserIds().split(","))
.filter(userId -> userMap.containsKey(userId))
.map(userId -> userMap.get(userId).getName())
.collect(Collectors.toList());
resp.setParticipantNames(participantNames);
}
// 计算进度这里可以根据实际业务逻辑调整
if (task.getStatus() == 2) {
resp.setProgress(100);
} else if (task.getStatus() == 1) {
resp.setProgress(50); // 默认进度实际应该从数据库字段获取
} else {
resp.setProgress(0);
}
// 计算任务在时间轴上的位置和持续时间
LocalDate projectStart = projectStartDates.get(task.getProjectId());
if (projectStart != null && task.getPlanStartDate() != null) {
int position = (int) ChronoUnit.DAYS.between(projectStart, task.getPlanStartDate());
resp.setPosition(position);
}
// 计算任务持续时间
if (task.getPlanStartDate() != null && task.getPlanEndDate() != null) {
int duration = (int) ChronoUnit.DAYS.between(task.getPlanStartDate(), task.getPlanEndDate()) + 1;
resp.setDuration(duration);
} else {
resp.setDuration(1); // 默认持续1天
}
return resp;
}).collect(Collectors.toList());
// 构建树形结构
return buildTreeStructure(ganttData);
}
@Override
public Object getGanttChartStatistics(String projectId) {
QueryWrapper<ProjectTaskEntity> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("project_id", projectId);
List<ProjectTaskEntity> taskList = projectTaskMapper.selectList(queryWrapper);
Map<String, Object> statistics = new HashMap<>();
statistics.put("totalTasks", taskList.size());
statistics.put("notStarted", (int) taskList.stream().filter(task -> task.getStatus() == 0).count());
statistics.put("inProgress", (int) taskList.stream().filter(task -> task.getStatus() == 1).count());
statistics.put("completed", (int) taskList.stream().filter(task -> task.getStatus() == 2).count());
statistics.put("overdue", (int) taskList.stream().filter(task -> task.getOverdueStatus() == 1).count());
// 计算总体进度
if (taskList.size() > 0) {
final double avgProgress = taskList.stream()
.mapToInt(task -> {
if (task.getStatus() == 2) return 100;
else if (task.getStatus() == 1) return 50;
else return 0;
})
.average()
.orElse(0.0);
statistics.put("overallProgress", Math.round(avgProgress));
} else {
statistics.put("overallProgress", 0);
}
return statistics;
}
@Override
public Object getGanttChartTimeline(String projectId) {
if (StrUtil.isBlank(projectId)) {
return null;
}
LocalDate projectStartDate = getProjectStartDate(projectId);
LocalDate projectEndDate = getProjectEndDate(projectId);
Map<String, Object> timeline = new HashMap<>();
timeline.put("projectId", projectId);
timeline.put("startDate", projectStartDate);
timeline.put("endDate", projectEndDate);
timeline.put("totalDays", ChronoUnit.DAYS.between(projectStartDate, projectEndDate) + 1);
// 计算时间轴上的关键时间点比如每周每月
List<Map<String, Object>> timePoints = new ArrayList<>();
LocalDate currentDate = projectStartDate;
while (!currentDate.isAfter(projectEndDate)) {
Map<String, Object> timePoint = new HashMap<>();
timePoint.put("date", currentDate);
timePoint.put("dayOfWeek", currentDate.getDayOfWeek().getDisplayName(java.time.format.TextStyle.SHORT, java.util.Locale.CHINESE));
timePoint.put("isWeekend", currentDate.getDayOfWeek().getValue() >= 6);
timePoints.add(timePoint);
currentDate = currentDate.plusDays(1);
}
timeline.put("timePoints", timePoints);
return timeline;
}
@Override
public boolean updateTaskProgress(String taskId, Integer progress) {
ProjectTaskEntity task = projectTaskMapper.selectById(taskId);
if (task == null) {
return false;
}
// 更新进度
// 这里需要根据实际业务逻辑添加进度字段
// task.setProgress(progress);
// 根据进度更新状态
if (progress >= 100) {
task.setStatus(2); // 已完成
task.setActualEndDate(LocalDate.now());
} else if (progress > 0) {
task.setStatus(1); // 进行中
if (task.getActualStartDate() == null) {
task.setActualStartDate(LocalDate.now());
}
}
return projectTaskMapper.updateById(task) > 0;
}
@Override
public boolean updateTaskTime(String taskId, String startDate, String endDate) {
ProjectTaskEntity task = projectTaskMapper.selectById(taskId);
if (task == null) {
return false;
}
final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
LocalDate newStartDate = null;
LocalDate newEndDate = null;
try {
if (StrUtil.isNotBlank(startDate)) {
newStartDate = LocalDate.parse(startDate, formatter);
}
if (StrUtil.isNotBlank(endDate)) {
newEndDate = LocalDate.parse(endDate, formatter);
}
} catch (DateTimeParseException e) {
return false; // 日期格式错误
}
// 验证时间约束
if (!validateTaskTimeConstraints(task, newStartDate, newEndDate)) {
return false;
}
// 更新任务时间
if (newStartDate != null) {
task.setPlanStartDate(newStartDate);
}
if (newEndDate != null) {
task.setPlanEndDate(newEndDate);
}
// 更新逾期状态
updateOverdueStatus(task);
return projectTaskMapper.updateById(task) > 0;
}
/**
* 构建树形结构
*/
private List<GanttChartResp> buildTreeStructure(List<GanttChartResp> allTasks) {
final Map<String, GanttChartResp> taskMap = allTasks.stream()
.collect(Collectors.toMap(GanttChartResp::getTaskId, task -> task));
final List<GanttChartResp> rootTasks = new ArrayList<>();
for (GanttChartResp task : allTasks) {
if (StrUtil.isBlank(task.getParentTaskId())) {
// 根任务
task.setLevel(0);
rootTasks.add(task);
} else {
// 子任务
GanttChartResp parentTask = taskMap.get(task.getParentTaskId());
if (parentTask != null) {
task.setLevel(parentTask.getLevel() + 1);
if (parentTask.getChildren() == null) {
parentTask.setChildren(new ArrayList<>());
}
parentTask.getChildren().add(task);
}
}
}
return rootTasks;
}
/**
* 获取项目开始时间
* 从项目任务中计算最早的计划开始时间作为项目开始时间
*/
private LocalDate getProjectStartDate(String projectId) {
if (StrUtil.isBlank(projectId)) {
return LocalDate.now().minusDays(30); // 默认值
}
// 查询项目中所有任务的计划开始时间取最早的时间
QueryWrapper<ProjectTaskEntity> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("project_id", projectId)
.isNotNull("plan_start_date")
.orderByAsc("plan_start_date")
.last("LIMIT 1");
ProjectTaskEntity earliestTask = projectTaskMapper.selectOne(queryWrapper);
if (earliestTask != null && earliestTask.getPlanStartDate() != null) {
return earliestTask.getPlanStartDate();
}
// 如果没有找到有效的开始时间返回当前时间减去30天作为默认值
return LocalDate.now().minusDays(30);
}
/**
* 获取项目结束时间
* 从项目任务中计算最晚的计划结束时间作为项目结束时间
*/
private LocalDate getProjectEndDate(String projectId) {
if (StrUtil.isBlank(projectId)) {
return LocalDate.now().plusDays(30); // 默认值
}
// 查询项目中所有任务的计划结束时间取最晚的时间
QueryWrapper<ProjectTaskEntity> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("project_id", projectId)
.isNotNull("plan_end_date")
.orderByDesc("plan_end_date")
.last("LIMIT 1");
ProjectTaskEntity latestTask = projectTaskMapper.selectOne(queryWrapper);
if (latestTask != null && latestTask.getPlanEndDate() != null) {
return latestTask.getPlanEndDate();
}
// 如果没有找到有效的结束时间返回当前时间加上30天作为默认值
return LocalDate.now().plusDays(30);
}
/**
* 获取项目时间范围
* 返回项目的开始时间和结束时间
*/
private Map<String, LocalDate> getProjectTimeRange(String projectId) {
Map<String, LocalDate> timeRange = new HashMap<>();
timeRange.put("startDate", getProjectStartDate(projectId));
timeRange.put("endDate", getProjectEndDate(projectId));
return timeRange;
}
/**
* 批量获取项目时间范围
* 通过一次查询获取多个项目的时间范围提高性能
*/
private Map<String, Map<String, LocalDate>> getBatchProjectTimeRanges(Set<String> projectIds) {
Map<String, Map<String, LocalDate>> timeRanges = new HashMap<>();
for (String projectId : projectIds) {
Map<String, LocalDate> timeRange = getProjectTimeRange(projectId);
timeRanges.put(projectId, timeRange);
}
return timeRanges;
}
/**
* 计算任务进度
*/
private Integer calculateProgress(ProjectTaskEntity task) {
if (task.getStatus() == 2) return 100; // 已完成
else if (task.getStatus() == 1) return 50; // 进行中
else return 0; // 未开始
}
/**
* 计算任务持续时间
*/
private Integer calculateDuration(ProjectTaskEntity task) {
if (task.getPlanStartDate() != null && task.getPlanEndDate() != null) {
return (int) ChronoUnit.DAYS.between(task.getPlanStartDate(), task.getPlanEndDate()) + 1;
}
return 1;
}
/**
* 计算任务在时间轴上的位置
*/
private Integer calculatePosition(ProjectTaskEntity task) {
LocalDate projectStart = getProjectStartDate(task.getProjectId());
if (task.getPlanStartDate() != null && projectStart != null) {
return (int) ChronoUnit.DAYS.between(projectStart, task.getPlanStartDate());
}
return 0;
}
/**
* 验证任务时间约束
*/
private boolean validateTaskTimeConstraints(ProjectTaskEntity task,
LocalDate newStartDate,
LocalDate newEndDate) {
// 验证子任务不能早于父任务开始
if (StrUtil.isNotBlank(task.getParentTaskId())) {
ProjectTaskEntity parentTask = projectTaskMapper.selectById(task.getParentTaskId());
if (parentTask != null && newStartDate != null) {
if (newStartDate.isBefore(parentTask.getPlanStartDate())) {
return false;
}
}
}
// 验证结束时间不能早于开始时间
if (newStartDate != null && newEndDate != null) {
if (newEndDate.isBefore(newStartDate)) {
return false;
}
}
return true;
}
/**
* 更新逾期状态
*/
private void updateOverdueStatus(ProjectTaskEntity task) {
if (task.getPlanEndDate() != null &&
task.getPlanEndDate().isBefore(LocalDate.now()) &&
task.getStatus() != 2) {
task.setOverdueStatus(1); // 标记为逾期
} else {
task.setOverdueStatus(0);
}
}
}

View File

@ -0,0 +1,61 @@
package com.dite.znpt.web.controller;
import com.dite.znpt.domain.Result;
import com.dite.znpt.domain.vo.GanttChartReq;
import com.dite.znpt.domain.vo.GanttChartResp;
import com.dite.znpt.service.GanttChartService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
/**
* 甘特图控制器
*/
@Api(tags = "甘特图管理")
@RestController
@RequestMapping("/gantt-chart")
public class GanttChartController {
@Resource
private GanttChartService ganttChartService;
@ApiOperation(value = "获取项目甘特图数据", httpMethod = "GET")
@GetMapping("/data")
public Result<List<GanttChartResp>> getGanttChartData(GanttChartReq req) {
List<GanttChartResp> data = ganttChartService.getGanttChartData(req);
return Result.ok(data);
}
@ApiOperation(value = "获取项目甘特图统计信息", httpMethod = "GET")
@GetMapping("/statistics/{projectId}")
public Result<Object> getGanttChartStatistics(@PathVariable String projectId) {
Object statistics = ganttChartService.getGanttChartStatistics(projectId);
return Result.ok(statistics);
}
@ApiOperation(value = "获取项目甘特图时间轴信息", httpMethod = "GET")
@GetMapping("/timeline/{projectId}")
public Result<Object> getGanttChartTimeline(@PathVariable String projectId) {
Object timeline = ganttChartService.getGanttChartTimeline(projectId);
return Result.ok(timeline);
}
@ApiOperation(value = "更新任务进度", httpMethod = "PUT")
@PutMapping("/progress/{taskId}")
public Result<Boolean> updateTaskProgress(@PathVariable String taskId, @RequestParam Integer progress) {
boolean success = ganttChartService.updateTaskProgress(taskId, progress);
return Result.ok(success);
}
@ApiOperation(value = "拖拽更新任务时间", httpMethod = "PUT")
@PutMapping("/time/{taskId}")
public Result<Boolean> updateTaskTime(@PathVariable String taskId,
@RequestParam(required = false) String startDate,
@RequestParam(required = false) String endDate) {
boolean success = ganttChartService.updateTaskTime(taskId, startDate, endDate);
return Result.ok(success);
}
}