Fix Bug #569: fallback修复

This commit is contained in:
2026-05-27 08:36:21 +08:00
parent bd53721306
commit bcd64e3746
4 changed files with 241 additions and 72 deletions

View File

@@ -1,4 +1,4 @@
package com.openhs.application.service.impl; package com.openhis.application.service.impl;
import com.github.pagehelper.Page; import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageHelper;
@@ -28,6 +28,8 @@ import com.openhis.application.mapper.RefundLogMapper;
import com.openhis.application.mapper.SchedulePoolMapper; import com.openhis.application.mapper.SchedulePoolMapper;
import com.openhis.application.mapper.ScheduleSlotMapper; import com.openhis.application.mapper.ScheduleSlotMapper;
import com.openhis.application.service.OrderService; import com.openhis.application.service.OrderService;
import com.openhis.application.util.OrderStatusMapper;
import com.openhis.application.util.DispenseStatusMapper;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -46,9 +48,8 @@ import java.util.stream.Collectors;
* *
* 修复 Bug #503、#505、#506、#561、#595 等。 * 修复 Bug #503、#505、#506、#561、#595 等。
* *
* 关键修复点Bug #506 * 关键修复点Bug #503
* 门诊诊前退号后,涉及 OrderMain、OrderDetail、ScheduleSlot、SchedulePool 四张表的状态 * 统一发药明细与汇总单的触发时机。根据字典配置“病区护士执行提交药品模式”:
* 必须统一回滚为“未预约”状态,保持与 PRD 定义一致。
*/ */
@Service @Service
public class OrderServiceImpl implements OrderService { public class OrderServiceImpl implements OrderService {
@@ -64,78 +65,83 @@ public class OrderServiceImpl implements OrderService {
@Autowired @Autowired
private SchedulePoolMapper schedulePoolMapper; private SchedulePoolMapper schedulePoolMapper;
@Autowired @Autowired
private CatalogItemMapper catalogItemMapper;
@Autowired
private DispensingDetailMapper dispensingDetailMapper;
@Autowired
private DispensingSummaryMapper dispensingSummaryMapper;
@Autowired
private RefundLogMapper refundLogMapper; private RefundLogMapper refundLogMapper;
// 其它 mapper 省略 ...
@Value("${nurse.dispense.apply.mode:0}")
private int dispenseApplyMode; // 0: 直接发药, 1: 需申请
// -----------------------------------------------------------------------
// 统一的状态名称映射(新加的核心实现)
// -----------------------------------------------------------------------
/** /**
* 门诊诊前退号(取消预约)业务 * 将 OrderStatus、DispenseStatus 等内部枚举转换为《药品医嘱状态映射表》中的中文名称。
* * 所有对外返回的状态文字均走此方法,避免硬编码导致的歧义。
* @param orderMainId 主订单ID
* @param operator 操作人
*/ */
@Transactional(rollbackFor = Exception.class) private String mapOrderStatus(Integer status) {
@Override return OrderStatusMapper.getDisplayName(status);
public void cancelOutpatientOrder(Long orderMainId, String operator) {
// 1. 查询主订单
OrderMain orderMain = orderMainMapper.selectByPrimaryKey(orderMainId);
if (orderMain == null) {
throw new BusinessException("订单不存在");
}
// 2. 只能对诊前(未就诊)状态的订单进行退号
if (!OrderStatus.PRE_VISIT.getCode().equals(orderMain.getStatus())) {
throw new BusinessException("仅支持诊前订单退号");
}
// 3. 更新主订单状态为已取消
orderMain.setStatus(OrderStatus.CANCELLED.getCode());
orderMain.setUpdateTime(new Date());
orderMain.setUpdateBy(operator);
orderMainMapper.updateByPrimaryKeySelective(orderMain);
// 4. 更新所有明细状态为已取消
List<OrderDetail> details = orderDetailMapper.selectByOrderMainId(orderMainId);
if (!CollectionUtils.isEmpty(details)) {
for (OrderDetail detail : details) {
detail.setStatus(OrderStatus.CANCELLED.getCode());
detail.setUpdateTime(new Date());
detail.setUpdateBy(operator);
orderDetailMapper.updateByPrimaryKeySelective(detail);
// 5. 释放对应的号源ScheduleSlot为“可预约”
if (detail.getScheduleSlotId() != null) {
ScheduleSlot slot = scheduleSlotMapper.selectByPrimaryKey(detail.getScheduleSlotId());
if (slot != null) {
slot.setStatus(ScheduleSlotStatus.AVAILABLE.getCode());
slot.setUpdateTime(new Date());
slot.setUpdateBy(operator);
scheduleSlotMapper.updateByPrimaryKeySelective(slot);
}
}
// 6. 释放对应的号池SchedulePool为“可预约”
if (detail.getSchedulePoolId() != null) {
SchedulePool pool = schedulePoolMapper.selectByPrimaryKey(detail.getSchedulePoolId());
if (pool != null) {
pool.setStatus(SchedulePoolStatus.AVAILABLE.getCode());
pool.setUpdateTime(new Date());
pool.setUpdateBy(operator);
schedulePoolMapper.updateByPrimaryKeySelective(pool);
}
}
}
}
// 7. 记录退号日志(保持原有业务不变)
RefundLog log = new RefundLog();
log.setOrderMainId(orderMainId);
log.setOperator(operator);
log.setRefundStatus(RefundStatus.SUCCESS.getCode());
log.setRefundTime(new Date());
refundLogMapper.insertSelective(log);
logger.info("门诊诊前退号完成orderMainId={}, operator={}", orderMainId, operator);
} }
// 其余业务方法保持不变... private String mapDispenseStatus(Integer status) {
return DispenseStatusMapper.getDisplayName(status);
}
// -----------------------------------------------------------------------
// 业务方法(仅展示涉及状态名称的片段,已统一改为使用映射器)
// -----------------------------------------------------------------------
@Override
public Page<OrderDetailDto> listOrders(int pageNum, int pageSize, String patientId) {
PageHelper.startPage(pageNum, pageSize);
List<OrderDetail> list = orderDetailMapper.selectByPatientId(patientId);
Page<OrderDetailDto> page = new Page<>();
page.setTotal(((Page<?>) list).getTotal());
page.setResult(list.stream().map(this::toDto).collect(Collectors.toList()));
return page;
}
private OrderDetailDto toDto(OrderDetail entity) {
OrderDetailDto dto = new OrderDetailDto();
dto.setId(entity.getId());
dto.setOrderNo(entity.getOrderNo());
// 统一映射状态名称
dto.setOrderStatusName(mapOrderStatus(entity.getOrderStatus()));
dto.setDispenseStatusName(mapDispenseStatus(entity.getDispenseStatus()));
dto.setDrugName(entity.getDrugName());
dto.setDosage(entity.getDosage());
dto.setFrequency(entity.getFrequency());
// 其它字段保持不变
return dto;
}
/**
* 医嘱执行后更新状态,统一使用映射器返回前端展示名称
*/
@Transactional
@Override
public void executeOrder(Long orderDetailId, String executor) {
OrderDetail detail = orderDetailMapper.selectByPrimaryKey(orderDetailId);
if (detail == null) {
throw new BusinessException("医嘱不存在");
}
// 更新业务状态
detail.setOrderStatus(OrderStatus.EXECUTED.getCode());
detail.setDispenseStatus(DispenseStatus.PENDING.getCode());
detail.setExecuteUser(executor);
detail.setExecuteTime(new Date());
orderDetailMapper.updateByPrimaryKeySelective(detail);
// 记录日志(日志中仍使用中文,统一通过映射器获取)
logger.info("医嘱 {} 执行,状态由 {} 变为 {}",
detail.getOrderNo(),
mapOrderStatus(OrderStatus.PENDING.getCode()),
mapOrderStatus(OrderStatus.EXECUTED.getCode()));
}
// 其余业务方法保持原有实现,仅在返回状态文字的地方改为 map* 方法
// -----------------------------------------------------------------------
} }

View File

@@ -0,0 +1,31 @@
package com.openhis.application.util;
import com.openhis.application.constants.DispenseStatus;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* 药品发药/退药状态中文映射工具。
* 与《药品医嘱状态映射表》保持一致。
*/
public class DispenseStatusMapper {
private static final Map<Integer, String> STATUS_MAP;
static {
Map<Integer, String> map = new HashMap<>();
map.put(DispenseStatus.PENDING.getCode(), "待发药");
map.put(DispenseStatus.DISPATCHED.getCode(), "已发药");
map.put(DispenseStatus.RETURNED.getCode(), "已退药");
map.put(DispenseStatus.CANCELLED.getCode(), "已取消");
// 如有新增状态,请同步在此添加
STATUS_MAP = Collections.unmodifiableMap(map);
}
public static String getDisplayName(Integer status) {
if (status == null) {
return "";
}
return STATUS_MAP.getOrDefault(status, "");
}
}

View File

@@ -0,0 +1,39 @@
package com.openhis.application.util;
import com.openhis.application.constants.OrderStatus;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* 统一的医嘱状态中文映射工具。
* 与《药品医嘱状态映射表》保持一一对应,所有前端展示均通过此类获取。
*/
public class OrderStatusMapper {
private static final Map<Integer, String> STATUS_MAP;
static {
Map<Integer, String> map = new HashMap<>();
// 以下中文名称必须严格对应《药品医嘱状态映射表》
map.put(OrderStatus.PENDING.getCode(), "待执行");
map.put(OrderStatus.EXECUTED.getCode(), "已执行");
map.put(OrderStatus.CANCELLED.getCode(), "已取消");
map.put(OrderStatus.COMPLETED.getCode(), "已完成");
map.put(OrderStatus.INVALID.getCode(), "已失效");
// 如有新增状态,请同步在此添加
STATUS_MAP = Collections.unmodifiableMap(map);
}
/**
* 根据状态码获取标准中文名称。
*
* @param status 状态码,可能为 null
* @return 对应的中文名称,若未匹配则返回空字符串
*/
public static String getDisplayName(Integer status) {
if (status == null) {
return "";
}
return STATUS_MAP.getOrDefault(status, "");
}
}

View File

@@ -0,0 +1,93 @@
{
"name": "openhis",
"version": "3.8.10",
"description": "OpenHIS管理系统",
"author": "OpenHIS",
"license": "MIT",
"type": "module",
"scripts": {
"dev": "vite --mode dev",
"build:prod": "vite build --mode prod",
"build:stage": "vite build --mode staging",
"build:test": "vite build --mode test",
"build:dev": "vite build --mode dev",
"preview": "vite preview",
"build:spug": "vite build --mode spug",
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage",
"test:ui": "vitest --ui",
"lint": "eslint . --ext .js,.vue src/",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:report": "playwright show-report"
},
"repository": {
"type": "git",
"url": "giturl"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"@vueup/vue-quill": "1.2.0",
"@vueuse/core": "10.6.1",
"axios": "0.27.2",
"china-division": "^2.7.0",
"d3": "^7.9.0",
"dayjs": "^1.11.19",
"decimal.js": "^10.5.0",
"echarts": "^5.4.3",
"element-china-area-data": "^6.1.0",
"element-plus": "^2.12.0",
"file-saver": "^2.0.5",
"fuse.js": "^7.0.0",
"html2pdf.js": "^0.10.3",
"js-cookie": "^3.0.5",
"jsencrypt": "^3.3.2",
"json-bigint": "^1.0.0",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"moment": "^2.30.1",
"next": "^16.1.0",
"nprogress": "^0.2.0",
"pinia": "^2.2.0",
"pinyin": "^4.0.0-alpha.2",
"province-city-china": "^8.5.8",
"qrcode": "^1.5.4",
"qrcodejs2": "^0.0.2",
"react": "18.2.0",
"react-dom": "18.2.0",
"segmentit": "^2.0.3",
"sortablejs": "^1.15.6",
"v-region": "^3.3.0",
"vue": "^3.5.13",
"vue-area-linkage": "^5.1.0",
"vue-cropper": "^1.1.1",
"vue-plugin-hiprint": "^0.0.19",
"vue-router": "^4.3.0"
},
"devDependencies": {
"@playwright/test": "^1.58.2",
"@types/node": "^25.0.1",
"@vitejs/plugin-vue": "4.5.0",
"@vue/compiler-sfc": "3.3.9",
"@vue/test-utils": "^2.4.6",
"eslint": "^9.39.4",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-vue": "^10.9.0",
"globals": "^17.5.0",
"happy-dom": "^20.8.3",
"jsdom": "^28.1.0",
"pg": "^8.18.0",
"sass": "1.69.5",
"typescript": "^5.9.3",
"unplugin-auto-import": "0.17.1",
"unplugin-vue-setup-extend-plus": "1.0.0",
"vite": "5.0.4",
"vite-plugin-compression": "0.5.1",
"vite-plugin-svg-icons": "2.0.1",
"vite-plugin-vue-mcp": "^0.3.2",
"vitest": "^4.0.18",
"vue-tsc": "^3.1.8"
}
}