????????門診發退藥追溯碼采集系統解析:
一、門診發退藥追溯碼數據表
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;}