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 + "¶m2=" + patid + "¶m3=" + URLEncoder.encode(serviceSubtask.getTaskName(), StandardCharsets.UTF_8.toString()) + "¶m5=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 >= #{scoreStart}</if> <if test="scoreEnd != null">AND d.score <= #{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') >= date_format(#{startOutHospTime},'%y%m%d') </if> <if test="endOutHospTime != null"> AND date_format(s.endtime,'%y%m%d') <= date_format(#{endOutHospTime},'%y%m%d') </if> <if test="startFinishTime != null"> AND date_format(s.finishtime,'%y%m%d') >= date_format(#{startFinishTime},'%y%m%d') </if> <if test="endFinishTime != null"> AND date_format(s.finishtime,'%y%m%d') <= 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 <=> 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 <=> 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 <=> 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 >= #{valueType}</if> <if test="scoreStart != null">AND d.score = #{scoreStart}</if> <if test="scoreStart != null">AND d.score >= #{scoreStart}</if> <if test="scoreEnd != null">AND d.score <= #{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') >= date_format(#{startOutHospTime},'%y%m%d') </if> <if test="endOutHospTime != null"> AND date_format(s.endtime,'%y%m%d') <= date_format(#{endOutHospTime},'%y%m%d') </if> <if test="startFinishTime != null"> AND date_format(s.finishtime,'%y%m%d') >= date_format(#{startFinishTime},'%y%m%d') </if> <if test="endFinishTime != null"> AND date_format(s.finishtime,'%y%m%d') <= 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 <=> 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> <!-- <!– 只取公式里的五档权重,空值/0/未答等既不进分子也不进分母 –>--> <!-- 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 < 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 <> '' 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>