diff --git a/docs/manuals/budget-version.md b/docs/manuals/budget-version.md new file mode 100644 index 00000000..ae87d8ca --- /dev/null +++ b/docs/manuals/budget-version.md @@ -0,0 +1,64 @@ +# 预算版本管理 操作手册 + +> 模块 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`;用 admin/admin123 分别登录 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) diff --git a/docs/manuals/multitenant-isolation.md b/docs/manuals/multitenant-isolation.md new file mode 100644 index 00000000..c48285b5 --- /dev/null +++ b/docs/manuals/multitenant-isolation.md @@ -0,0 +1,68 @@ +# 多租户隔离 / 账套切换 操作手册 + +> 模块 slug: multitenant-isolation(对应队列 cma-module-queue.json) +> 最近一次修复后状态定稿:2026-09-07(project-20260907-opencode-multitenant-fix 完成并验证后,commit ac645a5) +> 这是**横切所有数据模块**的隔离机制手册:任何新增模块/API 都要先读「④ 已知边界/坑」再动手。 + +## ① 模块定位 +- **模块名**:多租户隔离 / 账套切换(Multi-tenant Isolation / Entity Switch) +- **解决什么问题**:同一套 CMA 系统服务多家企业,各企业(账套/entity)数据天然隔离、互不可见、互不可写。企业清单:**entity 1 = 陕西酣客(白酒)、entity 2 = 陕西博海(IT,本司)**、3-5 = demo。登录 token 绑定 entity_id,全系统按 entity_id 过滤。 +- **所属流程环节**:数据 → 账套切换(登录/会话/所有业务查询写入口的前置) +- **主要角色**:所有用户(ceo/finance/business/it,登录选账套);系统管理员(多账套授权切换);Bot 服务通道(X-BOT-KEY 无 token 白名单) + +## ② 操作步骤 +1. **登录选账套**:/login 页先点开公司下拉(登录页公司列表来自公开接口 `GET /api/cma/auth/login-entities?username=xxx`,按用户名返回其授权企业)→ 选企业 → 填账号密码 → 点「登 录」按钮(按钮文本是"登 录"中间有空格)。**不带 entity_id 登录会返回「账套模式:请选择登录企业」**。登录成功返回 `{"token": "...", "entity_id": N, "entity_name": "...", "user": {...}}`;token 是 Redis `cma:token:{token}` 的 key,value=JSON `{user_id, entity_id}`(**旧 int 格式 token 一律视为无效 → 401 强制重新登录**) +2. **切换账套**:登录后右上角账套切换器 → 列出 `GET /api/cma/auth/my-entities`(当前用户授权企业)→ 选目标企业 → 前端调 `POST /api/cma/auth/switch-entity`(body `{"entity_id": N}`,带当前 Bearer token):后端校验 user_entities 授权 → 删除旧 token → 重新签发新 token → 写 OperationLog → 前端替换 localStorage(cma_token/cma_entity_id/cma_entity_name/cma_user)+ `location.reload()`。切后所有数据视图变为新账套 +3. **越权防护语义**(后端 deps.py `get_entity_id` 解析链): + - 有 Bearer token → **强制用 token 绑定 entity**;显式 query/header/body 的 entity_id 与 token 不一致 → **403** + - 无 token(Bot 白名单通道 X-BOT-KEY / X-Entity-Id header / body entity_id)→ 校验 `Entity.status == "active"`,不通过 403,缺省回落 entity 1 + - 用户未授权企业:登录该企业 403、切换该企业 403 +4. **隔离表现**:登录 A 账套只看到 A 账套数据(KPI/预算/成本/预警/待办/知识分区);跨账套直连 API 访问他人资源 → **404**(资源归属校验语义,见下);跨账套提醒/待办不会出现 +5. **Bot 数据通道**:`/api/cma/bot/*`(X-BOT-KEY)与 `/api/cma/bot-bridge/*`(X-BRIDGE-TOKEN)无 token,entity 经 header/body 显式传递,隔离同样生效(bot_kpis 等按 KPI 归属校验) + +## ③ 验证路径 +- **自动脚本**: + 1. 专项越权测试(新增隔离用例):`cd /root/cma-management/backend && venv/bin/python3 -m pytest tests/test_multitenant_ai_20260907.py -q` → 期望 **11 passed** + 2. 工作台隔离脚本:`cd /root/cma-management/backend && venv/bin/python3 scripts/verify_dashboard_isolation.py` → 登录 entity1 → my-dashboard action_plans/reminders 回查 DB 归属全为 entity1,输出末尾 `PASS: entity1 工作台数据无跨账套泄漏` + 3. 生产双 token 越权实测配方(建临时用户→断言→删除):见 cma-fullstack-ops 技能 references/multitenant-opencode-20260907.md「生产库双 token 越权实测配方」段(cost 双实体零交集 / ai kpi-analysis 跨 entity 404 / dashboard alert_count 与 DB 各自相等 / review-plans stats / bot-kpis 跨 entity 404 / kpis create 白名单注入拦截) +- **期望输出**:上列 1/2 脚本全过;配方断言逐项 PASS +- **已测基线**(最近一次验证日期+结果):2026-09-07 pytest 专项 11 passed(16.56s);生产双 token 全断言 PASS;同批全量 pytest 745 passed / 10 failed(10 failed 经 git stash 在基线 HEAD 复跑同批 = **存量非回归**,勿当回归)/ 1 xfailed + +## ④ 已知边界 / 坑 +- **历史 bug**: + - 2026-08-11 账套模式上线:token 改 JSON 结构绑 entity_id;此前登录无企业概念,存量接口 entity_id 回落默认 1(隔离失效) + - 2026-08-31 首轮全量隔离修复(security-multitenant-isolation):12+ API 文件统一 `Depends(get_entity_id)`(原 Query(1)/Form(1)/裸 int 默认全部回落 entity 1);KPIAlert/ActionPlan/OrgNode 等表**补 entity_id 列并回填**(kpi_alerts 472 行回填后 216 行属 entity≠1,证明隔离必要);dashboard.py my_plans/reminders 曾漏 entity 过滤 → 待办含其他账套方案(commit 815acda) + - 2026-09-07 OpenCode 二轮审查 7 处缺口(commit ac645a5,P0): + - ai_analysis.py:generate_rule_suggestions 的 pending 预警按 KPIAlert.entity_id 过滤;kpi_analysis/dashboard_analysis 查询带 entity(跨企业 kpi_id → 404);dashboard_analysis 系列端点补 `Depends(get_entity_id)` 签名;**stream/分析缓存 key 改为 `dashboard_analysis:{role}:{entity_id}`(原无 entity → 跨企业串缓存)** + - dashboard.py:summary alert_count 补 KPIAlert.entity_id 过滤 + - bot_kpis.py:update_bot_kpi_value 查 KPI 后 `if kpi.entity_id != entity_id: 404`(归属校验) + - cost.py + cost_engine.py:cost_overview/variance/breakdown/dashboard 补 get_entity_id;engine 函数加 `entity_id: Optional[int]=None` 参数(None=兼容存量调用不过滤,**API 端点必须传**) + - kpis.py create_kpi:字段白名单 `UPDATE_KPI_WHITELIST | {"kpi_code"}` 循环拦截,**防 status/important_flag/主键/entity_id=999 注入**(此前 `KPIDefinition(**data)` 裸展开) +- **边界条件**: + - **404 vs 403 语义**:跨账套访问**具体资源**(他人 KPI 详情/他人 bot KPI/跨企业 kpi_id)→ 404(资源不存在语义,防探测);**登录/切换未授权企业、token 与显式 entity 不一致** → 403。改隔离代码勿把 404 改成 403 或空返回混用 + - 同名 KPI 跨账套交集是**正常现象**(两家企业可各自定义 process 指标)——实测隔离用 **KPI id 回查 DB 归属**断言,勿用 code 集合判交集 + - 空态 ≠ bug:entity2(博海)无逾期数据时 reminders 为空属正常空态 + - 知识侧分区:knowledge_events/summaries 严格 entity_id 隔离(企业记忆);knowledge_articles 共享层 **entity_id NULL = 全局共享**(查询 `or_(entity_id==cur, entity_id.is_(None))`,勿按企业复制) + - OperationLog **无 entity_id 列**:按 user_id 成员过滤(宁可漏不可串),P2 另立项 +- **⚠️ 易踩点**(新增/修改任何写端点前自查): + 1. 签名缺 `entity_id: int = Depends(get_entity_id)` + 构造函数漏 `entity_id=entity_id` + update/delete 缺 `entity_id == entity_id` 过滤——三类缺口都会静默把数据写进错误账套(2026-09-05 cost 实证:写端点整个没绑 entity,前端任何账套录入全落 entity 1) + 2. 带 Redis 缓存的接口 cache key **必须含 entity_id**(串缓存是最隐蔽的隔离失效) + 3. 改函数签名后 grep 所有**内部直调点**(FastAPI 内部直调需显式传 db/entity_id,否则 Depends 对象 AttributeError) + 4. body 读 entity_id 的创建接口用 `resolve_entity_for_request(request, data.get("entity_id") or 1)`(token 优先),勿直接 `or 1` + 5. 截断会话残留:函数体引用 entity_id 但签名缺失 → NameError;改完逐端点扫「签名有/函数体有」两列(可复用 scan_entity_isolation.py) + 6. 任务单快照可能双向过期(说"有列"实际没有 / 说"没有列"实际 8-31 已加)——动手前 inspect 生产库列再定 直滤 vs JOIN + +## ⑤ 修改记录 +| 日期 | 修改人 | 改动 | commit | 影响 | +|------|--------|------|--------|------| +| 2026-09-07 | - | 手册创建(修复后状态定稿) | - | - | +| 2026-09-07 | OpenCode审查+全栈Bot | ai_analysis/dashboard/bot_kpis/cost/kpis 隔离缺口修复(缓存key/归属404/写白名单/engine entity 参数)+ 11 条越权测试 | ac645a5 | P0 越权/串缓存 | +| 2026-09-07 | 全栈Bot | .gitignore 补 cma.db/cma_import.db | 1fa0836 | CI deploy hook | +| 2026-08-31 | 全栈Bot | 首轮全量隔离:API 统一 Depends(get_entity_id) + KPIAlert/ActionPlan 等加列回填 + dashboard 提醒过滤 | 815acda 等 | 隔离门禁 5 项 | +| 2026-08-11 | 全栈Bot | 账套模式上线(token 绑 entity、user_entities 授权) | account-mode-refactor | 登录选企业 | + +## ⑥ 关联 +- **依赖表**:users / user_entities(授权,多对多唯一约束)/ entities(账套,status=active 校验)/ OperationLog;业务表一律带 entity_id 列(kpi_definitions/kpi_values/kpi_alerts/action_plans/budget_plans/cost*/ai_suggestions...) +- **依赖接口**:POST /api/cma/auth/login(必带 entity_id);POST /api/cma/auth/switch-entity;GET /api/cma/auth/my-entities;GET /api/cma/auth/login-entities(公开);deps.py get_entity_id / resolve_entity_for_request +- **关联模块**:全部数据模块(横切);Bot 通道(/api/cma/bot/*、/bot-bridge/*) +- **验证资产**:tests/test_multitenant_ai_20260907.py(11 条)、tests/test_security_multitenant.py、tests/test_security_knowledge_partition.py(8 条)、backend/scripts/verify_dashboard_isolation.py、verify_knowledge_partition_e2e.py;配方见 cma-fullstack-ops references/multitenant-opencode-20260907.md + security-multitenant-isolation-20260831.md diff --git a/docs/manuals/mydashboard.md b/docs/manuals/mydashboard.md new file mode 100644 index 00000000..a5a5157d --- /dev/null +++ b/docs/manuals/mydashboard.md @@ -0,0 +1,64 @@ +# MyDashboard 个人工作台 操作手册 + +> 模块 slug: mydashboard(对应队列 cma-module-queue.json) +> 最近一次修复后状态定稿:2026-09-04(project-20260904-my-dashboard-period-label 完成并验证后,commit 7ea0d2e,线上 09-34 部署生效) +> 展示层口径细节见 cma-fullstack-ops 技能 references/mydashboard-display-layer-20260904.md(改口径前必读) + +## ① 模块定位 +- **模块名**:MyDashboard 个人工作台(我的工作台) +- **解决什么问题**:登录后默认落地页,个人关注 KPI 卡片总览(实际值 vs 目标、红黄绿/未评状态一目了然)+ 行动方案待办 + 提醒聚合,让员工不看报表也知道自己指标完成情况与欠账 +- **所属流程环节**:工作台 → 我的KPI(北极星「数据找人」的主动触达面,与 Dashboard 经营看板并列但定位个人) +- **主要角色**:所有角色(ceo/finance/business/it 均可用;页面只读展示,KPI 卡片点击可下钻详情/行动方案) + +## ② 操作步骤 +页面路径:登录成功默认跳 /my-dashboard;或侧边栏「我的工作台」。(zone A 只读区) + +1. **看 KPI 卡片**:每张卡左侧大字号=实际值,右侧小字=目标及频率。展示文案由展示层函数生成,格式: + - 有采集值:`periodActual(k)` = `{期间}: {数值}`(如 `8月: 15.0万`);**期间前缀是采集值所属期间**,本年显示「8月」、跨年显示「2025年12月」 + - 未采集(后端无任何 KPIValue 记录 → actual_value=null):灰字小号「**暂无数据**」(右侧目标仍显示 → 员工理解"有目标但当期未采集") + - 真零(有记录但实际值=0):显示 `8月: 0` ——与「暂无数据」天然区分,不再把采集缺失误读为业绩归零 + - 卡片标签:达标(绿)/预警(黄)/危险(红)/未评(灰),由后端 level 判定(见④,勿在前端重算) +2. **理解目标标签**:右侧 `目标(月)` / `目标(季)` / `目标(年)`,按 KPI frequency 由后端 target 取数(monthly/weekly→目标月值、quarterly/half_year→季值、yearly→年值) +3. **行动方案/提醒区**:页面下方 action_plans 待办列表与 reminders 提醒(含类型/严重度/related 关联);点「查看」跳对应详情(plan_id 直达详情仅对**当前列表存在**的行生效,跨页静默属既有行为) +4. **数据口径须知**:actual_value = 该 KPI **最新一条 KPIValue**(按 calculated_at 降序),**不按周期过滤**——可能是上月甚至更早的陈旧采集值;期间前缀就是防"陈旧值伪装成当月值"。若需精确当期数,进 KPI 详情/报表看期间序列 +5. 提醒数据空态:某些账套(如 entity2 博海)无逾期数据时 reminders 为空属正常,不是故障 + +## ③ 验证路径 +- **自动脚本**:无独立 verify_*.py 入库(该修复纯前端展示层,正式脚本未沉淀);用下列手工/半自动步骤验证,**复用配方**:cma-fullstack-ops references/mydashboard-display-layer-20260904.md「CDP 页面实测配方」段 +- **手工验证步骤**: + 1. CDP 驱动已登录 Chrome(或按技能 CDP 登录序列登录)→ 导航 https://cma.sxbh.ltd/my-dashboard → 等 ~5s + 2. dump 每张卡片:`[...document.querySelectorAll('.kpi-card')].map(c=>c.innerText.replace(/\n/g,' | '))` + 3. 判据:出现 `N月: 值` 期间前缀(=期间标签过);同屏出现「暂无数据」与 `8月: 0`(=空值/真零区分过);目标标签仍 `目标(月/季/年)`(=口径一致性) + 4. 产物/部署验证:`md5sum /var/www/cma/assets/MyDashboard-CA66065P.js /root/cma-management/frontend/dist/assets/MyDashboard-CA66065P.js` 应一致(hash 名变则以实际 chunk 名为准),且 chunk 含标记文案「暂无数据」→ curl https://cma.sxbh.ltd/assets/.js HTTP 200(懒加载 chunk 不在 index.html 主引用,直接按 hash 名验证) +- **期望输出**:卡片 dump 样例(entity1 酣客 admin): + `渠补率 | 预警 | 4月: 81.7 | 目标(月) 80`;`净利润 | 危险 | 8月: 0 | 目标(月) 62`;`营业收入 | 达标 | 8月: 15.0万 | 目标(季) 1200`;`交付及时率 | 未评 | 暂无数据 | 目标(月) 85` +- **已测基线**(最近一次验证日期+结果):2026-09-04 线上 HTTP 200 + chunk 含「暂无数据」+ md5 与 dist 一致(39b1a42dda60af0f133d2eaf73f0b776)+ 构建独立重跑 12.25s 产物 chunk 名不变(内容确定性 hash) + +## ④ 已知边界 / 坑 +- **历史 bug**: + - 2026-09-04 口径可理解性(commit 7ea0d2e,仅改 frontend/src/views/MyDashboard.vue +20/-1):①员工误把"最近采集值"(可能陈旧)当本月值 → 卡片加 `{期间}: {数值}` 前缀(fmtPeriod:本年 '8月' / 跨年 '2025年12月' / 无 period 脏数据回退 '最近');②未采集显示成 0 被误读为"业绩归零" → null 显示灰字「暂无数据」,真 0 保持 `8月: 0`。后端零改动(my_dashboard kpi_list 本已返回 period 字段) + - 2026-08-31 关联修复:dashboard my_plans/reminders 曾漏 entity 过滤(见多租户手册),跨账套待办会出现在本账套工作台 +- **边界条件**: + - **actual_value 是最新一条采集值而非"本月值"**(order_by calculated_at desc,不按 period 过滤)——这是设计(展示"最近状况"),期间前缀负责自明口径;**勿改后端取数逻辑**去"修正"为当月值,会改红黄绿判定基准 + - 空值判定:无 KPIValue 记录 → actual=None →「暂无数据」;有记录 actual=0 → 真零。**后端天然区分,前端勿把 null 当 0** + - 红黄绿:正向 ≥0.9 绿 ≥0.7 黄 否则红;反向指标集合(F_COST_RATIO/C_REBATE_RATE/F_AR_DAYS/F_REBATE_RATE/F_FACTORY_REBATE_RATE/F_COST_CONTROL_RATE/F_INV_DAYS/F_DEBT_RATIO/P_BUG_RATE/P_REWORK_PCT)≤目标绿、≤1.1 黄;actual null → level=gray(未评) + - entity2(博海)无逾期数据 → reminders 空态正常 +- **⚠️ 易踩点**: + 1. **只改展示层**:勿动 level 判定 / kpi_target_by_frequency / REVERSE_INDICATORS / 采集器(2026-09-04 任务红线;改动会破坏红黄绿口径) + 2. el-progress 百分比计算引用 `k.target_value`(`(k.actual_value||0)/k.target_value`),改展示别碰 progress + 3. 目标标签 targetLabel 按 frequency 返回 目标(月/季/年)——保持(quarterly 卡"8月实际 vs 目标(季)"是自解释口径,勿再加角标噪音) + 4. 前端改动部署后验证线上必须**先刷新页面**(懒加载旧 bundle 陷阱:旧 JS 无新 @click/文案,刷新后按 hash 确认新 chunk 才测) + 5. Vite 产物验证先 grep 原文「暂无数据」(本 chunk 中文原文未转义);hex 转义探测假阴性勿当依据 + +## ⑤ 修改记录 +| 日期 | 修改人 | 改动 | commit | 影响 | +|------|--------|------|--------|------| +| 2026-09-04 | - | 手册创建(修复后状态定稿) | - | - | +| 2026-09-04 | 全栈Bot | KPI 卡实际值加期间前缀(本月 '8月'/跨年 '2025年12月')+ 未采集显示「暂无数据」(真 0 保持 'X月: 0');仅展示层 | 7ea0d2e | 口径可理解性修复 | +| 2026-08-31 | 全栈Bot | my_plans/reminders 补 entity 过滤(关联多租户) | 815acda | 防跨账套待办 | + +## ⑥ 关联 +- **依赖表**:kpi_definitions / kpi_values(actual 取最新一条,含 period/calculated_at)/ kpi_targets(target 按 frequency)/ action_plans / kpi_alerts(reminders 来源) +- **依赖接口**:GET /api/cma/dashboard/my-dashboard(返回 kpi_list 含 period + action_plans + reminders,账套隔离) +- **关联模块**:Dashboard 经营看板(dashboard.py 同源)、预警(ai_analysis level 判定)、行动方案/自动验证(action_plans + verify.py)、KPI 采集(kpi_value_collector.py 写 period=YYYY-MM) +- **验证资产**:配方见 cma-fullstack-ops references/mydashboard-display-layer-20260904.md;隔离验证 backend/scripts/verify_dashboard_isolation.py