liusheng
2026-11-03 f12b3e4c7c8d55217cc1d21f814a9a3d5fc57251
提交Merge branch 'subtask' of http://116.62.18.175:6699/r/~yxh/smartor-web into subtask
已修改3个文件
已添加3个文件
510 ■■■■■ 文件已修改
sql/p_ward_followup_stats.sql 146 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/v_ward_followup_stats.sql 152 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/ward_followup_stats_query.sql 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/smartor/pendingTask.js 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/discharge/index.vue 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/discharge/outpatientService.vue 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/p_ward_followup_stats.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,146 @@
-- =============================================================================
-- ç—…区出院随访统计 Â· å­˜å‚¨è¿‡ç¨‹ï¼ˆæŽ¨èï¼ŒçœŸæ­£æŒ‰è°ƒç”¨ä¼ å‚)
-- =============================================================================
-- MySQL è§†å›¾ä¸èƒ½å¸¦å‚数(视图定义里连 @变量 éƒ½ä¸è®¸ç”¨ï¼‰ï¼Œè¦ã€Œä¼ å‚」就用存储过程。
--
-- è°ƒç”¨ï¼š
--   CALL p_ward_followup_stats('2026-10-04', '2026-11-03', '1', 7);
--   CALL p_ward_followup_stats('2026-10-04', '2026-11-03', NULL, NULL);  -- å£å¾„/及时天数用默认值
--   ï¼ˆMySQL å­˜å‚¨è¿‡ç¨‹æ²¡æœ‰ã€Œå¯é€‰å‚数」,四个参数都必须写,不需要的传 NULL)
--
-- å‚数:
--   p_start_time  å‡ºé™¢æ—¶é—´èµ·ï¼ˆå«ï¼‰ï¼Œ'yyyy-MM-dd' æˆ– 'yyyy-MM-dd HH:mm:ss'
--   p_end_time    å‡ºé™¢æ—¶é—´æ­¢ï¼ˆå«å½“天 23:59:59),传 'yyyy-MM-dd' å³å¯
--   p_count_style éšè®¿æ–¹å¼å‘½ä¸­å£å¾„:1=应随访(默认) 3=仅成功 å…¶å®ƒ=全部
--   p_timely_days åŠæ—¶å¤©æ•°åŸºæ•°ï¼Œé»˜è®¤ 7(补偿天数另加)
-- =============================================================================
DROP PROCEDURE IF EXISTS p_ward_followup_stats;
DELIMITER $$
CREATE PROCEDURE p_ward_followup_stats(
  IN p_start_time  DATETIME,
  IN p_end_time    DATETIME,
  IN p_count_style VARCHAR(2),
  IN p_timely_days INT
)
BEGIN
  DECLARE v_start       DATETIME;
  DECLARE v_end         DATETIME;
  DECLARE v_style       VARCHAR(2);
  DECLARE v_timely_days INT;
  -- èµ·æ­¢æ—¶é—´å¿…传;止时间按「包含当天」处理
  IF p_start_time IS NULL OR p_end_time IS NULL THEN
    SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'p_start_time / p_end_time ä¸èƒ½ä¸ºç©º';
  END IF;
  SET v_start       = DATE(p_start_time);
  SET v_end         = DATE_ADD(DATE(p_end_time), INTERVAL 1 DAY);
  SET v_style       = IFNULL(p_count_style, '1');
  SET v_timely_days = IFNULL(p_timely_days, 7);
  SELECT
    t.leavehospitaldistrictname,                                -- å‡ºé™¢ç—…区
    t.deptname,                                                 -- å‡ºé™¢ç§‘室
    t.dischargeCount,                                           -- å‡ºé™¢äººæ¬¡ï¼ˆé¦–次随访记录数)
    t.nonFollowUp,                                              -- æ— éœ€éšè®¿äººæ¬¡
    t.followUpNeeded,                                           -- åº”随访人次
    t.needFollowUp,                                             -- é¦–次应随访(= åº”随访人次)
    t.pendingFollowUp,                                          -- é¦–次待随访
    t.followUpSuccess,                                          -- é¦–次随访成功
    t.followUpFail,                                             -- é¦–次随访失败(含人工超时)
    t.manualSuccess,                                            -- é¦–次人工随访成功
    t.voiceSuccess,                                             -- é¦–次语音随访成功
    t.smsSuccess,                                               -- é¦–次短信随访成功
    t.weChatSuccess,                                            -- é¦–次微信随访成功
    -- éšè®¿æˆåŠŸçŽ‡ = æˆåŠŸ / åº”随访(只算成功,不含失败)
    IF(t.needFollowUp = 0, '0.00%',
       CONCAT(ROUND(100 * t.followUpSuccess / t.needFollowUp, 2), '%'))   AS followUpSuccessRate,
    -- æ¸ é“合计成功率 = å››ç§æ–¹å¼æˆåŠŸä¹‹å’Œ / å››ç§æ–¹å¼å‘½ä¸­ä¹‹å’Œ
    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,
    -- åŠæ—¶çŽ‡ï¼šåŠæ—¶ = finishtime ä¸ä¸ºç©º ä¸” DATEDIFF(finishtime, endtime) <= åŠæ—¶å¤©æ•° + è¡¥å¿å¤©æ•°
    IF(IFNULL(rt.timely_base, 0) = 0, '0.00%',
       CONCAT(ROUND(100 * rt.timely_cnt / rt.timely_base, 2), '%'))       AS timelyRate
  FROM (
    SELECT
      r.ward_code                                    AS ward_code,
      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 = 2, 1, 0))                 AS pendingFollowUp,
      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
    FROM (
      SELECT
        ss.leavehospitaldistrictcode                              AS ward_code,
        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,
        -- éšè®¿æ–¹å¼å‘½ä¸­æ¡ä»¶ï¼šæ–¹å¼ 1 æŽ’除「无需随访」;方式 3 åªç®—已完成;其它方式不设限
        IF(v_style = '1',
           IF(ss.sendstate IS NOT NULL AND ss.sendstate <> 4, 1, 0),
           IF(v_style = '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(v_start)
        AND ss.endtime <  DATE_ADD(DATE(v_end), INTERVAL 1 DAY)
        AND ss.orgid = '20001001'
    ) r
    GROUP BY r.ward_code
  ) t
  LEFT JOIN (
    -- åŠæ—¶çŽ‡ï¼šæŒ‰ç—…åŒºé¢„èšåˆä¸€æ¬¡å†å…³è”ï¼Œé¿å…ä¸€å¯¹å¤šæ”¾å¤§åˆ†æ¯
    SELECT
      ss.leavehospitaldistrictcode AS ward_code,
      COUNT(*) AS timely_base,
      SUM(IF(ss.finishtime IS NOT NULL
             AND DATEDIFF(ss.finishtime, ss.endtime) <= v_timely_days + IFNULL(cp.compensate_days, 0),
             1, 0)) AS timely_cnt
    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 æ—¶çš„固定条件
      AND ss.endtime >= DATE(v_start)
      AND ss.endtime <  DATE_ADD(DATE(v_end), INTERVAL 1 DAY)
      AND ss.orgid = '20001001'
    GROUP BY ss.leavehospitaldistrictcode
  ) rt ON rt.ward_code = t.ward_code
  ORDER BY t.dischargeCount DESC;
END$$
DELIMITER ;
sql/v_ward_followup_stats.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,152 @@
-- =============================================================================
-- ç—…区出院随访统计 Â· å¯ä¼ å‚视图
-- =============================================================================
-- ä¸ºä»€ä¹ˆæ˜¯ã€Œå‚数表 + è§†å›¾ã€è€Œä¸æ˜¯ @变量:
--   MySQL è§†å›¾å®šä¹‰é‡Œç¦æ­¢å¼•用 @用户变量 / ç³»ç»Ÿå˜é‡ / å­˜å‚¨è¿‡ç¨‹å‚数,
--   å¦åˆ™åˆ›å»ºæ—¶æŠ¥ ERROR 1351 (HY000): View's SELECT contains a variable or parameter。
--   å®˜æ–¹åŽŸæ–‡ï¼šã€ŒThe SELECT statement cannot refer to system variables or
--   user-defined variables.」
--   https://dev.mysql.com/doc/refman/8.0/en/create-view.html
--
-- æ‰€ä»¥ç”¨å•行参数表 report_param æ‰¿è½½å‚数:
--   1) REPLACE INTO report_param ... æ”¹ç¬¬ 1 è¡Œï¼ˆèµ·æ­¢æ—¶é—´ / å£å¾„ / åŠæ—¶å¤©æ•°ï¼‰
--   2) SELECT * FROM v_ward_followup_stats;
--
-- å¹¶å‘提醒:参数表是共享状态,两个会话同时改会互相串。多用户并发请改用
--          åŒç›®å½•çš„ p_ward_followup_stats.sql(存储过程,真正按调用传参)。
-- =============================================================================
-- ---------------------------------------------------------------------------
-- 1. å‚数表(永远只有 id = 1 è¿™ä¸€è¡Œï¼‰
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS report_param (
  id          TINYINT    NOT NULL                COMMENT '固定为 1',
  start_time  DATETIME   NOT NULL                COMMENT '出院时间起(含),传 yyyy-MM-dd æˆ– yyyy-MM-dd HH:mm:ss å‡å¯',
  end_time    DATETIME   NOT NULL                COMMENT '出院时间止(含当天 23:59:59),传 yyyy-MM-dd å³å¯',
  count_style VARCHAR(2) NOT NULL DEFAULT '1'    COMMENT '随访方式命中口径:1=应随访 3=仅成功 å…¶å®ƒ=全部',
  timely_days INT        NOT NULL DEFAULT 7      COMMENT '及时率的天数基数(补偿天数另加)',
  PRIMARY KEY (id)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '报表参数(单行,id æ’为 1)';
-- ---------------------------------------------------------------------------
-- 2. è§†å›¾
-- ---------------------------------------------------------------------------
CREATE OR REPLACE VIEW v_ward_followup_stats AS
SELECT
  t.leavehospitaldistrictname,                                -- å‡ºé™¢ç—…区
  t.deptname,                                                 -- å‡ºé™¢ç§‘室
  t.dischargeCount,                                           -- å‡ºé™¢äººæ¬¡ï¼ˆé¦–次随访记录数)
  t.nonFollowUp,                                              -- æ— éœ€éšè®¿äººæ¬¡
  t.followUpNeeded,                                           -- åº”随访人次
  t.needFollowUp,                                             -- é¦–次应随访(= åº”随访人次)
  t.pendingFollowUp,                                          -- é¦–次待随访
  t.followUpSuccess,                                          -- é¦–次随访成功
  t.followUpFail,                                             -- é¦–次随访失败(含人工超时)
  t.manualSuccess,                                            -- é¦–次人工随访成功
  t.voiceSuccess,                                             -- é¦–次语音随访成功
  t.smsSuccess,                                               -- é¦–次短信随访成功
  t.weChatSuccess,                                            -- é¦–次微信随访成功
  -- éšè®¿æˆåŠŸçŽ‡ = æˆåŠŸ / åº”随访(只算成功,不含失败)
  IF(t.needFollowUp = 0, '0.00%',
     CONCAT(ROUND(100 * t.followUpSuccess / t.needFollowUp, 2), '%'))   AS followUpSuccessRate,
  -- æ¸ é“合计成功率 = å››ç§æ–¹å¼æˆåŠŸä¹‹å’Œ / å››ç§æ–¹å¼å‘½ä¸­ä¹‹å’Œ
  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,
  -- åŠæ—¶çŽ‡ï¼šåŠæ—¶ = finishtime ä¸ä¸ºç©º ä¸” DATEDIFF(finishtime, endtime) <= åŠæ—¶å¤©æ•° + è¡¥å¿å¤©æ•°
  IF(IFNULL(rt.timely_base, 0) = 0, '0.00%',
     CONCAT(ROUND(100 * rt.timely_cnt / rt.timely_base, 2), '%'))       AS timelyRate
FROM (
  SELECT
    r.ward_code                                    AS ward_code,
    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 = 2, 1, 0))                 AS pendingFollowUp,
    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
  FROM (
    SELECT
      ss.leavehospitaldistrictcode                              AS ward_code,
      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,
      -- éšè®¿æ–¹å¼å‘½ä¸­æ¡ä»¶ï¼šæ–¹å¼ 1 æŽ’除「无需随访」;方式 3 åªç®—已完成;其它方式不设限
      IF(p.count_style = '1',
         IF(ss.sendstate IS NOT NULL AND ss.sendstate <> 4, 1, 0),
         IF(p.count_style = '3',
            IF(ss.sendstate = 6, 1, 0),
            IF(ss.sendstate IS NOT NULL, 1, 0)))                AS ch_hit
    FROM service_subtask ss
    JOIN report_param p ON p.id = 1
    WHERE ss.del_flag = 0
      AND ss.service_type = '2'
      AND ss.visit_count = 1                                  -- åªè¦é¦–次随访
      AND ss.leavehospitaldistrictcode IS NOT NULL
      -- >>> å¯ä¼ å‚的时间范围:左闭右开,end_time ä¼ æ—¥æœŸå³å¯å«å½“天全天 <<<
      AND ss.endtime >= DATE(p.start_time)
      AND ss.endtime <  DATE_ADD(DATE(p.end_time), INTERVAL 1 DAY)
      AND ss.orgid = '20001001'
  ) r
  GROUP BY r.ward_code
) t
LEFT JOIN (
  -- åŠæ—¶çŽ‡ï¼šæŒ‰ç—…åŒºé¢„èšåˆä¸€æ¬¡å†å…³è”ï¼Œé¿å…ä¸€å¯¹å¤šæ”¾å¤§åˆ†æ¯
  SELECT
    ss.leavehospitaldistrictcode AS ward_code,
    COUNT(*) AS timely_base,
    SUM(IF(ss.finishtime IS NOT NULL
           AND DATEDIFF(ss.finishtime, ss.endtime) <= p.timely_days + IFNULL(cp.compensate_days, 0),
           1, 0)) AS timely_cnt
  FROM service_subtask ss
  JOIN report_param p ON p.id = 1
  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 æ—¶çš„固定条件
    AND ss.endtime >= DATE(p.start_time)
    AND ss.endtime <  DATE_ADD(DATE(p.end_time), INTERVAL 1 DAY)
    AND ss.orgid = '20001001'
  GROUP BY ss.leavehospitaldistrictcode
) rt ON rt.ward_code = t.ward_code
ORDER BY t.dischargeCount DESC;
-- ---------------------------------------------------------------------------
-- 3. ç”¨æ³•
-- ---------------------------------------------------------------------------
-- æ”¹å‚数(只改这一行,两个日期就是原来的 endtime èµ·æ­¢ï¼‰
REPLACE INTO report_param (id, start_time, end_time, count_style, timely_days)
VALUES (1, '2026-10-04', '2026-11-03', '1', 7);
-- æŸ¥æ•°
SELECT *
FROM v_ward_followup_stats
-- WHERE leavehospitaldistrictname LIKE '%骨科%'   -- è§†å›¾å¤–照样能再加条件
;
sql/ward_followup_stats_query.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,114 @@
-- =============================================================================
-- ç—…区出院随访统计 Â· çº¯æŸ¥è¯¢ç‰ˆï¼ˆä¼šè¯å˜é‡ä¼ å‚,适合 Navicat/DataGrip ä¸´æ—¶å–数)
-- =============================================================================
-- @start_time / @end_time å°±æ˜¯åŽŸæ¥å†™æ­»çš„ endtime èµ·æ­¢ï¼Œæ”¹è¿™ä¸¤è¡Œå³å¯ã€‚
-- æ³¨æ„ï¼šè¿™ä¸æ˜¯è§†å›¾ï¼Œå˜é‡å±žäºŽå½“前连接,换连接要重新 SET。
-- =============================================================================
SET @start_time         = '2026-10-04';   -- å‡ºé™¢æ—¶é—´èµ·ï¼ˆå«ï¼‰
SET @end_time           = '2026-11-03';   -- å‡ºé™¢æ—¶é—´æ­¢ï¼ˆå«å½“天)
SET @followUpCountStyle = '1';            -- 1=应随访 3=仅成功 å…¶å®ƒ=全部
SET @timelyDays         = 7;              -- åŠæ—¶å¤©æ•°åŸºæ•°
SELECT
  t.leavehospitaldistrictname,                                -- å‡ºé™¢ç—…区
  t.deptname,                                                 -- å‡ºé™¢ç§‘室
  t.dischargeCount,                                           -- å‡ºé™¢äººæ¬¡ï¼ˆé¦–次随访记录数)
  t.nonFollowUp,                                              -- æ— éœ€éšè®¿äººæ¬¡
  t.followUpNeeded,                                           -- åº”随访人次
  t.needFollowUp,                                             -- é¦–次应随访(= åº”随访人次)
  t.pendingFollowUp,                                          -- é¦–次待随访
  t.followUpSuccess,                                          -- é¦–次随访成功
  t.followUpFail,                                             -- é¦–次随访失败(含人工超时)
  t.manualSuccess,                                            -- é¦–次人工随访成功
  t.voiceSuccess,                                             -- é¦–次语音随访成功
  t.smsSuccess,                                               -- é¦–次短信随访成功
  t.weChatSuccess,                                            -- é¦–次微信随访成功
  -- éšè®¿æˆåŠŸçŽ‡ = æˆåŠŸ / åº”随访(只算成功,不含失败)
  IF(t.needFollowUp = 0, '0.00%',
     CONCAT(ROUND(100 * t.followUpSuccess / t.needFollowUp, 2), '%'))   AS followUpSuccessRate,
  -- æ¸ é“合计成功率 = å››ç§æ–¹å¼æˆåŠŸä¹‹å’Œ / å››ç§æ–¹å¼å‘½ä¸­ä¹‹å’Œ
  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,
  -- åŠæ—¶çŽ‡ï¼šåŠæ—¶ = finishtime ä¸ä¸ºç©º ä¸” DATEDIFF(finishtime, endtime) <= åŠæ—¶å¤©æ•° + è¡¥å¿å¤©æ•°
  IF(IFNULL(rt.timely_base, 0) = 0, '0.00%',
     CONCAT(ROUND(100 * rt.timely_cnt / rt.timely_base, 2), '%'))       AS timelyRate
FROM (
  SELECT
    r.ward_code                                    AS ward_code,
    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 = 2, 1, 0))                 AS pendingFollowUp,
    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
  FROM (
    SELECT
      ss.leavehospitaldistrictcode                              AS ward_code,
      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,
      -- éšè®¿æ–¹å¼å‘½ä¸­æ¡ä»¶ï¼šæ–¹å¼ 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(@start_time)
      AND ss.endtime <  DATE_ADD(DATE(@end_time), INTERVAL 1 DAY)
      AND ss.orgid = '20001001'
  ) r
  GROUP BY r.ward_code
) t
LEFT JOIN (
  -- åŠæ—¶çŽ‡ï¼šæŒ‰ç—…åŒºé¢„èšåˆä¸€æ¬¡å†å…³è”ï¼Œé¿å…ä¸€å¯¹å¤šæ”¾å¤§åˆ†æ¯
  SELECT
    ss.leavehospitaldistrictcode AS ward_code,
    COUNT(*) AS timely_base,
    SUM(IF(ss.finishtime IS NOT NULL
           AND DATEDIFF(ss.finishtime, ss.endtime) <= @timelyDays + IFNULL(cp.compensate_days, 0),
           1, 0)) AS timely_cnt
  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 æ—¶çš„固定条件
    AND ss.endtime >= DATE(@start_time)
    AND ss.endtime <  DATE_ADD(DATE(@end_time), INTERVAL 1 DAY)
    AND ss.orgid = '20001001'
  GROUP BY ss.leavehospitaldistrictcode
) rt ON rt.ward_code = t.ward_code
ORDER BY t.dischargeCount DESC;
src/api/smartor/pendingTask.js
@@ -7,7 +7,7 @@
 * åŽç«¯å£å¾„:service_subtask ä¸­
 *   service_type = '2' å‡ºé™¢éšè®¿ / '3' é—¨è¯Šéšè®¿ï¼ˆåˆ†å¼€è®¡æ•°ï¼‰
 *   current_preachform = '1'(人工)
 *   å¾…随访状态(sendstate in 1,2,3,5,7)
 *   å¾…随访状态(sendstate in 1,2,3,7;发送失败 5 ä¸è®¡å…¥ï¼‰
 *   å‡ºé™¢æ—¶é—´ endtime æ—©äºŽç­‰äºŽä»Šå¤© - 7 å¤©
 *   åªç»Ÿè®¡æœ€è¿‘ 4 ä¸ªå‡ºé™¢æœˆä»½ï¼ˆå«å½“月)
 *
src/views/followvisit/discharge/index.vue
@@ -1934,7 +1934,7 @@
    },
    /**
     * å³ä¸‹è§’「待处理随访任务」卡片点击数字跳转过来:
     * æŒ‰å‡ºé™¢å¹´æœˆ + å¾…随访(sendstate 1/2/3/5/7)+ å‡ºé™¢æ»¡ 7 å¤© è¿‡æ»¤ï¼Œå£å¾„与卡片一致
     * æŒ‰å‡ºé™¢å¹´æœˆ + å¾…随访(sendstate 1/2/3/7)+ å‡ºé™¢æ»¡ 7 å¤© è¿‡æ»¤ï¼Œå£å¾„与卡片一致
     * @returns {boolean} æ˜¯å¦å·²æŒ‰å¡ç‰‡æ¡ä»¶å‘起查询
     */
    applyPendingTaskQuery() {
@@ -1952,11 +1952,8 @@
      const serviceType = Number(query.serviceType) === 3 ? 3 : 2;
      this.errtype = undefined;
      const start = month + "-01";
      let end = this.pendingTaskMonthEnd(month);
      // æœˆåˆ 7 å¤©å†…,"今天-7天" ä¼šæ—©äºŽæœˆåˆï¼Œå…œåº•成月初,避免日期区间倒挂
      if (end < start) {
        end = start;
      }
      // å¡ç‰‡ç»Ÿè®¡çš„æ˜¯ã€Œå‡ºé™¢æ»¡ 7 å¤©ã€çš„任务,截止日 = min(该月最后一天, ä»Šå¤©-7 å¤©)
      const end = this.pendingTaskMonthEnd(month);
      this.dateRange = [start, end];
      this.topqueryParams.serviceType = serviceType;
      this.topqueryParams.sendstateView = 1;
@@ -1964,24 +1961,47 @@
      // å¡ç‰‡ç»Ÿè®¡çš„æ˜¯å…¨éƒ¨å¾…随访任务(不区分首轮/复诊),列表这里同步放开
      this.topqueryParams.visitCount = null;
      this.topqueryParams.pageNum = 1;
      // å½“月还没到第 8 å¤©æ—¶ï¼Œè¯¥æœˆä¸å¯èƒ½æœ‰ã€Œå‡ºé™¢æ»¡ 7 å¤©ã€çš„任务(卡片显示 0),列表直接给空。
      // æ—¥æœŸæ¡†ä»æ˜¾ç¤ºè¯¥æœˆçš„自然区间,避免出现「起止倒挂」的筛选条件。
      if (end < start) {
        this.dateRange = [start, this.pendingTaskMonthLastDay(month)];
        this.userList = [];
        this.total = 0;
        this.loading = false;
        this.$message.info("该月暂无出院满 7 å¤©çš„待随访任务");
        return true;
      }
      this.handleQuery(1);
      return true;
    },
    /**
     * å¡ç‰‡å£å¾„的月份结束日:
     * å½“月只统计出院满 7 å¤©çš„,所以取「今天 - 7 å¤©ã€ï¼›åŽ†å²æœˆä»½å–å½“æœˆæœ€åŽä¸€å¤©
     * è¯¥æœˆæœ€åŽä¸€å¤©ï¼ˆyyyy-MM-dd),只用于「当月还没有满 7 å¤©ä»»åŠ¡ã€æ—¶å±•ç¤ºæ­£å¸¸æœˆä»½åŒºé—´
     */
    pendingTaskMonthLastDay(month) {
      const parts = String(month).split("-");
      const date = new Date(Number(parts[0]), Number(parts[1]), 0);
      const mm = String(date.getMonth() + 1).padStart(2, "0");
      const dd = String(date.getDate()).padStart(2, "0");
      return date.getFullYear() + "-" + mm + "-" + dd;
    },
    /**
     * å¡ç‰‡å£å¾„的月份结束日:卡片只统计「出院满 7 å¤©ä»æœªéšè®¿ã€çš„任务,
     * æ‰€ä»¥æˆªæ­¢æ—¥æ°¸è¿œæ˜¯ min(该月最后一天, ä»Šå¤©-7 å¤©);
     * åŽ†å²æœˆä»½åŒæ ·è¦æŒ‰ã€Œä»Šå¤©-7 å¤©ã€æˆªæ–­ï¼Œå¦åˆ™æœˆæœ«å‡ å¤©å‡ºé™¢çš„æ‚£è€…会被算进来(卡片 49、列表 77)。
     */
    pendingTaskMonthEnd(month) {
      const parts = String(month).split("-");
      const year = Number(parts[0]);
      const mon = Number(parts[1]);
      const now = new Date();
      let date;
      if (year === now.getFullYear() && mon === now.getMonth() + 1) {
        date = new Date(year, mon - 1, now.getDate() - 7);
      } else {
        date = new Date(year, mon, 0);
      }
      // å‡ºé™¢æ»¡ 7 å¤©ï¼šå‡ºé™¢æ—¥æœŸ <= ä»Šå¤©-7(与后端 endtime < ä»Šå¤©-6 00:00 ç­‰ä»·ï¼‰
      const cutoff = new Date(
        now.getFullYear(),
        now.getMonth(),
        now.getDate() - 7
      );
      const monthEnd = new Date(year, mon, 0);
      const date = cutoff < monthEnd ? cutoff : monthEnd;
      const mm = String(date.getMonth() + 1).padStart(2, "0");
      const dd = String(date.getDate()).padStart(2, "0");
      return date.getFullYear() + "-" + mm + "-" + dd;
src/views/followvisit/discharge/outpatientService.vue
@@ -1373,7 +1373,7 @@
    },
    /**
     * å³ä¸‹è§’「待处理随访任务」卡片点击数字跳转过来:
     * æŒ‰å‡ºé™¢å¹´æœˆ + å¾…随访(sendstate 1/2/3/5/7)+ å‡ºé™¢æ»¡ 7 å¤© è¿‡æ»¤ï¼Œå£å¾„与卡片一致
     * æŒ‰å‡ºé™¢å¹´æœˆ + å¾…随访(sendstate 1/2/3/7)+ å‡ºé™¢æ»¡ 7 å¤© è¿‡æ»¤ï¼Œå£å¾„与卡片一致
     * @returns {boolean} æ˜¯å¦å·²æŒ‰å¡ç‰‡æ¡ä»¶å‘起查询
     */
    applyPendingTaskQuery() {
@@ -1391,11 +1391,8 @@
      const serviceType = Number(query.serviceType) === 3 ? 3 : 2;
      this.errtype = undefined;
      const start = month + "-01";
      let end = this.pendingTaskMonthEnd(month);
      // æœˆåˆ 7 å¤©å†…,"今天-7天" ä¼šæ—©äºŽæœˆåˆï¼Œå…œåº•成月初,避免日期区间倒挂
      if (end < start) {
        end = start;
      }
      // å¡ç‰‡ç»Ÿè®¡çš„æ˜¯ã€Œå‡ºé™¢æ»¡ 7 å¤©ã€çš„任务,截止日 = min(该月最后一天, ä»Šå¤©-7 å¤©)
      const end = this.pendingTaskMonthEnd(month);
      this.dateRange = [start, end];
      this.topqueryParams.serviceType = serviceType;
      this.topqueryParams.sendstateView = 1;
@@ -1403,24 +1400,47 @@
      // å¡ç‰‡ç»Ÿè®¡çš„æ˜¯å…¨éƒ¨å¾…随访任务(不区分首轮/复诊),列表这里同步放开
      this.topqueryParams.visitCount = null;
      this.topqueryParams.pageNum = 1;
      // å½“月还没到第 8 å¤©æ—¶ï¼Œè¯¥æœˆä¸å¯èƒ½æœ‰ã€Œå‡ºé™¢æ»¡ 7 å¤©ã€çš„任务(卡片显示 0),列表直接给空。
      // æ—¥æœŸæ¡†ä»æ˜¾ç¤ºè¯¥æœˆçš„自然区间,避免出现「起止倒挂」的筛选条件。
      if (end < start) {
        this.dateRange = [start, this.pendingTaskMonthLastDay(month)];
        this.userList = [];
        this.total = 0;
        this.loading = false;
        this.$message.info("该月暂无出院满 7 å¤©çš„待随访任务");
        return true;
      }
      this.handleQuery(1);
      return true;
    },
    /**
     * å¡ç‰‡å£å¾„的月份结束日:
     * å½“月只统计出院满 7 å¤©çš„,所以取「今天 - 7 å¤©ã€ï¼›åŽ†å²æœˆä»½å–å½“æœˆæœ€åŽä¸€å¤©
     * è¯¥æœˆæœ€åŽä¸€å¤©ï¼ˆyyyy-MM-dd),只用于「当月还没有满 7 å¤©ä»»åŠ¡ã€æ—¶å±•ç¤ºæ­£å¸¸æœˆä»½åŒºé—´
     */
    pendingTaskMonthLastDay(month) {
      const parts = String(month).split("-");
      const date = new Date(Number(parts[0]), Number(parts[1]), 0);
      const mm = String(date.getMonth() + 1).padStart(2, "0");
      const dd = String(date.getDate()).padStart(2, "0");
      return date.getFullYear() + "-" + mm + "-" + dd;
    },
    /**
     * å¡ç‰‡å£å¾„的月份结束日:卡片只统计「出院满 7 å¤©ä»æœªéšè®¿ã€çš„任务,
     * æ‰€ä»¥æˆªæ­¢æ—¥æ°¸è¿œæ˜¯ min(该月最后一天, ä»Šå¤©-7 å¤©);
     * åŽ†å²æœˆä»½åŒæ ·è¦æŒ‰ã€Œä»Šå¤©-7 å¤©ã€æˆªæ–­ï¼Œå¦åˆ™æœˆæœ«å‡ å¤©å‡ºé™¢çš„æ‚£è€…会被算进来(卡片 49、列表 77)。
     */
    pendingTaskMonthEnd(month) {
      const parts = String(month).split("-");
      const year = Number(parts[0]);
      const mon = Number(parts[1]);
      const now = new Date();
      let date;
      if (year === now.getFullYear() && mon === now.getMonth() + 1) {
        date = new Date(year, mon - 1, now.getDate() - 7);
      } else {
        date = new Date(year, mon, 0);
      }
      // å‡ºé™¢æ»¡ 7 å¤©ï¼šå‡ºé™¢æ—¥æœŸ <= ä»Šå¤©-7(与后端 endtime < ä»Šå¤©-6 00:00 ç­‰ä»·ï¼‰
      const cutoff = new Date(
        now.getFullYear(),
        now.getMonth(),
        now.getDate() - 7
      );
      const monthEnd = new Date(year, mon, 0);
      const date = cutoff < monthEnd ? cutoff : monthEnd;
      const mm = String(date.getMonth() + 1).padStart(2, "0");
      const dd = String(date.getDate()).padStart(2, "0");
      return date.getFullYear() + "-" + mm + "-" + dd;