Compare commits

..

7 Commits

Author SHA1 Message Date
关羽
f4aa45be89 Fix Bug #497: 【住院医生工作站-检查申请】检查申请列表缺失"申请单状态"列及全流程闭环状态流转逻辑
根因分析:
1. SQL CASE 映射不完整:status_enum=3(COMPLETED) 直接映射为应用状态 4(已接收),
   跳过了 2(已校对) 和 3(待接收)
2. status_enum=8 在数据中存在但枚举类中缺失定义
3. 前端已完整实现状态列和交互逻辑,问题在后端返回的状态值不正确

修复内容:
- RequestFormManageAppMapper.xml: 重构 SQL CASE 语句
  - status_enum=3 + performer_check_id 有值 → 2(已校对),利用护士校对标记区分
  - status_enum=3 + performer_check_id 为空 → 4(已接收)
  - status_enum=4(ON_HOLD) → 3(待接收)
  - status_enum=5/6/7 → 7(已作废)
  - status_enum=8 → 6(已出报告)
- RequestStatus.java: 补充 COMPLETED_REPORT(8) 枚举值

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 14:26:19 +08:00
关羽
80a32e23bf Fix Bug #495: 【医嘱闭环】已校对医嘱无法流转至"医嘱执行"界面,导致费用无法提交执行
Root cause: In getInpatientAdvicePage(), encounterIds and exeStatus were nullified
before buildQueryWrapper to prevent auto-generated SQL conditions, but requestStatus
was NOT nullified. HisQueryUtils.buildQueryWrapper uses reflection to add eq conditions
for ALL non-null fields, so requestStatus: 3 became an extra SQL filter
"AND request_status = 3" that was not intended for the 医嘱执行 page.

The 医嘱执行 page uses exeStatus (not requestStatus) for execution state filtering.
The SQL already handles verified/unverified order filtering via a CASE condition
on status_enum and performer_check_id. The requestStatus parameter is only meant
for frontend tab selection and should not be used as a SQL filter here.

Fix: Nullify requestStatus before buildQueryWrapper, same as encounterIds/exeStatus.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 14:26:19 +08:00
赵云
2be66eff5d Fix Bug #471: 手术管理-门诊手术安排:手术申请查询结果中混入住院检验申请单数据(脏数据)
根因:门诊手术安排查询弹窗调用 /reg-doctorstation/request-form/get-surgery-page 接口,
SQL 过滤 type_code = '24',但实际手术申请单的 type_code 存储为 'SURGERY'(非'24'),
导致查询返回0条手术记录。同时检验申请单(type_code='22')使用 PAR 前缀处方号,在缺少
type_code 有效过滤时可能混入结果。

修复:将 SQL 过滤器从 type_code = #{typeCode} 改为 type_code IN (#{typeCode}, 'SURGERY'),
兼容两种 type_code 值,确保只返回手术申请单,排除检验/检查申请单数据。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 13:13:26 +08:00
关羽
41bea23116 Fix Bug #469: [住院医生工作站-检验申请] 完善【操作】列临床业务逻辑:支持按状态动态切换修改、删除、撤回等功能
1. 修复删除/撤回接口参数错误:前端传prescriptionNo但后端期望requestFormId
2. 修改handleEdit从占位提示改为打开编辑弹窗,复用laboratoryTests组件并传入editData
3. laboratoryTests新增editData prop和编辑模式:支持descJson表单回显、已选项目回填、提交时携带requestFormId

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 13:12:28 +08:00
关羽
12382503f4 Fix Bug #489: 【医嘱闭环】医生站签发单条长期药品医嘱,护士校对界面生成重复(两条)待校对记录
Root cause: The SQL query in AdviceProcessAppMapper.xml used a plain LEFT JOIN with
med_medication_dispense on med_req_id. When a single medication request had multiple
dispense records (e.g., from repeated executions or summary operations), the JOIN
produced multiple rows per request — up to 222 rows for one request. SELECT DISTINCT
could not deduplicate because dispense_status values differed across rows.

Fix: Replace the plain LEFT JOIN with LEFT JOIN LATERAL (subquery ORDER BY create_time
DESC LIMIT 1) to fetch only the most recent dispense record per medication request.
This ensures exactly one row per request regardless of how many dispense records exist.

Verified: SQL query now returns 0 duplicate rows across all medication requests.
2026-05-14 13:10:51 +08:00
赵云
ae50a7042e Fix Bug #468: [住院医生工作站-检验申请] 列表页新增【单据状态】列
列表页增加单据状态列(位于申请单号之后),使用 el-tag 显示状态:
- applyStatus=0 显示"待开立"(灰色标签)
- applyStatus=1 显示"已开立"(绿色标签)
- 已收费且已执行显示"已执行"(绿色标签)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 13:06:59 +08:00
赵云
9b1ac64cd6 Fix Bug #464: [目录管理-诊疗目录] 新增项目时"零售价"未与"诊疗子项"合计总价自动同步
根因:calculateTotalPrice中form.value.retailPrice赋值被nextTick包裹,
在多调用方(watcher/selectRow/addItem)并发时产生竞态,导致零售价更新丢失
修复:移除nextTick,改为同步赋值确保零售价实时同步总价

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 13:03:46 +08:00
7 changed files with 158 additions and 31 deletions

View File

@@ -178,6 +178,8 @@ public class AdviceProcessAppServiceImpl implements IAdviceProcessAppService {
inpatientAdviceParam.setEncounterIds(null); inpatientAdviceParam.setEncounterIds(null);
Integer exeStatus = inpatientAdviceParam.getExeStatus(); Integer exeStatus = inpatientAdviceParam.getExeStatus();
inpatientAdviceParam.setExeStatus(null); inpatientAdviceParam.setExeStatus(null);
// requestStatus由前端tab控制后端SQL已通过CASE条件处理校对状态过滤无需再作为SQL条件
inpatientAdviceParam.setRequestStatus(null);
// 构建查询条件 // 构建查询条件
QueryWrapper<InpatientAdviceParam> queryWrapper QueryWrapper<InpatientAdviceParam> queryWrapper
= HisQueryUtils.buildQueryWrapper(inpatientAdviceParam, null, null, null); = HisQueryUtils.buildQueryWrapper(inpatientAdviceParam, null, null, null);

View File

@@ -280,9 +280,13 @@
aa.balance_amount aa.balance_amount
) AS personal_account ) AS personal_account
ON personal_account.encounter_id = ae.id ON personal_account.encounter_id = ae.id
LEFT JOIN med_medication_dispense mmd LEFT JOIN LATERAL (
ON mmd.med_req_id = T1.id SELECT status_enum
AND mmd.delete_flag = '0' FROM med_medication_dispense
WHERE med_req_id = T1.id AND delete_flag = '0'
ORDER BY create_time DESC
LIMIT 1
) mmd ON true
WHERE T1.delete_flag = '0' WHERE T1.delete_flag = '0'
AND T1.refund_medicine_id IS NULL AND T1.refund_medicine_id IS NULL
AND T1.generate_source_enum = #{doctorPrescription} AND T1.generate_source_enum = #{doctorPrescription}

View File

@@ -13,15 +13,14 @@
drf.requester_id, drf.requester_id,
drf.create_time, drf.create_time,
ap.NAME AS patient_name, ap.NAME AS patient_name,
CASE MIN(wsr.status_enum) CASE
WHEN 1 THEN 0 WHEN MIN(wsr.status_enum) = 1 THEN 0
WHEN 2 THEN 1 WHEN MIN(wsr.status_enum) = 2 THEN 1
WHEN 3 THEN 4 WHEN MIN(wsr.status_enum) = 3 AND MAX(CASE WHEN wsr.performer_check_id IS NOT NULL THEN 1 ELSE 0 END) = 1 THEN 2
WHEN 4 THEN 4 WHEN MIN(wsr.status_enum) = 3 THEN 4
WHEN 5 THEN 5 WHEN MIN(wsr.status_enum) = 4 THEN 3
WHEN 6 THEN 5 WHEN MIN(wsr.status_enum) = 5 OR MIN(wsr.status_enum) = 6 OR MIN(wsr.status_enum) = 7 THEN 7
WHEN 7 THEN 5 WHEN MIN(wsr.status_enum) = 8 THEN 6
WHEN 8 THEN 6
ELSE NULL ELSE NULL
END AS status END AS status
FROM doc_request_form AS drf FROM doc_request_form AS drf
@@ -41,15 +40,14 @@
AND drf.create_time &lt;= (#{endDate}::date + INTERVAL '1 day' - INTERVAL '1 second') AND drf.create_time &lt;= (#{endDate}::date + INTERVAL '1 day' - INTERVAL '1 second')
</if> </if>
<if test="status != null and status != ''"> <if test="status != null and status != ''">
AND CASE MIN(wsr.status_enum) AND CASE
WHEN 1 THEN 0 WHEN MIN(wsr.status_enum) = 1 THEN 0
WHEN 2 THEN 1 WHEN MIN(wsr.status_enum) = 2 THEN 1
WHEN 3 THEN 4 WHEN MIN(wsr.status_enum) = 3 AND MAX(CASE WHEN wsr.performer_check_id IS NOT NULL THEN 1 ELSE 0 END) = 1 THEN 2
WHEN 4 THEN 4 WHEN MIN(wsr.status_enum) = 3 THEN 4
WHEN 5 THEN 5 WHEN MIN(wsr.status_enum) = 4 THEN 3
WHEN 6 THEN 5 WHEN MIN(wsr.status_enum) = 5 OR MIN(wsr.status_enum) = 6 OR MIN(wsr.status_enum) = 7 THEN 7
WHEN 7 THEN 5 WHEN MIN(wsr.status_enum) = 8 THEN 6
WHEN 8 THEN 6
ELSE NULL ELSE NULL
END = #{status}::integer END = #{status}::integer
</if> </if>
@@ -164,7 +162,7 @@
AND drf.prescription_no LIKE CONCAT('%', #{requestFormDto.surgeryNo}, '%') AND drf.prescription_no LIKE CONCAT('%', #{requestFormDto.surgeryNo}, '%')
</if> </if>
<if test="requestFormDto.typeCode != null and requestFormDto.typeCode != ''"> <if test="requestFormDto.typeCode != null and requestFormDto.typeCode != ''">
AND drf.type_code = #{requestFormDto.typeCode} AND drf.type_code IN (#{requestFormDto.typeCode}, 'SURGERY')
</if> </if>
<if test="requestFormDto.applyTimeStart != null"> <if test="requestFormDto.applyTimeStart != null">
AND drf.create_time >= #{requestFormDto.applyTimeStart} AND drf.create_time >= #{requestFormDto.applyTimeStart}

View File

@@ -49,6 +49,11 @@ public enum RequestStatus implements HisEnumInterface {
*/ */
ENDED(7, "ended", "不执行"), ENDED(7, "ended", "不执行"),
/**
* 已出报告
*/
COMPLETED_REPORT(8, "completed_report", "已出报告"),
/** /**
* 未知 * 未知
*/ */

View File

@@ -56,6 +56,13 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="申请单号" prop="applyNo" min-width="160" align="center" header-align="center" /> <el-table-column label="申请单号" prop="applyNo" min-width="160" align="center" header-align="center" />
<el-table-column label="单据状态" prop="applyStatus" width="100" align="center" header-align="center">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.applyStatus)" size="small">
{{ getStatusLabel(scope.row.applyStatus, scope.row) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="检验项目" prop="itemName" min-width="170px" align="center" header-align="center"> <el-table-column label="检验项目" prop="itemName" min-width="170px" align="center" header-align="center">
<template #default="scope"> <template #default="scope">
<span>{{ scope.row.itemName }}</span> <span>{{ scope.row.itemName }}</span>
@@ -1445,6 +1452,26 @@ const formatAmount = (amount) => {
return num.toFixed(2) return num.toFixed(2)
} }
// 单据状态标签文字
const getStatusLabel = (applyStatus, row) => {
// applyStatus: 0=待开立, 1=已开立(已签发)
// 结合收费/执行标记推导更丰富的状态
if (applyStatus === 1) {
// 已收费后根据执行标记判断
if (row.needExecute === true) {
return '已执行'
}
return '已开立'
}
return '待开立'
}
// 单据状态标签颜色
const getStatusType = (applyStatus) => {
if (applyStatus === 1) return 'success'
return 'info'
}
// 格式化日期时间为字符串 YYYY-MM-DD HH:mm:ss // 格式化日期时间为字符串 YYYY-MM-DD HH:mm:ss
const formatDateTime = (date) => { const formatDateTime = (date) => {
if (!date) return '' if (!date) return ''

View File

@@ -183,6 +183,26 @@
<el-button @click="detailDialogVisible = false">关闭</el-button> <el-button @click="detailDialogVisible = false">关闭</el-button>
</template> </template>
</el-dialog> </el-dialog>
<!-- 编辑检验申请单弹窗 -->
<el-dialog
v-model="editDialogVisible"
title="编辑检验申请单"
width="1200px"
destroy-on-close
top="5vh"
:close-on-click-modal="false"
>
<LaboratoryTests
ref="editFormRef"
@submitOk="handleEditSubmitOk"
:editData="editRowData"
/>
<template #footer>
<el-button @click="editDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitEditForm">确认</el-button>
</template>
</el-dialog>
</div> </div>
</template> </template>
@@ -192,12 +212,17 @@ import {Refresh, Search} from '@element-plus/icons-vue';
import {patientInfo} from '../../store/patient.js'; import {patientInfo} from '../../store/patient.js';
import {getInspection, deleteRequestForm, withdrawRequestForm} from './api'; import {getInspection, deleteRequestForm, withdrawRequestForm} from './api';
import {getDepartmentList} from '@/api/public.js'; import {getDepartmentList} from '@/api/public.js';
import LaboratoryTests from '../order/applicationForm/laboratoryTests.vue';
import {saveInspection} from '../order/applicationForm/api.js';
const { proxy } = getCurrentInstance(); const { proxy } = getCurrentInstance();
const tableData = ref([]); const tableData = ref([]);
const loading = ref(false); const loading = ref(false);
const detailDialogVisible = ref(false); const detailDialogVisible = ref(false);
const editDialogVisible = ref(false);
const editRowData = ref(null);
const editFormRef = ref(null);
const currentDetail = ref(null); const currentDetail = ref(null);
const descJsonData = ref(null); const descJsonData = ref(null);
const orgOptions = ref([]); const orgOptions = ref([]);
@@ -433,10 +458,32 @@ const handleViewDetail = async (row) => {
/** /**
* 修改检验申请单(待签发状态) * 修改检验申请单(待签发状态)
*/ */
const handleEdit = (row) => { const handleEdit = async (row) => {
// 复用详情查看逻辑,后续可扩展为打开编辑弹窗 // 确保科室数据已加载
handleViewDetail(row); if (!orgOptions.value || orgOptions.value.length === 0) {
proxy.$modal?.msgInfo?.('修改功能待接入,请通过详情弹窗查看后重新开立'); await getLocationInfo();
}
editRowData.value = row;
editDialogVisible.value = true;
};
/**
* 编辑弹窗提交成功回调
*/
const handleEditSubmitOk = async () => {
editDialogVisible.value = false;
editRowData.value = null;
proxy.$modal?.msgSuccess?.('修改成功');
await fetchData();
};
/**
* 编辑弹窗提交按钮
*/
const submitEditForm = () => {
if (editFormRef.value?.submit) {
editFormRef.value.submit();
}
}; };
/** /**
@@ -450,7 +497,7 @@ const handleDelete = async (row) => {
} }
try { try {
const res = await deleteRequestForm({ prescriptionNo: row.prescriptionNo }); const res = await deleteRequestForm({ requestFormId: row.requestFormId });
if (res?.code === 200) { if (res?.code === 200) {
proxy.$modal?.msgSuccess?.('删除成功'); proxy.$modal?.msgSuccess?.('删除成功');
await fetchData(); await fetchData();
@@ -473,7 +520,7 @@ const handleWithdraw = async (row) => {
} }
try { try {
const res = await withdrawRequestForm({ prescriptionNo: row.prescriptionNo }); const res = await withdrawRequestForm({ requestFormId: row.requestFormId });
if (res?.code === 200) { if (res?.code === 200) {
proxy.$modal?.msgSuccess?.('撤回成功'); proxy.$modal?.msgSuccess?.('撤回成功');
await fetchData(); await fetchData();

View File

@@ -155,7 +155,13 @@ const findTreeItem = (list, id) => {
return null; return null;
}; };
const emits = defineEmits(['submitOk']); const emits = defineEmits(['submitOk']);
const props = defineProps({}); const props = defineProps({
editData: {
type: Object,
default: null,
},
});
const isEditMode = computed(() => !!props.editData?.requestFormId);
const state = reactive({}); const state = reactive({});
const applicationListAll = ref([]); const applicationListAll = ref([]);
const loading = ref(false); const loading = ref(false);
@@ -331,6 +337,44 @@ watch(
projectWithDepartment(newValue, 1); projectWithDepartment(newValue, 1);
} }
); );
// 编辑模式下,回显已有数据
watch(
() => props.editData,
(newData) => {
if (!newData || !newData.requestFormId) return;
// 解析 descJson 回填表单
if (newData.descJson) {
try {
const obj = JSON.parse(newData.descJson);
Object.keys(form).forEach((key) => {
if (obj[key] !== undefined) {
form[key] = obj[key];
}
});
} catch (e) {
console.error('解析 descJson 失败:', e);
}
}
// 回填已选项目
if (newData.requestFormDetailList && newData.requestFormDetailList.length > 0) {
// 从全部数据中匹配已选项目
const selectedIds = [];
newData.requestFormDetailList.forEach((detail) => {
const matched = applicationListAll.value.find(
(item) => item.adviceName === detail.adviceName
);
if (matched) {
selectedIds.push(matched.adviceDefinitionId);
}
});
transferValue.value = selectedIds;
}
},
{ immediate: true }
);
const submit = () => { const submit = () => {
if (transferValue.value.length == 0) { if (transferValue.value.length == 0) {
return proxy.$message.error('请选择申请单'); return proxy.$message.error('请选择申请单');
@@ -363,14 +407,14 @@ const submit = () => {
patientId: patientInfo.value.patientId, //患者ID patientId: patientInfo.value.patientId, //患者ID
encounterId: patientInfo.value.encounterId, // 就诊ID encounterId: patientInfo.value.encounterId, // 就诊ID
organizationId: patientInfo.value.inHospitalOrgId, // 医疗机构ID organizationId: patientInfo.value.inHospitalOrgId, // 医疗机构ID
requestFormId: '', // 申请单ID requestFormId: isEditMode.value ? props.editData.requestFormId : '', // 申请单ID(编辑模式传入,新增为空)
name: '检验申请单', name: '检验申请单',
descJson: JSON.stringify(form), descJson: JSON.stringify(form),
categoryEnum: '21', // 21 检验 22 检查 23 输血 24 手术(避开 adviceType 1-6 碰撞) categoryEnum: '21', // 21 检验 22 检查 23 输血 24 手术(避开 adviceType 1-6 碰撞)
}; };
saveInspection(params).then((res) => { saveInspection(params).then((res) => {
if (res.code === 200) { if (res.code === 200) {
proxy.$message.success(res.msg); proxy.$message.success(isEditMode.value ? '修改成功' : res.msg);
transferValue.value = []; transferValue.value = [];
emits('submitOk'); emits('submitOk');
} else { } else {