# 预算版本管理 操作手册 > 模块 slug: budget-version(对应队列 cma-module-queue.json) > 最近一次修复后状态定稿:2026-09-07(project-20260907-budget-version-ui-fix 完成并验证后) > 使用本手册前请先读「④ 已知边界/坑」——本模块历史上反复出现字段/状态机错位,改前必查。 ## ① 模块定位 - **模块名**:预算版本管理(Budget Version Management) - **解决什么问题**:年度预算允许保留多个版本(如零基 v1.0/v2.0、年度 2026H2),支持"编制 → 提交审批 → 批准/驳回 → 重新提交"的版本状态机,以及两个版本间的差异对比。防止把"草稿中版本"误当"已批准版本"执行。 - **所属流程环节**:预算 → 版本管理(预算编制/分解/执行的上游容器;预算计划行挂在具体 version 下,active 版本为当前生效版本) - **主要角色**:finance(编制/提交审批)、ceo(批准/驳回)、it(维护),entity1=陕西酣客、entity2=陕西博海 ## ② 操作步骤 页面路径:登录后侧边栏「预算管理」(/budget,zone B 可编辑)→ 顶部 tab「版本管理」。 1. **查看版本列表**(默认按当年 year 加载) - 版本列表加载:前端调 `GET /api/cma/budget/versions?year=YYYY`(**返回裸数组**,前端需 `Array.isArray(r) ? r : r.data || []` 解包) - 列含义:`版本`(标签,当前生效版本绿色高亮并带「(当前)」后缀) | `状态`(审批状态标签:草稿/待审批/已批准/已驳回) | `条目数`(plan_count=预算行数) | `总额`(total_budget) | `最后更新`(last_updated, YYYY-MM-DD HH:MM) | `操作`(按状态出按钮) 2. **状态机流转**(DB `budget_plans.status` 原值 vs UI 展示 `approval_status`) - UI 语义:`draft`(可提交审批) → `submitted`(待审批) → `approved`(已批准只读) / `rejected`(已驳回可重新提交) - **后端映射**:DB 默认态 `active` → UI 展示为 `draft`(服务端语义收敛,`approval_status = "draft" if status=="active" else status`);approved/submitted/rejected 原样透传。**DB status 保留 active/approved/submitted/rejected 四种,勿改 DB 值语义**(active=当前生效版本,版本下拉「(当前)」标记依赖 `status==='active'`) 3. **提交审批**:状态为「草稿」的行 → 点「提交审批」→ 确认弹窗 → 后端 `POST /api/cma/budget/versions/submit`(body `{"version": "v2026H2"}`),状态 active → submitted 4. **审批**:状态为「待审批」的行 → 点「批准」(`POST /versions/approve` body action=approved,submitted → approved) 或「驳回」(action=rejected,submitted → rejected) 5. **重新提交**:状态为「已驳回」的行 → 点「重新提交」(复用 submit 端点,rejected → submitted) 6. **已批准只读**:approved 版本**操作列无任何按钮**,不可再提交/修改(只读语义,正确表现即无按钮) 7. **版本差异对比**:勾选列表左侧复选框(**最多 2 个**,超过自动取消最早勾选)→ 点「版本差异对比」→ 弹窗展示两版本逐期间/逐 KPI 差异(`POST /api/cma/budget/versions/diff`,body 含 version_a/version_b/year) 8. 预算行编制在「预算编制」相关 tab 内按当前选中版本录入(顶部版本下拉 filterVersion 选择后 loadBudget) ## ③ 验证路径 - **自动脚本**: 1. 生产 API 双账套断言:`/root/cma-management/backend/scripts/verify_budget_versions_ui_fix.py`(`cd backend && venv/bin/python3 scripts/verify_budget_versions_ui_fix.py`;凭据见 600 密钥文件 `CMA_ADMIN_USER` / `CMA_ADMIN_PASS`(脚本同源读取:`$CMA_ADMIN_ENV` 或 `.secrets/cma-admin.env`)分别登录 entity1/entity2,断言新字段存在 + active→draft 映射 + plan_count==kpi_count + last_updated==updated_at) 2. 浏览器页面 DOM 实测:`/root/cma-management/backend/scripts/verify_budget_versions_ui_browser.py`(需先起 Chrome CDP 9224,命令见 cma-fullstack-ops 技能 references/budget-version-ui-field-state-fix-20260907.md 的 Chrome 起法;登录博海 entity2 断言 v2026H2 行=草稿/25条/462/最后更新非空/「提交审批」按钮,切酣客 entity1 断言 5 个 active 行有提交按钮 + 2026H2 approved「已批准」无按钮) - **期望输出**:两个脚本末尾均输出 `ALL_PASS` 且 exit 0 - **已测基线**(最近一次验证日期+结果):2026-09-07 复跑双脚本 ALL_PASS。实测数据:博海 entity2 `v2026H2 | status=active | approval_status=draft | plan_count=25 | total=462.0 | last_updated='2026-08-30 11:24'`;酣客 entity1 共 6 版本,5 个 active(v1.0/v2.0/incremental/flexible/zero_based-20260826)→ 草稿+「提交审批」按钮,2026H2 approved → 「已批准」无按钮 ## ④ 已知边界 / 坑 - **历史 bug**: - 2026-09-04 版本列表永远为空:后端 `list_budget_versions` 返回**裸数组**,前端 axios 拦截器剥层后 `r.data` 为 undefined → 空列表无报错。修复 commit 46096e8:前端 `Array.isArray` 兼容解包。**同根因 bug 出现在 BudgetManagement.vue loadVersions,全仓扫描见技能 axios-unwrap-bare-array reference** - 2026-09-07 用户反馈「版本管理仅有操作复选框」:**两个独立 bug 叠加**——①前端列取 `approval_status/plan_count/last_updated` 而后端返回 `status/kpi_count/updated_at` → 状态列恒显示 else 分支「草稿」、条目数/最后更新恒空白;②DB `budget_plans.status` 默认态是 `active`(不在前端按钮条件 draft/submitted/rejected 里)→ active 行整行无按钮。修复 commit b01a108:后端输出补 `approval_status`(active→draft 映射)+`plan_count`+`last_updated` 且**保留 status/kpi_count/updated_at 原字段**;前端按钮条件补 `|| row.approval_status === 'active'` 双端保险 - **边界条件**: - active(=UI draft)**可提交审批**;approved **只读**(无按钮、不应有编辑入口);submitted 只能批准/驳回不能重复提交;rejected 可重新提交 - 版本下拉「(当前)」标记用 **DB 原字段 `status==='active'`**,与 UI approval_status 展示解耦——两字段都必须在接口里 - 版本「选中当前版本」逻辑:加载版本后默认选 `status==='active'` 的版本,避免全版本堆叠重复期间(2026-08-27 根治) - **⚠️ 易踩点**: 1. **后端返回字段改名必须保留原字段别名**——前端/测试契约(下拉「当前」标记、verify 脚本断言)依赖 status/kpi_count/updated_at 2. **approval_status 由后端统一映射,勿在前端做 DB 状态翻译**(会出现双端不一致) 3. `/versions` 端点返回裸数组是既有契约,改后端请勿包一层 data(会复现 09-04 空列表 bug);改前端勿忘 Array.isArray 解包 4. 验证脚本(浏览器版)存在 tab 创建竞态(Target.createTarget 后偶发 about:blank → localStorage SecurityError),已加导航等待(commit 616f444);复跑若挂先确认 Chrome 起法与 host-resolver-rules ## ⑤ 修改记录 | 日期 | 修改人 | 改动 | commit | 影响 | |------|--------|------|--------|------| | 2026-09-07 | - | 手册创建(修复后状态定稿) | - | - | | 2026-09-07 | 全栈Bot | UI字段/状态机对齐:后端补 approval_status(active→draft)/plan_count/last_updated + 前端按钮 active 兜底 | b01a108 | 修复「版本管理只有复选框」 | | 2026-09-07 | 全栈Bot | 验证脚本入库(API+浏览器双录) | f8018aa | 可复跑资产 | | 2026-09-07 | 全栈Bot | 浏览器验证脚本 tab 竞态加导航等待 | 616f444 | 修复复跑必挂 | | 2026-09-04 | 全栈Bot | loadVersions 裸数组解包 bug 修复 | 46096e8 | 修复版本列表永远为空 | | 2026-08-27 | 全栈Bot | 加载版本默认选中当前 active 版本 | - | 防全版本堆叠 | ## ⑥ 关联 - **依赖表**:budget_plans(version/status/period/amount 等,预算行按 version 归属)、kpi_definitions(预算行关联 KPI) - **依赖接口**:GET /api/cma/budget/versions;POST /api/cma/budget/versions/submit;POST /api/cma/budget/versions/approve;POST /api/cma/budget/versions/diff(均带 Depends(get_entity_id) 账套隔离) - **关联模块**:预算编制/分解(auto-decompose,批量分解幂等)、预算执行(execution)、预编报表(proforma reports 取 active 版本预算);前端 BudgetManagement.vue - **验证资产**:backend/scripts/verify_budget_versions_ui_fix.py(API)、verify_budget_versions_ui_browser.py(浏览器 CDP)