-- ============================================================================= -- /smartor/serviceSubtask/getSfStatistics 的 SQL 版(病区维度 · 全部病区 · 出院随访 serviceType=2) -- ============================================================================= -- 对应代码: -- ServiceSubtaskController#getSfStatistics -- → ServiceSubtaskServiceImpl#getSfStatistics (分组 + 逐条累加) -- → ServiceSubtaskMapper.getSfStatistics / getSfStatisticsGroupKey -- → ServiceSubtaskMapper.selectTimelyRateBatch (及时率 rate) -- 数据源:service_subtask(本接口只查这一张表) -- -- 入参:只有 hospitalStartTime(出院时间下限)。 -- 分组:statisticaltype=1(病区),且不传 leavehospitaldistrictcodes → 全部病区。 -- 服务类型:serviceType = 2(出院随访)。 -- 说明:接口 Mapper 里出院时间条件是「hospitalStartTime 与 hospitalEndTime 都传才生效」, -- 只传 hospitalStartTime 时接口其实【完全不过滤出院时间】。下面按你要的语义实现: -- 出院时间 >= hospitalStartTime(上限可选)。 -- -- 已在 MySQL 5.7.34(smartor_sltd)实跑验证: -- hospitalStartTime = '2026-10-01' → 45 个病区 / 3088 条出院记录, -- 抽查病区 40003052:287 条、无需随访 4、应随访 283、首次随访成功 250、随访率 88.34%,与明细逐条核对一致。 -- ============================================================================= -- ----------------------------------------------------------------------------- -- PART 0. 参数(只有第一个是必填) -- ----------------------------------------------------------------------------- SET @hospitalStartTime = '2026-10-01'; -- 必填:出院时间下限(当天 0 点起) SET @hospitalEndTime = NULL; -- 可选:出院时间上限,NULL = 不限 SET @followUpCountStyle = '1'; -- 系统配置 followUpCountStyle;本库未配置该参数,接口按 '1' 处理 SET @orgid = NULL; -- 可选:机构;NULL = 不限(接口里由登录信息注入) SET @campusid = NULL; -- 可选:院区;NULL = 不限 -- ----------------------------------------------------------------------------- -- PART 1. 主查询:一行 = 一个出院病区,覆盖全部病区 -- · 内层 r :一行 = 一条随访子任务,把「统计口径」翻译成 0/1 标记 -- · 中间 t :按病区把标记累加 -- · 最外层 :把累加值算成接口返回的比率字符串 -- 想改统计维度:把 GROUP BY r.ward_code 换成 r.dept_code(科室)或 r.dr_code(医生)即可。 -- 想要总计行:把「GROUP BY r.ward_code」改成「GROUP BY r.ward_code WITH ROLLUP」, -- 最后一行 ward_code 为 NULL 的就是全部病区合计。 -- ----------------------------------------------------------------------------- SELECT t.leavehospitaldistrictcode, t.leavehospitaldistrictname, t.deptcode, t.deptname, t.drcode, t.drname, t.dischargeCount, -- 出院人次 t.nonFollowUp, -- 无需随访人次 t.filterCount, -- 过滤患者数量 t.followUpNeeded, -- 应随访人次 t.needFollowUp, -- 首次应随访 t.nonFollowUpFirst, -- 首次无需随访 t.pendingFollowUp, -- 首次待随访(本接口此路径恒为 0,见 PART 5) t.followUpSuccess, -- 首次随访成功 t.followUpFail, -- 首次随访失败(含人工超时) t.manual, -- 首次人工随访 t.voice, -- 首次语音随访 t.sms, -- 首次短信随访 t.weChat, -- 首次微信随访 t.manualSuccess, -- 首次人工随访成功 t.voiceSuccess, -- 首次语音随访成功 t.smsSuccess, -- 首次短信随访成功 t.weChatSuccess, -- 首次微信随访成功 -- 首次随访率 =(首次随访成功 + 首次随访失败)/ 首次应随访 IF(t.needFollowUp = 0, '0.00%', CONCAT(ROUND(100 * (t.followUpSuccess + t.followUpFail) / t.needFollowUp, 2), '%')) AS followUpRate, t.abnormal, -- 结果异常(首次) t.needFollowUpAgain, -- 二次应随访 t.nonFollowUpAgain, -- 二次无需随访 t.pendingFollowUpAgain, -- 二次待随访 t.followUpSuccessAgain, -- 二次随访成功 t.followUpFailAgain, -- 二次随访失败 t.manualAgain, t.voiceAgain, t.smsAgain, t.weChatAgain, t.manualAgainSuccess, t.voiceAgainSuccess, t.smsAgainSuccess, t.weChatAgainSuccess, -- 二次随访率 IF(t.needFollowUpAgain = 0, '0.00%', CONCAT(ROUND(100 * (t.followUpSuccessAgain + t.followUpFailAgain) / t.needFollowUpAgain, 2), '%')) AS followUpRateAgain, t.abnormalAgain, -- 结果异常(再次) t.taskSituation1, -- 随访情况:正常语音 t.taskSituation2, -- 随访情况:患者拒接/拒访 t.taskSituation3, -- 随访情况:面访/接诊 t.taskSituation4, -- 随访情况:微信随访 t.taskSituation5, -- 随访情况:随访电话不正确 t.taskSituation6 -- 随访情况:其他(task_situation = 7) FROM ( SELECT r.ward_code AS leavehospitaldistrictcode, MAX(r.ward_name) AS leavehospitaldistrictname, MAX(r.dept_code) AS deptcode, MAX(r.dept_name) AS deptname, MAX(r.dr_code) AS drcode, MAX(r.dr_name) AS drname, COUNT(*) AS dischargeCount, SUM(r.f_no) AS nonFollowUp, SUM(r.f_filter) AS filterCount, SUM(r.f_need) AS followUpNeeded, SUM(IF(r.f_first = 1 AND r.f_need = 1, 1, 0)) AS needFollowUp, -- followUpCountStyle='1' 时接口把「首次无需随访」加了两次(代码 L2195-2197 与 L2211-2213),这里 *2 复刻 SUM(IF(r.f_first = 1 AND r.f_no = 1, 1, 0)) * IF(@followUpCountStyle = '1', 2, 1) AS nonFollowUpFirst, 0 AS pendingFollowUp, SUM(IF(r.f_first = 1 AND r.f_succ_first = 1, 1, 0)) AS followUpSuccess, SUM(IF(r.f_first = 1 AND r.f_fail_first = 1, 1, 0)) AS followUpFail, SUM(IF(r.f_first = 1 AND r.ch_hit = 1 AND r.cp = '1', 1, 0)) AS manual, SUM(IF(r.f_first = 1 AND r.ch_hit = 1 AND r.cp = '3', 1, 0)) AS voice, SUM(IF(r.f_first = 1 AND r.ch_hit = 1 AND r.cp = '4', 1, 0)) AS sms, SUM(IF(r.f_first = 1 AND r.ch_hit = 1 AND r.cp = '5', 1, 0)) AS weChat, SUM(IF(r.f_first = 1 AND r.sendstate = 6 AND r.cp = '1', 1, 0)) AS manualSuccess, SUM(IF(r.f_first = 1 AND r.sendstate = 6 AND r.cp = '3', 1, 0)) AS voiceSuccess, SUM(IF(r.f_first = 1 AND r.sendstate = 6 AND r.cp = '4', 1, 0)) AS smsSuccess, SUM(IF(r.f_first = 1 AND r.sendstate = 6 AND r.cp = '5', 1, 0)) AS weChatSuccess, SUM(IF(r.f_first = 1 AND r.f_abnormal = 1, 1, 0)) AS abnormal, SUM(IF(r.f_again = 1 AND r.f_need = 1, 1, 0)) AS needFollowUpAgain, SUM(IF(r.f_again = 1 AND r.f_no = 1, 1, 0)) AS nonFollowUpAgain, SUM(IF(r.f_again = 1 AND r.sendstate = 2, 1, 0)) AS pendingFollowUpAgain, SUM(IF(r.f_again = 1 AND r.f_succ_again = 1, 1, 0)) AS followUpSuccessAgain, SUM(IF(r.f_again = 1 AND r.f_fail_again = 1, 1, 0)) AS followUpFailAgain, SUM(IF(r.f_again = 1 AND r.ch_hit = 1 AND r.cp = '1', 1, 0)) AS manualAgain, SUM(IF(r.f_again = 1 AND r.ch_hit = 1 AND r.cp = '3', 1, 0)) AS voiceAgain, SUM(IF(r.f_again = 1 AND r.ch_hit = 1 AND r.cp = '4', 1, 0)) AS smsAgain, SUM(IF(r.f_again = 1 AND r.ch_hit = 1 AND r.cp = '5', 1, 0)) AS weChatAgain, SUM(IF(r.f_again = 1 AND r.sendstate = 6 AND r.cp = '1', 1, 0)) AS manualAgainSuccess, SUM(IF(r.f_again = 1 AND r.sendstate = 6 AND r.cp = '3', 1, 0)) AS voiceAgainSuccess, SUM(IF(r.f_again = 1 AND r.sendstate = 6 AND r.cp = '4', 1, 0)) AS smsAgainSuccess, SUM(IF(r.f_again = 1 AND r.sendstate = 6 AND r.cp = '5', 1, 0)) AS weChatAgainSuccess, SUM(IF(r.f_again = 1 AND r.f_abnormal = 1, 1, 0)) AS abnormalAgain, SUM(IF(r.task_situation = 1, 1, 0)) AS taskSituation1, SUM(IF(r.task_situation = 2, 1, 0)) AS taskSituation2, SUM(IF(r.task_situation = 3, 1, 0)) AS taskSituation3, SUM(IF(r.task_situation = 4, 1, 0)) AS taskSituation4, SUM(IF(r.task_situation = 5, 1, 0)) AS taskSituation5, SUM(IF(r.task_situation = 7, 1, 0)) AS taskSituation6 FROM ( SELECT ss.leavehospitaldistrictcode AS ward_code, ss.leavehospitaldistrictname AS ward_name, ss.leaveldeptcode AS dept_code, ss.leaveldeptname AS dept_name, ss.drcode AS dr_code, ss.drname AS dr_name, ss.sendstate AS sendstate, ss.current_preachform AS cp, ss.task_situation AS task_situation, IF(ss.sendstate = 4, 1, 0) AS f_no, IF(ss.sendstate = 4 AND ss.task_situation = 6, 1, 0) AS f_filter, IF(ss.sendstate IS NOT NULL AND ss.sendstate <> 4, 1, 0) AS f_need, IF(ss.visit_count = 1, 1, 0) AS f_first, IF(ss.visit_count > 1, 1, 0) AS f_again, -- 首次随访成功:统计方式 1/3 只算已完成(6);其它方式算上不执行/已发送(3,4,6) IF(@followUpCountStyle IN ('1', '3'), IF(ss.sendstate = 6, 1, 0), IF(ss.sendstate IN (3, 4, 6), 1, 0)) AS f_succ_first, -- 再次随访成功:统计方式 1/3 只算已完成(6);其它方式算上待随访失败前的 1 等(1,3,4,6) IF(@followUpCountStyle IN ('1', '3'), IF(ss.sendstate = 6, 1, 0), IF(ss.sendstate IN (1, 3, 4, 6), 1, 0)) AS f_succ_again, IF(ss.sendstate IN (5, 7), 1, 0) AS f_fail_first, IF(ss.sendstate = 5, 1, 0) AS f_fail_again, IF(ss.excep = '1', 1, 0) AS f_abnormal, -- 随访方式命中条件:方式 1 排除「无需随访」;方式 3 只算已完成;其它方式不设限 IF(@followUpCountStyle = '1', IF(ss.sendstate IS NOT NULL AND ss.sendstate <> 4, 1, 0), IF(@followUpCountStyle = '3', IF(ss.sendstate = 6, 1, 0), IF(ss.sendstate IS NOT NULL, 1, 0))) AS ch_hit FROM service_subtask ss WHERE ss.del_flag = 0 AND ss.service_type = '2' -- serviceType = 2 出院随访 -- 接口里 groupKeyList 会带全量病区码,等价于「出院病区为 NULL 的行被丢掉」; -- 若想把空病区也统计进来,注释掉下一行。 AND ss.leavehospitaldistrictcode IS NOT NULL AND ss.endtime >= DATE(@hospitalStartTime) AND (@hospitalEndTime IS NULL OR ss.endtime < DATE_ADD(DATE(@hospitalEndTime), INTERVAL 1 DAY)) AND (@orgid IS NULL OR ss.orgid = @orgid) AND (@campusid IS NULL OR ss.campusid = @campusid) ) r GROUP BY r.ward_code ) t ORDER BY t.dischargeCount DESC; -- ----------------------------------------------------------------------------- -- PART 1B. 只要「首次随访」的数据(等价于接口入参 visitCount = 1 → getStatisticFirst) -- · 只统计 visit_count = 1 的记录:二次随访记录不进入任何一列 -- · 列:出院人次 / 应随访拆解 / 随访成功拆解 / 各成功率 / 及时率 / 出院时间范围 -- · 口径与 getStatisticFirst 对齐:pendingFollowUp 是 sendstate = 2 的真实数量 -- -- ★ 对账:接口这几列本身不等于「应随访」,差额是两个真实存在的口径缺项,不是数据错: -- 首次应随访 = 待发送(1) + 待随访(2) + 已发送未完成(3) + 其它状态 + 随访成功(6) + 随访失败(5,7) -- 首次随访成功 = 人工成功 + 语音成功 + 短信成功 + 微信成功 + 无方式成功(sendstate=6 但 current_preachform 为空) -- 接口只统计 2 / 6 / 5+7,所以「待随访 + 成功 + 失败」少于应随访; -- 渠道成功按 current_preachform 分,没有方式的成功记录不落任何渠道。 -- 下面把 notSent / sentNotDone / otherStatus / noChannelSuccess 显式列出来,并给出 diffNeed / diffSucc -- 两个自检列(正常恒为 0)。 -- -- ★ 出院时间:主查询给 endTimeMin/endTimeMax,及时率口径给 timelyEndMin/timelyEndMax。 -- ⚠ 及时率子查询里的出院时间条件当前是注释掉的(按你的版本),所以它统计的是该病区「全部历史」, -- 分母 timelyBase 会远大于应随访(实测某病区 643 条应随访 vs 2342 条及时率分母), -- timelyRate 与其它列不再同区间、不可直接比较。要恢复同区间就把那两行放开。 -- -- 主查询已实跑校验(2026-09-04 ~ 2026-11-03,orgid=20001001,45 个病区):7378 条 / 无需随访 428 / -- 应随访 6950 = 待发送 116 + 待随访 337 + 已发送未完成 941 + 成功 4188 + 失败 1368; -- 成功 4188 = 人工 2974 + 短信 1197 + 无方式 17(本库 current_preachform 只有 1 和 4,语音/微信为 0)。 -- ----------------------------------------------------------------------------- SELECT t.leavehospitaldistrictname, -- 出院病区 t.endTimeMin, -- 该病区命中的最早出院时间 t.endTimeMax, -- 该病区命中的最晚出院时间 t.deptname, -- 出院科室 t.dischargeCount, -- 出院人次(首次随访记录数) t.nonFollowUp, -- 无需随访人次 t.followUpNeeded, -- 应随访人次 t.needFollowUp, -- 首次应随访 t.notSent, -- 待发送(sendstate = 1) t.pendingFollowUp, -- 首次待随访(sendstate = 2) t.sentNotDone, -- 已发送未完成(sendstate = 3) t.otherStatus, -- 其它状态兜底(正常为 0) t.followUpSuccess, -- 首次随访成功(sendstate = 6) t.followUpFail, -- 首次随访失败(sendstate = 5,7,含人工超时) -- 各随访方式成功数(想同时看总数就放开下面 4 行,成功率分母就是这 4 个) -- t.manual, t.voice, t.sms, t.weChat, t.manualSuccess, -- 首次人工随访成功 t.voiceSuccess, -- 首次语音随访成功 t.smsSuccess, -- 首次短信随访成功 t.weChatSuccess, -- 首次微信随访成功 t.noChannelSuccess, -- 成功但没有随访方式(current_preachform 为空或不属于 1/3/4/5) -- 随访率 =(成功 + 失败)/ 应随访(接口 followUpRate 的口径) IF(t.needFollowUp = 0, '0.00%', CONCAT(ROUND(100 * (t.followUpSuccess + t.followUpFail) / t.needFollowUp, 2), '%')) AS followUpRate, -- 随访成功率 = 成功 / 应随访(只算成功,不含失败) IF(t.needFollowUp = 0, '0.00%', CONCAT(ROUND(100 * t.followUpSuccess / t.needFollowUp, 2), '%')) AS followUpSuccessRate, -- 各方式成功率 = 该方式成功 / 该方式命中(sendstate<>4 且 current_preachform 为该方式) IF(t.manual = 0, '0.00%', CONCAT(ROUND(100 * t.manualSuccess / t.manual, 2), '%')) AS manualSuccessRate, IF(t.voice = 0, '0.00%', CONCAT(ROUND(100 * t.voiceSuccess / t.voice, 2), '%')) AS voiceSuccessRate, IF(t.sms = 0, '0.00%', CONCAT(ROUND(100 * t.smsSuccess / t.sms, 2), '%')) AS smsSuccessRate, IF(t.weChat = 0, '0.00%', CONCAT(ROUND(100 * t.weChatSuccess / t.weChat, 2), '%')) AS weChatSuccessRate, -- 渠道合计成功率 = 四种方式成功之和 / 四种方式命中之和 IF(t.manual + t.voice + t.sms + t.weChat = 0, '0.00%', CONCAT(ROUND(100 * (t.manualSuccess + t.voiceSuccess + t.smsSuccess + t.weChatSuccess) / (t.manual + t.voice + t.sms + t.weChat), 2), '%')) AS channelSuccessRate, -- 及时率:口径与接口 selectTimelyRateBatch 相同(独立的随访时间窗口 + 排除「无需随访」), -- 及时 = finishtime 不为空 且 DATEDIFF(finishtime, endtime) <= 及时天数 + 补偿天数。 -- 及时天数阈值见下面 rt 子查询里的 7,按需修改(接口不传 rateDay 时返回 0)。 IFNULL(rt.timely_base, 0) AS timelyBase, -- 及时率分母(口径与「应随访」不同) IF(IFNULL(rt.timely_base, 0) = 0, '0.00%', CONCAT(ROUND(100 * rt.timely_cnt / rt.timely_base, 2), '%')) AS timelyRate, rt.timelyEndMin, -- 及时率口径命中的最早出院时间 rt.timelyEndMax, -- 及时率口径命中的最晚出院时间 -- 自检:两个差额,正常恒为 0 t.needFollowUp - (t.notSent + t.pendingFollowUp + t.sentNotDone + t.otherStatus + t.followUpSuccess + t.followUpFail) AS diffNeed, t.followUpSuccess - (t.manualSuccess + t.voiceSuccess + t.smsSuccess + t.weChatSuccess + t.noChannelSuccess) AS diffSucc -- 另外几列需要时放开: -- , t.abnormal -- , t.taskSituation1, t.taskSituation2, t.taskSituation3, t.taskSituation4, t.taskSituation5, t.taskSituation6 FROM ( SELECT r.ward_code AS leavehospitaldistrictcode, MAX(r.ward_name) AS leavehospitaldistrictname, MAX(r.dept_name) AS deptname, MIN(r.end_time) AS endTimeMin, MAX(r.end_time) AS endTimeMax, COUNT(*) AS dischargeCount, SUM(r.f_no) AS nonFollowUp, SUM(r.f_need) AS followUpNeeded, SUM(r.f_need) AS needFollowUp, SUM(IF(r.sendstate = 1, 1, 0)) AS notSent, SUM(IF(r.sendstate = 2, 1, 0)) AS pendingFollowUp, SUM(IF(r.sendstate = 3, 1, 0)) AS sentNotDone, SUM(IF(r.sendstate IS NOT NULL AND r.sendstate <> 4 AND r.sendstate NOT IN (1, 2, 3, 5, 6, 7), 1, 0)) AS otherStatus, SUM(r.f_succ) AS followUpSuccess, SUM(r.f_fail) AS followUpFail, SUM(IF(r.ch_hit = 1 AND r.cp = '1', 1, 0)) AS manual, SUM(IF(r.ch_hit = 1 AND r.cp = '3', 1, 0)) AS voice, SUM(IF(r.ch_hit = 1 AND r.cp = '4', 1, 0)) AS sms, SUM(IF(r.ch_hit = 1 AND r.cp = '5', 1, 0)) AS weChat, SUM(IF(r.sendstate = 6 AND r.cp = '1', 1, 0)) AS manualSuccess, SUM(IF(r.sendstate = 6 AND r.cp = '3', 1, 0)) AS voiceSuccess, SUM(IF(r.sendstate = 6 AND r.cp = '4', 1, 0)) AS smsSuccess, SUM(IF(r.sendstate = 6 AND r.cp = '5', 1, 0)) AS weChatSuccess, SUM(IF(r.sendstate = 6 AND (r.cp IS NULL OR r.cp NOT IN ('1','3','4','5')), 1, 0)) AS noChannelSuccess FROM ( SELECT ss.leavehospitaldistrictcode AS ward_code, ss.leavehospitaldistrictname AS ward_name, ss.leaveldeptname AS dept_name, ss.endtime AS end_time, ss.sendstate AS sendstate, ss.current_preachform AS cp, IF(ss.sendstate = 4, 1, 0) AS f_no, IF(ss.sendstate IS NOT NULL AND ss.sendstate <> 4, 1, 0) AS f_need, IF(ss.sendstate = 6, 1, 0) AS f_succ, IF(ss.sendstate IN (5, 7), 1, 0) AS f_fail, -- 随访方式命中条件:方式 1 排除「无需随访」;方式 3 只算已完成;其它方式不设限 IF(@followUpCountStyle = '1', IF(ss.sendstate IS NOT NULL AND ss.sendstate <> 4, 1, 0), IF(@followUpCountStyle = '3', IF(ss.sendstate = 6, 1, 0), IF(ss.sendstate IS NOT NULL, 1, 0))) AS ch_hit FROM service_subtask ss WHERE ss.del_flag = 0 AND ss.service_type = '2' AND ss.visit_count = 1 -- 只要首次随访 AND ss.leavehospitaldistrictcode IS NOT NULL AND ss.endtime >= DATE('2026-09-04 00:00:00') AND ss.endtime < DATE_ADD(DATE('2026-11-03 23:59:59'), INTERVAL 1 DAY) AND ss.orgid = '20001001' ) r GROUP BY r.ward_code ) t LEFT JOIN ( -- 及时率(selectTimelyRateBatch 的等价写法,按病区预聚合一次再关联,避免一对多放大分母) SELECT ss.leavehospitaldistrictcode AS ward_code, COUNT(*) AS timely_base, SUM(IF(ss.finishtime IS NOT NULL AND DATEDIFF(ss.finishtime, ss.endtime) <= 7 + IFNULL(cp.compensate_days, 0), 1, 0)) AS timely_cnt, MIN(ss.endtime) AS timelyEndMin, -- 这一口径实际覆盖到的出院时间范围 MAX(ss.endtime) AS timelyEndMax FROM service_subtask ss LEFT JOIN ( -- 补偿时间:varchar 且可能是空串,先按子任务聚合成一行 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 AND ss.leavehospitaldistrictcode IS NOT NULL AND ss.visit_time IS NOT NULL AND ss.sendstate <> 4 AND DATE(ss.visit_time) < DATE(NOW()) -- 接口未传 startTime/endTime 时的固定条件 -- 及时率要不要跟主查询同区间?放开下面两行即可(放开后 timelyBase 才会等于应随访) -- AND ss.endtime >= DATE('2026-09-04 00:00:00') -- AND ss.endtime < DATE_ADD(DATE('2026-11-03 23:59:59'), INTERVAL 1 DAY) AND ss.orgid = '20001001' GROUP BY ss.leavehospitaldistrictcode ) rt ON rt.ward_code = t.leavehospitaldistrictcode ORDER BY t.dischargeCount DESC; -- ----------------------------------------------------------------------------- -- PART 1C. 想看「出院时间」这一列:按 病区 + 出院日期(DATE(endtime)) 分组 -- 为什么不能只在 PART 1B 里 select ss.endtime 再 GROUP BY ward_code: -- · ONLY_FULL_GROUP_BY 下会报 1055; -- · 把带秒的 endtime 加进 GROUP BY = 一条记录一组,7378 条被拆成 7133 行,汇总没有意义; -- · 更坑的是及时率子查询仍按病区聚合,整病区的 timelyBase 会被重复挂到每一行 -- (实测 timelyBase 合计会变成 9,649,708)。这里 rt 也按「病区 + 出院日期」聚合, -- 所以每行的及时率和该行数据是同一个粒度。 -- 实测:1637 行 / dischargeCount 合计 7378 / timelyBase 合计 6950 / diffNeed、diffSucc 全 0。 -- ----------------------------------------------------------------------------- SELECT t.end_date AS 出院日期, -- 出院时间(按天) t.leavehospitaldistrictname AS 出院病区, t.deptname AS 出院科室, t.dischargeCount, t.nonFollowUp, t.followUpNeeded, t.needFollowUp, t.notSent, t.pendingFollowUp, t.sentNotDone, t.otherStatus, t.followUpSuccess, t.followUpFail, t.manualSuccess, t.voiceSuccess, t.smsSuccess, t.weChatSuccess, t.noChannelSuccess, IF(t.needFollowUp = 0, '0.00%', CONCAT(ROUND(100 * (t.followUpSuccess + t.followUpFail) / t.needFollowUp, 2), '%')) AS followUpRate, IF(t.needFollowUp = 0, '0.00%', CONCAT(ROUND(100 * t.followUpSuccess / t.needFollowUp, 2), '%')) AS followUpSuccessRate, IF(t.manual = 0, '0.00%', CONCAT(ROUND(100 * t.manualSuccess / t.manual, 2), '%')) AS manualSuccessRate, IF(t.voice = 0, '0.00%', CONCAT(ROUND(100 * t.voiceSuccess / t.voice, 2), '%')) AS voiceSuccessRate, IF(t.sms = 0, '0.00%', CONCAT(ROUND(100 * t.smsSuccess / t.sms, 2), '%')) AS smsSuccessRate, IF(t.weChat = 0, '0.00%', CONCAT(ROUND(100 * t.weChatSuccess / t.weChat, 2), '%')) AS weChatSuccessRate, IF(t.manual + t.voice + t.sms + t.weChat = 0, '0.00%', CONCAT(ROUND(100 * (t.manualSuccess + t.voiceSuccess + t.smsSuccess + t.weChatSuccess) / (t.manual + t.voice + t.sms + t.weChat), 2), '%')) AS channelSuccessRate, IFNULL(rt.timely_base, 0) AS timelyBase, IF(IFNULL(rt.timely_base, 0) = 0, '0.00%', CONCAT(ROUND(100 * rt.timely_cnt / rt.timely_base, 2), '%')) AS timelyRate, t.needFollowUp - (t.notSent + t.pendingFollowUp + t.sentNotDone + t.otherStatus + t.followUpSuccess + t.followUpFail) AS diffNeed, t.followUpSuccess - (t.manualSuccess + t.voiceSuccess + t.smsSuccess + t.weChatSuccess + t.noChannelSuccess) AS diffSucc FROM ( SELECT r.ward_code AS leavehospitaldistrictcode, r.end_date AS end_date, MAX(r.ward_name) AS leavehospitaldistrictname, MAX(r.dept_name) AS deptname, COUNT(*) AS dischargeCount, SUM(r.f_no) AS nonFollowUp, SUM(r.f_need) AS followUpNeeded, SUM(r.f_need) AS needFollowUp, SUM(IF(r.sendstate = 1, 1, 0)) AS notSent, SUM(IF(r.sendstate = 2, 1, 0)) AS pendingFollowUp, SUM(IF(r.sendstate = 3, 1, 0)) AS sentNotDone, SUM(IF(r.sendstate IS NOT NULL AND r.sendstate <> 4 AND r.sendstate NOT IN (1, 2, 3, 5, 6, 7), 1, 0)) AS otherStatus, SUM(r.f_succ) AS followUpSuccess, SUM(r.f_fail) AS followUpFail, SUM(IF(r.ch_hit = 1 AND r.cp = '1', 1, 0)) AS manual, SUM(IF(r.ch_hit = 1 AND r.cp = '3', 1, 0)) AS voice, SUM(IF(r.ch_hit = 1 AND r.cp = '4', 1, 0)) AS sms, SUM(IF(r.ch_hit = 1 AND r.cp = '5', 1, 0)) AS weChat, SUM(IF(r.sendstate = 6 AND r.cp = '1', 1, 0)) AS manualSuccess, SUM(IF(r.sendstate = 6 AND r.cp = '3', 1, 0)) AS voiceSuccess, SUM(IF(r.sendstate = 6 AND r.cp = '4', 1, 0)) AS smsSuccess, SUM(IF(r.sendstate = 6 AND r.cp = '5', 1, 0)) AS weChatSuccess, SUM(IF(r.sendstate = 6 AND (r.cp IS NULL OR r.cp NOT IN ('1','3','4','5')), 1, 0)) AS noChannelSuccess FROM ( SELECT ss.leavehospitaldistrictcode AS ward_code, DATE(ss.endtime) AS end_date, ss.leavehospitaldistrictname AS ward_name, ss.leaveldeptname AS dept_name, ss.sendstate AS sendstate, ss.current_preachform AS cp, IF(ss.sendstate = 4, 1, 0) AS f_no, IF(ss.sendstate IS NOT NULL AND ss.sendstate <> 4, 1, 0) AS f_need, IF(ss.sendstate = 6, 1, 0) AS f_succ, IF(ss.sendstate IN (5, 7), 1, 0) AS f_fail, IF(@followUpCountStyle = '1', IF(ss.sendstate IS NOT NULL AND ss.sendstate <> 4, 1, 0), IF(@followUpCountStyle = '3', IF(ss.sendstate = 6, 1, 0), IF(ss.sendstate IS NOT NULL, 1, 0))) AS ch_hit FROM service_subtask ss WHERE ss.del_flag = 0 AND ss.service_type = '2' AND ss.visit_count = 1 AND ss.leavehospitaldistrictcode IS NOT NULL AND ss.endtime >= DATE('2026-09-04 00:00:00') AND ss.endtime < DATE_ADD(DATE('2026-11-03 23:59:59'), INTERVAL 1 DAY) AND ss.orgid = '20001001' ) r GROUP BY r.ward_code, r.end_date ) t LEFT JOIN ( -- 及时率:同样按「病区 + 出院日期」聚合,才能和左边每一行对上 SELECT ss.leavehospitaldistrictcode AS ward_code, DATE(ss.endtime) AS end_date, COUNT(*) AS timely_base, SUM(IF(ss.finishtime IS NOT NULL AND DATEDIFF(ss.finishtime, ss.endtime) <= 7 + IFNULL(cp.compensate_days, 0), 1, 0)) AS timely_cnt FROM service_subtask ss 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 AND ss.leavehospitaldistrictcode IS NOT NULL AND ss.visit_time IS NOT NULL AND ss.sendstate <> 4 AND DATE(ss.visit_time) < DATE(NOW()) AND ss.orgid = '20001001' GROUP BY ss.leavehospitaldistrictcode, DATE(ss.endtime) ) rt ON rt.ward_code = t.leavehospitaldistrictcode AND rt.end_date = t.end_date ORDER BY t.leavehospitaldistrictname, t.end_date; -- ----------------------------------------------------------------------------- -- PART 1D. 明细版:一行 = 一条首次随访记录,直接带出院时间(要核对到人/到天就跑这个) -- ----------------------------------------------------------------------------- SELECT ss.endtime AS 出院时间, ss.leavehospitaldistrictname AS 出院病区, ss.leaveldeptname AS 出院科室, ss.sendname AS 患者, ss.sendstate AS 状态码, CASE ss.sendstate WHEN 1 THEN '待发送' WHEN 2 THEN '待随访' WHEN 3 THEN '已发送未完成' WHEN 4 THEN '无需随访' WHEN 5 THEN '随访失败' WHEN 6 THEN '随访成功' WHEN 7 THEN '失败/人工超时' ELSE '其它' END AS 随访状态, ss.current_preachform AS 随访方式, ss.visit_count AS 随访次数, ss.visit_time AS 随访时间, ss.finishtime AS 完成时间, IF(ss.sendstate = 4 OR ss.visit_time IS NULL, '-', IF(ss.finishtime IS NOT NULL AND DATEDIFF(ss.finishtime, ss.endtime) <= 7, '及时', '不及时')) AS 是否及时 FROM service_subtask ss WHERE ss.del_flag = 0 AND ss.service_type = '2' AND ss.visit_count = 1 AND ss.leavehospitaldistrictcode IS NOT NULL AND ss.endtime >= DATE('2026-09-04 00:00:00') AND ss.endtime < DATE_ADD(DATE('2026-11-03 23:59:59'), INTERVAL 1 DAY) AND ss.orgid = '20001001' ORDER BY ss.leavehospitaldistrictname, ss.endtime; -- ----------------------------------------------------------------------------- -- PART 2. 及时率 rate(接口只在请求里传了 rateDay 时才有值,不传返回 0.0) -- 口径与接口不同:这一块用的是「随访时间(visit_time)」窗口,分母是 visit_time 不为空且 -- 不是「无需随访」的记录数;出院时间只作为附加条件。 -- rate = 及时数 / 分母,及时 = finishtime 不为空 且 DATEDIFF(finishtime, endtime) <= rateDay + 补偿天数 -- ----------------------------------------------------------------------------- SET @rateDay = 7; -- 几天及时率;不想看就整段不跑 SELECT ss.leavehospitaldistrictcode AS leavehospitaldistrictcode, COUNT(*) AS base, -- 分母 SUM(IF(ss.finishtime IS NOT NULL AND DATEDIFF(ss.finishtime, ss.endtime) <= @rateDay + IFNULL(cp.compensate_days, 0), 1, 0)) AS timely, -- 及时数 ROUND(SUM(IF(ss.finishtime IS NOT NULL AND DATEDIFF(ss.finishtime, ss.endtime) <= @rateDay + IFNULL(cp.compensate_days, 0), 1, 0)) / COUNT(*), 4) AS rate FROM service_subtask ss 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_time IS NOT NULL AND ss.sendstate <> 4 AND DATE(ss.visit_time) < DATE(NOW()) -- 接口未传 startTime/endTime 时的固定条件 AND ss.endtime >= DATE(@hospitalStartTime) -- 等价于接口的 date_format(endtime,'%y%m%d') >= date_format(...) AND (@orgid IS NULL OR ss.orgid = @orgid) AND (@campusid IS NULL OR ss.campusid = @campusid) GROUP BY ss.leavehospitaldistrictcode ORDER BY base DESC; -- ----------------------------------------------------------------------------- -- PART 3. 想固定成「视图」的话用这个(MySQL 的视图不支持传入参数, -- 所以视图按「一条子任务一行」落明细 + 出院日期,参数过滤放在查询外层) -- 注意:视图里把 followUpCountStyle 固定成 '1'(本库未配置该参数,接口实际就走 '1')。 -- 若某机构配的是 '3' 或其它值,改视图里的 f_succ_first / f_succ_again / ch_hit 三处。 -- ----------------------------------------------------------------------------- CREATE OR REPLACE VIEW v_sf_statistic_flag AS SELECT ss.id, DATE(ss.endtime) AS stat_date, -- 出院日期(hospitalStartTime 过滤用) ss.leavehospitaldistrictcode AS ward_code, ss.leavehospitaldistrictname AS ward_name, ss.leaveldeptcode AS dept_code, ss.leaveldeptname AS dept_name, ss.drcode AS dr_code, ss.drname AS dr_name, ss.endtime AS end_time, ss.visit_time, ss.finishtime, ss.sendstate, ss.visit_count, ss.current_preachform AS cp, ss.task_situation, IF(ss.sendstate = 4, 1, 0) AS f_no, IF(ss.sendstate = 4 AND ss.task_situation = 6, 1, 0) AS f_filter, IF(ss.sendstate IS NOT NULL AND ss.sendstate <> 4, 1, 0) AS f_need, IF(ss.visit_count = 1, 1, 0) AS f_first, IF(ss.visit_count > 1, 1, 0) AS f_again, IF(ss.sendstate = 6, 1, 0) AS f_succ_first, -- style '1'/'3' IF(ss.sendstate = 6, 1, 0) AS f_succ_again, -- style '1'/'3' IF(ss.sendstate IN (5, 7), 1, 0) AS f_fail_first, IF(ss.sendstate = 5, 1, 0) AS f_fail_again, IF(ss.excep = '1', 1, 0) AS f_abnormal, IF(ss.sendstate IS NOT NULL AND ss.sendstate <> 4, 1, 0) AS ch_hit -- style '1' FROM service_subtask ss WHERE ss.del_flag = 0 AND ss.service_type = '2'; -- 建好视图后,把 PART 1 里内层 "FROM ( ... ) r" 整段换成下面这一行即可(其余不动): -- FROM v_sf_statistic_flag r -- WHERE r.stat_date >= DATE(@hospitalStartTime) -- AND (@hospitalEndTime IS NULL OR r.end_time < DATE_ADD(DATE(@hospitalEndTime), INTERVAL 1 DAY)) -- AND (@orgid IS NULL OR 1 = 1) -- 视图中未带 orgid/campusid,需要的话把它们也加进视图 -- 同时把中间层 nonFollowUpFirst 的 "* IF(@followUpCountStyle = '1', 2, 1)" 改成 "* 2"(视图里已是 style '1')。 -- ----------------------------------------------------------------------------- -- PART 4. 口径说明 / 与接口的差异 -- ----------------------------------------------------------------------------- -- 1) 接口返回里恒为 0、本次未输出的字段(代码这条路径没赋值): -- serviceCount、bzxCount、ysfCount、yfsCount、dfsCount、fssbCount、ywcCount、 -- dxfscs、dxsfwccs、dhfscs、dhsfwccs、yczs、hfycs、finishRate、joyCount、joyAllCount、joyTotal。 -- pendingFollowUp(首次待随访)同理:getStatistic 的“不分次数”分支里没有累加,恒为 0。 -- 2) nonFollowUpFirst 在 followUpCountStyle='1' 时接口会重复累加一次(两个 if 都命中 sendstate=4), -- SQL 用 *2 复刻。若按“一条记录算一次”应为 1 倍,把 IF(@followUpCountStyle='1',2,1) 改成 1。 -- 3) 比率格式:接口用 DecimalFormat("##.##%")(四舍六入、末尾 0 省略,如 85.5%), -- SQL 用 ROUND(...,2)(四舍五入、固定两位,如 85.50%);分母为 0 时接口给 "0.00%"。 -- 4) deptcode/deptname/drcode/drname:接口取「分组内第一条记录」的值(HashMap 顺序,无业务含义), -- SQL 用 MAX() 代替;按病区统计时这四列只作参考。 -- 5) 随访情况 taskSituation1~6:task_situation = 1~5 分别对应 1~5,其他(=7) 对应 6。 -- 本库 service_subtask.task_situation 全为 NULL,所以这几列目前必然都是 0。 -- 6) 只看首次/再次:接口传 visitCount=1 只看首次、>1 只看再次;本 SQL 两类同时给,均为 0 说明该窗口没有对应数据。 -- 7) 换维度:GROUP BY r.ward_code(病区,本 SQL)→ r.dept_code(科室)/ r.dr_code(医生)。 -- 统计方式、及时率天数都通过 PART 0 的变量控制,不需要改结构。 -- =============================================================================