陈昶聿
7 天以前 fc1f48f6cb2736bf03b7750563e5d9830d6fd4ba
ruoyi-task/src/main/java/com/ruoyi/task/controller/ServiceSubtaskController.java
@@ -3,10 +3,12 @@
import com.alibaba.fastjson.JSON;
import com.github.pagehelper.ISelect;
import com.ruoyi.common.annotation.AddOrgId;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.constant.HttpStatus;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.entity.SysUserDept;
import com.ruoyi.common.core.domain.model.LoginUser;
@@ -20,11 +22,8 @@
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.common.utils.reflect.ReflectUtils;
import com.ruoyi.common.core.service.TaskOpsTriggerApi;
import com.ruoyi.task.api.spi.TaskSysConfigPort;
import com.ruoyi.task.api.spi.TaskSysUserDeptPort;
import com.ruoyi.satisfaction.api.SatisfactionExportBundle;
import com.ruoyi.satisfaction.api.SatisfactionFacade;
import com.ruoyi.task.api.spi.TaskSubtaskRecordPort;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
@@ -54,6 +53,7 @@
import com.ruoyi.task.domain.ServiceSubtaskStatistic;
import com.ruoyi.task.domain.ServiceTask;
import com.ruoyi.task.domain.VO.ServiceSubtaskCotinueCountVO;
import com.ruoyi.task.service.IServiceSubtaskRecordService;
import com.ruoyi.task.service.IServiceSubtaskService;
import com.ruoyi.task.service.IServiceTaskService;
import com.ruoyi.task.domain.PatArchiveSrmVO;
@@ -65,6 +65,9 @@
import com.ruoyi.task.domain.VO.ServiceSubtaskVO;
import com.ruoyi.task.domain.VO.TransferDeptVO;
import com.ruoyi.task.domain.ServiceSubtaskExport;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.system.service.ISysDeptService;
import com.ruoyi.system.service.ISysUserDeptService;
/**
 * 单一任务(随访)Controller
@@ -104,13 +107,16 @@
    private IServiceTaskService serviceTaskService;
    @Autowired
    private TaskSysUserDeptPort taskSysUserDeptPort;
    private ISysUserDeptService sysUserDeptService;
    @Autowired
    private TaskSubtaskRecordPort serviceSubtaskRecordService;
    private IServiceSubtaskRecordService serviceSubtaskRecordService;
    @Autowired
    private TaskSysConfigPort taskSysConfigPort;
    private ISysConfigService sysConfigService;
    @Autowired
    private ISysDeptService sysDeptService;
    /**
     * 任务运维触发能力。
@@ -160,11 +166,16 @@
    @ApiOperation("查询患者随访信息")
    @PostMapping("/patItem")
    public Map<String, Object> patItem(@RequestBody ServiceSubtaskVO serviceSubtaskVO) {
        // ==== 临时诊断计时:确认慢在哪一段后整段删除即可(只加日志,不改逻辑)====
        long tTotal = System.currentTimeMillis();
        long tMark = tTotal;
        serviceSubtaskVO.setPageNum(PageUtils.getOffset(serviceSubtaskVO.getPageNum(), serviceSubtaskVO.getPageSize()));
        List<ServiceSubtaskRes> serviceSubtaskList = null;
        LoginUser loginUser = getLoginUser();
        SysUser user = loginUser.getUser();
        serviceSubtaskVO.setOrgid(user.getOrgid());
        // 院区取登录会话里的值(登录页选择的院区,见 SysLoginService#login)
        serviceSubtaskVO.setCampusid(user.getCampusid());
        ServiceSubtaskEntity serviceSubtaskEntity = null;
        if (serviceSubtaskVO != null) {
@@ -172,17 +183,26 @@
            serviceSubtaskEntity.setContinueContent(ObjectUtils.isNotEmpty(serviceSubtaskVO.getContinueContent()) ? serviceSubtaskVO.getContinueContent().toString() : null);
            serviceSubtaskList = serviceSubtaskService.patItem(serviceSubtaskEntity);
        }
        log.info("[patItem耗时] 1.列表页(两阶段分页)={}ms,行数={}", System.currentTimeMillis() - tMark,
                serviceSubtaskList == null ? -1 : serviceSubtaskList.size());
        tMark = System.currentTimeMillis();
        // 批量查 subtask_record,1次SQL替代循环中N次单独查
        List<Long> subtaskIds = serviceSubtaskList.stream().map(ServiceSubtaskRes::getId).filter(ObjectUtils::isNotEmpty).collect(Collectors.toList());
        Map<Long, List<ServiceSubtaskRecord>> recordMap = serviceSubtaskRecordService.selectRecordMapBySubtaskIds(subtaskIds);
        for (ServiceSubtaskRes serviceSubtask : serviceSubtaskList) {
            serviceSubtask.setServiceSubtaskRecordList(recordMap.getOrDefault(serviceSubtask.getId(), new ArrayList<>()));
        }
//        List<Long> subtaskIds = serviceSubtaskList.stream().map(ServiceSubtaskRes::getId).filter(ObjectUtils::isNotEmpty).collect(Collectors.toList());
//        Map<Long, List<ServiceSubtaskRecord>> recordMap = serviceSubtaskRecordService.selectRecordMapBySubtaskIds(subtaskIds);
//        log.info("[patItem耗时] 2.subtask_record批量={}ms,入参id数={}", System.currentTimeMillis() - tMark, subtaskIds.size());
//        tMark = System.currentTimeMillis();
//
//        for (ServiceSubtaskRes serviceSubtask : serviceSubtaskList) {
//            serviceSubtask.setServiceSubtaskRecordList(recordMap.getOrDefault(serviceSubtask.getId(), new ArrayList<>()));
//        }
        log.info("[patItem耗时] 3.记录回填={}ms", System.currentTimeMillis() - tMark);
        tMark = System.currentTimeMillis();
//      patItemCount 走Redis缓存版,避免每次全表聚合扫描
        Map<String, Object> map = getPatItemCountWithCache(serviceSubtaskEntity);
        log.info("[patItem耗时] 4.统计patItemCount(含缓存)={}ms", System.currentTimeMillis() - tMark);
        tMark = System.currentTimeMillis();
        map.put("serviceSubtaskList", serviceSubtaskList);
        List<Map<String, Object>> list = new ArrayList<>();
        list.add(map);
@@ -200,6 +220,8 @@
            total = CollectionUtils.isNotEmpty(serviceSubtasks) ? serviceSubtasks.size() : 0L;
        }
        log.info("[patItem耗时] 5.组装返回total={}={}ms,整体合计={}ms", total,
                System.currentTimeMillis() - tMark, System.currentTimeMillis() - tTotal);
        return getDataTable3(total, list);
    }
@@ -348,8 +370,7 @@
    public AjaxResult querySubtaskList(@RequestBody ServiceSubtaskVO serviceSubtaskVO) {
        ServiceSubtaskEntity serviceSubtaskEntity = DtoConversionUtils.sourceToTarget(serviceSubtaskVO, ServiceSubtaskEntity.class);
        serviceSubtaskEntity.setContinueContent(ObjectUtils.isNotEmpty(serviceSubtaskVO.getContinueContent()) ? serviceSubtaskVO.getContinueContent().toString() : null);
        PageUtils.startPageByPost(serviceSubtaskEntity.getPageNum(), serviceSubtaskEntity.getPageSize());
        serviceSubtaskService.selectServiceSubtaskList(serviceSubtaskEntity);
        // 该 SQL 已内置 limit ${pageSize} OFFSET ${pageNum},不再走 PageHelper,避免两套分页叠加产生两个 LIMIT
        return success(serviceSubtaskService.selectServiceSubtaskList(serviceSubtaskEntity));
    }
@@ -363,8 +384,8 @@
    public TableDataInfo querySubtaskListAgain(@RequestBody ServiceSubtaskVO serviceSubtaskVO) {
        ServiceSubtaskEntity serviceSubtaskEntity = DtoConversionUtils.sourceToTarget(serviceSubtaskVO, ServiceSubtaskEntity.class);
        serviceSubtaskEntity.setContinueContent(ObjectUtils.isNotEmpty(serviceSubtaskVO.getContinueContent()) ? serviceSubtaskVO.getContinueContent().toString() : null);
        // 该 SQL 未内置 limit,使用 PageHelper 分页;只调用一次,getDataTable 可基于 Page 取到正确 total
        PageUtils.startPageByPost(serviceSubtaskEntity.getPageNum(), serviceSubtaskEntity.getPageSize());
        serviceSubtaskService.selectServiceSubtaskListAgain(serviceSubtaskEntity);
        return getDataTable(serviceSubtaskService.selectServiceSubtaskListAgain(serviceSubtaskEntity));
    }
@@ -543,7 +564,7 @@
                        sysUserWard.setOrgid(user.getOrgid());
                        sysUserWard.setCampusid(user.getCampusid());
                        sysUserWard.setDeptType("2");
                        List<SysUserDept> wardList = taskSysUserDeptPort.selectSysUserDeptList(sysUserWard);
                        List<SysUserDept> wardList = sysUserDeptService.selectSysUserDeptList(sysUserWard);
                        if (CollectionUtils.isNotEmpty(wardList)) {
                            leavehospitaldistrictcodes = wardList.stream().map(SysUserDept::getDeptCode).collect(Collectors.toList());
                        }
@@ -558,7 +579,7 @@
                        sysUserDept.setOrgid(user.getOrgid());
                        sysUserDept.setCampusid(user.getCampusid());
                        sysUserDept.setDeptType("1");
                        List<SysUserDept> deptList = taskSysUserDeptPort.selectSysUserDeptList(sysUserDept);
                        List<SysUserDept> deptList = sysUserDeptService.selectSysUserDeptList(sysUserDept);
                        if (CollectionUtils.isNotEmpty(deptList)) {
                            deptCodes = deptList.stream().map(SysUserDept::getDeptCode).collect(Collectors.toList());
                        }
@@ -568,7 +589,7 @@
            }
        }
        String followUpCountStyle = taskSysConfigPort.selectConfigValue("followUpCountStyle", serviceSubtaskCountReq.getOrgid());
        String followUpCountStyle = sysConfigService.selectConfigByKey("followUpCountStyle", serviceSubtaskCountReq.getOrgid());
        if (ObjectUtils.isNotEmpty(followUpCountStyle)) {
            serviceSubtaskCountReq.setFollowUpCountStyle(followUpCountStyle);
        } else {
@@ -578,6 +599,155 @@
        serviceSubtaskCountReq.setPageSize(null);
        List<ServiceSubtaskStatistic> sfStatistics = serviceSubtaskService.getSfStatistics(serviceSubtaskCountReq);
        return getDataTable4(CollectionUtils.isEmpty(sfStatistics) ? sfStatistics.size() : 0, serviceSubtaskService.getSfStatistics(serviceSubtaskCountReq));
    }
    // =====================================================================================
    // 免登录随访统计(供 public/html/sfstat.html 独立页面使用,@Anonymous 即 SecurityConfig 的 permitAll)
    // 口径与 /getSfStatistics 完全一致,区别只有两点:
    //   1. 不依赖登录用户,orgid / campusid 由请求体显式传入;
    //   2. 未指定病区(科室)时,取该机构(院区)下的全部病区(科室),而非当前登录用户的权限范围。
    // =====================================================================================
    /**
     * 免登录:随访统计(首次 / 再次)
     */
    @Anonymous
    @PostMapping("/publicSfStatistics")
    public Map<String, Object> publicSfStatistics(@RequestBody ServiceSubtaskCountReq serviceSubtaskCountReq) {
        if (CollectionUtils.isEmpty(serviceSubtaskCountReq.getServiceType())) {
            return error("服务类型不能为空");
        }
        if (StringUtils.isEmpty(serviceSubtaskCountReq.getOrgid())) {
            return error("缺少机构参数 orgid");
        }
        Integer statisticaltype = serviceSubtaskCountReq.getStatisticaltype();
        if (statisticaltype != null && statisticaltype == 1) {
            List<String> wardCodes = isAllSelected(serviceSubtaskCountReq.getLeavehospitaldistrictcodes())
                    ? listDeptCodesByType(serviceSubtaskCountReq.getOrgid(), serviceSubtaskCountReq.getCampusid(), "2")
                    : serviceSubtaskCountReq.getLeavehospitaldistrictcodes();
            if (CollectionUtils.isEmpty(wardCodes)) {
                return getDataTable4(0, new ArrayList<>());
            }
            serviceSubtaskCountReq.setLeavehospitaldistrictcodes(wardCodes);
            serviceSubtaskCountReq.setDeptcodes(null);
        } else if (statisticaltype != null && statisticaltype == 2) {
            List<String> deptCodes = isAllSelected(serviceSubtaskCountReq.getDeptcodes())
                    ? listDeptCodesByType(serviceSubtaskCountReq.getOrgid(), serviceSubtaskCountReq.getCampusid(), "1")
                    : serviceSubtaskCountReq.getDeptcodes();
            if (CollectionUtils.isEmpty(deptCodes)) {
                return getDataTable4(0, new ArrayList<>());
            }
            serviceSubtaskCountReq.setDeptcodes(deptCodes);
            serviceSubtaskCountReq.setLeavehospitaldistrictcodes(null);
        }
        String followUpCountStyle = sysConfigService.selectConfigByKey("followUpCountStyle", serviceSubtaskCountReq.getOrgid());
        if (ObjectUtils.isNotEmpty(followUpCountStyle)) {
            serviceSubtaskCountReq.setFollowUpCountStyle(followUpCountStyle);
        } else {
            serviceSubtaskCountReq.setFollowUpCountStyle("1");
        }
        if (serviceSubtaskCountReq.getRateDay() == null) {
            serviceSubtaskCountReq.setRateDay(7);
        }
        serviceSubtaskCountReq.setPageNum(null);
        serviceSubtaskCountReq.setPageSize(null);
        List<ServiceSubtaskStatistic> sfStatistics = serviceSubtaskService.getSfStatistics(serviceSubtaskCountReq);
        return getDataTable4(CollectionUtils.isEmpty(sfStatistics) ? 0 : sfStatistics.size(), sfStatistics);
    }
    /**
     * 免登录:随访统计筛选项(病区列表 / 科室列表)
     *
     * @param orgid    机构ID,必填
     * @param campusid 院区ID,可为空;为空时取该机构全部院区
     */
    @Anonymous
    @GetMapping("/publicSfStatisticsOptions")    public AjaxResult publicSfStatisticsOptions(@RequestParam(value = "orgid", required = false) String orgid,
                                                @RequestParam(value = "campusid", required = false) String campusid) {
        if (StringUtils.isEmpty(orgid)) {
            return error("缺少机构参数 orgid");
        }
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("orgid", orgid);
        result.put("campusid", campusid);
        result.put("wards", listDepts(orgid, campusid, "2"));
        result.put("depts", listDepts(orgid, campusid, "1"));
        return success(result);
    }
    /**
     * 免登录:延续护理统计(口径与 /getContinueNerseCount 一致,orgid / campusid 由请求体传入)
     */
    @Anonymous
    @PostMapping("/publicContinueNerseCount")
    public AjaxResult publicContinueNerseCount(@RequestBody ServiceSubtaskCotinueCountVO serviceSubtaskCotinueCountVO) {
        if (StringUtils.isEmpty(serviceSubtaskCotinueCountVO.getOrgid())) {
            return error("缺少机构参数 orgid");
        }
        List<String> districts = serviceSubtaskCotinueCountVO.getLeavehospitaldistrictcodes();
        List<String> depts = serviceSubtaskCotinueCountVO.getDeptcodes();
        boolean hasDistrict = !CollectionUtils.isEmpty(districts) && !districts.contains("all");
        boolean hasDept = !CollectionUtils.isEmpty(depts) && !depts.contains("all");
        if (hasDistrict && hasDept) {
            return error("病区Code集合与科室Code集合互斥,不能同时传入");
        }
        if (!hasDistrict && !hasDept) {
            // 未指定病区/科室:按该机构(院区)下全部病区统计
            List<String> wardCodes = listDeptCodesByType(serviceSubtaskCotinueCountVO.getOrgid(),
                    serviceSubtaskCotinueCountVO.getCampusid(), "2");
            if (CollectionUtils.isEmpty(wardCodes)) {
                return success(new LinkedHashMap<>());
            }
            serviceSubtaskCotinueCountVO.setLeavehospitaldistrictcodes(wardCodes);
            serviceSubtaskCotinueCountVO.setDeptcodes(null);
        } else if (hasDistrict) {
            serviceSubtaskCotinueCountVO.setDeptcodes(null);
        } else {
            serviceSubtaskCotinueCountVO.setLeavehospitaldistrictcodes(null);
        }
        return success(serviceSubtaskService.getContinueNurseCount(serviceSubtaskCotinueCountVO));
    }
    /**
     * 前端未选择具体病区/科室(或选择了 all)时,视为“全部”
     */
    private boolean isAllSelected(List<String> codes) {
        return CollectionUtils.isEmpty(codes) || codes.contains("all");
    }
    private List<String> listDeptCodesByType(String orgid, String campusid, String deptType) {
        List<String> codes = new ArrayList<>();
        for (Map<String, String> item : listDepts(orgid, campusid, deptType)) {
            codes.add(item.get("code"));
        }
        return codes;
    }
    /**
     * 查询机构(院区)下指定类型的部门:deptType=2 病区,deptType=1 科室
     */
    private List<Map<String, String>> listDepts(String orgid, String campusid, String deptType) {
        List<Map<String, String>> result = new ArrayList<>();
        SysDept query = new SysDept();
        query.setOrgid(orgid);
        query.setDeptType(deptType);
        if (StringUtils.isNotEmpty(campusid) && !"all".equalsIgnoreCase(campusid)) {
            query.setCampusid(campusid);
        }
        List<SysDept> depts = sysDeptService.selectSysDeptList(query);
        if (CollectionUtils.isEmpty(depts)) {
            return result;
        }
        for (SysDept dept : depts) {
            if (StringUtils.isEmpty(dept.getDeptCode())) {
                continue;
            }
            Map<String, String> item = new LinkedHashMap<>();
            item.put("code", dept.getDeptCode());
            item.put("name", dept.getDeptName());
            result.add(item);
        }
        return result;
    }
    /**
@@ -593,7 +763,7 @@
        Integer offset = PageUtils.getOffset(serviceSubtaskCountReq.getPageNum(), serviceSubtaskCountReq.getPageSize());
        serviceSubtaskCountReq.setPageNum(offset);
        String followUpCountStyle = taskSysConfigPort.selectConfigValue("followUpCountStyle", serviceSubtaskCountReq.getOrgid());
        String followUpCountStyle = sysConfigService.selectConfigByKey("followUpCountStyle", serviceSubtaskCountReq.getOrgid());
        if (ObjectUtils.isNotEmpty(followUpCountStyle)) {
            serviceSubtaskCountReq.setFollowUpCountStyle(followUpCountStyle);
        } else {
@@ -677,6 +847,10 @@
     * manualInfo - visitCount = 1 && currentPreachForm = "1"
     * voiceInfo - visitCount = 1 && currentPreachForm = "3"
     * smsInfo - visitCount = 1 && currentPreachForm = "4"
     * manualSuccessInfo - visitCount = 1 && currentPreachForm = "1" && sendstate = 6
     * voiceSuccessInfo - visitCount = 1 && currentPreachForm = "3" && sendstate = 6
     * smsSuccessInfo - visitCount = 1 && currentPreachForm = "4" && sendstate = 6
     * wechatSuccessInfo - visitCount = 1 && currentPreachForm = "5" && sendstate = 6
     * wechatInfo - visitCount = 1 && currentPreachForm = "5"
     * abnormalInfo - visitCount = 1 && excep = "1"
     * needFollowUpAgainInfo - visitCount > 1 && sendstate != 4
@@ -694,6 +868,19 @@
     * taskSituation4Info - taskSituation = 4
     * taskSituation5Info - taskSituation = 5
     * taskSituation6Info - taskSituation = 7
     * manuallyCalledInfo - isManuallyCalled = 1L
     * manualCallTimeoutInfo - visitCount = 1 && sendstate = 7 && isManuallyCalled = 0
     * manualCallTimeoutAgainInfo - visitCount > 1 && sendstate = 7 && isManuallyCalled = 0
     * pendingFollowUpAllInfo - visitCount = 1 && sendstate in (1,2,3,7)
     * pendingFollowUpAllAgainInfo - visitCount > 1 && sendstate in (1,2,3,7)
     * pendingFollowUpWaitSendInfo - visitCount = 1 && sendstate in (1,2)
     * pendingFollowUpWaitSendAgainInfo - visitCount > 1 && sendstate in (1,2)
     * pendingFollowUpSentInfo - visitCount = 1 && sendstate in (3,7)
     * pendingFollowUpSentAgainInfo - visitCount > 1 && sendstate in (3,7)
     * sentInfo - visitCount = 1 && sendstate = 3
     * sentAgainInfo - visitCount > 1 && sendstate = 3
     * firstDischargeCountInfo - visitCount = 1
     * againFollowUpPushCountInfo - visitCount = 2 && visitDeptCode = sys.smartor.center.dept
     * filterCountList - taskSituation = 6 && sendstate = 4
     *
     */
@@ -705,7 +892,7 @@
        String orgid = loginUser.getUser().getOrgid();
        String configKey = serviceSubtaskCountReq.getConfigKey();
        if (StringUtils.isNotEmpty(configKey)) {
            String configValue = taskSysConfigPort.selectConfigValue(configKey, orgid);
            String configValue = sysConfigService.selectConfigByKey(configKey, orgid);
            serviceSubtaskCountReq.setConfigValue(configValue);
            if (StringUtils.isEmpty(configValue)) {
                String logInfo = "getSfStatisticsCount-参数 " + configKey + " 未配置, 请配置好后重试";
@@ -714,7 +901,7 @@
            }
        }
        String followUpCountStyle = taskSysConfigPort.selectConfigValue("followUpCountStyle", orgid);
        String followUpCountStyle = sysConfigService.selectConfigByKey("followUpCountStyle", orgid);
        if (ObjectUtils.isNotEmpty(followUpCountStyle)) {
            serviceSubtaskCountReq.setFollowUpCountStyle(followUpCountStyle);
        } else {
@@ -754,7 +941,7 @@
        }
        String configKey = serviceSubtaskCountReq.getConfigKey();
        if (StringUtils.isNotEmpty(configKey)) {
            String configValue = taskSysConfigPort.selectConfigValue(configKey, serviceSubtaskCountReq.getOrgid());
            String configValue = sysConfigService.selectConfigByKey(configKey, serviceSubtaskCountReq.getOrgid());
            serviceSubtaskCountReq.setConfigValue(configValue);
            if (StringUtils.isEmpty(configValue)) {
                String logInfo = "getSfStatisticsCount-参数 " + configKey + " 未配置, 请配置好后重试";
@@ -766,6 +953,9 @@
            log.error(logInfo);
            return error(logInfo);
        }
        // 分类统计为汇总结果,不参与分页;同时避免 pageNum/pageSize 影响分组 key 的收集。
        serviceSubtaskCountReq.setPageNum(null);
        serviceSubtaskCountReq.setPageSize(null);
        List<ServiceSubtaskStatistic> serviceSubtaskStatistics = serviceSubtaskService.getSfStatisticsScript(serviceSubtaskCountReq);
        return success(serviceSubtaskStatistics);
    }
@@ -774,13 +964,18 @@
    @AddOrgId(field = "orgid", paramIndex = 0, campusField = "campusid")
    @PostMapping("/getSfStatisticsCountDetails")
    public AjaxResult getSfStatisticsCountDetails(@RequestBody ServiceSubtaskCountReq serviceSubtaskCountReq) {
        if (serviceSubtaskCountReq.getLeavehospitaldistrictcodes().size() == 0 && serviceSubtaskCountReq.getDeptcodes().size() == 0) {
        boolean noDeptOrDistrict = CollectionUtils.isEmpty(serviceSubtaskCountReq.getLeavehospitaldistrictcodes())
                && CollectionUtils.isEmpty(serviceSubtaskCountReq.getDeptcodes())
                && CollectionUtils.isEmpty(serviceSubtaskCountReq.getLeaveldeptcodes())
                && CollectionUtils.isEmpty(serviceSubtaskCountReq.getHospitaldeptcodes())
                && CollectionUtils.isEmpty(serviceSubtaskCountReq.getHospitaldistrictcodes());
        if (noDeptOrDistrict) {
            return error("科室或病区不能为空");
        }
        //复诊统计
        String configKey = serviceSubtaskCountReq.getConfigKey();
        if (StringUtils.isNotEmpty(configKey)) {
            String configValue = taskSysConfigPort.selectConfigValue(configKey, serviceSubtaskCountReq.getOrgid());
            String configValue = sysConfigService.selectConfigByKey(configKey, serviceSubtaskCountReq.getOrgid());
            if (StringUtils.isEmpty(configValue)) {
                String logInfo = "getSfStatisticsCountDetails-参数 " + configKey + " 未配置, 请配置好后重试";
                log.error(logInfo);
@@ -796,10 +991,54 @@
        LoginUser loginUser = getLoginUser();
        String orgid = loginUser.getUser().getOrgid();
        serviceSubtaskCountReq.setOrgid(orgid);
        PageUtils.startPageByPost(serviceSubtaskCountReq.getPageNum(), serviceSubtaskCountReq.getPageSize());
        return success(serviceSubtaskService.getSfStatisticsScriptDetails(serviceSubtaskCountReq));
        //注意:这里不能再调用 PageUtils.startPageByPost。
        //返回结构是 Map,且不传 type 时会执行两条 SQL,PageHelper 只作用于第一条 SQL,
        //分页已放到 Service 里按题目(scriptid)完成,保证 total 是题目总数。
        Map<String, Object> details = serviceSubtaskService.getSfStatisticsScriptDetails(serviceSubtaskCountReq);
        AjaxResult ajax = success(details.get("data"));
        //兼容旧前端:data 仍然是 {scriptid: 题目} 的 Map;
        //同时按 RuoYi 习惯补上 rows/total/pageNum/pageSize 方便前端分页
        ajax.put("rows", details.get("rows"));
        ajax.put("total", details.get("total"));
        ajax.put("pageNum", details.get("pageNum"));
        ajax.put("pageSize", details.get("pageSize"));
        return ajax;
    }
    @ApiOperation("获取随访分类统计明细详情超链接")
    @AddOrgId(field = "orgid", paramIndex = 0, campusField = "campusid")
    @PostMapping("/getSfStatisticsCountDetailsHyperLink")
    public AjaxResult getSfStatisticsCountDetailsHyperLink(@RequestBody ServiceSubtaskCountReq serviceSubtaskCountReq) {
        Integer statisticaltype = serviceSubtaskCountReq.getStatisticaltype();
        if (statisticaltype != null && statisticaltype != 1 && statisticaltype != 2 && statisticaltype != 3) {
            return error("统计维度statisticaltype仅支持:1-按病区 2-按科室 3-按taskid");
        }
        if (serviceSubtaskCountReq.getScriptid() == null
                && StringUtils.isEmpty(serviceSubtaskCountReq.getScriptContent())) {
            return error("问题ID(scriptid)不能为空");
        }
        if (serviceSubtaskCountReq.getOptionText() == null) {
            return error("问题选项(optionText)不能为空");
        }
        LoginUser loginUser = getLoginUser();
        String orgid = loginUser.getUser().getOrgid();
        serviceSubtaskCountReq.setOrgid(orgid);
        // 与 getSfStatisticsCountDetails 保持相同的配置筛选口径
        String configKey = serviceSubtaskCountReq.getConfigKey();
        if (StringUtils.isNotEmpty(configKey)) {
            String configValue = sysConfigService.selectConfigByKey(configKey, orgid);
            serviceSubtaskCountReq.setConfigValue(configValue);
            if (StringUtils.isEmpty(configValue)) {
                String logInfo = "getSfStatisticsCountDetailsHyperLink-参数 " + configKey + " 未配置, 请配置好后重试";
                log.error(logInfo);
                return error(logInfo);
            }
        }
        return success(serviceSubtaskService.getSfStatisticsScriptDetailsHyperlink(serviceSubtaskCountReq));
    }
    /**
     * 发送短信模板
     */
@@ -817,48 +1056,125 @@
    }
    /**
     * 统计任务各种状态的数量(优化版)
     * 统计任务各种状态的数量(优化版)-【随访工作台】
     *
     * @param soonTimeoutHours 即将超时时间(小时),默认 48
     * @return Map<String, Object> 包含各状态统计数据
     * pendingVisitCount: 当前登录人截止今日待随访总量
     * failedVisitCount: 当前登录人随访任务失败总量
     * abnormalVisitVount: 当前登录人随访任务异常总量
     * allVisitCount: 当前登录人随访任务总量
     * soonTimeoutCount: 当前登录人即将超时数量(超时截止时间在 soonTimeoutHours 小时内)
     */
    @ApiOperation("获取当前用户随访数量统计")
    @PostMapping("/getCurrentUserServiceSubtaskCount")
    public Map<String, Object> getCurrentUserServiceSubtaskCount() {
        Map<String, Object> map = new HashMap<>();
        LoginUser loginUser = getLoginUser();
    public Map<String, Object> getCurrentUserServiceSubtaskCount(
            @RequestParam(value = "soonTimeoutHours", required = false, defaultValue = "48") Integer soonTimeoutHours) {
        LoginUser loginUser = SecurityUtils.getLoginUser();
        if (loginUser == null || loginUser.getUser() == null) {
            return new HashMap<>();
        }
        SysUser sysUser = loginUser.getUser();
        SysUserDept sysUserDept = new SysUserDept();
        sysUserDept.setUserId(sysUser.getUserId());
        sysUserDept.setOrgid(sysUser.getOrgid());
        sysUserDept.setCampusid(sysUser.getCampusid());
        sysUserDept.setDeptType("1");
        List<SysUserDept> sysUserDepts = taskSysUserDeptPort.selectSysUserDeptList(sysUserDept);
        SysUserDept sysUserWard = new SysUserDept();
        sysUserWard.setUserId(sysUser.getUserId());
        sysUserWard.setOrgid(sysUser.getOrgid());
        sysUserWard.setCampusid(sysUser.getCampusid());
        sysUserWard.setDeptType("2");
        List<SysUserDept> sysUserWards = taskSysUserDeptPort.selectSysUserDeptList(sysUserWard);
        List<String> deptCodes = sysUserDepts.stream().map(SysUserDept::getDeptCode).collect(Collectors.toList());
        List<String> wardCodes = sysUserWards.stream().map(SysUserDept::getDeptCode).collect(Collectors.toList());
        List<String> deptCodes = CollectionUtils.emptyIfNull(sysUser.getBelongDepts()).stream()
                .map(item -> item.get("deptCode"))
                .filter(Objects::nonNull) // 剔除null
                .map(Object::toString)
                .collect(Collectors.toList());
        List<String> wardCodes = CollectionUtils.emptyIfNull(sysUser.getBelongWards()).stream()
                .map(item -> item.get("districtCode"))
                .filter(Objects::nonNull) // 剔除null
                .map(Object::toString)
                .collect(Collectors.toList());
        ServiceSubtaskEntity entity = new ServiceSubtaskEntity();
        entity.setOrgid(sysUser.getOrgid());
        entity.setLeaveldeptcodes(deptCodes);
        entity.setLeavehospitaldistrictcodes(wardCodes);
        if (ObjectUtils.isNotEmpty(sysUser)) {
            if (!("admin").equals(sysUser.getUserName())) {
                if (ObjectUtils.isNotEmpty(sysUserWards) && ObjectUtils.isNotEmpty(sysUserDepts)) {
                    map = serviceSubtaskService.getCurrentUserServiceSubtaskCount(entity);
                }
            } else {
                map = serviceSubtaskService.getCurrentUserServiceSubtaskCount(entity);
            }
        entity.setSoonTimeoutHours(soonTimeoutHours);
        // admin 查看全院;非 admin 只要配置了科室或病区任一维度即可查询(SQL 中为 OR 关系)
        boolean isAdmin = "admin".equals(sysUser.getUserName());
        if (isAdmin || CollectionUtils.isNotEmpty(deptCodes) || CollectionUtils.isNotEmpty(wardCodes)) {
            return serviceSubtaskService.getCurrentUserServiceSubtaskCount(entity);
        }
        return map;
        // 非 admin 且未配置任何科室/病区权限时,返回默认 0 值,避免前端取不到 key
        Map<String, Object> empty = new HashMap<>();
        empty.put("pendingVisitCount", 0);
        empty.put("failedVisitCount", 0);
        empty.put("abnormalVisitCount", 0);
        empty.put("warnningVisitCount", 0);
        empty.put("allVisitCount", 0);
        empty.put("soonTimeoutCount", 0);
        return empty;
    }
    /**
     * 当前登录人待处理随访任务统计(按出院年月分组,出院随访、门诊随访分开计数)
     * <p>
     * 口径:service_subtask 中
     * service_type = '2' 出院随访 / '3' 门诊随访
     * + 待随访状态(sendstate in 1,2,3,7,发送失败(5) 不计入)
     * + 出院满 7 天(含「7 天前那一天」出院的患者)
     * + 只看最近 4 个出院月份(含当月);
     * 科室/病区权限取 OR(与随访列表 deptOrDistrict=2 一致)。
     *
     * @return { userName, nickName, total, months: [{ month: 'yyyy-MM', dischargeCount, outpatientCount }] }
     * 只返回有数据的月份,没有数据的月份由前端补 0
     */
    @ApiOperation("获取当前登录人待处理随访任务统计(按出院年月)")
    @PostMapping("/pendingTaskMonthStats")
    public Map<String, Object> pendingTaskMonthStats() {
        LoginUser loginUser = getLoginUser();
        SysUser sysUser = loginUser.getUser();
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("userName", sysUser.getUserName());
        result.put("nickName", sysUser.getNickName());
        result.put("total", 0L);
        result.put("months", new ArrayList<>());
        // 注意:不要用 sys_user_dept.campusid 过滤——这张表的 campusid 大量为 NULL,
        // 带上它会直接查不出权限,统计结果永远是 0。院区权限由 orgid + userId + deptType 已能定位。
        SysUserDept deptQuery = new SysUserDept();
        deptQuery.setUserId(sysUser.getUserId());
        deptQuery.setOrgid(sysUser.getOrgid());
        deptQuery.setDeptType("1");
        List<SysUserDept> sysUserDepts = sysUserDeptService.selectSysUserDeptList(deptQuery);
        SysUserDept wardQuery = new SysUserDept();
        wardQuery.setUserId(sysUser.getUserId());
        wardQuery.setOrgid(sysUser.getOrgid());
        wardQuery.setDeptType("2");
        List<SysUserDept> sysUserWards = sysUserDeptService.selectSysUserDeptList(wardQuery);
        boolean isAdmin = "admin".equals(sysUser.getUserName());
        // 没有科室/病区配置的账号(如部分管理员账号),列表本身是按 机构+院区 查的,
        // 这里也按同样口径统计,否则列表有数据、卡片却全是 0。
        if (!isAdmin && CollectionUtils.isEmpty(sysUserDepts) && CollectionUtils.isEmpty(sysUserWards)) {
            log.info("[pendingTaskMonthStats] 用户 {} 未配置科室/病区,按机构+院区统计", sysUser.getUserName());
        }
        // sys_user_dept 里同一科室/病区会重复很多行,去重后再拼 IN,避免 SQL 参数过多
        List<String> deptCodes = sysUserDepts.stream()
                .map(SysUserDept::getDeptCode)
                .filter(Objects::nonNull)
                .distinct()
                .collect(Collectors.toList());
        List<String> wardCodes = sysUserWards.stream()
                .map(SysUserDept::getDeptCode)
                .filter(Objects::nonNull)
                .distinct()
                .collect(Collectors.toList());
        ServiceSubtaskEntity entity = new ServiceSubtaskEntity();
        entity.setOrgid(sysUser.getOrgid());
        // 与列表一致:按登录会话里的院区过滤(登录页选择的院区)
        entity.setCampusid(sysUser.getCampusid());
        entity.setLeaveldeptcodes(deptCodes);
        entity.setLeavehospitaldistrictcodes(wardCodes);
        Map<String, Object> stats = serviceSubtaskService.getPendingManualTaskMonthStats(entity);
        if (stats != null) {
            result.putAll(stats);
        }
        return result;
    }
    @ApiOperation("获取当前用户宣教数量统计")
@@ -884,6 +1200,17 @@
    @GetMapping(value = "/smsSubTask/{subid}")
    public Map<String, Object> smsSubTask(@PathVariable("subid") Long subid) {
        return serviceSubtaskService.smsSubTask(subid);
    }
    /**
     * 手动发送问卷模板的微信功能
     *
     * @param subid
     * @return
     */
    @GetMapping(value = "/weChatSubTask/{subid}")
    public Map<String, Object> weChatSubTask(@PathVariable("subid") Long subid) {
        return serviceSubtaskService.weChatSubTask(subid);
    }
    /**
@@ -971,32 +1298,40 @@
            userId = loginUser.getUser().getUserId();
        }
        if (serviceSubtaskEntity.getSendstateView() != null) {
            //            1:待随访(1 被领取、2 待发送、3 已发送、5 发送失败、7、超时);2:已完成( 6 已完成)、3:无需随访(4 不执行)
            //            1:待随访(1 被领取、2 待发送、3 已发送、7 超时;发送失败(5) 单独统计);2:已完成( 6 已完成)、3:无需随访(4 不执行)
            if (serviceSubtaskEntity.getSendstateView() == 1)
                serviceSubtaskEntity.setSendstates(new ArrayList<>(Arrays.asList(1L, 2L, 3L, 5L, 7L)));
                serviceSubtaskEntity.setSendstates(new ArrayList<>(Arrays.asList(1L, 2L, 3L, 7L)));
            if (serviceSubtaskEntity.getSendstateView() == 2)
                serviceSubtaskEntity.setSendstates(new ArrayList<>(Arrays.asList(6L)));
            if (serviceSubtaskEntity.getSendstateView() == 3)
                serviceSubtaskEntity.setSendstates(new ArrayList<>(Arrays.asList(4L)));
        }
        if (ObjectUtils.isNotEmpty(userId)) {
            long tRedis = System.currentTimeMillis();
            Map<String, Object> redisMap = redisCache.getCacheObject(userId + "patItemCount");
            log.info("[patItem耗时] 4.1 Redis取count缓存={}ms", System.currentTimeMillis() - tRedis);
            Boolean redisFlag = false;
            if (MapUtils.isNotEmpty(redisMap)) {
                long tCmp = System.currentTimeMillis();
                ServiceSubtaskEntity oldCondition = (ServiceSubtaskEntity) redisMap.get("searchCondition");
                if (ObjectUtils.isNotEmpty(oldCondition) && isSameCondition(oldCondition, serviceSubtaskEntity)) {
                    redisFlag = true;
                    map = redisMap;
                    redisCache.setCacheObject(userId + "patItemCount", map, 60, TimeUnit.MINUTES);
                }
                log.info("[patItem耗时] 4.2 条件比对={}ms,命中={}", System.currentTimeMillis() - tCmp, redisFlag);
            }
            if (!redisFlag) {
                long tCalc = System.currentTimeMillis();
                map = serviceSubtaskService.patItemCount(serviceSubtaskEntity);
                log.info("[patItem耗时] 4.3 count聚合(未命中缓存,走库)={}ms", System.currentTimeMillis() - tCalc);
                map.put("searchCondition", serviceSubtaskEntity);
                redisCache.setCacheObject(userId + "patItemCount", map, 120, TimeUnit.MINUTES);
            }
        } else {
            long tCalc = System.currentTimeMillis();
            map = serviceSubtaskService.patItemCount(serviceSubtaskEntity);
            log.info("[patItem耗时] 4.3 count聚合(无登录人,走库)={}ms", System.currentTimeMillis() - tCalc);
        }
        return map;
    }