liusheng
昨天 18f876d637cf1e9b0b264a4586e4a676a21f8166
ruoyi-phone/src/main/java/com/ruoyi/phone/service/impl/PhoneServiceImpl.java
@@ -18,9 +18,8 @@
import com.ruoyi.phone.domain.QuestionMessage;
import com.ruoyi.phone.service.IPhoneService;
import com.ruoyi.task.api.TaskSubtaskApi;
import com.ruoyi.task.api.spi.TaskFtpPort;
import com.ruoyi.task.api.spi.TaskPreachformPort;
import com.ruoyi.task.api.spi.TaskSysConfigPort;
import com.ruoyi.common.utils.FtpService;
import com.ruoyi.task.mapper.ServiceSubtaskPreachformMapper;
import com.ruoyi.task.domain.IvrTaskVisitResult;
import com.ruoyi.task.domain.ServiceSubTaskDetailReq;
import com.ruoyi.task.domain.ServiceSubtask;
@@ -60,6 +59,7 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import com.ruoyi.system.service.ISysConfigService;
/**
 * 电话业务Service实现(原 {@code ruoyi-task} 里 {@code ServiceSubtaskServiceImpl} 的电话部分)。
@@ -110,19 +110,21 @@
     * 该表归 ruoyi-inhospvisit,由 smartor 侧适配实现本端口。
     */
    @Autowired
    private TaskPreachformPort taskPreachformPort;
    private ServiceSubtaskPreachformMapper taskPreachformPort;
    /**
     * 录音文件下载(雨绮通话记录回调里把当天录音从 FTP 拉回本地)
     * <p>2026-11:{@code FtpService} 已从 smartor 下沉到 {@code ruoyi-common},
     * 原来的过渡端口 {@code TaskFtpPort} 随之删除,这里直接注入通用工具。
     */
    @Autowired
    private TaskFtpPort taskFtpPort;
    private FtpService ftpService;
    /**
     * 参数配置读取({@code sys.voice.match.ai.url}:AI 选项匹配服务地址,按机构配置)
     */
    @Autowired
    private TaskSysConfigPort taskSysConfigPort;
    private ISysConfigService sysConfigService;
    /**
     * AI 选项匹配契约(ruoyi-common,实现在 ruoyi-task,即原 {@code matchOptionIndex})
@@ -327,6 +329,8 @@
                            ServiceSubtask ss = new ServiceSubtask();
                            ss.setId(serviceSubtask.getId());
                            ss.setSendstate(6L);
                            //电话已答完(完成):状态与完成时间一起写,避免"已完成却无完成时间"
                            ss.setFinishtime(new Date());
                            taskSubtaskApi.updateSubtask(ss);
                            String date = simpleDateFormat1.format(new Date());
                            log.error("没有下一题了,就结束了: {},uuid为:{}", date, phoneCallBackVO.getUuid());
@@ -372,6 +376,8 @@
                        ServiceSubtask ss = new ServiceSubtask();
                        ss.setId(serviceSubtask.getId());
                        ss.setSendstate(6L);
                        //电话已答完(完成):状态与完成时间一起写,避免"已完成却无完成时间"
                        ss.setFinishtime(new Date());
                        taskSubtaskApi.updateSubtask(ss);
                        String date = simpleDateFormat1.format(new Date());
@@ -605,12 +611,21 @@
                //去redis中,把该子任务ID删除
                Long id = serviceSubtask.getId();
                //先更新一下分数
                //⚠️ 静默挂机 = 电话没接通 / 患者一直没应答,属于"未通",不是"完成":
                //   ① 不写 finishtime —— 这条单子接下来要交给补偿任务(setFailPreachForm 置 5 或 3,
                //      再由 compensateTask 捞起来继续发下一种方式 / 判超时);一旦写了完成时间,
                //      后面被置成"超时(7)"时就会留下"超时却有完成时间"的脏数据,
                //      及时率、完成率(ServiceSubtaskMapper#selectTimelyRateBatch 认 finishtime 非空)也会被虚高;
                //   ② 不整行回写 serviceSubtask —— 那会把上面刚写的 sendstate=5(电话静默)覆盖回查库时的旧值,
                //      所以这里只回写 id + 分数 + 更新时间,状态交给下面的 setFailPreachForm 决定
                //      (最后一种方式 → 5 发送失败;还有下一种 → 3 已发送,两者都会被补偿任务捞起来)。
                double score = redisCache.getCacheObject(phoneCallReqYQVO.getUuid() + "SCORE");
                serviceSubtask.setScore(String.valueOf(score));
                serviceSubtask.setFinishtime(new Date());
                serviceSubtask.setUpdateTime(new Date());
                log.error("先更新一下分数,电话的serviceSubtask的值为:{}", serviceSubtask);
                taskSubtaskApi.updateSubtask(serviceSubtask);
                ServiceSubtask scoreUpdate = new ServiceSubtask();
                scoreUpdate.setId(id);
                scoreUpdate.setScore(String.valueOf(score));
                scoreUpdate.setUpdateTime(new Date());
                log.error("先更新一下分数,电话的scoreUpdate的值为:{}", scoreUpdate);
                taskSubtaskApi.updateSubtask(scoreUpdate);
                //记录状态
                setFailPreachForm(serviceSubtask, "3", "静默次数达到" + num + "次", "4");
@@ -667,6 +682,8 @@
                        ss.setId(serviceSubtask.getId());
                        ss.setSendstate(6L);
                        ss.setRemark("电话拨打已完成");
                        //电话已答完(完成):状态与完成时间一起写,避免"已完成却无完成时间"
                        ss.setFinishtime(new Date());
                        taskSubtaskApi.updateSubtask(ss);
                        //记录状态
                        setFailPreachForm(serviceSubtask, "3", "电话拨打已完成", "9");
@@ -700,7 +717,7 @@
                //用来标记,是否有匹配上的
                Integer flag = 0;
                //1-AI识别
                String url = taskSysConfigPort.selectConfigValue("sys.voice.match.ai.url", phoneCallReqYQVO.getOrgid());
                String url = sysConfigService.selectConfigByKey("sys.voice.match.ai.url", phoneCallReqYQVO.getOrgid());
                if (ObjectUtils.isNotEmpty(url)) {
                    PhoneCallBackYQVO aiMatchResult = AiMatch(url, phoneCallReqYQVO, phoneCallBackYQVO, ivrTaskTemplateScriptVO, serviceSubtask, ivrTaskTemplate, ivrTaskTemplateScripts, scriptId, flag, phoneCallReqYQVO.getUuid());
@@ -852,6 +869,8 @@
                                    ss.setId(serviceSubtask.getId());
                                    ss.setSendstate(6L);
                                    ss.setRemark("电话拨打已完成");
                                    //电话已答完(完成):状态与完成时间一起写,避免"已完成却无完成时间"
                                    ss.setFinishtime(new Date());
                                    taskSubtaskApi.updateSubtask(ss);
                                    //记录状态
                                    setFailPreachForm(serviceSubtask, "3", "电话拨打已完成", "9");
@@ -1415,7 +1434,7 @@
            taskSubtaskApi.updateSubtask(serviceSubtask);
            setFailPreachForm(serviceSubtask, StringUtils.isNotEmpty(serviceSubtask.getCurrentPreachform()) ? serviceSubtask.getCurrentPreachform() : "3", "电话拨打已完成", "9");
            //下载文件
            taskFtpPort.downloadFolder("/" + LocalDate.now().toString(), profile + "/upload/vadio/voice/" + LocalDate.now().toString());
            ftpService.downloadFolder("/" + LocalDate.now().toString(), profile + "/upload/vadio/voice/" + LocalDate.now().toString());
            redisCache.deleteObject(phoneCallRecordVO.getTaskid() + "recordAccept-hungup");
        } else {
            //患者拒绝随访
@@ -1564,12 +1583,13 @@
                serviceSubtask.setCurrentPreachform(preachform);
                serviceSubtask.setSendstate(5L);
                serviceSubtask.setRemark(remark + ";所有执行方式均未获取结果,任务失败");
                taskSubtaskApi.updateSubtask(serviceSubtask);
                //只回写本次要改的列(不再整行回写,避免把库里的旧 finishtime 等一起带回来)
                updateSubtaskState(serviceSubtask, 5L, preachform, serviceSubtask.getRemark(), null);
                return true;
            } else {
                //加入目前补偿里的 remark
                serviceSubtask.setRemark(StringUtils.isNotEmpty(serviceSubtask.getRemark()) ? serviceSubtask.getRemark() + "," + remark : remark);
                taskSubtaskApi.updateSubtask(serviceSubtask);
                updateSubtaskState(serviceSubtask, null, null, serviceSubtask.getRemark(), null);
            }
        }
@@ -1579,8 +1599,35 @@
        //如果等于9,则说明电话正常结束了
        if (failSendstate.equals("9")) serviceSubtask.setSendstate(6L);
        serviceSubtask.setVisitTime(getNextVisitTime(serviceSubtask.getId(), serviceSubtask.getTaskid(), serviceSubtask.getVisitTime()));
        taskSubtaskApi.updateSubtask(serviceSubtask);
        updateSubtaskState(serviceSubtask, serviceSubtask.getSendstate(), preachform, null, serviceSubtask.getVisitTime());
        return true;
    }
    /**
     * 只回写本次真正要改的列(替代"整行回写 serviceSubtask")。
     * <p>
     * 为什么必须收窄:mapper 的 updateServiceSubtask 是&lt;if test="xx != null"&gt;动态 SET,
     * 整行回写会把库里已有的 finishtime / visit_time / score 等旧值原样再写一遍 —— 既可能覆盖
     * 并发写入,也是"状态 3/5/7 却带着完成时间"这类脏数据的搬运工。
     * <p>
     * 完成时间口径:sendstate=6(完成)时补完成时间;3 已发送 / 5 发送失败 / 7 超时等非完成状态
     * 一律不写,Mapper 侧的"完成时间不变量"会顺手把旧值清掉。
     */
    private void updateSubtaskState(ServiceSubtask source, Long sendstate, String currentPreachform,
                                    String remark, Date visitTime) {
        if (source == null || source.getId() == null) {
            return;
        }
        ServiceSubtask update = new ServiceSubtask();
        update.setId(source.getId());
        update.setSendstate(sendstate);
        update.setCurrentPreachform(currentPreachform);
        update.setRemark(remark);
        update.setVisitTime(visitTime);
        if (sendstate != null && sendstate == 6L) {
            update.setFinishtime(source.getFinishtime() != null ? source.getFinishtime() : new Date());
        }
        taskSubtaskApi.updateSubtask(update);
    }
    private Date getNextVisitTime(Long subid, Long taskid, Date visitTime) {
@@ -1652,7 +1699,7 @@
    public PhoneCallBackSHVO processResust(PhoneCallResultYQVO phoneCallResultYQVO) {
        PhoneCallBackSHVO phoneCallBackSHVO = new PhoneCallBackSHVO();
        //通过scriptid,获取所有的结果
        String url = taskSysConfigPort.selectConfigValue("sys.voice.match.ai.url", phoneCallResultYQVO.getOrgid());
        String url = sysConfigService.selectConfigByKey("sys.voice.match.ai.url", phoneCallResultYQVO.getOrgid());
        ServiceSubtask serviceSubtask = taskSubtaskApi.getSubtask(phoneCallResultYQVO.getSubId());
        IvrTaskTemplate ivrTaskTemplate = ivrTaskTemplateService.selectIvrTaskTemplateByID(serviceSubtask.getTemplateid());
        IvrTaskTemplateScriptVO ivrTaskTemplateScriptVO = iIvrTaskTemplateScriptService.getTaskTempScriptInfoByid(Long.valueOf(phoneCallResultYQVO.getScriptId()));
@@ -1891,6 +1938,8 @@
                    ss.setId(serviceSubtask.getId());
                    ss.setSendstate(6L);
                    ss.setRemark("电话拨打已完成");
                    //电话已答完(完成):状态与完成时间一起写,避免"已完成却无完成时间"
                    ss.setFinishtime(new Date());
                    taskSubtaskApi.updateSubtask(ss);
                    //记录状态
                    setFailPreachForm(serviceSubtask, "3", "电话拨打已完成", "9");