刘光辉
14 小时以前 34981c30a78e8bbd7791131059a9210f9928b62c
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
package jnpf.limsController;
 
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jnpf.exception.DataException;
import jnpf.limsEntity.LimsHaocaiChukuEntity;
import jnpf.limsEntity.LimsHaocaiChukuShenqingZixiangEntity;
import jnpf.limsEntity.LimsHaocaiJiechuEntity;
import jnpf.limsEntity.LimsHaocaiRukuBatchParam;
import jnpf.limsEntity.LimsHaocaiRukuEntity;
import jnpf.limsEntity.LimsHaocaiRukuSubmitResult;
import jnpf.limsEntity.LimsHaocaiRukuZixiangEntity;
import jnpf.limsEntity.LimsHaocaiYanshouLingquParam;
import jnpf.limsEntity.LimsSxtFlowEventParam;
import jnpf.limsService.LimsHaocaiAlertService;
import jnpf.limsService.LimsHaocaiService;
import jnpf.util.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
 
import java.math.BigDecimal;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
 
@Slf4j
@Tag(name = "lims耗材接口", description = "耗材出入库等接口(当前:入库提交生成条码子表记录)")
@RestController
@RequestMapping("/lims/haocai")
public class LimsHaocaiController {
 
    private static final String BIZ_STATUS_YIXIAOHUI = "yixiaohui";
 
    @Autowired
    private LimsHaocaiService limsHaocaiService;
 
    @Autowired
    private LimsHaocaiAlertService limsHaocaiAlertService;
 
    /**
     * 耗材入库提交:按主表 f_id 取条码号并生成子表条码记录。
     * 不防重,重复提交会以新条码再生成一批。
     */
    @Operation(summary = "耗材入库提交")
    @PostMapping("/ruku/submit")
    public R<LimsHaocaiRukuSubmitResult> rukuSubmit(@RequestBody LimsHaocaiRukuEntity formData) {
        String id = formData.getId();
        if (id == null || id.isEmpty()) {
            return R.error("入库单主键(f_id)不能为空");
        }
 
        LimsHaocaiRukuEntity ruku = limsHaocaiService.getById(id);
        if (ruku == null || Integer.valueOf(1).equals(ruku.getDeleteMark())) {
            return R.error("入库单不存在:" + id);
        }
 
        try {
            LimsHaocaiRukuSubmitResult result = limsHaocaiService.submitRuku(ruku);
            return R.success("入库成功", result);
        } catch (DataException e) {
            log.warn("耗材入库提交失败:rukuId={}, msg={}", id, e.getMessage());
            return R.error(e.getMessage());
        }
    }
 
    /**
     * 耗材入库验收提交:按流程关联验收记录,并回写对应验收申请。
     */
    @Operation(summary = "耗材入库验收提交")
    @PostMapping("/ruku/yanshou/submit")
    public R<String> rukuYanshouSubmit(@RequestBody LimsSxtFlowEventParam param) {
        if (param == null || param.getFlowId() == null || param.getFlowId().trim().isEmpty()) {
            return R.error("流程id(flow_id)不能为空");
        }
        if (param.getTaskId() == null || param.getTaskId().trim().isEmpty()) {
            return R.error("流程任务id(task_id)不能为空");
        }
        try {
            limsHaocaiService.submitRukuYanshou(param.getFlowId(), param.getTaskId());
            return R.success("验收提交成功");
        } catch (DataException e) {
            log.warn("耗材入库验收提交失败:flowId={}, taskId={}, msg={}",
                    param.getFlowId(), param.getTaskId(), e.getMessage());
            return R.error(e.getMessage());
        }
    }
 
    /**
     * 耗材入库验收完成:按验收流程状态更新或重置关联验收申请。
     */
    @Operation(summary = "耗材入库验收完成")
    @PostMapping("/ruku/yanshou/finish")
    public R<String> rukuYanshouFinish(@RequestBody LimsSxtFlowEventParam param) {
        if (param == null || param.getFlowId() == null || param.getFlowId().trim().isEmpty()) {
            return R.error("流程id(flow_id)不能为空");
        }
        if (param.getTaskId() == null || param.getTaskId().trim().isEmpty()) {
            return R.error("流程任务id(task_id)不能为空");
        }
        try {
            limsHaocaiService.finishRukuYanshou(param.getFlowId(), param.getTaskId());
            return R.success("验收完成处理成功");
        } catch (DataException e) {
            log.warn("耗材入库验收完成处理失败:flowId={}, taskId={}, msg={}",
                    param.getFlowId(), param.getTaskId(), e.getMessage());
            return R.error(e.getMessage());
        }
    }
 
    /**
     * 领取耗材验收申请。仅待验收申请可领取,领取后转为验收中并登记当前实验员与领取时间。
     * biz_sign 当前仅接收,不参与业务处理。
     */
    @Operation(summary = "领取耗材验收申请")
    @PostMapping("/yanshou/lingqu")
    public R<String> lingquYanshou(@RequestBody LimsHaocaiYanshouLingquParam param) {
        if (param == null || param.getShenqingId() == null || param.getShenqingId().trim().isEmpty()) {
            return R.error("耗材验收申请主键(shenqing_id)不能为空");
        }
        String shenqingId = param.getShenqingId().trim();
        try {
            limsHaocaiService.lingquYanshouShenqing(shenqingId);
            return R.success("领取成功");
        } catch (DataException e) {
            log.warn("领取耗材验收申请失败:shenqingId={}, msg={}", shenqingId, e.getMessage());
            return R.error(e.getMessage());
        }
    }
 
    /**
     * 耗材借出提交:不写借出单,只按条码更新入库批次状态并写日志。
     */
    @Operation(summary = "耗材借出提交")
    @PostMapping("/jiechu/submit")
    public R<String> jiechuSubmit(@RequestBody LimsHaocaiJiechuEntity formData) {
        try {
            limsHaocaiService.submitJiechu(formData);
            return R.success("借出成功");
        } catch (DataException e) {
            String tiaomaBianma = formData == null ? null : formData.getTiaomaBianma();
            log.warn("耗材借出提交失败:tiaomaBianma={}, msg={}", tiaomaBianma, e.getMessage());
            return R.error(e.getMessage());
        }
    }
 
    /**
     * 耗材归还提交:按条码更新借出记录和入库批次状态,并写日志。
     */
    @Operation(summary = "耗材归还提交")
    @PostMapping("/guihuan/submit")
    public R<String> guihuanSubmit(@RequestBody LimsHaocaiJiechuEntity formData) {
        try {
            limsHaocaiService.submitGuihuan(formData);
            return R.success("归还成功");
        } catch (DataException e) {
            String tiaomaBianma = formData == null ? null : formData.getTiaomaBianma();
            log.warn("耗材归还提交失败:tiaomaBianma={}, msg={}", tiaomaBianma, e.getMessage());
            return R.error(e.getMessage());
        }
    }
 
    /**
     * 耗材出库提交:不写出库单,只按子表条码扣减库存、更新申领子项状态并写日志。
     */
    @Operation(summary = "耗材出库提交")
    @PostMapping("/chuku/submit")
    public R<String> chukuSubmit(@RequestBody LimsHaocaiChukuEntity formData) {
        try {
            limsHaocaiService.submitChuku(formData);
            return R.success("出库成功");
        } catch (DataException e) {
            String shenlingDanhao = formData == null ? null : formData.getShenlingDanhao();
            log.warn("耗材出库提交失败:shenlingDanhao={}, msg={}", shenlingDanhao, e.getMessage());
            return R.error(e.getMessage());
        }
    }
 
    /**
     * 耗材销毁提交:按销毁单号销毁子表条码对应的入库批次。
     */
    @Operation(summary = "耗材销毁提交")
    @PostMapping("/xiaohui/submit")
    public R<String> xiaohuiSubmit(@RequestBody(required = false) Map<String, Object> formData,
                                   @RequestParam(value = "xiaohui_danhao", required = false) String xiaohuiDanhao) {
        String danhao = resolveXiaohuiDanhao(formData, xiaohuiDanhao);
        try {
            limsHaocaiService.submitXiaohui(danhao);
            return R.success("销毁成功");
        } catch (DataException e) {
            log.warn("耗材销毁提交失败:xiaohuiDanhao={}, msg={}", danhao, e.getMessage());
            return R.error(e.getMessage());
        }
    }
 
    /**
     * 耗材销毁申请发起:按流程参数将销毁申请子项条码对应的入库子项标记为待销毁。
     */
    @Operation(summary = "耗材销毁申请发起")
    @PostMapping("/xiaohui/start")
    public R<Map<String, Object>> xiaohuiStart(@RequestBody(required = false) String formData,
                                               @RequestParam(value = "flow_id", required = false) String flowId,
                                               @RequestParam(value = "task_id", required = false) String taskId) {
        String resolvedFlowId = resolveParam(formData, flowId, "flow_id", "flowId");
        String resolvedTaskId = resolveParam(formData, taskId, "task_id", "taskId");
        try {
            int zixiangCount = limsHaocaiService.startXiaohuiShenqing(resolvedFlowId, resolvedTaskId);
            Map<String, Object> data = new java.util.HashMap<>(3);
            data.put("flow_id", resolvedFlowId);
            data.put("task_id", resolvedTaskId);
            data.put("zixiang_count", zixiangCount);
            return R.success("销毁申请发起成功", data);
        } catch (DataException e) {
            log.warn("耗材销毁申请发起失败:flowId={}, taskId={}, msg={}",
                    resolvedFlowId, resolvedTaskId, e.getMessage());
            return R.error(e.getMessage());
        }
    }
 
    /**
     * 批量转待销毁:按入库主表 f_id 批量将剩余量大于 0 的入库子项标记为待销毁。
     */
    @Operation(summary = "耗材入库批量转待销毁")
    @PostMapping("/ruku/batch-to-xiaohui")
    public R<Map<String, Object>> batchToXiaohui(@RequestBody LimsHaocaiRukuBatchParam param) {
        try {
            int zixiangCount = limsHaocaiService.batchMarkRukuZixiangDaixiaohui(
                    param == null ? null : param.getRukuIds());
            Map<String, Object> data = new java.util.HashMap<>(2);
            data.put("ruku_ids", param == null ? null : param.getRukuIds());
            data.put("zixiang_count", zixiangCount);
            return R.success("批量转待销毁成功", data);
        } catch (DataException e) {
            log.warn("耗材入库批量转待销毁失败:rukuIds={}, msg={}",
                    param == null ? null : param.getRukuIds(), e.getMessage());
            return R.error(e.getMessage());
        }
    }
 
    /**
     * 按条码编码查询耗材入库子项。
     */
    @Operation(summary = "按条码编码查询耗材入库子项")
    @GetMapping("/ruku-zixiang/by-tiaoma")
    public R<LimsHaocaiRukuZixiangEntity> getRukuZixiangByTiaomaBianma(
            @RequestParam("tiaomaBianma") String tiaomaBianma,
            @RequestParam(value = "shifou_guihuan", defaultValue = "no") String shifouGuihuan) {
        if (tiaomaBianma == null || tiaomaBianma.trim().isEmpty()) {
            return R.error("条码编码不能为空");
        }
        java.util.List<LimsHaocaiRukuZixiangEntity> list =
                limsHaocaiService.listRukuZixiangByTiaomaBianma(tiaomaBianma);
        if (list == null || list.isEmpty()) {
            return R.error("未找到对应的耗材入库子项:" + tiaomaBianma);
        }
        LimsHaocaiRukuZixiangEntity entity = list.get(0);
        if (isYes(shifouGuihuan)) {
            return R.success(entity);
        }
        if (BIZ_STATUS_YIXIAOHUI.equalsIgnoreCase(trimToEmpty(entity.getBizStatus()))) {
            return R.error("该耗材已销毁,无法使用:" + tiaomaBianma);
        }
        BigDecimal shengyuliang = parseShengyuliang(entity.getShengyuliang());
        if (shengyuliang == null) {
            return R.error("耗材剩余量无效:" + entity.getShengyuliang());
        }
        if (shengyuliang.compareTo(BigDecimal.ZERO) <= 0) {
            return R.error("该耗材剩余量不足,无法使用:" + tiaomaBianma);
        }
        return R.success(entity);
    }
 
    /**
     * 根据申领单号和条码编码查询可出库的耗材申领子项。
     */
    @Operation(summary = "根据申领单号和条码编码查询耗材申领子项")
    @GetMapping("/chuku-shenqing-zixiang/by-shenling-tiaoma")
    public R<LimsHaocaiChukuShenqingZixiangEntity> getChukuShenqingZixiangByShenlingAndTiaoma(
            @RequestParam(value = "shenlingDanhao", required = false) String shenlingDanhao,
            @RequestParam(value = "shenling_danhao", required = false) String shenlingDanhaoAlias,
            @RequestParam("tiaomaBianma") String tiaomaBianma) {
        String danhao = trimToEmpty(shenlingDanhao).isEmpty() ? shenlingDanhaoAlias : shenlingDanhao;
        try {
            return R.success(limsHaocaiService.getChukuShenqingZixiangForChuku(danhao, tiaomaBianma));
        } catch (DataException e) {
            log.warn("按申领单号和条码查询耗材申领子项失败:shenlingDanhao={}, tiaomaBianma={}, msg={}",
                    danhao, tiaomaBianma, e.getMessage());
            return R.error(e.getMessage());
        }
    }
 
    private String trimToEmpty(String value) {
        return value == null ? "" : value.trim();
    }
 
    private boolean isYes(String value) {
        return "yes".equalsIgnoreCase(trimToEmpty(value));
    }
 
    private BigDecimal parseShengyuliang(String shengyuliang) {
        if (shengyuliang == null || shengyuliang.trim().isEmpty()) {
            return null;
        }
        try {
            return new BigDecimal(shengyuliang.trim());
        } catch (NumberFormatException e) {
            return null;
        }
    }
 
    private String resolveXiaohuiDanhao(Map<String, Object> formData, String xiaohuiDanhao) {
        if (xiaohuiDanhao != null && !xiaohuiDanhao.trim().isEmpty()) {
            return xiaohuiDanhao;
        }
        if (formData == null) {
            return null;
        }
        Object value = formData.get("xiaohui_danhao");
        return value == null ? null : String.valueOf(value);
    }
 
    private String resolveParam(String formData, String requestParam, String... bodyKeys) {
        if (requestParam != null && !requestParam.trim().isEmpty()) {
            return requestParam.trim();
        }
        String body = trimToEmpty(formData);
        if (body.isEmpty()) {
            return null;
        }
        for (String bodyKey : bodyKeys) {
            Matcher matcher = Pattern.compile("[\"']?" + Pattern.quote(bodyKey) + "[\"']?\\s*[:=]\\s*[\"']?([^\"',}\\s]+)")
                    .matcher(body);
            if (matcher.find()) {
                return matcher.group(1).trim();
            }
        }
        return null;
    }
 
    /**
     * 手动触发耗材缺货预警扫描(开发 / 运维补发场景)。
     * 正常生产由 cron 每天 01:00 自动触发。
     *
     * @param dry true 仅日志、不实际发消息
     * @return 统计 {hit, sent, recipients}
     */
    @Operation(summary = "手动触发耗材缺货预警扫描")
    @PostMapping("/alert/run-now")
    public R<Map<String, Integer>> alertRunNow(
            @RequestParam(value = "dry", defaultValue = "true") boolean dry) {
        return R.success(limsHaocaiAlertService.scanAndAlert(dry));
    }
}