liusheng
5 天以前 cc479e6a056785103996cc7ea67dbd5faa3e800e
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
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
package com.ruoyi.inhospvisit.service;
 
import com.ruoyi.common.core.service.PublicityDataPort;
import com.ruoyi.common.core.service.VisitTaskCreateApi;
import com.ruoyi.common.core.service.dto.VisitInhospUpdateDto;
import com.ruoyi.common.core.service.dto.VisitPendingSubtaskDto;
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.domain.row.VisitTaskdeptRow;
import com.ruoyi.inhospvisit.domain.row.VisitTaskdiagRow;
import com.ruoyi.inhospvisit.mapper.VisitArchiveMapper;
import com.ruoyi.inhospvisit.mapper.VisitInhospMapper;
import com.ruoyi.inhospvisit.mapper.VisitTaskdeptMapper;
import com.ruoyi.inhospvisit.mapper.VisitTaskdiagMapper;
import com.ruoyi.inhospvisit.support.InhospFlagWriter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
 
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
 
/**
 * 出院随访「编排器」—— 逐行搬自 {@code PatMedInhospServiceImpl}
 * 的编排层({@code addSubTaskByRule} + 3 个 {@code *ForPatient} + {@code dealInHospInfo})。
 *
 * <h3>⚠️ 2026-10 口径变更:一个患者(一次出院)只生成 <b>一条</b> 随访子任务</h3>
 * 搬迁时本类是"逐行等价"的(原实现按维度全生成:每个 {@code *ForPatient} 把命中的
 * <b>所有</b>长期任务各写一条)。业务确认改为:
 * <ol>
 *   <li>维度按 {@code service_task_rule} 优先级排序尝试:首要 → 次要 → 通用兜底(科室、病区);</li>
 *   <li>第一个真正落库成功的维度即停止 → 只留一条;</li>
 *   <li>无规则时按 {@code service_taskdept} 的科室/病区"通用模块"生成;</li>
 *   <li>标志位统一回写,保证 {@code deptcheck_flag} 不再停在 0(防重复扫描);
 *       并在写库前按 {@code patid+inhospid+taskid} 判重(见 {@code VisitTaskCreateApi#existsSubtask})。</li>
 * </ol>
 * 因此 {@code verify_inhospvisit_orchestration_semantics.py} 里 O18~O23 那 6 条
 * "搬迁等价"断言已被改为"口径变更"断言(脚本里单独分组并注明依据)。
 *
 * <h3>来源对照(@ b25c3b51)</h3>
 * <table border="1">
 *   <tr><th>本类方法</th><th>原实现</th><th>行数</th></tr>
 *   <tr><td>{@link #addSubTaskByRule}</td><td>{@code addSubTaskByRule} L1691-1847</td><td>157</td></tr>
 *   <tr><td>{@link #addDeptSubTaskForPatient}</td><td>L1854-1887</td><td>34</td></tr>
 *   <tr><td>{@link #addWardSubTaskForPatient}</td><td>L1894-1935</td><td>42</td></tr>
 *   <tr><td>{@link #addIcd10SubTaskForPatient}</td><td>L1942-2001</td><td>60</td></tr>
 *   <tr><td>{@link #dealInHospInfo}</td><td>L1321-1359</td><td>39</td></tr>
 * </table>
 *
 * <h3>⚠️ 三个 {@code *ForPatient} 的失败回写<b>写进去的列集合不同</b>,已逐一保真</h3>
 * <table border="1">
 *   <tr><th>位置</th><th>原写法</th><th>实际写入的列</th></tr>
 *   <tr><td>科室·无模板(L1866-1872)</td>
 *       <td>重读对象 {@code setDeptcheckFlag("2")},另两个 flag 由重读对象原样带回</td>
 *       <td>deptcheck + wardcheck + diagcheck + reason</td></tr>
 *   <tr><td>科室·catch(L1879-1885)</td>
 *       <td>在<b>新鲜对象</b>上 set</td>
 *       <td>deptcheck + reason</td></tr>
 *   <tr><td>病区·失败(L1914-1920)</td>
 *       <td>重读对象 {@code setWardcheckFlag("2")},另两个 flag 原样带回</td>
 *       <td>wardcheck + deptcheck + diagcheck + reason</td></tr>
 *   <tr><td>病区·catch(L1927-1933)</td>
 *       <td>在新鲜对象上 set</td>
 *       <td>wardcheck + reason</td></tr>
 *   <tr><td>疾病·两处(L1967-1971 / L1985-1991)</td>
 *       <td>重读对象 {@code setDiagcheckFlag("2")} 并覆写 reason</td>
 *       <td>diagcheck + reason(注意:<b>没有</b>带回其它两个 flag)</td></tr>
 *   <tr><td>疾病·catch(L1993-1998)</td>
 *       <td>在新鲜对象上 set</td>
 *       <td>diagcheck + reason</td></tr>
 * </table>
 * 因此本类用 {@link PublicityDataPort#updateVisitInhosp(VisitInhospUpdateDto)}(可一次写多个 flag),
 * 而不是窄的"单 flag"接口 —— 后者表达不了这些差异。
 *
 * <h3>⚠️ 站点差异(原样保留)</h3>
 * <ul>
 *   <li>{@code nhfy}:疾病维度改用<b>主诊断名称</b> {@code leavediagname} 查询,而不是 ICD10 编码
 *       (因为南华附一的 icd10code 有重复);</li>
 *   <li>{@code xh}:{@code addSubTaskByRule} 里<b>整块跳过</b>病区 + 疾病两个维度(多处 {@code !xh} 判断)。</li>
 * </ul>
 */
@Slf4j
@Service
public class InhospTaskOrchestrator {
 
    /** 出院随访的服务类型 */
    private static final String SERVICE_TYPE_DISCHARGE = "2";
 
    /** 维度:科室 */
    private static final String DEPT_TYPE_DEPT = "1";
 
    /** 维度:病区 */
    private static final String DEPT_TYPE_WARD = "2";
 
    /** 站点:新华(跳过病区+疾病维度) */
    private static final String SITE_XH = "xh";
 
    /** 站点:南华附一(疾病维度用诊断名) */
    private static final String SITE_NHFY = "nhfy";
 
    @Value("${spring.profiles.active}")
    private String active;
 
    @Autowired
    private VisitInhospMapper inhospMapper;
 
    @Autowired
    private VisitArchiveMapper archiveMapper;
 
    @Autowired
    private VisitTaskdeptMapper taskdeptMapper;
 
    @Autowired
    private VisitTaskdiagMapper taskdiagMapper;
 
    @Autowired
    private InhospSubtaskWriter subtaskWriter;
 
    @Autowired
    private InhospFlagWriter flagWriter;
 
    @Autowired
    private ObjectProvider<PublicityDataPort> dataPortProvider;
 
    @Autowired
    private ObjectProvider<VisitTaskCreateApi> taskCreateApiProvider;
 
    // ==========================================================================
    // 1. 基于规则的任务生成(原 L1691-1847)
    // ==========================================================================
 
    /**
     * 按 {@code service_task_rule} 的「首要(1)/次要(2)」规则编排科室/病区/疾病三个维度。
     *
     * <h3>⚠️ 2026-10 口径变更:一个患者(一次出院)只生成 <b>一条</b> 随访子任务</h3>
     * 原实现(以及搬迁后的等价版)是按维度"全生成":先跑所有「首要」维度的
     * {@code *ForPatient},而每个 {@code *ForPatient} 又把命中的<b>所有</b>长期任务各写一条。
     * 结果是同一患者一次出院能生成几十条(线上实测 27 条),而且因为扫描键
     * {@code deptcheck_flag} 在"只有病区/疾病维度"的规则下不会被置 1,
     * 患者每轮扫描都被重新处理 → 重复子任务持续累积。
     * <p>
     * 现口径(业务确认):
     * <ol>
     *   <li>把三个维度按规则优先级排成一个<b>尝试顺序</b>:
     *       首要(1) → 次要(2) → 通用兜底(科室、病区);同优先级内固定 科室 &gt; 病区 &gt; 疾病。
     *       {@code service_task_rule} 没有该患者科室/病区对应的规则时 → 顺序为 科室 → 病区
     *       (即"按 {@code service_taskdept} 里的通用模块生成")。</li>
     *   <li>按顺序逐个维度尝试,<b>第一个真正落库成功(或已存在)的维度即停止</b> —— 只留一条。</li>
     *   <li>最后统一回写标志位:命中维度 = {@code 1};其余维度 = {@code 2}(跳过),
     *       且<b>无论命中哪个维度,{@code deptcheck_flag} 一定会被写成非 0</b>
     *       —— 这就是"不再被下一轮扫描重复生成"的保证。</li>
     * </ol>
     * 另有一道兜底在 {@code InhospSubtaskWriter#writeInSubTask}:落库前按
     * {@code patid + inhospid + taskid} 判重,见 {@link VisitTaskCreateApi#existsSubtask}。
     *
     * <h3>站点差异(保留)</h3>
     * {@code xh} 只做科室维度 —— {@link #addDimension} 里统一挡掉病区/疾病。
     *
     * @param inhosp 待处理的出院患者
     * @param archive 患者档案
     * @param rule 已查好的规则;{@code null} = 该科室/病区未配置规则
     * @param config {@code sys_config: visit.early.day}
     */
    public void addSubTaskByRule(VisitInhospRow inhosp, VisitArchiveRow archive,
                                 VisitTaskRule rule, String config) {
        try {
            List<Integer> order = resolveDimensionOrder(rule);
            log.info("【基于规则】患者{}规则:deptRule={}, wradRule={}, diagRule={};维度尝试顺序={}",
                    inhosp.getInhospid(),
                    rule == null ? null : rule.getDeptRule(),
                    rule == null ? null : rule.getWradRule(),
                    rule == null ? null : rule.getDiagRule(), order);
 
            Integer hit = null;
            for (Integer type : order) {
                log.info("【基于规则】患者{}尝试维度{}({})", inhosp.getInhospid(), type, dimensionName(type));
                if (generateOneForDimension(inhosp, archive, type, config)) {
                    hit = type;
                    break;
                }
            }
 
            writeFlagsAfterRun(inhosp, hit);
        } catch (Exception e) {
            // 原 L1839-1845
            e.printStackTrace();
            log.error("【基于规则】患者{}处理异常:{}", inhosp.getInhospid(), e.getMessage());
            updateCheckFlag(inhosp, 1, "2", "基于规则处理异常");
        }
    }
 
    /**
     * 维度尝试顺序(2026-10 口径)。
     * <p>
     * 规则为 {@code null} → {@code [科室, 病区]}:按 {@code service_taskdept} 里该患者的
     * 科室/病区"通用模块"生成;这是业务明确要求的兜底口径(此时<b>不进入疾病维度</b>)。
     * <p>
     * 有规则 → {@code [首要维度…, 次要维度…, 科室, 病区]}:
     * 首要优先、次要其后,最后用通用兜底保证"能生成一条就生成一条"。
     */
    private List<Integer> resolveDimensionOrder(VisitTaskRule rule) {
        boolean xhSite = SITE_XH.equals(active);
        List<Integer> order = new ArrayList<>(4);
        if (rule == null) {
            addDimension(order, 1, xhSite);
            addDimension(order, 2, xhSite);
            return order;
        }
        boolean deptIsPrimary = "1".equals(rule.getDeptRule());
        boolean wardIsPrimary = "1".equals(rule.getWradRule());
        boolean diagIsPrimary = "1".equals(rule.getDiagRule());
        boolean deptIsSecondary = "2".equals(rule.getDeptRule());
        boolean wardIsSecondary = "2".equals(rule.getWradRule());
        boolean diagIsSecondary = "2".equals(rule.getDiagRule());
        // 同优先级内固定顺序:科室 → 病区 → 疾病
        if (deptIsPrimary) {
            addDimension(order, 1, xhSite);
        }
        if (wardIsPrimary) {
            addDimension(order, 2, xhSite);
        }
        if (diagIsPrimary) {
            addDimension(order, 3, xhSite);
        }
        if (deptIsSecondary) {
            addDimension(order, 1, xhSite);
        }
        if (wardIsSecondary) {
            addDimension(order, 2, xhSite);
        }
        if (diagIsSecondary) {
            addDimension(order, 3, xhSite);
        }
        // 通用兜底:规则里配的维度全都取不到任务时,仍按 service_taskdept 的 科室/病区 生成一条
        addDimension(order, 1, xhSite);
        addDimension(order, 2, xhSite);
        return order;
    }
 
    /** 追加一个候选维度;{@code xh} 站点只允许科室维度(原 {@code !active.equals("xh")} 的等价写法) */
    private void addDimension(List<Integer> order, int type, boolean xhSite) {
        if (xhSite && type != 1) {
            return;
        }
        if (!order.contains(type)) {
            order.add(type);
        }
    }
 
    private String dimensionName(int type) {
        switch (type) {
            case 1:
                return "科室";
            case 2:
                return "病区";
            case 3:
                return "疾病";
            default:
                return "未知(" + type + ")";
        }
    }
 
    /** 尝试一个维度,返回"是否真的生成了(或本来就已经有)一条子任务" */
    private boolean generateOneForDimension(VisitInhospRow inhosp, VisitArchiveRow archive,
                                            int type, String config) {
        switch (type) {
            case 1:
                return addDeptSubTaskForPatient(inhosp, archive, config) > 0;
            case 2:
                return addWardSubTaskForPatient(inhosp, archive, config) > 0;
            case 3:
                return addIcd10SubTaskForPatient(inhosp, archive, config) > 0;
            default:
                return false;
        }
    }
 
    /**
     * 一轮生成结束后的标志位回写。
     * <p>
     * ⚠️ 关键点:扫描键是 {@code pat_med_inhosp.deptcheck_flag = '0'},
     * 所以<b>不论命中哪个维度,deptcheck_flag 都必须被写成 1 或 2</b>,
     * 否则下一轮扫描会把这个患者再处理一遍(2026-10 重复生成的直接原因)。
     * <p>
     * 没生成时({@code hit == null})刻意<b>只补扫描键</b>:各维度方法已经把更具体的原因
     * ("该患者所在科室未配置离院长期任务;" / "生成子任务失败 1" …)写进
     * {@code long_task_reason} 了,这里先用一次重读判断 deptcheck 是否已经非 0,
     * 避免把它覆盖成泛泛的"未生成随访任务"。
     *
     * @param hit 命中的维度;{@code null} = 三个维度都没能生成
     */
    private void writeFlagsAfterRun(VisitInhospRow inhosp, Integer hit) {
        boolean xhSite = SITE_XH.equals(active);
        if (hit == null) {
            VisitInhospRow fresh = inhospMapper.selectByInhospid(inhosp.getInhospid());
            String current = fresh == null ? null : fresh.getDeptcheckFlag();
            if (current == null || "0".equals(current)) {
                updateCheckFlag(inhosp, 1, "2", composeRemark(inhosp, "未生成随访任务(无可用长期任务)"));
            }
            return;
        }
        for (int type = 1; type <= 3; type++) {
            if (xhSite && type != 1) {
                continue;
            }
            if (hit == type) {
                updateCheckFlag(inhosp, type, "1", composeRemark(inhosp,
                        "按优先级生成一条随访任务(维度=" + dimensionName(type) + ")"));
            } else {
                updateCheckFlag(inhosp, type, "2", composeRemark(inhosp,
                        "已生成一条随访任务(维度=" + dimensionName(hit) + "),本维度跳过"));
            }
        }
    }
 
    /** 拼 reason:沿用原实现的 {@code remark + ";xxx"} 形式 */
    private String composeRemark(VisitInhospRow inhosp, String text) {
        String remark = inhosp.getRemark();
        return StringUtils.isNotEmpty(remark) ? remark + ";" + text : text;
    }
 
    /**
     * 统一回写标志位 —— 委托 {@link InhospFlagWriter}(原 {@code updateCheckFlag})。
     */
    private void updateCheckFlag(VisitInhospRow inhosp, Integer type, String flagValue, String remark) {
        flagWriter.updateCheckFlag(inhosp, type, flagValue, remark);
    }
 
    // ==========================================================================
    // 2. 科室维度(原 L1854-1887)
    // ==========================================================================
 
    /**
     * 为单个患者生成科室维度的子任务。
     * <p>
     * ⚠️ 2026-10 口径变更:命中多个长期任务时<b>只生成第一条真正落库的任务</b>
     * (原实现是 {@code for (t : taskdepts) writeInSubTask(...); generatedCount++;},
     * 一个科室配 N 个长期任务就会生成 N 条 —— 是"同一患者几十条"的主因)。
     *
     * @return 生成的子任务数量(0 或 1)
     */
    public int addDeptSubTaskForPatient(VisitInhospRow inhosp, VisitArchiveRow archive, String config) {
        try {
            // 原 L1857-1864:按患者所在科室查该科室的长期任务(dept_type=1 + service_type=2)
            VisitTaskdeptRow query = new VisitTaskdeptRow();
            query.setLongtask(1L);
            query.setDeptCode(inhosp.getLeaveldeptcode());
            query.setDeptType(DEPT_TYPE_DEPT);
            query.setServiceType(SERVICE_TYPE_DISCHARGE);
            query.setOrgid(inhosp.getOrgid());
            List<VisitTaskdeptRow> taskdepts = taskdeptMapper.selectList(query);
 
            if (CollectionUtils.isEmpty(taskdepts)) {
                // 原 L1866-1872:⚠️ 在【重读对象】上改 deptcheck,并把另两个 flag 原样带回
                VisitInhospRow fresh = inhospMapper.selectByInhospid(inhosp.getInhospid());
                VisitInhospUpdateDto u = new VisitInhospUpdateDto();
                u.setInhospid(inhosp.getInhospid());
                u.setDeptcheckFlag("2");
                u.setWardcheckFlag(fresh == null ? null : fresh.getWardcheckFlag());
                u.setDiagcheckFlag(fresh == null ? null : fresh.getDiagcheckFlag());
                // 原实现不判 null(fresh.getLongTaskReason() + "..."),原样保留
                u.setLongTaskReason((fresh == null ? null : fresh.getLongTaskReason())
                        + "该患者所在科室未配置离院长期任务;");
                updateInhosp(u);
            } else {
                for (VisitTaskdeptRow t : taskdepts) {
                    // 一个患者只留一条:第一条真正落库的即返回
                    if (subtaskWriter.writeInSubTask(t.getTaskId(), true, inhosp, archive, 1, config)) {
                        return 1;
                    }
                    log.info("【科室任务】患者{}任务{}未生成子任务,尝试下一个候选",
                            inhosp.getInhospid(), t.getTaskId());
                }
            }
        } catch (Exception e) {
            // 原 L1879-1885:⚠️ 在【新鲜对象】上 set —— 只写 deptcheck + reason
            e.printStackTrace();
            log.error("【科室任务】患者{}处理异常:{}", inhosp.getInhospid(), e.getMessage());
            VisitInhospUpdateDto u = new VisitInhospUpdateDto();
            u.setInhospid(inhosp.getInhospid());
            u.setDeptcheckFlag("2");
            u.setLongTaskReason("科室任务处理异常");
            updateInhosp(u);
        }
        return 0;
    }
 
    // ==========================================================================
    // 3. 病区维度(原 L1894-1935)
    // ==========================================================================
 
    /**
     * 为单个患者生成病区维度的子任务。
     * <p>
     * ⚠️ 同上:命中多个长期任务时只生成第一条真正落库的(原实现是全生成)。
     *
     * @return 生成的子任务数量(0 或 1)
     */
    public int addWardSubTaskForPatient(VisitInhospRow inhosp, VisitArchiveRow archive, String config) {
        try {
            String errorreason = null;
            List<VisitTaskdeptRow> taskdeptList = null;
            // 原 L1899-1912
            if (!StringUtils.isEmpty(inhosp.getLeavehospitaldistrictcode())) {
                VisitTaskdeptRow query = new VisitTaskdeptRow();
                query.setLongtask(1L);
                query.setDeptCode(inhosp.getLeavehospitaldistrictcode());
                query.setDeptType(DEPT_TYPE_WARD);
                query.setServiceType(SERVICE_TYPE_DISCHARGE);
                query.setOrgid(inhosp.getOrgid());
                taskdeptList = taskdeptMapper.selectList(query);
                if (CollectionUtils.isEmpty(taskdeptList)) {
                    errorreason = "该患者所在病区未配置长期任务;";
                }
            } else {
                errorreason = "离院病区为空;";
            }
 
            if (StringUtils.isNotEmpty(errorreason)) {
                // 原 L1914-1920:⚠️ 重读对象 setWardcheck,另两个 flag 原样带回
                VisitInhospRow fresh = inhospMapper.selectByInhospid(inhosp.getInhospid());
                VisitInhospUpdateDto u = new VisitInhospUpdateDto();
                u.setInhospid(inhosp.getInhospid());
                u.setWardcheckFlag("2");
                u.setDeptcheckFlag(fresh == null ? null : fresh.getDeptcheckFlag());
                u.setDiagcheckFlag(fresh == null ? null : fresh.getDiagcheckFlag());
                // 原有判空(与科室维度那处不同),原样保留
                u.setLongTaskReason(StringUtils.isNotEmpty(fresh == null ? null : fresh.getLongTaskReason())
                        ? fresh.getLongTaskReason() + errorreason : errorreason);
                updateInhosp(u);
            } else {
                for (VisitTaskdeptRow t : taskdeptList) {
                    // 一个患者只留一条:第一条真正落库的即返回
                    if (subtaskWriter.writeInSubTask(t.getTaskId(), true, inhosp, archive, 2, config)) {
                        return 1;
                    }
                    log.info("【病区任务】患者{}任务{}未生成子任务,尝试下一个候选",
                            inhosp.getInhospid(), t.getTaskId());
                }
            }
        } catch (Exception e) {
            // 原 L1927-1933:新鲜对象 —— 只写 wardcheck + reason
            e.printStackTrace();
            log.error("【病区任务】患者{}处理异常:{}", inhosp.getInhospid(), e.getMessage());
            VisitInhospUpdateDto u = new VisitInhospUpdateDto();
            u.setInhospid(inhosp.getInhospid());
            u.setWardcheckFlag("2");
            u.setLongTaskReason("病区任务处理异常");
            updateInhosp(u);
        }
        return 0;
    }
 
    // ==========================================================================
    // 4. 疾病维度(原 L1942-2001)
    // ==========================================================================
 
    /**
     * 为单个患者生成疾病维度的子任务。
     * <p>
     * ⚠️ 同上:命中多个长期任务时只生成第一条真正落库的(原实现是全生成)。
     *
     * @return 生成的子任务数量(0 或 1)
     */
    public int addIcd10SubTaskForPatient(VisitInhospRow inhosp, VisitArchiveRow archive, String config) {
        try {
            // 原 L1945-1947
            if (StringUtils.isEmpty(inhosp.getLeaveicd10code())) {
                return 0;
            }
 
            // 原 L1949-1963:站点差异 —— nhfy 用诊断名,其它用 ICD10 编码
            VisitTaskdiagRow query = new VisitTaskdiagRow();
            query.setLongtask(1L);
            if (SITE_NHFY.equals(active)) {
                query.setIcd10name(inhosp.getLeavediagname());
                if (StringUtils.isEmpty(inhosp.getLeavediagname())) {
                    // 原 L1954-1959:⚠️ 新鲜对象 —— 只写 diagcheck + reason(覆盖式)
                    VisitInhospUpdateDto u = new VisitInhospUpdateDto();
                    u.setInhospid(inhosp.getInhospid());
                    u.setDiagcheckFlag("2");
                    u.setLongTaskReason("该患者主诊断名称为空;");
                    updateInhosp(u);
                    return 0;
                }
            } else {
                query.setIcd10code(inhosp.getLeaveicd10code());
            }
            query.setOrgid(inhosp.getOrgid());
            List<VisitTaskdiagRow> taskdiags = taskdiagMapper.selectListByIcdName(query);
 
            if (CollectionUtils.isEmpty(taskdiags)) {
                // 原 L1967-1971:⚠️ 重读对象,但【只】setDiagcheckFlag + 覆写 reason
                //    (与科室/病区那两处不同:这里不带回其它 flag)
                VisitInhospUpdateDto u = new VisitInhospUpdateDto();
                u.setInhospid(inhosp.getInhospid());
                u.setDiagcheckFlag("2");
                u.setLongTaskReason("该患者所患疾病未配置长期任务;");
                updateInhosp(u);
            } else {
                int flag = 0;
                for (VisitTaskdiagRow t : taskdiags) {
                    // 原 L1975-1983:必须科室或病区能匹配上才建
                    if (StringUtils.isNotEmpty(t.getDeptCode())
                            && t.getDeptCode().equals(inhosp.getLeaveldeptcode())) {
                        flag = 1;
                        // 一个患者只留一条:第一条真正落库的即返回
                        if (subtaskWriter.writeInSubTask(t.getTaskId(), true, inhosp, archive, 3, config)) {
                            return 1;
                        }
                        log.info("【疾病任务】患者{}任务{}未生成子任务,尝试下一个候选",
                                inhosp.getInhospid(), t.getTaskId());
                    } else if (StringUtils.isNotEmpty(t.getWardCode())
                            && t.getWardCode().equals(inhosp.getLeavehospitaldistrictcode())) {
                        flag = 1;
                        if (subtaskWriter.writeInSubTask(t.getTaskId(), true, inhosp, archive, 3, config)) {
                            return 1;
                        }
                        log.info("【疾病任务】患者{}任务{}未生成子任务,尝试下一个候选",
                                inhosp.getInhospid(), t.getTaskId());
                    }
                }
                if (flag == 0) {
                    // 原 L1985-1991
                    log.error("该科室或病区对于患者所患疾病未配置长期任务,患者:{}", inhosp.getInhospid());
                    VisitInhospUpdateDto u = new VisitInhospUpdateDto();
                    u.setInhospid(inhosp.getInhospid());
                    u.setDiagcheckFlag("2");
                    u.setLongTaskReason("该科室或病区对于患者所患疾病未配置长期任务;");
                    updateInhosp(u);
                }
            }
        } catch (Exception e) {
            // 原 L1993-1998:新鲜对象 —— 只写 diagcheck + reason
            e.printStackTrace();
            log.error("【疾病任务】患者{}处理异常:{}", inhosp.getInhospid(), e.getMessage());
            VisitInhospUpdateDto u = new VisitInhospUpdateDto();
            u.setInhospid(inhosp.getInhospid());
            u.setDiagcheckFlag("2");
            u.setLongTaskReason("疾病任务处理异常");
            updateInhosp(u);
        }
        return 0;
    }
 
    // ==========================================================================
    // 5. 再入院处理(原 L1321-1359)
    // ==========================================================================
 
    /**
     * 再入院处理:新入院记录出现时,把该患者<b>旧的待执行随访</b>停掉。
     * <p>
     * 逐行对应原 {@code dealInHospInfo(List<PatMedInhosp> patMedInhospList)}。
     * <p>
     * ⚠️ 原实现的两个要点:
     * <ol>
     *   <li>查询条件是 {@code patid + sendstate=2},<b>不设 taskid</b>(按患者全量查);
     *       这里传 {@code null} 表示不按 taskid 过滤;</li>
     *   <li>景宁({@code 47255004333112711A1001})跳过停机 —— 不过这次判的是
     *       <b>{@code serviceSubtask1.getOrgid()}</b>(子任务的 orgid),不是入参患者的 orgid。</li>
     * </ol>
     *
     * @param newInHosp 新入院的出入院记录(用它和旧子任务的 endtime 做比较)
     */
    public void dealInHospInfo(VisitInhospRow newInHosp) {
        try {
            VisitTaskCreateApi api = taskApi();
            if (api == null) {
                log.warn("【出入院随访】VisitTaskCreateApi 不可用,跳过再入院处理。inhospid={}",
                        newInHosp.getInhospid());
                return;
            }
            // 原 L1324-1328:patid + sendstate=2(不设 taskid)
            List<VisitPendingSubtaskDto> pendingList =
                    api.findPendingSubtasks(newInHosp.getPatid(), null);
            if (CollectionUtils.isEmpty(pendingList)) {
                return;
            }
            for (VisitPendingSubtaskDto pending : pendingList) {
                // 原 L1332-1335:不是长期任务,不处理
                if (pending.getLongSendTime() == null) {
                    continue;
                }
                // 原 L1336-1340:⚠️ 判的是【子任务的 orgid】
                boolean firstVisit = ORG_JINGNING.equals(pending.getOrgid());
                if (firstVisit) {
                    continue;
                }
                // 原 L1342-1350:旧子任务的出院时间在新入院时间之前 → 停掉
                if (pending.getEndtime() != null && newInHosp.getStarttime() != null
                        && pending.getEndtime().before(newInHosp.getStarttime())) {
                    pending.setSendstate(4L);
                    pending.setIsVisitAgain(2);
                    pending.setRemark("患者再入院");
                    pending.setResult("error");
                    api.updatePendingSubtask(pending);
                }
            }
        } catch (Exception e) {
            // 原 L1355-1357
            log.error("入院数据信息出异常了:{},{}", newInHosp, e.getMessage());
        }
    }
 
    /** 景宁机构ID(原 L1338;与 InhospSubtaskWriter 里那份是同一常量,已逐字核对) */
    private static final String ORG_JINGNING = "47255004333112711A1001";
 
    // ==========================================================================
    // 辅助
    // ==========================================================================
 
    private void updateInhosp(VisitInhospUpdateDto u) {
        PublicityDataPort port = dataPort();
        if (port == null) {
            log.warn("【出入院随访】PublicityDataPort 不可用,无法写回标志位。inhospid={}", u.getInhospid());
            return;
        }
        port.updateVisitInhosp(u);
    }
 
    private PublicityDataPort dataPort() {
        if (dataPortProvider == null) {
            return null;
        }
        try {
            return dataPortProvider.stream().findFirst().orElse(null);
        } catch (Exception e) {
            log.warn("【出入院随访】获取 PublicityDataPort 失败:{}", e.toString());
            return null;
        }
    }
 
    private VisitTaskCreateApi taskApi() {
        if (taskCreateApiProvider == null) {
            return null;
        }
        try {
            return taskCreateApiProvider.stream().findFirst().orElse(null);
        } catch (Exception e) {
            log.warn("【出入院随访】获取 VisitTaskCreateApi 失败:{}", e.toString());
            return null;
        }
    }
 
    /**
     * {@code service_task_rule} 的规则快照(原 {@code ServiceTaskRule} 的随访侧视图)。
     * <p>
     * 规则查询本身仍由 {@code PublicityDataPort} 提供(它已经在服务宣教链路),
     * 本类只消费结果,避免为规则表再加一个 Mapper。
     */
    public static class VisitTaskRule {
 
        /** 科室规则:"1"=首要 "2"=次要,其它=不参与 */
        private String deptRule;
 
        /** 病区规则(原字段名就是拼错的 {@code wradRule},保持同名以免读错) */
        private String wradRule;
 
        /** 疾病规则 */
        private String diagRule;
 
        public String getDeptRule() {
            return deptRule;
        }
 
        public void setDeptRule(String deptRule) {
            this.deptRule = deptRule;
        }
 
        public String getWradRule() {
            return wradRule;
        }
 
        public void setWradRule(String wradRule) {
            this.wradRule = wradRule;
        }
 
        public String getDiagRule() {
            return diagRule;
        }
 
        public void setDiagRule(String diagRule) {
            this.diagRule = diagRule;
        }
    }
 
    /** 供测试/日志:本类是否拿到了必需的 Mapper */
    boolean hasMappers() {
        return inhospMapper != null && archiveMapper != null
                && taskdeptMapper != null && taskdiagMapper != null;
    }
 
    /** 预留:供日志使用的时间戳(避免 IDE 报未使用 import Date) */
    @SuppressWarnings("unused")
    private Date now() {
        return new Date();
    }
}