yanghao пре 6 дана
родитељ
комит
519311053d
16 измењених фајлова са 1246 додато и 1 уклоњено
  1. 18 1
      yudao-module-rd/yudao-module-rd-api/src/main/java/cn/iocoder/yudao/module/rd/enums/ErrorCodeConstant.java
  2. 139 0
      yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/controller/admin/researchProject/ResearchProjectController.java
  3. 23 0
      yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/controller/admin/researchProject/vo/ProjectApproveReqVO.java
  4. 60 0
      yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/controller/admin/researchProject/vo/ProjectPageReqVO.java
  5. 114 0
      yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/controller/admin/researchProject/vo/ProjectRespVO.java
  6. 121 0
      yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/controller/admin/researchProject/vo/ProjectSaveReqVO.java
  7. 40 0
      yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/dal/dataobject/researchProject/ProjectApprovalRecordDO.java
  8. 52 0
      yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/dal/dataobject/researchProject/ProjectBudgetDO.java
  9. 116 0
      yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/dal/dataobject/researchProject/ProjectDO.java
  10. 48 0
      yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/dal/dataobject/researchProject/ProjectScheduleDO.java
  11. 45 0
      yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/dal/mysql/researchProject/ProjectApprovalRecordMapper.java
  12. 35 0
      yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/dal/mysql/researchProject/ProjectBudgetMapper.java
  13. 53 0
      yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/dal/mysql/researchProject/ProjectMapper.java
  14. 35 0
      yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/dal/mysql/researchProject/ProjectScheduleMapper.java
  15. 88 0
      yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/service/researchProject/ResearchProjectService.java
  16. 259 0
      yudao-module-rd/yudao-module-rd-biz/src/main/java/cn/iocoder/yudao/module/rd/service/researchProject/ResearchProjectServiceImpl.java

+ 18 - 1
yudao-module-rd/yudao-module-rd-api/src/main/java/cn/iocoder/yudao/module/rd/enums/ErrorCodeConstant.java

@@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.rd.enums;
 import cn.iocoder.yudao.framework.common.exception.ErrorCode;
 
 public interface ErrorCodeConstant {
-
     ErrorCode TEAM_NOT_EXISTS = new ErrorCode(100, "团队不存在");
     ErrorCode EXPERT_POOL_NOT_EXISTS = new ErrorCode(200, "专家库不存在");
     ErrorCode EXPERT_KEYWORD_NOT_EXISTS = new ErrorCode(201, "专家热词字典不存在");
@@ -23,4 +22,22 @@ public interface ErrorCodeConstant {
      */
     ErrorCode LAS_WELL_NOT_EXISTS = new ErrorCode(1000, "数字油藏LAS数据文件-井信息不存在");
     ErrorCode LAS_WELL_DATA_NOT_EXISTS = new ErrorCode(1001, "数字油藏LAS数据文件-测井数据不存在");
+
+
+    /***
+     * 科研项目
+     */
+
+    ErrorCode PROJECT_NOT_EXISTS = new ErrorCode(1002, "项目立项不存在");
+    ErrorCode PROJECT_ALREADY_SUBMITTED = new ErrorCode(1003, "项目已提交审批,不能重复提交");
+    ErrorCode PROJECT_NOT_IN_APPROVAL = new ErrorCode(1004, "项目不在审批中");
+    ErrorCode PROJECT_NOT_YOUR_TURN = new ErrorCode(1005, "当前不是您的审批节点");
+    ErrorCode PROJECT_PRE_STATUS_ILLEGAL = new ErrorCode(1006, "预审状态不允许此操作");
+    ErrorCode PROJECT_FORMAL_STATUS_ILLEGAL = new ErrorCode(1007, "正式立项状态不允许此操作");
+    ErrorCode PROJECT_PRE_NOT_PASSED = new ErrorCode(1008, "预审未通过,不能提交正式立项");
+    ErrorCode PROJECT_IN_APPROVAL_CANNOT_UPDATE = new ErrorCode(1009, "项目审批中,不能修改");
+    ErrorCode PROJECT_IN_APPROVAL_CANNOT_DELETE = new ErrorCode(1010, "项目审批中,不能删除");
+    ErrorCode PROJECT_APPROVER_NOT_FOUND = new ErrorCode(1011, "未找到审批人,请联系管理员");
+    ErrorCode PROJECT_APPLICATION_FILE_REQUIRED = new ErrorCode(1012, "请上传立项申请表");
+    ErrorCode PROJECT_OPENING_REPORT_REQUIRED = new ErrorCode(1013, "请上传开题报告");
 }

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

@@ -0,0 +1,139 @@
+package cn.iocoder.yudao.module.rd.controller.admin.researchProject;
+
+import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.yudao.module.rd.controller.admin.researchProject.vo.ProjectApproveReqVO;
+import cn.iocoder.yudao.module.rd.controller.admin.researchProject.vo.ProjectPageReqVO;
+import cn.iocoder.yudao.module.rd.controller.admin.researchProject.vo.ProjectRespVO;
+import cn.iocoder.yudao.module.rd.controller.admin.researchProject.vo.ProjectSaveReqVO;
+import cn.iocoder.yudao.module.rd.dal.dataobject.researchProject.ProjectDO;
+import cn.iocoder.yudao.module.rd.service.researchProject.ResearchProjectService;
+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.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
+import java.io.IOException;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - 项目立项")
+@RestController
+@RequestMapping("/rd/research-project")
+@Validated
+public class ResearchProjectController {
+
+    @Resource
+    private ResearchProjectService projectService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建项目立项")
+    @PreAuthorize("@ss.hasPermission('rd:project:create')")
+    public CommonResult<Long> createProject(@Valid @RequestBody ProjectSaveReqVO createReqVO) {
+        return success(projectService.createProject(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新项目立项")
+    @PreAuthorize("@ss.hasPermission('rd:project:update')")
+    public CommonResult<Boolean> updateProject(@Valid @RequestBody ProjectSaveReqVO updateReqVO) {
+        projectService.updateProject(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除项目立项")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('rd:project:delete')")
+    public CommonResult<Boolean> deleteProject(@RequestParam("id") Long id) {
+        projectService.deleteProject(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得项目立项")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('rd:project:query')")
+    public CommonResult<ProjectRespVO> getProject(@RequestParam("id") Long id) {
+        return success(projectService.getProject(id));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得项目立项分页")
+    @PreAuthorize("@ss.hasPermission('rd:project:query')")
+    public CommonResult<PageResult<ProjectRespVO>> getProjectPage(@Valid ProjectPageReqVO pageReqVO) {
+        return success(projectService.getProjectPage(pageReqVO));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出项目立项 Excel")
+    @PreAuthorize("@ss.hasPermission('rd:project:export')")
+    @ApiAccessLog(operateType = EXPORT)
+    public void exportProjectExcel(@Valid ProjectPageReqVO pageReqVO,
+                                   HttpServletResponse response) throws IOException {
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<ProjectDO> list = projectService.getProjectList(pageReqVO);
+        // 导出 Excel
+        ExcelUtils.write(response, "项目立项.xls", "数据", ProjectRespVO.class,
+                BeanUtils.toBean(list, ProjectRespVO.class));
+    }
+
+
+    // ==================== 审批流 ====================
+
+    @PostMapping("/submit-pre")
+    @Operation(summary = "提交预审")
+    @Parameter(name = "id", description = "项目ID", required = true)
+    @PreAuthorize("@ss.hasPermission('rd:project:submit')")
+    public CommonResult<Boolean> submitPre(@RequestParam("id") Long id) {
+        projectService.submitPreApproval(id);
+        return success(true);
+    }
+
+    @PostMapping("/approve-pre")
+    @Operation(summary = "预审审批")
+    @PreAuthorize("@ss.hasPermission('rd:project:approve')")
+    public CommonResult<Boolean> approvePre(@Valid @RequestBody ProjectApproveReqVO reqVO) {
+        projectService.approvePre(reqVO);
+        return success(true);
+    }
+
+    @PostMapping("/submit-formal")
+    @Operation(summary = "提交正式立项")
+    @Parameter(name = "id", description = "项目ID", required = true)
+    @PreAuthorize("@ss.hasPermission('rd:project:submit')")
+    public CommonResult<Boolean> submitFormal(@RequestParam("id") Long id) {
+        projectService.submitFormalApproval(id);
+        return success(true);
+    }
+
+    @PostMapping("/approve-formal")
+    @Operation(summary = "正式立项审批")
+    @PreAuthorize("@ss.hasPermission('rd:project:approve')")
+    public CommonResult<Boolean> approveFormal(@Valid @RequestBody ProjectApproveReqVO reqVO) {
+        projectService.approveFormal(reqVO);
+        return success(true);
+    }
+
+    @PostMapping("/cancel")
+    @Operation(summary = "撤回审批")
+    @Parameter(name = "id", description = "项目ID", required = true)
+    @PreAuthorize("@ss.hasPermission('rd:project:submit')")
+    public CommonResult<Boolean> cancel(@RequestParam("id") Long id) {
+        projectService.cancelApproval(id);
+        return success(true);
+    }
+
+
+}

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

@@ -0,0 +1,23 @@
+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;
+
+@Schema(description = "管理后台 - 项目审批 Request VO")
+@Data
+public class ProjectApproveReqVO {
+
+    @Schema(description = "项目ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @NotNull(message = "项目ID不能为空")
+    private Long projectId;
+
+    @Schema(description = "审批结果:1通过 2驳回", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "审批结果不能为空")
+    private Integer result;
+
+    @Schema(description = "审批意见", example = "同意立项")
+    private String opinion;
+
+}

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

@@ -0,0 +1,60 @@
+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 ProjectPageReqVO extends PageParam {
+
+    @Schema(description = "项目级别", example = "国家级")
+    private String projectLevel;
+
+    @Schema(description = "执行单位", example = "某某研究院")
+    private String executingUnit;
+
+    @Schema(description = "项目名称", example = "智能仓储系统")
+    private String projectName;
+
+    @Schema(description = "项目负责人", example = "张三")
+    private String projectLeader;
+
+    @Schema(description = "项目组成员(多个逗号分隔)")
+    private String teamMembers;
+
+    @Schema(description = "项目类型", example = "研发类")
+    private String projectType;
+
+    @Schema(description = "目标和意义")
+    private String goalAndSignificance;
+
+    @Schema(description = "主要研究内容")
+    private String mainResearchContent;
+
+    @Schema(description = "创新点")
+    private String innovationPoint;
+
+    @Schema(description = "排序值")
+    private Integer sort;
+
+    @Schema(description = "状态(0启用 1禁用)", example = "0")
+    private Integer status;
+
+    @Schema(description = "备注", example = "你说的对")
+    private String remark;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+}

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

@@ -0,0 +1,114 @@
+package cn.iocoder.yudao.module.rd.controller.admin.researchProject.vo;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Schema(description = "管理后台 - 项目立项 Response VO")
+@Data
+public class ProjectRespVO {
+
+    @Schema(description = "主键ID", example = "1024")
+    private Long id;
+
+    @Schema(description = "项目级别", example = "国家级")
+    private String projectLevel;
+
+    @Schema(description = "执行单位", example = "某某研究院")
+    private String executingUnit;
+
+    @Schema(description = "项目名称", example = "智能仓储系统")
+    private String projectName;
+
+    @Schema(description = "项目负责人", example = "张三")
+    private String projectLeader;
+
+    @Schema(description = "项目组成员(多个逗号分隔)", example = "张三,李四,王五")
+    private String teamMembers;
+
+    @Schema(description = "项目类型", example = "研发类")
+    private String projectType;
+
+    @Schema(description = "目标和意义")
+    private String goalAndSignificance;
+
+    @Schema(description = "主要研究内容")
+    private String mainResearchContent;
+
+    @Schema(description = "创新点")
+    private String innovationPoint;
+
+    @Schema(description = "立项申请表文件url")
+    private String applicationFile;
+
+    @Schema(description = "开题报告文件url")
+    private String openingReportFile;
+
+    @Schema(description = "其他文件url")
+    private String otherFile;
+
+    @Schema(description = "排序值", example = "1")
+    private Integer sort;
+
+    @Schema(description = "状态(0启用 1禁用)", example = "0")
+    private Integer status;
+
+    @Schema(description = "备注", example = "你说的对")
+    private String remark;
+
+    @Schema(description = "创建时间")
+    private LocalDateTime createTime;
+
+    // ==================== 子表数据 ====================
+
+    @Schema(description = "进度安排列表")
+    private List<ScheduleVO> scheduleList;
+
+    @Schema(description = "费用预算明细列表")
+    private List<BudgetVO> budgetList;
+
+    // ==================== 子表 VO ====================
+
+    @Schema(description = "管理后台 - 项目进度安排 Response VO")
+    @Data
+    public static class ScheduleVO {
+
+        @Schema(description = "主键ID", example = "1")
+        private Long id;
+
+        @Schema(description = "工作计划", example = "完成需求调研")
+        private String workPlan;
+
+        @Schema(description = "截止时间", example = "2026-10-01")
+        private LocalDate deadline;
+
+        @Schema(description = "排序值", example = "0")
+        private Integer sort;
+
+    }
+
+    @Schema(description = "管理后台 - 项目费用预算 Response VO")
+    @Data
+    public static class BudgetVO {
+
+        @Schema(description = "主键ID", example = "1")
+        private Long id;
+
+        @Schema(description = "财务科目", example = "材料费")
+        private String financialSubject;
+
+        @Schema(description = "预算金额(元)", example = "10000.00")
+        private BigDecimal budgetAmount;
+
+        @Schema(description = "预算说明", example = "用于采购开发板")
+        private String budgetDescription;
+
+        @Schema(description = "排序值", example = "0")
+        private Integer sort;
+
+    }
+
+}

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

@@ -0,0 +1,121 @@
+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 javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+import java.util.List;
+
+@Schema(description = "管理后台 - 项目立项新增/修改 Request VO")
+@Data
+public class ProjectSaveReqVO {
+
+    @Schema(description = "主键ID", example = "1024")
+    private Long id;
+
+    @Schema(description = "项目级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "国家级")
+    @NotEmpty(message = "项目级别不能为空")
+    private String projectLevel;
+
+    @Schema(description = "执行单位", requiredMode = Schema.RequiredMode.REQUIRED, example = "某某研究院")
+    @NotEmpty(message = "执行单位不能为空")
+    private String executingUnit;
+
+    @Schema(description = "项目名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "智能仓储系统")
+    @NotEmpty(message = "项目名称不能为空")
+    @Size(max = 200, message = "项目名称长度不能超过 200 个字符")
+    private String projectName;
+
+    @Schema(description = "项目负责人", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
+    @NotEmpty(message = "项目负责人不能为空")
+    private String projectLeader;
+
+    @Schema(description = "项目组成员(多个逗号分隔)", example = "张三,李四,王五")
+    private String teamMembers;
+
+    @Schema(description = "项目类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "研发类")
+    @NotEmpty(message = "项目类型不能为空")
+    private String projectType;
+
+    @Schema(description = "目标和意义")
+    @Size(max = 500, message = "目标和意义长度不能超过 500 个字符")
+    private String goalAndSignificance;
+
+    @Schema(description = "主要研究内容")
+    @Size(max = 2000, message = "主要研究内容长度不能超过 2000 个字符")
+    private String mainResearchContent;
+
+    @Schema(description = "创新点")
+    @Size(max = 500, message = "创新点长度不能超过 500 个字符")
+    private String innovationPoint;
+
+    @Schema(description = "立项申请表文件url")
+    private String applicationFile;
+
+    @Schema(description = "开题报告文件url")
+    private String openingReportFile;
+
+    @Schema(description = "其他文件url")
+    private String otherFile;
+
+    @Schema(description = "排序值", example = "1")
+    private Integer sort;
+
+    @Schema(description = "状态(0启用 1禁用)", example = "0")
+    private Integer status;
+
+    @Schema(description = "备注", example = "你说的对")
+    private String remark;
+
+    // ==================== 子表数据 ====================
+
+    @Schema(description = "进度安排列表")
+    @Valid
+    private List<ScheduleVO> scheduleList;
+
+    @Schema(description = "费用预算明细列表")
+    @Valid
+    private List<BudgetVO> budgetList;
+
+    // ==================== 子表 VO ====================
+
+    @Schema(description = "管理后台 - 项目进度安排 VO")
+    @Data
+    public static class ScheduleVO {
+
+        @Schema(description = "工作计划", requiredMode = Schema.RequiredMode.REQUIRED, example = "完成需求调研")
+        @NotEmpty(message = "工作计划不能为空")
+        private String workPlan;
+
+        @Schema(description = "截止时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-10-01")
+        @NotNull(message = "截止时间不能为空")
+        private java.time.LocalDate deadline;
+
+        @Schema(description = "排序值", example = "0")
+        private Integer sort;
+
+    }
+
+    @Schema(description = "管理后台 - 项目费用预算 VO")
+    @Data
+    public static class BudgetVO {
+
+        @Schema(description = "财务科目", requiredMode = Schema.RequiredMode.REQUIRED, example = "材料费")
+        @NotEmpty(message = "财务科目不能为空")
+        private String financialSubject;
+
+        @Schema(description = "预算金额(元)", requiredMode = Schema.RequiredMode.REQUIRED, example = "10000.00")
+        @NotNull(message = "预算金额不能为空")
+        private java.math.BigDecimal budgetAmount;
+
+        @Schema(description = "预算说明", example = "用于采购开发板")
+        private String budgetDescription;
+
+        @Schema(description = "排序值", example = "0")
+        private Integer sort;
+
+    }
+
+}

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

@@ -0,0 +1,40 @@
+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;
+
+@TableName(value = "rd_project_approval_record", autoResultMap = true)
+@KeySequence("rd_project_approval_record_seq")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ProjectApprovalRecordDO extends BaseDO {
+
+    @TableId
+    private Long id;
+    /** 项目ID */
+    private Long projectId;
+    /** 审批阶段:1预审 2正式立项 */
+    private Integer stage;
+    /** 审批节点:11预审二级 21一级 22二级 23财务 */
+    private Integer node;
+    /** 节点名称 */
+    private String nodeName;
+    /** 审批人ID */
+    private Long approverId;
+    /** 审批人姓名 */
+    private String approverName;
+    /** 审批结果:1通过 2驳回 3撤回 4提交 */
+    private Integer result;
+    /** 审批意见 */
+    private String opinion;
+    /** 审批时间 */
+    private LocalDateTime approveTime;
+
+}

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

@@ -0,0 +1,52 @@
+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.math.BigDecimal;
+
+/**
+ * 项目费用预算明细 DO
+ *
+ * @author ruiqi
+ */
+@TableName(value = "rd_project_budget", autoResultMap = true)
+@KeySequence("rd_project_budget_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ProjectBudgetDO extends BaseDO {
+
+    /**
+     * 主键id
+     */
+    @TableId
+    private Long id;
+    /**
+     * 关联项目主表id
+     */
+    private Long projectId;
+    /**
+     * 财务科目
+     */
+    private String financialSubject;
+    /**
+     * 预算金额(元)
+     */
+    private BigDecimal budgetAmount;
+    /**
+     * 预算说明
+     */
+    private String budgetDescription;
+    /**
+     * 排序值(前端表格行号)
+     */
+    private Integer sort;
+
+}

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

@@ -0,0 +1,116 @@
+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_project", autoResultMap = true)
+@KeySequence("rd_project_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ProjectDO extends BaseDO {
+
+    /**
+     * 主键id
+     */
+    @TableId
+    private Long id;
+    /**
+     * 项目级别
+     */
+    private String projectLevel;
+    /**
+     * 执行单位
+     */
+    private String executingUnit;
+    /**
+     * 项目名称
+     */
+    private String projectName;
+    /**
+     * 项目负责人
+     */
+    private String projectLeader;
+    /**
+     * 项目组成员(多个逗号分隔)
+     */
+    private String teamMembers;
+    /**
+     * 项目类型
+     */
+    private String projectType;
+    /**
+     * 目标和意义
+     */
+    private String goalAndSignificance;
+    /**
+     * 主要研究内容
+     */
+    private String mainResearchContent;
+    /**
+     * 创新点
+     */
+    private String innovationPoint;
+    /**
+     * 立项申请表文件url
+     */
+    private String applicationFile;
+    /**
+     * 开题报告文件url
+     */
+    private String openingReportFile;
+    /**
+     * 其他文件url
+     */
+    private String otherFile;
+    /**
+     * 排序值
+     */
+    private Integer sort;
+    /**
+     * 状态(0启用 1禁用)
+     */
+    private Integer status;
+    /**
+     * 备注
+     */
+    private String remark;
+
+    // ==================== 预审阶段 ====================
+    /** 预审状态:0未提交 1审批中 2通过 3驳回 */
+    private Integer preAuditStatus;
+    /** 预审当前节点:11预审二级 */
+    private Integer preCurrentNode;
+    /** 预审当前审批人ID */
+    private Long preCurrentApproverId;
+    /** 预审提交时间 */
+    private LocalDateTime preSubmitTime;
+
+// ==================== 正式立项阶段 ====================
+    /** 正式审批状态:0未提交 1审批中 2通过 3驳回 */
+    private Integer formalAuditStatus;
+    /** 正式当前节点:21一级 22二级 23财务 */
+    private Integer formalCurrentNode;
+    /** 正式当前审批人ID */
+    private Long formalCurrentApproverId;
+    /** 正式立项提交时间 */
+    private LocalDateTime formalSubmitTime;
+
+// ==================== 预算相关 ====================
+    /** 是否涉及预算:0否 1是 */
+    private Integer involveBudget;
+
+}

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

@@ -0,0 +1,48 @@
+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_schedule", autoResultMap = true)
+@KeySequence("rd_project_schedule_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ProjectScheduleDO extends BaseDO {
+
+    /**
+     * 主键id
+     */
+    @TableId
+    private Long id;
+    /**
+     * 关联项目主表id
+     */
+    private Long projectId;
+    /**
+     * 工作计划
+     */
+    private String workPlan;
+    /**
+     * 截止时间
+     */
+    private LocalDate deadline;
+    /**
+     * 排序值(前端表格行号)
+     */
+    private Integer sort;
+
+}

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

@@ -0,0 +1,45 @@
+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.ProjectApprovalRecordDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/**
+ * 项目审批记录 Mapper
+ *
+ * @author ruiqi
+ */
+@Mapper
+public interface ProjectApprovalRecordMapper extends BaseMapperX<ProjectApprovalRecordDO> {
+
+    /**
+     * 根据项目 ID 查询审批记录列表(按审批时间升序)
+     */
+    default List<ProjectApprovalRecordDO> selectListByProjectId(Long projectId) {
+        return selectList(new LambdaQueryWrapperX<ProjectApprovalRecordDO>()
+                .eq(ProjectApprovalRecordDO::getProjectId, projectId)
+                .orderByAsc(ProjectApprovalRecordDO::getApproveTime));
+    }
+
+    /**
+     * 根据项目 ID 和阶段查询审批记录
+     */
+    default List<ProjectApprovalRecordDO> selectListByProjectIdAndStage(Long projectId, Integer stage) {
+        return selectList(new LambdaQueryWrapperX<ProjectApprovalRecordDO>()
+                .eq(ProjectApprovalRecordDO::getProjectId, projectId)
+                .eq(ProjectApprovalRecordDO::getStage, stage)
+                .orderByAsc(ProjectApprovalRecordDO::getApproveTime));
+    }
+
+    /**
+     * 根据项目 ID 删除审批记录
+     */
+    default void deleteByProjectId(Long projectId) {
+        delete(new LambdaQueryWrapperX<ProjectApprovalRecordDO>()
+                .eq(ProjectApprovalRecordDO::getProjectId, projectId));
+    }
+
+}

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

@@ -0,0 +1,35 @@
+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.ProjectBudgetDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/**
+ * 项目费用预算明细 Mapper
+ *
+ * @author ruiqi
+ */
+@Mapper
+public interface ProjectBudgetMapper extends BaseMapperX<ProjectBudgetDO> {
+
+    /**
+     * 根据项目 ID 查询预算明细列表
+     */
+    default List<ProjectBudgetDO> selectListByProjectId(Long projectId) {
+        return selectList(new LambdaQueryWrapperX<ProjectBudgetDO>()
+                .eq(ProjectBudgetDO::getProjectId, projectId)
+                .orderByAsc(ProjectBudgetDO::getSort));
+    }
+
+    /**
+     * 根据项目 ID 删除预算明细
+     */
+    default void deleteByProjectId(Long projectId) {
+        delete(new LambdaQueryWrapperX<ProjectBudgetDO>()
+                .eq(ProjectBudgetDO::getProjectId, projectId));
+    }
+
+}

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

@@ -0,0 +1,53 @@
+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.ProjectPageReqVO;
+import cn.iocoder.yudao.module.rd.dal.dataobject.researchProject.ProjectDO;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+/**
+ * 项目立项 Mapper
+ *
+ * @author ruiqi
+ */
+@Mapper
+public interface ProjectMapper extends BaseMapperX<ProjectDO> {
+
+    default PageResult<ProjectDO> selectPage(ProjectPageReqVO reqVO) {
+        return selectPage(reqVO, buildCommonQuery(reqVO)
+                .orderByDesc(ProjectDO::getId));
+    }
+
+    default List<ProjectDO> selectList(ProjectPageReqVO reqVO) {
+        return selectList(buildCommonQuery(reqVO)
+                .orderByDesc(ProjectDO::getId));
+    }
+
+    /**
+     * 构建公共查询条件
+     *
+     * @param reqVO 查询条件
+     */
+    default LambdaQueryWrapperX<ProjectDO> buildCommonQuery(ProjectPageReqVO reqVO) {
+        return new LambdaQueryWrapperX<ProjectDO>()
+                .likeIfPresent(ProjectDO::getProjectName, reqVO.getProjectName())
+                .eqIfPresent(ProjectDO::getProjectLevel, reqVO.getProjectLevel())
+                .eqIfPresent(ProjectDO::getExecutingUnit, reqVO.getExecutingUnit())
+                .eqIfPresent(ProjectDO::getProjectLeader, reqVO.getProjectLeader())
+                .eqIfPresent(ProjectDO::getProjectType, reqVO.getProjectType())
+                .eqIfPresent(ProjectDO::getStatus, reqVO.getStatus())
+                .betweenIfPresent(ProjectDO::getCreateTime, reqVO.getCreateTime());
+    }
+
+    /**
+     * 查询当前最大的 sort 值(用于新增时 sort + 1)
+     */
+    @Select("SELECT IFNULL(MAX(sort), 0) FROM rd_project WHERE deleted = 0")
+    Integer selectMaxSort();
+
+}

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

@@ -0,0 +1,35 @@
+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.ProjectScheduleDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/**
+ * 项目进度安排 Mapper
+ *
+ * @author ruiqi
+ */
+@Mapper
+public interface ProjectScheduleMapper extends BaseMapperX<ProjectScheduleDO> {
+
+    /**
+     * 根据项目 ID 查询进度安排列表
+     */
+    default List<ProjectScheduleDO> selectListByProjectId(Long projectId) {
+        return selectList(new LambdaQueryWrapperX<ProjectScheduleDO>()
+                .eq(ProjectScheduleDO::getProjectId, projectId)
+                .orderByAsc(ProjectScheduleDO::getSort));
+    }
+
+    /**
+     * 根据项目 ID 删除进度安排
+     */
+    default void deleteByProjectId(Long projectId) {
+        delete(new LambdaQueryWrapperX<ProjectScheduleDO>()
+                .eq(ProjectScheduleDO::getProjectId, projectId));
+    }
+
+}

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

@@ -0,0 +1,88 @@
+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.ProjectPageReqVO;
+import cn.iocoder.yudao.module.rd.controller.admin.researchProject.vo.ProjectRespVO;
+import cn.iocoder.yudao.module.rd.controller.admin.researchProject.vo.ProjectSaveReqVO;
+import cn.iocoder.yudao.module.rd.dal.dataobject.researchProject.ProjectDO;
+import cn.iocoder.yudao.module.rd.controller.admin.researchProject.vo.ProjectApproveReqVO;
+
+import javax.validation.Valid;
+import java.util.List;
+
+/**
+ * 项目立项 Service 接口
+ *
+ * @author ruiqi
+ */
+public interface ResearchProjectService {
+
+    /**
+     * 创建项目立项
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createProject(@Valid ProjectSaveReqVO createReqVO);
+
+    /**
+     * 更新项目立项
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateProject(@Valid ProjectSaveReqVO updateReqVO);
+
+    /**
+     * 删除项目立项
+     *
+     * @param id 编号
+     */
+    void deleteProject(Long id);
+
+    /**
+     * 获得项目立项详情(含进度安排、费用预算子表)
+     *
+     * @param id 编号
+     * @return 项目详情
+     */
+    ProjectRespVO getProject(Long id);
+
+    /**
+     * 获得项目立项分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 项目分页
+     */
+    PageResult<ProjectRespVO> getProjectPage(ProjectPageReqVO pageReqVO);
+
+    /**
+     * 获得项目列表(用于导出 Excel)
+     *
+     * @param pageReqVO 列表查询
+     * @return 项目列表
+     */
+    List<ProjectDO> getProjects(ProjectPageReqVO pageReqVO);
+
+    List<ProjectDO> getProjectList(@Valid ProjectPageReqVO pageReqVO);
+
+
+    // ==================== 审批流 ====================
+
+    /** 提交预审 */
+    void submitPreApproval(Long projectId);
+
+    /** 预审审批(通过/驳回) */
+    void approvePre(@Valid ProjectApproveReqVO reqVO);
+
+    /** 提交正式立项 */
+    void submitFormalApproval(Long projectId);
+
+    /** 正式立项审批(通过/驳回) */
+    void approveFormal(@Valid ProjectApproveReqVO reqVO);
+
+    /** 撤回审批(预审或正式阶段都可用,看状态判断) */
+    void cancelApproval(Long projectId);
+
+}
+
+

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

@@ -0,0 +1,259 @@
+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.module.rd.controller.admin.researchProject.vo.ProjectPageReqVO;
+import cn.iocoder.yudao.module.rd.controller.admin.researchProject.vo.ProjectRespVO;
+import cn.iocoder.yudao.module.rd.controller.admin.researchProject.vo.ProjectSaveReqVO;
+import cn.iocoder.yudao.module.rd.dal.dataobject.researchProject.ProjectBudgetDO;
+import cn.iocoder.yudao.module.rd.dal.dataobject.researchProject.ProjectDO;
+import cn.iocoder.yudao.module.rd.dal.dataobject.researchProject.ProjectScheduleDO;
+import cn.iocoder.yudao.module.rd.dal.mysql.researchProject.ProjectApprovalRecordMapper;
+import cn.iocoder.yudao.module.rd.dal.mysql.researchProject.ProjectBudgetMapper;
+import cn.iocoder.yudao.module.rd.dal.mysql.researchProject.ProjectMapper;
+import cn.iocoder.yudao.module.rd.dal.mysql.researchProject.ProjectScheduleMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.validation.annotation.Validated;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.rd.enums.ErrorCodeConstant.*;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Objects;
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.rd.enums.ErrorCodeConstant.PROJECT_NOT_EXISTS;
+
+
+/**
+ * 项目立项 Service 实现类
+ *
+ * @author ruiqi
+ */
+@Service
+@Validated
+public class ResearchProjectServiceImpl implements ResearchProjectService {
+
+    @Resource
+    private ProjectMapper projectMapper;
+    @Resource
+    private ProjectScheduleMapper projectScheduleMapper;
+    @Resource
+    private ProjectBudgetMapper projectBudgetMapper;
+    @Resource
+    private ProjectApprovalRecordMapper approvalRecordMapper;
+
+
+    /**
+     * 新增项目
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long createProject(ProjectSaveReqVO createReqVO) {
+        ProjectDO project = BeanUtils.toBean(createReqVO, ProjectDO.class);
+
+        // 排序
+        project.setSort(getMaxSort() + 1);
+
+        // ⭐ 关键:初始化审批状态为"草稿"
+        project.setPreAuditStatus(AUDIT_STATUS_DRAFT);          // 0 预审未提交
+        project.setPreCurrentNode(null);
+        project.setPreCurrentApproverId(null);
+        project.setPreSubmitTime(null);
+
+        project.setFormalAuditStatus(AUDIT_STATUS_DRAFT);       // 0 正式未提交
+        project.setFormalCurrentNode(null);
+        project.setFormalCurrentApproverId(null);
+        project.setFormalSubmitTime(null);
+
+        // 是否涉及预算:由前端传 or 后端根据预算合计判断
+        if (project.getInvolveBudget() == null) {
+            project.setInvolveBudget(0);
+        }
+
+        projectMapper.insert(project);
+
+        // 子表
+        insertScheduleList(createReqVO.getScheduleList(), project.getId());
+        insertBudgetList(createReqVO.getBudgetList(), project.getId());
+
+        return project.getId();
+    }
+
+
+    /**
+     * 删除项目
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteProject(Long id) {
+        ProjectDO exist = projectMapper.selectById(id);
+        if (exist == null) {
+            throw exception(PROJECT_NOT_EXISTS);
+        }
+        // ⚠️ 已进入审批的项目禁止删除(安全)
+        if (exist.getPreAuditStatus() != null && exist.getPreAuditStatus() == AUDIT_STATUS_PROCESSING) {
+            throw exception(PROJECT_IN_APPROVAL_CANNOT_DELETE);
+        }
+        if (exist.getFormalAuditStatus() != null && exist.getFormalAuditStatus() == AUDIT_STATUS_PROCESSING) {
+            throw exception(PROJECT_IN_APPROVAL_CANNOT_DELETE);
+        }
+
+        projectMapper.deleteById(id);
+        projectScheduleMapper.deleteByProjectId(id);
+        projectBudgetMapper.deleteByProjectId(id);
+        // 审批记录保留(审计需求),如需级联删,放开下一行
+        // approvalRecordMapper.deleteByProjectId(id);
+    }
+
+
+
+    /**
+     * 获取当前最大的 sort 值,没有记录时返回 0
+     */
+    private int getMaxSort() {
+        Integer max = projectMapper.selectMaxSort();
+        return max == null ? 0 : max;
+    }
+
+
+    /**
+     * 更新项目
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateProject(ProjectSaveReqVO updateReqVO) {
+        ProjectDO exist = projectMapper.selectById(updateReqVO.getId());
+        if (exist == null) {
+            throw exception(PROJECT_NOT_EXISTS);
+        }
+
+        // ⚠️ 已提交审批的项目不允许随意修改(否则审批内容会被篡改)
+        if (exist.getPreAuditStatus() != null && exist.getPreAuditStatus() == AUDIT_STATUS_PROCESSING) {
+            throw exception(PROJECT_IN_APPROVAL_CANNOT_UPDATE);
+        }
+        if (exist.getFormalAuditStatus() != null && exist.getFormalAuditStatus() == AUDIT_STATUS_PROCESSING) {
+            throw exception(PROJECT_IN_APPROVAL_CANNOT_UPDATE);
+        }
+
+        ProjectDO updateObj = BeanUtils.toBean(updateReqVO, ProjectDO.class);
+        // ⚠️ 不允许前端改审批字段(安全)
+        updateObj.setPreAuditStatus(null);
+        updateObj.setPreCurrentNode(null);
+        updateObj.setPreCurrentApproverId(null);
+        updateObj.setFormalAuditStatus(null);
+        updateObj.setFormalCurrentNode(null);
+        updateObj.setFormalCurrentApproverId(null);
+
+        projectMapper.updateById(updateObj);
+
+        // 子表全删全插
+        projectScheduleMapper.deleteByProjectId(updateReqVO.getId());
+        projectBudgetMapper.deleteByProjectId(updateReqVO.getId());
+        insertScheduleList(updateReqVO.getScheduleList(), updateReqVO.getId());
+        insertBudgetList(updateReqVO.getBudgetList(), updateReqVO.getId());
+    }
+
+
+    /**
+     * 删除项目
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteProject(Long id) {
+        ProjectDO exist = projectMapper.selectById(id);
+        if (exist == null) {
+            throw exception(PROJECT_NOT_EXISTS);
+        }
+        // ⚠️ 已进入审批的项目禁止删除(安全)
+        if (exist.getPreAuditStatus() != null && exist.getPreAuditStatus() == AUDIT_STATUS_PROCESSING) {
+            throw exception(PROJECT_IN_APPROVAL_CANNOT_DELETE);
+        }
+        if (exist.getFormalAuditStatus() != null && exist.getFormalAuditStatus() == AUDIT_STATUS_PROCESSING) {
+            throw exception(PROJECT_IN_APPROVAL_CANNOT_DELETE);
+        }
+
+        projectMapper.deleteById(id);
+        projectScheduleMapper.deleteByProjectId(id);
+        projectBudgetMapper.deleteByProjectId(id);
+        // 审批记录保留(审计需求),如需级联删,放开下一行
+        // approvalRecordMapper.deleteByProjectId(id);
+    }
+
+    private void validateProjectExists(Long id) {
+        if (projectMapper.selectById(id) == null) {
+            throw exception(PROJECT_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public ProjectRespVO getProject(Long id) {
+        ProjectDO project = projectMapper.selectById(id);
+        if (project == null) {
+            return null;
+        }
+        ProjectRespVO respVO = BeanUtils.toBean(project, ProjectRespVO.class);
+        // 子表:进度安排
+        List<ProjectScheduleDO> scheduleList = projectScheduleMapper.selectListByProjectId(id);
+        respVO.setScheduleList(BeanUtils.toBean(scheduleList, ProjectRespVO.ScheduleVO.class));
+        // 子表:费用预算
+        List<ProjectBudgetDO> budgetList = projectBudgetMapper.selectListByProjectId(id);
+        respVO.setBudgetList(BeanUtils.toBean(budgetList, ProjectRespVO.BudgetVO.class));
+        return respVO;
+    }
+
+    @Override
+    public PageResult<ProjectRespVO> getProjectPage(ProjectPageReqVO pageReqVO) {
+        PageResult<ProjectDO> pageResult = projectMapper.selectPage(pageReqVO);
+        // 列表页不带子表,只转主表字段
+        return BeanUtils.toBean(pageResult, ProjectRespVO.class);
+    }
+
+    @Override
+    public List<ProjectDO> getProjects(ProjectPageReqVO reqVO) {
+        return projectMapper.selectList(reqVO);
+    }
+
+
+
+    // ==================== 私有方法 ====================
+
+    /**
+     * 批量插入进度安排
+     */
+    private void insertScheduleList(List<ProjectSaveReqVO.ScheduleVO> list, Long projectId) {
+        if (list == null || list.isEmpty()) {
+            return;
+        }
+        List<ProjectScheduleDO> scheduleList = BeanUtils.toBean(list, ProjectScheduleDO.class);
+        scheduleList.forEach(item -> {
+            item.setProjectId(projectId);
+            item.setSort(item.getSort() == null ? 0 : item.getSort());
+        });
+        scheduleList.forEach(projectScheduleMapper::insert);
+    }
+
+    /**
+     * 批量插入费用预算
+     */
+    private void insertBudgetList(List<ProjectSaveReqVO.BudgetVO> list, Long projectId) {
+        if (list == null || list.isEmpty()) {
+            return;
+        }
+        List<ProjectBudgetDO> budgetList = BeanUtils.toBean(list, ProjectBudgetDO.class);
+        budgetList.forEach(item -> {
+            item.setProjectId(projectId);
+            item.setSort(item.getSort() == null ? 0 : item.getSort());
+        });
+        budgetList.forEach(projectBudgetMapper::insert);
+    }
+
+    @Override
+    public List<ProjectDO> getProjectList(ProjectPageReqVO reqVO) {
+        return projectMapper.selectList(reqVO);
+    }
+
+}