yxh
2 天以前 1f23e7dfc186c65434eda46fd6756d41fd01e867
ai优化
已修改5个文件
已添加1个文件
226 ■■■■■ 文件已修改
jh-module-ecg/jh-module-ecg-biz/src/main/java/cn/lihu/jh/module/ecg/service/queue/QueueAutoRefillJob.java 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jh-module-ecg/jh-module-ecg-biz/src/main/java/cn/lihu/jh/module/ecg/service/queue/QueueService.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jh-module-ecg/jh-module-ecg-biz/src/main/java/cn/lihu/jh/module/ecg/service/queue/QueueServiceImpl.java 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jh-module-ecg/jh-module-ecg-biz/src/main/java/cn/lihu/jh/module/ecg/service/queue/QueueServiceTxFunctions.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jh-server/src/main/resources/application-local.yaml 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jh-server/src/main/resources/application.yaml 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jh-module-ecg/jh-module-ecg-biz/src/main/java/cn/lihu/jh/module/ecg/service/queue/QueueAutoRefillJob.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,80 @@
package cn.lihu.jh.module.ecg.service.queue;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
 * ã€Œè‡ªåŠ¨è¡¥äººã€å‘¨æœŸå…œåº•ä»»åŠ¡ã€‚
 *
 * <h3>为什么需要它</h3>
 * åˆ†æµï¼ˆ{@code æŽ’队中(10) â†’ å€™è¯Šä¸­(20)})原本是**纯事件驱动**的,只在下面这些时机跑一次:
 * <ul>
 *   <li>签到时 â€”— {@code AppointmentServiceImpl#appoitmentConfirm} â†’ {@code queue()} â†’ {@code startHurryUpOneCheckType}</li>
 *   <li>医生入座 / æ¢å¤æŽ¥è¯Š / æ‰‹åŠ¨å¼€è¯Š / å«å· / æ£€æŸ¥å®Œæˆ â€”— å„自 {@code startHurryUpOneBed}</li>
 *   <li>开诊 Â· æœåŠ¡é‡å¯ Â· è¯Šå®¤é‡ç½® â€”— {@code startBedReload} é‡Œçš„ {@code hurryupAllBed}</li>
 * </ul>
 * ä¸€æ—¦æŸæ¬¡è§¦å‘没有跑到(事务未提交就查库、执行器异常、服务刚好在重启、
 * å·¥ä½å½“时还没进分流队列……),患者就会**永远**停在「排队中(未分配工位)」,
 * åªèƒ½ç”±åŒ»ç”Ÿåœ¨å·¥ä½œå°æ‰‹å·¥ç‚¹ã€Œæ‹‰å…¥ã€ã€‚
 *
 * <p>现场反馈:运动试验签到后出现该现象(低频检查项目,执行器空闲 â‡’
 * åˆ†æµä»»åŠ¡å¿…ç„¶è·‘åœ¨äº‹åŠ¡æäº¤ä¹‹å‰ï¼Œè§ {@code QueueServiceImpl#runAfterCommit})。
 *
 * <h3>做了什么</h3>
 * æ¯ {@value #INTERVAL_SECONDS} ç§’对**所有开诊工位**触发一次自动补人。
 * åªæ˜¯"多跑一次幂等的补人",门闸(未开诊 / åœæ­¢å€™è¯Š / å€™è¯ŠåŒºå·²æ»¡ï¼‰å…¨éƒ¨ä¿æŒä¸å˜ï¼Œ
 * å› æ­¤ä¸ä¼šæ”¹å˜æ—¢æœ‰ä¸šåŠ¡è§„åˆ™ï¼ŒåªæŠŠ"漏网的排队患者"在 1 åˆ†é’Ÿå†…补上。
 *
 * <p>用独立的单线程守护调度器,**不占用** {@code DynamicSchedulingConfig} é‡Œ
 * é‚£ä¸ªåªæœ‰ 1 ä¸ªçº¿ç¨‹çš„ cron æ± ï¼ˆé¿å…å½±å“å¼€è¯Š/闭诊/每日重置任务)。
 */
@Component
@Slf4j
public class QueueAutoRefillJob {
    /** è¡¥äººé—´éš”(秒) */
    private static final long INTERVAL_SECONDS = 60L;
    /** å¯åŠ¨åŽé¦–æ¬¡å»¶è¿Ÿï¼ˆç§’ï¼‰ï¼šé¿å¼€å¯åŠ¨é˜¶æ®µï¼ˆæ­¤æ—¶è¿˜æ²¡å¼€è¯Šã€å·¥ä½ä¹Ÿæ²¡è£…è½½å®Œï¼‰ */
    private static final long INITIAL_DELAY_SECONDS = 60L;
    @Resource
    private QueueService queueService;
    private ScheduledExecutorService scheduler;
    @PostConstruct
    public void start() {
        scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
            Thread t = new Thread(r, "ecg-queue-auto-refill");
            t.setDaemon(true);
            return t;
        });
        scheduler.scheduleWithFixedDelay(() -> {
            try {
                // å¼‚步投递到 QueueServiceImpl çš„串行执行器;这里只负责"按时踹一脚"
                queueService.startHurryUpAll();
            } catch (Exception e) {
                log.error("[QueueAutoRefillJob] å‘¨æœŸè¡¥äººè§¦å‘异常(下一轮继续)", e);
            }
        }, INITIAL_DELAY_SECONDS, INTERVAL_SECONDS, TimeUnit.SECONDS);
        log.info("[QueueAutoRefillJob] å·²å¯åŠ¨ï¼šæ¯ {} ç§’兜底触发一次「全体工位自动补人」"
                + "(门闸不变:未开诊/停止候诊/候诊区已满 æ—¶ä¸ä¼šè¡¥äººï¼‰", INTERVAL_SECONDS);
    }
    @PreDestroy
    public void stop() {
        if (null != scheduler) {
            scheduler.shutdownNow();
            scheduler = null;
        }
    }
}
jh-module-ecg/jh-module-ecg-biz/src/main/java/cn/lihu/jh/module/ecg/service/queue/QueueService.java
@@ -67,6 +67,18 @@
    java.util.Map<String, Object> hurryUpNow(Long roomId, String bedNo);
    void startHurryUpOneCheckType(Integer checkType);
    /**
     * å…œåº•:对**所有**开诊工位触发一次自动补人(等价于逐个 {@link #startHurryUpOneBed})。
     *
     * <p>由 {@code QueueAutoRefillJob} å‘¨æœŸæ€§è°ƒç”¨ï¼šåˆ†æµæœ¬èº«æ˜¯"事件驱动"的
     * ï¼ˆç­¾åˆ° / å…¥åº§ / å¼€è¯Š / å«å· / å®Œæˆå„触发一次),任何一次触发没跑到或跑早了
     * ï¼ˆä¾‹å¦‚签到事务还没提交、执行器异常),患者就会一直停在「排队中」,
     * åªèƒ½ç”±åŒ»ç”Ÿæ‰‹å·¥ã€Œæ‹‰å…¥ã€ã€‚周期兜底保证这种漏网最多 1 åˆ†é’Ÿå°±è¢«è¡¥ä¸Šã€‚
     *
     * <p>门闸不变:未开诊 / åœæ­¢å€™è¯Š / å€™è¯ŠåŒºå·²æ»¡ æ—¶ç…§æ ·ä¸ä¼šè¡¥äººã€‚
     */
    void startHurryUpAll();
    void startBedReload();
    void startResetRoom(Boolean needCloseBed);
    MonitorInfoVO getMonitorInfo();
jh-module-ecg/jh-module-ecg-biz/src/main/java/cn/lihu/jh/module/ecg/service/queue/QueueServiceImpl.java
@@ -22,6 +22,8 @@
import org.apache.cxf.common.util.CollectionUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.validation.annotation.Validated;
import cn.lihu.jh.module.ecg.dal.dataobject.room.RoomDO;
@@ -316,9 +318,45 @@
    @Override
    public void startHurryUpOneBed(Long roomId, String bedNo) {
        singleThreadExecutor.execute(() -> {
            queueServiceTxFunctions.hurryupOneBed(roomId, bedNo);
        });
        // ã€ç¼ºé™·ä¿®å¤ã€‘必须等事务提交后再跑:见 runAfterCommit çš„说明
        runAfterCommit(() -> singleThreadExecutor.execute(() -> {
            try {
                queueServiceTxFunctions.hurryupOneBed(roomId, bedNo);
            } catch (Exception e) {
                // åŽŸå®žçŽ°ä¸æ•èŽ·ï¼šå¼‚å¸¸åªç•™åœ¨å·¥ä½œçº¿ç¨‹é‡Œï¼Œæ—¥å¿—ä¸€æ¡éƒ½æ²¡æœ‰ï¼ŒçŽ°åœºæ— ä»ŽæŽ’æŸ¥
                log.error("[hurryupOneBed] è‡ªåŠ¨è¡¥äººæ‰§è¡Œå¼‚å¸¸. å·¥ä½={}/{}(不影响主流程,下一轮会重试)",
                        roomId, bedNo, e);
            }
        }));
    }
    /**
     * æŠŠã€Œè§¦å‘自动分流」推迟到**事务提交之后**再执行;不在事务里则直接执行。
     *
     * <p>【缺陷修复·现场反馈】运动试验签到后,患者一直停在医生工作台的
     * ã€Œæœ¬è¯Šå®¤æŽ’队中(未分配工位)」,必须手工点「拉入」才进候诊区。
     *
     * <p>原因:{@link #queue} æ˜¯ {@code @Transactional},插入「排队中」行之后**立即**把
     * åˆ†æµä»»åŠ¡ä¸¢ç»™ {@code singleThreadExecutor}。工作线程用的是**另一个数据库连接**,
     * READ COMMITTED ä¸‹çœ‹ä¸åˆ°ä¸»äº‹åŠ¡å°šæœªæäº¤çš„é‚£ä¸€è¡Œ â‡’
     * {@code getFirstWaitingByCheckTypes} è¿”回 null â‡’ è¿™æ¬¡åˆ†æµä»€ä¹ˆéƒ½æ²¡åšå°±é€€å‡ºï¼›
     * äº‹åŠ¡æäº¤åŽåˆæ²¡æœ‰ä»»ä½•è¡¥å¿ â‡’ æ‚£è€…永远留在「排队中」。
     *
     * <p>为什么低频检查项目(运动试验、食道电生理)几乎必然中招、高频的却"看起来正常":
     * æ‰§è¡Œå™¨å¹³æ—¶ç©ºé—²ï¼Œä»»åŠ¡èƒ½**立刻**执行(必然早于提交);高频项目任务会排队,
     * è½®åˆ°æ—¶äº‹åŠ¡å¾€å¾€å·²ç»æäº¤ï¼ŒäºŽæ˜¯ä¾¥å¹¸æˆåŠŸã€‚
     */
    private void runAfterCommit(Runnable task) {
        if (TransactionSynchronizationManager.isSynchronizationActive()) {
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                @Override
                public void afterCommit() {
                    task.run();
                }
            });
        } else {
            task.run();
        }
    }
    @Override
@@ -329,9 +367,32 @@
    @Override
    public void startHurryUpOneCheckType(Integer checkType) {
        singleThreadExecutor.execute(() -> {
            queueServiceTxFunctions.hurryupOneCheckType(checkType);
        });
        // ã€ç¼ºé™·ä¿®å¤ã€‘必须等事务提交后再跑:见 runAfterCommit çš„说明
        runAfterCommit(() -> singleThreadExecutor.execute(() -> {
            try {
                queueServiceTxFunctions.hurryupOneCheckType(checkType);
            } catch (Exception e) {
                log.error("[hurryupOneCheckType] è‡ªåŠ¨åˆ†æµæ‰§è¡Œå¼‚å¸¸. checkType={}(不影响主流程,下一轮会重试)",
                        checkType, e);
            }
        }));
    }
    /**
     * å…œåº•:对**有医生在座**的工位触发一次自动补人(由 {@code QueueAutoRefillJob} å‘¨æœŸè°ƒç”¨ï¼‰ã€‚
     *
     * <p>注意用的是 {@code hurryupAllSeatedBeds} è€Œä¸æ˜¯ {@code hurryupAllBed}:
     * åŽè€…会把患者推进"已开通但没医生"的工位,周期跑会放大该副作用。
     */
    @Override
    public void startHurryUpAll() {
        runAfterCommit(() -> singleThreadExecutor.execute(() -> {
            try {
                queueServiceTxFunctions.hurryupAllSeatedBeds();
            } catch (Exception e) {
                log.error("[hurryupAllSeatedBeds] å‘¨æœŸè¡¥äººæ‰§è¡Œå¼‚常(下一轮继续)", e);
            }
        }));
    }
    @Override
jh-module-ecg/jh-module-ecg-biz/src/main/java/cn/lihu/jh/module/ecg/service/queue/QueueServiceTxFunctions.java
@@ -665,6 +665,34 @@
    }
    /**
     * ã€å‘¨æœŸå…œåº•专用】只给**有医生在座(DOCTOR_ON)**的工位补人。
     *
     * <p>为什么不能直接用 {@link #hurryupAllBed()}:它遍历的是 `mapOpeningBed`
     * ï¼ˆå«ã€Œå·²å¼€é€šä½†æ²¡äººã€çš„工位),会把排队患者推进**没有医生**的工位候诊区 â€”—
     * åŒä¸€æ£€æŸ¥é¡¹ç›®æœ‰å¤šä¸ªå·¥ä½æ—¶ï¼ˆå¦‚常规心电图 116/117/118),患者可能被"分配"到
     * ä¸€ä¸ªæ²¡äººçœ‹çš„工位,真正在座的医生反而看不到。该方法原本只在开诊/重启时跑一次,
     * è‹¥æ”¹æˆæ¯åˆ†é’Ÿè·‘一遍,这个副作用会被放大。因此周期兜底只补在座工位,
     * ä¸Žè‡ªåŠ¨åˆ†æµé˜Ÿåˆ—ï¼ˆ{@code addBedToRoutingQueue} åªæ”¶ DOCTOR_ON)的口径保持一致。
     */
    public void hurryupAllSeatedBeds() {
        if (0 == openingFlag.get())
            return;
        mapOpeningBed.values().forEach(bedQueueBO -> {
            // åªè¡¥ã€Œæœ‰åŒ»ç”Ÿåœ¨åº§ã€çš„工位
            if (!Objects.equals(BedStatusEnum.DOCTOR_ON.getStatus(), bedQueueBO.getStatus())) {
                return;
            }
            // ã€Œåœæ­¢å€™è¯Šã€çš„工位直接跳过:hurryupOneBed å†…部本来也会跳过,
            // è¿™é‡Œæå‰åˆ¤æ–­æ˜¯ä¸ºäº†é¿å…æ¯åˆ†é’Ÿåˆ·ä¸€æ¡ "[补人] å·¥ä½å¤„于【停止候诊】" çš„ WARN。
            if (Boolean.TRUE.equals(bedQueueBO.getStopReady())) {
                return;
            }
            hurryupOneBed(bedQueueBO.roomId, bedQueueBO.bedNo);
        });
    }
    /**
     * æŠŠåŒ»ç”Ÿå€™è¯Šçš„队列塞满
     */
    public void hurryupOneBed(Long roomId, String bedNo) {
jh-server/src/main/resources/application-local.yaml
@@ -51,16 +51,15 @@
      primary: master
      datasource:
        master:
          url: jdbc:mysql://47.93.127.38:3308/lihu?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X è¿žæŽ¥çš„示例
#          url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=true&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai # MySQL Connector/J 5.X è¿žæŽ¥çš„示例
          #          url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL è¿žæŽ¥çš„示例
          #          url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle è¿žæŽ¥çš„示例
          #          url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=ruoyi-vue-pro;SelectMethod=cursor;encrypt=false;rewriteBatchedStatements=true;useUnicode=true;characterEncoding=utf-8 # SQLServer è¿žæŽ¥çš„示例
          #          url: jdbc:dm://127.0.0.1:5236?schema=RUOYI_VUE_PRO # DM è¿žæŽ¥çš„示例
          #          url: jdbc:kingbase8://127.0.0.1:54321/test  # äººå¤§é‡‘仓 KingbaseES è¿žæŽ¥çš„示例
          #          url: jdbc:postgresql://127.0.0.1:5432/postgres # OpenGauss è¿žæŽ¥çš„示例
          username: smartor
          password: Smartor.2016
#          url: jdbc:mysql://127.0.0.1:3308/lihu?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X è¿žæŽ¥çš„示例
#          url: jdbc:mysql://47.93.127.38:3308/lihu?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X è¿žæŽ¥çš„示例
#          username: smartor
#          password: Smartor.2016
          url: jdbc:mysql://127.0.0.1:3306/lihu?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X è¿žæŽ¥çš„示例
          username: root
          password: sendohill
          #          username: sa # SQL Server è¿žæŽ¥çš„示例
          #          password: Jh@2024 # SQL Server è¿žæŽ¥çš„示例
          #          username: SYSDBA # DM è¿žæŽ¥çš„示例
@@ -69,9 +68,13 @@
          #          password: Jh@2024 # OpenGauss è¿žæŽ¥çš„示例
        slave: # æ¨¡æ‹Ÿä»Žåº“,可根据自己需要修改
          lazy: true # å¼€å¯æ‡’加载,保证启动速度
          url: jdbc:mysql://47.93.127.38:3308/lihu?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X è¿žæŽ¥çš„示例
          username: smartor
          password: Smartor.2016
#          url: jdbc:mysql://127.0.0.1:3308/lihu?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X è¿žæŽ¥çš„示例
#          url: jdbc:mysql://47.93.127.38:3308/lihu?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X è¿žæŽ¥çš„示例
#          username: smartor
#          password: Smartor.2016
          url: jdbc:mysql://127.0.0.1:3306/lihu?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X è¿žæŽ¥çš„示例
          username: root
          password: sendohill
  # Redis é…ç½®ã€‚Redisson é»˜è®¤çš„配置足够使用,一般不需要进行调优
  redis:
jh-server/src/main/resources/application.yaml
@@ -3,8 +3,8 @@
    name: jh-server
  profiles:
#    active: local
    active: prod
    active: local
#    active: prod
  main:
    allow-circular-references: true # å…è®¸å¾ªçŽ¯ä¾èµ–ï¼Œå› ä¸ºé¡¹ç›®æ˜¯ä¸‰å±‚æž¶æž„ï¼Œæ— æ³•é¿å…è¿™ä¸ªæƒ…å†µã€‚