yxh
昨天 fce96ef468291fb9a0e6a4d34ab371315e9485d4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
package cn.lihu.jh.module.ecg.service.queue;
 
import cn.lihu.jh.framework.common.exception.ErrorCode;
import cn.lihu.jh.framework.common.exception.enums.GlobalErrorCodeConstants;
import cn.lihu.jh.framework.common.util.object.BeanUtils;
import cn.lihu.jh.module.ecg.Utils;
import cn.lihu.jh.module.ecg.controller.admin.room.vo.MonitorInfoVO;
import cn.lihu.jh.module.ecg.dal.dataobject.checktype.CheckTypeDO;
import cn.lihu.jh.module.ecg.dal.dataobject.queue.QueueDO;
import cn.lihu.jh.module.ecg.dal.dataobject.room.RoomDO;
import cn.lihu.jh.module.ecg.dal.mysql.call.CallMapper;
import cn.lihu.jh.module.ecg.dal.mysql.checktype.CheckTypeMapper;
import cn.lihu.jh.module.ecg.dal.mysql.devrent.DevRentMapper;
import cn.lihu.jh.module.ecg.dal.mysql.queue.QueueMapper;
import cn.lihu.jh.module.ecg.dal.mysql.room.RoomMapper;
import cn.lihu.jh.module.ecg.enums.BedOpTypeEnum;
import cn.lihu.jh.module.ecg.enums.BedStatusEnum;
import cn.lihu.jh.module.ecg.enums.QueueStatusEnum;
import cn.lihu.jh.module.infra.api.config.ConfigApi;
import cn.lihu.jh.module.system.api.oauth2.OAuth2TokenApi;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
 
import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
 
import static cn.lihu.jh.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.lihu.jh.framework.common.pojo.CommonResult.error;
import static cn.lihu.jh.module.ecg.enums.ErrorCodeConstants.*;
 
/**
 * 排队 Service 数据库事务相关的方法
 *
 * @author 芋道源码
 */
@Component
@Validated
@Slf4j
public class QueueServiceTxFunctions {
 
    @Resource
    private ConfigApi configApi;
 
    @Resource
    private OAuth2TokenApi oAuth2TokenApi;
 
    @Resource
    private QueueMapper queueMapper;
 
    @Resource
    private RoomMapper roomMapper;
 
    @Resource
    private CallMapper callMapper;
 
    @Resource
    private DevRentMapper devRentMapper;
 
    @Resource
    private CheckTypeMapper checkTypeMapper;
 
 
    AtomicInteger openingFlag = new AtomicInteger(0);
    AtomicInteger curSeqNum = new AtomicInteger(0);
 
    // 维护 开放的工位
    ConcurrentHashMap<String, BedBO> mapOpeningBed = new ConcurrentHashMap<>();
 
    // 装机工位..不进入分流队列
    ConcurrentHashMap<Integer, PriorityBlockingQueue<BedBO>> mapCheckTypeRoutingQueue = new ConcurrentHashMap<>();
    
    Map<Integer, CheckTypeDO> mapCheckTypeVsReadyMax = null;
 
    /**
     * 已关闭 或者 关闭中,可以开通工位
     * @param roomId
     * @param bedNo
     * @return
     */
    public ErrorCode bedOpen(Long roomId, String roomName, String bedNo) {
        BedBO bedBO2 = mapOpeningBed.get( Utils.formatRoomBed(roomId, bedNo) );
        if (null != bedBO2) {
            log.error("bedOpen mapBedVsQueue has existed. " + roomId + " " + bedNo);
            return QUEUE_BED_EXIST;
        }
 
        // DB update
        List statusList = new ArrayList<BedStatusEnum>();
        statusList.add(BedStatusEnum.CLOSED);
        Integer updateNum = roomMapper.setBedOpeningOpening(roomId, bedNo, BedStatusEnum.OPENING, statusList);
        if ( null==updateNum || 0 == updateNum ) {
            log.error("bedOpen DB invalid status. " + roomId + " " + bedNo);
            return ROOM_INVALID_STATUS;
        }
 
        RoomDO roomDO = roomMapper.getRoom(roomId, bedNo);
 
        // 新增..工位队列
        BedBO bedBO = new BedBO();
        bedBO.setRoomId(roomId);
        bedBO.setRoomName(roomName);
        bedBO.setBedNo(bedNo);
        bedBO.setStatus(BedStatusEnum.OPENING.getStatus());
        bedBO.setCheckTypes( roomDO.getCheckTypes() );
        bedBO.setOpType( roomDO.getOpType() );
        mapOpeningBed.put( Utils.formatRoomBed(roomId, bedNo), bedBO);
 
        return GlobalErrorCodeConstants.SUCCESS;
    }
 
    public ErrorCode bedClose(Long roomId, String bedNo) {
        BedBO bedBO = mapOpeningBed.get( Utils.formatRoomBed(roomId, bedNo) );
        if (null == bedBO) {
            log.error("bedClose mapBedVsQueue DONOT existed. " + roomId + " " + bedNo);
            return QUEUE_BED_NOT_EXIST;
        }
 
        // 检查是否还有患者需要处理
        List<Integer> queueStatusList = new ArrayList<>();
        queueStatusList.add(QueueStatusEnum.READY.getStatus());
        queueStatusList.add(QueueStatusEnum.ONSTAGE.getStatus());
        queueStatusList.add(QueueStatusEnum.INSTALLING.getStatus());
        queueStatusList.add(QueueStatusEnum.PASSED.getStatus());
        queueStatusList.add(QueueStatusEnum.PASSED_INSTALL.getStatus());
        queueStatusList.add(QueueStatusEnum.RECALLED.getStatus());
        queueStatusList.add(QueueStatusEnum.RECALLED_INSTALL.getStatus());
        queueStatusList.add(QueueStatusEnum.AFFINITY.getStatus());
        queueStatusList.add(QueueStatusEnum.AFFINITY_INSTALL.getStatus());
        List<QueueDO>  queueDOList = queueMapper.getBedQueueByStatus(roomId, bedNo, queueStatusList);
        if (queueDOList.size() > 0)
            return QUEUE_HAVE_PATIENT;
 
        // DB update
        List statusList = new ArrayList<BedStatusEnum>();
        statusList.add(BedStatusEnum.OPENING);
        statusList.add(BedStatusEnum.DOCTOR_ON);
        statusList.add(BedStatusEnum.DOCTOR_PAUSE);
        Integer updateNum = roomMapper.setBedOpeningClosed(roomId, bedNo,
                BedStatusEnum.CLOSED, statusList);
        if ( null==updateNum || 0 == updateNum ) {
            log.error("bedClose DB invalid status. " + roomId + " " + bedNo);
            return ROOM_INVALID_STATUS;
        }
 
        mapOpeningBed.remove( Utils.formatRoomBed(roomId, bedNo) );
        removeBedFromRoutingQueue(bedBO);
        return GlobalErrorCodeConstants.SUCCESS;
    }
 
    public ErrorCode bedDoctorPause(Long roomId, String bedNo, Long docId, String docName) {
        BedBO bedBO = mapOpeningBed.get( Utils.formatRoomBed(roomId, bedNo) );
        if (null == bedBO) {
            log.error("bedDoctorPause mapBedVsQueue DONOT existed. " + roomId + " " + bedNo);
            return QUEUE_BED_NOT_EXIST;
        }
 
        // DB update
        List statusList = new ArrayList<BedStatusEnum>();
        statusList.add(BedStatusEnum.DOCTOR_ON);
        Integer updateNum = roomMapper.setBedDoctorStatus(roomId, bedNo, docId, BedStatusEnum.DOCTOR_PAUSE, statusList);
        if ( null==updateNum || 0 == updateNum ) {
            log.error("bedDoctorPause DB invalid status. " + roomId + " " + bedNo);
            return ROOM_INVALID_STATUS;
        }
 
        bedBO.setStatus(BedStatusEnum.DOCTOR_PAUSE.getStatus());
        removeBedFromRoutingQueue(bedBO);
        return GlobalErrorCodeConstants.SUCCESS;
    }
 
    public ErrorCode bedDoctorResume(Long roomId, String bedNo, Long docId, String docName) {
        BedBO bedBO = mapOpeningBed.get( Utils.formatRoomBed(roomId, bedNo) );
        if (null == bedBO || !bedBO.getStatus().equals(BedStatusEnum.DOCTOR_PAUSE.getStatus())) {
            log.error("bedDoctorResume mapBedVsQueue DONOT existed OR NOT Paused. " + roomId + " " + bedNo);
            return QUEUE_BED_NOT_EXIST;
        }
 
        // DB update
        List statusList = new ArrayList<BedStatusEnum>();
        statusList.add(BedStatusEnum.DOCTOR_PAUSE);
        Integer updateNum = roomMapper.setBedDoctorStatus(roomId, bedNo, docId,
                BedStatusEnum.DOCTOR_ON, statusList);
        if ( null==updateNum || 0 == updateNum ) {
            log.error("bedDoctorResume DB invalid status. " + roomId + " " + bedNo);
            return ROOM_INVALID_STATUS;
        }
 
        bedBO.setStatus(BedStatusEnum.DOCTOR_ON.getStatus());
        addBedToRoutingQueue(bedBO);
        return GlobalErrorCodeConstants.SUCCESS;
    }
 
    // ==================== 需求3:停止候诊开关 ====================
 
    /**
     * 设置「停止候诊」开关。
     * <p>
     * {@code stop=true}:该工位**不再新增**候诊患者(移出分流队列),
     * 但**已有候诊患者仍可正常叫号**({@code nextPatient} 不检查此开关)。
     * <p>
     * {@code stop=false}:恢复正常,重新加入分流队列开始接收患者
     * (前提是工位状态为"医生在座",与 {@code bedReload} 的判定一致)。
     * <p>
     * 状态写入 {@code clinic_room.stop_ready},以在 {@code bedReload} 后保持。
     *
     * @return 成功返回 SUCCESS;工位不存在返回 {@link #QUEUE_BED_NOT_EXIST}
     */
    public ErrorCode setStopReady(Long roomId, String bedNo, boolean stop) {
        BedBO bedBO = mapOpeningBed.get( Utils.formatRoomBed(roomId, bedNo) );
        if (null == bedBO) {
            log.error("[setStopReady] 工位不存在. roomId={} bedNo={}", roomId, bedNo);
            return QUEUE_BED_NOT_EXIST;
        }
 
        // 先落库(失败则不改内存,避免状态不一致)
        //
        // 【部署保护】若数据库尚未执行升级脚本(缺 clinic_room.stop_ready 列),
        // roomMapper.setBedStopReady(...) 会在 Mapper 层直接抛 SQL 异常
        // (@Update 注解方法没有返回值可判断,异常直接冒泡)。
        // 这里显式捕获并转成明确的业务错误码,避免把原始 SQL 错误暴露给前端,
        // 便于现场一眼看出是"部署遗漏"而不是"功能坏了"。
        Integer updateNum;
        try {
            updateNum = roomMapper.setBedStopReady(roomId, bedNo, stop ? 1 : 0);
        } catch (Exception e) {
            log.error("[setStopReady] 写库失败,请确认已执行 _prep/11_需求升级_汇总SQL.sql"
                    + "(clinic_room.stop_ready). roomId={} bedNo={}", roomId, bedNo, e);
            return QUEUE_STOP_READY_NOT_CONFIGURED;
        }
        if (null == updateNum || 0 == updateNum) {
            // 工位可能尚未在 clinic_room 登记,仅告警但仍允许改内存,
            // 以免因 DB 记录缺失导致医生完全无法使用该功能。
            log.warn("[setStopReady] DB 未更新到行,仅更新内存. roomId={} bedNo={} stop={}", roomId, bedNo, stop);
        }
 
        bedBO.setStopReady(stop);
 
        // 同步分流队列:停止 → 移出;恢复 → 加入(仅在医生在座时)
        if (stop) {
            removeBedFromRoutingQueue(bedBO);
        } else if (BedStatusEnum.DOCTOR_ON.getStatus().equals(bedBO.getStatus())) {
            addBedToRoutingQueue(bedBO);
        }
        log.info("[setStopReady] roomId={} bedNo={} stopReady={}", roomId, bedNo, stop);
        return GlobalErrorCodeConstants.SUCCESS;
    }
 
    /** 查询当前「停止候诊」状态(工位不存在返回 null) */
    public Boolean getStopReady(Long roomId, String bedNo) {
        BedBO bedBO = mapOpeningBed.get( Utils.formatRoomBed(roomId, bedNo) );
        return null == bedBO ? null : Boolean.TRUE.equals(bedBO.getStopReady());
    }
 
    public ErrorCode bedDoctorOn(Long roomId, String bedNo, Long docId, String docName) {
        BedBO bedBO = mapOpeningBed.get( Utils.formatRoomBed(roomId, bedNo) );
        if (null == bedBO) {
            log.error("bedDoctorOn mapBedVsQueue DONOT existed. " + roomId + " " + bedNo);
            return QUEUE_BED_NOT_EXIST;
        }
 
        // DB update
        List statusList = new ArrayList<BedStatusEnum>();
        statusList.add(BedStatusEnum.OPENING);
        Integer updateNum = roomMapper.setBedDoctorOn(roomId, bedNo, docId, docName, BedStatusEnum.DOCTOR_ON, statusList);
        if ( null==updateNum || 0 == updateNum ) {
            log.error("bedDoctorOn DB invalid status. " + roomId + " " + bedNo);
            return ROOM_INVALID_STATUS;
        }
 
        bedBO.setStatus(BedStatusEnum.DOCTOR_ON.getStatus());
        addBedToRoutingQueue(bedBO);
        return GlobalErrorCodeConstants.SUCCESS;
    }
 
    public ErrorCode bedDoctorOff(Long roomId, String bedNo, Long docId, String docName) {
        BedBO bedBO = mapOpeningBed.get( Utils.formatRoomBed( roomId, bedNo ) );
        if (null == bedBO) {
            log.error("bedDoctorOff mapBedVsQueue DONOT existed. " + roomId + " " + bedNo);
            return QUEUE_BED_NOT_EXIST;
        }
 
        // DB update
        List statusList = new ArrayList<BedStatusEnum>();
        statusList.add(BedStatusEnum.DOCTOR_ON);
        statusList.add(BedStatusEnum.DOCTOR_PAUSE);
        Integer updateNum = roomMapper.setBedDoctorOff(roomId, bedNo, docId, BedStatusEnum.OPENING, statusList);
        if ( null==updateNum || 0 == updateNum ) {
            log.error("bedDoctorOff DB invalid status. " + roomId + " " + bedNo);
            return ROOM_INVALID_STATUS;
        }
 
        removeBedFromRoutingQueue(bedBO);
 
        bedBO.setStatus(BedStatusEnum.OPENING.getStatus());
        return GlobalErrorCodeConstants.SUCCESS;
    }
 
    /* 2024.11.11
    // 常规 或者 领用 时 医生 取下一位患者
    public ErrorCode nextPatient(Long roomId, String bedNo) {
        // 从 DB 把该工位.序号最小的 [候诊中中] 的人 设置为 [就诊中] (或领用中)
        Integer updateNum = queueMapper.updateBedQueueStatus(roomId, bedNo,
                QueueStatusEnum.READY.getStatus(), QueueStatusEnum.ONSTAGE.getStatus());
        // 该工位 没有 [候诊中中] 人员
        if (null == updateNum || 0 == updateNum) {
            return QUEUE_NOT_READY_PATIENT;
        }
 
        // 分流队列中 该工位 候诊中中人的数量 减一
        BedQueueBO bo = mapBedVsQueue.get( Utils.formatRoomBed( roomId, bedNo ) );
        bo.queueNum.getAndDecrement(); // 可能已经【并发的】在 hurry-up 中改变了值
 
        refreshRoutingQueue(bo);
        return GlobalErrorCodeConstants.SUCCESS;
    }
    */
 
    // 常规 或者 领用 时 医生 取下一位患者
    public ErrorCode nextPatient(Long roomId, String bedNo) {
        QueueDO firstReadyQueueItem = queueMapper.getBedFirstQueueItemByStatus(roomId, bedNo, QueueStatusEnum.READY.getStatus());
        // 该工位 没有 [候诊中中] 人员
        if (null == firstReadyQueueItem) {
            return QUEUE_NOT_READY_PATIENT;
        }
 
        // QueueStatusEnum.READY  =>  QueueStatusEnum.ONSTAGE
        firstReadyQueueItem.setStatus(QueueStatusEnum.ONSTAGE.getStatus());
        queueMapper.updateById( firstReadyQueueItem );
 
        // 分流队列中 该工位 【候诊中】患者数量 减一
        BedBO bo = mapOpeningBed.get( Utils.formatRoomBed( roomId, bedNo ) );
        bo.readyPatNum--; // 可能已经在 hurry-up 中改变了值
 
        refreshBedPriorityLevelInRoutingQueue(bo);
        return GlobalErrorCodeConstants.SUCCESS;
    }
 
    // 装机时 医生 取下一位患者
    public ErrorCode nextInstallPatient(Long roomId, String bedNo) {
        // 从 DB 把 序号最小的 [已领用] [已召回-安装] 的人 设置为 [安装中]
        QueueDO firstItem = queueMapper.getRoomFirstQueueItemByStatus(roomId, List.of(QueueStatusEnum.RECEIVED.getStatus(), QueueStatusEnum.RECALLED_INSTALL.getStatus()));
        // 该工位 没有 [已领用] | [已召回-安装] 人员
        if (null == firstItem) {
            return QUEUE_NOT_READY_PATIENT;
        }
 
        firstItem.setStatus( QueueStatusEnum.INSTALLING.getStatus() );
        firstItem.setBedNo( bedNo );  // 转到 安装工位
        queueMapper.updateById( firstItem );
 
        // 检查项目.亲和性 处理逻辑:  变为【安装中】后 拉 其他检查项来亲和
        CheckTypeDO checkTypeDO = getCheckTypeItem( firstItem.getBookCheckType() );
        if (checkTypeDO.getAffinityCheckTypes().length > 0) {
            procAffinityWhenSomeOneEntryInstalling(firstItem.getPatId(), roomId, bedNo, checkTypeDO.getAffinityCheckTypes() );
        }
 
        // 装机工位 不参与分流
 
        return GlobalErrorCodeConstants.SUCCESS;
    }
 
    /**
     * 1. 每天开诊前 从DB同步工位的患者队列数据到 工位分流队列
     * 2. 服务运维重启时
     */
    public ErrorCode bedReload() {
        mapCheckTypeRoutingQueue.clear();
        mapOpeningBed.clear();
 
        // 清除非当天的排队人员
        queueMapper.clearQueue();
        // 清除非当天的语音叫号记录
        callMapper.clearCall();
 
        // 从DB 获取 工位列表
        List<BedStatusEnum> bedStatusEnumList = new ArrayList<BedStatusEnum>();
        bedStatusEnumList.add(BedStatusEnum.OPENING);
        bedStatusEnumList.add(BedStatusEnum.DOCTOR_ON);
        bedStatusEnumList.add(BedStatusEnum.DOCTOR_PAUSE);
        List<RoomDO> roomDOList = roomMapper.simpleRoomList(bedStatusEnumList);
        List<BedBO> bedBOList = roomDOList.stream().map(item -> BeanUtils.toBean(item, BedBO.class)).toList();
 
        bedBOList.forEach(item -> {
            mapOpeningBed.put( Utils.formatRoomBed(item.roomId, item.bedNo), item);
            // 【需求3】「停止候诊」的工位不加入分流队列 —— 与医生在座(status=DOCTOR_ON)同样判定,
            // 否则每次 bedReload(开诊/重启/诊室重置)后开关会失效、又开始收病人。
            // 注意:stopReady 仅阻止**新增**候诊患者,已有候诊患者仍可正常叫号
            // (nextPatient 不检查该开关)。
            if (BedStatusEnum.DOCTOR_ON.getStatus() == item.getStatus()
                    && !Boolean.TRUE.equals(item.getStopReady())) {
                addBedToRoutingQueue(item);
            }
        });
 
        Integer num = queueMapper.getMaxSeqNum();
        curSeqNum = new AtomicInteger(null == num ? 0 : num);
 
        return GlobalErrorCodeConstants.SUCCESS;
    }
 
    public void resetRoom(Boolean needCloseBed) {
        if (needCloseBed) {
            // 踢出在座的医生
            List<BedStatusEnum> bedStatusEnumList = new ArrayList<BedStatusEnum>();
            bedStatusEnumList.add(BedStatusEnum.DOCTOR_ON);
            bedStatusEnumList.add(BedStatusEnum.DOCTOR_PAUSE);
            List<RoomDO> roomDOList = roomMapper.simpleRoomList(bedStatusEnumList);
            List<Long> userIdList = roomDOList.stream().map(roomDO -> roomDO.getDocId()).toList();
            oAuth2TokenApi.tick(userIdList);
 
            // 所有工位状态 => 关闭
            Integer ret = roomMapper.resetRoom(BedStatusEnum.CLOSED.getStatus());
        }
    }
 
    public void hurryupAllBed() {
        if (0 == openingFlag.get())
            return;
 
        mapOpeningBed.values().forEach(bedQueueBO -> hurryupOneBed(bedQueueBO.roomId, bedQueueBO.bedNo));
    }
 
    /**
     * 把医生候诊的队列塞满
     */
    public void hurryupOneBed(Long roomId, String bedNo) {
        if (0 == openingFlag.get())
            return;
 
        BedBO bedBO = getBedQueueBO(roomId, bedNo);
 
        // 【需求3】「停止候诊」时不再把【已召回】患者放入候诊区。
        // 注意:这**不影响**已有候诊患者被正常叫号(nextPatient 不检查本开关),
        // 也不影响医生对具体患者的"召回"操作本身(只影响其自动进入候诊的补位)。
        boolean stopReady = Boolean.TRUE.equals(bedBO.getStopReady());
 
        // 处理 过号-回来 的人
        while (!stopReady && bedBO.getReadyPatNum() < bedBO.getMaxReadyPatNum()) {
            // 查看 当前工位 是否有过号-回来的患者
            Integer updateNum = queueMapper.queueRecalledPatient(
                    bedBO.getRoomId(),
                    bedBO.getRoomName(),
                    bedBO.getBedNo(),
                    curSeqNum.get() + 1,  // 过号回来,排到当前的下一位
                    QueueStatusEnum.RECALLED.getStatus(), // 不需要处理 [安装召回], 原因: 安装工位 不进入分流队列
                    QueueStatusEnum.READY.getStatus());
            if (null == updateNum || 0 == updateNum)
                break;
 
            // [过号] 患者 => [候诊中] ,处理 检查项目.亲和性 逻辑
            QueueDO recalledQueueItem = queueMapper.getQueueItemBySeqNum(curSeqNum.get() + 1);
 
            CheckTypeDO checkTypeDO = getCheckTypeItem( recalledQueueItem.getBookCheckType() );
            if ( checkTypeDO.getAffinityCheckTypes().length > 0) {
                procAffinityWhenRecalled(recalledQueueItem, checkTypeDO.getAffinityCheckTypes());
            }
 
            curSeqNum.getAndIncrement();
 
            // 可能已经在 nextPatient 中改变了值
            bedBO.readyPatNum++;
 
            // 可能已经【并发的】在 nextPatient 中改变了分流队列顺序
            refreshBedPriorityLevelInRoutingQueue(bedBO);
        }
 
        // 处理 排队中 患者
        hurryupOneBedCheckTypeList(bedBO);
    }
 
    /**
     * 一个工位可以支持多个检查项目,所以需要保证多个检查项目的就诊时间先后顺序
     * 按照一个检查项目来处理,就不能保证同一个工位的多个检查项目的就诊时间的先后顺序
     * 所以实现逻辑是 让支持该检查项目的所有工位去抢占 【排队中】【工位支持的检查项目】的患者
     * 实际抢占的未必是 参数中的检查项目的患者,可能是其他检查项目的患者
     * @param checkType
     */
    public void hurryupOneCheckType(Integer checkType) {
 
        // 【修复既有缺陷】补上开诊门闸。原实现**唯独这里漏了** openingFlag 检查
        // (hurryupAllBed 与 hurryupOneBed 都有),导致手动"闭诊"后,
        // 只要有人预约确认 / 加急,患者仍会被推进候诊区。
        if (0 == openingFlag.get())
            return;
 
        PriorityBlockingQueue<BedBO> routingQueue = mapCheckTypeRoutingQueue.get(checkType);
        if (null == routingQueue)
            return;
 
        while (true) {
            BedBO bedBO = routingQueue.peek();
            if (null == bedBO)
                return;
 
            // 【需求3】跳过处于「停止候诊」的工位。
            // 不能直接 return —— 该队列按 readyPatNum 升序 peek,
            // 直接 return 会让其余工位也收不到患者(饿死)。
            // 这里 poll 出来后若命中停止态,仅将其从队列移除(不再 offer 回去),
            // 然后 continue 处理下一个工位。
            if (Boolean.TRUE.equals(bedBO.getStopReady())) {
                routingQueue.poll();
                log.info("[hurryupOneCheckType] 工位[停止候诊],移出分流队列. roomId={} bedNo={}",
                        bedBO.getRoomId(), bedBO.getBedNo());
                continue;
            }
 
            int curQueueNum = bedBO.getReadyPatNum();
            if (curQueueNum > bedBO.getMaxReadyPatNum())
                throw new RuntimeException("hurryupOneCheckType: exceed max queue number!");
 
            if (curQueueNum == bedBO.getMaxReadyPatNum())
                return;
 
            // 查看 是否有排队中的患者
            Integer updateNum = queueMapper.preemptWaitingPatientWithBedCheckTypes(
                    bedBO.getRoomId(),
                    bedBO.getRoomName(),
                    bedBO.getBedNo(),
                    curSeqNum.get() + 1,
                    QueueStatusEnum.WAITING.getStatus(),
                    QueueStatusEnum.READY.getStatus(),
                    bedBO.checkTypes);  //关键点 关键点 关键点 checkType
 
            // 没有抢到排队患者
            if (null == updateNum || 0 == updateNum) {
                return;
            }
 
            QueueDO preemptQueueItem = queueMapper.getQueueItemBySeqNum(curSeqNum.get() + 1);
            CheckTypeDO checkTypeDO = getCheckTypeItem( preemptQueueItem.getBookCheckType() );
 
            // 抢到排队患者后,处理 检查项目.亲和性 逻辑
            if ( checkTypeDO.getAffinityCheckTypes().length > 0) {
                procAffinityWhenPreempt(preemptQueueItem, checkTypeDO.getAffinityCheckTypes());
            }
 
            curSeqNum.getAndIncrement();
 
            // 可能已经在 nextPatient 中改变了值
            bedBO.readyPatNum++;
 
            // 可能已经【并发的】在 nextPatient 中改变了分流队列顺序
            refreshBedPriorityLevelInRoutingQueue(bedBO);
        }
    }
 
    public Integer getOpeningFlag() {
        return openingFlag.get();
    }
 
    public void setOpeningFlag(Integer flag) {
        openingFlag.set(flag);
    }
 
    public void initCheckType() {
        List<CheckTypeDO> checkTypeDOList = checkTypeMapper.simpleCheckTypeList();
        mapCheckTypeVsReadyMax = checkTypeDOList.stream().collect(Collectors.toMap(CheckTypeDO::getValue, checkType -> checkType));
    }
 
    public CheckTypeDO getCheckTypeItem(Integer checkType) {
        return mapCheckTypeVsReadyMax.get( checkType );
    }
 
    public BedBO getBedQueueBO(Long roomId, String bedNo) {
        return mapOpeningBed.get( Utils.formatRoomBed(roomId, bedNo) );
    }
 
    public MonitorInfoVO getMonitorInfo() {
        MonitorInfoVO monitorInfoVO = new MonitorInfoVO();
        monitorInfoVO.setOpeningFlag( openingFlag.get() );
        roomBedStatistic(monitorInfoVO);
 
        return monitorInfoVO;
    }
 
    public void monitorInfo() {
        MonitorInfoVO monitorInfoVO = new MonitorInfoVO();
        roomBedStatistic(monitorInfoVO);
        log.info(" opening " + openingFlag.get() + " " + monitorInfoVO.getOpeningBedNum() + " " + monitorInfoVO.getDocBedNum() + " " + monitorInfoVO.getCheckTypeBedInfo().toString() );
    }
 
    private void hurryupOneBedCheckTypeList(BedBO bedBO) {
        // 【需求3】「停止候诊」的工位不再新增候诊患者。
        // 已有候诊患者仍可正常叫号(本方法只负责"补齐候诊",不参与叫号)。
        if (Boolean.TRUE.equals(bedBO.getStopReady())) {
            return;
        }
        while (bedBO.getReadyPatNum() < bedBO.getMaxReadyPatNum()) {
            // 抢占 是否有排队中的患者
            Integer updateNum = queueMapper.preemptWaitingPatientWithBedCheckTypes(
                    bedBO.getRoomId(),
                    bedBO.getRoomName(),
                    bedBO.getBedNo(),
                    curSeqNum.get() + 1,
                    QueueStatusEnum.WAITING.getStatus(),
                    QueueStatusEnum.READY.getStatus(),
                    bedBO.checkTypes);
 
            // 没有抢到排队患者
            if (null == updateNum || 0 == updateNum) {
                return;
            }
 
            QueueDO preemptQueueItem = queueMapper.getQueueItemBySeqNum(curSeqNum.get() + 1);
            CheckTypeDO checkTypeDO = getCheckTypeItem( preemptQueueItem.getBookCheckType() );
 
            // 抢到排队患者后,处理 检查项目.亲和性 逻辑
            if ( checkTypeDO.getAffinityCheckTypes().length > 0) {
                procAffinityWhenPreempt(preemptQueueItem, checkTypeDO.getAffinityCheckTypes());
            }
 
            curSeqNum.getAndIncrement();
 
            bedBO.readyPatNum++;
        }
 
        // 改变了分流队列顺序
        refreshBedPriorityLevelInRoutingQueue(bedBO);
    }
 
    /**
     * 有检查项分流到诊室后,也就是进入[候诊中]后,拉 [排队中] + 具有亲和性的 检查项 来依附该检查项
     */
    private void procAffinityWhenPreempt(QueueDO preemptQueueItem, Integer[] affinityCheckTypes) {
        List<QueueDO> affinityItems = queueMapper.getCurPatGivenCheckTypesAndStatus(
                preemptQueueItem.getPatId(), affinityCheckTypes, QueueStatusEnum.WAITING.getStatus());
        for (int i = 0; i < affinityItems.size(); i++) {
            QueueDO queueItem = affinityItems.get(i);
            queueItem.setStatus(QueueStatusEnum.AFFINITY.getStatus()); //改变 排队状态
            queueItem.setAffinityItem( 1 );
            queueItem.setRoomId(preemptQueueItem.getRoomId());
            queueItem.setRoomName(preemptQueueItem.getRoomName());
            queueItem.setBedNo(preemptQueueItem.getBedNo());
            queueItem.setSeqNum( preemptQueueItem.getSeqNum());  // 使用同一个 内部序号
            queueMapper.updateById(queueItem);
        }
    }
 
    /**
     * [过号] 患者 => [候诊中] ,处理 检查项目.亲和性 逻辑
     */
    private void procAffinityWhenRecalled(QueueDO recalledQueueItem, Integer[] affinityCheckTypes) {
        List<QueueDO> affinityItems = queueMapper.getCurPatGivenCheckTypesAndStatus(
                recalledQueueItem.getPatId(), affinityCheckTypes, QueueStatusEnum.AFFINITY.getStatus());
        for (int i = 0; i < affinityItems.size(); i++) {
            QueueDO queueItem = affinityItems.get(i);
            queueItem.setSeqNum( recalledQueueItem.getSeqNum() );  // [过号] 患者 => [候诊中],原内部序号更新
            queueMapper.updateById(queueItem);
        }
    }
 
    private Integer getBedReadyMax(BedBO bedBO) {
        Integer[] checkTypes = bedBO.getCheckTypes();
        int ret = 0;
        for (Integer checkType : checkTypes) {
            CheckTypeDO checkTypeDO = mapCheckTypeVsReadyMax.get(checkType);
            Integer tmp = checkTypeDO.getReadyNum();
            ret = ret < tmp ? tmp : ret;  //【候诊中】数量 取大值
        }
        return ret;
    }
 
    private void addBedToRoutingQueue(BedBO bedBO) {
        // 装机工位 不进 分流队列,不接收 【排队中】患者分流
        if (Objects.equals(BedOpTypeEnum.OP_INSTALL.getOpType(), bedBO.getOpType()))
            return;
 
        // 【需求3】「停止候诊」的工位不进分流队列。
        // 这是**唯一入口**的统一门闸:只要工位被标记为停止候诊,
        // 无论从哪条路径(开诊 addBedToRoutingQueue、医生恢复、bedReload、
        // 或手动恢复开关)试图加入,都会被拒绝,从而保证:
        //   · 已有候诊患者仍可正常叫号(nextPatient 不检查该开关)
        //   · 但不会再被分配【排队中】的新患者
        if (Boolean.TRUE.equals(bedBO.getStopReady())) {
            log.info("[addBedToRoutingQueue] 工位处于[停止候诊],跳过加入分流队列. roomId={} bedNo={}",
                    bedBO.getRoomId(), bedBO.getBedNo());
            return;
        }
 
        // 同步DB, 设置工位的分流控制信息
        List<Integer> queueStatusList = new ArrayList<>();
        queueStatusList.add(QueueStatusEnum.READY.getStatus()); //[候诊中]
        List<QueueDO> queueDOList = queueMapper.getBedQueueByStatus(bedBO.getRoomId(), bedBO.getBedNo(), queueStatusList);
        bedBO.setMaxReadyPatNum( getBedReadyMax(bedBO) );
        bedBO.setReadyPatNum( queueDOList.size() );
 
        Integer[] checkTypes = bedBO.getCheckTypes();
        Arrays.stream(checkTypes).forEach(checkType -> {
            CheckTypeDO checkTypeDO = mapCheckTypeVsReadyMax.get(checkType);
            PriorityBlockingQueue<BedBO> routingQueue = mapCheckTypeRoutingQueue.get(checkType);
            if (null == routingQueue) {
                routingQueue = new PriorityBlockingQueue<BedBO>();
                mapCheckTypeRoutingQueue.put( checkType, routingQueue );
            }
 
            if (!routingQueue.contains(bedBO)) {
                routingQueue.offer(bedBO);
            } else {
                log.error("bedDoctorResume routingQueue still exist!. " + bedBO.getRoomId() + " " + bedBO.getBedNo());
            }
        });
    }
 
    private void removeBedFromRoutingQueue(BedBO bedBO) {
        // 装机工位 不进 分流队列,不接收 【排队中】患者分流
        if (Objects.equals(BedOpTypeEnum.OP_INSTALL.getOpType(), bedBO.getOpType()))
            return;
 
        Integer[] checkTypes = bedBO.getCheckTypes();
        Arrays.stream(checkTypes).forEach(checkType -> {
            PriorityBlockingQueue<BedBO> routingQueue = mapCheckTypeRoutingQueue.get(checkType);
            if (null != routingQueue)
                routingQueue.remove(bedBO);
        });
    }
 
    private void refreshBedPriorityLevelInRoutingQueue(BedBO bedBO) {
        // 装机工位 不进 分流队列,不接收 【排队中】患者分流
        if (Objects.equals(BedOpTypeEnum.OP_INSTALL.getOpType(), bedBO.getOpType()))
            return;
 
        Integer[] checkTypes = bedBO.getCheckTypes();
        Arrays.stream(checkTypes).forEach(checkType -> {
            PriorityBlockingQueue<BedBO> routingQueue = mapCheckTypeRoutingQueue.get(checkType);
            if (null != routingQueue) {
                routingQueue.remove(bedBO);
                routingQueue.offer(bedBO);
            }
        });
    }
 
    /**
     * 检查项进入到 [安装中] 时,拉 [已领用] + 具有亲和性的 检查项 来依附该检查项
     */
    private void procAffinityWhenSomeOneEntryInstalling(String patId, Long roomId_operator, String bedNo_operator, Integer[] affinityCheckTypes) {
        List<QueueDO> affinityItems = queueMapper.getCurPatGivenCheckTypesAndStatus(
                                patId, affinityCheckTypes, QueueStatusEnum.RECEIVED.getStatus());
        for (int i = 0; i < affinityItems.size(); i++) {
            QueueDO queueItem = affinityItems.get(i);
            queueItem.setStatus(QueueStatusEnum.AFFINITY_INSTALL.getStatus()); //改变 排队状态
            queueItem.setAffinityItem(1);
            queueItem.setRoomId(roomId_operator);
            queueItem.setBedNo(bedNo_operator);  // 跟随到 装机 工位
            queueMapper.updateById(queueItem);
        }
    }
 
    private void roomBedStatistic(MonitorInfoVO monitorInfoVO) {
        Map<Integer, Integer>  mapOpeningCheckBedStatInfo = new HashMap<>();
        Map<Integer, Integer>  mapOpeningInstallBedStatInfo = new HashMap<>();
        Map<Integer, Integer>  mapDocCheckBedStatInfo = new HashMap<>();
        Map<Integer, Integer>  mapDocInstallBedStatInfo = new HashMap<>();
        mapCheckTypeVsReadyMax.keySet().forEach( checkType -> {
            mapOpeningCheckBedStatInfo.put(checkType, 0);
            mapOpeningInstallBedStatInfo.put(checkType, 0);
            mapDocCheckBedStatInfo.put(checkType, 0);
            mapDocInstallBedStatInfo.put(checkType, 0);
        });
 
        //开通的 和 运营中 的工位统计
        mapOpeningBed.values().forEach(bedQueueBO -> {
            Integer[] checkTypes = bedQueueBO.getCheckTypes();
            Arrays.stream(checkTypes).forEach(checkType -> {
                if (bedQueueBO.opType == 0 || bedQueueBO.opType == 1)
                    mapOpeningCheckBedStatInfo.put(checkType, mapOpeningCheckBedStatInfo.get(checkType) + 1 );
                else
                    mapOpeningInstallBedStatInfo.put(checkType, mapOpeningInstallBedStatInfo.get(checkType) + 1 );
 
                if (bedQueueBO.getStatus() == BedStatusEnum.DOCTOR_ON.getStatus() ||
                        bedQueueBO.getStatus() == BedStatusEnum.DOCTOR_PAUSE.getStatus()) {
                    if (bedQueueBO.opType == 0 || bedQueueBO.opType == 1)
                        mapDocCheckBedStatInfo.put(checkType, mapDocCheckBedStatInfo.get(checkType) + 1);
                    else
                        mapDocInstallBedStatInfo.put(checkType, mapDocInstallBedStatInfo.get(checkType) + 1);
                }
            });
        });
 
        // 接收..排队中患者..的分流队列
        HashSet<BedBO> hashSetOperatingBedQueue = new HashSet<>();
        mapCheckTypeRoutingQueue.values().forEach(routingQueue->{
            routingQueue.stream().forEach(bedQueueBO -> hashSetOperatingBedQueue.add(bedQueueBO));
        });
 
 
        Map<Integer, String> mapCheckTypeBedInfo = new HashMap<>();
        mapCheckTypeVsReadyMax.keySet().forEach( checkType -> {
           String str = mapOpeningCheckBedStatInfo.get(checkType) + " " +
                        mapOpeningInstallBedStatInfo.get(checkType) + " " +
                        mapDocCheckBedStatInfo.get(checkType) + " " +
                        mapDocInstallBedStatInfo.get(checkType) + " ";
            mapCheckTypeBedInfo.put( checkType, str);
        });
 
        monitorInfoVO.setCheckTypeBedInfo(mapCheckTypeBedInfo);
        monitorInfoVO.setOpeningBedNum(mapOpeningBed.size());
        monitorInfoVO.setDocBedNum((int) mapOpeningBed.values().stream().filter(item ->
                item.getStatus() == BedStatusEnum.DOCTOR_ON.getStatus() ||
                item.getStatus() == BedStatusEnum.DOCTOR_PAUSE.getStatus())
            .count());
        monitorInfoVO.setRoutingBedNum( hashSetOperatingBedQueue.size() );
        monitorInfoVO.setMapBedVsQueue(mapOpeningBed);
    }
 
}