liusheng
13 小时以前 ab2884ade73fed97d1e7d7daf4b9b5b6dee83fdd
smartor/src/main/java/com/smartor/service/impl/ServiceSubtaskAnswerServiceImpl.java
@@ -11,9 +11,7 @@
import com.ruoyi.common.utils.RSAPublicKeyExample;
import com.ruoyi.common.utils.StringUtils;
import com.smartor.domain.*;
import com.smartor.domain.DTO.ServiceSubtaskDetailDTO;
import com.smartor.domain.VO.ServiceSubtaskDetailTraceVO;
import com.smartor.domain.entity.ServiceSubtaskEntity;
import com.ruoyi.task.domain.VO.ServiceSubtaskDetailTraceVO;
import com.smartor.mapper.*;
import com.smartor.service.*;
import lombok.extern.slf4j.Slf4j;
@@ -29,6 +27,44 @@
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import com.ruoyi.template.domain.HeLibrary;
import com.ruoyi.template.domain.IvrLibaScriptAssort;
import com.ruoyi.inhospvisit.domain.PatMedInhosp;
import com.ruoyi.outpatientvisit.domain.PatMedOuthosp;
import com.ruoyi.outpatientvisit.domain.PatMedOuthospQueryReq;
import com.ruoyi.inhospvisit.api.InhospVisitFacade;
import com.ruoyi.outpatientvisit.api.OuthospDataFacade;
import com.ruoyi.template.domain.IvrTaskTemplate;
import com.ruoyi.template.domain.IvrTaskTemplateScriptVO;
import com.ruoyi.template.domain.IvrTaskTemplateTargetoption;
import com.ruoyi.template.service.IIvrTaskTemplateService;
import com.ruoyi.template.domain.IvrLibaTemplateScript;
import com.ruoyi.template.mapper.IvrLibaScriptAssortMapper;
import com.ruoyi.template.service.IIvrLibaTemplateScriptService;
import com.ruoyi.template.domain.SvyLibScriptCategory;
import com.ruoyi.template.domain.SvyLibTemplateScript;
import com.ruoyi.template.domain.SvyTaskTemplate;
import com.ruoyi.template.domain.SvyTaskTemplateScriptVO;
import com.ruoyi.template.domain.SvyTaskTemplateTargetoption;
import com.ruoyi.template.mapper.SvyLibScriptCategoryMapper;
import com.ruoyi.template.service.ISvyTaskTemplateService;
import com.ruoyi.common.core.domain.entity.PatArchive;
import com.ruoyi.task.domain.DTO.ServiceSubtaskDetailDTO;
import com.ruoyi.task.domain.entity.ServiceSubtaskEntity;
import com.ruoyi.task.domain.ServiceSubtask;
import com.ruoyi.task.domain.ServiceSubtaskDetail;
import com.ruoyi.task.domain.ServiceSubtaskDetailVO;
import com.ruoyi.task.domain.ServiceTask;
import com.ruoyi.task.domain.TaskSendTimeVO;
import com.ruoyi.task.mapper.ServiceSubtaskDetailMapper;
import com.ruoyi.task.mapper.ServiceSubtaskMapper;
import com.ruoyi.task.domain.ServiceSubtaskDetailTrace;
import com.ruoyi.task.domain.VO.ServiceSubtaskDetailTraceVO;
import com.ruoyi.task.service.IServiceSubtaskDetailTraceService;
import com.ruoyi.task.domain.ServiceSubtaskPreachform;
import com.ruoyi.task.service.IServiceTaskService;
import com.ruoyi.task.mapper.ServiceSubtaskPreachformMapper;
import com.ruoyi.patarchive.mapper.PatArchiveMapper;
/**
 * 任务问卷问题选项Service业务层处理
 *
@@ -40,6 +76,18 @@
public class ServiceSubtaskAnswerServiceImpl implements IServiceSubtaskAnswerService {
    @Autowired
    private ServiceSubtaskAnswerMapper serviceSubtaskAnswerMapper;
    /**
     * 跨模块访问一律经模块门面(禁止直连已搬走的 Mapper)。
     */
    @Autowired
    private OuthospDataFacade outhospDataFacade;
    /**
     * 跨模块访问一律经模块门面(禁止直连已搬走的 Mapper)。
     */
    @Autowired
    private InhospVisitFacade inhospVisitFacade;
    @Autowired
    private ServiceSubtaskMapper serviceSubtaskMapper;
@@ -66,11 +114,7 @@
    private PatArchiveMapper patArchiveMapper;
    @Autowired
    private PatMedInhospMapper patMedInhospMapper;
    @Autowired
    private PatMedOuthospMapper patMedOuthospMapper;
    @Autowired
    private IServiceSubtaskDetailTraceService traceService;
@@ -81,8 +125,6 @@
    @Autowired
    private ISvyLibTemplateScriptService svyLibTemplateScriptService;
    @Autowired
    private ISvyTaskTemplateTargetoptionService svyTaskTemplateTargetoptionService;
    @Autowired
    private IIvrTaskTemplateService ivrTaskTemplateService;
@@ -236,6 +278,8 @@
    public Integer saveQuestionAnswer(ServiceSubTaskAnswerReq serviceSubTaskAnswerReq, Long flag) {
        String threadName = Thread.currentThread().getName();
        log.info("[{}] 开始异步处理保存问卷答案", threadName);
        // 幂等占位键:失败时要释放,允许患者重试
        String idemKey = null;
        try {
            //参数解密
            Long tid = null;
@@ -259,6 +303,17 @@
            //根据 PID、TID、SUBID 构建需要处理的 cache key
            String scriptCacheKeyPrefix = pid + "-" + tid + "-" + subid;
            Set<String> targetKeys = new HashSet<>();
            // ---------------- 幂等:同一份问卷 60 秒内只处理一次 ----------------
            // 本方法是 @Async 异步执行,患者端重复点击/网络重试会让它并发跑多次,
            // 每次都会 upsert 一遍明细;并发时彼此看不到对方还没提交的行 -> 插入重复副本
            // (nhfy 2026-09-08 就是这样堆出 2~6 份,并且把 del_flag 一起写坏)。
            idemKey = "subtaskAnswer:submit:" + scriptCacheKeyPrefix + ":"
                    + (serviceSubTaskAnswerReq == null || serviceSubTaskAnswerReq.getType() == null ? "all" : serviceSubTaskAnswerReq.getType());
            if (!redisCache.setCacheObjectIfAbsent(idemKey, "1", 60, TimeUnit.SECONDS)) {
                log.warn("[{}] 检测到重复提交,本次忽略:{}", threadName, idemKey);
                return 0;
            }
            if (serviceSubTaskAnswerReq != null && serviceSubTaskAnswerReq.getType() != null) {
                if (serviceSubTaskAnswerReq.getType() == 2) {
@@ -451,6 +506,14 @@
            return 1;
        } catch (Exception e) {
            log.error("[{}] 异步处理保存问卷答案发生异常 : {},请求的对象为:{}", Thread.currentThread().getName(), e.getMessage(), e, serviceSubTaskAnswerReq);
            // 处理失败时释放幂等键,允许患者重新提交
            if (idemKey != null) {
                try {
                    redisCache.deleteObject(idemKey);
                } catch (Exception ignore) {
                    log.warn("[{}] 释放幂等键失败:{}", Thread.currentThread().getName(), idemKey);
                }
            }
            throw new RuntimeException("保存问卷答案失败", e);
        }
    }
@@ -613,13 +676,13 @@
            //住院
            PatMedInhosp patMedInhosp = new PatMedInhosp();
            patMedInhosp.setSerialnum(serviceSubTaskMYDAnswerReq.getSerialnum());
            patMedInhospList = patMedInhospMapper.selectPatMedInhospList(patMedInhosp);
            patMedInhospList = inhospVisitFacade.selectPatMedInhospList(patMedInhosp);
            if (CollectionUtils.isNotEmpty(patMedInhospList)) patid = patMedInhospList.get(0).getPatid();
        } else if (serviceSubTaskMYDAnswerReq.getMzzy().equals("2")) {
            //门诊
            PatMedOuthospQueryReq patMedOuthosp = new PatMedOuthospQueryReq();
            patMedOuthosp.setSerialnum(serviceSubTaskMYDAnswerReq.getSerialnum());
            patMedOuthosps = patMedOuthospMapper.callSpQueryOuthosp(patMedOuthosp);
            patMedOuthosps = outhospDataFacade.callSpQueryOuthosp(patMedOuthosp);
            if (CollectionUtils.isNotEmpty(patMedOuthosps)) patid = patMedOuthosps.get(0).getPatid();
        } else if (serviceSubTaskMYDAnswerReq.getMzzy().equals("3")) {
            //指定一个部门,用来处理投诉建议
@@ -688,8 +751,8 @@
                serviceSubtask.setLibtemplateid(Long.valueOf(serviceTask.getLibtemplateid()));
                if (CollectionUtils.isNotEmpty(patMedInhospList)) {
                    serviceSubtask.setHospType("2");
                    serviceSubtask.setDeptname(patMedInhospList.get(0).getDeptname());
                    serviceSubtask.setDeptcode(patMedInhospList.get(0).getDeptcode());
                    serviceSubtask.setLeaveldeptname(patMedInhospList.get(0).getDeptname());
                    serviceSubtask.setLeaveldeptcode(patMedInhospList.get(0).getDeptcode());
                    serviceSubtask.setLeavehospitaldistrictcode(patMedInhospList.get(0).getLeavehospitaldistrictcode());
                    serviceSubtask.setLeavehospitaldistrictname(patMedInhospList.get(0).getLeavehospitaldistrictname());
                    serviceSubtask.setDrname(patMedInhospList.get(0).getDrname());
@@ -703,8 +766,8 @@
                    serviceSubtask.setHospType("1");
                    serviceSubtask.setAdmindate(patMedOuthosps.get(0).getAdmitdate());
                    serviceSubtask.setHospno(patMedOuthosps.get(0).getOuthospno());
                    serviceSubtask.setDeptname(patMedOuthosps.get(0).getDeptname());
                    serviceSubtask.setDeptcode(patMedOuthosps.get(0).getDeptcode());
                    serviceSubtask.setLeaveldeptname(patMedOuthosps.get(0).getDeptname());
                    serviceSubtask.setLeaveldeptcode(patMedOuthosps.get(0).getDeptcode());
                    serviceSubtask.setDrname(patMedOuthosps.get(0).getDrname());
                    serviceSubtask.setDrcode(patMedOuthosps.get(0).getDrcode());
                    serviceSubtask.setCampusid(patMedOuthosps.get(0).getCampusid());
@@ -726,7 +789,7 @@
                }
                if (StringUtils.isNotEmpty(dealDept)) {
                    //投诉建议的处理部门
                    serviceSubtask.setDeptcode(dealDept);
                    serviceSubtask.setLeaveldeptcode(dealDept);
                    serviceSubtask.setServiceType("15");
                }
                serviceSubtask.setSubmit(1L);
@@ -859,7 +922,8 @@
        Map<String, Object> map = new HashMap<>();
        if (serviceSubTaskQueryReq.getSubId() != null) {
            ServiceSubtask serviceSubtask = serviceSubtaskMapper.selectServiceSubtaskById(serviceSubTaskQueryReq.getSubId());
            if (serviceSubtask.getSendstate() == 4L) {
            // 任务可能已被删除/查不到,sendstate 也可能为空,这里必须判空,否则拆箱会 NPE
            if (serviceSubtask != null && Long.valueOf(4L).equals(serviceSubtask.getSendstate())) {
                log.info("患者子任务ID为:{},状态为:{}", serviceSubTaskQueryReq.getSubId(), serviceSubtask.getSendstate());
                throw new BaseException("该任务不执行");
            }
@@ -872,7 +936,7 @@
        //获取上次的问答结果
        ServiceSubtask serviceSubtask = serviceSubtaskMapper.selectServiceSubtaskById(serviceSubTaskQueryReq.getSubId());
        map.put("upScriptResult", null);
        map.put("templateName", serviceSubtask.getTemplatename());
        map.put("templateName", serviceSubtask == null ? null : serviceSubtask.getTemplatename());
        map.put("upTaskName", null);
        if (ObjectUtils.isNotEmpty(serviceSubtask) && serviceSubtask.getUpid() != null) {
            Map<String, Object> scriptInfoByCondition2 = serviceTaskService.getScriptInfoByCondition(serviceSubTaskQueryReq.getTaskid(), serviceSubTaskQueryReq.getPatid(), serviceSubTaskQueryReq.getIsFinish(), serviceSubTaskQueryReq.getPatfrom(), serviceSubTaskQueryReq.getSubId());
@@ -890,7 +954,7 @@
    private Map<String, Object> getMap(Map<String, Object> scriptInfoByCondition, Long subid, Long patid) {
        Map<String, Object> map = new HashMap<>();
        if (ObjectUtils.isNotEmpty(scriptInfoByCondition) && ObjectUtils.isNotEmpty(scriptInfoByCondition.get("script"))) {
            if (scriptInfoByCondition.get("type").equals("1")) {
            if ("1".equals(scriptInfoByCondition.get("type"))) {
                List<IvrTaskTemplateScriptVO> ivrTaskTemplateScriptVOS = (List<IvrTaskTemplateScriptVO>) scriptInfoByCondition.get("script");
                // 批量查询所有明细,避免循环中逐条查数据库(N+1问题优化)
                Map<Long, ServiceSubtaskDetail> detailMap = getScriptResultMap(subid, patid);
@@ -903,7 +967,7 @@
                }
                map.put("scriptResult", ivrTaskTemplateScriptVOS);
                map.put("taskName", scriptInfoByCondition.get("taskName"));
            } else if (scriptInfoByCondition.get("type").equals("2")) {
            } else if ("2".equals(scriptInfoByCondition.get("type"))) {
                List<SvyTaskTemplateScriptVO> svyTaskTemplateScriptVOList = (List) scriptInfoByCondition.get("script");
                // 批量查询所有明细,避免循环中逐条查数据库(N+1问题优化)
                Map<Long, ServiceSubtaskDetail> detailMap = getScriptResultMap(subid, patid);
@@ -919,7 +983,7 @@
                }
                map.put("scriptResult", svyTaskTemplateScriptVOList);
                map.put("taskName", scriptInfoByCondition.get("taskName"));
            } else if (scriptInfoByCondition.get("type").equals("3")) {
            } else if ("3".equals(scriptInfoByCondition.get("type"))) {
                //宣教不需要查看
                List<HeLibrary> heLibraryList = (List) scriptInfoByCondition.get("script");
                map.put("scriptResult", heLibraryList);
@@ -985,6 +1049,11 @@
    /**
     * 批量查询已存在的明细记录ID,按scriptid分组(避免循环中逐条查询)
     * <p>
     * ⚠️ 2026-10-21:<b>不再过滤 del_flag</b>,同一 scriptid 取 id 最大(最新)的那一行。
     * 原来只认 del_flag=0,明细被逻辑删除后就查不到 → 下一次保存必然 INSERT 新副本,
     * 同一患者同一题会堆出 2~6 条重复明细(nhfy 2026-09-08)。
     * 命中被删副本时由调用方再调 {@code reviveServiceSubtaskDetail} 置回 '0'。
     */
    private Map<Long, Long> getExistingDetailIdMap(Long subid) {
        if (subid == null) return Collections.emptyMap();
@@ -994,7 +1063,9 @@
        if (CollectionUtils.isEmpty(details)) return Collections.emptyMap();
        Map<Long, Long> map = new HashMap<>();
        for (ServiceSubtaskDetail d : details) {
            if ((d.getDelFlag() == null || "0".equals(d.getDelFlag())) && d.getScriptid() != null && !map.containsKey(d.getScriptid())) {
            if (d.getScriptid() == null || d.getId() == null) continue;
            Long existId = map.get(d.getScriptid());
            if (existId == null || d.getId() > existId) {
                map.put(d.getScriptid(), d.getId());
            }
        }
@@ -1091,6 +1162,8 @@
            serviceSubtaskDetail.setId(existDetailId);
            serviceSubtaskDetail.setUpdateTime(new Date());
            serviceSubtaskDetailMapper.updateServiceSubtaskDetail(serviceSubtaskDetail);
            //命中历史被逻辑删除的副本时置回 del_flag='0'(否则答案写进去了却查不到,且下次又会新增副本)
            serviceSubtaskDetailMapper.reviveServiceSubtaskDetail(existDetailId);
        } else {
            serviceSubtaskDetailMapper.insertServiceSubtaskDetail(serviceSubtaskDetail);
        }
@@ -1150,6 +1223,8 @@
            //非满意度意见异常自动填入空
            subtaskDetailTrace.setMatchedtext(stt != null ? stt.getOptioncontent() : svyTaskTemplateScriptVO.getIsMydException());
            subtaskDetailTrace.setAsrtext(stt != null ? stt.getOptioncontent() : svyTaskTemplateScriptVO.getIsMydException());
            //补充描述(选了"其他/有症状"时填的文字)一并带到 trace 表
            subtaskDetailTrace.setAnswerps(serviceSubtaskDetail.getAnswerps());
            subtaskDetailTrace.setIsMydException(svyTaskTemplateScriptVO.getIsMydException());
            subtaskDetailTrace.setRecordpath(null);
@@ -1390,6 +1465,8 @@
            serviceSubtaskDetail.setId(existServiceSubtaskDetail.getId());
            serviceSubtaskDetail.setUpdateTime(new Date());
            serviceSubtaskDetailMapper.updateServiceSubtaskDetail(serviceSubtaskDetail);
            //命中历史被逻辑删除的副本时置回 del_flag='0',避免重复副本
            serviceSubtaskDetailMapper.reviveServiceSubtaskDetail(existServiceSubtaskDetail.getId());
        } else {
            serviceSubtaskDetailMapper.insertServiceSubtaskDetail(serviceSubtaskDetail);
        }
@@ -1441,6 +1518,8 @@
            subtaskDetailTrace.setType(ivrLibaScriptAssort != null ? ivrLibaScriptAssort.getType() : null);
            subtaskDetailTrace.setScriptid(ivrTaskTemplateScriptVO.getId() == null ? ivrTaskTemplateScriptVO.getScriptID() : ivrTaskTemplateScriptVO.getId());
            subtaskDetailTrace.setAsrtext(ivrTaskTemplateScriptVO.getIvrtext());
            //补充描述一并带到 trace 表
            subtaskDetailTrace.setAnswerps(serviceSubtaskDetail.getAnswerps());
            subtaskDetailTrace.setRecordpath(null);
            subtaskDetailTrace.setPatid(selectServiceSubtaskList.get(0).getPatid());
            JSONObject patdescJson2 = new JSONObject();