docs(bug): 诸葛亮分析报告 Bug #732
This commit is contained in:
195
MD/bugs/BUG_732_ANALYSIS.md
Normal file
195
MD/bugs/BUG_732_ANALYSIS.md
Normal file
@@ -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`
|
||||
|
||||
在 `<el-progress :percentage="scope.row.closedRate || 0">` 中:
|
||||
- `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
|
||||
<!-- 修复前 -->
|
||||
<el-progress :percentage="statistics.drugClosedRate || 0" :stroke-width="8" />
|
||||
|
||||
<!-- 修复后 -->
|
||||
<el-progress :percentage="Number(statistics.drugClosedRate) || 0" :stroke-width="8" />
|
||||
```
|
||||
|
||||
同理修复 `closedRate` 的绑定:
|
||||
```html
|
||||
<el-progress :percentage="Number(scope.row.closedRate) || 0" :stroke-width="12" />
|
||||
```
|
||||
|
||||
#### 修复 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 分析决策
|
||||
|
||||
> ⚠️ 修复人员请先验证以上分析是否正确,再执行修复。
|
||||
Reference in New Issue
Block a user