8.1 KiB
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 列别名:
-- 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→totalordersclosedCount→closedcountorderNo→ordernopatientName→patientnameorderType→ordertypecurrentStep→currentsteporderTime→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
// 第 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 双引号:
-- 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:
// 修改前
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:
<!-- 修复前 -->
<el-progress :percentage="statistics.drugClosedRate || 0" :stroke-width="8" />
<!-- 修复后 -->
<el-progress :percentage="Number(statistics.drugClosedRate) || 0" :stroke-width="8" />
同理修复 closedRate 的绑定:
<el-progress :percentage="Number(scope.row.closedRate) || 0" :stroke-width="12" />
修复 4:getGroupStats() 确保 key 一致性
在 OrderClosedLoopAppServiceImpl.getGroupStats() 中,显式 put camelCase key(即使修复 1 后 SQL 已正确,这是防御性编码):
// 在 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 分析决策
⚠️ 修复人员请先验证以上分析是否正确,再执行修复。