| | |
| | | import javax.validation.constraints.NotEmpty; |
| | | import javax.validation.constraints.NotNull; |
| | | import java.util.Map; |
| | | import java.util.function.Consumer; |
| | | |
| | | import static cn.lihu.jh.framework.common.exception.util.ServiceExceptionUtil.exception; |
| | | import static cn.lihu.jh.framework.common.pojo.CommonResult.success; |
| | | import static cn.lihu.jh.module.ecg.enums.ErrorCodeConstants.APPOINTMENT_CREATE_FAIL; |
| | | import static cn.lihu.jh.module.ecg.enums.ErrorCodeConstants.*; |
| | | |
| | | /** |
| | | * 供第三方(HIS / 集成平台)调用的通用接口。 |
| | | * |
| | | * <h3>路由方式</h3> |
| | | * 通过请求头 {@code action} 区分服务,取值见 {@link ActionTypeEnum}。 |
| | | * |
| | | * <h3>本次改动要点</h3> |
| | | * <ol> |
| | | * <li><b>补注册 ECG 专属 action</b>:接口文档 6.4/6.5 定义 |
| | | * {@code S050402}(ECG 检查预约状态新增)、{@code S050502}(ECG 检查预约状态更新), |
| | | * 与 pacs 的 {@code S050401}/{@code S050501} 并列。实测日志中这两个编号 |
| | | * 尚未出现(0 次),此处**主动兼容**,避免院方切换编号时消息被拒。</li> |
| | | * <li><b>兼容无后缀写法</b>:文档定义 {@code S0201}/{@code S0202}, |
| | | * 而实测报文为 {@code S0201ECG}/{@code S0202ECG},两者均接收。</li> |
| | | * <li><b>细化错误码</b>:原实现把所有异常统一压成 |
| | | * {@code APPOINTMENT_CREATE_FAIL}("申请单创建失败"), |
| | | * 导致排障困难(本次问题排查成本即来自此)。现按 |
| | | * 「未知 action / 报文解析失败 / 记录不存在 / 其他」区分返回。</li> |
| | | * </ol> |
| | | * |
| | | * <h3>关于 S0405</h3> |
| | | * {@code S0405}(申请单状态更新)是**全院通用**服务,医院会把所有检查科室 |
| | | * (CT/MRI/DR/超声…)的状态一并推送。业务层已按心电范围过滤, |
| | | * 非心电单只记日志、不处理(详见 {@code HisEcgFilter})。 |
| | | * |
| | | * @author 心电排队装机系统 |
| | | */ |
| | | @Tag(name = "供第三方调用接口") |
| | | @RestController |
| | | @RequestMapping("/ecg/external") |
| | |
| | | @Slf4j |
| | | public class ExternalController { |
| | | |
| | | /** 请求头中的 action 键名 */ |
| | | private static final String HEADER_ACTION = "action"; |
| | | |
| | | @Resource |
| | | private AppointmentService appointmentService; |
| | | |
| | | /** |
| | | * 通用接口 |
| | | * <p> |
| | | * 支持以下action类型: |
| | | * - S0201ECG: 预约创建 |
| | | * - S0202ECG: 预约更新 |
| | | * 通用接口。 |
| | | * |
| | | * @param dataMap 请求数据 |
| | | * @param headers 请求头,必须包含action字段 |
| | | * <p>支持的 action 见 {@link ActionTypeEnum},包括: |
| | | * 申请单新增/更新、申请单状态更新、检查预约状态新增/更新。 |
| | | * |
| | | * @param dataMap 请求数据(HL7 V3 转换后的 Map) |
| | | * @param headers 请求头,必须包含 {@code action} |
| | | * @return 处理结果 |
| | | */ |
| | | @PermitAll |
| | |
| | | public CommonResult<Boolean> generalInterface( |
| | | @RequestBody @NotNull(message = "请求数据不能为空") Map<String, Object> dataMap, |
| | | @RequestHeader @NotEmpty(message = "请求头不能为空") Map<String, String> headers) { |
| | | log.info("[generalInterface][开始处理请求 action({}) dataMap({})]", headers.get("action"), dataMap); |
| | | |
| | | String actionType = headers.get("action"); |
| | | ActionTypeEnum action = ActionTypeEnum.getByType(actionType); |
| | | String rawAction = headers.get(HEADER_ACTION); |
| | | ActionTypeEnum action = ActionTypeEnum.getByType(rawAction); |
| | | if (action == null) { |
| | | log.warn("[generalInterface][未知的action类型 action({})]", actionType); |
| | | throw exception(APPOINTMENT_CREATE_FAIL); |
| | | // 不再吞成"申请单创建失败",明确告知 action 不受支持 |
| | | log.warn("[generalInterface][不支持的 action({}),请核对接口文档 6.x 交易编号]", rawAction); |
| | | throw exception(HIS_ACTION_NOT_SUPPORTED); |
| | | } |
| | | |
| | | log.info("[generalInterface][开始处理请求 action({}) dataMap({})]", rawAction, dataMap); |
| | | |
| | | try { |
| | | // 使用策略模式处理不同的action |
| | | Map<ActionTypeEnum, Consumer<Map<String, Object>>> actionHandlers = Map.of( |
| | | ActionTypeEnum.S0201ECG, appointmentService::handleAppointmentCreate, |
| | | ActionTypeEnum.S0202ECG, appointmentService::handleAppointmentUpdate, |
| | | ActionTypeEnum.S040501HIS, appointmentService::handleAppointmentStateUpdate, |
| | | ActionTypeEnum.S050401, appointmentService::handleCheckAppointmentUpdate, |
| | | ActionTypeEnum.S0405, appointmentService::handleAppointmentStatusUpdate, |
| | | ActionTypeEnum.S050501, appointmentService::handleCheckAppointmentUpdate |
| | | ); |
| | | |
| | | Consumer<Map<String, Object>> handler = actionHandlers.get(action); |
| | | if (handler == null) { |
| | | throw exception(APPOINTMENT_CREATE_FAIL); |
| | | dispatch(action, dataMap); |
| | | return success(true); |
| | | } catch (cn.lihu.jh.framework.common.exception.ServiceException e) { |
| | | // 业务异常:保留原始错误码与信息,便于 HIS 侧定位 |
| | | log.error("[generalInterface][业务处理失败 action({}) code({}) msg({})]", |
| | | rawAction, e.getCode(), e.getMessage()); |
| | | throw e; |
| | | } catch (Exception e) { |
| | | // 未预期异常:报文解析失败、字段缺失导致的 NPE/类型转换异常等 |
| | | log.error("[generalInterface][报文处理异常 action({}) dataMap({})]", rawAction, dataMap, e); |
| | | throw exception(HIS_MESSAGE_PARSE_FAIL); |
| | | } |
| | | } |
| | | |
| | | handler.accept(dataMap); |
| | | return success(true); |
| | | } catch (Exception e) { |
| | | throw exception(APPOINTMENT_CREATE_FAIL); |
| | | /** |
| | | * 按 action 分派到对应处理器。 |
| | | * <p> |
| | | * 使用 {@link ActionTypeEnum} 的分组方法而非 Map 映射,是为了让 |
| | | * 「同一服务的多种编号」(pacs / ECG、有后缀 / 无后缀)走同一处理器, |
| | | * 避免新增编号时漏配。 |
| | | */ |
| | | private void dispatch(ActionTypeEnum action, Map<String, Object> dataMap) { |
| | | if (action.isApplyCreate()) { |
| | | appointmentService.handleAppointmentCreate(dataMap); |
| | | } else if (action.isApplyUpdate()) { |
| | | appointmentService.handleAppointmentUpdate(dataMap); |
| | | } else if (action.isApplyStatusUpdate()) { |
| | | appointmentService.handleAppointmentStateUpdate(dataMap); |
| | | } else if (action.isAppointmentStatusCreate() |
| | | || action.isAppointmentStatusUpdate()) { |
| | | // 检查预约状态:新增(S050401/S050402) 与 更新(S050501/S050502) |
| | | // 报文结构相同,走同一处理器 |
| | | appointmentService.handleCheckAppointmentUpdate(dataMap); |
| | | } else { |
| | | // 枚举新增但未接入分派逻辑时的兜底,避免静默忽略 |
| | | throw exception(HIS_ACTION_NOT_SUPPORTED); |
| | | } |
| | | } |
| | | } |