Fix Bug #437: 【门诊手术计费】保存签章TOCTOU竞态致重复提交,且耗材计费项目缺失/重复、手术单号未关联

Fix: 频次总量计算改用字典store动态读取,el-input-number新增@input实时计算
This commit is contained in:
wangjian963
2026-05-14 12:00:18 +08:00
committed by 关羽
parent 4a03088ca0
commit 41bdfb13b8
6 changed files with 55 additions and 68 deletions

View File

@@ -107,8 +107,4 @@ public class OpScheduleDto extends OpSchedule {
*/
private String createByName;
/**
* 费用类别
*/
private String feeType;
}

View File

@@ -1529,6 +1529,7 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
deviceRequest.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.DEVICE_RES_NO.getPrefix(), 4));
}
deviceRequest.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 生成来源
deviceRequest.setPrescriptionNo(adviceSaveDto.getSourceBillNo()); // 来源业务单据号(手术单号)
deviceRequest.setQuantity(adviceSaveDto.getQuantity()); // 请求数量
deviceRequest.setUnitCode(adviceSaveDto.getUnitCode()); // 请求单位编码
deviceRequest.setLotNumber(adviceSaveDto.getLotNumber());// 产品批号
@@ -1835,6 +1836,7 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
serviceRequest.setBusNo(assignSeqUtil.getSeqByDay(AssignSeqEnum.SERVICE_RES_NO.getPrefix(), 4));
}
serviceRequest.setGenerateSourceEnum(GenerateSource.DOCTOR_PRESCRIPTION.getValue()); // 生成来源
serviceRequest.setPrescriptionNo(adviceSaveDto.getSourceBillNo()); // 来源业务单据号(手术单号)
serviceRequest.setQuantity(adviceSaveDto.getQuantity()); // 请求数量
serviceRequest.setUnitCode(adviceSaveDto.getUnitCode()); // 请求单位编码
@@ -2030,10 +2032,10 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
CommonConstants.TableName.MED_MEDICATION_REQUEST, CommonConstants.TableName.WOR_DEVICE_REQUEST,
CommonConstants.TableName.WOR_SERVICE_REQUEST, practitionerId, Whether.NO.getCode(),
sourceEnum, sourceBillNo);
// 手术计费场景sourceBillNo 不为空时,只保留诊疗请求3/6过滤掉药品1耗材2
// 手术计费场景sourceBillNo 不为空时过滤掉药品1,保留耗材2和诊疗3/6
if (sourceBillNo != null && !sourceBillNo.isEmpty()) {
requestBaseInfo.removeIf(dto -> dto.getAdviceType() != null
&& (dto.getAdviceType() == 1 || dto.getAdviceType() == 2));
&& dto.getAdviceType() == 1);
}
for (RequestBaseDto requestBaseDto : requestBaseInfo) {
// 请求状态

View File

@@ -539,7 +539,8 @@
AND T1.refund_medicine_id IS NULL
ORDER BY T1.status_enum,T1.sort_number)
UNION ALL
-- 🔧 新增:查询门诊术中计费生成的耗材数据(这些数据存在于 adm_charge_item 和 wor_device_request
-- 🔧 查询仅存在于 adm_charge_item 的"孤儿"耗材数据DeviceRequest 缺失或 generate_source_enum 未设置
-- 正常 DeviceRequestgenerate_source_enum 已赋值)由下方 Part 3 统一负责,此处不做重复覆盖避免 UNION ALL 重复行
(SELECT 2 AS advice_type,
CI.service_id AS request_id,
CI.service_id || '-ci-dev' AS unique_key,
@@ -584,7 +585,7 @@
WHERE CI.delete_flag = '0'
AND CI.service_table = 'wor_device_request'
<if test="generateSourceEnum != null">
AND (DR.generate_source_enum IS NULL OR DR.generate_source_enum = #{generateSourceEnum})
AND DR.generate_source_enum IS NULL <!-- 仅匹配孤儿记录normal DeviceRequest 由 Part 3 负责,避免 UNION ALL 重复 -->
</if>
<if test="historyFlag == '0'.toString()">
AND CI.encounter_id = #{encounterId}

View File

@@ -1,3 +1,5 @@
import useDictStore from '@/store/modules/dict';
// 日期格式化
export function parseTime(time, pattern) {
if (arguments.length === 0 || !time) {
@@ -275,30 +277,13 @@ export function blobValidate(data) {
// 按照频次天数计算总数量
export function calculateQuantityByDays(frequency, days) {
// const dict = useDict('rate_code').rate_code.value
// const rate = dict.find(item => item.value === frequency).remark
// if(rate){
// return Math.floor(Number(rate) * days)
// } else {
// return undefined
// }
const frequencyMap = {
ST: 1,
QD: 1, // 每日一次
BID: 2, // 每日两次
TID: 3, // 每日三次
QID: 4, // 每日四次
QN: 1, // 每晚一次
QOD: 1 / 2, // 每隔一日一次
QW: 1 / 7, // 每周一次
BIW: 2 / 7, // 每周两次
TIW: 3 / 7, // 每周三次
QOW: 1 / 14, // 隔周一次
};
if (!frequencyMap[frequency]) {
return;
}
const quantity = frequencyMap[frequency] * days;
const dicts = useDictStore().getDict('rate_code');
if (!dicts) return;
const dict = dicts.find(item => item.value === frequency);
if (!dict?.remark) return;
const rate = Number(dict.remark);
if (isNaN(rate) || !rate) return;
const quantity = rate * days;
return quantity < 1 ? 1 : Math.ceil(quantity);
}

View File

@@ -461,7 +461,7 @@ watch(
console.log(prescriptionList.value,"prescriptionList.value")
if(newValue&&newValue.length>0){
let saveList = prescriptionList.value.filter((item) => {
return item.statusEnum == 1&&(Number(item.bizRequestFlag)==1||!item.bizRequestFlag)
return item.check && item.statusEnum == 1&&(Number(item.bizRequestFlag)==1||!item.bizRequestFlag)
})
console.log(saveList,"prescriptionList.value")
if (saveList.length == 0) {
@@ -1015,7 +1015,7 @@ function handleSave() {
return;
}
let saveList = prescriptionList.value.filter((item) => {
return item.statusEnum == 1&&(Number(item.bizRequestFlag)==1||!item.bizRequestFlag)
return item.check && item.statusEnum == 1&&(Number(item.bizRequestFlag)==1||!item.bizRequestFlag)
});
// let saveList = prescriptionList.value
// .filter((item) => {
@@ -1080,42 +1080,44 @@ function handleSaveSign(row, index) {
proxy.$modal.msgWarning('诊疗项目必须选择执行科室');
return;
}
isSaving.value = true; // #437 立即加锁,消除 TOCTOU 竞态
proxy.$refs['formRef' + index].validate((valid) => {
if (valid) {
isSaving.value = true; // #437 加
row.isEdit = false;
isAdding.value = false;
expandOrder.value = [];
row.patientId = props.patientInfo.patientId;
row.encounterId = props.patientInfo.encounterId;
row.accountId = props.patientInfo.accountId;
const cleanRow = JSON.parse(JSON.stringify(row));
cleanRow.contentJson = JSON.stringify(cleanRow);
cleanRow.dbOpType = cleanRow.requestId ? '2' : '1';
cleanRow.minUnitQuantity = cleanRow.quantity * cleanRow.partPercent;
cleanRow.categoryEnum = cleanRow.adviceType
// 如果是手术计费,设置生成来源和来源业务单据号
if (props.patientInfo.sourceBillNo) {
cleanRow.generateSourceEnum = 6; // 手术计费
cleanRow.sourceBillNo = props.patientInfo.sourceBillNo;
}
console.log('cleanRow', cleanRow)
savePrescription({ adviceSaveList: [cleanRow] }).then((res) => {
if (res.code === 200) {
proxy.$modal.msgSuccess('保存成功');
getListInfo(false);
nextId.value = 1;
// 🔧 Bug Fix #238: 如果诊疗项目缺少执行科室,标记为需要修复的脏数据
if (row.adviceType === 3 && !row.orgId) {
console.warn('Bug #238: 检测到诊疗项目保存时缺少执行科室,请手动编辑修正:', cleanRow);
proxy.$modal.msgWarning('诊疗项目执行科室信息不完整,请编辑后重新保存');
}
}
}).finally(() => {
isSaving.value = false; // #437 释放锁
});
if (!valid) {
isSaving.value = false; // 验证失败释放
return;
}
});
row.isEdit = false;
isAdding.value = false;
expandOrder.value = [];
row.patientId = props.patientInfo.patientId;
row.encounterId = props.patientInfo.encounterId;
row.accountId = props.patientInfo.accountId;
const cleanRow = JSON.parse(JSON.stringify(row));
cleanRow.contentJson = JSON.stringify(cleanRow);
cleanRow.dbOpType = cleanRow.requestId ? '2' : '1';
cleanRow.minUnitQuantity = cleanRow.quantity * cleanRow.partPercent;
cleanRow.categoryEnum = cleanRow.adviceType
// 如果是手术计费,设置生成来源和来源业务单据号
if (props.patientInfo.sourceBillNo) {
cleanRow.generateSourceEnum = 6; // 手术计费
cleanRow.sourceBillNo = props.patientInfo.sourceBillNo;
}
console.log('cleanRow', cleanRow)
savePrescription({ adviceSaveList: [cleanRow] }, '1').then((res) => {
if (res.code === 200) {
proxy.$modal.msgSuccess('保存成功');
getListInfo(false);
nextId.value = 1;
// 🔧 Bug Fix #238: 如果诊疗项目缺少执行科室,标记为需要修复的脏数据
if (row.adviceType === 3 && !row.orgId) {
console.warn('Bug #238: 检测到诊疗项目保存时缺少执行科室,请手动编辑修正:', cleanRow);
proxy.$modal.msgWarning('诊疗项目执行科室信息不完整,请编辑后重新保存');
}
}
}).finally(() => {
isSaving.value = false; // #437 释放锁
});
})
}
// 签退

View File

@@ -315,6 +315,7 @@
data-prop="dispensePerDuration">
<el-input-number v-model="scope.row.dispensePerDuration" style="width: 80px" :min="1"
controls-position="right" :controls="false" :ref="(el) => (inputRefs.dispensePerDuration = el)"
@input="calculateTotalAmount(scope.row, scope.$index)"
@change="calculateTotalAmount(scope.row, scope.$index)"
@keyup.enter.prevent="
handleEnter('dispensePerDuration', scope.row, scope.$index)