☰
项目预算和交付日期变了,为什么不能直接改台账?
2026/10/24 4:08:02 网站建设 项目流程

项目预算和交付日期变了,为什么不能直接改台账?

一个项目开始执行后,预算需要增加,计划结束时间需要后移,项目经理也可能交接。给台账加一个编辑按钮,就能很快把最新数字填进去。但过一段时间再回头看,团队往往还要回答另一组问题:原来怎么定的,为什么要改,谁批准的,这次到底改了哪些内容?

台账适合表达当前执行口径,变更单适合表达一次调整的依据。把两种职责混在一起,页面上的值虽然更新了,调整过程却很容易消失。

我们在RuoYi Office的项目模块中,把变更申请、结构化前后值、流程审批和台账生效串成一条链。本文先用一个已有的项目经理变更样本看页面,再拆解预算与计划日期如何复用这条链。这里的日期调整落在项目计划开始、计划结束字段,不能据此推导合同交付承诺也会同步修改。

▲ 主线是“提出调整→审批确认→回写台账”。不同变更类型只更新各自负责的字段;附件归档还需要有实际附件和可用的文档服务。

台账看现在,变更单说明这一次为什么改

先看项目台账。它汇集项目经理、计划日期、预算,以及任务、工时、文档、变更等执行信息。如果每次调整都直接覆盖台账,其他业务仍能读到最新值,却难以解释这个值的来历。

▲ 本地系统既有演示项目PJ202606070002。当前项目经理为“hr 小姐姐”,备注含变更单号PROJECT602-2026072900001。预算与计划日期只是该项目现值,不能当成本次变更过的字段;手机可点开整屏图查看。

这里没有把预算、合同额、实际成本合并成一个金额,也没有把项目计划结束日当成任务树里每个任务的结束日。它们分别服务于预算控制、履约约定和执行排程。一个字段发生变化,是否需要连带调整其他对象,必须由具体业务规则决定。

变更单保留另一种视角:针对哪个项目,改哪一类内容,为什么改,影响是什么,以及本次希望写入的新值。审批人据此判断调整,而不是面对一张被人改过却不知道差异的台账。

这也解释了为什么产品需要同时保留立项原件和执行台账:立项表达初始申请,台账表达执行现状,后续变更把二者之间的调整过程连接起来。本文核对的是既有数据,不通过重新提交或改动演示记录制造前后效果。

从一张人员变更,看清申请与结果的对应关系

现有样本的变更类型是人员变更,内容为项目经理调整。页面将申请说明和结构化明细分开:说明给人读,字段值供程序处理。前后对比能同时看到经理ID从1到114,以及名称从原负责人变成新负责人。

▲ 同一变更单的完整浏览器视口,自然滚动到对比区。前值“宇擎源码”、后值“hr 小姐姐”;这张图说明申请记录里的差异,不是把两个后台页面拼接成前后效果。

再进入同一项目的“项目动态”,筛选变更,可以找到带同一单号的“变更生效”记录。台账经理与变更后值一致,备注和动态又保留了变更编号,读者能够把申请、现值和生效记录对应起来。

▲ 变更动态关联PROJECT602-2026072900001,时间为既有记录的2026-07-29 20:04:30。本轮只读核对这些历史状态,没有重新执行审批,也不把当前页面称作变更前台账。

这三张图分别回答:项目现在由谁负责,这次提出了怎样的调整,以及系统记录了哪张变更单生效。它们没有证明预算或日期也在这次调整中变化,后面两类规则需要回到实现中分别看。

把变更类型做成业务边界,而不是给所有字段开编辑口

同一张变更单支持多种类型,但并不意味着把整个台账对象原样提交。我们把“这次允许表达哪些新值”作为第一层边界。

类型当前处理目标
范围项目描述/范围
进度计划开始、计划结束
预算项目预算金额
人员项目经理ID与名称
基础信息项目名称、优先级、分类、项目集、类型、部门等
其他备注等补充信息

“人员变更”在这里具体落实到项目经理,不是任意项目成员的增删,也不意味着所有存量任务负责人同时改掉。“预算变更”更新台账预算金额,不自动生成付款单;“进度变更”更新项目计划日期,不自动重排WBS和里程碑。

Vue3表单先维护可编辑的新值,提交时再依据类型组装newValue。下面摘取进度、预算和人员三个分支,其他分支省略;返回对象中的其他单据元数据也省略了:

functionsyncPayload():ProjectChangeApi.Change{constafter:ProjectChangeApi.ChangeValueSnapshot={};consttype=formData.value.changeType;// 其他变更类型分支省略if(type===2){after.planStartDate=changeAfter.planStartDate;after.planEndDate=changeAfter.planEndDate;}elseif(type===3){after.budgetAmount=changeAfter.budgetAmount;}elseif(type===4){after.managerUserId=changeAfter.managerUserId;after.managerUserName=changeAfter.managerUserName;}return{...(formData.valueasProjectChangeApi.Change),newValue:stringifyChangeValue(after),// 附件与originalValue组装省略};}

这一步减少了无关字段进入变更载荷的机会,后端仍要按类型解释字段,不能把前端组装当成安全校验。页面的对比组件也使用同一套类型字段映射:进度只比较计划日期,人员只比较经理字段。否则完整快照中无关的空值、名称差异,可能淹没审批人真正需要判断的变化。

对比本身还有一条小规则:当前组件将空值显示为“-”,把值转换成展示文本后比较。这适合呈现字段变化,不等于已经完成金额精度归一、对象深层比较或语义相等判定。面向更复杂的变更对象时,需要补充对应的比较方式。

快照留下的是参照,生效更新的是当前项目

▲ 台账与变更单通过projectId关联;流程用businessKey关联变更单ID;活动和文档保留来源身份。这是逻辑关联图,不宣称数据库已建立物理外键。

变更记录中最关键的几组字段职责不同:

字段保存的业务含义
projectId、billCode被调整的项目与本次变更编号
changeType、changeContent类型与给人阅读的调整说明
originalValue、newValue参照快照与本次希望生效的字段值
processInstanceId、processStatus对应审批实例及其结果

originalValue与newValue都以字符串承载结构化内容,但不能简单把它们理解成数据库自动管理的版本。新建时,前端会读取所选项目形成参照;后端保存或提交时,如果原值为空,也会用项目现值补齐。

已有变更重新打开时,页面从已保存的originalValue和newValue恢复对比,另读当前台账作为上下文。这样查看旧单时,不必把“今天的台账”当成“当时的原值”。不过,服务端当前只在原值为空时补快照,并未在此处把客户端提交的原值验证成不可篡改基线。

通过钩子还有一条历史数据兜底:如果旧单一直没有原值,会在批准时补一份项目快照。因此不能承诺所有记录的originalValue都严格冻结于申请时点,更不能用它直接证明申请到批准期间无人调整过项目。

以下代码保留了批准后的快照补齐与生效顺序,省略日志:

@Override@Transactional(rollbackFor=Exception.class)publicvoidonProcessApproved(StringbusinessKey){Longid=Long.parseLong(businessKey);ProjectChangeDOchange=validateChangeExists(id);ProjectChangeDOupdateObj=newProjectChangeDO();updateObj.setId(id);updateObj.setProcessStatus(BpmTaskStatusEnum.APPROVE.getStatus());if(StrUtil.isBlank(change.getOriginalValue())){ProjectLedgerDOproject=validateProjectExists(change.getProjectId());updateObj.setOriginalValue(buildProjectSnapshot(project));change.setOriginalValue(updateObj.getOriginalValue());}changeMapper.updateById(updateObj);applyChangeToProject(change);}

这里没有直接把整份快照覆盖回台账。生效方法根据changeType调用各自的字段处理函数,再把局部更新交给台账服务。这样一次经理变更不会顺带把申请时的旧预算、旧日期写回来。

审批不是存一个结果,而是决定哪一次调整可以执行

保存草稿时,服务写变更记录和附件,不执行台账回写。提交时,服务再检查变更类型、说明和对应的新值能否解析,随后创建审批实例,把变更单ID作为businessKey,并回填实例ID。

状态上也有区别:未提交、审批不通过和已撤回可进入再次提交路径;审批中、已通过等状态不能重复按草稿提交。保存方法另外允许审批中的记录进入局部保存路径,但具体审批字段权限不能只靠这一个状态判断保证,仍要结合页面字段权限与后端输入约束。

我们把关键设计取舍归纳为四项:

决策当前落点与原因
说明与可执行值分开说明表达背景,newValue承担明确字段赋值,避免解析自然语言猜金额
草稿与生效分开草稿可以继续准备;通过钩子才进入台账更新
按类型局部回写每类只触碰负责的字段,减少无关旧值覆盖
当前状态与依据分开台账读现值,变更单、动态与附件保留来源

▲ 保存和提交不等于台账生效。通过后进入项目业务钩子,再更新台账、记录动态,并在文档服务可用时索引附件。各步骤的失败处理并不完全相同。

本文不展开引擎事件的传输方式,只关注结果到达项目服务之后做什么。接入时仍须确保所选通知通道确实调用项目的通过钩子;如果只有processStatus变成通过,却没有进入onProcessApproved,台账就不会因为一个绿色标签自动变化。

经理、日期和预算,各有一段明确的赋值规则

人员变更与前面页面直接对应。服务读取经理ID和名称,只对存在的新值赋值,最后由统一生效方法写入台账:

privatevoidapplyMemberChange(ProjectLedgerDOprojectUpdate,ProjectChangeDOchange,JsonNodenewValueNode){LongmanagerUserId=readLong(newValueNode,"managerUserId");StringmanagerUserName=readText(newValueNode,"managerUserName");if(managerUserId!=null){projectUpdate.setManagerUserId(managerUserId);}if(StrUtil.isNotBlank(managerUserName)){projectUpdate.setManagerUserName(managerUserName);}// 无有效经理信息时的警告日志省略}

它解释了为什么页面既要选择人员ID,也要保留用于展示的名称。当前提交校验允许经理ID或经理名称至少一项存在;如果业务要求负责人必须是有效用户,还需要服务端进一步校验身份和二者一致性,不能仅凭有名称就认定授权关系已同步。

进度变更采用类似的局部赋值方式,同时兼容历史startDate、endDate键。下面保留日期取值和更新条件,省略无法解析时的警告日志:

privatevoidapplyScheduleChange(ProjectLedgerDOprojectUpdate,ProjectChangeDOchange,JsonNodenewValueNode){LocalDateplanStartDate=readLocalDate(newValueNode,"planStartDate");LocalDateplanEndDate=readLocalDate(newValueNode,"planEndDate");if(planStartDate==null){planStartDate=readLocalDate(newValueNode,"startDate");}if(planEndDate==null){planEndDate=readLocalDate(newValueNode,"endDate");}if(planStartDate!=null){projectUpdate.setPlanStartDate(planStartDate);}if(planEndDate!=null){projectUpdate.setPlanEndDate(planEndDate);}}

这段规则允许只调整开始或结束中的一项;null在这里代表不赋值,不能直接当成清空已有日期的指令。日期解析支持日期字符串以及特定长度的时间戳,时间戳按服务端时区转为日期。纯日期场景优先使用明确的日期字符串,有助于减少时区转换带来的理解差异。

预算变更先读取结构化budgetAmount,解析不到时兼容旧的纯金额文本,最终赋给台账预算字段。它不是增量累加:把预算调到某个数,与“在现有预算上增加某个数”是两种不同指令,申请说明和新值应保持一致。

当前提交校验主要确认对应类型有可解析的新值,不能据此宣传日期前后关系、预算正数、增幅上限、成本占用等业务制度都已完整覆盖。若需要这些控制,应把规则补在业务服务边界,而不是只在日期选择器或数字输入框上限制。

动态与附件,让结果有出处,但不是完整幂等的替代品

字段处理之后,统一生效方法会在台账备注中追加带变更单号的标记,调用台账服务更新字段,再记录“变更生效”动态。这就是前面页面上备注与活动记录的来源。备注追加会检查同一标记是否已存在,避免重复堆同一行文字。

附件有两条路径需要区分:新单据通过AttachmentService按业务类型与单据ID保存附件;历史fileUrls字段则兼容保留。文档服务可用时,生效方法确保项目空间存在,并按项目、来源类型和变更单ID索引附件。索引会跳过空地址,并检查相同来源与URL是否已有记录。

这些规则让项目资料可以保留来源身份,但不能写成“每次变更必有附件”或“审批一过就产生新的文件内容”。本次人员变更样本的附件区为空,所以正文不拿它证明实际归档效果。

还要区分记录策略:项目动态的record方法捕获异常并记录警告,设计上不以动态失败阻断主业务。通过钩子虽有事务注解,也不能仅凭这一行注解,宣称流程引擎、通知、主数据消息和所有附件操作构成跨服务原子事务。

对于同一项目同时审批两张变更单,按类型局部更新能减少无关覆盖,却没有自动解决同字段竞争。两张预算变更先后批准,后执行的赋值仍可能覆盖先前结果。更严格的场景可以增加台账版本、批准时基线校验和冲突处理,并将本次生效身份记录下来。

重复执行也要单独治理。备注标记防重复、文档来源检查,只覆盖各自的小范围;活动记录和其他附带动作仍需检查。可以引入独立生效记录与唯一约束,明确哪些动作已完成、哪些允许重试。这些属于进一步设计建议,不把现有留痕包装成恰好执行一次的保证。

项目变更的价值,是把“最新值”与“为什么变成这个值”接在一起。预算、计划日期、经理都可以沿用同一申请和审批骨架,但各自的字段、校验和联动必须保持具体。台账继续服务执行,变更单承担调整依据,后续讨论才有一个能够追溯的起点。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询