liusheng
5 天以前 fb89df539c6293a41933e445df25fa9b8047ba18
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
-- =============================================================================
-- /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 的变量控制,不需要改结构。
-- =============================================================================