已修改21个文件
813 ■■■■■ 文件已修改
ruoyi-admin/src/main/java/com/ruoyi/web/controller/smartor/ServiceSubtaskController.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/smartor/ServiceSubtaskDetailController.java 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/RyTask.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/java/com/smartor/common/QwenLLMUtil.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/java/com/smartor/domain/PatMedInhosp.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/java/com/smartor/domain/ServiceSubtask.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/java/com/smartor/domain/ServiceSubtaskRes.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/java/com/smartor/domain/VO/MzMydScoreRankVO.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/java/com/smartor/domain/VO/ServiceSubtaskVO.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/java/com/smartor/domain/entity/ServiceSubtaskEntity.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/java/com/smartor/mapper/ServiceSubtaskMapper.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/java/com/smartor/service/IServiceSubtaskService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/java/com/smartor/service/impl/PatMedInhospServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/java/com/smartor/service/impl/ServiceSubtaskAnswerServiceImpl.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/java/com/smartor/service/impl/ServiceSubtaskServiceImpl.java 313 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/resources/mapper/smartor/IvrLibaTemplateScriptMapper.xml 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/resources/mapper/smartor/PatArchiveOutMapper.xml 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/resources/mapper/smartor/PatMedInhospMapper.xml 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/resources/mapper/smartor/ServiceSubtaskMapper.xml 147 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
smartor/src/main/resources/mapper/smartor/SvyLibTemplateScriptMapper.xml 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/smartor/ServiceSubtaskController.java
@@ -23,6 +23,7 @@
import com.ruoyi.quartz.task.RyTask;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.system.service.ISysUserDeptService;
import com.smartor.common.MydOptionRatioChartBuilder;
import com.smartor.domain.*;
import com.smartor.domain.DTO.QuestionResultDTO;
import com.smartor.domain.VO.*;
@@ -221,16 +222,22 @@
                    serviceSubtaskExport.setEndDay(endDay);
                }
            }
            SltdMydTotalVO sltdMydTotalVO = new SltdMydTotalVO();
            sltdMydTotalVO.setServiceTypeList(Arrays.asList(serviceSubtaskVO.getServiceType()));
            sltdMydTotalVO.setEndOutHospTime(serviceSubtaskVO.getEndOutHospTime());
            sltdMydTotalVO.setStartOutHospTime(serviceSubtaskVO.getStartOutHospTime());
            serviceSubtaskDetailRatioExports = serviceSubtaskService.getDetailRatio(sltdMydTotalVO, serviceSubtaskList);
            //问题详情占比:只统计本次导出的这批患者,数量=选该选项的患者数,比例=占该题作答人数
            List<Long> subtaskIds = new ArrayList<>();
            for (ServiceSubtaskRes serviceSubtaskRes : serviceSubtaskList) {
                if (serviceSubtaskRes.getId() != null) {
                    subtaskIds.add(serviceSubtaskRes.getId());
                }
            }
            serviceSubtaskDetailRatioExports = serviceSubtaskService.statMydQuestionOptionForExport(subtaskIds, null);
        }
        ExcelUtil<ServiceSubtaskExport> util = new ExcelUtil<ServiceSubtaskExport>(ServiceSubtaskExport.class);
        util.exportExcelTwoSheet(response, serviceSubtaskExports, "患者随访信息表单", serviceSubtaskDetailRatioExports, "问题详情占比", ServiceSubtaskDetailRatioExport.class);
        //第三个Sheet:问题详情占比图表(按任务名称分组,一个任务一张柱状图)
        util.exportExcelTwoSheetWithChart(response, serviceSubtaskExports, "患者随访信息表单",
                serviceSubtaskDetailRatioExports, "问题详情占比", ServiceSubtaskDetailRatioExport.class,
                new MydOptionRatioChartBuilder(serviceSubtaskDetailRatioExports));
    }
    /**
ruoyi-admin/src/main/java/com/ruoyi/web/controller/smartor/ServiceSubtaskDetailController.java
@@ -26,6 +26,7 @@
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.stream.Collectors;
@@ -204,9 +205,14 @@
    /**
     * 查询患者随访信息
     * 省立同德满意度数据统计(按题目统计选项数量/占比,并给出该题的综合得分)
     * <p>
     * 综合满意度 = Σ(选项分值 × 项数) / Σ项数 / 本题最高分值 × 100%,
     * 分值取 1、0.8、0.6、0.4、0.2 五档(很满意、满意、一般、不满意、很不满意);
     * 「不适用」、0 分、空值等既不进分子也不进分母。
     */
    @ApiOperation("省立同德满意度数据统计")
    @AddOrgId(field = "orgid", paramIndex = 0, campusField = "campusid")
    @PostMapping("/sltdMydTotalByScore")
    public Map<String, Object> sltdMydTotalByScore(@RequestBody SltdMydTotalVO sltdMydTotalVO) {
        List<ServiceSubtaskRes> serviceSubtaskResList = null;
@@ -220,6 +226,9 @@
                serviceSubtaskEntity.setDeptOrDistrict(sltdMydTotalVO.getDeptOrDistrict());
                serviceSubtaskEntity.setLeavehospitaldistrictcodes(sltdMydTotalVO.getLeavehospitaldistrictcodes());
                serviceSubtaskEntity.setLeaveldeptcodes(sltdMydTotalVO.getLeaveldeptcodes());
                //机构/院区由 @AddOrgId 注入,落到查询条件上,避免跨机构取数
                serviceSubtaskEntity.setOrgid(sltdMydTotalVO.getOrgid());
                serviceSubtaskEntity.setCampusid(sltdMydTotalVO.getCampusid());
                if (serviceSubtaskEntity != null) {
                    List<ServiceSubtask> serviceSubtasks = serviceSubtaskService.getServiceSubtasks(serviceSubtaskEntity);
                    List<ServiceSubtaskRes> serviceSubtaskRes = DtoConversionUtils.sourceToTarget(serviceSubtasks, ServiceSubtaskRes.class);
@@ -236,6 +245,10 @@
        if (CollectionUtils.isNotEmpty(serviceSubtaskResList)) {
            List<ServiceSubtaskDetailRatioExport> detailList = serviceSubtaskService.getDetailRatio(sltdMydTotalVO, serviceSubtaskResList);
            if (detailList == null) {
                //serviceType 组合不在支持范围内(如同时含随访 2 和满意度 6/14),给明确提示而不是悄悄返回空数组
                return error("当前 serviceType 组合暂不支持统计问题占比,请分开查询(仅支持随访 2 或满意度 6/14)");
            }
            if (CollectionUtils.isNotEmpty(detailList)) {
                Map<String, List<ServiceSubtaskDetailRatioExport>> groupedMap = detailList.stream().collect(Collectors.groupingBy(ServiceSubtaskDetailRatioExport::getQuestiontext));
@@ -243,17 +256,8 @@
                    QuestionResultDTO questionResultDTO = new QuestionResultDTO();
                    questionResultDTO.setQeustionText(questionText);
                    questionResultDTO.setSubtaskDetailRatioExportList(exportList);
                    BigDecimal allCount = new BigDecimal("0");
                    BigDecimal allScore = new BigDecimal("0.00");
                    for (ServiceSubtaskDetailRatioExport ssdre : exportList) {
                        if (StringUtils.isNotEmpty(ssdre.getCount()))
                            allCount.add(BigDecimal.valueOf(Long.valueOf(ssdre.getCount())));
                        allScore.add(BigDecimal.valueOf(Double.valueOf(ssdre.getScore())));
                    }
                    if (allCount.compareTo(BigDecimal.ZERO) > 0)
                        questionResultDTO.setAvgScore(allScore.divide(allCount, 2).toString());
                    //综合得分:Σ(分值×项数) / Σ项数 / 本题最高分值 × 100%,不适用/0分/空值不进分子也不进分母
                    questionResultDTO.setAvgScore(calcMydScore(exportList));
                    result.add(questionResultDTO);
                });
            }
@@ -287,4 +291,77 @@
        return success(serviceSubtaskService.mzMydScoreRank(mzMydScoreRankVO));
    }
    /**
     * 多选题的问题类型
     */
    private static final String VALUE_TYPE_MULTIPLE = "2";
    /**
     * 计算一道题的满意度综合得分(百分制,保留两位小数)
     * <p>
     * 综合满意度 = Σ(选项分值 × 项数) / Σ项数 / 本题最高分值 × 100%
     * 等价于 Σ(很满意×1 + 满意×0.8 + 一般×0.6 + 不满意×0.4 + 很不满意×0.2) / Σ(五档项数) × 100%。
     * 用本题最高分值归一化,兼容明细分值存 0-1 权重(1、0.8…)和存 5 分制(5、4…)两种口径。
     *
     * @param exportList 同一道题目的选项统计明细
     * @return 百分制综合得分,无有效项数时返回 "0.00"
     */
    private String calcMydScore(List<ServiceSubtaskDetailRatioExport> exportList) {
        if (CollectionUtils.isEmpty(exportList)) return "0.00";
        BigDecimal scoreSum = BigDecimal.ZERO;   //分子:Σ(选项分值 × 项数)
        BigDecimal countSum = BigDecimal.ZERO;   //分母:Σ项数
        BigDecimal maxScore = null;              //本题最高分值,用于归一化
        for (ServiceSubtaskDetailRatioExport detail : exportList) {
            //多选题一行是多个选项的分值之和,分母只算 1 项,会把得分算高,不计入综合得分
            if (VALUE_TYPE_MULTIPLE.equals(detail.getValueType())) continue;
            //「不适用」不在公式的五档里,不计入综合得分
            if (isNotApplicable(detail)) continue;
            BigDecimal score = parseDecimal(detail.getScore());
            BigDecimal count = parseDecimal(detail.getCount());
            if (score == null || count == null) continue;
            //0 分(未答/空答)、空数量:既不进分子也不进分母
            if (score.signum() <= 0 || count.signum() <= 0) continue;
            if (maxScore == null || score.compareTo(maxScore) > 0) maxScore = score;
            scoreSum = scoreSum.add(score.multiply(count));
            countSum = countSum.add(count);
        }
        if (maxScore == null || countSum.signum() <= 0) return "0.00";
        return scoreSum.divide(countSum, 6, RoundingMode.HALF_UP)
                .divide(maxScore, 6, RoundingMode.HALF_UP)
                .multiply(BigDecimal.valueOf(100))
                .setScale(2, RoundingMode.HALF_UP)
                .toPlainString();
    }
    /**
     * 是否「不适用」选项
     */
    private boolean isNotApplicable(ServiceSubtaskDetailRatioExport detail) {
        return containsNotApplicable(detail.getOptionresult()) || containsNotApplicable(detail.getTargetvalue());
    }
    private boolean containsNotApplicable(String optionText) {
        if (StringUtils.isEmpty(optionText)) return false;
        String text = optionText.trim();
        return text.contains("不适用") || text.contains("未答") || text.contains("拒答");
    }
    /**
     * 字符串转 BigDecimal,空值/非法值返回 null
     */
    private BigDecimal parseDecimal(String value) {
        if (StringUtils.isEmpty(value)) return null;
        try {
            return new BigDecimal(value.trim());
        } catch (NumberFormatException e) {
            return null;
        }
    }
}
ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java
@@ -474,6 +474,67 @@
    }
    /**
     * 图表Sheet构建回调:由调用方在给定工作簿上追加一个(或多个)带图表的Sheet
     */
    public interface ChartSheetBuilder {
        void build(XSSFWorkbook workbook);
    }
    /**
     * 取底层 XSSFWorkbook(SXSSF 流式工作簿也支持挂原生图表),不支持时返回 null
     */
    public XSSFWorkbook getXssfWorkbook() {
        if (wb instanceof SXSSFWorkbook) {
            return ((SXSSFWorkbook) wb).getXSSFWorkbook();
        }
        if (wb instanceof XSSFWorkbook) {
            return (XSSFWorkbook) wb;
        }
        return null;
    }
    /**
     * 导出两个数据Sheet + 一个图表Sheet(图表内容由调用方通过 builder 追加)
     *
     * @param response   返回数据
     * @param list1      第一个Sheet的数据集合
     * @param sheetName1 第一个Sheet的名称
     * @param list2      第二个Sheet的数据集合
     * @param sheetName2 第二个Sheet的名称
     * @param clazz2     第二个Sheet数据对象的Class
     * @param chartBuilder 图表Sheet构建回调,可为空(为空时与两Sheet导出完全一样)
     */
    public void exportExcelTwoSheetWithChart(HttpServletResponse response,
                                             List<T> list1, String sheetName1,
                                             List<?> list2, String sheetName2, Class<?> clazz2,
                                             ChartSheetBuilder chartBuilder) {
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        // 初始化并写入Sheet1
        this.init(list1, sheetName1, StringUtils.EMPTY, Type.EXPORT);
        writeSheet();
        // 追加写入Sheet2
        writeAdditionalSheet(list2, sheetName2, clazz2);
        // 追加图表Sheet
        if (chartBuilder != null) {
            XSSFWorkbook xssfWorkbook = getXssfWorkbook();
            if (xssfWorkbook != null) {
                chartBuilder.build(xssfWorkbook);
            } else {
                log.warn("当前工作簿不支持原生图表,跳过图表Sheet");
            }
        }
        // 输出到响应流
        try {
            wb.write(response.getOutputStream());
        } catch (Exception e) {
            log.error("导出Excel异常{}", e.getMessage());
        } finally {
            IOUtils.closeQuietly(wb);
        }
    }
    /**
     * 向当前工作簿追加写入一个Sheet
     *
     * @param dataList  数据集合
@@ -481,8 +542,7 @@
     * @param dataClazz 数据对象的Class
     */
    @SuppressWarnings("unchecked")
    public void writeAdditionalSheet(List<?> dataList, String sheetName, Class<?> dataClazz) {
        if (dataList == null) {
    public void writeAdditionalSheet(List<?> dataList, String sheetName, Class<?> dataClazz) {        if (dataList == null) {
            dataList = new ArrayList<>();
        }
        // 保存原始状态
ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/RyTask.java
@@ -324,7 +324,7 @@
                        log.info("【sendExceptInfo】发送人工号:{}", dutyDeptPersonCode);
                        SysUser sysUser = new SysUser();
                        SysUser sysUserVo = new SysUser();
                        sysUserVo.setUserCode(dutyDeptPersonCode);
                        sysUserVo.setUserName(dutyDeptPersonCode);
                        sysUserVo.setOrgid(serviceSubtaskDetailTrace.getOrgid());
                        List<SysUser> users = userService.getUserList(sysUserVo);
                        String phoneNumber = "";
smartor/src/main/java/com/smartor/common/QwenLLMUtil.java
@@ -33,7 +33,8 @@
    /** 百炼 API Key */
    @Value("${qwen.api-key:}")
    private static String apiKey = "sk-712da9346f0940ff909b40dce17579b1";
//    private static String apiKey = "sk-712da9346f0940ff909b40dce17579b1";
    private static String apiKey = "sk-712da9346f0940ff909b40dce17579b0";
    /** 模型名称 */
    @Value("${qwen.model:qwen-plus}")
smartor/src/main/java/com/smartor/domain/PatMedInhosp.java
@@ -400,6 +400,14 @@
    private Integer cry;
    /**
     * 入院床位排序:asc(或 1)=正序,desc(或 2)=倒序,不传=不排序
     * <p>
     * 只对「在院」患者查询(cry=0)生效,其它查询忽略该参数。
     */
    @ApiModelProperty("入院床位排序:asc/1=正序,desc/2=倒序,不传=不排序(仅 cry=0 在院患者生效)")
    private String bedSort;
    /**
     * 出院病区编号
     */
    @ApiModelProperty(value = "出院病区编号集合")
smartor/src/main/java/com/smartor/domain/ServiceSubtask.java
@@ -746,4 +746,11 @@
     */
    @ApiModelProperty(value = "所在循环轮次(非循环任务为0)")
    private Long loopCount;
    /**
     * 人工电话标记
     */
    @Excel(name = " 人工电话标记 ")
    @ApiModelProperty(value = "是否人工打过电话 0-没打过 1-打过")
    private Long isManuallyCalled;
}
smartor/src/main/java/com/smartor/domain/ServiceSubtaskRes.java
@@ -690,4 +690,11 @@
    @ApiModelProperty(value = "继续时间下次")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date continueTimeNext;
    /**
     * 人工电话标记
     */
    @Excel(name = " 人工电话标记 ")
    @ApiModelProperty(value = "是否人工打过电话 0-没打过 1-打过")
    private Long isManuallyCalled;
}
smartor/src/main/java/com/smartor/domain/VO/MzMydScoreRankVO.java
@@ -33,7 +33,7 @@
     * 服务类型,默认 14(门诊满意度调查)
     */
    @ApiModelProperty(value = "服务类型,默认14(门诊满意度调查)")
    private String serviceType = "14";
    private List<String> serviceTypes;
    /**
     * 科室编号集合,为空表示全部科室(按科室统计时使用)
@@ -68,4 +68,10 @@
     */
    @ApiModelProperty(value = "只返回前N名,为空或小于等于0表示返回全部")
    private Integer limit;
    /**
     * 问题内容
     */
    @ApiModelProperty(value = "问题内容")
    private String questionText;
}
smartor/src/main/java/com/smartor/domain/VO/ServiceSubtaskVO.java
@@ -217,6 +217,7 @@
     * 发送日期
     */
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @Excel(name = " 发送日期 ", width = 30, dateFormat = "yyyy-MM-dd")
    @ApiModelProperty(value = "发送日期")
    private Date senddate;
@@ -289,6 +290,7 @@
     * 完成时间
     */
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @Excel(name = " 完成时间 ", width = 30, dateFormat = "yyyy-MM-dd")
    @ApiModelProperty(value = "完成时间")
    private Date finishtime;
@@ -329,6 +331,7 @@
     * 拉取时间
     */
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @Excel(name = " 拉取时间 ", width = 30, dateFormat = "yyyy-MM-dd")
    @ApiModelProperty(value = "拉取时间")
    private Date pulltime;
@@ -412,6 +415,7 @@
     * 上传时间
     */
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @Excel(name = " 上传时间 ", width = 30, dateFormat = "yyyy-MM-dd")
    @ApiModelProperty(value = "上传时间")
    private Date uploadTime;
@@ -686,6 +690,7 @@
     * 开始出院日期
     */
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @ApiModelProperty(value = "开始出院日期")
    private Date startOutHospTime;
@@ -693,6 +698,7 @@
     * 开始发送日期
     */
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @ApiModelProperty(value = "开始发送日期")
    private Date startSendDateTime;
@@ -700,6 +706,7 @@
     * 结束发送日期
     */
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @ApiModelProperty(value = "结束发送日期")
    private Date endSendDateTime;
@@ -707,6 +714,7 @@
     * 结束出院日期
     */
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @ApiModelProperty(value = "结束出院日期")
    private Date endOutHospTime;
smartor/src/main/java/com/smartor/domain/entity/ServiceSubtaskEntity.java
@@ -774,6 +774,13 @@
    @ApiModelProperty(value = "异常预警:0绿色;1红色;2黄色;3已处理")
    private Integer isabnormal;
    /**
     * 人工电话标记
     */
    @Excel(name = " 人工电话标记 ")
    @ApiModelProperty(value = "是否人工打过电话 0-没打过 1-打过")
    private Long isManuallyCalled;
    @ApiModelProperty(value = "随访部门编码集合")
    private List<String> visitDeptCodes;
smartor/src/main/java/com/smartor/mapper/ServiceSubtaskMapper.java
@@ -4,6 +4,7 @@
import com.smartor.domain.DTO.MydTrendReqDTO;
import com.smartor.domain.DTO.MzMydScoreRankDTO;
import com.smartor.domain.DTO.QuestionCountKsOrBqDTO;
import com.smartor.domain.DTO.ServiceSubtaskOptionStatDTO;
import com.smartor.domain.VO.PatSatisfactionReqVO;
import com.smartor.domain.VO.HeLibraryCountVO;
import com.smartor.domain.VO.MzMydScoreRankVO;
@@ -269,11 +270,23 @@
     * 统计问题的选项的百分比
     *
     * @param taskIds          任务 ID 列表 (用于服务类型 2)
     * @param subtaskIds       子任务 ID 列表(可选)。传了就只统计这批子任务(即本次导出的患者),
     *                         不传则统计这些任务下的全部答卷,保持原行为
     * @param startOutHospTime
     * @param endOutHospTime
     * @return
     */
    List<ServiceSubtaskDetailRatioExport> statQuestionOption(@Param("taskIds") List<Long> taskIds, @Param("startOutHospTime") Date startOutHospTime, @Param("endOutHospTime") Date endOutHospTime, @Param("startFinishTime") Date startFinishTime, @Param("endFinishTime") Date endFinishTime, @Param("scoreStart") Double scoreStart, @Param("scoreEnd") Double scoreEnd, @Param("questiontext") String questiontext, @Param("valueType") String valueType);
    List<ServiceSubtaskDetailRatioExport> statQuestionOption(@Param("taskIds") List<Long> taskIds, @Param("startOutHospTime") Date startOutHospTime, @Param("endOutHospTime") Date endOutHospTime, @Param("startFinishTime") Date startFinishTime, @Param("endFinishTime") Date endFinishTime, @Param("scoreStart") Double scoreStart, @Param("scoreEnd") Double scoreEnd, @Param("questiontext") String questiontext, @Param("valueType") String valueType, @Param("subtaskIds") List<Long> subtaskIds);
    /**
     * 导出「问题详情占比」专用统计(按本次导出的子任务统计)
     * <p>
     * 数量 = 选择该选项的患者数(同一患者同一题只取最新一条明细),分数 = 该选项明细的 score 之和。
     *
     * @param subtaskIds   本次导出的子任务 ID(必传,为空则查不到数据)
     * @param questiontext 问题内容(可选,模糊匹配)
     */
    List<ServiceSubtaskOptionStatDTO> statMydQuestionOptionForExport(@Param("subtaskIds") List<Long> subtaskIds, @Param("questiontext") String questiontext);
    /**
     * 根据子任务 ID 统计问题的选项的百分比(用于满意度统计 serviceType=6)
smartor/src/main/java/com/smartor/service/IServiceSubtaskService.java
@@ -223,10 +223,22 @@
    /**
     * 统计问题的选项的百分比 (用于服务类型 2)
     *
     * @param taskIds
     * @param taskIds    任务 ID 列表
     * @param subtaskIds 子任务 ID 列表(可选):传了就只统计这批子任务(本次导出的患者),分母也用同一批数据
     * @return
     */
    public List<ServiceSubtaskDetailRatioExport> statQuestionOption(List<Long> taskIds, Date startFinishTime, Date endFinishTime, Date startOutHospTime, Date endOutHospTime, String scriptContent, Double scoreStart, Double scoreEnd, String valueType);
    public List<ServiceSubtaskDetailRatioExport> statQuestionOption(List<Long> taskIds, Date startFinishTime, Date endFinishTime, Date startOutHospTime, Date endOutHospTime, String scriptContent, Double scoreStart, Double scoreEnd, String valueType, List<Long> subtaskIds);
    /**
     * 导出「问题详情占比」专用统计(只统计本次导出的这批患者)
     * <p>
     * 数量 = 选择该选项的患者数;比例 = 数量 / 该题作答患者数;分数 = 该选项明细 score 之和;
     * 任务名称/任务编号 = service_task.task_name / taskid。
     *
     * @param subtaskIds   本次导出的子任务 ID
     * @param questiontext 问题内容(可选)
     */
    public List<ServiceSubtaskDetailRatioExport> statMydQuestionOptionForExport(List<Long> subtaskIds, String questiontext);
    /**
     * 根据子任务 ID 统计问题的选项的百分比(用于满意度统计 serviceType=6)
smartor/src/main/java/com/smartor/service/impl/PatMedInhospServiceImpl.java
@@ -99,6 +99,8 @@
     */
    @Override
    public List<PatMedInhosp> selectPatMedInhospList(PatMedInhosp patMedInhosp) {
        //入院床位排序只在「在院」查询(cry=0)下生效,其它查询一律忽略,保证原有排序不变
        patMedInhosp.setBedSort(normalizeBedSort(patMedInhosp.getCry(), patMedInhosp.getBedSort()));
        if (patMedInhosp.getCry() != null && patMedInhosp.getCry() == 0) {
            patMedInhosp.setDeptcodeList(patMedInhosp.getLeaveldeptcodes());
            patMedInhosp.setHospitaldistrictcodeList(patMedInhosp.getLeavehospitaldistrictcodes());
@@ -121,6 +123,24 @@
        return patMedInhospList;
    }
    /**
     * 归一化入院床位排序参数
     * <p>
     * asc / 1 -> asc(床位正序),desc / 2 -> desc(床位倒序),其余(含不传)-> null(不排序)。
     * 只对「在院」患者查询(cry=0)生效,出院/转科等查询忽略该参数。
     *
     * @param cry     出入院标识:0入院 1出院 2转科 3预入院
     * @param bedSort 前端传入的床位排序值
     * @return 归一化后的排序值(asc / desc / null)
     */
    private String normalizeBedSort(Integer cry, String bedSort) {
        if (cry == null || cry != 0 || StringUtils.isEmpty(bedSort)) return null;
        String value = bedSort.trim().toLowerCase();
        if ("asc".equals(value) || "1".equals(value)) return "asc";
        if ("desc".equals(value) || "2".equals(value)) return "desc";
        return null;
    }
    @Override
    public List<PatMedInhospDTO> selectPatMedInhospByApplication(PatMedInhosp patMedInhosp) {
        List<PatMedInhosp> patMedInhosps = selectPatMedInhospList(patMedInhosp);
smartor/src/main/java/com/smartor/service/impl/ServiceSubtaskAnswerServiceImpl.java
@@ -1146,7 +1146,7 @@
        //如何这需要服务补偿的服务ID不为空的话,则进行立即发送
        log.error("------svyTaskTemplateScriptVO.getSendTaskids()的值为:{},selectServiceSubtaskList的值为:{}", svyTaskTemplateScriptVO.getSendTaskids(), selectServiceSubtaskList.size());
        log.info("------svyTaskTemplateScriptVO.getSendTaskids()的值为:{},selectServiceSubtaskList的值为:{}", svyTaskTemplateScriptVO.getSendTaskids(), selectServiceSubtaskList.size());
        if (CollectionUtils.isNotEmpty(svyTaskTemplateScriptVO.getSendTaskids()) && CollectionUtils.isNotEmpty(selectServiceSubtaskList)) {
            sendTaskIds(svyTaskTemplateScriptVO.getSendTaskids(), selectServiceSubtaskList.get(0));
        }
smartor/src/main/java/com/smartor/service/impl/ServiceSubtaskServiceImpl.java
@@ -10,9 +10,11 @@
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.core.service.IConfigService;
import com.ruoyi.common.dx.MessageSend;
import com.ruoyi.common.enums.WxGZHEnum;
import com.ruoyi.common.exception.base.BaseException;
import com.ruoyi.common.utils.*;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.common.utils.sms.smsUtils;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.system.domain.SysConfig;
import com.ruoyi.system.mapper.SysConfigMapper;
@@ -25,6 +27,7 @@
import com.smartor.domain.DTO.MzMydScoreRankDTO;
import com.smartor.domain.DTO.QuestionCountKsOrBqDTO;
import com.smartor.domain.DTO.ServiceSubtaskDetailDTO;
import com.smartor.domain.DTO.ServiceSubtaskOptionStatDTO;
import com.smartor.domain.*;
import com.smartor.domain.VO.*;
import com.smartor.domain.entity.ServiceSubtaskEntity;
@@ -46,6 +49,9 @@
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.time.*;
@@ -56,6 +62,8 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static cn.hutool.core.convert.Convert.toHex;
/**
 * 单一任务(随访)Service业务层处理
@@ -1056,8 +1064,8 @@
            //将任务信息放到服务表中
            ServiceSubtask serviceSubtask = DtoConversionUtils.sourceToTarget(serviceTaskVO, ServiceSubtask.class);
            serviceSubtask.setTaskid(serviceTask.getTaskid().longValue());
            serviceSubtask.setTemplatename(tempName);
            serviceSubtask.setTemplateid(tempid);
            serviceSubtask.setTemplatename(ObjectUtils.isNotEmpty(tempid) ? tempName : serviceTask.getTemplatename());
            serviceSubtask.setTemplateid(ObjectUtils.isNotEmpty(tempid) ? tempid : serviceTask.getTemplateid());
            serviceSubtask.setLibtemplateid(ObjectUtils.isEmpty(serviceTask.getLibtemplateid()) ? null : Long.valueOf(serviceTask.getLibtemplateid()));
            serviceSubtask.setLibtemplatename(serviceTask.getLibtemplatename());
            //新增
@@ -5096,6 +5104,12 @@
        String subId = rsaPublicKeyExample.encryptedData(serviceSubtask.getId().toString(), pub_key);
        String taskId = rsaPublicKeyExample.encryptedData(serviceSubtask.getTaskid().toString(), pub_key);
        //丽水中医院发送的是微信公众号
        if (StringUtils.isNotEmpty(serviceSubtask.getOrgid()) && serviceSubtask.getOrgid().equals("47231022633110211A2101")) {
            resultMap = wechatSubTask(subid);
            return resultMap;
        }
        //发送数据的封装
        ServiceOutPath serviceOutPath = new ServiceOutPath();
        serviceOutPath.setParam1(taskId);
@@ -5140,14 +5154,90 @@
        return resultMap;
    }
    public Map<String,Object> wechatSubTask(Long subid){
        log.info("wechatSubTask的入参为:{}", subid);
        Map<String, Object> resultMap = new HashMap<>();
        resultMap.put("code", "500");
        resultMap.put("msg", "发送失败");
        try{
            //获取随访的基本信息
            ServiceSubtask serviceSubtask = selectServiceSubtaskById(subid);
            //获取患者信息
            PatArchive patArchive = patArchiveMapper.selectPatArchiveByPatid(serviceSubtask.getPatid());
            RSAPublicKeyExample rsaPublicKeyExample = new RSAPublicKeyExample();
            String patid = rsaPublicKeyExample.encryptedData(serviceSubtask.getPatid().toString(), pub_key);
            String subId = rsaPublicKeyExample.encryptedData(serviceSubtask.getId().toString(), pub_key);
            String taskId = rsaPublicKeyExample.encryptedData(serviceSubtask.getTaskid().toString(), pub_key);
            String url = localIP + ":" + req_path + "/outsideChainwt?param1=" + taskId + "&param2=" + patid + "&param3="
                    + URLEncoder.encode(serviceSubtask.getTaskName(), StandardCharsets.UTF_8.toString()) + "&param5=false"
                    + "$param6=" + subId;
            ServiceOutPath serviceOutPath = new ServiceOutPath();
            serviceOutPath.setParam1(taskId);
            serviceOutPath.setParam2(patid);
            serviceOutPath.setParam3(serviceSubtask.getTaskName());
            serviceOutPath.setParam6(subId);
            serviceOutPath.setCreateTime(new Date());
            serviceOutPath.setOrgid(serviceSubtask.getOrgid());
            iServiceOutPathService.insertServiceOutPath(serviceOutPath);
            String format = String.format("%03X", serviceOutPath.getId());
            serviceOutPath.setRadix(format);
            serviceOutPath.setUpdateTime(new Date());
            serviceOutPath.setUrl(url);
            iServiceOutPathService.updateServiceOutPath(serviceOutPath);
            //获取微信公众号请求信息根据机构ID
            List<String> wxqqxx = WxGZHEnum.getDescByCode(serviceSubtask.getOrgid());
            if (CollectionUtils.isEmpty(wxqqxx) || wxqqxx.size() < 4) {
                resultMap.put("code", -1);
                resultMap.put("message", "发送异常:" + "【公众号】该机构的公众号配置信息不全,无法通过公众号发送");
                return resultMap;
            }
            if (StringUtils.isEmpty(serviceSubtask.getPatidHis())) {
                resultMap.put("code", -1);
                resultMap.put("message", "发送异常:" + "【公众号】公众号发送失败,his系统的患者id为空");
                return resultMap;
            }
            String wxCode = "";
            wxCode = getWXCode(serviceSubtask.getSfzh(), url, serviceSubtask.getTaskName(), serviceSubtask.getTaskDesc(), patArchive.getTelcode(), serviceSubtask.getSendname(), patArchive.getPatidHis(), wxqqxx);
            Map map = JSONObject.parseObject(wxCode, Map.class);
            Boolean wxCodeSuccess = false;
            if (!map.isEmpty() && map.get("succ") != null) {
                wxCodeSuccess = (boolean) map.get("succ");
            }
            //发送完成后,修改状态
            serviceSubtask.setSendstate(3L);
            if (wxCodeSuccess) {
                serviceSubtask.setResult("success");
                resultMap.put("code", "200");
                resultMap.put("msg", "发送成功");
            } else {
                log.error("【sfHandlle】公众号发送失败,患者信息:{},错误信息:{}", serviceSubtask, map.get("msg"));
                resultMap.put("code", -1);
                resultMap.put("message", "【公众号】公众号发送失败");
                return resultMap;
            }
        } catch (Exception e) {
            String guid = UUID.randomUUID().toString();
            log.error("【sfHandlle】长期任务公众号发送失败,原因:{},GUID:{}", e.getMessage(), guid);
            resultMap.put("code", -1);
            resultMap.put("message", "【公众号】长期任务公众号发送失败,原因:" + e.getMessage());
            return resultMap;
        }
        return resultMap;
    }
    @Override
    public List<ServiceSubtaskDetailRatioExport> statQuestionOption(List<Long> taskIds, Date startFinishTime, Date endFinishTime, Date startOutHospTime, Date endOutHospTime, String scriptContent, Double scoreStart, Double scoreEnd, String valueType) {
    public List<ServiceSubtaskDetailRatioExport> statQuestionOption(List<Long> taskIds, Date startFinishTime, Date endFinishTime, Date startOutHospTime, Date endOutHospTime, String scriptContent, Double scoreStart, Double scoreEnd, String valueType, List<Long> subtaskIds) {
        //先清理一下重复数据
        List<Long> repeatIds = serviceSubtaskDetailMapper.selectRepeatDetailIds();
        if (!CollectionUtils.isEmpty(repeatIds)) {
            serviceSubtaskDetailMapper.deleteRepeatDetailByIds(repeatIds);
        }
        List<ServiceSubtaskDetailRatioExport> serviceSubtaskDetailRatioExports = serviceSubtaskMapper.statQuestionOption(taskIds, startOutHospTime, endOutHospTime, startFinishTime, endFinishTime, scoreStart, scoreEnd, scriptContent, valueType);
        List<ServiceSubtaskDetailRatioExport> serviceSubtaskDetailRatioExports = serviceSubtaskMapper.statQuestionOption(taskIds, startOutHospTime, endOutHospTime, startFinishTime, endFinishTime, scoreStart, scoreEnd, scriptContent, valueType, subtaskIds);
        // 处理 targetvalue:按 & 分割成多条记录
        List<ServiceSubtaskDetailRatioExport> result = new ArrayList<>();
@@ -5157,7 +5247,9 @@
                // 如果有多条目标值,拆分成多行
                for (String value : values) {
                    //先去 result 中查一下,有没有这个 questiontext 和对应的 targetvalue
                    ServiceSubtaskDetailRatioExport existingExport = result.stream().filter(e -> e.getQuestiontext().equals(export.getQuestiontext()) && value.trim().equals(e.getTargetvalue())).findFirst().orElse(null);
                    //注意必须带上 taskid 一起匹配:否则会把别的任务的数据填到本任务的行上,
                    //导致导出的「数量/比例」和该行的「任务名称/任务编号」对不上(同一任务同一题出现两个不同分母)
                    ServiceSubtaskDetailRatioExport existingExport = result.stream().filter(e -> Objects.equals(e.getTaskid(), export.getTaskid()) && e.getQuestiontext().equals(export.getQuestiontext()) && value.trim().equals(e.getTargetvalue())).findFirst().orElse(null);
                    if (existingExport != null && existingExport.getRatio().equals("0") && existingExport.getCount().equals("0")) {
                        //如果存在,则将 result 里的这个问题所对应的 Targetvalue=value 这条数据更新成当前这条数据
                        if (value.equals(export.getOptionresult())) {
@@ -5186,6 +5278,152 @@
        }
        return result;
    }
    @Override
    public List<ServiceSubtaskDetailRatioExport> statMydQuestionOptionForExport(List<Long> subtaskIds, String questiontext) {
        List<ServiceSubtaskDetailRatioExport> result = new ArrayList<>();
        if (CollectionUtils.isEmpty(subtaskIds)) {
            return result;
        }
        List<ServiceSubtaskOptionStatDTO> statList = serviceSubtaskMapper.statMydQuestionOptionForExport(subtaskIds, questiontext);
        if (CollectionUtils.isEmpty(statList)) {
            return result;
        }
        return buildMydOptionRows(statList);
    }
    /**
     * 把「任务+题目+选项」的作答统计拼成导出的「问题详情占比」行
     * <ul>
     *     <li>选项清单:题目配置过的选项在 targetvalue(用 & 连接)里,全部列出,没人选的显示 0</li>
     *     <li>数量:患者选中的选项在 asrtext 里(多选题是「A&B」),每个选中项各记一次人数,一个患者只算一条明细</li>
     *     <li>比例:数量 / 该题作答患者数,所以单选题各选项数量之和 = 该题作答患者数</li>
     *     <li>分数:该选项明细的 score 之和;多选行按选中项份数均分,保证各选项分数之和 = 明细分数之和</li>
     * </ul>
     */
    private static List<ServiceSubtaskDetailRatioExport> buildMydOptionRows(List<ServiceSubtaskOptionStatDTO> statList) {
        List<ServiceSubtaskDetailRatioExport> result = new ArrayList<>();
        // 按「任务 + 题目」分组,保持 SQL 的顺序(先题目后任务)
        Map<String, List<ServiceSubtaskOptionStatDTO>> grouped = new LinkedHashMap<>();
        for (ServiceSubtaskOptionStatDTO stat : statList) {
            String key = stat.getTaskid() + "\u0001" + stat.getQuestiontext();
            grouped.computeIfAbsent(key, k -> new ArrayList<>()).add(stat);
        }
        for (List<ServiceSubtaskOptionStatDTO> group : grouped.values()) {
            ServiceSubtaskOptionStatDTO first = group.get(0);
            // 选项 -> 人数(long[1] 便于累加),顺序即出现顺序
            Map<String, long[]> countMap = new LinkedHashMap<>();
            Map<String, BigDecimal> scoreMap = new LinkedHashMap<>();
            for (ServiceSubtaskOptionStatDTO stat : group) {
                // targetvalue = 本题所有选项的清单,没人选的也要列出来
                List<String> options = splitOptions(stat.getTargetvalue());
                // asrtext = 患者实际选中的选项,多选题会是「选中项1&选中项2」
                List<String> chosenOptions = splitOptions(stat.getOptionresult());
                for (String option : options) {
                    countMap.computeIfAbsent(option, k -> new long[1]);
                    scoreMap.computeIfAbsent(option, k -> BigDecimal.ZERO);
                }
                long patientCount = stat.getPatientCount() == null ? 0L : stat.getPatientCount();
                BigDecimal scoreSum = stat.getScoreSum() == null ? BigDecimal.ZERO : stat.getScoreSum();
                if (!chosenOptions.isEmpty()) {
                    BigDecimal[] scoreParts = splitScore(scoreSum, chosenOptions.size());
                    for (int i = 0; i < chosenOptions.size(); i++) {
                        String chosen = chosenOptions.get(i);
                        // 选中的选项可能不在配置清单里(历史数据),补一行
                        countMap.computeIfAbsent(chosen, k -> new long[1]);
                        scoreMap.computeIfAbsent(chosen, k -> BigDecimal.ZERO);
                        countMap.get(chosen)[0] += patientCount;
                        scoreMap.put(chosen, scoreMap.get(chosen).add(scoreParts[i]));
                    }
                } else if (options.size() == 1) {
                    // 没有 asrtext(未答/无应答)时,只有唯一选项才能确定归属
                    countMap.get(options.get(0))[0] += patientCount;
                    scoreMap.put(options.get(0), scoreMap.get(options.get(0)).add(scoreSum));
                }
            }
            long total = 0L;
            for (long[] count : countMap.values()) total += count[0];
            for (Map.Entry<String, long[]> entry : countMap.entrySet()) {
                long count = entry.getValue()[0];
                ServiceSubtaskDetailRatioExport row = new ServiceSubtaskDetailRatioExport();
                row.setQuestiontext(first.getQuestiontext());
                row.setOptionresult(entry.getKey());
                row.setTargetvalue(entry.getKey());
                row.setCount(String.valueOf(count));
                row.setRatio(formatRatio(count, total));
                row.setScore(formatScore(scoreMap.get(entry.getKey())));
                row.setTaskname(first.getTaskName());
                row.setTaskid(first.getTaskid());
                row.setValueType(first.getValueType());
                result.add(row);
            }
        }
        return result;
    }
    /**
     * 把一条明细的分数按选中项份数均分,余数补给第一个,保证各份之和 = 原分数
     */
    private static BigDecimal[] splitScore(BigDecimal total, int parts) {
        BigDecimal[] result = new BigDecimal[parts];
        if (parts <= 0) {
            return result;
        }
        BigDecimal each = total.divide(BigDecimal.valueOf(parts), 2, RoundingMode.HALF_UP);
        for (int i = 0; i < parts; i++) {
            result[i] = each;
        }
        result[0] = result[0].add(total.subtract(each.multiply(BigDecimal.valueOf(parts))));
        return result;
    }
    /**
     * 拆分题目配置的选项(多个选项用 & 连接),顺带去掉全角/半角空白
     */
    private static List<String> splitOptions(String targetvalue) {
        List<String> options = new ArrayList<>();
        if (StringUtils.isEmpty(targetvalue)) {
            return options;
        }
        for (String part : targetvalue.split("&")) {
            String option = trimAll(part);
            if (StringUtils.isNotEmpty(option) && !options.contains(option)) {
                options.add(option);
            }
        }
        return options;
    }
    /**
     * 比例:数量 / 该题作答患者数,保留两位小数
     */
    private static String formatRatio(long count, long total) {
        if (total <= 0) {
            return "0.00%";
        }
        return BigDecimal.valueOf(count * 100.0 / total).setScale(2, RoundingMode.HALF_UP).toPlainString() + "%";
    }
    /**
     * 分数之和:去掉多余的小数位(0.00 -> 0)
     */
    private static String formatScore(BigDecimal score) {
        if (score == null) {
            return "0";
        }
        BigDecimal value = score.stripTrailingZeros();
        if (value.scale() < 0) {
            value = value.setScale(0);
        }
        return value.toPlainString();
    }
    /*
@@ -6092,17 +6330,31 @@
     */
    public List<ServiceSubtaskDetailRatioExport> getDetailRatio(SltdMydTotalVO sltdMydTotalVO, List<ServiceSubtaskRes> serviceSubtaskRes) {
        List<ServiceSubtaskDetailRatioExport> serviceSubtaskDetailRatioExports = null;
        if (CollectionUtils.isNotEmpty(sltdMydTotalVO.getServiceTypeList()) && sltdMydTotalVO.getServiceTypeList().contains("2") && (sltdMydTotalVO.getServiceTypeList().contains("6") || sltdMydTotalVO.getServiceTypeList().contains("14"))) {
            //serviceType,即有随访又有满意度,暂时没法统计
        List<String> serviceTypeList = sltdMydTotalVO.getServiceTypeList();
        if (CollectionUtils.isEmpty(serviceTypeList)) {
            log.warn("getDetailRatio 未传 serviceTypeList,无法统计问题占比");
            return serviceSubtaskDetailRatioExports;
        } else if (CollectionUtils.isNotEmpty(sltdMydTotalVO.getServiceTypeList()) && sltdMydTotalVO.getServiceTypeList().contains("2")) {
        }
        // 判断口径:2=随访(按 taskid 统计),6/14=满意度(按 subtask id 统计)
        boolean hasFollowUp = serviceTypeList.contains("2");
        boolean hasSatisfaction = serviceTypeList.contains("6") || serviceTypeList.contains("14");
        if (hasFollowUp && hasSatisfaction) {
            //serviceType,即有随访又有满意度,暂时没法统计
            log.warn("getDetailRatio 暂不支持同时统计随访(2)与满意度(6/14),serviceTypeList={}", serviceTypeList);
            return serviceSubtaskDetailRatioExports;
        }
        if (hasFollowUp) {
            //获取serviceSubtaskList中的所有taskid
            List<Long> taskIds = serviceSubtaskRes.stream().map(ServiceSubtaskRes::getTaskid).filter(ObjectUtils::isNotEmpty).distinct().collect(Collectors.toList());
            serviceSubtaskDetailRatioExports = statQuestionOption(taskIds, sltdMydTotalVO.getStartFinishTime(), sltdMydTotalVO.getEndFinishTime(), sltdMydTotalVO.getStartOutHospTime(), sltdMydTotalVO.getEndOutHospTime(), sltdMydTotalVO.getQuestionContent(), sltdMydTotalVO.getStartScore(), sltdMydTotalVO.getEndScore(), sltdMydTotalVO.getValueType());
        } else if (CollectionUtils.isNotEmpty(sltdMydTotalVO.getServiceTypeList()) && sltdMydTotalVO.getServiceTypeList().contains("6") || sltdMydTotalVO.getServiceTypeList().contains("14")) {
            //同时带上本次导出的子任务 id:让「问题详情占比」只统计这批患者,与「患者随访信息表单」口径一致
            List<Long> subtaskIds = serviceSubtaskRes.stream().map(ServiceSubtaskRes::getId).filter(ObjectUtils::isNotEmpty).distinct().collect(Collectors.toList());
            serviceSubtaskDetailRatioExports = statQuestionOption(taskIds, sltdMydTotalVO.getStartFinishTime(), sltdMydTotalVO.getEndFinishTime(), sltdMydTotalVO.getStartOutHospTime(), sltdMydTotalVO.getEndOutHospTime(), sltdMydTotalVO.getQuestionContent(), sltdMydTotalVO.getStartScore(), sltdMydTotalVO.getEndScore(), sltdMydTotalVO.getValueType(), subtaskIds);
        } else if (hasSatisfaction) {
            //满意度统计 (通过 subtask 的 id 去统计)
            List<Long> subtaskIds = serviceSubtaskRes.stream().map(ServiceSubtaskRes::getId).collect(Collectors.toList());
            serviceSubtaskDetailRatioExports = statQuestionOptionBySubtaskIds(subtaskIds, sltdMydTotalVO.getStartOutHospTime(), sltdMydTotalVO.getEndOutHospTime(), sltdMydTotalVO.getStartFinishTime(), sltdMydTotalVO.getEndFinishTime(), sltdMydTotalVO.getQuestionContent(), sltdMydTotalVO.getStartScore(), sltdMydTotalVO.getEndScore(), sltdMydTotalVO.getValueType());
        } else {
            log.warn("getDetailRatio 暂不支持的 serviceTypeList={}(仅支持 2 或 6/14)", serviceTypeList);
        }
        return serviceSubtaskDetailRatioExports;
    }
@@ -6433,8 +6685,9 @@
        if (StringUtils.isEmpty(mzMydScoreRankVO.getDimensionType())) {
            mzMydScoreRankVO.setDimensionType("1");
        }
        if (StringUtils.isEmpty(mzMydScoreRankVO.getServiceType())) {
            mzMydScoreRankVO.setServiceType("14");
        if (CollectionUtils.isEmpty(mzMydScoreRankVO.getServiceTypes())) {
          List<String> list=new ArrayList<>(Arrays.asList("14"));
            mzMydScoreRankVO.setServiceTypes(list);
        }
        List<MzMydScoreRankDTO> list = serviceSubtaskMapper.selectMzMydScoreRank(mzMydScoreRankVO);
        if (CollectionUtils.isEmpty(list)) {
@@ -6446,4 +6699,40 @@
        }
        return list;
    }
    private String getWXCode(String idcard, String url, String taskName, String taskDesc, String phone, String setPatientname, String setPatientid, List<String> wxqqxx) {
        XinHuaWXReq xinHuaWXReq = new XinHuaWXReq();
        xinHuaWXReq.setIdcard(idcard);
        xinHuaWXReq.setUrl(url);
        xinHuaWXReq.setPatientid(setPatientid);
        xinHuaWXReq.setPatientname(setPatientname);
        xinHuaWXReq.setMobile(phone);
        xinHuaWXReq.setMedcardno(null);
        xinHuaWXReq.setTitlename(taskName);
        xinHuaWXReq.setContent(taskDesc);
        xinHuaWXReq.setAppid(wxqqxx.get(1));
        log.info("【getWXCode】微信公众号请求参数:{}", xinHuaWXReq);
        String body = new Gson().toJson(xinHuaWXReq);
        String encode = encode(wxqqxx.get(2).concat(body));
        Map<String, String> headerMap = new HashMap<>();
        headerMap.put("sign", encode);
        String result = HttpUtils.sendPostByHeader(wxqqxx.get(3), body, headerMap);
        log.info("【getWXCode】微信公众号返回参数:{}", result);
        JSONObject jsonObject = JSONObject.parseObject(result);
        String code = (String) jsonObject.toString();
        return code;
    }
    public String encode(String arg) {
        if (arg == null) {
            arg = "";
        }
        MessageDigest md5 = null;
        try {
            md5 = MessageDigest.getInstance("MD5");
            md5.update(arg.getBytes("UTF-8"));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return toHex(md5.digest());
    }
}
smartor/src/main/resources/mapper/smartor/IvrLibaTemplateScriptMapper.xml
@@ -210,12 +210,12 @@
            resultMap="IvrLibaTemplateScriptResult">
        <include refid="selectIvrLibaTemplateScriptVo"/>
        <where>del_flag=0
            <if test="dimensionList != null ">and dimension in
            <if test="dimensionList != null and dimensionList.size() > 0">and dimension in
                <foreach item="dimension" collection="dimensionList" open="(" separator="," close=")">
                    #{dimension}
                </foreach>
            </if>
            <if test="dimensionList == null ">and dimension IS NOT NULL </if>
            <if test="dimensionList == null or dimensionList.size() == 0">and dimension IS NOT NULL </if>
            <if test="dutyDeptPersonName != null  and dutyDeptPersonName != ''">and duty_dept_person_name like concat('%', #{dutyDeptPersonName},
                '%')
            </if>
smartor/src/main/resources/mapper/smartor/PatArchiveOutMapper.xml
@@ -120,12 +120,12 @@
        <if test="orgid != null and orgid != ''">
            AND orgid = #{orgid}
        </if>
        <if test="deptcodes != null ">and deptcode in
        <if test="deptcodes != null and deptcodes.size() > 0">and deptcode in
            <foreach collection="deptcodes" item="deptcode" open="(" separator="," close=")">
                #{deptcode}
            </foreach>
        </if>
        <if test="drCodes != null ">and drcode in
        <if test="drCodes != null and drCodes.length > 0">and drcode in
            <foreach collection="drCodes" item="drCode" open="(" separator="," close=")">
                #{drCode}
            </foreach>
@@ -137,12 +137,12 @@
        <if test="orgid != null and orgid != ''">
            AND orgid = #{orgid}
        </if>
        <if test="deptcodes != null ">and leaveldeptcode in
        <if test="deptcodes != null and deptcodes.size() > 0">and leaveldeptcode in
            <foreach collection="deptcodes" item="deptcode" open="(" separator="," close=")">
                #{deptcode}
            </foreach>
        </if>
        <if test="drCodes != null ">and drcode in
        <if test="drCodes != null and drCodes.length > 0">and drcode in
            <foreach collection="drCodes" item="drCode" open="(" separator="," close=")">
                #{drCode}
            </foreach>
smartor/src/main/resources/mapper/smartor/PatMedInhospMapper.xml
@@ -370,7 +370,38 @@
            order by b.endtime desc
        </if>
        <if test="cry != null and cry==0">
            order by b.starttime desc , b.bed_no asc
            <!--
              在院患者:可按入院床位正/倒序(bedSort=asc 或 desc),不传时保持默认排序(入院时间倒序 + 床位正序)。
              床位按自然序排(只用 5.7 也支持的 REGEXP/CAST/TRIM/LENGTH,不依赖 8.0 的 REGEXP_REPLACE):
                1) 床位为空/空串的排最后
                2) 纯数字床位排前面,按数值大小(2 在 10 前)
                3) 带字母/汉字的床位按长度 + 字符串(A2 在 A12 前)
                4) inhospid 兜底,保证翻页顺序稳定
              倒序只反转 2)、3) 两组内部顺序:空床位仍排最后,纯数字仍在前。
            -->
            <choose>
                <when test='"asc".equals(bedSort)'>
                    order by
                    coalesce(trim(b.bed_no), '') = '' ,
                    (trim(b.bed_no) regexp '^[0-9]+$') = 0 ,
                    cast(trim(b.bed_no) as unsigned) asc ,
                    length(trim(b.bed_no)) asc ,
                    trim(b.bed_no) asc ,
                    b.inhospid asc
                </when>
                <when test='"desc".equals(bedSort)'>
                    order by
                    coalesce(trim(b.bed_no), '') = '' ,
                    (trim(b.bed_no) regexp '^[0-9]+$') = 0 ,
                    cast(trim(b.bed_no) as unsigned) desc ,
                    length(trim(b.bed_no)) desc ,
                    trim(b.bed_no) desc ,
                    b.inhospid asc
                </when>
                <otherwise>
                    order by b.starttime desc , b.bed_no asc
                </otherwise>
            </choose>
        </if>
    </select>
@@ -808,7 +839,8 @@
            <if test="deptcode != null">#{deptcode},</if>
            <if test="deptname != null">#{deptname},</if>
            <if test="roomno != null">#{roomno},</if>
            <if test="bedNo != null">#{bedNo},</if>
            <!-- 床位空串(HIS 无当前床位)落库为 null,保持"有值 or null"两种状态 -->
            <if test="bedNo != null">nullif(#{bedNo}, ''),</if>
            <if test="orgid != null">#{orgid},</if>
            <if test="campusid != null">#{campusid},</if>
            <if test="delFlag != null and delFlag != ''">#{delFlag},</if>
@@ -894,7 +926,7 @@
            #{item.serialnum}, #{item.hospitalname}, #{item.hospitalcode}, #{item.hospitaldistrictcode},
            #{item.hospitaldistrictname},
            #{item.icd10code}, #{item.diagname}, #{item.starttime}, #{item.endtime}, #{item.deptcode}, #{item.deptname},
            #{item.roomno}, #{item.bedNo},
            #{item.roomno}, nullif(#{item.bedNo}, ''),
            #{item.orgid}, #{item.delFlag}, #{item.updateBy}, #{item.updateTime}, #{item.createBy}, #{item.createTime},
            #{item.isupload},
            #{item.uploadTime}, #{item.patid}, #{item.leavediagname}, #{item.leaveicd10code}, #{item.drcode},
@@ -919,6 +951,11 @@
    <update id="updatePatMedInhosp" parameterType="com.smartor.domain.PatMedInhosp">
        update pat_med_inhosp
        <trim prefix="SET" suffixOverrides=",">
            <!--
              兜底:下面字段全为空时(例如调用方只传了 inhospid),如果没有任何 SET 项会生成
              "update pat_med_inhosp where inhospid = ?" 这种非法 SQL。这里放一个自赋值,保证 SET 不为空且不改数据。
            -->
            inhospid = inhospid,
            <if test="serialnum != null">serialnum = #{serialnum},</if>
            <if test="hospitalname != null">hospitalname = #{hospitalname},</if>
            <if test="hospitalcode != null">hospitalcode = #{hospitalcode},</if>
@@ -931,7 +968,12 @@
            <if test="deptcode != null">deptcode = #{deptcode},</if>
            <if test="deptname != null">deptname = #{deptname},</if>
            <if test="roomno != null">roomno = #{roomno},</if>
            <if test="bedNo != null">bed_no = #{bedNo},</if>
            <!--
              床位:HIS 传空串代表"当前没有床位"(换床、或人已离院还没办出院手续),落库为 null;
              完全不传(null)则不改动床位。空串与 null 必须分开判断,不能都跳过,否则床位不会被清掉。
            -->
            <if test='bedNo != null and !"".equals(bedNo)'>bed_no = #{bedNo},</if>
            <if test='bedNo != null and "".equals(bedNo)'>bed_no = null,</if>
            <if test="orgid != null">orgid = #{orgid},</if>
            <if test="campusid != null">campusid = #{campusid},</if>
            <if test="delFlag != null and delFlag != ''">del_flag = #{delFlag},</if>
smartor/src/main/resources/mapper/smartor/ServiceSubtaskMapper.xml
@@ -98,6 +98,7 @@
        <result property="scheduleDetailId" column="schedule_detail_id"/>
        <result property="scheduleSeq" column="schedule_seq"/>
        <result property="loopCount" column="loop_count"/>
        <result property="isManuallyCalled" column="is_manually_called"/>
        <result property="campusid" column="campusid"/>
    </resultMap>
@@ -245,7 +246,8 @@
               schedule_detail_id,
               schedule_seq,
               loop_count,
               task_situation
               task_situation,
               is_manually_called
        from service_subtask
    </sql>
@@ -513,6 +515,7 @@
        <if test="createBy != null">and create_by = #{createBy}</if>
        <if test="taskGuid != null">and task_guid = #{taskGuid}</if>
        <if test="isabnormal != null">and isabnormal = #{isabnormal}</if>
        <if test="isManuallyCalled != null">and is_manually_called = #{isManuallyCalled}</if>
        <if test="isVisitAgain != null">and is_visit_again = #{isVisitAgain}</if>
        <if test="visitCount != null and visitCount > 1 and visitDeptCodes != null and visitDeptCodes.size() > 0">
            AND visit_dept_code IN
@@ -976,6 +979,7 @@
            <if test="scheduleDetailId != null ">schedule_detail_id,</if>
            <if test="scheduleSeq != null ">schedule_seq,</if>
            <if test="loopCount != null ">loop_count,</if>
            <if test="isManuallyCalled != null ">is_manually_called,</if>
        </trim>
        <trim prefix="values (" suffix=")" suffixOverrides=",">
            <if test="sendname != null">#{sendname},</if>
@@ -1073,6 +1077,7 @@
            <if test="scheduleDetailId != null ">#{scheduleDetailId},</if>
            <if test="scheduleSeq != null ">#{scheduleSeq},</if>
            <if test="loopCount != null ">#{loopCount},</if>
            <if test="isManuallyCalled != null ">#{isManuallyCalled},</if>
        </trim>
    </insert>
@@ -1175,6 +1180,7 @@
            <if test="scheduleDetailId != null ">schedule_detail_id = #{scheduleDetailId},</if>
            <if test="scheduleSeq != null ">schedule_seq = #{scheduleSeq},</if>
            <if test="loopCount != null ">loop_count = #{loopCount},</if>
            <if test="isManuallyCalled != null ">is_manually_called = #{isManuallyCalled},</if>
        </trim>
        where id = #{id}
    </update>
@@ -1273,6 +1279,7 @@
            <if test="continueCount != null ">continue_count = #{continueCount},</if>
            <if test="continueTimeNext != null ">continue_time_next = #{continueTimeNext,jdbcType=TIMESTAMP},</if>
            <if test="continueContent != null ">continue_content = #{continueContent},</if>
            <if test="isManuallyCalled != null ">is_manually_called = #{isManuallyCalled},</if>
        </trim>
        where 1=1
        <if test="patid != null ">and patid = #{patid}</if>
@@ -1375,6 +1382,7 @@
            <if test="continueCount != null ">continue_count = #{continueCount},</if>
            <if test="continueTimeNext != null ">continue_time_next = #{continueTimeNext,jdbcType=TIMESTAMP},</if>
            <if test="continueContent != null ">continue_content = #{continueContent},</if>
            <if test="isManuallyCalled != null ">is_manually_called = #{isManuallyCalled},</if>
        </trim>
        <where>
            <if test="patid != null ">
@@ -1485,6 +1493,7 @@
            <if test="continueCount != null ">continue_count = #{continueCount},</if>
            <if test="continueTimeNext != null ">continue_time_next = #{continueTimeNext,jdbcType=TIMESTAMP},</if>
            <if test="continueContent != null ">continue_content = #{continueContent},</if>
            <if test="isManuallyCalled != null ">is_manually_called = #{isManuallyCalled},</if>
        </trim>
        where task_guid = #{taskGuid} and task_name = #{taskName}
    </update>
@@ -3027,7 +3036,7 @@
               ROUND(avg(d.score), 2) AS totalScore
        FROM service_subtask_detail d
                 JOIN service_task t ON d.taskid = t.taskid
        WHERE t.service_type IN (6, 14)
        WHERE t.service_type IN (14)
          AND t.del_flag = 0
          AND d.del_flag = 0
          AND d.create_time >= #{startFinishTime}
@@ -3249,6 +3258,13 @@
                #{taskId}
            </foreach>
        </if>
        <!-- 传了 subtaskIds 只统计这批子任务(本次导出的患者) -->
        <if test="subtaskIds != null and subtaskIds.size() > 0">
            AND s.id IN
            <foreach collection="subtaskIds" item="subtaskId" open="(" separator="," close=")">
                #{subtaskId}
            </foreach>
        </if>
        <if test="valueType != null">AND d.value_type = #{valueType}</if>
        <if test="scoreStart != null">AND d.score &gt;= #{scoreStart}</if>
        <if test="scoreEnd != null">AND d.score &lt;= #{scoreEnd}</if>
@@ -3257,6 +3273,19 @@
        </if>
        and s.sendstate=6
        and d.del_flag=0
        <!-- 分母必须和明细用同一套时间条件,否则选了时间范围后比例会按全量历史被稀释 -->
        <if test="startOutHospTime != null">
            AND date_format(s.endtime,'%y%m%d') &gt;= date_format(#{startOutHospTime},'%y%m%d')
        </if>
        <if test="endOutHospTime != null">
            AND date_format(s.endtime,'%y%m%d') &lt;= date_format(#{endOutHospTime},'%y%m%d')
        </if>
        <if test="startFinishTime != null">
            AND date_format(s.finishtime,'%y%m%d') &gt;= date_format(#{startFinishTime},'%y%m%d')
        </if>
        <if test="endFinishTime != null">
            AND date_format(s.finishtime,'%y%m%d') &lt;= date_format(#{endFinishTime},'%y%m%d')
        </if>
        GROUP BY
        t.taskid,
        d.questiontext
@@ -3266,6 +3295,12 @@
            AND t.taskid IN
            <foreach collection="taskIds" item="taskId" open="(" separator="," close=")">
                #{taskId}
            </foreach>
        </if>
        <if test="subtaskIds != null and subtaskIds.size() > 0">
            AND s.id IN
            <foreach collection="subtaskIds" item="subtaskId" open="(" separator="," close=")">
                #{subtaskId}
            </foreach>
        </if>
        <if test="valueType != null">AND d.value_type = #{valueType}</if>
@@ -3300,6 +3335,64 @@
        t.taskid
    </select>
    <!--
      导出「问题详情占比」专用统计(只统计本次导出的这批患者)
      数量 = 选择该选项的患者数(COUNT(DISTINCT s.id),并用 MAX(id) 保证同一患者同一题只算最新一条)
      分数 = 该选项明细的 score 之和
      比例 = 数量 / 该题作答患者数,在 Java 侧统一计算,因此「单题各选项数量之和 = 该题作答患者数」
    -->
    <select id="statMydQuestionOptionForExport" resultType="com.smartor.domain.DTO.ServiceSubtaskOptionStatDTO">
        SELECT
        t.taskid AS taskid,
        t.task_name AS taskName,
        d.questiontext AS questiontext,
        d.asrtext AS optionresult,
        d.targetvalue AS targetvalue,
        d.value_type AS valueType,
        COUNT(DISTINCT s.id) AS patientCount,
        SUM(COALESCE(CAST(d.score AS DECIMAL(18,2)), 0)) AS scoreSum
        FROM
        service_task t
        INNER JOIN service_subtask s ON s.taskid = t.taskid
        INNER JOIN service_subtask_detail d ON d.sub_id = s.id
        WHERE 1=1
        AND s.del_flag = '0'
        AND d.del_flag = 0
        <if test="subtaskIds != null and subtaskIds.size() > 0">
            AND s.id IN
            <foreach collection="subtaskIds" item="subtaskId" open="(" separator="," close=")">
                #{subtaskId}
            </foreach>
        </if>
        <!-- 不传子任务时直接查不到数据,避免误统计全量 -->
        <if test="subtaskIds == null or subtaskIds.size() == 0">
            AND 1 = 0
        </if>
        <if test="questiontext != null and questiontext != ''">
            AND d.questiontext LIKE CONCAT('%', #{questiontext}, '%')
        </if>
        <!-- 同一患者同一题目只保留最新一条,重答不重复计入数量 -->
        AND d.id = (
        SELECT MAX(d2.id)
        FROM service_subtask_detail d2
        WHERE d2.sub_id = d.sub_id
        AND d2.questiontext &lt;=&gt; d.questiontext
        AND d2.del_flag = '0'
        )
        GROUP BY
        t.taskid,
        t.task_name,
        d.questiontext,
        d.asrtext,
        d.targetvalue,
        d.value_type
        <!-- 按任务名称分组展示:先任务名称、再任务编号,同一任务的题目/选项连续排列 -->
        ORDER BY
        t.task_name,
        t.taskid,
        d.questiontext
    </select>
    <!-- 新方法:根据子任务 ID 统计问题选项百分比 (用于满意度统计 serviceType=6) -->
    <select id="statQuestionOptionBySubtaskIds" resultType="com.smartor.domain.ServiceSubtaskDetailRatioExport">
        SELECT
@@ -3320,6 +3413,14 @@
        service_task t
        INNER JOIN service_subtask s ON s.taskid = t.taskid
        INNER JOIN service_subtask_detail d ON d.sub_id = s.id
        <!-- 去重:同一子任务同一题目只保留最新一条,避免重答/重复回调把同一道题重复计入项数(与 mzMydScoreRank 口径一致) -->
        AND d.id = (
        SELECT MAX(d2.id)
        FROM service_subtask_detail d2
        WHERE d2.sub_id = d.sub_id
        AND d2.questiontext &lt;=&gt; d.questiontext
        AND d2.del_flag = '0'
        )
        INNER JOIN (
        SELECT
        d.questiontext,
@@ -3328,6 +3429,14 @@
        service_task t
        INNER JOIN service_subtask s ON s.taskid = t.taskid
        INNER JOIN service_subtask_detail d ON d.sub_id = s.id
        <!-- 去重:分母(该题有效答卷总数)与明细口径保持一致,保证各选项比例合计 100% -->
        AND d.id = (
        SELECT MAX(d4.id)
        FROM service_subtask_detail d4
        WHERE d4.sub_id = d.sub_id
        AND d4.questiontext &lt;=&gt; d.questiontext
        AND d4.del_flag = '0'
        )
        WHERE 1=1
        <if test="subtaskIds != null and subtaskIds.size() > 0">
            AND s.id IN
@@ -3336,16 +3445,37 @@
            </foreach>
        </if>
        <if test="valueType != null">AND d.value_type &gt;= #{valueType}</if>
        <if test="scoreStart != null">AND d.score = #{scoreStart}</if>
        <if test="scoreStart != null">AND d.score &gt;= #{scoreStart}</if>
        <if test="scoreEnd != null">AND d.score &lt;= #{scoreEnd}</if>
        <if test="questiontext != null  and questiontext != ''">and d.questiontext like concat('%', #{questiontext},
            '%')
        </if>
        AND s.sendstate = 6
        AND d.del_flag=0
        <!-- 分母与明细用同一套时间条件,否则选了日期区间后各选项比例合计不到 100% -->
        <if test="startOutHospTime != null">
            AND date_format(s.endtime,'%y%m%d') &gt;= date_format(#{startOutHospTime},'%y%m%d')
        </if>
        <if test="endOutHospTime != null">
            AND date_format(s.endtime,'%y%m%d') &lt;= date_format(#{endOutHospTime},'%y%m%d')
        </if>
        <if test="startFinishTime != null">
            AND date_format(s.finishtime,'%y%m%d') &gt;= date_format(#{startFinishTime},'%y%m%d')
        </if>
        <if test="endFinishTime != null">
            AND date_format(s.finishtime,'%y%m%d') &lt;= date_format(#{endFinishTime},'%y%m%d')
        </if>
        GROUP BY d.questiontext
        ) t2 ON t2.questiontext = d.questiontext
        WHERE 1=1
        <!-- 去重:明细行同一子任务同一题目只保留最新一条,重答不重复计入项数 -->
        AND d.id = (
        SELECT MAX(d3.id)
        FROM service_subtask_detail d3
        WHERE d3.sub_id = d.sub_id
        AND d3.questiontext &lt;=&gt; d.questiontext
        AND d3.del_flag = '0'
        )
        <if test="subtaskIds != null and subtaskIds.size() > 0">
            AND s.id IN
            <foreach collection="subtaskIds" item="subtaskId" open="(" separator="," close=")">
@@ -3484,7 +3614,13 @@
        WHERE s.del_flag = '0'
        AND d.del_flag = '0'
        AND s.sendstate = 6
        AND s.service_type = #{serviceType}
        <if test="serviceTypes != null and serviceTypes.size() > 0">
            AND service_type IN
            <foreach collection="serviceTypes" item="serviceType" open="(" separator=","
                     close=")">
                #{serviceType}
            </foreach>
        </if>
<!--        &lt;!&ndash; 只取公式里的五档权重,空值/0/未答等既不进分子也不进分母 &ndash;&gt;-->
<!--        AND CAST(d.score AS DECIMAL(10,4)) IN (1, 0.8, 0.6, 0.4, 0.2)-->
        <!-- 同一子任务同一题目只保留最新一条,避免重答被重复计入项数 -->
@@ -3507,6 +3643,9 @@
        <if test="endFinishTime != null">
            AND s.finishtime &lt; DATE_ADD(#{endFinishTime}, INTERVAL 1 DAY)
        </if>
        <if test="questionText != null  and questionText != ''">and d.questiontext like concat('%', #{questionText},
            '%')
        </if>
        <choose>
            <when test='dimensionType == "2"'>
                AND s.leavehospitaldistrictcode IS NOT NULL AND s.leavehospitaldistrictcode &lt;&gt; ''
smartor/src/main/resources/mapper/smartor/SvyLibTemplateScriptMapper.xml
@@ -210,7 +210,7 @@
            resultMap="SvyLibTemplateScriptResult">
        <include refid="selectSvyLibTemplateScriptVo"/>
        <where>del_flag=0
            <if test="dimensionList != null ">and dimension in
            <if test="dimensionList != null and dimensionList.size() > 0">and dimension in
                <foreach item="dimension" collection="dimensionList" open="(" separator="," close=")">
                    #{dimension}
                </foreach>
@@ -239,7 +239,7 @@
                #{circulationDeptPersonName},
                '%')
            </if>
            <if test="dimensionList == null ">and dimension IS NOT NULL </if>
            <if test="dimensionList == null or dimensionList.size() == 0">and dimension IS NOT NULL </if>
        </where>
    </select>