liusheng
4 天以前 0bbbd746e8c39d91ad7b47a4e36e29ab972dc98c
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
package com.ruoyi.common.core.service;
 
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 java.util.List;
 
/**
 * 随访任务「创建 / 派发」能力的预留接口。
 *
 * <h3>为什么单独拆出这个接口</h3>
 * 出入院随访原来把两件事混在一起:
 * <ol>
 *   <li><b>随访编排</b> —— 按科室/病区/疾病/手术匹配长期任务、判断再入院该停谁(属 {@code ruoyi-inhospvisit});</li>
 *   <li><b>任务创建与落库</b> —— 查任务定义、写 {@code service_subtask}、终止旧任务(本接口)。</li>
 * </ol>
 * 你要求"任务创建将来单独拉一个模块,来调用这些接口",所以这里先把第 2 件事抽成接口:
 * <ul>
 *   <li>接口定义在 <b>ruoyi-common</b> —— 这是唯一能同时满足
 *       「{@code ruoyi-inhospvisit} 不依赖 smartor」+「smartor 依赖 {@code ruoyi-inhospvisit}」
 *       +「两者互相要调用」的位置。若把接口放 inhospvisit、实现放 smartor,
 *       就会形成 {@code smartor -> inhospvisit -> smartor} 的循环依赖,Maven 直接构建失败。</li>
 *   <li>本次(第一批)由 <b>smartor 提供一个过渡实现</b>(逐行搬运现有写库能力,零逻辑改动)。</li>
 *   <li>将来「随访任务创建模块」建好时,<b>只需把该模块的实现注册进来</b>
 *       (实现方从 smartor 换成新模块),{@code ruoyi-inhospvisit} <b>一行都不用改</b>。</li>
 * </ul>
 * 这与满意度的 {@link ISatisfactionCategoryService} 是同一手法:接口在 ruoyi-common,
 * 调用方用 {@code SpringUtils.getBean(...)} 按类型查找 + try/catch 降级。
 *
 * <h3>调用方务必降级</h3>
 * 取不到实现(模块没打进应用 / 未启用 / 容器异常)时,调用方<b>必须按"不生成子任务"处理并打 WARN</b>,
 * 绝不能让定时任务抛异常 —— 出入院随访挂了就是当天全院随访停摆。
 *
 * @see ISatisfactionCategoryService 同一手法的先例
 */
public interface VisitTaskCreateApi {
 
    /**
     * 按 taskid 取「已发布的长期任务」(原 {@code ServiceTaskMapper.selectServiceTaskList}
     * 配 {@code sendState=2})。取不到时<b>返回 {@code null}</b>。
     * <p>
     * 迁移对应:{@code PatMedInhospServiceImpl#writeInSubTask} L686-728(取任务 + 任务不存在时的分支)。
     *
     * @param taskid 任务ID
     * @return 任务快照;不存在或未发布返回 {@code null}
     */
    VisitLongTaskDto findPublishedLongTask(Long taskid);
 
    /**
     * 落库一条随访子任务(原 {@code ServiceSubtaskMapper.insertServiceSubtask})。
     * <p>
     * 迁移对应:{@code PatMedInhospServiceImpl#writeInSubTask} 里的
     * {@code insertServiceSubtask} 调用(L823 / L838 / L854 / L862 / L873)。
     * <p>
     * ⚠️ 实现必须保持原样:入参草稿怎么填就怎么落库,
     * <b>不得在这里"顺手补默认值"或"顺手改状态"</b>,否则线上子任务内容会变。
     * <p>
     * ⚠️ <b>方法名刻意不叫 createSubtask</b>:它不是"新建一个业务对象",
     * 而是"把一份已经组装好的草稿原样落库"。
     * 将来「随访任务创建模块」接管时,<b>方法意图必须仍然是"原样落库"</b>,
     * 不许在这一层加入去重/合并/状态机 —— 那些语义在 {@code ruoyi-inhospvisit} 里。
     *
     * @param draft 子任务草稿
     * @return true = 落库成功(原实现判断 {@code insertServiceSubtask(...) == 1})
     */
    boolean insertSubtask(VisitSubtaskDraft draft);
 
    /**
     * 把一条 {@link #findPendingSubtasks} 查出来的待执行子任务<b>原样回写</b>(整体回写)。
     * <p>
     * 迁移对应:{@code PatMedInhospServiceImpl#writeInSubTask} 里的
     * {@code serviceSubtaskMapper.updateServiceSubtask(serviceSubtask1)}
     * (L812 / L836)与 {@code #dealInHospInfo} 里的 L1349。
     * <p>
     * 原实现的做法是:先查完整对象 → 在内存里改几个字段
     * ({@code sendstate=4L}、{@code result="error"}、{@code remark=...}、{@code updateBy}、{@code updateTime})
     * → 把<b>整个对象</b>传给 {@code updateServiceSubtask}。
     * 因为 {@code ServiceSubtaskMapper.xml} 的 {@code updateServiceSubtask}(L1060-1162)
     * 是"每个字段各自 {@code <if test="xxx != null">}"、{@code where id = #{id}},
     * 所以<b>只有非 null 字段生效</b>。
     * <p>
     * 因此本方法刻意接收<b>同一个 DTO</b>(调用方已经在上面改过要改的字段),
     * 由实现把它映射回实体后整体回写 —— 这样"带了哪些字段过去"这件事与原实现逐字段一致,
     * 不会因为"只传个 id + 几个字段"而丢掉原实现会带过去的其它字段。
     *
     * @param pending 从 {@link #findPendingSubtasks} 拿到并已改好字段的 DTO({@code id} 必须非空)
     * @return true = 更新成功
     */
    boolean updatePendingSubtask(VisitPendingSubtaskDto pending);
 
    /**
     * 查"某患者 + 某 taskid"下所有 {@code sendstate=2}(待执行)的子任务,用于再入院去重判定。
     * <p>
     * 迁移对应:{@code PatMedInhospServiceImpl#writeInSubTask} L760-764 与
     * {@code #dealInHospInfo} L1325-1328 的
     * {@code selectServiceSubtaskList(new ServiceSubtaskEntity(patid, sendstate=2, taskid))}。
     *
     * @param patid  患者档案ID
     * @param taskid 任务ID;{@code dealInHospInfo} 那条链路传 {@code null}(按患者全量查)
     * @return 待执行子任务摘要列表;无数据返回空列表,<b>不返回 null</b>
     */
    List<VisitPendingSubtaskDto> findPendingSubtasks(Long patid, Long taskid);
 
    /**
     * 取任务的「执行方式明细」JSON 串(原 {@code serviceTask.preachformDesc})。
     * <p>
     * 迁移对应:{@code PatMedInhospServiceImpl#addServiceSubtaskPreachform} L1063-1068 ——
     * 它先 {@code selectServiceTaskByTaskid(taskid)} 取到任务,再读 {@code getPreachformDesc()}。
     * <p>
     * <b>为什么不复用 {@link #findPublishedLongTask(Long)}</b>:后者带 {@code sendState=2} 条件
     * (只要"已发布"的任务),而这里原实现用的是 {@code selectServiceTaskByTaskid}
     * (<b>不带 sendState 条件</b>)—— 换成前者会改变"任务查得到/查不到"的判断。
     * 所以刻意分开成两个方法。
     *
     * @param taskid 任务ID
     * @return {@code preachformDesc};任务不存在或该字段为空时返回 {@code null}
     */
    String findPreachformDesc(Long taskid);
 
    /**
     * 判断某患者是否存在「指定状态 + 指定服务类型」的随访子任务。
     * <p>
     * 只返回"有没有",不返回列表 —— 调用方(门诊列表富化)只用它来置
     * {@code serverState="1"},不需要子任务明细。
     *
     * <h3>原实现对应</h3>
     * 门诊 {@code PatMedOuthospServiceImpl#selectPatMedOuthospList} L108-117:
     * <pre>
     * ServiceSubtaskEntity v = new ServiceSubtaskEntity();
     * v.setPatid(patMedOuthosp1.getPatid());
     * v.setSendstates(Arrays.asList(1L, 2L, 3L));
     * v.setServiceType("3");
     * List&lt;ServiceSubtask&gt; list = serviceSubtaskMapper.selectServiceSubtaskBySendstate(v);
     * if (!CollectionUtils.isEmpty(list)) patMedOuthosp1.setServerState("1");
     * </pre>
     *
     * <h3>⚠️ 与 {@link #findPendingSubtasks(Long, Long)} 的区别(别混用)</h3>
     * <table border="1">
     *   <tr><th></th><th>本方法</th><th>findPendingSubtasks</th></tr>
     *   <tr><td>过滤条件</td><td>{@code sendstate IN (...)} + {@code service_type}</td>
     *       <td>固定 {@code sendstate = 2}</td></tr>
     *   <tr><td>是否按 taskid</td><td><b>否</b>(跨该患者所有任务)</td><td>是(可选)</td></tr>
     *   <tr><td>底层 Mapper 方法</td><td>{@code selectServiceSubtaskBySendstate}</td>
     *       <td>{@code selectServiceSubtaskList}</td></tr>
     * </table>
     * 两者查的是<b>不同口径</b>,不能互相替代。
     *
     * @param patid       患者档案ID
     * @param sendstates  子任务状态集合(门诊传 1/2/3);为空则退化为不按状态过滤
     * @param serviceType 服务类型(门诊传 {@code "3"});为 null 则不过滤
     * @return true = 至少存在一条
     */
    boolean existsSubtaskWithStates(Long patid, java.util.List<Long> sendstates, String serviceType);
 
    /**
     * 判断某患者「同一次出入院记录 + 同一个任务」是否<b>已经生成过</b>子任务。
     *
     * <h3>为什么需要它(2026-10 线上问题)</h3>
     * 出院随访扫描是按 {@code pat_med_inhosp.deptcheck_flag = '0'} 取患者的。
     * 一旦标志位因为规则配置/异常没被改成 1 或 2,下一轮扫描会把同一个患者再处理一遍,
     * 而 {@code writeInSubTask} 的"停旧"分支在 {@code endTime1 == endTime}(同一次出院)时
     * 走的是 else —— <b>会再 insert 一条 sendstate=4 的停用行</b>,
     * 于是同一个患者每次跑批都多一条,最终同一患者几十条子任务。
     * <p>
     * 这个方法是那道"最后防线":落库前按
     * {@code patid + inhospid + taskid}(且 {@code del_flag = 0})判重,
     * 已存在就不再加。口径是"同一次出院 + 同一个任务只应有一条子任务",
     * <b>不跨住院记录</b>(再次入院换了 inhospid 仍会新建,这是原有语义)。
     *
     * @param patid    患者档案ID
     * @param inhospid 出入院记录ID;为 {@code null} 时退化为只看 {@code patid + taskid}
     * @param taskid   任务ID
     * @return true = 已存在(不应再 insert)
     */
    boolean existsSubtask(Long patid, Long inhospid, Long taskid);
}