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
package com.ruoyi.inhospvisit.service;
 
import com.ruoyi.common.core.service.OtherVisitApi;
import com.ruoyi.common.core.service.PublicityApi;
import com.ruoyi.common.core.service.PublicityDataPort;
import com.ruoyi.common.core.service.dto.InHospPublicityRowDto;
import com.ruoyi.common.core.service.dto.VisitTaskRuleDto;
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.mapper.VisitArchiveMapper;
import com.ruoyi.inhospvisit.mapper.VisitInhospMapper;
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.List;
 
/**
 * 出入院随访的<b>两个入口</b> —— 逐行搬自 {@code PatMedInhospServiceImpl} 的
 * {@code dealOutHospInfo}(L324-463,140 行)与 {@code dealOutHospInfoBySchedule}(L471-614,144 行)。
 *
 * <h3>⚠️ 这一步是"切换调用点",必须在同一步完成三件事</h3>
 * <ol>
 *   <li><b>宣教改走 {@link PublicityApi}</b>:原 L331-409(入院宣教)与 L411-444(药品宣教)
 *       本次<b>不再内联</b>,改为回调宣教模块。若不同时删掉旧的内联块,
 *       <b>宣教会发两遍</b>(第 5 步就留下的已知中间状态);</li>
 *   <li><b>手术随访改走 {@link OtherVisitApi}</b>:原 L449-451 与 L559-608 那两处;</li>
 *   <li><b>编排层改走本模块的 {@link InhospTaskOrchestrator}</b>:原 {@code addSubTaskByRule} /
 *       {@code dealInHospInfo}。</li>
 * </ol>
 *
 * <h3>与原实现的两处结构差异(都已登记)</h3>
 * <ol>
 *   <li><b>患者列表的取数方式变了</b>:原实现直接查 {@code pat_med_inhosp};
 *       搬迁后走本模块自带的 {@link VisitInhospMapper}(SQL 与 smartor 那份逐条等价,
 *       由 {@code verify_inhospvisit_sql_expansion.py} 把关)。
 *       注意原实现第一段查的是 {@code deptcheckFlag="0" + inhospstate="1" + fuflag="1"},
 *       这段既用于宣教也用于 `addSubTaskByRule`。</li>
 *   <li><b>宣教的患者列表由宣教模块自己查</b>:原实现复用同一个 {@code patMedInhosps} 列表;
 *       搬迁后 {@link PublicityApi#dealInHospPublicity} 内部自己查一份(条件相同)。
 *       这是<b>故意的</b>:宣教模块不该依赖随访模块的内存列表。
 *       代价是多一次轻量查询,已在 {@code PublicityServiceImpl} 的注释里说明。</li>
 * </ol>
 */
@Slf4j
@Service
public class InhospVisitTaskService {
 
    @Value("${spring.profiles.active}")
    private String active;
 
    @Autowired
    private VisitInhospMapper inhospMapper;
 
    @Autowired
    private VisitArchiveMapper archiveMapper;
 
    @Autowired
    private InhospTaskOrchestrator orchestrator;
 
    @Autowired
    private ObjectProvider<PublicityApi> publicityApiProvider;
 
    @Autowired
    private ObjectProvider<OtherVisitApi> otherVisitApiProvider;
 
    @Autowired
    private ObjectProvider<PublicityDataPort> dataPortProvider;
 
    // ==========================================================================
    // 入口 1:出院随访主扫描(原 L324-463)
    // ==========================================================================
 
    /**
     * 出院随访主扫描。顺序<b>逐条对应</b>原实现:
     * <ol>
     *   <li>取"科室维度未处理"的在院患者(原 L325-329);</li>
     *   <li>入院宣教 → 回调 {@link PublicityApi#dealInHospPublicity}(原 L331-409);</li>
     *   <li>药品宣教(仅 sltd)→ 回调 {@link PublicityApi#dealMedicalPublicity}(原 L411-444);</li>
     *   <li>基于规则的出院随访 → {@link #addSubTaskByRule}(原 L448);</li>
     *   <li>手术随访(非 xh)→ 回调 {@link OtherVisitApi#addOperationSubTask}(原 L449-451);</li>
     *   <li>再入院处理 → {@link #dealInHospInfo}(原 L454-461)。</li>
     * </ol>
     *
     * @param config {@code sys_config: visit.early.day}
     * @return 原实现固定返回 1
     */
    public int dealOutHospInfo(String config) {
        // 原 L325-329:科室维度未处理的在院患者
        VisitInhospRow pmks = new VisitInhospRow();
        pmks.setDeptcheckFlag("0");
        pmks.setInhospstate("1");
        pmks.setFuflag("1");
        List<VisitInhospRow> patMedInhosps = inhospMapper.selectForScan(pmks);
 
        // 原 L331-409 + L411-444:两段宣教改为回调宣教模块
        // ⚠️ 这里不再内联实现 —— 否则会与 ruoyi-publicity 里的实现"发两遍"
        dealInHospPublicityAndMedical(config);
 
        // 原 L445-451:基于规则的出院随访 + 手术随访
        addSubTaskByRule(config, patMedInhosps);
        if (!"xh".equals(active)) {
            // 原 L450:addOperationSubTask(config)
            callOperationVisit(config);
        }
 
        // 原 L454-461:再入院处理
        VisitInhospRow inHospQuery = new VisitInhospRow();
        inHospQuery.setDeptcheckFlag("0");
        inHospQuery.setInhospstate("0");
        List<VisitInhospRow> inHospList = inhospMapper.selectForScan(inHospQuery);
        if (CollectionUtils.isNotEmpty(inHospList)) {
            dealInHospInfo(inHospList);
        }
        return 1;
    }
 
    /**
     * 两段宣教回调(原 L331-409 入院宣教 + L411-444 药品宣教)。
     * <p>
     * 顺序与原实现一致:<b>先入院宣教,再药品宣教</b>。
     * 取不到宣教模块时按"没有宣教要做"处理并打 WARN(fail-open)。
     */
    private void dealInHospPublicityAndMedical(String config) {
        PublicityApi api = publicityApi();
        if (api == null) {
            log.warn("【出入院随访】PublicityApi 不可用(宣教模块未装配),跳过入院宣教与药品宣教。");
            return;
        }
        try {
            // 原 L331-409
            api.dealInHospPublicity(config);
        } catch (Exception e) {
            // 宣教失败不能拖垮随访主流程
            log.warn("【出入院随访】入院宣教回调失败,已忽略:{}", e.toString());
        }
        try {
            // 原 L411-444
            api.dealMedicalPublicity(config);
        } catch (Exception e) {
            log.warn("【出入院随访】药品宣教回调失败,已忽略:{}", e.toString());
        }
    }
 
    /**
     * 手术随访回调(原 L449-451 与 L559-608 的 {@code addOperationSubTask})。
     */
    private void callOperationVisit(String config) {
        OtherVisitApi api = otherVisitApi();
        if (api == null) {
            log.warn("【出入院随访】OtherVisitApi 不可用(手术随访模块未装配),跳过手术随访。");
            return;
        }
        try {
            api.addOperationSubTask(config);
        } catch (Exception e) {
            log.warn("【出入院随访】手术随访回调失败,已忽略:{}", e.toString());
        }
    }
 
    /**
     * 逐患者做规则编排(原 L448 的 {@code addSubTaskByRule(config, patMedInhosps)})。
     * <p>
     * 原 {@code addSubTaskByRule}(L1691) 是个"遍历患者"的外层方法,
     * 本次把"遍历"放在这里,把"单患者规则判定"放进
     * {@link InhospTaskOrchestrator#addSubTaskByRule}。
     * 这样切分之后,两边的方法名相同但职责不同,已在各自注释里写明。
     */
    private void addSubTaskByRule(String config, List<VisitInhospRow> patMedInhosps) {
        if (CollectionUtils.isEmpty(patMedInhosps)) {
            return;
        }
        for (VisitInhospRow row : patMedInhosps) {
            // 原 L1692-1700:获取患者基本信息,为空则写 deptcheck=2 + reason
            VisitArchiveRow archive = archiveMapper.selectByPatid(row.getPatid());
            if (archive == null) {
                com.ruoyi.common.core.service.dto.VisitInhospUpdateDto u =
                        new com.ruoyi.common.core.service.dto.VisitInhospUpdateDto();
                u.setInhospid(row.getInhospid());
                u.setDeptcheckFlag("2");
                u.setLongTaskReason("患者基本信息为空");
                updateInhosp(u);
                continue;
            }
 
            try {
                // 原 L1703-1707:按离院科室/病区查规则
                VisitTaskRuleDto rule = findTaskRule(row);
                // 原 L1709 起:交给编排器按规则执行
                orchestrator.addSubTaskByRule(row, archive, toOrchestratorRule(rule), config);
            } catch (Exception e) {
                // 原 L1839-1845(在编排器里也有,但外层遍历这一层原实现没有 try/catch;
                // 这里加一层是为了"单患者失败不影响其它患者",属健壮性改进,已登记)
                log.error("【基于规则】患者{}外层处理异常:{}", row.getInhospid(), e.getMessage());
                com.ruoyi.common.core.service.dto.VisitInhospUpdateDto u =
                        new com.ruoyi.common.core.service.dto.VisitInhospUpdateDto();
                u.setInhospid(row.getInhospid());
                u.setDeptcheckFlag("2");
                u.setLongTaskReason("基于规则处理异常");
                updateInhosp(u);
            }
        }
    }
 
    /**
     * 再入院处理(原 L454-461 的 {@code dealInHospInfo(patMedInhospList)})。
     */
    private void dealInHospInfo(List<VisitInhospRow> inHospList) {
        for (VisitInhospRow row : inHospList) {
            orchestrator.dealInHospInfo(row);
        }
    }
 
    // ==========================================================================
    // 入口 2:任务组首轮纳入(原 L471-614)
    // ==========================================================================
 
    /**
     * 任务组(appltype=5)首轮纳入 —— <b>本次不搬,仍留在 smartor</b>。
     *
     * <h3>为什么不搬(一个必须说明的范围决定)</h3>
     * 原 {@code dealOutHospInfoBySchedule}(L471-614) 由两段构成:
     * <ul>
     *   <li><b>非手术分支</b>(L518-558):按维度过滤扫患者 → {@code boxedServiceSubtask} →
     *       {@code scheduleSubtaskBuilder.buildSubtasksForPatient}(一次生成 N 条)→ 置 flag;</li>
     *   <li><b>手术分支</b>(L559-608):{@code selectOperationItemListMaxLevel} +
     *       {@code boxedServiceSubtaskForOp} → 同一 builder。</li>
     * </ul>
     * 两段都依赖 {@code ScheduleSubtaskBuilder} —— 它同时服务"HIS 自动纳入"与
     * "前端手动勾选患者"两条路径(见其类注释),**不属于本次范围**。
     * 而手术分支按版图属第二批的 {@code ruoyi-othervisit}。
     * <p>
     * 所以本次把<b>整个方法留在 smartor</b>(行为一行不变),
     * 等第二批做 {@code ruoyi-othervisit} 时连同任务组链路一起收走。
     * <p>
     * 这样做的好处:本轮不需要为 {@code ScheduleSubtaskBuilder} 再造一个端口,
     * 也不会因为"搬一半留一半"而在两处生成任务组的子任务。
     *
     * @param config {@code sys_config: visit.early.day}
     * @return 原实现固定返回 1
     */
    public int dealOutHospInfoBySchedule(String config) {
        log.warn("【出入院随访】dealOutHospInfoBySchedule 本次未搬迁(含手术分支与任务组 builder,"
                + "属第二批 ruoyi-othervisit 范围),仍由 smartor 的实现承接。config={}", config);
        return 1;
    }
 
    // ==========================================================================
    // 辅助
    // ==========================================================================
 
    /** 原 L1703-1707:按离院科室/病区查规则(取不到端口时返回 null = 未配置规则 → 全部生成) */
    private VisitTaskRuleDto findTaskRule(VisitInhospRow row) {
        PublicityDataPort port = dataPort();
        if (port == null) {
            log.warn("【出入院随访】PublicityDataPort 不可用,规则按「未配置」处理(全部生成)。inhospid={}",
                    row.getInhospid());
            return null;
        }
        try {
            return port.findTaskRule(row.getLeaveldeptcode(), row.getLeavehospitaldistrictcode());
        } catch (Exception e) {
            log.warn("【出入院随访】查任务规则失败,按「未配置」处理:{}", e.toString());
            return null;
        }
    }
 
    /** 把端口的规则 DTO 适配成编排器内嵌的规则视图(字段一一对应,含拼错的 {@code wradRule}) */
    private InhospTaskOrchestrator.VisitTaskRule toOrchestratorRule(VisitTaskRuleDto dto) {
        if (dto == null) {
            return null;
        }
        InhospTaskOrchestrator.VisitTaskRule r = new InhospTaskOrchestrator.VisitTaskRule();
        r.setDeptRule(dto.getDeptRule());
        r.setWradRule(dto.getWradRule());
        r.setDiagRule(dto.getDiagRule());
        return r;
    }
 
    private void updateInhosp(com.ruoyi.common.core.service.dto.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 PublicityApi publicityApi() {
        if (publicityApiProvider == null) {
            return null;
        }
        try {
            return publicityApiProvider.stream().findFirst().orElse(null);
        } catch (Exception e) {
            log.warn("【出入院随访】获取 PublicityApi 失败:{}", e.toString());
            return null;
        }
    }
 
    private OtherVisitApi otherVisitApi() {
        if (otherVisitApiProvider == null) {
            return null;
        }
        try {
            return otherVisitApiProvider.stream().findFirst().orElse(null);
        } catch (Exception e) {
            log.warn("【出入院随访】获取 OtherVisitApi 失败:{}", e.toString());
            return null;
        }
    }
}