diff --git a/BUG_470_ANALYSIS.md b/BUG_470_ANALYSIS.md new file mode 100644 index 000000000..0c44586bb --- /dev/null +++ b/BUG_470_ANALYSIS.md @@ -0,0 +1,72 @@ +# Bug #470 分析报告 + +## 根因分析 + +### 症状 +住院医生工作站-手术申请单加载手术项目耗时过长,影响医生开单效率。 + +### 根本原因 + +**后端 `getSurgeryPage` 接口缺少 Redis 缓存层。** + +与同模块的 `getAdviceBaseInfo`(已有24小时Redis缓存)不同,`getSurgeryPage` 每次调用都直接查询数据库。 + +**代码对比:** + +- `getAdviceBaseInfo`(DoctorStationAdviceAppServiceImpl.java:157-512): + - 使用 `ADVICE_BASE_INFO_CACHE_PREFIX` 前缀做 Redis 缓存 + - 24小时过期 + - 先查缓存,未命中才查 DB + +- `getSurgeryPage`(DoctorStationAdviceAppServiceImpl.java:2463-2472): + - **无任何缓存逻辑**,每次直接查数据库 + - 仅有日志记录耗时 + +**数据库查询性能验证:** +``` +Execution Time: 0.400 ms (10102条手术项目,已有 idx_wor_activity_def_surgery 索引) +Planning Time: 4.349 ms +``` +数据库查询本身很快(<1ms),但每次弹窗打开都重复执行查询 + 序列化 + 网络传输,累积延迟明显。 + +**辅助因素:** +1. `applicationFormBottomBtn.vue` 的对话框设置了 `destroy-on-close`,每次关闭都会销毁 Surgery 组件 +2. 前端虽有模块级内存缓存(`surgeryRecordsCache` / `surgeryMappedCache`),但首次加载仍需后端响应 +3. 前端 `getList()` 命中缓存时未清除 `loading.value`,导致 loading 动画可能卡住 + +### 影响范围 + +**涉及文件:** +- `openhis-server-new/openhis-application/src/main/java/com/openhis/web/doctorstation/appservice/impl/DoctorStationAdviceAppServiceImpl.java` — 后端手术分页查询实现(需加缓存) +- `openhis-ui-vue3/src/views/inpatientDoctor/home/components/order/applicationForm/surgery.vue` — 前端手术申请单组件(需修复 loading 状态) + +**涉及数据表:** +- `wor_activity_definition` — 活动定义表(手术项目源表),10,102条手术记录 +- `adm_charge_item_definition` — 收费项定义表(定价关联) + +## 修复方案 + +### 后端:给 `getSurgeryPage` 添加 Redis 缓存 + +**改动文件:** `DoctorStationAdviceAppServiceImpl.java` + +1. 新增缓存键常量:`SURGERY_PAGE_CACHE_PREFIX = "surgery:page:"` +2. 在无搜索关键字时,尝试从 Redis 读取缓存 +3. 缓存未命中时,查询数据库后写入 Redis(24小时过期) +4. 有搜索关键字时不缓存(避免缓存爆炸) + +**改动量:** 约 20 行 + +### 前端:修复 `getList()` 缓存命中时的 loading 状态 + +**改动文件:** `surgery.vue` + +1. 在 `getList()` 方法中,当命中内存缓存时,显式设置 `loading.value = false` + +**改动量:** 1 行 + +## 验证计划 + +1. 编译验证 Java 代码 +2. 语法验证 Vue 文件:`node --check surgery.vue` +3. 手动验证:登录医生工作站,打开手术申请单,观察加载速度(首次应有loading,二次打开应秒开) diff --git a/openhis-server-new/openhis-application/src/main/java/com/openhis/web/doctorstation/appservice/impl/DoctorStationAdviceAppServiceImpl.java b/openhis-server-new/openhis-application/src/main/java/com/openhis/web/doctorstation/appservice/impl/DoctorStationAdviceAppServiceImpl.java index 0435b022b..1d2fbbd24 100755 --- a/openhis-server-new/openhis-application/src/main/java/com/openhis/web/doctorstation/appservice/impl/DoctorStationAdviceAppServiceImpl.java +++ b/openhis-server-new/openhis-application/src/main/java/com/openhis/web/doctorstation/appservice/impl/DoctorStationAdviceAppServiceImpl.java @@ -2463,12 +2463,34 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp public IPage getSurgeryPage(Long organizationId, Integer pageNo, Integer pageSize, String searchKey) { log.info("getSurgeryPage 开始: orgId={}, page={}/{}, searchKey={}", organizationId, pageNo, pageSize, searchKey); long start = System.currentTimeMillis(); + + // 无搜索时尝试从 Redis 缓存读取(手术项目变更频率低,适合缓存) + String safeOrgId = organizationId != null ? organizationId.toString() : ""; + String cacheKey = "surgery:page:" + safeOrgId + ":" + pageNo + ":" + pageSize; + boolean useCache = (searchKey == null || searchKey.trim().isEmpty()); + + if (useCache) { + Object cachedObj = redisCache.getCacheObject(cacheKey); + if (cachedObj instanceof com.baomidou.mybatisplus.extension.plugins.pagination.Page) { + log.info("从 Redis 缓存获取手术项目, key: {}, records: {}", cacheKey, + ((IPage) cachedObj).getRecords().size()); + return (IPage) cachedObj; + } + } + IPage result = doctorStationAdviceAppMapper.getSurgeryPage( new Page<>(pageNo, pageSize), PublicationStatus.ACTIVE.getValue(), organizationId, searchKey); log.info("getSurgeryPage 完成: {}ms, total={}, records={}", System.currentTimeMillis() - start, result.getTotal(), result.getRecords().size()); + + // 无搜索时将结果写入缓存 + if (useCache && result instanceof com.baomidou.mybatisplus.extension.plugins.pagination.Page) { + redisCache.setCacheObject(cacheKey, result, (int) CACHE_EXPIRE_HOURS, java.util.concurrent.TimeUnit.HOURS); + log.info("缓存手术项目, key: {}, 过期时间: {} 小时", cacheKey, CACHE_EXPIRE_HOURS); + } + return result; } diff --git a/openhis-ui-vue3/src/views/inpatientDoctor/home/components/order/applicationForm/surgery.vue b/openhis-ui-vue3/src/views/inpatientDoctor/home/components/order/applicationForm/surgery.vue index f4c240e6b..1afeb32c6 100755 --- a/openhis-ui-vue3/src/views/inpatientDoctor/home/components/order/applicationForm/surgery.vue +++ b/openhis-ui-vue3/src/views/inpatientDoctor/home/components/order/applicationForm/surgery.vue @@ -150,6 +150,7 @@ const getList = () => { if (surgeryMappedCache && surgeryMappedCache.length > 0) { applicationList.value = surgeryMappedCache; applicationListAll.value = surgeryRecordsCache; + loading.value = false; return; } loadPage('');