| | |
| | | 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; |
| | |
| | | import com.smartor.common.MtSubmitSmUtil; |
| | | import com.smartor.common.ScheduleSubtaskBuilder; |
| | | import com.smartor.config.PhoneUtils; |
| | | import com.smartor.domain.DTO.MydTrendReqDTO; |
| | | 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.DTO.*; |
| | | import com.smartor.domain.*; |
| | | import com.smartor.domain.VO.*; |
| | | import com.smartor.domain.entity.ServiceSubtaskEntity; |
| | |
| | | result.putIfAbsent("wxsf", 0); |
| | | result.putIfAbsent("xsf", 0); |
| | | result.putIfAbsent("dsf", 0); |
| | | result.putIfAbsent("ywc", 0); |
| | | result.putIfAbsent("yc", 0); |
| | | result.putIfAbsent("jg", 0); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | /** |
| | | * 统计任务各种状态的数量(已优化) |
| | | * 优化说明: |
| | | * 1. 使用数据库层聚合计算,替代应用层遍历 |
| | | * 2. 性能提升 10-100 倍(取决于数据量) |
| | | * 3. 降低内存占用和网络传输 |
| | | * |
| | | * @param serviceSubtaskEntity 查询条件 |
| | | * @return 各状态统计结果 |
| | | */ |
| | | @Override |
| | | public Map<String, Object> countByCondition(ServiceSubtaskEntity serviceSubtaskEntity) { |
| | | // 直接调用数据库统计方法,在数据库层完成聚合计算 |
| | | Map<String, Object> result = serviceSubtaskMapper.countByCondition(serviceSubtaskEntity); |
| | | // 处理空异常 |
| | | if (MapUtils.isEmpty(result)) { |
| | | result = new HashMap<>(); |
| | | // 确保所有 key 都有值,避免空指针异常 |
| | | // MyBatis 的 SUM 在没有匹配行时会返回 null |
| | | result.putIfAbsent("wzx", 0); |
| | | result.putIfAbsent("ysf", 0); |
| | | result.putIfAbsent("fssb", 0); |
| | | result.putIfAbsent("yfs", 0); |
| | | result.putIfAbsent("blq", 0); |
| | | result.putIfAbsent("total", 0); |
| | | result.putIfAbsent("wxsf", 0); |
| | | result.putIfAbsent("xsf", 0); |
| | | result.putIfAbsent("dsf", 0); |
| | | result.putIfAbsent("dfs", 0); |
| | | result.putIfAbsent("yfs", 0); |
| | | result.putIfAbsent("ywc", 0); |
| | | result.putIfAbsent("yc", 0); |
| | | result.putIfAbsent("jg", 0); |
| | |
| | | } |
| | | |
| | | List<ServiceSubtaskDetailRatioExport> serviceSubtaskDetailRatioExports = serviceSubtaskMapper.statQuestionOptionBySubtaskIds(subtaskIds, startOutHospTime, endOutHospTime, startFinishTime, endFinishTime, scoreStart, scoreEnd, scriptContent, valueType); |
| | | // 处理 targetvalue:按 & 分割成多条记录 |
| | | // SQL 已按「题目 + 选项」汇总数量,这里再按题目合并(不同模板的 targetvalue 选项顺序不同,同一选项会拆成多行), |
| | | // 并把没人选的选项补成 0 行、按各选项数量之和重算比例 |
| | | return mergeMydOptionRows(serviceSubtaskDetailRatioExports); |
| | | } |
| | | |
| | | /** |
| | | * 把 SQL 返回的「题目 + 选项」明细合并成「一行一个选项」 |
| | | * <ul> |
| | | * <li>同一题目下相同 optionresult 的数量累加:原实现遇到同名选项直接 continue, |
| | | * 每个选项只会保留第一条明细的数量,多任务/多患者的项数全被丢掉,前端「数量」永远只有 1</li> |
| | | * <li>选项清单取 targetvalue(用 & 连接),没人选的选项补 0 行,前端固定六列才能渲染</li> |
| | | * <li>比例 = 该选项数量 / 该题各选项数量之和,保证一道题各选项比例合计 100%</li> |
| | | * </ul> |
| | | * |
| | | * @param rows SQL 明细 |
| | | */ |
| | | private List<ServiceSubtaskDetailRatioExport> mergeMydOptionRows(List<ServiceSubtaskDetailRatioExport> rows) { |
| | | List<ServiceSubtaskDetailRatioExport> result = new ArrayList<>(); |
| | | for (ServiceSubtaskDetailRatioExport export : serviceSubtaskDetailRatioExports) { |
| | | if (StringUtils.isNotEmpty(export.getTargetvalue()) && export.getTargetvalue().contains("&")) { |
| | | String[] values = export.getTargetvalue().split("&"); |
| | | // 如果有多条目标值,拆分成多行 |
| | | for (String value : values) { |
| | | //先去 result 中查一下,有没有这个 questiontext 和对应的 targetvalue |
| | | ServiceSubtaskDetailRatioExport existingExport = result.stream().filter(e -> { |
| | | if (e == null || export == null) { |
| | | return false; |
| | | } |
| | | String q1 = e.getQuestiontext() != null ? e.getQuestiontext() : ""; |
| | | String q2 = export.getQuestiontext() != null ? export.getQuestiontext() : ""; |
| | | String t1 = value.trim(); |
| | | String t2 = e.getTargetvalue() != null ? e.getTargetvalue() : ""; |
| | | return q1.equals(q2) && t1.equals(t2); |
| | | }).findFirst().orElse(null); |
| | | if (existingExport != null && "0".equals(existingExport.getRatio()) && "0".equals(existingExport.getCount())) { |
| | | //如果存在,则将 result 里的这个问题所对应的 Targetvalue=value 这条数据更新成当前这条数据 |
| | | if (value.equals(export.getOptionresult())) { |
| | | existingExport.setRatio(export.getRatio()); |
| | | existingExport.setScore(export.getScore()); |
| | | existingExport.setCount(export.getCount()); |
| | | existingExport.setOptionresult(export.getOptionresult()); |
| | | } |
| | | } else if (existingExport != null && !"0".equals(existingExport.getRatio())) { |
| | | continue; |
| | | } else { |
| | | ServiceSubtaskDetailRatioExport newExport = DtoConversionUtils.sourceToTarget(export, ServiceSubtaskDetailRatioExport.class); |
| | | newExport.setTargetvalue(value.trim()); // targetvalue 显示单个选项 |
| | | if (!value.equals(export.getOptionresult())) { |
| | | newExport.setOptionresult(null); |
| | | newExport.setCount("0"); |
| | | newExport.setRatio("0"); |
| | | } |
| | | result.add(newExport); |
| | | } |
| | | if (CollectionUtils.isEmpty(rows)) { |
| | | return result; |
| | | } |
| | | |
| | | // key -> 选项 -> 数量,用 LinkedHashMap 保序:选项顺序沿用问卷配置 targetvalue 的顺序 |
| | | Map<String, Map<String, long[]>> countMap = new LinkedHashMap<>(); |
| | | Map<String, Map<String, String>> scoreMap = new LinkedHashMap<>(); |
| | | Map<String, ServiceSubtaskDetailRatioExport> templateMap = new LinkedHashMap<>(); |
| | | |
| | | for (ServiceSubtaskDetailRatioExport row : rows) { |
| | | if (row == null) { |
| | | continue; |
| | | } |
| | | String key = row.getQuestiontext(); |
| | | if (key == null) { |
| | | continue; |
| | | } |
| | | Map<String, long[]> counts = countMap.computeIfAbsent(key, k -> new LinkedHashMap<>()); |
| | | Map<String, String> scores = scoreMap.computeIfAbsent(key, k -> new LinkedHashMap<>()); |
| | | templateMap.putIfAbsent(key, row); |
| | | |
| | | // 题目配置的选项清单:没人选的选项也要列出来(数量为 0) |
| | | for (String option : splitOptions(row.getTargetvalue())) { |
| | | counts.computeIfAbsent(option, k -> new long[1]); |
| | | } |
| | | |
| | | // 患者实际选中的选项:多选题 asrtext 是「选项1&选项2」,单选/问答题是整段文本 |
| | | List<String> chosenOptions = "2".equals(row.getValueType()) |
| | | ? splitOptions(row.getOptionresult()) |
| | | : singleOption(row.getOptionresult()); |
| | | if (chosenOptions.isEmpty()) { |
| | | // 未作答:不进任何选项,也不进分母 |
| | | continue; |
| | | } |
| | | long count = parseCount(row.getCount()); |
| | | for (String chosen : chosenOptions) { |
| | | // 选中的选项可能不在配置清单里(历史数据),补一行,数量不能丢 |
| | | counts.computeIfAbsent(chosen, k -> new long[1])[0] += count; |
| | | if (StringUtils.isNotEmpty(row.getScore()) && StringUtils.isEmpty(scores.get(chosen))) { |
| | | scores.put(chosen, row.getScore()); |
| | | } |
| | | } else { |
| | | // targetvalue 不包含 &,直接添加到结果 |
| | | result.add(export); |
| | | } |
| | | } |
| | | |
| | | for (Map.Entry<String, Map<String, long[]>> entry : countMap.entrySet()) { |
| | | ServiceSubtaskDetailRatioExport template = templateMap.get(entry.getKey()); |
| | | Map<String, String> scores = scoreMap.get(entry.getKey()); |
| | | long total = 0L; |
| | | for (long[] count : entry.getValue().values()) { |
| | | total += count[0]; |
| | | } |
| | | for (Map.Entry<String, long[]> optionEntry : entry.getValue().entrySet()) { |
| | | String option = optionEntry.getKey(); |
| | | long count = optionEntry.getValue()[0]; |
| | | ServiceSubtaskDetailRatioExport merged = DtoConversionUtils.sourceToTarget(template, ServiceSubtaskDetailRatioExport.class); |
| | | merged.setQuestiontext(template.getQuestiontext()); |
| | | merged.setTargetvalue(option); |
| | | // 没人选的选项 optionresult 置空,前端按 targetvalue 兜底展示,保持原有展示口径 |
| | | merged.setOptionresult(count > 0 ? option : null); |
| | | merged.setCount(String.valueOf(count)); |
| | | merged.setRatio(formatRatio(count, total)); |
| | | merged.setScore(StringUtils.isNotEmpty(scores.get(option)) ? scores.get(option) : template.getScore()); |
| | | result.add(merged); |
| | | } |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | /** |
| | | * 单选/问答题的作答文本:不按 & 拆,避免把问答题的文本答案拆坏 |
| | | */ |
| | | private static List<String> singleOption(String answer) { |
| | | List<String> options = new ArrayList<>(); |
| | | String text = trimAll(answer); |
| | | if (StringUtils.isNotEmpty(text)) { |
| | | options.add(text); |
| | | } |
| | | return options; |
| | | } |
| | | |
| | | /** |
| | |
| | | Date newDate = new Date(); |
| | | serviceSubtask.setLongSendTime(newDate); |
| | | serviceSubtask.setVisitTime(newDate); |
| | | // 院区:/scanGenerateSubtask 是匿名接口(SecurityConfig permitAll),取不到登录用户; |
| | | // 而 service_task.campusid 在本库是占位值 '1',直接沿用会让统计接口(@AddOrgId 按院区过滤)查不到数据。 |
| | | // 这里用任务配置的病区(其次科室)反查 sys_dept.campusid 落库,保证问卷统计能按院区取到。 |
| | | String campusIdByDept = queryCampusIdByDeptCode(serviceSubtask.getLeavehospitaldistrictcode(), serviceTask.getOrgid()); |
| | | if (StringUtils.isEmpty(campusIdByDept)) { |
| | | campusIdByDept = queryCampusIdByDeptCode(serviceSubtask.getDeptcode(), serviceTask.getOrgid()); |
| | | } |
| | | if (StringUtils.isNotEmpty(campusIdByDept)) { |
| | | serviceSubtask.setCampusid(campusIdByDept); |
| | | } |
| | | // 只配了科室(没有病区)的满意度任务:这道答卷在按病区筛选的报表里统计不到,留日志便于运维发现 |
| | | if (StringUtils.isEmpty(serviceSubtask.getLeavehospitaldistrictcode()) |
| | | && ("6".equals(serviceSubtask.getServiceType()) || "14".equals(serviceSubtask.getServiceType()))) { |
| | | log.warn("【扫码生成任务】满意度子任务没有病区,按病区筛选的报表将统计不到该答卷:taskid={}, deptcode={}, campusid={}", |
| | | serviceSubtask.getTaskid(), serviceSubtask.getDeptcode(), serviceSubtask.getCampusid()); |
| | | } |
| | | return serviceSubtask; |
| | | } |
| | | |
| | | /** |
| | | * 按科室/病区编码反查所属院区(查不到返回 null) |
| | | */ |
| | | private String queryCampusIdByDeptCode(String deptCode, String orgid) { |
| | | if (StringUtils.isEmpty(deptCode)) { |
| | | return null; |
| | | } |
| | | try { |
| | | SysDept sysDept = sysDept2Mapper.selectDeptByCode(deptCode, orgid); |
| | | return sysDept == null ? null : sysDept.getCampusid(); |
| | | } catch (Exception e) { |
| | | log.warn("按编码反查院区失败:deptCode={}, orgid={}, err={}", deptCode, orgid, e.getMessage()); |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Ai 匹配选项 |
| | | * |
| | | * @param questionText |