Files
his/MD/bugs/BUG_732_ANALYSIS.md

8.1 KiB
Raw Blame History

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直接触发 TypeErrorPostgreSQL 列别名大小写折叠 + 前端 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 全部是小写:

  • totalOrderstotalorders
  • closedCountclosedcount
  • orderNoorderno
  • patientNamepatientname
  • orderTypeordertype
  • currentStepcurrentstep
  • orderTimeordertime

Java 的 getLong()/getString() helper 有 key.toLowerCase() fallback所以 Java 层计算正确。但 getGroupStats() 返回的 Map 中同时存在

  • 小写 key来自 SQLtotalorders, closedcount
  • camelCase keyJava 代码 putunclosedCount, closedRate

前端模板使用 prop="totalOrders" 无法匹配 totalorders,导致表格列值为 undefined

根因 2直接触发 TypeErrorel-progress 接收到非 number 类型的 percentage

<el-progress :percentage="scope.row.closedRate || 0"> 中:

  • scope.row.closedRate 是 Java 计算后 put 进 map 的 double,序列化为 JSON number → 正常
  • scope.row.closedRate 如果因上游数据异常为非数字对象,|| 不会拦截(对象是 truthyel-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_namedoctor_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()

三、修复方案

修复 1SQL 列别名加双引号保留大小写(根本修复)

修改 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 能正确匹配。

修复 2getUnclosedWarnings() 使用 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");
}

或者更彻底地,在修复 1SQL 加引号)之后,row.get("orderTime") 就能正确获取值。

修复 3前端 el-progress 添加防御性数值转换

statistics/index.vue 中,确保传给 el-progresspercentage 始终为 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" />

修复 4getGroupStats() 确保 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.vueel-progress 的数值转换),同时需要协调荀彧协助后端 Mapper SQL 修改(OrderExecuteRecordMapper.java)。前端权重更高(页面卡死的直接原因是前端渲染),由赵云主导。


路由决策

  • FIXER_ID: zhaoyun
  • 修复 Agent: zhaoyun前端
  • 原因: LLM 分析决策

⚠️ 修复人员请先验证以上分析是否正确,再执行修复。