陈昶聿
3 天以前 d7e3192804fca756d881af360ffd5eea157ef9ad
smartor/src/main/java/com/smartor/service/impl/ServiceSubtaskServiceImpl.java
@@ -18,7 +18,9 @@
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;
@@ -58,6 +60,8 @@
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;
@@ -137,6 +141,9 @@
    private IPatArchiveService patArchiveService;
    @Autowired
    private ISysDeptService sysDeptService;
    @Autowired
    private ScheduleSubtaskBuilder scheduleSubtaskBuilder;
    @Autowired
@@ -181,6 +188,30 @@
    @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;
@@ -216,8 +247,14 @@
    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;
    /**
     * 查询单一任务(随访)
@@ -782,6 +819,7 @@
     * @return 结果
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public String addSubTaskAgain(ServiceSubtask serviceSubtask) {
        ServiceSubtask serviceSubtask1 = selectServiceSubtaskById(serviceSubtask.getId());
        PatMedInhosp patMedInhosp = new PatMedInhosp();
@@ -791,7 +829,7 @@
        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) {
@@ -805,13 +843,17 @@
            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);
@@ -832,6 +874,155 @@
            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);
        }
    }
    /**
@@ -1831,6 +2022,130 @@
    }
    /**
     * 结束语缓存 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
@@ -1840,6 +2155,15 @@
        //先把该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");
@@ -1848,7 +2172,7 @@
        }
        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")) {
@@ -1859,7 +2183,9 @@
                //如果发送状态为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();
@@ -1867,11 +2193,8 @@
                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);
@@ -1884,36 +2207,53 @@
                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);
@@ -1933,6 +2273,10 @@
        //通过子任务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();
@@ -1941,8 +2285,24 @@
        //获取问题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) {
@@ -2215,9 +2575,10 @@
                            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");
@@ -2343,7 +2704,8 @@
                    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");
@@ -2773,7 +3135,9 @@
    @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()));
@@ -2786,9 +3150,13 @@
//        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);
@@ -4422,7 +4790,7 @@
                serviceSubtaskCountReq.setVisitCount(1);
                serviceSubtaskCountReq.setCurrentPreachform("4");
                break;
            case "wechatInfo":
            case "weChatInfo":
                serviceSubtaskCountReq.setVisitCount(1);
                serviceSubtaskCountReq.setCurrentPreachform("5");
                break;
@@ -4441,7 +4809,7 @@
                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");
@@ -4478,7 +4846,7 @@
                serviceSubtaskCountReq.setVisitCount(2);
                serviceSubtaskCountReq.setCurrentPreachform("4");
                break;
            case "wechatAgainInfo":
            case "weChatAgainInfo":
                serviceSubtaskCountReq.setVisitCount(2);
                serviceSubtaskCountReq.setCurrentPreachform("5");
                break;
@@ -4497,7 +4865,7 @@
                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");
@@ -5168,12 +5536,6 @@
        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);
@@ -5218,6 +5580,25 @@
        return resultMap;
    }
    @Override
    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<>();