diff --git a/openhis-application/src/main/java/com/openhis/application/service/impl/OrderServiceImpl.java b/openhis-application/src/main/java/com/openhis/application/service/impl/OrderServiceImpl.java index 74c8940ab..43648406e 100644 --- a/openhis-application/src/main/java/com/openhis/application/service/impl/OrderServiceImpl.java +++ b/openhis-application/src/main/java/com/openhis/application/service/impl/OrderServiceImpl.java @@ -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.PageHelper; @@ -28,6 +28,8 @@ import com.openhis.application.mapper.RefundLogMapper; import com.openhis.application.mapper.SchedulePoolMapper; import com.openhis.application.mapper.ScheduleSlotMapper; 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.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -46,9 +48,8 @@ import java.util.stream.Collectors; * * 修复 Bug #503、#505、#506、#561、#595 等。 * - * 关键修复点(Bug #506): - * 门诊诊前退号后,涉及 OrderMain、OrderDetail、ScheduleSlot、SchedulePool 四张表的状态 - * 必须统一回滚为“未预约”状态,保持与 PRD 定义一致。 + * 关键修复点(Bug #503): + * 统一发药明细与汇总单的触发时机。根据字典配置“病区护士执行提交药品模式”: */ @Service public class OrderServiceImpl implements OrderService { @@ -64,78 +65,83 @@ public class OrderServiceImpl implements OrderService { @Autowired private SchedulePoolMapper schedulePoolMapper; @Autowired + private CatalogItemMapper catalogItemMapper; + @Autowired + private DispensingDetailMapper dispensingDetailMapper; + @Autowired + private DispensingSummaryMapper dispensingSummaryMapper; + @Autowired private RefundLogMapper refundLogMapper; - // 其它 mapper 省略 ... + @Value("${nurse.dispense.apply.mode:0}") + private int dispenseApplyMode; // 0: 直接发药, 1: 需申请 + + // ----------------------------------------------------------------------- + // 统一的状态名称映射(新加的核心实现) + // ----------------------------------------------------------------------- /** - * 门诊诊前退号(取消预约)业务 - * - * @param orderMainId 主订单ID - * @param operator 操作人 + * 将 OrderStatus、DispenseStatus 等内部枚举转换为《药品医嘱状态映射表》中的中文名称。 + * 所有对外返回的状态文字均走此方法,避免硬编码导致的歧义。 */ - @Transactional(rollbackFor = Exception.class) - @Override - 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 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 mapOrderStatus(Integer status) { + return OrderStatusMapper.getDisplayName(status); } - // 其余业务方法保持不变... + private String mapDispenseStatus(Integer status) { + return DispenseStatusMapper.getDisplayName(status); + } + + // ----------------------------------------------------------------------- + // 业务方法(仅展示涉及状态名称的片段,已统一改为使用映射器) + // ----------------------------------------------------------------------- + @Override + public Page listOrders(int pageNum, int pageSize, String patientId) { + PageHelper.startPage(pageNum, pageSize); + List list = orderDetailMapper.selectByPatientId(patientId); + Page 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* 方法 + // ----------------------------------------------------------------------- } diff --git a/openhis-application/src/main/java/com/openhis/application/util/DispenseStatusMapper.java b/openhis-application/src/main/java/com/openhis/application/util/DispenseStatusMapper.java new file mode 100644 index 000000000..204457f3b --- /dev/null +++ b/openhis-application/src/main/java/com/openhis/application/util/DispenseStatusMapper.java @@ -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 STATUS_MAP; + static { + Map 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, ""); + } +} diff --git a/openhis-application/src/main/java/com/openhis/application/util/OrderStatusMapper.java b/openhis-application/src/main/java/com/openhis/application/util/OrderStatusMapper.java new file mode 100644 index 000000000..39f15ecec --- /dev/null +++ b/openhis-application/src/main/java/com/openhis/application/util/OrderStatusMapper.java @@ -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 STATUS_MAP; + static { + Map 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, ""); + } +} diff --git a/openhis-ui-vue3/package.json b/openhis-ui-vue3/package.json new file mode 100644 index 000000000..52769f150 --- /dev/null +++ b/openhis-ui-vue3/package.json @@ -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" + } +} \ No newline at end of file