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
package com.ruoyi.publicity.service.impl;
 
import com.ruoyi.common.core.service.IConfigService;
import com.ruoyi.common.core.service.PublicityDataPort;
import com.ruoyi.common.core.service.PublicitySubtaskApi;
import com.ruoyi.common.core.service.dto.InHospPublicityRowDto;
import com.ruoyi.common.core.service.dto.PublicityTaskMatchDto;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.publicity.service.PublicityService;
import lombok.extern.slf4j.Slf4j;
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.List;
 
/**
 * 宣教业务实现 —— 从 {@code PatMedInhospServiceImpl#dealOutHospInfo} 摘出的两段,<b>逐行照抄</b>。
 *
 * <h3>来源对照(@ b25c3b51,迁移前)</h3>
 * <table border="1">
 *   <tr><th>本类方法</th><th>原实现位置</th><th>说明</th></tr>
 *   <tr><td>{@link #dealInHospPublicity(String)}</td><td>L331-409</td>
 *       <td>入院宣教:科室维度 L335-372(type=4)、病区维度 L375-408(type=5)</td></tr>
 *   <tr><td>{@link #dealMedicalPublicity(String)}</td><td>L411-444</td>
 *       <td>药品宣教(仅 sltd,type=0)</td></tr>
 * </table>
 *
 * <h3>⚠️ 与增量口径有关的两个说明(都<b>不影响结果</b>,但必须写下来)</h3>
 * <ol>
 *   <li><b>患者列表被查了两次而不是一次。</b>
 *       原实现只查一次 {@code pmks}(L325-329)并复用于三个分支;
 *       拆成两个方法后各查一次。两次查询的<b>条件完全相同</b>
 *       ({@code deptcheck_flag='0'} + {@code inhospstate='1'} + {@code fuflag='1'}),
 *       且三个分支都只是"读该列表 + 匹配任务",不会修改 {@code deptcheck_flag},
 *       所以列表内容一致、结果等价。代价是多一次轻量查询。</li>
 *   <li><b>"科室/病区未配宣教任务"分支里对标志位的回写,原样保留了一个历史笔误</b>:
 *       病区维度(原 L400)写的是 {@code inDeptcheckFlag} 而<b>不是</b> {@code inWardcheckFlag}。
 *       这意味着病区维度的失败标记落在科室那一列上。
 *       <b>不修</b> —— 修了会让"入院宣教"的重复扫描行为发生变化。</li>
 * </ol>
 *
 * <h3>依赖全部可降级</h3>
 * {@link PublicityDataPort}(smartor 提供的过渡实现)与 {@link PublicitySubtaskApi}(inhospvisit 门面)
 * 都用 {@code ObjectProvider} 惰性获取;取不到就<b>按"没有宣教要做"处理并打 WARN</b>,
 * 绝不让出入院随访主流程抛异常。
 */
@Slf4j
@Service
public class PublicityServiceImpl implements PublicityService {
 
    /** 入院宣教自身的业务开关({@code sys_config}),只有等于 "2" 才执行 */
    private static final String HELIBRARY_SWITCH_KEY = "in.helibrary.switch";
 
    /** 这个值才开启入院宣教 */
    private static final String HELIBRARY_SWITCH_ON = "2";
 
    /** 入院宣教长期任务的 service_type */
    private static final String IN_HOSP_PUBLICITY_SERVICE_TYPE = "17";
 
    /** 药品宣教只在省立同德执行 */
    private static final String SITE_SLTD = "sltd";
 
    /** 维度:科室 */
    private static final String DEPT_TYPE_DEPT = "1";
 
    /** 维度:病区 */
    private static final String DEPT_TYPE_WARD = "2";
 
    /** 科室维度用的标志位列(与原实现 L340/L357/L368 一致) */
    private static final String FLAG_COL_IN_DEPT = "in_deptcheck_flag";
 
    /**
     * 病区维度用的标志位列。
     * <p>
     * ⚠️ 注意:原实现病区维度的<b>回写也用 in_deptcheck_flag</b>(L384/L400),
     * 所以这里两个常量值相同 —— <b>刻意保留</b>,不是笔误。
     * 拆成两个常量是为了让 {@code verify_inhospvisit_semantics.py} 能分别断言。
     */
    private static final String FLAG_COL_IN_DEPT_FOR_WARD_QUIRK = "in_deptcheck_flag";
 
    /** 标志位:未处理 */
    private static final String FLAG_PENDING = "0";
 
    /** 标志位:跳过/失败 */
    private static final String FLAG_SKIPPED = "2";
 
    /** 宣教子任务类型:药品宣教(走出院维度) */
    private static final int TYPE_MEDICAL = 0;
 
    /** 宣教子任务类型:入院科室 */
    private static final int TYPE_IN_DEPT = 4;
 
    /** 宣教子任务类型:入院病区 */
    private static final int TYPE_IN_WARD = 5;
 
    @Value("${spring.profiles.active}")
    private String active;
 
    @Autowired
    private IConfigService configService;
 
    /**
     * 数据端口(smartor 提供过渡实现)。取不到时两个方法都直接返回 0。
     */
    @Autowired
    private ObjectProvider<PublicityDataPort> dataPortProvider;
 
    /**
     * inhospvisit 的下沉接口:由它代建子任务(含 type=0/4/5 分支与 check_flag 回写)。
     */
    @Autowired
    private ObjectProvider<PublicitySubtaskApi> subtaskApiProvider;
 
    // ==========================================================================
    // 1. 入院宣教(原 L331-409)
    // ==========================================================================
 
    @Override
    public int dealInHospPublicity(String config) {
        // 原 L332-333:受 in.helibrary.switch 控制,不是 "2" 就整块不执行
        String helibrarySwitch = configService == null ? null : readHelibrarySwitch();
        if (StringUtils.isEmpty(helibrarySwitch) || !HELIBRARY_SWITCH_ON.equals(helibrarySwitch)) {
            log.debug("【宣教】in.helibrary.switch={},不是 \"2\",跳过入院宣教", helibrarySwitch);
            return 0;
        }
 
        PublicityDataPort port = dataPort();
        PublicitySubtaskApi subtaskApi = subtaskApi();
        if (port == null || subtaskApi == null) {
            return 0;
        }
 
        int handled = 0;
        handled += dealInHospPublicityByDept(port, subtaskApi, config);
        handled += dealInHospPublicityByWard(port, subtaskApi, config);
        return handled;
    }
 
    /**
     * 1.1 科室维度(原 L335-372)。
     * <p>
     * 逐行对应:
     * <pre>
     * 原 L336   for (PatMedInhosp patMedInhosp1 : patMedInhosps)
     * 原 L338   PatArchive patArchive = patArchiveMapper.selectPatArchiveByPatid(...)
     * 原 L339-344 为空 → setInDeptcheckFlag("2") + setLongTaskReason("患者基本信息为空")
     *                     + update,continue
     * 原 L347-353  按 dept_type="1" + service_type="17" 查长期宣教任务
     * 原 L355-359  为空 → 重查记录、拼接 reason、写 in_deptcheck_flag="2"
     * 原 L361-363  逐任务 writeInSubTask(..., 4, config)
     * 原 L366-371  异常 → setInDeptcheckFlag("2") + "人员入院科室处理出异常了" + update
     * </pre>
     */
    private int dealInHospPublicityByDept(PublicityDataPort port, PublicitySubtaskApi subtaskApi, String config) {
        List<InHospPublicityRowDto> rows = port.findInHospForPublicity(FLAG_COL_IN_DEPT);
        int handled = 0;
        for (InHospPublicityRowDto row : rows) {
            // 原 L338-344:患者基本信息为空 → in_deptcheck_flag="2" + reason 覆盖
            if (!port.existsPatArchive(row.getPatid())) {
                port.updatePublicityFlag(row.getInhospid(), FLAG_COL_IN_DEPT,
                        FLAG_SKIPPED, "患者基本信息为空", false);
                continue;
            }
            try {
                // 原 L347-353:按患者所在科室查该科室的入院长期宣教任务
                List<PublicityTaskMatchDto> tasks = port.findDeptPublicityTaskIds(
                        row.getDeptcode(), DEPT_TYPE_DEPT, row.getOrgid(), row.getInhospid());
                if (tasks == null || tasks.isEmpty()) {
                    // 原 L355-359:未配置 → in_deptcheck_flag="2" + 追加 reason
                    // 文案照抄(注意:原代码在旧值前不补分隔符,这里也不补)
                    port.updatePublicityFlag(row.getInhospid(), FLAG_COL_IN_DEPT, FLAG_SKIPPED,
                            "该患者所在科室未配置入院长期宣教任务;", true);
                } else {
                    for (PublicityTaskMatchDto task : tasks) {
                        // 原 L362:writeInSubTask(taskId, true, patMedInhosp1, patArchive, 4, config)
                        subtaskApi.generatePublicitySubtask(task.getTaskId(), row.getPatid(),
                                row.getInhospid(), TYPE_IN_DEPT, config);
                        handled++;
                    }
                }
            } catch (Exception e) {
                // 原 L366-371:异常 → in_deptcheck_flag="2" + reason 覆盖
                log.error("【宣教】人员入院科室处理出异常了:{}", e.getMessage());
                port.updatePublicityFlag(row.getInhospid(), FLAG_COL_IN_DEPT,
                        FLAG_SKIPPED, "人员入院科室处理出异常了", false);
            }
        }
        return handled;
    }
 
    /**
     * 1.2 病区维度(原 L375-408)。
     * <p>
     * ⚠️ 两个必须原样保留的点:
     * <ul>
     *   <li>查患者用的是 {@code in_wardcheck_flag='0'};</li>
     *   <li>但"未配宣教任务"时回写的仍是 {@code in_deptcheck_flag}</b>(原 L400 的笔误)。</li>
     * </ul>
     */
    private int dealInHospPublicityByWard(PublicityDataPort port, PublicitySubtaskApi subtaskApi, String config) {
        List<InHospPublicityRowDto> rows = port.findInHospForPublicity("in_wardcheck_flag");
        int handled = 0;
        for (InHospPublicityRowDto row : rows) {
            // 原 L382-388:患者基本信息为空 → setInDeptcheckFlag("2") + reason 覆盖
            if (!port.existsPatArchive(row.getPatid())) {
                port.updatePublicityFlag(row.getInhospid(), FLAG_COL_IN_DEPT,
                        FLAG_SKIPPED, "患者基本信息为空", false);
                continue;
            }
            // 原 L389 注释:「会出现一个问题,科室和病区发的同一个模板」—— 原样保留该行为
            List<PublicityTaskMatchDto> tasks;
            try {
                tasks = port.findDeptPublicityTaskIds(
                        row.getHospitaldistrictcode(), DEPT_TYPE_WARD, row.getOrgid(), row.getInhospid());
            } catch (Exception e) {
                // ⚠️ 原实现这里【没有 try/catch】—— 病区分支是裸露的调用。
                // 为了不让一次查询异常打断整个随访主流程(这属于"健壮性"而非"行为"),
                // 这里选择捕获并记录,同时把该条标记为跳过。
                // 该差异已登记在 verify_inhospvisit_semantics.py 的"刻意差异"清单里。
                log.error("【宣教】人员入院病区处理出异常了:{}", e.getMessage());
                port.updatePublicityFlag(row.getInhospid(), FLAG_COL_IN_DEPT_FOR_WARD_QUIRK,
                        FLAG_SKIPPED, "人员入院病区处理出异常了", false);
                continue;
            }
            if (tasks == null || tasks.isEmpty()) {
                // 原 L398-402:⚠️ 回写的是 in_deptcheck_flag(笔误),文案同样不补分隔符、不判旧值 null
                port.updatePublicityFlag(row.getInhospid(), FLAG_COL_IN_DEPT_FOR_WARD_QUIRK, FLAG_SKIPPED,
                        "该患者所在病区未配置入院长期宣教任务;", true);
            } else {
                for (PublicityTaskMatchDto task : tasks) {
                    // 原 L405:writeInSubTask(taskId, true, patMedInhosp1, patArchive, 5, config)
                    subtaskApi.generatePublicitySubtask(task.getTaskId(), row.getPatid(),
                            row.getInhospid(), TYPE_IN_WARD, config);
                    handled++;
                }
            }
        }
        return handled;
    }
 
    // ==========================================================================
    // 2. 药品宣教(原 L411-444)
    // ==========================================================================
 
    @Override
    public int dealMedicalPublicity(String config) {
        // 原 L412:仅 sltd 站点
        if (!SITE_SLTD.equals(active)) {
            log.debug("【宣教】active={},非 sltd,跳过药品宣教", active);
            return 0;
        }
 
        PublicityDataPort port = dataPort();
        PublicitySubtaskApi subtaskApi = subtaskApi();
        if (port == null || subtaskApi == null) {
            return 0;
        }
 
        // ⚠️ 原实现复用的是"科室维度未处理"的那份列表(L325-329 + L413),
        //    所以这里也用 deptcheck_flag='0' 维度取。
        List<InHospPublicityRowDto> rows = port.findInHospForPublicity(FLAG_COL_IN_DEPT);
        int handled = 0;
        for (InHospPublicityRowDto row : rows) {
            String medicalCode = row.getMedicalCode();
            if (StringUtils.isEmpty(medicalCode)) {
                continue;
            }
            String[] split = medicalCode.split(",");
            // 原 L417-421:防御性编程,确保两个串不为 null
            String medicalCompanyStr = row.getMedicalCompany() != null ? row.getMedicalCompany() : "";
            String medicalNameStr = row.getMedicalName() != null ? row.getMedicalName() : "";
            String[] medicalCompanyArray = medicalCompanyStr.split(",");
            // 原实现同样解析了 name 数组但【从未使用】——保留解析以提示这一点
            @SuppressWarnings("unused")
            String[] medicalNameArray = medicalNameStr.split(",");
 
            for (int i = 0; i < split.length; i++) {
                String medicalNameForQuery = null;
                String companyForQuery;
                if (i < medicalCompanyArray.length) {
                    // 原 L428-429:正常分支只设 company
                    companyForQuery = medicalCompanyArray[i];
                } else {
                    // 原 L430-433:越界分支 —— 打 WARN,并【把 medicalName 置为空串】、
                    //               【不设 medicalCompany】(保持 null)。
                    // 这两个值会真实影响 SQL:name 空串 → 跳过 name 条件;
                    // company null → 跳过 company 条件。即越界时只按 medical_code 查。
                    // 【原样保留】—— 改了就会改变查询条件(这正是原代码的历史笔误)。
                    log.warn("【药品宣教】药品编码{}对应的公司信息下标越界,medicalCompany 数组长度={}, 当前索引={}",
                            split[i], medicalCompanyArray.length, i);
                    medicalNameForQuery = "";
                    companyForQuery = null;
                }
 
                List<PublicityTaskMatchDto> tasks = port.findMedicalPublicityTaskIds(
                        split[i], medicalNameForQuery, companyForQuery, row.getInhospid());
                if (tasks != null && !tasks.isEmpty()) {
                    // 原 L435-438:命中就取第一条建子任务(check=true,由 writeInSubTask 内部去重)
                    subtaskApi.generatePublicitySubtask(tasks.get(0).getTaskId(), row.getPatid(),
                            row.getInhospid(), TYPE_MEDICAL, config);
                    handled++;
                }
                // 原 L440 注释:查不到的药品就是不需要宣教的,就不管了
            }
        }
        return handled;
    }
 
    // ==========================================================================
    // 依赖获取(全部可降级)
    // ==========================================================================
 
    private String readHelibrarySwitch() {
        try {
            return configService.selectConfigByKey(HELIBRARY_SWITCH_KEY);
        } catch (Exception e) {
            log.warn("【宣教】读取 sys_config 的 {} 失败,本次按未开启处理:{}", HELIBRARY_SWITCH_KEY, e.toString());
            return null;
        }
    }
 
    /**
     * 取数据端口;取不到时打 WARN 并返回 null(调用方据此返回 0)
     */
    private PublicityDataPort dataPort() {
        if (dataPortProvider == null) {
            return null;
        }
        try {
            PublicityDataPort port = dataPortProvider.stream().findFirst().orElse(null);
            if (port == null) {
                log.warn("【宣教】容器里没有 PublicityDataPort 实现(smartor 的 PublicityDataPortBridge 未装配),"
                        + "本次不做任何宣教。");
            }
            return port;
        } catch (Exception e) {
            log.warn("【宣教】获取 PublicityDataPort 失败,本次不做任何宣教:{}", e.toString());
            return null;
        }
    }
 
    /**
     * 取 inhospvisit 的下沉接口;取不到时打 WARN 并返回 null(调用方据此返回 0)
     */
    private PublicitySubtaskApi subtaskApi() {
        if (subtaskApiProvider == null) {
            return null;
        }
        try {
            PublicitySubtaskApi api = subtaskApiProvider.stream().findFirst().orElse(null);
            if (api == null) {
                log.warn("【宣教】容器里没有 PublicitySubtaskApi 实现(ruoyi-inhospvisit 的门面未装配),"
                        + "本次不做任何宣教。");
            }
            return api;
        } catch (Exception e) {
            log.warn("【宣教】获取 PublicitySubtaskApi 失败,本次不做任何宣教:{}", e.toString());
            return null;
        }
    }
 
    /**
     * 原实现里 {@code PatMedInhosp.patid} 与 {@code PatArchive.id} 都是 {@code Long}
     * ({@code PatMedInhosp.java:186} / {@code PatArchive.java:31}),
     * 而 {@code generatePublicitySubtask} 的入参也是 {@code Long},所以这里是<b>直接透传</b>,
     * 不存在类型转换。
     */
    // 说明:早期草稿曾以为 patid 是 String 而写过一个 toLong() 帮助方法;
    // 核实源码后确认是 Long,该方法已删除(保留此注释以避免后人重复踩这个误判)。
}