diff --git a/MD/bugs/BUG_732_ANALYSIS.md b/MD/bugs/BUG_732_ANALYSIS.md new file mode 100644 index 000000000..93189eaca --- /dev/null +++ b/MD/bugs/BUG_732_ANALYSIS.md @@ -0,0 +1,195 @@ +# Bug #732 诸葛亮分析报告 + +> **文档类型**: Bug分析 +> **分析时间**: 2026-06-13 00:30:55 +> **分析模型**: mimo-v2.5 (LLM深度分析) + +--- + +## 基本信息 +- **Bug #**: 732 +- **标题**: 【医嘱闭环-闭环统计】科室的闭环和未闭环医嘱预警加载卡死 +- **模块**: 系统管理 +- **提出人**: 王栩坤 + +--- + +Based on my analysis, I now have a clear picture of the root cause. Let me output the structured analysis. + +--- + +### 一、Bug 理解 + +**禅道原文引用:** +- **标题**:【医嘱闭环-闭环统计】科室的闭环和未闭环医嘱预警加载卡死 +- **重现步骤**:登录内科医生1的账号:doctor1 密码:123456进入医嘱闭环打开闭环统计查看界面 +- **结果**:出现报错:`main.js:84 TypeError: Cannot convert object to primitive value` +- **期望**:能够正常显示报卡的信息和操作界面 + +**附图分析关键信息:** +- 页面标题"科室/医生闭环统计",右侧有"按科室"下拉和"刷新"按钮 +- 两个数据区域(科室/医生闭环统计表格、未闭环医嘱预警表格)均为空白,仅有蓝色加载动画圆圈(loading spinner) +- 右上角红色标签"X 条待处理"但数字被遮挡 +- **核心异常**:控制台 `TypeError: Cannot convert object to primitive value`,导致两个表格数据加载卡死 + +**综合总结**:用户登录 doctor1 账号进入医嘱闭环统计页面,页面框架正常渲染但两个核心数据表格(科室/医生闭环统计、未闭环医嘱预警)均卡在 loading 状态无法加载数据。控制台报 `TypeError: Cannot convert object to primitive value`,表明前端在渲染过程中遇到了一个对象值被当作原始值使用的场景。 + +--- + +### 二、根因分析 + +经过全链路追踪(前端 Vue 组件 → API → Controller → AppService → Mapper SQL),根因是**多层问题叠加**: + +#### 根因 1(直接触发 TypeError):PostgreSQL 列别名大小写折叠 + 前端 prop 不匹配 + +`OrderExecuteRecordMapper.java` 中的 4 个 SQL 查询使用了 camelCase 列别名: +```sql +-- selectGroupByDepartment +SELECT COALESCE(m.department_name, '未知') AS department, COUNT(*) AS totalOrders, ... + +-- selectGroupByDoctor +SELECT COALESCE(m.doctor_name, '未知') AS doctorName, COUNT(*) AS totalOrders, ... + +-- selectUnclosedWarnings +SELECT e.order_no AS orderNo, e.patient_name AS patientName, ... +``` + +**PostgreSQL 会将未加引号的标识符折叠为小写**。因此 JDBC ResultSet 返回的 Map key 全部是小写: +- `totalOrders` → `totalorders` +- `closedCount` → `closedcount` +- `orderNo` → `orderno` +- `patientName` → `patientname` +- `orderType` → `ordertype` +- `currentStep` → `currentstep` +- `orderTime` → `ordertime` + +Java 的 `getLong()`/`getString()` helper 有 `key.toLowerCase()` fallback,所以 Java 层计算正确。但 **`getGroupStats()` 返回的 Map 中同时存在**: +- 小写 key(来自 SQL):`totalorders`, `closedcount` +- camelCase key(Java 代码 put):`unclosedCount`, `closedRate` + +前端模板使用 `prop="totalOrders"` 无法匹配 `totalorders`,导致表格列值为 `undefined`。 + +#### 根因 2(直接触发 TypeError):`el-progress` 接收到非 number 类型的 `percentage` + +在 `` 中: +- `scope.row.closedRate` 是 Java 计算后 put 进 map 的 `double`,序列化为 JSON number → 正常 +- 但 `scope.row.closedRate` 如果因上游数据异常为非数字对象,`||` 不会拦截(对象是 truthy),`el-progress` 内部尝试数值转换时抛出 `TypeError: Cannot convert object to primitive value` + +#### 根因 3(数据问题):`getUnclosedWarnings()` 未用 helper 处理 `orderTime` + +```java +// 第 143 行 +Object orderTimeObj = row.get("orderTime"); // ← PostgreSQL 返回的是 "ordertime",永远 null +``` + +导致所有未闭环医嘱的 `overdueDuration` 都显示"未知",`orderTime` 都显示空字符串。 + +#### 根因 4(缺失数据源):`order_execute_record` 表与 `order_main` 表无关联数据 + +`selectGroupByDepartment` 等 SQL 做了 `LEFT JOIN order_main m ON e.order_no = m.order_no`,但 `order_execute_record` 是闭环追踪表,其中的 `order_no` 可能与 `order_main`(门诊挂号单表)中的 `order_no` **语义不同**——`order_main` 是门诊挂号/预约表,不是医嘱表。即使 JOIN 成功,`department_name` 和 `doctor_name` 也可能全为 NULL。 + +**涉及的关键文件:** + +| 层 | 文件 | 行号 | +|---|---|---| +| Mapper SQL | `OrderExecuteRecordMapper.java` | 全部 4 个 `@Select` 方法 | +| AppService | `OrderClosedLoopAppServiceImpl.java` | `getGroupStats()` L131-143, `getUnclosedWarnings()` L146-168 | +| 前端组件 | `statistics/index.vue` | L74 `el-progress`, L84-88 `el-table-column`, L99-102 `el-progress` | +| 前端 API | `orderclosedloop.js` | `getClosedLoopStatistics()` | + +--- + +### 三、修复方案 + +#### 修复 1:SQL 列别名加双引号保留大小写(根本修复) + +修改 `OrderExecuteRecordMapper.java`,所有 SQL 别名加 PostgreSQL 双引号: + +```sql +-- selectOverviewByType +SELECT e.order_type AS "orderType", + COUNT(*) AS "totalOrders", + COUNT(CASE WHEN e.execute_status = 'completed' THEN 1 END) AS "closedCount" + +-- selectGroupByDepartment +SELECT COALESCE(m.department_name, '未知') AS "department", + COUNT(*) AS "totalOrders", + COUNT(CASE WHEN e.execute_status = 'completed' THEN 1 END) AS "closedCount" + +-- selectGroupByDoctor +SELECT COALESCE(m.doctor_name, '未知') AS "doctorName", + COUNT(*) AS "totalOrders", + COUNT(CASE WHEN e.execute_status = 'completed' THEN 1 END) AS "closedCount" + +-- selectUnclosedWarnings +SELECT e.order_no AS "orderNo", + e.patient_name AS "patientName", + e.order_type AS "orderType", + COALESCE(m.department_name, '未知') AS "department", + COALESCE(m.doctor_name, '未知') AS "doctorName", + e.current_step AS "currentStep", + e.create_time AS "orderTime" +``` + +这样 JDBC 返回的 Map key 就是精确的 camelCase,前端 `prop` 能正确匹配。 + +#### 修复 2:`getUnclosedWarnings()` 使用 helper 处理 `orderTime` + +将 `row.get("orderTime")` 改为使用 `getString()` helper 或直接用小写 key: + +```java +// 修改前 +Object orderTimeObj = row.get("orderTime"); + +// 修改后(兼容大小写) +Object orderTimeObj = row.get("orderTime"); +if (orderTimeObj == null) { + orderTimeObj = row.get("ordertime"); +} +``` + +或者更彻底地,在修复 1(SQL 加引号)之后,`row.get("orderTime")` 就能正确获取值。 + +#### 修复 3:前端 `el-progress` 添加防御性数值转换 + +在 `statistics/index.vue` 中,确保传给 `el-progress` 的 `percentage` 始终为 number: + +```html + + + + + +``` + +同理修复 `closedRate` 的绑定: +```html + +``` + +#### 修复 4:`getGroupStats()` 确保 key 一致性 + +在 `OrderClosedLoopAppServiceImpl.getGroupStats()` 中,显式 put camelCase key(即使修复 1 后 SQL 已正确,这是防御性编码): + +```java +// 在 item.put("unclosedCount", ...) 之前加: +item.put("totalOrders", totalOrders); +item.put("closedCount", closedCount); +``` + +--- + +### 四、路由决策 + +**FIXER: zhaoyun(赵云 — 前端开发)** + +**REASON**: 此 Bug 的直接表现是前端 TypeError 导致页面卡死,核心修复在前端模板的防御性编码(修复 3)和后端 Mapper SQL 的列别名引号处理(修复 1/2/4)。赵云负责前端修复(`statistics/index.vue` 中 `el-progress` 的数值转换),同时需要协调荀彧协助后端 Mapper SQL 修改(`OrderExecuteRecordMapper.java`)。前端权重更高(页面卡死的直接原因是前端渲染),由赵云主导。 + +--- + +## 路由决策 +- **FIXER_ID**: zhaoyun +- **修复 Agent**: zhaoyun(前端) +- **原因**: LLM 分析决策 + +> ⚠️ 修复人员请先验证以上分析是否正确,再执行修复。