Merge remote-tracking branch 'origin/develop' into develop

This commit is contained in:
2026-04-07 14:01:10 +08:00
31 changed files with 1682 additions and 206 deletions

91
BUGFIX_ANALYSIS.md Normal file
View File

@@ -0,0 +1,91 @@
# Bug 根因分析与修复方案
## Bug 335 - 门诊医生站开立药品医嘱保存报错
### 问题分析
根据代码分析,`DoctorStationAdviceAppServiceImpl.saveAdvice()` 方法处理药品医嘱保存时可能报错的原因:
1. **patientId/encounterId 为 null** - 删除操作时前端可能未传
2. **accountId 为 null** - 患者账户信息未正确获取
3. **definitionId/definitionDetailId 为 null** - 定价信息缺失
4. **库存校验失败** - 药品库存不足
### 修复方案
✅ 已部分修复(见代码中的 BugFix 注释)
- 已添加 patientId/encounterId 自动补全逻辑
- 已添加 accountId 自动创建逻辑
- 需要进一步验证 definitionId 的处理
---
## Bug 336 - 门诊医生站开立诊疗项目保存报错
### 问题分析
诊疗项目保存与药品类似,但有以下特殊点:
1. **必须选择执行科室** - 代码中有校验 `throw new ServiceException("诊疗项目必须选择执行科室")`
2. **活动绑定设备处理** - 需要处理 `handService()` 中的设备绑定逻辑
3. **库存校验** - 诊疗项目可能关联耗材
### 修复方案
- 确保前端传递 executeDeptId执行科室
- 检查 handService() 方法中的异常处理
- 添加更详细的错误日志
---
## Bug 338 - 门诊划价新增时未校验就诊记录及诊断记录
### 问题分析
**这是患者安全问题!** 未接诊患者也可新增划价项目可能导致:
- 收费错误
- 医疗纠纷
- 数据不一致
当前代码问题:
- `OutpatientPricingAppServiceImpl.getAdviceBaseInfo()` 仅查询医嘱,未校验就诊状态
- 前端划价保存接口未找到(可能在其他地方)
### 修复方案
1. 在划价查询时增加就诊状态校验
2. 在划价保存时增加诊断记录校验
3. 未接诊患者禁止划价
---
## Bug 339 - 药房筛选条件失效
### 问题分析
查询结果中包含非选中药房的数据,可能原因:
- SQL WHERE 条件未正确应用 locationId
- 多表关联时过滤条件丢失
### 修复方案
- 检查 `DoctorStationAdviceAppMapper.getAdviceBaseInfo()` 的 SQL
- 确保 locationId 条件正确应用
---
## 修复优先级
1. **Bug 338** - 患者安全问题,最高优先级
2. **Bug 335/336** - 核心功能阻断,高优先级
3. **Bug 339** - 数据准确性问题,中优先级
---
## 测试用例
### Bug 338 测试
1. 选择未接诊患者,尝试划价 → 应禁止
2. 选择已接诊但无诊断的患者,尝试划价 → 应提示补充诊断
3. 选择正常接诊患者,划价 → 应成功
### Bug 335/336 测试
1. 门诊医生站开立药品医嘱 → 应成功保存
2. 门诊医生站开立诊疗项目 → 应成功保存
3. 签发医嘱 → 应成功
### Bug 339 测试
1. 选择"西药房"筛选 → 结果应仅包含西药房数据
2. 选择"中药房"筛选 → 结果应仅包含中药房数据

84
BUGFIX_PLAN.md Normal file
View File

@@ -0,0 +1,84 @@
# HIS 系统 Bug 修复计划
## 修复负责人
华佗 (AI 团队)
## 修复时间
2026-04-05 开始
---
## Bug 清单与修复优先级
### 🔴 高优先级(核心业务阻断)
#### Bug 335 - 门诊医生站开立药品医嘱保存报错
- **模块**: 医生工作站
- **文件**: `DoctorStationAdviceAppServiceImpl.java`
- **根因分析**: 待分析
- **修复状态**: 🔄 分析中
#### Bug 336 - 门诊医生站开立诊疗项目保存报错
- **模块**: 医生工作站
- **文件**: `DoctorStationAdviceAppServiceImpl.java`
- **根因分析**: 待分析
- **修复状态**: ⏳ 等待 335 修复后验证
#### Bug 338 - 门诊划价新增时未校验就诊记录及诊断记录
- **模块**: 门诊收费
- **问题**: 未接诊患者也可新增划价项目(患者安全问题)
- **修复方案**: 在划价保存前增加就诊状态和诊断记录校验
- **修复状态**: ⏳ 待修复
### 🟡 中优先级(数据准确性/用户体验)
#### Bug 339 - 药房筛选条件失效
- **模块**: 药房药库报表管理
- **问题**: 查询结果中包含非选中药房的数据
- **修复状态**: ⏳ 待分析
#### Bug 333 - 耗材医嘱类型错误
- **模块**: 医生工作站
- **问题**: 类型误转为"中成药"且保存报错
- **修复状态**: ⏳ 待分析
#### Bug 337 - 挂号时间显示异常
- **模块**: 建档挂号管理
- **问题**: 未显示当前实际挂号时间
- **修复状态**: ⏳ 待分析
#### Bug 334 - 检验申请界面布局优化
- **模块**: 门诊医生工作站
- **问题**: 按钮布局需要调整
- **修复状态**: ⏳ 待修复(前端)
### 🟢 低优先级(历史遗留问题)
#### Bug 249/253/280/300 - 3 月份遗留 bug
- **修复状态**: ⏳ 后续处理
---
## 修复流程
1. **分析根因** - 查看代码和日志,定位问题
2. **编写修复** - 修改代码并添加必要校验
3. **本地测试** - 确保修复有效且不引入新问题
4. **提交代码** - commit 并推送到 gitea
5. **验证关闭** - 在禅道更新 Bug 状态
---
## 测试要求
- 修复后必须测试
- 测试不通过继续修
- 确保不影响其他功能
---
## 备注
- 所有修复基于 develop 分支
- 修复完成后统一提交
- 重要修复添加详细注释

61
BUG_FIX_PROGRESS.md Normal file
View File

@@ -0,0 +1,61 @@
# HIS项目 Bug修复与需求开发进度表
## 项目信息
- **项目名称**: 开源HIS改造落地
- **当前分支**: develop
- **代码路径**:
- 前端: openhis-ui-vue3
- 后端: openhis-server-new
- ** Git仓库**: https://gitea.gentronhealth.com/wangyizhe/his
- **禅道地址**: https://zentao.gentronhealth.com
## 当前状态
- ✅ 代码已克隆完成
- ✅ Bug 已重新分配(管理员操作)
- ⏳ 等待修复人员开始工作
- 📋 张飞负责测试验证
## Bug修复任务列表重新分配后
| Bug ID | 严重程度 | 状态 | 模块 | 标题 | 原指派给 | **新指派给** | 进度 |
|--------|----------|------|------|------|----------|--------------|------|
| 339 | 3 | 激活 | 药房药库报表管理 | 药房筛选条件失效 | 王怡哲 | **关羽** | 待处理 |
| 338 | 3 | 激活 | 门诊收费管理 | 未校验就诊记录 | 王怡哲 | **关羽** | 待处理 |
| 337 | 3 | 激活 | 建档挂号管理 | 挂号时间显示异常 | 王怡哲 | **关羽** | 待处理 |
| 336 | 3 | 激活 | 门诊医生工作站 | 开立诊疗项目保存报错 | 王怡哲 | **关羽** | 待处理 |
| 335 | 3 | 激活 | 门诊医生工作站 | 开立药品医嘱保存报错 | 王怡哲 | **关羽** | 待处理 |
| 334 | 3 | 激活 | 门诊医生工作站 | 检验申请界面布局优化 | 王建 | **子龙** | 待处理 |
| 333 | 3 | 激活 | 门诊医生工作站 | 耗材医嘱类型误转 | 陈显精 | **关羽** | 待处理 |
## P0 级别 Bug紧急优先修复
| Bug ID | 标题 | 严重程度 | 负责人 |
|--------|------|----------|--------|
| 335 | 开立药品医嘱保存报错 | 严重 | 关羽 |
| 336 | 开立诊疗项目保存报错 | 严重 | 关羽 |
| 338 | 未校验就诊记录 | 严重 | 关羽 |
## 需求开发任务列表10个全部未关闭
待进一步确认分配情况...
## 工作流程
1. **认领任务** - 在禅道将 Bug 分配给自己
2. **修改代码** - 从 develop 分支创建新分支:`bug/bug-id`
3. **本地测试** - 确保本地 JDK 17 环境编译通过
4. **提交PR** - 提交 Pull Request 到 develop 分支
5. **测试验证** - 张飞进行测试
6. **合并分支** - 测试通过后合并到 develop
## 注意事项
- 所有代码修改必须先创建新分支
- 分支命名:`bug/bug-id``feature/feedback-id`
- 提交信息必须包含禅道Bug/需求ID
- 修改前请先阅读 `AGENTS.md` 了解项目规范
- **JDK 17 配置** - 确保本地开发环境使用 JDK 17
## 今日会议纪要
- 2026-04-05 15:09: 管理员重新分配 Bug 给群内武将
- 2026-04-05 14:58: 确认将王怡哲的 Bug 分配给关羽、张飞、陈琳
- 2026-04-05 13:47: 统一调度分配人员任务
- 2026-04-05 12:45: 初始任务分配完成

2
GIT_TEST_GUANYU.md Normal file
View File

@@ -0,0 +1,2 @@
# 关羽 Git 配置测试
测试时间: Mon Apr 6 07:03:56 AM CST 2026

70
md/BUG_ANALYSIS.md Normal file
View File

@@ -0,0 +1,70 @@
# HIS项目 Bug 分析与修复日志
## 2026-04-05 23:55 - 子龙开始工作
### Bug 334 分析:门诊医生站-检验申请界面按钮布局优化
**文件位置**
- `/openhis-ui-vue3/src/views/doctorstation/components/inspection/inspectionApplication.vue`
**当前布局问题**
1. 顶部操作按钮区高度 60px可能有优化空间
2. 表单区域 padding 较大
3. 需要优化垂直空间利用率
**修复方案**
- 减少不必要的 padding 和 margin
- 优化表单字段布局
- 调整按钮区域高度
---
### Bug 335 分析:门诊医生站开立药品医嘱点击【保存】时报错
**文件位置**
- `/openhis-server-new/openhis-application/src/main/java/com/openhis/web/doctorstation/appservice/impl/DoctorStationAdviceAppServiceImpl.java`
**问题定位**
- 方法:`saveAdvice()` -> `handMedication()`
- 可能原因:
1. encounterId 或 patientId 为 null
2. 库存校验失败
3. 账户ID缺失
**代码已修复**
- 行 488-588已添加 encounterId 和 patientId 校验
- 行 497-588自动补全逻辑
---
### Bug 336 分析:门诊医生站开立诊疗项目后点击【保存】报错
**文件位置**
- 同上文件
**问题定位**
- 方法:`saveAdvice()` -> `handService()`
- 可能原因:
1. effectiveOrgId执行科室为 null
2. accountId 为 null
**代码已修复**
- 行 1290-1390已添加 accountId 自动补全
- 行 1338-1343诊疗项目执行科室非空校验
---
## 工作分工
| Bug ID | 负责人 | 状态 |
|--------|--------|------|
| 334 | 子龙 | 分析中 |
| 335 | 关羽 | 待修复 |
| 336 | 关羽 | 待修复 |
| 338 | 关羽 | 待修复 |
## 下一步行动
1. 子龙修复 Bug 334检验申请界面布局优化
2. 关羽修复 Bug 335、336、338
3. 张飞测试验证

View File

@@ -16,6 +16,7 @@ import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -123,6 +124,7 @@ public class TicketAppServiceImpl implements ITicketAppService {
if (query == null) {
query = new com.openhis.appointmentmanage.dto.TicketQueryDTO();
}
normalizeQueryStatus(query);
// 2. 构造 MyBatis 的分页对象 (传入前端给的当前页和每页条数)
com.baomidou.mybatisplus.extension.plugins.pagination.Page<com.openhis.appointmentmanage.domain.TicketSlotDTO> pageParam = new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(
@@ -140,42 +142,67 @@ public class TicketAppServiceImpl implements ITicketAppService {
// 基础字段映射
dto.setSlot_id(raw.getSlotId());
dto.setSeqNo(raw.getSeqNo());
dto.setBusNo(String.valueOf(raw.getSlotId()));
dto.setDoctor(raw.getDoctor());
dto.setDepartment(raw.getDepartmentName()); // 注意以前这里传成了ID导致前端出Bug现在修复成了真正的科室名
dto.setFee(raw.getFee());
dto.setPatientName(raw.getPatientName());
dto.setPatientId(raw.getPatientId() != null ? String.valueOf(raw.getPatientId()) : null);
dto.setPatientId(raw.getMedicalCard());
dto.setPhone(raw.getPhone());
dto.setIdCard(raw.getIdCard());
dto.setDoctorId(raw.getDoctorId());
dto.setDepartmentId(raw.getDepartmentId());
dto.setRealPatientId(raw.getPatientId());
// 性别处理:直接读取优先级最高的订单性别字段 (SQL 已处理优先级)
if (raw.getPatientGender() != null) {
String pg = raw.getPatientGender().trim();
dto.setGender("1".equals(pg) ? "" : ("2".equals(pg) ? "" : "未知"));
} else {
dto.setGender("未知");
}
// 号源类型处理 (底层是1前端要的是expert)
if (raw.getRegType() != null && raw.getRegType() == 1) {
dto.setTicketType("expert");
} else {
dto.setTicketType("general");
}
// 拼接就诊时间
if (raw.getScheduleDate() != null && raw.getExpectTime() != null) {
dto.setDateTime(raw.getScheduleDate().toString() + " " + raw.getExpectTime().toString());
try {
dto.setAppointmentDate(
new java.text.SimpleDateFormat("yyyy-MM-dd").parse(raw.getScheduleDate().toString()));
String timeStr = raw.getAppointmentTime() != null ? raw.getAppointmentTime() : (raw.getScheduleDate().toString() + " " + raw.getExpectTime().toString());
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat(timeStr.length() > 10 ? "yyyy-MM-dd HH:mm" : "yyyy-MM-dd");
java.util.Date date = sdf.parse(timeStr);
dto.setAppointmentDate(date);
dto.setAppointmentTime(date);
} catch (Exception e) {
dto.setAppointmentDate(new java.util.Date());
}
}
// 精准状态翻译把底层的1和2翻译回前端能懂的中文
if (Boolean.TRUE.equals(raw.getIsStopped())) {
dto.setStatus("已停诊");
} else {
Integer slotStatus = raw.getSlotStatus();
if (slotStatus != null) {
if (SlotStatus.BOOKED.equals(slotStatus)) {
dto.setStatus(AppointmentOrderStatus.CHECKED_IN.equals(raw.getOrderStatus()) ? "已取号" : "已预约");
} else if (SlotStatus.STOPPED.equals(slotStatus)) {
if (SlotStatus.CHECKED_IN.equals(slotStatus)) {
dto.setStatus("已取号");
} else if (SlotStatus.BOOKED.equals(slotStatus)) {
if (AppointmentOrderStatus.CHECKED_IN.equals(raw.getOrderStatus())) {
dto.setStatus("已取号");
} else if (AppointmentOrderStatus.RETURNED.equals(raw.getOrderStatus())) {
dto.setStatus("已退号");
} else {
dto.setStatus("已预约");
}
} else if (SlotStatus.RETURNED.equals(slotStatus)) {
dto.setStatus("已退号");
} else if (SlotStatus.CANCELLED.equals(slotStatus)) {
dto.setStatus("已停诊");
} else if (SlotStatus.LOCKED.equals(slotStatus)) {
dto.setStatus("已锁定");
} else {
dto.setStatus("未预约");
}
@@ -198,6 +225,62 @@ public class TicketAppServiceImpl implements ITicketAppService {
return R.ok(result);
}
/**
* 统一状态入参,避免前端状态值大小写/中文/数字差异导致 SQL 条件失效后回全量数据
*/
private void normalizeQueryStatus(com.openhis.appointmentmanage.dto.TicketQueryDTO query) {
String rawStatus = query.getStatus();
if (rawStatus == null) {
return;
}
String normalized = rawStatus.trim();
if (normalized.isEmpty()) {
query.setStatus(null);
return;
}
String lower = normalized.toLowerCase(Locale.ROOT);
switch (lower) {
case "all":
case "全部":
query.setStatus("all");
break;
case "unbooked":
case "0":
case "未预约":
query.setStatus("unbooked");
break;
case "booked":
case "1":
case "已预约":
query.setStatus("booked");
break;
case "checked":
case "checkin":
case "checkedin":
case "2":
case "已取号":
query.setStatus("checked");
break;
case "cancelled":
case "canceled":
case "3":
case "已停诊":
case "已取消":
query.setStatus("cancelled");
break;
case "returned":
case "4":
case "5":
case "已退号":
query.setStatus("returned");
break;
default:
// 设置为 impossible 值,配合 mapper 的 otherwise 分支直接返回空
query.setStatus("__invalid__");
break;
}
}
@Override
public R<?> listDoctorAvailability(com.openhis.appointmentmanage.dto.TicketQueryDTO query) {
if (query == null) {
@@ -237,12 +320,13 @@ public class TicketAppServiceImpl implements ITicketAppService {
// --- 基础字段处理 ---
// 注意:这里已经变成了极其舒服的 .getSlotId() 方法调用,告别魔鬼字符串!
dto.setSlot_id(raw.getSlotId());
dto.setSeqNo(raw.getSeqNo());
dto.setBusNo(String.valueOf(raw.getSlotId())); // 暂时借用真实槽位ID做唯一流水号
dto.setDoctor(raw.getDoctor());
dto.setDepartment(raw.getDepartmentName());
dto.setFee(raw.getFee());
dto.setPatientName(raw.getPatientName());
dto.setPatientId(raw.getPatientId() != null ? String.valueOf(raw.getPatientId()) : null);
dto.setPatientId(raw.getMedicalCard());
dto.setPhone(raw.getPhone());
// --- 号源类型处理 (普通/专家) ---
@@ -258,9 +342,13 @@ public class TicketAppServiceImpl implements ITicketAppService {
if (raw.getScheduleDate() != null && raw.getExpectTime() != null) {
dto.setDateTime(raw.getScheduleDate().toString() + " " + raw.getExpectTime().toString());
try {
dto.setAppointmentDate(
new java.text.SimpleDateFormat("yyyy-MM-dd").parse(raw.getScheduleDate().toString()));
String timeStr = raw.getAppointmentTime() != null ? raw.getAppointmentTime() : (raw.getScheduleDate().toString() + " " + raw.getExpectTime().toString());
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat(timeStr.length() > 10 ? "yyyy-MM-dd HH:mm" : "yyyy-MM-dd");
java.util.Date date = sdf.parse(timeStr);
dto.setAppointmentDate(date);
dto.setAppointmentTime(date);
} catch (Exception e) {
log.error("时间解析失败", e);
dto.setAppointmentDate(new java.util.Date());
}
}
@@ -273,10 +361,22 @@ public class TicketAppServiceImpl implements ITicketAppService {
// 第二关:看独立的细分槽位状态 (0: 可用, 1: 已预约, 2: 已取消...)
Integer slotStatus = raw.getSlotStatus();
if (slotStatus != null) {
if (SlotStatus.BOOKED.equals(slotStatus)) {
dto.setStatus(AppointmentOrderStatus.CHECKED_IN.equals(raw.getOrderStatus()) ? "已取号" : "已预约");
} else if (SlotStatus.STOPPED.equals(slotStatus)) {
dto.setStatus("已停诊"); // 视业务可改回已取消
if (SlotStatus.CHECKED_IN.equals(slotStatus)) {
dto.setStatus("已取号");
} else if (SlotStatus.BOOKED.equals(slotStatus)) {
if (AppointmentOrderStatus.CHECKED_IN.equals(raw.getOrderStatus())) {
dto.setStatus("已取号");
} else if (AppointmentOrderStatus.RETURNED.equals(raw.getOrderStatus())) {
dto.setStatus("已退号");
} else {
dto.setStatus("已预约");
}
} else if (SlotStatus.RETURNED.equals(slotStatus)) {
dto.setStatus("已退号");
} else if (SlotStatus.CANCELLED.equals(slotStatus)) {
dto.setStatus("已停诊");
} else if (SlotStatus.LOCKED.equals(slotStatus)) {
dto.setStatus("已锁定");
} else {
dto.setStatus("未预约");
}
@@ -355,15 +455,12 @@ public class TicketAppServiceImpl implements ITicketAppService {
if (patient != null) {
Integer genderEnum = patient.getGenderEnum();
if (genderEnum != null) {
switch (genderEnum) {
case 1:
dto.setGender("");
break;
case 2:
dto.setGender("");
break;
default:
dto.setGender("未知");
if (Integer.valueOf(1).equals(genderEnum)) {
dto.setGender("");
} else if (Integer.valueOf(2).equals(genderEnum)) {
dto.setGender("");
} else {
dto.setGender("未知");
}
}
}

View File

@@ -23,6 +23,11 @@ public class TicketDto {
@JsonSerialize(using = ToStringSerializer.class)
private Long slot_id;
/**
* 号源序号(对应 adm_schedule_slot.seq_no
*/
private Integer seqNo;
/**
* 号源编码
*/
@@ -99,4 +104,15 @@ public class TicketDto {
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long doctorId;
/**
* 真实患者ID数据库主键区别于 patientId 存的就诊卡号)
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long realPatientId;
/**
* 身份证号
*/
private String idCard;
}

View File

@@ -73,11 +73,10 @@ public class OutpatientPricingAppServiceImpl implements IOutpatientPricingAppSer
} else {
adviceTypes = List.of(1, 2, 3);
}
// 门诊划价:不要强制 pricingFlag=1 参与过滤wor_activity_definition.pricing_flag 可能为 0
// 否则会导致诊疗项目(adviceType=3)查询结果为空 records=[]
String categoryCode = adviceBaseDto != null ? adviceBaseDto.getCategoryCode() : null;
// 门诊划价:仅返回划价标记为“是”的项目
return iDoctorStationAdviceAppService.getAdviceBaseInfo(adviceBaseDto, searchKey, locationId, null,
organizationId, pageNo, pageSize, null, adviceTypes, null, categoryCode);
organizationId, pageNo, pageSize, Whether.YES.getValue(), adviceTypes, null, categoryCode);
}
}

View File

@@ -22,6 +22,10 @@ import com.openhis.common.enums.ybenums.YbPayment;
import com.openhis.common.utils.EnumUtils;
import com.openhis.common.utils.HisPageUtils;
import com.openhis.common.utils.HisQueryUtils;
import com.openhis.appointmentmanage.mapper.SchedulePoolMapper;
import com.openhis.appointmentmanage.mapper.ScheduleSlotMapper;
import com.openhis.clinical.domain.Order;
import com.openhis.clinical.service.IOrderService;
import com.openhis.financial.domain.PaymentReconciliation;
import com.openhis.financial.domain.RefundLog;
import com.openhis.financial.service.IRefundLogService;
@@ -48,6 +52,7 @@ import javax.servlet.http.HttpServletRequest;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
@@ -97,6 +102,15 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
@Resource
IRefundLogService iRefundLogService;
@Resource
IOrderService orderService;
@Resource
ScheduleSlotMapper scheduleSlotMapper;
@Resource
SchedulePoolMapper schedulePoolMapper;
/**
* 门诊挂号 - 查询患者信息
*
@@ -291,6 +305,11 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
}
}
// 如果本次门诊挂号来自预约签到,同步把预约订单与号源槽位状态改为已退号
if (result != null && result.getCode() == 200) {
syncAppointmentReturnStatus(byId, cancelRegPaymentDto.getReason());
}
// 记录退号日志
recordRefundLog(cancelRegPaymentDto, byId, result, paymentRecon);
@@ -399,6 +418,74 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
return R.ok("已取消挂号");
}
/**
* 同步预约号源状态为已退号。
* 说明:
* 1) 门诊退号主流程不依赖该步骤成功与否,因此此方法内部异常仅记录日志,不向上抛出。
* 2) 通过患者、科室、日期以及状态筛选最近一条预约订单,尽量避免误匹配。
*/
private void syncAppointmentReturnStatus(Encounter encounter, String reason) {
if (encounter == null || encounter.getPatientId() == null) {
return;
}
try {
LambdaQueryWrapper<Order> queryWrapper = new LambdaQueryWrapper<Order>()
.eq(Order::getPatientId, encounter.getPatientId())
.in(Order::getStatus, CommonConstants.AppointmentOrderStatus.BOOKED,
CommonConstants.AppointmentOrderStatus.CHECKED_IN)
.orderByDesc(Order::getUpdateTime)
.orderByDesc(Order::getCreateTime)
.last("LIMIT 1");
if (encounter.getOrganizationId() != null) {
queryWrapper.eq(Order::getDepartmentId, encounter.getOrganizationId());
}
if (encounter.getTenantId() != null) {
queryWrapper.eq(Order::getTenantId, encounter.getTenantId());
}
if (encounter.getCreateTime() != null) {
LocalDate encounterDate = encounter.getCreateTime().toInstant()
.atZone(ZoneId.systemDefault()).toLocalDate();
Date startOfDay = Date.from(encounterDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
Date nextDayStart = Date.from(encounterDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
queryWrapper.ge(Order::getAppointmentDate, startOfDay)
.lt(Order::getAppointmentDate, nextDayStart);
}
Order appointmentOrder = orderService.getOne(queryWrapper, false);
if (appointmentOrder == null) {
return;
}
Date now = new Date();
if (!CommonConstants.AppointmentOrderStatus.RETURNED.equals(appointmentOrder.getStatus())) {
Order updateOrder = new Order();
updateOrder.setId(appointmentOrder.getId());
updateOrder.setStatus(CommonConstants.AppointmentOrderStatus.RETURNED);
updateOrder.setCancelTime(now);
updateOrder.setCancelReason(
StringUtils.isNotEmpty(reason) ? reason : "门诊退号");
updateOrder.setUpdateTime(now);
orderService.updateById(updateOrder);
}
Long slotId = appointmentOrder.getSlotId();
if (slotId == null) {
return;
}
int slotRows = scheduleSlotMapper.updateSlotStatus(slotId, CommonConstants.SlotStatus.RETURNED);
if (slotRows > 0) {
Long poolId = scheduleSlotMapper.selectPoolIdBySlotId(slotId);
if (poolId != null) {
schedulePoolMapper.refreshPoolStats(poolId);
}
}
} catch (Exception e) {
log.warn("同步预约号源已退号状态失败, encounterId={}", encounter.getId(), e);
}
}
/**
* 补打挂号
* 补打挂号不需要修改数据库,只需要返回成功即可,前端已有所有需要的数据用于打印

View File

@@ -205,6 +205,11 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
// 构建查询条件
QueryWrapper<AdviceBaseDto> queryWrapper = HisQueryUtils.buildQueryWrapper(adviceBaseDto, searchKey,
new HashSet<>(Arrays.asList("advice_name", "py_str", "wb_str")), null);
// 🔧 BugFix#339: 药房筛选条件失效 - 添加 locationId 过滤条件
if (locationId != null) {
queryWrapper.eq("location_id", locationId);
log.info("BugFix#339: 添加药房筛选条件 locationId={}", locationId);
}
IPage<AdviceBaseDto> adviceBaseInfo = doctorStationAdviceAppMapper.getAdviceBaseInfo(
new Page<>(pageNo, pageSize), PublicationStatus.ACTIVE.getValue(), organizationId,
CommonConstants.TableName.MED_MEDICATION_DEFINITION, CommonConstants.TableName.ADM_DEVICE_DEFINITION,
@@ -561,6 +566,24 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
return R.fail(null, "无法获取患者信息,请重新选择患者");
}
}
// 🔧 BugFix#338: 门诊划价新增时校验就诊状态和诊断记录(患者安全)
// 仅对新增/修改操作进行校验,删除操作不需要
if (!DbOpType.DELETE.getCode().equals(adviceSaveDto.getDbOpType())) {
// 1. 校验就诊状态:必须是已接诊状态
Encounter encounterCheck = iEncounterService.getById(adviceSaveDto.getEncounterId());
if (encounterCheck != null) {
// 就诊状态1001=挂号1002=已接诊1003=已收费1004=已完成
if (encounterCheck.getStatusEnum() != null &&
encounterCheck.getStatusEnum() != 1002 &&
encounterCheck.getStatusEnum() != 1003 &&
encounterCheck.getStatusEnum() != 1004) {
log.error("BugFix#338: 患者未接诊,禁止划价/保存医嘱encounterId={}, status={}",
adviceSaveDto.getEncounterId(), encounterCheck.getStatusEnum());
return R.fail(null, "患者尚未接诊,无法保存医嘱。请先完成接诊操作!");
}
}
}
}
// 药品前端adviceType=1
@@ -770,6 +793,18 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
}
}
// 🔧 Bug Fix: 确保practitionerId不为null
if (adviceSaveDto.getPractitionerId() == null) {
adviceSaveDto.setPractitionerId(SecurityUtils.getLoginUser().getPractitionerId());
log.info("handMedication - 自动补全practitionerId: practitionerId={}", adviceSaveDto.getPractitionerId());
}
// 🔧 Bug Fix: 确保founderOrgId不为null
if (adviceSaveDto.getFounderOrgId() == null) {
adviceSaveDto.setFounderOrgId(SecurityUtils.getLoginUser().getOrgId());
log.info("handMedication - 自动补全founderOrgId: founderOrgId={}", adviceSaveDto.getFounderOrgId());
}
boolean firstTimeSave = false;// 第一次保存
medicationRequest = new MedicationRequest();
medicationRequest.setId(adviceSaveDto.getRequestId()); // 主键id
@@ -1137,6 +1172,18 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
}
}
// 🔧 Bug Fix: 确保practitionerId不为null
if (adviceSaveDto.getPractitionerId() == null) {
adviceSaveDto.setPractitionerId(SecurityUtils.getLoginUser().getPractitionerId());
log.info("自动补全practitionerId: practitionerId={}", adviceSaveDto.getPractitionerId());
}
// 🔧 Bug Fix: 确保founderOrgId不为null
if (adviceSaveDto.getFounderOrgId() == null) {
adviceSaveDto.setFounderOrgId(SecurityUtils.getLoginUser().getOrgId());
log.info("自动补全founderOrgId: founderOrgId={}", adviceSaveDto.getFounderOrgId());
}
deviceRequest = new DeviceRequest();
deviceRequest.setId(adviceSaveDto.getRequestId()); // 主键id
deviceRequest.setStatusEnum(is_save ? RequestStatus.DRAFT.getValue() : RequestStatus.ACTIVE.getValue()); // 请求状态
@@ -1201,6 +1248,47 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
chargeItem.setServiceId(deviceRequest.getId()); // 医疗服务ID
chargeItem.setProductTable(adviceSaveDto.getAdviceTableName());// 产品所在表
chargeItem.setProductId(adviceSaveDto.getAdviceDefinitionId());// 收费项id
// 🔧 Bug Fix: 如果 definitionId 或 definitionDetailId 为 null从定价信息中获取
if (chargeItem.getDefinitionId() == null || chargeItem.getDefDetailId() == null) {
log.warn("耗材的 definitionId 或 definitionDetailId 为 null尝试从定价信息中获取: deviceDefId={}",
adviceSaveDto.getAdviceDefinitionId());
// 查询耗材定价信息
IPage<AdviceBaseDto> devicePage = doctorStationAdviceAppMapper.getAdviceBaseInfo(
new Page<>(1, 1),
PublicationStatus.ACTIVE.getValue(),
orgId,
CommonConstants.TableName.ADM_DEVICE_DEFINITION,
null,
null,
null,
Arrays.asList(adviceSaveDto.getAdviceDefinitionId()),
null,
null,
null,
null);
if (devicePage != null && !devicePage.getRecords().isEmpty()) {
AdviceBaseDto deviceBaseInfo = devicePage.getRecords().get(0);
if (deviceBaseInfo.getPriceList() != null && !deviceBaseInfo.getPriceList().isEmpty()) {
AdvicePriceDto devicePrice = deviceBaseInfo.getPriceList().get(0);
if (chargeItem.getDefinitionId() == null) {
chargeItem.setDefinitionId(devicePrice.getDefinitionId());
log.info("从定价信息中获取 definitionId: {}", devicePrice.getDefinitionId());
}
if (chargeItem.getDefDetailId() == null) {
chargeItem.setDefDetailId(devicePrice.getDefinitionDetailId());
log.info("从定价信息中获取 definitionDetailId: {}", devicePrice.getDefinitionDetailId());
}
}
}
}
// 🔧 Bug Fix: 确保定义ID不为null
if (chargeItem.getDefinitionId() == null) {
log.error("无法获取耗材的 definitionId: deviceDefId={}", adviceSaveDto.getAdviceDefinitionId());
throw new ServiceException("无法获取耗材的定价信息,请联系管理员");
}
// 🔧 Bug Fix: 如果accountId为null从就诊中获取账户ID如果没有则自动创建
Long accountId = adviceSaveDto.getAccountId();
if (accountId == null) {
@@ -1323,6 +1411,18 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
}
}
// 🔧 Bug Fix: 确保practitionerId不为null
if (adviceSaveDto.getPractitionerId() == null) {
adviceSaveDto.setPractitionerId(SecurityUtils.getLoginUser().getPractitionerId());
log.info("handService - 自动补全practitionerId: practitionerId={}", adviceSaveDto.getPractitionerId());
}
// 🔧 Bug Fix: 确保founderOrgId不为null
if (adviceSaveDto.getFounderOrgId() == null) {
adviceSaveDto.setFounderOrgId(SecurityUtils.getLoginUser().getOrgId());
log.info("handService - 自动补全founderOrgId: founderOrgId={}", adviceSaveDto.getFounderOrgId());
}
// 🔧 Bug Fix #238: 诊疗项目执行科室非空校验
if (adviceSaveDto.getAdviceType() != null && adviceSaveDto.getAdviceType() == 3) {
Long effectiveOrgId = adviceSaveDto.getEffectiveOrgId();

View File

@@ -115,6 +115,15 @@ public class AdviceUtils {
matched = true;
// 检查库存是否充足
BigDecimal minUnitQuantity = saveDto.getMinUnitQuantity();
// 🔧 Bug Fix: 对于耗材类型如果没有设置minUnitQuantity则使用quantity作为默认值
if (minUnitQuantity == null) {
if (CommonConstants.TableName.ADM_DEVICE_DEFINITION.equals(inventoryDto.getItemTable())) {
// 耗材只有一个单位minUnitQuantity等于quantity
minUnitQuantity = saveDto.getQuantity();
} else {
return saveDto.getAdviceName() + "的小单位数量不能为空";
}
}
BigDecimal chineseHerbsDoseQuantity = saveDto.getChineseHerbsDoseQuantity(); // 中药付数
// 中草药医嘱的情况
if (chineseHerbsDoseQuantity != null && chineseHerbsDoseQuantity.compareTo(BigDecimal.ZERO) > 0) {

View File

@@ -59,7 +59,7 @@
T9.gender_enum AS genderEnum,
T9.id_card AS idCard,
T9.status_enum AS statusEnum,
T9.register_time AS registerTime,
T9.register_time AS register_time,
T9.total_price AS totalPrice,
T9.account_name AS accountName,
T9.enterer_name AS entererName,

View File

@@ -769,15 +769,21 @@ public class CommonConstants {
}
/**
* 号源槽位状态 (adm_schedule_slot.slot_status)
* 号源槽位状态 (adm_schedule_slot.status)
*/
public interface SlotStatus {
/** 可用 / 待预约 */
Integer AVAILABLE = 0;
/** 已预约 */
Integer BOOKED = 1;
/** 已停诊 / 已失效 */
Integer STOPPED = 2;
/** 已取消 / 已停诊 */
Integer CANCELLED = 2;
/** 已锁定 */
Integer LOCKED = 3;
/** 已签到 / 已取号 */
Integer CHECKED_IN = 4;
/** 已退号 */
Integer RETURNED = 5;
}
/**
@@ -790,6 +796,8 @@ public class CommonConstants {
Integer CHECKED_IN = 2;
/** 已取消 */
Integer CANCELLED = 3;
/** 已退号 */
Integer RETURNED = 4;
}
}

View File

@@ -9,6 +9,8 @@ import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.time.LocalTime;
import java.util.Date;
/**
* 号源池明细Entity
*
@@ -29,7 +31,7 @@ public class ScheduleSlot extends HisBaseEntity {
/** 序号 */
private Integer seqNo;
/** 序号状态: 0-可用,1-已预约,2-已取消,3-已过期等 */
/** 序号状态: 0-可用,1-已预约,2-已取消/已停诊,3-已锁定,4-已签到,5-已退号 */
private Integer status;
/** 预约订单ID */
@@ -37,4 +39,7 @@ public class ScheduleSlot extends HisBaseEntity {
/** 预计叫号时间 */
private LocalTime expectTime;
/** 签到时间 */
private Date checkInTime;
}

View File

@@ -11,6 +11,7 @@ import java.time.LocalTime;
public class TicketSlotDTO {
// 基础信息
private Long slotId;
private Integer seqNo;
private Long scheduleId;
private String doctor;
private Long doctorId;
@@ -22,6 +23,13 @@ public class TicketSlotDTO {
private Long patientId;
private String phone;
private Integer orderStatus;
private Long orderId;
private String orderNo;
private String patientGender;
private Integer genderEnum;
private String idCard;
private String encounterId;
private String appointmentTime;
// 底层逻辑判断专属字段
private Integer slotStatus;

View File

@@ -37,4 +37,21 @@ public interface SchedulePoolMapper extends BaseMapper<SchedulePool> {
AND p.delete_flag = '0'
""")
int refreshPoolStats(@Param("poolId") Long poolId);
/**
* 签到时更新号源池统计:锁定数-1已预约数+1
*
* @param poolId 号源池ID
* @return 结果
*/
@Update("""
UPDATE adm_schedule_pool
SET locked_num = locked_num - 1,
booked_num = booked_num + 1,
update_time = NOW()
WHERE id = #{poolId}
AND locked_num > 0
AND delete_flag = '0'
""")
int updatePoolStatsOnCheckIn(@Param("poolId") Integer poolId);
}

View File

@@ -6,6 +6,7 @@ import com.openhis.appointmentmanage.domain.ScheduleSlot;
import com.openhis.appointmentmanage.domain.TicketSlotDTO;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.openhis.appointmentmanage.dto.TicketQueryDTO;
import java.util.Date;
import java.util.List;
import org.springframework.stereotype.Repository;
import org.apache.ibatis.annotations.Param;
@@ -30,6 +31,16 @@ public interface ScheduleSlotMapper extends BaseMapper<ScheduleSlot> {
*/
int updateSlotStatus(@Param("slotId") Long slotId, @Param("status") Integer status);
/**
* 更新槽位状态并记录签到时间
*
* @param slotId 槽位ID
* @param status 状态
* @param checkInTime 签到时间
* @return 结果
*/
int updateSlotStatusAndCheckInTime(@Param("slotId") Long slotId, @Param("status") Integer status, @Param("checkInTime") Date checkInTime);
/**
* 根据槽位ID查询所属号源池ID。
*/

View File

@@ -20,7 +20,7 @@ import lombok.experimental.Accessors;
@EqualsAndHashCode(callSuper = false)
public class Order extends HisBaseEntity {
@TableId(type = IdType.ASSIGN_ID)
@TableId(type = IdType.AUTO)
@JsonSerialize(using = ToStringSerializer.class)
private Long id;

View File

@@ -32,4 +32,14 @@ public interface OrderMapper extends BaseMapper<Order> {
int updateOrderStatusById(Long id, Integer status);
int updateOrderCancelInfoById(Long id, Date cancelTime, String cancelReason);
/**
* 更新订单支付状态
*
* @param orderId 订单ID
* @param payStatus 支付状态0-未支付1-已支付
* @param payTime 支付时间
* @return 结果
*/
int updatePayStatus(@Param("orderId") Long orderId, @Param("payStatus") Integer payStatus, @Param("payTime") Date payTime);
}

View File

@@ -88,6 +88,7 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
}
Order order = new Order();
order.setId(null); // 显式置空,确保触发数据库自增,避免 MP 预分配雪花 ID 的干扰
String orderNo = assignSeqUtil.getSeq(AssignSeqEnum.ORDER_NUM.getPrefix(), 18);
order.setOrderNo(orderNo);

View File

@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.openhis.appointmentmanage.domain.AppointmentConfig;
import com.openhis.appointmentmanage.service.IAppointmentConfigService;
import com.openhis.appointmentmanage.domain.TicketSlotDTO;
import com.openhis.appointmentmanage.domain.ScheduleSlot;
import com.openhis.appointmentmanage.mapper.SchedulePoolMapper;
import com.openhis.appointmentmanage.mapper.ScheduleSlotMapper;
import com.openhis.clinical.domain.Order;
@@ -52,6 +53,9 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
@Resource
private SchedulePoolMapper schedulePoolMapper;
@Resource
private com.openhis.clinical.mapper.OrderMapper orderMapper;
@Resource
private IAppointmentConfigService appointmentConfigService;
@@ -146,7 +150,25 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
logger.debug("开始执行纯净打单路线slotId: {}, patientName: {}", slotId, dto.getPatientName());
// 1. 直查物理大底座!
// 1. 检查患者取消预约次数限制(应在预约挂号时限制,而非取消预约时)
Integer tenantId = dto.getTenant_id();
Long patientId = dto.getPatientId();
if (tenantId != null && patientId != null) {
AppointmentConfig config = appointmentConfigService.getConfigByTenantId(tenantId);
if (config != null && config.getCancelAppointmentCount() != null
&& config.getCancelAppointmentCount() > 0) {
// 计算当前周期的起始时间
LocalDateTime startTime = calculatePeriodStartTime(config.getCancelAppointmentType());
// 统计已取消次数
long cancelledCount = orderService.countPatientCancellations(patientId, tenantId, startTime);
if (cancelledCount >= config.getCancelAppointmentCount()) {
String periodName = getPeriodName(config.getCancelAppointmentType());
throw new RuntimeException("由于您在" + periodName + "内累计取消预约已达" + cancelledCount + "次,触发系统限制,暂时无法在线预约,请联系分诊台或咨询客服。");
}
}
}
// 2. 直查物理大底座!
TicketSlotDTO slot = scheduleSlotMapper.selectTicketSlotById(slotId);
if (slot == null) {
@@ -242,26 +264,11 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
throw new RuntimeException("当前号源没有可取消的预约订单");
}
// 核心逻辑:获取订单信息并检查机构取消限制
// 获取订单信息
Order latestOrder = orders.get(0);
Integer tenantId = latestOrder.getTenantId();
Long patientId = latestOrder.getPatientId();
if (tenantId != null && patientId != null) {
AppointmentConfig config = appointmentConfigService.getConfigByTenantId(tenantId);
if (config != null && config.getCancelAppointmentCount() != null
&& config.getCancelAppointmentCount() > 0) {
// 计算当前周期的起始时间
LocalDateTime startTime = calculatePeriodStartTime(config.getCancelAppointmentType());
// 统计已取消次数
long cancelledCount = orderService.countPatientCancellations(patientId, tenantId, startTime);
if (cancelledCount >= config.getCancelAppointmentCount()) {
String periodName = getPeriodName(config.getCancelAppointmentType());
throw new RuntimeException("您在" + periodName + "内已达到该机构取消预约次数上限(" + config.getCancelAppointmentCount() + "次),禁止取消");
}
}
}
// 直接执行取消,不再检查取消限制
// 根据需求,取消限制应在预约挂号时检查,而非取消预约时
for (Order order : orders) {
orderService.cancelAppointmentOrder(order.getId(), "患者取消预约");
}
@@ -274,7 +281,7 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
}
/**
* 取号
* 取号(签到)
*
* @param slotId 槽位ID
* @return 结果
@@ -287,7 +294,24 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
throw new RuntimeException("当前号源没有可取号的预约订单");
}
Order latestOrder = orders.get(0);
return orderService.updateOrderStatusById(latestOrder.getId(), AppointmentOrderStatus.CHECKED_IN);
// 1. 更新订单状态为已取号,并更新支付状态和支付时间
orderService.updateOrderStatusById(latestOrder.getId(), AppointmentOrderStatus.CHECKED_IN);
// 更新支付状态为已支付,记录支付时间
orderMapper.updatePayStatus(latestOrder.getId(), 1, new Date());
// 2. 查询号源槽位信息
ScheduleSlot slot = scheduleSlotMapper.selectById(slotId);
// 3. 更新号源槽位状态为已签到,记录签到时间
scheduleSlotMapper.updateSlotStatusAndCheckInTime(slotId, SlotStatus.CHECKED_IN, new Date());
// 4. 更新号源池统计:锁定数-1已预约数+1
if (slot != null && slot.getPoolId() != null) {
schedulePoolMapper.updatePoolStatsOnCheckIn(slot.getPoolId());
}
return 1;
}
/**
@@ -309,7 +333,7 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
orderService.cancelAppointmentOrder(order.getId(), "医生停诊");
}
int updated = scheduleSlotMapper.updateSlotStatus(slotId, SlotStatus.STOPPED);
int updated = scheduleSlotMapper.updateSlotStatus(slotId, SlotStatus.CANCELLED);
if (updated > 0) {
refreshPoolStatsBySlotId(slotId);
}

View File

@@ -17,7 +17,7 @@ import java.util.Date;
* @date 2025-06-03
*/
@Data
@TableName("doc_ital_signs")
@TableName("doc_vital_signs")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
public class VitalSigns extends HisBaseEntity {

View File

@@ -4,10 +4,46 @@
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.openhis.appointmentmanage.mapper.ScheduleSlotMapper">
<!-- 统一状态值(兼容数字/英文字符串存储),输出 Integer避免 resultType 映射 NumberFormatException -->
<sql id="slotStatusNormExpr">
CASE
WHEN LOWER(CONCAT('', s.status)) IN ('0', 'unbooked', 'available') THEN 0
WHEN LOWER(CONCAT('', s.status)) IN ('1', 'booked') THEN 1
WHEN LOWER(CONCAT('', s.status)) IN ('2', 'cancelled', 'canceled', 'stopped') THEN 2
WHEN LOWER(CONCAT('', s.status)) IN ('3', 'locked') THEN 3
WHEN LOWER(CONCAT('', s.status)) IN ('4', 'checked', 'checked_in', 'checkin') THEN 4
WHEN LOWER(CONCAT('', s.status)) IN ('5', 'returned') THEN 5
ELSE NULL
END
</sql>
<sql id="orderStatusNormExpr">
CASE
WHEN LOWER(CONCAT('', o.status)) IN ('1', 'booked') THEN 1
WHEN LOWER(CONCAT('', o.status)) IN ('2', 'checked', 'checked_in', 'checkin') THEN 2
WHEN LOWER(CONCAT('', o.status)) IN ('3', 'cancelled', 'canceled') THEN 3
WHEN LOWER(CONCAT('', o.status)) IN ('4', 'returned') THEN 4
ELSE NULL
END
</sql>
<sql id="poolStatusNormExpr">
CASE
WHEN LOWER(CONCAT('', p.status)) IN ('0', 'unbooked', 'available') THEN 0
WHEN LOWER(CONCAT('', p.status)) IN ('1', 'booked') THEN 1
WHEN LOWER(CONCAT('', p.status)) IN ('2', 'cancelled', 'canceled', 'stopped') THEN 2
WHEN LOWER(CONCAT('', p.status)) IN ('3', 'locked') THEN 3
WHEN LOWER(CONCAT('', p.status)) IN ('4', 'checked', 'checked_in', 'checkin') THEN 4
WHEN LOWER(CONCAT('', p.status)) IN ('5', 'returned') THEN 5
ELSE NULL
END
</sql>
<!-- 注意这里的 resultType 指向了您刚建好的 DTO 实体类 -->
<select id="selectAllTicketSlots" resultType="com.openhis.appointmentmanage.domain.TicketSlotDTO">
SELECT
s.id AS slotId,
s.seq_no AS seqNo,
p.schedule_id AS scheduleId,
p.doctor_name AS doctor,
p.dept_id AS departmentId,
@@ -16,12 +52,12 @@
o.patient_name AS patientName,
o.medical_card AS medicalCard,
o.phone AS phone,
o.status AS orderStatus,
s.status AS slotStatus,
<include refid="orderStatusNormExpr" /> AS orderStatus,
<include refid="slotStatusNormExpr" /> AS slotStatus,
s.expect_time AS expectTime,
p.schedule_date AS scheduleDate,
d.reg_type AS regType,
p.status AS poolStatus,
<include refid="poolStatusNormExpr" /> AS poolStatus,
p.stop_reason AS stopReason,
d.is_stopped AS isStopped
FROM
@@ -39,7 +75,7 @@
FROM
order_main
WHERE
status IN (1, 2)
LOWER(CONCAT('', status)) IN ('1', '2', '4', 'booked', 'checked', 'checked_in', 'checkin', 'returned')
ORDER BY
slot_id,
create_time DESC
@@ -56,6 +92,7 @@
<select id="selectTicketSlotById" resultType="com.openhis.appointmentmanage.domain.TicketSlotDTO">
SELECT
s.id AS slotId,
s.seq_no AS seqNo,
p.schedule_id AS scheduleId,
p.doctor_name AS doctor,
p.doctor_id AS doctorId,
@@ -66,12 +103,18 @@
o.patient_name AS patientName,
o.medical_card AS medicalCard,
o.phone AS phone,
o.status AS orderStatus,
s.status AS slotStatus,
o.id AS orderId,
o.order_no AS orderNo,
COALESCE(CAST(o.gender AS VARCHAR), CAST(pinfo.gender_enum AS VARCHAR)) AS patientGender,
pinfo.gender_enum AS genderEnum,
pinfo.id_card AS idCard,
o.appointment_time AS appointmentTime,
<include refid="orderStatusNormExpr" /> AS orderStatus,
<include refid="slotStatusNormExpr" /> AS slotStatus,
s.expect_time AS expectTime,
p.schedule_date AS scheduleDate,
d.reg_type AS regType,
p.status AS poolStatus,
<include refid="poolStatusNormExpr" /> AS poolStatus,
p.stop_reason AS stopReason,
d.is_stopped AS isStopped
FROM
@@ -87,15 +130,20 @@
patient_name,
medical_card,
phone,
id,
order_no,
gender,
appointment_time,
status
FROM
order_main
WHERE
status IN (1, 2)
LOWER(CONCAT('', status)) IN ('1', '2', '4', 'booked', 'checked', 'checked_in', 'checkin', 'returned')
ORDER BY
slot_id,
create_time DESC
) o ON o.slot_id = s.id
LEFT JOIN adm_patient pinfo ON o.patient_id = pinfo.id
WHERE
s.id = #{id}
</select>
@@ -115,7 +163,7 @@
UPDATE adm_schedule_slot
SET
status = #{status},
<if test="status == 0">
<if test="status != null and '0'.equals(status.toString())">
order_id = NULL,
</if>
update_time = now()
@@ -124,11 +172,25 @@
AND delete_flag = '0'
</update>
<update id="updateSlotStatusAndCheckInTime">
UPDATE adm_schedule_slot
SET
status = #{status},
check_in_time = #{checkInTime},
update_time = NOW()
WHERE
id = #{slotId}
AND delete_flag = '0'
</update>
<select id="selectPoolIdBySlotId" resultType="java.lang.Long">
SELECT pool_id
FROM adm_schedule_slot
WHERE id = #{slotId}
AND delete_flag = '0'
SELECT
pool_id
FROM
adm_schedule_slot
WHERE
id = #{slotId}
AND delete_flag = '0'
</select>
<update id="bindOrderToSlot">
@@ -145,6 +207,7 @@
<select id="selectTicketSlotsPage" resultType="com.openhis.appointmentmanage.domain.TicketSlotDTO">
SELECT
s.id AS slotId,
s.seq_no AS seqNo,
p.schedule_id AS scheduleId,
p.doctor_name AS doctor,
p.doctor_id AS doctorId,
@@ -155,12 +218,18 @@
o.patient_name AS patientName,
o.medical_card AS medicalCard,
o.phone AS phone,
o.status AS orderStatus,
s.status AS slotStatus,
o.id AS orderId,
o.order_no AS orderNo,
COALESCE(CAST(o.gender AS VARCHAR), CAST(pinfo.gender_enum AS VARCHAR)) AS patientGender,
pinfo.gender_enum AS genderEnum,
pinfo.id_card AS idCard,
o.appointment_time AS appointmentTime,
<include refid="orderStatusNormExpr" /> AS orderStatus,
<include refid="slotStatusNormExpr" /> AS slotStatus,
s.expect_time AS expectTime,
p.schedule_date AS scheduleDate,
d.reg_type AS regType,
p.status AS poolStatus,
<include refid="poolStatusNormExpr" /> AS poolStatus,
p.stop_reason AS stopReason,
d.is_stopped AS isStopped
FROM
@@ -176,15 +245,20 @@
patient_name,
medical_card,
phone,
id,
order_no,
gender,
appointment_time,
status
FROM
order_main
WHERE
status IN (1, 2)
LOWER(CONCAT('', status)) IN ('1', '2', '4', 'booked', 'checked', 'checked_in', 'checkin', 'returned')
ORDER BY
slot_id,
create_time DESC
) o ON o.slot_id = s.id
LEFT JOIN adm_patient pinfo ON o.patient_id = pinfo.id
<where>
p.delete_flag = '0'
AND s.delete_flag = '0' <!-- 1. 按日期查 -->
@@ -225,35 +299,49 @@
<!-- 5. 核心:解答您疑问的 4 种业务状态的复合查询! -->
<if test="query.status != null and query.status != '' and query.status != 'all'">
<choose>
<when test="query.status == 'unbooked'">
AND s.status = 0
<when test="'unbooked'.equals(query.status) or '未预约'.equals(query.status)">
AND <include refid="slotStatusNormExpr" /> = 0
AND (
d.is_stopped IS NULL
OR d.is_stopped = FALSE
)
</when>
<when test="query.status == 'booked'">
AND s.status = 1
AND o.status = 1
<when test="'booked'.equals(query.status) or '已预约'.equals(query.status)">
AND <include refid="slotStatusNormExpr" /> = 1
AND <include refid="orderStatusNormExpr" /> = 1
AND (
d.is_stopped IS NULL
OR d.is_stopped = FALSE
)
</when>
<when test="query.status == 'checked'">
AND s.status = 1
AND o.status = 2
<when test="'checked'.equals(query.status) or '已取号'.equals(query.status)">
AND (
<include refid="slotStatusNormExpr" /> = 4
OR (
<include refid="slotStatusNormExpr" /> = 1
AND <include refid="orderStatusNormExpr" /> = 2
)
)
AND (
d.is_stopped IS NULL
OR d.is_stopped = FALSE
)
</when>
<when test="query.status == 'cancelled'">
<when test="'cancelled'.equals(query.status) or '已停诊'.equals(query.status) or '已取消'.equals(query.status)">
AND (
s.status = 2
<include refid="slotStatusNormExpr" /> = 2
OR d.is_stopped = TRUE
)
</when>
<when test="'returned'.equals(query.status) or '已退号'.equals(query.status)">
AND (
<include refid="slotStatusNormExpr" /> = 5
OR <include refid="orderStatusNormExpr" /> = 4
)
</when>
<otherwise>
AND 1 = 2
</otherwise>
</choose>
</if>
</where>
@@ -266,9 +354,22 @@
SELECT
p.doctor_id AS doctorId,
p.doctor_name AS doctorName,
COALESCE(SUM(GREATEST(COALESCE(p.total_quota, 0) - COALESCE(p.booked_num, 0) - COALESCE(p.locked_num, 0), 0)), 0) AS available,
COALESCE(
SUM(
GREATEST(
COALESCE(p.total_quota, 0) - COALESCE(p.booked_num, 0) - COALESCE(p.locked_num, 0),
0
)
),
0
) AS available,
CASE
WHEN MAX(CASE WHEN d.reg_type = 1 THEN 1 ELSE 0 END) = 1 THEN 'expert'
WHEN MAX(
CASE
WHEN d.reg_type = 1 THEN 1
ELSE 0
END
) = 1 THEN 'expert'
ELSE 'general'
END AS ticketType
FROM
@@ -290,7 +391,10 @@
AND d.reg_type = 1
</when>
<when test="query.type == 'general'">
AND (d.reg_type != 1 OR d.reg_type IS NULL)
AND (
d.reg_type != 1
OR d.reg_type IS NULL
)
</when>
</choose>
</if>

View File

@@ -127,7 +127,7 @@
select * from order_main where slot_id = #{slotId} and status = 1
</select>
<insert id="insertOrder" parameterType="com.openhis.clinical.domain.Order">
<insert id="insertOrder" parameterType="com.openhis.clinical.domain.Order" useGeneratedKeys="true" keyProperty="id">
insert into order_main
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="orderNo != null and orderNo != ''">order_no,</if>
@@ -225,6 +225,12 @@
update order_main set status = 3, cancel_time = #{cancelTime}, cancel_reason = #{cancelReason} where id = #{id}
</update>
<update id="updatePayStatus">
update order_main
set pay_status = #{payStatus}, pay_time = #{payTime}, update_time = NOW()
where id = #{orderId}
</update>
<delete id="deleteOrderById">
delete from order_main where id = #{id}
</delete>

View File

@@ -162,3 +162,61 @@ export const STATUS = {
NORMAL: '0', // 正常/启用
DISABLE: '1' // 停用
};
/**
* 号源槽位状态(与后端 CommonConstants.SlotStatus 保持一致)
* adm_schedule_slot.status 字段
*/
export const SlotStatus = {
/** 可用 / 待预约 */
AVAILABLE: 0,
/** 已预约 */
BOOKED: 1,
/** 已取消 / 已停诊 */
CANCELLED: 2,
/** 已锁定 */
LOCKED: 3,
/** 已签到 / 已取号 */
CHECKED_IN: 4,
};
/**
* 号源槽位状态说明信息
*/
export const SlotStatusDescriptions = {
0: '未预约',
1: '已预约',
2: '已停诊',
3: '已锁定',
4: '已取号',
};
/**
* 号源槽位状态对应的CSS类名
*/
export const SlotStatusClassMap = {
'未预约': 'status-unbooked',
'已预约': 'status-booked',
'已取号': 'status-checked',
'已停诊': 'status-cancelled',
'已取消': 'status-cancelled',
'已锁定': 'status-locked',
};
/**
* 获取号源槽位状态的说明
* @param {number} value - 状态值
* @returns {string} - 说明信息
*/
export function getSlotStatusDescription(value) {
return SlotStatusDescriptions[value] || '未知状态';
}
/**
* 获取号源槽位状态对应的CSS类名
* @param {string} status - 状态说明
* @returns {string} - CSS类名
*/
export function getSlotStatusClass(status) {
return SlotStatusClassMap[status] || 'status-unbooked';
}

View File

@@ -37,6 +37,7 @@
<option value="booked">已预约</option>
<option value="checked">已取号</option>
<option value="cancelled">已停诊</option>
<option value="returned">已退号</option>
</select>
</div>
<div id="patientSearch" class="patient-search">
@@ -105,7 +106,7 @@
<div class="ticket-grid" v-if="filteredTickets.length > 0">
<div v-for="(item, index) in filteredTickets" :key="item.slot_id" class="ticket-card" @dblclick="handleDoubleClick(item)" @contextmenu.prevent="handleRightClick($event, item)">
<!-- 序号放在最右侧 -->
<div class="ticket-index">{{ index + 1 }}</div>
<div class="ticket-index">{{ item.seqNo != null ? item.seqNo : index + 1 }}</div>
<!-- 1.时间 -->
<div class="ticket-id-time">{{ item.dateTime }}</div>
<!-- 2. 状态标签 -->
@@ -125,7 +126,7 @@
<div class="ticket-type">{{ item.ticketType === 'general' ? '普通' : '专家' }}</div>
<!-- 7. 已预约患者信息 -->
<div v-if="(item.status === '已预约' || item.status === '已取号') && item.patientName" class="ticket-patient">
{{ item.patientName }}({{ item.patientId }})
{{ item.patientName }}({{ item.patientId }},{{ getGenderText(item.gender || item.patientGender) }})
</div>
<div class="ticket-actions">
<button class="action-button book-button" @click="openPatientSelectModal(item.slot_id)" :disabled="item.status !== '未预约'" :class="{ 'disabled': item.status !== '未预约' }">
@@ -194,7 +195,8 @@
>
<td>{{ index + 1 }}</td>
<td>{{ patient.name }}</td>
<td>{{ patient.id || patient.medicalCard }}</td>
<td>{{ patient.identifierNo || patient.medicalCard || patient.id }}</td>
<td>{{ patient.identifierNo }}</td>
<td>{{ getGenderText(patient.genderEnum_enumText || patient.genderEnum || patient.gender || patient.sex) }}</td>
<td>{{ patient.idCard }}</td>
<td>{{ patient.phone }}</td>
@@ -254,6 +256,7 @@ const STATUS_CLASS_MAP = {
'未预约': 'status-unbooked',
'已预约': 'status-booked',
'已取号': 'status-checked',
'已退号': 'status-returned',
'已停诊': 'status-cancelled',
'已取消': 'status-cancelled'
};
@@ -364,14 +367,23 @@ export default {
this.searchPatients();
},
getPatientUniqueId(patient, index = null) {
return patient.idCard || patient.medicalCard || patient.id || (index !== null ? `temp_${index}` : null);
return patient.id || patient.patientId || patient.identifierNo || patient.medicalCard || patient.idCard ||
(index !== null ? `temp_${index}` : null);
},
matchPatientKeyword(patient, keyword) {
const normalizedKeyword = String(keyword || '').trim().toLowerCase();
if (!normalizedKeyword) {
return true;
}
const fields = [patient.name, patient.id, patient.medicalCard, patient.idCard, patient.phone];
const fields = [
patient.name,
patient.id,
patient.patientId,
patient.identifierNo,
patient.medicalCard,
patient.idCard,
patient.phone
];
return fields.some(field => String(field || '').toLowerCase().includes(normalizedKeyword));
},
searchPatients() {
@@ -394,6 +406,8 @@ export default {
const rowKey = this.getPatientUniqueId(patient, index);
return {
...patient,
// 统一口径:预约场景的就诊卡号使用患者标识表中的 identifierNo
medicalCard: patient.identifierNo || patient.medicalCard || '',
_rowKey: rowKey
};
});
@@ -494,6 +508,7 @@ export default {
this.tickets[ticketIndex].patientName = null;
this.tickets[ticketIndex].patientId = null;
this.tickets[ticketIndex].patientGender = null;
this.tickets[ticketIndex].gender = null;
this.tickets[ticketIndex].medicalCard = null;
this.tickets[ticketIndex].phone = null;
}
@@ -554,12 +569,22 @@ export default {
try {
const userStore = useUserStore();
const patientPrimaryId = this.selectedPatient.id || this.selectedPatient.patientId;
const medicalCard = this.selectedPatient.identifierNo || this.selectedPatient.medicalCard;
if (!patientPrimaryId) {
ElMessage.error('患者ID缺失无法预约请重新选择患者');
return;
}
if (!medicalCard) {
ElMessage.error('就诊卡号缺失,无法预约,请先维护患者就诊卡号');
return;
}
const appointmentData = {
ticketId: this.currentTicket.slot_id,
slotId: this.currentTicket.slot_id,
patientId: this.selectedPatient.id || this.selectedPatient.idCard || this.selectedPatient.medicalCard,
patientId: patientPrimaryId,
patientName: this.selectedPatient.name,
medicalCard: this.selectedPatient.medicalCard || this.selectedPatient.id,
medicalCard,
phone: this.selectedPatient.phone,
gender: this.getGenderValueForBackend(this.selectedPatient),
fee: Number(this.currentTicket.fee) || 0,
@@ -578,8 +603,9 @@ export default {
if (ticketIndex !== -1) {
this.tickets[ticketIndex].status = '已预约';
this.tickets[ticketIndex].patientName = this.selectedPatient.name;
this.tickets[ticketIndex].patientId = this.selectedPatient.medicalCard || this.selectedPatient.id;
this.tickets[ticketIndex].patientId = medicalCard;
this.tickets[ticketIndex].patientGender = this.selectedPatient.genderEnum_enumText || this.selectedPatient.genderEnum || this.selectedPatient.gender || this.selectedPatient.sex;
this.tickets[ticketIndex].gender = this.getGenderText(this.tickets[ticketIndex].patientGender);
}
this.closePatientSelectModal();
@@ -601,9 +627,9 @@ export default {
},
// 获取性别文本
getGenderText(genderValue) {
// 如果值为null或undefined返回'-'
// 如果值为null或undefined返回"未知"
if (genderValue === null || genderValue === undefined) {
return '-';
return '未知';
}
// 将值转换为字符串进行比较
@@ -623,8 +649,8 @@ export default {
return '女';
}
// 如果都不是,返回原值或'-'
return '-';
// 如果都不是,返回"未知"
return '未知';
},
// 获取用于后端的性别值
getGenderValueForBackend(patient) {
@@ -703,10 +729,36 @@ export default {
return;
}
const records = payload.list || payload.records || [];
const filteredRecords = this.applyStatusFilter(records);
const total = Number(payload.total);
this.tickets = [...records];
this.allTickets = [...records];
this.totalTickets = Number.isFinite(total) ? total : this.tickets.length;
this.tickets = [...filteredRecords];
this.allTickets = [...filteredRecords];
// 当按状态筛选时,优先使用前端过滤后的数量,避免后端状态未生效导致“显示全部”
if (this.selectedStatus && this.selectedStatus !== 'all') {
this.totalTickets = this.tickets.length;
} else {
this.totalTickets = Number.isFinite(total) ? total : this.tickets.length;
}
},
applyStatusFilter(records = []) {
if (!Array.isArray(records) || records.length === 0) {
return [];
}
if (!this.selectedStatus || this.selectedStatus === 'all') {
return records;
}
const statusMap = {
unbooked: ['未预约'],
booked: ['已预约'],
checked: ['已取号'],
cancelled: ['已停诊', '已取消'],
returned: ['已退号']
};
const matchedStatusList = statusMap[this.selectedStatus] || [];
if (matchedStatusList.length === 0) {
return records;
}
return records.filter(item => matchedStatusList.includes(item?.status));
},
updateDoctorsListFromApi(doctorResponse) {
let doctorList = [];
@@ -1376,6 +1428,11 @@ export default {
color: #52c41a;
}
.status-returned {
background-color: #fff7e6;
color: #d46b08;
}
.status-cancelled {
background-color: #fff1f0;
color: #ff4d4f;

View File

@@ -7,6 +7,7 @@
<div style="display: flex; align-items: center; width: 100%">
<span style="font-size: 16px; font-weight: bold; margin-right: 20px;">门诊挂号</span>
<div style="flex: 1; display: flex; justify-content: center; align-items: center;">
<el-button type="success" icon="Check" @click="handleCheckIn" size="small">预约签到</el-button>
<el-button type="primary" icon="Document" @click="goToPatientRecord" size="small">档案</el-button>
<el-button type="primary" icon="Plus" @click="handleAddPatient" size="small">新建</el-button>
<el-button type="primary" plain icon="Search" @click="handleSearch" size="small">查询</el-button>
@@ -15,7 +16,7 @@
<el-button type="primary" plain @click="handleReadCard('03')" size="small">医保卡</el-button>
<el-button type="warning" plain icon="CircleClose" @click="handleClear" size="small">清空</el-button>
<el-button type="primary" icon="Plus" @click="handleAdd" size="small">保存挂号</el-button>
<el-button type="success" icon="Printer" @click="handleReprint" size="small">补打挂号</el-button>
<el-button type="info" icon="Printer" @click="handleReprint" size="small">补打挂号</el-button>
</div>
</div>
</template>
@@ -615,6 +616,74 @@
}
"
/>
<!-- 预约签到患者选择弹窗 -->
<el-dialog
v-model="showCheckInPatientModal"
title="请选择预约的患者"
width="1200px"
:close-on-click-modal="false"
>
<div style="margin-bottom: 20px; display: flex; gap: 10px;">
<el-input
v-model="checkInSearchKey"
placeholder="输入患者姓名回车查询"
style="width: 400px"
@keyup.enter="loadCheckInPatientList"
/>
<el-button type="primary" @click="loadCheckInPatientList">查询</el-button>
</div>
<el-table
v-loading="checkInLoading"
:data="checkInPatientList"
border
style="width: 100%"
@row-click="selectRow"
highlight-current-row
>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="patientId" label="就诊卡号" width="120" align="center" />
<el-table-column prop="patientName" label="姓名" width="120" align="center">
<template #default="scope">
<span style="color: #ff4d4f">{{ scope.row.patientName }}</span>
</template>
</el-table-column>
<el-table-column prop="gender" label="性别" width="80" align="center" />
<el-table-column label="证件类型" width="150" align="center">
<template #default>居民身份证</template>
</el-table-column>
<el-table-column prop="idCard" label="证件号码" width="200" align="center" />
<el-table-column prop="phone" label="手机号码" width="150" align="center" />
<el-table-column label="号源类型" width="100" align="center">
<template #default="scope">
<el-tag :type="scope.row.ticketType === 'expert' ? 'danger' : 'success'">
{{ scope.row.ticketType === 'expert' ? '专家号' : '普通号' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="fee" label="预约金额" width="100" align="center">
<template #default="scope">
<span style="font-weight: bold; color: #f5222d">{{ scope.row.fee }}</span>
</template>
</el-table-column>
<el-table-column prop="dateTime" label="就诊时间" width="180" align="center" />
</el-table>
<div style="margin-top: 20px; display: flex; justify-content: space-between; align-items: center;">
<el-pagination
v-model:current-page="checkInPage"
v-model:page-size="checkInLimit"
:total="checkInTotal"
layout="prev, pager, next"
@current-change="loadCheckInPatientList"
/>
<div class="dialog-footer">
<el-button @click="showCheckInPatientModal = false">取消</el-button>
<el-button type="primary" @click="confirmCheckIn" :disabled="!selectedCheckInPatient">确定</el-button>
</div>
</div>
</el-dialog>
</div>
</template>
@@ -632,7 +701,8 @@ import {
returnRegister,
updatePatientPhone,
} from './components/outpatientregistration';
import {invokeYbPlugin5000, invokeYbPlugin5001} from '@/api/public';
import { listTicket, checkInTicket } from '@/api/appoinmentmanage/ticket';
import { invokeYbPlugin5000, invokeYbPlugin5001 } from '@/api/public';
import patientInfoDialog from './components/patientInfoDialog';
import PatientAddDialog from './components/patientAddDialog';
import patientList from './components/patientList';
@@ -644,7 +714,7 @@ import {handleColor} from '@/utils/his';
import useUserStore from '@/store/modules/user';
import {formatDateStr} from '@/utils/index';
import {isValidCNPhoneNumber} from '../../../utils/validate';
import {ElMessage} from 'element-plus';
import {ElMessage, ElMessageBox} from 'element-plus';
import {hiprint} from 'vue-plugin-hiprint';
import outpatientRegistrationTemplate from '@/components/Print/OutpatientRegistration.json';
@@ -687,14 +757,25 @@ const ybTypeRef = ref(null);
const openDialog = ref(false);
const openRefundDialog = ref(false);
const openReprintDialog = ref(false);
// 预约签到相关变量
const showCheckInPatientModal = ref(false);
const checkInPatientList = ref([]);
const selectedCheckInPatient = ref(null);
const totalAmount = ref(0);
const chargeItemIdList = ref([]);
const chrgBchnoList = ref([]);
const paymentId = ref('');
const loadingText = ref('');
const checkInSearchKey = ref('');
const checkInPage = ref(1);
const checkInLimit = ref(10);
const checkInTotal = ref(0);
const checkInLoading = ref(false);
const registerInfo = ref({}); // 原挂号记录信息
const queryType = ref('all'); // 查询类型all-全部, normal-正常挂号, returned-退号记录
const guardianAgeConfig = ref(''); // 监护人规定年龄配置
const currentSlotId = ref(null); // 当前预约签到的号源ID
// 使用 ref 定义查询所得用户信息数据
const patientInfoList = ref(undefined);
@@ -1584,6 +1665,189 @@ function handleReprint() {
openReprintDialog.value = true;
}
/** 预约签到 - 打开患者选择弹窗 */
function handleCheckIn() {
// 打开患者选择弹窗,显示已预约但未签到的患者列表
showCheckInPatientModal.value = true;
// 加载已预约未签到的患者列表
loadCheckInPatientList();
}
/** 加载预约签到患者列表 */
function loadCheckInPatientList() {
checkInLoading.value = true;
const today = formatDateStr(new Date(), 'YYYY-MM-DD');
listTicket({
date: today,
status: 'booked',
name: checkInSearchKey.value, // 支持姓名等模糊查询,后端需适配
page: checkInPage.value,
limit: checkInLimit.value
}).then(res => {
const data = res.data?.list || res.list || res.data || [];
const total = res.data?.total || res.total || data.length;
checkInPatientList.value = data.map(item => ({
...item,
appointmentDate: item.scheduleDate + ' ' + (item.expectTime || '')
}));
checkInTotal.value = total;
}).catch(err => {
console.error('加载预约导出失败:', err);
ElMessage.error('获取预约列表失败');
}).finally(() => {
checkInLoading.value = false;
});
}
/** 弹窗行点击处理 */
function selectRow(row) {
selectedCheckInPatient.value = row;
}
/** 确认签到(一键签到:直接构建挂号参数 → 预结算 → 弹收费窗口) */
async function confirmCheckIn() {
if (!selectedCheckInPatient.value) {
ElMessage.warning('请先选择患者');
return;
}
const patient = selectedCheckInPatient.value;
// 每次开始新的签到流程先清理残留 slotId避免历史脏值串单
currentSlotId.value = null;
// 弹出确认提示
try {
await ElMessageBox.confirm(
`确认为患者【${patient.patientName}】办理签到挂号?\n` +
`科室:${patient.department || '-'}\n` +
`医生:${patient.doctor || '-'}\n` +
`费用:¥${patient.fee || '0.00'}`,
'签到确认',
{
confirmButtonText: '确认签到',
cancelButtonText: '取消',
type: 'info',
}
);
} catch {
// 用户点了取消
return;
}
showCheckInPatientModal.value = false;
readCardLoading.value = true;
loadingText.value = '正在处理签到挂号...';
try {
// 1. 用科室ID加载该科室的挂号类型列表获取 serviceTypeId 和 definitionId
const healthcareRes = await getHealthcareMetadata({ organizationId: patient.departmentId });
const healthcareRecords = healthcareRes.data?.records || [];
if (healthcareRecords.length === 0) {
ElMessage.error('该科室未配置挂号类型,无法自动签到');
readCardLoading.value = false;
return;
}
// 2. 按号源类型(专家/普通)模糊匹配挂号类型
const matchTypeName = (patient.ticketType === 'expert') ? '专家' : '普通';
const matchedService = healthcareRecords.find(h => h.name && h.name.includes(matchTypeName));
if (!matchedService) {
// 匹配不到就取第一个作为兜底
ElMessage.warning('未精确匹配到挂号类型,已使用默认类型');
}
const service = matchedService || healthcareRecords[0];
const realPatientId = patient.realPatientId; // 后端新增的真实患者数据库ID
if (!realPatientId) {
ElMessage.error('患者ID缺失请联系管理员检查预约数据');
readCardLoading.value = false;
return;
}
// 3. 构建挂号参数(与 transformFormData 结构一致)
const registrationParam = {
encounterFormData: {
patientId: realPatientId,
priorityEnum: 3, // 默认优先级
serviceTypeId: service.id,
organizationId: patient.departmentId,
},
encounterLocationFormData: {
locationId: null,
},
encounterParticipantFormData: {
practitionerId: patient.doctorId,
},
accountFormData: {
patientId: realPatientId,
typeCode: 1, // 个人现金账户
contractNo: '0000', // 默认自费
},
chargeItemFormData: {
patientId: realPatientId,
definitionId: service.definitionId,
serviceId: service.id,
totalPrice: parseFloat(patient.fee) || ((service.price || 0) + (service.activityPrice || 0)),
},
};
// 4. 设置 patientInfoChargeDialog 需要展示)
patientInfo.value = {
patientId: realPatientId,
patientName: patient.patientName,
genderEnum_enumText: patient.gender || '-',
age: '',
contractName: '自费',
idCard: patient.idCard,
phone: patient.phone,
categoryEnum: '门诊',
organizationName: patient.department || '',
practitionerName: patient.doctor || '',
healthcareName: service.name || '',
};
// 同步设置 form 的 contractNoChargeDialog 的 feeType 会读取它
form.value.contractNo = '0000';
// 5. 调用预结算接口reg-pre-pay
const res = await addOutpatientRegistration(registrationParam);
if (res.code == 200) {
// 仅在预结算成功后记录待签到的号源,避免失败路径残留脏数据
currentSlotId.value = patient.slot_id;
// 6. 设置收费弹窗所需的数据
chrgBchno.value = res.data.chrgBchno;
registerBusNo.value = res.data.busNo;
totalAmount.value = res.data.psnCashPay;
patientInfo.value.encounterId = res.data.encounterId || '';
patientInfo.value.busNo = res.data.busNo || '';
transformedData.value = registrationParam;
chargeItemIdList.value = [];
// 7. 打开收费弹窗
openDialog.value = true;
// 打印挂号单
printRegistrationByHiprint(res.data);
} else {
currentSlotId.value = null;
ElMessage.error(res.msg || '预结算失败');
}
} catch (err) {
currentSlotId.value = null;
console.error('预约签到失败:', err);
ElMessage.error('签到处理失败: ' + (err.message || '未知错误'));
} finally {
readCardLoading.value = false;
}
}
/**
* 点击患者列表给表单赋值
*/
@@ -1656,20 +1920,29 @@ function handleClose(value) {
proxy.$modal.msgSuccess('操作成功');
// 更新患者手机号
updatePhone();
// getList();
// reset();
// addOutpatientRegistration(transformedData.value).then((response) => {
// reset();
// proxy.$modal.msgSuccess('新增成功');
// getList();
// });
// 先取出并清空,避免接口失败/取消等路径导致 slotId 残留污染下一单
const pendingSlotId = currentSlotId.value;
currentSlotId.value = null;
// 如果是预约签到的挂号,执行签到状态更新
if (pendingSlotId) {
checkInTicket(pendingSlotId).then(() => {
console.log('预约状态已更新为已取号');
}).catch(err => {
console.error('更新预约状态失败:', err);
ElMessage.error('预约状态更新失败,请手动签到');
});
}
} else if (value == 'cancel') {
currentSlotId.value = null;
// cancelRegister(patientInfo.value.encounterId).then((res) => {
// if (res.code == 200) {
// getList();
// }
// });
} else {
currentSlotId.value = null;
openRefundDialog.value = false;
}
}

View File

@@ -257,15 +257,16 @@ function fetchFromApi(searchKey) {
searchKey: searchKey || '',
statusEnum: 2,
}).then((res) => {
console.log('[Debug] 耗材列表返回数据:', res.data);
console.log('[BugFix] 耗材列表返回数据:', res.data);
if (res.data && res.data.records) {
adviceBaseList.value = res.data.records.map((item) => {
console.log('[Debug] 耗材项:', item.name, 'price:', item.price, 'retailPrice:', item.retailPrice);
return {
console.log('[BugFix] 耗材项:', item.name, 'price:', item.price, 'retailPrice:', item.retailPrice);
const mappedItem = {
...item,
// 🔧 Bug Fix: 强制覆盖后端返回的字段,确保数据正确
adviceName: item.name || item.busNo,
adviceType: 4, // 强制设置为前端耗材类型
adviceType_dictText: '耗材', // 🔧 Bug Fix: 设置医嘱类型显示文本
adviceTableName: 'adm_device_definition',
unitCode: item.unitCode || '',
unitCode_dictText: item.unitCode_dictText || '',
@@ -273,7 +274,10 @@ function fetchFromApi(searchKey) {
minUnitCode_dictText: item.minUnitCode_dictText || item.unitCode_dictText || '',
volume: item.size || item.totalVolume || '',
partPercent: item.partPercent || 1,
inventoryList: [],
// 🔧 Bug Fix: 如果后端提供了inventoryList则使用否则为空数组
inventoryList: item.inventoryList || [],
// 🔧 Bug Fix: 构造stockList用于库存显示
stockList: item.inventoryList || [],
adviceDefinitionId: item.id,
chargeItemDefinitionId: item.id,
positionId: item.locationId,
@@ -290,12 +294,14 @@ function fetchFromApi(searchKey) {
deviceName: item.name,
// 🔧 Bug #220 修复正确处理耗材价格支持price或retailPrice字段
// 价格字段优先使用retailPrice
priceList: (item.retailPrice !== undefined && item.retailPrice !== null)
? [{ price: item.retailPrice }]
: ((item.price !== undefined && item.price !== null)
? [{ price: item.price }]
priceList: (item.retailPrice !== undefined && item.retailPrice !== null)
? [{ price: item.retailPrice }]
: ((item.price !== undefined && item.price !== null)
? [{ price: item.price }]
: []),
};
console.log('[BugFix] 映射后的耗材项:', mappedItem.adviceName, 'adviceType:', mappedItem.adviceType, 'adviceType_dictText:', mappedItem.adviceType_dictText);
return mappedItem;
});
nextTick(() => {
currentIndex.value = 0;
@@ -354,6 +360,11 @@ function handleQuantity(row) {
const totalQuantity = row.inventoryList.reduce((sum, item) => sum + (item.quantity || 0), 0);
return totalQuantity.toString() + row.minUnitCode_dictText;
}
// 🔧 Bug Fix: 耗材类型可能没有inventoryList但可能有stockList
if (row.stockList && row.stockList.length > 0) {
const totalQuantity = row.stockList.reduce((sum, item) => sum + (item.quantity || 0), 0);
return totalQuantity.toString() + row.minUnitCode_dictText;
}
return 0;
}

View File

@@ -164,7 +164,14 @@
</el-col>
<el-col :span="8">
<el-form-item label="检查方法">
<el-input v-model="form.inspectionMethod" readonly />
<el-select v-model="form.inspectionMethod" placeholder="请选择" clearable filterable style="width: 100%;">
<el-option
v-for="method in availableMethods"
:key="method.id"
:label="method.name"
:value="method.name"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
@@ -308,6 +315,7 @@ import { ElMessage, ElMessageBox } from 'element-plus';
import { Printer, Delete } from '@element-plus/icons-vue';
import useUserStore from '@/store/modules/user';
import request from '@/utils/request';
import { listCheckMethod } from '@/api/system/checkType';
const props = defineProps({
patientInfo: { type: Object, default: () => ({}) },
@@ -373,10 +381,69 @@ const categoryList = ref([]); // 原始分类+项目数据
const dictSearchKey = ref('');
const activeNames = ref([]); // 当前展开的折叠项
const allMethods = ref([]);
// 加载所有检查方法
async function loadAllMethods() {
try {
const res = await listCheckMethod(); // 使用已导入的或者直接利用 request 请求
let methods = [];
if (res && res.data) {
if (Array.isArray(res.data)) {
methods = res.data;
} else if (res.data.records) {
methods = res.data.records;
} else if (res.data.data && Array.isArray(res.data.data)) {
methods = res.data.data;
}
} else if (Array.isArray(res)) {
methods = res;
} else if (res && res.rows) {
methods = res.rows;
}
allMethods.value = methods;
} catch (err) {
console.error('加载检查方法失败', err);
}
}
onMounted(async () => {
await loadAllMethods();
await loadCategoryList();
});
// 动态可用的检查方法(根据已选部位所属的检查类型进行过滤)
const normalizeTypeValue = value => String(value ?? '').trim().toLowerCase();
const availableMethods = computed(() => {
// 获取当前已选部位的检查类型(可取第一个选中的部位的 checkType
const currentType = form.examTypeCode || (selectedItems.value.length > 0 ? selectedItems.value[0].checkType : '');
const normalizedCurrentType = normalizeTypeValue(currentType);
if (normalizedCurrentType) {
// 兼容脏数据method 的类型可能落在 checkType/type/typeCode/code/typeName/categoryName 中
const filtered = allMethods.value.filter(m => {
const typeCandidates = [
m.checkType,
m.type,
m.typeCode,
m.code,
m.typeName,
m.categoryName
].map(normalizeTypeValue).filter(Boolean);
return typeCandidates.includes(normalizedCurrentType);
});
return filtered.length > 0 ? filtered : allMethods.value;
}
return allMethods.value;
});
// 当可选方法列表改变时,如果当前选中的方法不在新列表中,则清空
watch(availableMethods, (newMethods) => {
if (form.inspectionMethod && !newMethods.find(m => m.name === form.inspectionMethod)) {
form.inspectionMethod = '';
}
});
/**
* 加载检查类型(分类)和检查项目(部位/项目),按类型分组展示
*/
@@ -390,25 +457,45 @@ async function loadCategoryList() {
params: { pageNo: 1, pageSize: 500 } // 取全量分类数据
});
let types = [];
if (typeRes.data?.records) types = typeRes.data.records;
else if (Array.isArray(typeRes.data)) types = typeRes.data;
else if (Array.isArray(typeRes.rows)) types = typeRes.rows;
if (typeRes && typeRes.data) {
if (Array.isArray(typeRes.data)) {
types = typeRes.data;
} else if (typeRes.data.records) {
types = typeRes.data.records;
} else if (typeRes.data.data && Array.isArray(typeRes.data.data)) {
types = typeRes.data.data;
}
} else if (Array.isArray(typeRes)) {
types = typeRes;
} else if (typeRes && typeRes.rows) {
types = typeRes.rows;
}
// 2. 加载检查项目(检查部位项目)
const partRes = await request({ url: '/check/part/list', method: 'get' });
let parts = [];
if (Array.isArray(partRes)) parts = partRes;
else if (Array.isArray(partRes.data?.data)) parts = partRes.data.data; // 双层嵌套:{ data: { data: [...] } }
else if (Array.isArray(partRes.data)) parts = partRes.data;
else if (Array.isArray(partRes.rows)) parts = partRes.rows;
else if (partRes.data?.records) parts = partRes.data.records;
if (partRes && partRes.data) {
if (Array.isArray(partRes.data)) {
parts = partRes.data;
} else if (partRes.data.records) {
parts = partRes.data.records;
} else if (partRes.data.data && Array.isArray(partRes.data.data)) {
parts = partRes.data.data;
}
} else if (Array.isArray(partRes)) {
parts = partRes;
} else if (partRes && partRes.rows) {
parts = partRes.rows;
}
// 3. 按 checkType 归类
const dict = [];
for (const t of types) {
dict.push({
typeId: t.id,
typeCode: t.type,
typeCode: t.code, // 保存 code 用于后备匹配
orgType: t.type, // 保存 type 用于后备匹配
typeName: t.name, // 保存 name
categoryName: t.name,
items: []
});
@@ -425,7 +512,15 @@ async function loadCategoryList() {
nationalCode: p.nationalCode || '',
checked: false
};
const target = dict.find(d => d.typeCode === p.checkType);
// 增强匹配逻辑:部位的 checkType (如 'ECG', 'CT') 优先去匹配大类的 orgType
// 如果大类的 type 字段脏了(比如填了中文),则尝试匹配 code甚至是分类名称
const target = dict.find(d =>
d.orgType === p.checkType ||
d.typeCode === p.checkType ||
d.typeName === p.checkType
);
if (target) target.items.push(mapped);
else unclassified.push(mapped);
}

View File

@@ -1,14 +1,14 @@
<template>
<el-container class="inspection-application-container">
<!-- 顶部操作按钮区 -->
<el-header class="top-action-bar" height="60px">
<el-row class="action-buttons" type="flex" justify="end" :gutter="10">
<el-button type="primary" size="large" @click="handleSave" class="save-btn" :loading="saving">
<!-- 顶部操作按钮区 - Bug#334: 优化垂直空间利用率 -->
<el-header class="top-action-bar" height="48px">
<el-row class="action-buttons" type="flex" justify="end" :gutter="8">
<el-button type="primary" size="default" @click="handleSave" class="save-btn" :loading="saving">
<el-icon><Document /></el-icon>
保存
</el-button>
<el-button type="primary" size="large" @click="handleNewApplication" class="new-btn">
<el-button type="primary" size="default" @click="handleNewApplication" class="new-btn">
<el-icon><Plus /></el-icon>
新增
</el-button>
@@ -1647,19 +1647,20 @@ defineExpose({
padding: 0;
}
/* 顶部操作按钮区 */
/* Bug#334: 顶部操作按钮区 - 优化垂直空间利用率 */
.top-action-bar {
display: flex;
align-items: center;
justify-content: flex-end;
border-bottom: 1px solid var(--el-border-color-light);
background: var(--el-bg-color);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
padding: 0 12px;
}
.action-buttons {
display: flex;
gap: 10px;
gap: 8px;
}
/* 新增按钮样式 - PRD要求蓝色背景 #4a89dc */
@@ -1686,9 +1687,9 @@ defineExpose({
border-color: #58dfbd !important;
}
/* 检验信息表格区 - 紧凑高度 */
/* Bug#334: 检验信息表格区 - 优化垂直空间利用率 */
.inspection-section {
padding: 4px 10px 0 10px;
padding: 2px 10px 0 10px;
}
.table-card {
@@ -1696,7 +1697,7 @@ defineExpose({
}
.table-card :deep(.el-card__body) {
padding-bottom: 8px;
padding-bottom: 6px;
}
.card-header {
@@ -1707,9 +1708,9 @@ defineExpose({
color: var(--el-text-color-primary);
}
/* 底部内容区域 */
/* Bug#334: 底部内容区域 - 优化垂直空间利用率 */
.bottom-content-area {
padding: 4px 10px;
padding: 2px 10px;
}
/* 表单区域 */
@@ -1732,7 +1733,7 @@ defineExpose({
.application-form {
overflow: visible;
padding: 6px 8px;
padding: 4px 8px;
border: 1px solid #e4e7ed;
border-radius: 4px;
margin: 2px;

View File

@@ -223,7 +223,12 @@
style="width: 70px; margin-right: 20px" />
</el-form-item>
<span class="medicine-info"> 诊断{{ diagnosisName }} </span>
<span class="medicine-info"> 皮试{{ scope.row.skinTestFlag_enumText }} </span>
<span class="medicine-info" style="display: flex; align-items: center; gap: 5px;">
皮试<el-checkbox v-model="scope.row.skinTestFlag" :true-value="1" :false-value="0"
@change="handleSkinTestChange(scope.row, scope.$index)">
</el-checkbox>
</span>
<span class="medicine-info"> 注射药品{{ scope.row.injectFlag_enumText }} </span>
<span class="total-amount">
总金额{{
@@ -470,6 +475,12 @@
</template>
</el-select>
</el-form-item>
<span class="medicine-info" style="display: flex; align-items: center; gap: 5px;">
皮试:<el-checkbox v-model="scope.row.skinTestFlag" :true-value="1" :false-value="0"
@change="handleSkinTestChange(scope.row, scope.$index)">
</el-checkbox>
</span>
<span class="total-amount">
总金额:{{
(scope.row.totalPrice !== undefined && scope.row.totalPrice !== null && !isNaN(scope.row.totalPrice)
@@ -546,6 +557,11 @@
expandOrder = [];
// 当医嘱类型改变时,清空当前选择的项目名称,因为不同类型项目的数据结构可能不兼容
prescriptionList[scope.$index].adviceName = undefined;
prescriptionList[scope.$index].adviceType_dictText = '';
// 🔧 Bug Fix: 医嘱类型改变时重置minUnitQuantity和minUnitCode避免null值
prescriptionList[scope.$index].minUnitQuantity = prescriptionList[scope.$index].quantity || 1;
prescriptionList[scope.$index].minUnitCode = prescriptionList[scope.$index].unitCode;
prescriptionList[scope.$index].minUnitCode_dictText = prescriptionList[scope.$index].unitCode_dictText;
adviceQueryParams.adviceTypes = value; // 🎯 修复:改为 adviceTypes复数
// 根据选择的类型设置categoryCode用于药品分类筛选
@@ -756,7 +772,13 @@
</el-table-column>
<el-table-column label="皮试" align="center" prop="" width="80">
<template #default="scope">
<span v-if="!scope.row.isEdit">
<template v-if="scope.row.isEdit">
<el-checkbox v-model="scope.row.skinTestFlag" :true-value="1" :false-value="0"
@change="handleSkinTestChange(scope.row, scope.$index)">
</el-checkbox>
</template>
<span v-else>
{{ scope.row.skinTestFlag_enumText || '-' }}
</span>
</template>
@@ -956,25 +978,38 @@ const { method_code, unit_code, rate_code, distribution_category_code, drord_doc
// drord_doctor_type: 1=西药, 2=中成药, 3=诊疗, 4=耗材, 5=会诊, 6=手术
const adviceTypeList = computed(() => {
// 如果字典已加载,使用字典数据;否则使用默认值
let list = [];
if (drord_doctor_type.value && drord_doctor_type.value.length > 0) {
return drord_doctor_type.value.map(item => ({
label: item.label,
value: parseInt(item.value) || item.value
}));
// 过滤掉字典中已有的"全部"选项,避免重复
list = drord_doctor_type.value
.filter(item => item.label !== '全部')
.map(item => ({
label: item.label,
value: parseInt(item.value) || item.value
}));
} else {
// 默认返回值,确保页面正常显示
list = [
{ label: '西药', value: 1 },
{ label: '中成药', value: 2 },
{ label: '诊疗', value: 3 },
{ label: '耗材', value: 4 },
{ label: '会诊', value: 5 },
{ label: '手术', value: 6 },
];
}
// 默认返回值,确保页面正常显示
return [
{ label: '西药', value: 1 },
{ label: '中成药', value: 2 },
{ label: '诊疗', value: 3 },
{ label: '耗材', value: 4 },
{ label: '会诊', value: 5 },
{ label: '手术', value: 6 },
];
// 在最后添加"全部"选项
list.push({ label: '全部', value: 0 });
return list;
});
// 根据类型值获取显示标签,避免非编辑态出现空标签
const mapAdviceTypeLabel = (type) => {
const mapAdviceTypeLabel = (type, adviceTableName) => {
// 🔧 Bug Fix: 根据adviceTableName判断耗材类型
// 后端adviceType=2既表示中成药又表示耗材需要通过表名区分
if (type === 2 && adviceTableName === 'adm_device_definition') {
return '耗材';
}
const found = adviceTypeList.value.find((item) => item.value === type);
return found ? found.label : '';
};
@@ -1602,11 +1637,8 @@ function getListInfo(addNewRow) {
// 🔧 Bug Fix: 后端保存时将耗材(4)转换为中成药(2),显示时需要转换回来
// 检查 adviceTableName如果是耗材表则应该是耗材类型
const adviceTableName = contentJson?.adviceTableName || item.adviceTableName;
if (adviceType === 2 && adviceTableName === 'adm_device_definition') {
adviceType = 4; // 后端2(中成药) -> 前端4(耗材)
}
let adviceType_dictText = item.adviceType_dictText || mapAdviceTypeLabel(adviceType);
let adviceType_dictText = item.adviceType_dictText || mapAdviceTypeLabel(adviceType, adviceTableName);
// 如果是会诊类型,设置为会诊类型
if (isConsultation) {
@@ -1893,6 +1925,34 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
* 选择药品回调
*/
function selectAdviceBase(key, row) {
// 🔧 Bug Fix: 检查药品是否需要皮试,如果需要则弹出确认框
if (row.skinTestFlag == 1) {
ElMessageBox.confirm(`药品:${row.adviceName}需要做皮试,是否做皮试?`, '提示', {
confirmButtonText: '是',
cancelButtonText: '否',
type: 'warning',
center: true,
customClass: 'skin-test-confirm-dialog',
beforeClose: (action, instance, done) => {
if (action === 'confirm') {
// 用户点击右边的按钮confirm保持皮试标记为1
setNewRow(key, row);
done();
} else if (action === 'cancel') {
// 用户点击左边的按钮cancel将皮试标记改为0
row.skinTestFlag = 0;
row.skinTestFlag_enumText = '否';
setNewRow(key, row);
done();
} else {
done();
}
}
});
return;
}
// 检查检查检验项目是否有历史记录30天内
if (row.categoryCode == 22 || row.categoryCode == 23) {
checkServicesHistory({
patientId: props.patientInfo.patientId,
@@ -1915,6 +1975,7 @@ function selectAdviceBase(key, row) {
}
async function setNewRow(key, row) {
console.log('[BugFix] setNewRow - row.adviceType:', row.adviceType, 'row.adviceType_dictText:', row.adviceType_dictText, 'row.adviceTableName:', row.adviceTableName);
// 每次选择药品时,将当前行数据完全重置,清空所有旧数据
const preservedData = {
uniqueKey: prescriptionList.value[rowIndex.value].uniqueKey,
@@ -1927,26 +1988,28 @@ function selectAdviceBase(key, row) {
prescriptionList.value[rowIndex.value] = preservedData;
setValue(row);
console.log('[BugFix] setNewRow after setValue - prescriptionList[rowIndex].adviceType:', prescriptionList.value[rowIndex.value].adviceType, 'adviceType_dictText:', prescriptionList.value[rowIndex.value].adviceType_dictText);
// 🔧 Bug #220 修复确保在setValue之后重新计算耗材类型的总金额
// 耗材(adviceType=4)和诊疗(adviceType=3)需要重新计算以确保显示正确
const currentRow = prescriptionList.value[rowIndex.value];
if (currentRow && (currentRow.adviceType == 3 || currentRow.adviceType == 4)) {
calculateTotalPrice(currentRow, rowIndex.value);
}
// 确保在setValue之后再次设置showPopover为false防止被覆盖
prescriptionList.value[rowIndex.value].showPopover = false;
expandOrder.value = [key];
// 自动聚焦到单次用量字段 - 使用 async/await 多次尝试
await nextTick();
// 第一次尝试 - 快速定位
await sleep(300);
focusDoseQuantityInput(row);
}
// 聚焦到单次用量输入框的函数
@@ -2343,6 +2406,13 @@ function handleSave(prescriptionId) {
item.accountId = finalAccountId;
}
item.dbOpType = '1';
// 🔧 Bug Fix: 确保耗材的minUnitQuantity被正确设置
if (item.adviceType == 4) {
item.minUnitQuantity = item.quantity;
item.minUnitCode = item.unitCode;
item.minUnitCode_dictText = item.unitCode_dictText;
}
});
loading.value = true;
@@ -2366,20 +2436,25 @@ function handleSave(prescriptionId) {
finalUnitCode = item.minUnitCode;
}
item.minUnitQuantity = finalQuantity;
} else if (item.adviceType == 4) {
// 🔧 Bug Fix: 耗材类型只有一个单位minUnitQuantity等于quantity
item.minUnitQuantity = item.quantity;
// 🔧 Bug Fix: 确保minUnitCode等于unitCode
item.minUnitCode = item.unitCode;
item.minUnitCode_dictText = item.unitCode_dictText;
finalUnitCode = item.unitCode;
} else {
item.minUnitQuantity = item.quantity;
}
// --- 【修改点3Bug 1 修复 (类型转换)】 ---
let saveAdviceType = item.adviceType;
if (item.adviceType == 4) {
saveAdviceType = 2; // 耗材前端4 -> 后端2
} else if (item.adviceType == 2) {
saveAdviceType = 1; // 中成药前端2 -> 后端1
} else if (item.adviceType == 5) {
// 🔧 Bug Fix: 保持原类型,不进行转换
// 前端类型: 1=西药, 2=中成药, 3=诊疗, 4=耗材, 5=会诊, 6=手术
// 后端类型: 1=药品, 2=耗材, 3=医疗活动, 6=手术
// 后端会通过 ItemType.DEVICE.getValue() || adviceType == 4 来识别耗材
if (item.adviceType == 5) {
saveAdviceType = 3; // 会诊前端5 -> 后端3诊疗类
} else if (item.adviceType == 6) {
saveAdviceType = 6; // 🔧 BugFix#318: 手术类型保持为6
}
// 🔧 Bug Fix: Validate and fix NaN values before sending to backend
@@ -2439,6 +2514,8 @@ function handleSave(prescriptionId) {
quantity: finalQuantity,
unitCode: finalUnitCode,
totalPrice: item.totalPrice, // Ensure totalPrice is valid
// 🔧 Bug Fix: 确保 categoryEnum 被传递(耗材必填字段)
categoryEnum: item.categoryEnum || parsedContent.categoryEnum,
// 🔧 Bug Fix: 确保库存匹配成功的关键字段
adviceTableName: adviceTableNameVal,
locationId: locationIdVal,
@@ -2446,6 +2523,13 @@ function handleSave(prescriptionId) {
methodCode: item.methodCode || parsedContent.methodCode,
// 🔧 确保 accountId 被传递(用于预结算)
accountId: item.accountId || parsedContent.accountId,
// 🔧 Bug Fix: 确保minUnitQuantity被传递耗材必填字段
minUnitQuantity: item.minUnitQuantity,
minUnitCode: item.minUnitCode,
minUnitCode_dictText: item.minUnitCode_dictText,
// 🔧 Bug Fix: 确保 definitionId 和 definitionDetailId 被传递(费用项必填字段)
definitionId: item.definitionId || parsedContent.definitionId,
definitionDetailId: item.definitionDetailId || parsedContent.definitionDetailId,
// 🔧 更新 contentJson 中的 adviceType确保后端分类正确
contentJson: JSON.stringify({
...parsedContent,
@@ -2635,12 +2719,17 @@ function handleOrderBindInfo(bindIdInfo, currentMethodCode) {
encounterDiagnosisId: encounterDiagnosisId.value,
// 🔧 确保 adviceType 和显示文本正确设置
adviceType: item.adviceType,
adviceType_dictText: mapAdviceTypeLabel(item.adviceType),
adviceType_dictText: mapAdviceTypeLabel(item.adviceType, item.adviceTableName),
};
// 计算价格和总量
const unitInfo = unitCodeList.value.find((k) => k.value == item.unitCode);
if (unitInfo && unitInfo.type == 'minUnit') {
// 🔧 Bug Fix: 耗材类型只有一个单位minUnitQuantity等于quantity
if (item.adviceType == 4) {
newRow.price = newRow.unitPrice;
newRow.totalPrice = (item.quantity * newRow.unitPrice).toFixed(6);
newRow.minUnitQuantity = item.quantity;
} else if (unitInfo && unitInfo.type == 'minUnit') {
newRow.price = newRow.minUnitPrice;
newRow.totalPrice = (item.quantity * newRow.minUnitPrice).toFixed(6);
newRow.minUnitQuantity = item.quantity;
@@ -2705,6 +2794,12 @@ function handleSaveSign(row, index, prescriptionId) {
return;
}
// 🔧 Bug Fix: 验证医嘱类型不能为"全部"
if (row.adviceType === 0) {
proxy.$modal.msgWarning('请选择医嘱类型');
return;
}
// 重新查找索引,确保使用当前处方列表中的正确索引
const actualIndex = prescriptionList.value.findIndex(item => item.uniqueKey === row.uniqueKey);
if (actualIndex === -1) {
@@ -2774,14 +2869,18 @@ function handleSaveSign(row, index, prescriptionId) {
row.encounterId = props.patientInfo.encounterId;
row.accountId = accountId.value;
// 确保非编辑态显示正确的医嘱类型标签
row.adviceType_dictText = mapAdviceTypeLabel(row.adviceType);
row.adviceType_dictText = mapAdviceTypeLabel(row.adviceType, row.adviceTableName);
// 更新本地显示的最小单位数量(仅用于前端逻辑,不影响显示单位)
if (row.adviceType == 1 || row.adviceType == 2) {
row.minUnitQuantity =
row.minUnitCode == row.unitCode ? row.quantity : row.quantity * row.partPercent;
} else {
// 🔧 Bug Fix: 耗材和其他类型minUnitQuantity等于quantity
row.minUnitQuantity = row.quantity;
// 🔧 Bug Fix: 确保minUnitCode等于unitCode耗材只有一个单位
row.minUnitCode = row.unitCode;
row.minUnitCode_dictText = row.unitCode_dictText;
}
row.conditionId = conditionId.value;
@@ -2976,14 +3075,12 @@ function handleSaveBatch(prescriptionId) {
// --- Bug 1 修复:类型转换 ---
let saveAdviceType = item.adviceType;
if (item.adviceType == 4) {
saveAdviceType = 2; // 耗材前端4 -> 后端2
} else if (item.adviceType == 2) {
saveAdviceType = 1; // 中成药前端2 -> 后端1
} else if (item.adviceType == 5) {
// 🔧 Bug Fix: 保持原类型,不进行转换
// 前端类型: 1=西药, 2=中成药, 3=诊疗, 4=耗材, 5=会诊, 6=手术
// 后端类型: 1=药品, 2=耗材, 3=医疗活动, 6=手术
// 后端会通过 ItemType.DEVICE.getValue() || adviceType == 4 来识别耗材
if (item.adviceType == 5) {
saveAdviceType = 3; // 会诊前端5 -> 后端3诊疗类
} else if (item.adviceType == 6) {
saveAdviceType = 6; // 🔧 BugFix#318: 手术类型保持为6
}
// 🔧 BugFix#318: 过滤掉手术特有字段,只保留标准医嘱字段
@@ -2994,7 +3091,11 @@ function handleSaveBatch(prescriptionId) {
'encounterId', 'groupId', 'injectFlag', 'lotNumber', 'methodCode', 'partPercent',
'patientId', 'positionId', 'positionName', 'prescriptionNo', 'quantity', 'rateCode',
'requestId', 'skinTestFlag', 'sortNumber', 'statusEnum', 'totalPrice',
'unitCode', 'unitPrice', 'volume', 'ybClassEnum'
'unitCode', 'unitPrice', 'volume', 'ybClassEnum',
// 🔧 Bug Fix: 添加 definitionId 和 definitionDetailId 字段
'definitionId', 'definitionDetailId',
// 🔧 Bug Fix: 添加 categoryEnum 字段(耗材必填)
'categoryEnum'
];
let filteredItem = {};
standardItemFields.forEach(field => {
@@ -3014,10 +3115,8 @@ function handleSaveBatch(prescriptionId) {
// 🔧 Bug Fix: 处理accountId如果是'ZIFEI'或0则转为null让后端查询默认账户
let itemAccountId = finalAccountId;
if (itemAccountId === 'ZIFEI' || itemAccountId === 0) {
if (itemAccountId === 'ZIFEI' || itemAccountId === 0 || itemAccountId === '0') {
itemAccountId = null;
} else if (itemAccountId && !isNaN(Number(itemAccountId))) {
itemAccountId = Number(itemAccountId);
}
// 🔧 Bug Fix: 确保库存匹配成功的关键字段
@@ -3064,7 +3163,11 @@ function handleSaveBatch(prescriptionId) {
accountId: itemAccountId,
// 🔧 Bug Fix: 确保库存匹配成功
adviceTableName: adviceTableNameVal,
locationId: locationIdVal
locationId: locationIdVal,
// 🔧 Bug Fix: 确保 minUnitQuantity、minUnitCode 等字段被传递(药品必填字段)
minUnitQuantity: item.minUnitQuantity,
minUnitCode: item.minUnitCode,
minUnitCode_dictText: item.minUnitCode_dictText
};
});
// --- 【修改结束】 ---
@@ -3193,17 +3296,26 @@ function syncGroupFields(row) {
function setValue(row) {
unitCodeList.value = [];
unitCodeList.value.push({ value: row.unitCode, label: row.unitCode_dictText, type: 'unit' });
unitCodeList.value.push({
value: row.doseUnitCode,
label: row.doseUnitCode_dictText,
type: 'dose',
});
// 🔧 Bug Fix: 耗材类型只有一个单位不需要dose和minUnit选项
if (row.adviceType == 4) {
// 耗材只添加一个单位选项
row.minUnitCode = row.unitCode;
row.minUnitCode_dictText = row.unitCode_dictText;
} else {
// 药品类型添加dose和minUnit选项
unitCodeList.value.push({
value: row.doseUnitCode,
label: row.doseUnitCode_dictText,
type: 'dose',
});
unitCodeList.value.push({
value: row.minUnitCode,
label: row.minUnitCode_dictText,
type: 'minUnit',
});
unitCodeList.value.push({
value: row.minUnitCode,
label: row.minUnitCode_dictText,
type: 'minUnit',
});
}
if (row.adviceType == 2 && row.minUnitCode != row.unitCode) {
unitCodeList.value.push({
value: row.minUnitCode,
@@ -3217,10 +3329,13 @@ function setValue(row) {
: 0;
// 创建一个新的对象,而不是合并旧数据,以避免残留数据问题
console.log('[BugFix] setValue - row.adviceType:', row.adviceType, 'row.adviceType_dictText:', row.adviceType_dictText, 'row.adviceTableName:', row.adviceTableName);
prescriptionList.value[rowIndex.value] = {
...JSON.parse(JSON.stringify(row)),
// 确保adviceType为数字类型避免类型不匹配导致的显示问题
adviceType: Number(row.adviceType),
// 🔧 Bug Fix: 确保adviceType_dictText被正确设置避免展开行时显示错误
adviceType_dictText: row.adviceType_dictText || mapAdviceTypeLabel(row.adviceType, row.adviceTableName),
skinTestFlag: skinTestFlag, // 确保皮试字段是数字类型
skinTestFlag_enumText: skinTestFlag == 1 ? '是' : '否', // 更新显示文本
// 保留原来设置的初始状态值
@@ -3229,6 +3344,7 @@ function setValue(row) {
statusEnum: prescriptionList.value[rowIndex.value].statusEnum,
showPopover: false, // 确保查询框关闭
};
console.log('[BugFix] setValue - prescriptionList[rowIndex].adviceType_dictText:', prescriptionList.value[rowIndex.value].adviceType_dictText);
// 🔧 Bug #218 修复保留组套中的值不要强制设为undefined
// 只有当值未定义时才使用默认值
prescriptionList.value[rowIndex.value].orgId = row.positionId || row.orgId;
@@ -3237,8 +3353,15 @@ function setValue(row) {
prescriptionList.value[rowIndex.value].unitCodeList = unitCodeList.value;
prescriptionList.value[rowIndex.value].doseUnitCode = row.doseUnitCode;
prescriptionList.value[rowIndex.value].minUnitCode = row.minUnitCode;
prescriptionList.value[rowIndex.value].unitCode =
row.partAttributeEnum == 1 ? row.minUnitCode : row.unitCode;
// 🔧 Bug Fix: 耗材类型只有一个单位minUnitCode应该等于unitCode
if (Number(row.adviceType) == 4) {
prescriptionList.value[rowIndex.value].minUnitCode = row.unitCode;
prescriptionList.value[rowIndex.value].minUnitCode_dictText = row.unitCode_dictText;
prescriptionList.value[rowIndex.value].unitCode = row.unitCode;
} else {
prescriptionList.value[rowIndex.value].unitCode =
row.partAttributeEnum == 1 ? row.minUnitCode : row.unitCode;
}
prescriptionList.value[rowIndex.value].categoryEnum = row.categoryCode;
prescriptionList.value[rowIndex.value].skinTestFlag = row.skinTestFlag;
prescriptionList.value[rowIndex.value].definitionId = row.chargeItemDefinitionId;
@@ -3307,6 +3430,10 @@ function setValue(row) {
// 🔧 Bug #218 修复保留组套中的quantity如果没有则默认1
prescriptionList.value[rowIndex.value].quantity = row.quantity || 1;
prescriptionList.value[rowIndex.value].totalPrice = validPrice * (row.quantity || 1);
// 🔧 Bug Fix: 设置耗材的minUnitQuantity避免保存时null错误
prescriptionList.value[rowIndex.value].minUnitQuantity = row.quantity || 1;
// 🔧 Bug Fix: 设置耗材的categoryEnum避免数据库约束错误
prescriptionList.value[rowIndex.value].categoryEnum = row.categoryCode || 3; // 默认为3
prescriptionList.value[rowIndex.value].positionName = row.positionName || '';
// 🔧 Bug Fix: 使用 positionId如果为空则使用患者信息中的 orgId
console.log('设置耗材locationId:', {
@@ -3325,6 +3452,10 @@ function setValue(row) {
prescriptionList.value[rowIndex.value].minUnitPrice = 0;
prescriptionList.value[rowIndex.value].quantity = row.quantity || 1;
prescriptionList.value[rowIndex.value].totalPrice = 0;
// 🔧 Bug Fix: 设置耗材的minUnitQuantity避免保存时null错误
prescriptionList.value[rowIndex.value].minUnitQuantity = row.quantity || 1;
// 🔧 Bug Fix: 设置耗材的categoryEnum避免数据库约束错误
prescriptionList.value[rowIndex.value].categoryEnum = row.categoryCode || 3; // 默认为3
prescriptionList.value[rowIndex.value].positionName = row.positionName || '';
const finalLocationId = row.positionId || props.patientInfo.orgId;
prescriptionList.value[rowIndex.value].locationId = finalLocationId;
@@ -3967,6 +4098,16 @@ function convertDoseValues(row, index) {
// 总量计算,仅适用只有两种单位的情况
function calculateTotalAmount(row, index) {
nextTick(() => {
// 🔧 Bug Fix: 处理耗材类型的总金额计算
if (row.adviceType == 4) {
row.totalPrice = (row.quantity * row.unitPrice).toFixed(6);
// 🔧 Bug Fix: 确保耗材的minUnitQuantity和minUnitCode正确设置
row.minUnitQuantity = row.quantity;
row.minUnitCode = row.unitCode;
row.minUnitCode_dictText = row.unitCode_dictText;
return;
}
if (row.adviceType == 2) {
calculateTotalPrice(row, index);
return;
@@ -4424,6 +4565,36 @@ function handleOrderSetSaved() {
defineExpose({ getListInfo, getDiagnosisInfo });
</script>
<style lang="scss">
/* 皮试确认弹窗全局样式 - 反转按钮顺序,左边是是,右边是否 */
.skin-test-confirm-dialog.el-message-box {
.el-message-box__btns {
display: flex !important;
flex-direction: row-reverse !important;
justify-content: center !important;
.el-button {
margin-left: 10px !important;
margin-right: 10px !important;
}
}
}
/* 如果自定义类名不生效,使用更强的选择器 */
.el-message-box.skin-test-confirm-dialog {
.el-message-box__btns {
display: flex !important;
flex-direction: row-reverse !important;
justify-content: center !important;
.el-button {
margin-left: 10px !important;
margin-right: 10px !important;
}
}
}
</style>
<style lang="scss" scoped>
:deep(.el-table__expand-icon) {
display: none !important;