药品追溯码(溯源码)采集系统(二):门诊发药后端
门诊发退药追溯码采集系统解析:
一、门诊发退药追溯码数据表
1.1、Wm_ware_dispense_bill表:该表用于存储处方信息
1.2 Wm_ware_dispense_tracecode:追溯码采集表
二、发退药后端代码
后端代码基于Springboot架构和mybatis-plus,先看主要接口信息:
1.1、该接口用于接收处方号、部门id和操作人三个参数,回参是处方单的药品信息
/*** 接口名称: WareDispenseBillController$* 功能描述:用于存储处方单信息,以及查询信息并输出到页面*/@RestController
@RequestMapping("/wareDispenseBill")
public class WareDispenseBillController {@Resourceprivate WmWareDispenseBillService wmWareDispenseBillService;@GetMapping("/split/{Rxno}/{departmentid}/{username}")@Transactional(rollbackFor = Exception.class)public R getWareBaseInfo_tz_v_split(@PathVariable String Rxno,@PathVariable String departmentid,@PathVariable String username) {try {AtomicInteger statusCode = new AtomicInteger(0);List<WareInfoSplit> wareInfo_t = wmWareDispenseBillService.querySplit(Rxno, departmentid, username, statusCode);// 已上传医保或查询失败时,返回空数组而非错误消息if (wareInfo_t == null) {return R.OK(new ArrayList<>());}return R.OK(wareInfo_t);} catch (Exception e) {return R.FAIL("系统错误");}}
}
其中回参WareInfoSplit,用于传递给前端渲染,供发药医师确定发药情况:
- splitFlag代表拆零标志,如果 splitFlag=00 说明该药品是非拆零药品,非拆零药品需要门诊挨个扫码,而拆零药品需要药库提前扫码,储备子码库方表调用
- billid是处方表的主键
- wareid是商品id
- patientName是患者名
- spec是规格
- quantity是发药数量
- manufacturer是生成厂家
- tracecodePrefix是药品追溯码前七位,也被称为标识码
- scanFlag是用于院内制剂,没有追溯码的药品的标识
- enoughFlag是当药品为拆零药品,且储备的剩余子码不足发药数量时,用于前端提示
- splitTracecode是待拆追溯码
- subcodeids是存储拆零子码的codeid
@Data
@AllArgsConstructor
public class WareInfoSplit {private String splitFlag;private Long billid;private Integer wareid;private String patientName;private String wareName;private String spec;private Integer quantity;private String manufacturer;private String tracecodePrefix; //药品标识码,前七位private String scanFlag; //是否需要扫码private String enoughFlag; //库存追溯码数量,是否能覆盖发药需求,00是能覆盖,10是不能覆盖private String splitTracecode; //待拆追溯码private List<Integer> subcodeids; //存储拆零子码的codeid
1.2 上面的querySplit方法如下:
1.2.1 该方法首先需要对接his接口,该接口通过处方号来获取药品基本信息。
@Transactional@Overridepublic List<WareInfoSplit> querySplit(String RecipeNo, String departmentid, String username, AtomicInteger statusCode) {// 1. 调用外部API获取处方数据final String API_URL = "http://ip:post/QueryRxno";//创建一个 JSON 格式的字符串//这里用到了转义字符,把双引号转义为普通的字符String requestJson = String.format("{\"RecipeNo\":\"%s\"}", RecipeNo);try {// 发送API请求RestTemplate restTemplate = new RestTemplate();HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_JSON);//HttpEntity封装请求体和请求头。HttpEntity<String> requestEntity = new HttpEntity<>(requestJson, headers);//返回的ResponseEntity<String>包含响应状态码和响应体ResponseEntity<String> response = restTemplate.exchange(API_URL, HttpMethod.POST, requestEntity, String.class);if (response.getStatusCode() != HttpStatus.OK) {statusCode.set(1); //此时就是没能成功调用接口return null;}//将 HTTP 响应中的 JSON 字符串转换为 Java 的Map<String, Object>对象ObjectMapper objectMapper = new ObjectMapper();Map<String, Object> resultMap = objectMapper.readValue(response.getBody(), new TypeReference<Map<String, Object>>() {});if (!"1".equals(resultMap.get("ReturnCode"))) { //ReturnCode如果不等于1就一定不正常statusCode.set(2);return null;}//ReturnCode等于1,也有可能回传空的列表List<Map<String, Object>> details = (List<Map<String, Object>>) resultMap.get("Details");if (details == null || details.isEmpty()) {statusCode.set(3);return null; //未查询到处方}
医院his接口部分回参如下:
1.2.2 第二段的代码逻辑:
1)医院只会传药品编码,我需要通过维护的药品基础表来查询对应的药品基础信息记录,其中比较重要的是
tracecode_prefix:药品标识码
split_flag:拆零标志
split_ratio:拆零系数,若药品为拆零药品,假设追溯码贴在药盒上,一盒有10支,则拆零系数为10
2)源码:
有些处方号可能之前已经扫过,这里存在两种情况:一是医生开具处方之后,患者没有及时过来拿,而医院没有报道机的情况下,就会在打印处方单的同时扫码;二是发药医师扫过处方单之后,又误扫处方单,此时在处方表内就会有历史记录,需要找到记录扫处方的记录。
// 2. 性能优化 - 批量收集药品编码并查询Set<String> drugCodes = new HashSet<>(); //发药的药品编码List<String> rxNos = new ArrayList<>(); //发药的处方号List<String> rxSerialNos = new ArrayList<>(); //发药的处方明细号for (Map<String, Object> item : details) {drugCodes.add(item.get("ware_code").toString());rxNos.add(item.get("rxno").toString());rxSerialNos.add(item.get("rx_serialno").toString());}// 批量查询药品基础信息// 通过药品编码查询药品信息,并转换为Map<ware_code, PubWareBase>的结构。Map<String, PubWareBase> wareBaseMap = drugCodes.isEmpty() ? new HashMap<>() :pubWareBaseMapper.selectList(new LambdaQueryWrapper<PubWareBase>().in(PubWareBase::getWareCode, drugCodes)).stream().collect(Collectors.toMap(PubWareBase::getWareCode,Function.identity(),(existing, replacement) -> existing));List<WmWareDispenseBill> billsToSave = new ArrayList<>(); //准备插入到WmWareDispenseBill表中的记录List<WareInfoSplit> wareInfoList = new ArrayList<>(); //准备回传的接口数据// 3. 批量查询已存在的处方记录// map对象的键是rxno|rxSerialno的组合字符串,值为对应的处方对象。Map<String, WmWareDispenseBill> existingBillsMap = new HashMap<>(); //记录哪写处方以前创建过if (!rxNos.isEmpty()) { //根据处方号和处方明细号,查询已有的处方记录List<WmWareDispenseBill> existingBills = this.list(new LambdaQueryWrapper<WmWareDispenseBill>().in(WmWareDispenseBill::getRxno, rxNos).in(WmWareDispenseBill::getRxSerialno, rxSerialNos));existingBills.forEach(bill ->existingBillsMap.put(bill.getRxno() + "|" + bill.getRxSerialno(), bill));}
1.2.3 对有扫码记录的处方单,我们区分了以下的几种情况。
(1)如果此处方号是第一次扫描,则在下表内新增处方信息
(2)如果此处方之前扫过,但是并没有扫描对应的追溯码,则处方表内有记录,且status为00,此时记录billid,返回该记录,并在高拍仪屏幕显示
(3)如果 此处方之前扫过,且已经扫过追溯码并提交,则默认为该患者开方当天并未取药,此时应重新扫描追溯码并作废之前已扫追溯码
(4)如果此处方之前扫过,且扫过追溯码,且已提交医保,则不返回任何回参。
for (Map<String, Object> item : details) {WareInfoSplit wareInfo = new WareInfoSplit(); //需上传数据String rxno = item.get("rxno").toString();String rxSerialno = item.get("rx_serialno").toString();String key = rxno + "|" + rxSerialno;String drugCode = item.get("ware_code").toString();PubWareBase pubWareBase = wareBaseMap.get(drugCode);// 跳过无药品信息的记录if (pubWareBase == null) {continue;}// 这个处方记录以前创建过if (existingBillsMap.containsKey(key)) {//获取其对应的处方记录对象WmWareDispenseBill existingBill = existingBillsMap.get(key);// 情况1: status = '00',这个已创建的记录仍是待扫码,if ("00".equals(existingBill.getStatus())) {//新增,现在这种情况,该记录可能是从视图抓取并插入进来的数据,要把他没有但是接口给的数据补充上LambdaUpdateWrapper<WmWareDispenseBill> updateWrapper = new LambdaUpdateWrapper<>();updateWrapper.eq(WmWareDispenseBill::getBillid, existingBill.getBillid()).set(WmWareDispenseBill::getOperator, username).set(WmWareDispenseBill::getOperateTime, new Date());if (!this.update(updateWrapper)) {throw new RuntimeException("更新处方部门信息失败,未找到对应记录或更新失败");}//创建最终回传的一个WareInfo对象wareInfo = createWareInfo_split(item, pubWareBase,departmentid);wareInfo.setBillid(existingBill.getBillid());wareInfoList.add(wareInfo); //添加到最终回传的数据中continue;}// 情况2: status = '10' 且 uploadStatus为null或'00',此时虽然已经扫过码,但是还没有上传医保,仍可以更改if ("10".equals(existingBill.getStatus()) &&(existingBill.getUploadStatus() == null || "00".equals(existingBill.getUploadStatus()))) {//把这条处方记录的状态值、上传状态值、操作员等信息通过billid来更新WmWareDispenseBill updateEntity = new WmWareDispenseBill();updateEntity.setBillid(existingBill.getBillid()); //billid取以前创建的billidupdateEntity.setStatus("00");updateEntity.setUploadStatus("00");updateEntity.setOperator(username);updateEntity.setOperateTime(new Date());//更新处方信息if (!this.updateById(updateEntity)) {throw new RuntimeException("处方单状态重置失败");}wareInfo = createWareInfo_split(item, pubWareBase,departmentid);wareInfo.setBillid(existingBill.getBillid());wareInfoList.add(wareInfo);//添加到最终回传的数据中continue;}// 情况3: status = '10' 且 uploadStatus = '10'if ("10".equals(existingBill.getStatus()) && "10".equals(existingBill.getUploadStatus())) {statusCode.set(4);return null; //此时不可更改}}// 程序运行到这,说明这个处方是新处方,所以需要准备需要新建的处方数据WmWareDispenseBill bill = createNewBill_n(item, wareBaseMap, username, "patientId", departmentid);billsToSave.add(bill); //添加到准备插入处方表的集合中wareInfoList.add(createWareInfo_split(item, pubWareBase,departmentid));}// 程序运行到这,说明这个处方是新处方,所以需要准备需要新建的处方数据WmWareDispenseBill bill = createNewBill_n(item, wareBaseMap, username, "patientId", departmentid);billsToSave.add(bill); //添加到准备插入处方表的集合中wareInfoList.add(createWareInfo_split(item, pubWareBase,departmentid));}// 5. 新处方if (!billsToSave.isEmpty()) { //需要插入新处方if (!this.saveBatch(billsToSave)) { //保存到处方表throw new RuntimeException("保存处方数据失败");}//遍历wareInfoList,为billid为null的记录设置正确的billidupdateWareInfoListWithBillIds_split(billsToSave, wareInfoList);}return wareInfoList;} catch (Exception e) {log.error("处方数据获取失败:", e);return null;}}
这里解释下,情况2: status = '10' 且 uploadStatus为null或'00',此时虽然已经扫过码,但是还没有上传医保,也就是说无论是误扫还是隔夜取药,都认为是这种情况。此时并不在wm_ware_dispense_bill表中插入新的记录,而是沿用之前的记录,并且把状态值复原。
这里用到的updateById是在 MyBatis(以及 MyBatis-Plus)中用于根据实体对象的主键(ID)更新对应的数据记录。
- 它会根据传入实体对象的 主键字段(通常是
id
) 定位到数据库中对应的记录 - 然后将实体对象中非空的字段值更新到数据库表中对应的字段
情况3意味着该条记录已经上传医保,此时不可再重新扫码,因为已经被监管机构记录,因此扫描该处方单时是不会出现药品信息的。
1.2.4 如果wm_ware_dispense_bill表中没有接口回传的处方信息,则会用到下面的创建处方表记录的代码
//处方对象原先不存在时,创建一个新的WmWareDispenseBill对象private WmWareDispenseBill createNewBill_n(Map<String, Object> item, Map<String, PubWareBase> wareBaseMap,String username, String patientid, String departmentid) {WmWareDispenseBill bill = new WmWareDispenseBill();bill.setHospitalid(1L);bill.setDepartmentid(Long.valueOf(departmentid));bill.setBillType(item.get("type").toString());bill.setRxno(item.get("rxno").toString());bill.setRxSerialno(item.get("rx_serialno").toString());bill.setPatientName(item.get("patient_name").toString());String drugCode = item.get("ware_code").toString();PubWareBase pubWareBase = wareBaseMap.get(drugCode);bill.setWareId(pubWareBase != null ? pubWareBase.getWareid() : null);if (pubWareBase != null) {String description = String.format("[%s]%s(%s)/%s/%s/%s",pubWareBase.getWareCode(),pubWareBase.getFormalName(),pubWareBase.getWareName(),pubWareBase.getSpec(),pubWareBase.getUnitName(),pubWareBase.getManufacturer());bill.setDescription(description);}bill.setQuantity(Integer.parseInt(item.get("quantity").toString()));bill.setOperator(username);bill.setOperateTime(new Date());bill.setStatus("00");bill.setPatientid(patientid);try {String dateStr = item.get("rx_date").toString();Date date = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse(dateStr);bill.setRxDate(date);} catch (ParseException e) {System.out.println(e);}bill.setDoctorName(item.get("doctor_name").toString());bill.setSettlementno(item.get("mdtrt_id").toString());return bill;}
1.2.5 拆零子码的分配
扫描处方时,会有些药是拆零药品,发药时按照1支1粒这种样式,此时储备相应的子码就很有必要。
具体拆零子码如何进表以后再讲,这里只要知道一个拆零药品的追溯码会根据其拆零系数,在该表内拆分成相应数量的子码。
有上面的子码,就可以在扫描处方单时,分配需要的子码:
private WareInfoSplit createWareInfo_split(Map<String, Object> item, PubWareBase pubWareBase,String departmentid) {WareInfoSplit wareInfo = new WareInfoSplit();if ("10".equals(pubWareBase.getSplitFlag()) && !"盒".equals(item.get("price_unit").toString())) {int quantitySplit = Integer.parseInt(item.get("quantity").toString());List<PubTracecodeSubcode> lockedSubcodes = pubTracecodeSubcodeMapper.selectList(new LambdaQueryWrapper<PubTracecodeSubcode>().eq(PubTracecodeSubcode::getTracecodePrefix, pubWareBase.getTracecodePrefix()).eq(PubTracecodeSubcode::getStatus, "00").eq(PubTracecodeSubcode::getLockedStatus, "00").eq(PubTracecodeSubcode::getDepartmentid, departmentid).orderByAsc(PubTracecodeSubcode::getCodeid) // 按固定顺序避免死锁.last("LIMIT " + quantitySplit + " FOR UPDATE") // 核心:锁定指定数量记录);// 检查是否锁定到足够数量 ===if (lockedSubcodes.size() < quantitySplit) {wareInfo.setEnoughFlag("10"); // 数量不足}else {// 更新锁定状态 ===List<Integer> codeIdsToLock = lockedSubcodes.stream().map(PubTracecodeSubcode::getCodeid).collect(Collectors.toList());pubTracecodeSubcodeMapper.update(null, new LambdaUpdateWrapper<PubTracecodeSubcode>().in(PubTracecodeSubcode::getCodeid, codeIdsToLock).set(PubTracecodeSubcode::getLockedStatus, "10"));// 设置分配结果wareInfo.setEnoughFlag("00");wareInfo.setSubcodeids(codeIdsToLock);wareInfo.setSplitTracecode(lockedSubcodes.get(0).getTracecode());}}wareInfo.setSplitFlag(pubWareBase.getSplitFlag());wareInfo.setWareid(pubWareBase.getWareid().intValue());wareInfo.setPatientName(item.get("patient_name").toString());wareInfo.setWareName(pubWareBase.getWareName());wareInfo.setSpec(pubWareBase.getSpec());wareInfo.setQuantity(Integer.parseInt(item.get("quantity").toString()));wareInfo.setManufacturer(pubWareBase.getManufacturer());wareInfo.setTracecodePrefix(pubWareBase.getTracecodePrefix());wareInfo.setScanFlag(pubWareBase.getScanFlag());return wareInfo;}