yanghao пре 1 недеља
родитељ
комит
13a90ae707
20 измењених фајлова са 950 додато и 1 уклоњено
  1. 5 0
      yudao-module-rd/yudao-module-rd-api/src/main/java/cn/iocoder/yudao/module/rd/enums/ErrorCodeConstant.java
  2. 77 0
      yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/controller/admin/researchProject/ProjectTaskController.java
  3. 59 0
      yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/controller/admin/researchProject/TaskProgressController.java
  4. 35 0
      yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/controller/admin/researchProject/vo/ProjectTaskRespVO.java
  5. 52 0
      yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/controller/admin/researchProject/vo/ProjectTaskSaveReqVO.java
  6. 19 0
      yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/controller/admin/researchProject/vo/TaskProgressBatchUpdateReqVO.java
  7. 36 0
      yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/controller/admin/researchProject/vo/TaskProgressPageReqVO.java
  8. 27 0
      yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/controller/admin/researchProject/vo/TaskProgressRespVO.java
  9. 40 0
      yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/controller/admin/researchProject/vo/TaskProgressSaveReqVO.java
  10. 18 1
      yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/dal/dataobject/researchProject/ProjectDO.java
  11. 61 0
      yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/dal/dataobject/researchProject/ProjectTaskDO.java
  12. 58 0
      yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/dal/dataobject/researchProject/TaskProgressDO.java
  13. 39 0
      yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/dal/dataobject/researchProject/TaskReminderDO.java
  14. 37 0
      yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/dal/mysql/researchProject/ProjectTaskMapper.java
  15. 31 0
      yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/dal/mysql/researchProject/TaskProgressMapper.java
  16. 20 0
      yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/dal/mysql/researchProject/TaskReminderMapper.java
  17. 29 0
      yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/service/researchProject/ProjectTaskService.java
  18. 187 0
      yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/service/researchProject/ProjectTaskServiceImpl.java
  19. 23 0
      yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/service/researchProject/TaskProgressService.java
  20. 97 0
      yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/service/researchProject/TaskProgressServiceImpl.java

+ 5 - 0
yudao-module-rd/yudao-module-rd-api/src/main/java/cn/iocoder/yudao/module/rd/enums/ErrorCodeConstant.java

@@ -40,4 +40,9 @@ public interface ErrorCodeConstant {
     ErrorCode PROJECT_APPROVER_NOT_FOUND = new ErrorCode(1011, "未找到审批人,请联系管理员");
     ErrorCode PROJECT_APPROVER_NOT_FOUND = new ErrorCode(1011, "未找到审批人,请联系管理员");
     ErrorCode PROJECT_APPLICATION_FILE_REQUIRED = new ErrorCode(1012, "请上传立项申请表");
     ErrorCode PROJECT_APPLICATION_FILE_REQUIRED = new ErrorCode(1012, "请上传立项申请表");
     ErrorCode PROJECT_OPENING_REPORT_REQUIRED = new ErrorCode(1013, "请上传开题报告");
     ErrorCode PROJECT_OPENING_REPORT_REQUIRED = new ErrorCode(1013, "请上传开题报告");
+
+    // 科研项目进度相关
+    ErrorCode TASK_NOT_EXISTS = new ErrorCode(1014, "任务不存在");
+    ErrorCode TASK_PARENT_NOT_EXISTS = new ErrorCode(1015, "父任务不存在");
+    ErrorCode TASK_LEVEL_EXCEED = new ErrorCode(1016, "任务分解最多支持3级");
 }
 }

+ 77 - 0
yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/controller/admin/researchProject/ProjectTaskController.java

@@ -0,0 +1,77 @@
+package cn.iocoder.yudao.module.rd.controller.admin.researchProject;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.module.rd.controller.admin.researchProject.vo.ProjectTaskRespVO;
+import cn.iocoder.yudao.module.rd.controller.admin.researchProject.vo.ProjectTaskSaveReqVO;
+import cn.iocoder.yudao.module.rd.service.researchProject.ProjectTaskService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - 项目任务")
+@RestController
+@RequestMapping("/rd/project-task")
+@Validated
+public class ProjectTaskController {
+
+    @Resource
+    private ProjectTaskService projectTaskService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建任务")
+    @PreAuthorize("@ss.hasPermission('rd:project-task:create')")
+    public CommonResult<Long> createTask(@Valid @RequestBody ProjectTaskSaveReqVO createReqVO) {
+        return success(projectTaskService.createTask(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新任务")
+    @PreAuthorize("@ss.hasPermission('rd:project-task:update')")
+    public CommonResult<Boolean> updateTask(@Valid @RequestBody ProjectTaskSaveReqVO updateReqVO) {
+        projectTaskService.updateTask(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除任务(含子任务)")
+    @Parameter(name = "id", required = true)
+    @PreAuthorize("@ss.hasPermission('rd:project-task:delete')")
+    public CommonResult<Boolean> deleteTask(@RequestParam("id") Long id) {
+        projectTaskService.deleteTask(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得任务详情")
+    @Parameter(name = "id", required = true)
+    @PreAuthorize("@ss.hasPermission('rd:project-task:query')")
+    public CommonResult<ProjectTaskRespVO> getTask(@RequestParam("id") Long id) {
+        return success(projectTaskService.getTask(id));
+    }
+
+    @GetMapping("/list-by-project")
+    @Operation(summary = "查询项目的任务列表(扁平)")
+    @Parameter(name = "projectId", required = true)
+    @PreAuthorize("@ss.hasPermission('rd:project-task:query')")
+    public CommonResult<List<ProjectTaskRespVO>> listByProject(@RequestParam("projectId") Long projectId) {
+        return success(projectTaskService.getTaskListByProjectId(projectId));
+    }
+
+    @GetMapping("/tree-by-project")
+    @Operation(summary = "查询项目的任务树")
+    @Parameter(name = "projectId", required = true)
+    @PreAuthorize("@ss.hasPermission('rd:project-task:query')")
+    public CommonResult<List<ProjectTaskRespVO>> treeByProject(@RequestParam("projectId") Long projectId) {
+        return success(projectTaskService.getTaskTreeByProjectId(projectId));
+    }
+
+}

+ 59 - 0
yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/controller/admin/researchProject/TaskProgressController.java

@@ -0,0 +1,59 @@
+package cn.iocoder.yudao.module.rd.controller.admin.researchProject;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.rd.controller.admin.researchProject.vo.*;
+import cn.iocoder.yudao.module.rd.service.researchProject.TaskProgressService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - 任务进度")
+@RestController
+@RequestMapping("/rd/task-progress")
+@Validated
+public class TaskProgressController {
+
+    @Resource
+    private TaskProgressService progressService;
+
+    @PostMapping("/create")
+    @Operation(summary = "录入进度")
+    @PreAuthorize("@ss.hasPermission('rd:task-progress:create')")
+    public CommonResult<Long> create(@Valid @RequestBody TaskProgressSaveReqVO reqVO) {
+        return success(progressService.createProgress(reqVO));
+    }
+
+    @PostMapping("/batch-create")
+    @Operation(summary = "批量录入进度")
+    @PreAuthorize("@ss.hasPermission('rd:task-progress:create')")
+    public CommonResult<Boolean> batchCreate(@Valid @RequestBody TaskProgressBatchUpdateReqVO batchReqVO) {
+        progressService.batchCreateProgress(batchReqVO);
+        return success(true);
+    }
+
+    @GetMapping("/list-by-task")
+    @Operation(summary = "查询任务的进度历史")
+    @Parameter(name = "taskId", required = true)
+    @PreAuthorize("@ss.hasPermission('rd:task-progress:query')")
+    public CommonResult<List<TaskProgressRespVO>> listByTask(@RequestParam("taskId") Long taskId) {
+        return success(progressService.getProgressListByTaskId(taskId));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "分页查询进度")
+    @PreAuthorize("@ss.hasPermission('rd:task-progress:query')")
+    public CommonResult<PageResult<TaskProgressRespVO>> page(@Valid TaskProgressPageReqVO pageReqVO) {
+        return success(progressService.getProgressPage(pageReqVO));
+    }
+
+}

+ 35 - 0
yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/controller/admin/researchProject/vo/ProjectTaskRespVO.java

@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.module.rd.controller.admin.researchProject.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Schema(description = "管理后台 - 项目任务 Response VO")
+@Data
+public class ProjectTaskRespVO {
+
+    private Long id;
+    private Long projectId;
+    private Long parentId;
+    private Integer level;
+    private String taskName;
+    private String taskDesc;
+    private Integer priority;
+    private Long ownerId;
+    private String ownerName;
+    private LocalDate planStartTime;
+    private LocalDate planEndTime;
+    private LocalDate actualStartTime;
+    private LocalDate actualEndTime;
+    private Integer status;
+    private Integer progress;
+    private Integer sort;
+    private String remark;
+    private LocalDateTime createTime;
+
+    /** 子任务(树形结构) */
+    private List<ProjectTaskRespVO> children;
+}

+ 52 - 0
yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/controller/admin/researchProject/vo/ProjectTaskSaveReqVO.java

@@ -0,0 +1,52 @@
+package cn.iocoder.yudao.module.rd.controller.admin.researchProject.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import java.time.LocalDate;
+
+@Schema(description = "管理后台 - 项目任务 新增/修改 Request VO")
+@Data
+public class ProjectTaskSaveReqVO {
+
+    @Schema(description = "主键ID")
+    private Long id;
+
+    @Schema(description = "所属项目ID", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "项目ID不能为空")
+    private Long projectId;
+
+    @Schema(description = "父任务ID(0=顶层)", example = "0")
+    private Long parentId;
+
+    @Schema(description = "任务名称", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotEmpty(message = "任务名称不能为空")
+    private String taskName;
+
+    @Schema(description = "任务描述")
+    private String taskDesc;
+
+    @Schema(description = "优先级:1高 2中 3低", example = "2")
+    private Integer priority;
+
+    @Schema(description = "责任人ID")
+    private Long ownerId;
+
+    @Schema(description = "责任人姓名")
+    private String ownerName;
+
+    @Schema(description = "计划开始时间")
+    private LocalDate planStartTime;
+
+    @Schema(description = "计划结束时间")
+    private LocalDate planEndTime;
+
+    @Schema(description = "排序")
+    private Integer sort;
+
+    @Schema(description = "备注")
+    private String remark;
+
+}

+ 19 - 0
yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/controller/admin/researchProject/vo/TaskProgressBatchUpdateReqVO.java

@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.rd.controller.admin.researchProject.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotEmpty;
+import java.util.List;
+
+@Schema(description = "管理后台 - 任务进度 批量更新 Request VO")
+@Data
+public class TaskProgressBatchUpdateReqVO {
+
+    @Schema(description = "批量进度列表", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotEmpty(message = "进度列表不能为空")
+    @Valid
+    private List<TaskProgressSaveReqVO> list;
+
+}

+ 36 - 0
yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/controller/admin/researchProject/vo/TaskProgressPageReqVO.java

@@ -0,0 +1,36 @@
+package cn.iocoder.yudao.module.rd.controller.admin.researchProject.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - 任务进度 分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class TaskProgressPageReqVO extends PageParam {
+
+    @Schema(description = "项目ID")
+    private Long projectId;
+
+    @Schema(description = "任务ID")
+    private Long taskId;
+
+    @Schema(description = "汇报人ID")
+    private Long reporterId;
+
+    @Schema(description = "进度状态")
+    private Integer status;
+
+    @Schema(description = "汇报时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] reportTime;
+
+}

+ 27 - 0
yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/controller/admin/researchProject/vo/TaskProgressRespVO.java

@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.module.rd.controller.admin.researchProject.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Schema(description = "管理后台 - 任务进度 Response VO")
+@Data
+public class TaskProgressRespVO {
+
+    private Long id;
+    private Long taskId;
+    private Long projectId;
+    private Integer status;
+    private Integer progress;
+    private String completedWorkload;
+    private String remainingWorkload;
+    private String delayReason;
+    private String progressDesc;
+    private List<ProjectAttachmentVO> attachments;
+    private Long reporterId;
+    private String reporterName;
+    private LocalDateTime reportTime;
+
+}

+ 40 - 0
yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/controller/admin/researchProject/vo/TaskProgressSaveReqVO.java

@@ -0,0 +1,40 @@
+package cn.iocoder.yudao.module.rd.controller.admin.researchProject.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+@Schema(description = "管理后台 - 任务进度 录入 Request VO")
+@Data
+public class TaskProgressSaveReqVO {
+
+    @Schema(description = "任务ID", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "任务ID不能为空")
+    private Long taskId;
+
+    @Schema(description = "进度状态:0未开始 1进行中 2已完成 3延期", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "进度状态不能为空")
+    private Integer status;
+
+    @Schema(description = "进度百分比(0-100)", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "进度百分比不能为空")
+    private Integer progress;
+
+    @Schema(description = "已完成工作量")
+    private String completedWorkload;
+
+    @Schema(description = "剩余工作量")
+    private String remainingWorkload;
+
+    @Schema(description = "延期原因")
+    private String delayReason;
+
+    @Schema(description = "进度详情描述")
+    private String progressDesc;
+
+    @Schema(description = "佐证材料")
+    private List<ProjectAttachmentVO> attachments;
+
+}

+ 18 - 1
yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/dal/dataobject/researchProject/ProjectDO.java

@@ -5,7 +5,8 @@ import com.baomidou.mybatisplus.annotation.KeySequence;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.*;
 import lombok.*;
-
+import java.math.BigDecimal;
+import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalDateTime;
 import java.util.List;
 import java.util.List;
 import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableField;
@@ -116,4 +117,20 @@ public class ProjectDO extends BaseDO {
     /** 是否涉及预算:0否 1是 */
     /** 是否涉及预算:0否 1是 */
     private Integer involveBudget;
     private Integer involveBudget;
 
 
+    // ==================== 任务进度 ====================
+    /** 任务总数 */
+    private Integer taskTotal;
+
+    /** 已完成任务数 */
+    private Integer taskCompleted;
+
+    /** 整体进度(%) */
+    private BigDecimal progressRate;
+
+    /** 计划开始时间 */
+    private LocalDate planStartDate;
+
+    /** 计划结束时间 */
+    private LocalDate planEndDate;
+
 }
 }

+ 61 - 0
yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/dal/dataobject/researchProject/ProjectTaskDO.java

@@ -0,0 +1,61 @@
+package cn.iocoder.yudao.module.rd.dal.dataobject.researchProject;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.time.LocalDate;
+
+/**
+ * 项目任务 DO
+ *
+ * @author ruiqi
+ */
+@TableName(value = "rd_project_task", autoResultMap = true)
+@KeySequence("rd_project_task_seq")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ProjectTaskDO extends BaseDO {
+
+    @TableId
+    private Long id;
+    /** 所属项目ID */
+    private Long projectId;
+    /** 父任务ID(0=顶层任务) */
+    private Long parentId;
+    /** 层级:1/2/3(最多3级) */
+    private Integer level;
+    /** 任务名称 */
+    private String taskName;
+    /** 任务描述 */
+    private String taskDesc;
+    /** 优先级:1高 2中 3低 */
+    private Integer priority;
+    /** 责任人ID */
+    private Long ownerId;
+    /** 责任人姓名 */
+    private String ownerName;
+    /** 计划开始时间 */
+    private LocalDate planStartTime;
+    /** 计划结束时间 */
+    private LocalDate planEndTime;
+    /** 实际开始时间 */
+    private LocalDate actualStartTime;
+    /** 实际完成时间 */
+    private LocalDate actualEndTime;
+    /** 状态:0未开始 1进行中 2已完成 3延期 4已取消 */
+    private Integer status;
+    /** 进度百分比(0-100) */
+    private Integer progress;
+    /** 排序 */
+    private Integer sort;
+    /** 备注 */
+    private String remark;
+
+}

+ 58 - 0
yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/dal/dataobject/researchProject/TaskProgressDO.java

@@ -0,0 +1,58 @@
+package cn.iocoder.yudao.module.rd.dal.dataobject.researchProject;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.rd.controller.admin.researchProject.vo.ProjectAttachmentVO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
+import lombok.*;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 任务进度记录 DO
+ *
+ * @author ruiqi
+ */
+@TableName(value = "rd_task_progress", autoResultMap = true)
+@KeySequence("rd_task_progress_seq")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class TaskProgressDO extends BaseDO {
+
+    @TableId
+    private Long id;
+    /** 任务ID */
+    private Long taskId;
+    /** 项目ID(冗余) */
+    private Long projectId;
+    /** 进度状态:0未开始 1进行中 2已完成 3延期 */
+    private Integer status;
+    /** 进度百分比(0-100) */
+    private Integer progress;
+    /** 已完成工作量 */
+    private String completedWorkload;
+    /** 剩余工作量 */
+    private String remainingWorkload;
+    /** 延期原因 */
+    private String delayReason;
+    /** 进度详情描述 */
+    private String progressDesc;
+    /** 佐证材料(JSON数组) */
+    @TableField(typeHandler = JacksonTypeHandler.class)
+    private List<ProjectAttachmentVO> attachments;
+    /** 汇报人ID */
+    private Long reporterId;
+    /** 汇报人姓名 */
+    private String reporterName;
+    /** 汇报时间 */
+    private LocalDateTime reportTime;
+
+}

+ 39 - 0
yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/dal/dataobject/researchProject/TaskReminderDO.java

@@ -0,0 +1,39 @@
+package cn.iocoder.yudao.module.rd.dal.dataobject.researchProject;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.time.LocalDateTime;
+
+/**
+ * 任务进度提醒 DO
+ *
+ * @author ruiqi
+ */
+@TableName(value = "rd_task_reminder", autoResultMap = true)
+@KeySequence("rd_task_reminder_seq")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class TaskReminderDO extends BaseDO {
+
+    @TableId
+    private Long id;
+    private Long taskId;
+    private Long projectId;
+    private Long receiverId;
+    private String receiverName;
+    private Integer receiverType;
+    private Integer reminderType;
+    private String reminderContent;
+    private LocalDateTime reminderTime;
+    private Integer readStatus;
+    private LocalDateTime readTime;
+
+}

+ 37 - 0
yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/dal/mysql/researchProject/ProjectTaskMapper.java

@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.module.rd.dal.mysql.researchProject;
+
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.rd.dal.dataobject.researchProject.ProjectTaskDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+@Mapper
+public interface ProjectTaskMapper extends BaseMapperX<ProjectTaskDO> {
+
+    default List<ProjectTaskDO> selectListByProjectId(Long projectId) {
+        return selectList(new LambdaQueryWrapperX<ProjectTaskDO>()
+                .eq(ProjectTaskDO::getProjectId, projectId)
+                .orderByAsc(ProjectTaskDO::getLevel)
+                .orderByAsc(ProjectTaskDO::getSort)
+                .orderByAsc(ProjectTaskDO::getId));
+    }
+
+    default List<ProjectTaskDO> selectListByParentId(Long parentId) {
+        return selectList(new LambdaQueryWrapperX<ProjectTaskDO>()
+                .eq(ProjectTaskDO::getParentId, parentId)
+                .orderByAsc(ProjectTaskDO::getSort));
+    }
+
+    default List<ProjectTaskDO> selectListByOwnerId(Long ownerId) {
+        return selectList(new LambdaQueryWrapperX<ProjectTaskDO>()
+                .eq(ProjectTaskDO::getOwnerId, ownerId));
+    }
+
+    default void deleteByProjectId(Long projectId) {
+        delete(new LambdaQueryWrapperX<ProjectTaskDO>()
+                .eq(ProjectTaskDO::getProjectId, projectId));
+    }
+
+}

+ 31 - 0
yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/dal/mysql/researchProject/TaskProgressMapper.java

@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.module.rd.dal.mysql.researchProject;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.rd.controller.admin.researchProject.vo.TaskProgressPageReqVO;
+import cn.iocoder.yudao.module.rd.dal.dataobject.researchProject.TaskProgressDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+@Mapper
+public interface TaskProgressMapper extends BaseMapperX<TaskProgressDO> {
+
+    default List<TaskProgressDO> selectListByTaskId(Long taskId) {
+        return selectList(new LambdaQueryWrapperX<TaskProgressDO>()
+                .eq(TaskProgressDO::getTaskId, taskId)
+                .orderByDesc(TaskProgressDO::getReportTime));
+    }
+
+    default PageResult<TaskProgressDO> selectPage(TaskProgressPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<TaskProgressDO>()
+                .eqIfPresent(TaskProgressDO::getProjectId, reqVO.getProjectId())
+                .eqIfPresent(TaskProgressDO::getTaskId, reqVO.getTaskId())
+                .eqIfPresent(TaskProgressDO::getReporterId, reqVO.getReporterId())
+                .eqIfPresent(TaskProgressDO::getStatus, reqVO.getStatus())
+                .betweenIfPresent(TaskProgressDO::getReportTime, reqVO.getReportTime())
+                .orderByDesc(TaskProgressDO::getId));
+    }
+
+}

+ 20 - 0
yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/dal/mysql/researchProject/TaskReminderMapper.java

@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.module.rd.dal.mysql.researchProject;
+
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.rd.dal.dataobject.researchProject.TaskReminderDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+@Mapper
+public interface TaskReminderMapper extends BaseMapperX<TaskReminderDO> {
+
+    default List<TaskReminderDO> selectUnreadByReceiverId(Long receiverId) {
+        return selectList(new LambdaQueryWrapperX<TaskReminderDO>()
+                .eq(TaskReminderDO::getReceiverId, receiverId)
+                .eq(TaskReminderDO::getReadStatus, 0)
+                .orderByDesc(TaskReminderDO::getReminderTime));
+    }
+
+}

+ 29 - 0
yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/service/researchProject/ProjectTaskService.java

@@ -0,0 +1,29 @@
+package cn.iocoder.yudao.module.rd.service.researchProject;
+
+import cn.iocoder.yudao.module.rd.controller.admin.researchProject.vo.ProjectTaskRespVO;
+import cn.iocoder.yudao.module.rd.controller.admin.researchProject.vo.ProjectTaskSaveReqVO;
+
+import javax.validation.Valid;
+import java.util.List;
+
+public interface ProjectTaskService {
+
+    /** 创建任务 */
+    Long createTask(@Valid ProjectTaskSaveReqVO createReqVO);
+
+    /** 更新任务 */
+    void updateTask(@Valid ProjectTaskSaveReqVO updateReqVO);
+
+    /** 删除任务(含子任务) */
+    void deleteTask(Long id);
+
+    /** 查询单个任务 */
+    ProjectTaskRespVO getTask(Long id);
+
+    /** 查询项目的任务树 */
+    List<ProjectTaskRespVO> getTaskTreeByProjectId(Long projectId);
+
+    /** 查询项目的任务列表(扁平) */
+    List<ProjectTaskRespVO> getTaskListByProjectId(Long projectId);
+
+}

+ 187 - 0
yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/service/researchProject/ProjectTaskServiceImpl.java

@@ -0,0 +1,187 @@
+package cn.iocoder.yudao.module.rd.service.researchProject;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
+import cn.iocoder.yudao.module.rd.controller.admin.researchProject.vo.ProjectTaskRespVO;
+import cn.iocoder.yudao.module.rd.controller.admin.researchProject.vo.ProjectTaskSaveReqVO;
+import cn.iocoder.yudao.module.rd.dal.dataobject.researchProject.ProjectDO;
+import cn.iocoder.yudao.module.rd.dal.dataobject.researchProject.ProjectTaskDO;
+import cn.iocoder.yudao.module.rd.dal.mysql.researchProject.ProjectMapper;
+import cn.iocoder.yudao.module.rd.dal.mysql.researchProject.ProjectTaskMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+import java.util.*;
+import java.util.stream.Collectors;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.rd.enums.ErrorCodeConstant.*;
+
+@Service
+@Validated
+public class ProjectTaskServiceImpl implements ProjectTaskService {
+
+    @Resource
+    private ProjectTaskMapper taskMapper;
+    @Resource
+    private ProjectMapper projectMapper;
+
+    /** 最大层级 */
+    private static final int MAX_LEVEL = 3;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long createTask(ProjectTaskSaveReqVO createReqVO) {
+        // 1. 校验项目存在
+        ProjectDO project = projectMapper.selectById(createReqVO.getProjectId());
+        if (project == null) {
+            throw exception(PROJECT_NOT_EXISTS);
+        }
+
+        // 2. 校验父任务 + 计算层级
+        int level = 1;
+        Long parentId = createReqVO.getParentId();
+        if (parentId != null && parentId != 0) {
+            ProjectTaskDO parent = taskMapper.selectById(parentId);
+            if (parent == null) {
+                throw exception(TASK_PARENT_NOT_EXISTS);
+            }
+            // ⭐ 硬性校验:不能超过 3 级
+            if (parent.getLevel() >= MAX_LEVEL) {
+                throw exception(TASK_LEVEL_EXCEED);
+            }
+            level = parent.getLevel() + 1;
+        } else {
+            parentId = 0L;
+        }
+
+        // 3. 插入
+        ProjectTaskDO task = BeanUtils.toBean(createReqVO, ProjectTaskDO.class);
+        task.setParentId(parentId);
+        task.setLevel(level);
+        task.setStatus(0);        // 未开始
+        task.setProgress(0);
+        task.setSort(task.getSort() == null ? 0 : task.getSort());
+        taskMapper.insert(task);
+
+        // 4. 更新项目任务统计
+        refreshProjectStat(createReqVO.getProjectId());
+
+        return task.getId();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateTask(ProjectTaskSaveReqVO updateReqVO) {
+        ProjectTaskDO exist = taskMapper.selectById(updateReqVO.getId());
+        if (exist == null) {
+            throw exception(TASK_NOT_EXISTS);
+        }
+        ProjectTaskDO updateObj = BeanUtils.toBean(updateReqVO, ProjectTaskDO.class);
+        // ⚠️ 不允许改父任务、层级、状态、进度(由进度录入改)
+        updateObj.setParentId(null);
+        updateObj.setLevel(null);
+        updateObj.setStatus(null);
+        updateObj.setProgress(null);
+        taskMapper.updateById(updateObj);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteTask(Long id) {
+        ProjectTaskDO task = taskMapper.selectById(id);
+        if (task == null) {
+            throw exception(TASK_NOT_EXISTS);
+        }
+        // 递归删除子任务
+        deleteRecursively(id);
+        refreshProjectStat(task.getProjectId());
+    }
+
+    /** 递归删除 */
+    private void deleteRecursively(Long taskId) {
+        List<ProjectTaskDO> children = taskMapper.selectListByParentId(taskId);
+        for (ProjectTaskDO child : children) {
+            deleteRecursively(child.getId());
+        }
+        taskMapper.deleteById(taskId);
+    }
+
+    @Override
+    public ProjectTaskRespVO getTask(Long id) {
+        ProjectTaskDO task = taskMapper.selectById(id);
+        return BeanUtils.toBean(task, ProjectTaskRespVO.class);
+    }
+
+    @Override
+    public List<ProjectTaskRespVO> getTaskListByProjectId(Long projectId) {
+        List<ProjectTaskDO> list = taskMapper.selectListByProjectId(projectId);
+        return BeanUtils.toBean(list, ProjectTaskRespVO.class);
+    }
+
+    @Override
+    public List<ProjectTaskRespVO> getTaskTreeByProjectId(Long projectId) {
+        List<ProjectTaskDO> list = taskMapper.selectListByProjectId(projectId);
+        List<ProjectTaskRespVO> voList = BeanUtils.toBean(list, ProjectTaskRespVO.class);
+        return buildTree(voList);
+    }
+
+    /**
+     * 把扁平列表组装成树
+     */
+    private List<ProjectTaskRespVO> buildTree(List<ProjectTaskRespVO> list) {
+        if (CollUtil.isEmpty(list)) {
+            return Collections.emptyList();
+        }
+        Map<Long, ProjectTaskRespVO> map = list.stream()
+                .collect(Collectors.toMap(ProjectTaskRespVO::getId, v -> v));
+        List<ProjectTaskRespVO> roots = new ArrayList<>();
+        for (ProjectTaskRespVO vo : list) {
+            if (vo.getParentId() == null || vo.getParentId() == 0) {
+                roots.add(vo);
+            } else {
+                ProjectTaskRespVO parent = map.get(vo.getParentId());
+                if (parent != null) {
+                    if (parent.getChildren() == null) {
+                        parent.setChildren(new ArrayList<>());
+                    }
+                    parent.getChildren().add(vo);
+                }
+            }
+        }
+        return roots;
+    }
+
+    /**
+     * 刷新项目任务统计(任务总数、已完成数、整体进度)
+     */
+    public void refreshProjectStat(Long projectId) {
+        List<ProjectTaskDO> tasks = taskMapper.selectListByProjectId(projectId);
+        if (CollUtil.isEmpty(tasks)) {
+            return;
+        }
+        int total = tasks.size();
+        long completed = tasks.stream().filter(t -> t.getStatus() != null && t.getStatus() == 2).count();
+        // 整体进度 = 所有任务进度的平均(只算叶子任务)
+        Set<Long> parentIds = tasks.stream()
+                .map(ProjectTaskDO::getParentId)
+                .filter(pid -> pid != null && pid != 0)
+                .collect(Collectors.toSet());
+        List<ProjectTaskDO> leafTasks = tasks.stream()
+                .filter(t -> !parentIds.contains(t.getId()))
+                .collect(Collectors.toList());
+        double avgProgress = leafTasks.isEmpty() ? 0 :
+                leafTasks.stream().mapToInt(t -> t.getProgress() == null ? 0 : t.getProgress()).average().orElse(0);
+
+        ProjectDO update = new ProjectDO();
+        update.setId(projectId);
+        update.setTaskTotal(total);
+        update.setTaskCompleted((int) completed);
+        update.setProgressRate(java.math.BigDecimal.valueOf(avgProgress).setScale(2, java.math.RoundingMode.HALF_UP));
+        projectMapper.updateById(update);
+    }
+
+}

+ 23 - 0
yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/service/researchProject/TaskProgressService.java

@@ -0,0 +1,23 @@
+package cn.iocoder.yudao.module.rd.service.researchProject;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.rd.controller.admin.researchProject.vo.*;
+
+import javax.validation.Valid;
+import java.util.List;
+
+public interface TaskProgressService {
+
+    /** 录入进度 */
+    Long createProgress(@Valid TaskProgressSaveReqVO createReqVO);
+
+    /** 批量录入进度 */
+    void batchCreateProgress(@Valid TaskProgressBatchUpdateReqVO batchReqVO);
+
+    /** 查询任务的进度历史 */
+    List<TaskProgressRespVO> getProgressListByTaskId(Long taskId);
+
+    /** 分页查询进度 */
+    PageResult<TaskProgressRespVO> getProgressPage(@Valid TaskProgressPageReqVO pageReqVO);
+
+}

+ 97 - 0
yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/service/researchProject/TaskProgressServiceImpl.java

@@ -0,0 +1,97 @@
+package cn.iocoder.yudao.module.rd.service.researchProject;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
+import cn.iocoder.yudao.module.rd.controller.admin.researchProject.vo.*;
+import cn.iocoder.yudao.module.rd.dal.dataobject.researchProject.ProjectTaskDO;
+import cn.iocoder.yudao.module.rd.dal.dataobject.researchProject.TaskProgressDO;
+import cn.iocoder.yudao.module.rd.dal.mysql.researchProject.ProjectTaskMapper;
+import cn.iocoder.yudao.module.rd.dal.mysql.researchProject.TaskProgressMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.rd.enums.ErrorCodeConstant.TASK_NOT_EXISTS;
+
+@Service
+@Validated
+public class TaskProgressServiceImpl implements TaskProgressService {
+
+    @Resource
+    private TaskProgressMapper progressMapper;
+    @Resource
+    private ProjectTaskMapper taskMapper;
+    @Resource
+    private ProjectTaskServiceImpl projectTaskService;   // 复用统计刷新
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long createProgress(TaskProgressSaveReqVO createReqVO) {
+        ProjectTaskDO task = taskMapper.selectById(createReqVO.getTaskId());
+        if (task == null) {
+            throw exception(TASK_NOT_EXISTS);
+        }
+
+        TaskProgressDO progress = BeanUtils.toBean(createReqVO, TaskProgressDO.class);
+        progress.setProjectId(task.getProjectId());
+        progress.setReporterId(SecurityFrameworkUtils.getLoginUserId());
+        progress.setReporterName(SecurityFrameworkUtils.getLoginUserNickname());
+        progress.setReportTime(LocalDateTime.now());
+        progressMapper.insert(progress);
+
+        // ⭐ 同步更新任务的 status / progress / 实际时间
+        updateTaskFromProgress(task, createReqVO);
+
+        // ⭐ 刷新项目整体统计
+        projectTaskService.refreshProjectStat(task.getProjectId());
+
+        return progress.getId();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void batchCreateProgress(TaskProgressBatchUpdateReqVO batchReqVO) {
+        for (TaskProgressSaveReqVO item : batchReqVO.getList()) {
+            createProgress(item);
+        }
+    }
+
+    @Override
+    public List<TaskProgressRespVO> getProgressListByTaskId(Long taskId) {
+        List<TaskProgressDO> list = progressMapper.selectListByTaskId(taskId);
+        return BeanUtils.toBean(list, TaskProgressRespVO.class);
+    }
+
+    @Override
+    public PageResult<TaskProgressRespVO> getProgressPage(TaskProgressPageReqVO pageReqVO) {
+        PageResult<TaskProgressDO> pageResult = progressMapper.selectPage(pageReqVO);
+        return BeanUtils.toBean(pageResult, TaskProgressRespVO.class);
+    }
+
+    /**
+     * 根据进度记录,更新任务本身的状态
+     */
+    private void updateTaskFromProgress(ProjectTaskDO task, TaskProgressSaveReqVO reqVO) {
+        ProjectTaskDO update = new ProjectTaskDO();
+        update.setId(task.getId());
+        update.setStatus(reqVO.getStatus());
+        update.setProgress(reqVO.getProgress());
+        // 第一次进行中 → 记录实际开始时间
+        if (reqVO.getStatus() == 1 && task.getActualStartTime() == null) {
+            update.setActualStartTime(LocalDate.now());
+        }
+        // 已完成 → 记录实际完成时间
+        if (reqVO.getStatus() == 2 && task.getActualEndTime() == null) {
+            update.setActualEndTime(LocalDate.now());
+        }
+        taskMapper.updateById(update);
+    }
+
+}