liusheng
5 天以前 cc479e6a056785103996cc7ea67dbd5faa3e800e
数据采集模块
已修改43个文件
已添加11个文件
3291 ■■■■■ 文件已修改
pom.xml 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/pom.xml 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/application-nhfy.yml 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/application.yml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/core/service/VisitTaskCreateApi.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/core/service/dto/OuthospSubtaskDraft.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/core/service/dto/VisitLongTaskDto.java 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/core/service/dto/VisitSubtaskDraft.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpUtils.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-datacollection/pom.xml 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-datacollection/src/main/java/com/ruoyi/datacollection/DataCollectionModule.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-datacollection/src/main/java/com/ruoyi/datacollection/api/DataCollectionApi.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-datacollection/src/main/java/com/ruoyi/datacollection/api/DataCollectionFacade.java 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-datacollection/src/main/java/com/ruoyi/datacollection/api/DataCollectionModuleDisabledException.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-datacollection/src/main/java/com/ruoyi/datacollection/config/DataCollectionProperties.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-datacollection/src/main/java/com/ruoyi/datacollection/config/DataCollectionSwitch.java 204 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-datacollection/src/main/java/com/ruoyi/datacollection/service/IHisDataCollectionService.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-datacollection/src/main/java/com/ruoyi/datacollection/service/IInhospDeptRefreshService.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-datacollection/src/main/java/com/ruoyi/datacollection/service/impl/HisDataCollectionServiceImpl.java 341 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-datacollection/src/main/java/com/ruoyi/datacollection/service/impl/InhospDeptRefreshServiceImpl.java 393 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-inhospvisit/src/main/java/com/ruoyi/inhospvisit/api/DefaultInhospVisitDataService.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-inhospvisit/src/main/java/com/ruoyi/inhospvisit/api/InhospVisitDataService.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-inhospvisit/src/main/java/com/ruoyi/inhospvisit/api/InhospVisitFacade.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-inhospvisit/src/main/java/com/ruoyi/inhospvisit/controller/InhospVisitDataController.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-inhospvisit/src/main/java/com/ruoyi/inhospvisit/domain/PatMedInhospVO.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-inhospvisit/src/main/java/com/ruoyi/inhospvisit/domain/row/VisitInhospRow.java 181 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-inhospvisit/src/main/java/com/ruoyi/inhospvisit/mapper/PatMedInhospMapper.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-inhospvisit/src/main/java/com/ruoyi/inhospvisit/service/InhospSubtaskWriter.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-inhospvisit/src/main/java/com/ruoyi/inhospvisit/service/InhospTaskOrchestrator.java 375 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-inhospvisit/src/main/java/com/ruoyi/inhospvisit/service/InhospVisitTaskService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-inhospvisit/src/main/java/com/ruoyi/inhospvisit/support/VisitSubtaskFactory.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-inhospvisit/src/main/resources/mapper/inhospvisit/PatMedInhospMapper.xml 304 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-outpatientvisit/src/main/java/com/ruoyi/outpatientvisit/domain/PatMedOuthosp.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-quartz/pom.xml 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/RyTask.java 318 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-task/src/main/java/com/ruoyi/task/api/contract/VisitTaskCreateApiImpl.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-task/src/main/java/com/ruoyi/task/domain/PatTaskRelevance.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-task/src/main/resources/mapper/task/ServiceSubtaskMapper.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-template/src/main/java/com/ruoyi/template/domain/IvrLibaExtemplatescript.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-template/src/main/java/com/ruoyi/template/domain/VO/HeLibraryCountVO.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-template/src/main/resources/mapper/template/IvrLibaTargetMapper.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-template/src/main/resources/mapper/template/IvrTaskTemplateMapper.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-template/src/main/resources/mapper/template/SvyLibScriptMapper.xml 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-template/src/main/resources/mapper/template/SvyTaskScriptMapper.xml 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-template/src/main/resources/mapper/template/SvyTaskTemplateScriptMapper.xml 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/java/com/smartor/domain/ServiceThirdData.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/java/com/smartor/service/IServiceSLTDHealthcareRecordService.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/java/com/smartor/service/impl/ServiceSLTDHealthcareRecordServiceImpl.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/resources/mapper/smartor/HospitalRecordMapper.xml 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/resources/mapper/smartor/HospitalUserinfoMapper.xml 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/resources/mapper/smartor/PatArchiveMapper.xml 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/resources/mapper/smartor/PatHetaskRelevanceMapper.xml 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/resources/mapper/smartor/PatMedInspectionMapper.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/resources/mapper/smartor/ServiceAppointRecordMapper.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pom.xml
@@ -248,6 +248,16 @@
                <version>${ruoyi.version}</version>
            </dependency>
            <!--
                æ•°æ®é‡‡é›†æ¨¡å—(HIS é‡‡é›†çš„调度编排,采集实现后续分批搬入);
                ä¾èµ– ruoyi-common + ruoyi-system(只取参数配置 API),不依赖 smartor。
            -->
            <dependency>
                <groupId>com.ruoyi</groupId>
                <artifactId>ruoyi-datacollection</artifactId>
                <version>${ruoyi.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
@@ -270,6 +280,12 @@
        <module>ruoyi-task</module>
        <!-- ç”µè¯æ¨¡å—:依赖 ruoyi-task(任务数据)与 ruoyi-template(模板话术),必须排在 ruoyi-task ä¹‹åŽã€smartor ä¹‹å‰ -->
        <module>ruoyi-phone</module>
        <!--
            æ•°æ®é‡‡é›†æ¨¡å—:只依赖 ruoyi-common ä¸Ž ruoyi-system(参数配置 API),
            ä¸ä¾èµ– smartor â€”— HIS é‡‡é›†å®žçŽ°æš‚æ—¶ç•™åœ¨ smartor,经 ruoyi-common çš„契约
            com.ruoyi.common.core.service.HisGatherApi è°ƒç”¨ï¼ˆsmartor ä¾§ HisGatherApiBridge å®žçŽ°ï¼‰ã€‚
        -->
        <module>ruoyi-datacollection</module>
        <!-- smartor ä¾èµ– ruoyi-satisfaction / ruoyi-inhospvisit / ruoyi-publicity / ruoyi-outpatientvisit / ruoyi-othervisit / ruoyi-template / ruoyi-task / ruoyi-phone,必须排最后 -->
        <module>smartor</module>
    </modules>
ruoyi-admin/pom.xml
@@ -132,6 +132,15 @@
            <groupId>com.ruoyi</groupId>
            <artifactId>ruoyi-phone</artifactId>
        </dependency>
        <!--
            æ•°æ®é‡‡é›†æ¨¡å—(HIS é‡‡é›†çš„调度编排)。
            âš ï¸ åŒæ ·ã€å¿…须显式声明】:本模块要把 ruoyi-datacollection æ‰“è¿› smartor-suifang.jar,
               å¦åˆ™ ruoyi-quartz çš„定时任务入口 ryTask.dealHisData('...') ä¼šå› ç¼º Bean è€Œå¤±è´¥ã€‚
        -->
        <dependency>
            <groupId>com.ruoyi</groupId>
            <artifactId>ruoyi-datacollection</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/net.glxn.qrgen/javase -->
        <dependency>
            <groupId>net.glxn.qrgen</groupId>
ruoyi-admin/src/main/resources/application-nhfy.yml
@@ -127,6 +127,9 @@
#钉钉的密钥
dingAppid: dingn8iip5ubj7clrrsv
dingAppSecret: qlEK8D3oOVwGPOTiBQIBYTqQVlAfy9S_qQizEQFjJdSScwemWFryg4gbneu-NqWD
# é’‰é’‰åº”用 agentId(DingTalkServiceImpl @Value("${dingAppAgentId}") æ— é»˜è®¤å€¼ï¼Œç¼ºå¤±ä¼šå¯åŠ¨å¤±è´¥ï¼‰
# æš‚与丽水/省立同德保持一致;南华附一若用独立钉钉应用,请替换为它自己的 agentId
dingAppAgentId: 4992829038
# websocket超时时间
server:
ruoyi-admin/src/main/resources/application.yml
@@ -75,7 +75,7 @@
    # å›½é™…化资源文件路径
    basename: i18n/messages
  profiles:
    active: druid
    active: sltd
  # æ–‡ä»¶ä¸Šä¼ 
  servlet:
ruoyi-common/src/main/java/com/ruoyi/common/core/service/VisitTaskCreateApi.java
@@ -152,4 +152,26 @@
     * @return true = è‡³å°‘存在一条
     */
    boolean existsSubtaskWithStates(Long patid, java.util.List<Long> sendstates, String serviceType);
    /**
     * åˆ¤æ–­æŸæ‚£è€…「同一次出入院记录 + åŒä¸€ä¸ªä»»åŠ¡ã€æ˜¯å¦<b>已经生成过</b>子任务。
     *
     * <h3>为什么需要它(2026-10 çº¿ä¸Šé—®é¢˜ï¼‰</h3>
     * å‡ºé™¢éšè®¿æ‰«ææ˜¯æŒ‰ {@code pat_med_inhosp.deptcheck_flag = '0'} å–患者的。
     * ä¸€æ—¦æ ‡å¿—位因为规则配置/异常没被改成 1 æˆ– 2,下一轮扫描会把同一个患者再处理一遍,
     * è€Œ {@code writeInSubTask} çš„"停旧"分支在 {@code endTime1 == endTime}(同一次出院)时
     * èµ°çš„æ˜¯ else â€”— <b>会再 insert ä¸€æ¡ sendstate=4 çš„停用行</b>,
     * äºŽæ˜¯åŒä¸€ä¸ªæ‚£è€…每次跑批都多一条,最终同一患者几十条子任务。
     * <p>
     * è¿™ä¸ªæ–¹æ³•是那道"最后防线":落库前按
     * {@code patid + inhospid + taskid}(且 {@code del_flag = 0})判重,
     * å·²å­˜åœ¨å°±ä¸å†åŠ ã€‚å£å¾„æ˜¯"同一次出院 + åŒä¸€ä¸ªä»»åŠ¡åªåº”æœ‰ä¸€æ¡å­ä»»åŠ¡",
     * <b>不跨住院记录</b>(再次入院换了 inhospid ä»ä¼šæ–°å»ºï¼Œè¿™æ˜¯åŽŸæœ‰è¯­ä¹‰ï¼‰ã€‚
     *
     * @param patid    æ‚£è€…档案ID
     * @param inhospid å‡ºå…¥é™¢è®°å½•ID;为 {@code null} æ—¶é€€åŒ–为只看 {@code patid + taskid}
     * @param taskid   ä»»åŠ¡ID
     * @return true = å·²å­˜åœ¨ï¼ˆä¸åº”再 insert)
     */
    boolean existsSubtask(Long patid, Long inhospid, Long taskid);
}
ruoyi-common/src/main/java/com/ruoyi/common/core/service/dto/OuthospSubtaskDraft.java
@@ -15,16 +15,21 @@
 *   <tr><td>{@code patArchive.telcode}(空则 {@code relativetelcode})</td><td>{@code phone}</td></tr>
 *   <tr><td>{@code patArchive.sex} / {@code age}</td><td>{@code sex} / {@code age}</td></tr>
 *   <tr><td>{@code patMedOuthosp.id}</td><td>{@code inhospid}</td></tr>
 *   <tr><td>{@code patMedOuthosp.outhospno}</td><td>{@code hospno}</td></tr>
 * </table>
 * æ‰€ä»¥æœ¬ç±»<b>只</b>补三个父类确实没有的字段,不做重复定义 â€”—
 * æ‰€ä»¥æœ¬ç±»<b>只</b>补父类确实没有的字段,不做重复定义 â€”—
 * é‡å¤å®šä¹‰ä¼šè®©"哪个字段才是真正落库的"变得不清楚。
 * <p>
 * âš ï¸ {@code hospno} åŽŸå…ˆåªåœ¨æœ¬ç±»é‡Œå£°æ˜Žï¼Œå¯¼è‡´å‡ºé™¢é“¾è·¯ï¼ˆ{@code addSubTaskByRule})
 * æ ¹æœ¬æ²¡æœ‰è¿™ä¸ªè½½ä½“、{@code service_subtask.hospno} ä¸€ç›´è½ç©ºã€‚
 * çŽ°å·²ä¸Šæåˆ°çˆ¶ç±» {@link VisitSubtaskDraft},本类不再重复声明。
 *
 * <h3>为什么要单独一个类(而不是往 VisitSubtaskDraft é‡ŒåŠ å­—æ®µï¼‰</h3>
 * {@code insertServiceSubtask} æœ‰ 39 ä¸ªæ¡ä»¶åˆ—({@code <if test="xxx != null">})。
 * ç»™å…±äº«çš„ {@code VisitSubtaskDraft} åŠ å­—æ®µï¼Œå°±å¿…é¡»åŒæ—¶å†³å®š"要不要在
 * {@code VisitTaskCreateApiBridge#toSubtaskEntity} é‡Œè¡¥æ˜ å°„"——
 * {@code VisitTaskCreateApiImpl#toSubtaskEntity} é‡Œè¡¥æ˜ å°„"——
 * è¡¥äº†æ˜ å°„会让<b>出入院那条已验收的写入路径多写列</b>。
 * æ‰€ä»¥æŒ‰"另建门诊专用草稿"处理:{@code VisitSubtaskDraft} ä¸Žå‡ºå…¥é™¢é“¾è·¯<b>一行不动</b>,
 * æ‰€ä»¥æŒ‰"另建门诊专用草稿"处理:门诊独有的字段放本类,
 * æ¡¥é‡Œç”¨ {@code instanceof} åˆ†æµæ˜ å°„。
 *
 * @see VisitSubtaskDraft
@@ -33,14 +38,6 @@
public class OuthospSubtaskDraft extends VisitSubtaskDraft {
    private static final long serialVersionUID = 1L;
    /**
     * é—¨è¯Šå·ï¼ˆåŽŸ {@code patMedOuthosp.outhospno} â†’ è½åº“到 {@code service_subtask.hospno})。
     * <p>
     * âš ï¸ çˆ¶ç±» {@link VisitSubtaskDraft} <b>没有</b> {@code hospno} å­—段
     * ï¼ˆ{@code ServiceSubtask} å®žä½“有,父类漏了),所以这里是必需的补充。
     */
    private String hospno;
    /**
     * é—¨è¯Šå°±è¯Šæ—¥æœŸï¼ˆåŽŸ {@code patMedOuthosp.admitdate})。
@@ -87,14 +84,6 @@
     * ä¸”全仓无人读取它 â€”— ä¸è¦æŠŠå®ƒä¸Žè¿™é‡Œçš„ {@code hospType} æ··ä¸ºä¸€è°ˆã€‚
     */
    private String hospType;
    public String getHospno() {
        return hospno;
    }
    public void setHospno(String hospno) {
        this.hospno = hospno;
    }
    public Date getAdmitdate() {
        return admitdate;
ruoyi-common/src/main/java/com/ruoyi/common/core/service/dto/VisitLongTaskDto.java
@@ -24,6 +24,9 @@
    /** ä»»åŠ¡åç§° */
    private String taskName;
    /** ä»»åŠ¡æè¿°ï¼ˆè½åº“åˆ° {@code service_subtask.task_desc}) */
    private String taskDesc;
    /** ä»»åŠ¡ç±»åž‹ï¼ˆ"3" = é€šçŸ¥ç±»ï¼Œå†³å®š visitTime æ˜¯å¦æå‰ N å¤©ï¼Œè§è¿ç§»å‰ L1188-1203) */
    private String type;
@@ -38,6 +41,9 @@
    /** è¯­éŸ³æ¨¡æ¿ID(原字段是 String,迁移前用 Long.valueOf() è½¬æ¢ï¼‰ */
    private String libtemplateid;
    /** è¯­éŸ³æ¨¡æ¿åç§°ï¼ˆè½åº“到 {@code service_subtask.libtemplatename}) */
    private String libtemplatename;
    /** æŽ¨é€/发送日偏移天数(原 sendDay,缺省按 1 å¤„理) */
    private Long sendDay;
@@ -56,6 +62,30 @@
    /** å®£æ•™æ¨¡æ¿ï¼ˆåŽŸ preachform çš„同义字段,组装时也在用) */
    private String hosType;
    // ------------------------------------------------------------------
    // ä»¥ä¸‹ 6 ä¸ªæ˜¯ DtoConversionUtils.sourceToTarget æ‹·è´ã€ä¸”原实现的
    // insertServiceSubtask ä¼šè½åº“的列。补齐它们之前,
    // æ–°é“¾è·¯é‡Œè¿™äº›åˆ—会静默落空(XML æ˜¯ <if test="xxx != null">)。
    // ------------------------------------------------------------------
    /** GUID(原 serviceTask.guid â†’ service_subtask.guid) */
    private String guid;
    /** pid(原 serviceTask.pid æ˜¯ Integer,目标 ServiceSubtask.pid æ˜¯ Long â€”— æ‹·è´æ—¶åšäº†è½¬æ¢ï¼‰ */
    private Integer pid;
    /** å‘送类型(原 serviceTask.sendType æ˜¯ Long,目标 ServiceSubtask.sendType æ˜¯ String) */
    private Long sendType;
    /** æ˜¯å¦å·²ä¸Šä¼ ï¼ˆåŽŸ serviceTask.isupload â†’ service_subtask.isupload) */
    private Long isupload;
    /** æ–‡æœ¬å‚数(原 serviceTask.textParam â†’ service_subtask.text_param) */
    private String textParam;
    /** ä¸Šä¼ æ—¶é—´ï¼ˆåŽŸ serviceTask.uploadTime â†’ service_subtask.upload_time) */
    private java.util.Date uploadTime;
    /** æ›´æ–°äººï¼ˆåŽŸ updateBy,迁移前多处 setUpdateBy(serviceTask.getUpdateBy())) */
    private String updateBy;
@@ -77,6 +107,14 @@
    public void setTaskName(String taskName) {
        this.taskName = taskName;
    }
    public String getTaskDesc() {
        return taskDesc;
    }
    public void setTaskDesc(String taskDesc) {
        this.taskDesc = taskDesc;
    }
    public String getType() {
@@ -117,6 +155,14 @@
    public void setLibtemplateid(String libtemplateid) {
        this.libtemplateid = libtemplateid;
    }
    public String getLibtemplatename() {
        return libtemplatename;
    }
    public void setLibtemplatename(String libtemplatename) {
        this.libtemplatename = libtemplatename;
    }
    public Long getSendDay() {
@@ -167,6 +213,54 @@
        this.hosType = hosType;
    }
    public String getGuid() {
        return guid;
    }
    public void setGuid(String guid) {
        this.guid = guid;
    }
    public Integer getPid() {
        return pid;
    }
    public void setPid(Integer pid) {
        this.pid = pid;
    }
    public Long getSendType() {
        return sendType;
    }
    public void setSendType(Long sendType) {
        this.sendType = sendType;
    }
    public Long getIsupload() {
        return isupload;
    }
    public void setIsupload(Long isupload) {
        this.isupload = isupload;
    }
    public String getTextParam() {
        return textParam;
    }
    public void setTextParam(String textParam) {
        this.textParam = textParam;
    }
    public java.util.Date getUploadTime() {
        return uploadTime;
    }
    public void setUploadTime(java.util.Date uploadTime) {
        this.uploadTime = uploadTime;
    }
    public String getUpdateBy() {
        return updateBy;
    }
ruoyi-common/src/main/java/com/ruoyi/common/core/service/dto/VisitSubtaskDraft.java
@@ -58,6 +58,17 @@
    private Long inhospid;
    /** ä½é™¢æµæ°´å· */
    private String serialnum;
    /**
     * å°±è¯Šç¼–号 â€”— è½åº“到 {@code service_subtask.hospno}("对应的 outhospno、inhospno")。
     * <p>
     * å‡ºé™¢é“¾è·¯çš„取值是 {@code pat_med_inhosp.inhospno}(门诊链路是
     * {@code pat_med_outhosp.outhospno},见 {@link OuthospSubtaskDraft})。
     * <p>
     * âš ï¸ è¿™ä¸ªå­—段原先只存在于 {@link OuthospSubtaskDraft},而出院的
     * {@code addSubTaskByRule} é“¾è·¯ä»Žä¸è®¾å®ƒ â†’ {@code service_subtask.hospno} ä¸€ç›´è½ç©ºã€‚
     * çŽ°åœ¨æåˆ°çˆ¶ç±»ï¼Œä¸¤æ¡é“¾è·¯å…±ç”¨åŒä¸€ä¸ªè½½ä½“ã€‚
     */
    private String hospno;
    // ==================== åŒ»æŠ¤ï¼ˆåŽŸ patMedInhosp) ====================
    /** è´£ä»»æŠ¤å£«ID */
@@ -352,6 +363,14 @@
        this.serialnum = serialnum;
    }
    public String getHospno() {
        return hospno;
    }
    public void setHospno(String hospno) {
        this.hospno = hospno;
    }
    public String getNurseId() {
        return nurseId;
    }
ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpUtils.java
@@ -215,6 +215,23 @@
    }
    public static String sendPostByHeader(String url, String param, Map<String, String> headers) {
        // ä¸è®¾è¶…时:与历史行为一致(老调用方一个字节都不变)
        return sendPostByHeader(url, param, headers, 0, 0);
    }
    /**
     * å¸¦è¶…æ—¶çš„ POST(JSON æŠ¥æ–‡å¤´ + è‡ªå®šä¹‰ header)。
     * <p>
     * âš ï¸ <b>为什么会有这个重载</b>:上面的老方法<b>没有连接/读超时</b>,
     * ä¸€æ—¦å¯¹ç«¯ä¸å“åº”,调用线程会无限期挂住。批量按患者调用的采集
     * ï¼ˆå¦‚"在院信息刷新",一轮几千次)承受不起这个风险,所以补一个显式超时的重载;
     * è€æ–¹æ³•改为委托本方法并传 0(0 = ä¸è®¾è¶…时),行为与改造前完全一致。
     *
     * @param connectTimeoutMs è¿žæŽ¥è¶…时(毫秒);{@code <=0} è¡¨ç¤ºä¸è®¾
     * @param readTimeoutMs    è¯»è¶…时(毫秒);{@code <=0} è¡¨ç¤ºä¸è®¾
     */
    public static String sendPostByHeader(String url, String param, Map<String, String> headers,
                                          int connectTimeoutMs, int readTimeoutMs) {
        StringBuilder result = new StringBuilder();
        HttpURLConnection conn = null;
        try {
@@ -228,6 +245,12 @@
            conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
            conn.setDoOutput(true);
            conn.setDoInput(true);
            if (connectTimeoutMs > 0) {
                conn.setConnectTimeout(connectTimeoutMs);
            }
            if (readTimeoutMs > 0) {
                conn.setReadTimeout(readTimeoutMs);
            }
            if (headers != null) {
                for (Map.Entry<String, String> entry : headers.entrySet()) {
ruoyi-datacollection/pom.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>ruoyi</artifactId>
        <groupId>com.ruoyi</groupId>
        <version>3.8.5</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>ruoyi-datacollection</artifactId>
    <description>
        æ•°æ®é‡‡é›†æ¨¡å—:HIS æ•°æ®é‡‡é›†çš„调度编排(原 ruoyi-quartz çš„ RyTask#dealHisData / collectSegmentData)
        ä¸ŽåŽç»­åˆ†æ‰¹æ¬å…¥çš„采集实现(新华 / æ²³å— / çœç«‹åŒå¾·Â·å—华附一 / ç–¾ç—…字典)。
        ä¾èµ–方向:ruoyi-datacollection -> ruoyi-common(契约与工具)
                                    -> ruoyi-system(sys_config æŒ‰ orgid çš„配置查询,走 ISysConfigService è¿™ä¸ª API)
                                    -> ruoyi-inhospvisit(pat_med_inhosp çš„归属模块:
                                       åœ¨é™¢ä¿¡æ¯åˆ·æ–°å–候选 + å†™å›žä½ç½®ï¼Œåªç»å…¶ api åŒ…的门面/数据契约)
        ã€ä¸ä¾èµ– smartor】:采集实现目前仍在 smartor,按「跨模块数据访问原则」
        ï¼ˆdoc/跨模块数据访问原则与收口方案.md)经 ruoyi-common çš„
        com.ruoyi.common.core.service.HisGatherApi å¥‘约调用,smartor ä¾§ç”¨çº¯è½¬å‘ Bridge å®žçŽ°ã€‚
        é‡‡é›†å®žçŽ°æŒ‰æ‰¹æ¬è¿›æœ¬æ¨¡å—åŽï¼Œå¥‘çº¦æ–¹æ³•éšä¹‹åˆ é™¤ï¼ˆè§ doc/数据采集模块化_调研与实施方案.md)。
        æœ¬æ¨¡å—无可访问的 HTTP ç«¯ç‚¹ï¼Œå¼€å…³ä½œç”¨ç‚¹åœ¨é—¨é¢ com.ruoyi.datacollection.api.DataCollectionFacade。
        æ”¯æŒé€šè¿‡ sys_config å‚数表里的 datacollection.enabled æ•´ä½“停采(运行时读取,改完最多 10 ç§’生效)。
    </description>
    <dependencies>
        <!-- é€šç”¨å·¥å…·ï¼šStringUtils / å¥‘约 HisGatherApi / å¥‘约 DTO SltdGatherReq -->
        <dependency>
            <groupId>com.ruoyi</groupId>
            <artifactId>ruoyi-common</artifactId>
        </dependency>
        <!--
            ç³»ç»Ÿæ¨¡å—:只用到它的参数配置服务 API â€”—
              Â· com.ruoyi.system.service.ISysConfigService#selectConfigList(SysConfig)
                ï¼ˆé‡‡é›†ç¼–排要按 config_key + orgid è¯» sltd.gather.sysuser / sltd.gather.campusid,
                 ä¸Žæ¬è¿å‰çš„调用完全一致,含 LIKE åŒ¹é…è¯­ä¹‰ï¼Œæ•…不改成 selectConfigByKey ä»¥å…æ”¹å˜è¡Œä¸ºï¼‰
              Â· com.ruoyi.system.domain.SysConfig
            ruoyi-system åªä¾èµ– ruoyi-common / ruoyi-message,不会与本模块形成循环依赖。
        -->
        <dependency>
            <groupId>com.ruoyi</groupId>
            <artifactId>ruoyi-system</artifactId>
        </dependency>
        <!--
            å‡ºå…¥é™¢éšè®¿æ¨¡å—:只用到它 api åŒ…的两个能力 â€”—
              Â· com.ruoyi.inhospvisit.api.InhospVisitFacade#selectInhospRefreshCandidates(String)
                ï¼ˆåœ¨é™¢ä¿¡æ¯åˆ·æ–°çš„取数:只查 pat_med_inhosp è‡ªèº«ã€åªå–位置相关列)
              Â· com.ruoyi.inhospvisit.api.InhospVisitDataService#updatePatMedInhosp(PatMedInhosp)
                ï¼ˆ"传什么改什么"的位置写回;不直接注入该模块的 Mapper)
            ä¾èµ–方向无环:ruoyi-inhospvisit åªä¾èµ– ruoyi-common,不依赖本模块。
        -->
        <dependency>
            <groupId>com.ruoyi</groupId>
            <artifactId>ruoyi-inhospvisit</artifactId>
        </dependency>
        <!-- lombok:ruoyi-common é‡Œæ˜¯ç›´æŽ¥ä¾èµ–,此处显式声明版本,避免注解处理器缺失 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.34</version>
        </dependency>
    </dependencies>
</project>
ruoyi-datacollection/src/main/java/com/ruoyi/datacollection/DataCollectionModule.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,41 @@
package com.ruoyi.datacollection;
/**
 * æ•°æ®é‡‡é›†æ¨¡å—包根。
 *
 * <h3>这个模块装了什么(阶段 1 Â· æœ¬æ¬¡ï¼‰</h3>
 * <ul>
 *   <li>{@code service} â€”— HIS æ•°æ®é‡‡é›†çš„<b>调度编排</b>:原 {@code ruoyi-quartz} é‡Œ
 *       {@code RyTask#dealHisData(String)} + ç§æœ‰å¸®æ‰‹ {@code collectSegmentData(...)}
 *       åŽŸæ ·æ¬æ¥ï¼Œé€»è¾‘ä¸€è¡Œæœªæ”¹ï¼ˆå«åˆ†ç«™ç‚¹åˆ†æ”¯ã€30 å¤©åˆ†æ®µå›žæº¯ã€æ—¥å¿—文案);</li>
 *   <li>{@code config} â€”— æ¨¡å—开关({@code sys_config: datacollection.enabled},默认 true,fail-open);</li>
 *   <li>{@code api} â€”— <b>唯一对外包</b>:其它模块只能经 {@code DataCollectionFacade} è°ƒæœ¬æ¨¡å—。</li>
 * </ul>
 *
 * <h3>数据访问边界</h3>
 * æœ¬æ¨¡å—<b>刻意不带 Mapper / XML</b>:编排只做"何时采、采哪个院区、采哪段时间",
 * çœŸæ­£çš„采集动作由契约 {@code com.ruoyi.common.core.service.HisGatherApi} æ‰¿æŽ¥ï¼Œ
 * å½“前实现在 smartor({@code HisGatherApiBridge},纯转发到既有 Service)。
 * è¿™æ ·æ–°æ¨¡å—不必依赖 smartor,也就不会把"模块化"倒退回单体。
 *
 * <h3>阶段 2 / 3(后续批次)</h3>
 * é‡‡é›†å®žçŽ°æŒ‰æ‰¹æ¬è¿›æœ¬æ¨¡å—ï¼ˆXH/HN ä¸€æ‰¹ã€SLTD ä¸€æ‰¹ï¼‰ï¼Œæ¯æ¬ä¸€æ‰¹å°±æŠŠ
 * {@code HisGatherApi} å¯¹åº”方法删掉;全部搬完时契约与 smartor çš„ Bridge ä¸€èµ·åˆ é™¤ã€‚
 * å„批的依赖盘点、需要的跨模块契约、风险点见
 * {@code doc/数据采集模块化_调研与实施方案.md}。
 *
 * <h3>开关</h3>
 * {@code sys_config: datacollection.enabled}(默认 true):关闭后
 * {@code DataCollectionFacade#dealHisData} æŠ› {@code DataCollectionModuleDisabledException},
 * ç”±è°ƒç”¨æ–¹ï¼ˆ{@code RyTask})明确降级并打 WARN â€”— ä¸é™é»˜è·³è¿‡ï¼Œä¹Ÿä¸é™é»˜ç»§ç»­é‡‡ã€‚
 */
public final class DataCollectionModule {
    /**
     * æ¨¡å—配置前缀(仅作为 yml å…œåº•配置的键前缀;主开关在 sys_config å‚数表里)
     */
    public static final String PROPERTIES_PREFIX = "datacollection";
    private DataCollectionModule() {
    }
}
ruoyi-datacollection/src/main/java/com/ruoyi/datacollection/api/DataCollectionApi.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,49 @@
package com.ruoyi.datacollection.api;
/**
 * æ•°æ®é‡‡é›†æ¨¡å—的对外契约(<b>其它模块唯一允许依赖的类型</b>)。
 * <p>
 * å®žçŽ°æ˜¯æœ¬æ¨¡å—çš„å¸¸é©»é—¨é¢ {@link DataCollectionFacade}。
 *
 * <h3>约定(照抄其它模块的 api å¥‘约纪律)</h3>
 * <ul>
 *   <li><b>只暴露基本类型</b>,不暴露本模块内部 Service / domain;</li>
 *   <li>门面<b>永远存在</b>,调用方的 {@code @Autowired} ä¸ä¼šå› å¼€å…³çŠ¶æ€è€Œå¯åŠ¨å¤±è´¥ï¼›</li>
 *   <li>模块被关闭时<b>抛 {@link DataCollectionModuleDisabledException}</b>
 *       ï¼ˆè€Œä¸æ˜¯é™é»˜è¿”回):采集是"数据进不来"的链路,
 *       é™é»˜è·³è¿‡ä¸Ž"本来就没数据"分不开,排查时最怕这个。</li>
 * </ul>
 */
public interface DataCollectionApi {
    /**
     * æ¨¡å—是否可用(开关打开)
     * <p>
     * âš ï¸ è¿™æ˜¯<b>查询开关状态</b>的方法,刻意不受守卫:调用方需要它来预判,
     * å¦‚果它也抛异常就自我矛盾了。
     */
    boolean isEnabled();
    /**
     * HIS æ•°æ®é‡‡é›†ï¼ˆåŽŸ {@code RyTask#dealHisData(String)}):
     * æŒ‰ {@code spring.profiles.active} åˆ†ç«™ç‚¹é‡‡é›†æ–°åŽ / æ²³å— / çœç«‹åŒå¾·ï¼ˆå—华附一)的
     * å‡ºå…¥é™¢ã€é¢„入院、出院与医院用户/部门信息。
     *
     * @param todayZeroStr é‡‡é›†åŸºå‡†æ—¶é—´ {@code yyyy-MM-dd HH:mm:ss};
     *                     ä¸ºç©ºæ—¶å–"今天 00:00",采集范围是 {@code [基准-1天, åŸºå‡†)}。
     *                     nhfy / sltd ç«™ç‚¹åœ¨å…¥å‚非空时会额外做 30 å¤©åˆ†æ®µå›žæº¯é‡‡é›†ã€‚
     * @throws DataCollectionModuleDisabledException æ¨¡å—被关闭({@code datacollection.enabled=false})
     */
    void dealHisData(String todayZeroStr);
    /**
     * åœ¨é™¢æ‚£è€…"科室 / ç—…区 / åºŠä½"刷新(南华附一,每天一次全量核对):
     * æŒ‰æ‚£è€…查 HIS çš„转科记录、取最新一段,与 {@code pat_med_inhosp} æ¯”对后只写变化的字段。
     * <p>
     * å¹‚等,可重复触发;站点不是 {@code nhfy} æ—¶ä¸åšä»»ä½•事并返回 0。
     *
     * @return å®žé™…发生更新的记录条数
     * @throws DataCollectionModuleDisabledException æ¨¡å—被关闭({@code datacollection.enabled=false})
     */
    int refreshInhospDeptInfo();
}
ruoyi-datacollection/src/main/java/com/ruoyi/datacollection/api/DataCollectionFacade.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,93 @@
package com.ruoyi.datacollection.api;
import com.ruoyi.datacollection.config.DataCollectionSwitch;
import com.ruoyi.datacollection.service.IHisDataCollectionService;
import com.ruoyi.datacollection.service.IInhospDeptRefreshService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
 * æ•°æ®é‡‡é›†æ¨¡å—对外门面(<b>常驻 Bean</b>)—— å…¶å®ƒæ¨¡å—触发 HIS é‡‡é›†çš„唯一入口。
 *
 * <h3>为什么必须"常驻"</h3>
 * æœ¬é—¨é¢<b>永远</b>是 Spring Bean,不随开关状态增删。调用方({@code RyTask})的
 * {@code @Autowired} å› æ­¤ä¸ä¼šåœ¨ä»»ä½•开关状态下启动失败 â€”—
 * ä¸Ž {@code TaskFacade} / {@code PhoneFacade} / {@code TemplateFacade} æ˜¯åŒä¸€æ¡è®¾è®¡çº¦æŸ
 * ï¼ˆåŽ†å²æ•™è®­ï¼šç”¨ {@code @ConditionalOnProperty} æ‘˜ Bean ä¼šè®©è°ƒç”¨æ–¹å¯åŠ¨å³å¤±è´¥ï¼Œ
 * ä¸”只在启动时求值一次,做不到"改完参数 10 ç§’生效")。
 *
 * <h3>⚠️ å…³é—­æ—¶ï¼šæŠ›å¼‚常,而不是静默跳过</h3>
 * æ¨¡å—关闭({@code sys_config: datacollection.enabled=false})时,
 * {@link #dealHisData(String)} å…ˆè¿‡ {@link #requireEnabled()},抛
 * {@link DataCollectionModuleDisabledException}:
 * <ul>
 *   <li><b>为什么不用"打条 WARN å°±è¿”回"</b>:采集是"数据进不来"的链路。
 *       é™é»˜è·³è¿‡ä¼šè®©"模块被关掉"和"这轮本来就没数据"再也分不开 â€”—
 *       ç­‰å‘现漏数据时,日志里已经过去几千轮,无从判断;</li>
 *   <li><b>调用方必须配降级</b>:本门面的调用点是定时任务
 *       {@code RyTask#dealHisData},它捕获本异常并打 WARN(本轮明确跳过),
 *       è§ {@code doc/数据采集模块化_调研与实施方案.md} Â§5。</li>
 * </ul>
 *
 * <h3>⚠️ ä¾‹å¤–:{@link #isEnabled()} ä¸å—守卫</h3>
 * å®ƒæœ¬èº«å°±æ˜¯æŸ¥å¼€å…³çŠ¶æ€ç”¨çš„ï¼›å¦‚æžœå®ƒä¹ŸæŠ›å¼‚å¸¸ï¼Œ"开关是不是关着"就再也问不出来了。
 */
@Slf4j
@Component
public class DataCollectionFacade implements DataCollectionApi {
    /**
     * è¿è¡Œæ—¶å¼€å…³ï¼ˆ{@code sys_config} ä¼˜å…ˆï¼Œyml å…œåº•)
     */
    @Autowired
    private DataCollectionSwitch dataCollectionSwitch;
    /**
     * æ¨¡å—内实现(唯一实现 {@code HisDataCollectionServiceImpl})
     */
    @Autowired
    private IHisDataCollectionService hisDataCollectionService;
    /**
     * æ¨¡å—内实现(唯一实现 {@code InhospDeptRefreshServiceImpl}):
     * åœ¨é™¢æ‚£è€…的科室/病区/床位每日刷新
     */
    @Autowired
    private IInhospDeptRefreshService inhospDeptRefreshService;
    /**
     * æ¨¡å—是否启用({@code sys_config: datacollection.enabled},运行时热读)
     * <p>
     * âš ï¸ <b>刻意不受守卫</b>:见类注释。
     */
    @Override
    public boolean isEnabled() {
        return dataCollectionSwitch.isEnabled();
    }
    @Override
    public void dealHisData(String todayZeroStr) {
        requireEnabled();
        hisDataCollectionService.dealHisData(todayZeroStr);
    }
    @Override
    public int refreshInhospDeptInfo() {
        requireEnabled();
        return inhospDeptRefreshService.refreshInhospDeptInfo();
    }
    /**
     * é—¨é¢æ•°æ®æ–¹æ³•的统一入口守卫:模块关闭时抛
     * {@link DataCollectionModuleDisabledException},由调用方决定如何降级。
     */
    private void requireEnabled() {
        if (!dataCollectionSwitch.isEnabled()) {
            log.warn("【数据采集】模块已关闭(参数配置 {} = false),拒绝门面访问",
                    DataCollectionSwitch.CONFIG_KEY);
            throw new DataCollectionModuleDisabledException(
                    DataCollectionModuleDisabledException.DISABLED_MESSAGE);
        }
    }
}
ruoyi-datacollection/src/main/java/com/ruoyi/datacollection/api/DataCollectionModuleDisabledException.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
package com.ruoyi.datacollection.api;
/**
 * æ•°æ®é‡‡é›†æ¨¡å—被关闭({@code sys_config: datacollection.enabled=false})时,
 * é—¨é¢è°ƒç”¨æŠ›å‡ºçš„异常。
 *
 * <h3>为什么需要它(而不是复用 {@code ServiceException})</h3>
 * {@code ServiceException} æ˜¯ <b>final</b> çš„(见 {@code com.ruoyi.common.exception.ServiceException}),
 * æ—¢ä¸èƒ½ç»§æ‰¿ã€ä¹Ÿæ²¡æœ‰å®žçŽ°ä»»ä½•æŽ¥å£ï¼Œå› æ­¤æ— æ³•"精确捕获"。
 * è°ƒç”¨æ–¹ï¼ˆ{@code ruoyi-quartz} çš„ {@code RyTask})需要把
 * <b>"模块被关掉"</b>和<b>"采集过程真的报错"</b>区分开:
 * å‰è€…只打一条 WARN è¯´æ˜Žæœ¬è½®è·³è¿‡ï¼ŒåŽè€…要按原有错误口径记录 â€”—
 * ç”¨ {@code ServiceException} æˆ–比对 message æ–‡æœ¬éƒ½ä¼šå¤±å‡†ã€‚
 *
 * <h3>为什么放在模块的 api åŒ…,而不是 ruoyi-common</h3>
 * æ¨¡æ¿æ¨¡å—把 {@code TemplateModuleDisabledException} æ”¾è¿› ruoyi-common,
 * æ˜¯å› ä¸ºå®ƒçš„调用点散在 4 ä¸ªæ¨¡å—里。本模块目前只有 {@code ruoyi-quartz} ä¸€ä¸ªè°ƒç”¨æ–¹ï¼Œ
 * è€Œ {@code ruoyi-quartz} å·²ç»ä¾èµ–本模块,所以放在 {@code api} åŒ…即可(api æ˜¯æœ¬æ¨¡å—唯一对外包)。
 * å°†æ¥è‹¥å‡ºçŽ°ä¸ä¾èµ–æœ¬æ¨¡å—çš„è°ƒç”¨æ–¹ï¼Œå†ä¸‹æ²‰åˆ° ruoyi-common。
 *
 * @see DataCollectionFacade#requireEnabled()
 */
public class DataCollectionModuleDisabledException extends RuntimeException {
    private static final long serialVersionUID = 1L;
    /**
     * å…³é—­æç¤ºï¼ˆä¸Ž {@code sys_config} é‡Œé‚£è¡Œé…ç½®çš„ remark å£å¾„一致)
     */
    public static final String DISABLED_MESSAGE =
            "数据采集模块未启用(参数配置 datacollection.enabled=false),本次采集已跳过";
    public DataCollectionModuleDisabledException(String message) {
        super(message);
    }
    public DataCollectionModuleDisabledException(String message, Throwable cause) {
        super(message, cause);
    }
}
ruoyi-datacollection/src/main/java/com/ruoyi/datacollection/config/DataCollectionProperties.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
package com.ruoyi.datacollection.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
 * æ•°æ®é‡‡é›†æ¨¡å—çš„ yml é…ç½®ï¼ˆ<b>仅作兜底,不是主开关</b>)。
 *
 * <h3>⚠️ ä¸»å¼€å…³ä¸åœ¨è¿™é‡Œ</h3>
 * ä¸»å¼€å…³æ˜¯ {@code sys_config} å‚数配置表里的 {@code datacollection.enabled},
 * ç”± {@link DataCollectionSwitch} <b>运行时热读</b>(进程内 10 ç§’缓存,改完最多 10 ç§’生效)。
 * æœ¬ç±»åªåœ¨ã€Œsys_config é‡Œæ²¡æœ‰é‚£ä¸€è¡Œã€æˆ–「非 Spring çŽ¯å¢ƒå–ä¸åˆ°é…ç½®æœåŠ¡ã€æ—¶å…œåº•ã€‚
 *
 * <h3>为什么默认 true</h3>
 * ä¸Žä»»åŠ¡/电话/模板模块同款口径:<b>参数行缺失时按启用处理</b>。
 * HIS é‡‡é›†æ˜¯"数据进不来"的链路,若因为库里少一行配置就默认停采,
 * å‡ºé—®é¢˜æ—¶æ˜¯<b>静默少数据</b>,比多采一轮难查得多。要停采请到
 * ã€Œç³»ç»Ÿç®¡ç† â†’ å‚数设置」显式把 {@code datacollection.enabled} è®¾ä¸º false。
 *
 * <h3>yml å…œåº•写法(一般不必要)</h3>
 * <pre>
 * datacollection:
 *   enabled: false
 * </pre>
 */
@Data
@Component
@ConfigurationProperties(prefix = "datacollection")
public class DataCollectionProperties {
    /**
     * æ˜¯å¦å¯ç”¨æ•°æ®é‡‡é›†æ¨¡å—(默认启用)。
     * <p>
     * ä»…当 {@code sys_config} ä¸­æ²¡æœ‰ {@code datacollection.enabled} æ—¶ç”Ÿæ•ˆï¼Œè§ {@link DataCollectionSwitch}。
     */
    private boolean enabled = true;
}
ruoyi-datacollection/src/main/java/com/ruoyi/datacollection/config/DataCollectionSwitch.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,204 @@
package com.ruoyi.datacollection.config;
import com.ruoyi.common.core.service.IConfigService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
 * æ•°æ®é‡‡é›†æ¨¡å—<b>总开关</b>:值存放在 {@code sys_config} å‚数配置表中,<b>运行时读取、可热切换</b>。
 *
 * <h3>参数配置</h3>
 * <pre>
 * å‚数键名:datacollection.enabled
 * å‚数键值:true  = å¯ç”¨ï¼ˆé»˜è®¤ï¼Œç­‰ä»·äºŽä¸é…ç½®è¿™ä¸€è¡Œï¼‰
 *          false = åœé‡‡
 * </pre>
 * ä½ç½®ï¼šç³»ç»Ÿç®¡ç† â†’ å‚数设置 â†’ æ–°å¢ž/修改。
 * <pre>
 * insert into sys_config (config_name, config_key, config_value, config_type, create_by, create_time, remark)
 * values ('数据采集模块开关', 'datacollection.enabled', 'true', 'Y', 'admin', sysdate(), 'true启用,false停采(默认true)');
 * </pre>
 *
 * <h3>取值优先级</h3>
 * <ol>
 *   <li>{@code sys_config} é‡Œ {@code datacollection.enabled} çš„值(<b>主用</b>,
 *       æ¥è‡ª {@code IConfigService} æŽ¥å£ï¼Œå®žçŽ°åœ¨ ruoyi-system çš„ {@code SysConfigServiceImpl});</li>
 *   <li>{@link DataCollectionProperties#isEnabled()}(<b>兜底</b>:仅当 sys_config é‡Œæ²¡æœ‰è¿™ä¸€è¡Œã€
 *       æˆ–非 Spring çŽ¯å¢ƒå–ä¸åˆ°é…ç½®æœåŠ¡æ—¶ä½¿ç”¨ï¼Œé»˜è®¤ true)。</li>
 * </ol>
 *
 * <h3>为什么读不到配置时按 true(fail-open)</h3>
 * è¯» {@code sys_config} éœ€è¦èµ°æ•°æ®åº“,数据库抖动时如果 fail-closed,
 * ç»“果就是"数据库抖一下,当天 HIS æ•°æ®å…¨éƒ½ä¸é‡‡äº†" â€”— é™é»˜å°‘数据。
 * è¿™é‡Œåªæ‰“ WARN å¹¶æ”¾è¡Œã€‚要停采必须显式配置 false。
 *
 * <h3>为什么有 10 ç§’缓存</h3>
 * {@code SysConfigServiceImpl#selectConfigByKey} å½“前每次都查库,而本开关在每轮定时采集
 * å¼€å§‹æ—¶éƒ½ä¼šè¢«è¯»åˆ°ã€‚这里加一个进程内 TTL ç¼“存,把查库降到"每个实例每 10 ç§’最多一次"。
 * ä»£ä»·æ˜¯å‚数改动最多 10 ç§’生效(多实例部署时各自计时)。
 */
@Slf4j
@Component
public class DataCollectionSwitch {
    /**
     * sys_config é‡Œçš„参数键名
     */
    public static final String CONFIG_KEY = "datacollection.enabled";
    /**
     * è¿›ç¨‹å†…缓存有效期(毫秒)
     */
    private static final long CACHE_TTL_MILLIS = 10_000L;
    private final DataCollectionProperties properties;
    /**
     * å‚数配置服务(ruoyi-common çš„æŽ¥å£ï¼Œå®žçŽ°åœ¨ ruoyi-system)。
     * <p>
     * ç”¨ {@code ObjectProvider} æƒ°æ€§å–:非 Spring çŽ¯å¢ƒ/独立部署等"容器里没有实现"的场景下
     * ä¸èƒ½å› æ­¤å¯åŠ¨å¤±è´¥ã€‚
     */
    private final ObjectProvider<IConfigService> configServiceProvider;
    /**
     * æœ€è¿‘一次解析结果
     */
    private volatile Boolean cachedEnabled;
    /**
     * æœ€è¿‘一次解析的时间戳
     */
    private volatile long cachedAt;
    /**
     * Spring ä½¿ç”¨çš„æž„造器。
     * <p>
     * âš ï¸ <b>必须显式标注 {@code @Autowired}</b>:本类有两个公开构造器
     * ï¼ˆå¦ä¸€ä¸ªç»™æµ‹è¯•/非 Spring çŽ¯å¢ƒç”¨ï¼‰ï¼Œä¸æ ‡æ³¨çš„è¯ Spring ä¼šå› "构造器不唯一"
     * é€€å›žæŸ¥æ‰¾æ— å‚构造器,启动即报 {@code No default constructor found}。
     * è¯¥è§„则已由 {@code sql/_probe/verify_spring_beans.py} é™æ€æŠŠå…³ã€‚
     */
    @Autowired
    public DataCollectionSwitch(DataCollectionProperties properties,
                                ObjectProvider<IConfigService> configServiceProvider) {
        this.properties = properties;
        this.configServiceProvider = configServiceProvider;
    }
    /**
     * ä¾›<b>测试/非 Spring çŽ¯å¢ƒ</b>直接构造:{@code configValue} å°±æ˜¯ sys_config é‡Œçš„键值,
     * ä¼  {@code null} è¡¨ç¤º"参数行不存在"(此时回落到 {@link DataCollectionProperties})。
     */
    public DataCollectionSwitch(DataCollectionProperties properties, final String configValue) {
        this(properties, new FixedProvider(configValue));
        // éž Spring çŽ¯å¢ƒä¸‹æ²¡æœ‰"改配置后等 10 ç§’"的概念,直接禁用缓存,保证断言即时可见
        this.cachedEnabled = resolve();
        this.cachedAt = System.currentTimeMillis() + CACHE_TTL_MILLIS * 1000L;
    }
    /**
     * æ•°æ®é‡‡é›†æ¨¡å—是否启用(运行时热读,最多 10 ç§’延迟)
     */
    public boolean isEnabled() {
        long now = System.currentTimeMillis();
        Boolean cached = this.cachedEnabled;
        if (cached != null && now - this.cachedAt < CACHE_TTL_MILLIS) {
            return cached;
        }
        boolean resolved = resolve();
        this.cachedEnabled = resolved;
        this.cachedAt = now;
        return resolved;
    }
    /**
     * æ¸…掉进程内缓存,下次调用立即重新读 sys_config(供测试/需要立即生效的场景)
     */
    public void invalidateCache() {
        this.cachedEnabled = null;
    }
    /**
     * è§£æžå½“前开关值:sys_config ä¼˜å…ˆï¼Œå…¶æ¬¡ yml å…œåº•
     */
    private boolean resolve() {
        String value = readFromSysConfig();
        if (value != null) {
            if (!"true".equalsIgnoreCase(value) && !"false".equalsIgnoreCase(value)) {
                log.warn("【数据采集】sys_config ä¸­ {} çš„值不是 true/false(实际=「{}」),将按「不是 false å³å¯ç”¨ã€å¤„理",
                        CONFIG_KEY, value);
            }
            boolean enabled = !"false".equalsIgnoreCase(value);
            log.debug("【数据采集】开关取自 sys_config:{}={} -> enabled={}", CONFIG_KEY, value, enabled);
            return enabled;
        }
        boolean enabled = properties.isEnabled();
        log.debug("【数据采集】sys_config ä¸­æœªé…ç½® {},回落 datacollection.enabled={}", CONFIG_KEY, enabled);
        return enabled;
    }
    /**
     * è¯» sys_config çš„原始键值;没有实现、没有这一行、或读库失败都返回 {@code null}
     */
    private String readFromSysConfig() {
        if (configServiceProvider == null) {
            return null;
        }
        try {
            IConfigService configService = configServiceProvider.stream().findFirst().orElse(null);
            if (configService == null) {
                return null;
            }
            String value = configService.selectConfigByKey(CONFIG_KEY);
            if (value == null || value.trim().isEmpty()) {
                return null;
            }
            return value.trim();
        } catch (Exception e) {
            // å®¹å™¨æ²¡æœ‰å®žçŽ°ã€æ•°æ®æºå¼‚å¸¸ã€SQL æŠ¥é”™â€¦â€¦ä¸€å¾‹æŒ‰"参数缺失"处理(fail-open),绝不影响采集主流程
            log.warn("【数据采集】读取 sys_config çš„ {} å¤±è´¥ï¼Œæœ¬æ¬¡æŒ‰æœªé…ç½®å¤„理(fail-open):{}",
                    CONFIG_KEY, e.toString());
            return null;
        }
    }
    /**
     * å›ºå®šå€¼çš„ {@code ObjectProvider}:只用于非 Spring çŽ¯å¢ƒ/测试
     */
    private static final class FixedProvider implements ObjectProvider<IConfigService> {
        private final IConfigService configService;
        private FixedProvider(final String configValue) {
            this.configService = configValue == null ? null : configKey -> configValue;
        }
        @Override
        public IConfigService getObject(Object... args) {
            return configService;
        }
        @Override
        public IConfigService getIfAvailable() {
            return configService;
        }
        @Override
        public IConfigService getIfUnique() {
            return configService;
        }
        @Override
        public IConfigService getObject() {
            return configService;
        }
        @Override
        public java.util.stream.Stream<IConfigService> stream() {
            return configService == null ? java.util.stream.Stream.empty() : java.util.stream.Stream.of(configService);
        }
    }
}
ruoyi-datacollection/src/main/java/com/ruoyi/datacollection/service/IHisDataCollectionService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,17 @@
package com.ruoyi.datacollection.service;
/**
 * HIS æ•°æ®é‡‡é›†çš„调度编排(模块内接口)。
 * <p>
 * å¯¹å¤–只经 {@code com.ruoyi.datacollection.api.DataCollectionFacade} æš´éœ²ï¼Œ
 * å…¶å®ƒæ¨¡å—不要直接注入本接口。
 */
public interface IHisDataCollectionService {
    /**
     * HIS æ•°æ®é‡‡é›†ï¼ˆåŽŸ {@code RyTask#dealHisData(String)},逻辑一行未改)。
     *
     * @param todayZeroStr é‡‡é›†åŸºå‡†æ—¶é—´ {@code yyyy-MM-dd HH:mm:ss};为空时取"今天 00:00"
     */
    void dealHisData(String todayZeroStr);
}
ruoyi-datacollection/src/main/java/com/ruoyi/datacollection/service/IInhospDeptRefreshService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
package com.ruoyi.datacollection.service;
/**
 * åœ¨é™¢æ‚£è€…"科室 / ç—…区 / åºŠä½"每日刷新(本模块内的编排接口)。
 * <p>
 * å¯¹å¤–只经 {@code com.ruoyi.datacollection.api.DataCollectionFacade} æš´éœ²ï¼Œ
 * å…¶å®ƒæ¨¡å—不要直接注入本接口。
 *
 * <h3>为什么单独一个 Service,而不是塞进 {@link IHisDataCollectionService}</h3>
 * {@code HisDataCollectionServiceImpl} æ˜¯"从 {@code RyTask#dealHisData} åŽŸæ ·æ¬å…¥ã€
 * åˆ»æ„é›¶æ”¹åЍ"的搬运件(连日志错别字都保留),本次是<b>新增能力</b>:
 * æ··è¿›åŽ»ä¼šè®©é‚£ä»½"可与搬迁前逐行比对"的证据失效。故另起一个类,各管一段。
 */
public interface IInhospDeptRefreshService {
    /**
     * åˆ·æ–°åœ¨é™¢æ‚£è€…的科室/病区/床位(南华附一,按患者逐条核对)。
     * <p>
     * æ•°æ®æºæ˜¯"入院采集用的那个健康记录接口"(按 {@code serialnum} å•查 + åœ¨é™¢çŠ¶æ€è¿‡æ»¤ï¼‰ï¼Œ
     * è¿”回的是该患者<b>当前</b>位置;已不在院的患者会被跳过,库内位置保持不动。
     * <p>
     * å¹‚等:只写"与库内不一致"的字段,重跑无害。
     *
     * @return å®žé™…发生更新的记录条数(0 è¡¨ç¤ºæ²¡å˜åŒ–、站点不匹配或本轮被跳过)
     */
    int refreshInhospDeptInfo();
}
ruoyi-datacollection/src/main/java/com/ruoyi/datacollection/service/impl/HisDataCollectionServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,341 @@
package com.ruoyi.datacollection.service.impl;
import com.ruoyi.common.core.service.HisGatherApi;
import com.ruoyi.common.core.service.dto.SltdGatherReq;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.datacollection.service.IHisDataCollectionService;
import com.ruoyi.system.domain.SysConfig;
import com.ruoyi.system.service.ISysConfigService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
/**
 * HIS æ•°æ®é‡‡é›†è°ƒåº¦ç¼–排实现 â€”—
 * <b>由 {@code ruoyi-quartz} çš„ {@code RyTask#dealHisData} + {@code collectSegmentData} åŽŸæ ·æ¬å…¥</b>。
 *
 * <h3>搬运纪律(本类刻意"零改动")</h3>
 * <ul>
 *   <li>站点分支({@code xh} / {@code hn} / {@code sltd}-{@code nhfy})、时间窗口算法、
 *       30 å¤©åˆ†æ®µå›žæº¯çš„æ®µé•¿ä¸Žé¡ºåºã€æ¯ä¸ªé‡‡é›†åŠ¨ä½œçš„å…ˆåŽã€å¼‚å¸¸åžæŽ‰çš„èŒƒå›´ã€æ—¥å¿—æ–‡æ¡ˆ
 *       â€”— å…¨éƒ¨ä¸Žæ¬è¿å‰ä¸€è‡´ï¼›</li>
 *   <li>唯一的形式变化:内部 VO({@code ServiceSLTDInhospReqVO} / {@code ServiceSLTDDeptReqVO} /
 *       {@code HnDataGatherVO})换成契约 DTO {@link SltdGatherReq} / ä¸¤ä¸ª {@code Date},
 *       ç”± smartor ä¾§ Bridge é€å­—段搬回原 VO;字段设置顺序与"显式置 null"都保留;
 *   <li>{@code CollectionUtils.isNotEmpty(...)} æ¢æˆç­‰ä»·çš„ {@code list != null && !list.isEmpty()}
 *       ï¼ˆé¿å…ä¸ºä¸€æ¬¡åˆ¤ç©ºç»™æ¨¡å—引 commons-collections4);</li>
 *   <li>{@code sys_config} çš„读法<b>刻意保持</b> {@code selectConfigList(条件)} + å–第 0 è¡Œ
 *       ï¼ˆå«åŽŸå®žçŽ°çš„ LIKE åŒ¹é…ä¸Ž"不排序取首行"语义),没有改成 {@code selectConfigByKey}
 *       â€”— é‚£æ˜¯"顺手优化",会改变查询语义,回归时无法逐条比对。</li>
 * </ul>
 *
 * <h3>它不管什么</h3>
 * çœŸæ­£çš„采集动作(HTTP å–数、入库)不在本类:那里仍在 smartor,经
 * {@link HisGatherApi} å¥‘约调用,后续分批搬进本模块。
 *
 * @see com.ruoyi.datacollection.api.DataCollectionFacade å¯¹å¤–门面
 */
@Slf4j
@Service
public class HisDataCollectionServiceImpl implements IHisDataCollectionService {
    /**
     * çœç«‹åŒå¾· / å—华附一的机构ID(原实现里的字面量 {@code "20001001"})
     */
    private static final String SLTD_ORG_ID = "20001001";
    /**
     * å½“时站点标识(原 {@code RyTask} çš„ {@code @Value("${spring.profiles.active}")})
     */
    @Value("${spring.profiles.active}")
    private String active;
    /**
     * HIS é‡‡é›†èƒ½åŠ›å¥‘çº¦ï¼ˆå½“å‰å®žçŽ°åœ¨ smartor:纯转发到既有 Service)。
     * <p>
     * âš ï¸ åˆ»æ„<b>直接注入</b>而不是 {@code ObjectProvider} æƒ°æ€§å–:
     * é‡‡é›†å®žçŽ°æ˜¯æ•°æ®é“¾è·¯çš„å¿…éœ€ä»¶ï¼Œ"取不到就静默不采"正是最危险的失败形态,
     * è¿™é‡Œè®©å®ƒå¯åŠ¨æœŸå°±æš´éœ²ï¼ˆå®å¯å¯åŠ¨å¤±è´¥ï¼Œä¹Ÿä¸è¦é™é»˜å°‘æ•°æ®ï¼‰ã€‚
     */
    @Autowired
    private HisGatherApi hisGatherApi;
    /**
     * å‚数配置服务(ruoyi-system çš„ API):读 {@code sltd.gather.sysuser} / {@code sltd.gather.campusid}
     */
    @Autowired
    private ISysConfigService configService;
    /**
     * HIS数据采集
     * å®šæ—¶ä»»åŠ¡ï¼šé‡‡é›†æ‚£è€…ä¿¡æ¯ã€å‡ºé™¢ã€é—¨è¯Šæ•°æ®
     *
     * @param todayZeroStr å½“前的时间   ä¾‹å¦‚:2026-05-06 00:00:00
     */
    @Override
    public void dealHisData(String todayZeroStr) {
        log.info("【dealHisData】开始执行HIS数据采集任务");
        // èŽ·å–æ˜¨å¤©0点到今天0点的时间范围
        LocalDateTime todayZero = LocalDateTime.now().with(LocalTime.MIN);
        if (StringUtils.isNotBlank(todayZeroStr)) {
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
            todayZero = LocalDateTime.parse(todayZeroStr, formatter);
        }
        LocalDateTime yesterdayZero = todayZero.minusDays(1);
        log.info("【dealHisData】采集时间范围:{} ~ {}", yesterdayZero, todayZero);
        if (active.trim().equals("xh")) {
            try {
                // é‡‡é›†å…¥é™¢æ•°æ®
                log.info("【dealHisData】新华开始采集入院数据");
                hisGatherApi.gatherXhInhospRecords("0", yesterdayZero, todayZero);
                log.info("【dealHisData】新华入院数据采集完成");
                // é‡‡é›†å‡ºé™¢æ•°æ®
                log.info("【dealHisData】新华开始采集出院数据");
                hisGatherApi.gatherXhInhospRecords("1", yesterdayZero, todayZero);
                log.info("【dealHisData】新华出院数据采集完成");
            } catch (Exception e) {
                log.error("【dealHisData】新华数据采集异常", e);
            }
        } else if (active.trim().equals("hn")) {
            try {
                // æ²³å—数据采集
                log.info("【dealHisData】河南开始采集数据");
                Date startTime = Date.from(yesterdayZero.atZone(ZoneId.systemDefault()).toInstant());
                Date endTime = Date.from(todayZero.atZone(ZoneId.systemDefault()).toInstant());
                hisGatherApi.gatherHnRecords(startTime, endTime);
                log.info("【dealHisData】河南结束采集数据");
            } catch (Exception e) {
                log.error("【dealHisData】河南数据采集异常", e);
            }
        } else if (active.trim().equals("sltd") || active.trim().equals("nhfy")) {
            //省立同德与华南附一用同一套采集方法(都是来未来提供)
            try {
                //获取是否需要采集用户、部门信息
                SysConfig config = new SysConfig();
                config.setConfigKey("sltd.gather.sysuser");
                config.setOrgid(SLTD_ORG_ID);
                List<SysConfig> sysConfigs = configService.selectConfigList(config);
                //获取省立同德要采集的院区
                SysConfig cf = new SysConfig();
                cf.setConfigKey("sltd.gather.campusid");
                cf.setOrgid(SLTD_ORG_ID);
                List<SysConfig> sysConfigcampusids = configService.selectConfigList(cf);
                List<Long> campusidList = new ArrayList<>();
                if (sysConfigcampusids != null && !sysConfigcampusids.isEmpty()) {
                    String[] split = sysConfigcampusids.get(0).getConfigValue().split(",");
                    campusidList = Arrays.stream(split).map(Long::valueOf).collect(Collectors.toList());
                } else {
                    log.error("SysConfig中的院区ID为空!");
                    return;
                }
                //configValue = 1 ä¸éœ€è¦é‡‡é›†       configValue = 2需要采集
                if (sysConfigs != null && !sysConfigs.isEmpty() && sysConfigs.get(0).getConfigValue().equals("2")) {
                    log.info("【dealHisData】省立同德“部门信息”开始采集数据");
                    SltdGatherReq serviceSLTDDeptReqVO = new SltdGatherReq();
                    serviceSLTDDeptReqVO.setOrgId(SLTD_ORG_ID);
                    serviceSLTDDeptReqVO.setCampusIds(campusidList);
                    Boolean aBoolean1 = hisGatherApi.gatherSltdDeptWardArea(serviceSLTDDeptReqVO);
                    log.info("【dealHisData】省立同德“部门信息”结束采集数据:{}", aBoolean1);
                    Boolean aBoolean = false;
                    for (Long cid : campusidList) {
                        log.info("【dealHisData】省立同德“用户信息”开始采集数据,院区:{}", cid);
                        SltdGatherReq reqVO = new SltdGatherReq();
                        reqVO.setOrgId(SLTD_ORG_ID);
                        reqVO.setCampusId(cid);
                        ArrayList<String> statusList = new ArrayList<>();
                        statusList.add("1");
                        reqVO.setStatusList(statusList);
                        aBoolean = hisGatherApi.gatherSltdHospUser(reqVO);
                    }
                    log.info("【dealHisData】省立同德“用户信息”结束采集数据:{}", aBoolean);
                    //疾病字典采集
                    hisGatherApi.gatherIcd10();
                }
                // nhfy/sltd回溯30天分段采集判断(每段最多4天)
                boolean isBackfill = (active.trim().equals("nhfy") || active.trim().equals("sltd")) && StringUtils.isNotBlank(todayZeroStr);
                if (isBackfill) {
                    DateTimeFormatter dateFmt = DateTimeFormatter.ofPattern("yyyy-MM-dd");
                    LocalDate segStart = todayZero.toLocalDate().minusDays(30);
                    LocalDate segEndDate = todayZero.toLocalDate();
                    int segCount = 0;
                    while (!segStart.isAfter(segEndDate)) {
                        LocalDate segEnd = segStart.plusDays(3);
                        if (segEnd.isAfter(segEndDate)) {
                            segEnd = segEndDate;
                        }
                        segCount++;
                        log.info("【dealHisData】{}回溯第{}段采集,日期范围:{} ~ {}", active.trim(), segCount, segStart.format(dateFmt), segEnd.format(dateFmt));
                        for (Long cid : campusidList) {
                            collectSegmentData(cid, segStart.format(dateFmt), segEnd.format(dateFmt));
                        }
                        segStart = segEnd.plusDays(1);
                    }
                    log.info("【dealHisData】{}回溯采集完成,共{}段", active.trim(), segCount);
                }
                // çœç«‹åŒå¾·æ•°æ®é‡‡é›†ï¼ˆéžå›žæº¯æ¨¡å¼æ‰æ‰§è¡Œï¼‰
                if (!isBackfill) {
                    for (Long cid : campusidList) {
                        log.info("【dealHisData】省立同德预入院开始采集数据,院区:{}", cid);
                        String nowTime = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
                        SltdGatherReq serviceSLTDInhospReqVO = new SltdGatherReq();
                        serviceSLTDInhospReqVO.setOrgId(SLTD_ORG_ID);
                        serviceSLTDInhospReqVO.setCampusId(cid);
                        serviceSLTDInhospReqVO.setEncounterTimeStart(nowTime);
                        serviceSLTDInhospReqVO.setEncounterTimeEnd(nowTime);
                        if (active.trim().equals("nhfy")) {
                            serviceSLTDInhospReqVO.setEncounterTimeStart(yesterdayZero.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
                            serviceSLTDInhospReqVO.setEncounterTimeEnd(todayZero.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
                        }
                        List<String> list = new ArrayList<>();
                        list.add("FH0109.22");
                        list.add("FH0109.23");
                        list.add("FH0109.53");
                        serviceSLTDInhospReqVO.setStatusList(list);
//                        log.info("【dealHisData】省立同德“预入院”采集数据入参:{}", serviceSLTDInhospReqVO);
                        hisGatherApi.gatherSltdHealthcareRecords(serviceSLTDInhospReqVO);
                        log.info("【dealHisData】省立同德“预入院”結束采集数据,院区:{}", cid);
                        log.info("【dealHisData】省立同德“入院”开始采集数据,院区:{}", cid);
                        serviceSLTDInhospReqVO.setOrgId(SLTD_ORG_ID);
                        serviceSLTDInhospReqVO.setCampusId(cid);
                        serviceSLTDInhospReqVO.setEncounterTimeStart(null);
                        serviceSLTDInhospReqVO.setEncounterTimeEnd(null);
                        serviceSLTDInhospReqVO.setStartHeadTime(nowTime);
                        serviceSLTDInhospReqVO.setStartTailTime(nowTime);
                        if (active.trim().equals("nhfy")) {
                            serviceSLTDInhospReqVO.setStartHeadTime(yesterdayZero.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
                            serviceSLTDInhospReqVO.setStartTailTime(todayZero.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
                        }
                        List<String> listStr = new ArrayList<>();
                        listStr.add("FH0109.24");
                        serviceSLTDInhospReqVO.setStatusList(listStr);
//                        log.info("【dealHisData】省立同德“入院”采集数据入参:{}", serviceSLTDInhospReqVO);
                        hisGatherApi.gatherSltdHealthcareRecords(serviceSLTDInhospReqVO);
                        log.info("【dealHisData】省立同德“入院”結束采集数据,院区:{}", cid);
                        log.info("【dealHisData】省立同德“出院”开始采集数据,院区:{}", cid);
                        serviceSLTDInhospReqVO.setStartHeadTime(null);
                        serviceSLTDInhospReqVO.setStartTailTime(null);
                        serviceSLTDInhospReqVO.setEncounterTimeStart(null);
                        serviceSLTDInhospReqVO.setEncounterTimeEnd(null);
                        serviceSLTDInhospReqVO.setPreOutHospitalHeadDate(nowTime);
                        serviceSLTDInhospReqVO.setPreOutHospitalTailDate(nowTime);
                        if (active.trim().equals("nhfy")) {
                            serviceSLTDInhospReqVO.setPreOutHospitalHeadDate(yesterdayZero.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
                            serviceSLTDInhospReqVO.setPreOutHospitalTailDate(todayZero.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
                        }
                        serviceSLTDInhospReqVO.setHealthcareRecordTypeList(null);
                        List<String> list2 = new ArrayList<>();
//                    list2.add("FH0108.02");
                        //出院
                        list2.add("FH0109.27");
                        //预出院
                        list2.add("FH0109.25");
                        serviceSLTDInhospReqVO.setStatusList(list2);
//                        log.info("【dealHisData】省立同德“出院”采集数据入参:{}", serviceSLTDInhospReqVO);
                        hisGatherApi.gatherSltdHealthcareRecords(serviceSLTDInhospReqVO);
                        log.info("【dealHisData】省立同德“出院”束采集数据,院区:{}", cid);
                    }
                }
            } catch (Exception e) {
                log.error("【dealHisData】省立同德数据采集异常", e);
            }
        }
    }
    /**
     * åˆ†æ®µé‡‡é›†æ•°æ®ï¼ˆé¢„入院、入院、出院)
     * ç”¨äºŽnhfy/sltd回溯采集,每次最多查询7天数据
     *
     * @param cid          é™¢åŒºID
     * @param startDateStr æ—¥æœŸå¼€å§‹ yyyy-MM-dd
     * @param endDateStr   æ—¥æœŸç»“束 yyyy-MM-dd
     */
    private void collectSegmentData(Long cid, String startDateStr, String endDateStr) {
//        // é¢„入院
        log.info("【dealHisData】预入院开始采集数据,院区:{},时间段:{} ~ {}", cid, startDateStr, endDateStr);
        SltdGatherReq reqVO = new SltdGatherReq();
        reqVO.setOrgId(SLTD_ORG_ID);
        reqVO.setCampusId(cid);
        reqVO.setEncounterTimeStart(startDateStr);
        reqVO.setEncounterTimeEnd(endDateStr);
        List<String> preAdmitStatus = new ArrayList<>();
        preAdmitStatus.add("FH0109.22");
        preAdmitStatus.add("FH0109.23");
        preAdmitStatus.add("FH0109.53");
        reqVO.setStatusList(preAdmitStatus);
        log.info("【dealHisData】预入院采集数据入参:{}", reqVO);
        try {
            hisGatherApi.gatherSltdHealthcareRecords(reqVO);
        } catch (Exception e) {
            // å•类数据失败不再向上抛:否则一段失败会让本次回溯后面的所有段都不执行
            log.error("【dealHisData】预入院采集失败,跳过该类继续。院区:{},时间段:{} ~ {}", cid, startDateStr, endDateStr, e);
        }
        log.info("【dealHisData】预入院終了采集数据,院区:{}", cid);
        // å…¥é™¢
        log.info("【dealHisData】入院开始采集数据,院区:{},时间段:{} ~ {}", cid, startDateStr, endDateStr);
        reqVO = new SltdGatherReq();
        reqVO.setOrgId(SLTD_ORG_ID);
        reqVO.setCampusId(cid);
        reqVO.setStartHeadTime(startDateStr);
        reqVO.setStartTailTime(endDateStr);
        List<String> admitStatus = new ArrayList<>();
        admitStatus.add("FH0109.24");
        reqVO.setStatusList(admitStatus);
        log.info("【dealHisData】入院采集数据入参:{}", reqVO);
        try {
            hisGatherApi.gatherSltdHealthcareRecords(reqVO);
        } catch (Exception e) {
            // å•类数据失败不再向上抛:否则一段失败会让本次回溯后面的所有段都不执行
            log.error("【dealHisData】入院采集失败,跳过该类继续。院区:{},时间段:{} ~ {}", cid, startDateStr, endDateStr, e);
        }
        log.info("【dealHisData】入院終了采集数据,院区:{}", cid);
        // å‡ºé™¢
        log.info("【dealHisData】出院开始采集数据,院区:{},时间段:{} ~ {}", cid, startDateStr, endDateStr);
        reqVO = new SltdGatherReq();
        reqVO.setOrgId(SLTD_ORG_ID);
        reqVO.setCampusId(cid);
        reqVO.setPreOutHospitalHeadDate(startDateStr);
        reqVO.setPreOutHospitalTailDate(endDateStr);
        List<String> dischargeStatus = new ArrayList<>();
        //出院
        dischargeStatus.add("FH0109.27");
        //预出院
        dischargeStatus.add("FH0109.25");
        reqVO.setStatusList(dischargeStatus);
        log.info("【dealHisData】出院采集数据入参:{}", reqVO);
        try {
            hisGatherApi.gatherSltdHealthcareRecords(reqVO);
        } catch (Exception e) {
            // å•类数据失败不再向上抛:否则一段失败会让本次回溯后面的所有段都不执行
            log.error("【dealHisData】出院采集失败,跳过该类继续。院区:{},时间段:{} ~ {}", cid, startDateStr, endDateStr, e);
        }
        log.info("【dealHisData】出院结束采集数据,院区:{}", cid);
    }
}
ruoyi-datacollection/src/main/java/com/ruoyi/datacollection/service/impl/InhospDeptRefreshServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,393 @@
package com.ruoyi.datacollection.service.impl;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.core.service.HisGatherApi;
import com.ruoyi.common.core.service.dto.InhospPositionRecord;
import com.ruoyi.common.core.service.dto.SltdGatherReq;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.datacollection.service.IInhospDeptRefreshService;
import com.ruoyi.inhospvisit.api.InhospVisitFacade;
import com.ruoyi.inhospvisit.domain.PatMedInhosp;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
 * åœ¨é™¢æ‚£è€…"科室 / ç—…区 / åºŠä½"每日刷新 â€”— ç¼–排与"改不改"的判定都在本类。
 *
 * <h3>它解决什么</h3>
 * åœ¨é™¢æ‚£è€…会换科室、换病区、换床。{@code pat_med_inhosp} çš„位置字段只在"入院采集"时写一次,
 * ä¹‹åŽä¸ä¼šè‡ªå·±æ›´æ–° â€”— äºŽæ˜¯è½¬ç§‘/换床患者会一直停在入院当时的位置
 * ï¼ˆå®žæµ‹ 120 äººæŠ½æ ·ï¼š25 äººåºŠä½ä¸Ž HIS ä¸ä¸€è‡´ã€çº¦ 9% çš„æ‚£è€…当天已出院但库里仍标在院)。
 * æœ¬ç±»æ¯å¤©å…¨é‡æ ¸å¯¹ä¸€æ¬¡ï¼ŒæŠŠä½ç½®çº å›ž HIS çš„现状。
 *
 * <h3>取数口径({@code dtcQueryHealthcareRecordList},与入院采集同一个接口)</h3>
 * æŒ‰ {@code healthcareRecordNo}(= {@code serialnum})单查,配
 * {@code statusList=["FH0109.24"]}(在院),返回的是<b>该患者此刻的状态</b>:
 * <ul>
 *   <li>{@code data} æ° 1 æ¡ â†’ å½“前位置见 {@link InhospPositionRecord};</li>
 *   <li>{@code data} ä¸º {@code []} â†’ è¯¥æ‚£è€…当前<b>不在"入院"状态</b>
 *       ï¼ˆå®žæµ‹è¿™æ‰¹å°±æ˜¯"当天已出院、但库里还是在院"的患者:{@code FH0109.27}
 *       ä¸” {@code preOutHospitalDate} å°±åœ¨å‡ å°æ—¶å‰ï¼‰â†’ <b>跳过并计数,不动库内位置</b>;
 *       çŠ¶æ€æœ¬èº«ç”±å‡ºé™¢é‡‡é›†è´Ÿè´£ï¼Œè¿™é‡Œä¸æ”¹ {@code inhospstate}。</li>
 * </ul>
 * ä¸Ž"分段历史"接口相比,这条路没有"取最新一段"和"空入科时间"的坑,
 * ä¸” {@code bedNumber} åœ¨é™¢æ‚£è€…实测恒有值 â€”— æ‰€ä»¥"床位为空则清空"可以按字面执行。
 *
 * <h3>字段映射(与"入院采集"同源,故不会出现两套口径)</h3>
 * <pre>
 *   pat_med_inhosp.deptcode                &lt;- String.valueOf(deptId)
 *   pat_med_inhosp.deptname                &lt;- deptName
 *   pat_med_inhosp.hospitaldistrictcode    &lt;- String.valueOf(areaId)
 *   pat_med_inhosp.hospitaldistrictid      &lt;- String.valueOf(areaId)
 *   pat_med_inhosp.hospitaldistrictname    &lt;- areaName
 *   pat_med_inhosp.bed_no                  &lt;- bedNumber(为空则清空)
 * </pre>
 *
 * <h3>刻意不碰的字段</h3>
 * {@code starttime}(入院时间)、{@code endtime}、{@code drcode/drname}、
 * {@code roomno}、{@code deptid}、{@code schemestatus}、
 * {@code deptcheck_flag}/{@code wardcheck_flag} ç­‰ä»»åŠ¡æ ‡å¿—ã€‚
 * <b>也不新建"转科"记录</b>:{@code inhospstate} ä¿æŒ 0,只就地改位置。
 *
 * <h3>失败的处置</h3>
 * å•条失败只记日志并继续下一条;连续失败达到 {@link #MAX_CONSECUTIVE_FAIL} åˆ¤å®šæŽ¥å£ä¸å¯ç”¨ã€
 * æœ¬è½®æå‰ç»“束(与门急诊采集同款纪律),剩余患者下一轮重新全量核对,不会漏。
 *
 * @see com.ruoyi.datacollection.api.DataCollectionFacade å¯¹å¤–门面
 */
@Slf4j
@Service
public class InhospDeptRefreshServiceImpl implements IInhospDeptRefreshService {
    /**
     * ä»…南华附一启用:接口按站点配置 {@code sltd_pub_path} ä¸Ž {@code lwl_app_key}
     * ï¼ˆå—华的 app-key å®žæµ‹å¯ç”¨ï¼›å…¶ä»–站点未验证,故不做默认放开)
     */
    private static final String SITE_NHFY = "nhfy";
    /**
     * æœºæž„ID(与 {@code HisDataCollectionServiceImpl} / é‡‡é›†å®žçŽ°é‡Œçš„å­—é¢é‡ä¸€è‡´ï¼‰
     */
    private static final String ORG_ID = "20001001";
    /**
     * é™¢åŒºID:按约定固定传南华附一主院区(实测按 {@code healthcareRecordNo} å•查时
     * è¯¥å‚数不参与过滤,传 30001002 æˆ– 30038001 è¿”回的都是同一条记录)
     */
    private static final Long CAMPUS_ID = 30001002L;
    /**
     * åªå–"在院"(入院)状态的记录;已出院/预出院会被 HIS ä¾§è¿‡æ»¤æŽ‰ï¼ŒäºŽæ˜¯è¿”回空列表
     */
    private static final List<String> STATUS_IN_HOSP =
            Collections.unmodifiableList(Arrays.asList("FH0109.24"));
    /**
     * åœ¨é™¢çŠ¶æ€ç ï¼ˆç”¨äºŽæ ¡éªŒæŽ¥å£è¿”å›žçš„é‚£æ¡è®°å½•ç¡®å®žæ˜¯åœ¨é™¢ï¼‰
     */
    private static final String STATUS_CODE_IN_HOSP = "FH0109.24";
    /**
     * è¿è¡Œé”ï¼šsys_job å·²é…"禁止并发",这里再防一手"手工触发叠加定时任务"
     */
    private static final String RUN_LOCK_KEY = "datacollection:inhospDept:running";
    /**
     * è¿è¡Œé” TTL:一轮正常约 7~10 åˆ†é’Ÿï¼ˆå®žæµ‹ 152ms/次 Ã— çº¦ 2900 äººï¼‰ï¼Œç»™è¶³ä½™é‡
     */
    private static final long RUN_LOCK_MINUTES = 120L;
    /**
     * è¿žç»­å¤±è´¥é˜ˆå€¼ï¼šè¾¾åˆ°å³åˆ¤å®šæŽ¥å£ä¸å¯ç”¨ï¼Œæœ¬è½®æå‰ç»“束(避免几千条错误日志 + ç©ºè½¬ï¼‰
     */
    private static final int MAX_CONSECUTIVE_FAIL = 20;
    /**
     * ç«™ç‚¹æ ‡è¯†ï¼ˆ{@code @Value("${spring.profiles.active}")},与既有采集编排同款)
     */
    @Value("${spring.profiles.active}")
    private String active;
    /**
     * HIS é‡‡é›†èƒ½åŠ›å¥‘çº¦ï¼ˆsmartor ä¾§ Bridge çº¯è½¬å‘到 healthcareRecord æŸ¥è¯¢ï¼Œåªå–数不落库)
     */
    @Autowired
    private HisGatherApi hisGatherApi;
    /**
     * å‡ºå…¥é™¢æ•°æ®å¥‘约({@code pat_med_inhosp} å½’属 ruoyi-inhospvisit):
     * å–在院候选 + å†™å›žä½ç½®ã€‚刻意不碰该模块的 Mapper。
     */
    @Autowired
    private InhospVisitFacade inhospVisitFacade;
    /**
     * è¿è¡Œé”ï¼ˆruoyi-common çš„ RedisCache)
     */
    @Autowired
    private RedisCache redisCache;
    @Override
    public int refreshInhospDeptInfo() {
        String site = active == null ? "" : active.trim();
        if (!SITE_NHFY.equals(site)) {
            log.info("【在院信息刷新】当前站点 {} ä¸æ‰§è¡Œï¼ˆä»… nhfy å¯ç”¨è¯¥æŽ¥å£ï¼‰ï¼Œè·³è¿‡", active);
            return 0;
        }
        if (!tryLock()) {
            log.warn("【在院信息刷新】上一轮尚未结束(Redis é” {} è¿˜åœ¨ï¼‰ï¼Œæœ¬è½®è·³è¿‡", RUN_LOCK_KEY);
            return 0;
        }
        long startMs = System.currentTimeMillis();
        int changed = 0;
        int unchanged = 0;
        int notInHosp = 0;
        int skipped = 0;
        int failed = 0;
        int consecutiveFail = 0;
        boolean aborted = false;
        try {
            // åªæŸ¥ pat_med_inhosp è‡ªèº«ï¼ˆä¸ join æ¡£æ¡ˆè¡¨ã€ä¸æŒ‰è¯ä»¶å·è¿‡æ»¤ï¼‰ï¼Œåœ¨é™¢å£å¾„ inhospstate='0' and del_flag='0'
            List<PatMedInhosp> candidates = inhospVisitFacade.selectInhospRefreshCandidates(ORG_ID);
            if (candidates == null || candidates.isEmpty()) {
                log.warn("【在院信息刷新】未取到任何在院患者(orgid={}),本轮不做事", ORG_ID);
                return 0;
            }
            log.info("【在院信息刷新】开始:在院患者 {} äººï¼Œé€äººæ ¸å¯¹ç§‘室/病区/床位", candidates.size());
            for (PatMedInhosp row : candidates) {
                try {
                    Outcome outcome = refreshOne(row);
                    if (outcome == Outcome.CHANGED) {
                        changed++;
                    } else if (outcome == Outcome.UNCHANGED) {
                        unchanged++;
                    } else if (outcome == Outcome.NOT_IN_HOSP) {
                        notInHosp++;
                    } else {
                        skipped++;
                    }
                    consecutiveFail = 0;
                } catch (Exception e) {
                    failed++;
                    consecutiveFail++;
                    log.error("【在院信息刷新】单条失败已跳过:inhospid={}, serialnum={}, patname={}",
                            row.getInhospid(), row.getSerialnum(), row.getPatname(), e);
                    if (consecutiveFail >= MAX_CONSECUTIVE_FAIL) {
                        aborted = true;
                        log.error("【在院信息刷新】连续 {} äººå¤±è´¥ï¼Œåˆ¤å®šæŽ¥å£ä¸å¯ç”¨ï¼Œæœ¬è½®æå‰ç»“束(已更新 {} æ¡ï¼‰ï¼›"
                                + "剩余患者下一轮会重新全量核对,不会漏", consecutiveFail, changed);
                        break;
                    }
                }
            }
            log.info("【在院信息刷新】结束:在院 {} äººï¼Œæ›´æ–° {},无变化 {},已不在院 {},跳过 {},失败 {}{},耗时 {} ç§’",
                    candidates.size(), changed, unchanged, notInHosp, skipped, failed,
                    aborted ? "(接口连续失败,提前结束)" : "",
                    (System.currentTimeMillis() - startMs) / 1000);
            return changed;
        } finally {
            unlock();
        }
    }
    /**
     * æ ¸å¯¹å¹¶ï¼ˆå¿…要时)更新一个在院患者的位置。
     *
     * @return æœ¬æ¬¡å¯¹è¯¥è¡Œçš„处置结果
     */
    private Outcome refreshOne(PatMedInhosp row) {
        String serialnum = StringUtils.trim(row.getSerialnum());
        if (StringUtils.isBlank(serialnum)) {
            log.warn("【在院信息刷新】在院记录没有流水号,无法核对,跳过:inhospid={}", row.getInhospid());
            return Outcome.SKIPPED;
        }
        SltdGatherReq req = new SltdGatherReq();
        req.setOrgId(ORG_ID);
        req.setCampusId(CAMPUS_ID);
        req.setStatusList(STATUS_IN_HOSP);
        req.setHealthcareRecordNo(serialnum);
        List<InhospPositionRecord> records = hisGatherApi.querySltdInhospPosition(req);
        InhospPositionRecord current = pickInHospRecord(records, serialnum);
        if (current == null) {
            // æŽ¥å£æŒ‰åœ¨é™¢çŠ¶æ€è¿‡æ»¤åŽæ²¡æœ‰è¿”å›žè®°å½•ï¼šè¯¥æ‚£è€…å½“å‰å·²ä¸åœ¨"入院"状态。
            // è¿™é‡Œåªè®¡æ•°ã€ä¸æ”¹åº“(状态翻转是出院采集的职责),更不拿空值去覆盖位置。
            log.warn("【在院信息刷新】HIS ä¸­è¯¥æ‚£è€…已不在院(返回为空),位置保持不动,等出院采集处理状态:"
                    + "inhospid={}, serialnum={}, patname={}", row.getInhospid(), serialnum, row.getPatname());
            return Outcome.NOT_IN_HOSP;
        }
        return applyIfDiff(row, current) ? Outcome.CHANGED : Outcome.UNCHANGED;
    }
    /**
     * ä»Žè¿”回记录里取"在院"的那一条。
     * <p>
     * æ­£å¸¸åªä¼šè¿”回 1 æ¡ï¼›è¿™é‡Œä»åšä¸€å±‚防御:只接受状态为在院的记录,
     * ä¸€æ¡éƒ½ä¸åŒ¹é…åˆ™è¿”回 {@code null}(按"已不在院"处理),
     * ç»ä¸æ‹¿ä¸€æ¡çŠ¶æ€ä¸æ˜Žçš„è®°å½•åŽ»è¦†ç›–åº“å†…ä½ç½®ã€‚
     */
    private InhospPositionRecord pickInHospRecord(List<InhospPositionRecord> records, String serialnum) {
        if (records == null || records.isEmpty()) {
            return null;
        }
        for (InhospPositionRecord record : records) {
            if (record == null) {
                continue;
            }
            String status = StringUtils.trim(record.getHealthcareRecordStatus());
            if (STATUS_CODE_IN_HOSP.equals(status)) {
                return record;
            }
            log.warn("【在院信息刷新】返回记录状态不是在院,已忽略:serialnum={}, status={}", serialnum, status);
        }
        return null;
    }
    /**
     * ä¸Žåº“内值逐字段比对,只把"变化的字段"写回;没有任何变化则不动库(也不刷新 update_time)。
     *
     * @return æ˜¯å¦å®žé™…执行了更新
     */
    private boolean applyIfDiff(PatMedInhosp row, InhospPositionRecord current) {
        PatMedInhosp update = new PatMedInhosp();
        update.setInhospid(row.getInhospid());
        StringBuilder changedFields = new StringBuilder();
        boolean diff = false;
        // ç§‘室:有值才更新(口径:科室有值更新科室,病区有值更新病区,互不牵连)
        if (current.getDeptId() != null) {
            String deptCode = String.valueOf(current.getDeptId());
            String old = StringUtils.trimToEmpty(row.getDeptcode());
            if (!deptCode.equals(old)) {
                update.setDeptcode(deptCode);
                changedFields.append("deptcode[").append(old).append("→").append(deptCode).append("] ");
                diff = true;
            }
        }
        if (StringUtils.isNotBlank(current.getDeptName())) {
            String old = StringUtils.trimToEmpty(row.getDeptname());
            if (!current.getDeptName().equals(old)) {
                update.setDeptname(current.getDeptName());
                changedFields.append("deptname[").append(old).append("→").append(current.getDeptName()).append("] ");
                diff = true;
            }
        }
        // ç—…区:code ä¸Ž id åŒå€¼ï¼ˆæ—¢æœ‰é‡‡é›†å£å¾„就是两个字段都写 areaId)
        if (current.getAreaId() != null) {
            String areaId = String.valueOf(current.getAreaId());
            String oldCode = StringUtils.trimToEmpty(row.getHospitaldistrictcode());
            if (!areaId.equals(oldCode)) {
                update.setHospitaldistrictcode(areaId);
                changedFields.append("hospitaldistrictcode[").append(oldCode).append("→").append(areaId).append("] ");
                diff = true;
            }
            String oldAreaId = StringUtils.trimToEmpty(row.getHospitaldistrictid());
            if (!areaId.equals(oldAreaId)) {
                update.setHospitaldistrictid(areaId);
                changedFields.append("hospitaldistrictid[").append(oldAreaId).append("→").append(areaId).append("] ");
                diff = true;
            }
        }
        if (StringUtils.isNotBlank(current.getAreaName())) {
            String old = StringUtils.trimToEmpty(row.getHospitaldistrictname());
            if (!current.getAreaName().equals(old)) {
                update.setHospitaldistrictname(current.getAreaName());
                changedFields.append("hospitaldistrictname[").append(old).append("→").append(current.getAreaName()).append("] ");
                diff = true;
            }
        }
        // åºŠä½ï¼šæŒ‰ä¸šåŠ¡å£å¾„"接口给什么就是什么,为空则清空"。
        // âš ï¸ mapper é‡Œ"空串=ç½® null、null=不改动",所以清空必须显式传空串。
        String apiBed = StringUtils.trim(current.getBedNumber());
        String dbBed = StringUtils.trimToEmpty(row.getBedNo());
        if (StringUtils.isBlank(apiBed)) {
            if (StringUtils.isNotBlank(dbBed)) {
                update.setBedNo("");
                changedFields.append("bed_no[").append(dbBed).append("→(清空)] ");
                diff = true;
            }
        } else if (!apiBed.equals(dbBed)) {
            update.setBedNo(apiBed);
            changedFields.append("bed_no[").append(dbBed).append("→").append(apiBed).append("] ");
            diff = true;
        }
        if (!diff) {
            return false;
        }
        update.setUpdateTime(new Date());
        int rows = inhospVisitFacade.updatePatMedInhosp(update);
        if (rows <= 0) {
            log.warn("【在院信息刷新】更新未生效(影响行数 {}):inhospid={}, serialnum={}, å¾…改字段:{}",
                    rows, row.getInhospid(), row.getSerialnum(), changedFields);
            return false;
        }
        log.info("【在院信息刷新】已更新:inhospid={}, serialnum={}, patname={}, å˜æ›´ï¼š{}",
                row.getInhospid(), row.getSerialnum(), row.getPatname(), changedFields);
        return true;
    }
    /**
     * æŠ¢å æœ¬è½®è¿è¡Œé”ã€‚
     * <p>
     * Redis ä¸å¯ç”¨æ—¶<b>退化为无锁执行</b>:每天一次的数据刷新不该因为缓存故障整体不跑,
     * æœ€åæƒ…况是手工触发与定时任务叠着跑 â€”— è€Œæœ¬åŠŸèƒ½æ˜¯å¹‚ç­‰æ¯”å¯¹ï¼Œå è·‘åªä¼šå¤šæ‰“å‡ æ¡æ—¥å¿—ã€‚
     */
    private boolean tryLock() {
        try {
            if (Boolean.TRUE.equals(redisCache.hasKey(RUN_LOCK_KEY))) {
                return false;
            }
            redisCache.setCacheObject(RUN_LOCK_KEY, String.valueOf(System.currentTimeMillis()),
                    RUN_LOCK_MINUTES, TimeUnit.MINUTES);
            return true;
        } catch (Exception e) {
            log.warn("【在院信息刷新】Redis è¿è¡Œé”ä¸å¯ç”¨ï¼ˆ{}),本轮无锁执行", e.getMessage());
            return true;
        }
    }
    private void unlock() {
        try {
            redisCache.deleteObject(RUN_LOCK_KEY);
        } catch (Exception e) {
            log.warn("【在院信息刷新】释放运行锁失败:{}", e.getMessage());
        }
    }
    /**
     * å•条患者的处置结果
     */
    private enum Outcome {
        /**
         * ä½ç½®æœ‰å˜åŒ–并已写回
         */
        CHANGED,
        /**
         * ä¸Žåº“内一致,未改动
         */
        UNCHANGED,
        /**
         * HIS é‡Œè¯¥æ‚£è€…已不在"入院"状态(接口返回为空),库内位置保持不动
         */
        NOT_IN_HOSP,
        /**
         * æ‹¿ä¸åˆ°å¯ç”¨ä¿¡æ¯ï¼ˆæ— æµæ°´å· / è¿”回记录状态不对)
         */
        SKIPPED
    }
}
ruoyi-inhospvisit/src/main/java/com/ruoyi/inhospvisit/api/DefaultInhospVisitDataService.java
@@ -73,6 +73,11 @@
    }
    @Override
    public List<PatMedInhosp> selectInhospRefreshCandidates(String orgid) {
        return patMedInhospMapper.selectInhospRefreshCandidates(orgid);
    }
    @Override
    public List<PatMedInhosp> selectPatMedInhospListByCondition(PatMedInhospVO patMedInhospVO) {
        return patMedInhospMapper.selectPatMedInhospListByCondition(patMedInhospVO);
    }
ruoyi-inhospvisit/src/main/java/com/ruoyi/inhospvisit/api/InhospVisitDataService.java
@@ -99,6 +99,21 @@
    List<PatMedInhosp> selectPatMedInhospListBySerialnum(PatMedInhosp patMedInhosp);
    /**
     * æŸ¥è¯¢æŸæœºæž„下全部在院记录("在院信息刷新"的取数入口)。
     * <p>
     * åªæŸ¥ {@code pat_med_inhosp} è‡ªèº«ã€åªå–刷新要用的列(inhospid / serialnum / orgid /
     * campusid / ç§‘室三列 / ç—…区三列 / bed_no),<b>不 join æ¡£æ¡ˆè¡¨ã€ä¸æŒ‰è¯ä»¶å·è¿‡æ»¤</b> â€”—
     * å¦åˆ™è¯ä»¶å·æœªè¡¥å…¨çš„在院患者会被静默漏刷新。
     * <p>
     * åœ¨é™¢å£å¾„:{@code inhospstate='0' and del_flag='0'}(与门户"在院患者"列表一致,
     * å®žæµ‹åœ¨é™¢è¡Œ endtime æ’为 null)。
     *
     * @param orgid æœºæž„ID(如 20001001)
     * @return åœ¨é™¢è®°å½•集合,按 inhospid å‡åºï¼›æ— æ•°æ®è¿”回空集合
     */
    List<PatMedInhosp> selectInhospRefreshCandidates(String orgid);
    /**
     * æŒ‰ VO æ¡ä»¶æŸ¥è¯¢ï¼ˆæ”¯æŒå¤šç§‘室/多病区集合条件)。
     *
     * @param patMedInhospVO æŸ¥è¯¢æ¡ä»¶
ruoyi-inhospvisit/src/main/java/com/ruoyi/inhospvisit/api/InhospVisitFacade.java
@@ -433,6 +433,13 @@
    }
    @Override
    public java.util.List<com.ruoyi.inhospvisit.domain.PatMedInhosp> selectInhospRefreshCandidates(String orgid) {
        return dataUnavailable("selectInhospRefreshCandidates")
                ? java.util.Collections.<com.ruoyi.inhospvisit.domain.PatMedInhosp>emptyList()
                : dataService.selectInhospRefreshCandidates(orgid);
    }
    @Override
    public java.util.List<com.ruoyi.inhospvisit.domain.PatMedInhosp> selectPatMedInhospListByCondition(
            com.ruoyi.inhospvisit.domain.PatMedInhospVO patMedInhospVO) {
        return dataUnavailable("selectPatMedInhospListByCondition")
ruoyi-inhospvisit/src/main/java/com/ruoyi/inhospvisit/controller/InhospVisitDataController.java
@@ -11,6 +11,7 @@
import com.ruoyi.common.core.service.dto.PatMedReq;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.PageUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.inhospvisit.api.InhospVisitFacade;
import com.ruoyi.inhospvisit.domain.PatMedInhosp;
@@ -79,7 +80,10 @@
    /**
     * æŸ¥è¯¢æ‚£è€…住院记录列表。
     * <p>
     * åŽŸ {@code PatMedInhospController#selectPatMedInhosplist}(L77-87)逐行对应。
     * åŽŸ {@code PatMedInhospController#selectPatMedInhosplist}(L77-87)逐行对应,
     * å”¯ä¸€å¤šå‡ºæ¥çš„一行是 {@link #normalizeBedSort} â€”— è§è¯¥æ–¹æ³•注释:
     * åŽŸå®žçŽ°é‡Œè¿™æ®µå½’ä¸€åŒ–åœ¨ {@code PatMedInhospServiceImpl} é‡Œï¼Œè¿åˆ°æœ¬æ¨¡å—后这条链
     * ç›´æŽ¥è°ƒé—¨é¢ï¼Œå¿…须把它跟着搬过来,否则 {@code bedSort} é™é»˜å¤±æ•ˆã€‚
     */
    @ApiOperation("查询患者住院记录列表")
    //@PreAuthorize("@ss.hasPermi('smartor:patinhosp:list')")
@@ -89,6 +93,8 @@
        PageUtils.startPageByPost(patMedInhosp.getPageNum(), patMedInhosp.getPageSize());
        SysUser user = getLoginUser().getUser();
        patMedInhosp.setOrgid(user.getOrgid());
        // å‰ç«¯ä¼ çš„æ˜¯æ•°å­— 1 / 2,SQL åªè®¤ "asc" / "desc";不在这里归一就等于没传
        patMedInhosp.setBedSort(normalizeBedSort(patMedInhosp.getCry(), patMedInhosp.getBedSort()));
        List<PatMedInhosp> list = inhospVisitFacade.selectPatMedInhospList(patMedInhosp);
        // startPage ä¹‹åŽ PageHelper å·²ç»è‡ªåŠ¨æ‰§è¡Œè¿‡ä¸€æ¬¡ count æŸ¥è¯¢ï¼Œç›´æŽ¥å¤ç”¨ Page é‡Œçš„ total,
        // é¿å…ç”¨ PageUtils.count æŠŠå¸¦ JOIN çš„统计语句再跑一遍
@@ -172,12 +178,17 @@
     * å¯¼å‡ºæ‚£è€…住院记录列表。
     * <p>
     * åŽŸ {@code PatMedInhospController#export}(L165-173)逐行对应。
     * <p>
     * {@code bedSort} ä¹Ÿè¦å½’一:迁出前的 export åŒæ ·èµ°
     * {@code patMedInhospService.selectPatMedInhospList(...)}(见 {@code ccf5fc76} ç‰ˆæœ¬ï¼‰ï¼Œ
     * å½’一化是随那次调用生效的。不补这一行就会出现"列表按床号排好了、导出顺序又变回去"。
     */
    @ApiOperation("导出患者住院记录列表")
    //@PreAuthorize("@ss.hasPermi('smartor:patinhosp:export')")
    @Log(title = "患者住院记录", businessType = BusinessType.EXPORT)
    @PostMapping("/export")
    public void export(HttpServletResponse response, PatMedInhosp patMedInhosp) {
        patMedInhosp.setBedSort(normalizeBedSort(patMedInhosp.getCry(), patMedInhosp.getBedSort()));
        List<PatMedInhosp> list = inhospVisitFacade.selectPatMedInhospList(patMedInhosp);
        ExcelUtil<PatMedInhosp> util = new ExcelUtil<PatMedInhosp>(PatMedInhosp.class);
        util.exportExcel(response, list, "患者住院记录数据");
@@ -237,4 +248,50 @@
    public AjaxResult remove(@PathVariable Long[] inhospids) {
        return toAjax(inhospVisitFacade.deletePatMedInhospByInhospids(inhospids));
    }
    /**
     * å½’一化"入院床位排序"参数。
     * <p>
     * è¡Œä¸ºä¸Ž <b>{@code æŽ¥å£æ–‡æ¡£/PatMedInhosp æ‚£è€…住院记录接口文档.md} é‡Œ bedSort çš„说明</b>
     * ä»¥åŠè¿å‡ºå‰çš„ {@code PatMedInhospServiceImpl#normalizeBedSort}(smartor,L213-219)
     * å®Œå…¨ä¸€è‡´ï¼š
     * <pre>
     *   asc  / 1 -> "asc"   åºŠä½æ­£åº
     *   desc / 2 -> "desc"  åºŠä½å€’序
     *   å…¶ä½™ï¼ˆå«ä¸ä¼ ã€ç©ºä¸²ã€éžæ³•值)-> null  ä¸æŽ’序,保持原有默认排序
     *   åªå¯¹ã€Œåœ¨é™¢ã€æŸ¥è¯¢ï¼ˆcry=0)生效;出院 / è½¬ç§‘ / é¢„入院 ä¸€å¾‹å¿½ç•¥è¯¥å‚æ•°
     * </pre>
     * <p>
     * âš ï¸ <b>为什么必须在 Java ä¾§å½’一,而不是让 SQL ä¹Ÿè®¤ "1"/"2"</b>:
     * <ul>
     *   <li>前端传的是 <b>数字</b> {@code bedSort:1},Jackson è½åˆ° {@code String} å­—段是
     *       {@code "1"},而 mapper çš„ {@code <choose>} åªåŒ¹é…å­—符串 {@code "asc"/"desc"}
     *       â€”— ä¸å½’一就静默落回默认排序,"按床位排序"这个功能等于没生效(真实线上缺陷);</li>
     *   <li>同一个排序契约还出现在 {@code VisitInhospMapper.xml}(随访扫描那条链,
     *       å…¥å‚是 {@code VisitInhospRow}),把"只认 asc/desc"留在 SQL é‡Œï¼Œ
     *       ä¸¤æ¡é“¾å¯¹å¤–的约定才是一致的;</li>
     *   <li>迁到本模块后这条链<b>不再经过</b> {@code PatMedInhospServiceImpl},
     *       åŽŸå½’ä¸€åŒ–ä¼šéšè¿å‡ºä¸€èµ·ä¸¢æŽ‰ï¼Œæ‰€ä»¥æ¬åˆ°è¿™é‡Œè¡¥å›žæ¥ã€‚</li>
     * </ul>
     * ç”± {@code sql/_probe/verify_patinhosp_bedsort.py} æŠŠå…³ã€‚
     *
     * @param cry     å‡ºå…¥é™¢æ ‡è¯†ï¼š0 å…¥é™¢(在院) 1 å‡ºé™¢ 2 è½¬ç§‘ 3 é¢„入院
     * @param bedSort å‰ç«¯ä¼ å…¥çš„床位排序值("asc"/"1"/"desc"/"2"/空)
     * @return {@code "asc"} / {@code "desc"} / {@code null}(不排序)
     */
    private static String normalizeBedSort(Integer cry, String bedSort) {
        // åªå¯¹ã€Œåœ¨é™¢ã€æŸ¥è¯¢ç”Ÿæ•ˆï¼šcry != 0 ä¸€å¾‹ç½®ç©ºï¼Œä¿è¯å‡ºé™¢ç­‰åœºæ™¯æŽ’序不变
        if (cry == null || cry != 0 || StringUtils.isEmpty(bedSort)) {
            return null;
        }
        String value = bedSort.trim().toLowerCase();
        if ("asc".equals(value) || "1".equals(value)) {
            return "asc";
        }
        if ("desc".equals(value) || "2".equals(value)) {
            return "desc";
        }
        // éžæ³•值:不排序(与"不传"同义),避免落到 XML çš„ <otherwise> ä¹‹å¤–的行为
        return null;
    }
}
ruoyi-inhospvisit/src/main/java/com/ruoyi/inhospvisit/domain/PatMedInhospVO.java
@@ -69,6 +69,21 @@
    private String inWardcheckFlag;
    /**
     * ä»»åŠ¡ç»„æ˜¯å¦å·²çº³å…¥ï¼š0未纳入   1已纳入   2跳过/失败
     * <p>
     * âš ï¸ æœ¬å­—段的存在理由是<b>硬性的</b>,不是"顺手补齐":
     * {@code PatMedInhospMapper.xml#selectPatMedInhospListByCondition} é‡Œæœ‰ä¸€æ¡
     * {@code <if test="scheduleCheckFlag != null">}(从原 smartor çš„ VO è¯­å¥é€æ¡ç…§æŠ„)。
     * MyBatis ç”¨ OGNL æ±‚值 {@code test},<b>缺 getter ä¼šç›´æŽ¥æŠ›</b>
     * {@code ReflectionException: There is no getter for property named 'scheduleCheckFlag'},
     * è€Œä¸æ˜¯"条件不成立"—— ä¹Ÿå°±æ˜¯è¯´é‚£æ¡æŸ¥è¯¢åœ¨è¡¥ä¸Šæœ¬å­—段之前<b>一调用就炸</b>
     * ï¼ˆåŽŸ smartor çš„ {@code PatMedInhospVO} åŒæ ·æ²¡æœ‰è¿™ä¸ªå­—段,属于继承下来的老缺陷)。
     * å®ˆå«ï¼š{@code sql/_probe/verify_mapper_ognl_params.py}(全仓)。
     */
    @ApiModelProperty(value = "任务组是否已纳入:0未纳入   1已纳入   2跳过/失败")
    private String scheduleCheckFlag;
    /**
     * æŠ¤å£«ID
     */
    @ApiModelProperty(value = "护士ID")
ruoyi-inhospvisit/src/main/java/com/ruoyi/inhospvisit/domain/row/VisitInhospRow.java
@@ -16,6 +16,12 @@
 *       {@code sql/_probe/verify_inhospvisit_mapper.py}(接口↔XML ä¸€è‡´æ€§ï¼‰ä¸Ž
 *       {@code sql/_probe/verify_inhospvisit_sql_expansion.py}(展开后逐条比对,AND é¡ºåºæ— å…³ï¼‰
 *       åŒå‘把关。改任一边都必须同步改另一边并重跑这两个探针。</li>
 *   <li>⚠️ è¿˜æœ‰ä¸€æ¡æ›´ç¡¬çš„纪律:<b>XML é‡Œå‡ºçŽ°çš„æ¯ä¸ªå±žæ€§ï¼Œæœ¬ç±»éƒ½å¿…é¡»æœ‰ getter</b>。
 *       å› ä¸º"逐条等价"意味着 XML ä¿ç•™äº†åŽŸæ–‡ä»¶çš„å…¨éƒ¨ {@code <if test=...>}(含随访链上
 *       ä¸å¯è¾¾çš„那些),而 MyBatis ç”¨ OGNL æ±‚值这些 {@code test}——
 *       ç¼º getter ä¼šç›´æŽ¥æŠ› {@code ReflectionException}(不是"条件不成立")。
 *       2026-10 å°±æ˜¯è¿™ä¹ˆæŠŠ {@code dealOutHospInfo} æ•´æ¡æ‰“停的(缺 {@code startOutHospTime}
 *       ç­‰ 15 ä¸ªè¿‡æ»¤å±žæ€§ï¼‰ã€‚守卫:{@code sql/_probe/verify_mapper_ognl_params.py}(全仓)。</li>
 *   <li>另一种做法是把查询"包一层端口"委托回 smartor,但那样本模块就只剩编排逻辑、
 *       CRUD å…¨é å›žè°ƒï¼Œåè€Œæ›´ç¢Žã€‚这里选择了"重复但可验证"。</li>
 * </ul>
@@ -484,16 +490,31 @@
    // ä»¥ä¸‹å­—段【不是 pat_med_inhosp çš„列】,只作为 selectForScan çš„过滤条件/排序开关。
    // æ”¾åœ¨åŒä¸€ä¸ªè¡Œå¯¹è±¡é‡Œï¼Œæ˜¯ä¸ºäº†è®©"查询条件"与"结果行"共用一套属性名 â€”—
    // ä¸ŽåŽŸå®žçŽ°"用 PatMedInhosp å½“查询入参"的写法保持一致,避免两套类来回映射出错。
    //
    // âš ï¸âš ï¸ è¿™ä¸€ç»„【必须有 getter】,哪怕随访生成链上一个都不设它:
    //     `VisitInhospMapper.xml#selectForScan` æ˜¯ä»ŽåŽŸ `PatMedInhospMapper.xml` é€æ¡ç…§æŠ„的,
    //     é‡Œé¢ä¿ç•™äº†åŽŸæ–‡ä»¶çš„å…¨éƒ¨ `<if test="xxx != null">`(含随访链上不可达的那些)。
    //     MyBatis ç”¨ OGNL æ±‚值 `test`,**读不到 getter ä¸æ˜¯"条件不成立",而是直接抛**:
    //       ReflectionException: There is no getter for property named 'startOutHospTime'
    //       in 'class com.ruoyi.inhospvisit.domain.row.VisitInhospRow'
    //     åŽæžœæ˜¯ `InhospVisitTaskService#dealOutHospInfo` åœ¨ç¬¬ä¸€æ¬¡ `selectForScan` å°±ä¸­æ–­ï¼Œ
    //     ä¸€æ¡éšè®¿ä»»åŠ¡éƒ½ä¸ç”Ÿæˆï¼ˆ2026-10 å®žæœºè¸©åˆ°ï¼Œ`sys_job_log` é‡Œè¡¨çŽ°ä¸º
    //     `InvocationTargetException`,很容易误判成模块开关没开)。
    //     å®ˆå«ï¼š`sql/_probe/verify_mapper_ognl_params.py`(全仓)
    //     ï¼ˆXML å¼•用属性 âІ å‚æ•°ç±» getter,缺一个就 FAIL)。
    // ==========================================================================
    /**
     * å‡ºé™¢/在院维度开关(原 {@code PatMedInhosp.cry}):
     * 0=在院({@code endtime is null})、1=已出院、2=转科、3=预入院。
     * <p>
     * âš ï¸ ç±»åž‹ä¸ŽåŽŸå®žä½“ä¸€è‡´æ˜¯ {@code Integer},因为 XML é‡Œæ˜¯**数值比较**
     * ï¼ˆ{@code cry == 0/1/2/3})—— å†™æˆ String ä¼šè®© OGNL é éšå¼è½¬æ¢åŽ»æ¯”ï¼Œè¯­ä¹‰ä¸æ˜Žç¡®ã€‚
     * <p>
     * âš ï¸ å®ƒåŒæ—¶å†³å®šæŽ’序:{@code cry=1/2/3} â†’ {@code order by endtime desc};
     * {@code cry=0} â†’ æŒ‰ {@link #bedSort} ä¸‰é€‰ä¸€ã€‚
     */
    private String cry;
    private Integer cry;
    /**
     * åœ¨é™¢æ‚£è€…({@code cry=0})的床位排序方向:"asc" / "desc";其它值走默认排序。
@@ -502,6 +523,40 @@
     * åºŠä½æŒ‰è‡ªç„¶åºæŽ’(空床位最后 â†’ çº¯æ•°å­—按数值 â†’ å¸¦å­—母按长度+字符串 â†’ inhospid å…œåº•)。
     */
    private String bedSort;
    // ---- æ—¶é—´åŒºé—´è¿‡æ»¤ï¼ˆåŽŸ PatMedInhospMapper.xml çš„ 4 ä¸ª <if>) ----
    /** è¿‡æ»¤ï¼šå‡ºé™¢æ—¶é—´åŒºé—´ä¸‹ç•Œ */
    private Date startOutHospTime;
    /** è¿‡æ»¤ï¼šå‡ºé™¢æ—¶é—´åŒºé—´ä¸Šç•Œ */
    private Date endOutHospTime;
    /** è¿‡æ»¤ï¼šå…¥é™¢æ—¶é—´åŒºé—´ä¸‹ç•Œ */
    private Date startInHospTime;
    /** è¿‡æ»¤ï¼šå…¥é™¢æ—¶é—´åŒºé—´ä¸Šç•Œ */
    private Date endInHospTime;
    // ---- å•值过滤(随访链不可达,但 XML é‡Œä¿ç•™äº†æ¡ä»¶ï¼‰ ----
    /** è¿‡æ»¤ï¼šå¯¼è¯ŠæŒ‚号标识(原 {@code inhosp_myd_flag}) */
    private String inhospMydFlag;
    /** è¿‡æ»¤ï¼šåºŠä½å·ï¼ˆåŽŸ {@code bed_no}) */
    private String bedNo;
    /** è¿‡æ»¤ï¼šç¦»é™¢æ–¹å¼ID */
    private String outWayId;
    /** è¿‡æ»¤ï¼šç¦»é™¢æ–¹å¼åç§° */
    private String outWayName;
    /** è¿‡æ»¤ï¼šå°±è¯Šæ¥æºID */
    private String encounterSourceId;
    /** è¿‡æ»¤ï¼šé‡è¦ç¨‹åº¦ä»£ç  */
    private String importanceFlagCode;
    /** è¿‡æ»¤ï¼šæ€¥è¯Šåˆ†çº§ä»£ç  */
    private String emergencyLevelCode;
    /** è¿‡æ»¤ï¼šåŒ»é™¢åç§°ï¼ˆåŽŸ XML é‡Œè¯¥åˆ—未加表别名) */
    private String hospitalname;
    /** è¿‡æ»¤ï¼šè¯å“åç§°ï¼ˆçœç«‹åŒå¾·è¯å“å®£æ•™ï¼‰ */
    private String medicalName;
    /** è¿‡æ»¤ï¼šè¯å“å…¬å¸ï¼ˆçœç«‹åŒå¾·è¯å“å®£æ•™ï¼‰ */
    private String medicalCompany;
    /** è¿‡æ»¤ï¼šæŽ’班状态(原类型 {@code Long}) */
    private Long schemestatus;
    /** è¿‡æ»¤ï¼šç¦»é™¢ç§‘室编码列表(原 {@code leaveldeptcodes}) */
    private java.util.List<String> leaveldeptcodes;
@@ -514,11 +569,11 @@
    /** è¿‡æ»¤ï¼šç¦»é™¢ ICD10 ç¼–码列表(原 {@code icd10codeList}) */
    private java.util.List<String> icd10codeList;
    public String getCry() {
    public Integer getCry() {
        return cry;
    }
    public void setCry(String cry) {
    public void setCry(Integer cry) {
        this.cry = cry;
    }
@@ -530,6 +585,126 @@
        this.bedSort = bedSort;
    }
    public Date getStartOutHospTime() {
        return startOutHospTime;
    }
    public void setStartOutHospTime(Date startOutHospTime) {
        this.startOutHospTime = startOutHospTime;
    }
    public Date getEndOutHospTime() {
        return endOutHospTime;
    }
    public void setEndOutHospTime(Date endOutHospTime) {
        this.endOutHospTime = endOutHospTime;
    }
    public Date getStartInHospTime() {
        return startInHospTime;
    }
    public void setStartInHospTime(Date startInHospTime) {
        this.startInHospTime = startInHospTime;
    }
    public Date getEndInHospTime() {
        return endInHospTime;
    }
    public void setEndInHospTime(Date endInHospTime) {
        this.endInHospTime = endInHospTime;
    }
    public String getInhospMydFlag() {
        return inhospMydFlag;
    }
    public void setInhospMydFlag(String inhospMydFlag) {
        this.inhospMydFlag = inhospMydFlag;
    }
    public String getBedNo() {
        return bedNo;
    }
    public void setBedNo(String bedNo) {
        this.bedNo = bedNo;
    }
    public String getOutWayId() {
        return outWayId;
    }
    public void setOutWayId(String outWayId) {
        this.outWayId = outWayId;
    }
    public String getOutWayName() {
        return outWayName;
    }
    public void setOutWayName(String outWayName) {
        this.outWayName = outWayName;
    }
    public String getEncounterSourceId() {
        return encounterSourceId;
    }
    public void setEncounterSourceId(String encounterSourceId) {
        this.encounterSourceId = encounterSourceId;
    }
    public String getImportanceFlagCode() {
        return importanceFlagCode;
    }
    public void setImportanceFlagCode(String importanceFlagCode) {
        this.importanceFlagCode = importanceFlagCode;
    }
    public String getEmergencyLevelCode() {
        return emergencyLevelCode;
    }
    public void setEmergencyLevelCode(String emergencyLevelCode) {
        this.emergencyLevelCode = emergencyLevelCode;
    }
    public String getHospitalname() {
        return hospitalname;
    }
    public void setHospitalname(String hospitalname) {
        this.hospitalname = hospitalname;
    }
    public String getMedicalName() {
        return medicalName;
    }
    public void setMedicalName(String medicalName) {
        this.medicalName = medicalName;
    }
    public String getMedicalCompany() {
        return medicalCompany;
    }
    public void setMedicalCompany(String medicalCompany) {
        this.medicalCompany = medicalCompany;
    }
    public Long getSchemestatus() {
        return schemestatus;
    }
    public void setSchemestatus(Long schemestatus) {
        this.schemestatus = schemestatus;
    }
    public java.util.List<String> getLeaveldeptcodes() {
        return leaveldeptcodes;
    }
ruoyi-inhospvisit/src/main/java/com/ruoyi/inhospvisit/mapper/PatMedInhospMapper.java
@@ -64,6 +64,23 @@
    public List<PatMedInhosp> selectPatMedInhospListByCondition(PatMedInhospVO patMedInhospVO);
    /**
     * æŸ¥è¯¢æŸæœºæž„下全部在院记录的"刷新候选"(供在院信息刷新按患者逐条核对位置)。
     * <p>
     * ä¸Ž {@link #selectPatMedInhospList} çš„区别是刻意的:
     * <ul>
     *   <li>不 join {@code pat_archive}、不做 GROUP BY â€”— æœ¬ç”¨é€”只看 {@code pat_med_inhosp} è‡ªèº«ï¼›
     *   <li>不带 {@code a.idcardno IS NOT NULL} è¿™ç±»è¿‡æ»¤ï¼ˆé‚£ä¼šè®©"证件号还没补上的新入院患者"
     *       é™é»˜æ¼åˆ·æ–° â€”— é‡‡é›†é“¾è·¯æœ€æ€•的就是静默少数据);
     *   <li>只 select åˆ·æ–°ç”¨å¾—到的列(inhospid / serialnum / patname / orgid / campusid /
     *       ç§‘室三列 / ç—…区三列 / bed_no),3000 è¡Œé‡çº§çš„æ¯æ—¥å…¨é‡æ‰«æè¶³å¤Ÿè½»ã€‚</li>
     * </ul>
     *
     * @param orgid æœºæž„ID(如 20001001)
     * @return åœ¨é™¢ï¼ˆ{@code inhospstate='0' and del_flag='0'})记录集合,按 inhospid å‡åº
     */
    public List<PatMedInhosp> selectInhospRefreshCandidates(@Param("orgid") String orgid);
    /**
     * æ–°å¢žæ‚£è€…住院记录
     *
     * @param patMedInhosp æ‚£è€…住院记录
ruoyi-inhospvisit/src/main/java/com/ruoyi/inhospvisit/service/InhospSubtaskWriter.java
@@ -105,9 +105,14 @@
     * @param archive  æ‚£è€…档案
     * @param type     0=药品宣教 / 1=离院科室 / 2=离院病区 / 3=疾病 / 4=入院科室 / 5=入院病区
     * @param config   {@code sys_config: visit.early.day}
     * @return <b>是否真的落了库</b>(或"本来就已经有")。
     *         è¿”回值是 2026-10"一个患者只生成一条随访任务"改造的判据:
     *         ç¼–排层按维度优先级逐个尝试,只有拿到 {@code true} æ‰ç®—该维度成功并停止。
     *         åŽŸå®žçŽ°æ˜¯ {@code void},编排层用"调用了几次"当成功数 â€”— é‚£æ˜¯é”™çš„
     *         ï¼ˆä»»åŠ¡æœªå‘å¸ƒ/被去重时同样是"生成了一条")。
     */
    public void writeInSubTask(Long taskid, Boolean check, VisitInhospRow inhosp,
                               VisitArchiveRow archive, Integer type, String config) {
    public boolean writeInSubTask(Long taskid, Boolean check, VisitInhospRow inhosp,
                                  VisitArchiveRow archive, Integer type, String config) {
        // åŽŸ L683-685
        String longTaskReason = inhosp.getLongTaskReason();
        String orgId = inhosp.getOrgid();
@@ -120,17 +125,32 @@
        if (taskApi == null) {
            log.warn("【出入院随访】VisitTaskCreateApi ä¸å¯ç”¨ï¼ˆä»»åŠ¡åˆ›å»ºæ¨¡å—æœªè£…é…ï¼‰ï¼Œæœ¬æ‚£è€…ä¸ç”Ÿæˆå­ä»»åŠ¡ã€‚"
                    + "inhospid={}, taskid={}", inhosp.getInhospid(), taskid);
            return;
            return false;
        }
        VisitLongTaskDto serviceTask = taskApi.findPublishedLongTask(taskid);
        if (serviceTask == null) {
            writeMissingTaskFlag(inhosp, type, longTaskReason, taskid);
            return;
            return false;
        }
        // åŽŸ L729-731
        if ("1".equals(inhosp.getInhospstate()) && inhosp.getEndtime() == null) {
            return;
            return false;
        }
        // ------------------------------------------------------------------
        // ã€æ”¹é€ Â·2026-10】落库前最后一道判重:
        //   åŒä¸€æ‚£è€… + åŒä¸€æ¬¡å‡ºé™¢è®°å½• + åŒä¸€ä»»åŠ¡ï¼Œåªè¦å·²ç»æœ‰ä¸€æ¡æœªåˆ é™¤çš„å­ä»»åŠ¡ï¼ˆå«å·²åœç”¨çš„ï¼‰ï¼Œ
        //   å°±ä¸å† insert。原因见 VisitTaskCreateApi#existsSubtask çš„æ³¨é‡Šï¼š
        //   æ‚£è€…被重复扫到时会走"停旧 + å† insert ä¸€æ¡ sendstate=4"的分支,
        //   æ¯è·‘一轮多一条,最终同一患者几十条。
        // ------------------------------------------------------------------
        if (taskApi.existsSubtask(archive == null ? null : archive.getId(),
                inhosp.getInhospid(), taskid)) {
            log.info("【出入院随访】患者{}出院记录{}任务{}已有子任务,跳过重复生成。",
                    inhosp.getPatid(), inhosp.getInhospid(), taskid);
            writeFlagByType(inhosp.getInhospid(), type, "1", null);
            return true;
        }
        // åŽŸ L732-736:组装(副作用由本类实现,见 missingOutHospTime)
@@ -138,7 +158,7 @@
                toTaskSource(serviceTask, inhosp), inhosp, archive, config, type,
                row -> onMissingOutHospTime(row));
        if (ObjectUtils.isEmpty(serviceSubtask)) {
            return;
            return false;
        }
        // åŽŸ L737-753:就诊科室/病区填充
@@ -286,6 +306,7 @@
                        serviceSubtask.getTaskid(), serviceSubtask.getPatid());
                writeFlagByType(inhosp.getInhospid(), type, "2", "生成子任务失败 " + type);
            }
            return i == 1;
        } catch (Exception e) {
            // åŽŸ L900-912:出错回滚事务
            log.info("生成子任务报错回滚serviceSubtask的taskid为:{},patid为:{}",
@@ -294,6 +315,7 @@
            //     åŽŸæ ·ä¿ç•™ï¼ˆè§ç±»æ³¨é‡Šç¬¬ 1 æ¡ï¼‰ã€‚
            writeFlagByTypeForRollback(inhosp.getInhospid(), type, "2", "生成子任务报错回滚 " + type);
            e.printStackTrace();
            return false;
        }
    }
@@ -478,16 +500,25 @@
        com.ruoyi.inhospvisit.domain.row.VisitTaskRow t = new com.ruoyi.inhospvisit.domain.row.VisitTaskRow();
        t.setTaskid(dto.getTaskid());
        t.setTaskName(dto.getTaskName());
        t.setTaskDesc(dto.getTaskDesc());
        t.setType(dto.getType());
        t.setServiceType(dto.getServiceType());
        t.setTemplateid(dto.getTemplateid());
        t.setTemplatename(dto.getTemplatename());
        t.setLibtemplateid(dto.getLibtemplateid());
        t.setLibtemplatename(dto.getLibtemplatename());
        t.setSendDay(dto.getSendDay());
        t.setPreachform(dto.getPreachform());
        t.setPreachformDesc(dto.getPreachformDesc());
        t.setOrgid(dto.getOrgid());
        t.setCampusid(dto.getCampusid());
        // ä»¥ä¸‹ 6 ä¸ªåŒæ ·å±žäºŽ"原 DtoConversionUtils æ‹·è´ã€éš insert è½åº“"的列
        t.setGuid(dto.getGuid());
        t.setPid(dto.getPid());
        t.setSendType(dto.getSendType());
        t.setIsupload(dto.getIsupload());
        t.setTextParam(dto.getTextParam());
        t.setUploadTime(dto.getUploadTime());
        t.setUpdateBy(dto.getUpdateBy());
        t.setCreateBy(dto.getCreateBy());
        return t;
ruoyi-inhospvisit/src/main/java/com/ruoyi/inhospvisit/service/InhospTaskOrchestrator.java
@@ -21,12 +21,26 @@
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
 * å‡ºé™¢éšè®¿ã€Œç¼–排器」—— é€è¡Œæ¬è‡ª {@code PatMedInhospServiceImpl}
 * çš„编排层({@code addSubTaskByRule} + 3 ä¸ª {@code *ForPatient} + {@code dealInHospInfo})。
 *
 * <h3>⚠️ 2026-10 å£å¾„变更:一个患者(一次出院)只生成 <b>一条</b> éšè®¿å­ä»»åŠ¡</h3>
 * æ¬è¿æ—¶æœ¬ç±»æ˜¯"逐行等价"的(原实现按维度全生成:每个 {@code *ForPatient} æŠŠå‘½ä¸­çš„
 * <b>所有</b>长期任务各写一条)。业务确认改为:
 * <ol>
 *   <li>维度按 {@code service_task_rule} ä¼˜å…ˆçº§æŽ’序尝试:首要 â†’ æ¬¡è¦ â†’ é€šç”¨å…œåº•(科室、病区);</li>
 *   <li>第一个真正落库成功的维度即停止 â†’ åªç•™ä¸€æ¡ï¼›</li>
 *   <li>无规则时按 {@code service_taskdept} çš„科室/病区"通用模块"生成;</li>
 *   <li>标志位统一回写,保证 {@code deptcheck_flag} ä¸å†åœåœ¨ 0(防重复扫描);
 *       å¹¶åœ¨å†™åº“前按 {@code patid+inhospid+taskid} åˆ¤é‡ï¼ˆè§ {@code VisitTaskCreateApi#existsSubtask})。</li>
 * </ol>
 * å› æ­¤ {@code verify_inhospvisit_orchestration_semantics.py} é‡Œ O18~O23 é‚£ 6 æ¡
 * "搬迁等价"断言已被改为"口径变更"断言(脚本里单独分组并注明依据)。
 *
 * <h3>来源对照(@ b25c3b51)</h3>
 * <table border="1">
@@ -122,165 +136,192 @@
    /**
     * æŒ‰ {@code service_task_rule} çš„「首要(1)/次要(2)」规则编排科室/病区/疾病三个维度。
     *
     * <h3>⚠️ 2026-10 å£å¾„变更:一个患者(一次出院)只生成 <b>一条</b> éšè®¿å­ä»»åŠ¡</h3>
     * åŽŸå®žçŽ°ï¼ˆä»¥åŠæ¬è¿åŽçš„ç­‰ä»·ç‰ˆï¼‰æ˜¯æŒ‰ç»´åº¦"全生成":先跑所有「首要」维度的
     * {@code *ForPatient},而每个 {@code *ForPatient} åˆæŠŠå‘½ä¸­çš„<b>所有</b>长期任务各写一条。
     * ç»“果是同一患者一次出院能生成几十条(线上实测 27 æ¡ï¼‰ï¼Œè€Œä¸”因为扫描键
     * {@code deptcheck_flag} åœ¨"只有病区/疾病维度"的规则下不会被置 1,
     * æ‚£è€…每轮扫描都被重新处理 â†’ é‡å¤å­ä»»åŠ¡æŒç»­ç´¯ç§¯ã€‚
     * <p>
     * é€è¡Œå¯¹åº”原 {@code addSubTaskByRule(String config, List<PatMedInhosp> patMedInhosps)}。
     * <p>
     * âš ï¸ è§„则查询 {@code serviceTaskRuleMapper.selectServiceTaskRuleByDeptOrWard} ä»åœ¨ smartor,
     * æ‰€ä»¥æœ¬æ–¹æ³•接收<b>已查好的规则</b>(由调用方 {@code InhospVisitTaskService} é€šè¿‡
     * {@code PublicityDataPort} å–——该端口已含"按科室/病区查规则"的能力,见下)。
     * è¿™æ ·åšæ˜¯ä¸ºäº†é¿å…ä¸º"规则表"再加一个 Mapper æˆ–端口。
     * çŽ°å£å¾„ï¼ˆä¸šåŠ¡ç¡®è®¤ï¼‰ï¼š
     * <ol>
     *   <li>把三个维度按规则优先级排成一个<b>尝试顺序</b>:
     *       é¦–要(1) â†’ æ¬¡è¦(2) â†’ é€šç”¨å…œåº•(科室、病区);同优先级内固定 ç§‘室 &gt; ç—…区 &gt; ç–¾ç—…。
     *       {@code service_task_rule} æ²¡æœ‰è¯¥æ‚£è€…科室/病区对应的规则时 â†’ é¡ºåºä¸º ç§‘室 â†’ ç—…区
     *       ï¼ˆå³"按 {@code service_taskdept} é‡Œçš„通用模块生成")。</li>
     *   <li>按顺序逐个维度尝试,<b>第一个真正落库成功(或已存在)的维度即停止</b> â€”— åªç•™ä¸€æ¡ã€‚</li>
     *   <li>最后统一回写标志位:命中维度 = {@code 1};其余维度 = {@code 2}(跳过),
     *       ä¸”<b>无论命中哪个维度,{@code deptcheck_flag} ä¸€å®šä¼šè¢«å†™æˆéž 0</b>
     *       â€”— è¿™å°±æ˜¯"不再被下一轮扫描重复生成"的保证。</li>
     * </ol>
     * å¦æœ‰ä¸€é“兜底在 {@code InhospSubtaskWriter#writeInSubTask}:落库前按
     * {@code patid + inhospid + taskid} åˆ¤é‡ï¼Œè§ {@link VisitTaskCreateApi#existsSubtask}。
     *
     * <h3>站点差异(保留)</h3>
     * {@code xh} åªåšç§‘室维度 â€”— {@link #addDimension} é‡Œç»Ÿä¸€æŒ¡æŽ‰ç—…区/疾病。
     *
     * @param inhosp å¾…处理的出院患者
     * @param archive æ‚£è€…档案
     * @param rule å·²æŸ¥å¥½çš„规则;{@code null} = æœªé…ç½®è§„则(全部生成)
     * @param rule å·²æŸ¥å¥½çš„规则;{@code null} = è¯¥ç§‘室/病区未配置规则
     * @param config {@code sys_config: visit.early.day}
     */
    public void addSubTaskByRule(VisitInhospRow inhosp, VisitArchiveRow archive,
                                 VisitTaskRule rule, String config) {
        try {
            if (rule == null) {
                // åŽŸ L1709-1716:规则为空,全部生成
                log.info("【基于规则】患者{}未配置规则,执行全部生成逻辑", inhosp.getInhospid());
                addDeptSubTaskForPatient(inhosp, archive, config);
                if (!SITE_XH.equals(active)) {
                    addWardSubTaskForPatient(inhosp, archive, config);
                    addIcd10SubTaskForPatient(inhosp, archive, config);
                }
                return;
            }
            // åŽŸ L1717-1726:有规则,按规则执行
            log.info("【基于规则】患者{}查询到规则:deptRule={}, wradRule={}, diagRule={}",
                    inhosp.getInhospid(), rule.getDeptRule(), rule.getWradRule(), rule.getDiagRule());
            List<Integer> order = resolveDimensionOrder(rule);
            log.info("【基于规则】患者{}规则:deptRule={}, wradRule={}, diagRule={};维度尝试顺序={}",
                    inhosp.getInhospid(),
                    rule == null ? null : rule.getDeptRule(),
                    rule == null ? null : rule.getWradRule(),
                    rule == null ? null : rule.getDiagRule(), order);
            boolean deptIsPrimary = "1".equals(rule.getDeptRule());
            boolean wardIsPrimary = "1".equals(rule.getWradRule());
            boolean diagIsPrimary = "1".equals(rule.getDiagRule());
            boolean deptIsSecondary = "2".equals(rule.getDeptRule());
            boolean wardIsSecondary = "2".equals(rule.getWradRule());
            boolean diagIsSecondary = "2".equals(rule.getDiagRule());
            boolean hasPrimaryRule = deptIsPrimary || wardIsPrimary || diagIsPrimary;
            if (!hasPrimaryRule) {
                // åŽŸ L1829-1837:没有首要规则,全部生成
                log.info("【基于规则】无首要规则,执行全部生成逻辑");
                addDeptSubTaskForPatient(inhosp, archive, config);
                if (!SITE_XH.equals(active)) {
                    addWardSubTaskForPatient(inhosp, archive, config);
                    addIcd10SubTaskForPatient(inhosp, archive, config);
                }
                return;
            }
            // åŽŸ L1730-1754:先执行所有规则=1 çš„æ–¹æ³•
            int deptGenCount = 0;
            int wardGenCount = 0;
            int diagGenCount = 0;
            if (deptIsPrimary) {
                log.info("【基于规则】执行科室首要规则");
                deptGenCount = addDeptSubTaskForPatient(inhosp, archive, config);
            }
            if (!SITE_XH.equals(active)) {
                if (wardIsPrimary) {
                    log.info("【基于规则】执行病区首要规则");
                    wardGenCount = addWardSubTaskForPatient(inhosp, archive, config);
                }
                if (diagIsPrimary) {
                    log.info("【基于规则】执行疾病首要规则");
                    diagGenCount = addIcd10SubTaskForPatient(inhosp, archive, config);
            Integer hit = null;
            for (Integer type : order) {
                log.info("【基于规则】患者{}尝试维度{}({})", inhosp.getInhospid(), type, dimensionName(type));
                if (generateOneForDimension(inhosp, archive, type, config)) {
                    hit = type;
                    break;
                }
            }
            int primaryGeneratedCount = deptGenCount + wardGenCount + diagGenCount;
            log.info("【基于规则】首要规则执行结果: deptGen={}, wardGen={}, diagGen={}, total={}",
                    deptGenCount, wardGenCount, diagGenCount, primaryGeneratedCount);
            String remark = inhosp.getRemark();
            if (primaryGeneratedCount > 0) {
                // åŽŸ L1758-1796:至少一个首要规则成功
                log.info("【基于规则】首要规则有成功(count={}),将次要规则对应flag设为2", primaryGeneratedCount);
                if (deptIsSecondary) {
                    updateCheckFlag(inhosp, 1, "2", StringUtils.isNotEmpty(remark)
                            ? remark + ";首要规则已生成,次要规则跳过" : "首要规则已生成,次要规则跳过");
                }
                if (!SITE_XH.equals(active)) {
                    if (wardIsSecondary) {
                        updateCheckFlag(inhosp, 2, "2", StringUtils.isNotEmpty(remark)
                                ? remark + ";首要规则已生成,次要规则跳过" : "首要规则已生成,次要规则跳过");
                    }
                    if (diagIsSecondary) {
                        updateCheckFlag(inhosp, 3, "2", StringUtils.isNotEmpty(remark)
                                ? remark + ";首要规则已生成,次要规则跳过" : "首要规则已生成,次要规则跳过");
                    }
                }
                if (deptIsPrimary && deptGenCount > 0) {
                    updateCheckFlag(inhosp, 1, "1", StringUtils.isNotEmpty(remark)
                            ? remark + ";科室首要规则生成成功" : "科室首要规则生成成功");
                }
                if (!SITE_XH.equals(active)) {
                    if (wardIsPrimary && wardGenCount > 0) {
                        updateCheckFlag(inhosp, 2, "1", StringUtils.isNotEmpty(remark)
                                ? remark + ";病区首要规则生成成功" : "病区首要规则生成成功");
                    }
                    if (diagIsPrimary && diagGenCount > 0) {
                        updateCheckFlag(inhosp, 3, "1", StringUtils.isNotEmpty(remark)
                                ? remark + ";疾病首要规则生成成功" : "疾病首要规则生成成功");
                    }
                }
                if (deptIsPrimary && deptGenCount == 0) {
                    updateCheckFlag(inhosp, 1, "2", StringUtils.isNotEmpty(remark)
                            ? remark + ";科室首要规则生成失败" : "科室首要规则生成失败");
                }
                if (!SITE_XH.equals(active)) {
                    if (wardIsPrimary && wardGenCount == 0) {
                        updateCheckFlag(inhosp, 2, "2", StringUtils.isNotEmpty(remark)
                                ? remark + ";病区首要规则生成失败" : "病区首要规则生成失败");
                    }
                    if (diagIsPrimary && diagGenCount == 0) {
                        updateCheckFlag(inhosp, 3, "2", StringUtils.isNotEmpty(remark)
                                ? remark + ";疾病首要规则生成失败" : "疾病首要规则生成失败");
                    }
                }
            } else {
                // åŽŸ L1797-1828:所有首要规则全部失败 â†’ æŠŠ =1 çš„ç½® 2,然后执行次要规则
                log.info("【基于规则】首要规则全部失败,将=1的flag设为2,开始执行次要规则");
                if (deptIsPrimary) {
                    updateCheckFlag(inhosp, 1, "2", StringUtils.isNotEmpty(remark)
                            ? remark + ";科室首要规则全部失败" : "科室首要规则全部失败");
                }
                if (!SITE_XH.equals(active)) {
                    if (wardIsPrimary) {
                        updateCheckFlag(inhosp, 2, "2", StringUtils.isNotEmpty(remark)
                                ? remark + ";病区首要规则全部失败" : "病区首要规则全部失败");
                    }
                    if (diagIsPrimary) {
                        updateCheckFlag(inhosp, 3, "2", StringUtils.isNotEmpty(remark)
                                ? remark + ";疾病首要规则全部失败" : "疾病首要规则全部失败");
                    }
                }
                if (deptIsSecondary) {
                    log.info("【基于规则】执行科室次要规则");
                    addDeptSubTaskForPatient(inhosp, archive, config);
                }
                if (!SITE_XH.equals(active)) {
                    if (wardIsSecondary) {
                        log.info("【基于规则】执行病区次要规则");
                        addWardSubTaskForPatient(inhosp, archive, config);
                    }
                    if (diagIsSecondary) {
                        log.info("【基于规则】执行疾病次要规则");
                        addIcd10SubTaskForPatient(inhosp, archive, config);
                    }
                }
            }
            writeFlagsAfterRun(inhosp, hit);
        } catch (Exception e) {
            // åŽŸ L1839-1845
            e.printStackTrace();
            log.error("【基于规则】患者{}处理异常:{}", inhosp.getInhospid(), e.getMessage());
            updateCheckFlag(inhosp, 1, "2", "基于规则处理异常");
        }
    }
    /**
     * ç»´åº¦å°è¯•顺序(2026-10 å£å¾„)。
     * <p>
     * è§„则为 {@code null} â†’ {@code [科室, ç—…区]}:按 {@code service_taskdept} é‡Œè¯¥æ‚£è€…çš„
     * ç§‘室/病区"通用模块"生成;这是业务明确要求的兜底口径(此时<b>不进入疾病维度</b>)。
     * <p>
     * æœ‰è§„则 â†’ {@code [首要维度…, æ¬¡è¦ç»´åº¦â€¦, ç§‘室, ç—…区]}:
     * é¦–要优先、次要其后,最后用通用兜底保证"能生成一条就生成一条"。
     */
    private List<Integer> resolveDimensionOrder(VisitTaskRule rule) {
        boolean xhSite = SITE_XH.equals(active);
        List<Integer> order = new ArrayList<>(4);
        if (rule == null) {
            addDimension(order, 1, xhSite);
            addDimension(order, 2, xhSite);
            return order;
        }
        boolean deptIsPrimary = "1".equals(rule.getDeptRule());
        boolean wardIsPrimary = "1".equals(rule.getWradRule());
        boolean diagIsPrimary = "1".equals(rule.getDiagRule());
        boolean deptIsSecondary = "2".equals(rule.getDeptRule());
        boolean wardIsSecondary = "2".equals(rule.getWradRule());
        boolean diagIsSecondary = "2".equals(rule.getDiagRule());
        // åŒä¼˜å…ˆçº§å†…固定顺序:科室 â†’ ç—…区 â†’ ç–¾ç—…
        if (deptIsPrimary) {
            addDimension(order, 1, xhSite);
        }
        if (wardIsPrimary) {
            addDimension(order, 2, xhSite);
        }
        if (diagIsPrimary) {
            addDimension(order, 3, xhSite);
        }
        if (deptIsSecondary) {
            addDimension(order, 1, xhSite);
        }
        if (wardIsSecondary) {
            addDimension(order, 2, xhSite);
        }
        if (diagIsSecondary) {
            addDimension(order, 3, xhSite);
        }
        // é€šç”¨å…œåº•:规则里配的维度全都取不到任务时,仍按 service_taskdept çš„ ç§‘室/病区 ç”Ÿæˆä¸€æ¡
        addDimension(order, 1, xhSite);
        addDimension(order, 2, xhSite);
        return order;
    }
    /** è¿½åŠ ä¸€ä¸ªå€™é€‰ç»´åº¦ï¼›{@code xh} ç«™ç‚¹åªå…è®¸ç§‘室维度(原 {@code !active.equals("xh")} çš„等价写法) */
    private void addDimension(List<Integer> order, int type, boolean xhSite) {
        if (xhSite && type != 1) {
            return;
        }
        if (!order.contains(type)) {
            order.add(type);
        }
    }
    private String dimensionName(int type) {
        switch (type) {
            case 1:
                return "科室";
            case 2:
                return "病区";
            case 3:
                return "疾病";
            default:
                return "未知(" + type + ")";
        }
    }
    /** å°è¯•一个维度,返回"是否真的生成了(或本来就已经有)一条子任务" */
    private boolean generateOneForDimension(VisitInhospRow inhosp, VisitArchiveRow archive,
                                            int type, String config) {
        switch (type) {
            case 1:
                return addDeptSubTaskForPatient(inhosp, archive, config) > 0;
            case 2:
                return addWardSubTaskForPatient(inhosp, archive, config) > 0;
            case 3:
                return addIcd10SubTaskForPatient(inhosp, archive, config) > 0;
            default:
                return false;
        }
    }
    /**
     * ä¸€è½®ç”Ÿæˆç»“束后的标志位回写。
     * <p>
     * âš ï¸ å…³é”®ç‚¹ï¼šæ‰«æé”®æ˜¯ {@code pat_med_inhosp.deptcheck_flag = '0'},
     * æ‰€ä»¥<b>不论命中哪个维度,deptcheck_flag éƒ½å¿…须被写成 1 æˆ– 2</b>,
     * å¦åˆ™ä¸‹ä¸€è½®æ‰«æä¼šæŠŠè¿™ä¸ªæ‚£è€…再处理一遍(2026-10 é‡å¤ç”Ÿæˆçš„直接原因)。
     * <p>
     * æ²¡ç”Ÿæˆæ—¶ï¼ˆ{@code hit == null})刻意<b>只补扫描键</b>:各维度方法已经把更具体的原因
     * ï¼ˆ"该患者所在科室未配置离院长期任务;" / "生成子任务失败 1" â€¦ï¼‰å†™è¿›
     * {@code long_task_reason} äº†ï¼Œè¿™é‡Œå…ˆç”¨ä¸€æ¬¡é‡è¯»åˆ¤æ–­ deptcheck æ˜¯å¦å·²ç»éž 0,
     * é¿å…æŠŠå®ƒè¦†ç›–成泛泛的"未生成随访任务"。
     *
     * @param hit å‘½ä¸­çš„维度;{@code null} = ä¸‰ä¸ªç»´åº¦éƒ½æ²¡èƒ½ç”Ÿæˆ
     */
    private void writeFlagsAfterRun(VisitInhospRow inhosp, Integer hit) {
        boolean xhSite = SITE_XH.equals(active);
        if (hit == null) {
            VisitInhospRow fresh = inhospMapper.selectByInhospid(inhosp.getInhospid());
            String current = fresh == null ? null : fresh.getDeptcheckFlag();
            if (current == null || "0".equals(current)) {
                updateCheckFlag(inhosp, 1, "2", composeRemark(inhosp, "未生成随访任务(无可用长期任务)"));
            }
            return;
        }
        for (int type = 1; type <= 3; type++) {
            if (xhSite && type != 1) {
                continue;
            }
            if (hit == type) {
                updateCheckFlag(inhosp, type, "1", composeRemark(inhosp,
                        "按优先级生成一条随访任务(维度=" + dimensionName(type) + ")"));
            } else {
                updateCheckFlag(inhosp, type, "2", composeRemark(inhosp,
                        "已生成一条随访任务(维度=" + dimensionName(hit) + "),本维度跳过"));
            }
        }
    }
    /** æ‹¼ reason:沿用原实现的 {@code remark + ";xxx"} å½¢å¼ */
    private String composeRemark(VisitInhospRow inhosp, String text) {
        String remark = inhosp.getRemark();
        return StringUtils.isNotEmpty(remark) ? remark + ";" + text : text;
    }
    /**
@@ -296,11 +337,14 @@
    /**
     * ä¸ºå•个患者生成科室维度的子任务。
     * <p>
     * âš ï¸ 2026-10 å£å¾„变更:命中多个长期任务时<b>只生成第一条真正落库的任务</b>
     * ï¼ˆåŽŸå®žçŽ°æ˜¯ {@code for (t : taskdepts) writeInSubTask(...); generatedCount++;},
     * ä¸€ä¸ªç§‘室配 N ä¸ªé•¿æœŸä»»åŠ¡å°±ä¼šç”Ÿæˆ N æ¡ â€”— æ˜¯"同一患者几十条"的主因)。
     *
     * @return ç”Ÿæˆçš„子任务数量
     * @return ç”Ÿæˆçš„子任务数量(0 æˆ– 1)
     */
    public int addDeptSubTaskForPatient(VisitInhospRow inhosp, VisitArchiveRow archive, String config) {
        int generatedCount = 0;
        try {
            // åŽŸ L1857-1864:按患者所在科室查该科室的长期任务(dept_type=1 + service_type=2)
            VisitTaskdeptRow query = new VisitTaskdeptRow();
@@ -325,8 +369,12 @@
                updateInhosp(u);
            } else {
                for (VisitTaskdeptRow t : taskdepts) {
                    subtaskWriter.writeInSubTask(t.getTaskId(), true, inhosp, archive, 1, config);
                    generatedCount++;
                    // ä¸€ä¸ªæ‚£è€…只留一条:第一条真正落库的即返回
                    if (subtaskWriter.writeInSubTask(t.getTaskId(), true, inhosp, archive, 1, config)) {
                        return 1;
                    }
                    log.info("【科室任务】患者{}任务{}未生成子任务,尝试下一个候选",
                            inhosp.getInhospid(), t.getTaskId());
                }
            }
        } catch (Exception e) {
@@ -339,7 +387,7 @@
            u.setLongTaskReason("科室任务处理异常");
            updateInhosp(u);
        }
        return generatedCount;
        return 0;
    }
    // ==========================================================================
@@ -348,11 +396,12 @@
    /**
     * ä¸ºå•个患者生成病区维度的子任务。
     * <p>
     * âš ï¸ åŒä¸Šï¼šå‘½ä¸­å¤šä¸ªé•¿æœŸä»»åŠ¡æ—¶åªç”Ÿæˆç¬¬ä¸€æ¡çœŸæ­£è½åº“çš„ï¼ˆåŽŸå®žçŽ°æ˜¯å…¨ç”Ÿæˆï¼‰ã€‚
     *
     * @return ç”Ÿæˆçš„子任务数量
     * @return ç”Ÿæˆçš„子任务数量(0 æˆ– 1)
     */
    public int addWardSubTaskForPatient(VisitInhospRow inhosp, VisitArchiveRow archive, String config) {
        int generatedCount = 0;
        try {
            String errorreason = null;
            List<VisitTaskdeptRow> taskdeptList = null;
@@ -386,8 +435,12 @@
                updateInhosp(u);
            } else {
                for (VisitTaskdeptRow t : taskdeptList) {
                    subtaskWriter.writeInSubTask(t.getTaskId(), true, inhosp, archive, 2, config);
                    generatedCount++;
                    // ä¸€ä¸ªæ‚£è€…只留一条:第一条真正落库的即返回
                    if (subtaskWriter.writeInSubTask(t.getTaskId(), true, inhosp, archive, 2, config)) {
                        return 1;
                    }
                    log.info("【病区任务】患者{}任务{}未生成子任务,尝试下一个候选",
                            inhosp.getInhospid(), t.getTaskId());
                }
            }
        } catch (Exception e) {
@@ -400,7 +453,7 @@
            u.setLongTaskReason("病区任务处理异常");
            updateInhosp(u);
        }
        return generatedCount;
        return 0;
    }
    // ==========================================================================
@@ -409,11 +462,12 @@
    /**
     * ä¸ºå•个患者生成疾病维度的子任务。
     * <p>
     * âš ï¸ åŒä¸Šï¼šå‘½ä¸­å¤šä¸ªé•¿æœŸä»»åŠ¡æ—¶åªç”Ÿæˆç¬¬ä¸€æ¡çœŸæ­£è½åº“çš„ï¼ˆåŽŸå®žçŽ°æ˜¯å…¨ç”Ÿæˆï¼‰ã€‚
     *
     * @return ç”Ÿæˆçš„子任务数量
     * @return ç”Ÿæˆçš„子任务数量(0 æˆ– 1)
     */
    public int addIcd10SubTaskForPatient(VisitInhospRow inhosp, VisitArchiveRow archive, String config) {
        int generatedCount = 0;
        try {
            // åŽŸ L1945-1947
            if (StringUtils.isEmpty(inhosp.getLeaveicd10code())) {
@@ -454,14 +508,21 @@
                    // åŽŸ L1975-1983:必须科室或病区能匹配上才建
                    if (StringUtils.isNotEmpty(t.getDeptCode())
                            && t.getDeptCode().equals(inhosp.getLeaveldeptcode())) {
                        subtaskWriter.writeInSubTask(t.getTaskId(), true, inhosp, archive, 3, config);
                        generatedCount++;
                        flag = 1;
                        // ä¸€ä¸ªæ‚£è€…只留一条:第一条真正落库的即返回
                        if (subtaskWriter.writeInSubTask(t.getTaskId(), true, inhosp, archive, 3, config)) {
                            return 1;
                        }
                        log.info("【疾病任务】患者{}任务{}未生成子任务,尝试下一个候选",
                                inhosp.getInhospid(), t.getTaskId());
                    } else if (StringUtils.isNotEmpty(t.getWardCode())
                            && t.getWardCode().equals(inhosp.getLeavehospitaldistrictcode())) {
                        subtaskWriter.writeInSubTask(t.getTaskId(), true, inhosp, archive, 3, config);
                        generatedCount++;
                        flag = 1;
                        if (subtaskWriter.writeInSubTask(t.getTaskId(), true, inhosp, archive, 3, config)) {
                            return 1;
                        }
                        log.info("【疾病任务】患者{}任务{}未生成子任务,尝试下一个候选",
                                inhosp.getInhospid(), t.getTaskId());
                    }
                }
                if (flag == 0) {
@@ -484,7 +545,7 @@
            u.setLongTaskReason("疾病任务处理异常");
            updateInhosp(u);
        }
        return generatedCount;
        return 0;
    }
    // ==========================================================================
ruoyi-inhospvisit/src/main/java/com/ruoyi/inhospvisit/service/InhospVisitTaskService.java
@@ -100,7 +100,7 @@
        // åŽŸ L331-409 + L411-444:两段宣教改为回调宣教模块
        // âš ï¸ è¿™é‡Œä¸å†å†…联实现 â€”— å¦åˆ™ä¼šä¸Ž ruoyi-publicity é‡Œçš„实现"发两遍"
        dealInHospPublicityAndMedical(config);
//        dealInHospPublicityAndMedical(config);
        // åŽŸ L445-451:基于规则的出院随访 + æ‰‹æœ¯éšè®¿
        addSubTaskByRule(config, patMedInhosps);
ruoyi-inhospvisit/src/main/java/com/ruoyi/inhospvisit/support/VisitSubtaskFactory.java
@@ -48,6 +48,13 @@
 * æ–¹æ¡ˆåŽŸè®¡åˆ’å« {@code VisitSubtaskFactory} å¹¶åŒæ—¶æ‰¿è½½ {@code writeInSubTask}。
 * å®žé™…实现时把它收窄为"纯组装 + çº¯å·¥å…·",因为 {@code writeInSubTask}(232 è¡Œï¼‰é‡Œæœ‰å¤§é‡
 * å†™åº“与去重编排,混在一起会让本类的 A æ¡£é€è¡Œç­‰ä»·æŽ¢é’ˆå¤±åŽ»æ„ä¹‰ã€‚
 *
 * <h3>⚠️ å”¯ä¸€çš„"非搬运"行:{@code hospno}</h3>
 * {@link #boxedServiceSubtask} é‡Œå¤šäº†ä¸€å¥ {@code d.setHospno(inhosp.getInhospno())}。
 * åŽŸå®žçŽ°ç¡®å®ž<b>没有</b>这一句,所以出院链路的 {@code service_subtask.hospno} ä¸€ç›´è½ç©ºï¼›
 * è€Œ {@code ServiceSubtask.hospno} çš„语义就是"就诊编号(对应的 outhospno、inhospno)"。
 * è¿™æ˜¯<b>有意补齐</b>,不是抄漏 â€”— åå‘探针({@code verify_inhospvisit_boxed_fields.py})
 * åªæ ¡éªŒ"原 setter æ˜¯å¦éƒ½æœ‰å¯¹åº”调用",不会因此报错。
 */
@Slf4j
@Component
@@ -104,6 +111,12 @@
        d.setNurseId(inhosp.getNurseId());
        d.setNurseName(inhosp.getNurseName());
        d.setInhospid(inhosp.getInhospid());
        // âš ï¸ è¿™ä¸€è¡Œæ˜¯ã€è¡¥çš„】,不是搬运:
        //   åŽŸ boxedServiceSubtask ä»Žæœªè®¾ hospno,而 ServiceSubtask.hospno çš„语义是
        //   "就诊编号(对应的 outhospno、inhospno)"(见 ServiceSubtask å­—段注释)。
        //   ç»“果就是出院链路的 service_subtask.hospno ä¸€ç›´æ˜¯ NULL,与门诊链路不一致。
        //   å–值:pat_med_inhosp.inhospno(门诊侧取 pat_med_outhosp.outhospno,见 OuthospSubtaskDraft)。
        d.setHospno(inhosp.getInhospno());
        d.setDrcode(inhosp.getDrcode());
        d.setDrname(inhosp.getDrname());
        d.setDeptcode(inhosp.getLeaveldeptcode());
ruoyi-inhospvisit/src/main/resources/mapper/inhospvisit/PatMedInhospMapper.xml
@@ -163,6 +163,148 @@
        from pat_med_inhosp
    </sql>
    <sql id="selectPatMedInhospListFromWhere">
        pat_med_inhosp b
        JOIN
        pat_archive a ON a.id = b.patid
        where 1=1
        <!--
          del_flag å¿…须用字符串常量比较:它取值是 '0'/'1'(char åˆ—)。
          å†™æˆ del_flag = 0 æ—¶ MySQL ä¼šæŠŠã€åˆ—】按 DOUBLE éšå¼è½¬æ¢ï¼Œè¯¥æ¡ä»¶å°±åªæ˜¯è¿‡æ»¤ã€
          ä¸èƒ½ç”¨æ¥å®šä½ç´¢å¼•;而 b.del_flag æ­£æ˜¯ idx_inhosp_optimize / idx_pmi_del_org_end
          çš„首列 â€”— æ•°å­—写法等于把索引首列白给。
          åŒä¸€ç»“论见 sql/sltdMydTotalByDimension_index_20260924.sql ç¬¬ 117-134 è¡Œã€‚
          ï¼ˆåˆ—是 int æ—¶å†™ '0' ä¹Ÿå®‰å…¨ï¼šè½¬æ¢å‘生在常量侧,索引照样可用。)
        -->
        AND a.del_flag = '0'
        AND b.del_flag = '0'
        AND a.idcardno IS NOT NULL
        <if test="inhospid != null">and b.inhospid = #{inhospid}</if>
        <if test="startOutHospTime != null ">and date_format(b.endtime,'%y%m%d') &gt;=
            date_format(#{startOutHospTime},'%y%m%d')
        </if>
        <if test="endOutHospTime != null ">and date_format(b.endtime,'%y%m%d') &lt;=
            date_format(#{endOutHospTime},'%y%m%d')
        </if>
        <if test="startInHospTime != null ">and date_format(b.starttime,'%y%m%d') &gt;=
            date_format(#{startInHospTime},'%y%m%d')
        </if>
        <if test="endInHospTime != null ">and date_format(b.starttime,'%y%m%d') &lt;=
            date_format(#{endInHospTime},'%y%m%d')
        </if>
        <if test="hospitalname != null  and hospitalname != ''">and hospitalname like concat('%', #{hospitalname},
            '%')
        </if>
        <if test="patname != null  and patname != ''">and patname like concat('%', #{patname}, '%')</if>
        <if test="hospitaldistrictname != null  and hospitaldistrictname != ''">and hospitaldistrictname like
            concat('%', #{hospitaldistrictname}, '%')
        </if>
        <if test="endtime != null ">and b.endtime = #{endtime}</if>
        <if test="bedNo != null ">and b.bed_no = #{bedNo}</if>
        <if test="inhospMydFlag != null ">and b.inhosp_myd_flag = #{inhospMydFlag}</if>
        <if test="managementDoctor != null ">and b.management_doctor = #{managementDoctor}</if>
        <if test="managementDoctorCode != null ">and b.management_doctor_code = #{managementDoctorCode}</if>
        <if test="inhospno != null ">and b.inhospno = #{inhospno}</if>
        <if test="fuflag != null ">and b.fuflag = #{fuflag}</if>
        <if test="orgid != null ">and b.orgid = #{orgid}</if>
        <if test="campusid != null ">and b.campusid = #{campusid}</if>
        <if test="serialnum != null ">and b.serialnum = #{serialnum}</if>
        <if test="endtime != null ">and b.endtime = #{endtime}</if>
        <if test="patno != null ">and b.patno = #{patno}</if>
        <if test="nurseId != null ">and b.nurse_id = #{nurseId}</if>
        <if test="nurseName != null and nurseName != ''">and b.nurse_name = #{nurseName}</if>
        <if test="deptcheckFlag != null ">and b.deptcheck_flag = #{deptcheckFlag}</if>
        <if test="scheduleCheckFlag != null ">and b.schedulecheck_flag = #{scheduleCheckFlag}</if>
        <if test="wardcheckFlag != null ">and b.wardcheck_flag = #{wardcheckFlag}</if>
        <if test="inDeptcheckFlag != null ">and b.in_deptcheck_flag = #{inDeptcheckFlag}</if>
        <if test="inWardcheckFlag != null ">and b.in_wardcheck_flag = #{inWardcheckFlag}</if>
        <if test="diagcheckFlag != null ">and b.diagcheck_flag = #{diagcheckFlag}</if>
        <if test="encounterSourceId != null ">and b.encounter_source_id = #{encounterSourceId}</if>
        <if test="importanceFlagCode != null ">and b.importance_flag_code = #{importanceFlagCode}</if>
        <if test="emergencyLevelCode != null ">and b.emergency_level_code = #{emergencyLevelCode}</if>
        <!--
          åœ¨é™¢ï¼ˆcry=0):两个条件都要。
          â‘  b.endtime is null â€”— åŽŸ XML å°±æœ‰ï¼Œä¿æŒä¸åŠ¨ï¼›
          â‘¡ b.inhospstate = '0' â€”— è¿å‡ºå‰ç”± PatMedInhospServiceImpl è¡¥ï¼ˆcry=0 æ—¶
             setInhospstate("0")),迁到本模块后这条链直连 Mapper,这句就丢了。
          ä¸ºä»€ä¹ˆå¿…须补回来(不只是"恢复原行为",也是性能问题):
            Â· ç´¢å¼• idx_inhosp_optimize / idx_pmi_del_org_state_end çš„前三列是
              (del_flag, orgid, inhospstate),只有三个等值条件齐了才能精确定位"在院"那几千行;
              å°‘了 inhospstate,就只能用到 (del_flag, orgid) å‰ç¼€ = è¯¥æœºæž„【全部历史住院记录】,
              å†åŠ ä¸ŠåŽé¢çš„ ORDER BY/GROUP BY,实测就是 4~5 ç§’的来源。
            Â· cry=1/2/3 æœ¬æ¥å°±æ˜¯æŒ‰ inhospstate åˆ¤çš„,只有 cry=0 ç”¨ endtime åˆ¤ â€”— è¡¥é½åŽæ‰å¯¹ç§°ã€‚
          ä¸ºä»€ä¹ˆåŠ  inhospstate == null è¿™ä¸ªå‰æï¼šè°ƒç”¨æ–¹è‹¥å·²æ˜¾å¼æŒ‡å®š inhospstate,就别和它打架
          ï¼ˆå¦åˆ™ cry=0 + inhospstate='2' ä¼šå˜æˆ"和自身矛盾"的空结果)。
        -->
        <if test="cry != null and cry == 0 ">and b.endtime is null</if>
        <if test="cry != null and cry == 0 and inhospstate == null ">and b.inhospstate = '0'</if>
        <!-- åŒ del_flag:inhospstate æ˜¯å–值范围 '0'~'3' çš„字符列,必须用字符串常量,
             å¦åˆ™ç´¢å¼•上用不到这个等值条件(出院/转科/预入院 ä¸‰ä¸ªè§†å›¾èµ°çš„就是这三行) -->
        <if test="cry != null and cry == 1 ">and b.inhospstate='1'</if>
        <if test="cry != null and cry == 2 ">and b.inhospstate='2'</if>
        <if test="cry != null and cry == 3 ">and b.inhospstate='3'</if>
        <if test="inhospstate != null ">and b.inhospstate = #{inhospstate}</if>
        <if test="schemestatus != null ">and b.schemestatus = #{schemestatus}</if>
        <if test="outWayId != null ">and b.out_way_id = #{outWayId}</if>
        <if test="outWayName != null ">and b.out_way_name = #{outWayName}</if>
        <if test="leaveldeptcode != null ">and b.leaveldeptcode = #{leaveldeptcode}</if>
        <if test="leavediagname != null  and leavediagname != ''">and b.leavediagname like concat('%',
            #{leavediagname}, '%')
        </if>
        <if test="drname != null  and drname != ''">and b.drname like concat('%', #{drname}, '%')</if>
        <if test="leaveldeptname != null  and leaveldeptname != ''">and b.leaveldeptname like concat('%',
            #{leaveldeptname}, '%')
        </if>
        <if test="deptname != null  and deptname != ''">and b.deptname like concat('%',
            #{deptname}, '%')
        </if>
        <if test="patid != null  and patid != ''">and a.id =#{patid}
        </if>
        <if test="leavehospitaldistrictname != null  and leavehospitaldistrictname != ''">and
            b.leavehospitaldistrictname like concat('%', #{leavehospitaldistrictname}, '%')
        </if>
        <if test="leavehospitaldistrictcodes != null and leavehospitaldistrictcodes.size()>0">
            AND b.leavehospitaldistrictcode IN
            <foreach collection="leavehospitaldistrictcodes" item="leavehospitaldistrictcode" open="(" separator=","
                     close=")">
                #{leavehospitaldistrictcode}
            </foreach>
        </if>
        <if test="hospitaldistrictcodeList != null and hospitaldistrictcodeList.size()>0">
            AND b.hospitaldistrictcode IN
            <foreach collection="hospitaldistrictcodeList" item="hospitaldistrictcode" open="(" separator=","
                     close=")">
                #{hospitaldistrictcode}
            </foreach>
        </if>
        <if test="leaveldeptcodes != null and leaveldeptcodes.size()>0">
            AND b.leaveldeptcode IN
            <foreach collection="leaveldeptcodes" item="leaveldeptcode" open="(" separator=","
                     close=")">
                #{leaveldeptcode}
            </foreach>
        </if>
        <if test="deptcodeList != null and deptcodeList.size()>0">
            AND b.deptcode IN
            <foreach collection="deptcodeList" item="deptcode" open="(" separator=","
                     close=")">
                #{deptcode}
            </foreach>
        </if>
        <if test="icd10codeList != null and icd10codeList.size()>0">
            AND b.leaveicd10code IN
            <foreach collection="icd10codeList" item="icd10code" open="(" separator=","
                     close=")">
                #{icd10code}
            </foreach>
        </if>
        <if test="medicalCode != null and medicalCode != ''">and b.medical_code = #{medicalCode}</if>
        <if test="medicalName != null and medicalName != ''">and b.medical_name = #{medicalName}</if>
        <if test="medicalCompany != null and medicalCompany != ''">and b.medical_company = #{medicalCompany}</if>
    </sql>
    <select id="selectPatMedInhospList" parameterType="com.ruoyi.inhospvisit.domain.PatMedInhosp" resultMap="PatMedInhospResult">
        select a.age AS age,
        a.telcode as telcode,
@@ -239,120 +381,7 @@
        b.medical_company,
        a.idcardno
        FROM
        pat_med_inhosp b
        JOIN
        pat_archive a ON a.id = b.patid
        where 1=1
        AND a.del_flag = 0
        AND b.del_flag = 0
        AND a.idcardno IS NOT NULL
        <if test="inhospid != null">and b.inhospid = #{inhospid}</if>
        <if test="startOutHospTime != null ">and date_format(b.endtime,'%y%m%d') &gt;=
            date_format(#{startOutHospTime},'%y%m%d')
        </if>
        <if test="endOutHospTime != null ">and date_format(b.endtime,'%y%m%d') &lt;=
            date_format(#{endOutHospTime},'%y%m%d')
        </if>
        <if test="startInHospTime != null ">and date_format(b.starttime,'%y%m%d') &gt;=
            date_format(#{startInHospTime},'%y%m%d')
        </if>
        <if test="endInHospTime != null ">and date_format(b.starttime,'%y%m%d') &lt;=
            date_format(#{endInHospTime},'%y%m%d')
        </if>
        <if test="hospitalname != null  and hospitalname != ''">and hospitalname like concat('%', #{hospitalname},
            '%')
        </if>
        <if test="patname != null  and patname != ''">and patname like concat('%', #{patname}, '%')</if>
        <if test="hospitaldistrictname != null  and hospitaldistrictname != ''">and hospitaldistrictname like
            concat('%', #{hospitaldistrictname}, '%')
        </if>
        <if test="endtime != null ">and b.endtime = #{endtime}</if>
        <if test="bedNo != null ">and b.bed_no = #{bedNo}</if>
        <if test="inhospMydFlag != null ">and b.inhosp_myd_flag = #{inhospMydFlag}</if>
        <if test="managementDoctor != null ">and b.management_doctor = #{managementDoctor}</if>
        <if test="managementDoctorCode != null ">and b.management_doctor_code = #{managementDoctorCode}</if>
        <if test="inhospno != null ">and b.inhospno = #{inhospno}</if>
        <if test="fuflag != null ">and b.fuflag = #{fuflag}</if>
        <if test="orgid != null ">and b.orgid = #{orgid}</if>
        <if test="campusid != null ">and b.campusid = #{campusid}</if>
        <if test="serialnum != null ">and b.serialnum = #{serialnum}</if>
        <if test="endtime != null ">and b.endtime = #{endtime}</if>
        <if test="patno != null ">and b.patno = #{patno}</if>
        <if test="nurseId != null ">and b.nurse_id = #{nurseId}</if>
        <if test="nurseName != null and nurseName != ''">and b.nurse_name = #{nurseName}</if>
        <if test="deptcheckFlag != null ">and b.deptcheck_flag = #{deptcheckFlag}</if>
        <if test="scheduleCheckFlag != null ">and b.schedulecheck_flag = #{scheduleCheckFlag}</if>
        <if test="wardcheckFlag != null ">and b.wardcheck_flag = #{wardcheckFlag}</if>
        <if test="inDeptcheckFlag != null ">and b.in_deptcheck_flag = #{inDeptcheckFlag}</if>
        <if test="inWardcheckFlag != null ">and b.in_wardcheck_flag = #{inWardcheckFlag}</if>
        <if test="diagcheckFlag != null ">and b.diagcheck_flag = #{diagcheckFlag}</if>
        <if test="encounterSourceId != null ">and b.encounter_source_id = #{encounterSourceId}</if>
        <if test="importanceFlagCode != null ">and b.importance_flag_code = #{importanceFlagCode}</if>
        <if test="emergencyLevelCode != null ">and b.emergency_level_code = #{emergencyLevelCode}</if>
        <if test="cry != null and cry == 0 ">and b.endtime is null</if>
        <if test="cry != null and cry == 1 ">and b.inhospstate=1</if>
        <if test="cry != null and cry == 2 ">and b.inhospstate=2</if>
        <if test="cry != null and cry == 3 ">and b.inhospstate=3</if>
        <if test="inhospstate != null ">and b.inhospstate = #{inhospstate}</if>
        <if test="schemestatus != null ">and b.schemestatus = #{schemestatus}</if>
        <if test="outWayId != null ">and b.out_way_id = #{outWayId}</if>
        <if test="outWayName != null ">and b.out_way_name = #{outWayName}</if>
        <if test="leaveldeptcode != null ">and b.leaveldeptcode = #{leaveldeptcode}</if>
        <if test="leavediagname != null  and leavediagname != ''">and b.leavediagname like concat('%',
            #{leavediagname}, '%')
        </if>
        <if test="drname != null  and drname != ''">and b.drname like concat('%', #{drname}, '%')</if>
        <if test="leaveldeptname != null  and leaveldeptname != ''">and b.leaveldeptname like concat('%',
            #{leaveldeptname}, '%')
        </if>
        <if test="deptname != null  and deptname != ''">and b.deptname like concat('%',
            #{deptname}, '%')
        </if>
        <if test="patid != null  and patid != ''">and a.id =#{patid}
        </if>
        <if test="leavehospitaldistrictname != null  and leavehospitaldistrictname != ''">and
            b.leavehospitaldistrictname like concat('%', #{leavehospitaldistrictname}, '%')
        </if>
        <if test="leavehospitaldistrictcodes != null and leavehospitaldistrictcodes.size()>0">
            AND b.leavehospitaldistrictcode IN
            <foreach collection="leavehospitaldistrictcodes" item="leavehospitaldistrictcode" open="(" separator=","
                     close=")">
                #{leavehospitaldistrictcode}
            </foreach>
        </if>
        <if test="hospitaldistrictcodeList != null and hospitaldistrictcodeList.size()>0">
            AND b.hospitaldistrictcode IN
            <foreach collection="hospitaldistrictcodeList" item="hospitaldistrictcode" open="(" separator=","
                     close=")">
                #{hospitaldistrictcode}
            </foreach>
        </if>
        <if test="leaveldeptcodes != null and leaveldeptcodes.size()>0">
            AND b.leaveldeptcode IN
            <foreach collection="leaveldeptcodes" item="leaveldeptcode" open="(" separator=","
                     close=")">
                #{leaveldeptcode}
            </foreach>
        </if>
        <if test="deptcodeList != null and deptcodeList.size()>0">
            AND b.deptcode IN
            <foreach collection="deptcodeList" item="deptcode" open="(" separator=","
                     close=")">
                #{deptcode}
            </foreach>
        </if>
        <if test="icd10codeList != null and icd10codeList.size()>0">
            AND b.leaveicd10code IN
            <foreach collection="icd10codeList" item="icd10code" open="(" separator=","
                     close=")">
                #{icd10code}
            </foreach>
        </if>
        <if test="medicalCode != null and medicalCode != ''">and b.medical_code = #{medicalCode}</if>
        <if test="medicalName != null and medicalName != ''">and b.medical_name = #{medicalName}</if>
        <if test="medicalCompany != null and medicalCompany != ''">and b.medical_company = #{medicalCompany}</if>
        <include refid="selectPatMedInhospListFromWhere"/>
        GROUP BY
        b.inhospid,
        a.age,
@@ -403,6 +432,28 @@
                </otherwise>
            </choose>
        </if>
    </select>
    <!--
      åˆ†é¡µæ€»æ•°ï¼ˆPageHelper æ‰‹å†™ count,id = ä¸»æŸ¥è¯¢ id + _COUNT ä¼šè¢«è‡ªåŠ¨é‡‡ç”¨ï¼‰ã€‚
      åŽŸçŠ¶æ€ï¼šä¸»æŸ¥è¯¢å¸¦ GROUP BY,pagehelper çš„ CountSqlParser#isSimpleCount åˆ¤å®šä¸º
      "不能简化",于是总数变成 SELECT count(0) FROM ( ä¸Šé¢é‚£æ¡ 60 åˆ—查询 ) tmp_count â€”
      ç­‰äºŽæ¯æ¬¡ç¿»é¡µæŠŠæ•´æ¡æŸ¥è¯¢å†è·‘一遍。
      è¿™é‡Œä¿æŒã€å®Œå…¨ç›¸åŒçš„ FROM/WHERE/GROUP BY】(共用 selectPatMedInhospListFromWhere ç‰‡æ®µï¼Œ
      æ¡ä»¶ä¸ä¼šæ¼‚),但子查询只 select 1,不物化 60 åˆ—,也不带 order by。
      å› ä¸ºåˆ†ç»„键包含 a.id è€Œè¿™ä¸ª join æ˜¯ä¸€å¯¹ä¸€ï¼Œåˆ†ç»„æ•° == join ç»“果行数,总数与原来一致。
    -->
    <select id="selectPatMedInhospList_COUNT" parameterType="com.ruoyi.inhospvisit.domain.PatMedInhosp"
            resultType="java.lang.Long">
        select count(1)
        from (
            select 1
            FROM
            <include refid="selectPatMedInhospListFromWhere"/>
            group by b.inhospid, a.age, a.age_unit, a.telcode, a.id, a.sex
        ) t
    </select>
    <select id="selectPatMedInhospListBySerialnum" parameterType="com.ruoyi.inhospvisit.domain.PatMedInhosp"
@@ -503,6 +554,31 @@
        order by b.inhospid desc
    </select>
    <!--
      åœ¨é™¢ä¿¡æ¯åˆ·æ–°çš„取数:只查 pat_med_inhosp è‡ªèº«ã€åªå–刷新要用的列。
      åˆ»æ„ä¸å¤ç”¨ selectPatMedInhospList â€”— é‚£ä¸ªæŸ¥è¯¢ join äº† pat_archive ä¸”要求
      a.idcardno IS NOT NULL,会让"证件号还没补上的新入院患者"静默漏刷新。
      WHERE (del_flag, orgid, inhospstate) å‘½ä¸­ idx_inhosp_optimize çš„前缀。
    -->
    <select id="selectInhospRefreshCandidates" resultMap="PatMedInhospResult">
        select inhospid,
               serialnum,
               patname,
               orgid,
               campusid,
               deptcode,
               deptname,
               hospitaldistrictcode,
               hospitaldistrictname,
               hospitaldistrictid,
               bed_no
        from pat_med_inhosp
        where inhospstate = '0'
          and del_flag = '0'
          and orgid = #{orgid}
        order by inhospid asc
    </select>
    <select id="selectPatMedInhospListByCondition" parameterType="com.ruoyi.inhospvisit.domain.PatMedInhospVO"
            resultMap="PatMedInhospResult">
        select CONCAT( a.age, a.age_unit ) AS age,
ruoyi-outpatientvisit/src/main/java/com/ruoyi/outpatientvisit/domain/PatMedOuthosp.java
@@ -49,6 +49,19 @@
    private Long patid;
    /**
     * HIS ä¾§æ‚£è€…主键({@code patid_his} åˆ—)
     * <p>
     * âš ï¸ ä¸èƒ½çœï¼š{@code ruoyi-quartz} çš„ {@code CollectHISMapper.xml}(市一 HIS é‡‡é›†ï¼‰
     * çš„ {@code PatMedOuthospResult} é‡Œæœ‰ {@code property="patidHis" column="patid_his"},
     * ä¸” {@code <if test="patidHis != null">and patid_his = #{patidHis}</if>}。
     * å®žä½“缺这个属性时,那些采集查询在结果映射阶段就会抛
     * {@code ReflectionException: no setter for 'patidHis'}。
     * ï¼ˆç±»åž‹ä¸Ž {@code PatArchive.patidHis} ä¿æŒä¸€è‡´ï¼šString)
     */
    @ApiModelProperty(value = "HIS ä¾§æ‚£è€…主键(patid_his)")
    private String patidHis;
    /**
     * ç—…人姓名
     */
    @ApiModelProperty(value = " ç—…人姓名")
ruoyi-quartz/pom.xml
@@ -95,6 +95,16 @@
            <artifactId>ruoyi-task</artifactId>
        </dependency>
        <!--
            æ•°æ®é‡‡é›†æ¨¡å—:RyTask çš„ HIS é‡‡é›†å…¥å£ï¼ˆdealHisData)方法体已整体搬进该模块,
            è¿™é‡Œåªä¿ç•™å®šæ—¶ä»»åŠ¡å…¥å£å¹¶è½¬å‘ç»™å…¶é—¨é¢ DataCollectionFacade
            ï¼ˆsys_job çš„ invoke_target ä»æ˜¯ ryTask.dealHisData('...'),入口名不能变)。
        -->
        <dependency>
            <groupId>com.ruoyi</groupId>
            <artifactId>ruoyi-datacollection</artifactId>
        </dependency>
    </dependencies>
</project>
ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/RyTask.java
@@ -118,8 +118,11 @@
    @Autowired
    private IServicePatientTempService iServicePatientTempService;
    @Autowired
    private IHNGatherPatArchiveService ihnGatherPatArchiveService;
    // âš ï¸ 2026-10 ç§»é™¤ï¼šåŽŸ `IHNGatherPatArchiveService ihnGatherPatArchiveService` å­—段。
    //    å®ƒåœ¨æœ¬ç±»é‡Œ**只**被 dealHisData çš„æ²³å—分支使用(死字段检查:改前全文件仅 1 å¤„使用),
    //    è¯¥åˆ†æ”¯å·²éšæ–¹æ³•体搬进 ruoyi-datacollection,故字段连带删除 â€”—
    //    ç•™ç€å®ƒä¼šè®©äººè¯¯ä»¥ä¸º smartor çš„æ²³å—采集还有本类这个使用方
    //    ï¼ˆçœŸæ­£çš„使用方已是数据采集模块经契约 HisGatherApi è°ƒç”¨çš„ Bridge)。
    @Autowired
    private IServiceSLTDHealthcareRecordService serviceSLTDHealthcareRecordService;
@@ -160,8 +163,9 @@
    @Autowired
    private IServiceSubtaskRecordService serviceSubtaskRecordService;
    @Autowired
    IIcd10Service iIcd10Service;
    // âš ï¸ 2026-10 ç§»é™¤ï¼šåŽŸ `IIcd10Service iIcd10Service` å­—段。
    //    å®ƒåœ¨æœ¬ç±»é‡Œ**只**被 dealHisData çš„"疾病字典增量采集"一行调用,已随方法体搬进
    //    ruoyi-datacollection(经契约 HisGatherApi#gatherIcd10 è°ƒå›ž smartor çš„实现)。
    @Autowired
    IPatArchiveService patArchiveService;
@@ -227,8 +231,27 @@
    @Autowired
    private com.ruoyi.publicity.api.PublicityFacade publicityFacade;
    @Value("${appid}")
    private String appid;
    /**
     * æ•°æ®é‡‡é›†æ¨¡å—çš„<b>常驻门面</b>。
     * <p>
     * âš ï¸ 2026-10 æ¨¡å—化:本类的 {@code dealHisData(String)} æ–¹æ³•体(连同私有帮手
     * {@code collectSegmentData})已整体搬进 {@code ruoyi-datacollection},
     * æœ¬ç±»åªä¿ç•™å®šæ—¶ä»»åŠ¡å…¥å£ï¼ˆsys_job çš„ invoke_target æ˜¯ {@code ryTask.dealHisData('...')},
     * å…¥å£åä¸èƒ½å˜ï¼‰ã€‚站点分支、时间窗口、分段回溯、日志文案都在模块里,一行未改。
     * <p>
     * æ³¨å…¥é—¨é¢è€Œä¸æ˜¯æ¨¡å—内部 Service:门面<b>永远存在</b>,且只有门面判
     * {@code sys_config: datacollection.enabled} å¼€å…³ã€‚
     */
    @Autowired
    private com.ruoyi.datacollection.api.DataCollectionFacade dataCollectionFacade;
    // âš ï¸ 2026-10 åˆ é™¤ï¼šåŽŸ `@Value("${appid}") private String appid;`
    //    å®ƒæ˜¯ã€Œæ–°åŽå¾®ä¿¡å…¬ä¼—号推送」的遗留字段(提交 8590ef6a å¼•入,只被
    //    xinHuaWXReq.setAppid(appid) ç”¨è¿‡ï¼‰ã€‚该推送逻辑已搬进 ruoyi-message
    //    ï¼ˆå…¬ä¼—号凭据改由 sys_config.wx.mp.config.* ä¸‹å‘),本类里已无引用;
    //    ä½†å®ƒçš„ @Value æ²¡æœ‰é»˜è®¤å€¼ï¼Œè€Œ application-nhfy.yml / -xh.yml / -ls.yml
    //    éƒ½æ²¡å®šä¹‰ appid è¿™ä¸ª key,这些 profile å¯åŠ¨æ—¶ä¼šç›´æŽ¥æŠ¥
    //    `Could not resolve placeholder 'appid'`。留着是定时炸弹,故删除。
    @Value("${server.port}")
    private String port;
@@ -442,262 +465,53 @@
     * HIS数据采集
     * å®šæ—¶ä»»åŠ¡ï¼šé‡‡é›†æ‚£è€…ä¿¡æ¯ã€å‡ºé™¢ã€é—¨è¯Šæ•°æ®
     * todayZeroStr  å½“前的时间   ä¾‹å¦‚:2026-05-06 00:00:00
     * <p>
     * âš ï¸ <b>2026-10 æ¨¡å—化</b>:方法体(连同私有帮手 {@code collectSegmentData})已整体搬进
     * {@code ruoyi-datacollection},实现见
     * {@code com.ruoyi.datacollection.service.impl.HisDataCollectionServiceImpl}。
     * è¿™é‡Œåªä¿ç•™<b>定时任务入口</b>:sys_job çš„ invoke_target ä»æ˜¯
     * {@code ryTask.dealHisData('...')},入口名不能变,所以本方法是<b>纯转发</b>。
     * <p>
     * å…³é—­æ•°æ®é‡‡é›†æ¨¡å—({@code sys_config: datacollection.enabled=false})时门面会抛
     * {@link com.ruoyi.datacollection.api.DataCollectionModuleDisabledException};
     * æœ¬å…¥å£æ•获后只打一条 WARN â€”— é‚£æ˜¯"模块被主动关掉",不是采集失败,
     * ä¸è¯¥è®©å®šæ—¶ä»»åŠ¡è®°æˆæ‰§è¡Œå¼‚å¸¸ï¼ˆå¦åˆ™è¿ç»´ä¼šçœ‹åˆ°ä¸€ç‰‡çº¢è‰²å¤±è´¥è®°å½•ï¼‰ã€‚
     */
    public void dealHisData(String todayZeroStr) {
        log.info("【dealHisData】开始执行HIS数据采集任务");
        // èŽ·å–æ˜¨å¤©0点到今天0点的时间范围
        LocalDateTime todayZero = LocalDateTime.now().with(LocalTime.MIN);
        if (StringUtils.isNotBlank(todayZeroStr)) {
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
            todayZero = LocalDateTime.parse(todayZeroStr, formatter);
        }
        LocalDateTime yesterdayZero = todayZero.minusDays(1);
        log.info("【dealHisData】采集时间范围:{} ~ {}", yesterdayZero, todayZero);
        if (active.trim().equals("xh")) {
            try {
                // é‡‡é›†å…¥é™¢æ•°æ®
                log.info("【dealHisData】新华开始采集入院数据");
                ixhGatherPatArchiveService.getInHospDataGather("0", yesterdayZero, todayZero);
                log.info("【dealHisData】新华入院数据采集完成");
                // é‡‡é›†å‡ºé™¢æ•°æ®
                log.info("【dealHisData】新华开始采集出院数据");
                ixhGatherPatArchiveService.getInHospDataGather("1", yesterdayZero, todayZero);
                log.info("【dealHisData】新华出院数据采集完成");
            } catch (Exception e) {
                log.error("【dealHisData】新华数据采集异常", e);
            }
        } else if (active.trim().equals("hn")) {
            try {
                // æ²³å—数据采集
                HnDataGatherVO hnDataGatherVO = new HnDataGatherVO();
                log.info("【dealHisData】河南开始采集数据");
                Date startTime = Date.from(yesterdayZero.atZone(ZoneId.systemDefault()).toInstant());
                Date endTime = Date.from(todayZero.atZone(ZoneId.systemDefault()).toInstant());
                hnDataGatherVO.setStartTime(startTime);
                hnDataGatherVO.setEndTime(endTime);
                ihnGatherPatArchiveService.hnDataGather(hnDataGatherVO);
                log.info("【dealHisData】河南结束采集数据");
            } catch (Exception e) {
                log.error("【dealHisData】河南数据采集异常", e);
            }
        } else if (active.trim().equals("sltd") || active.trim().equals("nhfy")) {
            //省立同德与华南附一用同一套采集方法(都是来未来提供)
            try {
                //获取是否需要采集用户、部门信息
                SysConfig config = new SysConfig();
                config.setConfigKey("sltd.gather.sysuser");
                config.setOrgid("20001001");
                List<SysConfig> sysConfigs = configService.selectConfigList(config);
                //获取省立同德要采集的院区
                SysConfig cf = new SysConfig();
                cf.setConfigKey("sltd.gather.campusid");
                cf.setOrgid("20001001");
                List<SysConfig> sysConfigcampusids = configService.selectConfigList(cf);
                List<Long> campusidList = new ArrayList<>();
                if (CollectionUtils.isNotEmpty(sysConfigcampusids)) {
                    String[] split = sysConfigcampusids.get(0).getConfigValue().split(",");
                    campusidList = Arrays.stream(split).map(Long::valueOf).collect(Collectors.toList());
                } else {
                    log.error("SysConfig中的院区ID为空!");
                    return;
                }
                //configValue = 1 ä¸éœ€è¦é‡‡é›†       configValue = 2需要采集
                if (CollectionUtils.isNotEmpty(sysConfigs) && sysConfigs.get(0).getConfigValue().equals("2")) {
                    log.info("【dealHisData】省立同德“部门信息”开始采集数据");
                    ServiceSLTDDeptReqVO serviceSLTDDeptReqVO = new ServiceSLTDDeptReqVO();
                    serviceSLTDDeptReqVO.setOrgId("20001001");
                    serviceSLTDDeptReqVO.setCampusIds(campusidList);
                    Boolean aBoolean1 = serviceSLTDHealthcareRecordService.queryDeptWardAreaInfoList(serviceSLTDDeptReqVO);
                    log.info("【dealHisData】省立同德“部门信息”结束采集数据:{}", aBoolean1);
                    Boolean aBoolean = false;
                    for (Long cid : campusidList) {
                        log.info("【dealHisData】省立同德“用户信息”开始采集数据,院区:{}", cid);
                        ServiceSLTDInhospReqVO reqVO = new ServiceSLTDInhospReqVO();
                        reqVO.setOrgId("20001001");
                        reqVO.setCampusId(cid);
                        ArrayList<String> statusList = new ArrayList<>();
                        statusList.add("1");
                        reqVO.setStatusList(statusList);
                        aBoolean = serviceSLTDHealthcareRecordService.queryHospUserInfoList(reqVO);
                    }
                    log.info("【dealHisData】省立同德“用户信息”结束采集数据:{}", aBoolean);
                    //疾病字典采集
                    iIcd10Service.queryAddIcd10();
                }
                // nhfy/sltd回溯30天分段采集判断(每段最多4天)
                boolean isBackfill = (active.trim().equals("nhfy") || active.trim().equals("sltd")) && StringUtils.isNotBlank(todayZeroStr);
                if (isBackfill) {
                    DateTimeFormatter dateFmt = DateTimeFormatter.ofPattern("yyyy-MM-dd");
                    LocalDate segStart = todayZero.toLocalDate().minusDays(30);
                    LocalDate segEndDate = todayZero.toLocalDate();
                    int segCount = 0;
                    while (!segStart.isAfter(segEndDate)) {
                        LocalDate segEnd = segStart.plusDays(3);
                        if (segEnd.isAfter(segEndDate)) {
                            segEnd = segEndDate;
                        }
                        segCount++;
                        log.info("【dealHisData】{}回溯第{}段采集,日期范围:{} ~ {}", active.trim(), segCount, segStart.format(dateFmt), segEnd.format(dateFmt));
                        for (Long cid : campusidList) {
                            collectSegmentData(cid, segStart.format(dateFmt), segEnd.format(dateFmt));
                        }
                        segStart = segEnd.plusDays(1);
                    }
                    log.info("【dealHisData】{}回溯采集完成,共{}段", active.trim(), segCount);
                }
                // çœç«‹åŒå¾·æ•°æ®é‡‡é›†ï¼ˆéžå›žæº¯æ¨¡å¼æ‰æ‰§è¡Œï¼‰
                if (!isBackfill) {
                    for (Long cid : campusidList) {
                        log.info("【dealHisData】省立同德预入院开始采集数据,院区:{}", cid);
                        String nowTime = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
                        ServiceSLTDInhospReqVO serviceSLTDInhospReqVO = new ServiceSLTDInhospReqVO();
                        serviceSLTDInhospReqVO.setOrgId("20001001");
                        serviceSLTDInhospReqVO.setCampusId(cid);
                        serviceSLTDInhospReqVO.setEncounterTimeStart(nowTime);
                        serviceSLTDInhospReqVO.setEncounterTimeEnd(nowTime);
                        if (active.trim().equals("nhfy")) {
                            serviceSLTDInhospReqVO.setEncounterTimeStart(yesterdayZero.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
                            serviceSLTDInhospReqVO.setEncounterTimeEnd(todayZero.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
                        }
                        List<String> list = new ArrayList<>();
                        list.add("FH0109.22");
                        list.add("FH0109.23");
                        list.add("FH0109.53");
                        serviceSLTDInhospReqVO.setStatusList(list);
//                        log.info("【dealHisData】省立同德“预入院”采集数据入参:{}", serviceSLTDInhospReqVO);
                        serviceSLTDHealthcareRecordService.queryHealthcareRecordList(serviceSLTDInhospReqVO);
                        log.info("【dealHisData】省立同德“预入院”結束采集数据,院区:{}", cid);
                        log.info("【dealHisData】省立同德“入院”开始采集数据,院区:{}", cid);
                        serviceSLTDInhospReqVO.setOrgId("20001001");
                        serviceSLTDInhospReqVO.setCampusId(cid);
                        serviceSLTDInhospReqVO.setEncounterTimeStart(null);
                        serviceSLTDInhospReqVO.setEncounterTimeEnd(null);
                        serviceSLTDInhospReqVO.setStartHeadTime(nowTime);
                        serviceSLTDInhospReqVO.setStartTailTime(nowTime);
                        if (active.trim().equals("nhfy")) {
                            serviceSLTDInhospReqVO.setStartHeadTime(yesterdayZero.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
                            serviceSLTDInhospReqVO.setStartTailTime(todayZero.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
                        }
                        List<String> listStr = new ArrayList<>();
                        listStr.add("FH0109.24");
                        serviceSLTDInhospReqVO.setStatusList(listStr);
//                        log.info("【dealHisData】省立同德“入院”采集数据入参:{}", serviceSLTDInhospReqVO);
                        serviceSLTDHealthcareRecordService.queryHealthcareRecordList(serviceSLTDInhospReqVO);
                        log.info("【dealHisData】省立同德“入院”結束采集数据,院区:{}", cid);
                        log.info("【dealHisData】省立同德“出院”开始采集数据,院区:{}", cid);
                        serviceSLTDInhospReqVO.setStartHeadTime(null);
                        serviceSLTDInhospReqVO.setStartTailTime(null);
                        serviceSLTDInhospReqVO.setEncounterTimeStart(null);
                        serviceSLTDInhospReqVO.setEncounterTimeEnd(null);
                        serviceSLTDInhospReqVO.setPreOutHospitalHeadDate(nowTime);
                        serviceSLTDInhospReqVO.setPreOutHospitalTailDate(nowTime);
                        if (active.trim().equals("nhfy")) {
                            serviceSLTDInhospReqVO.setPreOutHospitalHeadDate(yesterdayZero.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
                            serviceSLTDInhospReqVO.setPreOutHospitalTailDate(todayZero.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
                        }
                        serviceSLTDInhospReqVO.setHealthcareRecordTypeList(null);
                        List<String> list2 = new ArrayList<>();
//                    list2.add("FH0108.02");
                        //出院
                        list2.add("FH0109.27");
                        //预出院
                        list2.add("FH0109.25");
                        serviceSLTDInhospReqVO.setStatusList(list2);
//                        log.info("【dealHisData】省立同德“出院”采集数据入参:{}", serviceSLTDInhospReqVO);
                        serviceSLTDHealthcareRecordService.queryHealthcareRecordList(serviceSLTDInhospReqVO);
                        log.info("【dealHisData】省立同德“出院”束采集数据,院区:{}", cid);
                    }
                }
            } catch (Exception e) {
                log.error("【dealHisData】省立同德数据采集异常", e);
            }
        try {
            dataCollectionFacade.dealHisData(todayZeroStr);
        } catch (com.ruoyi.datacollection.api.DataCollectionModuleDisabledException e) {
            log.warn("【dealHisData】{}(本轮定时采集跳过)", e.getMessage());
        }
    }
    /**
     * åˆ†æ®µé‡‡é›†æ•°æ®ï¼ˆé¢„入院、入院、出院)
     * ç”¨äºŽnhfy/sltd回溯采集,每次最多查询7天数据
     * åœ¨é™¢æ‚£è€…"科室 / ç—…区 / åºŠä½"刷新(南华附一,每天一次)。
     * <p>
     * åœ¨é™¢æ‚£è€…会转科、转病区、换床,而 {@code pat_med_inhosp} çš„位置字段只在入院时写过一次,
     * ä¹‹åŽä¸ä¼šè‡ªå·±æ›´æ–° â€”— è¿™é‡Œæ¯å¤©æŒ‰æ‚£è€…查一遍 HIS é‡Œ<b>该患者当前的在院信息</b>
     * ï¼ˆ{@code dtcQueryHealthcareRecordList},与入院采集是同一个接口,按
     * {@code healthcareRecordNo} å•查 + {@code statusList=["FH0109.24"]}),
     * ä¸Žåº“内比对后只把变化的字段写回去。实现与判定口径见
     * {@code com.ruoyi.datacollection.service.impl.InhospDeptRefreshServiceImpl}。
     * <p>
     * ä¸Ž {@link #dealHisData(String)} åŒæ ·çš„降级口径:模块被关掉(
     * {@code sys_config: datacollection.enabled=false})时门面抛
     * {@link com.ruoyi.datacollection.api.DataCollectionModuleDisabledException},
     * è¿™é‡Œåªæ‰“一条 WARN â€”— é‚£æ˜¯"模块被主动关掉",不该记成定时任务执行异常。
     * <p>
     * sys_job çš„ invoke_target ä¸º {@code ryTask.refreshInhospDeptInfo()},
     * å»ºè®®æ¯å¤© 03:30(避开 01:00 å…¥é™¢é‡‡é›†ä¸Ž 02:00 å‡ºé™¢å¤„理),并发策略选"禁止并发"。
     *
     * @param cid          é™¢åŒºID
     * @param startDateStr æ—¥æœŸå¼€å§‹ yyyy-MM-dd
     * @param endDateStr   æ—¥æœŸç»“束 yyyy-MM-dd
     * @return æœ¬æ¬¡å®žé™…更新的在院记录条数(0 = æ— å˜åŒ– / ç«™ç‚¹ä¸åŒ¹é… / æ¨¡å—被关闭)
     */
    private void collectSegmentData(Long cid, String startDateStr, String endDateStr) {
//        // é¢„入院
        log.info("【dealHisData】预入院开始采集数据,院区:{},时间段:{} ~ {}", cid, startDateStr, endDateStr);
        ServiceSLTDInhospReqVO reqVO = new ServiceSLTDInhospReqVO();
        reqVO.setOrgId("20001001");
        reqVO.setCampusId(cid);
        reqVO.setEncounterTimeStart(startDateStr);
        reqVO.setEncounterTimeEnd(endDateStr);
        List<String> preAdmitStatus = new ArrayList<>();
        preAdmitStatus.add("FH0109.22");
        preAdmitStatus.add("FH0109.23");
        preAdmitStatus.add("FH0109.53");
        reqVO.setStatusList(preAdmitStatus);
        log.info("【dealHisData】预入院采集数据入参:{}", reqVO);
    public int refreshInhospDeptInfo() {
        try {
            serviceSLTDHealthcareRecordService.queryHealthcareRecordList(reqVO);
        } catch (Exception e) {
            // å•类数据失败不再向上抛:否则一段失败会让本次回溯后面的所有段都不执行
            log.error("【dealHisData】预入院采集失败,跳过该类继续。院区:{},时间段:{} ~ {}", cid, startDateStr, endDateStr, e);
            return dataCollectionFacade.refreshInhospDeptInfo();
        } catch (com.ruoyi.datacollection.api.DataCollectionModuleDisabledException e) {
            log.warn("【refreshInhospDeptInfo】{}(本轮在院信息刷新跳过)", e.getMessage());
            return 0;
        }
        log.info("【dealHisData】预入院終了采集数据,院区:{}", cid);
        // å…¥é™¢
        log.info("【dealHisData】入院开始采集数据,院区:{},时间段:{} ~ {}", cid, startDateStr, endDateStr);
        reqVO = new ServiceSLTDInhospReqVO();
        reqVO.setOrgId("20001001");
        reqVO.setCampusId(cid);
        reqVO.setStartHeadTime(startDateStr);
        reqVO.setStartTailTime(endDateStr);
        List<String> admitStatus = new ArrayList<>();
        admitStatus.add("FH0109.24");
        reqVO.setStatusList(admitStatus);
        log.info("【dealHisData】入院采集数据入参:{}", reqVO);
        try {
            serviceSLTDHealthcareRecordService.queryHealthcareRecordList(reqVO);
        } catch (Exception e) {
            // å•类数据失败不再向上抛:否则一段失败会让本次回溯后面的所有段都不执行
            log.error("【dealHisData】入院采集失败,跳过该类继续。院区:{},时间段:{} ~ {}", cid, startDateStr, endDateStr, e);
        }
        log.info("【dealHisData】入院終了采集数据,院区:{}", cid);
        // å‡ºé™¢
        log.info("【dealHisData】出院开始采集数据,院区:{},时间段:{} ~ {}", cid, startDateStr, endDateStr);
        reqVO = new ServiceSLTDInhospReqVO();
        reqVO.setOrgId("20001001");
        reqVO.setCampusId(cid);
        reqVO.setPreOutHospitalHeadDate(startDateStr);
        reqVO.setPreOutHospitalTailDate(endDateStr);
        List<String> dischargeStatus = new ArrayList<>();
        //出院
        dischargeStatus.add("FH0109.27");
        //预出院
        dischargeStatus.add("FH0109.25");
        reqVO.setStatusList(dischargeStatus);
        log.info("【dealHisData】出院采集数据入参:{}", reqVO);
        try {
            serviceSLTDHealthcareRecordService.queryHealthcareRecordList(reqVO);
        } catch (Exception e) {
            // å•类数据失败不再向上抛:否则一段失败会让本次回溯后面的所有段都不执行
            log.error("【dealHisData】出院采集失败,跳过该类继续。院区:{},时间段:{} ~ {}", cid, startDateStr, endDateStr, e);
        }
        log.info("【dealHisData】出院结束采集数据,院区:{}", cid);
    }
    /**
ruoyi-task/src/main/java/com/ruoyi/task/api/contract/VisitTaskCreateApiImpl.java
@@ -186,6 +186,28 @@
        return !CollectionUtils.isEmpty(list);
    }
    /**
     * æ˜¯å¦å­˜åœ¨ã€ŒåŒä¸€æ‚£è€… + åŒä¸€æ¬¡å‡ºé™¢è®°å½• + åŒä¸€ä»»åŠ¡ã€çš„å­ä»»åŠ¡ï¼ˆ{@code del_flag = 0})。
     * <p>
     * ç›´æŽ¥ç”¨çŽ°æˆçš„ {@code selectServiceSubtaskList}(本身就带
     * {@code del_flag = 0} + {@code patid} + {@code inhospid} + {@code taskid} ä¸‰ä¸ªæ¡ä»¶ï¼‰ï¼Œ
     * ä¸æ–°å¢ž XML â€”— é¿å…ä¸ºäº†ä¸€ä¸ª"有没有"再写一条等价 SQL。
     * <p>
     * âš ï¸ ä¸æŒ‰ {@code sendstate} è¿‡æ»¤ï¼šå·²åœç”¨ï¼ˆsendstate=4)的历史行也算"生成过"。
     * å¦åˆ™"停旧一条 + å†ç”Ÿæˆä¸€æ¡"会无限累积 â€”— è¿™æ­£æ˜¯ 2026-10 åŒä¸€æ‚£è€…几十条子任务的成因。
     */
    @Override
    public boolean existsSubtask(Long patid, Long inhospid, Long taskid) {
        if (patid == null || taskid == null) {
            return false;
        }
        ServiceSubtaskEntity q = new ServiceSubtaskEntity();
        q.setPatid(patid);
        q.setInhospid(inhospid);
        q.setTaskid(taskid);
        return !CollectionUtils.isEmpty(serviceSubtaskMapper.selectServiceSubtaskList(q));
    }
    // ==========================================================================
    // å­—段搬运(逐字段对照,勿"顺手精简")
    // ==========================================================================
@@ -194,17 +216,27 @@
        VisitLongTaskDto dto = new VisitLongTaskDto();
        dto.setTaskid(t.getTaskid());
        dto.setTaskName(t.getTaskName());
        dto.setTaskDesc(t.getTaskDesc());
        dto.setType(t.getType());
        dto.setServiceType(t.getServiceType());
        dto.setTemplateid(t.getTemplateid());
        dto.setTemplatename(t.getTemplatename());
        dto.setLibtemplateid(t.getLibtemplateid());
        dto.setLibtemplatename(t.getLibtemplatename());
        dto.setSendDay(t.getSendDay());
        dto.setPreachform(t.getPreachform());
        dto.setPreachformDesc(t.getPreachformDesc());
        dto.setOrgid(t.getOrgid());
        dto.setCampusid(t.getCampusid());
        dto.setHosType(t.getHospType());
        // åŽŸ boxedServiceSubtask ç¬¬ä¸€è¡Œçš„ DtoConversionUtils.sourceToTarget ä¼šæŠŠè¿™ 6 ä¸ª
        // æ‹·è¿›å­ä»»åŠ¡å¹¶éš insertServiceSubtask è½åº“;不搬就是静默丢列(见 VisitLongTaskDto æ³¨é‡Šï¼‰
        dto.setGuid(t.getGuid());
        dto.setPid(t.getPid());
        dto.setSendType(t.getSendType());
        dto.setIsupload(t.getIsupload());
        dto.setTextParam(t.getTextParam());
        dto.setUploadTime(t.getUploadTime());
        dto.setUpdateBy(t.getUpdateBy());
        dto.setCreateBy(t.getCreateBy());
        return dto;
@@ -230,6 +262,7 @@
        e.setSex(d.getSex());
        e.setAge(d.getAge());
        e.setInhospid(d.getInhospid());
        e.setHospno(d.getHospno());
        e.setNurseId(d.getNurseId());
        e.setNurseName(d.getNurseName());
        e.setDrcode(d.getDrcode());
@@ -246,6 +279,7 @@
        e.setTemplateid(d.getTemplateid());
        e.setTemplatename(d.getTemplatename());
        e.setLibtemplateid(d.getLibtemplateid());
        e.setLibtemplatename(d.getLibtemplatename());
        e.setStarttime(d.getStarttime());
        e.setEndtime(d.getEndtime());
        e.setLongSendTime(d.getLongSendTime());
@@ -266,6 +300,29 @@
        e.setOperationItemId(d.getOperationItemId());
        e.setCreateBy(d.getCreateBy());
        e.setUpdateBy(d.getUpdateBy());
        // âš ï¸ create_time / update_time ä¹‹å‰ä¹Ÿæ˜¯æ¼çš„:
        //    ç»„装器(boxedServiceSubtask åŽŸ L1157-1177)、writeInSubTask çš„æ¯ä¸ª insert åˆ†æ”¯
        //    éƒ½åœ¨ setCreateTime/setUpdateTime,XML ä¹Ÿæœ‰å¯¹åº”条件列,但这里没映射 â†’
        //    ä¸¤æ¡æ—¶é—´åˆ—静默落 NULL(2026-10 å®žæœºå‘现)。
        e.setCreateTime(d.getCreateTime());
        e.setUpdateTime(d.getUpdateTime());
        // ------------------------------------------------------------------
        // ä»»åŠ¡æ¥æºå­—æ®µï¼ˆåŽŸ boxedServiceSubtask ç¬¬ä¸€è¡Œ DtoConversionUtils æ•´æ‰¹æ‹·å…¥ï¼‰
        //
        // âš ï¸ è¿™ 8 ä¸ªæ˜¯"任务 â†’ å­ä»»åŠ¡"的快照列。原先只搬了 taskid/serviceType ç­‰ï¼Œ
        //    taskName/taskDesc/type/libtemplatename ä»¥åŠ guid/pid/sendType/isupload/
        //    textParam/uploadTime å…¨éƒ¨æ¼æ˜ å°„ â†’ XML çš„ <if test="xxx != null"> ç›´æŽ¥è·³è¿‡,
        //    service_subtask ä¸Šè¿™äº›åˆ—静默为 NULL(2026-10 ç”± addSubTaskByRule å®žæœºå‘现)。
        // ------------------------------------------------------------------
        e.setTaskName(d.getTaskName());
        e.setTaskDesc(d.getTaskDesc());
        e.setType(d.getType());
        e.setGuid(d.getGuid());
        e.setPid(d.getPid());
        e.setSendType(d.getSendType());
        e.setIsupload(d.getIsupload());
        e.setTextParam(d.getTextParam());
        e.setUploadTime(d.getUploadTime());
        // ------------------------------------------------------------------
        // é—¨æ€¥è¯Šéšè®¿ä¸“用字段(OuthospSubtaskDraft)。
@@ -279,8 +336,8 @@
        if (d instanceof com.ruoyi.common.core.service.dto.OuthospSubtaskDraft) {
            com.ruoyi.common.core.service.dto.OuthospSubtaskDraft o =
                    (com.ruoyi.common.core.service.dto.OuthospSubtaskDraft) d;
            // hospno å·²ä¸Šæåˆ°çˆ¶ç±» VisitSubtaskDraft(上面统一映射过了),这里不再重复
            // åªæœ‰çˆ¶ç±»æ²¡æœ‰çš„字段才在这里补映射(其余如 sendname/sfzh/phone/sex/age çˆ¶ç±»å·²æœ‰ï¼‰
            e.setHospno(o.getHospno());
            e.setAdmindate(o.getAdmitdate());
            e.setFinishtime(o.getFinishtime());
            // âš ï¸ åŽŸå®žçŽ°ç¡¬ç¼–ç  "1"(不读 serviceTask.getHospType()),此处保持一致
ruoyi-task/src/main/java/com/ruoyi/task/domain/PatTaskRelevance.java
@@ -258,6 +258,21 @@
    private String diagname;
    /**
     * ICD/疾病名称({@code pat_task_relevance.icd_name} åˆ—)
     * <p>
     * âš ï¸ ä¸èƒ½çœï¼šsmartor çš„ {@code PatHetaskRelevanceMapper.xml}
     * ï¼ˆnamespace æ˜¯ {@code com.smartor.mapper.PatTaskRelevanceMapper},被
     * {@code IPatTaskRelevanceService} ä½¿ç”¨ï¼‰é‡Œ resultMap æ˜¯
     * {@code property="icdName" column="icd_name"},select/insert/update ä¹Ÿéƒ½ç”¨
     * {@code icdName}。本实体是「任务模块化」时从 smartor æ¬è¿‡æ¥çš„,
     * æ¬çš„æ—¶å€™å°‘了这个字段 â€”— äºŽæ˜¯é‚£æ¡ mapper çš„**每一次调用**都会抛
     * {@code ReflectionException: no getter for 'icdName'}。
     */
    @Excel(name = "ICD名称")
    @ApiModelProperty(value = "ICD/疾病名称(icd_name åˆ—)")
    private String icdName;
    /**
     * æ˜¯å¦å­˜åœ¨æ“ä½œï¼š1 æ–°å¢ž 2修改 3删除
     */
    @ApiModelProperty(value = "是否存在操作:1 æ–°å¢ž 2修改 3删除")
ruoyi-task/src/main/resources/mapper/task/ServiceSubtaskMapper.xml
@@ -906,6 +906,10 @@
            <if test="deptname != null  and deptname != ''">deptname,</if>
            <if test="leaveicd10code != null  and leaveicd10code != ''">leaveicd10code,</if>
            <if test="leavediagname != null  and leavediagname != ''">leavediagname,</if>
            <!-- diagname:入院诊断名称。原 insert æ¼äº†è¿™ä¸€åˆ—,而出院链路的
                 boxedServiceSubtask ä¸€ç›´åœ¨ setDiagname(patMedInhosp.diagname),
                 äºŽæ˜¯ service_subtask.diagname æ°¸è¿œä¸º NULL(select ä¾§å´ä¸€ç›´åœ¨è¯»å®ƒï¼‰ã€‚ -->
            <if test="diagname != null  and diagname != ''">diagname,</if>
            <if test="libtemplateid != null ">libtemplateid,</if>
            <if test="libtemplatename != null ">libtemplatename,</if>
            <if test="patid != null ">patid,</if>
@@ -1004,6 +1008,7 @@
            <if test="deptname != null  and deptname != ''">#{deptname},</if>
            <if test="leaveicd10code != null  and leaveicd10code != ''">#{leaveicd10code},</if>
            <if test="leavediagname != null  and leavediagname != ''">#{leavediagname},</if>
            <if test="diagname != null  and diagname != ''">#{diagname},</if>
            <if test="libtemplateid != null ">#{libtemplateid},</if>
            <if test="libtemplatename != null ">#{libtemplatename},</if>
            <if test="patid != null ">#{patid},</if>
ruoyi-template/src/main/java/com/ruoyi/template/domain/IvrLibaExtemplatescript.java
@@ -31,6 +31,17 @@
    private Long ID;
    /**
     * æ‰¹é‡ä¸»é”®é›†åˆï¼ˆåªä½œæŸ¥è¯¢æ¡ä»¶ï¼Œä¸æ˜¯è¡¨åˆ—)
     * <p>
     * {@code IvrLibaExtemplatescriptMapper.xml#queryIvrLibaExtemplatescriptList} ç”¨
     * {@code <if test="IDs != null">} + {@code <foreach collection="IDs">} æ‹¼
     * {@code extID in (...)};实体上原本只有单个 {@code ID},所以那条语句一执行就抛
     * {@code ReflectionException: no getter for 'IDs'}。此处补上集合字段(列名不受影响)。
     */
    @ApiModelProperty("批量主键集合(查询条件)")
    private List<Long> IDs;
    /**
     * æ¨¡ç‰ˆåº“id
     */
    @Excel(name = "模版库id")
ruoyi-template/src/main/java/com/ruoyi/template/domain/VO/HeLibraryCountVO.java
@@ -42,6 +42,17 @@
    private String orgid;
    /**
     * é™¢åŒºID
     * <p>
     * âš ï¸ å¿…须存在:{@code ServiceSubtaskMapper.xml} çš„ {@code getHeLibraryCount} /
     * {@code gethelibraryCountHyperlink} é‡Œæœ‰ {@code <if test="campusid != null">},
     * MyBatis ç”¨ OGNL æ±‚值该表达式,缺 getter ä¼šç›´æŽ¥æŠ›
     * {@code ReflectionException: no getter for 'campusid'}(不是"条件不成立")。
     */
    @ApiModelProperty(value = "院区ID")
    private String campusid;
    /**
     * å‘送状态
     */
    @Excel(name = " å‘送状态 ")
ruoyi-template/src/main/resources/mapper/template/IvrLibaTargetMapper.xml
@@ -178,7 +178,10 @@
            <if test="targetdesc != null  and targetdesc != ''">and a.targetdesc = #{targetdesc}</if>
            <if test="language != null  and language != ''">and a.language = #{language}</if>
            <if test="version != null ">and a.version = #{version}</if>
            <if test="valueType != null ">and a.valueType = #{value_type}</if>
            <!-- åŽŸä¸º `and a.valueType = #{value_type}`:列名与属性名写反了(列是 value_type、
                 å±žæ€§æ˜¯ valueType)。MyBatis æ±‚值会抛
                 ReflectionException: no getter for 'value_type',且 a.valueType ä¹Ÿä¸æ˜¯åˆæ³•列。 -->
            <if test="valueType != null ">and a.value_type = #{valueType}</if>
            <if test="isabnormal != null ">and a.isabnormal = #{isabnormal}</if>
            <if test="warnup != null ">and a.warnup = #{warnup}</if>
            <if test="warndown != null ">and a.warndown = #{warndown}</if>
ruoyi-template/src/main/resources/mapper/template/IvrTaskTemplateMapper.xml
@@ -147,7 +147,10 @@
            <if test="revisitBefore != null  and revisitBefore != ''">and revisit_before = #{revisitBefore}</if>
            <if test="revisitAfter != null  and revisitAfter != ''">and revisit_after = #{revisitAfter}</if>
            <if test="noVoiceNum != null ">and no_voice_num = #{noVoiceNum}</if>
            <if test="recallcount != null">and recallcount = #{prologrecallcountue}</if>
            <!-- åŽŸä¸º `#{prologrecallcountue}`:实体上没有这个属性(是把 `prolog` ç²˜åˆ°äº†
                 `recallcount` å‰é¢ï¼‰ï¼Œtest åˆ¤çš„æ˜¯ recallcount å´å–不到值,
                 è¿è¡ŒæœŸæŠ› ReflectionException。属性按 test çš„ recallcount å–。 -->
            <if test="recallcount != null">and recallcount = #{recallcount}</if>
            <if test="deptCode != null">and dept_code = #{deptCode}</if>
            <if test="longTemp != null">and long_temp = #{longTemp}</if>
    </select>
ruoyi-template/src/main/resources/mapper/template/SvyLibScriptMapper.xml
@@ -138,7 +138,9 @@
            <if test="language != null and language!=''">language,</if>
            <if test="otherdata != null and otherdata!=''">otherdata,</if>
            <if test="valueType != null and valueType!=''">value_type,</if>
            <if test="reply != null and reply!=''">otherdata,</if>
            <!-- åŽŸä¸º `otherdata,`:与上面那条重复写同一个列(reply çš„内容会盖掉 otherdata),
                 è¡¨é‡Œ reply æ˜¯ç‹¬ç«‹åˆ—(见本文件 resultMap: property="reply" column="reply")。 -->
            <if test="reply != null and reply!=''">reply,</if>
            <if test="scoretype != null and scoretype!=''">scoretype,</if>
            <if test="score != null ">score,</if>
            <if test="icd10Name != null and icd10Name!=''">icd10_name,</if>
@@ -173,7 +175,9 @@
            <if test="language != null  and language!=''">#{language},</if>
            <if test="otherdata != null and otherdata!=''">#{otherdata},</if>
            <if test="valueType != null and valueType!=''">#{valueType},</if>
            <if test="reply != null and reply!=''">#{therdata},</if>
            <!-- åŽŸä¸º `#{therdata}`:实体上没有 therdata(是 otherdata çš„错拼),
                 è¿è¡ŒæœŸæŠ› ReflectionException。列与属性都按 reply å–。 -->
            <if test="reply != null and reply!=''">#{reply},</if>
            <if test="scoretype != null and scoretype!=''">#{scoretype},</if>
            <if test="score != null ">#{score},</if>
            <if test="icd10Name != null and icd10Name!=''">#{icd10Name},</if>
@@ -212,7 +216,10 @@
            <if test="language != null and language!=''">language = #{language},</if>
            <if test="otherdata != null and otherdata!=''">otherdata = #{otherdata},</if>
            <if test="valueType != null and valueType!=''">value_type = #{valueType},</if>
            <if test="reply != null and reply!=''">therdata =#{therdata},</if>
            <!-- åŽŸä¸º `therdata =#{therdata}`:同一列 otherdata å·²è¢«ä¸Šé¢èµ‹è¿‡å€¼ï¼Œ
                 MySQL ä¼šæŠ¥ "Column 'otherdata' specified twice",且属性 therdata ä¸å­˜åœ¨ã€‚
                 æŒ‰ reply åˆ— + reply å±žæ€§ä¿®æ­£ã€‚ -->
            <if test="reply != null and reply!=''">reply = #{reply},</if>
            <if test="scoretype != null and scoretype!=''">scoretype = #{scoretype},</if>
            <if test="score != null">score = #{score},</if>
            <if test="icd10Name != null and icd10Name!=''">icd10_name = #{icd10Name},</if>
ruoyi-template/src/main/resources/mapper/template/SvyTaskScriptMapper.xml
@@ -61,7 +61,10 @@
            <if test="scoretype != null  and scoretype != ''"> and scoretype = #{scoretype}</if>
            <if test="score != null  and score != ''"> and score = #{score}</if>
            <if test="showtype != null  and showtype != ''"> and showtype = #{showtype}</if>
            <if test="picturePath != null  and picturePath != ''"> and picture_path = #{picturePath}</if>
            <!-- åŽŸä¸º `and picture_path = #{picturePath}`:svy_task_script æ²¡æœ‰ picture_path åˆ—
                 ï¼ˆæœ¬æ–‡ä»¶ resultMap / select åˆ—清单里只有 script_picture),实体也没有
                 picturePath å±žæ€§ â€”— ä¸¤å¤„都不存在,这条条件从来不可能生效。
                 æŒ‰"只认真实列"删掉:图片由下面的 scriptPicture / script_picture è´Ÿè´£ã€‚ -->
    </select>
    <select id="selectSvyTaskScriptById" parameterType="Long" resultMap="SvyTaskScriptResult">
@@ -157,7 +160,8 @@
            <if test="scoretype != null">scoretype = #{scoretype},</if>
            <if test="score != null">score = #{score},</if>
            <if test="showtype != null">showtype = #{showtype},</if>
            <if test="picturePath != null">picture_path = #{picturePath},</if>
            <!-- åŒä¸Šï¼špicture_path åˆ—与 picturePath å±žæ€§éƒ½ä¸å­˜åœ¨ï¼Œåˆ æŽ‰è¿™æ¡ï¼ˆå¦åˆ™ä¸€æ—¦æœ‰äºº
                 ç»™å®žä½“补上 picturePath,SQL å°±ä¼šå›  Unknown column 'picture_path' æŠ¥é”™ï¼‰ã€‚ -->
            <if test="scriptPicture != null">script_picture = #{scriptPicture},</if>
        </trim>
        where id = #{id}
ruoyi-template/src/main/resources/mapper/template/SvyTaskTemplateScriptMapper.xml
@@ -336,7 +336,9 @@
            <if test="circulationDeptPersonName != null  and circulationDeptPersonName != ''">circulation_dept_person_name = #{circulationDeptPersonName},</if>
            <if test="circulationDeptPersonCode != null  and circulationDeptPersonCode != ''">circulation_dept_person_code = #{circulationDeptPersonCode},</if>
        </trim>
        where id = #{d}
        <!-- åŽŸä¸º `where id = #{d}`:属性 d ä¸å­˜åœ¨ï¼Œæ›´æ–°è¯­å¥ä¸€æ‰§è¡Œå°±æŠ›
             ReflectionException(no getter for 'd')。主键属性是 id。 -->
        where id = #{id}
    </update>
    <delete id="deleteSvyTaskTemplateScriptByID" parameterType="Long">
smartor/src/main/java/com/smartor/domain/ServiceThirdData.java
@@ -82,6 +82,25 @@
    @Excel(name = "备注")
    private Long userInfoId;
    /**
     * å”¯ä¸€æ ‡è¯†
     * <p>
     * âš ï¸ å¿…须存在:{@code ServiceThirdDataMapper.xml} çš„ resultMap å·²æ˜ å°„
     * {@code property="guid" column="guid"},三条 insert ä¹Ÿéƒ½æœ‰
     * {@code <if test="guid != null">}。实体缺这个字段时,
     * æŸ¥è¯¢ä¼šåœ¨ç»“果映射阶段、插入会在 OGNL æ±‚值阶段直接抛 ReflectionException。
     */
    @ApiModelProperty("唯一标识")
    private String guid;
    /**
     * æœºæž„ID
     * <p>
     * âš ï¸ åŒä¸Šï¼šresultMap æ˜ å°„ {@code property="orgid" column="orgid"},
     * {@code selectServiceThirdDataList} ä¸Žä¸‰æ¡ insert éƒ½å¼•用它。
     */
    @ApiModelProperty("机构ID")
    private String orgid;
}
smartor/src/main/java/com/smartor/service/IServiceSLTDHealthcareRecordService.java
@@ -1,6 +1,7 @@
package com.smartor.service;
import afu.org.checkerframework.checker.oigj.qual.O;
import com.ruoyi.common.core.service.dto.InhospPositionRecord;
import com.smartor.domain.MedicationItem;
import com.smartor.domain.ServiceSLTDDeptReqVO;
import com.smartor.domain.ServiceSLTDInhospReqVO;
@@ -80,5 +81,22 @@
     */
    Boolean queryDeptWardAreaInfoList(ServiceSLTDDeptReqVO serviceSLTDDeptReqVO);
    /**
     * æŸ¥è¯¢å•个在院患者的<b>当前</b>科室 / ç—…区 / åºŠä½
     * ï¼ˆä¸Žå…¥é™¢é‡‡é›†åŒä¸€ä¸ªæŽ¥å£ {@code dtcQueryHealthcareRecordList},用 {@code healthcareRecordNo} å•查)。
     * <p>
     * âš ï¸ <b>只取数,不落库</b>:由调用方(数据采集模块的"在院信息刷新")自己与
     * {@code pat_med_inhosp} æ¯”对。取数失败(无响应 / éž 200)<b>抛 RuntimeException</b>,
     * ä¸ç”¨ç©ºåˆ—表冒充"该患者已不在院"。
     * <p>
     * å…¥å‚约定:{@code orgId} + {@code campusId} + {@code statusList=["FH0109.24"]} +
     * {@code healthcareRecordNo}。{@code statusList} ç”± HIS ä¾§è¿‡æ»¤æŽ‰å·²å‡ºé™¢/预出院的记录,
     * å› æ­¤<b>返回空列表即表示"当前不在入院状态"</b>。
     *
     * @param reqVO è§ä¸Š
     * @return å½“前位置记录;空列表 = è¯¥æ‚£è€…当前不在"入院"状态
     */
    List<InhospPositionRecord> queryInhospPosition(ServiceSLTDInhospReqVO reqVO);
}
smartor/src/main/java/com/smartor/service/impl/ServiceSLTDHealthcareRecordServiceImpl.java
@@ -10,6 +10,7 @@
import com.ruoyi.common.core.domain.entity.SysUserRole;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.core.service.dto.InhospPositionRecord;
import com.smartor.common.enums.EmergencyLevelEnum;
import com.smartor.common.enums.ImportFlagEnum;
import com.ruoyi.common.utils.http.HttpUtils;
@@ -52,6 +53,23 @@
public class ServiceSLTDHealthcareRecordServiceImpl implements IServiceSLTDHealthcareRecordService {
    private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
    /**
     * åœ¨é™¢æ‚£è€…当前位置查询地址(网关根路径 {@code sltd_pub_path} + å›ºå®šåŽç¼€ï¼‰ã€‚
     * <p>
     * ä¸Ž"入院采集"是<b>同一个接口</b>({@code queryHealthcareRecordList} ç”¨çš„也是它),
     * åŒºåˆ«åªæ˜¯æœ¬æ–¹æ³•按 {@code healthcareRecordNo} å•查、只取位置字段、不落库。
     */
    private static final String INHOSP_POSITION_PATH =
            "/osj/hbos-thirdparty-integration/standard/common/healthcareRecord/dtcQueryHealthcareRecordList";
    /**
     * å½“前位置查询的连接/读超时:一轮要按患者调用几千次,
     * ä¸èƒ½è®©ä¸€æ¬¡æ— å“åº”把采集线程无限期挂住({@code HttpUtils} è€æ–¹æ³•没有超时)
     */
    private static final int INHOSP_POSITION_CONNECT_TIMEOUT_MS = 5000;
    private static final int INHOSP_POSITION_READ_TIMEOUT_MS = 15000;
    @Autowired
    private IPatArchiveService patArchiveService;
@@ -426,6 +444,47 @@
        return true;
    }
    @Override
    public List<InhospPositionRecord> queryInhospPosition(ServiceSLTDInhospReqVO reqVO) {
        // ä¸Ž"入院采集"共用同一套入参构造:orgId + campusIds + statusList + healthcareRecordNo
        Map<String, Object> requestParams = buildRequestParams(reqVO);
        Map<String, String> headers = buildRequestHeaders();
        String url = sltdPubPath + INHOSP_POSITION_PATH;
        String result = HttpUtils.sendPostByHeader(url, new Gson().toJson(requestParams), headers,
                INHOSP_POSITION_CONNECT_TIMEOUT_MS, INHOSP_POSITION_READ_TIMEOUT_MS);
        if (StringUtils.isEmpty(result)) {
            // sendPostByHeader æŠŠå¼‚常吞掉后返回空串:这里绝不能当成"该患者已不在院",
            // å¦åˆ™æŽ¥å£æ•´ä½“不可用时会被记成"几千个患者都出院了",与真相反。
            throw new RuntimeException("查询在院患者当前位置接口无响应,healthcareRecordNo=" + reqVO.getHealthcareRecordNo());
        }
        // getDataList åœ¨è¿”回码非 200 æ—¶æŠ› RuntimeException;data ä¸º [](该患者已不在入院状态)时返回空列表
        List<Map<String, Object>> dataList = getDataList(result, 1);
        List<InhospPositionRecord> records = new ArrayList<>(dataList.size());
        for (Map<String, Object> dataItem : dataList) {
            records.add(convertToInhospPosition(dataItem));
        }
        return records;
    }
    /**
     * å¥åº·è®°å½• JSON â†’ å¥‘约 DTO çš„逐字段搬运(只取"当前位置"要用的字段)。
     * <p>
     * ä¸€å¾‹èµ° {@code getStringValue} / {@code getLongValue}:接口对空值可能返回 {@code null}、
     * {@code ""} ç”šè‡³ç¼ºå­—段,直接让 Gson ååºåˆ—化到 Long å­—段会在空串上抛 NumberFormatException。
     */
    private InhospPositionRecord convertToInhospPosition(Map<String, Object> dataItem) {
        InhospPositionRecord record = new InhospPositionRecord();
        record.setHealthcareRecordNo(getStringValue(dataItem, "healthcareRecordNo"));
        record.setPatientName(getStringValue(dataItem, "patientName"));
        record.setDeptId(getLongValue(dataItem, "deptId"));
        record.setDeptName(getStringValue(dataItem, "deptName"));
        record.setAreaId(getLongValue(dataItem, "areaId"));
        record.setAreaName(getStringValue(dataItem, "areaName"));
        record.setBedNumber(getStringValue(dataItem, "bedNumber"));
        record.setHealthcareRecordStatus(getStringValue(dataItem, "healthcareRecordStatus"));
        return record;
    }
    private String determineCry(ServiceSLTDInhospReqVO reqVO) {
        if (CollectionUtils.isEmpty(reqVO.getStatusList()) && CollectionUtils.isEmpty(reqVO.getHealthcareRecordTypeList()))
            return null;
smartor/src/main/resources/mapper/smartor/HospitalRecordMapper.xml
@@ -204,7 +204,7 @@
            <if test="directorName != null  and directorName != ''">and DirectorName like concat('%', #{directorName},
                '%')
            </if>
            <if test="dncDiagnosisCode != null  and dncDiagnosisCode != ''">and EncDiagnosisCode = #{dncDiagnosisCode}
            <if test="encDiagnosisCode != null  and encDiagnosisCode != ''">and EncDiagnosisCode = #{encDiagnosisCode}
            </if>
            <if test="encDiagnosisDesc != null  and encDiagnosisDesc != ''">and EncDiagnosisDesc = #{encDiagnosisDesc}
            </if>
@@ -350,62 +350,62 @@
    <update id="updateHospitalRecord" parameterType="com.smartor.domain.HospitalRecord">
        update hospital_record
        <trim prefix="SET" suffixOverrides=",">
            <if test="SourceSystem != null">SourceSystem = #{SourceSystem},</if>
            <if test="MessageID != null">MessageID = #{MessageID},</if>
            <if test="CreateTime != null">CreateTime = #{CreateTime},</if>
            <if test="MethodName != null">MethodName = #{MethodName},</if>
            <if test="ReqIimRules != null">ReqIimRules = #{ReqIimRules},</if>
            <if test="PatientID != null">PatientID = #{PatientID},</if>
            <if test="OpreationTime != null">OpreationTime = #{OpreationTime},</if>
            <if test="EpisodeID != null">EpisodeID = #{EpisodeID},</if>
            <if test="OutMedNO != null">OutMedNO = #{OutMedNO},</if>
            <if test="InMedNO != null">InMedNO = #{InMedNO},</if>
            <if test="VisitNum != null">VisitNum = #{VisitNum},</if>
            <if test="AdmTypeCode != null">AdmTypeCode = #{AdmTypeCode},</if>
            <if test="AdmTypeDesc != null">AdmTypeDesc = #{AdmTypeDesc},</if>
            <if test="AdmStatus != null">AdmStatus = #{AdmStatus},</if>
            <if test="AdmDate != null">AdmDate = #{AdmDate},</if>
            <if test="EncPatName != null">EncPatName = #{EncPatName},</if>
            <if test="EncPatGender != null">EncPatGender = #{EncPatGender},</if>
            <if test="EncPatBirthDate != null">EncPatBirthDate = #{EncPatBirthDate},</if>
            <if test="Transferstart != null">Transferstart = #{Transferstart},</if>
            <if test="Transferend != null">Transferend = #{Transferend},</if>
            <if test="EncPatMaritalStatus != null">EncPatMaritalStatus = #{EncPatMaritalStatus},</if>
            <if test="OriginalText != null">OriginalText = #{OriginalText},</if>
            <if test="EncDocList != null">EncDocList = #{EncDocList},</if>
            <if test="EncAdmLocList != null">EncAdmLocList = #{EncAdmLocList},</if>
            <if test="sourceSystem != null">SourceSystem = #{sourceSystem},</if>
            <if test="messageID != null">MessageID = #{messageID},</if>
            <if test="createTime != null">CreateTime = #{createTime},</if>
            <if test="methodName != null">MethodName = #{methodName},</if>
            <if test="reqIimRules != null">ReqIimRules = #{reqIimRules},</if>
            <if test="patientID != null">PatientID = #{patientID},</if>
            <if test="opreationTime != null">OpreationTime = #{opreationTime},</if>
            <if test="episodeID != null">EpisodeID = #{episodeID},</if>
            <if test="outMedNO != null">OutMedNO = #{outMedNO},</if>
            <if test="inMedNO != null">InMedNO = #{inMedNO},</if>
            <if test="visitNum != null">VisitNum = #{visitNum},</if>
            <if test="admTypeCode != null">AdmTypeCode = #{admTypeCode},</if>
            <if test="admTypeDesc != null">AdmTypeDesc = #{admTypeDesc},</if>
            <if test="admStatus != null">AdmStatus = #{admStatus},</if>
            <if test="admDate != null">AdmDate = #{admDate},</if>
            <if test="encPatName != null">EncPatName = #{encPatName},</if>
            <if test="encPatGender != null">EncPatGender = #{encPatGender},</if>
            <if test="encPatBirthDate != null">EncPatBirthDate = #{encPatBirthDate},</if>
            <if test="transferstart != null">Transferstart = #{transferstart},</if>
            <if test="transferend != null">Transferend = #{transferend},</if>
            <if test="encPatMaritalStatus != null">EncPatMaritalStatus = #{encPatMaritalStatus},</if>
            <if test="originalText != null">OriginalText = #{originalText},</if>
            <if test="encDocList != null">EncDocList = #{encDocList},</if>
            <if test="encAdmLocList != null">EncAdmLocList = #{encAdmLocList},</if>
            <if test="encounterPeriodStart != null">Encounter_period_start = #{encounterPeriodStart},</if>
            <if test="encounterPeriodEnd != null">Encounter_period_end = #{encounterPeriodEnd},</if>
            <if test="encounterBooktime != null">Encounter_booktime = #{encounterBooktime},</if>
            <if test="EncAdmSource != null">EncAdmSource = #{EncAdmSource},</if>
            <if test="EncInhostype != null">EncInhostype = #{EncInhostype},</if>
            <if test="EncDietName != null">EncDietName = #{EncDietName},</if>
            <if test="EncOuthostype != null">EncOuthostype = #{EncOuthostype},</if>
            <if test="EncVisitnum != null">EncVisitnum = #{EncVisitnum},</if>
            <if test="EncChargeType != null">EncChargeType = #{EncChargeType},</if>
            <if test="EncChargeTypeName != null">EncChargeTypeName = #{EncChargeTypeName},</if>
            <if test="EncChargeCode != null">EncChargeCode = #{EncChargeCode},</if>
            <if test="EncChargeName != null">EncChargeName = #{EncChargeName},</if>
            <if test="EncReglevelcode != null">EncReglevelcode = #{EncReglevelcode},</if>
            <if test="EncCareLevel != null">EncCareLevel = #{EncCareLevel},</if>
            <if test="Specialday != null">Specialday = #{Specialday},</if>
            <if test="Primaryday != null">Primaryday = #{Primaryday},</if>
            <if test="Secondaryday != null">Secondaryday = #{Secondaryday},</if>
            <if test="Tertiaryday != null">Tertiaryday = #{Tertiaryday},</if>
            <if test="Enroomno != null">Enroomno = #{Enroomno},</if>
            <if test="EnBedno != null">EnBedno = #{EnBedno},</if>
            <if test="EncSeeno != null">EncSeeno = #{EncSeeno},</if>
            <if test="EncAgeUnit != null">EncAgeUnit = #{EncAgeUnit},</if>
            <if test="EncAgeValue != null">EncAgeValue = #{EncAgeValue},</if>
            <if test="EnIsbaby != null">EnIsbaby = #{EnIsbaby},</if>
            <if test="EncHavebaby != null">EncHavebaby = #{EncHavebaby},</if>
            <if test="EncDiagList != null">EncDiagList = #{EncDiagList},</if>
            <if test="DirectorCode != null">DirectorCode = #{DirectorCode},</if>
            <if test="DirectorName != null">DirectorName = #{DirectorName},</if>
            <if test="EncDiagnosisCode != null">EncDiagnosisCode = #{EncDiagnosisCode},</if>
            <if test="EncDiagnosisDesc != null">EncDiagnosisDesc = #{EncDiagnosisDesc},</if>
            <if test="EncAdmLocCode != null">EncAdmLocCode = #{EncAdmLocCode},</if>
            <if test="EncAdmLocName != null">EncAdmLocName = #{EncAdmLocName},</if>
            <if test="encAdmSource != null">EncAdmSource = #{encAdmSource},</if>
            <if test="encInhostype != null">EncInhostype = #{encInhostype},</if>
            <if test="encDietName != null">EncDietName = #{encDietName},</if>
            <if test="encOuthostype != null">EncOuthostype = #{encOuthostype},</if>
            <if test="encVisitnum != null">EncVisitnum = #{encVisitnum},</if>
            <if test="encChargeType != null">EncChargeType = #{encChargeType},</if>
            <if test="encChargeTypeName != null">EncChargeTypeName = #{encChargeTypeName},</if>
            <if test="encChargeCode != null">EncChargeCode = #{encChargeCode},</if>
            <if test="encChargeName != null">EncChargeName = #{encChargeName},</if>
            <if test="encReglevelcode != null">EncReglevelcode = #{encReglevelcode},</if>
            <if test="encCareLevel != null">EncCareLevel = #{encCareLevel},</if>
            <if test="specialday != null">Specialday = #{specialday},</if>
            <if test="primaryday != null">Primaryday = #{primaryday},</if>
            <if test="secondaryday != null">Secondaryday = #{secondaryday},</if>
            <if test="tertiaryday != null">Tertiaryday = #{tertiaryday},</if>
            <if test="enroomno != null">Enroomno = #{enroomno},</if>
            <if test="enBedno != null">EnBedno = #{enBedno},</if>
            <if test="encSeeno != null">EncSeeno = #{encSeeno},</if>
            <if test="encAgeUnit != null">EncAgeUnit = #{encAgeUnit},</if>
            <if test="encAgeValue != null">EncAgeValue = #{encAgeValue},</if>
            <if test="enIsbaby != null">EnIsbaby = #{enIsbaby},</if>
            <if test="encHavebaby != null">EncHavebaby = #{encHavebaby},</if>
            <if test="encDiagList != null">EncDiagList = #{encDiagList},</if>
            <if test="directorCode != null">DirectorCode = #{directorCode},</if>
            <if test="directorName != null">DirectorName = #{directorName},</if>
            <if test="encDiagnosisCode != null">EncDiagnosisCode = #{encDiagnosisCode},</if>
            <if test="encDiagnosisDesc != null">EncDiagnosisDesc = #{encDiagnosisDesc},</if>
            <if test="encAdmLocCode != null">EncAdmLocCode = #{encAdmLocCode},</if>
            <if test="encAdmLocName != null">EncAdmLocName = #{encAdmLocName},</if>
            <if test="taskStatus != null">taskStatus = #{taskStatus},</if>
            <if test="guid != null">guid = #{guid},</if>
            <if test="orgid != null">orgid = #{orgid},</if>
smartor/src/main/resources/mapper/smartor/HospitalUserinfoMapper.xml
@@ -8,7 +8,10 @@
        <result property="ID" column="ID"/>
        <result property="SourceSystem" column="SourceSystem"/>
        <result property="MessageID" column="MessageID"/>
        <result property="CreateTime" column="CreateTime"/>
        <!-- åŽŸä¸º property="CreateTime":实体上只有 createTime(其余 ID/SourceSystem/MessageID/
             PatientID ç­‰å¤§å†™å±žæ€§åœ¨å®žä½“里确有同名字段,只有这个没有),
             ç»“果映射会抛 ReflectionException: no setter for 'CreateTime'。 -->
        <result property="createTime" column="CreateTime"/>
        <result property="MethodName" column="MethodName"/>
        <result property="ReqIimRules" column="ReqIimRules"/>
        <result property="PatientID" column="PatientID"/>
@@ -186,7 +189,7 @@
        where 1=1
            <if test="SourceSystem != null  and SourceSystem != ''">and SourceSystem = #{SourceSystem}</if>
            <if test="MessageID != null  and MessageID != ''">and MessageID = #{MessageID}</if>
            <if test="CreateTime != null  and CreateTime != ''">and CreateTime = #{CreateTime}</if>
            <if test="createTime != null  and createTime != ''">and CreateTime = #{createTime}</if>
            <if test="MethodName != null  and MethodName != ''">and MethodName like concat('%', #{MethodName}, '%')</if>
            <if test="ReqIimRules != null  and ReqIimRules != ''">and ReqIimRules = #{ReqIimRules}</if>
            <if test="PatientID != null  and PatientID != ''">and PatientID = #{PatientID}</if>
@@ -295,7 +298,7 @@
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="SourceSystem != null">SourceSystem,</if>
            <if test="MessageID != null">MessageID,</if>
            <if test="CreateTime != null  and CreateTime != ''">CreateTime,</if>
            <if test="createTime != null  and createTime != ''">CreateTime,</if>
            <if test="MethodName != null">MethodName,</if>
            <if test="ReqIimRules != null">ReqIimRules,</if>
            <if test="PatientID != null">PatientID,</if>
@@ -381,7 +384,7 @@
        <trim prefix="values (" suffix=")" suffixOverrides=",">
            <if test="SourceSystem != null">#{SourceSystem},</if>
            <if test="MessageID != null">#{MessageID},</if>
            <if test="CreateTime != null and CreateTime != ''">#{CreateTime},</if>
            <if test="createTime != null and createTime != ''">#{createTime},</if>
            <if test="MethodName != null">#{MethodName},</if>
            <if test="ReqIimRules != null">#{ReqIimRules},</if>
            <if test="PatientID != null">#{PatientID},</if>
@@ -471,7 +474,7 @@
        <trim prefix="SET" suffixOverrides=",">
            <if test="SourceSystem != null">SourceSystem = #{SourceSystem},</if>
            <if test="MessageID != null">MessageID = #{MessageID},</if>
            <if test="CreateTime != null">CreateTime = #{CreateTime},</if>
            <if test="createTime != null">CreateTime = #{createTime},</if>
            <if test="MethodName != null">MethodName = #{MethodName},</if>
            <if test="ReqIimRules != null">ReqIimRules = #{ReqIimRules},</if>
            <if test="PatientID != null">PatientID = #{PatientID},</if>
@@ -562,7 +565,7 @@
        <trim prefix="SET" suffixOverrides=",">
            <if test="SourceSystem != null">SourceSystem = #{SourceSystem},</if>
            <if test="MessageID != null">MessageID = #{MessageID},</if>
            <if test="CreateTime != null">CreateTime = #{CreateTime},</if>
            <if test="createTime != null">CreateTime = #{createTime},</if>
            <if test="MethodName != null">MethodName = #{MethodName},</if>
            <if test="ReqIimRules != null">ReqIimRules = #{ReqIimRules},</if>
            <if test="PatientID != null">PatientID = #{PatientID},</if>
smartor/src/main/resources/mapper/smartor/PatArchiveMapper.xml
@@ -613,7 +613,10 @@
            and d.orgid = #{orgid}
            <if test="name != null  and name != ''">and a.name like concat('%', #{name}, '%')</if>
            <if test="idcardno != null  and idcardno != ''">and a.idcardno = #{idcardno}</if>
            <if test="pid != null  and pid != ''">and a.id = #{id}</if>
            <!-- åŽŸä¸º `and a.id = #{id}`:PatArchiveReq ä¸Šæ²¡æœ‰ id(只有 pid),
                 æ¡ä»¶åˆ¤çš„æ˜¯ pid å´å– id â€”— è¿™æ¡æŸ¥è¯¢ï¼ˆæŒ‰ä½é™¢è®°å½•查档案)一执行就抛
                 ReflectionException: no getter for 'id'。占位符按 pid å–。 -->
            <if test="pid != null  and pid != ''">and a.id = #{pid}</if>
            <if test="campusid != null  and campusid != ''">and a.campusid = #{campusid}</if>
            <if test="telcode != null  and telcode != ''">and a.telcode = #{telcode}</if>
            <if test="notrequiredFlag != null  and notrequiredFlag != ''">and a.notrequired_flag = #{notrequiredFlag}
@@ -938,7 +941,8 @@
            and d.orgid = #{orgid}
            <if test="name != null  and name != ''">and a.name like concat('%', #{name}, '%')</if>
            <if test="idcardno != null  and idcardno != ''">and a.idcardno = #{idcardno}</if>
            <if test="pid != null  and pid != ''">and a.id = #{id}</if>
            <!-- åŒä¸Šï¼ˆQC ç‰ˆè¯­å¥ï¼‰ï¼šå ä½ç¬¦ id -> pid。 -->
            <if test="pid != null  and pid != ''">and a.id = #{pid}</if>
            <if test="telcode != null  and telcode != ''">and a.telcode = #{telcode}</if>
            <if test="notrequiredFlag != null  and notrequiredFlag != ''">and a.notrequired_flag = #{notrequiredFlag}
            </if>
smartor/src/main/resources/mapper/smartor/PatHetaskRelevanceMapper.xml
@@ -24,7 +24,8 @@
        <result property="sendTime" column="send_time"/>
        <result property="operatePerson" column="operate_person"/>
        <result property="receiveTime" column="receive_time"/>
        <result property="visitTime" column="visit_time"/>
        <!-- åŽŸä¸º property="visitTime":任务模块化后实体字段叫 visittime(全小写,列仍是 visit_time)。 -->
        <result property="visittime" column="visit_time"/>
        <result property="guid" column="guid"/>
        <result property="orgid" column="orgid"/>
    </resultMap>
@@ -74,7 +75,7 @@
            <if test="icdName != null  and icdName != ''">and icd_name like concat('%', #{icdName}, '%')</if>
            <if test="sendStatus != null ">and send_status = #{sendStatus}</if>
            <if test="sendTime != null ">and send_time = #{sendTime}</if>
            <if test="visitTime != null ">and visit_time = #{visitTime}</if>
            <if test="visittime != null ">and visit_time = #{visittime}</if>
            <if test="receiveTime != null ">and receive_time = #{receiveTime}</if>
            <if test="operatePerson != null   and operatePerson != ''">and operate_person = #{operatePerson}</if>
    </select>
@@ -104,7 +105,7 @@
            <if test="sendStatus != null">send_status,</if>
            <if test="sendTime != null">send_time,</if>
            <if test="receiveTime != null">receive_time,</if>
            <if test="visitTime != null">visit_time,</if>
            <if test="visittime != null">visit_time,</if>
            <if test="operatePerson != null and operatePerson != ''">operate_person,</if>
            <if test="guid != null and guid != ''">guid,</if>
            <if test="orgid != null and orgid != ''">orgid,</if>
@@ -127,7 +128,7 @@
            <if test="sendStatus != null">#{sendStatus},</if>
            <if test="sendTime != null">#{sendTime},</if>
            <if test="receiveTime != null">#{receiveTime},</if>
            <if test="visitTime != null">#{visitTime},</if>
            <if test="visittime != null">#{visittime},</if>
            <if test="operatePerson != null and operatePerson != ''">#{operatePerson},</if>
            <if test="guid != null and guid != ''">#{guid},</if>
            <if test="orgid != null and orgid != ''">#{orgid},</if>
@@ -154,7 +155,7 @@
            <if test="sendStatus != null">send_status=#{sendStatus},</if>
            <if test="sendTime != null">send_time=#{sendTime},</if>
            <if test="receiveTime != null">receive_time=#{receiveTime},</if>
            <if test="visitTime != null">visit_time=#{visitTime},</if>
            <if test="visittime != null">visit_time=#{visittime},</if>
            <if test="operatePerson != null and operatePerson != ''">operate_person=#{operatePerson},</if>
            <if test="guid != null and guid != ''">guid=#{guid},</if>
            <if test="orgid != null and orgid != ''">orgid=#{orgid},</if>
smartor/src/main/resources/mapper/smartor/PatMedInspectionMapper.xml
@@ -131,7 +131,10 @@
            <if test="partId != null">and part_id = #{partId}</if>
            <if test="partName != null">and part_name = #{partName}</if>
            <if test="typeId != null">and type_id = #{typeId}</if>
            <if test="typeName != null">and type_name = #{type}</if>
            <!-- åŽŸä¸º `#{type}`:实体上只有 typeName(test é‡Œç”¨çš„也是 typeName),
                 `type` ä¸å­˜åœ¨ â€”— è¯¥åˆ†æ”¯ä¸€èµ°åˆ°å°±æŠ›
                 ReflectionException: no getter for 'type'。 -->
            <if test="typeName != null">and type_name = #{typeName}</if>
            <if test="filmFlag != null">and film_flag = #{filmFlag}</if>
            <if test="bedFlag != null">and bed_flag = #{bedFlag}</if>
            <if test="pushFlag != null">and push_flag = #{pushFlag}</if>
smartor/src/main/resources/mapper/smartor/ServiceAppointRecordMapper.xml
@@ -734,8 +734,11 @@
            <if test="registTimeRange != null">regist_time_range =
                #{registTimeRange},
            </if>
            <!-- åŽŸä¸º `#{schedDetailSn}`:实体属性叫 scheduleDetailSn(test é‡Œç”¨çš„也是它),
                 schedDetailSn ä¸å­˜åœ¨ â€”— ä¸€æ—¦èµ°åˆ°è¿™æ¡åˆ†æ”¯å°±æŠ›
                 ReflectionException: no getter for 'schedDetailSn'。 -->
            <if test="scheduleDetailSn != null">schedule_detail_sn =
                #{schedDetailSn},
                #{scheduleDetailSn},
            </if>
            <if test="treatDate != null">treat_date =
                #{treatDate},