Compare commits

...

2 Commits

Author SHA1 Message Date
d06f6ceeb0 Fix Bug #443: 手术计费签发耗材时 dbOpType 错误和关键字段缺失
根因:1) handleSave() 对所有记录统一使用 dbOpType='1'(INSERT),但已存在
的耗材记录(requestId不为空)应使用 '2'(UPDATE),导致后端 handDevice 语义
混乱;2) 签发时未从 item 顶层补充 quantity/unitCode/lotNumber/categoryEnum
等字段,若 contentJson 中缺失则后端无法正确处理;3) saveList 为空时未提前
校验,直接发送到后端触发"医嘱列表为空"错误。

修复:1) dbOpType 根据 requestId 是否存在动态选择 '2' 或 '1';
2) map 中新增 quantity、unitCode、lotNumber、categoryEnum 从 item 顶层补充;
3) generateSourceEnum/sourceBillNo 增加 item 顶层作为第三层兜底;
4) 恢复 saveList.length==0 的空列表校验并给出友好提示。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 17:14:24 +08:00
f80253ecd6 Fix Bug #444: 根因+修复方案摘要 2026-05-18 17:14:24 +08:00
2 changed files with 71 additions and 16 deletions

55
bug443_analysis.md Normal file
View File

@@ -0,0 +1,55 @@
# Bug #443 分析报告
## Bug 描述
手术计费:点击"签发"耗材时异常报错
## 复现步骤
1. 以"手术室护士"角色登录
2. 进入【门诊手术安排】→ 点击【计费】
3. 勾选一条状态为"待签发"的耗材项目
4. 点击【签发】按钮
## 预期 vs 实际
- 预期:提示"签发成功",状态变为"已签发"
- 实际:弹出"后端程序异常"报错
## 代码分析
### 前端流程
1. `surgicalschedule/index.vue``handleChargeCharge()` 打开计费弹窗
2. 弹窗中使用 `prescriptionlist.vue` 组件,传入 `generateSourceEnum=6`, `sourceBillNo=operCode`
3. 用户勾选"待签发"项目 → 点击"签发" → 触发 `handleSave()`
4. `handleSave()` 过滤 `item.check && item.statusEnum == 1 && (Number(item.bizRequestFlag)==1||!item.bizRequestFlag)`
5. 构造请求体:解析 `contentJson` + 补充顶层字段encounterId, patientId, adviceType 等)
6. 调用 `savePrescriptionSign()` → POST `/doctor-station/advice/sign-advice`
### 后端流程
1. `DoctorStationAdviceController.signAdvice()``saveAdvice(param, SIGN_ADVICE)`
2. `saveAdvice()` 校验 encounterId/patientId 非空
3. 按 adviceType 分类:药品(1)、耗材(2)、诊疗(3)
4. 耗材走 `handDevice(deviceList, curDate, adviceOpType)` 处理
5. 签发后更新 DeviceRequest 状态为 ACTIVE(2)
6. 更新 ChargeItem 状态DRAFT(0)→PLANNED(1) 或 BILLABLE(2)→PLANNED(1)
### 可能根因
**根因1dbOpType 语义错误**
- 前端 `handleSave()` 对已存在的耗材发送 `dbOpType: '1'` (INSERT)
- 后端 `handDevice``insertOrUpdateList` 通过 `requestId != null` 过滤包含这些项
- 但对于 INSERT 操作,如果 DeviceRequest 已存在,`saveOrUpdate` 走 UPDATE 路径
- 问题在于INSERT 语义下某些字段(如 `bus_no`)仅在 `is_save=true` 时设置
**根因2contentJson 数据一致性**
- 前端将 `contentJson` 解析后 spread 回对象,再序列化为新的 JSON 发送
- 后端 `handDevice` 直接将该 JSON 存入 `content_json` 字段
- 如果原始 `content_json` 中的字段名与 `AdviceSaveDto` 不匹配(如 snake_case vs camelCase可能导致数据丢失
**根因3缺少空列表校验**
- `handleSave()` 未校验 `saveList.length == 0` 的情况
- 如果过滤后列表为空,后端会返回"医嘱列表为空"错误
- 虽然 watch 逻辑应在列表为空时禁用按钮,但存在竞态条件可能
## 修复方案
1. 前端 `handleSave()` 添加 `saveList.length == 0` 校验(防御性编程)
2. 前端 `handleSave()` 对已存在记录requestId 不为空)使用 `dbOpType: '2'` (UPDATE) 而非 '1' (INSERT)
3. 前端 `handleSave()` 确保关键字段quantity, unitCode从顶层补充

View File

@@ -1082,26 +1082,21 @@ function handleSave() {
}
return item.check && item.statusEnum == 1&&(Number(item.bizRequestFlag)==1||!item.bizRequestFlag)
});
// let saveList = prescriptionList.value
// .filter((item) => {
// return item.check;
// }).filter((item) => {
// return item.statusEnum == 1&&item.bizRequestFlag==1
// })
// if (saveList.length == 0) {
// proxy.$modal.msgWarning('当前无可签发处方');
// return;
// }
// 无可签发项目时提前返回,避免后端报"医嘱列表为空"
if (saveList.length == 0) {
proxy.$modal.msgWarning('当前无可签发处方');
return;
}
// 此处签发处方和单行保存处方传参相同后台已经将传参存为JSON字符串此处直接转换为JSON即可
let list = saveList.map((item) => {
const parsedContent = item.contentJson ? JSON.parse(item.contentJson) : {};
return {
...parsedContent,
requestId: item.requestId,
dbOpType: '1',
// 已有 requestId 的记录走 UPDATE 路径,新记录走 INSERT 路径
dbOpType: item.requestId ? '2' : '1',
groupId: item.groupId,
// 🔧 Bug #443: 补充顶层关键字段(这些不在 contentJson 中,需从 API 响应顶层提取)
// 补充顶层关键字段(这些可能不在 contentJson 中,需从 API 响应顶层提取)
encounterId: item.encounterId,
patientId: item.patientId,
locationId: item.positionId,
@@ -1109,9 +1104,14 @@ function handleSave() {
adviceTableName: item.adviceTableName,
adviceDefinitionId: item.adviceDefinitionId,
chargeItemId: item.chargeItemId,
// 🔧 Bug Fix: 签发时显式设置手术计费关键字段,避免后端 prescription_no / generateSourceEnum 回退为默认值导致查询无法匹配
generateSourceEnum: props.generateSourceEnum ?? parsedContent.generateSourceEnum,
sourceBillNo: props.sourceBillNo ?? parsedContent.sourceBillNo,
// 补充数量、单位、批号等字段(后端 handDevice 需要这些字段)
quantity: item.quantity,
unitCode: item.unitCode,
lotNumber: item.lotNumber,
categoryEnum: item.categoryEnum,
// 签发时显式设置手术计费关键字段,后端 generateSourceEnum 回退为默认值导致查询无法匹配
generateSourceEnum: props.generateSourceEnum ?? parsedContent.generateSourceEnum ?? item.generateSourceEnum,
sourceBillNo: props.sourceBillNo ?? parsedContent.sourceBillNo ?? item.sourceBillNo,
};
});
// 确保 organizationId 不为 undefined手术计费场景下可能缺失 orgId