package com.ruoyi.inhospvisit.service; import com.ruoyi.common.core.service.VisitTaskCreateApi; import com.ruoyi.common.core.service.dto.VisitLongTaskDto; import com.ruoyi.common.core.service.dto.VisitPendingSubtaskDto; import com.ruoyi.common.core.service.dto.VisitSubtaskDraft; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.core.service.dto.VisitArchiveRow; import com.ruoyi.inhospvisit.domain.row.VisitInhospRow; import com.ruoyi.inhospvisit.support.InhospFlagWriter; import com.ruoyi.inhospvisit.support.VisitSubtaskFactory; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.ObjectUtils; import java.util.Date; import java.util.List; /** * 随访子任务「写入口」—— 逐行搬自 {@code PatMedInhospServiceImpl#writeInSubTask}(L682-913,232 行)。 * *

这次搬迁的形态:结构性改写(不是纯搬运)

* 原实现直接用 {@code serviceSubtaskMapper} / {@code patMedInhospMapper} 写库。 * 按你的要求「任务创建将来单独拉一个模块」,本次把三处写库能力换成 * {@link VisitTaskCreateApi} 的三个方法: * * * * * * * * * * *
原调用现在
{@code serviceTaskMapper.selectServiceTaskList(st)}(L689){@link VisitTaskCreateApi#findPublishedLongTask(Long)}
{@code serviceSubtaskMapper.insertServiceSubtask(...)}(L823/838/854/862/873){@link VisitTaskCreateApi#insertSubtask(VisitSubtaskDraft)}
{@code serviceSubtaskMapper.updateServiceSubtask(...)}(L782/791/812/836){@link VisitTaskCreateApi#updatePendingSubtask(VisitPendingSubtaskDto)}
{@code serviceSubtaskMapper.selectServiceSubtaskList(subtask)}(L764){@link VisitTaskCreateApi#findPendingSubtasks(Long, Long)}
* 因为属于结构性改写,逐行比对盖不住,所以本方法配的是 * B 档「口径双向断言」({@code sql/_probe/verify_inhospvisit_writein_semantics.py}), * 逐条断言原实现的口径在新实现里仍然存在。 * *

⚠️ 原实现的 3 处怪癖,全部原样保留(写错就是行为变更)

*
    *
  1. {@code type == 0}(药品宣教)用的是 {@code medcheck_flag}, * 而 catch 块里只有 {@code type==1/2/3/0} 四个分支 —— * type=4/5 抛异常时不会回写任何标志位(原 L900-912)。
  2. *
  3. {@code type == 3} 分支的 {@code updateServiceSubtask} 返回值被丢弃 * (原 L836 没有 {@code Boolean aBoolean = ...}),而 {@code i} 来自后面的 * {@code insertServiceSubtask} —— 所以 type=3 只看 insert 结果,不看停旧结果。
  4. *
  5. {@code serviceSubtask1.getServiceType().equals("2")} 未判 null(原 L803) * —— 待执行子任务的 serviceType 为 null 时会 NPE。原样保留。
  6. *
* *

⚠️ 降级策略

* {@link VisitTaskCreateApi} 是"任务创建模块"的预留接口,本次由 smartor 提供过渡实现。 * 取不到实现时(模块没打进应用 / 容器异常)本类按"不生成子任务"处理并打 WARN, * 绝不抛异常 —— 出入院随访挂了就是当天全院随访停摆。 */ @Slf4j @Service public class InhospSubtaskWriter { /** * 景宁机构ID(原 L798 硬编码;已逐字核对原实现 4 处) */ private static final String ORG_JINGNING = "47255004333112711A1001"; /** * 出院随访的服务类型(再入院停机分支的必要条件,原 L803) */ private static final String SERVICE_TYPE_DISCHARGE = "2"; /** * 待执行状态 */ private static final long SENDSTATE_PENDING = 2L; /** * 停机状态(原实现把 sendstate 置 4 表示不再执行) */ private static final long SENDSTATE_STOPPED = 4L; /** * 疾病维度(type=3)专用:停旧任务后把执行方式全置失败 */ private static final String PREACHFORM_STATE_FAIL = "5"; @Autowired private VisitSubtaskFactory subtaskFactory; @Autowired private InhospFlagWriter flagWriter; /** * {@code addServiceSubtaskPreachform} 的委托目标(写 service_subtask_preachform)。 *

* 该方法(原 L1061-1103,43 行)在第 8 步才搬进本模块, * 但 {@code writeInSubTask} 现在就用到它(L840/855/874)。 * 所以这里先用一个 {@code ObjectProvider} 惰性拿;取不到就跳过并打 WARN。 */ @Autowired private ObjectProvider preachformWriterProvider; @Autowired private ObjectProvider taskCreateApiProvider; /** * 把患者写进随访子任务表 —— 原 {@code writeInSubTask(Long taskid, Boolean check, PatMedInhosp, * PatArchive, Integer type, String config)}。 * * @param taskid 命中的长期任务ID * @param check 是否需要做"再入院"去重校验 * @param inhosp 出入院记录 * @param archive 患者档案 * @param type 0=药品宣教 / 1=离院科室 / 2=离院病区 / 3=疾病 / 4=入院科室 / 5=入院病区 * @param config {@code sys_config: visit.early.day} * @return 是否真的落了库(或"本来就已经有")。 * 返回值是 2026-10"一个患者只生成一条随访任务"改造的判据: * 编排层按维度优先级逐个尝试,只有拿到 {@code true} 才算该维度成功并停止。 * 原实现是 {@code void},编排层用"调用了几次"当成功数 —— 那是错的 * (任务未发布/被去重时同样是"生成了一条")。 */ public boolean writeInSubTask(Long taskid, Boolean check, VisitInhospRow inhosp, VisitArchiveRow archive, Integer type, String config) { String longTaskReason = inhosp.getLongTaskReason(); String orgId = inhosp.getOrgid(); if (StringUtils.isEmpty(longTaskReason)) { longTaskReason = ""; } // 查任务创建模块的方法是否可用 VisitTaskCreateApi taskApi = taskCreateApi(); if (taskApi == null) { log.warn("【出入院随访】VisitTaskCreateApi 不可用(任务创建模块未装配),本患者不生成子任务。" + "inhospid={}, taskid={}", inhosp.getInhospid(), taskid); return false; } VisitLongTaskDto serviceTask = taskApi.findPublishedLongTask(taskid); if (serviceTask == null) { writeMissingTaskFlag(inhosp, type, longTaskReason, taskid); return false; } // 原 L729-731 if ("1".equals(inhosp.getInhospstate()) && inhosp.getEndtime() == null) { return false; } // ------------------------------------------------------------------ // 【改造·2026-10】落库前最后一道判重: // 同一患者 + 同一次出院记录 + 同一任务,只要已经有一条未删除的子任务(含已停用的), // 就不再 insert。原因见 VisitTaskCreateApi#existsSubtask 的注释: // 患者被重复扫到时会走"停旧 + 再 insert 一条 sendstate=4"的分支, // 每跑一轮多一条,最终同一患者几十条。 // ------------------------------------------------------------------ if (taskApi.existsSubtask(archive == null ? null : archive.getId(), inhosp.getInhospid(), taskid)) { log.info("【出入院随访】患者{}出院记录{}任务{}已有子任务,跳过重复生成。", inhosp.getPatid(), inhosp.getInhospid(), taskid); writeFlagByType(inhosp.getInhospid(), type, "1", null); return true; } // serviceSubtask数据组装(副作用由本类实现,见 missingOutHospTime) VisitSubtaskDraft serviceSubtask = subtaskFactory.boxedServiceSubtask(toTaskSource(serviceTask, inhosp), inhosp, archive, config, type, row -> onMissingOutHospTime(row)); if (ObjectUtils.isEmpty(serviceSubtask)) { return false; } // 就诊科室/病区填充 if (type != null && type != 2) { // 科室和疾病 都用 科室填充 if ("1".equals(inhosp.getInhospstate())) { serviceSubtask.setVisitDeptCode(inhosp.getLeaveldeptcode()); serviceSubtask.setVisitDeptName(inhosp.getLeaveldeptname()); } else if ("0".equals(inhosp.getInhospstate())) { serviceSubtask.setVisitDeptCode(inhosp.getDeptcode()); serviceSubtask.setVisitDeptName(inhosp.getDeptname()); if (type == 5) { serviceSubtask.setVisitDeptCode(inhosp.getHospitaldistrictcode()); serviceSubtask.setVisitDeptName(inhosp.getHospitaldistrictname()); } } } else if (type != null && type == 2) { serviceSubtask.setVisitDeptCode(inhosp.getLeavehospitaldistrictcode()); serviceSubtask.setVisitDeptName(inhosp.getLeavehospitaldistrictname()); } // 记录 insertServiceSubtask 的返回状态 int i = 0; try { if (Boolean.TRUE.equals(check) && (serviceSubtask.getTaskSituation() == null || serviceSubtask.getTaskSituation() != 6)) { // 查该患者 + 该 taskid 下所有待执行子任务 List pendingList = taskApi.findPendingSubtasks(archive.getId(), taskid); if (pendingList != null && !pendingList.isEmpty()) { for (VisitPendingSubtaskDto pending : pendingList) { // 不是长期任务,不处理 if (pending.getLongSendTime() == null) { continue; } //type = 1或2 表示 "科室、病区" if (type != null && (type == 1 || type == 2)) { Date endTime = serviceSubtask.getEndtime(); Date endTime1 = pending.getEndtime(); // 原 L776-785:出院时间判空 if (ObjectUtils.isEmpty(endTime)) { serviceSubtask.setResult("error"); serviceSubtask.setRemark("出院时间为空,serviceSubtask的Id为" + serviceSubtask.getId()); serviceSubtask.setUpdateBy(serviceTask.getUpdateBy()); serviceSubtask.setUpdateTime(new Date()); // ⚠️ 这里原实现是 update 一个【尚未 insert】的草稿(id 为 null)。 // 保留该行为:靠 id 为 null 让 update 影响 0 行。 if (writeBackDraft(taskApi, serviceSubtask)) { i = 1; } continue; } if (ObjectUtils.isEmpty(endTime1)) { pending.setResult("error"); pending.setRemark("出院时间为空,serviceSubtask的Id为" + pending.getId()); pending.setUpdateBy(serviceTask.getUpdateBy()); pending.setUpdateTime(new Date()); if (taskApi.updatePendingSubtask(pending)) { i = 1; } continue; } //景宁 出院首次(7天)随访一定要做 boolean firstVisit = ORG_JINGNING.equals(orgId); if (!firstVisit) { // ⚠️ 原实现不判 serviceType 是否为 null(会 NPE),原样保留 if (endTime1.before(endTime) && SERVICE_TYPE_DISCHARGE.equals(pending.getServiceType())) { // 把循环到的旧子任务停掉 pending.setSendstate(SENDSTATE_STOPPED); if (StringUtils.isNotEmpty(serviceSubtask.getRemark())) { serviceSubtask.setRemark(serviceSubtask.getRemark() + ",科室/病区患者再入院"); } else { serviceSubtask.setRemark("科室/病区患者再入院"); } pending.setResult("error"); pending.setUpdateBy(serviceTask.getUpdateBy()); pending.setUpdateTime(new Date()); if (taskApi.updatePendingSubtask(pending)) { i = 1; } } else { //相反,则把"当前的"数据停掉(置 error + sendstate=4 后 insert) serviceSubtask.setResult("error"); serviceSubtask.setSendstate(SENDSTATE_STOPPED); if (StringUtils.isNotEmpty(serviceSubtask.getRemark())) { serviceSubtask.setRemark(serviceSubtask.getRemark() + ",当前患者出院时间后有待执行的出院随访长期任务"); } else { serviceSubtask.setRemark("当前患者出院时间后有待执行的出院随访长期任务"); } serviceSubtask.setCreateBy(serviceTask.getCreateBy()); serviceSubtask.setCreateTime(new Date()); i = insert(taskApi, serviceSubtask) ? 1 : 0; } } } else if (type != null && type == 3) { //type=3 疾病 —— 无条件停旧 + 重新新增 pending.setSendstate(SENDSTATE_STOPPED); if (StringUtils.isNotEmpty(serviceSubtask.getRemark())) { serviceSubtask.setRemark(serviceSubtask.getRemark() + ",疾病患者再入院"); } else { serviceSubtask.setRemark("疾病患者再入院"); } pending.setResult("error"); pending.setUpdateBy(serviceTask.getUpdateBy()); // ⚠️ 原实现【丢弃】了这里的返回值(异常见类注释第 2 条) taskApi.updatePendingSubtask(pending); // 重新新增子任务 i = insert(taskApi, serviceSubtask) ? 1 : 0; // 这里需要全失败 addServiceSubtaskPreachform(serviceSubtask, PREACHFORM_STATE_FAIL); } } } else { //没有待执行任务 markPhoneEmptyIfNeeded(serviceSubtask); serviceSubtask.setCreateBy(serviceTask.getCreateBy()); serviceSubtask.setCreateTime(new Date()); i = insert(taskApi, serviceSubtask) ? 1 : 0; addServiceSubtaskPreachform(serviceSubtask, null); } } else if (serviceSubtask.getTaskSituation() != null && serviceSubtask.getTaskSituation() == 6) { // 该患者被过滤,不用执行 serviceSubtask.setCreateBy(serviceTask.getCreateBy()); serviceSubtask.setCreateTime(new Date()); serviceSubtask.setSendstate(SENDSTATE_STOPPED); i = insert(taskApi, serviceSubtask) ? 1 : 0; } else { // 不需要校验(check=false) markPhoneEmptyIfNeeded(serviceSubtask); serviceSubtask.setCreateBy(serviceTask.getCreateBy()); serviceSubtask.setCreateTime(new Date()); i = insert(taskApi, serviceSubtask) ? 1 : 0; addServiceSubtaskPreachform(serviceSubtask, null); } //把 check_flag 改成 1(已处理)/ 生成失败置 2 if (i == 1) { writeFlagByType(inhosp.getInhospid(), type, "1", null); } else { log.info("生成子任务失败serviceSubtask的taskid为:{},patid为:{}", serviceSubtask.getTaskid(), serviceSubtask.getPatid()); writeFlagByType(inhosp.getInhospid(), type, "2", "生成子任务失败 " + type); } return i == 1; } catch (Exception e) { // 出错回滚事务 log.info("生成子任务报错回滚serviceSubtask的taskid为:{},patid为:{}", serviceSubtask.getTaskid(), serviceSubtask.getPatid()); // ⚠️ 原实现的 catch 只处理 type 1/2/3/0 —— type=4/5 抛异常时不回写任何标志位。 // 原样保留(见类注释第 1 条)。 writeFlagByTypeForRollback(inhosp.getInhospid(), type, "2", "生成子任务报错回滚 " + type); e.printStackTrace(); return false; } } // 任务不存在时按 type 写标志位 private void writeMissingTaskFlag(VisitInhospRow inhosp, Integer type, String longTaskReason, Long taskid) { if (type == null) { return; } // 原实现对 type 用的是 if / else-if 链,语义等价于 switch;这里保持同样的分支与文案 switch (type) { case 0: log.info("该患者药品宣教长期任务不存在,任务ID为:{}", taskid); flagWriter.setFlagWithReason(inhosp.getInhospid(), "medcheck_flag", "2", longTaskReason + " & 该患者药品宣教长期任务不存在,任务ID为:" + taskid); return; case 1: log.info("该患者出院随访长期任务不存在,任务ID为:{}", taskid); flagWriter.setFlagWithReason(inhosp.getInhospid(), "deptcheck_flag", "2", longTaskReason + " & 该患者离院科室长期任务不存在,任务ID为:" + taskid); return; case 2: flagWriter.setFlagWithReason(inhosp.getInhospid(), "wardcheck_flag", "2", longTaskReason + " & 该患者离院病区长期任务不存在,任务ID为:" + taskid); return; case 3: flagWriter.setFlagWithReason(inhosp.getInhospid(), "diagcheck_flag", "2", longTaskReason + " & 该患者疾病长期任务不存在,任务ID为:" + taskid); return; case 4: flagWriter.setFlagWithReason(inhosp.getInhospid(), "in_deptcheck_flag", "2", longTaskReason + " & 该患者入院科室长期任务不存在,任务ID为:" + taskid); return; case 5: flagWriter.setFlagWithReason(inhosp.getInhospid(), "in_wardcheck_flag", "2", longTaskReason + " & 该患者入院病区长期任务不存在,任务ID为:" + taskid); return; default: // 原实现:type 不属于 0-5 时,if/else-if 链全部不命中 → 什么都不做、也不 return // → 继续往下走,此时 serviceTask 仍是 null,到 L729 之后会 NPE。 // 这里【不复制】那个 NPE(它只会掩盖问题),改为记录并返回。 log.warn("【出入院随访】任务不存在且 type={} 不属于 0-5,无法判定回写哪个标志位。" + "inhospid={}, taskid={}", type, inhosp.getInhospid(), taskid); } } /** * type → 标志位回写。照抄原 L881-885 与 L892-896 的分支。 *

* ⚠️ 注意 type=0 在"成功/失败"分支里【没有】对应 if(原实现也没写), * 所以 type=0 生成成功后不会把 {@code medcheck_flag} 置 1 —— 原样保留。 *

* remark 只有"失败"分支会写(原 L897)。 */ private void writeFlagByType(Long inhospid, Integer type, String flagValue, String remark) { if (type == null) { return; } String column = flagColumnOfSuccessOrFail(type); if (column == null) { // type=0(药品宣教)在成功/失败分支里没有对应 flag,原实现同样什么都不做 return; } if (remark == null) { flagWriter.setFlag(inhospid, column, flagValue); } else { flagWriter.setFlagWithRemark(inhospid, column, flagValue, remark); } } /** * type → 标志位列(成功/失败分支)。照抄原 L881-885 / L892-896: *

     * 1 -> deptcheck_flag   2 -> wardcheck_flag   3 -> diagcheck_flag
     * 4 -> in_deptcheck_flag 5 -> in_wardcheck_flag   (0 无对应)
     * 
*/ private String flagColumnOfSuccessOrFail(Integer type) { switch (type) { case 1: return "deptcheck_flag"; case 2: return "wardcheck_flag"; case 3: return "diagcheck_flag"; case 4: return "in_deptcheck_flag"; case 5: return "in_wardcheck_flag"; default: return null; } } /** * 异常回滚时的标志位回写。照抄原 L905-908 —— * ⚠️ 这里的分支与原"成功/失败"分支不同: * * 两处不一致是原代码本身的不一致,原样保留。 */ private void writeFlagByTypeForRollback(Long inhospid, Integer type, String flagValue, String remark) { if (type == null) { return; } String column; switch (type) { case 1: column = "deptcheck_flag"; break; case 2: column = "wardcheck_flag"; break; case 3: column = "diagcheck_flag"; break; case 0: column = "medcheck_flag"; break; default: // type=4/5 落这里:原实现不回写任何标志位 return; } flagWriter.setFlagWithRemark(inhospid, column, flagValue, remark); } /** * 手机号为空时追加备注、置 sendstate=4、result=error。 */ private void markPhoneEmptyIfNeeded(VisitSubtaskDraft d) { if (StringUtils.isEmpty(d.getPhone())) { if (StringUtils.isNotEmpty(d.getRemark())) { d.setRemark(d.getRemark() + ",手机号为空"); } else { d.setRemark("手机号为空"); } d.setSendstate(SENDSTATE_STOPPED); d.setResult("error"); } } /** * 原 {@code boxedServiceSubtask} 出院时间为空时的副作用(L1174-1176): *
     * patMedInhosp1.setDeptcheckFlag("2");
     * patMedInhosp1.setRemark(patMedInhosp1.getRemark() + "& 该患者疾病出院时间为空,出院记录ID为:" + patMedInhosp1.getInhospid());
     * patMedInhospMapper.updatePatMedInhosp(patMedInhosp1);
     * 
* ⚠️ 原实现这次调用【同时】写了 flag 与 remark,所以这里两步都要做。 */ private void onMissingOutHospTime(VisitInhospRow row) { flagWriter.setFlag(row.getInhospid(), "deptcheck_flag", "2"); flagWriter.setFlagWithRemark(row.getInhospid(), "deptcheck_flag", "2", (row.getRemark() == null ? "" : row.getRemark()) + "& 该患者疾病出院时间为空,出院记录ID为:" + row.getInhospid()); } /** * 把 {@link VisitLongTaskDto} 适配成 {@link com.ruoyi.inhospvisit.domain.row.VisitTaskRow}, * 供组装器使用。 *

* 组装器只读 task 的少量字段,这里逐字段映射;不留任何"看起来没用"的字段不映射 * —— 少映射一个就会静默丢值(见 VisitSubtaskFactory 的类注释)。 *

* ⚠️ {@code type / sendDay / updateBy / createBy / serviceType / preachform} 等 * 都在 {@link VisitLongTaskDto} 里有对应字段,直接搬。 */ private com.ruoyi.inhospvisit.domain.row.VisitTaskRow toTaskSource(VisitLongTaskDto dto, VisitInhospRow inhosp) { com.ruoyi.inhospvisit.domain.row.VisitTaskRow t = new com.ruoyi.inhospvisit.domain.row.VisitTaskRow(); t.setTaskid(dto.getTaskid()); t.setTaskName(dto.getTaskName()); t.setTaskDesc(dto.getTaskDesc()); t.setType(dto.getType()); t.setServiceType(dto.getServiceType()); t.setTemplateid(dto.getTemplateid()); t.setTemplatename(dto.getTemplatename()); t.setLibtemplateid(dto.getLibtemplateid()); t.setLibtemplatename(dto.getLibtemplatename()); t.setSendDay(dto.getSendDay()); t.setPreachform(dto.getPreachform()); t.setPreachformDesc(dto.getPreachformDesc()); t.setOrgid(dto.getOrgid()); t.setCampusid(dto.getCampusid()); // 以下 6 个同样属于"原 DtoConversionUtils 拷贝、随 insert 落库"的列 t.setGuid(dto.getGuid()); t.setPid(dto.getPid()); t.setSendType(dto.getSendType()); t.setIsupload(dto.getIsupload()); t.setTextParam(dto.getTextParam()); t.setUploadTime(dto.getUploadTime()); t.setUpdateBy(dto.getUpdateBy()); t.setCreateBy(dto.getCreateBy()); return t; } /** * insert 草稿并把生成的自增主键回填(原实现 insert 后可继续用同一个对象)。 */ private boolean insert(VisitTaskCreateApi api, VisitSubtaskDraft d) { return api.insertSubtask(d); } /** * 原 L782:对尚未 insert 的草稿调 {@code updateServiceSubtask}。 *

* 该草稿此时 {@code id} 为 null,所以 {@code where id = null} 影响 0 行。 * 为保持这一"无效写入"的原语义,这里用一个 id 为 null 的 pending 对象走同一条路径。 */ private boolean writeBackDraft(VisitTaskCreateApi api, VisitSubtaskDraft d) { VisitPendingSubtaskDto p = new VisitPendingSubtaskDto(); p.setId(d.getId()); p.setTaskid(d.getTaskid()); p.setSendstate(d.getSendstate()); p.setResult(d.getResult()); p.setRemark(d.getRemark()); p.setUpdateBy(d.getUpdateBy()); p.setUpdateTime(d.getUpdateTime()); return api.updatePendingSubtask(p); } /** * 原 L840/855/874 的 {@code addServiceSubtaskPreachform}。该方法的实现第 8 步才搬进来, * 所以这里惰性获取;取不到就跳过(不抛异常)。 */ private void addServiceSubtaskPreachform(VisitSubtaskDraft subtask, String sendstate) { if (preachformWriterProvider == null) { return; } try { SubtaskPreachformWriter writer = preachformWriterProvider.stream().findFirst().orElse(null); if (writer == null) { log.warn("【出入院随访】SubtaskPreachformWriter 未装配,跳过执行方式明细写入。subtaskId={}", subtask.getId()); return; } writer.addServiceSubtaskPreachform(subtask, sendstate); } catch (Exception e) { // 这一步失败不应影响主流程(原实现在同一个 try 里,失败会走 catch 回滚标志位; // 但那是因为它与 insert 同属一次事务。这里拆开后按"尽力而为"处理并记录) log.warn("【出入院随访】写执行方式明细失败,已忽略。subtaskId={},原因={}", subtask.getId(), e.toString()); } } private VisitTaskCreateApi taskCreateApi() { if (taskCreateApiProvider == null) { return null; } try { return taskCreateApiProvider.stream().findFirst().orElse(null); } catch (Exception e) { log.warn("【出入院随访】获取 VisitTaskCreateApi 失败,按不可用处理:{}", e.toString()); return null; } } /** * 写执行方式明细的能力(原 {@code addServiceSubtaskPreachform},L1061-1103,43 行)。 *

* 该方法的实现第 8 步才搬进本模块,所以先定义接口、用 {@code ObjectProvider} 惰性获取。 * 这样第 7 步就能编译并跑断言,不必等第 8 步。 */ public interface SubtaskPreachformWriter { /** * @param subtask 子任务草稿({@code id} 为落库后的主键) * @param sendstate 指定的执行方式状态;{@code null} 表示按原逻辑处理 */ void addServiceSubtaskPreachform(VisitSubtaskDraft subtask, String sendstate); } }