package jnpf.limsController; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jnpf.exception.DataException; import jnpf.limsEntity.LimsHaocaiChukuEntity; import jnpf.limsEntity.LimsHaocaiChukuShenqingZixiangEntity; import jnpf.limsEntity.LimsHaocaiJiechuEntity; import jnpf.limsEntity.LimsHaocaiRukuBatchParam; import jnpf.limsEntity.LimsHaocaiRukuEntity; import jnpf.limsEntity.LimsHaocaiRukuSubmitResult; import jnpf.limsEntity.LimsHaocaiRukuZixiangEntity; import jnpf.limsEntity.LimsHaocaiYanshouLingquParam; import jnpf.limsEntity.LimsSxtFlowEventParam; import jnpf.limsService.LimsHaocaiAlertService; import jnpf.limsService.LimsHaocaiService; import jnpf.util.R; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.math.BigDecimal; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @Slf4j @Tag(name = "lims耗材接口", description = "耗材出入库等接口(当前:入库提交生成条码子表记录)") @RestController @RequestMapping("/lims/haocai") public class LimsHaocaiController { private static final String BIZ_STATUS_YIXIAOHUI = "yixiaohui"; @Autowired private LimsHaocaiService limsHaocaiService; @Autowired private LimsHaocaiAlertService limsHaocaiAlertService; /** * 耗材入库提交:按主表 f_id 取条码号并生成子表条码记录。 * 不防重,重复提交会以新条码再生成一批。 */ @Operation(summary = "耗材入库提交") @PostMapping("/ruku/submit") public R rukuSubmit(@RequestBody LimsHaocaiRukuEntity formData) { String id = formData.getId(); if (id == null || id.isEmpty()) { return R.error("入库单主键(f_id)不能为空"); } LimsHaocaiRukuEntity ruku = limsHaocaiService.getById(id); if (ruku == null || Integer.valueOf(1).equals(ruku.getDeleteMark())) { return R.error("入库单不存在:" + id); } try { LimsHaocaiRukuSubmitResult result = limsHaocaiService.submitRuku(ruku); return R.success("入库成功", result); } catch (DataException e) { log.warn("耗材入库提交失败:rukuId={}, msg={}", id, e.getMessage()); return R.error(e.getMessage()); } } /** * 耗材入库验收提交:按流程关联验收记录,并回写对应验收申请。 */ @Operation(summary = "耗材入库验收提交") @PostMapping("/ruku/yanshou/submit") public R rukuYanshouSubmit(@RequestBody LimsSxtFlowEventParam param) { if (param == null || param.getFlowId() == null || param.getFlowId().trim().isEmpty()) { return R.error("流程id(flow_id)不能为空"); } if (param.getTaskId() == null || param.getTaskId().trim().isEmpty()) { return R.error("流程任务id(task_id)不能为空"); } try { limsHaocaiService.submitRukuYanshou(param.getFlowId(), param.getTaskId()); return R.success("验收提交成功"); } catch (DataException e) { log.warn("耗材入库验收提交失败:flowId={}, taskId={}, msg={}", param.getFlowId(), param.getTaskId(), e.getMessage()); return R.error(e.getMessage()); } } /** * 耗材入库验收完成:按验收流程状态更新或重置关联验收申请。 */ @Operation(summary = "耗材入库验收完成") @PostMapping("/ruku/yanshou/finish") public R rukuYanshouFinish(@RequestBody LimsSxtFlowEventParam param) { if (param == null || param.getFlowId() == null || param.getFlowId().trim().isEmpty()) { return R.error("流程id(flow_id)不能为空"); } if (param.getTaskId() == null || param.getTaskId().trim().isEmpty()) { return R.error("流程任务id(task_id)不能为空"); } try { limsHaocaiService.finishRukuYanshou(param.getFlowId(), param.getTaskId()); return R.success("验收完成处理成功"); } catch (DataException e) { log.warn("耗材入库验收完成处理失败:flowId={}, taskId={}, msg={}", param.getFlowId(), param.getTaskId(), e.getMessage()); return R.error(e.getMessage()); } } /** * 领取耗材验收申请。仅待验收申请可领取,领取后转为验收中并登记当前实验员与领取时间。 * biz_sign 当前仅接收,不参与业务处理。 */ @Operation(summary = "领取耗材验收申请") @PostMapping("/yanshou/lingqu") public R lingquYanshou(@RequestBody LimsHaocaiYanshouLingquParam param) { if (param == null || param.getShenqingId() == null || param.getShenqingId().trim().isEmpty()) { return R.error("耗材验收申请主键(shenqing_id)不能为空"); } String shenqingId = param.getShenqingId().trim(); try { limsHaocaiService.lingquYanshouShenqing(shenqingId); return R.success("领取成功"); } catch (DataException e) { log.warn("领取耗材验收申请失败:shenqingId={}, msg={}", shenqingId, e.getMessage()); return R.error(e.getMessage()); } } /** * 耗材借出提交:不写借出单,只按条码更新入库批次状态并写日志。 */ @Operation(summary = "耗材借出提交") @PostMapping("/jiechu/submit") public R jiechuSubmit(@RequestBody LimsHaocaiJiechuEntity formData) { try { limsHaocaiService.submitJiechu(formData); return R.success("借出成功"); } catch (DataException e) { String tiaomaBianma = formData == null ? null : formData.getTiaomaBianma(); log.warn("耗材借出提交失败:tiaomaBianma={}, msg={}", tiaomaBianma, e.getMessage()); return R.error(e.getMessage()); } } /** * 耗材归还提交:按条码更新借出记录和入库批次状态,并写日志。 */ @Operation(summary = "耗材归还提交") @PostMapping("/guihuan/submit") public R guihuanSubmit(@RequestBody LimsHaocaiJiechuEntity formData) { try { limsHaocaiService.submitGuihuan(formData); return R.success("归还成功"); } catch (DataException e) { String tiaomaBianma = formData == null ? null : formData.getTiaomaBianma(); log.warn("耗材归还提交失败:tiaomaBianma={}, msg={}", tiaomaBianma, e.getMessage()); return R.error(e.getMessage()); } } /** * 耗材出库提交:不写出库单,只按子表条码扣减库存、更新申领子项状态并写日志。 */ @Operation(summary = "耗材出库提交") @PostMapping("/chuku/submit") public R chukuSubmit(@RequestBody LimsHaocaiChukuEntity formData) { try { limsHaocaiService.submitChuku(formData); return R.success("出库成功"); } catch (DataException e) { String shenlingDanhao = formData == null ? null : formData.getShenlingDanhao(); log.warn("耗材出库提交失败:shenlingDanhao={}, msg={}", shenlingDanhao, e.getMessage()); return R.error(e.getMessage()); } } /** * 耗材销毁提交:按销毁单号销毁子表条码对应的入库批次。 */ @Operation(summary = "耗材销毁提交") @PostMapping("/xiaohui/submit") public R xiaohuiSubmit(@RequestBody(required = false) Map formData, @RequestParam(value = "xiaohui_danhao", required = false) String xiaohuiDanhao) { String danhao = resolveXiaohuiDanhao(formData, xiaohuiDanhao); try { limsHaocaiService.submitXiaohui(danhao); return R.success("销毁成功"); } catch (DataException e) { log.warn("耗材销毁提交失败:xiaohuiDanhao={}, msg={}", danhao, e.getMessage()); return R.error(e.getMessage()); } } /** * 耗材销毁申请发起:按流程参数将销毁申请子项条码对应的入库子项标记为待销毁。 */ @Operation(summary = "耗材销毁申请发起") @PostMapping("/xiaohui/start") public R> xiaohuiStart(@RequestBody(required = false) String formData, @RequestParam(value = "flow_id", required = false) String flowId, @RequestParam(value = "task_id", required = false) String taskId) { String resolvedFlowId = resolveParam(formData, flowId, "flow_id", "flowId"); String resolvedTaskId = resolveParam(formData, taskId, "task_id", "taskId"); try { int zixiangCount = limsHaocaiService.startXiaohuiShenqing(resolvedFlowId, resolvedTaskId); Map data = new java.util.HashMap<>(3); data.put("flow_id", resolvedFlowId); data.put("task_id", resolvedTaskId); data.put("zixiang_count", zixiangCount); return R.success("销毁申请发起成功", data); } catch (DataException e) { log.warn("耗材销毁申请发起失败:flowId={}, taskId={}, msg={}", resolvedFlowId, resolvedTaskId, e.getMessage()); return R.error(e.getMessage()); } } /** * 批量转待销毁:按入库主表 f_id 批量将剩余量大于 0 的入库子项标记为待销毁。 */ @Operation(summary = "耗材入库批量转待销毁") @PostMapping("/ruku/batch-to-xiaohui") public R> batchToXiaohui(@RequestBody LimsHaocaiRukuBatchParam param) { try { int zixiangCount = limsHaocaiService.batchMarkRukuZixiangDaixiaohui( param == null ? null : param.getRukuIds()); Map data = new java.util.HashMap<>(2); data.put("ruku_ids", param == null ? null : param.getRukuIds()); data.put("zixiang_count", zixiangCount); return R.success("批量转待销毁成功", data); } catch (DataException e) { log.warn("耗材入库批量转待销毁失败:rukuIds={}, msg={}", param == null ? null : param.getRukuIds(), e.getMessage()); return R.error(e.getMessage()); } } /** * 按条码编码查询耗材入库子项。 */ @Operation(summary = "按条码编码查询耗材入库子项") @GetMapping("/ruku-zixiang/by-tiaoma") public R getRukuZixiangByTiaomaBianma( @RequestParam("tiaomaBianma") String tiaomaBianma, @RequestParam(value = "shifou_guihuan", defaultValue = "no") String shifouGuihuan) { if (tiaomaBianma == null || tiaomaBianma.trim().isEmpty()) { return R.error("条码编码不能为空"); } java.util.List list = limsHaocaiService.listRukuZixiangByTiaomaBianma(tiaomaBianma); if (list == null || list.isEmpty()) { return R.error("未找到对应的耗材入库子项:" + tiaomaBianma); } LimsHaocaiRukuZixiangEntity entity = list.get(0); if (isYes(shifouGuihuan)) { return R.success(entity); } if (BIZ_STATUS_YIXIAOHUI.equalsIgnoreCase(trimToEmpty(entity.getBizStatus()))) { return R.error("该耗材已销毁,无法使用:" + tiaomaBianma); } BigDecimal shengyuliang = parseShengyuliang(entity.getShengyuliang()); if (shengyuliang == null) { return R.error("耗材剩余量无效:" + entity.getShengyuliang()); } if (shengyuliang.compareTo(BigDecimal.ZERO) <= 0) { return R.error("该耗材剩余量不足,无法使用:" + tiaomaBianma); } return R.success(entity); } /** * 根据申领单号和条码编码查询可出库的耗材申领子项。 */ @Operation(summary = "根据申领单号和条码编码查询耗材申领子项") @GetMapping("/chuku-shenqing-zixiang/by-shenling-tiaoma") public R getChukuShenqingZixiangByShenlingAndTiaoma( @RequestParam(value = "shenlingDanhao", required = false) String shenlingDanhao, @RequestParam(value = "shenling_danhao", required = false) String shenlingDanhaoAlias, @RequestParam("tiaomaBianma") String tiaomaBianma) { String danhao = trimToEmpty(shenlingDanhao).isEmpty() ? shenlingDanhaoAlias : shenlingDanhao; try { return R.success(limsHaocaiService.getChukuShenqingZixiangForChuku(danhao, tiaomaBianma)); } catch (DataException e) { log.warn("按申领单号和条码查询耗材申领子项失败:shenlingDanhao={}, tiaomaBianma={}, msg={}", danhao, tiaomaBianma, e.getMessage()); return R.error(e.getMessage()); } } private String trimToEmpty(String value) { return value == null ? "" : value.trim(); } private boolean isYes(String value) { return "yes".equalsIgnoreCase(trimToEmpty(value)); } private BigDecimal parseShengyuliang(String shengyuliang) { if (shengyuliang == null || shengyuliang.trim().isEmpty()) { return null; } try { return new BigDecimal(shengyuliang.trim()); } catch (NumberFormatException e) { return null; } } private String resolveXiaohuiDanhao(Map formData, String xiaohuiDanhao) { if (xiaohuiDanhao != null && !xiaohuiDanhao.trim().isEmpty()) { return xiaohuiDanhao; } if (formData == null) { return null; } Object value = formData.get("xiaohui_danhao"); return value == null ? null : String.valueOf(value); } private String resolveParam(String formData, String requestParam, String... bodyKeys) { if (requestParam != null && !requestParam.trim().isEmpty()) { return requestParam.trim(); } String body = trimToEmpty(formData); if (body.isEmpty()) { return null; } for (String bodyKey : bodyKeys) { Matcher matcher = Pattern.compile("[\"']?" + Pattern.quote(bodyKey) + "[\"']?\\s*[:=]\\s*[\"']?([^\"',}\\s]+)") .matcher(body); if (matcher.find()) { return matcher.group(1).trim(); } } return null; } /** * 手动触发耗材缺货预警扫描(开发 / 运维补发场景)。 * 正常生产由 cron 每天 01:00 自动触发。 * * @param dry true 仅日志、不实际发消息 * @return 统计 {hit, sent, recipients} */ @Operation(summary = "手动触发耗材缺货预警扫描") @PostMapping("/alert/run-now") public R> alertRunNow( @RequestParam(value = "dry", defaultValue = "true") boolean dry) { return R.success(limsHaocaiAlertService.scanAndAlert(dry)); } }