liusheng
6 天以前 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
package com.ruoyi.outpatientvisit.service;
 
import com.ruoyi.common.core.service.ArchiveReadApi;
import com.ruoyi.common.core.service.ServiceTaskMatchApi;
import com.ruoyi.common.core.service.VisitTaskCreateApi;
import com.ruoyi.common.core.service.dto.OuthospSubtaskDraft;
import com.ruoyi.common.core.service.dto.ServiceTaskMatchDto;
import com.ruoyi.common.core.service.dto.VisitArchiveRow;
import com.ruoyi.common.core.service.dto.VisitLongTaskDto;
import com.ruoyi.common.core.service.dto.VisitPendingSubtaskDto;
import com.ruoyi.outpatientvisit.domain.PatMedOuthosp;
import com.ruoyi.outpatientvisit.domain.PatMedOuthospQueryReq;
import com.ruoyi.outpatientvisit.mapper.PatMedOuthospMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
 
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Objects;
 
/**
 * 门急诊随访编排:扫描未处理门诊记录 → 按科室/诊断维度匹配长期任务 → 写子任务。
 *
 * <h3>迁移对应(@ b25c3b51)</h3>
 * <table border="1">
 *   <tr><th>本类方法</th><th>原实现</th></tr>
 *   <tr><td>{@link #dealOutpatientInfo()}</td>
 *       <td>{@code PatMedOuthospServiceImpl#dealOutpatientInfo} L385-427(43 行)</td></tr>
 *   <tr><td>{@link #writeInSubTask}</td>
 *       <td>同文件 L436-521(86 行)</td></tr>
 *   <tr><td>{@link #boxedServiceSubtask}</td>
 *       <td>同文件 L525-581(35 个 setter)</td></tr>
 * </table>
 *
 * <h3>跨模块能力怎么拿(本模块只依赖 ruoyi-common)</h3>
 * <ul>
 *   <li>{@link ServiceTaskMatchApi} —— 按科室({@code service_type="3"})/ 诊断维度匹配长期任务;</li>
 *   <li>{@link VisitTaskCreateApi} —— 查已发布长期任务、查待执行子任务、停旧、新增;</li>
 *   <li>{@link ArchiveReadApi} —— 患者档案只读({@code pat_archive})。</li>
 * </ul>
 *
 * <h3>⚠️ 保留的原实现行为(勿"顺手优化")</h3>
 * <ol>
 *   <li>{@code dealOutpatientInfo} 返回<b>固定 {@code 1}</b>(不是处理条数);</li>
 *   <li>匹配不到时的 flag 与 remark 逐字照抄:
 *       <b>科室</b>维度 → {@code deptcheckFlag="2"} + "通过部门,没有找到门诊随访任务ID";
 *       <b>诊断</b>维度 → {@code diagcheckFlag="2"} + "通过icd10,没有找到门诊随访任务ID";</li>
 *   <li>⚠️ 但 {@link #writeInSubTask} <b>内部</b>的 flag 映射是<b>交叉的</b>:
 *       任务不存在时 {@code type==1 → diagcheckFlag="2"}、{@code type==2 → deptcheckFlag="2"}
 *       —— 与上面第 2 条正好相反。这是原实现的写法(出入院侧也有同类交叉痕迹),原样保留;</li>
 *   <li>{@code check=true} 且存在待执行子任务时,<b>先停旧再新增</b>,
 *       且"停旧"要设 {@code finishtime = new Date()}(出入院侧那行是被注释掉的,门诊侧是活的);</li>
 *   <li>不需要随访({@code notrequiredFlag == "1"})时置 {@code sendstate=4} + {@code result="error"}
 *       并把原因写进 remark,但<b>仍然继续 insert</b>(原实现没有提前 return);</li>
 *   <li>{@code setHospType("1")} 是<b>硬编码</b>,不读 {@code serviceTask.getHospType()}。</li>
 * </ol>
 *
 * <h3>⚠️ 与"原 selectPatMedOuthospList"的一处有意差异(已登记)</h3>
 * 原 {@code selectPatMedOuthospList} 除了调存储过程,还会对每条记录做<b>展示用富化</b>
 * (用档案算年龄、补 sex/telcode/idcardno、查子任务置 {@code serverState})。
 * 本方法的调用方是<b>后台扫描</b>而非列表展示,它随后自己要读 {@code patid/deptcode/icd10code} 等,
 * 富化字段并不参与判断 —— 故本方法直接调 {@code callSpQueryOuthosp},不做那层富化
 * (少一次档案查询 + 少一次子任务查询)。如需与列表接口完全一致,应另走列表服务。
 */
@Slf4j
@Service
public class OuthospVisitTaskService {
 
    /** 科室维度的服务类型(原实现硬编码 "3") */
    private static final String SERVICE_TYPE_DEPT = "3";
 
    /** 待执行状态(原实现硬编码 2L) */
    private static final Long SENDSTATE_PENDING = 2L;
 
    /** 已停止状态(原实现硬编码 6L) */
    private static final Long SENDSTATE_STOPPED = 6L;
 
    /** 不执行状态(原实现硬编码 4L:"手机号为空" / "不需要随访"时用) */
    private static final Long SENDSTATE_SKIP = 4L;
 
    /** 标记"不需要随访"的值(原 {@code patArchive.notrequiredFlag == "1"}) */
    private static final String NOT_REQUIRED_YES = "1";
 
    /** 门诊侧 {@code hospType} 的固定值(原实现硬编码) */
    private static final String HOSP_TYPE_OUTHOSP = "1";
 
    @Autowired
    private PatMedOuthospMapper patMedOuthospMapper;
 
    @Autowired
    private ObjectProvider<ArchiveReadApi> archiveApiProvider;
 
    @Autowired
    private ObjectProvider<ServiceTaskMatchApi> matchApiProvider;
 
    @Autowired
    private ObjectProvider<VisitTaskCreateApi> taskApiProvider;
 
    /**
     * 门诊随访主扫描。
     * <p>
     * 逐行对应原 {@code dealOutpatientInfo}(L385-427):
     * <pre>
     * PatMedOuthosp q = new PatMedOuthosp();
     * q.setDiagcheckFlag("0");
     * q.setFuflag("1");
     * for (每条未处理门诊记录) {
     *     查档案;
     *     科室维度:longtask=1 + deptCode + service_type="3" -> writeInSubTask(taskId, true, ..., 1)
     *     诊断维度:longtask=1 + icd10code                 -> writeInSubTask(taskId, true, ..., 2)
     * }
     * return 1;   // 固定值
     * </pre>
     *
     * @return 固定 {@code 1}(与原实现一致,<b>不是</b>处理条数)
     */
    public Integer dealOutpatientInfo() {
        ServiceTaskMatchApi matchApi = matchApi();
        VisitTaskCreateApi taskApi = taskApi();
        ArchiveReadApi archiveApi = archiveApi();
        if (matchApi == null || taskApi == null) {
            log.warn("【门急诊随访】跨模块契约不可用(ServiceTaskMatchApi={}, VisitTaskCreateApi={}),跳过本次门诊随访。",
                    matchApi != null, taskApi != null);
            return 1;
        }
 
        PatMedOuthosp query = new PatMedOuthosp();
        query.setDiagcheckFlag("0");
        query.setFuflag("1");
        List<PatMedOuthosp> list = patMedOuthospMapper.callSpQueryOuthosp(toQueryReq(query));
        if (CollectionUtils.isEmpty(list)) {
            return 1;
        }
 
        for (PatMedOuthosp row : list) {
            VisitArchiveRow archive = archiveApi == null ? null : archiveApi.findByPatid(row.getPatid());
 
            // ---- 科室维度(原 L396-410)----
            List<ServiceTaskMatchDto> deptTasks =
                    matchApi.findLongTasksByDept(row.getDeptcode(), SERVICE_TYPE_DEPT);
            if (CollectionUtils.isEmpty(deptTasks)) {
                // ⚠️ 原实现此处置的是 deptcheckFlag(不是 diagcheckFlag)
                row.setDeptcheckFlag("2");
                row.setRemark("通过部门,没有找到门诊随访任务ID");
                updateFlag(row);
            } else {
                for (ServiceTaskMatchDto t : deptTasks) {
                    writeInSubTask(taskApi, t.getTaskId(), Boolean.TRUE, row, archive, 1);
                }
            }
 
            // ---- 诊断维度(原 L412-426)----
            List<ServiceTaskMatchDto> diagTasks = matchApi.findLongTasksByDiag(row.getIcd10code());
            if (CollectionUtils.isEmpty(diagTasks)) {
                // 原实现此处置的是 diagcheckFlag
                row.setDiagcheckFlag("2");
                row.setRemark("通过icd10,没有找到门诊随访任务ID");
                updateFlag(row);
            } else {
                for (ServiceTaskMatchDto t : diagTasks) {
                    writeInSubTask(taskApi, t.getTaskId(), Boolean.TRUE, row, archive, 2);
                }
            }
        }
        return 1;
    }
 
    /**
     * 把一条门诊记录写入随访子任务。
     * <p>
     * 逐行对应原 {@code PatMedOuthospServiceImpl#writeInSubTask}(L436-521)。
     *
     * @param type 1 = 科室维度;2 = 诊断维度
     *             ⚠️ 原实现的 flag 映射是<b>交叉的</b>(见类注释第 2 条),原样保留。
     */
    private void writeInSubTask(VisitTaskCreateApi taskApi, Long taskid, Boolean check,
                                PatMedOuthosp row, VisitArchiveRow archive, Integer type) {
        // 原 L438-441:查「已发布的长期任务」(taskid + sendState=2)
        VisitLongTaskDto serviceTask = taskApi.findPublishedLongTask(taskid);
        if (serviceTask == null) {
            log.info("该患者疾病随访长期任务不存在,任务ID为:{}", taskid);
            // ⚠️ 原实现的交叉映射,原样保留
            if (type == 1) row.setDiagcheckFlag("2");
            if (type == 2) row.setDeptcheckFlag("2");
            row.setRemark("该患者门诊随访长期任务不存在,任务ID为:" + taskid);
            updateFlag(row);
            return;
        }
 
        // 原 L452:封装 serviceSubtask
        OuthospSubtaskDraft draft = boxedServiceSubtask(serviceTask, row, archive);
 
        boolean ok = false;
        // 原 L455:先判断一下,是否需要校验
        if (Boolean.TRUE.equals(check)) {
            // 原 L457-464:按 patid + sendstate=2 + taskid 查待执行子任务
            List<VisitPendingSubtaskDto> pendings =
                    taskApi.findPendingSubtasks(archive == null ? null : archive.getId(), taskid);
            log.info("该患者待执行的任务:{}",
                    CollectionUtils.isEmpty(pendings) ? null : pendings.size());
 
            if (CollectionUtils.isNotEmpty(pendings)) {
                for (VisitPendingSubtaskDto pending : pendings) {
                    if (Objects.isNull(pending.getLongSendTime())) {
                        // 不是长期任务,不处理(原 L465-468)
                        continue;
                    }
                    // 将之前的停掉(原 L470-479)
                    pending.setSendstate(SENDSTATE_STOPPED);
                    pending.setRemark("患者已经回院复诊");
                    pending.setResult("success");
                    // ⚠️ 门诊侧设了 finishtime(出入院侧那行是被注释掉的),必须带上
                    pending.setFinishtime(new Date());
                    pending.setUpdateBy(serviceTask.getUpdateBy());
                    taskApi.updatePendingSubtask(pending);
                    // 重新新增子任务(原 L481)
                    ok = insertSubtask(taskApi, draft, serviceTask);
                }
            } else {
                // 原 L483-490:没有待执行任务 -> 直接新增
                ok = insertSubtask(taskApi, draft, serviceTask);
            }
        } else {
            // 原 L491-502:不校验,直接新增
            ok = insertSubtask(taskApi, draft, serviceTask);
        }
 
        if (ok) {
            // 原 L503-509:将 check_flag 改成已处理(按 id 更新)
            PatMedOuthosp upd = new PatMedOuthosp();
            upd.setId(row.getId());
            if (type == 1) upd.setDiagcheckFlag("2");
            if (type == 2) upd.setDeptcheckFlag("2");
            updateFlag(upd);
        } else {
            // 原 L510-520:生成子任务失败
            log.info("生成子任务失败serviceSubtask的taskid为:{},patid为:{}",
                    draft.getTaskid(), draft.getPatid());
            PatMedOuthosp upd = new PatMedOuthosp();
            upd.setGuid(row.getGuid());
            upd.setSerialnum(row.getSerialnum());
            if (type == 1) upd.setDiagcheckFlag("2");
            if (type == 2) upd.setDeptcheckFlag("2");
            upd.setRemark("生成子任务失败");
            updateFlag(upd);
        }
    }
 
    /**
     * 新增子任务,并复刻原实现在"手机号为空"时的处理。
     * <p>
     * 原 L484-490 / L492-499 两处逻辑相同:
     * <pre>
     * if (StringUtils.isEmpty(serviceSubtask.getPhone())) {
     *     serviceSubtask.setRemark("手机号为空");
     *     serviceSubtask.setSendstate(4L);
     *     serviceSubtask.setResult("error");
     * }
     * serviceSubtask.setCreateBy(serviceTask.getCreateBy());
     * serviceSubtask.setCreateTime(new Date());
     * i = serviceSubtaskMapper.insertServiceSubtask(serviceSubtask);
     * </pre>
     */
    private boolean insertSubtask(VisitTaskCreateApi taskApi, OuthospSubtaskDraft draft,
                                  VisitLongTaskDto serviceTask) {
        if (StringUtils.isEmpty(draft.getPhone())) {
            draft.setRemark("手机号为空");
            draft.setSendstate(SENDSTATE_SKIP);
            draft.setResult("error");
        }
        draft.setCreateBy(serviceTask.getCreateBy());
        draft.setCreateTime(new Date());
        return taskApi.insertSubtask(draft);
    }
 
    /**
     * 门诊版子任务组装。
     * <p>
     * 逐字段对应原 {@code boxedServiceSubtask}(L525-581)。
     * 35 个赋值里绝大多数落在 {@code VisitSubtaskDraft} 已有字段上;
     * 只有 {@code admitdate} / {@code fudate} / {@code finishtime} / {@code hospType}
     * 落在 {@link OuthospSubtaskDraft}(见该类注释的对照表)。
     *
     * <h3>⚠️ 刻意保留的原行为</h3>
     * <ul>
     *   <li>{@code setSfzh} 被调用两次(同值),原样保留;</li>
     *   <li>{@code setUpdateTime(new Date())} 被调用三次,原样保留;</li>
     *   <li>{@code serviceTask.getSendDay() == null} 时按 1 天处理,且<b>写回 serviceTask</b>;</li>
     *   <li>{@code addDays} 的基准是门诊的 {@code admitdate}(可能为 null,
     *       原实现同样会 NPE —— 不额外加保护)。</li>
     * </ul>
     */
    private OuthospSubtaskDraft boxedServiceSubtask(VisitLongTaskDto serviceTask,
                                                    PatMedOuthosp row,
                                                    VisitArchiveRow archive) {
        OuthospSubtaskDraft d = new OuthospSubtaskDraft();
 
        // ---- 来自 serviceTask ----
        d.setTaskid(serviceTask.getTaskid());
        d.setOrgid(serviceTask.getOrgid());
        d.setCampusid(serviceTask.getCampusid());
        // 原 L533-534:libtemplateid 在 ServiceTask 里是 String,落库列/草稿里是 Long,
        // 原实现显式 Long.valueOf 转换,且【仅在非空时】设值(为空则保持 null -> 列不写)。原样保留。
        if (StringUtils.isNotEmpty(serviceTask.getLibtemplateid())) {
            d.setLibtemplateid(Long.valueOf(serviceTask.getLibtemplateid()));
        }
        d.setTemplateid(serviceTask.getTemplateid());
        d.setTemplatename(serviceTask.getTemplatename());
        d.setServiceType(serviceTask.getServiceType());
        d.setPreachform(serviceTask.getPreachform());
        d.setUpdateBy(serviceTask.getUpdateBy());
 
        // ---- 来自门诊记录 ----
        d.setDrcode(row.getDrcode());
        d.setDrname(row.getDrname());
        d.setDeptcode(row.getDeptcode());
        d.setInhospid(row.getId());
        d.setHospno(row.getOuthospno());
        d.setDeptname(row.getDeptname());
        d.setLeavediagname(row.getDiagname());
        d.setLeaveicd10code(row.getIcd10code());
        d.setVisitDeptCode(row.getDeptcode());
        d.setVisitDeptName(row.getDeptname());
        // 门诊侧独有(父类没有的字段)
        d.setAdmitdate(row.getAdmitdate());
        d.setFudate(row.getFudate());
 
        // ---- 来自患者档案 ----
        if (archive != null) {
            d.setPatid(archive.getId());
            d.setSendname(archive.getName());
            d.setSfzh(archive.getIdcardno());
            // 原实现:phone 取 telcode;为空则取 relativetelcode
            d.setPhone(StringUtils.isBlank(archive.getTelcode())
                    ? archive.getRelativetelcode() : archive.getTelcode());
            d.setSex(archive.getSex());
            d.setAge(archive.getAge());
        }
 
        // ---- 固定值 / 状态 ----
        d.setSendstate(SENDSTATE_PENDING);
        d.setSfzh(archive == null ? null : archive.getIdcardno());
        // ⚠️ 硬编码 "1"(原实现不读 serviceTask.getHospType()),原样保留
        d.setHospType(HOSP_TYPE_OUTHOSP);
        d.setCreateTime(new Date());
        d.setUpdateBy(serviceTask.getUpdateBy());
        d.setUpdateTime(new Date());
        d.setUpdateTime(new Date());
 
        // ---- 发送时间(原 L562-570)----
        if (serviceTask.getSendDay() == null) {
            serviceTask.setSendDay(1L);
        }
        Date newDate = addDays(row.getAdmitdate(), serviceTask.getSendDay().intValue());
        if (row.getFudate() != null) {
            // 如果门诊表有指定随访时间,那就用指定的
            newDate = row.getFudate();
        }
        d.setLongSendTime(newDate);
        d.setVisitTime(newDate);
 
        // ---- 患者不需要随访(原 L571-579)----
        if (archive != null && StringUtils.isNotEmpty(archive.getNotrequiredFlag())
                && NOT_REQUIRED_YES.equals(archive.getNotrequiredFlag())) {
            d.setRemark(archive.getNotrequiredreason());
            d.setResult("error");
            d.setSendstate(SENDSTATE_SKIP);
        }
        return d;
    }
 
    /**
     * 在 {@code date} 上加 {@code days} 天;{@code days} 为 null 时按 1 天。
     * <p>
     * 逐行对应原 {@code PatMedOuthospServiceImpl#addDays}(L561-568)。
     * ⚠️ 与出入院侧的 {@code minusDay} 不是同一个方法(那个硬编码减 2 天并忽略入参)。
     */
    private Date addDays(Date date, Integer days) {
        if (days == null) {
            days = 1;
        }
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(date);
        calendar.add(Calendar.DAY_OF_MONTH, days);
        return calendar.getTime();
    }
 
    /**
     * 按"传什么更新什么"更新门诊记录的标志位。
     * <p>
     * 原实现调 {@code updatePatMedOuthosp},其 SQL 走 {@code updateInTable}
     * (动态表名 + 全列可选更新),这里保持同样的入口与表名。
     */
    private void updateFlag(PatMedOuthosp row) {
        try {
            patMedOuthospMapper.updateInTable("pat_med_outhosp", row);
        } catch (Exception e) {
            log.warn("【门急诊随访】更新门诊记录标志位失败,row.id={}:{}", row.getId(), e.toString());
        }
    }
 
    /**
     * 门诊实体 → 存储过程入参。
     * <p>
     * 本路径只用到 {@code diagcheckFlag} 与 {@code fuflag} 两个筛选条件,
     * 故按最小必要字段设置;<b>不用 DtoConversionUtils</b>——
     * 它会把全部同名字段带过去,而多带的字段可能改变存储过程的 WHERE 分支。
     */
    private PatMedOuthospQueryReq toQueryReq(PatMedOuthosp q) {
        PatMedOuthospQueryReq req = new PatMedOuthospQueryReq();
        req.setDiagcheckFlag(q.getDiagcheckFlag());
        req.setFuflag(q.getFuflag());
        return req;
    }
 
    private ServiceTaskMatchApi matchApi() {
        return providerGet(matchApiProvider, "ServiceTaskMatchApi");
    }
 
    private ArchiveReadApi archiveApi() {
        return providerGet(archiveApiProvider, "ArchiveReadApi");
    }
 
    private VisitTaskCreateApi taskApi() {
        return providerGet(taskApiProvider, "VisitTaskCreateApi");
    }
 
    private <T> T providerGet(ObjectProvider<T> p, String name) {
        if (p == null) {
            return null;
        }
        try {
            return p.stream().findFirst().orElse(null);
        } catch (Exception e) {
            log.warn("【门急诊随访】获取 {} 失败,按不可用处理:{}", name, e.toString());
            return null;
        }
    }
}