SpringBoot3+Vue3 项目立项到甘特:审批冻结台账、任务树怎么汇进度
🌐 文档地址 :https://ruoyioffice.com
📦 源码1·GitHub :https://github.com/yuqing2026/ruoyi-office
📦 源码2·GitCode :https://gitcode.com/zhouzhongyan/ruoyi-office
📦 源码3·Gitee :https://gitee.com/yqzy1688/ruoyi-office
💬 微信:17156169080(备注「RuoYi Office」)
立项还在改预算,任务已经开工;甘特是项目经理手动画的,和 WBS 对不上。RuoYi Office 把申请单和台账拆开:审批通过才冻结立项、按同一 id 物化台账。进度从叶子填报往上卷,甘特读这棵树,不另存一套条形图。

▲ 提交立项 → 审批冻结 → 台账同 id 物化 → 进度上卷。甘特只读任务树,不要手填父进度
引言:项目难在「申请单和执行簿是两本账」
缺的不是甘特组件,是谁在什么时候变成执行对象。
| 现场 | 配错之后 |
|---|---|
| 立项通过还能改申请单 | 预算、经理、合同和现场各执一词 |
| 手工再建台账 | 编号对不上,附件丢在申请单里 |
| 父任务手填进度 | 子任务做完了,项目还是 0 |
| 甘特另存 Excel | 拖完条,任务树不更新 |
| 没立项就开工 | 没有负责人、没有基线,后面全是补票 |
本文只讲立项 → 台账 → 任务树 → 甘特。资料库 ACL、OnlyOffice 协同是另一条线,不在这里展开。
菜单:项目管理 → 项目立项 / 项目台账。流程 key:project_create。
一、产品能力:申请单冻结,台账才开工
PMO 批的是立项申请。项目经理干活看的是台账。两张皮必须在「审批通过」这一下接上。
| 能力 | 谁用 | 和「开一张项目表」差在哪 |
|---|---|---|
| 立项申请 | 项目经理 / 部门 | 草稿可改;提交后审批中;通过后冻结 |
| 台账 | 全员执行 | id 沿用申请单,状态进入进行中 |
| 任务 WBS | 执行人 | 父子树;叶子填报,父节点只汇总 |
| 甘特 | 项目经理 | 同一棵任务树的时间条,不是第二份计划 |
| 里程碑 | PMO | 计划节点,和任务可关联 |
| 合同基线 | 商务 / PMO | 挂了合同且没填预算,回填合同额 |
申请页 /project/project-apply-info?id= 就能带计划与 WBS、甘特、预算、里程碑------那是草案。通过之前,这些草稿不能冒充现场进度。
二、业务怎么走:冻结、物化、上卷
开发库 PJ202606070002(立项 / 台账 id=18)名称「软件研发项目2026060602」,申请单已立项。台账下 18 条任务,进度由上卷得到约 4.68%;演示数据里台账状态后来被结成已完成,甘特和 WBS 仍是同一棵树。
1. 立项:提交才进审批
保存是草稿。submitProjectInfo 把状态改成审批中(1),流程变量带编号和「项目名称 + 立项申请」,key 是 project_create。驳回 / 撤回退回草稿,现场不能当已批项目用。

▲ /project/project-apply-info?id=18。通过后状态已立项。再改预算应走变更,不是重开申请单
2. 通过:申请单冻结,台账按同一 id 落地
onProcessApproved 做两件事:申请单改已立项(2);upsertFromDraft 把申请单拷成台账。台账主键等于申请单 id ,draftProjectId 指回申请单。第一次插入记一条动态「立项审批通过,项目进入台账」;再次回调则更新。
挂了合同、预算为空时,用合同总额当预算基线,避免台账一打开预算是 0。可选地建文档空间、把申请附件编进资料库------那是旁路,主线是台账行必须在。
打开 /project/project-ledger-detail?id=18,默认「任务管理」。这是执行簿:预算 86 万、相对方演示客户,和申请单同一 id,不是另建一行。

▲ /project/project-ledger-detail?id=18 任务管理。叶子填报,父任务进度由子任务加权汇总
3. 进度:叶子填报,父任务上卷
ProjectTaskRollupService 先给没有孩子的叶子按执行记录算进度和实际工时,再从根往下递归:父任务进度 = 子任务加权,工时相加。根任务算完后,把加权进度写回台账 progress。
已取消的叶子不自动改状态。叶子全部 100% 且完成 / 归档,父任务才跟着重算。超期未完成标延期。项目经理不该在父任务格子里手填一个「看起来合理」的百分比。
4. 甘特:同一棵树的时间条
台账 Tab「甘特图」读任务的计划起止和进度。拖条、改日期会回刷任务看板和里程碑,避免甘特和 WBS 各改各的。申请单上的甘特是草案预览;现场以台账甘特为准。

▲ 同一 id=18,切到甘特图。条是任务,不是项目经理另画的一张图
三、设计怎么落地
设计思路
| 决策点 | 方案 | 理由 |
|---|---|---|
| 申请和执行是否一张表 | project_info + project_ledger |
通过前能改草案,通过后执行簿独立 |
| 台账 id 怎么来 | 沿用申请单 id | 详情、权限、文档空间不用二次映射 |
| 何时物化 | onProcessApproved |
驳回不能留下半个项目 |
| 进度谁说了算 | 叶子填报 + 加权上卷 | 父任务手填一定会漂 |
| 甘特存不存第二份 | 不存,读任务树 | 拖条必须回写任务 |
#mermaid-svg-1b2whMD1Uk7RQsFA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1b2whMD1Uk7RQsFA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1b2whMD1Uk7RQsFA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1b2whMD1Uk7RQsFA .error-icon{fill:#552222;}#mermaid-svg-1b2whMD1Uk7RQsFA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1b2whMD1Uk7RQsFA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1b2whMD1Uk7RQsFA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1b2whMD1Uk7RQsFA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1b2whMD1Uk7RQsFA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1b2whMD1Uk7RQsFA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1b2whMD1Uk7RQsFA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1b2whMD1Uk7RQsFA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1b2whMD1Uk7RQsFA .marker.cross{stroke:#333333;}#mermaid-svg-1b2whMD1Uk7RQsFA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1b2whMD1Uk7RQsFA p{margin:0;}#mermaid-svg-1b2whMD1Uk7RQsFA .entityBox{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-1b2whMD1Uk7RQsFA .relationshipLabelBox{fill:hsl(80, 100%, 96.2745098039%);opacity:0.7;background-color:hsl(80, 100%, 96.2745098039%);}#mermaid-svg-1b2whMD1Uk7RQsFA .relationshipLabelBox rect{opacity:0.5;}#mermaid-svg-1b2whMD1Uk7RQsFA .labelBkg{background-color:rgba(248.6666666666, 255, 235.9999999999, 0.5);}#mermaid-svg-1b2whMD1Uk7RQsFA .edgeLabel .label{fill:#9370DB;font-size:14px;}#mermaid-svg-1b2whMD1Uk7RQsFA .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-1b2whMD1Uk7RQsFA .edge-pattern-dashed{stroke-dasharray:8,8;}#mermaid-svg-1b2whMD1Uk7RQsFA .node rect,#mermaid-svg-1b2whMD1Uk7RQsFA .node circle,#mermaid-svg-1b2whMD1Uk7RQsFA .node ellipse,#mermaid-svg-1b2whMD1Uk7RQsFA .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1b2whMD1Uk7RQsFA .relationshipLine{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-1b2whMD1Uk7RQsFA .marker{fill:none!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-1b2whMD1Uk7RQsFA .edgeLabel{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1b2whMD1Uk7RQsFA .edgeLabel .label rect{fill:rgba(232,232,232, 0.8);}#mermaid-svg-1b2whMD1Uk7RQsFA .edgeLabel .label text{fill:#333;}#mermaid-svg-1b2whMD1Uk7RQsFA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} id 相同 / draft_project_id
project_id
parent_id
reports
PROJECT_INFO
bigint
id
PK
string
project_code
int
status
int
process_status
decimal
budget_amount
PROJECT_LEDGER
bigint
id
PK
bigint
draft_project_id
int
status
decimal
progress
PROJECT_TASK
bigint
id
PK
bigint
project_id
bigint
parent_id
decimal
progress
date
plan_start_date
date
plan_end_date
TASK_EXECUTION
申请单状态:0 草稿、1 审批中、2 已立项。台账通过后进进行中(2)。任务树用 parent_id,根是 0。
进度上卷 台账 Flowable 立项服务 Vue3 立项页 进度上卷 台账 Flowable 立项服务 Vue3 立项页 #mermaid-svg-eVR3Xcsegha5LI8N{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-eVR3Xcsegha5LI8N .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-eVR3Xcsegha5LI8N .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-eVR3Xcsegha5LI8N .error-icon{fill:#552222;}#mermaid-svg-eVR3Xcsegha5LI8N .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-eVR3Xcsegha5LI8N .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-eVR3Xcsegha5LI8N .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-eVR3Xcsegha5LI8N .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-eVR3Xcsegha5LI8N .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-eVR3Xcsegha5LI8N .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-eVR3Xcsegha5LI8N .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-eVR3Xcsegha5LI8N .marker{fill:#333333;stroke:#333333;}#mermaid-svg-eVR3Xcsegha5LI8N .marker.cross{stroke:#333333;}#mermaid-svg-eVR3Xcsegha5LI8N svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-eVR3Xcsegha5LI8N p{margin:0;}#mermaid-svg-eVR3Xcsegha5LI8N .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-eVR3Xcsegha5LI8N text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-eVR3Xcsegha5LI8N .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-eVR3Xcsegha5LI8N .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-eVR3Xcsegha5LI8N .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-eVR3Xcsegha5LI8N .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-eVR3Xcsegha5LI8N #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-eVR3Xcsegha5LI8N .sequenceNumber{fill:white;}#mermaid-svg-eVR3Xcsegha5LI8N #sequencenumber{fill:#333;}#mermaid-svg-eVR3Xcsegha5LI8N #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-eVR3Xcsegha5LI8N .messageText{fill:#333;stroke:none;}#mermaid-svg-eVR3Xcsegha5LI8N .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-eVR3Xcsegha5LI8N .labelText,#mermaid-svg-eVR3Xcsegha5LI8N .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-eVR3Xcsegha5LI8N .loopText,#mermaid-svg-eVR3Xcsegha5LI8N .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-eVR3Xcsegha5LI8N .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-eVR3Xcsegha5LI8N .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-eVR3Xcsegha5LI8N .noteText,#mermaid-svg-eVR3Xcsegha5LI8N .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-eVR3Xcsegha5LI8N .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-eVR3Xcsegha5LI8N .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-eVR3Xcsegha5LI8N .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-eVR3Xcsegha5LI8N .actorPopupMenu{position:absolute;}#mermaid-svg-eVR3Xcsegha5LI8N .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-eVR3Xcsegha5LI8N .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-eVR3Xcsegha5LI8N .actor-man circle,#mermaid-svg-eVR3Xcsegha5LI8N line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-eVR3Xcsegha5LI8N :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} PM 提交立项submitProjectInfoproject_createonProcessApprovedstatus=已立项upsertFromDraft(同一 id)叶子填报进度rollupProject叶子→父任务加权写回 progress PM
提交先改审批中再启流程。通过才物化。进度从执行记录往上卷,最后写台账一行。
java
public Long submitProjectInfo(ProjectInfoSaveReqVO saveReqVO) {
Long id = saveProjectInfo(saveReqVO);
ProjectInfoDO projectInfo = projectInfoMapper.selectById(id);
projectInfo.setStatus(STATUS_APPROVING); // 1 审批中
projectInfo.setProcessStatus(BpmTaskStatusEnum.RUNNING.getStatus());
String processInstanceId = processInstanceApi.submitProcessInstance(
..., new BpmProcessInstanceCreateReqDTO()
.setProcessDefinitionKey("project_create")
.setBusinessKey(String.valueOf(id)))
.getCheckedData();
projectInfo.setProcessInstanceId(processInstanceId);
projectInfoMapper.updateById(projectInfo);
return id;
}
通过回调冻结申请单,并物化台账。台账主键故意等于申请单,详情路由不用再翻译。
java
public void onProcessApproved(String businessKey) {
Long id = Long.parseLong(businessKey);
ProjectInfoDO project = validateProjectInfoExists(id);
projectInfoMapper.updateById(new ProjectInfoDO()
.setId(id).setStatus(STATUS_APPROVED)); // 2 已立项
projectLedgerService.upsertFromDraft(project, LEDGER_STATUS_IN_PROGRESS);
initLedgerMembers(project);
}
upsertFromDraft 拷字段、写 draftProjectId、必要时回填合同预算。已有行就更新,避免通过回调重入插两条。
java
public ProjectLedgerDO upsertFromDraft(ProjectInfoDO draft, Integer status) {
ProjectLedgerDO ledger = BeanUtils.toBean(draft, ProjectLedgerDO.class);
ledger.setId(draft.getId());
ledger.setDraftProjectId(draft.getId());
ledger.setStatus(status);
if (draft.getContractId() != null && draft.getBudgetAmount() == null) {
// 合同总额回填预算基线
}
if (projectLedgerMapper.selectById(ledger.getId()) == null) {
projectLedgerMapper.insert(ledger);
} else {
projectLedgerMapper.updateById(ledger);
}
return ledger;
}
上卷先处理叶子执行,再递归父节点,最后把根的加权进度写回台账。父任务没有自己的「拍脑袋百分比」。
四、和「Excel 甘特」差在哪
| 设计要点 | 落地 | 对业务的价值 |
|---|---|---|
| 申请 / 执行分表 | info 冻结 + ledger 物化 | 通过后不能偷偷改立项口径 |
| 同一 id | ledger.id = info.id |
权限、文档、甘特共用主键 |
| 进度上卷 | 叶子执行 → 父加权 → 台账 | 项目进度能解释 |
| 甘特不双写 | 读任务起止 | 拖条即改计划 |
| 驳回回草稿 | onProcessRejected |
现场不会出现「批了一半的项目」 |
资料库可以在物化时 ensureSpace,但验收这条链时先看:申请单已立项、台账打得开、任务能上卷、甘特条和任务对得上。
五、快速体验
在线演示:https://ruoyioffice.com/web/(账号 admin / admin123)
| 步 | 菜单 | 看什么 |
|---|---|---|
| 1 | 项目管理 → 项目立项 | 打开已通过单,状态应是已立项 |
| 2 | 切「计划与 WBS / 甘特图」 | 这是草案痕迹,对照台账 |
| 3 | 项目台账 | 用同一 id 打开详情 |
| 4 | Tab 任务管理 | 父子树、叶子进度 |
| 5 | Tab 甘特图 | 条与任务起止一致 |
| 6 | 给叶子填一笔执行 | 父任务和台账进度应上涨 |
| 7 | 不要在父任务手填进度 | 下一轮上卷会被覆盖 |
源码仓库:GitHub:https://github.com/yuqing2026/ruoyi-office | GitCode:https://gitcode.com/zhouzhongyan/ruoyi-office | Gitee:https://gitee.com/yqzy1688/ruoyi-office
常见问题
立项通过后还能改申请单吗?
产品上申请单已冻结。要改预算、经理、范围,应走变更,不要把已立项单当草稿改。驳回的单才退回草稿。
为什么台账 id 不另生成?
另生成就要处处映射:权限、文档空间、甘特、动态。沿用申请单 id,路由 /project/project-ledger-detail?id= 和立项 id 对得上。
父任务进度能不能手填?
上卷会按子任务加权覆盖。手填只在下一次填报之前看起来对。要调整体进度,改叶子执行或拆任务。
申请页也有甘特,和台账甘特有何不同?
申请页是草案预览,给审批人看计划长什么样。台账甘特跟执行走,拖条回写任务。现场以台账为准。
和项目文档协同是不是同一篇文章?
不是。文档树、ACL、在线编辑是资料空间。本文只保证立项冻结和进度能解释。两套可以同时开,不要用文件夹代替 WBS。
结语
立项链能跑住,靠的是:通过才冻结,台账沿用同一 id,进度从叶子往上卷。 甘特是这棵树的视图,不是第二本计划。Excel 条形图管不住的,是「申请口径」和「现场口径」什么时候分开。
同一套「单据冻结 → 台账物化 → 明细上卷」还可以用在预算编制对执行台账、销售订单对出库进度。先把项目这一条走通,再谈看板和盈利分析。
💡 想要体验 RuoYi Office 的强大功能?
🌐 在线演示:https://ruoyioffice.com/web/(账号 admin / admin123)
📦 源码仓库:GitHub:https://github.com/yuqing2026/ruoyi-office | GitCode:https://gitcode.com/zhouzhongyan/ruoyi-office | Gitee:https://gitee.com/yqzy1688/ruoyi-office
💬 技术咨询 :添加微信 17156169080,备注「RuoYi Office」
⭐ 如果觉得不错,请给个 Star 支持一下!