From 3fc52c9af20bd417a59748fa235def32d1f87428 Mon Sep 17 00:00:00 2001 From: liejiu946 Date: Mon, 13 Jul 2026 17:25:20 +0800 Subject: [PATCH] =?UTF-8?q?add:1.=E6=96=B0=E5=A2=9E=E6=8A=A5=E5=8A=9F?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E3=80=822.=E6=96=B0=E5=A2=9E=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=E6=8A=A5=E5=8A=9F=E5=8A=9F=E8=83=BD(=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E5=BD=93=E5=89=8D=E5=8F=82=E4=B8=8E=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E5=92=8C=E4=BB=BB=E5=8A=A1=E8=AF=A6=E6=83=85,=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E6=8A=A5=E5=8A=9F)=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ProjectProgressReportController.java | 38 ++ .../service/ProjectProgressReportService.java | 17 + .../ProjectProgressReportServiceImpl.java | 324 ++++++++++++++++++ pms_vue/src/router/index.js | 11 + .../src/views/project/progressReportApi.js | 25 ++ .../src/views/project/progressReportIndex.vue | 242 +++++++++++++ 6 files changed, 657 insertions(+) create mode 100644 pms/src/main/java/org/nl/pms/projectProgressReport/controller/ProjectProgressReportController.java create mode 100644 pms/src/main/java/org/nl/pms/projectProgressReport/service/ProjectProgressReportService.java create mode 100644 pms/src/main/java/org/nl/pms/projectProgressReport/service/impl/ProjectProgressReportServiceImpl.java create mode 100644 pms_vue/src/views/project/progressReportApi.js create mode 100644 pms_vue/src/views/project/progressReportIndex.vue diff --git a/pms/src/main/java/org/nl/pms/projectProgressReport/controller/ProjectProgressReportController.java b/pms/src/main/java/org/nl/pms/projectProgressReport/controller/ProjectProgressReportController.java new file mode 100644 index 0000000..8ad005f --- /dev/null +++ b/pms/src/main/java/org/nl/pms/projectProgressReport/controller/ProjectProgressReportController.java @@ -0,0 +1,38 @@ +package org.nl.pms.projectProgressReport.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.nl.pms.project.vo.ApiResponse; +import org.nl.pms.projectProgressReport.dto.ProjectProgressReportCreateDTO; +import org.nl.pms.projectProgressReport.service.ProjectProgressReportService; +import org.nl.pms.projectProgressReport.vo.ProjectProgressReportDetailVO; +import org.nl.pms.projectProgressReport.vo.ProjectProgressTaskListVO; +import org.springframework.web.bind.annotation.*; + +/** + * @author liejiu + */ +@RestController +@RequestMapping("/api/project-progress-report") +@RequiredArgsConstructor +@CrossOrigin +public class ProjectProgressReportController { + + private final ProjectProgressReportService projectProgressReportService; + + @GetMapping("/my-tasks") + public ApiResponse listMyProjectTasks() { + return ApiResponse.success(projectProgressReportService.listMyProjectTasks()); + } + + @GetMapping("/detail/{detailId}") + public ApiResponse getDetailReports(@PathVariable Long detailId) { + return ApiResponse.success(projectProgressReportService.getDetailReports(detailId)); + } + + @PostMapping + public ApiResponse create(@Valid @RequestBody ProjectProgressReportCreateDTO createDTO) { + projectProgressReportService.create(createDTO); + return ApiResponse.success("提交成功", null); + } +} diff --git a/pms/src/main/java/org/nl/pms/projectProgressReport/service/ProjectProgressReportService.java b/pms/src/main/java/org/nl/pms/projectProgressReport/service/ProjectProgressReportService.java new file mode 100644 index 0000000..7622959 --- /dev/null +++ b/pms/src/main/java/org/nl/pms/projectProgressReport/service/ProjectProgressReportService.java @@ -0,0 +1,17 @@ +package org.nl.pms.projectProgressReport.service; + +import org.nl.pms.projectProgressReport.dto.ProjectProgressReportCreateDTO; +import org.nl.pms.projectProgressReport.vo.ProjectProgressReportDetailVO; +import org.nl.pms.projectProgressReport.vo.ProjectProgressTaskListVO; + +/** + * @author liejiu + */ +public interface ProjectProgressReportService { + + ProjectProgressTaskListVO listMyProjectTasks(); + + ProjectProgressReportDetailVO getDetailReports(Long detailId); + + void create(ProjectProgressReportCreateDTO createDTO); +} diff --git a/pms/src/main/java/org/nl/pms/projectProgressReport/service/impl/ProjectProgressReportServiceImpl.java b/pms/src/main/java/org/nl/pms/projectProgressReport/service/impl/ProjectProgressReportServiceImpl.java new file mode 100644 index 0000000..a7bceaa --- /dev/null +++ b/pms/src/main/java/org/nl/pms/projectProgressReport/service/impl/ProjectProgressReportServiceImpl.java @@ -0,0 +1,324 @@ +package org.nl.pms.projectProgressReport.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import org.nl.pms.common.exception.BadRequestException; +import org.nl.pms.common.utils.SecurityUtils; +import org.nl.pms.project.entity.Project; +import org.nl.pms.project.mapper.ProjectMapper; +import org.nl.pms.projectPlan.entity.ProjectPlanDetail; +import org.nl.pms.projectPlan.entity.ProjectPlanMaster; +import org.nl.pms.projectPlan.mapper.ProjectPlanDetailMapper; +import org.nl.pms.projectPlan.mapper.ProjectPlanMasterMapper; +import org.nl.pms.projectProgressReport.dto.ProjectProgressReportCreateDTO; +import org.nl.pms.projectProgressReport.entity.ProjectProgressReport; +import org.nl.pms.projectProgressReport.mapper.ProjectProgressReportMapper; +import org.nl.pms.projectProgressReport.service.ProjectProgressReportService; +import org.nl.pms.projectProgressReport.vo.ProjectProgressReportDetailVO; +import org.nl.pms.projectProgressReport.vo.ProjectProgressReportVO; +import org.nl.pms.projectProgressReport.vo.ProjectProgressTaskListVO; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * @author liejiu + */ +@Service +@RequiredArgsConstructor +public class ProjectProgressReportServiceImpl implements ProjectProgressReportService { + private final ProjectMapper projectMapper; + private final ProjectPlanMasterMapper masterMapper; + private final ProjectPlanDetailMapper detailMapper; + private final ProjectProgressReportMapper reportMapper; + private final ObjectMapper objectMapper; + + @Override + public ProjectProgressTaskListVO listMyProjectTasks() { + String user = userName(); + List mine = detailMapper.selectList(new LambdaQueryWrapper()).stream() + .filter(i -> members(i.getTaskMembers()).contains(user)).toList(); + ProjectProgressTaskListVO vo = new ProjectProgressTaskListVO(); + if (mine.isEmpty()) { + return vo; + } + Map map = new LinkedHashMap<>(); + Map> reportMap = reportMap(mine.stream().map(ProjectPlanDetail::getId).toList()); + for (ProjectPlanDetail d : mine) { + Project p = projectMapper.selectById(d.getProjectId()); + ProjectPlanMaster m = masterMapper.selectById(d.getMasterId()); + if (p == null || m == null) { + continue; + } + ProjectProgressTaskListVO.ProjectTaskVO pvo = map.computeIfAbsent(p.getId(), k -> projectVO(p)); + pvo.getDetails().add(detailVO(d, m, reportMap.getOrDefault(d.getId(), Collections.emptyList()))); + pvo.setDetailCount(pvo.getDetails().size()); + } + vo.setProjects(new ArrayList<>(map.values())); + return vo; + } + + @Override + public ProjectProgressReportDetailVO getDetailReports(Long detailId) { + ProjectPlanDetail d = detail(detailId); + checkMember(d); + Project p = projectMapper.selectById(d.getProjectId()); + ProjectPlanMaster m = masterMapper.selectById(d.getMasterId()); + if (p == null || m == null) { + throw new BadRequestException("任务关联项目数据不存在"); + } + List rs = reports(detailId); + ProjectProgressReportDetailVO vo = new ProjectProgressReportDetailVO(); + vo.setDetailId(d.getId()); + vo.setMasterId(d.getMasterId()); + vo.setProjectId(d.getProjectId()); + vo.setProjectCode(p.getProjectCode()); + vo.setProjectName(p.getProjectName()); + vo.setMasterName(m.getMasterName()); + vo.setSubItemName(m.getSubItemName()); + vo.setDetailSeq(d.getDetailSeq()); + vo.setDetailName(d.getDetailName()); + vo.setDetailDescription(d.getDetailDescription()); + vo.setDevDays(d.getDevDays()); + vo.setDevStartDate(d.getDevStartDate()); + vo.setDevEndDate(d.getDevEndDate()); + vo.setTaskMembers(members(d.getTaskMembers())); + vo.setStatus(d.getStatus()); + vo.setStatusLabel(taskStatus(d.getStatus())); + vo.setPriority(d.getPriority()); + vo.setPriorityLabel(priority(d.getPriority())); + vo.setProgress(d.getProgress()); + vo.setProgressId(d.getProgressId()); + vo.setRemark(d.getRemark()); + vo.setCanCreateReport(canCreate(rs)); + vo.setCanCreateReportReason(reason(rs)); + vo.setReports(rs.stream().map(this::reportVO).toList()); + return vo; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void create(ProjectProgressReportCreateDTO dto) { + ProjectPlanDetail d = detail(dto.getDetailId()); + checkMember(d); + validate(dto, d); + List rs = reports(dto.getDetailId()); + if (!canCreate(rs)) { + throw new BadRequestException(reason(rs)); + } + int current = d.getProgress() == null ? 0 : d.getProgress(); + if (current != dto.getProgressBefore()) { + throw new BadRequestException("报功前进度与当前任务进度不一致,请刷新后重试"); + } + ProjectProgressReport r = new ProjectProgressReport(); + r.setDetailId(d.getId()); + r.setProjectId(d.getProjectId()); + r.setReportDate(dto.getReportDate()); + r.setWorkHours(dto.getWorkHours().setScale(1, RoundingMode.HALF_UP)); + r.setProgressBefore(dto.getProgressBefore()); + r.setProgressIncrement(dto.getProgressIncrement()); + r.setWorkContent(dto.getWorkContent()); + r.setRemainingWork(dto.getRemainingWork()); + r.setIsOvertime(dto.getIsOvertime()); + r.setStatus(1); + r.setCreatedName(userName()); + r.setCreatedTime(LocalDateTime.now()); + r.setUpdatedTime(LocalDateTime.now()); + reportMapper.insert(r); + int after = dto.getProgressBefore() + dto.getProgressIncrement(); + d.setProgress(after); + if (after >= 100) { + d.setStatus(3); + } else if (after > 0 && (d.getStatus() == null || d.getStatus() == 1)) { + d.setStatus(2); + } + detailMapper.updateById(d); + } + + private ProjectPlanDetail detail(Long id) { + ProjectPlanDetail d = detailMapper.selectById(id); + if (d == null) { + throw new BadRequestException("任务明细不存在"); + } + return d; + } + + private List reports(Long detailId) { + return reportMapper.selectList(new LambdaQueryWrapper() + .eq(ProjectProgressReport::getDetailId, detailId) + .orderByDesc(ProjectProgressReport::getCreatedTime) + .orderByDesc(ProjectProgressReport::getId)); + } + + private void checkMember(ProjectPlanDetail d) { + if (!members(d.getTaskMembers()).contains(userName())) { + throw new BadRequestException("您未参与该任务,无法操作报功数据"); + } + } + + private void validate(ProjectProgressReportCreateDTO dto, ProjectPlanDetail d) { + if (dto.getWorkHours().compareTo(BigDecimal.ZERO) <= 0) { + throw new BadRequestException("报功工时必须大于0"); + } + if (dto.getWorkHours().compareTo(new BigDecimal("24")) > 0) { + throw new BadRequestException("报功工时不能大于24小时"); + } + if (dto.getWorkHours().multiply(new BigDecimal("10")).stripTrailingZeros().scale() > 0) { + throw new BadRequestException("报功工时仅支持0.1小时精度"); + } + if (dto.getIsOvertime() == null || (dto.getIsOvertime() != 0 && dto.getIsOvertime() != 1)) { + throw new BadRequestException("是否加班取值错误"); + } + if (dto.getProgressBefore() + dto.getProgressIncrement() > 100) { + throw new BadRequestException("进度累计不能超过100"); + } + if (d.getStatus() != null && d.getStatus() == 3) { + throw new BadRequestException("当前任务已完成,不能继续报功"); + } + } + + private Map> reportMap(List detailIds) { + if (detailIds.isEmpty()) { + return Collections.emptyMap(); + } + Map> map = new LinkedHashMap<>(); + for (ProjectProgressReport i : reportMapper.selectList(new LambdaQueryWrapper().in(ProjectProgressReport::getDetailId, detailIds).orderByDesc(ProjectProgressReport::getCreatedTime).orderByDesc(ProjectProgressReport::getId))) { + map.computeIfAbsent(i.getDetailId(), k -> new ArrayList<>()).add(i); + } + return map; + } + + private ProjectProgressTaskListVO.ProjectTaskVO projectVO(Project p) { + ProjectProgressTaskListVO.ProjectTaskVO vo = new ProjectProgressTaskListVO.ProjectTaskVO(); + vo.setProjectId(p.getId()); + vo.setProjectCode(p.getProjectCode()); + vo.setProjectName(p.getProjectName()); + vo.setProjectType(p.getProjectType()); + vo.setDevelopLeader(p.getDevelopLeader()); + vo.setProjectLeader(p.getProjectLeader()); + vo.setDetailCount(0); return vo; + } + + private ProjectProgressTaskListVO.TaskDetailVO detailVO(ProjectPlanDetail d, ProjectPlanMaster m, List rs) { + ProjectProgressTaskListVO.TaskDetailVO vo = new ProjectProgressTaskListVO.TaskDetailVO(); + vo.setDetailId(d.getId()); vo.setMasterId(d.getMasterId()); + vo.setMasterName(m.getMasterName()); + vo.setSubItemName(m.getSubItemName()); + vo.setDetailSeq(d.getDetailSeq()); + vo.setDetailName(d.getDetailName()); + vo.setDetailDescription(d.getDetailDescription()); + vo.setDevDays(d.getDevDays()); + vo.setDevStartDate(d.getDevStartDate()); + vo.setDevEndDate(d.getDevEndDate()); + vo.setTaskMembers(members(d.getTaskMembers())); + vo.setStatus(d.getStatus()); + vo.setStatusLabel(taskStatus(d.getStatus())); + vo.setPriority(d.getPriority()); + vo.setPriorityLabel(priority(d.getPriority())); + vo.setProgress(d.getProgress()); + vo.setProgressId(d.getProgressId()); + vo.setRemark(d.getRemark()); + vo.setCanCreateReport(canCreate(rs)); + vo.setCanCreateReportReason(reason(rs)); + vo.setReportCount(rs.size()); + return vo; + } + + private ProjectProgressReportVO reportVO(ProjectProgressReport r) { + ProjectProgressReportVO vo = new ProjectProgressReportVO(); + vo.setId(r.getId()); vo.setDetailId(r.getDetailId()); + vo.setProjectId(r.getProjectId()); + vo.setReportDate(r.getReportDate()); + vo.setWorkHours(r.getWorkHours()); + vo.setProgressBefore(r.getProgressBefore()); + vo.setProgressIncrement(r.getProgressIncrement()); + vo.setProgressAfter((r.getProgressBefore() == null ? 0 : r.getProgressBefore()) + (r.getProgressIncrement() == null ? 0 : r.getProgressIncrement())); + vo.setWorkContent(r.getWorkContent()); vo.setRemainingWork(r.getRemainingWork()); + vo.setIsOvertime(r.getIsOvertime()); + vo.setIsOvertimeLabel(r.getIsOvertime() != null && r.getIsOvertime() == 1 ? "是" : "否"); + vo.setStatus(r.getStatus()); + vo.setStatusLabel(reportStatus(r.getStatus())); + vo.setCreatedName(r.getCreatedName()); + vo.setCreatedTime(r.getCreatedTime()); + vo.setUpdatedTime(r.getUpdatedTime()); + return vo; + } + + private boolean canCreate(List rs) { + return rs == null || rs.isEmpty() || (rs.get(0).getStatus() != null && rs.get(0).getStatus() == 2); + } + + private String reason(List rs) { + return canCreate(rs) ? "可以新增报功" : "上一条报功审核未结束,暂不能发起当前报功"; + } + + private List members(String json) { + if (!StringUtils.hasText(json)) { + return Collections.emptyList(); + } + try { + return objectMapper.readValue(json, new TypeReference>() {}); + } catch (Exception e) { + throw new BadRequestException("任务成员数据解析失败"); + } + } + + private String userName() { + String n = SecurityUtils.getCurrentNickName(); + if (!StringUtils.hasText(n)) { + n = SecurityUtils.getCurrentUsername(); + } + if (!StringUtils.hasText(n)) { + throw new BadRequestException("未获取到当前登录用户信息"); + } + return n; + } + + private String taskStatus(Integer s) { + if (ObjectUtil.isEmpty(s)) { + return "未知"; + } + return switch (s) { + case 1 -> "待开始"; + case 2 -> "进行中"; + case 3 -> "已完成"; + case 4 -> "阻塞/延期"; + default -> "未知"; + }; + } + + private String priority(Integer p) { + if (ObjectUtil.isEmpty(p)) { + return "普通"; + } + return switch (p) { + case 1 -> "紧急"; + case 2 -> "普通"; + case 3 -> "低"; + default -> "普通"; + }; + } + + private String reportStatus(Integer s) { + if (ObjectUtil.isEmpty(s)) { + return "未知"; + } + return switch (s) { + case 1 -> "待审核"; + case 2 -> "已确认"; + default -> "未知"; + }; + } +} diff --git a/pms_vue/src/router/index.js b/pms_vue/src/router/index.js index 3da2d12..1fcb4d1 100644 --- a/pms_vue/src/router/index.js +++ b/pms_vue/src/router/index.js @@ -139,6 +139,17 @@ const staticMenuData = [ icon: 'clipboard', noCache: true } + }, + { + name: 'ProgressReport', + path: 'progress-report', + hidden: false, + component: 'project/progressReportIndex', + meta: { + title: '报功', + icon: 'clipboard', + noCache: true + } } ] }, diff --git a/pms_vue/src/views/project/progressReportApi.js b/pms_vue/src/views/project/progressReportApi.js new file mode 100644 index 0000000..76839af --- /dev/null +++ b/pms_vue/src/views/project/progressReportApi.js @@ -0,0 +1,25 @@ +import request from '@/utils/request' + +export function getMyTasks() { + return request({ + url: 'api/project-progress-report/my-tasks', + method: 'get' + }) +} + +export function getReportsByDetail(detailId) { + return request({ + url: `api/project-progress-report/detail/${detailId}`, + method: 'get' + }) +} + +export function addReport(data) { + return request({ + url: 'api/project-progress-report', + method: 'post', + data + }) +} + +export default { getMyTasks, getReportsByDetail, addReport } diff --git a/pms_vue/src/views/project/progressReportIndex.vue b/pms_vue/src/views/project/progressReportIndex.vue new file mode 100644 index 0000000..31e024d --- /dev/null +++ b/pms_vue/src/views/project/progressReportIndex.vue @@ -0,0 +1,242 @@ + + +