| | |
| | | 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; |
| | | import com.ruoyi.system.mapper.SysDeptMapper; |
| | | import com.ruoyi.system.service.ISysConfigService; |
| | | import com.ruoyi.system.service.ISysDeptService; |
| | | import com.smartor.common.FtpService; |
| | | import com.smartor.common.MtSubmitSmUtil; |
| | | import com.smartor.common.ScheduleSubtaskBuilder; |
| | |
| | | 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; |
| | |
| | | |
| | | 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.*; |
| | | import java.time.format.DateTimeFormatter; |
| | | import java.time.temporal.ChronoUnit; |
| | | import java.util.*; |
| | | import java.util.concurrent.Executors; |
| | | import java.util.concurrent.ScheduledExecutorService; |
| | | import java.util.concurrent.TimeUnit; |
| | | import java.util.regex.Matcher; |
| | | import java.util.regex.Pattern; |
| | | import java.util.stream.Collectors; |
| | | |
| | | import static cn.hutool.core.convert.Convert.toHex; |
| | | |
| | | /** |
| | | * 单一任务(随访)Service业务层处理 |
| | |
| | | private IPatArchiveService patArchiveService; |
| | | |
| | | @Autowired |
| | | private ISysDeptService sysDeptService; |
| | | |
| | | @Autowired |
| | | private ScheduleSubtaskBuilder scheduleSubtaskBuilder; |
| | | |
| | | @Autowired |
| | |
| | | @Value("${hangup}") |
| | | private String hangup; |
| | | |
| | | /** |
| | | * 挂断请求最大重试次数(含首次) |
| | | */ |
| | | private static final int HANGUP_MAX_RETRY = 3; |
| | | |
| | | /** |
| | | * 挂断请求重试间隔(毫秒) |
| | | */ |
| | | private static final long HANGUP_RETRY_INTERVAL_MS = 300L; |
| | | |
| | | /** |
| | | * 兜底挂断的最小延时(毫秒):等平台把结束语播完再挂 |
| | | */ |
| | | private static final long HANGUP_FALLBACK_MIN_DELAY_MS = 8000L; |
| | | |
| | | /** |
| | | * 兜底挂断定时器。 |
| | | * <p> |
| | | * 雨绮这套的挂断原本依赖"平台播完结束语后再回调一次 PlayEventCallback(playstop=true)", |
| | | * 平台不回调电话就一直挂着。这里用一个守护线程池在结束语下发后按字数估算时长做兜底挂断, |
| | | * 与平台回调走同一个幂等标记,不会重复挂断。 |
| | | */ |
| | | private ScheduledExecutorService hangupFallbackExecutor; |
| | | |
| | | @Value("${app_key_yq}") |
| | | private String app_key_yq; |
| | | |
| | |
| | | private ServiceOutPathMapper serviceOutPathMapper; |
| | | |
| | | private static final String KEY_SEP = "\u0001"; |
| | | |
| | | /** 二次随访生成锁前缀,防止同一原任务并发生成多条二次随访 */ |
| | | private static final String AGAIN_LOCK_KEY_PREFIX = "smartor:again:lock:"; |
| | | private static final long AGAIN_LOCK_SECONDS = 30L; |
| | | @Autowired |
| | | private PatMedOperationItemMapper patMedOperationItemMapper; |
| | | @Autowired |
| | | private SysDeptMapper sysDeptMapper; |
| | | |
| | | /** |
| | | * 查询单一任务(随访) |
| | |
| | | |
| | | @Override |
| | | public List<ServiceSubtask> patItem2(ServiceSubtaskEntity serviceSubtaskEntity) { |
| | | if (serviceSubtaskEntity.getSendstateView() != null) { |
| | | // 1:待随访(1 被领取、2 待发送、3 已发送、5 发送失败、7、超时);2:已完成( 6 已完成)、3:无需随访(4 不执行) |
| | | if (serviceSubtaskEntity.getSendstateView() == 1) |
| | | serviceSubtaskEntity.setSendstates(new ArrayList<>(Arrays.asList(1L, 2L, 3L, 5L, 7L))); |
| | | if (serviceSubtaskEntity.getSendstateView() == 2) |
| | | serviceSubtaskEntity.setSendstates(new ArrayList<>(Arrays.asList(6L))); |
| | | if (serviceSubtaskEntity.getSendstateView() == 3) |
| | | serviceSubtaskEntity.setSendstates(new ArrayList<>(Arrays.asList(4L))); |
| | | } |
| | | List<ServiceSubtask> serviceSubtaskList = new ArrayList<>(); |
| | | for (Long sendstate : serviceSubtaskEntity.getSendstates()) { |
| | | serviceSubtaskEntity.setSendstate(sendstate); |
| | | List<ServiceSubtask> serviceSubtasks = serviceSubtaskMapper.selectServiceSubtaskList(serviceSubtaskEntity); |
| | | serviceSubtaskList.addAll(serviceSubtasks); |
| | | if(CollectionUtils.isNotEmpty(serviceSubtaskEntity.getSendstates())){ |
| | | for (Long sendstate : serviceSubtaskEntity.getSendstates()) { |
| | | serviceSubtaskEntity.setSendstate(sendstate); |
| | | List<ServiceSubtask> serviceSubtasks = serviceSubtaskMapper.selectServiceSubtaskList(serviceSubtaskEntity); |
| | | serviceSubtaskList.addAll(serviceSubtasks); |
| | | } |
| | | } |
| | | return serviceSubtaskList; |
| | | } |
| | |
| | | result.putIfAbsent("wxsf", 0); |
| | | result.putIfAbsent("xsf", 0); |
| | | result.putIfAbsent("dsf", 0); |
| | | result.putIfAbsent("ywc", 0); |
| | | result.putIfAbsent("cs", 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("cs", 0); |
| | | result.putIfAbsent("ywc", 0); |
| | | result.putIfAbsent("yc", 0); |
| | | result.putIfAbsent("jg", 0); |
| | |
| | | * @return 结果 |
| | | */ |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public String addSubTaskAgain(ServiceSubtask serviceSubtask) { |
| | | ServiceSubtask serviceSubtask1 = selectServiceSubtaskById(serviceSubtask.getId()); |
| | | PatMedInhosp patMedInhosp = new PatMedInhosp(); |
| | |
| | | PatArchive patArchive = patArchiveMapper.selectPatArchiveByPatid(serviceSubtask1.getPatid()); |
| | | //景宁 新增再次随访不能触发患者再入院 |
| | | boolean inhospAgain = true; |
| | | if (serviceSubtask1.getOrgid().equals("47255004333112711A1001")) { |
| | | if ("47255004333112711A1001".equals(serviceSubtask1.getOrgid())) { |
| | | inhospAgain = false; |
| | | } |
| | | if (patMedInhospList.size() > 0 && inhospAgain) { |
| | |
| | | serviceSubtask1.setRemark(StringUtils.isNotEmpty(serviceSubtask.getRemark()) ? serviceSubtask.getRemark() + ";无需再次随访,任务被标记不在执行" : "无需再次随访,任务被标记不在执行"); |
| | | serviceSubtaskMapper.updateServiceSubtask(serviceSubtask1); |
| | | return "无需再次随访,任务被标记不在执行"; |
| | | } else if (StringUtils.isNotEmpty(patArchive.getNotrequiredFlag()) && patArchive.getNotrequiredFlag().equals("1")) { |
| | | } else if (patArchive != null && StringUtils.isNotEmpty(patArchive.getNotrequiredFlag()) && patArchive.getNotrequiredFlag().equals("1")) { |
| | | serviceSubtask1.setIsVisitAgain(2); |
| | | serviceSubtaskMapper.updateServiceSubtask(serviceSubtask1); |
| | | serviceSubtask1.setRemark(StringUtils.isNotEmpty(serviceSubtask.getRemark()) ? serviceSubtask.getRemark() + ";无需再次随访,患者被标记不需要被服务" : "无需再次随访,患者被标记不需要被服务"); |
| | | return "无需再次随访,患者被标记不需要被服务"; |
| | | } else if (ObjectUtils.isNotEmpty(serviceSubtask1.getIsVisitAgain()) && serviceSubtask1.getIsVisitAgain() == 2) { |
| | | return "无需再次随访,该任务已经再次随访过了"; |
| | | } |
| | | //丽水中医院根据下次任务ID生成再次随访 |
| | | if ("47231022633110211A2101".equals(serviceSubtask1.getOrgid())) { |
| | | return addSubTaskAgainNextTaskId(serviceSubtask1); |
| | | } |
| | | //新增再次随访任务 |
| | | ServiceSubtask serviceSubtask2 = DtoConversionUtils.sourceToTarget(serviceSubtask, ServiceSubtask.class); |
| | |
| | | return "再次随访任务创建成功"; |
| | | } |
| | | return "再次随访任务创建失败,再请次重试"; |
| | | } |
| | | |
| | | public String addSubTaskAgainNextTaskId(ServiceSubtask serviceSubtask) { |
| | | if (ObjectUtils.isEmpty(serviceSubtask.getTaskid())) { |
| | | log.error("再次随访任务创建失败,缺少TaskId, 再请次重试"); |
| | | return "再次随访任务创建失败,缺少TaskId, 再请次重试"; |
| | | } |
| | | // 同一原任务串行生成,防止并发重复插入 |
| | | String lockToken = tryLockAgainTask(serviceSubtask.getId()); |
| | | if (lockToken == null) { |
| | | return "再次随访任务正在创建中,请勿重复提交"; |
| | | } |
| | | try { |
| | | ServiceTask serviceTask = serviceTaskService.selectServiceTaskByTaskid(serviceSubtask.getTaskid()); |
| | | if (ObjectUtils.isEmpty(serviceTask)) { |
| | | log.error("再次随访任务创建失败,找不到对应任务 taskid = {}, 再请次重试", serviceSubtask.getTaskid()); |
| | | return "再次随访任务创建失败,找不到对应任务 taskid = " + serviceSubtask.getTaskid() + ", 再请次重试"; |
| | | } |
| | | // 幂等校验:同一原任务只允许生成一条二次随访 |
| | | ServiceSubtaskEntity againQuery = new ServiceSubtaskEntity(); |
| | | againQuery.setUpid(serviceSubtask.getId().intValue()); |
| | | if (CollectionUtils.isNotEmpty(serviceSubtaskMapper.selectServiceSubtaskList(againQuery))) { |
| | | return "无需再次随访,该任务已经再次随访过了"; |
| | | } |
| | | // 查询出院记录 |
| | | PatMedInhosp patMedInhospVo = new PatMedInhosp(); |
| | | patMedInhospVo.setOrgid(serviceSubtask.getOrgid()); |
| | | patMedInhospVo.setInhospid(serviceSubtask.getInhospid()); |
| | | patMedInhospVo.setCry(1); |
| | | List<PatMedInhosp> patMedInhospList = patMedInhospService.selectPatMedInhospList(patMedInhospVo); |
| | | PatMedInhosp patMedInhosp = new PatMedInhosp(); |
| | | if (CollectionUtils.isNotEmpty(patMedInhospList)) { |
| | | patMedInhosp = patMedInhospList.get(0); |
| | | } else { |
| | | log.error("再次随访任务创建失败,任务缺少 对应的出院记录, taskid = {}, 再请次重试", serviceSubtask.getTaskid()); |
| | | return "再次随访任务创建失败,任务缺少 对应的出院记录, taskid = " + serviceSubtask.getTaskid() + ", 再请次重试"; |
| | | } |
| | | if (ObjectUtils.isEmpty(serviceTask.getNexttaskid())) { |
| | | log.error("再次随访任务创建失败,任务缺少 nexttaskid, taskid = {}, 再请次重试", serviceSubtask.getTaskid()); |
| | | return "再次随访任务创建失败,任务缺少 nexttaskid, taskid = " + serviceSubtask.getTaskid() + ", 再请次重试"; |
| | | } |
| | | ServiceTask nextServiceTask = serviceTaskService.selectServiceTaskByTaskid(serviceTask.getNexttaskid()); |
| | | if (ObjectUtils.isEmpty(nextServiceTask)) { |
| | | log.error("再次随访任务创建失败,找不到对应任务 nexttaskid = {}, 再请次重试", serviceTask.getNexttaskid()); |
| | | return "再次随访任务创建失败,找不到对应任务 nexttaskid = " + serviceTask.getNexttaskid() + ", 再请次重试"; |
| | | } |
| | | PatArchive patArchive = patArchiveService.selectPatArchiveByPatid(serviceSubtask.getPatid()); |
| | | if (ObjectUtils.isEmpty(patArchive)) { |
| | | log.error("再次随访任务创建失败,未找到患者档案 patid = {}, subTaskId = {}", serviceSubtask.getPatid(), serviceSubtask.getId()); |
| | | return "再次随访任务创建失败,未找到患者档案, subTaskId = " + serviceSubtask.getId() + ", 再请次重试"; |
| | | } |
| | | String config = configService.selectConfigByKey("visit.early.day"); |
| | | Integer type = 1; |
| | | // 随访中心科室 |
| | | String centerDept = configService.selectConfigByKey("sys.smartor.center.dept", serviceSubtask.getOrgid()); |
| | | if (ObjectUtils.isEmpty(centerDept)) { |
| | | log.error("再次随访任务创建失败,未配置随访中心科室参数 sys.smartor.center.dept ,subTaskId = {}, 再请次重试", serviceSubtask.getId()); |
| | | return "再次随访任务创建失败,未配置随访中心科室参数 sys.smartor.center.dept ,subTaskId = " + serviceSubtask.getId() + ", 再请次重试"; |
| | | } |
| | | SysDept sysDeptVo = new SysDept(); |
| | | sysDeptVo.setDeptType("1"); |
| | | sysDeptVo.setOrgid(serviceSubtask.getOrgid()); |
| | | sysDeptVo.setDeptCode(centerDept); |
| | | List<SysDept> sysDepts = sysDeptService.selectDeptList(sysDeptVo); |
| | | SysDept sysDept = new SysDept(); |
| | | if (CollectionUtils.isNotEmpty(sysDepts)) { |
| | | sysDept = sysDepts.get(0); |
| | | } else { |
| | | log.error("再次随访任务创建失败,未找到参数 sys.smartor.center.dept 配置的随访中心科室 deptCode = {}, 再请次重试", centerDept); |
| | | return "再次随访任务创建失败,未找到参数 sys.smartor.center.dept 配置的随访中心科室 deptCode = " + centerDept + ", 再请次重试"; |
| | | } |
| | | // 按下一任务生成二次随访子任务(表单、发送方式取自下一任务) |
| | | ServiceSubtask serviceSubtaskAgain = patMedInhospService.boxedServiceSubtask(nextServiceTask, |
| | | patMedInhosp, patArchive, config, type); |
| | | if (ObjectUtils.isEmpty(serviceSubtaskAgain) || ObjectUtils.isEmpty(serviceSubtaskAgain.getTaskid())) { |
| | | log.error("再次随访任务创建失败,下一任务生成子任务为空, subTaskId = {}, nextTaskId = {}", serviceSubtask.getId(), serviceTask.getNexttaskid()); |
| | | return "再次随访任务创建失败,患者出院记录不完整或下一任务配置异常, 再请次重试"; |
| | | } |
| | | Date now = new Date(); |
| | | serviceSubtaskAgain.setId(null); |
| | | serviceSubtaskAgain.setUpid(serviceSubtask.getId().intValue()); |
| | | serviceSubtaskAgain.setVisitCount(ObjectUtils.isNotEmpty(serviceSubtask.getVisitCount()) |
| | | ? serviceSubtask.getVisitCount() + 1 : 1); |
| | | serviceSubtaskAgain.setSendstate(2L); |
| | | serviceSubtaskAgain.setIsVisitAgain(1); |
| | | serviceSubtaskAgain.setCreateTime(now); |
| | | serviceSubtaskAgain.setUpdateTime(now); |
| | | |
| | | // // 二次随访时间:上次随访完成时间(未完成则当前时间)+ 下一任务发送天数;过去时间兜底为当前时间 |
| | | // Date baseTime = ObjectUtils.isNotEmpty(serviceSubtask.getFinishtime()) ? serviceSubtask.getFinishtime() : now; |
| | | // int intervalDays = ObjectUtils.isNotEmpty(nextServiceTask.getSendDay()) |
| | | // ? nextServiceTask.getSendDay().intValue() : 0; |
| | | // Date nextVisitTime = DateUtils.addDays(baseTime, intervalDays); |
| | | // if (nextVisitTime.before(now)) { |
| | | // nextVisitTime = now; |
| | | // } |
| | | // serviceSubtaskAgain.setVisitTime(nextVisitTime); |
| | | // serviceSubtaskAgain.setLongSendTime(nextVisitTime); |
| | | serviceSubtaskAgain.setVisitType(serviceSubtask.getVisitType()); |
| | | serviceSubtaskAgain.setVisitDeptCode(sysDept.getDeptCode()); |
| | | serviceSubtaskAgain.setVisitDeptName(sysDept.getDeptName()); |
| | | int i = serviceSubtaskMapper.insertServiceSubtask(serviceSubtaskAgain); |
| | | if (i != 1) { |
| | | return "再次随访任务创建失败,再请次重试"; |
| | | } |
| | | // 初始化发送方式明细,补偿(compensateTask)和超时判断依赖该表 |
| | | patMedInhospService.addServiceSubtaskPreachform(serviceSubtaskAgain, null); |
| | | // 条件更新原任务:0 行说明已被其他请求生成,抛异常回滚本次插入的重复数据 |
| | | int updated = serviceSubtaskMapper.updateIsVisitAgainById(serviceSubtask.getId(), 2); |
| | | if (updated != 1) { |
| | | throw new BaseException("无需再次随访,该任务已经再次随访过了"); |
| | | } |
| | | return "再次随访任务创建成功"; |
| | | } finally { |
| | | unlockAgainTask(serviceSubtask.getId(), lockToken); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 获取二次随访生成锁。Redis 异常时降级为数据库幂等校验,不阻断业务。 |
| | | */ |
| | | private String tryLockAgainTask(Long subId) { |
| | | String token = UUID.randomUUID().toString(); |
| | | try { |
| | | Boolean locked = redisTemplate.opsForValue().setIfAbsent( |
| | | AGAIN_LOCK_KEY_PREFIX + subId, token, AGAIN_LOCK_SECONDS, TimeUnit.SECONDS); |
| | | return Boolean.TRUE.equals(locked) ? token : null; |
| | | } catch (Exception e) { |
| | | log.warn("再次随访获取Redis锁失败,降级使用数据库幂等校验, subId={}", subId, e); |
| | | return token; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 释放二次随访生成锁,仅释放自己持有的锁。 |
| | | */ |
| | | private void unlockAgainTask(Long subId, String token) { |
| | | if (token == null) { |
| | | return; |
| | | } |
| | | try { |
| | | String lockKey = AGAIN_LOCK_KEY_PREFIX + subId; |
| | | Object current = redisTemplate.opsForValue().get(lockKey); |
| | | if (token.equals(current)) { |
| | | redisTemplate.delete(lockKey); |
| | | } |
| | | } catch (Exception e) { |
| | | log.warn("再次随访释放Redis锁失败, subId={}", subId, e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | |
| | | /** |
| | | * 结束语缓存 key(只要写过该 key 就说明本次对话已经收尾,结束语为空也算) |
| | | */ |
| | | private String jsyKey(String uuid) { |
| | | return uuid + "- jsy"; |
| | | } |
| | | |
| | | /** |
| | | * 本次对话是否已经收尾(即已经下发过结束语)。 |
| | | * <p> |
| | | * 注意:不能用 getCacheObject(...) != null 判断,结束语为空时写入的是空值, |
| | | * 读出来是 null 但 key 是存在的,必须用 hasKey。 |
| | | */ |
| | | private boolean isDialogueEnded(String uuid) { |
| | | return StringUtils.isNotEmpty(uuid) && Boolean.TRUE.equals(redisCache.hasKey(jsyKey(uuid))); |
| | | } |
| | | |
| | | /** |
| | | * 挂断电话:幂等 + 重试 + 异常不外抛 + 清理结束语缓存。 |
| | | * <p> |
| | | * 平台回调(PlayEventCallback 播放结束)和兜底定时任务都会调这里, |
| | | * 通过 redis 的 {uuid}- hangup-sent 标记保证同一次通话只真正挂断一次。 |
| | | * |
| | | * @param uuid 通话 uuid |
| | | * @param phone 被叫号码 |
| | | * @param reason 挂断原因(仅用于日志排查) |
| | | * @return true=本次确实发出了挂断请求 |
| | | */ |
| | | private boolean doHangup(String uuid, String phone, String reason) { |
| | | if (StringUtils.isEmpty(uuid)) { |
| | | log.warn("挂断跳过:uuid 为空, reason={}", reason); |
| | | return false; |
| | | } |
| | | String sentKey = uuid + "- hangup-sent"; |
| | | if (!redisCache.setCacheObjectIfAbsent(sentKey, "1", 30, TimeUnit.MINUTES)) { |
| | | log.info("挂断跳过:该通话已挂断过, uuid={}, reason={}", uuid, reason); |
| | | return false; |
| | | } |
| | | Map<String, String> req = new HashMap<>(); |
| | | req.put("uuid", uuid); |
| | | req.put("caller", phone); |
| | | String body = new Gson().toJson(req); |
| | | for (int i = 1; i <= HANGUP_MAX_RETRY; i++) { |
| | | try { |
| | | // 只校验 HTTP 状态码:/hangup 通常成功只回 200 且没有响应体, |
| | | // 用 postJsonRequest 会因"响应为null"把成功误判成失败 |
| | | HttpUtil.postJsonRequestAllowEmptyResponse(hangup, body); |
| | | log.info("挂断成功, uuid={}, reason={}, 第{}次, url={}", uuid, reason, i, hangup); |
| | | redisCache.setCacheObject(uuid + "- hangup", "1", 10, TimeUnit.MINUTES); |
| | | redisCache.deleteObject(jsyKey(uuid)); |
| | | redisCache.deleteObject(uuid + "- hangup-planned"); |
| | | return true; |
| | | } catch (Exception e) { |
| | | //把异常类型和信息放在同一行,单行 grep 就能看出是"连不上"还是"返回非200" |
| | | log.error("挂断失败, uuid={}, reason={}, 第{}次, url={}, body={}, 原因={}: {}", |
| | | uuid, reason, i, hangup, body, e.getClass().getSimpleName(), e.getMessage(), e); |
| | | if (i < HANGUP_MAX_RETRY) { |
| | | try { |
| | | Thread.sleep(HANGUP_RETRY_INTERVAL_MS); |
| | | } catch (InterruptedException ie) { |
| | | Thread.currentThread().interrupt(); |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | // 全部失败:释放幂等标记,允许平台下一次回调或下一个兜底任务重试 |
| | | redisCache.deleteObject(sentKey); |
| | | log.error("挂断最终失败,已释放重试标记, uuid={}, reason={}, url={}", uuid, reason, hangup); |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * 安排一次兜底挂断。 |
| | | * <p> |
| | | * 结束语下发后,平台正常应该再回调 PlayEventCallback(playstop=true),此时由 doHangup 立即挂断; |
| | | * 但平台字段缺失/不回调时电话会一直挂着,所以这里按结束语字数估算播放时长,到点兜底挂一次。 |
| | | * 同一个 uuid 只安排一次({uuid}- hangup-planned 幂等)。 |
| | | * |
| | | * @param uuid 通话 uuid |
| | | * @param phone 被叫号码 |
| | | * @param jsy 结束语(可能为空,为空时按最小延时处理) |
| | | */ |
| | | private void scheduleHangupFallback(String uuid, String phone, String jsy) { |
| | | if (StringUtils.isEmpty(uuid)) { |
| | | return; |
| | | } |
| | | if (!redisCache.setCacheObjectIfAbsent(uuid + "- hangup-planned", "1", 30, TimeUnit.MINUTES)) { |
| | | return; |
| | | } |
| | | // 中文 TTS 大约每秒 3~4 个字,留足余量后再加上最小延时 |
| | | int len = StringUtils.isEmpty(jsy) ? 0 : jsy.length(); |
| | | long delay = HANGUP_FALLBACK_MIN_DELAY_MS + len * 400L; |
| | | if (delay > 60000L) { |
| | | delay = 60000L; |
| | | } |
| | | log.info("安排兜底挂断, uuid={}, 结束语长度={}, 延时={}ms", uuid, len, delay); |
| | | getHangupFallbackExecutor().schedule(() -> { |
| | | try { |
| | | doHangup(uuid, phone, "结束语播完兜底"); |
| | | } catch (Exception e) { |
| | | log.error("兜底挂断异常, uuid={}", uuid, e); |
| | | } |
| | | }, delay, TimeUnit.MILLISECONDS); |
| | | } |
| | | |
| | | /** |
| | | * 兜底挂断线程池(懒加载,守护线程,进程退出不受影响) |
| | | */ |
| | | private ScheduledExecutorService getHangupFallbackExecutor() { |
| | | if (hangupFallbackExecutor == null) { |
| | | synchronized (this) { |
| | | if (hangupFallbackExecutor == null) { |
| | | hangupFallbackExecutor = Executors.newScheduledThreadPool(2, r -> { |
| | | Thread t = new Thread(r, "voice-hangup-fallback"); |
| | | t.setDaemon(true); |
| | | return t; |
| | | }); |
| | | } |
| | | } |
| | | } |
| | | return hangupFallbackExecutor; |
| | | } |
| | | |
| | | /** |
| | | * 电话ASR通话回调(雨绮) |
| | | * |
| | | * @param phoneCallReqYQVO |
| | |
| | | //先把该subId从cache_exist中剔除,只要有回调就剔除(目的是,尽快让cache-exist为空,去缓存下一批要打的电话;这里剔除不影响电话的正常业务) |
| | | // redisCache.removeElementFromList("cache-exist", phoneCallReqYQVO.getTaskid()); |
| | | |
| | | //uuid / taskid / phone 是后面所有逻辑的前提,缺了就直接忽略,避免 Long.valueOf(null)、null.trim() 之类的 500 |
| | | if (StringUtils.isEmpty(phoneCallReqYQVO.getUuid()) || StringUtils.isEmpty(phoneCallReqYQVO.getTaskid()) |
| | | || StringUtils.isEmpty(phoneCallReqYQVO.getPhone())) { |
| | | log.warn("雨绮回调缺少 uuid/taskid/phone,直接忽略: operate={}, uuid={}, taskid={}, phone={}", |
| | | phoneCallReqYQVO.getOperate(), phoneCallReqYQVO.getUuid(), |
| | | phoneCallReqYQVO.getTaskid(), phoneCallReqYQVO.getPhone()); |
| | | return new PhoneCallBackYQVO(); |
| | | } |
| | | |
| | | //判断是否挂机 |
| | | String hangupState = redisCache.getCacheObject(phoneCallReqYQVO.getUuid() + "- hangup"); |
| | | Object cacheObject1 = redisCache.getCacheObject(phoneCallReqYQVO.getTaskid() + "recordAccept-hungup"); |
| | |
| | | } |
| | | if ("AsrCallback".equals(phoneCallReqYQVO.getOperate()) && phoneCallReqYQVO.getEnd_time() != null && phoneCallReqYQVO.getEnd_time() != -1) { |
| | | //处理挂断 |
| | | if (phoneCallReqYQVO.getAsrtext().contains("拨叫的用户正忙")) { |
| | | if (StringUtils.isNotEmpty(phoneCallReqYQVO.getAsrtext()) && phoneCallReqYQVO.getAsrtext().contains("拨叫的用户正忙")) { |
| | | //判断 recordAccept-hungup 是否有值,如果有,则说明不用处理了,之前已经处理过了(recordAccept-hungup这个主要是给"通话记录用的,") |
| | | Object cacheObject = redisCache.getCacheObject(phoneCallReqYQVO.getTaskid() + "recordAccept-hungup"); |
| | | if (ObjectUtils.isNotEmpty(cacheObject) && String.valueOf(cacheObject).equals("1")) { |
| | |
| | | //如果发送状态为6,就说明已经打完了,也没有必要继续往下走了 |
| | | if (StringUtils.isNotEmpty(phoneCallReqYQVO.getTaskid())) { |
| | | ServiceSubtask serviceSubtask = serviceSubtaskMapper.selectServiceSubtaskById(Long.valueOf(phoneCallReqYQVO.getTaskid())); |
| | | if (serviceSubtask.getSendstate() == 6L) return new PhoneCallBackYQVO(); |
| | | if (serviceSubtask != null && serviceSubtask.getSendstate() != null && serviceSubtask.getSendstate() == 6L) { |
| | | return new PhoneCallBackYQVO(); |
| | | } |
| | | } |
| | | |
| | | ServiceSubtask serviceSubtask2 = new ServiceSubtask(); |
| | |
| | | serviceSubtask2.setSenduuid(phoneCallReqYQVO.getUuid()); |
| | | serviceSubtask2.setId(Long.valueOf(phoneCallReqYQVO.getTaskid())); |
| | | serviceSubtaskMapper.updateServiceSubtask(serviceSubtask2); |
| | | Map<String, String> req = new HashMap<>(); |
| | | req.put("uuid", phoneCallReqYQVO.getUuid()); |
| | | req.put("caller", phoneCallReqYQVO.getPhone()); |
| | | HttpUtil.postJsonRequest(hangup, new Gson().toJson(req)); |
| | | redisCache.setCacheObject(phoneCallReqYQVO.getUuid() + "- hangup", "1", 2, TimeUnit.MINUTES); |
| | | //挂断(幂等 + 失败重试,异常不再往外抛,避免回调直接 500) |
| | | doHangup(phoneCallReqYQVO.getUuid(), phoneCallReqYQVO.getPhone(), "拨叫的用户正忙"); |
| | | //先把默认的recordAccept-hungup删除,再更新 |
| | | redisCache.deleteObject(phoneCallReqYQVO.getTaskid() + "recordAccept-hungup"); |
| | | redisCache.setCacheObject(phoneCallReqYQVO.getTaskid() + "recordAccept-hungup", "1", 10, TimeUnit.MINUTES); |
| | |
| | | return new PhoneCallBackYQVO(); |
| | | } |
| | | } |
| | | //结束语已经下发过,说明本次对话已经收尾:安排一次兜底挂断。 |
| | | //平台正常情况下会再回调 PlayEventCallback(playstop=true) 让我们立刻挂断; |
| | | //但平台字段缺失/不回调时电话会一直挂着,所以这里按结束语字数估算播放时长到点兜底挂一次(幂等)。 |
| | | if (isDialogueEnded(phoneCallReqYQVO.getUuid())) { |
| | | scheduleHangupFallback(phoneCallReqYQVO.getUuid(), phoneCallReqYQVO.getPhone(), |
| | | redisCache.getCacheObject(jsyKey(phoneCallReqYQVO.getUuid()))); |
| | | } |
| | | //定义一个分数的变量 |
| | | Boolean aBoolean1 = redisCache.hasKey(phoneCallReqYQVO.getUuid() + "SCORE"); |
| | | if (!aBoolean1) redisCache.setCacheObject(phoneCallReqYQVO.getUuid() + "SCORE", 0.0, 120, TimeUnit.MINUTES); |
| | | PhoneCallBackYQVO phoneCallBackYQVO = new PhoneCallBackYQVO(); |
| | | //channel_create 通道创建的时候,可以执行一些其它操作,譬如发个短信之类的; 我们的业务可以不用管 PlayEventCallback 这个是播放语音的,暂时用不到 End_time()= -1或null表示当前的asrtext不是一句完整的话 |
| | | if (phoneCallReqYQVO.getOperate().equals("channel_create")) { |
| | | if ("channel_create".equals(phoneCallReqYQVO.getOperate())) { |
| | | return phoneCallBackYQVO; |
| | | } |
| | | //PlayEventCallback 这个是播放语音的 playstart:放音开始(问题播报开始) playstop: 放音结束(问题播报结束) |
| | | if (phoneCallReqYQVO.getOperate().equals("PlayEventCallback")) { |
| | | String cacheJSY = redisCache.getCacheObject(phoneCallReqYQVO.getUuid() + "- jsy"); |
| | | if (phoneCallReqYQVO.getOperate().equals("PlayEventCallback") && phoneCallReqYQVO.getPlaystop() == false) { |
| | | //判断redis中是否有结束语 |
| | | if (StringUtils.isEmpty(cacheJSY)) { |
| | | if ("PlayEventCallback".equals(phoneCallReqYQVO.getOperate())) { |
| | | //结束语是否已下发(用 hasKey 判断,结束语为空也算已收尾) |
| | | boolean dialogueEnded = isDialogueEnded(phoneCallReqYQVO.getUuid()); |
| | | //playstop 可能是 null,平台也可能用 play_stop,统一由 VO 判断 |
| | | boolean playStop = phoneCallReqYQVO.playStopIsTrue(); |
| | | String cacheJSY = redisCache.getCacheObject(jsyKey(phoneCallReqYQVO.getUuid())); |
| | | log.info("PlayEventCallback: uuid={}, playstop={}, 是否已下发结束语={}", |
| | | phoneCallReqYQVO.getUuid(), playStop, dialogueEnded); |
| | | if (!playStop) { |
| | | //放音开始 |
| | | if (!dialogueEnded) { |
| | | redisCache.setCacheObject(phoneCallReqYQVO.getUuid() + "PlayEventCallbackPlaystop", false, 120, TimeUnit.MINUTES); |
| | | log.error("PlayEventCallbackPlaystop的值为-------:{}", false); |
| | | } else { |
| | | //平台开始播结束语,兜底挂断在方法入口已经安排过,这里只留日志便于排查 |
| | | log.info("结束语开始播放, uuid={}, 结束语={}", phoneCallReqYQVO.getUuid(), cacheJSY); |
| | | } |
| | | } else { |
| | | //如果结束语不为空,则要挂电话了 |
| | | if (StringUtils.isNotEmpty(cacheJSY)) { |
| | | Map<String, String> req = new HashMap<>(); |
| | | req.put("uuid", phoneCallReqYQVO.getUuid()); |
| | | req.put("caller", phoneCallReqYQVO.getPhone()); |
| | | // HttpUtils.sendPost(hangup, new Gson().toJson(req)); |
| | | HttpUtil.postJsonRequest(hangup, new Gson().toJson(req)); |
| | | ServiceSubtask serviceSubtask = serviceSubtaskMapper.selectServiceSubtaskById(Long.valueOf(phoneCallReqYQVO.getTaskid())); |
| | | //记录电话发送状态 |
| | | setFailPreachForm(serviceSubtask, "3", "通话正常结束", "9"); |
| | | //删除结束语的缓存 |
| | | redisCache.deleteObject(phoneCallReqYQVO.getUuid() + "- jsy"); |
| | | //放音结束 |
| | | if (dialogueEnded) { |
| | | //结束语播完,立即挂断(幂等 + 失败重试,异常不外抛) |
| | | doHangup(phoneCallReqYQVO.getUuid(), phoneCallReqYQVO.getPhone(), "结束语播放结束"); |
| | | //记录电话发送状态:挂断成败都要落库,不能再让回调直接 500 |
| | | if (StringUtils.isNotEmpty(phoneCallReqYQVO.getTaskid())) { |
| | | try { |
| | | ServiceSubtask serviceSubtask = serviceSubtaskMapper.selectServiceSubtaskById(Long.valueOf(phoneCallReqYQVO.getTaskid())); |
| | | setFailPreachForm(serviceSubtask, "3", "通话正常结束", "9"); |
| | | } catch (Exception e) { |
| | | log.error("挂断后更新子任务状态失败, taskid={}", phoneCallReqYQVO.getTaskid(), e); |
| | | } |
| | | } |
| | | } else { |
| | | redisCache.setCacheObject(phoneCallReqYQVO.getUuid() + "PlayEventCallbackPlaystop", true, 120, TimeUnit.MINUTES); |
| | | log.error("-------PlayEventCallbackPlaystop的值为:{}", true); |
| | |
| | | |
| | | //通过子任务ID获取到模板信息 |
| | | ServiceSubtask serviceSubtask = serviceSubtaskMapper.selectServiceSubtaskById(Long.valueOf(phoneCallReqYQVO.getTaskid())); |
| | | if (serviceSubtask == null) { |
| | | log.warn("回调对应的子任务不存在,直接忽略, taskid={}, operate={}", phoneCallReqYQVO.getTaskid(), phoneCallReqYQVO.getOperate()); |
| | | return phoneCallBackYQVO; |
| | | } |
| | | IvrTaskTemplate ivrTaskTemplate = ivrTaskTemplateService.selectIvrTaskTemplateByID(serviceSubtask.getTemplateid()); |
| | | //获取模板问题 |
| | | IvrTaskTemplateScript ivrTaskTemplateScript = new IvrTaskTemplateScript(); |
| | |
| | | //获取问题ID 和 序号 |
| | | String scriptId = redisCache.getCacheObject(phoneCallReqYQVO.getTaskid().trim() + "-" + phoneCallReqYQVO.getPhone().trim()); |
| | | log.error("scriptId是多少:{}", scriptId); |
| | | if (StringUtils.isEmpty(scriptId)) { |
| | | //当前没有"正在问的问题",说明本次通话已经收尾(结束语已下发后,平台还会补 Silent/Asr 回调), |
| | | //原来这里直接 Long.valueOf(null) 会抛 NumberFormatException 让回调 500。 |
| | | //这里直接忽略,并确保兜底挂断已经排上,避免电话继续挂着。 |
| | | log.warn("未取到当前问题ID,按通话已收尾处理, taskid={}, phone={}, operate={}", |
| | | phoneCallReqYQVO.getTaskid(), phoneCallReqYQVO.getPhone(), phoneCallReqYQVO.getOperate()); |
| | | if (isDialogueEnded(phoneCallReqYQVO.getUuid())) { |
| | | scheduleHangupFallback(phoneCallReqYQVO.getUuid(), phoneCallReqYQVO.getPhone(), |
| | | redisCache.getCacheObject(jsyKey(phoneCallReqYQVO.getUuid()))); |
| | | } |
| | | return phoneCallBackYQVO; |
| | | } |
| | | //当前题的信息 |
| | | IvrTaskTemplateScriptVO ivrTaskTemplateScriptVO = iIvrTaskTemplateScriptService.getTaskTempScriptInfoByid(Long.valueOf(scriptId)); |
| | | if (ivrTaskTemplateScriptVO == null) { |
| | | log.warn("当前问题不存在,忽略回调, taskid={}, scriptId={}", phoneCallReqYQVO.getTaskid(), scriptId); |
| | | return phoneCallBackYQVO; |
| | | } |
| | | //判断UUID是否存在 |
| | | Boolean aBoolean = redisCache.hasKey(phoneCallReqYQVO.getUuid()); |
| | | if (!aBoolean) { |
| | |
| | | redisCache.deleteObject(phoneCallReqYQVO.getUuid() + "PlayEventCallbackPlaystop"); |
| | | // //获取下一题 |
| | | log.error("获取下一题的信息:{}", ivrTaskTemplateScriptVO); |
| | | if (ivrTaskTemplateScriptVO.getBranchFlag().equals("1") || ivrTaskTemplateScriptVO.getBranchFlag().equals("0") && ivrTaskTemplateScriptVO.getNextScriptno() != null && ivrTaskTemplateScriptVO.getNextScriptno() != 0) { |
| | | //branchFlag 可能为 null,统一按分支0处理,避免 NPE 把回调打 500 |
| | | if ("1".equals(ivrTaskTemplateScriptVO.getBranchFlag()) || "0".equals(ivrTaskTemplateScriptVO.getBranchFlag()) && ivrTaskTemplateScriptVO.getNextScriptno() != null && ivrTaskTemplateScriptVO.getNextScriptno() != 0) { |
| | | Long nextQuestion = null; |
| | | if (ivrTaskTemplateScriptVO.getBranchFlag().equals("1")) { |
| | | if ("1".equals(ivrTaskTemplateScriptVO.getBranchFlag())) { |
| | | nextQuestion = ivrTaskTemplateScriptVO.getIvrTaskScriptTargetoptionList().get(j).getNextQuestion(); |
| | | //更新分数 |
| | | double score = redisCache.getCacheObject(phoneCallReqYQVO.getUuid() + "SCORE"); |
| | |
| | | if (ObjectUtils.isNotEmpty(countObj)) count = (Integer) countObj; |
| | | if (count != null && count >= ivrTaskTemplate.getMateNum()) { |
| | | //如果count已经大于或等于没有匹配次数 |
| | | if (ivrTaskTemplateScriptVO.getBranchFlag().equals("0") && ivrTaskTemplateScriptVO.getNextScriptno() == null || ivrTaskTemplateScriptVO.getBranchFlag().equals("0") && ivrTaskTemplateScriptVO.getNextScriptno() == 0 || ivrTaskTemplateScriptVO.getBranchFlag().equals("1") && ivrTaskTemplateScriptVO.getNextScriptno() == null || ivrTaskTemplateScriptVO.getBranchFlag().equals("1") && ivrTaskTemplateScriptVO.getNextScriptno() == 0) { |
| | | //branchFlag 可能为 null,统一按分支0处理,避免 NPE 把回调打 500 |
| | | if ("0".equals(ivrTaskTemplateScriptVO.getBranchFlag()) && ivrTaskTemplateScriptVO.getNextScriptno() == null || "0".equals(ivrTaskTemplateScriptVO.getBranchFlag()) && ivrTaskTemplateScriptVO.getNextScriptno() == 0 || "1".equals(ivrTaskTemplateScriptVO.getBranchFlag()) && ivrTaskTemplateScriptVO.getNextScriptno() == null || "1".equals(ivrTaskTemplateScriptVO.getBranchFlag()) && ivrTaskTemplateScriptVO.getNextScriptno() == 0) { |
| | | //如果是最后一道题,或者没有下一题了,就直接挂机 |
| | | ServiceTask serviceTask1 = serviceTaskService.selectServiceTaskByTaskid(serviceSubtask.getTaskid()); |
| | | phoneCallBackYQVO.setType("text"); |
| | |
| | | @Override |
| | | public Integer recordAccept(PhoneCallRecordVO phoneCallRecordVO) { |
| | | Object cacheObject = redisCache.getCacheObject(phoneCallRecordVO.getTaskid() + "recordAccept-hungup"); |
| | | if (phoneCallRecordVO.getHangup_cause().equals("NO_USER_RESPONSE") || ObjectUtils.isNotEmpty(cacheObject) && String.valueOf(cacheObject).equals("1")) { |
| | | // hangup_cause 平台不一定每次都带,这里判空,避免整条通话记录回调直接 NPE 500 |
| | | boolean noUserResponse = "NO_USER_RESPONSE".equals(phoneCallRecordVO.getHangup_cause()); |
| | | if (noUserResponse || ObjectUtils.isNotEmpty(cacheObject) && String.valueOf(cacheObject).equals("1")) { |
| | | //无人接听 |
| | | //记录电话发送状态 |
| | | ServiceSubtask serviceSubtask = serviceSubtaskMapper.selectServiceSubtaskById(Long.valueOf(phoneCallRecordVO.getTaskid())); |
| | |
| | | // if (phoneCallRecordVO.getAnswered() != null && phoneCallRecordVO.getAnswered() == true) { |
| | | log.error("有人接电话了,并且完了"); |
| | | //说明有人接了 |
| | | int startIndex = phoneCallRecordVO.getRecord_path().indexOf("voice") + "voice".length() + 1; // 加1是跳过/符号 |
| | | int endIndex = phoneCallRecordVO.getRecord_path().lastIndexOf("/"); |
| | | String result = phoneCallRecordVO.getRecord_path().substring(startIndex, endIndex); |
| | | //录音路径只做日志留痕:平台给空/格式不符时原来的 substring 会抛异常,导致整条回调 500、状态和录音都不处理 |
| | | String recordPath = phoneCallRecordVO.getRecord_path(); |
| | | if (StringUtils.isNotEmpty(recordPath) && recordPath.contains("voice") && recordPath.lastIndexOf("/") > 0) { |
| | | log.info("通话录音路径, taskid={}, record_path={}", phoneCallRecordVO.getTaskid(), recordPath); |
| | | } else { |
| | | log.warn("通话记录回调未取到有效录音路径, taskid={}, record_path={}", phoneCallRecordVO.getTaskid(), recordPath); |
| | | } |
| | | ServiceSubtask serviceSubtask = new ServiceSubtask(); |
| | | serviceSubtask.setRemark("电话拨打已完成"); |
| | | serviceSubtask.setSendstate(6L); |
| | |
| | | * @return |
| | | */ |
| | | @Override |
| | | @Cacheable(value = "sfStatistics", key = "T(org.springframework.util.DigestUtils).md5DigestAsHex(#serviceSubtaskCountReq.toString().getBytes())", unless = "#result == null or #result.isEmpty()") |
| | | // @Cacheable(value = "sfStatistics", key = "T(org.springframework.util.DigestUtils).md5DigestAsHex(#serviceSubtaskCountReq.toString().getBytes())", unless = "#result == null or #result.isEmpty()") |
| | | public List<ServiceSubtaskStatistic> getSfStatistics(ServiceSubtaskCountReq serviceSubtaskCountReq) { |
| | | log.info("getSfStatistics的入参为:{}", serviceSubtaskCountReq); |
| | | String drcode = "drcode"; |
| | |
| | | ssRate.setOrgid(serviceSubtaskCountReq.getOrgid()); |
| | | ssRate.setStarttime(serviceSubtaskCountReq.getStartTime()); |
| | | ssRate.setEndtime(serviceSubtaskCountReq.getEndTime()); |
| | | ssRate.setStartOutHospTime(serviceSubtaskCountReq.getHospitalStartTime()); |
| | | ssRate.setEndOutHospTime(serviceSubtaskCountReq.getHospitalEndTime()); |
| | | ssRate.setVisitCount(serviceSubtaskCountReq.getVisitCount()); |
| | | ssRate.setServiceTypeList(serviceSubtaskCountReq.getServiceType()); |
| | | if (serviceSubtaskCountReq.getEndTime() != null && new Date().before(serviceSubtaskCountReq.getEndTime())) { |
| | |
| | | if (serviceSubtaskCountReq.getEndTime() != null && new Date().before(serviceSubtaskCountReq.getEndTime())) { |
| | | ss.setEndtime(new Date()); |
| | | } |
| | | ss.setStartOutHospTime(serviceSubtaskCountReq.getHospitalStartTime()); |
| | | ss.setEndOutHospTime(serviceSubtaskCountReq.getHospitalEndTime()); |
| | | Double jsRate = serviceSubtaskMapper.selectTimelyRate(ss); |
| | | statistic.setRate(jsRate); |
| | | if (ObjectUtils.isNotEmpty(statistic)) { |
| | |
| | | //应随访人次 |
| | | if (serviceSubtask.getSendstate() != null && serviceSubtask.getSendstate() != 4) { |
| | | serviceSubtaskStatistic.setFollowUpNeeded(serviceSubtaskStatistic.getFollowUpNeeded() + 1L); |
| | | } |
| | | |
| | | //人工已拨通总量 |
| | | if (ObjectUtils.isNotEmpty(serviceSubtask.getIsManuallyCalled()) |
| | | && serviceSubtask.getIsManuallyCalled().equals(1L)) { |
| | | serviceSubtaskStatistic.setManuallyCalled(serviceSubtaskStatistic.getManuallyCalled() + 1L); |
| | | } |
| | | |
| | | //首次出院随访 |
| | |
| | | * taskSituation4Info - taskSituation = 4 |
| | | * taskSituation5Info - taskSituation = 5 |
| | | * taskSituation6Info - taskSituation = 7 |
| | | * manuallyCalledInfo - isManuallyCalled = 1L |
| | | * filterCountList - taskSituation = 6 && sendstate = 4 |
| | | * |
| | | */ |
| | |
| | | serviceSubtaskCountReq.setVisitCount(1); |
| | | serviceSubtaskCountReq.setCurrentPreachform("4"); |
| | | break; |
| | | case "wechatInfo": |
| | | case "weChatInfo": |
| | | serviceSubtaskCountReq.setVisitCount(1); |
| | | serviceSubtaskCountReq.setCurrentPreachform("5"); |
| | | break; |
| | |
| | | serviceSubtaskCountReq.setSendstates(new ArrayList<>(Collections.singletonList(6L))); |
| | | serviceSubtaskCountReq.setCurrentPreachform("4"); |
| | | break; |
| | | case "wechatSuccessInfo": |
| | | case "weChatSuccessInfo": |
| | | serviceSubtaskCountReq.setVisitCount(1); |
| | | serviceSubtaskCountReq.setSendstates(new ArrayList<>(Collections.singletonList(6L))); |
| | | serviceSubtaskCountReq.setCurrentPreachform("5"); |
| | |
| | | serviceSubtaskCountReq.setVisitCount(2); |
| | | serviceSubtaskCountReq.setCurrentPreachform("4"); |
| | | break; |
| | | case "wechatAgainInfo": |
| | | case "weChatAgainInfo": |
| | | serviceSubtaskCountReq.setVisitCount(2); |
| | | serviceSubtaskCountReq.setCurrentPreachform("5"); |
| | | break; |
| | |
| | | serviceSubtaskCountReq.setSendstates(new ArrayList<>(Collections.singletonList(6L))); |
| | | serviceSubtaskCountReq.setCurrentPreachform("4"); |
| | | break; |
| | | case "wechatAgainSuccessInfo": |
| | | case "weChatAgainSuccessInfo": |
| | | serviceSubtaskCountReq.setVisitCount(2); |
| | | serviceSubtaskCountReq.setSendstates(new ArrayList<>(Collections.singletonList(6L))); |
| | | serviceSubtaskCountReq.setCurrentPreachform("5"); |
| | |
| | | case "filterCountList": |
| | | serviceSubtaskCountReq.setTaskSituation(6); |
| | | serviceSubtaskCountReq.setSendstates(new ArrayList<>(Collections.singletonList(4L))); |
| | | break; |
| | | case "manuallyCalledInfo": |
| | | serviceSubtaskCountReq.setIsManuallyCalled(1L); |
| | | break; |
| | | default: |
| | | break; |
| | |
| | | } |
| | | |
| | | @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 Map<String, Object> weChatSubTask(Long subid) { |
| | | log.info("smsSubTask的入参为:{}", subid); |
| | | Map<String, Object> resultMap = new HashMap<>(); |
| | | resultMap.put("code", "500"); |
| | | resultMap.put("msg", "发送失败"); |
| | | |
| | | //获取患者的基本信息 |
| | | ServiceSubtask serviceSubtask = selectServiceSubtaskById(subid); |
| | | 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); |
| | | |
| | | //丽水中医院发送的是微信公众号 |
| | | resultMap = wechatSubTask(subid); |
| | | 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, 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<>(); |
| | |
| | | // 如果有多条目标值,拆分成多行 |
| | | 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())) { |
| | |
| | | } |
| | | |
| | | 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(); |
| | | } |
| | | |
| | | /* |
| | |
| | | String voiceText = phoneCallReqYQVO.getAsrtext(); |
| | | String questionText = StringUtils.isNotEmpty(ivrTaskTemplateScriptVO.getIvrtext()) ? ivrTaskTemplateScriptVO.getIvrtext() : ivrTaskTemplateScriptVO.getScriptContent(); |
| | | Integer matched = -1; |
| | | log.info("AI匹配:url = {},questionText = {},voiceText = {},options = {},uuid ={}", |
| | | url,questionText,voiceText,options,uuid); |
| | | matched = matchOptionIndex(url, questionText, voiceText, options, uuid); |
| | | //说明匹配正确了 |
| | | if (matched != null && matched >= 0) { |
| | |
| | | phoneCallBackYQVO.setValue(phoneCallBackYQVO.getCommonValue() + serviceTask.getJsy()); |
| | | //记录状态 |
| | | setFailPreachForm(serviceSubtask, "3", "电话拨打已完成", "9"); |
| | | return phoneCallBackYQVO; |
| | | back = phoneCallBackYQVO; |
| | | return back; |
| | | } |
| | | |
| | | flag = 1; |
| | |
| | | redisCache.deleteObject(phoneCallReqYQVO.getUuid() + "PlayEventCallbackPlaystop"); |
| | | // //获取下一题 |
| | | log.error("获取下一题的信息:{}", ivrTaskTemplateScriptVO); |
| | | if (ivrTaskTemplateScriptVO.getBranchFlag().equals("1") || ivrTaskTemplateScriptVO.getBranchFlag().equals("0") && ivrTaskTemplateScriptVO.getNextScriptno() != null && ivrTaskTemplateScriptVO.getNextScriptno() != 0) { |
| | | //branchFlag为null时按分支0处理,避免NPE |
| | | if ("1".equals(ivrTaskTemplateScriptVO.getBranchFlag()) || "0".equals(ivrTaskTemplateScriptVO.getBranchFlag()) && ivrTaskTemplateScriptVO.getNextScriptno() != null && ivrTaskTemplateScriptVO.getNextScriptno() != 0) { |
| | | Long nextQuestion = null; |
| | | if (ivrTaskTemplateScriptVO.getBranchFlag().equals("1")) { |
| | | if ("1".equals(ivrTaskTemplateScriptVO.getBranchFlag())) { |
| | | nextQuestion = chosenOption.getNextQuestion(); |
| | | //更新分数 |
| | | double score = redisCache.getCacheObject(phoneCallReqYQVO.getUuid() + "SCORE"); |
| | |
| | | redisCache.setCacheObject(phoneCallReqYQVO.getUuid() + "- jsy", serviceTask.getJsy(), 120, TimeUnit.MINUTES); |
| | | phoneCallBackYQVO.setType("text"); |
| | | phoneCallBackYQVO.setValue(phoneCallBackYQVO.getCommonValue() + serviceTask.getJsy()); |
| | | //将结果写进detail表 |
| | | ServiceSubTaskDetailReq ssdReq = new ServiceSubTaskDetailReq(); |
| | | List<ServiceSubtaskDetail> serviceSubtaskDetailList2 = new ArrayList<>(); |
| | | serviceSubtaskDetailList.add(getServiceSubtaskDetail(phoneCallReqYQVO, ivrTaskTemplateScriptVO, serviceSubtask, ivrTaskTemplate)); |
| | | serviceSubTaskDetailReq.setServiceSubtaskDetailList(serviceSubtaskDetailList2); |
| | | ssdReq.setGuid(phoneCallReqYQVO.getGuid()); |
| | | ssdReq.setOrgid(phoneCallReqYQVO.getOrgid()); |
| | | saveQuestionAnswerPhone(ssdReq); |
| | | return phoneCallBackYQVO; |
| | | //明细在本题匹配成功时已落库(见上方 saveQuestionAnswerPhone),这里不再重复保存 |
| | | back = phoneCallBackYQVO; |
| | | return back; |
| | | } |
| | | |
| | | for (IvrTaskTemplateScript script : ivrTaskTemplateScripts) { |
| | |
| | | redisCache.deleteObject(phoneCallReqYQVO.getTaskid().trim() + "&&" + "mate" + "&&" + phoneCallReqYQVO.getUuid()); |
| | | } |
| | | } |
| | | //下一题脚本没找到时,兜底重问当前题,避免调用方误判为未处理而退回正则识别 |
| | | if (StringUtils.isEmpty(phoneCallBackYQVO.getValue())) { |
| | | log.error("下一题脚本未找到,重问当前题,nextQuestion:{}", nextQuestion); |
| | | phoneCallBackYQVO.setType("text"); |
| | | phoneCallBackYQVO.setSilent_interval(ivrTaskTemplate.getSilencetime().intValue()); |
| | | phoneCallBackYQVO.setValue(phoneCallBackYQVO.getCommonValue() + getObject(serviceSubtask, questionText)); |
| | | } |
| | | back = phoneCallBackYQVO; |
| | | return back; |
| | | } else if (ivrTaskTemplateScriptVO.getNextScriptno() == null || ivrTaskTemplateScriptVO.getNextScriptno() == 0) { |
| | | ServiceTask serviceTask1 = serviceTaskService.selectServiceTaskByTaskid(serviceSubtask.getTaskid()); |
| | | phoneCallBackYQVO.setType("text"); |
| | |
| | | //在redis中保存一下结束语,在调用挂电话的方法时删除 |
| | | ServiceTask serviceTask = serviceTaskService.selectServiceTaskByTaskid(serviceSubtask.getTaskid()); |
| | | redisCache.setCacheObject(phoneCallReqYQVO.getUuid() + "- jsy", serviceTask.getJsy(), 120, TimeUnit.MINUTES); |
| | | // return phoneCallBackYQVO; |
| | | //结束语已拼进返回值,挂机由平台按返回值执行 |
| | | back = phoneCallBackYQVO; |
| | | return back; |
| | | |
| | | } |
| | | } else { |
| | | log.info("没有匹配上----------------------------"); |
| | | //flag=0,说明没 匹配上,也要把患者说的话记录下来 |
| | | if (matched == ivrTaskTemplateScriptVO.getIvrTaskScriptTargetoptionList().size() - 1 && flag == 0) { |
| | | ServiceSubTaskDetailReq serviceSubTaskDetailReq = new ServiceSubTaskDetailReq(); |
| | | List<ServiceSubtaskDetail> serviceSubtaskDetailList = new ArrayList<>(); |
| | | ServiceSubtaskDetail serviceSubtaskDetail = getServiceSubtaskDetail(phoneCallReqYQVO, ivrTaskTemplateScriptVO, serviceSubtask, ivrTaskTemplate); |
| | | //修改一下语音路径(改成前端可以访问的,存到数据库中) |
| | | if (StringUtils.isNotEmpty(serviceSubtaskDetail.getQuestionvoice())) { |
| | | String questionvoice = serviceSubtaskDetail.getQuestionvoice(); |
| | | String[] split = questionvoice.split("\\\\"); |
| | | if (split.length > 0) { |
| | | String lastPart = split[split.length - 1]; |
| | | serviceSubtaskDetail.setQuestionvoice(voicePathPrefix + lastPart); |
| | | //AI没有匹配上:由AI分支自己按"没问清就重问"的次数处理,不再退回正则识别 |
| | | log.info("ai没有匹配上----------------------------"); |
| | | //没匹配上也要把患者说的话记录下来(matchedtext置空,与正则路径口径一致) |
| | | ServiceSubTaskDetailReq serviceSubTaskDetailReq = new ServiceSubTaskDetailReq(); |
| | | List<ServiceSubtaskDetail> serviceSubtaskDetailList = new ArrayList<>(); |
| | | ServiceSubtaskDetail serviceSubtaskDetail = getServiceSubtaskDetail(phoneCallReqYQVO, ivrTaskTemplateScriptVO, serviceSubtask, ivrTaskTemplate); |
| | | //修改一下语音路径(改成前端可以访问的,存到数据库中) |
| | | if (StringUtils.isNotEmpty(serviceSubtaskDetail.getQuestionvoice())) { |
| | | String questionvoice = serviceSubtaskDetail.getQuestionvoice(); |
| | | String[] split = questionvoice.split("\\\\"); |
| | | if (split.length > 0) { |
| | | String lastPart = split[split.length - 1]; |
| | | serviceSubtaskDetail.setQuestionvoice(voicePathPrefix + lastPart); |
| | | } |
| | | } |
| | | serviceSubtaskDetailList.add(serviceSubtaskDetail); |
| | | //如果没有 匹配上,这个必须为null |
| | | serviceSubtaskDetailList.get(0).setMatchedtext(""); |
| | | serviceSubTaskDetailReq.setServiceSubtaskDetailList(serviceSubtaskDetailList); |
| | | serviceSubTaskDetailReq.setGuid(phoneCallReqYQVO.getGuid()); |
| | | serviceSubTaskDetailReq.setOrgid(phoneCallReqYQVO.getOrgid()); |
| | | saveQuestionAnswerPhone(serviceSubTaskDetailReq); |
| | | |
| | | String mateKey = phoneCallReqYQVO.getTaskid().trim() + "&&" + "mate" + "&&" + phoneCallReqYQVO.getUuid(); |
| | | Integer count = null; |
| | | Object countObj = redisCache.getCacheObject(mateKey); |
| | | if (ObjectUtils.isNotEmpty(countObj)) count = (Integer) countObj; |
| | | if (count != null && count >= ivrTaskTemplate.getMateNum()) { |
| | | //没匹配次数已经用满,不能再重问了 |
| | | boolean noNextQuestion = ("0".equals(ivrTaskTemplateScriptVO.getBranchFlag()) || "1".equals(ivrTaskTemplateScriptVO.getBranchFlag())) |
| | | && (ivrTaskTemplateScriptVO.getNextScriptno() == null || ivrTaskTemplateScriptVO.getNextScriptno() == 0); |
| | | if (noNextQuestion) { |
| | | //已经是最后一题,直接挂机 |
| | | ServiceTask serviceTask1 = serviceTaskService.selectServiceTaskByTaskid(serviceSubtask.getTaskid()); |
| | | phoneCallBackYQVO.setType("text"); |
| | | phoneCallBackYQVO.setSilent_interval(ivrTaskTemplate.getSilencetime().intValue()); |
| | | phoneCallBackYQVO.setValue(phoneCallBackYQVO.getCommonValue() + serviceTask1.getJsy()); |
| | | //更新一下分数 |
| | | Double score = null; |
| | | Object scoreObj = redisCache.getCacheObject(phoneCallReqYQVO.getUuid() + "SCORE"); |
| | | if (ObjectUtils.isNotEmpty(scoreObj)) score = (Double) scoreObj; |
| | | serviceSubtask.setScore(String.valueOf(score)); |
| | | serviceSubtask.setFinishtime(new Date()); |
| | | serviceSubtask.setSendstate(6L); |
| | | serviceSubtask.setRemark("电话拨打已完成"); |
| | | serviceSubtaskMapper.updateServiceSubtask(serviceSubtask); |
| | | //记录状态 |
| | | setFailPreachForm(serviceSubtask, "3", "电话拨打已完成", "9"); |
| | | //在redis中保存一下结束语,在调用挂电话的方法时删除 |
| | | ServiceTask serviceTask = serviceTaskService.selectServiceTaskByTaskid(serviceSubtask.getTaskid()); |
| | | redisCache.setCacheObject(phoneCallReqYQVO.getUuid() + "- jsy", serviceTask.getJsy(), 120, TimeUnit.MINUTES); |
| | | //去redis中,把该子任务ID删除 |
| | | Long id = serviceSubtask.getId(); |
| | | Map<String, String> map = delRedisValue(null, id.toString()); |
| | | if (ObjectUtils.isNotEmpty(map)) { |
| | | redisCache.setCacheObject(map.get("cacheName"), map.get("val")); |
| | | redisCache.deleteObject(serviceSubtask.getId() + "-" + serviceSubtask.getPhone()); |
| | | redisCache.deleteObject(phoneCallReqYQVO.getUuid() + "SCORE"); |
| | | } |
| | | } else { |
| | | //还有下一题,按nextScriptno继续提问 |
| | | for (IvrTaskTemplateScript script : ivrTaskTemplateScripts) { |
| | | if (script.getSort() == ivrTaskTemplateScriptVO.getNextScriptno().intValue()) { |
| | | phoneCallBackYQVO.setType("text"); |
| | | phoneCallBackYQVO.setSilent_interval(ivrTaskTemplate.getSilencetime().intValue()); |
| | | String scriptContent = StringUtils.isNotEmpty(script.getIvrtext()) ? script.getIvrtext() : script.getScriptContent(); |
| | | phoneCallBackYQVO.setValue(phoneCallBackYQVO.getCommonValue() + getObject(serviceSubtask, scriptContent)); |
| | | //将该患者的Redis中的题目ID,进行修改 |
| | | redisCache.setCacheObject(phoneCallReqYQVO.getTaskid().trim() + "-" + phoneCallReqYQVO.getPhone().trim(), script.getId().toString(), 120, TimeUnit.MINUTES); |
| | | //更新一下分数 |
| | | Double score = null; |
| | | Object scoreObj = redisCache.getCacheObject(phoneCallReqYQVO.getUuid() + "SCORE"); |
| | | if (ObjectUtils.isNotEmpty(scoreObj)) score = (Double) scoreObj; |
| | | score = BigDecimal.valueOf(score).add(new BigDecimal(script.getScore())).doubleValue(); |
| | | redisCache.setCacheObject(phoneCallReqYQVO.getUuid() + "SCORE", score, 120, TimeUnit.MINUTES); |
| | | //换题成功,没匹配次数清零 |
| | | redisCache.deleteObject(mateKey); |
| | | } |
| | | } |
| | | } |
| | | serviceSubtaskDetailList.add(serviceSubtaskDetail); |
| | | //如果没有 匹配上,这个必须为null |
| | | serviceSubtaskDetailList.get(0).setMatchedtext(""); |
| | | serviceSubTaskDetailReq.setServiceSubtaskDetailList(serviceSubtaskDetailList); |
| | | serviceSubTaskDetailReq.setGuid(phoneCallReqYQVO.getGuid()); |
| | | serviceSubTaskDetailReq.setOrgid(phoneCallReqYQVO.getOrgid()); |
| | | saveQuestionAnswerPhone(serviceSubTaskDetailReq); |
| | | } else { |
| | | //还没问满次数,用noMatchText引导患者重答 |
| | | redisCache.setCacheObject(mateKey, count == null ? 1 : count + 1, 120, TimeUnit.MINUTES); |
| | | phoneCallBackYQVO.setType("text"); |
| | | phoneCallBackYQVO.setSilent_interval(ivrTaskTemplate.getSilencetime().intValue()); |
| | | phoneCallBackYQVO.setValue(ivrTaskTemplateScriptVO.getNoMatchText() + getObject(serviceSubtask, StringUtils.isNotEmpty(ivrTaskTemplateScriptVO.getIvrtext()) ? ivrTaskTemplateScriptVO.getIvrtext() : ivrTaskTemplateScriptVO.getScriptContent())); |
| | | } |
| | | back = phoneCallBackYQVO; |
| | | return back; |
| | | } |
| | | return back; |
| | | } |
| | |
| | | */ |
| | | 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; |
| | | } |
| | |
| | | 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)) { |
| | |
| | | } |
| | | 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()); |
| | | } |
| | | } |