package com.ruoyi.quartz.task; import com.alibaba.fastjson2.JSONObject; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.gson.Gson; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.domain.entity.SysDept; import com.ruoyi.common.core.domain.entity.SysUser; import com.ruoyi.common.core.redis.RedisCache; import com.ruoyi.common.core.service.IDingTalkService; import com.ruoyi.common.enums.ServiceFromEnum; import com.ruoyi.common.utils.*; import com.ruoyi.common.utils.http.HttpUtils; import com.ruoyi.message.SendResult; import com.ruoyi.message.sms.SmsSendRequest; import com.ruoyi.message.sms.SmsSenderFactory; import com.ruoyi.message.sms.provider.xh.XinHuaSmsSender; import com.ruoyi.message.wechat.WechatSendRequest; import com.ruoyi.message.wechat.WechatSenderFactory; import com.ruoyi.quartz.constant.MzGatherConstant; import com.ruoyi.quartz.enums.MzGatherParamEnum; import com.ruoyi.quartz.enums.MzGatherSlotEnum; import com.ruoyi.quartz.enums.MzGatherStatusEnum; import com.ruoyi.quartz.service.ICollectHISService; import com.ruoyi.system.domain.SysConfig; import com.ruoyi.system.mapper.SysConfigMapper; import com.ruoyi.system.service.ISysConfigService; import com.ruoyi.system.service.ISysDeptService; import com.ruoyi.system.service.ISysUserService; import com.ruoyi.message.sms.provider.sltd.ws.MessageSend; import com.smartor.common.QwenLLMUtil; import com.smartor.domain.*; import com.ruoyi.task.domain.VO.ServiceSubtaskDetailTraceVO; import com.smartor.mapper.*; import com.smartor.service.*; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.ObjectUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import com.ruoyi.template.domain.HeLibrary; import com.ruoyi.inhospvisit.domain.PatMedInhosp; import com.ruoyi.template.api.TemplateFacade; import com.ruoyi.common.core.domain.entity.PatArchive; import com.ruoyi.task.domain.entity.ServiceSubtaskEntity; import com.ruoyi.task.domain.ServiceSubtask; import com.ruoyi.task.domain.ServiceSubtaskRecord; import com.ruoyi.task.domain.ServiceTask; import com.ruoyi.task.mapper.ServiceSubtaskMapper; import com.ruoyi.task.mapper.ServiceTaskMapper; import com.ruoyi.task.domain.ServiceSubtaskDetailTrace; import com.ruoyi.task.domain.VO.ServiceSubtaskDetailTraceVO; import com.ruoyi.task.service.IServiceSubtaskDetailTraceService; import com.ruoyi.task.domain.ServiceOutPath; import com.ruoyi.task.service.IServiceOutPathService; import com.ruoyi.task.domain.ServiceSubtaskPreachform; import com.ruoyi.task.service.IServiceSubtaskService; import com.ruoyi.task.domain.SendMagParam; /** * 定时任务调度测试 * * @author ruoyi */ @Slf4j @Component("ryTask") public class RyTask { @Autowired private IServiceThirdDataService iServiceThirdDataService; /** * 模板数据一律经 ruoyi-template 的门面取用(跨模块数据访问原则)。 */ @Autowired private TemplateFacade templateFacade; @Autowired private IServiceOutPathService iServiceOutPathService; @Autowired private IServiceSubtaskAnswerService serviceSubtaskAnswerService; @Autowired private IPatMedInhospService iPatMedInhospService; /** * 手术随访模块的常驻门面(原注入 {@code IPatMedOperationService})。 *

* 手术随访编排已整体迁入 {@code ruoyi-othervisit},本类只做调用。 * ⚠️ 注入门面而不是模块内部实现:门面永远存在,模块未打进应用或 * {@code othervisit.enabled=false} 时只是降级并打 WARN,不会启动失败。 */ @Autowired private com.ruoyi.othervisit.api.OtherVisitFacade otherVisitFacade; @Autowired private IServiceSubtaskService iServiceSubtaskService; @Autowired private IServicePatientTempService iServicePatientTempService; // ⚠️ 2026-10 移除:原 `IHNGatherPatArchiveService ihnGatherPatArchiveService` 字段。 // 它在本类里**只**被 dealHisData 的河南分支使用(死字段检查:改前全文件仅 1 处使用), // 该分支已随方法体搬进 ruoyi-datacollection,故字段连带删除 —— // 留着它会让人误以为 smartor 的河南采集还有本类这个使用方 // (真正的使用方已是数据采集模块经契约 HisGatherApi 调用的 Bridge)。 @Autowired private IServiceSLTDHealthcareRecordService serviceSLTDHealthcareRecordService; @Autowired private SysUserImportMapper sysUserImportMapper; @Autowired private ICollectHISService ichService; @Autowired private ISysConfigService configService; @Autowired private IDingTalkService dingTalkService; @Autowired private ISysUserService userService; @Value("${localIP}") private String localIP; @Value("${pub_key}") private String pub_key; @Value("${req_path}") private String req_path; @Autowired private ServiceTaskMapper serviceTaskMapper; @Autowired private ServiceSubtaskMapper serviceSubtaskMapper; @Autowired private RedisCache redisCache; @Autowired private IServiceSubtaskRecordService serviceSubtaskRecordService; // ⚠️ 2026-10 移除:原 `IIcd10Service iIcd10Service` 字段。 // 它在本类里**只**被 dealHisData 的"疾病字典增量采集"一行调用,已随方法体搬进 // ruoyi-datacollection(经契约 HisGatherApi#gatherIcd10 调回 smartor 的实现)。 @Autowired IPatArchiveService patArchiveService; @Autowired ISysDeptService sysDeptService; @Autowired IServiceSubtaskDetailTraceService serviceSubtaskDetailTraceService; @Autowired IPatMedOuthospService patMedOuthospService; @Autowired IXHGatherPatArchiveService ixhGatherPatArchiveService; @Autowired ServiceSubtaskPreachformMapper serviceSubtaskPreachformMapper; @Autowired ICollectHISService icollectHis; /** * 门诊分表维护已整体迁入 {@code ruoyi-outpatientvisit},这里只持有它的常驻门面。 *

* ⚠️ 注入门面而不是内部实现:门面永远存在,即使模块没被打进应用本类也不会启动失败。 */ @Autowired private com.ruoyi.outpatientvisit.api.OuthospVisitFacade outhospVisitFacade; /** * 出入院随访模块的常驻门面。 *

* ⚠️ 2025 修复:本类的 3 个随访入口原先不判开关 —— * 它们调用 {@code IPatMedInhospService}(smartor 里的遗留实现,见 * {@code PatMedInhospServiceImpl}),而那个实现用门面只是为了取数据 * ({@code selectPatMedInhospList} / {@code updatePatMedInhosp}), * 从不调用门面的编排方法。于是 {@code inhospvisit.enabled} / * {@code publicity.enabled} / {@code othervisit.enabled} 对定时任务完全失效: * 门面的编排守卫({@code InhospVisitFacade#dealOutHospInfo} 等)虽然有实现、 * 有测试,却只有 HTTP 手动触发那一条路走得到。 *

* 现在改为与手术随访(手术随访早已走 {@code otherVisitFacade},见 {@link #dealOperationInfo()}) * 完全同形:定时任务走门面,开关在门面里统一判。模块实现内部会回调 * {@code PublicityApi} / {@code OtherVisitApi},那两处各自判自己的开关, * 所以一次切换让 3 个开关同时生效。 *

* ⚠️ 不能注入 {@code InhospVisitApi} 接口:容器里有两个实现 * (本门面 + 内部实现 {@code DefaultInhospVisitApi}),按类型注入会歧义; * 而且只有门面才判开关。Quartz 依赖本模块、不存在反向依赖,所以直接注入门面是安全的。 */ @Autowired private com.ruoyi.inhospvisit.api.InhospVisitFacade inhospVisitFacade; /** * 宣教模块的常驻门面,用于判 {@code sys_config: publicity.enabled}。 *

* ⚠️ 2025 修复:本类原本只在**生成**宣教时受开关管(走 inhospvisit 门面回调 PublicityApi), * 但**发送**宣教({@link #sfHandlle} 的 {@code type == 3} 分支)从来不读宣教开关 —— * 所以关掉 {@code publicity.enabled} 后,已生成的宣教子任务照样通过 * 短信/公众号/电话发出去。 */ @Autowired private com.ruoyi.publicity.api.PublicityFacade publicityFacade; /** * 数据采集模块的常驻门面。 *

* ⚠️ 2026-10 模块化:本类的 {@code dealHisData(String)} 方法体(连同私有帮手 * {@code collectSegmentData})已整体搬进 {@code ruoyi-datacollection}, * 本类只保留定时任务入口(sys_job 的 invoke_target 是 {@code ryTask.dealHisData('...')}, * 入口名不能变)。站点分支、时间窗口、分段回溯、日志文案都在模块里,一行未改。 *

* 注入门面而不是模块内部 Service:门面永远存在,且只有门面判 * {@code sys_config: datacollection.enabled} 开关。 */ @Autowired private com.ruoyi.datacollection.api.DataCollectionFacade dataCollectionFacade; // ⚠️ 2026-10 删除:原 `@Value("${appid}") private String appid;` // 它是「新华微信公众号推送」的遗留字段(提交 8590ef6a 引入,只被 // xinHuaWXReq.setAppid(appid) 用过)。该推送逻辑已搬进 ruoyi-message // (公众号凭据改由 sys_config.wx.mp.config.* 下发),本类里已无引用; // 但它的 @Value 没有默认值,而 application-nhfy.yml / -xh.yml / -ls.yml // 都没定义 appid 这个 key,这些 profile 启动时会直接报 // `Could not resolve placeholder 'appid'`。留着是定时炸弹,故删除。 @Value("${server.port}") private String port; /** * 消息发送方工厂(短信/微信公众号)。 *

* 原来短信与公众号的发送逻辑在本类里用 {@code if (active.equals("xx"))} 硬分支了约 300 行, * 现已收口到 {@code ruoyi-message} 模块,这里只负责按 {@code active} 路由。 */ @Autowired private SmsSenderFactory smsSenderFactory; @Autowired private WechatSenderFactory wechatSenderFactory; @Value("${spring.profiles.active}") private String active; @Autowired private SysConfigMapper sysConfigMapper; // ⚠️ 2025 移除:原 `IServiceTaskScheduleService serviceTaskScheduleService` 字段。 // 它在本类里**只**被 rollTaskGroupCycle() 用来调 rollCycleTasks()(死字段检查: // 改前全文件仅 1 处使用)。该调用已切到 inhospVisitFacade.rollTaskGroupCycle(), // 所以字段连带失效并删除 —— 留着它会让人误以为 smartor 的 rollCycleTasks 还有人用。 // smartor 侧的 ServiceTaskScheduleServiceImpl.rollCycleTasks() 本身**保留不删** // (它可能仍被现网其它入口/接口调用,删除属另一件事)。 public void ryMultipleParams(String s, Boolean b, Long l, Double d, Integer i) { System.out.println(StringUtils.format("执行多参方法: 字符串类型{},布尔类型{},长整型{},浮点型{},整形{}", s, b, l, d, i)); } public void ryParams(String params) { System.out.println("执行有参方法:" + params); } public void ryNoParams() { System.out.println("执行无参方法"); } public void importData() { String filePath = "C:\\Users\\86176\\Desktop\\新建文本文档 4.txt"; // 替换为你的文件路径 try { // 读取文件内容 String jsonContent = new String(Files.readAllBytes(Paths.get(filePath)), StandardCharsets.UTF_8); // 创建ObjectMapper对象 ObjectMapper objectMapper = new ObjectMapper(); // 解析JSON为JsonNode JsonNode rootNode = objectMapper.readTree(jsonContent); // 获取resultList JsonNode resultListNode = rootNode.path("results").path("resultList"); // 遍历resultList中的每个对象 for (JsonNode item : resultListNode) { SysUserImport person = objectMapper.treeToValue(item, SysUserImport.class); person.setStaffPhoto(""); sysUserImportMapper.insertSysUserImport(person); } } catch (IOException e) { e.printStackTrace(); } } public void analysisData() { //数据解析 ServiceThirdData serviceThirdData = new ServiceThirdData(); serviceThirdData.setDataType("ADT^A03"); serviceThirdData.setIsDeal(0); serviceThirdData.setCreateTime(new Date()); iServiceThirdDataService.queryList(serviceThirdData); } /** * 定时发送SLTD满意度异常信息 */ public void sendSLTDMYDExceptInfo() { //1.获取发送人工号 ServiceSubtaskDetailTraceVO subtaskDetailTraceVO = new ServiceSubtaskDetailTraceVO(); subtaskDetailTraceVO.setHandleFlag("0"); List serviceSubtaskDetailTraces = serviceSubtaskDetailTraceService.selectServiceSubtaskDetailTtraceList(subtaskDetailTraceVO); // 把 dutyDeptPersonCode 整理到 list 中(如果多个人用逗号分割,则展开并去重) List dutyDeptPersonCodeList = serviceSubtaskDetailTraces.stream().map(ServiceSubtaskDetailTrace::getDutyDeptPersonCode).filter(StringUtils::isNotBlank).flatMap(code -> Arrays.stream(code.split(","))).map(String::trim).filter(StringUtils::isNotBlank).distinct() // 去重 .collect(Collectors.toList()); // 获取“省立同德满意度异常内容提醒" SysConfig sysConfig = sysConfigMapper.checkConfigKeyUnique("sltd.myd.send.except.contont"); String exceptContent = sysConfig.getConfigValue(); // 获取“省立同德满意度异常内容提醒测试人工号” String testPerson = sysConfigMapper.checkConfigKeyUnique("sltd.myd.send.test.person").getConfigValue(); if (StringUtils.isNotBlank(testPerson)) { String[] split = testPerson.split(","); if (CollectionUtils.isEmpty(dutyDeptPersonCodeList)) dutyDeptPersonCodeList = Arrays.asList(split); else dutyDeptPersonCodeList.addAll(Arrays.asList(split)); } if (CollectionUtils.isNotEmpty(dutyDeptPersonCodeList)) { for (String dutyDeptPersonCode : dutyDeptPersonCodeList) { log.info("【sendSLTDMYDExceptInfo】发送人工号:{}", dutyDeptPersonCode); String data = MessageSend.sendMsg(exceptContent, "7", dutyDeptPersonCode, LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))); } } } /** * 定时发送异常信息-景宁 */ public void sendExceptInfo() throws Exception { //1.获取发送人工号 ServiceSubtaskDetailTraceVO subtaskDetailTraceVO = new ServiceSubtaskDetailTraceVO(); subtaskDetailTraceVO.setHandleFlag("0"); List serviceSubtaskDetailTraces = serviceSubtaskDetailTraceService.selectServiceSubtaskDetailTtraceList(subtaskDetailTraceVO); // 获取“异常内容提醒" SysConfig sysConfig = sysConfigMapper.checkConfigKeyUnique("send.except.content"); String exceptContent = sysConfig.getConfigValue(); // 获取“内容提醒测试人工号” SysConfig testPersonConfig = sysConfigMapper.checkConfigKeyUnique("send.test.person"); String testPerson = ObjectUtils.isNotEmpty(testPersonConfig) ? testPersonConfig.getConfigValue() : ""; if (CollectionUtils.isNotEmpty(serviceSubtaskDetailTraces)) { for (ServiceSubtaskDetailTrace serviceSubtaskDetailTrace : serviceSubtaskDetailTraces) { List dutyDeptPersonCodeList = new ArrayList<>(); // 把 dutyDeptPersonCode 整理到 list 中(如果多个人用逗号分割,则展开并去重) String dutyDeptPersonCodeStr = serviceSubtaskDetailTrace.getDutyDeptPersonCode(); if (StringUtils.isNotEmpty(dutyDeptPersonCodeStr)) { String[] dutyDeptPersonCodeSplit = dutyDeptPersonCodeStr.split(","); if (CollectionUtils.isEmpty(dutyDeptPersonCodeList)) { dutyDeptPersonCodeList = new ArrayList<>(Arrays.asList(dutyDeptPersonCodeSplit)); } else { dutyDeptPersonCodeList.addAll(Arrays.asList(dutyDeptPersonCodeSplit)); } } if (StringUtils.isNotBlank(testPerson)) { String[] split = testPerson.split(","); if (CollectionUtils.isEmpty(dutyDeptPersonCodeList)) { dutyDeptPersonCodeList = new ArrayList<>(Arrays.asList(split)); } else { dutyDeptPersonCodeList.addAll(Arrays.asList(split)); } } if (CollectionUtils.isNotEmpty(dutyDeptPersonCodeList)) { for (String dutyDeptPersonCode : dutyDeptPersonCodeList) { log.info("【sendExceptInfo】发送人工号:{}", dutyDeptPersonCode); SysUser sysUser = new SysUser(); SysUser sysUserVo = new SysUser(); sysUserVo.setUserName(dutyDeptPersonCode); sysUserVo.setOrgid(serviceSubtaskDetailTrace.getOrgid()); List users = userService.getUserList(sysUserVo); String phoneNumber = ""; if (ObjectUtils.isNotEmpty(users)) { sysUser = users.get(0); phoneNumber = sysUser.getPhonenumber(); } else { continue; } if (StringUtils.isEmpty(phoneNumber)) { continue; } String dutyDeptPerson = dingTalkService.getDingTalkUserByMobile(phoneNumber); if (StringUtils.isNotBlank(dutyDeptPerson)) { String context = TemplateUtils.render(exceptContent, serviceSubtaskDetailTrace); dingTalkService.sendDingTalkText(context, dutyDeptPerson); //更新状态 serviceSubtaskDetailTrace.setHandleFlag("1"); serviceSubtaskDetailTrace.setUpdateTime(new Date()); serviceSubtaskDetailTraceService.updateServiceSubtaskDetailTtrace(serviceSubtaskDetailTrace); } } } } } } /** * 定时处理外链的数据 */ public void runServiceSubTaskAnswer() { serviceSubtaskAnswerService.saveQuestionAnswer(null, 0L); } /** * 获取预约停诊记录信息 * */ public void getAppointRecordInfo() { LocalDateTime tomorrowStart = LocalDate.now().plusDays(1).atStartOfDay(); // 明天 00:00:00 LocalDateTime tomorrowEnd = tomorrowStart.plusDays(1).minusSeconds(1); // 明天 23:59:59 ixhGatherPatArchiveService.getAppointRecordInfo("2", tomorrowStart, tomorrowEnd); } /** * 每月15号和月底,查询并处理 HIS 数据(同德,南华) */ public void dealHisDataHalfMonth() { LocalDateTime now = LocalDateTime.now(); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); String formattedNow = now.format(formatter); dealHisData(formattedNow); } /** * HIS数据采集 * 定时任务:采集患者信息、出院、门诊数据 * todayZeroStr 当前的时间 例如:2026-05-06 00:00:00 *

* ⚠️ 2026-10 模块化:方法体(连同私有帮手 {@code collectSegmentData})已整体搬进 * {@code ruoyi-datacollection},实现见 * {@code com.ruoyi.datacollection.service.impl.HisDataCollectionServiceImpl}。 * 这里只保留定时任务入口:sys_job 的 invoke_target 仍是 * {@code ryTask.dealHisData('...')},入口名不能变,所以本方法是纯转发。 *

* 关闭数据采集模块({@code sys_config: datacollection.enabled=false})时门面会抛 * {@link com.ruoyi.datacollection.api.DataCollectionModuleDisabledException}; * 本入口捕获后只打一条 WARN —— 那是"模块被主动关掉",不是采集失败, * 不该让定时任务记成执行异常(否则运维会看到一片红色失败记录)。 */ public void dealHisData(String todayZeroStr) { try { dataCollectionFacade.dealHisData(todayZeroStr); } catch (com.ruoyi.datacollection.api.DataCollectionModuleDisabledException e) { log.warn("【dealHisData】{}(本轮定时采集跳过)", e.getMessage()); } } /** * 在院患者"科室 / 病区 / 床位"刷新(南华附一,每天一次)。 *

* 在院患者会转科、转病区、换床,而 {@code pat_med_inhosp} 的位置字段只在入院时写过一次, * 之后不会自己更新 —— 这里每天按患者查一遍 HIS 里该患者当前的在院信息 * ({@code dtcQueryHealthcareRecordList},与入院采集是同一个接口,按 * {@code healthcareRecordNo} 单查 + {@code statusList=["FH0109.24"]}), * 与库内比对后只把变化的字段写回去。实现与判定口径见 * {@code com.ruoyi.datacollection.service.impl.InhospDeptRefreshServiceImpl}。 *

* 与 {@link #dealHisData(String)} 同样的降级口径:模块被关掉( * {@code sys_config: datacollection.enabled=false})时门面抛 * {@link com.ruoyi.datacollection.api.DataCollectionModuleDisabledException}, * 这里只打一条 WARN —— 那是"模块被主动关掉",不该记成定时任务执行异常。 *

* sys_job 的 invoke_target 为 {@code ryTask.refreshInhospDeptInfo()}, * 建议每天 03:30(避开 01:00 入院采集与 02:00 出院处理),并发策略选"禁止并发"。 * * @return 本次实际更新的在院记录条数(0 = 无变化 / 站点不匹配 / 模块被关闭) */ public int refreshInhospDeptInfo() { try { return dataCollectionFacade.refreshInhospDeptInfo(); } catch (com.ruoyi.datacollection.api.DataCollectionModuleDisabledException e) { log.warn("【refreshInhospDeptInfo】{}(本轮在院信息刷新跳过)", e.getMessage()); return 0; } } /** * HIS省立同德门急诊数据采集(每半小时一轮,按天全量刷新 + 分页断点续采) *

* HIS 侧门急诊数据只能按天查,每轮拉到的都是当天全量(到晚上一万多条),这个取数成本改不了; * 能改的是处理成本:当天数据里绝大多数上一轮已经入库,靠"整页批量查重 + 已入库直接跳过", * 每轮真正需要做档案处理和入库的只有新出现的那几百条,一万多条也能在很短时间内跑完。 *

* 每一轮做三件事: *

    *
  1. 今天的整轮刷新:第 1 页翻到最后一页,只处理新记录;
  2. *
  3. 昨天的收尾采集:每天只做一次,兜住 23:xx 之后才写入 HIS 的记录和跨零点的数据;
  4. *
  5. 手工回溯:任务入参给了日期时,把那天补采一次(同一天只补一次,不会每轮重复补)。
  6. *
* 每轮最多跑 {@link MzGatherParamEnum#BUDGET_MINUTES}(默认 20 分钟),到点保存页码断点正常退出,下一轮从该页继续; * 每采完一页都会落一次断点,进程被杀也不会从头重扫。 *

* 失败一律不阻断采集进度: *

* * @param todayZeroStr 可选,手工指定要一次性回溯的日期,支持 yyyy-MM-dd HH:mm:ss 或 yyyy-MM-dd;为空时只刷新今天+收尾昨天 */ public void dealHisDataMZ(String todayZeroStr) { log.info("【dealHisDataMZ】门急诊数据采集开始,入参:{}", todayZeroStr); // 南华附一的先不采门急诊数据 if (!"sltd".equals(active.trim())) { log.info("【dealHisDataMZ】当前环境 {} 不采集门急诊数据,跳过", active.trim()); return; } long budgetMinutes = getMzConfigLong(MzGatherParamEnum.BUDGET_MINUTES); long pageSize = getMzConfigLong(MzGatherParamEnum.PAGE_SIZE); long maxPage = getMzConfigLong(MzGatherParamEnum.MAX_PAGE); LocalDateTime now = LocalDateTime.now().withNano(0); LocalDateTime deadline = now.plusMinutes(budgetMinutes); // 获取省立同德要采集的院区 SysConfig cf = new SysConfig(); cf.setConfigKey("sltd.gather.campusid"); cf.setOrgid(MzGatherConstant.ORG_ID); List sysConfigcampusids = configService.selectConfigList(cf); if (CollectionUtils.isEmpty(sysConfigcampusids) || StringUtils.isBlank(sysConfigcampusids.get(0).getConfigValue())) { log.error("【dealHisDataMZ】SysConfig中的院区ID为空!"); return; } List campusidList = Arrays.stream(sysConfigcampusids.get(0).getConfigValue().split(",")).map(String::trim).filter(StringUtils::isNotBlank).map(Long::valueOf).collect(Collectors.toList()); log.info("【dealHisDataMZ】院区:{},单轮预算:{}分钟,每页:{}条", campusidList, budgetMinutes, pageSize); for (Long cid : campusidList) { if (LocalDateTime.now().isAfter(deadline)) { log.warn("【dealHisDataMZ】本轮时间预算已用尽,剩余院区留到下一轮继续,院区:{}", cid); break; } // 防止上一轮还没跑完就被下一轮叠着跑(预算配置过大、或手工触发时兜底) String lockKey = "mzRunning:" + cid; if (Boolean.TRUE.equals(redisCache.hasKey(lockKey))) { log.warn("【dealHisDataMZ】该院区上一轮采集尚未结束,本轮跳过,院区:{}(断点已保留,下一轮继续)", cid); continue; } redisCache.setCacheObject(lockKey, MzGatherConstant.MINUTE_FMT.format(now), budgetMinutes * 2, TimeUnit.MINUTES); try { collectOutpatientDaily(cid, todayZeroStr, pageSize, maxPage, deadline); } catch (Exception e) { log.error("【dealHisDataMZ】门急诊采集异常,院区:{},断点已保留,下一轮从断点继续", cid, e); } finally { redisCache.deleteObject(lockKey); } } log.info("【dealHisDataMZ】门急诊采集本轮结束,耗时:{}秒", java.time.Duration.between(now, LocalDateTime.now()).getSeconds()); } /** * 采集某个院区:先补一次性回溯日期,再做昨天的收尾采集,最后整轮刷新今天 */ private void collectOutpatientDaily(Long cid, String backfillParam, long pageSize, long maxPage, LocalDateTime deadline) { String cursorKey = "mzDayCursor:" + cid; MzDayCursor cursor = loadMzDayCursor(cursorKey); LocalDate today = LocalDate.now(); LocalDate yesterday = today.minusDays(1); // 手工入参指定的一次性回溯:同一个日期只补一次,避免定时任务每轮重复补 LocalDateTime backfillTime = parseMzStartTime(backfillParam); if (backfillTime != null) { LocalDate backfillDay = backfillTime.toLocalDate(); if (backfillDay.toString().equals(cursor.backfillDone)) { log.info("【dealHisDataMZ】入参日期 {} 已完成回溯,跳过,院区:{}", backfillDay, cid); } else if (backfillDay.equals(today)) { // 入参就是今天,交给下面的整轮刷新处理即可 cursor.backfillDone = backfillDay.toString(); saveMzDayCursor(cursorKey, cursor); } else { cursor.backfillDay = backfillDay.toString(); cursor.backfillPage = 1L; saveMzDayCursor(cursorKey, cursor); log.info("【dealHisDataMZ】待回溯日期:{},院区:{}", backfillDay, cid); } } // 1) 一次性回溯 if (cursor.backfillDay != null) { LocalDate day = LocalDate.parse(cursor.backfillDay); MzGatherStatusEnum status = runDayPass(cid, cursorKey, cursor, MzGatherSlotEnum.BACKFILL, day, cursor.backfillPage, pageSize, maxPage, deadline); if (status != MzGatherStatusEnum.DONE) { log.warn("【dealHisDataMZ】回溯日期 {} 本轮未完成({}),断点已保留,院区:{}", day, status.getDesc(), cid); return; } cursor.backfillDone = cursor.backfillDay; cursor.backfillDay = null; cursor.backfillPage = 1L; saveMzDayCursor(cursorKey, cursor); } // 2) 昨天收尾采集:每天一次,兜住 23:xx 之后才写进 HIS 的记录和跨零点的数据 if (!yesterday.toString().equals(cursor.finalDay)) { MzGatherStatusEnum status = runDayPass(cid, cursorKey, cursor, MzGatherSlotEnum.FINAL, yesterday, cursor.finalPage, pageSize, maxPage, deadline); if (status != MzGatherStatusEnum.DONE) { log.warn("【dealHisDataMZ】昨日收尾采集 {} 本轮未完成({}),断点已保留,院区:{}", yesterday, status.getDesc(), cid); return; } cursor.finalDay = yesterday.toString(); cursor.finalPage = 1L; saveMzDayCursor(cursorKey, cursor); } // 3) 今天的整轮刷新 if (!today.toString().equals(cursor.day)) { cursor.day = today.toString(); cursor.page = 1L; saveMzDayCursor(cursorKey, cursor); } MzGatherStatusEnum status = runDayPass(cid, cursorKey, cursor, MzGatherSlotEnum.TODAY, today, cursor.page, pageSize, maxPage, deadline); if (status == MzGatherStatusEnum.DONE) { // 整轮刷新完成,页码归位,下一轮重新从第一页整轮刷新 cursor.page = 1L; saveMzDayCursor(cursorKey, cursor); log.info("【dealHisDataMZ】{} 门急诊整轮刷新完成,院区:{}", today, cid); } } /** * 全量采集某一天(分页直到采完),每采完一页立即落一次断点 * * @param slot 断点槽位(今天整轮刷新 / 昨日收尾 / 手工回溯),决定中断页码记到哪个字段 * @return {@link MzGatherStatusEnum} 采集完成 / 预算用尽 / 接口连续失败提前结束 */ private MzGatherStatusEnum runDayPass(Long cid, String cursorKey, MzDayCursor cursor, MzGatherSlotEnum slot, LocalDate day, long startPage, long pageSize, long maxPage, LocalDateTime deadline) { String startStr = day.format(MzGatherConstant.DAY_FMT); String endStr = day.plusDays(1).format(MzGatherConstant.DAY_FMT); ServiceSLTDInhospReqVO reqVO = new ServiceSLTDInhospReqVO(); reqVO.setOrgId(MzGatherConstant.ORG_ID); reqVO.setCampusId(cid); reqVO.setStartHeadTime(null); reqVO.setStartTailTime(null); reqVO.setStatusList(null); // 接口只支持按天查,起止都传日期 reqVO.setEncounterTimeStart(startStr); reqVO.setEncounterTimeEnd(endStr); List typeList = new ArrayList<>(); typeList.add("FH0108.01"); // 门诊 typeList.add("FH0108.03"); // 急诊 reqVO.setHealthcareRecordTypeList(typeList); reqVO.setSize(pageSize); long page = startPage < 1L ? 1L : startPage; long total = 0L; long pageCount = 0L; // 接口层失败跳过的页数、第一个失败页、连续失败页数 long skippedPages = 0L; long firstFailPage = -1L; int consecutiveFail = 0; while (true) { if (LocalDateTime.now().isAfter(deadline)) { // 本轮有跳过的页时,断点回到第一个失败页,保证下一轮先把这些页补回来 long resumePage = (skippedPages > 0 && firstFailPage > 0) ? firstFailPage : page; saveMzDayPage(cursorKey, cursor, slot, resumePage); log.warn("【dealHisDataMZ】本轮时间预算用尽,{} 本轮已采{}页{}条{},下次从第{}页继续,院区:{}", day, pageCount, total, skippedLog(skippedPages), resumePage, cid); return MzGatherStatusEnum.BUDGET_UP; } reqVO.setCurrent(page); log.info("【dealHisDataMZ】采集门急诊数据,院区:{},日期:{},第{}页", cid, day, page); List pageData = null; Exception lastError = null; for (int attempt = 1; attempt <= MzGatherConstant.PAGE_RETRY_TIMES; attempt++) { try { pageData = serviceSLTDHealthcareRecordService.queryHealthcareRecordList(reqVO); lastError = null; break; } catch (Exception e) { lastError = e; log.error("【dealHisDataMZ】门急诊第{}页第{}次采集失败,院区:{},日期:{}", page, attempt, cid, day, e); if (attempt < MzGatherConstant.PAGE_RETRY_TIMES) { try { Thread.sleep(MzGatherConstant.PAGE_RETRY_SLEEP_MS); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); break; } } } } if (lastError != null) { // 接口层失败(网络超时 / 来未来返回非200)没有数据可以落表,只写 error 日志,然后跳过该页继续往后采,不阻断进度。 // 每轮都是整天从头全量刷新,被跳过的页下一轮必然重新采到,所以不会漏数据。 skippedPages++; if (firstFailPage < 0) { firstFailPage = page; } consecutiveFail++; log.error("【dealHisDataMZ】接口层采集失败,已记 error 日志并跳过该页,本轮继续往后采;" + "院区:{},日期:{},第{}页,接口入参:{},异常:{}", cid, day, page, new Gson().toJson(reqVO), lastError.getMessage(), lastError); if (consecutiveFail >= MzGatherConstant.MAX_CONSECUTIVE_PAGE_FAIL) { // 连续多页接口失败,判定来未来接口整体不可用,本轮提前结束(不再空转后面几十页),断点留在第一个失败页 log.error("【dealHisDataMZ】连续{}页接口失败,判定来未来接口不可用,本轮提前结束;院区:{},日期:{},断点保留在第{}页", consecutiveFail, cid, day, firstFailPage); saveMzDayPage(cursorKey, cursor, slot, firstFailPage); return MzGatherStatusEnum.FAILED; } page++; continue; } consecutiveFail = 0; if (CollectionUtils.isEmpty(pageData)) { log.info("【dealHisDataMZ】{} 第{}页无数据,采集结束,院区:{},本轮{}页共{}条{}", day, page, cid, pageCount, total, skippedLog(skippedPages)); return MzGatherStatusEnum.DONE; } pageCount++; total += pageData.size(); log.info("【dealHisDataMZ】{} 第{}页采集{}条,院区:{}", day, page, pageData.size(), cid); if (pageData.size() < pageSize) { log.info("【dealHisDataMZ】{} 采集结束,院区:{},本轮{}页共{}条{}", day, cid, pageCount, total, skippedLog(skippedPages)); return MzGatherStatusEnum.DONE; } page++; if (page > maxPage) { log.error("【dealHisDataMZ】{} 已翻到第{}页,超过单日上限{}页,按采完处理,院区:{}", day, page, maxPage, cid); return MzGatherStatusEnum.DONE; } // 每采完一页立即落断点:进程被重启/强杀也能从这一页继续,不从头重扫,也不会漏 saveMzDayPage(cursorKey, cursor, slot, page); } } /** * 按槽位保存中断页码 */ private void saveMzDayPage(String cursorKey, MzDayCursor cursor, MzGatherSlotEnum slot, long page) { if (slot == MzGatherSlotEnum.FINAL) { cursor.finalPage = page; } else if (slot == MzGatherSlotEnum.BACKFILL) { cursor.backfillPage = page; } else { cursor.page = page; } saveMzDayCursor(cursorKey, cursor); } /** * 日志里提示接口层失败跳过了几页(下一轮整轮刷新会重新采到) */ private String skippedLog(long skippedPages) { return skippedPages > 0L ? ",接口失败跳过" + skippedPages + "页(下一轮整轮刷新会重新采到)" : ""; } /** * 读取 sys_config 里的门急诊采集参数(机构 orgid=20001001),取不到或超范围时用枚举里的默认值 */ private long getMzConfigLong(MzGatherParamEnum param) { try { SysConfig config = new SysConfig(); config.setConfigKey(param.getKey()); config.setOrgid(MzGatherConstant.ORG_ID); List configs = configService.selectConfigList(config); if (CollectionUtils.isNotEmpty(configs) && StringUtils.isNotBlank(configs.get(0).getConfigValue())) { long value = Long.parseLong(configs.get(0).getConfigValue().trim()); if (value >= param.getMin() && value <= param.getMax()) { return value; } log.warn("【dealHisDataMZ】配置{}的值{}超出范围[{},{}],使用默认值{}", param.getKey(), value, param.getMin(), param.getMax(), param.getDefaultValue()); } } catch (Exception e) { log.warn("【dealHisDataMZ】读取配置{}失败,使用默认值{}", param.getKey(), param.getDefaultValue(), e); } return param.getDefaultValue(); } /** * 解析任务入参(要一次性回溯的日期),支持 yyyy-MM-dd HH:mm:ss 与 yyyy-MM-dd */ private LocalDateTime parseMzStartTime(String text) { LocalDateTime time = parseMzTimeQuiet(text); if (time == null && StringUtils.isNotBlank(text)) { log.warn("【dealHisDataMZ】入参 {} 无法解析为日期,按未指定处理", text); } return time; } /** * 静默解析日期,支持 yyyy-MM-dd HH:mm:ss 与 yyyy-MM-dd,格式不符返回 null(不刷日志) */ private LocalDateTime parseMzTimeQuiet(String text) { if (StringUtils.isBlank(text)) { return null; } String value = text.trim(); try { return LocalDateTime.parse(value, MzGatherConstant.MINUTE_FMT); } catch (Exception ignore) { // 兼容只传日期的情况,落到下面按日期解析 } try { return LocalDate.parse(value).atStartOfDay(); } catch (Exception ignore) { return null; } } /** * 保存门急诊采集断点(各轮次中断的页码) */ private void saveMzDayCursor(String cursorKey, MzDayCursor cursor) { JSONObject json = new JSONObject(); json.put("day", cursor.day); json.put("page", cursor.page); json.put("finalDay", cursor.finalDay); json.put("finalPage", cursor.finalPage); json.put("backfillDay", cursor.backfillDay); json.put("backfillPage", cursor.backfillPage); json.put("backfillDone", cursor.backfillDone); json.put("updateTime", MzGatherConstant.MINUTE_FMT.format(LocalDateTime.now().withNano(0))); redisCache.setCacheObject(cursorKey, json.toJSONString(), MzGatherConstant.CACHE_TTL_HOURS, TimeUnit.HOURS); } /** * 读取门急诊采集断点,读不到或解析失败时返回空断点(等价于从第一页开始整轮刷新) */ private MzDayCursor loadMzDayCursor(String cursorKey) { MzDayCursor cursor = new MzDayCursor(); try { String json = redisCache.getCacheObject(cursorKey); if (StringUtils.isBlank(json)) { return cursor; } JSONObject obj = JSONObject.parseObject(json); cursor.day = obj.getString("day"); cursor.page = obj.getLongValue("page"); cursor.finalDay = obj.getString("finalDay"); cursor.finalPage = obj.getLongValue("finalPage"); cursor.backfillDay = obj.getString("backfillDay"); cursor.backfillPage = obj.getLongValue("backfillPage"); cursor.backfillDone = obj.getString("backfillDone"); if (cursor.page < 1L) { cursor.page = 1L; } if (cursor.finalPage < 1L) { cursor.finalPage = 1L; } if (cursor.backfillPage < 1L) { cursor.backfillPage = 1L; } return cursor; } catch (Exception e) { log.warn("【dealHisDataMZ】读取断点{}失败,按空断点处理", cursorKey, e); return new MzDayCursor(); } } /** * 门急诊采集断点:每天一轮全量刷新,这里分别记录三个轮次中断在第几页 * (页码为 1 表示该轮次下一轮从第一页整轮开始) */ private static class MzDayCursor { /** * 今天的整轮刷新 */ private String day; private long page = 1L; /** * 昨天的收尾采集 */ private String finalDay; private long finalPage = 1L; /** * 待做的一次性回溯 */ private String backfillDay; private long backfillPage = 1L; /** * 已经补过的回溯日期,避免定时任务每轮重复补同一天 */ private String backfillDone; } /** * 同德药品数据采集 */ public void dealHisDataMedication() { final long OUTP_PAGE_SIZE = 500L; ServiceSLTDInhospReqVO serviceSLTDInhospReqVO = new ServiceSLTDInhospReqVO(); serviceSLTDInhospReqVO.setOrgId("20001001"); Long outpPage = 1L; int totalCount = 0; // 统计总共采集的药品数量 while (true) { serviceSLTDInhospReqVO.setCurrent(outpPage); serviceSLTDInhospReqVO.setSize(OUTP_PAGE_SIZE); log.info("【dealHisData】省立同德\"药品数据\"采集第{}页数据", outpPage); try { Integer fetchedCount = serviceSLTDHealthcareRecordService.queryMedicationItemList(serviceSLTDInhospReqVO); log.info("【dealHisData】省立同德\"药品数据\"第{}页采集到{}条记录", outpPage, fetchedCount); totalCount += fetchedCount != null ? fetchedCount : 0; // 如果返回的数据量小于 pageSize,说明已经是最后一页了,退出循环 if (fetchedCount == null || fetchedCount < OUTP_PAGE_SIZE) { log.info("【dealHisData】省立同德\"药品数据\"采集完成,总页数:{}, 总计{}条记录", outpPage, totalCount); break; } outpPage++; } catch (Exception e) { log.error("【dealHisData】省立同德\"药品数据\"第{}页采集失败", outpPage, e); // 出错时也可以选择退出或继续,这里选择退出避免无限重试 break; } } } public void dealHisDataSLTD() { log.info("【dealHisData】开始执行HIS数据采集任务"); try { LocalDate startDate = LocalDate.of(2025, 11, 1); LocalDate endDate = LocalDate.of(2026, 2, 27); // 省立同德数据采集 // String nowTime = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); for (LocalDate date = startDate; !date.isAfter(endDate); date = date.plusDays(1)) { String nowTime = date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); ServiceSLTDInhospReqVO serviceSLTDInhospReqVO = new ServiceSLTDInhospReqVO(); serviceSLTDInhospReqVO.setOrgId("20001001"); serviceSLTDInhospReqVO.setCampusId(30001002L); log.info("【dealHisData】省立同德“门急诊”开始采集数据,院区:{}", "30001002"); serviceSLTDInhospReqVO.setStartHeadTime(null); serviceSLTDInhospReqVO.setStartTailTime(null); serviceSLTDInhospReqVO.setEncounterTimeStart(nowTime); serviceSLTDInhospReqVO.setEncounterTimeEnd(nowTime); serviceSLTDInhospReqVO.setStatusList(null); List list1 = new ArrayList<>(); list1.add("FH0108.01"); list1.add("FH0108.03"); serviceSLTDInhospReqVO.setHealthcareRecordTypeList(list1); log.info("【dealHisData】省立同德“门急诊”采集数据入参:{}", serviceSLTDInhospReqVO); serviceSLTDHealthcareRecordService.queryHealthcareRecordList(serviceSLTDInhospReqVO); log.info("【dealHisData】省立同德“门急诊”束采集数据,院区:{}", "30001002"); } } catch (Exception e) { log.error("【dealHisData】省立同德数据采集异常", e); } } /** * 这个只是新华临时使用,平时用不到 */ public void dealHisData2() { //将患者信息、出入院数据全部采集 // 指定的开始日期 LocalDate endDate = LocalDate.of(2026, 7, 6); // 当前日期 LocalDate currentDate = LocalDate.now(); // 定义日期格式 DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd "); // 从开始日期到当前日期逐天打印 for (LocalDate date = endDate; !date.isAfter(currentDate); date = date.plusDays(1)) { System.out.println(date.format(formatter)); ixhGatherPatArchiveService.getInHospDataGather("0", date.minusDays(1).atStartOfDay(), date.atStartOfDay()); ixhGatherPatArchiveService.getInHospDataGather("1", date.minusDays(1).atStartOfDay(), date.atStartOfDay()); } } /** * 处理患者信息,进入子任务表 *

* ⚠️ 2025 修复(开关失效):本方法原走 {@code iPatMedInhospService.dealOutHospInfo(config)}, * 那是 smartor 里的遗留实现,**不判任何模块开关** —— 于是: *

    *
  • 关 {@code inhospvisit.enabled} 照样生成出院/长期随访;
  • *
  • 关 {@code publicity.enabled} 照样发入院宣教与药品宣教;
  • *
  • 关 {@code othervisit.enabled} 照样生成手术随访。
  • *
* 现改走模块门面:门面判 {@code inhospvisit.enabled},模块实现内部再回调 * {@code PublicityApi} / {@code OtherVisitApi},三处各自判自己的开关。 */ public void dealOutHospInfo() { String config = configService.selectConfigByKey("visit.early.day"); if (port.equals("8095")) { //出院表(含入院宣教 / 药品宣教 / 手术随访 —— 均由模块实现回调对应模块门面) inhospVisitFacade.dealOutHospInfo(config); //门诊表(新华的暂时不做门诊随访) if (!active.equals("xh")) { // 【门急诊模块化切换】门面内部判 sys_config: outpatientvisit.enabled(运行时热读), // 关闭时返回 0 并打 WARN。 // // ⚠️ 2025 修复:这里原先是 iPatMedOuthospService.dealOutpatientInfo(), // 它经 IPatMedOuthospService → PatMedOuthospServiceDelegatingImpl // → OuthospDataFacade.dealOutpatientInfo(),而**那条路上没有开关守卫** —— // OuthospVisitFacade.dealOutpatientInfo() 才是带守卫的入口,却全仓零调用者。 // 结果是 outpatientvisit.enabled=false 对定时任务**完全失效**。 // 现在与下面 dealOperationInfo() 走 otherVisitFacade 的形状一致。 outhospVisitFacade.dealOutpatientInfo(); } } } /** * 处理患者信息,进入子任务表(任务组) *

* ⚠️ 2025 修复(开关失效):原走 smartor 遗留实现,不判开关(同 {@link #dealOutHospInfo()})。 * 现走模块门面。注意模块侧的 {@code dealOutHospInfoBySchedule} 内部仍会转发回 smartor * ({@code OtherVisitApiBridge},见该类注释),但开关在门面这一层判,所以关闭时不会走到那里。 */ public void dealOutHospInfoBySchedule() { String config = configService.selectConfigByKey("visit.early.day"); if (port.equals("8095")) { //出院表 inhospVisitFacade.dealOutHospInfoBySchedule(config); //门诊表(新华的暂时不做门诊随访) // if (!active.equals("xh")) { // //iPatMedOuthospService.dealOutpatientInfo(); // } } } /** * 处理手术信息,进入子任务表 */ public void dealOperationInfo() { // 【手术模块化切换】出口改为调用 ruoyi-othervisit 的常驻门面。 // 门面内部判 sys_config: othervisit.enabled(运行时热读),关闭时返回 0 并打 WARN。 if (port.equals("8095")) otherVisitFacade.dealOperationInfo(); } /** * 门诊分表定时任务:从 sys_config 读取分表模式,按模式建表并刷新视图 v_pat_med_outhosp * 配置 key: outhosp.split.mode * quarter -> 按季度,表名示例:pat_med_outhosp_20260103 * year -> 按年,表名示例:pat_med_outhosp_2026 * month -> 按月,表名示例:pat_med_outhosp_202601 * 查询不到配置则不分表 */ public void createOuthospQuarterTable() { log.info("【createOuthospQuarterTable】开始执行门诊分表任务"); // 【门诊模块化】CreateDataTable 已整体迁入 ruoyi-outpatientvisit // (实现为 OuthospPartitionMaintainer,经 OuthospVisitFacade 暴露)。 // 这里只做调用;建表逻辑一行未动。 outhospVisitFacade.createOuthospQuarterTable(); } /** * 处理患者信息,进入子任务表(微信小程序) */ public void dealOutHospInfoXHC() { if (port.equals("8095")) { PatMedInhosp patMedInhosp = new PatMedInhosp(); //获取未处理的数据 List servicePatientTemps = iServicePatientTempService.selectServicePatientTempList(new ServicePatientTemp()); } } /** * 长期任务发送 */ public void longTaskSend() { //获取任务信息 ServiceTask st = new ServiceTask(); st.setDelFlag("0"); st.setLongTask(1); List serviceTasks = serviceTaskMapper.selectServiceTaskList(st); log.info("【longTaskSend】获取到{}个长期任务", serviceTasks.size()); for (ServiceTask serviceTask : serviceTasks) { CommonTaskcallMQ commonTaskcallMQ = new CommonTaskcallMQ(); commonTaskcallMQ.setTaskid(serviceTask.getTaskid()); commonTaskcallMQ.setPreachform(serviceTask.getPreachform()); commonTaskcallMQ.setSendType("2"); //通过任务ID拿到患者信息,并且随访时间得是今天之前的 ServiceSubtaskEntity serviceSubtaskVO = new ServiceSubtaskEntity(); serviceSubtaskVO.setTaskid(commonTaskcallMQ.getTaskid()); serviceSubtaskVO.setSendstate(2L); serviceSubtaskVO.setIsVisitAgain(1); serviceSubtaskVO.setVisitTime(new Date()); List selectServiceSubtaskList = serviceSubtaskMapper.queryServiceSubtaskList(serviceSubtaskVO); for (ServiceSubtask serviceSubtask : selectServiceSubtaskList) { sfHandlle(serviceSubtask); } } } /** * 处理补偿任务 */ public void compensateTask() { //获取到sendstate=3、1 并且 visit_time为小于等于今天的subtask List list = Arrays.asList(3L, 1L); ServiceSubtaskEntity serviceSubtaskVO = new ServiceSubtaskEntity(); List serviceSubtaskList = new ArrayList<>(); for (Long sendstate : list) { serviceSubtaskVO.setSendstate(sendstate); serviceSubtaskVO.setVisitTime(new Date()); List compensateServiceSubtaskList = serviceSubtaskMapper.getCompensateServiceSubtaskList(serviceSubtaskVO); serviceSubtaskList.addAll(compensateServiceSubtaskList); } for (ServiceSubtask serviceSubtask : serviceSubtaskList) { //根据当前的执行方式,获取下一种执行方式 ServiceSubtaskPreachform serviceSubtaskPreachform = new ServiceSubtaskPreachform(); serviceSubtaskPreachform.setTaskid(serviceSubtask.getTaskid()); serviceSubtaskPreachform.setSubid(serviceSubtask.getId()); serviceSubtaskPreachform.setOrgid(serviceSubtask.getOrgid()); List serviceSubtaskPreachforms = serviceSubtaskPreachformMapper.selectServiceSubtaskPreachformList(serviceSubtaskPreachform); //获取当前执行方式的序号 Optional currentSort = serviceSubtaskPreachforms.stream().filter(item -> serviceSubtask.getCurrentPreachform().equals(item.getPreachform())).map(ServiceSubtaskPreachform::getSort).findFirst(); Optional id = serviceSubtaskPreachforms.stream().filter(item -> serviceSubtask.getCurrentPreachform().equals(item.getPreachform())).map(ServiceSubtaskPreachform::getId).findFirst(); if (currentSort.isPresent()) { //1先检查一下,是不是有执行状态是完成的(怕之前已经有完的了,没有将servuce_subtask的状态改成功,这里再检查一下) boolean finishState = serviceSubtaskPreachforms.stream().allMatch(item -> item.getSendstate().equals("9")); if (finishState) { serviceSubtask.setSendstate(6L); serviceSubtaskMapper.updateServiceSubtask(serviceSubtask); continue; } //2判断一下,当前的sort是不是等于需要执行的总个数,如果等于的话,说明是最后一个,直接将servuce_subtask的状态改成5,执行失败就行了 Long cs = currentSort.get(); if (cs.equals(serviceSubtaskPreachforms.size()) && isSubtaskDeadline(serviceSubtask, serviceSubtaskPreachforms)) { serviceSubtask.setSendstate(7L); serviceSubtask.setRemark("处理补偿任务,当前处理最后补偿,全部执行失败(超时)"); serviceSubtaskMapper.updateServiceSubtask(serviceSubtask); //修改发送方式的状态为失败 serviceSubtaskPreachform.setSendstate("5"); serviceSubtaskPreachform.setId(id.get()); serviceSubtaskPreachformMapper.updateServiceSubtaskPreachform(serviceSubtaskPreachform); continue; } //3.不是最后一个,获取到下一个执行方式(因为都是在今天执行,那就直接发出去就完了) sfHandlle(serviceSubtask); } } } /** * 设置失败任务默认值,并将失败任务重新置为成功 *

* *@param failDay (失败天数:距离当前日期失败天数) * * @return */ public void setFailSubTaskDefalutVal() { String failDay = configService.selectConfigByKey("hosp.default.fail.day"); if (StringUtils.isNotEmpty(failDay)) iServiceSubtaskService.setFailSubTaskDefalutVal(Long.valueOf(failDay)); } /** * 判断日期是否是今天 * * @param date * @return */ private boolean isDateToday(Date date) { if (date == null) { return false; } // 将 Date 转为 LocalDate LocalDate localDate = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); // 获取今天的 LocalDate LocalDate today = LocalDate.now(); // 如果日期早于或等于今天,返回 true return !localDate.isAfter(today); } /** * 随访-补偿处理 * * @param serviceSubtask */ private void sfHandlle(ServiceSubtask serviceSubtask) { //这里需要根据身份证去重(给微信小程序的固定任务用的) List serviceSubtaskListFilter = new ArrayList<>(); ServiceTask ivrTask1 = serviceTaskMapper.selectServiceTaskByTaskid(serviceSubtask.getTaskid()); ServiceSubtaskPreachform serviceSubtaskPreachform = new ServiceSubtaskPreachform(); serviceSubtaskPreachform.setTaskid(serviceSubtask.getTaskid()); serviceSubtaskPreachform.setSubid(serviceSubtask.getId()); //获取发送方式 String sendPreachform = getSendPreachform(serviceSubtaskPreachform, serviceSubtask); log.info("sendPreachform的发送方式是啥:{}", sendPreachform); if (sendPreachform.equals("-1") || serviceSubtask.getSendstate() == 6 || serviceSubtask.getSendstate() == 5 || serviceSubtask.getSendstate() == 4) { //说明已经全部遍历完了,当前的执行方式序号是最后一个了 或者 该患者已经随访结束(成功或失败了或不执行) log.error("说明已经全部遍历完了,当前的执行方式序号是最后一个了 或者 该患者已经随访结束(成功或失败了或不执行)"); return; } //先判断患者是不是还要发送 PatArchive patArchive = patArchiveService.selectPatArchiveByPatid(serviceSubtask.getPatid()); log.info("patArchive的值为:{}", patArchive); if (!Objects.isNull(patArchive) && patArchive.getNotrequiredFlag() == "1") { //不需要发送 serviceSubtask.setResult("患者不需要随访"); serviceSubtask.setRemark(StringUtils.isNotEmpty(serviceSubtask.getRemark()) ? serviceSubtask.getRemark() + "," + "患者不需要随访," + patArchive.getNotrequiredreason() : "患者不需要随访," + patArchive.getNotrequiredreason()); serviceSubtask.setSendstate(4L); iServiceSubtaskService.insertServiceSubtask(serviceSubtask); setSuccessPreachForm(serviceSubtask, "-1", "不需要执行", "6"); return; } String descByCode = ServiceFromEnum.getDescByCode(Integer.valueOf(sendPreachform)); // ⚠️ 2025 修复(宣教开关失效):type == 3 是**宣教/通知**(纯发送、无作答 —— // 见 setSuccessPreachForm 里「宣教或通知…直接改成 9」、以及本方法后面 // 「宣教或通知,发送成功则直接改成 6L」的判定),整条派发的所有通道 // (电话/短信/公众号/人工)发的都是宣教内容。关掉 publicity.enabled 时应**一个通道都不发**。 // // ⚠️ 刻意**不推进 subtask 状态、也不记失败**(与 RedisMqReceiver 同口径): // 关闭是"暂时不发",不是"这个患者不需要宣教"。标成失败/不需要会让这条宣教 // 在开关恢复后永远丢失。保持 sendstate 不动,开关打开后下一次 longTaskSend/compensateTask // 会照常把它捞出来发出。 if ("3".equals(serviceSubtask.getType()) && !publicityFacade.isEnabled()) { log.warn("【宣教】模块未启用(参数配置 publicity.enabled=false),跳过宣教发送。subtaskId={}, patid={}", serviceSubtask.getId(), serviceSubtask.getPatid()); return; } if (descByCode.equals("电话")) { List list = new ArrayList<>(); ServiceTask ivrTask = serviceTaskMapper.selectServiceTaskByTaskid(serviceSubtask.getTaskid()); if (ivrTask.getSendState() != null && ivrTask.getSendState() == 3 || ivrTask.getSendState() != null && ivrTask.getSendState() == 4) { //如何任务被"暂停"或"终止" return; } if (org.apache.commons.lang3.StringUtils.isEmpty(serviceSubtask.getPhone())) { setSuccessPreachForm(serviceSubtask, sendPreachform, "手机号为空", "4"); return; } boolean dateToday = isDateToday(serviceSubtask.getVisitTime()); //说明是立即发送(先把消息放到cache-0中, displayNo不知道怎么配置) if (dateToday) { list.add(serviceSubtask.getId().toString()); //在子任务表里记录一下 setSuccessPreachForm(serviceSubtask, sendPreachform, "长期任务,电话进入队列成功", "2"); //任务发送记录 ServiceSubtaskRecord serviceSubtaskRecord = new ServiceSubtaskRecord(); serviceSubtaskRecord.setTaskid(serviceSubtask.getTaskid().toString()); serviceSubtaskRecord.setSubtaskId(serviceSubtask.getId()); serviceSubtaskRecord.setUuid(UUID.randomUUID().toString()); serviceSubtaskRecord.setTasktype(serviceSubtask.getType()); serviceSubtaskRecord.setPreachform("3"); serviceSubtaskRecord.setStartTime(System.currentTimeMillis()); serviceSubtaskRecord.setRemark("长期任务,电话进入队列成功"); serviceSubtaskRecordService.insertServiceSubtaskRecord(serviceSubtaskRecord); } //将要执行的放到缓存中 if (list.size() > 0) { redisCache.setCacheListLeftAndDistinct("cache-0", list); log.info("【sfHandlle】电话任务已加入队列,数量:{}", list.size()); } } else if (descByCode.equals("微信小程序")) { //微信小程序 if (ObjectUtils.isNotEmpty(ivrTask1) && ivrTask1.getPatCycle() == 1) { //说明该任务的患者是循环执行的 //判断患者过滤集合中,是否存在当前执行患者的身份证 boolean continutFlag = false; for (ServiceSubtask ss : serviceSubtaskListFilter) { if (ss.getSfzh().equals(serviceSubtask.getSfzh())) { continutFlag = true; } } //如果存在当前执行患者的身份证,则不执行该患者了 if (continutFlag) { return; } //将子任务ID删除 、sendstate改成3(循环任务就是这样) serviceSubtask.setSendstate(3L); serviceSubtask.setId(null); serviceSubtask.setFinishtime(null); serviceSubtask.setCreateTime(new Date()); serviceSubtask.setUpdateTime(new Date()); serviceSubtaskMapper.insertServiceSubtask(serviceSubtask); //将当前要执行的患者放到过滤患者集合中 serviceSubtaskListFilter.add(serviceSubtask); } } else if (descByCode.equals("短信")) { RSAPublicKeyExample rsaPublicKeyExample = new RSAPublicKeyExample(); String taskId = rsaPublicKeyExample.encryptedData(ivrTask1.getTaskid().toString(), pub_key); log.info("【sfHandlle】处理短信长期任务,患者ID:{}", serviceSubtask.getPatid()); boolean dateToday = isDateToday(serviceSubtask.getVisitTime()); //记录当前时间 如果失败记录当前时间 LocalDateTime now = LocalDateTime.now(); if (dateToday) { try { String patid = rsaPublicKeyExample.encryptedData(serviceSubtask.getPatid().toString(), pub_key); String subId = rsaPublicKeyExample.encryptedData(serviceSubtask.getId().toString(), pub_key); SendMagParam sendMagParam = new SendMagParam(); sendMagParam.setType("4"); ServiceOutPath serviceOutPath = new ServiceOutPath(); serviceOutPath.setParam1(taskId); serviceOutPath.setParam2(patid); serviceOutPath.setParam3(ivrTask1.getTaskName()); serviceOutPath.setParam6(subId); serviceOutPath.setCreateTime(new Date()); serviceOutPath.setOrgid(serviceSubtask.getOrgid()); iServiceOutPathService.insertServiceOutPath(serviceOutPath); String url = ""; String format = String.format("%03X", serviceOutPath.getId()); //先判断一下发的是不是宣教 log.info("serviceSubtask.getType的值为:{}", serviceSubtask.getType()); if (!"3".equals(serviceSubtask.getType())) { sendMagParam.setPhone(serviceSubtask.getPhone()); url = StringUtils.isNotEmpty(req_path) ? localIP + ":" + req_path + "/wt?p=" + format : localIP + "/wt?p=" + format; sendMagParam.setUrl(url); //如果type是语音随访的话(说明补偿发送方式中有电话随访的方式,这里的外链就地址只能用/sf) if (serviceSubtask.getType().equals("1")) url = StringUtils.isNotEmpty(req_path) ? localIP + ":" + req_path + "/sf?p=" + format : localIP + "/sf?p=" + format; sendMagParam.setUrl(url); // SysConfig sysConfig = sysConfigMapper.checkConfigKeyUnique("wj.sms.template"); SysConfig sysConfig = new SysConfig(); if (StringUtils.isNotEmpty(serviceSubtask.getServiceType()) && serviceSubtask.getServiceType().equals("6")) { sysConfig = sysConfigMapper.checkConfigKeyUnique("zymyd.sms.template"); } else if (StringUtils.isNotEmpty(serviceSubtask.getServiceType()) && serviceSubtask.getServiceType().equals("14")) { sysConfig = sysConfigMapper.checkConfigKeyUnique("mzmyd.sms.template"); } else { sysConfig = sysConfigMapper.checkConfigKeyUnique("wj.sms.template"); } log.info("sysConfig的值为:{}", sysConfig); if (!Objects.isNull(sysConfig)) { String smsContemt = sysConfig.getConfigValue(); // sendMagParam.setContent("您好,邀请您填写出院调查表,请点击" + sendMagParam.getUrl() + "填写。感谢您配合!"); sendMagParam.setContent(smsContemt.replace("${wjurl}", sendMagParam.getUrl())); } } else { // ⚠️ 这里**刻意不**为 TemplateModuleDisabledException 做降级: // 模板模块关闭(sys_config: template.enabled=false)时,宣教内容确实取不到, // 短信也就确实发不出去。外层 try-catch 会如实记为"短信发送失败", // 并通过 setSuccessPreachForm(..., "5") 按既有机制升级到下一个通道(公众号/人工)。 // 若在这里硬记成"发送成功",等于用一条假记录掩盖"没发出去", // 事后从任务记录上完全看不出问题 —— 那是更坏的降级。 HeLibrary heLibrary = templateFacade.findHeLibraryById(serviceSubtask.getLibtemplateid()); if (heLibrary.getHetype().equals("1")) { sendMagParam.setPhone(serviceSubtask.getPhone()); url = StringUtils.isNotEmpty(req_path) ? localIP + ":" + req_path + "/xj?p=" + format : localIP + "/xj?p=" + format; sendMagParam.setUrl(url); SysConfig sysConfig = sysConfigMapper.checkConfigKeyUnique("xj.sms.template"); if (!Objects.isNull(sysConfig)) { String smsContemt = sysConfig.getConfigValue(); // sendMagParam.setContent("您好,邀请您填写出院宣教调查表,请点击" + sendMagParam.getUrl() + "查看。感谢您配合!"); sendMagParam.setContent(smsContemt.replace("${xjurl}", sendMagParam.getUrl())); } } else if (heLibrary.getHetype().equals("2")) { //通知 sendMagParam.setPhone(serviceSubtask.getPhone()); // 替换文本内容中的变量 String content = getObject(serviceSubtask, heLibrary.getPreachcontent()); sendMagParam.setContent(content); } } serviceOutPath.setRadix(format); serviceOutPath.setUpdateTime(new Date()); serviceOutPath.setUrl(url); iServiceOutPathService.updateServiceOutPath(serviceOutPath); //短信发送:各站点对接方式差异较大(报文/签名/token/返回判定都不同), //已收口到 ruoyi-message 模块,这里只负责组装入参并路由 log.info("active的值为:{}", active); SendResult smsSendResult = sendSmsByActive(serviceSubtask, sendMagParam); log.info("smsSendResult的值为:{}", smsSendResult); String isSuccess = smsSendResult.asLegacyFlag(); //任务发送记录 ServiceSubtaskRecord serviceSubtaskRecord = new ServiceSubtaskRecord(); serviceSubtaskRecord.setTaskid(serviceSubtask.getTaskid().toString()); serviceSubtaskRecord.setSubtaskId(serviceSubtask.getId()); serviceSubtaskRecord.setUuid(UUID.randomUUID().toString()); serviceSubtaskRecord.setTasktype(serviceSubtask.getType()); serviceSubtaskRecord.setPreachform("4"); serviceSubtaskRecord.setStartTime(System.currentTimeMillis()); serviceSubtaskRecord.setResult("success"); if (StringUtils.isNotEmpty(isSuccess) && !isSuccess.equals("true")) serviceSubtaskRecord.setResult("error"); serviceSubtaskRecordService.insertServiceSubtaskRecord(serviceSubtaskRecord); //发送完成后,修改状态 log.info("serviceSubtaskRecord的值为:{}", serviceSubtaskRecord); serviceSubtask.setSendstate(3L); if (StringUtils.isNotEmpty(isSuccess) && isSuccess.equals("true")) { serviceSubtask.setResult("success"); serviceSubtask.setRemark(StringUtils.isNotEmpty(serviceSubtask.getRemark()) ? serviceSubtask.getRemark() + ",短信发送成功" : "短信发送成功"); setSuccessPreachForm(serviceSubtask, sendPreachform, "短信发送成功", "2"); } else { serviceSubtask.setResult("error"); serviceSubtask.setRemark(StringUtils.isNotEmpty(serviceSubtask.getRemark()) ? serviceSubtask.getRemark() + ",短信发送失败" + now : "短信发送失败" + now); setSuccessPreachForm(serviceSubtask, sendPreachform, "短信发送失败" + now, "5"); } } catch (Exception e) { String guid = UUID.randomUUID().toString(); log.error("【sfHandlle】长期任务短信发送失败,原因:{},GUID:{}, 时间: {}", e.getMessage(), guid, now); setSuccessPreachForm(serviceSubtask, sendPreachform, "长期任务短信发送失败:系统错误, 时间: " + now, "5"); } } } else if (descByCode.equals("公众号")) { log.info("【sfHandlle】处理公众号任务,任务信息:{}", ivrTask1); //公众号 RSAPublicKeyExample rsaPublicKeyExample = new RSAPublicKeyExample(); String taskId = rsaPublicKeyExample.encryptedData(ivrTask1.getTaskid().toString(), pub_key); boolean dateToday = isDateToday(serviceSubtask.getVisitTime()); if (dateToday) { try { String patid = rsaPublicKeyExample.encryptedData(serviceSubtask.getPatid().toString(), pub_key); String subId = rsaPublicKeyExample.encryptedData(serviceSubtask.getId().toString(), pub_key); String url = null; url = localIP + ":" + req_path + "/outsideChainwt?param1=" + taskId + "¶m2=" + patid + "¶m3=" + URLEncoder.encode(ivrTask1.getTaskName(), StandardCharsets.UTF_8.toString()) + "¶m5=false" + "$param6=" + subId; ServiceOutPath serviceOutPath = new ServiceOutPath(); serviceOutPath.setParam1(taskId); serviceOutPath.setParam2(patid); serviceOutPath.setParam3(ivrTask1.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); //微信公众号发送:市一与新华是两套完全不同的网关与判定标准, //机构配置校验、HIS患者ID校验都已收口到 ruoyi-message 的发送方实现里 SendResult wechatSendResult = sendWechatByActive(serviceSubtask, patArchive, url); Boolean wxCodeSuccess = wechatSendResult.isOk(); //任务发送记录 ServiceSubtaskRecord serviceSubtaskRecord = new ServiceSubtaskRecord(); serviceSubtaskRecord.setTaskid(serviceSubtask.getTaskid().toString()); serviceSubtaskRecord.setUuid(UUID.randomUUID().toString()); serviceSubtaskRecord.setTasktype(serviceSubtask.getType()); serviceSubtaskRecord.setPreachform("4"); serviceSubtaskRecord.setStartTime(System.currentTimeMillis()); serviceSubtaskRecord.setResult("success"); if (!wxCodeSuccess) serviceSubtaskRecord.setResult("error"); serviceSubtaskRecordService.insertServiceSubtaskRecord(serviceSubtaskRecord); //发送完成后,修改状态 serviceSubtask.setSendstate(3L); if (wxCodeSuccess) { serviceSubtask.setResult("success"); setSuccessPreachForm(serviceSubtask, sendPreachform, "【公众号】公众号发送成功", "2"); } else { log.error("【sfHandlle】公众号发送失败,患者信息:{},错误信息:{}", serviceSubtask, wechatSendResult.getMessage()); setSuccessPreachForm(serviceSubtask, sendPreachform, "【公众号】公众号发送失败", "5"); } } catch (Exception e) { String guid = UUID.randomUUID().toString(); log.error("【sfHandlle】长期任务公众号发送失败,原因:{},GUID:{}", e.getMessage(), guid); setSuccessPreachForm(serviceSubtask, sendPreachform, "【公众号】长期任务公众号发送失败,原因:" + e.getMessage(), "5"); } } } else if (descByCode.equals("人工")) { setSuccessPreachForm(serviceSubtask, sendPreachform, "人工发送成功", "2"); } } /** * 按 {@code active}(站点)路由到对应的短信发送方。 *

* 各站点差异见 {@code ruoyi-message} 模块各实现类;本方法只做「入参组装 + 路由 + 日志」。 *

* ⚠️ 新华({@code xh})站点仍走「旧实现(发送空报文)」,与原 {@code RyTask} 行为一字不差, * 确认网关报文格式后应改为 {@code XinHuaSmsSender#sendForm}。详见该类的 TODO 说明。 */ private SendResult sendSmsByActive(ServiceSubtask serviceSubtask, SendMagParam sendMagParam) { SmsSendRequest request = SmsSendRequest.of(sendMagParam.getPhone(), sendMagParam.getContent(), serviceSubtask.getOrgid()); //市一区分湖滨/吴山两套短信签名 if ("hzszlyy".equals(active)) { request.setCampus(resolveShiyiCampus(serviceSubtask)); } if ("xh".equals(active)) { XinHuaSmsSender xinHuaSmsSender = (XinHuaSmsSender) smsSenderFactory.get(active); return xinHuaSmsSender.sendJsonLegacy(request); } return smsSenderFactory.send(active, request); } /** * 按 {@code active}(站点)路由到对应的微信公众号发送方。 *

* ⚠️ 新华公众号必须传患者信息(姓名 + HIS 患者ID),与原 * {@code getWXCode(...)} 的入参保持一一对应。 */ private SendResult sendWechatByActive(ServiceSubtask serviceSubtask, PatArchive patArchive, String url) { WechatSendRequest request = new WechatSendRequest(); request.setOrgid(patArchive.getOrgid()); request.setIdCard(serviceSubtask.getSfzh()); request.setUrl(url); request.setTitle(serviceSubtask.getTaskName()); request.setContent(serviceSubtask.getTaskDesc()); request.setPhone(patArchive.getTelcode()); request.setPatientName(serviceSubtask.getSendname()); request.setPatidHis(patArchive.getPatidHis()); return wechatSenderFactory.send(active, request); } /** * 查询患者所在院区(1=湖滨,其他=吴山)。 *

* TODO 待确认:原实现并未把 {@code sysDeptService.selectDeptList(...)} 的返回值赋给 * {@code sysDepts},因此 {@code sysDepts} 恒为空列表、{@code campusid} 恒为 {@code "2"}, * 湖滨院区实际上永远走不到 {@code sendSms1}。本次重构保持该行为不变, * 请在业务确认后再决定是否修成 {@code sysDepts = sysDeptService.selectDeptList(...)}。 */ private String resolveShiyiCampus(ServiceSubtask serviceSubtask) { SysDept sysDeptVo = new SysDept(); List sysDepts = new ArrayList<>(); if (ObjectUtils.isNotEmpty(serviceSubtask.getDeptcode())) { sysDeptVo.setDeptType("1"); sysDeptVo.setOrgid(serviceSubtask.getOrgid()); sysDeptVo.setDeptCode(serviceSubtask.getDeptcode()); sysDeptService.selectDeptList(sysDeptVo); } else if (ObjectUtils.isNotEmpty(serviceSubtask.getLeavehospitaldistrictcode())) { sysDeptVo.setDeptType("2"); sysDeptVo.setOrgid(serviceSubtask.getOrgid()); sysDeptVo.setDeptCode(serviceSubtask.getLeavehospitaldistrictcode()); sysDeptService.selectDeptList(sysDeptVo); } SysDept sysDept = CollectionUtils.isNotEmpty(sysDepts) ? sysDepts.get(0) : new SysDept(); return ObjectUtils.isNotEmpty(sysDept) ? sysDept.getCampusid() : "2"; } private Date getNextVisitTime(Long subid, Long taskid, Date visitTime) { ServiceSubtaskPreachform serviceSubtaskPreachform = new ServiceSubtaskPreachform(); serviceSubtaskPreachform.setSubid(subid); serviceSubtaskPreachform.setTaskid(taskid); List serviceSubtaskPreachforms = serviceSubtaskPreachformMapper.selectServiceSubtaskPreachformList(serviceSubtaskPreachform); if (CollectionUtils.isNotEmpty(serviceSubtaskPreachforms)) { String compensateTime = serviceSubtaskPreachforms.get(0).getCompensateTime(); int hoursToAdd = Integer.valueOf(compensateTime) * 24; Calendar calendar = Calendar.getInstance(); calendar.setTime(visitTime); calendar.add(Calendar.HOUR_OF_DAY, hoursToAdd); Date newDate = calendar.getTime(); return newDate; } return visitTime; } private String getSendPreachform(ServiceSubtaskPreachform serviceSubtaskPreachform, ServiceSubtask serviceSubtask) { List serviceSubtaskPreachforms = serviceSubtaskPreachformMapper.selectServiceSubtaskPreachformList(serviceSubtaskPreachform); if (CollectionUtils.isEmpty(serviceSubtaskPreachforms)) { //如果serviceSubtaskPreachforms为空的话,则需要进行新增一下 List serviceSubtaskPreachformList = addServiceSubtaskPreachform(serviceSubtask, "1"); if (CollectionUtils.isEmpty(serviceSubtaskPreachformList)) { log.error("ryTask--发送方式失败了serviceSubtask的入参为:{}", serviceSubtask); return null; } serviceSubtaskPreachforms = serviceSubtaskPreachformList; } // 情况1:currentPreachform为空,取sort为1的preachform if (StringUtils.isEmpty(serviceSubtask.getCurrentPreachform())) { String firstPreachform = serviceSubtaskPreachforms.stream().filter(item -> item.getSort() == 1L).map(ServiceSubtaskPreachform::getPreachform).findFirst().orElse(null); //更新一下当前发送时间 if (StringUtils.isNotEmpty(firstPreachform)) { serviceSubtaskPreachform.setPreachform(firstPreachform); serviceSubtaskPreachform.setCurrentSendTime(new Date()); serviceSubtaskPreachformMapper.updateSSPByCondition(serviceSubtaskPreachform); } return firstPreachform; } // 情况2:currentPreachform不为空,找下一个sort Optional currentSort = serviceSubtaskPreachforms.stream().filter(item -> serviceSubtask.getCurrentPreachform().equals(item.getPreachform())).map(ServiceSubtaskPreachform::getSort).findFirst(); if (currentSort.isPresent()) { // 检查是否所有sendstate不等于9 boolean allSendStateNot1AndNot9 = serviceSubtaskPreachforms.stream().allMatch(item -> !item.getSendstate().equals("9")); if (!allSendStateNot1AndNot9) { //说明,在某种方式发送失败后,又成功了(例如短信或公众号,在规定时间外患者做完了,这个时候,状态改成了9) serviceSubtask.setSendstate(6L); serviceSubtaskMapper.updateServiceSubtask(serviceSubtask); return "-1"; } // 当sort等于列表size时,并且所有sendstate都不等于9,则返回-1 if (currentSort.get().equals((long) serviceSubtaskPreachforms.size()) && isSubtaskDeadline(serviceSubtask, serviceSubtaskPreachforms)) { if (allSendStateNot1AndNot9) { //说明全发送失败了 serviceSubtask.setSendstate(7L); serviceSubtask.setFinishtime(null); log.error("getSendPreachform方法 检查是否所有sendstate不等于9, 全失败了"); serviceSubtask.setRemark(StringUtils.isNotEmpty(serviceSubtask.getRemark()) ? serviceSubtask.getRemark() + "," + "所有执行方式均未获取结果,任务失败(超时)" : "所有执行方式均未获取结果,任务失败(超时)"); serviceSubtaskMapper.updateServiceSubtask(serviceSubtask); //修改发送状态 setFail(serviceSubtaskPreachforms, serviceSubtask); } return "-1"; } // 当sort等于列表size时,下一个sort为-1 Long nextSort = currentSort.get() + 1; //如果当前的发送方式不为空,并且开始准备要用下一种方式发送了,那当前的肯定没有发送成功,需要设置失败 if (StringUtils.isNotEmpty(serviceSubtask.getCurrentPreachform())) { setFail(serviceSubtaskPreachforms, serviceSubtask); } // 找到下一个sort对应的preachform String nextPreachform = serviceSubtaskPreachforms.stream().filter(item -> nextSort.equals(item.getSort())).map(ServiceSubtaskPreachform::getPreachform).findFirst().orElse("-1"); //更新一下当前发送时间 if (StringUtils.isNotEmpty(nextPreachform)) { serviceSubtaskPreachform.setCurrentSendTime(new Date()); serviceSubtaskPreachform.setPreachform(nextPreachform); serviceSubtaskPreachformMapper.updateSSPByCondition(serviceSubtaskPreachform); } return nextPreachform; } return null; } private List addServiceSubtaskPreachform(ServiceSubtask serviceSubtask, String sendstate) { //将preachformDesc的json对象,放到list集合中 ServiceTask serviceTask = serviceTaskMapper.selectServiceTaskByTaskid(serviceSubtask.getTaskid()); String preachformDesc = serviceTask.getPreachformDesc(); List sspl = null; ObjectMapper objectMapper = new ObjectMapper(); if (org.apache.commons.lang3.StringUtils.isNotEmpty(preachformDesc)) { try { sspl = objectMapper.readValue(preachformDesc, new TypeReference>() { }); } catch (JsonProcessingException e) { log.error("ryTask--preachform转List报错了:{}", e.getMessage()); } for (ServiceSubtaskPreachform serviceSubtaskPreachform : sspl) { serviceSubtaskPreachform.setTaskid(serviceSubtask.getTaskid()); serviceSubtaskPreachform.setSubid(serviceSubtask.getId()); //这个sendstate需要在数据库设置一个默认值为“1” serviceSubtaskPreachform.setSendstate("1"); if (StringUtils.isNotEmpty(sendstate)) serviceSubtaskPreachform.setSendstate(sendstate); serviceSubtaskPreachform.setOrgid(serviceSubtask.getOrgid()); serviceSubtaskPreachformMapper.insertServiceSubtaskPreachform(serviceSubtaskPreachform); } } return sspl; } private Integer setFail(List serviceSubtaskPreachforms, ServiceSubtask serviceSubtask) { Optional id = serviceSubtaskPreachforms.stream().filter(item -> serviceSubtask.getCurrentPreachform().equals(item.getPreachform())).map(ServiceSubtaskPreachform::getId).findFirst(); if (id.isPresent()) { ServiceSubtaskPreachform subtaskPreachform = new ServiceSubtaskPreachform(); subtaskPreachform.setSendstate("5"); subtaskPreachform.setId(id.get()); subtaskPreachform.setRemark("当前发送方式在规定时间内未成功获取结果"); return serviceSubtaskPreachformMapper.updateServiceSubtaskPreachform(subtaskPreachform); } return 0; } // --------------------------------------------------------------------- // 说明:原 getDXCode / getWXCode / encode 三个私有方法已随发送方一起收口到 // ruoyi-message 模块(LishuiSmsSender / XinHuaMpApi),此处已删除,避免重复实现。 // --------------------------------------------------------------------- public void collectHIS() { try { // HIS数据采集 log.info("【dealHisData】HIS开始采集数据"); ichService.sync(); log.info("【dealHisData】HIS结束采集数据"); } catch (Exception e) { log.error("【dealHisData】HIS数据采集异常", e); } } public void collectHISYesterday() { try { // 获取昨天0点到今天0点的时间范围 LocalDateTime todayZero = LocalDateTime.now().with(LocalTime.MIN); LocalDateTime yesterdayZero = todayZero.minusDays(1); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); String startTime = yesterdayZero.format(formatter); String endTime = todayZero.format(formatter); // HIS数据采集 log.info("【collectHISYesterday】HIS开始采集数据"); // 立即返回,后台异步执行 CompletableFuture.runAsync(() -> { ichService.syncMedInhosp(startTime, endTime); }); log.info("【collectHISYesterday】HIS结束采集数据"); } catch (Exception e) { log.error("【collectHISYesterday】HIS数据采集异常", e); } } public void collectHISDaysBefore(Long daysBefore) { try { // 获取daysBefore 0点到daysBefore-1 0点的时间范围 LocalDateTime todayZero = LocalDateTime.now().with(LocalTime.MIN); LocalDateTime daysBeforeZeroStart = todayZero.minusDays(daysBefore); LocalDateTime daysBeforeZeroEnd = todayZero.minusDays(daysBefore - 1L); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); String startTime = daysBeforeZeroStart.format(formatter); String endTime = daysBeforeZeroEnd.format(formatter); // HIS数据采集 log.info("【collectHISDaysBefore】HIS开始采集数据: startTime:{}, endtime:{}", startTime, endTime); // 立即返回,后台异步执行 CompletableFuture.runAsync(() -> { ichService.syncMedInhosp(startTime, endTime); }); log.info("【collectHISDaysBefore】HIS结束采集数据: startTime:{}, endtime:{}", startTime, endTime); } catch (Exception e) { log.error("【collectHISDaysBefore】HIS数据采集异常", e); } } public Boolean setSuccessPreachForm(ServiceSubtask serviceSubtask, String preachform, String remark, String failSendstate) { log.info("setSuccessPreachForm的入参为:serviceSubtask = {}, preachform = {}, remark = {}, failSendstate = {}", serviceSubtask, preachform, remark, failSendstate); if (preachform.equals("-1")) { //说明都不需要执行了 ServiceSubtaskPreachform serviceSubtaskPreachform = new ServiceSubtaskPreachform(); serviceSubtaskPreachform.setSubid(serviceSubtask.getId()); serviceSubtaskPreachform.setTaskid(serviceSubtask.getTaskid()); serviceSubtaskPreachform.setOrgid(serviceSubtask.getOrgid()); serviceSubtaskPreachform.setSendstate(failSendstate); serviceSubtaskPreachform.setCurrentSendTime(new Date()); serviceSubtaskPreachformMapper.updateSSPByCondition(serviceSubtaskPreachform); return true; } ServiceSubtaskPreachform serviceSubtaskPreachform = new ServiceSubtaskPreachform(); serviceSubtaskPreachform.setSubid(serviceSubtask.getId()); serviceSubtaskPreachform.setTaskid(serviceSubtask.getTaskid()); serviceSubtaskPreachform.setOrgid(serviceSubtask.getOrgid()); List sspCount = serviceSubtaskPreachformMapper.selectServiceSubtaskPreachformList(serviceSubtaskPreachform); log.info("sspCount的值为:{}", sspCount); serviceSubtaskPreachform.setPreachform(preachform); //1:未发送状态 serviceSubtaskPreachform.setSendstate("1"); //电话的未发送状态,需要查2,因为,数据可能卡在队列里了; (没进队列,电话状态还是1) if (preachform.equals("3") && StringUtils.isNotEmpty(failSendstate) && Long.valueOf(failSendstate) > 2) serviceSubtaskPreachform.setSendstate("2"); // //人工的未发送状态,需要查2 // if (preachform.equals("1") && StringUtils.isNotEmpty(failSendstate) && Long.valueOf(failSendstate) > 2) // serviceSubtaskPreachform.setSendstate("2"); log.info("-----serviceSubtaskPreachform的入参是?{}", serviceSubtaskPreachform); List serviceSubtaskPreachforms = serviceSubtaskPreachformMapper.selectServiceSubtaskPreachformList(serviceSubtaskPreachform); if (CollectionUtils.isNotEmpty(serviceSubtaskPreachforms)) { ServiceSubtaskPreachform serviceSubtaskPreachform1 = serviceSubtaskPreachforms.get(0); serviceSubtaskPreachform1.setSendstate(failSendstate); if (serviceSubtask.getType().equals("3") || serviceSubtask.getType().equals("4")) { //如果是宣教或通知,并且发送成功,直接将发送状态改成9 if (failSendstate.equals("2")) serviceSubtaskPreachform1.setSendstate("9"); } serviceSubtaskPreachform1.setRemark(remark); serviceSubtaskPreachformMapper.updateServiceSubtaskPreachform(serviceSubtaskPreachform1); log.info("---------serviceSubtaskPreachform1的入参是 : {}", serviceSubtaskPreachform1); //如果当前的preachform已经是最后一个了,那直接将serviceSubtask的sendstate状态改成7就行了(全超时了) if (serviceSubtaskPreachform1.getSort() == sspCount.size() && isSubtaskDeadline(serviceSubtask, serviceSubtaskPreachforms)) { if (failSendstate.equals("4") || failSendstate.equals("5")) { serviceSubtask.setCurrentPreachform(preachform); serviceSubtask.setSendstate(7L); log.error("setSuccessPreachForm方法,当前的preachform已经是最后一个了,全部执行失败"); serviceSubtask.setRemark(StringUtils.isNotEmpty(serviceSubtask.getRemark()) ? serviceSubtask.getRemark() + "," + "所有发送方式均未获取结果,任务失败(超时)" : "所有发送方式均未获取结果,任务失败(超时)"); //serviceSubtask.setRemark("setSuccessPreachForm方法,当前的preachform已经是最后一个了,全部执行失败"); serviceSubtaskMapper.updateServiceSubtask(serviceSubtask); return true; } } } else { log.error("【定时任务中该患者没有查询到属于他的发送方式,subid:{},prechform:{},orgid:{}】", serviceSubtask.getId(), preachform, serviceSubtask.getOrgid()); return false; } log.info("到这里了不????"); //将下一次的随访时间与当前执行方式记录一下 serviceSubtask.setVisitTime(getNextVisitTime(serviceSubtask.getId(), serviceSubtask.getTaskid(), serviceSubtask.getVisitTime())); serviceSubtask.setCurrentPreachform(preachform); serviceSubtask.setSendstate(3L); if (serviceSubtask.getType().equals("3") || serviceSubtask.getType().equals("4")) { //如果是宣教或通知,并且发送成功,则直接将serviceSubtask状态改成6L(这个患者是没有题做的,不会再触发接口进行状态修改) if (failSendstate.equals("2")) { serviceSubtask.setSendstate(6L); } } serviceSubtask.setRemark(StringUtils.isNotEmpty(serviceSubtask.getRemark()) ? serviceSubtask.getRemark() + "," + remark : remark); log.info("serviceSubtask的入参是:{}",serviceSubtask); serviceSubtaskMapper.updateServiceSubtask(serviceSubtask); return true; } /** * 通配符变量替换 * * @param serviceSubtask * @param scriptContent * @return */ private String getObject(ServiceSubtask serviceSubtask, String scriptContent) { Map> param = getParam(serviceSubtask.getTaskid()); for (Map map : param.values()) { for (String key : map.keySet()) { scriptContent = scriptContent.replace(key, StringUtils.isNotEmpty(map.get(key)) ? map.get(key) : ""); } } if (StringUtils.isEmpty(scriptContent)) { return scriptContent; } scriptContent = scriptContent.replace("${name}", StringUtils.isNotEmpty(serviceSubtask.getSendname()) ? serviceSubtask.getSendname() : ""); scriptContent = scriptContent.replace("${dzz}", StringUtils.isNotEmpty(serviceSubtask.getAddr()) ? serviceSubtask.getAddr() : ""); scriptContent = scriptContent.replace("${phone}", StringUtils.isNotEmpty(serviceSubtask.getTelcode()) ? serviceSubtask.getTelcode() : ""); return scriptContent; } /** * 获取任务里的通配符 * * @param taskId * @return */ private Map> getParam(Long taskId) { ServiceTask serviceTask = serviceTaskMapper.selectServiceTaskByTaskid(taskId); ObjectMapper objectMapper = new ObjectMapper(); Map> serviceTaskMap = null; try { serviceTaskMap = objectMapper.readValue(serviceTask.getTextParam(), Map.class); } catch (JsonProcessingException e) { e.printStackTrace(); } return serviceTaskMap; } public int qwenLLMTest(String questionText, String voiceText, String value, String regexText) { int result = QwenLLMUtil.matchRegex(questionText, voiceText, value, regexText); return result; } /** * 任务组(appltype=5)首轮纳入:扫描新出院患者,按 schedule 绑定的维度匹配任务组, * 为命中的患者按计划明细一次性生成第 0 轮 N 条 subtask,并置 schedulecheck_flag=1。 * 后续轮次由 {@link #rollTaskGroupCycle()} 负责。 *

* 与主扫描 dealOutHospInfo 分离,互不干扰。建议每 30 分钟执行一次。 */ public void dealTaskGroupOutHosp() { log.info("【dealTaskGroupOutHosp】开始扫描任务组新出院患者"); if (!port.equals("8095")) { return; } try { String config = configService.selectConfigByKey("visit.early.day"); // ⚠️ 2025 修复(开关失效):原走 smartor 遗留实现,不判开关(同 dealOutHospInfo)。 // 现走模块门面,与 RyTask#dealOutHospInfo 一致。 inhospVisitFacade.dealOutHospInfoBySchedule(config); log.info("【dealTaskGroupOutHosp】任务组首轮纳入扫描完成"); } catch (Exception e) { log.error("【dealTaskGroupOutHosp】任务组扫描异常", e); } } /** * 任务组(appltype=5)循环任务滚动调度。 *

* 扫描所有 schedule_style=1 的计划,若本轮 subtask 全部 finalize(sendstate ∉ 1/2/3), * 则克隆本轮患者到下一轮,推进 current_loop_count 与 visit_time。 *

* 建议每小时或每半小时执行一次,避免轮次积压。 */ public void rollTaskGroupCycle() { log.info("【rollTaskGroupCycle】开始扫描循环任务组"); try { // ⚠️ 2025 修复(开关失效):原走 smartor 的 ServiceTaskScheduleServiceImpl.rollCycleTasks(), // 不判开关 —— 关掉 inhospvisit.enabled 照样「推进轮次 + 新建 subtask」。 // // 现走模块门面,完成模块化方案里第 10 步一直没做的那一步切换: // InhospVisitFacade#rollTaskGroupCycle(判开关) // → DefaultInhospVisitApi#rollTaskGroupCycle // → TaskGroupCycleRoller#rollCycleTasks(逐行搬自原 L170-190) // → ScheduleRollPortBridge(smartor 侧端口实现,116 字段深拷贝等留在实现侧) // 该类的注释明确写了「第 10 步切过来之前不能被挂到任何调度上,否则双跑」—— // 本行就是那个切换点,切完老的 rollCycleTasks 即退役(全仓只有本方法调它)。 int rolled = inhospVisitFacade.rollTaskGroupCycle(); // 门面内部的 TaskGroupCycleRoller 已打「本次推进 N 个计划」,这里只补一句总览 log.info("【rollTaskGroupCycle】本次推进 {} 个计划", rolled); } catch (Exception e) { log.error("【rollTaskGroupCycle】滚动调度异常", e); } } /** * 随访任务是否超时 * * @param serviceSubtask * @param serviceSubtaskPreachforms * @return */ public boolean isSubtaskDeadline(ServiceSubtask serviceSubtask, List serviceSubtaskPreachforms) { if (ObjectUtils.isNotEmpty(serviceSubtask)) { //只有出院随访和专病随访有超时 if (StringUtils.isNotEmpty(serviceSubtask.getServiceType()) && !serviceSubtask.getServiceType().equals("2") && !serviceSubtask.getServiceType().equals("13")) { return true; } } if (Objects.isNull(serviceSubtask) || CollectionUtils.isEmpty(serviceSubtaskPreachforms)) { return false; } // 基准时刻:出院时间 + 发送日(sendDay 缺省按 1),并兜底避免 NPE/脏数据 ServiceTask serviceTask = serviceTaskMapper.selectServiceTaskByTaskid(serviceSubtask.getTaskid()); int sendDay = serviceTask != null && serviceTask.getSendDay() != null ? serviceTask.getSendDay().intValue() : 1; Date deadline = DateUtils.addDays(serviceSubtask.getEndtime(), sendDay); int sumCompensateTime = CollectionUtils.isEmpty(serviceSubtaskPreachforms) ? 0 : serviceSubtaskPreachforms.stream().mapToInt(x -> x.getCompensateTime() == null ? 0 : Integer.parseInt(x.getCompensateTime())).sum(); sumCompensateTime = sumCompensateTime + 1; // 只补偿最后一步自身的补偿时间(天),脏值一律按 0 兜底 deadline = DateUtils.addDays(deadline, sumCompensateTime); // 出院(发送)时间 + 本步补偿时间 小于当前时间 -> 超时 return deadline.getTime() < new Date().getTime(); } /** * 处理超时随访任务(随访超时 → 生成"再次随访") *

* ⚠️ 2025 修复(开关失效):本方法原不判任何模块开关 —— * 关掉 {@code inhospvisit.enabled} 照样建"再次随访"子任务。 *

* 为什么这里是"入口判开关"而不是像 {@link #dealOutHospInfo()} 那样切到门面: * 模块侧只提供了 {@code InhospVisitApi#buildAgainVisitDraft}(组装草稿), * 没有完整的"扫描超时 → 落库 → 回写原任务状态"入口, * 而本方法自己组装并 insert(见下方 {@code insertServiceSubtask})。 * 造一个模块入口属于新增能力、超出"只解决开关"的范围,所以取最小改动:入口守卫。 *

* ⚠️ 全仓只有 {@code ServiceSubtaskController#dealSubtaskDeadline} 一个调用者。 */ public void dealSubtaskDeadline(String orgid, Integer deadlineDay) { // ⚠️ 开关守卫:先判再干活,与 dealOutHospInfo / rollTaskGroupCycle 同口径。 // 关闭时直接返回,不扫描、不建"再次随访"子任务。 if (!inhospVisitFacade.isEnabled()) { log.warn("【dealSubtaskDeadline】模块未启用(参数配置 inhospvisit.enabled=false),跳过超时随访处理。orgid={}", orgid); return; } //获取任务信息 ServiceTask st = new ServiceTask(); st.setDelFlag("0"); st.setLongTask(1); st.setNexttaskflag("1"); st.setOrgid(orgid); List serviceTasks = serviceTaskMapper.selectServiceTaskList(st); for (ServiceTask serviceTask : serviceTasks) { deadlineDay = -deadlineDay; Date deadline = DateUtils.addDays(new Date(), deadlineDay); //通过任务ID拿到患者信息,并且随访时间得是今天之前的 ServiceSubtaskEntity serviceSubtaskVO = new ServiceSubtaskEntity(); serviceSubtaskVO.setOrgid(orgid); serviceSubtaskVO.setSendstate(7L); //只扫描首次随访的 serviceSubtaskVO.setVisitCount(1); serviceSubtaskVO.setTaskid(serviceTask.getTaskid()); serviceSubtaskVO.setEndOutHospTime(deadline); List serviceSubtaskList = serviceSubtaskMapper.selectServiceSubtaskList(serviceSubtaskVO); log.info("【dealSubtaskDeadline】获取到{}个超时任务, taskid={}", serviceSubtaskList.size(), serviceTask.getTaskid()); for (ServiceSubtask serviceSubtask : serviceSubtaskList) { PatMedInhosp patMedInhospVo = new PatMedInhosp(); patMedInhospVo.setOrgid(serviceSubtask.getOrgid()); patMedInhospVo.setInhospid(serviceSubtask.getInhospid()); patMedInhospVo.setCry(1); List patMedInhospList = iPatMedInhospService.selectPatMedInhospList(patMedInhospVo); PatMedInhosp patMedInhosp = new PatMedInhosp(); if (!patMedInhospList.isEmpty()) { patMedInhosp = patMedInhospList.get(0); } else { continue; } if (ObjectUtils.isEmpty(serviceTask.getNexttaskid())) { continue; } ServiceTask nextServiceTask = serviceTaskMapper.selectServiceTaskByTaskid(serviceTask.getNexttaskid()); PatArchive patArchive = patArchiveService.selectPatArchiveByPatid(serviceSubtask.getPatid()); String config = configService.selectConfigByKey("visit.early.day"); Integer type = 1; //新增再次随访任务 ServiceSubtask serviceSubtaskAgain = iPatMedInhospService.boxedServiceSubtask(nextServiceTask, patMedInhosp, patArchive, config, type); serviceSubtaskAgain.setId(null); serviceSubtaskAgain.setUpid(serviceSubtask.getId().intValue()); serviceSubtaskAgain.setVisitCount(serviceSubtask.getVisitCount() + 1); serviceSubtaskAgain.setSendstate(2L); serviceSubtaskAgain.setCreateTime(new Date()); serviceSubtaskAgain.setUpdateTime(new Date()); serviceSubtaskAgain.setVisitType(serviceSubtask.getVisitType()); serviceSubtaskAgain.setVisitDeptCode(serviceSubtask.getVisitDeptCode()); serviceSubtaskAgain.setVisitDeptName(serviceSubtask.getVisitDeptName()); int i = serviceSubtaskMapper.insertServiceSubtask(serviceSubtaskAgain); if (i == 1) { //再次随访任务创建成功后,原任务改为失败状态 serviceSubtask.setSendstate(5L); serviceSubtaskMapper.updateServiceSubtask(serviceSubtask); } } } } }