liusheng
8 天以前 34f78281cf395a24e1f3f1c3b861186ff8a03b7b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
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 行)。
 *
 * <h3>这次搬迁的形态:<b>结构性改写</b>(不是纯搬运)</h3>
 * 原实现直接用 {@code serviceSubtaskMapper} / {@code patMedInhospMapper} 写库。
 * 按你的要求「任务创建将来单独拉一个模块」,本次把三处写库能力换成
 * {@link VisitTaskCreateApi} 的三个方法:
 * <table border="1">
 *   <tr><th>原调用</th><th>现在</th></tr>
 *   <tr><td>{@code serviceTaskMapper.selectServiceTaskList(st)}(L689)</td>
 *       <td>{@link VisitTaskCreateApi#findPublishedLongTask(Long)}</td></tr>
 *   <tr><td>{@code serviceSubtaskMapper.insertServiceSubtask(...)}(L823/838/854/862/873)</td>
 *       <td>{@link VisitTaskCreateApi#insertSubtask(VisitSubtaskDraft)}</td></tr>
 *   <tr><td>{@code serviceSubtaskMapper.updateServiceSubtask(...)}(L782/791/812/836)</td>
 *       <td>{@link VisitTaskCreateApi#updatePendingSubtask(VisitPendingSubtaskDto)}</td></tr>
 *   <tr><td>{@code serviceSubtaskMapper.selectServiceSubtaskList(subtask)}(L764)</td>
 *       <td>{@link VisitTaskCreateApi#findPendingSubtasks(Long, Long)}</td></tr>
 * </table>
 * 因为属于结构性改写,<b>逐行比对盖不住</b>,所以本方法配的是
 * <b>B 档「口径双向断言」</b>({@code sql/_probe/verify_inhospvisit_writein_semantics.py}),
 * 逐条断言原实现的口径在新实现里仍然存在。
 *
 * <h3>⚠️ 原实现的 3 处怪癖,全部原样保留(写错就是行为变更)</h3>
 * <ol>
 *   <li><b>{@code type == 0}(药品宣教)用的是 {@code medcheck_flag}</b>,
 *       而 catch 块里只有 {@code type==1/2/3/0} 四个分支 ——
 *       <b>type=4/5 抛异常时不会回写任何标志位</b>(原 L900-912)。</li>
 *   <li><b>{@code type == 3} 分支的 {@code updateServiceSubtask} 返回值被丢弃</b>
 *       (原 L836 没有 {@code Boolean aBoolean = ...}),而 {@code i} 来自后面的
 *       {@code insertServiceSubtask} —— 所以 type=3 只看 insert 结果,不看停旧结果。</li>
 *   <li><b>{@code serviceSubtask1.getServiceType().equals("2")} 未判 null</b>(原 L803)
 *       —— 待执行子任务的 serviceType 为 null 时会 NPE。原样保留。</li>
 * </ol>
 *
 * <h3>⚠️ 降级策略</h3>
 * {@link VisitTaskCreateApi} 是"任务创建模块"的预留接口,本次由 smartor 提供过渡实现。
 * 取不到实现时(模块没打进应用 / 容器异常)本类<b>按"不生成子任务"处理并打 WARN</b>,
 * 绝不抛异常 —— 出入院随访挂了就是当天全院随访停摆。
 */
@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)。
     * <p>
     * 该方法(原 L1061-1103,43 行)在<b>第 8 步</b>才搬进本模块,
     * 但 {@code writeInSubTask} 现在就用到它(L840/855/874)。
     * 所以这里先用一个 {@code ObjectProvider} 惰性拿;取不到就跳过并打 WARN。
     */
    @Autowired
    private ObjectProvider<SubtaskPreachformWriter> preachformWriterProvider;
 
    @Autowired
    private ObjectProvider<VisitTaskCreateApi> 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}
     */
    public void writeInSubTask(Long taskid, Boolean check, VisitInhospRow inhosp,
                               VisitArchiveRow archive, Integer type, String config) {
        // 原 L683-685
        String longTaskReason = inhosp.getLongTaskReason();
        String orgId = inhosp.getOrgid();
        if (StringUtils.isEmpty(longTaskReason)) {
            longTaskReason = "";
        }
 
        // 原 L686-728:查"已发布的长期任务";查不到时按 type 写不同的标志位后 return
        VisitTaskCreateApi taskApi = taskCreateApi();
        if (taskApi == null) {
            log.warn("【出入院随访】VisitTaskCreateApi 不可用(任务创建模块未装配),本患者不生成子任务。"
                    + "inhospid={}, taskid={}", inhosp.getInhospid(), taskid);
            return;
        }
        VisitLongTaskDto serviceTask = taskApi.findPublishedLongTask(taskid);
        if (serviceTask == null) {
            writeMissingTaskFlag(inhosp, type, longTaskReason, taskid);
            return;
        }
 
        // 原 L729-731
        if ("1".equals(inhosp.getInhospstate()) && inhosp.getEndtime() == null) {
            return;
        }
 
        // 原 L732-736:组装(副作用由本类实现,见 missingOutHospTime)
        VisitSubtaskDraft serviceSubtask = subtaskFactory.boxedServiceSubtask(
                toTaskSource(serviceTask, inhosp), inhosp, archive, config, type,
                row -> onMissingOutHospTime(row));
        if (ObjectUtils.isEmpty(serviceSubtask)) {
            return;
        }
 
        // 原 L737-753:就诊科室/病区填充
        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());
        }
 
        // 原 L754-755:记录 insertServiceSubtask 的返回状态
        int i = 0;
        try {
            // 原 L757-758
            if (Boolean.TRUE.equals(check)
                    && (serviceSubtask.getTaskSituation() == null || serviceSubtask.getTaskSituation() != 6)) {
                // 原 L759-764:查该患者 + 该 taskid 下所有待执行子任务
                List<VisitPendingSubtaskDto> pendingList =
                        taskApi.findPendingSubtasks(archive.getId(), taskid);
                if (pendingList != null && !pendingList.isEmpty()) {
                    for (VisitPendingSubtaskDto pending : pendingList) {
                        // 原 L768-771:不是长期任务,不处理
                        if (pending.getLongSendTime() == null) {
                            continue;
                        }
                        // 原 L772-773: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;
                            }
                            // 原 L795-800:景宁 出院首次(7天)随访一定要做
                            boolean firstVisit = ORG_JINGNING.equals(orgId);
                            if (!firstVisit) {
                                // 原 L802-803
                                // ⚠️ 原实现不判 serviceType 是否为 null(会 NPE),原样保留
                                if (endTime1.before(endTime)
                                        && SERVICE_TYPE_DISCHARGE.equals(pending.getServiceType())) {
                                    // 原 L804-813:把循环到的旧子任务停掉
                                    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 {
                                    // 原 L814-824:相反,则把"当前的"数据停掉(置 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) {
                            // 原 L826-841: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 {
                    // 原 L844-856:没有待执行任务
                    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) {
                // 原 L857-862:该患者被过滤,不用执行
                serviceSubtask.setCreateBy(serviceTask.getCreateBy());
                serviceSubtask.setCreateTime(new Date());
                serviceSubtask.setSendstate(SENDSTATE_STOPPED);
                i = insert(taskApi, serviceSubtask) ? 1 : 0;
            } else {
                // 原 L863-875:不需要校验(check=false)
                markPhoneEmptyIfNeeded(serviceSubtask);
                serviceSubtask.setCreateBy(serviceTask.getCreateBy());
                serviceSubtask.setCreateTime(new Date());
                i = insert(taskApi, serviceSubtask) ? 1 : 0;
                addServiceSubtaskPreachform(serviceSubtask, null);
            }
 
            // 原 L877-899:把 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);
            }
        } catch (Exception e) {
            // 原 L900-912:出错回滚事务
            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();
        }
    }
 
    // ==========================================================================
    // 原 L691-725:任务不存在时按 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);
        }
    }
 
    // ==========================================================================
    // 原 L877-912:标志位回写(成功 / 失败 / 异常回滚)
    // ==========================================================================
 
    /**
     * type → 标志位回写。<b>照抄原 L881-885 与 L892-896 的分支</b>。
     * <p>
     * ⚠️ 注意 type=0 在"成功/失败"分支里【没有】对应 if(原实现也没写),
     * 所以 type=0 生成成功后不会把 {@code medcheck_flag} 置 1 —— 原样保留。
     * <p>
     * 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:
     * <pre>
     * 1 -> deptcheck_flag   2 -> wardcheck_flag   3 -> diagcheck_flag
     * 4 -> in_deptcheck_flag 5 -> in_wardcheck_flag   (0 无对应)
     * </pre>
     */
    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 ——
     * ⚠️ <b>这里的分支与原"成功/失败"分支不同</b>:
     * <ul>
     *   <li>有 {@code type==0 → medcheck_flag};</li>
     *   <li><b>没有</b> {@code type==4/5} 的分支(即入院宣教抛异常时标志位不动)。</li>
     * </ul>
     * 两处不一致是原代码本身的不一致,<b>原样保留</b>。
     */
    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);
    }
 
    // ==========================================================================
    // 辅助
    // ==========================================================================
 
    /**
     * 原 L845-851 / L864-870:手机号为空时追加备注、置 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):
     * <pre>
     * patMedInhosp1.setDeptcheckFlag("2");
     * patMedInhosp1.setRemark(patMedInhosp1.getRemark() + "&amp; 该患者疾病出院时间为空,出院记录ID为:" + patMedInhosp1.getInhospid());
     * patMedInhospMapper.updatePatMedInhosp(patMedInhosp1);
     * </pre>
     * ⚠️ 原实现这次调用【同时】写了 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},
     * 供组装器使用。
     * <p>
     * 组装器只读 task 的少量字段,这里逐字段映射;<b>不留任何"看起来没用"的字段不映射</b>
     * —— 少映射一个就会静默丢值(见 VisitSubtaskFactory 的类注释)。
     * <p>
     * ⚠️ {@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.setType(dto.getType());
        t.setServiceType(dto.getServiceType());
        t.setTemplateid(dto.getTemplateid());
        t.setTemplatename(dto.getTemplatename());
        t.setLibtemplateid(dto.getLibtemplateid());
        t.setSendDay(dto.getSendDay());
        t.setPreachform(dto.getPreachform());
        t.setPreachformDesc(dto.getPreachformDesc());
        t.setOrgid(dto.getOrgid());
        t.setCampusid(dto.getCampusid());
        t.setUpdateBy(dto.getUpdateBy());
        t.setCreateBy(dto.getCreateBy());
        return t;
    }
 
    /**
     * insert 草稿并把生成的自增主键回填(原实现 insert 后可继续用同一个对象)。
     */
    private boolean insert(VisitTaskCreateApi api, VisitSubtaskDraft d) {
        return api.insertSubtask(d);
    }
 
    /**
     * 原 L782:对<b>尚未 insert 的草稿</b>调 {@code updateServiceSubtask}。
     * <p>
     * 该草稿此时 {@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}。该方法的实现<b>第 8 步</b>才搬进来,
     * 所以这里惰性获取;取不到就跳过(不抛异常)。
     */
    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 行)。
     * <p>
     * 该方法的实现<b>第 8 步</b>才搬进本模块,所以先定义接口、用 {@code ObjectProvider} 惰性获取。
     * 这样第 7 步就能编译并跑断言,不必等第 8 步。
     */
    public interface SubtaskPreachformWriter {
 
        /**
         * @param subtask   子任务草稿({@code id} 为落库后的主键)
         * @param sendstate 指定的执行方式状态;{@code null} 表示按原逻辑处理
         */
        void addServiceSubtaskPreachform(VisitSubtaskDraft subtask, String sendstate);
    }
}