项目预算和交付日期变了为什么不能直接改台账一个项目开始执行后预算需要增加计划结束时间需要后移项目经理也可能交接。给台账加一个编辑按钮就能很快把最新数字填进去。但过一段时间再回头看团队往往还要回答另一组问题原来怎么定的为什么要改谁批准的这次到底改了哪些内容台账适合表达当前执行口径变更单适合表达一次调整的依据。把两种职责混在一起页面上的值虽然更新了调整过程却很容易消失。我们在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{};consttypeformData.value.changeType;// 其他变更类型分支省略if(type2){after.planStartDatechangeAfter.planStartDate;after.planEndDatechangeAfter.planEndDate;}elseif(type3){after.budgetAmountchangeAfter.budgetAmount;}elseif(type4){after.managerUserIdchangeAfter.managerUserId;after.managerUserNamechangeAfter.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都严格冻结于申请时点更不能用它直接证明申请到批准期间无人调整过项目。以下代码保留了批准后的快照补齐与生效顺序省略日志OverrideTransactional(rollbackForException.class)publicvoidonProcessApproved(StringbusinessKey){LongidLong.parseLong(businessKey);ProjectChangeDOchangevalidateChangeExists(id);ProjectChangeDOupdateObjnewProjectChangeDO();updateObj.setId(id);updateObj.setProcessStatus(BpmTaskStatusEnum.APPROVE.getStatus());if(StrUtil.isBlank(change.getOriginalValue())){ProjectLedgerDOprojectvalidateProjectExists(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){LongmanagerUserIdreadLong(newValueNode,managerUserId);StringmanagerUserNamereadText(newValueNode,managerUserName);if(managerUserId!null){projectUpdate.setManagerUserId(managerUserId);}if(StrUtil.isNotBlank(managerUserName)){projectUpdate.setManagerUserName(managerUserName);}// 无有效经理信息时的警告日志省略}它解释了为什么页面既要选择人员ID也要保留用于展示的名称。当前提交校验允许经理ID或经理名称至少一项存在如果业务要求负责人必须是有效用户还需要服务端进一步校验身份和二者一致性不能仅凭有名称就认定授权关系已同步。进度变更采用类似的局部赋值方式同时兼容历史startDate、endDate键。下面保留日期取值和更新条件省略无法解析时的警告日志privatevoidapplyScheduleChange(ProjectLedgerDOprojectUpdate,ProjectChangeDOchange,JsonNodenewValueNode){LocalDateplanStartDatereadLocalDate(newValueNode,planStartDate);LocalDateplanEndDatereadLocalDate(newValueNode,planEndDate);if(planStartDatenull){planStartDatereadLocalDate(newValueNode,startDate);}if(planEndDatenull){planEndDatereadLocalDate(newValueNode,endDate);}if(planStartDate!null){projectUpdate.setPlanStartDate(planStartDate);}if(planEndDate!null){projectUpdate.setPlanEndDate(planEndDate);}}这段规则允许只调整开始或结束中的一项null在这里代表不赋值不能直接当成清空已有日期的指令。日期解析支持日期字符串以及特定长度的时间戳时间戳按服务端时区转为日期。纯日期场景优先使用明确的日期字符串有助于减少时区转换带来的理解差异。预算变更先读取结构化budgetAmount解析不到时兼容旧的纯金额文本最终赋给台账预算字段。它不是增量累加把预算调到某个数与“在现有预算上增加某个数”是两种不同指令申请说明和新值应保持一致。当前提交校验主要确认对应类型有可解析的新值不能据此宣传日期前后关系、预算正数、增幅上限、成本占用等业务制度都已完整覆盖。若需要这些控制应把规则补在业务服务边界而不是只在日期选择器或数字输入框上限制。动态与附件让结果有出处但不是完整幂等的替代品字段处理之后统一生效方法会在台账备注中追加带变更单号的标记调用台账服务更新字段再记录“变更生效”动态。这就是前面页面上备注与活动记录的来源。备注追加会检查同一标记是否已存在避免重复堆同一行文字。附件有两条路径需要区分新单据通过AttachmentService按业务类型与单据ID保存附件历史fileUrls字段则兼容保留。文档服务可用时生效方法确保项目空间存在并按项目、来源类型和变更单ID索引附件。索引会跳过空地址并检查相同来源与URL是否已有记录。这些规则让项目资料可以保留来源身份但不能写成“每次变更必有附件”或“审批一过就产生新的文件内容”。本次人员变更样本的附件区为空所以正文不拿它证明实际归档效果。还要区分记录策略项目动态的record方法捕获异常并记录警告设计上不以动态失败阻断主业务。通过钩子虽有事务注解也不能仅凭这一行注解宣称流程引擎、通知、主数据消息和所有附件操作构成跨服务原子事务。对于同一项目同时审批两张变更单按类型局部更新能减少无关覆盖却没有自动解决同字段竞争。两张预算变更先后批准后执行的赋值仍可能覆盖先前结果。更严格的场景可以增加台账版本、批准时基线校验和冲突处理并将本次生效身份记录下来。重复执行也要单独治理。备注标记防重复、文档来源检查只覆盖各自的小范围活动记录和其他附带动作仍需检查。可以引入独立生效记录与唯一约束明确哪些动作已完成、哪些允许重试。这些属于进一步设计建议不把现有留痕包装成恰好执行一次的保证。项目变更的价值是把“最新值”与“为什么变成这个值”接在一起。预算、计划日期、经理都可以沿用同一申请和审批骨架但各自的字段、校验和联动必须保持具体。台账继续服务执行变更单承担调整依据后续讨论才有一个能够追溯的起点。