陈昶聿
7 天以前 839e23b23b2bfb13282e9e5f621d32a1c55186ea
ruoyi-task/src/main/resources/mapper/task/ServiceSubtaskMapper.xml
@@ -23,10 +23,11 @@
        <result property="senduuid" column="senduuid"/>
        <result property="result" column="result"/>
        <result property="finishtime" column="finishtime"/>
        <result property="finishState" column="finish_state"/>
        <result property="recordid" column="recordid"/>
        <result property="exrecallcount" column="exrecallcount"/>
        <result property="pulltime" column="pulltime"/>
        <result property="deptcode" column="deptcode"/>
        <result property="leaveldeptcode" column="leaveldeptcode"/>
        <result property="labelstatus" column="labelstatus"/>
        <result property="delFlag" column="del_flag"/>
        <result property="updateBy" column="update_by"/>
@@ -53,6 +54,11 @@
        <result property="leaveicd10code" column="leaveicd10code"/>
        <result property="leavediagname" column="leavediagname"/>
        <result property="diagname" column="diagname"/>
        <result property="leaveldeptname" column="leaveldeptname"/>
        <!-- 2026-11:原 sendlimit* 四个空闲列复用为「在院维度」(hospitaldistrict* = 在院病区,deptcode/name = 在院科室) -->
        <result property="hospitaldistrictcode" column="hospitaldistrictcode"/>
        <result property="hospitaldistrictname" column="hospitaldistrictname"/>
        <result property="deptcode" column="deptcode"/>
        <result property="deptname" column="deptname"/>
        <result property="admindate" column="admindate"/>
        <result property="roomno" column="roomno"/>
@@ -72,6 +78,7 @@
        <result property="leavehospitaldistrictname" column="leavehospitaldistrictname"/>
        <result property="excep" column="excep"/>
        <result property="remark" column="remark"/>
        <result property="doctorOpinion" column="doctor_opinion"/>
        <result property="suggest" column="suggest"/>
        <result property="sendstate" column="sendstate"/>
        <result property="inhospid" column="inhospid"/>
@@ -80,6 +87,7 @@
        <result property="visitCount" column="visit_count"/>
        <result property="isVisitAgain" column="is_visit_again"/>
        <result property="upid" column="upid"/>
        <result property="visitType" column="visit_type"/>
        <result property="visitTime" column="visit_time"/>
        <result property="visitDeptName" column="visit_dept_name"/>
        <result property="visitDeptCode" column="visit_dept_code"/>
@@ -100,6 +108,7 @@
        <result property="loopCount" column="loop_count"/>
        <result property="isManuallyCalled" column="is_manually_called"/>
        <result property="campusid" column="campusid"/>
        <result property="diagType" column="diag_type"/>
    </resultMap>
    <resultMap type="com.ruoyi.task.domain.ServiceSubtaskCount" id="ServiceSubtaskResult2">
@@ -182,6 +191,7 @@
               long_send_time,
               openid,
               remark,
               doctor_opinion,
               service_type,
               libtemplateid,
               libtemplatename,
@@ -202,10 +212,11 @@
               senduuid,
               result,
               finishtime,
               finish_state,
               recordid,
               exrecallcount,
               pulltime,
               deptcode,
               leaveldeptcode,
               labelstatus,
               del_flag,
               update_by,
@@ -228,6 +239,10 @@
               sendname,
               roomno,
               admindate,
               leaveldeptname,
               hospitaldistrictcode,
               hospitaldistrictname,
               deptcode,
               deptname,
               stop_state,
               leavehospitaldistrictcode,
@@ -249,6 +264,58 @@
               task_situation,
               is_manually_called
        from service_subtask
    </sql>
    <!-- 超时完成、延期完成逻辑 完成时状态(finish_state)计算:仅当本次更新把 sendstate 置为 6(随访完成)时使用。
         基准截止时间 base = endtime + send_day(缺省1) + (Σ service_subtask_preachform.compensate_time + 1) 天,
         与 RyTask/RedisMqReceiver 的 isSubtaskDeadline 口径保持一致;
         delay = COALESCE(service_task.delay_finish_days, 0);
         finish_time = COALESCE(入参 finishtime, 库中 finishtime, NOW()):
           finish_time <= base                                    -> 1 正常完成
           delay > 0 且 finish_time <= base + delay               -> 2 延期完成
           其余(含 delay 为 0 或 NULL、或已超出延期窗口)          -> 3 超时完成
         finishtime 为空时用 NOW() 兜底,保证完成时一定写入状态;
         仅当 endtime 为空导致无法计算时才保持原值不覆盖。 -->
    <sql id="finishStateExpr">
        CASE
        WHEN service_subtask.endtime IS NULL THEN finish_state
        WHEN COALESCE(#{finishtime,jdbcType=TIMESTAMP}, service_subtask.finishtime, NOW())
        &lt;= DATE_ADD(
        DATE_ADD(
        service_subtask.endtime,
        INTERVAL COALESCE((SELECT st.send_day FROM service_task st
        WHERE st.taskid = service_subtask.taskid AND st.del_flag = '0' LIMIT 1), 1) DAY
        ),
        INTERVAL (COALESCE((SELECT SUM(CAST(COALESCE(NULLIF(TRIM(pf.compensate_time), ''), '0') AS SIGNED))
        FROM service_subtask_preachform pf
        WHERE pf.subid = service_subtask.id AND pf.del_flag = 0), 0) + 1) DAY
        )
        THEN 1
        WHEN COALESCE((SELECT st.delay_finish_days FROM service_task st
        WHERE st.taskid = service_subtask.taskid AND st.del_flag = '0' LIMIT 1), 0) &gt; 0
        AND COALESCE(#{finishtime,jdbcType=TIMESTAMP}, service_subtask.finishtime, NOW())
        &lt;= DATE_ADD(
        DATE_ADD(
        DATE_ADD(
        service_subtask.endtime,
        INTERVAL COALESCE((SELECT st.send_day FROM service_task st
        WHERE st.taskid = service_subtask.taskid AND st.del_flag = '0' LIMIT 1), 1) DAY
        ),
        INTERVAL (COALESCE((SELECT SUM(CAST(COALESCE(NULLIF(TRIM(pf.compensate_time), ''), '0') AS SIGNED))
        FROM service_subtask_preachform pf
        WHERE pf.subid = service_subtask.id AND pf.del_flag = 0), 0) + 1) DAY
        ),
        INTERVAL COALESCE((SELECT st.delay_finish_days FROM service_task st
        WHERE st.taskid = service_subtask.taskid AND st.del_flag = '0' LIMIT 1), 0) DAY
        )
        THEN 2
        ELSE 3
        END
    </sql>
    <!-- 当前执行方式:首次随访 current_preachform 为空时,取 preachform 的第一个执行方式兜底 -->
    <sql id="effectiveCurrentPreachformExpr">
        COALESCE(NULLIF(TRIM(current_preachform), ''), TRIM(SUBSTRING_INDEX(preachform, ',', 1)))
    </sql>
    <select id="selectServiceSubtaskBySendstate" parameterType="com.ruoyi.task.domain.entity.ServiceSubtaskEntity"
@@ -291,9 +358,142 @@
        <if test="continueTimeNext != null ">and continue_time_next = #{continueTimeNext}</if>
    </select>
    <select id="selectServiceSubtaskList" parameterType="com.ruoyi.task.domain.entity.ServiceSubtaskEntity"
            resultMap="ServiceSubtaskResult">
        <include refid="selectServiceSubtaskVo"/>
    <!-- ======================================================================
         科室 / 病区筛选(2026-11 修订版)
         两组条件【各自独立】,互不牵连(在院病区与离院病区不会同时存在):
           离院组:leavehospitaldistrictcodes → leavehospitaldistrictcode
                   leaveldeptcodes            → leaveldeptcode
           在院组:hospitaldistrictcodes      → hospitaldistrictcode(在院病区)
                   deptcodes                  → deptcode(在院科室)
         组内组合方式由 deptOrDistrict 决定:'1' = AND,'2' = OR。
         ('3'/'4' 是「离院病区 + 随访科室 visit_dept_code」那套,单独分支,不走这里)
         两组之间是 AND:只传一组时另一组自动不拼。
         空数组 [] 一律视为"没传",不会拼出 IN ()。
         ====================================================================== -->
    <sql id="filterLeaveDimAnd">
        /* f-v3 */
        <if test="leavehospitaldistrictcodes != null and leavehospitaldistrictcodes.size() > 0">
            AND leavehospitaldistrictcode IN
            <foreach collection="leavehospitaldistrictcodes" item="lvWardCodeItem" open="(" separator="," close=")">
                #{lvWardCodeItem}
            </foreach>
        </if>
        <if test="leaveldeptcodes != null and leaveldeptcodes.size() > 0">
            AND leaveldeptcode IN
            <foreach collection="leaveldeptcodes" item="lvDeptCodeItem" open="(" separator="," close=")">
                #{lvDeptCodeItem}
            </foreach>
        </if>
    </sql>
    <!-- 即将超时过滤:是否即将超时(soonTimeout=1) + 即将超时时间(soonTimeoutHours,单位小时)。
         截止时间口径与 RyTask/RedisMqReceiver 的 isSubtaskDeadline 保持一致:
         service_subtask.endtime + service_task.send_day(缺省1) + (Σ service_subtask_preachform.compensate_time + 1) 天;
         仅当存在有效补偿记录时才计算截止时间(无补偿记录不参与即将超时);
         仅统计“尚未超时、且在 N 小时内将要超时”的数据。 -->
    <sql id="soonTimeoutDeadlineExpr">
        (
        SELECT DATE_ADD(
        DATE_ADD(ss.endtime, INTERVAL COALESCE(st.send_day, 1) DAY),
        INTERVAL (COALESCE(pf.compensate_days, 0) + 1) DAY
        )
        FROM service_subtask ss
        LEFT JOIN service_task st ON st.taskid = ss.taskid AND st.del_flag = '0'
        INNER JOIN (
        SELECT subid,
        SUM(CAST(COALESCE(NULLIF(TRIM(compensate_time), ''), '0') AS SIGNED)) AS compensate_days
        FROM service_subtask_preachform
        WHERE del_flag = 0
        GROUP BY subid
        ) pf ON pf.subid = ss.id
        WHERE ss.id = service_subtask.id
        )
    </sql>
    <sql id="soonTimeoutFilter">
        <if test="soonTimeout != null and soonTimeout == 1">
            AND <include refid="soonTimeoutDeadlineExpr"/> &gt; NOW()
            AND <include refid="soonTimeoutDeadlineExpr"/> &lt;= DATE_ADD(NOW(), INTERVAL COALESCE(#{soonTimeoutHours}, 0) HOUR)
        </if>
    </sql>
    <sql id="filterLeaveDimOr">
        /* f-v3 */
        <if test="(leavehospitaldistrictcodes != null and leavehospitaldistrictcodes.size() > 0)
                  or (leaveldeptcodes != null and leaveldeptcodes.size() > 0)">
            AND (
            <if test="leavehospitaldistrictcodes != null and leavehospitaldistrictcodes.size() > 0">
                leavehospitaldistrictcode IN
                <foreach collection="leavehospitaldistrictcodes" item="lvWardCodeItem" open="(" separator="," close=")">
                    #{lvWardCodeItem}
                </foreach>
            </if>
            <if test="(leavehospitaldistrictcodes != null and leavehospitaldistrictcodes.size() > 0)
                      and (leaveldeptcodes != null and leaveldeptcodes.size() > 0)">
                OR
            </if>
            <if test="leaveldeptcodes != null and leaveldeptcodes.size() > 0">
                leaveldeptcode IN
                <foreach collection="leaveldeptcodes" item="lvDeptCodeItem" open="(" separator="," close=")">
                    #{lvDeptCodeItem}
                </foreach>
            </if>
            )
        </if>
    </sql>
    <sql id="filterInhospDimAnd">
        /* f-v3 */
        <if test="hospitaldistrictcodes != null and hospitaldistrictcodes.size() > 0">
            AND hospitaldistrictcode IN
            <foreach collection="hospitaldistrictcodes" item="inWardCodeItem" open="(" separator="," close=")">
                #{inWardCodeItem}
            </foreach>
        </if>
        <if test="deptcodes != null and deptcodes.size() > 0">
            AND deptcode IN
            <foreach collection="deptcodes" item="inDeptCodeItem" open="(" separator="," close=")">
                #{inDeptCodeItem}
            </foreach>
        </if>
    </sql>
    <sql id="filterInhospDimOr">
        /* f-v3 */
        <if test="(hospitaldistrictcodes != null and hospitaldistrictcodes.size() > 0)
                  or (deptcodes != null and deptcodes.size() > 0)">
            AND (
            <if test="hospitaldistrictcodes != null and hospitaldistrictcodes.size() > 0">
                hospitaldistrictcode IN
                <foreach collection="hospitaldistrictcodes" item="inWardCodeItem" open="(" separator="," close=")">
                    #{inWardCodeItem}
                </foreach>
            </if>
            <if test="(hospitaldistrictcodes != null and hospitaldistrictcodes.size() > 0)
                      and (deptcodes != null and deptcodes.size() > 0)">
                OR
            </if>
            <if test="deptcodes != null and deptcodes.size() > 0">
                deptcode IN
                <foreach collection="deptcodes" item="inDeptCodeItem" open="(" separator="," close=")">
                    #{inDeptCodeItem}
                </foreach>
            </if>
            )
        </if>
    </sql>
    <!-- ==================================================================
         service_subtask 列表查询的公共 where 片段
         ==================================================================
         patItem 走「两阶段分页」:
           ① selectServiceSubtaskIdsPage —— 只查 id,走覆盖索引,不为被 offset
              跳过的每一行回表读 100 列;
           ② selectServiceSubtaskList + subIdList —— 按 id 回表,只读一页的量。
         两条语句的 where 必须【逐字一致】,否则第②步会把第①步选出的行过滤掉,
         导致一页返回不足 pageSize 条。所以收成公共片段,杜绝两份口径漂移。
         ================================================================== -->
    <sql id="serviceSubtaskWhere">
        where del_flag = 0
        <if test="subId != null ">and id = #{subId}</if>
        <if test="continueFlag != null ">and continue_flag = #{continueFlag}</if>
@@ -367,63 +567,43 @@
            '%')
        </if>
        <if test="startOutHospTime != null">
            AND date_format(endtime,'%y%m%d') &gt;= date_format(#{startOutHospTime},'%y%m%d')
            AND endtime &gt;= DATE(#{startOutHospTime})
        </if>
        <if test="endOutHospTime != null">
            AND date_format(endtime,'%y%m%d') &lt;= date_format(#{endOutHospTime},'%y%m%d')
            AND endtime &lt; DATE_ADD(DATE(#{endOutHospTime}), INTERVAL 1 DAY)
        </if>
        <if test="startSendDateTime != null">
            AND date_format(visit_time,'%y%m%d') &gt;= date_format(#{startSendDateTime},'%y%m%d')
            AND visit_time &gt;= DATE(#{startSendDateTime})
        </if>
        <if test="endSendDateTime != null">
            AND date_format(visit_time,'%y%m%d') &lt;= date_format(#{endSendDateTime},'%y%m%d')
            AND visit_time &lt; DATE_ADD(DATE(#{endSendDateTime}), INTERVAL 1 DAY)
        </if>
        <if test="finishtimeStart != null">
            AND date_format(finishtime,'%y%m%d') &gt;= date_format(#{finishtimeStart},'%y%m%d')
            AND finishtime &gt;= DATE(#{finishtimeStart})
        </if>
        <if test="finishtimeEnd != null">
            AND date_format(finishtime,'%y%m%d') &lt;= date_format(#{finishtimeEnd},'%y%m%d')
            AND finishtime &lt; DATE_ADD(DATE(#{finishtimeEnd}), INTERVAL 1 DAY)
        </if>
        <if test="longSendTime != null">
            AND date_format(long_send_time,'%y%m%d') &lt;= date_format(#{longSendTime},'%y%m%d')
            AND long_send_time &lt; DATE_ADD(DATE(#{longSendTime}), INTERVAL 1 DAY)
        </if>
        <if test="deptOrDistrict==1">
            <if test="leavehospitaldistrictcodes != null and leavehospitaldistrictcodes.size() > 0">
                AND leavehospitaldistrictcode IN
                <foreach collection="leavehospitaldistrictcodes" item="leavehospitaldistrictcode" open="("
                         separator=","
                         close=")">
                    #{leavehospitaldistrictcode}
                </foreach>
            </if>
            <if test=" leaveldeptcodes != null and leaveldeptcodes.size()>0">
                AND deptcode IN
                <foreach collection="leaveldeptcodes" item="leaveldeptcode" open="(" separator=","
                         close=")">
                    #{leaveldeptcode}
                </foreach>
            </if>
        <!--
            科室/病区筛选(2026-11 修订):
              离院组(leaveldeptcodes / leavehospitaldistrictcodes)保持原逻辑;
              在院组(deptcodes / hospitaldistrictcodes → deptcode / hospitaldistrictcode)是新增的一组;
              组内按 deptOrDistrict 取 AND('1') / OR('2'),两组之间 AND(只传一组时另一组不拼)。
        -->
        <if test='deptOrDistrict == "1"'>
            <include refid="filterLeaveDimAnd"/>
            <include refid="filterInhospDimAnd"/>
        </if>
        <if test="deptOrDistrict==2">
            AND (1=1
            <if test="leavehospitaldistrictcodes != null">
                AND leavehospitaldistrictcode IN
                <foreach collection="leavehospitaldistrictcodes" item="leavehospitaldistrictcode" open="("
                         separator=","
                         close=")">
                    #{leavehospitaldistrictcode}
                </foreach>
            </if>
            <if test=" leaveldeptcodes != null">
                OR deptcode IN
                <foreach collection="leaveldeptcodes" item="leaveldeptcode" open="(" separator=","
                         close=")">
                    #{leaveldeptcode}
                </foreach>
            </if>
            )
        <if test='deptOrDistrict == "2"'>
            <include refid="filterLeaveDimOr"/>
            <include refid="filterInhospDimOr"/>
        </if>
        <if test="deptOrDistrict==3">
        <!-- ⚠️ deptOrDistrict 是 String:OGNL 里 '1'(单引号单字符)是 char,String != char 恒 false!
             必须写成双引号字符串(XML 属性用单引号包住),或直接和数字比(OGNL 会做 String→Number 转换)。 -->
        <if test='deptOrDistrict == "3"'>
            AND (1=1
            <if test="leavehospitaldistrictcodes != null and leavehospitaldistrictcodes.size() > 0">
                AND leavehospitaldistrictcode IN
@@ -442,7 +622,7 @@
            </if>
            )
        </if>
        <if test="deptOrDistrict==4">
        <if test='deptOrDistrict == "4"'>
            AND (1=1
            <if test="leavehospitaldistrictcodes != null and leavehospitaldistrictcodes.size() > 0">
                AND leavehospitaldistrictcode IN
@@ -462,7 +642,8 @@
            )
        </if>
        <if test="visitTime != null">
            AND date_format(visit_time,'%y%m%d') = date_format(#{visitTime},'%y%m%d')
            AND visit_time &gt;= DATE(#{visitTime})
            AND visit_time &lt; DATE_ADD(DATE(#{visitTime}), INTERVAL 1 DAY)
        </if>
        <if test="senddate != null ">and senddate = #{senddate}</if>
        <if test="senduuid != null  and senduuid != ''">and senduuid = #{senduuid}</if>
@@ -471,8 +652,8 @@
        <if test="recordid != null ">and recordid = #{recordid}</if>
        <if test="exrecallcount != null ">and exrecallcount = #{exrecallcount}</if>
        <if test="pulltime != null ">and pulltime = #{pulltime}</if>
        <if test="deptcode != null  and deptcode != ''">and deptcode = #{deptcode}</if>
        <if test="deptname != null  and deptname != ''">and deptname = #{deptname}</if>
        <if test="leaveldeptcode != null  and leaveldeptcode != ''">and leaveldeptcode = #{leaveldeptcode}</if>
        <if test="leaveldeptname != null  and leaveldeptname != ''">and leaveldeptname = #{leaveldeptname}</if>
        <if test="labelstatus != null ">and labelstatus = #{labelstatus}</if>
        <if test="isupload != null ">and isupload = #{isupload}</if>
        <if test="uploadTime != null ">and upload_time = #{uploadTime}</if>
@@ -480,7 +661,10 @@
        <if test="campusid != null  and campusid != ''">and campusid = #{campusid}</if>
        <if test="pid != null ">and pid = #{pid}</if>
        <if test="preachform != null ">and preachform = #{preachform}</if>
        <if test="currentPreachform != null ">and current_preachform = #{currentPreachform}</if>
        <if test="currentPreachform != null and currentPreachform != '' ">and current_preachform = #{currentPreachform}</if>
        <if test="currentPreachforms != null and currentPreachforms != ''">
            and FIND_IN_SET(<include refid="effectiveCurrentPreachformExpr"/>, #{currentPreachforms})
        </if>
        <if test="guid != null  and guid != ''">and guid = #{guid}</if>
        <if test="textParam != null  and textParam != ''">and text_param = #{textParam}</if>
        <if test="bedNo != null">and bed_no = #{bedNo}</if>
@@ -510,6 +694,7 @@
            AND visit_count > 1
        </if>
        <if test="remark != null">and remark = #{remark}</if>
        <if test="doctorOpinion != null and doctorOpinion != ''">and doctor_opinion like concat('%', #{doctorOpinion}, '%')</if>
        <if test="suggest != null">and suggest = #{suggest}</if>
        <if test="createBy != null">and create_by = #{createBy}</if>
        <if test="taskGuid != null">and task_guid = #{taskGuid}</if>
@@ -524,6 +709,12 @@
                #{visitDeptCode}
            </foreach>
        </if>
        <include refid="soonTimeoutFilter"/>
    </sql>
    <!-- 排序片段:保持原语义,只在调用方显式传 sort 时才拼 order by
         (导出等不分页的调用方行为完全不变) -->
    <sql id="serviceSubtaskOrderBy">
        <if test="sort != null  and sort==0">order by endtime asc</if>
        <if test="sort != null  and sort==1">order by endtime desc</if>
        <if test="sort != null  and sort==2">order by visit_time asc</if>
@@ -533,7 +724,37 @@
        <if test="sort != null  and sort==7">order by visit_time asc</if>
        <if test="sort != null  and sort==8">order by visit_time desc</if>
        <if test="sort != null  and sort==9">order by sendname asc</if>
    </sql>
    <!-- 分页场景专用排序:sort 为空时兜底 order by id desc。
         原来 sort 为空时整条 SQL 没有 order by,limit/offset 取到的是「不确定顺序」,
         翻页会重复或漏行;这里给一个稳定顺序(想改成 id asc 只改这一行)。 -->
    <sql id="serviceSubtaskOrderByPaged">
        <include refid="serviceSubtaskOrderBy"/>
        <if test="sort == null">order by id desc</if>
    </sql>
    <select id="selectServiceSubtaskList" parameterType="com.ruoyi.task.domain.entity.ServiceSubtaskEntity"
            resultMap="ServiceSubtaskResult">
        <include refid="selectServiceSubtaskVo"/>
        <include refid="serviceSubtaskWhere"/>
        <include refid="serviceSubtaskOrderBy"/>
        <if test="pageSize != null  and pageNum != null">limit ${pageSize} OFFSET ${pageNum}</if>
    </select>
    <!--
        两阶段分页第 1 步:只取 id。
        select 列表只有 id,where/order 涉及的列都在 idx_ss_org_cam_del_* 上,
        因此能走「覆盖索引 + 提前截断」;被 offset 跳过的那几千行不再回表读 100 列。
        第 2 步由调用方用 subIdList 回表取整行(见 ServiceSubtaskServiceImpl#selectServiceSubtaskPage)。
    -->
    <select id="selectServiceSubtaskIdsPage" parameterType="com.ruoyi.task.domain.entity.ServiceSubtaskEntity"
            resultType="java.lang.Long">
        select id
        from service_subtask
        <include refid="serviceSubtaskWhere"/>
        <include refid="serviceSubtaskOrderByPaged"/>
        limit ${pageSize} OFFSET ${pageNum}
    </select>
            <select id="getCompensateServiceSubtaskList" parameterType="com.ruoyi.task.domain.entity.ServiceSubtaskEntity"
@@ -589,7 +810,7 @@
        <if test="visitDeptName != null">and visit_dept_name = #{visitDeptName}</if>
        <if test="managementDoctorCode != null">and management_doctor_code = #{managementDoctorCode}</if>
        <if test="managementDoctor != null">and management_doctor = #{managementDoctor}</if>
        <if test="currentPreachform != null">and current_preachform = #{currentPreachform}</if>
        <if test="currentPreachform != null and currentPreachform != '' ">and current_preachform = #{currentPreachform}</if>
        <if test="isabnormal != null">and isabnormal = #{isabnormal}</if>
        <if test="continueFlag != null ">and continue_flag = #{continueFlag}</if>
        <if test="continueTimeNow != null ">and continue_time_now = #{continueTimeNow,jdbcType=TIMESTAMP}</if>
@@ -615,8 +836,8 @@
        <if test="recordid != null ">and recordid = #{recordid}</if>
        <if test="exrecallcount != null ">and exrecallcount = #{exrecallcount}</if>
        <if test="pulltime != null ">and pulltime = #{pulltime}</if>
        <if test="deptcode != null  and deptcode != ''">and deptcode = #{deptcode}</if>
        <if test="deptname != null  and deptname != ''">and deptname = #{deptname}</if>
        <if test="leaveldeptcode != null  and leaveldeptcode != ''">and leaveldeptcode = #{leaveldeptcode}</if>
        <if test="leaveldeptname != null  and leaveldeptname != ''">and leaveldeptname = #{leaveldeptname}</if>
        <if test="labelstatus != null ">and labelstatus = #{labelstatus}</if>
        <if test="isupload != null ">and isupload = #{isupload}</if>
        <if test="uploadTime != null ">and upload_time = #{uploadTime}</if>
@@ -648,7 +869,7 @@
        <if test="visitTime != null ">and visit_time = #{visitTime}</if>
        <if test="visitDeptCode != null">and visit_dept_code = #{visitDeptCode}</if>
        <if test="visitDeptName != null">and visit_dept_name = #{visitDeptName}</if>
        <if test="currentPreachform != null">and current_preachform = #{currentPreachform}</if>
        <if test="currentPreachform != null and currentPreachform != '' ">and current_preachform = #{currentPreachform}</if>
        <if test="managementDoctorCode != null">and management_doctor_code = #{managementDoctorCode}</if>
        <if test="isabnormal != null">and isabnormal = #{isabnormal}</if>
        <if test="managementDoctor != null">and management_doctor like concat('%',#{managementDoctor}, '%')</if>
@@ -656,6 +877,7 @@
        <if test="continueTimeNow != null ">and continue_time_now = #{continueTimeNow,jdbcType=TIMESTAMP}</if>
        <if test="continueCount != null ">and continue_count = #{continueCount}</if>
        <if test="continueTimeNext != null ">and continue_time_next = #{continueTimeNext}</if>
        <include refid="soonTimeoutFilter"/>
    </select>
    <select id="selectSendstateByCondition" parameterType="com.ruoyi.task.domain.ServiceSubtask" resultType="Integer">
@@ -691,8 +913,8 @@
        <if test="campusid != null and campusid != ''">and campusid = #{campusid}</if>
        <if test="serviceType != null and serviceType != ''">and service_type = #{serviceType}</if>
        <if test="serviceTypes != null and serviceTypes != ''">and service_type IN (#{serviceTypes})</if>
        <if test="deptcode != null  and deptcode != ''">and deptcode = #{deptcode}</if>
        <if test="deptname != null  and deptname != ''">and deptname = #{deptname}</if>
        <if test="leaveldeptcode != null  and leaveldeptcode != ''">and leaveldeptcode = #{leaveldeptcode}</if>
        <if test="leaveldeptname != null  and leaveldeptname != ''">and leaveldeptname = #{leaveldeptname}</if>
        <if test="isabnormal != null">and isabnormal = #{isabnormal}</if>
        <if test="leavehospitaldistrictcode != null  and leavehospitaldistrictcode != ''">and
            leavehospitaldistrictcode = #{leavehospitaldistrictcode}
@@ -703,10 +925,10 @@
        <if test="managementDoctorCode != null">and management_doctor_code = #{managementDoctorCode}</if>
        <if test="managementDoctor != null">and management_doctor like concat('%',#{managementDoctor}, '%')</if>
        <if test="startOutHospTime != null">
            AND date_format(endtime,'%y%m%d') &gt;= date_format(#{startOutHospTime},'%y%m%d')
            AND endtime &gt;= DATE(#{startOutHospTime})
        </if>
        <if test="endOutHospTime != null">
            AND date_format(endtime,'%y%m%d') &lt;= date_format(#{endOutHospTime},'%y%m%d')
            AND endtime &lt; DATE_ADD(DATE(#{endOutHospTime}), INTERVAL 1 DAY)
        </if>
        ) a
    </select>
@@ -718,26 +940,59 @@
        </if>
        SUM(CASE WHEN ss.finishtime IS NOT NULL AND DATE(ss.visit_time) &lt;= DATE(ss.finishtime) THEN 1 ELSE 0 END) /
        COUNT(*) AS finishRate,
        <if test="rateDay != null">
            SUM(CASE WHEN ss.finishtime IS NOT NULL
            AND DATEDIFF(ss.finishtime, ss.endtime) &lt;= #{rateDay} + COALESCE(cp.compensate_days, 0)
            THEN 1 ELSE 0 END) / COUNT(*) AS rate
        </if>
        <if test="rateDay == null">
            NULL AS rate
        </if>
        <!-- 及时率基准:
             1) 非任务组(appltype != 5):endtime + send_day(缺省1) + Σ补偿天数 + 1 天;
             2) 任务组(appltype = 5):按 schedule_benchmark 解析基准时间
                (1-入院时间 2-出院时间 3-就诊时间 4-入组时间 0/其它-自定义时间)
                + day_offset + send_day(缺省1) + Σ补偿天数 + 1 天;
             完成时间 finishtime <= 基准时间计为及时,分母仍为应随访数。
             补偿天数只取 service_subtask_preachform.compensate_time 的汇总值一次,
             避免与补偿明细相加造成重复补偿或一对多放大分母。 -->
        SUM(CASE
            WHEN ss.finishtime IS NOT NULL
             AND COALESCE(st.appltype, '') != '5'
             AND ss.endtime IS NOT NULL
             AND ss.finishtime &lt;= DATE_ADD(
                   DATE_ADD(ss.endtime, INTERVAL COALESCE(st.send_day, 1) DAY),
                   INTERVAL (COALESCE(cp.compensate_days, 0) + 1) DAY)
            THEN 1
            WHEN ss.finishtime IS NOT NULL
             AND st.appltype = '5'
             AND std.day_offset IS NOT NULL
             AND ss.finishtime &lt;= DATE_ADD(
                   DATE_ADD(
                     DATE_ADD(
                       CASE sts.schedule_benchmark
                         WHEN 1 THEN ss.starttime
                         WHEN 2 THEN ss.endtime
                         WHEN 3 THEN ss.admindate
                         WHEN 4 THEN ss.create_time
                         ELSE sts.schedule_benchmark_custom_time
                       END, INTERVAL std.day_offset DAY),
                     INTERVAL COALESCE(st.send_day, 1) DAY),
                   INTERVAL (COALESCE(cp.compensate_days, 0) + 1) DAY)
            THEN 1
            ELSE 0
        END) / COUNT(*) AS rate
        FROM service_subtask ss
        <if test="rateDay != null">
            <!-- 补偿时间:先按子任务聚合成一行再关联,避免一对多放大 COUNT(*) 分母。
                 compensate_time 是 varchar 且可能存成空串,需先置空再转数字 -->
            LEFT JOIN (
            SELECT subid,
            SUM(CAST(COALESCE(NULLIF(TRIM(compensate_time), ''), '0') AS SIGNED)) AS compensate_days
            FROM service_subtask_preachform
            WHERE del_flag = 0
            GROUP BY subid
            ) cp ON cp.subid = ss.id
        </if>
        <!-- 这里按 taskid / 历史 schedule 快照关联,不按 del_flag 过滤:
             任务或计划被编辑后旧 schedule/detail 会置为 del_flag=1,
             但历史 subtask 仍引用旧 ID,过滤会丢失历史及时率基准。 -->
        LEFT JOIN service_task st
               ON st.taskid = ss.taskid
        LEFT JOIN service_task_schedule sts
               ON sts.id = ss.schedule_id
        LEFT JOIN service_task_schedule_detail std
               ON std.id = ss.schedule_detail_id
        <!-- 补偿时间:先按子任务聚合成一行再关联,避免一对多放大 COUNT(*) 分母。
             compensate_time 是 varchar 且可能存成空串,需先置空再转数字 -->
        LEFT JOIN (
        SELECT subid,
        SUM(CAST(COALESCE(NULLIF(TRIM(compensate_time), ''), '0') AS SIGNED)) AS compensate_days
        FROM service_subtask_preachform
        WHERE del_flag = 0
        GROUP BY subid
        ) cp ON cp.subid = ss.id
        WHERE ss.del_flag = 0
        AND ss.visit_time IS NOT NULL
        AND ss.sendstate != 4
@@ -750,8 +1005,8 @@
        </if>
        <if test="orgid != null and orgid != ''">AND ss.orgid = #{orgid}</if>
        <if test="campusid != null and campusid != ''">and ss.campusid = #{campusid}</if>
        <if test="deptcode != null  and deptcode != ''">and ss.deptcode = #{deptcode}</if>
        <if test="deptname != null  and deptname != ''">and ss.deptname = #{deptname}</if>
        <if test="leaveldeptcode != null  and leaveldeptcode != ''">and ss.leaveldeptcode = #{leaveldeptcode}</if>
        <if test="leaveldeptname != null  and leaveldeptname != ''">and ss.leaveldeptname = #{leaveldeptname}</if>
        <if test="isabnormal != null">and ss.isabnormal = #{isabnormal}</if>
        <if test="leavehospitaldistrictcode != null  and leavehospitaldistrictcode != ''">and
            ss.leavehospitaldistrictcode = #{leavehospitaldistrictcode}
@@ -781,7 +1036,7 @@
            AND date_format(ss.endtime,'%y%m%d') &lt;= date_format(#{endOutHospTime},'%y%m%d')
        </if>
        <if test="groupKey != null and groupKey != ''">
            AND ss.${groupKey} IN
            AND IFNULL(ss.${groupKey}, 'Unknown') IN
            <foreach collection="groupKeyList" item="key" open="(" separator=","
                     close=")">
                #{key}
@@ -812,8 +1067,8 @@
        <if test="serviceType != null and serviceType != ''">and service_type = #{serviceType}</if>
        <if test="serviceTypes != null and serviceTypes != ''">and service_type IN (#{serviceTypes})</if>
        <if test="isabnormal != null ">and isabnormal = #{isabnormal}</if>
        <if test="deptcode != null  and deptcode != ''">and deptcode = #{deptcode}</if>
        <if test="deptname != null  and deptname != ''">and deptname = #{deptname}</if>
        <if test="leaveldeptcode != null  and leaveldeptcode != ''">and leaveldeptcode = #{leaveldeptcode}</if>
        <if test="leaveldeptname != null  and leaveldeptname != ''">and leaveldeptname = #{leaveldeptname}</if>
        <if test="leavehospitaldistrictcode != null  and leavehospitaldistrictcode != ''">and
            leavehospitaldistrictcode = #{leavehospitaldistrictcode}
        </if>
@@ -822,6 +1077,12 @@
        </if>
        <if test="managementDoctorCode != null">and management_doctor_code = #{managementDoctorCode}</if>
        <if test="managementDoctor != null">and management_doctor like concat('%',#{managementDoctor}, '%')</if>
        <if test="startOutHospTime != null">
            AND date_format(endtime,'%y%m%d') &gt;= date_format(#{startOutHospTime},'%y%m%d')
        </if>
        <if test="endOutHospTime != null">
            AND date_format(endtime,'%y%m%d') &lt;= date_format(#{endOutHospTime},'%y%m%d')
        </if>
        ) a where a.finished =1)
    </select>
@@ -846,10 +1107,10 @@
        <if test="continueCount != null ">and continue_count = #{continueCount}</if>
        <if test="continueTimeNext != null ">and continue_time_next = #{continueTimeNext}</if>
        <if test="startOutHospTime != null">
            AND date_format(endtime,'%y%m%d') &gt;= date_format(#{startOutHospTime},'%y%m%d')
            AND endtime &gt;= DATE(#{startOutHospTime})
        </if>
        <if test="endOutHospTime != null">
            AND date_format(endtime,'%y%m%d') &lt;= date_format(#{endOutHospTime},'%y%m%d')
            AND endtime &lt; DATE_ADD(DATE(#{endOutHospTime}), INTERVAL 1 DAY)
        </if>
        GROUP BY sendname,sendstate,starttime,endtime,nurse_name,phone,sex,task_name,leavediagname,visit_time
        ORDER BY sendname,visit_time
@@ -875,10 +1136,16 @@
            <if test="senduuid != null">senduuid,</if>
            <if test="result != null">result,</if>
            <if test="finishtime != null">finishtime,</if>
            <if test="finishState != null">finish_state,</if>
            <if test="recordid != null">recordid,</if>
            <if test="exrecallcount != null">exrecallcount,</if>
            <if test="pulltime != null">pulltime,</if>
            <if test="deptcode != null">deptcode,</if>
            <if test="leaveldeptcode != null">leaveldeptcode,</if>
            <!-- 2026-11:原 sendlimit* 复用列(在院维度:在院病区/在院科室),仅出院随访会填值 -->
            <if test="hospitaldistrictcode != null  and hospitaldistrictcode != ''">hospitaldistrictcode,</if>
            <if test="hospitaldistrictname != null  and hospitaldistrictname != ''">hospitaldistrictname,</if>
            <if test="deptcode != null  and deptcode != ''">deptcode,</if>
            <if test="deptname != null  and deptname != ''">deptname,</if>
            <if test="labelstatus != null">labelstatus,</if>
            <if test="updateBy != null">update_by,</if>
            <if test="updateTime != null">update_time,</if>
@@ -903,7 +1170,7 @@
            <if test="wechat != null  and wechat != ''">wechat,</if>
            <if test="roomno != null  and roomno != ''">roomno,</if>
            <if test="admindate != null">admindate,</if>
            <if test="deptname != null  and deptname != ''">deptname,</if>
            <if test="leaveldeptname != null  and leaveldeptname != ''">leaveldeptname,</if>
            <if test="leaveicd10code != null  and leaveicd10code != ''">leaveicd10code,</if>
            <if test="leavediagname != null  and leavediagname != ''">leavediagname,</if>
            <!-- diagname:入院诊断名称。原 insert 漏了这一列,而出院链路的
@@ -932,6 +1199,7 @@
            <if test="nurseName != null ">nurse_name,</if>
            <if test="nurseId != null ">nurse_id,</if>
            <if test="remark != null ">remark,</if>
            <if test="doctorOpinion != null ">doctor_opinion,</if>
            <if test="suggest != null ">suggest,</if>
            <if test="sendstate != null ">sendstate,</if>
            <if test="inhospid != null ">inhospid,</if>
@@ -946,7 +1214,7 @@
            <if test="visitDeptName != null">visit_dept_name,</if>
            <if test="managementDoctorCode != null">management_doctor_code,</if>
            <if test="managementDoctor != null">management_doctor,</if>
            <if test="currentPreachform != null">current_preachform,</if>
            <if test="currentPreachform != null and currentPreachform != '' ">current_preachform,</if>
            <if test="taskSituation != null">task_situation,</if>
            <if test="isabnormal != null">isabnormal,</if>
            <if test="continueFlag != null ">continue_flag,</if>
@@ -977,10 +1245,16 @@
            <if test="senduuid != null">#{senduuid},</if>
            <if test="result != null">#{result},</if>
            <if test="finishtime != null">#{finishtime},</if>
            <if test="finishState != null">#{finishState},</if>
            <if test="recordid != null">#{recordid},</if>
            <if test="exrecallcount != null">#{exrecallcount},</if>
            <if test="pulltime != null">#{pulltime},</if>
            <if test="deptcode != null">#{deptcode},</if>
            <if test="leaveldeptcode != null">#{leaveldeptcode},</if>
            <!-- 2026-11:原 sendlimit* 复用列(入院/在院维度),仅出院随访会填值 -->
            <if test="hospitaldistrictcode != null  and hospitaldistrictcode != ''">#{hospitaldistrictcode},</if>
            <if test="hospitaldistrictname != null  and hospitaldistrictname != ''">#{hospitaldistrictname},</if>
            <if test="deptcode != null  and deptcode != ''">#{deptcode},</if>
            <if test="deptname != null  and deptname != ''">#{deptname},</if>
            <if test="labelstatus != null">#{labelstatus},</if>
            <if test="updateBy != null">#{updateBy},</if>
            <if test="updateTime != null">#{updateTime},</if>
@@ -1005,7 +1279,7 @@
            <if test="wechat != null  and wechat != ''">#{wechat},</if>
            <if test="roomno != null  and roomno != ''">#{roomno},</if>
            <if test="admindate != null ">#{admindate},</if>
            <if test="deptname != null  and deptname != ''">#{deptname},</if>
            <if test="leaveldeptname != null  and leaveldeptname != ''">#{leaveldeptname},</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>
@@ -1031,6 +1305,7 @@
            <if test="nurseName != null ">#{nurseName},</if>
            <if test="nurseId != null ">#{nurseId},</if>
            <if test="remark != null ">#{remark},</if>
            <if test="doctorOpinion != null ">#{doctorOpinion},</if>
            <if test="suggest != null ">#{suggest},</if>
            <if test="sendstate != null ">#{sendstate},</if>
            <if test="inhospid != null ">#{inhospid},</if>
@@ -1045,7 +1320,7 @@
            <if test="visitDeptName != null">#{visitDeptName},</if>
            <if test="managementDoctorCode != null">#{managementDoctorCode},</if>
            <if test="managementDoctor != null">#{managementDoctor},</if>
            <if test="currentPreachform != null">#{currentPreachform},</if>
            <if test="currentPreachform != null and currentPreachform != '' ">#{currentPreachform},</if>
            <if test="taskSituation != null">#{taskSituation},</if>
            <if test="isabnormal != null">#{isabnormal},</if>
            <if test="continueFlag != null ">#{continueFlag},</if>
@@ -1061,6 +1336,15 @@
            <if test="isManuallyCalled != null ">#{isManuallyCalled},</if>
        </trim>
    </insert>
    <!-- 二次随访幂等:仅当 is_visit_again 不是目标值时更新,返回受影响行数 -->
    <update id="updateIsVisitAgainById">
        update service_subtask
        set is_visit_again = #{isVisitAgain},
            update_time = now()
        where id = #{id}
          and (is_visit_again is null or is_visit_again &lt;&gt; #{isVisitAgain})
    </update>
    <update id="updateServiceSubtask" parameterType="com.ruoyi.task.domain.ServiceSubtask">
        update service_subtask
@@ -1084,7 +1368,7 @@
            <if test="recordid != null">recordid = #{recordid},</if>
            <if test="exrecallcount != null">exrecallcount = #{exrecallcount},</if>
            <if test="pulltime != null">pulltime = #{pulltime},</if>
            <if test="deptcode != null">deptcode = #{deptcode},</if>
            <if test="leaveldeptcode != null">leaveldeptcode = #{leaveldeptcode},</if>
            <if test="labelstatus != null">labelstatus = #{labelstatus},</if>
            <if test="updateBy != null">update_by = #{updateBy},</if>
            <if test="updateTime != null">update_time = #{updateTime},</if>
@@ -1109,7 +1393,7 @@
            <if test="wechat != null  and wechat != ''">wechat = #{wechat},</if>
            <if test="roomno != null  and roomno != ''">roomno = #{roomno},</if>
            <if test="admindate != null ">admindate = #{admindate},</if>
            <if test="deptname != null  and deptname != ''">deptname = #{deptname},</if>
            <if test="leaveldeptname != null  and leaveldeptname != ''">leaveldeptname = #{leaveldeptname},</if>
            <if test="leaveicd10code != null  and leaveicd10code != ''">leaveicd10code = #{leaveicd10code},</if>
            <if test="leavediagname != null  and leavediagname != ''">leavediagname = #{leavediagname},</if>
            <if test="libtemplateid != null ">libtemplateid = #{libtemplateid},</if>
@@ -1118,6 +1402,7 @@
            <if test="openid != null ">openid = #{openid},</if>
            <if test="longSendTime != null ">long_send_time = #{longSendTime},</if>
            <if test="sendstate != null ">sendstate = #{sendstate},</if>
            <if test="sendstate != null and sendstate == 6">finish_state = <include refid="finishStateExpr"/>,</if>
            <if test="serviceType != null ">service_type = #{serviceType},</if>
            <if test="score != null ">score = #{score},</if>
            <if test="submit != null ">submit = #{submit},</if>
@@ -1135,6 +1420,7 @@
            <if test="nurseName != null ">nurse_name=#{nurseName},</if>
            <if test="nurseId != null ">nurse_id=#{nurseId},</if>
            <if test="remark != null ">remark=#{remark},</if>
            <if test="doctorOpinion != null ">doctor_opinion=#{doctorOpinion},</if>
            <if test="suggest != null ">suggest=#{suggest},</if>
            <if test="inhospid != null ">inhospid=#{inhospid},</if>
            <if test="patfrom != null ">patfrom=#{patfrom},</if>
@@ -1148,7 +1434,7 @@
            <if test="visitDeptName != null">visit_dept_name = #{visitDeptName},</if>
            <if test="managementDoctorCode != null">management_doctor_code = #{managementDoctorCode},</if>
            <if test="managementDoctor != null">management_doctor=#{managementDoctor},</if>
            <if test="currentPreachform != null">current_preachform=#{currentPreachform},</if>
            <if test="currentPreachform != null and currentPreachform != '' ">current_preachform=#{currentPreachform},</if>
            <if test="taskSituation != null">task_situation=#{taskSituation},</if>
            <if test="isabnormal != null">isabnormal=#{isabnormal},</if>
            <if test="continueFlag != null ">continue_flag = #{continueFlag},</if>
@@ -1164,6 +1450,25 @@
            <if test="isManuallyCalled != null ">is_manually_called = #{isManuallyCalled},</if>
        </trim>
        where id = #{id}
    </update>
    <!-- 微信公众号已领取未填写按默认选项完成:只更新当前子任务。
         条件 sendstate=1 + 未提交,保证只处理当前被领取且未填写的子任务,不波及其他任务。 -->
    <update id="updateWechatDefaultComplete" parameterType="com.ruoyi.task.domain.ServiceSubtask">
        update service_subtask
        set sendstate = 6,
            finishtime = #{finishtime},
            result = #{result},
            remark = case
                         when remark is null or remark = '' then #{remark}
                         else concat(remark, ',', #{remark})
                     end,
            update_time = now(),
            finish_state = <include refid="finishStateExpr"/>
        where id = #{id}
          and orgid = #{orgid}
          and sendstate = 1
          and (submit is null or submit != 1)
    </update>
    <update id="updateServiceSubtaskByPatId" parameterType="com.ruoyi.task.domain.ServiceSubtask">
@@ -1188,7 +1493,7 @@
            <if test="recordid != null">recordid = #{recordid},</if>
            <if test="exrecallcount != null">exrecallcount = #{exrecallcount},</if>
            <if test="pulltime != null">pulltime = #{pulltime},</if>
            <if test="deptcode != null">deptcode = #{deptcode},</if>
            <if test="leaveldeptcode != null">leaveldeptcode = #{leaveldeptcode},</if>
            <if test="labelstatus != null">labelstatus = #{labelstatus},</if>
            <if test="updateBy != null">update_by = #{updateBy},</if>
            <if test="updateTime != null">update_time = #{updateTime},</if>
@@ -1213,7 +1518,7 @@
            <if test="wechat != null  and wechat != ''">wechat = #{wechat},</if>
            <if test="roomno != null  and roomno != ''">roomno = #{roomno},</if>
            <if test="admindate != null">admindate = #{admindate},</if>
            <if test="deptname != null  and deptname != ''">deptname = #{deptname},</if>
            <if test="leaveldeptname != null  and leaveldeptname != ''">leaveldeptname = #{leaveldeptname},</if>
            <if test="leaveicd10code != null  and leaveicd10code != ''">leaveicd10code = #{leaveicd10code},</if>
            <if test="leavediagname != null  and leavediagname != ''">leavediagname = #{leavediagname},</if>
            <if test="libtemplateid != null ">libtemplateid = #{libtemplateid},</if>
@@ -1222,6 +1527,7 @@
            <if test="openid != null ">openid = #{openid},</if>
            <if test="longSendTime != null ">long_send_time = #{longSendTime},</if>
            <if test="sendstate != null ">sendstate = #{sendstate},</if>
            <if test="sendstate != null and sendstate == 6">finish_state = <include refid="finishStateExpr"/>,</if>
            <if test="serviceType != null ">service_type = #{serviceType},</if>
            <if test="score != null ">score = #{score},</if>
            <if test="visitCount != null ">visit_count = #{visitCount},</if>
@@ -1240,6 +1546,7 @@
            <if test="nurseName != null ">nurse_name=#{nurseName},</if>
            <if test="nurseId != null ">nurse_id=#{nurseId},</if>
            <if test="remark != null ">remark=#{remark},</if>
            <if test="doctorOpinion != null ">doctor_opinion=#{doctorOpinion},</if>
            <if test="suggest != null ">suggest=#{suggest},</if>
            <if test="inhospid != null ">inhospid=#{inhospid},</if>
            <if test="patfrom != null ">patfrom=#{patfrom},</if>
@@ -1252,7 +1559,7 @@
            <if test="visitDeptName != null">visit_dept_name = #{visitDeptName},</if>
            <if test="managementDoctorCode != null">management_doctor_code = #{managementDoctorCode},</if>
            <if test="managementDoctor != null">management_doctor=#{managementDoctor},</if>
            <if test="currentPreachform != null">current_preachform=#{currentPreachform},</if>
            <if test="currentPreachform != null and currentPreachform != '' ">current_preachform=#{currentPreachform},</if>
            <if test="taskSituation != null">task_situation=#{taskSituation},</if>
            <if test="isabnormal != null">isabnormal=#{isabnormal},</if>
            <if test="continueFlag != null ">continue_flag = #{continueFlag},</if>
@@ -1278,6 +1585,7 @@
            <if test="sex != null">sex = #{sex},</if>
            <if test="delFlag != null">del_flag = #{delFlag},</if>
            <if test="sendstate != null ">sendstate = #{sendstate},</if>
            <if test="sendstate != null and sendstate == 6">finish_state = <include refid="finishStateExpr"/>,</if>
            <if test="age != null">age = #{age},</if>
            <if test="sfzh != null">sfzh = #{sfzh},</if>
            <if test="addr != null">addr = #{addr},</if>
@@ -1293,7 +1601,7 @@
            <if test="recordid != null">recordid = #{recordid},</if>
            <if test="exrecallcount != null">exrecallcount = #{exrecallcount},</if>
            <if test="pulltime != null">pulltime = #{pulltime},</if>
            <if test="deptcode != null">deptcode = #{deptcode},</if>
            <if test="leaveldeptcode != null">leaveldeptcode = #{leaveldeptcode},</if>
            <if test="labelstatus != null">labelstatus = #{labelstatus},</if>
            <if test="updateBy != null">update_by = #{updateBy},</if>
            <if test="updateTime != null">update_time = #{updateTime},</if>
@@ -1320,7 +1628,7 @@
            <if test="wechat != null  and wechat != ''">wechat = #{wechat},</if>
            <if test="roomno != null  and roomno != ''">roomno = #{roomno},</if>
            <if test="admindate != null">admindate = #{admindate},</if>
            <if test="deptname != null  and deptname != ''">deptname = #{deptname},</if>
            <if test="leaveldeptname != null  and leaveldeptname != ''">leaveldeptname = #{leaveldeptname},</if>
            <if test="leaveicd10code != null  and leaveicd10code != ''">leaveicd10code = #{leaveicd10code},</if>
            <if test="leavediagname != null  and leavediagname != ''">leavediagname = #{leavediagname},</if>
            <if test="libtemplateid != null ">libtemplateid = #{libtemplateid},</if>
@@ -1345,6 +1653,7 @@
            <if test="nurseName != null ">nurse_name=#{nurseName},</if>
            <if test="nurseId != null ">nurse_id=#{nurseId},</if>
            <if test="remark != null ">remark=#{remark},</if>
            <if test="doctorOpinion != null ">doctor_opinion=#{doctorOpinion},</if>
            <if test="suggest != null ">suggest=#{suggest},</if>
            <if test="inhospid != null ">inhospid=#{inhospid},</if>
            <if test="patfrom != null ">patfrom=#{patfrom},</if>
@@ -1356,7 +1665,7 @@
            <if test="visitDeptName != null">visit_dept_name = #{visitDeptName},</if>
            <if test="managementDoctorCode != null">management_doctor_code = #{managementDoctorCode},</if>
            <if test="managementDoctor != null">management_doctor=#{managementDoctor},</if>
            <if test="currentPreachform != null">current_preachform=#{currentPreachform},</if>
            <if test="currentPreachform != null and currentPreachform != '' ">current_preachform=#{currentPreachform},</if>
            <if test="taskSituation != null">task_situation=#{taskSituation},</if>
            <if test="isabnormal != null">isabnormal=#{isabnormal},</if>
            <if test="continueFlag != null ">continue_flag = #{continueFlag},</if>
@@ -1389,6 +1698,7 @@
            <if test="phone != null">phone = #{phone},</if>
            <if test="sex != null">sex = #{sex},</if>
            <if test="sendstate != null ">sendstate = #{sendstate},</if>
            <if test="sendstate != null and sendstate == 6">finish_state = <include refid="finishStateExpr"/>,</if>
            <if test="age != null">age = #{age},</if>
            <if test="sfzh != null">sfzh = #{sfzh},</if>
            <if test="addr != null">addr = #{addr},</if>
@@ -1404,7 +1714,7 @@
            <if test="recordid != null">recordid = #{recordid},</if>
            <if test="exrecallcount != null">exrecallcount = #{exrecallcount},</if>
            <if test="pulltime != null">pulltime = #{pulltime},</if>
            <if test="deptcode != null">deptcode = #{deptcode},</if>
            <if test="leaveldeptcode != null">leaveldeptcode = #{leaveldeptcode},</if>
            <if test="labelstatus != null">labelstatus = #{labelstatus},</if>
            <if test="updateBy != null">update_by = #{updateBy},</if>
            <if test="updateTime != null">update_time = #{updateTime},</if>
@@ -1431,7 +1741,7 @@
            <if test="admindate != null ">admindate = #{admindate},</if>
            <if test="visitCount != null ">visit_count = #{visitCount},</if>
            <if test="isVisitAgain != null ">is_visit_again = #{isVisitAgain},</if>
            <if test="deptname != null  and deptname != ''">deptname = #{deptname},</if>
            <if test="leaveldeptname != null  and leaveldeptname != ''">leaveldeptname = #{leaveldeptname},</if>
            <if test="leaveicd10code != null  and leaveicd10code != ''">leaveicd10code = #{leaveicd10code},</if>
            <if test="leavediagname != null  and leavediagname != ''">leavediagname = #{leavediagname},</if>
            <if test="libtemplateid != null ">libtemplateid = #{libtemplateid},</if>
@@ -1456,6 +1766,7 @@
            <if test="nurseName != null ">nurse_name=#{nurseName},</if>
            <if test="nurseId != null ">nurse_id=#{nurseId},</if>
            <if test="remark != null ">remark=#{remark},</if>
            <if test="doctorOpinion != null ">doctor_opinion=#{doctorOpinion},</if>
            <if test="suggest != null ">suggest=#{suggest},</if>
            <if test="inhospid != null ">inhospid=#{inhospid},</if>
            <if test="patfrom != null ">patfrom=#{patfrom},</if>
@@ -1467,7 +1778,7 @@
            <if test="visitDeptName != null">visit_dept_name = #{visitDeptName},</if>
            <if test="managementDoctorCode != null">management_doctor_code = #{managementDoctorCode},</if>
            <if test="managementDoctor != null">management_doctor=#{managementDoctor},</if>
            <if test="currentPreachform != null">current_preachform=#{currentPreachform},</if>
            <if test="currentPreachform != null and currentPreachform != '' ">current_preachform=#{currentPreachform},</if>
            <if test="taskSituation != null">task_situation=#{taskSituation},</if>
            <if test="isabnormal != null">isabnormal=#{isabnormal},</if>
            <if test="continueFlag != null ">continue_flag = #{continueFlag},</if>
@@ -1496,6 +1807,35 @@
        where patid = #{patid} and taskid = #{taskid}
    </update>
    <!-- 人工随访:软删除患者出院记录对应的未完成随访任务 -->
    <update id="deleteUnfinishedServiceSubtaskByInhospids">
        update service_subtask
        set del_flag = 1
        where del_flag = 0
          and patid = #{patid}
          and sendstate in
          <foreach collection="sendstates" item="sendstate" open="(" separator="," close=")">
              #{sendstate}
          </foreach>
          and inhospid in
          <foreach collection="inhospids" item="inhospid" open="(" separator="," close=")">
              #{inhospid}
          </foreach>
    </update>
    <!-- 人工随访重复校验:同一患者同一任务下是否已有未完成随访任务 -->
    <select id="countUnfinishedServiceSubtaskByPatidAndTaskid" resultType="int">
        select count(1)
        from service_subtask
        where del_flag = 0
          and patid = #{patid}
          and taskid = #{taskid}
          and sendstate in
          <foreach collection="sendstates" item="sendstate" open="(" separator="," close=")">
              #{sendstate}
          </foreach>
    </select>
    <select id="getSfStatistics" parameterType="com.ruoyi.task.domain.ServiceSubtaskCountReq"
            resultMap="ServiceSubtaskResult">
        select id,
@@ -1509,17 +1849,21 @@
        management_doctor,
        leavehospitaldistrictcode,
        leavehospitaldistrictname,
        deptcode,
        deptname,
        leaveldeptcode,
        leaveldeptname,
        sendstate,
        preachform,
        current_preachform,
        <include refid="effectiveCurrentPreachformExpr"/> AS current_preachform,
        excep,
        nurse_id,
        nurse_name,
        taskid,
        task_name,
        <if test="groupKey != null and groupKey == 'taskid'">
            (SELECT st.diag_type FROM service_task st WHERE st.taskid = service_subtask.taskid LIMIT 1) AS diag_type,
        </if>
        visit_time,
        visit_dept_code,
        finishtime,
        endtime,
        senddate,
@@ -1537,7 +1881,9 @@
        continue_content,
        continue_count,
        continue_time_next,
        task_situation
        task_situation,
        finish_state,
        is_manually_called
        from service_subtask
        where 1=1
        and del_flag = 0
@@ -1547,18 +1893,30 @@
        <if test="campusid != null and campusid != ''">
            and campusid = #{campusid}
        </if>
        <!-- 2026-10-21:病区为空时视为"不限"
             南华满意度子任务的 leavehospitaldistrictcode 有 83%(3236/3811)为 NULL,
             原来「科室 AND 病区」两个条件永远不可能同时成立 → 整张统计表空。
             判空放行只影响病区为空的记录,有病区的数据行为不变。 -->
        <if test="leavehospitaldistrictcodes != null and leavehospitaldistrictcodes.size() > 0">
            AND leavehospitaldistrictcode IN
            AND (leavehospitaldistrictcode IS NULL OR leavehospitaldistrictcode IN
            <foreach collection="leavehospitaldistrictcodes" item="leavehospitaldistrictcode" open="(" separator=","
                     close=")">
                #{leavehospitaldistrictcode}
            </foreach>
            )
        </if>
        <if test="deptcodes != null and deptcodes.size() > 0">
            AND deptcode IN
            AND leaveldeptcode IN
            <foreach collection="deptcodes" item="deptcode" open="(" separator=","
                     close=")">
                #{deptcode}
            </foreach>
        </if>
        <if test="taskIds != null and taskIds.size() > 0">
            AND taskid IN
            <foreach collection="taskIds" item="taskId" open="(" separator=","
                     close=")">
                #{taskId}
            </foreach>
        </if>
        <if test="serviceType != null and serviceType.size() > 0">
@@ -1568,9 +1926,10 @@
                #{serviceType}
            </foreach>
        </if>
        <!-- 2026-10-21:visit_time 为空(南华满意度 3811/3811 全为 NULL)时按完成/创建时间兜底 -->
        <if test="startTime != null and endTime!=null">
            AND visit_time >= DATE(#{startTime})
            AND visit_time &lt; DATE_ADD(DATE(#{endTime}), INTERVAL 1 DAY)
            AND IFNULL(visit_time, IFNULL(finishtime, create_time)) >= DATE(#{startTime})
            AND IFNULL(visit_time, IFNULL(finishtime, create_time)) &lt; DATE_ADD(DATE(#{endTime}), INTERVAL 1 DAY)
        </if>
        <if test="hospitalStartTime != null and hospitalEndTime!=null">
            AND endtime >= DATE(#{hospitalStartTime})
@@ -1594,7 +1953,7 @@
            AND visit_count > 1
        </if>
        <if test="groupKey != null and groupKey != '' and groupKeyList != null and groupKeyList.size>0">
            AND ${groupKey} IN
            AND IFNULL(${groupKey}, 'Unknown') IN
            <foreach collection="groupKeyList" item="key" open="(" separator=","
                     close=")">
                #{key}
@@ -1673,7 +2032,7 @@
            </foreach>
        </if>
        <if test="deptcodes != null and deptcodes.size() > 0">
            AND b.deptcode IN
            AND b.leaveldeptcode IN
            <foreach collection="deptcodes" item="deptcode" open="(" separator=","
                     close=")">
                #{deptcode}
@@ -1718,9 +2077,17 @@
    <select id="getSfStatisticsGroupKey" parameterType="com.ruoyi.task.domain.ServiceSubtaskCountReq"
            resultType="String">
        select
        <if test="groupKey != null and groupKey != ''">
            ${groupKey} as groupKey
        </if>
        <choose>
            <when test="groupColumn != null and groupColumn != ''">
                ${groupColumn} as groupKey
            </when>
            <when test="groupKey != null and groupKey != ''">
                ${groupKey} as groupKey
            </when>
            <otherwise>
                NULL as groupKey
            </otherwise>
        </choose>
        from service_subtask
        where 1=1
        and del_flag = 0
@@ -1730,18 +2097,77 @@
        <if test="campusid != null and campusid != ''">
            and campusid = #{campusid}
        </if>
        <if test="leavehospitaldistrictcodes != null and leavehospitaldistrictcodes.size() > 0">
            AND leavehospitaldistrictcode IN
            <foreach collection="leavehospitaldistrictcodes" item="leavehospitaldistrictcode" open="(" separator=","
        <!-- 2026-10-21:病区为空时视为"不限"
             南华满意度子任务的 leavehospitaldistrictcode 有 83%(3236/3811)为 NULL,
             原来「科室 AND 病区」两个条件永远不可能同时成立 → 整张统计表空。
             判空放行只影响病区为空的记录,有病区的数据行为不变。 -->
        <choose>
            <!-- 满意度统计(getSfStatisticsScript):改用“有效病区”列,
                 出院病区为空则回退到在院病区。筛选因此真正生效,
                 空病区的记录也会归到它真实的在院病区,而不是混进用户选中的病区。 -->
            <when test="districtColumn != null and districtColumn != ''">
                <!-- 出院病区为空回退到在院病区。这里刻意写成 OR 展开而不是 IFNULL(a,b) IN (...):
                     列被函数包住就用不了索引,56 万行明细表上是全表扫(实测 5.1s → 0.7s)。
                     两个分支分别命中 idx_ss_district_pair 的 (leave..., hospital...) 前缀。 -->
                <if test="leavehospitaldistrictcodes != null and leavehospitaldistrictcodes.size() > 0">
                    AND (leavehospitaldistrictcode IN
                    <foreach collection="leavehospitaldistrictcodes" item="leavehospitaldistrictcode" open="(" separator=","
                             close=")">
                        #{leavehospitaldistrictcode}
                    </foreach>
                    OR (leavehospitaldistrictcode IS NULL AND hospitaldistrictcode IN
                    <foreach collection="leavehospitaldistrictcodes" item="leavehospitaldistrictcode" open="(" separator=","
                             close=")">
                        #{leavehospitaldistrictcode}
                    </foreach>
                    ))
                </if>
            </when>
            <otherwise>
                <if test="leavehospitaldistrictcodes != null and leavehospitaldistrictcodes.size() > 0">
                    AND (leavehospitaldistrictcode IS NULL OR leavehospitaldistrictcode IN
                    <foreach collection="leavehospitaldistrictcodes" item="leavehospitaldistrictcode" open="(" separator=","
                             close=")">
                        #{leavehospitaldistrictcode}
                    </foreach>
                    )
                </if>
            </otherwise>
        </choose>
        <if test="leaveldeptcodes != null and leaveldeptcodes.size() > 0">
            AND leaveldeptcode IN
            <foreach collection="leaveldeptcodes" item="leaveldeptcode" open="(" separator=","
                     close=")">
                #{leavehospitaldistrictcode}
                #{leaveldeptcode}
            </foreach>
        </if>
        <if test="hospitaldistrictcodes != null and hospitaldistrictcodes.size() > 0">
            AND (hospitaldistrictcode IS NULL OR hospitaldistrictcode IN
            <foreach collection="hospitaldistrictcodes" item="hospitaldistrictcode" open="(" separator=","
                     close=")">
                #{hospitaldistrictcode}
            </foreach>
            )
        </if>
        <if test="deptcodes != null and deptcodes.size() > 0">
            AND deptcode IN
            AND leaveldeptcode IN
            <foreach collection="deptcodes" item="deptcode" open="(" separator=","
                     close=")">
                #{deptcode}
            </foreach>
        </if>
        <if test="hospitaldeptcodes != null and hospitaldeptcodes.size() > 0">
            AND deptcode IN
            <foreach collection="hospitaldeptcodes" item="hospitaldeptcode" open="(" separator=","
                     close=")">
                #{hospitaldeptcode}
            </foreach>
        </if>
        <if test="taskIds != null and taskIds.size() > 0">
            AND taskid IN
            <foreach collection="taskIds" item="taskId" open="(" separator=","
                     close=")">
                #{taskId}
            </foreach>
        </if>
        <if test="serviceType != null and serviceType.size() > 0">
@@ -1751,9 +2177,10 @@
                #{serviceType}
            </foreach>
        </if>
        <!-- 2026-10-21:visit_time 为空(南华满意度 3811/3811 全为 NULL)时按完成/创建时间兜底 -->
        <if test="startTime != null and endTime!=null">
            AND visit_time >= DATE(#{startTime})
            AND visit_time &lt; DATE_ADD(DATE(#{endTime}), INTERVAL 1 DAY)
            AND IFNULL(visit_time, IFNULL(finishtime, create_time)) >= DATE(#{startTime})
            AND IFNULL(visit_time, IFNULL(finishtime, create_time)) &lt; DATE_ADD(DATE(#{endTime}), INTERVAL 1 DAY)
        </if>
        <if test="hospitalStartTime != null and hospitalEndTime!=null">
            AND endtime >= DATE(#{hospitalStartTime})
@@ -1776,12 +2203,405 @@
        <if test="visitCount != null and visitCount > 1">
            AND visit_count > 1
        </if>
        <if test="groupKey != null and groupKey != ''">
            group by ${groupKey}
        </if>
        <choose>
            <when test="groupColumn != null and groupColumn != ''">
                group by ${groupColumn}
                order by ${groupColumn}
            </when>
            <when test="groupKey != null and groupKey != ''">
                group by ${groupKey}
                order by ${groupKey}
            </when>
        </choose>
        <!-- pageNum 是 1 起始的,OFFSET 必须用 (pageNum-1)*pageSize;
             以前直接写 OFFSET ${pageNum},第一页会跳掉第一条分组。 -->
        <if test="pageSize != null  and pageNum != null">
            limit ${pageSize} OFFSET ${pageNum}
            limit ${pageSize} OFFSET ${pageOffset}
        </if>
    </select>
    <!--
        ==================================================================
        随访分类统计(/smartor/serviceSubtask/getSfStatisticsCount)性能优化
        ==================================================================
        改造前:getSfStatisticsScript 先把 service_subtask 命中的**全部明细行**
        (getSfStatistics 共 select 39 个字段,含 continue_content / result / suggest 等大字段)
        捞回 JVM,再在内存里按 drcode/科室/病区 逐行 if 累加;此外还要把这次命中的
        **全部子任务 id** 灌进 getSfStatisticsCategory 的 IN 列表再查一遍。
        命中几千行时,网络传输(远端的库尤其明显)+ 内存遍历 + 超大 IN 列表就是接口慢的主因。
        改造后:累加下推到数据库,一次 GROUP BY 只返回“分组数”(通常几十行)的统计结果;
        满意度题目量也同样按分组在库里聚合。统计口径与原来的 getStatisticCategory 逐条对齐。
        这两个语句的 where 条件与 getSfStatistics 保持一致,保证结果集不变。
        分组列(2026-10 起):
          离院维度 —— groupKey = leaveldeptcode / leavehospitaldistrictcode;
          在院维度 —— groupKey = deptcode / hospitaldistrictcode;
          过滤与分组使用同一列(groupColumn = groupKey),避免分组键与聚合行口径不一致。
        ==================================================================
    -->
    <resultMap type="com.ruoyi.task.domain.ServiceSubtaskStatistic" id="ServiceSubtaskScriptStatisticResult">
        <result property="groupCode" column="group_code"/>
        <result property="groupName" column="group_name"/>
        <result property="taskId" column="taskid"/>
        <result property="taskName" column="task_name"/>
        <result property="dischargeCount" column="discharge_count"/>
        <result property="firstVisitCount" column="first_visit_count"/>
        <result property="againVisitCount" column="again_visit_count"/>
        <result property="drcode" column="drcode"/>
        <result property="drname" column="drname"/>
        <result property="deptcode" column="deptcode"/>
        <result property="deptname" column="deptname"/>
        <result property="leavehospitaldistrictcode" column="leavehospitaldistrictcode"/>
        <result property="leavehospitaldistrictname" column="leavehospitaldistrictname"/>
        <result property="hospitaldistrictcode" column="hospitaldistrictcode"/>
        <result property="hospitaldistrictname" column="hospitaldistrictname"/>
        <result property="hospitaldeptcode" column="hospitaldeptcode"/>
        <result property="hospitaldeptname" column="hospitaldeptname"/>
        <result property="nonFollowUp" column="non_follow_up"/>
        <result property="followUpNeeded" column="follow_up_needed"/>
        <result property="needFollowUp" column="need_follow_up"/>
        <result property="pendingFollowUp" column="pending_follow_up"/>
        <result property="followUpSuccess" column="follow_up_success"/>
        <result property="followUpFail" column="follow_up_fail"/>
        <result property="manual" column="manual"/>
        <result property="voice" column="voice"/>
        <result property="sms" column="sms"/>
        <result property="weChat" column="we_chat"/>
        <result property="abnormal" column="abnormal"/>
        <result property="needFollowUpAgain" column="need_follow_up_again"/>
        <result property="pendingFollowUpAgain" column="pending_follow_up_again"/>
        <result property="followUpSuccessAgain" column="follow_up_success_again"/>
        <result property="followUpFailAgain" column="follow_up_fail_again"/>
        <result property="manualAgain" column="manual_again"/>
        <result property="voiceAgain" column="voice_again"/>
        <result property="smsAgain" column="sms_again"/>
        <result property="weChatAgain" column="we_chat_again"/>
        <result property="abnormalAgain" column="abnormal_again"/>
    </resultMap>
    <!--
        按分组聚合随访统计(替代“全量明细回内存再累加”)。
        口径说明(与原 ServiceSubtaskServiceImpl.getStatisticCategory 完全一致):
          * 无需随访:sendstate = 4
          * 应随访:sendstate 非空且 != 4
          * 首次(visit_count = 1) / 再次(visit_count > 1) 两个分支互斥,与原来的 if / else if 一致
          * 人工/语音/短信/微信:首次分支**不**排除 sendstate = 4(原代码如此),再次分支同样
          * 异常:excep = '1' 且 sendstate 非空
          * 首次随访率 / 再次随访率在 Java 里按原公式(含原来的分母判断)计算
    -->
    <select id="getSfStatisticsScriptSum" parameterType="com.ruoyi.task.domain.ServiceSubtaskCountReq"
            resultMap="ServiceSubtaskScriptStatisticResult">
        select
        IFNULL(${groupColumn}, 'Unknown') as group_code,
        <!-- 分组键对应的显示名:病区统计时就是这一组的病区名。
             以前这一列是 MAX(hospitaldistrictname),一个组混了多个病区时会随机显示其中一个,
             于是出现“选血液内科病区却显示骨科中心三病区”。 -->
        <choose>
            <when test="groupKey != null and groupKey == 'taskid'">
                MAX(task_name) as group_name,
            </when>
            <otherwise>
                MAX(IFNULL(leavehospitaldistrictname, hospitaldistrictname)) as group_name,
            </otherwise>
        </choose>
        MAX(taskid) as taskid,
        MAX(task_name) as task_name,
        MAX(drcode) as drcode,
        MAX(drname) as drname,
        MAX(leaveldeptcode) as deptcode,
        MAX(leaveldeptname) as deptname,
        MAX(leavehospitaldistrictcode) as leavehospitaldistrictcode,
        MAX(leavehospitaldistrictname) as leavehospitaldistrictname,
        MAX(hospitaldistrictcode) as hospitaldistrictcode,
        MAX(hospitaldistrictname) as hospitaldistrictname,
        MAX(deptcode) as hospitaldeptcode,
        MAX(deptname) as hospitaldeptname,
        SUM(CASE WHEN visit_count = 1 THEN 1 ELSE 0 END) as first_visit_count,
        SUM(CASE WHEN visit_count > 1 THEN 1 ELSE 0 END) as again_visit_count,
        SUM(CASE WHEN sendstate = 4 THEN 1 ELSE 0 END) as non_follow_up,
        SUM(CASE WHEN sendstate is not null and sendstate != 4 THEN 1 ELSE 0 END) as follow_up_needed,
        SUM(CASE WHEN visit_count = 1 and sendstate is not null and sendstate != 4 THEN 1 ELSE 0 END) as need_follow_up,
        SUM(CASE WHEN visit_count = 1 and sendstate = 2 THEN 1 ELSE 0 END) as pending_follow_up,
        SUM(CASE WHEN visit_count = 1 and sendstate = 6 THEN 1 ELSE 0 END) as follow_up_success,
        SUM(CASE WHEN visit_count = 1 and (sendstate = 5 or sendstate = 7) THEN 1 ELSE 0 END) as follow_up_fail,
        SUM(CASE WHEN visit_count = 1 and sendstate is not null and current_preachform = '1' THEN 1 ELSE 0 END) as manual,
        SUM(CASE WHEN visit_count = 1 and sendstate is not null and current_preachform = '3' THEN 1 ELSE 0 END) as voice,
        SUM(CASE WHEN visit_count = 1 and sendstate is not null and current_preachform = '4' THEN 1 ELSE 0 END) as sms,
        SUM(CASE WHEN visit_count = 1 and sendstate is not null and current_preachform = '5' THEN 1 ELSE 0 END) as we_chat,
        SUM(CASE WHEN visit_count = 1 and sendstate is not null and excep = '1' THEN 1 ELSE 0 END) as abnormal,
        SUM(CASE WHEN visit_count > 1 and sendstate is not null and sendstate != 4 THEN 1 ELSE 0 END) as need_follow_up_again,
        SUM(CASE WHEN visit_count > 1 and sendstate = 2 THEN 1 ELSE 0 END) as pending_follow_up_again,
        SUM(CASE WHEN visit_count > 1 and (sendstate = 1 or sendstate = 3 or sendstate = 6) THEN 1 ELSE 0 END) as follow_up_success_again,
        SUM(CASE WHEN visit_count > 1 and sendstate = 5 THEN 1 ELSE 0 END) as follow_up_fail_again,
        SUM(CASE WHEN visit_count > 1 and sendstate is not null and current_preachform = '1' THEN 1 ELSE 0 END) as manual_again,
        SUM(CASE WHEN visit_count > 1 and sendstate is not null and current_preachform = '3' THEN 1 ELSE 0 END) as voice_again,
        SUM(CASE WHEN visit_count > 1 and sendstate is not null and current_preachform = '4' THEN 1 ELSE 0 END) as sms_again,
        SUM(CASE WHEN visit_count > 1 and sendstate is not null and current_preachform = '5' THEN 1 ELSE 0 END) as we_chat_again,
        SUM(CASE WHEN visit_count > 1 and sendstate is not null and excep = '1' THEN 1 ELSE 0 END) as abnormal_again,
        <!-- 出院人次:原来的内存实现是 serviceSubtaskList.size(),即分组内命中的子任务条数。
             改成本语句后一直没有聚合这一列,页面上“出院人次”恒为 0,与应随访人次自相矛盾。 -->
        COUNT(*) as discharge_count
        from service_subtask
        where 1=1
        and del_flag = 0
        <if test="orgid != null and orgid != ''">
            and orgid = #{orgid}
        </if>
        <if test="campusid != null and campusid != ''">
            and campusid = #{campusid}
        </if>
        <choose>
            <!-- 满意度统计:改用“有效病区”列(出院病区为空回退在院病区),筛选才真正生效 -->
            <when test="districtColumn != null and districtColumn != ''">
                <!-- 出院病区为空回退到在院病区。这里刻意写成 OR 展开而不是 IFNULL(a,b) IN (...):
                     列被函数包住就用不了索引,56 万行明细表上是全表扫(实测 5.1s → 0.7s)。
                     两个分支分别命中 idx_ss_district_pair 的 (leave..., hospital...) 前缀。 -->
                <if test="leavehospitaldistrictcodes != null and leavehospitaldistrictcodes.size() > 0">
                    AND (leavehospitaldistrictcode IN
                    <foreach collection="leavehospitaldistrictcodes" item="leavehospitaldistrictcode" open="(" separator=","
                             close=")">
                        #{leavehospitaldistrictcode}
                    </foreach>
                    OR (leavehospitaldistrictcode IS NULL AND hospitaldistrictcode IN
                    <foreach collection="leavehospitaldistrictcodes" item="leavehospitaldistrictcode" open="(" separator=","
                             close=")">
                        #{leavehospitaldistrictcode}
                    </foreach>
                    ))
                </if>
            </when>
            <otherwise>
                <if test="leavehospitaldistrictcodes != null and leavehospitaldistrictcodes.size() > 0">
                    AND (leavehospitaldistrictcode IS NULL OR leavehospitaldistrictcode IN
                    <foreach collection="leavehospitaldistrictcodes" item="leavehospitaldistrictcode" open="(" separator=","
                             close=")">
                        #{leavehospitaldistrictcode}
                    </foreach>
                    )
                </if>
            </otherwise>
        </choose>
        <if test="deptcodes != null and deptcodes.size() > 0">
            AND leaveldeptcode IN
            <foreach collection="deptcodes" item="deptcode" open="(" separator=","
                     close=")">
                #{deptcode}
            </foreach>
        </if>
        <if test="leaveldeptcodes != null and leaveldeptcodes.size() > 0">
            AND leaveldeptcode IN
            <foreach collection="leaveldeptcodes" item="leaveldeptcode" open="(" separator=","
                     close=")">
                #{leaveldeptcode}
            </foreach>
        </if>
        <if test="hospitaldistrictcodes != null and hospitaldistrictcodes.size() > 0">
            AND (hospitaldistrictcode IS NULL OR hospitaldistrictcode IN
            <foreach collection="hospitaldistrictcodes" item="hospitaldistrictcode" open="(" separator=","
                     close=")">
                #{hospitaldistrictcode}
            </foreach>
            )
        </if>
        <if test="hospitaldeptcodes != null and hospitaldeptcodes.size() > 0">
            AND deptcode IN
            <foreach collection="hospitaldeptcodes" item="hospitaldeptcode" open="(" separator=","
                     close=")">
                #{hospitaldeptcode}
            </foreach>
        </if>
        <if test="serviceType != null and serviceType.size() > 0">
            AND service_type IN
            <foreach collection="serviceType" item="serviceType" open="(" separator=","
                     close=")">
                #{serviceType}
            </foreach>
        </if>
        <if test="startTime != null and endTime!=null">
            AND IFNULL(visit_time, IFNULL(finishtime, create_time)) >= DATE(#{startTime})
            AND IFNULL(visit_time, IFNULL(finishtime, create_time)) &lt; DATE_ADD(DATE(#{endTime}), INTERVAL 1 DAY)
        </if>
        <if test="hospitalStartTime != null and hospitalEndTime!=null">
            AND endtime >= DATE(#{hospitalStartTime})
            AND endtime &lt; DATE_ADD(DATE(#{hospitalEndTime}), INTERVAL 1 DAY)
        </if>
        <if test="visitDeptCode != null">and visit_dept_code = #{visitDeptCode}</if>
        <if test="visitDeptName != null">and visit_dept_name = #{visitDeptName}</if>
        <if test="isabnormal != null">and isabnormal = #{isabnormal}</if>
        <if test="continueFlag != null ">and continue_flag = #{continueFlag}</if>
        <if test="continueTimeNow != null ">and continue_time_now = #{continueTimeNow,jdbcType=TIMESTAMP}</if>
        <if test="continueCount != null ">and continue_count = #{continueCount}</if>
        <if test="continueTimeNext != null ">and continue_time_next = #{continueTimeNext,jdbcType=TIMESTAMP}</if>
        <if test="type != null">
            and type = #{type}
        </if>
        <if test="visitCount != null and visitCount == 1">
            AND visit_count = 1
        </if>
        <if test="visitCount != null and visitCount > 1">
            AND visit_count > 1
        </if>
        <!-- 过滤列必须与 GROUP BY 的列一致:都用 groupColumn(满意度病区统计时它是
             IFNULL(出院病区, 在院病区) 这样的表达式),否则聚合出来的分组键过滤不上,题目量会全变 0。 -->
        <if test="groupColumn != null and groupColumn != '' and groupKeyList != null and groupKeyList.size>0">
            AND IFNULL(${groupColumn}, 'Unknown') IN
            <foreach collection="groupKeyList" item="key" open="(" separator=","
                     close=")">
                #{key}
            </foreach>
        </if>
        group by IFNULL(${groupColumn}, 'Unknown')
    </select>
    <!--
        按分组聚合满意度题目量(替代 getSfStatisticsCategory 的“逐子任务 IN 查询 + 内存 N*M 过滤”)。
        where 条件 = 上面聚合查询的条件 + 原 getSfStatisticsCategory 的明细条件,
        因此命中的明细集合与改造前(getSfStatisticsCategory 结果再按子任务与分组求交)完全一致。
        configValue 为空时不调用本语句(原实现在这种场景下题目量一律为 0)。
    -->
    <select id="getSfStatisticsScriptJoySum" parameterType="com.ruoyi.task.domain.ServiceSubtaskCountReq"
            resultType="map">
        select
        <choose>
            <when test="districtColumn != null and districtColumn != ''">
                IFNULL(${districtColumn}, 'Unknown') as groupCode,
            </when>
            <otherwise>
                IFNULL(t.${groupColumn}, 'Unknown') as groupCode,
            </otherwise>
        </choose>
        SUM(CASE WHEN a.categoryid IN(${configValue})
        THEN 1 ELSE 0 END) as joyAllCount,
        SUM(CASE WHEN a.categoryid IN(${configValue})
        AND IFNULL(a.matchedtext,a.asrtext) is not null
        AND IFNULL(a.matchedtext,a.asrtext) != ''
        THEN 1 ELSE 0 END) as joyCount
        from service_subtask_detail a
        INNER JOIN service_subtask t on a.sub_id = t.id
        where 1=1
        and a.del_flag = 0
        and t.del_flag = 0
        <if test="orgid != null and orgid != ''">
            and t.orgid = #{orgid}
        </if>
        <if test="campusid != null and campusid != ''">
            and t.campusid = #{campusid}
        </if>
        <choose>
            <!-- 满意度统计:与聚合主查询一致,用“有效病区”列过滤,
                 否则题目量会和主查询的行对不上(分组键不一样,过滤不上就全变 0)。 -->
            <when test="districtColumn != null and districtColumn != ''">
                <!-- 出院病区为空回退到在院病区。这里刻意写成 OR 展开而不是 IFNULL(a,b) IN (...):
                     列被函数包住就用不了索引,56 万行明细表上是全表扫(实测 5.1s → 0.7s)。
                     两个分支分别命中 idx_ss_district_pair 的 (leave..., hospital...) 前缀。 -->
                <if test="leavehospitaldistrictcodes != null and leavehospitaldistrictcodes.size() > 0">
                    AND (leavehospitaldistrictcode IN
                    <foreach collection="leavehospitaldistrictcodes" item="leavehospitaldistrictcode" open="(" separator=","
                             close=")">
                        #{leavehospitaldistrictcode}
                    </foreach>
                    OR (leavehospitaldistrictcode IS NULL AND hospitaldistrictcode IN
                    <foreach collection="leavehospitaldistrictcodes" item="leavehospitaldistrictcode" open="(" separator=","
                             close=")">
                        #{leavehospitaldistrictcode}
                    </foreach>
                    ))
                </if>
            </when>
            <otherwise>
                <if test="leavehospitaldistrictcodes != null and leavehospitaldistrictcodes.size() > 0">
                    AND (t.leavehospitaldistrictcode IS NULL OR t.leavehospitaldistrictcode IN
                    <foreach collection="leavehospitaldistrictcodes" item="leavehospitaldistrictcode" open="(" separator=","
                             close=")">
                        #{leavehospitaldistrictcode}
                    </foreach>
                    )
                </if>
            </otherwise>
        </choose>
        <if test="deptcodes != null and deptcodes.size() > 0">
            AND t.leaveldeptcode IN
            <foreach collection="deptcodes" item="deptcode" open="(" separator=","
                     close=")">
                #{deptcode}
            </foreach>
        </if>
        <if test="leaveldeptcodes != null and leaveldeptcodes.size() > 0">
            AND t.leaveldeptcode IN
            <foreach collection="leaveldeptcodes" item="leaveldeptcode" open="(" separator=","
                     close=")">
                #{leaveldeptcode}
            </foreach>
        </if>
        <if test="hospitaldistrictcodes != null and hospitaldistrictcodes.size() > 0">
            AND (t.hospitaldistrictcode IS NULL OR t.hospitaldistrictcode IN
            <foreach collection="hospitaldistrictcodes" item="hospitaldistrictcode" open="(" separator=","
                     close=")">
                #{hospitaldistrictcode}
            </foreach>
            )
        </if>
        <if test="hospitaldeptcodes != null and hospitaldeptcodes.size() > 0">
            AND t.deptcode IN
            <foreach collection="hospitaldeptcodes" item="hospitaldeptcode" open="(" separator=","
                     close=")">
                #{hospitaldeptcode}
            </foreach>
        </if>
        <if test="serviceType != null and serviceType.size() > 0">
            AND t.service_type IN
            <foreach collection="serviceType" item="serviceType" open="(" separator=","
                     close=")">
                #{serviceType}
            </foreach>
        </if>
        <if test="startTime != null and endTime!=null">
            AND IFNULL(t.visit_time, IFNULL(t.finishtime, t.create_time)) >= DATE(#{startTime})
            AND IFNULL(t.visit_time, IFNULL(t.finishtime, t.create_time)) &lt; DATE_ADD(DATE(#{endTime}), INTERVAL 1 DAY)
        </if>
        <if test="hospitalStartTime != null and hospitalEndTime!=null">
            AND t.endtime >= DATE(#{hospitalStartTime})
            AND t.endtime &lt; DATE_ADD(DATE(#{hospitalEndTime}), INTERVAL 1 DAY)
        </if>
        <if test="visitDeptCode != null">and t.visit_dept_code = #{visitDeptCode}</if>
        <if test="visitDeptName != null">and t.visit_dept_name = #{visitDeptName}</if>
        <if test="isabnormal != null">and t.isabnormal = #{isabnormal}</if>
        <if test="continueFlag != null ">and t.continue_flag = #{continueFlag}</if>
        <if test="continueTimeNow != null ">and t.continue_time_now = #{continueTimeNow,jdbcType=TIMESTAMP}</if>
        <if test="continueCount != null ">and t.continue_count = #{continueCount}</if>
        <if test="continueTimeNext != null ">and t.continue_time_next = #{continueTimeNext,jdbcType=TIMESTAMP}</if>
        <if test="type != null">
            and t.type = #{type}
        </if>
        <if test="visitCount != null and visitCount == 1">
            AND t.visit_count = 1
        </if>
        <if test="visitCount != null and visitCount > 1">
            AND t.visit_count > 1
        </if>
        <if test="districtColumn != null and districtColumn != '' and groupKeyList != null and groupKeyList.size>0">
            AND IFNULL(${districtColumn}, 'Unknown') IN
            <foreach collection="groupKeyList" item="key" open="(" separator=","
                     close=")">
                #{key}
            </foreach>
        </if>
        <if test="(districtColumn == null or districtColumn == '') and groupKey != null and groupKey != '' and groupKeyList != null and groupKeyList.size>0">
            AND IFNULL(t.${groupKey}, 'Unknown') IN
            <foreach collection="groupKeyList" item="key" open="(" separator=","
                     close=")">
                #{key}
            </foreach>
        </if>
        <choose>
            <when test="districtColumn != null and districtColumn != ''">
                group by IFNULL(${districtColumn}, 'Unknown')
            </when>
            <otherwise>
                group by IFNULL(t.${groupColumn}, 'Unknown')
            </otherwise>
        </choose>
    </select>
    <select id="getSfStatisticsHyperlink" parameterType="com.ruoyi.task.domain.ServiceSubtaskCountReq"
@@ -1795,17 +2615,19 @@
        drname,
        leavehospitaldistrictcode,
        leavehospitaldistrictname,
        deptcode,
        deptname,
        leaveldeptcode,
        leaveldeptname,
        sendstate,
        preachform,
        current_preachform,
        <include refid="effectiveCurrentPreachformExpr"/> AS current_preachform,
        excep,
        nurse_id,
        nurse_name,
        taskid,
        task_name,
        visit_time,
        visit_dept_code,
        visit_dept_name,
        finishtime,
        endtime,
        senddate,
@@ -1824,7 +2646,10 @@
        continue_count,
        continue_time_next,
        task_situation,
        remark
        remark,
        doctor_opinion,
        finish_state,
        is_manually_called
        from service_subtask
        where 1=1
        and del_flag = 0
@@ -1834,15 +2659,24 @@
        <if test="campusid != null and campusid != ''">
            and campusid = #{campusid}
        </if>
        <!-- 2026-10-21:病区为空时视为"不限"(南华满意度 83% 子任务无病区),有病区的数据行为不变 -->
        <!-- 与 getSfStatisticsScript 的汇总行保持一致:出院病区为空则回退到在院病区,
             否则点“查看详情”查出来的条数会和汇总行的数字对不上。 -->
        <if test="leavehospitaldistrictcodes != null and leavehospitaldistrictcodes.size() > 0">
            AND leavehospitaldistrictcode IN
            AND (leavehospitaldistrictcode IN
            <foreach collection="leavehospitaldistrictcodes" item="leavehospitaldistrictcode" open="(" separator=","
                     close=")">
                #{leavehospitaldistrictcode}
            </foreach>
            OR (leavehospitaldistrictcode IS NULL AND hospitaldistrictcode IN
            <foreach collection="leavehospitaldistrictcodes" item="leavehospitaldistrictcode" open="(" separator=","
                     close=")">
                #{leavehospitaldistrictcode}
            </foreach>
            ))
        </if>
        <if test="deptcodes != null and deptcodes.size() > 0">
            AND deptcode IN
            AND leaveldeptcode IN
            <foreach collection="deptcodes" item="deptcode" open="(" separator=","
                     close=")">
                #{deptcode}
@@ -1855,9 +2689,14 @@
                #{serviceType}
            </foreach>
        </if>
        <!-- 2026-10-21:visit_time 为空时按完成/创建时间兜底 -->
        <if test="startTime != null and endTime!=null">
            AND visit_time >= DATE(#{startTime})
            AND visit_time &lt; DATE_ADD(DATE(#{endTime}), INTERVAL 1 DAY)
            AND IFNULL(visit_time, IFNULL(finishtime, create_time)) >= DATE(#{startTime})
            AND IFNULL(visit_time, IFNULL(finishtime, create_time)) &lt; DATE_ADD(DATE(#{endTime}), INTERVAL 1 DAY)
        </if>
        <if test="hospitalStartTime != null and hospitalEndTime!=null">
            AND endtime >= DATE(#{hospitalStartTime})
            AND endtime &lt; DATE_ADD(DATE(#{hospitalEndTime}), INTERVAL 1 DAY)
        </if>
        <if test="visitDeptCode != null">and visit_dept_code = #{visitDeptCode}</if>
        <if test="visitDeptName != null">and visit_dept_name = #{visitDeptName}</if>
@@ -1870,14 +2709,19 @@
        <if test="type != null">
            and type = #{type}
        </if>
        <if test="visitCount != null and visitCount == 1">
        <!-- 首个再次随访:精确查 visit_count = 2,与统计口径一致 -->
        <if test="hyperLinkInfoType == 'againFollowUpPushCountInfo'">
            AND visit_count = 2
        </if>
        <!-- 其他类型保持原语义:1 首次,>1 再次 -->
        <if test="hyperLinkInfoType != 'againFollowUpPushCountInfo' and visitCount != null and visitCount == 1">
            AND visit_count = 1
        </if>
        <if test="visitCount != null and visitCount > 1">
        <if test="hyperLinkInfoType != 'againFollowUpPushCountInfo' and visitCount != null and visitCount > 1">
            AND visit_count > 1
        </if>
        <if test="groupKey != null and groupKey != '' and groupKeyList != null and groupKeyList.size>0">
            AND ${groupKey} IN
            AND IFNULL(${groupKey}, 'Unknown') IN
            <foreach collection="groupKeyList" item="key" open="(" separator=","
                     close=")">
                #{key}
@@ -1890,8 +2734,8 @@
                #{state}
            </foreach>
        </if>
        <if test="currentPreachform != null">
            AND current_preachform = #{currentPreachform}
        <if test="currentPreachform != null and currentPreachform != '' ">
            AND <include refid="effectiveCurrentPreachformExpr"/> = #{currentPreachform}
            <if test="followUpCountStyle != null and followUpCountStyle = '1'">
                AND sendstate != 4
            </if>
@@ -1902,6 +2746,9 @@
        <if test="taskSituation != null">
            AND task_situation = #{taskSituation}
        </if>
        <if test="finishState != null">
            AND finish_state = #{finishState}
        </if>
        <if test="drcode != null and drcode != ''">
            AND drcode = #{drcode}
        </if>
@@ -1909,7 +2756,13 @@
            AND leavehospitaldistrictcode = #{leavehospitaldistrictcode}
        </if>
        <if test="deptcode != null and deptcode != ''">
            AND deptcode = #{deptcode}
            AND leaveldeptcode = #{deptcode}
        </if>
        <if test="isManuallyCalled != null">
            AND is_manually_called = #{isManuallyCalled}
        </if>
        <if test="suggest != null and suggest != ''">
            AND TRIM(suggest) = #{suggest}
        </if>
        order by endtime desc
        <if test="pageSize != null  and pageNum != null">
@@ -1932,15 +2785,17 @@
        <if test="campusid != null and campusid != ''">
            and campusid = #{campusid}
        </if>
        <!-- 2026-10-21:病区为空时视为"不限"(南华满意度 83% 子任务无病区),有病区的数据行为不变 -->
        <if test="leavehospitaldistrictcodes != null and leavehospitaldistrictcodes.size() > 0">
            AND leavehospitaldistrictcode IN
            AND (leavehospitaldistrictcode IS NULL OR leavehospitaldistrictcode IN
            <foreach collection="leavehospitaldistrictcodes" item="leavehospitaldistrictcode" open="(" separator=","
                     close=")">
                #{leavehospitaldistrictcode}
            </foreach>
            )
        </if>
        <if test="deptcodes != null and deptcodes.size() > 0">
            AND deptcode IN
            AND leaveldeptcode IN
            <foreach collection="deptcodes" item="deptcode" open="(" separator=","
                     close=")">
                #{deptcode}
@@ -1954,8 +2809,8 @@
            </foreach>
        </if>
        <if test="startTime != null and endTime!=null">
            AND date_format(visit_time,'%y%m%d') >= date_format(#{startTime},'%y%m%d')
            AND date_format(visit_time,'%y%m%d') &lt;= date_format(#{endTime},'%y%m%d')
            AND date_format(IFNULL(visit_time, IFNULL(finishtime, create_time)),'%y%m%d') >= date_format(#{startTime},'%y%m%d')
            AND date_format(IFNULL(visit_time, IFNULL(finishtime, create_time)),'%y%m%d') &lt;= date_format(#{endTime},'%y%m%d')
        </if>
        <if test="visitDeptCode != null">and visit_dept_code = #{visitDeptCode}</if>
        <if test="visitDeptName != null">and visit_dept_name = #{visitDeptName}</if>
@@ -1979,15 +2834,17 @@
        <if test="campusid != null and campusid != ''">
            and campusid = #{campusid}
        </if>
        <!-- 2026-10-21:病区为空时视为"不限"(南华满意度 83% 子任务无病区),有病区的数据行为不变 -->
        <if test="leavehospitaldistrictcodes != null and leavehospitaldistrictcodes.size() > 0">
            AND leavehospitaldistrictcode IN
            AND (leavehospitaldistrictcode IS NULL OR leavehospitaldistrictcode IN
            <foreach collection="leavehospitaldistrictcodes" item="leavehospitaldistrictcode" open="(" separator=","
                     close=")">
                #{leavehospitaldistrictcode}
            </foreach>
            )
        </if>
        <if test="deptcodes != null and deptcodes.size() > 0">
            AND deptcode IN
            AND leaveldeptcode IN
            <foreach collection="deptcodes" item="deptcode" open="(" separator=","
                     close=")">
                #{deptcode}
@@ -2001,8 +2858,8 @@
            </foreach>
        </if>
        <if test="startTime != null and endTime!=null">
            AND date_format(visit_time,'%y%m%d') >= date_format(#{startTime},'%y%m%d')
            AND date_format(visit_time,'%y%m%d') &lt;= date_format(#{endTime},'%y%m%d')
            AND date_format(IFNULL(visit_time, IFNULL(finishtime, create_time)),'%y%m%d') >= date_format(#{startTime},'%y%m%d')
            AND date_format(IFNULL(visit_time, IFNULL(finishtime, create_time)),'%y%m%d') &lt;= date_format(#{endTime},'%y%m%d')
        </if>
        ) AND targetid=a.id) joyCount , (SELECT COUNT(*) from (select sub_id FROM service_subtask_detail
        WHERE sub_id IN(SELECT id FROM service_subtask t
@@ -2023,7 +2880,7 @@
            </foreach>
        </if>
        <if test="deptcodes != null and deptcodes.size() > 0">
            AND deptcode IN
            AND leaveldeptcode IN
            <foreach collection="deptcodes" item="deptcode" open="(" separator=","
                     close=")">
                #{deptcode}
@@ -2037,8 +2894,8 @@
            </foreach>
        </if>
        <if test="startTime != null and endTime!=null">
            AND date_format(visit_time,'%y%m%d') >= date_format(#{startTime},'%y%m%d')
            AND date_format(visit_time,'%y%m%d') &lt;= date_format(#{endTime},'%y%m%d')
            AND date_format(IFNULL(visit_time, IFNULL(finishtime, create_time)),'%y%m%d') >= date_format(#{startTime},'%y%m%d')
            AND date_format(IFNULL(visit_time, IFNULL(finishtime, create_time)),'%y%m%d') &lt;= date_format(#{endTime},'%y%m%d')
        </if>
        GROUP BY sub_id ) ) ta ) joyAllCount
        FROM ivr_liba_target a
@@ -2072,15 +2929,20 @@
        <if test="campusid != null and campusid != ''">
            and t.campusid = #{campusid}
        </if>
        <!-- 2026-10-21:病区为空时视为"不限"
             南华满意度子任务的 leavehospitaldistrictcode 有 83%(3236/3811)为 NULL,
             原来「科室 AND 病区」两个条件永远不可能同时成立 → 整张统计表空。
             判空放行只影响病区为空的记录,有病区的数据行为不变。 -->
        <if test="leavehospitaldistrictcodes != null and leavehospitaldistrictcodes.size() > 0">
            AND t.leavehospitaldistrictcode IN
            AND (t.leavehospitaldistrictcode IS NULL OR t.leavehospitaldistrictcode IN
            <foreach collection="leavehospitaldistrictcodes" item="leavehospitaldistrictcode" open="(" separator=","
                     close=")">
                #{leavehospitaldistrictcode}
            </foreach>
            )
        </if>
        <if test="deptcodes != null and deptcodes.size() > 0">
            AND t.deptcode IN
            AND t.leaveldeptcode IN
            <foreach collection="deptcodes" item="deptcode" open="(" separator=","
                     close=")">
                #{deptcode}
@@ -2100,9 +2962,10 @@
                #{serviceType}
            </foreach>
        </if>
        <!-- 2026-10-21:visit_time 为空时按完成/创建时间兜底 -->
        <if test="startTime != null and endTime!=null">
            AND date_format(t.visit_time,'%y%m%d') >= date_format(#{startTime},'%y%m%d')
            AND date_format(t.visit_time,'%y%m%d') &lt;= date_format(#{endTime},'%y%m%d')
            AND date_format(IFNULL(t.visit_time, IFNULL(t.finishtime, t.create_time)),'%y%m%d') >= date_format(#{startTime},'%y%m%d')
            AND date_format(IFNULL(t.visit_time, IFNULL(t.finishtime, t.create_time)),'%y%m%d') &lt;= date_format(#{endTime},'%y%m%d')
        </if>
        <if test="visitDeptCode != null">
            and t.visit_dept_code = #{visitDeptCode}
@@ -2123,14 +2986,14 @@
    <select id="getSfStatisticsScriptDetails" parameterType="com.ruoyi.task.domain.ServiceSubtaskCountReq"
            resultMap="ServiceSubtaskScriptCountResult">
        SELECT
        a.id as scriptid,
        a.templateid as templateid,
        a.script_type,
        MIN(a.id) as scriptid,
        MIN(a.templateid) as templateid,
        MIN(a.script_type) as script_type,
        a.script_content,
        b.subtask_type,
        b.completed_quantity,
        b.all_quantity,
        ROUND(b.completed_quantity/b.all_quantity,2) as completed_percentage
        SUM(b.completed_quantity) as completed_quantity,
        SUM(b.all_quantity) as all_quantity,
        ROUND(SUM(b.completed_quantity)/SUM(b.all_quantity),2) as completed_percentage
        FROM
        <if test="type == 1">
            ivr_task_template_script a
@@ -2159,17 +3022,44 @@
            AND ss.campusid = #{campusid}
        </if>
        <if test="leavehospitaldistrictcodes != null and leavehospitaldistrictcodes.size() > 0">
            AND ss.leavehospitaldistrictcode IN
            AND (ss.leavehospitaldistrictcode IN
            <foreach collection="leavehospitaldistrictcodes" item="leavehospitaldistrictcode" open="(" separator=","
                     close=")">
                #{leavehospitaldistrictcode}
            </foreach>
            OR (ss.leavehospitaldistrictcode IS NULL AND ss.hospitaldistrictcode IN
            <foreach collection="leavehospitaldistrictcodes" item="leavehospitaldistrictcode" open="(" separator=","
                     close=")">
                #{leavehospitaldistrictcode}
            </foreach>
            ))
        </if>
        <if test="deptcodes != null and deptcodes.size() > 0">
            AND ss.deptcode IN
            AND ss.leaveldeptcode IN
            <foreach collection="deptcodes" item="deptcode" open="(" separator=","
                     close=")">
                #{deptcode}
            </foreach>
        </if>
        <if test="leaveldeptcodes != null and leaveldeptcodes.size() > 0">
            AND ss.leaveldeptcode IN
            <foreach collection="leaveldeptcodes" item="leaveldeptcode" open="(" separator=","
                     close=")">
                #{leaveldeptcode}
            </foreach>
        </if>
        <if test="hospitaldistrictcodes != null and hospitaldistrictcodes.size() > 0">
            AND ss.hospitaldistrictcode IN
            <foreach collection="hospitaldistrictcodes" item="hospitaldistrictcode" open="(" separator=","
                     close=")">
                #{hospitaldistrictcode}
            </foreach>
        </if>
        <if test="hospitaldeptcodes != null and hospitaldeptcodes.size() > 0">
            AND ss.deptcode IN
            <foreach collection="hospitaldeptcodes" item="hospitaldeptcode" open="(" separator=","
                     close=")">
                #{hospitaldeptcode}
            </foreach>
        </if>
        <if test="serviceType != null and serviceType.size() > 0">
@@ -2179,9 +3069,10 @@
                #{serviceType}
            </foreach>
        </if>
        <!-- 2026-10-21:visit_time 为空(南华满意度 3811/3811 全为 NULL)时按完成/创建时间兜底 -->
        <if test="startTime != null and endTime!=null">
            AND date_format(ss.visit_time,'%y%m%d') >= date_format(#{startTime},'%y%m%d')
            AND date_format(ss.visit_time,'%y%m%d') &lt;= date_format(#{endTime},'%y%m%d')
            AND date_format(IFNULL(ss.visit_time, IFNULL(ss.finishtime, ss.create_time)),'%y%m%d') >= date_format(#{startTime},'%y%m%d')
            AND date_format(IFNULL(ss.visit_time, IFNULL(ss.finishtime, ss.create_time)),'%y%m%d') &lt;= date_format(#{endTime},'%y%m%d')
        </if>
        <!-- 目前只统计语音和问卷 -->
        <if test="type != null">
@@ -2193,30 +3084,121 @@
        group by ssd.scriptid) b
        ON a.id = b.scriptid
        <!-- 适用单条查询 -->
        <if test="scriptids != null">
        <if test="scriptids != null and scriptids != ''">
            AND a.scriptid in
            (SELECT lib_script.scriptid FROM
            svy_lib_template_script lib_script
            WHERE lib_script.scriptid in (${scriptids}))
        </if>
        <!-- 同一道题会被复制到多个任务模板,按题目内容合并,保证一道题只返回一条 -->
        GROUP BY a.script_content, b.subtask_type
        ORDER BY MIN(a.id)
    </select>
    <select id="getSfStatisticsScriptItemDetails" parameterType="com.ruoyi.task.domain.ServiceSubtaskCountReq"
            resultMap="ServiceSubtaskScriptCountResult">
        SELECT
        ssd.scriptid,
        a.script_content,
        ss.type as subtask_type,
        IFNULL(ssd.matchedtext,ssd.asrtext) AS option_text,
        COUNT(*) AS chosen_quantity
        FROM
        service_subtask_detail ssd
        INNER JOIN service_subtask ss ON ssd.sub_id = ss.id
        INNER JOIN
        <if test="type == 1">
            ivr_task_template_script a ON a.id = ssd.scriptid
        </if>
        <if test="type == 2">
            svy_task_template_script a ON a.id = ssd.scriptid
        </if>
        where 1=1
        and ss.del_flag=0
        and ssd.del_flag=0
        <if test="orgid != null and orgid != ''">
            and ss.orgid = #{orgid}
        </if>
        <if test="campusid != null and campusid != ''">
            and ss.campusid = #{campusid}
        </if>
        <if test="leavehospitaldistrictcodes != null and leavehospitaldistrictcodes.size() > 0">
            AND (ss.leavehospitaldistrictcode IN
            <foreach collection="leavehospitaldistrictcodes" item="leavehospitaldistrictcode" open="(" separator=","
                     close=")">
                #{leavehospitaldistrictcode}
            </foreach>
            OR (ss.leavehospitaldistrictcode IS NULL AND ss.hospitaldistrictcode IN
            <foreach collection="leavehospitaldistrictcodes" item="leavehospitaldistrictcode" open="(" separator=","
                     close=")">
                #{leavehospitaldistrictcode}
            </foreach>
            ))
        </if>
        <if test="deptcodes != null and deptcodes.size() > 0">
            AND ss.leaveldeptcode IN
            <foreach collection="deptcodes" item="deptcode" open="(" separator=","
                     close=")">
                #{deptcode}
            </foreach>
        </if>
        <if test="leaveldeptcodes != null and leaveldeptcodes.size() > 0">
            AND ss.leaveldeptcode IN
            <foreach collection="leaveldeptcodes" item="leaveldeptcode" open="(" separator=","
                     close=")">
                #{leaveldeptcode}
            </foreach>
        </if>
        <if test="hospitaldistrictcodes != null and hospitaldistrictcodes.size() > 0">
            AND ss.hospitaldistrictcode IN
            <foreach collection="hospitaldistrictcodes" item="hospitaldistrictcode" open="(" separator=","
                     close=")">
                #{hospitaldistrictcode}
            </foreach>
        </if>
        <if test="hospitaldeptcodes != null and hospitaldeptcodes.size() > 0">
            AND ss.deptcode IN
            <foreach collection="hospitaldeptcodes" item="hospitaldeptcode" open="(" separator=","
                     close=")">
                #{hospitaldeptcode}
            </foreach>
        </if>
        <if test="serviceType != null and serviceType.size() > 0">
            AND ss.service_type IN
            <foreach collection="serviceType" item="serviceType" open="(" separator=","
                     close=")">
                #{serviceType}
            </foreach>
        </if>
        <!-- 2026-10-21:visit_time 为空(南华满意度 3811/3811 全为 NULL)时按完成/创建时间兜底 -->
        <if test="startTime != null and endTime!=null">
            AND date_format(IFNULL(ss.visit_time, IFNULL(ss.finishtime, ss.create_time)),'%y%m%d') >= date_format(#{startTime},'%y%m%d')
            AND date_format(IFNULL(ss.visit_time, IFNULL(ss.finishtime, ss.create_time)),'%y%m%d') &lt;= date_format(#{endTime},'%y%m%d')
        </if>
        <!-- 目前只统计语音和问卷 -->
        <if test="type != null">
            and ss.type = #{type}
        </if>
        <if test="configValue != null and configValue != ''">
            AND ssd.categoryid IN (${configValue})
        </if>
        group by a.script_content, ss.type, IFNULL(ssd.matchedtext,ssd.asrtext)
    </select>
    <!--
      问题选项数字下钻:返回选择了指定选项的 service_subtask 列表。
      过滤口径与 getSfStatisticsScriptDetails / getSfStatisticsScriptItemDetails 保持一致,
      额外支持 statisticaltype:1-病区、2-科室、3-病种。
    -->
    <select id="getSfStatisticsScriptDetailsHyperlink" parameterType="com.ruoyi.task.domain.ServiceSubtaskCountReq"
            resultMap="ServiceSubtaskResult">
        SELECT ss.*
        FROM service_subtask ss
        WHERE ss.del_flag = 0
        <if test="orgid != null and orgid != ''">
            AND ss.orgid = #{orgid}
        </if>
        <if test="campusid != null and campusid != ''">
            AND ss.campusid = #{campusid}
        </if>
        <if test="leavehospitaldistrictcodes != null and leavehospitaldistrictcodes.size() > 0">
            AND ss.leavehospitaldistrictcode IN
@@ -2227,30 +3209,109 @@
        </if>
        <if test="deptcodes != null and deptcodes.size() > 0">
            AND ss.deptcode IN
            <foreach collection="deptcodes" item="deptcode" open="(" separator=","
                     close=")">
            <foreach collection="deptcodes" item="deptcode" open="(" separator="," close=")">
                #{deptcode}
            </foreach>
        </if>
        <if test="serviceType != null and serviceType.size() > 0">
            AND ss.service_type IN
            <foreach collection="serviceType" item="serviceType" open="(" separator=","
                     close=")">
            <foreach collection="serviceType" item="serviceType" open="(" separator="," close=")">
                #{serviceType}
            </foreach>
        </if>
        <if test="startTime != null and endTime!=null">
            AND date_format(ss.visit_time,'%y%m%d') >= date_format(#{startTime},'%y%m%d')
            AND date_format(ss.visit_time,'%y%m%d') &lt;= date_format(#{endTime},'%y%m%d')
        <if test="startTime != null and endTime != null">
            AND ss.visit_time >= DATE(#{startTime})
            AND ss.visit_time &lt; DATE_ADD(DATE(#{endTime}), INTERVAL 1 DAY)
        </if>
        <!-- 目前只统计语音和问卷 -->
        <if test="type != null">
            and ss.type = #{type}
        <if test="type != null and type != ''">
            AND ss.type = #{type}
        </if>
        <if test="configValue != null and configValue != ''">
            AND ssd.categoryid IN (${configValue})
        <!-- 统计维度:1-按病区 2-按科室 3-按taskid -->
        <if test="statisticaltype != null and statisticaltype == 1">
            <choose>
                <when test="deptcode != null and deptcode != ''">
                    AND ss.leavehospitaldistrictcode = #{deptcode}
                </when>
                <when test="leavehospitaldistrictcode != null and leavehospitaldistrictcode != ''">
                    AND ss.leavehospitaldistrictcode = #{leavehospitaldistrictcode}
                </when>
            </choose>
        </if>
        group by ssd.scriptid, IFNULL(ssd.matchedtext,ssd.asrtext)
        <if test="statisticaltype != null and statisticaltype == 2 and deptcode != null and deptcode != ''">
            AND ss.deptcode = #{deptcode}
        </if>
        <if test="statisticaltype != null and statisticaltype == 3 and taskId != null">
            AND ss.taskid = #{taskId}
        </if>
        <!-- 兼容不传 statisticaltype 时按单值条件过滤 -->
        <if test="statisticaltype == null">
            <if test="leavehospitaldistrictcode != null and leavehospitaldistrictcode != ''">
                AND ss.leavehospitaldistrictcode = #{leavehospitaldistrictcode}
            </if>
            <if test="deptcode != null and deptcode != ''">
                AND ss.deptcode = #{deptcode}
            </if>
            <if test="taskId != null">
                AND ss.taskid = #{taskId}
            </if>
        </if>
        <!-- 题目 + 选项匹配:只保留回答中选择了该选项的子任务 -->
        AND EXISTS (
            SELECT 1
            FROM service_subtask_detail ssd
            WHERE ssd.sub_id = ss.id
              AND ssd.del_flag = 0
            <if test="configValue != null and configValue != ''">
                AND ssd.categoryid IN (${configValue})
            </if>
            <choose>
                <when test="optionText != null">
                    AND IFNULL(ssd.matchedtext, ssd.asrtext) = #{optionText}
                </when>
                <otherwise>
                    AND 1 = 0
                </otherwise>
            </choose>
            <choose>
                <!-- 优先按题目内容匹配:同一道题复制到多个任务模板时 scriptid 不同,但 script_content 相同 -->
                <when test="scriptContent != null and scriptContent != ''">
                    AND ssd.scriptid IN (
                        <choose>
                            <when test="type == '1'">
                                SELECT its.id FROM ivr_task_template_script its
                                WHERE its.script_content = #{scriptContent}
                            </when>
                            <when test="type == '2'">
                                SELECT sts.id FROM svy_task_template_script sts
                                WHERE sts.script_content = #{scriptContent}
                            </when>
                            <otherwise>
                                SELECT its.id FROM ivr_task_template_script its
                                WHERE its.script_content = #{scriptContent}
                                UNION
                                SELECT sts.id FROM svy_task_template_script sts
                                WHERE sts.script_content = #{scriptContent}
                            </otherwise>
                        </choose>
                    )
                </when>
                <when test="scriptid != null">
                    AND ssd.scriptid = #{scriptid}
                </when>
                <otherwise>
                    AND 1 = 0
                </otherwise>
            </choose>
        )
        ORDER BY ss.endtime DESC, ss.id DESC
        <if test="pageSize != null and pageSize > 0">
            LIMIT #{pageSize}
            <if test="pageNum != null and pageNum >= 0">
                OFFSET #{pageNum}
            </if>
        </if>
    </select>
    <select id="getDataByTime" resultMap="ServiceSubtaskResult">
@@ -2279,7 +3340,7 @@
               recordid,
               exrecallcount,
               pulltime,
               deptcode,
               leaveldeptcode,
               labelstatus,
               del_flag,
               upid,
@@ -2309,7 +3370,7 @@
               admindate,
               patid,
               service_type,
               deptname,
               leaveldeptname,
               score,
               drname,
               drcode,
@@ -2321,6 +3382,7 @@
               nurse_id,
               nurse_name,
               remark,
               doctor_opinion,
               inhospid,
               patfrom,
               task_guid,
@@ -2368,7 +3430,7 @@
    </select>
    <select id="getDeptRanking" parameterType="com.ruoyi.common.core.service.dto.PatMedReq" resultType="com.ruoyi.common.core.service.dto.PatMedRes">
        SELECT deptname,
        SELECT leaveldeptname AS deptname,
        count(1) AS rc
        FROM service_subtask
        WHERE DATE_FORMAT(visit_time, '%Y-%m-%d') >= #{startDate}
@@ -2381,7 +3443,7 @@
        <if test="campusid != null and campusid != ''">
            AND campusid = #{campusid}
        </if>
        GROUP BY deptname
        GROUP BY leaveldeptname
    </select>
    <!-- 服务统计查询 - 按时间维度统计出院/门诊随访数据 -->
@@ -2432,7 +3494,7 @@
            AND campusid = #{campusid}
        </if>
        <if test="deptcodes != null and deptcodes.size() > 0">
            AND deptcode IN
            AND leaveldeptcode IN
            <foreach collection="deptcodes" item="deptcode" open="(" separator="," close=")">
                #{deptcode}
            </foreach>
@@ -2563,11 +3625,12 @@
        SUM(CASE WHEN sendstate = 1 THEN 1 ELSE 0 END) AS blq,
        SUM(CASE WHEN sendstate = 4 THEN 1 ELSE 0 END) AS wxsf,
        SUM(CASE WHEN sendstate != 4 THEN 1 ELSE 0 END) AS xsf,
        SUM(CASE WHEN (sendstate = 1 OR sendstate = 2 OR sendstate = 3 OR sendstate = 5 OR sendstate = 7 ) THEN 1 ELSE 0
        SUM(CASE WHEN (sendstate = 1 OR sendstate = 2 OR sendstate = 3 OR sendstate = 7 ) THEN 1 ELSE 0
        END) AS dsf,
        SUM(CASE WHEN sendstate != 2 THEN 1 ELSE 0 END) AS yfs,
        SUM(CASE WHEN sendstate = 2 THEN 1 ELSE 0 END) AS wfs,
        SUM(CASE WHEN sendstate = 6 THEN 1 ELSE 0 END) AS ywc,
        SUM(CASE WHEN sendstate = 7 THEN 1 ELSE 0 END) AS cs,
        SUM(CASE WHEN excep IS NOT NULL AND excep = '1' THEN 1 ELSE 0 END) AS yc,
        SUM(CASE WHEN excep IS NOT NULL AND excep = '2' THEN 1 ELSE 0 END) AS jg,
        COUNT(1) as total
@@ -2581,46 +3644,24 @@
            AND phone = #{phone}
        </if>
        <if test="finishtimeStart != null">
            AND date_format(finishtime,'%y%m%d') &gt;= date_format(#{finishtimeStart},'%y%m%d')
            AND finishtime &gt;= DATE(#{finishtimeStart})
        </if>
        <if test="finishtimeEnd != null">
            AND date_format(finishtime,'%y%m%d') &lt;= date_format(#{finishtimeEnd},'%y%m%d')
            AND finishtime &lt; DATE_ADD(DATE(#{finishtimeEnd}), INTERVAL 1 DAY)
        </if>
        <if test="deptOrDistrict==1">
            <if test="leavehospitaldistrictcodes != null and leavehospitaldistrictcodes.size() > 0">
                AND leavehospitaldistrictcode IN
                <foreach collection="leavehospitaldistrictcodes" item="leavehospitaldistrictcode" open="("
                         separator=","
                         close=")">
                    #{leavehospitaldistrictcode}
                </foreach>
            </if>
            <if test=" leaveldeptcodes != null and leaveldeptcodes.size()>0">
                AND deptcode IN
                <foreach collection="leaveldeptcodes" item="leaveldeptcode" open="(" separator=","
                         close=")">
                    #{leaveldeptcode}
                </foreach>
            </if>
        <!--
            科室/病区筛选(2026-11 修订):
              离院组(leaveldeptcodes / leavehospitaldistrictcodes)保持原逻辑;
              在院组(deptcodes / hospitaldistrictcodes → deptcode / hospitaldistrictcode)是新增的一组;
              组内按 deptOrDistrict 取 AND('1') / OR('2'),两组之间 AND(只传一组时另一组不拼)。
        -->
        <if test='deptOrDistrict == "1"'>
            <include refid="filterLeaveDimAnd"/>
            <include refid="filterInhospDimAnd"/>
        </if>
        <if test="deptOrDistrict==2">
            AND (1=1
            <if test="leavehospitaldistrictcodes != null and leavehospitaldistrictcodes.size() > 0">
                AND leavehospitaldistrictcode IN
                <foreach collection="leavehospitaldistrictcodes" item="leavehospitaldistrictcode" open="("
                         separator=","
                         close=")">
                    #{leavehospitaldistrictcode}
                </foreach>
            </if>
            <if test=" leaveldeptcodes != null and leaveldeptcodes.size()>0">
                OR deptcode IN
                <foreach collection="leaveldeptcodes" item="leaveldeptcode" open="(" separator=","
                         close=")">
                    #{leaveldeptcode}
                </foreach>
            </if>
            )
        <if test='deptOrDistrict == "2"'>
            <include refid="filterLeaveDimOr"/>
            <include refid="filterInhospDimOr"/>
        </if>
        <if test="visitCount != null and visitCount > 1 and visitDeptCodes != null and visitDeptCodes.size() > 0">
            AND visit_dept_code IN
@@ -2694,37 +3735,24 @@
            AND templatename LIKE CONCAT('%', #{templatename}, '%')
        </if>
        <if test="startOutHospTime != null">
            AND DATE_FORMAT(endtime,'%y%m%d') &gt;= DATE_FORMAT(#{startOutHospTime},'%y%m%d')
            AND endtime &gt;= DATE(#{startOutHospTime})
        </if>
        <if test="endOutHospTime != null">
            AND DATE_FORMAT(endtime,'%y%m%d') &lt;= DATE_FORMAT(#{endOutHospTime},'%y%m%d')
            AND endtime &lt; DATE_ADD(DATE(#{endOutHospTime}), INTERVAL 1 DAY)
        </if>
        <if test="startSendDateTime != null">
            AND DATE_FORMAT(visit_time,'%y%m%d') &gt;= DATE_FORMAT(#{startSendDateTime},'%y%m%d')
            AND visit_time &gt;= DATE(#{startSendDateTime})
        </if>
        <if test="endSendDateTime != null">
            AND DATE_FORMAT(visit_time,'%y%m%d') &lt;= DATE_FORMAT(#{endSendDateTime},'%y%m%d')
            AND visit_time &lt; DATE_ADD(DATE(#{endSendDateTime}), INTERVAL 1 DAY)
        </if>
        <if test="longSendTime != null">
            AND DATE_FORMAT(long_send_time,'%y%m%d') &lt;= DATE_FORMAT(#{longSendTime},'%y%m%d')
            AND long_send_time &lt; DATE_ADD(DATE(#{longSendTime}), INTERVAL 1 DAY)
        </if>
        <if test="deptOrDistrict == 1">
            <if test="leavehospitaldistrictcodes != null and leavehospitaldistrictcodes.size() > 0">
                AND leavehospitaldistrictcode IN
                <foreach collection="leavehospitaldistrictcodes" item="leavehospitaldistrictcode" open="("
                         separator="," close=")">
                    #{leavehospitaldistrictcode}
                </foreach>
            </if>
            <if test="leaveldeptcodes != null and leaveldeptcodes.size() > 0">
                AND deptcode IN
                <foreach collection="leaveldeptcodes" item="leaveldeptcode" open="(" separator="," close=")">
                    #{leaveldeptcode}
                </foreach>
            </if>
        </if>
        <if test="deptcode != null and deptcode != ''">
            AND deptcode = #{deptcode}
        <!-- 2026-11:这里原本还有一份与上方 deptOrDistrict==1 完全重复的病区/科室条件,
             已删除(条件改用 filterSubtaskWardDim / filterSubtaskDeptDim 片段,避免两份口径不一致) -->
        <if test="leaveldeptcode != null and leaveldeptcode != ''">
            AND leaveldeptcode = #{leaveldeptcode}
        </if>
        <if test="sendstate != null">
            AND sendstate = #{sendstate}
@@ -2734,6 +3762,9 @@
            <foreach collection="sendstates" item="state" open="(" separator="," close=")">
                #{state}
            </foreach>
        </if>
        <if test="currentPreachforms != null and currentPreachforms != ''">
            AND FIND_IN_SET(<include refid="effectiveCurrentPreachformExpr"/>, #{currentPreachforms})
        </if>
        <if test="orgid != null and orgid != ''">
            AND orgid = #{orgid}
@@ -2784,11 +3815,53 @@
            AND date_format( visit_time, '%y%m%d' ) &lt;= date_format(#{endDate},'%y%m%d')
        </if>
        <if test="deptcodeList != null and deptcodeList.size()>0">
            and deptcode in
            and leaveldeptcode in
            <foreach collection="deptcodeList" item="deptcode" open="(" separator="," close=")">
                #{deptcode}
            </foreach>
        </if>
    </select>
    <!--
        当前登录人「待处理」随访任务,按出院年月(endtime)分组统计,出院随访 / 门诊随访分开计数。
        口径(与随访列表「待随访」页签一致):
          1) 出院随访 service_type = '2',门诊随访 service_type = '3'
          2) 统计状态 → sendstate IN (1,2,3,7):1 被领取 / 2 待发送 / 3 已发送 / 7 超时
             (不含 4 不执行、5 发送失败、6 已完成)
          3) 出院满 7 天 → endtime < (今天 - 7 天) + 1 天,
             即「7 天前那一天」出院的也计入(用 <= DATE_SUB(...) 只会截到当天 0 点,会漏掉 9 条)
          4) 只看最近 4 个出院月份(含当月)→ endtime >= 当月往前推 3 个月的 1 号
          5) 科室/病区权限用 OR(与列表 deptOrDistrict=2 一致):
             只有病区权限的用户,任务 leaveldeptcode 与本人科室对不上时也能量到,
             否则会把整月数据过滤成 0。
        说明:GROUP BY 只产出有数据的月份,前端按「当月 + 前 3 个月」补齐,没数据就显示 0 个。
              不再限制 current_preachform(人工):该字段只是执行方式,
              待随访任务里还有短信等方式,限定 '1' 会漏数。
    -->
    <select id="countPendingManualTaskByMonth"
            parameterType="com.ruoyi.task.domain.entity.ServiceSubtaskEntity"
            resultType="java.util.Map">
        SELECT DATE_FORMAT(endtime, '%Y-%m') AS month,
        SUM(CASE WHEN service_type = '2' THEN 1 ELSE 0 END) AS dischargeCnt,
        SUM(CASE WHEN service_type = '3' THEN 1 ELSE 0 END) AS outpatientCnt
        FROM service_subtask
        WHERE del_flag = '0'
        AND service_type IN ('2', '3')
        <!-- 浮窗「待处理随访任务」口径:1 被领取 / 2 待发送 / 3 已发送 / 7 超时 -->
        AND sendstate IN (1, 2, 3, 7)
        AND endtime IS NOT NULL
        AND endtime &lt; DATE_ADD(DATE_SUB(CURDATE(), INTERVAL 7 DAY), INTERVAL 1 DAY)
        AND endtime &gt;= DATE_FORMAT(DATE_SUB(CURDATE(), INTERVAL 3 MONTH), '%Y-%m-01')
        <if test="orgid != null and orgid != ''">
            AND orgid = #{orgid}
        </if>
        <if test="campusid != null and campusid != ''">
            AND campusid = #{campusid}
        </if>
        <!-- 离院科室/病区:有科室按科室、有病区按病区,两者都有时取 OR -->
        <include refid="filterLeaveDimOr"/>
        GROUP BY DATE_FORMAT(endtime, '%Y-%m')
        ORDER BY month
    </select>
    <select id="getCurrentUserServiceSubtaskCount" parameterType="com.ruoyi.task.domain.entity.ServiceSubtaskEntity"
@@ -2799,32 +3872,74 @@
        COUNT(CASE WHEN sendstate = 5 THEN 1 END) AS failedVisitCount,
        SUM(CASE WHEN excep IS NOT NULL AND excep = '1' THEN 1 ELSE 0 END) AS abnormalVisitCount,
        SUM(CASE WHEN excep IS NOT NULL AND excep = '2' THEN 1 ELSE 0 END) AS warnningVisitCount,
        COUNT(*) AS allVisitCount
        FROM service_subtask
        WHERE del_flag = '0'
        AND service_type = '2'
        AND visit_count = 1
        COUNT(*) AS allVisitCount,
        <choose>
            <when test="soonTimeoutHours != null and soonTimeoutHours > 0">
                COUNT(CASE WHEN t.soon_deadline &gt; NOW()
                AND t.soon_deadline &lt;= DATE_ADD(NOW(), INTERVAL #{soonTimeoutHours} HOUR)
                THEN 1 END) AS soonTimeoutCount
            </when>
            <otherwise>
                0 AS soonTimeoutCount
            </otherwise>
        </choose>
        FROM (
        SELECT ss.*,
        <!-- 即将超时截止时间 = 出院时间(endtime) + 发送日(send_day, 缺省1) + 补偿天数(compensate_time 求和后再 +1 天),
             与 RyTask/RedisMqReceiver 的 isSubtaskDeadline 口径保持一致;
             无有效补偿记录(cp.subid 为空)时截止时间为 NULL,不参与即将超时统计 -->
        CASE
        WHEN cp.subid IS NULL THEN NULL
        ELSE DATE_ADD(
        DATE_ADD(ss.endtime, INTERVAL COALESCE(st.send_day, 1) DAY),
        INTERVAL (COALESCE(cp.compensate_days, 0) + 1) DAY
        )
        END AS soon_deadline
        FROM service_subtask ss
        LEFT JOIN service_task st ON st.taskid = ss.taskid AND st.del_flag = '0'
        <!-- 补偿时间:先按子任务聚合成一行再关联,避免一对多放大计数;
             compensate_time 是 varchar 且可能存成空串,需先置空再转数字 -->
        LEFT JOIN (
        SELECT subid,
        SUM(CAST(COALESCE(NULLIF(TRIM(compensate_time), ''), '0') AS SIGNED)) AS compensate_days
        FROM service_subtask_preachform
        WHERE del_flag = 0
        GROUP BY subid
        ) cp ON cp.subid = ss.id
        WHERE ss.del_flag = '0'
        AND ss.service_type = '2'
        AND ss.visit_count = 1
        <if test="orgid != null and orgid != ''">
            AND orgid = #{orgid}
            AND ss.orgid = #{orgid}
        </if>
        <if test="campusid != null and campusid != ''">
            and campusid = #{campusid}
            and ss.campusid = #{campusid}
        </if>
        <if test="leavehospitaldistrictcodes != null and leavehospitaldistrictcodes.size() > 0">
            AND leavehospitaldistrictcode IN
            <foreach collection="leavehospitaldistrictcodes" item="leavehospitaldistrictcode" open="("
                     separator=","
                     close=")">
                #{leavehospitaldistrictcode}
            </foreach>
        <!-- 离院科室/病区:有科室按科室、有病区按病区,两者都有时取 OR(原写法 1=1 会让筛选失效) -->
        <if test="(leavehospitaldistrictcodes != null and leavehospitaldistrictcodes.size() > 0)
                or (leaveldeptcodes != null and leaveldeptcodes.size() > 0)">
            AND (
            <if test="leavehospitaldistrictcodes != null and leavehospitaldistrictcodes.size() > 0">
                ss.leavehospitaldistrictcode IN
                <foreach collection="leavehospitaldistrictcodes" item="leavehospitaldistrictcode" open="("
                         separator=","
                         close=")">
                    #{leavehospitaldistrictcode}
                </foreach>
            </if>
            <if test="(leavehospitaldistrictcodes != null and leavehospitaldistrictcodes.size() > 0)
                and (leaveldeptcodes != null and leaveldeptcodes.size() > 0)">
                OR
            </if>
            <if test=" leaveldeptcodes != null and leaveldeptcodes.size()>0">
                ss.leaveldeptcode IN
                <foreach collection="leaveldeptcodes" item="leaveldeptcode" open="(" separator=","
                         close=")">
                    #{leaveldeptcode}
                </foreach>
            </if>)
        </if>
        <if test=" leaveldeptcodes != null and leaveldeptcodes.size()>0">
            AND deptcode IN
            <foreach collection="leaveldeptcodes" item="leaveldeptcode" open="(" separator=","
                     close=")">
                #{leaveldeptcode}
            </foreach>
        </if>
        ) t
    </select>
            <!--
@@ -2867,14 +3982,14 @@
            parameterType="com.ruoyi.task.domain.VO.ServiceSubtaskCotinueCountVO"
            resultType="java.util.Map">
        SELECT
        deptcode AS groupCode,
        deptname AS groupName,
        leaveldeptcode AS groupCode,
        leaveldeptname AS groupName,
        SUM(CASE WHEN continue_flag = 1 THEN 1 ELSE 0 END) AS noContinueCnt,
        SUM(CASE WHEN continue_flag = 2 THEN 1 ELSE 0 END) AS continueCnt
        FROM service_subtask
        WHERE del_flag = 0
        AND continue_flag IN (1, 2)
        AND deptcode IS NOT NULL AND deptcode != ''
        AND leaveldeptcode IS NOT NULL AND leaveldeptcode != ''
        <if test="orgid != null and orgid != ''">
            AND orgid = #{orgid}
        </if>
@@ -2882,12 +3997,12 @@
            AND campusid = #{campusid}
        </if>
        <if test="deptcodes != null and deptcodes.size() > 0">
            AND deptcode IN
            AND leaveldeptcode IN
            <foreach collection="deptcodes" item="code" open="(" separator="," close=")">
                #{code}
            </foreach>
        </if>
        GROUP BY deptcode, deptname
        GROUP BY leaveldeptcode, leaveldeptname
        ORDER BY groupCode
    </select>
@@ -2900,8 +4015,8 @@
                leavehospitaldistrictname AS groupName,
            </when>
            <otherwise>
                deptcode AS groupCode,
                deptname AS groupName,
                leaveldeptcode AS groupCode,
                leaveldeptname AS groupName,
            </otherwise>
        </choose>
        COUNT(*) AS totalCount,
@@ -2938,12 +4053,12 @@
            </when>
            <otherwise>
                <if test="deptcodes != null and deptcodes.size() > 0">
                    AND deptcode IN
                    AND leaveldeptcode IN
                    <foreach collection="deptcodes" item="code" open="(" separator="," close=")">
                        #{code}
                    </foreach>
                </if>
                GROUP BY deptcode, deptname
                GROUP BY leaveldeptcode, leaveldeptname
                ORDER BY groupCode
            </otherwise>
        </choose>
@@ -2979,8 +4094,8 @@
        drname,
        leavehospitaldistrictcode,
        leavehospitaldistrictname,
        deptcode,
        deptname,
        leaveldeptcode,
        leaveldeptname,
        sendstate,
        preachform,
        current_preachform,
@@ -3008,7 +4123,8 @@
        continue_count,
        continue_time_next,
        task_situation,
        remark
        remark,
        doctor_opinion
        from service_subtask
        where 1=1
        and del_flag = 0
@@ -3033,7 +4149,7 @@
            </foreach>
        </if>
        <if test="deptcodes != null and deptcodes.size() > 0">
            AND deptcode IN
            AND leaveldeptcode IN
            <foreach collection="deptcodes" item="deptcode" open="(" separator=","
                     close=")">
                #{deptcode}